mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-07 07:55:13 +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
16
README.md
16
README.md
|
|
@ -108,9 +108,16 @@ ktx wiki search "refund policy" --json
|
|||
ktx sl query --connection-id warehouse --measure orders.revenue --format sql
|
||||
```
|
||||
|
||||
During agent setup, choose **MCP tools + analytics skill** for client agents.
|
||||
Choose **MCP tools + analytics skill + admin CLI skill** only when a developer
|
||||
or operator agent also needs pinned `ktx` admin commands.
|
||||
During agent setup, choose **Ask data questions with KTX MCP** for client
|
||||
agents. Choose **Ask data questions + manage KTX with CLI commands** only when
|
||||
a developer or operator agent also needs pinned `ktx` admin commands.
|
||||
|
||||
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.
|
||||
|
||||
The analytics skill teaches client agents the MCP workflow: discover data,
|
||||
prefer semantic-layer measures, inspect entity details before raw SQL, and
|
||||
|
|
@ -127,7 +134,8 @@ 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.
|
||||
analytics skill. Restart Claude Desktop after setup; no manual plugin install
|
||||
step is required.
|
||||
|
||||
The release artifact manifest contains the public npm tarball and the bundled
|
||||
`kaelio-ktx` runtime wheel. The `python/ktx-sl` and `python/ktx-daemon`
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ KTX exposes context to end-user agents through MCP tools. The CLI remains the
|
|||
admin surface for setup, ingest, status, daemon lifecycle, and debugging.
|
||||
|
||||
Run `ktx setup` and select your client agent targets, or configure manually
|
||||
using the snippets below. Choose **MCP tools + analytics skill** for client
|
||||
agents. Choose **MCP tools + analytics skill + admin CLI skill** only when a
|
||||
developer or operator agent also needs pinned `ktx` admin commands.
|
||||
using the snippets below. Choose **Ask data questions with KTX MCP** for client
|
||||
agents. Choose **Ask data questions + manage KTX with CLI commands** only when
|
||||
a developer or operator agent also needs pinned `ktx` admin commands.
|
||||
|
||||
## Install with setup
|
||||
|
||||
|
|
@ -31,8 +31,8 @@ Use `--target` for one target:
|
|||
ktx setup --agents --target codex
|
||||
```
|
||||
|
||||
Use `--global` only with `claude-code` or `codex`. Claude Desktop always
|
||||
generates a project-local plugin ZIP:
|
||||
Use `--global` only with `claude-code` or `codex`. Claude Desktop always writes
|
||||
global Claude Desktop config and generates a project-local plugin package:
|
||||
|
||||
```bash
|
||||
ktx setup --agents --target claude-code --global
|
||||
|
|
@ -46,9 +46,9 @@ remove only files KTX installed.
|
|||
The interactive command asks two questions:
|
||||
|
||||
```txt
|
||||
◆ How should client agents connect to this KTX project?
|
||||
│ ○ MCP tools + analytics skill
|
||||
│ ○ MCP tools + analytics skill + admin CLI skill
|
||||
◆ What should agents be allowed to do with this KTX project?
|
||||
│ ○ Ask data questions with KTX MCP
|
||||
│ ○ Ask data questions + manage KTX with CLI commands
|
||||
└
|
||||
|
||||
◆ Which agent targets should KTX install?
|
||||
|
|
@ -66,18 +66,28 @@ also asks where to install supported agent config:
|
|||
|
||||
```txt
|
||||
◆ Where should KTX install supported agent config?
|
||||
│ ○ Project
|
||||
│ ○ Global
|
||||
│
|
||||
│ KTX project: /path/to/your/ktx-project
|
||||
│
|
||||
│ ○ Project scope (KTX project directory)
|
||||
│ ○ Global scope (user config)
|
||||
└
|
||||
```
|
||||
|
||||
## Generated files
|
||||
|
||||
KTX writes MCP client configuration and an analytics skill by default. It writes
|
||||
admin CLI skills only when you choose **MCP tools + analytics skill + admin CLI
|
||||
skill**.
|
||||
KTX writes MCP client configuration and analytics guidance by default. It writes
|
||||
admin CLI guidance only when you choose **Ask data questions + manage KTX with
|
||||
CLI commands**.
|
||||
|
||||
| Target | MCP tools + analytics skill | Adds with admin CLI skill |
|
||||
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.
|
||||
|
||||
| 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 |
|
||||
|
|
@ -144,7 +154,7 @@ During setup, select **Cursor** from the agent targets. KTX writes:
|
|||
|
||||
| Mode | File |
|
||||
|------|------|
|
||||
| MCP tools + analytics skill | `.cursor/mcp.json`, `.cursor/rules/ktx-analytics.mdc` |
|
||||
| Ask data questions with KTX MCP | `.cursor/mcp.json`, `.cursor/rules/ktx-analytics.mdc` |
|
||||
| Admin CLI rules | `.cursor/rules/ktx.mdc` |
|
||||
|
||||
Cursor supports project-scoped installation only.
|
||||
|
|
@ -168,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 generates a separate
|
||||
plugin ZIP for the analytics skill:
|
||||
MCP server entry directly into Claude Desktop's config and prepares the
|
||||
Claude Desktop skill package for the analytics workflow:
|
||||
|
||||
- `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or
|
||||
`%AppData%/Claude/claude_desktop_config.json` (Windows) gets an
|
||||
|
|
@ -178,13 +188,13 @@ plugin ZIP for the analytics skill:
|
|||
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 **MCP tools + analytics skill + admin
|
||||
CLI skill**). Install the ZIP from Claude Desktop's plugin UI to load the
|
||||
skill.
|
||||
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.
|
||||
|
||||
After `ktx setup`, restart Claude Desktop so it picks up the new MCP server
|
||||
entry, then install the plugin ZIP. No daemon needs to be running - Claude
|
||||
Desktop spawns the MCP server itself per session.
|
||||
entry and bundled KTX skills. No daemon needs to be running — Claude Desktop
|
||||
spawns the MCP server itself per session.
|
||||
|
||||
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
|
||||
|
|
@ -192,7 +202,7 @@ 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 reinstall the regenerated plugin ZIP.
|
||||
shim, then restart Claude Desktop.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -235,7 +245,7 @@ During setup, select **OpenCode** from the agent targets. KTX writes:
|
|||
|
||||
| Mode | File |
|
||||
|------|------|
|
||||
| MCP tools + analytics skill | Snippet for `opencode.json`, `.opencode/commands/ktx-analytics.md` |
|
||||
| Ask data questions with KTX MCP | Snippet for `opencode.json`, `.opencode/commands/ktx-analytics.md` |
|
||||
| Admin CLI commands | `.opencode/commands/ktx.md` |
|
||||
|
||||
OpenCode supports project-scoped installation only.
|
||||
|
|
@ -282,6 +292,6 @@ Admin CLI skills call the same KTX CLI commands:
|
|||
| 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 |
|
||||
| Global install | Yes | Project-local ZIP | No | Yes | No |
|
||||
| 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 |
|
||||
|
|
|
|||
|
|
@ -79,7 +79,43 @@ describe('registerMcpCommands', () => {
|
|||
|
||||
expect(startDaemon).toHaveBeenCalledTimes(1);
|
||||
expect(context.io.stdout.write).toHaveBeenCalledWith(
|
||||
'KTX MCP daemon already running: http://127.0.0.1:7878/mcp\n',
|
||||
[
|
||||
'KTX MCP daemon already running: http://127.0.0.1:7878/mcp',
|
||||
'',
|
||||
'KTX is ready for configured agents.',
|
||||
'Open your agent for this KTX project and ask a data question, for example:',
|
||||
' "Use KTX to show me the available tables and metrics."',
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
});
|
||||
|
||||
it('prints a friendly next step after starting the daemon', async () => {
|
||||
const program = new Command().exitOverride().option('--project-dir <path>');
|
||||
const startDaemon = vi.fn().mockResolvedValue({
|
||||
status: 'started',
|
||||
url: 'http://127.0.0.1:7878/mcp',
|
||||
state: {
|
||||
schemaVersion: 1,
|
||||
pid: 4242,
|
||||
host: '127.0.0.1',
|
||||
port: 7878,
|
||||
tokenAuth: false,
|
||||
projectDir: '/tmp/ktx-started',
|
||||
startedAt: '2026-05-14T00:00:00.000Z',
|
||||
logPath: '/tmp/ktx-started/.ktx/logs/mcp.log',
|
||||
},
|
||||
});
|
||||
const context = makeContext({ deps: { mcp: { startDaemon } } });
|
||||
registerMcpCommands(program, context);
|
||||
|
||||
await program.parseAsync(['--project-dir', '/tmp/ktx-started', 'mcp', 'start'], { from: 'user' });
|
||||
|
||||
expect(context.io.stdout.write).toHaveBeenCalledWith(
|
||||
expect.stringContaining('KTX MCP daemon started: http://127.0.0.1:7878/mcp\n\nKTX is ready for configured agents.'),
|
||||
);
|
||||
expect(context.io.stdout.write).toHaveBeenCalledWith(
|
||||
expect.stringContaining('"Use KTX to show me the available tables and metrics."'),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,17 @@ function binPath(): string {
|
|||
return fileURLToPath(new URL('../bin.js', import.meta.url));
|
||||
}
|
||||
|
||||
function formatMcpStartResultMessage(input: { status: 'started' | 'already-running'; url: string }): string {
|
||||
return [
|
||||
input.status === 'started' ? `KTX MCP daemon started: ${input.url}` : `KTX MCP daemon already running: ${input.url}`,
|
||||
'',
|
||||
'KTX is ready for configured agents.',
|
||||
'Open your agent for this KTX project and ask a data question, for example:',
|
||||
' "Use KTX to show me the available tables and metrics."',
|
||||
'',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
export function registerMcpCommands(program: Command, context: KtxCliCommandContext): void {
|
||||
const mcp = program.command('mcp').description('Run the KTX MCP HTTP server');
|
||||
|
||||
|
|
@ -82,11 +93,7 @@ export function registerMcpCommands(program: Command, context: KtxCliCommandCont
|
|||
allowedOrigins: options.allowedOrigin,
|
||||
binPath: binPath(),
|
||||
});
|
||||
context.io.stdout.write(
|
||||
result.status === 'started'
|
||||
? `KTX MCP daemon started: ${result.url}\n`
|
||||
: `KTX MCP daemon already running: ${result.url}\n`,
|
||||
);
|
||||
context.io.stdout.write(formatMcpStartResultMessage({ status: result.status, url: result.url }));
|
||||
});
|
||||
|
||||
mcp
|
||||
|
|
|
|||
|
|
@ -49,8 +49,10 @@ describe('KTX demo next steps', () => {
|
|||
const rendered = formatNextStepLines().join('\n');
|
||||
|
||||
expect(rendered).toContain('KTX context is ready for agents.');
|
||||
expect(rendered).toContain('KTX project directory');
|
||||
expect(rendered).toContain('ask a data question');
|
||||
expect(rendered).toContain('Verify with:');
|
||||
expect(rendered).not.toContain('this directory');
|
||||
expect(rendered).not.toContain('Preferred route');
|
||||
expect(rendered).not.toContain('Optional MCP:');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ function commandLines(commands: ReadonlyArray<{ command: string; description: st
|
|||
|
||||
export function formatNextStepLines(indent = ' '): string[] {
|
||||
return [
|
||||
`${indent}KTX context is ready for agents. Open your coding agent in this directory and ask a data question.`,
|
||||
`${indent}KTX context is ready for agents. Open your coding agent from the KTX project directory and ask a data question.`,
|
||||
`${indent}Verify with:`,
|
||||
...commandLines(KTX_NEXT_STEP_DIRECT_COMMANDS, indent),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import {
|
|||
} from '@ktx/context/project';
|
||||
import { strToU8, zipSync } from 'fflate';
|
||||
import type { KtxCliIo } from './cli-runtime.js';
|
||||
import { bold, dim, green } from './io/symbols.js';
|
||||
import { withMultiselectNavigation } from './prompt-navigation.js';
|
||||
import {
|
||||
createKtxSetupPromptAdapter,
|
||||
|
|
@ -31,6 +30,7 @@ export interface KtxSetupAgentsArgs {
|
|||
scope: KtxAgentScope;
|
||||
mode: KtxAgentInstallMode;
|
||||
skipAgents: boolean;
|
||||
showNextActions?: boolean;
|
||||
}
|
||||
|
||||
export type KtxSetupAgentsResult =
|
||||
|
|
@ -38,6 +38,7 @@ export type KtxSetupAgentsResult =
|
|||
status: 'ready';
|
||||
projectDir: string;
|
||||
installs: Array<{ target: KtxAgentTarget; scope: KtxAgentScope; mode: KtxAgentInstallMode }>;
|
||||
nextActions?: string;
|
||||
}
|
||||
| { status: 'skipped'; projectDir: string }
|
||||
| { status: 'back'; projectDir: string }
|
||||
|
|
@ -69,6 +70,8 @@ interface KtxMcpClientInstallResult {
|
|||
notices: string[];
|
||||
}
|
||||
|
||||
const MCP_DAEMON_REQUIRED_NOTICE = 'mcp-daemon-required';
|
||||
|
||||
interface KtxCliLauncher {
|
||||
command: string;
|
||||
args: string[];
|
||||
|
|
@ -257,7 +260,7 @@ async function installMcpClientConfig(input: {
|
|||
|
||||
const endpoint = await resolveMcpEndpoint(input.projectDir);
|
||||
if (!endpoint.running) {
|
||||
notices.push('Run `ktx mcp start` to enable the configured KTX MCP server.');
|
||||
notices.push(MCP_DAEMON_REQUIRED_NOTICE);
|
||||
}
|
||||
|
||||
if (input.target === 'claude-code') {
|
||||
|
|
@ -269,15 +272,15 @@ async function installMcpClientConfig(input: {
|
|||
await writeJsonKey(config.path, config.jsonPath, cursorMcpEntry(endpoint));
|
||||
entries.push({ kind: 'json-key', path: config.path, jsonPath: config.jsonPath });
|
||||
} else if (input.target === 'codex') {
|
||||
snippets.push(`Codex MCP snippet for ~/.codex/config.toml:\n${codexSnippet(endpoint)}`);
|
||||
snippets.push(`Add this Codex MCP snippet to ~/.codex/config.toml:\n${codexSnippet(endpoint)}`);
|
||||
} else if (input.target === 'opencode') {
|
||||
const path =
|
||||
input.scope === 'global'
|
||||
? '~/.config/opencode/opencode.json'
|
||||
: relative(input.projectDir, join(input.projectDir, 'opencode.json'));
|
||||
snippets.push(`opencode MCP snippet for ${path}:\n${opencodeSnippet(endpoint)}`);
|
||||
snippets.push(`Add this OpenCode MCP snippet to ${path}:\n${opencodeSnippet(endpoint)}`);
|
||||
} else if (input.target === 'universal') {
|
||||
snippets.push(universalMcpSnippet(endpoint));
|
||||
snippets.push(`Use this universal MCP endpoint with unsupported MCP clients:\n${universalMcpSnippet(endpoint)}`);
|
||||
}
|
||||
|
||||
return { entries, snippets, notices };
|
||||
|
|
@ -504,7 +507,7 @@ function claudePluginSetupContent(input: { projectDir: string; withAdminCli: boo
|
|||
return [
|
||||
'# KTX Claude Plugin',
|
||||
'',
|
||||
'Install this plugin ZIP from Claude Desktop to load the KTX analytics skill.',
|
||||
'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}\``,
|
||||
'',
|
||||
|
|
@ -515,7 +518,7 @@ function claudePluginSetupContent(input: { projectDir: string; withAdminCli: boo
|
|||
'',
|
||||
'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 reinstall the regenerated plugin.',
|
||||
'If this checkout or project directory moves, rerun `ktx setup --agents` and restart Claude Desktop.',
|
||||
'',
|
||||
].join('\n');
|
||||
}
|
||||
|
|
@ -719,17 +722,8 @@ const targetDisplayNames: Record<KtxAgentTarget, string> = {
|
|||
universal: 'Universal .agents',
|
||||
};
|
||||
|
||||
const fileEntryLabels: Record<KtxAgentTarget, string> = {
|
||||
'claude-code': 'Skill installed',
|
||||
'claude-desktop': 'Skill installed',
|
||||
codex: 'Skill installed',
|
||||
cursor: 'Rule installed',
|
||||
opencode: 'Command installed',
|
||||
universal: 'Skill installed',
|
||||
};
|
||||
|
||||
function mcpEntryLabel(entry: Extract<InstallEntry, { kind: 'json-key' }>): string {
|
||||
return `MCP config installed — connects client agents to KTX MCP tools (${entry.jsonPath.join('.')})`;
|
||||
export function targetDisplayName(target: string): string {
|
||||
return Object.hasOwn(targetDisplayNames, target) ? targetDisplayNames[target as KtxAgentTarget] : target;
|
||||
}
|
||||
|
||||
function targetSupportsGlobalScope(target: KtxAgentTarget): boolean {
|
||||
|
|
@ -740,11 +734,58 @@ function effectiveInstallScope(target: KtxAgentTarget, requestedScope: KtxAgentS
|
|||
return target === 'claude-desktop' ? 'global' : requestedScope;
|
||||
}
|
||||
|
||||
function scopeDisplayName(scope: KtxAgentScope): string {
|
||||
if (scope === 'project') return 'Project scope';
|
||||
if (scope === 'global') return 'Global scope';
|
||||
return 'Local scope';
|
||||
}
|
||||
|
||||
function targetUsesHttpMcpDaemon(target: KtxAgentTarget): boolean {
|
||||
return target !== 'claude-desktop';
|
||||
}
|
||||
|
||||
function manualMcpConfigInstruction(target: KtxAgentTarget, scope: KtxAgentScope): string {
|
||||
if (target === 'codex') {
|
||||
return 'Add the snippet shown below to ~/.codex/config.toml.';
|
||||
}
|
||||
if (target === 'opencode') {
|
||||
return scope === 'global'
|
||||
? 'Add the snippet shown below to ~/.config/opencode/opencode.json.'
|
||||
: 'Add the snippet shown below to opencode.json.';
|
||||
}
|
||||
if (target === 'universal') {
|
||||
return 'Use the printed endpoint with unsupported MCP clients.';
|
||||
}
|
||||
return 'Add the printed snippet manually.';
|
||||
}
|
||||
|
||||
function guidanceInstallLine(target: KtxAgentTarget): string {
|
||||
if (target === 'codex') return 'Codex guidance installed';
|
||||
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';
|
||||
}
|
||||
|
||||
function hasEntryRole(entries: InstallEntry[], role: Extract<InstallEntry, { kind: 'file' }>['role']): boolean {
|
||||
return entries.some((entry) => entry.kind === 'file' && entry.role === role);
|
||||
}
|
||||
|
||||
function hasAdminCliEntries(entries: InstallEntry[]): boolean {
|
||||
return entries.some(
|
||||
(entry) =>
|
||||
entry.kind === 'file' &&
|
||||
(entry.role === 'skill' || entry.role === 'rule' || entry.role === undefined),
|
||||
);
|
||||
}
|
||||
|
||||
export function formatInstallSummary(
|
||||
installs: Array<{ target: KtxAgentTarget; scope: KtxAgentScope; mode: KtxAgentInstallMode }>,
|
||||
entries: InstallEntry[],
|
||||
projectDir: string,
|
||||
): string {
|
||||
const resolvedProjectDir = resolve(projectDir);
|
||||
const entriesByTarget = new Map<KtxAgentTarget, InstallEntry[]>();
|
||||
for (const install of installs) {
|
||||
const plannedFilePaths = new Set(
|
||||
|
|
@ -766,51 +807,193 @@ export function formatInstallSummary(
|
|||
);
|
||||
}
|
||||
|
||||
const fileHints: Record<string, string> = {
|
||||
skill: 'teaches admin agents which KTX CLI commands to run',
|
||||
rule: 'tells admin agents when to use KTX CLI',
|
||||
'analytics-skill': 'teaches your agent the KTX MCP analytics workflow',
|
||||
'claude-plugin': 'bundles KTX skills for Claude Desktop (MCP server is registered in claude_desktop_config.json)',
|
||||
launcher: 'runs the local KTX CLI with an available Node.js for Claude Desktop',
|
||||
};
|
||||
|
||||
const lines: string[] = [];
|
||||
const lines: string[] = ['KTX project', ` ${resolvedProjectDir}`, '', 'Installed agents'];
|
||||
for (const install of installs) {
|
||||
const targetEntries = entriesByTarget.get(install.target) ?? [];
|
||||
lines.push(` ${targetDisplayNames[install.target]}`);
|
||||
for (const entry of targetEntries) {
|
||||
if (entry.kind === 'file') {
|
||||
const isRule = entry.role === 'rule' || (!entry.role && fileEntryLabels[install.target] === 'Rule installed');
|
||||
const label =
|
||||
entry.role === 'analytics-skill'
|
||||
? 'Analytics skill installed'
|
||||
: entry.role === 'claude-plugin'
|
||||
? 'Claude plugin generated'
|
||||
: entry.role === 'launcher'
|
||||
? 'Launcher installed'
|
||||
: isRule
|
||||
? 'Rule installed'
|
||||
: fileEntryLabels[install.target];
|
||||
const hint = fileHints[isRule ? 'rule' : (entry.role ?? 'skill')] ?? '';
|
||||
lines.push(` + ${label} — ${hint}`);
|
||||
if (entry.role !== 'claude-plugin') {
|
||||
const displayPath =
|
||||
install.scope === 'global' ? entry.path : relative(projectDir, entry.path);
|
||||
lines.push(` ${displayPath}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const entry of mcpEntriesByTarget
|
||||
const mcpEntry = mcpEntriesByTarget
|
||||
.get(install.target)
|
||||
?.filter((entry): entry is Extract<InstallEntry, { kind: 'json-key' }> => entry.kind === 'json-key') ?? []) {
|
||||
const displayPath = install.scope === 'global' ? entry.path : relative(projectDir, entry.path);
|
||||
lines.push(` + ${mcpEntryLabel(entry)}`);
|
||||
lines.push(` ${displayPath}`);
|
||||
?.find((entry): entry is Extract<InstallEntry, { kind: 'json-key' }> => entry.kind === 'json-key');
|
||||
lines.push('', ` ${targetDisplayName(install.target)}`);
|
||||
if (mcpEntry) {
|
||||
lines.push(` ${scopeDisplayName(install.scope)}`);
|
||||
lines.push(` ${mcpEntry.path}`);
|
||||
} else if (install.target !== 'claude-desktop') {
|
||||
lines.push(' MCP config');
|
||||
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 hasPlugin = hasEntryRole(targetEntries, 'claude-plugin');
|
||||
if (install.target === 'claude-code') {
|
||||
if (hasAnalytics) {
|
||||
lines.push(' Analytics skill installed');
|
||||
}
|
||||
if (hasAdmin) {
|
||||
lines.push(' Admin CLI skill installed');
|
||||
}
|
||||
} else if (hasAnalytics || hasAdmin || hasPlugin) {
|
||||
lines.push(` ${guidanceInstallLine(install.target)}`);
|
||||
}
|
||||
if (hasEntryRole(targetEntries, 'launcher')) {
|
||||
lines.push(' Starts KTX over stdio from Claude Desktop');
|
||||
}
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function humanList(values: string[]): string {
|
||||
if (values.length <= 2) {
|
||||
return values.join(' and ');
|
||||
}
|
||||
return `${values.slice(0, -1).join(', ')}, and ${values[values.length - 1]}`;
|
||||
}
|
||||
|
||||
function pushBlankLine(lines: string[]): void {
|
||||
if (lines.length > 0 && lines[lines.length - 1] !== '') {
|
||||
lines.push('');
|
||||
}
|
||||
}
|
||||
|
||||
function trimTrailingBlankLines(lines: string[]): void {
|
||||
while (lines[lines.length - 1] === '') {
|
||||
lines.pop();
|
||||
}
|
||||
}
|
||||
|
||||
function manualActionFromSnippet(snippet: string): {
|
||||
title: string;
|
||||
instruction: string;
|
||||
marker: 'PASTE' | 'USE';
|
||||
body: string[];
|
||||
} {
|
||||
const [label = '', ...body] = snippet.split('\n');
|
||||
const codexPrefix = 'Add this Codex MCP snippet to ~/.codex/config.toml:';
|
||||
if (label === codexPrefix) {
|
||||
return {
|
||||
title: 'Configure Codex',
|
||||
instruction: 'Open ~/.codex/config.toml, then paste this block:',
|
||||
marker: 'PASTE',
|
||||
body,
|
||||
};
|
||||
}
|
||||
|
||||
const opencodeMatch = label.match(/^Add this OpenCode MCP snippet to (.+):$/);
|
||||
if (opencodeMatch) {
|
||||
return {
|
||||
title: 'Configure OpenCode',
|
||||
instruction: `Open ${opencodeMatch[1]}, then paste this block:`,
|
||||
marker: 'PASTE',
|
||||
body,
|
||||
};
|
||||
}
|
||||
|
||||
if (label === 'Use this universal MCP endpoint with unsupported MCP clients:') {
|
||||
return {
|
||||
title: 'Configure unsupported MCP clients',
|
||||
instruction: 'Use this endpoint when setting up unsupported MCP clients:',
|
||||
marker: 'USE',
|
||||
body,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: 'Configure MCP client',
|
||||
instruction: label,
|
||||
marker: 'PASTE',
|
||||
body,
|
||||
};
|
||||
}
|
||||
|
||||
function formatAgentNextActions(input: {
|
||||
projectDir: string;
|
||||
installs: Array<{ target: KtxAgentTarget; scope: KtxAgentScope; mode: KtxAgentInstallMode }>;
|
||||
notices: string[];
|
||||
snippets: string[];
|
||||
}): string {
|
||||
const projectDir = resolve(input.projectDir);
|
||||
const lines: string[] = [];
|
||||
let step = 1;
|
||||
|
||||
for (const snippet of input.snippets) {
|
||||
const action = manualActionFromSnippet(snippet);
|
||||
lines.push(`${step}. ${action.title}`);
|
||||
lines.push(` ${action.instruction}`);
|
||||
if (action.body.length > 0) {
|
||||
lines.push('', ` ${action.marker}:`);
|
||||
}
|
||||
for (const line of action.body) {
|
||||
lines.push(` ${line}`);
|
||||
}
|
||||
pushBlankLine(lines);
|
||||
step += 1;
|
||||
}
|
||||
|
||||
const httpTargets = input.installs
|
||||
.filter((install) => targetUsesHttpMcpDaemon(install.target))
|
||||
.map((install) => targetDisplayName(install.target));
|
||||
if (input.notices.length > 0 && httpTargets.length > 0) {
|
||||
lines.push(`${step}. Start MCP`);
|
||||
lines.push(` Run this command before using ${humanList(httpTargets)}:`);
|
||||
lines.push('');
|
||||
lines.push(' RUN:');
|
||||
lines.push(` ktx mcp start --project-dir ${projectDir}`);
|
||||
lines.push('');
|
||||
lines.push(' If you need to stop MCP later:');
|
||||
lines.push(` ktx mcp stop --project-dir ${projectDir}`);
|
||||
pushBlankLine(lines);
|
||||
step += 1;
|
||||
}
|
||||
|
||||
const claudeCodeInstall = input.installs.find((install) => install.target === 'claude-code');
|
||||
if (claudeCodeInstall) {
|
||||
lines.push(`${step}. Open Claude Code`);
|
||||
if (claudeCodeInstall.scope === 'project') {
|
||||
lines.push(' Open Claude Code from the KTX project directory:');
|
||||
lines.push('');
|
||||
lines.push(' RUN:');
|
||||
lines.push(` cd ${shellScriptQuote(projectDir)}`);
|
||||
lines.push(' claude');
|
||||
} else {
|
||||
lines.push(' RUN:');
|
||||
lines.push(' claude');
|
||||
}
|
||||
pushBlankLine(lines);
|
||||
step += 1;
|
||||
}
|
||||
|
||||
const cursorInstall = input.installs.find((install) => install.target === 'cursor');
|
||||
if (cursorInstall) {
|
||||
lines.push(`${step}. Open Cursor`);
|
||||
if (cursorInstall.scope === 'project') {
|
||||
lines.push(' Open Cursor from the KTX project directory:');
|
||||
lines.push('');
|
||||
lines.push(' OPEN:');
|
||||
lines.push(` ${projectDir}`);
|
||||
} else {
|
||||
lines.push(' Open Cursor.');
|
||||
}
|
||||
pushBlankLine(lines);
|
||||
step += 1;
|
||||
}
|
||||
|
||||
if (input.installs.some((install) => install.target === 'claude-desktop')) {
|
||||
lines.push(`${step}. Restart Claude Desktop`);
|
||||
lines.push(' Claude Desktop loads KTX after restart.');
|
||||
pushBlankLine(lines);
|
||||
step += 1;
|
||||
}
|
||||
|
||||
if (lines.length === 0) {
|
||||
lines.push('Open your configured agent and ask a data question.');
|
||||
}
|
||||
|
||||
trimTrailingBlankLines(lines);
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
async function installTarget(input: {
|
||||
projectDir: string;
|
||||
target: KtxAgentTarget;
|
||||
|
|
@ -870,10 +1053,18 @@ export async function runKtxSetupAgentsStep(
|
|||
args.inputMode === 'disabled'
|
||||
? args.mode
|
||||
: ((await prompts.select({
|
||||
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.',
|
||||
},
|
||||
],
|
||||
})) as KtxAgentInstallMode | 'back');
|
||||
if (mode === 'back') return { status: 'skipped', projectDir: args.projectDir };
|
||||
|
|
@ -908,10 +1099,18 @@ export async function runKtxSetupAgentsStep(
|
|||
scopeTargets.length > 0 &&
|
||||
scopeTargets.every(targetSupportsGlobalScope)
|
||||
? ((await prompts.select({
|
||||
message: 'Where should KTX install supported agent config?',
|
||||
message: `Where should KTX install supported agent config?\n\nKTX project: ${resolve(args.projectDir)}`,
|
||||
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.',
|
||||
},
|
||||
],
|
||||
})) as KtxAgentScope | 'back')
|
||||
: args.scope;
|
||||
|
|
@ -921,7 +1120,6 @@ export async function runKtxSetupAgentsStep(
|
|||
const entries: InstallEntry[] = [];
|
||||
const snippets: string[] = [];
|
||||
const notices = new Set<string>();
|
||||
let claudeDesktopTutorial: string | undefined;
|
||||
try {
|
||||
for (const install of installs) {
|
||||
const targetEntries = await installTarget({ projectDir: args.projectDir, ...install });
|
||||
|
|
@ -934,25 +1132,6 @@ export async function runKtxSetupAgentsStep(
|
|||
entries.push(...mcpResult.entries);
|
||||
for (const snippet of mcpResult.snippets) snippets.push(snippet);
|
||||
for (const notice of mcpResult.notices) notices.add(notice);
|
||||
if (install.target === 'claude-desktop') {
|
||||
const pluginEntry = targetEntries.find(
|
||||
(entry): entry is Extract<InstallEntry, { kind: 'file' }> =>
|
||||
entry.kind === 'file' && entry.role === 'claude-plugin',
|
||||
);
|
||||
const pluginPath = pluginEntry?.path ?? '';
|
||||
const configPath = claudeDesktopConfigPath().path;
|
||||
claudeDesktopTutorial = [
|
||||
`${green('✓')} ${bold('KTX MCP server registered')}`,
|
||||
` ${dim(configPath)}`,
|
||||
'',
|
||||
bold('1. Restart Claude Desktop'),
|
||||
' Quit and reopen so it picks up the new MCP server.',
|
||||
'',
|
||||
bold('2. Install the KTX plugin'),
|
||||
' Open Claude Desktop → Settings → Plugins and install from file:',
|
||||
` 📦 ${dim(pluginPath)}`,
|
||||
].join('\n');
|
||||
}
|
||||
}
|
||||
await writeManifest(
|
||||
args.projectDir,
|
||||
|
|
@ -965,18 +1144,16 @@ export async function runKtxSetupAgentsStep(
|
|||
'Agent integration complete',
|
||||
io,
|
||||
);
|
||||
if (claudeDesktopTutorial) {
|
||||
setupUi.note(claudeDesktopTutorial, 'Finish Claude Desktop setup', io, {
|
||||
format: (line) => line,
|
||||
});
|
||||
const nextActions = formatAgentNextActions({
|
||||
projectDir: args.projectDir,
|
||||
installs,
|
||||
notices: [...notices],
|
||||
snippets,
|
||||
});
|
||||
if (args.showNextActions !== false) {
|
||||
setupUi.note(nextActions, 'Required before using agents', io, { format: (line) => line });
|
||||
}
|
||||
const nextStepBlocks: string[] = [];
|
||||
for (const notice of notices) nextStepBlocks.push(notice);
|
||||
for (const snippet of snippets) nextStepBlocks.push(snippet);
|
||||
if (nextStepBlocks.length > 0) {
|
||||
setupUi.note(nextStepBlocks.join('\n\n'), 'Next steps', io, { format: bold });
|
||||
}
|
||||
return { status: 'ready', projectDir: args.projectDir, installs };
|
||||
return { status: 'ready', projectDir: args.projectDir, installs, nextActions };
|
||||
} catch (error) {
|
||||
io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
||||
return { status: 'failed', projectDir: args.projectDir };
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|||
import { localFakeBundleReport, persistLocalBundleReport } from './ingest.test-utils.js';
|
||||
import { contextBuildCommands, writeKtxSetupContextState } from './setup-context.js';
|
||||
import { runDemoTour } from './setup-demo-tour.js';
|
||||
import { formatKtxSetupStatus, readKtxSetupStatus, runKtxSetup } from './setup.js';
|
||||
import { formatKtxSetupCompletionSummary, formatKtxSetupStatus, readKtxSetupStatus, runKtxSetup } from './setup.js';
|
||||
|
||||
vi.mock('./setup-demo-tour.js', () => ({
|
||||
runDemoTour: vi.fn(async () => 0),
|
||||
|
|
@ -424,6 +424,108 @@ describe('setup status', () => {
|
|||
expect(rendered).not.toContain('No KTX project found.');
|
||||
});
|
||||
|
||||
it('formats a concise ready summary for completed agent setup', () => {
|
||||
const rendered = formatKtxSetupCompletionSummary(
|
||||
{
|
||||
project: { path: tempDir, ready: true },
|
||||
llm: { ready: true, model: 'sonnet' },
|
||||
embeddings: { ready: true, model: 'text-embedding-3-small' },
|
||||
databases: [{ connectionId: 'postgres-warehouse', ready: true }],
|
||||
sources: [{ connectionId: 'dbt-main', type: 'dbt', ready: true }],
|
||||
runtime: { required: true, ready: true, features: ['core'] },
|
||||
context: { ready: true, status: 'completed' },
|
||||
agents: [
|
||||
{ target: 'claude-code', scope: 'project', ready: true },
|
||||
{ target: 'claude-desktop', scope: 'global', ready: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
agentNextActions: [
|
||||
'1. Start MCP',
|
||||
' Run this command before using Claude Code:',
|
||||
'',
|
||||
' RUN:',
|
||||
` ktx mcp start --project-dir ${tempDir}`,
|
||||
'',
|
||||
' If you need to stop MCP later:',
|
||||
` ktx mcp stop --project-dir ${tempDir}`,
|
||||
'',
|
||||
'2. Open Claude Code',
|
||||
' Open Claude Code from the KTX project directory:',
|
||||
'',
|
||||
' RUN:',
|
||||
` cd '${tempDir}'`,
|
||||
' claude',
|
||||
].join('\n'),
|
||||
},
|
||||
);
|
||||
|
||||
expect(rendered).toContain(`Project\n ${tempDir}`);
|
||||
expect(rendered).toContain('Context\n built');
|
||||
expect(rendered).toContain('Agents configured\n Claude Code, Claude Desktop');
|
||||
expect(rendered).toContain('REQUIRED BEFORE USING AGENTS\n\n 1. Start MCP');
|
||||
expect(rendered).toContain(' Run this command before using Claude Code:');
|
||||
expect(rendered).toContain(' RUN:');
|
||||
expect(rendered).toContain(' If you need to stop MCP later:');
|
||||
expect(rendered).toContain(`ktx mcp stop --project-dir ${tempDir}`);
|
||||
expect(rendered).toContain('After that, try\n Ask your agent: "Use KTX to show me the available tables."');
|
||||
expect(rendered).not.toContain('Verify');
|
||||
expect(rendered).not.toContain('Project ready: yes');
|
||||
expect(rendered).not.toContain('What you can do next');
|
||||
});
|
||||
|
||||
it('prints agent next actions inside the final ready summary during full setup', async () => {
|
||||
const testIo = makeIo();
|
||||
|
||||
await expect(
|
||||
runKtxSetup(
|
||||
{
|
||||
command: 'run',
|
||||
projectDir: tempDir,
|
||||
mode: 'new',
|
||||
agents: false,
|
||||
target: 'claude-code',
|
||||
skipAgents: false,
|
||||
inputMode: 'disabled',
|
||||
yes: true,
|
||||
cliVersion: '0.2.0',
|
||||
skipLlm: true,
|
||||
skipEmbeddings: true,
|
||||
skipDatabases: true,
|
||||
skipSources: true,
|
||||
databaseSchemas: [],
|
||||
},
|
||||
testIo.io,
|
||||
{
|
||||
runtime: async () => runtimeReady(tempDir),
|
||||
context: async () => {
|
||||
await writeKtxSetupContextState(tempDir, {
|
||||
runId: 'setup-context-local-test',
|
||||
status: 'completed',
|
||||
primarySourceConnectionIds: [],
|
||||
contextSourceConnectionIds: [],
|
||||
reportIds: [],
|
||||
artifactPaths: [],
|
||||
retryableFailedTargets: [],
|
||||
commands: contextBuildCommands(tempDir, 'setup-context-local-test'),
|
||||
});
|
||||
await writeKtxSetupState(tempDir, { completed_steps: ['project', 'context'] });
|
||||
return { status: 'ready', projectDir: tempDir, runId: 'setup-context-local-test' };
|
||||
},
|
||||
},
|
||||
),
|
||||
).resolves.toBe(0);
|
||||
|
||||
const output = testIo.stdout();
|
||||
expect(output).toContain('Agent integration complete');
|
||||
expect(output).toContain('Finish KTX agent setup');
|
||||
expect(output).not.toContain('KTX project ready');
|
||||
expect(output).toContain('REQUIRED BEFORE USING AGENTS');
|
||||
expect(output).toContain('Run this command before using Claude Code:');
|
||||
expect(output).toContain(`ktx mcp start --project-dir ${tempDir}`);
|
||||
expect(output).not.toContain('Finish agent setup');
|
||||
});
|
||||
|
||||
it('prints the setup shell intro for auto-created run mode', async () => {
|
||||
const testIo = makeIo();
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
type KtxSetupAgentsDeps,
|
||||
readKtxAgentInstallManifest,
|
||||
runKtxSetupAgentsStep,
|
||||
targetDisplayName,
|
||||
} from './setup-agents.js';
|
||||
import {
|
||||
type KtxSetupDatabaseDriver,
|
||||
|
|
@ -435,6 +436,35 @@ export function formatKtxSetupStatus(status: KtxSetupStatus): string {
|
|||
return `${lines.join('\n')}\n`;
|
||||
}
|
||||
|
||||
export function formatKtxSetupCompletionSummary(
|
||||
status: KtxSetupStatus,
|
||||
options: { agentNextActions?: string } = {},
|
||||
): string {
|
||||
const readyAgents = status.agents.filter((agent) => agent.ready).map((agent) => targetDisplayName(agent.target));
|
||||
const lines = [
|
||||
'Project',
|
||||
` ${status.project.path}`,
|
||||
'',
|
||||
'Context',
|
||||
` ${status.context.ready ? 'built' : formatContextBuilt(status.context)}`,
|
||||
'',
|
||||
'Agents configured',
|
||||
` ${readyAgents.length > 0 ? readyAgents.join(', ') : 'not installed'}`,
|
||||
];
|
||||
const agentNextActions = options.agentNextActions?.trim();
|
||||
if (agentNextActions) {
|
||||
lines.push(
|
||||
'',
|
||||
'REQUIRED BEFORE USING AGENTS',
|
||||
'',
|
||||
...agentNextActions.split('\n').map((line) => (line ? ` ${line}` : '')),
|
||||
);
|
||||
}
|
||||
lines.push('', agentNextActions ? 'After that, try' : 'Try it');
|
||||
lines.push(' Ask your agent: "Use KTX to show me the available tables."');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function setupStatusReady(status: KtxSetupStatus): boolean {
|
||||
if (!status.project.ready) {
|
||||
return false;
|
||||
|
|
@ -459,6 +489,10 @@ function setupContextReady(status: KtxSetupStatus): boolean {
|
|||
return status.context.ready;
|
||||
}
|
||||
|
||||
function shouldPrintConciseReadySummary(status: KtxSetupStatus): boolean {
|
||||
return setupStatusReady(status) && setupContextReady(status) && status.agents.some((agent) => agent.ready);
|
||||
}
|
||||
|
||||
function writeContextNotReadyForAgents(projectDir: string, io: KtxCliIo): void {
|
||||
io.stderr.write('KTX context is not ready for agents.\n\n');
|
||||
io.stderr.write(`Build context first:\n ktx setup --project-dir ${resolve(projectDir)}\n\n`);
|
||||
|
|
@ -493,6 +527,7 @@ async function runKtxSetupInner(args: KtxSetupArgs, io: KtxCliIo, deps: KtxSetup
|
|||
setupUi.intro('KTX setup', io);
|
||||
let entryAction: KtxSetupEntryAction | undefined;
|
||||
let projectResult: Awaited<ReturnType<typeof runKtxSetupProjectStep>>;
|
||||
let agentNextActions: string | undefined;
|
||||
const canShowEntryMenu =
|
||||
args.showEntryMenu === true &&
|
||||
args.inputMode !== 'disabled' &&
|
||||
|
|
@ -724,7 +759,7 @@ async function runKtxSetupInner(args: KtxSetupArgs, io: KtxCliIo, deps: KtxSetup
|
|||
} else {
|
||||
const agentsRunner =
|
||||
deps.agents ?? ((agentArgs, agentIo) => runKtxSetupAgentsStep(agentArgs, agentIo, deps.agentsDeps));
|
||||
stepResult = await agentsRunner(
|
||||
const agentResult = await agentsRunner(
|
||||
{
|
||||
projectDir: projectResult.projectDir,
|
||||
inputMode: args.inputMode,
|
||||
|
|
@ -734,9 +769,14 @@ async function runKtxSetupInner(args: KtxSetupArgs, io: KtxCliIo, deps: KtxSetup
|
|||
scope: args.agentScope ?? 'project',
|
||||
mode: 'mcp',
|
||||
skipAgents: false,
|
||||
showNextActions: agentsRequested,
|
||||
},
|
||||
io,
|
||||
);
|
||||
stepResult = agentResult;
|
||||
if (agentResult.status === 'ready') {
|
||||
agentNextActions = agentResult.nextActions;
|
||||
}
|
||||
}
|
||||
|
||||
if (stepResult.status === 'failed' || stepResult.status === 'missing-input') {
|
||||
|
|
@ -779,19 +819,30 @@ async function runKtxSetupInner(args: KtxSetupArgs, io: KtxCliIo, deps: KtxSetup
|
|||
const status = await readKtxSetupStatus(projectResult.projectDir, { cliVersion: args.cliVersion });
|
||||
const focusedOnAgents = args.agents || entryAction === 'agents';
|
||||
if (!focusedOnAgents) {
|
||||
setupUi.note(formatKtxSetupStatus(status).trimEnd(), 'Project status', io, {
|
||||
format: (line) => line,
|
||||
});
|
||||
setupUi.note(
|
||||
formatSetupNextStepLines({
|
||||
setupReady: setupStatusReady(status),
|
||||
hasContextTargets: setupHasContextTargets(status),
|
||||
contextReady: setupContextReady(status),
|
||||
agentIntegrationReady: status.agents.some((agent) => agent.ready),
|
||||
}).join('\n'),
|
||||
'What you can do next',
|
||||
io,
|
||||
);
|
||||
if (shouldPrintConciseReadySummary(status)) {
|
||||
setupUi.note(
|
||||
formatKtxSetupCompletionSummary(status, { agentNextActions }),
|
||||
agentNextActions ? 'Finish KTX agent setup' : 'KTX project ready',
|
||||
io,
|
||||
{
|
||||
format: (line) => line,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
setupUi.note(formatKtxSetupStatus(status).trimEnd(), 'Project status', io, {
|
||||
format: (line) => line,
|
||||
});
|
||||
setupUi.note(
|
||||
formatSetupNextStepLines({
|
||||
setupReady: setupStatusReady(status),
|
||||
hasContextTargets: setupHasContextTargets(status),
|
||||
contextReady: setupContextReady(status),
|
||||
agentIntegrationReady: status.agents.some((agent) => agent.ready),
|
||||
}).join('\n'),
|
||||
'What you can do next',
|
||||
io,
|
||||
);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue