mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-13 08:15:14 +02:00
Merge origin/main into ingest-ignores-auto-commit-config
Resolve git.service.ts conflict: both branches independently fixed the "ktx adopts an enclosing git repo when the project dir is nested" bug. main (#282) used checkIsRepo(IS_REPO_ROOT) to init a dedicated repo unless the project dir is already a repo root. This branch's repo-ownership model (classifyKtxRepoOwnership) reads only <dir>/.git and never walks up, so it already handles the nested case (no local .git -> unowned -> dedicated init) and additionally rejects foreign repos via a ktx.managed marker. Kept the ownership model and dropped the now-unused CheckRepoActions import; #282's regression tests (reindex.nested-git-root, project nested-repo) pass against it.
This commit is contained in:
commit
dd68aadb35
13 changed files with 195 additions and 31 deletions
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
|
|
@ -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.<role>` 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 <path>` 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 |
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}'`);
|
||||
|
|
|
|||
|
|
@ -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<KtxSetupLlmBackend, string> = {
|
||||
'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(
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
},
|
||||
"publishedPackageSmoke": {
|
||||
"packageName": "@kaelio/ktx",
|
||||
"version": "0.10.0",
|
||||
"version": "0.11.0",
|
||||
"registry": null
|
||||
},
|
||||
"runtimeInstaller": {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue