@phnx-labs/agents-cli 1.22.114 → 1.22.116
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 +245 -0
- package/README.md +22 -102
- package/dist/browser.js +0 -0
- package/dist/cli/command-registry.js +0 -7
- package/dist/commands/accounts.d.ts +3 -77
- package/dist/commands/accounts.js +13 -366
- package/dist/commands/audit.js +1 -1
- package/dist/commands/auth.js +0 -2
- package/dist/commands/browser.d.ts +53 -0
- package/dist/commands/browser.js +210 -9
- package/dist/commands/doctor.d.ts +2 -3
- package/dist/commands/doctor.js +0 -11
- package/dist/commands/events.d.ts +1 -3
- package/dist/commands/events.js +4 -7
- package/dist/commands/exec.d.ts +0 -1
- package/dist/commands/exec.js +214 -148
- package/dist/commands/feed-watch.js +52 -8
- package/dist/commands/feed.js +65 -14
- package/dist/commands/focus.js +1 -1
- package/dist/commands/go.d.ts +0 -17
- package/dist/commands/go.js +2 -19
- package/dist/commands/logs.js +1 -1
- package/dist/commands/mcp.js +8 -83
- package/dist/commands/memory.js +4 -47
- package/dist/commands/message.js +4 -4
- package/dist/commands/plugins.js +1 -93
- package/dist/commands/repo.js +0 -44
- package/dist/commands/resume.js +4 -1
- package/dist/commands/secrets-passthrough.js +2 -2
- package/dist/commands/send.d.ts +4 -4
- package/dist/commands/send.js +6 -51
- package/dist/commands/sessions-backup-setup.js +1 -1
- package/dist/commands/sessions-picker.d.ts +11 -0
- package/dist/commands/sessions-picker.js +88 -7
- package/dist/commands/sessions-resume.js +0 -1
- package/dist/commands/sessions-share.d.ts +5 -7
- package/dist/commands/sessions-share.js +98 -49
- package/dist/commands/sessions.d.ts +21 -2
- package/dist/commands/sessions.js +158 -11
- package/dist/commands/setup-browser.js +18 -2
- package/dist/commands/setup-computer.js +20 -6
- package/dist/commands/setup-secrets.d.ts +2 -2
- package/dist/commands/setup-secrets.js +24 -4
- package/dist/commands/setup-term.d.ts +24 -0
- package/dist/commands/setup-term.js +70 -0
- package/dist/commands/setup-terminal.d.ts +3 -0
- package/dist/commands/setup-terminal.js +22 -0
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.js +29 -11
- package/dist/commands/skills.js +0 -8
- package/dist/commands/ssh.d.ts +6 -0
- package/dist/commands/ssh.js +93 -302
- package/dist/commands/sync.js +14 -5
- package/dist/commands/teams.js +1 -1
- package/dist/commands/traces.js +1 -1
- package/dist/lib/accounting/rotate.d.ts +63 -1
- package/dist/lib/accounting/rotate.js +56 -0
- package/dist/lib/accounts/add.d.ts +0 -5
- package/dist/lib/accounts/add.js +3 -9
- package/dist/lib/accounts/slots.js +32 -2
- package/dist/lib/answer-router.d.ts +11 -2
- package/dist/lib/answer-router.js +26 -2
- package/dist/lib/artifacts-client.d.ts +20 -0
- package/dist/lib/artifacts-client.js +46 -0
- package/dist/lib/auth-mint.d.ts +5 -4
- package/dist/lib/auth-mint.js +6 -5
- package/dist/lib/browser/drivers/arc.d.ts +1 -1
- package/dist/lib/browser/runtime-state.d.ts +55 -0
- package/dist/lib/browser/runtime-state.js +99 -18
- package/dist/lib/browser/service.d.ts +10 -0
- package/dist/lib/browser/service.js +229 -37
- package/dist/lib/browser/types.d.ts +18 -0
- package/dist/lib/cli-resources.js +3 -1
- package/dist/lib/cloud/dispatch.js +1 -1
- package/dist/lib/cloudflare/creds.d.ts +10 -0
- package/dist/lib/cloudflare/creds.js +46 -0
- package/dist/lib/cloudflare/provision.d.ts +35 -0
- package/dist/lib/cloudflare/provision.js +144 -0
- package/dist/lib/computer/sessions-list.d.ts +55 -0
- package/dist/lib/computer/sessions-list.js +168 -1
- package/dist/lib/config-keys.d.ts +1 -1
- package/dist/lib/config-keys.js +5 -0
- package/dist/lib/daemon/daemon.js +8 -1
- package/dist/lib/daemon/feed-stream-service.d.ts +23 -0
- package/dist/lib/daemon/feed-stream-service.js +40 -0
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +5 -0
- package/dist/lib/device-config.js +61 -0
- package/dist/lib/devices/connect.d.ts +49 -5
- package/dist/lib/devices/connect.js +169 -21
- package/dist/lib/devices/doctor-findings.js +2 -6
- package/dist/lib/feed/answer.d.ts +153 -4
- package/dist/lib/feed/answer.js +716 -105
- package/dist/lib/feed/envelope.d.ts +79 -0
- package/dist/lib/feed/envelope.js +23 -0
- package/dist/lib/feed/events.d.ts +6 -0
- package/dist/lib/feed/events.js +8 -0
- package/dist/lib/feed/feed.d.ts +61 -1
- package/dist/lib/feed/feed.js +226 -14
- package/dist/lib/feed/hub-server.d.ts +141 -0
- package/dist/lib/feed/hub-server.js +586 -0
- package/dist/lib/feed/hub.d.ts +95 -0
- package/dist/lib/feed/hub.js +255 -0
- package/dist/lib/feed/pr-status.d.ts +8 -0
- package/dist/lib/feed/pr-status.js +9 -1
- package/dist/lib/feed/tool-activity.d.ts +108 -0
- package/dist/lib/feed/tool-activity.js +313 -0
- package/dist/lib/feed/tools.d.ts +198 -0
- package/dist/lib/feed/tools.js +265 -0
- package/dist/lib/feed/watch.d.ts +50 -50
- package/dist/lib/feed/watch.js +147 -16
- package/dist/lib/feed-outcome.d.ts +1 -1
- package/dist/lib/feed-outcome.js +9 -2
- package/dist/lib/feed-policy.js +9 -3
- package/dist/lib/fleet/auth-sync.d.ts +2 -55
- package/dist/lib/fleet/auth-sync.js +2 -89
- package/dist/lib/format.d.ts +1 -1
- package/dist/lib/format.js +1 -1
- package/dist/lib/git.d.ts +0 -16
- package/dist/lib/git.js +0 -58
- package/dist/lib/harness-auth-capabilities.js +7 -2
- package/dist/lib/helper-versions.js +1 -1
- package/dist/lib/hosts/dispatch.d.ts +20 -1
- package/dist/lib/hosts/dispatch.js +52 -30
- package/dist/lib/hosts/remote-cmd.d.ts +72 -1
- package/dist/lib/hosts/remote-cmd.js +151 -10
- package/dist/lib/hosts/remote-cmd.test-fixture.d.ts +2 -0
- package/dist/lib/hosts/remote-cmd.test-fixture.js +22 -0
- package/dist/lib/mailbox.d.ts +12 -0
- package/dist/lib/mailbox.js +16 -2
- package/dist/lib/mcp.js +17 -11
- package/dist/lib/menubar/snapshot.d.ts +51 -0
- package/dist/lib/menubar/snapshot.js +42 -3
- package/dist/lib/open-url.js +2 -2
- package/dist/lib/probe.d.ts +4 -1
- package/dist/lib/probe.js +5 -2
- package/dist/lib/projects.d.ts +23 -0
- package/dist/lib/projects.js +78 -0
- package/dist/lib/pwsh.d.ts +33 -0
- package/dist/lib/pwsh.js +56 -0
- package/dist/lib/redact.d.ts +8 -0
- package/dist/lib/redact.js +11 -0
- package/dist/lib/refresh.d.ts +6 -2
- package/dist/lib/refresh.js +92 -72
- package/dist/lib/secrets-cli.d.ts +11 -0
- package/dist/lib/secrets-cli.js +30 -0
- package/dist/lib/secrets-client.js +3 -2
- package/dist/lib/session/active.d.ts +1 -0
- package/dist/lib/session/active.js +8 -0
- package/dist/lib/session/db.d.ts +67 -3
- package/dist/lib/session/db.js +381 -126
- package/dist/lib/session/detached.d.ts +7 -0
- package/dist/lib/session/detached.js +29 -0
- package/dist/lib/session/prompt.d.ts +23 -7
- package/dist/lib/session/prompt.js +46 -8
- package/dist/lib/session/remote/peer-stream.d.ts +24 -2
- package/dist/lib/session/remote/peer-stream.js +33 -6
- package/dist/lib/session/remote/remote-list.d.ts +20 -0
- package/dist/lib/session/remote/remote-list.js +22 -6
- package/dist/lib/session/remote/watch.d.ts +12 -0
- package/dist/lib/session/remote/watch.js +9 -0
- package/dist/lib/session/remote-preview-cache.d.ts +29 -0
- package/dist/lib/session/remote-preview-cache.js +373 -0
- package/dist/lib/session/sync/backend.d.ts +3 -3
- package/dist/lib/session/sync/backend.js +3 -3
- package/dist/lib/session/sync/provision.d.ts +1 -1
- package/dist/lib/session/sync/provision.js +2 -2
- package/dist/lib/session/tail.d.ts +50 -0
- package/dist/lib/session/tail.js +219 -0
- package/dist/lib/sessions-client.js +0 -3
- package/dist/lib/setup-tool-install.d.ts +3 -0
- package/dist/lib/setup-tool-install.js +27 -0
- package/dist/lib/setup-tool-status.d.ts +22 -0
- package/dist/lib/setup-tool-status.js +220 -0
- package/dist/lib/share-runtime.d.ts +11 -0
- package/dist/lib/share-runtime.js +63 -0
- package/dist/lib/signin-badge.d.ts +19 -4
- package/dist/lib/signin-badge.js +29 -11
- package/dist/lib/smart-launch.d.ts +1 -5
- package/dist/lib/smart-launch.js +3 -11
- package/dist/lib/ssh-exec.d.ts +44 -0
- package/dist/lib/ssh-exec.js +119 -0
- package/dist/lib/startup/command-registry.d.ts +6 -4
- package/dist/lib/startup/command-registry.js +10 -7
- package/dist/lib/state.js +2 -2
- package/dist/lib/storage/selection.d.ts +2 -2
- package/dist/lib/storage/selection.js +2 -2
- package/dist/lib/sync-umbrella.d.ts +5 -0
- package/dist/lib/sync-umbrella.js +18 -10
- package/dist/lib/term-driver.d.ts +24 -0
- package/dist/lib/term-driver.js +36 -0
- package/dist/lib/terminal/index.d.ts +1 -1
- package/dist/lib/terminal/index.js +1 -1
- package/dist/lib/terminal/inject.d.ts +38 -0
- package/dist/lib/terminal/inject.js +55 -9
- package/dist/lib/terminal/transport.d.ts +15 -5
- package/dist/lib/terminal/transport.js +61 -11
- package/dist/lib/traces/backend.d.ts +1 -2
- package/dist/lib/traces/backend.js +1 -2
- package/dist/lib/traces/provision.d.ts +1 -1
- package/dist/lib/traces/provision.js +2 -2
- package/dist/lib/types.d.ts +8 -6
- package/package.json +2 -3
- package/dist/commands/artifacts-setup.d.ts +0 -53
- package/dist/commands/artifacts-setup.js +0 -161
- package/dist/commands/artifacts.d.ts +0 -18
- package/dist/commands/artifacts.js +0 -58
- package/dist/commands/attach.d.ts +0 -12
- package/dist/commands/attach.js +0 -86
- package/dist/commands/auth-mint.d.ts +0 -12
- package/dist/commands/auth-mint.js +0 -108
- package/dist/commands/reconnect.d.ts +0 -46
- package/dist/commands/reconnect.js +0 -115
- package/dist/commands/share.d.ts +0 -293
- package/dist/commands/share.js +0 -1424
- package/dist/lib/fleet/remote-login.d.ts +0 -170
- package/dist/lib/fleet/remote-login.js +0 -568
- package/dist/lib/share/analytics.d.ts +0 -13
- package/dist/lib/share/analytics.js +0 -45
- package/dist/lib/share/backend.d.ts +0 -120
- package/dist/lib/share/backend.js +0 -176
- package/dist/lib/share/capture.d.ts +0 -31
- package/dist/lib/share/capture.js +0 -174
- package/dist/lib/share/config.d.ts +0 -72
- package/dist/lib/share/config.js +0 -211
- package/dist/lib/share/delete.d.ts +0 -123
- package/dist/lib/share/delete.js +0 -173
- package/dist/lib/share/html.d.ts +0 -20
- package/dist/lib/share/html.js +0 -88
- package/dist/lib/share/http-error.d.ts +0 -53
- package/dist/lib/share/http-error.js +0 -65
- package/dist/lib/share/og.d.ts +0 -26
- package/dist/lib/share/og.js +0 -84
- package/dist/lib/share/provision.d.ts +0 -127
- package/dist/lib/share/provision.js +0 -285
- package/dist/lib/share/publish.d.ts +0 -379
- package/dist/lib/share/publish.js +0 -818
- package/dist/lib/share/worker-template.d.ts +0 -27
- package/dist/lib/share/worker-template.js +0 -2424
- package/dist/lib/storage/index.d.ts +0 -14
- package/dist/lib/storage/index.js +0 -14
- package/dist/lib/storage/visibility.d.ts +0 -82
- package/dist/lib/storage/visibility.js +0 -99
|
@@ -112,6 +112,12 @@ interface WindowsAgentsCommand {
|
|
|
112
112
|
env?: Record<string, string>;
|
|
113
113
|
/** Directory to enter before running (`--remote-cwd`). */
|
|
114
114
|
cwd?: string;
|
|
115
|
+
/**
|
|
116
|
+
* `cwd` was DERIVED from the local cwd rather than named by the user (see
|
|
117
|
+
* `deriveMirroredCwd`): a directory the peer does not have falls back to
|
|
118
|
+
* `$HOME` instead of failing the run, exactly as `remoteCdPrefix({ mirror })`.
|
|
119
|
+
*/
|
|
120
|
+
mirrorCwd?: boolean;
|
|
115
121
|
/**
|
|
116
122
|
* Append `exit $LASTEXITCODE` so a native `agents` exit code propagates out
|
|
117
123
|
* through `powershell.exe` (which otherwise exits 0 regardless). Default true;
|
|
@@ -158,12 +164,77 @@ export declare const POWERSHELL_PROGRESS_SILENCE = "$ProgressPreference = 'Silen
|
|
|
158
164
|
* every remote-JSON boundary regardless of the peer's OS.
|
|
159
165
|
*/
|
|
160
166
|
export declare function stripClixml(stdout: string): string;
|
|
167
|
+
/**
|
|
168
|
+
* Statements that run the Agents CLI on a Windows peer with EXACT argv, leaving the
|
|
169
|
+
* child's exit code in `$zq`.
|
|
170
|
+
*
|
|
171
|
+
* `& agents …` cannot be used, and the reason is not our quoting. On Windows
|
|
172
|
+
* `agents` is an npm-generated `agents.ps1`, whose body ends in
|
|
173
|
+
*
|
|
174
|
+
* & "node$exe" --no-warnings=… "$basedir/node_modules/@phnx-labs/agents-cli/dist/index.js" $args
|
|
175
|
+
*
|
|
176
|
+
* — `$args` splatted into a NATIVE program, which is the PowerShell 5.1 lossy
|
|
177
|
+
* serializer. So the loss happens INSIDE the user's shim, after our tokens were
|
|
178
|
+
* already correct: measured on a real peer, `no-such-"menu"-proof` reached the
|
|
179
|
+
* Agents parser as `no-such-menu-proof`. Quoting harder upstream cannot fix that,
|
|
180
|
+
* and rewriting a user's npm shim is not ours to do.
|
|
181
|
+
*
|
|
182
|
+
* Instead the shim is bypassed the same way `getCliLaunch` (`lib/cli-entry.ts`)
|
|
183
|
+
* resolves a launch locally: a node-script entry becomes `<node> <entry> …args`.
|
|
184
|
+
* The entry and runtime are resolved ON THE PEER from the launcher's own location —
|
|
185
|
+
* the identical two-step the shim itself performs — and executed through .NET with
|
|
186
|
+
* a `CommandLineToArgvW`-escaped argument string.
|
|
187
|
+
*
|
|
188
|
+
* A peer whose `agents` is already a native executable is used directly. A peer
|
|
189
|
+
* where neither resolves FAILS LOUD: falling back to `& agents` would silently
|
|
190
|
+
* restore the argument loss, which is worse than an error naming what is missing.
|
|
191
|
+
*
|
|
192
|
+
* The entry comes from the package's DECLARED `bin` rather than a literal
|
|
193
|
+
* `dist/index.js`, so it follows the package instead of needing to be kept in sync
|
|
194
|
+
* with it.
|
|
195
|
+
*/
|
|
196
|
+
export declare function windowsAgentsInvocation(args: string[], binName?: 'agents' | 'ag'): string;
|
|
197
|
+
/**
|
|
198
|
+
* PowerShell expression for a remote path. A home-anchored path (`~/x`,
|
|
199
|
+
* `$HOME/x`) re-roots onto the peer's `$HOME` through `Join-Path` — the Windows
|
|
200
|
+
* counterpart of `remoteCdPrefix`'s `"$HOME"/x` — so a directory mirrored from
|
|
201
|
+
* another box resolves on the peer. Any other path is quoted verbatim.
|
|
202
|
+
*/
|
|
203
|
+
export declare function windowsRemotePath(p: string): string;
|
|
204
|
+
/**
|
|
205
|
+
* The `Set-Location` step of a Windows remote command. `-ErrorAction Stop` is
|
|
206
|
+
* spelled out because a missing directory is a NON-terminating error for
|
|
207
|
+
* `Set-Location`, which a `try`/`catch` would otherwise sail past: the mirrored
|
|
208
|
+
* form must genuinely land in `$HOME` when the peer lacks the checkout, and the
|
|
209
|
+
* explicit form must abort rather than run the command in the wrong directory.
|
|
210
|
+
*/
|
|
211
|
+
export declare function windowsSetLocation(cwd: string, mirror?: boolean): string;
|
|
161
212
|
export declare function windowsAgentsScript(cmd: WindowsAgentsCommand): string;
|
|
162
213
|
/**
|
|
163
214
|
* Build the `ssh <target> <cmd>` string for one `agents …` invocation on a
|
|
164
|
-
* Windows remote: a `powershell -NoProfile
|
|
215
|
+
* Windows remote: a rendered `powershell -NoProfile` call. The
|
|
165
216
|
* Windows counterpart of `bash -lc '<...>'`, shared by every `--device` site.
|
|
166
217
|
*/
|
|
218
|
+
/**
|
|
219
|
+
* Render a PowerShell script as the single command string ssh sends to a Windows
|
|
220
|
+
* peer, compressing it when that is genuinely shorter.
|
|
221
|
+
*
|
|
222
|
+
* The plain route is `-EncodedCommand <base64 of UTF-16LE>`, which inflates the
|
|
223
|
+
* script by ~2.67x. That matters because OpenSSH-for-Windows caps the whole remote
|
|
224
|
+
* command far below cmd.exe's 8191 — bisected against a live peer, 2934 characters
|
|
225
|
+
* succeed and 3102 fail — so a longer script eats the headroom a caller's
|
|
226
|
+
* arguments, `--remote-cwd` and forwarded env need.
|
|
227
|
+
*
|
|
228
|
+
* The alternative is to deflate the UTF-8 script and emit a small fixed bootstrap
|
|
229
|
+
* that inflates it back. That trades ~2.67x for ~1.33x on the compressible part,
|
|
230
|
+
* which on scripts of this shape is a large net win. The variable-free bootstrap
|
|
231
|
+
* uses `-Command` with a base64 payload and leaves stdin available to the child.
|
|
232
|
+
*
|
|
233
|
+
* Applied ONLY when the result is actually shorter: for a small script the fixed
|
|
234
|
+
* bootstrap costs more than it saves, so picking the shorter of the two keeps the
|
|
235
|
+
* plain form in play for every small caller and is obviously correct either way.
|
|
236
|
+
*/
|
|
237
|
+
export declare function renderPowershellCommand(script: string): string;
|
|
167
238
|
export declare function buildWindowsAgentsCommand(cmd: WindowsAgentsCommand): string;
|
|
168
239
|
/**
|
|
169
240
|
* Build the `ssh <target> <cmd>` string for `agents secrets import` on a Windows
|
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
* (`sshExec`/`sshStream`); orchestration lives in `passthrough.ts`.
|
|
9
9
|
*/
|
|
10
10
|
import { shellQuote } from '../ssh-exec.js';
|
|
11
|
+
import { pwshLiteral, pwshNativeExecStatements } from '../pwsh.js';
|
|
12
|
+
import { quoteWin32ExecArg } from '../platform/exec.js';
|
|
13
|
+
import { homeRemainder } from '../project-root.js';
|
|
14
|
+
import * as zlib from 'node:zlib';
|
|
11
15
|
/**
|
|
12
16
|
* Remove routing flags (and their values) from a command's args, leaving the
|
|
13
17
|
* rest untouched and in order so they forward verbatim to the remote binary.
|
|
@@ -136,8 +140,6 @@ export const RUN_OPTION_FORWARDING = {
|
|
|
136
140
|
// On a --device dispatch the remote box runs its own run-exit sync, so this
|
|
137
141
|
// flag is never forwarded — it is a local-exit-behavior toggle, not remote.
|
|
138
142
|
traceSync: 'local-only',
|
|
139
|
-
// Deprecated alias for --device auto; resolved on the launching box before SSH.
|
|
140
|
-
smart: 'local-only',
|
|
141
143
|
// Broadcast mode (agents run --broadcast) is its own fan-out dispatch — mutually
|
|
142
144
|
// exclusive with --host. All broadcast options are local-only; exec.ts handles them
|
|
143
145
|
// before any SSH dispatch path is reached.
|
|
@@ -293,30 +295,169 @@ export function stripClixml(stdout) {
|
|
|
293
295
|
.replace(/#< CLIXML[^\n]*\r?\n?(?:\s*<Objs\b[\s\S]*?<\/Objs>)*/g, '')
|
|
294
296
|
.trim();
|
|
295
297
|
}
|
|
298
|
+
/**
|
|
299
|
+
* Statements that run the Agents CLI on a Windows peer with EXACT argv, leaving the
|
|
300
|
+
* child's exit code in `$zq`.
|
|
301
|
+
*
|
|
302
|
+
* `& agents …` cannot be used, and the reason is not our quoting. On Windows
|
|
303
|
+
* `agents` is an npm-generated `agents.ps1`, whose body ends in
|
|
304
|
+
*
|
|
305
|
+
* & "node$exe" --no-warnings=… "$basedir/node_modules/@phnx-labs/agents-cli/dist/index.js" $args
|
|
306
|
+
*
|
|
307
|
+
* — `$args` splatted into a NATIVE program, which is the PowerShell 5.1 lossy
|
|
308
|
+
* serializer. So the loss happens INSIDE the user's shim, after our tokens were
|
|
309
|
+
* already correct: measured on a real peer, `no-such-"menu"-proof` reached the
|
|
310
|
+
* Agents parser as `no-such-menu-proof`. Quoting harder upstream cannot fix that,
|
|
311
|
+
* and rewriting a user's npm shim is not ours to do.
|
|
312
|
+
*
|
|
313
|
+
* Instead the shim is bypassed the same way `getCliLaunch` (`lib/cli-entry.ts`)
|
|
314
|
+
* resolves a launch locally: a node-script entry becomes `<node> <entry> …args`.
|
|
315
|
+
* The entry and runtime are resolved ON THE PEER from the launcher's own location —
|
|
316
|
+
* the identical two-step the shim itself performs — and executed through .NET with
|
|
317
|
+
* a `CommandLineToArgvW`-escaped argument string.
|
|
318
|
+
*
|
|
319
|
+
* A peer whose `agents` is already a native executable is used directly. A peer
|
|
320
|
+
* where neither resolves FAILS LOUD: falling back to `& agents` would silently
|
|
321
|
+
* restore the argument loss, which is worse than an error naming what is missing.
|
|
322
|
+
*
|
|
323
|
+
* The entry comes from the package's DECLARED `bin` rather than a literal
|
|
324
|
+
* `dist/index.js`, so it follows the package instead of needing to be kept in sync
|
|
325
|
+
* with it.
|
|
326
|
+
*/
|
|
327
|
+
export function windowsAgentsInvocation(args, binName = 'agents') {
|
|
328
|
+
const escaped = pwshLiteral(args.map(quoteWin32ExecArg).join(' '));
|
|
329
|
+
return [
|
|
330
|
+
// Set before any resolution: a non-terminating failure below would otherwise
|
|
331
|
+
// leave the exit code null, and `exit $null` reports 0.
|
|
332
|
+
`$ErrorActionPreference = 'Stop'`,
|
|
333
|
+
`$zc = Get-Command ${powershellQuote(binName)} -ErrorAction Stop`,
|
|
334
|
+
`$ze = $zc.Source`,
|
|
335
|
+
`$zr = ''`,
|
|
336
|
+
// A real native executable needs no interpreter prefix. `CommandType` alone is
|
|
337
|
+
// NOT that test: `Get-Command` reports `Application` for `agents.cmd` too, and
|
|
338
|
+
// a .cmd launcher re-parses through cmd.exe with the same argument loss as the
|
|
339
|
+
// .ps1 one — so a cmd-only install must not bypass this either.
|
|
340
|
+
`if ($zc.CommandType -ne 'Application' -or $ze -match '\\.(cmd|bat)$') {`,
|
|
341
|
+
` $zb = Split-Path $ze`,
|
|
342
|
+
` $zk = Join-Path $zb 'node_modules\\@phnx-labs\\agents-cli'`,
|
|
343
|
+
// The package's DECLARED bin, so the entry follows the package rather than a
|
|
344
|
+
// hand-synced `dist/index.js` literal that would rot on upgrade. One check
|
|
345
|
+
// covers a missing bin AND the single-string `bin` form (which yields $null).
|
|
346
|
+
` $zl = (Get-Content -Raw (Join-Path $zk 'package.json') | ConvertFrom-Json).bin.${binName}`,
|
|
347
|
+
` if (-not $zl) { throw "agents package at $zk declares no bin.${binName}" }`,
|
|
348
|
+
` $zt = Join-Path $zk $zl`,
|
|
349
|
+
` if (-not [IO.File]::Exists($zt)) { throw "agents CLI entry not found at $zt" }`,
|
|
350
|
+
// The npm shim prefers a node.exe beside itself before PATH; match that.
|
|
351
|
+
` $zd = Join-Path $zb 'node.exe'`,
|
|
352
|
+
` $ze = if ([IO.File]::Exists($zd)) { $zd } else { 'node' }`,
|
|
353
|
+
// A Windows path cannot contain `"`, so wrapping is sufficient escaping here.
|
|
354
|
+
` $zr = '--no-warnings=ExperimentalWarning "' + $zt + '" '`,
|
|
355
|
+
`}`,
|
|
356
|
+
...pwshNativeExecStatements('$ze', `$zr + ${escaped}`),
|
|
357
|
+
// Reported through a plain variable rather than by assigning the automatic
|
|
358
|
+
// $LASTEXITCODE, which nothing set here since no PowerShell command ran.
|
|
359
|
+
`$zq = $zp.ExitCode`,
|
|
360
|
+
// Never let an unknown outcome read as success.
|
|
361
|
+
`if ($null -eq $zq) { $zq = 1 }`,
|
|
362
|
+
// NEWLINE-joined, and that is required rather than stylistic: the caller joins
|
|
363
|
+
// its parts with `'; '`, which between a closing `}` and `else`/an indented block
|
|
364
|
+
// would produce invalid PowerShell.
|
|
365
|
+
].join('\n');
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* PowerShell expression for a remote path. A home-anchored path (`~/x`,
|
|
369
|
+
* `$HOME/x`) re-roots onto the peer's `$HOME` through `Join-Path` — the Windows
|
|
370
|
+
* counterpart of `remoteCdPrefix`'s `"$HOME"/x` — so a directory mirrored from
|
|
371
|
+
* another box resolves on the peer. Any other path is quoted verbatim.
|
|
372
|
+
*/
|
|
373
|
+
export function windowsRemotePath(p) {
|
|
374
|
+
const rest = homeRemainder(p);
|
|
375
|
+
if (rest === null)
|
|
376
|
+
return powershellQuote(p);
|
|
377
|
+
return rest === '' ? '$HOME' : `(Join-Path $HOME ${powershellQuote(rest)})`;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* The `Set-Location` step of a Windows remote command. `-ErrorAction Stop` is
|
|
381
|
+
* spelled out because a missing directory is a NON-terminating error for
|
|
382
|
+
* `Set-Location`, which a `try`/`catch` would otherwise sail past: the mirrored
|
|
383
|
+
* form must genuinely land in `$HOME` when the peer lacks the checkout, and the
|
|
384
|
+
* explicit form must abort rather than run the command in the wrong directory.
|
|
385
|
+
*/
|
|
386
|
+
export function windowsSetLocation(cwd, mirror = false) {
|
|
387
|
+
const enter = `Set-Location -LiteralPath ${windowsRemotePath(cwd)} -ErrorAction Stop`;
|
|
388
|
+
return mirror ? `try { ${enter} } catch { Set-Location -LiteralPath $HOME }` : enter;
|
|
389
|
+
}
|
|
296
390
|
export function windowsAgentsScript(cmd) {
|
|
297
|
-
const { args, env, cwd, propagateExit = true, remapExit255 = false } = cmd;
|
|
298
|
-
|
|
391
|
+
const { args, env, cwd, mirrorCwd = false, propagateExit = true, remapExit255 = false } = cmd;
|
|
392
|
+
// `Stop` FIRST, before the env assignments and the Set-Location: a failing
|
|
393
|
+
// `Set-Location` (a cwd that does not exist on the peer) must abort rather than
|
|
394
|
+
// continue into the launcher and run the command in the wrong directory.
|
|
395
|
+
const parts = [POWERSHELL_PROGRESS_SILENCE, `$ErrorActionPreference = 'Stop'`];
|
|
299
396
|
if (env)
|
|
300
397
|
for (const [k, v] of Object.entries(env))
|
|
301
398
|
parts.push(`$env:${k} = ${powershellQuote(v)}`);
|
|
302
399
|
if (cwd)
|
|
303
|
-
parts.push(
|
|
304
|
-
parts.push(
|
|
400
|
+
parts.push(windowsSetLocation(cwd, mirrorCwd));
|
|
401
|
+
parts.push(windowsAgentsInvocation(args));
|
|
305
402
|
if (propagateExit) {
|
|
306
403
|
if (remapExit255)
|
|
307
|
-
parts.push('
|
|
404
|
+
parts.push('if ($zq -eq 255) { exit 254 }', 'exit $zq');
|
|
308
405
|
else
|
|
309
|
-
parts.push('exit $
|
|
406
|
+
parts.push('exit $zq');
|
|
310
407
|
}
|
|
311
408
|
return parts.join('; ');
|
|
312
409
|
}
|
|
313
410
|
/**
|
|
314
411
|
* Build the `ssh <target> <cmd>` string for one `agents …` invocation on a
|
|
315
|
-
* Windows remote: a `powershell -NoProfile
|
|
412
|
+
* Windows remote: a rendered `powershell -NoProfile` call. The
|
|
316
413
|
* Windows counterpart of `bash -lc '<...>'`, shared by every `--device` site.
|
|
317
414
|
*/
|
|
415
|
+
/**
|
|
416
|
+
* Render a PowerShell script as the single command string ssh sends to a Windows
|
|
417
|
+
* peer, compressing it when that is genuinely shorter.
|
|
418
|
+
*
|
|
419
|
+
* The plain route is `-EncodedCommand <base64 of UTF-16LE>`, which inflates the
|
|
420
|
+
* script by ~2.67x. That matters because OpenSSH-for-Windows caps the whole remote
|
|
421
|
+
* command far below cmd.exe's 8191 — bisected against a live peer, 2934 characters
|
|
422
|
+
* succeed and 3102 fail — so a longer script eats the headroom a caller's
|
|
423
|
+
* arguments, `--remote-cwd` and forwarded env need.
|
|
424
|
+
*
|
|
425
|
+
* The alternative is to deflate the UTF-8 script and emit a small fixed bootstrap
|
|
426
|
+
* that inflates it back. That trades ~2.67x for ~1.33x on the compressible part,
|
|
427
|
+
* which on scripts of this shape is a large net win. The variable-free bootstrap
|
|
428
|
+
* uses `-Command` with a base64 payload and leaves stdin available to the child.
|
|
429
|
+
*
|
|
430
|
+
* Applied ONLY when the result is actually shorter: for a small script the fixed
|
|
431
|
+
* bootstrap costs more than it saves, so picking the shorter of the two keeps the
|
|
432
|
+
* plain form in play for every small caller and is obviously correct either way.
|
|
433
|
+
*/
|
|
434
|
+
export function renderPowershellCommand(script) {
|
|
435
|
+
const plain = `powershell -NoProfile -EncodedCommand ${encodePowershell(script)}`;
|
|
436
|
+
// Raw DEFLATE (RFC 1951) — what .NET's `DeflateStream` reads. `deflateSync`
|
|
437
|
+
// would prepend a zlib header that `DeflateStream` rejects.
|
|
438
|
+
const packed = zlib.deflateRawSync(Buffer.from(script, 'utf-8'), { level: 9 }).toString('base64');
|
|
439
|
+
// The blob is embedded DIRECTLY in a `-Command` string rather than wrapped in a
|
|
440
|
+
// second `-EncodedCommand`: routing an already-base64 payload through the encoded
|
|
441
|
+
// form would inflate it by another 2.67x and cancel most of the compression.
|
|
442
|
+
//
|
|
443
|
+
// The bootstrap is deliberately VARIABLE-FREE — one nested expression, no `$`
|
|
444
|
+
// anywhere. A `$b = …` form is unsafe here: OpenSSH-for-Windows may have
|
|
445
|
+
// PowerShell as its `DefaultShell`, in which case the outer double-quoted string
|
|
446
|
+
// is parsed by PowerShell first and `$b` would be expanded before our script ever
|
|
447
|
+
// runs. With no `$`, no `%`, no backtick and no cmd metacharacter — base64's
|
|
448
|
+
// alphabet is `A-Za-z0-9+/=` and the rest is ASCII punctuation neither shell
|
|
449
|
+
// touches inside quotes — the same text survives either default shell. stdin is
|
|
450
|
+
// untouched by both routes.
|
|
451
|
+
const bootstrap = 'iex ([IO.StreamReader]::new([IO.Compression.DeflateStream]::new('
|
|
452
|
+
+ `[IO.MemoryStream]::new([Convert]::FromBase64String('${packed}')),`
|
|
453
|
+
+ '[IO.Compression.CompressionMode]::Decompress),[Text.Encoding]::UTF8).ReadToEnd())';
|
|
454
|
+
const compressed = `powershell -NoProfile -Command "${bootstrap}"`;
|
|
455
|
+
// Only when genuinely shorter: for a small script the fixed bootstrap costs more
|
|
456
|
+
// than it saves, so this keeps the plain form in play for every small caller.
|
|
457
|
+
return compressed.length < plain.length ? compressed : plain;
|
|
458
|
+
}
|
|
318
459
|
export function buildWindowsAgentsCommand(cmd) {
|
|
319
|
-
return
|
|
460
|
+
return renderPowershellCommand(windowsAgentsScript(cmd));
|
|
320
461
|
}
|
|
321
462
|
/**
|
|
322
463
|
* Build the `ssh <target> <cmd>` string for `agents secrets import` on a Windows
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test-only decoder for a rendered remote PowerShell command.
|
|
3
|
+
*
|
|
4
|
+
* `renderPowershellCommand` emits whichever of two representations is shorter: the
|
|
5
|
+
* plain `-EncodedCommand <base64 UTF-16LE>`, or a `-Command` bootstrap carrying a
|
|
6
|
+
* deflated UTF-8 payload. Tests assert on the SCRIPT, not the representation, so
|
|
7
|
+
* every suite needs both — and four copies of that parsing is exactly how one of
|
|
8
|
+
* them silently stops decoding the form it was meant to check.
|
|
9
|
+
*
|
|
10
|
+
* Named `*.test-fixture.ts` after the existing `daemon.test-fixture.ts`.
|
|
11
|
+
*/
|
|
12
|
+
import * as zlib from 'node:zlib';
|
|
13
|
+
/** The script behind either render route. Throws if the command is neither. */
|
|
14
|
+
export function decodeRenderedPowershell(command) {
|
|
15
|
+
const encoded = /-EncodedCommand (\S+)\s*$/.exec(command);
|
|
16
|
+
if (encoded)
|
|
17
|
+
return Buffer.from(encoded[1], 'base64').toString('utf16le');
|
|
18
|
+
const packed = /FromBase64String\('([A-Za-z0-9+/=]+)'\)/.exec(command);
|
|
19
|
+
if (!packed)
|
|
20
|
+
throw new Error(`not a rendered PowerShell command: ${command}`);
|
|
21
|
+
return zlib.inflateRawSync(Buffer.from(packed[1], 'base64')).toString('utf-8');
|
|
22
|
+
}
|
package/dist/lib/mailbox.d.ts
CHANGED
|
@@ -22,6 +22,16 @@ export interface MailboxMessage {
|
|
|
22
22
|
* receipts back to the feed store.
|
|
23
23
|
*/
|
|
24
24
|
blockId?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Which ASK of that block this message answers, and which claim sent it. A
|
|
27
|
+
* blockId is per SESSION and is reused by every generation of that session's
|
|
28
|
+
* questions, so without these a `consumed` receipt raised on drain cannot say
|
|
29
|
+
* WHICH question it acknowledges -- and a late one for question N would
|
|
30
|
+
* resolve question N+1 (PHNX-3999). Carried on the message so the binding
|
|
31
|
+
* survives the process that enqueued it.
|
|
32
|
+
*/
|
|
33
|
+
generation?: string;
|
|
34
|
+
attempt?: string;
|
|
25
35
|
/**
|
|
26
36
|
* Drop reason when a message is archived without delivery (expired, dead box, etc.).
|
|
27
37
|
* Set by the TTL/liveness layer, not by writers.
|
|
@@ -49,6 +59,8 @@ export declare function enqueue(boxDir: string, msg: {
|
|
|
49
59
|
text: string;
|
|
50
60
|
from?: string;
|
|
51
61
|
blockId?: string;
|
|
62
|
+
generation?: string;
|
|
63
|
+
attempt?: string;
|
|
52
64
|
ttlSeconds?: number;
|
|
53
65
|
}): string;
|
|
54
66
|
/** Parse a message file. Returns null on missing/corrupt/invalid-shape. */
|
package/dist/lib/mailbox.js
CHANGED
|
@@ -97,6 +97,8 @@ export function enqueue(boxDir, msg) {
|
|
|
97
97
|
ts: now.toISOString(),
|
|
98
98
|
text: msg.text,
|
|
99
99
|
blockId: msg.blockId,
|
|
100
|
+
...(msg.generation !== undefined ? { generation: msg.generation } : {}),
|
|
101
|
+
...(msg.attempt !== undefined ? { attempt: msg.attempt } : {}),
|
|
100
102
|
};
|
|
101
103
|
const ttlSeconds = msg.ttlSeconds ?? resolveDefaultTtlSeconds();
|
|
102
104
|
if (ttlSeconds > 0) {
|
|
@@ -128,7 +130,14 @@ export function readMessage(file) {
|
|
|
128
130
|
if (typeof m?.msgId !== 'string' || typeof m?.to !== 'string' || typeof m?.text !== 'string') {
|
|
129
131
|
return null;
|
|
130
132
|
}
|
|
131
|
-
|
|
133
|
+
// Field-by-field on purpose (an unknown key from a foreign writer is dropped),
|
|
134
|
+
// so EVERY field the receipt binding depends on must be listed here or it is
|
|
135
|
+
// silently lost on read and the binding it enables never fires.
|
|
136
|
+
return {
|
|
137
|
+
msgId: m.msgId, to: m.to, from: m.from, ts: m.ts ?? '', text: m.text,
|
|
138
|
+
expiresAt: m.expiresAt, blockId: m.blockId, generation: m.generation, attempt: m.attempt,
|
|
139
|
+
dropped: m.dropped,
|
|
140
|
+
};
|
|
132
141
|
}
|
|
133
142
|
/** True when a message has a parsed expiry in the past. */
|
|
134
143
|
export function isExpired(msg, now = new Date()) {
|
|
@@ -230,7 +239,12 @@ function consumeClaimed(boxDir, name, expectedTo) {
|
|
|
230
239
|
if (msg.blockId) {
|
|
231
240
|
try {
|
|
232
241
|
const feedRoot = process.env.AGENTS_FEED_DIR;
|
|
233
|
-
|
|
242
|
+
// The origin rides the message, so the receipt names the ask it answers.
|
|
243
|
+
recordMessageReceipt(msg.blockId, {
|
|
244
|
+
msgId: msg.msgId, status: 'consumed', at: new Date().toISOString(), from: msg.from,
|
|
245
|
+
...(msg.generation !== undefined ? { generation: msg.generation } : {}),
|
|
246
|
+
...(msg.attempt !== undefined ? { attempt: msg.attempt } : {}),
|
|
247
|
+
}, feedRoot);
|
|
234
248
|
}
|
|
235
249
|
catch {
|
|
236
250
|
// Receipt surfacing is best-effort; never stall delivery.
|
package/dist/lib/mcp.js
CHANGED
|
@@ -380,25 +380,31 @@ function installMcpViaClaude(binaryPath, server, versionHome) {
|
|
|
380
380
|
* Uses: codex mcp add <name> -- <cmd> [args...]
|
|
381
381
|
*/
|
|
382
382
|
function installMcpViaCodex(binaryPath, server, versionHome) {
|
|
383
|
-
|
|
383
|
+
let args;
|
|
384
|
+
if (server.config.transport === 'http') {
|
|
385
|
+
if (!server.config.url)
|
|
386
|
+
throw new Error(`HTTP MCP '${server.name}' has no url`);
|
|
387
|
+
// codex mcp add <name> --url <url>
|
|
388
|
+
args = ['mcp', 'add', server.name, '--url', server.config.url];
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
384
391
|
// codex mcp add -- <name> <cmd> [args...]
|
|
385
|
-
|
|
392
|
+
args = [
|
|
386
393
|
'mcp', 'add',
|
|
387
394
|
'--',
|
|
388
395
|
server.name,
|
|
389
396
|
server.config.command,
|
|
390
397
|
...(server.config.args || [])
|
|
391
398
|
];
|
|
392
|
-
// RUSH-1752: user-controlled MCP command/args must not reach cmd.exe unquoted.
|
|
393
|
-
const spec = execFileShellSpec(binaryPath, args);
|
|
394
|
-
execFileSync(spec.command, spec.args, {
|
|
395
|
-
stdio: 'pipe',
|
|
396
|
-
timeout: 30000,
|
|
397
|
-
env: { ...process.env, HOME: versionHome },
|
|
398
|
-
shell: spec.shell,
|
|
399
|
-
});
|
|
400
399
|
}
|
|
401
|
-
//
|
|
400
|
+
// RUSH-1752: user-controlled MCP command/args must not reach cmd.exe unquoted.
|
|
401
|
+
const spec = execFileShellSpec(binaryPath, args);
|
|
402
|
+
execFileSync(spec.command, spec.args, {
|
|
403
|
+
stdio: 'pipe',
|
|
404
|
+
timeout: 30000,
|
|
405
|
+
env: { ...process.env, HOME: versionHome },
|
|
406
|
+
shell: spec.shell,
|
|
407
|
+
});
|
|
402
408
|
}
|
|
403
409
|
export async function registerMcpCommandToTargets(targets, name, commandSpec, scope = 'user', transport = 'stdio') {
|
|
404
410
|
const results = [];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DeviceStats } from '../devices/health.js';
|
|
1
2
|
import type { WatchdogTickResult } from '../watchdog/runner.js';
|
|
2
3
|
/**
|
|
3
4
|
* One registered fleet device, for the menu-bar's collapsible DEVICES section.
|
|
@@ -20,6 +21,47 @@ interface MenubarDevice {
|
|
|
20
21
|
interactive: boolean;
|
|
21
22
|
isLocal: boolean;
|
|
22
23
|
preferred: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* The operator's role mark — `worker` | `personal` | `desktop`, or `unknown`
|
|
26
|
+
* when the device was never marked (PHNX-3999 F25). Read from the same
|
|
27
|
+
* device-scope config `--device auto` consults, so the menu shows the fact that
|
|
28
|
+
* governs placement rather than a second notion of it.
|
|
29
|
+
*/
|
|
30
|
+
role: string;
|
|
31
|
+
/**
|
|
32
|
+
* True when automatic placement (`agents run --device auto`) may pick this
|
|
33
|
+
* device: the verdict of the ONE canonical pool filter, so a `personal` box the
|
|
34
|
+
* user sits at reads as ineligible here exactly as it behaves there.
|
|
35
|
+
*/
|
|
36
|
+
autoEligible: boolean;
|
|
37
|
+
/** Hardware facts and the current reading, or null when never observed. */
|
|
38
|
+
stats: MenubarDeviceStats | null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* One device's hardware facts and last reading, projected from the fleet-stats
|
|
42
|
+
* cache the CLI already keeps (`~/.agents/.cache/.fleet-stats.json`).
|
|
43
|
+
*
|
|
44
|
+
* Cache-only by construction: the snapshot never probes the fleet, so opening
|
|
45
|
+
* Settings costs nothing and a device nobody has measured says so. Every value
|
|
46
|
+
* is `null` when that number was not observed — never 0, which would render as a
|
|
47
|
+
* real "idle, empty disk" reading. `stale` carries the CLI's own freshness bound
|
|
48
|
+
* so the menu can label an old reading instead of showing it as live, and
|
|
49
|
+
* `observedAt` / `specsObservedAt` give the observation age F25 asks for
|
|
50
|
+
* (hardware totals survive an unreachable probe; current-state readings do not).
|
|
51
|
+
*/
|
|
52
|
+
interface MenubarDeviceStats {
|
|
53
|
+
reachable: boolean;
|
|
54
|
+
observedAt: string;
|
|
55
|
+
stale: boolean;
|
|
56
|
+
cpus: number | null;
|
|
57
|
+
memTotalBytes: number | null;
|
|
58
|
+
memFreeBytes: number | null;
|
|
59
|
+
memPercent: number | null;
|
|
60
|
+
diskTotalBytes: number | null;
|
|
61
|
+
diskFreeBytes: number | null;
|
|
62
|
+
diskUsedPercent: number | null;
|
|
63
|
+
loadPercent: number | null;
|
|
64
|
+
specsObservedAt: string | null;
|
|
23
65
|
}
|
|
24
66
|
interface MenubarSnapshot {
|
|
25
67
|
version: 1;
|
|
@@ -55,6 +97,15 @@ interface MenubarSnapshot {
|
|
|
55
97
|
* (only `defaultProject`) is omitted so the native app keeps its own default.
|
|
56
98
|
*/
|
|
57
99
|
export declare function buildMenuPreferences(): Record<string, unknown>;
|
|
100
|
+
/**
|
|
101
|
+
* Project one cached {@link DeviceStats} row into the snapshot shape, or `null`
|
|
102
|
+
* when the device has no cached reading at all.
|
|
103
|
+
*
|
|
104
|
+
* `?? null` rather than a default: an absent number means "not observed", and the
|
|
105
|
+
* menu renders that as unavailable. A 0 would be indistinguishable from a real
|
|
106
|
+
* measurement of an idle box or a full disk.
|
|
107
|
+
*/
|
|
108
|
+
export declare function projectDeviceStats(row: DeviceStats | undefined): MenubarDeviceStats | null;
|
|
58
109
|
export declare function readLastWatchdogTick(stateDir?: string): WatchdogTickResult | null;
|
|
59
110
|
/** One-process read model for AGI Menu's repeating three-minute refresh. */
|
|
60
111
|
export declare function computeMenubarSnapshot(): Promise<MenubarSnapshot>;
|
|
@@ -2,7 +2,9 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { buildRoutineListJson } from '../scheduling/routines.js';
|
|
4
4
|
import { backfillActiveRowsFromIndex, isRunningLiveSession, serializeActiveSessionsForJson, serializeSessionsJson } from '../session/active.js';
|
|
5
|
-
import { getConfigValue, loadAutoLaunchPreferences } from '../device-config.js';
|
|
5
|
+
import { getConfigValue, listConfiguredDeviceRoles, loadAutoLaunchPreferences } from '../device-config.js';
|
|
6
|
+
import { filterAutoPool } from '../devices/pool.js';
|
|
7
|
+
import { isFreshDeviceStats, readStatsCache } from '../devices/stats-cache.js';
|
|
6
8
|
import { MENUBAR_MENU_PROPERTIES } from '../config-keys.js';
|
|
7
9
|
import { migrateMenubarPreferencesFromUserDefaults } from './migrate-prefs.js';
|
|
8
10
|
import { loadDevices } from '../devices/registry.js';
|
|
@@ -35,12 +37,20 @@ export function buildMenuPreferences() {
|
|
|
35
37
|
*/
|
|
36
38
|
async function buildMenubarDevices() {
|
|
37
39
|
const reg = await loadDevices();
|
|
40
|
+
const roster = Object.keys(reg);
|
|
38
41
|
// Pass the roster so a fleet-wide default (fleet.defaults.config) reaches
|
|
39
42
|
// devices that have no doc of their own.
|
|
40
|
-
const prefs = loadAutoLaunchPreferences(
|
|
43
|
+
const prefs = loadAutoLaunchPreferences(roster);
|
|
44
|
+
const roles = listConfiguredDeviceRoles(roster);
|
|
45
|
+
// The placement verdict from the one canonical filter — not a re-implementation
|
|
46
|
+
// of the role rule (`devices/pool.ts` owns it).
|
|
47
|
+
const autoEligible = new Set(filterAutoPool(roster, { roles, autoLaunch: prefs }));
|
|
48
|
+
// Cache read only: no ssh, no probe, so this still rides the existing snapshot
|
|
49
|
+
// poll (docs/menubar.md: the menu bar must not probe the fleet per render).
|
|
50
|
+
const stats = readStatsCache();
|
|
41
51
|
const interactiveHost = getConfigValue('interactive.host').value;
|
|
42
52
|
const self = machineId();
|
|
43
|
-
return
|
|
53
|
+
return roster
|
|
44
54
|
.sort()
|
|
45
55
|
.map((name) => ({
|
|
46
56
|
name,
|
|
@@ -50,8 +60,37 @@ async function buildMenubarDevices() {
|
|
|
50
60
|
interactive: name === interactiveHost,
|
|
51
61
|
isLocal: name === self,
|
|
52
62
|
preferred: prefs[name]?.preferred === true,
|
|
63
|
+
role: roles[name] ?? 'unknown',
|
|
64
|
+
autoEligible: autoEligible.has(name),
|
|
65
|
+
stats: projectDeviceStats(stats[name]),
|
|
53
66
|
}));
|
|
54
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Project one cached {@link DeviceStats} row into the snapshot shape, or `null`
|
|
70
|
+
* when the device has no cached reading at all.
|
|
71
|
+
*
|
|
72
|
+
* `?? null` rather than a default: an absent number means "not observed", and the
|
|
73
|
+
* menu renders that as unavailable. A 0 would be indistinguishable from a real
|
|
74
|
+
* measurement of an idle box or a full disk.
|
|
75
|
+
*/
|
|
76
|
+
export function projectDeviceStats(row) {
|
|
77
|
+
if (!row)
|
|
78
|
+
return null;
|
|
79
|
+
return {
|
|
80
|
+
reachable: row.reachable,
|
|
81
|
+
observedAt: new Date(row.fetchedAt).toISOString(),
|
|
82
|
+
stale: !isFreshDeviceStats(row),
|
|
83
|
+
cpus: row.ncpu ?? null,
|
|
84
|
+
memTotalBytes: row.memTotalBytes ?? null,
|
|
85
|
+
memFreeBytes: row.memFreeBytes ?? null,
|
|
86
|
+
memPercent: row.memPercent ?? null,
|
|
87
|
+
diskTotalBytes: row.diskTotalBytes ?? null,
|
|
88
|
+
diskFreeBytes: row.diskFreeBytes ?? null,
|
|
89
|
+
diskUsedPercent: row.diskUsedPercent ?? null,
|
|
90
|
+
loadPercent: row.loadPercent ?? null,
|
|
91
|
+
specsObservedAt: row.specsFetchedAt ? new Date(row.specsFetchedAt).toISOString() : null,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
55
94
|
export function readLastWatchdogTick(stateDir = path.join(getRuntimeStateDir(), 'watchdog')) {
|
|
56
95
|
try {
|
|
57
96
|
return JSON.parse(fs.readFileSync(path.join(stateDir, 'last-tick.json'), 'utf-8'));
|
package/dist/lib/open-url.js
CHANGED
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
* The OS handler has none of that.
|
|
10
10
|
*
|
|
11
11
|
* Before this seam existed, `agents browser navigate` honoured the configured
|
|
12
|
-
* profile and nothing else did: `
|
|
12
|
+
* profile and nothing else did: `devices lease`, `feedback`, and
|
|
13
13
|
* the browser-session artifact opener each shelled straight to `open`/`xdg-open`,
|
|
14
14
|
* so every one of them landed in whatever the OS handler happened to be. This
|
|
15
|
-
* module replaces all of those call sites; do not add a
|
|
15
|
+
* module replaces all of those call sites; do not add a new raw `open`.
|
|
16
16
|
*
|
|
17
17
|
* Never throws. A viewer that cannot be reached degrades to the OS handler with
|
|
18
18
|
* one stderr line naming the reason, and a total failure returns `via: 'none'`
|
package/dist/lib/probe.d.ts
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
* timeout — matching the `execFileAsync` contract the version probe had — and
|
|
4
4
|
* reaps the probe's whole process group on every settle path.
|
|
5
5
|
*/
|
|
6
|
-
export declare function probeCapture(cmd: string, args: string[], timeoutMs: number
|
|
6
|
+
export declare function probeCapture(cmd: string, args: string[], timeoutMs: number, options?: {
|
|
7
|
+
acceptedExitCodes?: number[];
|
|
8
|
+
maxOutputBytes?: number;
|
|
9
|
+
}): Promise<{
|
|
7
10
|
stdout: string;
|
|
8
11
|
}>;
|
package/dist/lib/probe.js
CHANGED
|
@@ -59,7 +59,7 @@ function reapGroup(pid) {
|
|
|
59
59
|
* timeout — matching the `execFileAsync` contract the version probe had — and
|
|
60
60
|
* reaps the probe's whole process group on every settle path.
|
|
61
61
|
*/
|
|
62
|
-
export function probeCapture(cmd, args, timeoutMs) {
|
|
62
|
+
export function probeCapture(cmd, args, timeoutMs, options = {}) {
|
|
63
63
|
return new Promise((resolve, reject) => {
|
|
64
64
|
const child = spawn(cmd, args, {
|
|
65
65
|
detached: GROUP_REAP,
|
|
@@ -92,6 +92,9 @@ export function probeCapture(cmd, args, timeoutMs) {
|
|
|
92
92
|
child.stdout?.setEncoding('utf8');
|
|
93
93
|
child.stdout?.on('data', (d) => {
|
|
94
94
|
out += d;
|
|
95
|
+
if (Buffer.byteLength(out) > (options.maxOutputBytes ?? 1024 * 1024)) {
|
|
96
|
+
settle(new Error(`probe output exceeded limit: ${cmd}`));
|
|
97
|
+
}
|
|
95
98
|
});
|
|
96
99
|
child.on('error', (e) => settle(e));
|
|
97
100
|
// 'exit', not 'close': a forked grandchild inherits the stdout pipe, and
|
|
@@ -99,7 +102,7 @@ export function probeCapture(cmd, args, timeoutMs) {
|
|
|
99
102
|
// process this helper exists to reap. Settle when the probed binary
|
|
100
103
|
// itself exits; one tick's grace lets its final stdout chunks land.
|
|
101
104
|
child.on('exit', (code) => {
|
|
102
|
-
setImmediate(() => settle(code
|
|
105
|
+
setImmediate(() => settle(code !== null && (options.acceptedExitCodes ?? [0]).includes(code) ? null : new Error(`probe exited ${code}: ${cmd} ${args.join(' ')}`)));
|
|
103
106
|
});
|
|
104
107
|
});
|
|
105
108
|
}
|
package/dist/lib/projects.d.ts
CHANGED
|
@@ -207,6 +207,29 @@ export declare function projectDirsAbs(def: ProjectDef, opts: {
|
|
|
207
207
|
* home-relative variant lands (see docs/concepts.md).
|
|
208
208
|
*/
|
|
209
209
|
export declare function projectNameForCwd(cwd: string | undefined, defs: ProjectDef[]): string | undefined;
|
|
210
|
+
export declare function listProjectDefsCached(): ProjectDef[];
|
|
211
|
+
/** Drop the {@link listProjectDefsCached} memo — for tests that rewrite the dir within one mtime tick. */
|
|
212
|
+
export declare function resetProjectDefsCache(): void;
|
|
213
|
+
/**
|
|
214
|
+
* The **confirmed** project for a working directory, or `undefined` when the
|
|
215
|
+
* association is not confirmed (PHNX-3999 F08/F09).
|
|
216
|
+
*
|
|
217
|
+
* Confirmed means exactly one thing: a registered project definition
|
|
218
|
+
* ({@link projectNameForCwd}) whose root contains this path. Being inside *some*
|
|
219
|
+
* git repository is NOT a project association — a repo nobody registered, a
|
|
220
|
+
* checkout of someone else's code, or a loose directory that merely has a
|
|
221
|
+
* `.git` in it would each invent a project name out of a folder, which is the
|
|
222
|
+
* wrong-grouping the owner's recording shows at 01:30–01:56. So is a bare
|
|
223
|
+
* directory basename ({@link resolveProjectKey}, which always answers).
|
|
224
|
+
*
|
|
225
|
+
* A consumer renders `undefined` as Uncategorized and keeps the session
|
|
226
|
+
* reachable in its full list — nothing is hidden by not being grouped.
|
|
227
|
+
*
|
|
228
|
+
* Use {@link resolveProjectNameForCwd} instead when you want a best-effort
|
|
229
|
+
* bucket KEY for joining rows; use this when the answer is shown to a person as
|
|
230
|
+
* "this work belongs to that project".
|
|
231
|
+
*/
|
|
232
|
+
export declare function confirmedProjectForCwd(cwd: string | undefined | null, defs?: ProjectDef[]): string | undefined;
|
|
210
233
|
/**
|
|
211
234
|
* The canonical project label for a cwd, for every surface that buckets work by
|
|
212
235
|
* project (the activity timeline, feed posts, the sessions overview): the
|