@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.
Files changed (102) hide show
  1. package/THIRD-PARTY-NOTICES.md +66 -0
  2. package/dist/chunks/{canvas-preview-5hxat7hh.js → canvas-preview-11nadadc.js} +8 -6
  3. package/dist/chunks/{canvas-preview-rp6fz0w5.js → canvas-preview-2jwbe14k.js} +5 -5
  4. package/dist/chunks/{canvas-preview-m02v4yy6.js → canvas-preview-55s0y68g.js} +3 -2
  5. package/dist/chunks/{canvas-preview-xn10d4f8.js → canvas-preview-5ekjx6sw.js} +9 -4
  6. package/dist/chunks/{canvas-preview-75db60xy.js → canvas-preview-8xc0vn7v.js} +5 -4
  7. package/dist/chunks/{canvas-preview-zn2d0k9d.js → canvas-preview-af86jz8t.js} +14 -6
  8. package/dist/chunks/{canvas-preview-d9wwhbtz.js → canvas-preview-aq5j4p25.js} +1 -1
  9. package/dist/chunks/{canvas-preview-j1tnf51w.js → canvas-preview-b2v8wfnz.js} +1 -1
  10. package/dist/chunks/canvas-preview-befkjea2.js +60 -0
  11. package/dist/chunks/canvas-preview-bxnqt8cd.js +103 -0
  12. package/dist/chunks/{canvas-preview-vax9ne4e.js → canvas-preview-cawy6vbz.js} +22 -11
  13. package/dist/chunks/{canvas-preview-senpjwep.js → canvas-preview-crmzxxt8.js} +3 -3
  14. package/dist/chunks/{canvas-preview-ymgz0937.js → canvas-preview-drctegz4.js} +8 -4
  15. package/dist/chunks/{canvas-preview-yrfz2ak6.js → canvas-preview-e3vnkkc5.js} +6 -6
  16. package/dist/chunks/canvas-preview-ec2mt7es.js +108 -0
  17. package/dist/chunks/{canvas-preview-1ta3avah.js → canvas-preview-fxa0rfb0.js} +10 -5
  18. package/dist/chunks/{canvas-preview-8xgt67ka.js → canvas-preview-g5fkjs34.js} +1 -1
  19. package/dist/chunks/{canvas-preview-g3qj3y3k.js → canvas-preview-jeddjex8.js} +13 -3
  20. package/dist/chunks/{canvas-preview-0nrnbt76.js → canvas-preview-k1yg4fgp.js} +199 -125
  21. package/dist/chunks/canvas-preview-kp75gb63.js +141 -0
  22. package/dist/chunks/{canvas-preview-ewgspmsd.js → canvas-preview-n2a35zv5.js} +1 -1
  23. package/dist/chunks/{canvas-preview-h8hshnff.js → canvas-preview-pkzqg96v.js} +1 -1
  24. package/dist/chunks/{canvas-preview-evh92ywd.js → canvas-preview-tnqe7k0b.js} +15 -10
  25. package/dist/chunks/{canvas-preview-ehf3vp31.js → canvas-preview-vm9zdp2t.js} +45 -58
  26. package/dist/chunks/{canvas-preview-76fwvd6e.js → canvas-preview-w54ymtb3.js} +1 -1
  27. package/dist/chunks/canvas-preview-y5y6ghaw.js +125 -0
  28. package/dist/chunks/{canvas-preview-98aj5p34.js → canvas-preview-ycepb05s.js} +1 -1
  29. package/dist/src/canvas-preview.js +7 -3
  30. package/dist/src/deploy/cloudflare/worker.js +1 -1
  31. package/dist/src/deploy/vercel/server/routes/[...].js +2 -2
  32. package/dist/src/deploy/vercel/server/utils/host.js +1 -1
  33. package/dist/src/deploy/vercel/server/workflows/drive.js +3 -3
  34. package/dist/src/images/exif.d.ts +1 -0
  35. package/dist/src/images/exif.js +6 -0
  36. package/dist/src/images/node.d.ts +37 -0
  37. package/dist/src/images/node.js +12 -0
  38. package/dist/src/images/photon.d.ts +3 -0
  39. package/dist/src/images/photon.js +7 -0
  40. package/dist/src/images/worker.d.ts +1 -0
  41. package/dist/src/images/worker.js +23 -0
  42. package/dist/src/index.d.ts +0 -1
  43. package/dist/src/index.js +23 -22
  44. package/dist/src/listen-fetch.js +3 -3
  45. package/dist/src/listen.js +4 -4
  46. package/dist/src/local/command.d.ts +5 -0
  47. package/dist/src/local/command.js +6 -2
  48. package/dist/src/local/files.js +2 -2
  49. package/dist/src/local/git-watch.d.ts +1 -1
  50. package/dist/src/local/git-watch.js +1 -1
  51. package/dist/src/local/git.js +3 -3
  52. package/dist/src/local/github.js +2 -2
  53. package/dist/src/local/jobs.d.ts +1 -1
  54. package/dist/src/local/jobs.js +2 -1
  55. package/dist/src/local/provider.js +9 -9
  56. package/dist/src/local/snapshots.js +2 -2
  57. package/dist/src/local/worktrees.js +2 -2
  58. package/dist/src/mcp/connect.d.ts +0 -13
  59. package/dist/src/mcp/connect.js +1 -1
  60. package/dist/src/mcp/hub.js +2 -2
  61. package/dist/src/open.d.ts +2 -30
  62. package/dist/src/open.js +20 -18
  63. package/dist/src/serve/binding.js +1 -1
  64. package/dist/src/serve/handler.js +2 -2
  65. package/dist/src/sqlite.d.ts +2 -1
  66. package/dist/src/sqlite.js +4 -3
  67. package/dist/src/store-usage.js +4 -1
  68. package/dist/src/usage-history.d.ts +54 -0
  69. package/dist/src/usage-history.js +14 -0
  70. package/dist/src/usage-service.d.ts +2 -1
  71. package/dist/src/usage-service.js +7 -3
  72. package/dist/src/usage-worker.js +8 -4
  73. package/dist/src/usage.d.ts +150 -27
  74. package/dist/src/usage.js +12 -2
  75. package/package.json +16 -8
  76. package/src/canvas-preview.ts +4 -3
  77. package/src/images/exif.ts +69 -0
  78. package/src/images/node.ts +163 -0
  79. package/src/images/photon.ts +141 -0
  80. package/src/images/worker.ts +29 -0
  81. package/src/index.ts +19 -1
  82. package/src/local/command.ts +64 -12
  83. package/src/local/files.ts +8 -3
  84. package/src/local/git-watch.ts +16 -6
  85. package/src/local/git.ts +7 -2
  86. package/src/local/github.ts +3 -4
  87. package/src/local/jobs.ts +55 -69
  88. package/src/local/snapshots.ts +4 -2
  89. package/src/local/worktrees.ts +4 -4
  90. package/src/mcp/connect.ts +1 -14
  91. package/src/open.ts +16 -36
  92. package/src/serve/binding.ts +4 -3
  93. package/src/sqlite.ts +33 -8
  94. package/src/store-usage.ts +5 -2
  95. package/src/usage-history.ts +219 -0
  96. package/src/usage-service.ts +12 -3
  97. package/src/usage.ts +251 -176
  98. package/dist/chunks/canvas-preview-a3yw0j70.js +0 -72
  99. package/dist/chunks/canvas-preview-azg56359.js +0 -68
  100. package/dist/src/store-owner.d.ts +0 -15
  101. package/dist/src/store-owner.js +0 -9
  102. 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` answers whether a pty's shell sits at a prompt: by its OSC 133 prompt markers once it sent one, else when it has printed nothing for 300 ms and the cursor's line holds text after the last newline, as a prompt does (false for a pipe); `stop` sends SIGTERM to a pipe's process group (SIGHUP to a pty), SIGKILL after 2 s, 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). */
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 === null && driver.idle(),
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 group = process.platform !== "win32";
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: group,
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
- : spawn(file ?? "", args, settings);
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
- child.on("close", (code) => finish(code));
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 (group && child.pid !== undefined) process.kill(-child.pid, signal);
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 (marker !== null) return marker !== "C";
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
- return quietSince > 0 && Date.now() - quietSince >= QUIET_MS && line.trim() !== "";
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"));
@@ -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 ? null : { root, gitDir, prefix };
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> => {
@@ -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: repo.prefix === "" ? root : join(root, repo.prefix).replace(/\/+$/u, ""),
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 {
@@ -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 (for the CLI, \`nyte service stop\`).`,
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
- await mkdir(join(home, ".nyte"), { recursive: true, mode: 0o700 });
85
+ const path = storePath(home);
86
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
113
87
 
