@nyte-ai/host 0.0.18 → 0.0.21

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 (103) hide show
  1. package/THIRD-PARTY-NOTICES.md +66 -0
  2. package/dist/chunks/{canvas-preview-rp6fz0w5.js → canvas-preview-2jwbe14k.js} +5 -5
  3. package/dist/chunks/{canvas-preview-j1tnf51w.js → canvas-preview-3a6tst4d.js} +1 -1
  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-g3qj3y3k.js → canvas-preview-8hr08aeh.js} +13 -3
  7. package/dist/chunks/{canvas-preview-75db60xy.js → canvas-preview-8xc0vn7v.js} +5 -4
  8. package/dist/chunks/{canvas-preview-zn2d0k9d.js → canvas-preview-af86jz8t.js} +14 -6
  9. package/dist/chunks/{canvas-preview-d9wwhbtz.js → canvas-preview-aq5j4p25.js} +1 -1
  10. package/dist/chunks/{canvas-preview-ewgspmsd.js → canvas-preview-b3r1tna2.js} +1 -1
  11. package/dist/chunks/canvas-preview-befkjea2.js +60 -0
  12. package/dist/chunks/canvas-preview-bxnqt8cd.js +103 -0
  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-0nrnbt76.js → canvas-preview-eehzfefa.js} +199 -125
  18. package/dist/chunks/{canvas-preview-5hxat7hh.js → canvas-preview-geg2gckp.js} +8 -6
  19. package/dist/chunks/{canvas-preview-8xgt67ka.js → canvas-preview-gh7c9831.js} +20 -16
  20. package/dist/chunks/{canvas-preview-1ta3avah.js → canvas-preview-k2xaz0ym.js} +10 -5
  21. package/dist/chunks/canvas-preview-kp75gb63.js +141 -0
  22. package/dist/chunks/{canvas-preview-evh92ywd.js → canvas-preview-mstbvwz3.js} +26 -11
  23. package/dist/chunks/{canvas-preview-h8hshnff.js → canvas-preview-pkzqg96v.js} +1 -1
  24. package/dist/chunks/{canvas-preview-ehf3vp31.js → canvas-preview-vm9zdp2t.js} +45 -58
  25. package/dist/chunks/{canvas-preview-76fwvd6e.js → canvas-preview-w54ymtb3.js} +1 -1
  26. package/dist/chunks/canvas-preview-y5y6ghaw.js +125 -0
  27. package/dist/chunks/{canvas-preview-vax9ne4e.js → canvas-preview-y7kq0c8q.js} +22 -11
  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/mcp/hub.ts +18 -9
  92. package/src/open.ts +16 -36
  93. package/src/serve/binding.ts +4 -3
  94. package/src/sqlite.ts +46 -9
  95. package/src/store-usage.ts +5 -2
  96. package/src/usage-history.ts +219 -0
  97. package/src/usage-service.ts +12 -3
  98. package/src/usage.ts +251 -176
  99. package/dist/chunks/canvas-preview-a3yw0j70.js +0 -72
  100. package/dist/chunks/canvas-preview-azg56359.js +0 -68
  101. package/dist/src/store-owner.d.ts +0 -15
  102. package/dist/src/store-owner.js +0 -9
  103. package/src/store-owner.ts +0 -98
