@phnx-labs/agents-cli 1.22.118 → 1.22.119
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 +66 -0
- package/dist/commands/accounts.js +7 -4
- package/dist/commands/projects.js +92 -7
- package/dist/lib/accounts/add.d.ts +3 -1
- package/dist/lib/accounts/add.js +24 -13
- package/dist/lib/agent-spec/agents.js +1 -0
- package/dist/lib/claude-account-token.d.ts +7 -1
- package/dist/lib/claude-account-token.js +26 -4
- package/dist/lib/exec.js +10 -2
- package/dist/lib/github/pr-mergeable.d.ts +4 -2
- package/dist/lib/github/pr-mergeable.js +5 -3
- package/dist/lib/github/project-prs.d.ts +58 -3
- package/dist/lib/github/project-prs.js +205 -7
- package/dist/lib/harness/adapters/cursor.js +14 -4
- package/dist/lib/harness-auth-capabilities.d.ts +4 -3
- package/dist/lib/harness-auth-capabilities.js +1 -1
- package/dist/lib/helper-versions.js +1 -1
- package/dist/lib/installations/shims.d.ts +1 -1
- package/dist/lib/installations/shims.js +23 -5
- package/dist/lib/installations/store.d.ts +9 -0
- package/dist/lib/installations/store.js +24 -0
- package/dist/lib/installations/versions.js +3 -2
- package/dist/lib/projects.d.ts +16 -0
- package/dist/lib/projects.js +31 -0
- package/dist/lib/session/active.d.ts +17 -6
- package/dist/lib/session/active.js +105 -49
- package/dist/lib/session/pid-registry.d.ts +6 -3
- package/dist/lib/session/pid-registry.js +18 -8
- package/dist/lib/session/remote/watch.d.ts +1 -1
- package/dist/lib/session/remote/watch.js +7 -3
- package/dist/lib/types.d.ts +6 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,71 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.22.119
|
|
4
|
+
|
|
5
|
+
- **A setup-token (provider) Claude account no longer opens on Claude Code's first-run
|
|
6
|
+
theme picker on a worker.** A provider account has no slot, so it runs in the
|
|
7
|
+
executable's shared version home, and nothing ever marked that home onboarded: an AGI
|
|
8
|
+
EXT tab dispatched to a worker landed on "Choose the text style…" instead of the agent.
|
|
9
|
+
A credentialed Claude spawn on a non-headed device now seeds `hasCompletedOnboarding` in
|
|
10
|
+
the home it runs in, right after the worker login-trap preflight in `spawnAgentLeased`
|
|
11
|
+
(explicit `#account`, default account, balanced pick, interactive tmux). Headless paths
|
|
12
|
+
that spawn elsewhere (`--loop`, routines) never show onboarding in print mode; the
|
|
13
|
+
Windows `.cmd` shim passthrough is not covered. With no email the seed writes only the
|
|
14
|
+
flag, so the home's own login identity is untouched; it writes through the
|
|
15
|
+
`.claude/.claude.json -> ../.claude.json` link instead of replacing it; and a document
|
|
16
|
+
already onboarded is left byte-identical. Source: `cli/src/lib/exec.ts`,
|
|
17
|
+
`cli/src/lib/claude-account-token.ts`.
|
|
18
|
+
|
|
19
|
+
- **A headless Cursor run in plan mode now runs in folders Cursor has not seen
|
|
20
|
+
before, and prints its answer.** Two bugs stopped `agents run cursor "…" --mode
|
|
21
|
+
plan`, a Cursor teammate (`agents teams add … cursor …`), and a plan-mode Cursor
|
|
22
|
+
routine from working unattended:
|
|
23
|
+
- They passed `--plan` without `--trust`. In a directory Cursor had not seen,
|
|
24
|
+
Cursor stopped on "Do you trust the contents of this directory?" and exited
|
|
25
|
+
with no one to answer, so a teammate failed in 7 seconds with 0 tools. Every
|
|
26
|
+
headless mode except skip (which passes `-f`) now passes `--trust`. `--trust`
|
|
27
|
+
only accepts the configured working directory and does not bypass tool
|
|
28
|
+
permissions.
|
|
29
|
+
- In `--plan` mode Cursor delivers its answer through its createPlan tool, which
|
|
30
|
+
`-p` text output never prints. A run would read the repo for 3 minutes, exit 0,
|
|
31
|
+
and leave an empty log. A headless plan run now uses Cursor's read-only ask
|
|
32
|
+
mode (`--mode ask`), which refuses file writes the same way and prints its
|
|
33
|
+
answer.
|
|
34
|
+
|
|
35
|
+
Interactive Cursor runs keep `--plan` and Cursor's own trust prompt. Source:
|
|
36
|
+
`cli/src/lib/harness/adapters/cursor.ts`.
|
|
37
|
+
|
|
38
|
+
- **Grok subscription seats log in per worker through the device-code flow.** `grok` now has the
|
|
39
|
+
same `per-device:device-auth` worker path as `codex`: `agents accounts add grok <name> --per-device`
|
|
40
|
+
registers the account with no API key, and `agents accounts login grok#<name> --per-device` on a
|
|
41
|
+
worker runs `grok login --device-auth` into that account's slot. The box prints a URL and code
|
|
42
|
+
that you approve in your own browser, so SuperGrok / X Premium+ seats run on workers without an
|
|
43
|
+
`XAI_API_KEY`, which bills API credits rather than the subscription. `accounts login --per-device`
|
|
44
|
+
is new and is the one login a worker may run for a dual-path harness (codex too); the plain
|
|
45
|
+
re-login stays headed-only. Source: `cli/src/lib/harness-auth-capabilities.ts`,
|
|
46
|
+
`cli/src/lib/accounts/add.ts`, `cli/src/commands/accounts.ts`.
|
|
47
|
+
|
|
48
|
+
- **The `grok` shim runs the release `grok update` installed.** Grok's updater now writes
|
|
49
|
+
`~/.grok/bin/grok-<version>` and repoints `~/.grok/bin/grok`, leaving `downloads/` alone, but the
|
|
50
|
+
shim only scanned `downloads/`, so it kept running the old binary after an update (on one worker,
|
|
51
|
+
1.0.4, which xAI rejects with `426 ... outdated`). The shim, the `grok@<version>` alias, and
|
|
52
|
+
`getBinaryPath` now follow grok's own `bin/grok` pointer first and fall back to the `downloads/`
|
|
53
|
+
scan for older layouts. The shim schema version is bumped so installed shims regenerate.
|
|
54
|
+
Source: `cli/src/lib/installations/shims.ts`, `cli/src/lib/installations/store.ts`.
|
|
55
|
+
|
|
56
|
+
- **Editor terminals follow the current agent session after a restart (PHNX-4218).**
|
|
57
|
+
Active/watch rows adopt the live agent descendant's recorded session, transcript,
|
|
58
|
+
terminal ID and launch ID while keeping the tab's shell PID and metadata. Only the
|
|
59
|
+
nearest recorded agents qualify, so nested agents cannot take over the tab. The most
|
|
60
|
+
recent recorded start wins at that depth, with PID as a stable tie-breaker; known
|
|
61
|
+
tab kinds must match. Windows retains the published identity until process starts
|
|
62
|
+
can be verified. Published terminal IDs
|
|
63
|
+
also survive when no descendant supplies one. Rows expose a display-only `accountLabel`
|
|
64
|
+
when the indexed account identifies one registered native slot; ambiguous identities
|
|
65
|
+
stay unlabeled, and org-only keys require a matching email.
|
|
66
|
+
|
|
67
|
+
- **`agents projects prs` scopes a shared monorepo per project, and `agents projects prs merge` merges one PR (PHNX-4215).** When several projects share one repository, each project now lists only the PRs whose changed files touch the paths it claims (`scope: project`) or touch no sharing project's paths (`scope: repo-wide`). It uses the same `defaultPath`/`subpath` claim that attributes a session's cwd, and changed files are cached per head SHA. Rows gain `createdAt`. The envelope gains `viewer` and a per-repo `sharedWith`, and the `--number` detail adds `mergeable`/`mergeableState`. `agents projects prs merge <name> --repo … --number … --sha <head>` merges with one REST call pinned to the reviewed head, so GitHub refuses if the branch moved since. Repo canonicalization moved from GraphQL `gh repo view` to a cached REST read, which drops a warm refresh from about 2s to under 1s. Source: `cli/src/lib/github/project-prs.ts`, `cli/src/lib/projects.ts`, `cli/src/commands/projects.ts`.
|
|
68
|
+
|
|
3
69
|
## 1.22.118
|
|
4
70
|
|
|
5
71
|
- **`agents sessions <read> --device <name>` routes a SINGLE-device READ through the standalone `sessions --host` when both sides support it (PHNX-4012).**
|
|
@@ -364,7 +364,7 @@ agents accounts list --fleet`,
|
|
|
364
364
|
const addCmd = accounts.command('add <target> [name]')
|
|
365
365
|
.description('Add an account. Harness form: add <harness> [name] runs the native login in a fresh credential slot and provisions workers. Provider form: add <name> --provider <p> --auth <t> stores a durable credential.')
|
|
366
366
|
.option('--api-key <key>', 'Worker credential for api-key harnesses (codex/grok/cursor/opencode); prompted when omitted')
|
|
367
|
-
.option('--per-device', 'codex: ChatGPT
|
|
367
|
+
.option('--per-device', 'codex/grok: subscription seat (ChatGPT plan, SuperGrok, X Premium+) — no API key stored; workers log in per box')
|
|
368
368
|
.option('--no-worker-token', 'Skip minting/collecting the worker credential (this device only)')
|
|
369
369
|
.option('--provider <provider>', `Provider form: credential provider (${listAccountProviders().join(', ')})`)
|
|
370
370
|
.option('--auth <type>', 'Provider form: api-key | setup-token | bearer-token')
|
|
@@ -428,12 +428,13 @@ agents accounts add codex gmail --per-device # ChatGPT-plan seat; workers
|
|
|
428
428
|
agents accounts add claude # name derived from the login's email
|
|
429
429
|
agents accounts add work --provider anthropic --auth setup-token # provider form (non-harness name)
|
|
430
430
|
agents run claude#work`,
|
|
431
|
-
notes: `Headed devices only: on a worker, add refuses before any slot, install, or browser — workers are provisioned automatically from the durable credential the add mints (claude: a setup-token; codex/grok/cursor/opencode: an API key via --api-key or a prompt; codex also
|
|
431
|
+
notes: `Headed devices only: on a worker, add refuses before any slot, install, or browser — workers are provisioned automatically from the durable credential the add mints (claude: a setup-token; codex/grok/cursor/opencode: an API key via --api-key or a prompt; codex and grok also accept --per-device for a subscription seat; kimi/antigravity log in per box). An account is a credential SLOT, not a second installation: the harness has one managed install and each account is a HOME-shaped slot with its own native OAuth login (never copied, never fleet-synced). Re-running add for an already-added identity points at 'agents accounts login <harness>#<name>'. Supported: ${addSupportedList()}; other harnesses fail with the reason. An ambient CLAUDE_CODE_OAUTH_TOKEN in your shell refuses the mint (unset it or pass --no-worker-token).`,
|
|
432
432
|
});
|
|
433
433
|
const loginCmd = accounts.command('login <account>')
|
|
434
434
|
.description('Re-authenticate an account into its slot on this device (<harness>#<name>); re-mints and re-syncs the worker credential')
|
|
435
435
|
.option('--api-key <key>', 'Rotate the stored worker API key (api-key harnesses)')
|
|
436
436
|
.option('--no-worker-token', 'Skip re-minting the worker credential')
|
|
437
|
+
.option('--per-device', 'codex/grok: sign this box in to the subscription seat via the device-code flow (allowed on workers)')
|
|
437
438
|
.option('--json', 'Machine-readable result (never includes a credential)')
|
|
438
439
|
.action(async (selector, o, command) => {
|
|
439
440
|
await runAccountsAction(command, async () => {
|
|
@@ -448,6 +449,7 @@ agents run claude#work`,
|
|
|
448
449
|
console.log(chalk.gray(` ${m}`)); },
|
|
449
450
|
apiKey: o.apiKey,
|
|
450
451
|
noWorkerToken: o.workerToken === false,
|
|
452
|
+
perDevice: o.perDevice,
|
|
451
453
|
});
|
|
452
454
|
printAddResult(result, json);
|
|
453
455
|
});
|
|
@@ -455,8 +457,9 @@ agents run claude#work`,
|
|
|
455
457
|
setHelpSections(loginCmd, {
|
|
456
458
|
examples: `agents accounts login claude#work
|
|
457
459
|
agents accounts login codex#personal --api-key sk-… # rotate the worker key
|
|
458
|
-
agents accounts login kimi#main # per-device: logs THIS box in
|
|
459
|
-
|
|
460
|
+
agents accounts login kimi#main # per-device: logs THIS box in
|
|
461
|
+
agents ssh <worker> agents accounts login grok#work --per-device # the worker signs in to the subscription seat`,
|
|
462
|
+
notes: 'Re-runs the native login into the SAME slot (never a new home) and fails closed when the completed login is a different identity than the account. Headed devices only for harnesses with a portable worker credential; a per-device harness (kimi) may log in on any box — that is how that box gets its login. With --per-device a codex or grok account signs THIS box in to its subscription seat through the device-code flow: the box prints a URL and code, you approve it in your own browser, and no API key is stored or synced. The daemon re-syncs the account row and worker credential after a successful login.',
|
|
460
463
|
});
|
|
461
464
|
const defaultCmd = accounts.command('default <harness> [name]')
|
|
462
465
|
.description('Set the fleet-wide default account for a harness (picker when no name)')
|
|
@@ -30,7 +30,8 @@ import { checkRepoSlug } from '../lib/project-doctor.js';
|
|
|
30
30
|
import { formatFocusAreas, readFocusAreas } from '../lib/project-focus.js';
|
|
31
31
|
import { formatVerdict, scheduleVerdict } from '../lib/project-schedule.js';
|
|
32
32
|
import { buildLinearImportCandidates, validateImportOpts, } from '../lib/project-import.js';
|
|
33
|
-
import { buildProjectPrs } from '../lib/github/project-prs.js';
|
|
33
|
+
import { buildProjectPrs, mergeProjectPr, resolveTargetSlugs, MERGE_METHODS } from '../lib/github/project-prs.js';
|
|
34
|
+
import { ghExec } from '../lib/github/pr-mergeable.js';
|
|
34
35
|
/** Recursion guard: a peer answering a probe fan-out never re-fans-out itself. */
|
|
35
36
|
const PROJECTS_NO_FANOUT_ENV = 'AGENTS_PROJECTS_LOCAL';
|
|
36
37
|
/** Max peers named in the skipped note before the rest collapse to `+N`. */
|
|
@@ -820,9 +821,14 @@ export function registerProjectsCommands(program) {
|
|
|
820
821
|
await runProjectCard(name, opts, mode);
|
|
821
822
|
});
|
|
822
823
|
// ---- prs ----
|
|
824
|
+
// `list` is the default so `prs <name>` keeps its shape; a bare parent with
|
|
825
|
+
// no options of its own is what lets `prs merge` own --repo/--number/--json.
|
|
823
826
|
const prsCmd = projects
|
|
824
|
-
.command('prs
|
|
825
|
-
.description('
|
|
827
|
+
.command('prs')
|
|
828
|
+
.description('A project\'s open pull requests: list them (default), or merge one.');
|
|
829
|
+
const prsListCmd = prsCmd
|
|
830
|
+
.command('list <name>', { isDefault: true })
|
|
831
|
+
.description('Every OPEN pull request across a project\'s attached repos (drafts included, no author filter), scoped to this project\'s paths in a shared repo.')
|
|
826
832
|
.option('--json', 'Machine-readable output (the AGI Menu contract shape)')
|
|
827
833
|
.option('--repo <owner/repo>', 'Restrict to one of the project\'s attached repos')
|
|
828
834
|
.option('--number <n>', 'Lazy detail: enrich exactly this PR with checks + reviewDecision (requires --repo)')
|
|
@@ -849,7 +855,7 @@ export function registerProjectsCommands(program) {
|
|
|
849
855
|
}
|
|
850
856
|
let envelope;
|
|
851
857
|
try {
|
|
852
|
-
envelope = await buildProjectPrs(def, { repo: opts.repo, number }, undefined);
|
|
858
|
+
envelope = await buildProjectPrs(def, { repo: opts.repo, number }, undefined, listProjectDefs());
|
|
853
859
|
}
|
|
854
860
|
catch (e) {
|
|
855
861
|
// A restriction error (repo not attached) is a hard, up-front failure —
|
|
@@ -875,7 +881,8 @@ export function registerProjectsCommands(program) {
|
|
|
875
881
|
console.log(`${chalk.bold(r.slug)} ${chalk.dim(`${r.pullRequests.length} open`)}`);
|
|
876
882
|
for (const pr of r.pullRequests) {
|
|
877
883
|
const draft = pr.isDraft ? chalk.gray(' [draft]') : '';
|
|
878
|
-
|
|
884
|
+
const wide = pr.scope === 'repo-wide' ? chalk.gray(' [repo-wide]') : '';
|
|
885
|
+
console.log(` #${pr.number}${draft}${wide} ${pr.title} ${chalk.gray(`@${pr.author.login} · ${pr.headRefName}`)}`);
|
|
879
886
|
}
|
|
880
887
|
}
|
|
881
888
|
if (envelope.partial) {
|
|
@@ -886,11 +893,81 @@ export function registerProjectsCommands(program) {
|
|
|
886
893
|
if (envelope.partial)
|
|
887
894
|
process.exit(1);
|
|
888
895
|
});
|
|
896
|
+
const mergeCmd = prsCmd
|
|
897
|
+
.command('merge <name>')
|
|
898
|
+
.description('Merge one open PR of a project, pinned to the head SHA you reviewed.')
|
|
899
|
+
.requiredOption('--repo <owner/repo>', 'One of the project\'s attached repos')
|
|
900
|
+
.requiredOption('--number <n>', 'The PR number')
|
|
901
|
+
.requiredOption('--sha <head-sha>', 'The head SHA you reviewed; GitHub refuses if the branch moved since')
|
|
902
|
+
.option('--method <method>', `${MERGE_METHODS.join(' | ')} (default: the first the repo allows, in that order)`)
|
|
903
|
+
.option('--json', 'Machine-readable result')
|
|
904
|
+
.action(async (name, opts) => {
|
|
905
|
+
const def = loadProjectDef(name);
|
|
906
|
+
if (!def) {
|
|
907
|
+
console.error(chalk.red(`No project named "${name}". List them: agents projects list`));
|
|
908
|
+
process.exit(1);
|
|
909
|
+
}
|
|
910
|
+
const raw = opts.number.trim();
|
|
911
|
+
const number = /^\d+$/.test(raw) ? Number.parseInt(raw, 10) : NaN;
|
|
912
|
+
if (!Number.isSafeInteger(number) || number <= 0) {
|
|
913
|
+
console.error(chalk.red(`--number expects a positive integer, got "${opts.number}".`));
|
|
914
|
+
process.exit(1);
|
|
915
|
+
}
|
|
916
|
+
if (!/^[0-9a-f]{7,40}$/i.test(opts.sha.trim())) {
|
|
917
|
+
console.error(chalk.red(`--sha expects a commit SHA, got "${opts.sha}".`));
|
|
918
|
+
process.exit(1);
|
|
919
|
+
}
|
|
920
|
+
if (opts.method !== undefined && !MERGE_METHODS.includes(opts.method)) {
|
|
921
|
+
console.error(chalk.red(`--method expects one of ${MERGE_METHODS.join(', ')}, got "${opts.method}".`));
|
|
922
|
+
process.exit(1);
|
|
923
|
+
}
|
|
924
|
+
let repo;
|
|
925
|
+
try {
|
|
926
|
+
[repo] = await resolveTargetSlugs(def, opts.repo, ghExec);
|
|
927
|
+
}
|
|
928
|
+
catch (e) {
|
|
929
|
+
console.error(chalk.red(e instanceof Error ? e.message : String(e)));
|
|
930
|
+
process.exit(1);
|
|
931
|
+
}
|
|
932
|
+
const result = await mergeProjectPr(repo, number, opts.sha.trim(), opts.method);
|
|
933
|
+
if (opts.json) {
|
|
934
|
+
console.log(JSON.stringify(result, null, 2));
|
|
935
|
+
}
|
|
936
|
+
else if (result.merged) {
|
|
937
|
+
console.log(`${chalk.green('Merged')} ${repo}#${number} (${result.method}) ${chalk.gray(result.sha ?? '')}`);
|
|
938
|
+
}
|
|
939
|
+
else {
|
|
940
|
+
console.error(chalk.red(`Not merged: ${repo}#${number}: ${result.message}`));
|
|
941
|
+
}
|
|
942
|
+
if (!result.merged)
|
|
943
|
+
process.exit(1);
|
|
944
|
+
});
|
|
945
|
+
setHelpSections(mergeCmd, {
|
|
946
|
+
examples: `
|
|
947
|
+
agents projects prs rush --json --repo phnx-labs/agi-cli --number 3646 # read headSha + mergeableState
|
|
948
|
+
agents projects prs merge rush --repo phnx-labs/agi-cli --number 3646 --sha <headSha>
|
|
949
|
+
agents projects prs merge rush --repo phnx-labs/agi-cli --number 3646 --sha <headSha> --method squash --json
|
|
950
|
+
`,
|
|
951
|
+
notes: `
|
|
952
|
+
The merge is a single REST call pinned to --sha: if anything was pushed after
|
|
953
|
+
you read the PR, GitHub refuses and nothing merges. Branch protection, required
|
|
954
|
+
checks and reviews stay GitHub's to enforce; a refusal exits 1 with GitHub's
|
|
955
|
+
message (merged: false in --json).
|
|
956
|
+
`,
|
|
957
|
+
});
|
|
889
958
|
setHelpSections(prsCmd, {
|
|
890
959
|
examples: `
|
|
960
|
+
agents projects prs rush --json # every open PR across rush's repos (= prs list rush)
|
|
961
|
+
agents projects prs rush --json --repo phnx-labs/agi-cli --number 3646 # one PR, with checks + mergeability
|
|
962
|
+
agents projects prs merge rush --repo phnx-labs/agi-cli --number 3646 --sha <headSha>
|
|
963
|
+
`,
|
|
964
|
+
});
|
|
965
|
+
setHelpSections(prsListCmd, {
|
|
966
|
+
examples: `
|
|
891
967
|
agents projects prs rush --json # every open PR across rush's repos
|
|
892
968
|
agents projects prs rush --json --repo phnx-labs/agi-cli
|
|
893
|
-
agents projects prs rush --json --repo phnx-labs/agi-cli --number 3646 # one PR, with checks + review
|
|
969
|
+
agents projects prs rush --json --repo phnx-labs/agi-cli --number 3646 # one PR, with checks + review + mergeability
|
|
970
|
+
agents projects prs merge rush --repo phnx-labs/agi-cli --number 3646 --sha <headSha>
|
|
894
971
|
`,
|
|
895
972
|
notes: `
|
|
896
973
|
Repos come from the project definition's attached repos only (repo + repos[].slug);
|
|
@@ -898,7 +975,15 @@ export function registerProjectsCommands(program) {
|
|
|
898
975
|
|
|
899
976
|
The list is read from GitHub over REST and paginated in full, drafts included,
|
|
900
977
|
with no author filter. checks and reviewDecision are null in the list; --number
|
|
901
|
-
enriches exactly that one PR against its live head SHA
|
|
978
|
+
enriches exactly that one PR against its live head SHA, with mergeable and
|
|
979
|
+
mergeableState.
|
|
980
|
+
|
|
981
|
+
A repository attached to several projects (a monorepo) is scoped by the paths
|
|
982
|
+
each project claims, the same rule that attributes a session's cwd: a
|
|
983
|
+
defaultPath narrowed under root, or a repos[] subpath. A PR is listed when it
|
|
984
|
+
touches this project's paths (scope: project) or no sharing project's paths
|
|
985
|
+
(scope: repo-wide). Changed files are cached per head SHA, so only a new push
|
|
986
|
+
costs a read.
|
|
902
987
|
|
|
903
988
|
A repository whose fetch fails is reported with an error and marks the result
|
|
904
989
|
partial (JSON reports errors per repository; text mode exits non-zero).
|
|
@@ -128,6 +128,8 @@ export declare function runAdd(agent: AgentId, name: string | undefined, opts: A
|
|
|
128
128
|
* Drive one `agents accounts login <harness>#<name>`: re-auth into the SAME
|
|
129
129
|
* slot (re-running add's steps 4–8), re-minting the worker credential and
|
|
130
130
|
* re-syncing. On a per-device harness (worker `none`) any box may run it —
|
|
131
|
-
* that IS how such a box logs in.
|
|
131
|
+
* that IS how such a box logs in. `--per-device` extends that to a dual-path
|
|
132
|
+
* harness (codex, grok): this box signs the account's subscription seat in for
|
|
133
|
+
* itself through the device-code flow instead of using a portable key.
|
|
132
134
|
*/
|
|
133
135
|
export declare function runLogin(agent: AgentId, name: string, opts: AddOptions, runners?: AddRunners): Promise<AddResult>;
|
package/dist/lib/accounts/add.js
CHANGED
|
@@ -73,6 +73,12 @@ export function workerApiKeyEnv(agent) {
|
|
|
73
73
|
function hasPortableWorkerKind(agent) {
|
|
74
74
|
return harnessWorkerKinds(agent).some((k) => k === 'setup-token' || k.startsWith('api-key:'));
|
|
75
75
|
}
|
|
76
|
+
function hasPerDeviceWorkerKind(agent) {
|
|
77
|
+
return harnessWorkerKinds(agent).some((k) => k.startsWith('per-device'));
|
|
78
|
+
}
|
|
79
|
+
function perDeviceRefusal(agent) {
|
|
80
|
+
return `--per-device is only valid for a harness with a per-device worker path; ${agent} is provisioned from ${workerProvisioningHint(agent)}.`;
|
|
81
|
+
}
|
|
76
82
|
/** Whether `accounts add` can drive this harness (nameable, version-scoped, a real login command). */
|
|
77
83
|
export function addSupported(agent) {
|
|
78
84
|
const cap = nativeAccountCapability(agent);
|
|
@@ -232,9 +238,8 @@ async function mintWorkerCredential(agent, account, home, opts, runners, warning
|
|
|
232
238
|
return { outcome: 'skipped', provisioning: 'per-device' };
|
|
233
239
|
}
|
|
234
240
|
if (opts.perDevice) {
|
|
235
|
-
if (!
|
|
236
|
-
throw new Error(
|
|
237
|
-
}
|
|
241
|
+
if (!hasPerDeviceWorkerKind(agent))
|
|
242
|
+
throw new Error(perDeviceRefusal(agent));
|
|
238
243
|
return { outcome: 'per-device', provisioning: 'per-device' };
|
|
239
244
|
}
|
|
240
245
|
const { seedReservedStoreKey, seedReservedAuthToken, workerCredentialStoreKey, } = await import('../auth-mint.js');
|
|
@@ -260,7 +265,7 @@ async function mintWorkerCredential(agent, account, home, opts, runners, warning
|
|
|
260
265
|
let value = opts.apiKey?.trim();
|
|
261
266
|
if (!value) {
|
|
262
267
|
if (!runners.promptApiKey) {
|
|
263
|
-
throw new Error(`${agent}'s worker credential is an ${env} API key. Pass --api-key <key> (or --no-worker-token to skip, --per-device for a
|
|
268
|
+
throw new Error(`${agent}'s worker credential is an ${env} API key. Pass --api-key <key> (or --no-worker-token to skip, or --per-device for a subscription seat when the harness has one).`);
|
|
264
269
|
}
|
|
265
270
|
const entered = await runners.promptApiKey(agent, env);
|
|
266
271
|
if (entered === null) {
|
|
@@ -290,8 +295,8 @@ export async function runAdd(agent, name, opts, runners) {
|
|
|
290
295
|
// before the lock, slot, install, or browser — no side effects on a worker.
|
|
291
296
|
assertAddAllowedOnThisDevice(agent, name);
|
|
292
297
|
const kinds = harnessWorkerKinds(agent);
|
|
293
|
-
if (opts.perDevice && !
|
|
294
|
-
throw new Error(
|
|
298
|
+
if (opts.perDevice && !hasPerDeviceWorkerKind(agent)) {
|
|
299
|
+
throw new Error(perDeviceRefusal(agent));
|
|
295
300
|
}
|
|
296
301
|
// The ambient-token refusal is checked up front (not at the mint step) so the
|
|
297
302
|
// user doesn't complete a browser login only to be refused at the end.
|
|
@@ -410,13 +415,18 @@ async function _runAddLocked(agent, name, opts, lock, runners) {
|
|
|
410
415
|
* Drive one `agents accounts login <harness>#<name>`: re-auth into the SAME
|
|
411
416
|
* slot (re-running add's steps 4–8), re-minting the worker credential and
|
|
412
417
|
* re-syncing. On a per-device harness (worker `none`) any box may run it —
|
|
413
|
-
* that IS how such a box logs in.
|
|
418
|
+
* that IS how such a box logs in. `--per-device` extends that to a dual-path
|
|
419
|
+
* harness (codex, grok): this box signs the account's subscription seat in for
|
|
420
|
+
* itself through the device-code flow instead of using a portable key.
|
|
414
421
|
*/
|
|
415
422
|
export async function runLogin(agent, name, opts, runners) {
|
|
416
423
|
// A harness with a portable worker credential authenticates workers from that
|
|
417
424
|
// credential, so the interactive re-login stays headed-only. A per-device
|
|
418
|
-
// harness (kimi)
|
|
419
|
-
|
|
425
|
+
// harness (kimi), or a per-device login of a dual-path harness, is logged in
|
|
426
|
+
// per box BY DESIGN — any role may run it.
|
|
427
|
+
if (opts.perDevice && !hasPerDeviceWorkerKind(agent))
|
|
428
|
+
throw new Error(perDeviceRefusal(agent));
|
|
429
|
+
if (hasPortableWorkerKind(agent) && !opts.perDevice)
|
|
420
430
|
assertAddAllowedOnThisDevice(agent, name);
|
|
421
431
|
if (!opts.noWorkerToken && harnessWorkerKinds(agent).includes('setup-token')) {
|
|
422
432
|
const refusal = ambientTokenRefusal(agent, opts.env ?? process.env);
|
|
@@ -437,6 +447,7 @@ export async function runLogin(agent, name, opts, runners) {
|
|
|
437
447
|
async function _runLoginLocked(agent, name, opts, lock, runners) {
|
|
438
448
|
const run = runners ?? await defaultAddRunners();
|
|
439
449
|
lock.assertHeld();
|
|
450
|
+
const perDevice = !!opts.perDevice || !hasPortableWorkerKind(agent);
|
|
440
451
|
const meta = readMeta();
|
|
441
452
|
const account = findAddAccount(agent, name, meta);
|
|
442
453
|
if (!account) {
|
|
@@ -452,7 +463,7 @@ async function _runLoginLocked(agent, name, opts, lock, runners) {
|
|
|
452
463
|
const slot = ensureSlot(agent, account.id);
|
|
453
464
|
recordSlot(account.id, {
|
|
454
465
|
...slot,
|
|
455
|
-
authMode:
|
|
466
|
+
authMode: perDevice ? 'per-device' : 'native',
|
|
456
467
|
});
|
|
457
468
|
}
|
|
458
469
|
// Pre-launch guard: refuse to launch a login into a slot signed in as a
|
|
@@ -481,8 +492,8 @@ async function _runLoginLocked(agent, name, opts, lock, runners) {
|
|
|
481
492
|
const warnings = [];
|
|
482
493
|
let outcome = 'per-device';
|
|
483
494
|
let ref;
|
|
484
|
-
let provisioning =
|
|
485
|
-
if (
|
|
495
|
+
let provisioning = perDevice ? 'per-device' : (account.provisioning ?? 'portable');
|
|
496
|
+
if (!perDevice) {
|
|
486
497
|
if (opts.apiKey && workerApiKeyEnv(agent)) {
|
|
487
498
|
// Explicit rotation of the stored API key.
|
|
488
499
|
const minted = await mintWorkerCredential(agent, account, home, opts, run, warnings);
|
|
@@ -520,7 +531,7 @@ async function _runLoginLocked(agent, name, opts, lock, runners) {
|
|
|
520
531
|
recordSlot(account.id, {
|
|
521
532
|
accountId: account.id,
|
|
522
533
|
slotDir: home,
|
|
523
|
-
authMode:
|
|
534
|
+
authMode: perDevice ? 'per-device' : 'native',
|
|
524
535
|
verdict: 'live',
|
|
525
536
|
checkedAt: new Date().toISOString(),
|
|
526
537
|
});
|
|
@@ -269,6 +269,7 @@ export const AGENTS = {
|
|
|
269
269
|
variableSyntax: '$ARGUMENTS',
|
|
270
270
|
supportsHooks: true,
|
|
271
271
|
nativePluginSkills: true,
|
|
272
|
+
ownedSkillDirs: ['synced'],
|
|
272
273
|
// Claude Code grew a native `claude --cloud "<prompt>"` (Anthropic-managed
|
|
273
274
|
// infra, claude.ai/code; requires claude.ai subscription auth). Routing
|
|
274
275
|
// still goes to Rush Cloud deliberately — it keeps cloud tasks in one
|
|
@@ -103,5 +103,11 @@ export declare function isClaudeWorkerHomeSeeded(home: string): boolean;
|
|
|
103
103
|
* at it, so nothing else can complete Claude Code's first-run onboarding (theme
|
|
104
104
|
* picker, "Let's get started"); without the flag every slot launch re-onboarded.
|
|
105
105
|
* Everything else in the document is preserved.
|
|
106
|
+
*
|
|
107
|
+
* Without an email only the onboarding flag is seeded: a provider (setup-token)
|
|
108
|
+
* launch runs in the shared version home, whose identity belongs to whatever
|
|
109
|
+
* native login lives there, so it must not be rewritten. A document already
|
|
110
|
+
* carrying the requested state is left untouched, so a launch never rewrites a
|
|
111
|
+
* config Claude Code is using.
|
|
106
112
|
*/
|
|
107
|
-
export declare function seedClaudeWorkerHomeIdentity(versionHome: string, email
|
|
113
|
+
export declare function seedClaudeWorkerHomeIdentity(versionHome: string, email?: string): void;
|
|
@@ -393,15 +393,34 @@ export function isClaudeWorkerHomeSeeded(home) {
|
|
|
393
393
|
* at it, so nothing else can complete Claude Code's first-run onboarding (theme
|
|
394
394
|
* picker, "Let's get started"); without the flag every slot launch re-onboarded.
|
|
395
395
|
* Everything else in the document is preserved.
|
|
396
|
+
*
|
|
397
|
+
* Without an email only the onboarding flag is seeded: a provider (setup-token)
|
|
398
|
+
* launch runs in the shared version home, whose identity belongs to whatever
|
|
399
|
+
* native login lives there, so it must not be rewritten. A document already
|
|
400
|
+
* carrying the requested state is left untouched, so a launch never rewrites a
|
|
401
|
+
* config Claude Code is using.
|
|
396
402
|
*/
|
|
397
403
|
export function seedClaudeWorkerHomeIdentity(versionHome, email) {
|
|
398
|
-
const trimmed = email
|
|
399
|
-
|
|
400
|
-
|
|
404
|
+
const trimmed = email?.trim() || undefined;
|
|
405
|
+
// A version home usually links `.claude/.claude.json -> ../.claude.json`. The
|
|
406
|
+
// temp-write + rename below would replace that link with a regular file and
|
|
407
|
+
// split one config into two, so write through a link to the file it names.
|
|
408
|
+
const targets = new Set();
|
|
401
409
|
for (const p of [
|
|
402
410
|
path.join(versionHome, '.claude', '.claude.json'),
|
|
403
411
|
path.join(versionHome, '.claude.json'),
|
|
404
412
|
]) {
|
|
413
|
+
let target = p;
|
|
414
|
+
try {
|
|
415
|
+
if (fs.lstatSync(p).isSymbolicLink())
|
|
416
|
+
target = path.resolve(path.dirname(p), fs.readlinkSync(p));
|
|
417
|
+
}
|
|
418
|
+
catch {
|
|
419
|
+
// Missing: written fresh below.
|
|
420
|
+
}
|
|
421
|
+
targets.add(target);
|
|
422
|
+
}
|
|
423
|
+
for (const p of targets) {
|
|
405
424
|
let doc = {};
|
|
406
425
|
try {
|
|
407
426
|
doc = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
@@ -416,7 +435,10 @@ export function seedClaudeWorkerHomeIdentity(versionHome, email) {
|
|
|
416
435
|
const existing = (doc.oauthAccount && typeof doc.oauthAccount === 'object'
|
|
417
436
|
? doc.oauthAccount
|
|
418
437
|
: {});
|
|
419
|
-
doc.
|
|
438
|
+
if (doc.hasCompletedOnboarding === true && (!trimmed || existing.emailAddress === trimmed))
|
|
439
|
+
continue;
|
|
440
|
+
if (trimmed)
|
|
441
|
+
doc.oauthAccount = { ...existing, emailAddress: trimmed };
|
|
420
442
|
doc.hasCompletedOnboarding = true;
|
|
421
443
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
422
444
|
// Temp-write + rename: a reader mid-write never sees a truncated doc.
|
package/dist/lib/exec.js
CHANGED
|
@@ -33,12 +33,12 @@ import { recordRunName } from './session/run-names.js';
|
|
|
33
33
|
import { mailboxDir, isValidMailboxId } from './mailbox.js';
|
|
34
34
|
import { composeWin32CommandLine } from './platform/index.js';
|
|
35
35
|
import { isTmuxInstalled } from './tmux/binary.js';
|
|
36
|
-
import { isTmuxEnabled, selfConfiguredDeviceRole } from './device-config.js';
|
|
36
|
+
import { isHeadedDeviceRole, isTmuxEnabled, selfConfiguredDeviceRole } from './device-config.js';
|
|
37
37
|
import { machineId } from './machine-id.js';
|
|
38
38
|
import { shellQuote } from './ssh-exec.js';
|
|
39
39
|
import { codexEditWritableRoots, codexPolicyArgs } from './codex-policy.js';
|
|
40
40
|
import { probeUnprivilegedUserns } from './linux-userns.js';
|
|
41
|
-
import { resolveClaudeSetupToken } from './claude-account-token.js';
|
|
41
|
+
import { resolveClaudeSetupToken, seedClaudeWorkerHomeIdentity } from './claude-account-token.js';
|
|
42
42
|
import { applyAddDirs } from './add-dir.js';
|
|
43
43
|
import { applyActiveRulesPresetAtRun } from './rules/run-sync.js';
|
|
44
44
|
import { applySystemResourcesAtRun } from './system-run-sync.js';
|
|
@@ -1932,6 +1932,14 @@ async function spawnAgentLeased(options) {
|
|
|
1932
1932
|
process.stderr.write(`\x1b[31m${loginTrap}\x1b[0m\n`);
|
|
1933
1933
|
return { exitCode: 1, stdout: '', stderr: loginTrap };
|
|
1934
1934
|
}
|
|
1935
|
+
// A credentialed run on a worker never needs Claude Code's first-run
|
|
1936
|
+
// onboarding, and in a provider (setup-token) launch's shared version home
|
|
1937
|
+
// nothing else completes it: the tab opened on the theme picker instead of
|
|
1938
|
+
// the agent (yosemite-m1, 2026-09-28). A headed device keeps its own
|
|
1939
|
+
// onboarding; the person at it answers it.
|
|
1940
|
+
if (hasWorkerCredential && versionHome && !isHeadedDeviceRole(selfConfiguredDeviceRole())) {
|
|
1941
|
+
seedClaudeWorkerHomeIdentity(versionHome);
|
|
1942
|
+
}
|
|
1935
1943
|
}
|
|
1936
1944
|
// Budget live kill-switch (issue #346). For headless runs we incrementally
|
|
1937
1945
|
// parse stream-json usage off stdout, accumulate cost, and kill the child the
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* This helper:
|
|
9
9
|
* 1. Collects GitHub slugs from `~/.agents/projects/*.yaml` (`repo` / `repos[].slug`).
|
|
10
|
-
* 2. Canonicalizes each slug (
|
|
10
|
+
* 2. Canonicalizes each slug (REST `repos/{slug}` — `phnx-labs/agents-cli` lists
|
|
11
11
|
* nothing; the live name is `phnx-labs/agi-cli`).
|
|
12
12
|
* 3. Runs `gh pr list --repo <slug> --author @me` (cwd-independent).
|
|
13
13
|
* 4. Keeps CI-green PRs with a merge-guard verdict (formal APPROVED review
|
|
@@ -27,7 +27,9 @@ export declare function projectRepoSlugs(defs: readonly ProjectDef[]): string[];
|
|
|
27
27
|
/**
|
|
28
28
|
* Resolve a slug to GitHub's current `nameWithOwner`. A renamed repo
|
|
29
29
|
* (`phnx-labs/agents-cli` → `phnx-labs/agi-cli`) lists zero PRs under the old
|
|
30
|
-
* name;
|
|
30
|
+
* name; the REST repo read follows the rename redirect and returns the live one.
|
|
31
|
+
* REST + a 24h gh cache: a rename is rare, and every `projects prs` refresh asks.
|
|
32
|
+
* On error, keep the input slug.
|
|
31
33
|
*/
|
|
32
34
|
export declare function canonicalizeRepo(slug: string, gh: GhExec): Promise<string>;
|
|
33
35
|
interface ListedPr {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* This helper:
|
|
9
9
|
* 1. Collects GitHub slugs from `~/.agents/projects/*.yaml` (`repo` / `repos[].slug`).
|
|
10
|
-
* 2. Canonicalizes each slug (
|
|
10
|
+
* 2. Canonicalizes each slug (REST `repos/{slug}` — `phnx-labs/agents-cli` lists
|
|
11
11
|
* nothing; the live name is `phnx-labs/agi-cli`).
|
|
12
12
|
* 3. Runs `gh pr list --repo <slug> --author @me` (cwd-independent).
|
|
13
13
|
* 4. Keeps CI-green PRs with a merge-guard verdict (formal APPROVED review
|
|
@@ -65,12 +65,14 @@ export function projectRepoSlugs(defs) {
|
|
|
65
65
|
/**
|
|
66
66
|
* Resolve a slug to GitHub's current `nameWithOwner`. A renamed repo
|
|
67
67
|
* (`phnx-labs/agents-cli` → `phnx-labs/agi-cli`) lists zero PRs under the old
|
|
68
|
-
* name;
|
|
68
|
+
* name; the REST repo read follows the rename redirect and returns the live one.
|
|
69
|
+
* REST + a 24h gh cache: a rename is rare, and every `projects prs` refresh asks.
|
|
70
|
+
* On error, keep the input slug.
|
|
69
71
|
*/
|
|
70
72
|
export async function canonicalizeRepo(slug, gh) {
|
|
71
73
|
try {
|
|
72
74
|
const out = (await gh([
|
|
73
|
-
'
|
|
75
|
+
'api', `repos/${slug}`, '--cache', '24h', '--jq', '.full_name',
|
|
74
76
|
])).trim();
|
|
75
77
|
return out || slug;
|
|
76
78
|
}
|