@enactprotocol/shared 2.2.2 → 2.3.1

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 (111) hide show
  1. package/README.md +1 -18
  2. package/dist/config.d.ts +12 -0
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +32 -6
  5. package/dist/config.js.map +1 -1
  6. package/dist/execution/action-command.d.ts +131 -0
  7. package/dist/execution/action-command.d.ts.map +1 -0
  8. package/dist/execution/action-command.js +300 -0
  9. package/dist/execution/action-command.js.map +1 -0
  10. package/dist/execution/command.d.ts +8 -8
  11. package/dist/execution/command.js +6 -6
  12. package/dist/execution/index.d.ts +1 -0
  13. package/dist/execution/index.d.ts.map +1 -1
  14. package/dist/execution/index.js +2 -0
  15. package/dist/execution/index.js.map +1 -1
  16. package/dist/execution/types.d.ts +5 -2
  17. package/dist/execution/types.d.ts.map +1 -1
  18. package/dist/execution/types.js.map +1 -1
  19. package/dist/index.d.ts +9 -7
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +14 -5
  22. package/dist/index.js.map +1 -1
  23. package/dist/manifest/actions-loader.d.ts +29 -0
  24. package/dist/manifest/actions-loader.d.ts.map +1 -0
  25. package/dist/manifest/actions-loader.js +34 -0
  26. package/dist/manifest/actions-loader.js.map +1 -0
  27. package/dist/manifest/actions-parser.d.ts +69 -0
  28. package/dist/manifest/actions-parser.d.ts.map +1 -0
  29. package/dist/manifest/actions-parser.js +265 -0
  30. package/dist/manifest/actions-parser.js.map +1 -0
  31. package/dist/manifest/index.d.ts +2 -0
  32. package/dist/manifest/index.d.ts.map +1 -1
  33. package/dist/manifest/index.js +4 -0
  34. package/dist/manifest/index.js.map +1 -1
  35. package/dist/manifest/loader.d.ts +7 -2
  36. package/dist/manifest/loader.d.ts.map +1 -1
  37. package/dist/manifest/loader.js +71 -4
  38. package/dist/manifest/loader.js.map +1 -1
  39. package/dist/manifest/parser.d.ts +1 -0
  40. package/dist/manifest/parser.d.ts.map +1 -1
  41. package/dist/manifest/parser.js +1 -0
  42. package/dist/manifest/parser.js.map +1 -1
  43. package/dist/manifest/scripts.d.ts +19 -0
  44. package/dist/manifest/scripts.d.ts.map +1 -0
  45. package/dist/manifest/scripts.js +102 -0
  46. package/dist/manifest/scripts.js.map +1 -0
  47. package/dist/manifest/validator.d.ts +1 -8
  48. package/dist/manifest/validator.d.ts.map +1 -1
  49. package/dist/manifest/validator.js +14 -13
  50. package/dist/manifest/validator.js.map +1 -1
  51. package/dist/mcp-registry.js +5 -5
  52. package/dist/mcp-registry.js.map +1 -1
  53. package/dist/paths.d.ts +9 -2
  54. package/dist/paths.d.ts.map +1 -1
  55. package/dist/paths.js +12 -3
  56. package/dist/paths.js.map +1 -1
  57. package/dist/registry.d.ts +47 -2
  58. package/dist/registry.d.ts.map +1 -1
  59. package/dist/registry.js +100 -7
  60. package/dist/registry.js.map +1 -1
  61. package/dist/resolver.d.ts +55 -4
  62. package/dist/resolver.d.ts.map +1 -1
  63. package/dist/resolver.js +144 -77
  64. package/dist/resolver.js.map +1 -1
  65. package/dist/types/actions.d.ts +194 -0
  66. package/dist/types/actions.d.ts.map +1 -0
  67. package/dist/types/actions.js +32 -0
  68. package/dist/types/actions.js.map +1 -0
  69. package/dist/types/index.d.ts +3 -1
  70. package/dist/types/index.d.ts.map +1 -1
  71. package/dist/types/index.js +1 -0
  72. package/dist/types/index.js.map +1 -1
  73. package/dist/types/manifest.d.ts +50 -5
  74. package/dist/types/manifest.d.ts.map +1 -1
  75. package/dist/types/manifest.js +10 -2
  76. package/dist/types/manifest.js.map +1 -1
  77. package/package.json +2 -2
  78. package/src/config.ts +48 -6
  79. package/src/execution/action-command.ts +417 -0
  80. package/src/execution/command.ts +8 -8
  81. package/src/execution/index.ts +17 -0
  82. package/src/execution/types.ts +13 -2
  83. package/src/index.ts +43 -0
  84. package/src/manifest/actions-loader.ts +49 -0
  85. package/src/manifest/index.ts +12 -0
  86. package/src/manifest/loader.ts +77 -4
  87. package/src/manifest/parser.ts +1 -0
  88. package/src/manifest/scripts.ts +116 -0
  89. package/src/manifest/validator.ts +15 -14
  90. package/src/mcp-registry.ts +5 -5
  91. package/src/paths.ts +13 -3
  92. package/src/registry.ts +136 -7
  93. package/src/resolver.ts +185 -79
  94. package/src/types/actions.ts +223 -0
  95. package/src/types/index.ts +11 -0
  96. package/src/types/manifest.ts +67 -6
  97. package/tests/action-command.test.ts +249 -0
  98. package/tests/config-normalization.test.ts +279 -0
  99. package/tests/config.test.ts +4 -1
  100. package/tests/effective-input-schema.test.ts +86 -0
  101. package/tests/fixtures/valid-tool.md +5 -12
  102. package/tests/fixtures/valid-tool.yaml +3 -10
  103. package/tests/hooks.test.ts +177 -0
  104. package/tests/manifest/loader.test.ts +34 -20
  105. package/tests/manifest/parser.test.ts +11 -15
  106. package/tests/manifest/validator.test.ts +7 -17
  107. package/tests/manifest-types.test.ts +9 -11
  108. package/tests/paths.test.ts +11 -4
  109. package/tests/registry.test.ts +204 -8
  110. package/tests/resolver.test.ts +90 -6
  111. package/tsconfig.tsbuildinfo +1 -1
