Skip to content
Merged
Show file tree
Hide file tree
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
48 changes: 46 additions & 2 deletions src/agents/detect.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@ const registry = require('./registry');
const { run } = require('../core/runtime');

function registryEntries() {
if (typeof registry.getAgentDefinitions === 'function') {
const definitions = registry.getAgentDefinitions();
if (Array.isArray(definitions)) {
return definitions;
}
}

if (Array.isArray(registry.AGENT_IDS) && typeof registry.getAgentDefinition === 'function') {
return registry.AGENT_IDS
.map((agentId) => registry.getAgentDefinition(agentId))
.filter((entry) => entry && typeof entry === 'object');
}

const source =
registry.agents ||
registry.AGENTS ||
Expand All @@ -24,6 +37,20 @@ function registryEntries() {
}

function findAgent(agentId) {
if (typeof registry.getAgentDefinition === 'function') {
const entry = registry.getAgentDefinition(agentId);
if (entry) return entry;
}

if (typeof registry.resolveAgent === 'function') {
try {
const entry = registry.resolveAgent(agentId);
if (entry) return entry;
} catch (_error) {
return null;
}
}

if (typeof registry.getAgent === 'function') {
const entry = registry.getAgent(agentId);
if (entry) return entry;
Expand All @@ -32,6 +59,21 @@ function findAgent(agentId) {
return registryEntries().find((entry) => entry.id === agentId);
}

function registryAgentIds() {
if (Array.isArray(registry.AGENT_IDS)) {
return [...registry.AGENT_IDS];
}

if (typeof registry.getAgentDefinitions === 'function') {
const definitions = registry.getAgentDefinitions();
if (Array.isArray(definitions)) {
return definitions.map((entry) => entry && entry.id).filter(Boolean);
}
}

return registryEntries().map((entry) => entry.id).filter(Boolean);
}

function normalizeDetectCommand(detectCommand) {
if (Array.isArray(detectCommand)) {
const [cmd, ...args] = detectCommand;
Expand Down Expand Up @@ -84,7 +126,9 @@ function detectionResult(entry, available, command, error = null) {
function detectAgent(agentId) {
const entry = findAgent(agentId);
if (!entry) {
return detectionResult({ id: agentId, label: agentId }, false, null, `unknown agent: ${agentId}`);
const known = registryAgentIds();
const suffix = known.length > 0 ? ` (known agents: ${known.join(', ')})` : '';
return detectionResult({ id: agentId, label: agentId }, false, null, `unknown agent: ${agentId}${suffix}`);
}

const { cmd, args, command } = normalizeDetectCommand(entry.detectCommand);
Expand All @@ -101,7 +145,7 @@ function detectAgent(agentId) {
}

function detectAgents(agentIds) {
const ids = Array.isArray(agentIds) ? agentIds : registryEntries().map((entry) => entry.id);
const ids = Array.isArray(agentIds) ? agentIds : registryAgentIds();
return ids.map((agentId) => detectAgent(agentId));
}

Expand Down
93 changes: 77 additions & 16 deletions test/agents-detect.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,14 @@ function withMockedDetection({ registry, run }, fn) {
}
}

test('detectAgent reports an available registered agent without launching it', () => {
test('detectAgent uses getAgentDefinition for codex registry entries', () => {
const calls = [];
const registry = {
agents: [
{ id: 'codex', label: 'Codex', detectCommand: ['codex', '--version'] },
{ id: 'claude', label: 'Claude', detectCommand: ['claude', '--version'] },
],
getAgentDefinition: (agentId) => (
agentId === 'codex'
? { id: 'codex', label: 'Codex', detectCommand: ['codex', '--version'] }
: null
),
};

withMockedDetection(
Expand All @@ -69,11 +70,41 @@ test('detectAgent reports an available registered agent without launching it', (
]);
});

test('detectAgent uses resolveAgent for claude registry entries', () => {
const registry = {
getAgentDefinition: () => null,
resolveAgent: (agentId) => {
if (agentId === 'claude') {
return { id: 'claude', label: 'Claude Code', detectCommand: 'claude --version' };
}
throw new Error(`Unknown agent id: ${agentId}`);
},
};

withMockedDetection(
{
registry,
run: () => ({ status: 0, stdout: 'claude 1.2.3\n', stderr: '' }),
},
({ detectAgent }) => {
assert.deepEqual(detectAgent('claude'), {
id: 'claude',
label: 'Claude Code',
available: true,
command: 'claude --version',
error: null,
});
},
);
});

test('detectAgent reports command failures as unavailable with error text', () => {
const registry = {
agents: [
{ id: 'claude', label: 'Claude', detectCommand: { command: 'claude', args: ['--version'] } },
],
getAgentDefinition: (agentId) => (
agentId === 'claude'
? { id: 'claude', label: 'Claude', detectCommand: { command: 'claude', args: ['--version'] } }
: null
),
};

withMockedDetection(
Expand All @@ -93,20 +124,24 @@ test('detectAgent reports command failures as unavailable with error text', () =
);
});

test('detectAgents preserves requested order and supports registry maps', () => {
test('detectAgents with no args returns all registry agents', () => {
const registry = {
codex: { label: 'Codex', detectCommand: 'codex --version' },
gemini: { label: 'Gemini', detectCommand: ['gemini', '--version'] },
AGENT_IDS: ['codex', 'claude', 'gemini'],
getAgentDefinition: (agentId) => ({
codex: { id: 'codex', label: 'Codex', detectCommand: 'codex --version' },
claude: { id: 'claude', label: 'Claude', detectCommand: 'claude --version' },
gemini: { id: 'gemini', label: 'Gemini', detectCommand: 'gemini --version' },
}[agentId]),
};

withMockedDetection(
{
registry,
run: (cmd) => ({ status: cmd === 'gemini' ? 1 : 0, stdout: '', stderr: '' }),
run: () => ({ status: 0, stdout: '', stderr: '' }),
},
({ detectAgents }) => {
assert.deepEqual(detectAgents(['gemini', 'codex']).map((agent) => agent.id), ['gemini', 'codex']);
assert.deepEqual(detectAgents(['gemini', 'codex']).map((agent) => agent.available), [false, true]);
assert.deepEqual(detectAgents().map((agent) => agent.id), ['codex', 'claude', 'gemini']);
assert.deepEqual(detectAgents().map((agent) => agent.available), [true, true, true]);
},
);
});
Expand Down Expand Up @@ -144,7 +179,13 @@ test('detectAgent reports unknown agents without running commands', () => {

withMockedDetection(
{
registry: { agents: [] },
registry: {
AGENT_IDS: ['codex', 'claude'],
getAgentDefinition: () => null,
resolveAgent: () => {
throw new Error('unknown');
},
},
run: () => {
callCount += 1;
return { status: 0, stdout: '', stderr: '' };
Expand All @@ -156,10 +197,30 @@ test('detectAgent reports unknown agents without running commands', () => {
label: 'ghost',
available: false,
command: null,
error: 'unknown agent: ghost',
error: 'unknown agent: ghost (known agents: codex, claude)',
});
},
);

assert.equal(callCount, 0);
});

test('detectAgent reports successful mocked commands as available', () => {
const registry = {
getAgentDefinition: (agentId) => (
agentId === 'codex'
? { id: 'codex', label: 'Codex', detectCommand: 'codex --version' }
: null
),
};

withMockedDetection(
{
registry,
run: () => ({ status: 0, stdout: 'codex 1.2.3\n', stderr: '' }),
},
({ detectAgent }) => {
assert.equal(detectAgent('codex').available, true);
},
);
});
Loading