agent-control-plane 0.1.0

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 (317) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +589 -0
  3. package/SKILL.md +149 -0
  4. package/assets/workflow-catalog.json +57 -0
  5. package/bin/audit-issue-routing.sh +74 -0
  6. package/bin/issue-resource-class.sh +58 -0
  7. package/bin/label-follow-up-issues.sh +114 -0
  8. package/bin/pr-risk.sh +532 -0
  9. package/bin/sync-pr-labels.sh +112 -0
  10. package/hooks/heartbeat-hooks.sh +573 -0
  11. package/hooks/issue-reconcile-hooks.sh +217 -0
  12. package/hooks/pr-reconcile-hooks.sh +225 -0
  13. package/npm/bin/agent-control-plane.js +1984 -0
  14. package/npm/public-bin/agent-control-plane +3 -0
  15. package/package.json +61 -0
  16. package/tools/bin/agent-cleanup-worktree +247 -0
  17. package/tools/bin/agent-github-update-labels +66 -0
  18. package/tools/bin/agent-init-worktree +216 -0
  19. package/tools/bin/agent-project-archive-run +52 -0
  20. package/tools/bin/agent-project-capture-worker +46 -0
  21. package/tools/bin/agent-project-catch-up-merged-prs +137 -0
  22. package/tools/bin/agent-project-cleanup-session +244 -0
  23. package/tools/bin/agent-project-detached-launch +107 -0
  24. package/tools/bin/agent-project-heartbeat-loop +2347 -0
  25. package/tools/bin/agent-project-open-issue-worktree +89 -0
  26. package/tools/bin/agent-project-open-pr-worktree +80 -0
  27. package/tools/bin/agent-project-publish-issue-pr +349 -0
  28. package/tools/bin/agent-project-reconcile-issue-session +1128 -0
  29. package/tools/bin/agent-project-reconcile-pr-session +1005 -0
  30. package/tools/bin/agent-project-retry-state +147 -0
  31. package/tools/bin/agent-project-run-claude-session +657 -0
  32. package/tools/bin/agent-project-run-codex-resilient +718 -0
  33. package/tools/bin/agent-project-run-codex-session +316 -0
  34. package/tools/bin/agent-project-run-kilo-session +27 -0
  35. package/tools/bin/agent-project-run-openclaw-session +984 -0
  36. package/tools/bin/agent-project-run-opencode-session +27 -0
  37. package/tools/bin/agent-project-sync-anchor-repo +128 -0
  38. package/tools/bin/agent-project-worker-status +143 -0
  39. package/tools/bin/audit-agent-worktrees.sh +310 -0
  40. package/tools/bin/audit-issue-routing.sh +11 -0
  41. package/tools/bin/audit-retained-layout.sh +58 -0
  42. package/tools/bin/audit-retained-overlap.sh +135 -0
  43. package/tools/bin/audit-retained-worktrees.sh +228 -0
  44. package/tools/bin/branch-verification-guard.sh +351 -0
  45. package/tools/bin/capture-worker.sh +18 -0
  46. package/tools/bin/check-skill-contracts.sh +324 -0
  47. package/tools/bin/cleanup-worktree.sh +44 -0
  48. package/tools/bin/codex-quota +31 -0
  49. package/tools/bin/create-follow-up-issue.sh +114 -0
  50. package/tools/bin/dashboard-launchd-bootstrap.sh +38 -0
  51. package/tools/bin/flow-config-lib.sh +2127 -0
  52. package/tools/bin/flow-resident-worker-lib.sh +683 -0
  53. package/tools/bin/flow-runtime-doctor.sh +97 -0
  54. package/tools/bin/flow-shell-lib.sh +266 -0
  55. package/tools/bin/heartbeat-recovery-preflight.sh +106 -0
  56. package/tools/bin/heartbeat-safe-auto.sh +551 -0
  57. package/tools/bin/install-dashboard-launchd.sh +152 -0
  58. package/tools/bin/install-project-launchd.sh +219 -0
  59. package/tools/bin/issue-publish-scope-guard.sh +242 -0
  60. package/tools/bin/issue-requires-local-workspace-install.sh +31 -0
  61. package/tools/bin/issue-resource-class.sh +12 -0
  62. package/tools/bin/kick-scheduler.sh +75 -0
  63. package/tools/bin/label-follow-up-issues.sh +14 -0
  64. package/tools/bin/new-pr-worktree.sh +50 -0
  65. package/tools/bin/new-worktree.sh +49 -0
  66. package/tools/bin/pr-risk.sh +12 -0
  67. package/tools/bin/prepare-worktree.sh +140 -0
  68. package/tools/bin/profile-activate.sh +109 -0
  69. package/tools/bin/profile-adopt.sh +219 -0
  70. package/tools/bin/profile-smoke.sh +461 -0
  71. package/tools/bin/project-init.sh +189 -0
  72. package/tools/bin/project-launchd-bootstrap.sh +54 -0
  73. package/tools/bin/project-remove.sh +155 -0
  74. package/tools/bin/project-runtime-supervisor.sh +56 -0
  75. package/tools/bin/project-runtimectl.sh +586 -0
  76. package/tools/bin/provider-cooldown-state.sh +166 -0
  77. package/tools/bin/publish-issue-worker.sh +31 -0
  78. package/tools/bin/reconcile-issue-worker.sh +34 -0
  79. package/tools/bin/reconcile-pr-worker.sh +34 -0
  80. package/tools/bin/record-verification.sh +71 -0
  81. package/tools/bin/render-architecture-infographics.sh +110 -0
  82. package/tools/bin/render-dashboard-demo-media.sh +333 -0
  83. package/tools/bin/render-dashboard-snapshot.py +16 -0
  84. package/tools/bin/render-flow-config.sh +86 -0
  85. package/tools/bin/retry-state.sh +31 -0
  86. package/tools/bin/reuse-issue-worktree.sh +75 -0
  87. package/tools/bin/run-codex-bypass.sh +3 -0
  88. package/tools/bin/run-codex-safe.sh +3 -0
  89. package/tools/bin/run-codex-task.sh +231 -0
  90. package/tools/bin/scaffold-profile.sh +374 -0
  91. package/tools/bin/serve-dashboard.sh +5 -0
  92. package/tools/bin/split-retained-slice.sh +124 -0
  93. package/tools/bin/start-issue-worker.sh +796 -0
  94. package/tools/bin/start-pr-fix-worker.sh +458 -0
  95. package/tools/bin/start-pr-merge-repair-worker.sh +8 -0
  96. package/tools/bin/start-pr-review-worker.sh +227 -0
  97. package/tools/bin/start-resident-issue-loop.sh +908 -0
  98. package/tools/bin/sync-agent-repo.sh +52 -0
  99. package/tools/bin/sync-dependency-baseline.sh +247 -0
  100. package/tools/bin/sync-pr-labels.sh +12 -0
  101. package/tools/bin/sync-recurring-issue-checklist.sh +274 -0
  102. package/tools/bin/sync-shared-agent-home.sh +214 -0
  103. package/tools/bin/sync-vscode-workspace.sh +157 -0
  104. package/tools/bin/test-smoke.sh +63 -0
  105. package/tools/bin/uninstall-project-launchd.sh +55 -0
  106. package/tools/bin/update-github-labels.sh +14 -0
  107. package/tools/bin/worker-status.sh +19 -0
  108. package/tools/bin/workflow-catalog.sh +77 -0
  109. package/tools/dashboard/app.js +286 -0
  110. package/tools/dashboard/dashboard_snapshot.py +466 -0
  111. package/tools/dashboard/index.html +41 -0
  112. package/tools/dashboard/server.py +64 -0
  113. package/tools/dashboard/styles.css +351 -0
  114. package/tools/templates/issue-prompt-template.md +109 -0
  115. package/tools/templates/pr-fix-template.md +120 -0
  116. package/tools/templates/pr-merge-repair-template.md +91 -0
  117. package/tools/templates/pr-review-template.md +62 -0
  118. package/tools/templates/scheduled-issue-prompt-template.md +62 -0
  119. package/tools/tests/test-agent-control-plane-npm-cli.sh +279 -0
  120. package/tools/tests/test-agent-github-update-labels-falls-back-to-repository-id.sh +56 -0
  121. package/tools/tests/test-agent-project-claude-session-wrapper-clears-stale-sandbox-artifacts.sh +89 -0
  122. package/tools/tests/test-agent-project-claude-session-wrapper-does-not-retry-provider-quota.sh +82 -0
  123. package/tools/tests/test-agent-project-claude-session-wrapper-retries-transient-failures.sh +90 -0
  124. package/tools/tests/test-agent-project-claude-session-wrapper-times-out.sh +73 -0
  125. package/tools/tests/test-agent-project-claude-session-wrapper.sh +103 -0
  126. package/tools/tests/test-agent-project-cleanup-session-orphan-fallback.sh +90 -0
  127. package/tools/tests/test-agent-project-cleanup-session-skip-worktree-cleanup.sh +90 -0
  128. package/tools/tests/test-agent-project-codex-live-thread-persist.sh +76 -0
  129. package/tools/tests/test-agent-project-codex-recovery.sh +731 -0
  130. package/tools/tests/test-agent-project-codex-session-wrapper-clears-stale-sandbox-artifacts.sh +105 -0
  131. package/tools/tests/test-agent-project-codex-session-wrapper.sh +97 -0
  132. package/tools/tests/test-agent-project-open-pr-worktree-config-prefix.sh +81 -0
  133. package/tools/tests/test-agent-project-openclaw-session-wrapper-clears-stale-sandbox-artifacts.sh +109 -0
  134. package/tools/tests/test-agent-project-openclaw-session-wrapper-infers-blocked-result-contract.sh +89 -0
  135. package/tools/tests/test-agent-project-openclaw-session-wrapper-recovers-literal-env-artifacts.sh +113 -0
  136. package/tools/tests/test-agent-project-openclaw-session-wrapper-recovers-version-mismatch.sh +135 -0
  137. package/tools/tests/test-agent-project-openclaw-session-wrapper-resident.sh +179 -0
  138. package/tools/tests/test-agent-project-openclaw-session-wrapper-reuses-existing-agent-after-add-race.sh +119 -0
  139. package/tools/tests/test-agent-project-openclaw-session-wrapper-terminates-rate-limit-hang.sh +91 -0
  140. package/tools/tests/test-agent-project-openclaw-session-wrapper.sh +117 -0
  141. package/tools/tests/test-agent-project-publish-issue-pr-prunes-stale-worktree-entry.sh +148 -0
  142. package/tools/tests/test-agent-project-publish-issue-pr-reads-archived-session.sh +146 -0
  143. package/tools/tests/test-agent-project-publish-issue-pr-recovers-final-head.sh +145 -0
  144. package/tools/tests/test-agent-project-publish-issue-pr-reuses-existing-worktree.sh +147 -0
  145. package/tools/tests/test-agent-project-reconcile-failure-reason.sh +456 -0
  146. package/tools/tests/test-agent-project-reconcile-issue-archived-session-fallback.sh +96 -0
  147. package/tools/tests/test-agent-project-reconcile-issue-before-blocked.sh +90 -0
  148. package/tools/tests/test-agent-project-reconcile-issue-host-verification-recovery-uses-recovered-worktree.sh +212 -0
  149. package/tools/tests/test-agent-project-reconcile-issue-host-verification-recovery.sh +207 -0
  150. package/tools/tests/test-agent-project-reconcile-issue-provider-quota-schedules-provider-cooldown.sh +101 -0
  151. package/tools/tests/test-agent-project-reconcile-issue-session-backfills-lane-metadata-from-worker-key.sh +113 -0
  152. package/tools/tests/test-agent-project-reconcile-issue-session-clears-stale-failed-summary.sh +117 -0
  153. package/tools/tests/test-agent-project-reconcile-issue-session-initializes-shared-agent-home.sh +55 -0
  154. package/tools/tests/test-agent-project-reconcile-issue-session-normalizes-runner-state.sh +125 -0
  155. package/tools/tests/test-agent-project-reconcile-issue-session-records-invalid-contract-summary.sh +118 -0
  156. package/tools/tests/test-agent-project-reconcile-issue-session-skips-duplicate-blocked-comment.sh +144 -0
  157. package/tools/tests/test-agent-project-reconcile-issue-session-standardizes-no-commits-blocker.sh +145 -0
  158. package/tools/tests/test-agent-project-reconcile-issue-session-synthesizes-blocked-comment.sh +139 -0
  159. package/tools/tests/test-agent-project-reconcile-pr-blocked-host-recovery.sh +242 -0
  160. package/tools/tests/test-agent-project-reconcile-pr-guard-blocked-no-commit.sh +142 -0
  161. package/tools/tests/test-agent-project-reconcile-pr-provider-quota-schedules-provider-cooldown.sh +106 -0
  162. package/tools/tests/test-agent-project-reconcile-pr-session-initializes-shared-agent-home.sh +66 -0
  163. package/tools/tests/test-agent-project-reconcile-pr-updated-branch-noop.sh +129 -0
  164. package/tools/tests/test-audit-agent-worktrees-active-launch-skips-git-inspection.sh +69 -0
  165. package/tools/tests/test-audit-agent-worktrees-broken-worktree.sh +43 -0
  166. package/tools/tests/test-audit-agent-worktrees-pending-launch-owner.sh +46 -0
  167. package/tools/tests/test-audit-agent-worktrees-unreconciled-owner.sh +79 -0
  168. package/tools/tests/test-audit-issue-routing-managed-branch-globs.sh +56 -0
  169. package/tools/tests/test-branch-verification-guard-generated-artifacts.sh +72 -0
  170. package/tools/tests/test-branch-verification-guard-targeted-coverage.sh +125 -0
  171. package/tools/tests/test-codex-quota-manager-failure-driven-rotation.sh +178 -0
  172. package/tools/tests/test-codex-quota-wrapper.sh +37 -0
  173. package/tools/tests/test-contribution-docs.sh +18 -0
  174. package/tools/tests/test-control-plane-dashboard-runtime-smoke.sh +343 -0
  175. package/tools/tests/test-create-follow-up-issue.sh +73 -0
  176. package/tools/tests/test-dashboard-launchd-bootstrap.sh +55 -0
  177. package/tools/tests/test-flow-export-execution-env-exports-repo-id.sh +30 -0
  178. package/tools/tests/test-flow-export-github-cli-auth-env-prefers-git-credential.sh +48 -0
  179. package/tools/tests/test-flow-github-api-repo-fallback-preserves-input.sh +85 -0
  180. package/tools/tests/test-flow-github-api-repo-prefers-explicit-repository-id.sh +60 -0
  181. package/tools/tests/test-flow-github-issue-list-falls-back-to-repository-id.sh +64 -0
  182. package/tools/tests/test-flow-github-pr-list-falls-back-to-repository-id.sh +77 -0
  183. package/tools/tests/test-flow-resident-can-reuse-does-not-leak-metadata.sh +52 -0
  184. package/tools/tests/test-flow-resident-reap-stale-controllers.sh +63 -0
  185. package/tools/tests/test-flow-resolve-codex-quota-tools.sh +104 -0
  186. package/tools/tests/test-flow-runtime-doctor-profile-selection.sh +27 -0
  187. package/tools/tests/test-heartbeat-codex-pr-linked-issue-exclusion.sh +79 -0
  188. package/tools/tests/test-heartbeat-hooks-enqueue-resident-issue-for-idle-controller.sh +115 -0
  189. package/tools/tests/test-heartbeat-hooks-enqueue-resident-issue-for-live-lane-controller.sh +117 -0
  190. package/tools/tests/test-heartbeat-hooks-start-resident-issue-loop-claude.sh +96 -0
  191. package/tools/tests/test-heartbeat-hooks-start-resident-issue-loop-codex.sh +96 -0
  192. package/tools/tests/test-heartbeat-hooks-start-resident-issue-loop.sh +96 -0
  193. package/tools/tests/test-heartbeat-loop-auth-wait-does-not-consume-capacity.sh +170 -0
  194. package/tools/tests/test-heartbeat-loop-blocked-recovery-lane.sh +201 -0
  195. package/tools/tests/test-heartbeat-loop-blocked-recovery-vs-pr-reservation.sh +201 -0
  196. package/tools/tests/test-heartbeat-loop-idle-resident-controller-does-not-block-launches.sh +160 -0
  197. package/tools/tests/test-heartbeat-loop-pr-launch-dedup.sh +133 -0
  198. package/tools/tests/test-heartbeat-loop-provider-cooldown-suppresses-launches.sh +157 -0
  199. package/tools/tests/test-heartbeat-loop-reaps-stale-resident-controller.sh +181 -0
  200. package/tools/tests/test-heartbeat-loop-waiting-provider-resident-controller-does-not-block-launches.sh +160 -0
  201. package/tools/tests/test-heartbeat-ready-issues-blocked-recovery.sh +134 -0
  202. package/tools/tests/test-heartbeat-safe-auto-dynamic-concurrency.sh +162 -0
  203. package/tools/tests/test-heartbeat-safe-auto-no-tmux-sessions.sh +136 -0
  204. package/tools/tests/test-heartbeat-safe-auto-openclaw-skips-codex-quota.sh +139 -0
  205. package/tools/tests/test-heartbeat-safe-auto-quota-health-signal.sh +119 -0
  206. package/tools/tests/test-heartbeat-safe-auto-stale-shared-loop-pid-does-not-skip.sh +140 -0
  207. package/tools/tests/test-heartbeat-safe-auto-static-capacity-without-quota-cache.sh +142 -0
  208. package/tools/tests/test-heartbeat-safe-auto-zero-healthy-pools.sh +141 -0
  209. package/tools/tests/test-heartbeat-sync-issue-labels-empty-schedule.sh +65 -0
  210. package/tools/tests/test-heartbeat-sync-open-agent-prs-terminal-clears-running.sh +179 -0
  211. package/tools/tests/test-install-dashboard-launchd.sh +78 -0
  212. package/tools/tests/test-install-project-launchd-adds-tool-paths.sh +87 -0
  213. package/tools/tests/test-install-project-launchd.sh +110 -0
  214. package/tools/tests/test-issue-local-workspace-install-policy.sh +81 -0
  215. package/tools/tests/test-issue-publish-scope-guard-docs-signal.sh +70 -0
  216. package/tools/tests/test-issue-reconcile-hooks-success-clears-blocked.sh +36 -0
  217. package/tools/tests/test-kick-scheduler-requires-explicit-profile.sh +47 -0
  218. package/tools/tests/test-label-follow-up-issues-falls-back-to-repository-id.sh +132 -0
  219. package/tools/tests/test-manual-operator-entrypoints-require-explicit-profile.sh +64 -0
  220. package/tools/tests/test-package-funding-metadata.sh +21 -0
  221. package/tools/tests/test-package-public-metadata.sh +62 -0
  222. package/tools/tests/test-placeholder-worker-adapters.sh +38 -0
  223. package/tools/tests/test-pr-reconcile-hooks-refreshes-recurring-issue-checklist.sh +110 -0
  224. package/tools/tests/test-pr-risk-cohesive-mobile-locale-scope.sh +70 -0
  225. package/tools/tests/test-pr-risk-fix-label-semantics.sh +114 -0
  226. package/tools/tests/test-pr-risk-local-first-no-checks.sh +70 -0
  227. package/tools/tests/test-prepare-worktree-simple-repo-baseline.sh +67 -0
  228. package/tools/tests/test-profile-activate.sh +33 -0
  229. package/tools/tests/test-profile-adopt-allow-missing-repo.sh +68 -0
  230. package/tools/tests/test-profile-adopt-skip-workspace-sync-missing-file.sh +61 -0
  231. package/tools/tests/test-profile-adopt-syncs-anchor-and-workspace.sh +90 -0
  232. package/tools/tests/test-profile-smoke-collision.sh +44 -0
  233. package/tools/tests/test-profile-smoke-invalid-claude-config.sh +31 -0
  234. package/tools/tests/test-profile-smoke-invalid-provider-pool.sh +68 -0
  235. package/tools/tests/test-profile-smoke-repo-slug-mismatch.sh +36 -0
  236. package/tools/tests/test-profile-smoke.sh +45 -0
  237. package/tools/tests/test-project-init-force-and-skip-sync.sh +61 -0
  238. package/tools/tests/test-project-init-repo-slug-mismatch.sh +29 -0
  239. package/tools/tests/test-project-init.sh +66 -0
  240. package/tools/tests/test-project-launchd-bootstrap.sh +66 -0
  241. package/tools/tests/test-project-remove.sh +150 -0
  242. package/tools/tests/test-project-runtime-supervisor.sh +47 -0
  243. package/tools/tests/test-project-runtimectl-launchd.sh +115 -0
  244. package/tools/tests/test-project-runtimectl-missing-profile.sh +54 -0
  245. package/tools/tests/test-project-runtimectl-start-falls-back-to-bootstrap.sh +108 -0
  246. package/tools/tests/test-project-runtimectl-status-reports-supervisor-as-heartbeat-parent.sh +95 -0
  247. package/tools/tests/test-project-runtimectl-status-supervisor-running.sh +59 -0
  248. package/tools/tests/test-project-runtimectl-stop-cancels-pending-kick.sh +85 -0
  249. package/tools/tests/test-project-runtimectl-stop-clears-running-labels.sh +78 -0
  250. package/tools/tests/test-project-runtimectl.sh +212 -0
  251. package/tools/tests/test-provider-cooldown-state-prefers-runtime-worker-context.sh +39 -0
  252. package/tools/tests/test-provider-cooldown-state.sh +59 -0
  253. package/tools/tests/test-public-repo-docs.sh +159 -0
  254. package/tools/tests/test-reconcile-pr-worker-acp-config-routing.sh +75 -0
  255. package/tools/tests/test-render-dashboard-snapshot.sh +149 -0
  256. package/tools/tests/test-render-flow-config-demo-profile.sh +36 -0
  257. package/tools/tests/test-render-flow-config-provider-pool-fallback.sh +81 -0
  258. package/tools/tests/test-render-flow-config.sh +52 -0
  259. package/tools/tests/test-run-codex-task-claude-routing.sh +125 -0
  260. package/tools/tests/test-run-codex-task-codex-resident-routing.sh +108 -0
  261. package/tools/tests/test-run-codex-task-kilo-routing.sh +98 -0
  262. package/tools/tests/test-run-codex-task-openclaw-resident-routing.sh +117 -0
  263. package/tools/tests/test-run-codex-task-openclaw-routing.sh +113 -0
  264. package/tools/tests/test-run-codex-task-opencode-routing.sh +98 -0
  265. package/tools/tests/test-run-codex-task-provider-pool-fallback-routing.sh +146 -0
  266. package/tools/tests/test-scaffold-profile.sh +108 -0
  267. package/tools/tests/test-serve-dashboard.sh +93 -0
  268. package/tools/tests/test-start-issue-worker-blocked-context.sh +129 -0
  269. package/tools/tests/test-start-issue-worker-blocks-complete-recurring-checklist.sh +189 -0
  270. package/tools/tests/test-start-issue-worker-local-install-routing.sh +157 -0
  271. package/tools/tests/test-start-issue-worker-profile-template-routing.sh +149 -0
  272. package/tools/tests/test-start-issue-worker-recurring-resident-reuse-codex.sh +212 -0
  273. package/tools/tests/test-start-issue-worker-recurring-resident-reuse.sh +219 -0
  274. package/tools/tests/test-start-issue-worker-renders-verification-snippet.sh +155 -0
  275. package/tools/tests/test-start-issue-worker-resident-reuse-falls-back-to-new-worktree.sh +199 -0
  276. package/tools/tests/test-start-pr-fix-worker-host-blocker-context.sh +275 -0
  277. package/tools/tests/test-start-resident-issue-loop-adopts-next-recurring-issue.sh +185 -0
  278. package/tools/tests/test-start-resident-issue-loop-clears-pending-while-waiting-due.sh +152 -0
  279. package/tools/tests/test-start-resident-issue-loop-consumes-queued-lease.sh +186 -0
  280. package/tools/tests/test-start-resident-issue-loop-fails-over-provider-pool.sh +212 -0
  281. package/tools/tests/test-start-resident-issue-loop-immediate-cycles.sh +148 -0
  282. package/tools/tests/test-start-resident-issue-loop-waits-for-provider.sh +194 -0
  283. package/tools/tests/test-start-resident-issue-loop-waits-for-terminal-reconcile-status.sh +198 -0
  284. package/tools/tests/test-start-resident-issue-loop-yields-to-live-lane-controller.sh +145 -0
  285. package/tools/tests/test-sync-pr-labels-fix-lane-uses-repair-queued.sh +67 -0
  286. package/tools/tests/test-sync-recurring-issue-checklist-backfills-workflow-complete-blocker.sh +70 -0
  287. package/tools/tests/test-sync-recurring-issue-checklist.sh +95 -0
  288. package/tools/tests/test-sync-shared-agent-home-local-source-root.sh +66 -0
  289. package/tools/tests/test-sync-shared-agent-home-preserves-unrelated-workflow-catalog-skill.sh +47 -0
  290. package/tools/tests/test-test-smoke.sh +86 -0
  291. package/tools/tests/test-uninstall-project-launchd.sh +37 -0
  292. package/tools/tests/test-update-github-labels-prefers-sibling-helper.sh +49 -0
  293. package/tools/tests/test-workflow-catalog.sh +43 -0
  294. package/tools/vendor/codex-quota/LICENSE +21 -0
  295. package/tools/vendor/codex-quota/README.md +459 -0
  296. package/tools/vendor/codex-quota/codex-quota.js +261 -0
  297. package/tools/vendor/codex-quota/lib/claude-accounts.js +226 -0
  298. package/tools/vendor/codex-quota/lib/claude-oauth.js +174 -0
  299. package/tools/vendor/codex-quota/lib/claude-tokens.js +471 -0
  300. package/tools/vendor/codex-quota/lib/claude-usage.js +929 -0
  301. package/tools/vendor/codex-quota/lib/codex-accounts.js +205 -0
  302. package/tools/vendor/codex-quota/lib/codex-tokens.js +326 -0
  303. package/tools/vendor/codex-quota/lib/codex-usage.js +32 -0
  304. package/tools/vendor/codex-quota/lib/color.js +72 -0
  305. package/tools/vendor/codex-quota/lib/constants.js +57 -0
  306. package/tools/vendor/codex-quota/lib/container.js +143 -0
  307. package/tools/vendor/codex-quota/lib/display.js +1111 -0
  308. package/tools/vendor/codex-quota/lib/fs.js +63 -0
  309. package/tools/vendor/codex-quota/lib/handlers.js +2060 -0
  310. package/tools/vendor/codex-quota/lib/jwt.js +33 -0
  311. package/tools/vendor/codex-quota/lib/oauth.js +486 -0
  312. package/tools/vendor/codex-quota/lib/paths.js +34 -0
  313. package/tools/vendor/codex-quota/lib/prompts.js +44 -0
  314. package/tools/vendor/codex-quota/lib/sync.js +1438 -0
  315. package/tools/vendor/codex-quota/lib/token-match.js +96 -0
  316. package/tools/vendor/codex-quota-manager/scripts/auto-switch.sh +500 -0
  317. package/tools/vendor/codex-quota-manager/scripts/batch-add.sh +123 -0
@@ -0,0 +1,1111 @@
1
+ /**
2
+ * Bars, boxes, usage lines, help text.
3
+ * Depends on: lib/constants.js, lib/color.js, lib/jwt.js
4
+ */
5
+
6
+ import { PRIMARY_CMD } from "./constants.js";
7
+ import { GREEN, RED, YELLOW, colorize, getPackageVersion } from "./color.js";
8
+ import { extractProfile } from "./jwt.js";
9
+ import { normalizeClaudeOrgId } from "./claude-usage.js";
10
+
11
+ export function parseWindow(window) {
12
+ if (!window) return null;
13
+ const used = window.used_percent ?? window.usedPercent ?? window.percent_used;
14
+ const remaining = window.remaining_percent ?? window.remainingPercent;
15
+ const resets = window.resets_at ?? window.resetsAt ?? window.reset_at;
16
+ const resetAfterSeconds = window.reset_after_seconds ?? window.resetAfterSeconds;
17
+ return { used, remaining, resets, resetAfterSeconds };
18
+ }
19
+
20
+ export function formatPercent(used, remaining) {
21
+ // Prefer showing remaining (matches Codex CLI /status display)
22
+ if (remaining !== undefined) return `${Math.round(remaining)}% left`;
23
+ if (used !== undefined) return `${Math.round(100 - used)}% left`;
24
+ return null;
25
+ }
26
+
27
+ // normalizeClaudeOrgId and isClaudeAuthError are imported from ./claude-usage.js
28
+
29
+ export function formatResetTime(seconds, style = "parentheses") {
30
+ if (!seconds) return "";
31
+
32
+ const resetDate = new Date(Date.now() + seconds * 1000);
33
+ const hours = Math.floor(seconds / 3600);
34
+ const mins = Math.floor((seconds % 3600) / 60);
35
+
36
+ // Format time as HH:MM
37
+ const timeStr = resetDate.toLocaleTimeString("en-US", {
38
+ hour: "2-digit",
39
+ minute: "2-digit",
40
+ hour12: false
41
+ });
42
+
43
+ // For display matching Codex CLI style
44
+ if (style === "inline") {
45
+ if (hours >= 24) {
46
+ // Show date for weekly+ resets: "resets 20:26 on 19 Jan"
47
+ const day = resetDate.getDate();
48
+ const month = resetDate.toLocaleDateString("en-US", { month: "short" });
49
+ return `(resets ${timeStr} on ${day} ${month})`;
50
+ }
51
+ // Same day: "resets 23:14"
52
+ return `(resets ${timeStr})`;
53
+ }
54
+
55
+ // Legacy parentheses style for JSON/other uses
56
+ if (hours > 24) {
57
+ const days = Math.floor(hours / 24);
58
+ return `(resets in ${days}d ${hours % 24}h)`;
59
+ }
60
+ if (hours > 0) {
61
+ return `(resets in ${hours}h ${mins}m)`;
62
+ }
63
+ return `(resets in ${mins}m)`;
64
+ }
65
+
66
+ export function formatUsage(payload) {
67
+ const usage = payload?.usage ?? payload;
68
+
69
+ // Handle new API format: rate_limit.primary_window / secondary_window
70
+ const rateLimit = usage?.rate_limit;
71
+ const primaryWindow = rateLimit?.primary_window ?? usage?.primary ?? usage?.session ?? usage?.fiveHour;
72
+ const secondaryWindow = rateLimit?.secondary_window ?? usage?.secondary ?? usage?.weekly ?? usage?.week;
73
+ const tertiaryWindow = usage?.tertiary ?? usage?.monthly ?? usage?.month;
74
+
75
+ const session = parseWindow(primaryWindow);
76
+ const weekly = parseWindow(secondaryWindow);
77
+ const monthly = parseWindow(tertiaryWindow);
78
+
79
+ const lines = [];
80
+
81
+ if (session) {
82
+ const pct = formatPercent(session.used, session.remaining);
83
+ const reset = session.resetAfterSeconds ? formatResetTime(session.resetAfterSeconds) :
84
+ session.resets ? `(resets ${session.resets})` : "";
85
+ lines.push(` Session: ${pct || "?"} ${reset}`);
86
+ }
87
+ if (weekly) {
88
+ const pct = formatPercent(weekly.used, weekly.remaining);
89
+ const reset = weekly.resetAfterSeconds ? formatResetTime(weekly.resetAfterSeconds) :
90
+ weekly.resets ? `(resets ${weekly.resets})` : "";
91
+ lines.push(` Weekly: ${pct || "?"} ${reset}`);
92
+ }
93
+ if (monthly) {
94
+ const pct = formatPercent(monthly.used, monthly.remaining);
95
+ const reset = monthly.resetAfterSeconds ? formatResetTime(monthly.resetAfterSeconds) :
96
+ monthly.resets ? `(resets ${monthly.resets})` : "";
97
+ lines.push(` Monthly: ${pct || "?"} ${reset}`);
98
+ }
99
+
100
+ // Handle credits
101
+ const credits = usage?.credits;
102
+ if (credits) {
103
+ const balance = credits.balance ?? credits.remaining;
104
+ if (balance !== undefined) {
105
+ lines.push(` Credits: ${parseFloat(balance).toFixed(2)} remaining`);
106
+ }
107
+ }
108
+
109
+ // Plan type
110
+ const planType = usage?.plan_type;
111
+ if (planType) {
112
+ lines.push(` Plan: ${planType}`);
113
+ }
114
+
115
+ return lines.length ? lines : [" (no usage data)"];
116
+ }
117
+
118
+ export function printBar(remaining, width = 20) {
119
+ // Bar shows remaining quota: full = 100% left, empty = 0% left (matches Codex CLI)
120
+ const filled = Math.round((remaining / 100) * width);
121
+ const empty = width - filled;
122
+ const bar = "█".repeat(filled) + "░".repeat(empty);
123
+ return `[${bar}]`;
124
+ }
125
+
126
+ // Box drawing characters
127
+ const BOX = {
128
+ topLeft: "╭",
129
+ topRight: "╮",
130
+ bottomLeft: "╰",
131
+ bottomRight: "╯",
132
+ horizontal: "─",
133
+ vertical: "│",
134
+ };
135
+
136
+ /**
137
+ * Draw a box around content lines
138
+ * @param {string[]} lines - Lines to display inside the box
139
+ * @param {number} minWidth - Minimum box width (default 70)
140
+ * @returns {string[]} Lines with box characters
141
+ */
142
+ export function drawBox(lines, minWidth = 70) {
143
+ // Calculate content width (max line length)
144
+ const contentWidth = Math.max(minWidth, ...lines.map(l => l.length)) + 2;
145
+
146
+ const output = [];
147
+
148
+ // Top border
149
+ output.push(BOX.topLeft + BOX.horizontal.repeat(contentWidth) + BOX.topRight);
150
+
151
+ // Content lines with padding
152
+ for (const line of lines) {
153
+ const padding = contentWidth - line.length - 1;
154
+ output.push(BOX.vertical + " " + line + " ".repeat(padding) + BOX.vertical);
155
+ }
156
+
157
+ // Bottom border
158
+ output.push(BOX.bottomLeft + BOX.horizontal.repeat(contentWidth) + BOX.bottomRight);
159
+
160
+ return output;
161
+ }
162
+
163
+ /**
164
+ * Build usage lines for an account (for box display)
165
+ * @param {object} account - Account object
166
+ * @param {object} payload - Usage payload from API
167
+ * @returns {string[]} Lines to display
168
+ */
169
+ export function buildAccountUsageLines(account, payload) {
170
+ const lines = [];
171
+ const usage = payload?.usage ?? payload;
172
+ const rateLimit = usage?.rate_limit;
173
+ const primaryWindow = rateLimit?.primary_window ?? usage?.primary ?? usage?.session ?? usage?.fiveHour;
174
+ const secondaryWindow = rateLimit?.secondary_window ?? usage?.secondary ?? usage?.weekly ?? usage?.week;
175
+ const session = parseWindow(primaryWindow);
176
+ const weekly = parseWindow(secondaryWindow);
177
+
178
+ // Extract profile info from token
179
+ const profile = extractProfile(account.access);
180
+ const planType = usage?.plan_type ?? profile.planType;
181
+ const planDisplay = planType ? ` (${planType})` : "";
182
+
183
+ // Header: Codex (label) <email> (plan) — matches Claude format
184
+ const labelDisplay = account.label ? ` (${account.label})` : "";
185
+ const emailDisplay = profile.email ? ` <${profile.email}>` : "";
186
+ lines.push(`Codex${labelDisplay}${emailDisplay}${planDisplay}`);
187
+ lines.push("");
188
+
189
+ if (payload.error) {
190
+ lines.push(`Error: ${payload.error}`);
191
+ if (account.source) {
192
+ lines.push(` Source: ${shortenPath(account.source)}`);
193
+ }
194
+ return lines;
195
+ }
196
+
197
+ // 5h limit bar (session/primary window)
198
+ if (session) {
199
+ const remaining = session.remaining ?? (session.used !== undefined ? 100 - session.used : null);
200
+ if (remaining !== null) {
201
+ const reset = session.resetAfterSeconds ? formatResetTime(session.resetAfterSeconds, "inline") : "";
202
+ lines.push(`5h limit: ${printBar(remaining)} ${Math.round(remaining)}% left ${reset}`);
203
+ }
204
+ }
205
+
206
+ // Weekly limit bar (secondary window)
207
+ if (weekly) {
208
+ const remaining = weekly.remaining ?? (weekly.used !== undefined ? 100 - weekly.used : null);
209
+ if (remaining !== null) {
210
+ const reset = weekly.resetAfterSeconds ? formatResetTime(weekly.resetAfterSeconds, "inline") : "";
211
+ lines.push(`Weekly limit: ${printBar(remaining)} ${Math.round(remaining)}% left ${reset}`);
212
+ }
213
+ }
214
+
215
+ if (account.source) {
216
+ lines.push(` Source: ${shortenPath(account.source)}`);
217
+ }
218
+
219
+ return lines;
220
+ }
221
+
222
+ export function formatClaudePercentLeft(percentLeft) {
223
+ if (percentLeft === null || percentLeft === undefined || Number.isNaN(percentLeft)) {
224
+ return "?";
225
+ }
226
+ return `${Math.round(percentLeft)}% left`;
227
+ }
228
+
229
+ export function normalizePercentUsed(value) {
230
+ if (value === null || value === undefined) return null;
231
+ let used = Number(value);
232
+ if (!Number.isFinite(used)) return null;
233
+
234
+ // Claude OAuth usage now reports percentage points (0-100).
235
+ // Keep integer values like 1 as 1% used; only treat fractional values
236
+ // in (0, 1) as ratio form for backward compatibility.
237
+ if (used > 0 && used < 1) {
238
+ used *= 100;
239
+ }
240
+
241
+ return Math.min(100, Math.max(0, used));
242
+ }
243
+
244
+ export function parseClaudeUtilizationWindow(window) {
245
+ if (!window || typeof window !== "object") return null;
246
+ const utilization = window.utilization ?? window.used_percent ?? window.usedPercent ?? window.percent_used;
247
+ const remainingPercent = window.remaining_percent ?? window.remainingPercent ?? window.percent_remaining;
248
+ const resetsAt = window.resets_at ?? window.resetsAt ?? window.reset_at ?? window.resetAt;
249
+ let remaining = null;
250
+ if (remainingPercent !== undefined) {
251
+ remaining = Number(remainingPercent);
252
+ } else {
253
+ const used = normalizePercentUsed(utilization);
254
+ if (used !== null) {
255
+ remaining = 100 - used;
256
+ }
257
+ }
258
+ if (remaining !== null && Number.isFinite(remaining)) {
259
+ remaining = Math.min(100, Math.max(0, remaining));
260
+ }
261
+ return { remaining, resetsAt };
262
+ }
263
+
264
+ export function formatResetAt(dateString) {
265
+ if (!dateString) return "";
266
+ const date = new Date(dateString);
267
+ if (Number.isNaN(date.getTime())) return "";
268
+ const seconds = Math.max(0, Math.floor((date.getTime() - Date.now()) / 1000));
269
+ return formatResetTime(seconds, "inline");
270
+ }
271
+
272
+ export function parseClaudeWindow(window) {
273
+ if (!window || typeof window !== "object") return null;
274
+ const usedPercent = window.used_percent ?? window.usedPercent ?? window.percent_used ?? window.percentUsed;
275
+ const remainingPercent = window.remaining_percent ?? window.remainingPercent ?? window.percent_remaining ?? window.percentRemaining;
276
+ const used = window.used ?? window.used_units ?? window.usedUnits ?? window.used_tokens ?? window.usedTokens;
277
+ const remaining = window.remaining ?? window.remaining_units ?? window.remainingUnits ?? window.remaining_tokens ?? window.remainingTokens;
278
+ const limit = window.limit ?? window.quota ?? window.total ?? window.max ?? window.maximum;
279
+ const resets = window.resets_at ?? window.resetsAt ?? window.reset_at ?? window.resetAt ?? window.reset;
280
+ const resetAfterSeconds = window.reset_after_seconds ?? window.resetAfterSeconds;
281
+
282
+ let percentLeft = null;
283
+ if (remainingPercent !== undefined) {
284
+ percentLeft = remainingPercent;
285
+ } else if (usedPercent !== undefined) {
286
+ percentLeft = 100 - usedPercent;
287
+ } else if (remaining !== undefined && Number.isFinite(limit) && limit > 0) {
288
+ percentLeft = (remaining / limit) * 100;
289
+ } else if (used !== undefined && Number.isFinite(limit) && limit > 0) {
290
+ percentLeft = (1 - used / limit) * 100;
291
+ }
292
+
293
+ return { percentLeft, used, remaining, limit, resets, resetAfterSeconds };
294
+ }
295
+
296
+ export function formatClaudeLabel(label) {
297
+ if (!label) return "";
298
+ return label
299
+ .replace(/_/g, " ")
300
+ .replace(/(^|\s)\S/g, (m) => m.toUpperCase())
301
+ .trim();
302
+ }
303
+
304
+ export function getClaudeUsageWindows(usage) {
305
+ if (!usage || typeof usage !== "object") return [];
306
+ const root = usage.usage ?? usage.quotas ?? usage.quota ?? usage;
307
+ const windows = [];
308
+
309
+ const seen = new Set();
310
+ const pushWindow = (label, window) => {
311
+ if (!window || typeof window !== "object") return;
312
+ if (seen.has(label)) return;
313
+ seen.add(label);
314
+ windows.push({ label, window });
315
+ };
316
+
317
+ pushWindow("Session", root.session ?? root.sessions ?? root.fiveHour ?? root.five_hour ?? root.primary);
318
+ pushWindow("Weekly", root.weekly ?? root.week ?? root.secondary);
319
+
320
+ const modelContainer = root.models ?? root.model ?? root.usage_by_model ?? root.model_usage;
321
+ if (modelContainer && typeof modelContainer === "object" && !Array.isArray(modelContainer)) {
322
+ for (const [key, value] of Object.entries(modelContainer)) {
323
+ pushWindow(formatClaudeLabel(key), value);
324
+ }
325
+ }
326
+
327
+ pushWindow("Opus", root.opus ?? root.model_opus ?? root.claude_opus);
328
+
329
+ return windows;
330
+ }
331
+
332
+ export function formatClaudeOverageLine(overage) {
333
+ if (!overage || typeof overage !== "object") return null;
334
+ const limit = overage.limit ?? overage.spend_limit ?? overage.spendLimit ?? overage.overage_spend_limit;
335
+ const used = overage.used ?? overage.spent ?? overage.spend ?? overage.amount_used;
336
+ const remaining = overage.remaining ?? (limit !== undefined && used !== undefined ? limit - used : undefined);
337
+ const enabled = overage.enabled ?? overage.is_enabled ?? overage.active;
338
+
339
+ const parts = [];
340
+ if (enabled !== undefined) {
341
+ parts.push(enabled ? "enabled" : "disabled");
342
+ }
343
+ if (limit !== undefined) {
344
+ parts.push(`limit ${limit}`);
345
+ }
346
+ if (remaining !== undefined) {
347
+ parts.push(`remaining ${remaining}`);
348
+ }
349
+ if (!parts.length) return null;
350
+ return `Overage: ${parts.join(", ")}`;
351
+ }
352
+
353
+ export function buildClaudeUsageLines(payload) {
354
+ const lines = [];
355
+
356
+ const account = payload?.account ?? {};
357
+ const email = account.email ?? account.email_address ?? account?.user?.email ?? account?.account?.email ?? null;
358
+ const membership = Array.isArray(account.memberships)
359
+ ? account.memberships.find(m => normalizeClaudeOrgId(m?.organization?.uuid) === normalizeClaudeOrgId(payload?.orgId))
360
+ : null;
361
+ // Support both old format (from account API) and new OAuth format (from credentials)
362
+ const plan = payload?.subscriptionType
363
+ ?? payload?.rateLimitTier
364
+ ?? account.plan
365
+ ?? account.plan_type
366
+ ?? account.planType
367
+ ?? account?.subscription?.plan
368
+ ?? membership?.organization?.rate_limit_tier
369
+ ?? (membership?.organization?.capabilities?.includes("claude_max") ? "claude_max" : null);
370
+ let planDisplay = null;
371
+ if (plan) {
372
+ planDisplay = formatClaudeLabel(
373
+ String(plan)
374
+ .replace(/^default_/, "")
375
+ .replace(/_\d+x$/i, "")
376
+ );
377
+ }
378
+ const label = payload?.label ? ` (${payload.label})` : "";
379
+ const header = `Claude${label}${email ? ` <${email}>` : ""}${planDisplay ? ` (${planDisplay})` : ""}`;
380
+
381
+ lines.push(header);
382
+ lines.push("");
383
+
384
+ if (!payload || payload.success === false) {
385
+ lines.push(`Error: ${payload?.error ?? "Claude usage unavailable"}`);
386
+ return lines;
387
+ }
388
+
389
+ const usage = payload?.usage;
390
+ let renderedUsage = false;
391
+ if (usage && typeof usage === "object") {
392
+ const fiveHour = parseClaudeUtilizationWindow(usage.five_hour ?? usage.fiveHour);
393
+ if (fiveHour && fiveHour.remaining !== null) {
394
+ const reset = formatResetAt(fiveHour.resetsAt);
395
+ lines.push(`5h limit: ${printBar(fiveHour.remaining)} ${Math.round(fiveHour.remaining)}% left ${reset}`.trimEnd());
396
+ renderedUsage = true;
397
+ }
398
+ const weekly = parseClaudeUtilizationWindow(usage.seven_day ?? usage.sevenDay);
399
+ if (weekly && weekly.remaining !== null) {
400
+ const reset = formatResetAt(weekly.resetsAt);
401
+ lines.push(`Weekly limit: ${printBar(weekly.remaining)} ${Math.round(weekly.remaining)}% left ${reset}`.trimEnd());
402
+ renderedUsage = true;
403
+ }
404
+ const opus = parseClaudeUtilizationWindow(usage.seven_day_opus ?? usage.sevenDayOpus);
405
+ if (opus && opus.remaining !== null) {
406
+ const reset = formatResetAt(opus.resetsAt);
407
+ lines.push(`Opus weekly: ${printBar(opus.remaining)} ${Math.round(opus.remaining)}% left ${reset}`.trimEnd());
408
+ renderedUsage = true;
409
+ }
410
+ const sonnet = parseClaudeUtilizationWindow(usage.seven_day_sonnet ?? usage.sevenDaySonnet);
411
+ if (sonnet && sonnet.remaining !== null) {
412
+ const reset = formatResetAt(sonnet.resetsAt);
413
+ lines.push(`Sonnet weekly: ${printBar(sonnet.remaining)} ${Math.round(sonnet.remaining)}% left ${reset}`.trimEnd());
414
+ renderedUsage = true;
415
+ }
416
+ }
417
+
418
+ if (!renderedUsage) {
419
+ const windows = getClaudeUsageWindows(payload.usage);
420
+ if (windows.length) {
421
+ for (const { label, window } of windows) {
422
+ const parsed = parseClaudeWindow(window);
423
+ if (!parsed) continue;
424
+ const reset = parsed.resetAfterSeconds
425
+ ? formatResetTime(parsed.resetAfterSeconds)
426
+ : parsed.resets ? `(resets ${parsed.resets})` : "";
427
+ lines.push(` ${label}: ${formatClaudePercentLeft(parsed.percentLeft)} ${reset}`.trimEnd());
428
+ }
429
+ } else {
430
+ lines.push(" Usage: (no usage data)");
431
+ }
432
+ }
433
+
434
+ const overageLine = formatClaudeOverageLine(payload.overage);
435
+ if (overageLine) {
436
+ lines.push(` ${overageLine}`);
437
+ }
438
+
439
+ if (payload.orgId) {
440
+ lines.push(` Org: ${payload.orgId}`);
441
+ }
442
+
443
+ if (payload.source) {
444
+ lines.push(` Source: ${shortenPath(payload.source)}`);
445
+ }
446
+
447
+ if (payload.errors) {
448
+ const parts = Object.entries(payload.errors).map(([key, value]) => `${key}=${value}`);
449
+ lines.push(` Partial errors: ${parts.join(", ")}`);
450
+ }
451
+
452
+ return lines;
453
+ }
454
+
455
+ export function printHelp() {
456
+ console.log(`${PRIMARY_CMD} - Manage and monitor OpenAI Codex and Claude accounts
457
+ Version: ${getPackageVersion()}
458
+
459
+ Usage:
460
+ ${PRIMARY_CMD} <namespace> [command] [options]
461
+ ${PRIMARY_CMD} [label] Check quota for all accounts (Codex + Claude)
462
+
463
+ Namespaces:
464
+ codex Manage OpenAI Codex accounts
465
+ claude Manage Claude accounts
466
+
467
+ Options:
468
+ --json Output in JSON format
469
+ --local Use only stored account files; skip harness token checks
470
+ --dry-run Preview sync without writing files
471
+ --no-browser Print auth URL instead of opening browser
472
+ --no-color Disable colored output
473
+ --version, -v Show version number
474
+ --help, -h Show this help
475
+
476
+ Examples:
477
+ ${PRIMARY_CMD} Check quota for all accounts (Codex + Claude)
478
+ ${PRIMARY_CMD} codex Show Codex command help
479
+ ${PRIMARY_CMD} claude Show Claude command help
480
+ ${PRIMARY_CMD} codex quota Check quota for Codex accounts
481
+ ${PRIMARY_CMD} claude quota Check quota for Claude accounts
482
+ ${PRIMARY_CMD} codex add work Add Codex account with label "work"
483
+ ${PRIMARY_CMD} claude add work Add Claude credential with label "work"
484
+ ${PRIMARY_CMD} codex reauth work Re-authenticate existing "work" account
485
+ ${PRIMARY_CMD} claude reauth work Re-authenticate existing "work" account
486
+ ${PRIMARY_CMD} codex switch work Switch Codex/OpenCode/pi to "work"
487
+ ${PRIMARY_CMD} claude switch work Switch Claude Code/OpenCode/pi to "work"
488
+ ${PRIMARY_CMD} codex sync Sync active Codex account to CLI auth files
489
+ ${PRIMARY_CMD} codex sync --dry-run Preview Codex sync without writing
490
+ ${PRIMARY_CMD} claude sync --dry-run Preview Claude sync without writing
491
+
492
+ Account sources (checked in order):
493
+ 1. CODEX_ACCOUNTS env var (JSON array)
494
+ 2. ~/.codex-accounts.json
495
+ 3. ~/.opencode/openai-codex-auth-accounts.json
496
+ 4. ~/.codex/auth.json (Codex CLI format)
497
+
498
+ OpenCode & pi Integration:
499
+ The 'switch' and 'sync' commands update Codex CLI (~/.codex/auth.json) plus
500
+ OpenCode (~/.local/share/opencode/auth.json) and pi (~/.pi/agent/auth.json)
501
+ authentication files when they exist, enabling seamless account switching.
502
+ The activeLabel marker in multi-account files is used for sync and divergence
503
+ warnings in list/quota output.
504
+
505
+ Run '${PRIMARY_CMD} <namespace> <command> --help' for help on a specific command.
506
+ `);
507
+ }
508
+
509
+ export function printHelpCodex() {
510
+ console.log(`${PRIMARY_CMD} codex - Manage OpenAI Codex accounts
511
+
512
+ Usage:
513
+ ${PRIMARY_CMD} codex [command] [options]
514
+
515
+ Commands:
516
+ quota [label] Check usage quota (default command)
517
+ add [label] Add a new account via OAuth browser flow
518
+ reauth <label> Re-authenticate an existing account via OAuth
519
+ switch <label> Switch active account for Codex CLI, OpenCode, and pi
520
+ sync Sync activeLabel to Codex CLI, OpenCode, and pi
521
+ list List all accounts from all sources
522
+ remove <label> Remove an account from storage
523
+
524
+ Options:
525
+ --json Output in JSON format
526
+ --dry-run Preview sync without writing files
527
+ --no-browser Print auth URL instead of opening browser
528
+ --no-color Disable colored output
529
+ --help, -h Show this help
530
+
531
+ Examples:
532
+ ${PRIMARY_CMD} codex Check quota for Codex accounts
533
+ ${PRIMARY_CMD} codex personal Check quota for "personal" account
534
+ ${PRIMARY_CMD} codex add work Add new account with label "work"
535
+ ${PRIMARY_CMD} codex reauth work Re-authenticate "work" account
536
+ ${PRIMARY_CMD} codex switch personal Switch to "personal" account
537
+ ${PRIMARY_CMD} codex list List all configured accounts
538
+ ${PRIMARY_CMD} codex remove old Remove "old" account
539
+ ${PRIMARY_CMD} codex sync Sync the activeLabel account
540
+ ${PRIMARY_CMD} codex sync --dry-run Preview sync without writing
541
+
542
+ Notes:
543
+ - switch and sync update activeLabel in ~/.codex-accounts.json when available
544
+ - list/quota warn when CLI auth diverges (use '${PRIMARY_CMD} codex sync')
545
+ `);
546
+ }
547
+
548
+ export function printHelpClaude() {
549
+ console.log(`${PRIMARY_CMD} claude - Manage Claude credentials
550
+
551
+ Usage:
552
+ ${PRIMARY_CMD} claude [command] [options]
553
+
554
+ Commands:
555
+ quota [label] Check Claude usage (default command)
556
+ add [label] Add a Claude credential (via OAuth or manual entry)
557
+ reauth <label> Re-authenticate an existing Claude account via OAuth
558
+ switch <label> Switch Claude Code, OpenCode, and pi credentials
559
+ sync Sync activeLabel to Claude Code, OpenCode, and pi
560
+ list List Claude credentials
561
+ remove <label> Remove a Claude credential from storage
562
+
563
+ Options:
564
+ --json Output result in JSON format
565
+ --dry-run Preview sync without writing files
566
+ --oauth Use OAuth browser authentication (recommended)
567
+ --manual Use manual token entry
568
+ --no-browser Print OAuth URL instead of opening browser
569
+ --help, -h Show this help
570
+
571
+ Examples:
572
+ ${PRIMARY_CMD} claude Check Claude usage
573
+ ${PRIMARY_CMD} claude quota work Check Claude usage for "work"
574
+ ${PRIMARY_CMD} claude add Add Claude credential (prompts for method)
575
+ ${PRIMARY_CMD} claude add work --oauth Add via OAuth browser flow
576
+ ${PRIMARY_CMD} claude reauth work Re-authenticate "work" account
577
+ ${PRIMARY_CMD} claude switch work Switch Claude Code/OpenCode/pi to "work"
578
+ ${PRIMARY_CMD} claude list List Claude credentials
579
+ ${PRIMARY_CMD} claude remove old Remove Claude credential "old"
580
+ ${PRIMARY_CMD} claude sync Sync the activeLabel account
581
+ ${PRIMARY_CMD} claude sync --dry-run Preview sync without writing
582
+
583
+ Notes:
584
+ - switch and sync update activeLabel in ~/.claude-accounts.json when available
585
+ - session-key-only accounts cannot be synced (OAuth required)
586
+ `);
587
+ }
588
+
589
+ export function printHelpClaudeAdd() {
590
+ console.log(`${PRIMARY_CMD} claude add - Add a Claude credential
591
+
592
+ Usage:
593
+ ${PRIMARY_CMD} claude add [label] [options]
594
+
595
+ Arguments:
596
+ label Optional label for the Claude credential (e.g., "work", "personal")
597
+
598
+ Options:
599
+ --oauth Use OAuth browser authentication (recommended)
600
+ Opens browser for secure authentication
601
+ --manual Use manual token entry
602
+ Paste sessionKey or OAuth token directly
603
+ --no-browser Print OAuth URL instead of opening browser
604
+ Use this in headless/SSH environments
605
+ --json Output result in JSON format
606
+ --help, -h Show this help
607
+
608
+ Description:
609
+ Adds a Claude credential to ~/.claude-accounts.json.
610
+
611
+ OAuth flow (recommended):
612
+ 1. Opens browser for authentication at claude.ai
613
+ 2. User copies code#state from browser
614
+ 3. Tool exchanges code for tokens automatically
615
+
616
+ Manual flow:
617
+ Prompts for sessionKey or OAuth token (one is required).
618
+
619
+ Examples:
620
+ ${PRIMARY_CMD} claude add Interactive (prompts for method)
621
+ ${PRIMARY_CMD} claude add work --oauth OAuth browser flow
622
+ ${PRIMARY_CMD} claude add work --manual Manual token entry
623
+ ${PRIMARY_CMD} claude add work --oauth --no-browser OAuth without opening browser
624
+ ${PRIMARY_CMD} claude add work --json JSON output for scripting
625
+ `);
626
+ }
627
+
628
+ export function printHelpClaudeReauth() {
629
+ console.log(`${PRIMARY_CMD} claude reauth - Re-authenticate an existing Claude account
630
+
631
+ Usage:
632
+ ${PRIMARY_CMD} claude reauth <label> [options]
633
+
634
+ Arguments:
635
+ label Required. Label of the Claude account to re-authenticate
636
+
637
+ Options:
638
+ --no-browser Print the OAuth URL instead of opening browser
639
+ Use this in headless/SSH environments
640
+ --json Output result in JSON format
641
+ --help, -h Show this help
642
+
643
+ Description:
644
+ Re-authenticates an existing Claude account via the OAuth browser flow.
645
+ This is useful when your tokens have expired and cannot be refreshed,
646
+ or when you need to reset your authentication.
647
+
648
+ Unlike 'add', this command:
649
+ - Requires an existing account with the specified label
650
+ - Updates the existing entry instead of creating a new one
651
+ - Preserves any extra fields in the account configuration
652
+ - Always uses OAuth (no manual token entry)
653
+
654
+ If the re-authenticated account is the active account, CLI auth files
655
+ (Claude Code, OpenCode, pi) will also be updated automatically.
656
+
657
+ Examples:
658
+ ${PRIMARY_CMD} claude reauth work Re-authenticate "work" account
659
+ ${PRIMARY_CMD} claude reauth work --no-browser Print URL for manual browser auth
660
+ ${PRIMARY_CMD} claude reauth work --json JSON output for scripting
661
+
662
+ See also:
663
+ ${PRIMARY_CMD} claude add Add a new Claude account
664
+ ${PRIMARY_CMD} claude list Show all configured Claude accounts
665
+ `);
666
+ }
667
+
668
+ export function printHelpClaudeSwitch() {
669
+ console.log(`${PRIMARY_CMD} claude switch - Switch Claude credentials
670
+
671
+ Usage:
672
+ ${PRIMARY_CMD} claude switch <label> [options]
673
+
674
+ Arguments:
675
+ label Required. Label of the Claude credential to switch to
676
+
677
+ Options:
678
+ --json Output result in JSON format
679
+ --help, -h Show this help
680
+
681
+ Description:
682
+ Updates Claude Code (~/.claude/.credentials.json) and, when available,
683
+ OpenCode (~/.local/share/opencode/auth.json) plus pi (~/.pi/agent/auth.json).
684
+
685
+ Requires an OAuth-based Claude credential (add with --oauth).
686
+ Also updates activeLabel in ~/.claude-accounts.json when available.
687
+
688
+ Examples:
689
+ ${PRIMARY_CMD} claude switch work
690
+ ${PRIMARY_CMD} claude switch work --json
691
+
692
+ See also:
693
+ ${PRIMARY_CMD} claude sync
694
+ `);
695
+ }
696
+
697
+ export function printHelpClaudeSync() {
698
+ console.log(`${PRIMARY_CMD} claude sync - Sync activeLabel to Claude auth files
699
+
700
+ Usage:
701
+ ${PRIMARY_CMD} claude sync [options]
702
+
703
+ Options:
704
+ --dry-run Preview what would be synced without writing files
705
+ --json Output result in JSON format
706
+ --help, -h Show this help
707
+
708
+ Description:
709
+ Pushes the activeLabel Claude account from ~/.claude-accounts.json to:
710
+ - Claude Code (~/.claude/.credentials.json)
711
+ - OpenCode (~/.local/share/opencode/auth.json) when present
712
+ - pi (~/.pi/agent/auth.json) when present
713
+
714
+ Only OAuth-based accounts can be synced. Session-key-only accounts are
715
+ skipped with a warning.
716
+
717
+ Examples:
718
+ ${PRIMARY_CMD} claude sync
719
+ ${PRIMARY_CMD} claude sync --dry-run
720
+ ${PRIMARY_CMD} claude sync --json
721
+
722
+ See also:
723
+ ${PRIMARY_CMD} claude switch <label>
724
+ ${PRIMARY_CMD} claude list
725
+ `);
726
+ }
727
+
728
+ export function printHelpClaudeList() {
729
+ console.log(`${PRIMARY_CMD} claude list - List Claude credentials
730
+
731
+ Usage:
732
+ ${PRIMARY_CMD} claude list [options]
733
+
734
+ Options:
735
+ --json Output in JSON format
736
+ --local Skip harness token checks and divergence warnings
737
+ --help, -h Show this help
738
+
739
+ Description:
740
+ Lists Claude credentials stored in CLAUDE_ACCOUNTS or ~/.claude-accounts.json.
741
+ The activeLabel account is marked with '*'.
742
+ OAuth-based accounts are checked for divergence in Claude CLI stores.
743
+ Use --local to suppress harness checks and only use stored account files.
744
+
745
+ Examples:
746
+ ${PRIMARY_CMD} claude list
747
+ ${PRIMARY_CMD} claude list --json
748
+ `);
749
+ }
750
+
751
+ export function printHelpClaudeRemove() {
752
+ console.log(`${PRIMARY_CMD} claude remove - Remove a Claude credential
753
+
754
+ Usage:
755
+ ${PRIMARY_CMD} claude remove <label> [options]
756
+
757
+ Arguments:
758
+ label Required. Label of the Claude credential to remove
759
+
760
+ Options:
761
+ --json Output result in JSON format (skips confirmation)
762
+ --help, -h Show this help
763
+
764
+ Description:
765
+ Removes a Claude credential from ~/.claude-accounts.json.
766
+ Credentials stored in CLAUDE_ACCOUNTS env var cannot be removed via CLI.
767
+
768
+ Examples:
769
+ ${PRIMARY_CMD} claude remove old
770
+ ${PRIMARY_CMD} claude remove work --json
771
+ `);
772
+ }
773
+
774
+ export function printHelpClaudeQuota() {
775
+ console.log(`${PRIMARY_CMD} claude quota - Check Claude usage quota
776
+
777
+ Usage:
778
+ ${PRIMARY_CMD} claude quota [label] [options]
779
+
780
+ Arguments:
781
+ label Optional. Check quota for a specific Claude credential
782
+
783
+ Options:
784
+ --json Output in JSON format
785
+ --local Skip harness token checks and divergence warnings
786
+ --help, -h Show this help
787
+
788
+ Description:
789
+ Displays usage statistics for Claude accounts. Tokens are refreshed when
790
+ available. Uses OAuth credentials when possible and falls back to legacy
791
+ session credentials.
792
+ OAuth-based accounts are checked for divergence in Claude CLI stores.
793
+ Use --local to suppress harness checks and only use stored account files.
794
+
795
+ Examples:
796
+ ${PRIMARY_CMD} claude quota
797
+ ${PRIMARY_CMD} claude quota work
798
+ ${PRIMARY_CMD} claude quota --json
799
+ `);
800
+ }
801
+
802
+ export function printHelpAdd() {
803
+ console.log(`${PRIMARY_CMD} codex add - Add a new account via OAuth browser flow
804
+
805
+ Usage:
806
+ ${PRIMARY_CMD} codex add [label] [options]
807
+
808
+ Arguments:
809
+ label Optional label for the account (e.g., "work", "personal")
810
+ If not provided, derived from email address
811
+
812
+ Options:
813
+ --no-browser Print the auth URL instead of opening browser
814
+ Use this in headless/SSH environments
815
+ --json Output result in JSON format
816
+ --help, -h Show this help
817
+
818
+ Description:
819
+ Authenticates with OpenAI via OAuth in your browser and saves the
820
+ account credentials to ~/.codex-accounts.json.
821
+
822
+ The OAuth flow uses PKCE for security. A local server is started on
823
+ port 1455 to receive the authentication callback.
824
+
825
+ Examples:
826
+ ${PRIMARY_CMD} codex add Add account (label from email)
827
+ ${PRIMARY_CMD} codex add work Add account with label "work"
828
+ ${PRIMARY_CMD} codex add --no-browser Print URL for manual browser auth
829
+
830
+ Environment:
831
+ SSH/headless environments are auto-detected. The URL will be printed
832
+ instead of opening a browser when SSH_CLIENT or SSH_TTY is set, or
833
+ when DISPLAY/WAYLAND_DISPLAY is missing on Linux.
834
+ `);
835
+ }
836
+
837
+ export function printHelpCodexReauth() {
838
+ console.log(`${PRIMARY_CMD} codex reauth - Re-authenticate an existing account
839
+
840
+ Usage:
841
+ ${PRIMARY_CMD} codex reauth <label> [options]
842
+
843
+ Arguments:
844
+ label Required. Label of the account to re-authenticate
845
+
846
+ Options:
847
+ --no-browser Print the auth URL instead of opening browser
848
+ Use this in headless/SSH environments
849
+ --json Output result in JSON format
850
+ --help, -h Show this help
851
+
852
+ Description:
853
+ Re-authenticates an existing Codex account via the OAuth browser flow.
854
+ This is useful when your tokens have expired and cannot be refreshed,
855
+ or when you need to reset your authentication.
856
+
857
+ Unlike 'add', this command:
858
+ - Requires an existing account with the specified label
859
+ - Updates the existing entry instead of creating a new one
860
+ - Preserves any extra fields in the account configuration
861
+
862
+ If the re-authenticated account is the active account, CLI auth files
863
+ (Codex CLI, OpenCode, pi) will also be updated automatically.
864
+
865
+ Examples:
866
+ ${PRIMARY_CMD} codex reauth work Re-authenticate "work" account
867
+ ${PRIMARY_CMD} codex reauth work --no-browser Print URL for manual browser auth
868
+ ${PRIMARY_CMD} codex reauth work --json JSON output for scripting
869
+
870
+ See also:
871
+ ${PRIMARY_CMD} codex add Add a new account
872
+ ${PRIMARY_CMD} codex list Show all configured accounts
873
+ `);
874
+ }
875
+
876
+ export function printHelpSwitch() {
877
+ console.log(`${PRIMARY_CMD} codex switch - Switch the active account
878
+
879
+ Usage:
880
+ ${PRIMARY_CMD} codex switch <label> [options]
881
+
882
+ Arguments:
883
+ label Required. Label of the account to switch to
884
+
885
+ Options:
886
+ --json Output result in JSON format
887
+ --help, -h Show this help
888
+
889
+ Description:
890
+ Switches the active OpenAI account for Codex CLI, OpenCode, and pi.
891
+
892
+ This command updates authentication files when they exist:
893
+ 1. ~/.codex/auth.json - Used by Codex CLI
894
+ 2. ~/.local/share/opencode/auth.json - Used by OpenCode (if exists)
895
+ 3. ~/.pi/agent/auth.json - Used by pi (if exists)
896
+
897
+ The OpenCode auth file location respects XDG_DATA_HOME if set.
898
+ If the optional auth files don't exist, only the Codex CLI file is updated.
899
+
900
+ Also updates activeLabel in your multi-account file when available.
901
+
902
+ If the token is expired, it will be refreshed before switching.
903
+ Any existing OPENAI_API_KEY in auth.json is preserved.
904
+
905
+ Examples:
906
+ ${PRIMARY_CMD} codex switch personal Switch to "personal" account
907
+ ${PRIMARY_CMD} codex switch work --json Switch to "work" with JSON output
908
+
909
+ See also:
910
+ ${PRIMARY_CMD} codex list Show all available accounts and their labels
911
+ ${PRIMARY_CMD} codex sync Re-sync activeLabel to CLI auth files
912
+ `);
913
+ }
914
+
915
+ export function printHelpCodexSync() {
916
+ console.log(`${PRIMARY_CMD} codex sync - Sync activeLabel to CLI auth files
917
+
918
+ Usage:
919
+ ${PRIMARY_CMD} codex sync [options]
920
+
921
+ Options:
922
+ --dry-run Preview what would be synced without writing files
923
+ --json Output result in JSON format
924
+ --help, -h Show this help
925
+
926
+ Description:
927
+ Pushes the activeLabel account from your multi-account file to:
928
+ - Codex CLI (~/.codex/auth.json)
929
+ - OpenCode (~/.local/share/opencode/auth.json) when present
930
+ - pi (~/.pi/agent/auth.json) when present
931
+
932
+ This is useful after a native CLI login has diverged from the tracked
933
+ activeLabel account.
934
+
935
+ Examples:
936
+ ${PRIMARY_CMD} codex sync
937
+ ${PRIMARY_CMD} codex sync --dry-run
938
+ ${PRIMARY_CMD} codex sync --json
939
+
940
+ See also:
941
+ ${PRIMARY_CMD} codex switch <label>
942
+ ${PRIMARY_CMD} codex list
943
+ `);
944
+ }
945
+
946
+ export function printHelpList() {
947
+ console.log(`${PRIMARY_CMD} codex list - List all configured accounts
948
+
949
+ Usage:
950
+ ${PRIMARY_CMD} codex list [options]
951
+
952
+ Options:
953
+ --json Output in JSON format
954
+ --local Skip harness token checks and divergence warnings
955
+ --help, -h Show this help
956
+
957
+ Description:
958
+ Lists all accounts from all configured sources with details:
959
+ - Label and email address
960
+ - Plan type (plus, free, etc.)
961
+ - Token expiry status
962
+ - Source file location
963
+ - Active indicator (* for the activeLabel account)
964
+ Accounts are deduplicated by email for display and prefer the
965
+ activeLabel account when duplicates exist.
966
+ If CLI auth diverges from activeLabel, a warning is shown with a sync hint.
967
+ Use --local to suppress harness checks and only use stored account files.
968
+
969
+ Output columns:
970
+ * = active Active account from activeLabel
971
+ ~ = CLI auth CLI account when it diverges from activeLabel
972
+ label Account identifier
973
+ <email> Email address from token
974
+ Plan ChatGPT plan type
975
+ Expires Token expiry (e.g., "9d 17h", "Expired")
976
+ Source File path where account is stored
977
+
978
+ Examples:
979
+ ${PRIMARY_CMD} codex list Show all accounts
980
+ ${PRIMARY_CMD} codex list --json Get JSON output for scripting
981
+ `);
982
+ }
983
+
984
+ export function printHelpRemove() {
985
+ console.log(`${PRIMARY_CMD} codex remove - Remove an account from storage
986
+
987
+ Usage:
988
+ ${PRIMARY_CMD} codex remove <label> [options]
989
+
990
+ Arguments:
991
+ label Required. Label of the account to remove
992
+
993
+ Options:
994
+ --json Output result in JSON format (skips confirmation)
995
+ --help, -h Show this help
996
+
997
+ Description:
998
+ Removes an account from the multi-account storage file.
999
+
1000
+ - For accounts in ~/.codex-accounts.json: removes from the file
1001
+ - For the codex-cli account (~/.codex/auth.json): deletes the file
1002
+ - For accounts in CODEX_ACCOUNTS env var: shows error (modify env directly)
1003
+
1004
+ Safety:
1005
+ - Prompts for confirmation before removing (unless --json)
1006
+ - Warns when removing the last account in a file
1007
+ - Warns when removing the codex-cli account (clears authentication)
1008
+
1009
+ Examples:
1010
+ ${PRIMARY_CMD} codex remove old Remove "old" account with confirmation
1011
+ ${PRIMARY_CMD} codex remove work --json Remove "work" account (no prompt)
1012
+
1013
+ See also:
1014
+ ${PRIMARY_CMD} codex list Show all accounts and their sources
1015
+ `);
1016
+ }
1017
+
1018
+ export function printHelpQuota() {
1019
+ console.log(`${PRIMARY_CMD} codex quota - Check usage quota for accounts
1020
+
1021
+ Usage:
1022
+ ${PRIMARY_CMD} codex quota [label] [options]
1023
+
1024
+ Arguments:
1025
+ label Optional. Check quota for a specific account only
1026
+ If not provided, shows quota for all accounts
1027
+
1028
+ Options:
1029
+ --json Output in JSON format
1030
+ --local Skip harness token checks and divergence warnings
1031
+ --help, -h Show this help
1032
+
1033
+ Description:
1034
+ Displays usage statistics for OpenAI Codex and Claude accounts:
1035
+ - Session usage (queries per session)
1036
+ - Weekly usage (queries per 7-day period)
1037
+ - Available credits
1038
+
1039
+ This command shows Codex usage only. Use '${PRIMARY_CMD} claude quota' for Claude.
1040
+
1041
+ Accounts are deduplicated by ID to avoid showing the same account
1042
+ multiple times when sourced from different files.
1043
+
1044
+ Tokens are automatically refreshed if expired.
1045
+ If CLI auth diverges from activeLabel, a warning is shown with a sync hint.
1046
+ Use --local to suppress these checks and only use stored account files.
1047
+
1048
+ Examples:
1049
+ ${PRIMARY_CMD} codex quota Check all Codex accounts
1050
+ ${PRIMARY_CMD} codex quota personal Check "personal" account only
1051
+ ${PRIMARY_CMD} codex quota --json JSON output for all Codex accounts
1052
+ ${PRIMARY_CMD} codex quota work --json JSON output for "work" account
1053
+ ${PRIMARY_CMD} claude quota Check Claude accounts
1054
+ `);
1055
+ }
1056
+
1057
+
1058
+ import { homedir } from "node:os";
1059
+
1060
+ /**
1061
+ * Format expiry time as human-readable duration
1062
+ * @param {number | undefined} expires - Expiry timestamp in milliseconds
1063
+ * @returns {{ status: string, display: string }} Status and display string
1064
+ */
1065
+ export function formatExpiryStatus(expires) {
1066
+ if (!expires) {
1067
+ return { status: "unknown", display: "Unknown" };
1068
+ }
1069
+
1070
+ const now = Date.now();
1071
+ const diff = expires - now;
1072
+
1073
+ if (diff <= 0) {
1074
+ return { status: "expired", display: "Expired" };
1075
+ }
1076
+
1077
+ // Warn if expiring within 5 minutes
1078
+ if (diff < 5 * 60 * 1000) {
1079
+ const mins = Math.ceil(diff / 60000);
1080
+ return { status: "expiring", display: `Expiring in ${mins}m` };
1081
+ }
1082
+
1083
+ // Format remaining time
1084
+ const hours = Math.floor(diff / (60 * 60 * 1000));
1085
+ const mins = Math.floor((diff % (60 * 60 * 1000)) / 60000);
1086
+
1087
+ if (hours > 24) {
1088
+ const days = Math.floor(hours / 24);
1089
+ const remainingHours = hours % 24;
1090
+ return { status: "valid", display: `${days}d ${remainingHours}h` };
1091
+ }
1092
+
1093
+ if (hours > 0) {
1094
+ return { status: "valid", display: `${hours}h ${mins}m` };
1095
+ }
1096
+
1097
+ return { status: "valid", display: `${mins}m` };
1098
+ }
1099
+
1100
+ /**
1101
+ * Shorten a path for display (replace home directory with ~)
1102
+ * @param {string} filePath - Full file path
1103
+ * @returns {string} Shortened path
1104
+ */
1105
+ export function shortenPath(filePath) {
1106
+ const home = homedir();
1107
+ if (filePath.startsWith(home)) {
1108
+ return "~" + filePath.slice(home.length);
1109
+ }
1110
+ return filePath;
1111
+ }