me2lon 0.0.0-stage → 0.1.0

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 (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -2
  3. package/dist/api.d.ts +35 -0
  4. package/dist/api.d.ts.map +1 -0
  5. package/dist/api.js +286 -0
  6. package/dist/api.js.map +1 -0
  7. package/dist/cli.d.ts +4 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +261 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/client.d.ts +23 -0
  12. package/dist/client.d.ts.map +1 -0
  13. package/dist/client.js +272 -0
  14. package/dist/client.js.map +1 -0
  15. package/dist/config.d.ts +19 -0
  16. package/dist/config.d.ts.map +1 -0
  17. package/dist/config.js +173 -0
  18. package/dist/config.js.map +1 -0
  19. package/dist/errors.d.ts +14 -0
  20. package/dist/errors.d.ts.map +1 -0
  21. package/dist/errors.js +31 -0
  22. package/dist/errors.js.map +1 -0
  23. package/dist/index.d.ts +8 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +8 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/presentation.d.ts +12 -0
  28. package/dist/presentation.d.ts.map +1 -0
  29. package/dist/presentation.js +50 -0
  30. package/dist/presentation.js.map +1 -0
  31. package/dist/safe-output.d.ts +20 -0
  32. package/dist/safe-output.d.ts.map +1 -0
  33. package/dist/safe-output.js +770 -0
  34. package/dist/safe-output.js.map +1 -0
  35. package/dist/setup.d.ts +14 -0
  36. package/dist/setup.d.ts.map +1 -0
  37. package/dist/setup.js +92 -0
  38. package/dist/setup.js.map +1 -0
  39. package/dist/skill.d.ts +2 -0
  40. package/dist/skill.d.ts.map +1 -0
  41. package/dist/skill.js +30 -0
  42. package/dist/skill.js.map +1 -0
  43. package/dist/types.d.ts +76 -0
  44. package/dist/types.d.ts.map +1 -0
  45. package/dist/types.js +8 -0
  46. package/dist/types.js.map +1 -0
  47. package/dist/workspace-source.d.ts +11 -0
  48. package/dist/workspace-source.d.ts.map +1 -0
  49. package/dist/workspace-source.js +387 -0
  50. package/dist/workspace-source.js.map +1 -0
  51. package/package.json +43 -4
  52. package/skill/LICENSE +21 -0
  53. package/skill/SKILL.md +18 -0
  54. package/src/api.ts +426 -0
  55. package/src/cli.ts +305 -0
  56. package/src/client.ts +413 -0
  57. package/src/config.ts +252 -0
  58. package/src/errors.ts +40 -0
  59. package/src/index.ts +37 -0
  60. package/src/presentation.ts +39 -0
  61. package/src/safe-output.ts +826 -0
  62. package/src/setup.ts +95 -0
  63. package/src/skill.ts +22 -0
  64. package/src/types.ts +85 -0
  65. package/src/workspace-source.ts +526 -0
  66. package/tsconfig.build.json +18 -0
  67. package/tsconfig.json +14 -0
@@ -0,0 +1,526 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstat, open, readFile, readlink, realpath } from "node:fs/promises";
3
+ import { constants } from "node:fs";
4
+ import { isAbsolute, relative, resolve, sep } from "node:path";
5
+ import { spawn } from "node:child_process";
6
+
7
+ import { Me2lonClientError } from "./errors.js";
8
+ import {
9
+ LOCAL_SOURCE_LIMITS,
10
+ SOURCE_SCHEMA_VERSION,
11
+ SNAPSHOT_SCHEMA_VERSION,
12
+ type WorkspaceSnapshot,
13
+ type BoundWorkspace,
14
+ type SourceChange,
15
+ type SourceLimits,
16
+ type WorkspaceSource,
17
+ } from "./types.js";
18
+
19
+ const MAX_GIT_OUTPUT_BYTES = 64 * 1024 * 1024;
20
+ const exactCommitPattern = /^[0-9a-f]{40}$/u;
21
+ const utf8Decoder = new TextDecoder("utf-8", { fatal: true });
22
+
23
+ type TreeEntry = {
24
+ mode: string;
25
+ type: string;
26
+ oid: string;
27
+ }
28
+
29
+ async function runGit(root: string, args: string[]): Promise<Buffer> {
30
+ return await new Promise<Buffer>((resolvePromise, reject) => {
31
+ const child = spawn("git", ["-C", root, ...args], {
32
+ stdio: ["ignore", "pipe", "pipe"],
33
+ env: { ...process.env, GIT_OPTIONAL_LOCKS: "0", LC_ALL: "C" },
34
+ windowsHide: true,
35
+ });
36
+ const stdout: Buffer[] = [];
37
+ let stdoutBytes = 0;
38
+ let stderrBytes = 0;
39
+
40
+ child.stdout.on("data", (chunk: Buffer) => {
41
+ stdoutBytes += chunk.length;
42
+ if (stdoutBytes > MAX_GIT_OUTPUT_BYTES) {
43
+ child.kill();
44
+ reject(
45
+ new Me2lonClientError(
46
+ "ME2LON_GIT_OUTPUT_LIMIT",
47
+ "Git produced more repository metadata than the Me2lon client accepts.",
48
+ ),
49
+ );
50
+ return;
51
+ }
52
+ stdout.push(chunk);
53
+ });
54
+ child.stderr.on("data", (chunk: Buffer) => {
55
+ stderrBytes += chunk.length;
56
+ if (stderrBytes > 64 * 1024) {
57
+ child.kill();
58
+ }
59
+ });
60
+ child.once("error", (error) => {
61
+ reject(
62
+ new Me2lonClientError(
63
+ "ME2LON_GIT_UNAVAILABLE",
64
+ "The Me2lon client could not run Git.",
65
+ { cause: error },
66
+ ),
67
+ );
68
+ });
69
+ child.once("close", (code) => {
70
+ if (code !== 0) {
71
+ reject(
72
+ new Me2lonClientError(
73
+ "ME2LON_GIT_FAILURE",
74
+ "Git could not inspect the bound workspace and base commit.",
75
+ ),
76
+ );
77
+ return;
78
+ }
79
+ resolvePromise(Buffer.concat(stdout));
80
+ });
81
+ });
82
+ }
83
+
84
+ export async function discoverWorkspace(directory: string): Promise<{ root: string; head: string; branch: string | null }> {
85
+ const root = await realpath(decodeUtf8(await runGit(resolve(directory), ["rev-parse", "--show-toplevel"]), "Git root").trim());
86
+ const head = decodeUtf8(await runGit(root, ["rev-parse", "--verify", "HEAD^{commit}"]), "Git HEAD").trim();
87
+ if (!exactCommitPattern.test(head)) throw new Me2lonClientError("ME2LON_BASE_COMMIT_INVALID", "A local Git repository with an initial commit is required.");
88
+ const branch = decodeUtf8(await runGit(root, ["rev-parse", "--abbrev-ref", "HEAD"]), "Git branch").trim();
89
+ return { root, head, branch: branch === "HEAD" ? null : branch };
90
+ }
91
+
92
+ async function snapshotFile(root: string, path: string, maximumBytes: number): Promise<Buffer> {
93
+ const absolute = pathInsideRoot(root, path);
94
+ let parent = root;
95
+ for (const part of path.split("/")) {
96
+ parent = resolve(parent, part);
97
+ if ((await lstat(parent)).isSymbolicLink()) {
98
+ throw new Me2lonClientError("ME2LON_SOURCE_SYMLINK_UNSUPPORTED", `Symbolic link ${path} cannot be uploaded in a full snapshot.`);
99
+ }
100
+ }
101
+ if (!(await lstat(absolute)).isFile()) throw new Me2lonClientError("ME2LON_SOURCE_KIND_UNSUPPORTED", `Path ${path} is not a regular file; submodules and special files are unsupported.`);
102
+ const handle = await open(absolute, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
103
+ try {
104
+ const before = await handle.stat();
105
+ if (!before.isFile() || before.size > maximumBytes) throw new Me2lonClientError("ME2LON_SOURCE_FILE_LIMIT", `File ${path} is unsupported or exceeds the upload limit.`);
106
+ const buffer = Buffer.alloc(maximumBytes + 1);
107
+ let length = 0;
108
+ while (length < buffer.length) {
109
+ const { bytesRead } = await handle.read(buffer, length, buffer.length - length, null);
110
+ if (bytesRead === 0) break;
111
+ length += bytesRead;
112
+ }
113
+ if (length > maximumBytes) throw new Me2lonClientError("ME2LON_SOURCE_FILE_LIMIT", `File ${path} exceeds the upload limit.`);
114
+ const bytes = buffer.subarray(0, length);
115
+ const after = await handle.stat();
116
+ if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs ||
117
+ await realpath(absolute) !== absolute) {
118
+ throw new Me2lonClientError("ME2LON_WORKSPACE_CHANGED_DURING_READ", "The workspace changed during capture; retry the check.");
119
+ }
120
+ return bytes;
121
+ } finally { await handle.close(); }
122
+ }
123
+
124
+ /** Capture every supported Git-visible file, and verify the complete capture twice. */
125
+ export async function buildWorkspaceSnapshot(workspace: BoundWorkspace, limits: SourceLimits = LOCAL_SOURCE_LIMITS): Promise<WorkspaceSnapshot> {
126
+ const before = await discoverWorkspace(workspace.root);
127
+ if (before.root !== workspace.root) throw new Me2lonClientError("ME2LON_WORKSPACE_NOT_ROOT", "The bound Git root changed.");
128
+ const visible = await gitVisiblePaths(before.root);
129
+ const flags = await trackedPathFlags(before.root);
130
+ assertCompleteWorktree(flags);
131
+ const paths = [...await currentExistingPaths(before.root, visible)].sort(comparePaths);
132
+ checkLimits(paths.length, 0, 0, limits);
133
+ const files: WorkspaceSnapshot["files"] = [];
134
+ const captured = new Map<string, Buffer>();
135
+ let totalReplacementBytes = 0;
136
+ for (const path of paths) {
137
+ const bytes = await snapshotFile(before.root, path, limits.maxFileBytes);
138
+ totalReplacementBytes += bytes.length;
139
+ checkLimits(files.length + 1, bytes.length, totalReplacementBytes, limits);
140
+ const content = decodeUtf8(bytes, `File ${path}`);
141
+ if (content.includes("\0")) throw new Me2lonClientError("ME2LON_SOURCE_NOT_UTF8", `File ${path} contains NUL bytes and cannot be uploaded.`);
142
+ files.push({ path, content }); captured.set(path, bytes);
143
+ }
144
+ for (const [path, bytes] of captured) {
145
+ if (!bytes.equals(await snapshotFile(before.root, path, limits.maxFileBytes))) {
146
+ throw new Me2lonClientError("ME2LON_WORKSPACE_CHANGED_DURING_READ", "The workspace changed during capture; retry the check.");
147
+ }
148
+ }
149
+ const after = await discoverWorkspace(before.root);
150
+ if (before.head !== after.head || before.branch !== after.branch ||
151
+ !visible.equals(await gitVisiblePaths(before.root)) || !flags.equals(await trackedPathFlags(before.root))) {
152
+ throw new Me2lonClientError("ME2LON_WORKSPACE_CHANGED_DURING_READ", "Git changed during capture; retry the check.");
153
+ }
154
+ if (files.length === 0) throw new Me2lonClientError("ME2LON_SOURCE_EMPTY", "The project has no files to upload.");
155
+ return {
156
+ schemaVersion: SNAPSHOT_SCHEMA_VERSION, baseCommitSha: before.head, branchLabel: before.branch, files,
157
+ totalReplacementBytes,
158
+ digest: `sha256:${createHash("sha256").update(JSON.stringify({ head: before.head, files })).digest("hex")}`,
159
+ };
160
+ }
161
+
162
+ function decodeUtf8(bytes: Buffer, context: string): string {
163
+ try {
164
+ return utf8Decoder.decode(bytes);
165
+ } catch (error) {
166
+ throw new Me2lonClientError(
167
+ "ME2LON_SOURCE_NOT_UTF8",
168
+ `${context} is not valid UTF-8 and cannot be represented by ${SOURCE_SCHEMA_VERSION}.`,
169
+ { cause: error },
170
+ );
171
+ }
172
+ }
173
+
174
+ function parseNullDelimited(bytes: Buffer, context: string): string[] {
175
+ const decoded = decodeUtf8(bytes, context);
176
+ const values = decoded.split("\0");
177
+ if (values.at(-1) === "") {
178
+ values.pop();
179
+ }
180
+ return values;
181
+ }
182
+
183
+ function validateGitPath(path: string): void {
184
+ if (
185
+ path === "" ||
186
+ path.includes("\0") ||
187
+ isAbsolute(path) ||
188
+ path.split("/").some((part) => part === "..") ||
189
+ path === ".git" ||
190
+ path.startsWith(".git/")
191
+ ) {
192
+ throw new Me2lonClientError(
193
+ "ME2LON_SOURCE_PATH_INVALID",
194
+ "Git reported a path that cannot be uploaded safely.",
195
+ );
196
+ }
197
+ }
198
+
199
+ function pathInsideRoot(root: string, gitPath: string): string {
200
+ validateGitPath(gitPath);
201
+ const absolutePath = resolve(root, ...gitPath.split("/"));
202
+ const fromRoot = relative(root, absolutePath);
203
+ if (fromRoot.startsWith(`..${sep}`) || fromRoot === ".." || isAbsolute(fromRoot)) {
204
+ throw new Me2lonClientError(
205
+ "ME2LON_SOURCE_PATH_INVALID",
206
+ "Git reported a path outside the bound workspace.",
207
+ );
208
+ }
209
+ return absolutePath;
210
+ }
211
+
212
+ function comparePaths(left: string, right: string): number {
213
+ return Buffer.compare(Buffer.from(left, "utf8"), Buffer.from(right, "utf8"));
214
+ }
215
+
216
+ async function baseTree(
217
+ workspace: BoundWorkspace,
218
+ ): Promise<Map<string, TreeEntry>> {
219
+ const raw = await runGit(workspace.root, [
220
+ "ls-tree",
221
+ "-r",
222
+ "-z",
223
+ "--full-tree",
224
+ workspace.baseCommitSha,
225
+ ]);
226
+ const entries = new Map<string, TreeEntry>();
227
+ for (const record of parseNullDelimited(raw, "A Git tree path")) {
228
+ const tab = record.indexOf("\t");
229
+ if (tab === -1) {
230
+ throw new Me2lonClientError(
231
+ "ME2LON_GIT_METADATA_INVALID",
232
+ "Git returned an invalid base-tree record.",
233
+ );
234
+ }
235
+ const metadata = record.slice(0, tab).split(" ");
236
+ const path = record.slice(tab + 1);
237
+ validateGitPath(path);
238
+ const [mode, type, oid] = metadata;
239
+ if (!mode || !type || !oid) {
240
+ throw new Me2lonClientError(
241
+ "ME2LON_GIT_METADATA_INVALID",
242
+ "Git returned incomplete base-tree metadata.",
243
+ );
244
+ }
245
+ entries.set(path, { mode, type, oid });
246
+ }
247
+ return entries;
248
+ }
249
+
250
+ async function gitVisiblePaths(root: string): Promise<Buffer> {
251
+ return await runGit(root, [
252
+ "ls-files",
253
+ "-z",
254
+ "--cached",
255
+ "--others",
256
+ "--exclude-standard",
257
+ ]);
258
+ }
259
+
260
+ async function trackedPathFlags(root: string): Promise<Buffer> {
261
+ return await runGit(root, ["ls-files", "-v", "-z"]);
262
+ }
263
+
264
+ function assertCompleteWorktree(raw: Buffer): void {
265
+ for (const record of parseNullDelimited(raw, "A Git tracked-path flag")) {
266
+ const tag = record.at(0);
267
+ if (tag === "S" || (tag !== undefined && /^[a-z]$/u.test(tag))) {
268
+ throw new Me2lonClientError(
269
+ "ME2LON_INCOMPLETE_WORKTREE",
270
+ "The bound repository uses skip-worktree or assume-unchanged entries, so Me2lon cannot construct a complete source revision.",
271
+ );
272
+ }
273
+ }
274
+ }
275
+
276
+ async function changedPaths(
277
+ workspace: BoundWorkspace,
278
+ ): Promise<Buffer> {
279
+ return await runGit(workspace.root, [
280
+ "diff",
281
+ "--name-only",
282
+ "-z",
283
+ "--no-renames",
284
+ "--no-ext-diff",
285
+ "--no-textconv",
286
+ workspace.baseCommitSha,
287
+ "--",
288
+ ]);
289
+ }
290
+
291
+ async function currentExistingPaths(root: string, raw: Buffer): Promise<Set<string>> {
292
+ const result = new Set<string>();
293
+ for (const path of parseNullDelimited(raw, "A Git worktree path")) {
294
+ validateGitPath(path);
295
+ const absolutePath = pathInsideRoot(root, path);
296
+ try {
297
+ await lstat(absolutePath);
298
+ result.add(path);
299
+ } catch (error) {
300
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
301
+ throw error;
302
+ }
303
+ }
304
+ }
305
+ return result;
306
+ }
307
+
308
+ async function readBaseBlob(root: string, oid: string): Promise<Buffer> {
309
+ return await runGit(root, ["cat-file", "blob", oid]);
310
+ }
311
+
312
+ function checkLimits(
313
+ changes: number,
314
+ fileBytes: number,
315
+ totalBytes: number,
316
+ limits: SourceLimits,
317
+ ): void {
318
+ if (changes > limits.maxChanges) {
319
+ throw new Me2lonClientError(
320
+ "ME2LON_SOURCE_CHANGE_LIMIT",
321
+ `The workspace revision exceeds the ${limits.maxChanges}-change client limit.`,
322
+ );
323
+ }
324
+ if (fileBytes > limits.maxFileBytes) {
325
+ throw new Me2lonClientError(
326
+ "ME2LON_SOURCE_FILE_LIMIT",
327
+ `A replacement exceeds the ${limits.maxFileBytes}-byte client limit.`,
328
+ );
329
+ }
330
+ if (totalBytes > limits.maxTotalBytes) {
331
+ throw new Me2lonClientError(
332
+ "ME2LON_SOURCE_TOTAL_LIMIT",
333
+ `The workspace revision exceeds the ${limits.maxTotalBytes}-byte aggregate client limit.`,
334
+ );
335
+ }
336
+ }
337
+
338
+ export async function bindWorkspace(
339
+ requestedRoot: string,
340
+ repositoryId: string,
341
+ requestedBaseCommit: string,
342
+ ): Promise<BoundWorkspace> {
343
+ if (!repositoryId.trim()) {
344
+ throw new Me2lonClientError(
345
+ "ME2LON_REPOSITORY_REQUIRED",
346
+ "A repository id is required.",
347
+ );
348
+ }
349
+ if (!exactCommitPattern.test(requestedBaseCommit)) {
350
+ throw new Me2lonClientError(
351
+ "ME2LON_BASE_COMMIT_NOT_EXACT",
352
+ "The base commit must be a full 40-character hexadecimal Git commit id, not a branch or abbreviated revision.",
353
+ );
354
+ }
355
+
356
+ const root = await realpath(resolve(requestedRoot));
357
+ const reportedRoot = decodeUtf8(
358
+ await runGit(root, ["rev-parse", "--show-toplevel"]),
359
+ "The Git workspace root",
360
+ ).trim();
361
+ const canonicalRoot = await realpath(reportedRoot);
362
+ if (canonicalRoot !== root) {
363
+ throw new Me2lonClientError(
364
+ "ME2LON_WORKSPACE_NOT_ROOT",
365
+ "The MCP workspace must be bound to the Git repository root, not a parent or subdirectory.",
366
+ );
367
+ }
368
+
369
+ const canonicalCommit = decodeUtf8(
370
+ await runGit(root, [
371
+ "rev-parse",
372
+ "--verify",
373
+ `${requestedBaseCommit}^{commit}`,
374
+ ]),
375
+ "The Git base commit",
376
+ ).trim();
377
+ if (!exactCommitPattern.test(canonicalCommit)) {
378
+ throw new Me2lonClientError(
379
+ "ME2LON_BASE_COMMIT_INVALID",
380
+ "Git did not resolve the requested object to an exact commit id.",
381
+ );
382
+ }
383
+ return {
384
+ root,
385
+ repositoryId: repositoryId.trim(),
386
+ baseCommitSha: canonicalCommit,
387
+ };
388
+ }
389
+
390
+ export async function buildWorkspaceSource(
391
+ workspace: BoundWorkspace,
392
+ limits: SourceLimits = LOCAL_SOURCE_LIMITS,
393
+ ): Promise<WorkspaceSource> {
394
+ const [tree, visibleBeforeRaw, changedBeforeRaw, pathFlagsRaw] = await Promise.all([
395
+ baseTree(workspace),
396
+ gitVisiblePaths(workspace.root),
397
+ changedPaths(workspace),
398
+ trackedPathFlags(workspace.root),
399
+ ]);
400
+ assertCompleteWorktree(pathFlagsRaw);
401
+ const visible = await currentExistingPaths(workspace.root, visibleBeforeRaw);
402
+ const candidatePaths = new Set(
403
+ parseNullDelimited(changedBeforeRaw, "A changed Git path"),
404
+ );
405
+ for (const path of visible) {
406
+ if (!tree.has(path)) {
407
+ candidatePaths.add(path);
408
+ }
409
+ }
410
+ for (const path of tree.keys()) {
411
+ if (!visible.has(path)) {
412
+ candidatePaths.add(path);
413
+ }
414
+ }
415
+
416
+ const changes: SourceChange[] = [];
417
+ let replacementCount = 0;
418
+ let deletionCount = 0;
419
+ let totalReplacementBytes = 0;
420
+
421
+ for (const path of [...candidatePaths].sort(comparePaths)) {
422
+ validateGitPath(path);
423
+ const base = tree.get(path);
424
+ if (!visible.has(path)) {
425
+ if (base) {
426
+ changes.push({ operation: "delete", path });
427
+ deletionCount += 1;
428
+ checkLimits(changes.length, 0, totalReplacementBytes, limits);
429
+ }
430
+ continue;
431
+ }
432
+
433
+ const absolutePath = pathInsideRoot(workspace.root, path);
434
+ const before = await lstat(absolutePath);
435
+ if (before.isSymbolicLink()) {
436
+ if (!base || base.mode !== "120000" || base.type !== "blob") {
437
+ throw new Me2lonClientError(
438
+ "ME2LON_SOURCE_SYMLINK_UNSUPPORTED",
439
+ `Changed symbolic link ${path} cannot be represented by ${SOURCE_SCHEMA_VERSION}.`,
440
+ );
441
+ }
442
+ const [target, original] = await Promise.all([
443
+ readlink(absolutePath, "utf8"),
444
+ readBaseBlob(workspace.root, base.oid),
445
+ ]);
446
+ if (!Buffer.from(target, "utf8").equals(original)) {
447
+ throw new Me2lonClientError(
448
+ "ME2LON_SOURCE_SYMLINK_UNSUPPORTED",
449
+ `Changed symbolic link ${path} cannot be represented by ${SOURCE_SCHEMA_VERSION}.`,
450
+ );
451
+ }
452
+ continue;
453
+ }
454
+ if (base?.type === "commit") {
455
+ throw new Me2lonClientError(
456
+ "ME2LON_SOURCE_SUBMODULE_UNSUPPORTED",
457
+ `Changed submodule ${path} cannot be represented by ${SOURCE_SCHEMA_VERSION}.`,
458
+ );
459
+ }
460
+ if (!before.isFile()) {
461
+ throw new Me2lonClientError(
462
+ "ME2LON_SOURCE_KIND_UNSUPPORTED",
463
+ `Changed path ${path} is not a regular UTF-8 file.`,
464
+ );
465
+ }
466
+
467
+ checkLimits(changes.length, before.size, totalReplacementBytes, limits);
468
+ const bytes = await readFile(absolutePath);
469
+ const after = await lstat(absolutePath);
470
+ if (
471
+ before.size !== after.size ||
472
+ before.mtimeMs !== after.mtimeMs ||
473
+ before.ctimeMs !== after.ctimeMs
474
+ ) {
475
+ throw new Me2lonClientError(
476
+ "ME2LON_WORKSPACE_CHANGED_DURING_READ",
477
+ "The workspace changed while Me2lon was preparing its revision; retry the check.",
478
+ );
479
+ }
480
+ checkLimits(
481
+ changes.length + 1,
482
+ bytes.length,
483
+ totalReplacementBytes + bytes.length,
484
+ limits,
485
+ );
486
+ if (base?.type === "blob") {
487
+ const original = await readBaseBlob(workspace.root, base.oid);
488
+ if (bytes.equals(original)) {
489
+ continue;
490
+ }
491
+ }
492
+ const content = decodeUtf8(bytes, `Replacement ${path}`);
493
+ changes.push({ operation: "replace", path, content });
494
+ replacementCount += 1;
495
+ totalReplacementBytes += bytes.length;
496
+ }
497
+
498
+ const [visibleAfterRaw, changedAfterRaw] = await Promise.all([
499
+ gitVisiblePaths(workspace.root),
500
+ changedPaths(workspace),
501
+ ]);
502
+ if (
503
+ !visibleBeforeRaw.equals(visibleAfterRaw) ||
504
+ !changedBeforeRaw.equals(changedAfterRaw)
505
+ ) {
506
+ throw new Me2lonClientError(
507
+ "ME2LON_WORKSPACE_CHANGED_DURING_READ",
508
+ "The workspace changed while Me2lon was preparing its revision; retry the check.",
509
+ );
510
+ }
511
+
512
+ const digestPayload = JSON.stringify({
513
+ schemaVersion: SOURCE_SCHEMA_VERSION,
514
+ baseCommitSha: workspace.baseCommitSha,
515
+ changes,
516
+ });
517
+ return {
518
+ schemaVersion: SOURCE_SCHEMA_VERSION,
519
+ baseCommitSha: workspace.baseCommitSha,
520
+ changes,
521
+ digest: `sha256:${createHash("sha256").update(digestPayload).digest("hex")}`,
522
+ replacementCount,
523
+ deletionCount,
524
+ totalReplacementBytes,
525
+ };
526
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "declaration": true,
5
+ "declarationMap": true,
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "sourceMap": true,
9
+ "noEmit": false
10
+ },
11
+ "include": [
12
+ "src/**/*.ts"
13
+ ],
14
+ "exclude": [
15
+ "test/**/*.ts",
16
+ "src/mcp.ts"
17
+ ]
18
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2023", "DOM"],
7
+ "strict": true,
8
+ "noUncheckedIndexedAccess": true,
9
+ "exactOptionalPropertyTypes": true,
10
+ "noEmit": true,
11
+ "types": ["node"]
12
+ },
13
+ "include": ["src/**/*.ts", "test/**/*.ts"]
14
+ }