volute 0.33.0 → 0.35.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 (255) hide show
  1. package/README.md +7 -6
  2. package/dist/accept-ZBDVVCEU.js +42 -0
  3. package/dist/activity-events-ZW4SDL2C.js +15 -0
  4. package/dist/{ai-service-SBY2WG7O.js → ai-service-LURBEDDB.js} +6 -6
  5. package/dist/{api-client-YPKOZP2O.js → api-client-3A77HMH7.js} +2 -2
  6. package/dist/api.d.ts +1 -5195
  7. package/dist/{archive-INXYFVCW.js → archive-ESU2FUN4.js} +4 -4
  8. package/dist/{auth-GKCDSO4T.js → auth-WX4TESEI.js} +6 -6
  9. package/dist/bridge-PXIO6PS2.js +206 -0
  10. package/dist/chat-QXAJF3FU.js +51 -0
  11. package/dist/{chunk-NNB4WIG7.js → chunk-2TGZJFAT.js} +3 -3
  12. package/dist/{chunk-6LXAAQ43.js → chunk-33ODGMFZ.js} +1 -1
  13. package/dist/{chunk-RPZZSXV3.js → chunk-5N7Y5WAM.js} +21 -2
  14. package/dist/chunk-5T5YMX6S.js +23 -0
  15. package/dist/{chunk-7J3HEVR7.js → chunk-5XJYUFZH.js} +28 -16
  16. package/dist/chunk-7KJOFUNN.js +22 -0
  17. package/dist/{chunk-2NGTS5UU.js → chunk-A2ZLHBHG.js} +2 -2
  18. package/dist/{chunk-KIEPMIM5.js → chunk-AN2W47GW.js} +2 -2
  19. package/dist/{chunk-G53F3JA4.js → chunk-AOB6GVRM.js} +1 -1
  20. package/dist/{chunk-LRCG2JLP.js → chunk-BDYXIWA5.js} +9 -5
  21. package/dist/{chunk-YUIHSKR6.js → chunk-BKF4WQCY.js} +2 -2
  22. package/dist/{chunk-N432I7QH.js → chunk-BMZQYACC.js} +2 -2
  23. package/dist/{chunk-NAOW2CLO.js → chunk-BTY4WNFE.js} +1 -1
  24. package/dist/{chunk-ALEF47VT.js → chunk-BV65KRHM.js} +2 -2
  25. package/dist/{chunk-KVK2DLWI.js → chunk-CORXD635.js} +4 -4
  26. package/dist/{chunk-PVY5W6QN.js → chunk-F7ZNLYKZ.js} +2 -2
  27. package/dist/{chunk-QTUVYI7W.js → chunk-FT5KETXZ.js} +3 -3
  28. package/dist/{chunk-C7I35G4R.js → chunk-IJHIXLVN.js} +44 -8
  29. package/dist/{chunk-JUKK7FPS.js → chunk-J6CJQDWI.js} +37 -28
  30. package/dist/{chunk-4RQBJWQX.js → chunk-LOPXTW6H.js} +1 -1
  31. package/dist/{chunk-RSX4OPZY.js → chunk-MDJGMOSD.js} +8 -137
  32. package/dist/{chunk-LOEJ4HPQ.js → chunk-N446KRP7.js} +3 -3
  33. package/dist/{chunk-I5KY25PQ.js → chunk-N5LMGYXX.js} +2 -2
  34. package/dist/{chunk-G6BSYHPK.js → chunk-NJK5SDGR.js} +1 -1
  35. package/dist/{chunk-D424ZQGI.js → chunk-O7IGP7ZW.js} +11 -3
  36. package/dist/{chunk-M7UL5S3Q.js → chunk-OTC67N2Z.js} +2 -2
  37. package/dist/{chunk-GY5HBI7A.js → chunk-PWQ2ITYG.js} +4 -4
  38. package/dist/{chunk-KTLFDYPT.js → chunk-QCH6K235.js} +1 -1
  39. package/dist/chunk-QHG4OMZL.js +145 -0
  40. package/dist/{chunk-SKLSMHXO.js → chunk-QWTR6AWZ.js} +3 -3
  41. package/dist/chunk-TXSA4Q3V.js +116 -0
  42. package/dist/{chunk-VH33ZWMW.js → chunk-VHJRZM2S.js} +2 -2
  43. package/dist/{chunk-SSI47XP2.js → chunk-VHWGEJ4V.js} +1 -1
  44. package/dist/chunk-VY3RB2V7.js +164 -0
  45. package/dist/chunk-WJPROOU5.js +8314 -0
  46. package/dist/{chunk-RVGLDGMI.js → chunk-WZRZFFCL.js} +25 -27
  47. package/dist/{chunk-JYVGHWEJ.js → chunk-XRQSAMX2.js} +4 -4
  48. package/dist/{chunk-OYAKCAVY.js → chunk-ZSR72JB3.js} +1 -1
  49. package/dist/{chunk-UKVWJRKN.js → chunk-ZX7EAV5J.js} +17 -7
  50. package/dist/cli.js +90 -29
  51. package/dist/clock-HSEKS5AR.js +289 -0
  52. package/dist/{cloud-sync-4NWLMFVH.js → cloud-sync-6JL4C24T.js} +22 -23
  53. package/dist/config-UTS7QULS.js +76 -0
  54. package/dist/connectors/discord-bridge.js +4 -4
  55. package/dist/connectors/slack-bridge.js +4 -4
  56. package/dist/connectors/telegram-bridge.js +4 -4
  57. package/dist/{conversations-AWI5SZW2.js → conversations-2PW57WO2.js} +6 -6
  58. package/dist/create-5BPOOJAN.js +75 -0
  59. package/dist/create-UVCK2CS6.js +50 -0
  60. package/dist/daemon-client-RVIKXGFQ.js +12 -0
  61. package/dist/daemon-restart-HSZ3BCX5.js +65 -0
  62. package/dist/daemon.js +1349 -1211
  63. package/dist/db-BDMH4SZ2.js +20 -0
  64. package/dist/db-BVBJ57TU.js +9 -0
  65. package/dist/delete-L5PAVDGQ.js +42 -0
  66. package/dist/delivery-manager-H5ZVBMCQ.js +31 -0
  67. package/dist/{delivery-router-FL45JL7N.js → delivery-router-HEJSJAHQ.js} +5 -5
  68. package/dist/down-74VXM45A.js +17 -0
  69. package/dist/env-E4XHO2BI.js +223 -0
  70. package/dist/exec-PY7THYH4.js +17 -0
  71. package/dist/export-OAS6QVBN.js +113 -0
  72. package/dist/extension-D74CNM7G.js +89 -0
  73. package/dist/extensions-XDDFY72A.js +49 -0
  74. package/dist/files-CWTK6V3H.js +53 -0
  75. package/dist/import-5A3T7QV4.js +143 -0
  76. package/dist/{isolation-LLAYQYDY.js → isolation-TK5RX2WM.js} +4 -4
  77. package/dist/join-DF5XSJAC.js +67 -0
  78. package/dist/lib-DYEZMGW7.js +6588 -0
  79. package/dist/list-PDMQM7ZV.js +53 -0
  80. package/dist/login-7TE6CIZF.js +60 -0
  81. package/dist/login-GOTAYLXP.js +51 -0
  82. package/dist/logout-6KIA74EV.js +29 -0
  83. package/dist/logout-T4XS6LRU.js +50 -0
  84. package/dist/message-delivery-GRC4W6P7.js +41 -0
  85. package/dist/mind-5IEYKV7I.js +97 -0
  86. package/dist/mind-activity-tracker-QBLIV7ZJ.js +18 -0
  87. package/dist/mind-history-IE2QH7U5.js +275 -0
  88. package/dist/mind-list-GEWHWAL4.js +38 -0
  89. package/dist/mind-manager-HFLB5653.js +31 -0
  90. package/dist/mind-profile-DCBDVF5B.js +53 -0
  91. package/dist/mind-service-X2CAA6W6.js +37 -0
  92. package/dist/mind-sleep-ITCF6OQA.js +47 -0
  93. package/dist/mind-status-X4SX3YUG.js +65 -0
  94. package/dist/mind-wake-KXMKMGWX.js +42 -0
  95. package/dist/{package-U3VFO273.js → package-D2FSVFAX.js} +11 -8
  96. package/dist/read-67VRP2DO.js +91 -0
  97. package/dist/{read-stdin-HQJ7774D.js → read-stdin-3X5VYKNS.js} +2 -2
  98. package/dist/register-SB7NXCOE.js +51 -0
  99. package/dist/{registry-PJ4S5PHQ.js → registry-GBSNW3HG.js} +3 -3
  100. package/dist/reject-MUR2KWJ4.js +40 -0
  101. package/dist/restart-5EGG4JXU.js +42 -0
  102. package/dist/{sandbox-GJOK4QLQ.js → sandbox-R37VIU36.js} +6 -6
  103. package/dist/scheduler-Y7O4CJXL.js +31 -0
  104. package/dist/{schema-PA3M5ZKH.js → schema-XVZ2CLKW.js} +4 -2
  105. package/dist/{seed-QDYVLG74.js → seed-EQORWX77.js} +3 -3
  106. package/dist/seed-check-KJNTL72M.js +35 -0
  107. package/dist/seed-cmd-ZM2XGVU2.js +30 -0
  108. package/dist/seed-create-DRWGGHEI.js +113 -0
  109. package/dist/seed-sprout-JYXGXOP3.js +148 -0
  110. package/dist/send-JBJJQ7CA.js +409 -0
  111. package/dist/service-WNPCNHOX.js +121 -0
  112. package/dist/{setup-XMCBE3LF.js → setup-BJ4YAY26.js} +155 -129
  113. package/dist/{setup-TISPCO22.js → setup-RHJRFURI.js} +4 -4
  114. package/dist/skill-TAAKEYBV.js +389 -0
  115. package/dist/skills/plan-coordinator/SKILL.md +60 -0
  116. package/dist/skills/volute-mind/SKILL.md +9 -227
  117. package/dist/skills/volute-mind/references/extensions.md +34 -0
  118. package/dist/skills/volute-mind/references/integrations.md +48 -0
  119. package/dist/skills/volute-mind/references/routing.md +86 -0
  120. package/dist/skills/volute-mind/references/sleep.md +33 -0
  121. package/dist/skills/volute-mind/references/variants.md +31 -0
  122. package/dist/{skills-7FV7EJTE.js → skills-EKMCQ46K.js} +12 -8
  123. package/dist/sleep-manager-7KFK3USC.js +35 -0
  124. package/dist/spirit-ZFRDXMG7.js +23 -0
  125. package/dist/split-AWVOYOPZ.js +64 -0
  126. package/dist/{sprout-WKLZXUIQ.js → sprout-HE4TITMK.js} +3 -3
  127. package/dist/start-3UXOPXQG.js +39 -0
  128. package/dist/status-ZK34WYIM.js +125 -0
  129. package/dist/stop-3XYIBGFM.js +41 -0
  130. package/dist/system-chat-IDPHYHY4.js +35 -0
  131. package/dist/systems-O43WGQY6.js +52 -0
  132. package/dist/{tailscale-XHQBZROW.js → tailscale-ZIZ2HWJ5.js} +5 -5
  133. package/dist/template-hash-A7FNHTB7.js +9 -0
  134. package/dist/up-77ICEDEW.js +19 -0
  135. package/dist/update-ANE5ZM7F.js +225 -0
  136. package/dist/{update-check-ZD6OOIYQ.js → update-check-UV55CBEP.js} +4 -4
  137. package/dist/upgrade-ZMDGC7M2.js +74 -0
  138. package/dist/variant-QWL2WSRI.js +62 -0
  139. package/dist/{version-notify-NBI2MTJO.js → version-notify-FXSEMXWW.js} +29 -28
  140. package/dist/{volute-config-HD7WWUQC.js → volute-config-D2XVS2YI.js} +2 -2
  141. package/dist/web-assets/assets/index-BhxWKvbB.css +1 -0
  142. package/dist/web-assets/assets/index-CHVKJ9II.js +75 -0
  143. package/dist/web-assets/ext-theme.css +48 -9
  144. package/dist/web-assets/index.html +2 -2
  145. package/dist/web-assets/sw.js +117 -0
  146. package/drizzle/0005_meta_summaries.sql +15 -0
  147. package/drizzle/meta/0005_snapshot.json +7 -0
  148. package/drizzle/meta/_journal.json +7 -0
  149. package/package.json +10 -7
  150. package/packages/extensions/pages/dist/ui/assets/index-DKZLNMED.js +2 -0
  151. package/packages/extensions/pages/dist/ui/index.html +1 -1
  152. package/packages/extensions/pages/skills/pages/SKILL.md +84 -9
  153. package/packages/extensions/plan/dist/ui/assets/index-CJj2gZnZ.css +1 -0
  154. package/packages/extensions/plan/dist/ui/assets/index-FMEJmvQz.js +61 -0
  155. package/packages/extensions/plan/dist/ui/index.html +14 -0
  156. package/packages/extensions/plan/skills/plan/SKILL.md +43 -0
  157. package/packages/extensions/plan/skills/plan/scripts/plan-hook.sh +37 -0
  158. package/templates/_base/home/VOLUTE.md +12 -19
  159. package/templates/_base/src/lib/auto-commit.ts +8 -8
  160. package/templates/_base/src/lib/context-breakdown.ts +450 -0
  161. package/templates/_base/src/lib/format-prefix.ts +17 -0
  162. package/templates/_base/src/lib/hook-loader.ts +8 -2
  163. package/templates/_base/src/lib/router.ts +75 -33
  164. package/templates/_base/src/lib/routing.ts +4 -1
  165. package/templates/_base/src/lib/startup.ts +16 -8
  166. package/templates/_base/src/lib/types.ts +2 -1
  167. package/templates/_base/src/lib/volute-server.ts +75 -8
  168. package/templates/claude/.init/CLAUDE.md +4 -10
  169. package/templates/claude/package.json.tmpl +1 -0
  170. package/templates/claude/src/agent.ts +108 -33
  171. package/templates/claude/src/lib/hooks/reply-instructions.ts +27 -7
  172. package/templates/claude/src/lib/stream-consumer.ts +2 -2
  173. package/templates/claude/src/server.ts +1 -0
  174. package/templates/codex/package.json.tmpl +1 -0
  175. package/templates/codex/src/agent.ts +80 -8
  176. package/templates/codex/src/server.ts +1 -4
  177. package/templates/pi/package.json.tmpl +1 -0
  178. package/templates/pi/src/agent.ts +115 -36
  179. package/templates/pi/src/lib/event-handler.ts +22 -7
  180. package/templates/pi/src/lib/reply-instructions-extension.ts +23 -4
  181. package/templates/pi/src/lib/subagents.ts +20 -17
  182. package/templates/pi/src/server.ts +2 -5
  183. package/dist/accept-D5VBM7JW.js +0 -42
  184. package/dist/activity-events-XJO3P4RR.js +0 -15
  185. package/dist/bridge-TXWWPPOJ.js +0 -207
  186. package/dist/chat-U5ZOME3O.js +0 -68
  187. package/dist/chunk-3Z2DPESO.js +0 -3634
  188. package/dist/chunk-A2A4KLFE.js +0 -1528
  189. package/dist/chunk-K3NQKI34.js +0 -10
  190. package/dist/chunk-NPKSDYA2.js +0 -156
  191. package/dist/chunk-PB65JZK2.js +0 -85
  192. package/dist/clock-BVH3V6E3.js +0 -266
  193. package/dist/config-H2H4UIF7.js +0 -72
  194. package/dist/create-2FK7Z46Y.js +0 -44
  195. package/dist/create-YWD2TIP4.js +0 -71
  196. package/dist/daemon-client-6QXHZ7US.js +0 -12
  197. package/dist/daemon-restart-GOBUKLX7.js +0 -52
  198. package/dist/db-F34YLV7D.js +0 -9
  199. package/dist/db-RA45JBFG.js +0 -16
  200. package/dist/delete-QTGWEDBI.js +0 -35
  201. package/dist/delivery-manager-PFAKEJTC.js +0 -32
  202. package/dist/down-FWWTEKXM.js +0 -15
  203. package/dist/env-JCOF2222.js +0 -191
  204. package/dist/export-SUYRLI5Q.js +0 -112
  205. package/dist/extension-OBTGKQQD.js +0 -175
  206. package/dist/extensions-KYNTVTMO.js +0 -30
  207. package/dist/files-65PMW5IK.js +0 -47
  208. package/dist/history-DKCDI3JO.js +0 -128
  209. package/dist/import-DDUFE7AY.js +0 -23
  210. package/dist/join-I5QEE3LG.js +0 -66
  211. package/dist/list-JQ463EDA.js +0 -41
  212. package/dist/login-D7ETSU4R.js +0 -47
  213. package/dist/login-RIJF2F4G.js +0 -47
  214. package/dist/logout-5MLHZALK.js +0 -40
  215. package/dist/logout-UZJRGY4Z.js +0 -21
  216. package/dist/message-delivery-DFF5SJRM.js +0 -42
  217. package/dist/mind-IOJFLEM5.js +0 -108
  218. package/dist/mind-activity-tracker-F6O4Q2SL.js +0 -18
  219. package/dist/mind-list-WUPMQDYQ.js +0 -30
  220. package/dist/mind-manager-NBJF5D26.js +0 -32
  221. package/dist/mind-profile-P67FEHOY.js +0 -47
  222. package/dist/mind-service-2MQ6UK5N.js +0 -38
  223. package/dist/mind-sleep-WW2IX7JT.js +0 -42
  224. package/dist/mind-status-L3EFFRPR.js +0 -56
  225. package/dist/mind-wake-VSSGW465.js +0 -37
  226. package/dist/read-EBY56C33.js +0 -75
  227. package/dist/register-HD74C4TT.js +0 -47
  228. package/dist/reject-UJKFBHRO.js +0 -40
  229. package/dist/restart-3UCMRUVC.js +0 -33
  230. package/dist/scheduler-ZZ7XGQG6.js +0 -32
  231. package/dist/seed-check-S2IX25RL.js +0 -32
  232. package/dist/seed-cmd-DKOUFEAU.js +0 -36
  233. package/dist/seed-create-4XBBOLRH.js +0 -112
  234. package/dist/seed-sprout-GQEIIQRT.js +0 -132
  235. package/dist/send-QIV2INHB.js +0 -373
  236. package/dist/skill-PSQGRRJX.js +0 -358
  237. package/dist/skills/shared-files/SKILL.md +0 -44
  238. package/dist/skills/shared-files/scripts/merge.ts +0 -72
  239. package/dist/skills/shared-files/scripts/pull.ts +0 -52
  240. package/dist/sleep-manager-JTXSN7NV.js +0 -36
  241. package/dist/spirit-VRONKFMF.js +0 -23
  242. package/dist/split-STOROBYJ.js +0 -63
  243. package/dist/start-K2NCUUCG.js +0 -33
  244. package/dist/status-3JBTFSMI.js +0 -115
  245. package/dist/stop-H26JZDXF.js +0 -32
  246. package/dist/system-chat-JAPOJ3KE.js +0 -36
  247. package/dist/systems-XRI52VCH.js +0 -61
  248. package/dist/template-hash-A6VVKOXJ.js +0 -9
  249. package/dist/up-M5AS6SBV.js +0 -18
  250. package/dist/update-UD543CXX.js +0 -215
  251. package/dist/upgrade-O4Q7WJM3.js +0 -67
  252. package/dist/variant-7TGZHOU3.js +0 -41
  253. package/dist/web-assets/assets/index-CWJrVveV.css +0 -1
  254. package/dist/web-assets/assets/index-DJt14FRI.js +0 -75
  255. package/packages/extensions/pages/dist/ui/assets/index-tLTROSk5.js +0 -2
