@phnx-labs/agents-cli 1.20.50 → 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.
Files changed (82) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/commands/browser-picker.js +1 -18
  3. package/dist/commands/cloud.js +1 -25
  4. package/dist/commands/computer.d.ts +1 -0
  5. package/dist/commands/computer.js +129 -8
  6. package/dist/commands/exec.js +49 -6
  7. package/dist/commands/factory.js +1 -4
  8. package/dist/commands/inspect.js +1 -11
  9. package/dist/commands/mcp.js +2 -6
  10. package/dist/commands/message.js +1 -4
  11. package/dist/commands/profiles.js +1 -18
  12. package/dist/commands/repo.js +33 -14
  13. package/dist/commands/resource-view.d.ts +1 -0
  14. package/dist/commands/resource-view.js +5 -17
  15. package/dist/commands/secrets.d.ts +1 -0
  16. package/dist/commands/secrets.js +1 -28
  17. package/dist/commands/sessions-picker.js +1 -18
  18. package/dist/commands/sessions.js +6 -8
  19. package/dist/commands/teams-picker.js +1 -32
  20. package/dist/commands/teams.js +1 -27
  21. package/dist/commands/tmux.js +1 -3
  22. package/dist/commands/view.js +1 -9
  23. package/dist/commands/worktree.js +1 -4
  24. package/dist/lib/agents.d.ts +0 -4
  25. package/dist/lib/agents.js +20 -33
  26. package/dist/lib/auto-dispatch-linear.d.ts +18 -0
  27. package/dist/lib/auto-dispatch-linear.js +107 -0
  28. package/dist/lib/auto-dispatch-provider.d.ts +10 -0
  29. package/dist/lib/auto-dispatch-provider.js +25 -0
  30. package/dist/lib/auto-dispatch.d.ts +87 -0
  31. package/dist/lib/auto-dispatch.js +142 -0
  32. package/dist/lib/browser/cdp.js +11 -2
  33. package/dist/lib/browser/drivers/ssh.d.ts +28 -10
  34. package/dist/lib/browser/drivers/ssh.js +57 -18
  35. package/dist/lib/browser/refs.js +1 -5
  36. package/dist/lib/cli-resources.d.ts +0 -2
  37. package/dist/lib/cli-resources.js +30 -13
  38. package/dist/lib/cloud/rush.d.ts +0 -24
  39. package/dist/lib/cloud/rush.js +0 -31
  40. package/dist/lib/crabbox/cli.js +4 -1
  41. package/dist/lib/crabbox/lease.js +29 -1
  42. package/dist/lib/daemon.js +41 -0
  43. package/dist/lib/exec.js +31 -14
  44. package/dist/lib/format.d.ts +38 -0
  45. package/dist/lib/format.js +108 -0
  46. package/dist/lib/git.d.ts +21 -0
  47. package/dist/lib/git.js +92 -0
  48. package/dist/lib/hooks/cache.d.ts +9 -2
  49. package/dist/lib/hooks/cache.js +220 -8
  50. package/dist/lib/hooks.js +17 -8
  51. package/dist/lib/platform/exec.d.ts +4 -1
  52. package/dist/lib/platform/exec.js +8 -2
  53. package/dist/lib/resources.d.ts +0 -8
  54. package/dist/lib/resources.js +0 -10
  55. package/dist/lib/runner.js +10 -2
  56. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  57. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  58. package/dist/lib/session/active.d.ts +11 -1
  59. package/dist/lib/session/active.js +3 -0
  60. package/dist/lib/session/db.d.ts +1 -4
  61. package/dist/lib/session/db.js +20 -25
  62. package/dist/lib/session/discover.d.ts +2 -2
  63. package/dist/lib/session/discover.js +61 -48
  64. package/dist/lib/session/parse.js +35 -34
  65. package/dist/lib/session/render.d.ts +7 -3
  66. package/dist/lib/session/render.js +15 -9
  67. package/dist/lib/session/state.d.ts +55 -0
  68. package/dist/lib/session/state.js +87 -10
  69. package/dist/lib/session/types.d.ts +9 -0
  70. package/dist/lib/shims.d.ts +9 -3
  71. package/dist/lib/shims.js +44 -8
  72. package/dist/lib/ssh-tunnel.d.ts +33 -2
  73. package/dist/lib/ssh-tunnel.js +94 -7
  74. package/dist/lib/staleness/types.d.ts +0 -1
  75. package/dist/lib/types.d.ts +14 -1
  76. package/dist/lib/versions.d.ts +0 -26
  77. package/dist/lib/versions.js +2 -145
  78. package/dist/lib/warn-unpushed.d.ts +40 -0
  79. package/dist/lib/warn-unpushed.js +128 -0
  80. package/package.json +3 -1
  81. package/dist/lib/resources/index.d.ts +0 -53
  82. package/dist/lib/resources/index.js +0 -76
