@phnx-labs/agents-cli 1.20.67 → 1.20.69
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 +100 -0
- package/README.md +14 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/check.js +96 -2
- package/dist/commands/computer.d.ts +26 -0
- package/dist/commands/computer.js +173 -149
- package/dist/commands/doctor.js +37 -9
- package/dist/commands/exec.d.ts +18 -0
- package/dist/commands/exec.js +126 -6
- package/dist/commands/fork.d.ts +9 -0
- package/dist/commands/fork.js +67 -0
- package/dist/commands/plugins.js +11 -2
- package/dist/commands/repo.js +47 -2
- package/dist/commands/run-account-picker.d.ts +14 -0
- package/dist/commands/run-account-picker.js +131 -0
- package/dist/commands/setup-browser.d.ts +18 -0
- package/dist/commands/setup-browser.js +142 -0
- package/dist/commands/setup-computer.d.ts +19 -0
- package/dist/commands/setup-computer.js +133 -0
- package/dist/commands/setup-share.d.ts +17 -0
- package/dist/commands/setup-share.js +85 -0
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.js +58 -1
- package/dist/commands/share.d.ts +15 -0
- package/dist/commands/share.js +10 -4
- package/dist/commands/ssh.js +267 -2
- package/dist/commands/teams.d.ts +11 -0
- package/dist/commands/teams.js +39 -1
- package/dist/commands/view.d.ts +4 -14
- package/dist/commands/view.js +39 -36
- package/dist/index.js +2 -1
- package/dist/lib/agents.d.ts +21 -5
- package/dist/lib/agents.js +59 -24
- package/dist/lib/auth-health.d.ts +103 -0
- package/dist/lib/auth-health.js +232 -0
- package/dist/lib/browser/chrome.d.ts +9 -0
- package/dist/lib/browser/chrome.js +22 -0
- package/dist/lib/browser/profiles.js +4 -25
- package/dist/lib/cli-entry.d.ts +23 -0
- package/dist/lib/cli-entry.js +116 -0
- package/dist/lib/computer/download.d.ts +51 -0
- package/dist/lib/computer/download.js +145 -0
- package/dist/lib/computer-rpc.js +9 -3
- package/dist/lib/daemon.d.ts +2 -2
- package/dist/lib/daemon.js +8 -89
- package/dist/lib/devices/connect.d.ts +15 -0
- package/dist/lib/devices/connect.js +17 -5
- package/dist/lib/devices/fleet.d.ts +23 -0
- package/dist/lib/devices/fleet.js +38 -0
- package/dist/lib/devices/health-report.d.ts +46 -0
- package/dist/lib/devices/health-report.js +159 -0
- package/dist/lib/devices/health.d.ts +14 -4
- package/dist/lib/devices/health.js +49 -8
- package/dist/lib/devices/terminfo.d.ts +44 -0
- package/dist/lib/devices/terminfo.js +167 -0
- package/dist/lib/git.d.ts +10 -0
- package/dist/lib/git.js +23 -0
- package/dist/lib/hosts/option.js +1 -1
- package/dist/lib/hosts/ready.js +5 -3
- package/dist/lib/hosts/remote-cmd.d.ts +12 -0
- package/dist/lib/hosts/remote-cmd.js +17 -1
- package/dist/lib/mcp.d.ts +21 -0
- package/dist/lib/mcp.js +278 -13
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/resources/mcp.js +20 -342
- package/dist/lib/rotate.d.ts +12 -8
- package/dist/lib/rotate.js +22 -23
- package/dist/lib/routines.d.ts +13 -0
- package/dist/lib/routines.js +21 -0
- package/dist/lib/runner.js +18 -40
- package/dist/lib/secrets/agent.js +11 -15
- package/dist/lib/session/fork.d.ts +32 -0
- package/dist/lib/session/fork.js +101 -0
- package/dist/lib/share/capture.js +21 -3
- package/dist/lib/signin-badge.d.ts +41 -0
- package/dist/lib/signin-badge.js +64 -0
- package/dist/lib/ssh-exec.d.ts +5 -0
- package/dist/lib/ssh-exec.js +55 -1
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/usage.d.ts +40 -3
- package/dist/lib/usage.js +147 -16
- package/package.json +1 -1
package/dist/lib/runner.js
CHANGED
|
@@ -17,7 +17,7 @@ import { spawn, execFileSync } from 'child_process';
|
|
|
17
17
|
import * as fs from 'fs';
|
|
18
18
|
import * as path from 'path';
|
|
19
19
|
import * as os from 'os';
|
|
20
|
-
import { resolveJobPrompt, parseTimeout, writeRunMeta, getRunDir, checkJobDeviceEligibility, } from './routines.js';
|
|
20
|
+
import { resolveJobPrompt, parseTimeout, writeRunMeta, getRunDir, checkJobDeviceEligibility, finalizeRunMeta, } from './routines.js';
|
|
21
21
|
import { getRunsDir } from './state.js';
|
|
22
22
|
import { prepareJobHome, buildSpawnEnv } from './sandbox.js';
|
|
23
23
|
import { resolveModel, buildReasoningFlags } from './models.js';
|
|
@@ -529,9 +529,8 @@ export async function executeJob(config, deps) {
|
|
|
529
529
|
agent: effectiveAgent,
|
|
530
530
|
version: primaryVersion,
|
|
531
531
|
}, deps);
|
|
532
|
-
|
|
533
|
-
meta.
|
|
534
|
-
meta.exitCode = loopResult.stoppedBy === 'error' ? 1 : 0;
|
|
532
|
+
const loopFailed = loopResult.stoppedBy === 'error';
|
|
533
|
+
finalizeRunMeta(meta, loopFailed ? 'failed' : 'completed', loopFailed ? 1 : 0, loopFailed ? { errorMessage: `loop stopped: ${loopResult.stoppedBy}` } : undefined);
|
|
535
534
|
writeRunMeta(meta);
|
|
536
535
|
timer.end({ status: meta.status, exitCode: meta.exitCode ?? undefined, runId });
|
|
537
536
|
return { meta, reportPath: null };
|
|
@@ -574,17 +573,14 @@ export async function executeJob(config, deps) {
|
|
|
574
573
|
meta.pid = attempt.pid;
|
|
575
574
|
writeRunMeta(meta);
|
|
576
575
|
if (attempt.status === 'timeout') {
|
|
577
|
-
meta
|
|
578
|
-
meta.completedAt = new Date().toISOString();
|
|
576
|
+
finalizeRunMeta(meta, 'timeout', null, { errorMessage: 'run timed out' });
|
|
579
577
|
writeRunMeta(meta);
|
|
580
578
|
timer.end({ status: 'timeout', runId });
|
|
581
579
|
const reportPath = extractAndSaveReport(stdoutPath, effectiveAgent, runDir);
|
|
582
580
|
return { meta, reportPath };
|
|
583
581
|
}
|
|
584
582
|
if (attempt.status === 'completed') {
|
|
585
|
-
meta
|
|
586
|
-
meta.status = 'completed';
|
|
587
|
-
meta.completedAt = new Date().toISOString();
|
|
583
|
+
finalizeRunMeta(meta, 'completed', 0);
|
|
588
584
|
writeRunMeta(meta);
|
|
589
585
|
timer.end({ status: 'completed', exitCode: 0, runId });
|
|
590
586
|
const reportPath = extractAndSaveReport(stdoutPath, effectiveAgent, runDir);
|
|
@@ -603,9 +599,7 @@ export async function executeJob(config, deps) {
|
|
|
603
599
|
if (attempt.error) {
|
|
604
600
|
process.stderr.write(`[agents] routine ${config.name}: spawn failed for ${label}: ${attempt.error}\n`);
|
|
605
601
|
}
|
|
606
|
-
meta
|
|
607
|
-
meta.status = 'failed';
|
|
608
|
-
meta.completedAt = new Date().toISOString();
|
|
602
|
+
finalizeRunMeta(meta, 'failed', attempt.exitCode ?? 1, attempt.error ? { errorMessage: attempt.error } : undefined);
|
|
609
603
|
writeRunMeta(meta);
|
|
610
604
|
timer.end({
|
|
611
605
|
status: 'failed',
|
|
@@ -617,9 +611,7 @@ export async function executeJob(config, deps) {
|
|
|
617
611
|
return { meta, reportPath };
|
|
618
612
|
}
|
|
619
613
|
// Unreachable: chain is always non-empty, but keep a safe fallback.
|
|
620
|
-
meta
|
|
621
|
-
meta.exitCode = 1;
|
|
622
|
-
meta.completedAt = new Date().toISOString();
|
|
614
|
+
finalizeRunMeta(meta, 'failed', 1);
|
|
623
615
|
writeRunMeta(meta);
|
|
624
616
|
timer.end({ status: 'failed', exitCode: 1, runId });
|
|
625
617
|
return { meta, reportPath: null };
|
|
@@ -676,9 +668,7 @@ async function executeJobOnHost(config, opts) {
|
|
|
676
668
|
// Sync path: a real exit code finalizes now. -1 (follow window closed) and
|
|
677
669
|
// the detached path leave the meta `running` for the monitor to reconcile.
|
|
678
670
|
if (!opts.detached && exitCode !== null && exitCode !== undefined && exitCode !== -1) {
|
|
679
|
-
meta
|
|
680
|
-
meta.exitCode = exitCode;
|
|
681
|
-
meta.completedAt = new Date().toISOString();
|
|
671
|
+
finalizeRunMeta(meta, exitCode === 0 ? 'completed' : 'failed', exitCode);
|
|
682
672
|
}
|
|
683
673
|
writeRunMeta(meta);
|
|
684
674
|
timer.end({ status: meta.status, exitCode: meta.exitCode ?? undefined, runId });
|
|
@@ -755,9 +745,7 @@ async function executeCommandJobForeground(config) {
|
|
|
755
745
|
finish({ exitCode: 1, status: 'failed', error: err.message });
|
|
756
746
|
});
|
|
757
747
|
});
|
|
758
|
-
meta.status
|
|
759
|
-
meta.exitCode = result.exitCode ?? (result.status === 'completed' ? 0 : 1);
|
|
760
|
-
meta.completedAt = new Date().toISOString();
|
|
748
|
+
finalizeRunMeta(meta, result.status, result.exitCode ?? (result.status === 'completed' ? 0 : 1), result.error ? { errorMessage: result.error } : undefined);
|
|
761
749
|
writeRunMeta(meta);
|
|
762
750
|
if (result.error) {
|
|
763
751
|
process.stderr.write(`[agents] routine ${config.name}: command spawn failed: ${result.error}\n`);
|
|
@@ -847,9 +835,7 @@ export async function executeJobDetached(config) {
|
|
|
847
835
|
fs.closeSync(stdoutFd);
|
|
848
836
|
}
|
|
849
837
|
catch { /* fd already closed */ }
|
|
850
|
-
meta
|
|
851
|
-
meta.exitCode = 1;
|
|
852
|
-
meta.completedAt = new Date().toISOString();
|
|
838
|
+
finalizeRunMeta(meta, 'failed', 1, { errorMessage: err.message });
|
|
853
839
|
writeRunMeta(meta);
|
|
854
840
|
process.stderr.write(`[agents] daemon: spawn failed for job "${config.name}": ${err.message}\n`);
|
|
855
841
|
});
|
|
@@ -907,18 +893,16 @@ function executeCommandJobDetached(config) {
|
|
|
907
893
|
// fire-and-forget call, so the exit event fires here. (monitorRunningJobs no
|
|
908
894
|
// longer force-fails command jobs; it reads exit-code only on the restart edge.)
|
|
909
895
|
let settled = false;
|
|
910
|
-
const settle = (status, exitCode) => {
|
|
896
|
+
const settle = (status, exitCode, errorMessage) => {
|
|
911
897
|
if (settled)
|
|
912
898
|
return;
|
|
913
899
|
settled = true;
|
|
914
|
-
meta
|
|
915
|
-
meta.exitCode = exitCode;
|
|
916
|
-
meta.completedAt = new Date().toISOString();
|
|
900
|
+
finalizeRunMeta(meta, status, exitCode, errorMessage ? { errorMessage } : undefined);
|
|
917
901
|
writeRunMeta(meta);
|
|
918
902
|
};
|
|
919
903
|
child.on('exit', (code) => settle(code === 0 ? 'completed' : 'failed', code ?? 1));
|
|
920
904
|
child.on('error', (err) => {
|
|
921
|
-
settle('failed', 1);
|
|
905
|
+
settle('failed', 1, err.message);
|
|
922
906
|
process.stderr.write(`[agents] daemon: command spawn failed for job "${config.name}": ${err.message}\n`);
|
|
923
907
|
});
|
|
924
908
|
child.unref();
|
|
@@ -1072,9 +1056,7 @@ function finalizeHostRun(meta) {
|
|
|
1072
1056
|
const healed = reconcileHostTask(task);
|
|
1073
1057
|
if (healed.status !== 'completed' && healed.status !== 'failed')
|
|
1074
1058
|
return;
|
|
1075
|
-
meta.status
|
|
1076
|
-
meta.exitCode = healed.exitCode ?? (healed.status === 'completed' ? 0 : 1);
|
|
1077
|
-
meta.completedAt = healed.finishedAt ?? new Date().toISOString();
|
|
1059
|
+
finalizeRunMeta(meta, healed.status, healed.exitCode ?? (healed.status === 'completed' ? 0 : 1), { completedAt: healed.finishedAt ?? undefined });
|
|
1078
1060
|
writeRunMeta(meta);
|
|
1079
1061
|
}
|
|
1080
1062
|
catch { /* unreachable host or unreadable sidecar — retry next sweep */ }
|
|
@@ -1114,8 +1096,7 @@ export function monitorRunningJobs() {
|
|
|
1114
1096
|
const isCommandRun = Boolean(meta.command) || !meta.agent;
|
|
1115
1097
|
const wallClockMs = Date.now() - Date.parse(meta.startedAt);
|
|
1116
1098
|
if (Number.isFinite(wallClockMs) && wallClockMs > MAX_WALL_CLOCK_MS) {
|
|
1117
|
-
meta
|
|
1118
|
-
meta.completedAt = new Date().toISOString();
|
|
1099
|
+
finalizeRunMeta(meta, 'timeout', null, { errorMessage: 'exceeded max wall clock' });
|
|
1119
1100
|
writeRunMeta(meta);
|
|
1120
1101
|
if (!isCommandRun)
|
|
1121
1102
|
extractAndSaveReport(stdoutPath, meta.agent, runDirPath);
|
|
@@ -1129,20 +1110,17 @@ export function monitorRunningJobs() {
|
|
|
1129
1110
|
// missed the exit event — recover the true code from the exit-code file
|
|
1130
1111
|
// the child wrote; its absence means the child was killed/crashed.
|
|
1131
1112
|
const ec = readCommandExitCode(runDirPath);
|
|
1132
|
-
meta
|
|
1133
|
-
meta.exitCode = ec;
|
|
1113
|
+
finalizeRunMeta(meta, ec === 0 ? 'completed' : 'failed', ec);
|
|
1134
1114
|
}
|
|
1135
1115
|
else {
|
|
1136
1116
|
const inferred = inferFinalStatusFromLog(stdoutPath, meta.agent);
|
|
1137
1117
|
if (inferred) {
|
|
1138
|
-
meta.status
|
|
1139
|
-
meta.exitCode = inferred.exitCode;
|
|
1118
|
+
finalizeRunMeta(meta, inferred.status, inferred.exitCode);
|
|
1140
1119
|
}
|
|
1141
1120
|
else {
|
|
1142
|
-
meta
|
|
1121
|
+
finalizeRunMeta(meta, 'failed', null, { errorMessage: 'process exited before final status could be inferred' });
|
|
1143
1122
|
}
|
|
1144
1123
|
}
|
|
1145
|
-
meta.completedAt = new Date().toISOString();
|
|
1146
1124
|
writeRunMeta(meta);
|
|
1147
1125
|
if (!isCommandRun)
|
|
1148
1126
|
extractAndSaveReport(stdoutPath, meta.agent, runDirPath);
|
|
@@ -33,6 +33,7 @@ import { getHelpersDir, readMeta } from '../state.js';
|
|
|
33
33
|
import { isAlive } from '../platform/process.js';
|
|
34
34
|
import { getKeychainHelperPath } from './install-helper.js';
|
|
35
35
|
import { getCliVersion, getCliVersionFresh } from '../version.js';
|
|
36
|
+
import { getCliLaunch } from '../cli-entry.js';
|
|
36
37
|
/** Bumped when the wire protocol changes; a client that pings a mismatched
|
|
37
38
|
* server kills and respawns it rather than talking a stale dialect. */
|
|
38
39
|
const PROTOCOL_VERSION = 1;
|
|
@@ -148,23 +149,18 @@ function writeAgentToken() {
|
|
|
148
149
|
}
|
|
149
150
|
/**
|
|
150
151
|
* Argv for re-invoking THIS cli with a hidden subcommand, so a side-by-side dev
|
|
151
|
-
* build spawns its own helpers rather than the registry-installed one.
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
* `
|
|
152
|
+
* build spawns its own helpers rather than the registry-installed one. Routed
|
|
153
|
+
* through the shared getCliLaunch so it handles both install shapes — a JS entry
|
|
154
|
+
* (`node dist/index.js …`) and a Bun standalone binary (run directly). The old
|
|
155
|
+
* hand-rolled `[process.execPath, process.argv[1], …]` broke on standalone builds:
|
|
156
|
+
* process.argv[1] is the bun virtual entry `/$bunfs/root/agents` (fs.existsSync
|
|
157
|
+
* reports it as present), so it was passed as an argv element and the broker died
|
|
158
|
+
* with `unknown command '/$bunfs/root/agents'` — the daemon-hosted broker never
|
|
159
|
+
* bound its socket and every unlock reported "Could not start the secrets broker".
|
|
155
160
|
*/
|
|
156
161
|
function cliSpawn(sub) {
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
if (entry)
|
|
160
|
-
return { cmd: process.execPath, args: [entry, ...sub] };
|
|
161
|
-
// No resolvable entrypoint (unusual) — fall back to the PATH shim.
|
|
162
|
-
let bin = 'agents';
|
|
163
|
-
try {
|
|
164
|
-
bin = execFileSync('which', ['agents'], { encoding: 'utf-8' }).trim();
|
|
165
|
-
}
|
|
166
|
-
catch { /* default */ }
|
|
167
|
-
return { cmd: bin, args: sub };
|
|
162
|
+
const { command, args } = getCliLaunch(sub);
|
|
163
|
+
return { cmd: command, args };
|
|
168
164
|
}
|
|
169
165
|
function brokerSpawn() {
|
|
170
166
|
return cliSpawn(['secrets', '_agent-run']);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { SessionMeta } from './types.js';
|
|
2
|
+
/** Agents that `fork` can branch today (see the module doc for why). */
|
|
3
|
+
export declare const FORKABLE_AGENTS: readonly ["claude"];
|
|
4
|
+
/** Whether a session's agent can be forked by {@link forkSession}. */
|
|
5
|
+
export declare function isForkableAgent(agent: string): boolean;
|
|
6
|
+
/** Outcome of a successful fork. */
|
|
7
|
+
export interface ForkResult {
|
|
8
|
+
/** The new session's full id. */
|
|
9
|
+
newId: string;
|
|
10
|
+
/** The new session's short id (first 8 chars), for display/resume. */
|
|
11
|
+
shortId: string;
|
|
12
|
+
/** Absolute path of the copied transcript. */
|
|
13
|
+
filePath: string;
|
|
14
|
+
/** The label applied to the fork. */
|
|
15
|
+
label: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Fork a Claude session into a new, independent one.
|
|
19
|
+
*
|
|
20
|
+
* Copies `source.filePath` to a new `<uuid>.jsonl` beside it, rewrites the
|
|
21
|
+
* embedded session id, registers the new session in the index, and records a
|
|
22
|
+
* `--name`-style label. Returns the new ids/path. Throws if the source
|
|
23
|
+
* transcript is missing.
|
|
24
|
+
*
|
|
25
|
+
* @param source The resolved metadata of the session being forked.
|
|
26
|
+
* @param opts.name Optional explicit label; defaults to `fork of <original>`.
|
|
27
|
+
* @param now ISO timestamp to stamp the fork with (injectable for tests).
|
|
28
|
+
*/
|
|
29
|
+
export declare function forkSession(source: SessionMeta, opts?: {
|
|
30
|
+
name?: string;
|
|
31
|
+
now?: string;
|
|
32
|
+
}): ForkResult;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session forking — branch an existing conversation into a new, independent
|
|
3
|
+
* session that can be continued separately, leaving the original untouched.
|
|
4
|
+
*
|
|
5
|
+
* `resume` continues the SAME conversation (same id, same file — it appends).
|
|
6
|
+
* `fork` copies the transcript under a FRESH session id, so continuing the fork
|
|
7
|
+
* diverges from the original instead of mutating it. This is the "git branch"
|
|
8
|
+
* of conversations.
|
|
9
|
+
*
|
|
10
|
+
* v1 supports Claude, whose session id IS its `<id>.jsonl` filename and which
|
|
11
|
+
* resumes natively via `--resume`. A fork is therefore: copy the transcript to
|
|
12
|
+
* a new-uuid filename in the same directory, rewrite the embedded `sessionId`
|
|
13
|
+
* on each line, register the new session in the index, and label it. Other
|
|
14
|
+
* agents (codex single-file; grok/kimi multi-file; opencode DB-only) are a
|
|
15
|
+
* natural follow-up and are refused up front for now.
|
|
16
|
+
*/
|
|
17
|
+
import { randomUUID } from 'crypto';
|
|
18
|
+
import * as fs from 'fs';
|
|
19
|
+
import * as path from 'path';
|
|
20
|
+
import { upsertSession } from './db.js';
|
|
21
|
+
import { recordRunName } from './run-names.js';
|
|
22
|
+
/** Agents that `fork` can branch today (see the module doc for why). */
|
|
23
|
+
export const FORKABLE_AGENTS = ['claude'];
|
|
24
|
+
/** Whether a session's agent can be forked by {@link forkSession}. */
|
|
25
|
+
export function isForkableAgent(agent) {
|
|
26
|
+
return FORKABLE_AGENTS.includes(agent);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Rewrite the per-line `sessionId` field of a Claude JSONL transcript to a new
|
|
30
|
+
* id. Claude resolves a conversation by its filename, so this is belt-and-braces
|
|
31
|
+
* (keeps the in-file id consistent with the new filename); malformed lines are
|
|
32
|
+
* passed through untouched.
|
|
33
|
+
*/
|
|
34
|
+
function rewriteSessionId(transcript, newId) {
|
|
35
|
+
return transcript
|
|
36
|
+
.split('\n')
|
|
37
|
+
.map((line) => {
|
|
38
|
+
if (!line.trim())
|
|
39
|
+
return line;
|
|
40
|
+
try {
|
|
41
|
+
const obj = JSON.parse(line);
|
|
42
|
+
if (typeof obj.sessionId === 'string') {
|
|
43
|
+
obj.sessionId = newId;
|
|
44
|
+
return JSON.stringify(obj);
|
|
45
|
+
}
|
|
46
|
+
return line;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return line;
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
.join('\n');
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Fork a Claude session into a new, independent one.
|
|
56
|
+
*
|
|
57
|
+
* Copies `source.filePath` to a new `<uuid>.jsonl` beside it, rewrites the
|
|
58
|
+
* embedded session id, registers the new session in the index, and records a
|
|
59
|
+
* `--name`-style label. Returns the new ids/path. Throws if the source
|
|
60
|
+
* transcript is missing.
|
|
61
|
+
*
|
|
62
|
+
* @param source The resolved metadata of the session being forked.
|
|
63
|
+
* @param opts.name Optional explicit label; defaults to `fork of <original>`.
|
|
64
|
+
* @param now ISO timestamp to stamp the fork with (injectable for tests).
|
|
65
|
+
*/
|
|
66
|
+
export function forkSession(source, opts = {}) {
|
|
67
|
+
if (!fs.existsSync(source.filePath)) {
|
|
68
|
+
throw new Error(`transcript not found for session ${source.shortId}: ${source.filePath}`);
|
|
69
|
+
}
|
|
70
|
+
const newId = randomUUID();
|
|
71
|
+
const shortId = newId.slice(0, 8);
|
|
72
|
+
const dir = path.dirname(source.filePath);
|
|
73
|
+
const filePath = path.join(dir, `${newId}.jsonl`);
|
|
74
|
+
const transcript = fs.readFileSync(source.filePath, 'utf-8');
|
|
75
|
+
const rewritten = rewriteSessionId(transcript, newId);
|
|
76
|
+
fs.writeFileSync(filePath, rewritten);
|
|
77
|
+
const original = source.label || source.topic || source.shortId;
|
|
78
|
+
const label = opts.name || `fork of ${original}`;
|
|
79
|
+
// Label sidecar (seeds the DB label; survives rescans until an agent title
|
|
80
|
+
// supersedes it), mirroring `agents run --name`.
|
|
81
|
+
recordRunName({ sessionId: newId, name: label, agent: source.agent, cwd: source.cwd });
|
|
82
|
+
// Register the new session so it resolves immediately (by `agents resume`,
|
|
83
|
+
// `agents sessions`, etc.) without waiting for the next scan.
|
|
84
|
+
const stamp = opts.now ?? new Date().toISOString();
|
|
85
|
+
const meta = {
|
|
86
|
+
...source,
|
|
87
|
+
id: newId,
|
|
88
|
+
shortId,
|
|
89
|
+
filePath,
|
|
90
|
+
label,
|
|
91
|
+
timestamp: stamp,
|
|
92
|
+
lastActivity: stamp,
|
|
93
|
+
// The fork has not opened its own PR / team; drop origin-specific refs.
|
|
94
|
+
prUrl: undefined,
|
|
95
|
+
prNumber: undefined,
|
|
96
|
+
teamOrigin: undefined,
|
|
97
|
+
spawnedTeam: undefined,
|
|
98
|
+
};
|
|
99
|
+
upsertSession(meta, rewritten);
|
|
100
|
+
return { newId, shortId, filePath, label };
|
|
101
|
+
}
|
|
@@ -100,7 +100,17 @@ export async function captureCover(htmlPath, timeoutMs = 15_000) {
|
|
|
100
100
|
if (!fs.existsSync(abs))
|
|
101
101
|
return null;
|
|
102
102
|
const fileUrl = `file://${abs.split('/').map(encodeURIComponent).join('/')}`;
|
|
103
|
-
|
|
103
|
+
const candidates = candidateBrowsers();
|
|
104
|
+
if (candidates.length === 0) {
|
|
105
|
+
// A silently-dropped cover reads as "the CLI decided this page needs none",
|
|
106
|
+
// when in fact no headless browser was found. Say so and point at the escape
|
|
107
|
+
// hatch instead of leaving the publish coverless with no explanation.
|
|
108
|
+
process.stderr.write('[agents share] no headless browser found for the OG cover — install Chrome/Chromium ' +
|
|
109
|
+
'or set AGENTS_SHARE_BROWSER=/path/to/chrome. Publishing without a preview image.\n');
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
let lastFailure = '';
|
|
113
|
+
for (const bin of candidates) {
|
|
104
114
|
const outPng = path.join(os.tmpdir(), `agents-share-cover-${process.pid}-${Date.now()}.png`);
|
|
105
115
|
const userDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agents-share-chrome-'));
|
|
106
116
|
try {
|
|
@@ -126,15 +136,23 @@ export async function captureCover(htmlPath, timeoutMs = 15_000) {
|
|
|
126
136
|
// A valid PNG starts with the 8-byte signature; guard against 0-byte writes.
|
|
127
137
|
if (buf.length > 8 && buf[0] === 0x89 && buf[1] === 0x50)
|
|
128
138
|
return buf;
|
|
139
|
+
lastFailure = `${path.basename(bin)}: produced no valid PNG`;
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
lastFailure = `${path.basename(bin)}: no screenshot written`;
|
|
129
143
|
}
|
|
130
144
|
}
|
|
131
|
-
catch {
|
|
132
|
-
|
|
145
|
+
catch (err) {
|
|
146
|
+
lastFailure = `${path.basename(bin)}: ${err.message}`;
|
|
133
147
|
}
|
|
134
148
|
finally {
|
|
135
149
|
fs.rmSync(outPng, { force: true });
|
|
136
150
|
fs.rmSync(userDir, { recursive: true, force: true });
|
|
137
151
|
}
|
|
138
152
|
}
|
|
153
|
+
// Every candidate ran but none yielded a cover — surface the last reason so a
|
|
154
|
+
// missing preview card is diagnosable (timeout, crash, bad binary) rather than silent.
|
|
155
|
+
process.stderr.write(`[agents share] OG cover capture failed (${lastFailure || 'unknown error'}) — ` +
|
|
156
|
+
'publishing without a preview image. Set AGENTS_SHARE_BROWSER to override.\n');
|
|
139
157
|
return null;
|
|
140
158
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { AccountInfo } from './agents.js';
|
|
2
|
+
import type { AgentId } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* The exact command that logs a given agent in — for warn banners and nudges.
|
|
5
|
+
* Driven off the registry `cliCommand` with the per-agent subcommand overrides
|
|
6
|
+
* (verified against the real CLIs): codex/grok use `<cli> login`, opencode uses
|
|
7
|
+
* `<cli> auth login`, claude logs in from inside its TUI via `/login`, and the
|
|
8
|
+
* remaining agents (kimi, gemini, …) start their device/oauth flow on launch.
|
|
9
|
+
*/
|
|
10
|
+
export declare function loginHint(agentId: AgentId): string;
|
|
11
|
+
/**
|
|
12
|
+
* Whether `agents run` should probe login state before launching. True only for
|
|
13
|
+
* a launch that actually opens the interactive TUI — where discovering a logged-out
|
|
14
|
+
* account after the fact wastes time. Suppressed when there is no preamble surface
|
|
15
|
+
* (`--json`/`--quiet`), when the check is explicitly disabled
|
|
16
|
+
* (`--no-auth-check` / `AGENTS_NO_AUTH_CHECK=1`), or when a rotation already picked a
|
|
17
|
+
* signed-in account.
|
|
18
|
+
*
|
|
19
|
+
* `forceInteractive` is load-bearing: a resume of a non-native-resume agent
|
|
20
|
+
* (`agents run kimi --resume`, also grok/opencode/gemini) rewrites the prompt to
|
|
21
|
+
* `/continue <id>` — so `hasPrompt` is true even though the run still opens the TUI.
|
|
22
|
+
* Keying only off `hasPrompt` would silently skip the warning on exactly those
|
|
23
|
+
* agents (the ones the feature is for), so the resume's `forceInteractive` flag is
|
|
24
|
+
* consulted directly.
|
|
25
|
+
*/
|
|
26
|
+
export declare function shouldCheckLoginBeforeLaunch(o: {
|
|
27
|
+
interactive?: boolean;
|
|
28
|
+
forceInteractive?: boolean;
|
|
29
|
+
headless?: boolean;
|
|
30
|
+
hasPrompt: boolean;
|
|
31
|
+
json?: boolean;
|
|
32
|
+
quiet?: boolean;
|
|
33
|
+
authCheckDisabled?: boolean;
|
|
34
|
+
rotated?: boolean;
|
|
35
|
+
}): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Colored `✓ signed in <account>` / `✗ logged out` badge. When signed in and an
|
|
38
|
+
* account label is derivable (email, else an account id), it is appended in cyan;
|
|
39
|
+
* opaque-credential agents with no email still read as signed in.
|
|
40
|
+
*/
|
|
41
|
+
export declare function formatSignInBadge(info: Pick<AccountInfo, 'signedIn' | 'email' | 'accountId'> | null | undefined): string;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared login-state LOOK. One place decides how "signed in / logged out"
|
|
3
|
+
* renders and what the login command is, so `agents doctor`, `agents view`, and
|
|
4
|
+
* the `agents run` preflight banner all read identically.
|
|
5
|
+
*
|
|
6
|
+
* The signal is `AccountInfo.signedIn` from `getAccountInfo` (file-based, cheap,
|
|
7
|
+
* no Keychain ACL prompt). It is advisory — opaque-credential agents (Kimi,
|
|
8
|
+
* Antigravity) and keychain-bound Claude can false-negative — so callers that act
|
|
9
|
+
* on it (the run preflight) WARN and continue; they never block.
|
|
10
|
+
*/
|
|
11
|
+
import chalk from 'chalk';
|
|
12
|
+
import { AGENTS } from './agents.js';
|
|
13
|
+
/**
|
|
14
|
+
* The exact command that logs a given agent in — for warn banners and nudges.
|
|
15
|
+
* Driven off the registry `cliCommand` with the per-agent subcommand overrides
|
|
16
|
+
* (verified against the real CLIs): codex/grok use `<cli> login`, opencode uses
|
|
17
|
+
* `<cli> auth login`, claude logs in from inside its TUI via `/login`, and the
|
|
18
|
+
* remaining agents (kimi, gemini, …) start their device/oauth flow on launch.
|
|
19
|
+
*/
|
|
20
|
+
export function loginHint(agentId) {
|
|
21
|
+
const cli = AGENTS[agentId]?.cliCommand ?? agentId;
|
|
22
|
+
switch (agentId) {
|
|
23
|
+
case 'claude':
|
|
24
|
+
return `${cli}, then /login`;
|
|
25
|
+
case 'codex':
|
|
26
|
+
case 'grok':
|
|
27
|
+
return `${cli} login`;
|
|
28
|
+
case 'opencode':
|
|
29
|
+
return `${cli} auth login`;
|
|
30
|
+
default:
|
|
31
|
+
return cli;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Whether `agents run` should probe login state before launching. True only for
|
|
36
|
+
* a launch that actually opens the interactive TUI — where discovering a logged-out
|
|
37
|
+
* account after the fact wastes time. Suppressed when there is no preamble surface
|
|
38
|
+
* (`--json`/`--quiet`), when the check is explicitly disabled
|
|
39
|
+
* (`--no-auth-check` / `AGENTS_NO_AUTH_CHECK=1`), or when a rotation already picked a
|
|
40
|
+
* signed-in account.
|
|
41
|
+
*
|
|
42
|
+
* `forceInteractive` is load-bearing: a resume of a non-native-resume agent
|
|
43
|
+
* (`agents run kimi --resume`, also grok/opencode/gemini) rewrites the prompt to
|
|
44
|
+
* `/continue <id>` — so `hasPrompt` is true even though the run still opens the TUI.
|
|
45
|
+
* Keying only off `hasPrompt` would silently skip the warning on exactly those
|
|
46
|
+
* agents (the ones the feature is for), so the resume's `forceInteractive` flag is
|
|
47
|
+
* consulted directly.
|
|
48
|
+
*/
|
|
49
|
+
export function shouldCheckLoginBeforeLaunch(o) {
|
|
50
|
+
if (o.json || o.quiet || o.authCheckDisabled || o.rotated)
|
|
51
|
+
return false;
|
|
52
|
+
return o.interactive === true || o.forceInteractive === true || (!o.hasPrompt && o.headless !== true);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Colored `✓ signed in <account>` / `✗ logged out` badge. When signed in and an
|
|
56
|
+
* account label is derivable (email, else an account id), it is appended in cyan;
|
|
57
|
+
* opaque-credential agents with no email still read as signed in.
|
|
58
|
+
*/
|
|
59
|
+
export function formatSignInBadge(info) {
|
|
60
|
+
if (!info?.signedIn)
|
|
61
|
+
return chalk.red('✗ logged out');
|
|
62
|
+
const who = info.email ?? (info.accountId ? `id:${info.accountId}` : '');
|
|
63
|
+
return who ? `${chalk.green('✓ signed in')} ${chalk.cyan(who)}` : chalk.green('✓ signed in');
|
|
64
|
+
}
|
package/dist/lib/ssh-exec.d.ts
CHANGED
|
@@ -70,6 +70,11 @@ export interface SshExecResult {
|
|
|
70
70
|
* it); callers that build it from user input must `shellQuote` the pieces.
|
|
71
71
|
*/
|
|
72
72
|
export declare function sshExec(target: string, remoteCmd: string, opts?: SshExecOptions): SshExecResult;
|
|
73
|
+
/**
|
|
74
|
+
* Async variant of {@link sshExec}. Same hardened argv composition, but uses
|
|
75
|
+
* child_process.spawn so fleet fan-outs can probe multiple hosts concurrently.
|
|
76
|
+
*/
|
|
77
|
+
export declare function sshExecAsync(target: string, remoteCmd: string, opts?: SshExecOptions): Promise<SshExecResult>;
|
|
73
78
|
export interface SshExecRawResult {
|
|
74
79
|
code: number | null;
|
|
75
80
|
stdout: Buffer;
|
package/dist/lib/ssh-exec.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* target-injection guard live in exactly one place. Target validation is the
|
|
8
8
|
* canonical definition; `commands/secrets.ts` re-exports it.
|
|
9
9
|
*/
|
|
10
|
-
import { spawnSync } from 'child_process';
|
|
10
|
+
import { spawn, spawnSync } from 'child_process';
|
|
11
11
|
import * as fs from 'fs';
|
|
12
12
|
import * as path from 'path';
|
|
13
13
|
import { getCacheDir } from './state.js';
|
|
@@ -125,6 +125,60 @@ export function sshExec(target, remoteCmd, opts = {}) {
|
|
|
125
125
|
timedOut,
|
|
126
126
|
};
|
|
127
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Async variant of {@link sshExec}. Same hardened argv composition, but uses
|
|
130
|
+
* child_process.spawn so fleet fan-outs can probe multiple hosts concurrently.
|
|
131
|
+
*/
|
|
132
|
+
export function sshExecAsync(target, remoteCmd, opts = {}) {
|
|
133
|
+
assertValidSshTarget(target);
|
|
134
|
+
const mux = opts.multiplex === false ? [] : controlOpts();
|
|
135
|
+
const args = [...sshConnectOpts(mux, opts.hostKeyOpts), ...(opts.extraSshArgs ?? []), target, remoteCmd];
|
|
136
|
+
return new Promise((resolve) => {
|
|
137
|
+
const child = spawn('ssh', args, {
|
|
138
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
139
|
+
windowsHide: true,
|
|
140
|
+
});
|
|
141
|
+
let stdout = '';
|
|
142
|
+
let stderr = '';
|
|
143
|
+
let settled = false;
|
|
144
|
+
let timedOut = false;
|
|
145
|
+
const timer = opts.timeoutMs
|
|
146
|
+
? setTimeout(() => {
|
|
147
|
+
timedOut = true;
|
|
148
|
+
child.kill('SIGTERM');
|
|
149
|
+
}, opts.timeoutMs)
|
|
150
|
+
: null;
|
|
151
|
+
child.stdout.setEncoding('utf-8');
|
|
152
|
+
child.stderr.setEncoding('utf-8');
|
|
153
|
+
child.stdout.on('data', (chunk) => { stdout += chunk; });
|
|
154
|
+
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
155
|
+
// Guard the stdin pipe: if the child closes stdin early (e.g. exits fast), the
|
|
156
|
+
// end()/write below emits EPIPE on the stream. With no listener Node escalates
|
|
157
|
+
// that to an uncaught exception that kills the whole CLI. Swallow it — the real
|
|
158
|
+
// outcome is still reported by the 'close'/'error' handlers below.
|
|
159
|
+
child.stdin.on('error', () => { });
|
|
160
|
+
if (opts.input !== undefined)
|
|
161
|
+
child.stdin.end(opts.input);
|
|
162
|
+
else
|
|
163
|
+
child.stdin.end();
|
|
164
|
+
child.on('error', (err) => {
|
|
165
|
+
if (settled)
|
|
166
|
+
return;
|
|
167
|
+
settled = true;
|
|
168
|
+
if (timer)
|
|
169
|
+
clearTimeout(timer);
|
|
170
|
+
resolve({ code: null, stdout, stderr: stderr + err.message, timedOut });
|
|
171
|
+
});
|
|
172
|
+
child.on('close', (code) => {
|
|
173
|
+
if (settled)
|
|
174
|
+
return;
|
|
175
|
+
settled = true;
|
|
176
|
+
if (timer)
|
|
177
|
+
clearTimeout(timer);
|
|
178
|
+
resolve({ code, stdout, stderr, timedOut });
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
}
|
|
128
182
|
/**
|
|
129
183
|
* Like {@link sshExec} but returns raw stdout/stderr Buffers — no UTF-8 decode.
|
|
130
184
|
*
|
|
@@ -45,6 +45,7 @@ export declare const loadDaemon: ModuleLoader;
|
|
|
45
45
|
export declare const loadRoutines: ModuleLoader;
|
|
46
46
|
export declare const loadMonitors: ModuleLoader;
|
|
47
47
|
export declare const loadRun: ModuleLoader;
|
|
48
|
+
export declare const loadFork: ModuleLoader;
|
|
48
49
|
export declare const loadDefaults: ModuleLoader;
|
|
49
50
|
export declare const loadModels: ModuleLoader;
|
|
50
51
|
export declare const loadPrune: ModuleLoader;
|
|
@@ -23,6 +23,7 @@ export const loadDaemon = async () => (await import('../../commands/daemon.js'))
|
|
|
23
23
|
export const loadRoutines = async () => (await import('../../commands/routines.js')).registerRoutinesCommands;
|
|
24
24
|
export const loadMonitors = async () => (await import('../../commands/monitors.js')).registerMonitorsCommands;
|
|
25
25
|
export const loadRun = async () => (await import('../../commands/exec.js')).registerRunCommand;
|
|
26
|
+
export const loadFork = async () => (await import('../../commands/fork.js')).registerForkCommand;
|
|
26
27
|
export const loadDefaults = async () => (await import('../../commands/defaults.js')).registerDefaultsCommands;
|
|
27
28
|
export const loadModels = async () => (await import('../../commands/models.js')).registerModelsCommand;
|
|
28
29
|
export const loadPrune = async () => (await import('../../commands/prune.js')).registerPruneCommand;
|
|
@@ -126,6 +127,7 @@ export const COMMAND_LOADERS = {
|
|
|
126
127
|
routines: [loadRoutines],
|
|
127
128
|
monitors: [loadMonitors],
|
|
128
129
|
run: [loadRun],
|
|
130
|
+
fork: [loadFork],
|
|
129
131
|
defaults: [loadDefaults],
|
|
130
132
|
models: [loadModels],
|
|
131
133
|
trash: [loadTrash],
|