@nyte-ai/host 0.0.0-stage → 0.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +6 -2
- package/THIRD-PARTY-NOTICES.md +866 -0
- package/dist/chunks/canvas-preview-0cmbkp2j.js +65 -0
- package/dist/chunks/canvas-preview-0nq75h9k.js +7 -0
- package/dist/chunks/canvas-preview-11nadadc.js +241 -0
- package/dist/chunks/canvas-preview-1sq7z9jq.js +17 -0
- package/dist/chunks/canvas-preview-2jwbe14k.js +130 -0
- package/dist/chunks/canvas-preview-2zdyh9ny.js +26 -0
- package/dist/chunks/canvas-preview-31w7xm8z.js +34 -0
- package/dist/chunks/canvas-preview-4b6sayhg.js +295 -0
- package/dist/chunks/canvas-preview-4e0pxa32.js +63 -0
- package/dist/chunks/canvas-preview-518dh8by.js +169 -0
- package/dist/chunks/canvas-preview-55s0y68g.js +387 -0
- package/dist/chunks/canvas-preview-5ekjx6sw.js +613 -0
- package/dist/chunks/canvas-preview-5mex725c.js +14 -0
- package/dist/chunks/canvas-preview-5z63am63.js +295 -0
- package/dist/chunks/canvas-preview-65xqjw7f.js +20 -0
- package/dist/chunks/canvas-preview-69fkk9yn.js +95 -0
- package/dist/chunks/canvas-preview-8xc0vn7v.js +458 -0
- package/dist/chunks/canvas-preview-9aqczacs.js +73 -0
- package/dist/chunks/canvas-preview-af86jz8t.js +141 -0
- package/dist/chunks/canvas-preview-aq5j4p25.js +240 -0
- package/dist/chunks/canvas-preview-b2v8wfnz.js +36 -0
- package/dist/chunks/canvas-preview-befkjea2.js +60 -0
- package/dist/chunks/canvas-preview-bxnqt8cd.js +103 -0
- package/dist/chunks/canvas-preview-cawy6vbz.js +352 -0
- package/dist/chunks/canvas-preview-crmzxxt8.js +518 -0
- package/dist/chunks/canvas-preview-drctegz4.js +591 -0
- package/dist/chunks/canvas-preview-e3vnkkc5.js +134 -0
- package/dist/chunks/canvas-preview-ec2mt7es.js +108 -0
- package/dist/chunks/canvas-preview-fxa0rfb0.js +61 -0
- package/dist/chunks/canvas-preview-g5fkjs34.js +598 -0
- package/dist/chunks/canvas-preview-jeddjex8.js +166 -0
- package/dist/chunks/canvas-preview-k1yg4fgp.js +571 -0
- package/dist/chunks/canvas-preview-kdqgrstc.js +194 -0
- package/dist/chunks/canvas-preview-kn7kvbfj.js +12 -0
- package/dist/chunks/canvas-preview-kp75gb63.js +141 -0
- package/dist/chunks/canvas-preview-mgcx2b2p.js +271 -0
- package/dist/chunks/canvas-preview-n2a35zv5.js +81 -0
- package/dist/chunks/canvas-preview-pkzqg96v.js +63 -0
- package/dist/chunks/canvas-preview-rp4x6y1h.js +144 -0
- package/dist/chunks/canvas-preview-s94ebwf5.js +112 -0
- package/dist/chunks/canvas-preview-skzwvw4d.js +23 -0
- package/dist/chunks/canvas-preview-srgfpdmf.js +60 -0
- package/dist/chunks/canvas-preview-tjhabynd.js +252 -0
- package/dist/chunks/canvas-preview-tnqe7k0b.js +374 -0
- package/dist/chunks/canvas-preview-vaatfznm.js +189 -0
- package/dist/chunks/canvas-preview-vm9zdp2t.js +398 -0
- package/dist/chunks/canvas-preview-w54ymtb3.js +272 -0
- package/dist/chunks/canvas-preview-wg80rbc2.js +46 -0
- package/dist/chunks/canvas-preview-y5y6ghaw.js +125 -0
- package/dist/chunks/canvas-preview-ycepb05s.js +176 -0
- package/dist/chunks/canvas-preview-yjz0f7n4.js +50 -0
- package/dist/chunks/canvas-preview-zaaj99wd.js +244 -0
- package/dist/src/canvas-preview.d.ts +29 -0
- package/dist/src/canvas-preview.js +271 -0
- package/dist/src/credentials.d.ts +44 -0
- package/dist/src/credentials.js +10 -0
- package/dist/src/deploy/cloudflare/worker.d.ts +50 -0
- package/dist/src/deploy/cloudflare/worker.js +196 -0
- package/dist/src/deploy/vercel/nitro.config.d.ts +2 -0
- package/dist/src/deploy/vercel/nitro.config.js +30 -0
- package/dist/src/deploy/vercel/server/routes/[...].d.ts +2 -0
- package/dist/src/deploy/vercel/server/routes/[...].js +18 -0
- package/dist/src/deploy/vercel/server/utils/host.d.ts +13 -0
- package/dist/src/deploy/vercel/server/utils/host.js +7 -0
- package/dist/src/deploy/vercel/server/utils/repair.d.ts +1 -0
- package/dist/src/deploy/vercel/server/utils/repair.js +6 -0
- package/dist/src/deploy/vercel/server/utils/step.d.ts +10 -0
- package/dist/src/deploy/vercel/server/utils/step.js +6 -0
- package/dist/src/deploy/vercel/server/workflows/drive.d.ts +14 -0
- package/dist/src/deploy/vercel/server/workflows/drive.js +13 -0
- package/dist/src/durable-object.d.ts +14 -0
- package/dist/src/durable-object.js +231 -0
- package/dist/src/hash.d.ts +1 -0
- package/dist/src/hash.js +6 -0
- package/dist/src/identity-key.d.ts +11 -0
- package/dist/src/identity-key.js +9 -0
- package/dist/src/identity.d.ts +12 -0
- package/dist/src/identity.js +6 -0
- 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 +20 -0
- package/dist/src/index.js +121 -0
- package/dist/src/key-file.d.ts +1 -0
- package/dist/src/key-file.js +7 -0
- package/dist/src/listen-fetch.d.ts +8 -0
- package/dist/src/listen-fetch.js +9 -0
- package/dist/src/listen.d.ts +4 -0
- package/dist/src/listen.js +10 -0
- package/dist/src/local/command.d.ts +20 -0
- package/dist/src/local/command.js +12 -0
- package/dist/src/local/feed.d.ts +7 -0
- package/dist/src/local/feed.js +6 -0
- package/dist/src/local/files.d.ts +18 -0
- package/dist/src/local/files.js +13 -0
- package/dist/src/local/git-watch.d.ts +15 -0
- package/dist/src/local/git-watch.js +7 -0
- package/dist/src/local/git.d.ts +5 -0
- package/dist/src/local/git.js +13 -0
- package/dist/src/local/github.d.ts +30 -0
- package/dist/src/local/github.js +9 -0
- package/dist/src/local/identity.d.ts +3 -0
- package/dist/src/local/identity.js +7 -0
- package/dist/src/local/jobs.d.ts +7 -0
- package/dist/src/local/jobs.js +7 -0
- package/dist/src/local/legacy-trust.d.ts +10 -0
- package/dist/src/local/legacy-trust.js +6 -0
- package/dist/src/local/patch.d.ts +16 -0
- package/dist/src/local/patch.js +17 -0
- package/dist/src/local/provider.d.ts +11 -0
- package/dist/src/local/provider.js +20 -0
- package/dist/src/local/snapshots.d.ts +7 -0
- package/dist/src/local/snapshots.js +11 -0
- package/dist/src/local/text.d.ts +1 -0
- package/dist/src/local/text.js +6 -0
- package/dist/src/local/tree.d.ts +50 -0
- package/dist/src/local/tree.js +16 -0
- package/dist/src/local/workspace-migration.d.ts +2 -0
- package/dist/src/local/workspace-migration.js +7 -0
- package/dist/src/local/worktrees.d.ts +46 -0
- package/dist/src/local/worktrees.js +14 -0
- package/dist/src/mcp/config.d.ts +75 -0
- package/dist/src/mcp/config.js +12 -0
- package/dist/src/mcp/connect.d.ts +15 -0
- package/dist/src/mcp/connect.js +9 -0
- package/dist/src/mcp/hub.d.ts +19 -0
- package/dist/src/mcp/hub.js +11 -0
- package/dist/src/mcp/oauth.d.ts +46 -0
- package/dist/src/mcp/oauth.js +12 -0
- package/dist/src/mcp/tools.d.ts +29 -0
- package/dist/src/mcp/tools.js +12 -0
- package/dist/src/open.d.ts +14 -0
- package/dist/src/open.js +46 -0
- package/dist/src/plugins/loader.d.ts +21 -0
- package/dist/src/plugins/loader.js +11 -0
- package/dist/src/postgres.d.ts +19 -0
- package/dist/src/postgres.js +8 -0
- package/dist/src/providers.d.ts +16 -0
- package/dist/src/providers.js +112 -0
- package/dist/src/secrets.d.ts +12 -0
- package/dist/src/secrets.js +8 -0
- package/dist/src/serve/binding.d.ts +39 -0
- package/dist/src/serve/binding.js +6 -0
- package/dist/src/serve/handler.d.ts +15 -0
- package/dist/src/serve/handler.js +8 -0
- package/dist/src/serve/stream-server.d.ts +6 -0
- package/dist/src/serve/stream-server.js +6 -0
- package/dist/src/session-index.d.ts +11 -0
- package/dist/src/session-index.js +11 -0
- package/dist/src/shell-env.d.ts +14 -0
- package/dist/src/shell-env.js +108 -0
- package/dist/src/sqlite-workers.d.ts +6 -0
- package/dist/src/sqlite-workers.js +6 -0
- package/dist/src/sqlite.d.ts +8 -0
- package/dist/src/sqlite.js +11 -0
- package/dist/src/store-rows.d.ts +13 -0
- package/dist/src/store-rows.js +26 -0
- package/dist/src/store-usage.d.ts +25 -0
- package/dist/src/store-usage.js +19 -0
- package/dist/src/usage-history.d.ts +54 -0
- package/dist/src/usage-history.js +14 -0
- package/dist/src/usage-scan.d.ts +24 -0
- package/dist/src/usage-scan.js +65 -0
- package/dist/src/usage-service.d.ts +23 -0
- package/dist/src/usage-service.js +16 -0
- package/dist/src/usage-summary.d.ts +9 -0
- package/dist/src/usage-summary.js +16 -0
- package/dist/src/usage-types.d.ts +86 -0
- package/dist/src/usage-types.js +0 -0
- package/dist/src/usage-worker.d.ts +1 -0
- package/dist/src/usage-worker.js +29 -0
- package/dist/src/usage.d.ts +195 -0
- package/dist/src/usage.js +23 -0
- package/package.json +140 -3
- package/src/canvas-preview.ts +403 -0
- package/src/credentials.ts +294 -0
- package/src/deploy/cloudflare/README.md +3 -0
- package/src/deploy/cloudflare/worker.ts +306 -0
- package/src/deploy/cloudflare/wrangler.jsonc +12 -0
- package/src/deploy/vercel/README.md +3 -0
- package/src/deploy/vercel/nitro.config.ts +29 -0
- package/src/deploy/vercel/server/routes/[...].ts +10 -0
- package/src/deploy/vercel/server/utils/host.ts +104 -0
- package/src/deploy/vercel/server/utils/repair.ts +35 -0
- package/src/deploy/vercel/server/utils/step.ts +25 -0
- package/src/deploy/vercel/server/workflows/drive.ts +46 -0
- package/src/deploy/vercel/tsconfig.json +9 -0
- package/src/deploy/vercel/types/bun.d.ts +2 -0
- package/src/durable-object.ts +386 -0
- package/src/hash.ts +5 -0
- package/src/identity-key.ts +20 -0
- package/src/identity.ts +66 -0
- 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 +39 -0
- package/src/key-file.ts +53 -0
- package/src/listen-fetch.ts +409 -0
- package/src/listen.ts +228 -0
- package/src/local/command.ts +161 -0
- package/src/local/feed.ts +70 -0
- package/src/local/files.ts +817 -0
- package/src/local/git-watch.ts +199 -0
- package/src/local/git.ts +919 -0
- package/src/local/github.ts +691 -0
- package/src/local/identity.ts +21 -0
- package/src/local/jobs.ts +573 -0
- package/src/local/legacy-trust.ts +105 -0
- package/src/local/patch.ts +334 -0
- package/src/local/provider.ts +184 -0
- package/src/local/snapshots.ts +778 -0
- package/src/local/text.ts +9 -0
- package/src/local/tree.ts +398 -0
- package/src/local/workspace-migration.ts +95 -0
- package/src/local/worktrees.ts +239 -0
- package/src/mcp/config.ts +330 -0
- package/src/mcp/connect.ts +92 -0
- package/src/mcp/hub.ts +836 -0
- package/src/mcp/oauth.ts +436 -0
- package/src/mcp/tools.ts +333 -0
- package/src/open.ts +449 -0
- package/src/plugins/loader.ts +186 -0
- package/src/postgres.ts +492 -0
- package/src/providers.ts +135 -0
- package/src/secrets.ts +31 -0
- package/src/serve/binding.ts +629 -0
- package/src/serve/handler.ts +362 -0
- package/src/serve/stream-server.ts +176 -0
- package/src/session-index.ts +99 -0
- package/src/shell-env.ts +153 -0
- package/src/sqlite-workers.ts +283 -0
- package/src/sqlite.ts +553 -0
- package/src/store-rows.ts +110 -0
- package/src/store-usage.ts +347 -0
- package/src/usage-history.ts +219 -0
- package/src/usage-scan.ts +81 -0
- package/src/usage-service.ts +82 -0
- package/src/usage-summary.ts +79 -0
- package/src/usage-types.ts +87 -0
- package/src/usage-worker.ts +24 -0
- package/src/usage.ts +776 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { copyFile, mkdir, realpath, rm, stat } from "node:fs/promises";
|
|
3
|
+
import { homedir, tmpdir } from "node:os";
|
|
4
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
5
|
+
import { SdkError } from "@nyte-ai/schema";
|
|
6
|
+
import type { WorkspaceTarget } from "@nyte-ai/schema";
|
|
7
|
+
import type { WorktreeProvider } from "@nyte-ai/core";
|
|
8
|
+
import { sha256 } from "../hash.ts";
|
|
9
|
+
import { run } from "./command.ts";
|
|
10
|
+
|
|
11
|
+
const ENV: Readonly<Record<string, string>> = { GIT_TERMINAL_PROMPT: "0", LC_ALL: "C" };
|
|
12
|
+
|
|
13
|
+
/** One checkout of a repository: its folder and the branch it has out, null when HEAD is detached. */
|
|
14
|
+
export interface ListedWorktree {
|
|
15
|
+
readonly path: string;
|
|
16
|
+
readonly branch: string | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type Git = (
|
|
20
|
+
cwd: string,
|
|
21
|
+
args: readonly string[],
|
|
22
|
+
env?: Readonly<Record<string, string>>,
|
|
23
|
+
) => Promise<string>;
|
|
24
|
+
|
|
25
|
+
type Repository = { readonly root: string; readonly gitDir: string; readonly prefix: string };
|
|
26
|
+
|
|
27
|
+
function gitCommand(binary = "git"): Git {
|
|
28
|
+
return async (cwd, args, env) => {
|
|
29
|
+
const ran = await run(binary, ["-c", "core.quotepath=off", ...args], {
|
|
30
|
+
cwd,
|
|
31
|
+
env: { ...ENV, ...env },
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (ran.code !== 0)
|
|
35
|
+
throw new SdkError("unavailable", ran.stderr.trim() || `git ${args[0] ?? ""} failed.`);
|
|
36
|
+
|
|
37
|
+
return ran.stdout.toString("utf8");
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Lists every worktree of the repository holding `cwd` (`git worktree list --porcelain -z`), the main checkout first. */
|
|
42
|
+
export async function listWorktrees(
|
|
43
|
+
cwd: string,
|
|
44
|
+
options?: { readonly git?: string },
|
|
45
|
+
): Promise<readonly ListedWorktree[]> {
|
|
46
|
+
const out = await gitCommand(options?.git)(cwd, ["worktree", "list", "--porcelain", "-z"]);
|
|
47
|
+
|
|
48
|
+
return out.split("\0\0").flatMap((record) => {
|
|
49
|
+
const fields = record.split("\0");
|
|
50
|
+
const path = fields.find((field) => field.startsWith("worktree "))?.slice(9);
|
|
51
|
+
const branch = fields.find((field) => field.startsWith("branch refs/heads/"))?.slice(18);
|
|
52
|
+
|
|
53
|
+
return path === undefined ? [] : [{ path: resolve(path), branch: branch ?? null }];
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Checks `branch` out in a new worktree at `path` (`git worktree add`), creating the branch at `from` when given. */
|
|
58
|
+
export async function addWorktree(
|
|
59
|
+
cwd: string,
|
|
60
|
+
input: { readonly path: string; readonly branch: string; readonly from?: string },
|
|
61
|
+
options?: { readonly git?: string },
|
|
62
|
+
): Promise<void> {
|
|
63
|
+
await gitCommand(options?.git)(
|
|
64
|
+
cwd,
|
|
65
|
+
input.from === undefined
|
|
66
|
+
? ["worktree", "add", input.path, input.branch]
|
|
67
|
+
: ["worktree", "add", "-b", input.branch, input.path, input.from],
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Whether `name` is a local branch of the repository holding `cwd`. */
|
|
72
|
+
export async function hasBranch(
|
|
73
|
+
cwd: string,
|
|
74
|
+
name: string,
|
|
75
|
+
options?: { readonly git?: string },
|
|
76
|
+
): Promise<boolean> {
|
|
77
|
+
return gitCommand(options?.git)(cwd, ["show-ref", "--verify", "--quiet", `refs/heads/${name}`])
|
|
78
|
+
.then(() => true)
|
|
79
|
+
.catch(() => false);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Returns git worktrees for local workspaces (`git`, default `git` on the
|
|
84
|
+
* PATH), each under `<home>/.nyte/worktrees/<repository>/<slug>` on the branch
|
|
85
|
+
* `nyte/<slug>`, the slug the title's words and a hash of the name.
|
|
86
|
+
*
|
|
87
|
+
* `create` refuses a folder outside a repository or one without a commit, then
|
|
88
|
+
* answers the worktree already on that branch, or adds one from HEAD and
|
|
89
|
+
* copies the workspace's uncommitted changes in: the worktree's tracked and
|
|
90
|
+
* untracked files, ignored ones aside, are written to a tree through a
|
|
91
|
+
* temporary index seeded from the real one (which is never touched), and the
|
|
92
|
+
* new checkout moves to that tree with its index left at HEAD, so the changes
|
|
93
|
+
* show as uncommitted there too. A workspace below the repository's root gets
|
|
94
|
+
* the same folder below the worktree's.
|
|
95
|
+
*
|
|
96
|
+
* `remove` keeps a worktree, folder and branch, while it holds uncommitted
|
|
97
|
+
* changes or ignored files, since none were copied in and one may be a
|
|
98
|
+
* `.env` or a local database (a wholly ignored folder such as `node_modules`
|
|
99
|
+
* or a build's output aside); otherwise it removes the folder
|
|
100
|
+
* (`git worktree remove`) and deletes the branch unless it moved past `base`.
|
|
101
|
+
*/
|
|
102
|
+
export function localWorktrees(options?: {
|
|
103
|
+
readonly home?: string;
|
|
104
|
+
readonly git?: string;
|
|
105
|
+
}): WorktreeProvider {
|
|
106
|
+
const git = gitCommand(options?.git);
|
|
107
|
+
const folders = join(options?.home ?? homedir(), ".nyte", "worktrees");
|
|
108
|
+
|
|
109
|
+
const commitAt = (cwd: string, revision: string): Promise<string | null> =>
|
|
110
|
+
git(cwd, ["rev-parse", "--verify", "--quiet", `${revision}^{commit}`]).then(
|
|
111
|
+
(out) => out.trim(),
|
|
112
|
+
() => null,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const workingTree = async (repo: Repository): Promise<string> => {
|
|
116
|
+
const index = join(tmpdir(), `nyte-worktree-index-${randomUUID()}`);
|
|
117
|
+
const indexEnv = { GIT_INDEX_FILE: index };
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
await copyFile(join(repo.gitDir, "index"), index).catch(() => undefined);
|
|
121
|
+
await git(repo.root, ["add", "-A", "--", "."], indexEnv);
|
|
122
|
+
|
|
123
|
+
return (await git(repo.root, ["write-tree"], indexEnv)).trim();
|
|
124
|
+
} finally {
|
|
125
|
+
await rm(index, { force: true });
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
async create(env, input) {
|
|
131
|
+
const repo = await readRepository(git, env.cwd);
|
|
132
|
+
|
|
133
|
+
if (repo === null)
|
|
134
|
+
throw new SdkError(
|
|
135
|
+
"unavailable",
|
|
136
|
+
`${env.cwd} is not in a git repository, so it cannot have a worktree.`,
|
|
137
|
+
);
|
|
138
|
+
const head = await commitAt(repo.root, "HEAD");
|
|
139
|
+
|
|
140
|
+
if (head === null)
|
|
141
|
+
throw new SdkError(
|
|
142
|
+
"unavailable",
|
|
143
|
+
`${repo.root} has no commits yet, so a worktree has nothing to branch from.`,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const slug = worktreeSlug(input.name, input.title);
|
|
147
|
+
const branch = `nyte/${slug}`;
|
|
148
|
+
const below = (root: string): WorkspaceTarget => ({
|
|
149
|
+
kind: "local",
|
|
150
|
+
path: resolve(root, repo.prefix),
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const listed = (await listWorktrees(repo.root, options)).find(
|
|
154
|
+
(worktree) => worktree.branch === branch,
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
if (listed !== undefined)
|
|
158
|
+
return {
|
|
159
|
+
target: below(listed.path),
|
|
160
|
+
branch,
|
|
161
|
+
base: (await commitAt(repo.root, `refs/heads/${branch}`)) ?? head,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const folder = join(folders, basename(repo.root), slug);
|
|
165
|
+
const branched = await hasBranch(repo.root, branch, options);
|
|
166
|
+
const base = branched ? ((await commitAt(repo.root, `refs/heads/${branch}`)) ?? head) : head;
|
|
167
|
+
const tree = await workingTree(repo);
|
|
168
|
+
|
|
169
|
+
await mkdir(dirname(folder), { recursive: true });
|
|
170
|
+
await addWorktree(
|
|
171
|
+
repo.root,
|
|
172
|
+
branched ? { path: folder, branch } : { path: folder, branch, from: head },
|
|
173
|
+
options,
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
if (tree !== (await git(folder, ["rev-parse", "HEAD^{tree}"])).trim()) {
|
|
177
|
+
await git(folder, ["read-tree", "-m", "-u", "HEAD", tree]);
|
|
178
|
+
await git(folder, ["reset", "-q"]);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return { target: below(await realpath(folder)), branch, base };
|
|
182
|
+
},
|
|
183
|
+
async remove(worktree) {
|
|
184
|
+
const path = "path" in worktree.target ? worktree.target.path : null;
|
|
185
|
+
|
|
186
|
+
if (path === null || (await stat(path).catch(() => null)) === null)
|
|
187
|
+
return { folder: "removed", branch: "kept" };
|
|
188
|
+
const [root = path, common = ""] = (
|
|
189
|
+
await git(path, [
|
|
190
|
+
"rev-parse",
|
|
191
|
+
"--show-toplevel",
|
|
192
|
+
"--path-format=absolute",
|
|
193
|
+
"--git-common-dir",
|
|
194
|
+
])
|
|
195
|
+
).split("\n");
|
|
196
|
+
|
|
197
|
+
const status = await git(root, ["status", "--porcelain", "--ignored", "-z"]);
|
|
198
|
+
|
|
199
|
+
if (
|
|
200
|
+
status
|
|
201
|
+
.split("\0")
|
|
202
|
+
.some((entry) => entry !== "" && !(entry.startsWith("!! ") && entry.endsWith("/")))
|
|
203
|
+
)
|
|
204
|
+
return { folder: "kept", branch: "kept" };
|
|
205
|
+
await git(common, ["worktree", "remove", root]);
|
|
206
|
+
|
|
207
|
+
if ((await commitAt(common, `refs/heads/${worktree.branch}`)) !== worktree.base)
|
|
208
|
+
return { folder: "removed", branch: "kept" };
|
|
209
|
+
await git(common, ["branch", "-D", worktree.branch]);
|
|
210
|
+
|
|
211
|
+
return { folder: "removed", branch: "deleted" };
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function readRepository(git: Git, cwd: string): Promise<Repository | null> {
|
|
217
|
+
const out = await git(cwd, [
|
|
218
|
+
"rev-parse",
|
|
219
|
+
"--show-toplevel",
|
|
220
|
+
"--absolute-git-dir",
|
|
221
|
+
"--show-prefix",
|
|
222
|
+
]).catch(() => null);
|
|
223
|
+
const [root, gitDir, prefix = ""] = out?.split("\n") ?? [];
|
|
224
|
+
|
|
225
|
+
return root === undefined || gitDir === undefined || root === ""
|
|
226
|
+
? null
|
|
227
|
+
: { root: resolve(root), gitDir: resolve(gitDir), prefix };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function worktreeSlug(name: string, title: string | null): string {
|
|
231
|
+
const words = (title ?? "")
|
|
232
|
+
.toLowerCase()
|
|
233
|
+
.replace(/[^a-z0-9]+/gu, "-")
|
|
234
|
+
.slice(0, 40)
|
|
235
|
+
.replace(/^-+|-+$/gu, "");
|
|
236
|
+
const key = sha256(name).slice(0, 8);
|
|
237
|
+
|
|
238
|
+
return words === "" ? key : `${words}-${key}`;
|
|
239
|
+
}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, realpath, rename, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import { isJsonObject, McpExposureNameType, McpOAuthConfigType } from "@nyte-ai/schema";
|
|
5
|
+
import { isJson } from "@nyte-ai/schema";
|
|
6
|
+
import type {
|
|
7
|
+
Json,
|
|
8
|
+
McpChange,
|
|
9
|
+
McpExposure,
|
|
10
|
+
McpOAuthConfig,
|
|
11
|
+
McpScope,
|
|
12
|
+
ValidationIssue,
|
|
13
|
+
} from "@nyte-ai/schema";
|
|
14
|
+
import { Type } from "typebox";
|
|
15
|
+
import type { Static } from "typebox";
|
|
16
|
+
import { Value } from "typebox/value";
|
|
17
|
+
|
|
18
|
+
const ExposureName = McpExposureNameType;
|
|
19
|
+
|
|
20
|
+
const Entry = Type.Object({
|
|
21
|
+
type: Type.Optional(Type.Enum(["stdio", "http", "streamable-http", "sse"])),
|
|
22
|
+
command: Type.Optional(Type.String({ minLength: 1 })),
|
|
23
|
+
args: Type.Optional(Type.Array(Type.String())),
|
|
24
|
+
env: Type.Optional(Type.Record(Type.String(), Type.String())),
|
|
25
|
+
cwd: Type.Optional(Type.String()),
|
|
26
|
+
url: Type.Optional(Type.String({ minLength: 1 })),
|
|
27
|
+
headers: Type.Optional(Type.Record(Type.String(), Type.String())),
|
|
28
|
+
oauth: Type.Optional(McpOAuthConfigType),
|
|
29
|
+
enabled: Type.Optional(Type.Boolean()),
|
|
30
|
+
timeout: Type.Optional(Type.Number({ exclusiveMinimum: 0 })),
|
|
31
|
+
description: Type.Optional(Type.String()),
|
|
32
|
+
exposure: Type.Optional(ExposureName),
|
|
33
|
+
toolExposure: Type.Optional(Type.Record(Type.String(), ExposureName)),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const ConfigFile = Type.Object({
|
|
37
|
+
mcpServers: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const OVERRIDE_KEYS = new Set(["enabled", "exposure", "toolExposure"]);
|
|
41
|
+
|
|
42
|
+
const EXPOSURE: Readonly<Record<Static<typeof ExposureName>, McpExposure>> = {
|
|
43
|
+
direct: "model",
|
|
44
|
+
deferred: "deferred",
|
|
45
|
+
codemode: "code",
|
|
46
|
+
"codemode-deferred": "code",
|
|
47
|
+
hidden: "hidden",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
type McpTransportConfig =
|
|
51
|
+
| {
|
|
52
|
+
readonly kind: "stdio";
|
|
53
|
+
readonly command: string;
|
|
54
|
+
readonly args: readonly string[];
|
|
55
|
+
readonly env: Readonly<Record<string, string>>;
|
|
56
|
+
readonly cwd: string | null;
|
|
57
|
+
}
|
|
58
|
+
| {
|
|
59
|
+
readonly kind: "http";
|
|
60
|
+
readonly url: string;
|
|
61
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
62
|
+
readonly oauth: McpOAuthConfig;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export type McpServerEntry = {
|
|
66
|
+
readonly name: string;
|
|
67
|
+
readonly scope: McpScope;
|
|
68
|
+
readonly path: string;
|
|
69
|
+
readonly override: string | null;
|
|
70
|
+
readonly transport: McpTransportConfig;
|
|
71
|
+
readonly enabled: boolean;
|
|
72
|
+
readonly timeoutMs: number;
|
|
73
|
+
readonly description: string | null;
|
|
74
|
+
readonly exposure: McpExposure;
|
|
75
|
+
/** Patterns in file order; `*` matches any characters. */
|
|
76
|
+
readonly toolExposure: readonly (readonly [pattern: string, exposure: McpExposure])[];
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export type McpFileContents =
|
|
80
|
+
| { readonly kind: "missing" }
|
|
81
|
+
| { readonly kind: "text"; readonly text: string }
|
|
82
|
+
| { readonly kind: "unreadable"; readonly message: string };
|
|
83
|
+
|
|
84
|
+
export type McpFile = {
|
|
85
|
+
readonly path: string;
|
|
86
|
+
readonly scope: McpScope;
|
|
87
|
+
readonly contents: McpFileContents;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export type McpConfig = {
|
|
91
|
+
readonly servers: ReadonlyMap<string, McpServerEntry>;
|
|
92
|
+
readonly issues: readonly ValidationIssue[];
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/** Reads one `mcp.json`; a file that cannot be read is `unreadable`, not missing. */
|
|
96
|
+
export async function readMcpFile(path: string): Promise<McpFileContents> {
|
|
97
|
+
try {
|
|
98
|
+
return { kind: "text", text: await readFile(path, "utf8") };
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
101
|
+
return { kind: "missing" };
|
|
102
|
+
|
|
103
|
+
return { kind: "unreadable", message: error instanceof Error ? error.message : String(error) };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Merges the user file and then the project file into one server list. A
|
|
109
|
+
* project entry replaces the user entry of the same name; one with only
|
|
110
|
+
* `enabled`, `exposure` or `toolExposure` overrides those fields instead.
|
|
111
|
+
* An invalid entry becomes an issue and is left out.
|
|
112
|
+
*/
|
|
113
|
+
export function parseMcpConfig(files: readonly McpFile[]): McpConfig {
|
|
114
|
+
const servers = new Map<string, McpServerEntry>();
|
|
115
|
+
const canonical = new Map<string, string>();
|
|
116
|
+
const issues: ValidationIssue[] = [];
|
|
117
|
+
|
|
118
|
+
for (const file of files) {
|
|
119
|
+
if (file.contents.kind === "missing") continue;
|
|
120
|
+
|
|
121
|
+
const fail = (path: readonly string[], message: string): void => {
|
|
122
|
+
issues.push({
|
|
123
|
+
severity: "error",
|
|
124
|
+
path: ["mcpServers", ...path],
|
|
125
|
+
message,
|
|
126
|
+
source: { plugin: "mcp", file: file.path },
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
if (file.contents.kind === "unreadable") {
|
|
131
|
+
fail([], `The file cannot be read: ${file.contents.message}`);
|
|
132
|
+
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let parsed: unknown;
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
parsed = JSON.parse(file.contents.text);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
fail([], `The file is not valid JSON: ${error instanceof Error ? error.message : ""}`);
|
|
142
|
+
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!Value.Check(ConfigFile, parsed)) {
|
|
147
|
+
fail([], 'The file must be an object with an "mcpServers" object.');
|
|
148
|
+
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for (const [name, raw] of Object.entries(parsed.mcpServers ?? {})) {
|
|
153
|
+
if (!/^[A-Za-z0-9_-]+$/.test(name)) {
|
|
154
|
+
fail([name], `Server names may contain only letters, digits, "_" and "-": ${name}.`);
|
|
155
|
+
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const key = name.replaceAll("-", "_");
|
|
160
|
+
const taken = canonical.get(key);
|
|
161
|
+
|
|
162
|
+
if (taken !== undefined && taken !== name) {
|
|
163
|
+
fail([name], `${name} and ${taken} name the same server.`);
|
|
164
|
+
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!Value.Check(Entry, raw)) {
|
|
169
|
+
const [first] = Value.Errors(Entry, raw);
|
|
170
|
+
fail([name], `${name}${first?.instancePath ?? ""} ${first?.message ?? "is invalid"}.`);
|
|
171
|
+
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const base = servers.get(name);
|
|
176
|
+
|
|
177
|
+
if (raw.command === undefined && raw.url === undefined && raw.type === undefined) {
|
|
178
|
+
const extra = Object.keys(raw).filter((field) => !OVERRIDE_KEYS.has(field));
|
|
179
|
+
|
|
180
|
+
if (file.scope !== "project" || base === undefined)
|
|
181
|
+
fail([name], `${name} needs "command" or "url".`);
|
|
182
|
+
else if (extra.length > 0)
|
|
183
|
+
fail([name], `An override of ${name} can only set enabled, exposure and toolExposure.`);
|
|
184
|
+
else servers.set(name, { ...base, ...settings(raw, base), override: file.path });
|
|
185
|
+
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const transport = readTransport(raw);
|
|
190
|
+
|
|
191
|
+
if (typeof transport === "string") {
|
|
192
|
+
fail([name], `${name}: ${transport}`);
|
|
193
|
+
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
canonical.set(key, name);
|
|
198
|
+
servers.set(name, {
|
|
199
|
+
name,
|
|
200
|
+
scope: file.scope,
|
|
201
|
+
path: file.path,
|
|
202
|
+
override: null,
|
|
203
|
+
transport,
|
|
204
|
+
timeoutMs: (raw.timeout ?? 60) * 1000,
|
|
205
|
+
description: raw.description ?? null,
|
|
206
|
+
...settings(raw, null),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return { servers, issues };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function readTransport(raw: Static<typeof Entry>): McpTransportConfig | string {
|
|
215
|
+
if (raw.type === "sse")
|
|
216
|
+
return 'the SSE transport is not supported; use the server\'s streamable HTTP url, often ending in "/mcp".';
|
|
217
|
+
|
|
218
|
+
if (raw.command !== undefined && raw.url !== undefined)
|
|
219
|
+
return 'set "command" or "url", not both.';
|
|
220
|
+
|
|
221
|
+
if (raw.command !== undefined && raw.type !== undefined && raw.type !== "stdio")
|
|
222
|
+
return `type ${raw.type} needs "url", not "command".`;
|
|
223
|
+
|
|
224
|
+
if (raw.command !== undefined)
|
|
225
|
+
return {
|
|
226
|
+
kind: "stdio",
|
|
227
|
+
command: raw.command,
|
|
228
|
+
args: raw.args ?? [],
|
|
229
|
+
env: raw.env ?? {},
|
|
230
|
+
cwd: raw.cwd ?? null,
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
if (raw.url === undefined) return `type ${raw.type ?? "stdio"} needs "command" or "url".`;
|
|
234
|
+
|
|
235
|
+
if (raw.type === "stdio") return 'type stdio needs "command", not "url".';
|
|
236
|
+
|
|
237
|
+
const url = URL.parse(raw.url);
|
|
238
|
+
|
|
239
|
+
if (url === null || (url.protocol !== "https:" && url.protocol !== "http:"))
|
|
240
|
+
return `${raw.url} is not an http or https URL.`;
|
|
241
|
+
|
|
242
|
+
if (raw.command === undefined && raw.oauth?.callbackUrl !== undefined) {
|
|
243
|
+
const callback = URL.parse(raw.oauth.callbackUrl);
|
|
244
|
+
|
|
245
|
+
if (
|
|
246
|
+
callback === null ||
|
|
247
|
+
callback.protocol !== "http:" ||
|
|
248
|
+
!["localhost", "127.0.0.1", "[::1]"].includes(callback.hostname)
|
|
249
|
+
)
|
|
250
|
+
return "oauth.callbackUrl must be an http URL on localhost, 127.0.0.1 or [::1].";
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (raw.oauth?.authServerMetadataUrl !== undefined) {
|
|
254
|
+
const metadata = URL.parse(raw.oauth.authServerMetadataUrl);
|
|
255
|
+
|
|
256
|
+
if (
|
|
257
|
+
metadata === null ||
|
|
258
|
+
(metadata.protocol !== "https:" &&
|
|
259
|
+
!["localhost", "127.0.0.1", "[::1]"].includes(metadata.hostname))
|
|
260
|
+
)
|
|
261
|
+
return "oauth.authServerMetadataUrl must use https, except on localhost.";
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return { kind: "http", url: raw.url, headers: raw.headers ?? {}, oauth: raw.oauth ?? {} };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function settings(
|
|
268
|
+
raw: Static<typeof Entry>,
|
|
269
|
+
base: McpServerEntry | null,
|
|
270
|
+
): Pick<McpServerEntry, "enabled" | "exposure" | "toolExposure"> {
|
|
271
|
+
return {
|
|
272
|
+
enabled: raw.enabled ?? base?.enabled ?? true,
|
|
273
|
+
exposure: raw.exposure === undefined ? (base?.exposure ?? "code") : EXPOSURE[raw.exposure],
|
|
274
|
+
toolExposure:
|
|
275
|
+
raw.toolExposure === undefined
|
|
276
|
+
? (base?.toolExposure ?? [])
|
|
277
|
+
: Object.entries(raw.toolExposure).map(([pattern, name]) => [pattern, EXPOSURE[name]]),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Plans a change to the `mcpServers` of one `mcp.json`, keeping the rest of
|
|
283
|
+
* the file: `change` returns the new servers, or `null` when it has nothing
|
|
284
|
+
* to change. A missing file starts empty; one that is not a JSON object is
|
|
285
|
+
* refused. Nothing is written; pass the text to {@link writeMcpFile}.
|
|
286
|
+
*/
|
|
287
|
+
export async function planMcpEdit(
|
|
288
|
+
path: string,
|
|
289
|
+
change: (servers: { readonly [name: string]: Json }) => { readonly [name: string]: Json } | null,
|
|
290
|
+
): Promise<
|
|
291
|
+
{ readonly kind: "planned"; readonly text: string } | Exclude<McpChange, { kind: "updated" }>
|
|
292
|
+
> {
|
|
293
|
+
const contents = await readMcpFile(path);
|
|
294
|
+
|
|
295
|
+
if (contents.kind === "unreadable")
|
|
296
|
+
return { kind: "invalid", message: `${path} cannot be read: ${contents.message}` };
|
|
297
|
+
let parsed: unknown = {};
|
|
298
|
+
|
|
299
|
+
if (contents.kind === "text") {
|
|
300
|
+
try {
|
|
301
|
+
parsed = JSON.parse(contents.text);
|
|
302
|
+
} catch {
|
|
303
|
+
return { kind: "invalid", message: `${path} is not valid JSON; fix it first.` };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (!isJson(parsed) || !isJsonObject(parsed))
|
|
308
|
+
return { kind: "invalid", message: `${path} is not a JSON object.` };
|
|
309
|
+
const servers = parsed.mcpServers ?? {};
|
|
310
|
+
|
|
311
|
+
if (!isJsonObject(servers))
|
|
312
|
+
return { kind: "invalid", message: `${path} has an "mcpServers" that is not an object.` };
|
|
313
|
+
const next = change(servers);
|
|
314
|
+
|
|
315
|
+
if (next === null) return { kind: "missing" };
|
|
316
|
+
|
|
317
|
+
return { kind: "planned", text: `${JSON.stringify({ ...parsed, mcpServers: next }, null, 2)}\n` };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Replaces one `mcp.json` whole, readable only by the user as it may hold
|
|
322
|
+
* secrets. A symlinked file is written through the link.
|
|
323
|
+
*/
|
|
324
|
+
export async function writeMcpFile(path: string, text: string): Promise<void> {
|
|
325
|
+
const target = await realpath(path).catch(() => path);
|
|
326
|
+
const temporary = `${target}.${randomUUID()}.tmp`;
|
|
327
|
+
await mkdir(dirname(target), { recursive: true });
|
|
328
|
+
await writeFile(temporary, text, { mode: 0o600 });
|
|
329
|
+
await rename(temporary, target);
|
|
330
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { StdioTransport, StreamableHttpTransport } from "@earendil-works/pi-mcp";
|
|
4
|
+
import type { McpTransport } from "@earendil-works/pi-mcp";
|
|
5
|
+
import type { OAuthChallenge } from "@earendil-works/pi-mcp/oauth";
|
|
6
|
+
import type { PluginStorage } from "@nyte-ai/core";
|
|
7
|
+
import type { McpWorkspace } from "@nyte-ai/core/server";
|
|
8
|
+
import type { McpOAuthConfig } from "@nyte-ai/schema";
|
|
9
|
+
import type { McpServerEntry } from "./config.ts";
|
|
10
|
+
import { createAuthProvider, oauthKey, oauthStore } from "./oauth.ts";
|
|
11
|
+
import type { McpOAuthSettings } from "./oauth.ts";
|
|
12
|
+
|
|
13
|
+
export type McpConnector = (
|
|
14
|
+
entry: McpServerEntry,
|
|
15
|
+
context: {
|
|
16
|
+
readonly workspace: McpWorkspace;
|
|
17
|
+
readonly home: string;
|
|
18
|
+
readonly secrets: PluginStorage["secrets"];
|
|
19
|
+
readonly challenged: (challenge: OAuthChallenge) => void;
|
|
20
|
+
},
|
|
21
|
+
) => McpTransport;
|
|
22
|
+
|
|
23
|
+
export const connectServer: McpConnector = (entry, context) => {
|
|
24
|
+
const { transport } = entry;
|
|
25
|
+
|
|
26
|
+
if (transport.kind === "stdio") {
|
|
27
|
+
const base = context.workspace.root ?? context.home;
|
|
28
|
+
const cwd = transport.cwd === null ? base : home(transport.cwd, context.home);
|
|
29
|
+
|
|
30
|
+
return new StdioTransport({
|
|
31
|
+
command: home(transport.command, context.home),
|
|
32
|
+
args: transport.args.map((arg) => home(arg, context.home)),
|
|
33
|
+
env: expand(transport.env, entry.name),
|
|
34
|
+
cwd: isAbsolute(cwd) ? cwd : resolve(base, cwd),
|
|
35
|
+
stderr: "pipe",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const headers = expand(transport.headers, entry.name);
|
|
40
|
+
|
|
41
|
+
if (Object.keys(headers).some((name) => name.toLowerCase() === "authorization"))
|
|
42
|
+
return new StreamableHttpTransport({ url: transport.url, headers });
|
|
43
|
+
|
|
44
|
+
return new StreamableHttpTransport({
|
|
45
|
+
url: transport.url,
|
|
46
|
+
headers,
|
|
47
|
+
authProvider: createAuthProvider({
|
|
48
|
+
serverUrl: transport.url,
|
|
49
|
+
store: oauthStore(context.secrets, oauthKey(entry.name, transport.url)),
|
|
50
|
+
settings: () => oauthSettings(transport.oauth, entry.name),
|
|
51
|
+
onChallenge: context.challenged,
|
|
52
|
+
}),
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export function oauthSettings(config: McpOAuthConfig, server: string): McpOAuthSettings {
|
|
57
|
+
const { clientSecret, authServerMetadataUrl, ...rest } = config;
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
...rest,
|
|
61
|
+
...(clientSecret === undefined
|
|
62
|
+
? {}
|
|
63
|
+
: { clientSecret: expand({ clientSecret }, server).clientSecret }),
|
|
64
|
+
...(authServerMetadataUrl === undefined
|
|
65
|
+
? {}
|
|
66
|
+
: { authServerMetadataUrl: new URL(authServerMetadataUrl) }),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function home(path: string, directory: string): string {
|
|
71
|
+
if (path === "~") return directory;
|
|
72
|
+
|
|
73
|
+
return path.startsWith("~/") ? join(directory, path.slice(2)) : path;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function expand(values: Readonly<Record<string, string>>, server: string): Record<string, string> {
|
|
77
|
+
return Object.fromEntries(
|
|
78
|
+
Object.entries(values).map(([key, value]) => [
|
|
79
|
+
key,
|
|
80
|
+
value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_match, name: string) => {
|
|
81
|
+
const found = process.env[name];
|
|
82
|
+
|
|
83
|
+
if (found === undefined)
|
|
84
|
+
throw new Error(
|
|
85
|
+
`${server} needs the environment variable ${name}. Set it where Nyte runs, then restart Nyte.`,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
return found;
|
|
89
|
+
}),
|
|
90
|
+
]),
|
|
91
|
+
);
|
|
92
|
+
}
|