@kaelio/ktx 0.1.0 → 0.2.0

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 (122) hide show
  1. package/assets/python/{kaelio_ktx-0.1.0-py3-none-any.whl → kaelio_ktx-0.2.0-py3-none-any.whl} +0 -0
  2. package/assets/python/manifest.json +4 -4
  3. package/dist/admin-reindex.d.ts +15 -0
  4. package/dist/admin-reindex.js +168 -0
  5. package/dist/admin-reindex.test.js +116 -0
  6. package/dist/{dev.d.ts → admin.d.ts} +1 -1
  7. package/dist/{dev.js → admin.js} +14 -12
  8. package/dist/admin.test.d.ts +1 -0
  9. package/dist/{dev.test.js → admin.test.js} +36 -31
  10. package/dist/cli-program.js +7 -7
  11. package/dist/cli-program.test.js +1 -1
  12. package/dist/cli-runtime.d.ts +2 -0
  13. package/dist/commands/connection-commands.js +11 -10
  14. package/dist/commands/connection-selection.d.ts +11 -0
  15. package/dist/commands/connection-selection.js +9 -0
  16. package/dist/commands/ingest-commands.js +32 -26
  17. package/dist/commands/knowledge-commands.js +17 -28
  18. package/dist/commands/mcp-commands.js +17 -11
  19. package/dist/commands/setup-commands.js +14 -26
  20. package/dist/commands/sl-commands.js +27 -32
  21. package/dist/doctor.test.js +7 -8
  22. package/dist/example-smoke.test.js +3 -3
  23. package/dist/index.test.js +102 -70
  24. package/dist/ingest-depth.js +0 -1
  25. package/dist/ingest.test-utils.js +2 -2
  26. package/dist/ingest.test.js +4 -4
  27. package/dist/io/print-list.test.js +4 -4
  28. package/dist/knowledge.js +1 -1
  29. package/dist/managed-local-embeddings.d.ts +2 -0
  30. package/dist/managed-local-embeddings.js +2 -0
  31. package/dist/managed-local-embeddings.test.js +2 -0
  32. package/dist/managed-mcp-daemon.js +3 -2
  33. package/dist/managed-mcp-daemon.test.js +25 -0
  34. package/dist/managed-python-command.js +2 -2
  35. package/dist/managed-python-command.test.js +4 -3
  36. package/dist/managed-python-daemon.js +3 -2
  37. package/dist/managed-python-daemon.test.js +20 -0
  38. package/dist/managed-python-runtime.d.ts +5 -1
  39. package/dist/managed-python-runtime.js +50 -6
  40. package/dist/managed-python-runtime.test.js +53 -23
  41. package/dist/memory-flow-tui.test.js +2 -2
  42. package/dist/next-steps.d.ts +6 -6
  43. package/dist/next-steps.js +4 -4
  44. package/dist/next-steps.test.js +5 -5
  45. package/dist/print-command-tree.test.js +1 -1
  46. package/dist/proxy-env.d.ts +1 -0
  47. package/dist/proxy-env.js +23 -0
  48. package/dist/proxy-env.test.d.ts +1 -0
  49. package/dist/proxy-env.test.js +17 -0
  50. package/dist/public-ingest.js +3 -5
  51. package/dist/public-ingest.test.js +7 -3
  52. package/dist/runtime.test.js +2 -1
  53. package/dist/scan.test.js +2 -2
  54. package/dist/setup-agents.js +6 -4
  55. package/dist/setup-agents.test.js +35 -1
  56. package/dist/setup-embeddings.d.ts +1 -0
  57. package/dist/setup-embeddings.js +29 -7
  58. package/dist/setup-embeddings.test.js +49 -7
  59. package/dist/setup-models.d.ts +0 -1
  60. package/dist/setup-models.js +2 -3
  61. package/dist/setup-models.test.js +8 -10
  62. package/dist/setup-project.d.ts +9 -1
  63. package/dist/setup-project.js +52 -25
  64. package/dist/setup-project.test.js +8 -8
  65. package/dist/setup-runtime.test.js +4 -2
  66. package/dist/setup.d.ts +1 -2
  67. package/dist/setup.js +21 -5
  68. package/dist/setup.test.js +160 -43
  69. package/dist/sl.js +1 -1
  70. package/dist/sl.test.js +2 -1
  71. package/dist/standalone-smoke.test.js +8 -5
  72. package/dist/status-project.js +1 -10
  73. package/node_modules/@ktx/context/dist/index-sync/index.d.ts +2 -0
  74. package/node_modules/@ktx/context/dist/index-sync/index.js +1 -0
  75. package/node_modules/@ktx/context/dist/index-sync/reindex.d.ts +20 -0
  76. package/node_modules/@ktx/context/dist/index-sync/reindex.js +141 -0
  77. package/node_modules/@ktx/context/dist/index-sync/reindex.test.d.ts +1 -0
  78. package/node_modules/@ktx/context/dist/index-sync/reindex.test.js +139 -0
  79. package/node_modules/@ktx/context/dist/index-sync/types.d.ts +29 -0
  80. package/node_modules/@ktx/context/dist/index-sync/types.js +1 -0
  81. package/node_modules/@ktx/context/dist/index.d.ts +1 -0
  82. package/node_modules/@ktx/context/dist/index.js +1 -0
  83. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/local-ingest-acceptance.test.js +1 -1
  84. package/node_modules/@ktx/context/dist/ingest/local-bundle-ingest.test.js +8 -8
  85. package/node_modules/@ktx/context/dist/ingest/local-bundle-runtime.js +4 -1
  86. package/node_modules/@ktx/context/dist/ingest/local-bundle-runtime.test.js +3 -3
  87. package/node_modules/@ktx/context/dist/ingest/local-embedding-provider.integration.test.js +9 -10
  88. package/node_modules/@ktx/context/dist/ingest/memory-flow/schema.d.ts +2 -2
  89. package/node_modules/@ktx/context/dist/ingest/report-snapshot.d.ts +2 -2
  90. package/node_modules/@ktx/context/dist/llm/local-config.js +2 -15
  91. package/node_modules/@ktx/context/dist/llm/local-config.test.js +3 -7
  92. package/node_modules/@ktx/context/dist/memory/local-memory.js +9 -3
  93. package/node_modules/@ktx/context/dist/project/config.d.ts +0 -5
  94. package/node_modules/@ktx/context/dist/project/config.js +5 -5
  95. package/node_modules/@ktx/context/dist/project/config.test.js +4 -7
  96. package/node_modules/@ktx/context/dist/scan/enrichment-state.test.js +4 -4
  97. package/node_modules/@ktx/context/dist/scan/index.d.ts +1 -1
  98. package/node_modules/@ktx/context/dist/scan/local-enrichment.d.ts +2 -6
  99. package/node_modules/@ktx/context/dist/scan/local-enrichment.js +31 -47
  100. package/node_modules/@ktx/context/dist/scan/local-enrichment.test.js +35 -18
  101. package/node_modules/@ktx/context/dist/scan/local-scan.test.js +2 -3
  102. package/node_modules/@ktx/context/dist/sl/ports.d.ts +3 -3
  103. package/node_modules/@ktx/context/dist/sl/sl-search.service.d.ts +3 -2
  104. package/node_modules/@ktx/context/dist/sl/sl-search.service.js +47 -45
  105. package/node_modules/@ktx/context/dist/sl/sl-search.service.test.js +61 -0
  106. package/node_modules/@ktx/context/dist/sl/sqlite-sl-sources-index.d.ts +4 -3
  107. package/node_modules/@ktx/context/dist/sl/sqlite-sl-sources-index.js +15 -5
  108. package/node_modules/@ktx/context/dist/sl/sqlite-sl-sources-index.test.js +24 -0
  109. package/node_modules/@ktx/context/dist/wiki/knowledge-wiki.service.d.ts +3 -2
  110. package/node_modules/@ktx/context/dist/wiki/knowledge-wiki.service.js +62 -51
  111. package/node_modules/@ktx/context/dist/wiki/knowledge-wiki.service.test.js +59 -3
  112. package/node_modules/@ktx/context/dist/wiki/ports.d.ts +3 -3
  113. package/node_modules/@ktx/context/dist/wiki/sqlite-knowledge-index.d.ts +33 -0
  114. package/node_modules/@ktx/context/dist/wiki/sqlite-knowledge-index.js +155 -2
  115. package/node_modules/@ktx/context/dist/wiki/sqlite-knowledge-index.test.js +26 -0
  116. package/node_modules/@ktx/context/package.json +5 -0
  117. package/node_modules/@ktx/llm/dist/embedding-provider.d.ts +0 -7
  118. package/node_modules/@ktx/llm/dist/embedding-provider.js +12 -138
  119. package/node_modules/@ktx/llm/dist/embedding-provider.test.js +10 -25
  120. package/node_modules/@ktx/llm/dist/types.d.ts +1 -1
  121. package/package.json +1 -1
  122. /package/dist/{dev.test.d.ts → admin-reindex.test.d.ts} +0 -0
