@phnx-labs/agents-cli 1.20.33 → 1.20.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +32 -2
  3. package/dist/commands/computer-actions.d.ts +15 -0
  4. package/dist/commands/computer-actions.js +120 -22
  5. package/dist/commands/computer.d.ts +23 -0
  6. package/dist/commands/computer.js +45 -3
  7. package/dist/commands/cost.js +17 -12
  8. package/dist/commands/doctor.d.ts +11 -0
  9. package/dist/commands/doctor.js +108 -14
  10. package/dist/commands/events.d.ts +16 -0
  11. package/dist/commands/events.js +173 -0
  12. package/dist/commands/exec.js +6 -1
  13. package/dist/commands/hosts.js +28 -24
  14. package/dist/commands/import.js +1 -1
  15. package/dist/commands/inspect.d.ts +2 -0
  16. package/dist/commands/inspect.js +54 -7
  17. package/dist/commands/logs.d.ts +17 -0
  18. package/dist/commands/logs.js +139 -0
  19. package/dist/commands/models.d.ts +3 -0
  20. package/dist/commands/models.js +27 -8
  21. package/dist/commands/permissions.js +9 -2
  22. package/dist/commands/repo.d.ts +34 -0
  23. package/dist/commands/repo.js +243 -65
  24. package/dist/commands/resource-view.d.ts +20 -0
  25. package/dist/commands/resource-view.js +90 -28
  26. package/dist/commands/rules.js +1 -1
  27. package/dist/commands/secrets-migrate.js +23 -11
  28. package/dist/commands/secrets.d.ts +20 -0
  29. package/dist/commands/secrets.js +93 -17
  30. package/dist/commands/sessions-resume.d.ts +2 -0
  31. package/dist/commands/sessions-resume.js +209 -0
  32. package/dist/commands/sessions-tail.d.ts +10 -0
  33. package/dist/commands/sessions-tail.js +11 -0
  34. package/dist/commands/sessions.d.ts +73 -1
  35. package/dist/commands/sessions.js +273 -53
  36. package/dist/commands/status.d.ts +12 -0
  37. package/dist/commands/status.js +81 -0
  38. package/dist/commands/teams.js +70 -6
  39. package/dist/commands/versions.js +2 -1
  40. package/dist/commands/view.d.ts +42 -0
  41. package/dist/commands/view.js +213 -83
  42. package/dist/commands/wallet.d.ts +6 -0
  43. package/dist/commands/wallet.js +22 -5
  44. package/dist/index.js +61 -32
  45. package/dist/lib/acp/harnesses.d.ts +1 -1
  46. package/dist/lib/acp/harnesses.js +2 -2
  47. package/dist/lib/agents.d.ts +12 -0
  48. package/dist/lib/agents.js +145 -41
  49. package/dist/lib/browser/cdp.js +5 -1
  50. package/dist/lib/browser/chrome.js +20 -0
  51. package/dist/lib/browser/drivers/ssh.d.ts +27 -0
  52. package/dist/lib/browser/drivers/ssh.js +94 -19
  53. package/dist/lib/browser/ipc.d.ts +3 -0
  54. package/dist/lib/browser/ipc.js +13 -9
  55. package/dist/lib/browser/service.js +71 -15
  56. package/dist/lib/daemon.d.ts +35 -0
  57. package/dist/lib/daemon.js +33 -5
  58. package/dist/lib/devices/connect.d.ts +3 -2
  59. package/dist/lib/devices/connect.js +5 -3
  60. package/dist/lib/doctor-diff.js +29 -2
  61. package/dist/lib/drift-sync.d.ts +43 -0
  62. package/dist/lib/drift-sync.js +179 -0
  63. package/dist/lib/events.d.ts +9 -2
  64. package/dist/lib/events.js +70 -11
  65. package/dist/lib/exec.d.ts +15 -0
  66. package/dist/lib/exec.js +43 -11
  67. package/dist/lib/hooks.js +8 -2
  68. package/dist/lib/hosts/dispatch.js +6 -7
  69. package/dist/lib/hosts/logs.d.ts +16 -0
  70. package/dist/lib/hosts/logs.js +45 -0
  71. package/dist/lib/hosts/progress.d.ts +66 -0
  72. package/dist/lib/hosts/progress.js +125 -17
  73. package/dist/lib/hosts/ready.d.ts +23 -2
  74. package/dist/lib/hosts/ready.js +35 -13
  75. package/dist/lib/hosts/reconcile.d.ts +53 -0
  76. package/dist/lib/hosts/reconcile.js +81 -0
  77. package/dist/lib/hosts/tasks.d.ts +8 -0
  78. package/dist/lib/hosts/tasks.js +14 -0
  79. package/dist/lib/permissions.d.ts +4 -0
  80. package/dist/lib/permissions.js +35 -0
  81. package/dist/lib/picker.d.ts +24 -0
  82. package/dist/lib/picker.js +124 -0
  83. package/dist/lib/platform/winpath.d.ts +31 -2
  84. package/dist/lib/platform/winpath.js +133 -24
  85. package/dist/lib/plugin-marketplace.d.ts +30 -0
  86. package/dist/lib/plugin-marketplace.js +215 -2
  87. package/dist/lib/plugins.d.ts +5 -0
  88. package/dist/lib/plugins.js +45 -4
  89. package/dist/lib/pwsh.d.ts +11 -0
  90. package/dist/lib/pwsh.js +13 -0
  91. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  92. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  93. package/dist/lib/secrets/agent.d.ts +42 -1
  94. package/dist/lib/secrets/agent.js +89 -11
  95. package/dist/lib/secrets/bundles.js +40 -9
  96. package/dist/lib/secrets/filestore.js +31 -1
  97. package/dist/lib/secrets/index.d.ts +33 -1
  98. package/dist/lib/secrets/index.js +90 -9
  99. package/dist/lib/secrets/windows.d.ts +74 -0
  100. package/dist/lib/secrets/windows.js +440 -0
  101. package/dist/lib/session/active.d.ts +7 -0
  102. package/dist/lib/session/active.js +11 -3
  103. package/dist/lib/session/discover.js +94 -0
  104. package/dist/lib/session/parse.d.ts +6 -0
  105. package/dist/lib/session/parse.js +283 -2
  106. package/dist/lib/session/pid-registry.d.ts +21 -0
  107. package/dist/lib/session/pid-registry.js +80 -0
  108. package/dist/lib/session/remote-active.d.ts +26 -0
  109. package/dist/lib/session/remote-active.js +141 -0
  110. package/dist/lib/session/remote.js +2 -6
  111. package/dist/lib/session/render.js +1 -1
  112. package/dist/lib/session/sync/config.d.ts +8 -0
  113. package/dist/lib/session/sync/config.js +11 -2
  114. package/dist/lib/session/types.d.ts +1 -1
  115. package/dist/lib/session/types.js +1 -1
  116. package/dist/lib/shims.d.ts +30 -2
  117. package/dist/lib/shims.js +107 -50
  118. package/dist/lib/ssh-exec.d.ts +26 -3
  119. package/dist/lib/ssh-exec.js +45 -3
  120. package/dist/lib/ssh-tunnel.d.ts +24 -5
  121. package/dist/lib/ssh-tunnel.js +60 -62
  122. package/dist/lib/staleness/writers/hooks.js +1 -1
  123. package/dist/lib/startup/command-registry.d.ts +3 -0
  124. package/dist/lib/startup/command-registry.js +6 -0
  125. package/dist/lib/sync-status.d.ts +102 -0
  126. package/dist/lib/sync-status.js +135 -0
  127. package/dist/lib/teams/agents.d.ts +24 -0
  128. package/dist/lib/teams/agents.js +30 -1
  129. package/dist/lib/teams/registry.js +25 -9
  130. package/dist/lib/terminal/backends/ghostty.d.ts +6 -0
  131. package/dist/lib/terminal/backends/ghostty.js +69 -0
  132. package/dist/lib/terminal/backends/index.d.ts +18 -0
  133. package/dist/lib/terminal/backends/index.js +29 -0
  134. package/dist/lib/terminal/backends/iterm.d.ts +6 -0
  135. package/dist/lib/terminal/backends/iterm.js +62 -0
  136. package/dist/lib/terminal/backends/tmux.d.ts +14 -0
  137. package/dist/lib/terminal/backends/tmux.js +23 -0
  138. package/dist/lib/terminal/engine.d.ts +39 -0
  139. package/dist/lib/terminal/engine.js +54 -0
  140. package/dist/lib/terminal/index.d.ts +14 -0
  141. package/dist/lib/terminal/index.js +5 -0
  142. package/dist/lib/terminal/policy.d.ts +11 -0
  143. package/dist/lib/terminal/policy.js +11 -0
  144. package/dist/lib/terminal/quote.d.ts +11 -0
  145. package/dist/lib/terminal/quote.js +13 -0
  146. package/dist/lib/terminal/shell.d.ts +6 -0
  147. package/dist/lib/terminal/shell.js +23 -0
  148. package/dist/lib/terminal/transport.d.ts +15 -0
  149. package/dist/lib/terminal/transport.js +40 -0
  150. package/dist/lib/terminal/types.d.ts +59 -0
  151. package/dist/lib/terminal/types.js +13 -0
  152. package/dist/lib/types.d.ts +27 -1
  153. package/dist/lib/usage.d.ts +30 -0
  154. package/dist/lib/usage.js +159 -2
  155. package/dist/lib/versions.d.ts +7 -3
  156. package/dist/lib/versions.js +10 -5
  157. package/dist/lib/whats-new.d.ts +9 -0
  158. package/dist/lib/whats-new.js +35 -0
  159. package/package.json +1 -1
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Ghostty backend — drives Ghostty (>= 1.3) via AppleScript (`osascript`).
3
+ *
4
+ * Ghostty's `new surface configuration` record carries the working directory and
5
+ * command natively, so no `cd` wrapper is needed. Tab: `new tab` (or a window
6
+ * when none is open). Split: `split <surface> direction right|down`, where the
7
+ * current surface is `focused terminal of selected tab of front window`
8
+ * (verified against Ghostty 1.3.1 — a surface is a "terminal" in its AS model).
9
+ */
10
+ import * as fs from 'fs';
11
+ import { appleScriptStr } from '../quote.js';
12
+ import { execOnly, iLoginShell } from '../shell.js';
13
+ const GHOSTTY_APP = '/Applications/Ghostty.app';
14
+ function appExists(p) {
15
+ try {
16
+ return fs.existsSync(p);
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ }
22
+ /** Shared prologue: activate + a surface configuration carrying cwd + command. */
23
+ function configPrologue(cwd, command) {
24
+ const cmd = appleScriptStr(iLoginShell(execOnly(command)));
25
+ return [
26
+ 'tell application "Ghostty"',
27
+ ' activate',
28
+ ' set cfg to new surface configuration',
29
+ ` set initial working directory of cfg to ${appleScriptStr(cwd)}`,
30
+ ` set command of cfg to ${cmd}`,
31
+ ];
32
+ }
33
+ /** AppleScript that opens a Ghostty tab (a window if none is open). */
34
+ export function ghosttyTabScript(cwd, command) {
35
+ return [
36
+ ...configPrologue(cwd, command),
37
+ ' if (count of windows) is 0 then',
38
+ ' new window with configuration cfg',
39
+ ' else',
40
+ ' new tab in front window with configuration cfg',
41
+ ' end if',
42
+ 'end tell',
43
+ ].join('\n');
44
+ }
45
+ /** AppleScript that splits the current Ghostty surface (a window if none is open). */
46
+ export function ghosttySplitScript(cwd, command, direction) {
47
+ return [
48
+ ...configPrologue(cwd, command),
49
+ ' if (count of windows) is 0 then',
50
+ ' new window with configuration cfg',
51
+ ' else',
52
+ ` split (focused terminal of selected tab of front window) direction ${direction} with configuration cfg`,
53
+ ' end if',
54
+ 'end tell',
55
+ ].join('\n');
56
+ }
57
+ export const ghosttyBackend = {
58
+ id: 'ghostty',
59
+ label: 'Ghostty',
60
+ isAvailable(ctx) {
61
+ return ctx.platform === 'darwin' && appExists(GHOSTTY_APP);
62
+ },
63
+ buildTab(cwd, command) {
64
+ return { argv: ['osascript', '-e', ghosttyTabScript(cwd, command)] };
65
+ },
66
+ buildSplit(cwd, command, direction) {
67
+ return { argv: ['osascript', '-e', ghosttySplitScript(cwd, command, direction)] };
68
+ },
69
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Backend registry + current-terminal detection.
3
+ */
4
+ import type { Backend, EngineContext, TerminalBackend } from '../types.js';
5
+ import { itermBackend } from './iterm.js';
6
+ import { ghosttyBackend } from './ghostty.js';
7
+ import { tmuxBackend } from './tmux.js';
8
+ /** All known interactive backends, keyed by id. */
9
+ export declare const BACKENDS: Record<Backend, TerminalBackend>;
10
+ /**
11
+ * The backend for the terminal the CLI is currently running in, or null if we
12
+ * can't open a surface into it. tmux wins (via `$TMUX`) because a tmux pane can
13
+ * live inside iTerm/Ghostty; otherwise fall back to `TERM_PROGRAM`.
14
+ */
15
+ export declare function detectCurrentBackend(ctx: EngineContext): Backend | null;
16
+ /** Backends that can actually be driven in this context. */
17
+ export declare function availableBackends(ctx: EngineContext): TerminalBackend[];
18
+ export { itermBackend, ghosttyBackend, tmuxBackend };
@@ -0,0 +1,29 @@
1
+ import { itermBackend } from './iterm.js';
2
+ import { ghosttyBackend } from './ghostty.js';
3
+ import { tmuxBackend } from './tmux.js';
4
+ /** All known interactive backends, keyed by id. */
5
+ export const BACKENDS = {
6
+ iterm: itermBackend,
7
+ ghostty: ghosttyBackend,
8
+ tmux: tmuxBackend,
9
+ };
10
+ /**
11
+ * The backend for the terminal the CLI is currently running in, or null if we
12
+ * can't open a surface into it. tmux wins (via `$TMUX`) because a tmux pane can
13
+ * live inside iTerm/Ghostty; otherwise fall back to `TERM_PROGRAM`.
14
+ */
15
+ export function detectCurrentBackend(ctx) {
16
+ if (ctx.env.TMUX)
17
+ return 'tmux';
18
+ const term = (ctx.env.TERM_PROGRAM || '').toLowerCase();
19
+ if (term.includes('iterm'))
20
+ return 'iterm';
21
+ if (term.includes('ghostty'))
22
+ return 'ghostty';
23
+ return null;
24
+ }
25
+ /** Backends that can actually be driven in this context. */
26
+ export function availableBackends(ctx) {
27
+ return Object.values(BACKENDS).filter((b) => b.isAvailable(ctx));
28
+ }
29
+ export { itermBackend, ghosttyBackend, tmuxBackend };
@@ -0,0 +1,6 @@
1
+ import type { TerminalBackend, SplitDirection } from '../types.js';
2
+ /** AppleScript that opens an iTerm tab (a window if none is open) running the command. */
3
+ export declare function itermTabScript(cwd: string, command: string[]): string;
4
+ /** AppleScript that splits the current iTerm session (a window if none is open). */
5
+ export declare function itermSplitScript(cwd: string, command: string[], direction: SplitDirection): string;
6
+ export declare const itermBackend: TerminalBackend;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * iTerm backend — drives iTerm2 via AppleScript (`osascript`).
3
+ *
4
+ * Tab: creates a tab in the current window (or a window when none is open).
5
+ * Split: splits the current session — `split vertically` (side-by-side, a
6
+ * vertical divider) for `right`, `split horizontally` (stacked) for `down`.
7
+ */
8
+ import * as fs from 'fs';
9
+ import { appleScriptStr } from '../quote.js';
10
+ import { loginExec, iLoginShell } from '../shell.js';
11
+ const ITERM_APP = '/Applications/iTerm.app';
12
+ function appExists(p) {
13
+ try {
14
+ return fs.existsSync(p);
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ }
20
+ /** AppleScript that opens an iTerm tab (a window if none is open) running the command. */
21
+ export function itermTabScript(cwd, command) {
22
+ const cmd = appleScriptStr(iLoginShell(loginExec(cwd, command)));
23
+ return [
24
+ 'tell application "iTerm2"',
25
+ ' activate',
26
+ ' if (count of windows) is 0 then',
27
+ ` create window with default profile command ${cmd}`,
28
+ ' else',
29
+ ` tell current window to create tab with default profile command ${cmd}`,
30
+ ' end if',
31
+ 'end tell',
32
+ ].join('\n');
33
+ }
34
+ /** AppleScript that splits the current iTerm session (a window if none is open). */
35
+ export function itermSplitScript(cwd, command, direction) {
36
+ const cmd = appleScriptStr(iLoginShell(loginExec(cwd, command)));
37
+ // iTerm: "split vertically" = a vertical divider = panes side by side (right).
38
+ const verb = direction === 'right' ? 'split vertically' : 'split horizontally';
39
+ return [
40
+ 'tell application "iTerm2"',
41
+ ' activate',
42
+ ' if (count of windows) is 0 then',
43
+ ` create window with default profile command ${cmd}`,
44
+ ' else',
45
+ ` tell current session of current window to ${verb} with default profile command ${cmd}`,
46
+ ' end if',
47
+ 'end tell',
48
+ ].join('\n');
49
+ }
50
+ export const itermBackend = {
51
+ id: 'iterm',
52
+ label: 'iTerm',
53
+ isAvailable(ctx) {
54
+ return ctx.platform === 'darwin' && appExists(ITERM_APP);
55
+ },
56
+ buildTab(cwd, command) {
57
+ return { argv: ['osascript', '-e', itermTabScript(cwd, command)] };
58
+ },
59
+ buildSplit(cwd, command, direction) {
60
+ return { argv: ['osascript', '-e', itermSplitScript(cwd, command, direction)] };
61
+ },
62
+ };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * tmux backend — opens windows and splits in the running tmux server.
3
+ *
4
+ * cwd is passed natively via `-c`, so the command is wrapped in `zsh -ilc` with
5
+ * no `cd`. `split-window -h` = side-by-side (right); `-v` = stacked (down).
6
+ * Available only when the CLI is running inside tmux (`$TMUX` set) — otherwise
7
+ * there is no current window/pane to attach to.
8
+ */
9
+ import type { TerminalBackend, SplitDirection } from '../types.js';
10
+ /** argv that opens a new tmux window running the command in cwd. */
11
+ export declare function tmuxTabArgv(cwd: string, command: string[]): string[];
12
+ /** argv that splits the current tmux pane, running the command in cwd. */
13
+ export declare function tmuxSplitArgv(cwd: string, command: string[], direction: SplitDirection): string[];
14
+ export declare const tmuxBackend: TerminalBackend;
@@ -0,0 +1,23 @@
1
+ import { execOnly, iLoginShell } from '../shell.js';
2
+ /** argv that opens a new tmux window running the command in cwd. */
3
+ export function tmuxTabArgv(cwd, command) {
4
+ return ['tmux', 'new-window', '-c', cwd, iLoginShell(execOnly(command))];
5
+ }
6
+ /** argv that splits the current tmux pane, running the command in cwd. */
7
+ export function tmuxSplitArgv(cwd, command, direction) {
8
+ const flag = direction === 'right' ? '-h' : '-v';
9
+ return ['tmux', 'split-window', flag, '-c', cwd, iLoginShell(execOnly(command))];
10
+ }
11
+ export const tmuxBackend = {
12
+ id: 'tmux',
13
+ label: 'tmux',
14
+ isAvailable(ctx) {
15
+ return Boolean(ctx.env.TMUX);
16
+ },
17
+ buildTab(cwd, command) {
18
+ return { argv: tmuxTabArgv(cwd, command) };
19
+ },
20
+ buildSplit(cwd, command, direction) {
21
+ return { argv: tmuxSplitArgv(cwd, command, direction) };
22
+ },
23
+ };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The engine — turns requests into open surfaces.
3
+ *
4
+ * `specForRequest` and `buildRequests` are pure (planning); `openSurface` and
5
+ * `openSurfaces` add the side-effecting transport. A batch runs sequentially and
6
+ * staggered so a `split-right` lands in the tab that was just opened (the split
7
+ * targets the front pane).
8
+ */
9
+ import type { Backend, EngineContext, LaunchRequest, LaunchResult, LaunchSpec } from './types.js';
10
+ import { type Packing } from './policy.js';
11
+ import { type HostResolver } from './transport.js';
12
+ /** The concrete launch command for a request (pure — no side effects). */
13
+ export declare function specForRequest(req: LaunchRequest): LaunchSpec;
14
+ export interface OpenOptions {
15
+ resolveHost?: HostResolver;
16
+ ctx?: EngineContext;
17
+ }
18
+ /** Open a single surface for one request. Never throws — failures come back in the result. */
19
+ export declare function openSurface(req: LaunchRequest, opts?: OpenOptions): Promise<LaunchResult>;
20
+ /** One command to run as a surface. */
21
+ export interface SurfaceItem {
22
+ cwd: string;
23
+ command: string[];
24
+ }
25
+ export interface BuildRequestsOptions {
26
+ backend: Backend;
27
+ host?: string;
28
+ packing?: Packing;
29
+ }
30
+ /** Turn a list of commands into layout-assigned requests (pure — the planning step). */
31
+ export declare function buildRequests(items: SurfaceItem[], opts: BuildRequestsOptions): LaunchRequest[];
32
+ export interface OpenManyOptions extends OpenOptions, BuildRequestsOptions {
33
+ staggerMs?: number;
34
+ }
35
+ /**
36
+ * Open many surfaces, applying the layout policy (default: two-per-tab).
37
+ * Sequential + staggered so each split follows the tab it splits.
38
+ */
39
+ export declare function openSurfaces(items: SurfaceItem[], opts: OpenManyOptions): Promise<LaunchResult[]>;
@@ -0,0 +1,54 @@
1
+ import { BACKENDS } from './backends/index.js';
2
+ import { planLayouts } from './policy.js';
3
+ import { runSpec } from './transport.js';
4
+ const DEFAULT_STAGGER_MS = 400;
5
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
6
+ /** The concrete launch command for a request (pure — no side effects). */
7
+ export function specForRequest(req) {
8
+ const backend = BACKENDS[req.backend];
9
+ if (!backend)
10
+ throw new Error(`unknown backend: ${req.backend}`);
11
+ if (req.layout === 'tab')
12
+ return backend.buildTab(req.cwd, req.command);
13
+ return backend.buildSplit(req.cwd, req.command, req.layout === 'split-down' ? 'down' : 'right');
14
+ }
15
+ /** Open a single surface for one request. Never throws — failures come back in the result. */
16
+ export async function openSurface(req, opts = {}) {
17
+ try {
18
+ // Both can throw: specForRequest on an unknown backend, runSpec when the
19
+ // SSH transport rejects an invalid --host target. Keep them inside the
20
+ // catch so a bad request degrades to a per-surface failure, never a throw.
21
+ const spec = specForRequest(req);
22
+ const res = await runSpec(spec, req.host, opts.resolveHost);
23
+ return { ok: res.ok, request: req, error: res.error };
24
+ }
25
+ catch (err) {
26
+ return { ok: false, request: req, error: err?.message ?? String(err) };
27
+ }
28
+ }
29
+ /** Turn a list of commands into layout-assigned requests (pure — the planning step). */
30
+ export function buildRequests(items, opts) {
31
+ const layouts = planLayouts(items.length, opts.packing ?? 'two-per-tab');
32
+ return items.map((item, i) => ({
33
+ backend: opts.backend,
34
+ layout: layouts[i],
35
+ cwd: item.cwd,
36
+ command: item.command,
37
+ host: opts.host,
38
+ }));
39
+ }
40
+ /**
41
+ * Open many surfaces, applying the layout policy (default: two-per-tab).
42
+ * Sequential + staggered so each split follows the tab it splits.
43
+ */
44
+ export async function openSurfaces(items, opts) {
45
+ const requests = buildRequests(items, opts);
46
+ const stagger = opts.staggerMs ?? DEFAULT_STAGGER_MS;
47
+ const results = [];
48
+ for (let i = 0; i < requests.length; i++) {
49
+ results.push(await openSurface(requests[i], opts));
50
+ if (i < requests.length - 1)
51
+ await sleep(stagger);
52
+ }
53
+ return results;
54
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Terminal launch engine — open an interactive command as a tab or split pane
3
+ * in iTerm / Ghostty / tmux, on this machine or a remote host.
4
+ *
5
+ * Public entry point. Callers typically use `openSurfaces` (a batch with a
6
+ * layout policy) or `openSurface` (a single request), and `availableBackends` /
7
+ * `detectCurrentBackend` to pick a target. See docs/terminal-engine.md.
8
+ */
9
+ export type { Backend, SplitDirection, Layout, EngineContext, LaunchSpec, LaunchRequest, LaunchResult, TerminalBackend, } from './types.js';
10
+ export { currentContext } from './types.js';
11
+ export { BACKENDS, detectCurrentBackend, availableBackends, itermBackend, ghosttyBackend, tmuxBackend } from './backends/index.js';
12
+ export { planLayouts, type Packing } from './policy.js';
13
+ export { specForRequest, buildRequests, openSurface, openSurfaces, type OpenOptions, type OpenManyOptions, type BuildRequestsOptions, type SurfaceItem, } from './engine.js';
14
+ export { runLocal, runRemote, runSpec, remoteCommand, type HostResolver, type RunResult } from './transport.js';
@@ -0,0 +1,5 @@
1
+ export { currentContext } from './types.js';
2
+ export { BACKENDS, detectCurrentBackend, availableBackends, itermBackend, ghosttyBackend, tmuxBackend } from './backends/index.js';
3
+ export { planLayouts } from './policy.js';
4
+ export { specForRequest, buildRequests, openSurface, openSurfaces, } from './engine.js';
5
+ export { runLocal, runRemote, runSpec, remoteCommand } from './transport.js';
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Layout policy — how a batch of surfaces is arranged.
3
+ *
4
+ * `two-per-tab` (default) packs sessions two-up: session 1 opens a new tab,
5
+ * session 2 splits it (right), session 3 opens a new tab, and so on — so each
6
+ * tab holds a left+right pair. `tabs` gives every session its own tab.
7
+ */
8
+ import type { Layout } from './types.js';
9
+ export type Packing = 'two-per-tab' | 'tabs';
10
+ /** Assign a layout to each index in a batch of `count` surfaces. */
11
+ export declare function planLayouts(count: number, packing?: Packing): Layout[];
@@ -0,0 +1,11 @@
1
+ /** Assign a layout to each index in a batch of `count` surfaces. */
2
+ export function planLayouts(count, packing = 'two-per-tab') {
3
+ const out = [];
4
+ for (let i = 0; i < count; i++) {
5
+ if (packing === 'tabs')
6
+ out.push('tab');
7
+ else
8
+ out.push(i % 2 === 0 ? 'tab' : 'split-right');
9
+ }
10
+ return out;
11
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Quoting helpers for building launch commands.
3
+ *
4
+ * `shellQuote` is the single canonical POSIX single-quoter, re-exported from the
5
+ * SSH transport so the local and remote legs quote identically. `appleScriptStr`
6
+ * is the AppleScript string-literal escaper used by the iTerm/Ghostty backends.
7
+ */
8
+ import { shellQuote } from '../ssh-exec.js';
9
+ export { shellQuote };
10
+ /** AppleScript double-quoted string literal (escape backslash, then quote). */
11
+ export declare function appleScriptStr(s: string): string;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Quoting helpers for building launch commands.
3
+ *
4
+ * `shellQuote` is the single canonical POSIX single-quoter, re-exported from the
5
+ * SSH transport so the local and remote legs quote identically. `appleScriptStr`
6
+ * is the AppleScript string-literal escaper used by the iTerm/Ghostty backends.
7
+ */
8
+ import { shellQuote } from '../ssh-exec.js';
9
+ export { shellQuote };
10
+ /** AppleScript double-quoted string literal (escape backslash, then quote). */
11
+ export function appleScriptStr(s) {
12
+ return `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
13
+ }
@@ -0,0 +1,6 @@
1
+ /** `cd <cwd> && exec <command>` — for backends that don't set cwd natively. */
2
+ export declare function loginExec(cwd: string, command: string[]): string;
3
+ /** `exec <command>` — for backends that set the working directory natively. */
4
+ export declare function execOnly(command: string[]): string;
5
+ /** Wrap an inner shell script in an interactive login zsh: `zsh -ilc '<inner>'`. */
6
+ export declare function iLoginShell(inner: string): string;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Interactive-login-shell wrappers shared by every backend.
3
+ *
4
+ * `-i` is load-bearing, not cosmetic: the version-pinned shims (e.g.
5
+ * `claude@2.1.187`) live in `~/.agents/.cache/shims`, which `.zshrc` puts on
6
+ * PATH for *interactive* shells only. A plain `zsh -lc` (login, non-interactive)
7
+ * skips `.zshrc`, so the shim isn't found and the surface dies with "command not
8
+ * found". Every backend wraps its command in `zsh -ilc` for this reason — do not
9
+ * drop the `-i`.
10
+ */
11
+ import { shellQuote } from './quote.js';
12
+ /** `cd <cwd> && exec <command>` — for backends that don't set cwd natively. */
13
+ export function loginExec(cwd, command) {
14
+ return `cd ${shellQuote(cwd)} && exec ${command.join(' ')}`;
15
+ }
16
+ /** `exec <command>` — for backends that set the working directory natively. */
17
+ export function execOnly(command) {
18
+ return `exec ${command.join(' ')}`;
19
+ }
20
+ /** Wrap an inner shell script in an interactive login zsh: `zsh -ilc '<inner>'`. */
21
+ export function iLoginShell(inner) {
22
+ return `zsh -ilc ${shellQuote(inner)}`;
23
+ }
@@ -0,0 +1,15 @@
1
+ import type { LaunchSpec } from './types.js';
2
+ /** Resolve a host alias to an ssh target. Default: identity (ssh_config resolves it). */
3
+ export type HostResolver = (alias: string) => string;
4
+ export interface RunResult {
5
+ ok: boolean;
6
+ error?: string;
7
+ }
8
+ /** Run the spec on this machine: spawn the launcher, resolve when it exits. */
9
+ export declare function runLocal(spec: LaunchSpec): Promise<RunResult>;
10
+ /** Serialize a launch argv into a single POSIX-quoted shell command string. */
11
+ export declare function remoteCommand(spec: LaunchSpec): string;
12
+ /** Run the spec on a remote host over SSH. */
13
+ export declare function runRemote(spec: LaunchSpec, target: string): RunResult;
14
+ /** Run a spec locally (no host / 'local') or on a resolved remote host. */
15
+ export declare function runSpec(spec: LaunchSpec, host?: string, resolveHost?: HostResolver): Promise<RunResult>;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Transport — runs a LaunchSpec locally or on a remote host.
3
+ *
4
+ * Local: spawn the launcher (osascript / tmux) and wait for it to exit — these
5
+ * are short-lived commands that create the surface and return, so waiting gives
6
+ * a real success/failure. Remote: serialize the argv into one shell string and
7
+ * hand it to `sshExec` — the same hardened SSH primitive `agents sessions
8
+ * --host` and the browser driver use (target-injection guard, POSIX quoting,
9
+ * connection multiplexing).
10
+ */
11
+ import { spawn } from 'child_process';
12
+ import { sshExec } from '../ssh-exec.js';
13
+ import { shellQuote } from './quote.js';
14
+ /** Run the spec on this machine: spawn the launcher, resolve when it exits. */
15
+ export function runLocal(spec) {
16
+ return new Promise((resolve) => {
17
+ const child = spawn(spec.argv[0], spec.argv.slice(1), { stdio: 'ignore' });
18
+ child.on('error', (err) => resolve({ ok: false, error: err.message }));
19
+ child.on('close', (code) => resolve(code === 0 ? { ok: true } : { ok: false, error: `${spec.argv[0]} exited with code ${code}` }));
20
+ });
21
+ }
22
+ /** Serialize a launch argv into a single POSIX-quoted shell command string. */
23
+ export function remoteCommand(spec) {
24
+ return spec.argv.map(shellQuote).join(' ');
25
+ }
26
+ /** Run the spec on a remote host over SSH. */
27
+ export function runRemote(spec, target) {
28
+ const res = sshExec(target, remoteCommand(spec), { multiplex: true });
29
+ if (res.code === 0)
30
+ return { ok: true };
31
+ const err = (res.stderr || '').trim();
32
+ return { ok: false, error: err || `ssh exited with code ${res.code}` };
33
+ }
34
+ /** Run a spec locally (no host / 'local') or on a resolved remote host. */
35
+ export async function runSpec(spec, host, resolveHost) {
36
+ if (!host || host === 'local')
37
+ return runLocal(spec);
38
+ const target = resolveHost ? resolveHost(host) : host;
39
+ return runRemote(spec, target);
40
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Core types for the terminal launch engine.
3
+ *
4
+ * The engine opens an *interactive* surface — a tab or a split pane — running a
5
+ * command, in a chosen terminal backend (iTerm / Ghostty / tmux), on this
6
+ * machine or a remote host. This is distinct from cloud providers
7
+ * (src/lib/cloud), which dispatch autonomous headless tasks; a terminal surface
8
+ * is attended and live. See docs/terminal-engine.md.
9
+ */
10
+ /** An interactive terminal backend the engine can drive. */
11
+ export type Backend = 'iterm' | 'ghostty' | 'tmux';
12
+ /** Which way a split pane grows. `right` = side-by-side; `down` = stacked. */
13
+ export type SplitDirection = 'right' | 'down';
14
+ /** Where a surface lands: a new tab, or a split of the current pane. */
15
+ export type Layout = 'tab' | 'split-right' | 'split-down';
16
+ /** Ambient facts a backend needs to decide availability and detection. */
17
+ export interface EngineContext {
18
+ platform: NodeJS.Platform;
19
+ env: NodeJS.ProcessEnv;
20
+ }
21
+ /** Snapshot the live process context. */
22
+ export declare function currentContext(): EngineContext;
23
+ /** The concrete OS command that opens a surface — the pure output of a backend. */
24
+ export interface LaunchSpec {
25
+ /** argv to run (e.g. `['osascript','-e',script]` or `['tmux','split-window',…]`). */
26
+ argv: string[];
27
+ }
28
+ /** A single "open this command as this surface" instruction. */
29
+ export interface LaunchRequest {
30
+ backend: Backend;
31
+ layout: Layout;
32
+ /** Working directory the command runs in. */
33
+ cwd: string;
34
+ /** argv to exec in the surface (e.g. a resume command). */
35
+ command: string[];
36
+ /** undefined / 'local' = this machine; otherwise a resolvable host alias. */
37
+ host?: string;
38
+ }
39
+ /** Outcome of opening one surface. Never throws for launch failures — reports them. */
40
+ export interface LaunchResult {
41
+ ok: boolean;
42
+ request: LaunchRequest;
43
+ error?: string;
44
+ }
45
+ /**
46
+ * A terminal backend: pure builders + an availability check. No side effects —
47
+ * building a spec never opens anything, so every backend is unit-testable
48
+ * without a display. The engine's transport is what actually runs the spec.
49
+ */
50
+ export interface TerminalBackend {
51
+ readonly id: Backend;
52
+ readonly label: string;
53
+ /** Can this backend be driven here? (platform + app installed / inside tmux). */
54
+ isAvailable(ctx: EngineContext): boolean;
55
+ /** Command that opens a new tab running `command` in `cwd`. */
56
+ buildTab(cwd: string, command: string[]): LaunchSpec;
57
+ /** Command that splits the current surface, running `command` in `cwd`. */
58
+ buildSplit(cwd: string, command: string[], direction: SplitDirection): LaunchSpec;
59
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Core types for the terminal launch engine.
3
+ *
4
+ * The engine opens an *interactive* surface — a tab or a split pane — running a
5
+ * command, in a chosen terminal backend (iTerm / Ghostty / tmux), on this
6
+ * machine or a remote host. This is distinct from cloud providers
7
+ * (src/lib/cloud), which dispatch autonomous headless tasks; a terminal surface
8
+ * is attended and live. See docs/terminal-engine.md.
9
+ */
10
+ /** Snapshot the live process context. */
11
+ export function currentContext() {
12
+ return { platform: process.platform, env: process.env };
13
+ }
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import type { CloudProviderId } from './cloud/types.js';
9
9
  /** Unique identifier for a supported AI coding agent. */
10
- export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'copilot' | 'amp' | 'kiro' | 'goose' | 'roo' | 'antigravity' | 'grok' | 'kimi' | 'droid';
10
+ export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'copilot' | 'amp' | 'kiro' | 'goose' | 'antigravity' | 'grok' | 'kimi' | 'droid';
11
11
  /** How `agents run <agent>` chooses an installed version when none is pinned. */
12
12
  export type RunStrategy = 'pinned' | 'available' | 'balanced';
13
13
  /** Per-agent run strategy config. */
@@ -86,6 +86,13 @@ export interface AgentConfig {
86
86
  */
87
87
  nativeCommandRuntime?: boolean;
88
88
  hooksDir: string;
89
+ /**
90
+ * Directory (relative to a plugin's install dir) the agent reads its plugin
91
+ * manifest from, when it differs from the canonical `.claude-plugin/`. Codex
92
+ * uses `.codex-plugin`, Droid `.factory-plugin`. Set to `.` when the agent
93
+ * reads the manifest from the plugin ROOT (Copilot). syncPluginToVersion
94
+ * mirrors `.claude-plugin/plugin.json` into this dir.
95
+ */
89
96
  pluginManifestDir?: string;
90
97
  instructionsFile: string;
91
98
  format: 'markdown' | 'toml';
@@ -99,6 +106,25 @@ export interface AgentConfig {
99
106
  * cloud and falls back to the configured default.
100
107
  */
101
108
  cloudProvider?: CloudProviderId;
109
+ /**
110
+ * Set when the upstream vendor has retired this agent's CLI. Presence marks
111
+ * the agent deprecated (it is never blocked from use); `warnAgentDeprecated`
112
+ * surfaces this in yellow whenever a user installs the agent or adds it to a
113
+ * team. Point `replacement` at the successor agent so the warning can suggest
114
+ * a migration path.
115
+ */
116
+ deprecated?: {
117
+ /** Vendor that retired it, e.g. "Google". */
118
+ by: string;
119
+ /** Human date it stopped working / was retired, e.g. "June 18, 2026". */
120
+ date: string;
121
+ /** One-line explanation shown under the warning header. */
122
+ reason: string;
123
+ /** Successor agent id to suggest instead (e.g. 'antigravity'). */
124
+ replacement?: AgentId;
125
+ /** Announcement URL for the deprecation. */
126
+ url?: string;
127
+ };
102
128
  capabilities: {
103
129
  hooks: Capability;
104
130
  mcp: Capability;
@@ -17,6 +17,7 @@ export interface UsageSnapshot {
17
17
  sourceLabel: string;
18
18
  capturedAt: Date | null;
19
19
  windows: UsageWindow[];
20
+ plan?: string | null;
20
21
  }
21
22
  /** Usage data plus any error encountered while fetching. */
22
23
  export interface UsageInfo {
@@ -122,6 +123,35 @@ export declare function deriveUsageStatusFromSnapshot(snapshot: UsageSnapshot |
122
123
  export declare function formatUsageStatusBadge(usageStatus: 'available' | 'rate_limited' | 'out_of_credits' | null | undefined): string;
123
124
  /** Format a multi-line usage section for detailed agent views. */
124
125
  export declare function formatUsageSection(usage: UsageInfo): string[];
126
+ /** Raw quota bucket from the Kimi /usages response (numbers arrive as strings). */
127
+ interface KimiUsageQuota {
128
+ limit?: string | number | null;
129
+ used?: string | number | null;
130
+ remaining?: string | number | null;
131
+ resetTime?: string | null;
132
+ }
133
+ /** Response shape from the Kimi Code /usages endpoint (subset we render). */
134
+ export interface KimiUsagesResponse {
135
+ user?: {
136
+ userId?: string | null;
137
+ membership?: {
138
+ level?: string | null;
139
+ } | null;
140
+ } | null;
141
+ usage?: KimiUsageQuota | null;
142
+ limits?: Array<{
143
+ window?: {
144
+ duration?: number | null;
145
+ timeUnit?: string | null;
146
+ } | null;
147
+ detail?: KimiUsageQuota | null;
148
+ } | null> | null;
149
+ subType?: string | null;
150
+ }
151
+ /** Normalize the Kimi /usages payload into the common UsageWindow shape. */
152
+ export declare function normalizeKimiWindows(data: KimiUsagesResponse): UsageWindow[];
153
+ /** Derive a display plan label from Kimi's membership tier or subscription type. */
154
+ export declare function formatKimiPlan(data: KimiUsagesResponse): string | null;
125
155
  /** Load Claude OAuth credentials from the system keychain/keyring. */
126
156
  export declare function loadClaudeOauth(home?: string): Promise<ClaudeOauthCredentials | null>;
127
157
  /**