@@ -167,7 +167,7 @@ export function localFiles(
167
167
  if (stats === null) return null;
168
168
 
169
169
  if (stats.isSymbolicLink()) {
170
- const link = await readlink(target);
170
+ const link = await readLink(target);
171
171
 
172
172
  return { kind: "symlink", size: stats.size, version: hashVersion(link), target: link };
173
173
  }
@@ -218,7 +218,7 @@ export function localFiles(
218
218
 
219
219
  const current =
220
220
  (await currentVersion(target)) ??
221
- (link.isSymbolicLink() ? hashVersion(await readlink(target)) : null);
221
+ (link.isSymbolicLink() ? hashVersion(await readLink(target)) : null);
222
222
 
223
223
  if (removeOptions?.expect !== undefined && removeOptions.expect !== current)
224
224
  return { kind: "stale", current };
@@ -326,6 +326,10 @@ async function currentVersion(path: string): Promise<FileVersion | null> {
326
326
  return stats.isDirectory() ? directoryVersion(path) : hashFile(path).catch(missing);
327
327
  }
328
328
 
329
+ async function readLink(path: string): Promise<string> {
330
+ return (await readlink(path)).split(sep).join("/");
331
+ }
332
+
329
333
  async function resolveLink(path: string): Promise<string> {
330
334
  const link = await lstat(path).catch(missing);
331
335
 
@@ -529,7 +533,7 @@ function rejectFailure(code: number | null, stderr: string): void {
529
533
  }
530
534
 
531
535
  function ripgrepPath(path: string): string {
532
- return path.replace(/^\.\//, "").split(sep).join("/");
536
+ return path.split(sep).join("/").replace(/^\.\//, "");
533
537
  }
534
538
 
535
539
  async function* grep(
@@ -586,6 +590,7 @@ async function* grep(
586
590
  } finally {
587
591
  lines.close();
588
592
  child.kill();
593
+ await closed;
589
594
  }
590
595
  }
591
596
 
@@ -1,4 +1,5 @@
1
- import { isAbsolute, relative, sep } from "node:path";
1
+ import { lstat } from "node:fs/promises";
2
+ import { isAbsolute, join, relative, sep } from "node:path";
2
3
  import type { Disposer, GitStatus } from "@nyte-ai/schema";
3
4
  import type { FeedSink } from "./feed.ts";
4
5
  import { createIgnores, watchTree } from "./tree.ts";
@@ -33,7 +34,7 @@ const GIT_STATE = new Set([
33
34
  "rebase-apply",
34
35
  ]);
35
36
 
36
- /** Returns the status watch `localGit` shares among its watchers: the first subscriber starts it and the last one stops it, and each gets the status at once, then again whenever a change could move it. A recursive `fs.watch` on the workspace hears worktree paths that `.gitignore` and `.git/info/exclude` do not ignore, and, in the git directory and the common directory, only HEAD, the index, branch and remote refs, `packed-refs`, `config` and the merge, cherry-pick, revert and rebase state, so object writes, logs and Nyte's snapshot refs read nothing; the `.git` entry itself appearing or going drops the remembered repository and starts over. Only where `fs.watch` is unavailable does it poll, every 2 s. */
37
+ /** Returns the status watch `localGit` shares among its watchers: the first subscriber starts it and the last one stops it, and each gets the status at once, then again whenever a change could move it. A recursive `fs.watch` on the workspace hears worktree paths that `.gitignore` and `.git/info/exclude` do not ignore, and, in the git directory and the common directory, only HEAD, the index, branch and remote refs, `packed-refs`, `config` and the merge, cherry-pick, revert and rebase state, so object writes, logs and Nyte's snapshot refs read nothing; the `.git` entry itself appearing or going (not a change inside it, which Windows also reports on the folder) drops the remembered repository and starts over. Only where `fs.watch` is unavailable does it poll, every 2 s. */
37
38
  export function createWatcher(
38
39
  cwd: string,
39
40
  read: () => Promise<GitStatus>,
@@ -88,7 +89,10 @@ export function createWatcher(
88
89
  );
89
90
  };
90
91
 
91
- void repository().then((repo) => {
92
+ const gitEntry = async (): Promise<boolean> =>
93
+ (await lstat(join(cwd, ".git")).catch(() => null)) !== null;
94
+
95
+ void Promise.all([repository(), gitEntry()]).then(([repo, hadGit]) => {
92
96
  if (stopped) return;
93
97
  const insideGitDir = repo === null ? null : pathInside(cwd, repo.gitDir);
94
98
  const ignores = repo === null ? null : createIgnores(repo.root);
@@ -99,7 +103,11 @@ export function createWatcher(
99
103
  let found: "none" | "status" = "none";
100
104
 
101
105
  for (const path of paths) {
102
- if (path === ".git") return "repository";
106
+ if (path === ".git") {
107
+ if ((await gitEntry()) !== hadGit) return "repository";
108
+
109
+ continue;
110
+ }
103
111
 
104
112
  if (repo === null || ignores === null) continue;
105
113
 
@@ -115,7 +123,10 @@ export function createWatcher(
115
123
  if (path.endsWith(".gitignore"))
116
124
  ignores.forget(`${repo.prefix}${path}`.split("/").slice(0, -1).join("/"));
117
125
 
118
- if (found === "none" && !(await ignores.ignored(`${repo.prefix}${path}`, false)))
126
+ if (found !== "none") continue;
127
+ const directory = (await lstat(join(cwd, path)).catch(() => null))?.isDirectory();
128
+
129
+ if (!(await ignores.ignored(`${repo.prefix}${path}`, directory === true)))
119
130
  found = "status";
120
131
  }
121
132
 
@@ -175,7 +186,6 @@ function movesStatus(path: string): boolean {
175
186
  return (
176
187
  path === "" ||
177
188
  GIT_STATE.has(top) ||
178
- path === "refs" ||
179
189
  path.startsWith("refs/heads/") ||
180
190
  path.startsWith("refs/remotes/")
181
191
  );
package/src/local/git.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { lstat, readFile, stat } from "node:fs/promises";
2
- import { join } from "node:path";
2
+ import { join, resolve } from "node:path";
3
3
  import { asId, SdkError } from "@nyte-ai/schema";
4
4
  import type {
5
5
  BlameView,
@@ -101,7 +101,12 @@ export function localGit(env: ExecutionEnv, options?: { readonly git?: string })
101
101
 
102
102
  return root === undefined || gitDir === undefined
103
103
  ? null
104
- : { root, gitDir, commonDir: commonDir || gitDir, prefix };
104
+ : {
105
+ root: resolve(root),
106
+ gitDir: resolve(gitDir),
107
+ commonDir: resolve(commonDir || gitDir),
108
+ prefix,
109
+ };
105
110
  };
106
111
 
107
112
  const repository = async (): Promise<Repository | null> => {
@@ -1,4 +1,3 @@
1
- import { spawn } from "node:child_process";
2
1
  import { homedir } from "node:os";
3
2
  import type { ChildProcess } from "node:child_process";
4
3
  import { Type } from "typebox";
@@ -17,7 +16,7 @@ import type {
17
16
  } from "@nyte-ai/schema";
18
17
  import { dev } from "@nyte-ai/core";
19
18
  import type { ExecutionEnv, GitHubAccountApi, GitHubApi } from "@nyte-ai/core";
20
- import { findExecutable, run } from "./command.ts";
19
+ import { findExecutable, killTree, run, spawnCommand } from "./command.ts";
21
20
 
22
21
  const ENV = {
23
22
  GH_PROMPT_DISABLED: "1",
@@ -255,7 +254,7 @@ function startSignIn(file: string, cwd: string, ended: () => void): SignIn {
255
254
  let verificationUri: string | undefined;
256
255
  let done = false;
257
256
 
258
- const child: ChildProcess = spawn(
257
+ const child: ChildProcess = spawnCommand(
259
258
  file,
260
259
  [
261
260
  "auth",
@@ -278,7 +277,7 @@ function startSignIn(file: string, cwd: string, ended: () => void): SignIn {
278
277
  child.stdout?.destroy();
279
278
  child.stderr?.destroy();
280
279
 
281
- if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
280
+ if (child.exitCode === null && child.signalCode === null) killTree(child, "SIGKILL");
282
281
  started.resolve(start);
283
282
  ended();
284
283
  };
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/mcp/hub.ts CHANGED
@@ -104,8 +104,18 @@ function hub(input: {
104
104
  const contents = new Map<string, McpFileContents>();
105
105
  const watched = new Map<string, Disposer>();
106
106
  const userPath = join(input.home, ".nyte", "mcp.json");
107
+ const retiring = new Set<Promise<void>>();
107
108
  let closed = false;
108
109
 
110
+ const retire = (client: McpClient | null): Promise<void> => {
111
+ if (client === null) return Promise.resolve();
112
+ const done = client.close().catch(() => undefined);
113
+ retiring.add(done);
114
+ void done.then(() => retiring.delete(done));
115
+
116
+ return done;
117
+ };
118
+
109
119
  const notify = (pool: Pool): void => {
110
120
  for (const listener of pool.listeners) listener();
111
121
  };
@@ -149,6 +159,7 @@ function hub(input: {
149
159
  } | null = null;
150
160
 
151
161
  let client: McpClient | null = null;
162
+ let opening: McpClient | null = null;
152
163
  let stderr: () => string | null = () => null;
153
164
  let challenge: OAuthChallenge | null = null;
154
165
  let ended = false;
@@ -184,7 +195,7 @@ function hub(input: {
184
195
  const drop = (message: string): void => {
185
196
  const previous = client;
186
197
  client = null;
187
- void previous?.close().catch(() => undefined);
198
+ void retire(previous);
188
199
 
189
200
  if (ended) return;
190
201
  set({ kind: "failed", message, stderr: stderr() });
@@ -197,11 +208,7 @@ function hub(input: {
197
208
 
198
209
  if (ended) return;
199
210
 
200
- const next = new TracingMcpClient({
201
- name: "nyte",
202
- version: "0.1.0",
203
- requestTimeoutMs: link.entry.timeoutMs,
204
- });
211
+ const next = new TracingMcpClient({ name: "nyte", version: "0.1.0" });
205
212
 
206
213
  next.onClose(() => {
207
214
  if (client === next) drop(`The connection to ${entry.name} closed.`);
@@ -229,10 +236,11 @@ function hub(input: {
229
236
  transport instanceof StdioTransport && transport.stderr !== ""
230
237
  ? transport.stderr.slice(-STDERR_TAIL)
231
238
  : null;
239
+ opening = next;
232
240
  await next.connect(transport);
233
241
 
234
242
  if (ended) {
235
- await next.close();
243
+ await retire(next);
236
244
 
237
245
  return;
238
246
  }
@@ -261,7 +269,7 @@ function hub(input: {
261
269
  await refresh();
262
270
  } catch (error) {
263
271
  if (client === next) client = null;
264
- await next.close().catch(() => undefined);
272
+ await retire(next);
265
273
 
266
274
  if (ended) return;
267
275
 
@@ -321,7 +329,7 @@ function hub(input: {
321
329
  clearTimeout(timer);
322
330
  const previous = client;
323
331
  client = null;
324
- await previous?.close().catch(() => undefined);
332
+ await Promise.all([retire(previous), retire(opening)]);
325
333
  },
326
334
  };
327
335
 
@@ -788,6 +796,7 @@ function hub(input: {
788
796
 
789
797
  for (const pool of live) shut(pool);
790
798
  await Promise.all(links.map((link) => link.close()));
799
+ await Promise.all(retiring);
791
800
  },
792
801
  };
793
802
  }