@@ -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;
@@ -108,7 +112,7 @@ export interface ManagedPythonRuntimeDoctorCheck {
108
112
  detail: string;
109
113
  fix?: string;
110
114
  }
111
- export declare const MISSING_UV_RUNTIME_INSTALL_MESSAGE = "uv is required to install the KTX Python runtime. KTX does not download uv automatically. Install uv, make sure it is on PATH, and retry: ktx dev runtime install --yes";
115
+ export declare const MISSING_UV_RUNTIME_INSTALL_MESSAGE = "uv is required to install the KTX Python runtime. KTX does not download uv automatically. Install uv, make sure it is on PATH, and retry: ktx admin runtime install --yes";
112
116
  export declare function managedPythonRuntimeLayout(options: ManagedPythonRuntimeLayoutOptions): ManagedPythonRuntimeLayout;
113
117
  export declare function managedPythonDaemonLayout(options: ManagedPythonDaemonLayoutOptions): ManagedPythonDaemonLayout;
114
118
  export declare function verifyRuntimeAsset(input: {
@@ -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']);
@@ -31,7 +32,7 @@ const installedRuntimeManifestSchema = z.object({
31
32
  }),
32
33
  installLog: z.string().min(1),
33
34
  });
34
- export const MISSING_UV_RUNTIME_INSTALL_MESSAGE = 'uv is required to install the KTX Python runtime. KTX does not download uv automatically. Install uv, make sure it is on PATH, and retry: ktx dev runtime install --yes';
35
+ export const MISSING_UV_RUNTIME_INSTALL_MESSAGE = 'uv is required to install the KTX Python runtime. KTX does not download uv automatically. Install uv, make sure it is on PATH, and retry: ktx admin runtime install --yes';
35
36
  function defaultAssetDir() {
36
37
  return fileURLToPath(new URL('../assets/python/', import.meta.url));
37
38
  }
