@phnx-labs/agents-cli 1.20.89 → 1.20.90
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 +240 -0
- package/README.md +6 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/exec.js +7 -1
- package/dist/commands/harness.d.ts +27 -0
- package/dist/commands/harness.js +120 -13
- package/dist/commands/profiles.d.ts +3 -0
- package/dist/commands/profiles.js +1 -1
- package/dist/commands/routines.d.ts +19 -0
- package/dist/commands/routines.js +28 -6
- package/dist/commands/secrets.d.ts +10 -1
- package/dist/commands/secrets.js +18 -6
- package/dist/commands/sessions-browser.d.ts +4 -0
- package/dist/commands/sessions-browser.js +51 -9
- package/dist/commands/sessions-favorite.d.ts +20 -0
- package/dist/commands/sessions-favorite.js +120 -0
- package/dist/commands/sessions.d.ts +103 -20
- package/dist/commands/sessions.js +356 -62
- package/dist/commands/setup-secrets.d.ts +7 -0
- package/dist/commands/setup-secrets.js +12 -9
- package/dist/commands/versions.js +12 -4
- package/dist/commands/view.d.ts +14 -1
- package/dist/commands/view.js +103 -128
- package/dist/lib/agents.d.ts +4 -2
- package/dist/lib/agents.js +21 -6
- package/dist/lib/hosts/dispatch.js +19 -1
- package/dist/lib/hq/floor.js +12 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/picker.d.ts +27 -2
- package/dist/lib/picker.js +71 -7
- package/dist/lib/profiles.d.ts +48 -0
- package/dist/lib/profiles.js +67 -0
- package/dist/lib/rotate.d.ts +24 -2
- package/dist/lib/rotate.js +63 -6
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/session/active.d.ts +109 -3
- package/dist/lib/session/active.js +269 -13
- package/dist/lib/session/db.d.ts +14 -0
- package/dist/lib/session/db.js +35 -0
- package/dist/lib/session/favorites.d.ts +39 -0
- package/dist/lib/session/favorites.js +101 -0
- package/dist/lib/session/host-link.d.ts +68 -0
- package/dist/lib/session/host-link.js +64 -0
- package/dist/lib/session/presence.d.ts +85 -0
- package/dist/lib/session/presence.js +150 -0
- package/dist/lib/session/remote-list.d.ts +10 -0
- package/dist/lib/session/remote-list.js +47 -9
- package/dist/lib/tmux/binary.d.ts +7 -0
- package/dist/lib/tmux/binary.js +11 -1
- package/dist/lib/types.d.ts +4 -3
- package/dist/lib/usage-backoff.d.ts +29 -0
- package/dist/lib/usage-backoff.js +165 -0
- package/dist/lib/usage.d.ts +112 -5
- package/dist/lib/usage.js +464 -46
- package/dist/lib/watchdog/runner.d.ts +13 -0
- package/dist/lib/watchdog/runner.js +16 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,245 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.20.90
|
|
4
|
+
|
|
5
|
+
- **`agents sessions --active` now shows one row per agent, not one per directory.**
|
|
6
|
+
A live tmux agent pane whose durable identity records were missing (the common case
|
|
7
|
+
once meta/pid-registry entries age out) was dropped, then re-surfaced by the ps-scan
|
|
8
|
+
under the newest transcript in its cwd — so many distinct sessions collapsed onto one
|
|
9
|
+
stranger's id with an inflated `×N` badge, and `agents sessions focus <id>` could not
|
|
10
|
+
find them. The scanner now recovers the session id straight from the `ag-<agent>-<shortid>`
|
|
11
|
+
tmux pane name (resolved to the full UUID via the short-id index in one batched query),
|
|
12
|
+
and refuses to borrow a co-located sibling's transcript when no id is known — so every
|
|
13
|
+
live session surfaces as its own row and is focus-able again. Also adds a `runTmux`
|
|
14
|
+
timeout so a wedged tmux server can't hang the scan. Source: `apps/cli/src/lib/session/active.ts`.
|
|
15
|
+
|
|
16
|
+
- **`agents view` now shows live usage bars for Antigravity.** The `agy` account
|
|
17
|
+
row renders one bar per model quota bucket (`3.1P: ███░░ 42% (1d)` style),
|
|
18
|
+
sourced from the same Google Code Assist `:retrieveUserQuota` endpoint `agy`
|
|
19
|
+
itself talks to. Auth reuses the stored `agy` OAuth credential (macOS Keychain
|
|
20
|
+
item `gemini`/`antigravity`, Linux Secret Service, or the
|
|
21
|
+
`~/.gemini/antigravity-cli/antigravity-oauth-token` file fallback), refreshing
|
|
22
|
+
the access token in memory when expired — safe from a read path because
|
|
23
|
+
Google's refresh tokens are non-rotating, and never written back to the
|
|
24
|
+
keychain. Each per-model bucket also flows into the throttle badge, run
|
|
25
|
+
rotation eligibility, and `agents view --json` (whose usage windows now carry
|
|
26
|
+
a `label` so same-keyed per-model bars are distinguishable). Source:
|
|
27
|
+
`apps/cli/src/lib/usage.ts`, `apps/cli/src/lib/agents.ts`,
|
|
28
|
+
`apps/cli/src/commands/view.ts`.
|
|
29
|
+
|
|
30
|
+
- **A custom harness is now its own agent type in `agents view`.** A harness created
|
|
31
|
+
with `agents harness add` (or `agents profiles add`) used to render as an indented
|
|
32
|
+
`profile` row under whichever host CLI executes it. It now gets its own block beside
|
|
33
|
+
Claude and Codex — a bold name header, then one row carrying the pinned model, the
|
|
34
|
+
account/auth state, and `via <host> <version>` naming the native harness underneath.
|
|
35
|
+
That matches how it is already launched: `agents run <name>` treats a custom harness
|
|
36
|
+
exactly like a native agent id. A harness whose host CLI has no install is flagged
|
|
37
|
+
`(host <id> not installed)` rather than listed as runnable, and the separate
|
|
38
|
+
"Profile-only Agents" section is gone — those harnesses now render in the main list
|
|
39
|
+
like every other one. Source: `apps/cli/src/commands/view.ts`.
|
|
40
|
+
- **`agents view <harness>` describes a custom harness** — host, model, provider, auth,
|
|
41
|
+
fork lineage, YAML path — instead of failing with "unknown agent";
|
|
42
|
+
`agents view <harness> --json` emits its summary. Source:
|
|
43
|
+
`apps/cli/src/commands/harness.ts` (`renderHarnessDetail`).
|
|
44
|
+
- **New `agents harness fork <source> <name>`.** One verb over both starting points:
|
|
45
|
+
fork a native harness (`agents harness fork opencode deepseek --model
|
|
46
|
+
deepseek/deepseek-v4-flash-0731 --auth-provider openrouter`) or copy a custom one you
|
|
47
|
+
already tuned and change only what you name (`agents harness fork deepseek
|
|
48
|
+
deepseek-chat --model deepseek/deepseek-chat-v3`). Forking a custom harness is a full
|
|
49
|
+
copy — env, endpoint, auth binding, `fallback_model`, host version pin — so the two
|
|
50
|
+
diverge and deleting the source never affects the fork; forking a native harness
|
|
51
|
+
requires `--model` because there is no model to inherit. Flags: `--model`,
|
|
52
|
+
`--base-url`, `--auth-provider`, `--version`, `--label`, `--description`,
|
|
53
|
+
`--key-stdin`, `--force`. Source: `apps/cli/src/lib/profiles.ts` (`forkProfile`).
|
|
54
|
+
- **Profile YAML gains optional `label:` and `forkedFrom:`.** `label` sets the name
|
|
55
|
+
`agents view` prints for the harness (defaults to the file name); `forkedFrom` records
|
|
56
|
+
the parent as display-only lineage. Existing profiles keep working untouched. Source:
|
|
57
|
+
`apps/cli/src/lib/profiles.ts`.
|
|
58
|
+
- **Breaking (`--json`):** in `agents view <agent> --json`, the per-agent `profiles` key
|
|
59
|
+
is now `harnesses`, and each entry carries new `label`, `hostVersion`, `description`,
|
|
60
|
+
and `forkedFrom` fields alongside the existing ones. Source:
|
|
61
|
+
`apps/cli/src/commands/view.ts` (`ViewJsonAgent`).
|
|
62
|
+
|
|
63
|
+
- **Menu bar ACTIVE: project accordion + session detail submenu.** Projects are
|
|
64
|
+
collapsed by default as a status strip (`▶ agents-cli ●8 ◐1 zion`); click
|
|
65
|
+
`▶`/`▼` to fold agents open under the project (idle-row caps removed — collapse
|
|
66
|
+
is the wall protection). Focusing an agent opens a side submenu with linkable
|
|
67
|
+
detail (work title URL, cwd, Linear ticket, GitHub PR, duration, copy session
|
|
68
|
+
id) from the warm `sessions --active` cache. Accordion reopen rebuilds from
|
|
69
|
+
cache only (no teams walk / no CLI schedule). Local/remote uses the same host
|
|
70
|
+
normalize as CLI `machineId()` so local rows are not mislabeled remote. Source:
|
|
71
|
+
`apps/cli/menubar/Sources/MenubarHelper/StatusItemController.swift`,
|
|
72
|
+
`LocalState.swift`, `Models.swift`.
|
|
73
|
+
|
|
74
|
+
- **An offloaded editor tab no longer displays another session's id.** A Factory
|
|
75
|
+
tab launched with `agents run --host <device>` has no local agent process, but
|
|
76
|
+
the extension still resolved its "live" session id by reading the SessionStart
|
|
77
|
+
hook's `~/.agents/.cache/state/sessions/<pid>.json` for the local pid tree —
|
|
78
|
+
the pid of the ssh client. Those files are keyed by pid alone and are only
|
|
79
|
+
pruned when the pid is dead, so once the OS recycled a pid the tab adopted
|
|
80
|
+
whatever session had last held it: one remote tab showed the id and version of
|
|
81
|
+
an unrelated synthetic run from 20 days earlier while `/status` inside it
|
|
82
|
+
reported the truth. An offloaded tab now takes its identity from the device
|
|
83
|
+
instead of local disk, and a local tab rejects any state record whose
|
|
84
|
+
SessionStart timestamp predates the tab itself.
|
|
85
|
+
- **`AGENT_TERMINAL_ID` now rides the SSH hop.** `agents run --host` forwarded
|
|
86
|
+
actor provenance but not the launching tab's terminal id, so the remote pid
|
|
87
|
+
registry recorded no terminal — leaving `agents sessions --active --host
|
|
88
|
+
<device>` unable to answer "which session is this tab running?" once the agent
|
|
89
|
+
moved on (a `/clear`, or an exit and rerun in the same tab).
|
|
90
|
+
- **`agents sessions --active --json` now carries `terminalId`.** The pid registry
|
|
91
|
+
has always recorded it; the emitted row dropped it, so no consumer could join a
|
|
92
|
+
live session back to the editor tab that launched it.
|
|
93
|
+
|
|
94
|
+
- **Balanced routing no longer launches into an account it only *thinks* has
|
|
95
|
+
headroom.** Account usage is cached per machine under stale-while-revalidate:
|
|
96
|
+
a snapshot up to 24h old was served instantly, and the background refresh that
|
|
97
|
+
should have corrected it lands after the pick is already made. On a box whose
|
|
98
|
+
refresh is failing that state is permanent — measured on `yosemite-s1`, every
|
|
99
|
+
Claude snapshot sat 26 hours to 2.7 days old, so balanced read
|
|
100
|
+
`muqsit@getrush.ai` as 48% used and launched into it while the account was at
|
|
101
|
+
its weekly cap; the session answered "You've hit your weekly limit" on its
|
|
102
|
+
first turn. Routing now caps how stale a snapshot may be when it is about to
|
|
103
|
+
decide (5 minutes), blocking on one bounded, parallel live read past that — and
|
|
104
|
+
no read at all inside the existing 2-minute fresh window, which back-to-back
|
|
105
|
+
launches hit. Display paths (`agents view`) keep the full 24h window and stay
|
|
106
|
+
off the network.
|
|
107
|
+
- **A pick made on unconfirmed data says so.** When no account on the machine
|
|
108
|
+
could be refreshed, routing still launches — a broken refresh must not make a
|
|
109
|
+
box unusable — but the banner now reads `… (2 of 5 healthy, usage unverified —
|
|
110
|
+
no account could be refreshed)` instead of presenting a guess as a fact. An
|
|
111
|
+
account with a verified snapshot always wins over one with a stale snapshot,
|
|
112
|
+
even when the stale number looks emptier. This applies to `--strategy
|
|
113
|
+
available` as well as `balanced` — both route on the same cache, and
|
|
114
|
+
`available`'s headroom sort was inverted by a stale number in exactly the same
|
|
115
|
+
way. An explicit version preference is an instruction, not a ranking signal, so
|
|
116
|
+
it still wins.
|
|
117
|
+
- **The mid-run failover chain is unchanged.** Declining to *pick* an account on
|
|
118
|
+
unconfirmed data and declining to *fail over to* it after the primary already
|
|
119
|
+
hit a 429 are different risks — by then the alternative is not launching at
|
|
120
|
+
all. Every eligible account stays in the failover chain; only the initial pick
|
|
121
|
+
prefers verified ones.
|
|
122
|
+
|
|
123
|
+
- **`agents routines list` no longer reports another device's routine as failed.** Run
|
|
124
|
+
records are written into the runs dir of whichever machine fired the routine and carry
|
|
125
|
+
no device attribution, but the listing resolved Last Status from any local record and
|
|
126
|
+
rendered it even on rows for routines pinned elsewhere. A routine re-pinned to another
|
|
127
|
+
device therefore kept reporting the old machine's leftover records forever — on zion,
|
|
128
|
+
`security-sweep`, `review-open-prs` and `hetzner-lease-gc` all read `failed` from late
|
|
129
|
+
July while `yosemite-s0`/`s1`, the devices that actually fire them, had completed them
|
|
130
|
+
that morning. The macOS menu bar reads this JSON, so it painted a column of red `exit 1`
|
|
131
|
+
rows for routines that were green. Last Status is now scoped to the device that owns the
|
|
132
|
+
run: a routine this device does not fire shows `-`, and `--json` returns `null` for
|
|
133
|
+
`lastStatus`, `exitCode`, `failureReason`, `lastRunStartedAt` and `lastRunCompletedAt`
|
|
134
|
+
(`runsHere: false` already says why). A routine pinned to several devices renders one row
|
|
135
|
+
per device but carries a status only on its **This machine** row. Read a peer's status
|
|
136
|
+
with `agents routines list --device <name>`; the local history is untouched and still
|
|
137
|
+
readable via `agents routines runs <name>`. Source: `apps/cli/src/commands/routines.ts`
|
|
138
|
+
(`localLatestRun`, `groupRoutineJobsByDevice`), `apps/cli/docs/03-routines.md`.
|
|
139
|
+
|
|
140
|
+
- **`agents watchdog` now tracks per-session presence (RUSH-2007 Layer C).** Each
|
|
141
|
+
tick reconciles a per-session presence record — `{location, device, transport,
|
|
142
|
+
lastSeen, status}` at `~/.agents/.cache/state/watchdog/presence.json` — from the
|
|
143
|
+
tick's active scan, deriving `connected` / `disconnected` by diffing consecutive
|
|
144
|
+
ticks. A session that was tracked but is now absent (its SSH link dropped or the
|
|
145
|
+
peer went unreachable) flips to `disconnected`, and the flip is surfaced in
|
|
146
|
+
`agents watchdog --json` under `presence.transitions` — an interactive drop as a
|
|
147
|
+
`reconnect-nudge` candidate, a headless remote as `keep-alive`. Folded into the
|
|
148
|
+
existing tick (no revived daemon, no extra SSH fan-out); additive and does not
|
|
149
|
+
change the tick's nudge decisions. Source:
|
|
150
|
+
`apps/cli/src/lib/session/presence.ts`, `apps/cli/src/lib/watchdog/runner.ts`.
|
|
151
|
+
|
|
152
|
+
- **`agents setup secrets --policy hold` no longer fails, and `agents secrets
|
|
153
|
+
status` stops naming the retired `daily` policy.** The 1.20.79 `daily` → `hold`
|
|
154
|
+
rename swept the help, docs, and the `secrets list` POLICY column, but two
|
|
155
|
+
surfaces were never migrated. The worse one was functional: the onboarding
|
|
156
|
+
wizard carried its own copy of the policy vocabulary, so
|
|
157
|
+
`agents setup secrets --policy hold` — the canonical name every other secrets
|
|
158
|
+
command prints — exited with `Invalid --policy 'hold'. Use daily, always, or
|
|
159
|
+
never.`, and its interactive prompt still offered `daily` as the default
|
|
160
|
+
choice. It now shares `parsePolicyOpt` with `agents secrets policy`, so the two
|
|
161
|
+
commands can't disagree about what a policy is called; `daily`/`session` stay
|
|
162
|
+
accepted as aliases and the wizard's default is unchanged (the hold tier). The
|
|
163
|
+
second was cosmetic: `agents secrets status` printed "a daily bundle prompts
|
|
164
|
+
once…" and "the next read of each daily bundle…" — the one command a user runs
|
|
165
|
+
to answer *why did it prompt again*, naming a policy its sibling commands no
|
|
166
|
+
longer emit. Both lines now say `hold` and are pure values pinned by tests, so
|
|
167
|
+
the vocabulary can't drift again. Source:
|
|
168
|
+
`apps/cli/src/commands/setup-secrets.ts`, `apps/cli/src/commands/secrets.ts`.
|
|
169
|
+
|
|
170
|
+
- **Favorite sessions from the browser.** `*` stars the highlighted session in
|
|
171
|
+
`agents sessions` and `f` filters the list to the starred ones; outside a TTY,
|
|
172
|
+
`agents sessions favorite <id>` (`--remove` / `--list` / `--json`) and
|
|
173
|
+
`agents sessions --favorites` do the same. Stars live in
|
|
174
|
+
`~/.agents/.history/favorites.json` keyed by session id, so they survive a reindex
|
|
175
|
+
of the session cache. They are per-machine — session sync carries transcripts, not
|
|
176
|
+
this file. Source:
|
|
177
|
+
`apps/cli/src/lib/session/favorites.ts`, `apps/cli/src/commands/sessions-favorite.ts`.
|
|
178
|
+
- **Detect sessions that lost their host — two new statuses, `crashed` and `orphaned`.**
|
|
179
|
+
A session whose editor window or connection went down hard used to just VANISH from
|
|
180
|
+
`agents sessions --active` (its dead-pid registry entry was filtered out), and one
|
|
181
|
+
still running in tmux with nobody attached reported a plain `idle`. Both now say so:
|
|
182
|
+
`✗ crashed` when the host window stopped republishing and the agent died with it,
|
|
183
|
+
`◍ orphan` when the agent is alive with zero clients attached. Derived from tmux's
|
|
184
|
+
`#{session_attached}` and the IDE window's registry heartbeat — never from a
|
|
185
|
+
deliberate `agents sessions detach`, and never over a session that is still working.
|
|
186
|
+
Source: `apps/cli/src/lib/session/host-link.ts`, `apps/cli/src/lib/session/active.ts`.
|
|
187
|
+
- **`agents sessions --active --favorites` now actually filters.** The flag was wired
|
|
188
|
+
into the interactive browser only, so every path that skips it — `--json`,
|
|
189
|
+
`--waiting`, a pipe, a multi-host scope, an SSH-fanout peer — silently returned the
|
|
190
|
+
whole fleet. Source: `apps/cli/src/commands/sessions.ts`.
|
|
191
|
+
- **`agents sessions --active --waiting` no longer counts a dead session.** `activity`
|
|
192
|
+
is not rewritten when a session dies, so one that crashed mid-question reported "needs
|
|
193
|
+
your input" forever — what it needs is a relaunch. Source:
|
|
194
|
+
`apps/cli/src/commands/sessions.ts`.
|
|
195
|
+
|
|
196
|
+
- **Resolve historical sessions safely across the fleet (#1757).** `agents sessions --resolve <full-id|prefix|keywords> --json` uses a versioned safe peer protocol, returns only resolver metadata, reports every full-ID candidate on ambiguity, treats synced copies as one match, and exits 2 without deciding when a peer fails, returns malformed output, or runs an older CLI. Source: `apps/cli/src/commands/sessions.ts`.
|
|
197
|
+
|
|
198
|
+
- **A rate-limited usage endpoint is now backed off instead of hammered.** The
|
|
199
|
+
daemon warms auth-health every 3 minutes and probes *every installed version
|
|
200
|
+
home* in one parallel batch, so a machine with five Claude accounts sent five
|
|
201
|
+
concurrent requests to `api.anthropic.com/api/oauth/usage` every three minutes
|
|
202
|
+
— roughly 100/hour — before the usage refresh added its own. Nothing read
|
|
203
|
+
`Retry-After`. Measured on `yosemite-s1`: the endpoint answered
|
|
204
|
+
`429 rate_limit_error` with `retry-after: 2678` (about 45 minutes) for every
|
|
205
|
+
account while the credentials themselves read healthy, and the next tick fired
|
|
206
|
+
three minutes later, deep inside the penalty window, re-arming it. The box
|
|
207
|
+
never recovered, every usage read failed, and its cache froze — the
|
|
208
|
+
permanently-stale state balanced routing was already having to defend against.
|
|
209
|
+
- **A 429 now records its deadline and every read honours it.** Usage fetches and
|
|
210
|
+
health probes for that provider short-circuit until the window passes — no
|
|
211
|
+
request, no renewed penalty — and report
|
|
212
|
+
`Claude rate-limited this machine — not retrying for 45 minutes.` The state is
|
|
213
|
+
on disk, because the callers are separate processes: the long-lived daemon and
|
|
214
|
+
every one-shot `agents view` / `agents run` — one empty file per penalty under
|
|
215
|
+
`~/.agents/.cache/usage-backoff/`, named `<agent>.<deadline>`, so two
|
|
216
|
+
processes recording the same provider at once cannot displace each other and a
|
|
217
|
+
read takes the furthest deadline. A server delay is capped at an hour, and a
|
|
218
|
+
missing or unparseable `Retry-After` still backs off.
|
|
219
|
+
|
|
220
|
+
- **A usage read that fails now says so, instead of returning a silent null.**
|
|
221
|
+
Four branches in every networked usage fetch — Claude, Kimi, Droid and
|
|
222
|
+
Cursor — returned `{ snapshot: null, error: null }`: no readable credential, a
|
|
223
|
+
locally-expired one, a rejected request, and a request that threw (timeout,
|
|
224
|
+
DNS/TLS, an unparseable payload). The caller could not tell any of them apart
|
|
225
|
+
from a healthy read, so it fell
|
|
226
|
+
back to whatever the stale-while-revalidate cache held and drew those bars as
|
|
227
|
+
fact. Measured on `yosemite-s1`: every Claude account's stored access token had
|
|
228
|
+
expired (one of them eleven days earlier), so no read could succeed, and
|
|
229
|
+
`agents view claude --refresh` printed a full, healthy-looking table twice
|
|
230
|
+
while writing nothing to the cache. A usage read never refreshes a token
|
|
231
|
+
(RUSH-1822), so an expired credential does not heal on its own — the account
|
|
232
|
+
stays unreadable until that agent actually runs. A rate-limited endpoint (429)
|
|
233
|
+
now reads differently from a rejected credential (401), because re-authing
|
|
234
|
+
fixes one and not the other.
|
|
235
|
+
- **`agents view` marks bars the live read could not confirm.** A row whose
|
|
236
|
+
snapshot came from the cache after a failed live read renders the reading plus
|
|
237
|
+
`unverified`, rather than looking identical to a confirmed one. The number
|
|
238
|
+
still shows — it is the last thing we saw — but it no longer reads as current.
|
|
239
|
+
- **`agents view --refresh` reports what it could not refresh.** It now lists
|
|
240
|
+
each account it failed to reach and why, instead of rendering a table that
|
|
241
|
+
looks fully refreshed regardless.
|
|
242
|
+
|
|
3
243
|
## 1.20.89
|
|
4
244
|
|
|
5
245
|
- **Webhook handler layer for one-off agent/workflow/command/routine triggers.**
|
package/README.md
CHANGED
|
@@ -265,6 +265,8 @@ On a terminal, `agents sessions --active` (and a bare `agents sessions`) open th
|
|
|
265
265
|
|---|---|---|
|
|
266
266
|
| `s` | search text | `--query` / positional |
|
|
267
267
|
| `r` | running only | `--active` |
|
|
268
|
+
| `f` | favorites only | `--favorites` |
|
|
269
|
+
| `*` | star / unstar the highlighted session | `agents sessions favorite <id>` |
|
|
268
270
|
| `c` | team sessions | `--teams` |
|
|
269
271
|
| `a` | agent (cycles) | `-a` |
|
|
270
272
|
| `d` | device (cycles) | `--device` |
|
|
@@ -274,6 +276,10 @@ On a terminal, `agents sessions --active` (and a bare `agents sessions`) open th
|
|
|
274
276
|
| `⏎` | resume / attach | `resume` / `focus` |
|
|
275
277
|
| `y` | copy the equivalent command | `--print-cmd` |
|
|
276
278
|
|
|
279
|
+
**Star the sessions you keep coming back to.** `*` marks the highlighted row (a `★` shows in the listing), `f` narrows to the starred ones, and `agents sessions favorite <id>` / `--favorites` do the same outside a TTY. Stars live in `~/.agents/.history/favorites.json` keyed by session id, so they survive a reindex of the session cache. They're per-machine — session sync carries transcripts, not this file.
|
|
280
|
+
|
|
281
|
+
**A session that lost its host says so.** When an editor window or an SSH connection goes down hard, the agent it owned used to simply disappear from `--active`; when an agent outlived its window in tmux, it reported a plain `idle`. Both now carry their own status: `✗ crashed` (the host went down and took the agent with it) and `◍ orphan` (still alive, but no client is attached — nothing is showing it). Read from tmux's attached-client count and the editor window's registry heartbeat, so a deliberate `agents sessions detach` is never mistaken for one, and a session that is still *working* headlessly is left alone.
|
|
282
|
+
|
|
277
283
|
Filters **stack** (they AND together), the active set shows in the header, and the highlighted row **previews below by default** (`tab` hides it) — prompt, activity, last response, plus a links line where the worked-on ticket and the PR the session opened are **clickable** (OSC 8 hyperlinks: the ticket jumps to Linear, the `PR#` to GitHub, in terminals that support them). The Linear workspace is resolved from `LINEAR_WORKSPACE` or the linear-cli config, so tickets stay plain text when it's unknown. Because every hotkey has a flag, the view you build by hand is a real command: press `y` (or run `--print-cmd`) to get the exact `ag sessions …` line — explore interactively, hand the line to an agent. Piped output, `--json`, or `--no-interactive` keep the plain listing for scripts. Peek without opening the pager with `agents sessions <id> --preview`.
|
|
278
284
|
|
|
279
285
|
| before — preview hidden | after — preview open + clickable links |
|
package/dist/bin/agents
CHANGED
|
Binary file
|
package/dist/commands/exec.js
CHANGED
|
@@ -59,7 +59,13 @@ function formatRotationBanner(result, verb = 'balanced') {
|
|
|
59
59
|
const { picked, healthy, excluded } = result;
|
|
60
60
|
const label = picked.email ? `${picked.email} · ${picked.agent}@${picked.version}` : `${picked.agent}@${picked.version}`;
|
|
61
61
|
const ratio = `${healthy.length} of ${healthy.length + excluded.length} healthy`;
|
|
62
|
-
|
|
62
|
+
// Say it when the pick was a guess. A machine whose usage refresh is failing
|
|
63
|
+
// reports old percentages with total confidence, so a silent banner reads
|
|
64
|
+
// identical whether the router knew the account had headroom or merely hoped
|
|
65
|
+
// so — and the operator only finds out when the agent answers "you've hit
|
|
66
|
+
// your weekly limit".
|
|
67
|
+
const caveat = result.usageUnverified ? ', usage unverified — no account could be refreshed' : '';
|
|
68
|
+
return `[agents] ${verb} picked ${label} (${ratio}${caveat})`;
|
|
63
69
|
}
|
|
64
70
|
/**
|
|
65
71
|
* Whether `cwd` is inside a git work tree.
|
|
@@ -11,4 +11,31 @@
|
|
|
11
11
|
* native harness registry. The `agents profiles` tree stays unchanged.
|
|
12
12
|
*/
|
|
13
13
|
import type { Command } from 'commander';
|
|
14
|
+
import { type Profile } from '../lib/profiles.js';
|
|
15
|
+
/**
|
|
16
|
+
* Print one custom harness. Shared by `agents harness view <name>` and by
|
|
17
|
+
* `agents view <name>` — a custom harness resolves as an agent type there, so
|
|
18
|
+
* both entry points must describe it identically.
|
|
19
|
+
*/
|
|
20
|
+
export declare function renderHarnessDetail(name: string): void;
|
|
21
|
+
/** Options accepted by `agents harness fork`. */
|
|
22
|
+
export interface ForkOptions {
|
|
23
|
+
model?: string;
|
|
24
|
+
baseUrl?: string;
|
|
25
|
+
authProvider?: string;
|
|
26
|
+
version?: string;
|
|
27
|
+
label?: string;
|
|
28
|
+
description?: string;
|
|
29
|
+
keyStdin?: boolean;
|
|
30
|
+
force?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Build the new harness for `agents harness fork <source> <name>`.
|
|
34
|
+
*
|
|
35
|
+
* Two sources, one verb: an existing custom harness is copied and overridden;
|
|
36
|
+
* a native agent id is turned into a harness pinned to `--model` on that host.
|
|
37
|
+
* Forking a native harness therefore requires `--model` — there is nothing to
|
|
38
|
+
* copy a model from.
|
|
39
|
+
*/
|
|
40
|
+
export declare function buildFork(source: string, name: string, opts: ForkOptions): Profile;
|
|
14
41
|
export declare function registerHarnessCommands(program: Command): void;
|
package/dist/commands/harness.js
CHANGED
|
@@ -11,15 +11,82 @@
|
|
|
11
11
|
* native harness registry. The `agents profiles` tree stays unchanged.
|
|
12
12
|
*/
|
|
13
13
|
import chalk from 'chalk';
|
|
14
|
-
import { addProfile } from './profiles.js';
|
|
15
|
-
import { listProfiles, readProfile, deleteProfile, profileHostLabel, profileProviderLabel, profileModelLabel, profileAuthLabel, getProfilePath, } from '../lib/profiles.js';
|
|
14
|
+
import { addProfile, ensureProviderToken } from './profiles.js';
|
|
15
|
+
import { listProfiles, readProfile, writeProfile, deleteProfile, profileExists, profileHostLabel, profileProviderLabel, profileModelLabel, profileAuthLabel, profileLabel, forkProfile, profileFromHostModel, authEnvKeyForHost, getProfilePath, validateProfileName, } from '../lib/profiles.js';
|
|
16
16
|
import { listPresets } from '../lib/profiles-presets.js';
|
|
17
|
-
import { AGENTS, ALL_AGENT_IDS } from '../lib/agents.js';
|
|
17
|
+
import { AGENTS, ALL_AGENT_IDS, resolveAgentName } from '../lib/agents.js';
|
|
18
18
|
/** Short capability summary for a native harness — its supported run modes. */
|
|
19
19
|
function nativeModes(id) {
|
|
20
20
|
const modes = AGENTS[id]?.capabilities?.modes ?? [];
|
|
21
21
|
return modes.length ? modes.join('/') : '-';
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Print one custom harness. Shared by `agents harness view <name>` and by
|
|
25
|
+
* `agents view <name>` — a custom harness resolves as an agent type there, so
|
|
26
|
+
* both entry points must describe it identically.
|
|
27
|
+
*/
|
|
28
|
+
export function renderHarnessDetail(name) {
|
|
29
|
+
const p = readProfile(name);
|
|
30
|
+
console.log(chalk.bold(profileLabel(p)) + chalk.gray(' (custom harness)'));
|
|
31
|
+
if (p.description)
|
|
32
|
+
console.log(chalk.gray(p.description));
|
|
33
|
+
console.log('');
|
|
34
|
+
console.log(`Host: ${profileHostLabel(p)}`);
|
|
35
|
+
console.log(`Model: ${profileModelLabel(p)}`);
|
|
36
|
+
if (p.fallback_model)
|
|
37
|
+
console.log(`Fallback: ${p.fallback_model}`);
|
|
38
|
+
console.log(`Provider: ${profileProviderLabel(p)}`);
|
|
39
|
+
console.log(`Auth: ${profileAuthLabel(p)}`);
|
|
40
|
+
if (p.forkedFrom)
|
|
41
|
+
console.log(`Forked: from ${p.forkedFrom}`);
|
|
42
|
+
console.log(chalk.gray(getProfilePath(p.name)));
|
|
43
|
+
console.log('');
|
|
44
|
+
console.log(chalk.gray(`Run: agents run ${p.name} "hello"`));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Build the new harness for `agents harness fork <source> <name>`.
|
|
48
|
+
*
|
|
49
|
+
* Two sources, one verb: an existing custom harness is copied and overridden;
|
|
50
|
+
* a native agent id is turned into a harness pinned to `--model` on that host.
|
|
51
|
+
* Forking a native harness therefore requires `--model` — there is nothing to
|
|
52
|
+
* copy a model from.
|
|
53
|
+
*/
|
|
54
|
+
export function buildFork(source, name, opts) {
|
|
55
|
+
if (profileExists(source)) {
|
|
56
|
+
return forkProfile(readProfile(source), name, {
|
|
57
|
+
model: opts.model,
|
|
58
|
+
baseUrl: opts.baseUrl,
|
|
59
|
+
provider: opts.authProvider,
|
|
60
|
+
version: opts.version,
|
|
61
|
+
label: opts.label,
|
|
62
|
+
description: opts.description,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
const host = resolveAgentName(source);
|
|
66
|
+
if (!host) {
|
|
67
|
+
throw new Error(`No harness or agent named '${source}'.\n` +
|
|
68
|
+
`Fork from a custom harness (agents harness list) or a native one: ${ALL_AGENT_IDS.join(', ')}.`);
|
|
69
|
+
}
|
|
70
|
+
if (!opts.model) {
|
|
71
|
+
throw new Error(`--model <id> is required when forking the native '${host}' harness (there is no model to inherit).`);
|
|
72
|
+
}
|
|
73
|
+
return profileFromHostModel(name, host, opts.model, {
|
|
74
|
+
version: opts.version,
|
|
75
|
+
baseUrl: opts.baseUrl,
|
|
76
|
+
provider: opts.authProvider,
|
|
77
|
+
authEnvVar: opts.authProvider ? authEnvKeyForHostOrThrow(host) : undefined,
|
|
78
|
+
label: opts.label,
|
|
79
|
+
description: opts.description ?? `Forked from ${host}: ${opts.model}`,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/** Auth env var for a host, as a hard error when the host declares none. */
|
|
83
|
+
function authEnvKeyForHostOrThrow(host) {
|
|
84
|
+
const key = authEnvKeyForHost(host);
|
|
85
|
+
if (!key) {
|
|
86
|
+
throw new Error(`--auth-provider is set but host '${host}' has no known auth env var; it manages its own login.`);
|
|
87
|
+
}
|
|
88
|
+
return key;
|
|
89
|
+
}
|
|
23
90
|
export function registerHarnessCommands(program) {
|
|
24
91
|
const cmd = program
|
|
25
92
|
.command('harness')
|
|
@@ -30,11 +97,18 @@ A custom harness pins a host CLI (opencode, claude, codex, grok, antigravity, ..
|
|
|
30
97
|
model and gives it a name. 'agents run <name>' then behaves like a native agent
|
|
31
98
|
type, and 'agents repo push user' syncs it to every device.
|
|
32
99
|
|
|
100
|
+
A custom harness is its own agent type in 'agents view' — its own block beside Claude
|
|
101
|
+
and Codex, not a row indented under the host CLI that executes it.
|
|
102
|
+
|
|
33
103
|
Examples:
|
|
34
104
|
# Meta Muse Spark 1.1 through OpenCode, called 'spark'
|
|
35
105
|
agents harness add spark --host opencode --model meta/muse-spark-1.1
|
|
36
106
|
agents run spark "refactor api/handlers/checkout.py"
|
|
37
107
|
|
|
108
|
+
# Fork a native harness, or copy one of your own and swap the model
|
|
109
|
+
agents harness fork opencode deepseek --model deepseek/deepseek-v4-flash-0731 --auth-provider openrouter
|
|
110
|
+
agents harness fork deepseek deepseek-chat --model deepseek/deepseek-chat-v3
|
|
111
|
+
|
|
38
112
|
# Per-run model override still wins
|
|
39
113
|
agents run spark --model opencode/big-pickle "quick pass"
|
|
40
114
|
|
|
@@ -66,6 +140,48 @@ Examples:
|
|
|
66
140
|
process.exit(1);
|
|
67
141
|
}
|
|
68
142
|
});
|
|
143
|
+
cmd
|
|
144
|
+
.command('fork <source> <name>')
|
|
145
|
+
.description('Fork a native harness (claude, opencode, ...) or an existing custom one into a new named harness.')
|
|
146
|
+
.option('--model <id>', 'Model to pin on the fork (required when forking a native harness)')
|
|
147
|
+
.option('--base-url <url>', 'Custom endpoint base URL (claude/codex hosts)')
|
|
148
|
+
.option('--auth-provider <provider>', 'Attach a keychain-backed API key under this provider')
|
|
149
|
+
.option('--version <version>', 'Pin the host CLI version (e.g., 1.16.0)')
|
|
150
|
+
.option('--label <text>', 'Human-facing name shown by `agents view` (defaults to <name>)')
|
|
151
|
+
.option('--description <text>', 'One-line description')
|
|
152
|
+
.option('--key-stdin', 'Read the API key from stdin instead of prompting (for scripts/CI)')
|
|
153
|
+
.option('--force', 'Overwrite an existing harness with the same name')
|
|
154
|
+
.addHelpText('after', `
|
|
155
|
+
Examples:
|
|
156
|
+
# Fork OpenCode into a harness pinned to a DeepSeek model on OpenRouter
|
|
157
|
+
agents harness fork opencode deepseek --model deepseek/deepseek-v4-flash-0731 --auth-provider openrouter
|
|
158
|
+
|
|
159
|
+
# Fork Claude Code onto a private gateway
|
|
160
|
+
agents harness fork claude corp --model gpt-x --base-url https://gw.corp/v1 --auth-provider corp
|
|
161
|
+
|
|
162
|
+
# Copy an existing harness and swap only the model
|
|
163
|
+
agents harness fork deepseek deepseek-chat --model deepseek/deepseek-chat-v3
|
|
164
|
+
`)
|
|
165
|
+
.action(async (source, name, opts) => {
|
|
166
|
+
try {
|
|
167
|
+
validateProfileName(name);
|
|
168
|
+
if (profileExists(name) && !opts.force) {
|
|
169
|
+
throw new Error(`Harness '${name}' already exists. Use --force to overwrite.`);
|
|
170
|
+
}
|
|
171
|
+
// Build first so a bad source/flag combination fails before prompting
|
|
172
|
+
// for a key the user would then have stored for nothing.
|
|
173
|
+
const forked = buildFork(source, name, opts);
|
|
174
|
+
if (opts.authProvider)
|
|
175
|
+
await ensureProviderToken(opts.authProvider, undefined, opts.keyStdin);
|
|
176
|
+
writeProfile(forked);
|
|
177
|
+
console.log(chalk.green(`Harness '${name}' forked from ${source}.`));
|
|
178
|
+
console.log(chalk.gray(`Try: agents run ${name} "hello"`));
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
console.error(chalk.red(err.message));
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
69
185
|
cmd
|
|
70
186
|
.command('list')
|
|
71
187
|
.alias('ls')
|
|
@@ -114,16 +230,7 @@ Examples:
|
|
|
114
230
|
.description('Show one custom harness (host, model, provider, auth, path).')
|
|
115
231
|
.action((name) => {
|
|
116
232
|
try {
|
|
117
|
-
|
|
118
|
-
console.log(chalk.bold(p.name));
|
|
119
|
-
if (p.description)
|
|
120
|
-
console.log(chalk.gray(p.description));
|
|
121
|
-
console.log('');
|
|
122
|
-
console.log(`Host: ${profileHostLabel(p)}`);
|
|
123
|
-
console.log(`Model: ${profileModelLabel(p)}`);
|
|
124
|
-
console.log(`Provider: ${profileProviderLabel(p)}`);
|
|
125
|
-
console.log(`Auth: ${profileAuthLabel(p)}`);
|
|
126
|
-
console.log(chalk.gray(getProfilePath(p.name)));
|
|
233
|
+
renderHarnessDetail(name);
|
|
127
234
|
}
|
|
128
235
|
catch (err) {
|
|
129
236
|
console.error(chalk.red(err.message));
|
|
@@ -15,6 +15,9 @@ import { type Preset } from '../lib/profiles-presets.js';
|
|
|
15
15
|
* without mocking @inquirer/prompts.
|
|
16
16
|
*/
|
|
17
17
|
export declare function buildProfileFromCollection(name: string, preset: Preset, collected: Record<string, string>, version?: string): Profile;
|
|
18
|
+
/** Read all available data from stdin synchronously, trimmed. */
|
|
19
|
+
/** Ensure a provider API key exists in keychain, prompting or reading stdin if missing. */
|
|
20
|
+
export declare function ensureProviderToken(provider: string, signupUrl?: string, fromStdin?: boolean): Promise<void>;
|
|
18
21
|
/** Options accepted by {@link addProfile} — shared by `agents profiles add` and `agents harness add`. */
|
|
19
22
|
export interface AddProfileOptions {
|
|
20
23
|
preset?: string;
|
|
@@ -47,7 +47,7 @@ async function promptForSecret(message) {
|
|
|
47
47
|
}
|
|
48
48
|
/** Read all available data from stdin synchronously, trimmed. */
|
|
49
49
|
/** Ensure a provider API key exists in keychain, prompting or reading stdin if missing. */
|
|
50
|
-
async function ensureProviderToken(provider, signupUrl, fromStdin) {
|
|
50
|
+
export async function ensureProviderToken(provider, signupUrl, fromStdin) {
|
|
51
51
|
const item = keychainItemName(provider);
|
|
52
52
|
if (hasKeychainToken(item)) {
|
|
53
53
|
return;
|
|
@@ -14,10 +14,29 @@ import type { DeviceRegistry } from '../lib/devices/registry.js';
|
|
|
14
14
|
* callers drop it straight into a status line.
|
|
15
15
|
*/
|
|
16
16
|
export declare function formatRunDuration(startedAt: string, completedAt: string | null): string;
|
|
17
|
+
/**
|
|
18
|
+
* The last run THIS device can speak for.
|
|
19
|
+
*
|
|
20
|
+
* A run record is written by whichever daemon fired the routine, into that
|
|
21
|
+
* machine's own runs dir — records carry no device attribution, so a record
|
|
22
|
+
* found here only ever describes this device's history. When a routine is
|
|
23
|
+
* pinned away from this machine (`devices:` excludes it) any local record is a
|
|
24
|
+
* leftover from before the pin, and reporting it as the routine's status paints
|
|
25
|
+
* another device's healthy routine red. Report nothing instead; the local
|
|
26
|
+
* history stays readable via `agents routines runs <name>`, and the owning
|
|
27
|
+
* device's status via `agents routines list --device <name>`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function localLatestRun(job: JobConfig): RunMeta | null;
|
|
17
30
|
export interface RoutineListGroup {
|
|
18
31
|
key: string;
|
|
19
32
|
title: string;
|
|
20
33
|
jobs: JobConfig[];
|
|
34
|
+
/**
|
|
35
|
+
* Whether this device's run records describe the group. False for a
|
|
36
|
+
* `Device: <peer>` group — the rows are the same routine seen from a machine
|
|
37
|
+
* that does not fire it there, so Last Status is not ours to report.
|
|
38
|
+
*/
|
|
39
|
+
local: boolean;
|
|
21
40
|
}
|
|
22
41
|
export declare function groupRoutineJobsByDevice(jobs: JobConfig[], registry: DeviceRegistry, self?: string): RoutineListGroup[];
|
|
23
42
|
export declare function buildRunsJson(runs: RunMeta[]): Record<string, unknown>[];
|
|
@@ -138,6 +138,21 @@ function deviceLabel(job, width) {
|
|
|
138
138
|
: raw;
|
|
139
139
|
return { raw, display, dim: full.length === 0 || !jobRunsOnThisDevice(job) };
|
|
140
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* The last run THIS device can speak for.
|
|
143
|
+
*
|
|
144
|
+
* A run record is written by whichever daemon fired the routine, into that
|
|
145
|
+
* machine's own runs dir — records carry no device attribution, so a record
|
|
146
|
+
* found here only ever describes this device's history. When a routine is
|
|
147
|
+
* pinned away from this machine (`devices:` excludes it) any local record is a
|
|
148
|
+
* leftover from before the pin, and reporting it as the routine's status paints
|
|
149
|
+
* another device's healthy routine red. Report nothing instead; the local
|
|
150
|
+
* history stays readable via `agents routines runs <name>`, and the owning
|
|
151
|
+
* device's status via `agents routines list --device <name>`.
|
|
152
|
+
*/
|
|
153
|
+
export function localLatestRun(job) {
|
|
154
|
+
return jobRunsOnThisDevice(job) ? getLatestRun(job.name) : null;
|
|
155
|
+
}
|
|
141
156
|
export function groupRoutineJobsByDevice(jobs, registry, self = machineId()) {
|
|
142
157
|
const groups = new Map();
|
|
143
158
|
const add = (key, title, job) => {
|
|
@@ -146,7 +161,9 @@ export function groupRoutineJobsByDevice(jobs, registry, self = machineId()) {
|
|
|
146
161
|
existing.jobs.push(job);
|
|
147
162
|
return;
|
|
148
163
|
}
|
|
149
|
-
|
|
164
|
+
// `device:` is the only group that describes a machine other than this one,
|
|
165
|
+
// so it is the only one whose rows this device cannot report a status for.
|
|
166
|
+
groups.set(key, { key, title, jobs: [job], local: !key.startsWith('device:') });
|
|
150
167
|
};
|
|
151
168
|
for (const job of jobs) {
|
|
152
169
|
const strategy = resolveHostStrategy(job);
|
|
@@ -193,7 +210,7 @@ export function groupRoutineJobsByDevice(jobs, registry, self = machineId()) {
|
|
|
193
210
|
};
|
|
194
211
|
return [...groups.values()].sort((a, b) => order(a) - order(b) || a.title.localeCompare(b.title));
|
|
195
212
|
}
|
|
196
|
-
function renderRoutineRows({ jobs, scheduler, overdueSet, link, now }) {
|
|
213
|
+
function renderRoutineRows({ jobs, scheduler, overdueSet, link, now, local = true }) {
|
|
197
214
|
const NAME_W = 24;
|
|
198
215
|
const AGENT_W = 10;
|
|
199
216
|
const REPO_W = REPO_DISPLAY_MAX;
|
|
@@ -207,7 +224,7 @@ function renderRoutineRows({ jobs, scheduler, overdueSet, link, now }) {
|
|
|
207
224
|
for (const job of jobs) {
|
|
208
225
|
const nextStr = nextRunLabel(job, scheduler, now);
|
|
209
226
|
const schedStr = scheduleLabel(job);
|
|
210
|
-
const latestRun =
|
|
227
|
+
const latestRun = local ? localLatestRun(job) : null;
|
|
211
228
|
const lastStatus = latestRun?.status || '-';
|
|
212
229
|
const sourceRepo = job.source?.repo ?? job.repo;
|
|
213
230
|
const sourceLabel = sourceRepo
|
|
@@ -509,7 +526,7 @@ export function registerRoutinesCommands(program) {
|
|
|
509
526
|
if (options.json) {
|
|
510
527
|
const nowJson = new Date();
|
|
511
528
|
const payload = jobs.map((job) => {
|
|
512
|
-
const latestRun =
|
|
529
|
+
const latestRun = localLatestRun(job);
|
|
513
530
|
return {
|
|
514
531
|
name: job.name,
|
|
515
532
|
agent: job.agent ?? null,
|
|
@@ -562,9 +579,14 @@ export function registerRoutinesCommands(program) {
|
|
|
562
579
|
catch (err) {
|
|
563
580
|
console.error(chalk.yellow(`Could not read device registry: ${err.message}`));
|
|
564
581
|
}
|
|
565
|
-
|
|
582
|
+
const groups = groupRoutineJobsByDevice(jobs, registry);
|
|
583
|
+
for (const group of groups) {
|
|
566
584
|
console.log(chalk.bold(`\n${group.title}`));
|
|
567
|
-
renderRoutineRows({ jobs: group.jobs, scheduler, overdueSet, link, now });
|
|
585
|
+
renderRoutineRows({ jobs: group.jobs, scheduler, overdueSet, link, now, local: group.local });
|
|
586
|
+
}
|
|
587
|
+
if (groups.some((group) => !group.local)) {
|
|
588
|
+
console.log();
|
|
589
|
+
console.log(chalk.gray(' Last Status is per-device: rows under another device show "-" — read it there with: agents routines list --device <name>'));
|
|
568
590
|
}
|
|
569
591
|
}
|
|
570
592
|
if (overdueSet.size > 0) {
|
|
@@ -80,10 +80,19 @@ export declare function exportBundleToFile(env: Record<string, string>, filePath
|
|
|
80
80
|
*/
|
|
81
81
|
export declare function importBundleFromFile(filePath: string, passphrase: string): Record<string, string>;
|
|
82
82
|
/** The POLICY column for `secrets list`: the prompt policy, plus a concise
|
|
83
|
-
* state hint. `
|
|
83
|
+
* state hint. `hold` shows `held Nh` when the secrets-agent is currently
|
|
84
84
|
* caching the bundle; `always` and `never` show whether they prompt. `held`
|
|
85
85
|
* maps bundle name → expiry epoch-ms (from agentStatus()). */
|
|
86
86
|
export declare function renderPolicyCol(b: SecretsBundle, held?: Map<string, number>): string;
|
|
87
|
+
/** The hold-window line at the top of `secrets status`. Names the `hold` policy
|
|
88
|
+
* the window belongs to — the rename in #1604 left this surface still saying
|
|
89
|
+
* "daily", the one name the CLI no longer accepts in its own help. Pure so the
|
|
90
|
+
* vocabulary is pinned by a test rather than re-drifting on the next rename. */
|
|
91
|
+
export declare function renderHoldSummary(holdStr: string, configured: boolean): string;
|
|
92
|
+
/** The empty-broker line under the hold summary. Named here, beside
|
|
93
|
+
* `renderHoldSummary`, for the same reason: it is the second line the rename
|
|
94
|
+
* left saying `daily`, and a test pins both. */
|
|
95
|
+
export declare const NO_BUNDLES_HELD_LINE = "No bundles held. The next read of each hold-policy bundle will prompt once, then hold.";
|
|
87
96
|
/** Human-readable hold window for `secrets status`. Sub-hour values render in
|
|
88
97
|
* minutes (so a near-floor `holdMs` never shows a confusing "0 hours"), whole
|
|
89
98
|
* hours up to 2 days, whole days beyond. Pure — unit-tested. */
|