@phnx-labs/agents-cli 1.18.4 → 1.18.6
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/CHANGELOG.md +18 -7
- package/README.md +1 -1
- package/dist/commands/browser.js +275 -141
- package/dist/commands/factory.js +13 -1
- package/dist/commands/rules.js +14 -0
- package/dist/commands/secrets.js +66 -11
- package/dist/lib/browser/cdp.js +7 -1
- package/dist/lib/browser/chrome.d.ts +1 -1
- package/dist/lib/browser/chrome.js +52 -26
- package/dist/lib/browser/drivers/local.js +29 -2
- package/dist/lib/browser/drivers/ssh.js +82 -7
- package/dist/lib/browser/ipc.js +55 -0
- package/dist/lib/browser/profiles.d.ts +46 -2
- package/dist/lib/browser/profiles.js +123 -19
- package/dist/lib/browser/runtime-state.d.ts +117 -0
- package/dist/lib/browser/runtime-state.js +259 -0
- package/dist/lib/browser/service.d.ts +16 -0
- package/dist/lib/browser/service.js +163 -16
- package/dist/lib/browser/types.d.ts +13 -1
- package/dist/lib/daemon.js +36 -3
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/secrets/bundles.d.ts +20 -0
- package/dist/lib/secrets/bundles.js +56 -0
- package/dist/lib/secrets/index.js +8 -8
- package/dist/lib/teams/agents.d.ts +1 -1
- package/dist/lib/teams/agents.js +1 -1
- package/dist/lib/types.d.ts +4 -0
- package/dist/lib/version.d.ts +7 -0
- package/dist/lib/version.js +25 -0
- package/package.json +1 -1
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
2
3
|
import * as path from 'path';
|
|
4
|
+
import { execFile } from 'child_process';
|
|
5
|
+
import { promisify } from 'util';
|
|
3
6
|
import { CDPClient, discoverBrowserWsUrl, verifyBrowserIdentity } from './cdp.js';
|
|
4
7
|
import { getProfile, getProfileRuntimeDir, getBrowserRuntimeDir, listProfiles, extractConfiguredPort, resolveEndpoint, } from './profiles.js';
|
|
5
8
|
import { killChrome, getRunningChromeInfo, launchBrowser, allocatePort } from './chrome.js';
|
|
6
9
|
import { connectLocal } from './drivers/local.js';
|
|
7
10
|
import { connectSSH } from './drivers/ssh.js';
|
|
11
|
+
import { clearProfileRuntime } from './runtime-state.js';
|
|
8
12
|
import { generateTaskId, generateShortId, generateTaskName, } from './types.js';
|
|
9
13
|
import { getRefs, resolveRefToCoords } from './refs.js';
|
|
10
14
|
import { clickAtCoords, hoverAtCoords, scrollAtCoords, typeText, pressKey, focusNode } from './input.js';
|
|
@@ -124,7 +128,81 @@ export function pickWindowTarget(targets, filter) {
|
|
|
124
128
|
return visible;
|
|
125
129
|
return pages[0];
|
|
126
130
|
}
|
|
131
|
+
const execFileP = promisify(execFile);
|
|
132
|
+
/**
|
|
133
|
+
* Parse a `--since`/`--until` value. Accepts ISO-8601 absolute timestamps
|
|
134
|
+
* or relative offsets like `30s`, `5m`, `2h`, `1d`.
|
|
135
|
+
*/
|
|
136
|
+
export function parseSinceUntil(s) {
|
|
137
|
+
const ms = Date.parse(s);
|
|
138
|
+
if (!isNaN(ms))
|
|
139
|
+
return new Date(ms);
|
|
140
|
+
const m = s.match(/^(\d+)([smhd])$/);
|
|
141
|
+
if (!m)
|
|
142
|
+
throw new Error(`Invalid since/until: ${s}`);
|
|
143
|
+
const n = parseInt(m[1], 10);
|
|
144
|
+
const unitMs = { s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 };
|
|
145
|
+
return new Date(Date.now() - n * unitMs[m[2]]);
|
|
146
|
+
}
|
|
147
|
+
async function execSSH(host, cmd) {
|
|
148
|
+
const { stdout } = await execFileP('ssh', [host, cmd], {
|
|
149
|
+
timeout: 10_000,
|
|
150
|
+
maxBuffer: 10_000_000,
|
|
151
|
+
});
|
|
152
|
+
return stdout;
|
|
153
|
+
}
|
|
154
|
+
export function readNewestMatchingFile(dir, prefix, tailLines) {
|
|
155
|
+
let entries;
|
|
156
|
+
try {
|
|
157
|
+
entries = fs.readdirSync(dir);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return '';
|
|
161
|
+
}
|
|
162
|
+
const candidates = entries
|
|
163
|
+
.filter((f) => f.startsWith(prefix) && f.endsWith('.jsonl'))
|
|
164
|
+
.map((f) => ({ f, mtime: fs.statSync(path.join(dir, f)).mtimeMs }))
|
|
165
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
166
|
+
if (candidates.length === 0)
|
|
167
|
+
return '';
|
|
168
|
+
const lines = fs
|
|
169
|
+
.readFileSync(path.join(dir, candidates[0].f), 'utf8')
|
|
170
|
+
.split('\n')
|
|
171
|
+
.filter(Boolean);
|
|
172
|
+
return lines.slice(-tailLines).join('\n');
|
|
173
|
+
}
|
|
174
|
+
function expandHome(p) {
|
|
175
|
+
if (p.startsWith('~/'))
|
|
176
|
+
return path.join(os.homedir(), p.slice(2));
|
|
177
|
+
if (p === '~')
|
|
178
|
+
return os.homedir();
|
|
179
|
+
return p;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Probe a cached connection before reuse. A WebSocket can quietly transition
|
|
183
|
+
* to CLOSED without anyone noticing — most commonly when the user kills the
|
|
184
|
+
* browser process by hand. `Browser.getVersion` is the lightest CDP call we
|
|
185
|
+
* can make; if it doesn't round-trip within 1s the connection is dead.
|
|
186
|
+
*/
|
|
187
|
+
async function isConnHealthy(conn, timeoutMs = 1000) {
|
|
188
|
+
if (!conn.cdp.isOpen)
|
|
189
|
+
return false;
|
|
190
|
+
try {
|
|
191
|
+
await Promise.race([
|
|
192
|
+
conn.cdp.send('Browser.getVersion'),
|
|
193
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('healthcheck timeout')), timeoutMs)),
|
|
194
|
+
]);
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
127
201
|
export class BrowserService {
|
|
202
|
+
static SOURCE_PREFIX = {
|
|
203
|
+
'rush-app': 'rush-app-',
|
|
204
|
+
'rush-cli': 'rush-cli-',
|
|
205
|
+
};
|
|
128
206
|
connections = new Map();
|
|
129
207
|
forkingProfiles = new Set();
|
|
130
208
|
// Per-task storage for console, errors, network, downloads
|
|
@@ -163,6 +241,20 @@ export class BrowserService {
|
|
|
163
241
|
const taskId = generateTaskId();
|
|
164
242
|
let conn = this.connections.get(composite);
|
|
165
243
|
let effectiveProfileName = composite;
|
|
244
|
+
// If we have a cached connection, confirm it's still usable before any
|
|
245
|
+
// caller relies on it. A browser killed externally (Cmd-Q, crash, or a
|
|
246
|
+
// user clearing the profile by hand) leaves a closed WebSocket here.
|
|
247
|
+
// Without this check the next `cdp.send` throws "CDP connection not
|
|
248
|
+
// open" and the user has no way to recover short of killing the daemon.
|
|
249
|
+
if (conn && !(await isConnHealthy(conn))) {
|
|
250
|
+
try {
|
|
251
|
+
conn.cdp.close();
|
|
252
|
+
}
|
|
253
|
+
catch { /* already closed */ }
|
|
254
|
+
conn.cleanup?.();
|
|
255
|
+
this.connections.delete(composite);
|
|
256
|
+
conn = undefined;
|
|
257
|
+
}
|
|
166
258
|
if (conn && conn.electron && conn.tasks.size > 0) {
|
|
167
259
|
if (this.forkingProfiles.has(composite)) {
|
|
168
260
|
while (this.forkingProfiles.has(composite)) {
|
|
@@ -274,6 +366,7 @@ export class BrowserService {
|
|
|
274
366
|
if (conn.forkedFrom && conn.tasks.size === 0) {
|
|
275
367
|
conn.cdp.close();
|
|
276
368
|
killChrome(conn.pid);
|
|
369
|
+
conn.cleanup?.();
|
|
277
370
|
this.connections.delete(profileName);
|
|
278
371
|
}
|
|
279
372
|
return { ok: true, profile: profileName };
|
|
@@ -289,6 +382,7 @@ export class BrowserService {
|
|
|
289
382
|
if (conn) {
|
|
290
383
|
conn.cdp.close();
|
|
291
384
|
killChrome(conn.pid);
|
|
385
|
+
conn.cleanup?.();
|
|
292
386
|
this.connections.delete(profileName);
|
|
293
387
|
}
|
|
294
388
|
const runtimeDir = getProfileRuntimeDir(profileName);
|
|
@@ -1082,6 +1176,46 @@ export class BrowserService {
|
|
|
1082
1176
|
}
|
|
1083
1177
|
throw new Error(`No response matching "${urlPattern}" within ${timeout}ms`);
|
|
1084
1178
|
}
|
|
1179
|
+
// ─── App Logs (source-side JSONL) ───────────────────────────────────────────
|
|
1180
|
+
async getAppLogs(taskId, opts) {
|
|
1181
|
+
const { task } = await this.findTask(taskId);
|
|
1182
|
+
const baseProfileName = task.profile.split('@')[0];
|
|
1183
|
+
const profile = await getProfile(baseProfileName);
|
|
1184
|
+
if (!profile?.logDir) {
|
|
1185
|
+
throw new Error(`Profile '${task.profile}' has no logDir set`);
|
|
1186
|
+
}
|
|
1187
|
+
const logDir = expandHome(profile.logDir);
|
|
1188
|
+
const sources = opts.source ? [opts.source] : ['rush-app', 'rush-cli'];
|
|
1189
|
+
const since = opts.since ? parseSinceUntil(opts.since) : null;
|
|
1190
|
+
const until = opts.until ? parseSinceUntil(opts.until) : null;
|
|
1191
|
+
const tailN = since ? 100_000 : (opts.lines ?? 200);
|
|
1192
|
+
const raws = await Promise.all(sources.map(async (src) => {
|
|
1193
|
+
const prefix = BrowserService.SOURCE_PREFIX[src];
|
|
1194
|
+
if (!prefix)
|
|
1195
|
+
return '';
|
|
1196
|
+
if (profile.logHost) {
|
|
1197
|
+
return execSSH(profile.logHost, `ls -1t ${logDir}/${prefix}*.jsonl 2>/dev/null | head -1 | xargs -r tail -n ${tailN}`);
|
|
1198
|
+
}
|
|
1199
|
+
return readNewestMatchingFile(logDir, prefix, tailN);
|
|
1200
|
+
}));
|
|
1201
|
+
const entries = raws
|
|
1202
|
+
.flatMap((r) => r.split('\n').filter(Boolean))
|
|
1203
|
+
.map((line) => {
|
|
1204
|
+
try {
|
|
1205
|
+
return JSON.parse(line);
|
|
1206
|
+
}
|
|
1207
|
+
catch {
|
|
1208
|
+
return { raw: line };
|
|
1209
|
+
}
|
|
1210
|
+
})
|
|
1211
|
+
.filter((e) => !opts.level || e.level === opts.level)
|
|
1212
|
+
.filter((e) => !opts.message || e.message === opts.message)
|
|
1213
|
+
.filter((e) => !opts.filter || JSON.stringify(e).includes(opts.filter))
|
|
1214
|
+
.filter((e) => !since || (e.timestamp && new Date(e.timestamp) >= since))
|
|
1215
|
+
.filter((e) => !until || (e.timestamp && new Date(e.timestamp) <= until))
|
|
1216
|
+
.sort((a, b) => new Date(a.timestamp ?? 0).getTime() - new Date(b.timestamp ?? 0).getTime());
|
|
1217
|
+
return since ? entries : entries.slice(-(opts.lines ?? 200));
|
|
1218
|
+
}
|
|
1085
1219
|
// ─── Wait Conditions ─────────────────────────────────────────────────────────
|
|
1086
1220
|
async wait(taskId, type, value, options = {}) {
|
|
1087
1221
|
const timeout = options.timeout ?? 30000;
|
|
@@ -1213,6 +1347,7 @@ export class BrowserService {
|
|
|
1213
1347
|
}
|
|
1214
1348
|
for (const [, conn] of this.connections) {
|
|
1215
1349
|
conn.cdp.close();
|
|
1350
|
+
conn.cleanup?.();
|
|
1216
1351
|
}
|
|
1217
1352
|
this.connections.clear();
|
|
1218
1353
|
}
|
|
@@ -1232,7 +1367,7 @@ export class BrowserService {
|
|
|
1232
1367
|
const forkName = `${profile.name}.${forkNum}`;
|
|
1233
1368
|
const port = allocatePort();
|
|
1234
1369
|
const chromeOpts = { ...profile.chrome, viewport: profile.viewport };
|
|
1235
|
-
const { pid, wsUrl } = await launchBrowser(forkName, profile.browser, port, chromeOpts, profile.secrets, profile.binary);
|
|
1370
|
+
const { pid, wsUrl } = await launchBrowser(forkName, profile.browser, port, chromeOpts, profile.secrets, profile.binary, profile.electron === true);
|
|
1236
1371
|
const cdp = new CDPClient();
|
|
1237
1372
|
await cdp.connect(wsUrl);
|
|
1238
1373
|
await this.enableDomains(cdp);
|
|
@@ -1261,21 +1396,32 @@ export class BrowserService {
|
|
|
1261
1396
|
async connectProfile(effectiveProfile, target) {
|
|
1262
1397
|
const existingInfo = getRunningChromeInfo(effectiveProfile.name);
|
|
1263
1398
|
if (existingInfo) {
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1399
|
+
try {
|
|
1400
|
+
const { wsUrl, browser } = await discoverBrowserWsUrl(existingInfo.port);
|
|
1401
|
+
verifyBrowserIdentity(browser, effectiveProfile.browser, existingInfo.port);
|
|
1402
|
+
const cdp = new CDPClient();
|
|
1403
|
+
await cdp.connect(wsUrl);
|
|
1404
|
+
await this.enableDomains(cdp);
|
|
1405
|
+
const tasks = this.loadTaskState(effectiveProfile.name);
|
|
1406
|
+
return {
|
|
1407
|
+
cdp,
|
|
1408
|
+
port: existingInfo.port,
|
|
1409
|
+
pid: existingInfo.pid,
|
|
1410
|
+
electron: effectiveProfile.electron,
|
|
1411
|
+
targetFilter: effectiveProfile.targetFilter,
|
|
1412
|
+
tasks,
|
|
1413
|
+
sessionCache: new Map(),
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
catch (err) {
|
|
1417
|
+
// pid file says a process is alive on that port, but nothing is
|
|
1418
|
+
// actually responding to CDP — most commonly because the user
|
|
1419
|
+
// changed the configured endpoint port after a previous launch,
|
|
1420
|
+
// or because the OS reused the pid for an unrelated process.
|
|
1421
|
+
// Wipe the stale runtime files and fall through to a fresh
|
|
1422
|
+
// connect against the profile's currently-configured endpoint.
|
|
1423
|
+
clearProfileRuntime(effectiveProfile.name);
|
|
1424
|
+
}
|
|
1279
1425
|
}
|
|
1280
1426
|
const conn = await this.connectEndpoint(effectiveProfile, target);
|
|
1281
1427
|
if (!conn) {
|
|
@@ -1309,6 +1455,7 @@ export class BrowserService {
|
|
|
1309
1455
|
targetFilter: profile.targetFilter,
|
|
1310
1456
|
tasks: new Map(),
|
|
1311
1457
|
sessionCache: new Map(),
|
|
1458
|
+
cleanup: conn.cleanup,
|
|
1312
1459
|
};
|
|
1313
1460
|
}
|
|
1314
1461
|
if (url.protocol === 'wss:' || url.protocol === 'ws:') {
|
|
@@ -41,6 +41,10 @@ export interface BrowserProfile {
|
|
|
41
41
|
x?: number;
|
|
42
42
|
y?: number;
|
|
43
43
|
};
|
|
44
|
+
/** Directory holding source-side JSONL logs (e.g. ~/.rush/logs). */
|
|
45
|
+
logDir?: string;
|
|
46
|
+
/** Optional SSH host where logDir lives, e.g. "muqsit@mac-mini". */
|
|
47
|
+
logHost?: string;
|
|
44
48
|
}
|
|
45
49
|
/** Parsed form of `BrowserProfile.targetFilter`. */
|
|
46
50
|
export interface TargetFilter {
|
|
@@ -105,7 +109,7 @@ export interface HistoricalTask {
|
|
|
105
109
|
domains: string[];
|
|
106
110
|
tabCount: number;
|
|
107
111
|
}
|
|
108
|
-
export type IPCAction = 'start' | 'record-start' | 'record-stop' | 'done' | 'stop' | 'status' | 'history' | 'navigate' | 'tab-add' | 'tab-focus' | 'tab-close' | 'tab-list' | 'evaluate' | 'screenshot' | 'refs' | 'click' | 'type' | 'press' | 'hover' | 'scroll' | 'set-viewport' | 'set-device' | 'console' | 'errors' | 'requests' | 'response-body' | 'wait' | 'set-download-path' | 'wait-download' | 'upload';
|
|
112
|
+
export type IPCAction = 'start' | 'record-start' | 'record-stop' | 'done' | 'stop' | 'status' | 'history' | 'navigate' | 'tab-add' | 'tab-focus' | 'tab-close' | 'tab-list' | 'evaluate' | 'screenshot' | 'refs' | 'click' | 'type' | 'press' | 'hover' | 'scroll' | 'set-viewport' | 'set-device' | 'console' | 'errors' | 'requests' | 'response-body' | 'wait' | 'set-download-path' | 'wait-download' | 'upload' | 'getAppLogs' | 'version';
|
|
109
113
|
export interface IPCRequest {
|
|
110
114
|
action: IPCAction;
|
|
111
115
|
task?: string;
|
|
@@ -146,6 +150,12 @@ export interface IPCRequest {
|
|
|
146
150
|
fps?: number;
|
|
147
151
|
duration?: number;
|
|
148
152
|
maxMb?: number;
|
|
153
|
+
source?: string;
|
|
154
|
+
lines?: number;
|
|
155
|
+
message?: string;
|
|
156
|
+
since?: string;
|
|
157
|
+
until?: string;
|
|
158
|
+
appLevel?: string;
|
|
149
159
|
}
|
|
150
160
|
/** Subset of IPCResponse describing a recording start result. */
|
|
151
161
|
export interface RecordStartFields {
|
|
@@ -188,6 +198,8 @@ export interface IPCResponse {
|
|
|
188
198
|
downloadPath?: string;
|
|
189
199
|
devices?: string[];
|
|
190
200
|
uploadMode?: 'input' | 'drop' | 'chooser';
|
|
201
|
+
appLogs?: any[];
|
|
202
|
+
version?: string;
|
|
191
203
|
}
|
|
192
204
|
export interface ConsoleEntry {
|
|
193
205
|
level: 'log' | 'info' | 'warn' | 'error';
|
package/dist/lib/daemon.js
CHANGED
|
@@ -178,6 +178,24 @@ export async function runDaemon() {
|
|
|
178
178
|
for (const job of scheduled) {
|
|
179
179
|
log('INFO', ` ${job.name} -> next: ${job.nextRun?.toISOString() || 'unknown'}`);
|
|
180
180
|
}
|
|
181
|
+
// Before the BrowserService comes up, reap browser + tunnel processes
|
|
182
|
+
// spawned by previous daemons that are no longer alive. Without this,
|
|
183
|
+
// a daemon hard-crash (SIGKILL, OOM) would leak every browser and SSH
|
|
184
|
+
// tunnel it had open — and the next session would either hijack those
|
|
185
|
+
// (cdp:// profile silently driven via stale ssh tunnel) or fail to
|
|
186
|
+
// bind because the ports are still claimed.
|
|
187
|
+
try {
|
|
188
|
+
const { reapOrphanedProcesses } = await import('./browser/runtime-state.js');
|
|
189
|
+
const result = reapOrphanedProcesses();
|
|
190
|
+
if (result.reaped > 0) {
|
|
191
|
+
log('INFO', `Reaped ${result.reaped} orphan process(es) from prior daemon(s)`);
|
|
192
|
+
for (const d of result.details)
|
|
193
|
+
log('INFO', ` ${d}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
log('ERROR', `Orphan reaper failed: ${err.message}`);
|
|
198
|
+
}
|
|
181
199
|
const browserService = new BrowserService();
|
|
182
200
|
const browserIPC = new BrowserIPCServer(browserService);
|
|
183
201
|
try {
|
|
@@ -259,6 +277,14 @@ Environment=PATH=/usr/local/bin:/usr/bin:/bin:${os.homedir()}/.nvm/versions/node
|
|
|
259
277
|
WantedBy=default.target`;
|
|
260
278
|
}
|
|
261
279
|
function getAgentsBinPath() {
|
|
280
|
+
// Prefer the binary actively executing this code. `which agents` returns
|
|
281
|
+
// whatever happens to be first on PATH, which means a side-by-side dev
|
|
282
|
+
// build at ~/.local/bin would silently spawn the registry-installed
|
|
283
|
+
// daemon and run stale code. process.argv[1] is the absolute path of
|
|
284
|
+
// the JS entrypoint the user actually invoked.
|
|
285
|
+
const argv1 = process.argv[1];
|
|
286
|
+
if (argv1 && fs.existsSync(argv1))
|
|
287
|
+
return argv1;
|
|
262
288
|
try {
|
|
263
289
|
return execSync('which agents', { encoding: 'utf-8' }).trim();
|
|
264
290
|
}
|
|
@@ -299,13 +325,20 @@ function startDaemonLocked() {
|
|
|
299
325
|
execFileSync('launchctl', ['unload', plistPath], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
300
326
|
}
|
|
301
327
|
catch { /* not loaded, expected */ }
|
|
302
|
-
|
|
328
|
+
// launchctl prints `Load failed:` and exits 0 when the label is in a
|
|
329
|
+
// stuck state from a prior session — so a zero exit code isn't proof
|
|
330
|
+
// of success. If no pid materializes within the window, give up on
|
|
331
|
+
// launchd and fall through to a plain detached spawn.
|
|
332
|
+
execFileSync('launchctl', ['load', plistPath], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
303
333
|
const pid = waitForPid(3000);
|
|
304
|
-
|
|
334
|
+
if (pid)
|
|
335
|
+
return { pid, method: 'launchd' };
|
|
336
|
+
// launchctl claimed success but nothing ran. Fall through.
|
|
305
337
|
}
|
|
306
338
|
catch {
|
|
307
|
-
|
|
339
|
+
// load threw — fall through to detached spawn
|
|
308
340
|
}
|
|
341
|
+
return startDetached();
|
|
309
342
|
}
|
|
310
343
|
if (platform === 'linux') {
|
|
311
344
|
try {
|
package/dist/lib/events.d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - Permissions: logs dir is 0700, files are 0600 (owner-only)
|
|
12
12
|
* - Performance tracking: withTiming() wrapper for any async function
|
|
13
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';
|
|
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' | 'secrets.rename' | '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
15
|
export interface EventMeta {
|
|
16
16
|
ts: string;
|
|
17
17
|
tz: string;
|
|
@@ -82,6 +82,26 @@ export interface RotateOptions {
|
|
|
82
82
|
* unless `clearMeta` or a `meta` patch is supplied.
|
|
83
83
|
*/
|
|
84
84
|
export declare function rotateBundleSecret(bundle: SecretsBundle, key: string, opts: RotateOptions): void;
|
|
85
|
+
/** Options for renameBundle. */
|
|
86
|
+
export interface RenameOptions {
|
|
87
|
+
/** When true, overwrite an existing destination bundle (purges its keychain items first). */
|
|
88
|
+
force?: boolean;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Rename a bundle: move metadata + every keychain-backed value to a new name.
|
|
92
|
+
*
|
|
93
|
+
* Sequence is ordered so the source stays intact if anything in the copy
|
|
94
|
+
* phase fails:
|
|
95
|
+
* 1) read source, validate dest
|
|
96
|
+
* 2) purge dest if --force, refuse otherwise
|
|
97
|
+
* 3) copy each keychain value source -> dest
|
|
98
|
+
* 4) write new bundle metadata
|
|
99
|
+
* 5) delete the old per-key keychain items + old metadata
|
|
100
|
+
*
|
|
101
|
+
* Steps 1-4 are reversible. If 5 partially fails (e.g. iCloud Keychain
|
|
102
|
+
* hiccup), running `rename` again is a safe no-op for the source items.
|
|
103
|
+
*/
|
|
104
|
+
export declare function renameBundle(oldName: string, newName: string, opts?: RenameOptions): void;
|
|
85
105
|
export declare function keychainItemsForBundle(bundle: SecretsBundle): Array<{
|
|
86
106
|
key: string;
|
|
87
107
|
item: string;
|
|
@@ -306,6 +306,62 @@ export function rotateBundleSecret(bundle, key, opts) {
|
|
|
306
306
|
}
|
|
307
307
|
writeBundle(bundle);
|
|
308
308
|
}
|
|
309
|
+
/**
|
|
310
|
+
* Rename a bundle: move metadata + every keychain-backed value to a new name.
|
|
311
|
+
*
|
|
312
|
+
* Sequence is ordered so the source stays intact if anything in the copy
|
|
313
|
+
* phase fails:
|
|
314
|
+
* 1) read source, validate dest
|
|
315
|
+
* 2) purge dest if --force, refuse otherwise
|
|
316
|
+
* 3) copy each keychain value source -> dest
|
|
317
|
+
* 4) write new bundle metadata
|
|
318
|
+
* 5) delete the old per-key keychain items + old metadata
|
|
319
|
+
*
|
|
320
|
+
* Steps 1-4 are reversible. If 5 partially fails (e.g. iCloud Keychain
|
|
321
|
+
* hiccup), running `rename` again is a safe no-op for the source items.
|
|
322
|
+
*/
|
|
323
|
+
export function renameBundle(oldName, newName, opts = {}) {
|
|
324
|
+
validateBundleName(oldName);
|
|
325
|
+
validateBundleName(newName);
|
|
326
|
+
if (oldName === newName) {
|
|
327
|
+
throw new Error(`Bundle name unchanged ('${oldName}').`);
|
|
328
|
+
}
|
|
329
|
+
if (!bundleExists(oldName)) {
|
|
330
|
+
throw new Error(`Bundle '${oldName}' not found.`);
|
|
331
|
+
}
|
|
332
|
+
const source = readBundle(oldName);
|
|
333
|
+
if (bundleExists(newName)) {
|
|
334
|
+
if (!opts.force) {
|
|
335
|
+
throw new Error(`Bundle '${newName}' already exists. Use --force to overwrite.`);
|
|
336
|
+
}
|
|
337
|
+
const dest = readBundle(newName);
|
|
338
|
+
for (const { item } of keychainItemsForBundle(dest)) {
|
|
339
|
+
deleteKeychainToken(item, dest.icloud_sync);
|
|
340
|
+
}
|
|
341
|
+
deleteBundle(newName);
|
|
342
|
+
}
|
|
343
|
+
// Copy phase: read old item, write new item. Old items stay in place
|
|
344
|
+
// until step 5 so a partial failure here leaves the source intact.
|
|
345
|
+
const sourceItems = keychainItemsForBundle(source);
|
|
346
|
+
for (const { key, item: oldItem } of sourceItems) {
|
|
347
|
+
const raw = source.vars[key];
|
|
348
|
+
if (typeof raw !== 'string' || !raw.startsWith('keychain:'))
|
|
349
|
+
continue;
|
|
350
|
+
const shortId = raw.slice('keychain:'.length);
|
|
351
|
+
const newItem = secretsKeychainItem(newName, shortId);
|
|
352
|
+
const value = getKeychainToken(oldItem, source.icloud_sync);
|
|
353
|
+
setKeychainToken(newItem, value, source.icloud_sync);
|
|
354
|
+
}
|
|
355
|
+
// writeBundle preserves source.created_at and refreshes updated_at.
|
|
356
|
+
const renamed = { ...source, name: newName };
|
|
357
|
+
writeBundle(renamed);
|
|
358
|
+
// Cleanup: delete the old per-key keychain items, then the old metadata.
|
|
359
|
+
for (const { item: oldItem } of sourceItems) {
|
|
360
|
+
deleteKeychainToken(oldItem, source.icloud_sync);
|
|
361
|
+
}
|
|
362
|
+
deleteBundle(oldName);
|
|
363
|
+
emit('secrets.rename', { from: oldName, to: newName });
|
|
364
|
+
}
|
|
309
365
|
// Iterate all keychain-backed keys in a bundle for cleanup on rm/unset.
|
|
310
366
|
export function keychainItemsForBundle(bundle) {
|
|
311
367
|
const items = [];
|
|
@@ -182,14 +182,14 @@ export function deleteKeychainToken(item, sync = false) {
|
|
|
182
182
|
assertSupportedPlatform();
|
|
183
183
|
if (isLinux())
|
|
184
184
|
return linuxBackend.delete(item, sync);
|
|
185
|
-
// macOS
|
|
186
|
-
if (sync) {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
return spawnSync(
|
|
185
|
+
// macOS: Try security first (no prompts for local items), fall back to binary for synced items.
|
|
186
|
+
if (!sync && spawnSync('security', ['delete-generic-password', '-a', os.userInfo().username, '-s', item], {
|
|
187
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
188
|
+
}).status === 0)
|
|
189
|
+
return true;
|
|
190
|
+
// Fallback: binary deletes synced items via kSecAttrSynchronizableAny
|
|
191
|
+
const bin = ensureKeychainHelper();
|
|
192
|
+
return spawnSync(bin, ['delete', item, os.userInfo().username], {
|
|
193
193
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
194
194
|
}).status === 0;
|
|
195
195
|
}
|
|
@@ -36,7 +36,7 @@ export declare function captureProcessStartTime(pid: number): string | null;
|
|
|
36
36
|
* model_reasoning_effort override). Mode (plan/edit/full) is a separate knob.
|
|
37
37
|
*/
|
|
38
38
|
export type EffortLevel = 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'auto';
|
|
39
|
-
declare const VALID_MODES: readonly ["plan", "edit", "full"];
|
|
39
|
+
declare const VALID_MODES: readonly ["plan", "edit", "full", "auto"];
|
|
40
40
|
type Mode = typeof VALID_MODES[number];
|
|
41
41
|
/** Resolve a mode string to a validated Mode, falling back to the given default. */
|
|
42
42
|
export declare function resolveMode(requestedMode: string | null | undefined, defaultMode?: Mode): Mode;
|
package/dist/lib/teams/agents.js
CHANGED
|
@@ -183,7 +183,7 @@ When you're done, provide a brief summary of:
|
|
|
183
183
|
const CLAUDE_PLAN_MODE_PREFIX = `You are running in HEADLESS PLAN MODE. This mode works like normal plan mode with one exception: you cannot write to ~/.claude/plans/ directory. Instead of writing a plan file, output your complete plan/response as your final message.
|
|
184
184
|
|
|
185
185
|
`;
|
|
186
|
-
const VALID_MODES = ['plan', 'edit', 'full'];
|
|
186
|
+
const VALID_MODES = ['plan', 'edit', 'full', 'auto'];
|
|
187
187
|
function normalizeModeValue(modeValue) {
|
|
188
188
|
if (!modeValue)
|
|
189
189
|
return null;
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -463,6 +463,10 @@ export interface BrowserProfileConfig {
|
|
|
463
463
|
width: number;
|
|
464
464
|
height: number;
|
|
465
465
|
};
|
|
466
|
+
/** Directory holding source-side JSONL logs (e.g. ~/.rush/logs). */
|
|
467
|
+
logDir?: string;
|
|
468
|
+
/** Optional SSH host where logDir lives, e.g. "muqsit@mac-mini". */
|
|
469
|
+
logHost?: string;
|
|
466
470
|
}
|
|
467
471
|
/** Options controlling which agents and resources are synced during `agents pull` / `agents use`. */
|
|
468
472
|
export interface SyncOptions {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the CLI version from the shipping package.json. Used by the daemon
|
|
3
|
+
* to answer `IPCAction: 'version'` and by the client to detect daemon drift —
|
|
4
|
+
* a dev-build CLI talking to a launchd-managed registry daemon would silently
|
|
5
|
+
* get stale behavior without this check.
|
|
6
|
+
*/
|
|
7
|
+
export declare function getCliVersion(): string;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
5
|
+
const __dirname = path.dirname(__filename);
|
|
6
|
+
let cached = null;
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the CLI version from the shipping package.json. Used by the daemon
|
|
9
|
+
* to answer `IPCAction: 'version'` and by the client to detect daemon drift —
|
|
10
|
+
* a dev-build CLI talking to a launchd-managed registry daemon would silently
|
|
11
|
+
* get stale behavior without this check.
|
|
12
|
+
*/
|
|
13
|
+
export function getCliVersion() {
|
|
14
|
+
if (cached)
|
|
15
|
+
return cached;
|
|
16
|
+
try {
|
|
17
|
+
const pkgPath = path.join(__dirname, '..', '..', 'package.json');
|
|
18
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
19
|
+
cached = String(pkg.version || 'unknown');
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
cached = 'unknown';
|
|
23
|
+
}
|
|
24
|
+
return cached;
|
|
25
|
+
}
|
package/package.json
CHANGED