diff --git a/README.md b/README.md index 0a032543..61984fae 100644 --- a/README.md +++ b/README.md @@ -119,8 +119,8 @@ After setup, KTX prints **Required before using agents**. Complete those steps before opening the configured agent. If it shows `ktx mcp start --project-dir ...`, run that command before using Claude Code, Codex, Cursor, OpenCode, or generic MCP clients. The same output also prints the matching `ktx mcp stop` command -for when you want to stop MCP later. Claude Desktop uses its own launcher and -only needs a restart. +for when you want to stop MCP later. Claude Desktop uses its own launcher for +MCP and prints separate skill upload steps. The analytics skill teaches client agents the MCP workflow: discover data, prefer semantic-layer measures, inspect entity details before raw SQL, and @@ -136,9 +136,10 @@ ktx sl validate orders Supported client agents: Claude Code, Claude Desktop, Codex, Cursor, OpenCode, and clients that can use the printed MCP endpoint or `.agents` admin skills. Claude Desktop setup registers a local `ktx mcp stdio` server in Claude -Desktop's config and generates `.ktx/agents/claude/ktx-plugin.zip` with the -analytics skill. Restart Claude Desktop after setup; no manual plugin install -step is required. +Desktop's config and generates `.ktx/agents/claude/ktx-skills.zip` with one or +two skills, depending on the setup mode. Restart Claude Desktop after setup, +then upload that ZIP from **Customize** > **Skills** > **+** > **Create +skill** > **Upload a skill**. The release artifact manifest contains the public npm tarball and the bundled `kaelio-ktx` runtime wheel. The `python/ktx-sl` and `python/ktx-daemon` diff --git a/docs-site/content/docs/integrations/agent-clients.mdx b/docs-site/content/docs/integrations/agent-clients.mdx index 2b096640..82b97f25 100644 --- a/docs-site/content/docs/integrations/agent-clients.mdx +++ b/docs-site/content/docs/integrations/agent-clients.mdx @@ -32,7 +32,7 @@ ktx setup --agents --target codex ``` Use `--global` only with `claude-code` or `codex`. Claude Desktop always writes -global Claude Desktop config and generates a project-local plugin package: +global Claude Desktop config and generates a project-local skill ZIP: ```bash ktx setup --agents --target claude-code --global @@ -85,12 +85,12 @@ before opening the configured agent. If it shows `ktx mcp start --project-dir .. run that command before using Claude Code, Codex, Cursor, OpenCode, or generic MCP clients. The same output also prints the matching `ktx mcp stop` command for when you want to stop MCP later. Claude Desktop uses its own launcher and -only needs a restart. +prints separate skill upload steps. | Target | Ask data questions with KTX MCP | Adds when agents can manage KTX with CLI | |--------|------------------------------|---------------------------| | Claude Code | `.mcp.json`, `.claude/skills/ktx-analytics/SKILL.md` | `.claude/skills/ktx/SKILL.md`, `.claude/rules/ktx.md` | -| Claude Desktop | `~/Library/Application Support/Claude/claude_desktop_config.json` stdio entry + `.ktx/agents/claude/ktx-plugin.zip` with analytics skill | Adds `skills/ktx/SKILL.md` inside the plugin ZIP | +| Claude Desktop | `~/Library/Application Support/Claude/claude_desktop_config.json` stdio entry + `.ktx/agents/claude/ktx-skills.zip` upload | Adds `ktx/SKILL.md` inside the same ZIP | | Codex | Printed snippet for `~/.codex/config.toml`, `.agents/skills/ktx-analytics/SKILL.md` | `.agents/skills/ktx/SKILL.md`, `.codex/instructions/ktx.md` | | Cursor | `.cursor/mcp.json`, `.cursor/rules/ktx-analytics.mdc` | `.cursor/rules/ktx.mdc` | | OpenCode | Printed snippet for `opencode.json`, `.opencode/commands/ktx-analytics.md` | `.opencode/commands/ktx.md` | @@ -178,8 +178,8 @@ same markdown command definitions. ## Claude Desktop During setup, select **Claude Desktop** from the agent targets. KTX writes the -MCP server entry directly into Claude Desktop's config and prepares the -Claude Desktop skill package for the analytics workflow: +MCP server entry directly into Claude Desktop's config and prepares uploadable +Claude Desktop skill packages for the KTX workflows: - `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%AppData%/Claude/claude_desktop_config.json` (Windows) gets an @@ -187,14 +187,20 @@ Claude Desktop skill package for the analytics workflow: launcher shim at `.ktx/agents/claude/ktx-plugin-runner.sh`. The shim locates a usable Node.js (Volta, NVM, Homebrew, system) so Claude Desktop can spawn the server without needing `node` in PATH. -- `.ktx/agents/claude/ktx-plugin.zip` contains the `ktx-analytics` skill (and - the admin `ktx` skill if you choose **Ask data questions + manage KTX with - CLI commands**). This package is generated by KTX setup; no manual plugin - install step is required. +- `.ktx/agents/claude/ktx-skills.zip` contains the `ktx-analytics` skill. If + you choose **Ask data questions + manage KTX with CLI commands**, the same + ZIP also contains the admin `ktx` skill. After `ktx setup`, restart Claude Desktop so it picks up the new MCP server -entry and bundled KTX skills. No daemon needs to be running — Claude Desktop -spawns the MCP server itself per session. +entry. No daemon needs to be running -- Claude Desktop spawns the MCP server +itself per session. + +Upload the generated skill ZIP from Claude Desktop: + +1. Open **Customize** > **Skills**. +2. Click **+** > **Create skill** > **Upload a skill**. +3. Upload `.ktx/agents/claude/ktx-skills.zip`. +4. Toggle the uploaded KTX skills on. Claude Desktop does not introspect local stdio MCP servers, so the per-tool "Connector"-style UI is not rendered for KTX. The tools are still callable @@ -202,7 +208,8 @@ from any Claude Desktop chat. If you move the KTX checkout or project directory, rerun `ktx setup --agents` to refresh the absolute paths in `claude_desktop_config.json` and the launcher -shim, then restart Claude Desktop. +shim, regenerate the skill ZIP, then restart Claude Desktop and upload the new +ZIP. --- @@ -290,8 +297,8 @@ Admin CLI skills call the same KTX CLI commands: | | Claude Code | Claude Desktop | Cursor | Codex | OpenCode | |---|---|---|---|---|---| | MCP tools | Yes | Local stdio via `claude_desktop_config.json` | Yes | Snippet | Snippet | -| Analytics skill | `.claude/skills/ktx-analytics/SKILL.md` | Included in plugin ZIP | `.cursor/rules/ktx-analytics.mdc` | `.agents/skills/ktx-analytics/SKILL.md` | `.opencode/commands/ktx-analytics.md` | -| Admin CLI skills | Optional | Optional in plugin ZIP | Optional (.mdc) | Optional | Optional | +| Analytics skill | `.claude/skills/ktx-analytics/SKILL.md` | Upload `.ktx/agents/claude/ktx-skills.zip` | `.cursor/rules/ktx-analytics.mdc` | `.agents/skills/ktx-analytics/SKILL.md` | `.opencode/commands/ktx-analytics.md` | +| Admin CLI skills | Optional | Optional inside the same ZIP | Optional (.mdc) | Optional | Optional | | Global install | Yes | Claude Desktop config | No | Yes | No | -| Rule or instruction file | `.claude/rules/ktx.md` | Plugin `SETUP.md` | `.cursor/rules/ktx.mdc` | `.codex/instructions/ktx.md` | `.opencode/commands/ktx.md` | -| Skill file | `.claude/skills/ktx/SKILL.md` | `skills/ktx/SKILL.md` in plugin ZIP | Not separate | `.agents/skills/ktx/SKILL.md` | Not separate | +| Rule or instruction file | `.claude/rules/ktx.md` | Not separate | `.cursor/rules/ktx.mdc` | `.codex/instructions/ktx.md` | `.opencode/commands/ktx.md` | +| Skill file | `.claude/skills/ktx/SKILL.md` | `ktx/SKILL.md` inside `ktx-skills.zip` | Not separate | `.agents/skills/ktx/SKILL.md` | Not separate | diff --git a/packages/cli/src/setup-agents.test.ts b/packages/cli/src/setup-agents.test.ts index 74ed362a..341b1240 100644 --- a/packages/cli/src/setup-agents.test.ts +++ b/packages/cli/src/setup-agents.test.ts @@ -82,7 +82,11 @@ describe('setup agents', () => { ]); expect(plannedKtxAgentFiles({ projectDir: tempDir, target: 'claude-desktop', scope: 'global', mode: 'mcp' })).toEqual([ { kind: 'file', path: join(tempDir, '.ktx/agents/claude/ktx-plugin-runner.sh'), role: 'launcher' }, - { kind: 'file', path: join(tempDir, '.ktx/agents/claude/ktx-plugin.zip'), role: 'claude-plugin' }, + { + kind: 'file', + path: join(tempDir, '.ktx/agents/claude/ktx-skills.zip'), + role: 'claude-desktop-skill-bundle', + }, ]); expect(plannedKtxAgentFiles({ projectDir: tempDir, target: 'codex', scope: 'project', mode: 'mcp' })).toEqual([ { kind: 'file', path: join(tempDir, '.agents/skills/ktx-analytics/SKILL.md'), role: 'analytics-skill' }, @@ -123,7 +127,11 @@ describe('setup agents', () => { ]); expect(plannedKtxAgentFiles({ projectDir: tempDir, target: 'claude-desktop', scope: 'global', mode: 'mcp-cli' })).toEqual([ { kind: 'file', path: join(tempDir, '.ktx/agents/claude/ktx-plugin-runner.sh'), role: 'launcher' }, - { kind: 'file', path: join(tempDir, '.ktx/agents/claude/ktx-plugin.zip'), role: 'claude-plugin' }, + { + kind: 'file', + path: join(tempDir, '.ktx/agents/claude/ktx-skills.zip'), + role: 'claude-desktop-skill-bundle', + }, ]); }); @@ -414,7 +422,7 @@ describe('setup agents', () => { } }); - it('registers Claude Desktop MCP via claude_desktop_config.json and ships a skills-only plugin', async () => { + it('registers Claude Desktop MCP and ships an uploadable analytics skill zip', async () => { const home = await mkdtemp(join(tmpdir(), 'ktx-setup-agents-home-')); const previousHome = process.env.HOME; const envSnapshot = captureEnvKeys(process.env, ['OPENAI_API_KEY', 'ANTHROPIC_API_KEY']); @@ -444,9 +452,9 @@ describe('setup agents', () => { installs: [{ target: 'claude-desktop', scope: 'global', mode: 'mcp' }], }); - const pluginPath = join(tempDir, '.ktx/agents/claude/ktx-plugin.zip'); + const skillBundlePath = join(tempDir, '.ktx/agents/claude/ktx-skills.zip'); const launcherPath = join(tempDir, '.ktx/agents/claude/ktx-plugin-runner.sh'); - await expect(stat(pluginPath)).resolves.toBeDefined(); + await expect(stat(skillBundlePath)).resolves.toBeDefined(); const launcherStat = await stat(launcherPath); expect(launcherStat.mode & 0o111).not.toBe(0); const launcher = await readFile(launcherPath, 'utf-8'); @@ -462,23 +470,23 @@ describe('setup agents', () => { args: ['--project-dir', tempDir, 'mcp', 'stdio'], }); - expect(await readZipText(pluginPath, '.claude-plugin/plugin.json')).toContain('"name": "ktx"'); - await expect(readZipText(pluginPath, '.mcp.json')).rejects.toThrow('Missing zip entry'); - expect(await readZipText(pluginPath, 'skills/ktx-analytics/SKILL.md')).toContain('KTX Analytics Workflow'); - const setupMd = await readZipText(pluginPath, 'SETUP.md'); - expect(setupMd).not.toContain('ktx mcp start'); - expect(setupMd).toContain('no manual plugin install step is required'); - expect(setupMd).toContain('claude_desktop_config.json'); - expect(setupMd).not.toContain('Install this plugin ZIP from Claude Desktop'); - await expect(readZipText(pluginPath, 'skills/ktx/SKILL.md')).rejects.toThrow('Missing zip entry'); + 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( + 'Missing zip entry', + ); expect(io.stdout()).toContain('Claude Desktop'); - expect(io.stdout()).not.toContain('.ktx/agents/claude/ktx-plugin.zip'); + expect(io.stdout()).toContain(skillBundlePath); expect(io.stdout()).toContain('claude_desktop_config.json'); expect(io.stdout()).toContain('Required before using agents'); expect(io.stdout()).toContain('1. Restart Claude Desktop'); - expect(io.stdout()).toContain('Claude Desktop loads KTX after restart.'); - expect(io.stdout()).not.toContain('install plugin'); + expect(io.stdout()).toContain('Claude Desktop loads KTX MCP after restart.'); + expect(io.stdout()).toContain('2. Upload Claude Desktop skills'); + expect(io.stdout()).toContain('Customize > Skills > + > Create skill > Upload a skill'); + expect(io.stdout()).toContain('Upload this file:'); + expect(io.stdout()).toContain('Toggle the uploaded KTX skills on.'); expect(io.stdout()).not.toContain('Run `ktx mcp start`'); } finally { process.env.HOME = previousHome; @@ -535,7 +543,7 @@ describe('setup agents', () => { } }); - it('includes the admin CLI skill in the Claude Desktop plugin zip when requested', async () => { + it('includes an uploadable admin CLI skill zip for Claude Desktop when requested', async () => { const home = await mkdtemp(join(tmpdir(), 'ktx-setup-agents-home-')); const previousHome = process.env.HOME; process.env.HOME = home; @@ -561,12 +569,13 @@ describe('setup agents', () => { installs: [{ target: 'claude-desktop', scope: 'global', mode: 'mcp-cli' }], }); - const pluginPath = join(tempDir, '.ktx/agents/claude/ktx-plugin.zip'); - const adminSkill = await readZipText(pluginPath, 'skills/ktx/SKILL.md'); + 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'); expect(adminSkill).toContain(`--project-dir ${tempDir}`); expect(adminSkill).toContain('status --json'); - expect(await readZipText(pluginPath, 'SETUP.md')).toContain('admin CLI skill'); - await expect(readZipText(pluginPath, '.mcp.json')).rejects.toThrow('Missing zip entry'); + await expect(readZipText(skillBundlePath, '.mcp.json')).rejects.toThrow('Missing zip entry'); + expect(io.stdout()).toContain(skillBundlePath); } finally { process.env.HOME = previousHome; await rm(home, { recursive: true, force: true }); @@ -798,7 +807,7 @@ describe('setup agents', () => { await expect(readKtxAgentInstallManifest(tempDir)).resolves.toEqual(null); }); - it('removes generated Claude Desktop plugin from the manifest', async () => { + it('removes generated Claude Desktop skill zips from the manifest', async () => { const home = await mkdtemp(join(tmpdir(), 'ktx-setup-agents-home-')); const previousHome = process.env.HOME; process.env.HOME = home; @@ -817,10 +826,10 @@ describe('setup agents', () => { }, io.io, ); - const pluginPath = join(tempDir, '.ktx/agents/claude/ktx-plugin.zip'); + const skillBundlePath = join(tempDir, '.ktx/agents/claude/ktx-skills.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(pluginPath)).resolves.toBeDefined(); + await expect(stat(skillBundlePath)).resolves.toBeDefined(); await expect(stat(launcherPath)).resolves.toBeDefined(); const beforeConfig = JSON.parse(await readFile(configPath, 'utf-8')) as { mcpServers: Record; @@ -829,7 +838,7 @@ describe('setup agents', () => { await expect(removeKtxAgentInstall(tempDir, io.io)).resolves.toBe(0); - await expect(stat(pluginPath)).rejects.toThrow(); + await expect(stat(skillBundlePath)).rejects.toThrow(); await expect(stat(launcherPath)).rejects.toThrow(); const afterConfig = JSON.parse(await readFile(configPath, 'utf-8')) as { mcpServers: Record; @@ -1026,9 +1035,11 @@ describe('setup agents', () => { expect(output).toContain('RUN:'); expect(output).toContain(`cd '${tempDir}'`); expect(output).toContain('3. Restart Claude Desktop'); - expect(output).toContain('Claude Desktop loads KTX after restart.'); - expect(output).not.toContain('install plugin'); - expect(output).not.toContain(join(tempDir, '.ktx/agents/claude/ktx-plugin.zip')); + 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('Upload this file:'); expect(output).not.toContain('Finish Claude Desktop setup'); expect(output).not.toContain('Run `ktx mcp start` to enable the configured KTX MCP server.'); } finally { diff --git a/packages/cli/src/setup-agents.ts b/packages/cli/src/setup-agents.ts index 487ef271..88319808 100644 --- a/packages/cli/src/setup-agents.ts +++ b/packages/cli/src/setup-agents.ts @@ -51,7 +51,11 @@ export interface KtxAgentInstallManifest { installedAt: string; installs: Array<{ target: KtxAgentTarget; scope: KtxAgentScope; mode: KtxAgentInstallMode }>; entries: Array< - | { kind: 'file'; path: string; role?: 'skill' | 'rule' | 'analytics-skill' | 'claude-plugin' | 'launcher' } + | { + kind: 'file'; + path: string; + role?: 'skill' | 'rule' | 'analytics-skill' | 'claude-desktop-skill-bundle' | 'launcher'; + } | { kind: 'json-key'; path: string; jsonPath: string[] } >; } @@ -310,8 +314,8 @@ export function agentInstallManifestPath(projectDir: string): string { return join(resolve(projectDir), '.ktx/agents/install-manifest.json'); } -function claudeDesktopPluginPath(projectDir: string): string { - return join(resolve(projectDir), '.ktx/agents/claude/ktx-plugin.zip'); +function claudeDesktopSkillBundlePath(projectDir: string): string { + return join(resolve(projectDir), '.ktx/agents/claude/ktx-skills.zip'); } function claudeDesktopLauncherPath(projectDir: string): string { @@ -357,7 +361,11 @@ export function plannedKtxAgentFiles(input: { if (input.target === 'claude-desktop') { return [ { kind: 'file', path: claudeDesktopLauncherPath(input.projectDir), role: 'launcher' as const }, - { kind: 'file', path: claudeDesktopPluginPath(input.projectDir), role: 'claude-plugin' as const }, + { + kind: 'file', + path: claudeDesktopSkillBundlePath(input.projectDir), + role: 'claude-desktop-skill-bundle' as const, + }, ]; } throw new Error(`Global ${input.target} installation is not supported; omit --global.`); @@ -487,43 +495,7 @@ function cliInstructionContent(input: { projectDir: string; launcher: KtxCliLaun ].join('\n'); } -function claudePluginJsonContent(): string { - return `${JSON.stringify( - { - name: 'ktx', - version: '0.0.0-local', - description: 'KTX analytics workflow guidance and local MCP tools.', - }, - null, - 2, - )}\n`; -} - -function claudePluginVersionContent(): string { - return `${JSON.stringify({ version: '0.0.0-local' }, null, 2)}\n`; -} - -function claudePluginSetupContent(input: { projectDir: string; withAdminCli: boolean }): string { - return [ - '# KTX Claude Plugin', - '', - 'This package is generated by KTX setup. Claude Desktop loads KTX through the registered `claude_desktop_config.json` entry after restart; no manual plugin install step is required.', - '', - `KTX project: \`${input.projectDir}\``, - '', - 'Included:', - '', - '- `ktx-analytics` skill for the MCP analytics workflow', - ...(input.withAdminCli ? ['- `ktx` admin CLI skill for KTX maintenance commands'] : []), - '', - '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.', - '', - 'If this checkout or project directory moves, rerun `ktx setup --agents` and restart Claude Desktop.', - '', - ].join('\n'); -} - -function claudePluginLauncherContent(input: { launcher: KtxCliLauncher }): string { +function claudeDesktopLauncherContent(input: { launcher: KtxCliLauncher }): string { const binPath = input.launcher.args[0]; if (!binPath) { throw new Error('Expected KTX CLI launcher to include a bin path.'); @@ -572,27 +544,23 @@ function claudePluginLauncherContent(input: { launcher: KtxCliLauncher }): strin ' run_with_node "$(command -v node)" "$@"', 'fi', '', - 'echo "KTX plugin could not find Node.js. Set KTX_NODE to a Node executable and reinstall the plugin." >&2', + 'echo "KTX Claude Desktop launcher could not find Node.js. Set KTX_NODE to a Node executable and rerun ktx setup --agents." >&2', 'exit 127', '', ].join('\n'); } -async function writeClaudeDesktopPlugin(input: { +async function writeClaudeDesktopSkillBundle(input: { projectDir: string; path: string; mode: KtxAgentInstallMode; launcher: KtxCliLauncher; }): Promise { - const withAdminCli = input.mode === 'mcp-cli'; const files: Record = { - '.claude-plugin/plugin.json': strToU8(claudePluginJsonContent()), - 'version.json': strToU8(claudePluginVersionContent()), - 'skills/ktx-analytics/SKILL.md': strToU8(await readAnalyticsSkillContent()), - 'SETUP.md': strToU8(claudePluginSetupContent({ projectDir: input.projectDir, withAdminCli })), + 'ktx-analytics/SKILL.md': strToU8(await readAnalyticsSkillContent()), }; - if (withAdminCli) { - files['skills/ktx/SKILL.md'] = strToU8( + if (input.mode === 'mcp-cli') { + files['ktx/SKILL.md'] = strToU8( cliInstructionContent({ projectDir: input.projectDir, launcher: input.launcher }), ); } @@ -605,7 +573,7 @@ async function writeClaudeDesktopLauncher(input: { launcher: KtxCliLauncher; }): Promise { await mkdir(dirname(input.path), { recursive: true }); - await writeFile(input.path, claudePluginLauncherContent({ launcher: input.launcher }), 'utf-8'); + await writeFile(input.path, claudeDesktopLauncherContent({ launcher: input.launcher }), 'utf-8'); await chmod(input.path, 0o755); } @@ -764,7 +732,6 @@ function guidanceInstallLine(target: KtxAgentTarget): string { if (target === 'cursor') return 'Cursor rules installed'; if (target === 'opencode') return 'OpenCode commands installed'; if (target === 'universal') return '.agents guidance installed'; - if (target === 'claude-desktop') return 'Claude Desktop skills bundled'; return 'Agent guidance installed'; } @@ -826,7 +793,10 @@ export function formatInstallSummary( } const hasAnalytics = hasEntryRole(targetEntries, 'analytics-skill'); const hasAdmin = hasAdminCliEntries(targetEntries); - const hasPlugin = hasEntryRole(targetEntries, 'claude-plugin'); + const claudeDesktopSkillBundles = targetEntries.filter( + (entry): entry is Extract => + entry.kind === 'file' && entry.role === 'claude-desktop-skill-bundle', + ); if (install.target === 'claude-code') { if (hasAnalytics) { lines.push(' Analytics skill installed'); @@ -834,7 +804,14 @@ export function formatInstallSummary( if (hasAdmin) { lines.push(' Admin CLI skill installed'); } - } else if (hasAnalytics || hasAdmin || hasPlugin) { + } else if (install.target === 'claude-desktop') { + if (claudeDesktopSkillBundles.length > 0) { + lines.push(' Claude Desktop skill uploads'); + for (const bundle of claudeDesktopSkillBundles) { + lines.push(` ${bundle.path}`); + } + } + } else if (hasAnalytics || hasAdmin) { lines.push(` ${guidanceInstallLine(install.target)}`); } if (hasEntryRole(targetEntries, 'launcher')) { @@ -844,6 +821,20 @@ export function formatInstallSummary( return lines.join('\n'); } +function claudeDesktopSkillBundlePathsForInstalls( + projectDir: string, + installs: Array<{ target: KtxAgentTarget; scope: KtxAgentScope; mode: KtxAgentInstallMode }>, +): string[] { + return installs + .filter((install) => install.target === 'claude-desktop') + .flatMap((install) => plannedKtxAgentFiles({ projectDir, ...install })) + .filter( + (entry): entry is Extract => + entry.kind === 'file' && entry.role === 'claude-desktop-skill-bundle', + ) + .map((entry) => entry.path); +} + function humanList(values: string[]): string { if (values.length <= 2) { return values.join(' and '); @@ -981,9 +972,22 @@ function formatAgentNextActions(input: { if (input.installs.some((install) => install.target === 'claude-desktop')) { lines.push(`${step}. Restart Claude Desktop`); - lines.push(' Claude Desktop loads KTX after restart.'); + lines.push(' Claude Desktop loads KTX MCP after restart.'); pushBlankLine(lines); step += 1; + + const skillBundlePaths = claudeDesktopSkillBundlePathsForInstalls(projectDir, input.installs); + 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:'); + for (const path of skillBundlePaths) { + lines.push(` ${path}`); + } + lines.push(' Toggle the uploaded KTX skills on.'); + pushBlankLine(lines); + step += 1; + } } if (lines.length === 0) { @@ -1008,8 +1012,8 @@ async function installTarget(input: { await writeClaudeDesktopLauncher({ path: entry.path, launcher }); continue; } - if (entry.role === 'claude-plugin') { - await writeClaudeDesktopPlugin({ + if (entry.role === 'claude-desktop-skill-bundle') { + await writeClaudeDesktopSkillBundle({ projectDir: input.projectDir, path: entry.path, mode: input.mode,