orbital-command 0.3.0 → 1.0.1

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 (160) hide show
  1. package/README.md +67 -42
  2. package/bin/commands/config.js +19 -0
  3. package/bin/commands/events.js +40 -0
  4. package/bin/commands/launch.js +126 -0
  5. package/bin/commands/manifest.js +283 -0
  6. package/bin/commands/registry.js +104 -0
  7. package/bin/commands/update.js +24 -0
  8. package/bin/lib/helpers.js +229 -0
  9. package/bin/orbital.js +90 -873
  10. package/dist/assets/Landing-CfQdHR0N.js +11 -0
  11. package/dist/assets/PrimitivesConfig-DThSipFy.js +32 -0
  12. package/dist/assets/QualityGates-B4kxM5UU.js +26 -0
  13. package/dist/assets/SessionTimeline-Bz1iZnmg.js +1 -0
  14. package/dist/assets/Settings-DLcZwbCT.js +12 -0
  15. package/dist/assets/SourceControl-BMNIz7Lt.js +36 -0
  16. package/dist/assets/WorkflowVisualizer-CxuSBOYu.js +69 -0
  17. package/dist/assets/{arrow-down-CPy85_J6.js → arrow-down-DVPp6_qp.js} +1 -1
  18. package/dist/assets/bot-NFaJBDn_.js +6 -0
  19. package/dist/assets/{charts-DbDg0Psc.js → charts-LGLb8hyU.js} +1 -1
  20. package/dist/assets/{circle-x-Cwz6ZQDV.js → circle-x-IsFCkBZu.js} +1 -1
  21. package/dist/assets/{file-text-C46Xr65c.js → file-text-J1cebZXF.js} +1 -1
  22. package/dist/assets/{globe-Cn2yNZUD.js → globe-WzeyHsUc.js} +1 -1
  23. package/dist/assets/index-BdJ57EhC.css +1 -0
  24. package/dist/assets/index-o4ScMAuR.js +349 -0
  25. package/dist/assets/{key-OPaNTWJ5.js → key-CKR8JJSj.js} +1 -1
  26. package/dist/assets/{minus-GMsbpKym.js → minus-CHBsJyjp.js} +1 -1
  27. package/dist/assets/radio-xqZaR-Uk.js +6 -0
  28. package/dist/assets/rocket-D_xvvNG6.js +6 -0
  29. package/dist/assets/{shield-DwAFkDYI.js → shield-TdB1yv_a.js} +1 -1
  30. package/dist/assets/useSocketListener-0L5yiN5i.js +1 -0
  31. package/dist/assets/useWorkflowEditor-CqeRWVQX.js +11 -0
  32. package/dist/assets/workflow-constants-Rw-GmgHZ.js +6 -0
  33. package/dist/assets/zap-C9wqYMpl.js +6 -0
  34. package/dist/index.html +3 -3
  35. package/dist/server/server/__tests__/data-routes.test.js +2 -0
  36. package/dist/server/server/__tests__/scope-routes.test.js +1 -0
  37. package/dist/server/server/config-migrator.js +0 -3
  38. package/dist/server/server/config.js +35 -6
  39. package/dist/server/server/database.js +0 -22
  40. package/dist/server/server/index.js +28 -816
  41. package/dist/server/server/init.js +32 -399
  42. package/dist/server/server/launch.js +1 -1
  43. package/dist/server/server/parsers/event-parser.js +4 -1
  44. package/dist/server/server/project-context.js +19 -9
  45. package/dist/server/server/project-manager.js +6 -6
  46. package/dist/server/server/routes/aggregate-routes.js +871 -0
  47. package/dist/server/server/routes/config-routes.js +41 -88
  48. package/dist/server/server/routes/data-routes.js +5 -15
  49. package/dist/server/server/routes/dispatch-routes.js +24 -8
  50. package/dist/server/server/routes/manifest-routes.js +1 -1
  51. package/dist/server/server/routes/scope-routes.js +10 -7
  52. package/dist/server/server/schema.js +1 -0
  53. package/dist/server/server/services/batch-orchestrator.js +17 -3
  54. package/dist/server/server/services/config-service.js +10 -1
  55. package/dist/server/server/services/scope-service.js +7 -7
  56. package/dist/server/server/services/sprint-orchestrator.js +24 -11
  57. package/dist/server/server/services/sprint-service.js +2 -2
  58. package/dist/server/server/uninstall.js +195 -0
  59. package/dist/server/server/update.js +212 -0
  60. package/dist/server/server/utils/dispatch-utils.js +8 -6
  61. package/dist/server/server/utils/flag-builder.js +54 -0
  62. package/dist/server/server/utils/json-fields.js +14 -0
  63. package/dist/server/server/utils/json-fields.test.js +73 -0
  64. package/dist/server/server/utils/route-helpers.js +37 -0
  65. package/dist/server/server/utils/route-helpers.test.js +115 -0
  66. package/dist/server/server/watchers/event-watcher.js +28 -13
  67. package/dist/server/server/wizard/config-editor.js +4 -4
  68. package/dist/server/server/wizard/doctor.js +2 -2
  69. package/dist/server/server/wizard/index.js +224 -39
  70. package/dist/server/server/wizard/phases/welcome.js +1 -4
  71. package/dist/server/server/wizard/ui.js +6 -7
  72. package/dist/server/shared/api-types.js +80 -1
  73. package/dist/server/shared/workflow-engine.js +1 -1
  74. package/package.json +20 -20
  75. package/schemas/orbital.config.schema.json +1 -19
  76. package/scripts/postinstall.js +6 -42
  77. package/scripts/release.sh +53 -0
  78. package/server/__tests__/data-routes.test.ts +2 -0
  79. package/server/__tests__/scope-routes.test.ts +1 -0
  80. package/server/config-migrator.ts +0 -3
  81. package/server/config.ts +39 -11
  82. package/server/database.ts +0 -26
  83. package/server/global-config.ts +4 -0
  84. package/server/index.ts +31 -896
  85. package/server/init.ts +32 -443
  86. package/server/launch.ts +1 -1
  87. package/server/parsers/event-parser.ts +4 -1
  88. package/server/project-context.ts +26 -10
  89. package/server/project-manager.ts +5 -6
  90. package/server/routes/aggregate-routes.ts +968 -0
  91. package/server/routes/config-routes.ts +41 -81
  92. package/server/routes/data-routes.ts +7 -16
  93. package/server/routes/dispatch-routes.ts +29 -8
  94. package/server/routes/manifest-routes.ts +1 -1
  95. package/server/routes/scope-routes.ts +12 -7
  96. package/server/schema.ts +1 -0
  97. package/server/services/batch-orchestrator.ts +18 -2
  98. package/server/services/config-service.ts +10 -1
  99. package/server/services/scope-service.ts +6 -6
  100. package/server/services/sprint-orchestrator.ts +24 -9
  101. package/server/services/sprint-service.ts +2 -2
  102. package/server/uninstall.ts +214 -0
  103. package/server/update.ts +263 -0
  104. package/server/utils/dispatch-utils.ts +8 -6
  105. package/server/utils/flag-builder.ts +56 -0
  106. package/server/utils/json-fields.test.ts +83 -0
  107. package/server/utils/json-fields.ts +14 -0
  108. package/server/utils/route-helpers.test.ts +144 -0
  109. package/server/utils/route-helpers.ts +38 -0
  110. package/server/watchers/event-watcher.ts +24 -12
  111. package/server/wizard/config-editor.ts +4 -4
  112. package/server/wizard/doctor.ts +2 -2
  113. package/server/wizard/index.ts +291 -40
  114. package/server/wizard/phases/welcome.ts +1 -5
  115. package/server/wizard/ui.ts +6 -7
  116. package/shared/api-types.ts +106 -0
  117. package/shared/workflow-engine.ts +1 -1
  118. package/templates/agents/QUICK-REFERENCE.md +1 -0
  119. package/templates/agents/README.md +1 -0
  120. package/templates/agents/SKILL-TRIGGERS.md +11 -0
  121. package/templates/agents/green-team/deep-dive.md +361 -0
  122. package/templates/hooks/end-session.sh +1 -0
  123. package/templates/hooks/init-session.sh +1 -0
  124. package/templates/hooks/scope-commit-logger.sh +2 -2
  125. package/templates/hooks/scope-create-gate.sh +2 -4
  126. package/templates/hooks/scope-gate.sh +4 -6
  127. package/templates/hooks/scope-helpers.sh +10 -1
  128. package/templates/hooks/scope-lifecycle-gate.sh +14 -5
  129. package/templates/hooks/scope-prepare.sh +1 -1
  130. package/templates/hooks/scope-transition.sh +14 -6
  131. package/templates/hooks/time-tracker.sh +2 -5
  132. package/templates/orbital.config.json +1 -4
  133. package/templates/presets/development.json +4 -4
  134. package/templates/presets/gitflow.json +7 -0
  135. package/templates/prompts/README.md +23 -0
  136. package/templates/prompts/deep-dive-audit.md +94 -0
  137. package/templates/quick/rules.md +56 -5
  138. package/templates/skills/git-commit/SKILL.md +21 -6
  139. package/templates/skills/git-dev/SKILL.md +8 -4
  140. package/templates/skills/git-main/SKILL.md +8 -4
  141. package/templates/skills/git-production/SKILL.md +6 -3
  142. package/templates/skills/git-staging/SKILL.md +6 -3
  143. package/templates/skills/scope-fix-review/SKILL.md +8 -4
  144. package/templates/skills/scope-implement/SKILL.md +13 -5
  145. package/templates/skills/scope-post-review/SKILL.md +16 -4
  146. package/templates/skills/scope-pre-review/SKILL.md +6 -2
  147. package/dist/assets/PrimitivesConfig-CrmQXYh4.js +0 -32
  148. package/dist/assets/QualityGates-BbasOsF3.js +0 -21
  149. package/dist/assets/SessionTimeline-CGeJsVvy.js +0 -1
  150. package/dist/assets/Settings-oiM496mc.js +0 -12
  151. package/dist/assets/SourceControl-B1fP2nJL.js +0 -41
  152. package/dist/assets/WorkflowVisualizer-CWLYf-f0.js +0 -74
  153. package/dist/assets/formatDistanceToNow-BMqsSP44.js +0 -1
  154. package/dist/assets/index-Aj4sV8Al.css +0 -1
  155. package/dist/assets/index-Bc9dK3MW.js +0 -354
  156. package/dist/assets/useWorkflowEditor-BJkTX_NR.js +0 -16
  157. package/dist/assets/zap-DfbUoOty.js +0 -11
  158. package/dist/server/server/services/telemetry-service.js +0 -143
  159. package/server/services/telemetry-service.ts +0 -195
  160. /package/{shared/default-workflow.json → templates/presets/default.json} +0 -0
