diff --git a/docs-site/content/docs/cli-reference/ktx-setup.mdx b/docs-site/content/docs/cli-reference/ktx-setup.mdx index 4de40ecb..1bc623a9 100644 --- a/docs-site/content/docs/cli-reference/ktx-setup.mdx +++ b/docs-site/content/docs/cli-reference/ktx-setup.mdx @@ -51,7 +51,8 @@ scripted project creation. They are not shown in `ktx setup --help`. | Flag | Description | |------|-------------| -| `--llm-backend ` | LLM backend: `anthropic` or `vertex` | +| `--llm-backend ` | LLM backend: `anthropic`, `vertex`, or `claude-code` | +| `--llm-backend claude-code` | Use the local Claude Code session for KTX LLM calls | | `--anthropic-api-key-env ` | Environment variable containing the Anthropic API key | | `--anthropic-api-key-file ` | File containing the Anthropic API key | | `--anthropic-model ` | Anthropic model ID to validate and save | @@ -61,7 +62,8 @@ scripted project creation. They are not shown in `ktx setup --help`. Choose only one Anthropic credential source. Anthropic credential flags are only valid with the Anthropic backend; Vertex flags are only valid with the Vertex -backend. +backend. The `claude-code` backend uses local Claude Code authentication instead +of Anthropic API key or Vertex flags. ### Embeddings diff --git a/docs-site/content/docs/cli-reference/ktx-status.mdx b/docs-site/content/docs/cli-reference/ktx-status.mdx index dae22857..c6a1b715 100644 --- a/docs-site/content/docs/cli-reference/ktx-status.mdx +++ b/docs-site/content/docs/cli-reference/ktx-status.mdx @@ -47,6 +47,10 @@ ktx status --project-dir ./analytics `ktx status` prints grouped doctor checks. Agents should use `ktx status --json --no-input` when they need to branch on readiness state. +For `llm.provider.backend: claude-code`, `ktx status` checks that the local +Claude Code session is usable. If auth fails, run the Claude Code CLI login +flow, then rerun `ktx status`. + ```json { "title": "KTX project doctor", diff --git a/docs-site/content/docs/getting-started/quickstart.mdx b/docs-site/content/docs/getting-started/quickstart.mdx index 335aedfa..4922b372 100644 --- a/docs-site/content/docs/getting-started/quickstart.mdx +++ b/docs-site/content/docs/getting-started/quickstart.mdx @@ -59,12 +59,13 @@ completed setup steps and resumes from the remaining work. KTX uses a Claude model for ingest agents that turn schemas, SQL, BI metadata, and documents into semantic-layer sources and wiki context. -Setup supports two LLM provider paths: +Setup supports three LLM provider paths: | Provider | Use when | Credential model | |----------|----------|------------------| | Anthropic API | You have an Anthropic API key | `ANTHROPIC_API_KEY` or a local `file:` secret | | Google Vertex AI for Anthropic Claude | Your organization runs Claude through Google Cloud | Application Default Credentials plus Vertex project and location | +| Claude Code | You want KTX to use your local Claude Code session | Claude Code local authentication | For Anthropic API, setup can read the key from the environment or save a pasted key to `.ktx/secrets/anthropic-api-key`. `ktx.yaml` stores an `env:` or `file:` @@ -74,6 +75,25 @@ For Vertex AI, setup uses Google Application Default Credentials. It can read your active `gcloud` project, list visible projects, or accept explicit `--vertex-project` and `--vertex-location` values. +To use your local Claude Code session instead of an API key, set: + +```yaml +llm: + provider: + backend: claude-code + models: + default: sonnet + triage: haiku + candidateExtraction: sonnet + curator: sonnet + reconcile: sonnet + repair: sonnet +``` + +`claude-code` uses the Claude Code authentication already configured on your +machine. It doesn't use `ANTHROPIC_API_KEY`, Vertex credentials, AI Gateway +tokens, or Bedrock credentials. + Setup checks the selected model before saving. Anthropic API setup fetches live Claude model choices when possible and falls back to bundled defaults if model discovery is unavailable. diff --git a/docs-site/content/docs/guides/building-context.mdx b/docs-site/content/docs/guides/building-context.mdx index c21b7921..1bbfc8ec 100644 --- a/docs-site/content/docs/guides/building-context.mdx +++ b/docs-site/content/docs/guides/building-context.mdx @@ -58,6 +58,10 @@ ktx ingest --all --deep Deep ingest needs LLM and embedding readiness. If those providers are not configured, run `ktx setup` or use `--fast`. +When you use `claude-code`, KTX still controls the tool surface for ingest and +memory capture. Claude Code built-in tools, discovered MCP servers, hooks, +skills, plugins, agents, and slash commands are not exposed to KTX agent loops. + ## Query history PostgreSQL, BigQuery, and Snowflake can add query-history context. This helps diff --git a/docs-site/content/docs/guides/llm-configuration.mdx b/docs-site/content/docs/guides/llm-configuration.mdx new file mode 100644 index 00000000..82eb756a --- /dev/null +++ b/docs-site/content/docs/guides/llm-configuration.mdx @@ -0,0 +1,48 @@ +--- +title: LLM configuration +description: Configure KTX LLM providers, model roles, and prompt caching. +--- + +KTX uses the top-level `llm` block in `ktx.yaml` for text generation, +structured extraction, and ingest or memory agent loops. + +## Backends + +Set `llm.provider.backend` to one of these values: + +- `anthropic`: Use the Anthropic API through `ANTHROPIC_API_KEY` or the + configured `api_key` reference. +- `vertex`: Use Vertex AI Anthropic models through Google Cloud credentials. +- `gateway`: Use AI Gateway-compatible Anthropic model ids. +- `claude-code`: Use your local Claude Code session through the Claude Agent + SDK. KTX removes provider-routing environment variables from Claude Code + child processes, so this backend doesn't silently fall back to + `ANTHROPIC_API_KEY`, Vertex, Gateway, or Bedrock credentials. + +## Claude Code + +Use aliases or full Claude model ids in `llm.models`: + +```yaml +llm: + provider: + backend: claude-code + models: + default: sonnet + triage: haiku + candidateExtraction: sonnet + curator: sonnet + reconcile: sonnet + repair: sonnet +``` + +`claude-code` keeps KTX tool boundaries intact. KTX exposes only the MCP tools +needed for the current KTX agent loop and disables Claude Code built-in tools, +filesystem settings, skills, plugins, agents, hooks, and slash commands. + +## Prompt caching + +`llm.promptCaching` has partial parity on `claude-code`. KTX doesn't pass +Anthropic cache-control markers to the Claude Agent SDK. Status and doctor warn +when you configure prompt-cache TTL, tool, or history fields that the Claude +Agent SDK backend ignores. diff --git a/docs-site/content/docs/guides/meta.json b/docs-site/content/docs/guides/meta.json index 40b44438..2e9703ec 100644 --- a/docs-site/content/docs/guides/meta.json +++ b/docs-site/content/docs/guides/meta.json @@ -1,5 +1,5 @@ { "title": "Guides", "defaultOpen": true, - "pages": ["building-context", "writing-context", "serving-agents"] + "pages": ["building-context", "llm-configuration", "writing-context", "serving-agents"] } diff --git a/docs-site/next.config.mjs b/docs-site/next.config.mjs index a348acd0..3beb3073 100644 --- a/docs-site/next.config.mjs +++ b/docs-site/next.config.mjs @@ -15,6 +15,12 @@ const config = { }, async redirects() { return [ + { + source: "/docs", + destination: "/docs/getting-started/introduction", + permanent: false, + basePath: false, + }, { source: "/:path*", has: [{ type: "host", value: "docs.ktx.sh" }], diff --git a/packages/cli/src/commands/setup-commands.ts b/packages/cli/src/commands/setup-commands.ts index d09f8149..0bdb6c6b 100644 --- a/packages/cli/src/commands/setup-commands.ts +++ b/packages/cli/src/commands/setup-commands.ts @@ -29,7 +29,7 @@ function embeddingBackend(value: string): 'openai' | 'sentence-transformers' { } function llmBackend(value: string): KtxSetupLlmBackend { - if (value === 'anthropic' || value === 'vertex') { + if (value === 'anthropic' || value === 'vertex' || value === 'claude-code') { return value; } throw new InvalidArgumentError(`invalid choice '${value}'`); @@ -361,12 +361,16 @@ export function registerSetupCommands(program: Command, context: KtxCliCommandCo context.setExitCode(1); return; } - if (options.llmBackend === 'vertex' && (options.anthropicApiKeyEnv || options.anthropicApiKeyFile)) { + if ( + options.llmBackend && + options.llmBackend !== 'anthropic' && + (options.anthropicApiKeyEnv || options.anthropicApiKeyFile) + ) { context.io.stderr.write('Anthropic API key flags are only valid with --llm-backend anthropic.\n'); context.setExitCode(1); return; } - if (options.llmBackend === 'anthropic' && (options.vertexProject || options.vertexLocation)) { + if (options.llmBackend && options.llmBackend !== 'vertex' && (options.vertexProject || options.vertexLocation)) { context.io.stderr.write('Vertex AI flags are only valid with --llm-backend vertex.\n'); context.setExitCode(1); return; diff --git a/packages/cli/src/doctor.test.ts b/packages/cli/src/doctor.test.ts index daeb5b96..f08e9d2d 100644 --- a/packages/cli/src/doctor.test.ts +++ b/packages/cli/src/doctor.test.ts @@ -464,6 +464,44 @@ describe('runKtxDoctor', () => { delete process.env.OPENAI_API_KEY; }); + it('reports Claude Code auth failures and ignored prompt-caching fields in project doctor output', async () => { + await writeFile( + join(tempDir, 'ktx.yaml'), + [ + 'llm:', + ' provider:', + ' backend: claude-code', + ' models:', + ' default: sonnet', + ' promptCaching:', + ' enabled: true', + ' systemTtl: 1h', + ' toolsTtl: 1h', + ' historyTtl: 5m', + '', + ].join('\n'), + 'utf-8', + ); + const testIo = makeIo(); + + await expect( + runKtxDoctor( + { command: 'project', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' }, + testIo.io, + { + claudeCodeAuthProbe: async () => ({ + ok: false as const, + message: 'Authenticate Claude Code locally.', + }), + }, + ), + ).resolves.toBe(1); + + expect(testIo.stdout()).toContain('claude-code'); + expect(testIo.stdout()).toContain('Authenticate Claude Code locally'); + expect(testIo.stdout()).toContain('claude-code ignores llm.promptCaching'); + }); + it('includes Postgres query-history readiness in project doctor output', async () => { process.env.ANTHROPIC_API_KEY = 'test-key'; // pragma: allowlist secret process.env.OPENAI_API_KEY = 'test-key'; // pragma: allowlist secret diff --git a/packages/cli/src/ingest.test-utils.ts b/packages/cli/src/ingest.test-utils.ts index 41affbb9..a384f654 100644 --- a/packages/cli/src/ingest.test-utils.ts +++ b/packages/cli/src/ingest.test-utils.ts @@ -265,36 +265,30 @@ export class CliLookerSlWritingAgentRunner extends AgentRunnerService { if (!ledger?.execute) { throw new Error('record_verification_ledger tool was not available to the Looker WorkUnit'); } - await ledger.execute( - { - summary: 'Test fixture verified Looker explore target identifiers before writing SL.', - verifiedIdentifiers: ['prod-warehouse', 'public.orders'], - unverifiedIdentifiers: [], - }, - { toolCallId: 'cli-looker-verification-ledger', messages: [] }, - ); + await ledger.execute({ + summary: 'Test fixture verified Looker explore target identifiers before writing SL.', + verifiedIdentifiers: ['prod-warehouse', 'public.orders'], + unverifiedIdentifiers: [], + }); const slWrite = params.toolSet.sl_write_source; if (!slWrite?.execute) { throw new Error('sl_write_source tool was not available to the Looker WorkUnit'); } - const result = await slWrite.execute( - { - connectionId: 'prod-warehouse', - sourceName: 'looker__ecommerce__orders', - source: { - name: 'looker__ecommerce__orders', - table: 'public.orders', - grain: ['id'], - columns: [ - { name: 'id', type: 'number' }, - { name: 'revenue', type: 'number' }, - ], - measures: [{ name: 'total_revenue', expr: 'sum(revenue)' }], - }, + const result = await slWrite.execute({ + connectionId: 'prod-warehouse', + sourceName: 'looker__ecommerce__orders', + source: { + name: 'looker__ecommerce__orders', + table: 'public.orders', + grain: ['id'], + columns: [ + { name: 'id', type: 'number' }, + { name: 'revenue', type: 'number' }, + ], + measures: [{ name: 'total_revenue', expr: 'sum(revenue)' }], }, - { toolCallId: 'cli-looker-sl-write', messages: [] }, - ); - if (!result.structured.success) { + }); + if (!(result.structured as { success?: boolean } | undefined)?.success) { throw new Error(result.markdown); } } diff --git a/packages/cli/src/ingest.ts b/packages/cli/src/ingest.ts index d602833c..978ee6b6 100644 --- a/packages/cli/src/ingest.ts +++ b/packages/cli/src/ingest.ts @@ -86,11 +86,11 @@ export interface KtxIngestDeps { renderStoredMemoryFlow?: typeof renderMemoryFlowTui; startLiveMemoryFlow?: typeof startLiveMemoryFlowTui; env?: NodeJS.ProcessEnv; - localIngestOptions?: Pick< - RunLocalIngestOptions, - | 'agentRunner' - | 'llmProvider' - | 'memoryModel' + localIngestOptions?: Pick< + RunLocalIngestOptions, + | 'agentRunner' + | 'llmRuntime' + | 'memoryModel' | 'semanticLayerCompute' | 'queryExecutor' | 'logger' diff --git a/packages/cli/src/setup-models.test.ts b/packages/cli/src/setup-models.test.ts index fc41cf1d..5c69e0fa 100644 --- a/packages/cli/src/setup-models.test.ts +++ b/packages/cli/src/setup-models.test.ts @@ -61,7 +61,12 @@ function makePromptAdapter(options: { if (message.includes('LLM provider')) { providerPromptCount += 1; const nextProviderChoice = selectValues[0]; - if (nextProviderChoice === 'anthropic' || nextProviderChoice === 'vertex' || nextProviderChoice === 'back') { + if ( + nextProviderChoice === 'anthropic' || + nextProviderChoice === 'vertex' || + nextProviderChoice === 'claude-code' || + nextProviderChoice === 'back' + ) { return selectValues.shift() ?? nextProviderChoice; } if (options.credentialChoice === 'back' && providerPromptCount > 1) { @@ -180,6 +185,30 @@ describe('setup Anthropic model step', () => { ); }); + it('configures Claude Code backend and validates local auth', async () => { + const io = makeIo(); + const authProbe = vi.fn(async () => ({ ok: true as const })); + + const result = await runKtxSetupAnthropicModelStep( + { + projectDir: tempDir, + inputMode: 'disabled', + llmBackend: 'claude-code', + skipLlm: false, + }, + io.io, + { claudeCodeAuthProbe: authProbe }, + ); + + expect(result.status).toBe('ready'); + const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8')); + expect(config.llm).toMatchObject({ + provider: { backend: 'claude-code' }, + models: { default: 'sonnet' }, + }); + expect(authProbe).toHaveBeenCalledWith(expect.objectContaining({ projectDir: tempDir, model: 'sonnet' })); + }); + it('returns from Anthropic credential Back to provider selection', async () => { const prompts = makePromptAdapter({ selectValues: ['anthropic', 'back', 'back'] }); diff --git a/packages/cli/src/setup-models.ts b/packages/cli/src/setup-models.ts index 784a1d18..87d0d3df 100644 --- a/packages/cli/src/setup-models.ts +++ b/packages/cli/src/setup-models.ts @@ -1,7 +1,7 @@ import { execFile } from 'node:child_process'; import { writeFile } from 'node:fs/promises'; import { promisify } from 'node:util'; -import { resolveLocalKtxLlmConfig } from '@ktx/context'; +import { resolveLocalKtxLlmConfig, runClaudeCodeAuthProbe } from '@ktx/context'; import { resolveKtxConfigReference } from '@ktx/context/core'; import { type KtxProjectConfig, @@ -53,7 +53,7 @@ export interface AnthropicModelChoice { recommended: boolean; } -export type KtxSetupLlmBackend = 'anthropic' | 'vertex'; +export type KtxSetupLlmBackend = 'anthropic' | 'vertex' | 'claude-code'; export interface KtxSetupModelPromptAdapter { select(options: { message: string; options: KtxSetupPromptOption[] }): Promise; @@ -68,6 +68,11 @@ export interface KtxSetupModelDeps { prompts?: KtxSetupModelPromptAdapter; listModels?: (apiKey: string) => Promise; healthCheck?: (config: KtxLlmConfig) => Promise; + claudeCodeAuthProbe?: (input: { + projectDir: string; + model: string; + env?: NodeJS.ProcessEnv; + }) => Promise<{ ok: true } | { ok: false; message: string }>; readGcloudProject?: () => Promise; listGcloudProjects?: () => Promise; spinner?: () => KtxCliSpinner; @@ -252,7 +257,7 @@ export function isKtxSetupLlmConfigReady(config: KtxProjectLlmConfig): boolean { return typeof resolved.vertex?.location === 'string' && resolved.vertex.location.trim().length > 0; } - return resolved.backend === 'anthropic' || resolved.backend === 'gateway'; + return resolved.backend === 'anthropic' || resolved.backend === 'gateway' || resolved.backend === 'claude-code'; } function hasUsableConfiguredLlm(config: KtxProjectConfig): boolean { @@ -263,9 +268,18 @@ function buildProjectLlmConfig( existing: KtxProjectLlmConfig, provider: | { backend: 'anthropic'; credentialRef: string } - | { backend: 'vertex'; vertex: { project?: string; location: string } }, + | { backend: 'vertex'; vertex: { project?: string; location: string } } + | { backend: 'claude-code' }, model: string, ): KtxProjectLlmConfig { + if (provider.backend === 'claude-code') { + return { + provider: { backend: 'claude-code' }, + models: { ...existing.models, default: model }, + promptCaching: existing.promptCaching, + }; + } + if (provider.backend === 'vertex') { return { provider: { @@ -480,16 +494,21 @@ async function chooseBackend( } const choice = await prompts.select({ message: 'Which LLM provider should KTX use?', - options: [ - { value: 'anthropic', label: 'Anthropic API' }, - { value: 'vertex', label: 'Google Vertex AI for Anthropic Claude' }, - { value: 'back', label: 'Back' }, - ], + options: [ + { value: 'anthropic', label: 'Anthropic API' }, + { value: 'vertex', label: 'Google Vertex AI for Anthropic Claude' }, + { value: 'claude-code', label: 'Local Claude Code session' }, + { value: 'back', label: 'Back' }, + ], }); if (choice === 'back') { return { status: 'back' }; } - return { status: 'ready', backend: choice === 'vertex' ? 'vertex' : 'anthropic', prompted: true }; + return { + status: 'ready', + backend: choice === 'vertex' || choice === 'claude-code' ? choice : 'anthropic', + prompted: true, + }; } function resolveProvidedVertexRef( @@ -807,7 +826,8 @@ async function persistLlmConfig( projectDir: string, provider: | { backend: 'anthropic'; credentialRef: string } - | { backend: 'vertex'; vertex: { project?: string; location: string } }, + | { backend: 'vertex'; vertex: { project?: string; location: string } } + | { backend: 'claude-code' }, model: string, ): Promise { const project = await loadKtxProject({ projectDir }); @@ -918,6 +938,19 @@ export async function runKtxSetupAnthropicModelStep( continue; } + if (backendChoice.backend === 'claude-code') { + const model = backendArgs.anthropicModel ?? 'sonnet'; + const probe = deps.claudeCodeAuthProbe ?? runClaudeCodeAuthProbe; + const health = await probe({ projectDir: args.projectDir, model, env: deps.env ?? process.env }); + if (!health.ok) { + io.stderr.write(`${health.message}\n`); + return { status: 'failed', projectDir: args.projectDir }; + } + await persistLlmConfig(args.projectDir, { backend: 'claude-code' }, model); + io.stdout.write(`│ LLM ready: yes (${model})\n`); + return { status: 'ready', projectDir: args.projectDir }; + } + const credential = await chooseCredentialRef(backendArgs, io, deps); if (credential.status === 'back' && backendChoice.prompted) { attemptArgs = buildInteractiveRetryArgs(args); diff --git a/packages/cli/src/status-project.ts b/packages/cli/src/status-project.ts index 2aab1e5c..d527cb28 100644 --- a/packages/cli/src/status-project.ts +++ b/packages/cli/src/status-project.ts @@ -1,4 +1,5 @@ import { basename } from 'node:path'; +import { runClaudeCodeAuthProbe } from '@ktx/context'; import type { KtxConfigIssue, KtxLocalProject, @@ -70,6 +71,12 @@ interface WarningItem { fix?: string; } +type ClaudeCodeAuthProbe = (input: { + projectDir: string; + model: string; + env?: NodeJS.ProcessEnv; +}) => Promise<{ ok: true } | { ok: false; message: string }>; + const PROJECT_READY_COMMANDS = KTX_NEXT_STEP_DIRECT_COMMANDS.map((step) => step.command); function isRecord(value: unknown): value is Record { @@ -127,7 +134,15 @@ function envHint(value: unknown): string | undefined { return undefined; } -function buildLlmStatus(config: KtxProjectLlmConfig, env: NodeJS.ProcessEnv): LlmStatus { +async function buildLlmStatus( + config: KtxProjectLlmConfig, + options: { + projectDir: string; + env: NodeJS.ProcessEnv; + claudeCodeAuthProbe?: ClaudeCodeAuthProbe; + }, +): Promise { + const env = options.env; const backend = config.provider.backend; const model = config.models?.default; if (backend === 'none') { @@ -179,6 +194,26 @@ function buildLlmStatus(config: KtxProjectLlmConfig, env: NodeJS.ProcessEnv): Ll fix: hint ? `Set ${hint}` : 'Set the gateway api_key or rerun `ktx setup`', }; } + if (backend === 'claude-code') { + const modelName = model ?? 'sonnet'; + const probe = options.claudeCodeAuthProbe ?? runClaudeCodeAuthProbe; + const auth = await probe({ projectDir: options.projectDir, model: modelName, env }); + if (auth.ok) { + return { + backend, + model: modelName, + status: 'ok', + detail: 'local Claude Code session authenticated', + }; + } + return { + backend, + model: modelName, + status: 'fail', + detail: auth.message, + fix: 'Authenticate Claude Code locally with the Claude Code CLI, then rerun `ktx status`.', + }; + } return { backend, model, status: 'warn', detail: 'unknown LLM backend' }; } @@ -474,6 +509,13 @@ function buildPipelineStatus(config: KtxProjectConfig): PipelineStatus { }; } +function ignoredClaudeCodePromptCachingFields(config: KtxProjectLlmConfig): string[] { + if (config.provider.backend !== 'claude-code' || !config.promptCaching) { + return []; + } + return Object.keys(config.promptCaching).map((key) => `llm.promptCaching.${key}`); +} + function buildStorageStatus(config: KtxProjectConfig): StorageStatus { return { state: config.storage.state, @@ -561,6 +603,14 @@ function buildWarnings( }); } + const ignored = ignoredClaudeCodePromptCachingFields(config.llm); + if (ignored.length > 0) { + warnings.push({ + message: `claude-code ignores ${ignored.join(', ')} because the Claude Agent SDK does not expose KTX prompt-cache TTL, tool, or history markers.`, + fix: 'Remove those promptCaching fields or use anthropic, vertex, or gateway when those cache knobs are required.', + }); + } + return warnings; } @@ -622,6 +672,7 @@ function buildVerdict( export interface BuildProjectStatusOptions { env?: NodeJS.ProcessEnv; postgresQueryHistoryProbe?: PostgresQueryHistoryProbe; + claudeCodeAuthProbe?: ClaudeCodeAuthProbe; configIssues?: KtxConfigIssue[]; } @@ -642,7 +693,11 @@ export async function buildProjectStatus(project: KtxLocalProject, options: Buil const config = project.config; const configStatus = buildConfigStatus(options.configIssues); - const llm = buildLlmStatus(config.llm, env); + const llm = await buildLlmStatus(config.llm, { + projectDir: project.projectDir, + env, + claudeCodeAuthProbe: options.claudeCodeAuthProbe, + }); const embeddings = buildEmbeddingsStatus(config.ingest.embeddings, env); const storage = buildStorageStatus(config); const connections = Object.entries(config.connections).map(([name, conn]) =>