sidekick-shared 0.27.5 → 0.28.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 (314) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +93 -52
  3. package/dist/accountHealth.d.ts +14 -0
  4. package/dist/accountHealth.js +61 -14
  5. package/dist/accountLaunch.d.ts +6 -0
  6. package/dist/accountLaunch.js +61 -4
  7. package/dist/accountLease.d.ts +5 -0
  8. package/dist/accountLease.js +98 -0
  9. package/dist/accountManager.d.ts +7 -0
  10. package/dist/accountManager.js +226 -89
  11. package/dist/accountSync.d.ts +1 -5
  12. package/dist/accountSync.js +22 -6
  13. package/dist/accountSyncTypes.d.ts +9 -0
  14. package/dist/accounts.d.ts +75 -18
  15. package/dist/accounts.js +712 -189
  16. package/dist/browser.d.ts +13 -2
  17. package/dist/browser.js +30 -2
  18. package/dist/claudeCredentialDoctor.d.ts +50 -0
  19. package/dist/claudeCredentialDoctor.js +277 -0
  20. package/dist/claudeCredentialLedger.d.ts +15 -0
  21. package/dist/claudeCredentialLedger.js +130 -0
  22. package/dist/claudeCredentials.d.ts +20 -3
  23. package/dist/claudeCredentials.js +50 -3
  24. package/dist/claudeLiveGuard.d.ts +33 -0
  25. package/dist/claudeLiveGuard.js +46 -0
  26. package/dist/claudePaths.d.ts +11 -0
  27. package/dist/claudePaths.js +61 -0
  28. package/dist/claudeProfiles.d.ts +15 -8
  29. package/dist/claudeProfiles.js +30 -23
  30. package/dist/claudeRefreshLock.d.ts +15 -0
  31. package/dist/claudeRefreshLock.js +145 -0
  32. package/dist/codexAuth.d.ts +4 -0
  33. package/dist/codexAuth.js +18 -0
  34. package/dist/codexCredentialDoctor.d.ts +33 -0
  35. package/dist/codexCredentialDoctor.js +302 -0
  36. package/dist/codexCredentialStore.d.ts +29 -0
  37. package/dist/codexCredentialStore.js +213 -0
  38. package/dist/codexKeepAlive.d.ts +2 -0
  39. package/dist/codexKeepAlive.js +116 -0
  40. package/dist/codexPaths.d.ts +30 -2
  41. package/dist/codexPaths.js +118 -7
  42. package/dist/codexProfiles.d.ts +38 -35
  43. package/dist/codexProfiles.js +848 -715
  44. package/dist/credentialIO.d.ts +67 -7
  45. package/dist/credentialIO.js +211 -47
  46. package/dist/doctor.js +2 -2
  47. package/dist/ensureDefaultAccounts.js +1 -8
  48. package/dist/esm/accountChangeSignal.js +15 -0
  49. package/dist/esm/accountChanges.js +152 -0
  50. package/dist/esm/accountHealth.js +288 -0
  51. package/dist/esm/accountLaunch.js +145 -0
  52. package/dist/esm/accountLease.js +60 -0
  53. package/dist/esm/accountManager.js +606 -0
  54. package/dist/esm/accountRegistry.js +214 -0
  55. package/dist/esm/accountStatus.js +51 -0
  56. package/dist/esm/accountSwitch.js +74 -0
  57. package/dist/esm/accountSync.js +156 -0
  58. package/dist/esm/accountSyncTypes.js +4 -0
  59. package/dist/esm/accounts.js +1291 -0
  60. package/dist/esm/aggregation/EventAggregator.js +2034 -0
  61. package/dist/esm/aggregation/FrequencyTracker.js +69 -0
  62. package/dist/esm/aggregation/HeatmapTracker.js +89 -0
  63. package/dist/esm/aggregation/PatternExtractor.js +168 -0
  64. package/dist/esm/aggregation/costProvenance.js +31 -0
  65. package/dist/esm/aggregation/snapshot.js +161 -0
  66. package/dist/esm/aggregation/types.js +5 -0
  67. package/dist/esm/analytics/codeImpact.js +18 -0
  68. package/dist/esm/analytics/compactionLedger.js +37 -0
  69. package/dist/esm/analytics/qualityScore.js +112 -0
  70. package/dist/esm/autoSwitch.js +185 -0
  71. package/dist/esm/browser.js +34 -0
  72. package/dist/esm/claudeCredentialDoctor.js +239 -0
  73. package/dist/esm/claudeCredentialLedger.js +91 -0
  74. package/dist/esm/claudeCredentials.js +111 -0
  75. package/dist/esm/claudeLiveGuard.js +43 -0
  76. package/dist/esm/claudePaths.js +22 -0
  77. package/dist/esm/claudeProfiles.js +142 -0
  78. package/dist/esm/claudeRefreshLock.js +106 -0
  79. package/dist/esm/codexAuth.js +101 -0
  80. package/dist/esm/codexCredentialDoctor.js +265 -0
  81. package/dist/esm/codexCredentialStore.js +164 -0
  82. package/dist/esm/codexKeepAlive.js +112 -0
  83. package/dist/esm/codexPaths.js +172 -0
  84. package/dist/esm/codexProfiles.js +1251 -0
  85. package/dist/esm/codexQuota.js +564 -0
  86. package/dist/esm/codexQuotaWatcher.js +363 -0
  87. package/dist/esm/context/composer.js +71 -0
  88. package/dist/esm/context/sessionContext.js +546 -0
  89. package/dist/esm/credentialIO.js +283 -0
  90. package/dist/esm/credentials.js +49 -0
  91. package/dist/esm/doctor.js +305 -0
  92. package/dist/esm/ensureDefaultAccounts.js +62 -0
  93. package/dist/esm/errorHistory.js +56 -0
  94. package/dist/esm/extractors/errorTaxonomy.js +43 -0
  95. package/dist/esm/extractors/gatherAssets.js +32 -0
  96. package/dist/esm/extractors/sessionAssets.js +176 -0
  97. package/dist/esm/extractors/sources/claudeAssets.js +158 -0
  98. package/dist/esm/extractors/sources/codexAssets.js +234 -0
  99. package/dist/esm/extractors/tokenUsage.js +41 -0
  100. package/dist/esm/extractors/toolCall.js +142 -0
  101. package/dist/esm/failingTools.js +65 -0
  102. package/dist/esm/fileHead.js +27 -0
  103. package/dist/esm/formatters/eventHighlighter.js +332 -0
  104. package/dist/esm/formatters/noiseClassifier.js +209 -0
  105. package/dist/esm/formatters/sessionDump.js +328 -0
  106. package/dist/esm/formatters/toolSummary.js +226 -0
  107. package/dist/esm/formatting.js +82 -0
  108. package/dist/esm/handoffUrl.js +23 -0
  109. package/dist/esm/historicalStore.js +305 -0
  110. package/dist/esm/humanPrompt.js +88 -0
  111. package/dist/esm/index.js +213 -0
  112. package/dist/esm/keepAlive.js +189 -0
  113. package/dist/esm/liveCredentialGuard.js +33 -0
  114. package/dist/esm/modelAliases.js +51 -0
  115. package/dist/esm/modelCatalog.js +139 -0
  116. package/dist/esm/modelContext.js +280 -0
  117. package/dist/esm/modelInfo.js +832 -0
  118. package/dist/esm/multiProviderQuotaService.js +450 -0
  119. package/dist/esm/node.js +46 -0
  120. package/dist/esm/observedContextWindows.js +165 -0
  121. package/dist/esm/observedSessionCollector.js +939 -0
  122. package/dist/esm/parsers/changelogParser.js +71 -0
  123. package/dist/esm/parsers/codexHistory.js +89 -0
  124. package/dist/esm/parsers/codexParser.js +906 -0
  125. package/dist/esm/parsers/debugLogParser.js +126 -0
  126. package/dist/esm/parsers/jsonl.js +66 -0
  127. package/dist/esm/parsers/mcpToolName.js +28 -0
  128. package/dist/esm/parsers/openCodeParser.js +625 -0
  129. package/dist/esm/parsers/planExtractor.js +335 -0
  130. package/dist/esm/parsers/sessionActivityDetector.js +246 -0
  131. package/dist/esm/parsers/sessionPathResolver.js +713 -0
  132. package/dist/esm/parsers/subagentScanner.js +360 -0
  133. package/dist/esm/parsers/subagentStatsCache.js +45 -0
  134. package/dist/esm/parsers/subagentTraceParser.js +325 -0
  135. package/dist/esm/paths.js +134 -0
  136. package/dist/esm/peakHours.js +194 -0
  137. package/dist/esm/phrases.js +1330 -0
  138. package/dist/esm/pricingCatalog.js +247 -0
  139. package/dist/esm/processDetection.js +200 -0
  140. package/dist/esm/projectMigration.js +59 -0
  141. package/dist/esm/promptHistory.js +1221 -0
  142. package/dist/esm/providerFailure.js +362 -0
  143. package/dist/esm/providerIds.js +6 -0
  144. package/dist/esm/providerQuota.js +1 -0
  145. package/dist/esm/providerServiceStatus.js +145 -0
  146. package/dist/esm/providerServiceStatusTypes.js +1 -0
  147. package/dist/esm/providerStatus.js +117 -0
  148. package/dist/esm/providers/claudeCode.js +575 -0
  149. package/dist/esm/providers/claudeListing.js +121 -0
  150. package/dist/esm/providers/codex.js +1034 -0
  151. package/dist/esm/providers/codexDatabase.js +297 -0
  152. package/dist/esm/providers/codexRollouts.js +215 -0
  153. package/dist/esm/providers/detect.js +185 -0
  154. package/dist/esm/providers/diagnostics.js +83 -0
  155. package/dist/esm/providers/directoryListingCache.js +206 -0
  156. package/dist/esm/providers/factory.js +54 -0
  157. package/dist/esm/providers/openCode.js +2008 -0
  158. package/dist/esm/providers/openCodeDatabase.js +419 -0
  159. package/dist/esm/providers/rolloutWalker.js +252 -0
  160. package/dist/esm/providers/types.js +12 -0
  161. package/dist/esm/quota.js +182 -0
  162. package/dist/esm/quotaHistory.js +364 -0
  163. package/dist/esm/quotaPoller.js +230 -0
  164. package/dist/esm/quotaPresentation.js +97 -0
  165. package/dist/esm/quotaResolver.js +231 -0
  166. package/dist/esm/quotaSnapshots.js +130 -0
  167. package/dist/esm/quotaThresholds.js +85 -0
  168. package/dist/esm/readers/decisions.js +24 -0
  169. package/dist/esm/readers/handoff.js +20 -0
  170. package/dist/esm/readers/helpers.js +27 -0
  171. package/dist/esm/readers/history.js +9 -0
  172. package/dist/esm/readers/notes.js +48 -0
  173. package/dist/esm/readers/plans.js +225 -0
  174. package/dist/esm/readers/tasks.js +19 -0
  175. package/dist/esm/report/htmlHelpers.js +280 -0
  176. package/dist/esm/report/htmlReportGenerator.js +704 -0
  177. package/dist/esm/report/index.js +7 -0
  178. package/dist/esm/report/logo.js +2 -0
  179. package/dist/esm/report/openBrowser.js +19 -0
  180. package/dist/esm/report/sessionReportInputs.js +33 -0
  181. package/dist/esm/report/transcriptParser.js +83 -0
  182. package/dist/esm/report/types.js +4 -0
  183. package/dist/esm/schemas/accountManager.js +204 -0
  184. package/dist/esm/schemas/accountStatus.js +25 -0
  185. package/dist/esm/schemas/assistantTurn.js +74 -0
  186. package/dist/esm/schemas/index.js +25 -0
  187. package/dist/esm/schemas/observedSessionV1.js +77 -0
  188. package/dist/esm/schemas/providerFailure.js +37 -0
  189. package/dist/esm/schemas/quota.js +132 -0
  190. package/dist/esm/schemas/quotaHistory.js +40 -0
  191. package/dist/esm/schemas/sessionEvent.js +216 -0
  192. package/dist/esm/schemas/stateFile.js +75 -0
  193. package/dist/esm/schemas/tokenEstimation.js +7 -0
  194. package/dist/esm/schemas/transcript.js +86 -0
  195. package/dist/esm/schemas/usageNormalization.js +38 -0
  196. package/dist/esm/search/advancedFilter.js +197 -0
  197. package/dist/esm/search/sessionSearch.js +66 -0
  198. package/dist/esm/sessionFingerprint.js +40 -0
  199. package/dist/esm/sessionMonitor.js +198 -0
  200. package/dist/esm/sessionPreviews.js +522 -0
  201. package/dist/esm/sessionPromptHistory.js +254 -0
  202. package/dist/esm/sessionPromptSignals.js +364 -0
  203. package/dist/esm/sessionStats.js +160 -0
  204. package/dist/esm/sessionTokenTotals.js +98 -0
  205. package/dist/esm/sessionTranscripts.js +49 -0
  206. package/dist/esm/stateFile.js +119 -0
  207. package/dist/esm/statusline/BurnRateCalculator.js +40 -0
  208. package/dist/esm/statusline/claudeStatuslinePayload.js +148 -0
  209. package/dist/esm/statusline/formatter.js +122 -0
  210. package/dist/esm/statusline/index.js +8 -0
  211. package/dist/esm/terminalSync.js +95 -0
  212. package/dist/esm/testing/credentialGuard.js +197 -0
  213. package/dist/esm/testing/index.js +12 -0
  214. package/dist/esm/testing/sessionLogFixtures.js +128 -0
  215. package/dist/esm/tokenEstimation.js +67 -0
  216. package/dist/esm/tokenSummary.js +113 -0
  217. package/dist/esm/toolCategory.js +82 -0
  218. package/dist/esm/transcript.js +257 -0
  219. package/dist/esm/turns/assistantTurn.js +373 -0
  220. package/dist/esm/types/codex.js +23 -0
  221. package/dist/esm/types/decisionLog.js +5 -0
  222. package/dist/esm/types/historicalData.js +47 -0
  223. package/dist/esm/types/knowledgeNote.js +15 -0
  224. package/dist/esm/types/observedSessionV1.js +257 -0
  225. package/dist/esm/types/opencode.js +12 -0
  226. package/dist/esm/types/plan.js +7 -0
  227. package/dist/esm/types/sessionEvent.js +10 -0
  228. package/dist/esm/types/taskPersistence.js +12 -0
  229. package/dist/esm/usage/billingBlocks.js +148 -0
  230. package/dist/esm/usage/claudeUsageDedupe.js +189 -0
  231. package/dist/esm/usage/importSessionHistory.js +84 -0
  232. package/dist/esm/usage/subagentUsage.js +18 -0
  233. package/dist/esm/usage/usageEvents.js +402 -0
  234. package/dist/esm/usage/usageReports.js +202 -0
  235. package/dist/esm/usageNormalization.js +95 -0
  236. package/dist/esm/watchers/eventBridge.js +219 -0
  237. package/dist/esm/watchers/factory.js +63 -0
  238. package/dist/esm/watchers/jsonlTail.js +289 -0
  239. package/dist/esm/watchers/jsonlWatcher.js +435 -0
  240. package/dist/esm/watchers/providerReaderWatcher.js +118 -0
  241. package/dist/esm/watchers/sqliteWatcher.js +285 -0
  242. package/dist/esm/watchers/types.js +4 -0
  243. package/dist/esm/workspaceRoots.js +56 -0
  244. package/dist/esm/writers/atomic.js +375 -0
  245. package/dist/esm/writers/decisions.js +51 -0
  246. package/dist/esm/writers/notes.js +46 -0
  247. package/dist/esm/writers/tasks.js +63 -0
  248. package/dist/esm/zaiQuota.js +323 -0
  249. package/dist/esm/zaiQuotaApi.js +271 -0
  250. package/dist/esm/zaiQuotaWatcher.js +265 -0
  251. package/dist/fileHead.d.ts +22 -0
  252. package/dist/fileHead.js +33 -0
  253. package/dist/index.d.ts +30 -11
  254. package/dist/index.js +72 -15
  255. package/dist/keepAlive.d.ts +2 -1
  256. package/dist/keepAlive.js +116 -29
  257. package/dist/liveCredentialGuard.d.ts +28 -0
  258. package/dist/liveCredentialGuard.js +36 -0
  259. package/dist/node.d.ts +13 -1
  260. package/dist/node.js +43 -2
  261. package/dist/parsers/codexHistory.js +2 -2
  262. package/dist/parsers/codexParser.d.ts +12 -0
  263. package/dist/parsers/codexParser.js +34 -12
  264. package/dist/parsers/debugLogParser.js +2 -2
  265. package/dist/parsers/sessionPathResolver.js +5 -4
  266. package/dist/processDetection.d.ts +10 -3
  267. package/dist/processDetection.js +44 -11
  268. package/dist/promptHistory.d.ts +32 -0
  269. package/dist/promptHistory.js +115 -59
  270. package/dist/providerFailure.d.ts +12 -6
  271. package/dist/providerFailure.js +45 -0
  272. package/dist/providers/claudeCode.js +7 -99
  273. package/dist/providers/claudeListing.d.ts +22 -0
  274. package/dist/providers/claudeListing.js +158 -0
  275. package/dist/providers/codex.d.ts +3 -1
  276. package/dist/providers/codex.js +26 -227
  277. package/dist/providers/codexRollouts.d.ts +44 -0
  278. package/dist/providers/codexRollouts.js +261 -0
  279. package/dist/providers/detect.js +4 -4
  280. package/dist/readers/plans.js +3 -3
  281. package/dist/schemas/accountManager.d.ts +22 -0
  282. package/dist/schemas/accountManager.js +4 -0
  283. package/dist/schemas/assistantTurn.d.ts +2 -2
  284. package/dist/schemas/index.d.ts +2 -0
  285. package/dist/schemas/index.js +7 -2
  286. package/dist/schemas/providerFailure.d.ts +154 -0
  287. package/dist/schemas/providerFailure.js +40 -0
  288. package/dist/sessionPromptHistory.d.ts +12 -7
  289. package/dist/sessionPromptHistory.js +15 -3
  290. package/dist/sessionPromptSignals.d.ts +9 -0
  291. package/dist/sessionPromptSignals.js +13 -0
  292. package/dist/terminalSync.d.ts +12 -2
  293. package/dist/terminalSync.js +27 -2
  294. package/dist/testing/credentialGuard.d.ts +23 -0
  295. package/dist/testing/credentialGuard.js +233 -0
  296. package/dist/testing/index.d.ts +14 -0
  297. package/dist/testing/index.js +22 -0
  298. package/dist/testing/sessionLogFixtures.d.ts +28 -0
  299. package/dist/testing/sessionLogFixtures.js +169 -0
  300. package/dist/tokenEstimation.d.ts +2 -0
  301. package/dist/tokenEstimation.js +21 -11
  302. package/dist/toolCategory.d.ts +18 -0
  303. package/dist/toolCategory.js +86 -0
  304. package/dist/types/codex.d.ts +4 -0
  305. package/dist/usage/usageEvents.d.ts +16 -0
  306. package/dist/usage/usageEvents.js +53 -9
  307. package/dist/watchers/jsonlTail.d.ts +26 -1
  308. package/dist/watchers/jsonlTail.js +136 -28
  309. package/dist/watchers/jsonlWatcher.js +7 -0
  310. package/dist/watchers/providerReaderWatcher.js +1 -0
  311. package/dist/watchers/types.d.ts +6 -0
  312. package/dist/workspaceRoots.d.ts +23 -0
  313. package/dist/workspaceRoots.js +93 -0
  314. package/package.json +22 -2
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Subagent execution trace parser.
3
+ *
4
+ * Extends the existing subagent scanner to preserve full conversation events
5
+ * (not just stats) for drill-down trace visualization. Applies noise classification
6
+ * and tool summary formatting from the formatters module.
7
+ *
8
+ * Three-phase parent-child linking:
9
+ * 1. Result-based: match toolUseResult.agentId → parent tool_use_id
10
+ * 2. Team summary: parse <teammate-message> XML blocks
11
+ * 3. Positional fallback: timestamp ordering + proximity
12
+ *
13
+ * @module parsers/subagentTraceParser
14
+ */
15
+ import * as fs from 'fs';
16
+ import * as path from 'path';
17
+ import { formatToolSummary } from '../formatters/toolSummary';
18
+ import { isHardNoise, classifyMessage, getSoftNoiseReason } from '../formatters/noiseClassifier';
19
+ import { ClaudeUsageDeduper, dedupedRawClaudeUsage } from '../usage/claudeUsageDedupe';
20
+ import { addUsageToSubagent } from '../usage/subagentUsage';
21
+ import { readAgentMeta } from './subagentScanner';
22
+ // ── Constants ──
23
+ const AGENT_FILE_PATTERN = /^agent-(.+)\.jsonl$/;
24
+ // ── Public API ──
25
+ /**
26
+ * Scans and parses subagent traces with full conversation events.
27
+ *
28
+ * Unlike `scanSubagentDir()` which only returns stats, this returns
29
+ * the full conversation trace for each subagent, formatted with tool
30
+ * summaries and noise classification.
31
+ *
32
+ * @param sessionDir - Directory containing the session file
33
+ * @param sessionId - Session ID (filename without .jsonl extension)
34
+ * @returns Array of SubagentTrace objects forming a tree
35
+ */
36
+ export function scanSubagentTraces(sessionDir, sessionId) {
37
+ const subagentsDir = path.join(sessionDir, sessionId, 'subagents');
38
+ try {
39
+ if (!fs.existsSync(subagentsDir))
40
+ return [];
41
+ const files = fs.readdirSync(subagentsDir);
42
+ const traces = [];
43
+ for (const file of files) {
44
+ const match = file.match(AGENT_FILE_PATTERN);
45
+ if (!match)
46
+ continue;
47
+ const agentId = match[1];
48
+ const filePath = path.join(subagentsDir, file);
49
+ const trace = parseAgentTrace(filePath, agentId);
50
+ if (trace)
51
+ traces.push(trace);
52
+ }
53
+ // Link parent-child relationships
54
+ linkTraces(traces);
55
+ // Return only top-level traces (those without parents)
56
+ return traces.filter((t) => !t.parentToolUseId);
57
+ }
58
+ catch {
59
+ return [];
60
+ }
61
+ }
62
+ // ── Parsing ──
63
+ function parseAgentTrace(filePath, agentId) {
64
+ try {
65
+ const content = fs.readFileSync(filePath, 'utf-8');
66
+ const lines = content.split('\n').filter((l) => l.trim());
67
+ const events = [];
68
+ const toolCalls = [];
69
+ let agentType;
70
+ let description;
71
+ const tokens = { agentId, toolCalls: [], inputTokens: 0, outputTokens: 0 };
72
+ const usageDeduper = new ClaudeUsageDeduper();
73
+ let startTime;
74
+ let endTime;
75
+ for (const line of lines) {
76
+ try {
77
+ const raw = JSON.parse(line);
78
+ const timestamp = new Date(raw.timestamp || Date.now());
79
+ if (!startTime)
80
+ startTime = timestamp;
81
+ endTime = timestamp;
82
+ // Build SessionEvent from raw JSONL
83
+ const sessionEvent = rawToSessionEvent(raw);
84
+ if (!sessionEvent)
85
+ continue;
86
+ // Classify and format
87
+ const hardNoise = isHardNoise(sessionEvent);
88
+ const messageClass = classifyMessage(sessionEvent);
89
+ const softNoise = getSoftNoiseReason(sessionEvent);
90
+ // Compute noise level
91
+ let noiseLevel = 'system';
92
+ switch (sessionEvent.type) {
93
+ case 'user':
94
+ noiseLevel = 'user';
95
+ break;
96
+ case 'assistant':
97
+ noiseLevel = 'ai';
98
+ break;
99
+ case 'tool_use':
100
+ noiseLevel = 'system';
101
+ break;
102
+ case 'tool_result':
103
+ noiseLevel = 'noise';
104
+ break;
105
+ case 'summary':
106
+ noiseLevel = 'system';
107
+ break;
108
+ }
109
+ // Format tool summary
110
+ let toolSummary;
111
+ if (sessionEvent.tool) {
112
+ toolSummary = formatToolSummary(sessionEvent.tool.name, sessionEvent.tool.input);
113
+ }
114
+ events.push({
115
+ event: sessionEvent,
116
+ toolSummary,
117
+ noiseLevel,
118
+ messageClassification: messageClass,
119
+ softNoiseReason: softNoise ?? undefined,
120
+ isHardNoise: hardNoise,
121
+ });
122
+ // Extract token usage
123
+ // Split lines of one response repeat its usage; count each response once.
124
+ const usage = dedupedRawClaudeUsage(raw, usageDeduper);
125
+ if (usage)
126
+ addUsageToSubagent(tokens, usage);
127
+ // Extract tool calls
128
+ if (raw.type === 'assistant' && Array.isArray(raw.message?.content)) {
129
+ for (const block of raw.message.content) {
130
+ if (block?.type === 'tool_use') {
131
+ toolCalls.push({
132
+ name: block.name,
133
+ input: block.input || {},
134
+ timestamp,
135
+ });
136
+ // Extract agent info from Task tool
137
+ if (block.name === 'Task' && block.input) {
138
+ if (block.input.subagent_type && !agentType) {
139
+ agentType = String(block.input.subagent_type);
140
+ }
141
+ if (block.input.description && !description) {
142
+ description = String(block.input.description);
143
+ }
144
+ }
145
+ }
146
+ }
147
+ }
148
+ // Extract agent type from system messages
149
+ if (raw.type === 'system' && raw.message?.content) {
150
+ const contentStr = typeof raw.message.content === 'string'
151
+ ? raw.message.content
152
+ : JSON.stringify(raw.message.content);
153
+ const typeMatch = contentStr.match(/subagent_type['":\s]+(\w+)/i);
154
+ if (typeMatch && !agentType) {
155
+ agentType = typeMatch[1];
156
+ }
157
+ }
158
+ }
159
+ catch {
160
+ // Skip malformed lines
161
+ }
162
+ }
163
+ const meta = readAgentMeta(filePath);
164
+ agentType = meta.agentType ?? agentType;
165
+ description = meta.description ?? description;
166
+ if (events.length === 0 && !agentType && !description && !tokens.totalTokens) {
167
+ return null;
168
+ }
169
+ const durationMs = startTime && endTime ? endTime.getTime() - startTime.getTime() : undefined;
170
+ return {
171
+ agentId,
172
+ agentType,
173
+ description,
174
+ events,
175
+ children: [],
176
+ stats: {
177
+ agentId,
178
+ agentType,
179
+ description,
180
+ toolCalls,
181
+ inputTokens: tokens.inputTokens,
182
+ outputTokens: tokens.outputTokens,
183
+ cacheReadTokens: tokens.cacheReadTokens ?? 0,
184
+ cacheWriteTokens: tokens.cacheWriteTokens ?? 0,
185
+ reasoningTokens: tokens.reasoningTokens ?? 0,
186
+ totalTokens: tokens.totalTokens ?? 0,
187
+ startTime,
188
+ endTime,
189
+ durationMs,
190
+ },
191
+ };
192
+ }
193
+ catch {
194
+ return null;
195
+ }
196
+ }
197
+ // ── Parent-Child Linking ──
198
+ function linkTraces(traces) {
199
+ const traceById = new Map();
200
+ for (const trace of traces) {
201
+ traceById.set(trace.agentId, trace);
202
+ }
203
+ // Phase 1: Result-based linking
204
+ // Look for tool_result events that contain agentId references
205
+ for (const trace of traces) {
206
+ for (const traceEvent of trace.events) {
207
+ const evt = traceEvent.event;
208
+ if (evt.type === 'tool_use' && evt.tool?.name === 'Task') {
209
+ // The tool_use_id from this call should appear as a result in the parent
210
+ // but we can link via timestamp proximity to child traces
211
+ }
212
+ }
213
+ }
214
+ // Phase 2: Team summary linking
215
+ for (const trace of traces) {
216
+ for (const traceEvent of trace.events) {
217
+ const content = traceEvent.event.message?.content;
218
+ if (typeof content === 'string' && content.includes('<teammate-message>')) {
219
+ // Parse teammate agent IDs from XML blocks
220
+ const matches = content.matchAll(/agent[_-]id['":\s]+([a-f0-9]+)/gi);
221
+ for (const match of matches) {
222
+ const childId = match[1];
223
+ const child = traceById.get(childId);
224
+ if (child && child !== trace && !child.parentToolUseId) {
225
+ child.parentToolUseId = trace.agentId;
226
+ trace.children.push(child);
227
+ }
228
+ }
229
+ }
230
+ }
231
+ }
232
+ // Phase 3: Positional fallback — timestamp proximity
233
+ // Traces without parents that fall within another trace's time window
234
+ const parentless = traces.filter((t) => !t.parentToolUseId);
235
+ for (const child of parentless) {
236
+ if (!child.stats.startTime)
237
+ continue;
238
+ const childStart = child.stats.startTime.getTime();
239
+ // Find a trace that was active (Task tool call) right before this child started
240
+ for (const potential of traces) {
241
+ if (potential === child)
242
+ continue;
243
+ if (potential.parentToolUseId)
244
+ continue; // Already linked
245
+ for (const traceEvent of potential.events) {
246
+ const evt = traceEvent.event;
247
+ if (evt.type === 'assistant' && evt.tool?.name === 'Task') {
248
+ const taskTime = new Date(evt.timestamp).getTime();
249
+ // Within 2 seconds before child start
250
+ if (taskTime <= childStart && childStart - taskTime < 2000) {
251
+ child.parentToolUseId = potential.agentId;
252
+ potential.children.push(child);
253
+ break;
254
+ }
255
+ }
256
+ }
257
+ if (child.parentToolUseId)
258
+ break;
259
+ }
260
+ }
261
+ }
262
+ // ── Helpers ──
263
+ function rawToSessionEvent(raw) {
264
+ const type = raw.type;
265
+ if (!type)
266
+ return null;
267
+ // Map raw JSONL types to SessionEvent types
268
+ const typeMap = {
269
+ user: 'user',
270
+ assistant: 'assistant',
271
+ tool_use: 'tool_use',
272
+ tool_result: 'tool_result',
273
+ summary: 'summary',
274
+ };
275
+ const mappedType = typeMap[type];
276
+ if (!mappedType)
277
+ return null;
278
+ const message = raw.message;
279
+ const timestamp = raw.timestamp || new Date().toISOString();
280
+ // Extract tool info for tool_use events from content blocks
281
+ let tool;
282
+ let result;
283
+ if (mappedType === 'assistant' && message?.content && Array.isArray(message.content)) {
284
+ // Look for inline tool_use blocks
285
+ for (const block of message.content) {
286
+ if (block.type === 'tool_use') {
287
+ tool = {
288
+ name: block.name,
289
+ input: block.input || {},
290
+ };
291
+ break;
292
+ }
293
+ }
294
+ }
295
+ if (mappedType === 'tool_result') {
296
+ const msg = raw.message;
297
+ if (msg?.content && Array.isArray(msg.content)) {
298
+ for (const block of msg.content) {
299
+ if (block.type === 'tool_result') {
300
+ result = {
301
+ tool_use_id: block.tool_use_id,
302
+ output: block.content,
303
+ is_error: block.is_error,
304
+ };
305
+ break;
306
+ }
307
+ }
308
+ }
309
+ }
310
+ const common = {
311
+ timestamp,
312
+ isSidechain: raw.isSidechain,
313
+ permissionMode: raw.permissionMode,
314
+ };
315
+ if (mappedType === 'user' || mappedType === 'assistant') {
316
+ return { ...common, type: mappedType, message: message || { role: 'unknown' }, tool };
317
+ }
318
+ if (mappedType === 'summary') {
319
+ return { ...common, type: 'summary', message: message || { role: 'summary' } };
320
+ }
321
+ if (mappedType === 'tool_use') {
322
+ return tool ? { ...common, type: 'tool_use', message, tool } : null;
323
+ }
324
+ return result ? { ...common, type: 'tool_result', message, result } : null;
325
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Config path resolution and workspace encoding.
3
+ * Mirrors patterns from sidekick-vscode/src/services/SessionPathResolver.ts
4
+ * and TaskPersistenceService.ts
5
+ */
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+ import * as os from 'os';
9
+ /** Environment variable that redirects the Sidekick config root. */
10
+ export const CONFIG_DIR_ENV_VAR = 'SIDEKICK_CONFIG_DIR';
11
+ let configDirOverride = null;
12
+ /**
13
+ * The platform default config directory, ignoring both {@link setConfigDir}
14
+ * and `SIDEKICK_CONFIG_DIR`.
15
+ *
16
+ * `~/.config/sidekick` on Unix, `%APPDATA%/sidekick` on Windows.
17
+ */
18
+ export function getDefaultConfigDir() {
19
+ if (process.platform === 'win32') {
20
+ return path.join(process.env.APPDATA || os.homedir(), 'sidekick');
21
+ }
22
+ return path.join(os.homedir(), '.config', 'sidekick');
23
+ }
24
+ /**
25
+ * Redirect the Sidekick config root for this process.
26
+ *
27
+ * Takes precedence over `SIDEKICK_CONFIG_DIR`. Pass `null` (or an empty
28
+ * string) to clear it. Every reader and writer in the package resolves the
29
+ * root lazily through {@link getConfigDir}, so this takes effect immediately
30
+ * for all of them — including transitive callers of `getProjectDataPath`,
31
+ * `getGlobalDataPath`, and the account registry.
32
+ *
33
+ * Prefer `SIDEKICK_CONFIG_DIR` when the value is known before the process
34
+ * starts: it is read fresh on every call, so it stays correct even if a
35
+ * bundler ends up with more than one copy of this module.
36
+ *
37
+ * @example
38
+ * setConfigDir(path.join(os.tmpdir(), 'sidekick-fixture'));
39
+ * try {
40
+ * await addTask(project, { subject: 'isolated' });
41
+ * } finally {
42
+ * setConfigDir(null);
43
+ * }
44
+ */
45
+ export function setConfigDir(dir) {
46
+ // Resolve the trimmed value, not the original: `path.resolve(' /tmp/x ')`
47
+ // treats the leading spaces as a relative segment and lands under cwd. The
48
+ // `SIDEKICK_CONFIG_DIR` branch in getConfigDir() already trims, and the two
49
+ // routes are documented as interchangeable.
50
+ const trimmed = dir?.trim();
51
+ configDirOverride = trimmed ? path.resolve(trimmed) : null;
52
+ }
53
+ /** The active {@link setConfigDir} override, or `null` when unset. */
54
+ export function getConfigDirOverride() {
55
+ return configDirOverride;
56
+ }
57
+ /**
58
+ * Gets the Sidekick config directory.
59
+ *
60
+ * Resolution order: {@link setConfigDir} override → `SIDEKICK_CONFIG_DIR`
61
+ * → {@link getDefaultConfigDir}.
62
+ */
63
+ export function getConfigDir() {
64
+ if (configDirOverride)
65
+ return configDirOverride;
66
+ const fromEnv = process.env[CONFIG_DIR_ENV_VAR]?.trim();
67
+ if (fromEnv)
68
+ return path.resolve(fromEnv);
69
+ return getDefaultConfigDir();
70
+ }
71
+ /**
72
+ * Gets the path to a project-specific data file.
73
+ * e.g., ~/.config/sidekick/tasks/my-project.json
74
+ */
75
+ export function getProjectDataPath(slug, subdomain) {
76
+ return path.join(getConfigDir(), subdomain, `${slug}.json`);
77
+ }
78
+ /**
79
+ * Gets the path to a global data file.
80
+ * e.g., ~/.config/sidekick/historical-data.json
81
+ */
82
+ export function getGlobalDataPath(filename) {
83
+ return path.join(getConfigDir(), filename);
84
+ }
85
+ /**
86
+ * Encodes a workspace path into Sidekick's own config-store slug — path
87
+ * separators, colons, and underscores become hyphens, dots are kept.
88
+ *
89
+ * This is NOT Claude Code's `~/.claude/projects/` naming scheme, despite the
90
+ * shared name: Claude Code also replaces dots, spaces, and every other
91
+ * non-alphanumeric character. To locate Claude Code session directories use
92
+ * `encodeClaudeWorkspacePath` / `getClaudeSessionDirectory` from
93
+ * `parsers/sessionPathResolver` instead.
94
+ */
95
+ export function encodeWorkspacePath(workspacePath) {
96
+ const normalized = workspacePath.replace(/\\/g, '/');
97
+ return normalized.replace(/[:/_]/g, '-');
98
+ }
99
+ /**
100
+ * Derives a project slug from a workspace path.
101
+ * Resolves symlinks, then encodes for use as a filename.
102
+ */
103
+ export function getProjectSlug(cwd) {
104
+ return resolveProjectIdentity(cwd).canonicalSlug;
105
+ }
106
+ /** Resolves the canonical project identity used by all new readers and writers. */
107
+ export function resolveProjectIdentity(cwd) {
108
+ const dir = path.resolve(cwd || process.cwd());
109
+ let resolved;
110
+ try {
111
+ resolved = fs.realpathSync(dir);
112
+ }
113
+ catch {
114
+ resolved = dir;
115
+ }
116
+ const canonicalSlug = encodeWorkspacePath(resolved);
117
+ const legacySlug = encodeWorkspacePath(dir);
118
+ return {
119
+ cwd: dir,
120
+ resolvedCwd: resolved,
121
+ canonicalSlug,
122
+ legacySlug,
123
+ candidates: canonicalSlug === legacySlug ? [canonicalSlug] : [canonicalSlug, legacySlug],
124
+ hasLegacyMismatch: canonicalSlug !== legacySlug,
125
+ };
126
+ }
127
+ /**
128
+ * Derives a project slug WITHOUT resolving symlinks.
129
+ * Matches the VS Code extension's behavior (encodeWorkspacePath on raw path).
130
+ * Use this when reading data written by the extension.
131
+ */
132
+ export function getProjectSlugRaw(cwd) {
133
+ return resolveProjectIdentity(cwd).legacySlug;
134
+ }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Stateless peak-hours fetcher for promoclock.co.
3
+ *
4
+ * PromoClock is a free, third-party tracker (maintained by @onursendere,
5
+ * not affiliated with Anthropic) that reports whether Claude's session
6
+ * limits are draining at normal or faster rate. Peak window is weekdays
7
+ * 13:00–19:00 UTC; session limits drain faster during that window for
8
+ * Free / Pro / Max / Team subscriptions.
9
+ *
10
+ * Polling / eventing is the caller's responsibility.
11
+ */
12
+ const PROMOCLOCK_ENDPOINT = 'https://promoclock.co/api/status';
13
+ /**
14
+ * Default budget for the promoclock.co request, in milliseconds. The schedule
15
+ * fallback answers the question anyway, so a slow third-party host should not
16
+ * hold up a `sidekick status` or `sidekick quota` run for long.
17
+ */
18
+ export const DEFAULT_PEAK_HOURS_TIMEOUT_MS = 4_000;
19
+ /** A promoclock.co answer is reused for this long before the host is asked again. */
20
+ export const PEAK_HOURS_CACHE_MS = 10 * 60_000;
21
+ /** Published peak window: weekdays 13:00–19:00 UTC. */
22
+ export const PEAK_WINDOW_START_UTC_HOUR = 13;
23
+ export const PEAK_WINDOW_END_UTC_HOUR = 19;
24
+ export const PEAK_HOURS_DESCRIPTION = 'Weekdays 13:00–19:00 UTC';
25
+ let cachedNetworkState = null;
26
+ /** Forget the cached promoclock.co answer. Test-only. */
27
+ export function _resetPeakHoursCache() {
28
+ cachedNetworkState = null;
29
+ }
30
+ function isWeekdayUtc(date) {
31
+ const day = date.getUTCDay();
32
+ return day >= 1 && day <= 5;
33
+ }
34
+ function utcDateAtHour(date, hour, dayOffset = 0) {
35
+ return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate() + dayOffset, hour));
36
+ }
37
+ /**
38
+ * Peak-hours state computed from the published schedule alone, with no
39
+ * network access. Used directly by cache-only commands (`sidekick today`) and
40
+ * as the fallback when promoclock.co cannot be reached, so every surface
41
+ * agrees on whether it is currently peak.
42
+ */
43
+ export function getScheduledPeakHoursState(now = new Date()) {
44
+ const hour = now.getUTCHours() + now.getUTCMinutes() / 60;
45
+ const isPeak = isWeekdayUtc(now) && hour >= PEAK_WINDOW_START_UTC_HOUR && hour < PEAK_WINDOW_END_UTC_HOUR;
46
+ let nextChange;
47
+ if (isPeak) {
48
+ nextChange = utcDateAtHour(now, PEAK_WINDOW_END_UTC_HOUR);
49
+ }
50
+ else if (isWeekdayUtc(now) && hour < PEAK_WINDOW_START_UTC_HOUR) {
51
+ nextChange = utcDateAtHour(now, PEAK_WINDOW_START_UTC_HOUR);
52
+ }
53
+ else {
54
+ let offset = 1;
55
+ while (!isWeekdayUtc(utcDateAtHour(now, 0, offset)))
56
+ offset += 1;
57
+ nextChange = utcDateAtHour(now, PEAK_WINDOW_START_UTC_HOUR, offset);
58
+ }
59
+ return {
60
+ status: isPeak ? 'peak' : 'off_peak',
61
+ isPeak,
62
+ sessionLimitSpeed: isPeak ? 'faster' : 'normal',
63
+ label: isPeak ? 'Peak Hours' : 'Off-Peak',
64
+ peakHoursDescription: PEAK_HOURS_DESCRIPTION,
65
+ nextChange: nextChange.toISOString(),
66
+ minutesUntilChange: Math.max(0, Math.round((nextChange.getTime() - now.getTime()) / 60_000)),
67
+ note: 'Computed from the published weekday schedule.',
68
+ updatedAt: now.toISOString(),
69
+ unavailable: false,
70
+ source: 'schedule',
71
+ };
72
+ }
73
+ function unavailableState() {
74
+ return {
75
+ status: 'unknown',
76
+ isPeak: false,
77
+ sessionLimitSpeed: 'unknown',
78
+ label: 'Peak-hours status unavailable',
79
+ peakHoursDescription: '',
80
+ nextChange: null,
81
+ minutesUntilChange: null,
82
+ note: '',
83
+ updatedAt: new Date().toISOString(),
84
+ unavailable: true,
85
+ };
86
+ }
87
+ const PROVIDER_DISPLAY_NAMES = {
88
+ 'claude-code': 'Claude Code',
89
+ opencode: 'OpenCode',
90
+ codex: 'Codex CLI',
91
+ };
92
+ export function isClaudeCodeSessionProvider(providerId) {
93
+ return providerId === 'claude-code';
94
+ }
95
+ export function createPeakHoursNotApplicableState(providerId) {
96
+ const providerName = PROVIDER_DISPLAY_NAMES[providerId] ?? providerId;
97
+ return {
98
+ status: 'unknown',
99
+ isPeak: false,
100
+ sessionLimitSpeed: 'unknown',
101
+ label: 'Claude peak hours not applicable',
102
+ peakHoursDescription: '',
103
+ nextChange: null,
104
+ minutesUntilChange: null,
105
+ note: `Claude peak hours apply only to Claude Code sessions, not ${providerName}.`,
106
+ updatedAt: new Date().toISOString(),
107
+ unavailable: true,
108
+ notApplicable: true,
109
+ };
110
+ }
111
+ export function scopePeakHoursToSessionProvider(providerId, status) {
112
+ if (!isClaudeCodeSessionProvider(providerId))
113
+ return null;
114
+ return status ?? null;
115
+ }
116
+ function normalizeStatus(raw) {
117
+ if (raw === 'peak' || raw === 'off_peak')
118
+ return raw;
119
+ return 'unknown';
120
+ }
121
+ function normalizeSpeed(raw) {
122
+ if (raw === 'normal' || raw === 'faster')
123
+ return raw;
124
+ return 'unknown';
125
+ }
126
+ /**
127
+ * Fetch current Claude peak-hours state from promoclock.co.
128
+ *
129
+ * Single-shot — caller wraps in polling loop, EventEmitter, or interval.
130
+ * A successful answer is cached in-process for {@link PEAK_HOURS_CACHE_MS}.
131
+ * On any network, timeout, or parse error the state is computed from the
132
+ * published schedule instead (`source: 'schedule'`), so callers always get a
133
+ * usable answer and every surface agrees on it; {@link unavailableState} is
134
+ * only returned when the schedule itself cannot be evaluated.
135
+ *
136
+ * promoclock.co is a third-party host, so the request is always bounded by
137
+ * an abort timeout. Without one a hung host would stall the caller — and
138
+ * this is polled, so the stall would repeat.
139
+ */
140
+ export async function fetchPeakHoursStatus(options = {}) {
141
+ const now = Date.now();
142
+ if (!options.force &&
143
+ cachedNetworkState &&
144
+ now - cachedNetworkState.fetchedAt < PEAK_HOURS_CACHE_MS) {
145
+ return cachedNetworkState.state;
146
+ }
147
+ const timeoutMs = options.timeoutMs ?? DEFAULT_PEAK_HOURS_TIMEOUT_MS;
148
+ const controller = new AbortController();
149
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
150
+ timeout.unref();
151
+ try {
152
+ const res = await fetch(PROMOCLOCK_ENDPOINT, { signal: controller.signal });
153
+ if (!res.ok)
154
+ return scheduleFallback();
155
+ const data = await res.json();
156
+ const status = normalizeStatus(data.status);
157
+ const sessionLimitSpeed = normalizeSpeed(data.sessionLimitSpeed);
158
+ const isPeak = typeof data.isPeak === 'boolean' ? data.isPeak : status === 'peak';
159
+ const state = {
160
+ status,
161
+ isPeak,
162
+ sessionLimitSpeed,
163
+ label: data.label ?? (isPeak ? 'Peak Hours' : 'Off-Peak'),
164
+ peakHoursDescription: data.peakHours ?? '',
165
+ nextChange: data.nextChange ?? null,
166
+ minutesUntilChange: typeof data.minutesUntilChange === 'number' ? data.minutesUntilChange : null,
167
+ note: data.note ?? '',
168
+ updatedAt: data.timestamp ?? new Date().toISOString(),
169
+ unavailable: false,
170
+ source: 'promoclock',
171
+ };
172
+ cachedNetworkState = { state, fetchedAt: now };
173
+ return state;
174
+ }
175
+ catch {
176
+ // Covers AbortError from the timeout above alongside network/parse failures.
177
+ return scheduleFallback();
178
+ }
179
+ finally {
180
+ clearTimeout(timeout);
181
+ }
182
+ }
183
+ function scheduleFallback() {
184
+ try {
185
+ const state = getScheduledPeakHoursState();
186
+ return {
187
+ ...state,
188
+ note: 'promoclock.co unreachable; computed from the published weekday schedule.',
189
+ };
190
+ }
191
+ catch {
192
+ return unavailableState();
193
+ }
194
+ }