@phnx-labs/agents-cli 1.20.49 → 1.20.51
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 +31 -0
- package/README.md +3 -0
- package/dist/commands/browser-picker.js +1 -18
- package/dist/commands/cloud.js +1 -25
- package/dist/commands/computer.d.ts +1 -0
- package/dist/commands/computer.js +129 -8
- package/dist/commands/doctor.js +133 -2
- package/dist/commands/exec.js +49 -6
- package/dist/commands/factory.js +1 -4
- package/dist/commands/inspect.js +1 -11
- package/dist/commands/mcp.js +2 -6
- package/dist/commands/message.js +1 -4
- package/dist/commands/profiles.js +1 -18
- package/dist/commands/repo.js +33 -14
- package/dist/commands/resource-view.d.ts +1 -0
- package/dist/commands/resource-view.js +5 -17
- package/dist/commands/secrets.d.ts +1 -0
- package/dist/commands/secrets.js +1 -28
- package/dist/commands/sessions-picker.js +1 -18
- package/dist/commands/sessions.js +6 -8
- package/dist/commands/teams-picker.js +1 -32
- package/dist/commands/teams.js +218 -97
- package/dist/commands/tmux.js +1 -3
- package/dist/commands/view.js +1 -9
- package/dist/commands/worktree.js +1 -4
- package/dist/lib/agents.d.ts +0 -4
- package/dist/lib/agents.js +20 -33
- package/dist/lib/auto-dispatch-linear.d.ts +18 -0
- package/dist/lib/auto-dispatch-linear.js +107 -0
- package/dist/lib/auto-dispatch-provider.d.ts +10 -0
- package/dist/lib/auto-dispatch-provider.js +25 -0
- package/dist/lib/auto-dispatch.d.ts +87 -0
- package/dist/lib/auto-dispatch.js +142 -0
- package/dist/lib/browser/cdp.js +11 -2
- package/dist/lib/browser/drivers/ssh.d.ts +28 -10
- package/dist/lib/browser/drivers/ssh.js +57 -18
- package/dist/lib/browser/refs.js +1 -5
- package/dist/lib/cli-resources.d.ts +0 -2
- package/dist/lib/cli-resources.js +30 -13
- package/dist/lib/cloud/rush.d.ts +0 -24
- package/dist/lib/cloud/rush.js +0 -31
- package/dist/lib/crabbox/cli.js +4 -1
- package/dist/lib/crabbox/lease.js +29 -1
- package/dist/lib/daemon.js +41 -0
- package/dist/lib/exec.js +43 -18
- package/dist/lib/format.d.ts +38 -0
- package/dist/lib/format.js +108 -0
- package/dist/lib/git.d.ts +21 -0
- package/dist/lib/git.js +92 -0
- package/dist/lib/hooks/cache.d.ts +9 -2
- package/dist/lib/hooks/cache.js +220 -8
- package/dist/lib/hooks.js +17 -8
- package/dist/lib/hosts/passthrough.js +30 -1
- package/dist/lib/hosts/progress.d.ts +31 -0
- package/dist/lib/hosts/progress.js +35 -0
- package/dist/lib/hosts/remote-cmd.d.ts +1 -1
- package/dist/lib/hosts/remote-cmd.js +13 -3
- package/dist/lib/platform/exec.d.ts +4 -1
- package/dist/lib/platform/exec.js +8 -2
- package/dist/lib/resources.d.ts +0 -8
- package/dist/lib/resources.js +0 -10
- package/dist/lib/runner.js +10 -2
- 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/session/active.d.ts +11 -1
- package/dist/lib/session/active.js +3 -0
- package/dist/lib/session/db.d.ts +1 -4
- package/dist/lib/session/db.js +20 -25
- package/dist/lib/session/discover.d.ts +2 -2
- package/dist/lib/session/discover.js +61 -48
- package/dist/lib/session/parse.js +35 -34
- package/dist/lib/session/render.d.ts +7 -3
- package/dist/lib/session/render.js +15 -9
- package/dist/lib/session/state.d.ts +55 -0
- package/dist/lib/session/state.js +87 -10
- package/dist/lib/session/types.d.ts +9 -0
- package/dist/lib/shims.d.ts +25 -2
- package/dist/lib/shims.js +73 -8
- package/dist/lib/ssh-tunnel.d.ts +33 -2
- package/dist/lib/ssh-tunnel.js +94 -7
- package/dist/lib/staleness/types.d.ts +0 -1
- package/dist/lib/teams/agents.d.ts +108 -1
- package/dist/lib/teams/agents.js +511 -11
- package/dist/lib/teams/api.d.ts +7 -1
- package/dist/lib/teams/api.js +5 -2
- package/dist/lib/teams/registry.d.ts +17 -0
- package/dist/lib/teams/registry.js +2 -0
- package/dist/lib/teams/remoteWorktree.d.ts +57 -0
- package/dist/lib/teams/remoteWorktree.js +213 -0
- package/dist/lib/teams/scheduler.d.ts +29 -0
- package/dist/lib/teams/scheduler.js +78 -0
- package/dist/lib/teams/supervisor.js +7 -0
- package/dist/lib/types.d.ts +14 -1
- package/dist/lib/versions.d.ts +7 -26
- package/dist/lib/versions.js +44 -146
- package/dist/lib/warn-unpushed.d.ts +40 -0
- package/dist/lib/warn-unpushed.js +128 -0
- package/package.json +3 -1
- package/dist/lib/resources/index.d.ts +0 -53
- package/dist/lib/resources/index.js +0 -76
|
@@ -25,26 +25,44 @@ export declare function connectSSH(endpoint: string, profile: BrowserProfile): P
|
|
|
25
25
|
*/
|
|
26
26
|
export declare function encodePowerShell(script: string): string;
|
|
27
27
|
/**
|
|
28
|
-
* The PowerShell that launches the browser on a Windows remote.
|
|
28
|
+
* The PowerShell that launches the browser on a Windows remote. Three hard
|
|
29
29
|
* requirements shaped this:
|
|
30
30
|
* 1. The browser must OUTLIVE the ssh session. Windows OpenSSH terminates
|
|
31
31
|
* the session's job tree on disconnect, which reaps both `start /B` and
|
|
32
|
-
* `Start-Process` children (verified against a real box).
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
32
|
+
* `Start-Process` children (verified against a real box). A scheduled
|
|
33
|
+
* task runs under the Task Scheduler service, so the process survives
|
|
34
|
+
* after we drop the ssh connection and reconnect over the CDP tunnel.
|
|
35
|
+
* 2. The browser must run in the user's INTERACTIVE session. The previous
|
|
36
|
+
* WMI `Win32_Process.Create` launch also survived disconnect, but it
|
|
37
|
+
* spawned Edge in session 0 (the services session): the CDP socket bound
|
|
38
|
+
* and accepted, yet the DevTools server never initialized — every
|
|
39
|
+
* /json/version probe hung forever and DevToolsActivePort was never
|
|
40
|
+
* written (verified against a real box, Edg/150). A scheduled task
|
|
41
|
+
* registered and started by the logged-on user runs in that user's
|
|
42
|
+
* interactive session, where DevTools comes up normally.
|
|
43
|
+
* 3. A distinct `--user-data-dir` so a fresh instance bound to the debugging
|
|
37
44
|
* port comes up even when the user already has Edge open.
|
|
38
45
|
* CreateProcess ignores App Paths, so we resolve the real `.exe` from the
|
|
39
|
-
* registry at runtime rather than relying on a bare `msedge` name.
|
|
46
|
+
* registry at runtime rather than relying on a bare `msedge` name. The task is
|
|
47
|
+
* unregistered immediately after start — the browser keeps running; nothing
|
|
48
|
+
* lingers in the scheduler.
|
|
40
49
|
*/
|
|
41
50
|
export declare function buildWindowsLaunchScript(browserType: string, port: number, customBinary?: string): string;
|
|
42
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* The PowerShell that kills whatever holds the CDP port on a Windows remote.
|
|
53
|
+
* Tree-kill (`taskkill /T`), not `Stop-Process`: Chromium is multi-process, and
|
|
54
|
+
* killing only the port-owning main process orphans its children. The orphans
|
|
55
|
+
* keep the profile's SingletonLock, so every later launch against the same
|
|
56
|
+
* `--user-data-dir` delegates to the zombie tree, exits, and never binds the
|
|
57
|
+
* CDP port — `browser start --host` then fails until someone hand-cleans the
|
|
58
|
+
* box (found live on win-mini by the #561 e2e suite).
|
|
59
|
+
*/
|
|
43
60
|
export declare function buildWindowsKillScript(port: number): string;
|
|
44
61
|
/**
|
|
45
62
|
* Build the remote command that launches the browser detached with a CDP port.
|
|
46
63
|
* POSIX backgrounds the `.app` binary with `… &`; Windows resolves the exe and
|
|
47
|
-
*
|
|
64
|
+
* starts it via an interactive one-shot scheduled task (encoded PowerShell)
|
|
65
|
+
* so it survives the ssh session AND serves CDP (session 0 never does).
|
|
48
66
|
*/
|
|
49
67
|
export declare function buildLaunchCmd(remoteOs: RemoteOs, browserType: string, port: number, customBinary?: string): string;
|
|
50
68
|
/**
|
|
@@ -57,7 +75,7 @@ export declare function ensureRemoteBrowser(user: string, host: string, browserT
|
|
|
57
75
|
export declare function restartRemoteBrowser(user: string, host: string, browserType: string, port: number, remoteOs: RemoteOs, customBinary?: string): Promise<void>;
|
|
58
76
|
/**
|
|
59
77
|
* Kill the remote browser holding the CDP port. Invoked on stop/cleanup so the
|
|
60
|
-
*
|
|
78
|
+
* task-launched (Windows) / detached (posix) browser process is not orphaned when
|
|
61
79
|
* the ssh tunnel is torn down — killing only the tunnel left it running on
|
|
62
80
|
* win-mini after every `browser stop`. Best-effort: never rejects.
|
|
63
81
|
*/
|
|
@@ -5,8 +5,11 @@ import { getPortOccupant } from '../chrome.js';
|
|
|
5
5
|
import { parseEndpointUrl } from '../profiles.js';
|
|
6
6
|
import { writeProfileRuntime, clearProfileRuntime } from '../runtime-state.js';
|
|
7
7
|
// shellQuote lives in the shared ssh-exec helper (single choke point); re-export
|
|
8
|
-
// so existing importers of `shellQuote` from this module keep working.
|
|
9
|
-
|
|
8
|
+
// so existing importers of `shellQuote` from this module keep working. SSH_OPTS
|
|
9
|
+
// is the shared hardened baseline (BatchMode + ConnectTimeout + keepalive) —
|
|
10
|
+
// reuse it so the raw-ssh spawns below fail fast on an unreachable host instead
|
|
11
|
+
// of hanging on the default ~127s TCP timeout, rather than re-listing options.
|
|
12
|
+
import { shellQuote, SSH_OPTS } from '../../ssh-exec.js';
|
|
10
13
|
export { shellQuote };
|
|
11
14
|
// The `ssh -L` tunnel spawn is shared with `agents computer --host`; it lives in
|
|
12
15
|
// the single ssh-tunnel helper. Calling it with no options preserves this
|
|
@@ -168,18 +171,27 @@ export function encodePowerShell(script) {
|
|
|
168
171
|
return `powershell -NoProfile -EncodedCommand ${encodePwshBase64(script)}`;
|
|
169
172
|
}
|
|
170
173
|
/**
|
|
171
|
-
* The PowerShell that launches the browser on a Windows remote.
|
|
174
|
+
* The PowerShell that launches the browser on a Windows remote. Three hard
|
|
172
175
|
* requirements shaped this:
|
|
173
176
|
* 1. The browser must OUTLIVE the ssh session. Windows OpenSSH terminates
|
|
174
177
|
* the session's job tree on disconnect, which reaps both `start /B` and
|
|
175
|
-
* `Start-Process` children (verified against a real box).
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
178
|
+
* `Start-Process` children (verified against a real box). A scheduled
|
|
179
|
+
* task runs under the Task Scheduler service, so the process survives
|
|
180
|
+
* after we drop the ssh connection and reconnect over the CDP tunnel.
|
|
181
|
+
* 2. The browser must run in the user's INTERACTIVE session. The previous
|
|
182
|
+
* WMI `Win32_Process.Create` launch also survived disconnect, but it
|
|
183
|
+
* spawned Edge in session 0 (the services session): the CDP socket bound
|
|
184
|
+
* and accepted, yet the DevTools server never initialized — every
|
|
185
|
+
* /json/version probe hung forever and DevToolsActivePort was never
|
|
186
|
+
* written (verified against a real box, Edg/150). A scheduled task
|
|
187
|
+
* registered and started by the logged-on user runs in that user's
|
|
188
|
+
* interactive session, where DevTools comes up normally.
|
|
189
|
+
* 3. A distinct `--user-data-dir` so a fresh instance bound to the debugging
|
|
180
190
|
* port comes up even when the user already has Edge open.
|
|
181
191
|
* CreateProcess ignores App Paths, so we resolve the real `.exe` from the
|
|
182
|
-
* registry at runtime rather than relying on a bare `msedge` name.
|
|
192
|
+
* registry at runtime rather than relying on a bare `msedge` name. The task is
|
|
193
|
+
* unregistered immediately after start — the browser keeps running; nothing
|
|
194
|
+
* lingers in the scheduler.
|
|
183
195
|
*/
|
|
184
196
|
export function buildWindowsLaunchScript(browserType, port, customBinary) {
|
|
185
197
|
let exeStmts;
|
|
@@ -211,21 +223,42 @@ export function buildWindowsLaunchScript(browserType, port, customBinary) {
|
|
|
211
223
|
// source — a test asserts CDP is never opened to `*`.
|
|
212
224
|
return [
|
|
213
225
|
...exeStmts,
|
|
214
|
-
|
|
226
|
+
// First-run / default-browser modals block automation (same rationale as
|
|
227
|
+
// the local launcher in chrome.ts), and the crash-restore bubble is worse
|
|
228
|
+
// here: `browser stop` hard-kills the remote process, so the NEXT launch
|
|
229
|
+
// against the same --user-data-dir marks the profile crashed and session
|
|
230
|
+
// restore swaps the initial page target — closing its CDP websocket while
|
|
231
|
+
// a command is pending (reproduced live: Page.captureScreenshot rejected
|
|
232
|
+
// with "CDP connection closed" on every reused-profile launch).
|
|
233
|
+
`$args = '--remote-debugging-port=${port}` +
|
|
215
234
|
` --remote-allow-origins=http://127.0.0.1:${port}` +
|
|
235
|
+
` --no-first-run --no-default-browser-check` +
|
|
236
|
+
` --hide-crash-restore-bubble --disable-session-crashed-bubble` +
|
|
216
237
|
` --disable-background-timer-throttling --user-data-dir="' + $env:TEMP + '\\agents-browser-${port}"'`,
|
|
217
|
-
|
|
238
|
+
`$action = New-ScheduledTaskAction -Execute $exe -Argument $args`,
|
|
239
|
+
`Register-ScheduledTask -TaskName 'agents-browser-${port}' -Action $action -Force | Out-Null`,
|
|
240
|
+
`Start-ScheduledTask -TaskName 'agents-browser-${port}'`,
|
|
241
|
+
`Unregister-ScheduledTask -TaskName 'agents-browser-${port}' -Confirm:$false`,
|
|
218
242
|
].join('; ');
|
|
219
243
|
}
|
|
220
|
-
/**
|
|
244
|
+
/**
|
|
245
|
+
* The PowerShell that kills whatever holds the CDP port on a Windows remote.
|
|
246
|
+
* Tree-kill (`taskkill /T`), not `Stop-Process`: Chromium is multi-process, and
|
|
247
|
+
* killing only the port-owning main process orphans its children. The orphans
|
|
248
|
+
* keep the profile's SingletonLock, so every later launch against the same
|
|
249
|
+
* `--user-data-dir` delegates to the zombie tree, exits, and never binds the
|
|
250
|
+
* CDP port — `browser start --host` then fails until someone hand-cleans the
|
|
251
|
+
* box (found live on win-mini by the #561 e2e suite).
|
|
252
|
+
*/
|
|
221
253
|
export function buildWindowsKillScript(port) {
|
|
222
254
|
return (`Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue ` +
|
|
223
|
-
`| ForEach-Object {
|
|
255
|
+
`| ForEach-Object { taskkill /PID $_.OwningProcess /T /F 2>$null } | Out-Null`);
|
|
224
256
|
}
|
|
225
257
|
/**
|
|
226
258
|
* Build the remote command that launches the browser detached with a CDP port.
|
|
227
259
|
* POSIX backgrounds the `.app` binary with `… &`; Windows resolves the exe and
|
|
228
|
-
*
|
|
260
|
+
* starts it via an interactive one-shot scheduled task (encoded PowerShell)
|
|
261
|
+
* so it survives the ssh session AND serves CDP (session 0 never does).
|
|
229
262
|
*/
|
|
230
263
|
export function buildLaunchCmd(remoteOs, browserType, port, customBinary) {
|
|
231
264
|
if (remoteOs === 'windows') {
|
|
@@ -266,10 +299,12 @@ export async function ensureRemoteBrowser(user, host, browserType, port, remoteO
|
|
|
266
299
|
// ignored and this resolved on close regardless of exit code, so failures
|
|
267
300
|
// were swallowed and only re-emerged 8s later as a generic
|
|
268
301
|
// "SSH tunnel failed to establish".
|
|
302
|
+
// Options BEFORE the target (matching `sshExec`): OpenSSH's BSD getopt on
|
|
303
|
+
// macOS stops at the first non-option, so any `-o` after `user@host` would
|
|
304
|
+
// be swallowed into the remote command instead of applied.
|
|
269
305
|
const child = spawn('ssh', [
|
|
306
|
+
...SSH_OPTS,
|
|
270
307
|
`${user}@${host}`,
|
|
271
|
-
'-o',
|
|
272
|
-
'BatchMode=yes',
|
|
273
308
|
remoteCmd,
|
|
274
309
|
], { stdio: ['ignore', 'ignore', 'pipe'], windowsHide: true });
|
|
275
310
|
let stderr = '';
|
|
@@ -319,7 +354,7 @@ export async function restartRemoteBrowser(user, host, browserType, port, remote
|
|
|
319
354
|
}
|
|
320
355
|
/**
|
|
321
356
|
* Kill the remote browser holding the CDP port. Invoked on stop/cleanup so the
|
|
322
|
-
*
|
|
357
|
+
* task-launched (Windows) / detached (posix) browser process is not orphaned when
|
|
323
358
|
* the ssh tunnel is torn down — killing only the tunnel left it running on
|
|
324
359
|
* win-mini after every `browser stop`. Best-effort: never rejects.
|
|
325
360
|
*/
|
|
@@ -328,7 +363,11 @@ export function killRemoteBrowser(user, host, remoteOs, port) {
|
|
|
328
363
|
}
|
|
329
364
|
function runSSHCommand(user, host, cmd) {
|
|
330
365
|
return new Promise((resolve) => {
|
|
331
|
-
|
|
366
|
+
// Reuse the shared hardened baseline so a hung TCP SYN to an unreachable
|
|
367
|
+
// host is bounded by ConnectTimeout (the local 3s kill below only bounds a
|
|
368
|
+
// connected-but-slow command, not the connect itself). Options precede the
|
|
369
|
+
// target — see ensureRemoteBrowser.
|
|
370
|
+
const child = spawn('ssh', [...SSH_OPTS, `${user}@${host}`, cmd], {
|
|
332
371
|
stdio: 'ignore',
|
|
333
372
|
windowsHide: true,
|
|
334
373
|
});
|
package/dist/lib/browser/refs.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { truncate } from '../format.js';
|
|
1
2
|
/** Snapshot the stable descriptor for every ref in a freshly-built node map. */
|
|
2
3
|
export function describeRefs(nodeMap) {
|
|
3
4
|
return Array.from(nodeMap.values()).map((n) => ({
|
|
@@ -175,11 +176,6 @@ export async function getRefs(cdp, sessionId, opts = {}) {
|
|
|
175
176
|
}
|
|
176
177
|
return { refs: lines.join('\n'), nodeMap, opts: { interactive, limit } };
|
|
177
178
|
}
|
|
178
|
-
function truncate(s, max) {
|
|
179
|
-
if (s.length <= max)
|
|
180
|
-
return s;
|
|
181
|
-
return s.slice(0, max - 1) + '…';
|
|
182
|
-
}
|
|
183
179
|
export async function resolveRefToCoords(cdp, sessionId, nodeMap, ref) {
|
|
184
180
|
const node = nodeMap.get(ref);
|
|
185
181
|
if (!node)
|
|
@@ -133,5 +133,3 @@ export declare function listCliStatus(cwd?: string): {
|
|
|
133
133
|
statuses: CliStatus[];
|
|
134
134
|
errors: CliManifestError[];
|
|
135
135
|
};
|
|
136
|
-
/** Names of CLIs that are declared but not currently installed on the host. */
|
|
137
|
-
export declare function getMissingClis(cwd?: string): CliManifest[];
|
|
@@ -21,6 +21,7 @@ import * as path from 'path';
|
|
|
21
21
|
import { spawnSync } from 'child_process';
|
|
22
22
|
import * as yaml from 'yaml';
|
|
23
23
|
import { listResources, resolveResource } from './resources.js';
|
|
24
|
+
import { composeWin32CommandLine } from './platform/index.js';
|
|
24
25
|
// ─── Validation primitives ───────────────────────────────────────────────────
|
|
25
26
|
/** Token allowed inside `check:` strings — letters, digits, underscore, dot, slash, dash. */
|
|
26
27
|
const SAFE_CHECK_TOKEN = /^[a-zA-Z0-9_./-]+$/;
|
|
@@ -253,19 +254,27 @@ export function resolveCliManifest(name, cwd) {
|
|
|
253
254
|
// ─── Host detection ──────────────────────────────────────────────────────────
|
|
254
255
|
/**
|
|
255
256
|
* Return true if a command resolves on the current PATH. Uses POSIX `command -v`
|
|
256
|
-
* via spawn argv (no shell); results are cached for the
|
|
257
|
+
* (or `where` on Windows) via spawn argv (no shell); results are cached for the
|
|
258
|
+
* lifetime of the process.
|
|
257
259
|
*/
|
|
258
260
|
const cmdExistsCache = new Map();
|
|
259
261
|
export function hasCommand(cmd) {
|
|
260
262
|
if (cmdExistsCache.has(cmd))
|
|
261
263
|
return cmdExistsCache.get(cmd);
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
stdio: 'ignore'
|
|
267
|
-
}
|
|
268
|
-
|
|
264
|
+
let ok;
|
|
265
|
+
if (process.platform === 'win32') {
|
|
266
|
+
// `sh` only exists when Git Bash is installed; `where` is the native PATH
|
|
267
|
+
// probe (resolves .exe/.cmd/.bat via PATHEXT). Argv keeps `cmd` uninterpolated.
|
|
268
|
+
ok = spawnSync('where', [cmd], { stdio: 'ignore' }).status === 0;
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
// `command` is a shell builtin on most POSIX shells; invoking `sh -c 'command -v X'`
|
|
272
|
+
// with X as an *argument* (not interpolated) is the safe path. `cmd` may be passed
|
|
273
|
+
// by callers that haven't validated it, so we route via argv to neutralize metas.
|
|
274
|
+
ok = spawnSync('sh', ['-c', 'command -v "$1" >/dev/null 2>&1', '_', cmd], {
|
|
275
|
+
stdio: 'ignore',
|
|
276
|
+
}).status === 0;
|
|
277
|
+
}
|
|
269
278
|
cmdExistsCache.set(cmd, ok);
|
|
270
279
|
return ok;
|
|
271
280
|
}
|
|
@@ -280,7 +289,19 @@ export function isCliInstalled(manifest) {
|
|
|
280
289
|
return hasCommand(c.cmd);
|
|
281
290
|
}
|
|
282
291
|
const result = spawnSync(c.cmd, c.args, { stdio: 'ignore', timeout: 10_000 });
|
|
283
|
-
|
|
292
|
+
if (result.status === 0)
|
|
293
|
+
return true;
|
|
294
|
+
// On Windows the PATH entry point is often a `.cmd`/`.bat` shim (npm installs,
|
|
295
|
+
// script installers), which Node refuses to spawn without a shell (ENOENT /
|
|
296
|
+
// EINVAL). A failed *spawn* — not a failed run — gets one retry through the
|
|
297
|
+
// shell, as a single pre-composed line (DEP0190-safe). Check tokens are
|
|
298
|
+
// SAFE_CHECK_TOKEN-validated at parse time, so the line cannot inject.
|
|
299
|
+
if (process.platform === 'win32' && result.error) {
|
|
300
|
+
const line = composeWin32CommandLine(c.cmd, c.args);
|
|
301
|
+
const retry = spawnSync(line, { stdio: 'ignore', timeout: 10_000, shell: true });
|
|
302
|
+
return retry.status === 0;
|
|
303
|
+
}
|
|
304
|
+
return false;
|
|
284
305
|
}
|
|
285
306
|
// ─── Method selection ────────────────────────────────────────────────────────
|
|
286
307
|
/**
|
|
@@ -480,7 +501,3 @@ export function listCliStatus(cwd) {
|
|
|
480
501
|
}));
|
|
481
502
|
return { statuses, errors };
|
|
482
503
|
}
|
|
483
|
-
/** Names of CLIs that are declared but not currently installed on the host. */
|
|
484
|
-
export function getMissingClis(cwd) {
|
|
485
|
-
return listCliStatus(cwd).statuses.filter((s) => !s.installed).map((s) => s.manifest);
|
|
486
|
-
}
|
package/dist/lib/cloud/rush.d.ts
CHANGED
|
@@ -75,30 +75,6 @@ export declare function buildDispatchBody(input: {
|
|
|
75
75
|
*/
|
|
76
76
|
images?: ImageAttachment[] | null;
|
|
77
77
|
}): Record<string, unknown>;
|
|
78
|
-
/** A single account registered in Rush Cloud's multi-account rotation pool. */
|
|
79
|
-
export interface RemoteAccount {
|
|
80
|
-
id: string;
|
|
81
|
-
provider: string;
|
|
82
|
-
email: string | null;
|
|
83
|
-
subscription_type: string | null;
|
|
84
|
-
five_hour_pct: number | null;
|
|
85
|
-
seven_day_pct: number | null;
|
|
86
|
-
usage_fetched_at: string | null;
|
|
87
|
-
created_at: string;
|
|
88
|
-
}
|
|
89
|
-
/** Fetch all Claude accounts in this user's Rush Cloud rotation pool (no tokens). */
|
|
90
|
-
export declare function listRemoteAccounts(): Promise<RemoteAccount[]>;
|
|
91
|
-
/**
|
|
92
|
-
* Register a CLAUDE_CODE_OAUTH_TOKEN with Rush Cloud's rotation pool.
|
|
93
|
-
* The server validates the token against the Anthropic usage API and stores it
|
|
94
|
-
* encrypted in Vault. Returns the account metadata (no token).
|
|
95
|
-
*/
|
|
96
|
-
export declare function addRemoteAccount(provider: string, pastedToken: string): Promise<RemoteAccount & {
|
|
97
|
-
five_hour_pct: number | null;
|
|
98
|
-
seven_day_pct: number | null;
|
|
99
|
-
}>;
|
|
100
|
-
/** Remove a Claude account from Rush Cloud's rotation pool by its ID. */
|
|
101
|
-
export declare function removeRemoteAccount(id: string): Promise<void>;
|
|
102
78
|
export declare class RushCloudProvider implements CloudProvider {
|
|
103
79
|
id: "rush";
|
|
104
80
|
name: string;
|
package/dist/lib/cloud/rush.js
CHANGED
|
@@ -294,37 +294,6 @@ export function buildDispatchBody(input) {
|
|
|
294
294
|
}
|
|
295
295
|
return body;
|
|
296
296
|
}
|
|
297
|
-
/** Fetch all Claude accounts in this user's Rush Cloud rotation pool (no tokens). */
|
|
298
|
-
export async function listRemoteAccounts() {
|
|
299
|
-
const token = readToken();
|
|
300
|
-
const res = await api('GET', '/api/v1/cloud-accounts', token);
|
|
301
|
-
if (!res.ok) {
|
|
302
|
-
throw new Error(`Failed to list accounts (${res.status}): ${sanitizeErrorBody(await res.text())}`);
|
|
303
|
-
}
|
|
304
|
-
const data = await res.json();
|
|
305
|
-
return data.accounts ?? [];
|
|
306
|
-
}
|
|
307
|
-
/**
|
|
308
|
-
* Register a CLAUDE_CODE_OAUTH_TOKEN with Rush Cloud's rotation pool.
|
|
309
|
-
* The server validates the token against the Anthropic usage API and stores it
|
|
310
|
-
* encrypted in Vault. Returns the account metadata (no token).
|
|
311
|
-
*/
|
|
312
|
-
export async function addRemoteAccount(provider, pastedToken) {
|
|
313
|
-
const token = readToken();
|
|
314
|
-
const res = await api('POST', '/api/v1/cloud-accounts', token, { provider, token: pastedToken });
|
|
315
|
-
if (!res.ok) {
|
|
316
|
-
throw new Error(`Failed to add account (${res.status}): ${sanitizeErrorBody(await res.text())}`);
|
|
317
|
-
}
|
|
318
|
-
return await res.json();
|
|
319
|
-
}
|
|
320
|
-
/** Remove a Claude account from Rush Cloud's rotation pool by its ID. */
|
|
321
|
-
export async function removeRemoteAccount(id) {
|
|
322
|
-
const token = readToken();
|
|
323
|
-
const res = await api('DELETE', `/api/v1/cloud-accounts/${encodeURIComponent(id)}`, token);
|
|
324
|
-
if (!res.ok) {
|
|
325
|
-
throw new Error(`Failed to remove account (${res.status}): ${sanitizeErrorBody(await res.text())}`);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
297
|
export class RushCloudProvider {
|
|
329
298
|
id = 'rush';
|
|
330
299
|
name = 'Rush Cloud';
|
package/dist/lib/crabbox/cli.js
CHANGED
|
@@ -209,7 +209,10 @@ export function crabboxRunScript(slug, script, opts = {}) {
|
|
|
209
209
|
/** Release the lease / delete the box. Best-effort; never throws. */
|
|
210
210
|
export function crabboxStop(slug, opts = {}) {
|
|
211
211
|
try {
|
|
212
|
-
|
|
212
|
+
// Positional target: crabbox's stop subcommand has no --id flag (unlike
|
|
213
|
+
// status/run/ssh) — `stop --id <slug>` dies with "flag provided but not
|
|
214
|
+
// defined: -id" and the box leaks past the run it was leased for.
|
|
215
|
+
const r = spawnSync('crabbox', ['stop', slug], { encoding: 'utf-8', env: crabboxEnv(opts) });
|
|
213
216
|
return r.status === 0;
|
|
214
217
|
}
|
|
215
218
|
catch {
|
|
@@ -12,6 +12,34 @@ import { buildCredentialScript } from './runtimes.js';
|
|
|
12
12
|
function q(s) {
|
|
13
13
|
return "'" + s.replace(/'/g, "'\\''") + "'";
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Bash snippet that guarantees `agents` is runnable on the box. Fresh crabbox
|
|
17
|
+
* images ship without node, and the box user may not own the global npm prefix,
|
|
18
|
+
* so everything installs user-level under ~/.local (node from the official
|
|
19
|
+
* latest-v22.x tarball to satisfy engines.node >=22.5.0). Exits 96 with a
|
|
20
|
+
* diagnostic when the CLI still isn't runnable — a silent `|| true` here used
|
|
21
|
+
* to surface only as `agents: command not found` deep in the script.
|
|
22
|
+
*/
|
|
23
|
+
const ENSURE_AGENTS_CLI = [
|
|
24
|
+
'export PATH="$HOME/.local/bin:$PATH"',
|
|
25
|
+
'if ! command -v node >/dev/null 2>&1; then',
|
|
26
|
+
' case "$(uname -m)" in aarch64|arm64) narch=arm64;; *) narch=x64;; esac',
|
|
27
|
+
' nver=$(curl -fsSL https://nodejs.org/dist/latest-v22.x/ | grep -oE "v22\\.[0-9]+\\.[0-9]+" | head -1)',
|
|
28
|
+
' mkdir -p "$HOME/.local"',
|
|
29
|
+
' curl -fsSL "https://nodejs.org/dist/latest-v22.x/node-$nver-linux-$narch.tar.xz" | tar -xJ -C "$HOME/.local" --strip-components=1',
|
|
30
|
+
'fi',
|
|
31
|
+
'if ! command -v agents >/dev/null 2>&1; then',
|
|
32
|
+
' npm config set prefix "$HOME/.local" >/dev/null 2>&1 || true',
|
|
33
|
+
' npm install -g @phnx-labs/agents-cli >/dev/null 2>&1',
|
|
34
|
+
'fi',
|
|
35
|
+
'if ! command -v agents >/dev/null 2>&1; then',
|
|
36
|
+
' echo "lease bootstrap: agents-cli install failed (node: $(command -v node || echo missing))" >&2',
|
|
37
|
+
' exit 96',
|
|
38
|
+
'fi',
|
|
39
|
+
// Same first-run guard the hosts bootstrap uses (hosts/ready.ts) — a fresh
|
|
40
|
+
// install refuses `agents run` with "agents-cli is not set up" until setup ran.
|
|
41
|
+
'if [ ! -d "$HOME/.agents/.system" ]; then agents setup >/dev/null 2>&1 || true; fi',
|
|
42
|
+
].join('\n');
|
|
15
43
|
/**
|
|
16
44
|
* Build the single bootstrap script run on the box: ensure agents-cli, install
|
|
17
45
|
* the picked runtime CLIs, write their credentials, run the agent, then shred
|
|
@@ -35,7 +63,7 @@ export function buildBootstrapScript(opts) {
|
|
|
35
63
|
const installRuntimes = opts.runtimes.map((id) => `agents add ${q(id)} >/dev/null 2>&1 || true`).join('\n');
|
|
36
64
|
return [
|
|
37
65
|
'set -uo pipefail',
|
|
38
|
-
|
|
66
|
+
ENSURE_AGENTS_CLI,
|
|
39
67
|
installRuntimes,
|
|
40
68
|
credScript,
|
|
41
69
|
`${runParts.join(' ')}`,
|
package/dist/lib/daemon.js
CHANGED
|
@@ -393,6 +393,45 @@ export async function runDaemon() {
|
|
|
393
393
|
};
|
|
394
394
|
const healInterval = setInterval(() => { void runHealCheck(); }, 6 * 60 * 60_000);
|
|
395
395
|
const healKickoff = setTimeout(() => { void runHealCheck(); }, 30_000);
|
|
396
|
+
// Auto-dispatch: for any managed project that has opted in (autoDispatch:true +
|
|
397
|
+
// maxAgents>0 in ~/.agents/factory/projects.json), pick up Linear tickets that
|
|
398
|
+
// are delegated to an agent and still in Todo, and DISPATCH each through
|
|
399
|
+
// agents-cli's own cloud-provider layer (resolveProvider().dispatch(), same as
|
|
400
|
+
// `agents cloud run`) — then mark it Doing so it isn't re-picked. Capped at
|
|
401
|
+
// maxAgents concurrent per project. No hidden Prix dependency: Rush is one
|
|
402
|
+
// provider among rush/codex/factory, pinned per-project via `provider`. OFF
|
|
403
|
+
// unless a project opts in; no opted-in project or no LINEAR_API_KEY is a clean
|
|
404
|
+
// no-op. Overlap-guarded like the probes above. ~every 3 min.
|
|
405
|
+
let autoDispatching = false;
|
|
406
|
+
const runAutoDispatch = async () => {
|
|
407
|
+
if (autoDispatching)
|
|
408
|
+
return;
|
|
409
|
+
autoDispatching = true;
|
|
410
|
+
try {
|
|
411
|
+
const { readAutoDispatchProjects, isEligible, autoDispatchTick } = await import('./auto-dispatch.js');
|
|
412
|
+
const projects = readAutoDispatchProjects();
|
|
413
|
+
if (!projects.some(isEligible))
|
|
414
|
+
return; // opt-in: nothing enabled → skip
|
|
415
|
+
const { createLinearGateway } = await import('./auto-dispatch-linear.js');
|
|
416
|
+
const linear = createLinearGateway();
|
|
417
|
+
if (!linear)
|
|
418
|
+
return; // no LINEAR_API_KEY configured → skip
|
|
419
|
+
const { createProviderDispatcher } = await import('./auto-dispatch-provider.js');
|
|
420
|
+
const dispatcher = createProviderDispatcher();
|
|
421
|
+
const dispatched = await autoDispatchTick({ projects, linear, dispatcher, log: (lvl, m) => log(lvl, m) });
|
|
422
|
+
if (dispatched.length) {
|
|
423
|
+
log('INFO', `auto-dispatch: started ${dispatched.length} delegated ticket(s): ${dispatched.map((d) => d.identifier).join(', ')}`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
catch (err) {
|
|
427
|
+
log('ERROR', `auto-dispatch failed: ${err.message}`);
|
|
428
|
+
}
|
|
429
|
+
finally {
|
|
430
|
+
autoDispatching = false;
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
const autoDispatchInterval = setInterval(() => { void runAutoDispatch(); }, 3 * 60_000);
|
|
434
|
+
const autoDispatchKickoff = setTimeout(() => { void runAutoDispatch(); }, 45_000);
|
|
396
435
|
// Device probe: refresh registered devices' reachability and detect newly
|
|
397
436
|
// appeared tailnet nodes, dropping a sentinel per pending device so the
|
|
398
437
|
// menu-bar helper can surface "NEW DEVICES → Register / Ignore". Refresh mode
|
|
@@ -500,6 +539,8 @@ export async function runDaemon() {
|
|
|
500
539
|
clearInterval(syncInterval);
|
|
501
540
|
clearInterval(healInterval);
|
|
502
541
|
clearTimeout(healKickoff);
|
|
542
|
+
clearInterval(autoDispatchInterval);
|
|
543
|
+
clearTimeout(autoDispatchKickoff);
|
|
503
544
|
clearInterval(deviceProbeInterval);
|
|
504
545
|
clearTimeout(deviceProbeKickoff);
|
|
505
546
|
clearInterval(tmuxReconcileInterval);
|
package/dist/lib/exec.js
CHANGED
|
@@ -16,6 +16,7 @@ import { resolveModel, buildReasoningFlags } from './models.js';
|
|
|
16
16
|
import { maybeRotate, createTimer, redactPrompt, redactArgs } from './events.js';
|
|
17
17
|
import { sanitizeProcessEnv } from './secrets/bundles.js';
|
|
18
18
|
import { getShimsDir } from './state.js';
|
|
19
|
+
import { readCodexConfiguredModel } from './shims.js';
|
|
19
20
|
import { writePidSessionEntry, extractSessionIdArg } from './session/pid-registry.js';
|
|
20
21
|
import { recordRunName } from './session/run-names.js';
|
|
21
22
|
import { mailboxDir, isValidMailboxId } from './mailbox.js';
|
|
@@ -306,12 +307,12 @@ export const AGENT_COMMANDS = {
|
|
|
306
307
|
promptFlag: 'positional',
|
|
307
308
|
resume: { subcommand: 'resume' },
|
|
308
309
|
modeFlags: {
|
|
309
|
-
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
|
|
313
|
-
edit: ['--sandbox', 'workspace-write', '
|
|
314
|
-
// skip drops the sandbox entirely
|
|
310
|
+
plan: ['--sandbox', 'read-only'],
|
|
311
|
+
// Sandboxed writes inside the workspace. Network stays on (workspace-write
|
|
312
|
+
// disables it by default) so edit-mode runs can still use git/gh/package
|
|
313
|
+
// installs. No approval bypass here — only `skip` drops the guardrails.
|
|
314
|
+
edit: ['--sandbox', 'workspace-write', '-c', 'sandbox_workspace_write.network_access=true'],
|
|
315
|
+
// skip = codex --yolo: drops the sandbox entirely and approves anything.
|
|
315
316
|
skip: ['--dangerously-bypass-approvals-and-sandbox'],
|
|
316
317
|
},
|
|
317
318
|
jsonFlags: ['--json'],
|
|
@@ -562,14 +563,25 @@ export function buildExecCommand(options) {
|
|
|
562
563
|
throw new Error(`Internal error: ${options.agent} declares '${resolvedMode}' in capabilities.modes but has no entry in AGENT_COMMANDS.modeFlags.${resolvedMode}.`);
|
|
563
564
|
}
|
|
564
565
|
if (resumeSpec && 'subcommand' in resumeSpec) {
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
// the bypass or it stalls on approval prompts; plan and interactive resume
|
|
569
|
-
// inherit codex's default sandbox and pass no flag.
|
|
570
|
-
if (!interactive && resolvedMode !== 'plan') {
|
|
566
|
+
if (resolvedMode === 'skip') {
|
|
567
|
+
// skip = yolo on resume too; both `codex resume` (TUI) and
|
|
568
|
+
// `codex exec resume` accept the bypass flag.
|
|
571
569
|
cmd.push('--dangerously-bypass-approvals-and-sandbox');
|
|
572
570
|
}
|
|
571
|
+
else if (interactive) {
|
|
572
|
+
// `codex resume` (TUI) accepts the same -s/--sandbox flags as a fresh run.
|
|
573
|
+
cmd.push(...modeFlags);
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
// `codex exec resume` rejects `--sandbox <mode>` (verified against
|
|
577
|
+
// `codex exec resume --help` on 0.142.5), but takes -c config overrides —
|
|
578
|
+
// map the mode through sandbox_mode so a non-skip resume never gets the
|
|
579
|
+
// approval/sandbox bypass.
|
|
580
|
+
cmd.push('-c', `sandbox_mode=${resolvedMode === 'plan' ? 'read-only' : 'workspace-write'}`);
|
|
581
|
+
if (resolvedMode !== 'plan') {
|
|
582
|
+
cmd.push('-c', 'sandbox_workspace_write.network_access=true');
|
|
583
|
+
}
|
|
584
|
+
}
|
|
573
585
|
}
|
|
574
586
|
else if (options.agent === 'kimi' && !interactive) {
|
|
575
587
|
// kimi's headless prompt mode (`-p`/`--prompt`) is self-contained and REFUSES
|
|
@@ -610,18 +622,25 @@ export function buildExecCommand(options) {
|
|
|
610
622
|
else if (options.sessionId && options.agent === 'claude') {
|
|
611
623
|
cmd.push('--session-id', options.sessionId);
|
|
612
624
|
}
|
|
613
|
-
// Add model
|
|
614
|
-
|
|
625
|
+
// Add model. Prefer the user's explicit --model. Otherwise, for Codex, fall
|
|
626
|
+
// back to the model configured in the user's active ~/.codex/config.toml:
|
|
627
|
+
// Codex runs under a per-version CODEX_HOME (see buildExecEnv) that may not
|
|
628
|
+
// carry that setting, so without this it silently defaults to gpt-5.3-codex,
|
|
629
|
+
// which a ChatGPT-tier account can't use (HTTP 400). Forwarding keeps the
|
|
630
|
+
// user's default model setup for both `agents run` and `agents teams`.
|
|
631
|
+
const effectiveModel = options.model
|
|
632
|
+
?? (options.agent === 'codex' ? readCodexConfiguredModel() : undefined);
|
|
633
|
+
if (effectiveModel && template.modelFlag) {
|
|
615
634
|
const effectiveVersion = options.version || resolveVersion(options.agent, options.cwd || process.cwd());
|
|
616
635
|
if (effectiveVersion) {
|
|
617
|
-
const resolved = resolveModel(options.agent, effectiveVersion,
|
|
636
|
+
const resolved = resolveModel(options.agent, effectiveVersion, effectiveModel);
|
|
618
637
|
if (resolved.warning) {
|
|
619
638
|
process.stderr.write(`[agents] ${resolved.warning}\n`);
|
|
620
639
|
}
|
|
621
640
|
cmd.push(template.modelFlag, resolved.forwarded);
|
|
622
641
|
}
|
|
623
642
|
else {
|
|
624
|
-
cmd.push(template.modelFlag,
|
|
643
|
+
cmd.push(template.modelFlag, effectiveModel);
|
|
625
644
|
}
|
|
626
645
|
}
|
|
627
646
|
// Add JSON output flags if requested
|
|
@@ -651,8 +670,14 @@ export function buildExecCommand(options) {
|
|
|
651
670
|
cmd.push(template.promptFlag, options.prompt);
|
|
652
671
|
}
|
|
653
672
|
}
|
|
654
|
-
// Claude
|
|
655
|
-
|
|
673
|
+
// Extra writable dirs. Claude and Codex both take `--add-dir`; for Codex it
|
|
674
|
+
// widens the workspace-write sandbox (teams relies on this to let teammates
|
|
675
|
+
// write ~/.agents), so it must actually be forwarded — it used to be
|
|
676
|
+
// claude-only, silently dropped for codex and masked by edit mode carrying
|
|
677
|
+
// the approval/sandbox bypass. Codex's resume forms reject --add-dir, so
|
|
678
|
+
// skip it there (claude's flag-based resume accepts it).
|
|
679
|
+
if (options.addDirs &&
|
|
680
|
+
(options.agent === 'claude' || (options.agent === 'codex' && !options.resume))) {
|
|
656
681
|
for (const dir of options.addDirs) {
|
|
657
682
|
cmd.push('--add-dir', dir);
|
|
658
683
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Print `msg` in red to stderr and exit the process with `code`. */
|
|
2
|
+
export declare function die(msg: string, code?: number): never;
|
|
3
|
+
/**
|
|
4
|
+
* Truncate `s` to at most `max` characters, appending a single-char ellipsis
|
|
5
|
+
* (`…`) when shortened. Character-count based (not ANSI/width aware — use
|
|
6
|
+
* `truncateToWidth` from `session/width.ts` for colored strings).
|
|
7
|
+
*/
|
|
8
|
+
export declare function truncate(s: string, max: number): string;
|
|
9
|
+
/**
|
|
10
|
+
* Format an ISO timestamp as a compact relative age: "just now", "5m ago",
|
|
11
|
+
* "3h ago", "2d ago". The canonical short form — the long "5 minutes ago"
|
|
12
|
+
* variant that once lived in `cloud.ts` is deliberately dropped. (For the
|
|
13
|
+
* session-list long form with calendar fallback, see
|
|
14
|
+
* `formatRelativeTime` in `session/relative-time.ts`.)
|
|
15
|
+
*/
|
|
16
|
+
export declare function relTime(iso: string): string;
|
|
17
|
+
/** Format a millisecond duration as "45s", "3m", "2h 5m", "1d 3h". */
|
|
18
|
+
export declare function humanDuration(ms: number): string;
|
|
19
|
+
/**
|
|
20
|
+
* Visible column width of `s`, ignoring ANSI SGR color codes (e.g. chalk
|
|
21
|
+
* wrappers). Matches the full CSI sequence including the `\x1b` escape.
|
|
22
|
+
*/
|
|
23
|
+
export declare function visibleWidth(s: string): number;
|
|
24
|
+
/** Pad `s` with trailing spaces to a target character width. */
|
|
25
|
+
export declare function padRight(s: string, width: number): string;
|
|
26
|
+
/** Pad `s` with trailing spaces to a target *visible* width (ANSI-aware). */
|
|
27
|
+
export declare function padVisible(s: string, width: number): string;
|
|
28
|
+
/** True when `--json` was passed or stdout is not a TTY. */
|
|
29
|
+
export declare function isJsonMode(opts: {
|
|
30
|
+
json?: boolean;
|
|
31
|
+
}): boolean;
|
|
32
|
+
/** Read all of stdin synchronously and return it UTF-8 decoded and trimmed. */
|
|
33
|
+
export declare function readStdinSync(): string;
|
|
34
|
+
/**
|
|
35
|
+
* Wrap `text` in an OSC 8 hyperlink to `filePath` (as a `file://` URL) when
|
|
36
|
+
* stdout is a TTY; otherwise return `text` unchanged.
|
|
37
|
+
*/
|
|
38
|
+
export declare function termLink(text: string, filePath: string): string;
|