114
- return sqliteStore({ path: join(home, ".nyte", "nyte.db") });
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),
@@ -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, run }) => workspace(id).changes(run),
388
- "workspace.patches": async ({ workspace: id, run, paths, options }) =>
389
- mapped(workspace(id).patches(run, paths, options), patchesWire),
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
- /** Returns a `Store` over one `node:sqlite` `DatabaseSync`: a file at `path` (WAL mode, opened only by the process that `claimStore` makes its owner, so a second process rejects with `SdkError("unavailable")` before it reads or migrates the file) or an in-memory database. The first `open()` brings it to core's `STORE_VERSION` with `migrateStore` (core's `objects`, `refs`, `log` and `store_meta` tables; each commit one `BEGIN IMMEDIATE`), a file store whose migration rewrites records first saving those refs' rows (name, target, revision; objects are never deleted, so the rows restore them) to `backups/store-<time>-<pid>-<uuid>-v<from>-v<to>.json` beside it, keeping the newest three backups, and rejects with `SdkError("unavailable")` for a store a newer Nyte migrated; a `migrations` table an older Nyte left is kept and ignored. `put` ids the value with core's `objectId` and stores its JSON once; `putBlob` buffers the stream, ids it with `blobId` and stores the bytes once; an object and a blob that share an id share the row, each column filled once (§41f); `blob` reads the bytes back with their size and a null mime. `refs.set` is one `BEGIN IMMEDIATE` transaction with `RefStore.set`'s semantics, each revision a random UUID, that first rejects with `SdkError("unavailable")` when a newer Nyte has migrated the store since it opened. `list(prefix)` reads the name range from `prefix` in name order (BINARY, by code point); `log` returns the events after `after` in seq order up to `limit`, each validated as a `SessionEvent`; `tail` is the session's last seq, or 0; `commitMark` is the writer connection's `PRAGMA data_version`, which moves only for another connection's commit. `checkpoints` keeps one fold checkpoint per session in `fold_checkpoints`, a write at a lower seq leaving the kept one, and `gap` counts a session's events past a seq and sums their UTF-8 bytes. File stores run `list` and `log` on a read-only worker and WAL checkpoints on a separate worker, checking at 1,000 frames and after 100 ms idle with a 10,000-frame writer backstop; unavailable workers warn and fall back to the main connection. Checkpoints use fullfsync on macOS, WAL reuse retains at most 32 MiB, and opening refreshes planner statistics. Writes binding more than 64 KiB use uncached statements. Every `open()` returns a connection over the one writer; `close()` waits for opening and worker shutdown before closing it, and an open during close waits to reopen. */
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
@@ -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 = join(home, ".nyte", "nyte.db");
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) {