@phnx-labs/agents-cli 1.22.106 → 1.22.109
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 +78 -0
- package/README.md +9 -5
- package/dist/commands/daemon.js +45 -40
- package/dist/commands/doctor.js +7 -0
- package/dist/commands/exec.d.ts +78 -8
- package/dist/commands/exec.js +276 -53
- package/dist/commands/monitors.js +15 -2
- package/dist/commands/routines.js +46 -6
- package/dist/commands/run-account-picker.d.ts +11 -0
- package/dist/commands/run-account-picker.js +11 -1
- package/dist/commands/sessions-backup-setup.d.ts +12 -0
- package/dist/commands/sessions-backup-setup.js +65 -0
- package/dist/commands/sessions-resume.d.ts +14 -1
- package/dist/commands/sessions-resume.js +56 -9
- package/dist/commands/sessions.js +2 -0
- package/dist/commands/share.js +15 -2
- package/dist/commands/sync.js +30 -1
- package/dist/lib/accounts/slots.js +7 -0
- package/dist/lib/agent-spec/agents.d.ts +60 -8
- package/dist/lib/agent-spec/agents.js +118 -45
- package/dist/lib/daemon/daemon.d.ts +34 -1
- package/dist/lib/daemon/daemon.js +63 -9
- package/dist/lib/daemon/leaked-daemons.d.ts +60 -0
- package/dist/lib/daemon/leaked-daemons.js +180 -0
- package/dist/lib/devices/doctor-findings.d.ts +7 -1
- package/dist/lib/devices/doctor-findings.js +32 -1
- package/dist/lib/hooks/install.js +70 -63
- package/dist/lib/hosts/dispatch.d.ts +1 -1
- package/dist/lib/hosts/dispatch.js +1 -1
- package/dist/lib/models.js +76 -5
- package/dist/lib/session/cloud.js +3 -1
- package/dist/lib/session/parse.d.ts +1 -0
- package/dist/lib/session/parse.js +136 -2
- package/dist/lib/session/recovery.d.ts +9 -1
- package/dist/lib/session/recovery.js +14 -4
- package/dist/lib/session/tool-calls.d.ts +1 -1
- package/dist/lib/session/tool-calls.js +40 -5
- package/dist/lib/share/backend.d.ts +23 -0
- package/dist/lib/share/backend.js +24 -0
- package/dist/lib/share/provision.d.ts +12 -0
- package/dist/lib/share/provision.js +30 -0
- package/dist/lib/share/worker-template.js +273 -0
- package/dist/lib/terminal/engine.js +13 -1
- package/package.json +1 -1
|
@@ -204,6 +204,16 @@ export async function pickSwitchAccount(agent, rows) {
|
|
|
204
204
|
throw err;
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* The two-condition "human-facing" gate behind signInLaunchDecision and
|
|
209
|
+
* noVerifiedUsageDecision: a real TTY and no `--json`. Off a TTY nobody can
|
|
210
|
+
* answer a prompt, and `--json` marks a MACHINE consumer, which must never be
|
|
211
|
+
* handed a picker or dropped into a login TUI. Mirrors the canonical
|
|
212
|
+
* `Surface.interactive = tty && !json` in `commands/utils.ts`.
|
|
213
|
+
*/
|
|
214
|
+
export function isHumanFacingRun(input) {
|
|
215
|
+
return input.tty && !input.json;
|
|
216
|
+
}
|
|
207
217
|
/**
|
|
208
218
|
* Whether a zero-healthy run may recover by launching for a login, or must keep
|
|
209
219
|
* failing loud. Three inputs, all of which have to hold:
|
|
@@ -217,7 +227,7 @@ export async function pickSwitchAccount(agent, rows) {
|
|
|
217
227
|
* gets the parseable fail-loud error instead.
|
|
218
228
|
*/
|
|
219
229
|
export function signInLaunchDecision(input) {
|
|
220
|
-
const humanPresent = input
|
|
230
|
+
const humanPresent = isHumanFacingRun(input);
|
|
221
231
|
return input.recoverable > 0 && humanPresent ? 'launch' : 'fail-loud';
|
|
222
232
|
}
|
|
223
233
|
/**
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
interface BackupSetupOptions {
|
|
3
|
+
bundle: string;
|
|
4
|
+
worker: string;
|
|
5
|
+
bucket: string;
|
|
6
|
+
account?: string;
|
|
7
|
+
token?: string;
|
|
8
|
+
domain: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function handleSessionsBackupSetup(opts: BackupSetupOptions): Promise<void>;
|
|
11
|
+
export declare function registerSessionsBackupSetupCommand(sessionsCmd: Command): void;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// `agents sessions backup-setup` — the OPERATOR command that provisions the
|
|
2
|
+
// managed session-backup endpoint (`sessions.agents-cli.sh`): the Cloudflare
|
|
3
|
+
// Worker + R2 bucket a signed-in user's `sessions export --to-r2` talks to with
|
|
4
|
+
// NO `r2.backups` bucket of their own. It is the deploy producer for that
|
|
5
|
+
// endpoint — first-party infrastructure, not a per-user step. The zero-knowledge
|
|
6
|
+
// `--byo` backup path uses the user's own R2 bucket directly and never touches
|
|
7
|
+
// this Worker, so there is nothing here for an ordinary user to run.
|
|
8
|
+
//
|
|
9
|
+
// Mirrors `agents traces setup`: the same `readCloudflareCreds` bundle plumbing,
|
|
10
|
+
// the same idempotent `deployWorker`/`createBucket` primitives. Provisioning is
|
|
11
|
+
// idempotent — re-running redeploys the current Worker template in place.
|
|
12
|
+
import chalk from 'chalk';
|
|
13
|
+
import { DEFAULT_CF_BUNDLE, readCloudflareCreds } from '../lib/share/config.js';
|
|
14
|
+
import { PHOENIX_ID_BASE } from '../lib/identity/client.js';
|
|
15
|
+
import { provisionSessions } from '../lib/session/sync/provision.js';
|
|
16
|
+
import { DEFAULT_SESSIONS_BUCKET_NAME, DEFAULT_SESSIONS_DOMAIN, DEFAULT_SESSIONS_WORKER_NAME, } from '../lib/session/sync/managed-config.js';
|
|
17
|
+
import { setHelpSections } from '../lib/help.js';
|
|
18
|
+
export async function handleSessionsBackupSetup(opts) {
|
|
19
|
+
const { input } = await import('@inquirer/prompts');
|
|
20
|
+
const { apiToken, accountId: bundledAccountId } = readCloudflareCreds(opts.bundle, {
|
|
21
|
+
apiToken: opts.token,
|
|
22
|
+
accountId: opts.account,
|
|
23
|
+
});
|
|
24
|
+
const accountId = opts.account ?? bundledAccountId ?? await input({ message: 'Cloudflare account id' });
|
|
25
|
+
if (!accountId)
|
|
26
|
+
throw new Error('A Cloudflare account id is required.');
|
|
27
|
+
const result = await provisionSessions({
|
|
28
|
+
apiToken,
|
|
29
|
+
accountId,
|
|
30
|
+
workerName: opts.worker,
|
|
31
|
+
bucketName: opts.bucket,
|
|
32
|
+
domain: opts.domain,
|
|
33
|
+
phoenixIdBase: PHOENIX_ID_BASE,
|
|
34
|
+
});
|
|
35
|
+
console.log(chalk.green(`Managed session-backup endpoint ready → ${chalk.bold(result.baseUrl)}`));
|
|
36
|
+
console.log(chalk.dim('Signed-in users now back up with `agents sessions export --to-r2` — no r2.backups bucket.'));
|
|
37
|
+
}
|
|
38
|
+
const BACKUP_SETUP_EXAMPLES = `
|
|
39
|
+
$ agents secrets exec cloudflare -- agents sessions backup-setup
|
|
40
|
+
Provision the managed session-backup Worker + R2 bucket (creds from the bundle).
|
|
41
|
+
|
|
42
|
+
$ agents sessions backup-setup --account <id> --domain sessions.example.com
|
|
43
|
+
Provision against a private account service and custom domain.
|
|
44
|
+
`.trimStart();
|
|
45
|
+
export function registerSessionsBackupSetupCommand(sessionsCmd) {
|
|
46
|
+
const cmd = sessionsCmd
|
|
47
|
+
.command('backup-setup')
|
|
48
|
+
.description('(operator) Provision the managed session-backup Worker + R2 bucket — NOT a per-user step; signing in with `agents auth login` backs sessions up with zero setup')
|
|
49
|
+
.option('--bundle <name>', 'secrets bundle holding the Cloudflare API token', DEFAULT_CF_BUNDLE)
|
|
50
|
+
.option('--worker <name>', 'Worker name', DEFAULT_SESSIONS_WORKER_NAME)
|
|
51
|
+
.option('--bucket <name>', 'R2 bucket name', DEFAULT_SESSIONS_BUCKET_NAME)
|
|
52
|
+
.option('--account <id>', 'Cloudflare account id (else read from the bundle / prompt)')
|
|
53
|
+
.option('--token <token>', 'Cloudflare API token (else read from the --bundle)')
|
|
54
|
+
.option('--domain <host>', 'custom domain to map', DEFAULT_SESSIONS_DOMAIN)
|
|
55
|
+
.action(async (opts) => {
|
|
56
|
+
try {
|
|
57
|
+
await handleSessionsBackupSetup(opts);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
console.error(chalk.red(err.message));
|
|
61
|
+
process.exitCode = 1;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
setHelpSections(cmd, { examples: BACKUP_SETUP_EXAMPLES });
|
|
65
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
2
|
import { type SessionMeta } from '../lib/session/types.js';
|
|
3
|
-
import { type Backend, type EngineContext, type Packing } from '../lib/terminal/index.js';
|
|
3
|
+
import { type Backend, type SurfaceItem, type EngineContext, type Packing } from '../lib/terminal/index.js';
|
|
4
4
|
import { type StrictResumeOptions } from './resume.js';
|
|
5
5
|
/** Opening more than this many live sessions at once asks for confirmation first. */
|
|
6
6
|
export declare const CONFIRM_THRESHOLD = 5;
|
|
@@ -33,6 +33,15 @@ export declare function isDirectResumeSelector(query: string): boolean;
|
|
|
33
33
|
/** Re-enter through sessions resume so fleet routing and harness policy
|
|
34
34
|
* stay centralized. The child inherits this terminal for a real interactive resume. */
|
|
35
35
|
export declare function resumeSelectorInPlace(selector: string): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Local preflight cannot inspect a peer's files or index. Leave peer validation
|
|
38
|
+
* to the existing origin-device recovery hop. --here without a remote surface
|
|
39
|
+
* opts into local recovery and therefore uses the local transcript guard.
|
|
40
|
+
*/
|
|
41
|
+
export declare function partitionResumableSelections(chosen: SessionMeta[], options?: Pick<ResumeOptions, 'here' | 'device'>): {
|
|
42
|
+
resumable: SessionMeta[];
|
|
43
|
+
skipped: SessionMeta[];
|
|
44
|
+
};
|
|
36
45
|
/** Direct identities use focus as the lifecycle dispatcher: it rechecks the
|
|
37
46
|
* live fleet, attaches a healthy pane, and falls through to `agents resume`
|
|
38
47
|
* only when the process is no longer attachable. */
|
|
@@ -42,6 +51,10 @@ export declare function resumeUsesLifecycleDispatch(query: string | undefined, p
|
|
|
42
51
|
export declare function dispatchSessionLifecycleInPlace(selector: string, hosts?: string[], attachOnly?: boolean, local?: boolean): Promise<void>;
|
|
43
52
|
export declare function buildSessionLifecycleArgs(selector: string, hosts?: string[], attachOnly?: boolean, local?: boolean): string[];
|
|
44
53
|
export declare function resolveResumePacking(options: Pick<ResumeOptions, 'splits'>): Packing;
|
|
54
|
+
/** Surface commands use shell words; spawnCliInPlace keeps the original argv. */
|
|
55
|
+
export declare function buildSelectedResumeSurface(session: SessionMeta, prompt: string | undefined, options: ResumeOptions): SurfaceItem & {
|
|
56
|
+
session: SessionMeta;
|
|
57
|
+
};
|
|
45
58
|
export declare function resumeHostMismatch(session: Pick<SessionMeta, 'shortId' | 'machine'>, requestedHost: string, self?: string): string | null;
|
|
46
59
|
/**
|
|
47
60
|
* Decide which backend to launch into. Returns a concrete backend, `'inplace'`
|
|
@@ -19,15 +19,16 @@ import { multiItemPicker, itemPicker } from '../lib/picker.js';
|
|
|
19
19
|
import { buildPreview } from './sessions-picker.js';
|
|
20
20
|
import { formatPickerLabel, pickerColumnsFor, resolveSessionMetadataValue, parseAgentFilter, } from './sessions.js';
|
|
21
21
|
import { sessionMatchesQuery } from './sessions-browser.js';
|
|
22
|
-
import { openSurfaces, availableBackends, detectCurrentBackend, currentContext, } from '../lib/terminal/index.js';
|
|
22
|
+
import { openSurfaces, availableBackends, detectCurrentBackend, currentContext, shellQuote, } from '../lib/terminal/index.js';
|
|
23
23
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
24
24
|
import { setHelpSections } from '../lib/help.js';
|
|
25
25
|
import { confirm } from '@inquirer/prompts';
|
|
26
26
|
import { spawn } from 'node:child_process';
|
|
27
27
|
import { looksLikeSessionId } from '../lib/session/discover.js';
|
|
28
28
|
import { machineId } from '../lib/session/sync/config.js';
|
|
29
|
-
import { sessionOriginDevice, sessionRecoveryDestinationMatches } from '../lib/session/recovery.js';
|
|
29
|
+
import { sessionOriginDevice, sessionRecoveryDestinationMatches, sessionRecoveryPeer, sessionTranscriptReadable } from '../lib/session/recovery.js';
|
|
30
30
|
import { buildResumeRemoteArgs, runStrictResume, wantsStrictResume } from './resume.js';
|
|
31
|
+
import { toRemotePortable } from '../lib/project-root.js';
|
|
31
32
|
import { attachLocalLiveSelector } from '../lib/session/local-tmux-attach.js';
|
|
32
33
|
/** Opening more than this many live sessions at once asks for confirmation first. */
|
|
33
34
|
export const CONFIRM_THRESHOLD = 5;
|
|
@@ -98,6 +99,7 @@ export function registerSessionsResumeCommand(sessionsCmd) {
|
|
|
98
99
|
- --vscodium opens each session as an agent terminal tab in VSCodium via the swarm-ext extension (works with --device too).
|
|
99
100
|
- --device <alias> opens the terminal surface on that device only when it is the selected sessions' origin; recovery never migrates a session to another device.
|
|
100
101
|
- Recovery uses the installed harness and the conversation's account on its origin device. Context replay requires an explicit choice.
|
|
102
|
+
- Local picks without transcripts are skipped before opening a tab. Remote picks are validated on their origin device.
|
|
101
103
|
- agents run claude --resume opens this same picker; claude#work filters it with --account work.
|
|
102
104
|
`,
|
|
103
105
|
});
|
|
@@ -232,12 +234,16 @@ export async function sessionsResumeAction(query, prompt, options) {
|
|
|
232
234
|
}
|
|
233
235
|
}
|
|
234
236
|
// 2. Route every selection through the owning device's recovery resolver.
|
|
235
|
-
const
|
|
236
|
-
for (const s of
|
|
237
|
-
|
|
238
|
-
const cwd = s.cwd && fs.existsSync(s.cwd) ? s.cwd : process.cwd();
|
|
239
|
-
items.push({ session: s, cwd, command });
|
|
237
|
+
const { resumable, skipped } = partitionResumableSelections(chosen, options);
|
|
238
|
+
for (const s of skipped) {
|
|
239
|
+
console.log(chalk.yellow(`Skipping ${s.shortId} — nothing to resume (no transcript was written).`));
|
|
240
240
|
}
|
|
241
|
+
if (resumable.length === 0) {
|
|
242
|
+
console.error(chalk.red('Nothing to resume — no selected session has a transcript.'));
|
|
243
|
+
process.exitCode = 1;
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const items = resumable.map((session) => buildSelectedResumeSurface(session, prompt, options));
|
|
241
247
|
// 3. Resolve the backend (and host).
|
|
242
248
|
const ctx = currentContext();
|
|
243
249
|
const backend = await resolveBackend(options, ctx, items.length);
|
|
@@ -294,6 +300,8 @@ export async function sessionsResumeAction(query, prompt, options) {
|
|
|
294
300
|
}
|
|
295
301
|
});
|
|
296
302
|
console.log(chalk.gray(`\nOpened ${opened}/${items.length} in ${where}.`));
|
|
303
|
+
if (opened !== items.length)
|
|
304
|
+
process.exitCode = 1;
|
|
297
305
|
}
|
|
298
306
|
/** Preserve run options and lifecycle intent when the picker opens its selected rows. */
|
|
299
307
|
export function buildSelectedResumeArgs(id, prompt, options) {
|
|
@@ -307,6 +315,7 @@ export function buildSelectedResumeArgs(id, prompt, options) {
|
|
|
307
315
|
// The outer surface already placed this terminal on the selected device.
|
|
308
316
|
if (options.device) {
|
|
309
317
|
let remoteCwd;
|
|
318
|
+
let localCwd;
|
|
310
319
|
for (let i = 0; i < args.length && args[i] !== '--'; i++) {
|
|
311
320
|
if (args[i] === '--remote-cwd') {
|
|
312
321
|
remoteCwd = args[i + 1];
|
|
@@ -316,6 +325,14 @@ export function buildSelectedResumeArgs(id, prompt, options) {
|
|
|
316
325
|
remoteCwd = args[i].slice('--remote-cwd='.length);
|
|
317
326
|
args.splice(i--, 1);
|
|
318
327
|
}
|
|
328
|
+
else if (args[i] === '--cwd') {
|
|
329
|
+
localCwd = args[i + 1];
|
|
330
|
+
args.splice(i--, 2);
|
|
331
|
+
}
|
|
332
|
+
else if (args[i].startsWith('--cwd=')) {
|
|
333
|
+
localCwd = args[i].slice('--cwd='.length);
|
|
334
|
+
args.splice(i--, 1);
|
|
335
|
+
}
|
|
319
336
|
else if (['-D', '--device', '--host', '--where', '--on', '--computer'].includes(args[i])) {
|
|
320
337
|
args.splice(i, 2);
|
|
321
338
|
i--;
|
|
@@ -325,9 +342,10 @@ export function buildSelectedResumeArgs(id, prompt, options) {
|
|
|
325
342
|
i--;
|
|
326
343
|
}
|
|
327
344
|
}
|
|
328
|
-
|
|
345
|
+
const effectiveCwd = remoteCwd ?? options.cwd ?? (localCwd ? toRemotePortable(localCwd) : undefined);
|
|
346
|
+
if (effectiveCwd !== undefined) {
|
|
329
347
|
const end = args.indexOf('--');
|
|
330
|
-
args.splice(end < 0 ? args.length : end, 0, '--cwd',
|
|
348
|
+
args.splice(end < 0 ? args.length : end, 0, '--cwd', effectiveCwd);
|
|
331
349
|
}
|
|
332
350
|
}
|
|
333
351
|
return args;
|
|
@@ -352,6 +370,20 @@ export function isDirectResumeSelector(query) {
|
|
|
352
370
|
export async function resumeSelectorInPlace(selector) {
|
|
353
371
|
await spawnCliInPlace(['sessions', 'resume', selector]);
|
|
354
372
|
}
|
|
373
|
+
/**
|
|
374
|
+
* Local preflight cannot inspect a peer's files or index. Leave peer validation
|
|
375
|
+
* to the existing origin-device recovery hop. --here without a remote surface
|
|
376
|
+
* opts into local recovery and therefore uses the local transcript guard.
|
|
377
|
+
*/
|
|
378
|
+
export function partitionResumableSelections(chosen, options = {}) {
|
|
379
|
+
const resumable = [];
|
|
380
|
+
const skipped = [];
|
|
381
|
+
for (const s of chosen) {
|
|
382
|
+
const deferredToPeer = (!options.here || !!options.device) && sessionRecoveryPeer(s);
|
|
383
|
+
(deferredToPeer || sessionTranscriptReadable(s) ? resumable : skipped).push(s);
|
|
384
|
+
}
|
|
385
|
+
return { resumable, skipped };
|
|
386
|
+
}
|
|
355
387
|
/** Direct identities use focus as the lifecycle dispatcher: it rechecks the
|
|
356
388
|
* live fleet, attaches a healthy pane, and falls through to `agents resume`
|
|
357
389
|
* only when the process is no longer attachable. */
|
|
@@ -395,6 +427,21 @@ async function spawnCliInPlace(args) {
|
|
|
395
427
|
export function resolveResumePacking(options) {
|
|
396
428
|
return options.splits ? 'two-per-tab' : 'tabs';
|
|
397
429
|
}
|
|
430
|
+
/** Surface commands use shell words; spawnCliInPlace keeps the original argv. */
|
|
431
|
+
export function buildSelectedResumeSurface(session, prompt, options) {
|
|
432
|
+
const requestedCwd = options.cwd || session.cwd;
|
|
433
|
+
if (options.device && !requestedCwd) {
|
|
434
|
+
throw new Error(`Cannot open ${session.shortId} on ${options.device} without a recorded working directory. Pass --cwd <path>.`);
|
|
435
|
+
}
|
|
436
|
+
const cwd = options.device
|
|
437
|
+
? requestedCwd
|
|
438
|
+
: requestedCwd && fs.existsSync(requestedCwd) ? requestedCwd : process.cwd();
|
|
439
|
+
return {
|
|
440
|
+
session,
|
|
441
|
+
cwd,
|
|
442
|
+
command: ['agents', ...buildSelectedResumeArgs(session.id, prompt, options)].map(shellQuote),
|
|
443
|
+
};
|
|
444
|
+
}
|
|
398
445
|
export function resumeHostMismatch(session, requestedHost, self = machineId()) {
|
|
399
446
|
const origin = sessionOriginDevice(session, self);
|
|
400
447
|
return sessionRecoveryDestinationMatches(session, requestedHost, self)
|
|
@@ -75,6 +75,7 @@ import { registerSessionsRenderCommand } from './sessions-render.js';
|
|
|
75
75
|
import { registerSessionsTraceCommand } from './sessions-trace.js';
|
|
76
76
|
import { registerSessionsShareCommand } from './sessions-share.js';
|
|
77
77
|
import { registerSessionsImportCommand } from './sessions-import.js';
|
|
78
|
+
import { registerSessionsBackupSetupCommand } from './sessions-backup-setup.js';
|
|
78
79
|
import { registerSessionsMigrateCommand, registerSessionsMigrationsCommand } from './sessions-migrate.js';
|
|
79
80
|
import { registerSessionsBackfillCommand } from './sessions-backfill.js';
|
|
80
81
|
import { registerSessionsStatsCommand } from './sessions-stats.js';
|
|
@@ -5387,6 +5388,7 @@ export function registerSessionsCommands(program) {
|
|
|
5387
5388
|
registerSessionsTraceCommand(sessionsCmd);
|
|
5388
5389
|
registerSessionsShareCommand(sessionsCmd);
|
|
5389
5390
|
registerSessionsImportCommand(sessionsCmd);
|
|
5391
|
+
registerSessionsBackupSetupCommand(sessionsCmd);
|
|
5390
5392
|
registerSessionsMigrateCommand(sessionsCmd);
|
|
5391
5393
|
registerSessionsMigrationsCommand(sessionsCmd);
|
|
5392
5394
|
registerSessionsBackfillCommand(sessionsCmd);
|
package/dist/commands/share.js
CHANGED
|
@@ -10,13 +10,13 @@ import { formatBytes } from '../lib/format.js';
|
|
|
10
10
|
import { Argument, Option } from 'commander';
|
|
11
11
|
import chalk from 'chalk';
|
|
12
12
|
import { DEFAULT_BUCKET_NAME, DEFAULT_CF_BUNDLE, DEFAULT_SHARE_DOMAIN, DEFAULT_WORKER_NAME, generateWriteToken, readCloudflareCreds, readShareConfig, readWriteToken, readWriteTokenEnv, readWriteTokenFromBundle, storeWriteToken, writeShareConfig, } from '../lib/share/config.js';
|
|
13
|
-
import { addCustomDomain, configureBucketLifecycle, createBucket, deployWorker, enableWorkersDev, findZoneId, hashWorkerScript, putWorkerSecret, updateWorker, WORKER_PHOENIX_ID_BASE_SECRET, setWorkerSecret, } from '../lib/share/provision.js';
|
|
13
|
+
import { addCustomDomain, configureBucketLifecycle, createBucket, deployWorker, enableWorkersDev, findZoneId, hashWorkerScript, putWorkerSecret, updateWorker, WORKER_PHOENIX_ID_BASE_SECRET, WORKER_COLLAB_BASE_SECRET, WORKER_COLLAB_SERVICE_TOKEN_SECRET, setWorkerSecret, } from '../lib/share/provision.js';
|
|
14
14
|
import { publishFile, resolveShareUsername, parseMetaEntries, sanitizeLabel, scanShareContent, formatSensitiveContentError, unlistedNotPrivateWarning, SHARE_VISIBILITY_LEVELS, PUBLISH_VISIBILITY_LEVELS, } from '../lib/share/publish.js';
|
|
15
15
|
import { deleteShare, resolveDeleteTarget } from '../lib/share/delete.js';
|
|
16
16
|
import { renderWorkerBundle } from '../lib/share/worker-template.js';
|
|
17
17
|
import { analyticsEnabled } from '../lib/share/analytics.js';
|
|
18
18
|
import { extractShareHttpError, formatShareHttpErrorDetail } from '../lib/share/http-error.js';
|
|
19
|
-
import { phoenixIdBaseForDeploy, resolveShareBackend, shouldUseManaged, } from '../lib/share/backend.js';
|
|
19
|
+
import { collabConfigForDeploy, phoenixIdBaseForDeploy, resolveShareBackend, shouldUseManaged, } from '../lib/share/backend.js';
|
|
20
20
|
import { publishVisibility } from '../lib/storage/visibility.js';
|
|
21
21
|
import { resolveGitHubUsername } from '../lib/git.js';
|
|
22
22
|
import { refreshSessionAvatar } from '../lib/identity/index.js';
|
|
@@ -1271,6 +1271,15 @@ export async function runShareProvision(opts) {
|
|
|
1271
1271
|
await putWorkerSecret(apiToken, accountId, workerName, WORKER_PHOENIX_ID_BASE_SECRET, phoenixIdBase, provisionOpts);
|
|
1272
1272
|
spin.text = `Worker '${workerName}' Phoenix ID base set`;
|
|
1273
1273
|
}
|
|
1274
|
+
// Managed collaboration backend (PHNX-3835). Dormant unless BOTH
|
|
1275
|
+
// PRIX_ARTIFACT_COLLAB_BASE and ARTIFACT_COLLAB_SERVICE_TOKEN are set in the
|
|
1276
|
+
// deploy env; the service token rides the Secrets API, never rendered HTML.
|
|
1277
|
+
const collab = collabConfigForDeploy({ managed: opts.managed }, { baseUrl, domain });
|
|
1278
|
+
if (collab.collabBase && collab.collabServiceToken) {
|
|
1279
|
+
await putWorkerSecret(apiToken, accountId, workerName, WORKER_COLLAB_BASE_SECRET, collab.collabBase, provisionOpts);
|
|
1280
|
+
await putWorkerSecret(apiToken, accountId, workerName, WORKER_COLLAB_SERVICE_TOKEN_SECRET, collab.collabServiceToken, provisionOpts);
|
|
1281
|
+
spin.text = `Worker '${workerName}' collaboration backend set`;
|
|
1282
|
+
}
|
|
1274
1283
|
spin.succeed('Provisioned');
|
|
1275
1284
|
const cfg = {
|
|
1276
1285
|
baseUrl,
|
|
@@ -1336,10 +1345,14 @@ export async function runShareUpdate(opts = {}) {
|
|
|
1336
1345
|
}
|
|
1337
1346
|
const writeToken = readWriteToken();
|
|
1338
1347
|
const phoenixIdBase = phoenixIdBaseForDeploy({ managed: opts.managed }, cfg);
|
|
1348
|
+
const collab = collabConfigForDeploy({ managed: opts.managed }, cfg);
|
|
1339
1349
|
const provisionOpts = {
|
|
1340
1350
|
...(opts.request ? { request: opts.request } : {}),
|
|
1341
1351
|
force: opts.force,
|
|
1342
1352
|
...(phoenixIdBase !== undefined ? { phoenixIdBase } : {}),
|
|
1353
|
+
...(collab.collabBase !== undefined
|
|
1354
|
+
? { collabBase: collab.collabBase, collabServiceToken: collab.collabServiceToken }
|
|
1355
|
+
: {}),
|
|
1343
1356
|
};
|
|
1344
1357
|
const result = await updateWorker(apiToken, accountId, cfg.workerName, cfg.bucketName, worker, writeToken, cfg.templateHash, provisionOpts);
|
|
1345
1358
|
if (!result.skipped) {
|
package/dist/commands/sync.js
CHANGED
|
@@ -190,7 +190,7 @@ export function registerSyncCommand(program) {
|
|
|
190
190
|
.option('-y, --yes', 'Skip the interactive preview and auto-sync all detected resources', false)
|
|
191
191
|
.option('--force', 'Re-sync even if no changes are detected since the last sync', false)
|
|
192
192
|
.option('--quiet', 'Suppress all output (exit code indicates success)', false)
|
|
193
|
-
.option('--dry-run', 'Show what would be synced without making any changes', false)
|
|
193
|
+
.option('--dry-run', 'Show what would be synced without making any changes — requires an agent scope (e.g. agents sync claude --dry-run); the umbrella verb refuses it', false)
|
|
194
194
|
.option('--allow-exec-surfaces', 'Allow syncing plugin exec surfaces (scripts, binaries) — off by default for safety', false)
|
|
195
195
|
.option('--json', 'Emit machine-readable JSON (also accepted so fleet fan-out via --device all can parse each peer)', false)
|
|
196
196
|
// Umbrella verb (no agent given): make this machine current.
|
|
@@ -480,6 +480,35 @@ function evictCentralBrowserProfilesForSync(quiet, json, outLog, errLog) {
|
|
|
480
480
|
* a one-line summary. Stage failures are non-fatal and surfaced as warnings.
|
|
481
481
|
*/
|
|
482
482
|
async function runUmbrella(opts, quiet, outLog, errLog, json = false) {
|
|
483
|
+
// `--dry-run` on the umbrella verb is NOT supported and must fail LOUD before
|
|
484
|
+
// touching anything (PHNX-3923). The umbrella composes stages that only exist
|
|
485
|
+
// as mutating operations — repo `git pull`, a full `refresh()` reconcile into
|
|
486
|
+
// every installed version home, central browser-profile eviction, device sync,
|
|
487
|
+
// and `repairAfterSync` — none of which carry a non-mutating preview mode. The
|
|
488
|
+
// old code ignored `opts.dryRun` entirely, ran `runUmbrellaSync` + evict +
|
|
489
|
+
// repair, and so MUTATED every native home despite `--dry-run`. Rather than
|
|
490
|
+
// ship a partial preview that silently skips the stages it cannot model (a
|
|
491
|
+
// lying "would sync" that contradicts the flag's promise), refuse here and
|
|
492
|
+
// point at the scoped path, which DOES honor `--dry-run` non-destructively.
|
|
493
|
+
if (opts.dryRun) {
|
|
494
|
+
const installed = MANAGED_AGENT_IDS.filter((id) => listInstalledVersions(id).length > 0);
|
|
495
|
+
const example = installed[0] ?? 'claude';
|
|
496
|
+
const error = '`agents sync --dry-run` has no umbrella preview: the machine-wide sync pulls repos, ' +
|
|
497
|
+
'reconciles every installed version, syncs devices, and repairs homes — stages that ' +
|
|
498
|
+
'cannot be previewed without making changes.';
|
|
499
|
+
const hint = `Preview one agent instead (this is non-destructive): agents sync ${example} --dry-run [--repo <repo>]`;
|
|
500
|
+
if (json) {
|
|
501
|
+
emitJson({ ok: false, mode: 'umbrella', dryRun: true, error, hint, installedAgents: installed });
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
errLog(chalk.red(error));
|
|
505
|
+
errLog(chalk.gray(hint));
|
|
506
|
+
if (installed.length > 0)
|
|
507
|
+
errLog(chalk.gray(`Installed agents: ${installed.join(', ')}`));
|
|
508
|
+
}
|
|
509
|
+
process.exitCode = 1;
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
483
512
|
// Interactive bare `agents sync` (a TTY, no --yes, no scope flag) drops into
|
|
484
513
|
// the two-checklist picker: which repos to sync from, which agents to sync
|
|
485
514
|
// into. Any explicit flag, --yes, or --json keeps the non-interactive path.
|
|
@@ -15,6 +15,7 @@ import * as fs from 'node:fs';
|
|
|
15
15
|
import * as path from 'node:path';
|
|
16
16
|
import { agentConfigDirName } from '../agents.js';
|
|
17
17
|
import { harnessAuth, harnessWorkerIsPerDevice } from '../harness-auth-capabilities.js';
|
|
18
|
+
import { installSessionTrackerHookSync } from '../hooks/install.js';
|
|
18
19
|
import { getGlobalDefault, getVersionHomePath, listInstalledVersions } from '../installations/store.js';
|
|
19
20
|
import { carryForwardSettings } from '../settings-manifest.js';
|
|
20
21
|
import { getHistoryDir, readMeta, updateMeta } from '../state.js';
|
|
@@ -71,6 +72,12 @@ function projectResources(harness, version, destHome, fromHome) {
|
|
|
71
72
|
continue;
|
|
72
73
|
writer.write({ version, versionHome: destHome, selection: names, cwd });
|
|
73
74
|
}
|
|
75
|
+
if (supports(harness, 'hooks', version).ok) {
|
|
76
|
+
const tracker = installSessionTrackerHookSync(harness, version, destHome);
|
|
77
|
+
if (!tracker.installed && tracker.error) {
|
|
78
|
+
console.warn(`agents: SessionStart hook not installed for ${harness} account home ${destHome}: ${tracker.error}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
74
81
|
}
|
|
75
82
|
/**
|
|
76
83
|
* Remove slot artifacts whose source name is gone. Only the name-keyed kinds
|
|
@@ -355,25 +355,77 @@ export declare function antigravityOsKeyringProbe(platform?: NodeJS.Platform): {
|
|
|
355
355
|
} | null;
|
|
356
356
|
/** @internal test hook — clear the per-process keyring probe cache. */
|
|
357
357
|
export declare function __resetAntigravityKeychainCacheForTest(): void;
|
|
358
|
+
/** The XDG base dirs OpenCode reads, and the env var that overrides each. */
|
|
359
|
+
declare const OPENCODE_XDG_DIRS: {
|
|
360
|
+
readonly data: {
|
|
361
|
+
readonly env: "XDG_DATA_HOME";
|
|
362
|
+
readonly fallback: readonly [".local", "share"];
|
|
363
|
+
};
|
|
364
|
+
readonly state: {
|
|
365
|
+
readonly env: "XDG_STATE_HOME";
|
|
366
|
+
readonly fallback: readonly [".local", "state"];
|
|
367
|
+
};
|
|
368
|
+
};
|
|
369
|
+
/**
|
|
370
|
+
* Resolve one of OpenCode's (sst/opencode) XDG-rooted files.
|
|
371
|
+
*
|
|
372
|
+
* OpenCode keeps provider credentials under `$XDG_DATA_HOME/opencode/` and TUI
|
|
373
|
+
* state under `$XDG_STATE_HOME/opencode/`, defaulting to `~/.local/share` and
|
|
374
|
+
* `~/.local/state` on EVERY platform — its `xdg-basedir` dependency does not
|
|
375
|
+
* special-case macOS, so there is no `~/Library/Application Support` variant.
|
|
376
|
+
* Both roots are account-global (not per-version), matching how
|
|
377
|
+
* `session/discover.ts` already resolves `~/.local/share/opencode/opencode.db`.
|
|
378
|
+
*
|
|
379
|
+
* Resolution order, first existing wins:
|
|
380
|
+
* 1. `<base>/<fallback>/opencode/<file>` — the passed per-version home. This is
|
|
381
|
+
* primarily a test hook (suites write a hermetic file under a temp home)
|
|
382
|
+
* but also covers any relocated install.
|
|
383
|
+
* 2. `$XDG_<KIND>_HOME/opencode/<file>` — an explicit XDG override, exactly
|
|
384
|
+
* what OpenCode itself honours.
|
|
385
|
+
* 3. `<realHome>/<fallback>/opencode/<file>` — the active default, under
|
|
386
|
+
* `AGENTS_REAL_HOME` or `os.homedir()`, so every installed version reflects
|
|
387
|
+
* the one account-global state (same fallback shape as
|
|
388
|
+
* resolveAccountCredentialPath).
|
|
389
|
+
* Returns the first existing path, or null. Never throws.
|
|
390
|
+
*/
|
|
391
|
+
export declare function resolveOpenCodeXdgPath(base: string, kind: keyof typeof OPENCODE_XDG_DIRS, file: string): string | null;
|
|
392
|
+
/** OpenCode's signed-in identity, as far as `auth.json` can describe it. */
|
|
393
|
+
export interface OpenCodeIdentity {
|
|
394
|
+
/** Sorted, "+"-joined provider ids holding a valid credential. */
|
|
395
|
+
providers: string;
|
|
396
|
+
/** Account email, when some OAuth credential's token carries the claim. */
|
|
397
|
+
email: string | null;
|
|
398
|
+
/** Plan tier from the same token (e.g. `Pro`), when present. */
|
|
399
|
+
plan: string | null;
|
|
400
|
+
}
|
|
358
401
|
/**
|
|
359
|
-
* OpenCode's account identity
|
|
360
|
-
*
|
|
361
|
-
* `
|
|
362
|
-
*
|
|
363
|
-
*
|
|
402
|
+
* OpenCode's account identity, read from `auth.json`.
|
|
403
|
+
*
|
|
404
|
+
* The provider join (`"meta+openai+opencode-go"`) is the stable key: OpenCode is
|
|
405
|
+
* a multi-provider harness, so "which providers are configured" is what actually
|
|
406
|
+
* identifies an install, and `session/discover.ts` indexes sessions by it.
|
|
407
|
+
* Providers that sign in over OAuth additionally hand OpenCode a token with real
|
|
408
|
+
* identity claims, so the email and plan are surfaced alongside it instead of
|
|
409
|
+
* leaving `agents view` showing a bare `id:` key for a login that knows exactly
|
|
410
|
+
* whose it is. Providers are walked in sorted order so a multi-OAuth install
|
|
411
|
+
* resolves to the same email on every machine.
|
|
364
412
|
*
|
|
365
413
|
* This is the ONLY correct source for an OpenCode "account". OpenCode's SQLite
|
|
366
414
|
* `opencode.db` also carries `account`/`account_state`/`control_account` tables,
|
|
367
415
|
* but on a real, actively-used install (yosemite-s1, 1.16.0, 35 applied
|
|
368
416
|
* migrations) all three are permanently empty — no migration ever populates
|
|
369
417
|
* them, and no session has ever written a row. Reading from them instead of
|
|
370
|
-
* `auth.json` always yields `undefined`, credential or not
|
|
371
|
-
* uses this function rather than duplicating a sqlite lookup against those
|
|
372
|
-
* dead tables.
|
|
418
|
+
* `auth.json` always yields `undefined`, credential or not.
|
|
373
419
|
*
|
|
374
420
|
* Sync (`fs.readFileSync`), no network. Returns undefined when `auth.json` is
|
|
375
421
|
* missing, unreadable, or carries no valid credential.
|
|
376
422
|
*/
|
|
423
|
+
export declare function resolveOpenCodeIdentity(base: string): OpenCodeIdentity | undefined;
|
|
424
|
+
/**
|
|
425
|
+
* The provider join alone — the value `session/discover.ts` indexes sessions by,
|
|
426
|
+
* kept as its own entry point so callers that only need the key do not have to
|
|
427
|
+
* know about the OAuth claim walk.
|
|
428
|
+
*/
|
|
377
429
|
export declare function resolveOpenCodeAccountId(base: string): string | undefined;
|
|
378
430
|
/**
|
|
379
431
|
* Whether a Claude version home's credential file is present but carries no
|