@nyte-ai/host 0.0.0-stage → 0.0.19
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.
- package/LICENSE +21 -0
- package/README.md +6 -2
- package/THIRD-PARTY-NOTICES.md +866 -0
- package/dist/chunks/canvas-preview-0cmbkp2j.js +65 -0
- package/dist/chunks/canvas-preview-0nq75h9k.js +7 -0
- package/dist/chunks/canvas-preview-11nadadc.js +241 -0
- package/dist/chunks/canvas-preview-1sq7z9jq.js +17 -0
- package/dist/chunks/canvas-preview-2jwbe14k.js +130 -0
- package/dist/chunks/canvas-preview-2zdyh9ny.js +26 -0
- package/dist/chunks/canvas-preview-31w7xm8z.js +34 -0
- package/dist/chunks/canvas-preview-4b6sayhg.js +295 -0
- package/dist/chunks/canvas-preview-4e0pxa32.js +63 -0
- package/dist/chunks/canvas-preview-518dh8by.js +169 -0
- package/dist/chunks/canvas-preview-55s0y68g.js +387 -0
- package/dist/chunks/canvas-preview-5ekjx6sw.js +613 -0
- package/dist/chunks/canvas-preview-5mex725c.js +14 -0
- package/dist/chunks/canvas-preview-5z63am63.js +295 -0
- package/dist/chunks/canvas-preview-65xqjw7f.js +20 -0
- package/dist/chunks/canvas-preview-69fkk9yn.js +95 -0
- package/dist/chunks/canvas-preview-8xc0vn7v.js +458 -0
- package/dist/chunks/canvas-preview-9aqczacs.js +73 -0
- package/dist/chunks/canvas-preview-af86jz8t.js +141 -0
- package/dist/chunks/canvas-preview-aq5j4p25.js +240 -0
- package/dist/chunks/canvas-preview-b2v8wfnz.js +36 -0
- package/dist/chunks/canvas-preview-befkjea2.js +60 -0
- package/dist/chunks/canvas-preview-bxnqt8cd.js +103 -0
- package/dist/chunks/canvas-preview-cawy6vbz.js +352 -0
- package/dist/chunks/canvas-preview-crmzxxt8.js +518 -0
- package/dist/chunks/canvas-preview-drctegz4.js +591 -0
- package/dist/chunks/canvas-preview-e3vnkkc5.js +134 -0
- package/dist/chunks/canvas-preview-ec2mt7es.js +108 -0
- package/dist/chunks/canvas-preview-fxa0rfb0.js +61 -0
- package/dist/chunks/canvas-preview-g5fkjs34.js +598 -0
- package/dist/chunks/canvas-preview-jeddjex8.js +166 -0
- package/dist/chunks/canvas-preview-k1yg4fgp.js +571 -0
- package/dist/chunks/canvas-preview-kdqgrstc.js +194 -0
- package/dist/chunks/canvas-preview-kn7kvbfj.js +12 -0
- package/dist/chunks/canvas-preview-kp75gb63.js +141 -0
- package/dist/chunks/canvas-preview-mgcx2b2p.js +271 -0
- package/dist/chunks/canvas-preview-n2a35zv5.js +81 -0
- package/dist/chunks/canvas-preview-pkzqg96v.js +63 -0
- package/dist/chunks/canvas-preview-rp4x6y1h.js +144 -0
- package/dist/chunks/canvas-preview-s94ebwf5.js +112 -0
- package/dist/chunks/canvas-preview-skzwvw4d.js +23 -0
- package/dist/chunks/canvas-preview-srgfpdmf.js +60 -0
- package/dist/chunks/canvas-preview-tjhabynd.js +252 -0
- package/dist/chunks/canvas-preview-tnqe7k0b.js +374 -0
- package/dist/chunks/canvas-preview-vaatfznm.js +189 -0
- package/dist/chunks/canvas-preview-vm9zdp2t.js +398 -0
- package/dist/chunks/canvas-preview-w54ymtb3.js +272 -0
- package/dist/chunks/canvas-preview-wg80rbc2.js +46 -0
- package/dist/chunks/canvas-preview-y5y6ghaw.js +125 -0
- package/dist/chunks/canvas-preview-ycepb05s.js +176 -0
- package/dist/chunks/canvas-preview-yjz0f7n4.js +50 -0
- package/dist/chunks/canvas-preview-zaaj99wd.js +244 -0
- package/dist/src/canvas-preview.d.ts +29 -0
- package/dist/src/canvas-preview.js +271 -0
- package/dist/src/credentials.d.ts +44 -0
- package/dist/src/credentials.js +10 -0
- package/dist/src/deploy/cloudflare/worker.d.ts +50 -0
- package/dist/src/deploy/cloudflare/worker.js +196 -0
- package/dist/src/deploy/vercel/nitro.config.d.ts +2 -0
- package/dist/src/deploy/vercel/nitro.config.js +30 -0
- package/dist/src/deploy/vercel/server/routes/[...].d.ts +2 -0
- package/dist/src/deploy/vercel/server/routes/[...].js +18 -0
- package/dist/src/deploy/vercel/server/utils/host.d.ts +13 -0
- package/dist/src/deploy/vercel/server/utils/host.js +7 -0
- package/dist/src/deploy/vercel/server/utils/repair.d.ts +1 -0
- package/dist/src/deploy/vercel/server/utils/repair.js +6 -0
- package/dist/src/deploy/vercel/server/utils/step.d.ts +10 -0
- package/dist/src/deploy/vercel/server/utils/step.js +6 -0
- package/dist/src/deploy/vercel/server/workflows/drive.d.ts +14 -0
- package/dist/src/deploy/vercel/server/workflows/drive.js +13 -0
- package/dist/src/durable-object.d.ts +14 -0
- package/dist/src/durable-object.js +231 -0
- package/dist/src/hash.d.ts +1 -0
- package/dist/src/hash.js +6 -0
- package/dist/src/identity-key.d.ts +11 -0
- package/dist/src/identity-key.js +9 -0
- package/dist/src/identity.d.ts +12 -0
- package/dist/src/identity.js +6 -0
- package/dist/src/images/exif.d.ts +1 -0
- package/dist/src/images/exif.js +6 -0
- package/dist/src/images/node.d.ts +37 -0
- package/dist/src/images/node.js +12 -0
- package/dist/src/images/photon.d.ts +3 -0
- package/dist/src/images/photon.js +7 -0
- package/dist/src/images/worker.d.ts +1 -0
- package/dist/src/images/worker.js +23 -0
- package/dist/src/index.d.ts +20 -0
- package/dist/src/index.js +121 -0
- package/dist/src/key-file.d.ts +1 -0
- package/dist/src/key-file.js +7 -0
- package/dist/src/listen-fetch.d.ts +8 -0
- package/dist/src/listen-fetch.js +9 -0
- package/dist/src/listen.d.ts +4 -0
- package/dist/src/listen.js +10 -0
- package/dist/src/local/command.d.ts +20 -0
- package/dist/src/local/command.js +12 -0
- package/dist/src/local/feed.d.ts +7 -0
- package/dist/src/local/feed.js +6 -0
- package/dist/src/local/files.d.ts +18 -0
- package/dist/src/local/files.js +13 -0
- package/dist/src/local/git-watch.d.ts +15 -0
- package/dist/src/local/git-watch.js +7 -0
- package/dist/src/local/git.d.ts +5 -0
- package/dist/src/local/git.js +13 -0
- package/dist/src/local/github.d.ts +30 -0
- package/dist/src/local/github.js +9 -0
- package/dist/src/local/identity.d.ts +3 -0
- package/dist/src/local/identity.js +7 -0
- package/dist/src/local/jobs.d.ts +7 -0
- package/dist/src/local/jobs.js +7 -0
- package/dist/src/local/legacy-trust.d.ts +10 -0
- package/dist/src/local/legacy-trust.js +6 -0
- package/dist/src/local/patch.d.ts +16 -0
- package/dist/src/local/patch.js +17 -0
- package/dist/src/local/provider.d.ts +11 -0
- package/dist/src/local/provider.js +20 -0
- package/dist/src/local/snapshots.d.ts +7 -0
- package/dist/src/local/snapshots.js +11 -0
- package/dist/src/local/text.d.ts +1 -0
- package/dist/src/local/text.js +6 -0
- package/dist/src/local/tree.d.ts +50 -0
- package/dist/src/local/tree.js +16 -0
- package/dist/src/local/workspace-migration.d.ts +2 -0
- package/dist/src/local/workspace-migration.js +7 -0
- package/dist/src/local/worktrees.d.ts +46 -0
- package/dist/src/local/worktrees.js +14 -0
- package/dist/src/mcp/config.d.ts +75 -0
- package/dist/src/mcp/config.js +12 -0
- package/dist/src/mcp/connect.d.ts +15 -0
- package/dist/src/mcp/connect.js +9 -0
- package/dist/src/mcp/hub.d.ts +19 -0
- package/dist/src/mcp/hub.js +11 -0
- package/dist/src/mcp/oauth.d.ts +46 -0
- package/dist/src/mcp/oauth.js +12 -0
- package/dist/src/mcp/tools.d.ts +29 -0
- package/dist/src/mcp/tools.js +12 -0
- package/dist/src/open.d.ts +14 -0
- package/dist/src/open.js +46 -0
- package/dist/src/plugins/loader.d.ts +21 -0
- package/dist/src/plugins/loader.js +11 -0
- package/dist/src/postgres.d.ts +19 -0
- package/dist/src/postgres.js +8 -0
- package/dist/src/providers.d.ts +16 -0
- package/dist/src/providers.js +112 -0
- package/dist/src/secrets.d.ts +12 -0
- package/dist/src/secrets.js +8 -0
- package/dist/src/serve/binding.d.ts +39 -0
- package/dist/src/serve/binding.js +6 -0
- package/dist/src/serve/handler.d.ts +15 -0
- package/dist/src/serve/handler.js +8 -0
- package/dist/src/serve/stream-server.d.ts +6 -0
- package/dist/src/serve/stream-server.js +6 -0
- package/dist/src/session-index.d.ts +11 -0
- package/dist/src/session-index.js +11 -0
- package/dist/src/shell-env.d.ts +14 -0
- package/dist/src/shell-env.js +108 -0
- package/dist/src/sqlite-workers.d.ts +6 -0
- package/dist/src/sqlite-workers.js +6 -0
- package/dist/src/sqlite.d.ts +8 -0
- package/dist/src/sqlite.js +11 -0
- package/dist/src/store-rows.d.ts +13 -0
- package/dist/src/store-rows.js +26 -0
- package/dist/src/store-usage.d.ts +25 -0
- package/dist/src/store-usage.js +19 -0
- package/dist/src/usage-history.d.ts +54 -0
- package/dist/src/usage-history.js +14 -0
- package/dist/src/usage-scan.d.ts +24 -0
- package/dist/src/usage-scan.js +65 -0
- package/dist/src/usage-service.d.ts +23 -0
- package/dist/src/usage-service.js +16 -0
- package/dist/src/usage-summary.d.ts +9 -0
- package/dist/src/usage-summary.js +16 -0
- package/dist/src/usage-types.d.ts +86 -0
- package/dist/src/usage-types.js +0 -0
- package/dist/src/usage-worker.d.ts +1 -0
- package/dist/src/usage-worker.js +29 -0
- package/dist/src/usage.d.ts +195 -0
- package/dist/src/usage.js +23 -0
- package/package.json +140 -3
- package/src/canvas-preview.ts +403 -0
- package/src/credentials.ts +294 -0
- package/src/deploy/cloudflare/README.md +3 -0
- package/src/deploy/cloudflare/worker.ts +306 -0
- package/src/deploy/cloudflare/wrangler.jsonc +12 -0
- package/src/deploy/vercel/README.md +3 -0
- package/src/deploy/vercel/nitro.config.ts +29 -0
- package/src/deploy/vercel/server/routes/[...].ts +10 -0
- package/src/deploy/vercel/server/utils/host.ts +104 -0
- package/src/deploy/vercel/server/utils/repair.ts +35 -0
- package/src/deploy/vercel/server/utils/step.ts +25 -0
- package/src/deploy/vercel/server/workflows/drive.ts +46 -0
- package/src/deploy/vercel/tsconfig.json +9 -0
- package/src/deploy/vercel/types/bun.d.ts +2 -0
- package/src/durable-object.ts +386 -0
- package/src/hash.ts +5 -0
- package/src/identity-key.ts +20 -0
- package/src/identity.ts +66 -0
- package/src/images/exif.ts +69 -0
- package/src/images/node.ts +163 -0
- package/src/images/photon.ts +141 -0
- package/src/images/worker.ts +29 -0
- package/src/index.ts +39 -0
- package/src/key-file.ts +53 -0
- package/src/listen-fetch.ts +409 -0
- package/src/listen.ts +228 -0
- package/src/local/command.ts +161 -0
- package/src/local/feed.ts +70 -0
- package/src/local/files.ts +817 -0
- package/src/local/git-watch.ts +199 -0
- package/src/local/git.ts +919 -0
- package/src/local/github.ts +691 -0
- package/src/local/identity.ts +21 -0
- package/src/local/jobs.ts +573 -0
- package/src/local/legacy-trust.ts +105 -0
- package/src/local/patch.ts +334 -0
- package/src/local/provider.ts +184 -0
- package/src/local/snapshots.ts +778 -0
- package/src/local/text.ts +9 -0
- package/src/local/tree.ts +398 -0
- package/src/local/workspace-migration.ts +95 -0
- package/src/local/worktrees.ts +239 -0
- package/src/mcp/config.ts +330 -0
- package/src/mcp/connect.ts +92 -0
- package/src/mcp/hub.ts +836 -0
- package/src/mcp/oauth.ts +436 -0
- package/src/mcp/tools.ts +333 -0
- package/src/open.ts +449 -0
- package/src/plugins/loader.ts +186 -0
- package/src/postgres.ts +492 -0
- package/src/providers.ts +135 -0
- package/src/secrets.ts +31 -0
- package/src/serve/binding.ts +629 -0
- package/src/serve/handler.ts +362 -0
- package/src/serve/stream-server.ts +176 -0
- package/src/session-index.ts +99 -0
- package/src/shell-env.ts +153 -0
- package/src/sqlite-workers.ts +283 -0
- package/src/sqlite.ts +553 -0
- package/src/store-rows.ts +110 -0
- package/src/store-usage.ts +347 -0
- package/src/usage-history.ts +219 -0
- package/src/usage-scan.ts +81 -0
- package/src/usage-service.ts +82 -0
- package/src/usage-summary.ts +79 -0
- package/src/usage-types.ts +87 -0
- package/src/usage-worker.ts +24 -0
- package/src/usage.ts +776 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { DatabaseSync } from "node:sqlite";
|
|
3
|
+
import {
|
|
4
|
+
decodeObject,
|
|
5
|
+
decodeSessionRecord,
|
|
6
|
+
decodeWorkspaceRecord,
|
|
7
|
+
parseSessionRef,
|
|
8
|
+
} from "@nyte-ai/core/store";
|
|
9
|
+
import type { ModelRef, SessionId, Usage } from "@nyte-ai/schema";
|
|
10
|
+
import type {
|
|
11
|
+
UsageEntry,
|
|
12
|
+
UsageReport,
|
|
13
|
+
UsageSession,
|
|
14
|
+
UsageSource,
|
|
15
|
+
UsageSubject,
|
|
16
|
+
UsageTotals,
|
|
17
|
+
UsageWindow,
|
|
18
|
+
} from "./usage-types.ts";
|
|
19
|
+
import { storePath } from "./sqlite.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
|
+
|
|
133
|
+
const amounts = [
|
|
134
|
+
usage.total,
|
|
135
|
+
usage.input,
|
|
136
|
+
usage.output,
|
|
137
|
+
usage.cacheRead,
|
|
138
|
+
usage.cacheWrite,
|
|
139
|
+
usage.cost.total,
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
if (!amounts.some((amount) => amount > 0)) continue;
|
|
143
|
+
const day = localDay(spend.at);
|
|
144
|
+
|
|
145
|
+
if (earliestDay === undefined || day < earliestDay) earliestDay = day;
|
|
146
|
+
|
|
147
|
+
if (prior !== undefined && day >= prior.sinceDay && day <= prior.untilDay) {
|
|
148
|
+
sawPrior = true;
|
|
149
|
+
priorCost += spend.usage.cost.total;
|
|
150
|
+
priorTokens += usageTokens(spend.usage);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (day > untilDay) continue;
|
|
154
|
+
|
|
155
|
+
if (requestedSince !== null && day < requestedSince) continue;
|
|
156
|
+
|
|
157
|
+
const key = JSON.stringify([day, store.workspacePath, session.sessionId, spend.subject]);
|
|
158
|
+
const cell = cells.get(key);
|
|
159
|
+
|
|
160
|
+
if (cell === undefined) {
|
|
161
|
+
cells.set(key, {
|
|
162
|
+
entry: {
|
|
163
|
+
day,
|
|
164
|
+
workspacePath: store.workspacePath,
|
|
165
|
+
sessionId: session.sessionId,
|
|
166
|
+
subject: spend.subject,
|
|
167
|
+
},
|
|
168
|
+
totals: add(EMPTY_TOTALS, spend.usage),
|
|
169
|
+
});
|
|
170
|
+
} else {
|
|
171
|
+
cell.totals = add(cell.totals, spend.usage);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
spentInWindow = true;
|
|
175
|
+
lastActivityAt = Math.max(lastActivityAt, spend.at);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (spentInWindow) {
|
|
179
|
+
sessions.set(session.sessionId, {
|
|
180
|
+
sessionId: session.sessionId,
|
|
181
|
+
name: session.name,
|
|
182
|
+
workspacePath: store.workspacePath,
|
|
183
|
+
lastActivityAt: session.lastActivityAt ?? lastActivityAt,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
readAt,
|
|
191
|
+
sinceDay: requestedSince ?? earliestDay ?? untilDay,
|
|
192
|
+
untilDay,
|
|
193
|
+
entries: [...cells.values()]
|
|
194
|
+
.map((cell) => ({ ...cell.entry, totals: cell.totals }))
|
|
195
|
+
.toSorted((left, right) => left.day.localeCompare(right.day)),
|
|
196
|
+
sessions: [...sessions.values()],
|
|
197
|
+
sources,
|
|
198
|
+
previous: sawPrior ? { cost: priorCost, tokens: priorTokens } : undefined,
|
|
199
|
+
earliestDay,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function readNyteUsage(home: string, window: UsageWindow): UsageReport {
|
|
204
|
+
const readAt = Date.now();
|
|
205
|
+
const path = storePath(home);
|
|
206
|
+
|
|
207
|
+
if (!existsSync(path)) return projectUsageReport([], window, readAt);
|
|
208
|
+
let database: DatabaseSync | undefined;
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
database = new DatabaseSync(path, { readOnly: true, timeout: 5_000 });
|
|
212
|
+
database.exec("BEGIN");
|
|
213
|
+
const workspaces = new Map<string, string | null>();
|
|
214
|
+
|
|
215
|
+
const sessions = new Map<
|
|
216
|
+
SessionId,
|
|
217
|
+
{
|
|
218
|
+
readonly workspacePath: string | null;
|
|
219
|
+
readonly name: string | undefined;
|
|
220
|
+
readonly lastActivityAt: number | undefined;
|
|
221
|
+
readonly records: UsageRecord[];
|
|
222
|
+
}
|
|
223
|
+
>();
|
|
224
|
+
|
|
225
|
+
const refs = database
|
|
226
|
+
.prepare(
|
|
227
|
+
"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)",
|
|
228
|
+
)
|
|
229
|
+
.all();
|
|
230
|
+
|
|
231
|
+
for (const row of refs) {
|
|
232
|
+
const name = parseText(row.name);
|
|
233
|
+
const body = parseText(row.body);
|
|
234
|
+
|
|
235
|
+
if (name === null || body === null || !name.startsWith("workspaces/")) continue;
|
|
236
|
+
const workspace = decodeWorkspaceRecord(decodeObject(body));
|
|
237
|
+
|
|
238
|
+
if (workspace !== null)
|
|
239
|
+
workspaces.set(
|
|
240
|
+
workspace.id,
|
|
241
|
+
workspace.target.kind === "local" && "path" in workspace.target
|
|
242
|
+
? parseText(workspace.target.path)
|
|
243
|
+
: null,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
for (const row of refs) {
|
|
248
|
+
const name = parseText(row.name);
|
|
249
|
+
const body = parseText(row.body);
|
|
250
|
+
|
|
251
|
+
if (name === null || body === null || name.split("/").length !== 2) continue;
|
|
252
|
+
const id = parseSessionRef(name);
|
|
253
|
+
|
|
254
|
+
if (id === null) continue;
|
|
255
|
+
const session = decodeSessionRecord(decodeObject(body));
|
|
256
|
+
|
|
257
|
+
if (session === null) throw new Error(`Cannot read session ${id}.`);
|
|
258
|
+
sessions.set(id, {
|
|
259
|
+
workspacePath: workspaces.get(session.workspace) ?? null,
|
|
260
|
+
name: session.name ?? undefined,
|
|
261
|
+
lastActivityAt: session.lastActivityAt,
|
|
262
|
+
records: [],
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const models = new Map<SessionId, Map<string | null, ModelRef>>();
|
|
267
|
+
|
|
268
|
+
for (const row of database.prepare(USAGE_EVENTS).iterate()) {
|
|
269
|
+
const session = parseSessionRef(`sessions/${parseText(row.session) ?? ""}`);
|
|
270
|
+
|
|
271
|
+
if (session === null) throw new Error("Cannot read usage session.");
|
|
272
|
+
|
|
273
|
+
const event = decodeEvent(
|
|
274
|
+
parseNumber(row.seq),
|
|
275
|
+
parseNumber(row.at),
|
|
276
|
+
parseText(row.event) ?? "",
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
let heads = models.get(session);
|
|
280
|
+
|
|
281
|
+
if (heads === undefined) {
|
|
282
|
+
heads = new Map();
|
|
283
|
+
models.set(session, heads);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (event.kind === "run" && event.change === "started")
|
|
287
|
+
heads.set(event.head, event.view.configuration.model);
|
|
288
|
+
|
|
289
|
+
if (event.kind !== "request") continue;
|
|
290
|
+
const model = heads.get(event.head);
|
|
291
|
+
|
|
292
|
+
const subject: UsageSubject =
|
|
293
|
+
event.step === "compaction" || event.step === "summary"
|
|
294
|
+
? { kind: "compaction" }
|
|
295
|
+
: event.step === "plugin"
|
|
296
|
+
? { kind: "tool" }
|
|
297
|
+
: {
|
|
298
|
+
kind: "model",
|
|
299
|
+
provider: model?.provider ?? "unknown",
|
|
300
|
+
model: model?.id ?? "unknown",
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
let info = sessions.get(session);
|
|
304
|
+
|
|
305
|
+
if (info === undefined) {
|
|
306
|
+
info = { workspacePath: null, name: undefined, lastActivityAt: undefined, records: [] };
|
|
307
|
+
sessions.set(session, info);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
info.records.push({ at: event.at, subject, usage: event.usage });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
database.exec("COMMIT");
|
|
314
|
+
const stores = new Map<string | null, SessionUsage[]>();
|
|
315
|
+
|
|
316
|
+
for (const [sessionId, info] of sessions) {
|
|
317
|
+
const group = stores.get(info.workspacePath) ?? [];
|
|
318
|
+
group.push({
|
|
319
|
+
sessionId,
|
|
320
|
+
name: info.name,
|
|
321
|
+
records: info.records,
|
|
322
|
+
lastActivityAt: info.lastActivityAt,
|
|
323
|
+
});
|
|
324
|
+
stores.set(info.workspacePath, group);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return projectUsageReport(
|
|
328
|
+
[...stores].map(([workspacePath, sessions]) => ({ workspacePath, sessions, failure: null })),
|
|
329
|
+
window,
|
|
330
|
+
readAt,
|
|
331
|
+
);
|
|
332
|
+
} catch (cause) {
|
|
333
|
+
return projectUsageReport(
|
|
334
|
+
[
|
|
335
|
+
{
|
|
336
|
+
workspacePath: null,
|
|
337
|
+
sessions: [],
|
|
338
|
+
failure: { message: cause instanceof Error ? cause.message : String(cause) },
|
|
339
|
+
},
|
|
340
|
+
],
|
|
341
|
+
window,
|
|
342
|
+
readAt,
|
|
343
|
+
);
|
|
344
|
+
} finally {
|
|
345
|
+
database?.close();
|
|
346
|
+
}
|
|
347
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { constants } from "node:fs";
|
|
4
|
+
import type { Stats } from "node:fs";
|
|
5
|
+
import { mkdir, open, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
6
|
+
import { dirname } from "node:path";
|
|
7
|
+
import { Type, type TSchema } from "typebox";
|
|
8
|
+
|
|
9
|
+
export interface HistoryLine {
|
|
10
|
+
readonly text: string;
|
|
11
|
+
/** False for a last line without its newline: its writer may still be appending it. */
|
|
12
|
+
readonly terminated: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface HistoryFold<Scan> {
|
|
16
|
+
line(line: HistoryLine): void;
|
|
17
|
+
scan(): Scan;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Folds a history file's lines into a scan. `scan()` returns a copy, so folding goes on without changing it. */
|
|
21
|
+
export type HistoryParser<Scan> = (from: Scan | undefined) => HistoryFold<Scan>;
|
|
22
|
+
|
|
23
|
+
export interface FileScan<Scan> {
|
|
24
|
+
readonly size: number;
|
|
25
|
+
readonly mtimeMs: number;
|
|
26
|
+
/** The byte after the last newline, where a grown file resumes. */
|
|
27
|
+
readonly position: number;
|
|
28
|
+
/** The scan of every line up to `position`. */
|
|
29
|
+
readonly committed: Scan;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function fileScanSchema<Scan extends TSchema>(scan: Scan) {
|
|
33
|
+
return Type.Object({
|
|
34
|
+
size: Type.Number({ minimum: 0 }),
|
|
35
|
+
mtimeMs: Type.Number(),
|
|
36
|
+
position: Type.Number({ minimum: 0 }),
|
|
37
|
+
committed: scan,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Scans keyed by path. A scan answers while its file keeps the size and mtime it was read at. */
|
|
42
|
+
export class UsageScanCache<Scan> {
|
|
43
|
+
readonly #files: Map<string, FileScan<Scan>>;
|
|
44
|
+
#changed = false;
|
|
45
|
+
|
|
46
|
+
constructor(files: Iterable<readonly [string, FileScan<Scan>]> = []) {
|
|
47
|
+
this.#files = new Map(files);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
keys(): IterableIterator<string> {
|
|
51
|
+
return this.#files.keys();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
entries(): IterableIterator<[string, FileScan<Scan>]> {
|
|
55
|
+
return this.#files.entries();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get(path: string): FileScan<Scan> | undefined {
|
|
59
|
+
return this.#files.get(path);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
set(path: string, scan: FileScan<Scan>): void {
|
|
63
|
+
if (this.#files.get(path) === scan) return;
|
|
64
|
+
this.#files.set(path, scan);
|
|
65
|
+
this.#changed = true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Forgets every file a read did not reach. */
|
|
69
|
+
keepOnly(seen: ReadonlySet<string>): void {
|
|
70
|
+
for (const path of this.#files.keys()) {
|
|
71
|
+
if (seen.has(path)) continue;
|
|
72
|
+
this.#files.delete(path);
|
|
73
|
+
this.#changed = true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Whether a scan was added or forgotten since the last {@link markSaved}. */
|
|
78
|
+
get changed(): boolean {
|
|
79
|
+
return this.#changed;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
markSaved(): void {
|
|
83
|
+
this.#changed = false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Reads a history file through `cache`. An unchanged file answers from the cache without being
|
|
89
|
+
* opened, and a file that only grew is read from its last newline on; any other change rereads it.
|
|
90
|
+
* A last line without its newline is folded into the answer but not the cache, since its writer may
|
|
91
|
+
* still be appending it: the next read starts from that line again.
|
|
92
|
+
*/
|
|
93
|
+
export async function readHistory<Scan>(
|
|
94
|
+
path: string,
|
|
95
|
+
info: Stats,
|
|
96
|
+
parser: HistoryParser<Scan>,
|
|
97
|
+
cache: UsageScanCache<Scan> | undefined,
|
|
98
|
+
signal: AbortSignal | undefined,
|
|
99
|
+
): Promise<Scan> {
|
|
100
|
+
const hit = cache?.get(path);
|
|
101
|
+
const unchanged = hit !== undefined && hit.size === info.size && hit.mtimeMs === info.mtimeMs;
|
|
102
|
+
|
|
103
|
+
if (unchanged && hit.position === info.size) return hit.committed;
|
|
104
|
+
const from = unchanged || (hit !== undefined && info.size > hit.size) ? hit : undefined;
|
|
105
|
+
const fold = parser(from?.committed);
|
|
106
|
+
let position = from?.position ?? 0;
|
|
107
|
+
let unterminated: HistoryLine | undefined;
|
|
108
|
+
|
|
109
|
+
for await (const line of historyLines(path, position, signal)) {
|
|
110
|
+
signal?.throwIfAborted();
|
|
111
|
+
|
|
112
|
+
if (!line.terminated) {
|
|
113
|
+
unterminated = line;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
fold.line(line);
|
|
118
|
+
position = line.end;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const committed = fold.scan();
|
|
122
|
+
cache?.set(
|
|
123
|
+
path,
|
|
124
|
+
unchanged ? hit : { size: info.size, mtimeMs: info.mtimeMs, position, committed },
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
if (unterminated === undefined) return committed;
|
|
128
|
+
fold.line(unterminated);
|
|
129
|
+
|
|
130
|
+
return fold.scan();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Yields a history file's lines from byte `start` up to the size it had when opened, though its writer may keep appending. */
|
|
134
|
+
async function* historyLines(path: string, start: number, signal: AbortSignal | undefined) {
|
|
135
|
+
const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const stat = await file.stat();
|
|
139
|
+
|
|
140
|
+
if (!stat.isFile()) throw new Error("History is not a regular file.");
|
|
141
|
+
const buffer = Buffer.alloc(64 * 1024);
|
|
142
|
+
let pending = Buffer.alloc(0);
|
|
143
|
+
let pendingStart = start;
|
|
144
|
+
let position = start;
|
|
145
|
+
|
|
146
|
+
while (position < stat.size) {
|
|
147
|
+
signal?.throwIfAborted();
|
|
148
|
+
|
|
149
|
+
const { bytesRead } = await file.read(
|
|
150
|
+
buffer,
|
|
151
|
+
0,
|
|
152
|
+
Math.min(buffer.length, stat.size - position),
|
|
153
|
+
position,
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
if (bytesRead === 0) break;
|
|
157
|
+
position += bytesRead;
|
|
158
|
+
const read = buffer.subarray(0, bytesRead);
|
|
159
|
+
const chunk = pending.length === 0 ? read : Buffer.concat([pending, read]);
|
|
160
|
+
let from = 0;
|
|
161
|
+
|
|
162
|
+
// oxlint-disable-next-line nyte-code/no-index-of -- a byte search keeps offsets exact, which decoded text cannot
|
|
163
|
+
for (let newline = chunk.indexOf(0x0a); newline !== -1; newline = chunk.indexOf(0x0a, from)) {
|
|
164
|
+
yield {
|
|
165
|
+
text: chunk.toString("utf8", from, newline),
|
|
166
|
+
terminated: true,
|
|
167
|
+
end: pendingStart + newline + 1,
|
|
168
|
+
};
|
|
169
|
+
from = newline + 1;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
pendingStart += from;
|
|
173
|
+
pending = Buffer.from(chunk.subarray(from));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (pending.length > 0)
|
|
177
|
+
yield {
|
|
178
|
+
text: pending.toString("utf8"),
|
|
179
|
+
terminated: false,
|
|
180
|
+
end: pendingStart + pending.length,
|
|
181
|
+
};
|
|
182
|
+
} finally {
|
|
183
|
+
await file.close();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Reads a cache document written by {@link writeCacheDocument}; a missing, torn or outdated one reads as `undefined`. */
|
|
188
|
+
export async function readCacheDocument<Document>(
|
|
189
|
+
path: string,
|
|
190
|
+
validator: { Check(value: unknown): value is Document },
|
|
191
|
+
): Promise<Document | undefined> {
|
|
192
|
+
try {
|
|
193
|
+
const value: unknown = JSON.parse(await readFile(path, "utf8"));
|
|
194
|
+
|
|
195
|
+
return validator.Check(value) ? value : undefined;
|
|
196
|
+
} catch {
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Replaces the cache document whole, so a reader never sees half of one. Resolves whether the write landed. */
|
|
202
|
+
export async function writeCacheDocument<Document>(
|
|
203
|
+
path: string,
|
|
204
|
+
document: Document,
|
|
205
|
+
): Promise<boolean> {
|
|
206
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
await mkdir(dirname(path), { recursive: true });
|
|
210
|
+
await writeFile(temporary, JSON.stringify(document), { mode: 0o600 });
|
|
211
|
+
await rename(temporary, path);
|
|
212
|
+
|
|
213
|
+
return true;
|
|
214
|
+
} catch {
|
|
215
|
+
await rm(temporary, { force: true }).catch(() => undefined);
|
|
216
|
+
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -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
|
+
}
|