@phnx-labs/agents-cli 1.20.62 → 1.20.64
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 +62 -0
- package/README.md +19 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/browser.js +13 -3
- package/dist/commands/exec.js +96 -28
- package/dist/commands/feed.d.ts +4 -0
- package/dist/commands/feed.js +27 -8
- package/dist/commands/funnel.d.ts +5 -0
- package/dist/commands/funnel.js +62 -0
- package/dist/commands/hosts.js +42 -0
- package/dist/commands/lease.d.ts +23 -0
- package/dist/commands/lease.js +201 -0
- package/dist/commands/mailboxes.d.ts +20 -0
- package/dist/commands/mailboxes.js +390 -0
- package/dist/commands/repo.d.ts +4 -4
- package/dist/commands/repo.js +30 -19
- package/dist/commands/routines.js +92 -29
- package/dist/commands/sessions-export.d.ts +2 -0
- package/dist/commands/sessions-export.js +279 -0
- package/dist/commands/sessions-import.d.ts +2 -0
- package/dist/commands/sessions-import.js +230 -0
- package/dist/commands/sessions-sync.d.ts +1 -0
- package/dist/commands/sessions-sync.js +16 -2
- package/dist/commands/sessions.js +12 -1
- package/dist/commands/setup.js +9 -0
- package/dist/commands/ssh.js +170 -5
- package/dist/commands/sync-provision.d.ts +23 -0
- package/dist/commands/sync-provision.js +107 -0
- package/dist/commands/usage.d.ts +2 -0
- package/dist/commands/usage.js +7 -2
- package/dist/commands/view.d.ts +1 -1
- package/dist/commands/webhook.d.ts +9 -0
- package/dist/commands/webhook.js +93 -0
- package/dist/index.js +7 -2
- package/dist/lib/agents.d.ts +44 -0
- package/dist/lib/agents.js +85 -35
- package/dist/lib/browser/drivers/ssh.js +19 -2
- package/dist/lib/browser/ipc.js +5 -4
- package/dist/lib/browser/profiles.d.ts +13 -0
- package/dist/lib/browser/profiles.js +17 -0
- package/dist/lib/browser/service.d.ts +12 -1
- package/dist/lib/browser/service.js +48 -13
- package/dist/lib/browser/sessions-list.d.ts +40 -0
- package/dist/lib/browser/sessions-list.js +190 -0
- package/dist/lib/comms-render.d.ts +37 -0
- package/dist/lib/comms-render.js +89 -0
- package/dist/lib/crabbox/cli.d.ts +72 -0
- package/dist/lib/crabbox/cli.js +158 -9
- package/dist/lib/crabbox/runtimes.d.ts +13 -0
- package/dist/lib/crabbox/runtimes.js +24 -0
- package/dist/lib/daemon.js +8 -1
- package/dist/lib/devices/fleet.d.ts +62 -0
- package/dist/lib/devices/fleet.js +128 -0
- package/dist/lib/devices/health.d.ts +77 -0
- package/dist/lib/devices/health.js +186 -0
- package/dist/lib/funnel.d.ts +5 -0
- package/dist/lib/funnel.js +23 -0
- package/dist/lib/git.d.ts +21 -5
- package/dist/lib/git.js +64 -14
- package/dist/lib/hosts/credentials.d.ts +28 -0
- package/dist/lib/hosts/credentials.js +48 -0
- package/dist/lib/hosts/dispatch.d.ts +25 -0
- package/dist/lib/hosts/dispatch.js +68 -2
- package/dist/lib/hosts/passthrough.d.ts +13 -10
- package/dist/lib/hosts/passthrough.js +119 -29
- package/dist/lib/mailbox-gc.js +4 -16
- package/dist/lib/mailbox.d.ts +39 -0
- package/dist/lib/mailbox.js +112 -0
- package/dist/lib/migrate.d.ts +12 -0
- package/dist/lib/migrate.js +55 -1
- package/dist/lib/paths.d.ts +13 -0
- package/dist/lib/paths.js +26 -4
- package/dist/lib/routines.d.ts +50 -12
- package/dist/lib/routines.js +82 -27
- package/dist/lib/runner.js +255 -13
- package/dist/lib/sandbox.d.ts +9 -1
- package/dist/lib/sandbox.js +11 -2
- package/dist/lib/session/bundle.d.ts +150 -0
- package/dist/lib/session/bundle.js +189 -0
- package/dist/lib/session/remote-bundle.d.ts +12 -0
- package/dist/lib/session/remote-bundle.js +61 -0
- package/dist/lib/session/sync/agents.d.ts +56 -6
- package/dist/lib/session/sync/agents.js +0 -0
- package/dist/lib/session/sync/config.d.ts +8 -0
- package/dist/lib/session/sync/config.js +6 -1
- package/dist/lib/session/sync/manifest.d.ts +14 -3
- package/dist/lib/session/sync/manifest.js +4 -0
- package/dist/lib/session/sync/provision.d.ts +49 -0
- package/dist/lib/session/sync/provision.js +91 -0
- package/dist/lib/session/sync/sync.d.ts +26 -2
- package/dist/lib/session/sync/sync.js +192 -69
- package/dist/lib/session/sync/transcript-crypto.d.ts +77 -0
- package/dist/lib/session/sync/transcript-crypto.js +147 -0
- package/dist/lib/ssh-tunnel.js +13 -1
- package/dist/lib/staleness/detectors/subagents.d.ts +5 -0
- package/dist/lib/staleness/detectors/subagents.js +5 -192
- package/dist/lib/staleness/writers/subagents.d.ts +10 -0
- package/dist/lib/staleness/writers/subagents.js +11 -102
- package/dist/lib/startup/command-registry.d.ts +4 -0
- package/dist/lib/startup/command-registry.js +16 -0
- package/dist/lib/state.d.ts +10 -2
- package/dist/lib/state.js +14 -2
- package/dist/lib/subagents-registry.d.ts +85 -0
- package/dist/lib/subagents-registry.js +393 -0
- package/dist/lib/subagents.d.ts +8 -8
- package/dist/lib/subagents.js +32 -663
- package/dist/lib/sync-umbrella.d.ts +1 -0
- package/dist/lib/sync-umbrella.js +14 -3
- package/dist/lib/triggers/webhook.d.ts +70 -27
- package/dist/lib/triggers/webhook.js +264 -43
- package/dist/lib/types.d.ts +9 -0
- package/dist/lib/usage.d.ts +42 -3
- package/dist/lib/usage.js +162 -22
- package/package.json +1 -1
package/dist/commands/repo.js
CHANGED
|
@@ -9,6 +9,7 @@ import { confirm, input } from '@inquirer/prompts';
|
|
|
9
9
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
10
10
|
import { setHelpSections } from '../lib/help.js';
|
|
11
11
|
import { itemPicker } from '../lib/picker.js';
|
|
12
|
+
import { addHostOption } from '../lib/hosts/option.js';
|
|
12
13
|
import { inspectRepo, resolveRepoTarget, } from './inspect.js';
|
|
13
14
|
const HOME = os.homedir();
|
|
14
15
|
/**
|
|
@@ -27,7 +28,7 @@ function resolveRepoPath(target) {
|
|
|
27
28
|
return path.join(HOME, `.agents-${trimmed}`);
|
|
28
29
|
}
|
|
29
30
|
import { applyExtraAliasToVersions, ensureAgentsDir, getExtraRepoDir, getSystemAgentsDir, getUserAgentsDir, readMeta, resolveExtraRepoDir, updateMeta, } from '../lib/state.js';
|
|
30
|
-
import { parseSource, pullRepo, commitAndPush, isGitRepo, isSystemRepoOrigin, adoptRepo } from '../lib/git.js';
|
|
31
|
+
import { parseSource, pullRepo, commitAndPush, isGitRepo, isSystemRepoOrigin, adoptRepo, displayHomePath, } from '../lib/git.js';
|
|
31
32
|
import { DEFAULT_SYSTEM_REPO } from '../lib/types.js';
|
|
32
33
|
import { ALL_AGENT_IDS, isAgentName, resolveAgentName } from '../lib/agents.js';
|
|
33
34
|
import { refresh } from '../lib/refresh.js';
|
|
@@ -465,34 +466,44 @@ async function listRepos(alias, opts = {}) {
|
|
|
465
466
|
}
|
|
466
467
|
console.log('');
|
|
467
468
|
}
|
|
468
|
-
/**
|
|
469
|
+
/**
|
|
470
|
+
* Label for push/pull spinners and results: alias + resolved dir + tracking ref.
|
|
471
|
+
* e.g. `user (~/.agents → origin/main)`.
|
|
472
|
+
*/
|
|
473
|
+
function formatRepoTarget(alias, dir, branch) {
|
|
474
|
+
const ref = branch ? `origin/${branch}` : 'origin';
|
|
475
|
+
return `${alias} (${displayHomePath(dir)} → ${ref})`;
|
|
476
|
+
}
|
|
477
|
+
/** Register the `agents repos` command tree (`repo` is a convenience alias). */
|
|
469
478
|
export function registerRepoCommands(program) {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
.
|
|
479
|
+
// addHostOption on the group so --help documents --host/--device; remote
|
|
480
|
+
// routing is handled pre-parse by maybeRunOnHost (passthrough.ts).
|
|
481
|
+
const repoCmd = addHostOption(program
|
|
482
|
+
.command('repos')
|
|
483
|
+
.alias('repo')
|
|
484
|
+
.description('Manage extra DotAgent repos alongside ~/.agents/ (for private or team skills).'));
|
|
474
485
|
setHelpSections(repoCmd, {
|
|
475
486
|
examples: `
|
|
476
487
|
# Scaffold an editable repo (default: ~/.agents/)
|
|
477
|
-
agents
|
|
488
|
+
agents repos init
|
|
478
489
|
|
|
479
490
|
# Scaffold a named repo at ~/.agents-work/
|
|
480
|
-
agents
|
|
491
|
+
agents repos init work
|
|
481
492
|
|
|
482
493
|
# Register an existing repo (clones to ~/.agents-<alias>/)
|
|
483
|
-
agents
|
|
494
|
+
agents repos add gh:yourname/.agents-work
|
|
484
495
|
|
|
485
496
|
# Register with a custom alias
|
|
486
|
-
agents
|
|
497
|
+
agents repos add git@github.com:acme/team-skills.git --as acme
|
|
487
498
|
|
|
488
499
|
# See what's registered
|
|
489
|
-
agents
|
|
500
|
+
agents repos list
|
|
490
501
|
|
|
491
502
|
# View one repo's contents (git state + resource counts); omit the name for a picker
|
|
492
503
|
agents repos view system
|
|
493
504
|
|
|
494
505
|
# Temporarily disable without deleting
|
|
495
|
-
agents
|
|
506
|
+
agents repos disable acme
|
|
496
507
|
`,
|
|
497
508
|
notes: `
|
|
498
509
|
Managed extras live at ~/.agents-<alias>/ as peer dirs to ~/.agents/. User-owned
|
|
@@ -850,20 +861,20 @@ export function registerRepoCommands(program) {
|
|
|
850
861
|
}
|
|
851
862
|
continue;
|
|
852
863
|
}
|
|
853
|
-
const spinner = ora(`Pulling ${t.alias}...`).start();
|
|
864
|
+
const spinner = ora(`Pulling ${formatRepoTarget(t.alias, t.dir)}...`).start();
|
|
854
865
|
const result = await pullRepo(t.dir);
|
|
855
866
|
if (result.success) {
|
|
856
|
-
spinner.succeed(`${t.alias}
|
|
867
|
+
spinner.succeed(`${formatRepoTarget(t.alias, t.dir, result.branch)}: ${result.commit}`);
|
|
857
868
|
}
|
|
858
869
|
else {
|
|
859
|
-
spinner.fail(`${t.alias}: ${result.error}`);
|
|
870
|
+
spinner.fail(`${formatRepoTarget(t.alias, t.dir)}: ${result.error}`);
|
|
860
871
|
}
|
|
861
872
|
}
|
|
862
873
|
});
|
|
863
874
|
repoCmd
|
|
864
875
|
.command('push [alias]')
|
|
865
876
|
.description('Commit and push the user repo or a user-owned extra. Refuses to push the system repo.')
|
|
866
|
-
.option('-m, --message <msg>', 'Commit message', 'Update via agents
|
|
877
|
+
.option('-m, --message <msg>', 'Commit message', 'Update via agents repos push')
|
|
867
878
|
.action(async (alias, options) => {
|
|
868
879
|
const targets = collectRepoTargets(alias);
|
|
869
880
|
if (!targets) {
|
|
@@ -893,13 +904,13 @@ export function registerRepoCommands(program) {
|
|
|
893
904
|
return;
|
|
894
905
|
}
|
|
895
906
|
for (const t of pushable) {
|
|
896
|
-
const spinner = ora(`Pushing ${t.alias}...`).start();
|
|
907
|
+
const spinner = ora(`Pushing ${formatRepoTarget(t.alias, t.dir)}...`).start();
|
|
897
908
|
const result = await commitAndPush(t.dir, options.message);
|
|
898
909
|
if (result.success) {
|
|
899
|
-
spinner.succeed(`${t.alias} pushed`);
|
|
910
|
+
spinner.succeed(`${formatRepoTarget(t.alias, t.dir, result.branch)}: ${result.detail ?? 'pushed'}`);
|
|
900
911
|
}
|
|
901
912
|
else {
|
|
902
|
-
spinner.fail(`${t.alias}: ${result.error}`);
|
|
913
|
+
spinner.fail(`${formatRepoTarget(t.alias, t.dir)}: ${result.error}`);
|
|
903
914
|
}
|
|
904
915
|
}
|
|
905
916
|
});
|
|
@@ -12,7 +12,7 @@ import * as path from 'path';
|
|
|
12
12
|
import * as yaml from 'yaml';
|
|
13
13
|
import { isDaemonRunning, signalDaemonReload, startDaemon, stopDaemon, readDaemonLog, getDaemonStatus, } from '../lib/daemon.js';
|
|
14
14
|
import { humanizeCron, humanizeNextRun, formatRepoLink, REPO_DISPLAY_MAX } from '../lib/routines-format.js';
|
|
15
|
-
import { listJobs as listAllJobs, deleteJob, readJob, validateJob, writeJob, setJobEnabled, listRuns, getLatestRun, getRunDir, getJobPath, parseAtTime, jobRunsOnThisDevice, checkJobDeviceEligibility, } from '../lib/routines.js';
|
|
15
|
+
import { listJobs as listAllJobs, deleteJob, readJob, validateJob, writeJob, setJobEnabled, listRuns, getLatestRun, getRunDir, getJobPath, parseAtTime, jobRunsOnThisDevice, checkJobDeviceEligibility, normalizeTriggerEvent, } from '../lib/routines.js';
|
|
16
16
|
import { fireWebhookJobs, matchJobsToWebhook } from '../lib/triggers/webhook.js';
|
|
17
17
|
import { getRoutinesDir } from '../lib/state.js';
|
|
18
18
|
import { IS_WINDOWS } from '../lib/platform/index.js';
|
|
@@ -54,13 +54,49 @@ function fireConditionLabel(job) {
|
|
|
54
54
|
if (job.schedule)
|
|
55
55
|
return humanizeCron(job.schedule, job.timezone);
|
|
56
56
|
if (job.trigger) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
if (job.trigger.type === 'github_event') {
|
|
58
|
+
const scope = job.trigger.repo
|
|
59
|
+
? ` (${job.trigger.repo}${job.trigger.branch ? `@${job.trigger.branch}` : ''})`
|
|
60
|
+
: '';
|
|
61
|
+
return `on github:${job.trigger.event}${scope}`;
|
|
62
|
+
}
|
|
63
|
+
const filters = [
|
|
64
|
+
job.trigger.action ? `action=${job.trigger.action}` : null,
|
|
65
|
+
job.trigger.teamKey ? `team=${job.trigger.teamKey}` : null,
|
|
66
|
+
job.trigger.label ? `label=${job.trigger.label}` : null,
|
|
67
|
+
].filter(Boolean).join(', ');
|
|
68
|
+
return `on linear:${job.trigger.event}${filters ? ` (${filters})` : ''}`;
|
|
61
69
|
}
|
|
62
70
|
return '-';
|
|
63
71
|
}
|
|
72
|
+
function parseRoutineTrigger(options) {
|
|
73
|
+
const raw = typeof options.on === 'string' ? options.on : undefined;
|
|
74
|
+
if (!raw)
|
|
75
|
+
return undefined;
|
|
76
|
+
const [sourceMaybe, eventMaybe] = raw.includes(':') ? raw.split(':', 2) : ['github', raw];
|
|
77
|
+
if (sourceMaybe === 'github') {
|
|
78
|
+
const event = normalizeTriggerEvent(eventMaybe);
|
|
79
|
+
if (!event)
|
|
80
|
+
throw new Error(`Unknown GitHub trigger event '${eventMaybe}'`);
|
|
81
|
+
const trigger = { type: 'github_event', event };
|
|
82
|
+
if (typeof options.repo === 'string')
|
|
83
|
+
trigger.repo = options.repo;
|
|
84
|
+
if (typeof options.branch === 'string')
|
|
85
|
+
trigger.branch = options.branch;
|
|
86
|
+
return trigger;
|
|
87
|
+
}
|
|
88
|
+
if (sourceMaybe === 'linear') {
|
|
89
|
+
const trigger = { type: 'linear_event', event: eventMaybe };
|
|
90
|
+
if (typeof options.action === 'string')
|
|
91
|
+
trigger.action = options.action;
|
|
92
|
+
if (typeof options.teamKey === 'string')
|
|
93
|
+
trigger.teamKey = options.teamKey;
|
|
94
|
+
if (typeof options.label === 'string')
|
|
95
|
+
trigger.label = options.label;
|
|
96
|
+
return trigger;
|
|
97
|
+
}
|
|
98
|
+
throw new Error('--on source must be github or linear');
|
|
99
|
+
}
|
|
64
100
|
/** Start or reload the background scheduler so newly-added jobs fire on time. */
|
|
65
101
|
function ensureSchedulerRunning() {
|
|
66
102
|
if (isDaemonRunning()) {
|
|
@@ -113,7 +149,7 @@ async function pickJob(message, filter, alternatives = [], cwd) {
|
|
|
113
149
|
message,
|
|
114
150
|
choices: jobs.map((job) => ({
|
|
115
151
|
value: job.name,
|
|
116
|
-
name: `${job.name} ${chalk.gray(`(${job.workflow ? `wf:${job.workflow}` : job.agent}, ${job.schedule ?? fireConditionLabel(job)})`)}`,
|
|
152
|
+
name: `${job.name} ${chalk.gray(`(${job.command ? 'command' : job.workflow ? `wf:${job.workflow}` : job.agent}, ${job.schedule ?? fireConditionLabel(job)})`)}`,
|
|
117
153
|
})),
|
|
118
154
|
});
|
|
119
155
|
}
|
|
@@ -246,6 +282,7 @@ export function registerRoutinesCommands(program) {
|
|
|
246
282
|
name: job.name,
|
|
247
283
|
agent: job.agent ?? null,
|
|
248
284
|
workflow: job.workflow ?? null,
|
|
285
|
+
command: job.command ?? null,
|
|
249
286
|
repo: job.repo ?? null,
|
|
250
287
|
schedule: job.schedule ?? null,
|
|
251
288
|
scheduleHuman: fireConditionLabel(job),
|
|
@@ -320,9 +357,11 @@ export function registerRoutinesCommands(program) {
|
|
|
320
357
|
: lastStatus === 'timeout' ? chalk.yellow
|
|
321
358
|
: chalk.gray;
|
|
322
359
|
const overdueTag = overdueSet.has(job.name) ? chalk.yellow(' (overdue)') : '';
|
|
323
|
-
const agentLabelPadded = job.
|
|
324
|
-
? chalk.magenta(
|
|
325
|
-
:
|
|
360
|
+
const agentLabelPadded = job.command
|
|
361
|
+
? chalk.magenta('command'.padEnd(10))
|
|
362
|
+
: job.workflow
|
|
363
|
+
? chalk.magenta(`wf:${job.workflow}`.padEnd(10))
|
|
364
|
+
: (job.agent || '').padEnd(10);
|
|
326
365
|
console.log(` ${chalk.cyan(job.name.padEnd(NAME_W))} ${agentLabelPadded} ${repoCell}${' '.repeat(repoPadding)} ${deviceCell}${' '.repeat(devicePad)} ${schedStr.padEnd(SCHED_W)} ${enabledStr}${' '.repeat(enabledPad)} ${chalk.gray(nextStr.padEnd(NEXT_W))} ${statusColor(lastStatus)}${overdueTag}`);
|
|
327
366
|
}
|
|
328
367
|
if (overdueSet.size > 0) {
|
|
@@ -338,6 +377,7 @@ export function registerRoutinesCommands(program) {
|
|
|
338
377
|
.option('-s, --schedule <cron>', 'Cron schedule in standard format (5 fields: minute hour day month weekday)')
|
|
339
378
|
.option('-a, --agent <agent>', 'Which agent runs this routine: claude, codex, gemini, cursor, or opencode')
|
|
340
379
|
.option('--workflow <name>', 'Run an installed workflow (~/.agents/workflows/<name>) via `agents run`. Mutually exclusive with --agent.')
|
|
380
|
+
.option('--command <sh>', 'Run a plain shell command directly (no agent, no auth, no sandbox) — for deterministic housekeeping routines. Mutually exclusive with --agent and --workflow; --prompt is not used.')
|
|
341
381
|
.option('-p, --prompt <prompt>', 'Task instruction for the agent')
|
|
342
382
|
.option('-m, --mode <mode>', "Execution mode: plan (read-only), edit (can write files), auto (smart classifier, the default), or skip (bypass all permission prompts). 'full' accepted as alias for skip.", 'auto')
|
|
343
383
|
.option('-e, --effort <effort>', 'Reasoning effort: low | medium | high | xhigh | max | auto', 'auto')
|
|
@@ -345,12 +385,18 @@ export function registerRoutinesCommands(program) {
|
|
|
345
385
|
.option('--timezone <tz>', 'Interpret schedule in this timezone (e.g., America/Los_Angeles)')
|
|
346
386
|
.option('--devices <names>', 'Fleet allowlist (comma-separated): only listed devices schedule and fire this routine. Omit for unrestricted.')
|
|
347
387
|
.option('--at <time>', 'One-shot mode: run once at this time (e.g., "14:30" or "2026-02-24 09:00"), then disable')
|
|
388
|
+
.option('--on <source:event>', 'Webhook trigger instead of/in addition to a schedule: github:pull_request or linear:Issue')
|
|
389
|
+
.option('--repo <owner/name>', 'GitHub repo filter for --on github:<event>')
|
|
390
|
+
.option('--branch <name>', 'GitHub branch filter for --on github:<event>')
|
|
391
|
+
.option('--action <name>', 'Linear action filter for --on linear:<event> (e.g. update)')
|
|
392
|
+
.option('--team-key <key>', 'Linear team key filter for --on linear:<event> (e.g. RUSH)')
|
|
393
|
+
.option('--label <name>', 'Linear issue label filter for --on linear:Issue')
|
|
348
394
|
.option('--end-at <iso>', 'Stop firing on or after this ISO 8601 timestamp (e.g., "2026-12-31T23:59:00Z"); routine auto-disables.')
|
|
349
395
|
.option('--disabled', 'Create the routine but keep it paused (enable later with resume)')
|
|
350
396
|
.option('--resume <sessionId>', 'At fire time, resume this existing session id (via `agents run <agent> --resume`) instead of starting fresh — the actual session reopens with full context and the prompt becomes its next turn. Powers self-scheduled wake-ups (e.g. /hibernate). Requires --agent claude or codex; runs un-sandboxed (the session store lives in the real home, not the job overlay).')
|
|
351
397
|
.action(async (nameOrPath, options) => {
|
|
352
398
|
// Check if inline mode (has flags) or file mode
|
|
353
|
-
const hasInlineFlags = options.schedule || options.agent || options.workflow || options.prompt || options.at;
|
|
399
|
+
const hasInlineFlags = options.schedule || options.agent || options.workflow || options.command || options.prompt || options.at || options.on;
|
|
354
400
|
if (hasInlineFlags) {
|
|
355
401
|
// Inline mode: create job from flags
|
|
356
402
|
if (!nameOrPath) {
|
|
@@ -358,13 +404,21 @@ export function registerRoutinesCommands(program) {
|
|
|
358
404
|
console.log(chalk.gray('Usage: agents routines add <name> --schedule "..." --agent <agent> --prompt "..."'));
|
|
359
405
|
process.exit(1);
|
|
360
406
|
}
|
|
361
|
-
// Validate mutually exclusive --agent / --workflow
|
|
362
|
-
if (options.agent
|
|
363
|
-
console.log(chalk.red('--agent and --
|
|
407
|
+
// Validate mutually exclusive --agent / --workflow / --command
|
|
408
|
+
if ([options.agent, options.workflow, options.command].filter(Boolean).length > 1) {
|
|
409
|
+
console.log(chalk.red('--agent, --workflow, and --command are mutually exclusive; specify exactly one'));
|
|
364
410
|
process.exit(1);
|
|
365
411
|
}
|
|
366
412
|
let schedule = options.schedule;
|
|
413
|
+
let trigger;
|
|
367
414
|
let runOnce = false;
|
|
415
|
+
try {
|
|
416
|
+
trigger = parseRoutineTrigger(options);
|
|
417
|
+
}
|
|
418
|
+
catch (err) {
|
|
419
|
+
console.log(chalk.red(err.message));
|
|
420
|
+
process.exit(1);
|
|
421
|
+
}
|
|
368
422
|
// Handle --at for one-shot jobs
|
|
369
423
|
if (options.at) {
|
|
370
424
|
const parsed = parseAtTime(options.at);
|
|
@@ -376,15 +430,16 @@ export function registerRoutinesCommands(program) {
|
|
|
376
430
|
schedule = parsed.schedule;
|
|
377
431
|
runOnce = parsed.runOnce;
|
|
378
432
|
}
|
|
379
|
-
if (!schedule) {
|
|
380
|
-
console.log(chalk.red('Schedule is required (use --schedule or --
|
|
433
|
+
if (!schedule && !trigger) {
|
|
434
|
+
console.log(chalk.red('Schedule or trigger is required (use --schedule, --at, or --on)'));
|
|
381
435
|
process.exit(1);
|
|
382
436
|
}
|
|
383
|
-
if (!options.agent && !options.workflow) {
|
|
384
|
-
console.log(chalk.red('An agent or
|
|
437
|
+
if (!options.agent && !options.workflow && !options.command) {
|
|
438
|
+
console.log(chalk.red('An agent, workflow, or command is required (use --agent, --workflow, or --command)'));
|
|
385
439
|
process.exit(1);
|
|
386
440
|
}
|
|
387
|
-
|
|
441
|
+
// Command routines run a plain shell and take no prompt; agent/workflow routines require one.
|
|
442
|
+
if (!options.command && !options.prompt) {
|
|
388
443
|
console.log(chalk.red('Prompt is required (use --prompt)'));
|
|
389
444
|
process.exit(1);
|
|
390
445
|
}
|
|
@@ -395,14 +450,16 @@ export function registerRoutinesCommands(program) {
|
|
|
395
450
|
}
|
|
396
451
|
const config = {
|
|
397
452
|
name: nameOrPath,
|
|
398
|
-
schedule,
|
|
399
|
-
|
|
453
|
+
...(schedule ? { schedule } : {}),
|
|
454
|
+
...(trigger ? { trigger } : {}),
|
|
455
|
+
...(options.agent ? { agent: options.agent } : {}),
|
|
400
456
|
...(options.workflow ? { workflow: options.workflow } : {}),
|
|
457
|
+
...(options.command ? { command: options.command } : {}),
|
|
401
458
|
mode: options.mode,
|
|
402
459
|
effort: options.effort,
|
|
403
460
|
timeout: options.timeout,
|
|
404
461
|
enabled: !options.disabled,
|
|
405
|
-
prompt: options.prompt,
|
|
462
|
+
prompt: options.prompt ?? '',
|
|
406
463
|
timezone: options.timezone,
|
|
407
464
|
...(devices ? { devices } : {}),
|
|
408
465
|
...(runOnce ? { runOnce: true } : {}),
|
|
@@ -613,7 +670,7 @@ export function registerRoutinesCommands(program) {
|
|
|
613
670
|
console.log(chalk.gray(` ${eligibility.suggestion}`));
|
|
614
671
|
process.exit(1);
|
|
615
672
|
}
|
|
616
|
-
const runLabel = job.workflow ? `workflow: ${job.workflow}` : `agent: ${job.agent}`;
|
|
673
|
+
const runLabel = job.command ? 'command' : job.workflow ? `workflow: ${job.workflow}` : `agent: ${job.agent}`;
|
|
617
674
|
console.log(chalk.bold(`Running job '${name}' (${runLabel}, mode: ${job.mode})\n`));
|
|
618
675
|
const spinner = ora('Executing...').start();
|
|
619
676
|
try {
|
|
@@ -686,11 +743,17 @@ export function registerRoutinesCommands(program) {
|
|
|
686
743
|
});
|
|
687
744
|
routinesCmd
|
|
688
745
|
.command('webhook')
|
|
689
|
-
.description('Fire trigger-based routines from a single
|
|
690
|
-
.
|
|
746
|
+
.description('Fire trigger-based routines from a single webhook payload (read from --file or stdin). One-shot: matches and fires, then exits.')
|
|
747
|
+
.option('--source <name>', 'Webhook source: github or linear', 'github')
|
|
748
|
+
.requiredOption('--event <name>', 'Source event name: GitHub X-GitHub-Event value, or Linear payload type')
|
|
691
749
|
.option('--file <path>', 'Read the webhook JSON payload from this file instead of stdin')
|
|
692
750
|
.option('--dry-run', 'Show which routines would fire without firing them')
|
|
693
751
|
.action(async (options) => {
|
|
752
|
+
const source = options.source;
|
|
753
|
+
if (source !== 'github' && source !== 'linear') {
|
|
754
|
+
console.log(chalk.red(`Unknown webhook source "${options.source}". Use github or linear.`));
|
|
755
|
+
process.exit(1);
|
|
756
|
+
}
|
|
694
757
|
// Load the raw JSON payload: --file wins, else drain stdin.
|
|
695
758
|
let raw;
|
|
696
759
|
if (options.file) {
|
|
@@ -723,7 +786,7 @@ export function registerRoutinesCommands(program) {
|
|
|
723
786
|
console.log(chalk.red(`Invalid webhook payload JSON: ${err.message}`));
|
|
724
787
|
process.exit(1);
|
|
725
788
|
}
|
|
726
|
-
const webhook = { event: options.event, payload };
|
|
789
|
+
const webhook = { source, event: options.event, payload };
|
|
727
790
|
// Matching is intentionally user-layer only (fireWebhookJobs defaults to
|
|
728
791
|
// listJobs() with no cwd), mirroring `run`/`catchup`: a webhook must never
|
|
729
792
|
// fire a cloned project repo's `.agents/routines/*.yml` and run an
|
|
@@ -731,10 +794,10 @@ export function registerRoutinesCommands(program) {
|
|
|
731
794
|
if (options.dryRun) {
|
|
732
795
|
const matched = matchJobsToWebhook(listAllJobs(), webhook);
|
|
733
796
|
if (matched.length === 0) {
|
|
734
|
-
console.log(chalk.gray(`No routines match a ${options.event} event for this payload.`));
|
|
797
|
+
console.log(chalk.gray(`No routines match a ${source}:${options.event} event for this payload.`));
|
|
735
798
|
return;
|
|
736
799
|
}
|
|
737
|
-
console.log(chalk.bold(`${matched.length} routine(s) would fire on ${options.event}:\n`));
|
|
800
|
+
console.log(chalk.bold(`${matched.length} routine(s) would fire on ${source}:${options.event}:\n`));
|
|
738
801
|
for (const job of matched) {
|
|
739
802
|
console.log(` ${chalk.cyan(job.name)} — ${fireConditionLabel(job)}`);
|
|
740
803
|
}
|
|
@@ -751,10 +814,10 @@ export function registerRoutinesCommands(program) {
|
|
|
751
814
|
}
|
|
752
815
|
const fired = await fireWebhookJobs(webhook);
|
|
753
816
|
if (fired.length === 0) {
|
|
754
|
-
console.log(chalk.gray(`No routines match a ${options.event} event for this payload.`));
|
|
817
|
+
console.log(chalk.gray(`No routines match a ${source}:${options.event} event for this payload.`));
|
|
755
818
|
return;
|
|
756
819
|
}
|
|
757
|
-
console.log(chalk.bold(`Fired ${fired.length} routine(s) on ${options.event}:\n`));
|
|
820
|
+
console.log(chalk.bold(`Fired ${fired.length} routine(s) on ${source}:${options.event}:\n`));
|
|
758
821
|
for (const f of fired) {
|
|
759
822
|
console.log(` ${chalk.cyan(f.jobName)} → ${chalk.green('started')} (run: ${f.runId})`);
|
|
760
823
|
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents sessions export` — bundle N selected sessions into a portable,
|
|
3
|
+
* self-describing archive (RUSH-1710).
|
|
4
|
+
*
|
|
5
|
+
* The successor to background R2/CRDT sync for the durable-archive / hand-off
|
|
6
|
+
* case: instead of an always-on merge daemon, the user explicitly bundles the
|
|
7
|
+
* sessions they want to carry to an offline box or keep as an archive. The
|
|
8
|
+
* bundle format + placement live in ../lib/session/bundle.ts; this command owns
|
|
9
|
+
* only the SELECTION (which sessions) and the OUTPUT (file or stdout).
|
|
10
|
+
*
|
|
11
|
+
* Selection flags (`--since`, `-n/--limit`, `--all`, `-a/--agent`,
|
|
12
|
+
* `--no-redact`) are inherited from the parent `sessions` command and read via
|
|
13
|
+
* optsWithGlobals(), so they never shadow the parent's parsing; this command
|
|
14
|
+
* adds only the export-specific flags (`-o/--output`, `--stdout`, `--encrypt`).
|
|
15
|
+
*
|
|
16
|
+
* Rendered markdown/json of a single session is already served by
|
|
17
|
+
* `agents sessions <id> --markdown|--json`; export is specifically the portable,
|
|
18
|
+
* re-importable BUNDLE, so it does not re-expose those render formats.
|
|
19
|
+
*/
|
|
20
|
+
import * as fs from 'fs';
|
|
21
|
+
import * as path from 'path';
|
|
22
|
+
import chalk from 'chalk';
|
|
23
|
+
import { discoverSessions, resolveSessionById } from '../lib/session/discover.js';
|
|
24
|
+
import { filterSessionsByQuery, parseAgentFilter } from './sessions.js';
|
|
25
|
+
import { listLocalTranscripts, SYNC_AGENTS } from '../lib/session/sync/agents.js';
|
|
26
|
+
import { machineId } from '../lib/machine-id.js';
|
|
27
|
+
import { getHistoryDir } from '../lib/state.js';
|
|
28
|
+
import { loadR2Config } from '../lib/session/sync/config.js';
|
|
29
|
+
import { resolveSyncEncKey, generateSyncEncKey } from '../lib/session/sync/transcript-crypto.js';
|
|
30
|
+
import { buildRecord, makeHeader, mergeRecords, serializeBundle, specForAgent, } from '../lib/session/bundle.js';
|
|
31
|
+
import { pullBundlesFromHosts } from '../lib/session/remote-bundle.js';
|
|
32
|
+
import { setHelpSections } from '../lib/help.js';
|
|
33
|
+
/** Default cap when exporting a scope (not explicit ids) and the user gave no -n. */
|
|
34
|
+
const DEFAULT_LIMIT = 500;
|
|
35
|
+
export function registerSessionsExportCommand(sessionsCmd) {
|
|
36
|
+
const cmd = sessionsCmd
|
|
37
|
+
.command('export [selectors...]')
|
|
38
|
+
.description('Bundle sessions (by id, query, or the parent selection flags like --since/-a) into a portable archive.')
|
|
39
|
+
.option('-o, --output <path>', 'Write the bundle to this file')
|
|
40
|
+
.option('--stdout', 'Write the bundle to stdout (for piping into `sessions import -`)')
|
|
41
|
+
.option('--encrypt', 'Seal each transcript body with AES-256-GCM before writing');
|
|
42
|
+
setHelpSections(cmd, {
|
|
43
|
+
examples: `# Bundle the last week of sessions to a file
|
|
44
|
+
agents sessions export --since 7d -o week.bundle
|
|
45
|
+
|
|
46
|
+
# Bundle two specific sessions
|
|
47
|
+
agents sessions export 4f8a2b1c 9d3e7a55 -o pair.bundle
|
|
48
|
+
|
|
49
|
+
# Encrypt + pipe straight into another machine over SSH
|
|
50
|
+
agents sessions export --since 7d --stdout --encrypt | agents ssh boxB 'agents sessions import - --decrypt <key>'`,
|
|
51
|
+
notes: `Selection uses the same flags as 'agents sessions' (--since, -n/--limit, --all,
|
|
52
|
+
-a/--agent, --no-redact). Bundles are self-describing NDJSON: a header line + one
|
|
53
|
+
line per transcript file. Secrets are redacted by default. Dir-shaped sessions
|
|
54
|
+
(Kimi) carry all their files. Restore with 'agents sessions import'.`,
|
|
55
|
+
});
|
|
56
|
+
cmd.action(async (selectors, _options, command) => {
|
|
57
|
+
await runExport(selectors, command);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
async function runExport(selectors, command) {
|
|
61
|
+
const g = command.optsWithGlobals();
|
|
62
|
+
// --host: export sessions that live on remote peer(s) — run export there and
|
|
63
|
+
// stream the bundle back over the existing SSH transport (RUSH-1712).
|
|
64
|
+
if (g.host && g.host.length > 0) {
|
|
65
|
+
await runRemoteExport(g, selectors, command);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const explicitLimit = command.parent?.getOptionValueSource?.('limit') === 'cli';
|
|
69
|
+
const limit = explicitLimit ? Math.max(1, parseInt(String(g.limit), 10) || DEFAULT_LIMIT) : DEFAULT_LIMIT;
|
|
70
|
+
const agentFilter = parseAgentFilter(resolveAgentShorthand(g));
|
|
71
|
+
// 1. Discover candidate sessions in scope.
|
|
72
|
+
const metas = await discoverSessions({
|
|
73
|
+
all: g.all !== false,
|
|
74
|
+
agent: agentFilter.agent ?? undefined,
|
|
75
|
+
since: g.since,
|
|
76
|
+
limit,
|
|
77
|
+
});
|
|
78
|
+
// 2. Narrow to the selection (ids > query > everything-in-scope).
|
|
79
|
+
const selected = selectSessions(metas, selectors);
|
|
80
|
+
if (selected.length === 0) {
|
|
81
|
+
process.stderr.write(chalk.yellow('No sessions matched the selection.\n'));
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
if (!selectors.length && selected.length >= limit) {
|
|
85
|
+
process.stderr.write(chalk.yellow(`Note: capped at ${limit} sessions. Raise -n to bundle more.\n`));
|
|
86
|
+
}
|
|
87
|
+
// 3. Resolve each selected session to its on-disk file(s).
|
|
88
|
+
const index = buildLocalIndex();
|
|
89
|
+
const self = machineId();
|
|
90
|
+
const files = [];
|
|
91
|
+
const skippedAgents = new Set();
|
|
92
|
+
for (const meta of selected) {
|
|
93
|
+
const spec = specForAgent(meta.agent);
|
|
94
|
+
if (!spec) {
|
|
95
|
+
skippedAgents.add(meta.agent);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const machine = meta.machine || self;
|
|
99
|
+
const lt = index.get(`${meta.agent}:${meta.id}`);
|
|
100
|
+
if (lt) {
|
|
101
|
+
for (const f of lt.files) {
|
|
102
|
+
files.push({ agent: meta.agent, machine, sessionId: meta.id, relKey: f.relKey, absPath: f.absPath, label: meta.label });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else if (meta.filePath && fs.existsSync(meta.filePath)) {
|
|
106
|
+
// Not in the live-home index (e.g. a mirror of another machine): fall back
|
|
107
|
+
// to the single discovered file, deriving its subdir-relative key.
|
|
108
|
+
const relKey = relKeyFromPath(meta.filePath, meta.agent, machine, spec.subdir);
|
|
109
|
+
files.push({ agent: meta.agent, machine, sessionId: meta.id, relKey, absPath: meta.filePath, label: meta.label });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (skippedAgents.size > 0) {
|
|
113
|
+
process.stderr.write(chalk.yellow(`Skipped agents with no portable format: ${[...skippedAgents].sort().join(', ')}.\n`));
|
|
114
|
+
}
|
|
115
|
+
if (files.length === 0) {
|
|
116
|
+
process.stderr.write(chalk.red('Selected sessions have no exportable transcript files.\n'));
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
// 4. Resolve encryption key (opt-in) + redaction (default on via parent --no-redact).
|
|
120
|
+
const encryptKey = g.encrypt ? resolveExportKey() : null;
|
|
121
|
+
const redact = g.redact !== false;
|
|
122
|
+
// 5. Build records + header.
|
|
123
|
+
const records = [];
|
|
124
|
+
for (const f of files) {
|
|
125
|
+
try {
|
|
126
|
+
records.push(buildRecord(f, { redact, encryptKey }));
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
process.stderr.write(chalk.yellow(`Skipped ${f.agent}/${f.sessionId} (${f.relKey}): ${err.message}\n`));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (records.length === 0) {
|
|
133
|
+
process.stderr.write(chalk.red('Nothing to export after reading files.\n'));
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
const header = makeHeader({
|
|
137
|
+
origin: self,
|
|
138
|
+
exportedAt: new Date().toISOString(),
|
|
139
|
+
encrypted: encryptKey !== null,
|
|
140
|
+
redacted: redact,
|
|
141
|
+
records,
|
|
142
|
+
});
|
|
143
|
+
emitBundle(header, records, g);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* --host path: run `agents sessions export …` on each peer over SSH, stream the
|
|
147
|
+
* bundles back, merge (dedup by origin machine) and emit one local bundle.
|
|
148
|
+
* Encryption is not combined with a remote pull (each peer would seal under its
|
|
149
|
+
* own key); the SSH transport already encrypts the stream in transit.
|
|
150
|
+
*/
|
|
151
|
+
async function runRemoteExport(g, selectors, command) {
|
|
152
|
+
if (g.encrypt) {
|
|
153
|
+
process.stderr.write(chalk.yellow('Note: --encrypt is ignored with --host (the SSH stream is already encrypted). Encrypt a local bundle instead.\n'));
|
|
154
|
+
}
|
|
155
|
+
const { bundles, errors } = await pullBundlesFromHosts(g.host, forwardExportArgs(g, selectors, command));
|
|
156
|
+
for (const e of errors)
|
|
157
|
+
process.stderr.write(chalk.yellow(` ${e}\n`));
|
|
158
|
+
const records = mergeRecords(bundles.map(b => b.records));
|
|
159
|
+
if (records.length === 0) {
|
|
160
|
+
process.stderr.write(chalk.red('No sessions pulled from the given host(s).\n'));
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
const header = makeHeader({
|
|
164
|
+
origin: g.host.join(','),
|
|
165
|
+
exportedAt: new Date().toISOString(),
|
|
166
|
+
encrypted: false,
|
|
167
|
+
redacted: g.redact !== false,
|
|
168
|
+
records,
|
|
169
|
+
});
|
|
170
|
+
emitBundle(header, records, g);
|
|
171
|
+
}
|
|
172
|
+
/** Reconstruct the export flags to forward to a peer's own `sessions export`. */
|
|
173
|
+
function forwardExportArgs(g, selectors, command) {
|
|
174
|
+
const args = [...selectors];
|
|
175
|
+
if (g.since)
|
|
176
|
+
args.push('--since', g.since);
|
|
177
|
+
const agent = resolveAgentShorthand(g);
|
|
178
|
+
if (agent)
|
|
179
|
+
args.push('-a', agent);
|
|
180
|
+
if (g.all !== false)
|
|
181
|
+
args.push('--all');
|
|
182
|
+
if (g.redact === false)
|
|
183
|
+
args.push('--no-redact');
|
|
184
|
+
if (command.parent?.getOptionValueSource?.('limit') === 'cli' && g.limit)
|
|
185
|
+
args.push('-n', String(g.limit));
|
|
186
|
+
return args;
|
|
187
|
+
}
|
|
188
|
+
/** Write the assembled bundle to stdout or a file. */
|
|
189
|
+
function emitBundle(header, records, g) {
|
|
190
|
+
const wire = serializeBundle(header, records);
|
|
191
|
+
if (g.stdout) {
|
|
192
|
+
process.stdout.write(wire);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const outPath = g.output || defaultBundlePath();
|
|
196
|
+
fs.writeFileSync(outPath, wire, 'utf-8');
|
|
197
|
+
process.stderr.write(chalk.green(`Exported ${header.sessions} session${header.sessions === 1 ? '' : 's'} ` +
|
|
198
|
+
`(${header.count} file${header.count === 1 ? '' : 's'}${header.encrypted ? ', encrypted' : ''}${header.redacted ? ', redacted' : ''}) ` +
|
|
199
|
+
`→ ${outPath}\n`));
|
|
200
|
+
}
|
|
201
|
+
/** Map the parent's agent shorthands (--claude, --codex, …) or -a/--agent to a filter string. */
|
|
202
|
+
function resolveAgentShorthand(g) {
|
|
203
|
+
if (g.agent)
|
|
204
|
+
return g.agent;
|
|
205
|
+
if (g.claude)
|
|
206
|
+
return 'claude';
|
|
207
|
+
if (g.codex)
|
|
208
|
+
return 'codex';
|
|
209
|
+
if (g.kimi)
|
|
210
|
+
return 'kimi';
|
|
211
|
+
if (g.grok)
|
|
212
|
+
return 'grok';
|
|
213
|
+
if (g.opencode)
|
|
214
|
+
return 'opencode';
|
|
215
|
+
if (g.antigravity)
|
|
216
|
+
return 'antigravity';
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
/** ids > query > everything-in-scope. */
|
|
220
|
+
function selectSessions(metas, selectors) {
|
|
221
|
+
if (selectors.length === 0)
|
|
222
|
+
return metas;
|
|
223
|
+
const byId = [];
|
|
224
|
+
const unmatched = [];
|
|
225
|
+
for (const sel of selectors) {
|
|
226
|
+
const hits = resolveSessionById(metas, sel);
|
|
227
|
+
if (hits.length > 0)
|
|
228
|
+
byId.push(...hits);
|
|
229
|
+
else
|
|
230
|
+
unmatched.push(sel);
|
|
231
|
+
}
|
|
232
|
+
if (byId.length > 0 && unmatched.length === 0) {
|
|
233
|
+
const seen = new Set();
|
|
234
|
+
return byId.filter(s => (seen.has(s.id) ? false : (seen.add(s.id), true)));
|
|
235
|
+
}
|
|
236
|
+
// Any selector that isn't an id → treat the whole thing as a text query.
|
|
237
|
+
return filterSessionsByQuery(metas, selectors.join(' '));
|
|
238
|
+
}
|
|
239
|
+
/** Build `${agent}:${sessionId}` → LocalTranscript across every sync agent (live home only). */
|
|
240
|
+
function buildLocalIndex() {
|
|
241
|
+
const index = new Map();
|
|
242
|
+
for (const spec of SYNC_AGENTS) {
|
|
243
|
+
for (const lt of listLocalTranscripts(spec)) {
|
|
244
|
+
index.set(`${spec.id}:${lt.sessionId}`, lt);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return index;
|
|
248
|
+
}
|
|
249
|
+
/** Derive the subdir-relative key for a mirror file path, else fall back to the basename. */
|
|
250
|
+
function relKeyFromPath(filePath, agent, machine, subdir) {
|
|
251
|
+
const prefix = path.join(getHistoryDir(), 'backups', agent, machine, subdir) + path.sep;
|
|
252
|
+
if (filePath.startsWith(prefix))
|
|
253
|
+
return filePath.slice(prefix.length);
|
|
254
|
+
return path.basename(filePath);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Resolve the AES key for --encrypt: prefer the fleet-shared R2_SYNC_ENC_KEY (so
|
|
258
|
+
* any machine on the sync bundle can decrypt), else mint an ephemeral key and
|
|
259
|
+
* print it once — it is NOT stored in the bundle.
|
|
260
|
+
*/
|
|
261
|
+
function resolveExportKey() {
|
|
262
|
+
try {
|
|
263
|
+
const key = resolveSyncEncKey(loadR2Config());
|
|
264
|
+
if (key)
|
|
265
|
+
return key;
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
// sync bundle not configured — fall through to an ephemeral key
|
|
269
|
+
}
|
|
270
|
+
const b64 = generateSyncEncKey();
|
|
271
|
+
process.stderr.write(chalk.yellow(`Bundle encrypted with a fresh key (not in the bundle). Decrypt with:\n` +
|
|
272
|
+
` agents sessions import <bundle> --decrypt ${b64}\n`));
|
|
273
|
+
return Buffer.from(b64, 'base64');
|
|
274
|
+
}
|
|
275
|
+
/** Default output file when neither -o nor --stdout is given. */
|
|
276
|
+
function defaultBundlePath() {
|
|
277
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-').replace('T', '_').slice(0, 19);
|
|
278
|
+
return path.join(process.cwd(), `agents-sessions-${stamp}.bundle`);
|
|
279
|
+
}
|