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.
Files changed (46) hide show
  1. package/dashboard/dist/App.js +229 -35
  2. package/dashboard/dist/components/AgentRegistryPane.d.ts +35 -0
  3. package/dashboard/dist/components/AgentRegistryPane.js +89 -0
  4. package/dashboard/dist/components/AgentRegistryPane.test.d.ts +1 -0
  5. package/dashboard/dist/components/AgentRegistryPane.test.js +200 -0
  6. package/dashboard/dist/components/RouterPane.d.ts +5 -0
  7. package/dashboard/dist/components/RouterPane.js +65 -0
  8. package/dashboard/dist/components/RouterPane.test.d.ts +1 -0
  9. package/dashboard/dist/components/RouterPane.test.js +176 -0
  10. package/package.json +5 -2
  11. package/server/index.js +178 -0
  12. package/server/lib/agent-cleanup.js +177 -0
  13. package/server/lib/agent-cleanup.test.js +359 -0
  14. package/server/lib/agent-hooks.js +126 -0
  15. package/server/lib/agent-hooks.test.js +303 -0
  16. package/server/lib/agent-metadata.js +179 -0
  17. package/server/lib/agent-metadata.test.js +383 -0
  18. package/server/lib/agent-persistence.js +191 -0
  19. package/server/lib/agent-persistence.test.js +475 -0
  20. package/server/lib/agent-registry-command.js +340 -0
  21. package/server/lib/agent-registry-command.test.js +334 -0
  22. package/server/lib/agent-registry.js +155 -0
  23. package/server/lib/agent-registry.test.js +239 -0
  24. package/server/lib/agent-state.js +236 -0
  25. package/server/lib/agent-state.test.js +375 -0
  26. package/server/lib/api-provider.js +186 -0
  27. package/server/lib/api-provider.test.js +336 -0
  28. package/server/lib/cli-detector.js +166 -0
  29. package/server/lib/cli-detector.test.js +269 -0
  30. package/server/lib/cli-provider.js +212 -0
  31. package/server/lib/cli-provider.test.js +349 -0
  32. package/server/lib/debug.test.js +62 -0
  33. package/server/lib/devserver-router-api.js +249 -0
  34. package/server/lib/devserver-router-api.test.js +426 -0
  35. package/server/lib/model-router.js +245 -0
  36. package/server/lib/model-router.test.js +313 -0
  37. package/server/lib/output-schemas.js +269 -0
  38. package/server/lib/output-schemas.test.js +307 -0
  39. package/server/lib/provider-interface.js +153 -0
  40. package/server/lib/provider-interface.test.js +394 -0
  41. package/server/lib/provider-queue.js +158 -0
  42. package/server/lib/provider-queue.test.js +315 -0
  43. package/server/lib/router-config.js +221 -0
  44. package/server/lib/router-config.test.js +237 -0
  45. package/server/lib/router-setup-command.js +419 -0
  46. 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
+ });