@gokulvenkatareddy/cortex 0.1.7

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 (299) hide show
  1. package/README.md +1295 -0
  2. package/apps/octogent/.github/workflows/ci.yml +40 -0
  3. package/apps/octogent/.shims/claude +4 -0
  4. package/apps/octogent/AGENTS.md +71 -0
  5. package/apps/octogent/CONTRIBUTING.md +72 -0
  6. package/apps/octogent/LICENSE +21 -0
  7. package/apps/octogent/README.md +184 -0
  8. package/apps/octogent/apps/api/AGENTS.md +32 -0
  9. package/apps/octogent/apps/api/package.json +19 -0
  10. package/apps/octogent/apps/api/src/agentStateDetection.ts +181 -0
  11. package/apps/octogent/apps/api/src/claudeSessionScanner.ts +235 -0
  12. package/apps/octogent/apps/api/src/claudeSkills.ts +182 -0
  13. package/apps/octogent/apps/api/src/claudeUsage.ts +922 -0
  14. package/apps/octogent/apps/api/src/cli.ts +595 -0
  15. package/apps/octogent/apps/api/src/codeIntelStore.ts +46 -0
  16. package/apps/octogent/apps/api/src/codexUsage.ts +278 -0
  17. package/apps/octogent/apps/api/src/createApiServer/codeIntelRoutes.ts +60 -0
  18. package/apps/octogent/apps/api/src/createApiServer/conversationRoutes.ts +128 -0
  19. package/apps/octogent/apps/api/src/createApiServer/deckRoutes.ts +873 -0
  20. package/apps/octogent/apps/api/src/createApiServer/gitParsers.ts +140 -0
  21. package/apps/octogent/apps/api/src/createApiServer/gitRoutes.ts +214 -0
  22. package/apps/octogent/apps/api/src/createApiServer/miscRoutes.ts +316 -0
  23. package/apps/octogent/apps/api/src/createApiServer/monitorParsers.ts +137 -0
  24. package/apps/octogent/apps/api/src/createApiServer/monitorRoutes.ts +95 -0
  25. package/apps/octogent/apps/api/src/createApiServer/requestHandler.ts +311 -0
  26. package/apps/octogent/apps/api/src/createApiServer/requestParsers.ts +25 -0
  27. package/apps/octogent/apps/api/src/createApiServer/routeHelpers.ts +97 -0
  28. package/apps/octogent/apps/api/src/createApiServer/security.ts +70 -0
  29. package/apps/octogent/apps/api/src/createApiServer/terminalParsers.ts +167 -0
  30. package/apps/octogent/apps/api/src/createApiServer/terminalRoutes.ts +315 -0
  31. package/apps/octogent/apps/api/src/createApiServer/types.ts +24 -0
  32. package/apps/octogent/apps/api/src/createApiServer/uiStateParsers.ts +255 -0
  33. package/apps/octogent/apps/api/src/createApiServer/upgradeHandler.ts +38 -0
  34. package/apps/octogent/apps/api/src/createApiServer/usageRoutes.ts +84 -0
  35. package/apps/octogent/apps/api/src/createApiServer.ts +176 -0
  36. package/apps/octogent/apps/api/src/deck/readDeckTentacles.ts +595 -0
  37. package/apps/octogent/apps/api/src/githubRepoSummary.ts +397 -0
  38. package/apps/octogent/apps/api/src/logging.ts +9 -0
  39. package/apps/octogent/apps/api/src/monitor/defaults.ts +3 -0
  40. package/apps/octogent/apps/api/src/monitor/index.ts +8 -0
  41. package/apps/octogent/apps/api/src/monitor/repository.ts +303 -0
  42. package/apps/octogent/apps/api/src/monitor/service.ts +349 -0
  43. package/apps/octogent/apps/api/src/monitor/types.ts +120 -0
  44. package/apps/octogent/apps/api/src/monitor/xProvider.ts +587 -0
  45. package/apps/octogent/apps/api/src/projectPersistence.ts +377 -0
  46. package/apps/octogent/apps/api/src/prompts/index.ts +10 -0
  47. package/apps/octogent/apps/api/src/prompts/promptResolver.ts +145 -0
  48. package/apps/octogent/apps/api/src/runtimeMetadata.ts +69 -0
  49. package/apps/octogent/apps/api/src/server.ts +80 -0
  50. package/apps/octogent/apps/api/src/setupState.ts +80 -0
  51. package/apps/octogent/apps/api/src/setupStatus.ts +174 -0
  52. package/apps/octogent/apps/api/src/startupPrerequisites.ts +146 -0
  53. package/apps/octogent/apps/api/src/terminalRuntime/channelMessaging.ts +87 -0
  54. package/apps/octogent/apps/api/src/terminalRuntime/claudeTranscript.ts +279 -0
  55. package/apps/octogent/apps/api/src/terminalRuntime/constants.ts +15 -0
  56. package/apps/octogent/apps/api/src/terminalRuntime/conversations.ts +492 -0
  57. package/apps/octogent/apps/api/src/terminalRuntime/gitOperations.ts +341 -0
  58. package/apps/octogent/apps/api/src/terminalRuntime/hookProcessor.ts +405 -0
  59. package/apps/octogent/apps/api/src/terminalRuntime/protocol.ts +46 -0
  60. package/apps/octogent/apps/api/src/terminalRuntime/ptyEnvironment.ts +50 -0
  61. package/apps/octogent/apps/api/src/terminalRuntime/registry.ts +423 -0
  62. package/apps/octogent/apps/api/src/terminalRuntime/sessionRuntime.ts +671 -0
  63. package/apps/octogent/apps/api/src/terminalRuntime/systemClients.ts +432 -0
  64. package/apps/octogent/apps/api/src/terminalRuntime/types.ts +157 -0
  65. package/apps/octogent/apps/api/src/terminalRuntime/worktreeManager.ts +135 -0
  66. package/apps/octogent/apps/api/src/terminalRuntime.ts +567 -0
  67. package/apps/octogent/apps/api/src/usageUtils.ts +16 -0
  68. package/apps/octogent/apps/api/src/ws-shim.d.ts +28 -0
  69. package/apps/octogent/apps/api/tests/agentStateDetection.test.ts +67 -0
  70. package/apps/octogent/apps/api/tests/claudeUsage.test.ts +583 -0
  71. package/apps/octogent/apps/api/tests/codexUsage.test.ts +107 -0
  72. package/apps/octogent/apps/api/tests/createApiServer.test.ts +3207 -0
  73. package/apps/octogent/apps/api/tests/githubRepoSummary.test.ts +100 -0
  74. package/apps/octogent/apps/api/tests/logging.test.ts +33 -0
  75. package/apps/octogent/apps/api/tests/monitorApi.test.ts +467 -0
  76. package/apps/octogent/apps/api/tests/monitorCore.test.ts +104 -0
  77. package/apps/octogent/apps/api/tests/promptResolver.test.ts +109 -0
  78. package/apps/octogent/apps/api/tests/protocol.test.ts +14 -0
  79. package/apps/octogent/apps/api/tests/sessionRuntime.test.ts +608 -0
  80. package/apps/octogent/apps/api/tests/startupPrerequisites.test.ts +70 -0
  81. package/apps/octogent/apps/api/tests/upgradeHandler.test.ts +40 -0
  82. package/apps/octogent/apps/api/tests/xMonitorProvider.test.ts +109 -0
  83. package/apps/octogent/apps/api/tsconfig.json +7 -0
  84. package/apps/octogent/apps/api/vitest.config.ts +7 -0
  85. package/apps/octogent/apps/web/AGENTS.md +38 -0
  86. package/apps/octogent/apps/web/index.html +13 -0
  87. package/apps/octogent/apps/web/package.json +32 -0
  88. package/apps/octogent/apps/web/public/octopus-favicon.svg +26 -0
  89. package/apps/octogent/apps/web/src/App.tsx +646 -0
  90. package/apps/octogent/apps/web/src/app/canvas/types.ts +34 -0
  91. package/apps/octogent/apps/web/src/app/codeIntelAggregation.ts +278 -0
  92. package/apps/octogent/apps/web/src/app/constants.ts +28 -0
  93. package/apps/octogent/apps/web/src/app/conversationNormalizers.ts +135 -0
  94. package/apps/octogent/apps/web/src/app/formatTimestamp.ts +18 -0
  95. package/apps/octogent/apps/web/src/app/githubMetrics.ts +76 -0
  96. package/apps/octogent/apps/web/src/app/githubNormalizers.ts +91 -0
  97. package/apps/octogent/apps/web/src/app/hooks/useAgentRuntimeStates.ts +18 -0
  98. package/apps/octogent/apps/web/src/app/hooks/useBackendLivenessPolling.ts +53 -0
  99. package/apps/octogent/apps/web/src/app/hooks/useCanvasGraphData.ts +449 -0
  100. package/apps/octogent/apps/web/src/app/hooks/useCanvasTransform.ts +260 -0
  101. package/apps/octogent/apps/web/src/app/hooks/useClaudeUsagePolling.ts +40 -0
  102. package/apps/octogent/apps/web/src/app/hooks/useClickOutside.ts +30 -0
  103. package/apps/octogent/apps/web/src/app/hooks/useCodeIntelRuntime.ts +83 -0
  104. package/apps/octogent/apps/web/src/app/hooks/useCodexUsagePolling.ts +35 -0
  105. package/apps/octogent/apps/web/src/app/hooks/useConsoleKeyboardShortcuts.ts +31 -0
  106. package/apps/octogent/apps/web/src/app/hooks/useConversationsRuntime.ts +377 -0
  107. package/apps/octogent/apps/web/src/app/hooks/useForceSimulation.ts +319 -0
  108. package/apps/octogent/apps/web/src/app/hooks/useGitHubPrimaryViewModel.ts +143 -0
  109. package/apps/octogent/apps/web/src/app/hooks/useGithubSummaryPolling.ts +28 -0
  110. package/apps/octogent/apps/web/src/app/hooks/useInitialColumnsHydration.ts +64 -0
  111. package/apps/octogent/apps/web/src/app/hooks/useMonitorRuntime.ts +220 -0
  112. package/apps/octogent/apps/web/src/app/hooks/usePersistedUiState.ts +536 -0
  113. package/apps/octogent/apps/web/src/app/hooks/usePollingData.ts +79 -0
  114. package/apps/octogent/apps/web/src/app/hooks/usePromptLibrary.ts +185 -0
  115. package/apps/octogent/apps/web/src/app/hooks/useTentacleGitLifecycle.ts +530 -0
  116. package/apps/octogent/apps/web/src/app/hooks/useTerminalCompletionNotification.ts +94 -0
  117. package/apps/octogent/apps/web/src/app/hooks/useTerminalMutations.ts +266 -0
  118. package/apps/octogent/apps/web/src/app/hooks/useTerminalStateReconciliation.ts +23 -0
  119. package/apps/octogent/apps/web/src/app/hooks/useUsageHeatmapPolling.ts +43 -0
  120. package/apps/octogent/apps/web/src/app/hooks/useWorkspaceSetup.ts +80 -0
  121. package/apps/octogent/apps/web/src/app/hotkeys.ts +31 -0
  122. package/apps/octogent/apps/web/src/app/monitorNormalizers.ts +145 -0
  123. package/apps/octogent/apps/web/src/app/notificationSounds.ts +164 -0
  124. package/apps/octogent/apps/web/src/app/terminalRuntimeStateStore.ts +261 -0
  125. package/apps/octogent/apps/web/src/app/terminalState.ts +21 -0
  126. package/apps/octogent/apps/web/src/app/types.ts +42 -0
  127. package/apps/octogent/apps/web/src/app/uiStateNormalizers.ts +113 -0
  128. package/apps/octogent/apps/web/src/app/usageNormalizers.ts +58 -0
  129. package/apps/octogent/apps/web/src/components/ActiveAgentsSidebar.tsx +60 -0
  130. package/apps/octogent/apps/web/src/components/ActivityPrimaryView.tsx +21 -0
  131. package/apps/octogent/apps/web/src/components/AgentStateBadge.tsx +47 -0
  132. package/apps/octogent/apps/web/src/components/CanvasPrimaryView.tsx +1532 -0
  133. package/apps/octogent/apps/web/src/components/ClearAllConversationsDialog.tsx +33 -0
  134. package/apps/octogent/apps/web/src/components/CodeIntelArcDiagram.tsx +245 -0
  135. package/apps/octogent/apps/web/src/components/CodeIntelPrimaryView.tsx +104 -0
  136. package/apps/octogent/apps/web/src/components/CodeIntelTreemap.tsx +138 -0
  137. package/apps/octogent/apps/web/src/components/ConsolePrimaryNav.tsx +31 -0
  138. package/apps/octogent/apps/web/src/components/ConversationsPrimaryView.tsx +243 -0
  139. package/apps/octogent/apps/web/src/components/DeckPrimaryView.tsx +613 -0
  140. package/apps/octogent/apps/web/src/components/DeleteTentacleDialog.tsx +91 -0
  141. package/apps/octogent/apps/web/src/components/EmptyOctopus.tsx +715 -0
  142. package/apps/octogent/apps/web/src/components/GitHubPrimaryView.tsx +494 -0
  143. package/apps/octogent/apps/web/src/components/MonitorPrimaryView.tsx +475 -0
  144. package/apps/octogent/apps/web/src/components/PrimaryViewRouter.tsx +99 -0
  145. package/apps/octogent/apps/web/src/components/PromptsPrimaryView.tsx +243 -0
  146. package/apps/octogent/apps/web/src/components/RuntimeStatusStrip.tsx +273 -0
  147. package/apps/octogent/apps/web/src/components/SettingsPrimaryView.tsx +92 -0
  148. package/apps/octogent/apps/web/src/components/SidebarActionPanel.tsx +124 -0
  149. package/apps/octogent/apps/web/src/components/SidebarConversationsList.tsx +279 -0
  150. package/apps/octogent/apps/web/src/components/SidebarPromptsList.tsx +116 -0
  151. package/apps/octogent/apps/web/src/components/TelemetryTape.tsx +106 -0
  152. package/apps/octogent/apps/web/src/components/TentacleGitActionsDialog.tsx +341 -0
  153. package/apps/octogent/apps/web/src/components/Terminal.tsx +524 -0
  154. package/apps/octogent/apps/web/src/components/TerminalPromptPicker.tsx +140 -0
  155. package/apps/octogent/apps/web/src/components/UsageHeatmap.tsx +702 -0
  156. package/apps/octogent/apps/web/src/components/canvas/CanvasTentaclePanel.tsx +485 -0
  157. package/apps/octogent/apps/web/src/components/canvas/CanvasTerminalColumn.tsx +89 -0
  158. package/apps/octogent/apps/web/src/components/canvas/DeleteAllTerminalsDialog.tsx +221 -0
  159. package/apps/octogent/apps/web/src/components/canvas/OctopusNode.tsx +307 -0
  160. package/apps/octogent/apps/web/src/components/canvas/SessionNode.tsx +185 -0
  161. package/apps/octogent/apps/web/src/components/deck/ActionCards.tsx +118 -0
  162. package/apps/octogent/apps/web/src/components/deck/AddTentacleForm.tsx +269 -0
  163. package/apps/octogent/apps/web/src/components/deck/DeckBottomActions.tsx +56 -0
  164. package/apps/octogent/apps/web/src/components/deck/TentaclePod.tsx +334 -0
  165. package/apps/octogent/apps/web/src/components/deck/WorkspaceSetupCard.tsx +105 -0
  166. package/apps/octogent/apps/web/src/components/deck/octopusVisuals.ts +72 -0
  167. package/apps/octogent/apps/web/src/components/terminalReplay.ts +62 -0
  168. package/apps/octogent/apps/web/src/components/terminalWheel.ts +54 -0
  169. package/apps/octogent/apps/web/src/components/ui/ActionButton.tsx +34 -0
  170. package/apps/octogent/apps/web/src/components/ui/ConfirmationDialog.tsx +86 -0
  171. package/apps/octogent/apps/web/src/components/ui/MarkdownContent.tsx +43 -0
  172. package/apps/octogent/apps/web/src/components/ui/SettingsToggle.tsx +34 -0
  173. package/apps/octogent/apps/web/src/components/ui/StatusBadge.tsx +24 -0
  174. package/apps/octogent/apps/web/src/main.tsx +17 -0
  175. package/apps/octogent/apps/web/src/runtime/HttpTerminalSnapshotReader.ts +87 -0
  176. package/apps/octogent/apps/web/src/runtime/runtimeEndpoints.ts +412 -0
  177. package/apps/octogent/apps/web/src/styles/chrome-and-buttons.css +272 -0
  178. package/apps/octogent/apps/web/src/styles/console-canvas-activity.css +358 -0
  179. package/apps/octogent/apps/web/src/styles/console-canvas-canvas.css +1843 -0
  180. package/apps/octogent/apps/web/src/styles/console-canvas-code-intel.css +227 -0
  181. package/apps/octogent/apps/web/src/styles/console-canvas-conversations.css +705 -0
  182. package/apps/octogent/apps/web/src/styles/console-canvas-deck.css +1524 -0
  183. package/apps/octogent/apps/web/src/styles/console-canvas-github.css +541 -0
  184. package/apps/octogent/apps/web/src/styles/console-canvas-monitor.css +595 -0
  185. package/apps/octogent/apps/web/src/styles/console-canvas-pixpack.css +81 -0
  186. package/apps/octogent/apps/web/src/styles/console-canvas-prompts.css +474 -0
  187. package/apps/octogent/apps/web/src/styles/console-canvas-settings.css +207 -0
  188. package/apps/octogent/apps/web/src/styles/console-chrome-status-nav.css +441 -0
  189. package/apps/octogent/apps/web/src/styles/console-overrides-telemetry.css +320 -0
  190. package/apps/octogent/apps/web/src/styles/console-theme-tokens.css +25 -0
  191. package/apps/octogent/apps/web/src/styles/cortex-theme.css +412 -0
  192. package/apps/octogent/apps/web/src/styles/foundation.css +100 -0
  193. package/apps/octogent/apps/web/src/styles/sidebar-and-scrollbars.css +447 -0
  194. package/apps/octogent/apps/web/src/styles/terminal-and-status.css +356 -0
  195. package/apps/octogent/apps/web/src/styles.css +25 -0
  196. package/apps/octogent/apps/web/src/types/ws.d.ts +23 -0
  197. package/apps/octogent/apps/web/tests/CanvasPrimaryView.test.tsx +347 -0
  198. package/apps/octogent/apps/web/tests/HttpTerminalSnapshotReader.test.tsx +54 -0
  199. package/apps/octogent/apps/web/tests/RuntimeStatusStrip.test.tsx +70 -0
  200. package/apps/octogent/apps/web/tests/Terminal.test.tsx +87 -0
  201. package/apps/octogent/apps/web/tests/add-tentacle-form.test.tsx +48 -0
  202. package/apps/octogent/apps/web/tests/app-github-runtime.test.tsx +162 -0
  203. package/apps/octogent/apps/web/tests/app-monitor-runtime.test.tsx +657 -0
  204. package/apps/octogent/apps/web/tests/app-shell-navigation.test.tsx +109 -0
  205. package/apps/octogent/apps/web/tests/app-swarm-refresh.test.tsx +268 -0
  206. package/apps/octogent/apps/web/tests/app-ui-state-persistence.test.tsx +116 -0
  207. package/apps/octogent/apps/web/tests/app-workspace-setup.test.tsx +217 -0
  208. package/apps/octogent/apps/web/tests/canvas-tentacle-panel.test.tsx +195 -0
  209. package/apps/octogent/apps/web/tests/delete-all-terminals-dialog.test.tsx +76 -0
  210. package/apps/octogent/apps/web/tests/githubMetrics.test.tsx +52 -0
  211. package/apps/octogent/apps/web/tests/hotkeys.test.tsx +44 -0
  212. package/apps/octogent/apps/web/tests/runtimeEndpoints.test.tsx +240 -0
  213. package/apps/octogent/apps/web/tests/setup.ts +39 -0
  214. package/apps/octogent/apps/web/tests/tentacle-pod.test.tsx +62 -0
  215. package/apps/octogent/apps/web/tests/terminalReplay.test.ts +71 -0
  216. package/apps/octogent/apps/web/tests/terminalState.test.tsx +49 -0
  217. package/apps/octogent/apps/web/tests/terminalWheel.test.tsx +51 -0
  218. package/apps/octogent/apps/web/tests/test-utils/appTestHarness.ts +48 -0
  219. package/apps/octogent/apps/web/tests/uiPrimitives.test.tsx +31 -0
  220. package/apps/octogent/apps/web/tests/useAgentRuntimeStates.test.tsx +47 -0
  221. package/apps/octogent/apps/web/tsconfig.json +8 -0
  222. package/apps/octogent/apps/web/vite.api.bundle.config.mts +32 -0
  223. package/apps/octogent/apps/web/vite.config.ts +22 -0
  224. package/apps/octogent/bin/octogent +3 -0
  225. package/apps/octogent/biome.json +21 -0
  226. package/apps/octogent/docs/concepts/mental-model.md +79 -0
  227. package/apps/octogent/docs/concepts/runtime-and-api.md +60 -0
  228. package/apps/octogent/docs/concepts/tentacles.md +85 -0
  229. package/apps/octogent/docs/getting-started/installation.md +54 -0
  230. package/apps/octogent/docs/getting-started/quickstart.md +79 -0
  231. package/apps/octogent/docs/guides/inter-agent-messaging.md +43 -0
  232. package/apps/octogent/docs/guides/orchestrating-child-agents.md +49 -0
  233. package/apps/octogent/docs/guides/working-with-todos.md +56 -0
  234. package/apps/octogent/docs/index.md +40 -0
  235. package/apps/octogent/docs/reference/api.md +103 -0
  236. package/apps/octogent/docs/reference/cli.md +71 -0
  237. package/apps/octogent/docs/reference/experimental-features.md +28 -0
  238. package/apps/octogent/docs/reference/filesystem-layout.md +62 -0
  239. package/apps/octogent/docs/reference/troubleshooting.md +49 -0
  240. package/apps/octogent/package.json +35 -0
  241. package/apps/octogent/packages/core/AGENTS.md +31 -0
  242. package/apps/octogent/packages/core/package.json +12 -0
  243. package/apps/octogent/packages/core/src/adapters/InMemoryTerminalSnapshotReader.ts +10 -0
  244. package/apps/octogent/packages/core/src/application/buildTerminalList.ts +13 -0
  245. package/apps/octogent/packages/core/src/domain/agentRuntime.ts +18 -0
  246. package/apps/octogent/packages/core/src/domain/channel.ts +8 -0
  247. package/apps/octogent/packages/core/src/domain/completionSound.ts +14 -0
  248. package/apps/octogent/packages/core/src/domain/conversation.ts +48 -0
  249. package/apps/octogent/packages/core/src/domain/deck.ts +33 -0
  250. package/apps/octogent/packages/core/src/domain/git.ts +32 -0
  251. package/apps/octogent/packages/core/src/domain/monitor.ts +62 -0
  252. package/apps/octogent/packages/core/src/domain/setup.ts +27 -0
  253. package/apps/octogent/packages/core/src/domain/terminal.ts +17 -0
  254. package/apps/octogent/packages/core/src/domain/uiState.ts +22 -0
  255. package/apps/octogent/packages/core/src/domain/usage.ts +60 -0
  256. package/apps/octogent/packages/core/src/index.ts +15 -0
  257. package/apps/octogent/packages/core/src/ports/TerminalSnapshotReader.ts +5 -0
  258. package/apps/octogent/packages/core/src/util/typeCoercion.ts +20 -0
  259. package/apps/octogent/packages/core/tests/buildTerminalList.test.ts +75 -0
  260. package/apps/octogent/packages/core/tsconfig.json +7 -0
  261. package/apps/octogent/packages/core/tsconfig.tsbuildinfo +1 -0
  262. package/apps/octogent/packages/core/vitest.config.ts +7 -0
  263. package/apps/octogent/pnpm-lock.yaml +3212 -0
  264. package/apps/octogent/pnpm-workspace.yaml +3 -0
  265. package/apps/octogent/prompts/meta-prompt-generator.md +223 -0
  266. package/apps/octogent/prompts/octoboss-clean-contexts.md +30 -0
  267. package/apps/octogent/prompts/octoboss-reorganize-tentacles.md +29 -0
  268. package/apps/octogent/prompts/octoboss-reorganize-todos.md +27 -0
  269. package/apps/octogent/prompts/sandbox-init.md +3 -0
  270. package/apps/octogent/prompts/swarm-parent.md +83 -0
  271. package/apps/octogent/prompts/swarm-worker.md +50 -0
  272. package/apps/octogent/prompts/tentacle-context-init.md +1 -0
  273. package/apps/octogent/prompts/tentacle-planner.md +110 -0
  274. package/apps/octogent/prompts/tentacle-reorganize-todos.md +20 -0
  275. package/apps/octogent/prompts/tentacle-update-tentacle.md +18 -0
  276. package/apps/octogent/scripts/build-package.mjs +23 -0
  277. package/apps/octogent/scripts/dev.mjs +158 -0
  278. package/apps/octogent/scripts/smoke-public-install.mjs +271 -0
  279. package/apps/octogent/static/images/octogent-header.png +0 -0
  280. package/apps/octogent/static/images/preview_1.jpg +0 -0
  281. package/apps/octogent/static/images/preview_2.jpg +0 -0
  282. package/apps/octogent/static/images/preview_3.jpg +0 -0
  283. package/apps/octogent/static/images/preview_4.jpg +0 -0
  284. package/apps/octogent/static/images/preview_5.jpg +0 -0
  285. package/apps/octogent/static/images/preview_6.jpg +0 -0
  286. package/apps/octogent/tsconfig.base.json +16 -0
  287. package/bin/AGI +3 -0
  288. package/bin/AGI-install-app +71 -0
  289. package/bin/AGI-ui +16 -0
  290. package/bin/AGI-voice +15 -0
  291. package/bin/AGI-web +16 -0
  292. package/bin/cortex +109 -0
  293. package/bin/cortex-octogent +99 -0
  294. package/bin/import-specifier.mjs +13 -0
  295. package/bin/import-specifier.test.mjs +13 -0
  296. package/bin/octo +150 -0
  297. package/dist/cli.mjs +555650 -0
  298. package/package.json +157 -0
  299. package/scripts/setup-wizard.ts +390 -0
