@aigne/afs-git 1.0.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.
@@ -0,0 +1,597 @@
1
+ import { execFile } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { mkdir, rename, rm, stat, writeFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { basename, dirname, isAbsolute, join } from "node:path";
6
+ import { promisify } from "node:util";
7
+ const execFileAsync = promisify(execFile);
8
+ import { camelizeSchema, optionalize } from "@aigne/core/loader/schema.js";
9
+ import { checkArguments } from "@aigne/core/utils/type-utils.js";
10
+ import { simpleGit } from "simple-git";
11
+ import { z } from "zod";
12
+ const LIST_MAX_LIMIT = 1000;
13
+ const afsGitOptionsSchema = camelizeSchema(z.object({
14
+ name: optionalize(z.string()),
15
+ repoPath: z.string().describe("The path to the git repository"),
16
+ description: optionalize(z.string().describe("A description of the repository")),
17
+ branches: optionalize(z.array(z.string()).describe("List of branches to expose")),
18
+ accessMode: optionalize(z.enum(["readonly", "readwrite"]).describe("Access mode for this module")),
19
+ autoCommit: optionalize(z.boolean().describe("Automatically commit changes after write operations")),
20
+ commitAuthor: optionalize(z.object({
21
+ name: z.string(),
22
+ email: z.string(),
23
+ })),
24
+ }));
25
+ export class AFSGit {
26
+ options;
27
+ static schema() {
28
+ return afsGitOptionsSchema;
29
+ }
30
+ static async load({ filepath, parsed }) {
31
+ const valid = await AFSGit.schema().parseAsync(parsed);
32
+ return new AFSGit({ ...valid, cwd: dirname(filepath) });
33
+ }
34
+ git;
35
+ tempBase;
36
+ worktrees = new Map();
37
+ repoHash;
38
+ constructor(options) {
39
+ this.options = options;
40
+ checkArguments("AFSGit", afsGitOptionsSchema, options);
41
+ let repoPath;
42
+ if (isAbsolute(options.repoPath)) {
43
+ repoPath = options.repoPath;
44
+ }
45
+ else {
46
+ repoPath = join(options.cwd || process.cwd(), options.repoPath);
47
+ }
48
+ this.options.repoPath = repoPath;
49
+ this.name = options.name || basename(repoPath) || "git";
50
+ this.description = options.description;
51
+ this.accessMode = options.accessMode ?? "readonly";
52
+ this.git = simpleGit(repoPath);
53
+ // Create a hash of the repo path for unique temp directory
54
+ this.repoHash = createHash("md5").update(repoPath).digest("hex").substring(0, 8);
55
+ this.tempBase = join(tmpdir(), `afs-git-${this.repoHash}`);
56
+ }
57
+ name;
58
+ description;
59
+ accessMode;
60
+ /**
61
+ * Parse AFS path into branch and file path
62
+ * Branch names may contain slashes and are encoded with ~ in paths
63
+ * Examples:
64
+ * "/" -> { branch: undefined, filePath: "" }
65
+ * "/main" -> { branch: "main", filePath: "" }
66
+ * "/feature~new-feature" -> { branch: "feature/new-feature", filePath: "" }
67
+ * "/main/src/index.ts" -> { branch: "main", filePath: "src/index.ts" }
68
+ */
69
+ parsePath(path) {
70
+ const normalized = join("/", path); // Ensure leading slash
71
+ const segments = normalized.split("/").filter(Boolean);
72
+ if (segments.length === 0) {
73
+ return { branch: undefined, filePath: "" };
74
+ }
75
+ // Decode branch name (first segment): replace ~ with /
76
+ const branch = segments[0].replace(/~/g, "/");
77
+ const filePath = segments.slice(1).join("/");
78
+ return { branch, filePath };
79
+ }
80
+ /**
81
+ * Detect MIME type based on file extension
82
+ */
83
+ getMimeType(filePath) {
84
+ const ext = filePath.split(".").pop()?.toLowerCase();
85
+ const mimeTypes = {
86
+ // Images
87
+ png: "image/png",
88
+ jpg: "image/jpeg",
89
+ jpeg: "image/jpeg",
90
+ gif: "image/gif",
91
+ bmp: "image/bmp",
92
+ webp: "image/webp",
93
+ svg: "image/svg+xml",
94
+ ico: "image/x-icon",
95
+ // Documents
96
+ pdf: "application/pdf",
97
+ txt: "text/plain",
98
+ md: "text/markdown",
99
+ // Code
100
+ js: "text/javascript",
101
+ ts: "text/typescript",
102
+ json: "application/json",
103
+ html: "text/html",
104
+ css: "text/css",
105
+ xml: "text/xml",
106
+ };
107
+ return mimeTypes[ext || ""] || "application/octet-stream";
108
+ }
109
+ /**
110
+ * Check if file is likely binary based on extension
111
+ */
112
+ isBinaryFile(filePath) {
113
+ const ext = filePath.split(".").pop()?.toLowerCase();
114
+ const binaryExtensions = [
115
+ "png",
116
+ "jpg",
117
+ "jpeg",
118
+ "gif",
119
+ "bmp",
120
+ "webp",
121
+ "ico",
122
+ "pdf",
123
+ "zip",
124
+ "tar",
125
+ "gz",
126
+ "exe",
127
+ "dll",
128
+ "so",
129
+ "dylib",
130
+ "wasm",
131
+ ];
132
+ return binaryExtensions.includes(ext || "");
133
+ }
134
+ /**
135
+ * Get list of available branches
136
+ */
137
+ async getBranches() {
138
+ const branchSummary = await this.git.branchLocal();
139
+ const allBranches = branchSummary.all;
140
+ // Filter by allowed branches if specified
141
+ if (this.options.branches && this.options.branches.length > 0) {
142
+ return allBranches.filter((branch) => this.options.branches.includes(branch));
143
+ }
144
+ return allBranches;
145
+ }
146
+ /**
147
+ * Ensure worktree exists for a branch (lazy creation)
148
+ */
149
+ async ensureWorktree(branch) {
150
+ if (this.worktrees.has(branch)) {
151
+ return this.worktrees.get(branch);
152
+ }
153
+ // Check if this is the current branch in the main repo
154
+ const currentBranch = await this.git.revparse(["--abbrev-ref", "HEAD"]);
155
+ if (currentBranch.trim() === branch) {
156
+ // Use the main repo path for the current branch
157
+ this.worktrees.set(branch, this.options.repoPath);
158
+ return this.options.repoPath;
159
+ }
160
+ const worktreePath = join(this.tempBase, branch);
161
+ // Check if worktree directory already exists
162
+ const exists = await stat(worktreePath)
163
+ .then(() => true)
164
+ .catch(() => false);
165
+ if (!exists) {
166
+ await mkdir(this.tempBase, { recursive: true });
167
+ await this.git.raw(["worktree", "add", worktreePath, branch]);
168
+ }
169
+ this.worktrees.set(branch, worktreePath);
170
+ return worktreePath;
171
+ }
172
+ /**
173
+ * List files using git ls-tree (no worktree needed)
174
+ */
175
+ async listWithGitLsTree(branch, path, options) {
176
+ const maxDepth = options?.maxDepth ?? 1;
177
+ const limit = Math.min(options?.limit || LIST_MAX_LIMIT, LIST_MAX_LIMIT);
178
+ const entries = [];
179
+ const targetPath = path || "";
180
+ const treeish = targetPath ? `${branch}:${targetPath}` : branch;
181
+ try {
182
+ // Check if the path exists and is a directory
183
+ const pathType = await this.git
184
+ .raw(["cat-file", "-t", treeish])
185
+ .then((t) => t.trim())
186
+ .catch(() => null);
187
+ if (pathType === null) {
188
+ // Path doesn't exist
189
+ return { data: [] };
190
+ }
191
+ // If it's a file, just return it
192
+ if (pathType === "blob") {
193
+ const size = await this.git
194
+ .raw(["cat-file", "-s", treeish])
195
+ .then((s) => Number.parseInt(s.trim(), 10));
196
+ const afsPath = this.buildPath(branch, path);
197
+ entries.push({
198
+ id: afsPath,
199
+ path: afsPath,
200
+ metadata: {
201
+ type: "file",
202
+ size,
203
+ },
204
+ });
205
+ return { data: entries };
206
+ }
207
+ const queue = [{ path: targetPath, depth: 0 }];
208
+ while (queue.length > 0) {
209
+ const item = queue.shift();
210
+ const { path: itemPath, depth } = item;
211
+ // List directory contents
212
+ const itemTreeish = itemPath ? `${branch}:${itemPath}` : branch;
213
+ const output = await this.git.raw(["ls-tree", "-l", itemTreeish]);
214
+ const lines = output
215
+ .split("\n")
216
+ .filter((line) => line.trim())
217
+ .slice(0, limit - entries.length);
218
+ for (const line of lines) {
219
+ // Format: <mode> <type> <hash> <size (with padding)> <name>
220
+ // Example: 100644 blob abc123 1234\tREADME.md
221
+ // Note: size is "-" for trees/directories, and there can be multiple spaces/tabs before name
222
+ const match = line.match(/^(\d+)\s+(blob|tree)\s+(\w+)\s+(-|\d+)\s+(.+)$/);
223
+ if (!match)
224
+ continue;
225
+ const type = match[2];
226
+ const sizeStr = match[4];
227
+ const name = match[5];
228
+ const isDirectory = type === "tree";
229
+ const size = sizeStr === "-" ? undefined : Number.parseInt(sizeStr, 10);
230
+ const fullPath = itemPath ? `${itemPath}/${name}` : name;
231
+ const afsPath = this.buildPath(branch, fullPath);
232
+ entries.push({
233
+ id: afsPath,
234
+ path: afsPath,
235
+ metadata: {
236
+ type: isDirectory ? "directory" : "file",
237
+ size,
238
+ },
239
+ });
240
+ // Add to queue if it's a directory and we haven't reached max depth
241
+ if (isDirectory && depth + 1 < maxDepth) {
242
+ queue.push({ path: fullPath, depth: depth + 1 });
243
+ }
244
+ // Check limit
245
+ if (entries.length >= limit) {
246
+ return { data: entries };
247
+ }
248
+ }
249
+ }
250
+ return { data: entries };
251
+ }
252
+ catch (error) {
253
+ return { data: [], message: error.message };
254
+ }
255
+ }
256
+ /**
257
+ * Build AFS path with encoded branch name
258
+ * Branch names with slashes are encoded by replacing / with ~
259
+ * @param branch Branch name (may contain slashes)
260
+ * @param filePath File path within branch
261
+ */
262
+ buildPath(branch, filePath) {
263
+ // Replace / with ~ in branch name
264
+ const encodedBranch = branch.replace(/\//g, "~");
265
+ if (!filePath) {
266
+ return `/${encodedBranch}`;
267
+ }
268
+ return `/${encodedBranch}/${filePath}`;
269
+ }
270
+ async list(path, options) {
271
+ const { branch, filePath } = this.parsePath(path);
272
+ // Root path - list branches
273
+ if (!branch) {
274
+ const branches = await this.getBranches();
275
+ return {
276
+ data: branches.map((name) => {
277
+ const encodedPath = this.buildPath(name);
278
+ return {
279
+ id: encodedPath,
280
+ path: encodedPath,
281
+ metadata: {
282
+ type: "directory",
283
+ },
284
+ };
285
+ }),
286
+ };
287
+ }
288
+ // List files in branch using git ls-tree
289
+ return this.listWithGitLsTree(branch, filePath, options);
290
+ }
291
+ async read(path, _options) {
292
+ const { branch, filePath } = this.parsePath(path);
293
+ if (!branch) {
294
+ return {
295
+ data: {
296
+ id: "/",
297
+ path: "/",
298
+ metadata: { type: "directory" },
299
+ },
300
+ };
301
+ }
302
+ if (!filePath) {
303
+ const branchPath = this.buildPath(branch);
304
+ return {
305
+ data: {
306
+ id: branchPath,
307
+ path: branchPath,
308
+ metadata: { type: "directory" },
309
+ },
310
+ };
311
+ }
312
+ try {
313
+ // Check if path is a blob (file) or tree (directory)
314
+ const objectType = await this.git
315
+ .raw(["cat-file", "-t", `${branch}:${filePath}`])
316
+ .then((t) => t.trim());
317
+ if (objectType === "tree") {
318
+ // It's a directory
319
+ const afsPath = this.buildPath(branch, filePath);
320
+ return {
321
+ data: {
322
+ id: afsPath,
323
+ path: afsPath,
324
+ metadata: {
325
+ type: "directory",
326
+ },
327
+ },
328
+ };
329
+ }
330
+ // It's a file, get content
331
+ const size = await this.git
332
+ .raw(["cat-file", "-s", `${branch}:${filePath}`])
333
+ .then((s) => Number.parseInt(s.trim(), 10));
334
+ // Determine mimeType based on file extension
335
+ const mimeType = this.getMimeType(filePath);
336
+ const isBinary = this.isBinaryFile(filePath);
337
+ let content;
338
+ const metadata = {
339
+ type: "file",
340
+ size,
341
+ mimeType,
342
+ };
343
+ if (isBinary) {
344
+ // For binary files, use execFileAsync to get raw buffer
345
+ const { stdout } = await execFileAsync("git", ["cat-file", "-p", `${branch}:${filePath}`], {
346
+ cwd: this.options.repoPath,
347
+ encoding: "buffer",
348
+ maxBuffer: 10 * 1024 * 1024, // 10MB max
349
+ });
350
+ // Store only base64 string without data URL prefix
351
+ content = stdout.toString("base64");
352
+ // Mark content as base64 in metadata
353
+ metadata.contentType = "base64";
354
+ }
355
+ else {
356
+ // For text files, use git.show
357
+ content = await this.git.show([`${branch}:${filePath}`]);
358
+ }
359
+ const afsPath = this.buildPath(branch, filePath);
360
+ return {
361
+ data: {
362
+ id: afsPath,
363
+ path: afsPath,
364
+ content,
365
+ metadata,
366
+ },
367
+ };
368
+ }
369
+ catch (error) {
370
+ return {
371
+ data: undefined,
372
+ message: error.message,
373
+ };
374
+ }
375
+ }
376
+ async write(path, entry, options) {
377
+ const { branch, filePath } = this.parsePath(path);
378
+ if (!branch || !filePath) {
379
+ throw new Error("Cannot write to root or branch root");
380
+ }
381
+ // Create worktree for write operations
382
+ const worktreePath = await this.ensureWorktree(branch);
383
+ const fullPath = join(worktreePath, filePath);
384
+ const append = options?.append ?? false;
385
+ // Ensure parent directory exists
386
+ const parentDir = dirname(fullPath);
387
+ await mkdir(parentDir, { recursive: true });
388
+ // Write content
389
+ if (entry.content !== undefined) {
390
+ let contentToWrite;
391
+ if (typeof entry.content === "string") {
392
+ contentToWrite = entry.content;
393
+ }
394
+ else {
395
+ contentToWrite = JSON.stringify(entry.content, null, 2);
396
+ }
397
+ await writeFile(fullPath, contentToWrite, { encoding: "utf8", flag: append ? "a" : "w" });
398
+ }
399
+ // Auto commit if enabled
400
+ if (this.options.autoCommit) {
401
+ const gitInstance = simpleGit(worktreePath);
402
+ await gitInstance.add(filePath);
403
+ if (this.options.commitAuthor) {
404
+ await gitInstance.addConfig("user.name", this.options.commitAuthor.name, undefined, "local");
405
+ await gitInstance.addConfig("user.email", this.options.commitAuthor.email, undefined, "local");
406
+ }
407
+ await gitInstance.commit(`Update ${filePath}`);
408
+ }
409
+ // Get file stats
410
+ const stats = await stat(fullPath);
411
+ const afsPath = this.buildPath(branch, filePath);
412
+ const writtenEntry = {
413
+ id: afsPath,
414
+ path: afsPath,
415
+ content: entry.content,
416
+ summary: entry.summary,
417
+ createdAt: stats.birthtime,
418
+ updatedAt: stats.mtime,
419
+ metadata: {
420
+ ...entry.metadata,
421
+ type: stats.isDirectory() ? "directory" : "file",
422
+ size: stats.size,
423
+ },
424
+ userId: entry.userId,
425
+ sessionId: entry.sessionId,
426
+ linkTo: entry.linkTo,
427
+ };
428
+ return { data: writtenEntry };
429
+ }
430
+ async delete(path, options) {
431
+ const { branch, filePath } = this.parsePath(path);
432
+ if (!branch || !filePath) {
433
+ throw new Error("Cannot delete root or branch root");
434
+ }
435
+ // Create worktree for delete operations
436
+ const worktreePath = await this.ensureWorktree(branch);
437
+ const fullPath = join(worktreePath, filePath);
438
+ const recursive = options?.recursive ?? false;
439
+ const stats = await stat(fullPath);
440
+ if (stats.isDirectory() && !recursive) {
441
+ throw new Error(`Cannot delete directory '${path}' without recursive option. Set recursive: true to delete directories.`);
442
+ }
443
+ await rm(fullPath, { recursive, force: true });
444
+ // Auto commit if enabled
445
+ if (this.options.autoCommit) {
446
+ const gitInstance = simpleGit(worktreePath);
447
+ await gitInstance.add(filePath);
448
+ if (this.options.commitAuthor) {
449
+ await gitInstance.addConfig("user.name", this.options.commitAuthor.name, undefined, "local");
450
+ await gitInstance.addConfig("user.email", this.options.commitAuthor.email, undefined, "local");
451
+ }
452
+ await gitInstance.commit(`Delete ${filePath}`);
453
+ }
454
+ return { message: `Successfully deleted: ${path}` };
455
+ }
456
+ async rename(oldPath, newPath, options) {
457
+ const { branch: oldBranch, filePath: oldFilePath } = this.parsePath(oldPath);
458
+ const { branch: newBranch, filePath: newFilePath } = this.parsePath(newPath);
459
+ if (!oldBranch || !oldFilePath) {
460
+ throw new Error("Cannot rename from root or branch root");
461
+ }
462
+ if (!newBranch || !newFilePath) {
463
+ throw new Error("Cannot rename to root or branch root");
464
+ }
465
+ if (oldBranch !== newBranch) {
466
+ throw new Error("Cannot rename across branches");
467
+ }
468
+ // Create worktree for rename operations
469
+ const worktreePath = await this.ensureWorktree(oldBranch);
470
+ const oldFullPath = join(worktreePath, oldFilePath);
471
+ const newFullPath = join(worktreePath, newFilePath);
472
+ const overwrite = options?.overwrite ?? false;
473
+ // Check if source exists
474
+ await stat(oldFullPath);
475
+ // Check if destination exists
476
+ try {
477
+ await stat(newFullPath);
478
+ if (!overwrite) {
479
+ throw new Error(`Destination '${newPath}' already exists. Set overwrite: true to replace it.`);
480
+ }
481
+ }
482
+ catch (error) {
483
+ if (error.code !== "ENOENT") {
484
+ throw error;
485
+ }
486
+ }
487
+ // Ensure parent directory exists
488
+ const newParentDir = dirname(newFullPath);
489
+ await mkdir(newParentDir, { recursive: true });
490
+ // Perform rename
491
+ await rename(oldFullPath, newFullPath);
492
+ // Auto commit if enabled
493
+ if (this.options.autoCommit) {
494
+ const gitInstance = simpleGit(worktreePath);
495
+ await gitInstance.add([oldFilePath, newFilePath]);
496
+ if (this.options.commitAuthor) {
497
+ await gitInstance.addConfig("user.name", this.options.commitAuthor.name, undefined, "local");
498
+ await gitInstance.addConfig("user.email", this.options.commitAuthor.email, undefined, "local");
499
+ }
500
+ await gitInstance.commit(`Rename ${oldFilePath} to ${newFilePath}`);
501
+ }
502
+ return { message: `Successfully renamed '${oldPath}' to '${newPath}'` };
503
+ }
504
+ async search(path, query, options) {
505
+ const { branch, filePath } = this.parsePath(path);
506
+ if (!branch) {
507
+ return { data: [], message: "Search requires a branch path" };
508
+ }
509
+ const limit = Math.min(options?.limit || LIST_MAX_LIMIT, LIST_MAX_LIMIT);
510
+ try {
511
+ // Use git grep for searching (no worktree needed)
512
+ const args = ["grep", "-n", "-I"]; // -n for line numbers, -I to skip binary files
513
+ if (options?.caseSensitive === false) {
514
+ args.push("-i");
515
+ }
516
+ args.push(query, branch);
517
+ // Add path filter if specified
518
+ if (filePath) {
519
+ args.push("--", filePath);
520
+ }
521
+ const output = await this.git.raw(args);
522
+ const lines = output.split("\n").filter((line) => line.trim());
523
+ const entries = [];
524
+ const processedFiles = new Set();
525
+ for (const line of lines) {
526
+ // Format when searching in branch: branch:path:linenum:content
527
+ // Try the format with branch prefix first
528
+ let matchPath;
529
+ let lineNum;
530
+ let content;
531
+ const matchWithBranch = line.match(/^[^:]+:([^:]+):(\d+):(.+)$/);
532
+ if (matchWithBranch) {
533
+ matchPath = matchWithBranch[1];
534
+ lineNum = matchWithBranch[2];
535
+ content = matchWithBranch[3];
536
+ }
537
+ else {
538
+ // Try format without branch: path:linenum:content
539
+ const matchNoBranch = line.match(/^([^:]+):(\d+):(.+)$/);
540
+ if (!matchNoBranch)
541
+ continue;
542
+ matchPath = matchNoBranch[1];
543
+ lineNum = matchNoBranch[2];
544
+ content = matchNoBranch[3];
545
+ }
546
+ const afsPath = this.buildPath(branch, matchPath);
547
+ if (processedFiles.has(afsPath))
548
+ continue;
549
+ processedFiles.add(afsPath);
550
+ entries.push({
551
+ id: afsPath,
552
+ path: afsPath,
553
+ summary: `Line ${lineNum}: ${content}`,
554
+ metadata: {
555
+ type: "file",
556
+ },
557
+ });
558
+ if (entries.length >= limit) {
559
+ break;
560
+ }
561
+ }
562
+ return {
563
+ data: entries,
564
+ message: entries.length >= limit ? `Results truncated to limit ${limit}` : undefined,
565
+ };
566
+ }
567
+ catch (error) {
568
+ // git grep returns exit code 1 if no matches found
569
+ if (error.message.includes("did not match any file(s)")) {
570
+ return { data: [] };
571
+ }
572
+ return { data: [], message: error.message };
573
+ }
574
+ }
575
+ /**
576
+ * Cleanup all worktrees (useful when unmounting)
577
+ */
578
+ async cleanup() {
579
+ for (const [_branch, worktreePath] of this.worktrees) {
580
+ try {
581
+ await this.git.raw(["worktree", "remove", worktreePath, "--force"]);
582
+ }
583
+ catch (_error) {
584
+ // Ignore errors during cleanup
585
+ }
586
+ }
587
+ this.worktrees.clear();
588
+ // Remove temp directory
589
+ try {
590
+ await rm(this.tempBase, { recursive: true, force: true });
591
+ }
592
+ catch {
593
+ // Ignore errors
594
+ }
595
+ }
596
+ }
597
+ const _typeCheck = AFSGit;
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@aigne/afs-git",
3
+ "version": "1.0.0",
4
+ "description": "AIGNE AFS module for git storage",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "author": "Arcblock <blocklet@arcblock.io> https://github.com/blocklet",
9
+ "homepage": "https://www.aigne.io/framework",
10
+ "license": "Elastic-2.0",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/AIGNE-io/aigne-framework"
14
+ },
15
+ "bugs": {
16
+ "url": "https://github.com/AIGNE-io/aigne-framework/issues"
17
+ },
18
+ "files": [
19
+ "lib/cjs",
20
+ "lib/dts",
21
+ "lib/esm",
22
+ "LICENSE",
23
+ "README.md",
24
+ "CHANGELOG.md"
25
+ ],
26
+ "type": "module",
27
+ "main": "./lib/cjs/index.js",
28
+ "module": "./lib/esm/index.js",
29
+ "types": "./lib/dts/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "import": "./lib/esm/index.js",
33
+ "require": "./lib/cjs/index.js",
34
+ "types": "./lib/dts/index.d.ts"
35
+ },
36
+ "./*": {
37
+ "import": "./lib/esm/*",
38
+ "require": "./lib/cjs/*",
39
+ "types": "./lib/dts/*"
40
+ }
41
+ },
42
+ "typesVersions": {
43
+ "*": {
44
+ ".": [
45
+ "./lib/dts/index.d.ts"
46
+ ]
47
+ }
48
+ },
49
+ "dependencies": {
50
+ "simple-git": "^3.27.0",
51
+ "zod": "^3.25.67",
52
+ "@aigne/afs": "^1.4.0-beta.10",
53
+ "@aigne/core": "^1.72.0-beta.24"
54
+ },
55
+ "devDependencies": {
56
+ "@types/bun": "^1.2.22",
57
+ "npm-run-all": "^4.1.5",
58
+ "rimraf": "^6.0.1",
59
+ "typescript": "^5.9.2",
60
+ "@aigne/test-utils": "^0.5.69-beta.24"
61
+ },
62
+ "scripts": {
63
+ "lint": "tsc --noEmit",
64
+ "build": "tsc --build scripts/tsconfig.build.json",
65
+ "clean": "rimraf lib test/coverage",
66
+ "test": "bun test",
67
+ "test:coverage": "bun test --coverage --coverage-reporter=lcov --coverage-reporter=text",
68
+ "postbuild": "node ../../scripts/post-build-lib.mjs"
69
+ }
70
+ }