mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-07 07:55:13 +02:00
pin agent skill and MCP configs to absolute CLI path
Generated agent skill files and MCP configs previously assumed `ktx` was on PATH. This resolves the CLI entry point via `import.meta.url` and `process.execPath` so agents work without any PATH setup. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d89be2390f
commit
1a47297e43
3 changed files with 107 additions and 15 deletions
|
|
@ -59,7 +59,7 @@ function commandLines(commands: ReadonlyArray<{ command: string; description: st
|
|||
export function formatNextStepLines(indent = ' '): string[] {
|
||||
return [
|
||||
`${indent}KTX context is ready for agents.`,
|
||||
`${indent}Preferred route: CLI + Skills; installed rules call \`ktx agent ...\` directly, so no MCP server is required.`,
|
||||
`${indent}Preferred route: CLI + Skills; installed rules call the pinned local CLI directly, so no MCP server is required.`,
|
||||
`${indent}Direct CLI checks:`,
|
||||
...commandLines(KTX_NEXT_STEP_DIRECT_COMMANDS, indent),
|
||||
`${indent}Optional MCP:`,
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ describe('setup agents', () => {
|
|||
const skill = await readFile(join(tempDir, '.agents/skills/ktx/SKILL.md'), 'utf-8');
|
||||
expect(skill).toContain(`--project-dir ${tempDir}`);
|
||||
expect(skill).toContain('must not print secrets');
|
||||
expect(skill).toContain('ktx agent sql execute');
|
||||
expect(skill).toContain('agent sql execute');
|
||||
expect(await readKtxAgentInstallManifest(tempDir)).toMatchObject({
|
||||
version: 1,
|
||||
projectDir: tempDir,
|
||||
|
|
@ -93,6 +93,47 @@ describe('setup agents', () => {
|
|||
expect(io.stderr()).toBe('');
|
||||
});
|
||||
|
||||
it('writes PATH-independent launcher commands for skills and MCP configs', async () => {
|
||||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKtxSetupAgentsStep(
|
||||
{
|
||||
projectDir: tempDir,
|
||||
inputMode: 'disabled',
|
||||
yes: true,
|
||||
agents: true,
|
||||
target: 'universal',
|
||||
scope: 'project',
|
||||
mode: 'both',
|
||||
skipAgents: false,
|
||||
},
|
||||
io.io,
|
||||
),
|
||||
).resolves.toMatchObject({ status: 'ready' });
|
||||
|
||||
const skill = await readFile(join(tempDir, '.agents/skills/ktx/SKILL.md'), 'utf-8');
|
||||
expect(skill).not.toContain('`ktx agent');
|
||||
expect(skill).toContain('agent context --json');
|
||||
expect(skill).toContain('agent sql execute');
|
||||
|
||||
const mcp = JSON.parse(await readFile(join(tempDir, '.agents/mcp/ktx.json'), 'utf-8')) as {
|
||||
mcpServers?: { ktx?: { command?: string; args?: string[] } };
|
||||
};
|
||||
expect(mcp.mcpServers?.ktx?.command).toBe(process.execPath);
|
||||
expect(mcp.mcpServers?.ktx?.args?.[0]).toMatch(/packages\/cli\/(src|dist)\/bin\.(ts|js)$/);
|
||||
expect(mcp.mcpServers?.ktx?.args).toEqual([
|
||||
expect.stringMatching(/packages\/cli\/(src|dist)\/bin\.(ts|js)$/),
|
||||
'--project-dir',
|
||||
tempDir,
|
||||
'serve',
|
||||
'--mcp',
|
||||
'stdio',
|
||||
'--semantic-compute',
|
||||
'--execute-queries',
|
||||
]);
|
||||
});
|
||||
|
||||
it('removes only manifest-listed files and JSON keys', async () => {
|
||||
const io = makeIo();
|
||||
await runKtxSetupAgentsStep(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { cancel, isCancel, multiselect, select } from '@clack/prompts';
|
||||
import { loadKtxProject, markKtxSetupStepComplete, serializeKtxProjectConfig } from '@ktx/context/project';
|
||||
import type { KtxCliIo } from './cli-runtime.js';
|
||||
|
|
@ -42,6 +43,11 @@ export interface KtxAgentInstallManifest {
|
|||
|
||||
type InstallEntry = KtxAgentInstallManifest['entries'][number];
|
||||
|
||||
interface KtxCliLauncher {
|
||||
command: string;
|
||||
args: string[];
|
||||
}
|
||||
|
||||
export function agentInstallManifestPath(projectDir: string): string {
|
||||
return join(resolve(projectDir), '.ktx/agents/install-manifest.json');
|
||||
}
|
||||
|
|
@ -85,7 +91,26 @@ export function plannedKtxAgentFiles(input: {
|
|||
].filter((entry): entry is InstallEntry => entry !== undefined);
|
||||
}
|
||||
|
||||
function cliInstructionContent(input: { projectDir: string; target: KtxAgentTarget }): string {
|
||||
function ktxCliLauncher(): KtxCliLauncher {
|
||||
return {
|
||||
command: process.execPath,
|
||||
args: [fileURLToPath(new URL('./bin.js', import.meta.url))],
|
||||
};
|
||||
}
|
||||
|
||||
function shellQuote(value: string): string {
|
||||
if (/^[A-Za-z0-9_/:=.,@%+-]+$/.test(value)) {
|
||||
return value;
|
||||
}
|
||||
return `'${value.replaceAll("'", "'\\''")}'`;
|
||||
}
|
||||
|
||||
function ktxCommandLine(launcher: KtxCliLauncher, args: string[]): string {
|
||||
return [launcher.command, ...launcher.args, ...args].map(shellQuote).join(' ');
|
||||
}
|
||||
|
||||
function cliInstructionContent(input: { projectDir: string; launcher: KtxCliLauncher }): string {
|
||||
const projectDirArgs = ['--json', '--project-dir', input.projectDir];
|
||||
return [
|
||||
'---',
|
||||
'name: ktx',
|
||||
|
|
@ -95,28 +120,53 @@ function cliInstructionContent(input: { projectDir: string; target: KtxAgentTarg
|
|||
'# KTX Local Context',
|
||||
'',
|
||||
`Use this project with \`--project-dir ${input.projectDir}\`.`,
|
||||
'Commands are pinned to the local KTX CLI path that created this file, so agents do not need `ktx` in PATH.',
|
||||
'If the CLI path no longer exists after moving this checkout or reinstalling KTX, rerun `ktx setup --agents`.',
|
||||
'',
|
||||
'Agents must not print secrets, credential references, environment variable values, or file contents from `.ktx/secrets`.',
|
||||
'',
|
||||
'Available commands:',
|
||||
'',
|
||||
`- \`ktx agent context --json --project-dir ${input.projectDir}\``,
|
||||
`- \`ktx agent sl list --json --project-dir ${input.projectDir}\``,
|
||||
`- \`ktx agent sl read <sourceName> --json --project-dir ${input.projectDir}\``,
|
||||
`- \`ktx agent sl query --json --project-dir ${input.projectDir} --connection-id <id> --query-file <path> --execute --max-rows 100\``,
|
||||
`- \`ktx agent wiki search <query> --json --project-dir ${input.projectDir}\``,
|
||||
`- \`ktx agent wiki read <pageId> --json --project-dir ${input.projectDir}\``,
|
||||
`- \`ktx agent sql execute --json --project-dir ${input.projectDir} --connection-id <id> --sql-file <path> --max-rows 100\``,
|
||||
`- \`${ktxCommandLine(input.launcher, ['agent', 'context', ...projectDirArgs])}\``,
|
||||
`- \`${ktxCommandLine(input.launcher, ['agent', 'sl', 'list', ...projectDirArgs])}\``,
|
||||
`- \`${ktxCommandLine(input.launcher, ['agent', 'sl', 'read', '<sourceName>', ...projectDirArgs])}\``,
|
||||
`- \`${ktxCommandLine(input.launcher, [
|
||||
'agent',
|
||||
'sl',
|
||||
'query',
|
||||
...projectDirArgs,
|
||||
'--connection-id',
|
||||
'<id>',
|
||||
'--query-file',
|
||||
'<path>',
|
||||
'--execute',
|
||||
'--max-rows',
|
||||
'100',
|
||||
])}\``,
|
||||
`- \`${ktxCommandLine(input.launcher, ['agent', 'wiki', 'search', '<query>', ...projectDirArgs])}\``,
|
||||
`- \`${ktxCommandLine(input.launcher, ['agent', 'wiki', 'read', '<pageId>', ...projectDirArgs])}\``,
|
||||
`- \`${ktxCommandLine(input.launcher, [
|
||||
'agent',
|
||||
'sql',
|
||||
'execute',
|
||||
...projectDirArgs,
|
||||
'--connection-id',
|
||||
'<id>',
|
||||
'--sql-file',
|
||||
'<path>',
|
||||
'--max-rows',
|
||||
'100',
|
||||
])}\``,
|
||||
'',
|
||||
'SQL execution is read-only, requires an explicit row limit, and should use the smallest useful limit.',
|
||||
'',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function mcpConfig(projectDir: string): Record<string, unknown> {
|
||||
function mcpConfig(projectDir: string, launcher: KtxCliLauncher): Record<string, unknown> {
|
||||
return {
|
||||
command: 'ktx',
|
||||
args: ['--project-dir', projectDir, 'serve', '--mcp', 'stdio', '--semantic-compute', '--execute-queries'],
|
||||
command: launcher.command,
|
||||
args: [...launcher.args, '--project-dir', projectDir, 'serve', '--mcp', 'stdio', '--semantic-compute', '--execute-queries'],
|
||||
env: {},
|
||||
};
|
||||
}
|
||||
|
|
@ -252,12 +302,13 @@ async function installTarget(input: {
|
|||
mode: KtxAgentInstallMode;
|
||||
}): Promise<InstallEntry[]> {
|
||||
const entries = plannedKtxAgentFiles(input);
|
||||
const launcher = ktxCliLauncher();
|
||||
for (const entry of entries) {
|
||||
if (entry.kind === 'file') {
|
||||
await mkdir(dirname(entry.path), { recursive: true });
|
||||
await writeFile(entry.path, cliInstructionContent({ projectDir: input.projectDir, target: input.target }), 'utf-8');
|
||||
await writeFile(entry.path, cliInstructionContent({ projectDir: input.projectDir, launcher }), 'utf-8');
|
||||
} else {
|
||||
await writeJsonKey(entry.path, entry.jsonPath, mcpConfig(input.projectDir));
|
||||
await writeJsonKey(entry.path, entry.jsonPath, mcpConfig(input.projectDir, launcher));
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue