@phnx-labs/agents-cli 1.14.6 → 1.15.0
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 +148 -1
- package/dist/commands/beta.js +6 -1
- package/dist/commands/exec.js +9 -2
- package/dist/commands/init.js +10 -0
- package/dist/commands/mcp.js +4 -4
- package/dist/commands/prune.d.ts +0 -20
- package/dist/commands/prune.js +268 -15
- package/dist/commands/secrets.js +83 -0
- package/dist/commands/teams.js +2 -3
- package/dist/commands/usage.js +6 -0
- package/dist/commands/versions.js +8 -6
- package/dist/lib/browser/chrome.js +1 -1
- package/dist/lib/browser/drivers/ssh.d.ts +1 -0
- package/dist/lib/browser/drivers/ssh.js +23 -2
- package/dist/lib/browser/ipc.js +1 -0
- package/dist/lib/browser/service.d.ts +3 -0
- package/dist/lib/browser/service.js +114 -6
- package/dist/lib/daemon.js +4 -4
- package/dist/lib/events.d.ts +159 -0
- package/dist/lib/events.js +441 -0
- package/dist/lib/exec.js +29 -6
- package/dist/lib/permissions.d.ts +6 -3
- package/dist/lib/permissions.js +38 -34
- package/dist/lib/routines.d.ts +15 -0
- package/dist/lib/routines.js +68 -0
- package/dist/lib/runner.js +15 -0
- package/dist/lib/secrets/bundles.js +7 -1
- package/dist/lib/secrets/index.d.ts +14 -11
- package/dist/lib/secrets/index.js +49 -21
- package/dist/lib/secrets/linux.d.ts +27 -0
- package/dist/lib/secrets/linux.js +161 -0
- package/dist/lib/session/db.d.ts +4 -0
- package/dist/lib/session/db.js +26 -0
- package/dist/lib/skills.js +4 -0
- package/dist/lib/usage.d.ts +1 -1
- package/dist/lib/usage.js +13 -46
- package/dist/lib/versions.js +16 -0
- package/package.json +1 -1
- package/scripts/postinstall.js +37 -9
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized event logging for agents-cli.
|
|
3
|
+
*
|
|
4
|
+
* Structured JSONL logs at ~/.agents/logs/events-YYYY-MM-DD.jsonl
|
|
5
|
+
* with automatic daily rotation and rich metadata for debugging/auditing.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Rich metadata: hostname, platform, arch, pid, timezone
|
|
9
|
+
* - Timing helpers: measure operation duration automatically
|
|
10
|
+
* - Truncation: long inputs/outputs are trimmed with ellipsis
|
|
11
|
+
* - Permissions: logs dir is 0700, files are 0600 (owner-only)
|
|
12
|
+
* - Performance tracking: withTiming() wrapper for any async function
|
|
13
|
+
*/
|
|
14
|
+
import * as fs from 'fs';
|
|
15
|
+
import * as path from 'path';
|
|
16
|
+
import * as os from 'os';
|
|
17
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
18
|
+
const USER_AGENTS_DIR = path.join(os.homedir(), '.agents');
|
|
19
|
+
const LOGS_DIR = path.join(USER_AGENTS_DIR, 'logs');
|
|
20
|
+
/** Default retention period in days. */
|
|
21
|
+
const DEFAULT_RETENTION_DAYS = 30;
|
|
22
|
+
/** Default max length for truncated strings. */
|
|
23
|
+
const DEFAULT_TRUNCATE_LENGTH = 500;
|
|
24
|
+
/** Environment variable to disable event logging. */
|
|
25
|
+
const DISABLE_ENV_VAR = 'AGENTS_DISABLE_EVENT_LOG';
|
|
26
|
+
/** Check if audit logging is disabled via environment variable. */
|
|
27
|
+
function isDisabled() {
|
|
28
|
+
const val = process.env[DISABLE_ENV_VAR];
|
|
29
|
+
return val === '1' || val === 'true';
|
|
30
|
+
}
|
|
31
|
+
/** Directory permissions (owner read/write/execute only). */
|
|
32
|
+
const DIR_MODE = 0o700;
|
|
33
|
+
/** File permissions (owner read/write only). */
|
|
34
|
+
const FILE_MODE = 0o600;
|
|
35
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
36
|
+
function getTimezoneOffset() {
|
|
37
|
+
const offset = new Date().getTimezoneOffset();
|
|
38
|
+
const sign = offset <= 0 ? '+' : '-';
|
|
39
|
+
const hours = String(Math.floor(Math.abs(offset) / 60)).padStart(2, '0');
|
|
40
|
+
const mins = String(Math.abs(offset) % 60).padStart(2, '0');
|
|
41
|
+
return `${sign}${hours}:${mins}`;
|
|
42
|
+
}
|
|
43
|
+
function getTimezoneName() {
|
|
44
|
+
try {
|
|
45
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return 'Unknown';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function getLogFilePath(date = new Date()) {
|
|
52
|
+
const yyyy = date.getFullYear();
|
|
53
|
+
const mm = String(date.getMonth() + 1).padStart(2, '0');
|
|
54
|
+
const dd = String(date.getDate()).padStart(2, '0');
|
|
55
|
+
return path.join(LOGS_DIR, `events-${yyyy}-${mm}-${dd}.jsonl`);
|
|
56
|
+
}
|
|
57
|
+
function ensureLogsDir() {
|
|
58
|
+
if (!fs.existsSync(LOGS_DIR)) {
|
|
59
|
+
fs.mkdirSync(LOGS_DIR, { recursive: true, mode: DIR_MODE });
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
// Ensure permissions are correct on existing dir
|
|
63
|
+
try {
|
|
64
|
+
fs.chmodSync(LOGS_DIR, DIR_MODE);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// May fail if not owner
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// ─── Truncation ───────────────────────────────────────────────────────────────
|
|
72
|
+
/**
|
|
73
|
+
* Truncate a string to maxLength, adding ellipsis if truncated.
|
|
74
|
+
* Returns undefined for null/undefined input.
|
|
75
|
+
*/
|
|
76
|
+
export function truncate(str, maxLength = DEFAULT_TRUNCATE_LENGTH) {
|
|
77
|
+
if (str == null)
|
|
78
|
+
return undefined;
|
|
79
|
+
if (str.length <= maxLength)
|
|
80
|
+
return str;
|
|
81
|
+
return str.slice(0, maxLength - 3) + '...';
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Truncate all string values in a payload object.
|
|
85
|
+
*/
|
|
86
|
+
function truncatePayload(payload, maxLength = DEFAULT_TRUNCATE_LENGTH) {
|
|
87
|
+
const result = {};
|
|
88
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
89
|
+
if (typeof value === 'string') {
|
|
90
|
+
result[key] = truncate(value, maxLength);
|
|
91
|
+
}
|
|
92
|
+
else if (Array.isArray(value)) {
|
|
93
|
+
// Truncate array to first 10 items, truncate each string item
|
|
94
|
+
result[key] = value.slice(0, 10).map(v => typeof v === 'string' ? truncate(v, maxLength) : v);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
result[key] = value;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
// ─── Core API ─────────────────────────────────────────────────────────────────
|
|
103
|
+
/**
|
|
104
|
+
* Emit a structured event to the daily log file.
|
|
105
|
+
*
|
|
106
|
+
* @param event - The event type
|
|
107
|
+
* @param payload - Event-specific data (agent, version, cwd, etc.)
|
|
108
|
+
*/
|
|
109
|
+
export function emit(event, payload = {}) {
|
|
110
|
+
if (isDisabled())
|
|
111
|
+
return;
|
|
112
|
+
try {
|
|
113
|
+
ensureLogsDir();
|
|
114
|
+
const record = {
|
|
115
|
+
ts: new Date().toISOString(),
|
|
116
|
+
tz: getTimezoneOffset(),
|
|
117
|
+
tzName: getTimezoneName(),
|
|
118
|
+
hostname: os.hostname(),
|
|
119
|
+
platform: os.platform(),
|
|
120
|
+
arch: os.arch(),
|
|
121
|
+
pid: process.pid,
|
|
122
|
+
ppid: process.ppid,
|
|
123
|
+
event,
|
|
124
|
+
...truncatePayload(payload),
|
|
125
|
+
};
|
|
126
|
+
const line = JSON.stringify(record) + '\n';
|
|
127
|
+
const logPath = getLogFilePath();
|
|
128
|
+
fs.appendFileSync(logPath, line, { mode: FILE_MODE });
|
|
129
|
+
// Ensure file permissions (appendFileSync doesn't respect mode on existing files)
|
|
130
|
+
try {
|
|
131
|
+
fs.chmodSync(logPath, FILE_MODE);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// May fail if not owner
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// Silent failure - logging should never break the CLI
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Convenience wrapper for timed operations.
|
|
143
|
+
* Returns a function to call when the operation completes.
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* const done = emitStart('agent.run.start', { agent: 'claude' });
|
|
147
|
+
* // ... do work ...
|
|
148
|
+
* done({ exitCode: 0 }); // emits agent.run.end with durationMs
|
|
149
|
+
*/
|
|
150
|
+
export function emitStart(startEvent, payload = {}) {
|
|
151
|
+
const startTime = Date.now();
|
|
152
|
+
emit(startEvent, payload);
|
|
153
|
+
const endEvent = startEvent.replace('.start', '.end');
|
|
154
|
+
return (endPayload = {}) => {
|
|
155
|
+
emit(endEvent, {
|
|
156
|
+
...payload,
|
|
157
|
+
...endPayload,
|
|
158
|
+
durationMs: Date.now() - startTime,
|
|
159
|
+
});
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
// ─── Timing Utilities ─────────────────────────────────────────────────────────
|
|
163
|
+
/**
|
|
164
|
+
* Measure execution time of a synchronous function.
|
|
165
|
+
* Emits a perf.timing event with the duration.
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* const result = time('parse-config', () => parseConfig(path));
|
|
169
|
+
*/
|
|
170
|
+
export function time(label, fn, payload = {}) {
|
|
171
|
+
const start = Date.now();
|
|
172
|
+
try {
|
|
173
|
+
const result = fn();
|
|
174
|
+
emit('perf.timing', {
|
|
175
|
+
...payload,
|
|
176
|
+
label,
|
|
177
|
+
durationMs: Date.now() - start,
|
|
178
|
+
status: 'success',
|
|
179
|
+
});
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
emit('perf.timing', {
|
|
184
|
+
...payload,
|
|
185
|
+
label,
|
|
186
|
+
durationMs: Date.now() - start,
|
|
187
|
+
status: 'error',
|
|
188
|
+
error: err instanceof Error ? err.message : String(err),
|
|
189
|
+
});
|
|
190
|
+
throw err;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Measure execution time of an async function.
|
|
195
|
+
* Emits a perf.timing event with the duration.
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* const result = await timeAsync('fetch-data', () => fetchData(url));
|
|
199
|
+
*/
|
|
200
|
+
export async function timeAsync(label, fn, payload = {}) {
|
|
201
|
+
const start = Date.now();
|
|
202
|
+
try {
|
|
203
|
+
const result = await fn();
|
|
204
|
+
emit('perf.timing', {
|
|
205
|
+
...payload,
|
|
206
|
+
label,
|
|
207
|
+
durationMs: Date.now() - start,
|
|
208
|
+
status: 'success',
|
|
209
|
+
});
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
emit('perf.timing', {
|
|
214
|
+
...payload,
|
|
215
|
+
label,
|
|
216
|
+
durationMs: Date.now() - start,
|
|
217
|
+
status: 'error',
|
|
218
|
+
error: err instanceof Error ? err.message : String(err),
|
|
219
|
+
});
|
|
220
|
+
throw err;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Create a timing context for measuring multiple phases of an operation.
|
|
225
|
+
* Useful for tracking startup time vs execution time.
|
|
226
|
+
*
|
|
227
|
+
* @example
|
|
228
|
+
* const timer = createTimer('agent.run', { agent: 'claude' });
|
|
229
|
+
* // ... setup work ...
|
|
230
|
+
* timer.mark('startup'); // records startup time
|
|
231
|
+
* // ... main work ...
|
|
232
|
+
* timer.end({ exitCode: 0 }); // records total time and emits event
|
|
233
|
+
*/
|
|
234
|
+
export function createTimer(label, payload = {}) {
|
|
235
|
+
const start = Date.now();
|
|
236
|
+
const marks = {};
|
|
237
|
+
return {
|
|
238
|
+
mark(phase) {
|
|
239
|
+
const elapsed = Date.now() - start;
|
|
240
|
+
marks[phase] = elapsed;
|
|
241
|
+
return elapsed;
|
|
242
|
+
},
|
|
243
|
+
elapsed() {
|
|
244
|
+
return Date.now() - start;
|
|
245
|
+
},
|
|
246
|
+
end(endPayload = {}) {
|
|
247
|
+
const durationMs = Date.now() - start;
|
|
248
|
+
emit('perf.timing', {
|
|
249
|
+
...payload,
|
|
250
|
+
...endPayload,
|
|
251
|
+
label,
|
|
252
|
+
durationMs,
|
|
253
|
+
phases: marks,
|
|
254
|
+
});
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Higher-order function that wraps an async function with timing.
|
|
260
|
+
* The wrapper emits start/end events automatically.
|
|
261
|
+
*
|
|
262
|
+
* @example
|
|
263
|
+
* const timedFetch = withTiming('fetch', fetchData, { service: 'api' });
|
|
264
|
+
* const result = await timedFetch(url);
|
|
265
|
+
*/
|
|
266
|
+
export function withTiming(label, fn, basePayload = {}) {
|
|
267
|
+
return async (...args) => {
|
|
268
|
+
const start = Date.now();
|
|
269
|
+
try {
|
|
270
|
+
const result = await fn(...args);
|
|
271
|
+
emit('perf.timing', {
|
|
272
|
+
...basePayload,
|
|
273
|
+
label,
|
|
274
|
+
durationMs: Date.now() - start,
|
|
275
|
+
status: 'success',
|
|
276
|
+
});
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
emit('perf.timing', {
|
|
281
|
+
...basePayload,
|
|
282
|
+
label,
|
|
283
|
+
durationMs: Date.now() - start,
|
|
284
|
+
status: 'error',
|
|
285
|
+
error: err instanceof Error ? err.message : String(err),
|
|
286
|
+
});
|
|
287
|
+
throw err;
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
// ─── Command Tracking ─────────────────────────────────────────────────────────
|
|
292
|
+
/**
|
|
293
|
+
* Emit a command.start event with CLI args.
|
|
294
|
+
* Returns a done() function to emit command.end with duration.
|
|
295
|
+
*
|
|
296
|
+
* @example
|
|
297
|
+
* // At CLI entry point:
|
|
298
|
+
* const done = emitCommand('run', process.argv.slice(2));
|
|
299
|
+
* // ... execute command ...
|
|
300
|
+
* done({ exitCode: 0 });
|
|
301
|
+
*/
|
|
302
|
+
export function emitCommand(command, args = [], payload = {}) {
|
|
303
|
+
return emitStart('command.start', {
|
|
304
|
+
...payload,
|
|
305
|
+
command,
|
|
306
|
+
args: args.slice(0, 20), // Limit args to first 20
|
|
307
|
+
cwd: process.cwd(),
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
// ─── Error Tracking ───────────────────────────────────────────────────────────
|
|
311
|
+
/**
|
|
312
|
+
* Emit an error event with full details.
|
|
313
|
+
*/
|
|
314
|
+
export function emitError(err, payload = {}) {
|
|
315
|
+
const error = err instanceof Error ? err : new Error(err);
|
|
316
|
+
emit('error', {
|
|
317
|
+
...payload,
|
|
318
|
+
error: error.message,
|
|
319
|
+
errorStack: truncate(error.stack, 1000),
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
// ─── Rotation ─────────────────────────────────────────────────────────────────
|
|
323
|
+
/**
|
|
324
|
+
* Remove log files older than the retention period.
|
|
325
|
+
* Called lazily on emit or explicitly via CLI.
|
|
326
|
+
*
|
|
327
|
+
* @param retentionDays - Number of days to keep (default 30)
|
|
328
|
+
* @returns Number of files removed
|
|
329
|
+
*/
|
|
330
|
+
export function rotate(retentionDays = DEFAULT_RETENTION_DAYS) {
|
|
331
|
+
try {
|
|
332
|
+
if (!fs.existsSync(LOGS_DIR))
|
|
333
|
+
return 0;
|
|
334
|
+
const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
|
|
335
|
+
const files = fs.readdirSync(LOGS_DIR).filter(f => f.startsWith('events-') && f.endsWith('.jsonl'));
|
|
336
|
+
let removed = 0;
|
|
337
|
+
for (const file of files) {
|
|
338
|
+
const match = file.match(/^events-(\d{4})-(\d{2})-(\d{2})\.jsonl$/);
|
|
339
|
+
if (!match)
|
|
340
|
+
continue;
|
|
341
|
+
const [, yyyy, mm, dd] = match;
|
|
342
|
+
const fileDate = new Date(parseInt(yyyy), parseInt(mm) - 1, parseInt(dd));
|
|
343
|
+
if (fileDate.getTime() < cutoff) {
|
|
344
|
+
fs.unlinkSync(path.join(LOGS_DIR, file));
|
|
345
|
+
removed++;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return removed;
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
return 0;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Lazy rotation - runs at most once per day per process.
|
|
356
|
+
*/
|
|
357
|
+
let lastRotationCheck = 0;
|
|
358
|
+
export function maybeRotate() {
|
|
359
|
+
const now = Date.now();
|
|
360
|
+
const oneDayMs = 24 * 60 * 60 * 1000;
|
|
361
|
+
if (now - lastRotationCheck > oneDayMs) {
|
|
362
|
+
lastRotationCheck = now;
|
|
363
|
+
rotate();
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// ─── Query ────────────────────────────────────────────────────────────────────
|
|
367
|
+
/**
|
|
368
|
+
* Read events from log files within a date range.
|
|
369
|
+
*
|
|
370
|
+
* @param options - Query options
|
|
371
|
+
* @returns Array of event records
|
|
372
|
+
*/
|
|
373
|
+
export function query(options) {
|
|
374
|
+
const { startDate, endDate = new Date(), eventTypes, agent, command, limit } = options;
|
|
375
|
+
const results = [];
|
|
376
|
+
if (!fs.existsSync(LOGS_DIR))
|
|
377
|
+
return results;
|
|
378
|
+
const files = fs.readdirSync(LOGS_DIR)
|
|
379
|
+
.filter(f => f.startsWith('events-') && f.endsWith('.jsonl'))
|
|
380
|
+
.sort()
|
|
381
|
+
.reverse();
|
|
382
|
+
for (const file of files) {
|
|
383
|
+
const match = file.match(/^events-(\d{4})-(\d{2})-(\d{2})\.jsonl$/);
|
|
384
|
+
if (!match)
|
|
385
|
+
continue;
|
|
386
|
+
const [, yyyy, mm, dd] = match;
|
|
387
|
+
const fileDate = new Date(parseInt(yyyy), parseInt(mm) - 1, parseInt(dd));
|
|
388
|
+
if (startDate && fileDate < startDate)
|
|
389
|
+
continue;
|
|
390
|
+
if (endDate && fileDate > endDate)
|
|
391
|
+
continue;
|
|
392
|
+
const content = fs.readFileSync(path.join(LOGS_DIR, file), 'utf-8');
|
|
393
|
+
const lines = content.trim().split('\n').filter(Boolean);
|
|
394
|
+
for (const line of lines.reverse()) {
|
|
395
|
+
try {
|
|
396
|
+
const record = JSON.parse(line);
|
|
397
|
+
if (eventTypes && !eventTypes.includes(record.event))
|
|
398
|
+
continue;
|
|
399
|
+
if (agent && record.agent !== agent)
|
|
400
|
+
continue;
|
|
401
|
+
if (command && record.command !== command)
|
|
402
|
+
continue;
|
|
403
|
+
results.push(record);
|
|
404
|
+
if (limit && results.length >= limit) {
|
|
405
|
+
return results;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
// Skip malformed lines
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return results;
|
|
414
|
+
}
|
|
415
|
+
// ─── Stats ────────────────────────────────────────────────────────────────────
|
|
416
|
+
/**
|
|
417
|
+
* Get performance stats for a specific label.
|
|
418
|
+
*/
|
|
419
|
+
export function getTimingStats(label, options = {}) {
|
|
420
|
+
const days = options.days ?? 7;
|
|
421
|
+
const startDate = new Date();
|
|
422
|
+
startDate.setDate(startDate.getDate() - days);
|
|
423
|
+
const events = query({
|
|
424
|
+
startDate,
|
|
425
|
+
eventTypes: ['perf.timing'],
|
|
426
|
+
}).filter(e => e.label === label && typeof e.durationMs === 'number');
|
|
427
|
+
if (events.length === 0)
|
|
428
|
+
return null;
|
|
429
|
+
const durations = events.map(e => e.durationMs).sort((a, b) => a - b);
|
|
430
|
+
const sum = durations.reduce((a, b) => a + b, 0);
|
|
431
|
+
return {
|
|
432
|
+
count: durations.length,
|
|
433
|
+
avgMs: Math.round(sum / durations.length),
|
|
434
|
+
minMs: durations[0],
|
|
435
|
+
maxMs: durations[durations.length - 1],
|
|
436
|
+
p50Ms: durations[Math.floor(durations.length * 0.5)],
|
|
437
|
+
p95Ms: durations[Math.floor(durations.length * 0.95)],
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
// ─── Exports ──────────────────────────────────────────────────────────────────
|
|
441
|
+
export const LOGS_PATH = LOGS_DIR;
|
package/dist/lib/exec.js
CHANGED
|
@@ -10,6 +10,7 @@ import * as path from 'path';
|
|
|
10
10
|
import { parseTimeout } from './routines.js';
|
|
11
11
|
import { getVersionHomePath, isVersionInstalled, resolveVersion } from './versions.js';
|
|
12
12
|
import { resolveModel, buildReasoningFlags } from './models.js';
|
|
13
|
+
import { maybeRotate, createTimer, truncate } from './events.js';
|
|
13
14
|
/** Pattern for valid environment variable names (C identifier rules). */
|
|
14
15
|
const EXEC_ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
15
16
|
/** Parse a single KEY=VALUE string into a tuple, validating the key name. */
|
|
@@ -201,6 +202,11 @@ export function buildExecCommand(options) {
|
|
|
201
202
|
const template = AGENT_COMMANDS[options.agent];
|
|
202
203
|
const cmd = [...template.base];
|
|
203
204
|
const interactive = options.interactive === true || options.prompt === undefined;
|
|
205
|
+
// For Codex, 'exec' is the headless subcommand -- drop it for interactive mode
|
|
206
|
+
// so we run 'codex' (TUI) instead of 'codex exec' (one-shot)
|
|
207
|
+
if (options.agent === 'codex' && interactive && cmd[1] === 'exec') {
|
|
208
|
+
cmd.splice(1, 1);
|
|
209
|
+
}
|
|
204
210
|
// Use versioned alias if a specific version was requested (e.g., claude@2.1.98)
|
|
205
211
|
if (options.version && cmd.length > 0) {
|
|
206
212
|
cmd[0] = `${cmd[0]}@${options.version}`;
|
|
@@ -298,6 +304,19 @@ async function spawnAgent(options) {
|
|
|
298
304
|
const timeoutMs = options.timeout ? parseTimeout(options.timeout) : undefined;
|
|
299
305
|
const piped = !process.stdout.isTTY;
|
|
300
306
|
const interactive = options.interactive === true || options.prompt === undefined;
|
|
307
|
+
maybeRotate();
|
|
308
|
+
const timer = createTimer('agent.run', {
|
|
309
|
+
agent: options.agent,
|
|
310
|
+
version: options.version,
|
|
311
|
+
cwd: options.cwd || process.cwd(),
|
|
312
|
+
mode: options.mode,
|
|
313
|
+
model: options.model,
|
|
314
|
+
interactive,
|
|
315
|
+
sessionId: options.sessionId,
|
|
316
|
+
prompt: truncate(options.prompt, 200),
|
|
317
|
+
command: executable,
|
|
318
|
+
args: args.slice(0, 10),
|
|
319
|
+
});
|
|
301
320
|
return new Promise((resolve, reject) => {
|
|
302
321
|
// Interactive mode inherits all stdio so the CLI owns the TTY (TUI
|
|
303
322
|
// rendering, raw-mode keystrokes, colored output). Headless mode pipes
|
|
@@ -312,6 +331,8 @@ async function spawnAgent(options) {
|
|
|
312
331
|
env: buildExecEnv(options),
|
|
313
332
|
shell: false,
|
|
314
333
|
});
|
|
334
|
+
// Mark startup time (time from function call to process spawn)
|
|
335
|
+
timer.mark('startup');
|
|
315
336
|
if (!interactive && piped && child.stdout) {
|
|
316
337
|
child.stdout.pipe(process.stdout);
|
|
317
338
|
}
|
|
@@ -328,21 +349,23 @@ async function spawnAgent(options) {
|
|
|
328
349
|
}
|
|
329
350
|
});
|
|
330
351
|
}
|
|
331
|
-
let
|
|
352
|
+
let timeoutTimer;
|
|
332
353
|
if (timeoutMs) {
|
|
333
|
-
|
|
354
|
+
timeoutTimer = setTimeout(() => {
|
|
334
355
|
child.kill('SIGTERM');
|
|
335
356
|
setTimeout(() => child.kill('SIGKILL'), 5000);
|
|
336
357
|
}, timeoutMs);
|
|
337
358
|
}
|
|
338
359
|
child.on('error', (err) => {
|
|
339
|
-
if (
|
|
340
|
-
clearTimeout(
|
|
360
|
+
if (timeoutTimer)
|
|
361
|
+
clearTimeout(timeoutTimer);
|
|
362
|
+
timer.end({ error: err.message, exitCode: -1, status: 'error' });
|
|
341
363
|
reject(err);
|
|
342
364
|
});
|
|
343
365
|
child.on('close', (code) => {
|
|
344
|
-
if (
|
|
345
|
-
clearTimeout(
|
|
366
|
+
if (timeoutTimer)
|
|
367
|
+
clearTimeout(timeoutTimer);
|
|
368
|
+
timer.end({ exitCode: code ?? 0, status: code === 0 ? 'success' : 'failed' });
|
|
346
369
|
resolve({ exitCode: code ?? 0, stderr: stderrBuffer });
|
|
347
370
|
});
|
|
348
371
|
});
|
|
@@ -72,21 +72,24 @@ export declare function getActivePermissionSetName(): string | null;
|
|
|
72
72
|
export declare function buildPermissionsFromGroups(groupNames: string[]): PermissionSet;
|
|
73
73
|
/**
|
|
74
74
|
* List installed permission sets from central storage.
|
|
75
|
+
* User dir takes precedence; system entries are surfaced when user has no
|
|
76
|
+
* same-named override.
|
|
75
77
|
*/
|
|
76
78
|
export declare function listInstalledPermissions(): InstalledPermission[];
|
|
77
79
|
/**
|
|
78
|
-
* Get a specific permission set by name.
|
|
80
|
+
* Get a specific permission set by name. Searches user dir first, then system.
|
|
79
81
|
*/
|
|
80
82
|
export declare function getPermissionSet(name: string): InstalledPermission | null;
|
|
81
83
|
/**
|
|
82
|
-
* Install a permission set to central storage.
|
|
84
|
+
* Install a permission set to user-level central storage.
|
|
83
85
|
*/
|
|
84
86
|
export declare function installPermissionSet(sourcePath: string, name: string): {
|
|
85
87
|
success: boolean;
|
|
86
88
|
error?: string;
|
|
87
89
|
};
|
|
88
90
|
/**
|
|
89
|
-
* Remove a permission set from central storage.
|
|
91
|
+
* Remove a permission set from user-level central storage. System-shipped
|
|
92
|
+
* sets are intentionally not deletable from user commands.
|
|
90
93
|
*/
|
|
91
94
|
export declare function removePermissionSet(name: string): {
|
|
92
95
|
success: boolean;
|
package/dist/lib/permissions.js
CHANGED
|
@@ -259,55 +259,58 @@ export function buildPermissionsFromGroups(groupNames) {
|
|
|
259
259
|
}
|
|
260
260
|
/**
|
|
261
261
|
* List installed permission sets from central storage.
|
|
262
|
+
* User dir takes precedence; system entries are surfaced when user has no
|
|
263
|
+
* same-named override.
|
|
262
264
|
*/
|
|
263
265
|
export function listInstalledPermissions() {
|
|
264
266
|
ensureAgentsDir();
|
|
265
|
-
const
|
|
266
|
-
if (!fs.existsSync(dir)) {
|
|
267
|
-
return [];
|
|
268
|
-
}
|
|
267
|
+
const seen = new Set();
|
|
269
268
|
const results = [];
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
269
|
+
for (const dir of [getUserPermissionsDir(), getPermissionsDir()]) {
|
|
270
|
+
if (!fs.existsSync(dir))
|
|
271
|
+
continue;
|
|
272
|
+
try {
|
|
273
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
274
|
+
for (const entry of entries) {
|
|
275
|
+
if (!entry.isFile())
|
|
276
|
+
continue;
|
|
277
|
+
if (!entry.name.endsWith('.yml') && !entry.name.endsWith('.yaml'))
|
|
278
|
+
continue;
|
|
279
|
+
const filePath = path.join(dir, entry.name);
|
|
280
|
+
const set = parsePermissionSet(filePath);
|
|
281
|
+
if (!set)
|
|
282
|
+
continue;
|
|
283
|
+
if (seen.has(set.name))
|
|
284
|
+
continue;
|
|
285
|
+
seen.add(set.name);
|
|
286
|
+
results.push({ name: set.name, path: filePath, set });
|
|
285
287
|
}
|
|
286
288
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
289
|
+
catch {
|
|
290
|
+
// Skip inaccessible directory
|
|
291
|
+
}
|
|
290
292
|
}
|
|
291
293
|
return results;
|
|
292
294
|
}
|
|
293
295
|
/**
|
|
294
|
-
* Get a specific permission set by name.
|
|
296
|
+
* Get a specific permission set by name. Searches user dir first, then system.
|
|
295
297
|
*/
|
|
296
298
|
export function getPermissionSet(name) {
|
|
297
|
-
const dir
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
299
|
+
for (const dir of [getUserPermissionsDir(), getPermissionsDir()]) {
|
|
300
|
+
for (const ext of ['.yml', '.yaml']) {
|
|
301
|
+
const filePath = safeJoin(dir, name + ext);
|
|
302
|
+
if (fs.existsSync(filePath)) {
|
|
303
|
+
const set = parsePermissionSet(filePath);
|
|
304
|
+
if (set) {
|
|
305
|
+
return { name: set.name, path: filePath, set };
|
|
306
|
+
}
|
|
304
307
|
}
|
|
305
308
|
}
|
|
306
309
|
}
|
|
307
310
|
return null;
|
|
308
311
|
}
|
|
309
312
|
/**
|
|
310
|
-
* Install a permission set to central storage.
|
|
313
|
+
* Install a permission set to user-level central storage.
|
|
311
314
|
*/
|
|
312
315
|
export function installPermissionSet(sourcePath, name) {
|
|
313
316
|
ensurePermissionsDir();
|
|
@@ -315,7 +318,7 @@ export function installPermissionSet(sourcePath, name) {
|
|
|
315
318
|
if (!set) {
|
|
316
319
|
return { success: false, error: 'Invalid permission file' };
|
|
317
320
|
}
|
|
318
|
-
const targetPath = safeJoin(
|
|
321
|
+
const targetPath = safeJoin(getUserPermissionsDir(), name + '.yml');
|
|
319
322
|
try {
|
|
320
323
|
fs.copyFileSync(sourcePath, targetPath);
|
|
321
324
|
return { success: true };
|
|
@@ -325,10 +328,11 @@ export function installPermissionSet(sourcePath, name) {
|
|
|
325
328
|
}
|
|
326
329
|
}
|
|
327
330
|
/**
|
|
328
|
-
* Remove a permission set from central storage.
|
|
331
|
+
* Remove a permission set from user-level central storage. System-shipped
|
|
332
|
+
* sets are intentionally not deletable from user commands.
|
|
329
333
|
*/
|
|
330
334
|
export function removePermissionSet(name) {
|
|
331
|
-
const dir =
|
|
335
|
+
const dir = getUserPermissionsDir();
|
|
332
336
|
for (const ext of ['.yml', '.yaml']) {
|
|
333
337
|
const filePath = safeJoin(dir, name + ext);
|
|
334
338
|
if (fs.existsSync(filePath)) {
|
package/dist/lib/routines.d.ts
CHANGED
|
@@ -96,3 +96,18 @@ export declare function installJobFromSource(sourcePath: string, name: string):
|
|
|
96
96
|
success: boolean;
|
|
97
97
|
error?: string;
|
|
98
98
|
};
|
|
99
|
+
/** List all job names that have run directories. */
|
|
100
|
+
export declare function listJobsWithRuns(): string[];
|
|
101
|
+
/** Count total runs across all jobs. */
|
|
102
|
+
export declare function countAllRuns(): number;
|
|
103
|
+
/** Preview runs that would be pruned (keeping only the most recent `keep` per job). */
|
|
104
|
+
export declare function previewRunsPrune(keep: number): Array<{
|
|
105
|
+
jobName: string;
|
|
106
|
+
runId: string;
|
|
107
|
+
startedAt: string;
|
|
108
|
+
}>;
|
|
109
|
+
/** Delete old runs, keeping only the most recent `keep` per job. Returns bytes freed and run count. */
|
|
110
|
+
export declare function pruneRuns(keep: number): {
|
|
111
|
+
deleted: number;
|
|
112
|
+
bytesFreed: number;
|
|
113
|
+
};
|