@@ -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(`Python runtime install failed. Install log: ${input.logPath}`);
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: ['venv', layout.venvDir],
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;
@@ -287,7 +331,7 @@ export async function doctorManagedPythonRuntime(options) {
287
331
  id: 'uv',
288
332
  label: 'uv',
289
333
  detail: error instanceof Error ? error.message : String(error),
290
- fix: 'Install uv, make sure it is on PATH, and run: ktx dev runtime install --yes',
334
+ fix: 'Install uv, make sure it is on PATH, and run: ktx admin runtime install --yes',
291
335
  }));
292
336
  }
293
337
  try {
@@ -307,7 +351,7 @@ export async function doctorManagedPythonRuntime(options) {
307
351
  id: 'runtime',
308
352
  label: 'Managed Python runtime',
309
353
  detail: status.detail,
310
- ...(status.kind === 'ready' ? {} : { fix: 'Run: ktx dev runtime install --yes' }),
354
+ ...(status.kind === 'ready' ? {} : { fix: 'Run: ktx admin runtime install --yes' }),
311
355
  }));
312
356
  return checks;
313
357
  }
@@ -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
- async function writeAsset(root, contents = 'wheel-bytes') {
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: Buffer.byteLength(contents),
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: ['venv', result.layout.venvDir] },
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(3);
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'), { stdout: 'creating\n', stderr: 'bad python\n' });
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 runtime install failed/);
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('bad python');
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: '',
@@ -372,10 +402,10 @@ describe('doctorManagedPythonRuntime', () => {
372
402
  ['asset', 'pass'],
373
403
  ['runtime', 'fail'],
374
404
  ]);
375
- expect(checks[2]?.fix).toBe('Run: ktx dev runtime install --yes');
405
+ expect(checks[2]?.fix).toBe('Run: ktx admin 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
  });
@@ -390,7 +420,7 @@ describe('doctorManagedPythonRuntime', () => {
390
420
  label: 'uv',
391
421
  status: 'fail',
392
422
  detail: MISSING_UV_RUNTIME_INSTALL_MESSAGE,
393
- fix: 'Install uv, make sure it is on PATH, and run: ktx dev runtime install --yes',
423
+ fix: 'Install uv, make sure it is on PATH, and run: ktx admin runtime install --yes',
394
424
  });
395
425
  });
396
426
  });
