mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-22 08:38:08 +02:00
Improve KTX agent setup guidance (#137)
* feat(cli): clarify MCP start output * feat(cli): improve agent setup guidance * docs: update agent client setup guidance
This commit is contained in:
parent
b507ff171d
commit
1331e573dd
10 changed files with 804 additions and 173 deletions
|
|
@ -144,7 +144,7 @@ describe('setup agents', () => {
|
|||
},
|
||||
io.io,
|
||||
),
|
||||
).resolves.toEqual({
|
||||
).resolves.toMatchObject({
|
||||
status: 'ready',
|
||||
projectDir: tempDir,
|
||||
installs: [{ target: 'universal', scope: 'project', mode: 'mcp-cli' }],
|
||||
|
|
@ -170,6 +170,63 @@ describe('setup agents', () => {
|
|||
expect(io.stderr()).toBe('');
|
||||
});
|
||||
|
||||
it('prints standalone agent next actions after successful installation', async () => {
|
||||
const io = makeIo();
|
||||
|
||||
const result = await runKtxSetupAgentsStep(
|
||||
{
|
||||
projectDir: tempDir,
|
||||
inputMode: 'disabled',
|
||||
yes: true,
|
||||
agents: true,
|
||||
target: 'claude-code',
|
||||
scope: 'project',
|
||||
mode: 'mcp-cli',
|
||||
skipAgents: false,
|
||||
},
|
||||
io.io,
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
status: 'ready',
|
||||
nextActions: expect.stringContaining('Run this command before using Claude Code:'),
|
||||
});
|
||||
expect(io.stdout()).toContain('Required before using agents');
|
||||
expect(io.stdout()).toContain('Run this command before using Claude Code:');
|
||||
expect(io.stdout()).toContain('RUN:');
|
||||
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()).not.toContain('Finish agent setup');
|
||||
expect(io.stdout()).not.toContain('Next actions');
|
||||
});
|
||||
|
||||
it('can return agent next actions without printing them', async () => {
|
||||
const io = makeIo();
|
||||
|
||||
const result = await runKtxSetupAgentsStep(
|
||||
{
|
||||
projectDir: tempDir,
|
||||
inputMode: 'disabled',
|
||||
yes: true,
|
||||
agents: true,
|
||||
target: 'claude-code',
|
||||
scope: 'project',
|
||||
mode: 'mcp-cli',
|
||||
skipAgents: false,
|
||||
showNextActions: false,
|
||||
},
|
||||
io.io,
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
status: 'ready',
|
||||
nextActions: expect.stringContaining(`ktx mcp start --project-dir ${tempDir}`),
|
||||
});
|
||||
expect(io.stdout()).toContain('Agent integration complete');
|
||||
expect(io.stdout()).not.toContain('Required before using agents');
|
||||
});
|
||||
|
||||
it('installs the analytics skill from the runtime asset', async () => {
|
||||
const io = makeIo();
|
||||
|
||||
|
|
@ -252,7 +309,6 @@ describe('setup agents', () => {
|
|||
expect(await readKtxAgentInstallManifest(tempDir)).toMatchObject({
|
||||
entries: expect.arrayContaining([{ kind: 'json-key', path: join(tempDir, '.mcp.json'), jsonPath: ['mcpServers', 'ktx'] }]),
|
||||
});
|
||||
expect(io.stdout()).toContain('Run `ktx mcp start` to enable the configured KTX MCP server.');
|
||||
});
|
||||
|
||||
it('prompts for MCP-first client agent connection mode in interactive setup', async () => {
|
||||
|
|
@ -283,10 +339,18 @@ describe('setup agents', () => {
|
|||
});
|
||||
|
||||
expect(prompts.select).toHaveBeenCalledWith({
|
||||
message: 'How should client agents connect to this KTX project?',
|
||||
message: 'What should agents be allowed to do with this KTX project?',
|
||||
options: [
|
||||
{ value: 'mcp', label: 'MCP tools + analytics skill' },
|
||||
{ value: 'mcp-cli', label: 'MCP tools + analytics skill + admin CLI skill' },
|
||||
{
|
||||
value: 'mcp',
|
||||
label: 'Ask data questions with KTX MCP',
|
||||
hint: 'Installs the MCP connection and analytics workflow skill. Best for normal use.',
|
||||
},
|
||||
{
|
||||
value: 'mcp-cli',
|
||||
label: 'Ask data questions + manage KTX with CLI commands',
|
||||
hint: 'Adds an admin CLI skill so agents can run ktx status, sl, wiki, and setup commands.',
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(prompts.multiselect).toHaveBeenCalledWith(
|
||||
|
|
@ -330,10 +394,18 @@ describe('setup agents', () => {
|
|||
});
|
||||
|
||||
expect(prompts.select).toHaveBeenCalledWith({
|
||||
message: 'Where should KTX install supported agent config?',
|
||||
message: `Where should KTX install supported agent config?\n\nKTX project: ${tempDir}`,
|
||||
options: [
|
||||
{ value: 'project', label: 'Project' },
|
||||
{ value: 'global', label: 'Global' },
|
||||
{
|
||||
value: 'project',
|
||||
label: 'Project scope (KTX project directory)',
|
||||
hint: 'Only agents opened from this KTX project path load the project-scoped config.',
|
||||
},
|
||||
{
|
||||
value: 'global',
|
||||
label: 'Global scope (user config)',
|
||||
hint: 'Agents can load this KTX project from any working directory.',
|
||||
},
|
||||
],
|
||||
});
|
||||
} finally {
|
||||
|
|
@ -395,14 +467,18 @@ describe('setup agents', () => {
|
|||
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(io.stdout()).toContain('Claude plugin generated');
|
||||
expect(io.stdout()).toContain('.ktx/agents/claude/ktx-plugin.zip');
|
||||
expect(io.stdout()).toContain('KTX MCP server registered');
|
||||
expect(io.stdout()).toContain('Claude Desktop');
|
||||
expect(io.stdout()).not.toContain('.ktx/agents/claude/ktx-plugin.zip');
|
||||
expect(io.stdout()).toContain('claude_desktop_config.json');
|
||||
expect(io.stdout()).toContain('Restart Claude Desktop');
|
||||
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()).not.toContain('Run `ktx mcp start`');
|
||||
} finally {
|
||||
process.env.HOME = previousHome;
|
||||
|
|
@ -568,6 +644,9 @@ describe('setup agents', () => {
|
|||
);
|
||||
expect(codexIo.stdout()).toContain('[mcp_servers.ktx]');
|
||||
expect(codexIo.stdout()).toContain('url = "http://localhost:7878/mcp"');
|
||||
expect(codexIo.stdout()).toContain('1. Configure Codex');
|
||||
expect(codexIo.stdout()).toContain('Open ~/.codex/config.toml, then paste this block:');
|
||||
expect(codexIo.stdout()).toContain('PASTE:');
|
||||
|
||||
const opencodeIo = makeIo();
|
||||
await runKtxSetupAgentsStep(
|
||||
|
|
@ -585,6 +664,8 @@ describe('setup agents', () => {
|
|||
);
|
||||
expect(opencodeIo.stdout()).toContain('"mcp"');
|
||||
expect(opencodeIo.stdout()).toContain('"type": "remote"');
|
||||
expect(opencodeIo.stdout()).toContain('1. Configure OpenCode');
|
||||
expect(opencodeIo.stdout()).toContain('Open opencode.json, then paste this block:');
|
||||
await expect(readFile(join(tempDir, 'opencode.json'), 'utf-8')).rejects.toThrow();
|
||||
|
||||
const universalIo = makeIo();
|
||||
|
|
@ -603,6 +684,8 @@ describe('setup agents', () => {
|
|||
);
|
||||
expect(universalIo.stdout()).toContain('Universal MCP endpoint:');
|
||||
expect(universalIo.stdout()).toContain('http://localhost:7878/mcp');
|
||||
expect(universalIo.stdout()).toContain('1. Configure unsupported MCP clients');
|
||||
expect(universalIo.stdout()).toContain('Use this endpoint when setting up unsupported MCP clients:');
|
||||
});
|
||||
|
||||
it('uses MCP daemon state for port and token metadata without rendering literal tokens', async () => {
|
||||
|
|
@ -648,7 +731,9 @@ describe('setup agents', () => {
|
|||
expect(rendered).toContain('http://127.0.0.1:8787/mcp');
|
||||
expect(rendered).toContain('Bearer ${KTX_MCP_TOKEN}');
|
||||
expect(rendered).not.toContain('secret-token');
|
||||
expect(io.stdout()).toContain('Run `ktx mcp start` to enable the configured KTX MCP server.');
|
||||
expect(io.stdout()).toContain('Run this command before using Claude Code:');
|
||||
expect(io.stdout()).toContain('RUN:');
|
||||
expect(io.stdout()).toContain(`ktx mcp start --project-dir ${tempDir}`);
|
||||
} finally {
|
||||
if (previousToken === undefined) {
|
||||
delete process.env.KTX_MCP_TOKEN;
|
||||
|
|
@ -833,31 +918,35 @@ 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('+ Analytics skill installed — teaches your agent the KTX MCP analytics workflow');
|
||||
expect(output).toContain('.claude/skills/ktx-analytics/SKILL.md');
|
||||
expect(output).toContain('+ Skill installed — teaches admin agents which KTX CLI commands to run');
|
||||
expect(output).toContain('.claude/skills/ktx/SKILL.md');
|
||||
expect(output).toContain('+ Rule installed — tells admin agents when to use KTX CLI');
|
||||
expect(output).toContain('.claude/rules/ktx.md');
|
||||
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).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 relative paths for project scope', () => {
|
||||
it('formats summary with explicit project-scoped config paths', () => {
|
||||
const summary = formatInstallSummary(
|
||||
[{ 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).toContain('Cursor');
|
||||
expect(summary).toContain('+ Analytics skill installed — teaches your agent the KTX MCP analytics workflow');
|
||||
expect(summary).toContain('.cursor/rules/ktx-analytics.mdc');
|
||||
expect(summary).toContain('+ Rule installed — tells admin agents when to use KTX CLI');
|
||||
expect(summary).toContain('.cursor/rules/ktx.mdc');
|
||||
expect(summary).not.toContain(tempDir);
|
||||
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');
|
||||
});
|
||||
|
||||
it('formats summary with multiple agent targets', () => {
|
||||
|
|
@ -870,6 +959,7 @@ describe('setup agents', () => {
|
|||
{ 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' },
|
||||
|
|
@ -878,11 +968,159 @@ describe('setup agents', () => {
|
|||
);
|
||||
|
||||
expect(summary).toContain('Claude Code');
|
||||
expect(summary).toContain('+ Analytics skill installed — teaches your agent the KTX MCP analytics workflow');
|
||||
expect(summary).toContain('+ Skill installed — teaches admin agents which KTX CLI commands to run');
|
||||
expect(summary).toContain('+ Rule installed — tells admin agents when to use KTX CLI');
|
||||
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('.agents/skills/ktx-analytics/SKILL.md');
|
||||
expect(summary).toContain('.agents/skills/ktx/SKILL.md');
|
||||
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');
|
||||
});
|
||||
|
||||
it('prints one target-aware next actions block for mixed agent targets', async () => {
|
||||
const home = await mkdtemp(join(tmpdir(), 'ktx-setup-agents-home-'));
|
||||
const previousHome = process.env.HOME;
|
||||
process.env.HOME = home;
|
||||
try {
|
||||
const io = makeIo();
|
||||
const prompts = {
|
||||
select: vi.fn(async ({ message }: { message: string }) =>
|
||||
message.startsWith('Where should') ? 'project' : 'mcp',
|
||||
),
|
||||
multiselect: vi.fn(async () => ['claude-code', 'claude-desktop']),
|
||||
cancel: vi.fn(),
|
||||
};
|
||||
|
||||
await expect(
|
||||
runKtxSetupAgentsStep(
|
||||
{
|
||||
projectDir: tempDir,
|
||||
inputMode: 'auto',
|
||||
yes: false,
|
||||
agents: true,
|
||||
scope: 'project',
|
||||
mode: 'mcp',
|
||||
skipAgents: false,
|
||||
},
|
||||
io.io,
|
||||
{ prompts },
|
||||
),
|
||||
).resolves.toMatchObject({
|
||||
status: 'ready',
|
||||
installs: [
|
||||
{ target: 'claude-code', scope: 'project', mode: 'mcp' },
|
||||
{ target: 'claude-desktop', scope: 'global', mode: 'mcp' },
|
||||
],
|
||||
});
|
||||
|
||||
const output = io.stdout();
|
||||
expect(output).toContain('Required before using agents');
|
||||
expect(output).not.toContain('Next actions');
|
||||
expect(output).toContain('1. Start MCP');
|
||||
expect(output).toContain('Run this command before using Claude Code:');
|
||||
expect(output).toContain(`ktx mcp start --project-dir ${tempDir}`);
|
||||
expect(output).toContain(`ktx mcp stop --project-dir ${tempDir}\n\n2. Open Claude Code`);
|
||||
expect(output).toContain('Open Claude Code from the KTX project directory');
|
||||
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).not.toContain('Finish Claude Desktop setup');
|
||||
expect(output).not.toContain('Run `ktx mcp start` to enable the configured KTX MCP server.');
|
||||
} finally {
|
||||
process.env.HOME = previousHome;
|
||||
await rm(home, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('does not tell global Claude Code installs to open from the project directory', async () => {
|
||||
const home = await mkdtemp(join(tmpdir(), 'ktx-setup-agents-home-'));
|
||||
const previousHome = process.env.HOME;
|
||||
process.env.HOME = home;
|
||||
try {
|
||||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKtxSetupAgentsStep(
|
||||
{
|
||||
projectDir: tempDir,
|
||||
inputMode: 'disabled',
|
||||
yes: true,
|
||||
agents: true,
|
||||
target: 'claude-code',
|
||||
scope: 'global',
|
||||
mode: 'mcp',
|
||||
skipAgents: false,
|
||||
},
|
||||
io.io,
|
||||
),
|
||||
).resolves.toMatchObject({
|
||||
status: 'ready',
|
||||
installs: [{ target: 'claude-code', scope: 'global', mode: 'mcp' }],
|
||||
});
|
||||
|
||||
const output = io.stdout();
|
||||
expect(output).toContain('2. Open Claude Code');
|
||||
expect(output).toContain('RUN:');
|
||||
expect(output).toContain('claude');
|
||||
expect(output).not.toContain('Open Claude Code from the KTX project directory');
|
||||
expect(output).not.toContain(`cd '${tempDir}'`);
|
||||
} finally {
|
||||
process.env.HOME = previousHome;
|
||||
await rm(home, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('explains next actions for Codex, Cursor, OpenCode, and universal MCP targets', async () => {
|
||||
const io = makeIo();
|
||||
const prompts = {
|
||||
select: vi.fn(async () => 'mcp-cli'),
|
||||
multiselect: vi.fn(async () => ['codex', 'cursor', 'opencode', 'universal']),
|
||||
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.toMatchObject({
|
||||
status: 'ready',
|
||||
installs: [
|
||||
{ target: 'codex', scope: 'project', mode: 'mcp-cli' },
|
||||
{ target: 'cursor', scope: 'project', mode: 'mcp-cli' },
|
||||
{ target: 'opencode', scope: 'project', mode: 'mcp-cli' },
|
||||
{ target: 'universal', scope: 'project', mode: 'mcp-cli' },
|
||||
],
|
||||
});
|
||||
|
||||
const output = io.stdout();
|
||||
expect(output).toContain('Required before using agents');
|
||||
expect(output).toContain('1. Configure Codex');
|
||||
expect(output).toContain('2. Configure OpenCode');
|
||||
expect(output).toContain('3. Configure unsupported MCP clients');
|
||||
expect(output).toContain('4. Start MCP');
|
||||
expect(output).toContain('Run this command before using Codex, Cursor, OpenCode, and Universal .agents:');
|
||||
expect(output).toContain('Open Cursor from the KTX project directory');
|
||||
expect(output).toContain('Open ~/.codex/config.toml, then paste this block:\n\n PASTE:\n [mcp_servers.ktx]');
|
||||
expect(output).toContain('Open opencode.json, then paste this block:');
|
||||
expect(output).toContain('Use this endpoint when setting up unsupported MCP clients:');
|
||||
expect(output).toContain('Codex guidance installed');
|
||||
expect(output).toContain('Cursor rules installed');
|
||||
expect(output).toContain('OpenCode commands installed');
|
||||
expect(output).toContain('.agents guidance installed');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue