@enactprotocol/cli 2.2.4 → 2.3.4
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 +4 -37
- package/dist/commands/cache/index.js +5 -5
- package/dist/commands/cache/index.js.map +1 -1
- package/dist/commands/index.d.ts +1 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +2 -0
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/inspect/index.d.ts.map +1 -1
- package/dist/commands/inspect/index.js +3 -2
- package/dist/commands/inspect/index.js.map +1 -1
- package/dist/commands/install/index.d.ts +1 -1
- package/dist/commands/install/index.d.ts.map +1 -1
- package/dist/commands/install/index.js +69 -21
- package/dist/commands/install/index.js.map +1 -1
- package/dist/commands/learn/index.d.ts.map +1 -1
- package/dist/commands/learn/index.js +73 -14
- package/dist/commands/learn/index.js.map +1 -1
- package/dist/commands/list/index.d.ts +1 -1
- package/dist/commands/list/index.js +1 -1
- package/dist/commands/run/index.d.ts.map +1 -1
- package/dist/commands/run/index.js +166 -60
- package/dist/commands/run/index.js.map +1 -1
- package/dist/commands/serve/index.d.ts +9 -0
- package/dist/commands/serve/index.d.ts.map +1 -0
- package/dist/commands/serve/index.js +24 -0
- package/dist/commands/serve/index.js.map +1 -0
- package/dist/commands/validate/index.d.ts.map +1 -1
- package/dist/commands/validate/index.js +7 -37
- package/dist/commands/validate/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/utils/errors.js +2 -2
- package/package.json +6 -5
- package/src/commands/cache/index.ts +5 -5
- package/src/commands/env/README.md +1 -1
- package/src/commands/index.ts +3 -0
- package/src/commands/inspect/index.ts +3 -2
- package/src/commands/install/README.md +2 -2
- package/src/commands/install/index.ts +98 -21
- package/src/commands/learn/index.ts +89 -18
- package/src/commands/list/index.ts +1 -1
- package/src/commands/run/README.md +1 -1
- package/src/commands/run/index.ts +218 -67
- package/src/commands/serve/index.ts +26 -0
- package/src/commands/validate/index.ts +7 -42
- package/src/index.ts +5 -1
- package/src/utils/errors.ts +2 -2
- package/tests/commands/cache.test.ts +2 -2
- package/tests/commands/install-integration.test.ts +11 -12
- package/tests/commands/publish.test.ts +12 -2
- package/tests/commands/run.test.ts +3 -1
- package/tests/commands/serve.test.ts +82 -0
- package/tests/commands/sign.test.ts +1 -1
- package/tests/e2e.test.ts +56 -34
- package/tests/fixtures/calculator/skill.yaml +38 -0
- package/tests/fixtures/echo-tool/SKILL.md +3 -10
- package/tests/fixtures/env-tool/{enact.yaml → skill.yaml} +0 -6
- package/tests/fixtures/greeter/skill.yaml +22 -0
- package/tests/utils/ignore.test.ts +3 -1
- package/tsconfig.json +2 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/tests/fixtures/calculator/enact.yaml +0 -34
- package/tests/fixtures/greeter/enact.yaml +0 -18
- /package/tests/fixtures/invalid-tool/{enact.yaml → skill.yaml} +0 -0
|
@@ -33,17 +33,28 @@ import {
|
|
|
33
33
|
getToolVersion,
|
|
34
34
|
verifyAllAttestations,
|
|
35
35
|
} from "@enactprotocol/api";
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
DaggerExecutionProvider,
|
|
38
|
+
DockerExecutionProvider,
|
|
39
|
+
type ExecutionResult,
|
|
40
|
+
ExecutionRouter,
|
|
41
|
+
LocalExecutionProvider,
|
|
42
|
+
} from "@enactprotocol/execution";
|
|
37
43
|
import { resolveSecrets, resolveToolEnv } from "@enactprotocol/secrets";
|
|
38
44
|
import {
|
|
45
|
+
type Action,
|
|
46
|
+
type ActionsManifest,
|
|
39
47
|
type ToolManifest,
|
|
40
48
|
type ToolResolution,
|
|
41
49
|
applyDefaults,
|
|
42
50
|
getCacheDir,
|
|
51
|
+
getEffectiveInputSchema,
|
|
43
52
|
getMinimumAttestations,
|
|
44
53
|
getTrustPolicy,
|
|
45
54
|
getTrustedAuditors,
|
|
46
55
|
loadConfig,
|
|
56
|
+
parseActionSpecifier,
|
|
57
|
+
prepareActionCommand,
|
|
47
58
|
prepareCommand,
|
|
48
59
|
toolNameToPath,
|
|
49
60
|
tryResolveTool,
|
|
@@ -85,6 +96,7 @@ interface RunOptions extends GlobalOptions {
|
|
|
85
96
|
output?: string;
|
|
86
97
|
apply?: boolean;
|
|
87
98
|
debug?: boolean;
|
|
99
|
+
action?: string;
|
|
88
100
|
}
|
|
89
101
|
|
|
90
102
|
/**
|
|
@@ -306,22 +318,22 @@ function atomicReplace(targetDir: string, sourceDir: string): void {
|
|
|
306
318
|
async function extractToCache(
|
|
307
319
|
bundleData: ArrayBuffer,
|
|
308
320
|
toolName: string,
|
|
309
|
-
|
|
321
|
+
_version: string
|
|
310
322
|
): Promise<string> {
|
|
311
323
|
const cacheDir = getCacheDir();
|
|
312
324
|
const toolPath = toolNameToPath(toolName);
|
|
313
|
-
const
|
|
325
|
+
const destDir = join(cacheDir, toolPath);
|
|
314
326
|
|
|
315
327
|
// Create a temporary file for the bundle
|
|
316
328
|
const tempFile = join(cacheDir, `bundle-${Date.now()}.tar.gz`);
|
|
317
329
|
mkdirSync(dirname(tempFile), { recursive: true });
|
|
318
330
|
writeFileSync(tempFile, Buffer.from(bundleData));
|
|
319
331
|
|
|
320
|
-
// Create destination directory
|
|
321
|
-
mkdirSync(
|
|
332
|
+
// Create destination directory (flat, no version subdirectory — matches resolver)
|
|
333
|
+
mkdirSync(destDir, { recursive: true });
|
|
322
334
|
|
|
323
335
|
// Extract using tar command
|
|
324
|
-
const proc = Bun.spawn(["tar", "-xzf", tempFile, "-C",
|
|
336
|
+
const proc = Bun.spawn(["tar", "-xzf", tempFile, "-C", destDir], {
|
|
325
337
|
stdout: "pipe",
|
|
326
338
|
stderr: "pipe",
|
|
327
339
|
});
|
|
@@ -340,7 +352,7 @@ async function extractToCache(
|
|
|
340
352
|
throw new Error(`Failed to extract bundle: ${stderr}`);
|
|
341
353
|
}
|
|
342
354
|
|
|
343
|
-
return
|
|
355
|
+
return destDir;
|
|
344
356
|
}
|
|
345
357
|
|
|
346
358
|
/**
|
|
@@ -438,22 +450,26 @@ async function fetchAndCacheTool(
|
|
|
438
450
|
const attestations = attestationsResponse.attestations;
|
|
439
451
|
|
|
440
452
|
if (attestations.length === 0) {
|
|
441
|
-
// No attestations found
|
|
442
|
-
|
|
453
|
+
// No attestations found — but if minimum_attestations is 0, that's fine
|
|
454
|
+
if (minimumAttestations === 0) {
|
|
455
|
+
// User explicitly configured zero required attestations — allow execution
|
|
456
|
+
} else {
|
|
457
|
+
info(`${symbols.warning} Tool ${toolName}@${targetVersion} has no attestations.`);
|
|
443
458
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
459
|
+
if (trustPolicy === "require_attestation") {
|
|
460
|
+
throw new TrustError("Trust policy requires attestations. Execution blocked.");
|
|
461
|
+
}
|
|
462
|
+
if (ctx.isInteractive && trustPolicy === "prompt") {
|
|
463
|
+
const proceed = await confirm("Run unverified tool?");
|
|
464
|
+
if (!proceed) {
|
|
465
|
+
info("Execution cancelled.");
|
|
466
|
+
process.exit(0);
|
|
467
|
+
}
|
|
468
|
+
} else if (!ctx.isInteractive && trustPolicy === "prompt") {
|
|
469
|
+
throw new TrustError("Cannot run unverified tools in non-interactive mode.");
|
|
452
470
|
}
|
|
453
|
-
} else if (!ctx.isInteractive && trustPolicy === "prompt") {
|
|
454
|
-
throw new TrustError("Cannot run unverified tools in non-interactive mode.");
|
|
455
471
|
}
|
|
456
|
-
// trustPolicy === "allow" - continue without prompting
|
|
472
|
+
// trustPolicy === "allow" or minimumAttestations === 0 - continue without prompting
|
|
457
473
|
} else {
|
|
458
474
|
// Verify attestations locally (never trust registry's verification status)
|
|
459
475
|
const verifiedAuditors = await verifyAllAttestations(
|
|
@@ -635,7 +651,6 @@ function displayDryRun(
|
|
|
635
651
|
* Display debug information about parameter resolution
|
|
636
652
|
*/
|
|
637
653
|
function displayDebugInfo(
|
|
638
|
-
manifest: ToolManifest,
|
|
639
654
|
rawInputs: Record<string, unknown>,
|
|
640
655
|
inputsWithDefaults: Record<string, unknown>,
|
|
641
656
|
finalInputs: Record<string, unknown>,
|
|
@@ -646,21 +661,7 @@ function displayDebugInfo(
|
|
|
646
661
|
info(colors.bold("Debug: Parameter Resolution"));
|
|
647
662
|
newline();
|
|
648
663
|
|
|
649
|
-
//
|
|
650
|
-
if (manifest.inputSchema?.properties) {
|
|
651
|
-
info("Schema Properties:");
|
|
652
|
-
const required = new Set(manifest.inputSchema.required || []);
|
|
653
|
-
for (const [name, prop] of Object.entries(manifest.inputSchema.properties)) {
|
|
654
|
-
const propSchema = prop as { type?: string; default?: unknown; description?: string };
|
|
655
|
-
const isRequired = required.has(name);
|
|
656
|
-
const hasDefault = propSchema.default !== undefined;
|
|
657
|
-
const status = isRequired ? colors.error("required") : colors.dim("optional");
|
|
658
|
-
dim(
|
|
659
|
-
` ${name}: ${propSchema.type || "any"} [${status}]${hasDefault ? ` (default: ${JSON.stringify(propSchema.default)})` : ""}`
|
|
660
|
-
);
|
|
661
|
-
}
|
|
662
|
-
newline();
|
|
663
|
-
}
|
|
664
|
+
// Schema information is available per-script via action.inputSchema
|
|
664
665
|
|
|
665
666
|
// Show raw inputs (what was provided)
|
|
666
667
|
info("Raw Inputs (provided by user):");
|
|
@@ -768,6 +769,48 @@ function displayResult(result: ExecutionResult, options: RunOptions): void {
|
|
|
768
769
|
}
|
|
769
770
|
}
|
|
770
771
|
|
|
772
|
+
/**
|
|
773
|
+
* Check if a specifier looks like a file path
|
|
774
|
+
*/
|
|
775
|
+
function isFilePathSpecifier(specifier: string): boolean {
|
|
776
|
+
return (
|
|
777
|
+
specifier.startsWith("/") ||
|
|
778
|
+
specifier.startsWith("./") ||
|
|
779
|
+
specifier.startsWith("../") ||
|
|
780
|
+
specifier === "."
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* For file paths like /tmp/skill/action, try to parse the last segment as an action
|
|
786
|
+
* Returns { parentPath, actionName } or null if not applicable
|
|
787
|
+
*/
|
|
788
|
+
function tryParseFilePathAction(
|
|
789
|
+
specifier: string
|
|
790
|
+
): { parentPath: string; actionName: string } | null {
|
|
791
|
+
if (!isFilePathSpecifier(specifier)) {
|
|
792
|
+
return null;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const normalized = specifier.replace(/\\/g, "/");
|
|
796
|
+
const lastSlash = normalized.lastIndexOf("/");
|
|
797
|
+
|
|
798
|
+
// Need at least one slash and something after it
|
|
799
|
+
if (lastSlash <= 0 || lastSlash === normalized.length - 1) {
|
|
800
|
+
return null;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const parentPath = normalized.slice(0, lastSlash);
|
|
804
|
+
const actionName = normalized.slice(lastSlash + 1);
|
|
805
|
+
|
|
806
|
+
// Action name shouldn't look like a file extension
|
|
807
|
+
if (actionName.includes(".")) {
|
|
808
|
+
return null;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
return { parentPath, actionName };
|
|
812
|
+
}
|
|
813
|
+
|
|
771
814
|
/**
|
|
772
815
|
* Run command handler
|
|
773
816
|
*/
|
|
@@ -775,8 +818,16 @@ async function runHandler(tool: string, options: RunOptions, ctx: CommandContext
|
|
|
775
818
|
let resolution: ToolResolution | null = null;
|
|
776
819
|
let resolveResult: ReturnType<typeof tryResolveToolDetailed> | null = null;
|
|
777
820
|
|
|
821
|
+
// Parse the tool specifier to check for action (owner/skill/action format)
|
|
822
|
+
const { skillName, actionName: parsedActionName } = parseActionSpecifier(tool);
|
|
823
|
+
|
|
824
|
+
// Use --action flag if provided, otherwise use parsed action name
|
|
825
|
+
let actionName = options.action ?? parsedActionName;
|
|
826
|
+
let hasActionSpecifier = actionName !== undefined;
|
|
827
|
+
|
|
778
828
|
// Check if --remote flag is valid (requires namespace/name format)
|
|
779
|
-
const isRegistryFormat =
|
|
829
|
+
const isRegistryFormat =
|
|
830
|
+
skillName.includes("/") && !skillName.startsWith("/") && !skillName.startsWith(".");
|
|
780
831
|
if (options.remote && !isRegistryFormat) {
|
|
781
832
|
throw new ValidationError(
|
|
782
833
|
`--remote requires a registry tool name (e.g., user/tool), got: ${tool}`
|
|
@@ -786,21 +837,44 @@ async function runHandler(tool: string, options: RunOptions, ctx: CommandContext
|
|
|
786
837
|
// Skip local resolution if --remote is set
|
|
787
838
|
if (!options.remote) {
|
|
788
839
|
// First, try to resolve locally (project → user → cache)
|
|
840
|
+
// Use the skill name (without action) for initial resolution
|
|
789
841
|
if (!options.verbose) {
|
|
790
|
-
resolveResult = tryResolveToolDetailed(
|
|
842
|
+
resolveResult = tryResolveToolDetailed(skillName, { startDir: ctx.cwd });
|
|
791
843
|
resolution = resolveResult.resolution;
|
|
792
844
|
} else {
|
|
793
845
|
const spinner = clack.spinner();
|
|
794
|
-
spinner.start(
|
|
795
|
-
|
|
846
|
+
spinner.start(
|
|
847
|
+
`Resolving tool: ${skillName}${hasActionSpecifier ? ` (action: ${actionName})` : ""}`
|
|
848
|
+
);
|
|
849
|
+
resolveResult = tryResolveToolDetailed(skillName, { startDir: ctx.cwd });
|
|
796
850
|
resolution = resolveResult.resolution;
|
|
797
851
|
if (resolution) {
|
|
798
|
-
spinner.stop(`${symbols.success} Resolved: ${
|
|
852
|
+
spinner.stop(`${symbols.success} Resolved: ${skillName}`);
|
|
799
853
|
} else {
|
|
800
854
|
spinner.stop(`${symbols.info} Checking registry...`);
|
|
801
855
|
}
|
|
802
856
|
}
|
|
803
857
|
|
|
858
|
+
// If resolution failed and this is a file path, try treating the last segment as an action
|
|
859
|
+
// e.g., /tmp/skill/hello -> try /tmp/skill with action "hello"
|
|
860
|
+
if (!resolution && isFilePathSpecifier(tool) && !options.action) {
|
|
861
|
+
const parsed = tryParseFilePathAction(tool);
|
|
862
|
+
if (parsed) {
|
|
863
|
+
const parentResult = tryResolveToolDetailed(parsed.parentPath, { startDir: ctx.cwd });
|
|
864
|
+
if (parentResult.resolution?.actionsManifest) {
|
|
865
|
+
// Found a skill with actions at the parent path
|
|
866
|
+
resolution = parentResult.resolution;
|
|
867
|
+
resolveResult = parentResult;
|
|
868
|
+
actionName = parsed.actionName;
|
|
869
|
+
hasActionSpecifier = true;
|
|
870
|
+
|
|
871
|
+
if (options.verbose) {
|
|
872
|
+
info(`Detected action from path: ${parsed.actionName}`);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
804
878
|
// If manifest was found but had errors, throw a descriptive error immediately
|
|
805
879
|
if (!resolution && resolveResult?.manifestFound && resolveResult?.error) {
|
|
806
880
|
const errorMessage = resolveResult.error.message;
|
|
@@ -816,31 +890,61 @@ async function runHandler(tool: string, options: RunOptions, ctx: CommandContext
|
|
|
816
890
|
// Check if this looks like a tool name (namespace/name format)
|
|
817
891
|
if (isRegistryFormat) {
|
|
818
892
|
resolution = !options.verbose
|
|
819
|
-
? await fetchAndCacheTool(
|
|
893
|
+
? await fetchAndCacheTool(skillName, options, ctx)
|
|
820
894
|
: await withSpinner(
|
|
821
|
-
`Fetching ${
|
|
822
|
-
async () => fetchAndCacheTool(
|
|
823
|
-
`${symbols.success} Cached: ${
|
|
895
|
+
`Fetching ${skillName} from registry...`,
|
|
896
|
+
async () => fetchAndCacheTool(skillName, options, ctx),
|
|
897
|
+
`${symbols.success} Cached: ${skillName}`
|
|
824
898
|
);
|
|
825
899
|
}
|
|
826
900
|
}
|
|
827
901
|
|
|
828
902
|
if (!resolution) {
|
|
829
903
|
if (options.local) {
|
|
830
|
-
throw new ToolNotFoundError(
|
|
904
|
+
throw new ToolNotFoundError(skillName, {
|
|
831
905
|
localOnly: true,
|
|
832
906
|
...(resolveResult?.searchedLocations && {
|
|
833
907
|
searchedLocations: resolveResult.searchedLocations,
|
|
834
908
|
}),
|
|
835
909
|
});
|
|
836
910
|
}
|
|
837
|
-
throw new ToolNotFoundError(
|
|
911
|
+
throw new ToolNotFoundError(skillName, {
|
|
838
912
|
...(resolveResult?.searchedLocations && {
|
|
839
913
|
searchedLocations: resolveResult.searchedLocations,
|
|
840
914
|
}),
|
|
841
915
|
});
|
|
842
916
|
}
|
|
843
917
|
|
|
918
|
+
// If a script was specified via colon syntax, resolve it from the manifest's scripts
|
|
919
|
+
let resolvedAction: Action | undefined;
|
|
920
|
+
let actionsManifest: ActionsManifest | undefined;
|
|
921
|
+
|
|
922
|
+
// Use resolved skill identifier for error messages (manifest name or source directory)
|
|
923
|
+
const resolvedSkillId = resolution.manifest.name ?? resolution.sourceDir;
|
|
924
|
+
|
|
925
|
+
if (hasActionSpecifier) {
|
|
926
|
+
if (!resolution.actionsManifest) {
|
|
927
|
+
throw new ValidationError(
|
|
928
|
+
`Skill "${resolvedSkillId}" does not define any scripts. Cannot execute script "${actionName}".`
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
actionsManifest = resolution.actionsManifest;
|
|
933
|
+
// Map lookup - action name is the key
|
|
934
|
+
resolvedAction = actionsManifest.actions[actionName!];
|
|
935
|
+
|
|
936
|
+
if (!resolvedAction) {
|
|
937
|
+
const availableActions = Object.keys(actionsManifest.actions).join(", ");
|
|
938
|
+
throw new ValidationError(
|
|
939
|
+
`Action "${actionName}" not found in skill "${resolvedSkillId}". Available actions: ${availableActions}`
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
if (options.verbose) {
|
|
944
|
+
info(`Using action: ${actionName}`);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
844
948
|
const manifest = resolution.manifest;
|
|
845
949
|
|
|
846
950
|
// Parse --input flags to separate key=value params from path inputs
|
|
@@ -852,13 +956,16 @@ async function runHandler(tool: string, options: RunOptions, ctx: CommandContext
|
|
|
852
956
|
// Merge inputs: path params override other inputs
|
|
853
957
|
const inputs = { ...otherInputs, ...pathParams };
|
|
854
958
|
|
|
959
|
+
// Use action's inputSchema if executing an action, otherwise no schema
|
|
960
|
+
const effectiveInputSchema = resolvedAction ? getEffectiveInputSchema(resolvedAction) : undefined;
|
|
961
|
+
|
|
855
962
|
// Apply defaults from schema
|
|
856
|
-
const inputsWithDefaults =
|
|
857
|
-
? applyDefaults(inputs,
|
|
963
|
+
const inputsWithDefaults = effectiveInputSchema
|
|
964
|
+
? applyDefaults(inputs, effectiveInputSchema)
|
|
858
965
|
: inputs;
|
|
859
966
|
|
|
860
967
|
// Validate inputs against schema
|
|
861
|
-
const validation = validateInputs(inputsWithDefaults,
|
|
968
|
+
const validation = validateInputs(inputsWithDefaults, effectiveInputSchema);
|
|
862
969
|
if (!validation.valid) {
|
|
863
970
|
const errors = validation.errors.map((err) => `${err.path}: ${err.message}`).join(", ");
|
|
864
971
|
throw new ValidationError(`Input validation failed: ${errors}`);
|
|
@@ -905,8 +1012,8 @@ async function runHandler(tool: string, options: RunOptions, ctx: CommandContext
|
|
|
905
1012
|
}
|
|
906
1013
|
}
|
|
907
1014
|
|
|
908
|
-
// Check if this is an instruction-based tool (no command)
|
|
909
|
-
if (!manifest.command) {
|
|
1015
|
+
// Check if this is an instruction-based tool (no command) - but actions always have commands
|
|
1016
|
+
if (!manifest.command && !resolvedAction) {
|
|
910
1017
|
// For instruction tools, just display the markdown body
|
|
911
1018
|
let instructions: string | undefined;
|
|
912
1019
|
|
|
@@ -947,15 +1054,25 @@ async function runHandler(tool: string, options: RunOptions, ctx: CommandContext
|
|
|
947
1054
|
return;
|
|
948
1055
|
}
|
|
949
1056
|
|
|
950
|
-
// Prepare command
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1057
|
+
// Prepare command
|
|
1058
|
+
// For actions: use {{param}} template system (array form, no shell)
|
|
1059
|
+
// For regular tools: use ${param} template system (shell interpolation)
|
|
1060
|
+
let command: string[];
|
|
1061
|
+
|
|
1062
|
+
if (resolvedAction) {
|
|
1063
|
+
// Action execution: use prepareActionCommand for {{param}} templates
|
|
1064
|
+
const actionCommand = resolvedAction.command;
|
|
1065
|
+
if (typeof actionCommand === "string") {
|
|
1066
|
+
// String form (no templates allowed by validation)
|
|
1067
|
+
command = actionCommand.split(/\s+/).filter((s) => s.length > 0);
|
|
1068
|
+
} else {
|
|
1069
|
+
// Array form with {{param}} templates
|
|
1070
|
+
command = prepareActionCommand(actionCommand, finalInputs, resolvedAction.inputSchema);
|
|
1071
|
+
}
|
|
1072
|
+
} else {
|
|
1073
|
+
// Regular tool execution: use prepareCommand for ${param} templates
|
|
1074
|
+
command = prepareCommand(manifest.command!, finalInputs);
|
|
1075
|
+
}
|
|
959
1076
|
|
|
960
1077
|
// Resolve environment variables (non-secrets)
|
|
961
1078
|
const { resolved: envResolved } = resolveToolEnv(manifest.env ?? {}, ctx.cwd);
|
|
@@ -1016,7 +1133,7 @@ async function runHandler(tool: string, options: RunOptions, ctx: CommandContext
|
|
|
1016
1133
|
|
|
1017
1134
|
// Debug mode - show detailed parameter resolution info
|
|
1018
1135
|
if (options.debug) {
|
|
1019
|
-
displayDebugInfo(
|
|
1136
|
+
displayDebugInfo(inputs, inputsWithDefaults, finalInputs, envVars, command);
|
|
1020
1137
|
}
|
|
1021
1138
|
|
|
1022
1139
|
// Dry run mode
|
|
@@ -1033,7 +1150,7 @@ async function runHandler(tool: string, options: RunOptions, ctx: CommandContext
|
|
|
1033
1150
|
return;
|
|
1034
1151
|
}
|
|
1035
1152
|
|
|
1036
|
-
// Execute the tool
|
|
1153
|
+
// Execute the tool — select backend via execution router
|
|
1037
1154
|
const providerConfig: { defaultTimeout?: number; verbose?: boolean } = {};
|
|
1038
1155
|
if (options.timeout) {
|
|
1039
1156
|
providerConfig.defaultTimeout = parseTimeout(options.timeout);
|
|
@@ -1042,7 +1159,20 @@ async function runHandler(tool: string, options: RunOptions, ctx: CommandContext
|
|
|
1042
1159
|
providerConfig.verbose = true;
|
|
1043
1160
|
}
|
|
1044
1161
|
|
|
1045
|
-
const
|
|
1162
|
+
const config = loadConfig();
|
|
1163
|
+
const router = new ExecutionRouter({
|
|
1164
|
+
default: config.execution?.default,
|
|
1165
|
+
fallback: config.execution?.fallback,
|
|
1166
|
+
trusted_scopes: config.execution?.trusted_scopes,
|
|
1167
|
+
});
|
|
1168
|
+
router.registerProvider("local", new LocalExecutionProvider(providerConfig));
|
|
1169
|
+
router.registerProvider("docker", new DockerExecutionProvider(providerConfig));
|
|
1170
|
+
router.registerProvider("dagger", new DaggerExecutionProvider(providerConfig));
|
|
1171
|
+
|
|
1172
|
+
const provider = await router.selectProvider(manifest.name, {
|
|
1173
|
+
forceLocal: options.local,
|
|
1174
|
+
forceRemote: options.remote,
|
|
1175
|
+
});
|
|
1046
1176
|
|
|
1047
1177
|
// For --apply, we export to a temp directory first, then atomically replace
|
|
1048
1178
|
let tempOutputDir: string | undefined;
|
|
@@ -1070,6 +1200,21 @@ async function runHandler(tool: string, options: RunOptions, ctx: CommandContext
|
|
|
1070
1200
|
execOptions.outputPath = resolve(options.output);
|
|
1071
1201
|
}
|
|
1072
1202
|
|
|
1203
|
+
// Use executeAction for actions, execute for regular tools
|
|
1204
|
+
if (resolvedAction && actionsManifest && actionName) {
|
|
1205
|
+
return provider.executeAction(
|
|
1206
|
+
manifest,
|
|
1207
|
+
actionsManifest,
|
|
1208
|
+
actionName,
|
|
1209
|
+
resolvedAction,
|
|
1210
|
+
{
|
|
1211
|
+
params: finalInputs,
|
|
1212
|
+
envOverrides: envVars,
|
|
1213
|
+
},
|
|
1214
|
+
execOptions
|
|
1215
|
+
);
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1073
1218
|
return provider.execute(
|
|
1074
1219
|
manifest,
|
|
1075
1220
|
{
|
|
@@ -1082,7 +1227,9 @@ async function runHandler(tool: string, options: RunOptions, ctx: CommandContext
|
|
|
1082
1227
|
|
|
1083
1228
|
// Build a descriptive message - container may need to be pulled
|
|
1084
1229
|
const containerImage = manifest.from ?? "node:18-alpine";
|
|
1085
|
-
const
|
|
1230
|
+
const toolDisplayName =
|
|
1231
|
+
resolvedAction && actionName ? `${manifest.name}:${actionName}` : manifest.name;
|
|
1232
|
+
const spinnerMessage = `Running ${toolDisplayName} (${containerImage})...`;
|
|
1086
1233
|
|
|
1087
1234
|
const result = !options.verbose
|
|
1088
1235
|
? await executeTask()
|
|
@@ -1153,8 +1300,11 @@ function parseTimeout(timeout: string): number {
|
|
|
1153
1300
|
export function configureRunCommand(program: Command): void {
|
|
1154
1301
|
program
|
|
1155
1302
|
.command("run")
|
|
1156
|
-
.description("Execute a tool with its manifest-defined command")
|
|
1157
|
-
.argument(
|
|
1303
|
+
.description("Execute a tool or action with its manifest-defined command")
|
|
1304
|
+
.argument(
|
|
1305
|
+
"<tool>",
|
|
1306
|
+
"Tool or action to run (name, owner/skill/action, path, or '.' for current directory)"
|
|
1307
|
+
)
|
|
1158
1308
|
.option("-a, --args <json>", "Input arguments as JSON string (recommended)")
|
|
1159
1309
|
.option("-f, --input-file <path>", "Load input arguments from JSON file")
|
|
1160
1310
|
.option(
|
|
@@ -1172,6 +1322,7 @@ export function configureRunCommand(program: Command): void {
|
|
|
1172
1322
|
.option("-r, --remote", "Skip local resolution and fetch from registry")
|
|
1173
1323
|
.option("--dry-run", "Show what would be executed without running")
|
|
1174
1324
|
.option("--debug", "Show detailed parameter and environment variable resolution")
|
|
1325
|
+
.option("--action <name>", "Script to execute (alternative to colon syntax: tool:script)")
|
|
1175
1326
|
.option("-v, --verbose", "Show progress spinners and detailed output")
|
|
1176
1327
|
.option("--json", "Output result as JSON")
|
|
1177
1328
|
.action(async (tool: string, options: RunOptions) => {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* enact serve command
|
|
3
|
+
*
|
|
4
|
+
* Start a self-hosted Enact registry server.
|
|
5
|
+
* Uses SQLite + local file storage — no external dependencies.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { resolve } from "node:path";
|
|
9
|
+
import type { Command } from "commander";
|
|
10
|
+
|
|
11
|
+
export function configureServeCommand(program: Command): void {
|
|
12
|
+
program
|
|
13
|
+
.command("serve")
|
|
14
|
+
.description("Start a self-hosted Enact registry server")
|
|
15
|
+
.option("-p, --port <port>", "Port to listen on", "3000")
|
|
16
|
+
.option("-d, --data <path>", "Data directory for storage", "./registry-data")
|
|
17
|
+
.option("--host <host>", "Host to bind to", "0.0.0.0")
|
|
18
|
+
.action(async (options: { port: string; data: string; host: string }) => {
|
|
19
|
+
const { startServer } = await import("@enactprotocol/registry");
|
|
20
|
+
await startServer({
|
|
21
|
+
port: Number.parseInt(options.port, 10),
|
|
22
|
+
dataDir: resolve(options.data),
|
|
23
|
+
host: options.host,
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -102,49 +102,14 @@ function validateManifest(manifest: ToolManifest, sourceDir: string): Validation
|
|
|
102
102
|
message: "No 'command' field - this is an LLM instruction tool",
|
|
103
103
|
});
|
|
104
104
|
} else {
|
|
105
|
-
// Command-based tool -
|
|
105
|
+
// Command-based tool - basic validation
|
|
106
106
|
const commandParams = extractCommandParams(manifest.command);
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
for (const param of commandParams) {
|
|
114
|
-
if (!schemaProperties.includes(param)) {
|
|
115
|
-
issues.push({
|
|
116
|
-
level: "error",
|
|
117
|
-
message: `Command uses \${${param}} but it's not defined in inputSchema.properties`,
|
|
118
|
-
suggestion: `Add '${param}' to inputSchema.properties`,
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Check for required params without command usage (potential issue)
|
|
124
|
-
for (const param of requiredParams) {
|
|
125
|
-
if (!commandParams.includes(param)) {
|
|
126
|
-
issues.push({
|
|
127
|
-
level: "info",
|
|
128
|
-
message: `Required parameter '${param}' is not used in command template`,
|
|
129
|
-
suggestion: "This is fine if you access it via environment or files",
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Check for optional params without defaults
|
|
135
|
-
for (const prop of schemaProperties) {
|
|
136
|
-
if (!requiredParams.includes(prop)) {
|
|
137
|
-
const propSchema = manifest.inputSchema?.properties?.[prop] as
|
|
138
|
-
| { default?: unknown }
|
|
139
|
-
| undefined;
|
|
140
|
-
if (propSchema?.default === undefined) {
|
|
141
|
-
issues.push({
|
|
142
|
-
level: "warning",
|
|
143
|
-
message: `Optional parameter '${prop}' has no default value`,
|
|
144
|
-
suggestion: "Add a default value or it will be empty string in commands",
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
}
|
|
107
|
+
if (commandParams.length > 0) {
|
|
108
|
+
issues.push({
|
|
109
|
+
level: "info",
|
|
110
|
+
message: `Command uses parameters: ${commandParams.join(", ")}`,
|
|
111
|
+
suggestion: "Consider using scripts with {{param}} templates for better parameter handling",
|
|
112
|
+
});
|
|
148
113
|
}
|
|
149
114
|
|
|
150
115
|
// Check for double-quoting in command
|
package/src/index.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
configureReportCommand,
|
|
27
27
|
configureRunCommand,
|
|
28
28
|
configureSearchCommand,
|
|
29
|
+
configureServeCommand,
|
|
29
30
|
configureSetupCommand,
|
|
30
31
|
configureSignCommand,
|
|
31
32
|
configureTrustCommand,
|
|
@@ -36,7 +37,7 @@ import {
|
|
|
36
37
|
} from "./commands";
|
|
37
38
|
import { error, formatError } from "./utils";
|
|
38
39
|
|
|
39
|
-
export const version = "2.
|
|
40
|
+
export const version = "2.3.4";
|
|
40
41
|
|
|
41
42
|
// Export types for external use
|
|
42
43
|
export type { GlobalOptions, CommandContext } from "./types";
|
|
@@ -89,6 +90,9 @@ async function main() {
|
|
|
89
90
|
// Validation command
|
|
90
91
|
configureValidateCommand(program);
|
|
91
92
|
|
|
93
|
+
// Self-hosted registry
|
|
94
|
+
configureServeCommand(program);
|
|
95
|
+
|
|
92
96
|
// Global error handler - handle Commander's help/version exits gracefully
|
|
93
97
|
program.exitOverride((err) => {
|
|
94
98
|
// Commander throws errors for help, version, and other "exit" scenarios
|
package/src/utils/errors.ts
CHANGED
|
@@ -84,7 +84,7 @@ export class ManifestError extends CliError {
|
|
|
84
84
|
super(
|
|
85
85
|
fullMessage,
|
|
86
86
|
EXIT_MANIFEST_ERROR,
|
|
87
|
-
"Ensure the directory contains a valid
|
|
87
|
+
"Ensure the directory contains a valid skill.yaml or SKILL.md file."
|
|
88
88
|
);
|
|
89
89
|
this.name = "ManifestError";
|
|
90
90
|
}
|
|
@@ -377,7 +377,7 @@ export const ErrorMessages = {
|
|
|
377
377
|
message: `No manifest found in ${dir}`,
|
|
378
378
|
suggestions: [
|
|
379
379
|
`Create a manifest: ${colors.command("enact init")}`,
|
|
380
|
-
"Ensure the directory contains
|
|
380
|
+
"Ensure the directory contains skill.yaml or SKILL.md",
|
|
381
381
|
],
|
|
382
382
|
}),
|
|
383
383
|
|
|
@@ -271,14 +271,14 @@ describe("cache command", () => {
|
|
|
271
271
|
}
|
|
272
272
|
|
|
273
273
|
const info: CacheInfo = {
|
|
274
|
-
path: "/Users/test/.
|
|
274
|
+
path: "/Users/test/.agent/skills",
|
|
275
275
|
totalSize: 10_485_760, // 10 MB
|
|
276
276
|
toolCount: 25,
|
|
277
277
|
oldestEntry: "2024-01-01T00:00:00Z",
|
|
278
278
|
newestEntry: "2024-01-20T12:00:00Z",
|
|
279
279
|
};
|
|
280
280
|
|
|
281
|
-
expect(info.path).toContain(".
|
|
281
|
+
expect(info.path).toContain(".agent");
|
|
282
282
|
expect(info.totalSize).toBeGreaterThan(0);
|
|
283
283
|
expect(info.toolCount).toBeGreaterThanOrEqual(0);
|
|
284
284
|
});
|