@phnx-labs/agents-cli 1.20.33 → 1.20.35
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 +29 -0
- package/README.md +32 -2
- package/dist/commands/computer-actions.d.ts +15 -0
- package/dist/commands/computer-actions.js +120 -22
- package/dist/commands/computer.d.ts +23 -0
- package/dist/commands/computer.js +45 -3
- package/dist/commands/cost.js +17 -12
- package/dist/commands/doctor.d.ts +11 -0
- package/dist/commands/doctor.js +108 -14
- package/dist/commands/events.d.ts +16 -0
- package/dist/commands/events.js +173 -0
- package/dist/commands/exec.js +6 -1
- package/dist/commands/hosts.js +28 -24
- package/dist/commands/import.js +1 -1
- package/dist/commands/inspect.d.ts +2 -0
- package/dist/commands/inspect.js +54 -7
- package/dist/commands/logs.d.ts +17 -0
- package/dist/commands/logs.js +139 -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/rules.js +1 -1
- package/dist/commands/secrets-migrate.js +23 -11
- package/dist/commands/secrets.d.ts +20 -0
- package/dist/commands/secrets.js +93 -17
- package/dist/commands/sessions-resume.d.ts +2 -0
- package/dist/commands/sessions-resume.js +209 -0
- package/dist/commands/sessions-tail.d.ts +10 -0
- package/dist/commands/sessions-tail.js +11 -0
- package/dist/commands/sessions.d.ts +73 -1
- package/dist/commands/sessions.js +273 -53
- package/dist/commands/status.d.ts +12 -0
- package/dist/commands/status.js +81 -0
- package/dist/commands/teams.js +70 -6
- package/dist/commands/versions.js +2 -1
- package/dist/commands/view.d.ts +42 -0
- package/dist/commands/view.js +213 -83
- package/dist/commands/wallet.d.ts +6 -0
- package/dist/commands/wallet.js +22 -5
- package/dist/index.js +61 -32
- package/dist/lib/acp/harnesses.d.ts +1 -1
- package/dist/lib/acp/harnesses.js +2 -2
- package/dist/lib/agents.d.ts +12 -0
- package/dist/lib/agents.js +145 -41
- package/dist/lib/browser/cdp.js +5 -1
- package/dist/lib/browser/chrome.js +20 -0
- package/dist/lib/browser/drivers/ssh.d.ts +27 -0
- package/dist/lib/browser/drivers/ssh.js +94 -19
- package/dist/lib/browser/ipc.d.ts +3 -0
- package/dist/lib/browser/ipc.js +13 -9
- package/dist/lib/browser/service.js +71 -15
- package/dist/lib/daemon.d.ts +35 -0
- package/dist/lib/daemon.js +33 -5
- package/dist/lib/devices/connect.d.ts +3 -2
- package/dist/lib/devices/connect.js +5 -3
- package/dist/lib/doctor-diff.js +29 -2
- package/dist/lib/drift-sync.d.ts +43 -0
- package/dist/lib/drift-sync.js +179 -0
- package/dist/lib/events.d.ts +9 -2
- package/dist/lib/events.js +70 -11
- package/dist/lib/exec.d.ts +15 -0
- package/dist/lib/exec.js +43 -11
- package/dist/lib/hooks.js +8 -2
- package/dist/lib/hosts/dispatch.js +6 -7
- package/dist/lib/hosts/logs.d.ts +16 -0
- package/dist/lib/hosts/logs.js +45 -0
- package/dist/lib/hosts/progress.d.ts +66 -0
- package/dist/lib/hosts/progress.js +125 -17
- package/dist/lib/hosts/ready.d.ts +23 -2
- package/dist/lib/hosts/ready.js +35 -13
- package/dist/lib/hosts/reconcile.d.ts +53 -0
- package/dist/lib/hosts/reconcile.js +81 -0
- package/dist/lib/hosts/tasks.d.ts +8 -0
- package/dist/lib/hosts/tasks.js +14 -0
- package/dist/lib/permissions.d.ts +4 -0
- package/dist/lib/permissions.js +35 -0
- package/dist/lib/picker.d.ts +24 -0
- package/dist/lib/picker.js +124 -0
- package/dist/lib/platform/winpath.d.ts +31 -2
- package/dist/lib/platform/winpath.js +133 -24
- 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 +45 -4
- package/dist/lib/pwsh.d.ts +11 -0
- package/dist/lib/pwsh.js +13 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.d.ts +42 -1
- package/dist/lib/secrets/agent.js +89 -11
- package/dist/lib/secrets/bundles.js +40 -9
- package/dist/lib/secrets/filestore.js +31 -1
- package/dist/lib/secrets/index.d.ts +33 -1
- package/dist/lib/secrets/index.js +90 -9
- package/dist/lib/secrets/windows.d.ts +74 -0
- package/dist/lib/secrets/windows.js +440 -0
- package/dist/lib/session/active.d.ts +7 -0
- package/dist/lib/session/active.js +11 -3
- package/dist/lib/session/discover.js +94 -0
- package/dist/lib/session/parse.d.ts +6 -0
- package/dist/lib/session/parse.js +283 -2
- package/dist/lib/session/pid-registry.d.ts +21 -0
- package/dist/lib/session/pid-registry.js +80 -0
- package/dist/lib/session/remote-active.d.ts +26 -0
- package/dist/lib/session/remote-active.js +141 -0
- package/dist/lib/session/remote.js +2 -6
- package/dist/lib/session/render.js +1 -1
- package/dist/lib/session/sync/config.d.ts +8 -0
- package/dist/lib/session/sync/config.js +11 -2
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/shims.d.ts +30 -2
- package/dist/lib/shims.js +107 -50
- package/dist/lib/ssh-exec.d.ts +26 -3
- package/dist/lib/ssh-exec.js +45 -3
- package/dist/lib/ssh-tunnel.d.ts +24 -5
- package/dist/lib/ssh-tunnel.js +60 -62
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +3 -0
- package/dist/lib/startup/command-registry.js +6 -0
- package/dist/lib/sync-status.d.ts +102 -0
- package/dist/lib/sync-status.js +135 -0
- package/dist/lib/teams/agents.d.ts +24 -0
- package/dist/lib/teams/agents.js +30 -1
- package/dist/lib/teams/registry.js +25 -9
- 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 +18 -0
- package/dist/lib/terminal/backends/index.js +29 -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/engine.d.ts +39 -0
- package/dist/lib/terminal/engine.js +54 -0
- package/dist/lib/terminal/index.d.ts +14 -0
- package/dist/lib/terminal/index.js +5 -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/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/types.d.ts +27 -1
- package/dist/lib/usage.d.ts +30 -0
- package/dist/lib/usage.js +159 -2
- package/dist/lib/versions.d.ts +7 -3
- package/dist/lib/versions.js +10 -5
- package/dist/lib/whats-new.d.ts +9 -0
- package/dist/lib/whats-new.js +35 -0
- package/package.json +1 -1
|
@@ -93,6 +93,16 @@ export function isSyncConfigured(now = Date.now()) {
|
|
|
93
93
|
return false;
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Normalize a raw hostname into a stable device id: first label only,
|
|
98
|
+
* lowercased, non-alphanumerics collapsed to hyphens. `zion.tail…ts.net` and
|
|
99
|
+
* `ZION` both become `zion`. The single source for this transform — machineId()
|
|
100
|
+
* and cross-machine session grouping must agree or the local machine won't
|
|
101
|
+
* match its own registry key.
|
|
102
|
+
*/
|
|
103
|
+
export function normalizeHost(raw) {
|
|
104
|
+
return raw.split('.')[0].trim().toLowerCase().replace(/[^a-z0-9_-]/g, '-') || 'unknown';
|
|
105
|
+
}
|
|
96
106
|
/**
|
|
97
107
|
* This machine's stable, human-readable id, used as its R2 prefix and mirror
|
|
98
108
|
* directory name. Tailnet hostnames (zion, yosemite-s0, mac-mini) are already
|
|
@@ -100,6 +110,5 @@ export function isSyncConfigured(now = Date.now()) {
|
|
|
100
110
|
* via AGENTS_SYNC_MACHINE_ID for tests and unusual setups.
|
|
101
111
|
*/
|
|
102
112
|
export function machineId() {
|
|
103
|
-
|
|
104
|
-
return raw.split('.')[0].trim().toLowerCase().replace(/[^a-z0-9_-]/g, '-') || 'unknown';
|
|
113
|
+
return normalizeHost(process.env.AGENTS_SYNC_MACHINE_ID || os.hostname());
|
|
105
114
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* speaks these types.
|
|
8
8
|
*/
|
|
9
9
|
/** Agents that store session data on disk and can be discovered by `agents sessions`. */
|
|
10
|
-
export type SessionAgentId = 'claude' | 'codex' | 'gemini' | 'opencode' | 'openclaw' | 'rush' | 'hermes' | 'grok' | 'kimi' | 'droid';
|
|
10
|
+
export type SessionAgentId = 'claude' | 'codex' | 'gemini' | 'antigravity' | 'opencode' | 'openclaw' | 'rush' | 'hermes' | 'grok' | 'kimi' | 'droid';
|
|
11
11
|
/** All agents with session discovery support, in display order. */
|
|
12
12
|
export declare const SESSION_AGENTS: SessionAgentId[];
|
|
13
13
|
/** A single normalized event within a session (message, tool call, thinking, etc.). */
|
|
@@ -7,4 +7,4 @@
|
|
|
7
7
|
* speaks these types.
|
|
8
8
|
*/
|
|
9
9
|
/** All agents with session discovery support, in display order. */
|
|
10
|
-
export const SESSION_AGENTS = ['claude', 'codex', 'gemini', 'opencode', 'openclaw', 'rush', 'hermes', 'grok', 'kimi', 'droid'];
|
|
10
|
+
export const SESSION_AGENTS = ['claude', 'codex', 'gemini', 'antigravity', 'opencode', 'openclaw', 'rush', 'hermes', 'grok', 'kimi', 'droid'];
|
package/dist/lib/shims.d.ts
CHANGED
|
@@ -77,12 +77,24 @@ 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 = 21;
|
|
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.
|
|
84
84
|
*/
|
|
85
85
|
export declare function generateShimScript(agent: AgentId): string;
|
|
86
|
+
/**
|
|
87
|
+
* Which shim files to materialize for a platform. Pure — testable on any host.
|
|
88
|
+
*
|
|
89
|
+
* POSIX writes the extensionless `#!/bin/bash` shim — the file PATH resolution
|
|
90
|
+
* execs. Windows writes only the `.cmd` companion: PATHEXT makes it the runnable
|
|
91
|
+
* form, and the bash file (mode 0o755 is a no-op there) is never executed — so
|
|
92
|
+
* emitting it is dead weight that only ever confuses `where agents`.
|
|
93
|
+
*/
|
|
94
|
+
export declare function shimTargetsFor(platform: NodeJS.Platform): {
|
|
95
|
+
bash: boolean;
|
|
96
|
+
cmd: boolean;
|
|
97
|
+
};
|
|
86
98
|
/**
|
|
87
99
|
* Create a shim for an agent.
|
|
88
100
|
*/
|
|
@@ -114,8 +126,16 @@ export declare function removeShim(agent: AgentId): boolean;
|
|
|
114
126
|
* hardcoded node_modules/.bin, which never exists for these three and
|
|
115
127
|
* made every versioned alias (the path `agents teams` pins to) fail
|
|
116
128
|
* with "<agent>@<version> not installed". Also emit GROK_HOME.
|
|
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.
|
|
117
137
|
*/
|
|
118
|
-
export declare const VERSIONED_ALIAS_SCHEMA_VERSION =
|
|
138
|
+
export declare const VERSIONED_ALIAS_SCHEMA_VERSION = 10;
|
|
119
139
|
/**
|
|
120
140
|
* Generate a versioned alias script that directly execs a specific version.
|
|
121
141
|
* e.g., claude@2.0.65 -> directly runs that version's binary
|
|
@@ -219,6 +239,14 @@ export declare function getConfigSymlinkVersion(agent: AgentId): string | null;
|
|
|
219
239
|
/**
|
|
220
240
|
* Check if shim exists for an agent.
|
|
221
241
|
*/
|
|
242
|
+
/**
|
|
243
|
+
* The on-disk shim FILENAME for a platform — derived from `shimTargetsFor` (the
|
|
244
|
+
* write-side source of truth) so the exists/remove/version checks can never
|
|
245
|
+
* drift from what `createShim` actually writes: `<cmd>.cmd` on Windows (the only
|
|
246
|
+
* file written there), the bare `<cmd>` script on POSIX. Pure — testable on any
|
|
247
|
+
* host.
|
|
248
|
+
*/
|
|
249
|
+
export declare function onDiskShimFile(cliCommand: string, platform: NodeJS.Platform): string;
|
|
222
250
|
export declare function shimExists(agent: AgentId): boolean;
|
|
223
251
|
/**
|
|
224
252
|
* Regenerate the shim if it's missing or outdated. Returns a status describing
|
package/dist/lib/shims.js
CHANGED
|
@@ -202,7 +202,13 @@ 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
|
+
export const SHIM_SCHEMA_VERSION = 21;
|
|
206
212
|
/** Internal marker string used to embed the schema version in shim scripts. */
|
|
207
213
|
const SHIM_VERSION_MARKER = 'agents-shim-version:';
|
|
208
214
|
function shellQuote(value) {
|
|
@@ -401,19 +407,25 @@ if [ "$AGENT" = "grok" ]; then
|
|
|
401
407
|
fi
|
|
402
408
|
fi
|
|
403
409
|
if [ -z "$BINARY" ] || [ ! -x "$BINARY" ]; then
|
|
404
|
-
# Last resort: whatever is on PATH (user may have installed grok globally)
|
|
410
|
+
# Last resort: whatever is on PATH (user may have installed grok globally).
|
|
411
|
+
# Refuse anything under our own shims dir: the shims dir sits ahead of
|
|
412
|
+
# ~/.local/bin on PATH, so "command -v grok" resolves to THIS dispatcher.
|
|
413
|
+
# exec-ing it would re-enter and spin in an infinite re-exec loop (the same
|
|
414
|
+
# bug the droid branch below guards against). Fall through to the clean
|
|
415
|
+
# "not installed" error instead.
|
|
405
416
|
BINARY=$(command -v grok 2>/dev/null || echo "")
|
|
417
|
+
case "$BINARY" in
|
|
418
|
+
"$AGENTS_USER_DIR/.cache/shims/"*) BINARY="" ;;
|
|
419
|
+
esac
|
|
406
420
|
fi
|
|
407
|
-
# Kimi
|
|
408
|
-
#
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
BINARY=$(command -v kimi 2>/dev/null || echo "")
|
|
416
|
-
fi
|
|
421
|
+
# Kimi is a normal npm agent: "agents add kimi" npm-installs
|
|
422
|
+
# @moonshot-ai/kimi-code into the version dir and the binary lands at
|
|
423
|
+
# node_modules/.bin/kimi (a curl-installed kimi is symlinked to the same spot
|
|
424
|
+
# by installVersion). So kimi resolves via the generic node_modules branch
|
|
425
|
+
# below -- never a bespoke ~/.kimi-code/bin path that does not exist for npm
|
|
426
|
+
# installs and fell back to "command -v kimi", which resolves to THIS
|
|
427
|
+
# dispatcher (shims dir is ahead on PATH) and re-execs forever. Only
|
|
428
|
+
# KIMI_CODE_HOME (config isolation) stays special-cased, separately below.
|
|
417
429
|
# Droid (Factory AI) special case: the official installer drops a standalone
|
|
418
430
|
# native binary at ~/.local/bin/droid — there is no npm package and nothing
|
|
419
431
|
# lands in node_modules/.bin. Resolve the fixed install path directly. The
|
|
@@ -528,6 +540,19 @@ fi
|
|
|
528
540
|
exec "$BINARY"${launchArgs} "$@"
|
|
529
541
|
`;
|
|
530
542
|
}
|
|
543
|
+
/**
|
|
544
|
+
* Which shim files to materialize for a platform. Pure — testable on any host.
|
|
545
|
+
*
|
|
546
|
+
* POSIX writes the extensionless `#!/bin/bash` shim — the file PATH resolution
|
|
547
|
+
* execs. Windows writes only the `.cmd` companion: PATHEXT makes it the runnable
|
|
548
|
+
* form, and the bash file (mode 0o755 is a no-op there) is never executed — so
|
|
549
|
+
* emitting it is dead weight that only ever confuses `where agents`.
|
|
550
|
+
*/
|
|
551
|
+
export function shimTargetsFor(platform) {
|
|
552
|
+
if (platform === 'win32')
|
|
553
|
+
return { bash: false, cmd: true };
|
|
554
|
+
return { bash: true, cmd: false };
|
|
555
|
+
}
|
|
531
556
|
/**
|
|
532
557
|
* Create a shim for an agent.
|
|
533
558
|
*/
|
|
@@ -536,12 +561,15 @@ export function createShim(agent) {
|
|
|
536
561
|
const shimsDir = getShimsDir();
|
|
537
562
|
const agentConfig = AGENTS[agent];
|
|
538
563
|
const shimPath = path.join(shimsDir, agentConfig.cliCommand);
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
//
|
|
544
|
-
|
|
564
|
+
const targets = shimTargetsFor(process.platform);
|
|
565
|
+
if (targets.bash) {
|
|
566
|
+
fs.writeFileSync(shimPath, generateShimScript(agent), { mode: 0o755 });
|
|
567
|
+
}
|
|
568
|
+
// Windows can't execute the bash shim directly. Drop a `.cmd` companion — which
|
|
569
|
+
// delegates to the node-side transparent resolver (`agents __shim`) so version
|
|
570
|
+
// resolution stays single-sourced instead of reimplemented in batch — and skip
|
|
571
|
+
// the vestigial bash file entirely.
|
|
572
|
+
if (targets.cmd) {
|
|
545
573
|
writeWindowsCmdShim(shimPath + '.cmd', agentConfig.cliCommand);
|
|
546
574
|
}
|
|
547
575
|
return shimPath;
|
|
@@ -557,6 +585,7 @@ function writeWindowsCmdShim(cmdPath, spec) {
|
|
|
557
585
|
const indexJs = getAgentsBinForGeneratedShim();
|
|
558
586
|
const content = `@echo off\r\n` +
|
|
559
587
|
`rem Auto-generated by agents-cli - do not edit\r\n` +
|
|
588
|
+
`rem ${SHIM_VERSION_MARKER} ${SHIM_SCHEMA_VERSION}\r\n` +
|
|
560
589
|
`node "${indexJs}" __shim ${spec} %*\r\n`;
|
|
561
590
|
fs.writeFileSync(cmdPath, content);
|
|
562
591
|
}
|
|
@@ -567,17 +596,18 @@ export function removeShim(agent) {
|
|
|
567
596
|
const shimsDir = getShimsDir();
|
|
568
597
|
const agentConfig = AGENTS[agent];
|
|
569
598
|
const shimPath = path.join(shimsDir, agentConfig.cliCommand);
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
599
|
+
// Remove whichever companions exist: the extensionless script (POSIX, or a
|
|
600
|
+
// legacy Windows install that wrote it) AND the `.cmd` (Windows). Keying only
|
|
601
|
+
// off the extensionless path would orphan the `.cmd` on Windows, where
|
|
602
|
+
// createShim now writes only the `.cmd`.
|
|
603
|
+
let removed = false;
|
|
604
|
+
for (const p of [shimPath, shimPath + '.cmd']) {
|
|
605
|
+
if (fs.existsSync(p)) {
|
|
606
|
+
fs.unlinkSync(p);
|
|
607
|
+
removed = true;
|
|
577
608
|
}
|
|
578
|
-
return true;
|
|
579
609
|
}
|
|
580
|
-
return
|
|
610
|
+
return removed;
|
|
581
611
|
}
|
|
582
612
|
/**
|
|
583
613
|
* Current versioned-alias schema. Bump whenever `generateVersionedAliasScript`
|
|
@@ -602,8 +632,16 @@ export function removeShim(agent) {
|
|
|
602
632
|
* hardcoded node_modules/.bin, which never exists for these three and
|
|
603
633
|
* made every versioned alias (the path `agents teams` pins to) fail
|
|
604
634
|
* with "<agent>@<version> not installed". Also emit GROK_HOME.
|
|
635
|
+
* v9 — kimi was wrong in v8: it npm-installs @moonshot-ai/kimi-code into
|
|
636
|
+
* node_modules/.bin/kimi (grok/droid ship native binaries elsewhere,
|
|
637
|
+
* kimi does not). The ~/.kimi-code/bin path never existed for an npm
|
|
638
|
+
* install and the `command -v kimi` fallback resolved to this alias's
|
|
639
|
+
* sibling dispatcher shim, re-exec-looping forever. Resolve kimi via the
|
|
640
|
+
* generic node_modules/.bin branch.
|
|
641
|
+
* v10 — guard grok's `command -v grok` fallback against resolving to our own
|
|
642
|
+
* shims dir (same infinite re-exec loop), mirroring droid.
|
|
605
643
|
*/
|
|
606
|
-
export const VERSIONED_ALIAS_SCHEMA_VERSION =
|
|
644
|
+
export const VERSIONED_ALIAS_SCHEMA_VERSION = 10;
|
|
607
645
|
/** Internal marker string used to embed the schema version in versioned alias scripts. */
|
|
608
646
|
const VERSIONED_ALIAS_VERSION_MARKER = 'agents-versioned-alias-version:';
|
|
609
647
|
// The version string is interpolated into a generated bash script and into
|
|
@@ -662,12 +700,16 @@ export KIMI_CODE_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/
|
|
|
662
700
|
: '';
|
|
663
701
|
const launchArgs = agent === 'codex' ? ' -c check_for_update_on_startup=false' : '';
|
|
664
702
|
// Resolve the binary the same way the main shim does (see generateShimScript).
|
|
665
|
-
// Grok
|
|
666
|
-
//
|
|
667
|
-
//
|
|
668
|
-
//
|
|
669
|
-
//
|
|
670
|
-
//
|
|
703
|
+
// Grok and Droid do NOT ship into node_modules/.bin — Grok downloads a native
|
|
704
|
+
// binary to ~/.grok/downloads and Droid (Factory AI) installs a standalone
|
|
705
|
+
// binary to ~/.local/bin. Hardcoding the node_modules path made every
|
|
706
|
+
// versioned alias for those two fail with "<agent>@<version> not installed",
|
|
707
|
+
// which is exactly the path `agents teams` takes once it pins a teammate's
|
|
708
|
+
// version. Kimi is NOT one of them: `agents add kimi` npm-installs
|
|
709
|
+
// @moonshot-ai/kimi-code so its binary is at node_modules/.bin/kimi (the
|
|
710
|
+
// generic branch below). The old ~/.kimi-code/bin path never exists for an
|
|
711
|
+
// npm install and fell back to `command -v kimi`, which resolves to this
|
|
712
|
+
// alias's sibling dispatcher shim and re-execs forever.
|
|
671
713
|
// This template is unix-only — on Windows the .cmd companion delegates to
|
|
672
714
|
// "agents __shim" which resolves via getBinaryPath() instead.
|
|
673
715
|
const versionDir = `$HOME/.agents/.history/versions/${agent}/${version}`;
|
|
@@ -679,17 +721,17 @@ if [ -d "$GROK_DOWNLOADS" ]; then
|
|
|
679
721
|
BINARY=$(ls "$GROK_DOWNLOADS"/grok-* 2>/dev/null | grep -i "${version}" | head -1)
|
|
680
722
|
[ -n "$BINARY" ] || BINARY=$(ls "$GROK_DOWNLOADS"/grok-* 2>/dev/null | head -1)
|
|
681
723
|
fi
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
724
|
+
# Refuse a PATH match under our own shims dir — it resolves to this alias's
|
|
725
|
+
# sibling dispatcher shim (shims dir is ahead of ~/.local/bin on PATH) and
|
|
726
|
+
# re-execs forever. Fall through to the clean "not installed" error instead.
|
|
727
|
+
if [ -z "$BINARY" ] || [ ! -x "$BINARY" ]; then
|
|
728
|
+
BINARY=$(command -v grok 2>/dev/null || echo "")
|
|
729
|
+
case "$BINARY" in
|
|
730
|
+
"$HOME/.agents/.cache/shims/"*) BINARY="" ;;
|
|
731
|
+
esac
|
|
690
732
|
fi`
|
|
691
|
-
|
|
692
|
-
|
|
733
|
+
: agent === 'droid'
|
|
734
|
+
? `# Droid (Factory AI) installs a standalone native binary at ~/.local/bin/droid;
|
|
693
735
|
# there is no npm package and nothing lands in node_modules/.bin. The PATH
|
|
694
736
|
# fallback refuses anything under our shims dir to avoid an infinite re-exec.
|
|
695
737
|
DROID_BINARY="$HOME/.local/bin/droid"
|
|
@@ -701,7 +743,7 @@ else
|
|
|
701
743
|
"$HOME/.agents/.cache/shims/"*) BINARY="" ;;
|
|
702
744
|
esac
|
|
703
745
|
fi`
|
|
704
|
-
|
|
746
|
+
: `BINARY="${versionDir}/node_modules/.bin/${agentConfig.cliCommand}"`;
|
|
705
747
|
return `#!/bin/bash
|
|
706
748
|
# Auto-generated by agents-cli - do not edit
|
|
707
749
|
# ${VERSIONED_ALIAS_VERSION_MARKER} ${VERSIONED_ALIAS_SCHEMA_VERSION}
|
|
@@ -1376,11 +1418,26 @@ async function copyDirContents(src, dest, strategy = 'keep-dest', context) {
|
|
|
1376
1418
|
/**
|
|
1377
1419
|
* Check if shim exists for an agent.
|
|
1378
1420
|
*/
|
|
1421
|
+
/**
|
|
1422
|
+
* The on-disk shim FILENAME for a platform — derived from `shimTargetsFor` (the
|
|
1423
|
+
* write-side source of truth) so the exists/remove/version checks can never
|
|
1424
|
+
* drift from what `createShim` actually writes: `<cmd>.cmd` on Windows (the only
|
|
1425
|
+
* file written there), the bare `<cmd>` script on POSIX. Pure — testable on any
|
|
1426
|
+
* host.
|
|
1427
|
+
*/
|
|
1428
|
+
export function onDiskShimFile(cliCommand, platform) {
|
|
1429
|
+
return shimTargetsFor(platform).cmd ? `${cliCommand}.cmd` : cliCommand;
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* The actual on-disk shim path for the current platform. This is what
|
|
1433
|
+
* exists/version checks must stat — `getShimPath` returns the logical
|
|
1434
|
+
* (extensionless) launch path, which is not always a real file on Windows.
|
|
1435
|
+
*/
|
|
1436
|
+
function onDiskShimPath(agent) {
|
|
1437
|
+
return path.join(getShimsDir(), onDiskShimFile(AGENTS[agent].cliCommand, process.platform));
|
|
1438
|
+
}
|
|
1379
1439
|
export function shimExists(agent) {
|
|
1380
|
-
|
|
1381
|
-
const agentConfig = AGENTS[agent];
|
|
1382
|
-
const shimPath = path.join(shimsDir, agentConfig.cliCommand);
|
|
1383
|
-
return fs.existsSync(shimPath);
|
|
1440
|
+
return fs.existsSync(onDiskShimPath(agent));
|
|
1384
1441
|
}
|
|
1385
1442
|
/**
|
|
1386
1443
|
* Read the schema version embedded in an existing on-disk shim. Returns
|
|
@@ -1390,7 +1447,7 @@ function readShimSchemaVersion(agent) {
|
|
|
1390
1447
|
if (!shimExists(agent))
|
|
1391
1448
|
return null;
|
|
1392
1449
|
try {
|
|
1393
|
-
const content = fs.readFileSync(
|
|
1450
|
+
const content = fs.readFileSync(onDiskShimPath(agent), 'utf8');
|
|
1394
1451
|
// Look at the first ~10 lines only — the marker lives in the header.
|
|
1395
1452
|
const header = content.split('\n', 10).join('\n');
|
|
1396
1453
|
const match = header.match(new RegExp(SHIM_VERSION_MARKER + '\\s*(\\d+)'));
|
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,7 +96,7 @@ 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,
|
|
@@ -95,6 +112,31 @@ export function sshExec(target, remoteCmd, opts = {}) {
|
|
|
95
112
|
timedOut,
|
|
96
113
|
};
|
|
97
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Like {@link sshExec} but returns raw stdout/stderr Buffers — no UTF-8 decode.
|
|
117
|
+
*
|
|
118
|
+
* Use when byte-exactness matters, e.g. offset-tracked log tailing: a multibyte
|
|
119
|
+
* character split across a read boundary must stay raw bytes, not collapse to a
|
|
120
|
+
* U+FFFD replacement char (which would desync a byte offset from the wire).
|
|
121
|
+
*/
|
|
122
|
+
export function sshExecRaw(target, remoteCmd, opts = {}) {
|
|
123
|
+
assertValidSshTarget(target);
|
|
124
|
+
const mux = opts.multiplex === false ? [] : controlOpts();
|
|
125
|
+
const args = [...SSH_OPTS, ...mux, ...(opts.extraSshArgs ?? []), target, remoteCmd];
|
|
126
|
+
const res = spawnSync('ssh', args, {
|
|
127
|
+
input: opts.input,
|
|
128
|
+
// No `encoding` → spawnSync returns Buffers.
|
|
129
|
+
timeout: opts.timeoutMs,
|
|
130
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
131
|
+
});
|
|
132
|
+
const timedOut = !!(res.error && res.error.code === 'ETIMEDOUT');
|
|
133
|
+
return {
|
|
134
|
+
code: typeof res.status === 'number' ? res.status : null,
|
|
135
|
+
stdout: res.stdout ?? Buffer.alloc(0),
|
|
136
|
+
stderr: res.stderr ?? Buffer.alloc(0),
|
|
137
|
+
timedOut,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
98
140
|
/** True if `target` is reachable over ssh (a passwordless `true` succeeds quickly). */
|
|
99
141
|
export function sshReachable(target, timeoutMs = 10000) {
|
|
100
142
|
return sshExec(target, 'true', { timeoutMs, multiplex: true }).code === 0;
|
|
@@ -108,7 +150,7 @@ export function sshReachable(target, timeoutMs = 10000) {
|
|
|
108
150
|
*/
|
|
109
151
|
export function sshStream(target, remoteCmd, opts = {}) {
|
|
110
152
|
assertValidSshTarget(target);
|
|
111
|
-
const mux = opts.multiplex ? controlOpts()
|
|
153
|
+
const mux = opts.multiplex === false ? [] : controlOpts();
|
|
112
154
|
const tty = opts.tty ? ['-tt'] : [];
|
|
113
155
|
const args = [...SSH_OPTS, ...mux, ...tty, target, remoteCmd];
|
|
114
156
|
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;
|