@phnx-labs/agents-cli 1.20.71 → 1.20.73

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 (238) hide show
  1. package/CHANGELOG.md +325 -1
  2. package/README.md +40 -4
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/activity.d.ts +12 -0
  5. package/dist/commands/activity.js +68 -0
  6. package/dist/commands/apply.d.ts +13 -0
  7. package/dist/commands/apply.js +28 -3
  8. package/dist/commands/browser.js +1 -1
  9. package/dist/commands/cloud.js +21 -11
  10. package/dist/commands/commands.js +2 -0
  11. package/dist/commands/doctor.js +8 -4
  12. package/dist/commands/events.d.ts +11 -10
  13. package/dist/commands/events.js +30 -21
  14. package/dist/commands/exec.d.ts +34 -0
  15. package/dist/commands/exec.js +337 -39
  16. package/dist/commands/feed.d.ts +13 -0
  17. package/dist/commands/feed.js +130 -29
  18. package/dist/commands/hq.d.ts +2 -0
  19. package/dist/commands/hq.js +58 -0
  20. package/dist/commands/lease.d.ts +19 -0
  21. package/dist/commands/lease.js +138 -11
  22. package/dist/commands/login.d.ts +2 -0
  23. package/dist/commands/login.js +123 -0
  24. package/dist/commands/logs.js +39 -10
  25. package/dist/commands/mcp.js +2 -0
  26. package/dist/commands/mine.d.ts +27 -0
  27. package/dist/commands/mine.js +207 -0
  28. package/dist/commands/monitors.js +79 -49
  29. package/dist/commands/plugins.js +1 -0
  30. package/dist/commands/profiles.js +158 -0
  31. package/dist/commands/repo.js +2 -60
  32. package/dist/commands/resource-view.d.ts +2 -0
  33. package/dist/commands/resource-view.js +8 -0
  34. package/dist/commands/resources.d.ts +5 -0
  35. package/dist/commands/resources.js +95 -0
  36. package/dist/commands/routines.d.ts +2 -0
  37. package/dist/commands/routines.js +139 -37
  38. package/dist/commands/secrets.js +234 -47
  39. package/dist/commands/send.d.ts +15 -0
  40. package/dist/commands/send.js +62 -0
  41. package/dist/commands/sessions-tail.d.ts +1 -0
  42. package/dist/commands/sessions-tail.js +20 -9
  43. package/dist/commands/sessions.d.ts +6 -0
  44. package/dist/commands/sessions.js +26 -3
  45. package/dist/commands/setup-mine.d.ts +18 -0
  46. package/dist/commands/setup-mine.js +106 -0
  47. package/dist/commands/setup-share.js +2 -2
  48. package/dist/commands/setup.js +3 -1
  49. package/dist/commands/share.d.ts +8 -1
  50. package/dist/commands/share.js +122 -37
  51. package/dist/commands/skills.js +2 -0
  52. package/dist/commands/ssh.d.ts +9 -0
  53. package/dist/commands/ssh.js +196 -26
  54. package/dist/commands/status.js +8 -2
  55. package/dist/commands/subagents.js +3 -1
  56. package/dist/commands/sync.js +1 -1
  57. package/dist/commands/teams.js +11 -2
  58. package/dist/commands/uninstall.d.ts +11 -0
  59. package/dist/commands/uninstall.js +158 -0
  60. package/dist/commands/utils.d.ts +29 -0
  61. package/dist/commands/utils.js +24 -0
  62. package/dist/commands/versions.js +120 -11
  63. package/dist/index.js +83 -12
  64. package/dist/lib/activity.d.ts +124 -0
  65. package/dist/lib/activity.js +542 -0
  66. package/dist/lib/agents.js +16 -5
  67. package/dist/lib/artifact-actions.d.ts +1 -1
  68. package/dist/lib/artifact-actions.js +1 -1
  69. package/dist/lib/ask-classifier.d.ts +2 -1
  70. package/dist/lib/ask-classifier.js +3 -3
  71. package/dist/lib/auth-health.d.ts +32 -4
  72. package/dist/lib/auth-health.js +40 -1
  73. package/dist/lib/auto-pull.js +8 -1
  74. package/dist/lib/brand.d.ts +40 -0
  75. package/dist/lib/brand.js +122 -0
  76. package/dist/lib/browser/drivers/ssh.js +4 -7
  77. package/dist/lib/browser/service.js +6 -8
  78. package/dist/lib/channels/providers/index.d.ts +2 -0
  79. package/dist/lib/channels/providers/index.js +20 -0
  80. package/dist/lib/channels/providers/mailbox.d.ts +2 -0
  81. package/dist/lib/channels/providers/mailbox.js +26 -0
  82. package/dist/lib/channels/providers/openclaw-telegram.d.ts +2 -0
  83. package/dist/lib/channels/providers/openclaw-telegram.js +34 -0
  84. package/dist/lib/channels/providers/rush.d.ts +6 -0
  85. package/dist/lib/channels/providers/rush.js +55 -0
  86. package/dist/lib/channels/registry.d.ts +42 -0
  87. package/dist/lib/channels/registry.js +20 -0
  88. package/dist/lib/channels/resolve.d.ts +11 -0
  89. package/dist/lib/channels/resolve.js +12 -0
  90. package/dist/lib/cli-resources.d.ts +18 -0
  91. package/dist/lib/cli-resources.js +53 -4
  92. package/dist/lib/cloud/codex.js +6 -3
  93. package/dist/lib/cloud/factory.d.ts +1 -0
  94. package/dist/lib/cloud/factory.js +10 -4
  95. package/dist/lib/cloud/rush.d.ts +3 -1
  96. package/dist/lib/cloud/rush.js +6 -1
  97. package/dist/lib/cloud/types.d.ts +2 -0
  98. package/dist/lib/codex-home.d.ts +35 -0
  99. package/dist/lib/codex-home.js +136 -0
  100. package/dist/lib/crabbox/cli.d.ts +44 -0
  101. package/dist/lib/crabbox/cli.js +120 -20
  102. package/dist/lib/crabbox/lease.d.ts +52 -1
  103. package/dist/lib/crabbox/lease.js +117 -16
  104. package/dist/lib/crabbox/progress.d.ts +31 -0
  105. package/dist/lib/crabbox/progress.js +76 -0
  106. package/dist/lib/crabbox/runtimes.d.ts +4 -0
  107. package/dist/lib/crabbox/runtimes.js +33 -8
  108. package/dist/lib/crabbox/setup-copy.d.ts +87 -0
  109. package/dist/lib/crabbox/setup-copy.js +127 -0
  110. package/dist/lib/daemon.d.ts +14 -5
  111. package/dist/lib/daemon.js +52 -9
  112. package/dist/lib/drive-sync.js +6 -3
  113. package/dist/lib/event-stream.d.ts +36 -0
  114. package/dist/lib/event-stream.js +68 -0
  115. package/dist/lib/events.d.ts +1 -1
  116. package/dist/lib/exec.d.ts +11 -0
  117. package/dist/lib/exec.js +23 -2
  118. package/dist/lib/feed-policy.d.ts +1 -1
  119. package/dist/lib/feed-policy.js +14 -9
  120. package/dist/lib/feed-ranking.d.ts +32 -0
  121. package/dist/lib/feed-ranking.js +224 -0
  122. package/dist/lib/feed.d.ts +35 -2
  123. package/dist/lib/feed.js +66 -3
  124. package/dist/lib/fleet/auth-sync.d.ts +53 -0
  125. package/dist/lib/fleet/auth-sync.js +92 -0
  126. package/dist/lib/fleet/remote-login.d.ts +187 -0
  127. package/dist/lib/fleet/remote-login.js +556 -0
  128. package/dist/lib/format.d.ts +30 -3
  129. package/dist/lib/format.js +34 -5
  130. package/dist/lib/fs-atomic.d.ts +7 -2
  131. package/dist/lib/fs-atomic.js +8 -12
  132. package/dist/lib/git.d.ts +16 -0
  133. package/dist/lib/git.js +79 -2
  134. package/dist/lib/heal.js +11 -3
  135. package/dist/lib/hosts/logs.d.ts +12 -0
  136. package/dist/lib/hosts/logs.js +18 -0
  137. package/dist/lib/hosts/progress.d.ts +28 -10
  138. package/dist/lib/hosts/progress.js +79 -22
  139. package/dist/lib/hosts/remote-cmd.js +4 -0
  140. package/dist/lib/hq/floor.d.ts +87 -0
  141. package/dist/lib/hq/floor.js +226 -0
  142. package/dist/lib/menubar/install-menubar.js +14 -20
  143. package/dist/lib/notify.d.ts +1 -0
  144. package/dist/lib/notify.js +3 -3
  145. package/dist/lib/open-url.d.ts +2 -0
  146. package/dist/lib/open-url.js +19 -0
  147. package/dist/lib/openclaw-keychain.d.ts +56 -0
  148. package/dist/lib/openclaw-keychain.js +236 -0
  149. package/dist/lib/overdue.js +10 -0
  150. package/dist/lib/permissions.d.ts +44 -1
  151. package/dist/lib/permissions.js +284 -7
  152. package/dist/lib/project-launch.d.ts +6 -12
  153. package/dist/lib/project-launch.js +13 -228
  154. package/dist/lib/project-resources.d.ts +7 -0
  155. package/dist/lib/project-resources.js +291 -0
  156. package/dist/lib/pty-client.d.ts +27 -0
  157. package/dist/lib/pty-client.js +136 -10
  158. package/dist/lib/refresh.js +14 -10
  159. package/dist/lib/resource-profiles.d.ts +26 -0
  160. package/dist/lib/resource-profiles.js +157 -0
  161. package/dist/lib/resources/permissions.js +7 -1
  162. package/dist/lib/resources/types.d.ts +1 -1
  163. package/dist/lib/resources.d.ts +1 -1
  164. package/dist/lib/resources.js +32 -3
  165. package/dist/lib/routines.d.ts +11 -0
  166. package/dist/lib/routines.js +56 -15
  167. package/dist/lib/runner.d.ts +1 -0
  168. package/dist/lib/runner.js +66 -5
  169. package/dist/lib/secrets/bundles.d.ts +16 -3
  170. package/dist/lib/secrets/bundles.js +206 -37
  171. package/dist/lib/secrets/icloud-import.d.ts +2 -2
  172. package/dist/lib/secrets/icloud-import.js +9 -6
  173. package/dist/lib/secrets/mcp.js +1 -1
  174. package/dist/lib/secrets/vault-age-helper.d.ts +1 -0
  175. package/dist/lib/secrets/vault-age-helper.js +34 -0
  176. package/dist/lib/secrets/vault.d.ts +49 -0
  177. package/dist/lib/secrets/vault.js +397 -0
  178. package/dist/lib/self-heal/checks/path.js +3 -1
  179. package/dist/lib/self-heal/checks/shadowing.js +10 -2
  180. package/dist/lib/self-heal/checks/shims.js +19 -11
  181. package/dist/lib/session/cloud.d.ts +2 -2
  182. package/dist/lib/session/cloud.js +2 -2
  183. package/dist/lib/session/db.d.ts +4 -0
  184. package/dist/lib/session/db.js +44 -7
  185. package/dist/lib/session/discover.d.ts +2 -0
  186. package/dist/lib/session/discover.js +114 -3
  187. package/dist/lib/session/stream-render.d.ts +3 -0
  188. package/dist/lib/session/stream-render.js +130 -0
  189. package/dist/lib/session/types.d.ts +6 -0
  190. package/dist/lib/share/analytics.d.ts +13 -0
  191. package/dist/lib/share/analytics.js +45 -0
  192. package/dist/lib/share/config.d.ts +19 -5
  193. package/dist/lib/share/config.js +44 -8
  194. package/dist/lib/share/provision.d.ts +58 -6
  195. package/dist/lib/share/provision.js +97 -22
  196. package/dist/lib/share/publish.d.ts +36 -13
  197. package/dist/lib/share/publish.js +55 -8
  198. package/dist/lib/share/worker-template.js +83 -17
  199. package/dist/lib/shims.d.ts +36 -0
  200. package/dist/lib/shims.js +120 -16
  201. package/dist/lib/ssh-exec.d.ts +14 -0
  202. package/dist/lib/ssh-exec.js +57 -0
  203. package/dist/lib/staleness/detectors/hooks.js +25 -1
  204. package/dist/lib/staleness/detectors/permissions.js +66 -0
  205. package/dist/lib/staleness/detectors/workflows.js +20 -0
  206. package/dist/lib/staleness/writers/hooks.js +58 -4
  207. package/dist/lib/staleness/writers/permissions.js +2 -2
  208. package/dist/lib/staleness/writers/skills.js +1 -1
  209. package/dist/lib/staleness/writers/sources.d.ts +10 -1
  210. package/dist/lib/staleness/writers/sources.js +68 -1
  211. package/dist/lib/star-nudge.d.ts +45 -0
  212. package/dist/lib/star-nudge.js +91 -0
  213. package/dist/lib/startup/command-registry.d.ts +7 -1
  214. package/dist/lib/startup/command-registry.js +19 -3
  215. package/dist/lib/state.d.ts +2 -0
  216. package/dist/lib/state.js +3 -0
  217. package/dist/lib/subagents-registry.d.ts +2 -0
  218. package/dist/lib/subagents-registry.js +3 -0
  219. package/dist/lib/teams/parsers.js +214 -6
  220. package/dist/lib/teams/supervisor.d.ts +7 -0
  221. package/dist/lib/teams/supervisor.js +2 -1
  222. package/dist/lib/template.d.ts +1 -1
  223. package/dist/lib/template.js +1 -1
  224. package/dist/lib/triggers/webhook.js +36 -3
  225. package/dist/lib/types.d.ts +73 -0
  226. package/dist/lib/uninstall.d.ts +84 -0
  227. package/dist/lib/uninstall.js +347 -0
  228. package/dist/lib/usage.d.ts +13 -1
  229. package/dist/lib/usage.js +32 -14
  230. package/dist/lib/version.d.ts +40 -0
  231. package/dist/lib/version.js +94 -16
  232. package/dist/lib/versions.d.ts +27 -0
  233. package/dist/lib/versions.js +248 -79
  234. package/dist/lib/workflows.d.ts +2 -0
  235. package/dist/lib/workflows.js +88 -0
  236. package/package.json +2 -1
  237. package/dist/commands/daemon.d.ts +0 -10
  238. package/dist/commands/daemon.js +0 -121
