@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.
Files changed (39) hide show
  1. package/README.md +148 -1
  2. package/dist/commands/beta.js +6 -1
  3. package/dist/commands/exec.js +9 -2
  4. package/dist/commands/init.js +10 -0
  5. package/dist/commands/mcp.js +4 -4
  6. package/dist/commands/prune.d.ts +0 -20
  7. package/dist/commands/prune.js +268 -15
  8. package/dist/commands/secrets.js +83 -0
  9. package/dist/commands/teams.js +2 -3
  10. package/dist/commands/usage.js +6 -0
  11. package/dist/commands/versions.js +8 -6
  12. package/dist/lib/browser/chrome.js +1 -1
  13. package/dist/lib/browser/drivers/ssh.d.ts +1 -0
  14. package/dist/lib/browser/drivers/ssh.js +23 -2
  15. package/dist/lib/browser/ipc.js +1 -0
  16. package/dist/lib/browser/service.d.ts +3 -0
  17. package/dist/lib/browser/service.js +114 -6
  18. package/dist/lib/daemon.js +4 -4
  19. package/dist/lib/events.d.ts +159 -0
  20. package/dist/lib/events.js +441 -0
  21. package/dist/lib/exec.js +29 -6
  22. package/dist/lib/permissions.d.ts +6 -3
  23. package/dist/lib/permissions.js +38 -34
  24. package/dist/lib/routines.d.ts +15 -0
  25. package/dist/lib/routines.js +68 -0
  26. package/dist/lib/runner.js +15 -0
  27. package/dist/lib/secrets/bundles.js +7 -1
  28. package/dist/lib/secrets/index.d.ts +14 -11
  29. package/dist/lib/secrets/index.js +49 -21
  30. package/dist/lib/secrets/linux.d.ts +27 -0
  31. package/dist/lib/secrets/linux.js +161 -0
  32. package/dist/lib/session/db.d.ts +4 -0
  33. package/dist/lib/session/db.js +26 -0
  34. package/dist/lib/skills.js +4 -0
  35. package/dist/lib/usage.d.ts +1 -1
  36. package/dist/lib/usage.js +13 -46
  37. package/dist/lib/versions.js +16 -0
  38. package/package.json +1 -1
  39. package/scripts/postinstall.js +37 -9
@@ -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
  }
@@ -922,13 +922,12 @@ Name teammates with --name alice to refer to them as 'alice' instead of a UUID.
922
922
  .command('status [team]')
923
923
  .aliases(['s', 'st', 'check'])
924
924
  .description("Check in on a team: who's working, what files they touched, recent commands, last output. Pass --since for efficient delta polling.")
925
- .option('-f, --filter <state>', 'Show only teammates in this state: working, completed, failed, stopped, or all (default: all)', 'all')
925
+ .option('-f, --filter <state>', 'Show only teammates in this state: running, completed, failed, stopped, or all (default: all)', 'all')
926
926
  .option('-s, --since <iso>', 'Cursor from a previous status call; only show updates after this timestamp (enables efficient polling)')
927
927
  .option('--agent-id <id>', 'Show only this one teammate (by UUID or UUID prefix)')
928
928
  .option('--json', 'Output machine-readable JSON')
