@agent-api/sdk 1.0.6 → 1.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.
@@ -0,0 +1,1057 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { constants as fsConstants, watch as watchFS } from "node:fs";
3
+ import { access, copyFile, mkdir, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
4
+ import { homedir, tmpdir } from "node:os";
5
+ import path from "node:path";
6
+ import { localSkillFromDirectory } from "../local-skills.js";
7
+ export class LocalError extends Error {
8
+ code;
9
+ path;
10
+ constructor(code, message, options = {}) {
11
+ super(message, { cause: options.cause });
12
+ this.name = "LocalError";
13
+ this.code = code;
14
+ this.path = options.path;
15
+ }
16
+ }
17
+ export class LocalPathError extends LocalError {
18
+ constructor(message, path) {
19
+ super("local_path_error", message, { path });
20
+ this.name = "LocalPathError";
21
+ }
22
+ }
23
+ export class LocalIgnoredPathError extends LocalError {
24
+ constructor(path) {
25
+ super("local_ignored_path", `local workspace path is ignored: ${path}`, { path });
26
+ this.name = "LocalIgnoredPathError";
27
+ }
28
+ }
29
+ export class LocalFileTooLargeError extends LocalError {
30
+ constructor(path) {
31
+ super("local_file_too_large", `local file is too large: ${path}`, { path });
32
+ this.name = "LocalFileTooLargeError";
33
+ }
34
+ }
35
+ export class LocalNotTextFileError extends LocalError {
36
+ constructor(path) {
37
+ super("local_not_text_file", `local file must be text: ${path}`, { path });
38
+ this.name = "LocalNotTextFileError";
39
+ }
40
+ }
41
+ export class LocalConfigError extends LocalError {
42
+ constructor(message, path) {
43
+ super("local_config_error", message, { path });
44
+ this.name = "LocalConfigError";
45
+ }
46
+ }
47
+ export function createLocalRuntime(options) {
48
+ const appName = normalizeAppName(options.appName);
49
+ const dirs = localAppDirs({ ...options, appName });
50
+ const data = new LocalFileStore(dirs.data, { label: "data" });
51
+ const cache = new LocalFileStore(dirs.cache, { label: "cache" });
52
+ const logs = new LocalFileStore(dirs.logs, { label: "logs" });
53
+ const temp = new LocalFileStore(dirs.temp, { label: "temp" });
54
+ const configFiles = new LocalFileStore(dirs.config, { label: "config" });
55
+ return {
56
+ appName,
57
+ dirs,
58
+ files: data,
59
+ data,
60
+ cache,
61
+ logs,
62
+ temp,
63
+ config: new LocalConfigStore(configFiles),
64
+ skills: new LocalSkillStore(data.child("skills")),
65
+ workspaces: new LocalWorkspaceManager(),
66
+ workspace(root, workspaceOptions = {}) {
67
+ return new LocalWorkspace(root, workspaceOptions);
68
+ },
69
+ async ensure() {
70
+ await Promise.all([data.ensure(), cache.ensure(), logs.ensure(), temp.ensure(), configFiles.ensure()]);
71
+ },
72
+ };
73
+ }
74
+ export function localAppDirs(options) {
75
+ const appName = normalizeAppName(options.appName);
76
+ const env = options.env ?? process.env;
77
+ const platform = options.platform ?? process.platform;
78
+ const home = path.resolve(env.HOME || env.USERPROFILE || homedir());
79
+ const baseDir = options.baseDir ? path.resolve(options.baseDir) : "";
80
+ const authorSegment = sanitizePathSegment(options.appAuthor || appName);
81
+ const appSegment = sanitizePathSegment(appName);
82
+ const defaults = defaultDirs(platform, env, home, authorSegment, appSegment, baseDir);
83
+ return {
84
+ home,
85
+ data: path.resolve(options.dirs?.data ?? defaults.data),
86
+ config: path.resolve(options.dirs?.config ?? defaults.config),
87
+ cache: path.resolve(options.dirs?.cache ?? defaults.cache),
88
+ logs: path.resolve(options.dirs?.logs ?? defaults.logs),
89
+ temp: path.resolve(options.dirs?.temp ?? defaults.temp),
90
+ };
91
+ }
92
+ export class LocalFileStore {
93
+ root;
94
+ label;
95
+ constructor(root, options = {}) {
96
+ this.root = path.resolve(root);
97
+ this.label = options.label ?? "local";
98
+ }
99
+ child(relativePath, options = {}) {
100
+ return new LocalFileStore(this.resolvePath(relativePath), { label: options.label ?? this.label });
101
+ }
102
+ async ensure() {
103
+ await mkdir(this.root, { recursive: true });
104
+ }
105
+ resolvePath(relativePath = ".") {
106
+ const clean = normalizeRelativePath(relativePath);
107
+ const fullPath = path.resolve(this.root, clean);
108
+ assertInsideRoot(this.root, fullPath);
109
+ return fullPath;
110
+ }
111
+ relativePath(fullPath) {
112
+ const absolute = path.resolve(fullPath);
113
+ assertInsideRoot(this.root, absolute);
114
+ return toPortablePath(path.relative(this.root, absolute));
115
+ }
116
+ async exists(relativePath = ".") {
117
+ try {
118
+ await access(this.resolvePath(relativePath), fsConstants.F_OK);
119
+ return true;
120
+ }
121
+ catch {
122
+ return false;
123
+ }
124
+ }
125
+ async stat(relativePath = ".") {
126
+ const fullPath = this.resolvePath(relativePath);
127
+ const info = await stat(fullPath);
128
+ return {
129
+ path: toPortablePath(path.relative(this.root, fullPath)) || ".",
130
+ fullPath,
131
+ type: fileType(info),
132
+ size: info.size,
133
+ modifiedAt: info.mtime,
134
+ };
135
+ }
136
+ async mkdir(relativePath = ".") {
137
+ const fullPath = this.resolvePath(relativePath);
138
+ await mkdir(fullPath, { recursive: true });
139
+ return fullPath;
140
+ }
141
+ async list(relativePath = ".", options = {}) {
142
+ const base = this.resolvePath(relativePath);
143
+ const maxDepth = options.recursive ? options.maxDepth ?? Number.POSITIVE_INFINITY : 1;
144
+ const out = [];
145
+ await this.walk(this.root, base, base, out, maxDepth, options);
146
+ return out.sort((a, b) => a.path.localeCompare(b.path));
147
+ }
148
+ async listEntries(relativePath = ".", options = {}) {
149
+ const stats = await this.list(relativePath, { ...options, includeDirectories: options.includeDirectories ?? true });
150
+ return { object: "list", entries: stats.map(localEntryFromStat) };
151
+ }
152
+ async searchEntries(params) {
153
+ const query = params.query.trim().toLowerCase();
154
+ if (!query) {
155
+ throw new Error("query is required");
156
+ }
157
+ const limit = positiveInt(params.limit, 100);
158
+ const stats = await this.list(params.path ?? ".", { recursive: true, includeDirectories: true });
159
+ const entries = stats
160
+ .filter((item) => item.path.toLowerCase().includes(query))
161
+ .slice(0, limit)
162
+ .map(localEntryFromStat);
163
+ return { object: "list", entries };
164
+ }
165
+ async readText(relativePath) {
166
+ return await readFile(this.resolvePath(relativePath), "utf8");
167
+ }
168
+ async readJSON(relativePath) {
169
+ return JSON.parse(await this.readText(relativePath));
170
+ }
171
+ async readBytes(relativePath) {
172
+ return await readFile(this.resolvePath(relativePath));
173
+ }
174
+ async readFile(relativePath, params = {}) {
175
+ const fullPath = this.resolvePath(relativePath);
176
+ const info = await stat(fullPath);
177
+ if (!info.isFile()) {
178
+ throw new LocalPathError("local path is not a file", relativePath);
179
+ }
180
+ const maxBytes = params.maxBytes ?? Number.POSITIVE_INFINITY;
181
+ const raw = await readFile(fullPath);
182
+ const truncated = Number.isFinite(maxBytes) && raw.byteLength > maxBytes;
183
+ const content = truncated ? raw.subarray(0, maxBytes) : raw;
184
+ const portablePath = toPortablePath(path.relative(this.root, fullPath));
185
+ if ("format" in params && params.format === "raw") {
186
+ return {
187
+ path: portablePath,
188
+ size: info.size,
189
+ truncated,
190
+ content,
191
+ content_type: mimeTypeForPath(portablePath),
192
+ };
193
+ }
194
+ if (looksBinary(content)) {
195
+ return {
196
+ path: portablePath,
197
+ encoding: "base64",
198
+ mime_type: mimeTypeForPath(portablePath),
199
+ size: info.size,
200
+ truncated,
201
+ content_base64: Buffer.from(content).toString("base64"),
202
+ };
203
+ }
204
+ return {
205
+ path: portablePath,
206
+ encoding: "text",
207
+ mime_type: mimeTypeForPath(portablePath),
208
+ size: info.size,
209
+ truncated,
210
+ content: content.toString("utf8"),
211
+ };
212
+ }
213
+ async writeText(relativePath, content, options = {}) {
214
+ const fullPath = this.resolvePath(relativePath);
215
+ await mkdir(path.dirname(fullPath), { recursive: true });
216
+ if (options.atomic === false) {
217
+ await writeFile(fullPath, content, "utf8");
218
+ return fullPath;
219
+ }
220
+ await atomicWrite(fullPath, content);
221
+ return fullPath;
222
+ }
223
+ async writeJSON(relativePath, value, options = {}) {
224
+ const spaces = options.pretty === false ? 0 : 2;
225
+ const text = JSON.stringify(value, null, spaces) + "\n";
226
+ return await this.writeText(relativePath, text, { atomic: options.atomic });
227
+ }
228
+ async writeBytes(relativePath, content, options = {}) {
229
+ const fullPath = this.resolvePath(relativePath);
230
+ await mkdir(path.dirname(fullPath), { recursive: true });
231
+ if (options.atomic === false) {
232
+ await writeFile(fullPath, content);
233
+ return fullPath;
234
+ }
235
+ await atomicWrite(fullPath, content);
236
+ return fullPath;
237
+ }
238
+ async writeFile(relativePath, content, options = {}) {
239
+ const fullPath = typeof content === "string"
240
+ ? await this.writeText(relativePath, content, options)
241
+ : await this.writeBytes(relativePath, content, options);
242
+ const info = await stat(fullPath);
243
+ return { path: toPortablePath(path.relative(this.root, fullPath)), size: info.size };
244
+ }
245
+ async remove(relativePath) {
246
+ await rm(this.resolvePath(relativePath), { recursive: true, force: true });
247
+ }
248
+ async deletePath(relativePath) {
249
+ await this.remove(relativePath);
250
+ return { path: normalizeRelativePath(relativePath), recursive: true };
251
+ }
252
+ async createDirectory(relativePath = ".") {
253
+ await this.mkdir(relativePath);
254
+ return { path: normalizeRelativePath(relativePath) };
255
+ }
256
+ async copy(fromRelativePath, toRelativePath) {
257
+ const from = this.resolvePath(fromRelativePath);
258
+ const to = this.resolvePath(toRelativePath);
259
+ await mkdir(path.dirname(to), { recursive: true });
260
+ await copyFile(from, to);
261
+ return to;
262
+ }
263
+ async readLines(relativePath, params) {
264
+ const startLine = Math.trunc(params.startLine);
265
+ const endLine = params.endLine == null ? undefined : Math.trunc(params.endLine);
266
+ const file = await this.readFile(relativePath, { maxBytes: params.maxBytes, format: "raw" });
267
+ if (looksBinary(file.content)) {
268
+ throw new LocalNotTextFileError(relativePath);
269
+ }
270
+ const text = Buffer.from(file.content).toString("utf8");
271
+ const all = splitLines(text);
272
+ const selected = selectLineRange(all, startLine, endLine);
273
+ return {
274
+ path: file.path,
275
+ start_line: startLine,
276
+ end_line: selected.endLine,
277
+ total_lines: all.length,
278
+ lines: selected.lines,
279
+ file_truncated: file.truncated,
280
+ size: file.size,
281
+ };
282
+ }
283
+ async patchLines(relativePath, params) {
284
+ const startLine = Math.trunc(params.startLine);
285
+ const endLine = params.endLine == null ? undefined : Math.trunc(params.endLine);
286
+ const file = await this.readFile(relativePath, { maxBytes: params.maxBytes, format: "raw" });
287
+ if (file.truncated) {
288
+ throw new LocalFileTooLargeError(relativePath);
289
+ }
290
+ if (looksBinary(file.content)) {
291
+ throw new LocalNotTextFileError(relativePath);
292
+ }
293
+ const original = Buffer.from(file.content).toString("utf8");
294
+ const patched = patchLineRange(original, startLine, endLine, params.replacement ?? "");
295
+ const written = await this.writeFile(relativePath, patched.content);
296
+ return {
297
+ path: file.path,
298
+ start_line: startLine,
299
+ end_line: patched.endLine,
300
+ total_lines: patched.totalLines,
301
+ size: written.size,
302
+ };
303
+ }
304
+ async grep(params) {
305
+ const pattern = params.pattern.trim();
306
+ if (!pattern) {
307
+ throw new Error("pattern is required");
308
+ }
309
+ const limit = positiveInt(params.limit, 200);
310
+ const maxFiles = positiveInt(params.maxFiles, 500);
311
+ const maxBytesPerFile = positiveInt(params.maxBytesPerFile, 512 * 1024);
312
+ const maxLineLength = positiveInt(params.maxLineLength, 500);
313
+ const stats = await this.list(params.path ?? ".", { recursive: true, ignore: params.ignore });
314
+ const matches = [];
315
+ let filesScanned = 0;
316
+ let scanTruncated = false;
317
+ for (const item of stats) {
318
+ if (matches.length >= limit || filesScanned >= maxFiles) {
319
+ scanTruncated = true;
320
+ break;
321
+ }
322
+ if (item.type !== "file" || item.size > maxBytesPerFile || !isLikelyTextFile(item.path)) {
323
+ continue;
324
+ }
325
+ const raw = await readFile(item.fullPath);
326
+ if (looksBinary(raw)) {
327
+ continue;
328
+ }
329
+ filesScanned++;
330
+ const lines = splitLines(raw.toString("utf8"));
331
+ for (let i = 0; i < lines.length; i++) {
332
+ if (!lines[i].includes(pattern)) {
333
+ continue;
334
+ }
335
+ matches.push({
336
+ path: item.path,
337
+ line_number: i + 1,
338
+ line: lines[i].length > maxLineLength ? lines[i].slice(0, maxLineLength) : lines[i],
339
+ });
340
+ if (matches.length >= limit) {
341
+ scanTruncated = true;
342
+ break;
343
+ }
344
+ }
345
+ }
346
+ return { object: "list", matches, files_scanned: filesScanned, scan_truncated: scanTruncated };
347
+ }
348
+ async summarize(params = {}) {
349
+ const maxFiles = positiveInt(params.maxFiles, 2000);
350
+ const maxPreviews = positiveInt(params.maxPreviews, 20);
351
+ const previewBytes = positiveInt(params.previewBytes, 4096);
352
+ const topPaths = positiveInt(params.topPaths, 20);
353
+ const stats = await this.list(params.path ?? ".", { recursive: true, ignore: params.ignore });
354
+ const files = stats.filter((item) => item.type === "file").slice(0, maxFiles);
355
+ const scanTruncated = stats.filter((item) => item.type === "file").length > files.length;
356
+ const totalBytes = files.reduce((sum, item) => sum + item.size, 0);
357
+ const bySize = [...files].sort((a, b) => (b.size !== a.size ? b.size - a.size : a.path.localeCompare(b.path)));
358
+ const previews = [];
359
+ for (const item of bySize) {
360
+ if (previews.length >= maxPreviews) {
361
+ break;
362
+ }
363
+ if (!isLikelyTextFile(item.path) || item.size > previewBytes * 4) {
364
+ continue;
365
+ }
366
+ const raw = await readFile(item.fullPath);
367
+ if (looksBinary(raw)) {
368
+ continue;
369
+ }
370
+ const truncated = raw.byteLength > previewBytes;
371
+ previews.push({
372
+ path: item.path,
373
+ size: item.size,
374
+ preview: raw.subarray(0, previewBytes).toString("utf8"),
375
+ preview_truncated: truncated || undefined,
376
+ });
377
+ }
378
+ return {
379
+ summary_path: "",
380
+ file_count: files.length,
381
+ total_bytes: totalBytes,
382
+ top_paths_by_size: bySize.slice(0, topPaths).map((item) => `${item.path} (${item.size} bytes)`),
383
+ text_previews: previews,
384
+ generated_at_unix: Math.floor(Date.now() / 1000),
385
+ scan_truncated: scanTruncated,
386
+ };
387
+ }
388
+ async walk(storeRoot, scanRoot, dir, out, maxDepth, options) {
389
+ const entries = await readdir(dir, { withFileTypes: true });
390
+ for (const entry of entries) {
391
+ const fullPath = path.join(dir, entry.name);
392
+ const relativePath = toPortablePath(path.relative(storeRoot, fullPath));
393
+ if (ignored(relativePath, options.ignore)) {
394
+ continue;
395
+ }
396
+ const info = await stat(fullPath);
397
+ const item = {
398
+ path: relativePath,
399
+ fullPath,
400
+ type: fileType(info),
401
+ size: info.size,
402
+ modifiedAt: info.mtime,
403
+ };
404
+ if (entry.isDirectory()) {
405
+ if (options.includeDirectories) {
406
+ out.push(item);
407
+ }
408
+ const depth = toPortablePath(path.relative(scanRoot, fullPath)).split("/").filter(Boolean).length;
409
+ if (depth < maxDepth) {
410
+ await this.walk(storeRoot, scanRoot, fullPath, out, maxDepth, options);
411
+ }
412
+ continue;
413
+ }
414
+ out.push(item);
415
+ }
416
+ }
417
+ }
418
+ export class LocalConfigStore {
419
+ files;
420
+ constructor(files) {
421
+ this.files = files;
422
+ }
423
+ async read(name = "settings.json", fallback) {
424
+ try {
425
+ return await this.files.readJSON(name);
426
+ }
427
+ catch (error) {
428
+ if (error?.code === "ENOENT" && arguments.length >= 2) {
429
+ return fallback;
430
+ }
431
+ throw error;
432
+ }
433
+ }
434
+ async write(name, value) {
435
+ return await this.files.writeJSON(name, value);
436
+ }
437
+ async get(name, key, fallback) {
438
+ const config = await this.readRecord(name);
439
+ return (key in config ? config[key] : fallback);
440
+ }
441
+ async set(name, key, value) {
442
+ const config = await this.readRecord(name);
443
+ config[key] = value;
444
+ return await this.write(name, config);
445
+ }
446
+ async delete(name, key) {
447
+ const config = await this.readRecord(name);
448
+ delete config[key];
449
+ return await this.write(name, config);
450
+ }
451
+ async readRecord(name) {
452
+ const value = await this.read(name, {});
453
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
454
+ throw new LocalConfigError(`local config ${name} must contain a JSON object`, name);
455
+ }
456
+ return value;
457
+ }
458
+ }
459
+ export class LocalWorkspaceManager {
460
+ open(root, options = {}) {
461
+ return new LocalWorkspace(root, options);
462
+ }
463
+ }
464
+ export class LocalWorkspace {
465
+ root;
466
+ name;
467
+ metadata;
468
+ trusted;
469
+ files;
470
+ ignore;
471
+ gitignore;
472
+ maxFileBytes;
473
+ constructor(root, options = {}) {
474
+ this.root = path.resolve(root);
475
+ this.name = options.name?.trim() || path.basename(this.root) || "workspace";
476
+ this.metadata = { ...(options.metadata ?? {}) };
477
+ this.trusted = options.trusted ?? false;
478
+ this.files = new LocalFileStore(this.root, { label: "workspace" });
479
+ this.ignore = [...defaultWorkspaceIgnoreRules(), ...(options.ignore ?? [])];
480
+ this.gitignore = options.gitignore ?? true;
481
+ this.maxFileBytes = positiveInt(options.maxFileBytes, 10 * 1024 * 1024);
482
+ }
483
+ async ensure() {
484
+ await this.files.ensure();
485
+ if (this.gitignore) {
486
+ await this.loadIgnoreFiles();
487
+ }
488
+ }
489
+ async loadIgnoreFiles(files = [".gitignore"]) {
490
+ const loaded = [];
491
+ for (const file of files) {
492
+ try {
493
+ const text = await this.files.readText(file);
494
+ loaded.push(...parseIgnoreFile(text));
495
+ }
496
+ catch (error) {
497
+ if (error?.code !== "ENOENT") {
498
+ throw error;
499
+ }
500
+ }
501
+ }
502
+ this.ignore.push(...loaded);
503
+ return loaded;
504
+ }
505
+ child(relativePath, options = {}) {
506
+ return new LocalWorkspace(this.files.resolvePath(relativePath), {
507
+ name: options.name,
508
+ metadata: options.metadata ?? this.metadata,
509
+ trusted: options.trusted ?? this.trusted,
510
+ ignore: options.ignore ?? this.ignore,
511
+ gitignore: options.gitignore ?? this.gitignore,
512
+ maxFileBytes: options.maxFileBytes ?? this.maxFileBytes,
513
+ });
514
+ }
515
+ resolvePath(relativePath = ".") {
516
+ this.assertAllowed(relativePath);
517
+ return this.files.resolvePath(relativePath);
518
+ }
519
+ async list(relativePath = ".", options = {}) {
520
+ return await this.files.list(relativePath, this.scopedListOptions(options));
521
+ }
522
+ async listEntries(relativePath = ".", options = {}) {
523
+ return await this.files.listEntries(relativePath, this.scopedListOptions(options));
524
+ }
525
+ async searchEntries(params) {
526
+ const query = params.query.trim().toLowerCase();
527
+ if (!query) {
528
+ throw new Error("query is required");
529
+ }
530
+ const limit = positiveInt(params.limit, 100);
531
+ const stats = await this.list(params.path ?? ".", { recursive: true, includeDirectories: true });
532
+ return {
533
+ object: "list",
534
+ entries: stats
535
+ .filter((item) => item.path.toLowerCase().includes(query))
536
+ .slice(0, limit)
537
+ .map(localEntryFromStat),
538
+ };
539
+ }
540
+ async readFile(relativePath, params = {}) {
541
+ this.assertAllowed(relativePath);
542
+ return await this.files.readFile(relativePath, { maxBytes: params.maxBytes ?? this.maxFileBytes, ...params });
543
+ }
544
+ async writeFile(relativePath, content, options = {}) {
545
+ this.assertAllowed(relativePath);
546
+ return await this.files.writeFile(relativePath, content, options);
547
+ }
548
+ async readText(relativePath) {
549
+ this.assertAllowed(relativePath);
550
+ return await this.files.readText(relativePath);
551
+ }
552
+ async writeText(relativePath, content, options = {}) {
553
+ this.assertAllowed(relativePath);
554
+ return await this.files.writeText(relativePath, content, options);
555
+ }
556
+ async deletePath(relativePath) {
557
+ this.assertAllowed(relativePath);
558
+ return await this.files.deletePath(relativePath);
559
+ }
560
+ async createDirectory(relativePath = ".") {
561
+ this.assertAllowed(relativePath);
562
+ return await this.files.createDirectory(relativePath);
563
+ }
564
+ async readLines(relativePath, params) {
565
+ this.assertAllowed(relativePath);
566
+ return await this.files.readLines(relativePath, { maxBytes: params.maxBytes ?? this.maxFileBytes, ...params });
567
+ }
568
+ async previewPatchLines(relativePath, params) {
569
+ this.assertAllowed(relativePath);
570
+ const lines = await this.readLines(relativePath, {
571
+ startLine: params.startLine,
572
+ endLine: params.endLine,
573
+ maxBytes: params.maxBytes ?? this.maxFileBytes,
574
+ });
575
+ const file = await this.files.readFile(relativePath, { maxBytes: params.maxBytes ?? this.maxFileBytes, format: "raw" });
576
+ if (file.truncated) {
577
+ throw new Error("local file is too large to patch");
578
+ }
579
+ if (looksBinary(file.content)) {
580
+ throw new Error("local file must be text");
581
+ }
582
+ const patched = patchLineRange(Buffer.from(file.content).toString("utf8"), params.startLine, params.endLine, params.replacement ?? "");
583
+ return {
584
+ path: lines.path,
585
+ start_line: lines.start_line,
586
+ end_line: lines.end_line,
587
+ total_lines: patched.totalLines,
588
+ before: lines.lines,
589
+ after: params.replacement === "" ? [] : splitLines(params.replacement ?? ""),
590
+ };
591
+ }
592
+ async patchLines(relativePath, params) {
593
+ this.assertAllowed(relativePath);
594
+ return await this.files.patchLines(relativePath, { maxBytes: params.maxBytes ?? this.maxFileBytes, ...params });
595
+ }
596
+ async previewEdits(edits) {
597
+ const previews = [];
598
+ for (const edit of edits) {
599
+ await this.assertExpectedHash(edit.path, edit.expectedSha256);
600
+ previews.push(await this.previewPatchLines(edit.path, edit));
601
+ }
602
+ return { edits: edits.map((edit) => ({ ...edit })), previews };
603
+ }
604
+ async applyEdits(edits) {
605
+ const backups = [];
606
+ const applied = [];
607
+ try {
608
+ for (const edit of edits) {
609
+ await this.assertExpectedHash(edit.path, edit.expectedSha256);
610
+ backups.push({ path: edit.path, content: await this.readText(edit.path) });
611
+ applied.push(await this.patchLines(edit.path, edit));
612
+ }
613
+ }
614
+ catch (error) {
615
+ for (const backup of backups.reverse()) {
616
+ await this.writeText(backup.path, backup.content);
617
+ }
618
+ throw error;
619
+ }
620
+ return { applied, backups };
621
+ }
622
+ classifyPath(relativePath) {
623
+ return classifyLocalPathSensitivity(relativePath);
624
+ }
625
+ async grep(params) {
626
+ return await this.files.grep({ ...params, ignore: this.mergeIgnore(params.ignore) });
627
+ }
628
+ async summarize(params = {}) {
629
+ return await this.files.summarize({ ...params, ignore: this.mergeIgnore(params.ignore) });
630
+ }
631
+ async snapshot(params = {}) {
632
+ const maxBytes = positiveInt(params.maxBytesPerFile, this.maxFileBytes);
633
+ const shouldHash = params.hash ?? true;
634
+ const stats = await this.list(params.path ?? ".", { recursive: true });
635
+ const files = [];
636
+ for (const item of stats) {
637
+ if (item.type !== "file") {
638
+ continue;
639
+ }
640
+ const snap = {
641
+ path: item.path,
642
+ size: item.size,
643
+ modified_at_unix: Math.floor(item.modifiedAt.getTime() / 1000),
644
+ };
645
+ if (shouldHash && item.size <= maxBytes) {
646
+ snap.sha256 = createHash("sha256").update(await readFile(item.fullPath)).digest("hex");
647
+ }
648
+ files.push(snap);
649
+ }
650
+ return {
651
+ root: this.root,
652
+ name: this.name,
653
+ generated_at_unix: Math.floor(Date.now() / 1000),
654
+ files,
655
+ };
656
+ }
657
+ diff(before, after) {
658
+ const beforeByPath = new Map(before.files.map((file) => [file.path, file]));
659
+ const afterByPath = new Map(after.files.map((file) => [file.path, file]));
660
+ const added = [];
661
+ const modified = [];
662
+ const deleted = [];
663
+ const unchanged = [];
664
+ for (const afterFile of after.files) {
665
+ const beforeFile = beforeByPath.get(afterFile.path);
666
+ if (!beforeFile) {
667
+ added.push(afterFile);
668
+ }
669
+ else if (snapshotFileChanged(beforeFile, afterFile)) {
670
+ modified.push({ before: beforeFile, after: afterFile });
671
+ }
672
+ else {
673
+ unchanged.push(afterFile);
674
+ }
675
+ }
676
+ for (const beforeFile of before.files) {
677
+ if (!afterByPath.has(beforeFile.path)) {
678
+ deleted.push(beforeFile);
679
+ }
680
+ }
681
+ return { added, modified, deleted, unchanged };
682
+ }
683
+ watch(onEvent, options = {}) {
684
+ const watcher = watchFS(this.root, { recursive: options.recursive ?? process.platform !== "linux" }, (eventType, filename) => {
685
+ const rel = filename ? toPortablePath(String(filename)) : ".";
686
+ if (ignored(rel, this.ignore)) {
687
+ return;
688
+ }
689
+ onEvent({ type: eventType === "rename" ? "rename" : "change", path: rel });
690
+ });
691
+ return { close: () => watcher.close() };
692
+ }
693
+ scopedListOptions(options) {
694
+ return { ...options, ignore: this.mergeIgnore(options.ignore) };
695
+ }
696
+ mergeIgnore(ignore) {
697
+ return [...this.ignore, ...(ignore ?? [])];
698
+ }
699
+ assertAllowed(relativePath) {
700
+ const rel = normalizeRelativePath(relativePath);
701
+ if (rel !== "." && ignored(rel, this.ignore)) {
702
+ throw new LocalIgnoredPathError(rel);
703
+ }
704
+ }
705
+ async assertExpectedHash(relativePath, expectedSha256) {
706
+ if (!expectedSha256) {
707
+ return;
708
+ }
709
+ const raw = await readFile(this.files.resolvePath(relativePath));
710
+ const actual = createHash("sha256").update(raw).digest("hex");
711
+ if (actual !== expectedSha256) {
712
+ throw new LocalError("local_edit_conflict", `local file changed before edit: ${relativePath}`, { path: relativePath });
713
+ }
714
+ }
715
+ }
716
+ export class LocalSkillStore {
717
+ files;
718
+ constructor(files) {
719
+ this.files = files;
720
+ }
721
+ async fromDirectory(rootDir, options = {}) {
722
+ return await localSkillFromDirectory(rootDir, options);
723
+ }
724
+ async discover(options = {}) {
725
+ const roots = options.roots?.length ? options.roots : [this.files.root];
726
+ const skillDirs = [];
727
+ for (const root of roots) {
728
+ skillDirs.push(...await discoverSkillDirectories(path.resolve(root), options));
729
+ }
730
+ const unique = [...new Set(skillDirs)].sort((a, b) => a.localeCompare(b));
731
+ return await Promise.all(unique.map((dir) => localSkillFromDirectory(dir, options)));
732
+ }
733
+ }
734
+ function defaultDirs(platform, env, home, authorSegment, appSegment, baseDir) {
735
+ if (baseDir) {
736
+ return {
737
+ data: path.join(baseDir, "data"),
738
+ config: path.join(baseDir, "config"),
739
+ cache: path.join(baseDir, "cache"),
740
+ logs: path.join(baseDir, "logs"),
741
+ temp: path.join(baseDir, "tmp"),
742
+ };
743
+ }
744
+ if (platform === "darwin") {
745
+ return {
746
+ data: path.join(home, "Library", "Application Support", appSegment),
747
+ config: path.join(home, "Library", "Application Support", appSegment),
748
+ cache: path.join(home, "Library", "Caches", appSegment),
749
+ logs: path.join(home, "Library", "Logs", appSegment),
750
+ temp: path.join(tmpdir(), appSegment),
751
+ };
752
+ }
753
+ if (platform === "win32") {
754
+ const roaming = env.APPDATA || path.join(home, "AppData", "Roaming");
755
+ const local = env.LOCALAPPDATA || path.join(home, "AppData", "Local");
756
+ return {
757
+ data: path.join(roaming, authorSegment, appSegment),
758
+ config: path.join(roaming, authorSegment, appSegment),
759
+ cache: path.join(local, authorSegment, appSegment, "Cache"),
760
+ logs: path.join(local, authorSegment, appSegment, "Logs"),
761
+ temp: path.join(tmpdir(), appSegment),
762
+ };
763
+ }
764
+ return {
765
+ data: path.join(env.XDG_DATA_HOME || path.join(home, ".local", "share"), appSegment),
766
+ config: path.join(env.XDG_CONFIG_HOME || path.join(home, ".config"), appSegment),
767
+ cache: path.join(env.XDG_CACHE_HOME || path.join(home, ".cache"), appSegment),
768
+ logs: path.join(env.XDG_STATE_HOME || path.join(home, ".local", "state"), appSegment, "logs"),
769
+ temp: path.join(tmpdir(), appSegment),
770
+ };
771
+ }
772
+ async function discoverSkillDirectories(root, options) {
773
+ try {
774
+ const info = await stat(root);
775
+ if (!info.isDirectory()) {
776
+ return [];
777
+ }
778
+ }
779
+ catch (error) {
780
+ if (error?.code === "ENOENT") {
781
+ return [];
782
+ }
783
+ throw error;
784
+ }
785
+ const out = [];
786
+ const maxDepth = options.recursive ? options.maxDepth ?? Number.POSITIVE_INFINITY : 1;
787
+ await walkSkillDirectories(root, root, out, maxDepth);
788
+ return out;
789
+ }
790
+ async function walkSkillDirectories(root, dir, out, maxDepth) {
791
+ if (await fileExists(path.join(dir, "SKILL.md"))) {
792
+ out.push(dir);
793
+ }
794
+ const relative = path.relative(root, dir);
795
+ const depth = relative ? relative.split(path.sep).length : 0;
796
+ if (depth >= maxDepth) {
797
+ return;
798
+ }
799
+ const entries = await readdir(dir, { withFileTypes: true });
800
+ for (const entry of entries) {
801
+ if (!entry.isDirectory() || ignoredDirectoryName(entry.name)) {
802
+ continue;
803
+ }
804
+ await walkSkillDirectories(root, path.join(dir, entry.name), out, maxDepth);
805
+ }
806
+ }
807
+ async function fileExists(fullPath) {
808
+ try {
809
+ const info = await stat(fullPath);
810
+ return info.isFile();
811
+ }
812
+ catch {
813
+ return false;
814
+ }
815
+ }
816
+ function normalizeAppName(appName) {
817
+ const trimmed = appName.trim();
818
+ if (!trimmed) {
819
+ throw new LocalConfigError("appName is required");
820
+ }
821
+ return trimmed;
822
+ }
823
+ function sanitizePathSegment(value) {
824
+ return value
825
+ .trim()
826
+ .toLowerCase()
827
+ .replace(/[^a-z0-9._-]+/g, "-")
828
+ .replace(/^-+|-+$/g, "") || "agent-api";
829
+ }
830
+ function normalizeRelativePath(value) {
831
+ const trimmed = value.trim();
832
+ if (!trimmed || trimmed === ".") {
833
+ return ".";
834
+ }
835
+ if (path.isAbsolute(trimmed)) {
836
+ throw new LocalPathError("local path must be relative", value);
837
+ }
838
+ return trimmed;
839
+ }
840
+ function assertInsideRoot(root, fullPath) {
841
+ const relative = path.relative(root, fullPath);
842
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
843
+ throw new LocalPathError("local path must stay inside the store root", fullPath);
844
+ }
845
+ }
846
+ function toPortablePath(value) {
847
+ return value.split(path.sep).join("/");
848
+ }
849
+ function fileType(info) {
850
+ if (info.isFile())
851
+ return "file";
852
+ if (info.isDirectory())
853
+ return "directory";
854
+ if (info.isSymbolicLink())
855
+ return "symlink";
856
+ return "other";
857
+ }
858
+ function localEntryFromStat(item) {
859
+ return {
860
+ path: item.path,
861
+ is_dir: item.type === "directory",
862
+ size: item.type === "directory" ? 0 : item.size,
863
+ modified_at_unix: Math.floor(item.modifiedAt.getTime() / 1000),
864
+ };
865
+ }
866
+ async function atomicWrite(fullPath, content) {
867
+ const tmpPath = path.join(path.dirname(fullPath), `.${path.basename(fullPath)}.${process.pid}.${randomUUID()}.tmp`);
868
+ await writeFile(tmpPath, content);
869
+ await rename(tmpPath, fullPath);
870
+ }
871
+ function ignored(relativePath, ignore) {
872
+ if (!ignore?.length) {
873
+ return false;
874
+ }
875
+ return ignore.some((rule) => {
876
+ if (typeof rule === "string") {
877
+ return relativePath === rule || relativePath.startsWith(`${rule}/`) || relativePath.endsWith(`/${rule}`) || relativePath.includes(`/${rule}/`);
878
+ }
879
+ if (rule instanceof RegExp)
880
+ return rule.test(relativePath);
881
+ return rule(relativePath);
882
+ });
883
+ }
884
+ function ignoredDirectoryName(name) {
885
+ return name === ".git" || name === "node_modules" || name === "__pycache__";
886
+ }
887
+ function defaultWorkspaceIgnoreRules() {
888
+ return [
889
+ ".git",
890
+ "node_modules",
891
+ "__pycache__",
892
+ ".DS_Store",
893
+ "dist",
894
+ "build",
895
+ "coverage",
896
+ ".next",
897
+ ".turbo",
898
+ ".cache",
899
+ /\.pyc$/,
900
+ /\.pyo$/,
901
+ /\.class$/,
902
+ /\.log$/,
903
+ ];
904
+ }
905
+ function parseIgnoreFile(text) {
906
+ const rules = [];
907
+ for (const rawLine of text.split(/\r?\n/)) {
908
+ let line = rawLine.trim();
909
+ if (!line || line.startsWith("#")) {
910
+ continue;
911
+ }
912
+ if (line.startsWith("!")) {
913
+ continue;
914
+ }
915
+ line = line.replace(/\\/g, "/");
916
+ if (line.startsWith("/")) {
917
+ line = line.slice(1);
918
+ }
919
+ if (line.endsWith("/")) {
920
+ line = line.slice(0, -1);
921
+ }
922
+ if (!line || line.includes("*")) {
923
+ rules.push(globIgnoreRule(line));
924
+ continue;
925
+ }
926
+ rules.push(line);
927
+ }
928
+ return rules;
929
+ }
930
+ function globIgnoreRule(pattern) {
931
+ const escaped = pattern
932
+ .split("*")
933
+ .map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&"))
934
+ .join(".*");
935
+ const regex = new RegExp(`(^|/)${escaped}($|/)`);
936
+ return (relativePath) => regex.test(relativePath);
937
+ }
938
+ function snapshotFileChanged(before, after) {
939
+ if (before.sha256 || after.sha256) {
940
+ return before.sha256 !== after.sha256;
941
+ }
942
+ return before.size !== after.size || before.modified_at_unix !== after.modified_at_unix;
943
+ }
944
+ export function classifyLocalPathSensitivity(relativePath) {
945
+ const rel = normalizeRelativePath(relativePath);
946
+ const lower = rel.toLowerCase();
947
+ const base = path.basename(lower);
948
+ if (base === ".env" ||
949
+ base.startsWith(".env.") ||
950
+ lower.includes("id_rsa") ||
951
+ lower.includes("id_ed25519") ||
952
+ lower.endsWith(".pem") ||
953
+ lower.endsWith(".key") ||
954
+ lower.endsWith(".p12") ||
955
+ lower.endsWith(".pfx")) {
956
+ return { path: rel, sensitivity: "secret", reason: "path commonly contains credentials or private keys" };
957
+ }
958
+ if (lower.includes("secret") ||
959
+ lower.includes("token") ||
960
+ lower.includes("credential") ||
961
+ lower.includes("password") ||
962
+ lower.endsWith(".crt") ||
963
+ lower.endsWith(".cert")) {
964
+ return { path: rel, sensitivity: "sensitive", reason: "path name suggests sensitive material" };
965
+ }
966
+ return { path: rel, sensitivity: "normal" };
967
+ }
968
+ function positiveInt(value, fallback) {
969
+ const parsed = Number(value);
970
+ return Number.isFinite(parsed) && parsed > 0 ? Math.trunc(parsed) : fallback;
971
+ }
972
+ function looksBinary(content) {
973
+ return content.includes(0);
974
+ }
975
+ const textExtensions = new Set([
976
+ ".txt", ".md", ".markdown", ".json", ".yaml", ".yml", ".toml", ".xml", ".csv",
977
+ ".py", ".go", ".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx", ".html", ".htm",
978
+ ".css", ".scss", ".sh", ".bash", ".zsh", ".sql", ".env", ".ini", ".cfg",
979
+ ".conf", ".log", ".rst", ".adoc", ".gradle", ".properties", ".mod", ".sum",
980
+ ".dockerfile",
981
+ ]);
982
+ function isLikelyTextFile(relativePath) {
983
+ const lower = relativePath.toLowerCase();
984
+ const ext = path.extname(lower);
985
+ return !ext || textExtensions.has(ext) || lower.endsWith("dockerfile");
986
+ }
987
+ function mimeTypeForPath(relativePath) {
988
+ const ext = path.extname(relativePath).toLowerCase();
989
+ switch (ext) {
990
+ case ".json":
991
+ return "application/json";
992
+ case ".md":
993
+ case ".markdown":
994
+ return "text/markdown";
995
+ case ".html":
996
+ case ".htm":
997
+ return "text/html";
998
+ case ".css":
999
+ return "text/css";
1000
+ case ".csv":
1001
+ return "text/csv";
1002
+ case ".xml":
1003
+ return "application/xml";
1004
+ case ".png":
1005
+ return "image/png";
1006
+ case ".jpg":
1007
+ case ".jpeg":
1008
+ return "image/jpeg";
1009
+ case ".gif":
1010
+ return "image/gif";
1011
+ case ".svg":
1012
+ return "image/svg+xml";
1013
+ case ".pdf":
1014
+ return "application/pdf";
1015
+ default:
1016
+ return isLikelyTextFile(relativePath) ? "text/plain" : "application/octet-stream";
1017
+ }
1018
+ }
1019
+ function splitLines(content) {
1020
+ if (content.length === 0) {
1021
+ return [""];
1022
+ }
1023
+ let text = content;
1024
+ if (text.endsWith("\n")) {
1025
+ text = text.slice(0, -1);
1026
+ if (text === "") {
1027
+ return [""];
1028
+ }
1029
+ }
1030
+ return text.split("\n");
1031
+ }
1032
+ function selectLineRange(lines, startLine, endLine) {
1033
+ if (startLine < 1) {
1034
+ throw new Error("startLine must be >= 1");
1035
+ }
1036
+ const total = lines.length || 1;
1037
+ const end = endLine == null || endLine <= 0 ? total : Math.min(endLine, total);
1038
+ if (startLine > total || end < startLine) {
1039
+ throw new Error("invalid line range");
1040
+ }
1041
+ return { lines: lines.slice(startLine - 1, end), endLine: end };
1042
+ }
1043
+ function patchLineRange(content, startLine, endLine, replacement) {
1044
+ const lines = splitLines(content);
1045
+ const selected = selectLineRange(lines, startLine, endLine);
1046
+ const replacementLines = replacement === "" ? [] : splitLines(replacement);
1047
+ const patched = [
1048
+ ...lines.slice(0, startLine - 1),
1049
+ ...replacementLines,
1050
+ ...lines.slice(selected.endLine),
1051
+ ];
1052
+ let out = patched.join("\n");
1053
+ if (content.endsWith("\n")) {
1054
+ out += "\n";
1055
+ }
1056
+ return { content: out, endLine: selected.endLine, totalLines: patched.length };
1057
+ }