Polish ktx setup-agents output: hint, summaries, outro

This commit is contained in:
Andrey Avtomonov 2026-05-19 14:12:18 +02:00
parent ddabe517e3
commit 0cbf8121d9
4 changed files with 1151 additions and 87 deletions

View file

@ -0,0 +1,728 @@
# KTX Setup Agents Output Polish Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Make `ktx setup --agents` print the polished interactive hint,
per-install inline summaries, the required next-actions note, and the final
`All set.` outro described by the May 19 output-polish spec.
**Architecture:** Keep the change centered in
`packages/cli/src/setup-agents.ts` and preserve the existing
`withMultiselectNavigation` helper for other setup flows. Replace the old
boxed install-summary string with structured summary entries, then render those
entries with Clack `log.step` for real TTY output and plain `io.stdout` text in
tests or scripted output. The current uncommitted Claude Desktop split-ZIP
changes are treated as the baseline and must not be reverted.
**Tech Stack:** TypeScript, Vitest, Node stream types, `@clack/prompts`,
pnpm workspace commands.
---
## Current audit
Original spec:
`docs/superpowers/specs/2026-05-19-ktx-setup-agents-output-polish-design.md`
Current uncommitted files:
- `README.md`
- `docs-site/content/docs/integrations/agent-clients.mdx`
- `packages/cli/src/setup-agents.test.ts`
- `packages/cli/src/setup-agents.ts`
Observed implementation status:
- The branch currently contains unrelated Claude Desktop split-ZIP work:
`ktx-analytics.zip` and optional `ktx.zip` replace the old combined
`ktx-skills.zip`. Keep that work intact.
- `packages/cli/src/setup-agents.ts` still imports and uses
`withMultiselectNavigation` for the agents multiselect prompt.
- `runKtxSetupAgentsStep` does not emit
`Space to select, Enter to confirm, Esc to go back.` before interactive
prompts.
- `formatInstallSummary` still returns one boxed string containing
`KTX project`, `Installed agents`, and nested indentation.
- `runKtxSetupAgentsStep` still prints the install summary with
`setupUi.note(..., 'Agent integration complete', io)`.
- `runKtxSetupAgentsStep` does not emit `outro('All set.')` after the
"Required before using agents" note.
- Tests still assert the old long multiselect message and old boxed install
summary.
V1-blocking gaps:
- Move the multiselect navigation hint out of the question text and print it
once before interactive prompts.
- Render install summaries as inline `log.step` blocks, one block per install.
- Keep the next-actions note boxed and emit `All set.` only when next-actions
are shown.
- Update setup-agents tests to cover the new output contract.
Non-blocking gaps:
- The docs updates for Claude Desktop split ZIPs are outside this output-polish
spec and are already present in the current working tree.
- `setup --sources` and `setup --databases` still use
`withMultiselectNavigation`; the spec explicitly defers those flows.
- There is no docs-site update for the output shape because no public docs page
currently promises the old boxed install-summary layout.
## File structure
Modify:
- `packages/cli/src/setup-agents.ts`
- Remove the agents-flow dependency on `withMultiselectNavigation`.
- Add tiny output helpers that use Clack `log.info`, `log.step`, and `outro`
when `io.stdout` is a writable TTY and fall back to plain writes otherwise.
- Rename `formatInstallSummary` to `formatInstallSummaryLines` and return
structured entries.
- Render each summary entry with `log.step`.
- Emit the final outro only when next-actions are printed.
- `packages/cli/src/setup-agents.test.ts`
- Import `formatInstallSummaryLines`.
- Update the four output-polish tests named in the spec.
- Add or update assertions for the single hint line and final outro.
Do not modify:
- `packages/cli/src/prompt-navigation.ts`
- `packages/cli/src/prompt-navigation.test.ts`
- `README.md`
- `docs-site/content/docs/integrations/agent-clients.mdx`
## Task 1: Move the interactive multiselect hint out of the prompt
**Files:**
- Modify: `packages/cli/src/setup-agents.test.ts`
- Modify: `packages/cli/src/setup-agents.ts`
- [ ] **Step 1: Write the failing prompt-output test**
In `packages/cli/src/setup-agents.test.ts`, replace the existing test named
`explains how to select multiple agent targets in interactive mode` with:
```typescript
it('prints one navigation hint before interactive agent target prompts', async () => {
const io = makeIo();
const prompts = {
select: vi.fn(async () => 'mcp-cli'),
multiselect: vi.fn(async () => ['back']),
cancel: vi.fn(),
};
await expect(
runKtxSetupAgentsStep(
{
projectDir: tempDir,
inputMode: 'auto',
yes: false,
agents: true,
scope: 'project',
mode: 'mcp-cli',
skipAgents: false,
},
io.io,
{ prompts },
),
).resolves.toEqual({ status: 'back', projectDir: tempDir });
expect(io.stdout()).toContain('Space to select, Enter to confirm, Esc to go back.');
expect(io.stdout().match(/Space to select/g)).toHaveLength(1);
expect(prompts.multiselect).toHaveBeenCalledWith(
expect.objectContaining({
message: 'Which agent targets should KTX install?',
}),
);
});
```
- [ ] **Step 2: Run the focused test and verify it fails**
Run:
```bash
pnpm --filter @ktx/cli exec vitest run src/setup-agents.test.ts -t "prints one navigation hint before interactive agent target prompts"
```
Expected: FAIL because the current implementation embeds the long navigation
hint in the multiselect message and does not write the short hint to
`io.stdout`.
- [ ] **Step 3: Add Clack output helpers for setup-agents**
In `packages/cli/src/setup-agents.ts`, replace the current top imports:
```typescript
import { existsSync } from 'node:fs';
import { chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import { dirname, join, relative, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
```
with:
```typescript
import { existsSync } from 'node:fs';
import { chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import { dirname, join, relative, resolve } from 'node:path';
import type { Writable } from 'node:stream';
import { fileURLToPath } from 'node:url';
import { log, outro } from '@clack/prompts';
```
Remove this import:
```typescript
import { withMultiselectNavigation } from './prompt-navigation.js';
```
Add these helpers after the `KtxCliLauncher` interface:
```typescript
function isWritableTtyOutput(output: KtxCliIo['stdout']): output is KtxCliIo['stdout'] & Writable {
return (
output.isTTY === true &&
typeof (output as { on?: unknown }).on === 'function' &&
typeof (output as { columns?: unknown }).columns !== 'undefined'
);
}
function writeSetupInfo(io: KtxCliIo, message: string): void {
if (isWritableTtyOutput(io.stdout)) {
log.info(message, { output: io.stdout });
return;
}
io.stdout.write(`${message}\n`);
}
function writeSetupStep(io: KtxCliIo, message: string): void {
if (isWritableTtyOutput(io.stdout)) {
log.step(message, { output: io.stdout });
return;
}
io.stdout.write(`\n${message}\n`);
}
function writeSetupOutro(io: KtxCliIo, message: string): void {
if (isWritableTtyOutput(io.stdout)) {
outro(message, { output: io.stdout });
return;
}
io.stdout.write(`\n${message}\n`);
}
```
- [ ] **Step 4: Print the short hint only before interactive prompts**
In `runKtxSetupAgentsStep`, immediately after:
```typescript
const prompts = deps.prompts ?? createPromptAdapter();
```
add:
```typescript
if (args.inputMode === 'auto' && args.target === undefined) {
writeSetupInfo(io, 'Space to select, Enter to confirm, Esc to go back.');
}
```
In the multiselect call, replace:
```typescript
message: withMultiselectNavigation('Which agent targets should KTX install?'),
```
with:
```typescript
message: 'Which agent targets should KTX install?',
```
- [ ] **Step 5: Run the focused test and verify it passes**
Run:
```bash
pnpm --filter @ktx/cli exec vitest run src/setup-agents.test.ts -t "prints one navigation hint before interactive agent target prompts"
```
Expected: PASS.
## Task 2: Return structured install-summary entries
**Files:**
- Modify: `packages/cli/src/setup-agents.test.ts`
- Modify: `packages/cli/src/setup-agents.ts`
- [ ] **Step 1: Update the summary formatter import in tests**
In `packages/cli/src/setup-agents.test.ts`, replace:
```typescript
formatInstallSummary,
```
with:
```typescript
formatInstallSummaryLines,
```
- [ ] **Step 2: Write failing structured formatter tests**
In `packages/cli/src/setup-agents.test.ts`, replace the test named
`formats summary with explicit project-scoped config paths` with:
```typescript
it('formats summary with explicit project-scoped config paths', () => {
const summary = formatInstallSummaryLines(
[{ target: 'cursor', scope: 'project', mode: 'mcp-cli' }],
[
{ kind: 'file', path: join(tempDir, '.cursor/rules/ktx-analytics.mdc'), role: 'analytics-skill' },
{ kind: 'file', path: join(tempDir, '.cursor/rules/ktx.mdc') },
{ kind: 'json-key', path: join(tempDir, '.cursor/mcp.json'), jsonPath: ['mcpServers', 'ktx'] },
],
tempDir,
);
expect(summary).toEqual([
{
title: 'Cursor · Project scope',
lines: [
join(tempDir, '.cursor/mcp.json'),
'Requires MCP to be started.',
'Cursor rules installed.',
],
},
]);
});
```
Replace the test named `formats summary with multiple agent targets` with:
```typescript
it('formats summary with multiple agent targets', () => {
const summary = formatInstallSummaryLines(
[
{ target: 'claude-code', scope: 'project', mode: 'mcp-cli' },
{ target: 'codex', scope: 'project', mode: 'mcp-cli' },
],
[
{ kind: 'file', path: join(tempDir, '.claude/skills/ktx-analytics/SKILL.md'), role: 'analytics-skill' },
{ kind: 'file', path: join(tempDir, '.claude/skills/ktx/SKILL.md'), role: 'skill' },
{ kind: 'file', path: join(tempDir, '.claude/rules/ktx.md'), role: 'rule' },
{ kind: 'json-key', path: join(tempDir, '.mcp.json'), jsonPath: ['mcpServers', 'ktx'] },
{ kind: 'file', path: join(tempDir, '.agents/skills/ktx-analytics/SKILL.md'), role: 'analytics-skill' },
{ kind: 'file', path: join(tempDir, '.agents/skills/ktx/SKILL.md'), role: 'skill' },
{ kind: 'file', path: join(tempDir, '.codex/instructions/ktx.md'), role: 'rule' },
],
tempDir,
);
expect(summary).toEqual([
{
title: 'Claude Code · Project scope',
lines: [
join(tempDir, '.mcp.json'),
'Requires MCP to be started.',
'Analytics skill installed.',
'Admin CLI skill installed.',
],
},
{
title: 'Codex · Project scope',
lines: [
'Add the snippet shown below to ~/.codex/config.toml.',
'Requires MCP to be started.',
'Codex guidance installed.',
],
},
]);
});
```
- [ ] **Step 3: Run the focused formatter tests and verify they fail**
Run:
```bash
pnpm --filter @ktx/cli exec vitest run src/setup-agents.test.ts -t "formats summary"
```
Expected: FAIL because `formatInstallSummaryLines` is not exported and the
current formatter returns one string.
- [ ] **Step 4: Replace the string summary formatter with structured entries**
In `packages/cli/src/setup-agents.ts`, add this exported interface immediately
before the current `formatInstallSummary` function:
```typescript
export interface InstallSummaryEntry {
title: string;
lines: string[];
}
```
Replace the full `export function formatInstallSummary(...)` implementation
with:
```typescript
function formatInlinePath(path: string): string {
const home = process.env.HOME;
if (!home) return path;
const resolvedHome = resolve(home);
if (path === resolvedHome) return '~';
if (path.startsWith(`${resolvedHome}/`)) {
return `~/${relative(resolvedHome, path)}`;
}
return path;
}
export function formatInstallSummaryLines(
installs: Array<{ target: KtxAgentTarget; scope: KtxAgentScope; mode: KtxAgentInstallMode }>,
entries: InstallEntry[],
projectDir: string,
): InstallSummaryEntry[] {
const entriesByTarget = new Map<KtxAgentTarget, InstallEntry[]>();
for (const install of installs) {
const plannedFilePaths = new Set(
plannedKtxAgentFiles({ projectDir, ...install })
.filter((entry) => entry.kind === 'file')
.map((entry) => entry.path),
);
entriesByTarget.set(
install.target,
entries.filter((entry) => entry.kind === 'file' && plannedFilePaths.has(entry.path)),
);
}
const mcpEntriesByTarget = new Map<KtxAgentTarget, InstallEntry[]>();
for (const install of installs) {
const plannedMcpKeys = new Set(plannedMcpJsonEntries({ projectDir, ...install }).map(entryKey));
mcpEntriesByTarget.set(
install.target,
entries.filter((entry) => entry.kind === 'json-key' && plannedMcpKeys.has(entryKey(entry))),
);
}
return installs.map((install) => {
const targetEntries = entriesByTarget.get(install.target) ?? [];
const mcpEntry = mcpEntriesByTarget
.get(install.target)
?.find((entry): entry is Extract<InstallEntry, { kind: 'json-key' }> => entry.kind === 'json-key');
const lines: string[] = [];
if (mcpEntry) {
lines.push(formatInlinePath(mcpEntry.path));
} else if (install.target !== 'claude-desktop') {
lines.push(manualMcpConfigInstruction(install.target, install.scope));
}
if (targetUsesHttpMcpDaemon(install.target)) {
lines.push('Requires MCP to be started.');
}
const hasAnalytics = hasEntryRole(targetEntries, 'analytics-skill');
const hasAdmin = hasAdminCliEntries(targetEntries);
const claudeDesktopSkillBundles = targetEntries.filter(
(entry): entry is Extract<InstallEntry, { kind: 'file' }> =>
entry.kind === 'file' && entry.role === 'claude-desktop-skill-bundle',
);
if (install.target === 'claude-code') {
if (hasAnalytics) {
lines.push('Analytics skill installed.');
}
if (hasAdmin) {
lines.push('Admin CLI skill installed.');
}
} else if (install.target === 'claude-desktop') {
if (claudeDesktopSkillBundles.length > 0) {
lines.push('Skill bundles:');
for (const bundle of claudeDesktopSkillBundles) {
lines.push(` ${bundle.path}`);
}
}
} else if (hasAnalytics || hasAdmin) {
lines.push(`${guidanceInstallLine(install.target)}.`);
}
if (hasEntryRole(targetEntries, 'launcher')) {
lines.push('Starts KTX over stdio from Claude Desktop.');
}
return {
title: `${targetDisplayName(install.target)} · ${scopeDisplayName(install.scope)}`,
lines,
};
});
}
```
- [ ] **Step 5: Run the focused formatter tests and verify they pass**
Run:
```bash
pnpm --filter @ktx/cli exec vitest run src/setup-agents.test.ts -t "formats summary"
```
Expected: PASS.
## Task 3: Render inline install summaries with `log.step`
**Files:**
- Modify: `packages/cli/src/setup-agents.test.ts`
- Modify: `packages/cli/src/setup-agents.ts`
- [ ] **Step 1: Write failing install-summary output assertions**
In `packages/cli/src/setup-agents.test.ts`, replace the body of the test named
`prints per-agent install summary after successful installation` with:
```typescript
const io = makeIo();
await runKtxSetupAgentsStep(
{
projectDir: tempDir,
inputMode: 'disabled',
yes: true,
agents: true,
target: 'claude-code',
scope: 'project',
mode: 'mcp-cli',
skipAgents: false,
},
io.io,
);
const output = io.stdout();
expect(output).toContain('Claude Code · Project scope');
expect(output).toContain(join(tempDir, '.mcp.json'));
expect(output).toContain('Requires MCP to be started.');
expect(output).toContain('Analytics skill installed.');
expect(output).toContain('Admin CLI skill installed.');
expect(output).not.toContain('Agent integration complete');
expect(output).not.toContain(`KTX project\n ${tempDir}`);
expect(output).not.toContain('Installed agents');
expect(output).not.toContain('.claude/skills/ktx-analytics/SKILL.md');
expect(output).not.toContain('.claude/skills/ktx/SKILL.md');
expect(output).not.toContain('.claude/rules/ktx.md');
```
In the test named `can return agent next actions without printing them`, replace:
```typescript
expect(io.stdout()).toContain('Agent integration complete');
expect(io.stdout()).not.toContain('Required before using agents');
```
with:
```typescript
expect(io.stdout()).toContain('Claude Code · Project scope');
expect(io.stdout()).not.toContain('Agent integration complete');
expect(io.stdout()).not.toContain('Required before using agents');
expect(io.stdout()).not.toContain('All set.');
```
- [ ] **Step 2: Run the focused output tests and verify they fail**
Run:
```bash
pnpm --filter @ktx/cli exec vitest run src/setup-agents.test.ts -t "install summary|without printing"
```
Expected: FAIL because `Agent integration complete` is still printed and the
new inline summary heading is absent.
- [ ] **Step 3: Render each structured summary as an inline step**
In `runKtxSetupAgentsStep`, replace:
```typescript
setupUi.note(
formatInstallSummary(installs, entries, args.projectDir),
'Agent integration complete',
io,
);
```
with:
```typescript
for (const summary of formatInstallSummaryLines(installs, entries, args.projectDir)) {
writeSetupStep(
io,
summary.lines.length > 0 ? `${summary.title}\n${summary.lines.join('\n')}` : summary.title,
);
}
```
- [ ] **Step 4: Run the focused output tests and verify they pass**
Run:
```bash
pnpm --filter @ktx/cli exec vitest run src/setup-agents.test.ts -t "install summary|without printing"
```
Expected: PASS.
## Task 4: Emit the final outro after next actions
**Files:**
- Modify: `packages/cli/src/setup-agents.test.ts`
- Modify: `packages/cli/src/setup-agents.ts`
- [ ] **Step 1: Add failing outro assertions**
In `packages/cli/src/setup-agents.test.ts`, in the test named
`prints standalone agent next actions after successful installation`, add this
assertion after the existing next-actions stdout assertions:
```typescript
expect(io.stdout()).toContain('All set.');
```
In the test named `prints one target-aware next actions block for mixed agent
targets`, add this assertion near the other stdout assertions:
```typescript
expect(output).toContain('All set.');
```
- [ ] **Step 2: Run the focused next-actions tests and verify they fail**
Run:
```bash
pnpm --filter @ktx/cli exec vitest run src/setup-agents.test.ts -t "next actions|All set|without printing"
```
Expected: FAIL because no outro is emitted.
- [ ] **Step 3: Emit `All set.` only when next-actions are shown**
In `runKtxSetupAgentsStep`, replace:
```typescript
if (args.showNextActions !== false) {
setupUi.note(nextActions, 'Required before using agents', io, { format: (line) => line });
}
```
with:
```typescript
if (args.showNextActions !== false) {
setupUi.note(nextActions, 'Required before using agents', io, { format: (line) => line });
writeSetupOutro(io, 'All set.');
}
```
- [ ] **Step 4: Run the focused next-actions tests and verify they pass**
Run:
```bash
pnpm --filter @ktx/cli exec vitest run src/setup-agents.test.ts -t "next actions|All set|without printing"
```
Expected: PASS.
## Task 5: Verify the full CLI package surface
**Files:**
- Verify: `packages/cli/src/setup-agents.ts`
- Verify: `packages/cli/src/setup-agents.test.ts`
- [ ] **Step 1: Run the setup-agents test file**
Run:
```bash
pnpm --filter @ktx/cli exec vitest run src/setup-agents.test.ts
```
Expected: PASS.
- [ ] **Step 2: Run the CLI type-check**
Run:
```bash
pnpm --filter @ktx/cli run type-check
```
Expected: PASS.
- [ ] **Step 3: Run the CLI test script**
Run:
```bash
pnpm --filter @ktx/cli run test
```
Expected: PASS.
- [ ] **Step 4: Run dead-code checks for TypeScript changes**
Run:
```bash
pnpm run dead-code
```
Expected: PASS. If Knip reports unrelated existing findings, inspect them and
record the exact unrelated findings before finishing.
- [ ] **Step 5: Run pre-commit for modified TypeScript files**
Run:
```bash
uv run pre-commit run --files packages/cli/src/setup-agents.ts packages/cli/src/setup-agents.test.ts
```
Expected: PASS. If the repository has no usable pre-commit environment or the
configured tool versions are unavailable, state the exact failure and keep the
passing pnpm checks as the closest available verification.
- [ ] **Step 6: Inspect the final diff**
Run:
```bash
git diff -- packages/cli/src/setup-agents.ts packages/cli/src/setup-agents.test.ts
```
Expected: The diff only contains output-polish changes on top of the existing
Claude Desktop split-ZIP baseline. It must not revert the current
`ktx-analytics.zip` and `ktx.zip` behavior.
## Self-review checklist
- The agents multiselect message is exactly
`Which agent targets should KTX install?`.
- The short navigation hint appears once for interactive no-target runs and not
for scripted target runs.
- The shared `withMultiselectNavigation` helper and its tests are unchanged.
- `Agent integration complete`, `KTX project`, and `Installed agents` no longer
appear in setup-agents install-summary output.
- `Required before using agents` remains a `note()` box.
- `All set.` appears only when the next-actions note is printed.
- Current uncommitted README and docs-site split-ZIP edits are untouched.

