@phnx-labs/agents-cli 1.22.88 → 1.22.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 +159 -0
- package/dist/lib/accounts/migrate.js +99 -0
- package/dist/lib/answer-router.d.ts +8 -0
- package/dist/lib/answer-router.js +13 -1
- package/dist/lib/auth-mint.d.ts +22 -1
- package/dist/lib/auth-mint.js +85 -10
- package/dist/lib/daemon/attention-notify-service.d.ts +66 -0
- package/dist/lib/daemon/attention-notify-service.js +185 -0
- package/dist/lib/daemon/auth-sync-service.js +10 -0
- package/dist/lib/daemon/daemon.js +9 -0
- package/dist/lib/daemon/self-update-service.d.ts +9 -0
- package/dist/lib/daemon/self-update-service.js +51 -6
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +5 -0
- package/dist/lib/feed/answer.js +1 -1
- package/dist/lib/feed/attention.d.ts +2 -0
- package/dist/lib/feed/attention.js +0 -0
- package/dist/lib/fs-atomic.d.ts +7 -0
- package/dist/lib/fs-atomic.js +20 -0
- package/dist/lib/menubar/notify-desktop.d.ts +27 -0
- package/dist/lib/menubar/notify-desktop.js +11 -0
- package/dist/lib/run-notify.d.ts +4 -0
- package/dist/lib/run-notify.js +21 -2
- package/dist/lib/secrets-policy.d.ts +17 -0
- package/dist/lib/secrets-policy.js +14 -1
- package/dist/lib/self-update.d.ts +37 -11
- package/dist/lib/self-update.js +77 -20
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,164 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.22.90
|
|
4
|
+
|
|
5
|
+
- **`accounts migrate --apply` no longer aborts on a worker whose accounts already hold slots (PHNX-3940).** The daemon's auth-sync provisions a durable slot per account on every worker, so `--apply` hit `Slot already exists` on the first credential-bearing legacy home and cleaned nothing. The plan now routes such a home to `trash` (restorable with `agents trash restore`) with the reason "account already holds a provisioned slot on this device", rewrites any `<harness>@<label>` binding to the account, and clears the stale home record; a canonical install in that state keeps its binary, has its stale home moved to `~/.agents/.history/trash/homes/…` (manifest key `<harness>@<label>#home`), and gets an empty home back. A regular file at the slot path is still not a slot, so the apply-time guard still fails loud on that corruption. Source: `cli/src/lib/accounts/migrate.ts`.
|
|
6
|
+
|
|
7
|
+
- **AGI Menu: Sessions window, feed-driven RECENT, Notifications submenu (PHNX-4003).**
|
|
8
|
+
`Cmd-Shift-I` (or the new **Sessions** row at the top of the dropdown) opens a
|
|
9
|
+
persistent Sessions window: live sessions across the fleet grouped by project on
|
|
10
|
+
the left, with **Recent / Needs you / Running / Done today** filter chips, a
|
|
11
|
+
`Cmd-F` search (label, topic, project, ticket, device), and a device strip that
|
|
12
|
+
marks stale heartbeats; a session card on the right shows the phase, PR chip,
|
|
13
|
+
request excerpt, progress tally, latest action, today's rendered artifacts with an
|
|
14
|
+
**Open** button, and the attention block when the agent is waiting — numbered
|
|
15
|
+
option buttons, permission Allow/Deny, plan-review Approve/Send back — answered
|
|
16
|
+
atomically through `agents feed answer`. A reply field routes by capability
|
|
17
|
+
(`agents feed answer`, `agents sessions inject`, `agents cloud message`,
|
|
18
|
+
`agents teams message`) and shows the CLI's own result inline; keys `1`–`9`,
|
|
19
|
+
`Return`, `Cmd-Return`, `Cmd-K` answer, reply, open the terminal, or nudge. The
|
|
20
|
+
dropdown's RECENT section now renders the same feed-driven rows (status dot,
|
|
21
|
+
`n/N` checklist, `working · 18h · host`, latest action + PR chip) grouped by
|
|
22
|
+
project, each opening the window on that session, and a **Notifications**
|
|
23
|
+
submenu lists today's feed banners unanswered-first. Blocked sessions leave the
|
|
24
|
+
NEEDS YOU strip, which keeps only load, scheduler, and routine triage.
|
|
25
|
+
Source: `cli/menubar/Sources/MenubarHelper/SessionsWindow.swift`,
|
|
26
|
+
`cli/menubar/Sources/MenubarHelper/SessionCard.swift`,
|
|
27
|
+
`cli/menubar/Sources/MenubarHelper/SessionRowModel.swift`,
|
|
28
|
+
`cli/menubar/Sources/MenubarHelper/Reply.swift`,
|
|
29
|
+
`cli/menubar/Sources/MenubarHelper/RecentSectionBuilder.swift`.
|
|
30
|
+
|
|
31
|
+
- **AGI Menu delivers attention banners on `UNUserNotificationCenter` with real
|
|
32
|
+
action buttons (PHNX-4004).** The deprecated `NSUserNotification` toast is
|
|
33
|
+
replaced for actionable notices: the banner carries the mapped category, a
|
|
34
|
+
time-sensitive interruption level for a blocked ask, a per-session thread so a
|
|
35
|
+
session's banners group, and the harness avatar as an attachment. The
|
|
36
|
+
persistent menu-bar instance is the delegate; a tapped action runs one bounded
|
|
37
|
+
`agents feed answer` / `agents open` command off the delegate thread and posts
|
|
38
|
+
a "Could not deliver your reply" follow-up when it fails, or a "Could not open
|
|
39
|
+
the report/PR" follow-up when a done banner carried no link for that button.
|
|
40
|
+
When macOS declines notification authorization it degrades to the old plain
|
|
41
|
+
banner so a notice is never lost.
|
|
42
|
+
Source: `menubar/Sources/MenubarHelper/Notifier.swift`,
|
|
43
|
+
`menubar/Sources/MenubarHelper/AgentAvatar.swift`.
|
|
44
|
+
|
|
45
|
+
- **AGI Menu palette: dispatch form with remembered defaults, project pulse, and session-id minting (PHNX-4005).**
|
|
46
|
+
The `Cmd-Shift-O` palette now carries the dimensions a dispatch actually needs
|
|
47
|
+
below its text field — **mode** (Plan / Auto / Edit), **run on** (This Mac,
|
|
48
|
+
`auto`, or a fleet device by name), **surface** (Interactive / Headless), and
|
|
49
|
+
**watchdog** (Off / Keep moving / Hands-off) — collapsed to one summary line
|
|
50
|
+
(`Claude 2.1.263 · this-mac · agi · Auto · Interactive · Keep moving`) with a
|
|
51
|
+
`▸ details` disclosure. Every dimension is **remembered per project**, so
|
|
52
|
+
switching projects restores the agent, mode, and target you last used there and
|
|
53
|
+
one keystroke dispatches. `Cmd-P` switches **this one dispatch** to Plan without
|
|
54
|
+
touching the remembered defaults. A Claude dispatch mints its session id and
|
|
55
|
+
passes `--session-id`, which lets the palette apply a non-default watchdog policy
|
|
56
|
+
at dispatch and register a **launching placeholder** with the feed stream: the
|
|
57
|
+
row shows in the project pulse strip as `◌ launching · <name>` (and in any
|
|
58
|
+
window that reads the feed rows) until the real session reports, and reads
|
|
59
|
+
"did not start" — with the CLI's own stderr — if the launch exits non-zero or
|
|
60
|
+
no row arrives within 60 s. Under the project row a **pulse strip** shows the
|
|
61
|
+
headline milestone from `agents projects status`, open tickets and PRs, live
|
|
62
|
+
running / need-you / idle counts with NOW chips (not-progressing first), and
|
|
63
|
+
the NEXT UP ticket (`Cmd-N` attaches).
|
|
64
|
+
Source: `cli/menubar/Sources/MenubarHelper/DispatchForm.swift`,
|
|
65
|
+
`cli/menubar/Sources/MenubarHelper/DispatchDefaults.swift`,
|
|
66
|
+
`cli/menubar/Sources/MenubarHelper/ProjectPulse.swift`,
|
|
67
|
+
`cli/menubar/Sources/MenubarHelper/FeedStream.swift`,
|
|
68
|
+
`cli/menubar/Sources/MenubarHelper/AgentsCLI.swift`.
|
|
69
|
+
|
|
70
|
+
- **AGI Menu palette: find a screenshot by the text on it (PHNX-4006).**
|
|
71
|
+
Every capture in the palette's screenshot folders (the system `screencapture`
|
|
72
|
+
location, CleanShot's export path, and the clip attachments dir) is now OCR'd
|
|
73
|
+
once, on-device, and indexed so you can search screenshots by the words on them.
|
|
74
|
+
With the palette key, `Cmd-Shift-F` focuses a search field above the strip:
|
|
75
|
+
typing filters the thumbnails to captures whose recognized text matches every
|
|
76
|
+
token (case-insensitive, tokenized AND), newest first, with an `n of m match`
|
|
77
|
+
hint; clearing it restores the newest-six strip. A **more** disclosure expands
|
|
78
|
+
the strip into a grid grouped by hour (last 24 h) then day — each cell shows the
|
|
79
|
+
thumbnail, the time, and the first recognized line — where a click attaches and a
|
|
80
|
+
double-click previews the full image in Preview.app. Nothing leaves the machine:
|
|
81
|
+
recognition runs through Vision locally (`.fast`, no language correction) on a
|
|
82
|
+
background queue, images over 20 MB are skipped and the rest downsampled to
|
|
83
|
+
2000 px before recognition, each capture is keyed by the SHA-256 of its bytes so
|
|
84
|
+
a rename or duplicate is never OCR'd twice, a deleted capture drops out of the
|
|
85
|
+
index (search and grid never return it, and the next scan prunes its row), and
|
|
86
|
+
the index is a local SQLite file at `~/.agents/.history/menubar/screenshots.db`.
|
|
87
|
+
Source:
|
|
88
|
+
`cli/menubar/Sources/MenubarHelper/ScreenshotIndex.swift`,
|
|
89
|
+
`cli/menubar/Sources/MenubarHelper/ScreenshotGrid.swift`,
|
|
90
|
+
`cli/menubar/Sources/MenubarHelper/PromptPanel.swift`.
|
|
91
|
+
|
|
92
|
+
## 1.22.89
|
|
93
|
+
|
|
94
|
+
- **Worker daemons now actually relaunch onto a new agents-cli release, so a fix
|
|
95
|
+
that ships reaches every worker without a hand `agents daemon restart`
|
|
96
|
+
(PHNX-3940 rollout).** Two defects kept every fleet daemon on the code it
|
|
97
|
+
booted with: `resolveRunningPackageRoot(__dirname)` resolved one level up from
|
|
98
|
+
the calling module, which from `dist/lib/daemon/self-update-service.js` is
|
|
99
|
+
`dist/lib`, so every self-update tick failed with `… is not an npm-managed
|
|
100
|
+
install` (eight Linux workers logged it for days and ran 1.22.79 code while the
|
|
101
|
+
disk sat at 1.22.88); and the tick only exited after an install it performed
|
|
102
|
+
itself, so an install written by an operator's `agents` command never relaunched
|
|
103
|
+
the daemon, and on a box with a second `agents` on PATH the tick declined
|
|
104
|
+
silently. The resolver now walks up to the `package.json` naming this package;
|
|
105
|
+
the tick compares the on-disk version with the version it booted with and, when
|
|
106
|
+
the disk is newer and the install has settled (`package.json` at rest for a
|
|
107
|
+
minute and every `bin` entry present, because bun's write is not atomic), exits
|
|
108
|
+
for the OS-supervisor relaunch without downloading anything. The shadow decline
|
|
109
|
+
is logged once per daemon process, and `auth-sync` logs accounts skipped
|
|
110
|
+
because their durable key is not readable on the box. Source:
|
|
111
|
+
`cli/src/lib/self-update.ts`, `cli/src/lib/daemon/self-update-service.ts`,
|
|
112
|
+
`cli/src/lib/daemon/auth-sync-service.ts`.
|
|
113
|
+
|
|
114
|
+
- **AGI Menu palette: real projects, Linear from the binding, pin, live screenshots, image paste (PHNX-4001).**
|
|
115
|
+
The `Cmd-Shift-O` palette now picks a **project**, not a directory: it lists every
|
|
116
|
+
`agents projects` definition by name (with a type-ahead filter and the root as a
|
|
117
|
+
tooltip) instead of the last eight session working directories, so a project you
|
|
118
|
+
have not worked in today is still one keystroke away, and two definitions that
|
|
119
|
+
share a checkout — `prix` and `rush` both live in the same monorepo — stay two
|
|
120
|
+
entries. Dispatch passes `--project <name>`; recent session cwds now only offer a
|
|
121
|
+
worktree or subdirectory *inside* the chosen project. The ticket list is scoped by
|
|
122
|
+
the project's own Linear **binding** rather than by matching folder names against
|
|
123
|
+
Linear project names, which is why picking `agi` now shows the AGI board instead of
|
|
124
|
+
"no Linear project matches agents-cli"; a project with no binding says so and prints
|
|
125
|
+
`agents projects link <name> --linear`. The palette can be **pinned** (header button,
|
|
126
|
+
or press `Cmd-Shift-O` while it is already focused) so it survives another app
|
|
127
|
+
taking focus. The screenshot strip is **live** — a shot taken while the palette is
|
|
128
|
+
open appears within about a second, with no re-summon — and images can be pasted
|
|
129
|
+
with `Cmd-V` or dropped onto the panel; `Backspace` on a focused thumbnail removes
|
|
130
|
+
it. Attachments now travel to the agent as `<host>:<abs-path>` references (the same
|
|
131
|
+
form `Cmd-Shift-V` types), so a dispatch to another device can fetch them. The
|
|
132
|
+
ticket list starts folded and `Cmd-T` toggles it, remembered. On a machine with no
|
|
133
|
+
projects defined the palette falls back to recent session working directories, and
|
|
134
|
+
with neither a project nor a recent directory it now refuses to dispatch instead of
|
|
135
|
+
launching an agent with no working directory (which resolved to `/`).
|
|
136
|
+
Source: `cli/menubar/Sources/MenubarHelper/PromptPanel.swift`,
|
|
137
|
+
`cli/menubar/Sources/MenubarHelper/ScreenshotWatcher.swift`,
|
|
138
|
+
`cli/menubar/Sources/MenubarHelper/LinearTickets.swift`.
|
|
139
|
+
|
|
140
|
+
- **The CLI now posts one actionable desktop banner per new attention item, so a
|
|
141
|
+
session asking a question, a permission prompt, or a plan review is answerable
|
|
142
|
+
straight from the notification (PHNX-4004).** A new supervised `attention-notify`
|
|
143
|
+
daemon service (tick 5 s, reader-independent) reconciles this host's live
|
|
144
|
+
sessions each tick and posts a banner carrying the category, the attention key,
|
|
145
|
+
the session id, and the answerable choices — Approve / Approve for session / Deny
|
|
146
|
+
for a permission, the options plus a typed reply for a question, Approve / Send
|
|
147
|
+
back for a plan review, Open terminal for a stall — which the macOS helper routes
|
|
148
|
+
back through `agents feed answer <key> --choice <id>`. A `deny`/`send-back`
|
|
149
|
+
choice delivers a real Escape keystroke (no trailing Enter) rather than the
|
|
150
|
+
letters "esc", so cancelling a permission or plan prompt never confirms it.
|
|
151
|
+
`agents run --notify` finish banners now carry `category: done|failure`; the
|
|
152
|
+
finish-notification shape also carries the session id and open-report / open-pr
|
|
153
|
+
choices when the caller supplies a session id, report path, or PR url.
|
|
154
|
+
Idempotency is a filesystem ledger under `~/.agents/.history/feed/notified/`,
|
|
155
|
+
pruned at 14 days, so a daemon restart never re-posts a banner already sent. The
|
|
156
|
+
`--notify` argv gains `--category`, `--key`, `--session`, and repeated
|
|
157
|
+
`--choice <id>=<label>` (see `docs/menubar.md` → Actionable notifications).
|
|
158
|
+
Source: `src/lib/menubar/notify-desktop.ts`,
|
|
159
|
+
`src/lib/daemon/attention-notify-service.ts`, `src/lib/feed/attention.ts`,
|
|
160
|
+
`src/lib/answer-router.ts`, `src/lib/run-notify.ts`.
|
|
161
|
+
|
|
3
162
|
## 1.22.88
|
|
4
163
|
|
|
5
164
|
- **`agents run` no longer fails with `secrets request failed: spawnSync sh
|
|
@@ -163,6 +163,30 @@ async function duplicateTrashLabels(items) {
|
|
|
163
163
|
function plannedTrashPath(agent, label) {
|
|
164
164
|
return path.join(getHistoryDir(), 'trash', 'versions', agent, label, '<stamp>');
|
|
165
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Where a canonical install's stale `home/` goes when its account already holds
|
|
168
|
+
* a slot: the binary and the (recreated, empty) home stay under `versions/`, so
|
|
169
|
+
* the old home cannot use the versions trash — `agents trash restore` would put a
|
|
170
|
+
* whole install back over a live one. Restoring is a plain move back; the
|
|
171
|
+
* migration manifest records the path under `<agent>@<label>#home`.
|
|
172
|
+
*/
|
|
173
|
+
function homeTrashDir(agent, label) {
|
|
174
|
+
return path.join(getHistoryDir(), 'trash', 'homes', agent, label);
|
|
175
|
+
}
|
|
176
|
+
function plannedHomeTrashPath(agent, label) {
|
|
177
|
+
return path.join(homeTrashDir(agent, label), '<stamp>');
|
|
178
|
+
}
|
|
179
|
+
function trashHome(agent, item) {
|
|
180
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
181
|
+
const dest = path.join(homeTrashDir(agent, item.label), stamp);
|
|
182
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true, mode: 0o700 });
|
|
183
|
+
if (!fs.existsSync(item.home)) {
|
|
184
|
+
throw new Error(`Home ${item.home} is missing; cannot trash it.`);
|
|
185
|
+
}
|
|
186
|
+
fs.renameSync(item.home, dest);
|
|
187
|
+
fs.mkdirSync(item.home, { recursive: true, mode: 0o700 });
|
|
188
|
+
return dest;
|
|
189
|
+
}
|
|
166
190
|
function emptyDir(dir) {
|
|
167
191
|
if (!fs.existsSync(dir))
|
|
168
192
|
return true;
|
|
@@ -180,6 +204,23 @@ function assertSlotAbsent(agent, accountId) {
|
|
|
180
204
|
}
|
|
181
205
|
return dest;
|
|
182
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* True when this device already holds a populated slot directory for the
|
|
209
|
+
* account. The daemon's auth-sync provisions durable worker slots on its own
|
|
210
|
+
* (PHNX-3940), so by the time an operator runs `--apply` a worker commonly has
|
|
211
|
+
* a slot for every account whose legacy per-version home is still on disk.
|
|
212
|
+
* A regular file at the slot path is NOT a slot: the plan keeps routing such a
|
|
213
|
+
* home to `slot` so the apply-time guard fails loud on the corruption.
|
|
214
|
+
*/
|
|
215
|
+
function provisionedSlotExists(agent, accountId) {
|
|
216
|
+
const dest = slotDir(agent, accountId);
|
|
217
|
+
try {
|
|
218
|
+
return fs.statSync(dest).isDirectory() && fs.readdirSync(dest).length > 0;
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
183
224
|
function takenNames(meta) {
|
|
184
225
|
const taken = new Set();
|
|
185
226
|
for (const row of listNativeAccounts(meta)) {
|
|
@@ -255,6 +296,42 @@ async function planHarness(agent, deps) {
|
|
|
255
296
|
});
|
|
256
297
|
continue;
|
|
257
298
|
}
|
|
299
|
+
if (item.hasCredential && item.accountId && provisionedSlotExists(agent, item.accountId)) {
|
|
300
|
+
// The account already owns a populated slot on this device, so this
|
|
301
|
+
// per-version home is a stale copy of a credential the slot now carries.
|
|
302
|
+
// Routing it to `slot` would only trip `assertSlotAbsent` and abort the
|
|
303
|
+
// whole apply with nothing done — the state every auth-synced worker was
|
|
304
|
+
// in. Trash it instead (`agents trash restore` reverses). The canonical
|
|
305
|
+
// install keeps its binary and ends up with an empty home, exactly as it
|
|
306
|
+
// does when its home is the one that moves into the slot: two on-disk
|
|
307
|
+
// copies of one credential is not an end state the migration leaves.
|
|
308
|
+
if (canonical && item.label === canonical.label) {
|
|
309
|
+
actions.push({
|
|
310
|
+
kind: 'canonical',
|
|
311
|
+
label: item.label,
|
|
312
|
+
release: item.release,
|
|
313
|
+
reason: 'canonical install (binary kept); account already holds a slot — stale home trashed, empty home recreated',
|
|
314
|
+
accountId: item.accountId,
|
|
315
|
+
identityKey: item.identityKey,
|
|
316
|
+
email: item.email,
|
|
317
|
+
sessionCount: item.sessionCount,
|
|
318
|
+
pathMoves: [{ from: item.home, to: plannedHomeTrashPath(agent, item.label) }],
|
|
319
|
+
});
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
actions.push({
|
|
323
|
+
kind: 'trash',
|
|
324
|
+
label: item.label,
|
|
325
|
+
release: item.release,
|
|
326
|
+
reason: 'account already holds a provisioned slot on this device — stale home trashed',
|
|
327
|
+
accountId: item.accountId,
|
|
328
|
+
identityKey: item.identityKey,
|
|
329
|
+
email: item.email,
|
|
330
|
+
sessionCount: item.sessionCount,
|
|
331
|
+
pathMoves: [{ from: item.dir, to: plannedTrashPath(agent, item.label) }],
|
|
332
|
+
});
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
258
335
|
if (item.hasCredential && item.identityKey) {
|
|
259
336
|
const dest = item.accountId ? slotDir(agent, item.accountId) : slotDir(agent, '<new-account>');
|
|
260
337
|
actions.push({
|
|
@@ -494,6 +571,21 @@ export async function applyAccountMigration(agents = [...ALL_AGENT_IDS], deps =
|
|
|
494
571
|
if (kept)
|
|
495
572
|
labelToAccount.set(item.label, kept);
|
|
496
573
|
}
|
|
574
|
+
for (const action of h.actions.filter((a) => a.kind === 'canonical' && a.accountId)) {
|
|
575
|
+
// Canonical install whose account already holds a slot: hand the stale
|
|
576
|
+
// home off to the homes trash and leave an empty home behind, mirroring
|
|
577
|
+
// the slot branch's end state for a canonical home.
|
|
578
|
+
const item = byLabel.get(action.label);
|
|
579
|
+
if (!item)
|
|
580
|
+
throw new Error(`Plan named ${h.agent}@${action.label} but inventory has no such install.`);
|
|
581
|
+
const trashPath = trashHome(h.agent, item);
|
|
582
|
+
remaps.push({ from: item.home, to: trashPath });
|
|
583
|
+
labelToAccount.set(item.label, action.accountId);
|
|
584
|
+
movedAccountIds.push(action.accountId);
|
|
585
|
+
entry.trashed.push({ label: item.label, reason: 'stale canonical home — account already holds a provisioned slot', trashPath });
|
|
586
|
+
manifest.map[`${h.agent}@${item.label}#home`] = trashPath;
|
|
587
|
+
persistManifest(manifestPath, manifest);
|
|
588
|
+
}
|
|
497
589
|
for (const action of trashActions) {
|
|
498
590
|
const item = byLabel.get(action.label);
|
|
499
591
|
if (!item)
|
|
@@ -504,6 +596,13 @@ export async function applyAccountMigration(agents = [...ALL_AGENT_IDS], deps =
|
|
|
504
596
|
removeVersionedAlias(h.agent, item.label);
|
|
505
597
|
remaps.push({ from: item.dir, to: trashPath });
|
|
506
598
|
stillPresent.delete(item.label);
|
|
599
|
+
if (action.accountId) {
|
|
600
|
+
// A home trashed because its account already holds a slot: anything
|
|
601
|
+
// bound to this version label follows the account, and the stale
|
|
602
|
+
// home record clears so spawn resolves through the slot alone.
|
|
603
|
+
labelToAccount.set(item.label, action.accountId);
|
|
604
|
+
movedAccountIds.push(action.accountId);
|
|
605
|
+
}
|
|
507
606
|
entry.trashed.push({ label: item.label, reason: action.reason, trashPath });
|
|
508
607
|
manifest.map[`${h.agent}@${item.label}`] = trashPath;
|
|
509
608
|
persistManifest(manifestPath, manifest);
|
|
@@ -32,6 +32,13 @@ export interface AnswerRoute {
|
|
|
32
32
|
sessionId: string;
|
|
33
33
|
agent: string;
|
|
34
34
|
};
|
|
35
|
+
/**
|
|
36
|
+
* Whether to append Enter after the injected payload. Default (omitted) is
|
|
37
|
+
* true. A cancel keystroke (Escape, from a deny / send-back choice) sets this
|
|
38
|
+
* false — the ESC byte itself dismisses the prompt, and a trailing newline
|
|
39
|
+
* would submit a stray empty line into the composer behind it.
|
|
40
|
+
*/
|
|
41
|
+
enter?: boolean;
|
|
35
42
|
}
|
|
36
43
|
export interface AnswerRouterInput {
|
|
37
44
|
/** Resolved mailbox / session id. */
|
|
@@ -56,6 +63,7 @@ export declare function matchOptionIndex(answer: string, options: Array<Pick<Blo
|
|
|
56
63
|
export declare function keystrokesForAnswer(answer: string, options?: Array<Pick<BlockOption, 'label'>>): {
|
|
57
64
|
payload: string;
|
|
58
65
|
matched: 'option' | 'free-text' | 'other';
|
|
66
|
+
enter?: boolean;
|
|
59
67
|
};
|
|
60
68
|
/** True when the session is waiting on user input (parked on a question/plan). */
|
|
61
69
|
export declare function isParkedOnInput(session: ActiveSession | null | undefined): boolean;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { addressabilityRecoveryHint } from './terminal/resolve.js';
|
|
2
2
|
import { injectTargetFromReplyRail } from './session/inject.js';
|
|
3
|
+
/** The Escape keystroke as its raw control byte — what a PTY/tmux/iterm rail reads as a real Escape. */
|
|
4
|
+
const ESCAPE_KEY = '\u001b';
|
|
3
5
|
/**
|
|
4
6
|
* Match free-text answer against question options.
|
|
5
7
|
* Exact (case-insensitive) > startsWith > includes. Returns 0-based index or -1.
|
|
@@ -31,6 +33,15 @@ export function keystrokesForAnswer(answer, options) {
|
|
|
31
33
|
// AskUserQuestion / plan select-lists are 1-indexed digits.
|
|
32
34
|
return { payload: `${idx + 1}`, matched: 'option' };
|
|
33
35
|
}
|
|
36
|
+
// A symbolic cancel token — the `esc` deliveryKey a deny / send-back choice
|
|
37
|
+
// carries — is the Escape KEY, not the letters "esc". Send the ESC control
|
|
38
|
+
// byte (a real cancel on every raw pty/tmux/iterm rail) and suppress the
|
|
39
|
+
// trailing Enter, so the prompt is dismissed rather than confirmed by a stray
|
|
40
|
+
// newline. Runs after the option match so an option literally labelled "esc"
|
|
41
|
+
// still wins as a selection.
|
|
42
|
+
if (/^(?:esc|escape)$/i.test(answer.trim())) {
|
|
43
|
+
return { payload: ESCAPE_KEY, matched: 'other', enter: false };
|
|
44
|
+
}
|
|
34
45
|
if (options?.length) {
|
|
35
46
|
const otherIdx = options.findIndex((o) => /^other$/i.test((o.label ?? '').trim()));
|
|
36
47
|
if (otherIdx >= 0) {
|
|
@@ -92,7 +103,7 @@ export function resolveAnswerRoute(input) {
|
|
|
92
103
|
session?.question?.options?.map((o) => ({ label: o.label }));
|
|
93
104
|
if (openQ && parked && session) {
|
|
94
105
|
const inject = injectTargetForSession(session);
|
|
95
|
-
const { payload, matched } = keystrokesForAnswer(answer, options);
|
|
106
|
+
const { payload, matched, enter } = keystrokesForAnswer(answer, options);
|
|
96
107
|
if (inject) {
|
|
97
108
|
const kind = inject.backend === 'tmux' ? 'tmux'
|
|
98
109
|
: inject.backend === 'iterm' ? 'iterm'
|
|
@@ -103,6 +114,7 @@ export function resolveAnswerRoute(input) {
|
|
|
103
114
|
reason: `Parked on open question — drive ${inject.backend} selection (${matched}).`,
|
|
104
115
|
payload,
|
|
105
116
|
inject,
|
|
117
|
+
...(enter === false ? { enter: false } : {}),
|
|
106
118
|
};
|
|
107
119
|
}
|
|
108
120
|
// Headless / no rail: re-enter via resume.
|
package/dist/lib/auth-mint.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* stored and optionally synced. Interactive mint is Claude-only; every other
|
|
14
14
|
* harness fails loud with the command that actually provisions it.
|
|
15
15
|
*/
|
|
16
|
-
import type { AgentId } from './types.js';
|
|
16
|
+
import type { AgentId, Meta } from './types.js';
|
|
17
17
|
import { type CredentialAccount } from './account-registry.js';
|
|
18
18
|
import { type StorableCredentialKind } from './secrets-policy.js';
|
|
19
19
|
import { type DriveOptions, type PtyDriver } from './fleet/remote-login.js';
|
|
@@ -110,6 +110,27 @@ export declare function seedReservedStoreKey(harness: AgentId, kind: StorableCre
|
|
|
110
110
|
bundle: string;
|
|
111
111
|
key: string;
|
|
112
112
|
};
|
|
113
|
+
export interface AdoptLegacyReservedItemsResult {
|
|
114
|
+
adopted: Array<{
|
|
115
|
+
bundle: string;
|
|
116
|
+
key: string;
|
|
117
|
+
}>;
|
|
118
|
+
errors: Array<{
|
|
119
|
+
bundle: string;
|
|
120
|
+
key: string;
|
|
121
|
+
message: string;
|
|
122
|
+
}>;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Adopt reserved-store keys written by 1.22.84–1.22.89 as bare file items (no
|
|
126
|
+
* bundle record) into the file-backed bundle the daemon push reads. Every
|
|
127
|
+
* `accounts add codex|grok|cursor` from those releases left its worker key in
|
|
128
|
+
* that shape on the laptop, unpushable; the item name is the same in both
|
|
129
|
+
* shapes, so adoption re-seeds the bundle from the raw value in place and
|
|
130
|
+
* nothing already on a worker changes. Idempotent: a key the bundle already
|
|
131
|
+
* carries is skipped. Local data repair only — it never talks to a peer.
|
|
132
|
+
*/
|
|
133
|
+
export declare function adoptLegacyReservedStoreItems(meta: Pick<Meta, 'accounts' | 'deviceAccounts'>): AdoptLegacyReservedItemsResult;
|
|
113
134
|
/**
|
|
114
135
|
* Create or rotate the named provider account that `agents run --account` and
|
|
115
136
|
* `agents accounts sync` consume. Existing account of a different kind fails
|
package/dist/lib/auth-mint.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { resolveAgentName } from './agents.js';
|
|
2
|
-
import { AUTH_BUNDLE, claudeAccountTokenKey, isValidClaudeSetupToken, readClaudeAccountEmail, invalidateClaudeSetupTokenCache, } from './claude-account-token.js';
|
|
3
|
-
import { addAccount, findAccount, readAccountRegistry, setAccountSecret } from './account-registry.js';
|
|
4
|
-
import { bundleBackendSync, bundleExists, bundleExistsSync, keychainRef, pushBundleToHost, readBundleSync, rotateBundleSecretSync, secretsKeychainItem,
|
|
2
|
+
import { AUTH_BUNDLE, claudeAccountTokenKey, isValidClaudeSetupToken, readClaudeAccountEmail, readReservedCredential, invalidateClaudeSetupTokenCache, } from './claude-account-token.js';
|
|
3
|
+
import { addAccount, findAccount, listNativeAccounts, readAccountRegistry, setAccountSecret } from './account-registry.js';
|
|
4
|
+
import { bundleBackendSync, isSecretsClientError, bundleExists, bundleExistsSync, keychainRef, pushBundleToHost, readBundleSync, rotateBundleSecretSync, secretsKeychainItem, writeBundleWithItemsSync, } from './secrets-client.js';
|
|
5
5
|
import { assertStorableCredentialKind, reservedStoreName, } from './secrets-policy.js';
|
|
6
6
|
import { getBinaryPath, getGlobalDefault, getVersionHomePath, listInstalledVersions } from './installations/versions.js';
|
|
7
7
|
import { shellQuote } from './ssh-exec.js';
|
|
@@ -243,13 +243,88 @@ export function seedReservedStoreKey(harness, kind, key, value) {
|
|
|
243
243
|
const cleaned = value.trim();
|
|
244
244
|
if (!cleaned)
|
|
245
245
|
throw new Error(`Empty ${kind} for reserved store '${name}' key ${key}.`);
|
|
246
|
-
// A `__<harness>__` reserved store is
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
246
|
+
// A `__<harness>__` reserved store is a real FILE-backed, policy-`never` bundle
|
|
247
|
+
// — the same shape as the legacy `auth` bundle — because the only transport to
|
|
248
|
+
// a worker is the ordinary bundle push (`syncReservedStores` →
|
|
249
|
+
// `pushBundleToHost`), which reads the store as a bundle and imports it
|
|
250
|
+
// remotely as one. It used to be written as a bare `store.set` item with no
|
|
251
|
+
// bundle record (the standalone rejected the `__`-wrapped name at the time), so
|
|
252
|
+
// the push could never read it and no worker received a Cursor/Codex/Grok key.
|
|
253
|
+
// The item name is unchanged (`agents-cli.secrets.__<harness>__.<KEY>`), so
|
|
254
|
+
// `readReservedCredential` and every worker that already holds the raw item
|
|
255
|
+
// keep resolving it. Requires -labs/secrets-cli >= 0.1.1 (reserved-shape
|
|
256
|
+
// bundle names).
|
|
257
|
+
const item = secretsKeychainItem(name, key);
|
|
258
|
+
try {
|
|
259
|
+
if (bundleExistsSync(name)) {
|
|
260
|
+
const backend = bundleBackendSync(name);
|
|
261
|
+
if (backend !== 'file') {
|
|
262
|
+
throw new Error(`Reserved store '${name}' exists with backend '${backend}', but worker provisioning only reads a FILE-backed store. Recreate it with: agents secrets delete ${name} --yes`);
|
|
263
|
+
}
|
|
264
|
+
const bundle = readBundleSync(name);
|
|
265
|
+
if (key in bundle.vars) {
|
|
266
|
+
rotateBundleSecretSync(bundle, key, { newValue: cleaned, meta: { type: 'token' } });
|
|
267
|
+
return { bundle: name, key };
|
|
268
|
+
}
|
|
269
|
+
bundle.vars[key] = keychainRef(key);
|
|
270
|
+
if (!bundle.meta)
|
|
271
|
+
bundle.meta = {};
|
|
272
|
+
bundle.meta[key] = { type: 'token' };
|
|
273
|
+
writeBundleWithItemsSync(bundle, new Map([[item, cleaned]]));
|
|
274
|
+
return { bundle: name, key };
|
|
275
|
+
}
|
|
276
|
+
const bundle = {
|
|
277
|
+
name,
|
|
278
|
+
backend: 'file',
|
|
279
|
+
policy: 'never',
|
|
280
|
+
description: `Reserved ${harness} worker credentials (${kind}), one key per account; pushed to worker devices by the daemon. Never a native OAuth session.`,
|
|
281
|
+
vars: { [key]: keychainRef(key) },
|
|
282
|
+
meta: { [key]: { type: 'token' } },
|
|
283
|
+
};
|
|
284
|
+
writeBundleWithItemsSync(bundle, new Map([[item, cleaned]]));
|
|
285
|
+
return { bundle: name, key };
|
|
286
|
+
}
|
|
287
|
+
catch (err) {
|
|
288
|
+
// A standalone older than 0.1.1 rejects the `__<harness>__` bundle name and
|
|
289
|
+
// the client only sees a sanitized code. Name the real requirement instead
|
|
290
|
+
// of leaving `accounts add` to fail with `OPERATION_FAILED`.
|
|
291
|
+
if (isSecretsClientError(err, 'OPERATION_FAILED')) {
|
|
292
|
+
throw new Error(`Could not write the reserved store '${name}' as a bundle: ${err.message}. agents-cli needs @phnx-labs/secrets-cli 0.1.1 or newer, which accepts the __<harness>__ bundle name: npm i -g @phnx-labs/secrets-cli@latest`);
|
|
293
|
+
}
|
|
294
|
+
throw err;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Adopt reserved-store keys written by 1.22.84–1.22.89 as bare file items (no
|
|
299
|
+
* bundle record) into the file-backed bundle the daemon push reads. Every
|
|
300
|
+
* `accounts add codex|grok|cursor` from those releases left its worker key in
|
|
301
|
+
* that shape on the laptop, unpushable; the item name is the same in both
|
|
302
|
+
* shapes, so adoption re-seeds the bundle from the raw value in place and
|
|
303
|
+
* nothing already on a worker changes. Idempotent: a key the bundle already
|
|
304
|
+
* carries is skipped. Local data repair only — it never talks to a peer.
|
|
305
|
+
*/
|
|
306
|
+
export function adoptLegacyReservedStoreItems(meta) {
|
|
307
|
+
const result = { adopted: [], errors: [] };
|
|
308
|
+
for (const account of listNativeAccounts(meta)) {
|
|
309
|
+
const cred = account.workerCredential;
|
|
310
|
+
if (!cred || !cred.bundle.startsWith('__'))
|
|
311
|
+
continue;
|
|
312
|
+
if (cred.kind !== 'setup-token' && cred.kind !== 'api-key')
|
|
313
|
+
continue;
|
|
314
|
+
try {
|
|
315
|
+
if (bundleExistsSync(cred.bundle) && cred.key in readBundleSync(cred.bundle).vars)
|
|
316
|
+
continue;
|
|
317
|
+
const value = readReservedCredential(cred.bundle, cred.key);
|
|
318
|
+
if (value === null)
|
|
319
|
+
continue;
|
|
320
|
+
seedReservedStoreKey(account.agent, cred.kind, cred.key, value);
|
|
321
|
+
result.adopted.push({ bundle: cred.bundle, key: cred.key });
|
|
322
|
+
}
|
|
323
|
+
catch (err) {
|
|
324
|
+
result.errors.push({ bundle: cred.bundle, key: cred.key, message: err.message });
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return result;
|
|
253
328
|
}
|
|
254
329
|
/**
|
|
255
330
|
* Create or rotate the named provider account that `agents run --account` and
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attention desktop banners as a supervised periodic service (PHNX-4004).
|
|
3
|
+
*
|
|
4
|
+
* Each tick reconciles this host's live sessions into attention items through
|
|
5
|
+
* the CLI-owned reconciler (feed/attention.ts — never re-deriving detection) and
|
|
6
|
+
* posts ONE actionable desktop banner per attention key not yet notified. The
|
|
7
|
+
* banner carries the category, the attention key, the session id, and the
|
|
8
|
+
* answerable choices, so the macOS companion can offer Approve / Approve for
|
|
9
|
+
* session / Deny (permission), the options plus a typed reply (question),
|
|
10
|
+
* Approve / Send back (plan review), or Open terminal (a stall/failure), and
|
|
11
|
+
* route the answer back through `agents feed answer <key> --choice <id>`.
|
|
12
|
+
*
|
|
13
|
+
* Idempotency is a filesystem ledger, not memory: one sidecar file per notified
|
|
14
|
+
* key under `~/.agents/.history/feed/notified/`, so a daemon restart never
|
|
15
|
+
* re-posts a banner already sent — the ledger is the truth. It is pruned with
|
|
16
|
+
* the feed's 14-day retention rule.
|
|
17
|
+
*
|
|
18
|
+
* Reader-independent: unlike the session-state publisher this does NOT gate on a
|
|
19
|
+
* live `sessions watch` reader — an attention banner must fire whether or not
|
|
20
|
+
* anyone is watching the stream. `done` banners are deliberately NOT produced
|
|
21
|
+
* here; they ride the run process's own exit (`run-notify.ts`).
|
|
22
|
+
*/
|
|
23
|
+
import { type ActiveSession } from '../session/active.js';
|
|
24
|
+
import { type AttentionItem } from '../feed/attention.js';
|
|
25
|
+
import { type DesktopNotification } from '../menubar/notify-desktop.js';
|
|
26
|
+
import { BasePeriodicService, type DaemonContext } from './service.js';
|
|
27
|
+
import type { DaemonServiceId } from '../daemon-services.js';
|
|
28
|
+
/**
|
|
29
|
+
* The banner for one attention item, or `undefined` when its kind does not
|
|
30
|
+
* notify (`done` comes from the run process; `declared`/`review` are surfaced in
|
|
31
|
+
* the feed, not as a native banner). Pure — the service and its test both build
|
|
32
|
+
* through here, so what ships is what is asserted.
|
|
33
|
+
*/
|
|
34
|
+
export declare function buildAttentionNotification(item: AttentionItem, session: ActiveSession): DesktopNotification | undefined;
|
|
35
|
+
export interface AttentionNotifyServiceOptions {
|
|
36
|
+
/** Live-session source (default: the real active-session query). Injected in tests. */
|
|
37
|
+
getSessions?: () => Promise<ActiveSession[]>;
|
|
38
|
+
/** Notifier (default: the real desktop notifier). Injected in tests to capture posts. */
|
|
39
|
+
notify?: (n: DesktopNotification) => void;
|
|
40
|
+
/** Feed store root (default: the real feed dir). Overridden in tests. */
|
|
41
|
+
feedRoot?: string;
|
|
42
|
+
/** Notified-ledger dir (default: `<feedRoot>/notified`). Overridden in tests. */
|
|
43
|
+
ledgerDir?: string;
|
|
44
|
+
/** Clock (default: `Date.now`). Injected in tests. */
|
|
45
|
+
now?: () => number;
|
|
46
|
+
}
|
|
47
|
+
export declare class AttentionNotifyService extends BasePeriodicService {
|
|
48
|
+
readonly id: DaemonServiceId;
|
|
49
|
+
readonly intervalMs = 5000;
|
|
50
|
+
readonly deadlineMs = 10000;
|
|
51
|
+
private readonly getSessions;
|
|
52
|
+
private readonly notify;
|
|
53
|
+
private readonly feedRoot?;
|
|
54
|
+
private readonly ledgerDir;
|
|
55
|
+
private readonly now;
|
|
56
|
+
private lastPruneMs;
|
|
57
|
+
constructor(opts?: AttentionNotifyServiceOptions);
|
|
58
|
+
protected onStart(_ctx: DaemonContext): Promise<void>;
|
|
59
|
+
protected onStop(): Promise<void>;
|
|
60
|
+
protected onTick(ctx: DaemonContext): Promise<void>;
|
|
61
|
+
/** Filesystem-safe sidecar name for an attention key (`host/session/generation`). */
|
|
62
|
+
private ledgerPath;
|
|
63
|
+
private hasNotified;
|
|
64
|
+
private markNotified;
|
|
65
|
+
private pruneLedger;
|
|
66
|
+
}
|