@nema-project/nema 0.0.0-stage → 0.1.4
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/dist/chunks/chunk-HEIQINX3.js +825 -0
- package/dist/chunks/chunk-WT3XUFWU.js +86 -0
- package/dist/claude/adapter.d.ts +19 -0
- package/dist/claude/adapter.js +258 -0
- package/dist/claude/events.d.ts +29 -0
- package/dist/claude/events.js +75 -0
- package/dist/claude/launch.d.ts +22 -0
- package/dist/claude/launch.js +80 -0
- package/dist/claude/token.d.ts +9 -0
- package/dist/claude/token.js +87 -0
- package/dist/codex/adapter.d.ts +17 -0
- package/dist/codex/adapter.js +151 -0
- package/dist/codex/events.d.ts +16 -0
- package/dist/codex/events.js +47 -0
- package/dist/codex/isolation.d.ts +33 -0
- package/dist/codex/isolation.js +115 -0
- package/dist/config.d.ts +29 -0
- package/dist/config.js +75 -0
- package/dist/configCommands.d.ts +13 -0
- package/dist/configCommands.js +69 -0
- package/dist/daemon/cleanup.d.ts +5 -0
- package/dist/daemon/cleanup.js +49 -0
- package/dist/daemon/connection.d.ts +22 -0
- package/dist/daemon/connection.js +166 -0
- package/dist/daemon/rpc.d.ts +14 -0
- package/dist/daemon/rpc.js +20 -0
- package/dist/daemon/runner.d.ts +34 -0
- package/dist/daemon/runner.js +94 -0
- package/dist/daemon/setup.d.ts +39 -0
- package/dist/daemon/setup.js +95 -0
- package/dist/direct/adapter.d.ts +30 -0
- package/dist/direct/adapter.js +190 -0
- package/dist/direct/authCommands.d.ts +54 -0
- package/dist/direct/authCommands.js +268 -0
- package/dist/direct/authStore.d.ts +120 -0
- package/dist/direct/authStore.js +351 -0
- package/dist/direct/byok.d.ts +27 -0
- package/dist/direct/byok.js +67 -0
- package/dist/direct/codexLogin.d.ts +34 -0
- package/dist/direct/codexLogin.js +93 -0
- package/dist/direct/jwt.d.ts +4 -0
- package/dist/direct/jwt.js +22 -0
- package/dist/direct/lock.d.ts +16 -0
- package/dist/direct/lock.js +134 -0
- package/dist/direct/loop.d.ts +33 -0
- package/dist/direct/loop.js +110 -0
- package/dist/direct/siwc.d.ts +63 -0
- package/dist/direct/siwc.js +403 -0
- package/dist/direct/tools.d.ts +17 -0
- package/dist/direct/tools.js +104 -0
- package/dist/direct/transport.d.ts +177 -0
- package/dist/direct/transport.js +437 -0
- package/dist/doctor.d.ts +28 -0
- package/dist/doctor.js +178 -0
- package/dist/extensions/handlers.d.ts +2 -0
- package/dist/extensions/handlers.js +10 -0
- package/dist/extensions/store.d.ts +11 -0
- package/dist/extensions/store.js +132 -0
- package/dist/github/client.d.ts +47 -0
- package/dist/github/client.js +383 -0
- package/dist/github/gh.d.ts +56 -0
- package/dist/github/gh.js +385 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +144 -0
- package/dist/localSettings.d.ts +8 -0
- package/dist/localSettings.js +26 -0
- package/dist/mcp/server.d.ts +5 -0
- package/dist/mcp/server.js +83 -0
- package/dist/ownedProcess.d.ts +39 -0
- package/dist/ownedProcess.js +229 -0
- package/dist/platform.d.ts +10 -0
- package/dist/platform.js +60 -0
- package/dist/report/commands.d.ts +41 -0
- package/dist/report/commands.js +159 -0
- package/dist/report/desktop.d.ts +3 -0
- package/dist/report/desktop.js +123 -0
- package/dist/report/git.d.ts +5 -0
- package/dist/report/git.js +42 -0
- package/dist/report/install.d.ts +22 -0
- package/dist/report/install.js +183 -0
- package/dist/report/send.d.ts +35 -0
- package/dist/report/send.js +208 -0
- package/dist/report/token.d.ts +5 -0
- package/dist/report/token.js +86 -0
- package/dist/repos/git.d.ts +48 -0
- package/dist/repos/git.js +370 -0
- package/dist/repos/graphify.d.ts +32 -0
- package/dist/repos/graphify.js +292 -0
- package/dist/repos/handlers.d.ts +20 -0
- package/dist/repos/handlers.js +255 -0
- package/dist/repos/indexState.d.ts +10 -0
- package/dist/repos/indexState.js +45 -0
- package/dist/repos/pickDirectory.d.ts +11 -0
- package/dist/repos/pickDirectory.js +140 -0
- package/dist/repos/reportCheck.d.ts +13 -0
- package/dist/repos/reportCheck.js +268 -0
- package/dist/repos/watcher.d.ts +64 -0
- package/dist/repos/watcher.js +202 -0
- package/dist/room/client.d.ts +26 -0
- package/dist/room/client.js +76 -0
- package/dist/secrets/file.d.ts +12 -0
- package/dist/secrets/file.js +89 -0
- package/dist/secrets/github.d.ts +10 -0
- package/dist/secrets/github.js +29 -0
- package/dist/secrets/permissions.d.ts +7 -0
- package/dist/secrets/permissions.js +86 -0
- package/dist/systemGit.d.ts +14 -0
- package/dist/systemGit.js +75 -0
- package/dist/wp/commands.d.ts +15 -0
- package/dist/wp/commands.js +103 -0
- package/package.json +26 -4
- package/src/report/SKILL.md +21 -0
- package/README.md +0 -3
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
// direct/siwc.js
|
|
2
|
+
import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
3
|
+
import { closeSync, constants, mkdirSync, openSync, readFileSync, writeSync } from "node:fs";
|
|
4
|
+
import { createServer } from "node:http";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { AuthStore, RefreshError, ReloginRequiredError } from "./authStore.js";
|
|
7
|
+
import { decodeJwtPayload } from "./jwt.js";
|
|
8
|
+
import { withFileLock } from "./lock.js";
|
|
9
|
+
import { protectPrivatePath } from "../secrets/permissions.js";
|
|
10
|
+
var SIWC = {
|
|
11
|
+
/** [SIGN-IN] first-time registration entrypoint; never saved or used for token exchange. */
|
|
12
|
+
dynamicClientId: "dynamic_agent_client",
|
|
13
|
+
/** [SIGN-IN] app's actual name, used consistently, only on initial registration. */
|
|
14
|
+
agentNameHint: "Nema Room",
|
|
15
|
+
/** [SIGN-IN] */
|
|
16
|
+
authorizeUrl: "https://auth.openai.com/api/accounts/authorize",
|
|
17
|
+
/** [SIGN-IN] code exchange; [SESSIONS] refresh. */
|
|
18
|
+
tokenUrl: "https://auth.openai.com/api/accounts/oauth/token",
|
|
19
|
+
/** [TOKENS] access-token `iss` in production. */
|
|
20
|
+
issuer: "https://auth.openai.com",
|
|
21
|
+
/** [SIGN-IN] `resource` on authorize, exchange and refresh. */
|
|
22
|
+
resource: "https://api.openai.com/v1",
|
|
23
|
+
/** [SIGN-IN] identity scopes (needed for the id_token / account identity). */
|
|
24
|
+
identityScopes: "openid profile email",
|
|
25
|
+
/** [SIGN-IN] ChatGPT plan usage scopes. */
|
|
26
|
+
planScopes: "offline_access resource.invoke chatgpt.tokens.use.direct",
|
|
27
|
+
/** [SIGN-IN] the granted scope that authorizes plan usage. */
|
|
28
|
+
planUsageScope: "chatgpt.tokens.use.direct",
|
|
29
|
+
/** [SIGN-IN] loopback redirect `http://127.0.0.1:<port>/auth/callback`; only the port may vary. */
|
|
30
|
+
callbackHost: "127.0.0.1",
|
|
31
|
+
callbackPath: "/auth/callback",
|
|
32
|
+
/** [SIGN-IN] example port. */
|
|
33
|
+
defaultPort: 1455,
|
|
34
|
+
/** [MODELS] */
|
|
35
|
+
apiBase: "https://api.openai.com/v1",
|
|
36
|
+
/** Project rule (not from the docs). */
|
|
37
|
+
originator: "nema-room"
|
|
38
|
+
};
|
|
39
|
+
var UNUSABLE_REFRESH_CODES = [
|
|
40
|
+
"invalid_grant",
|
|
41
|
+
"invalid_client",
|
|
42
|
+
// the saved client registration is gone: log in again with a NEW client
|
|
43
|
+
"invalid_refresh_token",
|
|
44
|
+
"token_expired",
|
|
45
|
+
"refresh_token_expired",
|
|
46
|
+
"refresh_token_invalidated",
|
|
47
|
+
"refresh_token_reused"
|
|
48
|
+
];
|
|
49
|
+
var EXCHANGE_TIMEOUT_MS = 3e4;
|
|
50
|
+
var cachedVersion;
|
|
51
|
+
function cliVersion() {
|
|
52
|
+
if (cachedVersion)
|
|
53
|
+
return cachedVersion;
|
|
54
|
+
try {
|
|
55
|
+
const pkg = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
|
|
56
|
+
cachedVersion = typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
57
|
+
} catch {
|
|
58
|
+
cachedVersion = "0.0.0";
|
|
59
|
+
}
|
|
60
|
+
return cachedVersion;
|
|
61
|
+
}
|
|
62
|
+
var nemaUserAgent = () => `NemaRoom/${cliVersion()}`;
|
|
63
|
+
var nemaHeaders = () => ({ originator: SIWC.originator, "User-Agent": nemaUserAgent() });
|
|
64
|
+
function hostId(nemaHome) {
|
|
65
|
+
const file = path.join(nemaHome, "host-id");
|
|
66
|
+
const readExisting = () => {
|
|
67
|
+
try {
|
|
68
|
+
const v = readFileSync(file, "utf8").trim();
|
|
69
|
+
return v === "" ? null : v;
|
|
70
|
+
} catch (e) {
|
|
71
|
+
if (e.code === "ENOENT")
|
|
72
|
+
return null;
|
|
73
|
+
throw e;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const existing = readExisting();
|
|
77
|
+
if (existing)
|
|
78
|
+
return existing;
|
|
79
|
+
mkdirSync(nemaHome, { recursive: true, mode: 448 });
|
|
80
|
+
const id = `urn:uuid:${randomUUID()}`;
|
|
81
|
+
try {
|
|
82
|
+
const fd = openSync(file, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 384);
|
|
83
|
+
try {
|
|
84
|
+
writeSync(fd, id + "\n");
|
|
85
|
+
} finally {
|
|
86
|
+
closeSync(fd);
|
|
87
|
+
}
|
|
88
|
+
protectPrivatePath(file, 384);
|
|
89
|
+
return id;
|
|
90
|
+
} catch (e) {
|
|
91
|
+
if (e.code !== "EEXIST")
|
|
92
|
+
throw e;
|
|
93
|
+
const raced = readExisting();
|
|
94
|
+
if (!raced)
|
|
95
|
+
throw e;
|
|
96
|
+
return raced;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
var b64url = (buf) => buf.toString("base64url");
|
|
100
|
+
var randomToken = () => b64url(randomBytes(32));
|
|
101
|
+
function safeEqual(a, b) {
|
|
102
|
+
const ab = Buffer.from(a, "utf8");
|
|
103
|
+
const bb = Buffer.from(b, "utf8");
|
|
104
|
+
return ab.length === bb.length && timingSafeEqual(ab, bb);
|
|
105
|
+
}
|
|
106
|
+
function listen(server, port) {
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
108
|
+
const onError = (e) => {
|
|
109
|
+
server.off("listening", onListening);
|
|
110
|
+
reject(e);
|
|
111
|
+
};
|
|
112
|
+
const onListening = () => {
|
|
113
|
+
server.off("error", onError);
|
|
114
|
+
resolve(server.address().port);
|
|
115
|
+
};
|
|
116
|
+
server.once("error", onError);
|
|
117
|
+
server.once("listening", onListening);
|
|
118
|
+
server.listen(port, SIWC.callbackHost);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
async function startCallbackServer(server, port) {
|
|
122
|
+
const wanted = port ?? SIWC.defaultPort;
|
|
123
|
+
try {
|
|
124
|
+
return await listen(server, wanted);
|
|
125
|
+
} catch (e) {
|
|
126
|
+
const code = e.code;
|
|
127
|
+
if (code === "EADDRINUSE" && port === void 0)
|
|
128
|
+
return listen(server, 0);
|
|
129
|
+
if (code === "EADDRINUSE")
|
|
130
|
+
throw new Error(`\u767B\u5F55\u56DE\u8C03\u7AEF\u53E3 ${SIWC.callbackHost}:${wanted} \u5DF2\u88AB\u5360\u7528\uFF0C\u8BF7\u6362\u4E00\u4E2A\u7AEF\u53E3\uFF08--port\uFF09`, { cause: e });
|
|
131
|
+
throw e;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function page(res, status, message) {
|
|
135
|
+
if (res.writableEnded)
|
|
136
|
+
return;
|
|
137
|
+
res.writeHead(status, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
138
|
+
const esc = message.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
139
|
+
res.end(`<!doctype html><meta charset="utf-8"><title>Nema Room</title><p>${esc}</p>`);
|
|
140
|
+
}
|
|
141
|
+
function waitForCallback(server, expected, timeoutMs, signal, redactErrors = false) {
|
|
142
|
+
return new Promise((resolve, reject) => {
|
|
143
|
+
let done = false;
|
|
144
|
+
const finish = (fn) => {
|
|
145
|
+
if (done)
|
|
146
|
+
return;
|
|
147
|
+
done = true;
|
|
148
|
+
clearTimeout(timer);
|
|
149
|
+
signal?.removeEventListener("abort", onAbort);
|
|
150
|
+
fn();
|
|
151
|
+
};
|
|
152
|
+
const onAbort = () => finish(() => reject(signal?.reason ?? new DOMException("Aborted", "AbortError")));
|
|
153
|
+
const timer = setTimeout(() => finish(() => reject(new Error(`\u767B\u5F55\u8D85\u65F6\uFF1A${Math.round(timeoutMs / 1e3)} \u79D2\u5185\u672A\u6536\u5230\u6D4F\u89C8\u5668\u56DE\u8C03`))), timeoutMs);
|
|
154
|
+
if (signal?.aborted)
|
|
155
|
+
return onAbort();
|
|
156
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
157
|
+
server.on("request", (req, res) => {
|
|
158
|
+
const url = new URL(req.url ?? "/", `http://${SIWC.callbackHost}`);
|
|
159
|
+
if (req.method !== "GET" || url.pathname !== SIWC.callbackPath) {
|
|
160
|
+
res.writeHead(404).end();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (done) {
|
|
164
|
+
page(res, 409, "\u6B64\u767B\u5F55\u8BF7\u6C42\u5DF2\u5904\u7406\uFF0C\u53EF\u4EE5\u5173\u95ED\u6B64\u9875\u9762\u3002");
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const q = url.searchParams;
|
|
168
|
+
const fail = (status, msg) => {
|
|
169
|
+
page(res, status, redactErrors ? "Login failed. Return to Nema Room settings." : msg);
|
|
170
|
+
finish(() => reject(new Error(msg)));
|
|
171
|
+
};
|
|
172
|
+
if (!safeEqual(q.get("state") ?? "", expected.state))
|
|
173
|
+
return fail(400, "\u767B\u5F55\u5931\u8D25\uFF1Astate \u4E0D\u5339\u914D\uFF0C\u5DF2\u62D2\u7EDD\u6B64\u56DE\u8C03");
|
|
174
|
+
const error = q.get("error");
|
|
175
|
+
if (error)
|
|
176
|
+
return fail(400, `\u767B\u5F55\u5931\u8D25\uFF1A\u6388\u6743\u670D\u52A1\u5668\u8FD4\u56DE ${error}${error === "access_denied" ? "\uFF08\u7528\u6237\u62D2\u7EDD\u6388\u6743\uFF09" : ""}`);
|
|
177
|
+
const code = q.get("code");
|
|
178
|
+
if (!code)
|
|
179
|
+
return fail(400, "\u767B\u5F55\u5931\u8D25\uFF1A\u56DE\u8C03\u7F3A\u5C11 code");
|
|
180
|
+
const returned = q.get("client_id") ?? void 0;
|
|
181
|
+
let clientId;
|
|
182
|
+
if (expected.clientId) {
|
|
183
|
+
if (returned !== void 0 && returned !== expected.clientId) {
|
|
184
|
+
return fail(400, "\u767B\u5F55\u5931\u8D25\uFF1A\u56DE\u8C03\u8FD4\u56DE\u7684 client_id \u4E0E\u5DF2\u4FDD\u5B58\u7684\u6CE8\u518C\u4E0D\u4E00\u81F4\uFF0C\u5DF2\u62D2\u7EDD");
|
|
185
|
+
}
|
|
186
|
+
clientId = expected.clientId;
|
|
187
|
+
} else {
|
|
188
|
+
if (!returned || returned === SIWC.dynamicClientId)
|
|
189
|
+
return fail(400, "\u767B\u5F55\u5931\u8D25\uFF1A\u9996\u6B21\u6CE8\u518C\u7684\u56DE\u8C03\u7F3A\u5C11\u7B7E\u53D1\u7684 client_id\uFF0C\u6CE8\u518C\u672A\u5B8C\u6210");
|
|
190
|
+
clientId = returned;
|
|
191
|
+
}
|
|
192
|
+
finish(() => resolve({ code, clientId, scope: q.get("scope") ?? void 0, res }));
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
async function errorCode(res) {
|
|
197
|
+
try {
|
|
198
|
+
const body = await res.json();
|
|
199
|
+
const err = body.error;
|
|
200
|
+
if (typeof err === "string")
|
|
201
|
+
return { code: err, detail: typeof body.error_description === "string" ? body.error_description : void 0 };
|
|
202
|
+
if (err && typeof err === "object") {
|
|
203
|
+
const o = err;
|
|
204
|
+
return {
|
|
205
|
+
code: typeof o.code === "string" ? o.code : typeof o.type === "string" ? o.type : void 0,
|
|
206
|
+
detail: typeof o.message === "string" ? o.message : void 0
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
if (typeof body.detail === "string")
|
|
210
|
+
return { detail: body.detail };
|
|
211
|
+
} catch {
|
|
212
|
+
}
|
|
213
|
+
return {};
|
|
214
|
+
}
|
|
215
|
+
async function exchangeCode(tokenUrl, form, signal) {
|
|
216
|
+
const res = await fetch(tokenUrl, {
|
|
217
|
+
method: "POST",
|
|
218
|
+
headers: { ...nemaHeaders(), "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
|
|
219
|
+
body: new URLSearchParams(form).toString(),
|
|
220
|
+
signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(EXCHANGE_TIMEOUT_MS)]) : AbortSignal.timeout(EXCHANGE_TIMEOUT_MS)
|
|
221
|
+
});
|
|
222
|
+
if (!res.ok) {
|
|
223
|
+
const { code, detail } = await errorCode(res);
|
|
224
|
+
throw new Error(`\u6362\u53D6\u4EE4\u724C\u5931\u8D25\uFF1AHTTP ${res.status}${code ? ` ${code}` : ""}${detail ? `\uFF08${detail}\uFF09` : ""}`);
|
|
225
|
+
}
|
|
226
|
+
const body = await res.json();
|
|
227
|
+
if (typeof body.access_token !== "string" || body.access_token === "")
|
|
228
|
+
throw new Error("\u6362\u53D6\u4EE4\u724C\u5931\u8D25\uFF1A\u54CD\u5E94\u7F3A\u5C11 access_token");
|
|
229
|
+
if (typeof body.refresh_token !== "string" || body.refresh_token === "") {
|
|
230
|
+
throw new Error("\u6362\u53D6\u4EE4\u724C\u5931\u8D25\uFF1A\u54CD\u5E94\u7F3A\u5C11 refresh_token\uFF08\u672A\u6388\u4E88 offline_access\uFF09");
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
access_token: body.access_token,
|
|
234
|
+
refresh_token: body.refresh_token,
|
|
235
|
+
id_token: typeof body.id_token === "string" ? body.id_token : void 0,
|
|
236
|
+
scope: typeof body.scope === "string" ? body.scope : void 0
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function validateIdToken(idToken, expected) {
|
|
240
|
+
if (!idToken)
|
|
241
|
+
throw new Error("\u767B\u5F55\u5931\u8D25\uFF1A\u4EE4\u724C\u54CD\u5E94\u7F3A\u5C11 id_token");
|
|
242
|
+
let claims;
|
|
243
|
+
try {
|
|
244
|
+
claims = decodeJwtPayload(idToken);
|
|
245
|
+
} catch {
|
|
246
|
+
throw new Error("\u767B\u5F55\u5931\u8D25\uFF1Aid_token \u65E0\u6CD5\u89E3\u6790");
|
|
247
|
+
}
|
|
248
|
+
if (claims.iss !== SIWC.issuer)
|
|
249
|
+
throw new Error("\u767B\u5F55\u5931\u8D25\uFF1Aid_token \u7684 issuer \u4E0D\u7B26");
|
|
250
|
+
const aud = claims.aud;
|
|
251
|
+
const audOk = typeof aud === "string" ? aud === expected.clientId : Array.isArray(aud) && aud.includes(expected.clientId);
|
|
252
|
+
if (!audOk)
|
|
253
|
+
throw new Error("\u767B\u5F55\u5931\u8D25\uFF1Aid_token \u7684 audience \u4E0E\u7B7E\u53D1\u7684 client_id \u4E0D\u7B26");
|
|
254
|
+
if (typeof claims.nonce !== "string" || !safeEqual(claims.nonce, expected.nonce))
|
|
255
|
+
throw new Error("\u767B\u5F55\u5931\u8D25\uFF1Aid_token \u7684 nonce \u4E0D\u5339\u914D");
|
|
256
|
+
if (typeof claims.exp === "number" && claims.exp <= expected.nowS)
|
|
257
|
+
throw new Error("\u767B\u5F55\u5931\u8D25\uFF1Aid_token \u5DF2\u8FC7\u671F");
|
|
258
|
+
if (typeof claims.sub !== "string" || claims.sub === "")
|
|
259
|
+
throw new Error("\u767B\u5F55\u5931\u8D25\uFF1Aid_token \u7F3A\u5C11 sub");
|
|
260
|
+
return claims.sub;
|
|
261
|
+
}
|
|
262
|
+
function grantedScopes(tokens, callbackScope) {
|
|
263
|
+
if (tokens.scope !== void 0)
|
|
264
|
+
return tokens.scope.split(/[\s+]+/).filter(Boolean);
|
|
265
|
+
try {
|
|
266
|
+
const s = decodeJwtPayload(tokens.access_token).scope;
|
|
267
|
+
if (typeof s === "string")
|
|
268
|
+
return s.split(/\s+/).filter(Boolean);
|
|
269
|
+
} catch {
|
|
270
|
+
}
|
|
271
|
+
return callbackScope !== void 0 ? callbackScope.split(/[\s+]+/).filter(Boolean) : null;
|
|
272
|
+
}
|
|
273
|
+
async function siwcLogin(opts) {
|
|
274
|
+
const { nemaHome } = opts;
|
|
275
|
+
const authorizeUrl = opts.endpoints?.authorizeUrl ?? SIWC.authorizeUrl;
|
|
276
|
+
const tokenUrl = opts.endpoints?.tokenUrl ?? SIWC.tokenUrl;
|
|
277
|
+
const timeoutMs = opts.timeoutMs ?? 3e5;
|
|
278
|
+
const store = new AuthStore(nemaHome, siwcRefresher);
|
|
279
|
+
opts.signal?.throwIfAborted();
|
|
280
|
+
const prior = store.read();
|
|
281
|
+
const savedClientId = prior && prior.mode === "siwc" && prior.clientId !== "" && prior.clientId !== SIWC.dynamicClientId && prior.lastAuthError?.code !== "invalid_client" ? prior.clientId : void 0;
|
|
282
|
+
const extAgentHostId = hostId(nemaHome);
|
|
283
|
+
const verifier = randomToken();
|
|
284
|
+
const challenge = b64url(createHash("sha256").update(verifier).digest());
|
|
285
|
+
const state = randomToken();
|
|
286
|
+
const nonce = randomToken();
|
|
287
|
+
const server = createServer();
|
|
288
|
+
try {
|
|
289
|
+
const port = await startCallbackServer(server, opts.port);
|
|
290
|
+
const redirectUri = `http://${SIWC.callbackHost}:${port}${SIWC.callbackPath}`;
|
|
291
|
+
const params = new URLSearchParams({
|
|
292
|
+
client_id: savedClientId ?? SIWC.dynamicClientId,
|
|
293
|
+
...savedClientId ? {} : { agent_name_hint: SIWC.agentNameHint },
|
|
294
|
+
ext_agent_host_id: extAgentHostId,
|
|
295
|
+
response_type: "code",
|
|
296
|
+
redirect_uri: redirectUri,
|
|
297
|
+
scope: `${SIWC.identityScopes} ${SIWC.planScopes}`,
|
|
298
|
+
resource: SIWC.resource,
|
|
299
|
+
state,
|
|
300
|
+
nonce,
|
|
301
|
+
code_challenge_method: "S256",
|
|
302
|
+
code_challenge: challenge
|
|
303
|
+
});
|
|
304
|
+
const authorizeRequestUrl = `${authorizeUrl}?${params.toString()}`;
|
|
305
|
+
if (opts.requireTrustedAuthorizeUrl) {
|
|
306
|
+
const url = new URL(authorizeRequestUrl);
|
|
307
|
+
if (url.protocol !== "https:" || url.hostname !== "auth.openai.com" || url.pathname !== "/api/accounts/authorize" || url.username || url.password || url.port || url.hash) {
|
|
308
|
+
throw new Error("Untrusted OAuth authorize URL");
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const callback = waitForCallback(server, { state, clientId: savedClientId }, timeoutMs, opts.signal, opts.redactCallbackErrors);
|
|
312
|
+
try {
|
|
313
|
+
opts.openBrowser(authorizeRequestUrl);
|
|
314
|
+
} catch {
|
|
315
|
+
}
|
|
316
|
+
const cb = await callback;
|
|
317
|
+
try {
|
|
318
|
+
opts.signal?.throwIfAborted();
|
|
319
|
+
const tokens = await exchangeCode(tokenUrl, {
|
|
320
|
+
grant_type: "authorization_code",
|
|
321
|
+
client_id: cb.clientId,
|
|
322
|
+
code: cb.code,
|
|
323
|
+
code_verifier: verifier,
|
|
324
|
+
redirect_uri: redirectUri,
|
|
325
|
+
resource: SIWC.resource
|
|
326
|
+
}, opts.signal);
|
|
327
|
+
const scopes = grantedScopes(tokens, cb.scope);
|
|
328
|
+
if (!scopes || !scopes.includes(SIWC.planUsageScope)) {
|
|
329
|
+
throw new Error(`\u767B\u5F55\u5931\u8D25\uFF1A\u672A\u6388\u4E88 ${SIWC.planUsageScope}\uFF08\u8BE5\u8D26\u53F7\u672A\u5141\u8BB8 Nema \u4F7F\u7528 ChatGPT \u5957\u9910\u989D\u5EA6\uFF09\uFF0C\u672A\u4FDD\u5B58\u51ED\u8BC1`);
|
|
330
|
+
}
|
|
331
|
+
const accountId = validateIdToken(tokens.id_token, { clientId: cb.clientId, nonce, nowS: Math.floor(Date.now() / 1e3) });
|
|
332
|
+
const auth = {
|
|
333
|
+
version: 1,
|
|
334
|
+
mode: "siwc",
|
|
335
|
+
clientId: cb.clientId,
|
|
336
|
+
tokens: {
|
|
337
|
+
access_token: tokens.access_token,
|
|
338
|
+
refresh_token: tokens.refresh_token,
|
|
339
|
+
...tokens.id_token ? { id_token: tokens.id_token } : {}
|
|
340
|
+
},
|
|
341
|
+
accountId,
|
|
342
|
+
lastRefresh: (/* @__PURE__ */ new Date()).toISOString()
|
|
343
|
+
};
|
|
344
|
+
await withFileLock(path.join(nemaHome, "auth.lock"), async () => {
|
|
345
|
+
opts.signal?.throwIfAborted();
|
|
346
|
+
store.save(auth);
|
|
347
|
+
}, { timeoutMs: 15e3 });
|
|
348
|
+
page(cb.res, 200, "Nema Room \u767B\u5F55\u6210\u529F\uFF0C\u53EF\u4EE5\u5173\u95ED\u6B64\u9875\u9762\u5E76\u56DE\u5230\u7EC8\u7AEF\u3002");
|
|
349
|
+
return auth;
|
|
350
|
+
} catch (e) {
|
|
351
|
+
page(cb.res, 500, opts.redactCallbackErrors ? "Login failed. Return to Nema Room settings." : e instanceof Error ? e.message : "\u767B\u5F55\u5931\u8D25");
|
|
352
|
+
throw e;
|
|
353
|
+
}
|
|
354
|
+
} finally {
|
|
355
|
+
server.closeAllConnections();
|
|
356
|
+
await new Promise((r) => server.close(() => r()));
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
function makeSiwcRefresher(tokenUrl = SIWC.tokenUrl) {
|
|
360
|
+
return async (auth, signal) => {
|
|
361
|
+
if (auth.mode !== "siwc")
|
|
362
|
+
throw new ReloginRequiredError("mode_mismatch");
|
|
363
|
+
const res = await fetch(tokenUrl, {
|
|
364
|
+
method: "POST",
|
|
365
|
+
headers: { ...nemaHeaders(), "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
|
|
366
|
+
body: new URLSearchParams({
|
|
367
|
+
grant_type: "refresh_token",
|
|
368
|
+
refresh_token: auth.tokens.refresh_token,
|
|
369
|
+
client_id: auth.clientId,
|
|
370
|
+
resource: SIWC.resource
|
|
371
|
+
// scope omitted to retain the grant
|
|
372
|
+
}).toString(),
|
|
373
|
+
signal
|
|
374
|
+
});
|
|
375
|
+
if (!res.ok) {
|
|
376
|
+
const { code } = await errorCode(res);
|
|
377
|
+
if (code && UNUSABLE_REFRESH_CODES.includes(code))
|
|
378
|
+
throw new ReloginRequiredError(code);
|
|
379
|
+
const ra = Number(res.headers.get("retry-after"));
|
|
380
|
+
throw new RefreshError(res.status, { code, retryAfterS: Number.isFinite(ra) && ra >= 0 && res.headers.has("retry-after") ? ra : void 0 });
|
|
381
|
+
}
|
|
382
|
+
const body = await res.json();
|
|
383
|
+
if (typeof body.access_token !== "string" || body.access_token === "")
|
|
384
|
+
throw new Error("\u5237\u65B0\u54CD\u5E94\u7F3A\u5C11 access_token");
|
|
385
|
+
return {
|
|
386
|
+
access_token: body.access_token,
|
|
387
|
+
...typeof body.refresh_token === "string" && body.refresh_token !== "" ? { refresh_token: body.refresh_token } : {},
|
|
388
|
+
...typeof body.id_token === "string" && body.id_token !== "" ? { id_token: body.id_token } : {}
|
|
389
|
+
};
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
var siwcRefresher = makeSiwcRefresher();
|
|
393
|
+
export {
|
|
394
|
+
SIWC,
|
|
395
|
+
UNUSABLE_REFRESH_CODES,
|
|
396
|
+
cliVersion,
|
|
397
|
+
hostId,
|
|
398
|
+
makeSiwcRefresher,
|
|
399
|
+
nemaHeaders,
|
|
400
|
+
nemaUserAgent,
|
|
401
|
+
siwcLogin,
|
|
402
|
+
siwcRefresher
|
|
403
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type PluginToolSpec } from "@nema/shared";
|
|
2
|
+
import { type RoomClient } from "../room/client.js";
|
|
3
|
+
import type { FunctionTool } from "./transport.js";
|
|
4
|
+
export type RoomApi = Pick<RoomClient, "listEntries" | "readEntry" | "upsertEntry" | "openThread" | "callPlugin">;
|
|
5
|
+
export declare const ROOM_TOOLS: FunctionTool[];
|
|
6
|
+
/** Claude allowed names for the same functions `roomTools` declares, prefixed `mcp__room__`. */
|
|
7
|
+
export declare function claudeAllowedToolNames(pluginTools: PluginToolSpec[]): string[];
|
|
8
|
+
/** The 4 Room functions, then one non-strict function per plugin tool, in the order the task listed them. */
|
|
9
|
+
export declare function roomTools(pluginTools: PluginToolSpec[]): FunctionTool[];
|
|
10
|
+
/**
|
|
11
|
+
* Runs one model function call against the Room API. Unknown names and arguments that are not a JSON object
|
|
12
|
+
* return an error output with `known: false` (the loop counts these) and never reach the hub.
|
|
13
|
+
*/
|
|
14
|
+
export declare function dispatchTool(name: string, argsJson: string, room: RoomApi, pluginTools?: PluginToolSpec[]): Promise<{
|
|
15
|
+
output: string;
|
|
16
|
+
known: boolean;
|
|
17
|
+
}>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import {
|
|
2
|
+
UpsertEntryInput
|
|
3
|
+
} from "../chunks/chunk-HEIQINX3.js";
|
|
4
|
+
|
|
5
|
+
// direct/tools.js
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { capText } from "../room/client.js";
|
|
8
|
+
function upsertParameters() {
|
|
9
|
+
const { $schema: _drop, ...schema } = z.toJSONSchema(UpsertEntryInput);
|
|
10
|
+
const required = Array.isArray(schema.required) ? schema.required : [];
|
|
11
|
+
return { ...schema, required: required.includes("basis") ? required : [...required, "basis"] };
|
|
12
|
+
}
|
|
13
|
+
var ROOM_TOOLS = [
|
|
14
|
+
{
|
|
15
|
+
type: "function",
|
|
16
|
+
name: "list_entries",
|
|
17
|
+
description: "List all entries in the Room knowledge base.",
|
|
18
|
+
strict: false,
|
|
19
|
+
parameters: { type: "object", properties: {}, additionalProperties: false }
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
type: "function",
|
|
23
|
+
name: "read_entry",
|
|
24
|
+
description: "Read one Room entry by id.",
|
|
25
|
+
strict: false,
|
|
26
|
+
parameters: { type: "object", properties: { id: { type: "string" } }, required: ["id"], additionalProperties: false }
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
type: "function",
|
|
30
|
+
name: "upsert_entry",
|
|
31
|
+
description: "Create an entry, or update one when `id` is given. sourceMessages and reason are required.",
|
|
32
|
+
strict: false,
|
|
33
|
+
parameters: upsertParameters()
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
type: "function",
|
|
37
|
+
name: "open_thread",
|
|
38
|
+
description: "Move this conversation into a thread anchored on the current message. When the user asks to compare options or weigh tradeoffs and has not decided, call this first and discuss inside the thread. Your reply will be posted in the thread.",
|
|
39
|
+
strict: false,
|
|
40
|
+
parameters: { type: "object", properties: { title: { type: "string" } }, required: ["title"], additionalProperties: false }
|
|
41
|
+
}
|
|
42
|
+
];
|
|
43
|
+
function claudeAllowedToolNames(pluginTools) {
|
|
44
|
+
return roomTools(pluginTools).map((tool) => `mcp__room__${tool.name}`);
|
|
45
|
+
}
|
|
46
|
+
function roomTools(pluginTools) {
|
|
47
|
+
return [
|
|
48
|
+
...ROOM_TOOLS,
|
|
49
|
+
...pluginTools.map((tool) => ({
|
|
50
|
+
type: "function",
|
|
51
|
+
name: tool.name,
|
|
52
|
+
description: tool.description,
|
|
53
|
+
strict: false,
|
|
54
|
+
parameters: tool.parameters
|
|
55
|
+
}))
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
var errorOutput = (error, extra = {}) => JSON.stringify({ error, ...extra });
|
|
59
|
+
function resultOutput(r) {
|
|
60
|
+
if (!r.ok)
|
|
61
|
+
return capText(errorOutput(r.error, { status: r.status }));
|
|
62
|
+
return capText(typeof r.json === "string" ? r.json : JSON.stringify(r.json));
|
|
63
|
+
}
|
|
64
|
+
async function dispatchTool(name, argsJson, room, pluginTools = []) {
|
|
65
|
+
const plugin = pluginTools.find((tool) => tool.name === name);
|
|
66
|
+
const isRoom = name === "list_entries" || name === "read_entry" || name === "upsert_entry" || name === "open_thread";
|
|
67
|
+
if (!plugin && !isRoom) {
|
|
68
|
+
const shown = name.length > 100 ? name.slice(0, 100) + "\u2026" : name;
|
|
69
|
+
const allowed = ["list_entries", "read_entry", "upsert_entry", "open_thread", ...pluginTools.map((tool) => tool.name)].join("\u3001");
|
|
70
|
+
return { output: errorOutput(`\u672A\u77E5\u51FD\u6570 ${JSON.stringify(shown)}\uFF1B\u53EA\u80FD\u4F7F\u7528 ${allowed}`), known: false };
|
|
71
|
+
}
|
|
72
|
+
let args;
|
|
73
|
+
try {
|
|
74
|
+
args = argsJson.trim() === "" ? {} : JSON.parse(argsJson);
|
|
75
|
+
} catch {
|
|
76
|
+
return { output: errorOutput(`${name} \u7684\u53C2\u6570\u4E0D\u662F\u5408\u6CD5\u7684 JSON`), known: false };
|
|
77
|
+
}
|
|
78
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) {
|
|
79
|
+
return { output: errorOutput(`${name} \u7684\u53C2\u6570\u5FC5\u987B\u662F JSON \u5BF9\u8C61`), known: false };
|
|
80
|
+
}
|
|
81
|
+
if (plugin)
|
|
82
|
+
return { output: resultOutput(await room.callPlugin(name, args)), known: true };
|
|
83
|
+
switch (name) {
|
|
84
|
+
case "list_entries":
|
|
85
|
+
return { output: resultOutput(await room.listEntries()), known: true };
|
|
86
|
+
case "read_entry": {
|
|
87
|
+
const id = args.id;
|
|
88
|
+
if (typeof id !== "string" || id === "")
|
|
89
|
+
return { output: errorOutput("read_entry \u9700\u8981\u5B57\u7B26\u4E32\u53C2\u6570 id"), known: true };
|
|
90
|
+
return { output: resultOutput(await room.readEntry(id)), known: true };
|
|
91
|
+
}
|
|
92
|
+
case "upsert_entry":
|
|
93
|
+
return { output: resultOutput(await room.upsertEntry(args)), known: true };
|
|
94
|
+
case "open_thread":
|
|
95
|
+
return { output: resultOutput(await room.openThread(args)), known: true };
|
|
96
|
+
}
|
|
97
|
+
throw new Error(`unreachable tool ${name}`);
|
|
98
|
+
}
|
|
99
|
+
export {
|
|
100
|
+
ROOM_TOOLS,
|
|
101
|
+
claudeAllowedToolNames,
|
|
102
|
+
dispatchTool,
|
|
103
|
+
roomTools
|
|
104
|
+
};
|