View file

@ -0,0 +1,220 @@
# `ktx setup --agents` output polish
## Problem
The current `ktx setup --agents` flow renders four visual issues:
1. The multiselect prompt embeds a long navigation hint
(`Use Up/Down to move, Space to select or unselect, Enter to confirm,
Escape to go back, or Ctrl+C to exit.`) directly into the question text.
Clack uses the same message verbatim for the confirmed-state echo, so the
hint stays on screen after the user has already answered.
2. After install, two heavy `note()` boxes appear back-to-back ("Agent
integration complete" and "Required before using agents") with the same
visual weight, making the post-install output feel boxed in.
3. The install-complete box has 34 levels of nested indentation, which is
hard to scan.
4. The flow ends after the second box; there is no `outro` marker.
## Scope
Narrow: changes apply only to `runKtxSetupAgentsStep` in
`packages/cli/src/setup-agents.ts`. The shared `withMultiselectNavigation`
helper is left as-is so `setup --sources` and `setup --databases` keep their
current prompt rendering until a follow-up.
## Goals
- Replace the in-question navigation hint for the agents multiselect with a
one-time `log.info(...)` line emitted before the first interactive prompt.
- Render the per-install summary inline using `log.step` (one block per
install) instead of a `note()` box.
- Keep the "Required before using agents" `note()` box (the file paths users
copy/paste benefit from visual framing).
- End the flow with `outro('All set.')` when next-actions are shown.
Non-goals:
- No change to the actual install logic, manifest, or generated config files.
- No change to `setup --sources` or `setup --databases` (deferred).
- No change to `formatAgentNextActions` content (only the surrounding `note`
call site is touched; body stays).
- No path shortening to project-relative in next-actions output — users may
copy/paste from terminals outside the project, so absolute paths stay.
## Design
### Prompt polish
In `runKtxSetupAgentsStep`, emit the navigation hint **once, only when
interactive prompts will actually run** (`args.inputMode === 'auto'` and
`args.target === undefined`), immediately before the first prompt:
```ts
log.info('Space to select, Enter to confirm, Esc to go back.');
```
In scripted mode (`inputMode === 'disabled'` or `--target` supplied), the
hint is skipped — no prompts run, so the help would be noise.
Replace `withMultiselectNavigation('Which agent targets should KTX install?')`
with the plain string `'Which agent targets should KTX install?'`.
The shared `withMultiselectNavigation` helper, its export, and its
`prompt-navigation.test.ts` test remain untouched — they still apply to the
other two setup flows.
### Install summary
Today: `formatInstallSummary(...)` returns a multi-line string that
`runKtxSetupAgentsStep` passes to `setupUi.note(..., 'Agent integration
complete', io)`.
New: rename `formatInstallSummary` to `formatInstallSummaryLines` and have it
return a structured array — one entry per install — with:
```ts
type InstallSummaryEntry = {
title: string; // e.g. "Claude Desktop · Global scope"
lines: string[]; // body lines, no indentation prefix
};
```
`runKtxSetupAgentsStep` then iterates and emits one `log.step(title +
'\n' + body)` per install. The "KTX project / <path>" header line is dropped
(the user already sees the project path in the existing `intro` block, e.g.
`Project: /tmp/ktx7`).
Body lines for a Claude Desktop install:
```
~/Library/Application Support/Claude/claude_desktop_config.json
Skill bundles:
/tmp/ktx7/.ktx/agents/claude/ktx-analytics.zip
/tmp/ktx7/.ktx/agents/claude/ktx.zip
Starts KTX over stdio from Claude Desktop.
```
Body lines for a Claude Code project install (HTTP MCP target):
```
Project scope: /tmp/ktx7/.mcp.json
Requires MCP to be started.
Analytics skill installed.
Admin CLI skill installed.
```
Path rendering: replace a leading `$HOME` with `~` for absolute paths shown
inline in the body, leaving non-home paths untouched. Skill-bundle paths
inside `projectDir` stay absolute (users copy these into the Claude Desktop
file picker).
### Next actions
`formatAgentNextActions(...)` is unchanged. The `setupUi.note(...,
'Required before using agents', io, { format: (line) => line })` call site
stays. This is the one remaining box and acts as the visual payoff with
copy-pasteable instructions.
### Outro
After the next-actions note (when `args.showNextActions !== false`), emit:
```ts
outro('All set.');
```
When next actions are suppressed (`showNextActions === false`), no `outro` is
emitted — the caller is composing a larger flow.
## Final layout (Claude Desktop, global)
```
┌ KTX setup
│ Project: /tmp/ktx7
● Space to select, Enter to confirm, Esc to go back.
◇ What should agents be allowed to do with this KTX project?
│ Ask data questions + manage KTX with CLI commands
◇ Which agent targets should KTX install?
│ Claude Desktop
◆ Claude Desktop · Global scope
│ ~/Library/Application Support/Claude/claude_desktop_config.json
│ Skill bundles:
│ /tmp/ktx7/.ktx/agents/claude/ktx-analytics.zip
│ /tmp/ktx7/.ktx/agents/claude/ktx.zip
│ Starts KTX over stdio from Claude Desktop.
◇ Required before using agents ───────────────────────────────────╮
│ │
│ 1. Restart Claude Desktop │
│ Claude Desktop loads KTX MCP after restart. │
│ │
│ 2. Upload Claude Desktop skills │
│ Open Claude Desktop: Customize > Skills > + > Create skill. │
│ Upload these files: │
│ /tmp/ktx7/.ktx/agents/claude/ktx-analytics.zip │
│ /tmp/ktx7/.ktx/agents/claude/ktx.zip │
│ Toggle the uploaded KTX skills on. │
│ │
├──────────────────────────────────────────────────────────────────╯
└ All set.
```
## Test impact
`packages/cli/src/setup-agents.test.ts` — four tests update:
- **`explains how to select multiple agent targets in interactive mode`**
(L879) — currently asserts the multiselect `message` ends with the long
navigation hint string. Update to assert the plain message string. The test
name and intent shift: it now verifies the multiselect message no longer
embeds the hint and that the navigation hint was emitted once via stdout
before the prompts (`Space to select, Enter to confirm, Esc to go back.`).
- **`prints per-agent install summary after successful installation`**
(L911) — currently asserts `Agent integration complete` appears in stdout
plus the multi-level indented summary structure (`KTX project\n
${tempDir}`, `Installed agents`, `Project scope\n ${path}`).
Update to assert the new inline `log.step` lines:
- `Claude Code · Project scope` heading
- path on its own line (e.g., `${join(tempDir, '.mcp.json')}`)
- `Requires MCP to be started.`
- `Analytics skill installed.`
- `Admin CLI skill installed.`
- Stops asserting `Agent integration complete` and `KTX project\n …`.
- **`formats summary with explicit project-scoped config paths`** (L942) and
**`formats summary with multiple agent targets`** (L961) — call
`formatInstallSummary` directly. Update to call
`formatInstallSummaryLines` and assert on the returned structured array
shape and content per install.
- **`can return agent next actions without printing them`** (L212) — asserts
`'Agent integration complete'` appears in stdout when `showNextActions:
false`. Update to assert the new heading `Claude Code · Project scope`
appears instead.
The next-actions assertions (L181, L1028, L1089) keep passing — that code
path is unchanged.
## Verification
- `pnpm --filter @ktx/cli run type-check`
- `pnpm --filter @ktx/cli run test`
- Manual run: `ktx setup --agents` against `/tmp/ktx7` (or any fresh project)
with `--target claude-desktop --scope global --mode mcp-cli` to compare
against the layout above.
## Risks
- The `formatInstallSummary` rename is a breaking change for any external
importer. Grep confirms it is only imported from
`packages/cli/src/setup-agents.test.ts`; no production consumer outside
`runKtxSetupAgentsStep`. KTX has no public users (`feedback_ktx_no_backward_compat`),
so a clean rename is acceptable.
- `log.info` for the navigation hint runs in TTY mode only via Clack. In
non-TTY mode (`setupUi.note` falls back to plain stdout) the hint is not
emitted. Acceptable because non-TTY runs are scripted (`--target`/`--yes`)
and don't need keyboard help.

View file

@ -5,7 +5,7 @@ import { readKtxSetupState } from '@ktx/context/project';
import { strFromU8, unzipSync } from 'fflate';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
formatInstallSummary,
formatInstallSummaryLines,
plannedKtxAgentFiles,
readKtxAgentInstallManifest,
removeKtxAgentInstall,
@ -84,7 +84,7 @@ describe('setup agents', () => {
{ kind: 'file', path: join(tempDir, '.ktx/agents/claude/ktx-plugin-runner.sh'), role: 'launcher' },
{
kind: 'file',
path: join(tempDir, '.ktx/agents/claude/ktx-skills.zip'),
path: join(tempDir, '.ktx/agents/claude/ktx-analytics.zip'),
role: 'claude-desktop-skill-bundle',
},
]);
@ -129,7 +129,12 @@ describe('setup agents', () => {
{ kind: 'file', path: join(tempDir, '.ktx/agents/claude/ktx-plugin-runner.sh'), role: 'launcher' },
{
kind: 'file',
path: join(tempDir, '.ktx/agents/claude/ktx-skills.zip'),
path: join(tempDir, '.ktx/agents/claude/ktx-analytics.zip'),
role: 'claude-desktop-skill-bundle',
},
{
kind: 'file',
path: join(tempDir, '.ktx/agents/claude/ktx.zip'),
role: 'claude-desktop-skill-bundle',
},
]);
@ -205,6 +210,7 @@ describe('setup agents', () => {
expect(io.stdout()).toContain(`ktx mcp start --project-dir ${tempDir}`);
expect(io.stdout()).toContain('If you need to stop MCP later:');
expect(io.stdout()).toContain(`ktx mcp stop --project-dir ${tempDir}`);
expect(io.stdout()).toContain('All set.');
expect(io.stdout()).not.toContain('Finish agent setup');
expect(io.stdout()).not.toContain('Next actions');
});
@ -231,8 +237,10 @@ describe('setup agents', () => {
status: 'ready',
nextActions: expect.stringContaining(`ktx mcp start --project-dir ${tempDir}`),
});
expect(io.stdout()).toContain('Agent integration complete');
expect(io.stdout()).toContain('Claude Code · Project scope');
expect(io.stdout()).not.toContain('Agent integration complete');
expect(io.stdout()).not.toContain('Required before using agents');
expect(io.stdout()).not.toContain('All set.');
});
it('installs the analytics skill from the runtime asset', async () => {
@ -452,9 +460,11 @@ describe('setup agents', () => {
installs: [{ target: 'claude-desktop', scope: 'global', mode: 'mcp' }],
});
const skillBundlePath = join(tempDir, '.ktx/agents/claude/ktx-skills.zip');
const analyticsSkillPath = join(tempDir, '.ktx/agents/claude/ktx-analytics.zip');
const adminSkillPath = join(tempDir, '.ktx/agents/claude/ktx.zip');
const launcherPath = join(tempDir, '.ktx/agents/claude/ktx-plugin-runner.sh');
await expect(stat(skillBundlePath)).resolves.toBeDefined();
await expect(stat(analyticsSkillPath)).resolves.toBeDefined();
await expect(stat(adminSkillPath)).rejects.toThrow();
const launcherStat = await stat(launcherPath);
expect(launcherStat.mode & 0o111).not.toBe(0);
const launcher = await readFile(launcherPath, 'utf-8');
@ -470,15 +480,16 @@ describe('setup agents', () => {
args: ['--project-dir', tempDir, 'mcp', 'stdio'],
});
expect(await readZipText(skillBundlePath, 'ktx-analytics/SKILL.md')).toContain('KTX Analytics Workflow');
await expect(readZipText(skillBundlePath, 'ktx/SKILL.md')).rejects.toThrow('Missing zip entry');
await expect(readZipText(skillBundlePath, '.claude-plugin/plugin.json')).rejects.toThrow('Missing zip entry');
await expect(readZipText(skillBundlePath, 'skills/ktx-analytics/SKILL.md')).rejects.toThrow(
expect(await readZipText(analyticsSkillPath, 'ktx-analytics/SKILL.md')).toContain('KTX Analytics Workflow');
await expect(readZipText(analyticsSkillPath, 'ktx/SKILL.md')).rejects.toThrow('Missing zip entry');
await expect(readZipText(analyticsSkillPath, '.claude-plugin/plugin.json')).rejects.toThrow('Missing zip entry');
await expect(readZipText(analyticsSkillPath, 'skills/ktx-analytics/SKILL.md')).rejects.toThrow(
'Missing zip entry',
);
expect(io.stdout()).toContain('Claude Desktop');
expect(io.stdout()).toContain(skillBundlePath);
expect(io.stdout()).toContain(analyticsSkillPath);
expect(io.stdout()).not.toContain(adminSkillPath);
expect(io.stdout()).toContain('claude_desktop_config.json');
expect(io.stdout()).toContain('Required before using agents');
expect(io.stdout()).toContain('1. Restart Claude Desktop');
@ -569,13 +580,18 @@ describe('setup agents', () => {
installs: [{ target: 'claude-desktop', scope: 'global', mode: 'mcp-cli' }],
});
const skillBundlePath = join(tempDir, '.ktx/agents/claude/ktx-skills.zip');
expect(await readZipText(skillBundlePath, 'ktx-analytics/SKILL.md')).toContain('KTX Analytics Workflow');
const adminSkill = await readZipText(skillBundlePath, 'ktx/SKILL.md');
const analyticsSkillPath = join(tempDir, '.ktx/agents/claude/ktx-analytics.zip');
const adminSkillPath = join(tempDir, '.ktx/agents/claude/ktx.zip');
expect(await readZipText(analyticsSkillPath, 'ktx-analytics/SKILL.md')).toContain('KTX Analytics Workflow');
await expect(readZipText(analyticsSkillPath, 'ktx/SKILL.md')).rejects.toThrow('Missing zip entry');
const adminSkill = await readZipText(adminSkillPath, 'ktx/SKILL.md');
expect(adminSkill).toContain(`--project-dir ${tempDir}`);
expect(adminSkill).toContain('status --json');
await expect(readZipText(skillBundlePath, '.mcp.json')).rejects.toThrow('Missing zip entry');
expect(io.stdout()).toContain(skillBundlePath);
await expect(readZipText(adminSkillPath, '.mcp.json')).rejects.toThrow('Missing zip entry');
await expect(readZipText(adminSkillPath, 'ktx-analytics/SKILL.md')).rejects.toThrow('Missing zip entry');
expect(io.stdout()).toContain(analyticsSkillPath);
expect(io.stdout()).toContain(adminSkillPath);
expect(io.stdout()).toContain('Upload each file separately:');
} finally {
process.env.HOME = previousHome;
await rm(home, { recursive: true, force: true });
@ -826,10 +842,12 @@ describe('setup agents', () => {
},
io.io,
);
const skillBundlePath = join(tempDir, '.ktx/agents/claude/ktx-skills.zip');
const analyticsSkillPath = join(tempDir, '.ktx/agents/claude/ktx-analytics.zip');
const adminSkillPath = join(tempDir, '.ktx/agents/claude/ktx.zip');
const launcherPath = join(tempDir, '.ktx/agents/claude/ktx-plugin-runner.sh');
const configPath = join(home, 'Library/Application Support/Claude/claude_desktop_config.json');
await expect(stat(skillBundlePath)).resolves.toBeDefined();
await expect(stat(analyticsSkillPath)).resolves.toBeDefined();
await expect(stat(adminSkillPath)).resolves.toBeDefined();
await expect(stat(launcherPath)).resolves.toBeDefined();
const beforeConfig = JSON.parse(await readFile(configPath, 'utf-8')) as {
mcpServers: Record<string, unknown>;
@ -838,7 +856,8 @@ describe('setup agents', () => {
await expect(removeKtxAgentInstall(tempDir, io.io)).resolves.toBe(0);
await expect(stat(skillBundlePath)).rejects.toThrow();
await expect(stat(analyticsSkillPath)).rejects.toThrow();
await expect(stat(adminSkillPath)).rejects.toThrow();
await expect(stat(launcherPath)).rejects.toThrow();
const afterConfig = JSON.parse(await readFile(configPath, 'utf-8')) as {
mcpServers: Record<string, unknown>;
@ -876,7 +895,7 @@ describe('setup agents', () => {
).resolves.toEqual({ status: 'skipped', projectDir: tempDir });
});
it('explains how to select multiple agent targets in interactive mode', async () => {
it('prints one navigation hint before interactive agent target prompts', async () => {
const io = makeIo();
const prompts = {
select: vi.fn(async () => 'mcp-cli'),
@ -900,10 +919,11 @@ describe('setup agents', () => {
),
).resolves.toEqual({ status: 'back', projectDir: tempDir });
expect(io.stdout()).toContain('Space to select, Enter to confirm, Esc to go back.');
expect(io.stdout().match(/Space to select/g)).toHaveLength(1);
expect(prompts.multiselect).toHaveBeenCalledWith(
expect.objectContaining({
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.',
message: 'Which agent targets should KTX install?',
}),
);
});
@ -926,21 +946,21 @@ describe('setup agents', () => {
);
const output = io.stdout();
expect(output).toContain('Agent integration complete');
expect(output).toContain(`KTX project\n ${tempDir}`);
expect(output).toContain('Installed agents');
expect(output).toContain('Claude Code');
expect(output).toContain(`Project scope\n ${join(tempDir, '.mcp.json')}`);
expect(output).toContain('Requires MCP to be started');
expect(output).toContain('Analytics skill installed');
expect(output).toContain('Admin CLI skill installed');
expect(output).toContain('Claude Code · Project scope');
expect(output).toContain(join(tempDir, '.mcp.json'));
expect(output).toContain('Requires MCP to be started.');
expect(output).toContain('Analytics skill installed.');
expect(output).toContain('Admin CLI skill installed.');
expect(output).not.toContain('Agent integration complete');
expect(output).not.toContain(`KTX project\n ${tempDir}`);
expect(output).not.toContain('Installed agents');
expect(output).not.toContain('.claude/skills/ktx-analytics/SKILL.md');
expect(output).not.toContain('.claude/skills/ktx/SKILL.md');
expect(output).not.toContain('.claude/rules/ktx.md');
});
it('formats summary with explicit project-scoped config paths', () => {
const summary = formatInstallSummary(
const summary = formatInstallSummaryLines(
[{ target: 'cursor', scope: 'project', mode: 'mcp-cli' }],
[
{ kind: 'file', path: join(tempDir, '.cursor/rules/ktx-analytics.mdc'), role: 'analytics-skill' },
@ -950,16 +970,20 @@ describe('setup agents', () => {
tempDir,
);
expect(summary).toContain('Cursor');
expect(summary).toContain(`Project scope\n ${join(tempDir, '.cursor/mcp.json')}`);
expect(summary).toContain('Requires MCP to be started');
expect(summary).toContain('Cursor rules installed');
expect(summary).not.toContain('.cursor/rules/ktx-analytics.mdc');
expect(summary).not.toContain('.cursor/rules/ktx.mdc');
expect(summary).toEqual([
{
title: 'Cursor · Project scope',
lines: [
join(tempDir, '.cursor/mcp.json'),
'Requires MCP to be started.',
'Cursor rules installed.',
],
},
]);
});
it('formats summary with multiple agent targets', () => {
const summary = formatInstallSummary(
const summary = formatInstallSummaryLines(
[
{ target: 'claude-code', scope: 'project', mode: 'mcp-cli' },
{ target: 'codex', scope: 'project', mode: 'mcp-cli' },
@ -976,16 +1000,25 @@ describe('setup agents', () => {
tempDir,
);
expect(summary).toContain('Claude Code');
expect(summary).toContain('Project scope\n ');
expect(summary).toContain('Analytics skill installed');
expect(summary).toContain('Admin CLI skill installed');
expect(summary).toContain('\n\n Codex\n');
expect(summary).toContain('MCP config\n Add the snippet shown below to ~/.codex/config.toml.');
expect(summary).toContain('Codex');
expect(summary).toContain('Codex guidance installed');
expect(summary).not.toContain('.agents/skills/ktx-analytics/SKILL.md');
expect(summary).not.toContain('.agents/skills/ktx/SKILL.md');
expect(summary).toEqual([
{
title: 'Claude Code · Project scope',
lines: [
join(tempDir, '.mcp.json'),
'Requires MCP to be started.',
'Analytics skill installed.',
'Admin CLI skill installed.',
],
},
{
title: 'Codex · Project scope',
lines: [
'Add the snippet shown below to ~/.codex/config.toml.',
'Requires MCP to be started.',
'Codex guidance installed.',
],
},
]);
});
it('prints one target-aware next actions block for mixed agent targets', async () => {
@ -1038,8 +1071,10 @@ describe('setup agents', () => {
expect(output).toContain('Claude Desktop loads KTX MCP after restart.');
expect(output).toContain('4. Upload Claude Desktop skills');
expect(output).toContain('Customize > Skills > + > Create skill > Upload a skill');
expect(output).toContain(join(tempDir, '.ktx/agents/claude/ktx-skills.zip'));
expect(output).toContain(join(tempDir, '.ktx/agents/claude/ktx-analytics.zip'));
expect(output).not.toContain(join(tempDir, '.ktx/agents/claude/ktx.zip'));
expect(output).toContain('Upload this file:');
expect(output).toContain('All set.');
expect(output).not.toContain('Finish Claude Desktop setup');
expect(output).not.toContain('Run `ktx mcp start` to enable the configured KTX MCP server.');
} finally {

View file

@ -1,7 +1,9 @@
import { existsSync } from 'node:fs';
import { chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import { dirname, join, relative, resolve } from 'node:path';
import type { Writable } from 'node:stream';
import { fileURLToPath } from 'node:url';
import { log, outro } from '@clack/prompts';
import {
loadKtxProject,
markKtxSetupStateStepComplete,
@ -9,7 +11,6 @@ import {
} from '@ktx/context/project';
import { strToU8, zipSync } from 'fflate';
import type { KtxCliIo } from './cli-runtime.js';
import { withMultiselectNavigation } from './prompt-navigation.js';
import {
createKtxSetupPromptAdapter,
createKtxSetupUiAdapter,
@ -81,6 +82,38 @@ interface KtxCliLauncher {
args: string[];
}
function isWritableTtyOutput(output: KtxCliIo['stdout']): output is KtxCliIo['stdout'] & Writable {
return (
output.isTTY === true &&
typeof (output as { on?: unknown }).on === 'function' &&
typeof (output as { columns?: unknown }).columns !== 'undefined'
);
}
function writeSetupInfo(io: KtxCliIo, message: string): void {
if (isWritableTtyOutput(io.stdout)) {
log.info(message, { output: io.stdout });
return;
}
io.stdout.write(`${message}\n`);
}
function writeSetupStep(io: KtxCliIo, message: string): void {
if (isWritableTtyOutput(io.stdout)) {
log.step(message, { output: io.stdout });
return;
}
io.stdout.write(`\n${message}\n`);
}
function writeSetupOutro(io: KtxCliIo, message: string): void {
if (isWritableTtyOutput(io.stdout)) {
outro(message, { output: io.stdout });
return;
}
io.stdout.write(`\n${message}\n`);
}
async function readJsonObject(path: string): Promise<Record<string, unknown>> {
if (!existsSync(path)) return {};
const parsed = JSON.parse(await readFile(path, 'utf-8')) as unknown;
@ -314,8 +347,12 @@ export function agentInstallManifestPath(projectDir: string): string {
return join(resolve(projectDir), '.ktx/agents/install-manifest.json');
}
function claudeDesktopSkillBundlePath(projectDir: string): string {
return join(resolve(projectDir), '.ktx/agents/claude/ktx-skills.zip');
function claudeDesktopAnalyticsSkillBundlePath(projectDir: string): string {
return join(resolve(projectDir), '.ktx/agents/claude/ktx-analytics.zip');
}
function claudeDesktopAdminSkillBundlePath(projectDir: string): string {
return join(resolve(projectDir), '.ktx/agents/claude/ktx.zip');
}
function claudeDesktopLauncherPath(projectDir: string): string {
@ -363,9 +400,18 @@ export function plannedKtxAgentFiles(input: {
{ kind: 'file', path: claudeDesktopLauncherPath(input.projectDir), role: 'launcher' as const },
{
kind: 'file',
path: claudeDesktopSkillBundlePath(input.projectDir),
path: claudeDesktopAnalyticsSkillBundlePath(input.projectDir),
role: 'claude-desktop-skill-bundle' as const,
},
...(withAdminCli
? [
{
kind: 'file' as const,
path: claudeDesktopAdminSkillBundlePath(input.projectDir),
role: 'claude-desktop-skill-bundle' as const,
},
]
: []),
];
}
throw new Error(`Global ${input.target} installation is not supported; omit --global.`);
@ -553,21 +599,30 @@ function claudeDesktopLauncherContent(input: { launcher: KtxCliLauncher }): stri
async function writeClaudeDesktopSkillBundle(input: {
projectDir: string;
path: string;
mode: KtxAgentInstallMode;
skillName: 'ktx-analytics' | 'ktx';
launcher: KtxCliLauncher;
}): Promise<void> {
const content =
input.skillName === 'ktx-analytics'
? await readAnalyticsSkillContent()
: cliInstructionContent({ projectDir: input.projectDir, launcher: input.launcher });
const files: Record<string, Uint8Array> = {
'ktx-analytics/SKILL.md': strToU8(await readAnalyticsSkillContent()),
[`${input.skillName}/SKILL.md`]: strToU8(content),
};
if (input.mode === 'mcp-cli') {
files['ktx/SKILL.md'] = strToU8(
cliInstructionContent({ projectDir: input.projectDir, launcher: input.launcher }),
);
}
await mkdir(dirname(input.path), { recursive: true });
await writeFile(input.path, Buffer.from(zipSync(files)));
}
function claudeDesktopSkillNameForBundle(path: string): 'ktx-analytics' | 'ktx' {
if (path.endsWith('/ktx-analytics.zip')) {
return 'ktx-analytics';
}
if (path.endsWith('/ktx.zip')) {
return 'ktx';
}
throw new Error(`Unsupported Claude Desktop skill bundle path: ${path}`);
}
async function writeClaudeDesktopLauncher(input: {
path: string;
launcher: KtxCliLauncher;
@ -747,12 +802,27 @@ function hasAdminCliEntries(entries: InstallEntry[]): boolean {
);
}
export function formatInstallSummary(
export interface InstallSummaryEntry {
title: string;
lines: string[];
}
function formatInlinePath(path: string): string {
const home = process.env.HOME;
if (!home) return path;
const resolvedHome = resolve(home);
if (path === resolvedHome) return '~';
if (path.startsWith(`${resolvedHome}/`)) {
return `~/${relative(resolvedHome, path)}`;
}
return path;
}
export function formatInstallSummaryLines(
installs: Array<{ target: KtxAgentTarget; scope: KtxAgentScope; mode: KtxAgentInstallMode }>,
entries: InstallEntry[],
projectDir: string,
): string {
const resolvedProjectDir = resolve(projectDir);
): InstallSummaryEntry[] {
const entriesByTarget = new Map<KtxAgentTarget, InstallEntry[]>();
for (const install of installs) {
const plannedFilePaths = new Set(
@ -774,51 +844,57 @@ export function formatInstallSummary(
);
}
const lines: string[] = ['KTX project', ` ${resolvedProjectDir}`, '', 'Installed agents'];
for (const install of installs) {
return installs.map((install) => {
const targetEntries = entriesByTarget.get(install.target) ?? [];
const mcpEntry = mcpEntriesByTarget
.get(install.target)
?.find((entry): entry is Extract<InstallEntry, { kind: 'json-key' }> => entry.kind === 'json-key');
lines.push('', ` ${targetDisplayName(install.target)}`);
const lines: string[] = [];
if (mcpEntry) {
lines.push(` ${scopeDisplayName(install.scope)}`);
lines.push(` ${mcpEntry.path}`);
lines.push(formatInlinePath(mcpEntry.path));
} else if (install.target !== 'claude-desktop') {
lines.push(' MCP config');
lines.push(` ${manualMcpConfigInstruction(install.target, install.scope)}`);
lines.push(manualMcpConfigInstruction(install.target, install.scope));
}
if (targetUsesHttpMcpDaemon(install.target)) {
lines.push(' Requires MCP to be started');
lines.push('Requires MCP to be started.');
}
const hasAnalytics = hasEntryRole(targetEntries, 'analytics-skill');
const hasAdmin = hasAdminCliEntries(targetEntries);
const claudeDesktopSkillBundles = targetEntries.filter(
(entry): entry is Extract<InstallEntry, { kind: 'file' }> =>
entry.kind === 'file' && entry.role === 'claude-desktop-skill-bundle',
);
if (install.target === 'claude-code') {
if (hasAnalytics) {
lines.push(' Analytics skill installed');
lines.push('Analytics skill installed.');
}
if (hasAdmin) {
lines.push(' Admin CLI skill installed');
lines.push('Admin CLI skill installed.');
}
} else if (install.target === 'claude-desktop') {
if (claudeDesktopSkillBundles.length > 0) {
lines.push(' Claude Desktop skill uploads');
lines.push('Skill bundles:');
for (const bundle of claudeDesktopSkillBundles) {
lines.push(` ${bundle.path}`);
lines.push(` ${bundle.path}`);
}
}
} else if (hasAnalytics || hasAdmin) {
lines.push(` ${guidanceInstallLine(install.target)}`);
lines.push(`${guidanceInstallLine(install.target)}.`);
}
if (hasEntryRole(targetEntries, 'launcher')) {
lines.push(' Starts KTX over stdio from Claude Desktop');
lines.push('Starts KTX over stdio from Claude Desktop.');
}
}
return lines.join('\n');
return {
title: `${targetDisplayName(install.target)} · ${scopeDisplayName(install.scope)}`,
lines,
};
});
}
function claudeDesktopSkillBundlePathsForInstalls(
@ -980,7 +1056,7 @@ function formatAgentNextActions(input: {
if (skillBundlePaths.length > 0) {
lines.push(`${step}. Upload Claude Desktop skills`);
lines.push(' Open Claude Desktop: Customize > Skills > + > Create skill > Upload a skill.');
lines.push(' Upload this file:');
lines.push(skillBundlePaths.length === 1 ? ' Upload this file:' : ' Upload each file separately:');
for (const path of skillBundlePaths) {
lines.push(` ${path}`);
}
@ -1016,7 +1092,7 @@ async function installTarget(input: {
await writeClaudeDesktopSkillBundle({
projectDir: input.projectDir,
path: entry.path,
mode: input.mode,
skillName: claudeDesktopSkillNameForBundle(entry.path),
launcher,
});
continue;
@ -1053,6 +1129,9 @@ export async function runKtxSetupAgentsStep(
}
const prompts = deps.prompts ?? createPromptAdapter();
if (args.inputMode === 'auto' && args.target === undefined) {
writeSetupInfo(io, 'Space to select, Enter to confirm, Esc to go back.');
}
const mode =
args.inputMode === 'disabled'
? args.mode
@ -1079,7 +1158,7 @@ export async function runKtxSetupAgentsStep(
: args.inputMode === 'disabled'
? []
: ((await prompts.multiselect({
message: withMultiselectNavigation('Which agent targets should KTX install?'),
message: 'Which agent targets should KTX install?',
options: [
{ value: 'claude-code', label: 'Claude Code' },
{ value: 'claude-desktop', label: 'Claude Desktop' },
@ -1143,11 +1222,12 @@ export async function runKtxSetupAgentsStep(
);
await markAgentsComplete(args.projectDir);
const setupUi = createKtxSetupUiAdapter();
setupUi.note(
formatInstallSummary(installs, entries, args.projectDir),
'Agent integration complete',
io,
);
for (const summary of formatInstallSummaryLines(installs, entries, args.projectDir)) {
writeSetupStep(
io,
summary.lines.length > 0 ? `${summary.title}\n${summary.lines.join('\n')}` : summary.title,
);
}
const nextActions = formatAgentNextActions({
projectDir: args.projectDir,
installs,
@ -1156,6 +1236,7 @@ export async function runKtxSetupAgentsStep(
});
if (args.showNextActions !== false) {
setupUi.note(nextActions, 'Required before using agents', io, { format: (line) => line });
writeSetupOutro(io, 'All set.');
}
return { status: 'ready', projectDir: args.projectDir, installs, nextActions };
} catch (error) {