@@ -0,0 +1,450 @@
1
+ import { readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import type { ContextBreakdown } from "./volute-server.js";
4
+
5
+ // Lazy-loaded tokenizer — first call loads the encoding (~100ms), subsequent calls are fast
6
+ let _countTokens: ((text: string) => number) | null = null;
7
+
8
+ function countTokens(text: string): number {
9
+ if (!_countTokens) {
10
+ try {
11
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
12
+ const mod = require("@anthropic-ai/tokenizer");
13
+ _countTokens = mod.countTokens;
14
+ } catch (err) {
15
+ // Tokenizer not installed — fall back to character estimation
16
+ console.warn(
17
+ "context-breakdown: @anthropic-ai/tokenizer not available, using character estimation:",
18
+ err instanceof Error ? err.message : err,
19
+ );
20
+ _countTokens = (t: string) => Math.round(t.length / 3.5);
21
+ }
22
+ }
23
+ return _countTokens!(text);
24
+ }
25
+
26
+ // --- Claude JSONL parser ---
27
+
28
+ type ClaudeContentBlock = {
29
+ type: string;
30
+ text?: string;
31
+ thinking?: string;
32
+ input?: unknown;
33
+ content?: unknown;
34
+ };
35
+
36
+ type ClaudeMessage = {
37
+ type: string;
38
+ message?: {
39
+ role?: string;
40
+ usage?: {
41
+ input_tokens?: number;
42
+ cache_creation_input_tokens?: number;
43
+ cache_read_input_tokens?: number;
44
+ };
45
+ content?: ClaudeContentBlock[];
46
+ };
47
+ };
48
+
49
+ export type ParsedContext = {
50
+ contextTokens: number;
51
+ breakdown: ContextBreakdown;
52
+ };
53
+
54
+ export function parseClaudeSessionJSONL(
55
+ filePath: string,
56
+ systemPromptTokens: number,
57
+ claudeMdTokens: number,
58
+ skillDescriptionTokens: number,
59
+ ): ParsedContext | null {
60
+ let data: string;
61
+ try {
62
+ data = readFileSync(filePath, "utf-8");
63
+ } catch (err: any) {
64
+ if (err?.code !== "ENOENT") console.warn(`context-breakdown: ${filePath}:`, err?.message);
65
+ return null;
66
+ }
67
+
68
+ const lines = data.split("\n").filter((l) => l.trim());
69
+ let lastContextTokens = 0;
70
+ const conv = { userText: 0, assistantText: 0, thinking: 0, toolUse: 0, toolResult: 0 };
71
+
72
+ for (const line of lines) {
73
+ let entry: ClaudeMessage;
74
+ try {
75
+ entry = JSON.parse(line);
76
+ } catch {
77
+ continue;
78
+ }
79
+
80
+ if (entry.type === "assistant" && entry.message) {
81
+ const usage = entry.message.usage;
82
+ if (usage) {
83
+ const ctx =
84
+ (usage.input_tokens ?? 0) +
85
+ (usage.cache_creation_input_tokens ?? 0) +
86
+ (usage.cache_read_input_tokens ?? 0);
87
+ if (ctx > 0) lastContextTokens = ctx;
88
+ }
89
+ for (const block of entry.message.content ?? []) {
90
+ if (block.type === "thinking" && block.thinking) {
91
+ conv.thinking += countTokens(block.thinking);
92
+ } else if (block.type === "text" && block.text) {
93
+ conv.assistantText += countTokens(block.text);
94
+ } else if (block.type === "tool_use") {
95
+ const input = block.input;
96
+ conv.toolUse += countTokens(
97
+ typeof input === "string" ? input : JSON.stringify(input ?? {}),
98
+ );
99
+ }
100
+ }
101
+ } else if (entry.type === "user" && entry.message) {
102
+ for (const block of entry.message.content ?? []) {
103
+ if (block.type === "tool_result") {
104
+ const content = block.content;
105
+ if (typeof content === "string") {
106
+ conv.toolResult += countTokens(content);
107
+ } else if (Array.isArray(content)) {
108
+ for (const c of content) {
109
+ if (c && typeof c === "object" && "text" in c && typeof c.text === "string") {
110
+ conv.toolResult += countTokens(c.text);
111
+ }
112
+ }
113
+ }
114
+ } else if (block.type === "text" && block.text) {
115
+ conv.userText += countTokens(block.text);
116
+ }
117
+ }
118
+ }
119
+ }
120
+
121
+ if (lastContextTokens === 0) return null;
122
+
123
+ return {
124
+ contextTokens: lastContextTokens,
125
+ breakdown: {
126
+ systemPrompt: systemPromptTokens,
127
+ sdkInstructions: claudeMdTokens,
128
+ skillDescriptions: skillDescriptionTokens,
129
+ conversation: conv,
130
+ },
131
+ };
132
+ }
133
+
134
+ // --- Codex JSONL parser ---
135
+
136
+ type CodexEntry = {
137
+ type: string;
138
+ payload?: {
139
+ type?: string;
140
+ info?: {
141
+ last_token_usage?: { input_tokens?: number; cached_input_tokens?: number };
142
+ total_token_usage?: { input_tokens?: number };
143
+ model_context_window?: number;
144
+ };
145
+ content?: Array<{ type?: string; text?: string }>;
146
+ role?: string;
147
+ // function_call fields
148
+ name?: string;
149
+ arguments?: string;
150
+ // function_call_output fields
151
+ output?: string;
152
+ // reasoning fields
153
+ summary?: Array<{ type?: string; text?: string }> | string;
154
+ };
155
+ };
156
+
157
+ export function parseCodexSessionJSONL(
158
+ filePath: string,
159
+ systemPromptTokens: number,
160
+ claudeMdTokens: number,
161
+ skillDescriptionTokens: number,
162
+ ): ParsedContext | null {
163
+ let data: string;
164
+ try {
165
+ data = readFileSync(filePath, "utf-8");
166
+ } catch (err: any) {
167
+ if (err?.code !== "ENOENT") console.warn(`context-breakdown: ${filePath}:`, err?.message);
168
+ return null;
169
+ }
170
+
171
+ const lines = data.split("\n").filter((l) => l.trim());
172
+ let lastContextTokens = 0;
173
+ const conv = { userText: 0, assistantText: 0, thinking: 0, toolUse: 0, toolResult: 0 };
174
+
175
+ for (const line of lines) {
176
+ let entry: CodexEntry;
177
+ try {
178
+ entry = JSON.parse(line);
179
+ } catch {
180
+ continue;
181
+ }
182
+
183
+ const payload = entry.payload;
184
+ if (!payload) continue;
185
+
186
+ if (entry.type === "event_msg" && payload.type === "token_count" && payload.info) {
187
+ const lastUsage = payload.info.last_token_usage;
188
+ if (lastUsage?.input_tokens) {
189
+ lastContextTokens = lastUsage.input_tokens;
190
+ }
191
+ } else if (entry.type === "response_item") {
192
+ if (payload.type === "reasoning") {
193
+ // Reasoning summaries are in the `summary` field (array of {text} objects)
194
+ const summary = payload.summary;
195
+ if (Array.isArray(summary)) {
196
+ for (const s of summary) {
197
+ if (s && typeof s === "object" && s.text) conv.thinking += countTokens(s.text);
198
+ }
199
+ } else if (typeof summary === "string" && summary) {
200
+ conv.thinking += countTokens(summary);
201
+ }
202
+ } else if (payload.type === "message") {
203
+ const text = payload.content?.map((c) => c.text ?? "").join("") ?? "";
204
+ if (text) {
205
+ if (payload.role === "assistant") {
206
+ conv.assistantText += countTokens(text);
207
+ } else {
208
+ conv.userText += countTokens(text);
209
+ }
210
+ }
211
+ } else if (payload.type === "function_call") {
212
+ const args = payload.arguments ?? "";
213
+ if (args) conv.toolUse += countTokens(args);
214
+ } else if (payload.type === "function_call_output") {
215
+ const output = payload.output ?? "";
216
+ if (output) conv.toolResult += countTokens(output);
217
+ }
218
+ }
219
+ }
220
+
221
+ if (lastContextTokens === 0) return null;
222
+
223
+ return {
224
+ contextTokens: lastContextTokens,
225
+ breakdown: {
226
+ systemPrompt: systemPromptTokens,
227
+ sdkInstructions: claudeMdTokens,
228
+ skillDescriptions: skillDescriptionTokens,
229
+ conversation: conv,
230
+ },
231
+ };
232
+ }
233
+
234
+ // --- Pi JSONL parser ---
235
+
236
+ type PiEntry = {
237
+ type: string;
238
+ // message entries
239
+ message?: {
240
+ role?: string;
241
+ usage?: { input?: number; cacheRead?: number; cacheWrite?: number };
242
+ content?: Array<{ type?: string; text?: string; thinking?: string; arguments?: unknown }>;
243
+ };
244
+ // custom_message entries (hook-injected context: reply-instructions, startup-context, etc.)
245
+ content?: string;
246
+ };
247
+
248
+ export function parsePiSessionJSONL(
249
+ filePath: string,
250
+ systemPromptTokens: number,
251
+ claudeMdTokens: number,
252
+ skillDescriptionTokens: number,
253
+ ): ParsedContext | null {
254
+ let data: string;
255
+ try {
256
+ data = readFileSync(filePath, "utf-8");
257
+ } catch (err: any) {
258
+ if (err?.code !== "ENOENT") console.warn(`context-breakdown: ${filePath}:`, err?.message);
259
+ return null;
260
+ }
261
+
262
+ const lines = data.split("\n").filter((l) => l.trim());
263
+ let lastContextTokens = 0;
264
+ const conv = { userText: 0, assistantText: 0, thinking: 0, toolUse: 0, toolResult: 0 };
265
+
266
+ for (const line of lines) {
267
+ let entry: PiEntry;
268
+ try {
269
+ entry = JSON.parse(line);
270
+ } catch {
271
+ continue;
272
+ }
273
+
274
+ // custom_message entries are hook-injected context (reply-instructions, startup-context)
275
+ if (entry.type === "custom_message" && entry.content) {
276
+ conv.userText += countTokens(entry.content);
277
+ continue;
278
+ }
279
+
280
+ if (entry.type !== "message" || !entry.message) continue;
281
+ const msg = entry.message;
282
+
283
+ if (msg.role === "assistant") {
284
+ const usage = msg.usage;
285
+ if (usage) {
286
+ const ctx = (usage.input ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
287
+ if (ctx > 0) lastContextTokens = ctx;
288
+ }
289
+ for (const block of msg.content ?? []) {
290
+ if (block.type === "thinking" && block.thinking) {
291
+ conv.thinking += countTokens(block.thinking);
292
+ } else if (block.type === "text" && block.text) {
293
+ conv.assistantText += countTokens(block.text);
294
+ } else if (block.type === "toolCall") {
295
+ const args = block.arguments;
296
+ conv.toolUse += countTokens(typeof args === "string" ? args : JSON.stringify(args ?? {}));
297
+ }
298
+ }
299
+ } else if (msg.role === "toolResult") {
300
+ for (const block of msg.content ?? []) {
301
+ if (block.text) conv.toolResult += countTokens(block.text);
302
+ }
303
+ } else if (msg.role === "user") {
304
+ for (const block of msg.content ?? []) {
305
+ if (block.type === "text" && block.text) {
306
+ conv.userText += countTokens(block.text);
307
+ }
308
+ }
309
+ }
310
+ }
311
+
312
+ if (lastContextTokens === 0) return null;
313
+
314
+ return {
315
+ contextTokens: lastContextTokens,
316
+ breakdown: {
317
+ systemPrompt: systemPromptTokens,
318
+ sdkInstructions: claudeMdTokens,
319
+ skillDescriptions: skillDescriptionTokens,
320
+ conversation: conv,
321
+ },
322
+ };
323
+ }
324
+
325
+ // --- Measure known SDK overhead ---
326
+
327
+ /** Count tokens in the actual composed system prompt string. */
328
+ export function countSystemPromptTokens(systemPrompt: string): number {
329
+ return countTokens(systemPrompt);
330
+ }
331
+
332
+ /** Count tokens in the SDK instruction file (CLAUDE.md, MINDS.md, or AGENTS.md). */
333
+ export function countSdkInstructionTokens(cwd: string): number {
334
+ for (const name of ["CLAUDE.md", "MINDS.md", "AGENTS.md"]) {
335
+ try {
336
+ const content = readFileSync(resolve(cwd, name), "utf-8");
337
+ return countTokens(content);
338
+ } catch (err: any) {
339
+ if (err?.code !== "ENOENT")
340
+ console.warn(`context-breakdown: ${resolve(cwd, name)}:`, err?.message);
341
+ }
342
+ }
343
+ return 0;
344
+ }
345
+
346
+ /** Count tokens in skill description frontmatter (always in context). */
347
+ export function countSkillDescriptionTokens(skillsDirs: string[]): number {
348
+ let total = 0;
349
+ for (const dir of skillsDirs) {
350
+ try {
351
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
352
+ if (!entry.isDirectory()) continue;
353
+ try {
354
+ const content = readFileSync(resolve(dir, entry.name, "SKILL.md"), "utf-8");
355
+ // Extract just the frontmatter description — that's what's always in context
356
+ const match = content.match(/^description:\s*(.+?)$/m);
357
+ if (match) total += countTokens(match[1]);
358
+ } catch (err: any) {
359
+ if (err?.code !== "ENOENT")
360
+ console.warn(`context-breakdown: SKILL.md in ${entry.name}:`, err?.message);
361
+ }
362
+ }
363
+ } catch (err: any) {
364
+ if (err?.code !== "ENOENT")
365
+ console.warn(`context-breakdown: skills dir ${dir}:`, err?.message);
366
+ }
367
+ }
368
+ return total;
369
+ }
370
+
371
+ // --- Session file finders ---
372
+
373
+ /** Find the Claude SDK JSONL file for a session ID. */
374
+ export function findClaudeSessionFile(cwd: string, sessionId: string): string | null {
375
+ // The SDK stores JSONL in ~/.claude/projects/ (global), not inside the mind's home dir.
376
+ // Check both the global location and the local one (in case of sandboxed minds).
377
+ const homeDir = process.env.HOME ?? "";
378
+ const dirs = [resolve(homeDir, ".claude/projects"), resolve(cwd, ".claude/projects")];
379
+ for (const projectsDir of dirs) {
380
+ try {
381
+ for (const dir of readdirSync(projectsDir)) {
382
+ const candidate = resolve(projectsDir, dir, `${sessionId}.jsonl`);
383
+ try {
384
+ statSync(candidate);
385
+ return candidate;
386
+ } catch {
387
+ // Not in this project dir
388
+ }
389
+ }
390
+ } catch (err: any) {
391
+ if (err?.code !== "ENOENT") console.warn(`context-breakdown: ${projectsDir}:`, err?.message);
392
+ }
393
+ }
394
+ return null;
395
+ }
396
+
397
+ /** Find the Codex JSONL file for a thread ID. */
398
+ export function findCodexSessionFile(threadId: string): string | null {
399
+ const codexDir = resolve(process.env.HOME ?? "", ".codex/sessions");
400
+ try {
401
+ for (const year of readdirSync(codexDir)) {
402
+ const yearDir = resolve(codexDir, year);
403
+ try {
404
+ if (!statSync(yearDir).isDirectory()) continue;
405
+ } catch {
406
+ continue;
407
+ }
408
+ for (const month of readdirSync(yearDir)) {
409
+ const monthDir = resolve(yearDir, month);
410
+ try {
411
+ if (!statSync(monthDir).isDirectory()) continue;
412
+ } catch {
413
+ continue;
414
+ }
415
+ for (const day of readdirSync(monthDir)) {
416
+ const dayDir = resolve(monthDir, day);
417
+ try {
418
+ if (!statSync(dayDir).isDirectory()) continue;
419
+ } catch {
420
+ continue;
421
+ }
422
+ for (const file of readdirSync(dayDir)) {
423
+ if (file.endsWith(".jsonl") && file.includes(threadId)) {
424
+ return resolve(dayDir, file);
425
+ }
426
+ }
427
+ }
428
+ }
429
+ }
430
+ } catch (err: any) {
431
+ if (err?.code !== "ENOENT") console.warn("context-breakdown: codex sessions:", err?.message);
432
+ }
433
+ return null;
434
+ }
435
+
436
+ /** Find the latest Pi JSONL file for a session name. */
437
+ export function findPiSessionFile(sessionsDir: string, sessionName: string): string | null {
438
+ const dir = resolve(sessionsDir, sessionName);
439
+ try {
440
+ const files = readdirSync(dir)
441
+ .filter((f) => f.endsWith(".jsonl"))
442
+ .sort();
443
+ if (files.length === 0) return null;
444
+ return resolve(dir, files[files.length - 1]);
445
+ } catch (err: any) {
446
+ if (err?.code !== "ENOENT")
447
+ console.warn(`context-breakdown: pi sessions ${dir}:`, err?.message);
448
+ return null;
449
+ }
450
+ }
@@ -1,5 +1,22 @@
1
1
  import type { ChannelMeta, ParticipantProfile } from "./types.js";
2
2
 
3
+ /** Compact timestamp: YYYY-MM-DD HH:MM */
4
+ export function compactTimestamp(date: Date = new Date()): string {
5
+ const y = date.getFullYear();
6
+ const m = String(date.getMonth() + 1).padStart(2, "0");
7
+ const d = String(date.getDate()).padStart(2, "0");
8
+ const h = String(date.getHours()).padStart(2, "0");
9
+ const min = String(date.getMinutes()).padStart(2, "0");
10
+ return `${y}-${m}-${d} ${h}:${min}`;
11
+ }
12
+
13
+ /** Compact time-only: HH:MM */
14
+ export function compactTime(date: Date = new Date()): string {
15
+ const h = String(date.getHours()).padStart(2, "0");
16
+ const min = String(date.getMinutes()).padStart(2, "0");
17
+ return `${h}:${min}`;
18
+ }
19
+
3
20
  function derivePlatform(channel: string): string {
4
21
  if (!channel.includes(":")) return "Volute";
5
22
  const name = channel.split(":")[0];
@@ -55,13 +55,14 @@ export function executeHook(
55
55
  scriptPath: string,
56
56
  input: object,
57
57
  timeout = DEFAULT_TIMEOUT,
58
+ cwd?: string,
58
59
  ): Promise<HookResult> {
59
60
  return new Promise((resolve) => {
60
61
  const { cmd, args } = getRunner(scriptPath);
61
62
  const child = spawn(cmd, args, {
62
63
  timeout,
63
64
  stdio: ["pipe", "pipe", "pipe"],
64
- cwd: process.cwd(),
65
+ cwd: cwd ?? process.cwd(),
65
66
  env: process.env,
66
67
  });
67
68
 
@@ -130,12 +131,17 @@ export async function runHooks(
130
131
  const scripts = discoverHooks(hooksDir, event);
131
132
  if (scripts.length === 0) return { metadata: {}, blocked: false };
132
133
 
134
+ // Hook shim scripts use paths relative to the mind's home directory
135
+ // (e.g. .claude/skills/<id>/scripts/<script>). hooksDir is <home>/.local/hooks,
136
+ // so resolving ../.. gives the home directory.
137
+ const homeDir = resolve(hooksDir, "../..");
138
+
133
139
  const contextParts: string[] = [];
134
140
  const metadata: Record<string, unknown> = {};
135
141
  let blocked = false;
136
142
 
137
143
  for (const script of scripts) {
138
- const result = await executeHook(script, input, timeout);
144
+ const result = await executeHook(script, input, timeout, homeDir);
139
145
  if (result.additionalContext) {
140
146
  contextParts.push(result.additionalContext);
141
147
  }