@ai-studio-3d/vyasa 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/README.md +69 -0
  2. package/data/common-kit.json +34 -0
  3. package/data/pricing.json +119 -0
  4. package/data/schema/AnalyticsData.json +214 -0
  5. package/data/schema/FoundationData.json +42 -0
  6. package/data/schema/KitData.json +321 -0
  7. package/data/schema/SearchData.json +173 -0
  8. package/data/schema/SessionDetailData.json +231 -0
  9. package/data/schema/SessionsListData.json +115 -0
  10. package/data/schema/WasteData.json +95 -0
  11. package/dist/cli.d.ts +3 -0
  12. package/dist/cli.d.ts.map +1 -0
  13. package/dist/cli.js +343 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/core/billing/index.d.ts +4 -0
  16. package/dist/core/billing/index.d.ts.map +1 -0
  17. package/dist/core/billing/index.js +43 -0
  18. package/dist/core/billing/index.js.map +1 -0
  19. package/dist/core/budget/index.d.ts +30 -0
  20. package/dist/core/budget/index.d.ts.map +1 -0
  21. package/dist/core/budget/index.js +61 -0
  22. package/dist/core/budget/index.js.map +1 -0
  23. package/dist/core/codex/index.d.ts +3 -0
  24. package/dist/core/codex/index.d.ts.map +1 -0
  25. package/dist/core/codex/index.js +212 -0
  26. package/dist/core/codex/index.js.map +1 -0
  27. package/dist/core/cursor/index.d.ts +3 -0
  28. package/dist/core/cursor/index.d.ts.map +1 -0
  29. package/dist/core/cursor/index.js +304 -0
  30. package/dist/core/cursor/index.js.map +1 -0
  31. package/dist/core/cursor/scan.d.ts +28 -0
  32. package/dist/core/cursor/scan.d.ts.map +1 -0
  33. package/dist/core/cursor/scan.js +79 -0
  34. package/dist/core/cursor/scan.js.map +1 -0
  35. package/dist/core/decisions/index.d.ts +21 -0
  36. package/dist/core/decisions/index.d.ts.map +1 -0
  37. package/dist/core/decisions/index.js +133 -0
  38. package/dist/core/decisions/index.js.map +1 -0
  39. package/dist/core/dlp/index.d.ts +7 -0
  40. package/dist/core/dlp/index.d.ts.map +1 -0
  41. package/dist/core/dlp/index.js +115 -0
  42. package/dist/core/dlp/index.js.map +1 -0
  43. package/dist/core/health/index.d.ts +10 -0
  44. package/dist/core/health/index.d.ts.map +1 -0
  45. package/dist/core/health/index.js +21 -0
  46. package/dist/core/health/index.js.map +1 -0
  47. package/dist/core/index/index.d.ts +9 -0
  48. package/dist/core/index/index.d.ts.map +1 -0
  49. package/dist/core/index/index.js +103 -0
  50. package/dist/core/index/index.js.map +1 -0
  51. package/dist/core/index/ingest.d.ts +16 -0
  52. package/dist/core/index/ingest.d.ts.map +1 -0
  53. package/dist/core/index/ingest.js +184 -0
  54. package/dist/core/index/ingest.js.map +1 -0
  55. package/dist/core/index/platforms.d.ts +21 -0
  56. package/dist/core/index/platforms.d.ts.map +1 -0
  57. package/dist/core/index/platforms.js +44 -0
  58. package/dist/core/index/platforms.js.map +1 -0
  59. package/dist/core/index/pricing-map.d.ts +10 -0
  60. package/dist/core/index/pricing-map.d.ts.map +1 -0
  61. package/dist/core/index/pricing-map.js +16 -0
  62. package/dist/core/index/pricing-map.js.map +1 -0
  63. package/dist/core/index/progress.d.ts +26 -0
  64. package/dist/core/index/progress.d.ts.map +1 -0
  65. package/dist/core/index/progress.js +59 -0
  66. package/dist/core/index/progress.js.map +1 -0
  67. package/dist/core/index/scan.d.ts +9 -0
  68. package/dist/core/index/scan.d.ts.map +1 -0
  69. package/dist/core/index/scan.js +42 -0
  70. package/dist/core/index/scan.js.map +1 -0
  71. package/dist/core/index/statements.d.ts +18 -0
  72. package/dist/core/index/statements.d.ts.map +1 -0
  73. package/dist/core/index/statements.js +82 -0
  74. package/dist/core/index/statements.js.map +1 -0
  75. package/dist/core/index/tail.d.ts +42 -0
  76. package/dist/core/index/tail.d.ts.map +1 -0
  77. package/dist/core/index/tail.js +123 -0
  78. package/dist/core/index/tail.js.map +1 -0
  79. package/dist/core/jsonl/index.d.ts +3 -0
  80. package/dist/core/jsonl/index.d.ts.map +1 -0
  81. package/dist/core/jsonl/index.js +106 -0
  82. package/dist/core/jsonl/index.js.map +1 -0
  83. package/dist/core/jsonl-validate/index.d.ts +10 -0
  84. package/dist/core/jsonl-validate/index.d.ts.map +1 -0
  85. package/dist/core/jsonl-validate/index.js +40 -0
  86. package/dist/core/jsonl-validate/index.js.map +1 -0
  87. package/dist/core/kit/detection.d.ts +94 -0
  88. package/dist/core/kit/detection.d.ts.map +1 -0
  89. package/dist/core/kit/detection.js +312 -0
  90. package/dist/core/kit/detection.js.map +1 -0
  91. package/dist/core/kit/index.d.ts +12 -0
  92. package/dist/core/kit/index.d.ts.map +1 -0
  93. package/dist/core/kit/index.js +131 -0
  94. package/dist/core/kit/index.js.map +1 -0
  95. package/dist/core/kit/scan.d.ts +28 -0
  96. package/dist/core/kit/scan.d.ts.map +1 -0
  97. package/dist/core/kit/scan.js +432 -0
  98. package/dist/core/kit/scan.js.map +1 -0
  99. package/dist/core/recap/index.d.ts +18 -0
  100. package/dist/core/recap/index.d.ts.map +1 -0
  101. package/dist/core/recap/index.js +88 -0
  102. package/dist/core/recap/index.js.map +1 -0
  103. package/dist/core/repetition/index.d.ts +5 -0
  104. package/dist/core/repetition/index.d.ts.map +1 -0
  105. package/dist/core/repetition/index.js +0 -0
  106. package/dist/core/repetition/index.js.map +1 -0
  107. package/dist/core/schema/index.d.ts +514 -0
  108. package/dist/core/schema/index.d.ts.map +1 -0
  109. package/dist/core/schema/index.js +2 -0
  110. package/dist/core/schema/index.js.map +1 -0
  111. package/dist/core/schema-watch/index.d.ts +34 -0
  112. package/dist/core/schema-watch/index.d.ts.map +1 -0
  113. package/dist/core/schema-watch/index.js +87 -0
  114. package/dist/core/schema-watch/index.js.map +1 -0
  115. package/dist/core/search/index.d.ts +44 -0
  116. package/dist/core/search/index.d.ts.map +1 -0
  117. package/dist/core/search/index.js +641 -0
  118. package/dist/core/search/index.js.map +1 -0
  119. package/dist/core/settings/index.d.ts +24 -0
  120. package/dist/core/settings/index.d.ts.map +1 -0
  121. package/dist/core/settings/index.js +76 -0
  122. package/dist/core/settings/index.js.map +1 -0
  123. package/dist/core/share/bundle.d.ts +40 -0
  124. package/dist/core/share/bundle.d.ts.map +1 -0
  125. package/dist/core/share/bundle.js +158 -0
  126. package/dist/core/share/bundle.js.map +1 -0
  127. package/dist/core/share/canonical/from-claude.d.ts +13 -0
  128. package/dist/core/share/canonical/from-claude.d.ts.map +1 -0
  129. package/dist/core/share/canonical/from-claude.js +129 -0
  130. package/dist/core/share/canonical/from-claude.js.map +1 -0
  131. package/dist/core/share/canonical/from-codex.d.ts +18 -0
  132. package/dist/core/share/canonical/from-codex.d.ts.map +1 -0
  133. package/dist/core/share/canonical/from-codex.js +183 -0
  134. package/dist/core/share/canonical/from-codex.js.map +1 -0
  135. package/dist/core/share/canonical/to-claude.d.ts +15 -0
  136. package/dist/core/share/canonical/to-claude.d.ts.map +1 -0
  137. package/dist/core/share/canonical/to-claude.js +146 -0
  138. package/dist/core/share/canonical/to-claude.js.map +1 -0
  139. package/dist/core/share/canonical/to-codex.d.ts +21 -0
  140. package/dist/core/share/canonical/to-codex.d.ts.map +1 -0
  141. package/dist/core/share/canonical/to-codex.js +124 -0
  142. package/dist/core/share/canonical/to-codex.js.map +1 -0
  143. package/dist/core/share/canonical/tool-map.d.ts +61 -0
  144. package/dist/core/share/canonical/tool-map.d.ts.map +1 -0
  145. package/dist/core/share/canonical/tool-map.js +299 -0
  146. package/dist/core/share/canonical/tool-map.js.map +1 -0
  147. package/dist/core/share/canonical/types.d.ts +57 -0
  148. package/dist/core/share/canonical/types.d.ts.map +1 -0
  149. package/dist/core/share/canonical/types.js +9 -0
  150. package/dist/core/share/canonical/types.js.map +1 -0
  151. package/dist/core/share/import.d.ts +28 -0
  152. package/dist/core/share/import.d.ts.map +1 -0
  153. package/dist/core/share/import.js +174 -0
  154. package/dist/core/share/import.js.map +1 -0
  155. package/dist/core/share/manifest.d.ts +37 -0
  156. package/dist/core/share/manifest.d.ts.map +1 -0
  157. package/dist/core/share/manifest.js +31 -0
  158. package/dist/core/share/manifest.js.map +1 -0
  159. package/dist/core/share/preview.d.ts +4 -0
  160. package/dist/core/share/preview.d.ts.map +1 -0
  161. package/dist/core/share/preview.js +153 -0
  162. package/dist/core/share/preview.js.map +1 -0
  163. package/dist/core/share/primer.d.ts +19 -0
  164. package/dist/core/share/primer.d.ts.map +1 -0
  165. package/dist/core/share/primer.js +196 -0
  166. package/dist/core/share/primer.js.map +1 -0
  167. package/dist/core/share/resume.d.ts +10 -0
  168. package/dist/core/share/resume.d.ts.map +1 -0
  169. package/dist/core/share/resume.js +58 -0
  170. package/dist/core/share/resume.js.map +1 -0
  171. package/dist/core/share/scrub.d.ts +10 -0
  172. package/dist/core/share/scrub.d.ts.map +1 -0
  173. package/dist/core/share/scrub.js +198 -0
  174. package/dist/core/share/scrub.js.map +1 -0
  175. package/dist/core/share/tar.d.ts +12 -0
  176. package/dist/core/share/tar.d.ts.map +1 -0
  177. package/dist/core/share/tar.js +78 -0
  178. package/dist/core/share/tar.js.map +1 -0
  179. package/dist/core/tags/index.d.ts +48 -0
  180. package/dist/core/tags/index.d.ts.map +1 -0
  181. package/dist/core/tags/index.js +113 -0
  182. package/dist/core/tags/index.js.map +1 -0
  183. package/dist/core/today/index.d.ts +25 -0
  184. package/dist/core/today/index.d.ts.map +1 -0
  185. package/dist/core/today/index.js +42 -0
  186. package/dist/core/today/index.js.map +1 -0
  187. package/dist/core/waste/abandoned.d.ts +12 -0
  188. package/dist/core/waste/abandoned.d.ts.map +1 -0
  189. package/dist/core/waste/abandoned.js +127 -0
  190. package/dist/core/waste/abandoned.js.map +1 -0
  191. package/dist/core/waste/cache-miss.d.ts +9 -0
  192. package/dist/core/waste/cache-miss.d.ts.map +1 -0
  193. package/dist/core/waste/cache-miss.js +84 -0
  194. package/dist/core/waste/cache-miss.js.map +1 -0
  195. package/dist/core/waste/index.d.ts +12 -0
  196. package/dist/core/waste/index.d.ts.map +1 -0
  197. package/dist/core/waste/index.js +45 -0
  198. package/dist/core/waste/index.js.map +1 -0
  199. package/dist/core/waste/wrong-model.d.ts +11 -0
  200. package/dist/core/waste/wrong-model.d.ts.map +1 -0
  201. package/dist/core/waste/wrong-model.js +104 -0
  202. package/dist/core/waste/wrong-model.js.map +1 -0
  203. package/dist/db/index.d.ts +10 -0
  204. package/dist/db/index.d.ts.map +1 -0
  205. package/dist/db/index.js +29 -0
  206. package/dist/db/index.js.map +1 -0
  207. package/dist/db/migrate.d.ts +8 -0
  208. package/dist/db/migrate.d.ts.map +1 -0
  209. package/dist/db/migrate.js +282 -0
  210. package/dist/db/migrate.js.map +1 -0
  211. package/dist/server/events.d.ts +4 -0
  212. package/dist/server/events.d.ts.map +1 -0
  213. package/dist/server/events.js +54 -0
  214. package/dist/server/events.js.map +1 -0
  215. package/dist/server/index.d.ts +7 -0
  216. package/dist/server/index.d.ts.map +1 -0
  217. package/dist/server/index.js +238 -0
  218. package/dist/server/index.js.map +1 -0
  219. package/dist/server/routes/foundation.d.ts +2 -0
  220. package/dist/server/routes/foundation.d.ts.map +1 -0
  221. package/dist/server/routes/foundation.js +2 -0
  222. package/dist/server/routes/foundation.js.map +1 -0
  223. package/dist/server/routes/search.d.ts +2 -0
  224. package/dist/server/routes/search.d.ts.map +1 -0
  225. package/dist/server/routes/search.js +2 -0
  226. package/dist/server/routes/search.js.map +1 -0
  227. package/dist/server/routes/sessions.d.ts +2 -0
  228. package/dist/server/routes/sessions.d.ts.map +1 -0
  229. package/dist/server/routes/sessions.js +2 -0
  230. package/dist/server/routes/sessions.js.map +1 -0
  231. package/dist/server/routes/waste.d.ts +2 -0
  232. package/dist/server/routes/waste.d.ts.map +1 -0
  233. package/dist/server/routes/waste.js +2 -0
  234. package/dist/server/routes/waste.js.map +1 -0
  235. package/dist/web/assets/index-Ba1VvTj0.js +37 -0
  236. package/dist/web/index.html +12 -0
  237. package/package.json +76 -0
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Sub-case B only fires for short-ish sessions. A long session that merely ends on
3
+ * a tool_use almost always did real work — attributing its full cost as "abandoned"
4
+ * is not credible (it produced $1,499 false positives on multi-day work sessions).
5
+ */
6
+ const ABANDON_MAX_MS = 10 * 60 * 1000;
7
+ /** Drop sub-half-cent attributions so the UI never shows "$0.00" rows (e.g. zero-cost Cursor sessions). */
8
+ const MIN_CENTS = 0.5;
9
+ function hasToolUse(contentJson) {
10
+ try {
11
+ const parsed = JSON.parse(contentJson);
12
+ if (!parsed || typeof parsed !== 'object')
13
+ return false;
14
+ if (!Array.isArray(parsed.content))
15
+ return false;
16
+ return parsed.content.some((b) => b.type === 'tool_use');
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ }
22
+ /**
23
+ * Detects two sub-cases of abandoned sessions:
24
+ * A) Short sessions (≤ 60s) with no resumption in the same project within 24h.
25
+ * B) Sessions where the last assistant message has a pending tool_use but no
26
+ * subsequent user message with a tool_result.
27
+ *
28
+ * Sub-case B takes precedence over A if both match.
29
+ */
30
+ export function detectAbandoned(db) {
31
+ // --- Sub-case A: short sessions with no resumption ---
32
+ const shortSessions = db
33
+ .prepare(`
34
+ SELECT id, project, started_at, ended_at, total_cost_cents
35
+ FROM sessions
36
+ WHERE started_at IS NOT NULL AND ended_at IS NOT NULL
37
+ `)
38
+ .all()
39
+ .filter((s) => new Date(s.ended_at).getTime() - new Date(s.started_at).getTime() <= 60_000);
40
+ // Load all sessions by project once (no N+1 resumption queries)
41
+ const projectSessionsMap = new Map();
42
+ for (const row of db
43
+ .prepare('SELECT id, project, started_at FROM sessions WHERE started_at IS NOT NULL')
44
+ .all()) {
45
+ const list = projectSessionsMap.get(row.project) ?? [];
46
+ list.push(row);
47
+ projectSessionsMap.set(row.project, list);
48
+ }
49
+ const resultA = new Map();
50
+ for (const s of shortSessions) {
51
+ const endedAtMs = new Date(s.ended_at).getTime();
52
+ const plus24hMs = endedAtMs + 24 * 60 * 60 * 1000;
53
+ const projectSessions = projectSessionsMap.get(s.project) ?? [];
54
+ const hasResumption = projectSessions.some((other) => {
55
+ if (other.id === s.id)
56
+ return false;
57
+ const otherStart = new Date(other.started_at).getTime();
58
+ return otherStart > endedAtMs && otherStart < plus24hMs;
59
+ });
60
+ if (hasResumption)
61
+ continue;
62
+ if (s.total_cost_cents < MIN_CENTS)
63
+ continue; // skip zero/near-zero cost (e.g. Cursor) noise
64
+ const durationMs = new Date(s.ended_at).getTime() - new Date(s.started_at).getTime();
65
+ resultA.set(s.id, {
66
+ sessionId: s.id,
67
+ category: 'abandoned',
68
+ amountCents: s.total_cost_cents,
69
+ anchorMsgIdx: null,
70
+ evidenceJson: JSON.stringify({ reason: 'short-no-resumption', durationMs }),
71
+ });
72
+ }
73
+ // --- Sub-case B: mid-tool-call exit — single batch query, no N+1 ---
74
+ const lastAsstRows = db
75
+ .prepare(`
76
+ SELECT s.id AS session_id, s.project, s.started_at, s.ended_at, s.total_cost_cents,
77
+ la.content_json,
78
+ la.idx AS last_asst_idx,
79
+ (SELECT COUNT(*) FROM messages fu
80
+ WHERE fu.session_id = s.id AND fu.role = 'user' AND fu.idx > la.idx) AS following_user_count
81
+ FROM sessions s
82
+ INNER JOIN messages la ON la.id = (
83
+ SELECT id FROM messages
84
+ WHERE session_id = s.id AND role = 'assistant'
85
+ ORDER BY idx DESC LIMIT 1
86
+ )
87
+ `)
88
+ .all();
89
+ const resultB = new Map();
90
+ for (const row of lastAsstRows) {
91
+ if (!hasToolUse(row.content_json))
92
+ continue;
93
+ if (row.following_user_count > 0)
94
+ continue;
95
+ if (row.total_cost_cents < MIN_CENTS)
96
+ continue; // skip zero/near-zero cost noise
97
+ if (!row.started_at || !row.ended_at)
98
+ continue;
99
+ // Bound to short sessions: a long session ending on a tool_use did real work.
100
+ const durationMs = new Date(row.ended_at).getTime() - new Date(row.started_at).getTime();
101
+ if (durationMs > ABANDON_MAX_MS)
102
+ continue;
103
+ // ...and only if never resumed in the same project within 24h (reuse the A map).
104
+ const endedAtMs = new Date(row.ended_at).getTime();
105
+ const plus24hMs = endedAtMs + 24 * 60 * 60 * 1000;
106
+ const projectSessions = projectSessionsMap.get(row.project) ?? [];
107
+ const resumed = projectSessions.some((other) => {
108
+ if (other.id === row.session_id)
109
+ return false;
110
+ const otherStart = new Date(other.started_at).getTime();
111
+ return otherStart > endedAtMs && otherStart < plus24hMs;
112
+ });
113
+ if (resumed)
114
+ continue;
115
+ resultB.set(row.session_id, {
116
+ sessionId: row.session_id,
117
+ category: 'abandoned',
118
+ amountCents: row.total_cost_cents,
119
+ anchorMsgIdx: row.last_asst_idx,
120
+ evidenceJson: JSON.stringify({ reason: 'mid-tool-call-exit', durationMs }),
121
+ });
122
+ }
123
+ // Merge: B overrides A for the same session
124
+ const merged = new Map([...resultA, ...resultB]);
125
+ return Array.from(merged.values());
126
+ }
127
+ //# sourceMappingURL=abandoned.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"abandoned.js","sourceRoot":"","sources":["../../../src/core/waste/abandoned.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import Database from 'better-sqlite3';
2
+ import type { WasteAttribution } from '../schema/index.js';
3
+ /**
4
+ * Detects sessions whose cache utilisation is below an absolute best-practice
5
+ * target (80%) and attributes the delta cost as waste. The user's own p75 is
6
+ * retained in evidence as "your norm" for context, but is no longer the baseline.
7
+ */
8
+ export declare function detectCacheMiss(db: Database.Database): WasteAttribution[];
9
+ //# sourceMappingURL=cache-miss.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-miss.d.ts","sourceRoot":"","sources":["../../../src/core/waste/cache-miss.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAA;AACrC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AA4B1D;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,GAAG,gBAAgB,EAAE,CAmFzE"}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Absolute best-practice cache-hit target. Anchoring to a fixed target (not the
3
+ * user's own p75) is what makes the number credible: an efficient user at 99% sees
4
+ * ~zero waste (honest), an inefficient user at 40% sees real, actionable waste —
5
+ * instead of the old p75 baseline that adapted to your own habits and netted to ~0
6
+ * (and produced the "cache hit 100% vs 100% baseline → $0.00" noise rows).
7
+ */
8
+ const TARGET_CACHE_HIT = 0.8;
9
+ /** Drop sub-half-cent attributions so the UI never shows "$0.00" rows. */
10
+ const MIN_CENTS = 0.5;
11
+ /**
12
+ * Detects sessions whose cache utilisation is below an absolute best-practice
13
+ * target (80%) and attributes the delta cost as waste. The user's own p75 is
14
+ * retained in evidence as "your norm" for context, but is no longer the baseline.
15
+ */
16
+ export function detectCacheMiss(db) {
17
+ // Load all sessions with dominant model — single correlated subquery, no N+1
18
+ const sessions = db
19
+ .prepare(`
20
+ SELECT s.id, s.started_at, s.total_input_tokens, s.total_cache_read,
21
+ (SELECT m.model FROM messages m
22
+ WHERE m.session_id = s.id AND m.model IS NOT NULL
23
+ GROUP BY m.model ORDER BY COUNT(*) DESC LIMIT 1) AS primary_model
24
+ FROM sessions s
25
+ -- Cursor sessions excluded — Cursor's API layer hides prompt-cache signals these detectors need.
26
+ WHERE s.platform = 'claude-code'
27
+ `)
28
+ .all();
29
+ // Load all pricing into a Map once (later rows overwrite earlier → most recent per model)
30
+ const pricingMap = new Map();
31
+ for (const row of db
32
+ .prepare('SELECT model, input_per_mtok, cache_read_per_mtok FROM pricing ORDER BY period_start ASC')
33
+ .all()) {
34
+ pricingMap.set(row.model, row);
35
+ }
36
+ const now = Date.now();
37
+ const thirtyDaysAgo = now - 30 * 24 * 60 * 60 * 1000;
38
+ const withRates = [];
39
+ for (const s of sessions) {
40
+ const totalTokens = s.total_input_tokens + s.total_cache_read;
41
+ if (totalTokens === 0)
42
+ continue;
43
+ const cacheHitRate = s.total_cache_read / totalTokens;
44
+ const startedAt = s.started_at ? new Date(s.started_at).getTime() : 0;
45
+ withRates.push({ id: s.id, cacheHitRate, startedAt, totalTokens, primaryModel: s.primary_model });
46
+ }
47
+ // p75 of recent sessions — kept only as context ("vs your norm"), not the threshold.
48
+ const recent = withRates.filter((s) => s.startedAt >= thirtyDaysAgo);
49
+ let yourNorm = null;
50
+ if (recent.length >= 7) {
51
+ const sorted = [...recent].sort((a, b) => a.cacheHitRate - b.cacheHitRate);
52
+ yourNorm = sorted[Math.floor((sorted.length - 1) * 0.75)].cacheHitRate;
53
+ }
54
+ const attributions = [];
55
+ for (const s of withRates) {
56
+ // Flag against the absolute best-practice target, not the user's own habits.
57
+ if (s.cacheHitRate >= TARGET_CACHE_HIT)
58
+ continue;
59
+ if (!s.primaryModel)
60
+ continue;
61
+ const pricing = pricingMap.get(s.primaryModel);
62
+ if (!pricing)
63
+ continue;
64
+ const tokensUnderutilised = (TARGET_CACHE_HIT - s.cacheHitRate) * s.totalTokens;
65
+ const wastePerToken = ((pricing.input_per_mtok - pricing.cache_read_per_mtok) / 1_000_000) * 100;
66
+ const amountCents = tokensUnderutilised * wastePerToken;
67
+ if (amountCents < MIN_CENTS)
68
+ continue;
69
+ attributions.push({
70
+ sessionId: s.id,
71
+ category: 'cache-miss',
72
+ amountCents,
73
+ anchorMsgIdx: null,
74
+ evidenceJson: JSON.stringify({
75
+ cacheHitRate: s.cacheHitRate,
76
+ target: TARGET_CACHE_HIT,
77
+ yourNorm,
78
+ tokensUnderutilised,
79
+ }),
80
+ });
81
+ }
82
+ return attributions;
83
+ }
84
+ //# sourceMappingURL=cache-miss.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-miss.js","sourceRoot":"","sources":["../../../src/core/waste/cache-miss.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import Database from 'better-sqlite3';
2
+ export { detectCacheMiss } from './cache-miss.js';
3
+ export { detectWrongModel } from './wrong-model.js';
4
+ export { detectAbandoned } from './abandoned.js';
5
+ /**
6
+ * Runs all three waste detectors and upserts the results into
7
+ * the waste_attributions table. For the 'abandoned' category, sub-case B
8
+ * (mid-tool-call-exit) takes precedence over sub-case A — this is handled
9
+ * inside detectAbandoned() itself.
10
+ */
11
+ export declare function computeAllWaste(db: Database.Database): void;
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/waste/index.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAA;AAMrC,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAWhD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,GAAG,IAAI,CAgC3D"}
@@ -0,0 +1,45 @@
1
+ import { detectCacheMiss } from './cache-miss.js';
2
+ import { detectWrongModel } from './wrong-model.js';
3
+ import { detectAbandoned } from './abandoned.js';
4
+ export { detectCacheMiss } from './cache-miss.js';
5
+ export { detectWrongModel } from './wrong-model.js';
6
+ export { detectAbandoned } from './abandoned.js';
7
+ const UPSERT_SQL = `
8
+ INSERT INTO waste_attributions (session_id, category, amount_cents, anchor_msg_idx, evidence_json)
9
+ VALUES (?, ?, ?, ?, ?)
10
+ ON CONFLICT(session_id, category) DO UPDATE SET
11
+ amount_cents = excluded.amount_cents,
12
+ anchor_msg_idx = excluded.anchor_msg_idx,
13
+ evidence_json = excluded.evidence_json
14
+ `;
15
+ /**
16
+ * Runs all three waste detectors and upserts the results into
17
+ * the waste_attributions table. For the 'abandoned' category, sub-case B
18
+ * (mid-tool-call-exit) takes precedence over sub-case A — this is handled
19
+ * inside detectAbandoned() itself.
20
+ */
21
+ export function computeAllWaste(db) {
22
+ const cacheMissResults = detectCacheMiss(db);
23
+ const wrongModelResults = detectWrongModel(db);
24
+ const abandonedResults = detectAbandoned(db);
25
+ // Merge all attributions. For the same (session_id, category), last-write wins.
26
+ // Because detectAbandoned already merges B over A internally, we can just
27
+ // flatten and upsert — the ON CONFLICT clause handles any remaining duplicates.
28
+ const all = [
29
+ ...cacheMissResults,
30
+ ...wrongModelResults,
31
+ ...abandonedResults,
32
+ ];
33
+ const upsert = db.prepare(UPSERT_SQL);
34
+ const clear = db.prepare('DELETE FROM waste_attributions');
35
+ // Full recompute each run — clear stale rows first so a session that is no longer
36
+ // flagged (e.g. after a detector change, or after the user fixes their usage) stops
37
+ // showing as wasteful. Without this, removed/lowered attributions linger forever.
38
+ db.transaction((attributions) => {
39
+ clear.run();
40
+ for (const attr of attributions) {
41
+ upsert.run(attr.sessionId, attr.category, attr.amountCents, attr.anchorMsgIdx, attr.evidenceJson);
42
+ }
43
+ })(all);
44
+ }
45
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/waste/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAEhD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAEhD,MAAM,UAAU,GAAG;;;;;;;CAOlB,CAAA;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,EAAqB;IACnD,MAAM,gBAAgB,GAAG,eAAe,CAAC,EAAE,CAAC,CAAA;IAC5C,MAAM,iBAAiB,GAAG,gBAAgB,CAAC,EAAE,CAAC,CAAA;IAC9C,MAAM,gBAAgB,GAAG,eAAe,CAAC,EAAE,CAAC,CAAA;IAE5C,gFAAgF;IAChF,0EAA0E;IAC1E,gFAAgF;IAChF,MAAM,GAAG,GAAuB;QAC9B,GAAG,gBAAgB;QACnB,GAAG,iBAAiB;QACpB,GAAG,gBAAgB;KACpB,CAAA;IAED,MAAM,MAAM,GAAG,EAAE,CAAC,OAAO,CAAC,UAAU,CAAC,CAAA;IACrC,MAAM,KAAK,GAAG,EAAE,CAAC,OAAO,CAAC,gCAAgC,CAAC,CAAA;IAE1D,kFAAkF;IAClF,oFAAoF;IACpF,kFAAkF;IAClF,EAAE,CAAC,WAAW,CAAC,CAAC,YAAgC,EAAE,EAAE;QAClD,KAAK,CAAC,GAAG,EAAE,CAAA;QACX,KAAK,MAAM,IAAI,IAAI,YAAY,EAAE,CAAC;YAChC,MAAM,CAAC,GAAG,CACR,IAAI,CAAC,SAAS,EACd,IAAI,CAAC,QAAQ,EACb,IAAI,CAAC,WAAW,EAChB,IAAI,CAAC,YAAY,EACjB,IAAI,CAAC,YAAY,CAClB,CAAA;QACH,CAAC;IACH,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;AACT,CAAC"}
@@ -0,0 +1,11 @@
1
+ import Database from 'better-sqlite3';
2
+ import type { WasteAttribution } from '../schema/index.js';
3
+ /**
4
+ * Detects Opus overpay in two cases:
5
+ * 1. Opus-dominant SIMPLE session (short, few tools) → whole-session delta vs Sonnet.
6
+ * 2. Opus-MINORITY mixed session (mostly Sonnet, a few Opus turns) → the Opus turns'
7
+ * delta vs Sonnet. This is the case the old detector missed entirely.
8
+ * Thresholds loosened from the original (30s/2-tool/0-error) which flagged almost nothing.
9
+ */
10
+ export declare function detectWrongModel(db: Database.Database): WasteAttribution[];
11
+ //# sourceMappingURL=wrong-model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wrong-model.d.ts","sourceRoot":"","sources":["../../../src/core/waste/wrong-model.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAA;AACrC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AA4B1D;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,GAAG,gBAAgB,EAAE,CAyG1E"}
@@ -0,0 +1,104 @@
1
+ /** Drop sub-half-cent attributions so the UI never shows "$0.00" rows. */
2
+ const MIN_CENTS = 0.5;
3
+ /**
4
+ * Detects Opus overpay in two cases:
5
+ * 1. Opus-dominant SIMPLE session (short, few tools) → whole-session delta vs Sonnet.
6
+ * 2. Opus-MINORITY mixed session (mostly Sonnet, a few Opus turns) → the Opus turns'
7
+ * delta vs Sonnet. This is the case the old detector missed entirely.
8
+ * Thresholds loosened from the original (30s/2-tool/0-error) which flagged almost nothing.
9
+ */
10
+ export function detectWrongModel(db) {
11
+ // Single batch query: Opus sessions with tool_call_count + error_count inline — no N+1
12
+ const opusSessions = db
13
+ .prepare(`
14
+ SELECT m.session_id,
15
+ (SELECT m2.model FROM messages m2
16
+ WHERE m2.session_id = m.session_id AND m2.model LIKE '%opus%' AND m2.model IS NOT NULL
17
+ GROUP BY m2.model ORDER BY COUNT(*) DESC LIMIT 1) AS dominant_opus_model,
18
+ s.started_at, s.ended_at,
19
+ s.total_input_tokens, s.total_output_tokens,
20
+ s.total_cache_creation, s.total_cache_read,
21
+ (SELECT COUNT(*) FROM tool_calls tc
22
+ JOIN messages msg ON tc.message_id = msg.id
23
+ WHERE msg.session_id = m.session_id) AS tool_call_count,
24
+ (SELECT COUNT(*) FROM tool_calls tc
25
+ JOIN messages msg ON tc.message_id = msg.id
26
+ WHERE msg.session_id = m.session_id AND tc.error IS NOT NULL) AS error_count
27
+ FROM messages m
28
+ JOIN sessions s ON s.id = m.session_id
29
+ -- Cursor sessions excluded — Cursor's API layer hides model-routing signals these detectors need.
30
+ WHERE m.model LIKE '%opus%' AND m.model IS NOT NULL AND s.platform = 'claude-code'
31
+ GROUP BY m.session_id
32
+ `)
33
+ .all();
34
+ // Load all pricing into a Map once
35
+ const pricingMap = new Map();
36
+ for (const row of db
37
+ .prepare(`SELECT model, input_per_mtok, output_per_mtok, cache_write_per_mtok, cache_read_per_mtok
38
+ FROM pricing ORDER BY period_start ASC`)
39
+ .all()) {
40
+ pricingMap.set(row.model, row);
41
+ }
42
+ const sonnetPricing = pricingMap.get('claude-sonnet-4-6') ??
43
+ [...pricingMap.values()].filter((r) => r.model.includes('sonnet')).at(-1);
44
+ if (!sonnetPricing)
45
+ return [];
46
+ // Anchor query — hoisted, exact model equality
47
+ const getFirstOpusMsg = db.prepare(`
48
+ SELECT idx FROM messages
49
+ WHERE session_id = ? AND role = 'assistant' AND model = ?
50
+ ORDER BY idx ASC LIMIT 1
51
+ `);
52
+ const cost = (p, ti, to, tc, tr) => ((ti * p.input_per_mtok +
53
+ to * p.output_per_mtok +
54
+ tc * p.cache_write_per_mtok +
55
+ tr * p.cache_read_per_mtok) /
56
+ 1_000_000) *
57
+ 100;
58
+ const attributions = [];
59
+ for (const s of opusSessions) {
60
+ if (!s.dominant_opus_model)
61
+ continue;
62
+ if (!s.started_at || !s.ended_at)
63
+ continue;
64
+ const sessionDurationMs = new Date(s.ended_at).getTime() - new Date(s.started_at).getTime();
65
+ // Loosened: ≤90s + ≤4 tools. Errors no longer disqualify — you still overpaid
66
+ // for Opus on a short, simple task even if a tool call failed.
67
+ if (sessionDurationMs > 90_000)
68
+ continue;
69
+ if (s.tool_call_count > 4)
70
+ continue;
71
+ const opusPricing = pricingMap.get(s.dominant_opus_model);
72
+ if (!opusPricing)
73
+ continue;
74
+ const { total_input_tokens: ti, total_output_tokens: to, total_cache_creation: tc, total_cache_read: tr } = s;
75
+ const opusCents = cost(opusPricing, ti, to, tc, tr);
76
+ const sonnetCents = cost(sonnetPricing, ti, to, tc, tr);
77
+ const amountCents = opusCents - sonnetCents;
78
+ if (amountCents < MIN_CENTS)
79
+ continue;
80
+ const firstMsg = getFirstOpusMsg.get(s.session_id, s.dominant_opus_model);
81
+ attributions.push({
82
+ sessionId: s.session_id,
83
+ category: 'wrong-model',
84
+ amountCents,
85
+ anchorMsgIdx: firstMsg?.idx ?? null,
86
+ evidenceJson: JSON.stringify({
87
+ reason: 'opus-on-simple-session',
88
+ opusModel: s.dominant_opus_model,
89
+ sessionDurationMs,
90
+ toolCallCount: s.tool_call_count,
91
+ opusCents,
92
+ sonnetCents,
93
+ }),
94
+ });
95
+ }
96
+ // NOTE: a per-message "mixed-model overpay" case was tried (flag Opus turns in
97
+ // Opus-minority sessions) and removed — it attributed $680 to a legitimate
98
+ // 1018-Opus-turn work session. "Opus is a minority of messages" is NOT credible
99
+ // evidence that Opus was unneeded; a defensible per-turn signal of task triviality
100
+ // doesn't exist in the current schema. Honest > big. Revisit only with a real
101
+ // per-turn difficulty signal.
102
+ return attributions;
103
+ }
104
+ //# sourceMappingURL=wrong-model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wrong-model.js","sourceRoot":"","sources":["../../../src/core/waste/wrong-model.ts"],"names":[],"mappings":"AA0BA,0EAA0E;AAC1E,MAAM,SAAS,GAAG,GAAG,CAAA;AAErB;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,EAAqB;IACpD,uFAAuF;IACvF,MAAM,YAAY,GAAG,EAAE;SACpB,OAAO,CAAqB;;;;;;;;;;;;;;;;;;;KAmB5B,CAAC;SACD,GAAG,EAAE,CAAA;IAER,mCAAmC;IACnC,MAAM,UAAU,GAAG,IAAI,GAAG,EAAsB,CAAA;IAChD,KAAK,MAAM,GAAG,IAAI,EAAE;SACjB,OAAO,CACN;8CACwC,CACzC;SACA,GAAG,EAAE,EAAE,CAAC;QACT,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;IAChC,CAAC;IAED,MAAM,aAAa,GACjB,UAAU,CAAC,GAAG,CAAC,mBAAmB,CAAC;QACnC,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;IAE3E,IAAI,CAAC,aAAa;QAAE,OAAO,EAAE,CAAA;IAE7B,+CAA+C;IAC/C,MAAM,eAAe,GAAG,EAAE,CAAC,OAAO,CAAgC;;;;GAIjE,CAAC,CAAA;IAEF,MAAM,IAAI,GAAG,CAAC,CAAa,EAAE,EAAU,EAAE,EAAU,EAAE,EAAU,EAAE,EAAU,EAAE,EAAE,CAC7E,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,cAAc;QACrB,EAAE,GAAG,CAAC,CAAC,eAAe;QACtB,EAAE,GAAG,CAAC,CAAC,oBAAoB;QAC3B,EAAE,GAAG,CAAC,CAAC,mBAAmB,CAAC;QAC3B,SAAS,CAAC;QACZ,GAAG,CAAA;IAEL,MAAM,YAAY,GAAuB,EAAE,CAAA;IAE3C,KAAK,MAAM,CAAC,IAAI,YAAY,EAAE,CAAC;QAC7B,IAAI,CAAC,CAAC,CAAC,mBAAmB;YAAE,SAAQ;QACpC,IAAI,CAAC,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,CAAC,QAAQ;YAAE,SAAQ;QAE1C,MAAM,iBAAiB,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,OAAO,EAAE,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,OAAO,EAAE,CAAA;QAC3F,8EAA8E;QAC9E,+DAA+D;QAC/D,IAAI,iBAAiB,GAAG,MAAM;YAAE,SAAQ;QACxC,IAAI,CAAC,CAAC,eAAe,GAAG,CAAC;YAAE,SAAQ;QAEnC,MAAM,WAAW,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAA;QACzD,IAAI,CAAC,WAAW;YAAE,SAAQ;QAE1B,MAAM,EAAE,kBAAkB,EAAE,EAAE,EAAE,mBAAmB,EAAE,EAAE,EAAE,oBAAoB,EAAE,EAAE,EAAE,gBAAgB,EAAE,EAAE,EAAE,GAAG,CAAC,CAAA;QAC7G,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAA;QACnD,MAAM,WAAW,GAAG,IAAI,CAAC,aAAa,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAA;QACvD,MAAM,WAAW,GAAG,SAAS,GAAG,WAAW,CAAA;QAE3C,IAAI,WAAW,GAAG,SAAS;YAAE,SAAQ;QAErC,MAAM,QAAQ,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC,mBAAmB,CAAC,CAAA;QAEzE,YAAY,CAAC,IAAI,CAAC;YAChB,SAAS,EAAE,CAAC,CAAC,UAAU;YACvB,QAAQ,EAAE,aAAa;YACvB,WAAW;YACX,YAAY,EAAE,QAAQ,EAAE,GAAG,IAAI,IAAI;YACnC,YAAY,EAAE,IAAI,CAAC,SAAS,CAAC;gBAC3B,MAAM,EAAE,wBAAwB;gBAChC,SAAS,EAAE,CAAC,CAAC,mBAAmB;gBAChC,iBAAiB;gBACjB,aAAa,EAAE,CAAC,CAAC,eAAe;gBAChC,SAAS;gBACT,WAAW;aACZ,CAAC;SACH,CAAC,CAAA;IACJ,CAAC;IAED,+EAA+E;IAC/E,2EAA2E;IAC3E,gFAAgF;IAChF,mFAAmF;IACnF,8EAA8E;IAC9E,8BAA8B;IAE9B,OAAO,YAAY,CAAA;AACrB,CAAC"}
@@ -0,0 +1,10 @@
1
+ import Database from 'better-sqlite3';
2
+ export declare const DEFAULT_DB_PATH: string;
3
+ /**
4
+ * Returns the open database instance, creating and migrating it on first call.
5
+ * Pass a custom dbPath for testing.
6
+ */
7
+ export declare function getDb(dbPath?: string): Database.Database;
8
+ /** Close and reset the singleton. Used in tests to clean up temp databases. */
9
+ export declare function closeDb(): void;
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/db/index.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAA;AAOrC,eAAO,MAAM,eAAe,QAAwC,CAAA;AAKpE;;;GAGG;AACH,wBAAgB,KAAK,CAAC,MAAM,SAAkB,GAAG,QAAQ,CAAC,QAAQ,CAUjE;AAED,+EAA+E;AAC/E,wBAAgB,OAAO,IAAI,IAAI,CAI9B"}
@@ -0,0 +1,29 @@
1
+ import { homedir } from 'node:os';
2
+ import { join, dirname } from 'node:path';
3
+ import { mkdirSync } from 'node:fs';
4
+ import { migrate } from './migrate.js';
5
+ export const DEFAULT_DB_PATH = join(homedir(), '.vyasa', 'index.db');
6
+ let _db = null;
7
+ let _dbPath = null;
8
+ /**
9
+ * Returns the open database instance, creating and migrating it on first call.
10
+ * Pass a custom dbPath for testing.
11
+ */
12
+ export function getDb(dbPath = DEFAULT_DB_PATH) {
13
+ if (_db?.open) {
14
+ if (_dbPath !== dbPath)
15
+ throw new Error(`getDb called with "${dbPath}" but singleton already opened with "${_dbPath}"`);
16
+ return _db;
17
+ }
18
+ mkdirSync(dirname(dbPath), { recursive: true });
19
+ _db = migrate(dbPath);
20
+ _dbPath = dbPath;
21
+ return _db;
22
+ }
23
+ /** Close and reset the singleton. Used in tests to clean up temp databases. */
24
+ export function closeDb() {
25
+ _db?.close();
26
+ _db = null;
27
+ _dbPath = null;
28
+ }
29
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/db/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACjC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAEnC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAEtC,MAAM,CAAC,MAAM,eAAe,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAA;AAEpE,IAAI,GAAG,GAA6B,IAAI,CAAA;AACxC,IAAI,OAAO,GAAkB,IAAI,CAAA;AAEjC;;;GAGG;AACH,MAAM,UAAU,KAAK,CAAC,MAAM,GAAG,eAAe;IAC5C,IAAI,GAAG,EAAE,IAAI,EAAE,CAAC;QACd,IAAI,OAAO,KAAK,MAAM;YACpB,MAAM,IAAI,KAAK,CAAC,sBAAsB,MAAM,wCAAwC,OAAO,GAAG,CAAC,CAAA;QACjG,OAAO,GAAG,CAAA;IACZ,CAAC;IACD,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IAC/C,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IACrB,OAAO,GAAG,MAAM,CAAA;IAChB,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,OAAO;IACrB,GAAG,EAAE,KAAK,EAAE,CAAA;IACZ,GAAG,GAAG,IAAI,CAAA;IACV,OAAO,GAAG,IAAI,CAAA;AAChB,CAAC"}
@@ -0,0 +1,8 @@
1
+ import Database from 'better-sqlite3';
2
+ /**
3
+ * Opens (or creates) the SQLite database at dbPath, enables WAL mode,
4
+ * runs any pending migrations, and seeds pricing data.
5
+ * Returns the open database instance.
6
+ */
7
+ export declare function migrate(dbPath: string): Database.Database;
8
+ //# sourceMappingURL=migrate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../../src/db/migrate.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAA;AAiPrC;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,CAAC,QAAQ,CAoCzD"}