@phnx-labs/agents-cli 1.20.34 → 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 +22 -0
- package/README.md +5 -1
- package/dist/commands/computer-actions.d.ts +15 -0
- package/dist/commands/computer-actions.js +120 -22
- package/dist/commands/cost.js +17 -12
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +59 -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/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/secrets.js +40 -16
- 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/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/index.js +58 -31
- package/dist/lib/agents.js +30 -9
- package/dist/lib/browser/cdp.js +5 -1
- package/dist/lib/browser/drivers/ssh.d.ts +8 -0
- package/dist/lib/browser/drivers/ssh.js +76 -16
- 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/events.d.ts +9 -2
- package/dist/lib/events.js +70 -11
- package/dist/lib/exec.js +22 -0
- 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/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/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 +11 -3
- package/dist/lib/shims.js +54 -30
- 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 +2 -0
- package/dist/lib/startup/command-registry.js +4 -0
- 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 +7 -0
- 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
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-machine fan-out for `agents sessions --active`.
|
|
3
|
+
*
|
|
4
|
+
* A single `getActiveSessions()` only sees the local machine. To show the whole
|
|
5
|
+
* fleet in one view, we run `agents sessions --active --json --local` on each
|
|
6
|
+
* peer over SSH and merge the parsed results, tagging every row with the machine
|
|
7
|
+
* it came from so the renderer can bucket by computer.
|
|
8
|
+
*
|
|
9
|
+
* Peers are the registered, online devices from `ag devices` (or an explicit
|
|
10
|
+
* `--host` list). `--local` on the remote invocation is critical: it stops the
|
|
11
|
+
* peer from fanning out to *its* devices, so the sweep never recurses.
|
|
12
|
+
*
|
|
13
|
+
* A dead or slow host is skipped with a stderr note, never fatal — one asleep
|
|
14
|
+
* laptop must not blank the whole view. SSH runs are async + parallel (a fresh
|
|
15
|
+
* `spawn`, not the sync `sshExec`) so N peers cost one round-trip, not N.
|
|
16
|
+
*/
|
|
17
|
+
import { spawn } from 'child_process';
|
|
18
|
+
import chalk from 'chalk';
|
|
19
|
+
import { SSH_OPTS, controlOpts, assertValidSshTarget, shellQuote } from '../ssh-exec.js';
|
|
20
|
+
import { sshTargetFor } from '../devices/connect.js';
|
|
21
|
+
import { loadDevices } from '../devices/registry.js';
|
|
22
|
+
import { machineId, normalizeHost } from './sync/config.js';
|
|
23
|
+
/** Per-host SSH budget. Slightly above SSH_OPTS' ConnectTimeout=10 so a
|
|
24
|
+
* reachable-but-slow remote still answers before we give up. */
|
|
25
|
+
const REMOTE_TIMEOUT_MS = 12_000;
|
|
26
|
+
/**
|
|
27
|
+
* Recursion guard, passed as an env var (not a CLI flag) so an OLDER remote
|
|
28
|
+
* `agents` that predates this feature ignores it harmlessly instead of erroring
|
|
29
|
+
* on an unknown option. A remote new enough to fan out reads it and stays local.
|
|
30
|
+
*/
|
|
31
|
+
export const NO_FANOUT_ENV = 'AGENTS_SESSIONS_LOCAL';
|
|
32
|
+
/** The command run on each peer: answer for itself, as JSON, without recursing. */
|
|
33
|
+
function remoteActiveCommand() {
|
|
34
|
+
const inner = `${NO_FANOUT_ENV}=1 agents sessions --active --json`;
|
|
35
|
+
return `bash -lc ${shellQuote(inner)}`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Parse a peer's `--active --json` stdout into active sessions, tagging each
|
|
39
|
+
* with `machine`. Defensive against version skew / partial output: non-JSON or
|
|
40
|
+
* a non-array yields `[]`, and non-object entries are dropped rather than
|
|
41
|
+
* throwing. Exported for unit testing without a live tailnet.
|
|
42
|
+
*/
|
|
43
|
+
export function parseRemoteActive(stdout, machine) {
|
|
44
|
+
let parsed;
|
|
45
|
+
try {
|
|
46
|
+
parsed = JSON.parse(stdout);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
if (!Array.isArray(parsed))
|
|
52
|
+
return [];
|
|
53
|
+
const out = [];
|
|
54
|
+
for (const x of parsed) {
|
|
55
|
+
if (x && typeof x === 'object' && !Array.isArray(x)) {
|
|
56
|
+
out.push({ ...x, machine });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
/** Run one remote `agents sessions --active --json --local` and capture stdout.
|
|
62
|
+
* Resolves `{ code: null }` on spawn error or timeout (host treated as dead). */
|
|
63
|
+
function sshCapture(target, remoteCmd, timeoutMs) {
|
|
64
|
+
assertValidSshTarget(target);
|
|
65
|
+
return new Promise((resolve) => {
|
|
66
|
+
const args = [...SSH_OPTS, ...controlOpts(), target, remoteCmd];
|
|
67
|
+
const child = spawn('ssh', args, { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
68
|
+
let stdout = '';
|
|
69
|
+
let settled = false;
|
|
70
|
+
const done = (code) => {
|
|
71
|
+
if (settled)
|
|
72
|
+
return;
|
|
73
|
+
settled = true;
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
resolve({ code, stdout });
|
|
76
|
+
};
|
|
77
|
+
const timer = setTimeout(() => { child.kill('SIGKILL'); done(null); }, timeoutMs);
|
|
78
|
+
child.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
79
|
+
child.on('error', () => done(null));
|
|
80
|
+
child.on('close', (code) => done(code));
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
async function fetchByTarget(target, machine, display) {
|
|
84
|
+
const { code, stdout } = await sshCapture(target, remoteActiveCommand(), REMOTE_TIMEOUT_MS);
|
|
85
|
+
if (code !== 0) {
|
|
86
|
+
process.stderr.write(chalk.gray(` ${display}: unreachable or no agents CLI — skipped\n`));
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
return parseRemoteActive(stdout, machine);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Gather active sessions from other machines. With an explicit `hosts` list
|
|
93
|
+
* (from `--host`), fan out to exactly those. Otherwise sweep the registered,
|
|
94
|
+
* online devices from `ag devices`, excluding this machine and any without an
|
|
95
|
+
* address. Results from all peers run in parallel and are flattened.
|
|
96
|
+
*/
|
|
97
|
+
export async function gatherRemoteActive(hosts) {
|
|
98
|
+
const self = machineId();
|
|
99
|
+
const targets = [];
|
|
100
|
+
if (hosts && hosts.length > 0) {
|
|
101
|
+
for (const h of hosts) {
|
|
102
|
+
try {
|
|
103
|
+
assertValidSshTarget(h);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
process.stderr.write(chalk.gray(` ${h}: not a valid ssh target — skipped\n`));
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const bareHost = h.split('@').pop() || h;
|
|
110
|
+
targets.push({ target: h, machine: normalizeHost(bareHost), name: h });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
let reg;
|
|
115
|
+
try {
|
|
116
|
+
reg = await loadDevices();
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return { sessions: [], deviceCount: 0 };
|
|
120
|
+
}
|
|
121
|
+
for (const d of Object.values(reg)) {
|
|
122
|
+
if (d.tailscale?.online !== true)
|
|
123
|
+
continue;
|
|
124
|
+
if (normalizeHost(d.name) === self)
|
|
125
|
+
continue;
|
|
126
|
+
// Only machines that can actually run the CLI. iOS/tablet nodes register as
|
|
127
|
+
// `unknown` platform and can never answer, so skip them rather than burn a
|
|
128
|
+
// full ConnectTimeout on each.
|
|
129
|
+
if (d.platform !== 'windows' && d.platform !== 'linux' && d.platform !== 'macos')
|
|
130
|
+
continue;
|
|
131
|
+
try {
|
|
132
|
+
targets.push({ target: sshTargetFor(d), machine: normalizeHost(d.name), name: d.name });
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// No address on the profile — nothing to dial; skip silently.
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const results = await Promise.all(targets.map((t) => fetchByTarget(t.target, t.machine, t.name)));
|
|
140
|
+
return { sessions: results.flat(), deviceCount: targets.length };
|
|
141
|
+
}
|
|
@@ -27,6 +27,7 @@ import { join } from 'path';
|
|
|
27
27
|
import { createHash } from 'crypto';
|
|
28
28
|
import chalk from 'chalk';
|
|
29
29
|
import { getCacheDir } from '../state.js';
|
|
30
|
+
import { SSH_OPTS, controlOpts } from '../ssh-exec.js';
|
|
30
31
|
import { formatRelativeTime } from './relative-time.js';
|
|
31
32
|
import { terminalWidth } from './width.js';
|
|
32
33
|
/**
|
|
@@ -96,11 +97,6 @@ export function buildRemoteCommand(forwardedArgs, columns) {
|
|
|
96
97
|
const withCols = columns && columns > 0 ? `COLUMNS=${columns} ${inner}` : inner;
|
|
97
98
|
return `bash -lc ${shellQuote(withCols)}`;
|
|
98
99
|
}
|
|
99
|
-
const SSH_OPTS = [
|
|
100
|
-
'-o', 'BatchMode=yes',
|
|
101
|
-
'-o', 'StrictHostKeyChecking=accept-new',
|
|
102
|
-
'-o', 'ConnectTimeout=10',
|
|
103
|
-
];
|
|
104
100
|
/**
|
|
105
101
|
* Classify an ssh `spawnSync` result. ssh(1) reserves exit 255 for its own
|
|
106
102
|
* connection-layer failures (host down, timeout, refused, auth, changed host
|
|
@@ -186,7 +182,7 @@ export function runRemoteSessions(hosts, argv = process.argv) {
|
|
|
186
182
|
for (const host of hosts) {
|
|
187
183
|
if (multi)
|
|
188
184
|
process.stdout.write(chalk.cyan(`\n── ${host} ──\n`));
|
|
189
|
-
const res = spawnSync('ssh', [...SSH_OPTS, host, remoteCmd], {
|
|
185
|
+
const res = spawnSync('ssh', [...SSH_OPTS, ...controlOpts(), host, remoteCmd], {
|
|
190
186
|
encoding: 'utf8',
|
|
191
187
|
maxBuffer: 64 * 1024 * 1024,
|
|
192
188
|
});
|
|
@@ -541,7 +541,7 @@ export function renderSummary(events, cwd) {
|
|
|
541
541
|
if (p)
|
|
542
542
|
filesReadAbs.add(p);
|
|
543
543
|
}
|
|
544
|
-
else if (['Write', 'Edit', 'write_file', 'edit_file', 'create_file', 'replace', 'patch'].includes(tool)) {
|
|
544
|
+
else if (['Write', 'Edit', 'Create', 'write_file', 'edit_file', 'create_file', 'replace', 'patch'].includes(tool)) {
|
|
545
545
|
if (p) {
|
|
546
546
|
if (p.includes('.claude/plans/') && p.endsWith('.md')) {
|
|
547
547
|
planFilePath = p;
|
|
@@ -33,6 +33,14 @@ export declare function loadR2Config(): R2Config;
|
|
|
33
33
|
* does not re-storm every cycle. `now` is injectable for tests.
|
|
34
34
|
*/
|
|
35
35
|
export declare function isSyncConfigured(now?: number): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Normalize a raw hostname into a stable device id: first label only,
|
|
38
|
+
* lowercased, non-alphanumerics collapsed to hyphens. `zion.tail…ts.net` and
|
|
39
|
+
* `ZION` both become `zion`. The single source for this transform — machineId()
|
|
40
|
+
* and cross-machine session grouping must agree or the local machine won't
|
|
41
|
+
* match its own registry key.
|
|
42
|
+
*/
|
|
43
|
+
export declare function normalizeHost(raw: string): string;
|
|
36
44
|
/**
|
|
37
45
|
* This machine's stable, human-readable id, used as its R2 prefix and mirror
|
|
38
46
|
* directory name. Tailnet hostnames (zion, yosemite-s0, mac-mini) are already
|
|
@@ -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,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 = 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.
|
|
@@ -126,8 +126,16 @@ 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
|
+
*/
|
|
138
|
+
export declare const VERSIONED_ALIAS_SCHEMA_VERSION = 10;
|
|
131
139
|
/**
|
|
132
140
|
* Generate a versioned alias script that directly execs a specific version.
|
|
133
141
|
* e.g., claude@2.0.65 -> directly runs that version's binary
|
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
|
|
@@ -620,8 +632,16 @@ export function removeShim(agent) {
|
|
|
620
632
|
* hardcoded node_modules/.bin, which never exists for these three and
|
|
621
633
|
* made every versioned alias (the path `agents teams` pins to) fail
|
|
622
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.
|
|
623
643
|
*/
|
|
624
|
-
export const VERSIONED_ALIAS_SCHEMA_VERSION =
|
|
644
|
+
export const VERSIONED_ALIAS_SCHEMA_VERSION = 10;
|
|
625
645
|
/** Internal marker string used to embed the schema version in versioned alias scripts. */
|
|
626
646
|
const VERSIONED_ALIAS_VERSION_MARKER = 'agents-versioned-alias-version:';
|
|
627
647
|
// The version string is interpolated into a generated bash script and into
|
|
@@ -680,12 +700,16 @@ export KIMI_CODE_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/
|
|
|
680
700
|
: '';
|
|
681
701
|
const launchArgs = agent === 'codex' ? ' -c check_for_update_on_startup=false' : '';
|
|
682
702
|
// Resolve the binary the same way the main shim does (see generateShimScript).
|
|
683
|
-
// Grok
|
|
684
|
-
//
|
|
685
|
-
//
|
|
686
|
-
//
|
|
687
|
-
//
|
|
688
|
-
//
|
|
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.
|
|
689
713
|
// This template is unix-only — on Windows the .cmd companion delegates to
|
|
690
714
|
// "agents __shim" which resolves via getBinaryPath() instead.
|
|
691
715
|
const versionDir = `$HOME/.agents/.history/versions/${agent}/${version}`;
|
|
@@ -697,17 +721,17 @@ if [ -d "$GROK_DOWNLOADS" ]; then
|
|
|
697
721
|
BINARY=$(ls "$GROK_DOWNLOADS"/grok-* 2>/dev/null | grep -i "${version}" | head -1)
|
|
698
722
|
[ -n "$BINARY" ] || BINARY=$(ls "$GROK_DOWNLOADS"/grok-* 2>/dev/null | head -1)
|
|
699
723
|
fi
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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
|
|
708
732
|
fi`
|
|
709
|
-
|
|
710
|
-
|
|
733
|
+
: agent === 'droid'
|
|
734
|
+
? `# Droid (Factory AI) installs a standalone native binary at ~/.local/bin/droid;
|
|
711
735
|
# there is no npm package and nothing lands in node_modules/.bin. The PATH
|
|
712
736
|
# fallback refuses anything under our shims dir to avoid an infinite re-exec.
|
|
713
737
|
DROID_BINARY="$HOME/.local/bin/droid"
|
|
@@ -719,7 +743,7 @@ else
|
|
|
719
743
|
"$HOME/.agents/.cache/shims/"*) BINARY="" ;;
|
|
720
744
|
esac
|
|
721
745
|
fi`
|
|
722
|
-
|
|
746
|
+
: `BINARY="${versionDir}/node_modules/.bin/${agentConfig.cliCommand}"`;
|
|
723
747
|
return `#!/bin/bash
|
|
724
748
|
# Auto-generated by agents-cli - do not edit
|
|
725
749
|
# ${VERSIONED_ALIAS_VERSION_MARKER} ${VERSIONED_ALIAS_SCHEMA_VERSION}
|
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;
|