mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-13 08:15:14 +02:00
fix(cli): package Claude Desktop skills in one zip
This commit is contained in:
parent
b42f418adc
commit
ddabe517e3
4 changed files with 130 additions and 107 deletions
11
README.md
11
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`
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>;
|
||||
|
|
@ -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<string, unknown>;
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
const withAdminCli = input.mode === 'mcp-cli';
|
||||
const files: Record<string, Uint8Array> = {
|
||||
'.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<void> {
|
||||
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<InstallEntry, { kind: 'file' }> =>
|
||||
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<InstallEntry, { kind: 'file' }> =>
|
||||
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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue