@phnx-labs/agents-cli 1.14.6 → 1.14.7
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 +80 -0
- package/dist/commands/secrets.js +83 -0
- package/dist/lib/browser/service.js +31 -0
- package/dist/lib/events.d.ts +66 -0
- package/dist/lib/events.js +183 -0
- package/dist/lib/exec.js +17 -0
- package/dist/lib/runner.js +11 -0
- package/dist/lib/secrets/bundles.js +7 -1
- package/dist/lib/skills.js +4 -0
- package/dist/lib/versions.js +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,6 +47,7 @@ Also available as `ag` -- all commands work with both `agents` and `ag`.
|
|
|
47
47
|
- [Sessions across agents](#sessions-across-agents)
|
|
48
48
|
- [Run open models through Claude Code](#run-open-models-through-claude-code)
|
|
49
49
|
- [Teams](#teams)
|
|
50
|
+
- [Browser](#browser)
|
|
50
51
|
- [Secrets](#secrets)
|
|
51
52
|
- [Routines](#routines)
|
|
52
53
|
- [PTY](#pty)
|
|
@@ -242,6 +243,85 @@ Team state is observable via `agents teams list --json` / `agents teams status -
|
|
|
242
243
|
|
|
243
244
|
---
|
|
244
245
|
|
|
246
|
+
## Browser
|
|
247
|
+
|
|
248
|
+
Give agents access to a real browser — no relay extension, no cloud service, no Playwright getting blocked.
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
# Create an isolated profile for automation
|
|
252
|
+
agents browser profiles create work --browser chrome
|
|
253
|
+
|
|
254
|
+
# Start a task, navigate, interact
|
|
255
|
+
agents browser start login-flow --profile work
|
|
256
|
+
agents browser navigate login-flow https://app.example.com
|
|
257
|
+
agents browser refs login-flow # Get interactive element refs
|
|
258
|
+
agents browser click login-flow <tab> 42 # Click element ref 42
|
|
259
|
+
agents browser type login-flow <tab> 15 "hello"
|
|
260
|
+
agents browser screenshot login-flow # Smart resizing, token-efficient
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Why this works where Playwright fails
|
|
264
|
+
|
|
265
|
+
Playwright and Puppeteer spin up fresh browser instances with automation flags. Sites like LinkedIn, Google, and most finance apps detect and block them immediately.
|
|
266
|
+
|
|
267
|
+
`agents browser` launches your existing residential Chrome (or Brave, Edge, Chromium) on your machine via CDP. Same browser fingerprint, same IP, same everything. Sites can't detect automation because you're using the same browser you'd use manually.
|
|
268
|
+
|
|
269
|
+
### Token-efficient automation
|
|
270
|
+
|
|
271
|
+
The CLI handles the mechanical work so agents don't burn tokens on low-level browser commands. Screenshots are automatically resized without excessive compression — agents process smaller images while keeping the detail they need to make decisions.
|
|
272
|
+
|
|
273
|
+
### Profile isolation
|
|
274
|
+
|
|
275
|
+
Multiple agents can run browser tasks simultaneously without stepping on each other. Each profile gets its own user data directory, cookies, and state. One agent logs into your work Slack, another into your personal email — no conflicts, no shared state.
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
agents browser profiles create work-slack --browser chrome
|
|
279
|
+
agents browser profiles create personal-gmail --browser chrome
|
|
280
|
+
# Two agents, two profiles, no interference
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Safe credential access
|
|
284
|
+
|
|
285
|
+
Attach a [secrets bundle](#secrets) to a profile. The agent can log in without credentials in plaintext, and every secret access is recorded in the session log.
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
agents browser profiles create bank --browser chrome --secrets bank-creds
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Electron apps
|
|
292
|
+
|
|
293
|
+
Control Electron apps (Slack, Discord, VS Code, your own app) with custom binaries:
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
agents browser profiles create rush \
|
|
297
|
+
--browser custom \
|
|
298
|
+
--binary "/Applications/Rush.app/Contents/MacOS/Rush" \
|
|
299
|
+
--electron
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Remote browsers
|
|
303
|
+
|
|
304
|
+
Connect to browsers running anywhere — local, SSH tunnels, or cloud services:
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
# Local CDP (discovers WebSocket URL automatically)
|
|
308
|
+
agents browser profiles create local-debug \
|
|
309
|
+
--browser chrome \
|
|
310
|
+
--endpoint "http://localhost:9222"
|
|
311
|
+
|
|
312
|
+
# SSH tunnel to a remote machine
|
|
313
|
+
agents browser profiles create staging \
|
|
314
|
+
--browser chrome \
|
|
315
|
+
--endpoint "ssh://deploy@staging.example.com?port=9222"
|
|
316
|
+
|
|
317
|
+
# Cloud browser services (BrowserBase, Steel, etc.)
|
|
318
|
+
agents browser profiles create cloud \
|
|
319
|
+
--browser chrome \
|
|
320
|
+
--endpoint "wss://connect.browserbase.com?apiKey=..."
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
245
325
|
## Secrets
|
|
246
326
|
|
|
247
327
|
```bash
|
package/dist/commands/secrets.js
CHANGED
|
@@ -285,10 +285,19 @@ Examples:
|
|
|
285
285
|
|
|
286
286
|
# Delete the whole bundle and purge every keychain item it owned
|
|
287
287
|
agents secrets delete prod
|
|
288
|
+
|
|
289
|
+
# Generate a random password (32 chars, all character classes)
|
|
290
|
+
agents secrets generate
|
|
291
|
+
|
|
292
|
+
# Generate a 16-char password, or a PIN, or hex
|
|
293
|
+
agents secrets generate 16
|
|
294
|
+
agents secrets generate 8 --pin
|
|
295
|
+
agents secrets generate 32 --hex
|
|
288
296
|
`);
|
|
289
297
|
registerCommandGroups(cmd, [
|
|
290
298
|
{ title: 'Bundle commands', names: ['list', 'view', 'create', 'delete'] },
|
|
291
299
|
{ title: 'Secret commands', names: ['add', 'rotate', 'remove', 'import', 'export'] },
|
|
300
|
+
{ title: 'Utilities', names: ['generate'] },
|
|
292
301
|
]);
|
|
293
302
|
cmd
|
|
294
303
|
.command('list')
|
|
@@ -702,4 +711,78 @@ Examples:
|
|
|
702
711
|
process.exit(1);
|
|
703
712
|
}
|
|
704
713
|
});
|
|
714
|
+
cmd
|
|
715
|
+
.command('generate [length]')
|
|
716
|
+
.description('Generate a random password')
|
|
717
|
+
.option('-U, --uppercase', 'Include A-Z (default: on)')
|
|
718
|
+
.option('-l, --lowercase', 'Include a-z (default: on)')
|
|
719
|
+
.option('-d, --digits', 'Include 0-9 (default: on)')
|
|
720
|
+
.option('-s, --symbols', 'Include symbols (default: on)')
|
|
721
|
+
.option('--no-uppercase', 'Exclude A-Z')
|
|
722
|
+
.option('--no-lowercase', 'Exclude a-z')
|
|
723
|
+
.option('--no-digits', 'Exclude 0-9')
|
|
724
|
+
.option('--no-symbols', 'Exclude symbols')
|
|
725
|
+
.option('--strong', 'Include all character classes')
|
|
726
|
+
.option('--pin', 'Digits only (shortcut for -d --no-uppercase --no-lowercase --no-symbols)')
|
|
727
|
+
.option('--hex', 'Hex characters only (0-9, a-f)')
|
|
728
|
+
.option('-c, --copy', 'Copy to clipboard (does not print)')
|
|
729
|
+
.action(async (lengthArg, opts) => {
|
|
730
|
+
const length = lengthArg ? parseInt(lengthArg, 10) : 32;
|
|
731
|
+
if (isNaN(length) || length < 1 || length > 1024) {
|
|
732
|
+
console.error(chalk.red('Length must be a number between 1 and 1024.'));
|
|
733
|
+
process.exit(1);
|
|
734
|
+
}
|
|
735
|
+
const UPPER = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
736
|
+
const LOWER = 'abcdefghijklmnopqrstuvwxyz';
|
|
737
|
+
const DIGITS = '0123456789';
|
|
738
|
+
const SYMBOLS = '!@#$%^&*()-_=+[]{}|;:,.<>?';
|
|
739
|
+
const HEX_LOWER = '0123456789abcdef';
|
|
740
|
+
let charClasses = [];
|
|
741
|
+
if (opts.hex) {
|
|
742
|
+
charClasses = [HEX_LOWER];
|
|
743
|
+
}
|
|
744
|
+
else if (opts.pin) {
|
|
745
|
+
charClasses = [DIGITS];
|
|
746
|
+
}
|
|
747
|
+
else {
|
|
748
|
+
const useUpper = opts.strong || opts.uppercase !== false;
|
|
749
|
+
const useLower = opts.strong || opts.lowercase !== false;
|
|
750
|
+
const useDigits = opts.strong || opts.digits !== false;
|
|
751
|
+
const useSymbols = opts.strong || opts.symbols !== false;
|
|
752
|
+
if (useUpper)
|
|
753
|
+
charClasses.push(UPPER);
|
|
754
|
+
if (useLower)
|
|
755
|
+
charClasses.push(LOWER);
|
|
756
|
+
if (useDigits)
|
|
757
|
+
charClasses.push(DIGITS);
|
|
758
|
+
if (useSymbols)
|
|
759
|
+
charClasses.push(SYMBOLS);
|
|
760
|
+
}
|
|
761
|
+
if (charClasses.length === 0) {
|
|
762
|
+
console.error(chalk.red('At least one character class must be enabled.'));
|
|
763
|
+
process.exit(1);
|
|
764
|
+
}
|
|
765
|
+
const randomBytes = crypto.getRandomValues(new Uint32Array(length * 2));
|
|
766
|
+
let password = '';
|
|
767
|
+
for (let i = 0; i < length; i++) {
|
|
768
|
+
const classIndex = randomBytes[i * 2] % charClasses.length;
|
|
769
|
+
const charClass = charClasses[classIndex];
|
|
770
|
+
const charIndex = randomBytes[i * 2 + 1] % charClass.length;
|
|
771
|
+
password += charClass[charIndex];
|
|
772
|
+
}
|
|
773
|
+
if (opts.copy) {
|
|
774
|
+
const { spawn } = await import('child_process');
|
|
775
|
+
const proc = spawn('pbcopy', [], { stdio: ['pipe', 'inherit', 'inherit'] });
|
|
776
|
+
proc.stdin.write(password);
|
|
777
|
+
proc.stdin.end();
|
|
778
|
+
await new Promise((resolve, reject) => {
|
|
779
|
+
proc.on('close', (code) => code === 0 ? resolve() : reject(new Error('pbcopy failed')));
|
|
780
|
+
proc.on('error', reject);
|
|
781
|
+
});
|
|
782
|
+
console.log(chalk.green(`Password copied to clipboard (${length} chars)`));
|
|
783
|
+
}
|
|
784
|
+
else {
|
|
785
|
+
console.log(password);
|
|
786
|
+
}
|
|
787
|
+
});
|
|
705
788
|
}
|
|
@@ -8,6 +8,7 @@ import { connectSSH } from './drivers/ssh.js';
|
|
|
8
8
|
import { generateTaskId, isValidTaskId, } from './types.js';
|
|
9
9
|
import { getRefs, resolveRefToCoords } from './refs.js';
|
|
10
10
|
import { clickAtCoords, hoverAtCoords, typeText, pressKey, focusNode } from './input.js';
|
|
11
|
+
import { emit } from '../events.js';
|
|
11
12
|
export class BrowserService {
|
|
12
13
|
connections = new Map();
|
|
13
14
|
async start(profileName, taskId) {
|
|
@@ -39,6 +40,7 @@ export class BrowserService {
|
|
|
39
40
|
};
|
|
40
41
|
conn.tasks.set(finalTaskId, task);
|
|
41
42
|
await this.saveTaskState(profileName, conn.tasks);
|
|
43
|
+
emit('browser.launch', { profile: profileName, task: finalTaskId, pid: conn.pid });
|
|
42
44
|
return { task: finalTaskId, windowTargetId };
|
|
43
45
|
}
|
|
44
46
|
async stop(taskId) {
|
|
@@ -62,6 +64,7 @@ export class BrowserService {
|
|
|
62
64
|
}
|
|
63
65
|
conn.tasks.delete(taskId);
|
|
64
66
|
await this.saveTaskState(profileName, conn.tasks);
|
|
67
|
+
emit('browser.close', { profile: profileName, task: taskId });
|
|
65
68
|
return { ok: true, profile: profileName };
|
|
66
69
|
}
|
|
67
70
|
}
|
|
@@ -311,6 +314,34 @@ export class BrowserService {
|
|
|
311
314
|
sessionCache: new Map(),
|
|
312
315
|
};
|
|
313
316
|
}
|
|
317
|
+
if (url.protocol === 'wss:' || url.protocol === 'ws:') {
|
|
318
|
+
const cdp = new CDPClient();
|
|
319
|
+
await cdp.connect(endpoint);
|
|
320
|
+
await this.enableDomains(cdp);
|
|
321
|
+
return {
|
|
322
|
+
cdp,
|
|
323
|
+
port: 0,
|
|
324
|
+
pid: 0,
|
|
325
|
+
electron: profile.electron,
|
|
326
|
+
tasks: this.loadTaskState(profile.name),
|
|
327
|
+
sessionCache: new Map(),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
if (url.protocol === 'http:' || url.protocol === 'https:') {
|
|
331
|
+
const port = parseInt(url.port || (url.protocol === 'https:' ? '443' : '80'), 10);
|
|
332
|
+
const wsUrl = await discoverBrowserWsUrl(port, url.hostname);
|
|
333
|
+
const cdp = new CDPClient();
|
|
334
|
+
await cdp.connect(wsUrl);
|
|
335
|
+
await this.enableDomains(cdp);
|
|
336
|
+
return {
|
|
337
|
+
cdp,
|
|
338
|
+
port,
|
|
339
|
+
pid: 0,
|
|
340
|
+
electron: profile.electron,
|
|
341
|
+
tasks: this.loadTaskState(profile.name),
|
|
342
|
+
sessionCache: new Map(),
|
|
343
|
+
};
|
|
344
|
+
}
|
|
314
345
|
return null;
|
|
315
346
|
}
|
|
316
347
|
async enableDomains(cdp) {
|
|
@@ -0,0 +1,66 @@
|
|
|
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
|
+
export type EventType = 'agent.run.start' | 'agent.run.end' | 'version.install' | 'version.switch' | 'version.remove' | 'skill.install' | 'skill.remove' | 'browser.launch' | 'browser.close' | 'secrets.get' | 'secrets.set' | 'secrets.delete' | 'cloud.dispatch' | 'cloud.complete' | 'teams.create' | 'teams.start' | 'teams.complete' | 'hook.fire' | 'hook.error' | 'resource.sync' | 'error' | 'warn' | 'info';
|
|
8
|
+
export interface EventMeta {
|
|
9
|
+
ts: string;
|
|
10
|
+
tz: string;
|
|
11
|
+
tzName: string;
|
|
12
|
+
hostname: string;
|
|
13
|
+
platform: NodeJS.Platform;
|
|
14
|
+
arch: string;
|
|
15
|
+
pid: number;
|
|
16
|
+
event: EventType;
|
|
17
|
+
}
|
|
18
|
+
export interface EventPayload {
|
|
19
|
+
agent?: string;
|
|
20
|
+
version?: string;
|
|
21
|
+
cwd?: string;
|
|
22
|
+
durationMs?: number;
|
|
23
|
+
error?: string;
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
}
|
|
26
|
+
export type EventRecord = EventMeta & EventPayload;
|
|
27
|
+
/**
|
|
28
|
+
* Emit a structured event to the daily log file.
|
|
29
|
+
*
|
|
30
|
+
* @param event - The event type
|
|
31
|
+
* @param payload - Event-specific data (agent, version, cwd, etc.)
|
|
32
|
+
*/
|
|
33
|
+
export declare function emit(event: EventType, payload?: EventPayload): void;
|
|
34
|
+
/**
|
|
35
|
+
* Convenience wrapper for timed operations.
|
|
36
|
+
* Returns a function to call when the operation completes.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* const done = emitStart('agent.run.start', { agent: 'claude' });
|
|
40
|
+
* // ... do work ...
|
|
41
|
+
* done({ exitCode: 0 }); // emits agent.run.end with durationMs
|
|
42
|
+
*/
|
|
43
|
+
export declare function emitStart(startEvent: EventType, payload?: EventPayload): (endPayload?: EventPayload) => void;
|
|
44
|
+
/**
|
|
45
|
+
* Remove log files older than the retention period.
|
|
46
|
+
* Called lazily on emit or explicitly via CLI.
|
|
47
|
+
*
|
|
48
|
+
* @param retentionDays - Number of days to keep (default 30)
|
|
49
|
+
* @returns Number of files removed
|
|
50
|
+
*/
|
|
51
|
+
export declare function rotate(retentionDays?: number): number;
|
|
52
|
+
export declare function maybeRotate(): void;
|
|
53
|
+
/**
|
|
54
|
+
* Read events from log files within a date range.
|
|
55
|
+
*
|
|
56
|
+
* @param options - Query options
|
|
57
|
+
* @returns Array of event records
|
|
58
|
+
*/
|
|
59
|
+
export declare function query(options: {
|
|
60
|
+
startDate?: Date;
|
|
61
|
+
endDate?: Date;
|
|
62
|
+
eventTypes?: EventType[];
|
|
63
|
+
agent?: string;
|
|
64
|
+
limit?: number;
|
|
65
|
+
}): EventRecord[];
|
|
66
|
+
export declare const LOGS_PATH: string;
|
|
@@ -0,0 +1,183 @@
|
|
|
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
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import * as os from 'os';
|
|
10
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
11
|
+
const USER_AGENTS_DIR = path.join(os.homedir(), '.agents');
|
|
12
|
+
const LOGS_DIR = path.join(USER_AGENTS_DIR, 'logs');
|
|
13
|
+
/** Default retention period in days. */
|
|
14
|
+
const DEFAULT_RETENTION_DAYS = 30;
|
|
15
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
16
|
+
function getTimezoneOffset() {
|
|
17
|
+
const offset = new Date().getTimezoneOffset();
|
|
18
|
+
const sign = offset <= 0 ? '+' : '-';
|
|
19
|
+
const hours = String(Math.floor(Math.abs(offset) / 60)).padStart(2, '0');
|
|
20
|
+
const mins = String(Math.abs(offset) % 60).padStart(2, '0');
|
|
21
|
+
return `${sign}${hours}:${mins}`;
|
|
22
|
+
}
|
|
23
|
+
function getTimezoneName() {
|
|
24
|
+
try {
|
|
25
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return 'Unknown';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function getLogFilePath(date = new Date()) {
|
|
32
|
+
const yyyy = date.getFullYear();
|
|
33
|
+
const mm = String(date.getMonth() + 1).padStart(2, '0');
|
|
34
|
+
const dd = String(date.getDate()).padStart(2, '0');
|
|
35
|
+
return path.join(LOGS_DIR, `events-${yyyy}-${mm}-${dd}.jsonl`);
|
|
36
|
+
}
|
|
37
|
+
function ensureLogsDir() {
|
|
38
|
+
if (!fs.existsSync(LOGS_DIR)) {
|
|
39
|
+
fs.mkdirSync(LOGS_DIR, { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// ─── Core API ─────────────────────────────────────────────────────────────────
|
|
43
|
+
/**
|
|
44
|
+
* Emit a structured event to the daily log file.
|
|
45
|
+
*
|
|
46
|
+
* @param event - The event type
|
|
47
|
+
* @param payload - Event-specific data (agent, version, cwd, etc.)
|
|
48
|
+
*/
|
|
49
|
+
export function emit(event, payload = {}) {
|
|
50
|
+
try {
|
|
51
|
+
ensureLogsDir();
|
|
52
|
+
const record = {
|
|
53
|
+
ts: new Date().toISOString(),
|
|
54
|
+
tz: getTimezoneOffset(),
|
|
55
|
+
tzName: getTimezoneName(),
|
|
56
|
+
hostname: os.hostname(),
|
|
57
|
+
platform: os.platform(),
|
|
58
|
+
arch: os.arch(),
|
|
59
|
+
pid: process.pid,
|
|
60
|
+
event,
|
|
61
|
+
...payload,
|
|
62
|
+
};
|
|
63
|
+
const line = JSON.stringify(record) + '\n';
|
|
64
|
+
fs.appendFileSync(getLogFilePath(), line);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Silent failure - logging should never break the CLI
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Convenience wrapper for timed operations.
|
|
72
|
+
* Returns a function to call when the operation completes.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* const done = emitStart('agent.run.start', { agent: 'claude' });
|
|
76
|
+
* // ... do work ...
|
|
77
|
+
* done({ exitCode: 0 }); // emits agent.run.end with durationMs
|
|
78
|
+
*/
|
|
79
|
+
export function emitStart(startEvent, payload = {}) {
|
|
80
|
+
const startTime = Date.now();
|
|
81
|
+
emit(startEvent, payload);
|
|
82
|
+
const endEvent = startEvent.replace('.start', '.end');
|
|
83
|
+
return (endPayload = {}) => {
|
|
84
|
+
emit(endEvent, {
|
|
85
|
+
...payload,
|
|
86
|
+
...endPayload,
|
|
87
|
+
durationMs: Date.now() - startTime,
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// ─── Rotation ─────────────────────────────────────────────────────────────────
|
|
92
|
+
/**
|
|
93
|
+
* Remove log files older than the retention period.
|
|
94
|
+
* Called lazily on emit or explicitly via CLI.
|
|
95
|
+
*
|
|
96
|
+
* @param retentionDays - Number of days to keep (default 30)
|
|
97
|
+
* @returns Number of files removed
|
|
98
|
+
*/
|
|
99
|
+
export function rotate(retentionDays = DEFAULT_RETENTION_DAYS) {
|
|
100
|
+
try {
|
|
101
|
+
if (!fs.existsSync(LOGS_DIR))
|
|
102
|
+
return 0;
|
|
103
|
+
const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
|
|
104
|
+
const files = fs.readdirSync(LOGS_DIR).filter(f => f.startsWith('events-') && f.endsWith('.jsonl'));
|
|
105
|
+
let removed = 0;
|
|
106
|
+
for (const file of files) {
|
|
107
|
+
const match = file.match(/^events-(\d{4})-(\d{2})-(\d{2})\.jsonl$/);
|
|
108
|
+
if (!match)
|
|
109
|
+
continue;
|
|
110
|
+
const [, yyyy, mm, dd] = match;
|
|
111
|
+
const fileDate = new Date(parseInt(yyyy), parseInt(mm) - 1, parseInt(dd));
|
|
112
|
+
if (fileDate.getTime() < cutoff) {
|
|
113
|
+
fs.unlinkSync(path.join(LOGS_DIR, file));
|
|
114
|
+
removed++;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return removed;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Lazy rotation - runs at most once per day per process.
|
|
125
|
+
*/
|
|
126
|
+
let lastRotationCheck = 0;
|
|
127
|
+
export function maybeRotate() {
|
|
128
|
+
const now = Date.now();
|
|
129
|
+
const oneDayMs = 24 * 60 * 60 * 1000;
|
|
130
|
+
if (now - lastRotationCheck > oneDayMs) {
|
|
131
|
+
lastRotationCheck = now;
|
|
132
|
+
rotate();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// ─── Query ────────────────────────────────────────────────────────────────────
|
|
136
|
+
/**
|
|
137
|
+
* Read events from log files within a date range.
|
|
138
|
+
*
|
|
139
|
+
* @param options - Query options
|
|
140
|
+
* @returns Array of event records
|
|
141
|
+
*/
|
|
142
|
+
export function query(options) {
|
|
143
|
+
const { startDate, endDate = new Date(), eventTypes, agent, limit } = options;
|
|
144
|
+
const results = [];
|
|
145
|
+
if (!fs.existsSync(LOGS_DIR))
|
|
146
|
+
return results;
|
|
147
|
+
const files = fs.readdirSync(LOGS_DIR)
|
|
148
|
+
.filter(f => f.startsWith('events-') && f.endsWith('.jsonl'))
|
|
149
|
+
.sort()
|
|
150
|
+
.reverse();
|
|
151
|
+
for (const file of files) {
|
|
152
|
+
const match = file.match(/^events-(\d{4})-(\d{2})-(\d{2})\.jsonl$/);
|
|
153
|
+
if (!match)
|
|
154
|
+
continue;
|
|
155
|
+
const [, yyyy, mm, dd] = match;
|
|
156
|
+
const fileDate = new Date(parseInt(yyyy), parseInt(mm) - 1, parseInt(dd));
|
|
157
|
+
if (startDate && fileDate < startDate)
|
|
158
|
+
continue;
|
|
159
|
+
if (endDate && fileDate > endDate)
|
|
160
|
+
continue;
|
|
161
|
+
const content = fs.readFileSync(path.join(LOGS_DIR, file), 'utf-8');
|
|
162
|
+
const lines = content.trim().split('\n').filter(Boolean);
|
|
163
|
+
for (const line of lines.reverse()) {
|
|
164
|
+
try {
|
|
165
|
+
const record = JSON.parse(line);
|
|
166
|
+
if (eventTypes && !eventTypes.includes(record.event))
|
|
167
|
+
continue;
|
|
168
|
+
if (agent && record.agent !== agent)
|
|
169
|
+
continue;
|
|
170
|
+
results.push(record);
|
|
171
|
+
if (limit && results.length >= limit) {
|
|
172
|
+
return results;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// Skip malformed lines
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return results;
|
|
181
|
+
}
|
|
182
|
+
// ─── Exports ──────────────────────────────────────────────────────────────────
|
|
183
|
+
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 { emitStart, maybeRotate } 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,15 @@ 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 done = emitStart('agent.run.start', {
|
|
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
|
+
});
|
|
301
316
|
return new Promise((resolve, reject) => {
|
|
302
317
|
// Interactive mode inherits all stdio so the CLI owns the TTY (TUI
|
|
303
318
|
// rendering, raw-mode keystrokes, colored output). Headless mode pipes
|
|
@@ -338,11 +353,13 @@ async function spawnAgent(options) {
|
|
|
338
353
|
child.on('error', (err) => {
|
|
339
354
|
if (timer)
|
|
340
355
|
clearTimeout(timer);
|
|
356
|
+
done({ error: err.message, exitCode: -1 });
|
|
341
357
|
reject(err);
|
|
342
358
|
});
|
|
343
359
|
child.on('close', (code) => {
|
|
344
360
|
if (timer)
|
|
345
361
|
clearTimeout(timer);
|
|
362
|
+
done({ exitCode: code ?? 0 });
|
|
346
363
|
resolve({ exitCode: code ?? 0, stderr: stderrBuffer });
|
|
347
364
|
});
|
|
348
365
|
});
|
package/dist/lib/runner.js
CHANGED
|
@@ -14,6 +14,7 @@ import { resolveJobPrompt, parseTimeout, writeRunMeta, getRunDir, } from './rout
|
|
|
14
14
|
import { getRunsDir } from './state.js';
|
|
15
15
|
import { prepareJobHome, buildSpawnEnv } from './sandbox.js';
|
|
16
16
|
import { resolveModel, buildReasoningFlags } from './models.js';
|
|
17
|
+
import { emitStart, maybeRotate } from './events.js';
|
|
17
18
|
/** CLI command templates per agent, with {prompt} as a placeholder. */
|
|
18
19
|
const AGENT_COMMANDS = {
|
|
19
20
|
claude: ['claude', '-p', '--verbose', '{prompt}', '--output-format', 'stream-json', '--permission-mode', 'plan'],
|
|
@@ -107,6 +108,13 @@ function generateRunId() {
|
|
|
107
108
|
}
|
|
108
109
|
/** Execute a job synchronously (waits for completion or timeout before resolving). */
|
|
109
110
|
export async function executeJob(config) {
|
|
111
|
+
maybeRotate();
|
|
112
|
+
const done = emitStart('agent.run.start', {
|
|
113
|
+
agent: config.agent,
|
|
114
|
+
version: config.version,
|
|
115
|
+
jobName: config.name,
|
|
116
|
+
mode: config.mode,
|
|
117
|
+
});
|
|
110
118
|
const resolvedPrompt = resolveJobPrompt(config);
|
|
111
119
|
const cmd = buildJobCommand(config, resolvedPrompt);
|
|
112
120
|
const useSandbox = config.sandbox !== false;
|
|
@@ -160,6 +168,7 @@ export async function executeJob(config) {
|
|
|
160
168
|
meta.status = 'timeout';
|
|
161
169
|
meta.completedAt = new Date().toISOString();
|
|
162
170
|
writeRunMeta(meta);
|
|
171
|
+
done({ status: 'timeout', runId });
|
|
163
172
|
const reportPath = extractAndSaveReport(stdoutPath, config.agent, runDir);
|
|
164
173
|
resolve({ meta, reportPath });
|
|
165
174
|
}, timeoutMs);
|
|
@@ -176,6 +185,7 @@ export async function executeJob(config) {
|
|
|
176
185
|
meta.status = code === 0 ? 'completed' : 'failed';
|
|
177
186
|
meta.completedAt = new Date().toISOString();
|
|
178
187
|
writeRunMeta(meta);
|
|
188
|
+
done({ status: meta.status, exitCode: code, runId });
|
|
179
189
|
const reportPath = extractAndSaveReport(stdoutPath, config.agent, runDir);
|
|
180
190
|
resolve({ meta, reportPath });
|
|
181
191
|
});
|
|
@@ -191,6 +201,7 @@ export async function executeJob(config) {
|
|
|
191
201
|
meta.status = 'failed';
|
|
192
202
|
meta.completedAt = new Date().toISOString();
|
|
193
203
|
writeRunMeta(meta);
|
|
204
|
+
done({ status: 'failed', error: err.message, runId });
|
|
194
205
|
resolve({ meta, reportPath: null });
|
|
195
206
|
});
|
|
196
207
|
child.unref();
|
|
@@ -16,6 +16,7 @@ import * as os from 'os';
|
|
|
16
16
|
import * as path from 'path';
|
|
17
17
|
import * as yaml from 'yaml';
|
|
18
18
|
import { deleteKeychainToken, getKeychainToken, hasKeychainToken, listKeychainItems, parseBundleValue, resolveRef, secretsKeychainItem, setKeychainToken, } from './index.js';
|
|
19
|
+
import { emit } from '../events.js';
|
|
19
20
|
/** Allowed values for a secret's `type` metadata field. */
|
|
20
21
|
export const SECRET_TYPES = [
|
|
21
22
|
'api-key',
|
|
@@ -148,10 +149,15 @@ export function writeBundle(bundle) {
|
|
|
148
149
|
};
|
|
149
150
|
const json = JSON.stringify(payload);
|
|
150
151
|
setKeychainToken(bundleMetaItem(bundle.name), json, Boolean(bundle.icloud_sync));
|
|
152
|
+
emit('secrets.set', { bundle: bundle.name });
|
|
151
153
|
}
|
|
152
154
|
export function deleteBundle(name) {
|
|
153
155
|
validateBundleName(name);
|
|
154
|
-
|
|
156
|
+
const deleted = deleteKeychainToken(bundleMetaItem(name));
|
|
157
|
+
if (deleted) {
|
|
158
|
+
emit('secrets.delete', { bundle: name });
|
|
159
|
+
}
|
|
160
|
+
return deleted;
|
|
155
161
|
}
|
|
156
162
|
export function listBundles() {
|
|
157
163
|
let services;
|
package/dist/lib/skills.js
CHANGED
|
@@ -13,6 +13,7 @@ import * as yaml from 'yaml';
|
|
|
13
13
|
import { SKILLS_CAPABLE_AGENTS, ensureSkillsDir } from './agents.js';
|
|
14
14
|
import { getUserSkillsDir, getSkillsDir as getSystemSkillsDir, getProjectAgentsDir, getEnabledExtraRepos } from './state.js';
|
|
15
15
|
import { getEffectiveHome, getVersionHomePath, listInstalledVersions } from './versions.js';
|
|
16
|
+
import { emit } from './events.js';
|
|
16
17
|
const HOME = os.homedir();
|
|
17
18
|
/** User-scoped skills dir (~/.agents/skills/). Used for installs. */
|
|
18
19
|
export function getSkillsDir() {
|
|
@@ -255,6 +256,7 @@ export function installSkill(sourcePath, skillName, agents, method = 'symlink')
|
|
|
255
256
|
};
|
|
256
257
|
}
|
|
257
258
|
}
|
|
259
|
+
emit('skill.install', { skill: skillName, agents });
|
|
258
260
|
return { success: true };
|
|
259
261
|
}
|
|
260
262
|
/**
|
|
@@ -555,6 +557,7 @@ export function installSkillToVersion(agent, version, skillName, method = 'copy'
|
|
|
555
557
|
// Best-effort; failure here shouldn't unwind the install
|
|
556
558
|
}
|
|
557
559
|
}
|
|
560
|
+
emit('skill.install', { skill: skillName, agent, version });
|
|
558
561
|
return { success: true };
|
|
559
562
|
}
|
|
560
563
|
/**
|
|
@@ -616,6 +619,7 @@ export function uninstallSkill(skillName) {
|
|
|
616
619
|
catch {
|
|
617
620
|
// Ignore removal errors
|
|
618
621
|
}
|
|
622
|
+
emit('skill.remove', { skill: skillName });
|
|
619
623
|
return { success: true };
|
|
620
624
|
}
|
|
621
625
|
export function listInstalledSkills() {
|
package/dist/lib/versions.js
CHANGED
|
@@ -37,6 +37,7 @@ import { discoverPlugins, syncPluginToVersion, isPluginSynced, pluginSupportsAge
|
|
|
37
37
|
import { composeRulesFromState } from './rules/compose.js';
|
|
38
38
|
import { loadSyncManifest, saveSyncManifest, buildManifest, isSyncStale } from './sync-manifest.js';
|
|
39
39
|
import { PLUGINS_CAPABLE_AGENTS } from './agents.js';
|
|
40
|
+
import { emit } from './events.js';
|
|
40
41
|
import { safeJoin } from './paths.js';
|
|
41
42
|
import { installCommandSkillToVersion, listCommandSkillsInVersion, shouldInstallCommandAsSkill } from './command-skills.js';
|
|
42
43
|
/** Promisified exec for running shell commands. */
|
|
@@ -909,6 +910,7 @@ export function setGlobalDefault(agent, version) {
|
|
|
909
910
|
}
|
|
910
911
|
else {
|
|
911
912
|
meta.agents[agent] = version;
|
|
913
|
+
emit('version.switch', { agent, version });
|
|
912
914
|
}
|
|
913
915
|
writeMeta(meta);
|
|
914
916
|
}
|
|
@@ -989,6 +991,7 @@ export async function installVersion(agent, version, onProgress) {
|
|
|
989
991
|
/* non-fatal; the install itself succeeded */
|
|
990
992
|
}
|
|
991
993
|
}
|
|
994
|
+
emit('version.install', { agent, version: installedVersion });
|
|
992
995
|
return { success: true, installedVersion };
|
|
993
996
|
}
|
|
994
997
|
catch (err) {
|
|
@@ -997,6 +1000,7 @@ export async function installVersion(agent, version, onProgress) {
|
|
|
997
1000
|
if (fs.existsSync(versionDir)) {
|
|
998
1001
|
removeInstallArtifacts(versionDir);
|
|
999
1002
|
}
|
|
1003
|
+
emit('version.install', { agent, version, error: err.message });
|
|
1000
1004
|
return { success: false, installedVersion: version, error: err.message };
|
|
1001
1005
|
}
|
|
1002
1006
|
}
|
|
@@ -1086,6 +1090,7 @@ export function removeVersion(agent, version) {
|
|
|
1086
1090
|
// Ignore if already gone
|
|
1087
1091
|
}
|
|
1088
1092
|
}
|
|
1093
|
+
emit('version.remove', { agent, version });
|
|
1089
1094
|
return true;
|
|
1090
1095
|
}
|
|
1091
1096
|
/**
|
package/package.json
CHANGED