@@ -1,16 +0,0 @@
1
- import{c as b,aQ as V,u as q}from"./index-Bc9dK3MW.js";import{a as o}from"./vendor-Bqt8AJn2.js";/**
2
- * @license lucide-react v0.577.0 - ISC
3
- *
4
- * This source code is licensed under the ISC license.
5
- * See the LICENSE file in the root directory of this source tree.
6
- */const B=[["path",{d:"M12 8V4H8",key:"hb8ula"}],["rect",{width:"16",height:"12",x:"4",y:"8",rx:"2",key:"enze0r"}],["path",{d:"M2 14h2",key:"vft8re"}],["path",{d:"M20 14h2",key:"4cs60a"}],["path",{d:"M15 13v2",key:"1xurst"}],["path",{d:"M9 13v2",key:"rq6x2g"}]],Y=b("bot",B);/**
7
- * @license lucide-react v0.577.0 - ISC
8
- *
9
- * This source code is licensed under the ISC license.
10
- * See the LICENSE file in the root directory of this source tree.
11
- */const W=[["path",{d:"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z",key:"1a8usu"}],["path",{d:"m15 5 4 4",key:"1mk7zo"}]],Z=b("pencil",W);/**
12
- * @license lucide-react v0.577.0 - ISC
13
- *
14
- * This source code is licensed under the ISC license.
15
- * See the LICENSE file in the root directory of this source tree.
16
- */const F=[["path",{d:"M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z",key:"1c8476"}],["path",{d:"M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7",key:"1ydtos"}],["path",{d:"M7 3v4a1 1 0 0 0 1 1h7",key:"t51u73"}]],ee=b("save",F);function K(c){const[a,u]=o.useState({past:[],present:structuredClone(c),future:[]}),p=o.useCallback(r=>{u(l=>({past:[...l.past,l.present].slice(-50),present:r,future:[]}))},[]),d=o.useCallback(()=>{u(r=>{if(r.past.length===0)return r;const l=[...r.past],k=l.pop();return{past:l,present:k,future:[r.present,...r.future]}})},[]),s=o.useCallback(()=>{u(r=>{if(r.future.length===0)return r;const l=[...r.future],k=l.shift();return{past:[...r.past,r.present],present:k,future:l}})},[]),f=o.useCallback(r=>{u({past:[],present:structuredClone(r),future:[]})},[]),g=o.useMemo(()=>a.past.length,[a.past.length]);return{present:a.present,canUndo:a.past.length>0,canRedo:a.future.length>0,changeCount:g,pushState:p,undo:d,redo:s,reset:f}}function Q(c){const a=[];if(!V(c))return a.push("Invalid config shape: must have version=1, name, lists[], edges[]"),{valid:!1,errors:a};c.branchingMode!==void 0&&c.branchingMode!=="trunk"&&c.branchingMode!=="worktree"&&a.push(`Invalid branchingMode: "${c.branchingMode}" (must be "trunk" or "worktree")`);const u=new Set;for(const s of c.lists)u.has(s.id)&&a.push(`Duplicate list ID: "${s.id}"`),u.add(s.id);const p=new Set;for(const s of c.edges){u.has(s.from)||a.push(`Edge references unknown list: from="${s.from}"`),u.has(s.to)||a.push(`Edge references unknown list: to="${s.to}"`),s.from===s.to&&a.push(`Self-referencing edge: "${s.from}" → "${s.to}"`);const f=`${s.from}:${s.to}`;p.has(f)&&a.push(`Duplicate edge: ${f}`),p.add(f)}const d=c.lists.filter(s=>s.isEntryPoint);if(d.length===0&&a.push("No entry point defined (isEntryPoint=true)"),d.length>1&&a.push(`Multiple entry points: ${d.map(s=>s.id).join(", ")}`),d.length===1&&a.length===0){const s=new Set(c.terminalStatuses??[]),f=new Set,g=[d[0].id];for(;g.length>0;){const r=g.shift();if(!f.has(r)){f.add(r);for(const l of c.edges)l.from===r&&!f.has(l.to)&&g.push(l.to)}}for(const r of c.lists)!s.has(r.id)&&!f.has(r.id)&&a.push(`List "${r.id}" is not reachable from entry point`)}return{valid:a.length===0,errors:a}}function te(c){const[a,u]=o.useState(!1),[p,d]=o.useState(!1),s=q(),[f,g]=o.useState(null),[r,l]=o.useState(!1),[k,S]=o.useState(null),[$,m]=o.useState(!1),[x,v]=o.useState(!1),[j,M]=o.useState(!1),[L,E]=o.useState(!1),e=K(c),C=o.useMemo(()=>Q(e.present),[e.present]),N=o.useCallback(()=>{e.reset(c),u(!0)},[c,e]),w=o.useCallback(()=>{u(!1),m(!1),v(!1),M(!1),E(!1)},[]),T=o.useCallback(()=>{e.reset(c),w()},[c,e,w]),U=o.useCallback(n=>{const t=structuredClone(e.present);t.lists.push(n),e.pushState(t)},[e]),_=o.useCallback((n,t)=>{const i=structuredClone(e.present),y=i.lists.findIndex(h=>h.id===n.id);if(y!==-1){if(n.id!==t.id)for(const h of i.edges)h.from===n.id&&(h.from=t.id),h.to===n.id&&(h.to=t.id);i.lists[y]=t,e.pushState(i)}},[e]),I=o.useCallback(n=>{const t=structuredClone(e.present);t.lists=t.lists.filter(i=>i.id!==n),t.edges=t.edges.filter(i=>i.from!==n&&i.to!==n),e.pushState(t)},[e]),O=o.useCallback(n=>{const t=structuredClone(e.present);t.edges.push(n),e.pushState(t)},[e]),z=o.useCallback(n=>{e.pushState(structuredClone(n))},[e]),A=o.useCallback((n,t)=>{const i=structuredClone(e.present),y=`${n.from}:${n.to}`,h=i.edges.findIndex(P=>`${P.from}:${P.to}`===y);h!==-1&&(i.edges[h]=t,e.pushState(i))},[e]),H=o.useCallback((n,t)=>{const i=structuredClone(e.present);i.edges=i.edges.filter(y=>!(y.from===n&&y.to===t)),e.pushState(i)},[e]),D=o.useCallback(async()=>{if(!(!C.valid||p)){d(!0);try{const t=await(await fetch(s("/workflow"),{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(e.present)})).json();if(!t.success)throw new Error(t.error??"Save failed");w()}catch(n){S(n instanceof Error?n.message:"Save failed")}finally{d(!1)}}},[C.valid,p,e.present,w,s]),J=o.useCallback(async()=>{l(!0),S(null),g(null),m(!0);try{const t=await(await fetch(s("/workflow/preview"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e.present)})).json();if(!t.success)throw new Error(t.error??"Preview failed");g(t.data??null)}catch(n){S(n instanceof Error?n.message:"Preview failed")}finally{l(!1)}},[e.present,s]),R=o.useCallback(async n=>{d(!0);try{const i=await(await fetch(s("/workflow/apply"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({config:e.present,orphanMappings:n})})).json();if(!i.success)throw new Error(i.error??"Migration failed");m(!1),w()}catch(t){S(t instanceof Error?t.message:"Migration failed")}finally{d(!1)}},[e.present,w,s]);return{editMode:a,editConfig:e.present,canUndo:e.canUndo,canRedo:e.canRedo,changeCount:e.changeCount,validation:C,saving:p,previewPlan:f,previewLoading:r,previewError:k,showPreview:$,showAddList:x,showAddEdge:j,showConfigSettings:L,enterEditMode:N,exitEditMode:w,undo:e.undo,redo:e.redo,updateList:_,deleteList:I,addList:U,updateEdge:A,deleteEdge:H,addEdge:O,updateConfig:z,save:D,discard:T,preview:J,applyMigration:R,setShowPreview:m,setShowAddList:v,setShowAddEdge:M,setShowConfigSettings:E}}export{Y as B,Z as P,ee as S,te as u};
@@ -1,11 +0,0 @@
1
- import{c as e}from"./index-Bc9dK3MW.js";/**
2
- * @license lucide-react v0.577.0 - ISC
3
- *
4
- * This source code is licensed under the ISC license.
5
- * See the LICENSE file in the root directory of this source tree.
6
- */const t=[["path",{d:"M11 10.27 7 3.34",key:"16pf9h"}],["path",{d:"m11 13.73-4 6.93",key:"794ttg"}],["path",{d:"M12 22v-2",key:"1osdcq"}],["path",{d:"M12 2v2",key:"tus03m"}],["path",{d:"M14 12h8",key:"4f43i9"}],["path",{d:"m17 20.66-1-1.73",key:"eq3orb"}],["path",{d:"m17 3.34-1 1.73",key:"2wel8s"}],["path",{d:"M2 12h2",key:"1t8f8n"}],["path",{d:"m20.66 17-1.73-1",key:"sg0v6f"}],["path",{d:"m20.66 7-1.73 1",key:"1ow05n"}],["path",{d:"m3.34 17 1.73-1",key:"nuk764"}],["path",{d:"m3.34 7 1.73 1",key:"1ulond"}],["circle",{cx:"12",cy:"12",r:"2",key:"1c9p78"}],["circle",{cx:"12",cy:"12",r:"8",key:"46899m"}]],d=e("cog",t);/**
7
- * @license lucide-react v0.577.0 - ISC
8
- *
9
- * This source code is licensed under the ISC license.
10
- * See the LICENSE file in the root directory of this source tree.
11
- */const a=[["path",{d:"M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z",key:"1xq2db"}]],p=e("zap",a);export{d as C,p as Z};
@@ -1,143 +0,0 @@
1
- /**
2
- * Session Telemetry — uploads raw Claude session JSONL files to a remote
3
- * Cloudflare Worker + R2 endpoint. This entire feature lives in this single
4
- * file for easy removal.
5
- */
6
- import fs from 'fs';
7
- import path from 'path';
8
- import { Router } from 'express';
9
- import { getClaudeSessionsDir } from '../config.js';
10
- // ─── Service ───────────────────────────────────────────────
11
- export class TelemetryService {
12
- db;
13
- config;
14
- projectName;
15
- projectRoot;
16
- lastResult = null;
17
- constructor(db, config, projectName, projectRoot) {
18
- this.db = db;
19
- this.config = config;
20
- this.projectName = projectName;
21
- this.projectRoot = projectRoot;
22
- }
23
- get enabled() {
24
- return this.config.enabled && this.config.url.length > 0;
25
- }
26
- /** Upload sessions that have changed since last telemetry send. */
27
- async uploadChangedSessions() {
28
- if (!this.enabled)
29
- return { ok: true, uploaded: 0, errors: 0 };
30
- const rows = this.db.prepare(`SELECT id, claude_session_id, ended_at, telemetry_sent_at
31
- FROM sessions
32
- WHERE claude_session_id IS NOT NULL
33
- AND (telemetry_sent_at IS NULL OR ended_at > telemetry_sent_at)`).all();
34
- return this.uploadRows(rows);
35
- }
36
- /** Force re-upload all sessions regardless of telemetry_sent_at. */
37
- async uploadAllSessions() {
38
- if (!this.enabled)
39
- return { ok: true, uploaded: 0, errors: 0 };
40
- const rows = this.db.prepare(`SELECT id, claude_session_id, ended_at, telemetry_sent_at
41
- FROM sessions
42
- WHERE claude_session_id IS NOT NULL`).all();
43
- return this.uploadRows(rows);
44
- }
45
- /** Ping the remote health endpoint. */
46
- async testConnection() {
47
- try {
48
- const res = await fetch(`${this.config.url}/health`, {
49
- method: 'GET',
50
- headers: this.config.headers,
51
- signal: AbortSignal.timeout(10_000),
52
- });
53
- const body = await res.text();
54
- return { ok: res.ok, status: res.status, body };
55
- }
56
- catch (err) {
57
- return { ok: false, status: 0, body: err.message };
58
- }
59
- }
60
- getStatus() {
61
- return {
62
- enabled: this.enabled,
63
- url: this.config.url || null,
64
- lastResult: this.lastResult,
65
- };
66
- }
67
- // ─── Internal ──────────────────────────────────────────────
68
- async uploadRows(rows) {
69
- if (rows.length === 0) {
70
- this.lastResult = { ok: true, uploaded: 0, errors: 0 };
71
- return this.lastResult;
72
- }
73
- const sessionsDir = getClaudeSessionsDir(this.projectRoot);
74
- const now = new Date().toISOString();
75
- let uploaded = 0;
76
- let errors = 0;
77
- const updateStmt = this.db.prepare('UPDATE sessions SET telemetry_sent_at = ? WHERE id = ?');
78
- // Deduplicate by claude_session_id (multiple rows can share the same JSONL file)
79
- const seen = new Set();
80
- const unique = [];
81
- for (const row of rows) {
82
- if (row.claude_session_id && !seen.has(row.claude_session_id)) {
83
- seen.add(row.claude_session_id);
84
- unique.push(row);
85
- }
86
- }
87
- for (const row of unique) {
88
- const sessionId = row.claude_session_id;
89
- const filePath = path.join(sessionsDir, `${sessionId}.jsonl`);
90
- if (!fs.existsSync(filePath)) {
91
- continue;
92
- }
93
- try {
94
- const body = fs.readFileSync(filePath);
95
- const encodedProject = encodeURIComponent(this.projectName);
96
- const url = `${this.config.url}/upload/${encodedProject}/${sessionId}.jsonl`;
97
- const res = await fetch(url, {
98
- method: 'PUT',
99
- body,
100
- headers: {
101
- 'Content-Type': 'application/x-ndjson',
102
- ...this.config.headers,
103
- },
104
- signal: AbortSignal.timeout(30_000),
105
- });
106
- if (res.ok) {
107
- uploaded++;
108
- // Update telemetry_sent_at for ALL rows with this claude_session_id
109
- const matching = rows.filter((r) => r.claude_session_id === sessionId);
110
- for (const m of matching) {
111
- updateStmt.run(now, m.id);
112
- }
113
- }
114
- else {
115
- errors++;
116
- }
117
- }
118
- catch {
119
- errors++;
120
- }
121
- }
122
- this.lastResult = { ok: errors === 0, uploaded, errors };
123
- return this.lastResult;
124
- }
125
- }
126
- export function createTelemetryRoutes({ telemetryService }) {
127
- const router = Router();
128
- router.post('/telemetry/trigger', async (req, res) => {
129
- const force = req.query.force === 'true';
130
- const result = force
131
- ? await telemetryService.uploadAllSessions()
132
- : await telemetryService.uploadChangedSessions();
133
- res.json(result);
134
- });
135
- router.post('/telemetry/test', async (_req, res) => {
136
- const result = await telemetryService.testConnection();
137
- res.json(result);
138
- });
139
- router.get('/telemetry/status', (_req, res) => {
140
- res.json(telemetryService.getStatus());
141
- });
142
- return router;
143
- }
@@ -1,195 +0,0 @@
1
- /**
2
- * Session Telemetry — uploads raw Claude session JSONL files to a remote
3
- * Cloudflare Worker + R2 endpoint. This entire feature lives in this single
4
- * file for easy removal.
5
- */
6
-
7
- import fs from 'fs';
8
- import path from 'path';
9
- import { Router } from 'express';
10
- import type Database from 'better-sqlite3';
11
- import { getClaudeSessionsDir } from '../config.js';
12
-
13
- export interface TelemetryConfig {
14
- enabled: boolean;
15
- url: string;
16
- headers: Record<string, string>;
17
- }
18
-
19
- interface SessionRow {
20
- id: string;
21
- claude_session_id: string | null;
22
- ended_at: string | null;
23
- telemetry_sent_at: string | null;
24
- }
25
-
26
- interface TelemetryResult {
27
- ok: boolean;
28
- uploaded: number;
29
- errors: number;
30
- }
31
-
32
- // ─── Service ───────────────────────────────────────────────
33
-
34
- export class TelemetryService {
35
- private lastResult: TelemetryResult | null = null;
36
-
37
- constructor(
38
- private db: Database.Database,
39
- private config: TelemetryConfig,
40
- private projectName: string,
41
- private projectRoot: string,
42
- ) {}
43
-
44
- get enabled(): boolean {
45
- return this.config.enabled && this.config.url.length > 0;
46
- }
47
-
48
- /** Upload sessions that have changed since last telemetry send. */
49
- async uploadChangedSessions(): Promise<TelemetryResult> {
50
- if (!this.enabled) return { ok: true, uploaded: 0, errors: 0 };
51
-
52
- const rows = this.db.prepare(
53
- `SELECT id, claude_session_id, ended_at, telemetry_sent_at
54
- FROM sessions
55
- WHERE claude_session_id IS NOT NULL
56
- AND (telemetry_sent_at IS NULL OR ended_at > telemetry_sent_at)`
57
- ).all() as SessionRow[];
58
-
59
- return this.uploadRows(rows);
60
- }
61
-
62
- /** Force re-upload all sessions regardless of telemetry_sent_at. */
63
- async uploadAllSessions(): Promise<TelemetryResult> {
64
- if (!this.enabled) return { ok: true, uploaded: 0, errors: 0 };
65
-
66
- const rows = this.db.prepare(
67
- `SELECT id, claude_session_id, ended_at, telemetry_sent_at
68
- FROM sessions
69
- WHERE claude_session_id IS NOT NULL`
70
- ).all() as SessionRow[];
71
-
72
- return this.uploadRows(rows);
73
- }
74
-
75
- /** Ping the remote health endpoint. */
76
- async testConnection(): Promise<{ ok: boolean; status: number; body: string }> {
77
- try {
78
- const res = await fetch(`${this.config.url}/health`, {
79
- method: 'GET',
80
- headers: this.config.headers,
81
- signal: AbortSignal.timeout(10_000),
82
- });
83
- const body = await res.text();
84
- return { ok: res.ok, status: res.status, body };
85
- } catch (err) {
86
- return { ok: false, status: 0, body: (err as Error).message };
87
- }
88
- }
89
-
90
- getStatus() {
91
- return {
92
- enabled: this.enabled,
93
- url: this.config.url || null,
94
- lastResult: this.lastResult,
95
- };
96
- }
97
-
98
- // ─── Internal ──────────────────────────────────────────────
99
-
100
- private async uploadRows(rows: SessionRow[]): Promise<TelemetryResult> {
101
- if (rows.length === 0) {
102
- this.lastResult = { ok: true, uploaded: 0, errors: 0 };
103
- return this.lastResult;
104
- }
105
-
106
- const sessionsDir = getClaudeSessionsDir(this.projectRoot);
107
- const now = new Date().toISOString();
108
- let uploaded = 0;
109
- let errors = 0;
110
-
111
- const updateStmt = this.db.prepare(
112
- 'UPDATE sessions SET telemetry_sent_at = ? WHERE id = ?'
113
- );
114
-
115
- // Deduplicate by claude_session_id (multiple rows can share the same JSONL file)
116
- const seen = new Set<string>();
117
- const unique: SessionRow[] = [];
118
- for (const row of rows) {
119
- if (row.claude_session_id && !seen.has(row.claude_session_id)) {
120
- seen.add(row.claude_session_id);
121
- unique.push(row);
122
- }
123
- }
124
-
125
- for (const row of unique) {
126
- const sessionId = row.claude_session_id!;
127
- const filePath = path.join(sessionsDir, `${sessionId}.jsonl`);
128
-
129
- if (!fs.existsSync(filePath)) {
130
- continue;
131
- }
132
-
133
- try {
134
- const body = fs.readFileSync(filePath);
135
- const encodedProject = encodeURIComponent(this.projectName);
136
- const url = `${this.config.url}/upload/${encodedProject}/${sessionId}.jsonl`;
137
-
138
- const res = await fetch(url, {
139
- method: 'PUT',
140
- body,
141
- headers: {
142
- 'Content-Type': 'application/x-ndjson',
143
- ...this.config.headers,
144
- },
145
- signal: AbortSignal.timeout(30_000),
146
- });
147
-
148
- if (res.ok) {
149
- uploaded++;
150
- // Update telemetry_sent_at for ALL rows with this claude_session_id
151
- const matching = rows.filter((r) => r.claude_session_id === sessionId);
152
- for (const m of matching) {
153
- updateStmt.run(now, m.id);
154
- }
155
- } else {
156
- errors++;
157
- }
158
- } catch {
159
- errors++;
160
- }
161
- }
162
-
163
- this.lastResult = { ok: errors === 0, uploaded, errors };
164
- return this.lastResult;
165
- }
166
- }
167
-
168
- // ─── Routes ────────────────────────────────────────────────
169
-
170
- interface TelemetryRouteDeps {
171
- telemetryService: TelemetryService;
172
- }
173
-
174
- export function createTelemetryRoutes({ telemetryService }: TelemetryRouteDeps): Router {
175
- const router = Router();
176
-
177
- router.post('/telemetry/trigger', async (req, res) => {
178
- const force = req.query.force === 'true';
179
- const result = force
180
- ? await telemetryService.uploadAllSessions()
181
- : await telemetryService.uploadChangedSessions();
182
- res.json(result);
183
- });
184
-
185
- router.post('/telemetry/test', async (_req, res) => {
186
- const result = await telemetryService.testConnection();
187
- res.json(result);
188
- });
189
-
190
- router.get('/telemetry/status', (_req, res) => {
191
- res.json(telemetryService.getStatus());
192
- });
193
-
194
- return router;
195
- }