@phnx-labs/agents-cli 1.20.34 → 1.20.36
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 +113 -0
- package/LICENSE +185 -21
- package/README.md +13 -5
- package/dist/commands/audit.d.ts +14 -0
- package/dist/commands/audit.js +68 -0
- package/dist/commands/browser.js +82 -8
- package/dist/commands/check.d.ts +15 -0
- package/dist/commands/check.js +84 -0
- package/dist/commands/cloud.js +143 -3
- package/dist/commands/computer-actions.d.ts +15 -0
- package/dist/commands/computer-actions.js +120 -22
- package/dist/commands/computer.js +81 -0
- package/dist/commands/cost.js +17 -12
- package/dist/commands/daemon.js +4 -1
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +54 -97
- package/dist/commands/events.d.ts +16 -0
- package/dist/commands/events.js +173 -0
- package/dist/commands/exec.d.ts +19 -0
- package/dist/commands/exec.js +283 -12
- package/dist/commands/hosts.js +37 -29
- package/dist/commands/inspect.d.ts +2 -0
- package/dist/commands/inspect.js +62 -16
- package/dist/commands/lock.d.ts +12 -0
- package/dist/commands/lock.js +70 -0
- package/dist/commands/logs.d.ts +17 -0
- package/dist/commands/logs.js +139 -0
- package/dist/commands/message.d.ts +15 -0
- package/dist/commands/message.js +56 -0
- package/dist/commands/models.d.ts +3 -0
- package/dist/commands/models.js +27 -8
- package/dist/commands/permissions.js +9 -2
- package/dist/commands/repo.d.ts +34 -0
- package/dist/commands/repo.js +243 -65
- package/dist/commands/resource-view.d.ts +20 -0
- package/dist/commands/resource-view.js +90 -28
- package/dist/commands/routines.js +101 -5
- package/dist/commands/secrets-migrate.js +106 -57
- package/dist/commands/secrets.d.ts +31 -18
- package/dist/commands/secrets.js +196 -91
- package/dist/commands/serve.d.ts +10 -0
- package/dist/commands/serve.js +37 -0
- package/dist/commands/sessions-inject.d.ts +14 -0
- package/dist/commands/sessions-inject.js +111 -0
- package/dist/commands/sessions-picker.d.ts +2 -0
- package/dist/commands/sessions-picker.js +24 -3
- package/dist/commands/sessions-resume.d.ts +2 -0
- package/dist/commands/sessions-resume.js +221 -0
- package/dist/commands/sessions-tail.d.ts +10 -0
- package/dist/commands/sessions-tail.js +11 -0
- package/dist/commands/sessions.d.ts +143 -1
- package/dist/commands/sessions.js +628 -80
- package/dist/commands/setup.js +4 -2
- package/dist/commands/sync.d.ts +3 -1
- package/dist/commands/sync.js +156 -4
- package/dist/commands/teams.js +217 -0
- package/dist/commands/versions.js +2 -4
- package/dist/commands/view.d.ts +3 -0
- package/dist/commands/view.js +19 -8
- package/dist/commands/wallet.d.ts +6 -0
- package/dist/commands/wallet.js +22 -5
- package/dist/commands/watchdog.d.ts +18 -0
- package/dist/commands/watchdog.js +238 -0
- package/dist/index.js +82 -32
- package/dist/lib/agents.js +30 -9
- package/dist/lib/audit/log.d.ts +92 -0
- package/dist/lib/audit/log.js +177 -0
- package/dist/lib/auto-pull.js +2 -1
- package/dist/lib/browser/cdp.js +5 -1
- package/dist/lib/browser/chrome.d.ts +10 -0
- package/dist/lib/browser/chrome.js +18 -7
- package/dist/lib/browser/drivers/ssh.d.ts +8 -0
- package/dist/lib/browser/drivers/ssh.js +77 -16
- package/dist/lib/browser/har.d.ts +84 -0
- package/dist/lib/browser/har.js +77 -0
- package/dist/lib/browser/ipc.d.ts +3 -0
- package/dist/lib/browser/ipc.js +37 -12
- package/dist/lib/browser/profiles.d.ts +1 -1
- package/dist/lib/browser/profiles.js +8 -10
- package/dist/lib/browser/refs.d.ts +65 -0
- package/dist/lib/browser/refs.js +73 -1
- package/dist/lib/browser/runtime-state.js +1 -0
- package/dist/lib/browser/service.d.ts +38 -2
- package/dist/lib/browser/service.js +183 -23
- package/dist/lib/browser/types.d.ts +14 -1
- package/dist/lib/budget/live-cloud.d.ts +42 -0
- package/dist/lib/budget/live-cloud.js +79 -0
- package/dist/lib/budget/live-team.d.ts +31 -0
- package/dist/lib/budget/live-team.js +115 -0
- package/dist/lib/cloud/codex.js +4 -0
- package/dist/lib/cloud/rush.d.ts +12 -1
- package/dist/lib/cloud/rush.js +13 -3
- package/dist/lib/cloud/types.d.ts +9 -0
- package/dist/lib/computer/dispatch.d.ts +8 -0
- package/dist/lib/computer/dispatch.js +125 -0
- package/dist/lib/computer/loop.d.ts +62 -0
- package/dist/lib/computer/loop.js +98 -0
- package/dist/lib/computer/model.d.ts +44 -0
- package/dist/lib/computer/model.js +157 -0
- package/dist/lib/concurrency.d.ts +19 -0
- package/dist/lib/concurrency.js +33 -0
- package/dist/lib/daemon.d.ts +92 -0
- package/dist/lib/daemon.js +222 -18
- package/dist/lib/devices/connect.d.ts +3 -2
- package/dist/lib/devices/connect.js +5 -3
- package/dist/lib/devices/registry.d.ts +7 -0
- package/dist/lib/devices/registry.js +24 -0
- package/dist/lib/devices/tailscale.js +1 -1
- package/dist/lib/drift.d.ts +52 -0
- package/dist/lib/drift.js +112 -0
- package/dist/lib/events.d.ts +10 -3
- package/dist/lib/events.js +101 -24
- package/dist/lib/exec.d.ts +17 -0
- package/dist/lib/exec.js +100 -12
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.js +56 -1
- package/dist/lib/hooks/cache.d.ts +6 -0
- package/dist/lib/hooks/cache.js +54 -12
- package/dist/lib/hooks.d.ts +27 -0
- package/dist/lib/hooks.js +135 -10
- package/dist/lib/hosts/dispatch.d.ts +15 -0
- package/dist/lib/hosts/dispatch.js +45 -13
- package/dist/lib/hosts/logs.d.ts +16 -0
- package/dist/lib/hosts/logs.js +74 -0
- package/dist/lib/hosts/option.js +1 -1
- package/dist/lib/hosts/passthrough.js +3 -1
- package/dist/lib/hosts/progress.d.ts +66 -0
- package/dist/lib/hosts/progress.js +125 -17
- package/dist/lib/hosts/ready.d.ts +51 -7
- package/dist/lib/hosts/ready.js +98 -25
- package/dist/lib/hosts/reconcile.d.ts +53 -0
- package/dist/lib/hosts/reconcile.js +81 -0
- package/dist/lib/hosts/registry.d.ts +19 -2
- package/dist/lib/hosts/registry.js +58 -2
- package/dist/lib/hosts/remote-cmd.d.ts +62 -1
- package/dist/lib/hosts/remote-cmd.js +70 -1
- package/dist/lib/hosts/remote-os.d.ts +17 -0
- package/dist/lib/hosts/remote-os.js +30 -0
- package/dist/lib/hosts/session-index.d.ts +34 -0
- package/dist/lib/hosts/session-index.js +56 -0
- package/dist/lib/hosts/tasks.d.ts +22 -0
- package/dist/lib/hosts/tasks.js +29 -0
- package/dist/lib/lock.d.ts +93 -0
- package/dist/lib/lock.js +207 -0
- package/dist/lib/loop.js +16 -1
- package/dist/lib/machine-id.d.ts +21 -0
- package/dist/lib/machine-id.js +26 -0
- package/dist/lib/mailbox-target.d.ts +36 -0
- package/dist/lib/mailbox-target.js +45 -0
- package/dist/lib/mailbox.d.ts +47 -0
- package/dist/lib/mailbox.js +194 -0
- package/dist/lib/mcp.d.ts +5 -0
- package/dist/lib/mcp.js +24 -8
- package/dist/lib/migrate.d.ts +19 -0
- package/dist/lib/migrate.js +134 -26
- package/dist/lib/overdue.js +3 -0
- package/dist/lib/permissions.d.ts +4 -0
- package/dist/lib/permissions.js +35 -0
- package/dist/lib/picker.d.ts +26 -0
- package/dist/lib/picker.js +127 -0
- package/dist/lib/platform/exec.d.ts +46 -0
- package/dist/lib/platform/exec.js +74 -0
- package/dist/lib/platform/process.d.ts +31 -0
- package/dist/lib/platform/process.js +34 -1
- package/dist/lib/platform/winpath.js +2 -0
- package/dist/lib/plugin-marketplace.d.ts +30 -0
- package/dist/lib/plugin-marketplace.js +215 -2
- package/dist/lib/plugins.d.ts +5 -0
- package/dist/lib/plugins.js +61 -10
- package/dist/lib/profiles.d.ts +25 -0
- package/dist/lib/profiles.js +22 -6
- package/dist/lib/pty-client.js +2 -1
- package/dist/lib/rotate.d.ts +61 -0
- package/dist/lib/rotate.js +52 -0
- package/dist/lib/routines.d.ts +40 -2
- package/dist/lib/routines.js +66 -8
- package/dist/lib/runner.d.ts +11 -2
- package/dist/lib/runner.js +49 -7
- package/dist/lib/scheduler.js +6 -1
- package/dist/lib/secrets/bundles.d.ts +60 -4
- package/dist/lib/secrets/bundles.js +131 -12
- package/dist/lib/secrets/filestore.d.ts +3 -0
- package/dist/lib/secrets/filestore.js +42 -16
- package/dist/lib/secrets/index.d.ts +43 -2
- package/dist/lib/secrets/index.js +102 -3
- package/dist/lib/secrets/mcp.d.ts +93 -0
- package/dist/lib/secrets/mcp.js +205 -0
- package/dist/lib/secrets/remote.js +12 -5
- package/dist/lib/secrets/sync.js +83 -4
- package/dist/lib/secrets/windows.js +14 -3
- package/dist/lib/serve/data.d.ts +81 -0
- package/dist/lib/serve/data.js +91 -0
- package/dist/lib/serve/page.d.ts +7 -0
- package/dist/lib/serve/page.js +140 -0
- package/dist/lib/serve/server.d.ts +46 -0
- package/dist/lib/serve/server.js +115 -0
- package/dist/lib/session/active.d.ts +61 -0
- package/dist/lib/session/active.js +196 -17
- package/dist/lib/session/discover.d.ts +37 -0
- package/dist/lib/session/discover.js +204 -27
- package/dist/lib/session/inject.d.ts +18 -0
- package/dist/lib/session/inject.js +21 -0
- package/dist/lib/session/parse.d.ts +6 -0
- package/dist/lib/session/parse.js +286 -2
- package/dist/lib/session/pid-registry.d.ts +22 -0
- package/dist/lib/session/pid-registry.js +104 -0
- package/dist/lib/session/provenance.d.ts +14 -2
- package/dist/lib/session/provenance.js +39 -8
- package/dist/lib/session/remote-active.d.ts +26 -0
- package/dist/lib/session/remote-active.js +153 -0
- package/dist/lib/session/remote-list.d.ts +51 -0
- package/dist/lib/session/remote-list.js +213 -0
- package/dist/lib/session/remote.d.ts +7 -1
- package/dist/lib/session/remote.js +18 -8
- package/dist/lib/session/render.js +1 -1
- package/dist/lib/session/sync/config.d.ts +1 -7
- package/dist/lib/session/sync/config.js +4 -11
- package/dist/lib/session/types.d.ts +18 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/shims.d.ts +44 -6
- package/dist/lib/shims.js +143 -57
- package/dist/lib/ssh-exec.d.ts +26 -3
- package/dist/lib/ssh-exec.js +47 -3
- package/dist/lib/ssh-tunnel.d.ts +24 -5
- package/dist/lib/ssh-tunnel.js +62 -63
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +8 -0
- package/dist/lib/startup/command-registry.js +17 -1
- package/dist/lib/state.d.ts +13 -0
- package/dist/lib/state.js +103 -9
- package/dist/lib/sync-umbrella.d.ts +14 -7
- package/dist/lib/sync-umbrella.js +17 -9
- package/dist/lib/teams/forEach.d.ts +110 -0
- package/dist/lib/teams/forEach.js +186 -0
- package/dist/lib/teams/index.d.ts +1 -0
- package/dist/lib/teams/index.js +1 -0
- package/dist/lib/teams/pr-watch.d.ts +226 -0
- package/dist/lib/teams/pr-watch.js +371 -0
- package/dist/lib/teams/registry.js +25 -9
- package/dist/lib/teams/supervisor.d.ts +14 -1
- package/dist/lib/teams/supervisor.js +19 -0
- package/dist/lib/teams/worktree.d.ts +9 -0
- package/dist/lib/teams/worktree.js +32 -0
- package/dist/lib/terminal/backends/ghostty.d.ts +6 -0
- package/dist/lib/terminal/backends/ghostty.js +69 -0
- package/dist/lib/terminal/backends/index.d.ts +19 -0
- package/dist/lib/terminal/backends/index.js +31 -0
- package/dist/lib/terminal/backends/iterm.d.ts +6 -0
- package/dist/lib/terminal/backends/iterm.js +62 -0
- package/dist/lib/terminal/backends/tmux.d.ts +14 -0
- package/dist/lib/terminal/backends/tmux.js +23 -0
- package/dist/lib/terminal/backends/vscodium-agent.d.ts +31 -0
- package/dist/lib/terminal/backends/vscodium-agent.js +72 -0
- package/dist/lib/terminal/engine.d.ts +39 -0
- package/dist/lib/terminal/engine.js +54 -0
- package/dist/lib/terminal/index.d.ts +17 -0
- package/dist/lib/terminal/index.js +8 -0
- package/dist/lib/terminal/inject.d.ts +204 -0
- package/dist/lib/terminal/inject.js +247 -0
- package/dist/lib/terminal/policy.d.ts +11 -0
- package/dist/lib/terminal/policy.js +11 -0
- package/dist/lib/terminal/quote.d.ts +11 -0
- package/dist/lib/terminal/quote.js +13 -0
- package/dist/lib/terminal/resolve.d.ts +64 -0
- package/dist/lib/terminal/resolve.js +90 -0
- package/dist/lib/terminal/shell.d.ts +6 -0
- package/dist/lib/terminal/shell.js +23 -0
- package/dist/lib/terminal/transport.d.ts +15 -0
- package/dist/lib/terminal/transport.js +40 -0
- package/dist/lib/terminal/types.d.ts +59 -0
- package/dist/lib/terminal/types.js +13 -0
- package/dist/lib/triggers/webhook.d.ts +85 -0
- package/dist/lib/triggers/webhook.js +141 -0
- package/dist/lib/types.d.ts +7 -0
- package/dist/lib/versions.d.ts +30 -3
- package/dist/lib/versions.js +129 -18
- package/dist/lib/watchdog/index.d.ts +3 -0
- package/dist/lib/watchdog/index.js +5 -0
- package/dist/lib/watchdog/read.d.ts +35 -0
- package/dist/lib/watchdog/read.js +149 -0
- package/dist/lib/watchdog/runner.d.ts +127 -0
- package/dist/lib/watchdog/runner.js +322 -0
- package/dist/lib/watchdog/watchdog.d.ts +40 -0
- package/dist/lib/watchdog/watchdog.js +166 -0
- package/dist/lib/watchdog/watchdogTail.d.ts +5 -0
- package/dist/lib/watchdog/watchdogTail.js +154 -0
- package/dist/lib/whats-new.d.ts +9 -0
- package/dist/lib/whats-new.js +35 -0
- package/dist/lib/workflows.d.ts +166 -0
- package/dist/lib/workflows.js +193 -0
- package/package.json +5 -4
package/dist/lib/shims.d.ts
CHANGED
|
@@ -77,7 +77,7 @@ export interface ConflictInfo {
|
|
|
77
77
|
* top-level entry add/remove — deep edits to plugin contents won't
|
|
78
78
|
* trigger auto-resync, run `agents sync` for that.
|
|
79
79
|
*/
|
|
80
|
-
export declare const SHIM_SCHEMA_VERSION =
|
|
80
|
+
export declare const SHIM_SCHEMA_VERSION = 22;
|
|
81
81
|
/**
|
|
82
82
|
* Generate the full bash shim script for the given agent. The returned string
|
|
83
83
|
* is written to ~/.agents/shims/{cliCommand} and made executable.
|
|
@@ -126,8 +126,27 @@ export declare function removeShim(agent: AgentId): boolean;
|
|
|
126
126
|
* hardcoded node_modules/.bin, which never exists for these three and
|
|
127
127
|
* made every versioned alias (the path `agents teams` pins to) fail
|
|
128
128
|
* with "<agent>@<version> not installed". Also emit GROK_HOME.
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
* v9 — kimi was wrong in v8: it npm-installs @moonshot-ai/kimi-code into
|
|
130
|
+
* node_modules/.bin/kimi (grok/droid ship native binaries elsewhere,
|
|
131
|
+
* kimi does not). The ~/.kimi-code/bin path never existed for an npm
|
|
132
|
+
* install and the `command -v kimi` fallback resolved to this alias's
|
|
133
|
+
* sibling dispatcher shim, re-exec-looping forever. Resolve kimi via the
|
|
134
|
+
* generic node_modules/.bin branch.
|
|
135
|
+
* v10 — guard grok's `command -v grok` fallback against resolving to our own
|
|
136
|
+
* shims dir (same infinite re-exec loop), mirroring droid.
|
|
137
|
+
* v11 — export DISABLE_AUTOUPDATER=1 for claude aliases so a pinned per-version
|
|
138
|
+
* install can't self-mutate via Claude Code's background auto-updater.
|
|
139
|
+
* Explicit user value wins.
|
|
140
|
+
* v12 — Windows: stop writing the extensionless bash alias next to the `.cmd`
|
|
141
|
+
* (and delete a lingering one). The version suffix contains dots, so
|
|
142
|
+
* cmd.exe and PowerShell treat `claude@2.1.201` as a complete filename
|
|
143
|
+
* with extension `.201`, exact-match the bash script AHEAD of PATHEXT
|
|
144
|
+
* probing, and ShellExecute it to the `.sh` editor association — the
|
|
145
|
+
* editor opens the script and the agent never launches. The `.cmd` is
|
|
146
|
+
* now the only Windows artifact and carries this alias marker so
|
|
147
|
+
* staleness checks read it directly.
|
|
148
|
+
*/
|
|
149
|
+
export declare const VERSIONED_ALIAS_SCHEMA_VERSION = 12;
|
|
131
150
|
/**
|
|
132
151
|
* Generate a versioned alias script that directly execs a specific version.
|
|
133
152
|
* e.g., claude@2.0.65 -> directly runs that version's binary
|
|
@@ -149,20 +168,39 @@ export declare function isVersionedAliasCurrent(agent: AgentId, version: string)
|
|
|
149
168
|
*/
|
|
150
169
|
export declare function ensureVersionedAliasCurrent(agent: AgentId, version: string): 'created' | 'updated' | 'current';
|
|
151
170
|
/**
|
|
152
|
-
* Get the filesystem path for a versioned alias script
|
|
171
|
+
* Get the filesystem path for a versioned alias script — the logical
|
|
172
|
+
* (extensionless) launch name. On Windows this is not a real file (see
|
|
173
|
+
* versionedAliasOnDiskFile); stat/read checks must use the on-disk path.
|
|
153
174
|
*/
|
|
154
175
|
export declare function getVersionedAliasPath(agent: AgentId, version: string): string;
|
|
176
|
+
/**
|
|
177
|
+
* The file createVersionedAlias actually materializes for a platform:
|
|
178
|
+
* `<cmd>@<version>.cmd` on Windows, the bare bash script on POSIX. Pure —
|
|
179
|
+
* testable on any host. Mirrors onDiskShimFile for the main shim.
|
|
180
|
+
*/
|
|
181
|
+
export declare function versionedAliasOnDiskFile(cliCommand: string, version: string, platform: NodeJS.Platform): string;
|
|
155
182
|
/**
|
|
156
183
|
* Create a versioned alias for a specific agent version.
|
|
157
184
|
* e.g., claude@2.0.65
|
|
185
|
+
*
|
|
186
|
+
* Same platform split as createShim (shimTargetsFor): POSIX writes the
|
|
187
|
+
* extensionless bash script; Windows writes ONLY the `.cmd`. Unlike the main
|
|
188
|
+
* shim — where the bash file was merely dead weight — a bash alias next to the
|
|
189
|
+
* versioned `.cmd` is actively harmful: the dotted version suffix makes
|
|
190
|
+
* cmd.exe/PowerShell treat `claude@2.1.201` as a complete filename (extension
|
|
191
|
+
* `.201`), so the exact match wins over PATHEXT probing and the shell
|
|
192
|
+
* ShellExecutes the bash script to the `.sh` editor association — the editor
|
|
193
|
+
* opens, the agent never launches. Any legacy bash alias is deleted here.
|
|
158
194
|
*/
|
|
159
195
|
export declare function createVersionedAlias(agent: AgentId, version: string): string;
|
|
160
196
|
/**
|
|
161
|
-
* Remove a versioned alias for a specific agent version.
|
|
197
|
+
* Remove a versioned alias for a specific agent version. Removes whichever
|
|
198
|
+
* companions exist — the extensionless script (POSIX, or a legacy Windows
|
|
199
|
+
* install that wrote it) AND the `.cmd` (Windows) — mirroring removeShim.
|
|
162
200
|
*/
|
|
163
201
|
export declare function removeVersionedAlias(agent: AgentId, version: string): boolean;
|
|
164
202
|
/**
|
|
165
|
-
* Check if a versioned alias exists.
|
|
203
|
+
* Check if a versioned alias exists (the on-disk artifact for this platform).
|
|
166
204
|
*/
|
|
167
205
|
export declare function versionedAliasExists(agent: AgentId, version: string): boolean;
|
|
168
206
|
/**
|
package/dist/lib/shims.js
CHANGED
|
@@ -202,7 +202,16 @@ async function promptConflictStrategy(conflictInfos) {
|
|
|
202
202
|
* top-level entry add/remove — deep edits to plugin contents won't
|
|
203
203
|
* trigger auto-resync, run `agents sync` for that.
|
|
204
204
|
*/
|
|
205
|
-
|
|
205
|
+
// v20 — stop treating kimi like grok/droid: it npm-installs into
|
|
206
|
+
// node_modules/.bin/kimi, so resolve it via the generic branch. The old
|
|
207
|
+
// ~/.kimi-code/bin special-case never existed for npm installs and
|
|
208
|
+
// re-exec-looped through `command -v kimi` (the dispatcher itself).
|
|
209
|
+
// v21 — guard grok's `command -v grok` fallback against resolving to our own
|
|
210
|
+
// shims dir (same infinite re-exec loop), mirroring droid.
|
|
211
|
+
// v22 — export DISABLE_AUTOUPDATER=1 for claude shims so a pinned per-version
|
|
212
|
+
// install can't self-mutate: Claude Code's background auto-updater would
|
|
213
|
+
// otherwise rewrite the pinned binary in place. Explicit user value wins.
|
|
214
|
+
export const SHIM_SCHEMA_VERSION = 22;
|
|
206
215
|
/** Internal marker string used to embed the schema version in shim scripts. */
|
|
207
216
|
const SHIM_VERSION_MARKER = 'agents-shim-version:';
|
|
208
217
|
function shellQuote(value) {
|
|
@@ -230,6 +239,10 @@ export function generateShimScript(agent) {
|
|
|
230
239
|
# selected version's config directory so switching versions also switches the
|
|
231
240
|
# live Claude account.
|
|
232
241
|
export CLAUDE_CONFIG_DIR="$VERSION_DIR/home/${configDirName}"
|
|
242
|
+
# Managed installs are pinned in a per-version dir; Claude Code's background
|
|
243
|
+
# auto-updater would rewrite the pinned binary in place. Disable it so a pin
|
|
244
|
+
# stays a pin. An explicit user value always wins.
|
|
245
|
+
export DISABLE_AUTOUPDATER="\${DISABLE_AUTOUPDATER:-1}"
|
|
233
246
|
# On Linux sandboxes (no keychain), fall back to a per-version token file.
|
|
234
247
|
# The env var always wins if already set; no-op on macOS.
|
|
235
248
|
if [ "\$(uname -s)" = "Linux" ] && [ -z "\${CLAUDE_CODE_OAUTH_TOKEN:-}" ] && [ -f "\$CLAUDE_CONFIG_DIR/.oauth_token" ]; then
|
|
@@ -401,19 +414,25 @@ if [ "$AGENT" = "grok" ]; then
|
|
|
401
414
|
fi
|
|
402
415
|
fi
|
|
403
416
|
if [ -z "$BINARY" ] || [ ! -x "$BINARY" ]; then
|
|
404
|
-
# Last resort: whatever is on PATH (user may have installed grok globally)
|
|
417
|
+
# Last resort: whatever is on PATH (user may have installed grok globally).
|
|
418
|
+
# Refuse anything under our own shims dir: the shims dir sits ahead of
|
|
419
|
+
# ~/.local/bin on PATH, so "command -v grok" resolves to THIS dispatcher.
|
|
420
|
+
# exec-ing it would re-enter and spin in an infinite re-exec loop (the same
|
|
421
|
+
# bug the droid branch below guards against). Fall through to the clean
|
|
422
|
+
# "not installed" error instead.
|
|
405
423
|
BINARY=$(command -v grok 2>/dev/null || echo "")
|
|
424
|
+
case "$BINARY" in
|
|
425
|
+
"$AGENTS_USER_DIR/.cache/shims/"*) BINARY="" ;;
|
|
426
|
+
esac
|
|
406
427
|
fi
|
|
407
|
-
# Kimi
|
|
408
|
-
#
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
BINARY=$(command -v kimi 2>/dev/null || echo "")
|
|
416
|
-
fi
|
|
428
|
+
# Kimi is a normal npm agent: "agents add kimi" npm-installs
|
|
429
|
+
# @moonshot-ai/kimi-code into the version dir and the binary lands at
|
|
430
|
+
# node_modules/.bin/kimi (a curl-installed kimi is symlinked to the same spot
|
|
431
|
+
# by installVersion). So kimi resolves via the generic node_modules branch
|
|
432
|
+
# below -- never a bespoke ~/.kimi-code/bin path that does not exist for npm
|
|
433
|
+
# installs and fell back to "command -v kimi", which resolves to THIS
|
|
434
|
+
# dispatcher (shims dir is ahead on PATH) and re-execs forever. Only
|
|
435
|
+
# KIMI_CODE_HOME (config isolation) stays special-cased, separately below.
|
|
417
436
|
# Droid (Factory AI) special case: the official installer drops a standalone
|
|
418
437
|
# native binary at ~/.local/bin/droid — there is no npm package and nothing
|
|
419
438
|
# lands in node_modules/.bin. Resolve the fixed install path directly. The
|
|
@@ -568,12 +587,18 @@ export function createShim(agent) {
|
|
|
568
587
|
* `cliCommand@version` for a versioned alias. node + the dist entrypoint are
|
|
569
588
|
* resolved at generation time so the launcher does not depend on `agents`
|
|
570
589
|
* already being on PATH.
|
|
590
|
+
*
|
|
591
|
+
* `extraMarkerLines` lets callers stamp additional schema markers into the
|
|
592
|
+
* header — versioned aliases embed their own alias-schema marker so
|
|
593
|
+
* readVersionedAliasSchemaVersion can stat the `.cmd` (the only Windows
|
|
594
|
+
* artifact) the same way it reads the bash script on POSIX.
|
|
571
595
|
*/
|
|
572
|
-
function writeWindowsCmdShim(cmdPath, spec) {
|
|
596
|
+
function writeWindowsCmdShim(cmdPath, spec, extraMarkerLines = []) {
|
|
573
597
|
const indexJs = getAgentsBinForGeneratedShim();
|
|
574
598
|
const content = `@echo off\r\n` +
|
|
575
599
|
`rem Auto-generated by agents-cli - do not edit\r\n` +
|
|
576
600
|
`rem ${SHIM_VERSION_MARKER} ${SHIM_SCHEMA_VERSION}\r\n` +
|
|
601
|
+
extraMarkerLines.map((line) => `rem ${line}\r\n`).join('') +
|
|
577
602
|
`node "${indexJs}" __shim ${spec} %*\r\n`;
|
|
578
603
|
fs.writeFileSync(cmdPath, content);
|
|
579
604
|
}
|
|
@@ -620,8 +645,27 @@ export function removeShim(agent) {
|
|
|
620
645
|
* hardcoded node_modules/.bin, which never exists for these three and
|
|
621
646
|
* made every versioned alias (the path `agents teams` pins to) fail
|
|
622
647
|
* with "<agent>@<version> not installed". Also emit GROK_HOME.
|
|
648
|
+
* v9 — kimi was wrong in v8: it npm-installs @moonshot-ai/kimi-code into
|
|
649
|
+
* node_modules/.bin/kimi (grok/droid ship native binaries elsewhere,
|
|
650
|
+
* kimi does not). The ~/.kimi-code/bin path never existed for an npm
|
|
651
|
+
* install and the `command -v kimi` fallback resolved to this alias's
|
|
652
|
+
* sibling dispatcher shim, re-exec-looping forever. Resolve kimi via the
|
|
653
|
+
* generic node_modules/.bin branch.
|
|
654
|
+
* v10 — guard grok's `command -v grok` fallback against resolving to our own
|
|
655
|
+
* shims dir (same infinite re-exec loop), mirroring droid.
|
|
656
|
+
* v11 — export DISABLE_AUTOUPDATER=1 for claude aliases so a pinned per-version
|
|
657
|
+
* install can't self-mutate via Claude Code's background auto-updater.
|
|
658
|
+
* Explicit user value wins.
|
|
659
|
+
* v12 — Windows: stop writing the extensionless bash alias next to the `.cmd`
|
|
660
|
+
* (and delete a lingering one). The version suffix contains dots, so
|
|
661
|
+
* cmd.exe and PowerShell treat `claude@2.1.201` as a complete filename
|
|
662
|
+
* with extension `.201`, exact-match the bash script AHEAD of PATHEXT
|
|
663
|
+
* probing, and ShellExecute it to the `.sh` editor association — the
|
|
664
|
+
* editor opens the script and the agent never launches. The `.cmd` is
|
|
665
|
+
* now the only Windows artifact and carries this alias marker so
|
|
666
|
+
* staleness checks read it directly.
|
|
623
667
|
*/
|
|
624
|
-
export const VERSIONED_ALIAS_SCHEMA_VERSION =
|
|
668
|
+
export const VERSIONED_ALIAS_SCHEMA_VERSION = 12;
|
|
625
669
|
/** Internal marker string used to embed the schema version in versioned alias scripts. */
|
|
626
670
|
const VERSIONED_ALIAS_VERSION_MARKER = 'agents-versioned-alias-version:';
|
|
627
671
|
// The version string is interpolated into a generated bash script and into
|
|
@@ -649,6 +693,10 @@ export function generateVersionedAliasScript(agent, version) {
|
|
|
649
693
|
# Claude stores OAuth credentials in the macOS keychain. Scope them to this
|
|
650
694
|
# version's config directory so direct aliases also switch the live account.
|
|
651
695
|
export CLAUDE_CONFIG_DIR="$HOME/.agents/.history/versions/${agent}/${version}/home/${configDirName}"
|
|
696
|
+
# Managed installs are pinned in a per-version dir; Claude Code's background
|
|
697
|
+
# auto-updater would rewrite the pinned binary in place. Disable it so a pin
|
|
698
|
+
# stays a pin. An explicit user value always wins.
|
|
699
|
+
export DISABLE_AUTOUPDATER="\${DISABLE_AUTOUPDATER:-1}"
|
|
652
700
|
`
|
|
653
701
|
: agent === 'codex'
|
|
654
702
|
? `
|
|
@@ -680,12 +728,16 @@ export KIMI_CODE_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/
|
|
|
680
728
|
: '';
|
|
681
729
|
const launchArgs = agent === 'codex' ? ' -c check_for_update_on_startup=false' : '';
|
|
682
730
|
// Resolve the binary the same way the main shim does (see generateShimScript).
|
|
683
|
-
// Grok
|
|
684
|
-
//
|
|
685
|
-
//
|
|
686
|
-
//
|
|
687
|
-
//
|
|
688
|
-
//
|
|
731
|
+
// Grok and Droid do NOT ship into node_modules/.bin — Grok downloads a native
|
|
732
|
+
// binary to ~/.grok/downloads and Droid (Factory AI) installs a standalone
|
|
733
|
+
// binary to ~/.local/bin. Hardcoding the node_modules path made every
|
|
734
|
+
// versioned alias for those two fail with "<agent>@<version> not installed",
|
|
735
|
+
// which is exactly the path `agents teams` takes once it pins a teammate's
|
|
736
|
+
// version. Kimi is NOT one of them: `agents add kimi` npm-installs
|
|
737
|
+
// @moonshot-ai/kimi-code so its binary is at node_modules/.bin/kimi (the
|
|
738
|
+
// generic branch below). The old ~/.kimi-code/bin path never exists for an
|
|
739
|
+
// npm install and fell back to `command -v kimi`, which resolves to this
|
|
740
|
+
// alias's sibling dispatcher shim and re-execs forever.
|
|
689
741
|
// This template is unix-only — on Windows the .cmd companion delegates to
|
|
690
742
|
// "agents __shim" which resolves via getBinaryPath() instead.
|
|
691
743
|
const versionDir = `$HOME/.agents/.history/versions/${agent}/${version}`;
|
|
@@ -697,17 +749,17 @@ if [ -d "$GROK_DOWNLOADS" ]; then
|
|
|
697
749
|
BINARY=$(ls "$GROK_DOWNLOADS"/grok-* 2>/dev/null | grep -i "${version}" | head -1)
|
|
698
750
|
[ -n "$BINARY" ] || BINARY=$(ls "$GROK_DOWNLOADS"/grok-* 2>/dev/null | head -1)
|
|
699
751
|
fi
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
752
|
+
# Refuse a PATH match under our own shims dir — it resolves to this alias's
|
|
753
|
+
# sibling dispatcher shim (shims dir is ahead of ~/.local/bin on PATH) and
|
|
754
|
+
# re-execs forever. Fall through to the clean "not installed" error instead.
|
|
755
|
+
if [ -z "$BINARY" ] || [ ! -x "$BINARY" ]; then
|
|
756
|
+
BINARY=$(command -v grok 2>/dev/null || echo "")
|
|
757
|
+
case "$BINARY" in
|
|
758
|
+
"$HOME/.agents/.cache/shims/"*) BINARY="" ;;
|
|
759
|
+
esac
|
|
708
760
|
fi`
|
|
709
|
-
|
|
710
|
-
|
|
761
|
+
: agent === 'droid'
|
|
762
|
+
? `# Droid (Factory AI) installs a standalone native binary at ~/.local/bin/droid;
|
|
711
763
|
# there is no npm package and nothing lands in node_modules/.bin. The PATH
|
|
712
764
|
# fallback refuses anything under our shims dir to avoid an infinite re-exec.
|
|
713
765
|
DROID_BINARY="$HOME/.local/bin/droid"
|
|
@@ -719,7 +771,7 @@ else
|
|
|
719
771
|
"$HOME/.agents/.cache/shims/"*) BINARY="" ;;
|
|
720
772
|
esac
|
|
721
773
|
fi`
|
|
722
|
-
|
|
774
|
+
: `BINARY="${versionDir}/node_modules/.bin/${agentConfig.cliCommand}"`;
|
|
723
775
|
return `#!/bin/bash
|
|
724
776
|
# Auto-generated by agents-cli - do not edit
|
|
725
777
|
# ${VERSIONED_ALIAS_VERSION_MARKER} ${VERSIONED_ALIAS_SCHEMA_VERSION}
|
|
@@ -741,7 +793,7 @@ exec "$BINARY"${launchArgs} "$@"
|
|
|
741
793
|
* alias doesn't exist or is a pre-v2 alias (no marker — treated as stale).
|
|
742
794
|
*/
|
|
743
795
|
export function readVersionedAliasSchemaVersion(agent, version) {
|
|
744
|
-
const aliasPath =
|
|
796
|
+
const aliasPath = versionedAliasOnDiskPath(agent, version);
|
|
745
797
|
if (!fs.existsSync(aliasPath))
|
|
746
798
|
return null;
|
|
747
799
|
try {
|
|
@@ -768,11 +820,19 @@ export function isVersionedAliasCurrent(agent, version) {
|
|
|
768
820
|
* was upgraded.
|
|
769
821
|
*/
|
|
770
822
|
export function ensureVersionedAliasCurrent(agent, version) {
|
|
771
|
-
|
|
772
|
-
if (!fs.existsSync(aliasPath)) {
|
|
823
|
+
if (!fs.existsSync(versionedAliasOnDiskPath(agent, version))) {
|
|
773
824
|
createVersionedAlias(agent, version);
|
|
774
825
|
return 'created';
|
|
775
826
|
}
|
|
827
|
+
// A lingering extensionless bash alias on Windows shadows the `.cmd` in both
|
|
828
|
+
// cmd.exe and PowerShell (the dotted version reads as a file extension, and
|
|
829
|
+
// an exact filename match beats PATHEXT probing), ShellExecuting the bash
|
|
830
|
+
// script to the `.sh` editor association instead of launching the agent.
|
|
831
|
+
// Regenerate regardless of the `.cmd`'s stamp so the shadow gets deleted.
|
|
832
|
+
if (shimTargetsFor(process.platform).cmd && fs.existsSync(getVersionedAliasPath(agent, version))) {
|
|
833
|
+
createVersionedAlias(agent, version);
|
|
834
|
+
return 'updated';
|
|
835
|
+
}
|
|
776
836
|
// Upgrade-only (newest-wins), same rationale as ensureShimCurrent: never
|
|
777
837
|
// downgrade an alias stamped by a newer install sharing the shims dir.
|
|
778
838
|
const onDisk = readVersionedAliasSchemaVersion(agent, version);
|
|
@@ -783,14 +843,38 @@ export function ensureVersionedAliasCurrent(agent, version) {
|
|
|
783
843
|
return 'current';
|
|
784
844
|
}
|
|
785
845
|
/**
|
|
786
|
-
* Get the filesystem path for a versioned alias script
|
|
846
|
+
* Get the filesystem path for a versioned alias script — the logical
|
|
847
|
+
* (extensionless) launch name. On Windows this is not a real file (see
|
|
848
|
+
* versionedAliasOnDiskFile); stat/read checks must use the on-disk path.
|
|
787
849
|
*/
|
|
788
850
|
export function getVersionedAliasPath(agent, version) {
|
|
789
851
|
return path.join(getShimsDir(), `${AGENTS[agent].cliCommand}@${version}`);
|
|
790
852
|
}
|
|
853
|
+
/**
|
|
854
|
+
* The file createVersionedAlias actually materializes for a platform:
|
|
855
|
+
* `<cmd>@<version>.cmd` on Windows, the bare bash script on POSIX. Pure —
|
|
856
|
+
* testable on any host. Mirrors onDiskShimFile for the main shim.
|
|
857
|
+
*/
|
|
858
|
+
export function versionedAliasOnDiskFile(cliCommand, version, platform) {
|
|
859
|
+
const name = `${cliCommand}@${version}`;
|
|
860
|
+
return shimTargetsFor(platform).cmd ? `${name}.cmd` : name;
|
|
861
|
+
}
|
|
862
|
+
/** The on-disk versioned-alias path for the current platform. */
|
|
863
|
+
function versionedAliasOnDiskPath(agent, version) {
|
|
864
|
+
return path.join(getShimsDir(), versionedAliasOnDiskFile(AGENTS[agent].cliCommand, version, process.platform));
|
|
865
|
+
}
|
|
791
866
|
/**
|
|
792
867
|
* Create a versioned alias for a specific agent version.
|
|
793
868
|
* e.g., claude@2.0.65
|
|
869
|
+
*
|
|
870
|
+
* Same platform split as createShim (shimTargetsFor): POSIX writes the
|
|
871
|
+
* extensionless bash script; Windows writes ONLY the `.cmd`. Unlike the main
|
|
872
|
+
* shim — where the bash file was merely dead weight — a bash alias next to the
|
|
873
|
+
* versioned `.cmd` is actively harmful: the dotted version suffix makes
|
|
874
|
+
* cmd.exe/PowerShell treat `claude@2.1.201` as a complete filename (extension
|
|
875
|
+
* `.201`), so the exact match wins over PATHEXT probing and the shell
|
|
876
|
+
* ShellExecutes the bash script to the `.sh` editor association — the editor
|
|
877
|
+
* opens, the agent never launches. Any legacy bash alias is deleted here.
|
|
794
878
|
*/
|
|
795
879
|
export function createVersionedAlias(agent, version) {
|
|
796
880
|
assertSafeVersion(version);
|
|
@@ -798,40 +882,42 @@ export function createVersionedAlias(agent, version) {
|
|
|
798
882
|
const shimsDir = getShimsDir();
|
|
799
883
|
const agentConfig = AGENTS[agent];
|
|
800
884
|
const aliasPath = path.join(shimsDir, `${agentConfig.cliCommand}@${version}`);
|
|
801
|
-
const
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
885
|
+
const targets = shimTargetsFor(process.platform);
|
|
886
|
+
if (targets.bash) {
|
|
887
|
+
fs.writeFileSync(aliasPath, generateVersionedAliasScript(agent, version), { mode: 0o755 });
|
|
888
|
+
}
|
|
889
|
+
else {
|
|
890
|
+
try {
|
|
891
|
+
fs.unlinkSync(aliasPath);
|
|
892
|
+
}
|
|
893
|
+
catch { }
|
|
894
|
+
}
|
|
895
|
+
if (targets.cmd) {
|
|
896
|
+
writeWindowsCmdShim(aliasPath + '.cmd', `${agentConfig.cliCommand}@${version}`, [`${VERSIONED_ALIAS_VERSION_MARKER} ${VERSIONED_ALIAS_SCHEMA_VERSION}`]);
|
|
805
897
|
}
|
|
806
898
|
return aliasPath;
|
|
807
899
|
}
|
|
808
900
|
/**
|
|
809
|
-
* Remove a versioned alias for a specific agent version.
|
|
901
|
+
* Remove a versioned alias for a specific agent version. Removes whichever
|
|
902
|
+
* companions exist — the extensionless script (POSIX, or a legacy Windows
|
|
903
|
+
* install that wrote it) AND the `.cmd` (Windows) — mirroring removeShim.
|
|
810
904
|
*/
|
|
811
905
|
export function removeVersionedAlias(agent, version) {
|
|
812
|
-
const
|
|
813
|
-
|
|
814
|
-
const aliasPath
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
try {
|
|
819
|
-
fs.unlinkSync(aliasPath + '.cmd');
|
|
820
|
-
}
|
|
821
|
-
catch { }
|
|
906
|
+
const aliasPath = getVersionedAliasPath(agent, version);
|
|
907
|
+
let removed = false;
|
|
908
|
+
for (const p of [aliasPath, aliasPath + '.cmd']) {
|
|
909
|
+
if (fs.existsSync(p)) {
|
|
910
|
+
fs.unlinkSync(p);
|
|
911
|
+
removed = true;
|
|
822
912
|
}
|
|
823
|
-
return true;
|
|
824
913
|
}
|
|
825
|
-
return
|
|
914
|
+
return removed;
|
|
826
915
|
}
|
|
827
916
|
/**
|
|
828
|
-
* Check if a versioned alias exists.
|
|
917
|
+
* Check if a versioned alias exists (the on-disk artifact for this platform).
|
|
829
918
|
*/
|
|
830
919
|
export function versionedAliasExists(agent, version) {
|
|
831
|
-
|
|
832
|
-
const agentConfig = AGENTS[agent];
|
|
833
|
-
const aliasPath = path.join(shimsDir, `${agentConfig.cliCommand}@${version}`);
|
|
834
|
-
return fs.existsSync(aliasPath);
|
|
920
|
+
return fs.existsSync(versionedAliasOnDiskPath(agent, version));
|
|
835
921
|
}
|
|
836
922
|
/**
|
|
837
923
|
* Get the path to the agent's config directory in HOME.
|
package/dist/lib/ssh-exec.d.ts
CHANGED
|
@@ -17,7 +17,16 @@ export declare const SSH_TARGET_RE: RegExp;
|
|
|
17
17
|
export declare function assertValidSshTarget(host: string): void;
|
|
18
18
|
/** POSIX single-quote a string for safe interpolation into a remote shell command. */
|
|
19
19
|
export declare function shellQuote(s: string): string;
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Hardened ssh options applied to every connection — the single baseline every
|
|
22
|
+
* `ssh` in the codebase composes from (directly here, or as `[...SSH_OPTS, …]`
|
|
23
|
+
* in the few callers that need extra flags like `-L`/`-N`/`ProxyCommand`).
|
|
24
|
+
*
|
|
25
|
+
* `ServerAliveInterval`/`ServerAliveCountMax` add in-connection keepalive: a
|
|
26
|
+
* silently-dropped link (laptop sleeps, Wi-Fi flips) is detected and the ssh
|
|
27
|
+
* process exits within ~45s instead of hanging forever — so a followed run or a
|
|
28
|
+
* long-lived `-N` tunnel can't leave a zombie ssh + socket pinned on the laptop.
|
|
29
|
+
*/
|
|
21
30
|
export declare const SSH_OPTS: readonly string[];
|
|
22
31
|
export declare function controlOpts(): string[];
|
|
23
32
|
export interface SshExecOptions {
|
|
@@ -27,7 +36,7 @@ export interface SshExecOptions {
|
|
|
27
36
|
timeoutMs?: number;
|
|
28
37
|
/** Extra ssh flags inserted before the target (e.g. `-tt`). */
|
|
29
38
|
extraSshArgs?: string[];
|
|
30
|
-
/** Reuse a persistent control socket across calls (see `controlOpts`). */
|
|
39
|
+
/** Reuse a persistent control socket across calls (default true; see `controlOpts`). */
|
|
31
40
|
multiplex?: boolean;
|
|
32
41
|
}
|
|
33
42
|
export interface SshExecResult {
|
|
@@ -44,6 +53,20 @@ export interface SshExecResult {
|
|
|
44
53
|
* it); callers that build it from user input must `shellQuote` the pieces.
|
|
45
54
|
*/
|
|
46
55
|
export declare function sshExec(target: string, remoteCmd: string, opts?: SshExecOptions): SshExecResult;
|
|
56
|
+
export interface SshExecRawResult {
|
|
57
|
+
code: number | null;
|
|
58
|
+
stdout: Buffer;
|
|
59
|
+
stderr: Buffer;
|
|
60
|
+
timedOut: boolean;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Like {@link sshExec} but returns raw stdout/stderr Buffers — no UTF-8 decode.
|
|
64
|
+
*
|
|
65
|
+
* Use when byte-exactness matters, e.g. offset-tracked log tailing: a multibyte
|
|
66
|
+
* character split across a read boundary must stay raw bytes, not collapse to a
|
|
67
|
+
* U+FFFD replacement char (which would desync a byte offset from the wire).
|
|
68
|
+
*/
|
|
69
|
+
export declare function sshExecRaw(target: string, remoteCmd: string, opts?: SshExecOptions): SshExecRawResult;
|
|
47
70
|
/** True if `target` is reachable over ssh (a passwordless `true` succeeds quickly). */
|
|
48
71
|
export declare function sshReachable(target: string, timeoutMs?: number): boolean;
|
|
49
72
|
export interface SshStreamOptions {
|
|
@@ -54,7 +77,7 @@ export interface SshStreamOptions {
|
|
|
54
77
|
* leave it off and forward a non-interactive invocation instead.
|
|
55
78
|
*/
|
|
56
79
|
tty?: boolean;
|
|
57
|
-
/** Reuse a persistent control socket across calls (see `controlOpts`). */
|
|
80
|
+
/** Reuse a persistent control socket across calls (default true; see `controlOpts`). */
|
|
58
81
|
multiplex?: boolean;
|
|
59
82
|
}
|
|
60
83
|
/**
|
package/dist/lib/ssh-exec.js
CHANGED
|
@@ -29,11 +29,22 @@ export function shellQuote(s) {
|
|
|
29
29
|
return s;
|
|
30
30
|
return "'" + s.replace(/'/g, "'\\''") + "'";
|
|
31
31
|
}
|
|
32
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Hardened ssh options applied to every connection — the single baseline every
|
|
34
|
+
* `ssh` in the codebase composes from (directly here, or as `[...SSH_OPTS, …]`
|
|
35
|
+
* in the few callers that need extra flags like `-L`/`-N`/`ProxyCommand`).
|
|
36
|
+
*
|
|
37
|
+
* `ServerAliveInterval`/`ServerAliveCountMax` add in-connection keepalive: a
|
|
38
|
+
* silently-dropped link (laptop sleeps, Wi-Fi flips) is detected and the ssh
|
|
39
|
+
* process exits within ~45s instead of hanging forever — so a followed run or a
|
|
40
|
+
* long-lived `-N` tunnel can't leave a zombie ssh + socket pinned on the laptop.
|
|
41
|
+
*/
|
|
33
42
|
export const SSH_OPTS = [
|
|
34
43
|
'-o', 'StrictHostKeyChecking=accept-new',
|
|
35
44
|
'-o', 'BatchMode=yes',
|
|
36
45
|
'-o', 'ConnectTimeout=10',
|
|
46
|
+
'-o', 'ServerAliveInterval=15',
|
|
47
|
+
'-o', 'ServerAliveCountMax=3',
|
|
37
48
|
];
|
|
38
49
|
/**
|
|
39
50
|
* OpenSSH connection-multiplexing options. The first connection to a host opens
|
|
@@ -44,6 +55,12 @@ export const SSH_OPTS = [
|
|
|
44
55
|
* client exits. `%C` (a short fixed-length hash of local-host/remote/port/user)
|
|
45
56
|
* keeps the socket path well under macOS's 104-char `sun_path` limit.
|
|
46
57
|
*
|
|
58
|
+
* This is **on by default** for every `sshExec`/`sshStream` call: the poll loops
|
|
59
|
+
* (`followHostTask`), readiness probes, and per-host fan-outs are exactly the
|
|
60
|
+
* high-frequency callers that benefit most from socket reuse, and they should
|
|
61
|
+
* never have to remember to opt in. A caller passes `multiplex: false` only for
|
|
62
|
+
* a genuine one-shot where a lingering 60s master is pure overhead.
|
|
63
|
+
*
|
|
47
64
|
* The socket directory is created lazily; if ssh can't open the control socket
|
|
48
65
|
* it falls back to a normal connection (multiplexing is an optimisation, never a
|
|
49
66
|
* requirement), so this can never make a reachable host unreachable.
|
|
@@ -79,13 +96,14 @@ export function controlOpts() {
|
|
|
79
96
|
*/
|
|
80
97
|
export function sshExec(target, remoteCmd, opts = {}) {
|
|
81
98
|
assertValidSshTarget(target);
|
|
82
|
-
const mux = opts.multiplex ? controlOpts()
|
|
99
|
+
const mux = opts.multiplex === false ? [] : controlOpts();
|
|
83
100
|
const args = [...SSH_OPTS, ...mux, ...(opts.extraSshArgs ?? []), target, remoteCmd];
|
|
84
101
|
const res = spawnSync('ssh', args, {
|
|
85
102
|
input: opts.input,
|
|
86
103
|
encoding: 'utf-8',
|
|
87
104
|
timeout: opts.timeoutMs,
|
|
88
105
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
106
|
+
windowsHide: true,
|
|
89
107
|
});
|
|
90
108
|
const timedOut = !!(res.error && res.error.code === 'ETIMEDOUT');
|
|
91
109
|
return {
|
|
@@ -95,6 +113,32 @@ export function sshExec(target, remoteCmd, opts = {}) {
|
|
|
95
113
|
timedOut,
|
|
96
114
|
};
|
|
97
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Like {@link sshExec} but returns raw stdout/stderr Buffers — no UTF-8 decode.
|
|
118
|
+
*
|
|
119
|
+
* Use when byte-exactness matters, e.g. offset-tracked log tailing: a multibyte
|
|
120
|
+
* character split across a read boundary must stay raw bytes, not collapse to a
|
|
121
|
+
* U+FFFD replacement char (which would desync a byte offset from the wire).
|
|
122
|
+
*/
|
|
123
|
+
export function sshExecRaw(target, remoteCmd, opts = {}) {
|
|
124
|
+
assertValidSshTarget(target);
|
|
125
|
+
const mux = opts.multiplex === false ? [] : controlOpts();
|
|
126
|
+
const args = [...SSH_OPTS, ...mux, ...(opts.extraSshArgs ?? []), target, remoteCmd];
|
|
127
|
+
const res = spawnSync('ssh', args, {
|
|
128
|
+
input: opts.input,
|
|
129
|
+
// No `encoding` → spawnSync returns Buffers.
|
|
130
|
+
timeout: opts.timeoutMs,
|
|
131
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
132
|
+
windowsHide: true,
|
|
133
|
+
});
|
|
134
|
+
const timedOut = !!(res.error && res.error.code === 'ETIMEDOUT');
|
|
135
|
+
return {
|
|
136
|
+
code: typeof res.status === 'number' ? res.status : null,
|
|
137
|
+
stdout: res.stdout ?? Buffer.alloc(0),
|
|
138
|
+
stderr: res.stderr ?? Buffer.alloc(0),
|
|
139
|
+
timedOut,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
98
142
|
/** True if `target` is reachable over ssh (a passwordless `true` succeeds quickly). */
|
|
99
143
|
export function sshReachable(target, timeoutMs = 10000) {
|
|
100
144
|
return sshExec(target, 'true', { timeoutMs, multiplex: true }).code === 0;
|
|
@@ -108,7 +152,7 @@ export function sshReachable(target, timeoutMs = 10000) {
|
|
|
108
152
|
*/
|
|
109
153
|
export function sshStream(target, remoteCmd, opts = {}) {
|
|
110
154
|
assertValidSshTarget(target);
|
|
111
|
-
const mux = opts.multiplex ? controlOpts()
|
|
155
|
+
const mux = opts.multiplex === false ? [] : controlOpts();
|
|
112
156
|
const tty = opts.tty ? ['-tt'] : [];
|
|
113
157
|
const args = [...SSH_OPTS, ...mux, ...tty, target, remoteCmd];
|
|
114
158
|
const res = spawnSync('ssh', args, { stdio: 'inherit' });
|
package/dist/lib/ssh-tunnel.d.ts
CHANGED
|
@@ -28,7 +28,11 @@ export interface StartTunnelOptions {
|
|
|
28
28
|
*/
|
|
29
29
|
detached?: boolean;
|
|
30
30
|
}
|
|
31
|
-
/** Build the ssh argv (after the `ssh` program name) for an `-L` tunnel. Pure.
|
|
31
|
+
/** Build the ssh argv (after the `ssh` program name) for an `-L` tunnel. Pure.
|
|
32
|
+
*
|
|
33
|
+
* Composes the shared hardened baseline (`SSH_OPTS`) rather than re-listing it,
|
|
34
|
+
* so the tunnel inherits the same options — crucially the keepalive, which lets
|
|
35
|
+
* a dropped `-N` tunnel exit instead of lingering as a zombie on the laptop. */
|
|
32
36
|
export declare function buildTunnelArgs(user: string, host: string, localPort: number, remotePort: number): string[];
|
|
33
37
|
/**
|
|
34
38
|
* Spawn `ssh -L localPort:127.0.0.1:remotePort -N user@host`.
|
|
@@ -74,12 +78,13 @@ export declare function resolveRemoteDevice(name: string): Promise<{
|
|
|
74
78
|
host: string;
|
|
75
79
|
}>;
|
|
76
80
|
/**
|
|
77
|
-
* PowerShell that
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* is streaming — the ~156MB exe never lands in memory whole on the remote.
|
|
81
|
+
* PowerShell that resolves the destination under %LOCALAPPDATA%\agents and
|
|
82
|
+
* stops any running instance first so the file is not locked. The caller copies
|
|
83
|
+
* the exe with scp and then verifies the byte count separately.
|
|
81
84
|
*/
|
|
82
85
|
export declare function buildPushScript(): string;
|
|
86
|
+
/** PowerShell that verifies scp wrote the expected number of bytes. */
|
|
87
|
+
export declare function buildVerifyPushScript(remotePath: string, expectedBytes: number): string;
|
|
83
88
|
/**
|
|
84
89
|
* PowerShell that registers the daemon as a LOGON scheduled task. Interactive
|
|
85
90
|
* logon type + Highest run level so the daemon runs in the real desktop session
|
|
@@ -90,6 +95,20 @@ export declare function buildPushScript(): string;
|
|
|
90
95
|
export declare function buildRegisterTaskScript(port: number, taskName: string): string;
|
|
91
96
|
/** PowerShell that unregisters the task and stops any running daemon process. */
|
|
92
97
|
export declare function buildUnregisterTaskScript(taskName: string): string;
|
|
98
|
+
/** Convert a Windows path returned by PowerShell into the scp/SFTP path form. */
|
|
99
|
+
export declare function scpRemotePath(remotePath: string): string;
|
|
100
|
+
/**
|
|
101
|
+
* Build the scp argv used for the helper exe transfer. Exported so tests can
|
|
102
|
+
* assert the real binary copy path keeps BatchMode and does not route bytes
|
|
103
|
+
* through a PowerShell decoder.
|
|
104
|
+
*/
|
|
105
|
+
export declare function buildScpArgs(target: string, remotePath: string, filePath: string): string[];
|
|
106
|
+
/**
|
|
107
|
+
* `setup --host`: push the exe, then register + start the LOGON task. Remote
|
|
108
|
+
* PowerShell hops go through `sshExec` (BatchMode key auth — the same hardening
|
|
109
|
+
* the browser driver and `agents ssh` use), and the large exe rides a binary
|
|
110
|
+
* scp transfer. Throws with the remote stderr on any failure.
|
|
111
|
+
*/
|
|
93
112
|
export declare function setupRemoteHelper(name: string): Promise<{
|
|
94
113
|
target: string;
|
|
95
114
|
taskName: string;
|