@kaelio/ktx 0.4.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.4.0-py3-none-any.whl → kaelio_ktx-0.4.1-py3-none-any.whl} +0 -0
  2. package/assets/python/manifest.json +3 -3
  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
@@ -2,10 +2,10 @@
2
2
  "schemaVersion": 1,
3
3
  "distributionName": "kaelio-ktx",
4
4
  "normalizedName": "kaelio_ktx",
5
- "version": "0.4.0",
5
+ "version": "0.4.1",
6
6
  "wheel": {
7
- "file": "kaelio_ktx-0.4.0-py3-none-any.whl",
8
- "sha256": "f969fd507d6ac483ae8ce2aab8257a4cb73dec0b3edcdef9be38dc133eb9a377",
7
+ "file": "kaelio_ktx-0.4.1-py3-none-any.whl",
8
+ "sha256": "18209f0ec9434f0c4073691b6b311562b66209ddb33c83953eca88d90fd3de65",
9
9
  "bytes": 80523
10
10
  }
11
11
  }
@@ -1,8 +1,9 @@
1
- import { createLocalKtxEmbeddingProviderFromConfig, KtxIngestEmbeddingPortAdapter, } from '@ktx/context';
1
+ import { KtxIngestEmbeddingPortAdapter } from '@ktx/context';
2
2
  import { reindexLocalIndexes } from '@ktx/context/index-sync';
3
+ import { loadKtxProject } from '@ktx/context/project';
3
4
  import { Option } from '@commander-js/extra-typings';
4
5
  import { cancel, intro, log, note, outro } from '@clack/prompts';
5
- import { loadKtxCliProject } from './cli-project.js';
6
+ import { resolveProjectEmbeddingProvider } from './embedding-resolution.js';
6
7
  import { resolveOutputMode } from './io/mode.js';
7
8
  import { green, red, SYMBOLS } from './io/symbols.js';
8
9
  export function registerAdminReindexCommand(admin, context) {
@@ -24,14 +25,6 @@ export function registerAdminReindexCommand(admin, context) {
24
25
  }, context.io));
25
26
  });
26
27
  }
