@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
package/assets/python/{kaelio_ktx-0.1.0-py3-none-any.whl → kaelio_ktx-0.1.1-py3-none-any.whl}
RENAMED
|
Binary file
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"distributionName": "kaelio-ktx",
|
|
4
4
|
"normalizedName": "kaelio_ktx",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.1",
|
|
6
6
|
"wheel": {
|
|
7
|
-
"file": "kaelio_ktx-0.1.
|
|
8
|
-
"sha256": "
|
|
9
|
-
"bytes":
|
|
7
|
+
"file": "kaelio_ktx-0.1.1-py3-none-any.whl",
|
|
8
|
+
"sha256": "2f4089f27519dbc10b0fdc1b03d5324cd69984171589907647961d1edbcab01b",
|
|
9
|
+
"bytes": 80519
|
|
10
10
|
}
|
|
11
11
|
}
|
|
@@ -79,8 +79,6 @@ function shouldShowSetupEntryMenu(options, command) {
|
|
|
79
79
|
return false;
|
|
80
80
|
}
|
|
81
81
|
return ![
|
|
82
|
-
'new',
|
|
83
|
-
'existing',
|
|
84
82
|
'agents',
|
|
85
83
|
'target',
|
|
86
84
|
'global',
|
|
@@ -92,7 +90,6 @@ function shouldShowSetupEntryMenu(options, command) {
|
|
|
92
90
|
'anthropicApiKeyEnv',
|
|
93
91
|
'anthropicApiKeyFile',
|
|
94
92
|
'llmModel',
|
|
95
|
-
'anthropicModel',
|
|
96
93
|
'vertexProject',
|
|
97
94
|
'vertexLocation',
|
|
98
95
|
'skipLlm',
|
|
@@ -100,7 +97,6 @@ function shouldShowSetupEntryMenu(options, command) {
|
|
|
100
97
|
'embeddingApiKeyEnv',
|
|
101
98
|
'embeddingApiKeyFile',
|
|
102
99
|
'skipEmbeddings',
|
|
103
|
-
'newDatabaseConnectionId',
|
|
104
100
|
'databaseUrl',
|
|
105
101
|
'enableQueryHistory',
|
|
106
102
|
'disableQueryHistory',
|
|
@@ -132,8 +128,6 @@ export function registerSetupCommands(program, context) {
|
|
|
132
128
|
.command('setup')
|
|
133
129
|
.description('Set up or resume a local KTX project')
|
|
134
130
|
.addOption(new Option('--project-dir <path>', 'KTX project directory').hideHelp())
|
|
135
|
-
.addOption(new Option('--new', 'Create a new KTX project before setup').hideHelp().default(false))
|
|
136
|
-
.addOption(new Option('--existing', 'Use an existing KTX project').hideHelp().default(false))
|
|
137
131
|
.option('--agents', 'Install agent integration only', false)
|
|
138
132
|
.addOption(new Option('--target <target>', 'Agent target').choices([
|
|
139
133
|
'claude-code',
|
|
@@ -146,13 +140,12 @@ export function registerSetupCommands(program, context) {
|
|
|
146
140
|
.option('--global', 'Install agent integration into the global target scope', false)
|
|
147
141
|
.option('--local', 'Install Claude Code MCP config into the private per-project ~/.claude.json scope', false)
|
|
148
142
|
.addOption(new Option('--skip-agents', 'Leave agent integration incomplete for now').hideHelp().default(false))
|
|
149
|
-
.option('--yes', 'Accept
|
|
143
|
+
.option('--yes', 'Accept project creation and runtime install defaults where setup confirms', false)
|
|
150
144
|
.option('--no-input', 'Disable interactive terminal input')
|
|
151
145
|
.addOption(new Option('--llm-backend <backend>', 'LLM backend').argParser(llmBackend).hideHelp())
|
|
152
146
|
.addOption(new Option('--anthropic-api-key-env <name>', 'Environment variable containing the Anthropic API key').hideHelp())
|
|
153
147
|
.addOption(new Option('--anthropic-api-key-file <path>', 'File containing the Anthropic API key').hideHelp())
|
|
154
148
|
.addOption(new Option('--llm-model <model>', 'LLM model ID or backend model alias').hideHelp())
|
|
155
|
-
.addOption(new Option('--anthropic-model <model>', 'Anthropic model ID to validate and save').hideHelp())
|
|
156
149
|
.addOption(new Option('--vertex-project <project>', 'Google Vertex AI project ID, env:NAME, or file:/path').hideHelp())
|
|
157
150
|
.addOption(new Option('--vertex-location <location>', 'Google Vertex AI location, env:NAME, or file:/path').hideHelp())
|
|
158
151
|
.addOption(new Option('--skip-llm', 'Leave LLM setup incomplete for now').hideHelp().default(false))
|
|
@@ -169,14 +162,6 @@ export function registerSetupCommands(program, context) {
|
|
|
169
162
|
.addOption(new Option('--database-connection-id <id>', 'Existing selected connection id or new connection id')
|
|
170
163
|
.argParser((value, previous) => [...previous, value])
|
|
171
164
|
.default([])
|
|
172
|
-
.hideHelp())
|
|
173
|
-
.addOption(new Option('--new-database-connection-id <id>', 'Connection id for one new database connection')
|
|
174
|
-
.argParser((value) => {
|
|
175
|
-
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(value)) {
|
|
176
|
-
throw new InvalidArgumentError(`Unsafe connection id: ${value}`);
|
|
177
|
-
}
|
|
178
|
-
return value;
|
|
179
|
-
})
|
|
180
165
|
.hideHelp())
|
|
181
166
|
.addOption(new Option('--database-url <url>', 'URL, env:NAME, or file:/path for one new URL-style database connection').hideHelp())
|
|
182
167
|
.addOption(new Option('--database-schema <schema>', 'Database schema to include; repeatable')
|
|
@@ -238,11 +223,6 @@ export function registerSetupCommands(program, context) {
|
|
|
238
223
|
context.setExitCode(1);
|
|
239
224
|
return;
|
|
240
225
|
}
|
|
241
|
-
if (options.llmModel && options.anthropicModel) {
|
|
242
|
-
context.io.stderr.write('Choose only one LLM model flag: --llm-model or --anthropic-model.\n');
|
|
243
|
-
context.setExitCode(1);
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
226
|
if (options.llmBackend &&
|
|
247
227
|
options.llmBackend !== 'anthropic' &&
|
|
248
228
|
(options.anthropicApiKeyEnv || options.anthropicApiKeyFile)) {
|
|
@@ -285,12 +265,17 @@ export function registerSetupCommands(program, context) {
|
|
|
285
265
|
context.setExitCode(1);
|
|
286
266
|
return;
|
|
287
267
|
}
|
|
288
|
-
const
|
|
268
|
+
const creatingDatabaseConnection = options.database.length > 0 || options.databaseUrl !== undefined;
|
|
269
|
+
if (creatingDatabaseConnection && options.databaseConnectionId.length > 1) {
|
|
270
|
+
context.io.stderr.write('Choose only one new database connection id when configuring a database.\n');
|
|
271
|
+
context.setExitCode(1);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
289
274
|
const resolvedAgentScope = options.local ? 'local' : options.global ? 'global' : 'project';
|
|
290
275
|
await runSetupArgs(context, {
|
|
291
276
|
command: 'run',
|
|
292
277
|
projectDir: resolveCommandProjectDir(command),
|
|
293
|
-
mode,
|
|
278
|
+
mode: 'auto',
|
|
294
279
|
agents: options.agents === true,
|
|
295
280
|
...(options.target ? { target: options.target } : {}),
|
|
296
281
|
agentScope: resolvedAgentScope,
|
|
@@ -302,7 +287,6 @@ export function registerSetupCommands(program, context) {
|
|
|
302
287
|
...(options.anthropicApiKeyEnv ? { anthropicApiKeyEnv: options.anthropicApiKeyEnv } : {}),
|
|
303
288
|
...(options.anthropicApiKeyFile ? { anthropicApiKeyFile: options.anthropicApiKeyFile } : {}),
|
|
304
289
|
...(options.llmModel ? { llmModel: options.llmModel } : {}),
|
|
305
|
-
...(options.anthropicModel ? { anthropicModel: options.anthropicModel } : {}),
|
|
306
290
|
...(options.vertexProject ? { vertexProject: options.vertexProject } : {}),
|
|
307
291
|
...(options.vertexLocation ? { vertexLocation: options.vertexLocation } : {}),
|
|
308
292
|
skipLlm: options.skipLlm === true,
|
|
@@ -311,8 +295,12 @@ export function registerSetupCommands(program, context) {
|
|
|
311
295
|
...(options.embeddingApiKeyFile ? { embeddingApiKeyFile: options.embeddingApiKeyFile } : {}),
|
|
312
296
|
skipEmbeddings: options.skipEmbeddings === true,
|
|
313
297
|
...(options.database.length > 0 ? { databaseDrivers: options.database } : {}),
|
|
314
|
-
...(options.databaseConnectionId.length > 0
|
|
315
|
-
|
|
298
|
+
...(options.databaseConnectionId.length > 0 && creatingDatabaseConnection
|
|
299
|
+
? { databaseConnectionId: options.databaseConnectionId[0] }
|
|
300
|
+
: {}),
|
|
301
|
+
...(options.databaseConnectionId.length > 0 && !creatingDatabaseConnection
|
|
302
|
+
? { databaseConnectionIds: options.databaseConnectionId }
|
|
303
|
+
: {}),
|
|
316
304
|
...(options.databaseUrl ? { databaseUrl: options.databaseUrl } : {}),
|
|
317
305
|
databaseSchemas: options.databaseSchema,
|
|
318
306
|
...(options.enableQueryHistory ? { enableQueryHistory: true } : {}),
|
package/dist/doctor.test.js
CHANGED
|
@@ -498,16 +498,15 @@ describe('runKtxDoctor', () => {
|
|
|
498
498
|
' adapters:',
|
|
499
499
|
' - live-database',
|
|
500
500
|
' embeddings:',
|
|
501
|
-
' backend:
|
|
502
|
-
' model: deterministic',
|
|
501
|
+
' backend: none',
|
|
503
502
|
' dimensions: 8',
|
|
504
503
|
'',
|
|
505
504
|
].join('\n'), 'utf-8');
|
|
506
505
|
const testIo = makeIo();
|
|
507
506
|
await expect(runKtxDoctor({ command: 'project', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' }, testIo.io, {})).resolves.toBe(0);
|
|
508
507
|
expect(testIo.stdout()).toContain('Embeddings');
|
|
509
|
-
expect(testIo.stdout()).toContain('
|
|
510
|
-
expect(testIo.stdout()).toContain('semantic search
|
|
508
|
+
expect(testIo.stdout()).toContain('none');
|
|
509
|
+
expect(testIo.stdout()).toContain('semantic search will be skipped');
|
|
511
510
|
delete process.env.ANTHROPIC_API_KEY;
|
|
512
511
|
});
|
|
513
512
|
describe('command: validate', () => {
|
package/dist/index.test.js
CHANGED
|
@@ -318,8 +318,6 @@ describe('runKtxCli', () => {
|
|
|
318
318
|
expect(stdout).not.toContain('setup demo');
|
|
319
319
|
expect(stdout).not.toContain('setup context');
|
|
320
320
|
for (const hiddenFlag of [
|
|
321
|
-
'--new',
|
|
322
|
-
'--existing',
|
|
323
321
|
'--agent-scope',
|
|
324
322
|
'--skip-agents',
|
|
325
323
|
'--llm-backend',
|
|
@@ -328,7 +326,6 @@ describe('runKtxCli', () => {
|
|
|
328
326
|
'--embedding-backend',
|
|
329
327
|
'--database ',
|
|
330
328
|
'--database-connection-id',
|
|
331
|
-
'--new-database-connection-id',
|
|
332
329
|
'--enable-historic-sql',
|
|
333
330
|
'--historic-sql-min-executions',
|
|
334
331
|
'--enable-query-history',
|
|
@@ -612,8 +609,12 @@ describe('runKtxCli', () => {
|
|
|
612
609
|
it('rejects removed setup options', async () => {
|
|
613
610
|
const setup = vi.fn(async () => 0);
|
|
614
611
|
const cases = [
|
|
612
|
+
['setup', '--new'],
|
|
613
|
+
['setup', '--existing'],
|
|
615
614
|
['setup', '--project'],
|
|
616
615
|
['setup', '--agent-scope', 'global'],
|
|
616
|
+
['setup', '--anthropic-model', 'claude-sonnet-4-6'],
|
|
617
|
+
['setup', '--new-database-connection-id', 'warehouse'],
|
|
617
618
|
['setup', '--skip-initial-source-ingest'],
|
|
618
619
|
];
|
|
619
620
|
for (const args of cases) {
|
|
@@ -778,7 +779,7 @@ describe('runKtxCli', () => {
|
|
|
778
779
|
'--no-input',
|
|
779
780
|
'--anthropic-api-key-env',
|
|
780
781
|
'ANTHROPIC_API_KEY',
|
|
781
|
-
'--
|
|
782
|
+
'--llm-model',
|
|
782
783
|
'claude-sonnet-4-6',
|
|
783
784
|
], setupIo.io, { setup })).resolves.toBe(0);
|
|
784
785
|
expect(setup).toHaveBeenCalledWith(expect.objectContaining({
|
|
@@ -787,7 +788,7 @@ describe('runKtxCli', () => {
|
|
|
787
788
|
inputMode: 'disabled',
|
|
788
789
|
cliVersion: '0.1.0-rc.1',
|
|
789
790
|
anthropicApiKeyEnv: 'ANTHROPIC_API_KEY', // pragma: allowlist secret
|
|
790
|
-
|
|
791
|
+
llmModel: 'claude-sonnet-4-6',
|
|
791
792
|
skipLlm: false,
|
|
792
793
|
}), setupIo.io);
|
|
793
794
|
});
|
|
@@ -805,7 +806,7 @@ describe('runKtxCli', () => {
|
|
|
805
806
|
'local-gcp-project',
|
|
806
807
|
'--vertex-location',
|
|
807
808
|
'us-east5',
|
|
808
|
-
'--
|
|
809
|
+
'--llm-model',
|
|
809
810
|
'claude-sonnet-4-6',
|
|
810
811
|
], setupIo.io, { setup })).resolves.toBe(0);
|
|
811
812
|
expect(setup).toHaveBeenCalledWith(expect.objectContaining({
|
|
@@ -816,7 +817,7 @@ describe('runKtxCli', () => {
|
|
|
816
817
|
llmBackend: 'vertex',
|
|
817
818
|
vertexProject: 'local-gcp-project',
|
|
818
819
|
vertexLocation: 'us-east5',
|
|
819
|
-
|
|
820
|
+
llmModel: 'claude-sonnet-4-6',
|
|
820
821
|
skipLlm: false,
|
|
821
822
|
}), setupIo.io);
|
|
822
823
|
});
|
|
@@ -895,7 +896,7 @@ describe('runKtxCli', () => {
|
|
|
895
896
|
'--skip-embeddings',
|
|
896
897
|
'--database',
|
|
897
898
|
'postgres',
|
|
898
|
-
'--
|
|
899
|
+
'--database-connection-id',
|
|
899
900
|
'warehouse',
|
|
900
901
|
'--database-url',
|
|
901
902
|
'env:DATABASE_URL',
|
|
@@ -928,11 +929,26 @@ describe('runKtxCli', () => {
|
|
|
928
929
|
it('dispatches setup database connection ids that match former ingest subcommand names', async () => {
|
|
929
930
|
const testIo = makeIo();
|
|
930
931
|
const setup = vi.fn(async () => 0);
|
|
931
|
-
await expect(runKtxCli(['setup', '--
|
|
932
|
+
await expect(runKtxCli(['setup', '--database-connection-id', 'status', '--no-input'], testIo.io, { setup })).resolves.toBe(0);
|
|
933
|
+
expect(setup).toHaveBeenCalledWith(expect.objectContaining({
|
|
934
|
+
command: 'run',
|
|
935
|
+
databaseConnectionIds: ['status'],
|
|
936
|
+
}), testIo.io);
|
|
937
|
+
});
|
|
938
|
+
it('dispatches non-TTY agents setup with target without requiring --no-input or --yes', async () => {
|
|
939
|
+
const testIo = makeIo({ stdoutIsTty: false });
|
|
940
|
+
const setup = vi.fn(async () => 0);
|
|
941
|
+
await expect(runKtxCli(['--project-dir', tempDir, 'setup', '--agents', '--target', 'claude-code'], testIo.io, { setup })).resolves.toBe(0);
|
|
932
942
|
expect(setup).toHaveBeenCalledWith(expect.objectContaining({
|
|
933
943
|
command: 'run',
|
|
934
|
-
|
|
944
|
+
projectDir: tempDir,
|
|
945
|
+
agents: true,
|
|
946
|
+
target: 'claude-code',
|
|
947
|
+
agentScope: 'project',
|
|
948
|
+
inputMode: 'auto',
|
|
949
|
+
yes: false,
|
|
935
950
|
}), testIo.io);
|
|
951
|
+
expect(testIo.stderr()).toBe('');
|
|
936
952
|
});
|
|
937
953
|
it('dispatches setup source flags', async () => {
|
|
938
954
|
const setup = vi.fn(async () => 0);
|
package/dist/ingest-depth.js
CHANGED
|
@@ -46,7 +46,6 @@ export function deepReadinessGaps(config) {
|
|
|
46
46
|
const embeddings = config.scan.enrichment.embeddings;
|
|
47
47
|
if (!embeddings ||
|
|
48
48
|
embeddings.backend === 'none' ||
|
|
49
|
-
embeddings.backend === 'deterministic' ||
|
|
50
49
|
!embeddings.model ||
|
|
51
50
|
embeddings.dimensions <= 0) {
|
|
52
51
|
gaps.push('scan embeddings');
|
|
@@ -84,7 +84,7 @@ export async function writeMetabaseConfig(projectDir) {
|
|
|
84
84
|
' adapters:',
|
|
85
85
|
' - metabase',
|
|
86
86
|
' embeddings:',
|
|
87
|
-
' backend:
|
|
87
|
+
' backend: none',
|
|
88
88
|
'',
|
|
89
89
|
].join('\n'), 'utf-8');
|
|
90
90
|
}
|
|
@@ -418,7 +418,7 @@ export async function runPublicMetabaseSyncModeCase(tempDir, input) {
|
|
|
418
418
|
' adapters:',
|
|
419
419
|
' - metabase',
|
|
420
420
|
' embeddings:',
|
|
421
|
-
' backend:
|
|
421
|
+
' backend: none',
|
|
422
422
|
'',
|
|
423
423
|
].join('\n'), 'utf-8');
|
|
424
424
|
const project = await loadKtxProject({ projectDir });
|
package/dist/ingest.test.js
CHANGED
|
@@ -182,7 +182,7 @@ describe('runKtxIngest', () => {
|
|
|
182
182
|
await expect(runKtxSetup({
|
|
183
183
|
command: 'run',
|
|
184
184
|
projectDir,
|
|
185
|
-
mode: '
|
|
185
|
+
mode: 'auto',
|
|
186
186
|
agents: false,
|
|
187
187
|
agentScope: 'project',
|
|
188
188
|
skipAgents: true,
|
|
@@ -227,7 +227,7 @@ describe('runKtxIngest', () => {
|
|
|
227
227
|
expect(runIo.stderr()).toContain('ktx ingest requires llm.provider.backend: anthropic, vertex, gateway, or claude-code, or an injected agentRunner.');
|
|
228
228
|
expect(runIo.stderr()).toContain('Configure a local Claude Code session or API-backed LLM, then rerun ingest:');
|
|
229
229
|
expect(runIo.stderr()).toContain(`ktx setup --project-dir ${projectDir} --llm-backend claude-code --no-input`);
|
|
230
|
-
expect(runIo.stderr()).toContain(`ktx setup --project-dir ${projectDir} --llm-backend anthropic --anthropic-api-key-env ANTHROPIC_API_KEY --
|
|
230
|
+
expect(runIo.stderr()).toContain(`ktx setup --project-dir ${projectDir} --llm-backend anthropic --anthropic-api-key-env ANTHROPIC_API_KEY --llm-model claude-sonnet-4-6 --no-input`);
|
|
231
231
|
});
|
|
232
232
|
it('routes metabase scheduled pulls to the fan-out runner and prints child summaries', async () => {
|
|
233
233
|
const projectDir = join(tempDir, 'project');
|
|
@@ -620,7 +620,7 @@ describe('runKtxIngest', () => {
|
|
|
620
620
|
' adapters:',
|
|
621
621
|
' - metabase',
|
|
622
622
|
' embeddings:',
|
|
623
|
-
' backend:
|
|
623
|
+
' backend: none',
|
|
624
624
|
'',
|
|
625
625
|
].join('\n'), 'utf-8');
|
|
626
626
|
const project = await loadKtxProject({ projectDir });
|
|
@@ -1449,7 +1449,7 @@ describe('runKtxIngest', () => {
|
|
|
1449
1449
|
' adapters:',
|
|
1450
1450
|
' - looker',
|
|
1451
1451
|
' embeddings:',
|
|
1452
|
-
' backend:
|
|
1452
|
+
' backend: none',
|
|
1453
1453
|
'',
|
|
1454
1454
|
].join('\n'), 'utf-8');
|
|
1455
1455
|
const project = await loadKtxProject({ projectDir });
|
|
@@ -6,6 +6,8 @@ import { type KtxManagedPythonInstallPolicy, type ManagedPythonCommandRuntime }
|
|
|
6
6
|
import { type ManagedPythonDaemonStartResult } from './managed-python-daemon.js';
|
|
7
7
|
export interface ManagedLocalEmbeddingsDaemon {
|
|
8
8
|
baseUrl: string;
|
|
9
|
+
stdoutLog: string;
|
|
10
|
+
stderrLog: string;
|
|
9
11
|
env: Record<typeof MANAGED_SENTENCE_TRANSFORMERS_BASE_URL_ENV, string>;
|
|
10
12
|
}
|
|
11
13
|
export interface ManagedLocalEmbeddingsOptions {
|
|
@@ -42,6 +42,8 @@ export async function ensureManagedLocalEmbeddingsDaemon(options) {
|
|
|
42
42
|
options.io.stderr.write(`${verb} KTX local embeddings daemon: ${daemon.baseUrl}\n`);
|
|
43
43
|
return {
|
|
44
44
|
baseUrl: daemon.baseUrl,
|
|
45
|
+
stdoutLog: daemon.state.stdoutLog,
|
|
46
|
+
stderrLog: daemon.state.stderrLog,
|
|
45
47
|
env: {
|
|
46
48
|
[MANAGED_SENTENCE_TRANSFORMERS_BASE_URL_ENV]: daemon.baseUrl,
|
|
47
49
|
},
|
|
@@ -128,6 +128,8 @@ describe('ensureManagedLocalEmbeddingsDaemon', () => {
|
|
|
128
128
|
startDaemon,
|
|
129
129
|
})).resolves.toEqual({
|
|
130
130
|
baseUrl: 'http://127.0.0.1:61234',
|
|
131
|
+
stdoutLog: '/work/proj/.ktx/runtime/daemon.stdout.log',
|
|
132
|
+
stderrLog: '/work/proj/.ktx/runtime/daemon.stderr.log',
|
|
131
133
|
env: {
|
|
132
134
|
[MANAGED_SENTENCE_TRANSFORMERS_BASE_URL_ENV]: 'http://127.0.0.1:61234',
|
|
133
135
|
},
|
|
@@ -4,6 +4,7 @@ import { createServer } from 'node:net';
|
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
6
6
|
import { z } from 'zod';
|
|
7
|
+
import { sanitizeChildProxyEnv } from './proxy-env.js';
|
|
7
8
|
const stateSchema = z.object({
|
|
8
9
|
schemaVersion: z.literal(1),
|
|
9
10
|
pid: z.number().int().positive(),
|
|
@@ -118,11 +119,11 @@ export async function startKtxMcpDaemon(options) {
|
|
|
118
119
|
const child = (options.spawnDaemon ?? defaultSpawnDaemon)(process.execPath, args, {
|
|
119
120
|
detached: true,
|
|
120
121
|
stdio: ['ignore', log.fd, log.fd],
|
|
121
|
-
env: {
|
|
122
|
+
env: sanitizeChildProxyEnv({
|
|
122
123
|
...process.env,
|
|
123
124
|
KTX_CLI_VERSION: options.cliVersion,
|
|
124
125
|
...(options.token ? { KTX_MCP_TOKEN: options.token } : {}),
|
|
125
|
-
},
|
|
126
|
+
}),
|
|
126
127
|
});
|
|
127
128
|
if (!child.pid) {
|
|
128
129
|
throw new Error('Failed to start KTX MCP daemon: child process pid was not available.');
|
|
@@ -28,6 +28,7 @@ describe('managed MCP daemon lifecycle', () => {
|
|
|
28
28
|
await mkdir(projectDir, { recursive: true });
|
|
29
29
|
});
|
|
30
30
|
afterEach(async () => {
|
|
31
|
+
vi.unstubAllEnvs();
|
|
31
32
|
await rm(tempDir, { recursive: true, force: true });
|
|
32
33
|
});
|
|
33
34
|
it('uses the spec state and log paths', () => {
|
|
@@ -72,6 +73,30 @@ describe('managed MCP daemon lifecycle', () => {
|
|
|
72
73
|
}));
|
|
73
74
|
expect(JSON.stringify(JSON.parse(await readFile(join(projectDir, '.ktx/mcp.json'), 'utf8')))).not.toContain('secret-token');
|
|
74
75
|
});
|
|
76
|
+
it('sanitizes IPv6 CIDR entries from child NO_PROXY env', async () => {
|
|
77
|
+
vi.stubEnv('NO_PROXY', 'localhost,fd07:b51a:cc66:f0::/64');
|
|
78
|
+
vi.stubEnv('no_proxy', '::1,fd00::/8,*.orb.local');
|
|
79
|
+
const spawnDaemon = vi.fn(() => child(5555));
|
|
80
|
+
await startKtxMcpDaemon({
|
|
81
|
+
projectDir,
|
|
82
|
+
cliVersion: '0.0.0-test',
|
|
83
|
+
host: '127.0.0.1',
|
|
84
|
+
port: 7879,
|
|
85
|
+
allowedHosts: [],
|
|
86
|
+
allowedOrigins: [],
|
|
87
|
+
binPath: '/repo/packages/cli/dist/bin.js',
|
|
88
|
+
spawnDaemon,
|
|
89
|
+
processAlive: vi.fn(() => false),
|
|
90
|
+
portAvailable: vi.fn(async () => true),
|
|
91
|
+
now: () => new Date('2026-05-14T00:00:00.000Z'),
|
|
92
|
+
});
|
|
93
|
+
const env = spawnDaemon.mock.calls[0]?.[2].env;
|
|
94
|
+
if (!env) {
|
|
95
|
+
throw new Error('Expected MCP daemon spawn env');
|
|
96
|
+
}
|
|
97
|
+
expect(env.NO_PROXY).toBe('localhost,::1,*.orb.local');
|
|
98
|
+
expect(env.no_proxy).toBe(env.NO_PROXY);
|
|
99
|
+
});
|
|
75
100
|
it('returns already-running without spawning when the daemon is alive at the same host/port', async () => {
|
|
76
101
|
await mkdir(join(projectDir, '.ktx'), { recursive: true });
|
|
77
102
|
await writeFile(join(projectDir, '.ktx/mcp.json'), `${JSON.stringify(state(projectDir), null, 2)}\n`);
|
|
@@ -81,6 +81,7 @@ function installResult(features = ['core']) {
|
|
|
81
81
|
asset: {
|
|
82
82
|
manifest: installedManifest.asset,
|
|
83
83
|
wheelPath: '/assets/python/kaelio_ktx-0.2.0-py3-none-any.whl',
|
|
84
|
+
requiresPython: { specifier: '>=3.13', minimumVersion: '3.13' },
|
|
84
85
|
},
|
|
85
86
|
manifest: installedManifest,
|
|
86
87
|
};
|
|
@@ -5,6 +5,7 @@ import { setTimeout as delay } from 'node:timers/promises';
|
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
import { installManagedPythonRuntime, managedPythonDaemonLayout, runtimeFeatureSchema, } from './managed-python-runtime.js';
|
|
8
|
+
import { sanitizeChildProxyEnv } from './proxy-env.js';
|
|
8
9
|
const execFileAsync = promisify(execFile);
|
|
9
10
|
const daemonStateSchema = z.object({
|
|
10
11
|
schemaVersion: z.literal(1),
|
|
@@ -483,10 +484,10 @@ export async function startManagedPythonDaemon(options) {
|
|
|
483
484
|
const child = spawnDaemon(installed.manifest.python.daemonExecutable, ['serve-http', '--host', '127.0.0.1', '--port', String(port)], {
|
|
484
485
|
detached: true,
|
|
485
486
|
stdio: ['ignore', stdout.fd, stderr.fd],
|
|
486
|
-
env: {
|
|
487
|
+
env: sanitizeChildProxyEnv({
|
|
487
488
|
...process.env,
|
|
488
489
|
KTX_DAEMON_VERSION: options.cliVersion,
|
|
489
|
-
},
|
|
490
|
+
}),
|
|
490
491
|
});
|
|
491
492
|
child.unref();
|
|
492
493
|
if (!child.pid) {
|
|
@@ -59,6 +59,7 @@ function installResult(root, features = ['core']) {
|
|
|
59
59
|
asset: {
|
|
60
60
|
manifest: manifest(root, features).asset,
|
|
61
61
|
wheelPath: join(root, 'assets', 'python', 'kaelio_ktx-0.2.0-py3-none-any.whl'),
|
|
62
|
+
requiresPython: { specifier: '>=3.13', minimumVersion: '3.13' },
|
|
62
63
|
},
|
|
63
64
|
manifest: manifest(root, features),
|
|
64
65
|
};
|
|
@@ -105,6 +106,7 @@ describe('managed Python daemon lifecycle', () => {
|
|
|
105
106
|
tempDir = await mkdtemp(join(tmpdir(), 'ktx-managed-daemon-'));
|
|
106
107
|
});
|
|
107
108
|
afterEach(async () => {
|
|
109
|
+
vi.unstubAllEnvs();
|
|
108
110
|
await rm(tempDir, { recursive: true, force: true });
|
|
109
111
|
});
|
|
110
112
|
it('reports stopped when no daemon state exists', async () => {
|
|
@@ -150,6 +152,24 @@ describe('managed Python daemon lifecycle', () => {
|
|
|
150
152
|
stderrLog: layout(tempDir).daemonStderrPath,
|
|
151
153
|
});
|
|
152
154
|
});
|
|
155
|
+
it('sanitizes IPv6 CIDR entries from child NO_PROXY env', async () => {
|
|
156
|
+
vi.stubEnv('NO_PROXY', 'localhost,fd07:b51a:cc66:f0::/64,127.0.0.0/8');
|
|
157
|
+
vi.stubEnv('no_proxy', '::1,fd00::/8,*.orb.local');
|
|
158
|
+
const spawnDaemon = makeSpawn(5555);
|
|
159
|
+
await startManagedPythonDaemon({
|
|
160
|
+
...daemonOptionsBase(tempDir),
|
|
161
|
+
features: ['local-embeddings'],
|
|
162
|
+
installRuntime: vi.fn(async () => installResult(tempDir, ['core', 'local-embeddings'])),
|
|
163
|
+
spawnDaemon,
|
|
164
|
+
fetch: makeFetch(),
|
|
165
|
+
allocatePort: vi.fn(async () => 61234),
|
|
166
|
+
now: () => new Date('2026-05-11T00:00:00.000Z'),
|
|
167
|
+
pollIntervalMs: 1,
|
|
168
|
+
});
|
|
169
|
+
const env = vi.mocked(spawnDaemon).mock.calls[0]?.[2].env;
|
|
170
|
+
expect(env?.NO_PROXY).toBe('localhost,127.0.0.0/8,::1,*.orb.local');
|
|
171
|
+
expect(env?.no_proxy).toBe(env?.NO_PROXY);
|
|
172
|
+
});
|
|
153
173
|
it('makes a final health probe before reporting startup failure', async () => {
|
|
154
174
|
const spawnDaemon = makeSpawn(5556);
|
|
155
175
|
const installRuntime = vi.fn(async () => installResult(tempDir));
|
|
@@ -75,6 +75,10 @@ export interface ManagedPythonDaemonLayout extends ManagedPythonRuntimeLayout {
|
|
|
75
75
|
export interface ManagedRuntimeAsset {
|
|
76
76
|
manifest: KtxRuntimeAssetManifest;
|
|
77
77
|
wheelPath: string;
|
|
78
|
+
requiresPython: {
|
|
79
|
+
specifier: string;
|
|
80
|
+
minimumVersion: string;
|
|
81
|
+
};
|
|
78
82
|
}
|
|
79
83
|
export type ManagedPythonRuntimeExec = (command: string, args: string[], options?: {
|
|
80
84
|
cwd?: string;
|
|
@@ -5,6 +5,7 @@ import { homedir } from 'node:os';
|
|
|
5
5
|
import { basename, join } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { promisify } from 'node:util';
|
|
8
|
+
import { strFromU8, unzipSync } from 'fflate';
|
|
8
9
|
import { z } from 'zod';
|
|
9
10
|
const execFileAsync = promisify(execFile);
|
|
10
11
|
export const runtimeFeatureSchema = z.enum(['core', 'local-embeddings']);
|
|
@@ -100,6 +101,35 @@ async function readJsonFile(path) {
|
|
|
100
101
|
function isErrnoException(error, code) {
|
|
101
102
|
return typeof error === 'object' && error !== null && 'code' in error && error.code === code;
|
|
102
103
|
}
|
|
104
|
+
function parseRequiresPythonFromWheel(input) {
|
|
105
|
+
let files;
|
|
106
|
+
try {
|
|
107
|
+
files = unzipSync(new Uint8Array(input.contents));
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
throw new Error(`Unable to read bundled Python runtime wheel metadata: ${error instanceof Error ? error.message : String(error)}`);
|
|
111
|
+
}
|
|
112
|
+
const metadataEntry = Object.entries(files).find(([path]) => path.endsWith('.dist-info/METADATA'));
|
|
113
|
+
if (!metadataEntry) {
|
|
114
|
+
throw new Error(`Bundled Python runtime wheel metadata is missing: ${input.wheelPath}`);
|
|
115
|
+
}
|
|
116
|
+
const metadata = strFromU8(metadataEntry[1]);
|
|
117
|
+
const requiresPython = metadata
|
|
118
|
+
.split(/\r?\n/)
|
|
119
|
+
.map((line) => line.match(/^Requires-Python:\s*(.+)\s*$/i)?.[1]?.trim())
|
|
120
|
+
.find((value) => typeof value === 'string' && value.length > 0);
|
|
121
|
+
if (!requiresPython) {
|
|
122
|
+
throw new Error('Bundled Python runtime wheel metadata is missing Requires-Python');
|
|
123
|
+
}
|
|
124
|
+
const minimumMatch = requiresPython.match(/(?:^|[,\s])>=\s*([0-9]+)\.([0-9]+)(?:\.[0-9]+)?\b/);
|
|
125
|
+
if (!minimumMatch) {
|
|
126
|
+
throw new Error(`Unsupported bundled Python runtime Requires-Python: ${requiresPython}`);
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
specifier: requiresPython,
|
|
130
|
+
minimumVersion: `${minimumMatch[1]}.${minimumMatch[2]}`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
103
133
|
export async function verifyRuntimeAsset(input) {
|
|
104
134
|
const manifestPath = join(input.assetDir, 'manifest.json');
|
|
105
135
|
let manifestData;
|
|
@@ -124,7 +154,7 @@ export async function verifyRuntimeAsset(input) {
|
|
|
124
154
|
if (sha256 !== manifest.wheel.sha256 || wheel.byteLength !== manifest.wheel.bytes) {
|
|
125
155
|
throw new Error(`Bundled Python runtime wheel checksum mismatch: ${wheelPath}`);
|
|
126
156
|
}
|
|
127
|
-
return { manifest, wheelPath };
|
|
157
|
+
return { manifest, wheelPath, requiresPython: parseRequiresPythonFromWheel({ wheelPath, contents: wheel }) };
|
|
128
158
|
}
|
|
129
159
|
function normalizeFeatures(features) {
|
|
130
160
|
const requested = new Set(['core', ...features]);
|
|
@@ -155,6 +185,13 @@ function errorOutput(error) {
|
|
|
155
185
|
stderr: typeof value.stderr === 'string' ? value.stderr : '',
|
|
156
186
|
};
|
|
157
187
|
}
|
|
188
|
+
function installFailureMessage(input) {
|
|
189
|
+
const output = [input.stderr.trim(), input.stdout.trim()].filter((part) => part.length > 0).join('\n');
|
|
190
|
+
if (!output) {
|
|
191
|
+
return `Python runtime install failed. Install log: ${input.logPath}`;
|
|
192
|
+
}
|
|
193
|
+
return `Python runtime install failed.\n${output}\nInstall log: ${input.logPath}`;
|
|
194
|
+
}
|
|
158
195
|
async function runLogged(input) {
|
|
159
196
|
await appendFile(input.logPath, `$ ${input.command} ${input.args.join(' ')}\n`);
|
|
160
197
|
try {
|
|
@@ -175,7 +212,7 @@ async function runLogged(input) {
|
|
|
175
212
|
if (output.stderr) {
|
|
176
213
|
await appendFile(input.logPath, output.stderr.endsWith('\n') ? output.stderr : `${output.stderr}\n`);
|
|
177
214
|
}
|
|
178
|
-
throw new Error(
|
|
215
|
+
throw new Error(installFailureMessage({ logPath: input.logPath, stdout: output.stdout, stderr: output.stderr }));
|
|
179
216
|
}
|
|
180
217
|
}
|
|
181
218
|
function managedRuntimeUvEnv(baseEnv) {
|
|
@@ -214,7 +251,14 @@ export async function installManagedPythonRuntime(options) {
|
|
|
214
251
|
exec,
|
|
215
252
|
logPath: layout.installLogPath,
|
|
216
253
|
command: 'uv',
|
|
217
|
-
args: ['
|
|
254
|
+
args: ['python', 'install', asset.requiresPython.minimumVersion],
|
|
255
|
+
env: uvEnv,
|
|
256
|
+
});
|
|
257
|
+
await runLogged({
|
|
258
|
+
exec,
|
|
259
|
+
logPath: layout.installLogPath,
|
|
260
|
+
command: 'uv',
|
|
261
|
+
args: ['venv', '--python', asset.requiresPython.minimumVersion, layout.venvDir],
|
|
218
262
|
env: uvEnv,
|
|
219
263
|
});
|
|
220
264
|
const wheelSpec = features.includes('local-embeddings') ? `${asset.wheelPath}[local-embeddings]` : asset.wheelPath;
|