package/src/registry.ts CHANGED
@@ -19,6 +19,8 @@ import { getCacheDir, getEnactHome, getProjectEnactDir } from "./paths";
19
19
  export interface ToolsRegistry {
20
20
  /** Map of tool name to installed version */
21
21
  tools: Record<string, string>;
22
+ /** Map of alias to full tool name */
23
+ aliases?: Record<string, string>;
22
24
  }
23
25
 
24
26
  /**
@@ -56,7 +58,7 @@ export function loadToolsRegistry(scope: RegistryScope, startDir?: string): Tool
56
58
  const registryPath = getToolsJsonPath(scope, startDir);
57
59
 
58
60
  if (!registryPath || !existsSync(registryPath)) {
59
- return { tools: {} };
61
+ return { tools: {}, aliases: {} };
60
62
  }
61
63
 
62
64
  try {
@@ -64,10 +66,11 @@ export function loadToolsRegistry(scope: RegistryScope, startDir?: string): Tool
64
66
  const parsed = JSON.parse(content);
65
67
  return {
66
68
  tools: parsed.tools ?? {},
69
+ aliases: parsed.aliases ?? {},
67
70
  };
68
71
  } catch {
69
72
  // Return empty registry on parse error
70
- return { tools: {} };
73
+ return { tools: {}, aliases: {} };
71
74
  }
72
75
  }
73
76
 
@@ -162,12 +165,12 @@ export function getInstalledVersion(
162
165
  }
163
166
 
164
167
  /**
165
- * Get the cache path for an installed tool
168
+ * Get the install path for a skill
169
+ * Skills are stored at ~/.agent/skills/{name}/ (flat, no version subdirectory)
166
170
  */
