@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
@@ -2,12 +2,83 @@ import { existsSync } from 'node:fs';
2
2
  import { chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
3
3
  import { dirname, join, relative, resolve } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
+ import { styleText } from 'node:util';
6
+ import { log, outro } from '@clack/prompts';
5
7
  import { loadKtxProject, markKtxSetupStateStepComplete, serializeKtxProjectConfig, } from '@ktx/context/project';
6
8
  import { strToU8, zipSync } from 'fflate';
7
- import { bold, dim, green } from './io/symbols.js';
8
- import { withMultiselectNavigation } from './prompt-navigation.js';
9
9
  import { createKtxSetupPromptAdapter, createKtxSetupUiAdapter, } from './setup-prompts.js';
10
10
  import { readKtxMcpDaemonStatus } from './managed-mcp-daemon.js';
11
+ const MCP_DAEMON_REQUIRED_NOTICE = 'mcp-daemon-required';
12
+ function isWritableTtyOutput(output) {
13
+ return (output.isTTY === true &&
14
+ typeof output.on === 'function' &&
15
+ typeof output.columns !== 'undefined');
16
+ }
17
+ function writeSetupInfo(io, message) {
18
+ if (isWritableTtyOutput(io.stdout)) {
19
+ log.info(message, { output: io.stdout });
20
+ return;
21
+ }
22
+ io.stdout.write(`${message}\n`);
23
+ }
24
+ function writeSetupStep(io, message) {
25
+ if (isWritableTtyOutput(io.stdout)) {
26
+ log.step(message, { output: io.stdout });
27
+ return;
28
+ }
29
+ io.stdout.write(`\n${message}\n`);
30
+ }
31
+ function writeSetupOutro(io, message) {
32
+ if (isWritableTtyOutput(io.stdout)) {
33
+ outro(message, { output: io.stdout });
34
+ return;
35
+ }
36
+ io.stdout.write(`\n${message}\n`);
37
+ }
38
+ const STEP_HEADING_RE = /^(\d+)\. (.+)$/;
39
+ const ACTION_MARKER_RE = /^(RUN|PASTE|USE|OPEN):$/;
40
+ export function createAgentNextActionsLineFormatter(stdout) {
41
+ const maybeHasColors = stdout.hasColors;
42
+ const supportsColor = typeof maybeHasColors === 'function' && Boolean(maybeHasColors.call(stdout));
43
+ if (!supportsColor)
44
+ return (line) => line;
45
+ const homeDir = process.env.HOME ? resolve(process.env.HOME) : '';
46
+ const styleOptions = { validateStream: false };
47
+ const dim = (s) => styleText('dim', s, styleOptions);
48
+ const bold = (s) => styleText('bold', s, styleOptions);
49
+ const cyanBold = (s) => styleText(['cyan', 'bold'], s, styleOptions);
50
+ const dimCyan = (s) => styleText(['dim', 'cyan'], s, styleOptions);
51
+ const shortenPath = (path) => {
52
+ if (!homeDir)
53
+ return path;
54
+ if (path === homeDir)
55
+ return '~';
56
+ if (path.startsWith(`${homeDir}/`))
57
+ return `~/${path.slice(homeDir.length + 1)}`;
58
+ return path;
59
+ };
60
+ return (rawLine) => {
61
+ if (rawLine.length === 0 || rawLine.includes('['))
62
+ return rawLine;
63
+ const heading = rawLine.match(STEP_HEADING_RE);
64
+ if (heading) {
65
+ return `${cyanBold(heading[1])} ${bold(heading[2])}`;
66
+ }
67
+ if (!rawLine.startsWith(' '))
68
+ return rawLine;
69
+ const body = rawLine.slice(2);
70
+ if (ACTION_MARKER_RE.test(body)) {
71
+ return ` ${dim(body)}`;
72
+ }
73
+ if (body.endsWith('.zip') && (body.startsWith('/') || body.startsWith('~'))) {
74
+ return ` ${dimCyan('•')} ${shortenPath(body)}`;
75
+ }
76
+ if (body.includes(' > ')) {
77
+ return ` ${body.replaceAll(' > ', ` ${dim('›')} `)}`;
78
+ }
79
+ return ` ${dim(body)}`;
80
+ };
81
+ }
11
82
  async function readJsonObject(path) {
12
83
  if (!existsSync(path))
13
84
  return {};
@@ -158,7 +229,7 @@ async function installMcpClientConfig(input) {
158
229
  }
159
230
  const endpoint = await resolveMcpEndpoint(input.projectDir);
160
231
  if (!endpoint.running) {
161
- notices.push('Run `ktx mcp start` to enable the configured KTX MCP server.');
232
+ notices.push(MCP_DAEMON_REQUIRED_NOTICE);
162
233
  }
163
234
  if (input.target === 'claude-code') {
164
235
  const config = claudeConfigPath(input.projectDir, input.scope);
@@ -171,16 +242,16 @@ async function installMcpClientConfig(input) {
171
242
  entries.push({ kind: 'json-key', path: config.path, jsonPath: config.jsonPath });
172
243
  }
173
244
  else if (input.target === 'codex') {
174
- snippets.push(`Codex MCP snippet for ~/.codex/config.toml:\n${codexSnippet(endpoint)}`);
245
+ snippets.push(`Add this Codex MCP snippet to ~/.codex/config.toml:\n${codexSnippet(endpoint)}`);
175
246
  }
176
247
  else if (input.target === 'opencode') {
177
248
  const path = input.scope === 'global'
178
249
  ? '~/.config/opencode/opencode.json'
179
250
  : relative(input.projectDir, join(input.projectDir, 'opencode.json'));
180
- snippets.push(`opencode MCP snippet for ${path}:\n${opencodeSnippet(endpoint)}`);
251
+ snippets.push(`Add this OpenCode MCP snippet to ${path}:\n${opencodeSnippet(endpoint)}`);
181
252
  }
182
253
  else if (input.target === 'universal') {
183
- snippets.push(universalMcpSnippet(endpoint));
254
+ snippets.push(`Use this universal MCP endpoint with unsupported MCP clients:\n${universalMcpSnippet(endpoint)}`);
184
255
  }
185
256
  return { entries, snippets, notices };
186
257
  }
@@ -202,8 +273,11 @@ function plannedMcpJsonEntries(input) {
202
273
  export function agentInstallManifestPath(projectDir) {
203
274
  return join(resolve(projectDir), '.ktx/agents/install-manifest.json');
204
275
  }
205
- function claudeDesktopPluginPath(projectDir) {
206
- return join(resolve(projectDir), '.ktx/agents/claude/ktx-plugin.zip');
276
+ function claudeDesktopAnalyticsSkillBundlePath(projectDir) {
277
+ return join(resolve(projectDir), '.ktx/agents/claude/ktx-analytics.zip');
278
+ }
279
+ function claudeDesktopAdminSkillBundlePath(projectDir) {
280
+ return join(resolve(projectDir), '.ktx/agents/claude/ktx.zip');
207
281
  }
208
282
  function claudeDesktopLauncherPath(projectDir) {
209
283
  return join(resolve(projectDir), '.ktx/agents/claude/ktx-plugin-runner.sh');
@@ -241,7 +315,20 @@ export function plannedKtxAgentFiles(input) {
241
315
  if (input.target === 'claude-desktop') {
242
316
  return [
243
317
  { kind: 'file', path: claudeDesktopLauncherPath(input.projectDir), role: 'launcher' },
244
- { kind: 'file', path: claudeDesktopPluginPath(input.projectDir), role: 'claude-plugin' },
318
+ {
319
+ kind: 'file',
320
+ path: claudeDesktopAnalyticsSkillBundlePath(input.projectDir),
321
+ role: 'claude-desktop-skill-bundle',
322
+ },
323
+ ...(withAdminCli
324
+ ? [
325
+ {
326
+ kind: 'file',
327
+ path: claudeDesktopAdminSkillBundlePath(input.projectDir),
328
+ role: 'claude-desktop-skill-bundle',
329
+ },
330
+ ]
331
+ : []),
245
332
  ];
246
333
  }
247
334
  throw new Error(`Global ${input.target} installation is not supported; omit --global.`);
@@ -361,36 +448,7 @@ function cliInstructionContent(input) {
361
448
  '',
362
449
  ].join('\n');
363
450
  }
364
- function claudePluginJsonContent() {
365
- return `${JSON.stringify({
366
- name: 'ktx',
367
- version: '0.0.0-local',
368
- description: 'KTX analytics workflow guidance and local MCP tools.',
369
- }, null, 2)}\n`;
370
- }
371
- function claudePluginVersionContent() {
372
- return `${JSON.stringify({ version: '0.0.0-local' }, null, 2)}\n`;
373
- }
374
- function claudePluginSetupContent(input) {
375
- return [
376
- '# KTX Claude Plugin',
377
- '',
378
- 'Install this plugin ZIP from Claude Desktop to load the KTX analytics skill.',
379
- '',
380
- `KTX project: \`${input.projectDir}\``,
381
- '',
382
- 'Included:',
383
- '',
384
- '- `ktx-analytics` skill for the MCP analytics workflow',
385
- ...(input.withAdminCli ? ['- `ktx` admin CLI skill for KTX maintenance commands'] : []),
386
- '',
387
- 'The KTX MCP server is registered separately in `claude_desktop_config.json` by `ktx setup` and runs as a local stdio child of Claude Desktop — no daemon to start.',
388
- '',
389
- 'If this checkout or project directory moves, rerun `ktx setup --agents` and reinstall the regenerated plugin.',
390
- '',
391
- ].join('\n');
392
- }
393
- function claudePluginLauncherContent(input) {
451
+ function claudeDesktopLauncherContent(input) {
394
452
  const binPath = input.launcher.args[0];
395
453
  if (!binPath) {
396
454
  throw new Error('Expected KTX CLI launcher to include a bin path.');
@@ -437,28 +495,33 @@ function claudePluginLauncherContent(input) {
437
495
  ' run_with_node "$(command -v node)" "$@"',
438
496
  'fi',
439
497
  '',
440
- 'echo "KTX plugin could not find Node.js. Set KTX_NODE to a Node executable and reinstall the plugin." >&2',
498
+ 'echo "KTX Claude Desktop launcher could not find Node.js. Set KTX_NODE to a Node executable and rerun ktx setup --agents." >&2',
441
499
  'exit 127',
442
500
  '',
443
501
  ].join('\n');
444
502
  }
445
- async function writeClaudeDesktopPlugin(input) {
446
- const withAdminCli = input.mode === 'mcp-cli';
503
+ async function writeClaudeDesktopSkillBundle(input) {
504
+ const content = input.skillName === 'ktx-analytics'
505
+ ? await readAnalyticsSkillContent()
506
+ : cliInstructionContent({ projectDir: input.projectDir, launcher: input.launcher });
447
507
  const files = {
448
- '.claude-plugin/plugin.json': strToU8(claudePluginJsonContent()),
449
- 'version.json': strToU8(claudePluginVersionContent()),
450
- 'skills/ktx-analytics/SKILL.md': strToU8(await readAnalyticsSkillContent()),
451
- 'SETUP.md': strToU8(claudePluginSetupContent({ projectDir: input.projectDir, withAdminCli })),
508
+ [`${input.skillName}/SKILL.md`]: strToU8(content),
452
509
  };
453
- if (withAdminCli) {
454
- files['skills/ktx/SKILL.md'] = strToU8(cliInstructionContent({ projectDir: input.projectDir, launcher: input.launcher }));
455
- }
456
510
  await mkdir(dirname(input.path), { recursive: true });
457
511
  await writeFile(input.path, Buffer.from(zipSync(files)));
458
512
  }
513
+ function claudeDesktopSkillNameForBundle(path) {
514
+ if (path.endsWith('/ktx-analytics.zip')) {
515
+ return 'ktx-analytics';
516
+ }
517
+ if (path.endsWith('/ktx.zip')) {
518
+ return 'ktx';
519
+ }
520
+ throw new Error(`Unsupported Claude Desktop skill bundle path: ${path}`);
521
+ }
459
522
  async function writeClaudeDesktopLauncher(input) {
460
523
  await mkdir(dirname(input.path), { recursive: true });
461
- await writeFile(input.path, claudePluginLauncherContent({ launcher: input.launcher }), 'utf-8');
524
+ await writeFile(input.path, claudeDesktopLauncherContent({ launcher: input.launcher }), 'utf-8');
462
525
  await chmod(input.path, 0o755);
463
526
  }
464
527
  function ruleInstructionContent(input) {
@@ -550,16 +613,8 @@ const targetDisplayNames = {
550
613
  opencode: 'OpenCode',
551
614
  universal: 'Universal .agents',
552
615
  };
553
- const fileEntryLabels = {
554
- 'claude-code': 'Skill installed',
555
- 'claude-desktop': 'Skill installed',
556
- codex: 'Skill installed',
557
- cursor: 'Rule installed',
558
- opencode: 'Command installed',
559
- universal: 'Skill installed',
560
- };
561
- function mcpEntryLabel(entry) {
562
- return `MCP config installed — connects client agents to KTX MCP tools (${entry.jsonPath.join('.')})`;
616
+ export function targetDisplayName(target) {
617
+ return Object.hasOwn(targetDisplayNames, target) ? targetDisplayNames[target] : target;
563
618
  }
564
619
  function targetSupportsGlobalScope(target) {
565
620
  return target === 'claude-code' || target === 'codex';
@@ -567,7 +622,61 @@ function targetSupportsGlobalScope(target) {
567
622
  function effectiveInstallScope(target, requestedScope) {
568
623
  return target === 'claude-desktop' ? 'global' : requestedScope;
569
624
  }
570
- export function formatInstallSummary(installs, entries, projectDir) {
625
+ function scopeDisplayName(scope) {
626
+ if (scope === 'project')
627
+ return 'Project scope';
628
+ if (scope === 'global')
629
+ return 'Global scope';
630
+ return 'Local scope';
631
+ }
632
+ function targetUsesHttpMcpDaemon(target) {
633
+ return target !== 'claude-desktop';
634
+ }
635
+ function manualMcpConfigInstruction(target, scope) {
636
+ if (target === 'codex') {
637
+ return 'Add the snippet shown below to ~/.codex/config.toml.';
638
+ }
639
+ if (target === 'opencode') {
640
+ return scope === 'global'
641
+ ? 'Add the snippet shown below to ~/.config/opencode/opencode.json.'
642
+ : 'Add the snippet shown below to opencode.json.';
643
+ }
644
+ if (target === 'universal') {
645
+ return 'Use the printed endpoint with unsupported MCP clients.';
646
+ }
647
+ return 'Add the printed snippet manually.';
648
+ }
649
+ function guidanceInstallLine(target) {
650
+ if (target === 'codex')
651
+ return 'Codex guidance installed';
652
+ if (target === 'cursor')
653
+ return 'Cursor rules installed';
654
+ if (target === 'opencode')
655
+ return 'OpenCode commands installed';
656
+ if (target === 'universal')
657
+ return '.agents guidance installed';
658
+ return 'Agent guidance installed';
659
+ }
660
+ function hasEntryRole(entries, role) {
661
+ return entries.some((entry) => entry.kind === 'file' && entry.role === role);
662
+ }
663
+ function hasAdminCliEntries(entries) {
664
+ return entries.some((entry) => entry.kind === 'file' &&
665
+ (entry.role === 'skill' || entry.role === 'rule' || entry.role === undefined));
666
+ }
667
+ function formatInlinePath(path) {
668
+ const home = process.env.HOME;
669
+ if (!home)
670
+ return path;
671
+ const resolvedHome = resolve(home);
672
+ if (path === resolvedHome)
673
+ return '~';
674
+ if (path.startsWith(`${resolvedHome}/`)) {
675
+ return `~/${relative(resolvedHome, path)}`;
676
+ }
677
+ return path;
678
+ }
679
+ export function formatInstallSummaryLines(installs, entries, projectDir) {
571
680
  const entriesByTarget = new Map();
572
681
  for (const install of installs) {
573
682
  const plannedFilePaths = new Set(plannedKtxAgentFiles({ projectDir, ...install })
@@ -580,45 +689,196 @@ export function formatInstallSummary(installs, entries, projectDir) {
580
689
  const plannedMcpKeys = new Set(plannedMcpJsonEntries({ projectDir, ...install }).map(entryKey));
581
690
  mcpEntriesByTarget.set(install.target, entries.filter((entry) => entry.kind === 'json-key' && plannedMcpKeys.has(entryKey(entry))));
582
691
  }
583
- const fileHints = {
584
- skill: 'teaches admin agents which KTX CLI commands to run',
585
- rule: 'tells admin agents when to use KTX CLI',
586
- 'analytics-skill': 'teaches your agent the KTX MCP analytics workflow',
587
- 'claude-plugin': 'bundles KTX skills for Claude Desktop (MCP server is registered in claude_desktop_config.json)',
588
- launcher: 'runs the local KTX CLI with an available Node.js for Claude Desktop',
589
- };
590
- const lines = [];
591
- for (const install of installs) {
692
+ return installs.map((install) => {
592
693
  const targetEntries = entriesByTarget.get(install.target) ?? [];
593
- lines.push(` ${targetDisplayNames[install.target]}`);
594
- for (const entry of targetEntries) {
595
- if (entry.kind === 'file') {
596
- const isRule = entry.role === 'rule' || (!entry.role && fileEntryLabels[install.target] === 'Rule installed');
597
- const label = entry.role === 'analytics-skill'
598
- ? 'Analytics skill installed'
599
- : entry.role === 'claude-plugin'
600
- ? 'Claude plugin generated'
601
- : entry.role === 'launcher'
602
- ? 'Launcher installed'
603
- : isRule
604
- ? 'Rule installed'
605
- : fileEntryLabels[install.target];
606
- const hint = fileHints[isRule ? 'rule' : (entry.role ?? 'skill')] ?? '';
607
- lines.push(` + ${label} ${hint}`);
608
- if (entry.role !== 'claude-plugin') {
609
- const displayPath = install.scope === 'global' ? entry.path : relative(projectDir, entry.path);
610
- lines.push(` ${displayPath}`);
694
+ const mcpEntry = mcpEntriesByTarget
695
+ .get(install.target)
696
+ ?.find((entry) => entry.kind === 'json-key');
697
+ const lines = [];
698
+ if (mcpEntry) {
699
+ lines.push(formatInlinePath(mcpEntry.path));
700
+ }
701
+ else if (install.target !== 'claude-desktop') {
702
+ lines.push(manualMcpConfigInstruction(install.target, install.scope));
703
+ }
704
+ if (targetUsesHttpMcpDaemon(install.target)) {
705
+ lines.push('Requires MCP to be started.');
706
+ }
707
+ const hasAnalytics = hasEntryRole(targetEntries, 'analytics-skill');
708
+ const hasAdmin = hasAdminCliEntries(targetEntries);
709
+ const claudeDesktopSkillBundles = targetEntries.filter((entry) => entry.kind === 'file' && entry.role === 'claude-desktop-skill-bundle');
710
+ if (install.target === 'claude-code') {
711
+ if (hasAnalytics) {
712
+ lines.push('Analytics skill installed.');
713
+ }
714
+ if (hasAdmin) {
715
+ lines.push('Admin CLI skill installed.');
716
+ }
717
+ }
718
+ else if (install.target === 'claude-desktop') {
719
+ if (claudeDesktopSkillBundles.length > 0) {
720
+ lines.push('Skill bundles:');
721
+ for (const bundle of claudeDesktopSkillBundles) {
722
+ lines.push(` ${bundle.path}`);
611
723
  }
612
724
  }
613
725
  }
614
- for (const entry of mcpEntriesByTarget
615
- .get(install.target)
616
- ?.filter((entry) => entry.kind === 'json-key') ?? []) {
617
- const displayPath = install.scope === 'global' ? entry.path : relative(projectDir, entry.path);
618
- lines.push(` + ${mcpEntryLabel(entry)}`);
619
- lines.push(` ${displayPath}`);
726
+ else if (hasAnalytics || hasAdmin) {
727
+ lines.push(`${guidanceInstallLine(install.target)}.`);
728
+ }
729
+ if (hasEntryRole(targetEntries, 'launcher')) {
730
+ lines.push('Starts KTX over stdio from Claude Desktop.');
731
+ }
732
+ return {
733
+ title: `${targetDisplayName(install.target)} · ${scopeDisplayName(install.scope)}`,
734
+ lines,
735
+ };
736
+ });
737
+ }
738
+ function claudeDesktopSkillBundlePathsForInstalls(projectDir, installs) {
739
+ return installs
740
+ .filter((install) => install.target === 'claude-desktop')
741
+ .flatMap((install) => plannedKtxAgentFiles({ projectDir, ...install }))
742
+ .filter((entry) => entry.kind === 'file' && entry.role === 'claude-desktop-skill-bundle')
743
+ .map((entry) => entry.path);
744
+ }
745
+ function humanList(values) {
746
+ if (values.length <= 2) {
747
+ return values.join(' and ');
748
+ }
749
+ return `${values.slice(0, -1).join(', ')}, and ${values[values.length - 1]}`;
750
+ }
751
+ function pushBlankLine(lines) {
752
+ if (lines.length > 0 && lines[lines.length - 1] !== '') {
753
+ lines.push('');
754
+ }
755
+ }
756
+ function trimTrailingBlankLines(lines) {
757
+ while (lines[lines.length - 1] === '') {
758
+ lines.pop();
759
+ }
760
+ }
761
+ function manualActionFromSnippet(snippet) {
762
+ const [label = '', ...body] = snippet.split('\n');
763
+ const codexPrefix = 'Add this Codex MCP snippet to ~/.codex/config.toml:';
764
+ if (label === codexPrefix) {
765
+ return {
766
+ title: 'Configure Codex',
767
+ instruction: 'Open ~/.codex/config.toml, then paste this block:',
768
+ marker: 'PASTE',
769
+ body,
770
+ };
771
+ }
772
+ const opencodeMatch = label.match(/^Add this OpenCode MCP snippet to (.+):$/);
773
+ if (opencodeMatch) {
774
+ return {
775
+ title: 'Configure OpenCode',
776
+ instruction: `Open ${opencodeMatch[1]}, then paste this block:`,
777
+ marker: 'PASTE',
778
+ body,
779
+ };
780
+ }
781
+ if (label === 'Use this universal MCP endpoint with unsupported MCP clients:') {
782
+ return {
783
+ title: 'Configure unsupported MCP clients',
784
+ instruction: 'Use this endpoint when setting up unsupported MCP clients:',
785
+ marker: 'USE',
786
+ body,
787
+ };
788
+ }
789
+ return {
790
+ title: 'Configure MCP client',
791
+ instruction: label,
792
+ marker: 'PASTE',
793
+ body,
794
+ };
795
+ }
796
+ function formatAgentNextActions(input) {
797
+ const projectDir = resolve(input.projectDir);
798
+ const lines = [];
799
+ let step = 1;
800
+ for (const snippet of input.snippets) {
801
+ const action = manualActionFromSnippet(snippet);
802
+ lines.push(`${step}. ${action.title}`);
803
+ lines.push(` ${action.instruction}`);
804
+ if (action.body.length > 0) {
805
+ lines.push('', ` ${action.marker}:`);
806
+ }
807
+ for (const line of action.body) {
808
+ lines.push(` ${line}`);
620
809
  }
810
+ pushBlankLine(lines);
811
+ step += 1;
621
812
  }
813
+ const httpTargets = input.installs
814
+ .filter((install) => targetUsesHttpMcpDaemon(install.target))
815
+ .map((install) => targetDisplayName(install.target));
816
+ if (input.notices.length > 0 && httpTargets.length > 0) {
817
+ lines.push(`${step}. Start MCP`);
818
+ lines.push(` Run this command before using ${humanList(httpTargets)}:`);
819
+ lines.push('');
820
+ lines.push(' RUN:');
821
+ lines.push(` ktx mcp start --project-dir ${projectDir}`);
822
+ lines.push('');
823
+ lines.push(' If you need to stop MCP later:');
824
+ lines.push(` ktx mcp stop --project-dir ${projectDir}`);
825
+ pushBlankLine(lines);
826
+ step += 1;
827
+ }
828
+ const claudeCodeInstall = input.installs.find((install) => install.target === 'claude-code');
829
+ if (claudeCodeInstall) {
830
+ lines.push(`${step}. Open Claude Code`);
831
+ if (claudeCodeInstall.scope === 'project') {
832
+ lines.push(' Open Claude Code from the KTX project directory:');
833
+ lines.push('');
834
+ lines.push(' RUN:');
835
+ lines.push(` cd ${shellScriptQuote(projectDir)}`);
836
+ lines.push(' claude');
837
+ }
838
+ else {
839
+ lines.push(' RUN:');
840
+ lines.push(' claude');
841
+ }
842
+ pushBlankLine(lines);
843
+ step += 1;
844
+ }
845
+ const cursorInstall = input.installs.find((install) => install.target === 'cursor');
846
+ if (cursorInstall) {
847
+ lines.push(`${step}. Open Cursor`);
848
+ if (cursorInstall.scope === 'project') {
849
+ lines.push(' Open Cursor from the KTX project directory:');
850
+ lines.push('');
851
+ lines.push(' OPEN:');
852
+ lines.push(` ${projectDir}`);
853
+ }
854
+ else {
855
+ lines.push(' Open Cursor.');
856
+ }
857
+ pushBlankLine(lines);
858
+ step += 1;
859
+ }
860
+ if (input.installs.some((install) => install.target === 'claude-desktop')) {
861
+ lines.push(`${step}. Restart Claude Desktop`);
862
+ lines.push(' Claude Desktop loads KTX MCP after restart.');
863
+ pushBlankLine(lines);
864
+ step += 1;
865
+ const skillBundlePaths = claudeDesktopSkillBundlePathsForInstalls(projectDir, input.installs);
866
+ if (skillBundlePaths.length > 0) {
867
+ lines.push(`${step}. Upload Claude Desktop skills`);
868
+ lines.push(' Open Claude Desktop: Customize > Skills > + > Create skill > Upload a skill.');
869
+ lines.push(skillBundlePaths.length === 1 ? ' Upload this file:' : ' Upload each file separately:');
870
+ for (const path of skillBundlePaths) {
871
+ lines.push(` ${path}`);
872
+ }
873
+ lines.push(' Toggle the uploaded KTX skills on.');
874
+ pushBlankLine(lines);
875
+ step += 1;
876
+ }
877
+ }
878
+ if (lines.length === 0) {
879
+ lines.push('Open your configured agent and ask a data question.');
880
+ }
881
+ trimTrailingBlankLines(lines);
622
882
  return lines.join('\n');
623
883
  }
624
884
  async function installTarget(input) {
@@ -631,11 +891,11 @@ async function installTarget(input) {
631
891
  await writeClaudeDesktopLauncher({ path: entry.path, launcher });
632
892
  continue;
633
893
  }
634
- if (entry.role === 'claude-plugin') {
635
- await writeClaudeDesktopPlugin({
894
+ if (entry.role === 'claude-desktop-skill-bundle') {
895
+ await writeClaudeDesktopSkillBundle({
636
896
  projectDir: input.projectDir,
637
897
  path: entry.path,
638
- mode: input.mode,
898
+ skillName: claudeDesktopSkillNameForBundle(entry.path),
639
899
  launcher,
640
900
  });
641
901
  continue;
@@ -664,13 +924,24 @@ export async function runKtxSetupAgentsStep(args, io, deps = {}) {
664
924
  return { status: 'skipped', projectDir: args.projectDir };
665
925
  }
666
926
  const prompts = deps.prompts ?? createPromptAdapter();
927
+ if (args.inputMode === 'auto' && args.target === undefined) {
928
+ writeSetupInfo(io, 'Space to select, Enter to confirm, Esc to go back.');
929
+ }
667
930
  const mode = args.inputMode === 'disabled'
668
931
  ? args.mode
669
932
  : (await prompts.select({
670
- message: 'How should client agents connect to this KTX project?',
933
+ message: 'What should agents be allowed to do with this KTX project?',
671
934
  options: [
672
- { value: 'mcp', label: 'MCP tools + analytics skill' },
673
- { value: 'mcp-cli', label: 'MCP tools + analytics skill + admin CLI skill' },
935
+ {
936
+ value: 'mcp',
937
+ label: 'Ask data questions with KTX MCP',
938
+ hint: 'Installs the MCP connection and analytics workflow skill. Best for normal use.',
939
+ },
940
+ {
941
+ value: 'mcp-cli',
942
+ label: 'Ask data questions + manage KTX with CLI commands',
943
+ hint: 'Adds an admin CLI skill so agents can run ktx status, sl, wiki, and setup commands.',
944
+ },
674
945
  ],
675
946
  }));
676
947
  if (mode === 'back')
@@ -680,7 +951,7 @@ export async function runKtxSetupAgentsStep(args, io, deps = {}) {
680
951
  : args.inputMode === 'disabled'
681
952
  ? []
682
953
  : (await prompts.multiselect({
683
- message: withMultiselectNavigation('Which agent targets should KTX install?'),
954
+ message: 'Which agent targets should KTX install?',
684
955
  options: [
685
956
  { value: 'claude-code', label: 'Claude Code' },
686
957
  { value: 'claude-desktop', label: 'Claude Desktop' },
@@ -703,10 +974,18 @@ export async function runKtxSetupAgentsStep(args, io, deps = {}) {
703
974
  scopeTargets.length > 0 &&
704
975
  scopeTargets.every(targetSupportsGlobalScope)
705
976
  ? (await prompts.select({
706
- message: 'Where should KTX install supported agent config?',
977
+ message: `Where should KTX install supported agent config?\n\nKTX project: ${resolve(args.projectDir)}`,
707
978
  options: [
708
- { value: 'project', label: 'Project' },
709
- { value: 'global', label: 'Global' },
979
+ {
980
+ value: 'project',
981
+ label: 'Project scope (KTX project directory)',
982
+ hint: 'Only agents opened from this KTX project path load the project-scoped config.',
983
+ },
984
+ {
985
+ value: 'global',
986
+ label: 'Global scope (user config)',
987
+ hint: 'Agents can load this KTX project from any working directory.',
988
+ },
710
989
  ],
711
990
  }))
712
991
  : args.scope;
@@ -716,7 +995,6 @@ export async function runKtxSetupAgentsStep(args, io, deps = {}) {
716
995
  const entries = [];
717
996
  const snippets = [];
718
997
  const notices = new Set();
719
- let claudeDesktopTutorial;
720
998
  try {
721
999
  for (const install of installs) {
722
1000
  const targetEntries = await installTarget({ projectDir: args.projectDir, ...install });
@@ -731,41 +1009,26 @@ export async function runKtxSetupAgentsStep(args, io, deps = {}) {
731
1009
  snippets.push(snippet);
732
1010
  for (const notice of mcpResult.notices)
733
1011
  notices.add(notice);
734
- if (install.target === 'claude-desktop') {
735
- const pluginEntry = targetEntries.find((entry) => entry.kind === 'file' && entry.role === 'claude-plugin');
736
- const pluginPath = pluginEntry?.path ?? '';
737
- const configPath = claudeDesktopConfigPath().path;
738
- claudeDesktopTutorial = [
739
- `${green('✓')} ${bold('KTX MCP server registered')}`,
740
- ` ${dim(configPath)}`,
741
- '',
742
- bold('1. Restart Claude Desktop'),
743
- ' Quit and reopen so it picks up the new MCP server.',
744
- '',
745
- bold('2. Install the KTX plugin'),
746
- ' Open Claude Desktop → Settings → Plugins and install from file:',
747
- ` 📦 ${dim(pluginPath)}`,
748
- ].join('\n');
749
- }
750
1012
  }
751
1013
  await writeManifest(args.projectDir, mergeManifest(args.projectDir, await readKtxAgentInstallManifest(args.projectDir), installs, entries));
752
1014
  await markAgentsComplete(args.projectDir);
753
1015
  const setupUi = createKtxSetupUiAdapter();
754
- setupUi.note(formatInstallSummary(installs, entries, args.projectDir), 'Agent integration complete', io);
755
- if (claudeDesktopTutorial) {
756
- setupUi.note(claudeDesktopTutorial, 'Finish Claude Desktop setup', io, {
757
- format: (line) => line,
758
- });
1016
+ for (const summary of formatInstallSummaryLines(installs, entries, args.projectDir)) {
1017
+ writeSetupStep(io, summary.lines.length > 0 ? `${summary.title}\n${summary.lines.join('\n')}` : summary.title);
759
1018
  }
760
- const nextStepBlocks = [];
761
- for (const notice of notices)
762
- nextStepBlocks.push(notice);
763
- for (const snippet of snippets)
764
- nextStepBlocks.push(snippet);
765
- if (nextStepBlocks.length > 0) {
766
- setupUi.note(nextStepBlocks.join('\n\n'), 'Next steps', io, { format: bold });
1019
+ const nextActions = formatAgentNextActions({
1020
+ projectDir: args.projectDir,
1021
+ installs,
1022
+ notices: [...notices],
1023
+ snippets,
1024
+ });
1025
+ if (args.showNextActions !== false) {
1026
+ setupUi.note(nextActions, 'Required before using agents', io, {
1027
+ format: createAgentNextActionsLineFormatter(io.stdout),
1028
+ });
1029
+ writeSetupOutro(io, 'All set.');
767
1030
  }
768
- return { status: 'ready', projectDir: args.projectDir, installs };
1031
+ return { status: 'ready', projectDir: args.projectDir, installs, nextActions };
769
1032
  }
770
1033
  catch (error) {
771
1034
  io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);