@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,151 @@
|
|
|
1
|
+
// codex/adapter.js
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { lstatSync, renameSync, symlinkSync, chmodSync, readFileSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { buildEnv, checkCodexAuth, cleanupRun, prepareIsolation, preflight } from "./isolation.js";
|
|
7
|
+
import { parseCodexLine } from "./events.js";
|
|
8
|
+
function restoreAuthSymlink(paths, userCodexHome) {
|
|
9
|
+
const link = path.join(paths.codexHome, "auth.json");
|
|
10
|
+
try {
|
|
11
|
+
if (lstatSync(link).isSymbolicLink())
|
|
12
|
+
return;
|
|
13
|
+
} catch {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const target = path.join(userCodexHome, "auth.json");
|
|
17
|
+
renameSync(link, target);
|
|
18
|
+
chmodSync(target, 384);
|
|
19
|
+
symlinkSync(target, link);
|
|
20
|
+
}
|
|
21
|
+
var CONFIDENTIAL_REFUSAL = "codex-cli \u9002\u914D\u5668\u5728\u4FDD\u5BC6 workspace \u4E2D\u88AB\u7981\u7528\uFF08\u9694\u79BB\u672A\u901A\u8FC7\u9A8C\u8BC1\uFF09\u3002\u8BF7\u6539\u7528 codex-direct\uFF1Anema config set adapter codex-direct\uFF0C\u6216\u5728 workspace \u8BBE\u7F6E\u4E2D\u5173\u95ED\u4FDD\u5BC6\u3002";
|
|
22
|
+
var CodexAdapter = class {
|
|
23
|
+
opts;
|
|
24
|
+
name = "codex-cli";
|
|
25
|
+
constructor(opts) {
|
|
26
|
+
this.opts = opts;
|
|
27
|
+
}
|
|
28
|
+
async run(task, onEvent) {
|
|
29
|
+
if (task.confidential)
|
|
30
|
+
return { ok: false, error: CONFIDENTIAL_REFUSAL };
|
|
31
|
+
const auth = checkCodexAuth(this.opts.userCodexHome);
|
|
32
|
+
if (!auth.ok)
|
|
33
|
+
return { ok: false, error: auth.error };
|
|
34
|
+
const paths = await prepareIsolation({
|
|
35
|
+
nemaHome: this.opts.nemaHome,
|
|
36
|
+
workspaceId: task.workspaceId,
|
|
37
|
+
userCodexHome: this.opts.userCodexHome,
|
|
38
|
+
mcp: {
|
|
39
|
+
command: process.execPath,
|
|
40
|
+
args: [this.opts.cliEntry, "mcp"],
|
|
41
|
+
env: { NEMA_HUB_URL: task.hubUrl, NEMA_RUN_TOKEN: task.runToken }
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
try {
|
|
45
|
+
const pf = preflight(paths);
|
|
46
|
+
if (!pf.ok)
|
|
47
|
+
return { ok: false, error: "\u9694\u79BB\u81EA\u68C0\u5931\u8D25\uFF1A" + pf.problems.join("\uFF1B") };
|
|
48
|
+
return await this.exec(task, paths, onEvent);
|
|
49
|
+
} finally {
|
|
50
|
+
try {
|
|
51
|
+
restoreAuthSymlink(paths, this.opts.userCodexHome);
|
|
52
|
+
} catch {
|
|
53
|
+
}
|
|
54
|
+
await cleanupRun(paths);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
exec(task, paths, onEvent) {
|
|
58
|
+
const bin = this.opts.codexBin ?? process.env.NEMA_CODEX_BIN ?? "codex";
|
|
59
|
+
const lastFile = path.join(paths.workDir, "last.txt");
|
|
60
|
+
const timeoutMs = this.opts.timeoutMs ?? 6e5;
|
|
61
|
+
return new Promise((resolve) => {
|
|
62
|
+
let settled = false;
|
|
63
|
+
let lastMessage = "";
|
|
64
|
+
let stderr = "";
|
|
65
|
+
let buf = "";
|
|
66
|
+
let exited = false;
|
|
67
|
+
let exitWait;
|
|
68
|
+
const killTree = () => {
|
|
69
|
+
try {
|
|
70
|
+
if (child.pid !== void 0)
|
|
71
|
+
process.kill(-child.pid, "SIGKILL");
|
|
72
|
+
} catch {
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const finish = (r, kill = false) => {
|
|
76
|
+
if (settled)
|
|
77
|
+
return;
|
|
78
|
+
settled = true;
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
if (!kill)
|
|
81
|
+
return resolve(r);
|
|
82
|
+
killTree();
|
|
83
|
+
if (exited)
|
|
84
|
+
return resolve(r);
|
|
85
|
+
exitWait = setTimeout(() => resolve(r), 2e3);
|
|
86
|
+
child.once("close", () => {
|
|
87
|
+
clearTimeout(exitWait);
|
|
88
|
+
resolve(r);
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
const child = spawn(bin, ["exec", "--json", "--ephemeral", "--skip-git-repo-check", "-C", paths.workDir, "-o", lastFile, "-"], { env: buildEnv(paths, process.env), cwd: paths.workDir, stdio: ["pipe", "pipe", "pipe"], detached: true });
|
|
92
|
+
const timer = setTimeout(() => finish({ ok: false, error: "Codex \u8FD0\u884C\u8D85\u65F6" }, true), timeoutMs);
|
|
93
|
+
child.on("error", (e) => {
|
|
94
|
+
finish({ ok: false, error: e.code === "ENOENT" ? "\u627E\u4E0D\u5230 codex \u547D\u4EE4\uFF0C\u8BF7\u5148\u5B89\u88C5 Codex CLI" : `\u65E0\u6CD5\u542F\u52A8 Codex\uFF1A${e.message}` });
|
|
95
|
+
});
|
|
96
|
+
child.stdin.on("error", () => {
|
|
97
|
+
});
|
|
98
|
+
child.stdin.end(task.prompt);
|
|
99
|
+
const handleLine = (line) => {
|
|
100
|
+
const ev = parseCodexLine(line);
|
|
101
|
+
if (!ev || settled)
|
|
102
|
+
return;
|
|
103
|
+
if (ev.kind === "agent_message") {
|
|
104
|
+
lastMessage = ev.text;
|
|
105
|
+
onEvent({ type: "agent_message", text: ev.text });
|
|
106
|
+
} else if (ev.kind === "mcp_call" && ev.server !== "room") {
|
|
107
|
+
finish({ ok: false, error: `Agent \u8C03\u7528\u4E86 Room \u4EE5\u5916\u7684 MCP server\uFF1A${ev.server}` }, true);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
child.stdout.setEncoding("utf8");
|
|
111
|
+
child.stderr.setEncoding("utf8");
|
|
112
|
+
child.stdout.on("data", (d) => {
|
|
113
|
+
buf += d;
|
|
114
|
+
let i;
|
|
115
|
+
while ((i = buf.indexOf("\n")) >= 0) {
|
|
116
|
+
handleLine(buf.slice(0, i));
|
|
117
|
+
buf = buf.slice(i + 1);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
child.stderr.on("data", (d) => {
|
|
121
|
+
stderr += d;
|
|
122
|
+
if (stderr.length > 64e3)
|
|
123
|
+
stderr = stderr.slice(-64e3);
|
|
124
|
+
});
|
|
125
|
+
child.on("close", (code) => {
|
|
126
|
+
exited = true;
|
|
127
|
+
killTree();
|
|
128
|
+
if (buf.trim())
|
|
129
|
+
handleLine(buf);
|
|
130
|
+
if (settled)
|
|
131
|
+
return;
|
|
132
|
+
if (code !== 0) {
|
|
133
|
+
const tail = stderr.trimEnd().split("\n").slice(-20).join("\n");
|
|
134
|
+
return finish({ ok: false, error: `Codex \u9000\u51FA\u7801 ${code}\uFF1A${tail}` });
|
|
135
|
+
}
|
|
136
|
+
let reply = "";
|
|
137
|
+
try {
|
|
138
|
+
reply = readFileSync(lastFile, "utf8").trim();
|
|
139
|
+
} catch {
|
|
140
|
+
}
|
|
141
|
+
if (!reply)
|
|
142
|
+
reply = lastMessage.trim();
|
|
143
|
+
finish(reply ? { ok: true, reply } : { ok: false, error: "Agent \u6CA1\u6709\u8FD4\u56DE\u56DE\u590D" });
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
export {
|
|
149
|
+
CONFIDENTIAL_REFUSAL,
|
|
150
|
+
CodexAdapter
|
|
151
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type CodexEvent = {
|
|
2
|
+
kind: "agent_message";
|
|
3
|
+
text: string;
|
|
4
|
+
} | {
|
|
5
|
+
kind: "mcp_call";
|
|
6
|
+
server: string;
|
|
7
|
+
tool: string;
|
|
8
|
+
} | {
|
|
9
|
+
kind: "turn_completed";
|
|
10
|
+
} | {
|
|
11
|
+
kind: "error";
|
|
12
|
+
message: string;
|
|
13
|
+
} | {
|
|
14
|
+
kind: "other";
|
|
15
|
+
};
|
|
16
|
+
export declare function parseCodexLine(line: string): CodexEvent | null;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// codex/events.js
|
|
2
|
+
var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
3
|
+
function parseCodexLine(line) {
|
|
4
|
+
let raw;
|
|
5
|
+
try {
|
|
6
|
+
raw = JSON.parse(line);
|
|
7
|
+
} catch {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
if (!isObj(raw) || typeof raw.type !== "string")
|
|
11
|
+
return null;
|
|
12
|
+
switch (raw.type) {
|
|
13
|
+
case "turn.completed":
|
|
14
|
+
return { kind: "turn_completed" };
|
|
15
|
+
case "turn.failed": {
|
|
16
|
+
const e = raw.error;
|
|
17
|
+
return { kind: "error", message: isObj(e) && typeof e.message === "string" ? e.message : "turn failed" };
|
|
18
|
+
}
|
|
19
|
+
case "error":
|
|
20
|
+
return { kind: "error", message: typeof raw.message === "string" ? raw.message : "error" };
|
|
21
|
+
case "item.started":
|
|
22
|
+
case "item.completed":
|
|
23
|
+
case "item.updated": {
|
|
24
|
+
const item = raw.item;
|
|
25
|
+
if (!isObj(item))
|
|
26
|
+
return { kind: "other" };
|
|
27
|
+
if (item.type === "agent_message" && raw.type === "item.completed" && typeof item.text === "string") {
|
|
28
|
+
return { kind: "agent_message", text: item.text };
|
|
29
|
+
}
|
|
30
|
+
if (item.type === "mcp_tool_call") {
|
|
31
|
+
return {
|
|
32
|
+
kind: "mcp_call",
|
|
33
|
+
server: typeof item.server === "string" ? item.server : "",
|
|
34
|
+
tool: typeof item.tool === "string" ? item.tool : ""
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (item.type === "error" && typeof item.message === "string")
|
|
38
|
+
return { kind: "error", message: item.message };
|
|
39
|
+
return { kind: "other" };
|
|
40
|
+
}
|
|
41
|
+
default:
|
|
42
|
+
return { kind: "other" };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export {
|
|
46
|
+
parseCodexLine
|
|
47
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type IsolationPaths = {
|
|
2
|
+
wsHome: string;
|
|
3
|
+
codexHome: string;
|
|
4
|
+
workDir: string;
|
|
5
|
+
tmpDir: string;
|
|
6
|
+
};
|
|
7
|
+
export type McpSpec = {
|
|
8
|
+
command: string;
|
|
9
|
+
args: string[];
|
|
10
|
+
env: Record<string, string>;
|
|
11
|
+
};
|
|
12
|
+
export declare function checkCodexAuth(userCodexHome: string): {
|
|
13
|
+
ok: true;
|
|
14
|
+
authPath: string;
|
|
15
|
+
} | {
|
|
16
|
+
ok: false;
|
|
17
|
+
error: string;
|
|
18
|
+
};
|
|
19
|
+
export declare function renderCodexConfig(mcp: McpSpec): string;
|
|
20
|
+
export declare function prepareIsolation(opts: {
|
|
21
|
+
nemaHome: string;
|
|
22
|
+
workspaceId: string;
|
|
23
|
+
userCodexHome: string;
|
|
24
|
+
mcp: McpSpec;
|
|
25
|
+
}): Promise<IsolationPaths>;
|
|
26
|
+
export declare function buildEnv(paths: IsolationPaths, parentEnv: NodeJS.ProcessEnv): Record<string, string>;
|
|
27
|
+
export declare function preflight(paths: IsolationPaths): {
|
|
28
|
+
ok: true;
|
|
29
|
+
} | {
|
|
30
|
+
ok: false;
|
|
31
|
+
problems: string[];
|
|
32
|
+
};
|
|
33
|
+
export declare function cleanupRun(paths: IsolationPaths): Promise<void>;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// codex/isolation.js
|
|
2
|
+
import { existsSync, lstatSync, readFileSync } from "node:fs";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
7
|
+
function checkCodexAuth(userCodexHome) {
|
|
8
|
+
const authPath = path.join(userCodexHome, "auth.json");
|
|
9
|
+
try {
|
|
10
|
+
const cfg = parseToml(readFileSync(path.join(userCodexHome, "config.toml"), "utf8"));
|
|
11
|
+
if (cfg.cli_auth_credentials_store === "keyring") {
|
|
12
|
+
return {
|
|
13
|
+
ok: false,
|
|
14
|
+
error: 'Codex \u51ED\u8BC1\u5B58\u653E\u5728\u7CFB\u7EDF\u94A5\u5319\u4E32\u4E2D\uFF0CNema \u65E0\u6CD5\u8BFB\u53D6\u3002\u8BF7\u5728 ~/.codex/config.toml \u8BBE\u7F6E cli_auth_credentials_store = "file"\uFF0C\u7136\u540E\u91CD\u65B0\u8FD0\u884C codex login\u3002'
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
} catch {
|
|
18
|
+
}
|
|
19
|
+
if (!existsSync(authPath)) {
|
|
20
|
+
return {
|
|
21
|
+
ok: false,
|
|
22
|
+
error: `\u627E\u4E0D\u5230 Codex \u767B\u5F55\u6587\u4EF6 ${authPath}\u3002\u8BF7\u5148\u8FD0\u884C codex login\uFF1B\u5982\u679C\u51ED\u8BC1\u5B58\u5728\u7CFB\u7EDF\u94A5\u5319\u4E32\u91CC\uFF0C\u8BF7\u5728 ~/.codex/config.toml \u8BBE\u7F6E cli_auth_credentials_store = "file" \u540E\u91CD\u65B0\u767B\u5F55\u3002`
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return { ok: true, authPath };
|
|
26
|
+
}
|
|
27
|
+
function renderCodexConfig(mcp) {
|
|
28
|
+
return stringifyToml({
|
|
29
|
+
approval_policy: "never",
|
|
30
|
+
sandbox_mode: "read-only",
|
|
31
|
+
web_search: "disabled",
|
|
32
|
+
check_for_update_on_startup: false,
|
|
33
|
+
history: { persistence: "none" },
|
|
34
|
+
features: { memories: false },
|
|
35
|
+
analytics: { enabled: false },
|
|
36
|
+
shell_environment_policy: { inherit: "core" },
|
|
37
|
+
mcp_servers: { room: { command: mcp.command, args: mcp.args, env: mcp.env, default_tools_approval_mode: "approve" } }
|
|
38
|
+
}) + "\n";
|
|
39
|
+
}
|
|
40
|
+
async function prepareIsolation(opts) {
|
|
41
|
+
if (!/^[A-Za-z0-9_-]+$/.test(opts.workspaceId))
|
|
42
|
+
throw new Error(`invalid workspace id: ${opts.workspaceId}`);
|
|
43
|
+
const wsHome = path.join(opts.nemaHome, "ws", opts.workspaceId);
|
|
44
|
+
const codexHome = path.join(wsHome, "codex-home");
|
|
45
|
+
await fs.mkdir(codexHome, { recursive: true, mode: 448 });
|
|
46
|
+
await fs.chmod(wsHome, 448);
|
|
47
|
+
await fs.chmod(codexHome, 448);
|
|
48
|
+
const link = path.join(codexHome, "auth.json");
|
|
49
|
+
await fs.rm(link, { force: true });
|
|
50
|
+
await fs.symlink(path.join(opts.userCodexHome, "auth.json"), link);
|
|
51
|
+
const cfgPath = path.join(codexHome, "config.toml");
|
|
52
|
+
await fs.writeFile(cfgPath, renderCodexConfig(opts.mcp), { mode: 384 });
|
|
53
|
+
await fs.chmod(cfgPath, 384);
|
|
54
|
+
const workDir = await fs.mkdtemp(path.join(os.tmpdir(), "nema-run-"));
|
|
55
|
+
const tmpDir = path.join(workDir, ".tmp");
|
|
56
|
+
await fs.mkdir(tmpDir, { recursive: true, mode: 448 });
|
|
57
|
+
return { wsHome, codexHome, workDir, tmpDir };
|
|
58
|
+
}
|
|
59
|
+
function buildEnv(paths, parentEnv) {
|
|
60
|
+
const env = {};
|
|
61
|
+
for (const k of ["PATH", "LANG", "LC_ALL", "TERM"]) {
|
|
62
|
+
const v = parentEnv[k];
|
|
63
|
+
if (v !== void 0)
|
|
64
|
+
env[k] = v;
|
|
65
|
+
}
|
|
66
|
+
env.HOME = paths.wsHome;
|
|
67
|
+
env.CODEX_HOME = paths.codexHome;
|
|
68
|
+
env.TMPDIR = paths.tmpDir;
|
|
69
|
+
return env;
|
|
70
|
+
}
|
|
71
|
+
function preflight(paths) {
|
|
72
|
+
const problems = [];
|
|
73
|
+
for (const f of ["AGENTS.md", "AGENTS.override.md", "instructions.md"]) {
|
|
74
|
+
if (existsSync(path.join(paths.codexHome, f)))
|
|
75
|
+
problems.push(`codexHome \u4E2D\u5B58\u5728\u4E0D\u5E94\u6709\u7684 ${f}`);
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const cfg = parseToml(readFileSync(path.join(paths.codexHome, "config.toml"), "utf8"));
|
|
79
|
+
const names = Object.keys(cfg.mcp_servers ?? {});
|
|
80
|
+
if (names.length !== 1 || names[0] !== "room") {
|
|
81
|
+
problems.push(`config.toml \u7684 mcp_servers \u5FC5\u987B\u6070\u597D\u53EA\u6709 room\uFF0C\u5B9E\u9645\u4E3A [${names.join(", ")}]`);
|
|
82
|
+
}
|
|
83
|
+
} catch (e) {
|
|
84
|
+
problems.push(`\u65E0\u6CD5\u8BFB\u53D6\u6216\u89E3\u6790 config.toml\uFF1A${e instanceof Error ? e.message : String(e)}`);
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
if (!lstatSync(path.join(paths.codexHome, "auth.json")).isSymbolicLink())
|
|
88
|
+
problems.push("auth.json \u4E0D\u662F\u7B26\u53F7\u94FE\u63A5");
|
|
89
|
+
} catch {
|
|
90
|
+
problems.push("auth.json \u4E0D\u5B58\u5728");
|
|
91
|
+
}
|
|
92
|
+
let dir = path.resolve(paths.workDir);
|
|
93
|
+
for (; ; ) {
|
|
94
|
+
for (const f of [".git", "AGENTS.md", "AGENTS.override.md"]) {
|
|
95
|
+
if (existsSync(path.join(dir, f)))
|
|
96
|
+
problems.push(`${dir} \u4E2D\u5B58\u5728 ${f}`);
|
|
97
|
+
}
|
|
98
|
+
const parent = path.dirname(dir);
|
|
99
|
+
if (parent === dir)
|
|
100
|
+
break;
|
|
101
|
+
dir = parent;
|
|
102
|
+
}
|
|
103
|
+
return problems.length ? { ok: false, problems } : { ok: true };
|
|
104
|
+
}
|
|
105
|
+
async function cleanupRun(paths) {
|
|
106
|
+
await fs.rm(paths.workDir, { recursive: true, force: true });
|
|
107
|
+
}
|
|
108
|
+
export {
|
|
109
|
+
buildEnv,
|
|
110
|
+
checkCodexAuth,
|
|
111
|
+
cleanupRun,
|
|
112
|
+
preflight,
|
|
113
|
+
prepareIsolation,
|
|
114
|
+
renderCodexConfig
|
|
115
|
+
};
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const DEFAULT_BYOK_BASE_URL = "https://api.openai.com/v1";
|
|
3
|
+
export declare function normalizeByokBaseUrl(raw: string): string;
|
|
4
|
+
declare const ConfigSchema: z.ZodObject<{
|
|
5
|
+
adapter: z.ZodEnum<{
|
|
6
|
+
"codex-direct": "codex-direct";
|
|
7
|
+
"claude-code": "claude-code";
|
|
8
|
+
}>;
|
|
9
|
+
model: z.ZodOptional<z.ZodString>;
|
|
10
|
+
reasoningEffort: z.ZodEnum<{
|
|
11
|
+
low: "low";
|
|
12
|
+
medium: "medium";
|
|
13
|
+
high: "high";
|
|
14
|
+
}>;
|
|
15
|
+
authMode: z.ZodEnum<{
|
|
16
|
+
siwc: "siwc";
|
|
17
|
+
compat: "compat";
|
|
18
|
+
byok: "byok";
|
|
19
|
+
}>;
|
|
20
|
+
byokBaseUrl: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
|
|
21
|
+
}, z.core.$strict>;
|
|
22
|
+
export type NemaConfig = z.infer<typeof ConfigSchema>;
|
|
23
|
+
/** The one default config: used by loadConfig and by the Runner when no config getter is given. */
|
|
24
|
+
export declare const DEFAULT_CONFIG: Readonly<NemaConfig>;
|
|
25
|
+
/** A user-facing Chinese message for an unreadable config.json (bad JSON or schema), without zod/stack noise. */
|
|
26
|
+
export declare function configErrorMessage(e: unknown): string;
|
|
27
|
+
export declare function loadConfig(nemaHome: string): NemaConfig;
|
|
28
|
+
export declare function saveConfig(nemaHome: string, patch: Partial<NemaConfig>): NemaConfig;
|
|
29
|
+
export {};
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// config.js
|
|
2
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { protectPrivatePath } from "./secrets/permissions.js";
|
|
6
|
+
var DEFAULT_BYOK_BASE_URL = "https://api.openai.com/v1";
|
|
7
|
+
var BYOK_BASE_URL_ERROR = "byokBaseUrl \u53EA\u5141\u8BB8 https\uFF0C\u6216 http://127.0.0.1 \u4E0E http://localhost\uFF08\u53EF\u5E26\u7AEF\u53E3\uFF09";
|
|
8
|
+
function normalizeByokBaseUrl(raw) {
|
|
9
|
+
let url;
|
|
10
|
+
try {
|
|
11
|
+
url = new URL(raw);
|
|
12
|
+
} catch {
|
|
13
|
+
throw new Error(BYOK_BASE_URL_ERROR);
|
|
14
|
+
}
|
|
15
|
+
const localHttp = url.protocol === "http:" && (url.hostname === "127.0.0.1" || url.hostname === "localhost");
|
|
16
|
+
if (url.protocol !== "https:" && !localHttp || url.username !== "" || url.password !== "" || raw.includes("?") || raw.includes("#") || raw !== raw.trim()) {
|
|
17
|
+
throw new Error(BYOK_BASE_URL_ERROR);
|
|
18
|
+
}
|
|
19
|
+
return raw.endsWith("/") ? raw.slice(0, -1) : raw;
|
|
20
|
+
}
|
|
21
|
+
var ConfigSchema = z.object({
|
|
22
|
+
adapter: z.enum(["codex-direct", "claude-code"]),
|
|
23
|
+
model: z.string().min(1).optional(),
|
|
24
|
+
reasoningEffort: z.enum(["low", "medium", "high"]),
|
|
25
|
+
authMode: z.enum(["siwc", "compat", "byok"]),
|
|
26
|
+
byokBaseUrl: z.string().transform(normalizeByokBaseUrl).optional()
|
|
27
|
+
}).strict();
|
|
28
|
+
var DEFAULT_CONFIG = Object.freeze({ adapter: "codex-direct", reasoningEffort: "medium", authMode: "siwc" });
|
|
29
|
+
function configErrorMessage(e) {
|
|
30
|
+
let detail;
|
|
31
|
+
if (e instanceof z.ZodError)
|
|
32
|
+
detail = e.issues.map((i) => `${i.path.join(".") || "(\u6839)"}\uFF1A${i.message}`).join("\uFF1B");
|
|
33
|
+
else if (e instanceof SyntaxError)
|
|
34
|
+
detail = `\u4E0D\u662F\u5408\u6CD5\u7684 JSON\uFF08${e.message}\uFF09`;
|
|
35
|
+
else
|
|
36
|
+
detail = e instanceof Error ? e.message : String(e);
|
|
37
|
+
return `\u914D\u7F6E\u6587\u4EF6 ~/.nema/config.json \u65E0\u6CD5\u8BFB\u53D6\uFF1A${detail}`;
|
|
38
|
+
}
|
|
39
|
+
var configFile = (nemaHome) => path.join(nemaHome, "config.json");
|
|
40
|
+
function migrateRetiredCli(stored) {
|
|
41
|
+
if (stored.adapter !== "codex" && stored.adapter !== "codex-cli")
|
|
42
|
+
return stored;
|
|
43
|
+
return { ...stored, adapter: "codex-direct", authMode: "compat" };
|
|
44
|
+
}
|
|
45
|
+
function loadConfig(nemaHome) {
|
|
46
|
+
let raw;
|
|
47
|
+
try {
|
|
48
|
+
raw = readFileSync(configFile(nemaHome), "utf8");
|
|
49
|
+
} catch (e) {
|
|
50
|
+
if (e.code === "ENOENT")
|
|
51
|
+
return { ...DEFAULT_CONFIG };
|
|
52
|
+
throw e;
|
|
53
|
+
}
|
|
54
|
+
const stored = z.object({}).passthrough().parse(JSON.parse(raw));
|
|
55
|
+
return ConfigSchema.parse({ ...DEFAULT_CONFIG, ...migrateRetiredCli(stored) });
|
|
56
|
+
}
|
|
57
|
+
function saveConfig(nemaHome, patch) {
|
|
58
|
+
const next = ConfigSchema.parse({ ...loadConfig(nemaHome), ...patch });
|
|
59
|
+
mkdirSync(nemaHome, { recursive: true });
|
|
60
|
+
protectPrivatePath(nemaHome, 448);
|
|
61
|
+
const file = configFile(nemaHome);
|
|
62
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
63
|
+
writeFileSync(tmp, JSON.stringify(next, null, 2) + "\n", { mode: 384 });
|
|
64
|
+
protectPrivatePath(tmp);
|
|
65
|
+
renameSync(tmp, file);
|
|
66
|
+
return next;
|
|
67
|
+
}
|
|
68
|
+
export {
|
|
69
|
+
DEFAULT_BYOK_BASE_URL,
|
|
70
|
+
DEFAULT_CONFIG,
|
|
71
|
+
configErrorMessage,
|
|
72
|
+
loadConfig,
|
|
73
|
+
normalizeByokBaseUrl,
|
|
74
|
+
saveConfig
|
|
75
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Out } from "./direct/authCommands.js";
|
|
2
|
+
export declare const CONFIG_KEYS: readonly ["adapter", "model", "reasoningEffort", "authMode", "byokBaseUrl"];
|
|
3
|
+
export declare function configGet(o: {
|
|
4
|
+
nemaHome: string;
|
|
5
|
+
out: Out;
|
|
6
|
+
key?: string;
|
|
7
|
+
}): number;
|
|
8
|
+
export declare function configSet(o: {
|
|
9
|
+
nemaHome: string;
|
|
10
|
+
out: Out;
|
|
11
|
+
key: string;
|
|
12
|
+
value: string;
|
|
13
|
+
}): number;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// configCommands.js
|
|
2
|
+
import { configErrorMessage, loadConfig, saveConfig } from "./config.js";
|
|
3
|
+
var CONFIG_KEYS = ["adapter", "model", "reasoningEffort", "authMode", "byokBaseUrl"];
|
|
4
|
+
var ENUMS = {
|
|
5
|
+
adapter: ["codex-direct", "claude-code"],
|
|
6
|
+
reasoningEffort: ["low", "medium", "high"],
|
|
7
|
+
authMode: ["siwc", "compat", "byok"]
|
|
8
|
+
};
|
|
9
|
+
var isKey = (k) => CONFIG_KEYS.includes(k);
|
|
10
|
+
var unknownKey = (k) => `\u672A\u77E5\u914D\u7F6E\u9879 ${JSON.stringify(k)}\uFF1B\u53EF\u7528\uFF1A${CONFIG_KEYS.join("\u3001")}`;
|
|
11
|
+
var msg = (e) => e instanceof Error ? e.message : String(e);
|
|
12
|
+
function configGet(o) {
|
|
13
|
+
if (o.key !== void 0 && !isKey(o.key)) {
|
|
14
|
+
o.out.error(unknownKey(o.key));
|
|
15
|
+
return 1;
|
|
16
|
+
}
|
|
17
|
+
let config;
|
|
18
|
+
try {
|
|
19
|
+
config = loadConfig(o.nemaHome);
|
|
20
|
+
} catch (e) {
|
|
21
|
+
o.out.error(configErrorMessage(e));
|
|
22
|
+
return 1;
|
|
23
|
+
}
|
|
24
|
+
if (o.key !== void 0) {
|
|
25
|
+
o.out.log(String(config[o.key] ?? ""));
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
for (const k of CONFIG_KEYS)
|
|
29
|
+
o.out.log(`${k}=${config[k] ?? ""}`);
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
function configSet(o) {
|
|
33
|
+
if (!isKey(o.key)) {
|
|
34
|
+
o.out.error(unknownKey(o.key));
|
|
35
|
+
return 1;
|
|
36
|
+
}
|
|
37
|
+
if (o.key === "adapter" && (o.value === "codex-cli" || o.value === "codex")) {
|
|
38
|
+
o.out.error("codex-cli \u9002\u914D\u5668\u5DF2\u53D6\u6D88\uFF0C\u672C\u9879\u76EE\u4E0D\u518D\u542F\u52A8 Codex CLI\u3002\u8BF7\u4FDD\u6301 adapter=codex-direct\uFF0C\u5E76\u7528 nema config set authMode compat \u8BFB\u53D6\u5DF2\u6709\u7684 Codex \u767B\u5F55\u3002");
|
|
39
|
+
return 1;
|
|
40
|
+
}
|
|
41
|
+
const allowed = ENUMS[o.key];
|
|
42
|
+
if (allowed && !allowed.includes(o.value)) {
|
|
43
|
+
o.out.error(`${o.key} \u7684\u503C\u5FC5\u987B\u662F ${allowed.join(" | ")}\uFF08\u6536\u5230 ${JSON.stringify(o.value)}\uFF09`);
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
if (o.key === "model" && o.value.trim() === "") {
|
|
47
|
+
o.out.error("model \u4E0D\u80FD\u4E3A\u7A7A");
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
loadConfig(o.nemaHome);
|
|
52
|
+
} catch (e) {
|
|
53
|
+
o.out.error(configErrorMessage(e));
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
saveConfig(o.nemaHome, { [o.key]: o.value });
|
|
58
|
+
} catch (e) {
|
|
59
|
+
o.out.error(`\u65E0\u6CD5\u4FDD\u5B58\u914D\u7F6E\uFF1A${msg(e)}`);
|
|
60
|
+
return 1;
|
|
61
|
+
}
|
|
62
|
+
o.out.log(`\u5DF2\u8BBE\u7F6E ${o.key}=${o.value}`);
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
export {
|
|
66
|
+
CONFIG_KEYS,
|
|
67
|
+
configGet,
|
|
68
|
+
configSet
|
|
69
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const WORKSPACE_ID_RE: RegExp;
|
|
2
|
+
/** Removes `<nemaHome>/ws/<id>`. Returns whether something was removed; invalid ids are ignored. */
|
|
3
|
+
export declare function removeWorkspaceDir(nemaHome: string, workspaceId: string): Promise<boolean>;
|
|
4
|
+
/** Removes the isolation dirs of every workspace not in `existingIds`. Returns the removed ids. */
|
|
5
|
+
export declare function reconcileWorkspaceDirs(nemaHome: string, existingIds: Iterable<string>): Promise<string[]>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// daemon/cleanup.js
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
var WORKSPACE_ID_RE = /^ws_[A-Za-z0-9_-]{1,32}$/;
|
|
5
|
+
async function wsRoot(nemaHome) {
|
|
6
|
+
const root = path.join(nemaHome, "ws");
|
|
7
|
+
try {
|
|
8
|
+
const st = await fs.lstat(root);
|
|
9
|
+
return st.isDirectory() && !st.isSymbolicLink() ? root : null;
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
async function removeWorkspaceDir(nemaHome, workspaceId) {
|
|
15
|
+
if (!WORKSPACE_ID_RE.test(workspaceId))
|
|
16
|
+
return false;
|
|
17
|
+
const root = await wsRoot(nemaHome);
|
|
18
|
+
if (!root)
|
|
19
|
+
return false;
|
|
20
|
+
const target = path.join(root, workspaceId);
|
|
21
|
+
if (path.dirname(target) !== root)
|
|
22
|
+
return false;
|
|
23
|
+
try {
|
|
24
|
+
await fs.lstat(target);
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
await fs.rm(target, { recursive: true, force: true });
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
async function reconcileWorkspaceDirs(nemaHome, existingIds) {
|
|
32
|
+
const root = await wsRoot(nemaHome);
|
|
33
|
+
if (!root)
|
|
34
|
+
return [];
|
|
35
|
+
const keep = new Set(existingIds);
|
|
36
|
+
const removed = [];
|
|
37
|
+
for (const name of await fs.readdir(root)) {
|
|
38
|
+
if (keep.has(name) || !WORKSPACE_ID_RE.test(name))
|
|
39
|
+
continue;
|
|
40
|
+
if (await removeWorkspaceDir(nemaHome, name))
|
|
41
|
+
removed.push(name);
|
|
42
|
+
}
|
|
43
|
+
return removed;
|
|
44
|
+
}
|
|
45
|
+
export {
|
|
46
|
+
WORKSPACE_ID_RE,
|
|
47
|
+
reconcileWorkspaceDirs,
|
|
48
|
+
removeWorkspaceDir
|
|
49
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { RpcRouter } from "./rpc.js";
|
|
2
|
+
import type { Runner } from "./runner.js";
|
|
3
|
+
export type DaemonOpts = {
|
|
4
|
+
hubUrl: string;
|
|
5
|
+
token: string;
|
|
6
|
+
runner: Runner;
|
|
7
|
+
reconnectMs?: number;
|
|
8
|
+
/** `~/.nema`: when set, isolation dirs of deleted workspaces (`<nemaHome>/ws/<id>`) are removed. */
|
|
9
|
+
nemaHome?: string;
|
|
10
|
+
/** Whose `report-token` the hello registers; defaults to `nemaHome`. The desktop app points it at the user-level `~/.nema` its hooks use. */
|
|
11
|
+
reportTokenHome?: string;
|
|
12
|
+
/** Reported in `hello` (defaults to apps/cli/package.json's version). */
|
|
13
|
+
version?: string;
|
|
14
|
+
/** When set, `rpc.request` is answered immediately and does not wait for the task runner. */
|
|
15
|
+
rpc?: RpcRouter;
|
|
16
|
+
/** Called with the live socket send, and with null when that socket closes. */
|
|
17
|
+
bindSend?: (send: ((message: unknown) => boolean) | null) => void;
|
|
18
|
+
};
|
|
19
|
+
export declare const VERSION_SKEW_ERROR = "hub \u4E0E nema CLI \u7248\u672C\u4E0D\u4E00\u81F4\uFF0C\u8BF7\u540C\u65F6\u66F4\u65B0\uFF08\u91CD\u65B0\u6784\u5EFA compose \u5E76\u91CD\u542F nema daemon\uFF09";
|
|
20
|
+
export declare function connectDaemon(opts: DaemonOpts): {
|
|
21
|
+
close(): Promise<void>;
|
|
22
|
+
};
|