tlc-claude-code 1.4.9 → 1.5.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 (122) hide show
  1. package/CLAUDE.md +23 -0
  2. package/CODING-STANDARDS.md +408 -0
  3. package/bin/install.js +2 -0
  4. package/dashboard/dist/components/QualityGatePane.d.ts +38 -0
  5. package/dashboard/dist/components/QualityGatePane.js +31 -0
  6. package/dashboard/dist/components/QualityGatePane.test.d.ts +1 -0
  7. package/dashboard/dist/components/QualityGatePane.test.js +147 -0
  8. package/dashboard/dist/components/orchestration/AgentCard.d.ts +26 -0
  9. package/dashboard/dist/components/orchestration/AgentCard.js +60 -0
  10. package/dashboard/dist/components/orchestration/AgentCard.test.d.ts +1 -0
  11. package/dashboard/dist/components/orchestration/AgentCard.test.js +63 -0
  12. package/dashboard/dist/components/orchestration/AgentControls.d.ts +11 -0
  13. package/dashboard/dist/components/orchestration/AgentControls.js +20 -0
  14. package/dashboard/dist/components/orchestration/AgentControls.test.d.ts +1 -0
  15. package/dashboard/dist/components/orchestration/AgentControls.test.js +52 -0
  16. package/dashboard/dist/components/orchestration/AgentDetail.d.ts +35 -0
  17. package/dashboard/dist/components/orchestration/AgentDetail.js +37 -0
  18. package/dashboard/dist/components/orchestration/AgentDetail.test.d.ts +1 -0
  19. package/dashboard/dist/components/orchestration/AgentDetail.test.js +79 -0
  20. package/dashboard/dist/components/orchestration/AgentList.d.ts +31 -0
  21. package/dashboard/dist/components/orchestration/AgentList.js +47 -0
  22. package/dashboard/dist/components/orchestration/AgentList.test.d.ts +1 -0
  23. package/dashboard/dist/components/orchestration/AgentList.test.js +64 -0
  24. package/dashboard/dist/components/orchestration/CostMeter.d.ts +11 -0
  25. package/dashboard/dist/components/orchestration/CostMeter.js +28 -0
  26. package/dashboard/dist/components/orchestration/CostMeter.test.d.ts +1 -0
  27. package/dashboard/dist/components/orchestration/CostMeter.test.js +50 -0
  28. package/dashboard/dist/components/orchestration/ModelSelector.d.ts +20 -0
  29. package/dashboard/dist/components/orchestration/ModelSelector.js +12 -0
  30. package/dashboard/dist/components/orchestration/ModelSelector.test.d.ts +1 -0
  31. package/dashboard/dist/components/orchestration/ModelSelector.test.js +56 -0
  32. package/dashboard/dist/components/orchestration/OrchestrationDashboard.d.ts +28 -0
  33. package/dashboard/dist/components/orchestration/OrchestrationDashboard.js +28 -0
  34. package/dashboard/dist/components/orchestration/OrchestrationDashboard.test.d.ts +1 -0
  35. package/dashboard/dist/components/orchestration/OrchestrationDashboard.test.js +56 -0
  36. package/dashboard/dist/components/orchestration/QualityIndicator.d.ts +11 -0
  37. package/dashboard/dist/components/orchestration/QualityIndicator.js +37 -0
  38. package/dashboard/dist/components/orchestration/QualityIndicator.test.d.ts +1 -0
  39. package/dashboard/dist/components/orchestration/QualityIndicator.test.js +52 -0
  40. package/dashboard/dist/components/orchestration/index.d.ts +8 -0
  41. package/dashboard/dist/components/orchestration/index.js +8 -0
  42. package/package.json +1 -1
  43. package/server/lib/access-control.js +352 -0
  44. package/server/lib/access-control.test.js +322 -0
  45. package/server/lib/agents-cancel-command.js +139 -0
  46. package/server/lib/agents-cancel-command.test.js +180 -0
  47. package/server/lib/agents-get-command.js +159 -0
  48. package/server/lib/agents-get-command.test.js +167 -0
  49. package/server/lib/agents-list-command.js +150 -0
  50. package/server/lib/agents-list-command.test.js +149 -0
  51. package/server/lib/agents-logs-command.js +126 -0
  52. package/server/lib/agents-logs-command.test.js +198 -0
  53. package/server/lib/agents-retry-command.js +117 -0
  54. package/server/lib/agents-retry-command.test.js +192 -0
  55. package/server/lib/budget-limits.js +222 -0
  56. package/server/lib/budget-limits.test.js +214 -0
  57. package/server/lib/code-generator.js +291 -0
  58. package/server/lib/code-generator.test.js +307 -0
  59. package/server/lib/cost-command.js +290 -0
  60. package/server/lib/cost-command.test.js +202 -0
  61. package/server/lib/cost-optimizer.js +404 -0
  62. package/server/lib/cost-optimizer.test.js +232 -0
  63. package/server/lib/cost-projections.js +302 -0
  64. package/server/lib/cost-projections.test.js +217 -0
  65. package/server/lib/cost-reports.js +277 -0
  66. package/server/lib/cost-reports.test.js +254 -0
  67. package/server/lib/cost-tracker.js +216 -0
  68. package/server/lib/cost-tracker.test.js +302 -0
  69. package/server/lib/crypto-patterns.js +433 -0
  70. package/server/lib/crypto-patterns.test.js +346 -0
  71. package/server/lib/design-command.js +385 -0
  72. package/server/lib/design-command.test.js +249 -0
  73. package/server/lib/design-parser.js +237 -0
  74. package/server/lib/design-parser.test.js +290 -0
  75. package/server/lib/gemini-vision.js +377 -0
  76. package/server/lib/gemini-vision.test.js +282 -0
  77. package/server/lib/input-validator.js +360 -0
  78. package/server/lib/input-validator.test.js +295 -0
  79. package/server/lib/litellm-client.js +232 -0
  80. package/server/lib/litellm-client.test.js +267 -0
  81. package/server/lib/litellm-command.js +291 -0
  82. package/server/lib/litellm-command.test.js +260 -0
  83. package/server/lib/litellm-config.js +273 -0
  84. package/server/lib/litellm-config.test.js +212 -0
  85. package/server/lib/model-pricing.js +189 -0
  86. package/server/lib/model-pricing.test.js +178 -0
  87. package/server/lib/models-command.js +223 -0
  88. package/server/lib/models-command.test.js +193 -0
  89. package/server/lib/optimize-command.js +197 -0
  90. package/server/lib/optimize-command.test.js +193 -0
  91. package/server/lib/orchestration-integration.js +206 -0
  92. package/server/lib/orchestration-integration.test.js +235 -0
  93. package/server/lib/output-encoder.js +308 -0
  94. package/server/lib/output-encoder.test.js +312 -0
  95. package/server/lib/quality-evaluator.js +396 -0
  96. package/server/lib/quality-evaluator.test.js +337 -0
  97. package/server/lib/quality-gate-command.js +340 -0
  98. package/server/lib/quality-gate-command.test.js +321 -0
  99. package/server/lib/quality-gate-scorer.js +378 -0
  100. package/server/lib/quality-gate-scorer.test.js +376 -0
  101. package/server/lib/quality-history.js +265 -0
  102. package/server/lib/quality-history.test.js +359 -0
  103. package/server/lib/quality-presets.js +288 -0
  104. package/server/lib/quality-presets.test.js +269 -0
  105. package/server/lib/quality-retry.js +323 -0
  106. package/server/lib/quality-retry.test.js +325 -0
  107. package/server/lib/quality-thresholds.js +255 -0
  108. package/server/lib/quality-thresholds.test.js +237 -0
  109. package/server/lib/secure-auth.js +333 -0
  110. package/server/lib/secure-auth.test.js +288 -0
  111. package/server/lib/secure-code-command.js +540 -0
  112. package/server/lib/secure-code-command.test.js +309 -0
  113. package/server/lib/secure-errors.js +521 -0
  114. package/server/lib/secure-errors.test.js +298 -0
  115. package/server/lib/vision-command.js +372 -0
  116. package/server/lib/vision-command.test.js +255 -0
  117. package/server/lib/visual-command.js +350 -0
  118. package/server/lib/visual-command.test.js +256 -0
  119. package/server/lib/visual-testing.js +315 -0
  120. package/server/lib/visual-testing.test.js +357 -0
  121. package/server/package-lock.json +2 -2
  122. package/server/package.json +1 -1
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Agents List Command
3
+ * Lists all agents with filtering capabilities
4
+ */
5
+
6
+ /**
7
+ * Parse time string to milliseconds
8
+ * @param {string} timeStr - Time string like '1h', '30m', '2d'
9
+ * @returns {number} Milliseconds
10
+ */
11
+ function parseTime(timeStr) {
12
+ const match = timeStr.match(/^(\d+)(s|m|h|d)$/);
13
+ if (!match) return 0;
14
+
15
+ const value = parseInt(match[1], 10);
16
+ const unit = match[2];
17
+
18
+ switch (unit) {
19
+ case 's': return value * 1000;
20
+ case 'm': return value * 60 * 1000;
21
+ case 'h': return value * 60 * 60 * 1000;
22
+ case 'd': return value * 24 * 60 * 60 * 1000;
23
+ default: return 0;
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Filter agents by criteria
29
+ * @param {Array} agents - Agents to filter
30
+ * @param {object} options - Filter options
31
+ * @returns {Array} Filtered agents
32
+ */
33
+ function filterAgents(agents, options = {}) {
34
+ let result = [...agents];
35
+
36
+ if (options.status) {
37
+ result = result.filter(a => a.status === options.status);
38
+ }
39
+
40
+ if (options.model) {
41
+ result = result.filter(a => a.model.toLowerCase().includes(options.model.toLowerCase()));
42
+ }
43
+
44
+ if (options.since) {
45
+ const sinceMs = parseTime(options.since);
46
+ const cutoff = Date.now() - sinceMs;
47
+ result = result.filter(a => new Date(a.startTime).getTime() >= cutoff);
48
+ }
49
+
50
+ return result;
51
+ }
52
+
53
+ /**
54
+ * Truncate string to max length
55
+ * @param {string} str - String to truncate
56
+ * @param {number} maxLen - Maximum length
57
+ * @returns {string} Truncated string
58
+ */
59
+ function truncate(str, maxLen) {
60
+ if (!str) return '';
61
+ if (str.length <= maxLen) return str;
62
+ return str.substring(0, maxLen - 3) + '...';
63
+ }
64
+
65
+ /**
66
+ * Format agents as table
67
+ * @param {Array} agents - Agents to format
68
+ * @param {object} options - Format options
69
+ * @returns {string} Formatted table
70
+ */
71
+ function formatTable(agents, options = {}) {
72
+ if (!agents || agents.length === 0) {
73
+ return 'No agents found';
74
+ }
75
+
76
+ const maxWidth = options.maxWidth || 120;
77
+
78
+ // Calculate column widths
79
+ const colWidths = {
80
+ id: Math.min(15, maxWidth / 5),
81
+ name: Math.min(20, maxWidth / 4),
82
+ model: Math.min(12, maxWidth / 6),
83
+ status: 10,
84
+ cost: 10,
85
+ };
86
+
87
+ // Header
88
+ const header = [
89
+ 'ID'.padEnd(colWidths.id),
90
+ 'Name'.padEnd(colWidths.name),
91
+ 'Model'.padEnd(colWidths.model),
92
+ 'Status'.padEnd(colWidths.status),
93
+ 'Cost'.padEnd(colWidths.cost),
94
+ ].join(' | ');
95
+
96
+ const separator = '-'.repeat(header.length);
97
+
98
+ // Rows
99
+ const rows = agents.map(agent => [
100
+ truncate(agent.id, colWidths.id).padEnd(colWidths.id),
101
+ truncate(agent.name || '-', colWidths.name).padEnd(colWidths.name),
102
+ truncate(agent.model, colWidths.model).padEnd(colWidths.model),
103
+ agent.status.padEnd(colWidths.status),
104
+ `$${(agent.cost || 0).toFixed(4)}`.padEnd(colWidths.cost),
105
+ ].join(' | '));
106
+
107
+ return [header, separator, ...rows].join('\n');
108
+ }
109
+
110
+ /**
111
+ * Format agents as JSON
112
+ * @param {Array} agents - Agents to format
113
+ * @returns {string} JSON string
114
+ */
115
+ function formatJSON(agents) {
116
+ return JSON.stringify(agents, null, 2);
117
+ }
118
+
119
+ /**
120
+ * Execute agents list command
121
+ * @param {object} context - Execution context
122
+ * @returns {Promise<object>} Command result
123
+ */
124
+ async function execute(context) {
125
+ const { registry, options = {} } = context;
126
+
127
+ const allAgents = registry.listAgents();
128
+ const filtered = filterAgents(allAgents, options);
129
+
130
+ let output;
131
+ if (options.json) {
132
+ output = formatJSON(filtered);
133
+ } else {
134
+ output = formatTable(filtered, { maxWidth: options.width });
135
+ }
136
+
137
+ return {
138
+ success: true,
139
+ count: filtered.length,
140
+ output,
141
+ };
142
+ }
143
+
144
+ module.exports = {
145
+ execute,
146
+ filterAgents,
147
+ formatTable,
148
+ formatJSON,
149
+ parseTime,
150
+ };
@@ -0,0 +1,149 @@
1
+ const { describe, it, beforeEach, mock } = require('node:test');
2
+ const assert = require('node:assert');
3
+ const {
4
+ execute,
5
+ filterAgents,
6
+ formatTable,
7
+ formatJSON,
8
+ } = require('./agents-list-command.js');
9
+
10
+ describe('agents-list-command', () => {
11
+ describe('execute', () => {
12
+ const mockAgents = [
13
+ { id: 'agent-1', name: 'Builder', model: 'claude', status: 'running', startTime: new Date(), cost: 0.05 },
14
+ { id: 'agent-2', name: 'Reviewer', model: 'gpt-4', status: 'completed', startTime: new Date(Date.now() - 3600000), cost: 0.10 },
15
+ { id: 'agent-3', name: 'Tester', model: 'claude', status: 'failed', startTime: new Date(Date.now() - 7200000), cost: 0.02 },
16
+ ];
17
+
18
+ it('lists all agents', async () => {
19
+ const result = await execute({ registry: { listAgents: () => mockAgents } });
20
+ assert.ok(result.output);
21
+ assert.ok(result.output.includes('agent-1'));
22
+ assert.ok(result.output.includes('agent-2'));
23
+ assert.ok(result.output.includes('agent-3'));
24
+ });
25
+
26
+ it('filters by status', async () => {
27
+ const result = await execute({
28
+ registry: { listAgents: () => mockAgents },
29
+ options: { status: 'running' },
30
+ });
31
+ assert.ok(result.output.includes('agent-1'));
32
+ assert.ok(!result.output.includes('agent-2'));
33
+ });
34
+
35
+ it('filters by model', async () => {
36
+ const result = await execute({
37
+ registry: { listAgents: () => mockAgents },
38
+ options: { model: 'claude' },
39
+ });
40
+ assert.ok(result.output.includes('agent-1'));
41
+ assert.ok(result.output.includes('agent-3'));
42
+ assert.ok(!result.output.includes('Reviewer'));
43
+ });
44
+
45
+ it('filters by time', async () => {
46
+ const result = await execute({
47
+ registry: { listAgents: () => mockAgents },
48
+ options: { since: '30m' },
49
+ });
50
+ assert.ok(result.output.includes('agent-1'));
51
+ assert.ok(!result.output.includes('agent-3'));
52
+ });
53
+
54
+ it('handles empty list', async () => {
55
+ const result = await execute({ registry: { listAgents: () => [] } });
56
+ assert.ok(result.output.includes('No agents found'));
57
+ });
58
+
59
+ it('shows helpful headers', async () => {
60
+ const result = await execute({ registry: { listAgents: () => mockAgents } });
61
+ assert.ok(result.output.includes('ID'));
62
+ assert.ok(result.output.includes('Status'));
63
+ assert.ok(result.output.includes('Model'));
64
+ });
65
+ });
66
+
67
+ describe('filterAgents', () => {
68
+ const agents = [
69
+ { id: '1', status: 'running', model: 'claude', startTime: new Date() },
70
+ { id: '2', status: 'completed', model: 'gpt-4', startTime: new Date(Date.now() - 3600000) },
71
+ ];
72
+
73
+ it('filters by status', () => {
74
+ const result = filterAgents(agents, { status: 'running' });
75
+ assert.strictEqual(result.length, 1);
76
+ assert.strictEqual(result[0].id, '1');
77
+ });
78
+
79
+ it('filters by model', () => {
80
+ const result = filterAgents(agents, { model: 'gpt-4' });
81
+ assert.strictEqual(result.length, 1);
82
+ assert.strictEqual(result[0].id, '2');
83
+ });
84
+
85
+ it('filters by time since', () => {
86
+ const result = filterAgents(agents, { since: '30m' });
87
+ assert.strictEqual(result.length, 1);
88
+ assert.strictEqual(result[0].id, '1');
89
+ });
90
+
91
+ it('returns all when no filters', () => {
92
+ const result = filterAgents(agents, {});
93
+ assert.strictEqual(result.length, 2);
94
+ });
95
+ });
96
+
97
+ describe('formatTable', () => {
98
+ const agents = [
99
+ { id: 'agent-1', name: 'Builder', model: 'claude', status: 'running', cost: 0.05 },
100
+ { id: 'agent-2', name: 'Very Long Agent Name That Exceeds Normal Width', model: 'gpt-4', status: 'completed', cost: 0.10 },
101
+ ];
102
+
103
+ it('creates readable output', () => {
104
+ const result = formatTable(agents);
105
+ assert.ok(result.includes('agent-1'));
106
+ assert.ok(result.includes('running'));
107
+ });
108
+
109
+ it('truncates long values', () => {
110
+ const result = formatTable(agents, { maxWidth: 80 });
111
+ assert.ok(!result.includes('Very Long Agent Name That Exceeds Normal Width'));
112
+ assert.ok(result.includes('...') || result.includes('Very Long'));
113
+ });
114
+
115
+ it('respects terminal width', () => {
116
+ const result = formatTable(agents, { maxWidth: 60 });
117
+ const lines = result.split('\n');
118
+ const maxLine = Math.max(...lines.map(l => l.length));
119
+ assert.ok(maxLine <= 80); // Some buffer for formatting
120
+ });
121
+
122
+ it('handles empty list', () => {
123
+ const result = formatTable([]);
124
+ assert.ok(result.includes('No agents'));
125
+ });
126
+ });
127
+
128
+ describe('formatJSON', () => {
129
+ const agents = [
130
+ { id: 'agent-1', name: 'Builder', model: 'claude', status: 'running' },
131
+ ];
132
+
133
+ it('creates valid JSON', () => {
134
+ const result = formatJSON(agents);
135
+ const parsed = JSON.parse(result);
136
+ assert.ok(Array.isArray(parsed));
137
+ assert.strictEqual(parsed[0].id, 'agent-1');
138
+ });
139
+
140
+ it('includes all fields', () => {
141
+ const result = formatJSON(agents);
142
+ const parsed = JSON.parse(result);
143
+ assert.ok('id' in parsed[0]);
144
+ assert.ok('name' in parsed[0]);
145
+ assert.ok('model' in parsed[0]);
146
+ assert.ok('status' in parsed[0]);
147
+ });
148
+ });
149
+ });
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Agents Logs Command
3
+ * View agent output and logs
4
+ */
5
+
6
+ /**
7
+ * Format a single log line
8
+ * @param {object} entry - Log entry
9
+ * @param {object} options - Format options
10
+ * @returns {string} Formatted line
11
+ */
12
+ function formatLogLine(entry, options = {}) {
13
+ const parts = [];
14
+
15
+ if (options.timestamps) {
16
+ const time = new Date(entry.timestamp).toLocaleTimeString();
17
+ parts.push(`[${time}]`);
18
+ }
19
+
20
+ if (entry.stream === 'stderr') {
21
+ parts.push('[ERR]');
22
+ }
23
+
24
+ parts.push(entry.message);
25
+
26
+ return parts.join(' ');
27
+ }
28
+
29
+ /**
30
+ * Format all logs
31
+ * @param {Array} logs - Log entries
32
+ * @param {object} options - Format options
33
+ * @returns {string} Formatted output
34
+ */
35
+ function formatLogs(logs, options = {}) {
36
+ if (!logs || logs.length === 0) {
37
+ return 'No logs available';
38
+ }
39
+
40
+ return logs.map(entry => formatLogLine(entry, options)).join('\n');
41
+ }
42
+
43
+ /**
44
+ * Get last n log entries
45
+ * @param {Array} logs - All logs
46
+ * @param {number} n - Number of entries
47
+ * @returns {Array} Last n entries
48
+ */
49
+ function tailLogs(logs, n) {
50
+ if (!logs || logs.length === 0) return [];
51
+ if (logs.length <= n) return logs;
52
+ return logs.slice(-n);
53
+ }
54
+
55
+ /**
56
+ * Setup streaming for live logs
57
+ * @param {object} agent - Agent to stream
58
+ * @returns {object} Streaming info
59
+ */
60
+ function streamLogs(agent) {
61
+ return {
62
+ streaming: true,
63
+ agentId: agent.id,
64
+ status: agent.status,
65
+ currentLogs: agent.logs || [],
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Execute agents logs command
71
+ * @param {object} context - Execution context
72
+ * @returns {Promise<object>} Command result
73
+ */
74
+ async function execute(context) {
75
+ const { registry, agentId, options = {} } = context;
76
+
77
+ const agent = registry.getAgent(agentId);
78
+
79
+ if (!agent) {
80
+ return {
81
+ success: false,
82
+ error: `Agent not found: ${agentId}`,
83
+ };
84
+ }
85
+
86
+ // Handle streaming mode
87
+ if (options.follow && agent.status === 'running') {
88
+ return {
89
+ success: true,
90
+ ...streamLogs(agent),
91
+ output: formatLogs(agent.logs || [], options),
92
+ };
93
+ }
94
+
95
+ // Get logs
96
+ let logs = agent.logs || [];
97
+
98
+ // Apply tail
99
+ if (options.tail) {
100
+ logs = tailLogs(logs, options.tail);
101
+ }
102
+
103
+ // Format output
104
+ const output = formatLogs(logs, {
105
+ timestamps: options.timestamps !== false,
106
+ color: options.color,
107
+ });
108
+
109
+ return {
110
+ success: true,
111
+ agent: {
112
+ id: agent.id,
113
+ status: agent.status,
114
+ },
115
+ logCount: logs.length,
116
+ output,
117
+ };
118
+ }
119
+
120
+ module.exports = {
121
+ execute,
122
+ formatLogLine,
123
+ formatLogs,
124
+ streamLogs,
125
+ tailLogs,
126
+ };
@@ -0,0 +1,198 @@
1
+ const { describe, it, beforeEach, mock } = require('node:test');
2
+ const assert = require('node:assert');
3
+ const {
4
+ execute,
5
+ formatLogLine,
6
+ formatLogs,
7
+ streamLogs,
8
+ tailLogs,
9
+ } = require('./agents-logs-command.js');
10
+
11
+ describe('agents-logs-command', () => {
12
+ const agentWithLogs = {
13
+ id: 'agent-1',
14
+ status: 'completed',
15
+ logs: [
16
+ { timestamp: new Date('2024-01-01T10:00:00'), stream: 'stdout', message: 'Starting task...' },
17
+ { timestamp: new Date('2024-01-01T10:00:01'), stream: 'stdout', message: 'Processing...' },
18
+ { timestamp: new Date('2024-01-01T10:00:02'), stream: 'stderr', message: 'Warning: rate limit' },
19
+ { timestamp: new Date('2024-01-01T10:00:03'), stream: 'stdout', message: 'Complete!' },
20
+ ],
21
+ };
22
+
23
+ describe('execute', () => {
24
+ const createMockRegistry = (agents = []) => ({
25
+ getAgent: (id) => agents.find(a => a.id === id),
26
+ });
27
+
28
+ it('shows agent output', async () => {
29
+ const result = await execute({
30
+ registry: createMockRegistry([agentWithLogs]),
31
+ agentId: 'agent-1',
32
+ });
33
+ assert.ok(result.success);
34
+ assert.ok(result.output.includes('Starting'));
35
+ assert.ok(result.output.includes('Complete'));
36
+ });
37
+
38
+ it('with --follow streams', async () => {
39
+ const result = await execute({
40
+ registry: createMockRegistry([{ ...agentWithLogs, status: 'running' }]),
41
+ agentId: 'agent-1',
42
+ options: { follow: true },
43
+ });
44
+ assert.ok(result.streaming || result.output);
45
+ });
46
+
47
+ it('with --tail limits lines', async () => {
48
+ const result = await execute({
49
+ registry: createMockRegistry([agentWithLogs]),
50
+ agentId: 'agent-1',
51
+ options: { tail: 2 },
52
+ });
53
+ const lines = result.output.split('\n').filter(l => l.trim());
54
+ assert.ok(lines.length <= 2);
55
+ });
56
+
57
+ it('shows stdout content', async () => {
58
+ const result = await execute({
59
+ registry: createMockRegistry([agentWithLogs]),
60
+ agentId: 'agent-1',
61
+ });
62
+ assert.ok(result.output.includes('Starting task'));
63
+ });
64
+
65
+ it('shows stderr content', async () => {
66
+ const result = await execute({
67
+ registry: createMockRegistry([agentWithLogs]),
68
+ agentId: 'agent-1',
69
+ });
70
+ assert.ok(result.output.includes('Warning'));
71
+ });
72
+
73
+ it('timestamps are prefixed', async () => {
74
+ const result = await execute({
75
+ registry: createMockRegistry([agentWithLogs]),
76
+ agentId: 'agent-1',
77
+ options: { timestamps: true },
78
+ });
79
+ assert.ok(result.output.includes('10:00') || result.output.includes(':'));
80
+ });
81
+
82
+ it('errors are colored red', async () => {
83
+ const result = await execute({
84
+ registry: createMockRegistry([agentWithLogs]),
85
+ agentId: 'agent-1',
86
+ options: { color: true },
87
+ });
88
+ // Check for ANSI red code or stderr marker
89
+ assert.ok(result.output.includes('stderr') || result.output.includes('\x1b[31m') || result.output.includes('Warning'));
90
+ });
91
+
92
+ it('handles completed agent', async () => {
93
+ const result = await execute({
94
+ registry: createMockRegistry([agentWithLogs]),
95
+ agentId: 'agent-1',
96
+ });
97
+ assert.ok(result.success);
98
+ });
99
+
100
+ it('handles no output yet', async () => {
101
+ const result = await execute({
102
+ registry: createMockRegistry([{ id: 'agent-1', status: 'running', logs: [] }]),
103
+ agentId: 'agent-1',
104
+ });
105
+ assert.ok(result.output.includes('No logs') || result.output === '');
106
+ });
107
+
108
+ it('handles unknown agent ID', async () => {
109
+ const result = await execute({
110
+ registry: createMockRegistry([]),
111
+ agentId: 'unknown',
112
+ });
113
+ assert.strictEqual(result.success, false);
114
+ assert.ok(result.error.includes('not found'));
115
+ });
116
+ });
117
+
118
+ describe('formatLogLine', () => {
119
+ it('formats with timestamp', () => {
120
+ const line = formatLogLine({
121
+ timestamp: new Date('2024-01-01T10:00:00'),
122
+ stream: 'stdout',
123
+ message: 'Test',
124
+ }, { timestamps: true });
125
+ assert.ok(line.includes('10:00'));
126
+ assert.ok(line.includes('Test'));
127
+ });
128
+
129
+ it('formats without timestamp', () => {
130
+ const line = formatLogLine({
131
+ timestamp: new Date(),
132
+ stream: 'stdout',
133
+ message: 'Test',
134
+ }, { timestamps: false });
135
+ assert.strictEqual(line, 'Test');
136
+ });
137
+
138
+ it('marks stderr', () => {
139
+ const line = formatLogLine({
140
+ timestamp: new Date(),
141
+ stream: 'stderr',
142
+ message: 'Error',
143
+ }, { timestamps: true });
144
+ assert.ok(line.includes('stderr') || line.includes('[ERR]'));
145
+ });
146
+ });
147
+
148
+ describe('formatLogs', () => {
149
+ it('formats all log lines', () => {
150
+ const logs = [
151
+ { timestamp: new Date(), stream: 'stdout', message: 'Line 1' },
152
+ { timestamp: new Date(), stream: 'stdout', message: 'Line 2' },
153
+ ];
154
+ const result = formatLogs(logs, {});
155
+ assert.ok(result.includes('Line 1'));
156
+ assert.ok(result.includes('Line 2'));
157
+ });
158
+
159
+ it('returns empty message for no logs', () => {
160
+ const result = formatLogs([], {});
161
+ assert.ok(result.includes('No logs') || result === '');
162
+ });
163
+ });
164
+
165
+ describe('tailLogs', () => {
166
+ const logs = [
167
+ { message: 'Line 1' },
168
+ { message: 'Line 2' },
169
+ { message: 'Line 3' },
170
+ { message: 'Line 4' },
171
+ ];
172
+
173
+ it('returns last n entries', () => {
174
+ const result = tailLogs(logs, 2);
175
+ assert.strictEqual(result.length, 2);
176
+ assert.strictEqual(result[0].message, 'Line 3');
177
+ assert.strictEqual(result[1].message, 'Line 4');
178
+ });
179
+
180
+ it('returns all if fewer than n', () => {
181
+ const result = tailLogs(logs, 10);
182
+ assert.strictEqual(result.length, 4);
183
+ });
184
+ });
185
+
186
+ describe('streamLogs', () => {
187
+ it('returns stream info', () => {
188
+ const result = streamLogs({ id: 'agent-1', status: 'running' });
189
+ assert.ok(result.streaming);
190
+ assert.ok(result.agentId);
191
+ });
192
+
193
+ it('includes current logs', () => {
194
+ const result = streamLogs({ id: 'agent-1', status: 'running', logs: [{ message: 'test' }] });
195
+ assert.ok(result.currentLogs);
196
+ });
197
+ });
198
+ });