167
- export function getToolCachePath(toolName: string, version: string): string {
168
- const cacheDir = getCacheDir();
169
- const normalizedVersion = version.startsWith("v") ? version.slice(1) : version;
170
- return join(cacheDir, toolName, `v${normalizedVersion}`);
171
+ export function getToolCachePath(toolName: string, _version: string): string {
172
+ const skillsDir = getCacheDir();
173
+ return join(skillsDir, toolName);
171
174
  }
172
175
 
173
176
  /**
@@ -217,3 +220,129 @@ export function getInstalledToolInfo(
217
220
  cachePath,
218
221
  };
219
222
  }
223
+
224
+ /**
225
+ * Add an alias for a tool
226
+ * @param alias - Short name for the tool (e.g., "firebase")
227
+ * @param toolName - Full tool name (e.g., "user/api/firebase")
228
+ * @param scope - Registry scope (global or project)
229
+ * @param startDir - Starting directory for project scope
230
+ * @throws Error if alias already exists for a different tool
231
+ */
232
+ export function addAlias(
233
+ alias: string,
234
+ toolName: string,
235
+ scope: RegistryScope,
236
+ startDir?: string
237
+ ): void {
238
+ const registry = loadToolsRegistry(scope, startDir);
239
+
240
+ // Initialize aliases if not present
241
+ if (!registry.aliases) {
242
+ registry.aliases = {};
243
+ }
244
+
245
+ // Check if alias already exists for a different tool
246
+ const existingTarget = registry.aliases[alias];
247
+ if (existingTarget && existingTarget !== toolName) {
248
+ throw new Error(
249
+ `Alias "${alias}" already exists for tool "${existingTarget}". Remove it first with 'enact alias --remove ${alias}'.`
250
+ );
251
+ }
252
+
253
+ registry.aliases[alias] = toolName;
254
+ saveToolsRegistry(registry, scope, startDir);
255
+ }
256
+
257
+ /**
258
+ * Remove an alias
259
+ * @param alias - Alias to remove
260
+ * @param scope - Registry scope
261
+ * @param startDir - Starting directory for project scope
262
+ * @returns true if alias was removed, false if it didn't exist
263
+ */
264
+ export function removeAlias(alias: string, scope: RegistryScope, startDir?: string): boolean {
265
+ const registry = loadToolsRegistry(scope, startDir);
266
+
267
+ if (!registry.aliases || !(alias in registry.aliases)) {
268
+ return false;
269
+ }
270
+
271
+ delete registry.aliases[alias];
272
+ saveToolsRegistry(registry, scope, startDir);
273
+ return true;
274
+ }
275
+
276
+ /**
277
+ * Resolve an alias to its full tool name
278
+ * @param alias - Alias to resolve
279
+ * @param scope - Registry scope
280
+ * @param startDir - Starting directory for project scope
281
+ * @returns Full tool name or null if alias doesn't exist
282
+ */
283
+ export function resolveAlias(
284
+ alias: string,
285
+ scope: RegistryScope,
286
+ startDir?: string
287
+ ): string | null {
288
+ const registry = loadToolsRegistry(scope, startDir);
289
+ return registry.aliases?.[alias] ?? null;
290
+ }
291
+
292
+ /**
293
+ * Get all aliases for a specific tool
294
+ * @param toolName - Full tool name
295
+ * @param scope - Registry scope
296
+ * @param startDir - Starting directory for project scope
297
+ * @returns Array of aliases for the tool
298
+ */
299
+ export function getAliasesForTool(
300
+ toolName: string,
301
+ scope: RegistryScope,
302
+ startDir?: string
303
+ ): string[] {
304
+ const registry = loadToolsRegistry(scope, startDir);
305
+ const aliases: string[] = [];
306
+
307
+ if (registry.aliases) {
308
+ for (const [alias, target] of Object.entries(registry.aliases)) {
309
+ if (target === toolName) {
310
+ aliases.push(alias);
311
+ }
312
+ }
313
+ }
314
+
315
+ return aliases;
316
+ }
317
+
318
+ /**
319
+ * Remove all aliases for a specific tool
320
+ * Useful when uninstalling a tool
321
+ * @param toolName - Full tool name
322
+ * @param scope - Registry scope
323
+ * @param startDir - Starting directory for project scope
324
+ * @returns Number of aliases removed
325
+ */
326
+ export function removeAliasesForTool(
327
+ toolName: string,
328
+ scope: RegistryScope,
329
+ startDir?: string
330
+ ): number {
331
+ const registry = loadToolsRegistry(scope, startDir);
332
+ let removed = 0;
333
+
334
+ if (registry.aliases) {
335
+ for (const [alias, target] of Object.entries(registry.aliases)) {
336
+ if (target === toolName) {
337
+ delete registry.aliases[alias];
338
+ removed++;
339
+ }
340
+ }
341
+
342
+ if (removed > 0) {
343
+ saveToolsRegistry(registry, scope, startDir);
344
+ }
345
+ }
346
+
347
+ return removed;
348
+ }
package/src/resolver.ts CHANGED
@@ -4,20 +4,22 @@
4
4
  * Resolution order:
5
5
  * 1. Direct file path (if provided path exists)
6
6
  * 2. Project tools (.enact/tools/{name}/)
7
- * 3. Global tools (via ~/.enact/tools.json → cache)
8
- * 4. Cache (~/.enact/cache/{name}/{version}/)
7
+ * 3. Global tools (via ~/.enact/tools.json → ~/.agent/skills/)
8
+ * 4. Skills directory (~/.agent/skills/{name}/)
9
9
  */
10
10
 
11
- import { existsSync, readdirSync } from "node:fs";
11
+ import { existsSync } from "node:fs";
12
12
  import { dirname, isAbsolute, join, resolve } from "node:path";
13
13
  import {
14
14
  type LoadManifestOptions,
15
15
  ManifestLoadError,
16
16
  findManifestFile,
17
17
  loadManifest,
18
+ loadManifestFromDir,
18
19
  } from "./manifest/loader";
19
- import { getCacheDir, getProjectEnactDir } from "./paths";
20
- import { getInstalledVersion, getToolCachePath } from "./registry";
20
+ import { manifestScriptsToActionsManifest } from "./manifest/scripts";
21
+ import { getProjectEnactDir, getSkillsDir } from "./paths";
22
+ import { getInstalledVersion, getToolCachePath, resolveAlias } from "./registry";
21
23
  import type { ToolLocation, ToolResolution } from "./types/manifest";
22
24
 
23
25
  /**
@@ -66,13 +68,70 @@ export interface ResolveOptions {
66
68
  skipCache?: boolean;
67
69
  }
68
70
 
71
+ /**
72
+ * Result of parsing an action specifier
73
+ */
74
+ export interface ParsedActionSpecifier {
75
+ /** The skill name (e.g., "owner/skill" from "owner/skill:action") */
76
+ skillName: string;
77
+ /** The action name (e.g., "action" from "owner/skill:action"), or undefined if not specified */
78
+ actionName?: string;
79
+ }
80
+
81
+ /**
82
+ * Parse an action specifier into skill name and action name
83
+ *
84
+ * Specifier formats (uses colon separator):
85
+ * - "owner/skill" - skill only, no action
86
+ * - "owner/skill:action" - skill with action
87
+ * - "./path" or "/path" - file path (no action parsing)
88
+ * - "./path:action" - file path with action
89
+ *
90
+ * Examples:
91
+ * - "mendable/firecrawl" → skill="mendable/firecrawl", action=undefined
92
+ * - "mendable/firecrawl:scrape" → skill="mendable/firecrawl", action="scrape"
93
+ * - "acme/tools/greeter:hello" → skill="acme/tools/greeter", action="hello"
94
+ * - "/tmp/skill" → skill="/tmp/skill", action=undefined (file path)
95
+ * - "./skill:hello" → skill="./skill", action="hello" (file path with action)
96
+ *
97
+ * @param specifier - The tool/action specifier string
98
+ * @returns Parsed skill name and optional action name
99
+ */
100
+ export function parseActionSpecifier(specifier: string): ParsedActionSpecifier {
101
+ const normalized = specifier.replace(/\\/g, "/").trim();
102
+
103
+ // Check for colon separator (action specifier)
104
+ const colonIndex = normalized.lastIndexOf(":");
105
+
106
+ // No colon - just a skill name or file path
107
+ if (colonIndex === -1) {
108
+ return { skillName: normalized };
109
+ }
110
+
111
+ // Windows drive letter check (e.g., "C:/path")
112
+ // If colon is at index 1 and followed by /, it's a Windows path
113
+ if (colonIndex === 1 && normalized.length > 2 && normalized[2] === "/") {
114
+ return { skillName: normalized };
115
+ }
116
+
117
+ // Split on colon
118
+ const skillName = normalized.slice(0, colonIndex);
119
+ const actionName = normalized.slice(colonIndex + 1);
120
+
121
+ // Validate action name is non-empty
122
+ if (!actionName || actionName.length === 0) {
123
+ return { skillName: normalized };
124
+ }
125
+
126
+ return { skillName, actionName };
127
+ }
128
+
69
129
  /**
70
130
  * Convert tool name to directory path
71
- * e.g., "acme/utils/greeter" -> "acme/utils/greeter"
131
+ * Strips the @ prefix for disk paths: "@org/my-skill" -> "org/my-skill"
72
132
  */
73
133
  export function toolNameToPath(name: string): string {
74
- // Tool names are already path-like, just normalize
75
- return name.replace(/\\/g, "/");
134
+ return name.replace(/^@/, "").replace(/\\/g, "/");
76
135
  }
77
136
 
78
137
  /**
@@ -92,6 +151,8 @@ export function getToolPath(toolsDir: string, toolName: string): string {
92
151
  /**
93
152
  * Try to load a tool from a specific directory
94
153
  *
154
+ * Supports two-file model (skill.yaml + SKILL.md) via loadManifestFromDir().
155
+ *
95
156
  * @param dir - Directory to check
96
157
  * @param location - The location type for metadata
97
158
  * @param options - Options for loading the manifest
@@ -106,22 +167,26 @@ function tryLoadFromDir(
106
167
  return null;
107
168
  }
108
169
 
109
- const manifestPath = findManifestFile(dir);
110
- if (!manifestPath) {
111
- return null;
112
- }
113
-
114
170
  try {
115
- const loaded = loadManifest(manifestPath, options);
116
- return {
171
+ const loaded = loadManifestFromDir(dir, options);
172
+
173
+ const resolution: ToolResolution = {
117
174
  manifest: loaded.manifest,
118
175
  sourceDir: dir,
119
176
  location,
120
- manifestPath,
177
+ manifestPath: loaded.filePath,
121
178
  version: loaded.manifest.version,
122
179
  };
180
+
181
+ // Convert inline scripts to actionsManifest
182
+ const scriptsManifest = manifestScriptsToActionsManifest(loaded.manifest);
183
+ if (scriptsManifest) {
184
+ resolution.actionsManifest = scriptsManifest;
185
+ }
186
+
187
+ return resolution;
123
188
  } catch {
124
- // Invalid manifest, skip
189
+ // No manifest or invalid manifest, skip
125
190
  return null;
126
191
  }
127
192
  }
@@ -153,13 +218,23 @@ export function resolveToolFromPath(filePath: string): ToolResolution {
153
218
  }
154
219
 
155
220
  const loaded = loadManifest(absolutePath, localOptions);
156
- return {
221
+ const sourceDir = dirname(absolutePath);
222
+
223
+ const resolution: ToolResolution = {
157
224
  manifest: loaded.manifest,
158
- sourceDir: dirname(absolutePath),
225
+ sourceDir,
159
226
  location: "file",
160
227
  manifestPath: absolutePath,
161
228
  version: loaded.manifest.version,
162
229
  };
230
+
231
+ // Convert inline scripts to actionsManifest
232
+ const scriptsManifest = manifestScriptsToActionsManifest(loaded.manifest);
233
+ if (scriptsManifest) {
234
+ resolution.actionsManifest = scriptsManifest;
235
+ }
236
+
237
+ return resolution;
163
238
  }
164
239
 
165
240
  // Treat as directory
@@ -171,18 +246,95 @@ export function resolveToolFromPath(filePath: string): ToolResolution {
171
246
  throw new ToolResolveError(`No manifest found at: ${absolutePath}`, filePath);
172
247
  }
173
248
 
249
+ /**
250
+ * Resolve a specific action from a tool/skill
251
+ *
252
+ * @param resolution - Already resolved tool
253
+ * @param actionName - Name of the action to resolve
254
+ * @returns ToolResolution with action field populated
255
+ * @throws ToolResolveError if action not found
256
+ */
257
+ export function resolveAction(resolution: ToolResolution, actionName: string): ToolResolution {
258
+ // Check if the skill has actions (from scripts or ACTIONS.yaml)
259
+ if (!resolution.actionsManifest) {
260
+ throw new ToolResolveError(
261
+ `Skill "${resolution.manifest.name}" does not define any scripts or actions. ` +
262
+ `Cannot resolve "${actionName}".`,
263
+ `${resolution.manifest.name}:${actionName}`
264
+ );
265
+ }
266
+
267
+ // Find the script/action (map lookup)
268
+ const action = resolution.actionsManifest.actions[actionName];
269
+ if (!action) {
270
+ const availableActions = Object.keys(resolution.actionsManifest.actions);
271
+ throw new ToolResolveError(
272
+ `Action "${actionName}" not found in skill "${resolution.manifest.name}". ` +
273
+ `Available actions: ${availableActions.join(", ")}`,
274
+ `${resolution.manifest.name}:${actionName}`
275
+ );
276
+ }
277
+
278
+ return {
279
+ ...resolution,
280
+ action,
281
+ actionName,
282
+ };
283
+ }
284
+
285
+ /**
286
+ * Resolve a tool with optional action specifier
287
+ *
288
+ * Supports formats:
289
+ * - "owner/skill" - resolves skill only
290
+ * - "owner/skill:action" - resolves skill and specific action
291
+ *
292
+ * @param specifier - Tool/action specifier (e.g., "mendable/firecrawl:scrape")
293
+ * @param options - Resolution options
294
+ * @returns ToolResolution with action populated if specified
295
+ * @throws ToolResolveError if not found
296
+ */
297
+ export function resolveToolWithAction(
298
+ specifier: string,
299
+ options: ResolveOptions = {}
300
+ ): ToolResolution {
301
+ const { skillName, actionName } = parseActionSpecifier(specifier);
302
+
303
+ // Resolve the skill first
304
+ const resolution = resolveTool(skillName, options);
305
+
306
+ // If no action specified, return as-is
307
+ if (!actionName) {
308
+ return resolution;
309
+ }
310
+
311
+ // Resolve the action
312
+ return resolveAction(resolution, actionName);
313
+ }
314
+
174
315
  /**
175
316
  * Resolve a tool by name, searching through standard locations
176
317
  *
177
- * @param toolName - Tool name (e.g., "acme/utils/greeter")
318
+ * @param toolName - Tool name (e.g., "acme/greeter" or "@acme/greeter") or alias (e.g., "firebase")
178
319
  * @param options - Resolution options
179
320
  * @returns ToolResolution
180
321
  * @throws ToolResolveError if not found
181
322
  */
182
323
  export function resolveTool(toolName: string, options: ResolveOptions = {}): ToolResolution {
183
- const normalizedName = normalizeToolName(toolName);
324
+ let normalizedName = normalizeToolName(toolName);
184
325
  const searchedLocations: string[] = [];
185
326
 
327
+ // Check if this might be an alias (no slashes = not a full tool name)
328
+ if (!normalizedName.includes("/")) {
329
+ // Try project-level alias first, then global
330
+ const aliasedName =
331
+ resolveAlias(normalizedName, "project", options.startDir) ??
332
+ resolveAlias(normalizedName, "global");
333
+ if (aliasedName) {
334
+ normalizedName = normalizeToolName(aliasedName);
335
+ }
336
+ }
337
+
186
338
  // 1. Try project tools (.enact/tools/{name}/)
187
339
  if (!options.skipProject) {
188
340
  const projectDir = getProjectEnactDir(options.startDir);
@@ -198,48 +350,29 @@ export function resolveTool(toolName: string, options: ResolveOptions = {}): Too
198
350
  }
199
351
  }
200
352
 
201
- // 2. Try global tools (via ~/.enact/tools.json → cache)
353
+ // 2. Try global tools (via ~/.enact/tools.json → ~/.agent/skills/)
202
354
  if (!options.skipUser) {
203
355
  const globalVersion = getInstalledVersion(normalizedName, "global");
204
356
  if (globalVersion) {
205
- const cachePath = getToolCachePath(normalizedName, globalVersion);
206
- searchedLocations.push(cachePath);
357
+ const skillPath = getToolCachePath(normalizedName, globalVersion);
358
+ searchedLocations.push(skillPath);
207
359
 
208
- const result = tryLoadFromDir(cachePath, "user");
360
+ const result = tryLoadFromDir(skillPath, "user");
209
361
  if (result) {
210
362
  return result;
211
363
  }
212
364
  }
213
365
  }
214
366
 
215
- // 3. Try cache (with optional version)
367
+ // 3. Try skills directory (~/.agent/skills/{name}/)
216
368
  if (!options.skipCache) {
217
- const cacheDir = getCacheDir();
218
- const toolCacheBase = getToolPath(cacheDir, normalizedName);
219
-
220
- if (options.version) {
221
- // Look for specific version
222
- const versionDir = join(toolCacheBase, `v${options.version.replace(/^v/, "")}`);
223
- searchedLocations.push(versionDir);
369
+ const skillsDir = getSkillsDir();
370
+ const skillDir = getToolPath(skillsDir, normalizedName);
371
+ searchedLocations.push(skillDir);
224
372
 
225
- const result = tryLoadFromDir(versionDir, "cache");
226
- if (result) {
227
- return result;
228
- }
229
- } else {
230
- // Look for latest cached version
231
- if (existsSync(toolCacheBase)) {
232
- const latestVersion = findLatestCachedVersion(toolCacheBase);
233
- if (latestVersion) {
234
- const versionDir = join(toolCacheBase, latestVersion);
235
- searchedLocations.push(versionDir);
236
-
237
- const result = tryLoadFromDir(versionDir, "cache");
238
- if (result) {
239
- return result;
240
- }
241
- }
242
- }
373
+ const result = tryLoadFromDir(skillDir, "cache");
374
+ if (result) {
375
+ return result;
243
376
  }
244
377
  }
245
378
 
@@ -250,33 +383,6 @@ export function resolveTool(toolName: string, options: ResolveOptions = {}): Too
250
383
  );
251
384
  }
