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.
Files changed (73) hide show
  1. package/README.md +14 -6
  2. package/dist/__tests__/api.test.js +1 -0
  3. package/dist/__tests__/channels.test.js +1 -1
  4. package/dist/__tests__/code-agents-orchestrator.test.js +74 -7
  5. package/dist/__tests__/code-agents-preflight.test.d.ts +1 -0
  6. package/dist/__tests__/code-agents-preflight.test.js +88 -0
  7. package/dist/__tests__/code-agents-sandbox.test.d.ts +1 -0
  8. package/dist/__tests__/code-agents-sandbox.test.js +163 -0
  9. package/dist/__tests__/code-agents-utils.test.js +12 -1
  10. package/dist/__tests__/context-manager.test.d.ts +1 -0
  11. package/dist/__tests__/context-manager.test.js +236 -0
  12. package/dist/__tests__/package-manager-detection.test.js +5 -5
  13. package/dist/__tests__/setup.test.js +7 -5
  14. package/dist/__tests__/skills.test.js +2 -2
  15. package/dist/__tests__/structured-context.test.d.ts +1 -0
  16. package/dist/__tests__/structured-context.test.js +100 -0
  17. package/dist/__tests__/tools.test.js +65 -3
  18. package/dist/agent.js +4 -5
  19. package/dist/api.js +10 -58
  20. package/dist/audit.js +5 -51
  21. package/dist/channels/telegram/handlers.js +2 -60
  22. package/dist/channels/telegram/index.js +0 -7
  23. package/dist/channels.js +1 -1
  24. package/dist/cli.js +151 -16
  25. package/dist/code-agents/executor.d.ts +9 -4
  26. package/dist/code-agents/executor.js +187 -13
  27. package/dist/code-agents/index.d.ts +1 -1
  28. package/dist/code-agents/index.js +30 -22
  29. package/dist/code-agents/orchestrator.d.ts +8 -2
  30. package/dist/code-agents/orchestrator.js +318 -27
  31. package/dist/code-agents/structured-context.d.ts +7 -0
  32. package/dist/code-agents/structured-context.js +54 -0
  33. package/dist/code-agents/types.d.ts +2 -0
  34. package/dist/code-agents/utils.d.ts +4 -0
  35. package/dist/code-agents/utils.js +38 -2
  36. package/dist/code-agents/worktree.d.ts +40 -0
  37. package/dist/code-agents/worktree.js +215 -0
  38. package/dist/config.d.ts +1 -0
  39. package/dist/config.js +5 -3
  40. package/dist/cron.js +18 -4
  41. package/dist/dashboard/assets/{index-CkonC7Cd.js → index-BoTHPby4.js} +20 -20
  42. package/dist/dashboard/assets/{index-EAg6lqF5.css → index-D4mufvBg.css} +1 -1
  43. package/dist/dashboard/index.html +2 -2
  44. package/dist/discord.js +4 -40
  45. package/dist/exec-approval.js +1 -1
  46. package/dist/file-lock.js +1 -1
  47. package/dist/gateway.js +3 -10
  48. package/dist/providers/anthropic.js +9 -5
  49. package/dist/providers/codex.js +10 -6
  50. package/dist/providers/context-manager.d.ts +22 -0
  51. package/dist/providers/context-manager.js +100 -0
  52. package/dist/providers/openai.js +9 -5
  53. package/dist/providers/types.d.ts +1 -0
  54. package/dist/security.js +9 -0
  55. package/dist/setup.js +122 -27
  56. package/dist/skills.js +9 -2
  57. package/dist/subagent.js +33 -2
  58. package/dist/tools/bash-tool.js +8 -0
  59. package/dist/tools/browser-tool.js +2 -1
  60. package/dist/tools/definitions.d.ts +0 -27
  61. package/dist/tools/definitions.js +0 -18
  62. package/dist/tools/execute-context.d.ts +4 -4
  63. package/dist/tools/file-tools.d.ts +1 -1
  64. package/dist/tools/file-tools.js +1 -1
  65. package/dist/tools.d.ts +5 -5
  66. package/dist/tools.js +87 -98
  67. package/dist/types.d.ts +14 -22
  68. package/dist/usage.d.ts +1 -0
  69. package/dist/usage.js +30 -46
  70. package/dist/utils.d.ts +18 -0
  71. package/dist/utils.js +71 -0
  72. package/dist/voice.js +9 -7
  73. 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 { timingSafeEqual } from 'crypto';
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
- function validateAgentId(agentId) {
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
- const authHeader = request.headers.authorization;
102
- if (!authHeader || !authHeader.startsWith('Bearer ')) {
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 (!validateAgentId(agentId)) {
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 (!validateAgentId(agentId)) {
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 (!validateAgentId(agentId)) {
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 (!validateAgentId(agentId)) {
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 (!validateAgentId(agentId)) {
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 { readFileSync, appendFileSync, existsSync, mkdirSync, readdirSync } from 'fs';
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); // 90 days back
66
- // Get all audit JSONL files in the directory
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
- const active = getActiveTasks();
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
- const id = String(ctx.match || '').trim();
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
- if (!launchctlAvailable()) {
159
- console.error('Daemon control is only supported on macOS with launchctl.');
160
- return 1;
161
- }
162
- if (!existsSync(LAUNCHD_PLIST)) {
163
- console.error(`Launchd plist not found: ${LAUNCHD_PLIST}`);
164
- return 1;
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
- const result = runLaunchctl(['unload', LAUNCHD_PLIST]);
167
- if (!result.ok && !result.output.includes('Could not find specified service')) {
168
- console.error(result.output || 'Failed to unload daemon');
169
- return 1;
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(`Daemon stopped: ${LAUNCHD_LABEL}`);
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
- const idx = args.indexOf(flag);
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
- * Checks for `build` and `test` scripts in package.json, then runs them
15
- * with the detected package manager. Falls back to `<pm> build && <pm> test`.
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>;