@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
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
|
}
|
package/dist/commands/teams.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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) {
|
package/dist/commands/usage.js
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
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 (
|
|
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,
|
|
@@ -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.
|
|
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
|
-
|
|
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
|
}
|
package/dist/lib/browser/ipc.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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',
|
package/dist/lib/daemon.js
CHANGED
|
@@ -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
|
-
|
|
300
|
+
execFileSync('launchctl', ['unload', plistPath], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
301
301
|
}
|
|
302
302
|
catch { /* not loaded, expected */ }
|
|
303
|
-
|
|
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
|
-
|
|
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;
|