@aiwg/cli 2026.7.19 → 2026.7.21

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 (88) hide show
  1. package/README.md +397 -385
  2. package/dist/src/api/index.d.ts +1 -0
  3. package/dist/src/api/index.js +1 -0
  4. package/dist/src/artifacts/browser-export.js +7 -0
  5. package/dist/src/artifacts/citation-parser.js +96 -35
  6. package/dist/src/artifacts/cli.js +59 -5
  7. package/dist/src/artifacts/discover-facets.js +15 -0
  8. package/dist/src/artifacts/discovery-eval.js +290 -0
  9. package/dist/src/artifacts/fortemi-core-query-adapter.js +1 -1
  10. package/dist/src/artifacts/fortemi-shard-export.js +1 -1
  11. package/dist/src/artifacts/index-builder.js +54 -17
  12. package/dist/src/artifacts/query-engine.js +10 -6
  13. package/dist/src/artifacts/state-transfer.js +27 -0
  14. package/dist/src/artifacts/stats.js +8 -0
  15. package/dist/src/cli/cli-extension-loader.js +73 -0
  16. package/dist/src/cli/handlers/help.js +2 -1
  17. package/dist/src/cli/handlers/index.js +6 -2
  18. package/dist/src/cli/handlers/resource-versions.js +247 -0
  19. package/dist/src/cli/handlers/sessions.js +966 -0
  20. package/dist/src/cli/handlers/skill-lint.js +49 -45
  21. package/dist/src/cli/handlers/subcommands.js +55 -3
  22. package/dist/src/cli/handlers/use.js +154 -59
  23. package/dist/src/cli/handlers/utilities.js +49 -34
  24. package/dist/src/cli/skill-usage.js +146 -24
  25. package/dist/src/config/cli.js +13 -9
  26. package/dist/src/config/project-artifacts-runtime.mjs +68 -0
  27. package/dist/src/config/project-artifacts.js +1 -68
  28. package/dist/src/extensions/commands/definitions.js +65 -2
  29. package/dist/src/extensions/manifest.js +29 -0
  30. package/dist/src/extensions/project-local-discovery.js +86 -2
  31. package/dist/src/extensions/project-local-remove.js +52 -56
  32. package/dist/src/extensions/shadow-resolver.js +3 -1
  33. package/dist/src/plugins/standalone-packager.js +143 -0
  34. package/dist/src/resources/cache-cleanup.js +67 -0
  35. package/dist/src/resources/doctor.js +107 -0
  36. package/dist/src/resources/lockfile.js +125 -0
  37. package/dist/src/resources/resolver.js +133 -0
  38. package/dist/src/resources/web-release.d.ts +8 -0
  39. package/dist/src/resources/web-release.js +159 -1
  40. package/dist/src/sessions/adapters/claude.js +357 -0
  41. package/dist/src/sessions/adapters/codex.js +521 -0
  42. package/dist/src/sessions/adapters/copilot.js +226 -0
  43. package/dist/src/sessions/adapters/cursor.js +372 -0
  44. package/dist/src/sessions/adapters/factory.js +345 -0
  45. package/dist/src/sessions/adapters/generic.js +225 -0
  46. package/dist/src/sessions/adapters/hermes.js +341 -0
  47. package/dist/src/sessions/adapters/openclaw.js +381 -0
  48. package/dist/src/sessions/adapters/opencode.js +454 -0
  49. package/dist/src/sessions/adapters/openhuman.js +315 -0
  50. package/dist/src/sessions/adapters/warp.js +160 -0
  51. package/dist/src/sessions/adapters/windsurf.js +212 -0
  52. package/dist/src/sessions/candidates.js +210 -0
  53. package/dist/src/sessions/contracts.js +310 -0
  54. package/dist/src/sessions/discovery.js +51 -0
  55. package/dist/src/sessions/fixtures.js +12 -0
  56. package/dist/src/sessions/importer.js +315 -0
  57. package/dist/src/sessions/index.js +25 -0
  58. package/dist/src/sessions/knowledge-shard.js +61 -0
  59. package/dist/src/sessions/optional-backends.js +238 -0
  60. package/dist/src/sessions/policy.js +192 -0
  61. package/dist/src/sessions/ports.js +2 -0
  62. package/dist/src/sessions/promotion.js +367 -0
  63. package/dist/src/sessions/readers.js +176 -0
  64. package/dist/src/sessions/repository.js +1551 -0
  65. package/dist/src/skills/adapters/agent-skills.js +59 -0
  66. package/dist/src/skills/adapters/local.js +19 -1
  67. package/dist/src/skills/agent-skills.js +249 -0
  68. package/dist/src/skills/cli.js +463 -7
  69. package/dist/src/skills/deployer.js +554 -0
  70. package/dist/src/skills/doctor.js +105 -0
  71. package/dist/src/skills/exporter.js +382 -0
  72. package/dist/src/skills/importer.js +921 -0
  73. package/dist/src/skills/registry.js +19 -0
  74. package/dist/src/skills/validator.js +323 -0
  75. package/dist/src/smiths/context-pipeline/aiwg-md.js +5 -1
  76. package/dist/src/smiths/context-pipeline/claude-hook.js +21 -1
  77. package/dist/src/smiths/context-pipeline/finalization.js +5 -3
  78. package/dist/src/smiths/context-pipeline/generator.js +4 -1
  79. package/dist/src/smiths/context-pipeline/parallelism-section.js +34 -1
  80. package/dist/src/smiths/context-pipeline/workspace-context.js +15 -3
  81. package/dist/src/smiths/mcpsmith/example.js +3 -1
  82. package/dist/src/smiths/mcpsmith/generator.js +3 -1
  83. package/dist/src/smiths/toolsmith/runtime-discovery.mjs +2 -1
  84. package/dist/src/storage/cli.js +3 -2
  85. package/dist/src/storage/subsystem-cli.js +7 -2
  86. package/dist/src/update/notifier.mjs +1 -1
  87. package/dist/src/update/service.mjs +123 -0
  88. package/package.json +3 -2
