@phnx-labs/agents-cli 1.20.50 → 1.20.52

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 (124) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/commands/browser-picker.js +1 -18
  3. package/dist/commands/browser.js +215 -7
  4. package/dist/commands/cloud.js +7 -25
  5. package/dist/commands/computer.d.ts +1 -0
  6. package/dist/commands/computer.js +129 -8
  7. package/dist/commands/events.d.ts +1 -1
  8. package/dist/commands/events.js +2 -3
  9. package/dist/commands/exec.js +66 -8
  10. package/dist/commands/factory.js +9 -4
  11. package/dist/commands/feed.d.ts +9 -0
  12. package/dist/commands/feed.js +69 -0
  13. package/dist/commands/inspect.js +1 -11
  14. package/dist/commands/logs.d.ts +5 -1
  15. package/dist/commands/logs.js +248 -3
  16. package/dist/commands/mcp.js +9 -6
  17. package/dist/commands/message.js +1 -4
  18. package/dist/commands/profiles.js +1 -18
  19. package/dist/commands/repo.js +33 -14
  20. package/dist/commands/resource-view.d.ts +1 -0
  21. package/dist/commands/resource-view.js +5 -17
  22. package/dist/commands/secrets.d.ts +23 -0
  23. package/dist/commands/secrets.js +174 -70
  24. package/dist/commands/sessions-picker.js +1 -18
  25. package/dist/commands/sessions.js +6 -8
  26. package/dist/commands/teams-picker.js +1 -32
  27. package/dist/commands/teams.js +5 -27
  28. package/dist/commands/tmux.js +1 -3
  29. package/dist/commands/view.js +1 -9
  30. package/dist/commands/worktree.js +1 -4
  31. package/dist/index.js +6 -2
  32. package/dist/lib/agents.d.ts +0 -4
  33. package/dist/lib/agents.js +20 -33
  34. package/dist/lib/auto-dispatch-linear.d.ts +18 -0
  35. package/dist/lib/auto-dispatch-linear.js +107 -0
  36. package/dist/lib/auto-dispatch-provider.d.ts +10 -0
  37. package/dist/lib/auto-dispatch-provider.js +25 -0
  38. package/dist/lib/auto-dispatch.d.ts +87 -0
  39. package/dist/lib/auto-dispatch.js +142 -0
  40. package/dist/lib/browser/cdp.js +11 -2
  41. package/dist/lib/browser/drivers/ssh.d.ts +28 -10
  42. package/dist/lib/browser/drivers/ssh.js +57 -18
  43. package/dist/lib/browser/login-detection.d.ts +94 -0
  44. package/dist/lib/browser/login-detection.js +274 -0
  45. package/dist/lib/browser/profiles.d.ts +17 -8
  46. package/dist/lib/browser/profiles.js +27 -8
  47. package/dist/lib/browser/refs.js +1 -5
  48. package/dist/lib/browser/secret-ref.d.ts +10 -0
  49. package/dist/lib/browser/secret-ref.js +14 -0
  50. package/dist/lib/browser/service.js +14 -12
  51. package/dist/lib/cli-resources.d.ts +0 -2
  52. package/dist/lib/cli-resources.js +30 -13
  53. package/dist/lib/cloud/rush.d.ts +15 -24
  54. package/dist/lib/cloud/rush.js +7 -32
  55. package/dist/lib/crabbox/cli.js +4 -1
  56. package/dist/lib/crabbox/lease.d.ts +6 -0
  57. package/dist/lib/crabbox/lease.js +40 -10
  58. package/dist/lib/crabbox/runtimes.d.ts +38 -1
  59. package/dist/lib/crabbox/runtimes.js +98 -5
  60. package/dist/lib/daemon.d.ts +12 -9
  61. package/dist/lib/daemon.js +73 -17
  62. package/dist/lib/events.d.ts +31 -5
  63. package/dist/lib/events.js +288 -101
  64. package/dist/lib/exec.js +32 -14
  65. package/dist/lib/feed.d.ts +56 -0
  66. package/dist/lib/feed.js +251 -0
  67. package/dist/lib/format.d.ts +38 -0
  68. package/dist/lib/format.js +108 -0
  69. package/dist/lib/git.d.ts +21 -0
  70. package/dist/lib/git.js +92 -0
  71. package/dist/lib/hooks/cache.d.ts +9 -2
  72. package/dist/lib/hooks/cache.js +220 -8
  73. package/dist/lib/hooks.js +24 -10
  74. package/dist/lib/hosts/passthrough.js +1 -0
  75. package/dist/lib/platform/exec.d.ts +4 -1
  76. package/dist/lib/platform/exec.js +8 -2
  77. package/dist/lib/resources.d.ts +0 -8
  78. package/dist/lib/resources.js +0 -10
  79. package/dist/lib/rotate.js +2 -0
  80. package/dist/lib/runner.js +10 -2
  81. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  82. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  83. package/dist/lib/secrets/agent.d.ts +21 -0
  84. package/dist/lib/secrets/agent.js +63 -1
  85. package/dist/lib/secrets/bundles.d.ts +33 -1
  86. package/dist/lib/secrets/bundles.js +38 -8
  87. package/dist/lib/secrets/icloud-import.d.ts +70 -0
  88. package/dist/lib/secrets/icloud-import.js +173 -0
  89. package/dist/lib/secrets/index.d.ts +36 -0
  90. package/dist/lib/secrets/index.js +99 -9
  91. package/dist/lib/secrets/remote.js +1 -1
  92. package/dist/lib/secrets/sync.js +1 -1
  93. package/dist/lib/session/active.d.ts +11 -1
  94. package/dist/lib/session/active.js +3 -0
  95. package/dist/lib/session/db.d.ts +1 -4
  96. package/dist/lib/session/db.js +20 -25
  97. package/dist/lib/session/discover.d.ts +2 -2
  98. package/dist/lib/session/discover.js +60 -48
  99. package/dist/lib/session/parse.js +35 -34
  100. package/dist/lib/session/render.d.ts +7 -3
  101. package/dist/lib/session/render.js +15 -9
  102. package/dist/lib/session/state.d.ts +55 -0
  103. package/dist/lib/session/state.js +100 -11
  104. package/dist/lib/session/types.d.ts +9 -0
  105. package/dist/lib/shims.d.ts +9 -3
  106. package/dist/lib/shims.js +44 -8
  107. package/dist/lib/ssh-tunnel.d.ts +33 -2
  108. package/dist/lib/ssh-tunnel.js +94 -7
  109. package/dist/lib/staleness/types.d.ts +0 -1
  110. package/dist/lib/startup/command-registry.d.ts +1 -0
  111. package/dist/lib/startup/command-registry.js +2 -0
  112. package/dist/lib/state.d.ts +2 -0
  113. package/dist/lib/state.js +25 -8
  114. package/dist/lib/teams/agents.js +6 -3
  115. package/dist/lib/types.d.ts +24 -1
  116. package/dist/lib/versions.d.ts +0 -26
  117. package/dist/lib/versions.js +2 -145
  118. package/dist/lib/warn-unpushed.d.ts +40 -0
  119. package/dist/lib/warn-unpushed.js +128 -0
  120. package/dist/lib/whats-new.d.ts +5 -3
  121. package/dist/lib/whats-new.js +25 -5
  122. package/package.json +3 -1
  123. package/dist/lib/resources/index.d.ts +0 -53
  124. package/dist/lib/resources/index.js +0 -76
