@phnx-labs/agents-cli 1.22.24 → 1.22.26
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 +350 -1
- package/README.md +31 -6
- package/dist/bin/agents +0 -0
- package/dist/browser.js +14 -4
- package/dist/commands/apply.js +52 -8
- package/dist/commands/browser.js +35 -0
- package/dist/commands/cloud.js +9 -5
- package/dist/commands/doctor.d.ts +24 -0
- package/dist/commands/doctor.js +108 -9
- package/dist/commands/exec.js +19 -16
- package/dist/commands/feed.d.ts +5 -0
- package/dist/commands/feed.js +21 -1
- package/dist/commands/focus.js +2 -2
- package/dist/commands/insights.d.ts +25 -19
- package/dist/commands/insights.js +107 -33
- package/dist/commands/menubar.js +8 -0
- package/dist/commands/reconnect.d.ts +46 -0
- package/dist/commands/reconnect.js +109 -0
- package/dist/commands/routines.d.ts +3 -0
- package/dist/commands/routines.js +72 -49
- package/dist/commands/run-cloud.js +1 -1
- package/dist/commands/secrets.d.ts +2 -8
- package/dist/commands/secrets.js +29 -105
- package/dist/commands/sessions-browser.d.ts +1 -1
- package/dist/commands/sessions-browser.js +27 -7
- package/dist/commands/sessions-resume.js +3 -2
- package/dist/commands/sessions.d.ts +13 -1
- package/dist/commands/sessions.js +28 -2
- package/dist/commands/setup-secrets.d.ts +1 -0
- package/dist/commands/setup-secrets.js +1 -1
- package/dist/commands/setup-watchdog.js +5 -10
- package/dist/commands/setup.d.ts +26 -3
- package/dist/commands/setup.js +106 -47
- package/dist/commands/teams.d.ts +6 -0
- package/dist/commands/teams.js +46 -2
- package/dist/commands/trends.d.ts +8 -0
- package/dist/commands/trends.js +10 -156
- package/dist/commands/watchdog.d.ts +3 -4
- package/dist/commands/watchdog.js +26 -66
- package/dist/index.js +37 -2
- package/dist/lib/agents.d.ts +11 -0
- package/dist/lib/agents.js +155 -23
- package/dist/lib/analytics/dashboard.d.ts +10 -6
- package/dist/lib/analytics/dashboard.js +6 -4
- package/dist/lib/analytics/mix-commands.d.ts +53 -0
- package/dist/lib/analytics/mix-commands.js +229 -0
- package/dist/lib/analytics/recipes.d.ts +19 -14
- package/dist/lib/analytics/recipes.js +4 -2
- package/dist/lib/browser/ipc.d.ts +26 -0
- package/dist/lib/browser/ipc.js +139 -24
- package/dist/lib/browser/profiles.d.ts +11 -0
- package/dist/lib/browser/profiles.js +1 -1
- package/dist/lib/browser/stream.d.ts +14 -0
- package/dist/lib/browser/stream.js +71 -0
- package/dist/lib/channels/owner-sink.d.ts +27 -0
- package/dist/lib/channels/owner-sink.js +93 -0
- package/dist/lib/cloud/cursor.d.ts +79 -0
- package/dist/lib/cloud/cursor.js +228 -0
- package/dist/lib/cloud/registry.js +2 -0
- package/dist/lib/cloud/types.d.ts +7 -2
- package/dist/lib/cloud/types.js +14 -0
- package/dist/lib/crabbox/cli.d.ts +2 -2
- package/dist/lib/crabbox/config.d.ts +7 -8
- package/dist/lib/crabbox/config.js +14 -14
- package/dist/lib/crabbox/lease.d.ts +11 -4
- package/dist/lib/crabbox/lease.js +40 -8
- package/dist/lib/crabbox/setup-copy.d.ts +5 -0
- package/dist/lib/crabbox/setup-copy.js +17 -1
- package/dist/lib/daemon.js +27 -1
- package/dist/lib/device-config.js +7 -0
- package/dist/lib/devices/doctor-findings.d.ts +13 -1
- package/dist/lib/devices/doctor-findings.js +73 -2
- package/dist/lib/events.d.ts +9 -0
- package/dist/lib/events.js +58 -0
- package/dist/lib/exec.d.ts +3 -3
- package/dist/lib/exec.js +24 -10
- package/dist/lib/feed-outcome.d.ts +3 -0
- package/dist/lib/feed-outcome.js +18 -10
- package/dist/lib/feed.d.ts +4 -0
- package/dist/lib/fleet/apply.d.ts +59 -3
- package/dist/lib/fleet/apply.js +183 -6
- package/dist/lib/fleet/types.d.ts +21 -2
- package/dist/lib/hooks/cache.js +15 -0
- package/dist/lib/hosts/passthrough.d.ts +44 -0
- package/dist/lib/hosts/passthrough.js +84 -12
- package/dist/lib/hosts/ready.d.ts +2 -0
- package/dist/lib/hosts/ready.js +10 -1
- package/dist/lib/hosts/reconnect.d.ts +14 -12
- package/dist/lib/hosts/reconnect.js +41 -40
- package/dist/lib/mcp.js +5 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/snapshot.d.ts +15 -0
- package/dist/lib/menubar/snapshot.js +40 -0
- package/dist/lib/plugins.js +13 -1
- package/dist/lib/resources/mcp.js +3 -0
- package/dist/lib/routine-process-cleanup.d.ts +9 -0
- package/dist/lib/routine-process-cleanup.js +73 -0
- package/dist/lib/routines.js +14 -2
- package/dist/lib/runner.d.ts +0 -3
- package/dist/lib/runner.js +6 -19
- 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/secrets/bundles.js +1 -20
- package/dist/lib/secrets/filestore.d.ts +2 -0
- package/dist/lib/secrets/filestore.js +13 -0
- package/dist/lib/secrets/push.d.ts +94 -0
- package/dist/lib/secrets/push.js +145 -0
- package/dist/lib/secrets/rc-hygiene.d.ts +14 -0
- package/dist/lib/secrets/rc-hygiene.js +14 -1
- package/dist/lib/secrets/reaper.d.ts +15 -1
- package/dist/lib/secrets/reaper.js +30 -3
- package/dist/lib/session/active.d.ts +4 -0
- package/dist/lib/session/db.d.ts +21 -3
- package/dist/lib/session/db.js +229 -15
- package/dist/lib/session/discover.d.ts +1 -0
- package/dist/lib/session/discover.js +115 -19
- package/dist/lib/session/insights.d.ts +18 -0
- package/dist/lib/session/insights.js +143 -1
- package/dist/lib/session/remote-list.d.ts +2 -0
- package/dist/lib/session/remote-list.js +1 -0
- package/dist/lib/session/session-cache.d.ts +4 -4
- package/dist/lib/session/session-cache.js +4 -4
- package/dist/lib/session/tool-index.js +133 -22
- package/dist/lib/session/tool-store.d.ts +26 -2
- package/dist/lib/session/tool-store.js +36 -17
- package/dist/lib/shims.js +21 -1
- package/dist/lib/signin-badge.js +2 -0
- package/dist/lib/ssh-exec.js +8 -2
- package/dist/lib/startup/command-registry.d.ts +16 -0
- package/dist/lib/startup/command-registry.js +46 -0
- package/dist/lib/teams/agents.d.ts +13 -0
- package/dist/lib/teams/agents.js +76 -8
- package/dist/lib/teams/parsers.d.ts +1 -1
- package/dist/lib/teams/placement-probe.d.ts +21 -0
- package/dist/lib/teams/placement-probe.js +135 -0
- package/dist/lib/teams/scheduler.d.ts +74 -1
- package/dist/lib/teams/scheduler.js +187 -10
- package/dist/lib/types.d.ts +1 -1
- package/dist/lib/versions.js +16 -1
- package/dist/lib/watchdog/service.d.ts +17 -0
- package/dist/lib/watchdog/service.js +39 -0
- package/package.json +1 -1
package/dist/lib/hosts/ready.js
CHANGED
|
@@ -121,7 +121,11 @@ export function buildReadyProbeCommand(os) {
|
|
|
121
121
|
return `bash -lc ${shellQuote(script)}`;
|
|
122
122
|
}
|
|
123
123
|
export function readyProbe(target, os) {
|
|
124
|
-
|
|
124
|
+
// Disable multiplexing: a stale control socket can hang the local ssh client
|
|
125
|
+
// until the timeout fires, just like sshExecAsync does for the same reason.
|
|
126
|
+
const r = sshExec(target, buildReadyProbeCommand(os), { timeoutMs: 20000, multiplex: false });
|
|
127
|
+
if (r.timedOut)
|
|
128
|
+
return { reachable: false, version: null, view: '', timedOut: true };
|
|
125
129
|
return parseReadyProbe(r.stdout);
|
|
126
130
|
}
|
|
127
131
|
/** Pure parser for `readyProbe` output (unit-tested without ssh). */
|
|
@@ -146,6 +150,11 @@ export function viewHasAgent(view, agent) {
|
|
|
146
150
|
export function ensureHostReady(host, opts) {
|
|
147
151
|
const target = sshTargetFor(host);
|
|
148
152
|
const probe = readyProbe(target, host.os ?? resolveRemoteOsSync(host.name));
|
|
153
|
+
if (probe.timedOut) {
|
|
154
|
+
throw new Error(`Host "${host.name}" (${target}) did not respond in time — the SSH probe timed out after 20 seconds. ` +
|
|
155
|
+
`The host may be slow to start a login shell (nvm/sdkman init, cold node startup). ` +
|
|
156
|
+
`Retry, or run \`agents ssh ${host.name} agents view\` to confirm manually.`);
|
|
157
|
+
}
|
|
149
158
|
if (!probe.reachable) {
|
|
150
159
|
throw new Error(`Host "${host.name}" (${target}) is not reachable over SSH. Check it's online and key auth works.`);
|
|
151
160
|
}
|
|
@@ -48,7 +48,8 @@ export declare function backoffMs(attempt: number): number;
|
|
|
48
48
|
export declare function reconnectStep(state: ReconnectState, outcome: ReconnectOutcome): ReconnectDecision;
|
|
49
49
|
/** Human-readable notice shown before each reconnect wait. "13 seconds", not "12.8s". */
|
|
50
50
|
export declare function reconnectNotice(sessionId: string, host: string, attempt: number, waitMs: number): string;
|
|
51
|
-
/** Notice shown once the retry budget is spent.
|
|
51
|
+
/** Notice shown once the retry budget is spent. Hands back the one verb that
|
|
52
|
+
* re-enters the terminal — attach the live pane if it survived, else resume. */
|
|
52
53
|
export declare function exhaustedNotice(sessionId: string, host: string): string;
|
|
53
54
|
/** Notice shown when a reattach stops on a remapped remote-side exit
|
|
54
55
|
* ({@link REMOTE_EXIT_255_REMAPPED} — a would-be-255 the remote command decided
|
|
@@ -75,13 +76,14 @@ export declare function remoteExitNotice(sessionId: string, host: string): strin
|
|
|
75
76
|
*/
|
|
76
77
|
export declare function wrapRemoteExitCode(cmd: string): string;
|
|
77
78
|
/**
|
|
78
|
-
* The remote command a reattach runs — the peer's own
|
|
79
|
-
* (`agents sessions focus <id> --local
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
79
|
+
* The remote command a reattach runs — the peer's own recovery verb
|
|
80
|
+
* (`agents sessions focus <id> --local`), wrapped by {@link wrapRemoteExitCode}
|
|
81
|
+
* so a stray remote-origin 255 (from this command, whatever produces it — see the
|
|
82
|
+
* file header) can never masquerade as a network drop. No `--attach-only`: focus
|
|
83
|
+
* joins the live pane when it survived, else RESUMES the session in place, so a
|
|
84
|
+
* reattach landing after the pane died recovers the agent instead of dead-ending
|
|
85
|
+
* (RUSH-2085). Split out from {@link reattachRemoteSession} so it is unit-tested
|
|
86
|
+
* without SSH — mirrors `remoteAgentsJsonCommand` in lib/remote-agents-json.ts.
|
|
85
87
|
*/
|
|
86
88
|
export declare function reattachRemoteCommand(sessionId: string): string;
|
|
87
89
|
/**
|
|
@@ -89,10 +91,10 @@ export declare function reattachRemoteCommand(sessionId: string): string;
|
|
|
89
91
|
* `agents sessions focus`. A fast, un-multiplexed preflight probe (`ssh … true`)
|
|
90
92
|
* first establishes whether the host is actually reachable this attempt — that
|
|
91
93
|
* `connected` bit, not the call duration, is what the retry policy keys on. Only on
|
|
92
|
-
* a reachable host do we run the interactive attach (which carries no
|
|
93
|
-
* the agent already runs on the peer — so it rides the normal
|
|
94
|
-
* the ssh exit code (255 = dropped again / unreachable; 0 =
|
|
95
|
-
* session ended) plus whether this attempt connected.
|
|
94
|
+
* a reachable host do we run the interactive attach-or-resume (which carries no
|
|
95
|
+
* credentials — the agent already runs on the peer — so it rides the normal
|
|
96
|
+
* transport). Returns the ssh exit code (255 = dropped again / unreachable; 0 =
|
|
97
|
+
* clean detach; other = session ended) plus whether this attempt connected.
|
|
96
98
|
*/
|
|
97
99
|
export declare function reattachRemoteSession(host: Host, sessionId: string): ReconnectOutcome;
|
|
98
100
|
export interface ReconnectLoopOpts {
|
|
@@ -11,9 +11,13 @@
|
|
|
11
11
|
* SSH automatically, with bounded backoff, until the user detaches cleanly (the
|
|
12
12
|
* remote returns 0) or the agent exits (the tmux session is gone; a non-255 code).
|
|
13
13
|
*
|
|
14
|
-
* The re-attach reuses the peer's OWN
|
|
15
|
-
* --local
|
|
16
|
-
*
|
|
14
|
+
* The re-attach reuses the peer's OWN recovery verb — `agents sessions focus <id>
|
|
15
|
+
* --local` — which JOINS the live local tmux pane there (a second client, no fork)
|
|
16
|
+
* when it still exists, and RESUMES the session in place when the pane is already
|
|
17
|
+
* gone. Dropping `--attach-only` is deliberate: a reattach that lands after the
|
|
18
|
+
* remote pane died must not dead-end at a bare shell (the RUSH-2085 bug), it must
|
|
19
|
+
* fall through to resume so the user is put back into the agent. There is one
|
|
20
|
+
* re-attach implementation (the peer's focus) to keep in sync.
|
|
17
21
|
*
|
|
18
22
|
* **Why the budget refills on `connected`, not on call duration.** ssh returns 255
|
|
19
23
|
* for BOTH "couldn't connect at all" and "connected, then the link dropped." A
|
|
@@ -31,29 +35,24 @@
|
|
|
31
35
|
*
|
|
32
36
|
* **255 from the REMOTE side should never be trusted as "the link dropped."**
|
|
33
37
|
* `reattachRemoteSession`'s `connected` flag is set as soon as the fast preflight
|
|
34
|
-
* probe succeeds — it says nothing about whether the interactive attach
|
|
35
|
-
* follows actually
|
|
36
|
-
* sessions focus <id> --local
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
38
|
+
* probe succeeds — it says nothing about whether the interactive attach/resume
|
|
39
|
+
* that follows actually put the user back into the agent. If the REMOTE command
|
|
40
|
+
* (`agents sessions focus <id> --local`) itself ever happened to exit 255 for a
|
|
41
|
+
* reason that has nothing to do with the ssh transport, `sshStream` would return
|
|
42
|
+
* that same 255, `reconnectStep` couldn't tell it apart from a genuine drop, and
|
|
43
|
+
* `connected: true` would refill the retry budget forever — "attempt 1/N" printed
|
|
44
|
+
* on every single cycle, the terminal filling with aborted-TTY escape-code
|
|
45
|
+
* garbage, `MAX_ATTEMPTS` never actually bounding anything.
|
|
42
46
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* or not — indistinguishable from a real drop. {@link wrapRemoteExitCode} wraps
|
|
53
|
-
* the entire remote command so that whatever exit code it decides on, a 255 is
|
|
54
|
-
* remapped to {@link REMOTE_EXIT_255_REMAPPED} before `sshStream` ever sees it,
|
|
55
|
-
* regardless of which internal branch produced it and regardless of the peer's
|
|
56
|
-
* `agents` version (the remap happens in the shell wrapper THIS process sends).
|
|
47
|
+
* The resume fall-through only widens that surface — the peer's focus now runs a
|
|
48
|
+
* full recovery path (`resumeSessionInPlace` / `runOnPeer`) on a dead pane, any
|
|
49
|
+
* step of which could in principle exit 255 for its own reasons. So the channel-
|
|
50
|
+
* level defense is what matters, not an audit of which branch can fire:
|
|
51
|
+
* {@link wrapRemoteExitCode} wraps the entire remote command so that whatever exit
|
|
52
|
+
* code it decides on, a 255 is remapped to {@link REMOTE_EXIT_255_REMAPPED} before
|
|
53
|
+
* `sshStream` ever sees it, regardless of which internal branch produced it and
|
|
54
|
+
* regardless of the peer's `agents` version (the remap happens in the shell
|
|
55
|
+
* wrapper THIS process sends).
|
|
57
56
|
*
|
|
58
57
|
* This does NOT close every way `reconnectStep` can loop on a real transport
|
|
59
58
|
* 255: a genuinely recurring LOCAL ssh failure (a fast-flapping link, an
|
|
@@ -111,9 +110,10 @@ export function reconnectNotice(sessionId, host, attempt, waitMs) {
|
|
|
111
110
|
const when = secs <= 1 ? 'now' : `in ${secs} seconds`;
|
|
112
111
|
return `\nConnection to ${host} dropped — the agent is still running there. Reconnecting to ${sessionId.slice(0, 8)} ${when} (attempt ${attempt}/${MAX_ATTEMPTS})…\n`;
|
|
113
112
|
}
|
|
114
|
-
/** Notice shown once the retry budget is spent.
|
|
113
|
+
/** Notice shown once the retry budget is spent. Hands back the one verb that
|
|
114
|
+
* re-enters the terminal — attach the live pane if it survived, else resume. */
|
|
115
115
|
export function exhaustedNotice(sessionId, host) {
|
|
116
|
-
return `\nCouldn't reconnect to ${host} after ${MAX_ATTEMPTS} attempts. The agent may still be running —
|
|
116
|
+
return `\nCouldn't reconnect to ${host} after ${MAX_ATTEMPTS} attempts. The agent may still be running — get back in when the network is back:\n agents reconnect ${sessionId.slice(0, 8)}\n`;
|
|
117
117
|
}
|
|
118
118
|
/** Notice shown when a reattach stops on a remapped remote-side exit
|
|
119
119
|
* ({@link REMOTE_EXIT_255_REMAPPED} — a would-be-255 the remote command decided
|
|
@@ -121,7 +121,7 @@ export function exhaustedNotice(sessionId, host) {
|
|
|
121
121
|
* {@link wrapRemoteExitCode}). Distinct from {@link exhaustedNotice}, which is
|
|
122
122
|
* only for a genuinely spent retry budget. */
|
|
123
123
|
export function remoteExitNotice(sessionId, host) {
|
|
124
|
-
return `\nReattach to ${sessionId.slice(0, 8)} on ${host} ended (not a network drop) — check whether it's still live:\n agents
|
|
124
|
+
return `\nReattach to ${sessionId.slice(0, 8)} on ${host} ended (not a network drop) — get back in, or check whether it's still live:\n agents reconnect ${sessionId.slice(0, 8)}\n`;
|
|
125
125
|
}
|
|
126
126
|
/**
|
|
127
127
|
* Wrap `cmd` in `bash -lc` (the login-shell pattern `buildRemoteAgentsInvocation`
|
|
@@ -145,16 +145,17 @@ export function wrapRemoteExitCode(cmd) {
|
|
|
145
145
|
return `bash -lc ${shellQuote(guarded)}`;
|
|
146
146
|
}
|
|
147
147
|
/**
|
|
148
|
-
* The remote command a reattach runs — the peer's own
|
|
149
|
-
* (`agents sessions focus <id> --local
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
148
|
+
* The remote command a reattach runs — the peer's own recovery verb
|
|
149
|
+
* (`agents sessions focus <id> --local`), wrapped by {@link wrapRemoteExitCode}
|
|
150
|
+
* so a stray remote-origin 255 (from this command, whatever produces it — see the
|
|
151
|
+
* file header) can never masquerade as a network drop. No `--attach-only`: focus
|
|
152
|
+
* joins the live pane when it survived, else RESUMES the session in place, so a
|
|
153
|
+
* reattach landing after the pane died recovers the agent instead of dead-ending
|
|
154
|
+
* (RUSH-2085). Split out from {@link reattachRemoteSession} so it is unit-tested
|
|
155
|
+
* without SSH — mirrors `remoteAgentsJsonCommand` in lib/remote-agents-json.ts.
|
|
155
156
|
*/
|
|
156
157
|
export function reattachRemoteCommand(sessionId) {
|
|
157
|
-
const inner = ['agents', 'sessions', 'focus', sessionId, '--local'
|
|
158
|
+
const inner = ['agents', 'sessions', 'focus', sessionId, '--local']
|
|
158
159
|
.map(shellQuote)
|
|
159
160
|
.join(' ');
|
|
160
161
|
return wrapRemoteExitCode(inner);
|
|
@@ -164,10 +165,10 @@ export function reattachRemoteCommand(sessionId) {
|
|
|
164
165
|
* `agents sessions focus`. A fast, un-multiplexed preflight probe (`ssh … true`)
|
|
165
166
|
* first establishes whether the host is actually reachable this attempt — that
|
|
166
167
|
* `connected` bit, not the call duration, is what the retry policy keys on. Only on
|
|
167
|
-
* a reachable host do we run the interactive attach (which carries no
|
|
168
|
-
* the agent already runs on the peer — so it rides the normal
|
|
169
|
-
* the ssh exit code (255 = dropped again / unreachable; 0 =
|
|
170
|
-
* session ended) plus whether this attempt connected.
|
|
168
|
+
* a reachable host do we run the interactive attach-or-resume (which carries no
|
|
169
|
+
* credentials — the agent already runs on the peer — so it rides the normal
|
|
170
|
+
* transport). Returns the ssh exit code (255 = dropped again / unreachable; 0 =
|
|
171
|
+
* clean detach; other = session ended) plus whether this attempt connected.
|
|
171
172
|
*/
|
|
172
173
|
export function reattachRemoteSession(host, sessionId) {
|
|
173
174
|
const target = sshTargetFor(host);
|
package/dist/lib/mcp.js
CHANGED
|
@@ -596,6 +596,7 @@ function writeMcpConfigSupportsAgent(agentId) {
|
|
|
596
596
|
case 'hermes':
|
|
597
597
|
case 'pi':
|
|
598
598
|
case 'muse':
|
|
599
|
+
case 'warp':
|
|
599
600
|
return true;
|
|
600
601
|
default:
|
|
601
602
|
return false;
|
|
@@ -621,7 +622,10 @@ export function writeMcpConfig(agentId, configPath, servers, mode = 'overwrite')
|
|
|
621
622
|
// omp reads the same Claude `{ "mcpServers": {...} }` schema from .mcp.json
|
|
622
623
|
// (stdio: command/args/env; http/sse: url/headers — transport inferred from
|
|
623
624
|
// command/url presence).
|
|
624
|
-
case 'pi':
|
|
625
|
+
case 'pi':
|
|
626
|
+
// Oz reads the same Claude `{ "mcpServers": {...} }` schema from
|
|
627
|
+
// `.warp/.mcp.json` (stdio + http with headers).
|
|
628
|
+
case 'warp': {
|
|
625
629
|
let config = {};
|
|
626
630
|
if (fs.existsSync(configPath)) {
|
|
627
631
|
try {
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { WatchdogTickResult } from '../watchdog/runner.js';
|
|
2
|
+
export interface MenubarSnapshot {
|
|
3
|
+
version: 1;
|
|
4
|
+
capturedAt: string;
|
|
5
|
+
routines: Record<string, unknown>[];
|
|
6
|
+
recentSessions: Record<string, unknown>[];
|
|
7
|
+
activeSessions: Record<string, unknown>[];
|
|
8
|
+
watchdog: {
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
lastTick: Pick<WatchdogTickResult, 'didNudge' | 'counts'> | null;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export declare function readLastWatchdogTick(stateDir?: string): WatchdogTickResult | null;
|
|
14
|
+
/** One-process read model for AGI Menu's repeating three-minute refresh. */
|
|
15
|
+
export declare function computeMenubarSnapshot(): Promise<MenubarSnapshot>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { buildRoutineListJson } from '../../commands/routines.js';
|
|
4
|
+
import { backfillActiveRowsFromIndex, serializeActiveSessionsForJson, serializeSessionsJson } from '../../commands/sessions.js';
|
|
5
|
+
import { getConfigValue } from '../device-config.js';
|
|
6
|
+
import { querySessions } from '../session/db.js';
|
|
7
|
+
import { readActiveSessionsCache } from '../session/session-cache.js';
|
|
8
|
+
import { getRuntimeStateDir } from '../state.js';
|
|
9
|
+
export function readLastWatchdogTick(stateDir = path.join(getRuntimeStateDir(), 'watchdog')) {
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(fs.readFileSync(path.join(stateDir, 'last-tick.json'), 'utf-8'));
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** One-process read model for AGI Menu's repeating three-minute refresh. */
|
|
18
|
+
export async function computeMenubarSnapshot() {
|
|
19
|
+
const [routines, recent] = await Promise.all([
|
|
20
|
+
Promise.resolve(buildRoutineListJson()),
|
|
21
|
+
Promise.resolve(querySessions({ limit: 40, skipExistenceCheck: true })),
|
|
22
|
+
]);
|
|
23
|
+
const active = readActiveSessionsCache('local');
|
|
24
|
+
const activeSessions = active?.sessions ?? [];
|
|
25
|
+
backfillActiveRowsFromIndex(activeSessions);
|
|
26
|
+
return {
|
|
27
|
+
version: 1,
|
|
28
|
+
capturedAt: new Date().toISOString(),
|
|
29
|
+
routines,
|
|
30
|
+
recentSessions: JSON.parse(serializeSessionsJson(recent)),
|
|
31
|
+
activeSessions: serializeActiveSessionsForJson(activeSessions),
|
|
32
|
+
watchdog: {
|
|
33
|
+
enabled: getConfigValue('watchdog.enabled').value === true,
|
|
34
|
+
lastTick: (() => {
|
|
35
|
+
const tick = readLastWatchdogTick();
|
|
36
|
+
return tick ? { didNudge: tick.didNudge, counts: tick.counts } : null;
|
|
37
|
+
})(),
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
package/dist/lib/plugins.js
CHANGED
|
@@ -68,8 +68,20 @@ export function discoverPluginsInDir(pluginsDir, spec = { kind: 'user' }) {
|
|
|
68
68
|
continue;
|
|
69
69
|
const pluginRoot = path.join(pluginsDir, entry.name);
|
|
70
70
|
const manifest = loadPluginManifest(pluginRoot);
|
|
71
|
-
if (!manifest)
|
|
71
|
+
if (!manifest) {
|
|
72
|
+
// A directory that looks like a plugin root but has no valid manifest is
|
|
73
|
+
// silently invisible to every downstream command (list/info/sync, and the
|
|
74
|
+
// materialize-into-version-homes copy) with no other diagnostic anywhere
|
|
75
|
+
// in the chain — it can sit here indefinitely, on the correct git commit,
|
|
76
|
+
// and never surface as broken (RUSH-2270: the `work` plugin shipped
|
|
77
|
+
// without .claude-plugin/plugin.json and nothing noticed for a full merge
|
|
78
|
+
// cycle). Warn once here, at the one place that decides discoverability,
|
|
79
|
+
// so the gap is visible the moment `agents sync` runs.
|
|
80
|
+
const manifestPath = path.join(entry.name, PLUGIN_MANIFEST_DIR, PLUGIN_MANIFEST_FILE);
|
|
81
|
+
process.stderr.write(`agents-cli: '${entry.name}' in ${pluginsDir} has no valid ${manifestPath} ` +
|
|
82
|
+
`(missing, malformed JSON, or missing name/version) — skipped, not discovered as a plugin.\n`);
|
|
72
83
|
continue;
|
|
84
|
+
}
|
|
73
85
|
plugins.push(buildDiscoveredPlugin(pluginRoot, manifest, spec));
|
|
74
86
|
}
|
|
75
87
|
return plugins;
|
|
@@ -129,6 +129,9 @@ export function getMcpConfigPath(agent, versionHome) {
|
|
|
129
129
|
return path.join(versionHome, '.omp', 'agent', '.mcp.json');
|
|
130
130
|
case 'muse':
|
|
131
131
|
return path.join(versionHome, '.config', 'muse', 'settings.json');
|
|
132
|
+
case 'warp':
|
|
133
|
+
// Oz reads user-scope MCP from ~/.warp/.mcp.json (Claude schema).
|
|
134
|
+
return path.join(versionHome, '.warp', '.mcp.json');
|
|
132
135
|
default:
|
|
133
136
|
return null;
|
|
134
137
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RunMeta } from './routines.js';
|
|
2
|
+
export interface RoutineProcessCleanupOptions {
|
|
3
|
+
runsDir?: string;
|
|
4
|
+
alive?: (pid: number) => boolean;
|
|
5
|
+
owns?: (meta: RunMeta) => boolean;
|
|
6
|
+
terminate?: (pid: number) => void;
|
|
7
|
+
}
|
|
8
|
+
/** Reap process groups whose durable run record is already terminal. */
|
|
9
|
+
export declare function reapTerminalRoutineProcesses(opts?: RoutineProcessCleanupOptions): number[];
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { execFileSync } from 'child_process';
|
|
4
|
+
import { isAlive, killTree } from './platform/index.js';
|
|
5
|
+
import { getRunsDir } from './state.js';
|
|
6
|
+
function processMatchesRun(meta) {
|
|
7
|
+
if (!meta.pid || !meta.spawnedAt)
|
|
8
|
+
return false;
|
|
9
|
+
try {
|
|
10
|
+
if (process.platform === 'win32') {
|
|
11
|
+
const startedAt = execFileSync('powershell.exe', [
|
|
12
|
+
'-NoProfile',
|
|
13
|
+
'-NonInteractive',
|
|
14
|
+
'-Command',
|
|
15
|
+
`(Get-Process -Id ${meta.pid} -ErrorAction Stop).StartTime.ToUniversalTime().ToString("o")`,
|
|
16
|
+
], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true }).trim();
|
|
17
|
+
const processStart = Date.parse(startedAt);
|
|
18
|
+
return Number.isFinite(processStart) && Math.abs(processStart - meta.spawnedAt) < 30_000;
|
|
19
|
+
}
|
|
20
|
+
const elapsed = execFileSync('ps', ['-p', String(meta.pid), '-o', 'etime='], {
|
|
21
|
+
encoding: 'utf-8',
|
|
22
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
23
|
+
}).trim();
|
|
24
|
+
if (!elapsed)
|
|
25
|
+
return false;
|
|
26
|
+
const fields = elapsed.replace(/-/g, ':').split(':').reverse();
|
|
27
|
+
const seconds = Number(fields[0] ?? 0)
|
|
28
|
+
+ Number(fields[1] ?? 0) * 60
|
|
29
|
+
+ Number(fields[2] ?? 0) * 3600
|
|
30
|
+
+ Number(fields[3] ?? 0) * 86400;
|
|
31
|
+
return Math.abs((Date.now() - seconds * 1000) - meta.spawnedAt) < 30_000;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Reap process groups whose durable run record is already terminal. */
|
|
38
|
+
export function reapTerminalRoutineProcesses(opts = {}) {
|
|
39
|
+
const runsDir = opts.runsDir ?? getRunsDir();
|
|
40
|
+
const alive = opts.alive ?? isAlive;
|
|
41
|
+
const owns = opts.owns ?? processMatchesRun;
|
|
42
|
+
const terminate = opts.terminate ?? ((pid) => killTree(process.platform === 'win32' ? pid : -pid));
|
|
43
|
+
if (!fs.existsSync(runsDir))
|
|
44
|
+
return [];
|
|
45
|
+
const reaped = [];
|
|
46
|
+
for (const job of fs.readdirSync(runsDir, { withFileTypes: true })) {
|
|
47
|
+
if (!job.isDirectory())
|
|
48
|
+
continue;
|
|
49
|
+
const jobDir = path.join(runsDir, job.name);
|
|
50
|
+
for (const run of fs.readdirSync(jobDir, { withFileTypes: true })) {
|
|
51
|
+
if (!run.isDirectory())
|
|
52
|
+
continue;
|
|
53
|
+
try {
|
|
54
|
+
const meta = JSON.parse(fs.readFileSync(path.join(jobDir, run.name, 'meta.json'), 'utf-8'));
|
|
55
|
+
if (!['failed', 'timeout'].includes(meta.status) || !meta.pid || meta.hostTaskId || meta.cloudTaskId)
|
|
56
|
+
continue;
|
|
57
|
+
const completedAt = Date.parse(meta.completedAt ?? '');
|
|
58
|
+
if (!Number.isFinite(completedAt) || Date.now() - completedAt < 5_000)
|
|
59
|
+
continue;
|
|
60
|
+
if (!alive(meta.pid))
|
|
61
|
+
continue;
|
|
62
|
+
if (!owns(meta))
|
|
63
|
+
continue;
|
|
64
|
+
terminate(meta.pid);
|
|
65
|
+
reaped.push(meta.pid);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Corrupt or concurrently-replaced records are left untouched.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return reaped;
|
|
73
|
+
}
|
package/dist/lib/routines.js
CHANGED
|
@@ -14,7 +14,7 @@ import { getRoutinesDir, getSystemRoutinesDir, getRunsDir, ensureAgentsDir, getP
|
|
|
14
14
|
import { safeJoin, isSafeSegmentName } from './paths.js';
|
|
15
15
|
import { isSafeProjectName } from './projects.js';
|
|
16
16
|
import { atomicWriteFileSync } from './fs-atomic.js';
|
|
17
|
-
import { ALL_AGENT_IDS } from './agents.js';
|
|
17
|
+
import { ALL_AGENT_IDS, ROUTINE_AGENT_IDS } from './agents.js';
|
|
18
18
|
import { machineId, normalizeHost } from './machine-id.js';
|
|
19
19
|
import { resolveActor } from './actor.js';
|
|
20
20
|
import { percentile } from './percentile.js';
|
|
@@ -600,6 +600,7 @@ export function validateJob(config) {
|
|
|
600
600
|
const hasAgent = Boolean(config.agent && typeof config.agent === 'string');
|
|
601
601
|
const hasWorkflow = Boolean(config.workflow && typeof config.workflow === 'string');
|
|
602
602
|
const hasCommand = Boolean(config.command && typeof config.command === 'string');
|
|
603
|
+
const strategy = resolveHostStrategy(config);
|
|
603
604
|
const set = [hasAgent, hasWorkflow, hasCommand].filter(Boolean).length;
|
|
604
605
|
if (set === 0) {
|
|
605
606
|
errors.push('exactly one of agent, workflow, or command is required');
|
|
@@ -613,6 +614,18 @@ export function validateJob(config) {
|
|
|
613
614
|
if (hasAgent && config.agent && !ALL_AGENT_IDS.includes(config.agent)) {
|
|
614
615
|
errors.push(`agent must be one of: ${ALL_AGENT_IDS.join(', ')}`);
|
|
615
616
|
}
|
|
617
|
+
else if (hasAgent && config.agent && strategy === 'local' &&
|
|
618
|
+
!ROUTINE_AGENT_IDS.includes(config.agent)) {
|
|
619
|
+
// The local daemon only knows how to build a command for the agents in
|
|
620
|
+
// ROUTINE_AGENT_IDS (runner.ts's AGENT_COMMANDS table) — anything else is a
|
|
621
|
+
// real, installable agent (it passed the ALL_AGENT_IDS check above) but one
|
|
622
|
+
// the daemon can't fire itself, so reject it now instead of accepting the
|
|
623
|
+
// routine and failing at fire time (runner.ts buildJobCommand: "Unsupported
|
|
624
|
+
// agent for daemon jobs"). host/fleet/cloud placement dispatches through
|
|
625
|
+
// `agents run`/a cloud provider instead of this table, so they're exempt.
|
|
626
|
+
errors.push(`agent '${config.agent}' is not supported by the local routine daemon; use one of: ` +
|
|
627
|
+
`${ROUTINE_AGENT_IDS.join(', ')} (or set hostStrategy: host/fleet/cloud to run it elsewhere)`);
|
|
628
|
+
}
|
|
616
629
|
if (hasWorkflow && config.workflow) {
|
|
617
630
|
if (!/^[a-z0-9][a-z0-9_-]*$/.test(config.workflow)) {
|
|
618
631
|
errors.push('workflow must be a lowercase alphanumeric name (hyphens and underscores allowed, e.g. autodev)');
|
|
@@ -662,7 +675,6 @@ export function validateJob(config) {
|
|
|
662
675
|
errors.push(`hostStrategy must be one of: ${HOST_STRATEGIES.join(', ')}`);
|
|
663
676
|
}
|
|
664
677
|
}
|
|
665
|
-
const strategy = resolveHostStrategy(config);
|
|
666
678
|
if (config.host !== undefined) {
|
|
667
679
|
if (typeof config.host !== 'string' || config.host.trim() === '') {
|
|
668
680
|
errors.push('host must be a non-empty machine name (a registered host, device, capability tag, or user@host)');
|
package/dist/lib/runner.d.ts
CHANGED
|
@@ -26,9 +26,6 @@ export interface RunResult {
|
|
|
26
26
|
export declare class RoutineAlreadyRunningError extends Error {
|
|
27
27
|
constructor(jobName: string, runId: string);
|
|
28
28
|
}
|
|
29
|
-
/** Agents the daemon can actually run, derived from the command table above
|
|
30
|
-
* so the `--agent` help and any validation can never drift from it. */
|
|
31
|
-
export declare const ROUTINE_AGENT_IDS: readonly string[];
|
|
32
29
|
/** Stable working directory for routine children, independent of the daemon's launch cwd. */
|
|
33
30
|
export declare function routineSpawnCwd(config: Pick<JobConfig, 'repo'>, configuredRoot?: string | undefined): string;
|
|
34
31
|
/** Build the full CLI argv for executing a job, applying mode, model, and permission flags. */
|
package/dist/lib/runner.js
CHANGED
|
@@ -36,7 +36,7 @@ import { resolveClaudeSetupToken } from './claude-account-token.js';
|
|
|
36
36
|
import { getConfiguredRunStrategy, resolveRunVersion, resolveAccountVersion, rotationFailoverChain, readinessFromCandidate, formatNoHealthyAccountError, } from './rotate.js';
|
|
37
37
|
import { readAuthHealth, isDeadVerdict } from './auth-health.js';
|
|
38
38
|
import { machineId } from './machine-id.js';
|
|
39
|
-
import { isSelfUpdatingAgent } from './agents.js';
|
|
39
|
+
import { isSelfUpdatingAgent, ROUTINE_AGENT_COMMANDS as AGENT_COMMANDS } from './agents.js';
|
|
40
40
|
import { expandLocalHome, getProjectRoot } from './project-root.js';
|
|
41
41
|
export class RoutineAlreadyRunningError extends Error {
|
|
42
42
|
constructor(jobName, runId) {
|
|
@@ -52,7 +52,7 @@ function activeRoutineRun(config) {
|
|
|
52
52
|
const runs = listRuns(config.name);
|
|
53
53
|
for (let i = runs.length - 1; i >= 0; i--) {
|
|
54
54
|
const run = runs[i];
|
|
55
|
-
if (
|
|
55
|
+
if (!['running', 'failed', 'timeout'].includes(run.status))
|
|
56
56
|
continue;
|
|
57
57
|
if (run.pid && isPidOurs(run.pid, run.spawnedAt))
|
|
58
58
|
return run;
|
|
@@ -98,19 +98,6 @@ function terminateRoutineTree(pid) {
|
|
|
98
98
|
}
|
|
99
99
|
catch { /* already exited */ }
|
|
100
100
|
}
|
|
101
|
-
/** CLI command templates per agent, with {prompt} as a placeholder. */
|
|
102
|
-
const AGENT_COMMANDS = {
|
|
103
|
-
claude: ['claude', '-p', '--verbose', '{prompt}', '--output-format', 'stream-json', '--permission-mode', 'plan'],
|
|
104
|
-
codex: ['codex', 'exec', '{prompt}', '--json'],
|
|
105
|
-
gemini: ['gemini', '{prompt}', '--output-format', 'stream-json'],
|
|
106
|
-
cursor: ['cursor-agent', '-p', '{prompt}', '--output-format', 'stream-json'],
|
|
107
|
-
kimi: ['kimi', '--prompt', '{prompt}', '--output-format', 'stream-json'],
|
|
108
|
-
droid: ['droid', 'exec', '{prompt}', '-o', 'stream-json'],
|
|
109
|
-
muse: ['muse', 'exec', '{prompt}', '--json'],
|
|
110
|
-
};
|
|
111
|
-
/** Agents the daemon can actually run, derived from the command table above
|
|
112
|
-
* so the `--agent` help and any validation can never drift from it. */
|
|
113
|
-
export const ROUTINE_AGENT_IDS = Object.freeze(Object.keys(AGENT_COMMANDS));
|
|
114
101
|
/**
|
|
115
102
|
* Where each agent's transcript files live under an overlay HOME, mirroring
|
|
116
103
|
* `SESSION_ROOT_SPECS` (session/discover.ts) — the CLI's own source of truth
|
|
@@ -235,10 +222,10 @@ export function buildJobCommand(config, resolvedPrompt) {
|
|
|
235
222
|
appendModelAndReasoning(cmd, config);
|
|
236
223
|
}
|
|
237
224
|
if (config.agent === 'cursor') {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
if (
|
|
225
|
+
if (mode === 'plan') {
|
|
226
|
+
cmd.push('--plan');
|
|
227
|
+
}
|
|
228
|
+
else if (mode === 'skip') {
|
|
242
229
|
cmd.push('-f');
|
|
243
230
|
}
|
|
244
231
|
else {
|
|
Binary file
|
|
Binary file
|
|
@@ -55,19 +55,7 @@ const keychainStore = {
|
|
|
55
55
|
const fileItemStore = {
|
|
56
56
|
has: (item) => fileStore.has(item),
|
|
57
57
|
get: (item) => fileStore.get(item),
|
|
58
|
-
getBatch: (items) =>
|
|
59
|
-
const out = new Map();
|
|
60
|
-
for (const item of items) {
|
|
61
|
-
try {
|
|
62
|
-
out.set(item, fileStore.get(item));
|
|
63
|
-
}
|
|
64
|
-
catch {
|
|
65
|
-
// Missing/undecryptable item — absent from the map, mirroring
|
|
66
|
-
// getKeychainTokens (caller decides whether that's an error).
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return out;
|
|
70
|
-
},
|
|
58
|
+
getBatch: (items) => fileStore.getBatch(items),
|
|
71
59
|
set: (item, value) => fileStore.set(item, value),
|
|
72
60
|
setBatch: (items) => {
|
|
73
61
|
for (const [item, value] of items) {
|
|
@@ -1340,13 +1328,6 @@ export function readAndResolveBundleEnv(name, opts = {}) {
|
|
|
1340
1328
|
: store.getBatch([...new Set([metaItem, ...secretItems])]);
|
|
1341
1329
|
const json = fetched.get(metaItem);
|
|
1342
1330
|
if (json === undefined) {
|
|
1343
|
-
// For a file-backed bundle the metadata item is on disk (that's how
|
|
1344
|
-
// bundleBackend resolved to 'file'); a missing decrypt means the wrong
|
|
1345
|
-
// passphrase, not a missing bundle. getBatch swallowed the decrypt error,
|
|
1346
|
-
// so distinguish here rather than report a misleading "not found".
|
|
1347
|
-
if (backend === 'file' && fileStore.has(metaItem)) {
|
|
1348
|
-
throw new Error(`Bundle '${name}': failed to decrypt — wrong AGENTS_SECRETS_PASSPHRASE or tampered file store.`);
|
|
1349
|
-
}
|
|
1350
1331
|
if (vaultExists() && !getVaultSession().loggedIn) {
|
|
1351
1332
|
throw new Error(`Synced secrets are locked. Run: agents login`);
|
|
1352
1333
|
}
|
|
@@ -53,6 +53,7 @@ export declare function encryptForFallback(plaintext: string, passphrase: string
|
|
|
53
53
|
export declare function decryptForFallback(enc: EncFile, passphrase: string): string;
|
|
54
54
|
declare function fileHas(item: string): boolean;
|
|
55
55
|
declare function fileGet(item: string): string;
|
|
56
|
+
declare function fileGetBatch(items: string[]): Map<string, string>;
|
|
56
57
|
declare function fileSet(item: string, value: string): void;
|
|
57
58
|
declare function fileDelete(item: string): boolean;
|
|
58
59
|
declare function fileList(prefix: string): string[];
|
|
@@ -63,6 +64,7 @@ export declare function fileStoreHasItems(): boolean;
|
|
|
63
64
|
export declare const fileStore: {
|
|
64
65
|
has: typeof fileHas;
|
|
65
66
|
get: typeof fileGet;
|
|
67
|
+
getBatch: typeof fileGetBatch;
|
|
66
68
|
set: typeof fileSet;
|
|
67
69
|
delete: typeof fileDelete;
|
|
68
70
|
list: typeof fileList;
|
|
@@ -237,6 +237,18 @@ function fileGet(item) {
|
|
|
237
237
|
throw new Error(`Failed to decrypt '${item}'. Wrong AGENTS_SECRETS_PASSPHRASE or tampered file.`);
|
|
238
238
|
}
|
|
239
239
|
}
|
|
240
|
+
function fileGetBatch(items) {
|
|
241
|
+
const out = new Map();
|
|
242
|
+
for (const item of items) {
|
|
243
|
+
// A missing file is an absent item. Any error reading an existing file,
|
|
244
|
+
// especially an AES-GCM authentication failure, is a broken store and must
|
|
245
|
+
// stop the caller rather than silently produce an incomplete environment.
|
|
246
|
+
if (!fileHas(item))
|
|
247
|
+
continue;
|
|
248
|
+
out.set(item, fileGet(item));
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
240
252
|
function fileSet(item, value) {
|
|
241
253
|
ensureFileDir();
|
|
242
254
|
// Under the store lock: a write must not interleave with a rotation's swap.
|
|
@@ -283,6 +295,7 @@ export function fileStoreHasItems() {
|
|
|
283
295
|
export const fileStore = {
|
|
284
296
|
has: fileHas,
|
|
285
297
|
get: fileGet,
|
|
298
|
+
getBatch: fileGetBatch,
|
|
286
299
|
set: fileSet,
|
|
287
300
|
delete: fileDelete,
|
|
288
301
|
list: fileList,
|