skimpyclaw 0.3.6 → 0.3.9
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/README.md +14 -6
- package/dist/__tests__/api.test.js +1 -0
- package/dist/__tests__/channels.test.js +1 -1
- package/dist/__tests__/code-agents-orchestrator.test.js +74 -7
- package/dist/__tests__/code-agents-preflight.test.d.ts +1 -0
- package/dist/__tests__/code-agents-preflight.test.js +88 -0
- package/dist/__tests__/code-agents-sandbox.test.d.ts +1 -0
- package/dist/__tests__/code-agents-sandbox.test.js +163 -0
- package/dist/__tests__/code-agents-utils.test.js +12 -1
- package/dist/__tests__/context-manager.test.d.ts +1 -0
- package/dist/__tests__/context-manager.test.js +236 -0
- package/dist/__tests__/package-manager-detection.test.js +5 -5
- package/dist/__tests__/setup.test.js +7 -5
- package/dist/__tests__/skills.test.js +2 -2
- package/dist/__tests__/structured-context.test.d.ts +1 -0
- package/dist/__tests__/structured-context.test.js +100 -0
- package/dist/__tests__/tools.test.js +65 -3
- package/dist/agent.js +4 -5
- package/dist/api.js +10 -58
- package/dist/audit.js +5 -51
- package/dist/channels/telegram/handlers.js +2 -60
- package/dist/channels/telegram/index.js +0 -7
- package/dist/channels.js +1 -1
- package/dist/cli.js +151 -16
- package/dist/code-agents/executor.d.ts +9 -4
- package/dist/code-agents/executor.js +187 -13
- package/dist/code-agents/index.d.ts +1 -1
- package/dist/code-agents/index.js +30 -22
- package/dist/code-agents/orchestrator.d.ts +8 -2
- package/dist/code-agents/orchestrator.js +318 -27
- package/dist/code-agents/structured-context.d.ts +7 -0
- package/dist/code-agents/structured-context.js +54 -0
- package/dist/code-agents/types.d.ts +2 -0
- package/dist/code-agents/utils.d.ts +4 -0
- package/dist/code-agents/utils.js +38 -2
- package/dist/code-agents/worktree.d.ts +40 -0
- package/dist/code-agents/worktree.js +215 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +5 -3
- package/dist/cron.js +18 -4
- package/dist/dashboard/assets/{index-CkonC7Cd.js → index-BoTHPby4.js} +20 -20
- package/dist/dashboard/assets/{index-EAg6lqF5.css → index-D4mufvBg.css} +1 -1
- package/dist/dashboard/index.html +2 -2
- package/dist/discord.js +4 -40
- package/dist/exec-approval.js +1 -1
- package/dist/file-lock.js +1 -1
- package/dist/gateway.js +3 -10
- package/dist/providers/anthropic.js +9 -5
- package/dist/providers/codex.js +10 -6
- package/dist/providers/context-manager.d.ts +22 -0
- package/dist/providers/context-manager.js +100 -0
- package/dist/providers/openai.js +9 -5
- package/dist/providers/types.d.ts +1 -0
- package/dist/security.js +9 -0
- package/dist/setup.js +122 -27
- package/dist/skills.js +9 -2
- package/dist/subagent.js +33 -2
- package/dist/tools/bash-tool.js +8 -0
- package/dist/tools/browser-tool.js +2 -1
- package/dist/tools/definitions.d.ts +0 -27
- package/dist/tools/definitions.js +0 -18
- package/dist/tools/execute-context.d.ts +4 -4
- package/dist/tools/file-tools.d.ts +1 -1
- package/dist/tools/file-tools.js +1 -1
- package/dist/tools.d.ts +5 -5
- package/dist/tools.js +87 -98
- package/dist/types.d.ts +14 -22
- package/dist/usage.d.ts +1 -0
- package/dist/usage.js +30 -46
- package/dist/utils.d.ts +18 -0
- package/dist/utils.js +71 -0
- package/dist/voice.js +9 -7
- package/package.json +26 -21
package/dist/api.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
// Dashboard API endpoints
|
|
2
2
|
import { readFileSync, writeFileSync, existsSync, readdirSync, statSync, mkdirSync, rmSync } from 'fs';
|
|
3
|
-
import {
|
|
3
|
+
import { validateBearerToken } from './utils.js';
|
|
4
4
|
import { join, basename, resolve } from 'path';
|
|
5
5
|
import { homedir } from 'os';
|
|
6
|
-
import { loadConfig, loadRawConfig, saveConfig, getSessionsDir, getLogsDir, getAgentDir, listMemoryFiles, readMemoryFile, } from './config.js';
|
|
6
|
+
import { loadConfig, loadRawConfig, saveConfig, getSessionsDir, getLogsDir, getAgentDir, isValidAgentId, listMemoryFiles, readMemoryFile, } from './config.js';
|
|
7
7
|
import { TEMPLATE_FILES, getAgentTemplateContent, saveAgentTemplate, } from './agent.js';
|
|
8
8
|
import { getCronJobs, getCronJobDetails, runCronJob } from './cron.js';
|
|
9
9
|
import { getCurrentModel, setCurrentModel, getLastMessage, setGatewayConfig } from './gateway.js';
|
|
10
10
|
import { redactSecrets } from './security.js';
|
|
11
|
-
import { getActiveTasks, getRecentTasks } from './subagent.js';
|
|
12
11
|
import { readAuditTraces } from './audit.js';
|
|
13
12
|
import { getUsageSummary, readUsageRecords } from './usage.js';
|
|
14
13
|
import { getAllCodeAgents, getCodeAgent, cancelCodeAgent } from './tools.js';
|
|
@@ -36,10 +35,7 @@ const DEFAULT_MODEL_ALIASES = {
|
|
|
36
35
|
function validateFilename(filename) {
|
|
37
36
|
return !filename.includes('..') && filename === basename(filename);
|
|
38
37
|
}
|
|
39
|
-
|
|
40
|
-
// Agent IDs should be simple identifiers: alphanumeric, hyphens, underscores
|
|
41
|
-
return /^[a-zA-Z0-9_-]+$/.test(agentId);
|
|
42
|
-
}
|
|
38
|
+
// Use isValidAgentId from config.ts
|
|
43
39
|
function validateSkillName(name) {
|
|
44
40
|
return /^[a-zA-Z0-9-]+$/.test(name) && name.length <= 100;
|
|
45
41
|
}
|
|
@@ -98,30 +94,14 @@ export function registerDashboardAPI(fastify, config) {
|
|
|
98
94
|
if (!token) {
|
|
99
95
|
return; // No token configured, allow access
|
|
100
96
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return reply.code(401).send({ error: 'Unauthorized: Bearer token required' });
|
|
104
|
-
}
|
|
105
|
-
const providedToken = authHeader.slice(7);
|
|
106
|
-
// Timing-safe comparison to prevent token extraction via timing attacks
|
|
107
|
-
const tokenBuf = Buffer.from(token, 'utf8');
|
|
108
|
-
const providedBuf = Buffer.from(providedToken, 'utf8');
|
|
109
|
-
if (tokenBuf.length !== providedBuf.length || !timingSafeEqual(tokenBuf, providedBuf)) {
|
|
110
|
-
return reply.code(401).send({ error: 'Unauthorized: Invalid token' });
|
|
97
|
+
if (!validateBearerToken(token, request.headers.authorization)) {
|
|
98
|
+
return reply.code(401).send({ error: 'Unauthorized: Invalid or missing token' });
|
|
111
99
|
}
|
|
112
100
|
});
|
|
113
101
|
// --- Status ---
|
|
114
102
|
fastify.get('/api/dashboard/status', async () => {
|
|
115
103
|
const jobs = getCronJobs();
|
|
116
104
|
const uptime = process.uptime();
|
|
117
|
-
const activeTasks = getActiveTasks();
|
|
118
|
-
const recentTasks = getRecentTasks(20);
|
|
119
|
-
const pendingCount = activeTasks.filter(t => t.status === 'pending').length;
|
|
120
|
-
const runningCount = activeTasks.filter(t => t.status === 'running').length;
|
|
121
|
-
const recentCompleted = recentTasks.filter(t => t.status === 'completed').length;
|
|
122
|
-
const recentFailed = recentTasks.filter(t => t.status === 'failed').length;
|
|
123
|
-
const recentCancelled = recentTasks.filter(t => t.status === 'cancelled').length;
|
|
124
|
-
const maxConcurrent = runtimeConfig.subagents?.maxConcurrent ?? 5;
|
|
125
105
|
return {
|
|
126
106
|
uptime,
|
|
127
107
|
model: getCurrentModel(),
|
|
@@ -129,34 +109,6 @@ export function registerDashboardAPI(fastify, config) {
|
|
|
129
109
|
lastMessage: getLastMessage(),
|
|
130
110
|
activeChannel: getActiveChannelId() ?? runtimeConfig.channels.active ?? null,
|
|
131
111
|
cronJobs: jobs,
|
|
132
|
-
subagents: {
|
|
133
|
-
maxConcurrent,
|
|
134
|
-
active: activeTasks.length,
|
|
135
|
-
running: runningCount,
|
|
136
|
-
pending: pendingCount,
|
|
137
|
-
recentTotal: recentTasks.length,
|
|
138
|
-
recentCompleted,
|
|
139
|
-
recentFailed,
|
|
140
|
-
recentCancelled,
|
|
141
|
-
},
|
|
142
|
-
activeSubagents: activeTasks
|
|
143
|
-
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
|
|
144
|
-
.map(task => {
|
|
145
|
-
const started = task.startedAt || task.createdAt;
|
|
146
|
-
return {
|
|
147
|
-
id: task.id,
|
|
148
|
-
type: task.type,
|
|
149
|
-
status: task.status,
|
|
150
|
-
model: task.model,
|
|
151
|
-
label: task.label,
|
|
152
|
-
promptPreview: task.prompt.slice(0, 120),
|
|
153
|
-
retryCount: task.retryCount ?? 0,
|
|
154
|
-
maxRetries: task.maxRetries ?? 2,
|
|
155
|
-
createdAt: task.createdAt,
|
|
156
|
-
startedAt: task.startedAt,
|
|
157
|
-
elapsedSeconds: Math.max(0, Math.round((Date.now() - started.getTime()) / 1000)),
|
|
158
|
-
};
|
|
159
|
-
}),
|
|
160
112
|
};
|
|
161
113
|
});
|
|
162
114
|
// --- Sessions ---
|
|
@@ -306,7 +258,7 @@ export function registerDashboardAPI(fastify, config) {
|
|
|
306
258
|
// --- Memory ---
|
|
307
259
|
fastify.get('/api/dashboard/memory/:agentId', async (request, reply) => {
|
|
308
260
|
const { agentId } = request.params;
|
|
309
|
-
if (!
|
|
261
|
+
if (!isValidAgentId(agentId)) {
|
|
310
262
|
return reply.code(400).send({ error: 'Invalid agent ID' });
|
|
311
263
|
}
|
|
312
264
|
const files = listMemoryFiles(agentId);
|
|
@@ -314,7 +266,7 @@ export function registerDashboardAPI(fastify, config) {
|
|
|
314
266
|
});
|
|
315
267
|
fastify.get('/api/dashboard/memory/:agentId/:filename', async (request, reply) => {
|
|
316
268
|
const { agentId, filename } = request.params;
|
|
317
|
-
if (!
|
|
269
|
+
if (!isValidAgentId(agentId)) {
|
|
318
270
|
return reply.code(400).send({ error: 'Invalid agent ID' });
|
|
319
271
|
}
|
|
320
272
|
// Special case: "curated" reads MEMORY.md from the agent root
|
|
@@ -455,7 +407,7 @@ export function registerDashboardAPI(fastify, config) {
|
|
|
455
407
|
// --- Templates ---
|
|
456
408
|
fastify.get('/api/dashboard/templates/:agentId', async (request, reply) => {
|
|
457
409
|
const { agentId } = request.params;
|
|
458
|
-
if (!
|
|
410
|
+
if (!isValidAgentId(agentId)) {
|
|
459
411
|
return reply.code(400).send({ error: 'Invalid agent ID' });
|
|
460
412
|
}
|
|
461
413
|
const agentDir = getAgentDir(agentId);
|
|
@@ -472,7 +424,7 @@ export function registerDashboardAPI(fastify, config) {
|
|
|
472
424
|
});
|
|
473
425
|
fastify.get('/api/dashboard/templates/:agentId/:name', async (request, reply) => {
|
|
474
426
|
const { agentId, name } = request.params;
|
|
475
|
-
if (!
|
|
427
|
+
if (!isValidAgentId(agentId)) {
|
|
476
428
|
return reply.code(400).send({ error: 'Invalid agent ID' });
|
|
477
429
|
}
|
|
478
430
|
const content = getAgentTemplateContent(agentId, name);
|
|
@@ -483,7 +435,7 @@ export function registerDashboardAPI(fastify, config) {
|
|
|
483
435
|
});
|
|
484
436
|
fastify.put('/api/dashboard/templates/:agentId/:name', async (request, reply) => {
|
|
485
437
|
const { agentId, name } = request.params;
|
|
486
|
-
if (!
|
|
438
|
+
if (!isValidAgentId(agentId)) {
|
|
487
439
|
return reply.code(400).send({ error: 'Invalid agent ID' });
|
|
488
440
|
}
|
|
489
441
|
const { content } = request.body;
|
package/dist/audit.js
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
// Audit log reader/writer for ~/.skimpyclaw/logs/audit/YYYY-MM-DD.jsonl
|
|
2
2
|
import { randomUUID } from 'crypto';
|
|
3
|
-
import {
|
|
3
|
+
import { appendFileSync, existsSync, mkdirSync } from 'fs';
|
|
4
4
|
import { join } from 'path';
|
|
5
5
|
import { homedir } from 'os';
|
|
6
|
+
import { formatDate, readJsonlDir } from './utils.js';
|
|
6
7
|
const AUDIT_DIR = join(homedir(), '.skimpyclaw', 'logs', 'audit');
|
|
7
|
-
function formatDate(date) {
|
|
8
|
-
const y = date.getFullYear();
|
|
9
|
-
const m = String(date.getMonth() + 1).padStart(2, '0');
|
|
10
|
-
const d = String(date.getDate()).padStart(2, '0');
|
|
11
|
-
return `${y}-${m}-${d}`;
|
|
12
|
-
}
|
|
13
8
|
// --- In-memory trace lifecycle ---
|
|
14
9
|
/** Active traces keyed by traceId */
|
|
15
10
|
const activeTraces = new Map();
|
|
@@ -60,52 +55,11 @@ export async function readAuditTraces(options = {}) {
|
|
|
60
55
|
const limit = options.limit ?? 50;
|
|
61
56
|
const offset = options.offset ?? 0;
|
|
62
57
|
const triggerFilter = options.trigger;
|
|
63
|
-
// Determine date range
|
|
64
58
|
const endDate = options.endDate ?? new Date();
|
|
65
|
-
const startDate = options.startDate ?? new Date(endDate.getTime() - 90 * 24 * 60 * 60 * 1000);
|
|
66
|
-
|
|
67
|
-
if (!existsSync(AUDIT_DIR)) {
|
|
68
|
-
return { traces: [], total: 0 };
|
|
69
|
-
}
|
|
70
|
-
const files = readdirSync(AUDIT_DIR)
|
|
71
|
-
.filter(f => f.endsWith('.jsonl'))
|
|
72
|
-
.sort()
|
|
73
|
-
.reverse(); // Newest first
|
|
74
|
-
const startStr = formatDate(startDate);
|
|
75
|
-
const endStr = formatDate(endDate);
|
|
76
|
-
// Collect all matching traces
|
|
77
|
-
const allTraces = [];
|
|
78
|
-
for (const file of files) {
|
|
79
|
-
const dateStr = file.replace('.jsonl', '');
|
|
80
|
-
if (dateStr < startStr || dateStr > endStr)
|
|
81
|
-
continue;
|
|
82
|
-
const filePath = join(AUDIT_DIR, file);
|
|
83
|
-
try {
|
|
84
|
-
const content = readFileSync(filePath, 'utf-8');
|
|
85
|
-
const lines = content.trim().split('\n').filter(Boolean);
|
|
86
|
-
// Parse lines in reverse (newest first within file)
|
|
87
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
88
|
-
try {
|
|
89
|
-
const trace = JSON.parse(lines[i]);
|
|
90
|
-
if (triggerFilter && trace.trigger !== triggerFilter)
|
|
91
|
-
continue;
|
|
92
|
-
allTraces.push(trace);
|
|
93
|
-
}
|
|
94
|
-
catch {
|
|
95
|
-
// Skip malformed lines
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
catch {
|
|
100
|
-
// Skip unreadable files
|
|
101
|
-
}
|
|
102
|
-
}
|
|
59
|
+
const startDate = options.startDate ?? new Date(endDate.getTime() - 90 * 24 * 60 * 60 * 1000);
|
|
60
|
+
const allTraces = readJsonlDir(AUDIT_DIR, formatDate(startDate), formatDate(endDate), triggerFilter ? (t) => t.trigger === triggerFilter : undefined);
|
|
103
61
|
// Sort newest first by startedAt
|
|
104
|
-
allTraces.sort((a, b) =>
|
|
105
|
-
const dateA = new Date(b.startedAt || b.endedAt).getTime();
|
|
106
|
-
const dateB = new Date(a.startedAt || a.endedAt).getTime();
|
|
107
|
-
return dateA - dateB;
|
|
108
|
-
});
|
|
62
|
+
allTraces.sort((a, b) => Date.parse(b.startedAt || b.endedAt) - Date.parse(a.startedAt || a.endedAt));
|
|
109
63
|
const total = allTraces.length;
|
|
110
64
|
const paged = allTraces.slice(offset, offset + limit);
|
|
111
65
|
return { traces: paged, total };
|
|
@@ -4,7 +4,6 @@ import { spawnSync } from 'child_process';
|
|
|
4
4
|
import { getCurrentModel, setCurrentModel, getLastMessage } from '../../gateway.js';
|
|
5
5
|
import { getCronJobs, runCronJob } from '../../cron.js';
|
|
6
6
|
import { runHeartbeatCheck } from '../../heartbeat.js';
|
|
7
|
-
import { cancelTask, getActiveTasks, getRecentTasks } from '../../subagent.js';
|
|
8
7
|
import { getActiveCodeAgents, getRecentCodeAgents } from '../../code-agents/index.js';
|
|
9
8
|
import { listApprovals, approveRequest, denyRequest, getApproval, onApprovalEvent } from '../../exec-approval.js';
|
|
10
9
|
import { loadSkills } from '../../skills.js';
|
|
@@ -47,28 +46,9 @@ export async function handleStatus(ctx, cfg) {
|
|
|
47
46
|
const model = getCurrentModel();
|
|
48
47
|
const last = getLastMessage();
|
|
49
48
|
const jobs = getCronJobs();
|
|
50
|
-
const activeTasks = getActiveTasks();
|
|
51
|
-
const recentTasks = getRecentTasks(20);
|
|
52
49
|
const jobList = jobs
|
|
53
50
|
.map((j) => ` - ${j.name}: ${j.nextRun?.toLocaleString() || 'unknown'}`)
|
|
54
51
|
.join('\n');
|
|
55
|
-
const pendingCount = activeTasks.filter((t) => t.status === 'pending').length;
|
|
56
|
-
const runningCount = activeTasks.filter((t) => t.status === 'running').length;
|
|
57
|
-
const maxConcurrent = cfg.subagents?.maxConcurrent ?? 5;
|
|
58
|
-
const recentCompleted = recentTasks.filter((t) => t.status === 'completed').length;
|
|
59
|
-
const recentFailed = recentTasks.filter((t) => t.status === 'failed').length;
|
|
60
|
-
const recentCancelled = recentTasks.filter((t) => t.status === 'cancelled').length;
|
|
61
|
-
const activePreview = activeTasks
|
|
62
|
-
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
|
|
63
|
-
.slice(0, 3)
|
|
64
|
-
.map((task) => {
|
|
65
|
-
const started = task.startedAt || task.createdAt;
|
|
66
|
-
const elapsedSeconds = Math.max(0, Math.round((Date.now() - started.getTime()) / 1000));
|
|
67
|
-
const elapsed = elapsedSeconds < 60 ? `${elapsedSeconds}s` : `${Math.round(elapsedSeconds / 60)}m`;
|
|
68
|
-
const label = task.label ? ` (${task.label})` : '';
|
|
69
|
-
return ` - ${task.id} [${task.type}] ${task.status}${label} • ${elapsed}`;
|
|
70
|
-
})
|
|
71
|
-
.join('\n');
|
|
72
52
|
// Coding agents status (multi-agent)
|
|
73
53
|
const caActive = getActiveCodeAgents();
|
|
74
54
|
const caRecent = getRecentCodeAgents(20);
|
|
@@ -109,9 +89,6 @@ export async function handleStatus(ctx, cfg) {
|
|
|
109
89
|
`Last message: ${last?.toLocaleString() || 'never'}\n` +
|
|
110
90
|
`Silence until: ${state.silenceUntil?.toLocaleTimeString() || 'not silenced'}\n\n` +
|
|
111
91
|
`${caLine}\n\n` +
|
|
112
|
-
`Subagents: ${activeTasks.length}/${maxConcurrent} active (running: ${runningCount}, pending: ${pendingCount})\n` +
|
|
113
|
-
`Recent (last ${recentTasks.length}): ✅ ${recentCompleted} • ❌ ${recentFailed} • 🚫 ${recentCancelled}\n` +
|
|
114
|
-
`${activePreview ? `Active now:\n${activePreview}\n\n` : '\n'}` +
|
|
115
92
|
`Scheduled jobs:\n${jobList || ' (none)'}`);
|
|
116
93
|
}
|
|
117
94
|
export async function handleCron(ctx, cfg) {
|
|
@@ -182,45 +159,10 @@ export async function handleRestart(ctx, cfg) {
|
|
|
182
159
|
}
|
|
183
160
|
}
|
|
184
161
|
export async function handleTasks(ctx, cfg) {
|
|
185
|
-
|
|
186
|
-
const recent = getRecentTasks(5);
|
|
187
|
-
if (recent.length === 0) {
|
|
188
|
-
await ctx.reply('No agent tasks yet. Subagents spawn automatically for complex requests.');
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
const formatTask = (t) => {
|
|
192
|
-
const elapsed = ((t.completedAt || new Date()).getTime() - t.createdAt.getTime()) / 1000;
|
|
193
|
-
const elapsedStr = elapsed < 60 ? `${Math.round(elapsed)}s` : `${Math.round(elapsed / 60)}m`;
|
|
194
|
-
const status = {
|
|
195
|
-
pending: '⏳ Pending',
|
|
196
|
-
running: `🔄 Running (${elapsedStr})`,
|
|
197
|
-
completed: `✅ Done (${elapsedStr})`,
|
|
198
|
-
failed: `❌ Failed (${elapsedStr})`,
|
|
199
|
-
cancelled: '🚫 Cancelled'
|
|
200
|
-
};
|
|
201
|
-
const promptPreview = t.prompt.slice(0, 60) + (t.prompt.length > 60 ? '...' : '');
|
|
202
|
-
return `${t.id}: ${status[t.status] || t.status} [${t.type}] ${promptPreview}`;
|
|
203
|
-
};
|
|
204
|
-
const lines = [...active, ...recent].slice(0, 10).map(formatTask).join('\n');
|
|
205
|
-
await ctx.reply(`Agent tasks:\n\n${lines}`);
|
|
162
|
+
await ctx.reply('Use /agents to list active coding agents, or /cron to manage scheduled tasks.');
|
|
206
163
|
}
|
|
207
164
|
export async function handleCancel(ctx, cfg) {
|
|
208
|
-
|
|
209
|
-
if (!id) {
|
|
210
|
-
await ctx.reply('Usage: /cancel <task-id>\nExample: /cancel t1');
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
const task = cancelTask(id);
|
|
214
|
-
if (!task) {
|
|
215
|
-
await ctx.reply(`No task found: ${id}`);
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
if (task.status === 'cancelled') {
|
|
219
|
-
await ctx.reply(`Cancelled ${id}.`);
|
|
220
|
-
}
|
|
221
|
-
else {
|
|
222
|
-
await ctx.reply(`Task ${id} is already ${task.status}.`);
|
|
223
|
-
}
|
|
165
|
+
await ctx.reply('Use the dashboard to cancel coding agents, or /cron to manage scheduled tasks.');
|
|
224
166
|
}
|
|
225
167
|
export async function handleSkills(ctx, cfg) {
|
|
226
168
|
const skillConfig = cfg.skills;
|
|
@@ -3,7 +3,6 @@ import { Bot, GrammyError, HttpError, InputFile } from 'grammy';
|
|
|
3
3
|
import { run } from '@grammyjs/runner';
|
|
4
4
|
import { isAllowed, isRateLimited } from '../../security.js';
|
|
5
5
|
import { runAgentTurn } from '../../agent.js';
|
|
6
|
-
import { initSubagentSystem } from '../../subagent.js';
|
|
7
6
|
import { getCurrentModel } from '../../gateway.js';
|
|
8
7
|
import { getApproval, approveRequest, denyRequest } from '../../exec-approval.js';
|
|
9
8
|
import { transcribeAudio, synthesizeSpeech } from '../../voice.js';
|
|
@@ -93,12 +92,6 @@ export async function initTelegram(cfg) {
|
|
|
93
92
|
}
|
|
94
93
|
const bot = new Bot(cfg.channels.telegram.token);
|
|
95
94
|
activeBot = bot;
|
|
96
|
-
// Initialize subagent system with message delivery callback
|
|
97
|
-
initSubagentSystem(async (chatId, message) => {
|
|
98
|
-
if (!activeBot)
|
|
99
|
-
return;
|
|
100
|
-
await sendLongMessage({ reply: (text) => activeBot.api.sendMessage(chatId, text) }, message);
|
|
101
|
-
});
|
|
102
95
|
// Register commands with Telegram for the / menu
|
|
103
96
|
bot.api.setMyCommands(BOT_COMMANDS).catch((err) => {
|
|
104
97
|
console.error('[telegram] Failed to set bot commands:', err);
|
package/dist/channels.js
CHANGED
|
@@ -27,7 +27,7 @@ function resolveChannelPreference(config) {
|
|
|
27
27
|
}
|
|
28
28
|
async function loadAdapter(channel) {
|
|
29
29
|
if (channel === 'telegram') {
|
|
30
|
-
const telegram = await import('./telegram.js');
|
|
30
|
+
const telegram = await import('./channels/telegram/index.js');
|
|
31
31
|
return {
|
|
32
32
|
init: async (config) => (await telegram.initTelegram(config)) !== null,
|
|
33
33
|
start: telegram.startTelegram,
|
package/dist/cli.js
CHANGED
|
@@ -43,6 +43,9 @@ Commands:
|
|
|
43
43
|
tools list List available tools (built-in + MCP)
|
|
44
44
|
tools install <name> Add MCP server (--command <cmd> [--args ...] or --url <url>)
|
|
45
45
|
tools remove <name> Remove MCP server
|
|
46
|
+
agents List coding agents (active + recent)
|
|
47
|
+
agents <id> Show details for a coding agent (with live output)
|
|
48
|
+
agents <id> --follow Follow live output for an agent
|
|
46
49
|
sandbox status Show active sandbox containers
|
|
47
50
|
sandbox prune Force-prune all sandbox containers
|
|
48
51
|
sandbox init Auto-setup sandbox runtime/image/config (supports --profile)
|
|
@@ -154,21 +157,44 @@ function startDaemon() {
|
|
|
154
157
|
console.log(`Daemon started: ${LAUNCHD_LABEL}`);
|
|
155
158
|
return 0;
|
|
156
159
|
}
|
|
160
|
+
// All launchd labels that may be running (current + legacy)
|
|
161
|
+
const ALL_LAUNCHD_LABELS = [LAUNCHD_LABEL, 'com.katre.skimpyclaw'];
|
|
157
162
|
function stopDaemon() {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
163
|
+
const launchAgentsDir = join(homedir(), 'Library', 'LaunchAgents');
|
|
164
|
+
const uid = process.getuid?.();
|
|
165
|
+
// 1. Unload and remove plists for all known labels
|
|
166
|
+
if (launchctlAvailable()) {
|
|
167
|
+
for (const label of ALL_LAUNCHD_LABELS) {
|
|
168
|
+
const plist = join(launchAgentsDir, `${label}.plist`);
|
|
169
|
+
if (existsSync(plist)) {
|
|
170
|
+
runLaunchctl(['unload', plist]);
|
|
171
|
+
rmSync(plist, { force: true });
|
|
172
|
+
console.log(`Unloaded and removed: ${label}`);
|
|
173
|
+
}
|
|
174
|
+
// Also try bootout in case the service is loaded without a plist
|
|
175
|
+
if (uid !== undefined) {
|
|
176
|
+
runLaunchctl(['bootout', `gui/${uid}/${label}`]);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
165
179
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
180
|
+
// 2. Kill anything still listening on the gateway port
|
|
181
|
+
const lsofResult = spawnSync('lsof', ['-ti', `:${DEFAULT_PORT}`], { encoding: 'utf-8' });
|
|
182
|
+
const pids = (lsofResult.stdout || '')
|
|
183
|
+
.split('\n')
|
|
184
|
+
.map((s) => s.trim())
|
|
185
|
+
.filter(Boolean);
|
|
186
|
+
if (pids.length > 0) {
|
|
187
|
+
for (const pid of pids) {
|
|
188
|
+
try {
|
|
189
|
+
process.kill(Number(pid), 'SIGTERM');
|
|
190
|
+
console.log(`Killed process ${pid} on port ${DEFAULT_PORT}`);
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
// already dead
|
|
194
|
+
}
|
|
195
|
+
}
|
|
170
196
|
}
|
|
171
|
-
console.log(
|
|
197
|
+
console.log('Daemon stopped.');
|
|
172
198
|
return 0;
|
|
173
199
|
}
|
|
174
200
|
function commandUninstall(args) {
|
|
@@ -795,10 +821,7 @@ function resolveSandboxDir() {
|
|
|
795
821
|
return null;
|
|
796
822
|
}
|
|
797
823
|
function parseSandboxOption(args, flag) {
|
|
798
|
-
|
|
799
|
-
if (idx === -1 || idx + 1 >= args.length)
|
|
800
|
-
return undefined;
|
|
801
|
-
return args[idx + 1];
|
|
824
|
+
return parseOption(args, flag, '') || undefined;
|
|
802
825
|
}
|
|
803
826
|
function runSandboxImageCheck(runtime, image, network, cmd) {
|
|
804
827
|
const result = spawnSync(runtime, ['run', '--rm', '--network', network, image, 'sh', '-lc', cmd], { encoding: 'utf-8' });
|
|
@@ -815,6 +838,115 @@ function printSandboxCheck(ok, name, detail, hint) {
|
|
|
815
838
|
console.log(` → ${hint}`);
|
|
816
839
|
}
|
|
817
840
|
}
|
|
841
|
+
async function commandAgents(args) {
|
|
842
|
+
const { getAllCodeAgents, getCodeAgent, restoreCodeAgentTasks } = await import('./code-agents/index.js');
|
|
843
|
+
// Restore tasks from disk so we can see them
|
|
844
|
+
restoreCodeAgentTasks();
|
|
845
|
+
const id = args.find(a => !a.startsWith('-'));
|
|
846
|
+
const follow = args.includes('--follow') || args.includes('-f');
|
|
847
|
+
if (id) {
|
|
848
|
+
// Show details for a specific agent
|
|
849
|
+
const showAgent = () => {
|
|
850
|
+
const agent = getCodeAgent(id);
|
|
851
|
+
if (!agent) {
|
|
852
|
+
console.error(`No coding agent found with ID "${id}".`);
|
|
853
|
+
return false;
|
|
854
|
+
}
|
|
855
|
+
// Clear screen in follow mode
|
|
856
|
+
if (follow)
|
|
857
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
858
|
+
const elapsed = agent.durationSeconds != null
|
|
859
|
+
? agent.durationSeconds
|
|
860
|
+
: Math.round((Date.now() - new Date(agent.startedAt).getTime()) / 1000);
|
|
861
|
+
const elapsedStr = elapsed < 60 ? `${elapsed}s` : `${Math.floor(elapsed / 60)}m${elapsed % 60}s`;
|
|
862
|
+
console.log(`\x1b[1m${agent.id}\x1b[0m ${agent.agent} \x1b[33m${agent.status}\x1b[0m (${elapsedStr})`);
|
|
863
|
+
if (agent.model)
|
|
864
|
+
console.log(`Model: ${agent.model}`);
|
|
865
|
+
console.log(`Workdir: ${agent.workdir}`);
|
|
866
|
+
console.log(`Task: ${agent.task.slice(0, 200)}${agent.task.length > 200 ? '...' : ''}`);
|
|
867
|
+
// Show children for team coordinator
|
|
868
|
+
if (agent.childTaskIds && agent.childTaskIds.length > 0) {
|
|
869
|
+
console.log(`\n\x1b[1mChildren:\x1b[0m`);
|
|
870
|
+
for (const childId of agent.childTaskIds) {
|
|
871
|
+
const child = getCodeAgent(childId);
|
|
872
|
+
if (!child)
|
|
873
|
+
continue;
|
|
874
|
+
const cElapsed = child.durationSeconds != null
|
|
875
|
+
? child.durationSeconds
|
|
876
|
+
: Math.round((Date.now() - new Date(child.startedAt).getTime()) / 1000);
|
|
877
|
+
const cStr = cElapsed < 60 ? `${cElapsed}s` : `${Math.floor(cElapsed / 60)}m${cElapsed % 60}s`;
|
|
878
|
+
const waveLabel = child.wave != null ? ` [wave ${child.wave + 1}]` : '';
|
|
879
|
+
const icon = child.status === 'completed' ? '✅' : child.status === 'failed' ? '❌' : child.status === 'running' ? '🔄' : child.status === 'pending' ? '⏳' : '❓';
|
|
880
|
+
console.log(` ${icon} ${child.id} ${child.status} (${cStr})${waveLabel}`);
|
|
881
|
+
const subtask = (child.subtask || child.task).slice(0, 120);
|
|
882
|
+
console.log(` ${subtask}${(child.subtask || child.task).length > 120 ? '...' : ''}`);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
// Show live output
|
|
886
|
+
if (agent.liveOutput) {
|
|
887
|
+
console.log(`\n\x1b[1mLive Output:\x1b[0m`);
|
|
888
|
+
console.log(agent.liveOutput.slice(-3000));
|
|
889
|
+
}
|
|
890
|
+
// Show result
|
|
891
|
+
if (agent.outputPreview) {
|
|
892
|
+
console.log(`\n\x1b[1mResult:\x1b[0m`);
|
|
893
|
+
console.log(agent.outputPreview.slice(0, 2000));
|
|
894
|
+
}
|
|
895
|
+
if (agent.error) {
|
|
896
|
+
console.log(`\n\x1b[31mError: ${agent.error}\x1b[0m`);
|
|
897
|
+
}
|
|
898
|
+
if (agent.validationOutput) {
|
|
899
|
+
console.log(`\n\x1b[1mValidation:\x1b[0m`);
|
|
900
|
+
console.log(agent.validationOutput.slice(0, 1000));
|
|
901
|
+
}
|
|
902
|
+
return agent.status === 'running' || agent.status === 'validating' || agent.status === 'pending';
|
|
903
|
+
};
|
|
904
|
+
if (follow) {
|
|
905
|
+
let stillRunning = showAgent();
|
|
906
|
+
while (stillRunning) {
|
|
907
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
908
|
+
restoreCodeAgentTasks();
|
|
909
|
+
stillRunning = showAgent();
|
|
910
|
+
}
|
|
911
|
+
// Show final state
|
|
912
|
+
showAgent();
|
|
913
|
+
return 0;
|
|
914
|
+
}
|
|
915
|
+
showAgent();
|
|
916
|
+
return 0;
|
|
917
|
+
}
|
|
918
|
+
// List all agents
|
|
919
|
+
const all = getAllCodeAgents();
|
|
920
|
+
if (all.length === 0) {
|
|
921
|
+
console.log('No coding agents have run yet.');
|
|
922
|
+
return 0;
|
|
923
|
+
}
|
|
924
|
+
// Group: active first, then recent
|
|
925
|
+
const active = all.filter(a => a.status === 'running' || a.status === 'validating' || a.status === 'pending');
|
|
926
|
+
const finished = all.filter(a => a.status !== 'running' && a.status !== 'validating' && a.status !== 'pending');
|
|
927
|
+
if (active.length > 0) {
|
|
928
|
+
console.log('\x1b[1mActive:\x1b[0m');
|
|
929
|
+
for (const a of active) {
|
|
930
|
+
const elapsed = Math.round((Date.now() - new Date(a.startedAt).getTime()) / 1000);
|
|
931
|
+
const elapsedStr = elapsed < 60 ? `${elapsed}s` : `${Math.floor(elapsed / 60)}m${elapsed % 60}s`;
|
|
932
|
+
const taskPreview = a.task.slice(0, 80) + (a.task.length > 80 ? '...' : '');
|
|
933
|
+
const children = a.childTaskIds ? ` (${a.childTaskIds.length} children)` : '';
|
|
934
|
+
console.log(` ${a.id}: \x1b[33m${a.status}\x1b[0m ${a.agent} (${elapsedStr})${children} — ${taskPreview}`);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
if (finished.length > 0) {
|
|
938
|
+
console.log(active.length > 0 ? '\n\x1b[1mRecent:\x1b[0m' : '\x1b[1mRecent:\x1b[0m');
|
|
939
|
+
for (const a of finished.slice(-15)) {
|
|
940
|
+
const dur = a.durationSeconds != null
|
|
941
|
+
? (a.durationSeconds < 60 ? `${a.durationSeconds}s` : `${Math.floor(a.durationSeconds / 60)}m`)
|
|
942
|
+
: '?';
|
|
943
|
+
const icon = a.status === 'completed' ? '✅' : a.status === 'failed' ? '❌' : a.status === 'timeout' ? '⏰' : a.status === 'cancelled' ? '🚫' : '❓';
|
|
944
|
+
const taskPreview = a.task.slice(0, 80) + (a.task.length > 80 ? '...' : '');
|
|
945
|
+
console.log(` ${icon} ${a.id}: ${a.status} ${a.agent} (${dur}) — ${taskPreview}`);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
return 0;
|
|
949
|
+
}
|
|
818
950
|
async function commandSandbox(args) {
|
|
819
951
|
const sub = args[0];
|
|
820
952
|
if (sub === 'status') {
|
|
@@ -1050,6 +1182,9 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
1050
1182
|
if (command === 'tools') {
|
|
1051
1183
|
return await commandTools(args);
|
|
1052
1184
|
}
|
|
1185
|
+
if (command === 'agents') {
|
|
1186
|
+
return await commandAgents(args);
|
|
1187
|
+
}
|
|
1053
1188
|
if (command === 'sandbox') {
|
|
1054
1189
|
return await commandSandbox(args);
|
|
1055
1190
|
}
|
|
@@ -11,11 +11,16 @@ export type PackageManager = 'pnpm' | 'yarn' | 'npm' | 'bun';
|
|
|
11
11
|
export declare function detectPackageManager(workdir: string): PackageManager;
|
|
12
12
|
/**
|
|
13
13
|
* Build the validation command for a project directory.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
*
|
|
15
|
+
* Resolution order:
|
|
16
|
+
* 1. Per-project override from config `codeAgents.validationCommands`
|
|
17
|
+
* 2. Monorepo auto-detection: scope to changed packages only
|
|
18
|
+
* - Works both when workdir is the repo root AND when it's a package subdir
|
|
19
|
+
* 3. Auto-detect from package.json scripts (build + test)
|
|
20
|
+
* 4. Empty string (skip validation) if no scripts found
|
|
16
21
|
*/
|
|
17
|
-
export declare function buildValidationCommand(workdir: string): string;
|
|
22
|
+
export declare function buildValidationCommand(workdir: string, validationCommands?: Record<string, string>): string;
|
|
18
23
|
/** Run build/test validation. Shared by solo agents and team orchestrator. */
|
|
19
|
-
export declare function runValidation(workdir: string): Promise<ValidationResult>;
|
|
24
|
+
export declare function runValidation(workdir: string, validationCommands?: Record<string, string>): Promise<ValidationResult>;
|
|
20
25
|
/** Background execution of a coding agent. Updates task status throughout. */
|
|
21
26
|
export declare function runCodeAgentBackground(id: string, agent: string, task: string, workdir: string, validate: boolean, input: Record<string, any>, startedAt: Date, options?: CodeAgentBackgroundOptions): Promise<void>;
|