@kaelio/ktx 0.1.0 → 0.1.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.
- package/assets/python/{kaelio_ktx-0.1.0-py3-none-any.whl → kaelio_ktx-0.1.1-py3-none-any.whl} +0 -0
- package/assets/python/manifest.json +4 -4
- package/dist/commands/setup-commands.js +14 -26
- package/dist/doctor.test.js +3 -4
- package/dist/index.test.js +26 -10
- package/dist/ingest-depth.js +0 -1
- package/dist/ingest.test-utils.js +2 -2
- package/dist/ingest.test.js +4 -4
- package/dist/managed-local-embeddings.d.ts +2 -0
- package/dist/managed-local-embeddings.js +2 -0
- package/dist/managed-local-embeddings.test.js +2 -0
- package/dist/managed-mcp-daemon.js +3 -2
- package/dist/managed-mcp-daemon.test.js +25 -0
- package/dist/managed-python-command.test.js +1 -0
- package/dist/managed-python-daemon.js +3 -2
- package/dist/managed-python-daemon.test.js +20 -0
- package/dist/managed-python-runtime.d.ts +4 -0
- package/dist/managed-python-runtime.js +47 -3
- package/dist/managed-python-runtime.test.js +51 -21
- package/dist/proxy-env.d.ts +1 -0
- package/dist/proxy-env.js +23 -0
- package/dist/proxy-env.test.d.ts +1 -0
- package/dist/proxy-env.test.js +17 -0
- package/dist/runtime.test.js +1 -0
- package/dist/setup-agents.js +3 -1
- package/dist/setup-agents.test.js +34 -0
- package/dist/setup-embeddings.d.ts +1 -0
- package/dist/setup-embeddings.js +28 -6
- package/dist/setup-embeddings.test.js +46 -4
- package/dist/setup-models.d.ts +0 -1
- package/dist/setup-models.js +2 -3
- package/dist/setup-models.test.js +8 -10
- package/dist/setup-project.d.ts +9 -1
- package/dist/setup-project.js +52 -25
- package/dist/setup-project.test.js +8 -8
- package/dist/setup-runtime.test.js +2 -0
- package/dist/setup.d.ts +1 -2
- package/dist/setup.js +21 -5
- package/dist/setup.test.js +160 -43
- package/dist/sl.test.js +2 -1
- package/dist/standalone-smoke.test.js +2 -3
- package/dist/status-project.js +1 -10
- package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/local-ingest-acceptance.test.js +1 -1
- package/node_modules/@ktx/context/dist/ingest/local-bundle-ingest.test.js +8 -8
- package/node_modules/@ktx/context/dist/ingest/local-bundle-runtime.js +1 -1
- package/node_modules/@ktx/context/dist/ingest/local-bundle-runtime.test.js +3 -3
- package/node_modules/@ktx/context/dist/ingest/local-embedding-provider.integration.test.js +9 -10
- package/node_modules/@ktx/context/dist/llm/local-config.js +2 -15
- package/node_modules/@ktx/context/dist/llm/local-config.test.js +3 -7
- package/node_modules/@ktx/context/dist/project/config.d.ts +0 -5
- package/node_modules/@ktx/context/dist/project/config.js +5 -5
- package/node_modules/@ktx/context/dist/project/config.test.js +4 -7
- package/node_modules/@ktx/context/dist/scan/enrichment-state.test.js +4 -4
- package/node_modules/@ktx/context/dist/scan/index.d.ts +1 -1
- package/node_modules/@ktx/context/dist/scan/local-enrichment.d.ts +2 -6
- package/node_modules/@ktx/context/dist/scan/local-enrichment.js +31 -47
- package/node_modules/@ktx/context/dist/scan/local-enrichment.test.js +35 -18
- package/node_modules/@ktx/context/dist/scan/local-scan.test.js +2 -3
- package/node_modules/@ktx/llm/dist/embedding-provider.d.ts +0 -7
- package/node_modules/@ktx/llm/dist/embedding-provider.js +12 -138
- package/node_modules/@ktx/llm/dist/embedding-provider.test.js +10 -25
- package/node_modules/@ktx/llm/dist/types.d.ts +1 -1
- package/package.json +1 -1
|
@@ -2,12 +2,28 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
+
import { strToU8, zipSync } from 'fflate';
|
|
5
6
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
7
|
import { MISSING_UV_RUNTIME_INSTALL_MESSAGE, doctorManagedPythonRuntime, installManagedPythonRuntime, managedPythonDaemonLayout, managedPythonRuntimeLayout, readManagedPythonRuntimeStatus, verifyRuntimeAsset, } from './managed-python-runtime.js';
|
|
7
|
-
|
|
8
|
+
function runtimeWheelContents(input = {}) {
|
|
9
|
+
const label = input.label ?? 'runtime-wheel';
|
|
10
|
+
const requiresPython = input.requiresPython === null ? [] : [`Requires-Python: ${input.requiresPython ?? '>=3.13'}`];
|
|
11
|
+
return Buffer.from(zipSync({
|
|
12
|
+
'kaelio_ktx-0.1.0.dist-info/METADATA': strToU8([
|
|
13
|
+
'Metadata-Version: 2.4',
|
|
14
|
+
'Name: kaelio-ktx',
|
|
15
|
+
'Version: 0.1.0',
|
|
16
|
+
...requiresPython,
|
|
17
|
+
`Summary: ${label}`,
|
|
18
|
+
'',
|
|
19
|
+
].join('\n')),
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
22
|
+
async function writeAsset(root, options = {}) {
|
|
8
23
|
const assetDir = join(root, 'assets', 'python');
|
|
9
24
|
await mkdir(assetDir, { recursive: true });
|
|
10
25
|
const wheelPath = join(assetDir, 'kaelio_ktx-0.1.0-py3-none-any.whl');
|
|
26
|
+
const contents = options.contents ?? runtimeWheelContents(options);
|
|
11
27
|
await writeFile(wheelPath, contents);
|
|
12
28
|
await writeFile(join(assetDir, 'manifest.json'), `${JSON.stringify({
|
|
13
29
|
schemaVersion: 1,
|
|
@@ -17,7 +33,7 @@ async function writeAsset(root, contents = 'wheel-bytes') {
|
|
|
17
33
|
wheel: {
|
|
18
34
|
file: 'kaelio_ktx-0.1.0-py3-none-any.whl',
|
|
19
35
|
sha256: createHash('sha256').update(contents).digest('hex'),
|
|
20
|
-
bytes:
|
|
36
|
+
bytes: contents.byteLength,
|
|
21
37
|
},
|
|
22
38
|
}, null, 2)}\n`);
|
|
23
39
|
return { assetDir, wheelPath };
|
|
@@ -112,19 +128,20 @@ describe('verifyRuntimeAsset', () => {
|
|
|
112
128
|
await rm(tempDir, { recursive: true, force: true });
|
|
113
129
|
});
|
|
114
130
|
it('reads the manifest and verifies the wheel checksum', async () => {
|
|
115
|
-
const { assetDir, wheelPath } = await writeAsset(tempDir, 'valid-wheel');
|
|
131
|
+
const { assetDir, wheelPath } = await writeAsset(tempDir, { label: 'valid-wheel' });
|
|
116
132
|
const asset = await verifyRuntimeAsset({ assetDir });
|
|
117
133
|
expect(asset.manifest.distributionName).toBe('kaelio-ktx');
|
|
118
134
|
expect(asset.manifest.normalizedName).toBe('kaelio_ktx');
|
|
119
135
|
expect(asset.wheelPath).toBe(wheelPath);
|
|
136
|
+
expect(asset.requiresPython).toEqual({ specifier: '>=3.13', minimumVersion: '3.13' });
|
|
120
137
|
});
|
|
121
138
|
it('rejects a wheel whose checksum does not match the manifest', async () => {
|
|
122
|
-
const { assetDir, wheelPath } = await writeAsset(tempDir, 'original');
|
|
139
|
+
const { assetDir, wheelPath } = await writeAsset(tempDir, { label: 'original' });
|
|
123
140
|
await writeFile(wheelPath, 'tampered');
|
|
124
141
|
await expect(verifyRuntimeAsset({ assetDir })).rejects.toThrow(/Bundled Python runtime wheel checksum mismatch/);
|
|
125
142
|
});
|
|
126
143
|
it('rejects an unsafe wheel filename in the manifest', async () => {
|
|
127
|
-
const { assetDir } = await writeAsset(tempDir, 'valid-wheel');
|
|
144
|
+
const { assetDir } = await writeAsset(tempDir, { label: 'valid-wheel' });
|
|
128
145
|
await writeFile(join(assetDir, 'manifest.json'), `${JSON.stringify({
|
|
129
146
|
schemaVersion: 1,
|
|
130
147
|
distributionName: 'kaelio-ktx',
|
|
@@ -142,6 +159,14 @@ describe('verifyRuntimeAsset', () => {
|
|
|
142
159
|
const assetDir = join(tempDir, 'packages', 'cli', 'assets', 'python');
|
|
143
160
|
await expect(verifyRuntimeAsset({ assetDir })).rejects.toThrow(/Missing bundled Python runtime manifest.*pnpm run artifacts:build/s);
|
|
144
161
|
});
|
|
162
|
+
it('rejects a bundled wheel without Requires-Python metadata', async () => {
|
|
163
|
+
const { assetDir } = await writeAsset(tempDir, { requiresPython: null });
|
|
164
|
+
await expect(verifyRuntimeAsset({ assetDir })).rejects.toThrow(/Bundled Python runtime wheel metadata is missing Requires-Python/);
|
|
165
|
+
});
|
|
166
|
+
it('rejects a bundled wheel without a supported minimum Python version', async () => {
|
|
167
|
+
const { assetDir } = await writeAsset(tempDir, { requiresPython: '<4' });
|
|
168
|
+
await expect(verifyRuntimeAsset({ assetDir })).rejects.toThrow(/Unsupported bundled Python runtime Requires-Python: <4/);
|
|
169
|
+
});
|
|
145
170
|
});
|
|
146
171
|
describe('installManagedPythonRuntime', () => {
|
|
147
172
|
let tempDir;
|
|
@@ -152,7 +177,7 @@ describe('installManagedPythonRuntime', () => {
|
|
|
152
177
|
await rm(tempDir, { recursive: true, force: true });
|
|
153
178
|
});
|
|
154
179
|
it('creates a venv, installs the core wheel, and writes a manifest', async () => {
|
|
155
|
-
const { assetDir } = await writeAsset(tempDir, 'core-wheel');
|
|
180
|
+
const { assetDir } = await writeAsset(tempDir, { label: 'core-wheel' });
|
|
156
181
|
const commands = [];
|
|
157
182
|
const exec = vi.fn(async (command, args) => {
|
|
158
183
|
commands.push({ command, args });
|
|
@@ -168,7 +193,8 @@ describe('installManagedPythonRuntime', () => {
|
|
|
168
193
|
expect(result.status).toBe('installed');
|
|
169
194
|
expect(commands).toEqual([
|
|
170
195
|
{ command: 'uv', args: ['--version'] },
|
|
171
|
-
{ command: 'uv', args: ['
|
|
196
|
+
{ command: 'uv', args: ['python', 'install', '3.13'] },
|
|
197
|
+
{ command: 'uv', args: ['venv', '--python', '3.13', result.layout.venvDir] },
|
|
172
198
|
{
|
|
173
199
|
command: 'uv',
|
|
174
200
|
args: ['pip', 'install', '--python', result.layout.pythonPath, result.asset.wheelPath],
|
|
@@ -181,7 +207,7 @@ describe('installManagedPythonRuntime', () => {
|
|
|
181
207
|
expect(manifest.python.daemonExecutable).toBe(result.layout.daemonPath);
|
|
182
208
|
});
|
|
183
209
|
it('disables repo uv config for managed runtime uv commands', async () => {
|
|
184
|
-
const { assetDir } = await writeAsset(tempDir, 'core-wheel');
|
|
210
|
+
const { assetDir } = await writeAsset(tempDir, { label: 'core-wheel' });
|
|
185
211
|
const commands = [];
|
|
186
212
|
const exec = vi.fn(async (command, args, options) => {
|
|
187
213
|
commands.push({ command, args, env: options?.env });
|
|
@@ -197,12 +223,13 @@ describe('installManagedPythonRuntime', () => {
|
|
|
197
223
|
});
|
|
198
224
|
expect(commands.map((call) => [call.command, call.args[0], call.env?.UV_NO_CONFIG, call.env?.PATH])).toEqual([
|
|
199
225
|
['uv', '--version', '1', '/opt/homebrew/bin'],
|
|
226
|
+
['uv', 'python', '1', '/opt/homebrew/bin'],
|
|
200
227
|
['uv', 'venv', '1', '/opt/homebrew/bin'],
|
|
201
228
|
['uv', 'pip', '1', '/opt/homebrew/bin'],
|
|
202
229
|
]);
|
|
203
230
|
});
|
|
204
231
|
it('installs the local-embeddings extra when requested', async () => {
|
|
205
|
-
const { assetDir } = await writeAsset(tempDir, 'embedding-wheel');
|
|
232
|
+
const { assetDir } = await writeAsset(tempDir, { label: 'embedding-wheel' });
|
|
206
233
|
const commands = [];
|
|
207
234
|
const exec = vi.fn(async (command, args) => {
|
|
208
235
|
commands.push({ command, args });
|
|
@@ -223,7 +250,7 @@ describe('installManagedPythonRuntime', () => {
|
|
|
223
250
|
expect(manifest.features).toEqual(['core', 'local-embeddings']);
|
|
224
251
|
});
|
|
225
252
|
it('fails with the hard-prerequisite message when uv is missing', async () => {
|
|
226
|
-
const { assetDir } = await writeAsset(tempDir, 'core-wheel');
|
|
253
|
+
const { assetDir } = await writeAsset(tempDir, { label: 'core-wheel' });
|
|
227
254
|
const commands = [];
|
|
228
255
|
const exec = vi.fn(async (command, args) => {
|
|
229
256
|
commands.push({ command, args });
|
|
@@ -239,7 +266,7 @@ describe('installManagedPythonRuntime', () => {
|
|
|
239
266
|
expect(commands).toEqual([{ command: 'uv', args: ['--version'] }]);
|
|
240
267
|
});
|
|
241
268
|
it('reuses an existing compatible runtime when force is false', async () => {
|
|
242
|
-
const { assetDir } = await writeAsset(tempDir, 'core-wheel');
|
|
269
|
+
const { assetDir } = await writeAsset(tempDir, { label: 'core-wheel' });
|
|
243
270
|
const exec = vi.fn(async (command, args) => ({
|
|
244
271
|
stdout: command === 'uv' && args[0] === '--version' ? 'uv 0.9.5\n' : '',
|
|
245
272
|
stderr: '',
|
|
@@ -262,13 +289,16 @@ describe('installManagedPythonRuntime', () => {
|
|
|
262
289
|
exec,
|
|
263
290
|
});
|
|
264
291
|
expect(second.status).toBe('ready');
|
|
265
|
-
expect(exec).toHaveBeenCalledTimes(
|
|
292
|
+
expect(exec).toHaveBeenCalledTimes(4);
|
|
266
293
|
});
|
|
267
294
|
it('keeps failed install logs in the versioned runtime directory', async () => {
|
|
268
|
-
const { assetDir } = await writeAsset(tempDir, 'core-wheel');
|
|
295
|
+
const { assetDir } = await writeAsset(tempDir, { label: 'core-wheel' });
|
|
269
296
|
const exec = vi.fn(async (command, args) => {
|
|
270
297
|
if (command === 'uv' && args[0] === 'venv') {
|
|
271
|
-
throw Object.assign(new Error('uv venv failed'), {
|
|
298
|
+
throw Object.assign(new Error('uv venv failed'), {
|
|
299
|
+
stdout: 'creating\n',
|
|
300
|
+
stderr: '× No solution found\n╰─▶ current Python version (3.12.3) does not satisfy Python>=3.13\n',
|
|
301
|
+
});
|
|
272
302
|
}
|
|
273
303
|
return { stdout: command === 'uv' && args[0] === '--version' ? 'uv 0.9.5\n' : '', stderr: '' };
|
|
274
304
|
});
|
|
@@ -278,10 +308,10 @@ describe('installManagedPythonRuntime', () => {
|
|
|
278
308
|
assetDir,
|
|
279
309
|
features: ['core'],
|
|
280
310
|
exec,
|
|
281
|
-
})).rejects.toThrow(/Python
|
|
311
|
+
})).rejects.toThrow(/current Python version \(3\.12\.3\) does not satisfy Python>=3\.13/);
|
|
282
312
|
const log = await readFile(join(tempDir, 'runtime', '0.2.0', 'install.log'), 'utf8');
|
|
283
|
-
expect(log).toContain('$ uv venv');
|
|
284
|
-
expect(log).toContain('
|
|
313
|
+
expect(log).toContain('$ uv venv --python 3.13');
|
|
314
|
+
expect(log).toContain('current Python version (3.12.3) does not satisfy Python>=3.13');
|
|
285
315
|
});
|
|
286
316
|
});
|
|
287
317
|
describe('readManagedPythonRuntimeStatus', () => {
|
|
@@ -302,7 +332,7 @@ describe('readManagedPythonRuntimeStatus', () => {
|
|
|
302
332
|
expect(status.detail).toContain('No runtime manifest');
|
|
303
333
|
});
|
|
304
334
|
it('reports ready when manifest and executables exist', async () => {
|
|
305
|
-
const { assetDir } = await writeAsset(tempDir, 'core-wheel');
|
|
335
|
+
const { assetDir } = await writeAsset(tempDir, { label: 'core-wheel' });
|
|
306
336
|
const exec = vi.fn(async (command, args) => ({
|
|
307
337
|
stdout: command === 'uv' && args[0] === '--version' ? 'uv 0.9.5\n' : '',
|
|
308
338
|
stderr: '',
|
|
@@ -326,7 +356,7 @@ describe('readManagedPythonRuntimeStatus', () => {
|
|
|
326
356
|
expect(status.manifest?.features).toEqual(['core']);
|
|
327
357
|
});
|
|
328
358
|
it('reports broken when an executable is missing', async () => {
|
|
329
|
-
const { assetDir } = await writeAsset(tempDir, 'core-wheel');
|
|
359
|
+
const { assetDir } = await writeAsset(tempDir, { label: 'core-wheel' });
|
|
330
360
|
const exec = vi.fn(async (command, args) => ({
|
|
331
361
|
stdout: command === 'uv' && args[0] === '--version' ? 'uv 0.9.5\n' : '',
|
|
332
362
|
stderr: '',
|
|
@@ -356,7 +386,7 @@ describe('doctorManagedPythonRuntime', () => {
|
|
|
356
386
|
await rm(tempDir, { recursive: true, force: true });
|
|
357
387
|
});
|
|
358
388
|
it('checks uv, bundled assets, and installed runtime status', async () => {
|
|
359
|
-
const { assetDir } = await writeAsset(tempDir, 'core-wheel');
|
|
389
|
+
const { assetDir } = await writeAsset(tempDir, { label: 'core-wheel' });
|
|
360
390
|
const exec = vi.fn(async (command, args) => ({
|
|
361
391
|
stdout: command === 'uv' && args[0] === '--version' ? 'uv 0.9.5\n' : '',
|
|
362
392
|
stderr: '',
|
|
@@ -375,7 +405,7 @@ describe('doctorManagedPythonRuntime', () => {
|
|
|
375
405
|
expect(checks[2]?.fix).toBe('Run: ktx dev runtime install --yes');
|
|
376
406
|
});
|
|
377
407
|
it('reports uv as a hard prerequisite when uv is missing', async () => {
|
|
378
|
-
const { assetDir } = await writeAsset(tempDir, 'core-wheel');
|
|
408
|
+
const { assetDir } = await writeAsset(tempDir, { label: 'core-wheel' });
|
|
379
409
|
const exec = vi.fn(async () => {
|
|
380
410
|
throw new Error('spawn uv ENOENT');
|
|
381
411
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function sanitizeChildProxyEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const NO_PROXY_KEYS = ['NO_PROXY', 'no_proxy'];
|
|
2
|
+
function isIpv6CidrNoProxyEntry(entry) {
|
|
3
|
+
return entry.includes('/') && entry.includes(':');
|
|
4
|
+
}
|
|
5
|
+
function cleanedNoProxyValue(env) {
|
|
6
|
+
const entries = NO_PROXY_KEYS.flatMap((key) => (env[key] ?? '').split(','))
|
|
7
|
+
.map((entry) => entry.trim())
|
|
8
|
+
.filter((entry) => entry.length > 0 && !isIpv6CidrNoProxyEntry(entry));
|
|
9
|
+
if (!NO_PROXY_KEYS.some((key) => env[key] !== undefined)) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
return [...new Set(entries)].join(',');
|
|
13
|
+
}
|
|
14
|
+
export function sanitizeChildProxyEnv(env) {
|
|
15
|
+
const sanitized = { ...env };
|
|
16
|
+
const noProxy = cleanedNoProxyValue(env);
|
|
17
|
+
if (noProxy === undefined) {
|
|
18
|
+
return sanitized;
|
|
19
|
+
}
|
|
20
|
+
sanitized.NO_PROXY = noProxy;
|
|
21
|
+
sanitized.no_proxy = noProxy;
|
|
22
|
+
return sanitized;
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { sanitizeChildProxyEnv } from './proxy-env.js';
|
|
3
|
+
describe('sanitizeChildProxyEnv', () => {
|
|
4
|
+
it('drops IPv6 CIDR no-proxy entries and normalizes both env keys', () => {
|
|
5
|
+
const env = sanitizeChildProxyEnv({
|
|
6
|
+
NO_PROXY: 'localhost,127.0.0.1,127.0.0.0/8,fd07:b51a:cc66:f0::/64,*.orb.local',
|
|
7
|
+
no_proxy: '::1,0.250.250.0/24,fd00::/8,*.orb.internal',
|
|
8
|
+
});
|
|
9
|
+
expect(env.NO_PROXY).toBe('localhost,127.0.0.1,127.0.0.0/8,*.orb.local,::1,0.250.250.0/24,*.orb.internal');
|
|
10
|
+
expect(env.no_proxy).toBe(env.NO_PROXY);
|
|
11
|
+
});
|
|
12
|
+
it('preserves the input object and leaves missing proxy env unset', () => {
|
|
13
|
+
const input = { PATH: '/usr/bin' };
|
|
14
|
+
expect(sanitizeChildProxyEnv(input)).toEqual({ PATH: '/usr/bin' });
|
|
15
|
+
expect(input).toEqual({ PATH: '/usr/bin' });
|
|
16
|
+
});
|
|
17
|
+
});
|
package/dist/runtime.test.js
CHANGED
package/dist/setup-agents.js
CHANGED
|
@@ -965,7 +965,9 @@ export async function runKtxSetupAgentsStep(args, io, deps = {}) {
|
|
|
965
965
|
if (targets.includes('back'))
|
|
966
966
|
return { status: 'back', projectDir: args.projectDir };
|
|
967
967
|
if (targets.length === 0) {
|
|
968
|
-
io.stderr.write(
|
|
968
|
+
io.stderr.write(args.inputMode === 'disabled'
|
|
969
|
+
? 'Run in a TTY, or pass --target <target>.\n'
|
|
970
|
+
: 'Missing agent target: pass --target or use interactive setup.\n');
|
|
969
971
|
return { status: 'missing-input', projectDir: args.projectDir };
|
|
970
972
|
}
|
|
971
973
|
const scopeTargets = targets.filter((target) => target !== 'claude-desktop');
|
|
@@ -163,6 +163,40 @@ describe('setup agents', () => {
|
|
|
163
163
|
expect(await readKtxSetupState(tempDir)).toEqual({ completed_steps: ['agents'] });
|
|
164
164
|
expect(io.stderr()).toBe('');
|
|
165
165
|
});
|
|
166
|
+
it('installs a specified target in non-interactive mode without --yes', async () => {
|
|
167
|
+
const io = makeIo();
|
|
168
|
+
await expect(runKtxSetupAgentsStep({
|
|
169
|
+
projectDir: tempDir,
|
|
170
|
+
inputMode: 'disabled',
|
|
171
|
+
yes: false,
|
|
172
|
+
agents: true,
|
|
173
|
+
target: 'claude-code',
|
|
174
|
+
scope: 'project',
|
|
175
|
+
mode: 'mcp',
|
|
176
|
+
skipAgents: false,
|
|
177
|
+
}, io.io)).resolves.toMatchObject({
|
|
178
|
+
status: 'ready',
|
|
179
|
+
projectDir: tempDir,
|
|
180
|
+
installs: [{ target: 'claude-code', scope: 'project', mode: 'mcp' }],
|
|
181
|
+
});
|
|
182
|
+
await expect(stat(join(tempDir, '.claude/skills/ktx-analytics/SKILL.md'))).resolves.toBeDefined();
|
|
183
|
+
const mcpConfig = JSON.parse(await readFile(join(tempDir, '.mcp.json'), 'utf-8'));
|
|
184
|
+
expect(mcpConfig.mcpServers).toHaveProperty('ktx');
|
|
185
|
+
expect(io.stderr()).toBe('');
|
|
186
|
+
});
|
|
187
|
+
it('prints concrete target guidance when non-interactive agent setup has no target', async () => {
|
|
188
|
+
const io = makeIo();
|
|
189
|
+
await expect(runKtxSetupAgentsStep({
|
|
190
|
+
projectDir: tempDir,
|
|
191
|
+
inputMode: 'disabled',
|
|
192
|
+
yes: false,
|
|
193
|
+
agents: true,
|
|
194
|
+
scope: 'project',
|
|
195
|
+
mode: 'mcp',
|
|
196
|
+
skipAgents: false,
|
|
197
|
+
}, io.io)).resolves.toEqual({ status: 'missing-input', projectDir: tempDir });
|
|
198
|
+
expect(io.stderr()).toBe('Run in a TTY, or pass --target <target>.\n');
|
|
199
|
+
});
|
|
166
200
|
it('prints standalone agent next actions after successful installation', async () => {
|
|
167
201
|
const io = makeIo();
|
|
168
202
|
const result = await runKtxSetupAgentsStep({
|
|
@@ -49,6 +49,7 @@ export interface KtxSetupEmbeddingsDeps {
|
|
|
49
49
|
healthCheck?: (config: KtxEmbeddingConfig) => Promise<KtxEmbeddingHealthCheckResult>;
|
|
50
50
|
ensureLocalEmbeddings?: (options: {
|
|
51
51
|
cliVersion: string;
|
|
52
|
+
projectDir: string;
|
|
52
53
|
installPolicy: KtxManagedPythonInstallPolicy;
|
|
53
54
|
io: KtxCliIo;
|
|
54
55
|
}) => Promise<ManagedLocalEmbeddingsDaemon>;
|
package/dist/setup-embeddings.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { writeFile } from 'node:fs/promises';
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { resolveKtxConfigReference } from '@ktx/context/core';
|
|
3
3
|
import { loadKtxProject, markKtxSetupStateStepComplete, readKtxSetupState, serializeKtxProjectConfig, } from '@ktx/context/project';
|
|
4
4
|
import { runKtxEmbeddingHealthCheck } from '@ktx/llm';
|
|
@@ -20,13 +20,13 @@ const LOCAL_EMBEDDING_BACKEND = 'sentence-transformers';
|
|
|
20
20
|
const EMBEDDING_OPTION_PROMPT_CONTEXT = 'KTX uses embeddings for semantic search over semantic-layer sources, wiki context, schema metadata, ' +
|
|
21
21
|
'and relationship evidence.';
|
|
22
22
|
const LOCAL_EMBEDDING_HEALTH_TIMEOUT_MS = 120_000;
|
|
23
|
+
const LOCAL_EMBEDDING_STDERR_TAIL_LINES = 40;
|
|
23
24
|
function createPromptAdapter() {
|
|
24
25
|
return createKtxSetupPromptAdapter({ selectCancelValue: 'back' });
|
|
25
26
|
}
|
|
26
27
|
async function hasCompletedEmbeddings(projectDir, config) {
|
|
27
28
|
return ((await readKtxSetupState(projectDir)).completed_steps.includes('embeddings') &&
|
|
28
29
|
config.ingest.embeddings.backend !== 'none' &&
|
|
29
|
-
config.ingest.embeddings.backend !== 'deterministic' &&
|
|
30
30
|
typeof config.ingest.embeddings.model === 'string' &&
|
|
31
31
|
config.ingest.embeddings.model.length > 0 &&
|
|
32
32
|
config.ingest.embeddings.dimensions > 0);
|
|
@@ -190,14 +190,33 @@ async function chooseEmbeddingBackend(args, deps) {
|
|
|
190
190
|
}
|
|
191
191
|
return 'back';
|
|
192
192
|
}
|
|
193
|
-
function
|
|
194
|
-
|
|
193
|
+
async function readLocalEmbeddingDaemonStderrTail(stderrLog) {
|
|
194
|
+
if (!stderrLog) {
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const lines = (await readFile(stderrLog, 'utf8'))
|
|
199
|
+
.split(/\r?\n/)
|
|
200
|
+
.map((line) => line.trimEnd())
|
|
201
|
+
.filter((line) => line.trim().length > 0);
|
|
202
|
+
return lines.slice(-LOCAL_EMBEDDING_STDERR_TAIL_LINES);
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function localEmbeddingSetupMessage(message, stderrTail = []) {
|
|
209
|
+
const lines = [
|
|
195
210
|
`Local embedding health check failed: ${message}`,
|
|
196
211
|
'Local embeddings use the KTX-managed Python runtime.',
|
|
197
212
|
'Prepare the runtime with: ktx dev runtime start --feature local-embeddings',
|
|
198
213
|
'Use --yes with setup to install and start the runtime without prompting.',
|
|
199
214
|
'The first run may download Python packages and the all-MiniLM-L6-v2 model.',
|
|
200
|
-
]
|
|
215
|
+
];
|
|
216
|
+
if (stderrTail.length > 0) {
|
|
217
|
+
lines.push('Recent local embeddings daemon stderr:', ...stderrTail);
|
|
218
|
+
}
|
|
219
|
+
return lines.join('\n');
|
|
201
220
|
}
|
|
202
221
|
async function promptAfterLocalEmbeddingFailure(deps) {
|
|
203
222
|
const choice = await (deps.prompts ?? createPromptAdapter()).select({
|
|
@@ -324,8 +343,11 @@ export async function runKtxSetupEmbeddingsStep(args, io, deps = {}) {
|
|
|
324
343
|
return { status: 'ready', projectDir: args.projectDir };
|
|
325
344
|
}
|
|
326
345
|
progress.fail('Embedding test failed');
|
|
346
|
+
const stderrTail = selectedBackend === 'sentence-transformers'
|
|
347
|
+
? await readLocalEmbeddingDaemonStderrTail(managedLocalEmbeddings?.stderrLog)
|
|
348
|
+
: [];
|
|
327
349
|
io.stderr.write(selectedBackend === 'sentence-transformers'
|
|
328
|
-
? `${localEmbeddingSetupMessage(health.message)}\n`
|
|
350
|
+
? `${localEmbeddingSetupMessage(health.message, stderrTail)}\n`
|
|
329
351
|
: `Embedding health check failed: ${health.message}\n`);
|
|
330
352
|
if (args.inputMode === 'disabled') {
|
|
331
353
|
return { status: 'failed', projectDir: args.projectDir };
|
|
@@ -39,9 +39,11 @@ function makePromptAdapter(options) {
|
|
|
39
39
|
cancel: vi.fn(),
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
|
-
function managedDaemon(baseUrl = 'http://127.0.0.1:61234') {
|
|
42
|
+
function managedDaemon(baseUrl = 'http://127.0.0.1:61234', logs = {}) {
|
|
43
43
|
return {
|
|
44
44
|
baseUrl,
|
|
45
|
+
stdoutLog: logs.stdoutLog ?? '/tmp/ktx-daemon.stdout.log',
|
|
46
|
+
stderrLog: logs.stderrLog ?? '/tmp/ktx-daemon.stderr.log',
|
|
45
47
|
env: {
|
|
46
48
|
KTX_MANAGED_SENTENCE_TRANSFORMERS_BASE_URL: baseUrl,
|
|
47
49
|
},
|
|
@@ -251,11 +253,51 @@ describe('setup embeddings step', () => {
|
|
|
251
253
|
expect(result.status).toBe('failed');
|
|
252
254
|
const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8'));
|
|
253
255
|
expect(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8')).not.toContain('completed_steps:');
|
|
254
|
-
expect(config.ingest.embeddings.backend).toBe('
|
|
256
|
+
expect(config.ingest.embeddings.backend).toBe('none');
|
|
255
257
|
expect(io.stderr()).toContain('Local embedding health check failed: 401 invalid api key [redacted]');
|
|
256
258
|
expect(io.stderr()).toContain('Prepare the runtime with: ktx dev runtime start --feature local-embeddings');
|
|
257
259
|
expect(io.stderr()).not.toContain('skip for now');
|
|
258
260
|
});
|
|
261
|
+
it('prints the recent daemon stderr tail when local embedding health check fails', async () => {
|
|
262
|
+
const io = makeIo();
|
|
263
|
+
const stderrLog = join(tempDir, '.ktx', 'runtime', 'daemon.stderr.log');
|
|
264
|
+
await mkdir(join(tempDir, '.ktx', 'runtime'), { recursive: true });
|
|
265
|
+
await writeFile(stderrLog, Array.from({ length: 45 }, (_value, index) => `daemon traceback line ${index + 1}`).join('\n'));
|
|
266
|
+
const result = await runKtxSetupEmbeddingsStep({
|
|
267
|
+
projectDir: tempDir,
|
|
268
|
+
inputMode: 'disabled',
|
|
269
|
+
cliVersion: '0.2.0',
|
|
270
|
+
runtimeInstallPolicy: 'auto',
|
|
271
|
+
skipEmbeddings: false,
|
|
272
|
+
}, io.io, {
|
|
273
|
+
env: {},
|
|
274
|
+
ensureLocalEmbeddings: vi.fn(async () => managedDaemon('http://127.0.0.1:61234', { stderrLog })),
|
|
275
|
+
healthCheck: vi.fn(async () => ({ ok: false, message: 'HTTP 500' })),
|
|
276
|
+
});
|
|
277
|
+
expect(result.status).toBe('failed');
|
|
278
|
+
expect(io.stderr()).toContain('Recent local embeddings daemon stderr:');
|
|
279
|
+
expect(io.stderr()).toContain('daemon traceback line 6');
|
|
280
|
+
expect(io.stderr()).toContain('daemon traceback line 45');
|
|
281
|
+
expect(io.stderr()).not.toContain('daemon traceback line 5');
|
|
282
|
+
});
|
|
283
|
+
it('does not print daemon stderr diagnostics when the log is unavailable or empty', async () => {
|
|
284
|
+
const io = makeIo();
|
|
285
|
+
const result = await runKtxSetupEmbeddingsStep({
|
|
286
|
+
projectDir: tempDir,
|
|
287
|
+
inputMode: 'disabled',
|
|
288
|
+
cliVersion: '0.2.0',
|
|
289
|
+
runtimeInstallPolicy: 'auto',
|
|
290
|
+
skipEmbeddings: false,
|
|
291
|
+
}, io.io, {
|
|
292
|
+
env: {},
|
|
293
|
+
ensureLocalEmbeddings: vi.fn(async () => managedDaemon('http://127.0.0.1:61234', {
|
|
294
|
+
stderrLog: join(tempDir, '.ktx', 'runtime', 'missing.stderr.log'),
|
|
295
|
+
})),
|
|
296
|
+
healthCheck: vi.fn(async () => ({ ok: false, message: 'HTTP 500' })),
|
|
297
|
+
});
|
|
298
|
+
expect(result.status).toBe('failed');
|
|
299
|
+
expect(io.stderr()).not.toContain('Recent local embeddings daemon stderr:');
|
|
300
|
+
});
|
|
259
301
|
it('uses fixed OpenAI defaults and only asks for credentials when OpenAI is selected', async () => {
|
|
260
302
|
const io = makeIo();
|
|
261
303
|
const healthCheck = vi.fn(async () => ({ ok: true }));
|
|
@@ -342,7 +384,7 @@ describe('setup embeddings step', () => {
|
|
|
342
384
|
expect(result.status).toBe('skipped');
|
|
343
385
|
const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8'));
|
|
344
386
|
expect(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8')).not.toContain('completed_steps:');
|
|
345
|
-
expect(config.ingest.embeddings.backend).toBe('
|
|
387
|
+
expect(config.ingest.embeddings.backend).toBe('none');
|
|
346
388
|
});
|
|
347
389
|
it('returns back without writing config when the local health check fails and Back is selected', async () => {
|
|
348
390
|
const prompts = makePromptAdapter({ selectValues: ['sentence-transformers', 'back'] });
|
|
@@ -360,7 +402,7 @@ describe('setup embeddings step', () => {
|
|
|
360
402
|
});
|
|
361
403
|
expect(result.status).toBe('back');
|
|
362
404
|
const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8'));
|
|
363
|
-
expect(config.ingest.embeddings.backend).toBe('
|
|
405
|
+
expect(config.ingest.embeddings.backend).toBe('none');
|
|
364
406
|
});
|
|
365
407
|
it('preserves already completed embeddings setup when no embedding args request changes', async () => {
|
|
366
408
|
await mkdir(join(tempDir, '.ktx'), { recursive: true });
|
package/dist/setup-models.d.ts
CHANGED
package/dist/setup-models.js
CHANGED
|
@@ -312,13 +312,13 @@ function requestedBackend(args) {
|
|
|
312
312
|
if (args.vertexProject || args.vertexLocation) {
|
|
313
313
|
return 'vertex';
|
|
314
314
|
}
|
|
315
|
-
if (args.anthropicApiKeyEnv || args.anthropicApiKeyFile || args.llmModel
|
|
315
|
+
if (args.anthropicApiKeyEnv || args.anthropicApiKeyFile || args.llmModel) {
|
|
316
316
|
return 'anthropic';
|
|
317
317
|
}
|
|
318
318
|
return undefined;
|
|
319
319
|
}
|
|
320
320
|
function requestedModel(args) {
|
|
321
|
-
return args.llmModel
|
|
321
|
+
return args.llmModel;
|
|
322
322
|
}
|
|
323
323
|
async function chooseBackend(args, io, deps) {
|
|
324
324
|
const explicit = requestedBackend(args);
|
|
@@ -701,7 +701,6 @@ export async function runKtxSetupAnthropicModelStep(args, io, deps = {}) {
|
|
|
701
701
|
!args.anthropicApiKeyEnv &&
|
|
702
702
|
!args.anthropicApiKeyFile &&
|
|
703
703
|
!args.llmModel &&
|
|
704
|
-
!args.anthropicModel &&
|
|
705
704
|
!args.vertexProject &&
|
|
706
705
|
!args.vertexLocation) {
|
|
707
706
|
io.stdout.write(`│ LLM ready: yes (${project.config.llm.models.default})\n`);
|
|
@@ -216,7 +216,7 @@ describe('setup Anthropic model step', () => {
|
|
|
216
216
|
projectDir: tempDir,
|
|
217
217
|
inputMode: 'disabled',
|
|
218
218
|
anthropicApiKeyEnv: 'ANTHROPIC_API_KEY', // pragma: allowlist secret
|
|
219
|
-
|
|
219
|
+
llmModel: 'claude-sonnet-4-6',
|
|
220
220
|
skipLlm: false,
|
|
221
221
|
}, io.io, {
|
|
222
222
|
env: { ANTHROPIC_API_KEY: 'sk-ant-test' }, // pragma: allowlist secret
|
|
@@ -253,7 +253,7 @@ describe('setup Anthropic model step', () => {
|
|
|
253
253
|
llmBackend: 'vertex',
|
|
254
254
|
vertexProject: 'local-gcp-project',
|
|
255
255
|
vertexLocation: 'us-east5',
|
|
256
|
-
|
|
256
|
+
llmModel: 'claude-sonnet-4-6',
|
|
257
257
|
skipLlm: false,
|
|
258
258
|
}, io.io, { env: {}, healthCheck, spinner });
|
|
259
259
|
expect(result.status).toBe('ready');
|
|
@@ -476,7 +476,7 @@ describe('setup Anthropic model step', () => {
|
|
|
476
476
|
llmBackend: 'vertex',
|
|
477
477
|
vertexProject: 'kaelio-orbit-looker-20260430',
|
|
478
478
|
vertexLocation: 'us-east5',
|
|
479
|
-
|
|
479
|
+
llmModel: 'claude-sonnet-4-6',
|
|
480
480
|
skipLlm: false,
|
|
481
481
|
}, io.io, {
|
|
482
482
|
env: {},
|
|
@@ -497,7 +497,7 @@ describe('setup Anthropic model step', () => {
|
|
|
497
497
|
projectDir: tempDir,
|
|
498
498
|
inputMode: 'disabled',
|
|
499
499
|
anthropicApiKeyFile: secretPath,
|
|
500
|
-
|
|
500
|
+
llmModel: 'claude-sonnet-4-6',
|
|
501
501
|
skipLlm: false,
|
|
502
502
|
}, io.io, { env: {}, healthCheck });
|
|
503
503
|
expect(result.status).toBe('ready');
|
|
@@ -525,7 +525,7 @@ describe('setup Anthropic model step', () => {
|
|
|
525
525
|
projectDir: tempDir,
|
|
526
526
|
inputMode: 'disabled',
|
|
527
527
|
anthropicApiKeyFile: missingSecretPath,
|
|
528
|
-
|
|
528
|
+
llmModel: 'claude-sonnet-4-6',
|
|
529
529
|
skipLlm: false,
|
|
530
530
|
}, io.io, { env: {}, healthCheck });
|
|
531
531
|
expect(result.status).toBe('missing-input');
|
|
@@ -720,7 +720,7 @@ describe('setup Anthropic model step', () => {
|
|
|
720
720
|
projectDir: tempDir,
|
|
721
721
|
inputMode: 'disabled',
|
|
722
722
|
anthropicApiKeyEnv: 'ANTHROPIC_API_KEY', // pragma: allowlist secret
|
|
723
|
-
|
|
723
|
+
llmModel: 'claude-sonnet-4-6',
|
|
724
724
|
skipLlm: false,
|
|
725
725
|
}, io.io, {
|
|
726
726
|
env: { ANTHROPIC_API_KEY: 'sk-ant-test' }, // pragma: allowlist secret
|
|
@@ -830,8 +830,7 @@ describe('setup Anthropic model step', () => {
|
|
|
830
830
|
' default: claude-sonnet-4-6',
|
|
831
831
|
'ingest:',
|
|
832
832
|
' embeddings:',
|
|
833
|
-
' backend:
|
|
834
|
-
' model: deterministic',
|
|
833
|
+
' backend: none',
|
|
835
834
|
' dimensions: 8',
|
|
836
835
|
].join('\n'), 'utf-8');
|
|
837
836
|
await writeKtxSetupState(tempDir, { completed_steps: ['project', 'llm'] });
|
|
@@ -865,8 +864,7 @@ describe('setup Anthropic model step', () => {
|
|
|
865
864
|
` default: ${fixture.model}`,
|
|
866
865
|
'ingest:',
|
|
867
866
|
' embeddings:',
|
|
868
|
-
' backend:
|
|
869
|
-
' model: deterministic',
|
|
867
|
+
' backend: none',
|
|
870
868
|
' dimensions: 8',
|
|
871
869
|
].join('\n'), 'utf-8');
|
|
872
870
|
await writeKtxSetupState(tempDir, { completed_steps: ['project', 'llm'] });
|
package/dist/setup-project.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { initKtxProject, type KtxLocalProject, loadKtxProject } from '@ktx/context/project';
|
|
2
2
|
import type { KtxCliIo } from './cli-runtime.js';
|
|
3
3
|
import { type KtxSetupPromptOption } from './setup-prompts.js';
|
|
4
|
-
export type KtxSetupProjectMode = 'auto' | '
|
|
4
|
+
export type KtxSetupProjectMode = 'auto' | 'prompt-new';
|
|
5
5
|
export type KtxSetupInputMode = 'auto' | 'disabled';
|
|
6
6
|
export interface KtxSetupProjectArgs {
|
|
7
7
|
projectDir: string;
|
|
@@ -10,11 +10,19 @@ export interface KtxSetupProjectArgs {
|
|
|
10
10
|
yes: boolean;
|
|
11
11
|
allowBack?: boolean;
|
|
12
12
|
}
|
|
13
|
+
export type KtxSetupCreatedProjectCleanup = {
|
|
14
|
+
kind: 'remove-project-dir';
|
|
15
|
+
projectDir: string;
|
|
16
|
+
} | {
|
|
17
|
+
kind: 'remove-ktx-scaffold';
|
|
18
|
+
projectDir: string;
|
|
19
|
+
};
|
|
13
20
|
export type KtxSetupProjectResult = {
|
|
14
21
|
status: 'ready';
|
|
15
22
|
projectDir: string;
|
|
16
23
|
project: KtxLocalProject;
|
|
17
24
|
confirmedCreation?: boolean;
|
|
25
|
+
createdProjectCleanup?: KtxSetupCreatedProjectCleanup;
|
|
18
26
|
} | {
|
|
19
27
|
status: 'back';
|
|
20
28
|
projectDir: string;
|