tlc-claude-code 1.4.0 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/dist/App.js +229 -35
- package/dashboard/dist/components/AgentRegistryPane.d.ts +35 -0
- package/dashboard/dist/components/AgentRegistryPane.js +89 -0
- package/dashboard/dist/components/AgentRegistryPane.test.d.ts +1 -0
- package/dashboard/dist/components/AgentRegistryPane.test.js +200 -0
- package/dashboard/dist/components/RouterPane.d.ts +5 -0
- package/dashboard/dist/components/RouterPane.js +65 -0
- package/dashboard/dist/components/RouterPane.test.d.ts +1 -0
- package/dashboard/dist/components/RouterPane.test.js +176 -0
- package/package.json +5 -2
- package/server/index.js +178 -0
- package/server/lib/agent-cleanup.js +177 -0
- package/server/lib/agent-cleanup.test.js +359 -0
- package/server/lib/agent-hooks.js +126 -0
- package/server/lib/agent-hooks.test.js +303 -0
- package/server/lib/agent-metadata.js +179 -0
- package/server/lib/agent-metadata.test.js +383 -0
- package/server/lib/agent-persistence.js +191 -0
- package/server/lib/agent-persistence.test.js +475 -0
- package/server/lib/agent-registry-command.js +340 -0
- package/server/lib/agent-registry-command.test.js +334 -0
- package/server/lib/agent-registry.js +155 -0
- package/server/lib/agent-registry.test.js +239 -0
- package/server/lib/agent-state.js +236 -0
- package/server/lib/agent-state.test.js +375 -0
- package/server/lib/api-provider.js +186 -0
- package/server/lib/api-provider.test.js +336 -0
- package/server/lib/cli-detector.js +166 -0
- package/server/lib/cli-detector.test.js +269 -0
- package/server/lib/cli-provider.js +212 -0
- package/server/lib/cli-provider.test.js +349 -0
- package/server/lib/debug.test.js +62 -0
- package/server/lib/devserver-router-api.js +249 -0
- package/server/lib/devserver-router-api.test.js +426 -0
- package/server/lib/model-router.js +245 -0
- package/server/lib/model-router.test.js +313 -0
- package/server/lib/output-schemas.js +269 -0
- package/server/lib/output-schemas.test.js +307 -0
- package/server/lib/provider-interface.js +153 -0
- package/server/lib/provider-interface.test.js +394 -0
- package/server/lib/provider-queue.js +158 -0
- package/server/lib/provider-queue.test.js +315 -0
- package/server/lib/router-config.js +221 -0
- package/server/lib/router-config.test.js +237 -0
- package/server/lib/router-setup-command.js +419 -0
- package/server/lib/router-setup-command.test.js +375 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Registry Command
|
|
3
|
+
* CLI for agent registry operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import registry from './agent-registry.js';
|
|
7
|
+
import { transitionTo, STATES } from './agent-state.js';
|
|
8
|
+
import { cleanupOrphans, getCleanupStats } from './agent-cleanup.js';
|
|
9
|
+
import { triggerHook } from './agent-hooks.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parse command line arguments
|
|
13
|
+
* @param {string[]} args - Command line arguments
|
|
14
|
+
* @returns {Object} Parsed arguments
|
|
15
|
+
*/
|
|
16
|
+
export function parseArgs(args) {
|
|
17
|
+
if (!args || args.length === 0) {
|
|
18
|
+
return { command: 'help', args: [], flags: {} };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const command = args[0];
|
|
22
|
+
const flags = {};
|
|
23
|
+
const positionalArgs = [];
|
|
24
|
+
|
|
25
|
+
for (let i = 1; i < args.length; i++) {
|
|
26
|
+
const arg = args[i];
|
|
27
|
+
if (arg.startsWith('--')) {
|
|
28
|
+
const flagName = arg.slice(2);
|
|
29
|
+
// Check if next arg is a value or another flag
|
|
30
|
+
if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
|
|
31
|
+
flags[flagName] = args[i + 1];
|
|
32
|
+
i++;
|
|
33
|
+
} else {
|
|
34
|
+
flags[flagName] = true;
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
positionalArgs.push(arg);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { command, args: positionalArgs, flags };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Execute a registry command
|
|
46
|
+
* @param {string[]} args - Command arguments
|
|
47
|
+
* @returns {Promise<Object>} Command result
|
|
48
|
+
*/
|
|
49
|
+
export async function execute(args) {
|
|
50
|
+
const parsed = parseArgs(args);
|
|
51
|
+
|
|
52
|
+
switch (parsed.command) {
|
|
53
|
+
case 'list':
|
|
54
|
+
return executeList(parsed);
|
|
55
|
+
case 'get':
|
|
56
|
+
return executeGet(parsed);
|
|
57
|
+
case 'cancel':
|
|
58
|
+
return executeCancel(parsed);
|
|
59
|
+
case 'cleanup':
|
|
60
|
+
return executeCleanup(parsed);
|
|
61
|
+
case 'help':
|
|
62
|
+
return executeHelp();
|
|
63
|
+
default:
|
|
64
|
+
return {
|
|
65
|
+
success: false,
|
|
66
|
+
error: `Unknown command: ${parsed.command}. Use 'help' to see available commands.`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Execute list command
|
|
73
|
+
*/
|
|
74
|
+
function executeList(parsed) {
|
|
75
|
+
const filters = {};
|
|
76
|
+
|
|
77
|
+
if (parsed.flags.status) {
|
|
78
|
+
filters.status = parsed.flags.status;
|
|
79
|
+
}
|
|
80
|
+
if (parsed.flags.model) {
|
|
81
|
+
filters.model = parsed.flags.model;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const agents = registry.listAgents(Object.keys(filters).length > 0 ? filters : undefined);
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
success: true,
|
|
88
|
+
data: agents,
|
|
89
|
+
formatted: formatAgentList(agents),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Execute get command
|
|
95
|
+
*/
|
|
96
|
+
function executeGet(parsed) {
|
|
97
|
+
const agentId = parsed.args[0];
|
|
98
|
+
|
|
99
|
+
if (!agentId) {
|
|
100
|
+
return {
|
|
101
|
+
success: false,
|
|
102
|
+
error: 'Agent ID required. Usage: tlc agents get <id>',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const agent = registry.getAgent(agentId);
|
|
107
|
+
|
|
108
|
+
if (!agent) {
|
|
109
|
+
return {
|
|
110
|
+
success: false,
|
|
111
|
+
error: `Agent '${agentId}' not found`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
success: true,
|
|
117
|
+
data: agent,
|
|
118
|
+
formatted: formatAgentDetails(agent),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Execute cancel command
|
|
124
|
+
*/
|
|
125
|
+
async function executeCancel(parsed) {
|
|
126
|
+
const agentId = parsed.args[0];
|
|
127
|
+
|
|
128
|
+
if (!agentId) {
|
|
129
|
+
return {
|
|
130
|
+
success: false,
|
|
131
|
+
error: 'Agent ID required. Usage: tlc agents cancel <id>',
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const agent = registry.getAgent(agentId);
|
|
136
|
+
|
|
137
|
+
if (!agent) {
|
|
138
|
+
return {
|
|
139
|
+
success: false,
|
|
140
|
+
error: `Agent '${agentId}' not found`,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Check if agent can be cancelled
|
|
145
|
+
const cancellableStates = [STATES.PENDING, STATES.RUNNING];
|
|
146
|
+
if (!cancellableStates.includes(agent.state.current)) {
|
|
147
|
+
return {
|
|
148
|
+
success: false,
|
|
149
|
+
error: `Cannot cancel agent in '${agent.state.current}' state. Only pending or running agents can be cancelled.`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Transition to cancelled
|
|
154
|
+
transitionTo(agent.state, STATES.CANCELLED, { reason: 'User requested cancellation' });
|
|
155
|
+
|
|
156
|
+
// Trigger cancel hook
|
|
157
|
+
await triggerHook('onCancel', agent);
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
success: true,
|
|
161
|
+
message: `Agent '${agentId}' cancelled successfully`,
|
|
162
|
+
data: agent,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Execute cleanup command
|
|
168
|
+
*/
|
|
169
|
+
async function executeCleanup(parsed) {
|
|
170
|
+
const result = await cleanupOrphans();
|
|
171
|
+
const stats = getCleanupStats();
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
success: true,
|
|
175
|
+
data: {
|
|
176
|
+
cleaned: result.cleaned,
|
|
177
|
+
errors: result.errors,
|
|
178
|
+
stats,
|
|
179
|
+
},
|
|
180
|
+
formatted: formatCleanupResult(result, stats),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Execute help command
|
|
186
|
+
*/
|
|
187
|
+
function executeHelp() {
|
|
188
|
+
const message = `
|
|
189
|
+
Usage: tlc agents <command> [options]
|
|
190
|
+
|
|
191
|
+
Commands:
|
|
192
|
+
list List all agents
|
|
193
|
+
--status <status> Filter by status (pending, running, completed, failed, cancelled)
|
|
194
|
+
--model <model> Filter by model
|
|
195
|
+
--json Output as JSON
|
|
196
|
+
|
|
197
|
+
get <id> Show agent details
|
|
198
|
+
--json Output as JSON
|
|
199
|
+
|
|
200
|
+
cancel <id> Cancel a running agent
|
|
201
|
+
|
|
202
|
+
cleanup Run cleanup for orphaned agents
|
|
203
|
+
|
|
204
|
+
help Show this help message
|
|
205
|
+
|
|
206
|
+
Examples:
|
|
207
|
+
tlc agents list
|
|
208
|
+
tlc agents list --status running
|
|
209
|
+
tlc agents list --model claude --status completed
|
|
210
|
+
tlc agents get agent-abc123
|
|
211
|
+
tlc agents cancel agent-abc123
|
|
212
|
+
tlc agents cleanup
|
|
213
|
+
`.trim();
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
success: true,
|
|
217
|
+
message,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Format agent list as table
|
|
223
|
+
* @param {Object[]} agents - List of agents
|
|
224
|
+
* @returns {string} Formatted table
|
|
225
|
+
*/
|
|
226
|
+
export function formatAgentList(agents) {
|
|
227
|
+
if (!agents || agents.length === 0) {
|
|
228
|
+
return 'No agents found.';
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const header = '| ID | Name | Status | Model | Created |';
|
|
232
|
+
const separator = '|------|------|--------|-------|---------|';
|
|
233
|
+
|
|
234
|
+
const rows = agents.map(agent => {
|
|
235
|
+
const id = truncate(agent.id, 15);
|
|
236
|
+
const name = truncate(agent.name || '-', 20);
|
|
237
|
+
const status = agent.state?.current || '-';
|
|
238
|
+
const model = agent.metadata?.model || '-';
|
|
239
|
+
const created = agent.createdAt ? formatDate(agent.createdAt) : '-';
|
|
240
|
+
|
|
241
|
+
return `| ${id} | ${name} | ${status} | ${model} | ${created} |`;
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
return [header, separator, ...rows].join('\n');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Format agent details
|
|
249
|
+
* @param {Object} agent - Agent object
|
|
250
|
+
* @returns {string} Formatted details
|
|
251
|
+
*/
|
|
252
|
+
export function formatAgentDetails(agent) {
|
|
253
|
+
const lines = [
|
|
254
|
+
`Agent: ${agent.id}`,
|
|
255
|
+
`Name: ${agent.name || '-'}`,
|
|
256
|
+
`Status: ${agent.state?.current || '-'}`,
|
|
257
|
+
`Model: ${agent.metadata?.model || '-'}`,
|
|
258
|
+
`Created: ${agent.createdAt || '-'}`,
|
|
259
|
+
'',
|
|
260
|
+
];
|
|
261
|
+
|
|
262
|
+
// Add token info if available
|
|
263
|
+
if (agent.metadata?.tokens) {
|
|
264
|
+
lines.push('Tokens:');
|
|
265
|
+
lines.push(` Input: ${agent.metadata.tokens.input || 0}`);
|
|
266
|
+
lines.push(` Output: ${agent.metadata.tokens.output || 0}`);
|
|
267
|
+
lines.push(` Total: ${(agent.metadata.tokens.input || 0) + (agent.metadata.tokens.output || 0)}`);
|
|
268
|
+
lines.push('');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Add cost if available
|
|
272
|
+
if (agent.metadata?.cost !== undefined) {
|
|
273
|
+
lines.push(`Cost: $${agent.metadata.cost.toFixed(4)}`);
|
|
274
|
+
lines.push('');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Add state history if available
|
|
278
|
+
if (agent.state?.history && agent.state.history.length > 0) {
|
|
279
|
+
lines.push('State History:');
|
|
280
|
+
agent.state.history.forEach(entry => {
|
|
281
|
+
const time = entry.timestamp ? formatDate(entry.timestamp) : '-';
|
|
282
|
+
lines.push(` ${time}: ${entry.state}`);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return lines.join('\n');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Format cleanup result
|
|
291
|
+
*/
|
|
292
|
+
function formatCleanupResult(result, stats) {
|
|
293
|
+
const lines = [
|
|
294
|
+
'Cleanup Results:',
|
|
295
|
+
` Cleaned this run: ${result.cleaned.length}`,
|
|
296
|
+
` Errors: ${result.errors.length}`,
|
|
297
|
+
'',
|
|
298
|
+
'Overall Stats:',
|
|
299
|
+
` Total cleaned: ${stats.totalCleaned}`,
|
|
300
|
+
` Cleanup runs: ${stats.cleanupRuns}`,
|
|
301
|
+
` Last cleanup: ${stats.lastCleanupAt ? formatDate(stats.lastCleanupAt) : 'Never'}`,
|
|
302
|
+
];
|
|
303
|
+
|
|
304
|
+
if (result.cleaned.length > 0) {
|
|
305
|
+
lines.push('');
|
|
306
|
+
lines.push('Cleaned agents:');
|
|
307
|
+
result.cleaned.forEach(id => lines.push(` - ${id}`));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (result.errors.length > 0) {
|
|
311
|
+
lines.push('');
|
|
312
|
+
lines.push('Errors:');
|
|
313
|
+
result.errors.forEach(err => lines.push(` - ${err.id}: ${err.error}`));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return lines.join('\n');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Truncate string to max length
|
|
321
|
+
*/
|
|
322
|
+
function truncate(str, maxLength) {
|
|
323
|
+
if (!str) return '-';
|
|
324
|
+
if (str.length <= maxLength) return str;
|
|
325
|
+
return str.slice(0, maxLength - 3) + '...';
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Format date for display
|
|
330
|
+
*/
|
|
331
|
+
function formatDate(dateStr) {
|
|
332
|
+
try {
|
|
333
|
+
const date = new Date(dateStr);
|
|
334
|
+
return date.toISOString().replace('T', ' ').slice(0, 19);
|
|
335
|
+
} catch {
|
|
336
|
+
return dateStr;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export default { execute, parseArgs, formatAgentList, formatAgentDetails };
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Mock dependencies
|
|
4
|
+
vi.mock('./agent-registry.js', () => ({
|
|
5
|
+
default: {
|
|
6
|
+
listAgents: vi.fn(),
|
|
7
|
+
getAgent: vi.fn(),
|
|
8
|
+
removeAgent: vi.fn(),
|
|
9
|
+
},
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock('./agent-state.js', () => ({
|
|
13
|
+
transitionTo: vi.fn(),
|
|
14
|
+
STATES: {
|
|
15
|
+
PENDING: 'pending',
|
|
16
|
+
RUNNING: 'running',
|
|
17
|
+
COMPLETED: 'completed',
|
|
18
|
+
FAILED: 'failed',
|
|
19
|
+
CANCELLED: 'cancelled',
|
|
20
|
+
},
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
vi.mock('./agent-cleanup.js', () => ({
|
|
24
|
+
cleanupOrphans: vi.fn(),
|
|
25
|
+
getCleanupStats: vi.fn(),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
vi.mock('./agent-hooks.js', () => ({
|
|
29
|
+
triggerHook: vi.fn(),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
import {
|
|
33
|
+
execute,
|
|
34
|
+
formatAgentList,
|
|
35
|
+
formatAgentDetails,
|
|
36
|
+
parseArgs,
|
|
37
|
+
} from './agent-registry-command.js';
|
|
38
|
+
import registry from './agent-registry.js';
|
|
39
|
+
import { transitionTo, STATES } from './agent-state.js';
|
|
40
|
+
import { cleanupOrphans, getCleanupStats } from './agent-cleanup.js';
|
|
41
|
+
import { triggerHook } from './agent-hooks.js';
|
|
42
|
+
|
|
43
|
+
describe('agent-registry-command', () => {
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
vi.clearAllMocks();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('execute list', () => {
|
|
49
|
+
it('shows all agents', async () => {
|
|
50
|
+
const agents = [
|
|
51
|
+
{ id: 'agent-1', name: 'test-1', state: { current: 'running' }, metadata: { model: 'claude' } },
|
|
52
|
+
{ id: 'agent-2', name: 'test-2', state: { current: 'completed' }, metadata: { model: 'gpt-4' } },
|
|
53
|
+
];
|
|
54
|
+
registry.listAgents.mockReturnValue(agents);
|
|
55
|
+
|
|
56
|
+
const result = await execute(['list']);
|
|
57
|
+
|
|
58
|
+
expect(registry.listAgents).toHaveBeenCalled();
|
|
59
|
+
expect(result.success).toBe(true);
|
|
60
|
+
expect(result.data).toEqual(agents);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('filters by status', async () => {
|
|
64
|
+
const agents = [
|
|
65
|
+
{ id: 'agent-1', name: 'test-1', state: { current: 'running' }, metadata: { model: 'claude' } },
|
|
66
|
+
];
|
|
67
|
+
registry.listAgents.mockReturnValue(agents);
|
|
68
|
+
|
|
69
|
+
const result = await execute(['list', '--status', 'running']);
|
|
70
|
+
|
|
71
|
+
expect(registry.listAgents).toHaveBeenCalledWith({ status: 'running' });
|
|
72
|
+
expect(result.success).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('filters by model', async () => {
|
|
76
|
+
const agents = [
|
|
77
|
+
{ id: 'agent-1', name: 'test-1', state: { current: 'running' }, metadata: { model: 'claude' } },
|
|
78
|
+
];
|
|
79
|
+
registry.listAgents.mockReturnValue(agents);
|
|
80
|
+
|
|
81
|
+
const result = await execute(['list', '--model', 'claude']);
|
|
82
|
+
|
|
83
|
+
expect(registry.listAgents).toHaveBeenCalledWith({ model: 'claude' });
|
|
84
|
+
expect(result.success).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('combines filters', async () => {
|
|
88
|
+
registry.listAgents.mockReturnValue([]);
|
|
89
|
+
|
|
90
|
+
const result = await execute(['list', '--status', 'running', '--model', 'claude']);
|
|
91
|
+
|
|
92
|
+
expect(registry.listAgents).toHaveBeenCalledWith({ status: 'running', model: 'claude' });
|
|
93
|
+
expect(result.success).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('execute get', () => {
|
|
98
|
+
it('shows agent details', async () => {
|
|
99
|
+
const agent = {
|
|
100
|
+
id: 'agent-123',
|
|
101
|
+
name: 'test-agent',
|
|
102
|
+
state: { current: 'running', history: [] },
|
|
103
|
+
metadata: { model: 'claude', tokens: { input: 100, output: 50 } },
|
|
104
|
+
createdAt: new Date().toISOString(),
|
|
105
|
+
};
|
|
106
|
+
registry.getAgent.mockReturnValue(agent);
|
|
107
|
+
|
|
108
|
+
const result = await execute(['get', 'agent-123']);
|
|
109
|
+
|
|
110
|
+
expect(registry.getAgent).toHaveBeenCalledWith('agent-123');
|
|
111
|
+
expect(result.success).toBe(true);
|
|
112
|
+
expect(result.data).toEqual(agent);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('shows error for missing agent', async () => {
|
|
116
|
+
registry.getAgent.mockReturnValue(null);
|
|
117
|
+
|
|
118
|
+
const result = await execute(['get', 'unknown-id']);
|
|
119
|
+
|
|
120
|
+
expect(result.success).toBe(false);
|
|
121
|
+
expect(result.error).toContain('not found');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('requires agent ID', async () => {
|
|
125
|
+
const result = await execute(['get']);
|
|
126
|
+
|
|
127
|
+
expect(result.success).toBe(false);
|
|
128
|
+
expect(result.error).toContain('Agent ID required');
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('execute cancel', () => {
|
|
133
|
+
it('cancels running agent', async () => {
|
|
134
|
+
const agent = {
|
|
135
|
+
id: 'agent-123',
|
|
136
|
+
name: 'test-agent',
|
|
137
|
+
state: { current: 'running' },
|
|
138
|
+
};
|
|
139
|
+
registry.getAgent.mockReturnValue(agent);
|
|
140
|
+
transitionTo.mockReturnValue({ current: 'cancelled' });
|
|
141
|
+
|
|
142
|
+
const result = await execute(['cancel', 'agent-123']);
|
|
143
|
+
|
|
144
|
+
expect(registry.getAgent).toHaveBeenCalledWith('agent-123');
|
|
145
|
+
expect(transitionTo).toHaveBeenCalledWith(agent.state, STATES.CANCELLED, expect.any(Object));
|
|
146
|
+
expect(triggerHook).toHaveBeenCalledWith('onCancel', agent);
|
|
147
|
+
expect(result.success).toBe(true);
|
|
148
|
+
expect(result.message).toContain('cancelled');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('shows error for missing agent', async () => {
|
|
152
|
+
registry.getAgent.mockReturnValue(null);
|
|
153
|
+
|
|
154
|
+
const result = await execute(['cancel', 'unknown-id']);
|
|
155
|
+
|
|
156
|
+
expect(result.success).toBe(false);
|
|
157
|
+
expect(result.error).toContain('not found');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('shows error for already completed agent', async () => {
|
|
161
|
+
const agent = {
|
|
162
|
+
id: 'agent-123',
|
|
163
|
+
state: { current: 'completed' },
|
|
164
|
+
};
|
|
165
|
+
registry.getAgent.mockReturnValue(agent);
|
|
166
|
+
|
|
167
|
+
const result = await execute(['cancel', 'agent-123']);
|
|
168
|
+
|
|
169
|
+
expect(result.success).toBe(false);
|
|
170
|
+
expect(result.error).toContain('Cannot cancel');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('requires agent ID', async () => {
|
|
174
|
+
const result = await execute(['cancel']);
|
|
175
|
+
|
|
176
|
+
expect(result.success).toBe(false);
|
|
177
|
+
expect(result.error).toContain('Agent ID required');
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe('execute cleanup', () => {
|
|
182
|
+
it('runs cleanup and shows stats', async () => {
|
|
183
|
+
cleanupOrphans.mockResolvedValue({ cleaned: ['agent-1', 'agent-2'], errors: [] });
|
|
184
|
+
getCleanupStats.mockReturnValue({ totalCleaned: 5, cleanupRuns: 3, lastCleanupAt: new Date() });
|
|
185
|
+
|
|
186
|
+
const result = await execute(['cleanup']);
|
|
187
|
+
|
|
188
|
+
expect(cleanupOrphans).toHaveBeenCalled();
|
|
189
|
+
expect(getCleanupStats).toHaveBeenCalled();
|
|
190
|
+
expect(result.success).toBe(true);
|
|
191
|
+
expect(result.data.cleaned).toHaveLength(2);
|
|
192
|
+
expect(result.data.stats.totalCleaned).toBe(5);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('reports cleanup errors', async () => {
|
|
196
|
+
cleanupOrphans.mockResolvedValue({ cleaned: [], errors: [{ id: 'agent-1', error: 'failed' }] });
|
|
197
|
+
getCleanupStats.mockReturnValue({ totalCleaned: 0, cleanupRuns: 1 });
|
|
198
|
+
|
|
199
|
+
const result = await execute(['cleanup']);
|
|
200
|
+
|
|
201
|
+
expect(result.success).toBe(true);
|
|
202
|
+
expect(result.data.errors).toHaveLength(1);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe('formatAgentList', () => {
|
|
207
|
+
it('creates readable table', () => {
|
|
208
|
+
const agents = [
|
|
209
|
+
{ id: 'agent-123', name: 'task-1', state: { current: 'running' }, metadata: { model: 'claude' }, createdAt: '2025-01-01T00:00:00Z' },
|
|
210
|
+
{ id: 'agent-456', name: 'task-2', state: { current: 'completed' }, metadata: { model: 'gpt-4' }, createdAt: '2025-01-01T00:01:00Z' },
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
const output = formatAgentList(agents);
|
|
214
|
+
|
|
215
|
+
expect(output).toContain('agent-123');
|
|
216
|
+
expect(output).toContain('running');
|
|
217
|
+
expect(output).toContain('claude');
|
|
218
|
+
expect(output).toContain('agent-456');
|
|
219
|
+
expect(output).toContain('completed');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('handles empty list', () => {
|
|
223
|
+
const output = formatAgentList([]);
|
|
224
|
+
|
|
225
|
+
expect(output).toContain('No agents');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('truncates long names', () => {
|
|
229
|
+
const agents = [
|
|
230
|
+
{ id: 'agent-123', name: 'this-is-a-very-long-agent-name-that-should-be-truncated', state: { current: 'running' }, metadata: { model: 'claude' } },
|
|
231
|
+
];
|
|
232
|
+
|
|
233
|
+
const output = formatAgentList(agents);
|
|
234
|
+
|
|
235
|
+
expect(output.length).toBeLessThan(agents[0].name.length + 200);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe('formatAgentDetails', () => {
|
|
240
|
+
it('shows all agent fields', () => {
|
|
241
|
+
const agent = {
|
|
242
|
+
id: 'agent-123',
|
|
243
|
+
name: 'test-agent',
|
|
244
|
+
state: { current: 'running', history: [{ state: 'pending', timestamp: '2025-01-01T00:00:00Z' }] },
|
|
245
|
+
metadata: { model: 'claude', tokens: { input: 100, output: 50 }, cost: 0.0015 },
|
|
246
|
+
createdAt: '2025-01-01T00:00:00Z',
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const output = formatAgentDetails(agent);
|
|
250
|
+
|
|
251
|
+
expect(output).toContain('agent-123');
|
|
252
|
+
expect(output).toContain('test-agent');
|
|
253
|
+
expect(output).toContain('running');
|
|
254
|
+
expect(output).toContain('claude');
|
|
255
|
+
expect(output).toContain('100');
|
|
256
|
+
expect(output).toContain('50');
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('shows state history', () => {
|
|
260
|
+
const agent = {
|
|
261
|
+
id: 'agent-123',
|
|
262
|
+
name: 'test',
|
|
263
|
+
state: {
|
|
264
|
+
current: 'completed',
|
|
265
|
+
history: [
|
|
266
|
+
{ state: 'pending', timestamp: '2025-01-01T00:00:00Z' },
|
|
267
|
+
{ state: 'running', timestamp: '2025-01-01T00:00:01Z' },
|
|
268
|
+
{ state: 'completed', timestamp: '2025-01-01T00:00:10Z' },
|
|
269
|
+
],
|
|
270
|
+
},
|
|
271
|
+
metadata: { model: 'claude' },
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const output = formatAgentDetails(agent);
|
|
275
|
+
|
|
276
|
+
expect(output).toContain('pending');
|
|
277
|
+
expect(output).toContain('running');
|
|
278
|
+
expect(output).toContain('completed');
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe('parseArgs', () => {
|
|
283
|
+
it('parses command', () => {
|
|
284
|
+
const parsed = parseArgs(['list']);
|
|
285
|
+
expect(parsed.command).toBe('list');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('parses command with flags', () => {
|
|
289
|
+
const parsed = parseArgs(['list', '--status', 'running']);
|
|
290
|
+
expect(parsed.command).toBe('list');
|
|
291
|
+
expect(parsed.flags.status).toBe('running');
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('parses command with positional arg', () => {
|
|
295
|
+
const parsed = parseArgs(['get', 'agent-123']);
|
|
296
|
+
expect(parsed.command).toBe('get');
|
|
297
|
+
expect(parsed.args).toContain('agent-123');
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('handles multiple flags', () => {
|
|
301
|
+
const parsed = parseArgs(['list', '--status', 'running', '--model', 'claude', '--json']);
|
|
302
|
+
expect(parsed.flags.status).toBe('running');
|
|
303
|
+
expect(parsed.flags.model).toBe('claude');
|
|
304
|
+
expect(parsed.flags.json).toBe(true);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('returns help for empty args', () => {
|
|
308
|
+
const parsed = parseArgs([]);
|
|
309
|
+
expect(parsed.command).toBe('help');
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
describe('unknown command', () => {
|
|
314
|
+
it('shows error for unknown command', async () => {
|
|
315
|
+
const result = await execute(['unknown']);
|
|
316
|
+
|
|
317
|
+
expect(result.success).toBe(false);
|
|
318
|
+
expect(result.error).toContain('Unknown command');
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
describe('help command', () => {
|
|
323
|
+
it('shows usage information', async () => {
|
|
324
|
+
const result = await execute(['help']);
|
|
325
|
+
|
|
326
|
+
expect(result.success).toBe(true);
|
|
327
|
+
expect(result.message).toContain('Usage');
|
|
328
|
+
expect(result.message).toContain('list');
|
|
329
|
+
expect(result.message).toContain('get');
|
|
330
|
+
expect(result.message).toContain('cancel');
|
|
331
|
+
expect(result.message).toContain('cleanup');
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
});
|