orbital-command 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 (325) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +396 -0
  3. package/bin/orbital.js +362 -0
  4. package/dist/assets/WorkflowVisualizer-BZ21PIIF.js +84 -0
  5. package/dist/assets/WorkflowVisualizer-BZV40eAE.css +1 -0
  6. package/dist/assets/charts-D__PA1zp.js +72 -0
  7. package/dist/assets/index-D1G6i0nS.css +1 -0
  8. package/dist/assets/index-DpItvKpf.js +419 -0
  9. package/dist/assets/ui-BvF022GT.js +53 -0
  10. package/dist/assets/vendor-Dzv9lrRc.js +59 -0
  11. package/dist/index.html +19 -0
  12. package/dist/scanner-sweep.png +0 -0
  13. package/dist/server/server/adapters/index.js +34 -0
  14. package/dist/server/server/adapters/iterm2-adapter.js +29 -0
  15. package/dist/server/server/adapters/subprocess-adapter.js +21 -0
  16. package/dist/server/server/adapters/terminal-adapter.js +1 -0
  17. package/dist/server/server/config.js +156 -0
  18. package/dist/server/server/database.js +90 -0
  19. package/dist/server/server/index.js +372 -0
  20. package/dist/server/server/init.js +811 -0
  21. package/dist/server/server/parsers/event-parser.js +64 -0
  22. package/dist/server/server/parsers/scope-parser.js +188 -0
  23. package/dist/server/server/routes/config-routes.js +163 -0
  24. package/dist/server/server/routes/data-routes.js +461 -0
  25. package/dist/server/server/routes/dispatch-routes.js +215 -0
  26. package/dist/server/server/routes/git-routes.js +92 -0
  27. package/dist/server/server/routes/scope-routes.js +215 -0
  28. package/dist/server/server/routes/sprint-routes.js +116 -0
  29. package/dist/server/server/routes/version-routes.js +130 -0
  30. package/dist/server/server/routes/workflow-routes.js +185 -0
  31. package/dist/server/server/schema.js +90 -0
  32. package/dist/server/server/services/batch-orchestrator.js +253 -0
  33. package/dist/server/server/services/claude-session-service.js +352 -0
  34. package/dist/server/server/services/config-service.js +132 -0
  35. package/dist/server/server/services/deploy-service.js +51 -0
  36. package/dist/server/server/services/event-service.js +63 -0
  37. package/dist/server/server/services/gate-service.js +83 -0
  38. package/dist/server/server/services/git-service.js +309 -0
  39. package/dist/server/server/services/github-service.js +145 -0
  40. package/dist/server/server/services/readiness-service.js +184 -0
  41. package/dist/server/server/services/scope-cache.js +72 -0
  42. package/dist/server/server/services/scope-service.js +424 -0
  43. package/dist/server/server/services/sprint-orchestrator.js +312 -0
  44. package/dist/server/server/services/sprint-service.js +293 -0
  45. package/dist/server/server/services/workflow-service.js +397 -0
  46. package/dist/server/server/utils/cc-hooks-parser.js +49 -0
  47. package/dist/server/server/utils/dispatch-utils.js +305 -0
  48. package/dist/server/server/utils/logger.js +86 -0
  49. package/dist/server/server/utils/terminal-launcher.js +388 -0
  50. package/dist/server/server/utils/worktree-manager.js +98 -0
  51. package/dist/server/server/watchers/event-watcher.js +81 -0
  52. package/dist/server/server/watchers/scope-watcher.js +33 -0
  53. package/dist/server/shared/api-types.js +5 -0
  54. package/dist/server/shared/default-workflow.json +616 -0
  55. package/dist/server/shared/workflow-config.js +44 -0
  56. package/dist/server/shared/workflow-engine.js +353 -0
  57. package/index.html +15 -0
  58. package/package.json +110 -0
  59. package/postcss.config.js +6 -0
  60. package/schemas/orbital.config.schema.json +83 -0
  61. package/scripts/postinstall.js +24 -0
  62. package/scripts/start.sh +20 -0
  63. package/server/adapters/index.ts +41 -0
  64. package/server/adapters/iterm2-adapter.ts +37 -0
  65. package/server/adapters/subprocess-adapter.ts +25 -0
  66. package/server/adapters/terminal-adapter.ts +24 -0
  67. package/server/config.ts +234 -0
  68. package/server/database.ts +107 -0
  69. package/server/index.ts +452 -0
  70. package/server/init.ts +891 -0
  71. package/server/parsers/event-parser.ts +74 -0
  72. package/server/parsers/scope-parser.ts +240 -0
  73. package/server/routes/config-routes.ts +182 -0
  74. package/server/routes/data-routes.ts +548 -0
  75. package/server/routes/dispatch-routes.ts +275 -0
  76. package/server/routes/git-routes.ts +112 -0
  77. package/server/routes/scope-routes.ts +262 -0
  78. package/server/routes/sprint-routes.ts +142 -0
  79. package/server/routes/version-routes.ts +156 -0
  80. package/server/routes/workflow-routes.ts +198 -0
  81. package/server/schema.ts +90 -0
  82. package/server/services/batch-orchestrator.ts +286 -0
  83. package/server/services/claude-session-service.ts +441 -0
  84. package/server/services/config-service.ts +151 -0
  85. package/server/services/deploy-service.ts +98 -0
  86. package/server/services/event-service.ts +98 -0
  87. package/server/services/gate-service.ts +126 -0
  88. package/server/services/git-service.ts +391 -0
  89. package/server/services/github-service.ts +183 -0
  90. package/server/services/readiness-service.ts +250 -0
  91. package/server/services/scope-cache.ts +81 -0
  92. package/server/services/scope-service.ts +476 -0
  93. package/server/services/sprint-orchestrator.ts +361 -0
  94. package/server/services/sprint-service.ts +415 -0
  95. package/server/services/workflow-service.ts +461 -0
  96. package/server/utils/cc-hooks-parser.ts +70 -0
  97. package/server/utils/dispatch-utils.ts +395 -0
  98. package/server/utils/logger.ts +109 -0
  99. package/server/utils/terminal-launcher.ts +462 -0
  100. package/server/utils/worktree-manager.ts +104 -0
  101. package/server/watchers/event-watcher.ts +100 -0
  102. package/server/watchers/scope-watcher.ts +38 -0
  103. package/shared/api-types.ts +20 -0
  104. package/shared/default-workflow.json +616 -0
  105. package/shared/workflow-config.ts +170 -0
  106. package/shared/workflow-engine.ts +427 -0
  107. package/src/App.tsx +33 -0
  108. package/src/components/AgentBadge.tsx +40 -0
  109. package/src/components/BatchPreflightModal.tsx +115 -0
  110. package/src/components/CardDisplayToggle.tsx +74 -0
  111. package/src/components/ColumnHeaderActions.tsx +55 -0
  112. package/src/components/ColumnMenu.tsx +99 -0
  113. package/src/components/DeployHistory.tsx +141 -0
  114. package/src/components/DispatchModal.tsx +164 -0
  115. package/src/components/DispatchPopover.tsx +139 -0
  116. package/src/components/DragOverlay.tsx +25 -0
  117. package/src/components/DriftSidebar.tsx +140 -0
  118. package/src/components/EnvironmentStrip.tsx +88 -0
  119. package/src/components/ErrorBoundary.tsx +62 -0
  120. package/src/components/FilterChip.tsx +105 -0
  121. package/src/components/GateIndicator.tsx +33 -0
  122. package/src/components/IdeaDetailModal.tsx +190 -0
  123. package/src/components/IdeaFormDialog.tsx +113 -0
  124. package/src/components/KanbanColumn.tsx +201 -0
  125. package/src/components/MarkdownRenderer.tsx +114 -0
  126. package/src/components/NeonGrid.tsx +128 -0
  127. package/src/components/PromotionQueue.tsx +89 -0
  128. package/src/components/ScopeCard.tsx +234 -0
  129. package/src/components/ScopeDetailModal.tsx +255 -0
  130. package/src/components/ScopeFilterBar.tsx +152 -0
  131. package/src/components/SearchInput.tsx +102 -0
  132. package/src/components/SessionPanel.tsx +335 -0
  133. package/src/components/SprintContainer.tsx +303 -0
  134. package/src/components/SprintDependencyDialog.tsx +78 -0
  135. package/src/components/SprintPreflightModal.tsx +138 -0
  136. package/src/components/StatusBar.tsx +168 -0
  137. package/src/components/SwimCell.tsx +67 -0
  138. package/src/components/SwimLaneRow.tsx +94 -0
  139. package/src/components/SwimlaneBoardView.tsx +108 -0
  140. package/src/components/VersionBadge.tsx +139 -0
  141. package/src/components/ViewModeSelector.tsx +114 -0
  142. package/src/components/config/AgentChip.tsx +53 -0
  143. package/src/components/config/AgentCreateDialog.tsx +321 -0
  144. package/src/components/config/AgentEditor.tsx +175 -0
  145. package/src/components/config/DirectoryTree.tsx +582 -0
  146. package/src/components/config/FileEditor.tsx +550 -0
  147. package/src/components/config/HookChip.tsx +50 -0
  148. package/src/components/config/StageCard.tsx +198 -0
  149. package/src/components/config/TransitionZone.tsx +173 -0
  150. package/src/components/config/UnifiedWorkflowPipeline.tsx +216 -0
  151. package/src/components/config/WorkflowPipeline.tsx +161 -0
  152. package/src/components/source-control/BranchList.tsx +93 -0
  153. package/src/components/source-control/BranchPanel.tsx +105 -0
  154. package/src/components/source-control/CommitLog.tsx +100 -0
  155. package/src/components/source-control/CommitRow.tsx +47 -0
  156. package/src/components/source-control/GitHubPanel.tsx +110 -0
  157. package/src/components/source-control/GitHubSetupGuide.tsx +52 -0
  158. package/src/components/source-control/GitOverviewBar.tsx +101 -0
  159. package/src/components/source-control/PullRequestList.tsx +69 -0
  160. package/src/components/source-control/WorktreeList.tsx +80 -0
  161. package/src/components/ui/badge.tsx +41 -0
  162. package/src/components/ui/button.tsx +55 -0
  163. package/src/components/ui/card.tsx +78 -0
  164. package/src/components/ui/dialog.tsx +94 -0
  165. package/src/components/ui/popover.tsx +33 -0
  166. package/src/components/ui/scroll-area.tsx +54 -0
  167. package/src/components/ui/separator.tsx +28 -0
  168. package/src/components/ui/tabs.tsx +52 -0
  169. package/src/components/ui/toggle-switch.tsx +35 -0
  170. package/src/components/ui/tooltip.tsx +27 -0
  171. package/src/components/workflow/AddEdgeDialog.tsx +217 -0
  172. package/src/components/workflow/AddListDialog.tsx +201 -0
  173. package/src/components/workflow/ChecklistEditor.tsx +239 -0
  174. package/src/components/workflow/CommandPrefixManager.tsx +118 -0
  175. package/src/components/workflow/ConfigSettingsPanel.tsx +189 -0
  176. package/src/components/workflow/DirectionSelector.tsx +133 -0
  177. package/src/components/workflow/DispatchConfigPanel.tsx +180 -0
  178. package/src/components/workflow/EdgeDetailPanel.tsx +236 -0
  179. package/src/components/workflow/EdgePropertyEditor.tsx +251 -0
  180. package/src/components/workflow/EditToolbar.tsx +138 -0
  181. package/src/components/workflow/HookDetailPanel.tsx +250 -0
  182. package/src/components/workflow/HookExecutionLog.tsx +24 -0
  183. package/src/components/workflow/HookSourceModal.tsx +129 -0
  184. package/src/components/workflow/HooksDashboard.tsx +363 -0
  185. package/src/components/workflow/ListPropertyEditor.tsx +251 -0
  186. package/src/components/workflow/MigrationPreviewDialog.tsx +237 -0
  187. package/src/components/workflow/MovementRulesPanel.tsx +188 -0
  188. package/src/components/workflow/NodeDetailPanel.tsx +245 -0
  189. package/src/components/workflow/PresetSelector.tsx +414 -0
  190. package/src/components/workflow/SkillCommandBuilder.tsx +174 -0
  191. package/src/components/workflow/WorkflowEdgeComponent.tsx +145 -0
  192. package/src/components/workflow/WorkflowNode.tsx +147 -0
  193. package/src/components/workflow/graphLayout.ts +186 -0
  194. package/src/components/workflow/mergeHooks.ts +85 -0
  195. package/src/components/workflow/useEditHistory.ts +88 -0
  196. package/src/components/workflow/useWorkflowEditor.ts +262 -0
  197. package/src/components/workflow/validateConfig.ts +70 -0
  198. package/src/hooks/useActiveDispatches.ts +198 -0
  199. package/src/hooks/useBoardSettings.ts +170 -0
  200. package/src/hooks/useCardDisplay.ts +57 -0
  201. package/src/hooks/useCcHooks.ts +24 -0
  202. package/src/hooks/useConfigTree.ts +51 -0
  203. package/src/hooks/useEnforcementRules.ts +46 -0
  204. package/src/hooks/useEvents.ts +59 -0
  205. package/src/hooks/useFileEditor.ts +165 -0
  206. package/src/hooks/useGates.ts +57 -0
  207. package/src/hooks/useIdeaActions.ts +53 -0
  208. package/src/hooks/useKanbanDnd.ts +410 -0
  209. package/src/hooks/useOrbitalConfig.ts +54 -0
  210. package/src/hooks/usePipeline.ts +47 -0
  211. package/src/hooks/usePipelineData.ts +338 -0
  212. package/src/hooks/useReconnect.ts +25 -0
  213. package/src/hooks/useScopeFilters.ts +125 -0
  214. package/src/hooks/useScopeSessions.ts +44 -0
  215. package/src/hooks/useScopes.ts +67 -0
  216. package/src/hooks/useSearch.ts +67 -0
  217. package/src/hooks/useSettings.tsx +187 -0
  218. package/src/hooks/useSocket.ts +25 -0
  219. package/src/hooks/useSourceControl.ts +105 -0
  220. package/src/hooks/useSprintPreflight.ts +55 -0
  221. package/src/hooks/useSprints.ts +154 -0
  222. package/src/hooks/useStatusBarHighlight.ts +18 -0
  223. package/src/hooks/useSwimlaneBoardSettings.ts +104 -0
  224. package/src/hooks/useTheme.ts +9 -0
  225. package/src/hooks/useTransitionReadiness.ts +53 -0
  226. package/src/hooks/useVersion.ts +155 -0
  227. package/src/hooks/useViolations.ts +65 -0
  228. package/src/hooks/useWorkflow.tsx +125 -0
  229. package/src/hooks/useZoomModifier.ts +19 -0
  230. package/src/index.css +797 -0
  231. package/src/layouts/DashboardLayout.tsx +113 -0
  232. package/src/lib/collisionDetection.ts +20 -0
  233. package/src/lib/scope-fields.ts +61 -0
  234. package/src/lib/swimlane.ts +146 -0
  235. package/src/lib/utils.ts +15 -0
  236. package/src/main.tsx +19 -0
  237. package/src/socket.ts +11 -0
  238. package/src/types/index.ts +497 -0
  239. package/src/views/AgentFeed.tsx +339 -0
  240. package/src/views/DeployPipeline.tsx +59 -0
  241. package/src/views/EnforcementView.tsx +378 -0
  242. package/src/views/PrimitivesConfig.tsx +500 -0
  243. package/src/views/QualityGates.tsx +1012 -0
  244. package/src/views/ScopeBoard.tsx +454 -0
  245. package/src/views/SessionTimeline.tsx +516 -0
  246. package/src/views/Settings.tsx +183 -0
  247. package/src/views/SourceControl.tsx +95 -0
  248. package/src/views/WorkflowVisualizer.tsx +382 -0
  249. package/tailwind.config.js +161 -0
  250. package/templates/agents/AUTO-INVOKE.md +180 -0
  251. package/templates/agents/CONFLICT-RESOLUTION.md +128 -0
  252. package/templates/agents/QUICK-REFERENCE.md +122 -0
  253. package/templates/agents/README.md +188 -0
  254. package/templates/agents/SKILL-TRIGGERS.md +100 -0
  255. package/templates/agents/blue-team/frontend-designer.md +424 -0
  256. package/templates/agents/green-team/architect.md +526 -0
  257. package/templates/agents/green-team/rules-enforcer.md +131 -0
  258. package/templates/agents/red-team/attacker-learned.md +24 -0
  259. package/templates/agents/red-team/attacker.md +486 -0
  260. package/templates/agents/red-team/chaos.md +548 -0
  261. package/templates/agents/reference/component-registry.md +82 -0
  262. package/templates/agents/workflows/full-mode.md +218 -0
  263. package/templates/agents/workflows/quick-mode.md +118 -0
  264. package/templates/agents/workflows/security-mode.md +283 -0
  265. package/templates/anti-patterns/dangerous-shortcuts.md +427 -0
  266. package/templates/config/agent-triggers.json +92 -0
  267. package/templates/hooks/agent-team-gate.sh +31 -0
  268. package/templates/hooks/agent-trigger.sh +97 -0
  269. package/templates/hooks/block-push.sh +66 -0
  270. package/templates/hooks/block-workarounds.sh +61 -0
  271. package/templates/hooks/blocker-check.sh +28 -0
  272. package/templates/hooks/completion-checklist.sh +28 -0
  273. package/templates/hooks/decision-capture.sh +15 -0
  274. package/templates/hooks/dependency-check.sh +27 -0
  275. package/templates/hooks/end-session.sh +31 -0
  276. package/templates/hooks/exploration-logger.sh +37 -0
  277. package/templates/hooks/files-changed-summary.sh +37 -0
  278. package/templates/hooks/get-session-id.sh +49 -0
  279. package/templates/hooks/git-commit-guard.sh +34 -0
  280. package/templates/hooks/init-session.sh +93 -0
  281. package/templates/hooks/orbital-emit.sh +79 -0
  282. package/templates/hooks/orbital-report-deploy.sh +78 -0
  283. package/templates/hooks/orbital-report-gates.sh +40 -0
  284. package/templates/hooks/orbital-report-violation.sh +36 -0
  285. package/templates/hooks/orbital-scope-update.sh +53 -0
  286. package/templates/hooks/phase-verify-reminder.sh +26 -0
  287. package/templates/hooks/review-gate-check.sh +82 -0
  288. package/templates/hooks/scope-commit-logger.sh +37 -0
  289. package/templates/hooks/scope-create-cleanup.sh +36 -0
  290. package/templates/hooks/scope-create-gate.sh +80 -0
  291. package/templates/hooks/scope-create-tracker.sh +17 -0
  292. package/templates/hooks/scope-file-sync.sh +53 -0
  293. package/templates/hooks/scope-gate.sh +35 -0
  294. package/templates/hooks/scope-helpers.sh +188 -0
  295. package/templates/hooks/scope-lifecycle-gate.sh +139 -0
  296. package/templates/hooks/scope-prepare.sh +244 -0
  297. package/templates/hooks/scope-transition.sh +172 -0
  298. package/templates/hooks/session-enforcer.sh +143 -0
  299. package/templates/hooks/time-tracker.sh +33 -0
  300. package/templates/lessons-learned.md +15 -0
  301. package/templates/orbital.config.json +35 -0
  302. package/templates/presets/development.json +42 -0
  303. package/templates/presets/gitflow.json +712 -0
  304. package/templates/presets/minimal.json +23 -0
  305. package/templates/quick/rules.md +218 -0
  306. package/templates/scopes/_template.md +255 -0
  307. package/templates/settings-hooks.json +98 -0
  308. package/templates/skills/git-commit/SKILL.md +85 -0
  309. package/templates/skills/git-dev/SKILL.md +99 -0
  310. package/templates/skills/git-hotfix/SKILL.md +223 -0
  311. package/templates/skills/git-main/SKILL.md +84 -0
  312. package/templates/skills/git-production/SKILL.md +165 -0
  313. package/templates/skills/git-staging/SKILL.md +112 -0
  314. package/templates/skills/scope-create/SKILL.md +81 -0
  315. package/templates/skills/scope-fix-review/SKILL.md +168 -0
  316. package/templates/skills/scope-implement/SKILL.md +110 -0
  317. package/templates/skills/scope-post-review/SKILL.md +144 -0
  318. package/templates/skills/scope-pre-review/SKILL.md +211 -0
  319. package/templates/skills/scope-verify/SKILL.md +201 -0
  320. package/templates/skills/session-init/SKILL.md +62 -0
  321. package/templates/skills/session-resume/SKILL.md +201 -0
  322. package/templates/skills/test-checks/SKILL.md +171 -0
  323. package/templates/skills/test-code-review/SKILL.md +252 -0
  324. package/tsconfig.json +25 -0
  325. package/vite.config.ts +38 -0
