From 1a48ff02a9a29516be1e11d26a34908b9c5b76c9 Mon Sep 17 00:00:00 2001 From: Andrey Avtomonov Date: Thu, 21 May 2026 01:46:40 +0200 Subject: [PATCH] fix(cli): wire wiki/knowledge search through resolveProjectEmbeddingProvider --- .../cli/src/commands/knowledge-commands.ts | 2 + packages/cli/src/index.test.ts | 16 ++-- packages/cli/src/knowledge.test.ts | 87 ++++++++++++++++--- packages/cli/src/knowledge.ts | 50 +++++++---- 4 files changed, 120 insertions(+), 35 deletions(-) diff --git a/packages/cli/src/commands/knowledge-commands.ts b/packages/cli/src/commands/knowledge-commands.ts index c0fe4f06..c7b7c8d7 100644 --- a/packages/cli/src/commands/knowledge-commands.ts +++ b/packages/cli/src/commands/knowledge-commands.ts @@ -59,6 +59,7 @@ export function registerWikiCommands(program: Command, context: KtxCliCommandCon userId: options.userId, output: options.output, json: options.json, + cliVersion: context.packageInfo.version, }); return; } @@ -71,6 +72,7 @@ export function registerWikiCommands(program: Command, context: KtxCliCommandCon json: options.json, ...(isDebugEnabled(command) ? { debug: true } : {}), ...(options.limit !== undefined ? { limit: options.limit } : {}), + cliVersion: context.packageInfo.version, }); }, ); diff --git a/packages/cli/src/index.test.ts b/packages/cli/src/index.test.ts index 4f9939e2..3a71ead2 100644 --- a/packages/cli/src/index.test.ts +++ b/packages/cli/src/index.test.ts @@ -148,12 +148,12 @@ describe('runKtxCli', () => { await expect(runKtxCli(['--project-dir', tempDir, 'wiki', '--json'], listIo.io, { knowledge })) .resolves.toBe(0); expect(knowledge).toHaveBeenCalledWith( - { + expect.objectContaining({ command: 'list', projectDir: tempDir, userId: 'local', json: true, - }, + }), listIo.io, ); @@ -162,14 +162,14 @@ describe('runKtxCli', () => { runKtxCli(['--project-dir', tempDir, 'wiki', 'revenue', '--limit', '5'], searchIo.io, { knowledge }), ).resolves.toBe(0); expect(knowledge).toHaveBeenLastCalledWith( - { + expect.objectContaining({ command: 'search', projectDir: tempDir, query: 'revenue', userId: 'local', json: false, limit: 5, - }, + }), searchIo.io, ); @@ -178,14 +178,14 @@ describe('runKtxCli', () => { runKtxCli(['--project-dir', tempDir, '--debug', 'wiki', 'revenue'], debugSearchIo.io, { knowledge }), ).resolves.toBe(0); expect(knowledge).toHaveBeenLastCalledWith( - { + expect.objectContaining({ command: 'search', projectDir: tempDir, query: 'revenue', userId: 'local', json: false, debug: true, - }, + }), debugSearchIo.io, ); @@ -194,13 +194,13 @@ describe('runKtxCli', () => { runKtxCli(['--project-dir', tempDir, 'wiki', 'revenue', 'policy'], multiWordIo.io, { knowledge }), ).resolves.toBe(0); expect(knowledge).toHaveBeenLastCalledWith( - { + expect.objectContaining({ command: 'search', projectDir: tempDir, query: 'revenue policy', userId: 'local', json: false, - }, + }), multiWordIo.io, ); }); diff --git a/packages/cli/src/knowledge.test.ts b/packages/cli/src/knowledge.test.ts index 63b952d0..d1872221 100644 --- a/packages/cli/src/knowledge.test.ts +++ b/packages/cli/src/knowledge.test.ts @@ -5,7 +5,7 @@ import { stripVTControlCharacters } from 'node:util'; import { initKtxProject, loadKtxProject } from '@ktx/context/project'; import type { KtxEmbeddingPort } from '@ktx/context'; import { writeLocalKnowledgePage } from '@ktx/context/wiki'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { runKtxKnowledge } from './knowledge.js'; function makeIo() { @@ -81,12 +81,17 @@ describe('runKtxKnowledge', () => { await seedWikiPage(projectDir); const listIo = makeIo(); - await expect(runKtxKnowledge({ command: 'list', projectDir, userId: 'local' }, listIo.io)).resolves.toBe(0); + await expect( + runKtxKnowledge({ command: 'list', projectDir, userId: 'local', cliVersion: '0.0.0-test' }, listIo.io), + ).resolves.toBe(0); expect(listIo.stdout()).toContain('GLOBAL\tmetrics-revenue\tRevenue'); const searchIo = makeIo(); await expect( - runKtxKnowledge({ command: 'search', projectDir, query: 'paid order', userId: 'local' }, searchIo.io), + runKtxKnowledge( + { command: 'search', projectDir, query: 'paid order', userId: 'local', cliVersion: '0.0.0-test' }, + searchIo.io, + ), ).resolves.toBe(0); expect(searchIo.stdout()).toContain('metrics-revenue'); }); @@ -99,7 +104,14 @@ describe('runKtxKnowledge', () => { const searchIo = makeIo(); await expect( runKtxKnowledge( - { command: 'search', projectDir, query: 'paid order', userId: 'local', output: 'pretty' }, + { + command: 'search', + projectDir, + query: 'paid order', + userId: 'local', + output: 'pretty', + cliVersion: '0.0.0-test', + }, searchIo.io, ), ).resolves.toBe(0); @@ -115,9 +127,12 @@ describe('runKtxKnowledge', () => { await seedWikiPage(projectDir); const listIo = makeIo(); - await expect(runKtxKnowledge({ command: 'list', projectDir, userId: 'local', json: true }, listIo.io)).resolves.toBe( - 0, - ); + await expect( + runKtxKnowledge( + { command: 'list', projectDir, userId: 'local', json: true, cliVersion: '0.0.0-test' }, + listIo.io, + ), + ).resolves.toBe(0); expect(JSON.parse(listIo.stdout())).toMatchObject({ kind: 'list', data: { items: [expect.objectContaining({ key: 'metrics-revenue', summary: 'Revenue' })] }, @@ -127,7 +142,15 @@ describe('runKtxKnowledge', () => { const searchIo = makeIo(); await expect( runKtxKnowledge( - { command: 'search', projectDir, query: 'paid order', userId: 'local', json: true, limit: 5 }, + { + command: 'search', + projectDir, + query: 'paid order', + userId: 'local', + json: true, + limit: 5, + cliVersion: '0.0.0-test', + }, searchIo.io, ), ).resolves.toBe(0); @@ -144,7 +167,10 @@ describe('runKtxKnowledge', () => { const searchIo = makeIo(); await expect( - runKtxKnowledge({ command: 'search', projectDir, query: 'revenue', userId: 'local' }, searchIo.io), + runKtxKnowledge( + { command: 'search', projectDir, query: 'revenue', userId: 'local', cliVersion: '0.0.0-test' }, + searchIo.io, + ), ).resolves.toBe(0); expect(searchIo.stdout()).toBe(''); @@ -166,7 +192,7 @@ describe('runKtxKnowledge', () => { const searchIo = makeIo(); await expect( runKtxKnowledge( - { command: 'search', projectDir, query: 'revenue', userId: 'local' }, + { command: 'search', projectDir, query: 'revenue', userId: 'local', cliVersion: '0.0.0-test' }, searchIo.io, { embeddingService: new FakeEmbeddingPort() }, ), @@ -176,6 +202,37 @@ describe('runKtxKnowledge', () => { expect(searchIo.stderr()).toBe(''); }); + it('routes wiki search through resolveEmbeddingProvider when no embeddingService is injected', async () => { + const projectDir = join(tempDir, 'resolver-project'); + await initKtxProject({ projectDir }); + const search = vi.fn(async () => []); + const searchIo = makeIo(); + await expect( + runKtxKnowledge( + { + command: 'search', + projectDir, + query: 'income', + userId: 'local', + cliVersion: '0.5.0', + }, + searchIo.io, + { + resolveEmbeddingProvider: async () => ({ + kind: 'managed-running', + provider: { id: 'fake' } as never, + baseUrl: 'http://127.0.0.1:51234', + }), + searchLocalKnowledgePages: search, + }, + ), + ).resolves.toBe(0); + expect(search).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ embeddingService: expect.any(Object) }), + ); + }); + it('writes wiki search lane diagnostics to stderr when debug is enabled', async () => { const projectDir = join(tempDir, 'debug-project'); await initKtxProject({ projectDir }); @@ -184,7 +241,15 @@ describe('runKtxKnowledge', () => { const searchIo = makeIo(); await expect( runKtxKnowledge( - { command: 'search', projectDir, query: 'paid order', userId: 'local', json: true, debug: true }, + { + command: 'search', + projectDir, + query: 'paid order', + userId: 'local', + json: true, + debug: true, + cliVersion: '0.0.0-test', + }, searchIo.io, { embeddingService: new FakeEmbeddingPort() }, ), diff --git a/packages/cli/src/knowledge.ts b/packages/cli/src/knowledge.ts index f12d3567..07d68381 100644 --- a/packages/cli/src/knowledge.ts +++ b/packages/cli/src/knowledge.ts @@ -1,20 +1,20 @@ -import { - createLocalKtxEmbeddingProviderFromConfig, - KtxIngestEmbeddingPortAdapter, - type KtxEmbeddingPort, -} from '@ktx/context'; +import { KtxIngestEmbeddingPortAdapter, type KtxEmbeddingPort } from '@ktx/context'; import { loadKtxProject } from '@ktx/context/project'; import { type LocalKnowledgeSearchResult, type LocalKnowledgeSummary, listLocalKnowledgePages, - searchLocalKnowledgePages, + searchLocalKnowledgePages as defaultSearchLocalKnowledgePages, } from '@ktx/context/wiki'; +import { + resolveProjectEmbeddingProvider, + type EmbeddingProviderResolution, +} from './embedding-resolution.js'; import { resolveOutputMode } from './io/mode.js'; import { createRankBadgeFormatter, printList, type PrintListColumn } from './io/print-list.js'; export type KtxKnowledgeArgs = - | { command: 'list'; projectDir: string; userId: string; output?: string; json?: boolean } + | { command: 'list'; projectDir: string; userId: string; output?: string; json?: boolean; cliVersion: string } | { command: 'search'; projectDir: string; @@ -24,6 +24,7 @@ export type KtxKnowledgeArgs = json?: boolean; limit?: number; debug?: boolean; + cliVersion: string; }; type KtxKnowledgeIo = import('./cli-runtime.js').KtxCliIo; @@ -54,20 +55,36 @@ function wikiSearchColumns( interface KtxKnowledgeDeps { embeddingService?: KtxEmbeddingPort | null; - createEmbeddingProvider?: typeof createLocalKtxEmbeddingProviderFromConfig; + resolveEmbeddingProvider?: typeof resolveProjectEmbeddingProvider; + searchLocalKnowledgePages?: typeof defaultSearchLocalKnowledgePages; } -function wikiSearchEmbeddingService( +function resolutionToEmbeddingPort(resolution: EmbeddingProviderResolution): KtxEmbeddingPort | null { + if ( + resolution.kind === 'configured' || + resolution.kind === 'managed-running' || + resolution.kind === 'managed-started' + ) { + return new KtxIngestEmbeddingPortAdapter(resolution.provider); + } + return null; +} + +async function wikiSearchEmbeddingService( project: Awaited>, deps: KtxKnowledgeDeps, -): KtxEmbeddingPort | null { + args: { cliVersion: string }, + io: KtxKnowledgeIo, +): Promise { if ('embeddingService' in deps) { return deps.embeddingService ?? null; } - const provider = (deps.createEmbeddingProvider ?? createLocalKtxEmbeddingProviderFromConfig)( - project.config.ingest.embeddings, - ); - return provider ? new KtxIngestEmbeddingPortAdapter(provider) : null; + const resolution = await (deps.resolveEmbeddingProvider ?? resolveProjectEmbeddingProvider)(project, { + mode: 'use-if-running', + cliVersion: args.cliVersion, + io, + }); + return resolutionToEmbeddingPort(resolution); } function writeWikiSearchDebug( @@ -114,8 +131,9 @@ export async function runKtxKnowledge( return 0; } if (args.command === 'search') { - const embeddingService = wikiSearchEmbeddingService(project, deps); - const results = await searchLocalKnowledgePages(project, { + const embeddingService = await wikiSearchEmbeddingService(project, deps, { cliVersion: args.cliVersion }, io); + const search = deps.searchLocalKnowledgePages ?? defaultSearchLocalKnowledgePages; + const results = await search(project, { query: args.query, userId: args.userId, embeddingService,