@xingular/harness 1.1.1 → 1.3.1-unstable.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 (264) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +36 -28
  3. package/dist/cli.js +10837 -6159
  4. package/dist/sdk/accounts/accounts.d.ts +27 -3
  5. package/dist/sdk/accounts/accounts.js +78 -24
  6. package/dist/sdk/accounts/secrets.d.ts +12 -0
  7. package/dist/sdk/accounts/secrets.js +114 -7
  8. package/dist/sdk/accounts/store.d.ts +26 -1
  9. package/dist/sdk/accounts/store.js +1 -1
  10. package/dist/sdk/agents/builtin.js +18 -4
  11. package/dist/sdk/agents/planResearch.d.ts +27 -0
  12. package/dist/sdk/agents/planResearch.js +82 -0
  13. package/dist/sdk/agents/profiles.d.ts +5 -1
  14. package/dist/sdk/agents/profiles.js +21 -5
  15. package/dist/sdk/annotations/pageScript.js +349 -48
  16. package/dist/sdk/annotations/part.js +34 -23
  17. package/dist/sdk/annotations/store.d.ts +7 -0
  18. package/dist/sdk/annotations/store.js +13 -5
  19. package/dist/sdk/browser/backend.d.ts +16 -0
  20. package/dist/sdk/browser/backend.js +17 -0
  21. package/dist/sdk/browser/contracts.d.ts +19 -0
  22. package/dist/sdk/browser/contracts.js +116 -0
  23. package/dist/sdk/browser/tools.d.ts +1 -4
  24. package/dist/sdk/browser/tools.js +14 -91
  25. package/dist/sdk/capabilities/runSnapshot.js +10 -4
  26. package/dist/sdk/cli.js +5 -4
  27. package/dist/sdk/commands/list.d.ts +3 -0
  28. package/dist/sdk/commands/list.js +75 -0
  29. package/dist/sdk/commands/registry.d.ts +258 -0
  30. package/dist/sdk/commands/registry.js +48 -0
  31. package/dist/sdk/commands/routes.d.ts +16 -0
  32. package/dist/sdk/commands/routes.js +33 -0
  33. package/dist/sdk/core/commandBudget.d.ts +23 -0
  34. package/dist/sdk/core/commandBudget.js +57 -0
  35. package/dist/sdk/core/index.js +17 -1
  36. package/dist/sdk/core/interactiveProfile.d.ts +46 -0
  37. package/dist/sdk/core/interactiveProfile.js +143 -0
  38. package/dist/sdk/core/shell.d.ts +20 -0
  39. package/dist/sdk/core/shell.js +73 -0
  40. package/dist/sdk/core/workspace.d.ts +13 -0
  41. package/dist/sdk/core/workspace.js +88 -0
  42. package/dist/sdk/core/workspaceBashTool.d.ts +20 -0
  43. package/dist/sdk/core/workspaceBashTool.js +50 -0
  44. package/dist/sdk/core/workspaceSandbox.d.ts +41 -0
  45. package/dist/sdk/core/workspaceSandbox.js +360 -0
  46. package/dist/sdk/core/workspaceWriteOperations.d.ts +33 -0
  47. package/dist/sdk/core/workspaceWriteOperations.js +224 -0
  48. package/dist/sdk/doctor/checks.js +71 -12
  49. package/dist/sdk/doctor/types.d.ts +2 -2
  50. package/dist/sdk/harness.d.ts +7 -0
  51. package/dist/sdk/harness.js +42 -2
  52. package/dist/sdk/hub/hub.js +5 -3
  53. package/dist/sdk/index.d.ts +8 -3
  54. package/dist/sdk/index.js +4 -1
  55. package/dist/sdk/pi/runtime.d.ts +2 -1
  56. package/dist/sdk/pi/runtime.js +2 -2
  57. package/dist/sdk/plugins/builtin.js +32 -15
  58. package/dist/sdk/plugins/builtinRegistry.d.ts +10 -3
  59. package/dist/sdk/plugins/builtinRegistry.js +30 -8
  60. package/dist/sdk/plugins/catalogue.js +2 -1
  61. package/dist/sdk/plugins/commandPrefix.d.ts +8 -0
  62. package/dist/sdk/plugins/commandPrefix.js +21 -0
  63. package/dist/sdk/plugins/firstParty.d.ts +1 -1
  64. package/dist/sdk/plugins/firstParty.js +8 -2
  65. package/dist/sdk/plugins/install.d.ts +6 -2
  66. package/dist/sdk/plugins/install.js +29 -5
  67. package/dist/sdk/plugins/inventory.js +6 -5
  68. package/dist/sdk/plugins/manifest.d.ts +2 -0
  69. package/dist/sdk/plugins/manifest.js +2 -1
  70. package/dist/sdk/plugins/surfaces.d.ts +5 -0
  71. package/dist/sdk/plugins/surfaces.js +11 -0
  72. package/dist/sdk/plugins/types.d.ts +15 -0
  73. package/dist/sdk/prompt/compile.d.ts +14 -0
  74. package/dist/sdk/prompt/compile.js +65 -31
  75. package/dist/sdk/prompt/standard.d.ts +17 -0
  76. package/dist/sdk/prompt/standard.js +21 -0
  77. package/dist/sdk/server/methods.js +22 -2
  78. package/dist/sdk/server/remote.js +36 -11
  79. package/dist/sdk/sessions/compactionCheckpoint.d.ts +11 -0
  80. package/dist/sdk/sessions/compactionCheckpoint.js +79 -0
  81. package/dist/sdk/sessions/continuation.d.ts +7 -2
  82. package/dist/sdk/sessions/continuation.js +11 -6
  83. package/dist/sdk/sessions/contracts.d.ts +190 -4
  84. package/dist/sdk/sessions/contracts.js +71 -8
  85. package/dist/sdk/sessions/control.d.ts +72 -0
  86. package/dist/sdk/sessions/control.js +158 -0
  87. package/dist/sdk/sessions/initialBash.d.ts +7 -0
  88. package/dist/sdk/sessions/initialBash.js +7 -0
  89. package/dist/sdk/sessions/planMode.d.ts +14 -3
  90. package/dist/sdk/sessions/planMode.js +74 -17
  91. package/dist/sdk/sessions/registry.d.ts +1 -1
  92. package/dist/sdk/sessions/registry.js +2 -2
  93. package/dist/sdk/sessions/runStore.d.ts +14 -0
  94. package/dist/sdk/sessions/runStore.js +38 -1
  95. package/dist/sdk/sessions/runner.d.ts +40 -2
  96. package/dist/sdk/sessions/runner.js +161 -13
  97. package/dist/sdk/sessions/sessions.d.ts +23 -3
  98. package/dist/sdk/sessions/sessions.js +225 -22
  99. package/dist/sdk/sessions/subagentService.d.ts +24 -0
  100. package/dist/sdk/sessions/subagentService.js +225 -26
  101. package/dist/sdk/sessions/subagents.d.ts +10 -1
  102. package/dist/sdk/sessions/subagents.js +4 -0
  103. package/dist/sdk/sessions/workSlice.d.ts +106 -0
  104. package/dist/sdk/sessions/workSlice.js +329 -0
  105. package/dist/sdk/sessions/workspaceBoundary.d.ts +15 -0
  106. package/dist/sdk/sessions/workspaceBoundary.js +46 -0
  107. package/dist/sdk/terminal/browserControl.js +2 -1
  108. package/dist/sdk/terminal/controller.d.ts +31 -3
  109. package/dist/sdk/terminal/controller.js +81 -29
  110. package/dist/sdk/terminal/opentui/ansi.d.ts +25 -0
  111. package/dist/sdk/terminal/opentui/ansi.js +192 -0
  112. package/dist/sdk/terminal/opentui/model.d.ts +163 -0
  113. package/dist/sdk/terminal/opentui/model.js +482 -0
  114. package/dist/sdk/terminal/opentui/subagentsWidget.d.ts +3 -3
  115. package/dist/sdk/terminal/opentui/subagentsWidget.js +7 -5
  116. package/dist/sdk/terminal/opentui/theme.d.ts +43 -0
  117. package/dist/sdk/terminal/opentui/theme.js +88 -0
  118. package/dist/sdk/terminal/piExtension.d.ts +6 -1
  119. package/dist/sdk/terminal/piExtension.js +177 -63
  120. package/dist/sdk/terminal/standalone.d.ts +7 -0
  121. package/dist/sdk/terminal/standalone.js +72 -13
  122. package/dist/sdk/terminal/subagentMonitor.d.ts +28 -6
  123. package/dist/sdk/terminal/subagentMonitor.js +61 -9
  124. package/dist/sdk/terminal/terminals.d.ts +26 -2
  125. package/dist/sdk/terminal/terminals.js +325 -43
  126. package/dist/sdk/terminal/trajectory.d.ts +6 -1
  127. package/dist/sdk/terminal/trajectory.js +8 -0
  128. package/dist/sdk/terminal/tui/annotateCommand.d.ts +1 -1
  129. package/dist/sdk/terminal/tui/annotateCommand.js +3 -3
  130. package/dist/sdk/terminal/tui/chrome.d.ts +4 -0
  131. package/dist/sdk/terminal/tui/chrome.js +13 -0
  132. package/dist/sdk/terminal/tui/commandPages.d.ts +3 -1
  133. package/dist/sdk/terminal/tui/commandPages.js +17 -4
  134. package/dist/sdk/terminal/tui/commands.d.ts +6 -0
  135. package/dist/sdk/terminal/tui/commands.js +213 -124
  136. package/dist/sdk/terminal/tui/list.d.ts +14 -1
  137. package/dist/sdk/terminal/tui/list.js +96 -14
  138. package/dist/sdk/terminal/tui/pageOverlay.js +21 -5
  139. package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +1 -1
  140. package/dist/sdk/terminal/tui/pages/browserPage.js +5 -4
  141. package/dist/sdk/terminal/tui/pages/hubPage.d.ts +14 -1
  142. package/dist/sdk/terminal/tui/pages/hubPage.js +192 -28
  143. package/dist/sdk/terminal/tui/pages/notesPage.d.ts +5 -5
  144. package/dist/sdk/terminal/tui/pages/notesPage.js +18 -18
  145. package/dist/sdk/terminal/tui/pages/pluginsPage.d.ts +8 -6
  146. package/dist/sdk/terminal/tui/pages/pluginsPage.js +206 -28
  147. package/dist/sdk/terminal/tui/pages/subagentsPage.d.ts +1 -0
  148. package/dist/sdk/terminal/tui/pages/subagentsPage.js +104 -19
  149. package/dist/sdk/terminal/tui/pages/usagePage.d.ts +43 -0
  150. package/dist/sdk/terminal/tui/pages/usagePage.js +370 -0
  151. package/dist/sdk/terminal/tui/piAdapter.d.ts +1 -2
  152. package/dist/sdk/terminal/tui/piAdapter.js +3 -5
  153. package/dist/sdk/terminal/tui/pluginCommands.d.ts +2 -0
  154. package/dist/sdk/terminal/tui/pluginCommands.js +13 -9
  155. package/dist/sdk/terminal/tui/runtime.js +23 -3
  156. package/dist/sdk/terminal/tui/sessionPicker.d.ts +40 -0
  157. package/dist/sdk/terminal/tui/sessionPicker.js +141 -0
  158. package/dist/sdk/tools/checkpoint.d.ts +27 -0
  159. package/dist/sdk/tools/checkpoint.js +154 -0
  160. package/dist/sdk/tools/getCapabilities.d.ts +31 -0
  161. package/dist/sdk/tools/getCapabilities.js +34 -0
  162. package/dist/sdk/tools/sessionName.d.ts +2 -0
  163. package/dist/sdk/tools/sessionName.js +5 -3
  164. package/dist/sdk/tools/spawnSubagent.js +11 -8
  165. package/dist/sdk/tools/todo.d.ts +15 -0
  166. package/dist/sdk/tools/todo.js +64 -0
  167. package/dist/sdk/usage/analytics.d.ts +20 -0
  168. package/dist/sdk/usage/analytics.js +151 -0
  169. package/dist/sdk/usage/ledger.d.ts +38 -0
  170. package/dist/sdk/usage/ledger.js +142 -0
  171. package/dist/sdk/usage/limits/adapters.d.ts +11 -0
  172. package/dist/sdk/usage/limits/adapters.js +349 -0
  173. package/dist/sdk/usage/limits/capture.d.ts +8 -0
  174. package/dist/sdk/usage/limits/capture.js +47 -0
  175. package/dist/sdk/usage/limits/http.d.ts +12 -0
  176. package/dist/sdk/usage/limits/http.js +51 -0
  177. package/dist/sdk/usage/limits/normalize.d.ts +15 -0
  178. package/dist/sdk/usage/limits/normalize.js +211 -0
  179. package/dist/sdk/usage/limits/service.d.ts +30 -0
  180. package/dist/sdk/usage/limits/service.js +176 -0
  181. package/dist/sdk/usage/limits/types.d.ts +65 -0
  182. package/dist/sdk/usage/limits/types.js +1 -0
  183. package/dist/sdk/usage/serverMethods.d.ts +3 -0
  184. package/dist/sdk/usage/serverMethods.js +22 -0
  185. package/dist/sdk/usage/service.d.ts +32 -0
  186. package/dist/sdk/usage/service.js +66 -0
  187. package/dist/sdk/usage/settings.d.ts +7 -0
  188. package/dist/sdk/usage/settings.js +28 -0
  189. package/dist/sdk/usage/types.d.ts +60 -0
  190. package/dist/sdk/usage/types.js +4 -0
  191. package/dist/terminal-opentui.js +9204 -4812
  192. package/dist/terminal-pi.js +1848 -272
  193. package/docs/architecture/00-overview.md +1 -1
  194. package/docs/architecture/01-extension-points.md +1 -1
  195. package/docs/architecture/02-sessions-and-runs.md +169 -36
  196. package/docs/architecture/04-prompt.md +55 -15
  197. package/docs/architecture/06-plugins-and-capabilities.md +36 -15
  198. package/docs/architecture/07-tools-and-gate.md +131 -25
  199. package/docs/architecture/08-memory-and-history.md +4 -2
  200. package/docs/architecture/09-accounts-and-connectors.md +39 -3
  201. package/docs/architecture/10-server-protocol.md +71 -9
  202. package/docs/architecture/11-terminal.md +148 -60
  203. package/docs/architecture/12-storage.md +30 -2
  204. package/docs/architecture/13-world-tools.md +22 -1
  205. package/docs/architecture/14-surfaces-and-parity.md +58 -8
  206. package/docs/architecture/README.md +20 -17
  207. package/docs/architecture/proposals/PROP-0006-run-checklist.md +2 -0
  208. package/docs/architecture/proposals/PROP-0017-page-annotations.md +6 -2
  209. package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +1 -1
  210. package/docs/architecture/proposals/PROP-0031-plugin-command-owner-namespaces.md +54 -0
  211. package/docs/architecture/proposals/PROP-0032-planning-quality.md +196 -0
  212. package/docs/architecture/proposals/PROP-0033-compaction-plan-checkpoints.md +75 -0
  213. package/docs/architecture/proposals/PROP-0034-conversation-fast-mode.md +120 -0
  214. package/docs/architecture/proposals/PROP-0035-retain-subagent-status.md +47 -0
  215. package/docs/architecture/proposals/PROP-0036-first-class-harness-commands.md +59 -0
  216. package/docs/architecture/proposals/PROP-0037-workspace-write-boundary.md +63 -0
  217. package/docs/architecture/proposals/PROP-0038-annotate-open-shadow-roots.md +73 -0
  218. package/docs/architecture/proposals/PROP-0039-workspace-boundary-hardening.md +139 -0
  219. package/docs/architecture/proposals/PROP-0040-one-launch-path.md +87 -0
  220. package/docs/architecture/proposals/PROP-0041-bounded-work-slices.md +258 -0
  221. package/docs/architecture/proposals/PROP-0042-usage-and-provider-limits.md +127 -0
  222. package/docs/architecture/proposals/PROP-0043-read-only-plan-bash.md +74 -0
  223. package/docs/architecture/proposals/PROP-0044-work-slice-defaults.md +93 -0
  224. package/docs/architecture/proposals/PROP-0045-rename-checklist-to-todo.md +132 -0
  225. package/docs/architecture/proposals/PROP-0046-progress-aware-stall-guards.md +82 -0
  226. package/docs/architecture/proposals/PROP-0047-plugins-hub-separation.md +122 -0
  227. package/docs/architecture/proposals/README.md +24 -6
  228. package/docs/decisions/ADR-0001-owned-terminal.md +1 -1
  229. package/docs/decisions/ADR-0021-required-command-namespaces.md +1 -1
  230. package/docs/decisions/ADR-0031-plugin-command-owner-namespaces.md +37 -0
  231. package/docs/decisions/ADR-0032-compaction-run-checkpoints.md +33 -0
  232. package/docs/decisions/ADR-0033-plan-mode-research-delegation.md +47 -0
  233. package/docs/decisions/ADR-0034-first-class-harness-terminal-commands.md +25 -0
  234. package/docs/decisions/ADR-0035-one-launch-path.md +40 -0
  235. package/docs/decisions/ADR-0036-workspace-write-operation-boundary.md +54 -0
  236. package/docs/decisions/ADR-0037-usage-ledger-and-provider-limits.md +44 -0
  237. package/docs/decisions/ADR-0038-plan-bash-os-read-only.md +29 -0
  238. package/docs/decisions/ADR-0039-progress-aware-stall-guards.md +39 -0
  239. package/docs/decisions/ADR-0040-architecture-change-workflow.md +31 -0
  240. package/docs/decisions/ADR-0041-plugins-hub-separation.md +41 -0
  241. package/docs/decisions/ADR-0042-windows-shell-without-sandbox.md +40 -0
  242. package/docs/decisions/ADR-0043-parents-continue-stopped-subagents.md +49 -0
  243. package/docs/doctor.md +14 -1
  244. package/docs/experiments/EXP-0003-performance-audit/data/session-runtime-replication.json +420 -0
  245. package/docs/experiments/EXP-0003-performance-audit/data/session-runtime.json +420 -0
  246. package/docs/experiments/EXP-0003-performance-audit/data/terminal-compact.json +376 -0
  247. package/docs/experiments/EXP-0003-performance-audit/data/terminal-history.json +422 -0
  248. package/docs/experiments/EXP-0003-performance-audit/data/terminal-question.json +95 -0
  249. package/docs/experiments/EXP-0003-performance-audit/data/terminal-wide.json +1036 -0
  250. package/docs/experiments/EXP-0003-performance-audit/plan.md +109 -0
  251. package/docs/experiments/EXP-0003-performance-audit/results.md +151 -0
  252. package/docs/handoff-2026-10-25-app-parity-and-solo-tabs.md +202 -0
  253. package/docs/parity.md +83 -27
  254. package/docs/releases/1.1.1.md +28 -0
  255. package/docs/releases/1.2.0.md +49 -0
  256. package/docs/releases/1.3.0.md +21 -0
  257. package/docs/releases/4.0.0.md +10 -9
  258. package/docs/releases/README.md +35 -30
  259. package/docs/study/00-index.md +3 -0
  260. package/docs/study/STUDY-0003-long-horizon-runs.md +210 -0
  261. package/docs/terminal-tui.md +162 -82
  262. package/package.json +4 -3
  263. package/dist/sdk/tools/checklist.d.ts +0 -9
  264. package/dist/sdk/tools/checklist.js +0 -54
