@phnx-labs/agents-cli 1.20.15 → 1.20.17
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 +9 -0
- package/dist/commands/secrets.js +53 -1
- package/dist/commands/sessions-sync.d.ts +13 -0
- package/dist/commands/sessions-sync.js +73 -0
- package/dist/commands/sessions.js +2 -0
- package/dist/commands/sync.d.ts +10 -3
- package/dist/commands/sync.js +72 -9
- package/dist/commands/view.js +11 -3
- package/dist/index.js +1 -1
- package/dist/lib/agents.d.ts +11 -0
- package/dist/lib/agents.js +11 -9
- package/dist/lib/daemon.d.ts +19 -0
- package/dist/lib/daemon.js +97 -2
- package/dist/lib/hooks.js +12 -0
- package/dist/lib/migrate.d.ts +22 -0
- package/dist/lib/migrate.js +99 -1
- package/dist/lib/plugin-marketplace.d.ts +15 -0
- package/dist/lib/plugin-marketplace.js +54 -0
- package/dist/lib/secrets/drivers/rush.d.ts +14 -0
- package/dist/lib/secrets/drivers/rush.js +84 -0
- package/dist/lib/secrets/index.js +20 -0
- package/dist/lib/secrets/linux.js +88 -10
- package/dist/lib/secrets/sync-backend.d.ts +48 -0
- package/dist/lib/secrets/sync-backend.js +13 -0
- package/dist/lib/secrets/sync.d.ts +15 -23
- package/dist/lib/secrets/sync.js +31 -66
- package/dist/lib/session/parse.d.ts +2 -0
- package/dist/lib/session/parse.js +168 -2
- package/dist/lib/session/sync/agents.d.ts +46 -0
- package/dist/lib/session/sync/agents.js +94 -0
- package/dist/lib/session/sync/config.d.ts +30 -0
- package/dist/lib/session/sync/config.js +58 -0
- package/dist/lib/session/sync/crdt.d.ts +44 -0
- package/dist/lib/session/sync/crdt.js +119 -0
- package/dist/lib/session/sync/manifest.d.ts +51 -0
- package/dist/lib/session/sync/manifest.js +96 -0
- package/dist/lib/session/sync/r2.d.ts +32 -0
- package/dist/lib/session/sync/r2.js +121 -0
- package/dist/lib/session/sync/sync.d.ts +82 -0
- package/dist/lib/session/sync/sync.js +251 -0
- package/dist/lib/shims.d.ts +1 -1
- package/dist/lib/shims.js +17 -1
- package/dist/lib/sync-umbrella.d.ts +76 -0
- package/dist/lib/sync-umbrella.js +125 -0
- package/dist/lib/teams/parsers.js +159 -1
- package/dist/lib/usage.d.ts +18 -0
- package/dist/lib/usage.js +25 -0
- package/dist/lib/versions.js +30 -13
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
**Headless Linux: `agents secrets` works out of the box when the keyring is locked**
|
|
6
|
+
|
|
7
|
+
- On a headless server the libsecret/GNOME-keyring collection is locked, so the encrypted-file fallback is the only option — but it previously hard-failed unless `AGENTS_SECRETS_PASSPHRASE` was set, leaving `agents secrets` silently unusable. Now, on a headless run with no passphrase set, a random machine-local passphrase is auto-provisioned once at `~/.agents/.cache/secrets/.passphrase` (mode 0600) so the encrypted-file store just works. `AGENTS_SECRETS_PASSPHRASE` still takes precedence (off-disk key), an existing `.passphrase` is reused for stable interactive/headless behavior, and interactive TTY sessions are still prompted. Security model + resolution order documented in `docs/secrets.md`. (#371)
|
|
8
|
+
|
|
9
|
+
**`agents secrets get/set <item>`: raw, cross-platform keychain access for hooks**
|
|
10
|
+
|
|
11
|
+
- New `agents secrets get <item>` / `agents secrets set <item>` read and write a single keychain item **by bare name** (outside the bundle namespace), so shell hooks and automation have one platform-agnostic credential primitive to call instead of hardcoding `/usr/bin/security` (macOS-only) or `secret-tool` (Linux-only). `get` prints the value to stdout (newline-terminated for clean `$(…)` capture), sends diagnostics to stderr, and exits 1 with empty stdout when the item is missing — exactly what a `SessionStart` hook needs to probe-and-fallback quietly. Routing goes through the existing cross-platform keychain layer: macOS via `/usr/bin/security`, Linux via `secret-tool` with the encrypted-file fallback.
|
|
12
|
+
- `setKeychainToken` now writes bare (non-`agents-cli.`) items on macOS **without** the biometry ACL, mirroring the existing no-prompt read path for such items. This is what lets a hook read e.g. `linear-api-key` silently on every launch — routing it through the Touch ID helper would attach an ACL the `/usr/bin/security` read can't satisfy without popping the legacy password sheet. The change is purely additive: every existing caller passes an `agents-cli.`-namespaced item and is unaffected (still biometry-gated via the signed helper).
|
|
13
|
+
|
|
5
14
|
**`agents inspect` summary: expanded detail for hooks, plugins, and MCP**
|
|
6
15
|
|
|
7
16
|
- The bare `agents inspect <agent>` / `agents inspect <repo>` summary no longer collapses everything to a count table. Simple kinds (commands, skills, rules, subagents, workflows) keep a count line but now preview a few names; the rich kinds get their own expanded sections: **hooks** show their events + `matches:` predicates + cache (`PreToolUse(Bash) · git_dirty · prompt~"deploy" (5m cache)`), **plugins** show version + bundle contents (`v2.1.0 skills:6 commands:5 hooks:2 mcp:1`), and **MCP** show transport + url/command. Drill-down flags (`--hooks`, `--plugins`, `--mcp`) and `--brief` are unchanged; `--json` gains the structured detail additively (existing keys retained).
|
package/dist/commands/secrets.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import chalk from 'chalk';
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import { bundleExists, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, migrateLegacyBundles, parseDotenv, readBundle, renameBundle, rotateBundleSecret, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, } from '../lib/secrets/bundles.js';
|
|
11
|
-
import { deleteKeychainToken, getKeychainTokens, hasKeychainToken, secretsKeychainItem, setKeychainToken, } from '../lib/secrets/index.js';
|
|
11
|
+
import { deleteKeychainToken, getKeychainToken, getKeychainTokens, hasKeychainToken, secretsKeychainItem, setKeychainToken, } from '../lib/secrets/index.js';
|
|
12
12
|
import { assertOpAvailable, createPasswordItem, deleteItemByTitle, extractSecrets, itemExistsByTitle, listItems, listVaults, } from '../lib/onepassword.js';
|
|
13
13
|
import { registerCommandGroups, setHelpSections } from '../lib/help.js';
|
|
14
14
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
@@ -365,6 +365,7 @@ export function registerSecretsCommands(program) {
|
|
|
365
365
|
registerCommandGroups(cmd, [
|
|
366
366
|
{ title: 'Bundle commands', names: ['list', 'view', 'create', 'rename', 'describe', 'delete'] },
|
|
367
367
|
{ title: 'Secret commands', names: ['add', 'rotate', 'remove', 'import', 'export'] },
|
|
368
|
+
{ title: 'Raw item commands', names: ['get', 'set'] },
|
|
368
369
|
{ title: 'Sync commands', names: ['push', 'pull', 'remote-list'] },
|
|
369
370
|
{ title: 'Utilities', names: ['exec', 'generate', 'migrate-acl'] },
|
|
370
371
|
]);
|
|
@@ -465,6 +466,57 @@ export function registerSecretsCommands(program) {
|
|
|
465
466
|
process.exit(1);
|
|
466
467
|
}
|
|
467
468
|
});
|
|
469
|
+
cmd
|
|
470
|
+
.command('get <item>')
|
|
471
|
+
.description('Print a raw keychain item by name (for shell hooks/automation). Cross-platform; no bundle required.')
|
|
472
|
+
.action((item) => {
|
|
473
|
+
try {
|
|
474
|
+
// Routes through the platform keychain layer: macOS reads bare items
|
|
475
|
+
// via /usr/bin/security (no Touch ID), Linux via secret-tool with the
|
|
476
|
+
// encrypted-file fallback. The value goes to stdout (newline-terminated
|
|
477
|
+
// so `$(agents secrets get NAME)` captures it cleanly); diagnostics go
|
|
478
|
+
// to stderr so they never pollute the captured value.
|
|
479
|
+
const value = getKeychainToken(item);
|
|
480
|
+
process.stdout.write(value.endsWith('\n') ? value : `${value}\n`);
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
// Missing item is a normal, quiet outcome for a hook probe: exit 1,
|
|
484
|
+
// print nothing to stdout. Callers test the exit code / empty capture.
|
|
485
|
+
process.exit(1);
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
cmd
|
|
489
|
+
.command('set <item>')
|
|
490
|
+
.description('Store a raw keychain item by name (for shell hooks/automation). Cross-platform; no bundle required.')
|
|
491
|
+
.option('--value <v>', 'Value to store (omit to read from stdin or be prompted)')
|
|
492
|
+
.option('--value-stdin', 'Read the value from stdin')
|
|
493
|
+
.action(async (item, opts) => {
|
|
494
|
+
try {
|
|
495
|
+
let value;
|
|
496
|
+
if (opts.value !== undefined) {
|
|
497
|
+
value = opts.value;
|
|
498
|
+
}
|
|
499
|
+
else if (opts.valueStdin) {
|
|
500
|
+
value = readStdinSync();
|
|
501
|
+
if (!value)
|
|
502
|
+
throw new Error('No value received on stdin.');
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
value = await promptForSecret(`Enter value for ${item}`);
|
|
506
|
+
}
|
|
507
|
+
// setKeychainToken stores bare items WITHOUT the biometry ACL on macOS
|
|
508
|
+
// so `agents secrets get` can read them back without a password sheet;
|
|
509
|
+
// on Linux it goes through secret-tool / encrypted-file fallback.
|
|
510
|
+
setKeychainToken(item, value);
|
|
511
|
+
console.error(chalk.green(`Stored keychain item '${item}'.`));
|
|
512
|
+
}
|
|
513
|
+
catch (err) {
|
|
514
|
+
if (isPromptCancelled(err))
|
|
515
|
+
return;
|
|
516
|
+
console.error(chalk.red(err.message));
|
|
517
|
+
process.exit(1);
|
|
518
|
+
}
|
|
519
|
+
});
|
|
468
520
|
cmd
|
|
469
521
|
.command('create [name]')
|
|
470
522
|
.description('Create an empty bundle')
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents sessions sync` — push this machine's transcripts to R2 and pull every
|
|
3
|
+
* other machine's, merging copies of the same session via CRDT union. The local
|
|
4
|
+
* sessions index is rebuilt from the synced-in mirror by the normal scanner.
|
|
5
|
+
*/
|
|
6
|
+
import type { Command } from 'commander';
|
|
7
|
+
interface SyncCmdOptions {
|
|
8
|
+
verbose?: boolean;
|
|
9
|
+
json?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function runSessionsSync(options: SyncCmdOptions): Promise<void>;
|
|
12
|
+
export declare function registerSessionsSyncCommand(sessionsCmd: Command): void;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents sessions sync` — push this machine's transcripts to R2 and pull every
|
|
3
|
+
* other machine's, merging copies of the same session via CRDT union. The local
|
|
4
|
+
* sessions index is rebuilt from the synced-in mirror by the normal scanner.
|
|
5
|
+
*/
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import { setHelpSections } from '../lib/help.js';
|
|
8
|
+
import { isSyncConfigured, SYNC_BUNDLE } from '../lib/session/sync/config.js';
|
|
9
|
+
import { syncSessions } from '../lib/session/sync/sync.js';
|
|
10
|
+
export async function runSessionsSync(options) {
|
|
11
|
+
if (!isSyncConfigured()) {
|
|
12
|
+
console.error(chalk.red(`Sessions sync is not configured.`) +
|
|
13
|
+
`\nAdd R2 credentials to the '${SYNC_BUNDLE}' bundle:\n` +
|
|
14
|
+
` agents secrets add ${SYNC_BUNDLE} R2_ACCOUNT_ID\n` +
|
|
15
|
+
` agents secrets add ${SYNC_BUNDLE} R2_BUCKET_NAME\n` +
|
|
16
|
+
` agents secrets add ${SYNC_BUNDLE} R2_ACCESS_KEY_ID\n` +
|
|
17
|
+
` agents secrets add ${SYNC_BUNDLE} R2_SECRET_ACCESS_KEY`);
|
|
18
|
+
process.exitCode = 1;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const result = await syncSessions({
|
|
23
|
+
verbose: options.verbose,
|
|
24
|
+
log: msg => console.error(chalk.dim(msg)),
|
|
25
|
+
});
|
|
26
|
+
if (options.json) {
|
|
27
|
+
console.log(JSON.stringify(result, null, 2));
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
const parts = [
|
|
31
|
+
`pushed ${result.pushed}`,
|
|
32
|
+
`pulled ${result.pulled}`,
|
|
33
|
+
result.merged > 0 ? `merged ${result.merged}` : null,
|
|
34
|
+
].filter(Boolean);
|
|
35
|
+
console.log(chalk.green('synced') + ` ${result.machine}: ` + parts.join(', ') +
|
|
36
|
+
chalk.dim(` (${result.pushSkipped + result.pullSkipped} unchanged)`));
|
|
37
|
+
}
|
|
38
|
+
if (result.errors.length > 0) {
|
|
39
|
+
for (const e of result.errors)
|
|
40
|
+
console.error(chalk.yellow(` ! ${e}`));
|
|
41
|
+
process.exitCode = 1;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
console.error(chalk.red(`sync failed: ${err.message}`));
|
|
46
|
+
process.exitCode = 1;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function registerSessionsSyncCommand(sessionsCmd) {
|
|
50
|
+
const syncCmd = sessionsCmd
|
|
51
|
+
.command('sync')
|
|
52
|
+
.description('Sync session transcripts across machines via R2 (CRDT merge). Claude and Codex.')
|
|
53
|
+
.option('-v, --verbose', 'Log each pushed and pulled session')
|
|
54
|
+
.option('--json', 'Output the sync result as JSON');
|
|
55
|
+
setHelpSections(syncCmd, {
|
|
56
|
+
examples: `
|
|
57
|
+
# One sync cycle (push local changes, pull + merge from other machines)
|
|
58
|
+
agents sessions sync
|
|
59
|
+
|
|
60
|
+
# See exactly what moved
|
|
61
|
+
agents sessions sync --verbose
|
|
62
|
+
`,
|
|
63
|
+
notes: `
|
|
64
|
+
- Credentials come from the '${SYNC_BUNDLE}' secrets bundle (R2 S3 API, read+write).
|
|
65
|
+
- Each machine writes only its own prefix; conflicts are impossible by construction.
|
|
66
|
+
- The daemon runs this automatically (~90s); this command forces an immediate cycle.
|
|
67
|
+
- Sessions present locally always win; synced-in copies fill in other machines' sessions.
|
|
68
|
+
`,
|
|
69
|
+
});
|
|
70
|
+
syncCmd.action(async (options) => {
|
|
71
|
+
await runSessionsSync(options);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
@@ -29,6 +29,7 @@ import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
|
29
29
|
import { sessionPicker } from './sessions-picker.js';
|
|
30
30
|
import { setHelpSections } from '../lib/help.js';
|
|
31
31
|
import { registerSessionsTailCommand } from './sessions-tail.js';
|
|
32
|
+
import { registerSessionsSyncCommand } from './sessions-sync.js';
|
|
32
33
|
const SESSION_AGENT_FILTER_HELP = `Filter by agent, e.g. claude, codex, claude@2.0.65`;
|
|
33
34
|
const CLAUDE_RESUME_MATCH_WINDOW_MS = 10 * 60_000;
|
|
34
35
|
const LOAD_VERBS = ['Loading', 'Scanning', 'Gathering', 'Indexing', 'Reading'];
|
|
@@ -1144,6 +1145,7 @@ export function registerSessionsCommands(program) {
|
|
|
1144
1145
|
await sessionsAction(query, options);
|
|
1145
1146
|
});
|
|
1146
1147
|
registerSessionsTailCommand(sessionsCmd);
|
|
1148
|
+
registerSessionsSyncCommand(sessionsCmd);
|
|
1147
1149
|
}
|
|
1148
1150
|
function formatNoSessionsMessage(showAll, project) {
|
|
1149
1151
|
const projectQuery = project?.trim();
|
package/dist/commands/sync.d.ts
CHANGED
|
@@ -2,11 +2,18 @@
|
|
|
2
2
|
* `agents sync` — synchronize central resources into an installed agent version.
|
|
3
3
|
*
|
|
4
4
|
* Forms:
|
|
5
|
-
* agents sync
|
|
6
|
-
* agents sync
|
|
7
|
-
* agents sync
|
|
5
|
+
* agents sync # umbrella: fetch remote (repos+secrets+sessions) -> reconcile all
|
|
6
|
+
* agents sync --repos|--secrets|--sessions # umbrella: fetch only those, then reconcile
|
|
7
|
+
* agents sync --cloud # umbrella: fetch all, skip reconcile
|
|
8
|
+
* agents sync --local # umbrella: reconcile all, no fetch
|
|
9
|
+
* agents sync claude # one agent: uses default/sole installed version
|
|
10
|
+
* agents sync claude@2.1.142 # one agent: explicit version
|
|
11
|
+
* agents sync claude@latest # one agent: newest installed
|
|
8
12
|
* agents sync --agent claude --agent-version 2.1.142 # legacy form, still supported
|
|
9
13
|
*
|
|
14
|
+
* The umbrella stages live in lib/sync-umbrella.ts; this file dispatches to them
|
|
15
|
+
* when no agent is given.
|
|
16
|
+
*
|
|
10
17
|
* In a TTY the command previews available/new resources and lets the user
|
|
11
18
|
* select what to sync (same prompts shown after `agents add`). Pass
|
|
12
19
|
* --yes for non-interactive auto-sync, --force to re-sync when nothing
|
package/dist/commands/sync.js
CHANGED
|
@@ -2,11 +2,18 @@
|
|
|
2
2
|
* `agents sync` — synchronize central resources into an installed agent version.
|
|
3
3
|
*
|
|
4
4
|
* Forms:
|
|
5
|
-
* agents sync
|
|
6
|
-
* agents sync
|
|
7
|
-
* agents sync
|
|
5
|
+
* agents sync # umbrella: fetch remote (repos+secrets+sessions) -> reconcile all
|
|
6
|
+
* agents sync --repos|--secrets|--sessions # umbrella: fetch only those, then reconcile
|
|
7
|
+
* agents sync --cloud # umbrella: fetch all, skip reconcile
|
|
8
|
+
* agents sync --local # umbrella: reconcile all, no fetch
|
|
9
|
+
* agents sync claude # one agent: uses default/sole installed version
|
|
10
|
+
* agents sync claude@2.1.142 # one agent: explicit version
|
|
11
|
+
* agents sync claude@latest # one agent: newest installed
|
|
8
12
|
* agents sync --agent claude --agent-version 2.1.142 # legacy form, still supported
|
|
9
13
|
*
|
|
14
|
+
* The umbrella stages live in lib/sync-umbrella.ts; this file dispatches to them
|
|
15
|
+
* when no agent is given.
|
|
16
|
+
*
|
|
10
17
|
* In a TTY the command previews available/new resources and lets the user
|
|
11
18
|
* select what to sync (same prompts shown after `agents add`). Pass
|
|
12
19
|
* --yes for non-interactive auto-sync, --force to re-sync when nothing
|
|
@@ -25,12 +32,13 @@ import { isVersionInstalled, syncResourcesToVersion, parseAgentSpec, resolveVers
|
|
|
25
32
|
import { compileRulesForProject } from '../lib/rules/compile.js';
|
|
26
33
|
import { runLaunchSync } from '../lib/project-launch.js';
|
|
27
34
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
35
|
+
import { runUmbrellaSync } from '../lib/sync-umbrella.js';
|
|
28
36
|
/** Register the `agents sync` command. */
|
|
29
37
|
export function registerSyncCommand(program) {
|
|
30
38
|
program
|
|
31
39
|
.command('sync [agentSpec]')
|
|
32
|
-
.summary('
|
|
33
|
-
.description('
|
|
40
|
+
.summary('Make this machine current, or sync resources into one agent')
|
|
41
|
+
.description('With an [agentSpec], syncs resources (commands, skills, hooks, rules, MCPs, plugins, etc.) into that installed agent version — previews changes and lets you pick. e.g. "claude" or "claude@2.1.142".\n\nWith NO agent, runs the umbrella verb: fetch remote state (config repos + secrets + sessions) then reconcile it into every installed agent. Scope it with --repos / --secrets / --sessions, --cloud (fetch only), or --local (reconcile only).')
|
|
34
42
|
.option('--agent <agent>', 'Agent identifier (legacy form; prefer the positional spec)')
|
|
35
43
|
.option('--agent-version <version>', 'Version to sync into (legacy form; prefer "agent@version")')
|
|
36
44
|
.option('--project-dir <path>', 'Path to project-level .agents/ directory containing project-scoped resources')
|
|
@@ -39,10 +47,66 @@ export function registerSyncCommand(program) {
|
|
|
39
47
|
.option('-y, --yes', 'Skip the interactive preview and auto-sync all detected resources', false)
|
|
40
48
|
.option('--force', 'Re-sync even if no changes are detected since the last sync', false)
|
|
41
49
|
.option('--quiet', 'Suppress all output (exit code indicates success)', false)
|
|
50
|
+
// Umbrella verb (no agent given): make this machine current.
|
|
51
|
+
.option('--repos', 'Umbrella: git-pull ~/.agents + enabled ~/.agents-* extras', false)
|
|
52
|
+
.option('--secrets', 'Umbrella: pull encrypted secret bundles from the remote', false)
|
|
53
|
+
.option('--sessions', 'Umbrella: sync session transcripts across machines', false)
|
|
54
|
+
.option('--cloud', 'Umbrella: fetch all remote state but skip the local reconcile', false)
|
|
55
|
+
.option('--local', "Umbrella: reconcile resources into installed agents only (no fetch)", false)
|
|
42
56
|
.action(async (agentSpec, opts) => {
|
|
43
57
|
await runSync(agentSpec, opts);
|
|
44
58
|
});
|
|
45
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* The umbrella verb: bare `agents sync` (no agent) makes this machine current.
|
|
62
|
+
* Resolves the flags + a secrets passphrase (env-only for now; tokenized auth
|
|
63
|
+
* arrives with `agents login`) and runs the fetch+reconcile stages, then prints
|
|
64
|
+
* a one-line summary. Stage failures are non-fatal and surfaced as warnings.
|
|
65
|
+
*/
|
|
66
|
+
async function runUmbrella(opts, quiet, outLog, errLog) {
|
|
67
|
+
const flags = {
|
|
68
|
+
repos: opts.repos,
|
|
69
|
+
secrets: opts.secrets,
|
|
70
|
+
sessions: opts.sessions,
|
|
71
|
+
cloud: opts.cloud,
|
|
72
|
+
local: opts.local,
|
|
73
|
+
};
|
|
74
|
+
const passphrase = process.env.AGENTS_SECRETS_PASSPHRASE || undefined;
|
|
75
|
+
if (!quiet)
|
|
76
|
+
outLog(chalk.bold('Syncing this machine…'));
|
|
77
|
+
try {
|
|
78
|
+
const result = await runUmbrellaSync({
|
|
79
|
+
flags,
|
|
80
|
+
yes: !!opts.yes,
|
|
81
|
+
passphrase,
|
|
82
|
+
log: (msg) => { if (!quiet)
|
|
83
|
+
outLog(chalk.gray(` ${msg}`)); },
|
|
84
|
+
});
|
|
85
|
+
if (!quiet) {
|
|
86
|
+
const parts = [];
|
|
87
|
+
if (result.repos) {
|
|
88
|
+
parts.push(`repos ${result.repos.pulled} pulled` +
|
|
89
|
+
(result.repos.errors.length ? `, ${result.repos.errors.length} failed` : ''));
|
|
90
|
+
}
|
|
91
|
+
if (result.secrets) {
|
|
92
|
+
parts.push(result.secrets.skipped ? 'secrets skipped' : `secrets ${result.secrets.pulled} pulled`);
|
|
93
|
+
}
|
|
94
|
+
if (result.sessions) {
|
|
95
|
+
parts.push(result.sessions.ran ? `sessions ${result.sessions.merged} merged` : 'sessions off');
|
|
96
|
+
}
|
|
97
|
+
if (result.reconciled)
|
|
98
|
+
parts.push('reconciled');
|
|
99
|
+
outLog(chalk.green(`✓ sync: ${parts.join(' · ') || 'nothing to do'}`));
|
|
100
|
+
const errs = [...(result.repos?.errors ?? []), ...(result.secrets?.errors ?? [])];
|
|
101
|
+
for (const e of errs)
|
|
102
|
+
errLog(chalk.yellow(` ! ${e}`));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
errLog(chalk.red(`sync failed: ${err.message}`));
|
|
107
|
+
process.exitCode = 1;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
46
110
|
async function runSync(agentSpec, opts) {
|
|
47
111
|
const quiet = !!opts.quiet;
|
|
48
112
|
const errLog = (msg) => { if (!quiet)
|
|
@@ -77,10 +141,9 @@ async function runSync(agentSpec, opts) {
|
|
|
77
141
|
version = opts.agentVersion;
|
|
78
142
|
}
|
|
79
143
|
if (!agentId) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
process.exitCode = 1;
|
|
144
|
+
// No agent specified → the umbrella verb: make this machine current
|
|
145
|
+
// (fetch repos + secrets + sessions, then reconcile all installed agents).
|
|
146
|
+
await runUmbrella(opts, quiet, outLog, errLog);
|
|
84
147
|
return;
|
|
85
148
|
}
|
|
86
149
|
// ---------- 2. Resolve version (project pin → global default → sole installed) ----------
|
package/dist/commands/view.js
CHANGED
|
@@ -3,7 +3,7 @@ import ora from 'ora';
|
|
|
3
3
|
import * as fs from 'fs';
|
|
4
4
|
import * as path from 'path';
|
|
5
5
|
import { AGENTS, ALL_AGENT_IDS, getAllCliStates, getAccountInfo, resolveAgentName, formatAgentError, agentLabel, colorAgent, } from '../lib/agents.js';
|
|
6
|
-
import { formatUsageSection, formatUsageSummary, formatUsageStatusBadge, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
|
|
6
|
+
import { deriveUsageStatusFromSnapshot, formatUsageSection, formatUsageSummary, formatUsageStatusBadge, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
|
|
7
7
|
import { readManifest } from '../lib/manifest.js';
|
|
8
8
|
import { listInstalledVersions, listInstalledVersionDirs, getGlobalDefault, getVersionHomePath, getVersionDir, resolveVersionAlias, getAvailableResources, getActuallySyncedResources, getNewResources, getProjectOnlyResources, hasNewResources, promptNewResourceSelection, syncResourcesToVersion, removeVersion, printTrashFooter, } from '../lib/versions.js';
|
|
9
9
|
import { ensureVersionedAliasCurrent, removeShim, } from '../lib/shims.js';
|
|
@@ -306,7 +306,11 @@ async function showInstalledVersions(filterAgentId) {
|
|
|
306
306
|
return {
|
|
307
307
|
...info,
|
|
308
308
|
plan: canon.plan,
|
|
309
|
-
|
|
309
|
+
// Throttle state comes from the live usage windows, not the pay-as-you-go
|
|
310
|
+
// overage flag that AccountInfo.usageStatus used to carry. A maxed window
|
|
311
|
+
// means rate-limited; no snapshot means no badge. See
|
|
312
|
+
// deriveUsageStatusFromSnapshot.
|
|
313
|
+
usageStatus: deriveUsageStatusFromSnapshot(usageByKey.get(key)?.snapshot),
|
|
310
314
|
overageCredits: canon.overageCredits,
|
|
311
315
|
};
|
|
312
316
|
};
|
|
@@ -984,7 +988,11 @@ async function collectAgentsJson(filterAgentId) {
|
|
|
984
988
|
return {
|
|
985
989
|
...info,
|
|
986
990
|
plan: canon.plan,
|
|
987
|
-
|
|
991
|
+
// Throttle state comes from the live usage windows, not the pay-as-you-go
|
|
992
|
+
// overage flag that AccountInfo.usageStatus used to carry. A maxed window
|
|
993
|
+
// means rate-limited; no snapshot means no badge. See
|
|
994
|
+
// deriveUsageStatusFromSnapshot.
|
|
995
|
+
usageStatus: deriveUsageStatusFromSnapshot(usageByKey.get(key)?.snapshot),
|
|
988
996
|
overageCredits: canon.overageCredits,
|
|
989
997
|
};
|
|
990
998
|
};
|
package/dist/index.js
CHANGED
|
@@ -893,7 +893,7 @@ if (process.env.AGENTS_SKIP_MIGRATION !== '1') {
|
|
|
893
893
|
// Bumping the suffix re-runs migrations for every user; binary releases that
|
|
894
894
|
// don't change the schema must NOT re-run (they would destroy user content
|
|
895
895
|
// when migration steps overlap with user-authored paths). See issue #20.
|
|
896
|
-
const sentinelValue = '
|
|
896
|
+
const sentinelValue = 'v10';
|
|
897
897
|
let needRun = true;
|
|
898
898
|
try {
|
|
899
899
|
if (fs.existsSync(sentinel) && fs.readFileSync(sentinel, 'utf-8').trim() === sentinelValue) {
|
package/dist/lib/agents.d.ts
CHANGED
|
@@ -13,6 +13,17 @@ export interface CliState {
|
|
|
13
13
|
export declare const CODEX_HOOKS_MIN_VERSION = "0.116.0";
|
|
14
14
|
/** Minimum Gemini CLI version that supports the hooks system (v0.26.0, Jan 2026). */
|
|
15
15
|
export declare const GEMINI_HOOKS_MIN_VERSION = "0.26.0";
|
|
16
|
+
/**
|
|
17
|
+
* Synchronous PATH search -- no subprocess. Returns first matching binary path.
|
|
18
|
+
*
|
|
19
|
+
* Skips our own shims dir (`~/.agents/.cache/shims/`) — those shims are
|
|
20
|
+
* dispatch helpers, not real installs. Counting them as installed produced a
|
|
21
|
+
* false positive where agents with NO real binary on the host (e.g. a
|
|
22
|
+
* never-installed Cursor whose only PATH entry was our `cursor-agent` shim
|
|
23
|
+
* dispatcher) showed up under `agents view`'s "Not Managed by Agents CLI"
|
|
24
|
+
* section, even though the user had nothing to import.
|
|
25
|
+
*/
|
|
26
|
+
export declare function findInPath(command: string): string | null;
|
|
16
27
|
/**
|
|
17
28
|
* Master registry of all supported agents keyed by AgentId.
|
|
18
29
|
*
|
package/dist/lib/agents.js
CHANGED
|
@@ -66,7 +66,7 @@ function saveCliVersionCache() {
|
|
|
66
66
|
* dispatcher) showed up under `agents view`'s "Not Managed by Agents CLI"
|
|
67
67
|
* section, even though the user had nothing to import.
|
|
68
68
|
*/
|
|
69
|
-
function findInPath(command) {
|
|
69
|
+
export function findInPath(command) {
|
|
70
70
|
const pathEnv = process.env.PATH || '';
|
|
71
71
|
const pathExt = process.platform === 'win32' ? (process.env.PATHEXT || '').split(';') : [''];
|
|
72
72
|
const shimsDir = getShimsDir();
|
|
@@ -814,14 +814,16 @@ export async function getAccountInfo(agentId, home) {
|
|
|
814
814
|
else if (oa?.billingType) {
|
|
815
815
|
plan = oa.billingType;
|
|
816
816
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
817
|
+
// usageStatus is NOT derived from cachedExtraUsageDisabledReason. That
|
|
818
|
+
// field reports why pay-as-you-go overage is off (out_of_credits = no
|
|
819
|
+
// overage credits purchased; org_level_disabled = admin turned overage
|
|
820
|
+
// off), which says nothing about whether the account is throttled — a
|
|
821
|
+
// Pro account at 5% weekly usage with overage disabled is fully usable.
|
|
822
|
+
// Real throttle state comes from the live usage windows; callers derive
|
|
823
|
+
// it via deriveUsageStatusFromSnapshot(). Here we only report whether
|
|
824
|
+
// the account is signed in at all. Overage state stays visible through
|
|
825
|
+
// overageCredits below.
|
|
826
|
+
const usageStatus = email ? 'available' : null;
|
|
825
827
|
let overageCredits = null;
|
|
826
828
|
const orgId = oa?.organizationUuid;
|
|
827
829
|
const creditCache = orgId && data.overageCreditGrantCache?.[orgId];
|
package/dist/lib/daemon.d.ts
CHANGED
|
@@ -18,6 +18,16 @@ export declare function isDaemonRunning(): boolean;
|
|
|
18
18
|
export declare function log(level: string, message: string): void;
|
|
19
19
|
/** Main daemon loop: load jobs, schedule crons, monitor runs, and handle signals. */
|
|
20
20
|
export declare function runDaemon(): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Read the long-lived Claude OAuth token (from `claude setup-token`) that the
|
|
23
|
+
* user stored under the `claude` secrets bundle. Resolves the bundle the same
|
|
24
|
+
* way `agents run --secrets` does, so the token is found whether it was stored
|
|
25
|
+
* keychain-backed or as a literal. Returns null when the bundle/key isn't
|
|
26
|
+
* configured, the Keychain read is cancelled, or the platform has no keychain —
|
|
27
|
+
* the daemon then behaves exactly as before (relying on the interactive OAuth
|
|
28
|
+
* session). Never throws: a misconfigured token must not block daemon startup.
|
|
29
|
+
*/
|
|
30
|
+
export declare function readDaemonClaudeOAuthToken(): string | null;
|
|
21
31
|
/** Generate a macOS launchd plist for auto-starting the daemon. */
|
|
22
32
|
export declare function generateLaunchdPlist(): string;
|
|
23
33
|
/** Generate a Linux systemd user unit for auto-starting the daemon. */
|
|
@@ -27,6 +37,15 @@ export declare function startDaemon(): {
|
|
|
27
37
|
pid: number | null;
|
|
28
38
|
method: string;
|
|
29
39
|
};
|
|
40
|
+
/**
|
|
41
|
+
* Environment for the detached daemon fallback. The launchd/systemd paths
|
|
42
|
+
* deliver the long-lived OAuth token via the service manifest's environment;
|
|
43
|
+
* the detached path has no manifest, so inject it here. Read happens during an
|
|
44
|
+
* interactive `routines start`, so a Keychain Touch ID prompt can be satisfied;
|
|
45
|
+
* the daemon then passes it to every routine run it spawns. An already-set
|
|
46
|
+
* value (e.g. inherited from launchd) is left untouched.
|
|
47
|
+
*/
|
|
48
|
+
export declare function buildDetachedDaemonEnv(baseEnv?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
30
49
|
/** Stop the daemon, unloading it from launchd/systemd if applicable. */
|
|
31
50
|
export declare function stopDaemon(): boolean;
|
|
32
51
|
/** Get current daemon status including running state, PID, and enabled job count. */
|