@@ -0,0 +1,16 @@
1
+ import { asNumber } from "@octogent/core";
2
+
3
+ export const toResetIso = (value: unknown): string | null => {
4
+ if (typeof value === "string") {
5
+ const parsed = new Date(value);
6
+ return Number.isFinite(parsed.getTime()) ? parsed.toISOString() : null;
7
+ }
8
+
9
+ const numberValue = asNumber(value);
10
+ if (numberValue === null) {
11
+ return null;
12
+ }
13
+
14
+ const milliseconds = numberValue >= 1_000_000_000_000 ? numberValue : numberValue * 1000;
15
+ return new Date(milliseconds).toISOString();
16
+ };
@@ -0,0 +1,28 @@
1
+ declare module "ws" {
2
+ import type { EventEmitter } from "node:events";
3
+ import type { IncomingMessage } from "node:http";
4
+ import type { Duplex } from "node:stream";
5
+
6
+ type WebSocketData = string | Buffer | ArrayBuffer | Buffer[];
7
+
8
+ export interface WebSocket extends EventEmitter {
9
+ readonly readyState: number;
10
+ send(data: string | Buffer): void;
11
+ close(): void;
12
+ on(event: "message", listener: (data: WebSocketData) => void): this;
13
+ on(event: "close", listener: () => void): this;
14
+ }
15
+
16
+ type HandleUpgradeCallback = (websocket: WebSocket) => void;
17
+
18
+ export class WebSocketServer extends EventEmitter {
19
+ constructor(options: { noServer: true });
20
+ handleUpgrade(
21
+ request: IncomingMessage,
22
+ socket: Duplex,
23
+ head: Buffer,
24
+ callback: HandleUpgradeCallback,
25
+ ): void;
26
+ close(): void;
27
+ }
28
+ }
@@ -0,0 +1,67 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { AgentStateTracker } from "../src/agentStateDetection";
4
+
5
+ describe("AgentStateTracker", () => {
6
+ it("defaults to idle", () => {
7
+ const tracker = new AgentStateTracker();
8
+
9
+ expect(tracker.currentState).toBe("idle");
10
+ });
11
+
12
+ it("switches to processing when interrupt marker appears", () => {
13
+ const tracker = new AgentStateTracker({ idleAfterMs: 1_000 });
14
+
15
+ expect(tracker.observeChunk("running... (3s • esc to interrupt)", 0)).toBe("processing");
16
+ expect(tracker.currentState).toBe("processing");
17
+ });
18
+
19
+ it("detects markers split across multiple stdout chunks", () => {
20
+ const tracker = new AgentStateTracker({ idleAfterMs: 1_000 });
21
+
22
+ expect(tracker.observeChunk("working... esc to inter", 0)).toBeNull();
23
+ expect(tracker.observeChunk("rupt", 25)).toBe("processing");
24
+ });
25
+
26
+ it("ignores ANSI control sequences around processing marker", () => {
27
+ const tracker = new AgentStateTracker({ idleAfterMs: 1_000 });
28
+
29
+ const chunk = "\u001b[2K\u001b[1A\rWorking... \u001b[2mesc to interrupt\u001b[0m\n";
30
+
31
+ expect(tracker.observeChunk(chunk, 0)).toBe("processing");
32
+ });
33
+
34
+ it("forces processing on submit and returns idle after inactivity", () => {
35
+ const tracker = new AgentStateTracker({ idleAfterMs: 1_000 });
36
+
37
+ expect(tracker.observeSubmit(0)).toBe("processing");
38
+ expect(tracker.poll(900)).toBeNull();
39
+ expect(tracker.currentState).toBe("processing");
40
+
41
+ expect(tracker.poll(1_000)).toBe("idle");
42
+ expect(tracker.currentState).toBe("idle");
43
+ });
44
+
45
+ it("extends processing while output is still streaming", () => {
46
+ const tracker = new AgentStateTracker({ idleAfterMs: 1_000 });
47
+
48
+ tracker.observeSubmit(0);
49
+ expect(tracker.observeChunk("Once upon a time...", 700)).toBeNull();
50
+
51
+ expect(tracker.poll(1_500)).toBeNull();
52
+ expect(tracker.currentState).toBe("processing");
53
+
54
+ expect(tracker.poll(1_700)).toBe("idle");
55
+ expect(tracker.currentState).toBe("idle");
56
+ });
57
+
58
+ it("does not treat codex title/footer text as idle while processing", () => {
59
+ const tracker = new AgentStateTracker({ idleAfterMs: 1_000 });
60
+
61
+ tracker.observeSubmit(0);
62
+
63
+ expect(tracker.observeChunk("OpenAI Codex\n100% context left\n", 100)).toBeNull();
64
+ expect(tracker.currentState).toBe("processing");
65
+ expect(tracker.poll(1_050)).toBeNull();
66
+ });
67
+ });
@@ -0,0 +1,583 @@
1
+ import { mkdtempSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+
7
+ import {
8
+ invalidateUsageCache,
9
+ parseCliUsageOutput,
10
+ readClaudeUsageSnapshot,
11
+ resetCliSession,
12
+ stripAnsiCodes,
13
+ } from "../src/claudeUsage";
14
+
15
+ const noCliPty = async () => null;
16
+
17
+ const validCredentials = (overrides: Record<string, unknown> = {}) => ({
18
+ claudeAiOauth: {
19
+ accessToken: "oauth-token",
20
+ scopes: ["user:profile", "offline_access"],
21
+ ...overrides,
22
+ },
23
+ });
24
+
25
+ const usageResponseBody = JSON.stringify({
26
+ plan_type: "pro",
27
+ five_hour: { used_percent: 14, reset_at: "2026-03-03T15:00:00.000Z" },
28
+ seven_day: { used_percent: 52, reset_at: 1_772_539_200 },
29
+ seven_day_sonnet: { used_percent: 33, reset_at: 1_772_711_999 },
30
+ });
31
+
32
+ const cliUsageOutput = [
33
+ "Current session",
34
+ " 2% used",
35
+ "Current week (all models)",
36
+ " 0% used",
37
+ "Current week (Sonnet only)",
38
+ " 0% used",
39
+ ].join("\n");
40
+
41
+ const temporaryDirectories: string[] = [];
42
+
43
+ afterEach(() => {
44
+ for (const directory of temporaryDirectories) {
45
+ rmSync(directory, { recursive: true, force: true });
46
+ }
47
+ temporaryDirectories.length = 0;
48
+ });
49
+
50
+ describe("stripAnsiCodes", () => {
51
+ it("strips CSI sequences", () => {
52
+ expect(stripAnsiCodes("\u001B[32mHello\u001B[0m")).toBe("Hello");
53
+ });
54
+
55
+ it("strips complex SGR sequences", () => {
56
+ expect(stripAnsiCodes("\u001B[1;34;48;5;220mBold Blue\u001B[0m")).toBe("Bold Blue");
57
+ });
58
+
59
+ it("returns plain text unchanged", () => {
60
+ expect(stripAnsiCodes("plain text")).toBe("plain text");
61
+ });
62
+ });
63
+
64
+ describe("parseCliUsageOutput", () => {
65
+ it("passes through used percentages directly", () => {
66
+ const output = [
67
+ "Current session",
68
+ " 2% used",
69
+ "Current week (all models)",
70
+ " 0% used",
71
+ "Current week (Sonnet only)",
72
+ " 0% used",
73
+ ].join("\n");
74
+
75
+ const result = parseCliUsageOutput(output);
76
+ expect(result.primaryUsedPercent).toBe(2);
77
+ expect(result.secondaryUsedPercent).toBe(0);
78
+ expect(result.sonnetUsedPercent).toBe(0);
79
+ });
80
+
81
+ it("inverts remaining percentages to used (100 - value)", () => {
82
+ const output = [
83
+ "Current session",
84
+ " 72.5% remaining",
85
+ "Current week (all models)",
86
+ " 45% remaining",
87
+ "Current week (Sonnet only)",
88
+ " 88.3% remaining",
89
+ ].join("\n");
90
+
91
+ const result = parseCliUsageOutput(output);
92
+ expect(result.primaryUsedPercent).toBe(27.5);
93
+ expect(result.secondaryUsedPercent).toBe(55);
94
+ expect(result.sonnetUsedPercent).toBe(11.7);
95
+ });
96
+
97
+ it("handles ANSI codes in output", () => {
98
+ const output = [
99
+ "\u001B[1mCurrent session\u001B[0m",
100
+ " \u001B[32m85%\u001B[0m remaining",
101
+ "\u001B[1mCurrent week (all models)\u001B[0m",
102
+ " \u001B[33m50%\u001B[0m remaining",
103
+ ].join("\n");
104
+
105
+ const result = parseCliUsageOutput(output);
106
+ expect(result.primaryUsedPercent).toBe(15);
107
+ expect(result.secondaryUsedPercent).toBe(50);
108
+ });
109
+
110
+ it("returns nulls when no labels found", () => {
111
+ const result = parseCliUsageOutput("some unrelated output\nno percentages here");
112
+ expect(result.primaryUsedPercent).toBeNull();
113
+ expect(result.secondaryUsedPercent).toBeNull();
114
+ expect(result.sonnetUsedPercent).toBeNull();
115
+ });
116
+
117
+ it("handles Opus label variant", () => {
118
+ const output = ["Current session", " 10% used", "Current week (Opus)", " 30% used"].join(
119
+ "\n",
120
+ );
121
+
122
+ const result = parseCliUsageOutput(output);
123
+ expect(result.primaryUsedPercent).toBe(10);
124
+ expect(result.secondaryUsedPercent).toBe(30);
125
+ });
126
+
127
+ it("handles percentage on same line as label", () => {
128
+ const output = "Current session: 35% used\nCurrent week (all models): 60% used";
129
+ const result = parseCliUsageOutput(output);
130
+ expect(result.primaryUsedPercent).toBe(35);
131
+ expect(result.secondaryUsedPercent).toBe(60);
132
+ });
133
+
134
+ it("does not reuse the session percentage for week when labels are tightly packed", () => {
135
+ const output = [
136
+ "Status Config Usage Stats",
137
+ "Current session 1% used Current week (all models) 52% used Current week (Sonnet only) 33% used",
138
+ ].join("\n");
139
+
140
+ const result = parseCliUsageOutput(output);
141
+ expect(result.primaryUsedPercent).toBe(1);
142
+ expect(result.secondaryUsedPercent).toBe(52);
143
+ expect(result.sonnetUsedPercent).toBe(33);
144
+ });
145
+ });
146
+
147
+ describe("readClaudeUsageSnapshot", () => {
148
+ beforeEach(() => resetCliSession());
149
+
150
+ it("falls back to OAuth when CLI returns null", async () => {
151
+ const fetchMock = vi.fn<typeof fetch>().mockResolvedValueOnce(
152
+ new Response(usageResponseBody, {
153
+ status: 200,
154
+ headers: { "Content-Type": "application/json" },
155
+ }),
156
+ );
157
+
158
+ const snapshot = await readClaudeUsageSnapshot({
159
+ now: () => new Date("2026-03-03T12:00:00.000Z"),
160
+ spawnCliUsage: noCliPty,
161
+ readCredentialsJson: async () => validCredentials(),
162
+ fetchImpl: fetchMock,
163
+ });
164
+
165
+ expect(snapshot.status).toBe("ok");
166
+ expect(snapshot.source).toBe("oauth-api");
167
+ expect(fetchMock).toHaveBeenCalledTimes(1);
168
+ });
169
+
170
+ it("falls back to OAuth when CLI output has no parseable percentages", async () => {
171
+ const fetchMock = vi.fn<typeof fetch>().mockResolvedValueOnce(
172
+ new Response(usageResponseBody, {
173
+ status: 200,
174
+ headers: { "Content-Type": "application/json" },
175
+ }),
176
+ );
177
+
178
+ const snapshot = await readClaudeUsageSnapshot({
179
+ now: () => new Date("2026-03-03T12:00:00.000Z"),
180
+ spawnCliUsage: async () => "Welcome to Claude! No usage data here.",
181
+ readCredentialsJson: async () => validCredentials(),
182
+ fetchImpl: fetchMock,
183
+ });
184
+
185
+ expect(snapshot.source).toBe("oauth-api");
186
+ });
187
+
188
+ it("falls back to OAuth when CLI throws", async () => {
189
+ const fetchMock = vi.fn<typeof fetch>().mockResolvedValueOnce(
190
+ new Response(usageResponseBody, {
191
+ status: 200,
192
+ headers: { "Content-Type": "application/json" },
193
+ }),
194
+ );
195
+
196
+ const snapshot = await readClaudeUsageSnapshot({
197
+ now: () => new Date("2026-03-03T12:00:00.000Z"),
198
+ spawnCliUsage: async () => {
199
+ throw new Error("pty crashed");
200
+ },
201
+ readCredentialsJson: async () => validCredentials(),
202
+ fetchImpl: fetchMock,
203
+ });
204
+
205
+ expect(snapshot.source).toBe("oauth-api");
206
+ });
207
+
208
+ it("prefers CLI data over OAuth when both are available", async () => {
209
+ const fetchMock = vi.fn<typeof fetch>().mockResolvedValueOnce(
210
+ new Response(usageResponseBody, {
211
+ status: 200,
212
+ headers: { "Content-Type": "application/json" },
213
+ }),
214
+ );
215
+
216
+ const snapshot = await readClaudeUsageSnapshot({
217
+ now: () => new Date("2026-03-03T12:00:00.000Z"),
218
+ spawnCliUsage: async () => cliUsageOutput,
219
+ readCredentialsJson: async () => validCredentials(),
220
+ fetchImpl: fetchMock,
221
+ });
222
+
223
+ expect(snapshot.status).toBe("ok");
224
+ expect(snapshot.source).toBe("cli-pty");
225
+ expect(snapshot.primaryUsedPercent).toBe(2);
226
+ expect(snapshot.secondaryUsedPercent).toBe(0);
227
+ expect(snapshot.sonnetUsedPercent).toBe(0);
228
+ });
229
+
230
+ it("returns unavailable when credentials cannot be found", async () => {
231
+ const snapshot = await readClaudeUsageSnapshot({
232
+ now: () => new Date("2026-03-03T12:00:00.000Z"),
233
+ spawnCliUsage: noCliPty,
234
+ readCredentialsJson: async () => {
235
+ const error = new Error("missing");
236
+ Object.assign(error, { code: "ENOENT" });
237
+ throw error;
238
+ },
239
+ });
240
+
241
+ expect(snapshot.status).toBe("unavailable");
242
+ expect(snapshot.message).toMatch(/credentials not found/i);
243
+ });
244
+
245
+ it("returns unavailable when OAuth token is missing", async () => {
246
+ const snapshot = await readClaudeUsageSnapshot({
247
+ now: () => new Date("2026-03-03T12:00:00.000Z"),
248
+ spawnCliUsage: noCliPty,
249
+ readCredentialsJson: async () => ({
250
+ claudeAiOauth: { scopes: ["user:profile"] },
251
+ }),
252
+ });
253
+
254
+ expect(snapshot.status).toBe("unavailable");
255
+ expect(snapshot.message).toMatch(/access token.*missing/i);
256
+ });
257
+
258
+ it("returns unavailable when required user:profile scope is missing", async () => {
259
+ const snapshot = await readClaudeUsageSnapshot({
260
+ now: () => new Date("2026-03-03T12:00:00.000Z"),
261
+ spawnCliUsage: noCliPty,
262
+ readCredentialsJson: async () => validCredentials({ scopes: ["offline_access"] }),
263
+ });
264
+
265
+ expect(snapshot.status).toBe("unavailable");
266
+ expect(snapshot.message).toMatch(/user:profile/i);
267
+ });
268
+
269
+ it("maps usage windows from OAuth API", async () => {
270
+ const fetchMock = vi.fn<typeof fetch>().mockResolvedValueOnce(
271
+ new Response(usageResponseBody, {
272
+ status: 200,
273
+ headers: { "Content-Type": "application/json" },
274
+ }),
275
+ );
276
+
277
+ const snapshot = await readClaudeUsageSnapshot({
278
+ now: () => new Date("2026-03-03T12:00:00.000Z"),
279
+ spawnCliUsage: noCliPty,
280
+ readCredentialsJson: async () => validCredentials(),
281
+ fetchImpl: fetchMock,
282
+ });
283
+
284
+ expect(snapshot).toEqual(
285
+ expect.objectContaining({
286
+ status: "ok",
287
+ source: "oauth-api",
288
+ planType: "pro",
289
+ primaryUsedPercent: 14,
290
+ primaryResetAt: "2026-03-03T15:00:00.000Z",
291
+ secondaryUsedPercent: 52,
292
+ secondaryResetAt: "2026-03-03T12:00:00.000Z",
293
+ sonnetUsedPercent: 33,
294
+ sonnetResetAt: "2026-03-05T11:59:59.000Z",
295
+ }),
296
+ );
297
+ expect(fetchMock).toHaveBeenCalledTimes(1);
298
+ expect(fetchMock.mock.calls[0]?.[0]).toBe("https://api.anthropic.com/api/oauth/usage");
299
+ expect(fetchMock.mock.calls[0]?.[1]).toEqual(
300
+ expect.objectContaining({
301
+ method: "GET",
302
+ headers: expect.objectContaining({
303
+ Authorization: "Bearer oauth-token",
304
+ "anthropic-beta": "oauth-2025-04-20",
305
+ }),
306
+ }),
307
+ );
308
+ });
309
+
310
+ it("returns unavailable on oauth unauthorized response", async () => {
311
+ const snapshot = await readClaudeUsageSnapshot({
312
+ now: () => new Date("2026-03-03T12:00:00.000Z"),
313
+ spawnCliUsage: noCliPty,
314
+ readCredentialsJson: async () => validCredentials(),
315
+ fetchImpl: async () => new Response("unauthorized", { status: 401 }),
316
+ });
317
+
318
+ expect(snapshot.status).toBe("unavailable");
319
+ expect(snapshot.message).toMatch(/expired|unauthorized/i);
320
+ });
321
+
322
+ it("returns unavailable on oauth rate limit response", async () => {
323
+ const snapshot = await readClaudeUsageSnapshot({
324
+ now: () => new Date("2026-03-03T12:00:00.000Z"),
325
+ spawnCliUsage: noCliPty,
326
+ readCredentialsJson: async () => validCredentials(),
327
+ fetchImpl: async () =>
328
+ new Response(
329
+ JSON.stringify({
330
+ error: {
331
+ type: "rate_limit_error",
332
+ message: "Rate limited. Please try again later.",
333
+ },
334
+ }),
335
+ {
336
+ status: 429,
337
+ headers: { "Content-Type": "application/json" },
338
+ },
339
+ ),
340
+ });
341
+
342
+ expect(snapshot.status).toBe("unavailable");
343
+ expect(snapshot.message).toMatch(/rate limit|rate limited/i);
344
+ });
345
+
346
+ it("maps utilization field directly as percent value", async () => {
347
+ const fetchMock = vi.fn<typeof fetch>().mockResolvedValueOnce(
348
+ new Response(
349
+ JSON.stringify({
350
+ five_hour: { utilization: 14, resets_at: "2026-03-03T15:00:00.000Z" },
351
+ seven_day: { utilization: 52.3, resets_at: null },
352
+ seven_day_sonnet: { utilization: 0.0, resets_at: null },
353
+ }),
354
+ { status: 200, headers: { "Content-Type": "application/json" } },
355
+ ),
356
+ );
357
+
358
+ const snapshot = await readClaudeUsageSnapshot({
359
+ now: () => new Date("2026-03-03T12:00:00.000Z"),
360
+ spawnCliUsage: noCliPty,
361
+ readCredentialsJson: async () => validCredentials(),
362
+ fetchImpl: fetchMock,
363
+ });
364
+
365
+ expect(snapshot.status).toBe("ok");
366
+ expect(snapshot.primaryUsedPercent).toBe(14);
367
+ expect(snapshot.primaryResetAt).toBe("2026-03-03T15:00:00.000Z");
368
+ expect(snapshot.secondaryUsedPercent).toBe(52.3);
369
+ expect(snapshot.sonnetUsedPercent).toBe(0);
370
+ });
371
+
372
+ it("maps extra_usage costs from cents to dollars for Max plans", async () => {
373
+ const fetchMock = vi.fn<typeof fetch>().mockResolvedValueOnce(
374
+ new Response(
375
+ JSON.stringify({
376
+ five_hour: { utilization: 0.0, resets_at: null },
377
+ seven_day: { utilization: 0.0, resets_at: null },
378
+ seven_day_sonnet: { utilization: 0.0, resets_at: null },
379
+ extra_usage: {
380
+ is_enabled: true,
381
+ monthly_limit: 4250,
382
+ used_credits: 1275,
383
+ utilization: null,
384
+ },
385
+ }),
386
+ { status: 200, headers: { "Content-Type": "application/json" } },
387
+ ),
388
+ );
389
+
390
+ const snapshot = await readClaudeUsageSnapshot({
391
+ now: () => new Date("2026-03-03T12:00:00.000Z"),
392
+ spawnCliUsage: noCliPty,
393
+ readCredentialsJson: async () => validCredentials({ rateLimitTier: "default_claude_max_5x" }),
394
+ fetchImpl: fetchMock,
395
+ });
396
+
397
+ expect(snapshot.status).toBe("ok");
398
+ expect(snapshot.planType).toBe("Claude Max");
399
+ expect(snapshot.extraUsageCostUsed).toBe(12.75);
400
+ expect(snapshot.extraUsageCostLimit).toBe(42.5);
401
+ // Rate limit fields are also populated alongside extra usage
402
+ expect(snapshot.primaryUsedPercent).toBe(0);
403
+ expect(snapshot.secondaryUsedPercent).toBe(0);
404
+ expect(snapshot.sonnetUsedPercent).toBe(0);
405
+ });
406
+
407
+ it("invalidateUsageCache forces a fresh fetch on next read", async () => {
408
+ let callCount = 0;
409
+ const fetchMock = vi.fn<typeof fetch>().mockImplementation(async () => {
410
+ callCount++;
411
+ return new Response(
412
+ JSON.stringify({
413
+ plan_type: "pro",
414
+ five_hour: { used_percent: callCount * 10, reset_at: null },
415
+ seven_day: { used_percent: 50, reset_at: null },
416
+ seven_day_sonnet: { used_percent: 30, reset_at: null },
417
+ }),
418
+ { status: 200, headers: { "Content-Type": "application/json" } },
419
+ );
420
+ });
421
+
422
+ const deps = {
423
+ now: () => new Date("2026-03-03T12:00:00.000Z"),
424
+ spawnCliUsage: noCliPty,
425
+ readCredentialsJson: async () => validCredentials(),
426
+ fetchImpl: fetchMock,
427
+ };
428
+
429
+ const first = await readClaudeUsageSnapshot(deps);
430
+ expect(first.primaryUsedPercent).toBe(10);
431
+ expect(fetchMock).toHaveBeenCalledTimes(1);
432
+
433
+ // Cached — same result without a new fetch
434
+ const cached = await readClaudeUsageSnapshot(deps);
435
+ expect(cached.primaryUsedPercent).toBe(10);
436
+ expect(fetchMock).toHaveBeenCalledTimes(1);
437
+
438
+ // After invalidation, next read triggers a fresh fetch
439
+ invalidateUsageCache();
440
+ const fresh = await readClaudeUsageSnapshot(deps);
441
+ expect(fresh.primaryUsedPercent).toBe(20);
442
+ expect(fetchMock).toHaveBeenCalledTimes(2);
443
+ });
444
+
445
+ it("serves the last successful oauth snapshot when a later oauth request is rate limited", async () => {
446
+ const deps = {
447
+ now: () => new Date("2026-03-03T12:00:00.000Z"),
448
+ spawnCliUsage: noCliPty,
449
+ readCredentialsJson: async () => validCredentials(),
450
+ };
451
+
452
+ const okSnapshot = await readClaudeUsageSnapshot({
453
+ ...deps,
454
+ fetchImpl: async () =>
455
+ new Response(usageResponseBody, {
456
+ status: 200,
457
+ headers: { "Content-Type": "application/json" },
458
+ }),
459
+ });
460
+
461
+ expect(okSnapshot.status).toBe("ok");
462
+ expect(okSnapshot.source).toBe("oauth-api");
463
+
464
+ const staleSnapshot = await readClaudeUsageSnapshot({
465
+ ...deps,
466
+ fetchImpl: async () =>
467
+ new Response(
468
+ JSON.stringify({
469
+ error: {
470
+ type: "rate_limit_error",
471
+ message: "Rate limited. Please try again later.",
472
+ },
473
+ }),
474
+ {
475
+ status: 429,
476
+ headers: { "Content-Type": "application/json" },
477
+ },
478
+ ),
479
+ });
480
+
481
+ expect(staleSnapshot.status).toBe("ok");
482
+ expect(staleSnapshot.source).toBe("oauth-api");
483
+ expect(staleSnapshot.primaryUsedPercent).toBe(14);
484
+ expect(staleSnapshot.secondaryUsedPercent).toBe(52);
485
+ expect(staleSnapshot.sonnetUsedPercent).toBe(33);
486
+ });
487
+
488
+ it("returns error when credentials json is not parseable", async () => {
489
+ const snapshot = await readClaudeUsageSnapshot({
490
+ now: () => new Date("2026-03-03T12:00:00.000Z"),
491
+ spawnCliUsage: noCliPty,
492
+ readCredentialsJson: async () => {
493
+ throw new Error("bad json");
494
+ },
495
+ });
496
+
497
+ expect(snapshot.status).toBe("error");
498
+ expect(snapshot.message).toMatch(/unable to read/i);
499
+ });
500
+
501
+ it("serves a persisted snapshot immediately and refreshes in background", async () => {
502
+ const projectStateDir = mkdtempSync(join(tmpdir(), "octogent-claude-usage-"));
503
+ temporaryDirectories.push(projectStateDir);
504
+
505
+ await readClaudeUsageSnapshot({
506
+ now: () => new Date("2026-03-03T12:00:00.000Z"),
507
+ projectStateDir,
508
+ spawnCliUsage: noCliPty,
509
+ readCredentialsJson: async () => validCredentials(),
510
+ fetchImpl: async () =>
511
+ new Response(usageResponseBody, {
512
+ status: 200,
513
+ headers: { "Content-Type": "application/json" },
514
+ }),
515
+ });
516
+
517
+ resetCliSession();
518
+
519
+ const fetchMock = vi.fn<typeof fetch>().mockImplementation(
520
+ async () =>
521
+ await new Promise<Response>((resolve) => {
522
+ setTimeout(() => {
523
+ resolve(
524
+ new Response(usageResponseBody, {
525
+ status: 200,
526
+ headers: { "Content-Type": "application/json" },
527
+ }),
528
+ );
529
+ }, 50);
530
+ }),
531
+ );
532
+
533
+ const startedAt = Date.now();
534
+ const snapshot = await readClaudeUsageSnapshot({
535
+ now: () => new Date("2026-03-03T12:05:00.000Z"),
536
+ projectStateDir,
537
+ backgroundRefreshOnly: true,
538
+ spawnCliUsage: noCliPty,
539
+ readCredentialsJson: async () => validCredentials(),
540
+ fetchImpl: fetchMock,
541
+ });
542
+
543
+ expect(snapshot.status).toBe("ok");
544
+ expect(snapshot.source).toBe("oauth-api");
545
+ expect(snapshot.primaryUsedPercent).toBe(14);
546
+ expect(Date.now() - startedAt).toBeLessThan(40);
547
+
548
+ await new Promise((resolve) => setTimeout(resolve, 90));
549
+ expect(fetchMock).toHaveBeenCalledTimes(1);
550
+ });
551
+
552
+ it("returns immediately on a cold cache miss when background refresh mode is enabled", async () => {
553
+ const fetchMock = vi.fn<typeof fetch>().mockImplementation(
554
+ async () =>
555
+ await new Promise<Response>((resolve) => {
556
+ setTimeout(() => {
557
+ resolve(
558
+ new Response(usageResponseBody, {
559
+ status: 200,
560
+ headers: { "Content-Type": "application/json" },
561
+ }),
562
+ );
563
+ }, 50);
564
+ }),
565
+ );
566
+
567
+ const startedAt = Date.now();
568
+ const snapshot = await readClaudeUsageSnapshot({
569
+ now: () => new Date("2026-03-03T12:00:00.000Z"),
570
+ backgroundRefreshOnly: true,
571
+ spawnCliUsage: noCliPty,
572
+ readCredentialsJson: async () => validCredentials(),
573
+ fetchImpl: fetchMock,
574
+ });
575
+
576
+ expect(snapshot.status).toBe("unavailable");
577
+ expect(snapshot.message).toMatch(/refresh in progress/i);
578
+ expect(Date.now() - startedAt).toBeLessThan(40);
579
+
580
+ await new Promise((resolve) => setTimeout(resolve, 90));
581
+ expect(fetchMock).toHaveBeenCalledTimes(1);
582
+ });
583
+ });