@kaelio/ktx 0.1.0-rc.6 → 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.
Files changed (105) hide show
  1. package/assets/python/{kaelio_ktx-0.1.0rc6-py3-none-any.whl → kaelio_ktx-0.1.1-py3-none-any.whl} +0 -0
  2. package/assets/python/manifest.json +4 -4
  3. package/dist/commands/mcp-commands.js +11 -3
  4. package/dist/commands/mcp-commands.test.js +30 -1
  5. package/dist/commands/setup-commands.js +14 -26
  6. package/dist/doctor.test.js +3 -4
  7. package/dist/index.test.js +26 -10
  8. package/dist/ingest-depth.js +0 -1
  9. package/dist/ingest.test-utils.js +2 -2
  10. package/dist/ingest.test.js +6 -30
  11. package/dist/managed-local-embeddings.d.ts +2 -0
  12. package/dist/managed-local-embeddings.js +2 -0
  13. package/dist/managed-local-embeddings.test.js +2 -0
  14. package/dist/managed-mcp-daemon.js +3 -2
  15. package/dist/managed-mcp-daemon.test.js +25 -0
  16. package/dist/managed-python-command.test.js +1 -0
  17. package/dist/managed-python-daemon.js +3 -2
  18. package/dist/managed-python-daemon.test.js +20 -0
  19. package/dist/managed-python-runtime.d.ts +4 -0
  20. package/dist/managed-python-runtime.js +47 -3
  21. package/dist/managed-python-runtime.test.js +51 -21
  22. package/dist/next-steps.js +1 -1
  23. package/dist/next-steps.test.js +2 -0
  24. package/dist/proxy-env.d.ts +1 -0
  25. package/dist/proxy-env.js +23 -0
  26. package/dist/proxy-env.test.js +17 -0
  27. package/dist/runtime-requirements.d.ts +1 -2
  28. package/dist/runtime-requirements.js +0 -7
  29. package/dist/runtime-requirements.test.js +2 -2
  30. package/dist/runtime.test.js +1 -0
  31. package/dist/setup-agents.d.ts +11 -3
  32. package/dist/setup-agents.js +400 -135
  33. package/dist/setup-agents.test.js +394 -62
  34. package/dist/setup-embeddings.d.ts +1 -0
  35. package/dist/setup-embeddings.js +28 -6
  36. package/dist/setup-embeddings.test.js +46 -4
  37. package/dist/setup-models.d.ts +0 -1
  38. package/dist/setup-models.js +2 -3
  39. package/dist/setup-models.test.js +8 -10
  40. package/dist/setup-project.d.ts +9 -1
  41. package/dist/setup-project.js +52 -25
  42. package/dist/setup-project.test.js +8 -8
  43. package/dist/setup-runtime.d.ts +0 -1
  44. package/dist/setup-runtime.js +0 -1
  45. package/dist/setup-runtime.test.js +9 -13
  46. package/dist/setup.d.ts +4 -2
  47. package/dist/setup.js +72 -30
  48. package/dist/setup.test.js +271 -58
  49. package/dist/sl.test.js +2 -1
  50. package/dist/standalone-smoke.test.js +2 -3
  51. package/dist/status-project.js +1 -10
  52. package/node_modules/@ktx/connector-clickhouse/dist/package-exports.test.js +1 -1
  53. package/node_modules/@ktx/context/dist/core/git.service.d.ts +0 -1
  54. package/node_modules/@ktx/context/dist/core/git.service.js +0 -12
  55. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/historic-sql.adapter.d.ts +1 -2
  56. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/historic-sql.adapter.js +0 -18
  57. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/local-ingest-acceptance.test.js +7 -7
  58. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/post-processor.d.ts +4 -0
  59. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/post-processor.js +38 -0
  60. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/post-processor.test.d.ts +1 -0
  61. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/post-processor.test.js +63 -0
  62. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/projection.d.ts +0 -5
  63. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/projection.js +0 -48
  64. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/projection.test.js +0 -83
  65. package/node_modules/@ktx/context/dist/ingest/index.d.ts +2 -1
  66. package/node_modules/@ktx/context/dist/ingest/index.js +1 -0
  67. package/node_modules/@ktx/context/dist/ingest/ingest-bundle.runner.d.ts +0 -2
  68. package/node_modules/@ktx/context/dist/ingest/ingest-bundle.runner.isolated-diff.test.js +0 -166
  69. package/node_modules/@ktx/context/dist/ingest/ingest-bundle.runner.js +45 -235
  70. package/node_modules/@ktx/context/dist/ingest/ingest-bundle.runner.test.js +38 -193
  71. package/node_modules/@ktx/context/dist/ingest/local-bundle-ingest.test.js +11 -30
  72. package/node_modules/@ktx/context/dist/ingest/local-bundle-runtime.js +5 -1
  73. package/node_modules/@ktx/context/dist/ingest/local-bundle-runtime.test.js +3 -3
  74. package/node_modules/@ktx/context/dist/ingest/local-embedding-provider.integration.test.js +9 -10
  75. package/node_modules/@ktx/context/dist/ingest/local-ingest.js +7 -0
  76. package/node_modules/@ktx/context/dist/ingest/memory-flow/schema.d.ts +4 -4
  77. package/node_modules/@ktx/context/dist/ingest/memory-flow/schema.js +1 -1
  78. package/node_modules/@ktx/context/dist/ingest/memory-flow/types.d.ts +1 -1
  79. package/node_modules/@ktx/context/dist/ingest/ports.d.ts +20 -1
  80. package/node_modules/@ktx/context/dist/ingest/report-snapshot.d.ts +2 -73
  81. package/node_modules/@ktx/context/dist/ingest/report-snapshot.js +0 -27
  82. package/node_modules/@ktx/context/dist/ingest/reports.d.ts +5 -23
  83. package/node_modules/@ktx/context/dist/ingest/reports.js +24 -7
  84. package/node_modules/@ktx/context/dist/ingest/types.d.ts +0 -33
  85. package/node_modules/@ktx/context/dist/llm/local-config.js +2 -15
  86. package/node_modules/@ktx/context/dist/llm/local-config.test.js +3 -7
  87. package/node_modules/@ktx/context/dist/package-exports.test.js +1 -2
  88. package/node_modules/@ktx/context/dist/project/config.d.ts +0 -5
  89. package/node_modules/@ktx/context/dist/project/config.js +5 -5
  90. package/node_modules/@ktx/context/dist/project/config.test.js +4 -7
  91. package/node_modules/@ktx/context/dist/scan/enrichment-state.test.js +4 -4
  92. package/node_modules/@ktx/context/dist/scan/index.d.ts +1 -1
  93. package/node_modules/@ktx/context/dist/scan/local-enrichment.d.ts +2 -6
  94. package/node_modules/@ktx/context/dist/scan/local-enrichment.js +31 -47
  95. package/node_modules/@ktx/context/dist/scan/local-enrichment.test.js +35 -18
  96. package/node_modules/@ktx/context/dist/scan/local-scan.test.js +2 -3
  97. package/node_modules/@ktx/llm/dist/embedding-provider.d.ts +0 -7
  98. package/node_modules/@ktx/llm/dist/embedding-provider.js +12 -138
  99. package/node_modules/@ktx/llm/dist/embedding-provider.test.js +10 -25
  100. package/node_modules/@ktx/llm/dist/types.d.ts +1 -1
  101. package/package.json +4 -4
  102. package/node_modules/@ktx/context/dist/ingest/finalization-scope.d.ts +0 -22
  103. package/node_modules/@ktx/context/dist/ingest/finalization-scope.js +0 -95
  104. package/node_modules/@ktx/context/dist/ingest/finalization-scope.test.js +0 -114
  105. /package/{node_modules/@ktx/context/dist/ingest/finalization-scope.test.d.ts → dist/proxy-env.test.d.ts} +0 -0