252
385
 
253
- /**
254
- * Find the latest cached version directory
255
- */
256
- function findLatestCachedVersion(toolCacheBase: string): string | null {
257
- try {
258
- const entries = readdirSync(toolCacheBase, { withFileTypes: true });
259
- const versions = entries
260
- .filter((e) => e.isDirectory() && e.name.startsWith("v"))
261
- .map((e) => e.name)
262
- .sort((a, b) => {
263
- // Sort by semver (v1.0.0 format)
264
- const aVer = a.slice(1).split(".").map(Number);
265
- const bVer = b.slice(1).split(".").map(Number);
266
- for (let i = 0; i < 3; i++) {
267
- if ((aVer[i] ?? 0) !== (bVer[i] ?? 0)) {
268
- return (bVer[i] ?? 0) - (aVer[i] ?? 0);
269
- }
270
- }
271
- return 0;
272
- });
273
-
274
- return versions[0] ?? null;
275
- } catch {
276
- return null;
277
- }
278
- }
279
-
280
386
  /**
281
387
  * Try to resolve a tool, returning null instead of throwing
282
388
  *
@@ -472,7 +578,7 @@ export function getToolSearchPaths(toolName: string, options: ResolveOptions = {
472
578
 
473
579
  // Cache
474
580
  if (!options.skipCache) {
475
- const cacheDir = getCacheDir();
581
+ const cacheDir = getSkillsDir();
476
582
  paths.push(join(cacheDir, toolNameToPath(normalizedName)));
477
583
  }
478
584