27
- function resolveReindexEmbeddingService(project) {
28
- const config = project.config.ingest.embeddings;
29
- if (config.backend === 'none') {
30
- return null;
31
- }
32
- const provider = createLocalKtxEmbeddingProviderFromConfig(config);
33
- return provider ? new KtxIngestEmbeddingPortAdapter(provider) : null;
34
- }
35
28
  function scopeKey(scope) {
36
29
  if (scope.kind === 'wiki') {
37
30
  return scope.scope === 'user' ? `wiki/user/${scope.scopeId ?? 'local'}` : 'wiki/global';
@@ -131,13 +124,15 @@ function renderReindexPretty(summary, io) {
131
124
  }
132
125
  async function runKtxAdminReindex(args, io = process) {
133
126
  try {
134
- const project = await loadKtxCliProject({
135
- projectDir: args.projectDir,
127
+ const project = await loadKtxProject({ projectDir: args.projectDir });
128
+ const resolution = await resolveProjectEmbeddingProvider(project, {
129
+ mode: 'use-if-running',
136
130
  cliVersion: args.cliVersion,
137
- installPolicy: 'never',
138
131
  io,
139
132
  });
140
- const embeddingService = resolveReindexEmbeddingService(project);
133
+ const embeddingService = resolution.kind === 'configured' || resolution.kind === 'managed-running' || resolution.kind === 'managed-started'
134
+ ? new KtxIngestEmbeddingPortAdapter(resolution.provider)
135
+ : null;
141
136
  const summary = await reindexLocalIndexes(project, { force: args.force, embeddingService });
142
137
  const mode = resolveOutputMode({ explicit: args.output, json: args.json, io });
143
138
  if (!summary.embeddingsAvailable && mode === 'plain') {
@@ -1,6 +1,9 @@
1
+ import { createRequire } from 'node:module';
1
2
  import { describe, expect, it, vi } from 'vitest';
2
3
  import { renderReindexJson, renderReindexPlain, reindexHasErrors } from './admin-reindex.js';
3
4
  import { runKtxCli } from './index.js';
5
+ const cliVersion = createRequire(import.meta.url)('@ktx/cli/package.json')
6
+ .version;
4
7
  function makeIo(options = {}) {
5
8
  let stdout = '';
6
9
  let stderr = '';
@@ -110,7 +113,7 @@ describe('admin reindex Commander routing', () => {
110
113
  force: true,
111
114
  json: true,
112
115
  output: 'plain',
113
- cliVersion: '0.0.0-private',
116
+ cliVersion,
114
117
  }, io.io);
115
118
  });
116
119
  });
@@ -1,18 +1,12 @@
1
1
  import { loadKtxProject, type KtxLocalProject } from '@ktx/context/project';
2
- import type { KtxProjectConfig } from '@ktx/context/project';
3
- import type { KtxCliIo } from './cli-runtime.js';
4
- import { ensureManagedLocalEmbeddingsDaemon, type ManagedLocalEmbeddingsDaemon } from './managed-local-embeddings.js';
5
- import type { KtxManagedPythonInstallPolicy } from './managed-python-command.js';
6
2
  export interface LoadKtxCliProjectOptions {
7
3
  projectDir: string;
8
- cliVersion: string;
9
- installPolicy: KtxManagedPythonInstallPolicy;
10
- io: KtxCliIo;
11
4
  }
12
5
  export interface LoadKtxCliProjectDeps {
13
6
  loadProject?: typeof loadKtxProject;
14
- ensureLocalEmbeddings?: (options: Parameters<typeof ensureManagedLocalEmbeddingsDaemon>[0]) => Promise<ManagedLocalEmbeddingsDaemon>;
15
7
  }
8
+ /**
9
+ * Thin wrapper around `loadKtxProject`. Kept as a single entrypoint so the CLI can grow shared
10
+ * pre-load behavior later (telemetry, project lock, etc.). Today it does no extra work.
11
+ */
16
12
  export declare function loadKtxCliProject(options: LoadKtxCliProjectOptions, deps?: LoadKtxCliProjectDeps): Promise<KtxLocalProject>;
17
- export declare function projectNeedsManagedLocalEmbeddings(config: KtxProjectConfig): boolean;
18
- export declare function substituteManagedLocalEmbeddingsUrl(config: KtxProjectConfig, baseUrl: string): KtxProjectConfig;
@@ -1,52 +1,8 @@
1
- import { MANAGED_SENTENCE_TRANSFORMERS_BASE_URL } from '@ktx/context';
2
1
  import { loadKtxProject } from '@ktx/context/project';
3
- import { ensureManagedLocalEmbeddingsDaemon, } from './managed-local-embeddings.js';
2
+ /**
3
+ * Thin wrapper around `loadKtxProject`. Kept as a single entrypoint so the CLI can grow shared
4
+ * pre-load behavior later (telemetry, project lock, etc.). Today it does no extra work.
5
+ */
4
6
  export async function loadKtxCliProject(options, deps = {}) {
5
- const loadProject = deps.loadProject ?? loadKtxProject;
6
- const ensureLocalEmbeddings = deps.ensureLocalEmbeddings ?? ensureManagedLocalEmbeddingsDaemon;
7
- const project = await loadProject({ projectDir: options.projectDir });
8
- if (!projectNeedsManagedLocalEmbeddings(project.config)) {
9
- return project;
10
- }
11
- const daemon = await ensureLocalEmbeddings({
12
- cliVersion: options.cliVersion,
13
- projectDir: options.projectDir,
14
- installPolicy: options.installPolicy,
15
- io: options.io,
16
- });
17
- return {
18
- ...project,
19
- config: substituteManagedLocalEmbeddingsUrl(project.config, daemon.baseUrl),
20
- };
21
- }
22
- export function projectNeedsManagedLocalEmbeddings(config) {
23
- return (embeddingUsesManagedSentinel(config.ingest.embeddings) ||
24
- embeddingUsesManagedSentinel(config.scan.enrichment.embeddings));
25
- }
26
- export function substituteManagedLocalEmbeddingsUrl(config, baseUrl) {
27
- const ingestEmbeddings = rewriteManagedEmbeddingConfig(config.ingest.embeddings, baseUrl);
28
- const scanEnrichmentEmbeddings = rewriteManagedEmbeddingConfig(config.scan.enrichment.embeddings, baseUrl);
29
- return {
30
- ...config,
31
- ingest: { ...config.ingest, embeddings: ingestEmbeddings },
32
- scan: {
33
- ...config.scan,
34
- enrichment: { ...config.scan.enrichment, embeddings: scanEnrichmentEmbeddings },
35
- },
36
- };
37
- }
38
- function embeddingUsesManagedSentinel(embedding) {
39
- return embedding?.sentenceTransformers?.base_url === MANAGED_SENTENCE_TRANSFORMERS_BASE_URL;
40
- }
41
- function rewriteManagedEmbeddingConfig(embedding, baseUrl) {
42
- if (!embedding || !embeddingUsesManagedSentinel(embedding)) {
43
- return embedding;
44
- }
45
- return {
46
- ...embedding,
47
- sentenceTransformers: {
48
- ...embedding.sentenceTransformers,
49
- base_url: baseUrl,
50
- },
51
- };
7
+ return (deps.loadProject ?? loadKtxProject)({ projectDir: options.projectDir });
52
8
  }
@@ -1,22 +1,6 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
- import { MANAGED_SENTENCE_TRANSFORMERS_BASE_URL } from '@ktx/context';
3
2
  import { buildDefaultKtxProjectConfig } from '@ktx/context/project';
4
- import { loadKtxCliProject, projectNeedsManagedLocalEmbeddings, substituteManagedLocalEmbeddingsUrl, } from './cli-project.js';
5
- const RESOLVED_BASE_URL = 'http://127.0.0.1:51234';
6
- function makeIo() {
7
- let stderr = '';
8
- return {
9
- io: {
10
- stdout: { write: (_chunk) => { } },
11
- stderr: {
12
- write: (chunk) => {
13
- stderr += chunk;
14
- },
15
- },
16
- },
17
- stderr: () => stderr,
18
- };
19
- }
3
+ import { loadKtxCliProject } from './cli-project.js';
20
4
  function projectWithConfig(config) {
21
5
  return {
22
6
  projectDir: '/work/proj',
@@ -27,123 +11,12 @@ function projectWithConfig(config) {
27
11
  fileStore: {},
28
12
  };
29
13
  }
30
- function withManagedIngestEmbedding(config) {
31
- return {
32
- ...config,
33
- ingest: {
34
- ...config.ingest,
35
- embeddings: {
36
- backend: 'sentence-transformers',
37
- model: 'all-MiniLM-L6-v2',
38
- dimensions: 384,
39
- sentenceTransformers: { base_url: MANAGED_SENTENCE_TRANSFORMERS_BASE_URL, pathPrefix: '' },
40
- },
41
- },
42
- };
43
- }
44
- function withManagedScanEnrichmentEmbedding(config) {
45
- return {
46
- ...config,
47
- scan: {
48
- ...config.scan,
49
- enrichment: {
50
- ...config.scan.enrichment,
51
- embeddings: {
52
- backend: 'sentence-transformers',
53
- model: 'all-MiniLM-L6-v2',
54
- dimensions: 384,
55
- sentenceTransformers: { base_url: MANAGED_SENTENCE_TRANSFORMERS_BASE_URL, pathPrefix: '' },
56
- },
57
- },
58
- },
59
- };
60
- }
61
- const fakeDaemon = {
62
- baseUrl: RESOLVED_BASE_URL,
63
- stdoutLog: '/work/proj/.ktx/runtime/daemon.stdout.log',
64
- stderrLog: '/work/proj/.ktx/runtime/daemon.stderr.log',
65
- };
66
- describe('projectNeedsManagedLocalEmbeddings', () => {
67
- it('returns false when neither ingest nor scan embeddings reference the managed sentinel', () => {
68
- expect(projectNeedsManagedLocalEmbeddings(buildDefaultKtxProjectConfig())).toBe(false);
69
- });
70
- it('returns true when ingest.embeddings uses the managed sentinel', () => {
71
- expect(projectNeedsManagedLocalEmbeddings(withManagedIngestEmbedding(buildDefaultKtxProjectConfig()))).toBe(true);
72
- });
73
- it('returns true when scan.enrichment.embeddings uses the managed sentinel', () => {
74
- expect(projectNeedsManagedLocalEmbeddings(withManagedScanEnrichmentEmbedding(buildDefaultKtxProjectConfig()))).toBe(true);
75
- });
76
- });
77
- describe('substituteManagedLocalEmbeddingsUrl', () => {
78
- it('rewrites the managed sentinel in both ingest.embeddings and scan.enrichment.embeddings', () => {
79
- const config = withManagedScanEnrichmentEmbedding(withManagedIngestEmbedding(buildDefaultKtxProjectConfig()));
80
- const resolved = substituteManagedLocalEmbeddingsUrl(config, RESOLVED_BASE_URL);
81
- expect(resolved.ingest.embeddings.sentenceTransformers?.base_url).toBe(RESOLVED_BASE_URL);
82
- expect(resolved.scan.enrichment.embeddings?.sentenceTransformers?.base_url).toBe(RESOLVED_BASE_URL);
83
- });
84
- it('returns the input unchanged when no sentinel is present', () => {
85
- const config = buildDefaultKtxProjectConfig();
86
- const resolved = substituteManagedLocalEmbeddingsUrl(config, RESOLVED_BASE_URL);
87
- expect(resolved.ingest.embeddings).toEqual(config.ingest.embeddings);
88
- expect(resolved.scan.enrichment.embeddings).toEqual(config.scan.enrichment.embeddings);
89
- });
90
- it('does not touch non-sentinel sentence-transformers URLs', () => {
91
- const config = {
92
- ...buildDefaultKtxProjectConfig(),
93
- ingest: {
94
- ...buildDefaultKtxProjectConfig().ingest,
95
- embeddings: {
96
- backend: 'sentence-transformers',
97
- model: 'all-MiniLM-L6-v2',
98
- dimensions: 384,
99
- sentenceTransformers: { base_url: 'http://localhost:9999', pathPrefix: '' },
100
- },
101
- },
102
- };
103
- const resolved = substituteManagedLocalEmbeddingsUrl(config, RESOLVED_BASE_URL);
104
- expect(resolved.ingest.embeddings.sentenceTransformers?.base_url).toBe('http://localhost:9999');
105
- });
106
- });
107
14
  describe('loadKtxCliProject', () => {
108
- it('returns the project unchanged and does not start the daemon when no sentinel is present', async () => {
109
- const io = makeIo();
15
+ it('delegates to loadKtxProject and returns the project unchanged', async () => {
110
16
  const project = projectWithConfig(buildDefaultKtxProjectConfig());
111
17
  const loadProject = vi.fn(async () => project);
112
- const ensureLocalEmbeddings = vi.fn(async () => fakeDaemon);
113
- const result = await loadKtxCliProject({ projectDir: '/work/proj', cliVersion: '0.2.0', installPolicy: 'never', io: io.io }, { loadProject, ensureLocalEmbeddings });
18
+ const result = await loadKtxCliProject({ projectDir: '/work/proj' }, { loadProject });
114
19
  expect(result).toBe(project);
115
- expect(ensureLocalEmbeddings).not.toHaveBeenCalled();
116
- });
117
- it('starts the daemon and substitutes the resolved URL when ingest.embeddings uses the sentinel', async () => {
118
- const io = makeIo();
119
- const project = projectWithConfig(withManagedIngestEmbedding(buildDefaultKtxProjectConfig()));
120
- const loadProject = vi.fn(async () => project);
121
- const ensureLocalEmbeddings = vi.fn(async () => fakeDaemon);
122
- const result = await loadKtxCliProject({ projectDir: '/work/proj', cliVersion: '0.2.0', installPolicy: 'never', io: io.io }, { loadProject, ensureLocalEmbeddings });
123
- expect(ensureLocalEmbeddings).toHaveBeenCalledWith({
124
- cliVersion: '0.2.0',
125
- projectDir: '/work/proj',
126
- installPolicy: 'never',
127
- io: io.io,
128
- });
129
- expect(result.config.ingest.embeddings.sentenceTransformers?.base_url).toBe(RESOLVED_BASE_URL);
130
- });
131
- it('does not mutate process.env', async () => {
132
- const io = makeIo();
133
- const before = process.env.KTX_MANAGED_SENTENCE_TRANSFORMERS_BASE_URL;
134
- delete process.env.KTX_MANAGED_SENTENCE_TRANSFORMERS_BASE_URL;
135
- try {
136
- const project = projectWithConfig(withManagedIngestEmbedding(buildDefaultKtxProjectConfig()));
137
- await loadKtxCliProject({ projectDir: '/work/proj', cliVersion: '0.2.0', installPolicy: 'never', io: io.io }, { loadProject: vi.fn(async () => project), ensureLocalEmbeddings: vi.fn(async () => fakeDaemon) });
138
- expect(process.env.KTX_MANAGED_SENTENCE_TRANSFORMERS_BASE_URL).toBeUndefined();
139
- }
140
- finally {
141
- if (before === undefined) {
142
- delete process.env.KTX_MANAGED_SENTENCE_TRANSFORMERS_BASE_URL;
143
- }
144
- else {
145
- process.env.KTX_MANAGED_SENTENCE_TRANSFORMERS_BASE_URL = before;
146
- }
147
- }
20
+ expect(loadProject).toHaveBeenCalledWith({ projectDir: '/work/proj' });
148
21
  });
149
22
  });
@@ -34,6 +34,7 @@ export function registerWikiCommands(program, context) {
34
34
  userId: options.userId,
35
35
  output: options.output,
36
36
  json: options.json,
37
+ cliVersion: context.packageInfo.version,
37
38
  });
38
39
  return;
39
40
  }
@@ -46,6 +47,7 @@ export function registerWikiCommands(program, context) {
46
47
  json: options.json,
47
48
  ...(isDebugEnabled(command) ? { debug: true } : {}),
48
49
  ...(options.limit !== undefined ? { limit: options.limit } : {}),
50
+ cliVersion: context.packageInfo.version,
49
51
  });
50
52
  });
51
53
  }
@@ -48,6 +48,7 @@ export function registerSlCommands(program, context, commandName = 'sl') {
48
48
  connectionId: options.connectionId,
49
49
  output: options.output,
50
50
  json: options.json,
51
+ cliVersion: context.packageInfo.version,
51
52
  });