@@ -4,7 +4,7 @@ import { join } from 'node:path';
4
4
  import { readKtxSetupState } from '@ktx/context/project';
5
5
  import { strFromU8, unzipSync } from 'fflate';
6
6
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
7
- import { formatInstallSummary, plannedKtxAgentFiles, readKtxAgentInstallManifest, removeKtxAgentInstall, runKtxSetupAgentsStep, } from './setup-agents.js';
7
+ import { createAgentNextActionsLineFormatter, formatInstallSummaryLines, plannedKtxAgentFiles, readKtxAgentInstallManifest, removeKtxAgentInstall, runKtxSetupAgentsStep, } from './setup-agents.js';
8
8
  function makeIo() {
9
9
  let stdout = '';
10
10
  let stderr = '';
@@ -72,7 +72,11 @@ describe('setup agents', () => {
72
72
  ]);
73
73
  expect(plannedKtxAgentFiles({ projectDir: tempDir, target: 'claude-desktop', scope: 'global', mode: 'mcp' })).toEqual([
74
74
  { kind: 'file', path: join(tempDir, '.ktx/agents/claude/ktx-plugin-runner.sh'), role: 'launcher' },
75
- { kind: 'file', path: join(tempDir, '.ktx/agents/claude/ktx-plugin.zip'), role: 'claude-plugin' },
75
+ {
76
+ kind: 'file',
77
+ path: join(tempDir, '.ktx/agents/claude/ktx-analytics.zip'),
78
+ role: 'claude-desktop-skill-bundle',
79
+ },
76
80
  ]);
77
81
  expect(plannedKtxAgentFiles({ projectDir: tempDir, target: 'codex', scope: 'project', mode: 'mcp' })).toEqual([
78
82
  { kind: 'file', path: join(tempDir, '.agents/skills/ktx-analytics/SKILL.md'), role: 'analytics-skill' },
@@ -112,7 +116,16 @@ describe('setup agents', () => {
112
116
  ]);
113
117
  expect(plannedKtxAgentFiles({ projectDir: tempDir, target: 'claude-desktop', scope: 'global', mode: 'mcp-cli' })).toEqual([
114
118
  { kind: 'file', path: join(tempDir, '.ktx/agents/claude/ktx-plugin-runner.sh'), role: 'launcher' },
115
- { kind: 'file', path: join(tempDir, '.ktx/agents/claude/ktx-plugin.zip'), role: 'claude-plugin' },
119
+ {
120
+ kind: 'file',
121
+ path: join(tempDir, '.ktx/agents/claude/ktx-analytics.zip'),
122
+ role: 'claude-desktop-skill-bundle',
123
+ },
124
+ {
125
+ kind: 'file',
126
+ path: join(tempDir, '.ktx/agents/claude/ktx.zip'),
127
+ role: 'claude-desktop-skill-bundle',
128
+ },
116
129
  ]);
117
130
  });
118
131
  it('installs target files, writes a manifest, and marks agents complete', async () => {
@@ -126,7 +139,7 @@ describe('setup agents', () => {
126
139
  scope: 'project',
127
140
  mode: 'mcp-cli',
128
141
  skipAgents: false,
129
- }, io.io)).resolves.toEqual({
142
+ }, io.io)).resolves.toMatchObject({
130
143
  status: 'ready',
131
144
  projectDir: tempDir,
132
145
  installs: [{ target: 'universal', scope: 'project', mode: 'mcp-cli' }],
@@ -150,6 +163,88 @@ describe('setup agents', () => {
150
163
  expect(await readKtxSetupState(tempDir)).toEqual({ completed_steps: ['agents'] });
151
164
  expect(io.stderr()).toBe('');
152
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
+ });
200
+ it('prints standalone agent next actions after successful installation', async () => {
201
+ const io = makeIo();
202
+ const result = await runKtxSetupAgentsStep({
203
+ projectDir: tempDir,
204
+ inputMode: 'disabled',
205
+ yes: true,
206
+ agents: true,
207
+ target: 'claude-code',
208
+ scope: 'project',
209
+ mode: 'mcp-cli',
210
+ skipAgents: false,
211
+ }, io.io);
212
+ expect(result).toMatchObject({
213
+ status: 'ready',
214
+ nextActions: expect.stringContaining('Run this command before using Claude Code:'),
215
+ });
216
+ expect(io.stdout()).toContain('Required before using agents');
217
+ expect(io.stdout()).toContain('Run this command before using Claude Code:');
218
+ expect(io.stdout()).toContain('RUN:');
219
+ expect(io.stdout()).toContain(`ktx mcp start --project-dir ${tempDir}`);
220
+ expect(io.stdout()).toContain('If you need to stop MCP later:');
221
+ expect(io.stdout()).toContain(`ktx mcp stop --project-dir ${tempDir}`);
222
+ expect(io.stdout()).toContain('All set.');
223
+ expect(io.stdout()).not.toContain('Finish agent setup');
224
+ expect(io.stdout()).not.toContain('Next actions');
225
+ });
226
+ it('can return agent next actions without printing them', async () => {
227
+ const io = makeIo();
228
+ const result = await runKtxSetupAgentsStep({
229
+ projectDir: tempDir,
230
+ inputMode: 'disabled',
231
+ yes: true,
232
+ agents: true,
233
+ target: 'claude-code',
234
+ scope: 'project',
235
+ mode: 'mcp-cli',
236
+ skipAgents: false,
237
+ showNextActions: false,
238
+ }, io.io);
239
+ expect(result).toMatchObject({
240
+ status: 'ready',
241
+ nextActions: expect.stringContaining(`ktx mcp start --project-dir ${tempDir}`),
242
+ });
243
+ expect(io.stdout()).toContain('Claude Code · Project scope');
244
+ expect(io.stdout()).not.toContain('Agent integration complete');
245
+ expect(io.stdout()).not.toContain('Required before using agents');
246
+ expect(io.stdout()).not.toContain('All set.');
247
+ });
153
248
  it('installs the analytics skill from the runtime asset', async () => {
154
249
  const io = makeIo();
155
250
  await expect(runKtxSetupAgentsStep({
@@ -207,7 +302,6 @@ describe('setup agents', () => {
207
302
  expect(await readKtxAgentInstallManifest(tempDir)).toMatchObject({
208
303
  entries: expect.arrayContaining([{ kind: 'json-key', path: join(tempDir, '.mcp.json'), jsonPath: ['mcpServers', 'ktx'] }]),
209
304
  });
210
- expect(io.stdout()).toContain('Run `ktx mcp start` to enable the configured KTX MCP server.');
211
305
  });
212
306
  it('prompts for MCP-first client agent connection mode in interactive setup', async () => {
213
307
  const io = makeIo();
@@ -229,10 +323,18 @@ describe('setup agents', () => {
229
323
  installs: [{ target: 'claude-code', scope: 'project', mode: 'mcp' }],
230
324
  });
231
325
  expect(prompts.select).toHaveBeenCalledWith({
232
- message: 'How should client agents connect to this KTX project?',
326
+ message: 'What should agents be allowed to do with this KTX project?',
233
327
  options: [
234
- { value: 'mcp', label: 'MCP tools + analytics skill' },
235
- { value: 'mcp-cli', label: 'MCP tools + analytics skill + admin CLI skill' },
328
+ {
329
+ value: 'mcp',
330
+ label: 'Ask data questions with KTX MCP',
331
+ hint: 'Installs the MCP connection and analytics workflow skill. Best for normal use.',
332
+ },
333
+ {
334
+ value: 'mcp-cli',
335
+ label: 'Ask data questions + manage KTX with CLI commands',
336
+ hint: 'Adds an admin CLI skill so agents can run ktx status, sl, wiki, and setup commands.',
337
+ },
236
338
  ],
237
339
  });
238
340
  expect(prompts.multiselect).toHaveBeenCalledWith(expect.objectContaining({
@@ -263,10 +365,18 @@ describe('setup agents', () => {
263
365
  installs: [{ target: 'claude-code', scope: 'global', mode: 'mcp' }],
264
366
  });
265
367
  expect(prompts.select).toHaveBeenCalledWith({
266
- message: 'Where should KTX install supported agent config?',
368
+ message: `Where should KTX install supported agent config?\n\nKTX project: ${tempDir}`,
267
369
  options: [
268
- { value: 'project', label: 'Project' },
269
- { value: 'global', label: 'Global' },
370
+ {
371
+ value: 'project',
372
+ label: 'Project scope (KTX project directory)',
373
+ hint: 'Only agents opened from this KTX project path load the project-scoped config.',
374
+ },
375
+ {
376
+ value: 'global',
377
+ label: 'Global scope (user config)',
378
+ hint: 'Agents can load this KTX project from any working directory.',
379
+ },
270
380
  ],
271
381
  });
272
382
  }
@@ -275,7 +385,7 @@ describe('setup agents', () => {
275
385
  await rm(home, { recursive: true, force: true });
276
386
  }
277
387
  });
278
- it('registers Claude Desktop MCP via claude_desktop_config.json and ships a skills-only plugin', async () => {
388
+ it('registers Claude Desktop MCP and ships an uploadable analytics skill zip', async () => {
279
389
  const home = await mkdtemp(join(tmpdir(), 'ktx-setup-agents-home-'));
280
390
  const previousHome = process.env.HOME;
281
391
  const envSnapshot = captureEnvKeys(process.env, ['OPENAI_API_KEY', 'ANTHROPIC_API_KEY']);
@@ -298,9 +408,11 @@ describe('setup agents', () => {
298
408
  status: 'ready',
299
409
  installs: [{ target: 'claude-desktop', scope: 'global', mode: 'mcp' }],
300
410
  });
301
- const pluginPath = join(tempDir, '.ktx/agents/claude/ktx-plugin.zip');
411
+ const analyticsSkillPath = join(tempDir, '.ktx/agents/claude/ktx-analytics.zip');
412
+ const adminSkillPath = join(tempDir, '.ktx/agents/claude/ktx.zip');
302
413
  const launcherPath = join(tempDir, '.ktx/agents/claude/ktx-plugin-runner.sh');
303
- await expect(stat(pluginPath)).resolves.toBeDefined();
414
+ await expect(stat(analyticsSkillPath)).resolves.toBeDefined();
415
+ await expect(stat(adminSkillPath)).rejects.toThrow();
304
416
  const launcherStat = await stat(launcherPath);
305
417
  expect(launcherStat.mode & 0o111).not.toBe(0);
306
418
  const launcher = await readFile(launcherPath, 'utf-8');
@@ -312,18 +424,21 @@ describe('setup agents', () => {
312
424
  command: launcherPath,
313
425
  args: ['--project-dir', tempDir, 'mcp', 'stdio'],
314
426
  });
315
- expect(await readZipText(pluginPath, '.claude-plugin/plugin.json')).toContain('"name": "ktx"');
316
- await expect(readZipText(pluginPath, '.mcp.json')).rejects.toThrow('Missing zip entry');
317
- expect(await readZipText(pluginPath, 'skills/ktx-analytics/SKILL.md')).toContain('KTX Analytics Workflow');
318
- const setupMd = await readZipText(pluginPath, 'SETUP.md');
319
- expect(setupMd).not.toContain('ktx mcp start');
320
- expect(setupMd).toContain('claude_desktop_config.json');
321
- await expect(readZipText(pluginPath, 'skills/ktx/SKILL.md')).rejects.toThrow('Missing zip entry');
322
- expect(io.stdout()).toContain('Claude plugin generated');
323
- expect(io.stdout()).toContain('.ktx/agents/claude/ktx-plugin.zip');
324
- expect(io.stdout()).toContain('KTX MCP server registered');
427
+ expect(await readZipText(analyticsSkillPath, 'ktx-analytics/SKILL.md')).toContain('KTX Analytics Workflow');
428
+ await expect(readZipText(analyticsSkillPath, 'ktx/SKILL.md')).rejects.toThrow('Missing zip entry');
429
+ await expect(readZipText(analyticsSkillPath, '.claude-plugin/plugin.json')).rejects.toThrow('Missing zip entry');
430
+ await expect(readZipText(analyticsSkillPath, 'skills/ktx-analytics/SKILL.md')).rejects.toThrow('Missing zip entry');
431
+ expect(io.stdout()).toContain('Claude Desktop');
432
+ expect(io.stdout()).toContain(analyticsSkillPath);
433
+ expect(io.stdout()).not.toContain(adminSkillPath);
325
434
  expect(io.stdout()).toContain('claude_desktop_config.json');
326
- expect(io.stdout()).toContain('Restart Claude Desktop');
435
+ expect(io.stdout()).toContain('Required before using agents');
436
+ expect(io.stdout()).toContain('1. Restart Claude Desktop');
437
+ expect(io.stdout()).toContain('Claude Desktop loads KTX MCP after restart.');
438
+ expect(io.stdout()).toContain('2. Upload Claude Desktop skills');
439
+ expect(io.stdout()).toContain('Customize > Skills > + > Create skill > Upload a skill');
440
+ expect(io.stdout()).toContain('Upload this file:');
441
+ expect(io.stdout()).toContain('Toggle the uploaded KTX skills on.');
327
442
  expect(io.stdout()).not.toContain('Run `ktx mcp start`');
328
443
  }
329
444
  finally {
@@ -374,7 +489,7 @@ describe('setup agents', () => {
374
489
  await rm(home, { recursive: true, force: true });
375
490
  }
376
491
  });
377
- it('includes the admin CLI skill in the Claude Desktop plugin zip when requested', async () => {
492
+ it('includes an uploadable admin CLI skill zip for Claude Desktop when requested', async () => {
378
493
  const home = await mkdtemp(join(tmpdir(), 'ktx-setup-agents-home-'));
379
494
  const previousHome = process.env.HOME;
380
495
  process.env.HOME = home;
@@ -393,12 +508,18 @@ describe('setup agents', () => {
393
508
  status: 'ready',
394
509
  installs: [{ target: 'claude-desktop', scope: 'global', mode: 'mcp-cli' }],
395
510
  });
396
- const pluginPath = join(tempDir, '.ktx/agents/claude/ktx-plugin.zip');
397
- const adminSkill = await readZipText(pluginPath, 'skills/ktx/SKILL.md');
511
+ const analyticsSkillPath = join(tempDir, '.ktx/agents/claude/ktx-analytics.zip');
512
+ const adminSkillPath = join(tempDir, '.ktx/agents/claude/ktx.zip');
513
+ expect(await readZipText(analyticsSkillPath, 'ktx-analytics/SKILL.md')).toContain('KTX Analytics Workflow');
514
+ await expect(readZipText(analyticsSkillPath, 'ktx/SKILL.md')).rejects.toThrow('Missing zip entry');
515
+ const adminSkill = await readZipText(adminSkillPath, 'ktx/SKILL.md');
398
516
  expect(adminSkill).toContain(`--project-dir ${tempDir}`);
399
517
  expect(adminSkill).toContain('status --json');
400
- expect(await readZipText(pluginPath, 'SETUP.md')).toContain('admin CLI skill');
401
- await expect(readZipText(pluginPath, '.mcp.json')).rejects.toThrow('Missing zip entry');
518
+ await expect(readZipText(adminSkillPath, '.mcp.json')).rejects.toThrow('Missing zip entry');
519
+ await expect(readZipText(adminSkillPath, 'ktx-analytics/SKILL.md')).rejects.toThrow('Missing zip entry');
520
+ expect(io.stdout()).toContain(analyticsSkillPath);
521
+ expect(io.stdout()).toContain(adminSkillPath);
522
+ expect(io.stdout()).toContain('Upload each file separately:');
402
523
  }
403
524
  finally {
404
525
  process.env.HOME = previousHome;
@@ -455,6 +576,9 @@ describe('setup agents', () => {
455
576
  }, codexIo.io);
456
577
  expect(codexIo.stdout()).toContain('[mcp_servers.ktx]');
457
578
  expect(codexIo.stdout()).toContain('url = "http://localhost:7878/mcp"');
579
+ expect(codexIo.stdout()).toContain('1. Configure Codex');
580
+ expect(codexIo.stdout()).toContain('Open ~/.codex/config.toml, then paste this block:');
581
+ expect(codexIo.stdout()).toContain('PASTE:');
458
582
  const opencodeIo = makeIo();
459
583
  await runKtxSetupAgentsStep({
460
584
  projectDir: tempDir,
@@ -468,6 +592,8 @@ describe('setup agents', () => {
468
592
  }, opencodeIo.io);
469
593
  expect(opencodeIo.stdout()).toContain('"mcp"');
470
594
  expect(opencodeIo.stdout()).toContain('"type": "remote"');
595
+ expect(opencodeIo.stdout()).toContain('1. Configure OpenCode');
596
+ expect(opencodeIo.stdout()).toContain('Open opencode.json, then paste this block:');
471
597
  await expect(readFile(join(tempDir, 'opencode.json'), 'utf-8')).rejects.toThrow();
472
598
  const universalIo = makeIo();
473
599
  await runKtxSetupAgentsStep({
@@ -482,6 +608,8 @@ describe('setup agents', () => {
482
608
  }, universalIo.io);
483
609
  expect(universalIo.stdout()).toContain('Universal MCP endpoint:');
484
610
  expect(universalIo.stdout()).toContain('http://localhost:7878/mcp');
611
+ expect(universalIo.stdout()).toContain('1. Configure unsupported MCP clients');
612
+ expect(universalIo.stdout()).toContain('Use this endpoint when setting up unsupported MCP clients:');
485
613
  });
486
614
  it('uses MCP daemon state for port and token metadata without rendering literal tokens', async () => {
487
615
  await mkdir(join(tempDir, '.ktx'), { recursive: true });
@@ -513,7 +641,9 @@ describe('setup agents', () => {
513
641
  expect(rendered).toContain('http://127.0.0.1:8787/mcp');
514
642
  expect(rendered).toContain('Bearer ${KTX_MCP_TOKEN}');
515
643
  expect(rendered).not.toContain('secret-token');
516
- expect(io.stdout()).toContain('Run `ktx mcp start` to enable the configured KTX MCP server.');
644
+ expect(io.stdout()).toContain('Run this command before using Claude Code:');
645
+ expect(io.stdout()).toContain('RUN:');
646
+ expect(io.stdout()).toContain(`ktx mcp start --project-dir ${tempDir}`);
517
647
  }
518
648
  finally {
519
649
  if (previousToken === undefined) {
@@ -567,7 +697,7 @@ describe('setup agents', () => {
567
697
  await expect(stat(join(tempDir, '.claude/skills/ktx/keep.txt'))).resolves.toBeDefined();
568
698
  await expect(readKtxAgentInstallManifest(tempDir)).resolves.toEqual(null);
569
699
  });
570
- it('removes generated Claude Desktop plugin from the manifest', async () => {
700
+ it('removes generated Claude Desktop skill zips from the manifest', async () => {
571
701
  const home = await mkdtemp(join(tmpdir(), 'ktx-setup-agents-home-'));
572
702
  const previousHome = process.env.HOME;
573
703
  process.env.HOME = home;
@@ -583,15 +713,18 @@ describe('setup agents', () => {
583
713
  mode: 'mcp-cli',
584
714
  skipAgents: false,
585
715
  }, io.io);
586
- const pluginPath = join(tempDir, '.ktx/agents/claude/ktx-plugin.zip');
716
+ const analyticsSkillPath = join(tempDir, '.ktx/agents/claude/ktx-analytics.zip');
717
+ const adminSkillPath = join(tempDir, '.ktx/agents/claude/ktx.zip');
587
718
  const launcherPath = join(tempDir, '.ktx/agents/claude/ktx-plugin-runner.sh');
588
719
  const configPath = join(home, 'Library/Application Support/Claude/claude_desktop_config.json');
589
- await expect(stat(pluginPath)).resolves.toBeDefined();
720
+ await expect(stat(analyticsSkillPath)).resolves.toBeDefined();
721
+ await expect(stat(adminSkillPath)).resolves.toBeDefined();
590
722
  await expect(stat(launcherPath)).resolves.toBeDefined();
591
723
  const beforeConfig = JSON.parse(await readFile(configPath, 'utf-8'));
592
724
  expect(beforeConfig.mcpServers.ktx).toBeDefined();
593
725
  await expect(removeKtxAgentInstall(tempDir, io.io)).resolves.toBe(0);
594
- await expect(stat(pluginPath)).rejects.toThrow();
726
+ await expect(stat(analyticsSkillPath)).rejects.toThrow();
727
+ await expect(stat(adminSkillPath)).rejects.toThrow();
595
728
  await expect(stat(launcherPath)).rejects.toThrow();
596
729
  const afterConfig = JSON.parse(await readFile(configPath, 'utf-8'));
597
730
  expect(afterConfig.mcpServers.ktx).toBeUndefined();
@@ -619,7 +752,7 @@ describe('setup agents', () => {
619
752
  skipAgents: false,
620
753
  }, io.io, { prompts })).resolves.toEqual({ status: 'skipped', projectDir: tempDir });
621
754
  });
622
- it('explains how to select multiple agent targets in interactive mode', async () => {
755
+ it('prints one navigation hint before interactive agent target prompts', async () => {
623
756
  const io = makeIo();
624
757
  const prompts = {
625
758
  select: vi.fn(async () => 'mcp-cli'),
@@ -635,8 +768,10 @@ describe('setup agents', () => {
635
768
  mode: 'mcp-cli',
636
769
  skipAgents: false,
637
770
  }, io.io, { prompts })).resolves.toEqual({ status: 'back', projectDir: tempDir });
771
+ expect(io.stdout()).toContain('Space to select, Enter to confirm, Esc to go back.');
772
+ expect(io.stdout().match(/Space to select/g)).toHaveLength(1);
638
773
  expect(prompts.multiselect).toHaveBeenCalledWith(expect.objectContaining({
639
- message: 'Which agent targets should KTX install?\nUse Up/Down to move, Space to select or unselect, Enter to confirm, Escape to go back, or Ctrl+C to exit.',
774
+ message: 'Which agent targets should KTX install?',
640
775
  }));
641
776
  });
642
777
  it('prints per-agent install summary after successful installation', async () => {
@@ -652,45 +787,242 @@ describe('setup agents', () => {
652
787
  skipAgents: false,
653
788
  }, io.io);
654
789
  const output = io.stdout();
655
- expect(output).toContain('Agent integration complete');
656
- expect(output).toContain('Claude Code');
657
- expect(output).toContain('+ Analytics skill installed — teaches your agent the KTX MCP analytics workflow');
658
- expect(output).toContain('.claude/skills/ktx-analytics/SKILL.md');
659
- expect(output).toContain('+ Skill installed — teaches admin agents which KTX CLI commands to run');
660
- expect(output).toContain('.claude/skills/ktx/SKILL.md');
661
- expect(output).toContain('+ Rule installed — tells admin agents when to use KTX CLI');
662
- expect(output).toContain('.claude/rules/ktx.md');
663
- });
664
- it('formats summary with relative paths for project scope', () => {
665
- const summary = formatInstallSummary([{ target: 'cursor', scope: 'project', mode: 'mcp-cli' }], [
790
+ expect(output).toContain('Claude Code · Project scope');
791
+ expect(output).toContain(join(tempDir, '.mcp.json'));
792
+ expect(output).toContain('Requires MCP to be started.');
793
+ expect(output).toContain('Analytics skill installed.');
794
+ expect(output).toContain('Admin CLI skill installed.');
795
+ expect(output).not.toContain('Agent integration complete');
796
+ expect(output).not.toContain(`KTX project\n ${tempDir}`);
797
+ expect(output).not.toContain('Installed agents');
798
+ expect(output).not.toContain('.claude/skills/ktx-analytics/SKILL.md');
799
+ expect(output).not.toContain('.claude/skills/ktx/SKILL.md');
800
+ expect(output).not.toContain('.claude/rules/ktx.md');
801
+ });
802
+ it('formats summary with explicit project-scoped config paths', () => {
803
+ const summary = formatInstallSummaryLines([{ target: 'cursor', scope: 'project', mode: 'mcp-cli' }], [
666
804
  { kind: 'file', path: join(tempDir, '.cursor/rules/ktx-analytics.mdc'), role: 'analytics-skill' },
667
805
  { kind: 'file', path: join(tempDir, '.cursor/rules/ktx.mdc') },
806
+ { kind: 'json-key', path: join(tempDir, '.cursor/mcp.json'), jsonPath: ['mcpServers', 'ktx'] },
668
807
  ], tempDir);
669
- expect(summary).toContain('Cursor');
670
- expect(summary).toContain('+ Analytics skill installed — teaches your agent the KTX MCP analytics workflow');
671
- expect(summary).toContain('.cursor/rules/ktx-analytics.mdc');
672
- expect(summary).toContain('+ Rule installed — tells admin agents when to use KTX CLI');
673
- expect(summary).toContain('.cursor/rules/ktx.mdc');
674
- expect(summary).not.toContain(tempDir);
808
+ expect(summary).toEqual([
809
+ {
810
+ title: 'Cursor · Project scope',
811
+ lines: [
812
+ join(tempDir, '.cursor/mcp.json'),
813
+ 'Requires MCP to be started.',
814
+ 'Cursor rules installed.',
815
+ ],
816
+ },
817
+ ]);
675
818
  });
676
819
  it('formats summary with multiple agent targets', () => {
677
- const summary = formatInstallSummary([
820
+ const summary = formatInstallSummaryLines([
678
821
  { target: 'claude-code', scope: 'project', mode: 'mcp-cli' },
679
822
  { target: 'codex', scope: 'project', mode: 'mcp-cli' },
680
823
  ], [
681
824
  { kind: 'file', path: join(tempDir, '.claude/skills/ktx-analytics/SKILL.md'), role: 'analytics-skill' },
682
825
  { kind: 'file', path: join(tempDir, '.claude/skills/ktx/SKILL.md'), role: 'skill' },
683
826
  { kind: 'file', path: join(tempDir, '.claude/rules/ktx.md'), role: 'rule' },
827
+ { kind: 'json-key', path: join(tempDir, '.mcp.json'), jsonPath: ['mcpServers', 'ktx'] },
684
828
  { kind: 'file', path: join(tempDir, '.agents/skills/ktx-analytics/SKILL.md'), role: 'analytics-skill' },
685
829
  { kind: 'file', path: join(tempDir, '.agents/skills/ktx/SKILL.md'), role: 'skill' },
686
830
  { kind: 'file', path: join(tempDir, '.codex/instructions/ktx.md'), role: 'rule' },
687
831
  ], tempDir);
688
- expect(summary).toContain('Claude Code');
689
- expect(summary).toContain('+ Analytics skill installed — teaches your agent the KTX MCP analytics workflow');
690
- expect(summary).toContain('+ Skill installed teaches admin agents which KTX CLI commands to run');
691
- expect(summary).toContain('+ Rule installed — tells admin agents when to use KTX CLI');
692
- expect(summary).toContain('Codex');
693
- expect(summary).toContain('.agents/skills/ktx-analytics/SKILL.md');
694
- expect(summary).toContain('.agents/skills/ktx/SKILL.md');
832
+ expect(summary).toEqual([
833
+ {
834
+ title: 'Claude Code · Project scope',
835
+ lines: [
836
+ join(tempDir, '.mcp.json'),
837
+ 'Requires MCP to be started.',
838
+ 'Analytics skill installed.',
839
+ 'Admin CLI skill installed.',
840
+ ],
841
+ },
842
+ {
843
+ title: 'Codex · Project scope',
844
+ lines: [
845
+ 'Add the snippet shown below to ~/.codex/config.toml.',
846
+ 'Requires MCP to be started.',
847
+ 'Codex guidance installed.',
848
+ ],
849
+ },
850
+ ]);
851
+ });
852
+ it('prints one target-aware next actions block for mixed agent targets', async () => {
853
+ const home = await mkdtemp(join(tmpdir(), 'ktx-setup-agents-home-'));
854
+ const previousHome = process.env.HOME;
855
+ process.env.HOME = home;
856
+ try {
857
+ const io = makeIo();
858
+ const prompts = {
859
+ select: vi.fn(async ({ message }) => message.startsWith('Where should') ? 'project' : 'mcp'),
860
+ multiselect: vi.fn(async () => ['claude-code', 'claude-desktop']),
861
+ cancel: vi.fn(),
862
+ };
863
+ await expect(runKtxSetupAgentsStep({
864
+ projectDir: tempDir,
865
+ inputMode: 'auto',
866
+ yes: false,
867
+ agents: true,
868
+ scope: 'project',
869
+ mode: 'mcp',
870
+ skipAgents: false,
871
+ }, io.io, { prompts })).resolves.toMatchObject({
872
+ status: 'ready',
873
+ installs: [
874
+ { target: 'claude-code', scope: 'project', mode: 'mcp' },
875
+ { target: 'claude-desktop', scope: 'global', mode: 'mcp' },
876
+ ],
877
+ });
878
+ const output = io.stdout();
879
+ expect(output).toContain('Required before using agents');
880
+ expect(output).not.toContain('Next actions');
881
+ expect(output).toContain('1. Start MCP');
882
+ expect(output).toContain('Run this command before using Claude Code:');
883
+ expect(output).toContain(`ktx mcp start --project-dir ${tempDir}`);
884
+ expect(output).toContain(`ktx mcp stop --project-dir ${tempDir}\n\n2. Open Claude Code`);
885
+ expect(output).toContain('Open Claude Code from the KTX project directory');
886
+ expect(output).toContain('RUN:');
887
+ expect(output).toContain(`cd '${tempDir}'`);
888
+ expect(output).toContain('3. Restart Claude Desktop');
889
+ expect(output).toContain('Claude Desktop loads KTX MCP after restart.');
890
+ expect(output).toContain('4. Upload Claude Desktop skills');
891
+ expect(output).toContain('Customize > Skills > + > Create skill > Upload a skill');
892
+ expect(output).toContain(join(tempDir, '.ktx/agents/claude/ktx-analytics.zip'));
893
+ expect(output).not.toContain(join(tempDir, '.ktx/agents/claude/ktx.zip'));
894
+ expect(output).toContain('Upload this file:');
895
+ expect(output).toContain('All set.');
896
+ expect(output).not.toContain('Finish Claude Desktop setup');
897
+ expect(output).not.toContain('Run `ktx mcp start` to enable the configured KTX MCP server.');
898
+ }
899
+ finally {
900
+ process.env.HOME = previousHome;
901
+ await rm(home, { recursive: true, force: true });
902
+ }
903
+ });
904
+ it('does not tell global Claude Code installs to open from the project directory', async () => {
905
+ const home = await mkdtemp(join(tmpdir(), 'ktx-setup-agents-home-'));
906
+ const previousHome = process.env.HOME;
907
+ process.env.HOME = home;
908
+ try {
909
+ const io = makeIo();
910
+ await expect(runKtxSetupAgentsStep({
911
+ projectDir: tempDir,
912
+ inputMode: 'disabled',
913
+ yes: true,
914
+ agents: true,
915
+ target: 'claude-code',
916
+ scope: 'global',
917
+ mode: 'mcp',
918
+ skipAgents: false,
919
+ }, io.io)).resolves.toMatchObject({
920
+ status: 'ready',
921
+ installs: [{ target: 'claude-code', scope: 'global', mode: 'mcp' }],
922
+ });
923
+ const output = io.stdout();
924
+ expect(output).toContain('2. Open Claude Code');
925
+ expect(output).toContain('RUN:');
926
+ expect(output).toContain('claude');
927
+ expect(output).not.toContain('Open Claude Code from the KTX project directory');
928
+ expect(output).not.toContain(`cd '${tempDir}'`);
929
+ }
930
+ finally {
931
+ process.env.HOME = previousHome;
932
+ await rm(home, { recursive: true, force: true });
933
+ }
934
+ });
935
+ it('explains next actions for Codex, Cursor, OpenCode, and universal MCP targets', async () => {
936
+ const io = makeIo();
937
+ const prompts = {
938
+ select: vi.fn(async () => 'mcp-cli'),
939
+ multiselect: vi.fn(async () => ['codex', 'cursor', 'opencode', 'universal']),
940
+ cancel: vi.fn(),
941
+ };
942
+ await expect(runKtxSetupAgentsStep({
943
+ projectDir: tempDir,
944
+ inputMode: 'auto',
945
+ yes: false,
946
+ agents: true,
947
+ scope: 'project',
948
+ mode: 'mcp-cli',
949
+ skipAgents: false,
950
+ }, io.io, { prompts })).resolves.toMatchObject({
951
+ status: 'ready',
952
+ installs: [
953
+ { target: 'codex', scope: 'project', mode: 'mcp-cli' },
954
+ { target: 'cursor', scope: 'project', mode: 'mcp-cli' },
955
+ { target: 'opencode', scope: 'project', mode: 'mcp-cli' },
956
+ { target: 'universal', scope: 'project', mode: 'mcp-cli' },
957
+ ],
958
+ });
959
+ const output = io.stdout();
960
+ expect(output).toContain('Required before using agents');
961
+ expect(output).toContain('1. Configure Codex');
962
+ expect(output).toContain('2. Configure OpenCode');
963
+ expect(output).toContain('3. Configure unsupported MCP clients');
964
+ expect(output).toContain('4. Start MCP');
965
+ expect(output).toContain('Run this command before using Codex, Cursor, OpenCode, and Universal .agents:');
966
+ expect(output).toContain('Open Cursor from the KTX project directory');
967
+ expect(output).toContain('Open ~/.codex/config.toml, then paste this block:\n\n PASTE:\n [mcp_servers.ktx]');
968
+ expect(output).toContain('Open opencode.json, then paste this block:');
969
+ expect(output).toContain('Use this endpoint when setting up unsupported MCP clients:');
970
+ expect(output).toContain('Codex guidance installed');
971
+ expect(output).toContain('Cursor rules installed');
972
+ expect(output).toContain('OpenCode commands installed');
973
+ expect(output).toContain('.agents guidance installed');
974
+ });
975
+ describe('createAgentNextActionsLineFormatter', () => {
976
+ function makeColorStdout() {
977
+ return { write: () => true, hasColors: () => true };
978
+ }
979
+ function makePlainStdout() {
980
+ return { write: () => true, hasColors: () => false };
981
+ }
982
+ const ESC = String.fromCharCode(27);
983
+ it('returns the line untouched when the stream cannot render colors', () => {
984
+ const format = createAgentNextActionsLineFormatter(makePlainStdout());
985
+ expect(format('2. Upload Claude Desktop skills')).toBe('2. Upload Claude Desktop skills');
986
+ expect(format(' /tmp/ktx/.ktx/agents/claude/ktx.zip')).toBe(' /tmp/ktx/.ktx/agents/claude/ktx.zip');
987
+ });
988
+ it('styles step headings and aligns sub-prose under the title', () => {
989
+ const format = createAgentNextActionsLineFormatter(makeColorStdout());
990
+ const heading = format('2. Upload Claude Desktop skills');
991
+ expect(heading).toContain(ESC);
992
+ expect(heading).toContain('2');
993
+ expect(heading).toContain('Upload Claude Desktop skills');
994
+ expect(heading).not.toMatch(/^2\. /);
995
+ const sub = format(' Toggle the uploaded KTX skills on.');
996
+ expect(sub).toMatch(/^ {3}/);
997
+ expect(sub).toContain('Toggle the uploaded KTX skills on.');
998
+ });
999
+ it('renders skill bundle .zip paths as bullets and shortens HOME to ~', () => {
1000
+ const previousHome = process.env.HOME;
1001
+ process.env.HOME = '/tmp/test-home';
1002
+ try {
1003
+ const format = createAgentNextActionsLineFormatter(makeColorStdout());
1004
+ const line = format(' /tmp/test-home/.ktx/agents/claude/ktx-analytics.zip');
1005
+ expect(line).toContain('•');
1006
+ expect(line).toContain('~/.ktx/agents/claude/ktx-analytics.zip');
1007
+ expect(line).not.toContain('/tmp/test-home/');
1008
+ }
1009
+ finally {
1010
+ if (previousHome === undefined)
1011
+ delete process.env.HOME;
1012
+ else
1013
+ process.env.HOME = previousHome;
1014
+ }
1015
+ });
1016
+ it('replaces breadcrumb separators with a typographic chevron', () => {
1017
+ const format = createAgentNextActionsLineFormatter(makeColorStdout());
1018
+ const line = format(' Open Claude Desktop: Customize > Skills > + > Create skill > Upload a skill.');
1019
+ expect(line).toContain('›');
1020
+ expect(line).not.toContain(' > ');
1021
+ });
1022
+ it('leaves already-styled lines untouched to avoid double-wrapping', () => {
1023
+ const format = createAgentNextActionsLineFormatter(makeColorStdout());
1024
+ const preStyled = `${ESC}[1m2. Already styled${ESC}[22m`;
1025
+ expect(format(preStyled)).toBe(preStyled);
1026
+ });
695
1027
  });
696
1028
  });
@@ -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>;