929
929
  .action(async (team, opts) => {
930
- // Map friendly 'working' → internal 'running' for filter.
931
- const filter = opts.filter === 'working' ? 'running' : opts.filter;
930
+ const filter = opts.filter;
932
931
  const mgr = mkManager();
933
932
  // No team given → drop into the picker (TTY) or fail clearly (script).
934
933
  if (!team) {
@@ -8,6 +8,12 @@ export function registerUsageCommand(program) {
8
8
  program
9
9
  .command('usage [agent]')
10
10
  .description('Show rate-limit / quota usage per agent')
11
+ .addHelpText('after', `
12
+ Examples:
13
+ agents usage Show usage for all installed agents
14
+ agents usage claude Show usage for Claude only
15
+ agents usage codex Show usage for Codex only
16
+ `)
11
17
  .action(async (agentFilter) => {
12
18
  const filter = agentFilter;
13
19
  const targets = filter
@@ -104,7 +104,7 @@ When to use:
104
104
  - Multi-account: install different versions for different accounts (each version has its own auth)
105
105
  - Project-specific: lock a version for a repo with --project
106
106
 
107
- Note: The first version you install is NOT set as default automatically. Run 'agents use' to set it.
107
+ Note: The first version you install becomes the default automatically.
108
108
  `)
109
109
  .action(async (specs, options) => {
110
110
  const isProject = options.project;
@@ -220,10 +220,14 @@ Note: The first version you install is NOT set as default automatically. Run 'ag
220
220
  console.log(chalk.green(` Synced: ${synced.join(', ')}`));
221
221
  }
222
222
  }
223
- // Prompt to set as default
223
+ // Set as default: auto-set if no default exists, otherwise prompt
224
224
  const currentDefault = getGlobalDefault(agent);
225
225
  if (currentDefault !== installedVersion) {
226
- if (skipPrompts) {
226
+ if (!currentDefault) {
227
+ // First install for this agent - auto-set without prompting
228
+ await setDefaultVersion(agent, installedVersion);
229
+ }
230
+ else if (skipPrompts) {
227
231
  await setDefaultVersion(agent, installedVersion);
228
232
  }
229
233
  else {
@@ -238,9 +242,7 @@ Note: The first version you install is NOT set as default automatically. Run 'ag
238
242
  info,
239
243
  });
240
244
  const accountHint = formatAccountHint(info, usage.snapshot);
241
- const message = currentDefault
242
- ? `Switch default from ${agentLabel(agentConfig.id)}@${currentDefault} to ${agentLabel(agentConfig.id)}@${installedVersion}${accountHint}?`
243
- : `Set ${agentLabel(agentConfig.id)}@${installedVersion}${accountHint} as default?`;
245
+ const message = `Switch default from ${agentLabel(agentConfig.id)}@${currentDefault} to ${agentLabel(agentConfig.id)}@${installedVersion}${accountHint}?`;
244
246
  const setAsDefault = await confirm({
245
247
  message,
246
248
  default: true,
@@ -120,7 +120,7 @@ export async function attachToChrome(port) {
120
120
  }
121
121
  export function killChrome(pid) {
122
122
  try {
123
- process.kill(pid, 'SIGTERM');
123
+ process.kill(pid, 'SIGINT');
124
124
  }
125
125
  catch {
126
126
  // Process already dead
@@ -7,3 +7,4 @@ export interface SSHConnection {
7
7
  cleanup: () => void;
8
8
  }
9
9
  export declare function connectSSH(endpoint: string, profile: BrowserProfile): Promise<SSHConnection>;
10
+ export declare function restartRemoteBrowser(user: string, host: string, browserType: string, port: number, customBinary?: string): Promise<void>;
@@ -9,7 +9,7 @@ export async function connectSSH(endpoint, profile) {
9
9
  }
10
10
  const user = url.username || process.env.USER || 'root';
11
11
  const host = url.hostname;
12
- const remotePort = parseInt(url.searchParams.get('port') || '9222', 10);
12
+ const remotePort = url.port ? parseInt(url.port, 10) : 9222;
13
13
  const localPort = allocatePort();
14
14
  try {
15
15
  await ensureRemoteBrowser(user, host, profile.browser, remotePort, profile.binary);
@@ -17,7 +17,7 @@ export async function connectSSH(endpoint, profile) {
17
17
  catch {
18
18
  // Browser may already be running, continue
19
19
  }
20
- const tunnel = await startSSHTunnel(user, host, localPort, remotePort);
20
+ let tunnel = await startSSHTunnel(user, host, localPort, remotePort);
21
21
  try {
22
22
  await waitForPort(localPort, 8000);
23
23
  }
@@ -133,6 +133,27 @@ async function ensureRemoteBrowser(user, host, browserType, port, customBinary)
133
133
  }, 2000);
134
134
  });
135
135
  }
136
+ export async function restartRemoteBrowser(user, host, browserType, port, customBinary) {
137
+ // Kill any process using the remote debugging port
138
+ const killCmd = `lsof -ti :${port} | xargs kill -9 2>/dev/null || true`;
139
+ await runSSHCommand(user, host, killCmd);
140
+ await sleep(500);
141
+ await ensureRemoteBrowser(user, host, browserType, port, customBinary);
142
+ await sleep(1500);
143
+ }
144
+ function runSSHCommand(user, host, cmd) {
145
+ return new Promise((resolve) => {
146
+ const child = spawn('ssh', [`${user}@${host}`, '-o', 'BatchMode=yes', cmd], {
147
+ stdio: 'ignore',
148
+ });
149
+ child.on('close', () => resolve());
150
+ child.on('error', () => resolve());
151
+ setTimeout(() => {
152
+ child.kill();
153
+ resolve();
154
+ }, 3000);
155
+ });
156
+ }
136
157
  function sleep(ms) {
137
158
  return new Promise((resolve) => setTimeout(resolve, ms));
138
159
  }
@@ -195,6 +195,7 @@ export async function sendIPCRequest(request) {
195
195
  if (!fs.existsSync(socketPath)) {
196
196
  throw new Error('Failed to start browser daemon');
197
197
  }
198
+ await new Promise((r) => setTimeout(r, 300));
198
199
  }
199
200
  return new Promise((resolve, reject) => {
200
201
  const socket = net.createConnection(socketPath);
@@ -2,6 +2,7 @@ import { type TabInfo, type ProfileStatus } from './types.js';
2
2
  import { type RefOpts, type RefNode } from './refs.js';
3
3
  export declare class BrowserService {
4
4
  private connections;
5
+ private forkingProfiles;
5
6
  start(profileName: string, taskId?: string): Promise<{
6
7
  task: string;
7
8
  windowTargetId?: string;
@@ -30,6 +31,8 @@ export declare class BrowserService {
30
31
  hover(taskId: string, tabId: string, ref: number): Promise<void>;
31
32
  status(profileName?: string): Promise<ProfileStatus[]>;
32
33
  shutdown(): Promise<void>;
34
+ private findAvailableFork;
35
+ private forkElectronProfile;
33
36
  private connectProfile;
34
37
  private connectEndpoint;
35
38
  private enableDomains;
@@ -2,14 +2,16 @@ import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { CDPClient, discoverBrowserWsUrl } from './cdp.js';
4
4
  import { getProfile, getProfileRuntimeDir, getBrowserRuntimeDir } from './profiles.js';
5
- import { killChrome, getRunningChromeInfo } from './chrome.js';
5
+ import { killChrome, getRunningChromeInfo, launchBrowser, allocatePort } from './chrome.js';
6
6
  import { connectLocal } from './drivers/local.js';
7
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();
14
+ forkingProfiles = new Set();
13
15
  async start(profileName, taskId) {
14
16
  const profile = await getProfile(profileName);
15
17
  if (!profile) {
@@ -20,7 +22,34 @@ export class BrowserService {
20
22
  throw new Error(`Invalid task ID "${finalTaskId}". Must be lowercase alphanumeric with hyphens.`);
21
23
  }
22
24
  let conn = this.connections.get(profileName);
23
- if (!conn) {
25
+ let effectiveProfileName = profileName;
26
+ if (conn && conn.electron && conn.tasks.size > 0) {
27
+ if (this.forkingProfiles.has(profileName)) {
28
+ while (this.forkingProfiles.has(profileName)) {
29
+ await new Promise((r) => setTimeout(r, 50));
30
+ }
31
+ const existingFork = this.findAvailableFork(profileName);
32
+ if (existingFork) {
33
+ conn = existingFork.conn;
34
+ effectiveProfileName = existingFork.name;
35
+ }
36
+ else {
37
+ throw new Error(`Fork in progress but no available fork found for "${profileName}"`);
38
+ }
39
+ }
40
+ else {
41
+ this.forkingProfiles.add(profileName);
42
+ try {
43
+ const { forkName, connection } = await this.forkElectronProfile(profile);
44
+ conn = connection;
45
+ effectiveProfileName = forkName;
46
+ }
47
+ finally {
48
+ this.forkingProfiles.delete(profileName);
49
+ }
50
+ }
51
+ }
52
+ else if (!conn) {
24
53
  conn = await this.connectProfile(profile);
25
54
  this.connections.set(profileName, conn);
26
55
  }
@@ -31,14 +60,15 @@ export class BrowserService {
31
60
  const { windowTargetId } = await this.createTaskWindow(conn, finalTaskId);
32
61
  const task = {
33
62
  id: finalTaskId,
34
- profile: profileName,
63
+ profile: effectiveProfileName,
35
64
  windowTargetId,
36
- tabIds: [],
65
+ tabIds: conn.electron && windowTargetId ? [windowTargetId] : [],
37
66
  createdAt: Date.now(),
38
67
  pid: conn.pid,
39
68
  };
40
69
  conn.tasks.set(finalTaskId, task);
41
- await this.saveTaskState(profileName, conn.tasks);
70
+ await this.saveTaskState(effectiveProfileName, conn.tasks);
71
+ emit('browser.launch', { profile: effectiveProfileName, task: finalTaskId, pid: conn.pid });
42
72
  return { task: finalTaskId, windowTargetId };
43
73
  }
44
74
  async stop(taskId) {
@@ -62,6 +92,12 @@ export class BrowserService {
62
92
  }
63
93
  conn.tasks.delete(taskId);
64
94
  await this.saveTaskState(profileName, conn.tasks);
95
+ emit('browser.close', { profile: profileName, task: taskId });
96
+ if (conn.forkedFrom && conn.tasks.size === 0) {
97
+ conn.cdp.close();
98
+ killChrome(conn.pid);
99
+ this.connections.delete(profileName);
100
+ }
65
101
  return { ok: true, profile: profileName };
66
102
  }
67
103
  }
@@ -256,6 +292,37 @@ export class BrowserService {
256
292
  }
257
293
  this.connections.clear();
258
294
  }
295
+ findAvailableFork(profileName) {
296
+ for (const [name, conn] of this.connections) {
297
+ if (conn.forkedFrom === profileName && conn.tasks.size === 0) {
298
+ return { name, conn };
299
+ }
300
+ }
301
+ return null;
302
+ }
303
+ async forkElectronProfile(profile) {
304
+ let forkNum = 2;
305
+ while (this.connections.has(`${profile.name}.${forkNum}`)) {
306
+ forkNum++;
307
+ }
308
+ const forkName = `${profile.name}.${forkNum}`;
309
+ const port = allocatePort();
310
+ const { pid, wsUrl } = await launchBrowser(forkName, profile.browser, port, profile.chrome, profile.secrets, profile.binary);
311
+ const cdp = new CDPClient();
312
+ await cdp.connect(wsUrl);
313
+ await this.enableDomains(cdp);
314
+ const connection = {
315
+ cdp,
316
+ port,
317
+ pid,
318
+ electron: true,
319
+ forkedFrom: profile.name,
320
+ tasks: new Map(),
321
+ sessionCache: new Map(),
322
+ };
323
+ this.connections.set(forkName, connection);
324
+ return { forkName, connection };
325
+ }
259
326
  async connectProfile(profile) {
260
327
  const existingInfo = getRunningChromeInfo(profile.name);
261
328
  if (existingInfo) {
@@ -311,6 +378,34 @@ export class BrowserService {
311
378
  sessionCache: new Map(),
312
379
  };
313
380
  }
381
+ if (url.protocol === 'wss:' || url.protocol === 'ws:') {
382
+ const cdp = new CDPClient();
383
+ await cdp.connect(endpoint);
384
+ await this.enableDomains(cdp);
385
+ return {
386
+ cdp,
387
+ port: 0,
388
+ pid: 0,
389
+ electron: profile.electron,
390
+ tasks: this.loadTaskState(profile.name),
391
+ sessionCache: new Map(),
392
+ };
393
+ }
394
+ if (url.protocol === 'http:' || url.protocol === 'https:') {
395
+ const port = parseInt(url.port || (url.protocol === 'https:' ? '443' : '80'), 10);
396
+ const wsUrl = await discoverBrowserWsUrl(port, url.hostname);
397
+ const cdp = new CDPClient();
398
+ await cdp.connect(wsUrl);
399
+ await this.enableDomains(cdp);
400
+ return {
401
+ cdp,
402
+ port,
403
+ pid: 0,
404
+ electron: profile.electron,
405
+ tasks: this.loadTaskState(profile.name),
406
+ sessionCache: new Map(),
407
+ };
408
+ }
314
409
  return null;
315
410
  }
316
411
  async enableDomains(cdp) {
@@ -320,7 +415,20 @@ export class BrowserService {
320
415
  if (conn.electron) {
321
416
  const { targetInfos } = (await conn.cdp.send('Target.getTargets'));
322
417
  const pageTarget = targetInfos.find((t) => t.type === 'page');
323
- return { windowTargetId: pageTarget?.targetId };
418
+ if (pageTarget) {
419
+ return { windowTargetId: pageTarget.targetId };
420
+ }
421
+ // No existing page - try to create one (works on some Electron apps)
422
+ try {
423
+ const result = (await conn.cdp.send('Target.createTarget', {
424
+ url: 'about:blank',
425
+ newWindow: true,
426
+ }));
427
+ return { windowTargetId: result.targetId };
428
+ }
429
+ catch {
430
+ throw new Error('No page targets found and unable to create new window');
431
+ }
324
432
  }
325
433
  const result = (await conn.cdp.send('Target.createTarget', {
326
434
  url: 'about:blank',
@@ -6,7 +6,7 @@
6
6
  * (macOS), systemd (Linux), or as a plain detached process. PID tracking,
7
7
  * log output, reload (SIGHUP), and graceful shutdown are handled here.
8
8
  */
9
- import { spawn, execSync } from 'child_process';
9
+ import { spawn, execSync, execFileSync } from 'child_process';
10
10
  import * as fs from 'fs';
11
11
  import * as path from 'path';
12
12
  import * as os from 'os';
@@ -297,10 +297,10 @@ function startDaemonLocked() {
297
297
  }
298
298
  fs.writeFileSync(plistPath, generateLaunchdPlist(), 'utf-8');
299
299
  try {
300
- execSync(`launchctl unload "${plistPath}" 2>/dev/null`, { encoding: 'utf-8' });
300
+ execFileSync('launchctl', ['unload', plistPath], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
301
301
  }
302
302
  catch { /* not loaded, expected */ }
303
- execSync(`launchctl load "${plistPath}"`, { encoding: 'utf-8' });
303
+ execFileSync('launchctl', ['load', plistPath], { encoding: 'utf-8' });
304
304
  const pid = waitForPid(3000);
305
305
  return { pid, method: 'launchd' };
306
306
  }
@@ -358,7 +358,7 @@ export function stopDaemon() {
358
358
  const plistPath = getLaunchdPlistPath();
359
359
  if (fs.existsSync(plistPath)) {
360
360
  try {
361
- execSync(`launchctl unload "${plistPath}"`, { encoding: 'utf-8' });
361
+ execFileSync('launchctl', ['unload', plistPath], { encoding: 'utf-8' });
362
362
  fs.unlinkSync(plistPath);
363
363
  }
364
364
  catch (err) {
@@ -0,0 +1,159 @@
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
+ export type EventType = 'agent.run.start' | 'agent.run.end' | 'agent.spawn.start' | 'agent.spawn.end' | 'version.install' | 'version.switch' | 'version.remove' | 'skill.install' | 'skill.remove' | 'browser.launch' | 'browser.close' | 'browser.navigate' | 'browser.screenshot' | 'secrets.get' | 'secrets.set' | 'secrets.delete' | 'cloud.dispatch' | 'cloud.complete' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'resource.sync' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'error' | 'warn' | 'info' | 'debug';
15
+ export interface EventMeta {
16
+ ts: string;
17
+ tz: string;
18
+ tzName: string;
19
+ hostname: string;
20
+ platform: NodeJS.Platform;
21
+ arch: string;
22
+ pid: number;
23
+ ppid: number;
24
+ event: EventType;
25
+ }
26
+ export interface EventPayload {
27
+ agent?: string;
28
+ version?: string;
29
+ sessionId?: string;
30
+ cwd?: string;
31
+ command?: string;
32
+ args?: string[];
33
+ input?: string;
34
+ output?: string;
35
+ prompt?: string;
36
+ durationMs?: number;
37
+ startupMs?: number;
38
+ exitCode?: number;
39
+ status?: string;
40
+ error?: string;
41
+ errorStack?: string;
42
+ [key: string]: unknown;
43
+ }
44
+ export type EventRecord = EventMeta & EventPayload;
45
+ /**
46
+ * Truncate a string to maxLength, adding ellipsis if truncated.
47
+ * Returns undefined for null/undefined input.
48
+ */
49
+ export declare function truncate(str: string | null | undefined, maxLength?: number): string | undefined;
50
+ /**
51
+ * Emit a structured event to the daily log file.
52
+ *
53
+ * @param event - The event type
54
+ * @param payload - Event-specific data (agent, version, cwd, etc.)
55
+ */
56
+ export declare function emit(event: EventType, payload?: EventPayload): void;
57
+ /**
58
+ * Convenience wrapper for timed operations.
59
+ * Returns a function to call when the operation completes.
60
+ *
61
+ * @example
62
+ * const done = emitStart('agent.run.start', { agent: 'claude' });
63
+ * // ... do work ...
64
+ * done({ exitCode: 0 }); // emits agent.run.end with durationMs
65
+ */
66
+ export declare function emitStart(startEvent: EventType, payload?: EventPayload): (endPayload?: EventPayload) => void;
67
+ /**
68
+ * Measure execution time of a synchronous function.
69
+ * Emits a perf.timing event with the duration.
70
+ *
71
+ * @example
72
+ * const result = time('parse-config', () => parseConfig(path));
73
+ */
74
+ export declare function time<T>(label: string, fn: () => T, payload?: EventPayload): T;
75
+ /**
76
+ * Measure execution time of an async function.
77
+ * Emits a perf.timing event with the duration.
78
+ *
79
+ * @example
80
+ * const result = await timeAsync('fetch-data', () => fetchData(url));
81
+ */
82
+ export declare function timeAsync<T>(label: string, fn: () => Promise<T>, payload?: EventPayload): Promise<T>;
83
+ /**
84
+ * Create a timing context for measuring multiple phases of an operation.
85
+ * Useful for tracking startup time vs execution time.
86
+ *
87
+ * @example
88
+ * const timer = createTimer('agent.run', { agent: 'claude' });
89
+ * // ... setup work ...
90
+ * timer.mark('startup'); // records startup time
91
+ * // ... main work ...
92
+ * timer.end({ exitCode: 0 }); // records total time and emits event
93
+ */
94
+ export declare function createTimer(label: string, payload?: EventPayload): {
95
+ mark: (phase: string) => number;
96
+ end: (endPayload?: EventPayload) => void;
97
+ elapsed: () => number;
98
+ };
99
+ /**
100
+ * Higher-order function that wraps an async function with timing.
101
+ * The wrapper emits start/end events automatically.
102
+ *
103
+ * @example
104
+ * const timedFetch = withTiming('fetch', fetchData, { service: 'api' });
105
+ * const result = await timedFetch(url);
106
+ */
107
+ export declare function withTiming<Args extends unknown[], R>(label: string, fn: (...args: Args) => Promise<R>, basePayload?: EventPayload): (...args: Args) => Promise<R>;
108
+ /**
109
+ * Emit a command.start event with CLI args.
110
+ * Returns a done() function to emit command.end with duration.
111
+ *
112
+ * @example
113
+ * // At CLI entry point:
114
+ * const done = emitCommand('run', process.argv.slice(2));
115
+ * // ... execute command ...
116
+ * done({ exitCode: 0 });
117
+ */
118
+ export declare function emitCommand(command: string, args?: string[], payload?: EventPayload): (endPayload?: EventPayload) => void;
119
+ /**
120
+ * Emit an error event with full details.
121
+ */
122
+ export declare function emitError(err: Error | string, payload?: EventPayload): void;
123
+ /**
124
+ * Remove log files older than the retention period.
125
+ * Called lazily on emit or explicitly via CLI.
126
+ *
127
+ * @param retentionDays - Number of days to keep (default 30)
128
+ * @returns Number of files removed
129
+ */
130
+ export declare function rotate(retentionDays?: number): number;
131
+ export declare function maybeRotate(): void;
132
+ /**
133
+ * Read events from log files within a date range.
134
+ *
135
+ * @param options - Query options
136
+ * @returns Array of event records
137
+ */
138
+ export declare function query(options: {
139
+ startDate?: Date;
140
+ endDate?: Date;
141
+ eventTypes?: EventType[];
142
+ agent?: string;
143
+ command?: string;
144
+ limit?: number;
145
+ }): EventRecord[];
146
+ /**
147
+ * Get performance stats for a specific label.
148
+ */
149
+ export declare function getTimingStats(label: string, options?: {
150
+ days?: number;
151
+ }): {
152
+ count: number;
153
+ avgMs: number;
154
+ minMs: number;
155
+ maxMs: number;
156
+ p50Ms: number;
157
+ p95Ms: number;
158
+ } | null;
159
+ export declare const LOGS_PATH: string;