@phnx-labs/agents-cli 1.22.71 → 1.22.73
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 +45 -0
- package/README.md +2 -0
- package/dist/cli/command-registry.d.ts +1 -1
- package/dist/cli/command-registry.js +2 -1
- package/dist/commands/packages-materialize.d.ts +17 -0
- package/dist/commands/packages-materialize.js +93 -0
- package/dist/commands/packages.d.ts +6 -4
- package/dist/commands/packages.js +8 -4
- package/dist/commands/repo.js +8 -8
- package/dist/commands/sessions-picker.js +38 -7
- package/dist/commands/sessions.js +6 -5
- package/dist/lib/actor.d.ts +36 -4
- package/dist/lib/actor.js +73 -9
- package/dist/lib/agent-spec/index.d.ts +4 -0
- package/dist/lib/agent-spec/index.js +5 -0
- package/dist/lib/agent-spec/materialize.d.ts +13 -0
- package/dist/lib/agent-spec/materialize.js +414 -0
- package/dist/lib/agent-spec/package-resolve.d.ts +12 -0
- package/dist/lib/agent-spec/package-resolve.js +274 -0
- package/dist/lib/agent-spec/package-schema.d.ts +5 -0
- package/dist/lib/agent-spec/package-schema.js +147 -0
- package/dist/lib/agent-spec/package-types.d.ts +121 -0
- package/dist/lib/agent-spec/package-types.js +11 -0
- package/dist/lib/daemon/usage-sync-service.d.ts +12 -5
- package/dist/lib/daemon/usage-sync-service.js +27 -5
- package/dist/lib/exec.js +3 -0
- package/dist/lib/fleet-shared-state.d.ts +30 -0
- package/dist/lib/fleet-shared-state.js +5 -0
- package/dist/lib/hooks/install.d.ts +31 -1
- package/dist/lib/hooks/install.js +44 -2
- package/dist/lib/mcp.d.ts +14 -2
- package/dist/lib/mcp.js +12 -2
- package/dist/lib/packages/output-home.d.ts +39 -0
- package/dist/lib/packages/output-home.js +203 -0
- package/dist/lib/paths.d.ts +9 -0
- package/dist/lib/paths.js +26 -0
- package/dist/lib/project-resources.js +14 -5
- package/dist/lib/session/active.d.ts +12 -0
- package/dist/lib/session/active.js +5 -1
- package/dist/lib/session/actor-sidecar.d.ts +7 -0
- package/dist/lib/session/actor-sidecar.js +2 -0
- package/dist/lib/session/db.d.ts +60 -1
- package/dist/lib/session/db.js +191 -6
- package/dist/lib/session/mirror.d.ts +67 -0
- package/dist/lib/session/mirror.js +158 -0
- package/dist/lib/session/types.d.ts +18 -0
- package/dist/lib/spinner.d.ts +39 -0
- package/dist/lib/spinner.js +41 -0
- package/dist/lib/startup/command-registry.js +1 -1
- package/dist/lib/types.d.ts +6 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.22.73
|
|
4
|
+
|
|
5
|
+
- **Ctrl-C works again while `agents sessions` is reaching other machines (PHNX-3791).** The static session list, `Searching the fleet…`, and the cloud session/fetch paths wrap their long SSH/network waits in `ora(...)`. ora defaults to `discardStdin: true`, which on a TTY flips stdin into raw mode for the spinner's lifetime (via `stdin-discarder`) — and raw mode disables the terminal's own ISIG, so a Ctrl-C keystroke no longer raises SIGINT. ora's compensating "re-emit SIGINT when I read a 0x03 byte" only fires when stdin is in *flowing* mode, but a fresh `process.stdin` has `flowing === null`, `isPaused()` returns false, the discarder skips its `resume()`, and `prependListener('data')` doesn't auto-resume — so the byte is never read and SIGINT is never raised. The global handler in `index.ts` (`process.exit(130)`) was correct; it simply never fired. The user-visible result: a sweep that stalls on offline/unreachable boxes (each burning a ~12s SSH `ConnectTimeout`) could not be aborted — measured, `agents sessions --flat` had to be SIGKILLed. New `lib/spinner.ts#interruptibleSpinner` is a drop-in for `ora(text)` built with `discardStdin: false`: the terminal stays in cooked mode, Ctrl-C raises SIGINT normally (and the terminal SIGINTs the whole foreground group, reaping the outstanding `ssh` children), and the process exits promptly — only cosmetic stray-keystroke discarding during the spin is lost. The five cross-machine/network spinners in `sessions.ts` route through it; short local spinners are unchanged. Source: `cli/src/lib/spinner.ts`, `cli/src/lib/spinner.test.ts`, `cli/src/commands/sessions.ts`.
|
|
6
|
+
|
|
7
|
+
- **Remote-host session previews render inline, no per-row SSH (PHNX-3792).** On
|
|
8
|
+
the interactive device, a session that originated on another box used to show
|
|
9
|
+
as a bare `[host/<peer>]` row with no topic, and its preview pane fetched the
|
|
10
|
+
peer's digest live over SSH — slow, and blank when the peer was asleep. Each
|
|
11
|
+
box now mirrors its own sessions' lightweight preview/metadata (topic, first
|
|
12
|
+
user message, last activity, agent+version, cwd, ticket, PR) into its
|
|
13
|
+
conflict-free `devices/<device>/daemon-state.json`, which the daemon's existing
|
|
14
|
+
bounded Git transport already delivers fleet-wide. The picker, `agents sessions`
|
|
15
|
+
list, and `focus` read the local mirror, so a peer session shows its real
|
|
16
|
+
topic/preview inline — even while the peer is offline (last-synced). The live
|
|
17
|
+
SSH digest fetch remains the fallback for a never-synced session, and `space`
|
|
18
|
+
still reads the full transcript live. No transcript leaves the worker; the
|
|
19
|
+
mirror is bounded and pruned by age. Source: `src/lib/session/mirror.ts`,
|
|
20
|
+
`src/lib/fleet-shared-state.ts`, `src/lib/session/db.ts`,
|
|
21
|
+
`src/lib/daemon/usage-sync-service.ts`, `src/commands/sessions-picker.ts`.
|
|
22
|
+
|
|
23
|
+
- **Ctrl-C works during `agents repo` git operations too (PHNX-3793).** Follow-up to PHNX-3791. `cli/src/commands/repo.ts` wrapped its seven git network ops — clone, git-backing, pull, push, adopt, sync — in bare `ora(...)` spinners, whose default `discardStdin: true` raw-modes the TTY and swallows Ctrl-C for the whole operation (same trap PHNX-3791 fixed for the session sweep). A `git clone`/`pull` stalled on a slow or dead remote could hang longer than a fleet sweep with no way to abort. All seven now use `lib/spinner.ts#interruptibleSpinner` (`discardStdin: false`), so Ctrl-C raises SIGINT and the process exits promptly. Source: `cli/src/commands/repo.ts`.
|
|
24
|
+
|
|
25
|
+
- **`agents sessions --json` now carries the initiator's Phoenix ID (PHNX-3798).**
|
|
26
|
+
The actor resolver already mapped a run's tailnet identity to a `phoenixId`
|
|
27
|
+
(the stable internal work identity) and rode it through the exec env, but it was
|
|
28
|
+
never persisted per session, so a durable listing couldn't attribute a session
|
|
29
|
+
to a person's work identity. The `phoenixId` now flows the same path the
|
|
30
|
+
`actor`/`initiatedBy` fields already take — stamped into the durable actor
|
|
31
|
+
sidecar at launch, joined into a new write-once `phoenix_id` column via a v47
|
|
32
|
+
schema migration, and emitted by the sessions feed. Best-effort throughout: a
|
|
33
|
+
session with no mapped Phoenix ID (the common case) simply omits the field
|
|
34
|
+
rather than emitting a null/empty sentinel. This is the persistence half of the
|
|
35
|
+
ownership work whose resolver landed in PHNX-3798's first PR. Source:
|
|
36
|
+
`src/lib/session/actor-sidecar.ts`, `src/lib/session/db.ts`,
|
|
37
|
+
`src/lib/session/types.ts`, `src/lib/exec.ts`.
|
|
38
|
+
|
|
39
|
+
- **`agents packages materialize` materializes a schema-v3 `agent.yaml` package into an ephemeral Claude, Codex, or OpenCode home (PHNX-3838).** The noun-first surface takes `--harness`, `--harness-version` (root `--version` is the CLI version flag), and `--output-home`. It is a thin front door over the canonical native-home materializer (`agent-spec/materialize.ts`): it resolves the package once and projects every declared resource (instructions, skills, subagents, mcp, hooks) into the output home, then prints the materialization receipt (agent ref + digest, harness, per-resource targets, warnings); `--json` is the Factory/Prix Cloud handoff and is byte-identical to the `materialization-receipt.json` written into the home. It writes only under `--output-home`: it never mutates the live user home, never copies secrets, and never execs a harness. A non-portable harness, a non-exact `--harness-version`, a missing/invalid package, and an output-path escape (including the live `~/.claude`/`~/.codex`/`~/.opencode` homes) all fail loud. Source: `cli/src/commands/packages-materialize.ts`, `cli/src/lib/packages/output-home.ts`, `cli/src/lib/agent-spec/materialize.ts`.
|
|
40
|
+
- **`agents packages materialize` is hardened against four path-traversal / symlink escapes (PHNX-3838).** (1) `materializeAgentPackage` now realpath-verifies every per-resource write/delete target against the canonical output home before touching disk, and `--output-home` additionally refuses the live home ROOT and canonicalizes symlinked ancestors. So neither `--output-home "$HOME"` (the materializer appends `.claude`/`.codex`/`.opencode`), a symlink aliasing `$HOME`, nor a symlink planted at the harness-config-dir join point inside the output home can write into the live harness home — and a direct (Factory / Prix Cloud) caller that skips the CLI front door is protected too. (2) The stale-prune step strictly validates the unsigned `materialization-receipt.json` schema and realpath-checks every `resources[].target` as a contained path at deletion time, so a planted receipt can no longer make it `rm -rf` a `../victim`, an absolute path, or a path under a symlinked ancestor outside the output home. (3) A package `hook.name` must be a safe single path segment, re-checked for containment before the script is copied and `chmod +x`'d. (4) Package resources reject symlinked file/directory/hook sources and realpath-check every source under the package's real root, so `instructions.md -> /etc/passwd` or a skill dir symlinked outside the package can no longer be read or copied. Source: `cli/src/lib/packages/output-home.ts`, `cli/src/lib/agent-spec/materialize.ts`, `cli/src/lib/agent-spec/package-resolve.ts`, `cli/src/lib/paths.ts`.
|
|
41
|
+
- **`agents packages materialize` closes two more output-home boundary gaps (PHNX-3838).** (1) When any protected `~/.claude`/`~/.codex`/`~/.opencode` home is a DANGLING symlink or dangling chain (following relative links and chains), `resolveOutputHome` now fails closed and refuses EVERY output home until the link is repaired — because the materializer's first `mkdir -p` would otherwise follow the link and re-create the operator's live harness home at the materialized tree, and the absent target it points at has no canonical spelling to compare a specific candidate against. (2) Materializing a package that carries hooks no longer runs the normal orphan-shim sweep against the ONE process-global shim dir (`~/.agents/.cache/shims/hooks/`): that sweep deletes every shim absent from the manifest it is handed, so with a package's hooks-only manifest it would wipe the operator's unrelated hook shims. `registerHooksToSettings` gained an isolated `skipGlobalShimSweep` option that the materializer sets; normal install/sync keeps the sweep. Source: `cli/src/lib/packages/output-home.ts`, `cli/src/lib/hooks/install.ts`, `cli/src/lib/agent-spec/materialize.ts`.
|
|
42
|
+
- **`agents packages materialize` fails closed on a dangling protected home instead of approximating filesystem collation (PHNX-3838).** An earlier iteration compared the live-home identity case- and Unicode-normalization-insensitively (NFC + `toLowerCase`) on macOS/Windows to catch a spelling-equivalent alias of a DANGLING `~/.claude` (e.g. an absent `.../Real-Claude` vs `--output-home .../real-claude`). An independent macOS review proved that approximation wrong: APFS treats e.g. U+017F ſ and ASCII `s` as identical while JS `toLowerCase` does not, and an OS-derived case-sensitivity flag is wrong for per-volume semantics. The guard no longer tries to reproduce a volume's collation from the path text — when a protected home dangles it refuses every output home (fail closed, above), and an EXISTING protected home is compared with exact `realpath` identity, which needs no approximation. The `pathIdentityKey`/`pathIsWithin`/`FS_CASE_INSENSITIVE` helpers added for the approximation are removed. Source: `cli/src/lib/packages/output-home.ts`, `cli/src/lib/paths.ts`.
|
|
43
|
+
|
|
44
|
+
## 1.22.72
|
|
45
|
+
|
|
46
|
+
- **The self-managed `.gitignore` block now ignores the `.agents-managed.json` manifest too, so a synced harness dir is actually clean in `git status` (PHNX-3717).** The project-resource sync's gitignore reconciliation (shipped in 1.22.71) listed the synced commands/skills but not the manifest marker file the sync also writes into each harness dir — so `.factory/` (etc.) still showed as untracked on the strength of that one unignored file, defeating the point. `reconcileProjectGitignore` now feeds `.agents-managed.json` through the same anchoring + escape guard, and the block persists (shrunk to just the manifest) while that file exists rather than fully pruning. Covered by a new test that asserts a real `git status --porcelain` no longer reports the generated dir. Source: `cli/src/lib/project-resources.ts`, `cli/src/lib/project-resources.test.ts`.
|
|
47
|
+
|
|
3
48
|
## 1.22.71
|
|
4
49
|
|
|
5
50
|
- **The daemon now keeps itself current instead of running stale code for days (PHNX-3695).** The long-running daemon (`agents __daemon-run`) deliberately opted out of the interactive CLI's auto-update (`AGENTS_CLI_DISABLE_AUTO_UPDATE=1` forced on for `__daemon-run`), so new agents-cli code never reached a running daemon until a human ran `agents daemon restart` — R5 ("the installed CLI auto-updates") silently did not hold for the one process that runs unattended the longest. A new supervised `self-update` service checks npm roughly every 75 minutes, installs and byte-verifies a newer version with the same primitives `agents upgrade` already uses (never a bare `npm install -g`), best-effort pulls the `.system` companion repo and reconciles with `agents sync --local`, then exits so the OS supervisor (launchd `KeepAlive` / systemd `Restart=always`) relaunches it onto the new code — clients reconnect on their own, and the scheduler's atomic `(routine, scheduledFor)` claim dedupes any routine mid-fire across the restart. It fails closed on every step: an install or verify failure leaves the running daemon untouched and retries next tick, and it no-ops on a dev build or a shadowed install. A version-skewed browser-IPC client no longer just prints "run agents daemon restart" — `reconcileDaemonVersion` now asks the daemon to run this same fail-closed path on demand (`request-self-update`), sharing one in-flight attempt with the periodic tick so concurrent requests can't race two installs. That on-demand trigger is DECOUPLED from the install: because every version-skewed `agents browser <verb>` routes through it, the daemon kicks the self-update off in the background and answers "triggered" immediately rather than making the browser verb wait out the whole check→download→install→verify (tens of seconds, worst case ~15 min) — the daemon installs, verifies, and exits on its own, and the browser reconnects. Source: `cli/src/lib/daemon/self-update-service.ts`, `cli/src/lib/browser/ipc.ts`.
|
package/README.md
CHANGED
|
@@ -154,6 +154,8 @@ agents mcp list
|
|
|
154
154
|
|
|
155
155
|
Skills, slash commands, rules, hooks, and permissions work the same way -- install once in `~/.agents/`, synced to every agent's native format automatically.
|
|
156
156
|
|
|
157
|
+
A schema-v3 `agent.yaml` package materializes into an ephemeral Claude, Codex, or OpenCode home for Factory / Prix Cloud workers (`agents packages materialize ./reviewer --harness codex --harness-version 0.42.0 --output-home "$OUTPUT_HOME" --json`) -- it never writes the live user home.
|
|
158
|
+
|
|
157
159
|
```bash
|
|
158
160
|
agents skills add gh:yourteam/python-expert # Knowledge pack -> all agents
|
|
159
161
|
agents commands add gh:yourteam/commands # Slash commands -> all agents
|
|
@@ -105,7 +105,7 @@ export declare const LAZY_COMMAND_NAMES: ReadonlySet<string>;
|
|
|
105
105
|
*
|
|
106
106
|
* Most names map to a single loader. The exceptions encode real coupling on main:
|
|
107
107
|
* - `add`/`use`/`list`/`remove`/`rm`/`purge` all come from the versions module.
|
|
108
|
-
* - `registry`/`search`/`install` all come from the packages module.
|
|
108
|
+
* - `registry`/`search`/`install`/`packages` all come from the packages module.
|
|
109
109
|
* - `trash` and `restore` are separate registrars in the trash module.
|
|
110
110
|
* - `prune` needs versions FIRST (it creates `prune <specs...>`) then prune.js
|
|
111
111
|
* (which finds that command and attaches the `cleanup` subcommand).
|
|
@@ -115,7 +115,7 @@ export const LAZY_COMMAND_NAMES = new Set([
|
|
|
115
115
|
*
|
|
116
116
|
* Most names map to a single loader. The exceptions encode real coupling on main:
|
|
117
117
|
* - `add`/`use`/`list`/`remove`/`rm`/`purge` all come from the versions module.
|
|
118
|
-
* - `registry`/`search`/`install` all come from the packages module.
|
|
118
|
+
* - `registry`/`search`/`install`/`packages` all come from the packages module.
|
|
119
119
|
* - `trash` and `restore` are separate registrars in the trash module.
|
|
120
120
|
* - `prune` needs versions FIRST (it creates `prune <specs...>`) then prune.js
|
|
121
121
|
* (which finds that command and attaches the `cleanup` subcommand).
|
|
@@ -152,6 +152,7 @@ export const COMMAND_LOADERS = {
|
|
|
152
152
|
registry: [loadPackages],
|
|
153
153
|
search: [loadPackages],
|
|
154
154
|
install: [loadPackages],
|
|
155
|
+
packages: [loadPackages],
|
|
155
156
|
routines: [loadRoutines],
|
|
156
157
|
monitors: [loadMonitors],
|
|
157
158
|
projects: [loadProjects],
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents packages materialize` — user-facing front door for portable-agent
|
|
3
|
+
* materialization (PHNX-3838). ONE execution path: this command resolves the
|
|
4
|
+
* schema-v3 package once with {@link resolveAgentPackage} and projects it into
|
|
5
|
+
* an ephemeral native home with the canonical {@link materializeAgentPackage}
|
|
6
|
+
* (agent-spec/materialize.ts). The front door owns only what a materializer must
|
|
7
|
+
* not: the portable-harness allowlist, an exact harness version, and the
|
|
8
|
+
* output-home refusal that keeps a run off the live `~/.claude` / `~/.codex` /
|
|
9
|
+
* `~/.opencode` homes.
|
|
10
|
+
*/
|
|
11
|
+
import type { Command } from 'commander';
|
|
12
|
+
import { type MaterializationReceipt } from '../lib/agent-spec/index.js';
|
|
13
|
+
import { PORTABLE_HARNESSES } from '../lib/packages/output-home.js';
|
|
14
|
+
export { PORTABLE_HARNESSES };
|
|
15
|
+
export type { MaterializationReceipt };
|
|
16
|
+
/** Register `agents packages materialize`. */
|
|
17
|
+
export declare function registerPortablePackageCommands(program: Command): void;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { die, isJsonMode } from '../lib/format.js';
|
|
3
|
+
import { setHelpSections } from '../lib/help.js';
|
|
4
|
+
import { resolveAgentPackage, materializeAgentPackage, AgentPackageError, } from '../lib/agent-spec/index.js';
|
|
5
|
+
import { MaterializeGuardError, PORTABLE_HARNESSES, assertExactHarnessVersion, assertPortableHarness, resolveOutputHome, } from '../lib/packages/output-home.js';
|
|
6
|
+
export { PORTABLE_HARNESSES };
|
|
7
|
+
function printReceipt(receipt, outputHome) {
|
|
8
|
+
console.log(chalk.bold(`Materialized ${receipt.agent.ref} → ${receipt.harness.id}@${receipt.harness.version}`));
|
|
9
|
+
console.log(` digest ${receipt.agent.digest}`);
|
|
10
|
+
console.log(` output home ${outputHome}`);
|
|
11
|
+
for (const entry of receipt.resources) {
|
|
12
|
+
console.log(` ${entry.kind.padEnd(12)} ${entry.name} → ${entry.target}`);
|
|
13
|
+
}
|
|
14
|
+
if (receipt.warnings.length > 0) {
|
|
15
|
+
console.log(chalk.yellow(' warnings'));
|
|
16
|
+
for (const warning of receipt.warnings)
|
|
17
|
+
console.log(chalk.yellow(` ${warning}`));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function fail(err, json) {
|
|
21
|
+
if (err instanceof MaterializeGuardError || err instanceof AgentPackageError) {
|
|
22
|
+
die(err.message, 1, { json });
|
|
23
|
+
}
|
|
24
|
+
die(err instanceof Error ? err.message : String(err), 1, { json });
|
|
25
|
+
}
|
|
26
|
+
/** Register `agents packages materialize`. */
|
|
27
|
+
export function registerPortablePackageCommands(program) {
|
|
28
|
+
const packagesCmd = program
|
|
29
|
+
.command('packages')
|
|
30
|
+
.description('Portable agent packages — materialize schema-v3 agent.yaml into an ephemeral harness home');
|
|
31
|
+
setHelpSections(packagesCmd, {
|
|
32
|
+
examples: `
|
|
33
|
+
# Materialize a package into an ephemeral Claude home
|
|
34
|
+
agents packages materialize ./reviewer --harness claude --harness-version 2.1.0 --output-home /tmp/reviewer-claude
|
|
35
|
+
|
|
36
|
+
# Machine-readable receipt for Factory / Prix Cloud
|
|
37
|
+
agents packages materialize ./reviewer --harness codex --harness-version 0.42.0 --output-home "$OUTPUT_HOME" --json
|
|
38
|
+
|
|
39
|
+
# OpenCode worker on a Factory box
|
|
40
|
+
agents packages materialize ./reviewer --harness opencode --harness-version 1.0.0 --output-home "$OUTPUT_HOME" --json
|
|
41
|
+
`,
|
|
42
|
+
notes: `
|
|
43
|
+
Materialize writes only under --output-home. It never mutates ~/.claude,
|
|
44
|
+
~/.codex, or ~/.opencode, never copies secrets, and never execs a harness.
|
|
45
|
+
|
|
46
|
+
Factory usage: point --output-home at the worker's ephemeral home and pass
|
|
47
|
+
--json so the orchestrator can read the receipt (agent ref + digest,
|
|
48
|
+
harness, per-resource targets, warnings).
|
|
49
|
+
`,
|
|
50
|
+
});
|
|
51
|
+
const materializeCmd = packagesCmd
|
|
52
|
+
.command('materialize <package>')
|
|
53
|
+
.description('Materialize a schema-v3 agent.yaml package into an ephemeral Claude, Codex, or OpenCode home')
|
|
54
|
+
.requiredOption('--harness <id>', `Target harness: ${PORTABLE_HARNESSES.join(', ')}`)
|
|
55
|
+
.requiredOption('--harness-version <version>', 'Exact harness version to gate capabilities and stamp on the receipt (not --version: that is the CLI version flag)')
|
|
56
|
+
.requiredOption('--output-home <dir>', 'Ephemeral home to write into (must not escape or target the live user home)')
|
|
57
|
+
.option('--json', 'Emit the materialization receipt as JSON')
|
|
58
|
+
.action((pkg, opts) => {
|
|
59
|
+
const json = isJsonMode(opts);
|
|
60
|
+
try {
|
|
61
|
+
const harness = assertPortableHarness(opts.harness);
|
|
62
|
+
const harnessVersion = assertExactHarnessVersion(opts.harnessVersion);
|
|
63
|
+
const outputHome = resolveOutputHome(opts.outputHome);
|
|
64
|
+
const resolved = resolveAgentPackage(pkg);
|
|
65
|
+
const receipt = materializeAgentPackage(resolved, { harness, harnessVersion, outputHome });
|
|
66
|
+
if (json) {
|
|
67
|
+
// Verbatim canonical receipt — byte-identical to the
|
|
68
|
+
// materialization-receipt.json the materializer wrote into the home.
|
|
69
|
+
console.log(JSON.stringify(receipt, null, 2));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
printReceipt(receipt, outputHome);
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
fail(err, json);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
setHelpSections(materializeCmd, {
|
|
79
|
+
examples: `
|
|
80
|
+
agents packages materialize ./reviewer --harness claude --harness-version 2.1.0 --output-home /tmp/reviewer-claude
|
|
81
|
+
agents packages materialize ./reviewer --harness codex --harness-version 0.42.0 --output-home "$OUTPUT_HOME" --json
|
|
82
|
+
agents packages materialize ./reviewer --harness opencode --harness-version 1.0.0 --output-home "$OUTPUT_HOME" --json
|
|
83
|
+
`,
|
|
84
|
+
notes: `
|
|
85
|
+
Factory / Prix Cloud: materialize into the worker's ephemeral home, then
|
|
86
|
+
exec the harness with HOME=$OUTPUT_HOME. The --json receipt is the handoff.
|
|
87
|
+
|
|
88
|
+
Supported harnesses: claude, codex, opencode. Any other id fails as an
|
|
89
|
+
unsupported capability. A package must be a directory containing agent.yaml
|
|
90
|
+
with schema_version: 3 and an execution block declaring the harness.
|
|
91
|
+
`,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Package registry and installation commands.
|
|
3
3
|
*
|
|
4
|
-
* Registers `agents registry`, `agents search`,
|
|
5
|
-
* for discovering and installing MCP servers,
|
|
6
|
-
* hooks from configured registries or GitHub sources
|
|
4
|
+
* Registers `agents registry`, `agents search`, `agents install`, and
|
|
5
|
+
* `agents packages materialize` for discovering and installing MCP servers,
|
|
6
|
+
* skills, commands, and hooks from configured registries or GitHub sources,
|
|
7
|
+
* and for materializing a schema-v3 agent.yaml package into an ephemeral
|
|
8
|
+
* harness home (PHNX-3838).
|
|
7
9
|
*/
|
|
8
10
|
import type { Command } from 'commander';
|
|
9
11
|
import type { McpPackage } from '../lib/types.js';
|
|
10
12
|
import { type McpCommandSpec } from '../lib/mcp.js';
|
|
11
13
|
export declare function buildMcpPackageCommand(pkg: McpPackage): McpCommandSpec;
|
|
12
|
-
/** Register the `agents registry`, `agents search`, and `agents
|
|
14
|
+
/** Register the `agents registry`, `agents search`, `agents install`, and `agents packages` commands. */
|
|
13
15
|
export declare function registerPackagesCommands(program: Command): void;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Package registry and installation commands.
|
|
3
3
|
*
|
|
4
|
-
* Registers `agents registry`, `agents search`,
|
|
5
|
-
* for discovering and installing MCP servers,
|
|
6
|
-
* hooks from configured registries or GitHub sources
|
|
4
|
+
* Registers `agents registry`, `agents search`, `agents install`, and
|
|
5
|
+
* `agents packages materialize` for discovering and installing MCP servers,
|
|
6
|
+
* skills, commands, and hooks from configured registries or GitHub sources,
|
|
7
|
+
* and for materializing a schema-v3 agent.yaml package into an ephemeral
|
|
8
|
+
* harness home (PHNX-3838).
|
|
7
9
|
*/
|
|
8
10
|
import * as fs from 'fs';
|
|
9
11
|
import chalk from 'chalk';
|
|
@@ -23,6 +25,7 @@ import { listInstalledVersions, resolveConfiguredAgentTargets, syncResourcesToVe
|
|
|
23
25
|
import { formatPath, isInteractiveTerminal, isPromptCancelled, parseCommaSeparatedList, requireDestructiveArg, requireInteractiveSelection, resolveInstalledAgentTargetsAutoInstalling, } from './utils.js';
|
|
24
26
|
import { itemPicker } from '../lib/picker.js';
|
|
25
27
|
import { registerMcpCommandToTargets, discoverMcpConfigsFromRepo, installMcpConfigCentrally, } from '../lib/mcp.js';
|
|
28
|
+
import { registerPortablePackageCommands } from './packages-materialize.js';
|
|
26
29
|
export function buildMcpPackageCommand(pkg) {
|
|
27
30
|
const packageName = pkg.name || pkg.registry_name;
|
|
28
31
|
if (pkg.runtime === 'node') {
|
|
@@ -80,8 +83,9 @@ async function pickRegistryName(type, verb, pred) {
|
|
|
80
83
|
throw err;
|
|
81
84
|
}
|
|
82
85
|
}
|
|
83
|
-
/** Register the `agents registry`, `agents search`, and `agents
|
|
86
|
+
/** Register the `agents registry`, `agents search`, `agents install`, and `agents packages` commands. */
|
|
84
87
|
export function registerPackagesCommands(program) {
|
|
88
|
+
registerPortablePackageCommands(program);
|
|
85
89
|
// ==========================================================================
|
|
86
90
|
// REGISTRY COMMANDS
|
|
87
91
|
// ==========================================================================
|
package/dist/commands/repo.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { visibleWidth, padVisible } from '../lib/format.js';
|
|
3
3
|
import { stripAnsi } from '../lib/session/width.js';
|
|
4
|
-
import
|
|
4
|
+
import { interruptibleSpinner } from '../lib/spinner.js';
|
|
5
5
|
import * as fs from 'fs';
|
|
6
6
|
import * as path from 'path';
|
|
7
7
|
import * as os from 'os';
|
|
@@ -785,7 +785,7 @@ export function registerRepoCommands(program) {
|
|
|
785
785
|
return;
|
|
786
786
|
}
|
|
787
787
|
const parsed = parseSource(options.from);
|
|
788
|
-
const spinner =
|
|
788
|
+
const spinner = interruptibleSpinner(`Cloning ${options.from} into ${targetDir}...`).start();
|
|
789
789
|
try {
|
|
790
790
|
fs.mkdirSync(path.dirname(targetDir), { recursive: true });
|
|
791
791
|
await simpleGit().clone(parsed.url, targetDir);
|
|
@@ -885,7 +885,7 @@ export function registerRepoCommands(program) {
|
|
|
885
885
|
process.exitCode = 1;
|
|
886
886
|
return;
|
|
887
887
|
}
|
|
888
|
-
const spinner =
|
|
888
|
+
const spinner = interruptibleSpinner(`Cloning ${source}...`).start();
|
|
889
889
|
try {
|
|
890
890
|
fs.mkdirSync(path.dirname(targetDir), { recursive: true });
|
|
891
891
|
await simpleGit().clone(parsed.url, targetDir);
|
|
@@ -1023,7 +1023,7 @@ export function registerRepoCommands(program) {
|
|
|
1023
1023
|
// user. Only the USER repo gets this — system is cloned by setup, extras
|
|
1024
1024
|
// by `repo add`.
|
|
1025
1025
|
if (t.alias === 'user' && url) {
|
|
1026
|
-
const spinner =
|
|
1026
|
+
const spinner = interruptibleSpinner(`Git-backing ${t.dir} from ${url}...`).start();
|
|
1027
1027
|
const res = await adoptRepo(url, t.dir);
|
|
1028
1028
|
if (!res.success) {
|
|
1029
1029
|
spinner.fail(`user: could not git-back — ${res.error}`);
|
|
@@ -1049,7 +1049,7 @@ export function registerRepoCommands(program) {
|
|
|
1049
1049
|
// Skip system repo unless explicitly requested
|
|
1050
1050
|
continue;
|
|
1051
1051
|
}
|
|
1052
|
-
const spinner =
|
|
1052
|
+
const spinner = interruptibleSpinner(`Pulling ${formatRepoTarget(t.alias, t.dir)}...`).start();
|
|
1053
1053
|
const result = await pullRepo(t.dir);
|
|
1054
1054
|
if (result.success) {
|
|
1055
1055
|
spinner.succeed(`${formatRepoTarget(t.alias, t.dir, result.branch)}: ${result.commit}`);
|
|
@@ -1127,7 +1127,7 @@ export function registerRepoCommands(program) {
|
|
|
1127
1127
|
// the previous tick's state and delays propagation by one repo cycle.
|
|
1128
1128
|
if (t.alias === 'user')
|
|
1129
1129
|
await publishUserRepoAccountState(true);
|
|
1130
|
-
const spinner =
|
|
1130
|
+
const spinner = interruptibleSpinner(`Pushing ${formatRepoTarget(t.alias, t.dir)}...`).start();
|
|
1131
1131
|
const result = await commitAndPush(t.dir, options.message);
|
|
1132
1132
|
if (result.success) {
|
|
1133
1133
|
spinner.succeed(`${formatRepoTarget(t.alias, t.dir, result.branch)}: ${result.detail ?? 'pushed'}`);
|
|
@@ -1171,7 +1171,7 @@ Examples:
|
|
|
1171
1171
|
// state, then fall through to the normal sync (PHNX-3301). System is
|
|
1172
1172
|
// cloned by setup and extras by `repo add`, so those still skip.
|
|
1173
1173
|
if (t.alias === 'user' && fs.existsSync(t.dir)) {
|
|
1174
|
-
const spinner =
|
|
1174
|
+
const spinner = interruptibleSpinner(`Adopting ${formatRepoTarget(t.alias, t.dir)} in place...`).start();
|
|
1175
1175
|
const adopted = await adoptUserRepoIfNeeded(t.dir);
|
|
1176
1176
|
if (!adopted || !adopted.success) {
|
|
1177
1177
|
spinner.fail(`${formatRepoTarget(t.alias, t.dir)}: ${adopted?.error ?? 'adopt failed'}`);
|
|
@@ -1199,7 +1199,7 @@ Examples:
|
|
|
1199
1199
|
// so the same transaction carries it; consume peers after the pull.
|
|
1200
1200
|
if (t.alias === 'user')
|
|
1201
1201
|
await publishUserRepoAccountState(false);
|
|
1202
|
-
const spinner =
|
|
1202
|
+
const spinner = interruptibleSpinner(`Syncing ${formatRepoTarget(t.alias, t.dir)}...`).start();
|
|
1203
1203
|
const result = t.alias === 'user'
|
|
1204
1204
|
? await (async () => {
|
|
1205
1205
|
const { syncFleetSharedStateRepo } = await import('../lib/fleet-shared-repo-sync.js');
|
|
@@ -306,6 +306,7 @@ function previewCacheKey(session, remote) {
|
|
|
306
306
|
session.topic, session.ticketId, session.prUrl, session.messageCount,
|
|
307
307
|
session.tokenCount, session.model, session.todos, session.plan,
|
|
308
308
|
session.recentDirectoriesTouched, session.skillsUsed,
|
|
309
|
+
session.firstUserMessage, session.mirrorSyncedAt,
|
|
309
310
|
]);
|
|
310
311
|
}
|
|
311
312
|
export function clearPreviewMemoryCacheForTest() {
|
|
@@ -363,12 +364,38 @@ export function buildPreview(session) {
|
|
|
363
364
|
return cached;
|
|
364
365
|
const safe = sanitizeMeta(session);
|
|
365
366
|
// Remote session: the transcript is on the peer's disk, so there is nothing to
|
|
366
|
-
// parse here.
|
|
367
|
-
//
|
|
368
|
-
//
|
|
369
|
-
//
|
|
370
|
-
//
|
|
367
|
+
// parse here. The common case is now a fleet-synced MIRROR row (PHNX-3792): its
|
|
368
|
+
// topic + first-user snippet + metadata are already local, so the compact card
|
|
369
|
+
// renders INLINE with no per-row SSH. The live SSH digest fetch stays as the
|
|
370
|
+
// fallback for a never-synced row (or a peer whose digest was already fetched
|
|
371
|
+
// this session), and `space` still reads the full transcript live over SSH.
|
|
371
372
|
if (remote) {
|
|
373
|
+
// A fleet-synced MIRROR row is the explicit signal that this box already
|
|
374
|
+
// holds the peer session's topic + first-user snippet locally (PHNX-3792):
|
|
375
|
+
// render inline with NO per-row SSH. An ordinary remote row (a live fan-out
|
|
376
|
+
// row, or a never-synced host-dispatch stub) is NOT treated as synced — it
|
|
377
|
+
// keeps the live digest fetch below, so this change is scoped to mirror rows.
|
|
378
|
+
const fetched = remoteDigestCache.get(remoteDigestKey(session.id, remote));
|
|
379
|
+
if (fetched?.state === 'ready') {
|
|
380
|
+
// A richer live digest already landed this session (changed files, tool
|
|
381
|
+
// mix) — prefer it over the lightweight mirror card.
|
|
382
|
+
const note = ' ' + chalk.gray(`on `) + chalk.bold.white(remote)
|
|
383
|
+
+ chalk.gray(` — enter to resume there`);
|
|
384
|
+
const body = formatCompactPreview(fetched.digest, safe);
|
|
385
|
+
const output = [formatHeader(safe, []), '', note, body].filter(Boolean).join('\n');
|
|
386
|
+
previewCache.set(cacheKey, output);
|
|
387
|
+
return output;
|
|
388
|
+
}
|
|
389
|
+
if (safe.mirrorSyncedAt !== undefined) {
|
|
390
|
+
const note = ' ' + chalk.gray(`on `) + chalk.bold.white(remote)
|
|
391
|
+
+ chalk.gray(` — synced from the fleet; enter to resume there, or space to read it live over SSH`);
|
|
392
|
+
const metaBody = formatMetaOnlyBody(safe);
|
|
393
|
+
const output = [formatHeader(safe, []), '', note, metaBody].filter(Boolean).join('\n');
|
|
394
|
+
previewCache.set(cacheKey, output);
|
|
395
|
+
return output;
|
|
396
|
+
}
|
|
397
|
+
// Not a mirror row: fall back to the live SSH digest fetch (kicked off on
|
|
398
|
+
// first render; the pane repaints when it lands).
|
|
372
399
|
const note = ' ' + chalk.gray(`on `) + chalk.bold.white(remote)
|
|
373
400
|
+ chalk.gray(` — enter to resume there, or space then enter to read it over SSH`);
|
|
374
401
|
const entry = remoteDigestForPreview(session, remote);
|
|
@@ -554,8 +581,12 @@ function formatMetaOnlyBody(session) {
|
|
|
554
581
|
const termWidth = process.stdout.columns || 80;
|
|
555
582
|
const valueWidth = termWidth - VERB_GUTTER - 5;
|
|
556
583
|
// Same verb-led rows as the digest body (RUSH-2757), from SessionMeta alone.
|
|
557
|
-
|
|
558
|
-
|
|
584
|
+
// Prefer the fuller first genuine user turn over the one-line topic when the
|
|
585
|
+
// row carries it (a fleet-synced mirror row does — PHNX-3792) so the inline
|
|
586
|
+
// card reads like the originating prompt, not just its title.
|
|
587
|
+
const asked = session.firstUserMessage?.trim() || session.topic?.trim();
|
|
588
|
+
if (asked) {
|
|
589
|
+
lines.push(verbLabel('Asked') + chalk.white(`"${truncate(asked, valueWidth)}"`));
|
|
559
590
|
}
|
|
560
591
|
const compact = formatTodoCompact(session.todos);
|
|
561
592
|
const teamLine = formatTeamLineage(session);
|
|
@@ -18,6 +18,7 @@ import { sanitizeForTerminal, redactSecrets } from '../lib/redact.js';
|
|
|
18
18
|
import { resolveProjectKey } from '../lib/project-key.js';
|
|
19
19
|
import { listProjectDefs, resolveProjectNameForCwd } from '../lib/projects.js';
|
|
20
20
|
import ora from 'ora';
|
|
21
|
+
import { interruptibleSpinner } from '../lib/spinner.js';
|
|
21
22
|
import { SESSION_AGENTS, isAgentTmuxAlias, sessionDisplayAgent } from '../lib/session/types.js';
|
|
22
23
|
import { discoverArtifacts, readArtifact, resolveArtifact } from '../lib/session/artifacts.js';
|
|
23
24
|
import { looksLikePath, toComparablePath, homeDir, needsWindowsShell, composeWin32CommandLine } from '../lib/platform/index.js';
|
|
@@ -2801,7 +2802,7 @@ limitSource) {
|
|
|
2801
2802
|
const forwarded = ensureWholeIndex(buildForwardedArgs(process.argv, new Set(options.host ?? [])));
|
|
2802
2803
|
if (!forwarded.includes('--json'))
|
|
2803
2804
|
forwarded.push('--json');
|
|
2804
|
-
const fanSpinner = isInteractiveTerminal() ?
|
|
2805
|
+
const fanSpinner = isInteractiveTerminal() ? interruptibleSpinner('Reaching other machines...').start() : null;
|
|
2805
2806
|
try {
|
|
2806
2807
|
const { sessions: remoteSessions } = await gatherRemoteList(forwarded, undefined);
|
|
2807
2808
|
if (remoteSessions.length > 0) {
|
|
@@ -2841,7 +2842,7 @@ limitSource) {
|
|
|
2841
2842
|
const forwarded = buildForwardedArgs(process.argv, new Set(options.host ?? []));
|
|
2842
2843
|
if (!forwarded.includes('--json'))
|
|
2843
2844
|
forwarded.push('--json');
|
|
2844
|
-
const fanSpinner = isInteractiveTerminal() ?
|
|
2845
|
+
const fanSpinner = isInteractiveTerminal() ? interruptibleSpinner('Reaching other machines...').start() : null;
|
|
2845
2846
|
try {
|
|
2846
2847
|
const { sessions: remoteSessions } = await gatherRemoteList(forwarded, options.host);
|
|
2847
2848
|
if (remoteSessions.length > 0) {
|
|
@@ -4105,7 +4106,7 @@ async function runCloudSessions(query, options) {
|
|
|
4105
4106
|
process.exit(1);
|
|
4106
4107
|
}
|
|
4107
4108
|
const mode = resolveViewMode(options, filterOpts);
|
|
4108
|
-
const spinner = options.json ? null :
|
|
4109
|
+
const spinner = options.json ? null : interruptibleSpinner('Loading cloud sessions...').start();
|
|
4109
4110
|
let sessions;
|
|
4110
4111
|
try {
|
|
4111
4112
|
sessions = await discoverCloudSessions({ limit: parseInt(options.limit || '50', 10) });
|
|
@@ -4141,7 +4142,7 @@ async function runCloudSessions(query, options) {
|
|
|
4141
4142
|
process.exit(1);
|
|
4142
4143
|
}
|
|
4143
4144
|
const meta = matches[0];
|
|
4144
|
-
const cachedSpinner = options.json ? null :
|
|
4145
|
+
const cachedSpinner = options.json ? null : interruptibleSpinner('Fetching session...').start();
|
|
4145
4146
|
let cachedPath;
|
|
4146
4147
|
try {
|
|
4147
4148
|
cachedPath = await ensureCloudSessionCached(meta.id);
|
|
@@ -4972,7 +4973,7 @@ export async function resolveSessionMetadata(selector, scope, deps = { gatherRem
|
|
|
4972
4973
|
process.stdout.write(serializeResolvedSessionsJson([outcome.session]));
|
|
4973
4974
|
}
|
|
4974
4975
|
export async function resolveSessionAcrossFleet(query, mode, hosts, deps = { gatherRemoteList, runOnPeer }) {
|
|
4975
|
-
const spinner = isInteractiveTerminal() ?
|
|
4976
|
+
const spinner = isInteractiveTerminal() ? interruptibleSpinner('Searching the fleet...').start() : null;
|
|
4976
4977
|
let candidates = [];
|
|
4977
4978
|
let deviceCount = 0;
|
|
4978
4979
|
let unreachable = [];
|
package/dist/lib/actor.d.ts
CHANGED
|
@@ -13,6 +13,13 @@ export interface ResolvedActor {
|
|
|
13
13
|
email?: string;
|
|
14
14
|
/** GitHub handle, when the actors map records one. */
|
|
15
15
|
github?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Phoenix (work) identity id for this human, when the actors map records one.
|
|
18
|
+
* Bridges a personal tailnet login (e.g. a personal gmail) to the stable
|
|
19
|
+
* internal work identity, so attribution survives whichever email a person
|
|
20
|
+
* happens to be signed into tailscale with.
|
|
21
|
+
*/
|
|
22
|
+
phoenixId?: string;
|
|
16
23
|
}
|
|
17
24
|
/** Result of `tailscale whois --json <ip>` we care about. */
|
|
18
25
|
export interface WhoisIdentity {
|
|
@@ -28,16 +35,41 @@ export interface WhoisIdentity {
|
|
|
28
35
|
*/
|
|
29
36
|
export declare function actorFromIdentity(who: WhoisIdentity | undefined, host: string, actors: Record<string, ActorConfig>): ResolvedActor;
|
|
30
37
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
38
|
+
* Injectable tailscale resolvers, so tests can drive the SSH-whois and
|
|
39
|
+
* local-self branches deterministically without a real tailscale on the box
|
|
40
|
+
* (a dev machine that *is* on the tailnet would otherwise make the local path
|
|
41
|
+
* non-deterministic). Production callers use the defaults.
|
|
34
42
|
*/
|
|
35
|
-
export
|
|
43
|
+
export interface ActorResolvers {
|
|
44
|
+
whois: (ip: string) => WhoisIdentity | undefined;
|
|
45
|
+
self: () => WhoisIdentity | undefined;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Compute the actor for a given environment. The only impurity is the tailscale
|
|
49
|
+
* shell-out (injectable via `resolvers`), so tests drive every branch explicitly.
|
|
50
|
+
*
|
|
51
|
+
* Resolution order: an inherited env actor wins; otherwise an SSH run whois-es
|
|
52
|
+
* its client IP; a local run (no SSH) credits the device's own tailnet owner;
|
|
53
|
+
* and anything unresolvable degrades to `UNRESOLVED@<host>`. Note the self
|
|
54
|
+
* fallback fires ONLY for a truly local run — an SSH run whose whois fails must
|
|
55
|
+
* NOT be credited to the box owner (that would misattribute a remote human to
|
|
56
|
+
* whoever owns the machine).
|
|
57
|
+
*/
|
|
58
|
+
export declare function computeActor(env?: NodeJS.ProcessEnv, resolvers?: ActorResolvers): ResolvedActor;
|
|
36
59
|
/**
|
|
37
60
|
* Resolve the actor for the current process, cached for the process lifetime
|
|
38
61
|
* (the SSH `whois` shell-out runs at most once).
|
|
39
62
|
*/
|
|
40
63
|
export declare function resolveActor(): ResolvedActor;
|
|
64
|
+
/**
|
|
65
|
+
* Test-only: pin the tailscale resolvers `resolveActor()` uses, so a test that
|
|
66
|
+
* exercises the cached production entrypoint (e.g. `withActorEnv()`) is isolated
|
|
67
|
+
* from whether the box running it is on the tailnet. `computeActor` already takes
|
|
68
|
+
* injected resolvers for its unit tests; this extends the same seam to the cached
|
|
69
|
+
* path. Pass `undefined` to restore the real tailscale resolvers. Resets the
|
|
70
|
+
* cache so the next `resolveActor()` recomputes under the new resolvers.
|
|
71
|
+
*/
|
|
72
|
+
export declare function setActorResolvers(resolvers: ActorResolvers | undefined): void;
|
|
41
73
|
/** Clear the per-process cache. For tests, and for env changes within a run. */
|
|
42
74
|
export declare function resetActorCache(): void;
|
|
43
75
|
/**
|