@@ -0,0 +1,441 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import readline from 'readline';
4
+ import type Database from 'better-sqlite3';
5
+ import type { ScopeService } from './scope-service.js';
6
+ import { getConfig, getClaudeSessionsDir } from '../config.js';
7
+
8
+ export interface ClaudeSession {
9
+ id: string;
10
+ slug: string;
11
+ branch: string;
12
+ startedAt: string;
13
+ lastActiveAt: string;
14
+ summary: string | null;
15
+ fileSize: number;
16
+ }
17
+
18
+ export interface SessionStats {
19
+ /** Count of each JSONL line type */
20
+ typeCounts: Record<string, number>;
21
+ /** Fields extracted from 'user' lines */
22
+ user: {
23
+ totalMessages: number;
24
+ metaMessages: number;
25
+ toolResults: number;
26
+ commands: string[];
27
+ permissionModes: string[];
28
+ cwd: string | null;
29
+ version: string | null;
30
+ };
31
+ /** Fields extracted from 'assistant' lines */
32
+ assistant: {
33
+ totalMessages: number;
34
+ models: string[];
35
+ totalInputTokens: number;
36
+ totalOutputTokens: number;
37
+ totalCacheReadTokens: number;
38
+ totalCacheCreationTokens: number;
39
+ toolsUsed: Record<string, number>;
40
+ };
41
+ /** Fields extracted from 'system' lines */
42
+ system: {
43
+ totalMessages: number;
44
+ subtypes: string[];
45
+ stopReasons: string[];
46
+ totalDurationMs: number;
47
+ hookCount: number;
48
+ hookErrors: number;
49
+ };
50
+ /** Fields extracted from 'progress' lines */
51
+ progress: {
52
+ totalLines: number;
53
+ };
54
+ /** Timing */
55
+ timing: {
56
+ firstTimestamp: string | null;
57
+ lastTimestamp: string | null;
58
+ durationMs: number;
59
+ };
60
+ }
61
+
62
+ function getSessionsDir(): string {
63
+ return getClaudeSessionsDir(getConfig().projectRoot);
64
+ }
65
+
66
+ let cache: { sessions: ClaudeSession[]; expiry: number } | null = null;
67
+ const CACHE_TTL_MS = 60_000;
68
+
69
+ /**
70
+ * Extract metadata from a JSONL session file by reading the first few
71
+ * lines and the last line (avoids parsing the entire file).
72
+ */
73
+ async function parseSessionFile(filePath: string): Promise<ClaudeSession | null> {
74
+ const stat = fs.statSync(filePath);
75
+ const filename = path.basename(filePath, '.jsonl');
76
+
77
+ let sessionId = filename;
78
+ let slug = '';
79
+ let branch = '';
80
+ let startedAt = '';
81
+ let summary: string | null = null;
82
+
83
+ // Read first 20 lines for metadata
84
+ const stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
85
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
86
+ let lineNum = 0;
87
+
88
+ for await (const line of rl) {
89
+ if (lineNum > 20) break;
90
+ lineNum++;
91
+
92
+ try {
93
+ const data = JSON.parse(line);
94
+
95
+ if (data.sessionId && !sessionId) sessionId = data.sessionId;
96
+ if (data.slug && !slug) slug = data.slug;
97
+ if (data.gitBranch && !branch) branch = data.gitBranch;
98
+
99
+ // Capture the earliest timestamp
100
+ if (!startedAt) {
101
+ const ts =
102
+ data.timestamp ??
103
+ data.snapshot?.timestamp;
104
+ if (ts) startedAt = ts;
105
+ }
106
+
107
+ // If we have all fields, stop early
108
+ if (sessionId && slug && branch && startedAt) break;
109
+ } catch {
110
+ // skip unparseable lines
111
+ }
112
+ }
113
+
114
+ rl.close();
115
+ stream.destroy();
116
+
117
+ if (!sessionId) return null;
118
+
119
+ // Read file content for summary extraction
120
+ try {
121
+ const fullContent = fs.readFileSync(filePath, 'utf-8');
122
+ const lines = fullContent.trimEnd().split('\n');
123
+
124
+ // Prefer explicit summary line from Claude
125
+ const lastLine = JSON.parse(lines[lines.length - 1]);
126
+ if (lastLine.type === 'summary' && lastLine.summary) {
127
+ summary = lastLine.summary;
128
+ }
129
+
130
+ // Fall back to first user message
131
+ if (!summary) {
132
+ summary = extractFirstUserMessage(lines, 120);
133
+ }
134
+ } catch {
135
+ // ignore
136
+ }
137
+
138
+ return {
139
+ id: sessionId,
140
+ slug: slug || filename,
141
+ branch: branch || 'unknown',
142
+ startedAt: startedAt || stat.birthtime.toISOString(),
143
+ lastActiveAt: stat.mtime.toISOString(),
144
+ summary,
145
+ fileSize: stat.size,
146
+ };
147
+ }
148
+
149
+ export async function getClaudeSessions(since?: string): Promise<ClaudeSession[]> {
150
+ if (cache && Date.now() < cache.expiry) {
151
+ return filterSince(cache.sessions, since);
152
+ }
153
+
154
+ const sessionsDir = getSessionsDir();
155
+ if (!fs.existsSync(sessionsDir)) return [];
156
+
157
+ const files = fs
158
+ .readdirSync(sessionsDir)
159
+ .filter((f) => f.endsWith('.jsonl'))
160
+ .map((f) => path.join(sessionsDir, f));
161
+
162
+ const sessions: ClaudeSession[] = [];
163
+
164
+ for (const file of files) {
165
+ const session = await parseSessionFile(file);
166
+ if (session) sessions.push(session);
167
+ }
168
+
169
+ // Sort by most recent first
170
+ sessions.sort(
171
+ (a, b) => new Date(b.lastActiveAt).getTime() - new Date(a.lastActiveAt).getTime(),
172
+ );
173
+
174
+ cache = { sessions, expiry: Date.now() + CACHE_TTL_MS };
175
+ return filterSince(sessions, since);
176
+ }
177
+
178
+ function filterSince(sessions: ClaudeSession[], since?: string): ClaudeSession[] {
179
+ if (!since) return sessions;
180
+ const cutoff = new Date(since).getTime();
181
+ return sessions.filter((s) => new Date(s.lastActiveAt).getTime() >= cutoff);
182
+ }
183
+
184
+ /** Truncate text to max length with ellipsis */
185
+ function truncate(text: string | null | undefined, max: number): string {
186
+ if (!text) return '';
187
+ return text.length > max ? text.slice(0, max) + '...' : text;
188
+ }
189
+
190
+ /**
191
+ * Extract a meaningful session name from JSONL lines.
192
+ *
193
+ * Priority:
194
+ * 1. First non-meta user message (what the user actually typed)
195
+ * 2. Slash command name from the first command-message (e.g. "/scope review 1")
196
+ * 3. null if nothing useful found
197
+ *
198
+ * Skips isMeta messages (skill prompts injected by the system),
199
+ * tool_result lines, and raw command XML.
200
+ */
201
+ function extractFirstUserMessage(lines: string[], max: number): string | null {
202
+ let slashCommand: string | null = null;
203
+
204
+ for (const line of lines) {
205
+ try {
206
+ const data = JSON.parse(line);
207
+ if (data.type !== 'user') continue;
208
+
209
+ const content = data.message?.content;
210
+ let text = '';
211
+
212
+ if (typeof content === 'string') {
213
+ text = content;
214
+ } else if (Array.isArray(content)) {
215
+ for (const block of content) {
216
+ if (block?.type === 'text' && typeof block.text === 'string') {
217
+ text = block.text;
218
+ break;
219
+ }
220
+ }
221
+ }
222
+
223
+ if (!text) continue;
224
+
225
+ // Capture slash command as fallback (e.g. "/scope review 1")
226
+ if (!slashCommand && text.includes('<command-name>')) {
227
+ const cmdMatch = text.match(/<command-name>\/?(.+?)<\/command-name>/);
228
+ const argsMatch = text.match(/<command-args>(.+?)<\/command-args>/);
229
+ if (cmdMatch) {
230
+ slashCommand = '/' + cmdMatch[1] + (argsMatch ? ' ' + argsMatch[1] : '');
231
+ }
232
+ }
233
+
234
+ // Skip system-injected lines: commands, meta/skill prompts, tool results
235
+ if (text.startsWith('<command') || text.startsWith('<tool_result')) continue;
236
+ if (data.isMeta) continue;
237
+
238
+ return truncate(text.trim(), max);
239
+ } catch {
240
+ // skip unparseable lines
241
+ }
242
+ }
243
+
244
+ return slashCommand ?? null;
245
+ }
246
+
247
+ /**
248
+ * Parse a full JSONL file and return detailed stats grouped by line type.
249
+ * This is heavier than parseSessionFile — only called for the detail view.
250
+ */
251
+ export function getSessionStats(claudeSessionId: string): SessionStats | null {
252
+ const filePath = path.join(getSessionsDir(), `${claudeSessionId}.jsonl`);
253
+ if (!fs.existsSync(filePath)) return null;
254
+
255
+ const stats: SessionStats = {
256
+ typeCounts: {},
257
+ user: { totalMessages: 0, metaMessages: 0, toolResults: 0, commands: [], permissionModes: [], cwd: null, version: null },
258
+ assistant: { totalMessages: 0, models: [], totalInputTokens: 0, totalOutputTokens: 0, totalCacheReadTokens: 0, totalCacheCreationTokens: 0, toolsUsed: {} },
259
+ system: { totalMessages: 0, subtypes: [], stopReasons: [], totalDurationMs: 0, hookCount: 0, hookErrors: 0 },
260
+ progress: { totalLines: 0 },
261
+ timing: { firstTimestamp: null, lastTimestamp: null, durationMs: 0 },
262
+ };
263
+
264
+ let content: string;
265
+ try { content = fs.readFileSync(filePath, 'utf-8'); } catch { return null; }
266
+
267
+ const lines = content.trimEnd().split('\n');
268
+
269
+ for (const line of lines) {
270
+ let data: Record<string, unknown>;
271
+ try { data = JSON.parse(line); } catch { continue; }
272
+
273
+ const type = (data.type as string) ?? 'unknown';
274
+ stats.typeCounts[type] = (stats.typeCounts[type] ?? 0) + 1;
275
+
276
+ // Track timestamps
277
+ const ts = (data.timestamp as string) ?? null;
278
+ if (ts) {
279
+ if (!stats.timing.firstTimestamp) stats.timing.firstTimestamp = ts;
280
+ stats.timing.lastTimestamp = ts;
281
+ }
282
+
283
+ if (type === 'user') {
284
+ stats.user.totalMessages++;
285
+ if (data.isMeta) stats.user.metaMessages++;
286
+ if (data.toolUseResult) stats.user.toolResults++;
287
+ if (!stats.user.cwd && data.cwd) stats.user.cwd = data.cwd as string;
288
+ if (!stats.user.version && data.version) stats.user.version = data.version as string;
289
+
290
+ const pm = data.permissionMode as string | undefined;
291
+ if (pm && !stats.user.permissionModes.includes(pm)) stats.user.permissionModes.push(pm);
292
+
293
+ // Extract slash commands
294
+ const content = (data.message as Record<string, unknown>)?.content;
295
+ const text = typeof content === 'string' ? content : '';
296
+ const cmdMatch = text.match(/<command-name>\/?(.+?)<\/command-name>/);
297
+ if (cmdMatch) {
298
+ const cmd = '/' + cmdMatch[1];
299
+ if (!stats.user.commands.includes(cmd)) stats.user.commands.push(cmd);
300
+ }
301
+ }
302
+
303
+ if (type === 'assistant') {
304
+ stats.assistant.totalMessages++;
305
+ const msg = data.message as Record<string, unknown> | undefined;
306
+ if (msg) {
307
+ const model = msg.model as string | undefined;
308
+ if (model && !stats.assistant.models.includes(model)) stats.assistant.models.push(model);
309
+
310
+ const usage = msg.usage as Record<string, unknown> | undefined;
311
+ if (usage) {
312
+ stats.assistant.totalInputTokens += Number(usage.input_tokens) || 0;
313
+ stats.assistant.totalOutputTokens += Number(usage.output_tokens) || 0;
314
+ stats.assistant.totalCacheReadTokens += Number(usage.cache_read_input_tokens) || 0;
315
+ stats.assistant.totalCacheCreationTokens += Number(usage.cache_creation_input_tokens) || 0;
316
+ }
317
+
318
+ // Track tool usage
319
+ const msgContent = msg.content;
320
+ if (Array.isArray(msgContent)) {
321
+ for (const block of msgContent) {
322
+ if (block?.type === 'tool_use' && block.name) {
323
+ const name = block.name as string;
324
+ stats.assistant.toolsUsed[name] = (stats.assistant.toolsUsed[name] ?? 0) + 1;
325
+ }
326
+ }
327
+ }
328
+ }
329
+ }
330
+
331
+ if (type === 'system') {
332
+ stats.system.totalMessages++;
333
+ const subtype = data.subtype as string | undefined;
334
+ if (subtype && !stats.system.subtypes.includes(subtype)) stats.system.subtypes.push(subtype);
335
+ const stopReason = data.stopReason as string | undefined;
336
+ if (stopReason && !stats.system.stopReasons.includes(stopReason)) stats.system.stopReasons.push(stopReason);
337
+ stats.system.totalDurationMs += Number(data.durationMs) || 0;
338
+ stats.system.hookCount += Number(data.hookCount) || 0;
339
+ stats.system.hookErrors += Number(data.hookErrors) || 0;
340
+ }
341
+
342
+ if (type === 'progress') {
343
+ stats.progress.totalLines++;
344
+ }
345
+ }
346
+
347
+ // Compute session duration
348
+ if (stats.timing.firstTimestamp && stats.timing.lastTimestamp) {
349
+ stats.timing.durationMs = new Date(stats.timing.lastTimestamp).getTime() - new Date(stats.timing.firstTimestamp).getTime();
350
+ }
351
+
352
+ return stats;
353
+ }
354
+
355
+ /**
356
+ * Sync sessions into the DB from scope frontmatter.
357
+ *
358
+ * Algorithm:
359
+ * 1. Read scopes with non-empty sessions JSON from DB
360
+ * 2. For each scope, parse the sessions JSON: Record<phase, uuid[]>
361
+ * 3. For each (phase, uuid), UPSERT into sessions table with JSONL metadata if available
362
+ */
363
+ export async function syncClaudeSessionsToDB(db: Database.Database, scopeService: ScopeService): Promise<number> {
364
+ cache = null; // Force fresh read from filesystem
365
+
366
+ const scopeRows = scopeService.getAll()
367
+ .filter(s => Object.keys(s.sessions).length > 0)
368
+ .map(s => ({ id: s.id, sessions: s.sessions }));
369
+
370
+ const upsert = db.prepare(`
371
+ INSERT INTO sessions (id, scope_id, claude_session_id, action, started_at, ended_at, summary, handoff_file, discoveries, next_steps)
372
+ VALUES (?, ?, ?, ?, ?, ?, ?, NULL, '[]', '[]')
373
+ ON CONFLICT(id) DO UPDATE SET
374
+ started_at = excluded.started_at,
375
+ ended_at = excluded.ended_at,
376
+ summary = excluded.summary,
377
+ claude_session_id = excluded.claude_session_id,
378
+ action = excluded.action
379
+ `);
380
+
381
+ let count = 0;
382
+
383
+ const insertAll = db.transaction(() => {
384
+ for (const row of scopeRows) {
385
+ for (const [phase, uuids] of Object.entries(row.sessions)) {
386
+ if (!Array.isArray(uuids)) continue;
387
+
388
+ for (const uuid of uuids) {
389
+ if (typeof uuid !== 'string' || !uuid) continue;
390
+
391
+ // Check if JSONL file exists for metadata enrichment
392
+ const jsonlPath = path.join(getSessionsDir(), `${uuid}.jsonl`);
393
+ let startedAt: string | null = null;
394
+ let endedAt: string | null = null;
395
+ let summary: string | null = null;
396
+
397
+ if (fs.existsSync(jsonlPath)) {
398
+ try {
399
+ const stat = fs.statSync(jsonlPath);
400
+ startedAt = stat.birthtime.toISOString();
401
+ endedAt = stat.mtime.toISOString();
402
+
403
+ const content = fs.readFileSync(jsonlPath, 'utf-8');
404
+ const lines = content.trimEnd().split('\n');
405
+
406
+ // Prefer explicit summary line from Claude
407
+ const lastLine = JSON.parse(lines[lines.length - 1]);
408
+ if (lastLine.type === 'summary' && lastLine.summary) {
409
+ summary = truncate(lastLine.summary, 200);
410
+ }
411
+
412
+ // Fall back to first user message
413
+ if (!summary) {
414
+ summary = extractFirstUserMessage(lines, 200);
415
+ }
416
+ } catch {
417
+ // Metadata unavailable — row still created with nulls
418
+ }
419
+ }
420
+
421
+ // Composite key includes phase so same UUID under different phases creates distinct rows
422
+ const compositeId = `${uuid}-scope-${row.id}-${phase}`;
423
+
424
+ upsert.run(
425
+ compositeId,
426
+ row.id,
427
+ uuid,
428
+ phase,
429
+ startedAt,
430
+ endedAt,
431
+ summary,
432
+ );
433
+ count++;
434
+ }
435
+ }
436
+ }
437
+ });
438
+
439
+ insertAll();
440
+ return count;
441
+ }
@@ -0,0 +1,151 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import matter from 'gray-matter';
4
+
5
+ export interface ConfigFileNode {
6
+ name: string;
7
+ path: string; // relative path from base
8
+ type: 'file' | 'folder';
9
+ children?: ConfigFileNode[];
10
+ frontmatter?: Record<string, unknown>;
11
+ }
12
+
13
+ export type ConfigPrimitiveType = 'agents' | 'skills' | 'hooks';
14
+
15
+ const VALID_TYPES = new Set<ConfigPrimitiveType>(['agents', 'skills', 'hooks']);
16
+
17
+ export function isValidPrimitiveType(type: string): type is ConfigPrimitiveType {
18
+ return VALID_TYPES.has(type as ConfigPrimitiveType);
19
+ }
20
+
21
+ export class ConfigService {
22
+ constructor(private projectRoot: string) {}
23
+
24
+ /** Resolve the base directory for a primitive type */
25
+ getBasePath(type: ConfigPrimitiveType): string {
26
+ switch (type) {
27
+ case 'agents': return path.join(this.projectRoot, '.claude', 'agents');
28
+ case 'skills': return path.join(this.projectRoot, '.claude', 'skills');
29
+ case 'hooks': return path.join(this.projectRoot, '.claude', 'hooks');
30
+ }
31
+ }
32
+
33
+ /** Scan a directory tree and parse SKILL.md / agent frontmatter */
34
+ scanDirectory(basePath: string, parseFrontmatter = true): ConfigFileNode[] {
35
+ if (!fs.existsSync(basePath)) return [];
36
+ return this.walkDir(basePath, basePath, parseFrontmatter);
37
+ }
38
+
39
+ readFile(basePath: string, relativePath: string): string {
40
+ const resolved = this.validatePath(basePath, relativePath);
41
+ return fs.readFileSync(resolved, 'utf-8');
42
+ }
43
+
44
+ writeFile(basePath: string, relativePath: string, content: string): void {
45
+ const resolved = this.validatePath(basePath, relativePath);
46
+ if (!fs.existsSync(resolved)) {
47
+ throw new Error('File not found');
48
+ }
49
+ // Atomic write: write to .tmp, then rename
50
+ const tmpPath = resolved + '.tmp';
51
+ fs.writeFileSync(tmpPath, content, 'utf-8');
52
+ fs.renameSync(tmpPath, resolved);
53
+ }
54
+
55
+ createFile(basePath: string, relativePath: string, content: string): void {
56
+ const resolved = this.validatePath(basePath, relativePath);
57
+ if (fs.existsSync(resolved)) {
58
+ throw new Error('File already exists');
59
+ }
60
+ const dir = path.dirname(resolved);
61
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
62
+ fs.writeFileSync(resolved, content, 'utf-8');
63
+ }
64
+
65
+ deleteFile(basePath: string, relativePath: string): void {
66
+ const resolved = this.validatePath(basePath, relativePath);
67
+ if (!fs.existsSync(resolved)) {
68
+ throw new Error('File not found');
69
+ }
70
+ const stat = fs.statSync(resolved);
71
+ if (stat.isDirectory()) {
72
+ throw new Error('Cannot delete a directory');
73
+ }
74
+ fs.unlinkSync(resolved);
75
+ }
76
+
77
+ renameFile(basePath: string, oldPath: string, newPath: string): void {
78
+ const resolvedOld = this.validatePath(basePath, oldPath);
79
+ const resolvedNew = this.validatePath(basePath, newPath);
80
+ if (!fs.existsSync(resolvedOld)) {
81
+ throw new Error('Source file not found');
82
+ }
83
+ if (fs.existsSync(resolvedNew)) {
84
+ throw new Error('Destination already exists');
85
+ }
86
+ const destDir = path.dirname(resolvedNew);
87
+ if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
88
+ fs.renameSync(resolvedOld, resolvedNew);
89
+ }
90
+
91
+ createFolder(basePath: string, relativePath: string): void {
92
+ const resolved = this.validatePath(basePath, relativePath);
93
+ if (fs.existsSync(resolved)) {
94
+ throw new Error('Folder already exists');
95
+ }
96
+ fs.mkdirSync(resolved, { recursive: true });
97
+ }
98
+
99
+ // ─── Private Helpers ────────────────────────────────────
100
+
101
+ /** Path traversal validation — ensure resolved path is within basePath */
102
+ private validatePath(basePath: string, relativePath: string): string {
103
+ const resolvedBase = path.resolve(basePath);
104
+ const resolved = path.resolve(basePath, relativePath);
105
+ if (resolved !== resolvedBase && !resolved.startsWith(resolvedBase + path.sep)) {
106
+ throw new Error('Path traversal detected');
107
+ }
108
+ return resolved;
109
+ }
110
+
111
+ /** Recursively walk a directory and build a file tree */
112
+ private walkDir(currentPath: string, basePath: string, parseFrontmatter: boolean): ConfigFileNode[] {
113
+ const entries = fs.readdirSync(currentPath, { withFileTypes: true });
114
+ const nodes: ConfigFileNode[] = [];
115
+
116
+ for (const entry of entries) {
117
+ // Skip hidden files/dirs (e.g. .DS_Store)
118
+ if (entry.name.startsWith('.')) continue;
119
+
120
+ const fullPath = path.join(currentPath, entry.name);
121
+ const relPath = path.relative(basePath, fullPath);
122
+
123
+ if (entry.isDirectory()) {
124
+ const children = this.walkDir(fullPath, basePath, parseFrontmatter);
125
+ nodes.push({ name: entry.name, path: relPath, type: 'folder', children });
126
+ } else {
127
+ const node: ConfigFileNode = { name: entry.name, path: relPath, type: 'file' };
128
+ if (parseFrontmatter && entry.name.endsWith('.md')) {
129
+ try {
130
+ const content = fs.readFileSync(fullPath, 'utf-8');
131
+ const parsed = matter(content);
132
+ if (Object.keys(parsed.data).length > 0) {
133
+ node.frontmatter = parsed.data;
134
+ }
135
+ } catch {
136
+ // Skip frontmatter parsing errors
137
+ }
138
+ }
139
+ nodes.push(node);
140
+ }
141
+ }
142
+
143
+ // Sort: folders first, then files, alphabetical within each group
144
+ nodes.sort((a, b) => {
145
+ if (a.type !== b.type) return a.type === 'folder' ? -1 : 1;
146
+ return a.name.localeCompare(b.name);
147
+ });
148
+
149
+ return nodes;
150
+ }
151
+ }
@@ -0,0 +1,98 @@
1
+ import type Database from 'better-sqlite3';
2
+ import type { Server } from 'socket.io';
3
+ import type { DeployStatus, DeployEnvironment } from '../../shared/api-types.js';
4
+ import { createLogger } from '../utils/logger.js';
5
+
6
+ const log = createLogger('deploy');
7
+
8
+ export interface DeployRecord {
9
+ environment: DeployEnvironment;
10
+ status: DeployStatus;
11
+ commit_sha: string | null;
12
+ branch: string | null;
13
+ pr_number: number | null;
14
+ health_check_url: string | null;
15
+ details: Record<string, unknown> | null;
16
+ }
17
+
18
+ export interface DeployRow {
19
+ id: number;
20
+ environment: DeployEnvironment;
21
+ status: DeployStatus;
22
+ commit_sha: string | null;
23
+ branch: string | null;
24
+ pr_number: number | null;
25
+ health_check_url: string | null;
26
+ started_at: string | null;
27
+ completed_at: string | null;
28
+ details: string;
29
+ }
30
+
31
+ export class DeployService {
32
+ constructor(
33
+ private db: Database.Database,
34
+ private io: Server
35
+ ) {}
36
+
37
+ /** Record a deployment event */
38
+ record(deploy: DeployRecord): number {
39
+ const now = new Date().toISOString();
40
+ const result = this.db.prepare(
41
+ `INSERT INTO deployments (environment, status, commit_sha, branch, pr_number, health_check_url, started_at, details)
42
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
43
+ ).run(
44
+ deploy.environment,
45
+ deploy.status,
46
+ deploy.commit_sha,
47
+ deploy.branch,
48
+ deploy.pr_number,
49
+ deploy.health_check_url,
50
+ now,
51
+ JSON.stringify(deploy.details ?? {})
52
+ );
53
+
54
+ const id = result.lastInsertRowid as number;
55
+ log.info('Deploy recorded', { id, env: deploy.environment, status: deploy.status, commit_sha: deploy.commit_sha, branch: deploy.branch });
56
+ const inserted = this.db.prepare('SELECT * FROM deployments WHERE id = ?').get(id);
57
+ if (inserted) {
58
+ this.io.emit('deploy:updated', inserted);
59
+ }
60
+
61
+ return id;
62
+ }
63
+
64
+ /** Update deployment status */
65
+ updateStatus(id: number, status: DeployStatus, details?: string): void {
66
+ const completedAt = (status === 'healthy' || status === 'failed' || status === 'rolled-back')
67
+ ? new Date().toISOString()
68
+ : null;
69
+
70
+ this.db.prepare(
71
+ `UPDATE deployments SET status = ?, completed_at = COALESCE(?, completed_at), details = COALESCE(?, details) WHERE id = ?`
72
+ ).run(status, completedAt, details, id);
73
+
74
+ log.info('Deploy status updated', { id, status });
75
+ const updated = this.db.prepare('SELECT * FROM deployments WHERE id = ?').get(id);
76
+ if (updated) {
77
+ this.io.emit('deploy:updated', updated);
78
+ }
79
+ }
80
+
81
+ /** Get recent deployments */
82
+ getRecent(limit: number = 20): DeployRow[] {
83
+ return this.db
84
+ .prepare('SELECT * FROM deployments ORDER BY started_at DESC LIMIT ?')
85
+ .all(limit) as DeployRow[];
86
+ }
87
+
88
+ /** Get latest deployment per environment */
89
+ getLatestPerEnv(): DeployRow[] {
90
+ return this.db.prepare(`
91
+ SELECT * FROM deployments
92
+ WHERE id IN (
93
+ SELECT MAX(id) FROM deployments GROUP BY environment
94
+ )
95
+ ORDER BY environment
96
+ `).all() as DeployRow[];
97
+ }
98
+ }