@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
package/src/local/jobs.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import type { ChildProcess, SpawnOptions } from "node:child_process";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
-
import { resolve } from "node:path";
|
|
4
|
+
import { basename, resolve } from "node:path";
|
|
5
5
|
import type { IPty, IPtyForkOptions } from "@lydell/node-pty";
|
|
6
6
|
import { asId, SdkError } from "@nyte-ai/schema";
|
|
7
7
|
import type { JobEnd, OutputFrame, RequestOptions } from "@nyte-ai/schema";
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
tracing,
|
|
15
15
|
} from "@nyte-ai/core";
|
|
16
16
|
import type { EndSpan, ExecutionEnv, Job } from "@nyte-ai/core";
|
|
17
|
+
import { findExecutable, killTree, run, spawnCommand } from "./command.ts";
|
|
17
18
|
|
|
18
19
|
type PtyModule = {
|
|
19
20
|
spawn(file: string, args: string[] | string, options: IPtyForkOptions): IPty;
|
|
@@ -22,7 +23,7 @@ type PtyModule = {
|
|
|
22
23
|
type Driver = {
|
|
23
24
|
write(bytes: Uint8Array): Promise<void>;
|
|
24
25
|
resize(size: { readonly columns: number; readonly rows: number }): boolean;
|
|
25
|
-
idle(): boolean
|
|
26
|
+
idle(): boolean | Promise<boolean>;
|
|
26
27
|
kill(signal: NodeJS.Signals): void;
|
|
27
28
|
readonly pty: boolean;
|
|
28
29
|
};
|
|
@@ -38,19 +39,11 @@ const STOP_GRACE_MS = 2_000;
|
|
|
38
39
|
|
|
39
40
|
const CLOSE_GRACE_MS = 2_000;
|
|
40
41
|
|
|
41
|
-
const QUIET_MS = 300;
|
|
42
|
-
|
|
43
42
|
const DONE = { done: true, value: undefined } as const;
|
|
44
43
|
|
|
45
|
-
const ESC = "\u001b";
|
|
46
|
-
|
|
47
|
-
const BEL = "\u0007";
|
|
48
|
-
|
|
49
|
-
const PROMPT_MARKER = new RegExp(`${ESC}\\]133;([ABCD])`, "g");
|
|
50
|
-
|
|
51
44
|
let loaded: PtyModule | null | undefined;
|
|
52
45
|
|
|
53
|
-
/** Returns `exec` for a folder: each call starts one process as a `Job` (a fresh id; `key` null, core keys pty jobs) in `cwd` resolved against `root` (default `root`), with `env` laid over `process.env`. A string command runs through the platform shell (`/bin/sh -c`, `cmd.exe /d /s /c` on Windows) and an argv directly; `[]` with `pty` runs the login shell (`shell()` read at each start, else `$SHELL`, else `/bin/sh`, as a login shell; §37h), a pty's environment carrying `TERM=xterm-256color`, `COLORTERM=truecolor` and `TERM_PROGRAM=Nyte` without Electron's `ELECTRON_RUN_AS_NODE` and `NODE_OPTIONS`, and `[]` without one throws `SdkError("invalid")`. A program that cannot start throws `SdkError("unavailable")`. Without `pty` the process is piped through `node:child_process` in its own process group, stdout and stderr interleaved in `output` (with `stdio: "protocol"`, stderr's frames tagged `stream: "stderr"` and the pipe paced like a pty, so a reader open from the start misses nothing) and stdin fed by `write`; with `pty` it runs under `@lydell/node-pty`, loaded on first use through `createRequire` (§41a; `loadPty`, given, loads it instead, on every pty start), at the given size, and a platform where it does not load throws `SdkError("unavailable")`. `output()` opens a reader over the last 1 MiB of output that yields `bytes`, a `gap` where the reader fell behind or opened after output was dropped, and `exit { code }` last; a merged pipe never stalls, and a pty or a protocol pipe pauses while its slowest open reader is 256 KiB behind, resuming once it is within 64 KiB. `resize` resizes a pty (`not-pty` for a pipe, `missing` once it ended); `idle`
|
|
46
|
+
/** Returns `exec` for a folder: each call starts one process as a `Job` (a fresh id; `key` null, core keys pty jobs) in `cwd` resolved against `root` (default `root`), with `env` laid over `process.env`. A string command runs through the platform shell (`/bin/sh -c`, `cmd.exe /d /s /c` on Windows) and an argv directly; `[]` with `pty` runs the login shell (`shell()` read at each start, else `$SHELL`, else `/bin/sh`, as a login shell; §37h), a pty's environment carrying `TERM=xterm-256color`, `COLORTERM=truecolor` and `TERM_PROGRAM=Nyte` without Electron's `ELECTRON_RUN_AS_NODE` and `NODE_OPTIONS`, and `[]` without one throws `SdkError("invalid")`. A program that cannot start throws `SdkError("unavailable")`. Without `pty` the process is piped through `node:child_process` in its own process group, stdout and stderr interleaved in `output` (with `stdio: "protocol"`, stderr's frames tagged `stream: "stderr"` and the pipe paced like a pty, so a reader open from the start misses nothing) and stdin fed by `write`; with `pty` it runs under `@lydell/node-pty`, loaded on first use through `createRequire` (§41a; `loadPty`, given, loads it instead, on every pty start), at the given size, and a platform where it does not load throws `SdkError("unavailable")`. `output()` opens a reader over the last 1 MiB of output that yields `bytes`, a `gap` where the reader fell behind or opened after output was dropped, and `exit { code }` last; a merged pipe never stalls, and a pty or a protocol pipe pauses while its slowest open reader is 256 KiB behind, resuming once it is within 64 KiB. `resize` resizes a pty (`not-pty` for a pipe, `missing` once it ended); `idle` checks the OS process table for a login shell with no live children (false for a pipe or explicit command, true once it ended); `stop` sends SIGTERM to a pipe's process group (SIGHUP to a pty), SIGKILL after 2 s (on Windows, which has no signals, it ends a pipe's process tree at once with `taskkill /T /F`), and answers `settled { outcome }` (also once it ended); `settled` resolves `exited { code, reason }` (`code` null after a signal), `reason` `exited` when the process ended on its own and `terminated` after `stop` or an aborted `signal`, and an aborted `settled` signal rejects with `SdkError("unavailable")`; `close` detaches this handle's readers. An aborted `signal` stops the process. `signal` on the options ends every reader of every job started here, leaving the processes running (what the provider's `close` does). */
|
|
54
47
|
export function localExec(
|
|
55
48
|
root: string,
|
|
56
49
|
options?: {
|
|
@@ -167,7 +160,7 @@ export function localExec(
|
|
|
167
160
|
|
|
168
161
|
return finished === null && driver.resize(size) ? { kind: "resized" } : { kind: "missing" };
|
|
169
162
|
},
|
|
170
|
-
idle: async () => finished
|
|
163
|
+
idle: async () => driver.pty && (finished !== null || driver.idle()),
|
|
171
164
|
stop: async () => ({ kind: "settled", outcome: await stop() }),
|
|
172
165
|
settled: (request) => waitEnd(ended, request),
|
|
173
166
|
close,
|
|
@@ -184,13 +177,13 @@ async function pipe(
|
|
|
184
177
|
log: OutputLog,
|
|
185
178
|
finish: (code: number | null) => void,
|
|
186
179
|
): Promise<Driver> {
|
|
187
|
-
const
|
|
180
|
+
const windows = process.platform === "win32";
|
|
188
181
|
const [file, ...args] = typeof command === "string" ? [command] : command;
|
|
189
182
|
|
|
190
183
|
const settings: SpawnOptions = {
|
|
191
184
|
cwd: options.cwd,
|
|
192
185
|
env: options.env,
|
|
193
|
-
detached:
|
|
186
|
+
detached: !windows,
|
|
194
187
|
stdio: "pipe",
|
|
195
188
|
windowsHide: true,
|
|
196
189
|
};
|
|
@@ -198,7 +191,11 @@ async function pipe(
|
|
|
198
191
|
const child: ChildProcess =
|
|
199
192
|
typeof command === "string"
|
|
200
193
|
? spawn(command, { ...settings, shell: true })
|
|
201
|
-
:
|
|
194
|
+
: spawnCommand(
|
|
195
|
+
windows ? ((await findExecutable(file ?? "")) ?? file ?? "") : (file ?? ""),
|
|
196
|
+
args,
|
|
197
|
+
settings,
|
|
198
|
+
);
|
|
202
199
|
|
|
203
200
|
await new Promise<void>((started, failed) => {
|
|
204
201
|
child.once("spawn", started);
|
|
@@ -224,12 +221,13 @@ async function pipe(
|
|
|
224
221
|
},
|
|
225
222
|
});
|
|
226
223
|
child.stdin?.on("error", () => undefined);
|
|
227
|
-
|
|
224
|
+
let killed = false;
|
|
225
|
+
child.on("close", (code) => finish(killed ? null : code));
|
|
228
226
|
child.on("exit", (code) => {
|
|
229
227
|
setTimeout(() => {
|
|
230
228
|
child.stdout?.destroy();
|
|
231
229
|
child.stderr?.destroy();
|
|
232
|
-
finish(code);
|
|
230
|
+
finish(killed ? null : code);
|
|
233
231
|
}, CLOSE_GRACE_MS).unref();
|
|
234
232
|
});
|
|
235
233
|
|
|
@@ -245,8 +243,15 @@ async function pipe(
|
|
|
245
243
|
resize: () => false,
|
|
246
244
|
idle: () => false,
|
|
247
245
|
kill(signal) {
|
|
246
|
+
if (windows) {
|
|
247
|
+
killed = true;
|
|
248
|
+
killTree(child, signal);
|
|
249
|
+
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
248
253
|
try {
|
|
249
|
-
if (
|
|
254
|
+
if (child.pid !== undefined) process.kill(-child.pid, signal);
|
|
250
255
|
else child.kill(signal);
|
|
251
256
|
} catch {
|
|
252
257
|
child.kill(signal);
|
|
@@ -316,22 +321,8 @@ async function terminal(
|
|
|
316
321
|
}
|
|
317
322
|
|
|
318
323
|
const encoder = new TextEncoder();
|
|
319
|
-
let marker: string | null = null;
|
|
320
|
-
let line = "";
|
|
321
|
-
let quietSince = 0;
|
|
322
324
|
|
|
323
|
-
term.onData((text) =>
|
|
324
|
-
log.push(encoder.encode(text));
|
|
325
|
-
quietSince = Date.now();
|
|
326
|
-
|
|
327
|
-
marker = lastMarker(text) ?? marker;
|
|
328
|
-
const plain = withoutEscapes(text);
|
|
329
|
-
const broken = plain.lastIndexOf("\n");
|
|
330
|
-
line = (broken < 0 ? line + plain : plain.slice(broken + 1)).slice(-1024);
|
|
331
|
-
const returned = line.replace(/\r+$/, "").lastIndexOf("\r");
|
|
332
|
-
|
|
333
|
-
if (returned >= 0) line = line.slice(returned + 1);
|
|
334
|
-
});
|
|
325
|
+
term.onData((text) => log.push(encoder.encode(text)));
|
|
335
326
|
term.onExit(({ exitCode, signal }) => finish(signal ? null : exitCode));
|
|
336
327
|
log.throttle({ pause: () => term.pause(), resume: () => term.resume() });
|
|
337
328
|
|
|
@@ -345,10 +336,38 @@ async function terminal(
|
|
|
345
336
|
|
|
346
337
|
return true;
|
|
347
338
|
},
|
|
348
|
-
idle() {
|
|
349
|
-
if (
|
|
339
|
+
async idle() {
|
|
340
|
+
if (typeof command === "string" || command.length !== 0) return false;
|
|
341
|
+
|
|
342
|
+
const result = await run(
|
|
343
|
+
windows ? "powershell.exe" : "ps",
|
|
344
|
+
windows
|
|
345
|
+
? [
|
|
346
|
+
"-NoProfile",
|
|
347
|
+
"-NonInteractive",
|
|
348
|
+
"-Command",
|
|
349
|
+
`Get-CimInstance Win32_Process -Filter "ParentProcessId = ${term.pid} AND Name != 'conhost.exe' AND Name != 'OpenConsole.exe'" | Select-Object -ExpandProperty ProcessId`,
|
|
350
|
+
]
|
|
351
|
+
: ["-A", "-o", "pid=,ppid=,stat=,comm="],
|
|
352
|
+
{ cwd: options.cwd, timeoutMs: 1_500 },
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
if (result.code !== 0) throw new SdkError("unavailable", "Cannot inspect terminal processes");
|
|
356
|
+
const processes = result.stdout.toString("utf8").trim();
|
|
357
|
+
|
|
358
|
+
if (windows) return processes === "";
|
|
359
|
+
|
|
360
|
+
return !processes.split("\n").some((line) => {
|
|
361
|
+
const [pid, parent, state, ...name] = line.trim().split(/\s+/);
|
|
350
362
|
|
|
351
|
-
|
|
363
|
+
if (state?.startsWith("Z")) return false;
|
|
364
|
+
|
|
365
|
+
if (Number(parent) === term.pid) return true;
|
|
366
|
+
|
|
367
|
+
return (
|
|
368
|
+
Number(pid) === term.pid && basename(name.join(" ")).replace(/^-/, "") !== basename(shell)
|
|
369
|
+
);
|
|
370
|
+
});
|
|
352
371
|
},
|
|
353
372
|
kill(signal) {
|
|
354
373
|
try {
|
|
@@ -358,39 +377,6 @@ async function terminal(
|
|
|
358
377
|
};
|
|
359
378
|
}
|
|
360
379
|
|
|
361
|
-
function lastMarker(text: string): string | null {
|
|
362
|
-
return Array.from(text.matchAll(PROMPT_MARKER), (match) => match[1]).at(-1) ?? null;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
function withoutEscapes(text: string): string {
|
|
366
|
-
let plain = "";
|
|
367
|
-
let at = 0;
|
|
368
|
-
|
|
369
|
-
while (at < text.length) {
|
|
370
|
-
const char = text[at] ?? "";
|
|
371
|
-
|
|
372
|
-
if (char !== ESC) {
|
|
373
|
-
plain += char;
|
|
374
|
-
at += 1;
|
|
375
|
-
continue;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
const kind = text[at + 1] ?? "";
|
|
379
|
-
at += 2;
|
|
380
|
-
|
|
381
|
-
if (kind === "[") {
|
|
382
|
-
while (at < text.length && !/[@-~]/.test(text[at] ?? "")) at += 1;
|
|
383
|
-
at += 1;
|
|
384
|
-
} else if (kind === "]") {
|
|
385
|
-
while (at < text.length && text[at] !== BEL && !(text[at] === ESC && text[at + 1] === "\\"))
|
|
386
|
-
at += 1;
|
|
387
|
-
at += text[at] === BEL ? 1 : 2;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
return plain;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
380
|
function loadPty(): PtyModule | null {
|
|
395
381
|
if (loaded === undefined)
|
|
396
382
|
loaded = ptyFrom(() => createRequire(import.meta.url)("@lydell/node-pty"));
|
package/src/local/snapshots.ts
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
writeFile,
|
|
12
12
|
} from "node:fs/promises";
|
|
13
13
|
import { tmpdir } from "node:os";
|
|
14
|
-
import { join } from "node:path";
|
|
14
|
+
import { join, resolve } from "node:path";
|
|
15
15
|
import { asId, SdkError } from "@nyte-ai/schema";
|
|
16
16
|
import type {
|
|
17
17
|
ChangeSet,
|
|
@@ -132,7 +132,9 @@ export function localSnapshots(
|
|
|
132
132
|
if (ran === null || ran.code !== 0) return null;
|
|
133
133
|
const [root, gitDir, prefix = ""] = ran.stdout.toString("utf8").split("\n");
|
|
134
134
|
|
|
135
|
-
return root === undefined || gitDir === undefined
|
|
135
|
+
return root === undefined || gitDir === undefined
|
|
136
|
+
? null
|
|
137
|
+
: { root: resolve(root), gitDir: resolve(gitDir), prefix };
|
|
136
138
|
};
|
|
137
139
|
|
|
138
140
|
const repository = (): Promise<Repository | null> => {
|
package/src/local/worktrees.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { copyFile, mkdir, realpath, rm, stat } from "node:fs/promises";
|
|
3
3
|
import { homedir, tmpdir } from "node:os";
|
|
4
|
-
import { basename, dirname, join } from "node:path";
|
|
4
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
5
5
|
import { SdkError } from "@nyte-ai/schema";
|
|
6
6
|
import type { WorkspaceTarget } from "@nyte-ai/schema";
|
|
7
7
|
import type { WorktreeProvider } from "@nyte-ai/core";
|
|
@@ -50,7 +50,7 @@ export async function listWorktrees(
|
|
|
50
50
|
const path = fields.find((field) => field.startsWith("worktree "))?.slice(9);
|
|
51
51
|
const branch = fields.find((field) => field.startsWith("branch refs/heads/"))?.slice(18);
|
|
52
52
|
|
|
53
|
-
return path === undefined ? [] : [{ path, branch: branch ?? null }];
|
|
53
|
+
return path === undefined ? [] : [{ path: resolve(path), branch: branch ?? null }];
|
|
54
54
|
});
|
|
55
55
|
}
|
|
56
56
|
|
|
@@ -147,7 +147,7 @@ export function localWorktrees(options?: {
|
|
|
147
147
|
const branch = `nyte/${slug}`;
|
|
148
148
|
const below = (root: string): WorkspaceTarget => ({
|
|
149
149
|
kind: "local",
|
|
150
|
-
path:
|
|
150
|
+
path: resolve(root, repo.prefix),
|
|
151
151
|
});
|
|
152
152
|
|
|
153
153
|
const listed = (await listWorktrees(repo.root, options)).find(
|
|
@@ -224,7 +224,7 @@ async function readRepository(git: Git, cwd: string): Promise<Repository | null>
|
|
|
224
224
|
|
|
225
225
|
return root === undefined || gitDir === undefined || root === ""
|
|
226
226
|
? null
|
|
227
|
-
: { root, gitDir, prefix };
|
|
227
|
+
: { root: resolve(root), gitDir: resolve(gitDir), prefix };
|
|
228
228
|
}
|
|
229
229
|
|
|
230
230
|
function worktreeSlug(name: string, title: string | null): string {
|
package/src/mcp/connect.ts
CHANGED
|
@@ -10,13 +10,6 @@ import type { McpServerEntry } from "./config.ts";
|
|
|
10
10
|
import { createAuthProvider, oauthKey, oauthStore } from "./oauth.ts";
|
|
11
11
|
import type { McpOAuthSettings } from "./oauth.ts";
|
|
12
12
|
|
|
13
|
-
/**
|
|
14
|
-
* Opens the transport to one server for one workspace. A url server that
|
|
15
|
-
* needs OAuth sends its stored tokens; when it needs a new sign-in the
|
|
16
|
-
* connection fails with `McpOAuthAuthorizationRequiredError`, after passing
|
|
17
|
-
* the server's challenge to `challenged`. Throws when the entry cannot be
|
|
18
|
-
* started, such as a missing `${VAR}`.
|
|
19
|
-
*/
|
|
20
13
|
export type McpConnector = (
|
|
21
14
|
entry: McpServerEntry,
|
|
22
15
|
context: {
|
|
@@ -27,11 +20,6 @@ export type McpConnector = (
|
|
|
27
20
|
},
|
|
28
21
|
) => McpTransport;
|
|
29
22
|
|
|
30
|
-
/**
|
|
31
|
-
* Runs stdio servers on this machine, in `cwd` resolved against the
|
|
32
|
-
* workspace folder, and connects to url servers over streamable HTTP. A url
|
|
33
|
-
* server without an `Authorization` header signs in with OAuth.
|
|
34
|
-
*/
|
|
35
23
|
export const connectServer: McpConnector = (entry, context) => {
|
|
36
24
|
const { transport } = entry;
|
|
37
25
|
|
|
@@ -65,7 +53,6 @@ export const connectServer: McpConnector = (entry, context) => {
|
|
|
65
53
|
});
|
|
66
54
|
};
|
|
67
55
|
|
|
68
|
-
/** A server's OAuth options with `${VAR}`s in `clientSecret` resolved. */
|
|
69
56
|
export function oauthSettings(config: McpOAuthConfig, server: string): McpOAuthSettings {
|
|
70
57
|
const { clientSecret, authServerMetadataUrl, ...rest } = config;
|
|
71
58
|
|
|
@@ -95,7 +82,7 @@ function expand(values: Readonly<Record<string, string>>, server: string): Recor
|
|
|
95
82
|
|
|
96
83
|
if (found === undefined)
|
|
97
84
|
throw new Error(
|
|
98
|
-
`${server} needs the environment variable ${name}. Set it where Nyte runs, then restart Nyte
|
|
85
|
+
`${server} needs the environment variable ${name}. Set it where Nyte runs, then restart Nyte.`,
|
|
99
86
|
);
|
|
100
87
|
|
|
101
88
|
return found;
|
package/src/open.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { watch } from "node:fs";
|
|
2
2
|
import { mkdir, readFile } from "node:fs/promises";
|
|
3
3
|
import { homedir, hostname } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
5
|
import process from "node:process";
|
|
6
6
|
import type { Models, Provider } from "@nyte-ai/ai";
|
|
7
7
|
import { defaultProviderAuthContext } from "@nyte-ai/ai/auth/context";
|
|
@@ -43,55 +43,28 @@ import { localProvider } from "./local/provider.ts";
|
|
|
43
43
|
import { importLegacyTrust } from "./local/legacy-trust.ts";
|
|
44
44
|
import { migrateLocalWorkspaceIdentities } from "./local/workspace-migration.ts";
|
|
45
45
|
import { listen } from "./listen.ts";
|
|
46
|
+
import { nodeImages } from "./images/node.ts";
|
|
46
47
|
import { createMcpHub } from "./mcp/hub.ts";
|
|
47
48
|
import { loadPluginDirectory, pluginLoader } from "./plugins/loader.ts";
|
|
48
49
|
import { defaultSecrets } from "./secrets.ts";
|
|
49
50
|
import type { SecretsSource } from "./secrets.ts";
|
|
50
|
-
import { sqliteStore } from "./sqlite.ts";
|
|
51
|
+
import { sqliteStore, storePath } from "./sqlite.ts";
|
|
51
52
|
|
|
52
53
|
const RELOAD_DEBOUNCE_MS = 250;
|
|
53
54
|
|
|
54
55
|
const STORE_POLL_MS = 500;
|
|
55
56
|
|
|
56
|
-
/**
|
|
57
|
-
* Describes what `openHost` takes: `OpenOptions` with `store`, `workspace`,
|
|
58
|
-
* `models` and `plugins` optional, `secrets` defaulting to this machine's key,
|
|
59
|
-
* and the home directory holding `.nyte`. Every `OpenOptions` is one, so the
|
|
60
|
-
* sdk's Node `open` passes its options straight through.
|
|
61
|
-
*/
|
|
62
57
|
export type OpenHostOptions = Omit<OpenOptions, "store" | "workspace" | "models" | "plugins"> & {
|
|
63
|
-
/** The host's store; absent, `sqliteStore` at `<home>/.nyte/nyte.db`, which the host then closes with itself (§51c). */
|
|
64
58
|
readonly store?: Store;
|
|
65
|
-
/** Where a root session acts when `create` names none; default the local folder `process.cwd()` (§51c). */
|
|
66
59
|
readonly workspace?: WorkspaceTarget;
|
|
67
|
-
/** The `@nyte-ai/ai` catalog; absent, `nyteModels` over this host's encrypted credential store (§42e). */
|
|
68
60
|
readonly models?: Models;
|
|
69
|
-
/** Core plugins loaded after the first-party built-ins and before the plugin files; default none. One with a built-in's id replaces that built-in, as a bundled client's `codemode` runtime does, and a `local` environment provider among them replaces the default one. */
|
|
70
61
|
readonly plugins?: readonly Plugin<"core">[];
|
|
71
|
-
/** The directory whose `.nyte` holds the store, settings, plugins, secrets key and identity key; default `os.homedir()`. */
|
|
72
62
|
readonly home?: string;
|
|
73
|
-
/** The login shell the default local provider's pty jobs run, read at each start; absent or null is `$SHELL`. */
|
|
74
63
|
readonly shell?: () => string | null;
|
|
64
|
+
readonly label?: string;
|
|
65
|
+
readonly onRevoked?: () => void;
|
|
75
66
|
};
|
|
76
67
|
|
|
77
|
-
/**
|
|
78
|
-
* Opens the Node host the sdk's Node `open` calls: `composeHost` with
|
|
79
|
-
* `home` (default `os.homedir()`), `store` (default `<home>/.nyte/nyte.db`,
|
|
80
|
-
* checked every 500 ms for another process's commits, which are then read so
|
|
81
|
-
* a second process on the file sees the first one's runs live),
|
|
82
|
-
* `workspace` (default the local folder `process.cwd()`), `secrets` (default
|
|
83
|
-
* `defaultSecrets(home)`), `models` (default `nyteModels`), `localProvider`
|
|
84
|
-
* unless a plugin provides the `local` environment, every first-party plugin
|
|
85
|
-
* of `@nyte-ai/plugin` that `plugins` does not replace by id, then `plugins`
|
|
86
|
-
* and `filePlugins`' files, `settingsSeed`'s seed and
|
|
87
|
-
* `hostDescription()`; each file that did not load becomes a
|
|
88
|
-
* `server.report` row. The host's `loadPlugins` is `pluginLoader` over
|
|
89
|
-
* the plugin directories (§47l), which `watchPlugins` watches for hot reload
|
|
90
|
-
* (§49a). `listen` defaults `identity` to `identitySigner(host.hostId, home)`
|
|
91
|
-
* and `describe` to the description; nothing here reads the secrets key, even
|
|
92
|
-
* with a credential stored (§50d, §55). `close()` stops the watch, closes the
|
|
93
|
-
* open listeners, the host, then a store it opened.
|
|
94
|
-
*/
|
|
95
68
|
export async function openHost(options: OpenHostOptions): Promise<Host> {
|
|
96
69
|
const home = options.home ?? homedir();
|
|
97
70
|
const workspace = options.workspace ?? { kind: "local", path: process.cwd() };
|
|
@@ -109,9 +82,10 @@ export async function openHost(options: OpenHostOptions): Promise<Host> {
|
|
|
109
82
|
}
|
|
110
83
|
|
|
111
84
|
async function defaultStore(home: string): Promise<Store> {
|
|
112
|
-
|
|
85
|
+
const path = storePath(home);
|
|
86
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
113
87
|
|
|
114
|
-
return sqliteStore({ path
|
|
88
|
+
return sqliteStore({ path });
|
|
115
89
|
}
|
|
116
90
|
|
|
117
91
|
async function composeOpen(
|
|
@@ -156,7 +130,6 @@ async function composeOpen(
|
|
|
156
130
|
openaiAstraContext,
|
|
157
131
|
telemetry,
|
|
158
132
|
canvas,
|
|
159
|
-
browserTools,
|
|
160
133
|
claudeCode.plugin,
|
|
161
134
|
acp.plugin,
|
|
162
135
|
];
|
|
@@ -179,6 +152,7 @@ async function composeOpen(
|
|
|
179
152
|
models,
|
|
180
153
|
mcp: options.mcp ?? createMcpHub({ home }),
|
|
181
154
|
github: options.github ?? (usesLocalProvider ? localGitHubAccount({ home }) : undefined),
|
|
155
|
+
images: options.images ?? nodeImages(),
|
|
182
156
|
plugins: [...plugins, ...files.plugins],
|
|
183
157
|
pluginSources: files.sources,
|
|
184
158
|
secrets,
|
|
@@ -186,6 +160,12 @@ async function composeOpen(
|
|
|
186
160
|
description,
|
|
187
161
|
...(settings.seed === null ? {} : { seed: settings.seed }),
|
|
188
162
|
...(owned === null ? {} : { poll: STORE_POLL_MS }),
|
|
163
|
+
member: {
|
|
164
|
+
label: options.label ?? "nyte",
|
|
165
|
+
pid: process.pid,
|
|
166
|
+
capabilities: given.some((plugin) => plugin.id === browserTools.id) ? [browserTools.id] : [],
|
|
167
|
+
},
|
|
168
|
+
revoked: options.onRevoked,
|
|
189
169
|
});
|
|
190
170
|
|
|
191
171
|
projectDirs = async () =>
|
|
@@ -281,7 +261,6 @@ async function nyteModels(input: {
|
|
|
281
261
|
readonly store: Store;
|
|
282
262
|
readonly secrets: SecretsSource;
|
|
283
263
|
readonly home: string;
|
|
284
|
-
/** Providers beside Nyte's catalog, each refreshed offline at open. */
|
|
285
264
|
readonly providers: readonly Provider[];
|
|
286
265
|
}): Promise<Models> {
|
|
287
266
|
const credentials = credentialStore(input.store, input.secrets);
|
|
@@ -302,6 +281,7 @@ async function nyteModels(input: {
|
|
|
302
281
|
});
|
|
303
282
|
|
|
304
283
|
for (const provider of [...nyteProviders(), ...input.providers]) models.setProvider(provider);
|
|
284
|
+
|
|
305
285
|
const [stored, selected] = await Promise.all([
|
|
306
286
|
credentials.list(),
|
|
307
287
|
input.store.open().then(readModelSelection),
|
package/src/serve/binding.ts
CHANGED
|
@@ -193,6 +193,7 @@ export function bindConnection(connection: Connection, options: BindingOptions):
|
|
|
193
193
|
return null;
|
|
194
194
|
},
|
|
195
195
|
"models.setDefault": ({ ref }) => connection.models.setDefault(ref),
|
|
196
|
+
"models.setListing": ({ refs, listing }) => connection.models.setListing(refs, listing),
|
|
196
197
|
"models.verify": ({ provider }) => connection.models.verify(provider),
|
|
197
198
|
"models.login": async (input) => {
|
|
198
199
|
const { provider } = input;
|
|
@@ -384,9 +385,9 @@ export function bindConnection(connection: Connection, options: BindingOptions):
|
|
|
384
385
|
"mcp.remove": ({ workspace: id, server, scope }) => workspace(id).mcp.remove(server, scope),
|
|
385
386
|
"workspace.update": ({ workspace: id, patch }) => workspace(id).update(patch),
|
|
386
387
|
"workspace.reload": ({ workspace: id }) => workspace(id).reload(),
|
|
387
|
-
"workspace.changes": ({ workspace: id,
|
|
388
|
-
"workspace.patches": async ({ workspace: id,
|
|
389
|
-
mapped(workspace(id).patches(
|
|
388
|
+
"workspace.changes": ({ workspace: id, source }) => workspace(id).changes(source),
|
|
389
|
+
"workspace.patches": async ({ workspace: id, source, paths, options }) =>
|
|
390
|
+
mapped(workspace(id).patches(source, paths, options), patchesWire),
|
|
390
391
|
"workspace.files": async ({ workspace: id }) => capabilityWire(await workspace(id).files()),
|
|
391
392
|
"workspace.git": async ({ workspace: id }) => capabilityWire(await workspace(id).git()),
|
|
392
393
|
"workspace.github": async ({ workspace: id }) => capabilityWire(await workspace(id).github()),
|
package/src/sqlite.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
+
import process from "node:process";
|
|
3
4
|
import { DatabaseSync } from "node:sqlite";
|
|
4
5
|
import type { SQLInputValue, StatementSync } from "node:sqlite";
|
|
5
6
|
import { asSeq } from "@nyte-ai/schema";
|
|
@@ -34,7 +35,6 @@ import {
|
|
|
34
35
|
} from "./store-rows.ts";
|
|
35
36
|
import { sqliteWorkers } from "./sqlite-workers.ts";
|
|
36
37
|
import { sessionIndexChanges, sqlSessionIndex } from "./session-index.ts";
|
|
37
|
-
import { claimStore } from "./store-owner.ts";
|
|
38
38
|
|
|
39
39
|
const BUSY_TIMEOUT_MS = 5_000;
|
|
40
40
|
|
|
@@ -43,16 +43,18 @@ const BACKUPS_KEPT = 3;
|
|
|
43
43
|
const PUT_BODY =
|
|
44
44
|
"INSERT INTO objects (id, body) VALUES (?, ?) ON CONFLICT (id) DO UPDATE SET body = excluded.body WHERE objects.body IS NULL";
|
|
45
45
|
|
|
46
|
-
/**
|
|
46
|
+
/** The local store's file: `NYTE_DB` when set, so a source run keeps its own chats, else `.nyte/nyte.db` under `home`. */
|
|
47
|
+
export function storePath(home: string): string {
|
|
48
|
+
return process.env["NYTE_DB"] ?? join(home, ".nyte", "nyte.db");
|
|
49
|
+
}
|
|
50
|
+
|
|
47
51
|
export function sqliteStore(input: { readonly path: string } | { readonly memory: true }): Store {
|
|
48
52
|
let database: DatabaseSync | null = null;
|
|
49
53
|
let opening: Promise<StoreConnection> | null = null;
|
|
50
54
|
let closing: Promise<void> | null = null;
|
|
51
55
|
let workers: ReturnType<typeof sqliteWorkers> | null = null;
|
|
52
|
-
let release: (() => void) | null = null;
|
|
53
56
|
|
|
54
57
|
const open = async (): Promise<StoreConnection> => {
|
|
55
|
-
const claim = "path" in input ? claimStore(input.path) : null;
|
|
56
58
|
let created: DatabaseSync | undefined;
|
|
57
59
|
|
|
58
60
|
try {
|
|
@@ -69,13 +71,11 @@ export function sqliteStore(input: { readonly path: string } | { readonly memory
|
|
|
69
71
|
created.exec("PRAGMA optimize = 0x10012");
|
|
70
72
|
} catch (error) {
|
|
71
73
|
created?.close();
|
|
72
|
-
claim?.();
|
|
73
74
|
|
|
74
75
|
throw error;
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
database = created;
|
|
78
|
-
release = claim;
|
|
79
79
|
workers = "path" in input ? sqliteWorkers(created, input.path) : null;
|
|
80
80
|
|
|
81
81
|
return connect(created, workers);
|
|
@@ -99,10 +99,8 @@ export function sqliteStore(input: { readonly path: string } | { readonly memory
|
|
|
99
99
|
await opening?.catch(() => undefined);
|
|
100
100
|
await workers?.close();
|
|
101
101
|
database?.close();
|
|
102
|
-
release?.();
|
|
103
102
|
workers = null;
|
|
104
103
|
database = null;
|
|
105
|
-
release = null;
|
|
106
104
|
opening = null;
|
|
107
105
|
})().finally(() => {
|
|
108
106
|
closing = null;
|
|
@@ -189,6 +187,7 @@ function sqliteTarget(database: DatabaseSync, path: string | null): MigrationTar
|
|
|
189
187
|
|
|
190
188
|
return "conflict";
|
|
191
189
|
}
|
|
190
|
+
|
|
192
191
|
updateSessionIndex(database, moved.name, moved.id, mintRevision());
|
|
193
192
|
}
|
|
194
193
|
|
|
@@ -308,6 +307,7 @@ function connect(
|
|
|
308
307
|
);
|
|
309
308
|
|
|
310
309
|
const deleteRef = database.prepare("DELETE FROM refs WHERE name = ?");
|
|
310
|
+
const audit = auditing(database);
|
|
311
311
|
const append = database.prepare("INSERT INTO log (session, seq, at, event) VALUES (?, ?, ?, ?)");
|
|
312
312
|
|
|
313
313
|
const readLog = database.prepare(
|
|
@@ -327,6 +327,7 @@ function connect(
|
|
|
327
327
|
const readGap = database.prepare(
|
|
328
328
|
"SELECT COUNT(*) AS events, COALESCE(SUM(LENGTH(CAST(event AS BLOB))), 0) AS bytes FROM log WHERE session = ? AND seq > ?",
|
|
329
329
|
);
|
|
330
|
+
|
|
330
331
|
const readVersion = database.prepare("SELECT version FROM store_meta WHERE id = 1");
|
|
331
332
|
const readDataVersion = database.prepare("PRAGMA data_version");
|
|
332
333
|
|
|
@@ -422,11 +423,23 @@ function connect(
|
|
|
422
423
|
}
|
|
423
424
|
|
|
424
425
|
const touched = new Map<SessionId, Revision>();
|
|
426
|
+
const current = new Map([...before].map(([name, row]) => [name, row?.revision ?? null]));
|
|
427
|
+
|
|
425
428
|
const revisions = updates.map((update) => {
|
|
426
429
|
const revision = mintRevision();
|
|
427
430
|
|
|
428
431
|
if (update.target === null) deleteRef.run(update.name);
|
|
429
432
|
else putRef.run(update.name, update.target, revision);
|
|
433
|
+
audit?.run(
|
|
434
|
+
Date.now(),
|
|
435
|
+
process.pid,
|
|
436
|
+
fenceNames([update]).find((name) => name.startsWith("processes/")) ?? null,
|
|
437
|
+
update.name,
|
|
438
|
+
current.get(update.name) ?? null,
|
|
439
|
+
update.target === null ? null : revision,
|
|
440
|
+
update.target,
|
|
441
|
+
);
|
|
442
|
+
current.set(update.name, update.target === null ? null : revision);
|
|
430
443
|
updateSessionIndex(database, update.name, update.target, revision);
|
|
431
444
|
const session = parseSessionRef(update.name);
|
|
432
445
|
|
|
@@ -496,6 +509,17 @@ function connect(
|
|
|
496
509
|
};
|
|
497
510
|
}
|
|
498
511
|
|
|
512
|
+
function auditing(database: DatabaseSync): StatementSync | null {
|
|
513
|
+
if (process.env.NYTE_STORE_AUDIT !== "1") return null;
|
|
514
|
+
database.exec(
|
|
515
|
+
"CREATE TABLE IF NOT EXISTS store_audit (id INTEGER PRIMARY KEY, at INTEGER NOT NULL, pid INTEGER NOT NULL, writer TEXT, name TEXT NOT NULL, before TEXT, after TEXT, target TEXT)",
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
return database.prepare(
|
|
519
|
+
"INSERT INTO store_audit (at, pid, writer, name, before, after, target) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
|
|
499
523
|
function updateSessionIndex(
|
|
500
524
|
database: DatabaseSync,
|
|
501
525
|
name: string,
|
|
@@ -503,6 +527,7 @@ function updateSessionIndex(
|
|
|
503
527
|
revision: Revision,
|
|
504
528
|
): void {
|
|
505
529
|
if (sessionIndexRef(name) === null) return;
|
|
530
|
+
|
|
506
531
|
const body =
|
|
507
532
|
target === null
|
|
508
533
|
? null
|
package/src/store-usage.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
2
|
import { DatabaseSync } from "node:sqlite";
|
|
4
3
|
import {
|
|
5
4
|
decodeObject,
|
|
@@ -17,6 +16,7 @@ import type {
|
|
|
17
16
|
UsageTotals,
|
|
18
17
|
UsageWindow,
|
|
19
18
|
} from "./usage-types.ts";
|
|
19
|
+
import { storePath } from "./sqlite.ts";
|
|
20
20
|
import { cacheSavings, usageTokens } from "./usage-summary.ts";
|
|
21
21
|
import { decodeEvent, parseNumber, parseText } from "./store-rows.ts";
|
|
22
22
|
|
|
@@ -129,6 +129,7 @@ export function projectUsageReport(
|
|
|
129
129
|
|
|
130
130
|
for (const spend of session.records) {
|
|
131
131
|
const { usage } = spend;
|
|
132
|
+
|
|
132
133
|
const amounts = [
|
|
133
134
|
usage.total,
|
|
134
135
|
usage.input,
|
|
@@ -201,7 +202,7 @@ export function projectUsageReport(
|
|
|
201
202
|
|
|
202
203
|
export function readNyteUsage(home: string, window: UsageWindow): UsageReport {
|
|
203
204
|
const readAt = Date.now();
|
|
204
|
-
const path =
|
|
205
|
+
const path = storePath(home);
|
|
205
206
|
|
|
206
207
|
if (!existsSync(path)) return projectUsageReport([], window, readAt);
|
|
207
208
|
let database: DatabaseSync | undefined;
|
|
@@ -268,11 +269,13 @@ export function readNyteUsage(home: string, window: UsageWindow): UsageReport {
|
|
|
268
269
|
const session = parseSessionRef(`sessions/${parseText(row.session) ?? ""}`);
|
|
269
270
|
|
|
270
271
|
if (session === null) throw new Error("Cannot read usage session.");
|
|
272
|
+
|
|
271
273
|
const event = decodeEvent(
|
|
272
274
|
parseNumber(row.seq),
|
|
273
275
|
parseNumber(row.at),
|
|
274
276
|
parseText(row.event) ?? "",
|
|
275
277
|
);
|
|
278
|
+
|
|
276
279
|
let heads = models.get(session);
|
|
277
280
|
|
|
278
281
|
if (heads === undefined) {
|