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.
- package/CLAUDE.md +23 -0
- package/CODING-STANDARDS.md +408 -0
- package/bin/install.js +2 -0
- package/dashboard/dist/components/QualityGatePane.d.ts +38 -0
- package/dashboard/dist/components/QualityGatePane.js +31 -0
- package/dashboard/dist/components/QualityGatePane.test.d.ts +1 -0
- package/dashboard/dist/components/QualityGatePane.test.js +147 -0
- package/dashboard/dist/components/orchestration/AgentCard.d.ts +26 -0
- package/dashboard/dist/components/orchestration/AgentCard.js +60 -0
- package/dashboard/dist/components/orchestration/AgentCard.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/AgentCard.test.js +63 -0
- package/dashboard/dist/components/orchestration/AgentControls.d.ts +11 -0
- package/dashboard/dist/components/orchestration/AgentControls.js +20 -0
- package/dashboard/dist/components/orchestration/AgentControls.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/AgentControls.test.js +52 -0
- package/dashboard/dist/components/orchestration/AgentDetail.d.ts +35 -0
- package/dashboard/dist/components/orchestration/AgentDetail.js +37 -0
- package/dashboard/dist/components/orchestration/AgentDetail.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/AgentDetail.test.js +79 -0
- package/dashboard/dist/components/orchestration/AgentList.d.ts +31 -0
- package/dashboard/dist/components/orchestration/AgentList.js +47 -0
- package/dashboard/dist/components/orchestration/AgentList.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/AgentList.test.js +64 -0
- package/dashboard/dist/components/orchestration/CostMeter.d.ts +11 -0
- package/dashboard/dist/components/orchestration/CostMeter.js +28 -0
- package/dashboard/dist/components/orchestration/CostMeter.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/CostMeter.test.js +50 -0
- package/dashboard/dist/components/orchestration/ModelSelector.d.ts +20 -0
- package/dashboard/dist/components/orchestration/ModelSelector.js +12 -0
- package/dashboard/dist/components/orchestration/ModelSelector.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/ModelSelector.test.js +56 -0
- package/dashboard/dist/components/orchestration/OrchestrationDashboard.d.ts +28 -0
- package/dashboard/dist/components/orchestration/OrchestrationDashboard.js +28 -0
- package/dashboard/dist/components/orchestration/OrchestrationDashboard.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/OrchestrationDashboard.test.js +56 -0
- package/dashboard/dist/components/orchestration/QualityIndicator.d.ts +11 -0
- package/dashboard/dist/components/orchestration/QualityIndicator.js +37 -0
- package/dashboard/dist/components/orchestration/QualityIndicator.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/QualityIndicator.test.js +52 -0
- package/dashboard/dist/components/orchestration/index.d.ts +8 -0
- package/dashboard/dist/components/orchestration/index.js +8 -0
- package/package.json +1 -1
- package/server/lib/access-control.js +352 -0
- package/server/lib/access-control.test.js +322 -0
- package/server/lib/agents-cancel-command.js +139 -0
- package/server/lib/agents-cancel-command.test.js +180 -0
- package/server/lib/agents-get-command.js +159 -0
- package/server/lib/agents-get-command.test.js +167 -0
- package/server/lib/agents-list-command.js +150 -0
- package/server/lib/agents-list-command.test.js +149 -0
- package/server/lib/agents-logs-command.js +126 -0
- package/server/lib/agents-logs-command.test.js +198 -0
- package/server/lib/agents-retry-command.js +117 -0
- package/server/lib/agents-retry-command.test.js +192 -0
- package/server/lib/budget-limits.js +222 -0
- package/server/lib/budget-limits.test.js +214 -0
- package/server/lib/code-generator.js +291 -0
- package/server/lib/code-generator.test.js +307 -0
- package/server/lib/cost-command.js +290 -0
- package/server/lib/cost-command.test.js +202 -0
- package/server/lib/cost-optimizer.js +404 -0
- package/server/lib/cost-optimizer.test.js +232 -0
- package/server/lib/cost-projections.js +302 -0
- package/server/lib/cost-projections.test.js +217 -0
- package/server/lib/cost-reports.js +277 -0
- package/server/lib/cost-reports.test.js +254 -0
- package/server/lib/cost-tracker.js +216 -0
- package/server/lib/cost-tracker.test.js +302 -0
- package/server/lib/crypto-patterns.js +433 -0
- package/server/lib/crypto-patterns.test.js +346 -0
- package/server/lib/design-command.js +385 -0
- package/server/lib/design-command.test.js +249 -0
- package/server/lib/design-parser.js +237 -0
- package/server/lib/design-parser.test.js +290 -0
- package/server/lib/gemini-vision.js +377 -0
- package/server/lib/gemini-vision.test.js +282 -0
- package/server/lib/input-validator.js +360 -0
- package/server/lib/input-validator.test.js +295 -0
- package/server/lib/litellm-client.js +232 -0
- package/server/lib/litellm-client.test.js +267 -0
- package/server/lib/litellm-command.js +291 -0
- package/server/lib/litellm-command.test.js +260 -0
- package/server/lib/litellm-config.js +273 -0
- package/server/lib/litellm-config.test.js +212 -0
- package/server/lib/model-pricing.js +189 -0
- package/server/lib/model-pricing.test.js +178 -0
- package/server/lib/models-command.js +223 -0
- package/server/lib/models-command.test.js +193 -0
- package/server/lib/optimize-command.js +197 -0
- package/server/lib/optimize-command.test.js +193 -0
- package/server/lib/orchestration-integration.js +206 -0
- package/server/lib/orchestration-integration.test.js +235 -0
- package/server/lib/output-encoder.js +308 -0
- package/server/lib/output-encoder.test.js +312 -0
- package/server/lib/quality-evaluator.js +396 -0
- package/server/lib/quality-evaluator.test.js +337 -0
- package/server/lib/quality-gate-command.js +340 -0
- package/server/lib/quality-gate-command.test.js +321 -0
- package/server/lib/quality-gate-scorer.js +378 -0
- package/server/lib/quality-gate-scorer.test.js +376 -0
- package/server/lib/quality-history.js +265 -0
- package/server/lib/quality-history.test.js +359 -0
- package/server/lib/quality-presets.js +288 -0
- package/server/lib/quality-presets.test.js +269 -0
- package/server/lib/quality-retry.js +323 -0
- package/server/lib/quality-retry.test.js +325 -0
- package/server/lib/quality-thresholds.js +255 -0
- package/server/lib/quality-thresholds.test.js +237 -0
- package/server/lib/secure-auth.js +333 -0
- package/server/lib/secure-auth.test.js +288 -0
- package/server/lib/secure-code-command.js +540 -0
- package/server/lib/secure-code-command.test.js +309 -0
- package/server/lib/secure-errors.js +521 -0
- package/server/lib/secure-errors.test.js +298 -0
- package/server/lib/vision-command.js +372 -0
- package/server/lib/vision-command.test.js +255 -0
- package/server/lib/visual-command.js +350 -0
- package/server/lib/visual-command.test.js +256 -0
- package/server/lib/visual-testing.js +315 -0
- package/server/lib/visual-testing.test.js +357 -0
- package/server/package-lock.json +2 -2
- 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
|
+
});
|