@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.
- package/THIRD-PARTY-NOTICES.md +66 -0
- package/dist/chunks/{canvas-preview-rp6fz0w5.js → canvas-preview-2jwbe14k.js} +5 -5
- package/dist/chunks/{canvas-preview-j1tnf51w.js → canvas-preview-3a6tst4d.js} +1 -1
- 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-g3qj3y3k.js → canvas-preview-8hr08aeh.js} +13 -3
- 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-ewgspmsd.js → canvas-preview-b3r1tna2.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-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-0nrnbt76.js → canvas-preview-eehzfefa.js} +199 -125
- package/dist/chunks/{canvas-preview-5hxat7hh.js → canvas-preview-geg2gckp.js} +8 -6
- package/dist/chunks/{canvas-preview-8xgt67ka.js → canvas-preview-gh7c9831.js} +20 -16
- package/dist/chunks/{canvas-preview-1ta3avah.js → canvas-preview-k2xaz0ym.js} +10 -5
- package/dist/chunks/canvas-preview-kp75gb63.js +141 -0
- package/dist/chunks/{canvas-preview-evh92ywd.js → canvas-preview-mstbvwz3.js} +26 -11
- package/dist/chunks/{canvas-preview-h8hshnff.js → canvas-preview-pkzqg96v.js} +1 -1
- 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-vax9ne4e.js → canvas-preview-y7kq0c8q.js} +22 -11
- 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/mcp/hub.ts +18 -9
- package/src/open.ts +16 -36
- package/src/serve/binding.ts +4 -3
- package/src/sqlite.ts +46 -9
- 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/files.ts
CHANGED
|
@@ -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
|
|
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
|
|
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.
|
|
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
|
|
package/src/local/git-watch.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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")
|
|
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
|
|
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
|
-
: {
|
|
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> => {
|
package/src/local/github.ts
CHANGED
|
@@ -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 =
|
|
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
|
|
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`
|
|
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/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
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|