@nyte-ai/host 0.0.0-stage → 0.0.18

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 (235) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +6 -2
  3. package/THIRD-PARTY-NOTICES.md +800 -0
  4. package/dist/chunks/canvas-preview-0cmbkp2j.js +65 -0
  5. package/dist/chunks/canvas-preview-0nq75h9k.js +7 -0
  6. package/dist/chunks/canvas-preview-0nrnbt76.js +497 -0
  7. package/dist/chunks/canvas-preview-1sq7z9jq.js +17 -0
  8. package/dist/chunks/canvas-preview-1ta3avah.js +56 -0
  9. package/dist/chunks/canvas-preview-2zdyh9ny.js +26 -0
  10. package/dist/chunks/canvas-preview-31w7xm8z.js +34 -0
  11. package/dist/chunks/canvas-preview-4b6sayhg.js +295 -0
  12. package/dist/chunks/canvas-preview-4e0pxa32.js +63 -0
  13. package/dist/chunks/canvas-preview-518dh8by.js +169 -0
  14. package/dist/chunks/canvas-preview-5hxat7hh.js +239 -0
  15. package/dist/chunks/canvas-preview-5mex725c.js +14 -0
  16. package/dist/chunks/canvas-preview-5z63am63.js +295 -0
  17. package/dist/chunks/canvas-preview-65xqjw7f.js +20 -0
  18. package/dist/chunks/canvas-preview-69fkk9yn.js +95 -0
  19. package/dist/chunks/canvas-preview-75db60xy.js +457 -0
  20. package/dist/chunks/canvas-preview-76fwvd6e.js +272 -0
  21. package/dist/chunks/canvas-preview-8xgt67ka.js +598 -0
  22. package/dist/chunks/canvas-preview-98aj5p34.js +176 -0
  23. package/dist/chunks/canvas-preview-9aqczacs.js +73 -0
  24. package/dist/chunks/canvas-preview-a3yw0j70.js +72 -0
  25. package/dist/chunks/canvas-preview-azg56359.js +68 -0
  26. package/dist/chunks/canvas-preview-d9wwhbtz.js +240 -0
  27. package/dist/chunks/canvas-preview-ehf3vp31.js +411 -0
  28. package/dist/chunks/canvas-preview-evh92ywd.js +369 -0
  29. package/dist/chunks/canvas-preview-ewgspmsd.js +81 -0
  30. package/dist/chunks/canvas-preview-g3qj3y3k.js +156 -0
  31. package/dist/chunks/canvas-preview-h8hshnff.js +63 -0
  32. package/dist/chunks/canvas-preview-j1tnf51w.js +36 -0
  33. package/dist/chunks/canvas-preview-kdqgrstc.js +194 -0
  34. package/dist/chunks/canvas-preview-kn7kvbfj.js +12 -0
  35. package/dist/chunks/canvas-preview-m02v4yy6.js +386 -0
  36. package/dist/chunks/canvas-preview-mgcx2b2p.js +271 -0
  37. package/dist/chunks/canvas-preview-rp4x6y1h.js +144 -0
  38. package/dist/chunks/canvas-preview-rp6fz0w5.js +130 -0
  39. package/dist/chunks/canvas-preview-s94ebwf5.js +112 -0
  40. package/dist/chunks/canvas-preview-senpjwep.js +518 -0
  41. package/dist/chunks/canvas-preview-skzwvw4d.js +23 -0
  42. package/dist/chunks/canvas-preview-srgfpdmf.js +60 -0
  43. package/dist/chunks/canvas-preview-tjhabynd.js +252 -0
  44. package/dist/chunks/canvas-preview-vaatfznm.js +189 -0
  45. package/dist/chunks/canvas-preview-vax9ne4e.js +341 -0
  46. package/dist/chunks/canvas-preview-wg80rbc2.js +46 -0
  47. package/dist/chunks/canvas-preview-xn10d4f8.js +608 -0
  48. package/dist/chunks/canvas-preview-yjz0f7n4.js +50 -0
  49. package/dist/chunks/canvas-preview-ymgz0937.js +587 -0
  50. package/dist/chunks/canvas-preview-yrfz2ak6.js +134 -0
  51. package/dist/chunks/canvas-preview-zaaj99wd.js +244 -0
  52. package/dist/chunks/canvas-preview-zn2d0k9d.js +133 -0
  53. package/dist/src/canvas-preview.d.ts +29 -0
  54. package/dist/src/canvas-preview.js +267 -0
  55. package/dist/src/credentials.d.ts +44 -0
  56. package/dist/src/credentials.js +10 -0
  57. package/dist/src/deploy/cloudflare/worker.d.ts +50 -0
  58. package/dist/src/deploy/cloudflare/worker.js +196 -0
  59. package/dist/src/deploy/vercel/nitro.config.d.ts +2 -0
  60. package/dist/src/deploy/vercel/nitro.config.js +30 -0
  61. package/dist/src/deploy/vercel/server/routes/[...].d.ts +2 -0
  62. package/dist/src/deploy/vercel/server/routes/[...].js +18 -0
  63. package/dist/src/deploy/vercel/server/utils/host.d.ts +13 -0
  64. package/dist/src/deploy/vercel/server/utils/host.js +7 -0
  65. package/dist/src/deploy/vercel/server/utils/repair.d.ts +1 -0
  66. package/dist/src/deploy/vercel/server/utils/repair.js +6 -0
  67. package/dist/src/deploy/vercel/server/utils/step.d.ts +10 -0
  68. package/dist/src/deploy/vercel/server/utils/step.js +6 -0
  69. package/dist/src/deploy/vercel/server/workflows/drive.d.ts +14 -0
  70. package/dist/src/deploy/vercel/server/workflows/drive.js +13 -0
  71. package/dist/src/durable-object.d.ts +14 -0
  72. package/dist/src/durable-object.js +231 -0
  73. package/dist/src/hash.d.ts +1 -0
  74. package/dist/src/hash.js +6 -0
  75. package/dist/src/identity-key.d.ts +11 -0
  76. package/dist/src/identity-key.js +9 -0
  77. package/dist/src/identity.d.ts +12 -0
  78. package/dist/src/identity.js +6 -0
  79. package/dist/src/index.d.ts +21 -0
  80. package/dist/src/index.js +120 -0
  81. package/dist/src/key-file.d.ts +1 -0
  82. package/dist/src/key-file.js +7 -0
  83. package/dist/src/listen-fetch.d.ts +8 -0
  84. package/dist/src/listen-fetch.js +9 -0
  85. package/dist/src/listen.d.ts +4 -0
  86. package/dist/src/listen.js +10 -0
  87. package/dist/src/local/command.d.ts +15 -0
  88. package/dist/src/local/command.js +8 -0
  89. package/dist/src/local/feed.d.ts +7 -0
  90. package/dist/src/local/feed.js +6 -0
  91. package/dist/src/local/files.d.ts +18 -0
  92. package/dist/src/local/files.js +13 -0
  93. package/dist/src/local/git-watch.d.ts +15 -0
  94. package/dist/src/local/git-watch.js +7 -0
  95. package/dist/src/local/git.d.ts +5 -0
  96. package/dist/src/local/git.js +13 -0
  97. package/dist/src/local/github.d.ts +30 -0
  98. package/dist/src/local/github.js +9 -0
  99. package/dist/src/local/identity.d.ts +3 -0
  100. package/dist/src/local/identity.js +7 -0
  101. package/dist/src/local/jobs.d.ts +7 -0
  102. package/dist/src/local/jobs.js +6 -0
  103. package/dist/src/local/legacy-trust.d.ts +10 -0
  104. package/dist/src/local/legacy-trust.js +6 -0
  105. package/dist/src/local/patch.d.ts +16 -0
  106. package/dist/src/local/patch.js +17 -0
  107. package/dist/src/local/provider.d.ts +11 -0
  108. package/dist/src/local/provider.js +20 -0
  109. package/dist/src/local/snapshots.d.ts +7 -0
  110. package/dist/src/local/snapshots.js +11 -0
  111. package/dist/src/local/text.d.ts +1 -0
  112. package/dist/src/local/text.js +6 -0
  113. package/dist/src/local/tree.d.ts +50 -0
  114. package/dist/src/local/tree.js +16 -0
  115. package/dist/src/local/workspace-migration.d.ts +2 -0
  116. package/dist/src/local/workspace-migration.js +7 -0
  117. package/dist/src/local/worktrees.d.ts +46 -0
  118. package/dist/src/local/worktrees.js +14 -0
  119. package/dist/src/mcp/config.d.ts +75 -0
  120. package/dist/src/mcp/config.js +12 -0
  121. package/dist/src/mcp/connect.d.ts +28 -0
  122. package/dist/src/mcp/connect.js +9 -0
  123. package/dist/src/mcp/hub.d.ts +19 -0
  124. package/dist/src/mcp/hub.js +11 -0
  125. package/dist/src/mcp/oauth.d.ts +46 -0
  126. package/dist/src/mcp/oauth.js +12 -0
  127. package/dist/src/mcp/tools.d.ts +29 -0
  128. package/dist/src/mcp/tools.js +12 -0
  129. package/dist/src/open.d.ts +42 -0
  130. package/dist/src/open.js +44 -0
  131. package/dist/src/plugins/loader.d.ts +21 -0
  132. package/dist/src/plugins/loader.js +11 -0
  133. package/dist/src/postgres.d.ts +19 -0
  134. package/dist/src/postgres.js +8 -0
  135. package/dist/src/providers.d.ts +16 -0
  136. package/dist/src/providers.js +112 -0
  137. package/dist/src/secrets.d.ts +12 -0
  138. package/dist/src/secrets.js +8 -0
  139. package/dist/src/serve/binding.d.ts +39 -0
  140. package/dist/src/serve/binding.js +6 -0
  141. package/dist/src/serve/handler.d.ts +15 -0
  142. package/dist/src/serve/handler.js +8 -0
  143. package/dist/src/serve/stream-server.d.ts +6 -0
  144. package/dist/src/serve/stream-server.js +6 -0
  145. package/dist/src/session-index.d.ts +11 -0
  146. package/dist/src/session-index.js +11 -0
  147. package/dist/src/shell-env.d.ts +14 -0
  148. package/dist/src/shell-env.js +108 -0
  149. package/dist/src/sqlite-workers.d.ts +6 -0
  150. package/dist/src/sqlite-workers.js +6 -0
  151. package/dist/src/sqlite.d.ts +7 -0
  152. package/dist/src/sqlite.js +10 -0
  153. package/dist/src/store-owner.d.ts +15 -0
  154. package/dist/src/store-owner.js +9 -0
  155. package/dist/src/store-rows.d.ts +13 -0
  156. package/dist/src/store-rows.js +26 -0
  157. package/dist/src/store-usage.d.ts +25 -0
  158. package/dist/src/store-usage.js +16 -0
  159. package/dist/src/usage-scan.d.ts +24 -0
  160. package/dist/src/usage-scan.js +65 -0
  161. package/dist/src/usage-service.d.ts +22 -0
  162. package/dist/src/usage-service.js +12 -0
  163. package/dist/src/usage-summary.d.ts +9 -0
  164. package/dist/src/usage-summary.js +16 -0
  165. package/dist/src/usage-types.d.ts +86 -0
  166. package/dist/src/usage-types.js +0 -0
  167. package/dist/src/usage-worker.d.ts +1 -0
  168. package/dist/src/usage-worker.js +25 -0
  169. package/dist/src/usage.d.ts +72 -0
  170. package/dist/src/usage.js +13 -0
  171. package/package.json +132 -3
  172. package/src/canvas-preview.ts +402 -0
  173. package/src/credentials.ts +294 -0
  174. package/src/deploy/cloudflare/README.md +3 -0
  175. package/src/deploy/cloudflare/worker.ts +306 -0
  176. package/src/deploy/cloudflare/wrangler.jsonc +12 -0
  177. package/src/deploy/vercel/README.md +3 -0
  178. package/src/deploy/vercel/nitro.config.ts +29 -0
  179. package/src/deploy/vercel/server/routes/[...].ts +10 -0
  180. package/src/deploy/vercel/server/utils/host.ts +104 -0
  181. package/src/deploy/vercel/server/utils/repair.ts +35 -0
  182. package/src/deploy/vercel/server/utils/step.ts +25 -0
  183. package/src/deploy/vercel/server/workflows/drive.ts +46 -0
  184. package/src/deploy/vercel/tsconfig.json +9 -0
  185. package/src/deploy/vercel/types/bun.d.ts +2 -0
  186. package/src/durable-object.ts +386 -0
  187. package/src/hash.ts +5 -0
  188. package/src/identity-key.ts +20 -0
  189. package/src/identity.ts +66 -0
  190. package/src/index.ts +21 -0
  191. package/src/key-file.ts +53 -0
  192. package/src/listen-fetch.ts +409 -0
  193. package/src/listen.ts +228 -0
  194. package/src/local/command.ts +109 -0
  195. package/src/local/feed.ts +70 -0
  196. package/src/local/files.ts +812 -0
  197. package/src/local/git-watch.ts +189 -0
  198. package/src/local/git.ts +914 -0
  199. package/src/local/github.ts +692 -0
  200. package/src/local/identity.ts +21 -0
  201. package/src/local/jobs.ts +587 -0
  202. package/src/local/legacy-trust.ts +105 -0
  203. package/src/local/patch.ts +334 -0
  204. package/src/local/provider.ts +184 -0
  205. package/src/local/snapshots.ts +776 -0
  206. package/src/local/text.ts +9 -0
  207. package/src/local/tree.ts +398 -0
  208. package/src/local/workspace-migration.ts +95 -0
  209. package/src/local/worktrees.ts +239 -0
  210. package/src/mcp/config.ts +330 -0
  211. package/src/mcp/connect.ts +105 -0
  212. package/src/mcp/hub.ts +836 -0
  213. package/src/mcp/oauth.ts +436 -0
  214. package/src/mcp/tools.ts +333 -0
  215. package/src/open.ts +469 -0
  216. package/src/plugins/loader.ts +186 -0
  217. package/src/postgres.ts +492 -0
  218. package/src/providers.ts +135 -0
  219. package/src/secrets.ts +31 -0
  220. package/src/serve/binding.ts +628 -0
  221. package/src/serve/handler.ts +362 -0
  222. package/src/serve/stream-server.ts +176 -0
  223. package/src/session-index.ts +99 -0
  224. package/src/shell-env.ts +153 -0
  225. package/src/sqlite-workers.ts +283 -0
  226. package/src/sqlite.ts +528 -0
  227. package/src/store-owner.ts +98 -0
  228. package/src/store-rows.ts +110 -0
  229. package/src/store-usage.ts +344 -0
  230. package/src/usage-scan.ts +81 -0
  231. package/src/usage-service.ts +73 -0
  232. package/src/usage-summary.ts +79 -0
  233. package/src/usage-types.ts +87 -0
  234. package/src/usage-worker.ts +24 -0
  235. package/src/usage.ts +701 -0