@@ -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
@@ -639,6 +652,16 @@ export interface Meta {
639
652
  * lives separately in ~/.agents/.cache/browser/<profile>/.
640
653
  */
641
654
  browser?: Record<string, BrowserProfileConfig>;
655
+ /**
656
+ * Device-local pointer: the browser profile `agents browser start` resolves to
657
+ * when no `--profile` is passed (and what an explicit `--profile default`
658
+ * re-points to). Stored per-machine in `~/.agents/devices/<machine>/agents.yaml`,
659
+ * NOT central — the target profile may carry machine-local logins, so the choice
660
+ * must not ride `agents repo push/pull` to other machines. Unset = auto-detect an
661
+ * installed Chromium-family browser (legacy behavior). Set via
662
+ * `agents browser profiles set-default <name>`.
663
+ */
664
+ defaultBrowserProfile?: string;
642
665
  /**
643
666
  * Agent-host registry keyed by host name (`agents hosts`). Portable user
644
667
  * config synced with `agents repo push/pull`. For `ssh-config` hosts this is
@@ -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>;
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Post-run guard against silently stranded work.
3
+ *
4
+ * A headless `agents run` in a writable mode can end with the agent having
5
+ * committed on a branch but never pushed it — the CLI's exit path does no git
6
+ * work, so those commits sit invisible in a worktree until someone audits the
7
+ * box. This module detects that state and prints a loud stderr warning with the
8
+ * exact push / PR commands. It is advisory only: it never pushes, never mutates
9
+ * the repo, and never throws (a non-repo cwd or any git error yields an inert
10
+ * result), so it can sit on the run's exit path without ever breaking it.
11
+ */
12
+ import { execFile } from 'child_process';
13
+ import { promisify } from 'util';
14
+ import chalk from 'chalk';
15
+ const execFileAsync = promisify(execFile);
16
+ // Unit-separator byte (0x1f) as the git-log field delimiter: it cannot occur in
17
+ // a commit subject, so splitting on it never truncates a subject with spaces.
18
+ const SEP = '\x1f';
19
+ /**
20
+ * Whether a just-finished run should be checked for stranded work: only
21
+ * writable modes can leave commits (plan is read-only), and only non-interactive
22
+ * runs need the warning (an interactive user sees their own shell). Centralizes
23
+ * the gate so every exit path in the run command applies it identically.
24
+ */
25
+ export function shouldWarnUnpushed(mode, interactive) {
26
+ return mode !== 'plan' && !interactive;
27
+ }
28
+ const INERT = { isRepo: false, branch: null, hasUpstream: false, unpushed: [] };
29
+ async function git(args, cwd) {
30
+ // These are all local, non-prompting reads, but the call sits on a run's exit
31
+ // path — a hard timeout guarantees a wedged git can never delay exit unbounded.
32
+ const { stdout } = await execFileAsync('git', args, { cwd, timeout: 5000 });
33
+ return stdout.trim();
34
+ }
35
+ /**
36
+ * Inspect `cwd` for commits on the current branch that have not reached any
37
+ * remote. Uses `git log --not --remotes` so it is correct even when the branch
38
+ * has no upstream set: commits already present on some `origin/*` ref are NOT
39
+ * reported (no false positive), and a never-pushed branch reports all its
40
+ * commits. Returns an inert result — never throws — for a non-repo cwd, a
41
+ * detached HEAD, or a repo with no remotes (nothing to push to).
42
+ */
43
+ export async function getUnpushedState(cwd) {
44
+ let branch;
45
+ try {
46
+ branch = await git(['rev-parse', '--abbrev-ref', 'HEAD'], cwd);
47
+ }
48
+ catch {
49
+ return INERT; // not a git repo
50
+ }
51
+ // Detached HEAD: agents commit on branches; nothing nameable to push.
52
+ if (!branch || branch === 'HEAD') {
53
+ return { isRepo: true, branch: null, hasUpstream: false, unpushed: [] };
54
+ }
55
+ // No remote configured -> `--remotes` matches nothing and would report the
56
+ // entire history as "unpushed". There's nowhere to push, so stay silent.
57
+ let hasRemote = false;
58
+ try {
59
+ hasRemote = (await git(['remote'], cwd)).length > 0;
60
+ }
61
+ catch {
62
+ hasRemote = false;
63
+ }
64
+ if (!hasRemote) {
65
+ return { isRepo: true, branch, hasUpstream: false, unpushed: [] };
66
+ }
67
+ let unpushed = [];
68
+ try {
69
+ // HEAD must precede `--not`: everything after `--not` is negated, so
70
+ // `--not --remotes HEAD` would negate HEAD too and always return empty.
71
+ const out = await git(['log', 'HEAD', '--not', '--remotes', `--pretty=format:%h${SEP}%s`], cwd);
72
+ unpushed = out
73
+ ? out.split('\n').map((line) => {
74
+ const idx = line.indexOf(SEP);
75
+ return idx === -1
76
+ ? { sha: line, subject: '' }
77
+ : { sha: line.slice(0, idx), subject: line.slice(idx + 1) };
78
+ })
79
+ : [];
80
+ }
81
+ catch {
82
+ return { isRepo: true, branch, hasUpstream: false, unpushed: [] };
83
+ }
84
+ let hasUpstream = false;
85
+ try {
86
+ await git(['rev-parse', '--abbrev-ref', '@{u}'], cwd);
87
+ hasUpstream = true;
88
+ }
89
+ catch {
90
+ hasUpstream = false;
91
+ }
92
+ return { isRepo: true, branch, hasUpstream, unpushed };
93
+ }
94
+ /**
95
+ * Render the warning for an unpushed state, or null when there is nothing to
96
+ * warn about. Split out from the printer so it is directly testable.
97
+ */
98
+ export function formatUnpushedWarning(state, cwd) {
99
+ if (!state.isRepo || !state.branch || state.unpushed.length === 0)
100
+ return null;
101
+ const n = state.unpushed.length;
102
+ const lines = [`\n⚠ agent left ${n} commit${n === 1 ? '' : 's'} on '${state.branch}' not pushed to any remote:`];
103
+ for (const c of state.unpushed.slice(0, 5))
104
+ lines.push(` ${c.sha} ${c.subject}`);
105
+ if (n > 5)
106
+ lines.push(` … and ${n - 5} more`);
107
+ const pushCmd = state.hasUpstream
108
+ ? `git -C "${cwd}" push`
109
+ : `git -C "${cwd}" push -u origin ${state.branch}`;
110
+ lines.push(` push them: ${pushCmd}`);
111
+ lines.push(` open a PR: gh pr create --head ${state.branch}`);
112
+ return lines.join('\n');
113
+ }
114
+ /**
115
+ * If the just-finished run left committed-but-unpushed work in `cwd`, print a
116
+ * loud stderr warning with the exact push / PR commands. Non-fatal by contract:
117
+ * any failure is swallowed so it can never break a run's exit path.
118
+ */
119
+ export async function warnUnpushedWork(cwd) {
120
+ try {
121
+ const warning = formatUnpushedWarning(await getUnpushedState(cwd), cwd);
122
+ if (warning)
123
+ process.stderr.write(chalk.yellow(warning) + '\n');
124
+ }
125
+ catch {
126
+ // Advisory only — never break the run over a warning.
127
+ }
128
+ }
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * Render a compact "What's new" summary from a CHANGELOG.md body: one bullet
3
- * per feature/fix heading (the `**...**` lines) for each version in the range
4
- * the user actually moved through, `(fromVersion, toVersion]`. The verbose
5
- * sub-bullets are intentionally dropped — the full notes live in the changelog.
3
+ * per feature/fix heading for each version in the range the user actually
4
+ * moved through, `(fromVersion, toVersion]`. Headings are recognized in both
5
+ * changelog formats — the current `- **Title.** prose…` single-line bullets
6
+ * and the older standalone `**Heading**` lines. The verbose prose/sub-bullets
7
+ * are intentionally dropped — the full notes live in the changelog.
6
8
  *
7
9
  * Returns colored lines ready to print, empty when nothing is in range.
8
10
  */
@@ -2,15 +2,23 @@ import chalk from 'chalk';
2
2
  import { compareVersions } from './agent-spec/primitives.js';
3
3
  /**
4
4
  * Render a compact "What's new" summary from a CHANGELOG.md body: one bullet
5
- * per feature/fix heading (the `**...**` lines) for each version in the range
6
- * the user actually moved through, `(fromVersion, toVersion]`. The verbose
7
- * sub-bullets are intentionally dropped — the full notes live in the changelog.
5
+ * per feature/fix heading for each version in the range the user actually
6
+ * moved through, `(fromVersion, toVersion]`. Headings are recognized in both
7
+ * changelog formats — the current `- **Title.** prose…` single-line bullets
8
+ * and the older standalone `**Heading**` lines. The verbose prose/sub-bullets
9
+ * are intentionally dropped — the full notes live in the changelog.
8
10
  *
9
11
  * Returns colored lines ready to print, empty when nothing is in range.
10
12
  */
11
13
  export function renderWhatsNew(changelog, fromVersion, toVersion) {
12
14
  const out = [];
13
15
  let inRelevantSection = false;
16
+ // Whether the CURRENT version section uses the old standalone-heading format.
17
+ // Old sections nest `-` sub-bullets under each `**Heading**` line, and some
18
+ // sub-bullets are themselves bold-led (`- **Claim.** detail…`) — once a
19
+ // standalone heading is seen, `- **` lines in that section are sub-bullets,
20
+ // not entries, and must not render.
21
+ let sectionUsesStandaloneHeadings = false;
14
22
  for (const line of changelog.split('\n')) {
15
23
  const versionMatch = line.match(/^## (\d+\.\d+\.\d+)/);
16
24
  if (versionMatch) {
@@ -20,15 +28,27 @@ export function renderWhatsNew(changelog, fromVersion, toVersion) {
20
28
  inRelevantSection =
21
29
  compareVersions(currentVersion, fromVersion) > 0 &&
22
30
  compareVersions(currentVersion, toVersion) <= 0;
31
+ sectionUsesStandaloneHeadings = false;
23
32
  if (inRelevantSection) {
24
33
  out.push('');
25
34
  out.push(chalk.bold(`v${currentVersion}`));
26
35
  }
27
36
  continue;
28
37
  }
29
- // Only the bold headings — one bullet per feature/fix.
30
- if (inRelevantSection && line.startsWith('**') && line.endsWith('**')) {
38
+ // Only the entry headings — one bullet per feature/fix. Two formats exist
39
+ // across the changelog's history: the current single-line bullets
40
+ // (`- **Title.** verbose prose…`, heading kept, prose dropped) and the
41
+ // older standalone `**Heading**` lines with `-` sub-bullets beneath.
42
+ if (!inRelevantSection)
43
+ continue;
44
+ if (line.startsWith('**') && line.endsWith('**')) {
45
+ sectionUsesStandaloneHeadings = true;
31
46
  out.push(` ${chalk.cyan('•')} ${line.replace(/\*\*/g, '')}`);
47
+ continue;
48
+ }
49
+ const entryBullet = sectionUsesStandaloneHeadings ? null : line.match(/^- \*\*(.+?)\*\*/);
50
+ if (entryBullet) {
51
+ out.push(` ${chalk.cyan('•')} ${entryBullet[1].replace(/\*\*/g, '')}`);
32
52
  }
33
53
  }
34
54
  return out;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.20.50",
3
+ "version": "1.20.52",
4
4
  "description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -98,12 +98,14 @@
98
98
  "proper-lockfile": "4.1.2",
99
99
  "simple-git": "3.36.0",
100
100
  "smol-toml": "1.7.0",
101
+ "ws": "^8.21.0",
101
102
  "yaml": "2.9.0"
102
103
  },
103
104
  "devDependencies": {
104
105
  "@types/diff": "8.0.0",
105
106
  "@types/marked-terminal": "6.1.1",
106
107
  "@types/node": "26.1.0",
108
+ "@types/ws": "^8.18.1",
107
109
  "tsx": "4.23.0",
108
110
  "typescript": "6.0.3",
109
111
  "vitest": "4.1.9"
@@ -1,53 +0,0 @@
1
- /**
2
- * Unified resource system - exports all handlers and provides a registry.
3
- *
4
- * Usage:
5
- * import { handlers, getHandler } from './resources/index.js';
6
- * const cmds = handlers.commands.listAll('claude');
7
- */
8
- export * from './types.js';
9
- export { CommandsHandler, commandsHandler, type CommandItem } from './commands.js';
10
- export { HooksHandler, type HookItem } from './hooks.js';
11
- export { SkillsHandler, type SkillItem } from './skills.js';
12
- export { RulesHandler, type RuleItem } from './rules.js';
13
- export { McpHandler, getMcpConfigPath, type McpItem } from './mcp.js';
14
- export { PermissionsHandler, type PermissionItem } from './permissions.js';
15
- export { SubagentsHandler, subagentsHandler, type SubagentItem } from './subagents.js';
16
- export { WorkflowsHandler, type WorkflowItem } from './workflows.js';
17
- import type { ResourceKind, ResourceHandler } from './types.js';
18
- /** All resource handlers keyed by kind. */
19
- export declare const handlers: {
20
- readonly command: import("./commands.js").CommandsHandler;
21
- readonly commands: import("./commands.js").CommandsHandler;
22
- readonly hook: ResourceHandler<import("../types.js").ManifestHook>;
23
- readonly hooks: ResourceHandler<import("../types.js").ManifestHook>;
24
- readonly skill: ResourceHandler<import("./skills.js").SkillItem>;
25
- readonly skills: ResourceHandler<import("./skills.js").SkillItem>;
26
- readonly rule: ResourceHandler<import("./rules.js").RuleItem>;
27
- readonly rules: ResourceHandler<import("./rules.js").RuleItem>;
28
- readonly mcp: ResourceHandler<import("./mcp.js").McpItem>;
29
- readonly permission: ResourceHandler<import("../types.js").PermissionSet>;
30
- readonly permissions: ResourceHandler<import("../types.js").PermissionSet>;
31
- readonly subagent: import("./subagents.js").SubagentsHandler;
32
- readonly subagents: import("./subagents.js").SubagentsHandler;
33
- readonly workflow: {
34
- readonly kind: "workflow";
35
- listAll(_agent: import("./types.js").AgentId, cwd?: string): import("./types.js").ResolvedItem<import("./workflows.js").WorkflowItem>[];
36
- resolve(_agent: import("./types.js").AgentId, name: string, cwd?: string): import("./types.js").ResolvedItem<import("./workflows.js").WorkflowItem> | null;
37
- sync(_agent: import("./types.js").AgentId, _versionHome: string, _cwd?: string): void;
38
- format(_agent: import("./types.js").AgentId): "md";
39
- targetDir(_agent: import("./types.js").AgentId): string;
40
- };
41
- readonly workflows: {
42
- readonly kind: "workflow";
43
- listAll(_agent: import("./types.js").AgentId, cwd?: string): import("./types.js").ResolvedItem<import("./workflows.js").WorkflowItem>[];
44
- resolve(_agent: import("./types.js").AgentId, name: string, cwd?: string): import("./types.js").ResolvedItem<import("./workflows.js").WorkflowItem> | null;
45
- sync(_agent: import("./types.js").AgentId, _versionHome: string, _cwd?: string): void;
46
- format(_agent: import("./types.js").AgentId): "md";
47
- targetDir(_agent: import("./types.js").AgentId): string;
48
- };
49
- };
50
- /** Get a handler by resource kind. */
51
- export declare function getHandler(kind: ResourceKind): ResourceHandler<unknown> | null;
52
- /** All resource kinds. */
53
- export declare const RESOURCE_KINDS: ResourceKind[];
@@ -1,76 +0,0 @@
1
- /**
2
- * Unified resource system - exports all handlers and provides a registry.
3
- *
4
- * Usage:
5
- * import { handlers, getHandler } from './resources/index.js';
6
- * const cmds = handlers.commands.listAll('claude');
7
- */
8
- export * from './types.js';
9
- export { CommandsHandler, commandsHandler } from './commands.js';
10
- export { HooksHandler } from './hooks.js';
11
- export { SkillsHandler } from './skills.js';
12
- export { RulesHandler } from './rules.js';
13
- export { McpHandler, getMcpConfigPath } from './mcp.js';
14
- export { PermissionsHandler } from './permissions.js';
15
- export { SubagentsHandler, subagentsHandler } from './subagents.js';
16
- export { WorkflowsHandler } from './workflows.js';
17
- import { commandsHandler } from './commands.js';
18
- import { HooksHandler } from './hooks.js';
19
- import { SkillsHandler } from './skills.js';
20
- import { RulesHandler } from './rules.js';
21
- import { McpHandler } from './mcp.js';
22
- import { PermissionsHandler } from './permissions.js';
23
- import { subagentsHandler } from './subagents.js';
24
- import { WorkflowsHandler } from './workflows.js';
25
- /** All resource handlers keyed by kind. */
26
- export const handlers = {
27
- command: commandsHandler,
28
- commands: commandsHandler,
29
- hook: HooksHandler,
30
- hooks: HooksHandler,
31
- skill: SkillsHandler,
32
- skills: SkillsHandler,
33
- rule: RulesHandler,
34
- rules: RulesHandler,
35
- mcp: McpHandler,
36
- permission: PermissionsHandler,
37
- permissions: PermissionsHandler,
38
- subagent: subagentsHandler,
39
- subagents: subagentsHandler,
40
- workflow: WorkflowsHandler,
41
- workflows: WorkflowsHandler,
42
- };
43
- /** Get a handler by resource kind. */
44
- export function getHandler(kind) {
45
- switch (kind) {
46
- case 'command':
47
- return commandsHandler;
48
- case 'hook':
49
- return HooksHandler;
50
- case 'skill':
51
- return SkillsHandler;
52
- case 'rule':
53
- return RulesHandler;
54
- case 'mcp':
55
- return McpHandler;
56
- case 'permission':
57
- return PermissionsHandler;
58
- case 'subagent':
59
- return subagentsHandler;
60
- case 'workflow':
61
- return WorkflowsHandler;
62
- default:
63
- return null;
64
- }
65
- }
66
- /** All resource kinds. */
67
- export const RESOURCE_KINDS = [
68
- 'command',
69
- 'hook',
70
- 'skill',
71
- 'rule',
72
- 'mcp',
73
- 'permission',
74
- 'subagent',
75
- 'workflow',
76
- ];