@@ -145,8 +145,8 @@ describe('MemoryFlowTuiApp', () => {
145
145
  expect(frame).toContain('order lifecycle');
146
146
  expect(frame).toContain('customer metrics');
147
147
  expect(frame).toContain('KTX finished ingesting your data');
148
- expect(frame).toContain('ktx sl list');
149
- expect(frame).toContain('ktx wiki list');
148
+ expect(frame).toContain('ktx sl');
149
+ expect(frame).toContain('ktx wiki');
150
150
  expect(frame).not.toContain('ktx serve --mcp stdio --user-id local');
151
151
  expect(frame).not.toContain(['ktx', 'ask'].join(' '));
152
152
  expect(frame).not.toContain(['ktx', 'mcp'].join(' '));
@@ -1,6 +1,6 @@
1
1
  export declare const KTX_CONTEXT_BUILD_COMMANDS: readonly [{
2
- readonly command: "ktx ingest --all";
3
- readonly description: "Build or refresh agent-ready context from configured connections";
2
+ readonly command: "ktx ingest";
3
+ readonly description: "Build or refresh agent-ready context from all configured connections";
4
4
  }, {
5
5
  readonly command: "ktx status";
6
6
  readonly description: "Check setup and context readiness";
@@ -9,20 +9,20 @@ export declare const KTX_NEXT_STEP_DIRECT_COMMANDS: readonly [{
9
9
  readonly command: "ktx status --json";
10
10
  readonly description: "Verify project setup and context readiness";
11
11
  }, {
12
- readonly command: "ktx sl list";
12
+ readonly command: "ktx sl";
13
13
  readonly description: "Inspect generated semantic-layer sources";
14
14
  }, {
15
- readonly command: "ktx wiki list";
15
+ readonly command: "ktx wiki";
16
16
  readonly description: "Inspect generated wiki pages";
17
17
  }];
18
18
  export declare const KTX_NEXT_STEP_COMMANDS: readonly [{
19
19
  readonly command: "ktx status --json";
20
20
  readonly description: "Verify project setup and context readiness";
21
21
  }, {
22
- readonly command: "ktx sl list";
22
+ readonly command: "ktx sl";
23
23
  readonly description: "Inspect generated semantic-layer sources";
24
24
  }, {
25
- readonly command: "ktx wiki list";
25
+ readonly command: "ktx wiki";
26
26
  readonly description: "Inspect generated wiki pages";
27
27
  }];
28
28
  export declare const KTX_NEXT_STEP_COMMAND_WIDTH: number;
@@ -1,7 +1,7 @@
1
1
  export const KTX_CONTEXT_BUILD_COMMANDS = [
2
2
  {
3
- command: 'ktx ingest --all',
4
- description: 'Build or refresh agent-ready context from configured connections',
3
+ command: 'ktx ingest',
4
+ description: 'Build or refresh agent-ready context from all configured connections',
5
5
  },
6
6
  {
7
7
  command: 'ktx status',
@@ -14,11 +14,11 @@ export const KTX_NEXT_STEP_DIRECT_COMMANDS = [
14
14
  description: 'Verify project setup and context readiness',
15
15
  },
16
16
  {
17
- command: 'ktx sl list',
17
+ command: 'ktx sl',
18
18
  description: 'Inspect generated semantic-layer sources',
19
19
  },
20
20
  {
21
- command: 'ktx wiki list',
21
+ command: 'ktx wiki',
22
22
  description: 'Inspect generated wiki pages',
23
23
  },
24
24
  ];
@@ -4,8 +4,8 @@ describe('KTX demo next steps', () => {
4
4
  it('uses supported context-build commands before agent usage', () => {
5
5
  expect(KTX_CONTEXT_BUILD_COMMANDS).toEqual([
6
6
  {
7
- command: 'ktx ingest --all',
8
- description: 'Build or refresh agent-ready context from configured connections',
7
+ command: 'ktx ingest',
8
+ description: 'Build or refresh agent-ready context from all configured connections',
9
9
  },
10
10
  {
11
11
  command: 'ktx status',
@@ -20,11 +20,11 @@ describe('KTX demo next steps', () => {
20
20
  description: 'Verify project setup and context readiness',
21
21
  },
22
22
  {
23
- command: 'ktx sl list',
23
+ command: 'ktx sl',
24
24
  description: 'Inspect generated semantic-layer sources',
25
25
  },
26
26
  {
27
- command: 'ktx wiki list',
27
+ command: 'ktx wiki',
28
28
  description: 'Inspect generated wiki pages',
29
29
  },
30
30
  ]);
@@ -54,7 +54,7 @@ describe('KTX demo next steps', () => {
54
54
  }).join('\n');
55
55
  expect(rendered).toContain('Build KTX context next.');
56
56
  expect(rendered).toContain('Run ingest to build database schema context before context-source ingest.');
57
- expect(rendered).toContain('ktx ingest --all');
57
+ expect(rendered).toContain('ktx ingest');
58
58
  expect(rendered).not.toContain('resume');
59
59
  expect(rendered).not.toContain('scan');
60
60
  expect(rendered).toContain('ktx status');
@@ -8,7 +8,7 @@ describe('renderKtxCommandTree', () => {
8
8
  const topLevel = lines
9
9
  .filter((line) => /^ {2}[├└]── \S/.test(line))
10
10
  .map((line) => line.replace(/^ {2}[├└]── /, '').trim().split(' ')[0]);
11
- for (const expected of ['setup', 'connection', 'ingest', 'sl', 'mcp', 'dev']) {
11
+ for (const expected of ['setup', 'connection', 'ingest', 'sl', 'mcp', 'admin']) {
12
12
  expect(topLevel).toContain(expected);
13
13
  }
14
14
  expect(output).toContain('│ └── test [connectionId]');
@@ -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
+ });
@@ -234,12 +234,10 @@ function targetForConnection(connectionId, connection, projectConfig, args, warn
234
234
  throw new Error(`Connection "${connectionId}" uses unsupported public ingest driver "${driver || 'unknown'}"`);
235
235
  }
236
236
  export function buildPublicIngestPlan(project, args) {
237
- if (!args.all && !args.targetConnectionId) {
238
- throw new Error('Context build requires a connection id or all targets');
239
- }
237
+ const allConnections = args.all || !args.targetConnectionId;
240
238
  const entries = Object.entries(project.config.connections).sort(([a], [b]) => a.localeCompare(b));
241
- const selected = args.all ? entries : entries.filter(([connectionId]) => connectionId === args.targetConnectionId);
242
- if (!args.all && selected.length === 0) {
239
+ const selected = allConnections ? entries : entries.filter(([connectionId]) => connectionId === args.targetConnectionId);
240
+ if (!allConnections && selected.length === 0) {
243
241
  throw new Error(`Connection "${args.targetConnectionId}" is not configured in ktx.yaml`);
244
242
  }
245
243
  if (selected.length === 0) {
@@ -109,9 +109,13 @@ describe('buildPublicIngestPlan', () => {
109
109
  warnings: [],
110
110
  });
111
111
  });
112
- it('rejects bare non-interactive ingest until the interactive confirmation slice exists', () => {
113
- const project = projectWithConnections({ warehouse: { driver: 'postgres' } });
114
- expect(() => buildPublicIngestPlan(project, { projectDir: '/tmp/project', all: false })).toThrow('Context build requires a connection id or all targets');
112
+ it('treats a bare invocation (no connection id, no --all) as all configured connections', () => {
113
+ const project = projectWithConnections({
114
+ warehouse: { driver: 'postgres' },
115
+ docs: { driver: 'notion' },
116
+ });
117
+ const plan = buildPublicIngestPlan(project, { projectDir: '/tmp/project', all: false });
118
+ expect(plan.targets.map((target) => target.connectionId).sort()).toEqual(['docs', 'warehouse']);
115
119
  });
116
120
  it('resolves database depth from flags, stored context, and defaults', () => {
117
121
  const project = projectWithConnections({
@@ -40,6 +40,7 @@ describe('runKtxRuntime', () => {
40
40
  },
41
41
  asset: {
42
42
  wheelPath: '/assets/python/kaelio_ktx-0.1.0-py3-none-any.whl',
43
+ requiresPython: { specifier: '>=3.13', minimumVersion: '3.13' },
43
44
  manifest: {
44
45
  schemaVersion: 1,
45
46
  distributionName: 'kaelio-ktx',
@@ -245,7 +246,7 @@ describe('runKtxRuntime', () => {
245
246
  label: 'Managed Python runtime',
246
247
  status: 'fail',
247
248
  detail: 'No runtime manifest at /runtime/0.2.0/manifest.json',
248
- fix: 'Run: ktx dev runtime install --yes',
249
+ fix: 'Run: ktx admin runtime install --yes',
249
250
  },
250
251
  ]),
251
252
  };