@enactprotocol/cli 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 (68) 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 +64 -4
  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 +54 -0
  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.d.ts.map +1 -1
  20. package/dist/commands/list/index.js +7 -3
  21. package/dist/commands/list/index.js.map +1 -1
  22. package/dist/commands/run/index.d.ts.map +1 -1
  23. package/dist/commands/run/index.js +142 -41
  24. package/dist/commands/run/index.js.map +1 -1
  25. package/dist/commands/serve/index.d.ts +9 -0
  26. package/dist/commands/serve/index.d.ts.map +1 -0
  27. package/dist/commands/serve/index.js +24 -0
  28. package/dist/commands/serve/index.js.map +1 -0
  29. package/dist/commands/validate/index.d.ts.map +1 -1
  30. package/dist/commands/validate/index.js +7 -37
  31. package/dist/commands/validate/index.js.map +1 -1
  32. package/dist/index.d.ts +1 -1
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +4 -2
  35. package/dist/index.js.map +1 -1
  36. package/dist/utils/errors.js +2 -2
  37. package/package.json +7 -6
  38. package/src/commands/cache/index.ts +5 -5
  39. package/src/commands/env/README.md +1 -1
  40. package/src/commands/index.ts +3 -0
  41. package/src/commands/inspect/index.ts +3 -2
  42. package/src/commands/install/README.md +2 -2
  43. package/src/commands/install/index.ts +98 -4
  44. package/src/commands/learn/index.ts +67 -0
  45. package/src/commands/list/index.ts +12 -3
  46. package/src/commands/run/README.md +1 -1
  47. package/src/commands/run/index.ts +195 -48
  48. package/src/commands/serve/index.ts +26 -0
  49. package/src/commands/validate/index.ts +7 -42
  50. package/src/index.ts +5 -1
  51. package/src/utils/errors.ts +2 -2
  52. package/tests/commands/cache.test.ts +2 -2
  53. package/tests/commands/install-integration.test.ts +11 -12
  54. package/tests/commands/publish.test.ts +12 -2
  55. package/tests/commands/run.test.ts +3 -1
  56. package/tests/commands/serve.test.ts +82 -0
  57. package/tests/commands/sign.test.ts +1 -1
  58. package/tests/e2e.test.ts +56 -34
  59. package/tests/fixtures/calculator/skill.yaml +38 -0
  60. package/tests/fixtures/echo-tool/SKILL.md +3 -10
  61. package/tests/fixtures/env-tool/{enact.yaml → skill.yaml} +0 -6
  62. package/tests/fixtures/greeter/skill.yaml +22 -0
  63. package/tests/utils/ignore.test.ts +3 -1
  64. package/tsconfig.json +2 -1
  65. package/tsconfig.tsbuildinfo +1 -1
  66. package/tests/fixtures/calculator/enact.yaml +0 -34
  67. package/tests/fixtures/greeter/enact.yaml +0 -18
  68. /package/tests/fixtures/invalid-tool/{enact.yaml → skill.yaml} +0 -0
@@ -5,10 +5,14 @@
5
5
  * - Default: project tools (via .enact/tools.json)
6
6
  * - --global/-g: global tools (via ~/.enact/tools.json)
7
7
  *
8
- * All tools are stored in ~/.enact/cache/{tool}/{version}/
8
+ * All tools are stored in ~/.agent/skills/{tool}/
9
9
  */
10
10
 
11
- import { listInstalledTools, tryLoadManifestFromDir } from "@enactprotocol/shared";
11
+ import {
12
+ getAliasesForTool,
13
+ listInstalledTools,
14
+ tryLoadManifestFromDir,
15
+ } from "@enactprotocol/shared";
12
16
  import type { Command } from "commander";
13
17
  import type { CommandContext, GlobalOptions } from "../../types";
