@nema-project/nema 0.0.0-stage → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/LICENSE +21 -0
  2. package/dist/chunks/chunk-HEIQINX3.js +825 -0
  3. package/dist/chunks/chunk-WT3XUFWU.js +86 -0
  4. package/dist/claude/adapter.d.ts +19 -0
  5. package/dist/claude/adapter.js +258 -0
  6. package/dist/claude/events.d.ts +29 -0
  7. package/dist/claude/events.js +75 -0
  8. package/dist/claude/launch.d.ts +22 -0
  9. package/dist/claude/launch.js +80 -0
  10. package/dist/claude/token.d.ts +9 -0
  11. package/dist/claude/token.js +87 -0
  12. package/dist/codex/adapter.d.ts +17 -0
  13. package/dist/codex/adapter.js +151 -0
  14. package/dist/codex/events.d.ts +16 -0
  15. package/dist/codex/events.js +47 -0
  16. package/dist/codex/isolation.d.ts +33 -0
  17. package/dist/codex/isolation.js +115 -0
  18. package/dist/config.d.ts +29 -0
  19. package/dist/config.js +75 -0
  20. package/dist/configCommands.d.ts +13 -0
  21. package/dist/configCommands.js +69 -0
  22. package/dist/daemon/cleanup.d.ts +5 -0
  23. package/dist/daemon/cleanup.js +49 -0
  24. package/dist/daemon/connection.d.ts +22 -0
  25. package/dist/daemon/connection.js +166 -0
  26. package/dist/daemon/rpc.d.ts +14 -0
  27. package/dist/daemon/rpc.js +20 -0
  28. package/dist/daemon/runner.d.ts +34 -0
  29. package/dist/daemon/runner.js +94 -0
  30. package/dist/daemon/setup.d.ts +39 -0
  31. package/dist/daemon/setup.js +95 -0
  32. package/dist/direct/adapter.d.ts +30 -0
  33. package/dist/direct/adapter.js +190 -0
  34. package/dist/direct/authCommands.d.ts +54 -0
  35. package/dist/direct/authCommands.js +268 -0
  36. package/dist/direct/authStore.d.ts +120 -0
  37. package/dist/direct/authStore.js +351 -0
  38. package/dist/direct/byok.d.ts +27 -0
  39. package/dist/direct/byok.js +67 -0
  40. package/dist/direct/codexLogin.d.ts +34 -0
  41. package/dist/direct/codexLogin.js +93 -0
  42. package/dist/direct/jwt.d.ts +4 -0
  43. package/dist/direct/jwt.js +22 -0
  44. package/dist/direct/lock.d.ts +16 -0
  45. package/dist/direct/lock.js +134 -0
  46. package/dist/direct/loop.d.ts +33 -0
  47. package/dist/direct/loop.js +110 -0
  48. package/dist/direct/siwc.d.ts +63 -0
  49. package/dist/direct/siwc.js +403 -0
  50. package/dist/direct/tools.d.ts +17 -0
  51. package/dist/direct/tools.js +104 -0
  52. package/dist/direct/transport.d.ts +177 -0
  53. package/dist/direct/transport.js +437 -0
  54. package/dist/doctor.d.ts +28 -0
  55. package/dist/doctor.js +178 -0
  56. package/dist/extensions/handlers.d.ts +2 -0
  57. package/dist/extensions/handlers.js +10 -0
  58. package/dist/extensions/store.d.ts +11 -0
  59. package/dist/extensions/store.js +132 -0
  60. package/dist/github/client.d.ts +47 -0
  61. package/dist/github/client.js +383 -0
  62. package/dist/github/gh.d.ts +56 -0
  63. package/dist/github/gh.js +385 -0
  64. package/dist/index.d.ts +2 -0
  65. package/dist/index.js +144 -0
  66. package/dist/localSettings.d.ts +8 -0
  67. package/dist/localSettings.js +26 -0
  68. package/dist/mcp/server.d.ts +5 -0
  69. package/dist/mcp/server.js +83 -0
  70. package/dist/ownedProcess.d.ts +39 -0
  71. package/dist/ownedProcess.js +229 -0
  72. package/dist/platform.d.ts +10 -0
  73. package/dist/platform.js +60 -0
  74. package/dist/report/commands.d.ts +41 -0
  75. package/dist/report/commands.js +159 -0
  76. package/dist/report/desktop.d.ts +3 -0
  77. package/dist/report/desktop.js +123 -0
  78. package/dist/report/git.d.ts +5 -0
  79. package/dist/report/git.js +42 -0
  80. package/dist/report/install.d.ts +22 -0
  81. package/dist/report/install.js +183 -0
  82. package/dist/report/send.d.ts +35 -0
  83. package/dist/report/send.js +208 -0
  84. package/dist/report/token.d.ts +5 -0
  85. package/dist/report/token.js +86 -0
  86. package/dist/repos/git.d.ts +48 -0
  87. package/dist/repos/git.js +370 -0
  88. package/dist/repos/graphify.d.ts +32 -0
  89. package/dist/repos/graphify.js +292 -0
  90. package/dist/repos/handlers.d.ts +20 -0
  91. package/dist/repos/handlers.js +255 -0
  92. package/dist/repos/indexState.d.ts +10 -0
  93. package/dist/repos/indexState.js +45 -0
  94. package/dist/repos/pickDirectory.d.ts +11 -0
  95. package/dist/repos/pickDirectory.js +140 -0
  96. package/dist/repos/reportCheck.d.ts +13 -0
  97. package/dist/repos/reportCheck.js +268 -0
  98. package/dist/repos/watcher.d.ts +64 -0
  99. package/dist/repos/watcher.js +202 -0
  100. package/dist/room/client.d.ts +26 -0
  101. package/dist/room/client.js +76 -0
  102. package/dist/secrets/file.d.ts +12 -0
  103. package/dist/secrets/file.js +89 -0
  104. package/dist/secrets/github.d.ts +10 -0
  105. package/dist/secrets/github.js +29 -0
  106. package/dist/secrets/permissions.d.ts +7 -0
  107. package/dist/secrets/permissions.js +86 -0
  108. package/dist/systemGit.d.ts +14 -0
  109. package/dist/systemGit.js +75 -0
  110. package/dist/wp/commands.d.ts +15 -0
  111. package/dist/wp/commands.js +103 -0
  112. package/package.json +26 -4
  113. package/src/report/SKILL.md +21 -0
  114. package/README.md +0 -3
