@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.
- package/README.md +1 -18
- package/dist/config.d.ts +12 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +32 -6
- package/dist/config.js.map +1 -1
- package/dist/execution/action-command.d.ts +131 -0
- package/dist/execution/action-command.d.ts.map +1 -0
- package/dist/execution/action-command.js +300 -0
- package/dist/execution/action-command.js.map +1 -0
- package/dist/execution/command.d.ts +8 -8
- package/dist/execution/command.js +6 -6
- package/dist/execution/index.d.ts +1 -0
- package/dist/execution/index.d.ts.map +1 -1
- package/dist/execution/index.js +2 -0
- package/dist/execution/index.js.map +1 -1
- package/dist/execution/types.d.ts +5 -2
- package/dist/execution/types.d.ts.map +1 -1
- package/dist/execution/types.js.map +1 -1
- package/dist/index.d.ts +9 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -5
- package/dist/index.js.map +1 -1
- package/dist/manifest/actions-loader.d.ts +29 -0
- package/dist/manifest/actions-loader.d.ts.map +1 -0
- package/dist/manifest/actions-loader.js +34 -0
- package/dist/manifest/actions-loader.js.map +1 -0
- package/dist/manifest/actions-parser.d.ts +69 -0
- package/dist/manifest/actions-parser.d.ts.map +1 -0
- package/dist/manifest/actions-parser.js +265 -0
- package/dist/manifest/actions-parser.js.map +1 -0
- package/dist/manifest/index.d.ts +2 -0
- package/dist/manifest/index.d.ts.map +1 -1
- package/dist/manifest/index.js +4 -0
- package/dist/manifest/index.js.map +1 -1
- package/dist/manifest/loader.d.ts +7 -2
- package/dist/manifest/loader.d.ts.map +1 -1
- package/dist/manifest/loader.js +71 -4
- package/dist/manifest/loader.js.map +1 -1
- package/dist/manifest/parser.d.ts +1 -0
- package/dist/manifest/parser.d.ts.map +1 -1
- package/dist/manifest/parser.js +1 -0
- package/dist/manifest/parser.js.map +1 -1
- package/dist/manifest/scripts.d.ts +19 -0
- package/dist/manifest/scripts.d.ts.map +1 -0
- package/dist/manifest/scripts.js +102 -0
- package/dist/manifest/scripts.js.map +1 -0
- package/dist/manifest/validator.d.ts +1 -8
- package/dist/manifest/validator.d.ts.map +1 -1
- package/dist/manifest/validator.js +14 -13
- package/dist/manifest/validator.js.map +1 -1
- package/dist/mcp-registry.js +5 -5
- package/dist/mcp-registry.js.map +1 -1
- package/dist/paths.d.ts +9 -2
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +12 -3
- package/dist/paths.js.map +1 -1
- package/dist/registry.d.ts +47 -2
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +100 -7
- package/dist/registry.js.map +1 -1
- package/dist/resolver.d.ts +55 -4
- package/dist/resolver.d.ts.map +1 -1
- package/dist/resolver.js +144 -77
- package/dist/resolver.js.map +1 -1
- package/dist/types/actions.d.ts +194 -0
- package/dist/types/actions.d.ts.map +1 -0
- package/dist/types/actions.js +32 -0
- package/dist/types/actions.js.map +1 -0
- package/dist/types/index.d.ts +3 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/manifest.d.ts +50 -5
- package/dist/types/manifest.d.ts.map +1 -1
- package/dist/types/manifest.js +10 -2
- package/dist/types/manifest.js.map +1 -1
- package/package.json +2 -2
- package/src/config.ts +48 -6
- package/src/execution/action-command.ts +417 -0
- package/src/execution/command.ts +8 -8
- package/src/execution/index.ts +17 -0
- package/src/execution/types.ts +13 -2
- package/src/index.ts +43 -0
- package/src/manifest/actions-loader.ts +49 -0
- package/src/manifest/index.ts +12 -0
- package/src/manifest/loader.ts +77 -4
- package/src/manifest/parser.ts +1 -0
- package/src/manifest/scripts.ts +116 -0
- package/src/manifest/validator.ts +15 -14
- package/src/mcp-registry.ts +5 -5
- package/src/paths.ts +13 -3
- package/src/registry.ts +136 -7
- package/src/resolver.ts +185 -79
- package/src/types/actions.ts +223 -0
- package/src/types/index.ts +11 -0
- package/src/types/manifest.ts +67 -6
- package/tests/action-command.test.ts +249 -0
- package/tests/config-normalization.test.ts +279 -0
- package/tests/config.test.ts +4 -1
- package/tests/effective-input-schema.test.ts +86 -0
- package/tests/fixtures/valid-tool.md +5 -12
- package/tests/fixtures/valid-tool.yaml +3 -10
- package/tests/hooks.test.ts +177 -0
- package/tests/manifest/loader.test.ts +34 -20
- package/tests/manifest/parser.test.ts +11 -15
- package/tests/manifest/validator.test.ts +7 -17
- package/tests/manifest-types.test.ts +9 -11
- package/tests/paths.test.ts +11 -4
- package/tests/registry.test.ts +204 -8
- package/tests/resolver.test.ts +90 -6
- 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
|
|
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,
|
|
168
|
-
const
|
|
169
|
-
|
|
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 →
|
|
8
|
-
* 4.
|
|
7
|
+
* 3. Global tools (via ~/.enact/tools.json → ~/.agent/skills/)
|
|
8
|
+
* 4. Skills directory (~/.agent/skills/{name}/)
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { existsSync
|
|
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 {
|
|
20
|
-
import {
|
|
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
|
-
*
|
|
131
|
+
* Strips the @ prefix for disk paths: "@org/my-skill" -> "org/my-skill"
|
|
72
132
|
*/
|
|
73
133
|
export function toolNameToPath(name: string): string {
|
|
74
|
-
|
|
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 =
|
|
116
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
221
|
+
const sourceDir = dirname(absolutePath);
|
|
222
|
+
|
|
223
|
+
const resolution: ToolResolution = {
|
|
157
224
|
manifest: loaded.manifest,
|
|
158
|
-
sourceDir
|
|
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/
|
|
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
|
-
|
|
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 →
|
|
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
|
|
206
|
-
searchedLocations.push(
|
|
357
|
+
const skillPath = getToolCachePath(normalizedName, globalVersion);
|
|
358
|
+
searchedLocations.push(skillPath);
|
|
207
359
|
|
|
208
|
-
const result = tryLoadFromDir(
|
|
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
|
|
367
|
+
// 3. Try skills directory (~/.agent/skills/{name}/)
|
|
216
368
|
if (!options.skipCache) {
|
|
217
|
-
const
|
|
218
|
-
const
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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 =
|
|
581
|
+
const cacheDir = getSkillsDir();
|
|
476
582
|
paths.push(join(cacheDir, toolNameToPath(normalizedName)));
|
|
477
583
|
}
|
|
478
584
|
|