Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 90 additions & 5 deletions packages/cli/src/commands/agents.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,73 @@
import { Command } from 'commander';
import kleur from 'kleur';
import { exec } from '@profullstack/sh1pt-core';

interface AgentDef {
id: string;
binary: string;
versionArgs: string[];
installHint: string;
authHint: string;
}

const KNOWN_AGENTS: AgentDef[] = [
{
id: 'claude',
binary: 'claude',
versionArgs: ['--version'],
installHint: 'mise use npm:@anthropic-ai/claude-code',
authHint: 'run `claude /login`',
},
{
id: 'codex',
binary: 'codex',
versionArgs: ['--version'],
installHint: 'npm install -g @openai/codex',
authHint: 'set OPENAI_API_KEY env var',
},
{
id: 'qwen',
binary: 'qwen',
versionArgs: ['--version'],
installHint: 'pip install qwen-agent',
authHint: 'set DASHSCOPE_API_KEY env var',
},
];

interface AgentStatus {
id: string;
installed: boolean;
version?: string;
installHint: string;
authHint: string;
}

async function checkAgent(agent: AgentDef): Promise<AgentStatus> {
const noop = () => {};
try {
const result = await exec(agent.binary, agent.versionArgs, {
log: noop,
throwOnNonZero: false,
});
if (result.exitCode === 0) {
return {
id: agent.id,
installed: true,
version: result.stdout.trim() || undefined,
installHint: agent.installHint,
authHint: agent.authHint,
};
Comment on lines +53 to +59
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 The v prefix is prepended to the raw, un-parsed stdout of --version. In practice claude --version outputs something like "Claude Code 1.2.3" (the full tool name), so the displayed string becomes vClaude Code 1.2.3 — not the clean v1.2.3 the PR description promises. The JSON output has the same issue: version contains the entire first-line dump rather than just the semver. A simple regex extracts the version number regardless of the prefix the CLI adds.

Suggested change
return {
id: agent.id,
installed: true,
version: result.stdout.trim() || undefined,
installHint: agent.installHint,
authHint: agent.authHint,
};
const rawOut = result.stdout.trim() || result.stderr.trim();
const semverMatch = rawOut.match(/\d+\.\d+\.\d+[\w.-]*/);
return {
id: agent.id,
installed: true,
version: semverMatch ? semverMatch[0] : rawOut || undefined,
installHint: agent.installHint,
authHint: agent.authHint,
};

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

}
} catch {
// binary not found
}
Comment on lines +52 to +63
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 When a binary exists on PATH but --version exits with a non-zero code, the function falls through the if (result.exitCode === 0) guard and returns installed: false. A handful of real CLIs (including some versions of codex) exit non-zero for --version. Treating any successful resolution (i.e., the process started at all) as installed would be more robust, since ENOENT is already handled by the outer catch.

Suggested change
if (result.exitCode === 0) {
return {
id: agent.id,
installed: true,
version: result.stdout.trim() || undefined,
installHint: agent.installHint,
authHint: agent.authHint,
};
}
} catch {
// binary not found
}
// Binary was found (spawn succeeded); non-zero exit for --version is still "installed".
const rawOut = (result.stdout.trim() || result.stderr.trim());
const semverMatch = rawOut.match(/\d+\.\d+\.\d+[\w.-]*/);
return {
id: agent.id,
installed: true,
version: semverMatch ? semverMatch[0] : rawOut || undefined,
installHint: agent.installHint,
authHint: agent.authHint,
};
} catch {
// binary not found (ENOENT) or other spawn error
}

return {
id: agent.id,
installed: false,
installHint: agent.installHint,
authHint: agent.authHint,
};
}

export const agentsCmd = new Command('agents')
.description('Orchestrate AI coding CLIs (Claude Code, Codex, Qwen) — generate, edit, and talk')
Expand All @@ -10,12 +78,29 @@ export const agentsCmd = new Command('agents')
agentsCmd
.command('list')
.description('Which agent CLIs are installed on this machine')
.action(() => {
const agents = ['claude', 'codex', 'qwen'];
for (const a of agents) {
// TODO: resolve adapter, call check(), render real status
console.log(` ${kleur.gray('○')} ${kleur.bold(a)} ${kleur.dim('run `sh1pt agents setup --agent ' + a + '`')}`);
.option('--json', 'output as JSON')
.action(async (opts: { json?: boolean }) => {
const results = await Promise.all(KNOWN_AGENTS.map(checkAgent));

if (opts.json) {
console.log(JSON.stringify(results, null, 2));
return;
}

for (const r of results) {
if (r.installed) {
const ver = r.version ? kleur.dim(` v${r.version}`) : '';
console.log(` ${kleur.green('●')} ${kleur.bold(r.id)}${ver}`);
} else {
console.log(
` ${kleur.gray('○')} ${kleur.bold(r.id)} ${kleur.dim('not installed — ' + r.installHint)}`
);
}
}

const installed = results.filter((r) => r.installed).length;
console.log('');
console.log(kleur.dim(`${installed}/${results.length} agent(s) installed. Run \`sh1pt agents setup\` to install missing agents.`));
});

agentsCmd
Expand Down