14
18
  import {
@@ -33,6 +37,7 @@ interface ToolInfo {
33
37
  version: string;
34
38
  location: string;
35
39
  scope: string;
40
+ alias: string;
36
41
  [key: string]: string; // Index signature for table compatibility
37
42
  }
38
43
 
@@ -46,12 +51,15 @@ function listToolsFromRegistry(scope: "global" | "project", cwd?: string): ToolI
46
51
  for (const tool of installedTools) {
47
52
  // Load manifest from cache to get description
48
53
  const loaded = tryLoadManifestFromDir(tool.cachePath);
54
+ // Get aliases for this tool
55
+ const aliases = getAliasesForTool(tool.name, scope, cwd);
49
56
  tools.push({
50
57
  name: tool.name,
51
58
  description: loaded?.manifest.description ?? "-",
52
59
  version: tool.version,
53
60
  location: tool.cachePath,
54
61
  scope,
62
+ alias: aliases.length > 0 ? aliases.join(", ") : "-",
55
63
  });
56
64
  }
57
65
 
@@ -96,7 +104,8 @@ async function listHandler(options: ListOptions, ctx: CommandContext): Promise<v
96
104
 
97
105
  const columns: TableColumn[] = [
98
106
  { key: "name", header: "Name", width: 28 },
99
- { key: "description", header: "Description", width: 50 },
107
+ { key: "alias", header: "Alias", width: 12 },
108
+ { key: "description", header: "Description", width: 40 },
100
109
  ];
101
110
 
102
111
  if (options.verbose) {
@@ -10,7 +10,7 @@ enact run <tool> [options]
10
10
 
11
11
  ## Description
12
12
 
13
- The `run` command executes a tool using the command defined in its manifest (`enact.yaml` or `enact.md`). The tool runs in an isolated container environment with:
13
+ The `run` command executes a tool using the command defined in its manifest (`skill.yaml` or `SKILL.md`). The tool runs in an isolated container environment with:
14
14
 
15
15
  - Input validation against the tool's JSON Schema
16
16
  - Automatic secret resolution from the OS keyring
@@ -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
  /**
@@ -635,7 +647,6 @@ function displayDryRun(
635
647
  * Display debug information about parameter resolution
636
648
  */
637
649
  function displayDebugInfo(
638
- manifest: ToolManifest,
639
650
  rawInputs: Record<string, unknown>,
640
651
  inputsWithDefaults: Record<string, unknown>,
641
652
  finalInputs: Record<string, unknown>,
@@ -646,21 +657,7 @@ function displayDebugInfo(
646
657
  info(colors.bold("Debug: Parameter Resolution"));
647
658
  newline();
648
659
 
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
- }
660
+ // Schema information is available per-script via action.inputSchema
664
661
 
665
662
  // Show raw inputs (what was provided)
666
663
  info("Raw Inputs (provided by user):");
@@ -768,6 +765,48 @@ function displayResult(result: ExecutionResult, options: RunOptions): void {
768
765
  }
769
766
  }
770
767
 
768
+ /**
769
+ * Check if a specifier looks like a file path
770
+ */
771
+ function isFilePathSpecifier(specifier: string): boolean {
772
+ return (
773
+ specifier.startsWith("/") ||
774
+ specifier.startsWith("./") ||
775
+ specifier.startsWith("../") ||
776
+ specifier === "."
777
+ );
778
+ }
779
+
780
+ /**
781
+ * For file paths like /tmp/skill/action, try to parse the last segment as an action
782
+ * Returns { parentPath, actionName } or null if not applicable
783
+ */
784
+ function tryParseFilePathAction(
785
+ specifier: string
786
+ ): { parentPath: string; actionName: string } | null {
787
+ if (!isFilePathSpecifier(specifier)) {
788
+ return null;
789
+ }
790
+
791
+ const normalized = specifier.replace(/\\/g, "/");
792
+ const lastSlash = normalized.lastIndexOf("/");
793
+
794
+ // Need at least one slash and something after it
795
+ if (lastSlash <= 0 || lastSlash === normalized.length - 1) {
796
+ return null;
797
+ }
798
+
799
+ const parentPath = normalized.slice(0, lastSlash);
800
+ const actionName = normalized.slice(lastSlash + 1);
801
+
802
+ // Action name shouldn't look like a file extension
803
+ if (actionName.includes(".")) {
804
+ return null;
805
+ }
806
+
807
+ return { parentPath, actionName };
808
+ }
809
+
771
810
  /**
772
811
  * Run command handler
773
812
  */
@@ -775,8 +814,16 @@ async function runHandler(tool: string, options: RunOptions, ctx: CommandContext
775
814
  let resolution: ToolResolution | null = null;
776
815
  let resolveResult: ReturnType<typeof tryResolveToolDetailed> | null = null;
777
816
 
817
+ // Parse the tool specifier to check for action (owner/skill/action format)
818
+ const { skillName, actionName: parsedActionName } = parseActionSpecifier(tool);
819
+
820
+ // Use --action flag if provided, otherwise use parsed action name
821
+ let actionName = options.action ?? parsedActionName;
822
+ let hasActionSpecifier = actionName !== undefined;
823
+
778
824
  // Check if --remote flag is valid (requires namespace/name format)
779
- const isRegistryFormat = tool.includes("/") && !tool.startsWith("/") && !tool.startsWith(".");
825
+ const isRegistryFormat =
826
+ skillName.includes("/") && !skillName.startsWith("/") && !skillName.startsWith(".");
780
827
  if (options.remote && !isRegistryFormat) {
781
828
  throw new ValidationError(
782
829
  `--remote requires a registry tool name (e.g., user/tool), got: ${tool}`
@@ -786,21 +833,44 @@ async function runHandler(tool: string, options: RunOptions, ctx: CommandContext
786
833
  // Skip local resolution if --remote is set
787
834
  if (!options.remote) {
788
835
  // First, try to resolve locally (project → user → cache)
836
+ // Use the skill name (without action) for initial resolution
789
837
  if (!options.verbose) {
790
- resolveResult = tryResolveToolDetailed(tool, { startDir: ctx.cwd });
838
+ resolveResult = tryResolveToolDetailed(skillName, { startDir: ctx.cwd });
791
839
  resolution = resolveResult.resolution;
792
840
  } else {
793
841
  const spinner = clack.spinner();
794
- spinner.start(`Resolving tool: ${tool}`);
795
- resolveResult = tryResolveToolDetailed(tool, { startDir: ctx.cwd });
842
+ spinner.start(
843
+ `Resolving tool: ${skillName}${hasActionSpecifier ? ` (action: ${actionName})` : ""}`
844
+ );
845
+ resolveResult = tryResolveToolDetailed(skillName, { startDir: ctx.cwd });
796
846
  resolution = resolveResult.resolution;
797
847
  if (resolution) {
798
- spinner.stop(`${symbols.success} Resolved: ${tool}`);
848
+ spinner.stop(`${symbols.success} Resolved: ${skillName}`);
799
849
  } else {
800
850
  spinner.stop(`${symbols.info} Checking registry...`);
801
851
  }
802
852
  }
803
853
 
854
+ // If resolution failed and this is a file path, try treating the last segment as an action
855
+ // e.g., /tmp/skill/hello -> try /tmp/skill with action "hello"
856
+ if (!resolution && isFilePathSpecifier(tool) && !options.action) {
857
+ const parsed = tryParseFilePathAction(tool);
858
+ if (parsed) {
859
+ const parentResult = tryResolveToolDetailed(parsed.parentPath, { startDir: ctx.cwd });
860
+ if (parentResult.resolution?.actionsManifest) {
861
+ // Found a skill with actions at the parent path
862
+ resolution = parentResult.resolution;
863
+ resolveResult = parentResult;
864
+ actionName = parsed.actionName;
865
+ hasActionSpecifier = true;
866
+
867
+ if (options.verbose) {
868
+ info(`Detected action from path: ${parsed.actionName}`);
869
+ }
870
+ }
871
+ }
872
+ }
873
+
804
874
  // If manifest was found but had errors, throw a descriptive error immediately
805
875
  if (!resolution && resolveResult?.manifestFound && resolveResult?.error) {
806
876
  const errorMessage = resolveResult.error.message;
@@ -816,31 +886,61 @@ async function runHandler(tool: string, options: RunOptions, ctx: CommandContext
816
886
  // Check if this looks like a tool name (namespace/name format)
817
887
  if (isRegistryFormat) {
818
888
  resolution = !options.verbose
819
- ? await fetchAndCacheTool(tool, options, ctx)
889
+ ? await fetchAndCacheTool(skillName, options, ctx)
820
890
  : await withSpinner(
821
- `Fetching ${tool} from registry...`,
822
- async () => fetchAndCacheTool(tool, options, ctx),
823
- `${symbols.success} Cached: ${tool}`
891
+ `Fetching ${skillName} from registry...`,
892
+ async () => fetchAndCacheTool(skillName, options, ctx),
893
+ `${symbols.success} Cached: ${skillName}`
824
894
  );
825
895
  }
826
896
  }
827
897
 
828
898
  if (!resolution) {
829
899
  if (options.local) {
830
- throw new ToolNotFoundError(tool, {
900
+ throw new ToolNotFoundError(skillName, {
831
901
  localOnly: true,
832
902
  ...(resolveResult?.searchedLocations && {
833
903
  searchedLocations: resolveResult.searchedLocations,
834
904
  }),
835
905
  });
836
906
  }
837
- throw new ToolNotFoundError(tool, {
907
+ throw new ToolNotFoundError(skillName, {
838
908
  ...(resolveResult?.searchedLocations && {
839
909
  searchedLocations: resolveResult.searchedLocations,
840
910
  }),
841
911
  });
842
912
  }
843
913
 
914
+ // If a script was specified via colon syntax, resolve it from the manifest's scripts
915
+ let resolvedAction: Action | undefined;
916
+ let actionsManifest: ActionsManifest | undefined;
917
+
918
+ // Use resolved skill identifier for error messages (manifest name or source directory)
919
+ const resolvedSkillId = resolution.manifest.name ?? resolution.sourceDir;
920
+
921
+ if (hasActionSpecifier) {
922
+ if (!resolution.actionsManifest) {
923
+ throw new ValidationError(
924
+ `Skill "${resolvedSkillId}" does not define any scripts. Cannot execute script "${actionName}".`
925
+ );
926
+ }
927
+
928
+ actionsManifest = resolution.actionsManifest;
929
+ // Map lookup - action name is the key
930
+ resolvedAction = actionsManifest.actions[actionName!];
931
+
932
+ if (!resolvedAction) {
933
+ const availableActions = Object.keys(actionsManifest.actions).join(", ");
934
+ throw new ValidationError(
935
+ `Action "${actionName}" not found in skill "${resolvedSkillId}". Available actions: ${availableActions}`
936
+ );
937
+ }
938
+
939
+ if (options.verbose) {
940
+ info(`Using action: ${actionName}`);
941
+ }
942
+ }
943
+
844
944
  const manifest = resolution.manifest;
845
945
 
846
946
  // Parse --input flags to separate key=value params from path inputs
@@ -852,13 +952,16 @@ async function runHandler(tool: string, options: RunOptions, ctx: CommandContext
852
952
  // Merge inputs: path params override other inputs
853
953
  const inputs = { ...otherInputs, ...pathParams };
854
954
 
955
+ // Use action's inputSchema if executing an action, otherwise no schema
956
+ const effectiveInputSchema = resolvedAction ? getEffectiveInputSchema(resolvedAction) : undefined;
957
+
855
958
  // Apply defaults from schema
856
- const inputsWithDefaults = manifest.inputSchema
857
- ? applyDefaults(inputs, manifest.inputSchema)
959
+ const inputsWithDefaults = effectiveInputSchema
960
+ ? applyDefaults(inputs, effectiveInputSchema)
858
961
  : inputs;
859
962
 
860
963
  // Validate inputs against schema
861
- const validation = validateInputs(inputsWithDefaults, manifest.inputSchema);
964
+ const validation = validateInputs(inputsWithDefaults, effectiveInputSchema);
862
965
  if (!validation.valid) {
863
966
  const errors = validation.errors.map((err) => `${err.path}: ${err.message}`).join(", ");
864
967
  throw new ValidationError(`Input validation failed: ${errors}`);
@@ -905,8 +1008,8 @@ async function runHandler(tool: string, options: RunOptions, ctx: CommandContext
905
1008
  }
906
1009
  }
907
1010
 
908
- // Check if this is an instruction-based tool (no command)
909
- if (!manifest.command) {
1011
+ // Check if this is an instruction-based tool (no command) - but actions always have commands
1012
+ if (!manifest.command && !resolvedAction) {
910
1013
  // For instruction tools, just display the markdown body
911
1014
  let instructions: string | undefined;
912
1015
 
@@ -947,15 +1050,25 @@ async function runHandler(tool: string, options: RunOptions, ctx: CommandContext
947
1050
  return;
948
1051
  }
949
1052
 
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
- );
1053
+ // Prepare command
1054
+ // For actions: use {{param}} template system (array form, no shell)
1055
+ // For regular tools: use ${param} template system (shell interpolation)
1056
+ let command: string[];
1057
+
1058
+ if (resolvedAction) {
1059
+ // Action execution: use prepareActionCommand for {{param}} templates
1060
+ const actionCommand = resolvedAction.command;
1061
+ if (typeof actionCommand === "string") {
1062
+ // String form (no templates allowed by validation)
1063
+ command = actionCommand.split(/\s+/).filter((s) => s.length > 0);
1064
+ } else {
1065
+ // Array form with {{param}} templates
1066
+ command = prepareActionCommand(actionCommand, finalInputs, resolvedAction.inputSchema);
1067
+ }
1068
+ } else {
1069
+ // Regular tool execution: use prepareCommand for ${param} templates
1070
+ command = prepareCommand(manifest.command!, finalInputs);
1071
+ }
959
1072
 
960
1073
  // Resolve environment variables (non-secrets)
961
1074
  const { resolved: envResolved } = resolveToolEnv(manifest.env ?? {}, ctx.cwd);
@@ -1016,7 +1129,7 @@ async function runHandler(tool: string, options: RunOptions, ctx: CommandContext
1016
1129
 
1017
1130
  // Debug mode - show detailed parameter resolution info
1018
1131
  if (options.debug) {
1019
- displayDebugInfo(manifest, inputs, inputsWithDefaults, finalInputs, envVars, command);
1132
+ displayDebugInfo(inputs, inputsWithDefaults, finalInputs, envVars, command);
1020
1133
  }
1021
1134
 
1022
1135
  // Dry run mode
@@ -1033,7 +1146,7 @@ async function runHandler(tool: string, options: RunOptions, ctx: CommandContext
1033
1146
  return;
1034
1147
  }
1035
1148
 
1036
- // Execute the tool
1149
+ // Execute the tool — select backend via execution router
1037
1150
  const providerConfig: { defaultTimeout?: number; verbose?: boolean } = {};
1038
1151
  if (options.timeout) {
1039
1152
  providerConfig.defaultTimeout = parseTimeout(options.timeout);
@@ -1042,7 +1155,20 @@ async function runHandler(tool: string, options: RunOptions, ctx: CommandContext
1042
1155
  providerConfig.verbose = true;
1043
1156
  }
1044
1157
 
1045
- const provider = new DaggerExecutionProvider(providerConfig);
1158
+ const config = loadConfig();
1159
+ const router = new ExecutionRouter({
1160
+ default: config.execution?.default,
1161
+ fallback: config.execution?.fallback,
1162
+ trusted_scopes: config.execution?.trusted_scopes,
1163
+ });
1164
+ router.registerProvider("local", new LocalExecutionProvider(providerConfig));
1165
+ router.registerProvider("docker", new DockerExecutionProvider(providerConfig));
1166
+ router.registerProvider("dagger", new DaggerExecutionProvider(providerConfig));
1167
+
1168
+ const provider = await router.selectProvider(manifest.name, {
1169
+ forceLocal: options.local,
1170
+ forceRemote: options.remote,
1171
+ });
1046
1172
 
1047
1173
  // For --apply, we export to a temp directory first, then atomically replace
1048
1174
  let tempOutputDir: string | undefined;
@@ -1070,6 +1196,21 @@ async function runHandler(tool: string, options: RunOptions, ctx: CommandContext
1070
1196
  execOptions.outputPath = resolve(options.output);
1071
1197
  }
1072
1198
 
1199
+ // Use executeAction for actions, execute for regular tools
1200
+ if (resolvedAction && actionsManifest && actionName) {
1201
+ return provider.executeAction(
1202
+ manifest,
1203
+ actionsManifest,
1204
+ actionName,
1205
+ resolvedAction,
1206
+ {
1207
+ params: finalInputs,
1208
+ envOverrides: envVars,
1209
+ },
1210
+ execOptions
1211
+ );
1212
+ }
1213
+
1073
1214
  return provider.execute(
1074
1215
  manifest,
1075
1216
  {
@@ -1082,7 +1223,9 @@ async function runHandler(tool: string, options: RunOptions, ctx: CommandContext
1082
1223
 
1083
1224
  // Build a descriptive message - container may need to be pulled
1084
1225
  const containerImage = manifest.from ?? "node:18-alpine";
1085
- const spinnerMessage = `Running ${manifest.name} (${containerImage})...`;
1226
+ const toolDisplayName =
1227
+ resolvedAction && actionName ? `${manifest.name}:${actionName}` : manifest.name;
1228
+ const spinnerMessage = `Running ${toolDisplayName} (${containerImage})...`;
1086
1229
 
1087
1230
  const result = !options.verbose
1088
1231
  ? await executeTask()
@@ -1153,8 +1296,11 @@ function parseTimeout(timeout: string): number {
1153
1296
  export function configureRunCommand(program: Command): void {
1154
1297
  program
1155
1298
  .command("run")
1156
- .description("Execute a tool with its manifest-defined command")
1157
- .argument("<tool>", "Tool to run (name, path, or '.' for current directory)")
1299
+ .description("Execute a tool or action with its manifest-defined command")
1300
+ .argument(
1301
+ "<tool>",
1302
+ "Tool or action to run (name, owner/skill/action, path, or '.' for current directory)"
1303
+ )
1158
1304
  .option("-a, --args <json>", "Input arguments as JSON string (recommended)")
1159
1305
  .option("-f, --input-file <path>", "Load input arguments from JSON file")
1160
1306
  .option(
@@ -1172,6 +1318,7 @@ export function configureRunCommand(program: Command): void {
1172
1318
  .option("-r, --remote", "Skip local resolution and fetch from registry")
1173
1319
  .option("--dry-run", "Show what would be executed without running")
1174
1320
  .option("--debug", "Show detailed parameter and environment variable resolution")
1321
+ .option("--action <name>", "Script to execute (alternative to colon syntax: tool:script)")
1175
1322
  .option("-v, --verbose", "Show progress spinners and detailed output")
1176
1323
  .option("--json", "Output result as JSON")
1177
1324
  .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.2";
40
+ export const version = "2.3.1";
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
  });