52
53
  return;
53
54
  }
@@ -59,6 +60,7 @@ export function registerSlCommands(program, context, commandName = 'sl') {
59
60
  ...(options.limit !== undefined ? { limit: options.limit } : {}),
60
61
  output: options.output,
61
62
  json: options.json,
63
+ cliVersion: context.packageInfo.version,
62
64
  });
63
65
  });
64
66
  sl.command('validate')
@@ -0,0 +1,36 @@
1
+ import { type KtxEmbeddingProvider, createKtxEmbeddingProvider as defaultCreateKtxEmbeddingProvider } from '@ktx/llm';
2
+ import type { KtxLocalProject } from '@ktx/context/project';
3
+ import type { KtxCliIo } from './cli-runtime.js';
4
+ import { ensureManagedLocalEmbeddingsDaemon as defaultEnsureManagedDaemon, tryUseManagedLocalEmbeddingsDaemon as defaultTryUseManagedDaemon } from './managed-local-embeddings.js';
5
+ import type { KtxManagedPythonInstallPolicy } from './managed-python-command.js';
6
+ type EmbeddingResolutionMode = 'ensure' | 'use-if-running';
7
+ export type EmbeddingProviderResolution = {
8
+ kind: 'disabled';
9
+ } | {
10
+ kind: 'configured';
11
+ provider: KtxEmbeddingProvider;
12
+ baseUrl: string;
13
+ } | {
14
+ kind: 'managed-running';
15
+ provider: KtxEmbeddingProvider;
16
+ baseUrl: string;
17
+ } | {
18
+ kind: 'managed-started';
19
+ provider: KtxEmbeddingProvider;
20
+ baseUrl: string;
21
+ } | {
22
+ kind: 'managed-unavailable';
23
+ reason: string;
24
+ };
25
+ export interface ResolveProjectEmbeddingProviderOptions {
26
+ mode: EmbeddingResolutionMode;
27
+ cliVersion: string;
28
+ io: KtxCliIo;
29
+ /** Required when mode === 'ensure'. */
30
+ installPolicy?: KtxManagedPythonInstallPolicy;
31
+ tryUseManagedDaemon?: typeof defaultTryUseManagedDaemon;
32
+ ensureManagedDaemon?: typeof defaultEnsureManagedDaemon;
33
+ createKtxEmbeddingProvider?: typeof defaultCreateKtxEmbeddingProvider;
34
+ }
35
+ export declare function resolveProjectEmbeddingProvider(project: KtxLocalProject, options: ResolveProjectEmbeddingProviderOptions): Promise<EmbeddingProviderResolution>;
36
+ export {};
@@ -0,0 +1,63 @@
1
+ import { createKtxEmbeddingProvider as defaultCreateKtxEmbeddingProvider, } from '@ktx/llm';
2
+ import { resolveLocalKtxEmbeddingConfig } from '@ktx/context';
3
+ import { ensureManagedLocalEmbeddingsDaemon as defaultEnsureManagedDaemon, tryUseManagedLocalEmbeddingsDaemon as defaultTryUseManagedDaemon, } from './managed-local-embeddings.js';
4
+ function usesManagedDaemon(embeddings) {
5
+ if (embeddings.backend !== 'sentence-transformers') {
6
+ return false;
7
+ }
8
+ const baseUrl = embeddings.sentenceTransformers?.base_url;
9
+ return baseUrl === undefined || baseUrl === '';
10
+ }
11
+ export async function resolveProjectEmbeddingProvider(project, options) {
12
+ const embeddings = project.config.ingest.embeddings;
13
+ if (embeddings.backend === 'none') {
14
+ return { kind: 'disabled' };
15
+ }
16
+ const createProvider = options.createKtxEmbeddingProvider ?? defaultCreateKtxEmbeddingProvider;
17
+ if (!usesManagedDaemon(embeddings)) {
18
+ const resolved = resolveLocalKtxEmbeddingConfig(embeddings, process.env);
19
+ if (!resolved) {
20
+ return { kind: 'managed-unavailable', reason: 'embedding config missing required fields' };
21
+ }
22
+ const provider = createProvider(resolved);
23
+ const baseUrl = embeddings.sentenceTransformers?.base_url ?? '';
24
+ return { kind: 'configured', provider, baseUrl };
25
+ }
26
+ const tryUse = options.tryUseManagedDaemon ?? defaultTryUseManagedDaemon;
27
+ const running = await tryUse({ cliVersion: options.cliVersion, projectDir: project.projectDir });
28
+ if (running) {
29
+ const provider = buildManagedProvider(embeddings, running.baseUrl, createProvider);
30
+ return provider
31
+ ? { kind: 'managed-running', provider, baseUrl: running.baseUrl }
32
+ : { kind: 'managed-unavailable', reason: 'failed to build embedding provider from running daemon' };
33
+ }
34
+ if (options.mode === 'use-if-running') {
35
+ return { kind: 'managed-unavailable', reason: 'managed embeddings daemon is not running' };
36
+ }
37
+ const ensure = options.ensureManagedDaemon ?? defaultEnsureManagedDaemon;
38
+ if (!options.installPolicy) {
39
+ throw new Error("installPolicy is required when mode === 'ensure'");
40
+ }
41
+ const daemon = await ensure({
42
+ cliVersion: options.cliVersion,
43
+ projectDir: project.projectDir,
44
+ installPolicy: options.installPolicy,
45
+ io: options.io,
46
+ });
47
+ const provider = buildManagedProvider(embeddings, daemon.baseUrl, createProvider);
48
+ return provider
49
+ ? { kind: 'managed-started', provider, baseUrl: daemon.baseUrl }
50
+ : { kind: 'managed-unavailable', reason: 'failed to build embedding provider after starting daemon' };
51
+ }
52
+ function buildManagedProvider(embeddings, baseUrl, createProvider) {
53
+ const merged = {
54
+ ...embeddings,
55
+ sentenceTransformers: {
56
+ ...embeddings.sentenceTransformers,
57
+ base_url: baseUrl,
58
+ pathPrefix: '',
59
+ },
60
+ };
61
+ const resolved = resolveLocalKtxEmbeddingConfig(merged, process.env);
62
+ return resolved ? createProvider(resolved) : null;
63
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,132 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { buildDefaultKtxProjectConfig } from '@ktx/context/project';
3
+ import { resolveProjectEmbeddingProvider } from './embedding-resolution.js';
4
+ function projectWithConfig(config) {
5
+ return {
6
+ projectDir: '/work/proj',
7
+ configPath: '/work/proj/ktx.yaml',
8
+ config,
9
+ coreConfig: {},
10
+ git: {},
11
+ fileStore: {},
12
+ };
13
+ }
14
+ function withManagedEmbedding(config, base_url) {
15
+ return {
16
+ ...config,
17
+ ingest: {
18
+ ...config.ingest,
19
+ embeddings: {
20
+ backend: 'sentence-transformers',
21
+ model: 'all-MiniLM-L6-v2',
22
+ dimensions: 384,
23
+ ...(base_url === undefined
24
+ ? {}
25
+ : { sentenceTransformers: { base_url, pathPrefix: '' } }),
26
+ },
27
+ },
28
+ };
29
+ }
30
+ const noopIo = {
31
+ stdout: { write: (_chunk) => { } },
32
+ stderr: { write: (_chunk) => { } },
33
+ };
34
+ const fakeDaemon = {
35
+ baseUrl: 'http://127.0.0.1:51234',
36
+ stdoutLog: '/tmp/o',
37
+ stderrLog: '/tmp/e',
38
+ };
39
+ describe('resolveProjectEmbeddingProvider', () => {
40
+ it('returns disabled when backend is none', async () => {
41
+ const project = projectWithConfig(buildDefaultKtxProjectConfig());
42
+ const result = await resolveProjectEmbeddingProvider(project, {
43
+ mode: 'use-if-running',
44
+ cliVersion: '0.5.0',
45
+ io: noopIo,
46
+ });
47
+ expect(result.kind).toBe('disabled');
48
+ });
49
+ it('returns a configured provider when base_url is explicit', async () => {
50
+ const project = projectWithConfig(withManagedEmbedding(buildDefaultKtxProjectConfig(), 'http://my-st:8080'));
51
+ const createKtxEmbeddingProvider = vi.fn(() => ({ id: 'fake' }));
52
+ const result = await resolveProjectEmbeddingProvider(project, {
53
+ mode: 'use-if-running',
54
+ cliVersion: '0.5.0',
55
+ io: noopIo,
56
+ createKtxEmbeddingProvider,
57
+ });
58
+ expect(result.kind).toBe('configured');
59
+ expect(createKtxEmbeddingProvider).toHaveBeenCalledOnce();
60
+ });
61
+ it('connects to the running managed daemon when base_url is omitted', async () => {
62
+ const project = projectWithConfig(withManagedEmbedding(buildDefaultKtxProjectConfig(), undefined));
63
+ const tryUseManaged = vi.fn(async () => fakeDaemon);
64
+ const createKtxEmbeddingProvider = vi.fn(() => ({ id: 'fake' }));
65
+ const ensureManaged = vi.fn(async () => fakeDaemon);
66
+ const result = await resolveProjectEmbeddingProvider(project, {
67
+ mode: 'use-if-running',
68
+ cliVersion: '0.5.0',
69
+ io: noopIo,
70
+ createKtxEmbeddingProvider,
71
+ tryUseManagedDaemon: tryUseManaged,
72
+ ensureManagedDaemon: ensureManaged,
73
+ });
74
+ expect(result.kind).toBe('managed-running');
75
+ expect(tryUseManaged).toHaveBeenCalledOnce();
76
+ expect(ensureManaged).not.toHaveBeenCalled();
77
+ });
78
+ it('passes pathPrefix="" to the embedding provider when targeting the managed daemon', async () => {
79
+ const project = projectWithConfig(withManagedEmbedding(buildDefaultKtxProjectConfig(), undefined));
80
+ const tryUseManaged = vi.fn(async () => fakeDaemon);
81
+ const createKtxEmbeddingProvider = vi.fn(() => ({ id: 'fake' }));
82
+ await resolveProjectEmbeddingProvider(project, {
83
+ mode: 'use-if-running',
84
+ cliVersion: '0.5.0',
85
+ io: noopIo,
86
+ createKtxEmbeddingProvider,
87
+ tryUseManagedDaemon: tryUseManaged,
88
+ });
89
+ expect(createKtxEmbeddingProvider).toHaveBeenCalledWith(expect.objectContaining({
90
+ sentenceTransformers: expect.objectContaining({
91
+ baseURL: fakeDaemon.baseUrl,
92
+ pathPrefix: '',
93
+ }),
94
+ }));
95
+ });
96
+ it('returns managed-unavailable when no daemon is running and mode is use-if-running', async () => {
97
+ const project = projectWithConfig(withManagedEmbedding(buildDefaultKtxProjectConfig(), ''));
98
+ const tryUseManaged = vi.fn(async () => null);
99
+ const ensureManaged = vi.fn(async () => fakeDaemon);
100
+ const result = await resolveProjectEmbeddingProvider(project, {
101
+ mode: 'use-if-running',
102
+ cliVersion: '0.5.0',
103
+ io: noopIo,
104
+ tryUseManagedDaemon: tryUseManaged,
105
+ ensureManagedDaemon: ensureManaged,
106
+ });
107
+ expect(result.kind).toBe('managed-unavailable');
108
+ expect(ensureManaged).not.toHaveBeenCalled();
109
+ });
110
+ it('starts the managed daemon when mode is ensure', async () => {
111
+ const project = projectWithConfig(withManagedEmbedding(buildDefaultKtxProjectConfig(), undefined));
112
+ const tryUseManaged = vi.fn(async () => null);
113
+ const ensureManaged = vi.fn(async () => fakeDaemon);
114
+ const createKtxEmbeddingProvider = vi.fn(() => ({ id: 'fake' }));
115
+ const result = await resolveProjectEmbeddingProvider(project, {
116
+ mode: 'ensure',
117
+ installPolicy: 'auto',
118
+ cliVersion: '0.5.0',
119
+ io: noopIo,
120
+ createKtxEmbeddingProvider,
121
+ tryUseManagedDaemon: tryUseManaged,
122
+ ensureManagedDaemon: ensureManaged,
123
+ });
124
+ expect(result.kind).toBe('managed-started');
125
+ expect(ensureManaged).toHaveBeenCalledWith({
126
+ cliVersion: '0.5.0',
127
+ projectDir: '/work/proj',
128
+ installPolicy: 'auto',
129
+ io: noopIo,
130
+ });
131
+ });
132
+ });
package/dist/index.d.ts CHANGED
@@ -10,7 +10,7 @@ export { runKtxRuntime, type KtxRuntimeArgs, type KtxRuntimeDeps } from './runti
10
10
  export { runKtxSql, type KtxSqlArgs, type KtxSqlDeps } from './sql.js';
11
11
  export { allocateDaemonPort, readManagedPythonDaemonStatus, stopAllManagedPythonDaemons, startManagedPythonDaemon, stopManagedPythonDaemon, } from './managed-python-daemon.js';
12
12
  export type { ManagedPythonDaemonProcessInfo, ManagedPythonDaemonStartResult, ManagedPythonDaemonState, ManagedPythonDaemonStatus, ManagedPythonDaemonStopAllEntry, ManagedPythonDaemonStopAllFailure, ManagedPythonDaemonStopAllResult, ManagedPythonDaemonStopResult, } from './managed-python-daemon.js';
13
- export { ensureManagedLocalEmbeddingsDaemon, managedLocalEmbeddingHealthConfig, managedLocalEmbeddingProjectConfig, type ManagedLocalEmbeddingsDaemon, type ManagedLocalEmbeddingsOptions, } from './managed-local-embeddings.js';
13
+ export { ensureManagedLocalEmbeddingsDaemon, managedLocalEmbeddingHealthConfig, type ManagedLocalEmbeddingsDaemon, type ManagedLocalEmbeddingsOptions, } from './managed-local-embeddings.js';
14
14
  export type { KtxMemoryFlowTuiIo, MemoryFlowTuiLiveSession } from './memory-flow-tui.js';
15
15
  export { renderMemoryFlowTui, sanitizeMemoryFlowTuiError, startLiveMemoryFlowTui, } from './memory-flow-tui.js';
16
16
  export { rendererUnavailableVizFallback, resolveVizFallback, warnVizFallbackOnce } from './viz-fallback.js';
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ export { runKtxSetupSourcesStep } from './setup-sources.js';
7
7
  export { runKtxRuntime } from './runtime.js';
8
8
  export { runKtxSql } from './sql.js';
9
9
  export { allocateDaemonPort, readManagedPythonDaemonStatus, stopAllManagedPythonDaemons, startManagedPythonDaemon, stopManagedPythonDaemon, } from './managed-python-daemon.js';
10
- export { ensureManagedLocalEmbeddingsDaemon, managedLocalEmbeddingHealthConfig, managedLocalEmbeddingProjectConfig, } from './managed-local-embeddings.js';
10
+ export { ensureManagedLocalEmbeddingsDaemon, managedLocalEmbeddingHealthConfig, } from './managed-local-embeddings.js';
11
11
  export { renderMemoryFlowTui, sanitizeMemoryFlowTuiError, startLiveMemoryFlowTui, } from './memory-flow-tui.js';
12
12
  export { rendererUnavailableVizFallback, resolveVizFallback, warnVizFallbackOnce } from './viz-fallback.js';
13
13
  profileMark('module:index');