@@ -0,0 +1,110 @@
1
+ import { asId, SdkError } from "@nyte-ai/schema";
2
+ import type { ByteReader, Ref, Revision, SessionEvent } from "@nyte-ai/schema";
3
+ import { loggedEvent } from "@nyte-ai/core/store";
4
+ import type { LoggedRow, StoredObject, StoredRecord } from "@nyte-ai/core/store";
5
+
6
+ export function prefixEnd(prefix: string): string | null {
7
+ const points = Array.from(prefix);
8
+ const last = points.pop()?.codePointAt(0);
9
+
10
+ if (last === undefined) return null;
11
+
12
+ if (last === 0x10ffff) return prefixEnd(points.join(""));
13
+
14
+ return points.join("") + String.fromCodePoint(last === 0xd7ff ? 0xe000 : last + 1);
15
+ }
16
+
17
+ export function mintRevision(): Revision {
18
+ return asId<"revision">(crypto.randomUUID());
19
+ }
20
+
21
+ export function decodeEvent(seq: number, at: number, text: string): SessionEvent {
22
+ return loggedEvent(seq, at, JSON.parse(text));
23
+ }
24
+
25
+ export function parseNumber(value: unknown): number {
26
+ if (typeof value === "number") return value;
27
+
28
+ if (typeof value === "bigint" || typeof value === "string") return Number(value);
29
+
30
+ throw new SdkError("internal", "A stored number is malformed.");
31
+ }
32
+
33
+ export function parseText(value: unknown): string | null {
34
+ if (value === null || value === undefined) return null;
35
+
36
+ if (typeof value === "string") return value;
37
+
38
+ throw new SdkError("internal", "A stored text column is malformed.");
39
+ }
40
+
41
+ export function parseRef(row: Readonly<Record<string, unknown>>): Ref {
42
+ const name = parseText(row.name);
43
+ const revision = parseText(row.revision);
44
+ const target = parseText(row.target);
45
+
46
+ if (name === null || revision === null)
47
+ throw new SdkError("internal", "A stored ref is malformed.");
48
+
49
+ return {
50
+ name,
51
+ target: target === null ? null : asId<"object">(target),
52
+ revision: asId<"revision">(revision),
53
+ };
54
+ }
55
+
56
+ export function parseStoredRecord(row: Readonly<Record<string, unknown>>): StoredRecord {
57
+ const name = parseText(row.name);
58
+ const target = parseText(row.target);
59
+ const revision = parseText(row.revision);
60
+ const body = parseText(row.body);
61
+
62
+ if (name === null || target === null || revision === null || body === null)
63
+ throw new SdkError("internal", "A stored ref is malformed.");
64
+
65
+ return { name, target: asId<"object">(target), revision, body };
66
+ }
67
+
68
+ export function parseStoredObject(row: Readonly<Record<string, unknown>>): StoredObject {
69
+ const id = parseText(row.id);
70
+ const body = parseText(row.body);
71
+
72
+ if (id === null || body === null) throw new SdkError("internal", "A stored object is malformed.");
73
+
74
+ return { id: asId<"object">(id), body };
75
+ }
76
+
77
+ export function loggedRow(row: Readonly<Record<string, unknown>>): LoggedRow {
78
+ const event = parseText(row.event);
79
+
80
+ if (event === null) throw new SdkError("internal", "A logged event is malformed.");
81
+
82
+ return { seq: parseNumber(row.seq), event };
83
+ }
84
+
85
+ export async function collect(bytes: AsyncIterable<Uint8Array>): Promise<Uint8Array> {
86
+ const chunks: Uint8Array[] = [];
87
+
88
+ for await (const chunk of bytes) chunks.push(chunk);
89
+ const joined = new Uint8Array(chunks.reduce((size, chunk) => size + chunk.byteLength, 0));
90
+ let offset = 0;
91
+
92
+ for (const chunk of chunks) {
93
+ joined.set(chunk, offset);
94
+ offset += chunk.byteLength;
95
+ }
96
+
97
+ return joined;
98
+ }
99
+
100
+ export function memoryReader(bytes: Uint8Array): ByteReader {
101
+ return {
102
+ size: bytes.byteLength,
103
+ mime: null,
104
+ async *chunks() {
105
+ yield bytes;
106
+ },
107
+ close: async () => undefined,
108
+ [Symbol.asyncDispose]: async () => undefined,
109
+ };
110
+ }
@@ -0,0 +1,344 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { DatabaseSync } from "node:sqlite";
4
+ import {
5
+ decodeObject,
6
+ decodeSessionRecord,
7
+ decodeWorkspaceRecord,
8
+ parseSessionRef,
9
+ } from "@nyte-ai/core/store";
10
+ import type { ModelRef, SessionId, Usage } from "@nyte-ai/schema";
11
+ import type {
12
+ UsageEntry,
13
+ UsageReport,
14
+ UsageSession,
15
+ UsageSource,
16
+ UsageSubject,
17
+ UsageTotals,
18
+ UsageWindow,
19
+ } from "./usage-types.ts";
20
+ import { cacheSavings, usageTokens } from "./usage-summary.ts";
21
+ import { decodeEvent, parseNumber, parseText } from "./store-rows.ts";
22
+
23
+ export interface UsageRecord {
24
+ readonly at: number;
25
+ readonly subject: UsageSubject;
26
+ readonly usage: Usage;
27
+ }
28
+
29
+ export interface SessionUsage {
30
+ readonly sessionId: SessionId;
31
+ readonly name: string | undefined;
32
+ readonly records: readonly UsageRecord[];
33
+ readonly lastActivityAt?: number;
34
+ }
35
+
36
+ export interface StoreRead {
37
+ readonly workspacePath: string | null;
38
+ readonly sessions: readonly SessionUsage[];
39
+ readonly failure: { readonly message: string } | null;
40
+ }
41
+
42
+ const EMPTY_TOTALS: UsageTotals = {
43
+ input: 0,
44
+ output: 0,
45
+ cacheRead: 0,
46
+ cacheWrite: 0,
47
+ reasoning: 0,
48
+ tokens: 0,
49
+ cost: 0,
50
+ cacheSavings: 0,
51
+ };
52
+
53
+ function add(totals: UsageTotals, usage: Usage): UsageTotals {
54
+ return {
55
+ input: totals.input + usage.input,
56
+ output: totals.output + usage.output,
57
+ cacheRead: totals.cacheRead + usage.cacheRead,
58
+ cacheWrite: totals.cacheWrite + usage.cacheWrite,
59
+ reasoning: totals.reasoning + (usage.reasoning ?? 0),
60
+ tokens: totals.tokens + usageTokens(usage),
61
+ cost: totals.cost + usage.cost.total,
62
+ cacheSavings: totals.cacheSavings + cacheSavings(usage),
63
+ };
64
+ }
65
+
66
+ export const USAGE_EVENTS =
67
+ "SELECT session, seq, at, event FROM log WHERE json_extract(event, '$.kind') = 'request' OR (json_extract(event, '$.kind') = 'run' AND json_extract(event, '$.change') = 'started') ORDER BY session, seq";
68
+
69
+ export function localDay(at: number): string {
70
+ const date = new Date(at);
71
+ const month = String(date.getMonth() + 1).padStart(2, "0");
72
+ const day = String(date.getDate()).padStart(2, "0");
73
+
74
+ return `${String(date.getFullYear())}-${month}-${day}`;
75
+ }
76
+
77
+ export function shiftDay(day: string, delta: number): string {
78
+ const date = new Date(`${day}T00:00:00`);
79
+ date.setDate(date.getDate() + delta);
80
+
81
+ return localDay(date.getTime());
82
+ }
83
+
84
+ function priorWindow(
85
+ sinceDay: string | null,
86
+ untilDay: string,
87
+ ): { readonly sinceDay: string; readonly untilDay: string } | undefined {
88
+ if (sinceDay === null) return undefined;
89
+ const priorUntil = shiftDay(sinceDay, -1);
90
+
91
+ const span =
92
+ (Date.parse(`${untilDay}T00:00:00Z`) - Date.parse(`${sinceDay}T00:00:00Z`)) / 86_400_000 + 1;
93
+
94
+ return { sinceDay: shiftDay(priorUntil, -(span - 1)), untilDay: priorUntil };
95
+ }
96
+
97
+ export function projectUsageReport(
98
+ stores: readonly StoreRead[],
99
+ window: UsageWindow,
100
+ readAt: number,
101
+ ): UsageReport {
102
+ const requestedSince = window.sinceDay;
103
+ const untilDay = window.untilDay;
104
+ const prior = priorWindow(requestedSince, untilDay);
105
+
106
+ const cells = new Map<
107
+ string,
108
+ { readonly entry: Omit<UsageEntry, "totals">; totals: UsageTotals }
109
+ >();
110
+
111
+ const sessions = new Map<SessionId, UsageSession>();
112
+ const sources: UsageSource[] = [];
113
+ let priorCost = 0;
114
+ let priorTokens = 0;
115
+ let sawPrior = false;
116
+ let earliestDay: string | undefined;
117
+
118
+ for (const store of stores) {
119
+ sources.push({
120
+ workspacePath: store.workspacePath,
121
+ status: store.failure === null ? "ok" : "failed",
122
+ sessions: store.sessions.length,
123
+ message: store.failure?.message ?? null,
124
+ });
125
+
126
+ for (const session of store.sessions) {
127
+ let lastActivityAt = 0;
128
+ let spentInWindow = false;
129
+
130
+ for (const spend of session.records) {
131
+ const { usage } = spend;
132
+ const amounts = [
133
+ usage.total,
134
+ usage.input,
135
+ usage.output,
136
+ usage.cacheRead,
137
+ usage.cacheWrite,
138
+ usage.cost.total,
139
+ ];
140
+
141
+ if (!amounts.some((amount) => amount > 0)) continue;
142
+ const day = localDay(spend.at);
143
+
144
+ if (earliestDay === undefined || day < earliestDay) earliestDay = day;
145
+
146
+ if (prior !== undefined && day >= prior.sinceDay && day <= prior.untilDay) {
147
+ sawPrior = true;
148
+ priorCost += spend.usage.cost.total;
149
+ priorTokens += usageTokens(spend.usage);
150
+ }
151
+
152
+ if (day > untilDay) continue;
153
+
154
+ if (requestedSince !== null && day < requestedSince) continue;
155
+
156
+ const key = JSON.stringify([day, store.workspacePath, session.sessionId, spend.subject]);
157
+ const cell = cells.get(key);
158
+
159
+ if (cell === undefined) {
160
+ cells.set(key, {
161
+ entry: {
162
+ day,
163
+ workspacePath: store.workspacePath,
164
+ sessionId: session.sessionId,
165
+ subject: spend.subject,
166
+ },
167
+ totals: add(EMPTY_TOTALS, spend.usage),
168
+ });
169
+ } else {
170
+ cell.totals = add(cell.totals, spend.usage);
171
+ }
172
+
173
+ spentInWindow = true;
174
+ lastActivityAt = Math.max(lastActivityAt, spend.at);
175
+ }
176
+
177
+ if (spentInWindow) {
178
+ sessions.set(session.sessionId, {
179
+ sessionId: session.sessionId,
180
+ name: session.name,
181
+ workspacePath: store.workspacePath,
182
+ lastActivityAt: session.lastActivityAt ?? lastActivityAt,
183
+ });
184
+ }
185
+ }
186
+ }
187
+
188
+ return {
189
+ readAt,
190
+ sinceDay: requestedSince ?? earliestDay ?? untilDay,
191
+ untilDay,
192
+ entries: [...cells.values()]
193
+ .map((cell) => ({ ...cell.entry, totals: cell.totals }))
194
+ .toSorted((left, right) => left.day.localeCompare(right.day)),
195
+ sessions: [...sessions.values()],
196
+ sources,
197
+ previous: sawPrior ? { cost: priorCost, tokens: priorTokens } : undefined,
198
+ earliestDay,
199
+ };
200
+ }
201
+
202
+ export function readNyteUsage(home: string, window: UsageWindow): UsageReport {
203
+ const readAt = Date.now();
204
+ const path = join(home, ".nyte", "nyte.db");
205
+
206
+ if (!existsSync(path)) return projectUsageReport([], window, readAt);
207
+ let database: DatabaseSync | undefined;
208
+
209
+ try {
210
+ database = new DatabaseSync(path, { readOnly: true, timeout: 5_000 });
211
+ database.exec("BEGIN");
212
+ const workspaces = new Map<string, string | null>();
213
+
214
+ const sessions = new Map<
215
+ SessionId,
216
+ {
217
+ readonly workspacePath: string | null;
218
+ readonly name: string | undefined;
219
+ readonly lastActivityAt: number | undefined;
220
+ readonly records: UsageRecord[];
221
+ }
222
+ >();
223
+
224
+ const refs = database
225
+ .prepare(
226
+ "SELECT refs.name, objects.body FROM refs JOIN objects ON objects.id = refs.target WHERE refs.name LIKE 'workspaces/%' OR (refs.name LIKE 'sessions/%' AND instr(substr(refs.name, 10), '/') = 0)",
227
+ )
228
+ .all();
229
+
230
+ for (const row of refs) {
231
+ const name = parseText(row.name);
232
+ const body = parseText(row.body);
233
+
234
+ if (name === null || body === null || !name.startsWith("workspaces/")) continue;
235
+ const workspace = decodeWorkspaceRecord(decodeObject(body));
236
+
237
+ if (workspace !== null)
238
+ workspaces.set(
239
+ workspace.id,
240
+ workspace.target.kind === "local" && "path" in workspace.target
241
+ ? parseText(workspace.target.path)
242
+ : null,
243
+ );
244
+ }
245
+
246
+ for (const row of refs) {
247
+ const name = parseText(row.name);
248
+ const body = parseText(row.body);
249
+
250
+ if (name === null || body === null || name.split("/").length !== 2) continue;
251
+ const id = parseSessionRef(name);
252
+
253
+ if (id === null) continue;
254
+ const session = decodeSessionRecord(decodeObject(body));
255
+
256
+ if (session === null) throw new Error(`Cannot read session ${id}.`);
257
+ sessions.set(id, {
258
+ workspacePath: workspaces.get(session.workspace) ?? null,
259
+ name: session.name ?? undefined,
260
+ lastActivityAt: session.lastActivityAt,
261
+ records: [],
262
+ });
263
+ }
264
+
265
+ const models = new Map<SessionId, Map<string | null, ModelRef>>();
266
+
267
+ for (const row of database.prepare(USAGE_EVENTS).iterate()) {
268
+ const session = parseSessionRef(`sessions/${parseText(row.session) ?? ""}`);
269
+
270
+ if (session === null) throw new Error("Cannot read usage session.");
271
+ const event = decodeEvent(
272
+ parseNumber(row.seq),
273
+ parseNumber(row.at),
274
+ parseText(row.event) ?? "",
275
+ );
276
+ let heads = models.get(session);
277
+
278
+ if (heads === undefined) {
279
+ heads = new Map();
280
+ models.set(session, heads);
281
+ }
282
+
283
+ if (event.kind === "run" && event.change === "started")
284
+ heads.set(event.head, event.view.configuration.model);
285
+
286
+ if (event.kind !== "request") continue;
287
+ const model = heads.get(event.head);
288
+
289
+ const subject: UsageSubject =
290
+ event.step === "compaction" || event.step === "summary"
291
+ ? { kind: "compaction" }
292
+ : event.step === "plugin"
293
+ ? { kind: "tool" }
294
+ : {
295
+ kind: "model",
296
+ provider: model?.provider ?? "unknown",
297
+ model: model?.id ?? "unknown",
298
+ };
299
+
300
+ let info = sessions.get(session);
301
+
302
+ if (info === undefined) {
303
+ info = { workspacePath: null, name: undefined, lastActivityAt: undefined, records: [] };
304
+ sessions.set(session, info);
305
+ }
306
+
307
+ info.records.push({ at: event.at, subject, usage: event.usage });
308
+ }
309
+
310
+ database.exec("COMMIT");
311
+ const stores = new Map<string | null, SessionUsage[]>();
312
+
313
+ for (const [sessionId, info] of sessions) {
314
+ const group = stores.get(info.workspacePath) ?? [];
315
+ group.push({
316
+ sessionId,
317
+ name: info.name,
318
+ records: info.records,
319
+ lastActivityAt: info.lastActivityAt,
320
+ });
321
+ stores.set(info.workspacePath, group);
322
+ }
323
+
324
+ return projectUsageReport(
325
+ [...stores].map(([workspacePath, sessions]) => ({ workspacePath, sessions, failure: null })),
326
+ window,
327
+ readAt,
328
+ );
329
+ } catch (cause) {
330
+ return projectUsageReport(
331
+ [
332
+ {
333
+ workspacePath: null,
334
+ sessions: [],
335
+ failure: { message: cause instanceof Error ? cause.message : String(cause) },
336
+ },
337
+ ],
338
+ window,
339
+ readAt,
340
+ );
341
+ } finally {
342
+ database?.close();
343
+ }
344
+ }
@@ -0,0 +1,81 @@
1
+ import { Worker } from "node:worker_threads";
2
+ import type { UsageScan, UsageScanReader, UsageScanRequest } from "./usage-service.ts";
3
+
4
+ export interface UsageWorkerRequest extends UsageScanRequest {
5
+ readonly id: number;
6
+ readonly home: string;
7
+ }
8
+
9
+ export type UsageWorkerReply =
10
+ | { readonly id: number; readonly scan: UsageScan }
11
+ | { readonly id: number; readonly error: string };
12
+
13
+ export class UsageScanWorker implements UsageScanReader {
14
+ private readonly home: string;
15
+ private readonly entry: string | URL;
16
+ private worker: Worker | undefined;
17
+ private nextId = 1;
18
+ private readonly pending = new Map<
19
+ number,
20
+ { resolve: (scan: UsageScan) => void; reject: (cause: unknown) => void }
21
+ >();
22
+
23
+ constructor(home: string, entry: string | URL) {
24
+ this.home = home;
25
+ this.entry = entry;
26
+ }
27
+
28
+ async scan(request: UsageScanRequest): Promise<UsageScan> {
29
+ const worker = this.spawn();
30
+ const id = this.nextId++;
31
+
32
+ return new Promise((resolve, reject) => {
33
+ this.pending.set(id, { resolve, reject });
34
+
35
+ try {
36
+ worker.postMessage({ id, home: this.home, ...request } satisfies UsageWorkerRequest);
37
+ } catch (cause) {
38
+ this.pending.delete(id);
39
+ reject(cause);
40
+ }
41
+ });
42
+ }
43
+
44
+ async close(): Promise<void> {
45
+ const worker = this.worker;
46
+
47
+ if (worker === undefined) return;
48
+
49
+ this.fail(worker, new Error("Usage worker closed."));
50
+ await worker.terminate();
51
+ }
52
+
53
+ private spawn(): Worker {
54
+ if (this.worker !== undefined) return this.worker;
55
+ const worker = new Worker(this.entry);
56
+ worker.unref();
57
+ worker.on("message", (reply: UsageWorkerReply) => {
58
+ const request = this.pending.get(reply.id);
59
+ this.pending.delete(reply.id);
60
+
61
+ if ("scan" in reply) request?.resolve(reply.scan);
62
+ else request?.reject(new Error(reply.error));
63
+ });
64
+
65
+ worker.on("error", (cause) => this.fail(worker, cause));
66
+ worker.on("exit", (code) => {
67
+ this.fail(worker, new Error(`Usage worker exited with code ${String(code)}.`));
68
+ });
69
+ this.worker = worker;
70
+
71
+ return worker;
72
+ }
73
+
74
+ private fail(worker: Worker, cause: unknown): void {
75
+ if (this.worker !== worker) return;
76
+ this.worker = undefined;
77
+
78
+ for (const request of this.pending.values()) request.reject(cause);
79
+ this.pending.clear();
80
+ }
81
+ }
@@ -0,0 +1,73 @@
1
+ import { join } from "node:path";
2
+ import { FileModelsStore, nyteProviders } from "@nyte-ai/ai";
3
+ import { createModels } from "@nyte-ai/ai/models";
4
+ import type { Models } from "@nyte-ai/ai";
5
+ import { serial } from "@nyte-ai/schema/serial";
6
+ import type { Api, Model } from "@nyte-ai/schema/model";
7
+ import { readNyteUsage } from "./store-usage.ts";
8
+ import { createUsageScanCaches, readLocalUsage } from "./usage.ts";
9
+ import type { UsageSnapshot, UsageWindow } from "./usage-types.ts";
10
+
11
+ export interface UsageScanRequest {
12
+ readonly window: UsageWindow;
13
+ readonly catalog?: readonly Model<Api>[];
14
+ }
15
+
16
+ export type UsageScan = UsageSnapshot;
17
+
18
+ export interface UsageScanReader {
19
+ scan(request: UsageScanRequest): Promise<UsageScan>;
20
+ close(): Promise<void>;
21
+ }
22
+
23
+ export function catalogForUsage(models: Pick<Models, "getModels">): readonly Model<Api>[] {
24
+ return [...models.getModels("anthropic"), ...models.getModels("openai-codex")];
25
+ }
26
+
27
+ export class UsageScanner implements UsageScanReader {
28
+ private readonly home: string;
29
+ private readonly caches = createUsageScanCaches();
30
+ private readonly models: ReturnType<typeof createModels>;
31
+ private readonly scans = serial();
32
+
33
+ constructor(home: string) {
34
+ this.home = home;
35
+ this.models = createModels({
36
+ modelsStore: new FileModelsStore(join(home, ".nyte", "models-store.json")),
37
+ });
38
+
39
+ for (const provider of nyteProviders()) this.models.setProvider(provider);
40
+ }
41
+
42
+ scan(request: UsageScanRequest): Promise<UsageScan> {
43
+ return this.scans.run(async () => {
44
+ if (request.catalog === undefined)
45
+ await this.models.refresh({
46
+ providers: ["anthropic", "openai-codex"],
47
+ allowNetwork: false,
48
+ });
49
+ const catalog = request.catalog;
50
+
51
+ const models: Pick<Models, "getModels"> =
52
+ catalog === undefined
53
+ ? this.models
54
+ : {
55
+ getModels: (provider) =>
56
+ catalog.filter((model) => provider === undefined || model.provider === provider),
57
+ };
58
+
59
+ const local = await readLocalUsage({ models, caches: this.caches });
60
+ const report = readNyteUsage(this.home, request.window);
61
+
62
+ return {
63
+ ...report,
64
+ ...local,
65
+ nyteError: report.sources.find((source) => source.status === "failed")?.message ?? null,
66
+ };
67
+ });
68
+ }
69
+
70
+ close(): Promise<void> {
71
+ return this.scans.settled();
72
+ }
73
+ }
@@ -0,0 +1,79 @@
1
+ import { addUsage } from "@nyte-ai/core/server";
2
+ import type { Usage } from "@nyte-ai/schema";
3
+ import type { Usage as ModelUsageCounts } from "@nyte-ai/schema/model";
4
+ import type { ModelUsage, UsageSummary } from "./usage-types.ts";
5
+
6
+ export function emptyModelUsage(): ModelUsageCounts {
7
+ return {
8
+ input: 0,
9
+ output: 0,
10
+ cacheRead: 0,
11
+ cacheWrite: 0,
12
+ totalTokens: 0,
13
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
14
+ };
15
+ }
16
+
17
+ export function recordedUsage(usage: ModelUsageCounts): Usage {
18
+ return {
19
+ input: usage.input,
20
+ output: usage.output,
21
+ cacheRead: usage.cacheRead,
22
+ cacheWrite: usage.cacheWrite,
23
+ reasoning: usage.reasoning ?? null,
24
+ total: usage.totalTokens,
25
+ cost: { ...usage.cost },
26
+ };
27
+ }
28
+
29
+ export function emptyUsageSummary(): UsageSummary {
30
+ return {
31
+ models: [],
32
+ compaction: recordedUsage(emptyModelUsage()),
33
+ tools: recordedUsage(emptyModelUsage()),
34
+ total: recordedUsage(emptyModelUsage()),
35
+ cacheSavings: 0,
36
+ };
37
+ }
38
+
39
+ export function summarizeModels(rows: readonly ModelUsage[]): UsageSummary {
40
+ const models = new Map<string, ModelUsage>();
41
+ let total = recordedUsage(emptyModelUsage());
42
+ let savings = 0;
43
+
44
+ for (const row of rows) {
45
+ savings += cacheSavings(row.usage);
46
+ const key = JSON.stringify([row.provider, row.model]);
47
+ const previous = models.get(key);
48
+ models.set(
49
+ key,
50
+ previous === undefined
51
+ ? row
52
+ : { ...row, turns: previous.turns + row.turns, usage: addUsage(previous.usage, row.usage) },
53
+ );
54
+ total = addUsage(total, row.usage);
55
+ }
56
+
57
+ return {
58
+ ...emptyUsageSummary(),
59
+ models: [...models.values()].toSorted(
60
+ (left, right) =>
61
+ right.usage.cost.total - left.usage.cost.total ||
62
+ right.usage.total - left.usage.total ||
63
+ left.model.localeCompare(right.model) ||
64
+ left.provider.localeCompare(right.provider),
65
+ ),
66
+ total,
67
+ cacheSavings: savings,
68
+ };
69
+ }
70
+
71
+ export function cacheSavings(usage: Usage): number {
72
+ if (usage.input === 0) return 0;
73
+
74
+ return usage.cacheRead * (usage.cost.input / usage.input) - usage.cost.cacheRead;
75
+ }
76
+
77
+ export function usageTokens(usage: Usage): number {
78
+ return usage.total || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
79
+ }