diff --git a/assets/star-history.svg b/assets/star-history.svg
index 87592b49..8fa7bc08 100644
--- a/assets/star-history.svg
+++ b/assets/star-history.svg
@@ -1 +1 @@
-
+star-history.comMay 17May 24May 31Jun 07 200400600800kaelio/ktxStar HistoryDateGitHub Stars
diff --git a/docs-site/content/docs/cli-reference/ktx-setup.mdx b/docs-site/content/docs/cli-reference/ktx-setup.mdx
index 8ae4469d..f75fdf46 100644
--- a/docs-site/content/docs/cli-reference/ktx-setup.mdx
+++ b/docs-site/content/docs/cli-reference/ktx-setup.mdx
@@ -67,6 +67,13 @@ of Anthropic API key or Vertex flags. After you choose a backend, `ktx setup`
writes that backend's per-role model preset to `ktx.yaml`. To change a model,
edit the matching `llm.models.` value in `ktx.yaml`.
+With `--no-input`, `ktx setup` does not assume a default LLM provider, because
+every backend needs credentials only you can supply. Pass `--llm-backend`
+explicitly. Note that `--target` selects the agent integration, not the LLM
+provider: `ktx setup --target claude-code --no-input` still needs
+`--llm-backend claude-code` to use your Claude subscription for **ktx** LLM
+calls.
+
### Embeddings
| Flag | Description |
@@ -276,6 +283,7 @@ Use `ktx status` for repeatable readiness checks after setup exits.
|-------|-------|----------|
| Setup resumes an unexpected project | `KTX_PROJECT_DIR` or nearest `ktx.yaml` points to another directory | Pass `--project-dir ` explicitly |
| Setup cannot run in CI | Required values are missing and `--no-input` disables prompts | Provide the relevant automation flags or create a fixture `ktx.yaml` |
+| `Missing LLM backend: pass --llm-backend …` | `--no-input` setup ran without an LLM backend; `--target` does not select one | Pass `--llm-backend claude-code`, `codex`, `anthropic`, or `vertex` (with that backend's credential flags) |
| Provider health check fails | Provider key, model id, Vertex project, or Vertex location is invalid | Fix the `env:` or `file:` reference and rerun setup |
| Python runtime is missing | The selected setup needs runtime-backed agent, query-history, Looker, or local embedding features | Accept the interactive prompt, rerun with `--yes`, or run the suggested `ktx admin runtime install` command |
| `--enable-query-history` is rejected | The selected database driver does not support query history | Use Postgres, BigQuery, or Snowflake, or rerun without query-history flags |
diff --git a/package.json b/package.json
index 10643e11..f941241a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "ktx-workspace",
- "version": "0.10.0",
+ "version": "0.11.0",
"description": "Workspace root for ktx packages",
"private": true,
"type": "module",
@@ -69,11 +69,6 @@
"typescript": "^6.0.3",
"yaml": "^2.9.0"
},
- "pnpm": {
- "onlyBuiltDependencies": [
- "better-sqlite3"
- ]
- },
"license": "Apache-2.0",
"repository": {
"type": "git",
diff --git a/packages/cli/package.json b/packages/cli/package.json
index 85695a8a..9bb4c5a1 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@kaelio/ktx",
- "version": "0.10.0",
+ "version": "0.11.0",
"description": "Standalone ktx context layer for data agents",
"author": {
"name": "Kaelio",
diff --git a/packages/cli/src/commands/setup-commands.ts b/packages/cli/src/commands/setup-commands.ts
index 418b27f9..e8510210 100644
--- a/packages/cli/src/commands/setup-commands.ts
+++ b/packages/cli/src/commands/setup-commands.ts
@@ -2,7 +2,7 @@ import { type Command, InvalidArgumentError, Option } from '@commander-js/extra-
import type { KtxCliCommandContext } from '../cli-program.js';
import { resolveCommandProjectDir } from '../cli-program.js';
import type { KtxSetupDatabaseDriver } from '../setup-databases.js';
-import type { KtxSetupLlmBackend } from '../setup-models.js';
+import { isKtxSetupLlmBackend, type KtxSetupLlmBackend } from '../setup-models.js';
import type { KtxSetupSourceType } from '../setup-sources.js';
async function runSetupArgs(
@@ -29,7 +29,7 @@ function embeddingBackend(value: string): 'openai' | 'sentence-transformers' {
}
function llmBackend(value: string): KtxSetupLlmBackend {
- if (value === 'anthropic' || value === 'vertex' || value === 'claude-code' || value === 'codex') {
+ if (isKtxSetupLlmBackend(value)) {
return value;
}
throw new InvalidArgumentError(`invalid choice '${value}'`);
diff --git a/packages/cli/src/setup-models.ts b/packages/cli/src/setup-models.ts
index 911579a9..fbbabbdb 100644
--- a/packages/cli/src/setup-models.ts
+++ b/packages/cli/src/setup-models.ts
@@ -51,7 +51,27 @@ export type KtxSetupModelResult =
| { status: 'missing-input'; projectDir: string }
| { status: 'failed'; projectDir: string };
-export type KtxSetupLlmBackend = 'anthropic' | 'vertex' | 'claude-code' | 'codex';
+// Single source of truth for the LLM backends a user can pick during setup.
+// The CLI arg parser, the interactive prompt, and the missing-backend error all
+// derive from this list, so adding a backend is one edit. Order is the prompt's
+// preference order (subscription backends first).
+const KTX_SETUP_LLM_BACKENDS = ['claude-code', 'codex', 'anthropic', 'vertex'] as const;
+export type KtxSetupLlmBackend = (typeof KTX_SETUP_LLM_BACKENDS)[number];
+
+/** Validates a raw CLI or prompt value against the setup-selectable LLM backends. */
+export function isKtxSetupLlmBackend(value: string): value is KtxSetupLlmBackend {
+ return KTX_SETUP_LLM_BACKENDS.some((backend) => backend === value);
+}
+
+// Display labels for the interactive provider prompt. The Record key type forces
+// every backend to carry a label, so adding one to KTX_SETUP_LLM_BACKENDS fails
+// to compile until its prompt option exists here.
+const KTX_SETUP_LLM_BACKEND_LABELS: Record = {
+ 'claude-code': 'Claude subscription (Pro/Max)',
+ codex: 'Codex subscription',
+ anthropic: 'Anthropic API key',
+ vertex: 'Google Vertex AI for Anthropic Claude',
+};
/** @internal */
export interface KtxSetupModelPromptAdapter {
@@ -135,7 +155,18 @@ const execFileAsync = promisify(execFile);
type ChooseBackendResult =
| { status: 'ready'; backend: KtxSetupLlmBackend; prompted: boolean }
- | { status: 'back' };
+ | { status: 'back' }
+ | { status: 'missing-input' };
+
+// Non-interactive setup cannot pick a provider safely: every backend needs
+// something the user must supply (an API key, gcloud ADC, or a logged-in local
+// CLI), so there is no credential-free default to fall back to. Name the hidden
+// --llm-backend flag and its choices here instead, mirroring how the other
+// automation errors guide users to the flag they need.
+const MISSING_LLM_BACKEND_MESSAGE =
+ `Missing LLM backend: pass --llm-backend with one of ${KTX_SETUP_LLM_BACKENDS.join(', ')}. ` +
+ 'claude-code and codex use local CLI authentication; anthropic also needs --anthropic-api-key-env or ' +
+ '--anthropic-api-key-file, and vertex also needs --vertex-project.';
type VertexConfigChoice =
| {
@@ -446,7 +477,8 @@ async function chooseBackend(
return { status: 'ready', backend: explicit, prompted: false };
}
if (args.inputMode === 'disabled') {
- return { status: 'ready', backend: 'anthropic', prompted: false };
+ io.stderr.write(`${MISSING_LLM_BACKEND_MESSAGE}\n`);
+ return { status: 'missing-input' };
}
const prompts = deps.prompts ?? createPromptAdapter();
@@ -458,21 +490,20 @@ async function chooseBackend(
const choice = await prompts.select({
message: 'Which LLM provider should KTX use?',
options: [
- { value: 'claude-code', label: 'Claude subscription (Pro/Max)' },
- { value: 'codex', label: 'Codex subscription' },
- { value: 'anthropic', label: 'Anthropic API key' },
- { value: 'vertex', label: 'Google Vertex AI for Anthropic Claude' },
+ ...KTX_SETUP_LLM_BACKENDS.map((backend) => ({ value: backend, label: KTX_SETUP_LLM_BACKEND_LABELS[backend] })),
{ value: 'back', label: 'Back' },
],
});
if (choice === 'back') {
return { status: 'back' };
}
- return {
- status: 'ready',
- backend: choice === 'vertex' || choice === 'claude-code' || choice === 'codex' ? choice : 'anthropic',
- prompted: true,
- };
+ if (isKtxSetupLlmBackend(choice)) {
+ return { status: 'ready', backend: choice, prompted: true };
+ }
+ // Options are derived from KTX_SETUP_LLM_BACKENDS, so the only other value is
+ // 'back' (handled above). Treat any unexpected value as a cancel rather than
+ // silently assuming a provider.
+ return { status: 'back' };
}
function resolveProvidedVertexRef(
diff --git a/packages/cli/test/context/index-sync/reindex.nested-git-root.test.ts b/packages/cli/test/context/index-sync/reindex.nested-git-root.test.ts
new file mode 100644
index 00000000..97efa993
--- /dev/null
+++ b/packages/cli/test/context/index-sync/reindex.nested-git-root.test.ts
@@ -0,0 +1,66 @@
+import { execFileSync } from 'node:child_process';
+import { mkdir, mkdtemp, rm, stat } from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+import { afterEach, beforeEach, describe, expect, it } from 'vitest';
+import { reindexLocalIndexes } from '../../../src/context/index-sync/reindex.js';
+import { initKtxProject, type KtxLocalProject } from '../../../src/context/project/project.js';
+
+const AUTHOR = 'Agent';
+const EMAIL = 'agent@example.com';
+
+const WIKI_PAGE = '---\nsummary: Revenue\nusage_mode: auto\n---\n\nPaid orders.\n';
+
+/**
+ * Regression for the "wiki silently unsearchable when the project dir is not the git root"
+ * bug: a ktx project initialized below an existing git working tree. ingest writes wiki
+ * pages through a session worktree and squash-merges into main, so the page must land
+ * inside the project dir (where reindex scans), not at the enclosing git root.
+ */
+describe('reindex with a ktx project nested inside an enclosing git repo', () => {
+ let tempDir: string;
+ let enclosing: string;
+ let projectDir: string;
+
+ beforeEach(async () => {
+ tempDir = await mkdtemp(join(tmpdir(), 'ktx-nested-git-root-'));
+ enclosing = join(tempDir, 'enclosing');
+ await mkdir(enclosing, { recursive: true });
+ execFileSync('git', ['init', '-q'], { cwd: enclosing });
+ projectDir = join(enclosing, 'analytics');
+ });
+
+ afterEach(async () => {
+ await rm(tempDir, { recursive: true, force: true });
+ });
+
+ it('indexes a wiki page written through a session worktree and squash-merged into main', async () => {
+ const project: KtxLocalProject = await initKtxProject({
+ projectDir,
+ authorName: AUTHOR,
+ authorEmail: EMAIL,
+ });
+
+ // Mirror the ingest write path: create a session worktree, write the page on its
+ // branch through the worktree-scoped file store, then squash-merge into main.
+ const mainHead = await project.git.revParseHead();
+ const workdir = join(projectDir, '.ktx/worktrees/session-test');
+ const branch = 'session/test';
+ await project.git.addWorktree(workdir, branch, mainHead);
+ const worktreeStore = project.fileStore.forWorktree(workdir);
+ await worktreeStore.writeFile('wiki/global/revenue.md', WIKI_PAGE, AUTHOR, EMAIL, 'Add revenue page');
+ const merge = await project.git.squashMergeIntoMain(branch, AUTHOR, EMAIL, 'Merge session');
+ expect(merge.ok).toBe(true);
+ await project.git.removeWorktree(workdir);
+ await project.git.deleteBranch(branch, true);
+
+ // The page must land inside the project dir, not the enclosing git root.
+ await expect(stat(join(projectDir, 'wiki/global/revenue.md'))).resolves.toBeDefined();
+ await expect(stat(join(enclosing, 'wiki/global/revenue.md'))).rejects.toMatchObject({ code: 'ENOENT' });
+
+ // ...and reindex must discover and index it.
+ const summary = await reindexLocalIndexes(project, { force: false, embeddingService: null });
+ const global = summary.scopes.find((scope) => scope.label === 'global');
+ expect(global).toMatchObject({ scanned: 1, updated: 1 });
+ });
+});
diff --git a/packages/cli/test/context/project/project.test.ts b/packages/cli/test/context/project/project.test.ts
index 668fa264..1027d174 100644
--- a/packages/cli/test/context/project/project.test.ts
+++ b/packages/cli/test/context/project/project.test.ts
@@ -1,4 +1,5 @@
-import { mkdtemp, readFile, rm, stat } from 'node:fs/promises';
+import { execFileSync } from 'node:child_process';
+import { mkdir, mkdtemp, readFile, realpath, rm, stat } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
@@ -60,6 +61,30 @@ describe('KTX local project runtime', () => {
});
});
+ it('initializes a dedicated git repo at the project dir even when nested inside an enclosing repo', async () => {
+ // A ktx project dir living below an existing git working tree (e.g. an analytics
+ // subfolder of an app repo). ktx must own its own repo rooted at the project dir,
+ // not silently adopt the enclosing repo — otherwise worktree writes resolve against
+ // the enclosing root and land outside the project dir.
+ const enclosing = join(tempDir, 'enclosing');
+ await mkdir(enclosing, { recursive: true });
+ execFileSync('git', ['init', '-q'], { cwd: enclosing });
+
+ const projectDir = join(enclosing, 'analytics');
+ await initKtxProject({ projectDir, authorName: 'Agent', authorEmail: 'agent@example.com' });
+
+ await expect(stat(join(projectDir, '.git'))).resolves.toBeDefined();
+ const toplevel = execFileSync('git', ['rev-parse', '--show-toplevel'], {
+ cwd: projectDir,
+ encoding: 'utf-8',
+ }).trim();
+ expect(await realpath(toplevel)).toBe(await realpath(projectDir));
+
+ // ktx must not write its scaffold commits into the user's enclosing repo.
+ const enclosingTracked = execFileSync('git', ['ls-files'], { cwd: enclosing, encoding: 'utf-8' });
+ expect(enclosingTracked).not.toContain('ktx.yaml');
+ });
+
it('rejects reinitializing an existing project unless force is set', async () => {
const projectDir = join(tempDir, 'warehouse');
await initKtxProject({ projectDir });
diff --git a/packages/cli/test/setup-models.test.ts b/packages/cli/test/setup-models.test.ts
index f09691e0..ba5ce21f 100644
--- a/packages/cli/test/setup-models.test.ts
+++ b/packages/cli/test/setup-models.test.ts
@@ -814,7 +814,7 @@ describe('setup Anthropic model step', () => {
expect(io.stderr()).toContain(`Missing Anthropic API key file: ${missingSecretPath}`);
});
- it('does not recommend skipping when non-interactive setup is missing an Anthropic credential source', async () => {
+ it('fails clearly when non-interactive setup has no LLM backend instead of assuming Anthropic', async () => {
const io = makeIo();
const result = await runKtxSetupAnthropicModelStep(
@@ -823,10 +823,17 @@ describe('setup Anthropic model step', () => {
);
expect(result.status).toBe('missing-input');
- expect(io.stderr()).toContain(
- 'Missing Anthropic API key: pass --anthropic-api-key-env or --anthropic-api-key-file.',
- );
- expect(io.stderr()).not.toContain('--skip-llm');
+ const stderr = io.stderr();
+ expect(stderr).toContain('Missing LLM backend: pass --llm-backend');
+ // Names every backend so the user can choose without reading hidden --help flags.
+ expect(stderr).toContain('claude-code');
+ expect(stderr).toContain('codex');
+ expect(stderr).toContain('anthropic');
+ expect(stderr).toContain('vertex');
+ // Does not mislead with an Anthropic-key error the user never opted into.
+ expect(stderr).not.toContain('Missing Anthropic API key');
+ // Does not nudge users to skip the LLM.
+ expect(stderr).not.toContain('--skip-llm');
});
it('writes pasted keys to .ktx/secrets and never prints the key', async () => {
diff --git a/packages/cli/test/setup.test.ts b/packages/cli/test/setup.test.ts
index 04ca32d1..546136fa 100644
--- a/packages/cli/test/setup.test.ts
+++ b/packages/cli/test/setup.test.ts
@@ -664,6 +664,38 @@ describe('setup status', () => {
expect(testIo.stderr()).toBe('');
});
+ it('fails clearly when a non-interactive run has an agent target but no LLM backend', async () => {
+ const testIo = makeIo();
+
+ // --target selects agent integration, not the LLM provider. A non-interactive
+ // run with no --llm-backend must say so plainly instead of assuming Anthropic.
+ await expect(
+ runKtxSetup(
+ {
+ command: 'run',
+ projectDir: tempDir,
+ mode: 'auto',
+ agents: false,
+ target: 'claude-code',
+ skipAgents: true,
+ inputMode: 'disabled',
+ yes: true,
+ cliVersion: '0.2.0',
+ skipLlm: false,
+ skipEmbeddings: true,
+ skipDatabases: true,
+ skipSources: true,
+ databaseSchemas: [],
+ },
+ testIo.io,
+ ),
+ ).resolves.toBe(1);
+
+ const stderr = testIo.stderr();
+ expect(stderr).toContain('Missing LLM backend: pass --llm-backend');
+ expect(stderr).not.toContain('Missing Anthropic API key');
+ });
+
it('preserves a newly created missing project directory when a later setup step fails', async () => {
const projectDir = join(tempDir, 'missing-project');
const testIo = makeIo();
diff --git a/python/ktx-daemon/pyproject.toml b/python/ktx-daemon/pyproject.toml
index b7e9fc99..3bbb3f60 100644
--- a/python/ktx-daemon/pyproject.toml
+++ b/python/ktx-daemon/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "ktx-daemon"
-version = "0.10.0"
+version = "0.11.0"
description = "Portable compute package for KTX semantic-layer operations"
readme = "README.md"
requires-python = ">=3.13"
diff --git a/python/ktx-sl/pyproject.toml b/python/ktx-sl/pyproject.toml
index dae1f42a..83fcb6f4 100644
--- a/python/ktx-sl/pyproject.toml
+++ b/python/ktx-sl/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "ktx-sl"
-version = "0.10.0"
+version = "0.11.0"
description = "Agent-first semantic layer engine with aggregate locality"
readme = "README.md"
requires-python = ">=3.13"
diff --git a/release-policy.json b/release-policy.json
index 6c9d2d4b..35a2d1d0 100644
--- a/release-policy.json
+++ b/release-policy.json
@@ -19,7 +19,7 @@
},
"publishedPackageSmoke": {
"packageName": "@kaelio/ktx",
- "version": "0.10.0",
+ "version": "0.11.0",
"registry": null
},
"runtimeInstaller": {