@phnx-labs/agents-cli 1.20.40 → 1.20.42
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 +17 -0
- package/dist/commands/computer-actions.d.ts +2 -0
- package/dist/commands/computer-actions.js +60 -1
- package/dist/commands/computer.d.ts +2 -2
- package/dist/commands/computer.js +4 -4
- package/dist/commands/exec.js +2 -0
- package/dist/commands/focus.d.ts +31 -0
- package/dist/commands/focus.js +150 -0
- package/dist/commands/go.d.ts +34 -11
- package/dist/commands/go.js +50 -65
- package/dist/commands/secrets.js +49 -10
- package/dist/commands/sessions.d.ts +9 -0
- package/dist/commands/sessions.js +77 -20
- package/dist/lib/computer-rpc.js +3 -3
- package/dist/lib/exec.d.ts +52 -0
- package/dist/lib/exec.js +150 -0
- package/dist/lib/hooks/cache.d.ts +1 -1
- package/dist/lib/hooks/cache.js +4 -2
- package/dist/lib/hosts/option.js +1 -0
- package/dist/lib/hosts/passthrough.d.ts +3 -3
- package/dist/lib/hosts/passthrough.js +14 -4
- package/dist/lib/hosts/remote-cmd.d.ts +7 -1
- package/dist/lib/hosts/remote-cmd.js +8 -1
- package/dist/lib/menubar/install-menubar.js +2 -2
- package/dist/lib/secrets/agent.d.ts +18 -7
- package/dist/lib/secrets/agent.js +32 -15
- package/dist/lib/secrets/bundles.d.ts +8 -6
- package/dist/lib/secrets/bundles.js +14 -8
- package/dist/lib/secrets/remote.js +14 -0
- package/dist/lib/secrets/sync.js +13 -0
- package/dist/lib/session/active.d.ts +47 -3
- package/dist/lib/session/active.js +132 -10
- package/dist/lib/session/db.js +45 -31
- package/dist/lib/session/discover.d.ts +5 -0
- package/dist/lib/session/discover.js +9 -2
- package/dist/lib/session/viewing-in.d.ts +54 -0
- package/dist/lib/session/viewing-in.js +155 -0
- package/dist/lib/shims.d.ts +1 -1
- package/dist/lib/shims.js +32 -10
- package/dist/lib/ssh-tunnel.d.ts +1 -1
- package/dist/lib/ssh-tunnel.js +3 -3
- package/dist/lib/tmux/session.d.ts +46 -0
- package/dist/lib/tmux/session.js +84 -2
- package/dist/lib/types.d.ts +3 -3
- package/package.json +1 -1
package/dist/lib/shims.js
CHANGED
|
@@ -211,7 +211,7 @@ async function promptConflictStrategy(conflictInfos) {
|
|
|
211
211
|
// v22 — export DISABLE_AUTOUPDATER=1 for claude shims so a pinned per-version
|
|
212
212
|
// install can't self-mutate: Claude Code's background auto-updater would
|
|
213
213
|
// otherwise rewrite the pinned binary in place. Explicit user value wins.
|
|
214
|
-
export const SHIM_SCHEMA_VERSION =
|
|
214
|
+
export const SHIM_SCHEMA_VERSION = 24;
|
|
215
215
|
/** Internal marker string used to embed the schema version in shim scripts. */
|
|
216
216
|
const SHIM_VERSION_MARKER = 'agents-shim-version:';
|
|
217
217
|
function shellQuote(value) {
|
|
@@ -346,16 +346,38 @@ find_project_version() {
|
|
|
346
346
|
return 1
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
-
#
|
|
349
|
+
# Parse the agents: default map of one agents.yaml for this AGENT's version.
|
|
350
|
+
parse_agents_default() {
|
|
351
|
+
local meta="$1"
|
|
352
|
+
[ -f "$meta" ] || return 0
|
|
353
|
+
awk -v agent="$AGENT" '
|
|
354
|
+
/^agents:/ { in_agents=1; next }
|
|
355
|
+
in_agents && /^[^ ]/ { in_agents=0 }
|
|
356
|
+
in_agents && $0 ~ "^ " agent ":" { gsub(/.*:[[:space:]]*["'"'"']?|["'"'"']?[[:space:]]*$/, ""); print; exit }
|
|
357
|
+
' "$meta"
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
# This machine's device id — mirrors machineId()/normalizeHost() in
|
|
361
|
+
# src/lib/machine-id.ts: first hostname label, lowercased, non-[a-z0-9_-] -> '-'.
|
|
362
|
+
# MUST stay in sync or the shim reads the wrong device folder.
|
|
363
|
+
machine_id() {
|
|
364
|
+
local raw="\${AGENTS_SYNC_MACHINE_ID:-$(hostname 2>/dev/null)}"
|
|
365
|
+
# first label -> trim -> lowercase -> non-[a-z0-9_-] to '-' (matches normalizeHost order).
|
|
366
|
+
raw=$(printf '%s' "$raw" | cut -d. -f1 | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9_-]/-/g')
|
|
367
|
+
[ -n "$raw" ] && printf '%s' "$raw" || printf 'unknown'
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
# Resolve the default version. The agents: version pins are stored PER-DEVICE at
|
|
371
|
+
# devices/<machine>/agents.yaml (moved there so multi-machine syncs never
|
|
372
|
+
# conflict); read that first, then fall back to the central agents.yaml for
|
|
373
|
+
# pre-split installs. Must match readMeta()'s central+device merge in state.ts --
|
|
374
|
+
# reading only the central file (the old behavior) missed every device pin and
|
|
375
|
+
# made the shim re-prompt "no default set" on every launch.
|
|
350
376
|
resolve_default_version() {
|
|
351
|
-
local
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
in_agents && /^[^ ]/ { in_agents=0 }
|
|
356
|
-
in_agents && $0 ~ "^ " agent ":" { gsub(/.*:[[:space:]]*["'"'"']?|["'"'"']?[[:space:]]*$/, ""); print; exit }
|
|
357
|
-
' "$meta"
|
|
358
|
-
fi
|
|
377
|
+
local v
|
|
378
|
+
v=$(parse_agents_default "$AGENTS_USER_DIR/devices/$(machine_id)/agents.yaml")
|
|
379
|
+
[ -n "$v" ] || v=$(parse_agents_default "$AGENTS_USER_DIR/agents.yaml")
|
|
380
|
+
printf '%s' "$v"
|
|
359
381
|
}
|
|
360
382
|
|
|
361
383
|
# Find the latest installed version by numeric component comparison.
|
package/dist/lib/ssh-tunnel.d.ts
CHANGED
|
@@ -47,7 +47,7 @@ export declare function startSSHTunnel(user: string, host: string, localPort: nu
|
|
|
47
47
|
export declare const REMOTE_HELPER_PORT = 8765;
|
|
48
48
|
/** Task Scheduler task name for the daemon. Stable so setup/stop pair up. */
|
|
49
49
|
export declare const REMOTE_TASK_NAME = "AgentsComputerHelper";
|
|
50
|
-
/** Basename of the cross-published exe under
|
|
50
|
+
/** Basename of the cross-published exe under native/computer-win/dist. */
|
|
51
51
|
export declare const WIN_HELPER_EXE = "computer-helper-win.exe";
|
|
52
52
|
/**
|
|
53
53
|
* Locate the cross-published Windows daemon exe. Only the local build output is
|
package/dist/lib/ssh-tunnel.js
CHANGED
|
@@ -86,7 +86,7 @@ export function startSSHTunnel(user, host, localPort, remotePort, opts = {}) {
|
|
|
86
86
|
export const REMOTE_HELPER_PORT = 8765;
|
|
87
87
|
/** Task Scheduler task name for the daemon. Stable so setup/stop pair up. */
|
|
88
88
|
export const REMOTE_TASK_NAME = 'AgentsComputerHelper';
|
|
89
|
-
/** Basename of the cross-published exe under
|
|
89
|
+
/** Basename of the cross-published exe under native/computer-win/dist. */
|
|
90
90
|
export const WIN_HELPER_EXE = 'computer-helper-win.exe';
|
|
91
91
|
/**
|
|
92
92
|
* Locate the cross-published Windows daemon exe. Only the local build output is
|
|
@@ -95,8 +95,8 @@ export const WIN_HELPER_EXE = 'computer-helper-win.exe';
|
|
|
95
95
|
export function resolveWinHelperExe() {
|
|
96
96
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
97
97
|
const candidates = [
|
|
98
|
-
// Running from the agents-cli checkout
|
|
99
|
-
path.resolve(here, '..', '..', '
|
|
98
|
+
// Running from the agents-cli checkout. apps/cli/dist/lib -> repo root (4 up) -> native/computer-win.
|
|
99
|
+
path.resolve(here, '..', '..', '..', '..', 'native', 'computer-win', 'dist', WIN_HELPER_EXE),
|
|
100
100
|
// Bundled with the npm package (dist/lib -> package root).
|
|
101
101
|
path.resolve(here, '..', 'computer-helper-win', WIN_HELPER_EXE),
|
|
102
102
|
];
|
|
@@ -22,6 +22,13 @@ export interface SessionMeta {
|
|
|
22
22
|
source: 'cli' | 'extension' | 'teams' | 'external';
|
|
23
23
|
/** Free-form labels callers can stamp (e.g. `{ agent: 'claude', vscodePid: 1234 }`). */
|
|
24
24
|
labels?: Record<string, string>;
|
|
25
|
+
/**
|
|
26
|
+
* The first pane's id (`%N`) captured at creation. The exact send-keys /
|
|
27
|
+
* attach handle for the agent that runs in this session — recorded so
|
|
28
|
+
* `agents sessions --active` and the spawn-wrap path (src/lib/exec.ts) don't
|
|
29
|
+
* have to re-query it. Absent for pre-existing/`attach-existing` sessions.
|
|
30
|
+
*/
|
|
31
|
+
pane?: string;
|
|
25
32
|
}
|
|
26
33
|
export interface CreateSessionOptions {
|
|
27
34
|
name: string;
|
|
@@ -80,6 +87,45 @@ export declare function killAll(socket?: string): Promise<number>;
|
|
|
80
87
|
* failure (tmux gone, foreign socket) so callers fall back to the raw pane id.
|
|
81
88
|
*/
|
|
82
89
|
export declare function mapPanesToTargets(socket?: string): Promise<Map<string, string>>;
|
|
90
|
+
/** One tmux client attached to the shared server. */
|
|
91
|
+
export interface TmuxClient {
|
|
92
|
+
/** Controlling TTY of the terminal running `tmux attach` (e.g. '/dev/ttys004'). */
|
|
93
|
+
tty: string;
|
|
94
|
+
/** PID of the `tmux attach` client process — the leaf whose ancestry names the host app. */
|
|
95
|
+
pid: number;
|
|
96
|
+
/** The `session:window.pane` the client is currently displaying. */
|
|
97
|
+
target: string;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* List every client attached to the shared server, with the terminal PID and
|
|
101
|
+
* the session/window/pane it's viewing. This is how "viewing in <app> tab N"
|
|
102
|
+
* resolves: a client's `pid` walks the process ancestry to name the host app,
|
|
103
|
+
* and its `target` says which session it's attached to. Best-effort — returns
|
|
104
|
+
* an empty list on any failure (no server, foreign socket) so the renderer
|
|
105
|
+
* degrades to "detached".
|
|
106
|
+
*/
|
|
107
|
+
export declare function listClients(socket?: string): Promise<TmuxClient[]>;
|
|
108
|
+
/** A dead pane's exit status, read from tmux while the pane lingers under remain-on-exit. */
|
|
109
|
+
export interface PaneExit {
|
|
110
|
+
/** True once the process that ran in the pane has exited (pane is dead). */
|
|
111
|
+
dead: boolean;
|
|
112
|
+
/** Exit status of the dead pane's process, when tmux reports it. */
|
|
113
|
+
status?: number;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Read whether a pane's process has exited and, if so, its exit status. Used by
|
|
117
|
+
* the spawn-wrap path to recover the wrapped agent's exit code after the attach
|
|
118
|
+
* client returns. Returns `{ dead: false }` when tmux can't answer (session gone,
|
|
119
|
+
* pane missing) so the caller treats an unreadable pane as "still alive / detach".
|
|
120
|
+
*/
|
|
121
|
+
export declare function paneExitStatus(pane: string, socket?: string): Promise<PaneExit>;
|
|
122
|
+
/**
|
|
123
|
+
* Bind a per-session hook. Used by the spawn-wrap path to install a `pane-died`
|
|
124
|
+
* hook that detaches the attach client the instant the wrapped agent exits (the
|
|
125
|
+
* global `remain-on-exit on` otherwise leaves the client staring at a dead pane).
|
|
126
|
+
* Best-effort — a failed hook just means the user Ctrl-b d's out manually.
|
|
127
|
+
*/
|
|
128
|
+
export declare function setSessionHook(name: string, hook: string, command: string, socket?: string): Promise<void>;
|
|
83
129
|
/**
|
|
84
130
|
* List live sessions on the socket. Reconciles meta JSONs against tmux's view:
|
|
85
131
|
* - tmux session with no meta → returned without `meta` (external session)
|
package/dist/lib/tmux/session.js
CHANGED
|
@@ -76,7 +76,9 @@ export async function createSession(opts) {
|
|
|
76
76
|
// exit the server, and the follow-up `set-option` would race with "no
|
|
77
77
|
// server running". Server-wide (`-g`) is applied in the same tmux
|
|
78
78
|
// invocation as new-session so they share one server lifetime.
|
|
79
|
-
|
|
79
|
+
// `-P -F '#{pane_id}'` prints the new session's first pane id on stdout so we
|
|
80
|
+
// can record the exact `%N` handle without a follow-up `list-panes`.
|
|
81
|
+
const args = ['set-option', '-g', 'remain-on-exit', 'on', ';', 'new-session', '-d', '-s', opts.name, '-P', '-F', '#{pane_id}'];
|
|
80
82
|
if (opts.width)
|
|
81
83
|
args.push('-x', String(opts.width));
|
|
82
84
|
if (opts.height)
|
|
@@ -88,7 +90,9 @@ export async function createSession(opts) {
|
|
|
88
90
|
if (opts.cmd) {
|
|
89
91
|
args.push('--', 'sh', '-c', opts.cmd);
|
|
90
92
|
}
|
|
91
|
-
await runTmux({ socket, args, env: opts.env });
|
|
93
|
+
const res = await runTmux({ socket, args, env: opts.env });
|
|
94
|
+
// Only the new-session command in the `;`-chained invocation emits output.
|
|
95
|
+
const pane = /^%\d+$/.test(res.stdout.trim()) ? res.stdout.trim() : undefined;
|
|
92
96
|
const meta = {
|
|
93
97
|
name: opts.name,
|
|
94
98
|
socket,
|
|
@@ -97,6 +101,7 @@ export async function createSession(opts) {
|
|
|
97
101
|
cwd: opts.cwd,
|
|
98
102
|
source: opts.source ?? 'cli',
|
|
99
103
|
labels: opts.labels,
|
|
104
|
+
pane,
|
|
100
105
|
};
|
|
101
106
|
writeSessionMeta(meta);
|
|
102
107
|
return meta;
|
|
@@ -187,6 +192,83 @@ export async function mapPanesToTargets(socket) {
|
|
|
187
192
|
}
|
|
188
193
|
return out;
|
|
189
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* List every client attached to the shared server, with the terminal PID and
|
|
197
|
+
* the session/window/pane it's viewing. This is how "viewing in <app> tab N"
|
|
198
|
+
* resolves: a client's `pid` walks the process ancestry to name the host app,
|
|
199
|
+
* and its `target` says which session it's attached to. Best-effort — returns
|
|
200
|
+
* an empty list on any failure (no server, foreign socket) so the renderer
|
|
201
|
+
* degrades to "detached".
|
|
202
|
+
*/
|
|
203
|
+
export async function listClients(socket) {
|
|
204
|
+
let res;
|
|
205
|
+
try {
|
|
206
|
+
res = await runTmux({
|
|
207
|
+
socket,
|
|
208
|
+
args: ['list-clients', '-F', '#{client_tty} #{client_pid} #{session_name}:#{window_index}.#{pane_index}'],
|
|
209
|
+
throwOnError: false,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
if (res.code !== 0)
|
|
216
|
+
return [];
|
|
217
|
+
const out = [];
|
|
218
|
+
for (const line of res.stdout.split('\n')) {
|
|
219
|
+
const t = line.trim();
|
|
220
|
+
if (!t)
|
|
221
|
+
continue;
|
|
222
|
+
const sp1 = t.indexOf(' ');
|
|
223
|
+
if (sp1 < 0)
|
|
224
|
+
continue;
|
|
225
|
+
const sp2 = t.indexOf(' ', sp1 + 1);
|
|
226
|
+
if (sp2 < 0)
|
|
227
|
+
continue;
|
|
228
|
+
const tty = t.slice(0, sp1);
|
|
229
|
+
const pid = parseInt(t.slice(sp1 + 1, sp2), 10);
|
|
230
|
+
const target = t.slice(sp2 + 1).trim();
|
|
231
|
+
if (!Number.isFinite(pid) || !target)
|
|
232
|
+
continue;
|
|
233
|
+
out.push({ tty, pid, target });
|
|
234
|
+
}
|
|
235
|
+
return out;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Read whether a pane's process has exited and, if so, its exit status. Used by
|
|
239
|
+
* the spawn-wrap path to recover the wrapped agent's exit code after the attach
|
|
240
|
+
* client returns. Returns `{ dead: false }` when tmux can't answer (session gone,
|
|
241
|
+
* pane missing) so the caller treats an unreadable pane as "still alive / detach".
|
|
242
|
+
*/
|
|
243
|
+
export async function paneExitStatus(pane, socket) {
|
|
244
|
+
let res;
|
|
245
|
+
try {
|
|
246
|
+
res = await runTmux({
|
|
247
|
+
socket,
|
|
248
|
+
args: ['display-message', '-pt', pane, '-p', '#{pane_dead} #{pane_dead_status}'],
|
|
249
|
+
throwOnError: false,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return { dead: false };
|
|
254
|
+
}
|
|
255
|
+
if (res.code !== 0)
|
|
256
|
+
return { dead: false };
|
|
257
|
+
const [deadRaw, statusRaw] = res.stdout.trim().split(/\s+/);
|
|
258
|
+
const status = statusRaw !== undefined && statusRaw !== '' ? parseInt(statusRaw, 10) : undefined;
|
|
259
|
+
return { dead: deadRaw === '1', status: Number.isFinite(status) ? status : undefined };
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Bind a per-session hook. Used by the spawn-wrap path to install a `pane-died`
|
|
263
|
+
* hook that detaches the attach client the instant the wrapped agent exits (the
|
|
264
|
+
* global `remain-on-exit on` otherwise leaves the client staring at a dead pane).
|
|
265
|
+
* Best-effort — a failed hook just means the user Ctrl-b d's out manually.
|
|
266
|
+
*/
|
|
267
|
+
export async function setSessionHook(name, hook, command, socket) {
|
|
268
|
+
assertValidSessionName(name);
|
|
269
|
+
const sock = socket ?? getDefaultSocketPath();
|
|
270
|
+
await runTmux({ socket: sock, args: ['set-hook', '-t', name, hook, command], throwOnError: false }).catch(() => { });
|
|
271
|
+
}
|
|
190
272
|
/**
|
|
191
273
|
* List live sessions on the socket. Reconciles meta JSONs against tmux's view:
|
|
192
274
|
* - tmux session with no meta → returned without `meta` (external session)
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -599,9 +599,9 @@ export interface Meta {
|
|
|
599
599
|
run?: RunConfig;
|
|
600
600
|
/** macOS secrets-agent config. `policy` is the default prompt policy for
|
|
601
601
|
* bundles without an explicit per-bundle policy: `daily` (the default) asks
|
|
602
|
-
* once per ~
|
|
603
|
-
* real keychain read of a `daily` bundle populate the broker so
|
|
604
|
-
* runs read silently — set it `false` to force a prompt on every read. */
|
|
602
|
+
* once per ~7 days, `always` asks every time. `auto` (default on) lets the
|
|
603
|
+
* first real keychain read of a `daily` bundle populate the broker so
|
|
604
|
+
* concurrent runs read silently — set it `false` to force a prompt on every read. */
|
|
605
605
|
secrets?: {
|
|
606
606
|
policy?: 'always' | 'daily';
|
|
607
607
|
agent?: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.42",
|
|
4
4
|
"description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|