@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.
Files changed (66) hide show
  1. package/README.md +4 -37
  2. package/dist/commands/cache/index.js +5 -5
  3. package/dist/commands/cache/index.js.map +1 -1
  4. package/dist/commands/index.d.ts +1 -0
  5. package/dist/commands/index.d.ts.map +1 -1
  6. package/dist/commands/index.js +2 -0
  7. package/dist/commands/index.js.map +1 -1
  8. package/dist/commands/inspect/index.d.ts.map +1 -1
  9. package/dist/commands/inspect/index.js +3 -2
  10. package/dist/commands/inspect/index.js.map +1 -1
  11. package/dist/commands/install/index.d.ts +1 -1
  12. package/dist/commands/install/index.d.ts.map +1 -1
  13. package/dist/commands/install/index.js +69 -21
  14. package/dist/commands/install/index.js.map +1 -1
  15. package/dist/commands/learn/index.d.ts.map +1 -1
  16. package/dist/commands/learn/index.js +73 -14
  17. package/dist/commands/learn/index.js.map +1 -1
  18. package/dist/commands/list/index.d.ts +1 -1
  19. package/dist/commands/list/index.js +1 -1
  20. package/dist/commands/run/index.d.ts.map +1 -1
  21. package/dist/commands/run/index.js +166 -60
  22. package/dist/commands/run/index.js.map +1 -1
  23. package/dist/commands/serve/index.d.ts +9 -0
  24. package/dist/commands/serve/index.d.ts.map +1 -0
  25. package/dist/commands/serve/index.js +24 -0
  26. package/dist/commands/serve/index.js.map +1 -0
  27. package/dist/commands/validate/index.d.ts.map +1 -1
  28. package/dist/commands/validate/index.js +7 -37
  29. package/dist/commands/validate/index.js.map +1 -1
  30. package/dist/index.d.ts +1 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +4 -2
  33. package/dist/index.js.map +1 -1
  34. package/dist/utils/errors.js +2 -2
  35. package/package.json +6 -5
  36. package/src/commands/cache/index.ts +5 -5
  37. package/src/commands/env/README.md +1 -1
  38. package/src/commands/index.ts +3 -0
  39. package/src/commands/inspect/index.ts +3 -2
  40. package/src/commands/install/README.md +2 -2
  41. package/src/commands/install/index.ts +98 -21
  42. package/src/commands/learn/index.ts +89 -18
  43. package/src/commands/list/index.ts +1 -1
  44. package/src/commands/run/README.md +1 -1
  45. package/src/commands/run/index.ts +218 -67
  46. package/src/commands/serve/index.ts +26 -0
  47. package/src/commands/validate/index.ts +7 -42
  48. package/src/index.ts +5 -1
  49. package/src/utils/errors.ts +2 -2
  50. package/tests/commands/cache.test.ts +2 -2
  51. package/tests/commands/install-integration.test.ts +11 -12
  52. package/tests/commands/publish.test.ts +12 -2
  53. package/tests/commands/run.test.ts +3 -1
  54. package/tests/commands/serve.test.ts +82 -0
  55. package/tests/commands/sign.test.ts +1 -1
  56. package/tests/e2e.test.ts +56 -34
  57. package/tests/fixtures/calculator/skill.yaml +38 -0
  58. package/tests/fixtures/echo-tool/SKILL.md +3 -10
  59. package/tests/fixtures/env-tool/{enact.yaml → skill.yaml} +0 -6
  60. package/tests/fixtures/greeter/skill.yaml +22 -0
  61. package/tests/utils/ignore.test.ts +3 -1
  62. package/tsconfig.json +2 -1
  63. package/tsconfig.tsbuildinfo +1 -1
  64. package/tests/fixtures/calculator/enact.yaml +0 -34
  65. package/tests/fixtures/greeter/enact.yaml +0 -18
  66. /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 { DaggerExecutionProvider, type ExecutionResult } from "@enactprotocol/execution";
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
- version: string
321
+ _version: string
310
322
  ): Promise<string> {
311
323
  const cacheDir = getCacheDir();
312
324
  const toolPath = toolNameToPath(toolName);
313
- const versionDir = join(cacheDir, toolPath, `v${version.replace(/^v/, "")}`);
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(versionDir, { recursive: true });
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", versionDir], {
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 versionDir;
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
- info(`${symbols.warning} Tool ${toolName}@${targetVersion} has no attestations.`);
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
- if (trustPolicy === "require_attestation") {
445
- throw new TrustError("Trust policy requires attestations. Execution blocked.");
446
- }
447
- if (ctx.isInteractive && trustPolicy === "prompt") {
448
- const proceed = await confirm("Run unverified tool?");
449
- if (!proceed) {
450
- info("Execution cancelled.");
451
- process.exit(0);
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
- // Show schema information
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 = tool.includes("/") && !tool.startsWith("/") && !tool.startsWith(".");
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(tool, { startDir: ctx.cwd });
842
+ resolveResult = tryResolveToolDetailed(skillName, { startDir: ctx.cwd });
791
843
  resolution = resolveResult.resolution;
792
844
  } else {
793
845
  const spinner = clack.spinner();
794
- spinner.start(`Resolving tool: ${tool}`);
795
- resolveResult = tryResolveToolDetailed(tool, { startDir: ctx.cwd });
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: ${tool}`);
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(tool, options, ctx)
893
+ ? await fetchAndCacheTool(skillName, options, ctx)
820
894
  : await withSpinner(
821
- `Fetching ${tool} from registry...`,
822
- async () => fetchAndCacheTool(tool, options, ctx),
823
- `${symbols.success} Cached: ${tool}`
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(tool, {
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(tool, {
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 = manifest.inputSchema
857
- ? applyDefaults(inputs, manifest.inputSchema)
963
+ const inputsWithDefaults = effectiveInputSchema
964
+ ? applyDefaults(inputs, effectiveInputSchema)
858
965
  : inputs;
859
966
 
860
967
  // Validate inputs against schema
861
- const validation = validateInputs(inputsWithDefaults, manifest.inputSchema);
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 - only substitute ${...} patterns that match inputSchema properties
951
- const knownParameters = manifest.inputSchema?.properties
952
- ? new Set(Object.keys(manifest.inputSchema.properties))
953
- : undefined;
954
- const command = prepareCommand(
955
- manifest.command,
956
- finalInputs,
957
- knownParameters ? { knownParameters } : {}
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(manifest, inputs, inputsWithDefaults, finalInputs, envVars, command);
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 provider = new DaggerExecutionProvider(providerConfig);
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 spinnerMessage = `Running ${manifest.name} (${containerImage})...`;
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("<tool>", "Tool to run (name, path, or '.' for current directory)")
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 - validate parameters
105
+ // Command-based tool - basic validation
106
106
  const commandParams = extractCommandParams(manifest.command);
107
- const schemaProperties = manifest.inputSchema?.properties
108
- ? Object.keys(manifest.inputSchema.properties)
109
- : [];
110
- const requiredParams = manifest.inputSchema?.required || [];
111
-
112
- // Check for command params not in schema
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.2.4";
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
@@ -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 enact.yaml or enact.md file."
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 enact.yaml or enact.md",
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/.enact/cache",
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(".enact");
281
+ expect(info.path).toContain(".agent");
282
282
  expect(info.totalSize).toBeGreaterThan(0);
283
283
  expect(info.toolCount).toBeGreaterThanOrEqual(0);
284
284
  });