@@ -77,18 +77,46 @@ function linearTeamKey(payload) {
77
77
  return typeof team?.key === 'string' ? team.key : null;
78
78
  }
79
79
  function linearLabels(payload) {
80
+ // Linear webhook bodies flatten list relations: an Issue event carries
81
+ // `data.labels` as a flat array of label objects (`[{ id, name, color }]`),
82
+ // NOT the `{ nodes: [...] }` connection shape returned by the GraphQL API.
83
+ // Reading `.nodes` here made every `--label` filter match nothing.
80
84
  const data = payload.data;
81
- const labels = data?.labels;
82
- const nodes = Array.isArray(labels?.nodes) ? labels.nodes : [];
83
- return nodes
85
+ const labels = Array.isArray(data?.labels) ? data?.labels : [];
86
+ return labels
84
87
  .map((n) => n.name)
85
88
  .filter((n) => typeof n === 'string' && n.length > 0);
86
89
  }
90
+ function githubAction(payload) {
91
+ return typeof payload.action === 'string' ? payload.action : null;
92
+ }
93
+ function githubLabels(payload) {
94
+ const names = new Set();
95
+ const add = (value) => {
96
+ if (typeof value === 'string' && value.length > 0)
97
+ names.add(value);
98
+ };
99
+ const deliveryLabel = payload.label;
100
+ add(deliveryLabel?.name);
101
+ const pr = payload.pull_request;
102
+ const prLabels = Array.isArray(pr?.labels) ? pr.labels : [];
103
+ for (const label of prLabels) {
104
+ add(label.name);
105
+ }
106
+ const issue = payload.issue;
107
+ const issueLabels = Array.isArray(issue?.labels) ? issue.labels : [];
108
+ for (const label of issueLabels) {
109
+ add(label.name);
110
+ }
111
+ return [...names];
112
+ }
87
113
  function githubTriggerMatches(trigger, webhook) {
88
114
  if (webhook.source !== 'github')
89
115
  return false;
90
116
  if (trigger.event !== webhook.event)
91
117
  return false;
118
+ if (trigger.action && githubAction(webhook.payload) !== trigger.action)
119
+ return false;
92
120
  if (trigger.repo) {
93
121
  const repo = webhookRepo(webhook.payload);
94
122
  if (!repo || repo.toLowerCase() !== trigger.repo.toLowerCase())
@@ -99,6 +127,11 @@ function githubTriggerMatches(trigger, webhook) {
99
127
  if (!branches.some((b) => b === trigger.branch))
100
128
  return false;
101
129
  }
130
+ if (trigger.label) {
131
+ const expected = trigger.label.toLowerCase();
132
+ if (!githubLabels(webhook.payload).some((name) => name.toLowerCase() === expected))
133
+ return false;
134
+ }
102
135
  return true;
103
136
  }
104
137
  function linearTriggerMatches(trigger, webhook) {
@@ -508,6 +508,29 @@ export interface VersionResources {
508
508
  permissions?: ResourcePattern[];
509
509
  mcp?: ResourcePattern[];
510
510
  }
511
+ /** Resource-kind selectors controlled by top-level resource profiles. */
512
+ export interface ResourceProfilePreset {
513
+ description?: string;
514
+ commands?: ResourcePattern[];
515
+ skills?: ResourcePattern[];
516
+ hooks?: ResourcePattern[];
517
+ subagents?: ResourcePattern[];
518
+ plugins?: ResourcePattern[];
519
+ workflows?: ResourcePattern[];
520
+ permissions?: ResourcePattern[];
521
+ mcp?: ResourcePattern[];
522
+ /** Rule preset name to compose while this profile is active. */
523
+ rules?: string;
524
+ /** Alias for rules, accepted so YAML can mirror VersionResources naming. */
525
+ rulesPreset?: string;
526
+ /** Secrets bundle names, or "*" for every bundle. */
527
+ secrets?: string[];
528
+ }
529
+ /** Top-level profiles/presets that switch the resolved resource view. */
530
+ export interface ResourceProfilesConfig {
531
+ active?: string;
532
+ presets?: Record<string, ResourceProfilePreset>;
533
+ }
511
534
  /** A userConfig field declared in a plugin manifest. */
512
535
  export interface PluginUserConfigField {
513
536
  key: string;
@@ -644,6 +667,28 @@ export interface ExtraRepoConfig {
644
667
  path?: string;
645
668
  enabled: boolean;
646
669
  }
670
+ /**
671
+ * A white-label brand — a personally-named CLI (e.g. `jack`) that IS agents-cli,
672
+ * minted by `agents setup mine` / `agents mine`. The brand's shim exports
673
+ * `AGENTS_BRAND=<name>`; the entrypoint reads it to present under this name and
674
+ * apply the customization below. Portable user config — rides `agents repo
675
+ * push/pull`. See lib/brand.ts.
676
+ */
677
+ export interface BrandConfig {
678
+ /** The brand name; also the binary name on PATH. */
679
+ name: string;
680
+ /** Built-in top-level commands this brand hides/disables (e.g. `["teams"]`). */
681
+ disabledCommands?: string[];
682
+ /**
683
+ * Resource-profile preset this brand pins (a key in `profiles.presets`). When
684
+ * the CLI runs under this brand, that preset becomes the active profile, so
685
+ * skills/plugins/mcp/hooks/etc. filter to the brand's curated set. Defaults to
686
+ * `mine-<name>`.
687
+ */
688
+ profile?: string;
689
+ /** False to keep the config but stop minting/using the brand. */
690
+ enabled: boolean;
691
+ }
647
692
  /** Top-level structure of ~/.agents/.system/agents.yaml -- the CLI's persistent state. */
648
693
  export interface Meta {
649
694
  agents?: Partial<Record<AgentId, string>>;
@@ -682,6 +727,12 @@ export interface Meta {
682
727
  enabled?: BetaFeatureName[];
683
728
  };
684
729
  registries?: Record<RegistryType, Record<string, RegistryConfig>>;
730
+ /**
731
+ * Top-level resource profiles. Activating one filters the resolved resource
732
+ * set across commands, skills, hooks, rules, MCP, permissions, and secrets.
733
+ * Model-provider run profiles are separate YAML files under profiles/.
734
+ */
735
+ profiles?: ResourceProfilesConfig;
685
736
  versions?: Partial<Record<AgentId, Record<string, VersionResources>>>;
686
737
  source?: string;
687
738
  /**
@@ -696,6 +747,12 @@ export interface Meta {
696
747
  * via the `path` field.
697
748
  */
698
749
  extraRepos?: Record<string, ExtraRepoConfig>;
750
+ /**
751
+ * White-label brands keyed by name. Each mints a personally-named binary
752
+ * (e.g. `jack`) that runs agents-cli under that name with its own disabled
753
+ * commands + curated resource profile. See lib/brand.ts.
754
+ */
755
+ brands?: Record<string, BrandConfig>;
699
756
  /**
700
757
  * Keys like `skill.hermes` — registries seeded from SEEDED_REGISTRIES exactly
701
758
  * once. Tracked so a user `registry remove` won't silently re-seed.
@@ -746,6 +803,22 @@ export interface Meta {
746
803
  workerName?: string;
747
804
  bucketName?: string;
748
805
  domain?: string;
806
+ /** Cloudflare Web Analytics token injected into published HTML pages. */
807
+ analyticsToken?: string;
808
+ };
809
+ /**
810
+ * Owner/channel notification config for `agents send` / `agents notify`.
811
+ * `owner` is the default recipient for `agents notify` (channel + target).
812
+ * `transports` maps a user-facing channel name to the provider that actually
813
+ * delivers it — explicit, one provider per channel, no fallback. Omitted keys
814
+ * default to name-identity (channel `slack` -> provider `slack`).
815
+ */
816
+ notify?: {
817
+ owner?: {
818
+ channel: string;
819
+ to: string;
820
+ };
821
+ transports?: Record<string, string>;
749
822
  };
750
823
  }
751
824
  /** Persisted agent-host entry in agents.yaml (overlay or inline). */
@@ -0,0 +1,84 @@
1
+ import type { AgentId } from './types.js';
2
+ /** What the uninstall intends to do with one agent's config directory. */
3
+ export type ConfigAction = {
4
+ agent: AgentId;
5
+ realPath: string;
6
+ kind: 'restore-backup';
7
+ source: string;
8
+ } | {
9
+ agent: AgentId;
10
+ realPath: string;
11
+ kind: 'restore-version-home';
12
+ source: string;
13
+ } | {
14
+ agent: AgentId;
15
+ realPath: string;
16
+ kind: 'remove-dangling';
17
+ } | {
18
+ agent: AgentId;
19
+ realPath: string;
20
+ kind: 'leave-real';
21
+ } | {
22
+ agent: AgentId;
23
+ realPath: string;
24
+ kind: 'leave-foreign';
25
+ } | {
26
+ agent: AgentId;
27
+ realPath: string;
28
+ kind: 'absent';
29
+ };
30
+ /** A home-level file symlink (e.g. `~/.claude.json`) agents-cli owns. */
31
+ export interface HomeFileAction {
32
+ realPath: string;
33
+ /** Resolved symlink target whose contents are copied back to `realPath`. */
34
+ source: string;
35
+ }
36
+ /** The full, read-only plan describing what an uninstall would change. */
37
+ export interface UninstallPlan {
38
+ isInstalled: boolean;
39
+ agentsDir: string;
40
+ legacySymlink: string | null;
41
+ configs: ConfigAction[];
42
+ homeFiles: HomeFileAction[];
43
+ launchers: string[];
44
+ rcFiles: string[];
45
+ }
46
+ /** Structured result of an executed uninstall (for reporting). */
47
+ export interface UninstallResult {
48
+ restoredConfigs: Array<{
49
+ agent: AgentId;
50
+ realPath: string;
51
+ }>;
52
+ removedDanglingConfigs: Array<{
53
+ agent: AgentId;
54
+ realPath: string;
55
+ }>;
56
+ restoredHomeFiles: string[];
57
+ releasedLaunchers: string[];
58
+ cleanedRcFiles: string[];
59
+ agentsDir: {
60
+ path: string;
61
+ disposition: 'moved' | 'purged' | 'absent';
62
+ movedTo?: string;
63
+ };
64
+ legacySymlinkRemoved: boolean;
65
+ /** True when `--purge` was requested but downgraded to move-aside after errors. */
66
+ purgeDowngraded: boolean;
67
+ errors: string[];
68
+ }
69
+ /**
70
+ * Build a read-only plan of everything a complete uninstall would change.
71
+ * Performs no mutations; safe to run for `--dry-run` and to print for confirm.
72
+ */
73
+ export declare function planUninstall(): UninstallPlan;
74
+ /**
75
+ * Execute a plan built by {@link planUninstall}. Restores adopted config dirs
76
+ * and home files, releases adopted launchers, strips shim PATH lines, then
77
+ * disposes of `~/.agents` — moved aside to `~/.agents.removed-<ts>` (recoverable)
78
+ * by default, or hard-deleted when `purge` is set. Config restore always runs
79
+ * before disposal because the backups live inside `~/.agents`.
80
+ */
81
+ export declare function executeUninstall(plan: UninstallPlan, opts: {
82
+ purge?: boolean;
83
+ timestamp: number;
84
+ }): UninstallResult;
@@ -0,0 +1,347 @@
1
+ /**
2
+ * Complete teardown of agents-cli — the reverse of `agents setup`.
3
+ *
4
+ * The hard part is undoing "adoption": normal installs move the user's real
5
+ * `~/.<agent>` aside and replace it with a symlink into agents-cli's version
6
+ * homes (see `switchConfigSymlink` / `importAgent`). A clean uninstall must put
7
+ * those real directories back, release any adopted launchers, strip the shim
8
+ * directory from the user's PATH, and only then dispose of `~/.agents`.
9
+ *
10
+ * Safety invariant: a `~/.<agent>` that agents-cli never adopted is a REAL user
11
+ * directory and is never touched. Ownership is decided structurally by
12
+ * `getConfigSymlinkVersion` (non-null only for a symlink into our versions dir),
13
+ * exactly as `removeVersion` does it — no marker files, no guessing.
14
+ *
15
+ * This module is split into a read-only {@link planUninstall} and a mutating
16
+ * {@link executeUninstall} so `--dry-run` and the real run share one code path.
17
+ */
18
+ import * as fs from 'fs';
19
+ import * as os from 'os';
20
+ import * as path from 'path';
21
+ import { AGENTS, ALL_AGENT_IDS } from './agents.js';
22
+ import { getAgentConfigPath, getConfigSymlinkVersion, stripShimPathLines, releaseAdoptedLauncher, } from './shims.js';
23
+ import { getUserAgentsDir, getBackupsDir, getHistoryDir, getShimsDir, getLegacySystemAgentsDir, } from './state.js';
24
+ /** Home dir honoring the AGENTS_REAL_HOME test/override, mirroring shims.ts. */
25
+ function realHome() {
26
+ return process.env.AGENTS_REAL_HOME || os.homedir();
27
+ }
28
+ /** Newest timestamped backup dir under `<backups>/<agent>/`, or null. */
29
+ function newestBackupDir(agent) {
30
+ const dir = path.join(getBackupsDir(), agent);
31
+ let entries;
32
+ try {
33
+ entries = fs.readdirSync(dir).filter((n) => /^\d+$/.test(n));
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ if (entries.length === 0)
39
+ return null;
40
+ entries.sort((a, b) => Number(a) - Number(b));
41
+ return path.join(dir, entries[entries.length - 1]);
42
+ }
43
+ /** Absolute target of a symlink, or null if `p` is not a readable symlink. */
44
+ function symlinkTarget(p) {
45
+ try {
46
+ const raw = fs.readlinkSync(p);
47
+ return path.resolve(path.dirname(p), raw);
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ }
53
+ /**
54
+ * Remove a symlink/junction at `p` without following into its target. On POSIX
55
+ * this is a plain `unlinkSync`; on Windows the same call correctly deletes a
56
+ * junction or directory-symlink reparse point while leaving the target intact —
57
+ * verified on a real Windows host, where `fs.rmSync(p, { force: true })` instead
58
+ * throws `EFAULT` on a reparse point. `rmSync` is deliberately NOT used here.
59
+ */
60
+ function removeLink(p) {
61
+ fs.unlinkSync(p);
62
+ }
63
+ /**
64
+ * Move `source` onto `dest` across possibly-different volumes. `renameSync` is
65
+ * atomic but throws EXDEV when `~/.agents` lives on a different filesystem than
66
+ * `$HOME`; fall back to copy-then-remove so the restore still completes. The
67
+ * source (a backup inside `~/.agents`) is removed only after the copy succeeds,
68
+ * so a mid-copy failure never destroys the sole surviving copy.
69
+ */
70
+ function moveDirCrossDevice(source, dest) {
71
+ try {
72
+ fs.renameSync(source, dest);
73
+ }
74
+ catch (err) {
75
+ if (err.code !== 'EXDEV')
76
+ throw err;
77
+ fs.cpSync(source, dest, { recursive: true });
78
+ fs.rmSync(source, { recursive: true, force: true });
79
+ }
80
+ }
81
+ /**
82
+ * Copy `source` to `dest`, dropping any symlink whose target resolves back into
83
+ * `~/.agents`. Adoption syncs managed resources (skills/commands) into the
84
+ * version home as symlinks into `~/.agents`; copying them verbatim would leave
85
+ * the restored config full of links that dangle the moment `~/.agents` is
86
+ * disposed. Stripping them yields a clean, self-contained restore.
87
+ */
88
+ function copyDirStrippingAgentsSymlinks(source, dest, agentsDir) {
89
+ const inside = agentsDir + path.sep;
90
+ fs.cpSync(source, dest, {
91
+ recursive: true,
92
+ filter: (src) => {
93
+ try {
94
+ const st = fs.lstatSync(src);
95
+ if (st.isSymbolicLink()) {
96
+ const tgt = path.resolve(path.dirname(src), fs.readlinkSync(src));
97
+ if (tgt === agentsDir || tgt.startsWith(inside))
98
+ return false;
99
+ }
100
+ }
101
+ catch {
102
+ /* unreadable entry — let cpSync surface it on the real copy */
103
+ }
104
+ return true;
105
+ },
106
+ });
107
+ }
108
+ /** Classify one agent's config dir without mutating anything. */
109
+ function planConfig(agent) {
110
+ const realPath = getAgentConfigPath(agent);
111
+ let stat;
112
+ try {
113
+ stat = fs.lstatSync(realPath);
114
+ }
115
+ catch {
116
+ return { agent, realPath, kind: 'absent' };
117
+ }
118
+ // A real directory means agents-cli never adopted it — leave it alone.
119
+ if (!stat.isSymbolicLink())
120
+ return { agent, realPath, kind: 'leave-real' };
121
+ // A symlink we don't own (target not under our versions dir) — leave it alone.
122
+ if (getConfigSymlinkVersion(agent) === null)
123
+ return { agent, realPath, kind: 'leave-foreign' };
124
+ // Owned symlink: prefer the timestamped backup (switchConfigSymlink moved the
125
+ // real dir there); otherwise the symlink target itself holds the user's data
126
+ // (importAgent renamed the real dir INTO the version home).
127
+ const backup = newestBackupDir(agent);
128
+ if (backup)
129
+ return { agent, realPath, kind: 'restore-backup', source: backup };
130
+ const target = symlinkTarget(realPath);
131
+ if (target && fs.existsSync(target)) {
132
+ return { agent, realPath, kind: 'restore-version-home', source: target };
133
+ }
134
+ return { agent, realPath, kind: 'remove-dangling' };
135
+ }
136
+ /** Owned home-file symlinks (e.g. `~/.claude.json`) to copy back as real files. */
137
+ function planHomeFiles() {
138
+ const home = realHome();
139
+ const userDir = getUserAgentsDir();
140
+ const out = [];
141
+ for (const agent of ALL_AGENT_IDS) {
142
+ const homeFiles = AGENTS[agent].homeFiles;
143
+ if (!homeFiles)
144
+ continue;
145
+ for (const fileName of homeFiles) {
146
+ const realPath = path.join(home, fileName);
147
+ let stat;
148
+ try {
149
+ stat = fs.lstatSync(realPath);
150
+ }
151
+ catch {
152
+ continue;
153
+ }
154
+ if (!stat.isSymbolicLink())
155
+ continue;
156
+ const target = symlinkTarget(realPath);
157
+ // Only ours (points into ~/.agents) and only if the target still exists.
158
+ if (target && target.startsWith(userDir + path.sep) && fs.existsSync(target)) {
159
+ out.push({ realPath, source: target });
160
+ }
161
+ }
162
+ }
163
+ return out;
164
+ }
165
+ /** cliCommand basenames that have an adopted-launcher record to release. */
166
+ function planLaunchers() {
167
+ const dir = path.join(getHistoryDir(), 'adopted-launchers');
168
+ try {
169
+ return fs.readdirSync(dir).filter((n) => !n.startsWith('.'));
170
+ }
171
+ catch {
172
+ return [];
173
+ }
174
+ }
175
+ /** Candidate shell rc files that currently contain a shim PATH entry. */
176
+ function planRcFiles() {
177
+ const home = realHome();
178
+ const shimsDir = getShimsDir();
179
+ const candidates = ['.zshrc', '.bashrc', '.bash_profile', '.profile', path.join('.config', 'fish', 'config.fish')];
180
+ const out = [];
181
+ for (const rel of candidates) {
182
+ const rc = path.join(home, rel);
183
+ let content;
184
+ try {
185
+ content = fs.readFileSync(rc, 'utf-8');
186
+ }
187
+ catch {
188
+ continue;
189
+ }
190
+ if (stripShimPathLines(content, shimsDir) !== content)
191
+ out.push(rc);
192
+ }
193
+ return out;
194
+ }
195
+ /**
196
+ * Build a read-only plan of everything a complete uninstall would change.
197
+ * Performs no mutations; safe to run for `--dry-run` and to print for confirm.
198
+ */
199
+ export function planUninstall() {
200
+ const agentsDir = getUserAgentsDir();
201
+ const legacy = getLegacySystemAgentsDir();
202
+ let legacySymlink = null;
203
+ try {
204
+ // Only claim it if it's actually a link (symlink on POSIX, junction on Windows —
205
+ // both report isSymbolicLink()); a real directory here is left alone so removeLink
206
+ // (unlinkSync) is always the correct primitive for what we captured.
207
+ if (fs.lstatSync(legacy).isSymbolicLink())
208
+ legacySymlink = legacy;
209
+ }
210
+ catch {
211
+ legacySymlink = null;
212
+ }
213
+ return {
214
+ isInstalled: fs.existsSync(agentsDir),
215
+ agentsDir,
216
+ legacySymlink,
217
+ configs: ALL_AGENT_IDS.map(planConfig),
218
+ homeFiles: planHomeFiles(),
219
+ launchers: planLaunchers(),
220
+ rcFiles: planRcFiles(),
221
+ };
222
+ }
223
+ /**
224
+ * Execute a plan built by {@link planUninstall}. Restores adopted config dirs
225
+ * and home files, releases adopted launchers, strips shim PATH lines, then
226
+ * disposes of `~/.agents` — moved aside to `~/.agents.removed-<ts>` (recoverable)
227
+ * by default, or hard-deleted when `purge` is set. Config restore always runs
228
+ * before disposal because the backups live inside `~/.agents`.
229
+ */
230
+ export function executeUninstall(plan, opts) {
231
+ const result = {
232
+ restoredConfigs: [],
233
+ removedDanglingConfigs: [],
234
+ restoredHomeFiles: [],
235
+ releasedLaunchers: [],
236
+ cleanedRcFiles: [],
237
+ agentsDir: { path: plan.agentsDir, disposition: 'absent' },
238
+ legacySymlinkRemoved: false,
239
+ purgeDowngraded: false,
240
+ errors: [],
241
+ };
242
+ // 1. Restore adopted config directories (reads backups inside ~/.agents).
243
+ for (const c of plan.configs) {
244
+ try {
245
+ if (c.kind === 'restore-backup') {
246
+ // The adopted link carries no data (the real dir is the backup); drop it,
247
+ // then move the backup out of ~/.agents onto the real path — EXDEV-safe so a
248
+ // cross-volume ~/.agents can't strand the backup mid-restore. unlinkSync (not
249
+ // rmSync) is deliberate: it removes a POSIX symlink AND a Windows junction/
250
+ // dir-symlink without following into the target, whereas rmSync throws EFAULT
251
+ // on a Windows reparse point.
252
+ removeLink(c.realPath);
253
+ moveDirCrossDevice(c.source, c.realPath);
254
+ result.restoredConfigs.push({ agent: c.agent, realPath: c.realPath });
255
+ }
256
+ else if (c.kind === 'restore-version-home') {
257
+ // importAgent renamed the real dir INTO the version home; copy it back
258
+ // (step 6 disposes the original) while stripping resource symlinks that
259
+ // would dangle once ~/.agents is gone.
260
+ removeLink(c.realPath);
261
+ copyDirStrippingAgentsSymlinks(c.source, c.realPath, plan.agentsDir);
262
+ result.restoredConfigs.push({ agent: c.agent, realPath: c.realPath });
263
+ }
264
+ else if (c.kind === 'remove-dangling') {
265
+ removeLink(c.realPath);
266
+ result.removedDanglingConfigs.push({ agent: c.agent, realPath: c.realPath });
267
+ }
268
+ // leave-real / leave-foreign / absent: intentionally untouched.
269
+ }
270
+ catch (err) {
271
+ result.errors.push(`config ${c.agent} (${c.realPath}): ${err.message}`);
272
+ }
273
+ }
274
+ // 2. Restore owned home-file symlinks as real files (e.g. ~/.claude.json).
275
+ for (const hf of plan.homeFiles) {
276
+ try {
277
+ removeLink(hf.realPath);
278
+ fs.cpSync(hf.source, hf.realPath, { recursive: true });
279
+ result.restoredHomeFiles.push(hf.realPath);
280
+ }
281
+ catch (err) {
282
+ result.errors.push(`home file ${hf.realPath}: ${err.message}`);
283
+ }
284
+ }
285
+ // 3. Release adopted launchers (reads records inside ~/.agents).
286
+ const byCli = new Map(ALL_AGENT_IDS.map((a) => [AGENTS[a].cliCommand, a]));
287
+ for (const cli of plan.launchers) {
288
+ const agent = byCli.get(cli);
289
+ if (!agent)
290
+ continue;
291
+ try {
292
+ releaseAdoptedLauncher(agent);
293
+ result.releasedLaunchers.push(cli);
294
+ }
295
+ catch (err) {
296
+ result.errors.push(`launcher ${cli}: ${err.message}`);
297
+ }
298
+ }
299
+ // 4. Strip the shim directory from the user's PATH across all rc files.
300
+ const shimsDir = getShimsDir();
301
+ for (const rc of plan.rcFiles) {
302
+ try {
303
+ const content = fs.readFileSync(rc, 'utf-8');
304
+ fs.writeFileSync(rc, stripShimPathLines(content, shimsDir));
305
+ result.cleanedRcFiles.push(rc);
306
+ }
307
+ catch (err) {
308
+ result.errors.push(`rc file ${rc}: ${err.message}`);
309
+ }
310
+ }
311
+ // 5. Remove the legacy back-compat symlink, if present. `~/.agents-system` is a
312
+ // link (junction on Windows — createLink uses 'junction' for a dir source), so it
313
+ // goes through removeLink for the same reason as the config links: rmSync throws
314
+ // EFAULT on a Windows reparse point.
315
+ if (plan.legacySymlink) {
316
+ try {
317
+ removeLink(plan.legacySymlink);
318
+ result.legacySymlinkRemoved = true;
319
+ }
320
+ catch (err) {
321
+ result.errors.push(`legacy ${plan.legacySymlink}: ${err.message}`);
322
+ }
323
+ }
324
+ // 6. Dispose of ~/.agents LAST (its backups fed step 1). If any restore above
325
+ // failed, downgrade a --purge to a recoverable move-aside: a swallowed restore
326
+ // error must never let the hard-delete take the user's only copy with it.
327
+ if (fs.existsSync(plan.agentsDir)) {
328
+ const purge = !!opts.purge && result.errors.length === 0;
329
+ if (opts.purge && !purge)
330
+ result.purgeDowngraded = true;
331
+ try {
332
+ if (purge) {
333
+ fs.rmSync(plan.agentsDir, { recursive: true, force: true });
334
+ result.agentsDir = { path: plan.agentsDir, disposition: 'purged' };
335
+ }
336
+ else {
337
+ const movedTo = `${plan.agentsDir}.removed-${opts.timestamp}`;
338
+ fs.renameSync(plan.agentsDir, movedTo);
339
+ result.agentsDir = { path: plan.agentsDir, disposition: 'moved', movedTo };
340
+ }
341
+ }
342
+ catch (err) {
343
+ result.errors.push(`dispose ${plan.agentsDir}: ${err.message}`);
344
+ }
345
+ }
346
+ return result;
347
+ }
@@ -1,5 +1,17 @@
1
1
  import { type AccountInfo } from './agents.js';
2
2
  import type { AgentId } from './types.js';
3
+ /**
4
+ * True when a Claude OAuth access token is within the refresh leeway of expiry
5
+ * (or already expired) — i.e. it "would need a refresh" before the next use.
6
+ *
7
+ * Single source of truth for the expiry gate, shared by the two callers that
8
+ * must agree on it but act differently: the run/usage hot path
9
+ * (`getClaudeAccessToken`) refreshes when this is true; the health probe
10
+ * (`probeClaudeStatus`) must NOT refresh and instead reports the non-fatal
11
+ * `expired` state (RUSH-1822). A missing `expiresAt` is treated as "still
12
+ * fresh" (never force a refresh on a token with no known expiry).
13
+ */
14
+ export declare function claudeAccessTokenNeedsRefresh(expiresAt: number | null | undefined, nowMs?: number): boolean;
3
15
  /** Discriminator for usage window types. */
4
16
  export type UsageWindowKey = 'session' | 'week' | 'sonnet_week' | 'month';
5
17
  /** A single rate-limit window with utilization percentage and reset time. */
@@ -201,7 +213,7 @@ export interface ProviderProbe {
201
213
  /** Network/parse error message when status is null but a token was present. */
202
214
  error?: string;
203
215
  }
204
- /** Probe Claude's OAuth token against the usage endpoint. Refreshes an expired access token (safe for Claude). */
216
+ /** Probe Claude's OAuth token against the usage endpoint. Never refreshes — reports `expired` for a near-expiry token; see the comment below (RUSH-1822). */
205
217
  export declare function probeClaudeStatus(home?: string, cliVersion?: string | null): Promise<ProviderProbe>;
206
218
  /** Probe Kimi's OAuth token against the /usages endpoint. Never refreshes (single-use rotation — see getKimiUsageInfo). */
207
219
  export declare function probeKimiStatus(home?: string): Promise<ProviderProbe>;