@kaelio/ktx 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/assets/python/{kaelio_ktx-0.3.0-py3-none-any.whl → kaelio_ktx-0.4.1-py3-none-any.whl} +0 -0
  2. package/assets/python/manifest.json +4 -4
  3. package/dist/admin-reindex.js +9 -14
  4. package/dist/admin-reindex.test.js +4 -1
  5. package/dist/cli-project.d.ts +4 -10
  6. package/dist/cli-project.js +5 -49
  7. package/dist/cli-project.test.js +4 -131
  8. package/dist/commands/knowledge-commands.js +2 -0
  9. package/dist/commands/sl-commands.js +2 -0
  10. package/dist/embedding-resolution.d.ts +36 -0
  11. package/dist/embedding-resolution.js +63 -0
  12. package/dist/embedding-resolution.test.d.ts +1 -0
  13. package/dist/embedding-resolution.test.js +132 -0
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.js +1 -1
  16. package/dist/index.test.js +36 -33
  17. package/dist/ingest.js +12 -9
  18. package/dist/knowledge.d.ts +7 -2
  19. package/dist/knowledge.js +21 -7
  20. package/dist/knowledge.test.js +53 -9
  21. package/dist/managed-local-embeddings.d.ts +7 -6
  22. package/dist/managed-local-embeddings.js +19 -13
  23. package/dist/managed-local-embeddings.test.js +87 -18
  24. package/dist/mcp-server-factory.js +11 -0
  25. package/dist/public-ingest.js +2 -8
  26. package/dist/runtime-requirements.js +1 -2
  27. package/dist/runtime-requirements.test.js +1 -2
  28. package/dist/scan.js +8 -4
  29. package/dist/setup-embeddings.js +6 -2
  30. package/dist/setup-embeddings.test.js +2 -5
  31. package/dist/setup-runtime.test.js +1 -3
  32. package/dist/sl.d.ts +6 -4
  33. package/dist/sl.js +23 -9
  34. package/dist/sl.test.js +77 -6
  35. package/node_modules/@ktx/context/dist/ingest/local-bundle-runtime.d.ts +2 -0
  36. package/node_modules/@ktx/context/dist/ingest/local-bundle-runtime.js +2 -2
  37. package/node_modules/@ktx/context/dist/ingest/local-ingest.d.ts +1 -0
  38. package/node_modules/@ktx/context/dist/ingest/local-ingest.js +2 -0
  39. package/node_modules/@ktx/context/dist/llm/index.d.ts +1 -1
  40. package/node_modules/@ktx/context/dist/llm/index.js +1 -1
  41. package/node_modules/@ktx/context/dist/llm/local-config.d.ts +0 -1
  42. package/node_modules/@ktx/context/dist/llm/local-config.js +1 -2
  43. package/node_modules/@ktx/context/dist/llm/local-config.test.js +3 -3
  44. package/node_modules/@ktx/context/dist/mcp/local-project-ports.d.ts +2 -2
  45. package/node_modules/@ktx/context/dist/mcp/local-project-ports.js +2 -5
  46. package/node_modules/@ktx/context/dist/mcp/local-project-ports.test.js +14 -10
  47. package/node_modules/@ktx/context/dist/package-exports.test.js +0 -1
  48. package/node_modules/@ktx/context/dist/project/config.js +1 -1
  49. package/node_modules/@ktx/context/dist/scan/local-enrichment.test.js +4 -5
  50. package/node_modules/@ktx/context/dist/scan/local-scan.d.ts +3 -1
  51. package/node_modules/@ktx/context/dist/scan/local-scan.js +3 -2
  52. package/package.json +1 -1
@@ -6,6 +6,8 @@ import { initKtxProject } from '@ktx/context/project';
6
6
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
7
7
  import { getKtxCliPackageInfo, packageInfoFromJson, rendererUnavailableVizFallback, renderMemoryFlowTui, resolveVizFallback, runKtxCli, sanitizeMemoryFlowTuiError, startLiveMemoryFlowTui, warnVizFallbackOnce, } from './index.js';
8
8
  const require = createRequire(import.meta.url);