@@ -0,0 +1,921 @@
1
+ /**
2
+ * Secure Agent Skills import boundary.
3
+ *
4
+ * Sources are materialized as inert bytes, validated in staging, and promoted
5
+ * atomically into the project AIWG artifact root. No source script is loaded or
6
+ * executed by this module.
7
+ *
8
+ * @implements #1877
9
+ */
10
+ import fs from 'node:fs';
11
+ import os from 'node:os';
12
+ import path from 'node:path';
13
+ import { spawnSync } from 'node:child_process';
14
+ import { createHash, randomUUID } from 'node:crypto';
15
+ import { fileURLToPath } from 'node:url';
16
+ import { projectAiwgPath } from '../config/project-artifacts.js';
17
+ import { listProviderDefinitions, resolveProviderPathValue, } from '../providers/provider-definitions.js';
18
+ import { AGENT_SKILLS_BASELINE, } from './agent-skills.js';
19
+ import { validateAgentSkillContent } from './validator.js';
20
+ export const DEFAULT_AGENT_SKILL_IMPORT_LIMITS = Object.freeze({
21
+ maxFiles: 1_000,
22
+ maxFileBytes: 5 * 1024 * 1024,
23
+ maxTotalBytes: 20 * 1024 * 1024,
24
+ maxDepth: 20,
25
+ maxGitRepositoryBytes: 100 * 1024 * 1024,
26
+ });
27
+ export class AgentSkillImportError extends Error {
28
+ code;
29
+ diagnostics;
30
+ constructor(code, message, diagnostics = []) {
31
+ super(message);
32
+ this.name = 'AgentSkillImportError';
33
+ this.code = code;
34
+ this.diagnostics = diagnostics;
35
+ }
36
+ }
37
+ function diagnostic(code, file, yamlPath, message, remediation, severity = 'error') {
38
+ return {
39
+ code,
40
+ severity,
41
+ file,
42
+ yamlPath,
43
+ message,
44
+ upstreamBaseline: AGENT_SKILLS_BASELINE.revision,
45
+ remediation,
46
+ };
47
+ }
48
+ function fail(code, message, file = '.', remediation = 'Correct the source and retry the import.') {
49
+ throw new AgentSkillImportError(code, message, [
50
+ diagnostic(code, file, '$', message, remediation),
51
+ ]);
52
+ }
53
+ function pathContains(parent, child) {
54
+ const relative = path.relative(parent, child);
55
+ return relative === ''
56
+ || (!relative.startsWith('..') && !path.isAbsolute(relative));
57
+ }
58
+ function normalizedLimits(overrides) {
59
+ const limits = {
60
+ ...DEFAULT_AGENT_SKILL_IMPORT_LIMITS,
61
+ ...overrides,
62
+ };
63
+ for (const [key, value] of Object.entries(limits)) {
64
+ if (!Number.isSafeInteger(value) || value <= 0) {
65
+ throw new AgentSkillImportError('AS_IMPORT_LIMIT_INVALID', `${key} must be a positive safe integer`);
66
+ }
67
+ }
68
+ return limits;
69
+ }
70
+ function validateRelativePath(relativePath) {
71
+ if (relativePath.length === 0
72
+ || /[\u0000-\u001f\u007f]/.test(relativePath)
73
+ || relativePath.includes('\\')
74
+ || path.posix.isAbsolute(relativePath)) {
75
+ fail('AS_IMPORT_PATH_INVALID', `unsafe source path "${relativePath}"`, relativePath || '.', 'Use non-empty relative POSIX paths without backslashes or control characters.');
76
+ }
77
+ const normalized = path.posix.normalize(relativePath);
78
+ if (normalized !== relativePath
79
+ || normalized === '..'
80
+ || normalized.startsWith('../')
81
+ || normalized.split('/').some((segment) => segment === '.' || segment === '..')) {
82
+ fail('AS_IMPORT_PATH_TRAVERSAL', `source path escapes or changes under normalization: "${relativePath}"`, relativePath, 'Remove traversal and redundant path segments.');
83
+ }
84
+ return normalized;
85
+ }
86
+ function enforceFileLimits(file, files, totalBytes, limits) {
87
+ if (files > limits.maxFiles) {
88
+ fail('AS_IMPORT_FILE_COUNT', `source exceeds the ${limits.maxFiles}-file limit`, file.path, 'Reduce the resource tree or raise the explicit import limit.');
89
+ }
90
+ if (file.bytes.length > limits.maxFileBytes) {
91
+ fail('AS_IMPORT_FILE_SIZE', `"${file.path}" exceeds the ${limits.maxFileBytes}-byte file limit`, file.path, 'Reduce the file or raise the explicit import limit.');
92
+ }
93
+ if (totalBytes > limits.maxTotalBytes) {
94
+ fail('AS_IMPORT_TOTAL_SIZE', `source exceeds the ${limits.maxTotalBytes}-byte total limit`, file.path, 'Reduce the resource tree or raise the explicit import limit.');
95
+ }
96
+ }
97
+ function collectLocalTree(sourceRoot, limits) {
98
+ const rootStat = fs.lstatSync(sourceRoot);
99
+ if (rootStat.isSymbolicLink()) {
100
+ fail('AS_IMPORT_SYMLINK', 'the import root cannot be a symbolic link', sourceRoot, 'Import from a real directory whose tree contains no symbolic links.');
101
+ }
102
+ if (!rootStat.isDirectory()) {
103
+ fail('AS_IMPORT_SOURCE_TYPE', 'the local import source must be a directory', sourceRoot, 'Pass a directory containing SKILL.md.');
104
+ }
105
+ const realRoot = fs.realpathSync.native(sourceRoot);
106
+ const entries = [];
107
+ let fileCount = 0;
108
+ let totalBytes = 0;
109
+ const walk = (current, relativeDirectory) => {
110
+ const children = fs.readdirSync(current, { withFileTypes: true })
111
+ .sort((left, right) => left.name.localeCompare(right.name));
112
+ for (const child of children) {
113
+ const relativePath = validateRelativePath(relativeDirectory
114
+ ? `${relativeDirectory}/${child.name}`
115
+ : child.name);
116
+ const depth = relativePath.split('/').length;
117
+ if (depth > limits.maxDepth) {
118
+ fail('AS_IMPORT_DEPTH', `"${relativePath}" exceeds the ${limits.maxDepth}-level depth limit`, relativePath, 'Flatten the resource tree or raise the explicit import limit.');
119
+ }
120
+ const fullPath = path.join(current, child.name);
121
+ const stat = fs.lstatSync(fullPath);
122
+ if (stat.isSymbolicLink()) {
123
+ fail('AS_IMPORT_SYMLINK', `symbolic links are not permitted: "${relativePath}"`, relativePath, 'Replace the link with an in-tree regular file or directory.');
124
+ }
125
+ if (stat.isDirectory()) {
126
+ const realDirectory = fs.realpathSync.native(fullPath);
127
+ if (!pathContains(realRoot, realDirectory)) {
128
+ fail('AS_IMPORT_PATH_ESCAPE', `directory escapes the source root: "${relativePath}"`, relativePath, 'Keep every imported resource inside the source directory.');
129
+ }
130
+ entries.push({ kind: 'directory', path: relativePath });
131
+ walk(fullPath, relativePath);
132
+ continue;
133
+ }
134
+ if (!stat.isFile()) {
135
+ fail('AS_IMPORT_SPECIAL_FILE', `special files are not permitted: "${relativePath}"`, relativePath, 'Remove sockets, devices, FIFOs, and other non-regular files.');
136
+ }
137
+ let descriptor;
138
+ try {
139
+ descriptor = fs.openSync(fullPath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
140
+ const openedStat = fs.fstatSync(descriptor);
141
+ if (!openedStat.isFile()) {
142
+ fail('AS_IMPORT_SPECIAL_FILE', `source changed to a non-regular file: "${relativePath}"`, relativePath);
143
+ }
144
+ if (openedStat.size > limits.maxFileBytes) {
145
+ fail('AS_IMPORT_FILE_SIZE', `"${relativePath}" exceeds the ${limits.maxFileBytes}-byte file limit`, relativePath, 'Reduce the file or raise the explicit import limit.');
146
+ }
147
+ const bytes = fs.readFileSync(descriptor);
148
+ const file = { kind: 'file', path: relativePath, bytes };
149
+ fileCount += 1;
150
+ totalBytes += bytes.length;
151
+ enforceFileLimits(file, fileCount, totalBytes, limits);
152
+ entries.push(file);
153
+ }
154
+ finally {
155
+ if (descriptor !== undefined)
156
+ fs.closeSync(descriptor);
157
+ }
158
+ }
159
+ };
160
+ walk(realRoot, '');
161
+ return entries;
162
+ }
163
+ function gitEnvironment() {
164
+ const environment = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith('GIT_')));
165
+ return {
166
+ ...environment,
167
+ GIT_ALLOW_PROTOCOL: 'file:git:http:https:ssh',
168
+ GIT_CONFIG_GLOBAL: os.devNull,
169
+ GIT_CONFIG_NOSYSTEM: '1',
170
+ GIT_OPTIONAL_LOCKS: '0',
171
+ GIT_TERMINAL_PROMPT: '0',
172
+ };
173
+ }
174
+ function runGit(args, options = {}) {
175
+ const result = spawnSync('git', args, {
176
+ encoding: options.encoding === null ? null : (options.encoding ?? 'utf8'),
177
+ env: gitEnvironment(),
178
+ maxBuffer: options.maxBuffer ?? 16 * 1024 * 1024,
179
+ stdio: ['ignore', 'pipe', 'pipe'],
180
+ timeout: 120_000,
181
+ });
182
+ if (result.error) {
183
+ if (result.error.code === 'ETIMEDOUT') {
184
+ throw new AgentSkillImportError('AS_IMPORT_GIT_TIMEOUT', 'git invocation exceeded the 120-second import timeout');
185
+ }
186
+ throw new AgentSkillImportError('AS_IMPORT_GIT_UNAVAILABLE', `git invocation failed: ${result.error.message}`);
187
+ }
188
+ if (result.status !== 0) {
189
+ const stderr = Buffer.isBuffer(result.stderr)
190
+ ? result.stderr.toString('utf8')
191
+ : String(result.stderr ?? '');
192
+ throw new AgentSkillImportError('AS_IMPORT_GIT_FAILED', stderr.trim() || `git exited with status ${result.status}`);
193
+ }
194
+ return result.stdout ?? (options.encoding === null ? Buffer.alloc(0) : '');
195
+ }
196
+ function directorySize(root) {
197
+ let total = 0;
198
+ const walk = (current) => {
199
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
200
+ const fullPath = path.join(current, entry.name);
201
+ if (entry.isDirectory()) {
202
+ walk(fullPath);
203
+ }
204
+ else if (entry.isFile()) {
205
+ total += fs.statSync(fullPath).size;
206
+ }
207
+ }
208
+ };
209
+ walk(root);
210
+ return total;
211
+ }
212
+ function normalizeGitSubpath(subpath) {
213
+ if (subpath.length === 0
214
+ || /[\u0000-\u001f\u007f]/.test(subpath)
215
+ || subpath.includes('\\')
216
+ || path.posix.isAbsolute(subpath)) {
217
+ fail('AS_IMPORT_GIT_SUBPATH', 'Git skill subpath must be a non-empty relative POSIX directory', subpath || '.', 'Pass an explicit repository-relative skill directory.');
218
+ }
219
+ const normalized = path.posix.normalize(subpath.replace(/\/+$/, ''));
220
+ if (normalized === '.'
221
+ || normalized === '..'
222
+ || normalized.startsWith('../')
223
+ || normalized !== subpath.replace(/\/+$/, '')) {
224
+ fail('AS_IMPORT_GIT_SUBPATH', `unsafe Git skill subpath "${subpath}"`, subpath, 'Pass a normalized repository-relative skill directory, not the repository root.');
225
+ }
226
+ return normalized;
227
+ }
228
+ function validateGitLocator(url, revision) {
229
+ if (url.length === 0
230
+ || revision.length === 0
231
+ || /[\u0000-\u001f\u007f]/.test(url)
232
+ || /[\u0000-\u001f\u007f]/.test(revision)
233
+ || url.startsWith('-')
234
+ || revision.startsWith('-')) {
235
+ fail('AS_IMPORT_GIT_ARGUMENT', 'Git URL and revision must be explicit non-option arguments', '.', 'Pass a URL and pinned revision that do not begin with "-".');
236
+ }
237
+ if (!/^[A-Za-z0-9][A-Za-z0-9._/-]*$/.test(revision)
238
+ || revision.includes('..')
239
+ || revision.includes('//')
240
+ || revision.endsWith('/')
241
+ || revision.endsWith('.lock')) {
242
+ fail('AS_IMPORT_GIT_REVISION', `unsafe Git revision "${revision}"`, '.', 'Use a commit ID, tag, or refs/... name without refspec or revision operators.');
243
+ }
244
+ let parsed;
245
+ try {
246
+ parsed = new URL(url);
247
+ }
248
+ catch {
249
+ // SCP-style URLs and local repository paths are valid Git locators.
250
+ }
251
+ if (parsed?.username || parsed?.password) {
252
+ fail('AS_IMPORT_GIT_CREDENTIALS', 'credential-bearing Git URLs are not stored as provenance', '.', 'Use an SSH agent, credential helper, or credential-free URL.');
253
+ }
254
+ if (parsed && !['file:', 'git:', 'http:', 'https:', 'ssh:'].includes(parsed.protocol)) {
255
+ fail('AS_IMPORT_GIT_PROTOCOL', `unsupported Git URL protocol "${parsed.protocol}"`, '.', 'Use file, git, HTTP(S), SSH, SCP-style SSH, or a local repository path.');
256
+ }
257
+ if (!parsed && url.includes('::')) {
258
+ fail('AS_IMPORT_GIT_PROTOCOL', 'Git remote-helper command syntax is not permitted', '.', 'Use a direct repository URL or local path.');
259
+ }
260
+ }
261
+ function decodeGitOutput(bytes, context) {
262
+ try {
263
+ return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
264
+ }
265
+ catch {
266
+ fail('AS_IMPORT_PATH_ENCODING', `${context} contains a non-UTF-8 path`, '.', 'Use UTF-8 resource paths.');
267
+ }
268
+ }
269
+ function collectGitTree(source, limits) {
270
+ validateGitLocator(source.url, source.revision);
271
+ const subpath = normalizeGitSubpath(source.subpath);
272
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'aiwg-skill-git-'));
273
+ const gitDir = path.join(tempRoot, 'repo.git');
274
+ try {
275
+ runGit(['init', '--bare', '--quiet', gitDir]);
276
+ const common = [
277
+ '-c', 'core.hooksPath=/dev/null',
278
+ '-c', 'protocol.file.allow=always',
279
+ '-c', 'submodule.recurse=false',
280
+ '-c', 'fetch.fsckObjects=true',
281
+ `--git-dir=${gitDir}`,
282
+ ];
283
+ runGit([
284
+ ...common,
285
+ 'fetch',
286
+ '--quiet',
287
+ '--no-tags',
288
+ '--depth=1',
289
+ '--',
290
+ source.url,
291
+ source.revision,
292
+ ]);
293
+ if (directorySize(gitDir) > limits.maxGitRepositoryBytes) {
294
+ fail('AS_IMPORT_GIT_SIZE', `Git staging data exceeds ${limits.maxGitRepositoryBytes} bytes`, '.', 'Use a smaller pinned source or raise the explicit Git staging limit.');
295
+ }
296
+ const resolvedRevision = String(runGit([
297
+ ...common,
298
+ 'rev-parse',
299
+ '--verify',
300
+ 'FETCH_HEAD^{commit}',
301
+ ])).trim();
302
+ const treeBytes = runGit([
303
+ ...common,
304
+ 'ls-tree',
305
+ '--full-tree',
306
+ '-r',
307
+ '-z',
308
+ resolvedRevision,
309
+ '--',
310
+ subpath,
311
+ ], {
312
+ encoding: null,
313
+ maxBuffer: Math.max(16 * 1024 * 1024, limits.maxFiles * 512),
314
+ });
315
+ const records = decodeGitOutput(treeBytes, 'Git tree')
316
+ .split('\0')
317
+ .filter(Boolean);
318
+ const files = [];
319
+ const directories = new Set();
320
+ let totalBytes = 0;
321
+ for (const record of records) {
322
+ const match = /^([0-9]{6}) ([a-z]+) ([0-9a-f]+)\t([\s\S]+)$/.exec(record);
323
+ if (!match) {
324
+ fail('AS_IMPORT_GIT_TREE', 'Git returned an unrecognized tree entry', '.', 'Verify the pinned revision and repository integrity.');
325
+ }
326
+ const [, mode, objectType, objectId, repositoryPath] = match;
327
+ if (repositoryPath !== subpath
328
+ && !repositoryPath.startsWith(`${subpath}/`)) {
329
+ fail('AS_IMPORT_PATH_ESCAPE', `Git path escapes the requested subpath: "${repositoryPath}"`, repositoryPath);
330
+ }
331
+ const relativePath = validateRelativePath(repositoryPath.slice(subpath.length + 1));
332
+ const depth = relativePath.split('/').length;
333
+ if (depth > limits.maxDepth) {
334
+ fail('AS_IMPORT_DEPTH', `"${relativePath}" exceeds the ${limits.maxDepth}-level depth limit`, relativePath);
335
+ }
336
+ if (mode === '120000') {
337
+ fail('AS_IMPORT_SYMLINK', `Git symbolic links are not permitted: "${relativePath}"`, relativePath, 'Replace the link with an in-tree regular file.');
338
+ }
339
+ if (objectType !== 'blob'
340
+ || (mode !== '100644' && mode !== '100755')) {
341
+ fail('AS_IMPORT_SPECIAL_FILE', `unsupported Git tree entry ${mode} ${objectType}: "${relativePath}"`, relativePath, 'Use only regular files and directories; remove submodules and special entries.');
342
+ }
343
+ const size = Number(String(runGit([
344
+ ...common,
345
+ 'cat-file',
346
+ '-s',
347
+ objectId,
348
+ ])).trim());
349
+ if (!Number.isSafeInteger(size) || size < 0) {
350
+ fail('AS_IMPORT_GIT_BLOB', `invalid Git blob size for "${relativePath}"`, relativePath);
351
+ }
352
+ if (size > limits.maxFileBytes) {
353
+ fail('AS_IMPORT_FILE_SIZE', `"${relativePath}" exceeds the ${limits.maxFileBytes}-byte file limit`, relativePath);
354
+ }
355
+ const bytes = runGit([
356
+ ...common,
357
+ 'cat-file',
358
+ 'blob',
359
+ objectId,
360
+ ], {
361
+ encoding: null,
362
+ maxBuffer: limits.maxFileBytes + 64 * 1024,
363
+ });
364
+ if (bytes.length !== size) {
365
+ fail('AS_IMPORT_GIT_BLOB', `Git blob size changed while reading "${relativePath}"`, relativePath);
366
+ }
367
+ const file = { kind: 'file', path: relativePath, bytes };
368
+ totalBytes += bytes.length;
369
+ enforceFileLimits(file, files.length + 1, totalBytes, limits);
370
+ files.push(file);
371
+ let parent = path.posix.dirname(relativePath);
372
+ while (parent !== '.') {
373
+ directories.add(parent);
374
+ parent = path.posix.dirname(parent);
375
+ }
376
+ }
377
+ if (files.length === 0) {
378
+ fail('AS_IMPORT_GIT_SUBPATH_MISSING', `Git subpath "${subpath}" contains no files at the resolved revision`, subpath, 'Pass a subpath that contains SKILL.md.');
379
+ }
380
+ const entries = [
381
+ ...[...directories]
382
+ .sort((left, right) => (left.split('/').length - right.split('/').length
383
+ || left.localeCompare(right)))
384
+ .map((directoryPath) => ({
385
+ kind: 'directory',
386
+ path: directoryPath,
387
+ })),
388
+ ...files.sort((left, right) => left.path.localeCompare(right.path)),
389
+ ];
390
+ return {
391
+ entries,
392
+ source: {
393
+ kind: 'git',
394
+ locator: source.url,
395
+ subpath,
396
+ requestedRevision: source.revision,
397
+ resolvedRevision,
398
+ },
399
+ directoryName: path.posix.basename(subpath),
400
+ };
401
+ }
402
+ finally {
403
+ fs.rmSync(tempRoot, { recursive: true, force: true });
404
+ }
405
+ }
406
+ function materializeSource(source, projectDir, limits) {
407
+ if (source.kind === 'git')
408
+ return collectGitTree(source, limits);
409
+ const sourceRoot = path.resolve(projectDir, source.path);
410
+ const managedStore = projectAiwgPath(projectDir, 'skills', 'imported');
411
+ if (pathContains(sourceRoot, managedStore)
412
+ || pathContains(managedStore, sourceRoot)) {
413
+ fail('AS_IMPORT_RECURSIVE_SOURCE', 'the local source and managed import store cannot contain one another', sourceRoot, 'Import from a source directory outside the managed store.');
414
+ }
415
+ if (!fs.existsSync(sourceRoot)) {
416
+ fail('AS_IMPORT_SOURCE_MISSING', `local import source does not exist: "${sourceRoot}"`, sourceRoot, 'Pass an existing directory containing SKILL.md.');
417
+ }
418
+ return {
419
+ entries: collectLocalTree(sourceRoot, limits),
420
+ source: {
421
+ kind: 'directory',
422
+ locator: fs.realpathSync.native(sourceRoot),
423
+ },
424
+ directoryName: path.basename(sourceRoot),
425
+ };
426
+ }
427
+ function parseImportedSkill(entries, directoryName, profile) {
428
+ const skillFile = entries.find((entry) => (entry.kind === 'file' && entry.path === 'SKILL.md'));
429
+ if (!skillFile) {
430
+ fail('AS_IMPORT_SKILL_MISSING', 'the source root must contain SKILL.md', 'SKILL.md', 'Select the skill directory itself, not its parent or a nested resource.');
431
+ }
432
+ let content;
433
+ try {
434
+ content = new TextDecoder('utf-8', { fatal: true }).decode(skillFile.bytes);
435
+ }
436
+ catch {
437
+ fail('AS_IMPORT_ENCODING', 'SKILL.md must be valid UTF-8', 'SKILL.md', 'Encode SKILL.md as UTF-8 without invalid byte sequences.');
438
+ }
439
+ const validation = validateAgentSkillContent(content, {
440
+ profile,
441
+ file: 'SKILL.md',
442
+ directoryName,
443
+ });
444
+ const diagnostics = validation.diagnostics;
445
+ const errors = diagnostics.filter((item) => item.severity === 'error');
446
+ if (errors.length > 0) {
447
+ throw new AgentSkillImportError('AS_IMPORT_VALIDATION', `Agent Skill validation failed with ${errors.length} error(s)`, diagnostics);
448
+ }
449
+ return {
450
+ name: validation.frontmatter?.['name'],
451
+ description: validation.frontmatter?.['description'],
452
+ diagnostics,
453
+ };
454
+ }
455
+ function computeTreeDigest(entries) {
456
+ const hash = createHash('sha256');
457
+ hash.update('aiwg-agent-skill-tree-v1\0');
458
+ for (const entry of [...entries].sort((left, right) => (left.path.localeCompare(right.path) || left.kind.localeCompare(right.kind)))) {
459
+ hash.update(entry.kind);
460
+ hash.update('\0');
461
+ hash.update(entry.path);
462
+ hash.update('\0');
463
+ if (entry.kind === 'file') {
464
+ hash.update(String(entry.bytes.length));
465
+ hash.update('\0');
466
+ hash.update(entry.bytes);
467
+ }
468
+ hash.update('\0');
469
+ }
470
+ return hash.digest('hex');
471
+ }
472
+ function detectAiwgVersion() {
473
+ let current = path.dirname(fileURLToPath(import.meta.url));
474
+ for (let depth = 0; depth < 6; depth += 1) {
475
+ const packagePath = path.join(current, 'package.json');
476
+ if (fs.existsSync(packagePath)) {
477
+ try {
478
+ const parsed = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
479
+ if (parsed.name === 'aiwg'
480
+ && typeof parsed.version === 'string'
481
+ && parsed.version.length > 0) {
482
+ return parsed.version;
483
+ }
484
+ }
485
+ catch {
486
+ // Continue toward the package root.
487
+ }
488
+ }
489
+ current = path.dirname(current);
490
+ }
491
+ return 'unknown';
492
+ }
493
+ function resolveImportedAt(value) {
494
+ if (value === undefined)
495
+ return new Date().toISOString();
496
+ const parsed = new Date(value);
497
+ if (!Number.isFinite(parsed.valueOf()) || parsed.toISOString() !== value) {
498
+ throw new AgentSkillImportError('AS_IMPORT_TIME_INVALID', 'importedAt must be an exact ISO-8601 timestamp');
499
+ }
500
+ return value;
501
+ }
502
+ function readStoredRecord(skillRoot) {
503
+ const manifestPath = path.join(skillRoot, 'manifest.json');
504
+ if (!fs.existsSync(manifestPath))
505
+ return undefined;
506
+ try {
507
+ const rootStat = fs.lstatSync(skillRoot);
508
+ const manifestStat = fs.lstatSync(manifestPath);
509
+ if (rootStat.isSymbolicLink()
510
+ || !rootStat.isDirectory()
511
+ || manifestStat.isSymbolicLink()
512
+ || !manifestStat.isFile()
513
+ || !pathContains(fs.realpathSync.native(skillRoot), fs.realpathSync.native(manifestPath))) {
514
+ throw new Error('manifest must be a regular file inside the managed skill');
515
+ }
516
+ const record = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
517
+ if (record.schemaVersion !== 1
518
+ || typeof record.name !== 'string'
519
+ || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(record.name)
520
+ || typeof record.description !== 'string'
521
+ || typeof record.digest !== 'string'
522
+ || !/^[0-9a-f]{64}$/.test(record.digest)
523
+ || (record.source?.kind !== 'directory'
524
+ && record.source?.kind !== 'git')
525
+ || typeof record.source.locator !== 'string'
526
+ || (record.source.kind === 'git'
527
+ && (typeof record.source.subpath !== 'string'
528
+ || typeof record.source.requestedRevision !== 'string'
529
+ || typeof record.source.resolvedRevision !== 'string'))
530
+ || (record.validationProfile !== 'strict'
531
+ && record.validationProfile !== 'compatible')
532
+ || !Array.isArray(record.diagnostics)
533
+ || (record.trust?.state !== 'trusted'
534
+ && record.trust?.state !== 'untrusted')
535
+ || (record.trust?.activation !== 'active'
536
+ && record.trust?.activation !== 'inactive')
537
+ || typeof record.importedAt !== 'string'
538
+ || typeof record.aiwgVersion !== 'string'
539
+ || record.sourceDirectory !== 'source'
540
+ || !Number.isSafeInteger(record.fileCount)
541
+ || record.fileCount < 1
542
+ || !Number.isSafeInteger(record.totalBytes)
543
+ || record.totalBytes < 0) {
544
+ throw new Error('unsupported manifest shape');
545
+ }
546
+ return record;
547
+ }
548
+ catch (error) {
549
+ throw new AgentSkillImportError('AS_IMPORT_MANIFEST_INVALID', `managed import manifest is invalid: ${error instanceof Error ? error.message : String(error)}`);
550
+ }
551
+ }
552
+ function managedTreeMatches(skillRoot, record) {
553
+ const sourceRoot = path.join(skillRoot, record.sourceDirectory);
554
+ if (!fs.existsSync(sourceRoot))
555
+ return false;
556
+ try {
557
+ return computeTreeDigest(collectLocalTree(sourceRoot, { ...DEFAULT_AGENT_SKILL_IMPORT_LIMITS })) === record.digest;
558
+ }
559
+ catch {
560
+ return false;
561
+ }
562
+ }
563
+ function resultFromRecord(record, skillRoot, status, dryRun) {
564
+ const integrityMatches = (dryRun
565
+ || !fs.existsSync(skillRoot)
566
+ || managedTreeMatches(skillRoot, record));
567
+ const diagnostics = integrityMatches
568
+ ? record.diagnostics
569
+ : [
570
+ ...record.diagnostics,
571
+ diagnostic('AS_IMPORT_MANAGED_DRIFT', path.join(skillRoot, record.sourceDirectory), '$.digest', 'managed source bytes no longer match the imported digest', 'Review the managed tree, then restore it with an explicit forced import.'),
572
+ ];
573
+ return {
574
+ schemaVersion: 1,
575
+ status,
576
+ dryRun,
577
+ name: record.name,
578
+ description: record.description,
579
+ digest: record.digest,
580
+ source: record.source,
581
+ validationProfile: record.validationProfile,
582
+ diagnostics,
583
+ trust: integrityMatches
584
+ ? record.trust
585
+ : { state: 'untrusted', activation: 'inactive' },
586
+ importedAt: record.importedAt,
587
+ aiwgVersion: record.aiwgVersion,
588
+ managedLocation: path.join(skillRoot, record.sourceDirectory),
589
+ fileCount: record.fileCount,
590
+ totalBytes: record.totalBytes,
591
+ };
592
+ }
593
+ function higherPrecedenceCollision(name, projectDir, managedStore) {
594
+ const candidates = new Map();
595
+ for (const definition of listProviderDefinitions()) {
596
+ const namespace = definition.skillNamespace;
597
+ const basePath = namespace.pathType === 'home-dir'
598
+ ? `~/${namespace.skillsBaseDir}`
599
+ : namespace.skillsBaseDir;
600
+ const resolved = resolveProviderPathValue(basePath, projectDir);
601
+ candidates.set(path.join(resolved, name), namespace.pathType === 'home-dir' ? 'user' : 'project');
602
+ }
603
+ candidates.set(path.join(projectDir, '.agents', 'skills', name), 'project');
604
+ candidates.set(path.join(projectDir, '.claude', '.aiwg', 'skills', name), 'project');
605
+ const match = [...candidates.entries()]
606
+ .sort(([left], [right]) => left.localeCompare(right))
607
+ .find(([candidate]) => {
608
+ if (pathContains(managedStore, candidate))
609
+ return false;
610
+ return (fs.existsSync(path.join(candidate, 'SKILL.md'))
611
+ && !fs.existsSync(path.join(candidate, '.aiwg-managed')));
612
+ });
613
+ return match ? { path: match[0], origin: match[1] } : undefined;
614
+ }
615
+ function writeTree(root, entries) {
616
+ fs.mkdirSync(root, { recursive: true, mode: 0o700 });
617
+ const directories = entries
618
+ .filter((entry) => entry.kind === 'directory')
619
+ .sort((left, right) => (left.path.split('/').length - right.path.split('/').length
620
+ || left.path.localeCompare(right.path)));
621
+ for (const entry of directories) {
622
+ fs.mkdirSync(path.join(root, ...entry.path.split('/')), {
623
+ recursive: false,
624
+ mode: 0o700,
625
+ });
626
+ }
627
+ for (const entry of entries
628
+ .filter((item) => item.kind === 'file')
629
+ .sort((left, right) => left.path.localeCompare(right.path))) {
630
+ const target = path.join(root, ...entry.path.split('/'));
631
+ fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
632
+ fs.writeFileSync(target, entry.bytes, { flag: 'wx', mode: 0o600 });
633
+ }
634
+ }
635
+ function lockOwnerIsActive(lockPath) {
636
+ try {
637
+ const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
638
+ if (Number.isSafeInteger(lock.pid) && (lock.pid ?? 0) > 0) {
639
+ try {
640
+ process.kill(lock.pid, 0);
641
+ return true;
642
+ }
643
+ catch (error) {
644
+ return error.code === 'EPERM';
645
+ }
646
+ }
647
+ const age = Date.now() - fs.statSync(lockPath).mtimeMs;
648
+ return age < 10 * 60 * 1_000;
649
+ }
650
+ catch {
651
+ return false;
652
+ }
653
+ }
654
+ function recoverInterruptedPromotion(importedStore, name) {
655
+ if (!fs.existsSync(importedStore))
656
+ return;
657
+ const entries = fs.readdirSync(importedStore)
658
+ .filter((entry) => (entry.startsWith(`.${name}.staging-`)
659
+ || entry.startsWith(`.${name}.backup-`)))
660
+ .sort();
661
+ const staging = entries.filter((entry) => entry.includes('.staging-'));
662
+ const backups = entries.filter((entry) => entry.includes('.backup-'));
663
+ const finalRoot = path.join(importedStore, name);
664
+ for (const entry of staging) {
665
+ fs.rmSync(path.join(importedStore, entry), {
666
+ recursive: true,
667
+ force: true,
668
+ });
669
+ }
670
+ if (fs.existsSync(finalRoot)) {
671
+ for (const entry of backups) {
672
+ fs.rmSync(path.join(importedStore, entry), {
673
+ recursive: true,
674
+ force: true,
675
+ });
676
+ }
677
+ return;
678
+ }
679
+ if (backups.length > 1) {
680
+ throw new AgentSkillImportError('AS_IMPORT_RECOVERY_AMBIGUOUS', `multiple interrupted backups exist for "${name}"`);
681
+ }
682
+ if (backups.length === 1) {
683
+ fs.renameSync(path.join(importedStore, backups[0]), finalRoot);
684
+ }
685
+ }
686
+ function acquireImportLock(importedStore, name) {
687
+ fs.mkdirSync(importedStore, { recursive: true, mode: 0o700 });
688
+ const lockPath = path.join(importedStore, `.${name}.lock`);
689
+ for (let attempt = 0; attempt < 2; attempt += 1) {
690
+ try {
691
+ const descriptor = fs.openSync(lockPath, 'wx', 0o600);
692
+ try {
693
+ fs.writeFileSync(descriptor, `${JSON.stringify({
694
+ pid: process.pid,
695
+ createdAt: new Date().toISOString(),
696
+ })}\n`);
697
+ }
698
+ finally {
699
+ fs.closeSync(descriptor);
700
+ }
701
+ return () => fs.rmSync(lockPath, { force: true });
702
+ }
703
+ catch (error) {
704
+ const code = error.code;
705
+ if (code !== 'EEXIST')
706
+ throw error;
707
+ if (lockOwnerIsActive(lockPath)) {
708
+ throw new AgentSkillImportError('AS_IMPORT_BUSY', `another import is active for "${name}"`);
709
+ }
710
+ fs.rmSync(lockPath, { force: true });
711
+ }
712
+ }
713
+ throw new AgentSkillImportError('AS_IMPORT_BUSY', `could not acquire the import lock for "${name}"`);
714
+ }
715
+ function recoverInterruptedImportForRead(importedStore, name) {
716
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name))
717
+ return;
718
+ const lockPath = path.join(importedStore, `.${name}.lock`);
719
+ if (fs.existsSync(lockPath) && lockOwnerIsActive(lockPath))
720
+ return;
721
+ const hasRecoveryState = fs.existsSync(lockPath)
722
+ || fs.readdirSync(importedStore).some((entry) => (entry.startsWith(`.${name}.staging-`)
723
+ || entry.startsWith(`.${name}.backup-`)));
724
+ if (!hasRecoveryState)
725
+ return;
726
+ if (fs.existsSync(lockPath))
727
+ fs.rmSync(lockPath, { force: true });
728
+ const release = acquireImportLock(importedStore, name);
729
+ try {
730
+ recoverInterruptedPromotion(importedStore, name);
731
+ }
732
+ finally {
733
+ release();
734
+ }
735
+ }
736
+ function promoteAtomically(entries, record, importedStore, finalRoot) {
737
+ fs.mkdirSync(importedStore, { recursive: true, mode: 0o700 });
738
+ const stagingRoot = fs.mkdtempSync(path.join(importedStore, `.${record.name}.staging-`));
739
+ let backupRoot;
740
+ try {
741
+ writeTree(path.join(stagingRoot, record.sourceDirectory), entries);
742
+ fs.writeFileSync(path.join(stagingRoot, 'manifest.json'), `${JSON.stringify(record, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
743
+ if (fs.existsSync(finalRoot)) {
744
+ backupRoot = path.join(importedStore, `.${record.name}.backup-${randomUUID()}`);
745
+ fs.renameSync(finalRoot, backupRoot);
746
+ }
747
+ try {
748
+ fs.renameSync(stagingRoot, finalRoot);
749
+ }
750
+ catch (error) {
751
+ if (backupRoot && fs.existsSync(backupRoot)) {
752
+ fs.renameSync(backupRoot, finalRoot);
753
+ backupRoot = undefined;
754
+ }
755
+ throw error;
756
+ }
757
+ if (backupRoot) {
758
+ fs.rmSync(backupRoot, { recursive: true, force: true });
759
+ backupRoot = undefined;
760
+ }
761
+ }
762
+ finally {
763
+ fs.rmSync(stagingRoot, { recursive: true, force: true });
764
+ if (backupRoot && fs.existsSync(backupRoot) && !fs.existsSync(finalRoot)) {
765
+ fs.renameSync(backupRoot, finalRoot);
766
+ }
767
+ }
768
+ }
769
+ function sameSource(left, right) {
770
+ return (left.kind === right.kind
771
+ && left.locator === right.locator
772
+ && (left.kind !== 'git'
773
+ || right.kind !== 'git'
774
+ || left.subpath === right.subpath));
775
+ }
776
+ export async function importAgentSkill(source, options) {
777
+ const projectDir = path.resolve(options.projectDir);
778
+ const profile = options.profile ?? 'strict';
779
+ if (profile !== 'strict' && profile !== 'compatible') {
780
+ throw new AgentSkillImportError('AS_IMPORT_PROFILE', `unsupported import validation profile "${String(profile)}"`);
781
+ }
782
+ if (options.activate && !options.trust) {
783
+ throw new AgentSkillImportError('AS_IMPORT_TRUST_REQUIRED', 'activation requires explicit trust for this source and digest', [
784
+ diagnostic('AS_IMPORT_TRUST_REQUIRED', '.', '$.trust', 'activation requires explicit trust for this source and digest', 'Repeat the operation with both trust and activation explicitly enabled.'),
785
+ ]);
786
+ }
787
+ const limits = normalizedLimits(options.limits);
788
+ const materialized = materializeSource(source, projectDir, limits);
789
+ const parsed = parseImportedSkill(materialized.entries, materialized.directoryName, profile);
790
+ const digest = computeTreeDigest(materialized.entries);
791
+ const importedStore = projectAiwgPath(projectDir, 'skills', 'imported');
792
+ const finalRoot = path.join(importedStore, parsed.name);
793
+ const collision = higherPrecedenceCollision(parsed.name, projectDir, importedStore);
794
+ if (collision) {
795
+ throw new AgentSkillImportError('AS_IMPORT_COLLISION', `${collision.origin}-owned skill "${parsed.name}" already exists at ${collision.path}`, [
796
+ diagnostic('AS_IMPORT_COLLISION', collision.path, '$.name', `${collision.origin}-owned skill "${parsed.name}" has higher precedence`, 'Rename the imported skill or remove the higher-precedence collision explicitly.'),
797
+ ]);
798
+ }
799
+ const releaseLock = options.dryRun
800
+ ? undefined
801
+ : acquireImportLock(importedStore, parsed.name);
802
+ try {
803
+ if (releaseLock)
804
+ recoverInterruptedPromotion(importedStore, parsed.name);
805
+ let existing;
806
+ if (fs.existsSync(finalRoot)) {
807
+ existing = readStoredRecord(finalRoot);
808
+ if (!existing) {
809
+ throw new AgentSkillImportError('AS_IMPORT_MANIFEST_MISSING', `managed skill "${parsed.name}" exists without manifest.json`);
810
+ }
811
+ }
812
+ const existingTreeMatches = existing
813
+ ? managedTreeMatches(finalRoot, existing)
814
+ : true;
815
+ if (existing && !existingTreeMatches && !options.force) {
816
+ throw new AgentSkillImportError('AS_IMPORT_MANAGED_DRIFT', `managed source bytes for "${parsed.name}" do not match its recorded digest`, [
817
+ diagnostic('AS_IMPORT_MANAGED_DRIFT', path.join(finalRoot, existing.sourceDirectory), '$.digest', `managed source bytes no longer match ${existing.digest}`, 'Review the managed tree, then restore it with an explicit forced import.'),
818
+ ]);
819
+ }
820
+ const requestedTrust = options.trust === true || options.activate === true;
821
+ const trust = requestedTrust
822
+ ? {
823
+ state: options.trust ? 'trusted' : 'untrusted',
824
+ activation: options.activate ? 'active' : 'inactive',
825
+ }
826
+ : existing?.digest === digest
827
+ ? existing.trust
828
+ : {
829
+ state: 'untrusted',
830
+ activation: 'inactive',
831
+ };
832
+ if (existing
833
+ && existingTreeMatches
834
+ && existing.digest === digest
835
+ && existing.validationProfile === profile
836
+ && existing.trust.state === trust.state
837
+ && existing.trust.activation === trust.activation) {
838
+ return resultFromRecord(existing, finalRoot, 'unchanged', Boolean(options.dryRun));
839
+ }
840
+ let status = 'imported';
841
+ if (existing) {
842
+ const trustOnlyChange = (existing.digest === digest
843
+ && existing.validationProfile === profile
844
+ && requestedTrust);
845
+ if (!trustOnlyChange) {
846
+ if (!options.force && !(options.update && sameSource(existing.source, materialized.source))) {
847
+ throw new AgentSkillImportError('AS_IMPORT_COLLISION', options.update
848
+ ? `source drift for "${parsed.name}" requires --force`
849
+ : `imported skill "${parsed.name}" already exists with different content`, [
850
+ diagnostic('AS_IMPORT_COLLISION', path.join(finalRoot, 'manifest.json'), '$.digest', `managed digest ${existing.digest} differs from ${digest}`, options.update
851
+ ? 'Use force only after reviewing the changed source locator.'
852
+ : 'Use update for the same source or force for an intentional source replacement.'),
853
+ ]);
854
+ }
855
+ }
856
+ status = 'updated';
857
+ }
858
+ const files = materialized.entries.filter((entry) => entry.kind === 'file');
859
+ const importedAt = resolveImportedAt(options.importedAt);
860
+ const record = {
861
+ schemaVersion: 1,
862
+ name: parsed.name,
863
+ description: parsed.description,
864
+ digest,
865
+ source: materialized.source,
866
+ validationProfile: profile,
867
+ diagnostics: parsed.diagnostics,
868
+ trust,
869
+ importedAt,
870
+ aiwgVersion: options.aiwgVersion ?? detectAiwgVersion(),
871
+ sourceDirectory: 'source',
872
+ fileCount: files.length,
873
+ totalBytes: files.reduce((total, file) => total + file.bytes.length, 0),
874
+ };
875
+ if (options.dryRun) {
876
+ return resultFromRecord(record, finalRoot, 'planned', true);
877
+ }
878
+ promoteAtomically(materialized.entries, record, importedStore, finalRoot);
879
+ return resultFromRecord(record, finalRoot, status, false);
880
+ }
881
+ finally {
882
+ releaseLock?.();
883
+ }
884
+ }
885
+ export function listImportedAgentSkills(projectDir) {
886
+ const importedStore = projectAiwgPath(path.resolve(projectDir), 'skills', 'imported');
887
+ if (!fs.existsSync(importedStore))
888
+ return [];
889
+ const recoverableNames = new Set(fs.readdirSync(importedStore)
890
+ .map((entry) => /^\.(.+)\.(?:lock|staging-|backup-)/.exec(entry)?.[1])
891
+ .filter((name) => Boolean(name)));
892
+ for (const name of [...recoverableNames].sort()) {
893
+ recoverInterruptedImportForRead(importedStore, name);
894
+ }
895
+ return fs.readdirSync(importedStore, { withFileTypes: true })
896
+ .filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'))
897
+ .sort((left, right) => left.name.localeCompare(right.name))
898
+ .flatMap((entry) => {
899
+ const skillRoot = path.join(importedStore, entry.name);
900
+ const record = readStoredRecord(skillRoot);
901
+ return record
902
+ ? [resultFromRecord(record, skillRoot, 'imported', false)]
903
+ : [];
904
+ });
905
+ }
906
+ export function getImportedAgentSkill(projectDir, name) {
907
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name))
908
+ return undefined;
909
+ const importedStore = projectAiwgPath(path.resolve(projectDir), 'skills', 'imported');
910
+ if (fs.existsSync(importedStore)) {
911
+ recoverInterruptedImportForRead(importedStore, name);
912
+ }
913
+ const skillRoot = projectAiwgPath(path.resolve(projectDir), 'skills', 'imported', name);
914
+ if (!fs.existsSync(skillRoot))
915
+ return undefined;
916
+ const record = readStoredRecord(skillRoot);
917
+ return record
918
+ ? resultFromRecord(record, skillRoot, 'imported', false)
919
+ : undefined;
920
+ }
921
+ //# sourceMappingURL=importer.js.map