@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,100 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+
3
+ import { readGithubRepoSummary } from "../src/githubRepoSummary";
4
+
5
+ describe("readGithubRepoSummary", () => {
6
+ it("returns repository stats, 30-day commit series, and last 50 recent commits", async () => {
7
+ const recentCommitRecords = Array.from({ length: 52 }, (_, index) => {
8
+ const offset = index + 1;
9
+ const day = String(Math.min(28, 28 - index)).padStart(2, "0");
10
+ return `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa${offset.toString(16)}\u001fshort${offset}\u001fAuthor ${offset}\u001fauthor${offset}@example.com\u001f2026-02-${day}T10:00:00.000Z\u001fbody ${offset}\u001fsubject ${offset}\u001e`;
11
+ }).join("");
12
+
13
+ const shortstatRecords = Array.from({ length: 52 }, (_, index) => {
14
+ const offset = index + 1;
15
+ return `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa${offset.toString(16)}\n ${offset + 1} files changed, ${offset * 10} insertions(+), ${offset * 2} deletions(-)\n`;
16
+ }).join("");
17
+
18
+ const runCommand = vi.fn(async (command: string, args: string[]) => {
19
+ if (command === "gh" && args[0] === "repo" && args[1] === "view") {
20
+ return {
21
+ stdout: "hesamsheikh/octogent\n",
22
+ stderr: "",
23
+ };
24
+ }
25
+
26
+ if (command === "gh" && args[0] === "api" && args[1] === "graphql") {
27
+ return {
28
+ stdout: JSON.stringify({
29
+ data: {
30
+ repository: {
31
+ nameWithOwner: "hesamsheikh/octogent",
32
+ stargazerCount: 42,
33
+ issues: { totalCount: 7 },
34
+ pullRequests: { totalCount: 3 },
35
+ },
36
+ },
37
+ }),
38
+ stderr: "",
39
+ };
40
+ }
41
+
42
+ if (command === "git" && args[0] === "log" && args.includes("--pretty=format:%ad")) {
43
+ return {
44
+ stdout: "2026-02-27\n2026-02-27\n2026-03-01\n",
45
+ stderr: "",
46
+ };
47
+ }
48
+
49
+ if (
50
+ command === "git" &&
51
+ args[0] === "log" &&
52
+ args.includes("--pretty=format:%H%x1f%h%x1f%an%x1f%ae%x1f%aI%x1f%b%x1f%s%x1e")
53
+ ) {
54
+ return {
55
+ stdout: recentCommitRecords,
56
+ stderr: "",
57
+ };
58
+ }
59
+
60
+ if (command === "git" && args[0] === "log" && args.includes("--shortstat")) {
61
+ return {
62
+ stdout: shortstatRecords,
63
+ stderr: "",
64
+ };
65
+ }
66
+
67
+ throw new Error(`Unexpected command: ${command} ${args.join(" ")}`);
68
+ });
69
+
70
+ const snapshot = await readGithubRepoSummary({
71
+ now: () => new Date("2026-03-03T12:00:00.000Z"),
72
+ runCommand,
73
+ cwd: "/workspace",
74
+ env: {},
75
+ });
76
+
77
+ expect(snapshot.status).toBe("ok");
78
+ expect(snapshot.repo).toBe("hesamsheikh/octogent");
79
+ expect(snapshot.stargazerCount).toBe(42);
80
+ expect(snapshot.openIssueCount).toBe(7);
81
+ expect(snapshot.openPullRequestCount).toBe(3);
82
+ expect(snapshot.commitsPerDay).toHaveLength(30);
83
+ expect(snapshot.commitsPerDay?.find((point) => point.date === "2026-02-27")?.count).toBe(2);
84
+ expect(snapshot.commitsPerDay?.find((point) => point.date === "2026-03-01")?.count).toBe(1);
85
+ expect(snapshot.recentCommits).toHaveLength(50);
86
+ expect(snapshot.recentCommits?.[0]).toEqual({
87
+ hash: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1",
88
+ shortHash: "short1",
89
+ subject: "subject 1",
90
+ authorName: "Author 1",
91
+ authorEmail: "author1@example.com",
92
+ authoredAt: "2026-02-28T10:00:00.000Z",
93
+ body: "body 1",
94
+ filesChanged: 2,
95
+ insertions: 10,
96
+ deletions: 2,
97
+ });
98
+ expect(snapshot.recentCommits?.[49]?.shortHash).toBe("short50");
99
+ });
100
+ });
@@ -0,0 +1,33 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+
3
+ import { isVerboseLoggingEnabled, logVerbose } from "../src/logging";
4
+
5
+ describe("logging", () => {
6
+ afterEach(() => {
7
+ vi.unstubAllEnvs();
8
+ vi.restoreAllMocks();
9
+ });
10
+
11
+ it("keeps verbose logs disabled by default", () => {
12
+ vi.stubEnv("OCTOGENT_VERBOSE_LOGS", undefined);
13
+
14
+ expect(isVerboseLoggingEnabled()).toBe(false);
15
+ });
16
+
17
+ it("enables verbose logs when OCTOGENT_VERBOSE_LOGS=1", () => {
18
+ vi.stubEnv("OCTOGENT_VERBOSE_LOGS", "1");
19
+
20
+ expect(isVerboseLoggingEnabled()).toBe(true);
21
+ });
22
+
23
+ it("only writes verbose logs when enabled", () => {
24
+ const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
25
+
26
+ logVerbose("hidden");
27
+ vi.stubEnv("OCTOGENT_VERBOSE_LOGS", "1");
28
+ logVerbose("shown");
29
+
30
+ expect(consoleSpy).toHaveBeenCalledTimes(1);
31
+ expect(consoleSpy).toHaveBeenCalledWith("shown");
32
+ });
33
+ });
@@ -0,0 +1,467 @@
1
+ import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
5
+
6
+ const { spawnMock } = vi.hoisted(() => ({
7
+ spawnMock: vi.fn(),
8
+ }));
9
+
10
+ vi.mock("node-pty", () => ({
11
+ spawn: spawnMock,
12
+ }));
13
+
14
+ import { createApiServer } from "../src/createApiServer";
15
+ import type { GitClient } from "../src/terminalRuntime";
16
+
17
+ class FakeGitClient implements GitClient {
18
+ private readonly worktrees = new Map<
19
+ string,
20
+ { branchName: string; baseRef: string; cwd: string }
21
+ >();
22
+ private readonly branches = new Set<string>();
23
+
24
+ assertAvailable(): void {}
25
+
26
+ isRepository(): boolean {
27
+ return true;
28
+ }
29
+
30
+ addWorktree({
31
+ cwd,
32
+ path,
33
+ branchName,
34
+ baseRef,
35
+ }: {
36
+ cwd: string;
37
+ path: string;
38
+ branchName: string;
39
+ baseRef: string;
40
+ }): void {
41
+ if (this.worktrees.has(path)) {
42
+ throw new Error(`Worktree already exists: ${path}`);
43
+ }
44
+ mkdirSync(path, { recursive: true });
45
+ this.branches.add(branchName);
46
+ this.worktrees.set(path, { cwd, branchName, baseRef });
47
+ }
48
+
49
+ removeWorktree({ path }: { cwd: string; path: string }): void {
50
+ this.worktrees.delete(path);
51
+ }
52
+
53
+ removeBranch({ branchName }: { cwd: string; branchName: string }): void {
54
+ this.branches.delete(branchName);
55
+ }
56
+
57
+ readWorktreeStatus(): {
58
+ branchName: string;
59
+ upstreamBranchName: string | null;
60
+ isDirty: boolean;
61
+ aheadCount: number;
62
+ behindCount: number;
63
+ insertedLineCount: number;
64
+ deletedLineCount: number;
65
+ hasConflicts: boolean;
66
+ changedFiles: string[];
67
+ defaultBaseBranchName: string | null;
68
+ } {
69
+ return {
70
+ branchName: "octogent/tentacle-1",
71
+ upstreamBranchName: null,
72
+ isDirty: false,
73
+ aheadCount: 0,
74
+ behindCount: 0,
75
+ insertedLineCount: 0,
76
+ deletedLineCount: 0,
77
+ hasConflicts: false,
78
+ changedFiles: [],
79
+ defaultBaseBranchName: "main",
80
+ };
81
+ }
82
+
83
+ commitAll(): void {}
84
+
85
+ pushCurrentBranch(): void {}
86
+
87
+ syncWithBase(): void {}
88
+
89
+ readCurrentBranchPullRequest(): {
90
+ number: number;
91
+ url: string;
92
+ title: string;
93
+ baseRef: string;
94
+ headRef: string;
95
+ state: "OPEN" | "MERGED" | "CLOSED";
96
+ isDraft: boolean;
97
+ mergeable: "MERGEABLE" | "CONFLICTING" | "UNKNOWN";
98
+ mergeStateStatus: string | null;
99
+ } | null {
100
+ return null;
101
+ }
102
+
103
+ createPullRequest(): {
104
+ number: number;
105
+ url: string;
106
+ title: string;
107
+ baseRef: string;
108
+ headRef: string;
109
+ state: "OPEN" | "MERGED" | "CLOSED";
110
+ isDraft: boolean;
111
+ mergeable: "MERGEABLE" | "CONFLICTING" | "UNKNOWN";
112
+ mergeStateStatus: string | null;
113
+ } | null {
114
+ return null;
115
+ }
116
+
117
+ mergeCurrentBranchPullRequest(): void {}
118
+ }
119
+
120
+ describe("monitor API routes", () => {
121
+ let stopServer: (() => Promise<void>) | null = null;
122
+ const temporaryDirectories: string[] = [];
123
+
124
+ afterEach(async () => {
125
+ if (stopServer) {
126
+ await stopServer();
127
+ stopServer = null;
128
+ }
129
+
130
+ for (const directory of temporaryDirectories) {
131
+ rmSync(directory, { recursive: true, force: true });
132
+ }
133
+ temporaryDirectories.length = 0;
134
+ });
135
+
136
+ const startServer = async (options: Partial<Parameters<typeof createApiServer>[0]> = {}) => {
137
+ const workspaceCwd =
138
+ options.workspaceCwd ??
139
+ (() => {
140
+ const directory = mkdtempSync(join(tmpdir(), "octogent-monitor-api-test-"));
141
+ temporaryDirectories.push(directory);
142
+ return directory;
143
+ })();
144
+
145
+ const apiServer = createApiServer({
146
+ workspaceCwd,
147
+ gitClient: options.gitClient ?? new FakeGitClient(),
148
+ ...options,
149
+ });
150
+
151
+ const address = await apiServer.start(0, "127.0.0.1");
152
+ stopServer = () => apiServer.stop();
153
+ return `http://${address.host}:${address.port}`;
154
+ };
155
+
156
+ it("returns empty query terms for a fresh workspace config", async () => {
157
+ const baseUrl = await startServer();
158
+
159
+ const response = await fetch(`${baseUrl}/api/monitor/config`, {
160
+ method: "GET",
161
+ headers: {
162
+ Accept: "application/json",
163
+ },
164
+ });
165
+
166
+ expect(response.status).toBe(200);
167
+ await expect(response.json()).resolves.toMatchObject({
168
+ providerId: "x",
169
+ queryTerms: [],
170
+ refreshPolicy: {
171
+ maxPosts: 30,
172
+ searchWindowDays: 7,
173
+ },
174
+ });
175
+ });
176
+
177
+ it("saves credentials even when query terms have not been configured yet", async () => {
178
+ const baseUrl = await startServer();
179
+
180
+ const patchResponse = await fetch(`${baseUrl}/api/monitor/config`, {
181
+ method: "PATCH",
182
+ headers: {
183
+ Accept: "application/json",
184
+ "Content-Type": "application/json",
185
+ },
186
+ body: JSON.stringify({
187
+ providerId: "x",
188
+ validateCredentials: false,
189
+ credentials: {
190
+ bearerToken: "x-example-secret-token",
191
+ },
192
+ }),
193
+ });
194
+
195
+ expect(patchResponse.status).toBe(200);
196
+ await expect(patchResponse.json()).resolves.toMatchObject({
197
+ providerId: "x",
198
+ queryTerms: [],
199
+ refreshPolicy: {
200
+ maxPosts: 30,
201
+ searchWindowDays: 7,
202
+ },
203
+ providers: {
204
+ x: {
205
+ credentials: {
206
+ isConfigured: true,
207
+ },
208
+ },
209
+ },
210
+ });
211
+
212
+ const feedResponse = await fetch(`${baseUrl}/api/monitor/feed`, {
213
+ method: "GET",
214
+ headers: {
215
+ Accept: "application/json",
216
+ },
217
+ });
218
+
219
+ expect(feedResponse.status).toBe(200);
220
+ await expect(feedResponse.json()).resolves.toMatchObject({
221
+ providerId: "x",
222
+ queryTerms: [],
223
+ lastError: "At least one monitor query term is required.",
224
+ });
225
+ });
226
+
227
+ it("saves monitor credentials and returns redacted config", async () => {
228
+ const baseUrl = await startServer();
229
+
230
+ const patchResponse = await fetch(`${baseUrl}/api/monitor/config`, {
231
+ method: "PATCH",
232
+ headers: {
233
+ Accept: "application/json",
234
+ "Content-Type": "application/json",
235
+ },
236
+ body: JSON.stringify({
237
+ providerId: "x",
238
+ validateCredentials: false,
239
+ queryTerms: ["AI Engineering", "Codex"],
240
+ credentials: {
241
+ bearerToken: "x-example-secret-token",
242
+ apiKey: "x-api-key-value",
243
+ apiSecret: "x-api-secret-value",
244
+ },
245
+ }),
246
+ });
247
+
248
+ expect(patchResponse.status).toBe(200);
249
+ const patchPayload = (await patchResponse.json()) as Record<string, unknown>;
250
+ expect(patchPayload.providerId).toBe("x");
251
+ expect(patchPayload.queryTerms).toEqual(["AI Engineering", "Codex"]);
252
+ expect(
253
+ (patchPayload.refreshPolicy as { maxPosts?: number; searchWindowDays?: number } | undefined)
254
+ ?.maxPosts,
255
+ ).toBe(30);
256
+ expect(
257
+ (patchPayload.refreshPolicy as { maxPosts?: number; searchWindowDays?: number } | undefined)
258
+ ?.searchWindowDays,
259
+ ).toBe(7);
260
+
261
+ const providers = patchPayload.providers as Record<string, unknown>;
262
+ const provider = providers.x as Record<string, unknown>;
263
+ const credentials = provider.credentials as Record<string, unknown>;
264
+
265
+ expect(credentials.isConfigured).toBe(true);
266
+ expect(typeof credentials.bearerTokenHint).toBe("string");
267
+ expect((credentials.bearerTokenHint as string).endsWith("oken")).toBe(true);
268
+ expect(typeof credentials.apiKeyHint).toBe("string");
269
+ expect((credentials.apiKeyHint as string).endsWith("alue")).toBe(true);
270
+ expect(Object.hasOwn(credentials, "bearerToken")).toBe(false);
271
+ expect(Object.hasOwn(credentials, "apiSecret")).toBe(false);
272
+
273
+ const getResponse = await fetch(`${baseUrl}/api/monitor/config`, {
274
+ method: "GET",
275
+ headers: {
276
+ Accept: "application/json",
277
+ },
278
+ });
279
+
280
+ expect(getResponse.status).toBe(200);
281
+ await expect(getResponse.json()).resolves.toEqual(patchPayload);
282
+ });
283
+
284
+ it("returns 400 for invalid monitor config patch payload", async () => {
285
+ const baseUrl = await startServer();
286
+
287
+ const response = await fetch(`${baseUrl}/api/monitor/config`, {
288
+ method: "PATCH",
289
+ headers: {
290
+ Accept: "application/json",
291
+ "Content-Type": "application/json",
292
+ },
293
+ body: JSON.stringify({
294
+ queryTerms: "AI Engineering",
295
+ }),
296
+ });
297
+
298
+ expect(response.status).toBe(400);
299
+ await expect(response.json()).resolves.toEqual({
300
+ error: "queryTerms must be an array of strings.",
301
+ });
302
+ });
303
+
304
+ it("returns 400 for invalid refreshPolicy.maxPosts in monitor config patch payload", async () => {
305
+ const baseUrl = await startServer();
306
+
307
+ const response = await fetch(`${baseUrl}/api/monitor/config`, {
308
+ method: "PATCH",
309
+ headers: {
310
+ Accept: "application/json",
311
+ "Content-Type": "application/json",
312
+ },
313
+ body: JSON.stringify({
314
+ refreshPolicy: {
315
+ maxPosts: 0,
316
+ },
317
+ }),
318
+ });
319
+
320
+ expect(response.status).toBe(400);
321
+ await expect(response.json()).resolves.toEqual({
322
+ error: "refreshPolicy.maxPosts must be a positive number.",
323
+ });
324
+ });
325
+
326
+ it("returns 400 for invalid refreshPolicy.searchWindowDays in monitor config patch payload", async () => {
327
+ const baseUrl = await startServer();
328
+
329
+ const response = await fetch(`${baseUrl}/api/monitor/config`, {
330
+ method: "PATCH",
331
+ headers: {
332
+ Accept: "application/json",
333
+ "Content-Type": "application/json",
334
+ },
335
+ body: JSON.stringify({
336
+ refreshPolicy: {
337
+ searchWindowDays: 2,
338
+ },
339
+ }),
340
+ });
341
+
342
+ expect(response.status).toBe(400);
343
+ await expect(response.json()).resolves.toEqual({
344
+ error: "refreshPolicy.searchWindowDays must be one of: 1, 3, 7.",
345
+ });
346
+ });
347
+
348
+ it("persists configurable maxPosts in monitor refresh policy", async () => {
349
+ const baseUrl = await startServer();
350
+
351
+ const patchResponse = await fetch(`${baseUrl}/api/monitor/config`, {
352
+ method: "PATCH",
353
+ headers: {
354
+ Accept: "application/json",
355
+ "Content-Type": "application/json",
356
+ },
357
+ body: JSON.stringify({
358
+ refreshPolicy: {
359
+ maxPosts: 12,
360
+ searchWindowDays: 3,
361
+ },
362
+ }),
363
+ });
364
+
365
+ expect(patchResponse.status).toBe(200);
366
+ await expect(patchResponse.json()).resolves.toMatchObject({
367
+ refreshPolicy: {
368
+ maxPosts: 12,
369
+ searchWindowDays: 3,
370
+ },
371
+ });
372
+
373
+ const getResponse = await fetch(`${baseUrl}/api/monitor/config`, {
374
+ method: "GET",
375
+ headers: {
376
+ Accept: "application/json",
377
+ },
378
+ });
379
+ expect(getResponse.status).toBe(200);
380
+ await expect(getResponse.json()).resolves.toMatchObject({
381
+ refreshPolicy: {
382
+ maxPosts: 12,
383
+ searchWindowDays: 3,
384
+ },
385
+ });
386
+ });
387
+
388
+ it("uses non-forced read for feed and forced read for manual refresh", async () => {
389
+ const readFeedCalls: Array<Record<string, unknown>> = [];
390
+ const monitorService = {
391
+ readConfig: async () => ({
392
+ providerId: "x",
393
+ queryTerms: ["Codex"],
394
+ refreshPolicy: {
395
+ maxCacheAgeMs: 24 * 60 * 60 * 1000,
396
+ maxPosts: 30,
397
+ searchWindowDays: 7,
398
+ },
399
+ providers: {
400
+ x: {
401
+ credentials: {
402
+ isConfigured: true,
403
+ bearerTokenHint: "***oken",
404
+ apiKeyHint: null,
405
+ hasApiSecret: false,
406
+ hasAccessToken: false,
407
+ hasAccessTokenSecret: false,
408
+ updatedAt: "2026-02-28T12:00:00.000Z",
409
+ },
410
+ },
411
+ },
412
+ }),
413
+ patchConfig: async () => {
414
+ throw new Error("not implemented");
415
+ },
416
+ readFeed: async (options?: Record<string, unknown>) => {
417
+ readFeedCalls.push(options ?? {});
418
+ return {
419
+ providerId: "x",
420
+ queryTerms: ["Codex"],
421
+ refreshPolicy: {
422
+ maxCacheAgeMs: 24 * 60 * 60 * 1000,
423
+ maxPosts: 30,
424
+ searchWindowDays: 7,
425
+ },
426
+ lastFetchedAt: "2026-02-28T12:00:00.000Z",
427
+ staleAfter: "2026-03-01T12:00:00.000Z",
428
+ isStale: false,
429
+ lastError: null,
430
+ posts: [],
431
+ usage: null,
432
+ };
433
+ },
434
+ };
435
+
436
+ const baseUrl = await startServer({
437
+ monitorService: monitorService as never,
438
+ });
439
+
440
+ const feedResponse = await fetch(`${baseUrl}/api/monitor/feed`, {
441
+ method: "GET",
442
+ headers: {
443
+ Accept: "application/json",
444
+ },
445
+ });
446
+ expect(feedResponse.status).toBe(200);
447
+
448
+ const manualRefreshResponse = await fetch(`${baseUrl}/api/monitor/refresh`, {
449
+ method: "POST",
450
+ headers: {
451
+ Accept: "application/json",
452
+ },
453
+ });
454
+ expect(manualRefreshResponse.status).toBe(200);
455
+
456
+ expect(readFeedCalls).toEqual([
457
+ {
458
+ forceRefresh: false,
459
+ refreshIfStale: true,
460
+ },
461
+ {
462
+ forceRefresh: true,
463
+ refreshIfStale: true,
464
+ },
465
+ ]);
466
+ });
467
+ });
@@ -0,0 +1,104 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { isMonitorCacheStale, rankAndLimitPostsByLikes } from "../src/monitor/service";
4
+ import type { MonitorPost } from "../src/monitor/types";
5
+ import { buildXRecentSearchQuery } from "../src/monitor/xProvider";
6
+
7
+ describe("monitor core logic", () => {
8
+ it("builds X recent-search query with OR terms, lang filter, and no-retweet filter", () => {
9
+ const query = buildXRecentSearchQuery(["AI Engineering", "Codex"]);
10
+
11
+ expect(query).toContain('"AI Engineering"');
12
+ expect(query).toContain("Codex");
13
+ expect(query).toContain(" OR ");
14
+ expect(query).toContain("lang:en");
15
+ expect(query).toContain("-is:retweet");
16
+ });
17
+
18
+ it("throws when no valid query terms are provided", () => {
19
+ expect(() => buildXRecentSearchQuery(["", " "])).toThrow(
20
+ "At least one X query term is required.",
21
+ );
22
+ });
23
+
24
+ it("flags cache as stale when older than refresh policy window", () => {
25
+ const now = new Date("2026-02-28T12:00:00.000Z");
26
+ const configuredQueryTerms = ["Codex"];
27
+
28
+ expect(
29
+ isMonitorCacheStale({
30
+ now,
31
+ maxCacheAgeMs: 24 * 60 * 60 * 1000,
32
+ lastFetchedAt: null,
33
+ cachedQueryTerms: configuredQueryTerms,
34
+ currentQueryTerms: configuredQueryTerms,
35
+ }),
36
+ ).toBe(true);
37
+
38
+ expect(
39
+ isMonitorCacheStale({
40
+ now,
41
+ maxCacheAgeMs: 24 * 60 * 60 * 1000,
42
+ lastFetchedAt: "2026-02-27T13:00:00.000Z",
43
+ cachedQueryTerms: configuredQueryTerms,
44
+ currentQueryTerms: configuredQueryTerms,
45
+ }),
46
+ ).toBe(false);
47
+
48
+ expect(
49
+ isMonitorCacheStale({
50
+ now,
51
+ maxCacheAgeMs: 24 * 60 * 60 * 1000,
52
+ lastFetchedAt: "2026-02-27T11:59:59.000Z",
53
+ cachedQueryTerms: configuredQueryTerms,
54
+ currentQueryTerms: configuredQueryTerms,
55
+ }),
56
+ ).toBe(true);
57
+
58
+ expect(
59
+ isMonitorCacheStale({
60
+ now,
61
+ maxCacheAgeMs: 24 * 60 * 60 * 1000,
62
+ lastFetchedAt: "2026-02-28T11:00:00.000Z",
63
+ cachedQueryTerms: ["Codex"],
64
+ currentQueryTerms: ["Agent Engineering"],
65
+ }),
66
+ ).toBe(true);
67
+ });
68
+
69
+ it("dedupes, ranks by like count desc, and limits to top 30", () => {
70
+ const basePosts: MonitorPost[] = Array.from({ length: 32 }).map((_, index) => ({
71
+ source: "x",
72
+ id: `id-${index + 1}`,
73
+ text: `Post ${index + 1}`,
74
+ author: `author-${index + 1}`,
75
+ createdAt: `2026-02-${String((index % 28) + 1).padStart(2, "0")}T10:00:00.000Z`,
76
+ likeCount: index,
77
+ permalink: `https://x.com/user/status/${index + 1}`,
78
+ matchedQueryTerm: "Codex",
79
+ }));
80
+ const firstPost = basePosts[0] as MonitorPost;
81
+ const lastPost = basePosts[31] as MonitorPost;
82
+
83
+ const withDuplicate: MonitorPost[] = [
84
+ ...basePosts,
85
+ {
86
+ ...firstPost,
87
+ text: "duplicate with lower likes should be dropped",
88
+ likeCount: 0,
89
+ },
90
+ {
91
+ ...lastPost,
92
+ likeCount: 999,
93
+ },
94
+ ];
95
+
96
+ const ranked = rankAndLimitPostsByLikes(withDuplicate, 30);
97
+
98
+ expect(ranked).toHaveLength(30);
99
+ expect(ranked[0]?.id).toBe("id-32");
100
+ expect(ranked[0]?.likeCount).toBe(999);
101
+ expect(new Set(ranked.map((post) => post.id)).size).toBe(ranked.length);
102
+ expect(ranked.at(-1)?.likeCount).toBeGreaterThanOrEqual(2);
103
+ });
104
+ });