9
+ const cliPackageJson = require('@ktx/cli/package.json');
10
+ const cliVersion = cliPackageJson.version;
9
11
  function makeIo(options = {}) {
10
12
  let stdout = '';
11
13
  let stderr = '';
@@ -31,7 +33,7 @@ describe('getKtxCliPackageInfo', () => {
31
33
  it('identifies the CLI package and its context dependency', () => {
32
34
  expect(getKtxCliPackageInfo()).toEqual({
33
35
  name: '@ktx/cli',
34
- version: '0.0.0-private',
36
+ version: cliVersion,
35
37
  contextPackageName: '@ktx/context',
36
38
  });
37
39
  });
@@ -39,8 +41,9 @@ describe('getKtxCliPackageInfo', () => {
39
41
  const packageJson = require('@ktx/cli/package.json');
40
42
  expect(packageJson).toMatchObject({
41
43
  name: '@ktx/cli',
42
- version: '0.0.0-private',
44
+ version: cliVersion,
43
45
  });
46
+ expect(cliVersion).toMatch(/^\d+\.\d+\.\d+/);
44
47
  });
45
48
  it('normalizes public package metadata from package.json contents', () => {
46
49
  expect(packageInfoFromJson({
@@ -86,7 +89,7 @@ describe('runKtxCli', () => {
86
89
  it('prints version information', async () => {
87
90
  const testIo = makeIo();
88
91
  await expect(runKtxCli(['--version'], testIo.io)).resolves.toBe(0);
89
- expect(testIo.stdout()).toBe('@ktx/cli 0.0.0-private\n');
92
+ expect(testIo.stdout()).toBe(`@ktx/cli ${cliVersion}\n`);
90
93
  expect(testIo.stderr()).toBe('');
91
94
  });
92
95
  it('prints the public command surface in root help', async () => {
@@ -114,41 +117,41 @@ describe('runKtxCli', () => {
114
117
  const listIo = makeIo();
115
118
  await expect(runKtxCli(['--project-dir', tempDir, 'wiki', '--json'], listIo.io, { knowledge }))
116
119
  .resolves.toBe(0);
117
- expect(knowledge).toHaveBeenCalledWith({
120
+ expect(knowledge).toHaveBeenCalledWith(expect.objectContaining({
118
121
  command: 'list',
119
122
  projectDir: tempDir,
120
123
  userId: 'local',
121
124
  json: true,
122
- }, listIo.io);
125
+ }), listIo.io);
123
126
  const searchIo = makeIo();
124
127
  await expect(runKtxCli(['--project-dir', tempDir, 'wiki', 'revenue', '--limit', '5'], searchIo.io, { knowledge })).resolves.toBe(0);
125
- expect(knowledge).toHaveBeenLastCalledWith({
128
+ expect(knowledge).toHaveBeenLastCalledWith(expect.objectContaining({
126
129
  command: 'search',
127
130
  projectDir: tempDir,
128
131
  query: 'revenue',
129
132
  userId: 'local',
130
133
  json: false,
131
134
  limit: 5,
132
- }, searchIo.io);
135
+ }), searchIo.io);
133
136
  const debugSearchIo = makeIo();
134
137
  await expect(runKtxCli(['--project-dir', tempDir, '--debug', 'wiki', 'revenue'], debugSearchIo.io, { knowledge })).resolves.toBe(0);
135
- expect(knowledge).toHaveBeenLastCalledWith({
138
+ expect(knowledge).toHaveBeenLastCalledWith(expect.objectContaining({
136
139
  command: 'search',
137
140
  projectDir: tempDir,
138
141
  query: 'revenue',
139
142
  userId: 'local',
140
143
  json: false,
141
144
  debug: true,
142
- }, debugSearchIo.io);
145
+ }), debugSearchIo.io);
143
146
  const multiWordIo = makeIo();
144
147
  await expect(runKtxCli(['--project-dir', tempDir, 'wiki', 'revenue', 'policy'], multiWordIo.io, { knowledge })).resolves.toBe(0);
145
- expect(knowledge).toHaveBeenLastCalledWith({
148
+ expect(knowledge).toHaveBeenLastCalledWith(expect.objectContaining({
146
149
  command: 'search',
147
150
  projectDir: tempDir,
148
151
  query: 'revenue policy',
149
152
  userId: 'local',
150
153
  json: false,
151
- }, multiWordIo.io);
154
+ }), multiWordIo.io);
152
155
  });
153
156
  it('rejects unknown write-style flags on the flattened wiki and sl commands', async () => {
154
157
  const knowledge = vi.fn(async () => 0);
@@ -166,7 +169,7 @@ describe('runKtxCli', () => {
166
169
  const sl = vi.fn(async () => 0);
167
170
  const searchIo = makeIo();
168
171
  await expect(runKtxCli(['--project-dir', tempDir, 'sl', 'revenue', '--connection-id', 'warehouse', '--limit', '5', '--json'], searchIo.io, { sl })).resolves.toBe(0);
169
- expect(sl).toHaveBeenCalledWith({
172
+ expect(sl).toHaveBeenCalledWith(expect.objectContaining({
170
173
  command: 'search',
171
174
  projectDir: tempDir,
172
175
  connectionId: 'warehouse',
@@ -174,16 +177,16 @@ describe('runKtxCli', () => {
174
177
  limit: 5,
175
178
  json: true,
176
179
  output: undefined,
177
- }, searchIo.io);
180
+ }), searchIo.io);
178
181
  const bareIo = makeIo();
179
182
  await expect(runKtxCli(['--project-dir', tempDir, 'sl', '--connection-id', 'warehouse', '--json'], bareIo.io, { sl })).resolves.toBe(0);
180
- expect(sl).toHaveBeenLastCalledWith({
183
+ expect(sl).toHaveBeenLastCalledWith(expect.objectContaining({
181
184
  command: 'list',
182
185
  projectDir: tempDir,
183
186
  connectionId: 'warehouse',
184
187
  json: true,
185
188
  output: undefined,
186
- }, bareIo.io);
189
+ }), bareIo.io);
187
190
  const unknownIo = makeIo();
188
191
  await expect(runKtxCli(['--project-dir', tempDir, 'sl', '--query', 'revenue'], unknownIo.io, { sl })).resolves.toBe(1);
189
192
  expect(unknownIo.stderr()).toContain("unknown option '--query'");
@@ -206,32 +209,32 @@ describe('runKtxCli', () => {
206
209
  await expect(runKtxCli(['admin', 'runtime', 'prune', '--dry-run'], pruneIo.io, { runtime })).resolves.toBe(1);
207
210
  expect(runtime).toHaveBeenNthCalledWith(1, {
208
211
  command: 'install',
209
- cliVersion: '0.0.0-private',
212
+ cliVersion,
210
213
  feature: 'local-embeddings',
211
214
  force: true,
212
215
  }, installIo.io);
213
216
  expect(runtime).toHaveBeenNthCalledWith(2, {
214
217
  command: 'start',
215
- cliVersion: '0.0.0-private',
218
+ cliVersion,
216
219
  projectDir: expect.any(String),
217
220
  feature: 'local-embeddings',
218
221
  force: true,
219
222
  }, startIo.io);
220
223
  expect(runtime).toHaveBeenNthCalledWith(3, {
221
224
  command: 'stop',
222
- cliVersion: '0.0.0-private',
225
+ cliVersion,
223
226
  projectDir: expect.any(String),
224
227
  all: false,
225
228
  }, stopIo.io);
226
229
  expect(runtime).toHaveBeenNthCalledWith(4, {
227
230
  command: 'stop',
228
- cliVersion: '0.0.0-private',
231
+ cliVersion,
229
232
  projectDir: expect.any(String),
230
233
  all: true,
231
234
  }, stopAllIo.io);
232
235
  expect(runtime).toHaveBeenNthCalledWith(5, {
233
236
  command: 'status',
234
- cliVersion: '0.0.0-private',
237
+ cliVersion,
235
238
  json: true,
236
239
  }, statusIo.io);
237
240
  expect(runtime).toHaveBeenCalledTimes(5);
@@ -278,7 +281,7 @@ describe('runKtxCli', () => {
278
281
  expect(sl).toHaveBeenLastCalledWith(expect.objectContaining({
279
282
  command: 'query',
280
283
  projectDir: tempDir,
281
- cliVersion: '0.0.0-private',
284
+ cliVersion,
282
285
  runtimeInstallPolicy: 'prompt',
283
286
  query: expect.objectContaining({ measures: ['orders.order_count'], dimensions: [] }),
284
287
  }), promptIo.io);
@@ -287,13 +290,13 @@ describe('runKtxCli', () => {
287
290
  sl,
288
291
  })).resolves.toBe(0);
289
292
  expect(sl).toHaveBeenLastCalledWith(expect.objectContaining({
290
- cliVersion: '0.0.0-private',
293
+ cliVersion,
291
294
  runtimeInstallPolicy: 'auto',
292
295
  }), autoIo.io);
293
296
  const noInputIo = makeIo();
294
297
  await expect(runKtxCli(['--project-dir', tempDir, 'sl', 'query', '--measure', 'orders.order_count', '--no-input'], noInputIo.io, { sl })).resolves.toBe(0);
295
298
  expect(sl).toHaveBeenLastCalledWith(expect.objectContaining({
296
- cliVersion: '0.0.0-private',
299
+ cliVersion,
297
300
  runtimeInstallPolicy: 'never',
298
301
  }), noInputIo.io);
299
302
  });
@@ -403,7 +406,7 @@ describe('runKtxCli', () => {
403
406
  skipAgents: false,
404
407
  inputMode: 'auto',
405
408
  yes: false,
406
- cliVersion: '0.0.0-private',
409
+ cliVersion,
407
410
  skipLlm: false,
408
411
  skipEmbeddings: false,
409
412
  databaseSchemas: [],
@@ -512,7 +515,7 @@ describe('runKtxCli', () => {
512
515
  inputMode: 'disabled',
513
516
  depth: 'fast',
514
517
  queryHistory: 'default',
515
- cliVersion: '0.0.0-private',
518
+ cliVersion,
516
519
  runtimeInstallPolicy: 'never',
517
520
  }, testIo.io);
518
521
  expect(testIo.stderr()).toBe(`Project: ${tempDir}\n`);
@@ -531,7 +534,7 @@ describe('runKtxCli', () => {
531
534
  inputMode: 'auto',
532
535
  depth: 'deep',
533
536
  queryHistory: 'default',
534
- cliVersion: '0.0.0-private',
537
+ cliVersion,
535
538
  runtimeInstallPolicy: 'prompt',
536
539
  }, testIo.io);
537
540
  expect(testIo.stderr()).toBe('');
@@ -580,7 +583,7 @@ describe('runKtxCli', () => {
580
583
  json: false,
581
584
  inputMode: 'disabled',
582
585
  queryHistory: 'default',
583
- cliVersion: '0.0.0-private',
586
+ cliVersion,
584
587
  runtimeInstallPolicy: 'never',
585
588
  }, testIo.io);
586
589
  });
@@ -798,7 +801,7 @@ describe('runKtxCli', () => {
798
801
  command: 'run',
799
802
  projectDir: tempDir,
800
803
  inputMode: 'disabled',
801
- cliVersion: '0.0.0-private',
804
+ cliVersion,
802
805
  anthropicApiKeyEnv: 'ANTHROPIC_API_KEY', // pragma: allowlist secret
803
806
  llmModel: 'claude-sonnet-4-6',
804
807
  skipLlm: false,
@@ -825,7 +828,7 @@ describe('runKtxCli', () => {
825
828
  command: 'run',
826
829
  projectDir: tempDir,
827
830
  inputMode: 'disabled',
828
- cliVersion: '0.0.0-private',
831
+ cliVersion,
829
832
  llmBackend: 'vertex',
830
833
  vertexProject: 'local-gcp-project',
831
834
  vertexLocation: 'us-east5',
@@ -850,7 +853,7 @@ describe('runKtxCli', () => {
850
853
  command: 'run',
851
854
  projectDir: tempDir,
852
855
  inputMode: 'disabled',
853
- cliVersion: '0.0.0-private',
856
+ cliVersion,
854
857
  llmBackend: 'claude-code',
855
858
  llmModel: 'opus',
856
859
  skipLlm: false,
@@ -925,7 +928,7 @@ describe('runKtxCli', () => {
925
928
  projectDir: '/tmp/project',
926
929
  inputMode: 'disabled',
927
930
  yes: true,
928
- cliVersion: '0.0.0-private',
931
+ cliVersion,
929
932
  skipLlm: true,
930
933
  skipEmbeddings: true,
931
934
  databaseDrivers: ['postgres'],
@@ -1143,7 +1146,7 @@ describe('runKtxCli', () => {
1143
1146
  queryFile: '/tmp/query.json',
1144
1147
  execute: false,
1145
1148
  format: 'json',
1146
- cliVersion: '0.0.0-private',
1149
+ cliVersion,
1147
1150
  runtimeInstallPolicy: 'auto',
1148
1151
  }, autoIo.io);
1149
1152
  expect(sl).toHaveBeenNthCalledWith(2, {
@@ -1153,7 +1156,7 @@ describe('runKtxCli', () => {
1153
1156
  queryFile: '/tmp/query.json',
1154
1157
  execute: false,
1155
1158
  format: 'json',
1156
- cliVersion: '0.0.0-private',
1159
+ cliVersion,
1157
1160
  runtimeInstallPolicy: 'never',
1158
1161
  }, neverIo.io);
1159
1162
  expect(conflictIo.stderr()).toContain('Choose only one runtime install mode: --yes or --no-input');
package/dist/ingest.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { buildMemoryFlowViewModel, createMemoryFlowLiveBuffer, formatMemoryFlowFinalSummary, getLatestLocalIngestStatus, getLocalIngestStatus, ingestReportToMemoryFlowReplay, renderMemoryFlowReplay, runLocalIngest, runLocalMetabaseIngest, savedMemoryCountsForReport, sanitizeMemoryFlowError, } from '@ktx/context/ingest';
2
- import { loadKtxCliProject } from './cli-project.js';
2
+ import { loadKtxProject } from '@ktx/context/project';
3
+ import { resolveProjectEmbeddingProvider } from './embedding-resolution.js';
3
4
  import { createKtxCliIngestQueryExecutor } from './ingest-query-executor.js';
4
5
  import { readIngestReportSnapshotFile } from './ingest-report-file.js';
5
6
  import { createCliOperationalLogger } from './io/logger.js';
@@ -470,16 +471,16 @@ async function writeReportRecord(report, outputMode, io, options = {}) {
470
471
  }
471
472
  export async function runKtxIngest(args, io = process, deps = {}) {
472
473
  try {
473
- const cliVersion = args.command === 'run' ? args.cliVersion : undefined;
474
- const runtimeInstallPolicy = args.command === 'run' ? args.runtimeInstallPolicy : undefined;
475
- const project = await loadKtxCliProject({
476
- projectDir: args.projectDir,
477
- cliVersion: cliVersion ?? '0.0.0-private',
478
- installPolicy: runtimeInstallPolicy ?? 'never',
479
- io,
480
- });
474
+ const project = await loadKtxProject({ projectDir: args.projectDir });
481
475
  const env = deps.env ?? process.env;
482
476
  if (args.command === 'run') {
477
+ const resolution = await resolveProjectEmbeddingProvider(project, {
478
+ mode: 'ensure',
479
+ installPolicy: args.runtimeInstallPolicy ?? 'never',
480
+ cliVersion: args.cliVersion ?? '0.0.0-private',
481
+ io,
482
+ });
483
+ const embeddingProvider = resolution.kind === 'disabled' || resolution.kind === 'managed-unavailable' ? null : resolution.provider;
483
484
  const ingestProject = args.allowImplicitAdapter && !project.config.ingest.adapters.includes(args.adapter)
484
485
  ? {
485
486
  ...project,
@@ -550,6 +551,7 @@ export async function runKtxIngest(args, io = process, deps = {}) {
550
551
  queryExecutor,
551
552
  trigger: 'manual_resync',
552
553
  jobIdFactory: deps.jobIdFactory,
554
+ embeddingProvider,
553
555
  ...(memoryFlow ? { memoryFlow } : {}),
554
556
  ...(progress ? { progress } : {}),
555
557
  });
@@ -617,6 +619,7 @@ export async function runKtxIngest(args, io = process, deps = {}) {
617
619
  ...localIngestOptions,
618
620
  queryExecutor,
619
621
  pullConfigOptions: adapterOptions,
622
+ embeddingProvider,
620
623
  ...(args.debugLlmRequestFile ? { llmDebugRequestFile: args.debugLlmRequestFile } : {}),
621
624
  ...(memoryFlow ? { memoryFlow } : {}),
622
625
  });
@@ -1,10 +1,13 @@
1
- import { createLocalKtxEmbeddingProviderFromConfig, type KtxEmbeddingPort } from '@ktx/context';
1
+ import { type KtxEmbeddingPort } from '@ktx/context';
2
+ import { searchLocalKnowledgePages as defaultSearchLocalKnowledgePages } from '@ktx/context/wiki';
3
+ import { resolveProjectEmbeddingProvider } from './embedding-resolution.js';
2
4
  export type KtxKnowledgeArgs = {
3
5
  command: 'list';
4
6
  projectDir: string;
5
7
  userId: string;
6
8
  output?: string;
7
9
  json?: boolean;
10
+ cliVersion: string;
8
11
  } | {
9
12
  command: 'search';
10
13
  projectDir: string;
@@ -14,11 +17,13 @@ export type KtxKnowledgeArgs = {
14
17
  json?: boolean;
15
18
  limit?: number;
16
19
  debug?: boolean;
20
+ cliVersion: string;
17
21
  };
18
22
  type KtxKnowledgeIo = import('./cli-runtime.js').KtxCliIo;
19
23
  interface KtxKnowledgeDeps {
20
24
  embeddingService?: KtxEmbeddingPort | null;
21
- createEmbeddingProvider?: typeof createLocalKtxEmbeddingProviderFromConfig;
25
+ resolveEmbeddingProvider?: typeof resolveProjectEmbeddingProvider;
26
+ searchLocalKnowledgePages?: typeof defaultSearchLocalKnowledgePages;
22
27
  }
23
28
  export declare function runKtxKnowledge(args: KtxKnowledgeArgs, io?: KtxKnowledgeIo, deps?: KtxKnowledgeDeps): Promise<number>;
24
29
  export {};
package/dist/knowledge.js CHANGED
@@ -1,6 +1,7 @@
1
- import { createLocalKtxEmbeddingProviderFromConfig, KtxIngestEmbeddingPortAdapter, } from '@ktx/context';
1
+ import { KtxIngestEmbeddingPortAdapter } from '@ktx/context';
2
2
  import { loadKtxProject } from '@ktx/context/project';
3
- import { listLocalKnowledgePages, searchLocalKnowledgePages, } from '@ktx/context/wiki';
3
+ import { listLocalKnowledgePages, searchLocalKnowledgePages as defaultSearchLocalKnowledgePages, } from '@ktx/context/wiki';
4
+ import { resolveProjectEmbeddingProvider, } from './embedding-resolution.js';
4
5
  import { resolveOutputMode } from './io/mode.js';
5
6
  import { createRankBadgeFormatter, printList } from './io/print-list.js';
6
7
  const WIKI_LIST_COLUMNS = [
@@ -23,12 +24,24 @@ function wikiSearchColumns(rows) {
23
24
  { key: 'summary', label: 'SUMMARY', plain: '', optional: true, dim: true },
24
25
  ];
25
26
  }
26
- function wikiSearchEmbeddingService(project, deps) {
27
+ function resolutionToEmbeddingPort(resolution) {
28
+ if (resolution.kind === 'configured' ||
29
+ resolution.kind === 'managed-running' ||
30
+ resolution.kind === 'managed-started') {
31
+ return new KtxIngestEmbeddingPortAdapter(resolution.provider);
32
+ }
33
+ return null;
34
+ }
35
+ async function wikiSearchEmbeddingService(project, deps, args, io) {
27
36
  if ('embeddingService' in deps) {
28
37
  return deps.embeddingService ?? null;
29
38
  }
30
- const provider = (deps.createEmbeddingProvider ?? createLocalKtxEmbeddingProviderFromConfig)(project.config.ingest.embeddings);
31
- return provider ? new KtxIngestEmbeddingPortAdapter(provider) : null;
39
+ const resolution = await (deps.resolveEmbeddingProvider ?? resolveProjectEmbeddingProvider)(project, {
40
+ mode: 'use-if-running',
41
+ cliVersion: args.cliVersion,
42
+ io,
43
+ });
44
+ return resolutionToEmbeddingPort(resolution);
32
45
  }
33
46
  function writeWikiSearchDebug(io, input) {
34
47
  io.stderr.write(`[debug] wiki search mode=${input.mode} embedding=${input.embeddingConfigured ? 'configured' : 'unconfigured'} results=${input.results.length}\n`);
@@ -58,8 +71,9 @@ export async function runKtxKnowledge(args, io = process, deps = {}) {
58
71
  return 0;
59
72
  }
60
73
  if (args.command === 'search') {
61
- const embeddingService = wikiSearchEmbeddingService(project, deps);
62
- const results = await searchLocalKnowledgePages(project, {
74
+ const embeddingService = await wikiSearchEmbeddingService(project, deps, { cliVersion: args.cliVersion }, io);
75
+ const search = deps.searchLocalKnowledgePages ?? defaultSearchLocalKnowledgePages;
76
+ const results = await search(project, {
63
77
  query: args.query,
64
78
  userId: args.userId,
65
79
  embeddingService,
@@ -4,7 +4,7 @@ import { join } from 'node:path';
4
4
  import { stripVTControlCharacters } from 'node:util';
5
5
  import { initKtxProject, loadKtxProject } from '@ktx/context/project';
6
6
  import { writeLocalKnowledgePage } from '@ktx/context/wiki';
7
- import { afterEach, beforeEach, describe, expect, it } from 'vitest';
7
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
8
8
  import { runKtxKnowledge } from './knowledge.js';
9
9
  function makeIo() {
10
10
  let stdout = '';
@@ -62,10 +62,10 @@ describe('runKtxKnowledge', () => {
62
62
  await initKtxProject({ projectDir });
63
63
  await seedWikiPage(projectDir);
64
64
  const listIo = makeIo();
65
- await expect(runKtxKnowledge({ command: 'list', projectDir, userId: 'local' }, listIo.io)).resolves.toBe(0);
65
+ await expect(runKtxKnowledge({ command: 'list', projectDir, userId: 'local', cliVersion: '0.0.0-test' }, listIo.io)).resolves.toBe(0);
66
66
  expect(listIo.stdout()).toContain('GLOBAL\tmetrics-revenue\tRevenue');
67
67
  const searchIo = makeIo();
68
- await expect(runKtxKnowledge({ command: 'search', projectDir, query: 'paid order', userId: 'local' }, searchIo.io)).resolves.toBe(0);
68
+ await expect(runKtxKnowledge({ command: 'search', projectDir, query: 'paid order', userId: 'local', cliVersion: '0.0.0-test' }, searchIo.io)).resolves.toBe(0);
69
69
  expect(searchIo.stdout()).toContain('metrics-revenue');
70
70
  });
71
71
  it('prints wiki search rank badges in pretty output', async () => {
@@ -73,7 +73,14 @@ describe('runKtxKnowledge', () => {
73
73
  await initKtxProject({ projectDir });
74
74
  await seedWikiPage(projectDir);
75
75
  const searchIo = makeIo();
76
- await expect(runKtxKnowledge({ command: 'search', projectDir, query: 'paid order', userId: 'local', output: 'pretty' }, searchIo.io)).resolves.toBe(0);
76
+ await expect(runKtxKnowledge({
77
+ command: 'search',
78
+ projectDir,
79
+ query: 'paid order',
80
+ userId: 'local',
81
+ output: 'pretty',
82
+ cliVersion: '0.0.0-test',
83
+ }, searchIo.io)).resolves.toBe(0);
77
84
  const stdout = stripVTControlCharacters(searchIo.stdout());
78
85
  expect(stdout).toMatch(/#1\s+metrics-revenue/);
79
86
  expect(stdout).not.toContain('%');
@@ -83,14 +90,22 @@ describe('runKtxKnowledge', () => {
83
90
  await initKtxProject({ projectDir });
84
91
  await seedWikiPage(projectDir);
85
92
  const listIo = makeIo();
86
- await expect(runKtxKnowledge({ command: 'list', projectDir, userId: 'local', json: true }, listIo.io)).resolves.toBe(0);
93
+ await expect(runKtxKnowledge({ command: 'list', projectDir, userId: 'local', json: true, cliVersion: '0.0.0-test' }, listIo.io)).resolves.toBe(0);
87
94
  expect(JSON.parse(listIo.stdout())).toMatchObject({
88
95
  kind: 'list',
89
96
  data: { items: [expect.objectContaining({ key: 'metrics-revenue', summary: 'Revenue' })] },
90
97
  meta: { command: 'wiki list' },
91
98
  });
92
99
  const searchIo = makeIo();
93
- await expect(runKtxKnowledge({ command: 'search', projectDir, query: 'paid order', userId: 'local', json: true, limit: 5 }, searchIo.io)).resolves.toBe(0);
100
+ await expect(runKtxKnowledge({
101
+ command: 'search',
102
+ projectDir,
103
+ query: 'paid order',
104
+ userId: 'local',
105
+ json: true,
106
+ limit: 5,
107
+ cliVersion: '0.0.0-test',
108
+ }, searchIo.io)).resolves.toBe(0);
94
109
  expect(JSON.parse(searchIo.stdout())).toMatchObject({
95
110
  kind: 'list',
96
111
  data: { items: [expect.objectContaining({ key: 'metrics-revenue', summary: 'Revenue' })] },
@@ -101,7 +116,7 @@ describe('runKtxKnowledge', () => {
101
116
  const projectDir = join(tempDir, 'empty-project');
102
117
  await initKtxProject({ projectDir });
103
118
  const searchIo = makeIo();
104
- await expect(runKtxKnowledge({ command: 'search', projectDir, query: 'revenue', userId: 'local' }, searchIo.io)).resolves.toBe(0);
119
+ await expect(runKtxKnowledge({ command: 'search', projectDir, query: 'revenue', userId: 'local', cliVersion: '0.0.0-test' }, searchIo.io)).resolves.toBe(0);
105
120
  expect(searchIo.stdout()).toBe('');
106
121
  expect(searchIo.stderr()).toContain('No local wiki pages found');
107
122
  expect(searchIo.stderr()).toContain('ktx ingest <connectionId>');
@@ -117,16 +132,45 @@ describe('runKtxKnowledge', () => {
117
132
  slRefs: [],
118
133
  });
119
134
  const searchIo = makeIo();
120
- await expect(runKtxKnowledge({ command: 'search', projectDir, query: 'revenue', userId: 'local' }, searchIo.io, { embeddingService: new FakeEmbeddingPort() })).resolves.toBe(0);
135
+ await expect(runKtxKnowledge({ command: 'search', projectDir, query: 'revenue', userId: 'local', cliVersion: '0.0.0-test' }, searchIo.io, { embeddingService: new FakeEmbeddingPort() })).resolves.toBe(0);
121
136
  expect(searchIo.stdout()).toContain('active-contract-arr-open-tickets');
122
137
  expect(searchIo.stderr()).toBe('');
123
138
  });
139
+ it('routes wiki search through resolveEmbeddingProvider when no embeddingService is injected', async () => {
140
+ const projectDir = join(tempDir, 'resolver-project');
141
+ await initKtxProject({ projectDir });
142
+ const search = vi.fn(async () => []);
143
+ const searchIo = makeIo();
144
+ await expect(runKtxKnowledge({
145
+ command: 'search',
146
+ projectDir,
147
+ query: 'income',
148
+ userId: 'local',
149
+ cliVersion: '0.5.0',
150
+ }, searchIo.io, {
151
+ resolveEmbeddingProvider: async () => ({
152
+ kind: 'managed-running',
153
+ provider: { id: 'fake' },
154
+ baseUrl: 'http://127.0.0.1:51234',
155
+ }),
156
+ searchLocalKnowledgePages: search,
157
+ })).resolves.toBe(0);
158
+ expect(search).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ embeddingService: expect.any(Object) }));
159
+ });
124
160
  it('writes wiki search lane diagnostics to stderr when debug is enabled', async () => {
125
161
  const projectDir = join(tempDir, 'debug-project');
126
162
  await initKtxProject({ projectDir });
127
163
  await seedWikiPage(projectDir);
128
164
  const searchIo = makeIo();
129
- await expect(runKtxKnowledge({ command: 'search', projectDir, query: 'paid order', userId: 'local', json: true, debug: true }, searchIo.io, { embeddingService: new FakeEmbeddingPort() })).resolves.toBe(0);
165
+ await expect(runKtxKnowledge({
166
+ command: 'search',
167
+ projectDir,
168
+ query: 'paid order',
169
+ userId: 'local',
170
+ json: true,
171
+ debug: true,
172
+ cliVersion: '0.0.0-test',
173
+ }, searchIo.io, { embeddingService: new FakeEmbeddingPort() })).resolves.toBe(0);
130
174
  expect(JSON.parse(searchIo.stdout())).toMatchObject({
131
175
  kind: 'list',
132
176
  data: { items: [expect.objectContaining({ key: 'metrics-revenue' })] },
@@ -1,8 +1,7 @@
1
- import type { KtxProjectEmbeddingConfig } from '@ktx/context/project';
2
1
  import type { KtxEmbeddingConfig } from '@ktx/llm';
3
2
  import type { KtxCliIo } from './cli-runtime.js';
4
3
  import { type KtxManagedPythonInstallPolicy, type ManagedPythonCommandRuntime } from './managed-python-command.js';
5
- import { type ManagedPythonDaemonStartResult } from './managed-python-daemon.js';
4
+ import { readManagedPythonDaemonStatus, type ManagedPythonDaemonStartResult } from './managed-python-daemon.js';
6
5
  export interface ManagedLocalEmbeddingsDaemon {
7
6
  baseUrl: string;
8
7
  stdoutLog: string;
@@ -26,13 +25,15 @@ export interface ManagedLocalEmbeddingsOptions {
26
25
  force: boolean;
27
26
  }) => Promise<ManagedPythonDaemonStartResult>;
28
27
  }
29
- export declare function managedLocalEmbeddingProjectConfig(input: {
30
- model: string;
31
- dimensions: number;
32
- }): KtxProjectEmbeddingConfig;
33
28
  export declare function managedLocalEmbeddingHealthConfig(input: {
34
29
  baseUrl: string;
35
30
  model: string;
36
31
  dimensions: number;
37
32
  }): KtxEmbeddingConfig;
38
33
  export declare function ensureManagedLocalEmbeddingsDaemon(options: ManagedLocalEmbeddingsOptions): Promise<ManagedLocalEmbeddingsDaemon>;
34
+ export interface TryUseManagedLocalEmbeddingsOptions {
35
+ cliVersion: string;
36
+ projectDir: string;
37
+ readStatus?: typeof readManagedPythonDaemonStatus;
38
+ }
39
+ export declare function tryUseManagedLocalEmbeddingsDaemon(options: TryUseManagedLocalEmbeddingsOptions): Promise<ManagedLocalEmbeddingsDaemon | null>;
@@ -1,17 +1,5 @@
1
- import { MANAGED_SENTENCE_TRANSFORMERS_BASE_URL } from '@ktx/context';
2
1
  import { ensureManagedPythonCommandRuntime, } from './managed-python-command.js';
3
- import { startManagedPythonDaemon } from './managed-python-daemon.js';
4
- export function managedLocalEmbeddingProjectConfig(input) {
5
- return {
6
- backend: 'sentence-transformers',
7
- model: input.model,
8
- dimensions: input.dimensions,
9
- sentenceTransformers: {
10
- base_url: MANAGED_SENTENCE_TRANSFORMERS_BASE_URL,
11
- pathPrefix: '',
12
- },
13
- };
14
- }
2
+ import { readManagedPythonDaemonStatus, startManagedPythonDaemon, } from './managed-python-daemon.js';
15
3
  export function managedLocalEmbeddingHealthConfig(input) {
16
4
  return {
17
5
  backend: 'sentence-transformers',
@@ -46,3 +34,21 @@ export async function ensureManagedLocalEmbeddingsDaemon(options) {
46
34
  stderrLog: daemon.state.stderrLog,
47
35
  };
48
36
  }
37
+ export async function tryUseManagedLocalEmbeddingsDaemon(options) {
38
+ const readStatus = options.readStatus ?? readManagedPythonDaemonStatus;
39
+ const status = await readStatus({
40
+ cliVersion: options.cliVersion,
41
+ projectDir: options.projectDir,
42
+ });
43
+ if (status.kind !== 'running') {
44
+ return null;
45
+ }
46
+ if (!status.state.features.includes('local-embeddings')) {
47
+ return null;
48
+ }
49
+ return {
50
+ baseUrl: status.baseUrl,
51
+ stdoutLog: status.state.stdoutLog,
52
+ stderrLog: status.state.stderrLog,
53
+ };
54
+ }