@@ -0,0 +1,20 @@
1
+ import { type PriceRates } from "../sessions/pricing.js";
2
+ import type { UsageFilter, UsagePeriod, UsageSummary } from "./types.js";
3
+ /** A price lookup, injected so tests never load Pi's catalogue. */
4
+ export type RatesLookup = (provider: string, model: string) => Promise<PriceRates | null>;
5
+ /** The local-day boundaries `period` covers, expressed as UTC instants. */
6
+ export declare function periodRange(period: UsagePeriod, offsetMinutes: number, now?: Date): {
7
+ from: string | null;
8
+ to: string;
9
+ };
10
+ export interface SummaryInput {
11
+ root: string;
12
+ filter: UsageFilter;
13
+ period: UsagePeriod;
14
+ timezoneOffsetMinutes: number;
15
+ /** Model labels are `provider/model`; only accounts need a lookup. */
16
+ accountLabel?: (accountId: string) => string;
17
+ rates?: RatesLookup;
18
+ now?: Date;
19
+ }
20
+ export declare function summarize(input: SummaryInput): Promise<UsageSummary>;
@@ -0,0 +1,151 @@
1
+ import { all, withIndex } from "../core/index.js";
2
+ import { costAt } from "../sessions/pricing.js";
3
+ import { lastIndexedAt } from "./ledger.js";
4
+ const ROW_LIMIT = 10;
5
+ const DAY_LIMIT = 400;
6
+ const UNPRICED_LIMIT = 40;
7
+ const zero = (value) => value ?? 0;
8
+ const round = (value, places = 6) => Math.round(value * 10 ** places) / 10 ** places;
9
+ function totals(row, estimated = 0) {
10
+ const input = zero(row?.input);
11
+ const cacheRead = zero(row?.cacheRead);
12
+ const cacheWrite = zero(row?.cacheWrite);
13
+ const cost = row?.cost == null && !estimated ? null : round(zero(row?.cost) + estimated);
14
+ const cached = cacheRead + input + cacheWrite;
15
+ return {
16
+ requests: zero(row?.requests),
17
+ inputTokens: input,
18
+ cacheReadTokens: cacheRead,
19
+ cacheWriteTokens: cacheWrite,
20
+ outputTokens: zero(row?.output),
21
+ reasoningTokens: zero(row?.reasoning),
22
+ costUsd: cost,
23
+ estimatedCostUsd: estimated ? round(estimated) : 0,
24
+ unpricedRequests: zero(row?.unpriced),
25
+ avgTtftMs: row?.ttft == null ? null : Math.round(row.ttft),
26
+ avgDurationMs: row?.duration == null ? null : Math.round(row.duration),
27
+ cacheHitPercent: cached ? Math.round((cacheRead / cached) * 1000) / 10 : null
28
+ };
29
+ }
30
+ /** The local-day boundaries `period` covers, expressed as UTC instants. */
31
+ export function periodRange(period, offsetMinutes, now = new Date()) {
32
+ const to = now.toISOString();
33
+ if (period === 'all')
34
+ return { from: null, to };
35
+ const shifted = new Date(now.getTime() + offsetMinutes * 60_000);
36
+ const midnight = Date.UTC(shifted.getUTCFullYear(), shifted.getUTCMonth(), shifted.getUTCDate());
37
+ const days = period === 'today' ? 0 : period === '7d' ? 6 : 29;
38
+ return { from: new Date(midnight - days * 86_400_000 - offsetMinutes * 60_000).toISOString(), to };
39
+ }
40
+ function where(filter, from, to) {
41
+ const clauses = ['at < ?'];
42
+ const params = [to];
43
+ if (from) {
44
+ clauses.push('at >= ?');
45
+ params.push(from);
46
+ }
47
+ if (filter.session) {
48
+ clauses.push('session_id = ?');
49
+ params.push(filter.session);
50
+ }
51
+ if (filter.account) {
52
+ clauses.push('account_id = ?');
53
+ params.push(filter.account);
54
+ }
55
+ if (filter.project) {
56
+ clauses.push('project_id = ?');
57
+ params.push(filter.project);
58
+ }
59
+ return { sql: clauses.join(' AND '), params };
60
+ }
61
+ const SUM = `COUNT(*) AS requests, SUM(input_tokens) AS input, SUM(cache_read_tokens) AS cacheRead,
62
+ SUM(cache_write_tokens) AS cacheWrite, SUM(output_tokens) AS output, SUM(reasoning_tokens) AS reasoning,
63
+ SUM(cost_usd) AS cost, SUM(CASE WHEN cost_usd IS NULL THEN 1 ELSE 0 END) AS unpriced,
64
+ AVG(ttft_ms) AS ttft, AVG(duration_ms) AS duration`;
65
+ function grouped(database, column, clause, params, limit) {
66
+ return all(database, `SELECT ${column} AS key, ${SUM} FROM usage_requests WHERE ${clause}
67
+ GROUP BY ${column} ORDER BY SUM(COALESCE(input_tokens,0)+COALESCE(cache_read_tokens,0)+COALESCE(cache_write_tokens,0)+COALESCE(output_tokens,0)) DESC
68
+ LIMIT ${limit}`, ...params).map((row) => ({ key: row.key ?? '', values: row }));
69
+ }
70
+ function modelKey(provider, model) {
71
+ return `${provider ?? 'unknown'}/${model ?? 'unknown'}`;
72
+ }
73
+ /** Estimates the cost of the requests a provider did not price, grouped by model. */
74
+ async function estimate(database, clause, params, rates) {
75
+ const rows = all(database, `SELECT provider, model, ${SUM} FROM usage_requests
76
+ WHERE ${clause} AND cost_usd IS NULL GROUP BY provider, model LIMIT ${UNPRICED_LIMIT}`, ...params);
77
+ const estimates = new Map();
78
+ for (const row of rows) {
79
+ if (!row.model)
80
+ continue;
81
+ const table = await rates(row.provider ?? '', row.model).catch(() => null);
82
+ if (!table)
83
+ continue;
84
+ const cost = costAt(table, {
85
+ inputTokens: zero(row.input),
86
+ outputTokens: zero(row.output),
87
+ cacheReadTokens: zero(row.cacheRead),
88
+ cacheWriteTokens: zero(row.cacheWrite)
89
+ });
90
+ const key = modelKey(row.provider, row.model);
91
+ const previous = estimates.get(key) ?? { cost: 0, requests: 0 };
92
+ estimates.set(key, { cost: previous.cost + cost, requests: previous.requests + zero(row.requests) });
93
+ }
94
+ return estimates;
95
+ }
96
+ export async function summarize(input) {
97
+ const offset = Number.isFinite(input.timezoneOffsetMinutes) ? Math.max(-840, Math.min(840, Math.trunc(input.timezoneOffsetMinutes))) : 0;
98
+ const { from, to } = periodRange(input.period, offset, input.now ?? new Date());
99
+ const { sql: clause, params } = where(input.filter, from, to);
100
+ const shift = `${offset >= 0 ? '+' : ''}${offset} minutes`;
101
+ const label = input.accountLabel ?? ((id) => id.slice(0, 8));
102
+ const summary = withIndex(input.root, (database) => {
103
+ const total = all(database, `SELECT ${SUM} FROM usage_requests WHERE ${clause}`, ...params)[0];
104
+ const days = all(database, `SELECT date(at, ?) AS day, ${SUM} FROM usage_requests WHERE ${clause}
105
+ GROUP BY day ORDER BY day DESC LIMIT ${DAY_LIMIT}`, shift, ...params);
106
+ return {
107
+ total,
108
+ days: days.reverse(),
109
+ byModel: grouped(database, "COALESCE(provider,'unknown') || '/' || COALESCE(model,'unknown')", clause, params, ROW_LIMIT + 1),
110
+ byProvider: grouped(database, "COALESCE(provider,'unknown')", clause, params, ROW_LIMIT + 1),
111
+ byAccount: grouped(database, "COALESCE(account_id,'')", clause, params, ROW_LIMIT + 1),
112
+ bySession: grouped(database, 'session_id', clause, params, ROW_LIMIT + 1)
113
+ };
114
+ });
115
+ const estimates = input.rates && zero(summary.total?.unpriced)
116
+ ? await withIndex(input.root, (database) => estimate(database, clause, params, input.rates))
117
+ : new Map();
118
+ const estimated = [...estimates.values()].reduce((sum, entry) => sum + entry.cost, 0);
119
+ const row = (key, values, labelText, detail) => {
120
+ const estimateEntry = estimates.get(key);
121
+ return { key, label: labelText, detail, ...totals(values, estimateEntry?.cost ?? 0) };
122
+ };
123
+ const modelRows = summary.byModel.map((entry) => row(entry.key, entry.values, entry.key, null));
124
+ const truncate = (entries) => entries.length > ROW_LIMIT;
125
+ const accountRows = summary.byAccount.slice(0, ROW_LIMIT).map((entry) => row(entry.key, entry.values, entry.key ? label(entry.key) : 'No account', null));
126
+ const sessionRows = summary.bySession.slice(0, ROW_LIMIT).map((entry) => row(entry.key, entry.values, entry.key.slice(0, 12), null));
127
+ const days = summary.days.map((entry) => ({
128
+ day: entry.day,
129
+ requests: zero(entry.requests),
130
+ inputTokens: zero(entry.input),
131
+ cacheReadTokens: zero(entry.cacheRead),
132
+ outputTokens: zero(entry.output),
133
+ costUsd: entry.cost == null ? null : round(entry.cost)
134
+ }));
135
+ return {
136
+ scope: input.filter.session ? 'session' : input.filter.project ? 'project' : input.filter.account ? 'account' : 'all',
137
+ period: input.period,
138
+ from,
139
+ to,
140
+ timezoneOffsetMinutes: offset,
141
+ generatedAt: new Date().toISOString(),
142
+ totals: totals(summary.total, estimated),
143
+ byDay: days,
144
+ byModel: modelRows.slice(0, ROW_LIMIT),
145
+ byProvider: summary.byProvider.slice(0, ROW_LIMIT).map((entry) => row(entry.key, entry.values, entry.key, null)),
146
+ byAccount: accountRows,
147
+ bySession: sessionRows,
148
+ truncated: truncate(summary.byModel) || truncate(summary.byProvider) || truncate(summary.byAccount) || truncate(summary.bySession),
149
+ lastIndexedAt: lastIndexedAt(input.root)
150
+ };
151
+ }
@@ -0,0 +1,38 @@
1
+ import { type RunEvent, type RunRecord } from "../sessions/contracts.js";
2
+ export interface UsageRow {
3
+ runId: string;
4
+ sequence: number;
5
+ requestIndex: number;
6
+ at: string;
7
+ day: string;
8
+ accountId: string | null;
9
+ projectId: string | null;
10
+ sessionId: string;
11
+ agentId: string | null;
12
+ mode: string;
13
+ trigger: string | null;
14
+ provider: string | null;
15
+ model: string | null;
16
+ inputTokens: number;
17
+ cacheReadTokens: number;
18
+ cacheWriteTokens: number;
19
+ outputTokens: number;
20
+ reasoningTokens: number;
21
+ costUsd: number | null;
22
+ costSource: string | null;
23
+ contextTokens: number | null;
24
+ contextWindow: number | null;
25
+ durationMs: number | null;
26
+ ttftMs: number | null;
27
+ requests: number;
28
+ }
29
+ /** One row per request: the event's own `request`, or the run's totals when a provider reported only those. */
30
+ export declare function usageRowsFor(run: RunRecord, events: readonly RunEvent[]): UsageRow[];
31
+ /** Indexes one run's usage. Idempotent, and safe to call when the index is unavailable. */
32
+ export declare function indexRunUsage(root: string, runId: string): boolean;
33
+ /** Indexes every run the ledger has not seen yet. Returns how many runs it added. */
34
+ export declare function syncUsage(root: string): number;
35
+ /** The ledger's last write, or null when it holds nothing. */
36
+ export declare function lastIndexedAt(root: string): string | null;
37
+ /** Run ids the ledger knows, for diagnostics and tests. */
38
+ export declare function indexedRuns(root: string): string[];
@@ -0,0 +1,142 @@
1
+ import { all, run as execute, transaction, withIndex } from "../core/index.js";
2
+ import { now } from "../core/fs.js";
3
+ import { readAllRuns } from "../sessions/runIndex.js";
4
+ import { findRun, readEvents } from "../sessions/runStore.js";
5
+ import { usagePayloadSchema } from "../sessions/contracts.js";
6
+ const number = (value) => (typeof value === 'number' && Number.isFinite(value) ? value : 0);
7
+ const optionalNumber = (value) => (typeof value === 'number' && Number.isFinite(value) ? value : null);
8
+ const text = (value) => (typeof value === 'string' && value ? value : null);
9
+ /** One row per request: the event's own `request`, or the run's totals when a provider reported only those. */
10
+ export function usageRowsFor(run, events) {
11
+ const rows = [];
12
+ let lastTotals = null;
13
+ let lastTotalsAt = null;
14
+ for (const event of events) {
15
+ if (event.type !== 'usage.updated')
16
+ continue;
17
+ const parsed = usagePayloadSchema.safeParse(event.payload);
18
+ if (!parsed.success)
19
+ continue;
20
+ const payload = parsed.data;
21
+ lastTotals = payload;
22
+ lastTotalsAt = event.timestamp;
23
+ const request = payload.request;
24
+ // Without a per-request update the event's fields are cumulative; only the last one may count.
25
+ if (!request)
26
+ continue;
27
+ rows.push({
28
+ runId: run.id,
29
+ sequence: event.sequence,
30
+ requestIndex: request?.index ?? payload.requests ?? rows.length + 1,
31
+ at: event.timestamp,
32
+ day: event.timestamp.slice(0, 10),
33
+ accountId: run.accountId ?? null,
34
+ projectId: run.projectId ?? null,
35
+ sessionId: run.sessionId,
36
+ agentId: run.agentId ?? null,
37
+ mode: run.mode ?? 'chat',
38
+ trigger: run.trigger ?? null,
39
+ provider: request?.provider ?? payload.provider ?? null,
40
+ model: request?.model ?? payload.model ?? null,
41
+ inputTokens: number(request?.inputTokens ?? payload.inputTokens),
42
+ cacheReadTokens: number(request?.cacheReadTokens ?? payload.cacheReadTokens),
43
+ cacheWriteTokens: number(request?.cacheWriteTokens ?? payload.cacheWriteTokens),
44
+ outputTokens: number(request?.outputTokens ?? payload.outputTokens),
45
+ reasoningTokens: number(request?.reasoningTokens ?? payload.reasoningTokens),
46
+ costUsd: optionalNumber(request?.costUsd ?? payload.costUsd),
47
+ costSource: text(request?.costSource ?? payload.costSource),
48
+ contextTokens: optionalNumber(payload.contextTokens),
49
+ contextWindow: optionalNumber(payload.contextWindow),
50
+ durationMs: optionalNumber(request?.durationMs),
51
+ ttftMs: optionalNumber(request?.ttftMs),
52
+ requests: number(payload.requests)
53
+ });
54
+ }
55
+ // A run whose provider reported totals but no per-request update (an older record) still counts once.
56
+ if (!rows.length && lastTotals) {
57
+ rows.push({
58
+ runId: run.id, sequence: 0, requestIndex: lastTotals.requests ?? 1,
59
+ at: lastTotalsAt ?? run.endedAt ?? run.startedAt, day: (lastTotalsAt ?? run.endedAt ?? run.startedAt).slice(0, 10),
60
+ accountId: run.accountId ?? null, projectId: run.projectId ?? null, sessionId: run.sessionId,
61
+ agentId: run.agentId ?? null, mode: run.mode ?? 'chat', trigger: run.trigger ?? null,
62
+ provider: lastTotals.provider ?? null, model: lastTotals.model ?? null,
63
+ inputTokens: number(lastTotals.inputTokens), cacheReadTokens: number(lastTotals.cacheReadTokens),
64
+ cacheWriteTokens: number(lastTotals.cacheWriteTokens), outputTokens: number(lastTotals.outputTokens),
65
+ reasoningTokens: number(lastTotals.reasoningTokens), costUsd: optionalNumber(lastTotals.costUsd),
66
+ costSource: text(lastTotals.costSource), contextTokens: optionalNumber(lastTotals.contextTokens),
67
+ contextWindow: optionalNumber(lastTotals.contextWindow), durationMs: null, ttftMs: null,
68
+ requests: number(lastTotals.requests)
69
+ });
70
+ }
71
+ return rows;
72
+ }
73
+ function write(database, run, rows) {
74
+ execute(database, 'DELETE FROM usage_requests WHERE run_id = ?', run.id);
75
+ for (const row of rows) {
76
+ execute(database, `INSERT INTO usage_requests (run_id, sequence, request_index, at, day, account_id, project_id, session_id, agent_id, mode, trigger,
77
+ provider, model, input_tokens, cache_read_tokens, cache_write_tokens, output_tokens, reasoning_tokens,
78
+ cost_usd, cost_source, context_tokens, context_window, duration_ms, ttft_ms, requests)
79
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, row.runId, row.sequence, row.requestIndex, row.at, row.day, row.accountId, row.projectId, row.sessionId, row.agentId, row.mode, row.trigger, row.provider, row.model, row.inputTokens, row.cacheReadTokens, row.cacheWriteTokens, row.outputTokens, row.reasoningTokens, row.costUsd, row.costSource, row.contextTokens, row.contextWindow, row.durationMs, row.ttftMs, row.requests);
80
+ }
81
+ execute(database, 'INSERT OR REPLACE INTO usage_indexed (run_id, indexed_at, rows) VALUES (?, ?, ?)', run.id, now(), rows.length);
82
+ }
83
+ /** Indexes one run's usage. Idempotent, and safe to call when the index is unavailable. */
84
+ export function indexRunUsage(root, runId) {
85
+ const run = findRun(root, runId);
86
+ if (!run)
87
+ return false;
88
+ let events;
89
+ try {
90
+ events = readEvents(root, runId);
91
+ }
92
+ catch {
93
+ return false;
94
+ }
95
+ const rows = usageRowsFor(run, events);
96
+ try {
97
+ withIndex(root, (database) => transaction(database, () => write(database, run, rows)));
98
+ return true;
99
+ }
100
+ catch {
101
+ return false;
102
+ }
103
+ }
104
+ /** Indexes every run the ledger has not seen yet. Returns how many runs it added. */
105
+ export function syncUsage(root) {
106
+ const indexed = new Set();
107
+ try {
108
+ withIndex(root, (database) => {
109
+ for (const row of all(database, 'SELECT run_id FROM usage_indexed'))
110
+ indexed.add(row.run_id);
111
+ });
112
+ }
113
+ catch {
114
+ return 0;
115
+ }
116
+ let added = 0;
117
+ for (const record of readAllRuns(root)) {
118
+ if (indexed.has(record.id))
119
+ continue;
120
+ if (indexRunUsage(root, record.id))
121
+ added += 1;
122
+ }
123
+ return added;
124
+ }
125
+ /** The ledger's last write, or null when it holds nothing. */
126
+ export function lastIndexedAt(root) {
127
+ try {
128
+ return withIndex(root, (database) => all(database, 'SELECT MAX(indexed_at) AS at FROM usage_indexed')[0]?.at ?? null);
129
+ }
130
+ catch {
131
+ return null;
132
+ }
133
+ }
134
+ /** Run ids the ledger knows, for diagnostics and tests. */
135
+ export function indexedRuns(root) {
136
+ try {
137
+ return withIndex(root, (database) => all(database, 'SELECT run_id FROM usage_indexed').map((row) => row.run_id));
138
+ }
139
+ catch {
140
+ return [];
141
+ }
142
+ }
@@ -0,0 +1,11 @@
1
+ import type { ProviderLimitAdapter } from "./types.js";
2
+ export declare const OPENROUTER_ADAPTER: ProviderLimitAdapter;
3
+ export declare const DEEPSEEK_ADAPTER: ProviderLimitAdapter;
4
+ export declare const ANTHROPIC_ADMIN_ADAPTER: ProviderLimitAdapter;
5
+ /** ChatGPT/Codex plan windows and credits, from the account's own usage endpoint. */
6
+ export declare const CODEX_ADAPTER: ProviderLimitAdapter;
7
+ /** OpenCode Go's 5-hour, weekly and monthly subscription windows. */
8
+ export declare const OPENCODE_GO_ADAPTER: ProviderLimitAdapter;
9
+ /** The documented adapter for a provider, when one exists. Headers remain the other source. */
10
+ export declare function adapterFor(provider: string): ProviderLimitAdapter | null;
11
+ export declare const DOCUMENTED_ADAPTERS: readonly ProviderLimitAdapter[];