package/dist/lib/shims.js CHANGED
@@ -214,7 +214,14 @@ async function promptConflictStrategy(conflictInfos) {
214
214
  // v25 — dispatcher self-recovery: if the baked AGENTS_BIN is gone (a removed/moved
215
215
  // dev build that generated the shim), resolve `agents` on PATH instead of
216
216
  // exiting 127, so a stale/vanished dev build can't brick every launch.
217
- export const SHIM_SCHEMA_VERSION = 25;
217
+ // v26 grok resolves its binary from the versioned home's .grok/downloads first
218
+ // ($VERSION_DIR/home/.grok/downloads, where the binary lands when the
219
+ // installer runs with GROK_HOME set or grok self-updates under the shim),
220
+ // then falls back to the global ~/.grok/downloads for pre-fix installs.
221
+ // The old dispatcher checked only the global dir, so a pinned grok that
222
+ // installed into the versioned home fell through to the "not installed"
223
+ // error.
224
+ export const SHIM_SCHEMA_VERSION = 26;
218
225
  /** Internal marker string used to embed the schema version in shim scripts. */
219
226
  const SHIM_VERSION_MARKER = 'agents-shim-version:';
220
227
  function shellQuote(value) {
@@ -468,11 +475,14 @@ fi
468
475
 
469
476
  VERSION_DIR="$AGENTS_USER_DIR/.history/versions/$AGENT/$VERSION"
470
477
 
471
- # Grok special case: binary lives in ~/.grok/downloads/, not node_modules.
472
- # We still use the agents-cli version dir purely for GROK_HOME isolation.
478
+ # Grok special case: binary lives in the versioned home's .grok/downloads (or,
479
+ # for pre-fix installs, the global ~/.grok/downloads), not node_modules. We
480
+ # still use the agents-cli version dir purely for GROK_HOME isolation.
473
481
  if [ "$AGENT" = "grok" ]; then
474
- # Try to find a matching binary for the pinned version in the global grok downloads dir.
475
- GROK_DOWNLOADS="$HOME/.grok/downloads"
482
+ # Check the versioned home first this is where the binary lands when the
483
+ # installer runs with GROK_HOME set (i.e. via the shim or a correct
484
+ # \`agents add grok\`), or when grok self-updates from within the shim.
485
+ GROK_DOWNLOADS="$VERSION_DIR/home/.grok/downloads"
476
486
  if [ -d "$GROK_DOWNLOADS" ]; then
477
487
  # Prefer a binary whose filename contains the exact version
478
488
  BINARY=$(ls "$GROK_DOWNLOADS"/grok-* 2>/dev/null | grep -i "$VERSION" | head -1)
@@ -481,6 +491,17 @@ if [ "$AGENT" = "grok" ]; then
481
491
  BINARY=$(ls "$GROK_DOWNLOADS"/grok-* 2>/dev/null | head -1)
482
492
  fi
483
493
  fi
494
+ # Fall back to the global grok home (binary installed without GROK_HOME set,
495
+ # e.g. an earlier \`agents add grok@latest\` before this resolution fix).
496
+ if [ -z "$BINARY" ] || [ ! -x "$BINARY" ]; then
497
+ GROK_DOWNLOADS="$HOME/.grok/downloads"
498
+ if [ -d "$GROK_DOWNLOADS" ]; then
499
+ BINARY=$(ls "$GROK_DOWNLOADS"/grok-* 2>/dev/null | grep -i "$VERSION" | head -1)
500
+ if [ -z "$BINARY" ]; then
501
+ BINARY=$(ls "$GROK_DOWNLOADS"/grok-* 2>/dev/null | head -1)
502
+ fi
503
+ fi
504
+ fi
484
505
  if [ -z "$BINARY" ] || [ ! -x "$BINARY" ]; then
485
506
  # Last resort: the adopted native launcher (recorded absolute path) if we
486
507
  # adopted grok, else whatever is on PATH. Prefer the adopted record — after
@@ -744,8 +765,14 @@ export function removeShim(agent) {
744
765
  * editor opens the script and the agent never launches. The `.cmd` is
745
766
  * now the only Windows artifact and carries this alias marker so
746
767
  * staleness checks read it directly.
768
+ * v13 — grok aliases resolve the binary from the versioned home's
769
+ * .grok/downloads first, then fall back to the global ~/.grok/downloads.
770
+ * The old template checked only the global dir, so a `grok@<version>`
771
+ * alias failed with "not installed" whenever the binary was staged into
772
+ * the versioned home (installer run with GROK_HOME set, or grok
773
+ * self-update under the shim).
747
774
  */
748
- export const VERSIONED_ALIAS_SCHEMA_VERSION = 12;
775
+ export const VERSIONED_ALIAS_SCHEMA_VERSION = 13;
749
776
  /** Internal marker string used to embed the schema version in versioned alias scripts. */
750
777
  const VERSIONED_ALIAS_VERSION_MARKER = 'agents-versioned-alias-version:';
751
778
  // The version string is interpolated into a generated bash script and into
@@ -822,13 +849,22 @@ export KIMI_CODE_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/
822
849
  // "agents __shim" which resolves via getBinaryPath() instead.
823
850
  const versionDir = `$HOME/.agents/.history/versions/${agent}/${version}`;
824
851
  const binaryResolution = agent === 'grok'
825
- ? `# Grok ships its native binary in ~/.grok/downloads, not node_modules.
826
- GROK_DOWNLOADS="$HOME/.grok/downloads"
852
+ ? `# Grok ships its native binary in the versioned home's .grok/downloads (or,
853
+ # for pre-fix installs, the global ~/.grok/downloads), not node_modules.
854
+ GROK_DOWNLOADS="${versionDir}/home/.grok/downloads"
827
855
  BINARY=""
828
856
  if [ -d "$GROK_DOWNLOADS" ]; then
829
857
  BINARY=$(ls "$GROK_DOWNLOADS"/grok-* 2>/dev/null | grep -i "${version}" | head -1)
830
858
  [ -n "$BINARY" ] || BINARY=$(ls "$GROK_DOWNLOADS"/grok-* 2>/dev/null | head -1)
831
859
  fi
860
+ # Fall back to the global grok home (binary installed without GROK_HOME set).
861
+ if [ -z "$BINARY" ] || [ ! -x "$BINARY" ]; then
862
+ GROK_GLOBAL_DOWNLOADS="$HOME/.grok/downloads"
863
+ if [ -d "$GROK_GLOBAL_DOWNLOADS" ]; then
864
+ BINARY=$(ls "$GROK_GLOBAL_DOWNLOADS"/grok-* 2>/dev/null | grep -i "${version}" | head -1)
865
+ [ -n "$BINARY" ] || BINARY=$(ls "$GROK_GLOBAL_DOWNLOADS"/grok-* 2>/dev/null | head -1)
866
+ fi
867
+ fi
832
868
  # Refuse a PATH match under our own shims dir — it resolves to this alias's
833
869
  # sibling dispatcher shim (shims dir is ahead of ~/.local/bin on PATH) and
834
870
  # re-execs forever. Fall through to the clean "not installed" error instead.
@@ -50,10 +50,41 @@ export declare const REMOTE_TASK_NAME = "AgentsComputerHelper";
50
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
- * Locate the cross-published Windows daemon exe. Only the local build output is
54
- * a candidate — `scripts/build-win.sh` writes it to packages/.../dist/.
53
+ * Locate a locally built Windows daemon exe a repo-checkout build output from
54
+ * `scripts/build-win.sh`, or one bundled next to the package. Local paths are
55
+ * only the first candidate; npm-installed CLIs have neither and fall through to
56
+ * the release-asset download (`ensureWinHelperExe`).
55
57
  */
56
58
  export declare function resolveWinHelperExe(): string | null;
59
+ /** GitHub repo whose `v<version>` releases carry the exe as an asset. */
60
+ export declare const WIN_HELPER_RELEASE_REPO = "phnx-labs/agents-cli";
61
+ /** Cache dir for downloaded helper exes, one subdir per release tag. */
62
+ export declare function winHelperCacheDir(version: string): string;
63
+ /** Release-asset URLs for the exe + its checksum at one exact `v<version>` tag. */
64
+ export declare function winHelperAssetUrls(version: string): {
65
+ exe: string;
66
+ sha256: string;
67
+ };
68
+ /**
69
+ * Parse the published `.sha256` asset — `sha256sum` format (`<hex> <name>`) or
70
+ * a bare hex digest. Throws on anything that does not lead with 64 hex chars.
71
+ */
72
+ export declare function parseSha256Asset(text: string): string;
73
+ /** Stream a file through sha256 — the exe is ~157MB, never read it whole. */
74
+ export declare function sha256File(file: string): Promise<string>;
75
+ /**
76
+ * Download the exe release asset for this CLI version, verify its sha256
77
+ * against the published `.sha256` asset, and cache it under the agents cache
78
+ * dir. Only the exact `v<version>` tag is consulted — a missing asset is a
79
+ * hard error naming that tag, never a silent fallback to another release.
80
+ */
81
+ export declare function downloadWinHelperExe(version: string): Promise<string>;
82
+ /**
83
+ * Resolve the helper exe for `setup --host`: local build outputs first (repo
84
+ * checkout / bundled), then the checksum-verified release-asset download for
85
+ * the running CLI version. Throws with the tag it checked when neither exists.
86
+ */
87
+ export declare function ensureWinHelperExe(version?: string): Promise<string>;
57
88
  /** Persisted per-device tunnel state so verbs can reconnect after `start --host`. */
58
89
  export interface RemoteTunnelState {
59
90
  device: string;
@@ -21,7 +21,9 @@ import * as net from 'net';
21
21
  import * as fs from 'fs';
22
22
  import * as path from 'path';
23
23
  import { fileURLToPath } from 'url';
24
- import { randomBytes } from 'crypto';
24
+ import { randomBytes, createHash } from 'crypto';
25
+ import { Readable } from 'stream';
26
+ import { pipeline } from 'stream/promises';
25
27
  import { sshExec, SSH_OPTS } from './ssh-exec.js';
26
28
  import { backgroundSpawnOptions } from './platform/process.js';
27
29
  import { encodePowerShell } from './browser/drivers/ssh.js';
@@ -29,6 +31,7 @@ import { getDevice } from './devices/registry.js';
29
31
  import { sshTargetFor } from './devices/connect.js';
30
32
  import { hostNameFor } from './devices/ssh-config.js';
31
33
  import { getCacheDir } from './state.js';
34
+ import { getCliVersion } from './version.js';
32
35
  import { openComputerClient, resolveTcpEndpoint } from './computer-rpc.js';
33
36
  /** Build the ssh argv (after the `ssh` program name) for an `-L` tunnel. Pure.
34
37
  *
@@ -89,8 +92,10 @@ export const REMOTE_TASK_NAME = 'AgentsComputerHelper';
89
92
  /** Basename of the cross-published exe under native/computer-win/dist. */
90
93
  export const WIN_HELPER_EXE = 'computer-helper-win.exe';
91
94
  /**
92
- * Locate the cross-published Windows daemon exe. Only the local build output is
93
- * a candidate — `scripts/build-win.sh` writes it to packages/.../dist/.
95
+ * Locate a locally built Windows daemon exe a repo-checkout build output from
96
+ * `scripts/build-win.sh`, or one bundled next to the package. Local paths are
97
+ * only the first candidate; npm-installed CLIs have neither and fall through to
98
+ * the release-asset download (`ensureWinHelperExe`).
94
99
  */
95
100
  export function resolveWinHelperExe() {
96
101
  const here = path.dirname(fileURLToPath(import.meta.url));
@@ -106,6 +111,89 @@ export function resolveWinHelperExe() {
106
111
  }
107
112
  return null;
108
113
  }
114
+ /** GitHub repo whose `v<version>` releases carry the exe as an asset. */
115
+ export const WIN_HELPER_RELEASE_REPO = 'phnx-labs/agents-cli';
116
+ /** Cache dir for downloaded helper exes, one subdir per release tag. */
117
+ export function winHelperCacheDir(version) {
118
+ return path.join(getCacheDir(), 'computer', 'win-helper', `v${version}`);
119
+ }
120
+ /** Release-asset URLs for the exe + its checksum at one exact `v<version>` tag. */
121
+ export function winHelperAssetUrls(version) {
122
+ const base = `https://github.com/${WIN_HELPER_RELEASE_REPO}/releases/download/v${version}`;
123
+ return { exe: `${base}/${WIN_HELPER_EXE}`, sha256: `${base}/${WIN_HELPER_EXE}.sha256` };
124
+ }
125
+ /**
126
+ * Parse the published `.sha256` asset — `sha256sum` format (`<hex> <name>`) or
127
+ * a bare hex digest. Throws on anything that does not lead with 64 hex chars.
128
+ */
129
+ export function parseSha256Asset(text) {
130
+ const m = text.trim().match(/^([A-Fa-f0-9]{64})(\s|$)/);
131
+ if (!m)
132
+ throw new Error(`malformed .sha256 release asset: ${JSON.stringify(text.slice(0, 80))}`);
133
+ return m[1].toLowerCase();
134
+ }
135
+ /** Stream a file through sha256 — the exe is ~157MB, never read it whole. */
136
+ export function sha256File(file) {
137
+ return new Promise((resolve, reject) => {
138
+ const hash = createHash('sha256');
139
+ fs.createReadStream(file)
140
+ .on('error', reject)
141
+ .on('data', (d) => hash.update(d))
142
+ .on('end', () => resolve(hash.digest('hex')));
143
+ });
144
+ }
145
+ /**
146
+ * Download the exe release asset for this CLI version, verify its sha256
147
+ * against the published `.sha256` asset, and cache it under the agents cache
148
+ * dir. Only the exact `v<version>` tag is consulted — a missing asset is a
149
+ * hard error naming that tag, never a silent fallback to another release.
150
+ */
151
+ export async function downloadWinHelperExe(version) {
152
+ const cached = path.join(winHelperCacheDir(version), WIN_HELPER_EXE);
153
+ if (fs.existsSync(cached))
154
+ return cached;
155
+ const tag = `v${version}`;
156
+ const { exe: exeUrl, sha256: shaUrl } = winHelperAssetUrls(version);
157
+ const missing = (status, url) => new Error(`no ${WIN_HELPER_EXE} release asset for tag ${tag} (HTTP ${status} on ${url}). ` +
158
+ `The Windows helper ships as a GitHub release asset per tagged CLI version; ` +
159
+ `from a repo checkout you can build it locally instead: bash scripts/build-win.sh`);
160
+ // Checksum first: it is tiny and 404s fast when the tag has no assets.
161
+ const shaRes = await fetch(shaUrl, { signal: AbortSignal.timeout(30_000) });
162
+ if (!shaRes.ok)
163
+ throw missing(shaRes.status, shaUrl);
164
+ const expected = parseSha256Asset(await shaRes.text());
165
+ console.error(`Downloading ${WIN_HELPER_EXE} ${tag} from GitHub releases (~160 MB)...`);
166
+ const exeRes = await fetch(exeUrl, { signal: AbortSignal.timeout(15 * 60_000) });
167
+ if (!exeRes.ok || !exeRes.body)
168
+ throw missing(exeRes.status, exeUrl);
169
+ fs.mkdirSync(path.dirname(cached), { recursive: true });
170
+ // Stream to a partial file and rename only after the checksum passes, so an
171
+ // interrupted download can never be picked up as a valid cache hit.
172
+ const partial = `${cached}.download`;
173
+ try {
174
+ await pipeline(Readable.fromWeb(exeRes.body), fs.createWriteStream(partial));
175
+ const actual = await sha256File(partial);
176
+ if (actual !== expected) {
177
+ throw new Error(`sha256 mismatch for ${exeUrl}: expected ${expected}, got ${actual}`);
178
+ }
179
+ fs.renameSync(partial, cached);
180
+ }
181
+ finally {
182
+ fs.rmSync(partial, { force: true });
183
+ }
184
+ return cached;
185
+ }
186
+ /**
187
+ * Resolve the helper exe for `setup --host`: local build outputs first (repo
188
+ * checkout / bundled), then the checksum-verified release-asset download for
189
+ * the running CLI version. Throws with the tag it checked when neither exists.
190
+ */
191
+ export async function ensureWinHelperExe(version = getCliVersion()) {
192
+ const local = resolveWinHelperExe();
193
+ if (local)
194
+ return local;
195
+ return downloadWinHelperExe(version);
196
+ }
109
197
  function remoteStateDir() {
110
198
  return path.join(getCacheDir(), 'computer', 'remote');
111
199
  }
@@ -249,10 +337,9 @@ function copyFileOverScp(target, remotePath, filePath, timeoutMs = 600_000) {
249
337
  */
250
338
  export async function setupRemoteHelper(name) {
251
339
  const { target } = await resolveRemoteDevice(name);
252
- const exe = resolveWinHelperExe();
253
- if (!exe) {
254
- throw new Error(`Windows helper exe not built. Run: bash scripts/build-win.sh`);
255
- }
340
+ // Local build output, else the checksum-verified GitHub release asset for
341
+ // this CLI version. Throws naming the tag it checked when neither exists.
342
+ const exe = await ensureWinHelperExe();
256
343
  const prep = sshExec(target, encodePowerShell(buildPushScript()), { timeoutMs: 60_000 });
257
344
  if (prep.code !== 0) {
258
345
  throw new Error(`preparing helper exe path on '${name}' failed (exit ${prep.code ?? 'null'}): ${prep.stderr.trim() || prep.stdout.trim()}`);
@@ -53,4 +53,3 @@ export interface SyncManifest {
53
53
  workflows?: Record<string, DirEntry>;
54
54
  plugins?: Record<string, PluginEntry>;
55
55
  }
56
- export type ResourceType = 'commands' | 'skills' | 'hooks' | 'mcp' | 'rules' | 'subagents' | 'workflows' | 'plugins' | 'permissions';
@@ -128,6 +128,19 @@ export interface AgentConfig {
128
128
  capabilities: {
129
129
  hooks: Capability;
130
130
  mcp: Capability;
131
+ /**
132
+ * Whether `mcp add --transport http` is supported. Only true for agents
133
+ * whose CLI accepts an HTTP-transport MCP server registration; false for
134
+ * agents that only accept stdio (registerMcp skips HTTP registration with
135
+ * a clear reason).
136
+ */
137
+ mcpHttp: Capability;
138
+ /**
139
+ * Whether HTTP-MCP registration accepts `--header` args. Independent of
140
+ * `mcpHttp`: only Claude's CLI takes headers today; Codex/Gemini accept
141
+ * HTTP MCP but reject header args.
142
+ */
143
+ mcpHeaders: Capability;
131
144
  allowlist: Capability;
132
145
  skills: Capability;
133
146
  commands: Capability;
@@ -164,7 +177,7 @@ export type RulesCapability = false | {
164
177
  file: string;
165
178
  };
166
179
  /** Names of every gateable capability on AgentConfig. */
167
- export type CapabilityName = 'hooks' | 'mcp' | 'allowlist' | 'skills' | 'commands' | 'plugins' | 'subagents' | 'rules' | 'workflows';
180
+ export type CapabilityName = 'hooks' | 'mcp' | 'mcpHttp' | 'mcpHeaders' | 'allowlist' | 'skills' | 'commands' | 'plugins' | 'subagents' | 'rules' | 'workflows';
168
181
  /**
169
182
  * Permission modes controlling agent autonomy.
170
183
  * plan read-only investigation; no writes, no shell side-effects
@@ -379,32 +379,6 @@ export interface SyncResult {
379
379
  plugins: string[];
380
380
  workflows: string[];
381
381
  }
382
- /** Diff between central ~/.agents/ resources and what is synced to a version home. */
383
- export interface ResourceDiff {
384
- commands: {
385
- added: string[];
386
- dangling: string[];
387
- };
388
- skills: {
389
- added: string[];
390
- dangling: string[];
391
- };
392
- hooks: {
393
- added: string[];
394
- dangling: string[];
395
- };
396
- memory: {
397
- added: string[];
398
- dangling: string[];
399
- };
400
- totalAdded: number;
401
- totalDangling: number;
402
- }
403
- /**
404
- * Get the diff between central resources (~/.agents/) and what's synced to a version.
405
- * Uses filesystem state - no tracking needed.
406
- */
407
- export declare function getResourceDiff(agent: AgentId, version: string): ResourceDiff;
408
382
  /**
409
383
  * Enumerate the DotAgent repo names that resources can be scoped to:
410
384
  * the fixed `project` / `user` / `system` layers plus every enabled extra
@@ -21,7 +21,7 @@ import { exec, execFile } from 'child_process';
21
21
  import { promisify } from 'util';
22
22
  import chalk from 'chalk';
23
23
  import { checkbox, select } from '@inquirer/prompts';
24
- import { getVersionsDir, ensureAgentsDir, readMeta, writeMeta, getCommandsDir, getSkillsDir, getHooksDir, getResolvedRulesDir, getUserRulesDir, getVersionResources, ensureVersionResourcePatterns, getProjectAgentsDir, getPromptcutsPath, getUserPromptcutsPath, getEnabledExtraRepos, getAgentsDir, getUserAgentsDir, getTrashVersionsDir, getActiveRulesPreset, getHomeDir } from './state.js';
24
+ import { getVersionsDir, ensureAgentsDir, readMeta, writeMeta, getSkillsDir, getResolvedRulesDir, getUserRulesDir, getVersionResources, ensureVersionResourcePatterns, getProjectAgentsDir, getPromptcutsPath, getUserPromptcutsPath, getEnabledExtraRepos, getAgentsDir, getUserAgentsDir, getTrashVersionsDir, getActiveRulesPreset, getHomeDir } from './state.js';
25
25
  import { defaultPatterns, expandPatterns } from './resource-patterns.js';
26
26
  import { listResources } from './resources.js';
27
27
  // VERSION_RE + compareVersions are owned by the agent-spec engine primitives
@@ -40,7 +40,7 @@ import { discoverPlugins } from './plugins.js';
40
40
  import { loadManifest, saveManifest, buildManifest as buildSyncManifest, isStale } from './staleness/index.js';
41
41
  import { emit } from './events.js';
42
42
  import { safeJoin } from './paths.js';
43
- import { listCommandSkillsInVersion, readSkillSourceCommandMarker, shouldInstallCommandAsSkill } from './command-skills.js';
43
+ import { readSkillSourceCommandMarker, shouldInstallCommandAsSkill } from './command-skills.js';
44
44
  import { getWriter, getDetector } from './staleness/registry.js';
45
45
  /** Promisified exec for running shell commands. */
46
46
  const execAsync = promisify(exec);
@@ -1826,149 +1826,6 @@ async function getCliVersionFromPath(agent) {
1826
1826
  return null;
1827
1827
  }
1828
1828
  }
1829
- /**
1830
- * Get the diff between central resources (~/.agents/) and what's synced to a version.
1831
- * Uses filesystem state - no tracking needed.
1832
- */
1833
- export function getResourceDiff(agent, version) {
1834
- const agentConfig = AGENTS[agent];
1835
- const versionHome = getVersionHomePath(agent, version);
1836
- const agentDir = path.join(versionHome, agentConfigDirName(agent));
1837
- const diff = {
1838
- commands: { added: [], dangling: [] },
1839
- skills: { added: [], dangling: [] },
1840
- hooks: { added: [], dangling: [] },
1841
- memory: { added: [], dangling: [] },
1842
- totalAdded: 0,
1843
- totalDangling: 0,
1844
- };
1845
- // Helper to check symlink status
1846
- const getSymlinkStatus = (linkPath) => {
1847
- try {
1848
- const stat = fs.lstatSync(linkPath);
1849
- if (!stat.isSymbolicLink())
1850
- return 'none';
1851
- // Check if target exists
1852
- try {
1853
- fs.statSync(linkPath);
1854
- return 'valid';
1855
- }
1856
- catch {
1857
- return 'dangling';
1858
- }
1859
- }
1860
- catch {
1861
- return 'none';
1862
- }
1863
- };
1864
- // Commands: check directory symlink (or individual files for Gemini / generated skills for newer Codex)
1865
- const centralCommands = getCommandsDir();
1866
- const commandsTarget = path.join(agentDir, agentConfig.commandsSubdir);
1867
- if (shouldInstallCommandAsSkill(agent, version)) {
1868
- const centralFiles = fs.existsSync(centralCommands)
1869
- ? fs.readdirSync(centralCommands).filter(f => f.endsWith('.md'))
1870
- : [];
1871
- const centralNames = new Set(centralFiles.map(f => f.replace('.md', '')));
1872
- const versionNames = new Set(listCommandSkillsInVersion(agentDir));
1873
- for (const file of centralFiles) {
1874
- const name = file.replace('.md', '');
1875
- if (!versionNames.has(name)) {
1876
- diff.commands.added.push(file);
1877
- }
1878
- }
1879
- for (const name of versionNames) {
1880
- if (!centralNames.has(name)) {
1881
- diff.commands.dangling.push(`${name}.md`);
1882
- }
1883
- }
1884
- }
1885
- else if (agentConfig.format === 'toml') {
1886
- // Gemini: compare .md files in central vs .toml files in version
1887
- if (fs.existsSync(centralCommands)) {
1888
- const centralFiles = fs.readdirSync(centralCommands).filter(f => f.endsWith('.md'));
1889
- const versionFiles = fs.existsSync(commandsTarget)
1890
- ? fs.readdirSync(commandsTarget).filter(f => f.endsWith('.toml'))
1891
- : [];
1892
- const versionNames = new Set(versionFiles.map(f => f.replace('.toml', '')));
1893
- for (const file of centralFiles) {
1894
- const name = file.replace('.md', '');
1895
- if (!versionNames.has(name)) {
1896
- diff.commands.added.push(file);
1897
- }
1898
- }
1899
- // Check for dangling (toml exists but no md source)
1900
- const centralNames = new Set(centralFiles.map(f => f.replace('.md', '')));
1901
- for (const file of versionFiles) {
1902
- const name = file.replace('.toml', '');
1903
- if (!centralNames.has(name)) {
1904
- diff.commands.dangling.push(file);
1905
- }
1906
- }
1907
- }
1908
- }
1909
- else {
1910
- // Other agents: check directory symlink
1911
- const status = getSymlinkStatus(commandsTarget);
1912
- if (status === 'none' && fs.existsSync(centralCommands)) {
1913
- const files = fs.readdirSync(centralCommands).filter(f => f.endsWith('.md'));
1914
- diff.commands.added = files;
1915
- }
1916
- else if (status === 'dangling') {
1917
- diff.commands.dangling = ['commands/'];
1918
- }
1919
- }
1920
- // Skills: check directory symlink (skip if agent natively reads ~/.agents/skills/)
1921
- if (!agentConfig.nativeAgentsSkillsDir) {
1922
- const centralSkills = getSkillsDir();
1923
- const skillsTarget = path.join(agentDir, 'skills');
1924
- const skillsStatus = getSymlinkStatus(skillsTarget);
1925
- if (skillsStatus === 'none' && fs.existsSync(centralSkills)) {
1926
- const dirs = fs.readdirSync(centralSkills).filter(f => {
1927
- const stat = fs.statSync(path.join(centralSkills, f));
1928
- return stat.isDirectory() && !f.startsWith('.');
1929
- });
1930
- diff.skills.added = dirs;
1931
- }
1932
- else if (skillsStatus === 'dangling') {
1933
- diff.skills.dangling = ['skills/'];
1934
- }
1935
- }
1936
- // Hooks: check directory symlink (if agent supports hooks)
1937
- if (agentConfig.supportsHooks) {
1938
- const centralHooks = getHooksDir();
1939
- const hooksTarget = path.join(agentDir, 'hooks');
1940
- const hooksStatus = getSymlinkStatus(hooksTarget);
1941
- if (hooksStatus === 'none' && fs.existsSync(centralHooks)) {
1942
- const files = fs.readdirSync(centralHooks).filter(f => !f.startsWith('.'));
1943
- diff.hooks.added = files;
1944
- }
1945
- else if (hooksStatus === 'dangling') {
1946
- diff.hooks.dangling = ['hooks/'];
1947
- }
1948
- }
1949
- // Rules: check individual file symlinks
1950
- const systemRulesDir = getResolvedRulesDir();
1951
- if (fs.existsSync(systemRulesDir)) {
1952
- const ruleFiles = fs.readdirSync(systemRulesDir).filter(f => f.endsWith('.md') && f !== RULES_DOC_FILENAME);
1953
- for (const file of ruleFiles) {
1954
- const targetName = file === 'AGENTS.md' ? agentConfig.instructionsFile : file;
1955
- const targetPath = path.join(agentDir, targetName);
1956
- const status = getSymlinkStatus(targetPath);
1957
- if (status === 'none') {
1958
- diff.memory.added.push(file);
1959
- }
1960
- else if (status === 'dangling') {
1961
- diff.memory.dangling.push(targetName);
1962
- }
1963
- }
1964
- }
1965
- // Calculate totals
1966
- diff.totalAdded = diff.commands.added.length + diff.skills.added.length +
1967
- diff.hooks.added.length + diff.memory.added.length;
1968
- diff.totalDangling = diff.commands.dangling.length + diff.skills.dangling.length +
1969
- diff.hooks.dangling.length + diff.memory.dangling.length;
1970
- return diff;
1971
- }
1972
1829
  /**
1973
1830
  * Enumerate the DotAgent repo names that resources can be scoped to:
1974
1831
  * the fixed `project` / `user` / `system` layers plus every enabled extra
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Whether a just-finished run should be checked for stranded work: only
3
+ * writable modes can leave commits (plan is read-only), and only non-interactive
4
+ * runs need the warning (an interactive user sees their own shell). Centralizes
5
+ * the gate so every exit path in the run command applies it identically.
6
+ */
7
+ export declare function shouldWarnUnpushed(mode: string, interactive: boolean): boolean;
8
+ export interface UnpushedState {
9
+ /** cwd is inside a git work tree. */
10
+ isRepo: boolean;
11
+ /** current branch, or null when detached / not a repo. */
12
+ branch: string | null;
13
+ /** the branch has an upstream tracking ref configured. */
14
+ hasUpstream: boolean;
15
+ /** commits reachable from HEAD but not from any remote-tracking ref. */
16
+ unpushed: {
17
+ sha: string;
18
+ subject: string;
19
+ }[];
20
+ }
21
+ /**
22
+ * Inspect `cwd` for commits on the current branch that have not reached any
23
+ * remote. Uses `git log --not --remotes` so it is correct even when the branch
24
+ * has no upstream set: commits already present on some `origin/*` ref are NOT
25
+ * reported (no false positive), and a never-pushed branch reports all its
26
+ * commits. Returns an inert result — never throws — for a non-repo cwd, a
27
+ * detached HEAD, or a repo with no remotes (nothing to push to).
28
+ */
29
+ export declare function getUnpushedState(cwd: string): Promise<UnpushedState>;
30
+ /**
31
+ * Render the warning for an unpushed state, or null when there is nothing to
32
+ * warn about. Split out from the printer so it is directly testable.
33
+ */
34
+ export declare function formatUnpushedWarning(state: UnpushedState, cwd: string): string | null;
35
+ /**
36
+ * If the just-finished run left committed-but-unpushed work in `cwd`, print a
37
+ * loud stderr warning with the exact push / PR commands. Non-fatal by contract:
38
+ * any failure is swallowed so it can never break a run's exit path.
39
+ */
40
+ export declare function warnUnpushedWork(cwd: string): Promise<void>;