@kaelio/ktx 0.1.0-rc.6 → 0.1.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 (55) hide show
  1. package/assets/python/{kaelio_ktx-0.1.0rc6-py3-none-any.whl → kaelio_ktx-0.1.0-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/ingest.test.js +2 -26
  6. package/dist/next-steps.js +1 -1
  7. package/dist/next-steps.test.js +2 -0
  8. package/dist/runtime-requirements.d.ts +1 -2
  9. package/dist/runtime-requirements.js +0 -7
  10. package/dist/runtime-requirements.test.js +2 -2
  11. package/dist/setup-agents.d.ts +11 -3
  12. package/dist/setup-agents.js +397 -134
  13. package/dist/setup-agents.test.js +359 -61
  14. package/dist/setup-runtime.d.ts +0 -1
  15. package/dist/setup-runtime.js +0 -1
  16. package/dist/setup-runtime.test.js +7 -13
  17. package/dist/setup.d.ts +3 -0
  18. package/dist/setup.js +51 -25
  19. package/dist/setup.test.js +112 -16
  20. package/node_modules/@ktx/connector-clickhouse/dist/package-exports.test.js +1 -1
  21. package/node_modules/@ktx/context/dist/core/git.service.d.ts +0 -1
  22. package/node_modules/@ktx/context/dist/core/git.service.js +0 -12
  23. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/historic-sql.adapter.d.ts +1 -2
  24. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/historic-sql.adapter.js +0 -18
  25. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/local-ingest-acceptance.test.js +6 -6
  26. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/post-processor.d.ts +4 -0
  27. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/post-processor.js +38 -0
  28. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/post-processor.test.js +63 -0
  29. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/projection.d.ts +0 -5
  30. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/projection.js +0 -48
  31. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/projection.test.js +0 -83
  32. package/node_modules/@ktx/context/dist/ingest/index.d.ts +2 -1
  33. package/node_modules/@ktx/context/dist/ingest/index.js +1 -0
  34. package/node_modules/@ktx/context/dist/ingest/ingest-bundle.runner.d.ts +0 -2
  35. package/node_modules/@ktx/context/dist/ingest/ingest-bundle.runner.isolated-diff.test.js +0 -166
  36. package/node_modules/@ktx/context/dist/ingest/ingest-bundle.runner.js +45 -235
  37. package/node_modules/@ktx/context/dist/ingest/ingest-bundle.runner.test.js +38 -193
  38. package/node_modules/@ktx/context/dist/ingest/local-bundle-ingest.test.js +3 -22
  39. package/node_modules/@ktx/context/dist/ingest/local-bundle-runtime.js +4 -0
  40. package/node_modules/@ktx/context/dist/ingest/local-ingest.js +7 -0
  41. package/node_modules/@ktx/context/dist/ingest/memory-flow/schema.d.ts +4 -4
  42. package/node_modules/@ktx/context/dist/ingest/memory-flow/schema.js +1 -1
  43. package/node_modules/@ktx/context/dist/ingest/memory-flow/types.d.ts +1 -1
  44. package/node_modules/@ktx/context/dist/ingest/ports.d.ts +20 -1
  45. package/node_modules/@ktx/context/dist/ingest/report-snapshot.d.ts +2 -73
  46. package/node_modules/@ktx/context/dist/ingest/report-snapshot.js +0 -27
  47. package/node_modules/@ktx/context/dist/ingest/reports.d.ts +5 -23
  48. package/node_modules/@ktx/context/dist/ingest/reports.js +24 -7
  49. package/node_modules/@ktx/context/dist/ingest/types.d.ts +0 -33
  50. package/node_modules/@ktx/context/dist/package-exports.test.js +1 -2
  51. package/package.json +4 -4
  52. package/node_modules/@ktx/context/dist/ingest/finalization-scope.d.ts +0 -22
  53. package/node_modules/@ktx/context/dist/ingest/finalization-scope.js +0 -95
  54. package/node_modules/@ktx/context/dist/ingest/finalization-scope.test.js +0 -114
  55. /package/node_modules/@ktx/context/dist/ingest/{finalization-scope.test.d.ts → adapters/historic-sql/post-processor.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,54 @@ describe('setup agents', () => {
150
163
  expect(await readKtxSetupState(tempDir)).toEqual({ completed_steps: ['agents'] });
151
164
  expect(io.stderr()).toBe('');
152
165
  });
166
+ it('prints standalone agent next actions after successful installation', async () => {
167
+ const io = makeIo();
168
+ const result = await runKtxSetupAgentsStep({
169
+ projectDir: tempDir,
170
+ inputMode: 'disabled',
171
+ yes: true,
172
+ agents: true,
173
+ target: 'claude-code',
174
+ scope: 'project',
175
+ mode: 'mcp-cli',
176
+ skipAgents: false,
177
+ }, io.io);
178
+ expect(result).toMatchObject({
179
+ status: 'ready',
180
+ nextActions: expect.stringContaining('Run this command before using Claude Code:'),
181
+ });
182
+ expect(io.stdout()).toContain('Required before using agents');
183
+ expect(io.stdout()).toContain('Run this command before using Claude Code:');
184
+ expect(io.stdout()).toContain('RUN:');
185
+ expect(io.stdout()).toContain(`ktx mcp start --project-dir ${tempDir}`);
186
+ expect(io.stdout()).toContain('If you need to stop MCP later:');
187
+ expect(io.stdout()).toContain(`ktx mcp stop --project-dir ${tempDir}`);
188
+ expect(io.stdout()).toContain('All set.');
189
+ expect(io.stdout()).not.toContain('Finish agent setup');
190
+ expect(io.stdout()).not.toContain('Next actions');
191
+ });
192
+ it('can return agent next actions without printing them', async () => {
193
+ const io = makeIo();
194
+ const result = await runKtxSetupAgentsStep({
195
+ projectDir: tempDir,
196
+ inputMode: 'disabled',
197
+ yes: true,
198
+ agents: true,
199
+ target: 'claude-code',
200
+ scope: 'project',
201
+ mode: 'mcp-cli',
202
+ skipAgents: false,
203
+ showNextActions: false,
204
+ }, io.io);
205
+ expect(result).toMatchObject({
206
+ status: 'ready',
207
+ nextActions: expect.stringContaining(`ktx mcp start --project-dir ${tempDir}`),
208
+ });
209
+ expect(io.stdout()).toContain('Claude Code · Project scope');
210
+ expect(io.stdout()).not.toContain('Agent integration complete');
211
+ expect(io.stdout()).not.toContain('Required before using agents');
212
+ expect(io.stdout()).not.toContain('All set.');
213
+ });
153
214
  it('installs the analytics skill from the runtime asset', async () => {
154
215
  const io = makeIo();
155
216
  await expect(runKtxSetupAgentsStep({
@@ -207,7 +268,6 @@ describe('setup agents', () => {
207
268
  expect(await readKtxAgentInstallManifest(tempDir)).toMatchObject({
208
269
  entries: expect.arrayContaining([{ kind: 'json-key', path: join(tempDir, '.mcp.json'), jsonPath: ['mcpServers', 'ktx'] }]),
209
270
  });
210
- expect(io.stdout()).toContain('Run `ktx mcp start` to enable the configured KTX MCP server.');
211
271
  });
212
272
  it('prompts for MCP-first client agent connection mode in interactive setup', async () => {
213
273
  const io = makeIo();
@@ -229,10 +289,18 @@ describe('setup agents', () => {
229
289
  installs: [{ target: 'claude-code', scope: 'project', mode: 'mcp' }],
230
290
  });
231
291
  expect(prompts.select).toHaveBeenCalledWith({
232
- message: 'How should client agents connect to this KTX project?',
292
+ message: 'What should agents be allowed to do with this KTX project?',
233
293
  options: [
234
- { value: 'mcp', label: 'MCP tools + analytics skill' },
235
- { value: 'mcp-cli', label: 'MCP tools + analytics skill + admin CLI skill' },
294
+ {
295
+ value: 'mcp',
296
+ label: 'Ask data questions with KTX MCP',
297
+ hint: 'Installs the MCP connection and analytics workflow skill. Best for normal use.',
298
+ },
299
+ {
300
+ value: 'mcp-cli',
301
+ label: 'Ask data questions + manage KTX with CLI commands',
302
+ hint: 'Adds an admin CLI skill so agents can run ktx status, sl, wiki, and setup commands.',
303
+ },
236
304
  ],
237
305
  });
238
306
  expect(prompts.multiselect).toHaveBeenCalledWith(expect.objectContaining({
@@ -263,10 +331,18 @@ describe('setup agents', () => {
263
331
  installs: [{ target: 'claude-code', scope: 'global', mode: 'mcp' }],
264
332
  });
265
333
  expect(prompts.select).toHaveBeenCalledWith({
266
- message: 'Where should KTX install supported agent config?',
334
+ message: `Where should KTX install supported agent config?\n\nKTX project: ${tempDir}`,
267
335
  options: [
268
- { value: 'project', label: 'Project' },
269
- { value: 'global', label: 'Global' },
336
+ {
337
+ value: 'project',
338
+ label: 'Project scope (KTX project directory)',
339
+ hint: 'Only agents opened from this KTX project path load the project-scoped config.',
340
+ },
341
+ {
342
+ value: 'global',
343
+ label: 'Global scope (user config)',
344
+ hint: 'Agents can load this KTX project from any working directory.',
345
+ },
270
346
  ],
271
347
  });
272
348
  }
@@ -275,7 +351,7 @@ describe('setup agents', () => {
275
351
  await rm(home, { recursive: true, force: true });
276
352
  }
277
353
  });
278
- it('registers Claude Desktop MCP via claude_desktop_config.json and ships a skills-only plugin', async () => {
354
+ it('registers Claude Desktop MCP and ships an uploadable analytics skill zip', async () => {
279
355
  const home = await mkdtemp(join(tmpdir(), 'ktx-setup-agents-home-'));
280
356
  const previousHome = process.env.HOME;
281
357
  const envSnapshot = captureEnvKeys(process.env, ['OPENAI_API_KEY', 'ANTHROPIC_API_KEY']);
@@ -298,9 +374,11 @@ describe('setup agents', () => {
298
374
  status: 'ready',
299
375
  installs: [{ target: 'claude-desktop', scope: 'global', mode: 'mcp' }],
300
376
  });
301
- const pluginPath = join(tempDir, '.ktx/agents/claude/ktx-plugin.zip');
377
+ const analyticsSkillPath = join(tempDir, '.ktx/agents/claude/ktx-analytics.zip');
378
+ const adminSkillPath = join(tempDir, '.ktx/agents/claude/ktx.zip');
302
379
  const launcherPath = join(tempDir, '.ktx/agents/claude/ktx-plugin-runner.sh');
303
- await expect(stat(pluginPath)).resolves.toBeDefined();
380
+ await expect(stat(analyticsSkillPath)).resolves.toBeDefined();
381
+ await expect(stat(adminSkillPath)).rejects.toThrow();
304
382
  const launcherStat = await stat(launcherPath);
305
383
  expect(launcherStat.mode & 0o111).not.toBe(0);
306
384
  const launcher = await readFile(launcherPath, 'utf-8');
@@ -312,18 +390,21 @@ describe('setup agents', () => {
312
390
  command: launcherPath,
313
391
  args: ['--project-dir', tempDir, 'mcp', 'stdio'],
314
392
  });
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');
393
+ expect(await readZipText(analyticsSkillPath, 'ktx-analytics/SKILL.md')).toContain('KTX Analytics Workflow');
394
+ await expect(readZipText(analyticsSkillPath, 'ktx/SKILL.md')).rejects.toThrow('Missing zip entry');
395
+ await expect(readZipText(analyticsSkillPath, '.claude-plugin/plugin.json')).rejects.toThrow('Missing zip entry');
396
+ await expect(readZipText(analyticsSkillPath, 'skills/ktx-analytics/SKILL.md')).rejects.toThrow('Missing zip entry');
397
+ expect(io.stdout()).toContain('Claude Desktop');
398
+ expect(io.stdout()).toContain(analyticsSkillPath);
399
+ expect(io.stdout()).not.toContain(adminSkillPath);
325
400
  expect(io.stdout()).toContain('claude_desktop_config.json');
326
- expect(io.stdout()).toContain('Restart Claude Desktop');
401
+ expect(io.stdout()).toContain('Required before using agents');
402
+ expect(io.stdout()).toContain('1. Restart Claude Desktop');
403
+ expect(io.stdout()).toContain('Claude Desktop loads KTX MCP after restart.');
404
+ expect(io.stdout()).toContain('2. Upload Claude Desktop skills');
405
+ expect(io.stdout()).toContain('Customize > Skills > + > Create skill > Upload a skill');
406
+ expect(io.stdout()).toContain('Upload this file:');
407
+ expect(io.stdout()).toContain('Toggle the uploaded KTX skills on.');
327
408
  expect(io.stdout()).not.toContain('Run `ktx mcp start`');
328
409
  }
329
410
  finally {
@@ -374,7 +455,7 @@ describe('setup agents', () => {
374
455
  await rm(home, { recursive: true, force: true });
375
456
  }
376
457
  });
377
- it('includes the admin CLI skill in the Claude Desktop plugin zip when requested', async () => {
458
+ it('includes an uploadable admin CLI skill zip for Claude Desktop when requested', async () => {
378
459
  const home = await mkdtemp(join(tmpdir(), 'ktx-setup-agents-home-'));
379
460
  const previousHome = process.env.HOME;
380
461
  process.env.HOME = home;
@@ -393,12 +474,18 @@ describe('setup agents', () => {
393
474
  status: 'ready',
394
475
  installs: [{ target: 'claude-desktop', scope: 'global', mode: 'mcp-cli' }],
395
476
  });
396
- const pluginPath = join(tempDir, '.ktx/agents/claude/ktx-plugin.zip');
397
- const adminSkill = await readZipText(pluginPath, 'skills/ktx/SKILL.md');
477
+ const analyticsSkillPath = join(tempDir, '.ktx/agents/claude/ktx-analytics.zip');
478
+ const adminSkillPath = join(tempDir, '.ktx/agents/claude/ktx.zip');
479
+ expect(await readZipText(analyticsSkillPath, 'ktx-analytics/SKILL.md')).toContain('KTX Analytics Workflow');
480
+ await expect(readZipText(analyticsSkillPath, 'ktx/SKILL.md')).rejects.toThrow('Missing zip entry');
481
+ const adminSkill = await readZipText(adminSkillPath, 'ktx/SKILL.md');
398
482
  expect(adminSkill).toContain(`--project-dir ${tempDir}`);
399
483
  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');
484
+ await expect(readZipText(adminSkillPath, '.mcp.json')).rejects.toThrow('Missing zip entry');
485
+ await expect(readZipText(adminSkillPath, 'ktx-analytics/SKILL.md')).rejects.toThrow('Missing zip entry');
486
+ expect(io.stdout()).toContain(analyticsSkillPath);
487
+ expect(io.stdout()).toContain(adminSkillPath);
488
+ expect(io.stdout()).toContain('Upload each file separately:');
402
489
  }
403
490
  finally {
404
491
  process.env.HOME = previousHome;
@@ -455,6 +542,9 @@ describe('setup agents', () => {
455
542
  }, codexIo.io);
456
543
  expect(codexIo.stdout()).toContain('[mcp_servers.ktx]');
457
544
  expect(codexIo.stdout()).toContain('url = "http://localhost:7878/mcp"');
545
+ expect(codexIo.stdout()).toContain('1. Configure Codex');
546
+ expect(codexIo.stdout()).toContain('Open ~/.codex/config.toml, then paste this block:');
547
+ expect(codexIo.stdout()).toContain('PASTE:');
458
548
  const opencodeIo = makeIo();
459
549
  await runKtxSetupAgentsStep({
460
550
  projectDir: tempDir,
@@ -468,6 +558,8 @@ describe('setup agents', () => {
468
558
  }, opencodeIo.io);
469
559
  expect(opencodeIo.stdout()).toContain('"mcp"');
470
560
  expect(opencodeIo.stdout()).toContain('"type": "remote"');
561
+ expect(opencodeIo.stdout()).toContain('1. Configure OpenCode');
562
+ expect(opencodeIo.stdout()).toContain('Open opencode.json, then paste this block:');
471
563
  await expect(readFile(join(tempDir, 'opencode.json'), 'utf-8')).rejects.toThrow();
472
564
  const universalIo = makeIo();
473
565
  await runKtxSetupAgentsStep({
@@ -482,6 +574,8 @@ describe('setup agents', () => {
482
574
  }, universalIo.io);
483
575
  expect(universalIo.stdout()).toContain('Universal MCP endpoint:');
484
576
  expect(universalIo.stdout()).toContain('http://localhost:7878/mcp');
577
+ expect(universalIo.stdout()).toContain('1. Configure unsupported MCP clients');
578
+ expect(universalIo.stdout()).toContain('Use this endpoint when setting up unsupported MCP clients:');
485
579
  });
486
580
  it('uses MCP daemon state for port and token metadata without rendering literal tokens', async () => {
487
581
  await mkdir(join(tempDir, '.ktx'), { recursive: true });
@@ -513,7 +607,9 @@ describe('setup agents', () => {
513
607
  expect(rendered).toContain('http://127.0.0.1:8787/mcp');
514
608
  expect(rendered).toContain('Bearer ${KTX_MCP_TOKEN}');
515
609
  expect(rendered).not.toContain('secret-token');
516
- expect(io.stdout()).toContain('Run `ktx mcp start` to enable the configured KTX MCP server.');
610
+ expect(io.stdout()).toContain('Run this command before using Claude Code:');
611
+ expect(io.stdout()).toContain('RUN:');
612
+ expect(io.stdout()).toContain(`ktx mcp start --project-dir ${tempDir}`);
517
613
  }
518
614
  finally {
519
615
  if (previousToken === undefined) {
@@ -567,7 +663,7 @@ describe('setup agents', () => {
567
663
  await expect(stat(join(tempDir, '.claude/skills/ktx/keep.txt'))).resolves.toBeDefined();
568
664
  await expect(readKtxAgentInstallManifest(tempDir)).resolves.toEqual(null);
569
665
  });
570
- it('removes generated Claude Desktop plugin from the manifest', async () => {
666
+ it('removes generated Claude Desktop skill zips from the manifest', async () => {
571
667
  const home = await mkdtemp(join(tmpdir(), 'ktx-setup-agents-home-'));
572
668
  const previousHome = process.env.HOME;
573
669
  process.env.HOME = home;
@@ -583,15 +679,18 @@ describe('setup agents', () => {
583
679
  mode: 'mcp-cli',
584
680
  skipAgents: false,
585
681
  }, io.io);
586
- const pluginPath = join(tempDir, '.ktx/agents/claude/ktx-plugin.zip');
682
+ const analyticsSkillPath = join(tempDir, '.ktx/agents/claude/ktx-analytics.zip');
683
+ const adminSkillPath = join(tempDir, '.ktx/agents/claude/ktx.zip');
587
684
  const launcherPath = join(tempDir, '.ktx/agents/claude/ktx-plugin-runner.sh');
588
685
  const configPath = join(home, 'Library/Application Support/Claude/claude_desktop_config.json');
589
- await expect(stat(pluginPath)).resolves.toBeDefined();
686
+ await expect(stat(analyticsSkillPath)).resolves.toBeDefined();
687
+ await expect(stat(adminSkillPath)).resolves.toBeDefined();
590
688
  await expect(stat(launcherPath)).resolves.toBeDefined();
591
689
  const beforeConfig = JSON.parse(await readFile(configPath, 'utf-8'));
592
690
  expect(beforeConfig.mcpServers.ktx).toBeDefined();
593
691
  await expect(removeKtxAgentInstall(tempDir, io.io)).resolves.toBe(0);
594
- await expect(stat(pluginPath)).rejects.toThrow();
692
+ await expect(stat(analyticsSkillPath)).rejects.toThrow();
693
+ await expect(stat(adminSkillPath)).rejects.toThrow();
595
694
  await expect(stat(launcherPath)).rejects.toThrow();
596
695
  const afterConfig = JSON.parse(await readFile(configPath, 'utf-8'));
597
696
  expect(afterConfig.mcpServers.ktx).toBeUndefined();
@@ -619,7 +718,7 @@ describe('setup agents', () => {
619
718
  skipAgents: false,
620
719
  }, io.io, { prompts })).resolves.toEqual({ status: 'skipped', projectDir: tempDir });
621
720
  });
622
- it('explains how to select multiple agent targets in interactive mode', async () => {
721
+ it('prints one navigation hint before interactive agent target prompts', async () => {
623
722
  const io = makeIo();
624
723
  const prompts = {
625
724
  select: vi.fn(async () => 'mcp-cli'),
@@ -635,8 +734,10 @@ describe('setup agents', () => {
635
734
  mode: 'mcp-cli',
636
735
  skipAgents: false,
637
736
  }, io.io, { prompts })).resolves.toEqual({ status: 'back', projectDir: tempDir });
737
+ expect(io.stdout()).toContain('Space to select, Enter to confirm, Esc to go back.');
738
+ expect(io.stdout().match(/Space to select/g)).toHaveLength(1);
638
739
  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.',
740
+ message: 'Which agent targets should KTX install?',
640
741
  }));
641
742
  });
642
743
  it('prints per-agent install summary after successful installation', async () => {
@@ -652,45 +753,242 @@ describe('setup agents', () => {
652
753
  skipAgents: false,
653
754
  }, io.io);
654
755
  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');
756
+ expect(output).toContain('Claude Code · Project scope');
757
+ expect(output).toContain(join(tempDir, '.mcp.json'));
758
+ expect(output).toContain('Requires MCP to be started.');
759
+ expect(output).toContain('Analytics skill installed.');
760
+ expect(output).toContain('Admin CLI skill installed.');
761
+ expect(output).not.toContain('Agent integration complete');
762
+ expect(output).not.toContain(`KTX project\n ${tempDir}`);
763
+ expect(output).not.toContain('Installed agents');
764
+ expect(output).not.toContain('.claude/skills/ktx-analytics/SKILL.md');
765
+ expect(output).not.toContain('.claude/skills/ktx/SKILL.md');
766
+ expect(output).not.toContain('.claude/rules/ktx.md');
663
767
  });
664
- it('formats summary with relative paths for project scope', () => {
665
- const summary = formatInstallSummary([{ target: 'cursor', scope: 'project', mode: 'mcp-cli' }], [
768
+ it('formats summary with explicit project-scoped config paths', () => {
769
+ const summary = formatInstallSummaryLines([{ target: 'cursor', scope: 'project', mode: 'mcp-cli' }], [
666
770
  { kind: 'file', path: join(tempDir, '.cursor/rules/ktx-analytics.mdc'), role: 'analytics-skill' },
667
771
  { kind: 'file', path: join(tempDir, '.cursor/rules/ktx.mdc') },
772
+ { kind: 'json-key', path: join(tempDir, '.cursor/mcp.json'), jsonPath: ['mcpServers', 'ktx'] },
668
773
  ], 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);
774
+ expect(summary).toEqual([
775
+ {
776
+ title: 'Cursor · Project scope',
777
+ lines: [
778
+ join(tempDir, '.cursor/mcp.json'),
779
+ 'Requires MCP to be started.',
780
+ 'Cursor rules installed.',
781
+ ],
782
+ },
783
+ ]);
675
784
  });
676
785
  it('formats summary with multiple agent targets', () => {
677
- const summary = formatInstallSummary([
786
+ const summary = formatInstallSummaryLines([
678
787
  { target: 'claude-code', scope: 'project', mode: 'mcp-cli' },
679
788
  { target: 'codex', scope: 'project', mode: 'mcp-cli' },
680
789
  ], [
681
790
  { kind: 'file', path: join(tempDir, '.claude/skills/ktx-analytics/SKILL.md'), role: 'analytics-skill' },
682
791
  { kind: 'file', path: join(tempDir, '.claude/skills/ktx/SKILL.md'), role: 'skill' },
683
792
  { kind: 'file', path: join(tempDir, '.claude/rules/ktx.md'), role: 'rule' },
793
+ { kind: 'json-key', path: join(tempDir, '.mcp.json'), jsonPath: ['mcpServers', 'ktx'] },
684
794
  { kind: 'file', path: join(tempDir, '.agents/skills/ktx-analytics/SKILL.md'), role: 'analytics-skill' },
685
795
  { kind: 'file', path: join(tempDir, '.agents/skills/ktx/SKILL.md'), role: 'skill' },
686
796
  { kind: 'file', path: join(tempDir, '.codex/instructions/ktx.md'), role: 'rule' },
687
797
  ], 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');
798
+ expect(summary).toEqual([
799
+ {
800
+ title: 'Claude Code · Project scope',
801
+ lines: [
802
+ join(tempDir, '.mcp.json'),
803
+ 'Requires MCP to be started.',
804
+ 'Analytics skill installed.',
805
+ 'Admin CLI skill installed.',
806
+ ],
807
+ },
808
+ {
809
+ title: 'Codex · Project scope',
810
+ lines: [
811
+ 'Add the snippet shown below to ~/.codex/config.toml.',
812
+ 'Requires MCP to be started.',
813
+ 'Codex guidance installed.',
814
+ ],
815
+ },
816
+ ]);
817
+ });
818
+ it('prints one target-aware next actions block for mixed agent targets', async () => {
819
+ const home = await mkdtemp(join(tmpdir(), 'ktx-setup-agents-home-'));
820
+ const previousHome = process.env.HOME;
821
+ process.env.HOME = home;
822
+ try {
823
+ const io = makeIo();
824
+ const prompts = {
825
+ select: vi.fn(async ({ message }) => message.startsWith('Where should') ? 'project' : 'mcp'),
826
+ multiselect: vi.fn(async () => ['claude-code', 'claude-desktop']),
827
+ cancel: vi.fn(),
828
+ };
829
+ await expect(runKtxSetupAgentsStep({
830
+ projectDir: tempDir,
831
+ inputMode: 'auto',
832
+ yes: false,
833
+ agents: true,
834
+ scope: 'project',
835
+ mode: 'mcp',
836
+ skipAgents: false,
837
+ }, io.io, { prompts })).resolves.toMatchObject({
838
+ status: 'ready',
839
+ installs: [
840
+ { target: 'claude-code', scope: 'project', mode: 'mcp' },
841
+ { target: 'claude-desktop', scope: 'global', mode: 'mcp' },
842
+ ],
843
+ });
844
+ const output = io.stdout();
845
+ expect(output).toContain('Required before using agents');
846
+ expect(output).not.toContain('Next actions');
847
+ expect(output).toContain('1. Start MCP');
848
+ expect(output).toContain('Run this command before using Claude Code:');
849
+ expect(output).toContain(`ktx mcp start --project-dir ${tempDir}`);
850
+ expect(output).toContain(`ktx mcp stop --project-dir ${tempDir}\n\n2. Open Claude Code`);
851
+ expect(output).toContain('Open Claude Code from the KTX project directory');
852
+ expect(output).toContain('RUN:');
853
+ expect(output).toContain(`cd '${tempDir}'`);
854
+ expect(output).toContain('3. Restart Claude Desktop');
855
+ expect(output).toContain('Claude Desktop loads KTX MCP after restart.');
856
+ expect(output).toContain('4. Upload Claude Desktop skills');
857
+ expect(output).toContain('Customize > Skills > + > Create skill > Upload a skill');
858
+ expect(output).toContain(join(tempDir, '.ktx/agents/claude/ktx-analytics.zip'));
859
+ expect(output).not.toContain(join(tempDir, '.ktx/agents/claude/ktx.zip'));
860
+ expect(output).toContain('Upload this file:');
861
+ expect(output).toContain('All set.');
862
+ expect(output).not.toContain('Finish Claude Desktop setup');
863
+ expect(output).not.toContain('Run `ktx mcp start` to enable the configured KTX MCP server.');
864
+ }
865
+ finally {
866
+ process.env.HOME = previousHome;
867
+ await rm(home, { recursive: true, force: true });
868
+ }
869
+ });
870
+ it('does not tell global Claude Code installs to open from the project directory', async () => {
871
+ const home = await mkdtemp(join(tmpdir(), 'ktx-setup-agents-home-'));
872
+ const previousHome = process.env.HOME;
873
+ process.env.HOME = home;
874
+ try {
875
+ const io = makeIo();
876
+ await expect(runKtxSetupAgentsStep({
877
+ projectDir: tempDir,
878
+ inputMode: 'disabled',
879
+ yes: true,
880
+ agents: true,
881
+ target: 'claude-code',
882
+ scope: 'global',
883
+ mode: 'mcp',
884
+ skipAgents: false,
885
+ }, io.io)).resolves.toMatchObject({
886
+ status: 'ready',
887
+ installs: [{ target: 'claude-code', scope: 'global', mode: 'mcp' }],
888
+ });
889
+ const output = io.stdout();
890
+ expect(output).toContain('2. Open Claude Code');
891
+ expect(output).toContain('RUN:');
892
+ expect(output).toContain('claude');
893
+ expect(output).not.toContain('Open Claude Code from the KTX project directory');
894
+ expect(output).not.toContain(`cd '${tempDir}'`);
895
+ }
896
+ finally {
897
+ process.env.HOME = previousHome;
898
+ await rm(home, { recursive: true, force: true });
899
+ }
900
+ });
901
+ it('explains next actions for Codex, Cursor, OpenCode, and universal MCP targets', async () => {
902
+ const io = makeIo();
903
+ const prompts = {
904
+ select: vi.fn(async () => 'mcp-cli'),
905
+ multiselect: vi.fn(async () => ['codex', 'cursor', 'opencode', 'universal']),
906
+ cancel: vi.fn(),
907
+ };
908
+ await expect(runKtxSetupAgentsStep({
909
+ projectDir: tempDir,
910
+ inputMode: 'auto',
911
+ yes: false,
912
+ agents: true,
913
+ scope: 'project',
914
+ mode: 'mcp-cli',
915
+ skipAgents: false,
916
+ }, io.io, { prompts })).resolves.toMatchObject({
917
+ status: 'ready',
918
+ installs: [
919
+ { target: 'codex', scope: 'project', mode: 'mcp-cli' },
920
+ { target: 'cursor', scope: 'project', mode: 'mcp-cli' },
921
+ { target: 'opencode', scope: 'project', mode: 'mcp-cli' },
922
+ { target: 'universal', scope: 'project', mode: 'mcp-cli' },
923
+ ],
924
+ });
925
+ const output = io.stdout();
926
+ expect(output).toContain('Required before using agents');
927
+ expect(output).toContain('1. Configure Codex');
928
+ expect(output).toContain('2. Configure OpenCode');
929
+ expect(output).toContain('3. Configure unsupported MCP clients');
930
+ expect(output).toContain('4. Start MCP');
931
+ expect(output).toContain('Run this command before using Codex, Cursor, OpenCode, and Universal .agents:');
932
+ expect(output).toContain('Open Cursor from the KTX project directory');
933
+ expect(output).toContain('Open ~/.codex/config.toml, then paste this block:\n\n PASTE:\n [mcp_servers.ktx]');
934
+ expect(output).toContain('Open opencode.json, then paste this block:');
935
+ expect(output).toContain('Use this endpoint when setting up unsupported MCP clients:');
936
+ expect(output).toContain('Codex guidance installed');
937
+ expect(output).toContain('Cursor rules installed');
938
+ expect(output).toContain('OpenCode commands installed');
939
+ expect(output).toContain('.agents guidance installed');
940
+ });
941
+ describe('createAgentNextActionsLineFormatter', () => {
942
+ function makeColorStdout() {
943
+ return { write: () => true, hasColors: () => true };
944
+ }
945
+ function makePlainStdout() {
946
+ return { write: () => true, hasColors: () => false };
947
+ }
948
+ const ESC = String.fromCharCode(27);
949
+ it('returns the line untouched when the stream cannot render colors', () => {
950
+ const format = createAgentNextActionsLineFormatter(makePlainStdout());
951
+ expect(format('2. Upload Claude Desktop skills')).toBe('2. Upload Claude Desktop skills');
952
+ expect(format(' /tmp/ktx/.ktx/agents/claude/ktx.zip')).toBe(' /tmp/ktx/.ktx/agents/claude/ktx.zip');
953
+ });
954
+ it('styles step headings and aligns sub-prose under the title', () => {
955
+ const format = createAgentNextActionsLineFormatter(makeColorStdout());
956
+ const heading = format('2. Upload Claude Desktop skills');
957
+ expect(heading).toContain(ESC);
958
+ expect(heading).toContain('2');
959
+ expect(heading).toContain('Upload Claude Desktop skills');
960
+ expect(heading).not.toMatch(/^2\. /);
961
+ const sub = format(' Toggle the uploaded KTX skills on.');
962
+ expect(sub).toMatch(/^ {3}/);
963
+ expect(sub).toContain('Toggle the uploaded KTX skills on.');
964
+ });
965
+ it('renders skill bundle .zip paths as bullets and shortens HOME to ~', () => {
966
+ const previousHome = process.env.HOME;
967
+ process.env.HOME = '/tmp/test-home';
968
+ try {
969
+ const format = createAgentNextActionsLineFormatter(makeColorStdout());
970
+ const line = format(' /tmp/test-home/.ktx/agents/claude/ktx-analytics.zip');
971
+ expect(line).toContain('•');
972
+ expect(line).toContain('~/.ktx/agents/claude/ktx-analytics.zip');
973
+ expect(line).not.toContain('/tmp/test-home/');
974
+ }
975
+ finally {
976
+ if (previousHome === undefined)
977
+ delete process.env.HOME;
978
+ else
979
+ process.env.HOME = previousHome;
980
+ }
981
+ });
982
+ it('replaces breadcrumb separators with a typographic chevron', () => {
983
+ const format = createAgentNextActionsLineFormatter(makeColorStdout());
984
+ const line = format(' Open Claude Desktop: Customize > Skills > + > Create skill > Upload a skill.');
985
+ expect(line).toContain('›');
986
+ expect(line).not.toContain(' > ');
987
+ });
988
+ it('leaves already-styled lines untouched to avoid double-wrapping', () => {
989
+ const format = createAgentNextActionsLineFormatter(makeColorStdout());
990
+ const preStyled = `${ESC}[1m2. Already styled${ESC}[22m`;
991
+ expect(format(preStyled)).toBe(preStyled);
992
+ });
695
993
  });
696
994
  });
@@ -9,7 +9,6 @@ export interface KtxSetupRuntimeArgs {
9
9
  inputMode: 'auto' | 'disabled';
10
10
  cliVersion: string;
11
11
  runtimeInstallPolicy: KtxManagedPythonInstallPolicy;
12
- agents: boolean;
13
12
  databaseIntrospectionFallback?: boolean;
14
13
  }
15
14
  export type KtxSetupRuntimeResult = {
@@ -9,7 +9,6 @@ export async function runKtxSetupRuntimeStep(args, io, deps = {}) {
9
9
  const loadProjectForRuntime = deps.loadProject ?? loadKtxProject;
10
10
  const project = await loadProjectForRuntime({ projectDir: args.projectDir });
11
11
  const requirements = resolveProjectRuntimeRequirements(project.config, {
12
- agents: args.agents,
13
12
  databaseIntrospectionFallback: args.databaseIntrospectionFallback,
14
13
  env: deps.env ?? process.env,
15
14
  });