@@ -0,0 +1,268 @@
1
+ import {
2
+ runHelperFile
3
+ } from "../chunks/chunk-WT3XUFWU.js";
4
+
5
+ // repos/reportCheck.js
6
+ import { spawn } from "node:child_process";
7
+ import path from "node:path";
8
+ import { commandEnv, gitRaw, gitText } from "./git.js";
9
+ var SHA_RE = /^[0-9a-f]{7,40}$/;
10
+ var EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
11
+ var DIFF_OPTS = ["--no-color", "--no-ext-diff", "--no-renames", "--no-textconv"];
12
+ function assertSha(sha) {
13
+ if (!SHA_RE.test(sha))
14
+ throw new Error(`\u65E0\u6548\u7684\u63D0\u4EA4 ${sha}`);
15
+ }
16
+ function assertRef(ref) {
17
+ if (!ref || ref.startsWith("-"))
18
+ throw new Error(`\u65E0\u6548\u7684\u5F15\u7528 ${ref}`);
19
+ }
20
+ async function resolveCommit(repoPath, sha, helpers) {
21
+ try {
22
+ await gitText(repoPath, ["cat-file", "-e", `${sha}^{commit}`], void 0, helpers);
23
+ return await gitText(repoPath, ["rev-parse", "--verify", `${sha}^{commit}`], void 0, helpers);
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+ async function isAncestor(repoPath, sha, ref, helpers) {
29
+ try {
30
+ await gitText(repoPath, ["merge-base", "--is-ancestor", sha, ref], void 0, helpers);
31
+ return true;
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+ function patchIds(repoPath, input, helpers) {
37
+ if (input.length === 0)
38
+ return Promise.resolve([]);
39
+ if (helpers)
40
+ return runHelperFile("git", ["-C", repoPath, "patch-id", "--stable"], {
41
+ env: commandEnv(),
42
+ timeout: 6e4,
43
+ maxBuffer: 64 * 1024 * 1024,
44
+ encoding: "buffer",
45
+ input
46
+ }, helpers).then((result) => {
47
+ if (result.error)
48
+ throw new Error(result.stderr.toString("utf8").trim() || `git patch-id exited ${result.code}`);
49
+ return parsePatchIds(result.stdout);
50
+ });
51
+ return new Promise((resolve, reject) => {
52
+ const child = spawn("git", ["-C", repoPath, "patch-id", "--stable"], { env: commandEnv(), shell: false, stdio: ["pipe", "pipe", "pipe"] });
53
+ const out = [];
54
+ const err = [];
55
+ const timer = setTimeout(() => child.kill("SIGKILL"), 6e4);
56
+ child.stdout.on("data", (b) => out.push(b));
57
+ child.stderr.on("data", (b) => err.push(b));
58
+ child.on("error", (e) => {
59
+ clearTimeout(timer);
60
+ reject(e);
61
+ });
62
+ child.on("close", (code) => {
63
+ clearTimeout(timer);
64
+ if (code !== 0) {
65
+ reject(new Error(Buffer.concat(err).toString("utf8").trim() || `git patch-id exited ${code}`));
66
+ return;
67
+ }
68
+ resolve(parsePatchIds(Buffer.concat(out)));
69
+ });
70
+ child.stdin.on("error", () => {
71
+ });
72
+ child.stdin.end(input);
73
+ });
74
+ }
75
+ function parsePatchIds(output) {
76
+ const rows = [];
77
+ for (const line of output.toString("utf8").split("\n")) {
78
+ const [patchId, commit] = line.trim().split(/\s+/);
79
+ if (patchId && commit)
80
+ rows.push({ patchId, commit });
81
+ }
82
+ return rows;
83
+ }
84
+ async function combinedPatchId(repoPath, oldest, newest, helpers) {
85
+ const parent = await gitText(repoPath, ["rev-parse", "--verify", "--quiet", `${oldest}^`], void 0, helpers).catch(() => EMPTY_TREE);
86
+ const diff = await gitRaw(repoPath, ["diff", ...DIFF_OPTS, parent, newest, "--"], void 0, helpers);
87
+ return (await patchIds(repoPath, diff, helpers))[0]?.patchId ?? null;
88
+ }
89
+ async function logPatchIds(repoPath, args, helpers) {
90
+ const log = await gitRaw(repoPath, ["log", "-p", ...DIFF_OPTS, "--no-merges", "--format=commit %H", ...args, "--"], 12e4, helpers);
91
+ const byCommit = /* @__PURE__ */ new Map();
92
+ for (const row of await patchIds(repoPath, log, helpers))
93
+ byCommit.set(row.commit, row.patchId);
94
+ return byCommit;
95
+ }
96
+ async function chainEnds(repoPath, shas, helpers) {
97
+ let oldest = shas[0];
98
+ let newest = shas[shas.length - 1];
99
+ for (const a of shas) {
100
+ let isOldest = true;
101
+ let isNewest = true;
102
+ for (const b of shas) {
103
+ if (a === b)
104
+ continue;
105
+ if (isOldest && !await isAncestor(repoPath, a, b, helpers))
106
+ isOldest = false;
107
+ if (isNewest && !await isAncestor(repoPath, b, a, helpers))
108
+ isNewest = false;
109
+ }
110
+ if (isOldest)
111
+ oldest = a;
112
+ if (isNewest)
113
+ newest = a;
114
+ }
115
+ return [oldest, newest];
116
+ }
117
+ async function parentsOf(repoPath, shas, helpers) {
118
+ const parents = /* @__PURE__ */ new Map();
119
+ if (shas.length === 0)
120
+ return parents;
121
+ const text = await gitText(repoPath, ["rev-list", "--no-walk=unsorted", "--parents", ...shas, "--"], void 0, helpers);
122
+ for (const line of text.split("\n")) {
123
+ const [sha, ...rest] = line.trim().split(/\s+/);
124
+ if (sha)
125
+ parents.set(sha, rest);
126
+ }
127
+ return parents;
128
+ }
129
+ function normalizeClaim(file) {
130
+ return path.posix.normalize(file.replace(/\\/g, "/")).replace(/^\.\//, "").replace(/\/$/, "");
131
+ }
132
+ async function checkReport(repoPath, mainRefName, commits, files, helpers) {
133
+ assertRef(mainRefName);
134
+ for (const sha of commits)
135
+ assertSha(sha);
136
+ const mainSha = await gitText(repoPath, ["rev-parse", "--verify", `${mainRefName}^{commit}`], void 0, helpers);
137
+ const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
138
+ const resolved = [];
139
+ const missingCommits = [];
140
+ for (const sha of commits) {
141
+ const full = await resolveCommit(repoPath, sha, helpers);
142
+ if (full === null)
143
+ missingCommits.push(sha);
144
+ else if (!resolved.includes(full))
145
+ resolved.push(full);
146
+ }
147
+ const commitsExist = missingCommits.length === 0 && resolved.length > 0;
148
+ let onMain = "not-yet";
149
+ if (commitsExist) {
150
+ let all = true;
151
+ for (const sha of resolved) {
152
+ if (!await isAncestor(repoPath, sha, mainSha, helpers)) {
153
+ all = false;
154
+ break;
155
+ }
156
+ }
157
+ if (all)
158
+ onMain = "yes";
159
+ else {
160
+ const [oldest, newest] = await chainEnds(repoPath, resolved, helpers);
161
+ const combined = await combinedPatchId(repoPath, oldest, newest, helpers);
162
+ const base = await gitText(repoPath, ["merge-base", resolved[0], mainSha], void 0, helpers).catch(() => null);
163
+ if (combined !== null && base !== null) {
164
+ const mainIds = new Set((await logPatchIds(repoPath, [`${base}..${mainSha}`], helpers)).values());
165
+ if (mainIds.has(combined))
166
+ onMain = "yes";
167
+ else {
168
+ const parents2 = await parentsOf(repoPath, resolved, helpers);
169
+ const own = await logPatchIds(repoPath, ["--no-walk=unsorted", ...resolved], helpers);
170
+ let every = true;
171
+ let matched = 0;
172
+ for (const sha of resolved) {
173
+ if ((parents2.get(sha)?.length ?? 0) > 1)
174
+ continue;
175
+ if (await isAncestor(repoPath, sha, mainSha, helpers))
176
+ continue;
177
+ const id = own.get(sha);
178
+ if (id === void 0)
179
+ continue;
180
+ if (!mainIds.has(id)) {
181
+ every = false;
182
+ break;
183
+ }
184
+ matched++;
185
+ }
186
+ if (every && matched > 0)
187
+ onMain = "yes";
188
+ }
189
+ }
190
+ }
191
+ }
192
+ const touched = /* @__PURE__ */ new Set();
193
+ const parents = await parentsOf(repoPath, resolved, helpers);
194
+ for (const sha of resolved) {
195
+ if ((parents.get(sha)?.length ?? 0) > 1)
196
+ continue;
197
+ const out = await gitRaw(repoPath, ["diff-tree", "--no-commit-id", "--name-only", "-r", "-M", "--root", "-z", sha, "--"], void 0, helpers);
198
+ for (const name of out.toString("utf8").split("\0"))
199
+ if (name)
200
+ touched.add(name);
201
+ }
202
+ const untouchedFiles = files.filter((file) => !touched.has(normalizeClaim(file)));
203
+ return { commitsExist, missingCommits, onMain, filesOk: untouchedFiles.length === 0, untouchedFiles, touchedFiles: [...touched], checkedAt, mainSha };
204
+ }
205
+ async function uncoveredCommits(repoPath, mainRefName, from, to, covered, helpers) {
206
+ assertRef(mainRefName);
207
+ for (const rev of [from, to]) {
208
+ if (rev === mainRefName)
209
+ continue;
210
+ assertSha(rev);
211
+ if (!await isAncestor(repoPath, rev, mainRefName, helpers))
212
+ throw new Error(`\u63D0\u4EA4 ${rev} \u4E0D\u5728\u4E3B\u5206\u652F\u4E0A`);
213
+ }
214
+ for (const sha of covered)
215
+ assertSha(sha);
216
+ const listed = await gitText(repoPath, ["rev-list", "--no-merges", "--reverse", `${from}..${to}`, "--"], void 0, helpers);
217
+ const candidates = listed.split("\n").filter(Boolean);
218
+ if (candidates.length === 0)
219
+ return [];
220
+ const coveredSet = /* @__PURE__ */ new Set();
221
+ for (const sha of covered) {
222
+ const full = await resolveCommit(repoPath, sha, helpers);
223
+ if (full !== null)
224
+ coveredSet.add(full);
225
+ }
226
+ const coveredList = [...coveredSet];
227
+ const coveredIds = /* @__PURE__ */ new Set();
228
+ if (coveredList.length > 0) {
229
+ for (const id of (await logPatchIds(repoPath, ["--no-walk=unsorted", ...coveredList], helpers)).values())
230
+ coveredIds.add(id);
231
+ const offMain = [];
232
+ for (const sha of coveredList)
233
+ if (!await isAncestor(repoPath, sha, to, helpers))
234
+ offMain.push(sha);
235
+ const parents = await parentsOf(repoPath, offMain, helpers);
236
+ const offSet = new Set(offMain.filter((sha) => (parents.get(sha)?.length ?? 0) <= 1));
237
+ const childOf = /* @__PURE__ */ new Map();
238
+ for (const sha of offSet) {
239
+ const parent = parents.get(sha)?.[0];
240
+ if (parent && offSet.has(parent))
241
+ childOf.set(parent, sha);
242
+ }
243
+ for (const start of offSet) {
244
+ const parent = parents.get(start)?.[0];
245
+ if (parent && offSet.has(parent))
246
+ continue;
247
+ let end = start;
248
+ while (childOf.has(end))
249
+ end = childOf.get(end);
250
+ if (end === start)
251
+ continue;
252
+ const combined = await combinedPatchId(repoPath, start, end, helpers);
253
+ if (combined !== null)
254
+ coveredIds.add(combined);
255
+ }
256
+ }
257
+ const idOf = await logPatchIds(repoPath, [`${from}..${to}`], helpers);
258
+ return candidates.filter((sha) => {
259
+ if (coveredSet.has(sha))
260
+ return false;
261
+ const id = idOf.get(sha);
262
+ return id === void 0 || !coveredIds.has(id);
263
+ });
264
+ }
265
+ export {
266
+ checkReport,
267
+ uncoveredCommits
268
+ };
@@ -0,0 +1,64 @@
1
+ import type { GraphifyOpts } from "./graphify.js";
2
+ export type WatchedRepo = {
3
+ workspaceId: string;
4
+ repoId: string;
5
+ path: string;
6
+ mainBranch: string;
7
+ lastMainSha: string | null;
8
+ };
9
+ export type RepoMoved = {
10
+ repoId: string;
11
+ mainRef: string;
12
+ from: string | null;
13
+ to: string;
14
+ };
15
+ export type RepoFetched = {
16
+ repoId: string;
17
+ ok: boolean;
18
+ error?: string;
19
+ at: string;
20
+ };
21
+ export declare class RepoWatcher {
22
+ private readonly nemaHome;
23
+ private readonly intervalMs;
24
+ private readonly onMoved;
25
+ private readonly onFetch;
26
+ private readonly graphify;
27
+ private repos;
28
+ /** Last sha this process has recorded. `null` means the first tick still has to baseline. */
29
+ private readonly known;
30
+ private timer;
31
+ private inflight;
32
+ /** Tasks that already refreshed a repo. Capped so a long-lived daemon does not keep every id. */
33
+ private readonly prepared;
34
+ /** Full ticks and single-repo ticks run one at a time, so one move is never reported twice. */
35
+ private queue;
36
+ private stopped;
37
+ private stopping?;
38
+ constructor(opts: {
39
+ nemaHome: string;
40
+ intervalMs?: number;
41
+ /** `false` means the move was not sent. The sha stays put and the next tick retries. */
42
+ onMoved: (move: RepoMoved) => boolean | void;
43
+ onFetch: (report: RepoFetched) => void;
44
+ /** For tests: production passes nothing and runs `graphify` from PATH. */
45
+ graphify?: GraphifyOpts;
46
+ });
47
+ setRepos(list: WatchedRepo[]): void;
48
+ private serial;
49
+ tick(): Promise<void>;
50
+ /**
51
+ * Fetches one watched repo. The same task id does this once; a missing task id fetches every call.
52
+ * An unknown repo id is a no-op.
53
+ */
54
+ prepare(repoId: string, taskId?: string): Promise<void>;
55
+ /** Ticks one watched repo now (a coding agent's hook fired). An unknown repo id is a no-op. */
56
+ tickRepo(repoId: string): Promise<void>;
57
+ private runTick;
58
+ /** False once `setRepos` dropped this repo, for example after an unbind while its tick ran. */
59
+ private isWatched;
60
+ private tickOne;
61
+ start(): void;
62
+ stop(): Promise<void>;
63
+ }
64
+ export declare function parseReposSync(params: unknown): WatchedRepo[];
@@ -0,0 +1,202 @@
1
+ // repos/watcher.js
2
+ import { removeSnapshot, snapshotDir } from "./handlers.js";
3
+ import { fetchMain, mainRef, revParse } from "./git.js";
4
+ import { syncIndex, withIndexLock } from "./indexState.js";
5
+ var DEFAULT_INTERVAL_MS = 3e5;
6
+ var RepoWatcher = class {
7
+ nemaHome;
8
+ intervalMs;
9
+ onMoved;
10
+ onFetch;
11
+ graphify;
12
+ repos = [];
13
+ /** Last sha this process has recorded. `null` means the first tick still has to baseline. */
14
+ known = /* @__PURE__ */ new Map();
15
+ timer = null;
16
+ inflight = null;
17
+ /** Tasks that already refreshed a repo. Capped so a long-lived daemon does not keep every id. */
18
+ prepared = /* @__PURE__ */ new Set();
19
+ /** Full ticks and single-repo ticks run one at a time, so one move is never reported twice. */
20
+ queue = Promise.resolve();
21
+ stopped = false;
22
+ stopping;
23
+ constructor(opts) {
24
+ this.graphify = opts.graphify;
25
+ this.nemaHome = opts.nemaHome;
26
+ this.intervalMs = opts.intervalMs ?? DEFAULT_INTERVAL_MS;
27
+ this.onMoved = opts.onMoved;
28
+ this.onFetch = opts.onFetch;
29
+ }
30
+ setRepos(list) {
31
+ if (this.stopped)
32
+ return;
33
+ const keep = new Set(list.map((repo) => repo.repoId));
34
+ for (const id of this.known.keys()) {
35
+ if (!keep.has(id))
36
+ this.known.delete(id);
37
+ }
38
+ for (const repo of list) {
39
+ const recorded = this.known.get(repo.repoId);
40
+ this.known.set(repo.repoId, repo.lastMainSha === null && recorded !== void 0 ? recorded : repo.lastMainSha);
41
+ }
42
+ this.repos = list;
43
+ }
44
+ serial(fn) {
45
+ const run = this.queue.then(fn);
46
+ this.queue = run.catch(() => {
47
+ });
48
+ return run;
49
+ }
50
+ tick() {
51
+ if (this.stopped)
52
+ return this.stopping ?? Promise.resolve();
53
+ if (this.inflight)
54
+ return this.inflight;
55
+ const run = this.serial(() => this.runTick()).finally(() => {
56
+ this.inflight = null;
57
+ });
58
+ this.inflight = run;
59
+ return run;
60
+ }
61
+ /**
62
+ * Fetches one watched repo. The same task id does this once; a missing task id fetches every call.
63
+ * An unknown repo id is a no-op.
64
+ */
65
+ prepare(repoId, taskId) {
66
+ if (!taskId)
67
+ return this.tickRepo(repoId);
68
+ const key = `${taskId}\0${repoId}`;
69
+ if (this.prepared.has(key))
70
+ return Promise.resolve();
71
+ return this.serial(async () => {
72
+ if (this.stopped || this.prepared.has(key))
73
+ return;
74
+ const repo = this.repos.find((row) => row.repoId === repoId);
75
+ if (!repo || !this.isWatched(repo))
76
+ return;
77
+ try {
78
+ await this.tickOne(repo);
79
+ } catch (error) {
80
+ console.error(`[nema] repo watch failed for ${repo.repoId}`, error);
81
+ return;
82
+ }
83
+ if (this.stopped || !this.isWatched(repo))
84
+ return;
85
+ this.prepared.add(key);
86
+ if (this.prepared.size > 500)
87
+ this.prepared.clear();
88
+ });
89
+ }
90
+ /** Ticks one watched repo now (a coding agent's hook fired). An unknown repo id is a no-op. */
91
+ tickRepo(repoId) {
92
+ if (this.stopped)
93
+ return this.stopping ?? Promise.resolve();
94
+ return this.serial(async () => {
95
+ if (this.stopped)
96
+ return;
97
+ const repo = this.repos.find((row) => row.repoId === repoId);
98
+ if (!repo)
99
+ return;
100
+ try {
101
+ await this.tickOne(repo);
102
+ } catch (error) {
103
+ console.error(`[nema] repo watch failed for ${repo.repoId}`, error);
104
+ }
105
+ });
106
+ }
107
+ async runTick() {
108
+ for (const repo of this.repos) {
109
+ if (this.stopped)
110
+ return;
111
+ try {
112
+ await this.tickOne(repo);
113
+ } catch (error) {
114
+ console.error(`[nema] repo watch failed for ${repo.repoId}`, error);
115
+ }
116
+ }
117
+ }
118
+ /** False once `setRepos` dropped this repo, for example after an unbind while its tick ran. */
119
+ isWatched(repo) {
120
+ return this.repos.some((row) => row.repoId === repo.repoId && row.workspaceId === repo.workspaceId);
121
+ }
122
+ async tickOne(repo) {
123
+ if (this.stopped || !this.isWatched(repo))
124
+ return;
125
+ const fetched = await fetchMain(repo.path, repo.mainBranch, this.graphify?.ownedHelpers);
126
+ if (this.stopped || !this.isWatched(repo))
127
+ return;
128
+ const at = (/* @__PURE__ */ new Date()).toISOString();
129
+ if (fetched.ok)
130
+ this.onFetch({ repoId: repo.repoId, ok: true, at });
131
+ else
132
+ this.onFetch({ repoId: repo.repoId, ok: false, error: fetched.error, at });
133
+ const ref = await mainRef(repo.path, repo.mainBranch, this.graphify?.ownedHelpers);
134
+ const sha = await revParse(repo.path, ref, this.graphify?.ownedHelpers);
135
+ const root = snapshotDir(this.nemaHome, repo.workspaceId, repo.repoId);
136
+ try {
137
+ await withIndexLock(root, () => syncIndex(root, repo.path, sha, this.graphify));
138
+ } catch (error) {
139
+ console.error(`[nema] repo index failed for ${repo.repoId}`, error);
140
+ }
141
+ if (this.stopped)
142
+ return;
143
+ if (!this.isWatched(repo)) {
144
+ await withIndexLock(root, () => removeSnapshot(root));
145
+ return;
146
+ }
147
+ const last = this.known.get(repo.repoId) ?? null;
148
+ if (last === null) {
149
+ this.known.set(repo.repoId, sha);
150
+ return;
151
+ }
152
+ if (sha === last)
153
+ return;
154
+ if (this.onMoved({ repoId: repo.repoId, mainRef: ref, from: last, to: sha }) === false)
155
+ return;
156
+ this.known.set(repo.repoId, sha);
157
+ }
158
+ start() {
159
+ if (this.stopped || this.intervalMs < 0)
160
+ return;
161
+ if (this.timer)
162
+ clearInterval(this.timer);
163
+ this.timer = null;
164
+ }
165
+ stop() {
166
+ if (this.stopping)
167
+ return this.stopping;
168
+ this.stopped = true;
169
+ if (this.timer)
170
+ clearInterval(this.timer);
171
+ this.timer = null;
172
+ this.stopping = Promise.all([this.inflight ?? Promise.resolve(), this.queue]).then(() => {
173
+ });
174
+ return this.stopping;
175
+ }
176
+ };
177
+ function parseReposSync(params) {
178
+ if (!params || typeof params !== "object" || !Array.isArray(params.repos)) {
179
+ throw new Error("invalid repo params");
180
+ }
181
+ return params.repos.map((row) => {
182
+ if (!row || typeof row !== "object")
183
+ throw new Error("invalid repo params");
184
+ const repo = row;
185
+ const workspaceId = repo.workspaceId;
186
+ const repoId = repo.repoId;
187
+ const repoPath = repo.path;
188
+ const mainBranch = repo.mainBranch;
189
+ const lastMainSha = repo.lastMainSha;
190
+ if (typeof workspaceId !== "string" || typeof repoId !== "string" || typeof repoPath !== "string" || typeof mainBranch !== "string" || !workspaceId || !repoId || !repoPath || !mainBranch) {
191
+ throw new Error("invalid repo params");
192
+ }
193
+ if (!(lastMainSha === null || typeof lastMainSha === "string" && lastMainSha !== "")) {
194
+ throw new Error("invalid repo params");
195
+ }
196
+ return { workspaceId, repoId, path: repoPath, mainBranch, lastMainSha };
197
+ });
198
+ }
199
+ export {
200
+ RepoWatcher,
201
+ parseReposSync
202
+ };
@@ -0,0 +1,26 @@
1
+ /** Tool output returned to the model is capped at this many UTF-8 bytes (marker included). */
2
+ export declare const MAX_TOOL_OUTPUT_BYTES: number;
3
+ /** Caps `text` at `maxBytes` UTF-8 bytes (never splitting a character), appending a truncation marker. */
4
+ export declare function capText(text: string, maxBytes?: number): string;
5
+ export type RoomResult = {
6
+ ok: true;
7
+ json: unknown;
8
+ } | {
9
+ ok: false;
10
+ status: number;
11
+ error: string;
12
+ };
13
+ export declare class RoomClient {
14
+ private readonly runToken;
15
+ private readonly signal?;
16
+ private readonly base;
17
+ /** `signal` (optional) aborts in-flight requests, e.g. when the run times out. */
18
+ constructor(hubUrl: string, runToken: string, signal?: AbortSignal | undefined);
19
+ listEntries(): Promise<RoomResult>;
20
+ readEntry(id: string): Promise<RoomResult>;
21
+ upsertEntry(input: unknown): Promise<RoomResult>;
22
+ openThread(args: unknown): Promise<RoomResult>;
23
+ listPluginTools(): Promise<RoomResult>;
24
+ callPlugin(name: string, args: unknown): Promise<RoomResult>;
25
+ private call;
26
+ }
@@ -0,0 +1,76 @@
1
+ // room/client.js
2
+ var MAX_TOOL_OUTPUT_BYTES = 64 * 1024;
3
+ function capText(text, maxBytes = MAX_TOOL_OUTPUT_BYTES) {
4
+ const buf = Buffer.from(text, "utf8");
5
+ if (buf.length <= maxBytes)
6
+ return text;
7
+ const marker = `\u2026[\u5DF2\u622A\u65AD\uFF1A\u539F\u59CB ${buf.length} \u5B57\u8282\uFF0C\u4EC5\u4FDD\u7559\u524D 64KB]`;
8
+ let end = Math.max(0, maxBytes - Buffer.byteLength(marker));
9
+ while (end > 0 && (buf[end] & 192) === 128)
10
+ end--;
11
+ return buf.subarray(0, end).toString("utf8") + marker;
12
+ }
13
+ var RoomClient = class {
14
+ runToken;
15
+ signal;
16
+ base;
17
+ /** `signal` (optional) aborts in-flight requests, e.g. when the run times out. */
18
+ constructor(hubUrl, runToken, signal) {
19
+ this.runToken = runToken;
20
+ this.signal = signal;
21
+ this.base = hubUrl.replace(/\/+$/, "");
22
+ }
23
+ listEntries() {
24
+ return this.call("GET", "/entries");
25
+ }
26
+ readEntry(id) {
27
+ return this.call("GET", `/entries/${encodeURIComponent(id)}`);
28
+ }
29
+ upsertEntry(input) {
30
+ return this.call("POST", "/entries", input);
31
+ }
32
+ openThread(args) {
33
+ return this.call("POST", "/threads", args);
34
+ }
35
+ listPluginTools() {
36
+ return this.call("GET", "/plugins");
37
+ }
38
+ callPlugin(name, args) {
39
+ return this.call("POST", `/plugins/${encodeURIComponent(name)}`, args);
40
+ }
41
+ async call(method, path, body) {
42
+ let res;
43
+ let text;
44
+ try {
45
+ res = await fetch(`${this.base}/room${path}`, {
46
+ method,
47
+ headers: {
48
+ authorization: `Bearer ${this.runToken}`,
49
+ ...body !== void 0 ? { "content-type": "application/json" } : {}
50
+ },
51
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
52
+ signal: this.signal
53
+ });
54
+ text = await res.text();
55
+ } catch (e) {
56
+ return { ok: false, status: 0, error: `\u65E0\u6CD5\u8FDE\u63A5 hub\uFF1A${e instanceof Error ? e.message : String(e)}` };
57
+ }
58
+ let parsed;
59
+ let isJson = false;
60
+ try {
61
+ parsed = JSON.parse(text);
62
+ isJson = true;
63
+ } catch {
64
+ }
65
+ if (!res.ok) {
66
+ const err = isJson && parsed && typeof parsed === "object" ? parsed.error : void 0;
67
+ return { ok: false, status: res.status, error: capText(typeof err === "string" ? err : text || `hub returned HTTP ${res.status}`) };
68
+ }
69
+ return { ok: true, json: isJson ? parsed : capText(text) };
70
+ }
71
+ };
72
+ export {
73
+ MAX_TOOL_OUTPUT_BYTES,
74
+ RoomClient,
75
+ capText
76
+ };
@@ -0,0 +1,12 @@
1
+ export type SecretFileErrorKind = "invalid" | "symlink" | "mode";
2
+ /** Errors from the secret-file helpers. Messages name the file, never its contents. */
3
+ export declare class SecretFileError extends Error {
4
+ readonly kind: SecretFileErrorKind;
5
+ constructor(kind: SecretFileErrorKind, message: string);
6
+ }
7
+ /** Atomically writes one line to `file` with mode 0600. Refuses a symlink; on reject the existing file is untouched. */
8
+ export declare function saveSecretLine(file: string, value: string, dirMode?: number): void;
9
+ /** Reads one line from a 0600 regular file without following symlinks. The value is never echoed in errors. */
10
+ export declare function readSecretLine(file: string, missingMessage: string, invalidMessage: string): string;
11
+ /** Reads the first line of stdin, trimmed. */
12
+ export declare function readStdinLine(stdin: NodeJS.ReadableStream): Promise<string>;