@nyte-ai/host 0.0.18 → 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/THIRD-PARTY-NOTICES.md +66 -0
- package/dist/chunks/{canvas-preview-5hxat7hh.js → canvas-preview-11nadadc.js} +8 -6
- package/dist/chunks/{canvas-preview-rp6fz0w5.js → canvas-preview-2jwbe14k.js} +5 -5
- package/dist/chunks/{canvas-preview-m02v4yy6.js → canvas-preview-55s0y68g.js} +3 -2
- package/dist/chunks/{canvas-preview-xn10d4f8.js → canvas-preview-5ekjx6sw.js} +9 -4
- package/dist/chunks/{canvas-preview-75db60xy.js → canvas-preview-8xc0vn7v.js} +5 -4
- package/dist/chunks/{canvas-preview-zn2d0k9d.js → canvas-preview-af86jz8t.js} +14 -6
- package/dist/chunks/{canvas-preview-d9wwhbtz.js → canvas-preview-aq5j4p25.js} +1 -1
- package/dist/chunks/{canvas-preview-j1tnf51w.js → canvas-preview-b2v8wfnz.js} +1 -1
- package/dist/chunks/canvas-preview-befkjea2.js +60 -0
- package/dist/chunks/canvas-preview-bxnqt8cd.js +103 -0
- package/dist/chunks/{canvas-preview-vax9ne4e.js → canvas-preview-cawy6vbz.js} +22 -11
- package/dist/chunks/{canvas-preview-senpjwep.js → canvas-preview-crmzxxt8.js} +3 -3
- package/dist/chunks/{canvas-preview-ymgz0937.js → canvas-preview-drctegz4.js} +8 -4
- package/dist/chunks/{canvas-preview-yrfz2ak6.js → canvas-preview-e3vnkkc5.js} +6 -6
- package/dist/chunks/canvas-preview-ec2mt7es.js +108 -0
- package/dist/chunks/{canvas-preview-1ta3avah.js → canvas-preview-fxa0rfb0.js} +10 -5
- package/dist/chunks/{canvas-preview-8xgt67ka.js → canvas-preview-g5fkjs34.js} +1 -1
- package/dist/chunks/{canvas-preview-g3qj3y3k.js → canvas-preview-jeddjex8.js} +13 -3
- package/dist/chunks/{canvas-preview-0nrnbt76.js → canvas-preview-k1yg4fgp.js} +199 -125
- package/dist/chunks/canvas-preview-kp75gb63.js +141 -0
- package/dist/chunks/{canvas-preview-ewgspmsd.js → canvas-preview-n2a35zv5.js} +1 -1
- package/dist/chunks/{canvas-preview-h8hshnff.js → canvas-preview-pkzqg96v.js} +1 -1
- package/dist/chunks/{canvas-preview-evh92ywd.js → canvas-preview-tnqe7k0b.js} +15 -10
- package/dist/chunks/{canvas-preview-ehf3vp31.js → canvas-preview-vm9zdp2t.js} +45 -58
- package/dist/chunks/{canvas-preview-76fwvd6e.js → canvas-preview-w54ymtb3.js} +1 -1
- package/dist/chunks/canvas-preview-y5y6ghaw.js +125 -0
- package/dist/chunks/{canvas-preview-98aj5p34.js → canvas-preview-ycepb05s.js} +1 -1
- package/dist/src/canvas-preview.js +7 -3
- package/dist/src/deploy/cloudflare/worker.js +1 -1
- package/dist/src/deploy/vercel/server/routes/[...].js +2 -2
- package/dist/src/deploy/vercel/server/utils/host.js +1 -1
- package/dist/src/deploy/vercel/server/workflows/drive.js +3 -3
- 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 +0 -1
- package/dist/src/index.js +23 -22
- package/dist/src/listen-fetch.js +3 -3
- package/dist/src/listen.js +4 -4
- package/dist/src/local/command.d.ts +5 -0
- package/dist/src/local/command.js +6 -2
- package/dist/src/local/files.js +2 -2
- package/dist/src/local/git-watch.d.ts +1 -1
- package/dist/src/local/git-watch.js +1 -1
- package/dist/src/local/git.js +3 -3
- package/dist/src/local/github.js +2 -2
- package/dist/src/local/jobs.d.ts +1 -1
- package/dist/src/local/jobs.js +2 -1
- package/dist/src/local/provider.js +9 -9
- package/dist/src/local/snapshots.js +2 -2
- package/dist/src/local/worktrees.js +2 -2
- package/dist/src/mcp/connect.d.ts +0 -13
- package/dist/src/mcp/connect.js +1 -1
- package/dist/src/mcp/hub.js +2 -2
- package/dist/src/open.d.ts +2 -30
- package/dist/src/open.js +20 -18
- package/dist/src/serve/binding.js +1 -1
- package/dist/src/serve/handler.js +2 -2
- package/dist/src/sqlite.d.ts +2 -1
- package/dist/src/sqlite.js +4 -3
- package/dist/src/store-usage.js +4 -1
- package/dist/src/usage-history.d.ts +54 -0
- package/dist/src/usage-history.js +14 -0
- package/dist/src/usage-service.d.ts +2 -1
- package/dist/src/usage-service.js +7 -3
- package/dist/src/usage-worker.js +8 -4
- package/dist/src/usage.d.ts +150 -27
- package/dist/src/usage.js +12 -2
- package/package.json +16 -8
- package/src/canvas-preview.ts +4 -3
- 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 +19 -1
- package/src/local/command.ts +64 -12
- package/src/local/files.ts +8 -3
- package/src/local/git-watch.ts +16 -6
- package/src/local/git.ts +7 -2
- package/src/local/github.ts +3 -4
- package/src/local/jobs.ts +55 -69
- package/src/local/snapshots.ts +4 -2
- package/src/local/worktrees.ts +4 -4
- package/src/mcp/connect.ts +1 -14
- package/src/open.ts +16 -36
- package/src/serve/binding.ts +4 -3
- package/src/sqlite.ts +33 -8
- package/src/store-usage.ts +5 -2
- package/src/usage-history.ts +219 -0
- package/src/usage-service.ts +12 -3
- package/src/usage.ts +251 -176
- package/dist/chunks/canvas-preview-a3yw0j70.js +0 -72
- package/dist/chunks/canvas-preview-azg56359.js +0 -68
- package/dist/src/store-owner.d.ts +0 -15
- package/dist/src/store-owner.js +0 -9
- package/src/store-owner.ts +0 -98
|
@@ -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
|
+
}
|
package/src/usage-service.ts
CHANGED
|
@@ -5,7 +5,12 @@ import type { Models } from "@nyte-ai/ai";
|
|
|
5
5
|
import { serial } from "@nyte-ai/schema/serial";
|
|
6
6
|
import type { Api, Model } from "@nyte-ai/schema/model";
|
|
7
7
|
import { readNyteUsage } from "./store-usage.ts";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
loadUsageScanCaches,
|
|
10
|
+
readLocalUsage,
|
|
11
|
+
saveUsageScanCaches,
|
|
12
|
+
type UsageScanCaches,
|
|
13
|
+
} from "./usage.ts";
|
|
9
14
|
import type { UsageSnapshot, UsageWindow } from "./usage-types.ts";
|
|
10
15
|
|
|
11
16
|
export interface UsageScanRequest {
|
|
@@ -26,12 +31,14 @@ export function catalogForUsage(models: Pick<Models, "getModels">): readonly Mod
|
|
|
26
31
|
|
|
27
32
|
export class UsageScanner implements UsageScanReader {
|
|
28
33
|
private readonly home: string;
|
|
29
|
-
private readonly
|
|
34
|
+
private readonly cachePath: string;
|
|
35
|
+
private caches: UsageScanCaches | undefined;
|
|
30
36
|
private readonly models: ReturnType<typeof createModels>;
|
|
31
37
|
private readonly scans = serial();
|
|
32
38
|
|
|
33
39
|
constructor(home: string) {
|
|
34
40
|
this.home = home;
|
|
41
|
+
this.cachePath = join(home, ".nyte", "usage-scan-cache.json");
|
|
35
42
|
this.models = createModels({
|
|
36
43
|
modelsStore: new FileModelsStore(join(home, ".nyte", "models-store.json")),
|
|
37
44
|
});
|
|
@@ -56,8 +63,10 @@ export class UsageScanner implements UsageScanReader {
|
|
|
56
63
|
catalog.filter((model) => provider === undefined || model.provider === provider),
|
|
57
64
|
};
|
|
58
65
|
|
|
59
|
-
const
|
|
66
|
+
const caches = (this.caches ??= await loadUsageScanCaches(this.cachePath));
|
|
67
|
+
const local = await readLocalUsage({ models, window: request.window, caches });
|
|
60
68
|
const report = readNyteUsage(this.home, request.window);
|
|
69
|
+
void this.scans.run(() => saveUsageScanCaches(this.cachePath, caches));
|
|
61
70
|
|
|
62
71
|
return {
|
|
63
72
|
...report,
|