@enactprotocol/shared 2.2.2 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +1 -18
  2. package/dist/config.d.ts +12 -0
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +32 -6
  5. package/dist/config.js.map +1 -1
  6. package/dist/execution/action-command.d.ts +131 -0
  7. package/dist/execution/action-command.d.ts.map +1 -0
  8. package/dist/execution/action-command.js +300 -0
  9. package/dist/execution/action-command.js.map +1 -0
  10. package/dist/execution/command.d.ts +8 -8
  11. package/dist/execution/command.js +6 -6
  12. package/dist/execution/index.d.ts +1 -0
  13. package/dist/execution/index.d.ts.map +1 -1
  14. package/dist/execution/index.js +2 -0
  15. package/dist/execution/index.js.map +1 -1
  16. package/dist/execution/types.d.ts +5 -2
  17. package/dist/execution/types.d.ts.map +1 -1
  18. package/dist/execution/types.js.map +1 -1
  19. package/dist/index.d.ts +9 -7
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +14 -5
  22. package/dist/index.js.map +1 -1
  23. package/dist/manifest/actions-loader.d.ts +29 -0
  24. package/dist/manifest/actions-loader.d.ts.map +1 -0
  25. package/dist/manifest/actions-loader.js +34 -0
  26. package/dist/manifest/actions-loader.js.map +1 -0
  27. package/dist/manifest/actions-parser.d.ts +69 -0
  28. package/dist/manifest/actions-parser.d.ts.map +1 -0
  29. package/dist/manifest/actions-parser.js +265 -0
  30. package/dist/manifest/actions-parser.js.map +1 -0
  31. package/dist/manifest/index.d.ts +2 -0
  32. package/dist/manifest/index.d.ts.map +1 -1
  33. package/dist/manifest/index.js +4 -0
  34. package/dist/manifest/index.js.map +1 -1
  35. package/dist/manifest/loader.d.ts +7 -2
  36. package/dist/manifest/loader.d.ts.map +1 -1
  37. package/dist/manifest/loader.js +71 -4
  38. package/dist/manifest/loader.js.map +1 -1
  39. package/dist/manifest/parser.d.ts +1 -0
  40. package/dist/manifest/parser.d.ts.map +1 -1
  41. package/dist/manifest/parser.js +1 -0
  42. package/dist/manifest/parser.js.map +1 -1
  43. package/dist/manifest/scripts.d.ts +19 -0
  44. package/dist/manifest/scripts.d.ts.map +1 -0
  45. package/dist/manifest/scripts.js +102 -0
  46. package/dist/manifest/scripts.js.map +1 -0
  47. package/dist/manifest/validator.d.ts +1 -8
  48. package/dist/manifest/validator.d.ts.map +1 -1
  49. package/dist/manifest/validator.js +14 -13
  50. package/dist/manifest/validator.js.map +1 -1
  51. package/dist/mcp-registry.js +5 -5
  52. package/dist/mcp-registry.js.map +1 -1
  53. package/dist/paths.d.ts +9 -2
  54. package/dist/paths.d.ts.map +1 -1
  55. package/dist/paths.js +12 -3
  56. package/dist/paths.js.map +1 -1
  57. package/dist/registry.d.ts +47 -2
  58. package/dist/registry.d.ts.map +1 -1
  59. package/dist/registry.js +100 -7
  60. package/dist/registry.js.map +1 -1
  61. package/dist/resolver.d.ts +55 -4
  62. package/dist/resolver.d.ts.map +1 -1
  63. package/dist/resolver.js +144 -77
  64. package/dist/resolver.js.map +1 -1
  65. package/dist/types/actions.d.ts +194 -0
  66. package/dist/types/actions.d.ts.map +1 -0
  67. package/dist/types/actions.js +32 -0
  68. package/dist/types/actions.js.map +1 -0
  69. package/dist/types/index.d.ts +3 -1
  70. package/dist/types/index.d.ts.map +1 -1
  71. package/dist/types/index.js +1 -0
  72. package/dist/types/index.js.map +1 -1
  73. package/dist/types/manifest.d.ts +50 -5
  74. package/dist/types/manifest.d.ts.map +1 -1
  75. package/dist/types/manifest.js +10 -2
  76. package/dist/types/manifest.js.map +1 -1
  77. package/package.json +2 -2
  78. package/src/config.ts +48 -6
  79. package/src/execution/action-command.ts +417 -0
  80. package/src/execution/command.ts +8 -8
  81. package/src/execution/index.ts +17 -0
  82. package/src/execution/types.ts +13 -2
  83. package/src/index.ts +43 -0
  84. package/src/manifest/actions-loader.ts +49 -0
  85. package/src/manifest/index.ts +12 -0
  86. package/src/manifest/loader.ts +77 -4
  87. package/src/manifest/parser.ts +1 -0
  88. package/src/manifest/scripts.ts +116 -0
  89. package/src/manifest/validator.ts +15 -14
  90. package/src/mcp-registry.ts +5 -5
  91. package/src/paths.ts +13 -3
  92. package/src/registry.ts +136 -7
  93. package/src/resolver.ts +185 -79
  94. package/src/types/actions.ts +223 -0
  95. package/src/types/index.ts +11 -0
  96. package/src/types/manifest.ts +67 -6
  97. package/tests/action-command.test.ts +249 -0
  98. package/tests/config-normalization.test.ts +279 -0
  99. package/tests/config.test.ts +4 -1
  100. package/tests/effective-input-schema.test.ts +86 -0
  101. package/tests/fixtures/valid-tool.md +5 -12
  102. package/tests/fixtures/valid-tool.yaml +3 -10
  103. package/tests/hooks.test.ts +177 -0
  104. package/tests/manifest/loader.test.ts +34 -20
  105. package/tests/manifest/parser.test.ts +11 -15
  106. package/tests/manifest/validator.test.ts +7 -17
  107. package/tests/manifest-types.test.ts +9 -11
  108. package/tests/paths.test.ts +11 -4
  109. package/tests/registry.test.ts +204 -8
  110. package/tests/resolver.test.ts +90 -6
  111. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,223 @@
1
+ /**
2
+ * TypeScript types for Agent Actions (ACTIONS.yaml)
3
+ *
4
+ * Agent Actions extends the Agent Skills specification with structured execution semantics,
5
+ * enabling skills to define executable actions with typed inputs, validated outputs,
6
+ * and secure credential handling.
7
+ *
8
+ * @see RFC-001-AGENT-ACTIONS.md
9
+ */
10
+
11
+ import type { JSONSchema7 } from "json-schema";
12
+ import type { ToolAnnotations } from "./manifest";
13
+
14
+ /**
15
+ * Environment variable declaration in ACTIONS.yaml
16
+ */
17
+ export interface ActionEnvVar {
18
+ /** Human-readable description of what this variable is for */
19
+ description?: string;
20
+ /** If true, value should be stored securely and masked in logs */
21
+ secret?: boolean;
22
+ /** If true, execution fails if not set */
23
+ required?: boolean;
24
+ /** Default value if not provided */
25
+ default?: string;
26
+ }
27
+
28
+ /**
29
+ * Environment variables map for actions
30
+ */
31
+ export type ActionEnvVars = Record<string, ActionEnvVar>;
32
+
33
+ /**
34
+ * A single executable action within a skill
35
+ *
36
+ * Each action maps directly to an MCP tool with:
37
+ * - (key) → tool name (action name is the map key, not a field)
38
+ * - description → tool description
39
+ * - inputSchema → tool parameters
40
+ * - outputSchema → expected response shape
41
+ * - annotations → behavioral hints
42
+ */
43
+ export interface Action {
44
+ /** Human-readable description of what this action does */
45
+ description: string;
46
+
47
+ /**
48
+ * Execution command
49
+ *
50
+ * Can be string form (simple commands without templates) or array form
51
+ * (required when using {{}} templates).
52
+ *
53
+ * Template syntax: {{param}} - each template is replaced with the literal
54
+ * value as a single argument, regardless of content.
55
+ *
56
+ * @example
57
+ * // String form (no templates)
58
+ * command: "python main.py --version"
59
+ *
60
+ * // Array form (with templates)
61
+ * command: ["python", "main.py", "scrape", "{{url}}"]
62
+ */
63
+ command: string | string[];
64
+
65
+ /**
66
+ * JSON Schema defining expected input parameters
67
+ *
68
+ * Uses standard JSON Schema conventions:
69
+ * - Required fields listed in 'required' array must be provided
70
+ * - Optional fields with 'default' use the default value if not provided
71
+ * - Optional fields without 'default' cause the argument to be omitted entirely
72
+ *
73
+ * If omitted, defaults to { type: 'object', properties: {} } (no parameters)
74
+ */
75
+ inputSchema?: JSONSchema7;
76
+
77
+ /**
78
+ * JSON Schema defining expected output structure
79
+ *
80
+ * If provided, clients must validate results against this schema.
81
+ * Results that don't conform are treated as errors.
82
+ */
83
+ outputSchema?: JSONSchema7;
84
+
85
+ /**
86
+ * Behavioral hints for AI models and clients
87
+ *
88
+ * Open-ended object for attaching metadata to actions.
89
+ * Clients may use these for UI presentation, filtering, or custom behavior.
90
+ */
91
+ annotations?: ToolAnnotations;
92
+ }
93
+
94
+ /**
95
+ * Complete ACTIONS.yaml manifest structure
96
+ *
97
+ * Defines how to execute actions for a skill, including environment
98
+ * variables, build steps, and the map of executable actions.
99
+ *
100
+ * @example
101
+ * ```yaml
102
+ * actions:
103
+ * scrape:
104
+ * description: Scrape a URL
105
+ * command: ["python", "main.py", "{{url}}"]
106
+ * inputSchema:
107
+ * type: object
108
+ * required: [url]
109
+ * properties:
110
+ * url: { type: string }
111
+ * ```
112
+ */
113
+ export interface ActionsManifest {
114
+ /**
115
+ * Environment variables and secrets required by all actions
116
+ *
117
+ * Key benefit: Unlike traditional skills where you discover missing
118
+ * credentials at runtime, ACTIONS.yaml declares requirements upfront.
119
+ */
120
+ env?: ActionEnvVars;
121
+
122
+ /**
123
+ * Map of action names to action definitions
124
+ *
125
+ * Each action becomes an MCP tool that can be executed directly.
126
+ * The key is the action name (e.g., "scrape", "crawl").
127
+ *
128
+ * @example
129
+ * actions:
130
+ * scrape:
131
+ * description: Scrape a URL
132
+ * command: ["python", "main.py", "{{url}}"]
133
+ * list-formats:
134
+ * description: List supported formats
135
+ * command: ffmpeg -formats
136
+ */
137
+ actions: Record<string, Action>;
138
+
139
+ /**
140
+ * Build commands to run before execution
141
+ *
142
+ * For projects that require setup (e.g., pip install, npm install).
143
+ * Build runs once per environment setup, not per action invocation.
144
+ * Build failures prevent action execution.
145
+ *
146
+ * @example
147
+ * build:
148
+ * - pip install -r requirements.txt
149
+ * - npm install
150
+ * - npm run build
151
+ */
152
+ build?: string | string[];
153
+ }
154
+
155
+ /**
156
+ * Result of parsing an ACTIONS.yaml file
157
+ */
158
+ export interface ParsedActionsManifest {
159
+ /** The parsed actions manifest */
160
+ actions: ActionsManifest;
161
+ /** The file path the manifest was loaded from */
162
+ filePath: string;
163
+ }
164
+
165
+ /**
166
+ * Validation error specific to actions
167
+ */
168
+ export interface ActionValidationError {
169
+ /** Path to the field with the error (e.g., "actions[0].command") */
170
+ path: string;
171
+ /** Error message */
172
+ message: string;
173
+ /** Error code for programmatic handling */
174
+ code: ActionValidationErrorCode;
175
+ }
176
+
177
+ /**
178
+ * Error codes for action validation
179
+ */
180
+ export type ActionValidationErrorCode =
181
+ | "MISSING_REQUIRED_FIELD"
182
+ | "INVALID_COMMAND_FORMAT"
183
+ | "STRING_COMMAND_WITH_TEMPLATE"
184
+ | "DUPLICATE_ACTION_NAME"
185
+ | "INVALID_INPUT_SCHEMA"
186
+ | "INVALID_OUTPUT_SCHEMA"
187
+ | "EMPTY_ACTIONS_ARRAY";
188
+
189
+ /**
190
+ * Result of validating an actions manifest
191
+ */
192
+ export interface ActionValidationResult {
193
+ /** Whether the manifest is valid */
194
+ valid: boolean;
195
+ /** Validation errors (if any) */
196
+ errors?: ActionValidationError[];
197
+ }
198
+
199
+ /**
200
+ * Actions manifest file names (in order of preference)
201
+ */
202
+ export const ACTIONS_FILES = ["ACTIONS.yaml", "ACTIONS.yml"] as const;
203
+ export type ActionsFileName = (typeof ACTIONS_FILES)[number];
204
+
205
+ /**
206
+ * Default inputSchema when not provided
207
+ *
208
+ * Actions without inputSchema default to accepting no parameters.
209
+ */
210
+ export const DEFAULT_INPUT_SCHEMA: JSONSchema7 = {
211
+ type: "object",
212
+ properties: {},
213
+ } as const;
214
+
215
+ /**
216
+ * Get the effective inputSchema for an action
217
+ *
218
+ * Returns the action's inputSchema if provided, otherwise returns
219
+ * the default empty schema.
220
+ */
221
+ export function getEffectiveInputSchema(action: Action): JSONSchema7 {
222
+ return action.inputSchema ?? DEFAULT_INPUT_SCHEMA;
223
+ }
@@ -13,6 +13,7 @@ export type {
13
13
  Author,
14
14
  ToolAnnotations,
15
15
  ResourceRequirements,
16
+ ToolHooks,
16
17
  ToolExample,
17
18
  // Validation types
18
19
  ValidationResult,
@@ -28,3 +29,13 @@ export {
28
29
  MANIFEST_FILES,
29
30
  PACKAGE_MANIFEST_FILE,
30
31
  } from "./manifest";
32
+
33
+ // Actions types (internal — used by scripts bridge and execution pipeline)
34
+ export type {
35
+ ActionEnvVar,
36
+ ActionEnvVars,
37
+ Action,
38
+ ActionsManifest,
39
+ } from "./actions";
40
+
41
+ export { DEFAULT_INPUT_SCHEMA, getEffectiveInputSchema } from "./actions";
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * TypeScript types for Enact tool manifests
3
- * These types define the structure of SKILL.md (and legacy enact.yaml/enact.md) frontmatter
3
+ * These types define the structure of SKILL.md (and skill.yaml/legacy enact.yaml/enact.md) frontmatter
4
4
  */
5
5
 
6
6
  import type { JSONSchema7 } from "json-schema";
@@ -13,6 +13,8 @@ export interface EnvVariable {
13
13
  description: string;
14
14
  /** If true, stored in OS keyring; if false, stored in .env files */
15
15
  secret?: boolean;
16
+ /** If true, must be set before execution (no default fallback) */
17
+ required?: boolean;
16
18
  /** Default value if not set (only for non-secrets) */
17
19
  default?: string;
18
20
  }
@@ -50,6 +52,32 @@ export interface ToolAnnotations {
50
52
  openWorldHint?: boolean;
51
53
  }
52
54
 
55
+ /**
56
+ * Lifecycle hooks for tool installation and management
57
+ */
58
+ export interface ToolHooks {
59
+ /** Command(s) to run after the tool is installed/extracted (e.g., "npm install", "pip install -r requirements.txt") */
60
+ postinstall?: string | string[];
61
+ /** Build command(s) to run before execution (e.g., "pip install -r requirements.txt") */
62
+ build?: string | string[];
63
+ }
64
+
65
+ /**
66
+ * Script definition — either a simple command string or an expanded object
67
+ *
68
+ * Simple form: "python main.py {{url}}"
69
+ * Expanded form: { command: "python main.py {{url}}", description: "Scrape a URL" }
70
+ */
71
+ export type ScriptDefinition =
72
+ | string
73
+ | {
74
+ command: string;
75
+ description?: string;
76
+ inputSchema?: JSONSchema7;
77
+ outputSchema?: JSONSchema7;
78
+ annotations?: ToolAnnotations;
79
+ };
80
+
53
81
  /**
54
82
  * Resource requirements for tool execution
55
83
  */
@@ -76,7 +104,7 @@ export interface ToolExample {
76
104
 
77
105
  /**
78
106
  * Complete tool manifest structure
79
- * This represents the YAML frontmatter in SKILL.md (or legacy enact.md/enact.yaml)
107
+ * This represents the YAML frontmatter in SKILL.md (or skill.yaml/legacy enact.md/enact.yaml)
80
108
  */
81
109
  export interface ToolManifest {
82
110
  // ==================== Required Fields ====================
@@ -115,9 +143,6 @@ export interface ToolManifest {
115
143
 
116
144
  // ==================== Schema Fields ====================
117
145
 
118
- /** JSON Schema defining input parameters */
119
- inputSchema?: JSONSchema7;
120
-
121
146
  /** JSON Schema defining output structure */
122
147
  outputSchema?: JSONSchema7;
123
148
 
@@ -134,6 +159,28 @@ export interface ToolManifest {
134
159
  /** Resource limits and requirements */
135
160
  resources?: ResourceRequirements;
136
161
 
162
+ // ==================== Lifecycle Hooks ====================
163
+
164
+ /** Lifecycle hooks (e.g., postinstall build step) */
165
+ hooks?: ToolHooks;
166
+
167
+ // ==================== Scripts ====================
168
+
169
+ /**
170
+ * Inline executable scripts (each becomes an MCP tool via colon syntax)
171
+ *
172
+ * Scripts replace the need for a separate ACTIONS.yaml file.
173
+ * Each script maps a name to a command with {{param}} template syntax.
174
+ *
175
+ * @example
176
+ * scripts:
177
+ * scrape: python scripts/scrape.py {{url}}
178
+ * crawl:
179
+ * command: python scripts/crawl.py {{url}} {{depth}}
180
+ * description: Crawl a website to specified depth
181
+ */
182
+ scripts?: Record<string, ScriptDefinition>;
183
+
137
184
  // ==================== Agent Skills Spec Fields ====================
138
185
 
139
186
  /** Environment requirements (intended product, system packages, network access, etc.) */
@@ -252,14 +299,28 @@ export interface ToolResolution {
252
299
  manifestPath: string;
253
300
  /** Tool version (if available) */
254
301
  version?: string | undefined;
302
+ /** The resolved script (if a script was specified via colon syntax) */
303
+ action?: import("./actions").Action | undefined;
304
+ /** The requested script name (if specified via colon syntax) */
305
+ actionName?: string | undefined;
306
+ /** The scripts manifest (converted from inline scripts) */
307
+ actionsManifest?: import("./actions").ActionsManifest | undefined;
255
308
  }
256
309
 
257
310
  /**
258
311
  * Supported manifest file names
259
312
  * SKILL.md is the primary format (aligned with Anthropic Agent Skills)
313
+ * skill.yaml/yml is the package manifest
260
314
  * enact.md/yaml/yml are supported for backwards compatibility
261
315
  */
262
- export const MANIFEST_FILES = ["SKILL.md", "enact.md", "enact.yaml", "enact.yml"] as const;
316
+ export const MANIFEST_FILES = [
317
+ "SKILL.md",
318
+ "skill.yaml",
319
+ "skill.yml",
320
+ "enact.md",
321
+ "enact.yaml",
322
+ "enact.yml",
323
+ ] as const;
263
324
  export type ManifestFileName = (typeof MANIFEST_FILES)[number];
264
325
 
265
326
  /**
@@ -0,0 +1,249 @@
1
+ /**
2
+ * Tests for the action command interpolation module.
3
+ *
4
+ * Covers {{param}} template parsing, interpolation, omission of optionals,
5
+ * and the prepareActionCommand entry point.
6
+ */
7
+
8
+ import { describe, expect, test } from "bun:test";
9
+ import {
10
+ getActionCommandParams,
11
+ getMissingRequiredParams,
12
+ hasActionTemplates,
13
+ interpolateActionCommand,
14
+ parseActionArgument,
15
+ parseActionCommand,
16
+ prepareActionCommand,
17
+ } from "../src/execution/action-command";
18
+
19
+ describe("hasActionTemplates", () => {
20
+ test("returns true for string with {{param}}", () => {
21
+ expect(hasActionTemplates("hello {{name}}")).toBe(true);
22
+ });
23
+
24
+ test("returns false for plain string", () => {
25
+ expect(hasActionTemplates("echo hello")).toBe(false);
26
+ });
27
+
28
+ test("returns false for ${param} syntax", () => {
29
+ expect(hasActionTemplates("echo ${name}")).toBe(false);
30
+ });
31
+
32
+ test("returns true for standalone {{param}}", () => {
33
+ expect(hasActionTemplates("{{url}}")).toBe(true);
34
+ });
35
+ });
36
+
37
+ describe("parseActionArgument", () => {
38
+ test("parses a literal argument", () => {
39
+ const result = parseActionArgument("echo");
40
+ expect(result.tokens).toHaveLength(1);
41
+ expect(result.tokens[0]?.type).toBe("literal");
42
+ expect(result.tokens[0]?.type === "literal" && result.tokens[0].value).toBe("echo");
43
+ expect(result.parameters).toHaveLength(0);
44
+ });
45
+
46
+ test("parses a standalone parameter", () => {
47
+ const result = parseActionArgument("{{url}}");
48
+ expect(result.tokens).toHaveLength(1);
49
+ expect(result.tokens[0]?.type).toBe("parameter");
50
+ expect(result.tokens[0]?.type === "parameter" && result.tokens[0].name).toBe("url");
51
+ expect(result.parameters).toEqual(["url"]);
52
+ });
53
+
54
+ test("parses mixed literal and parameter", () => {
55
+ const result = parseActionArgument("--name={{name}}");
56
+ expect(result.tokens).toHaveLength(2);
57
+ expect(result.tokens[0]?.type).toBe("literal");
58
+ expect(result.tokens[1]?.type).toBe("parameter");
59
+ expect(result.parameters).toEqual(["name"]);
60
+ });
61
+
62
+ test("parses multiple parameters in one argument", () => {
63
+ const result = parseActionArgument("{{host}}:{{port}}");
64
+ expect(result.tokens).toHaveLength(3);
65
+ expect(result.parameters).toEqual(["host", "port"]);
66
+ });
67
+
68
+ test("trims whitespace in parameter names", () => {
69
+ const result = parseActionArgument("{{ name }}");
70
+ expect(result.parameters).toEqual(["name"]);
71
+ });
72
+ });
73
+
74
+ describe("parseActionCommand", () => {
75
+ test("parses command with no templates", () => {
76
+ const result = parseActionCommand(["echo", "hello", "world"]);
77
+ expect(result.allParameters).toHaveLength(0);
78
+ expect(result.arguments).toHaveLength(3);
79
+ });
80
+
81
+ test("parses command with templates", () => {
82
+ const result = parseActionCommand(["python", "main.py", "{{url}}"]);
83
+ expect(result.allParameters).toEqual(["url"]);
84
+ });
85
+
86
+ test("deduplicates parameter names", () => {
87
+ const result = parseActionCommand(["echo", "{{name}}", "{{name}}"]);
88
+ expect(result.allParameters).toEqual(["name"]);
89
+ });
90
+
91
+ test("collects all unique parameters", () => {
92
+ const result = parseActionCommand(["cmd", "{{a}}", "{{b}}", "{{c}}"]);
93
+ expect(result.allParameters).toEqual(["a", "b", "c"]);
94
+ });
95
+ });
96
+
97
+ describe("interpolateActionCommand", () => {
98
+ test("replaces parameter with value", () => {
99
+ const result = interpolateActionCommand(["echo", "{{name}}"], { name: "Alice" });
100
+ expect(result).toEqual(["echo", "Alice"]);
101
+ });
102
+
103
+ test("preserves literal arguments", () => {
104
+ const result = interpolateActionCommand(["python", "main.py", "--verbose"], {});
105
+ expect(result).toEqual(["python", "main.py", "--verbose"]);
106
+ });
107
+
108
+ test("handles mixed literal and parameter", () => {
109
+ const result = interpolateActionCommand(["--output={{format}}"], { format: "json" });
110
+ expect(result).toEqual(["--output=json"]);
111
+ });
112
+
113
+ test("converts number to string", () => {
114
+ const result = interpolateActionCommand(["echo", "{{count}}"], { count: 42 });
115
+ expect(result).toEqual(["echo", "42"]);
116
+ });
117
+
118
+ test("converts boolean to string", () => {
119
+ const result = interpolateActionCommand(["echo", "{{flag}}"], { flag: true });
120
+ expect(result).toEqual(["echo", "true"]);
121
+ });
122
+
123
+ test("converts object to JSON string", () => {
124
+ const result = interpolateActionCommand(["echo", "{{data}}"], { data: { key: "value" } });
125
+ expect(result).toEqual(["echo", '{"key":"value"}']);
126
+ });
127
+
128
+ test("omits argument for optional param with no value", () => {
129
+ const schema = {
130
+ type: "object" as const,
131
+ properties: {
132
+ name: { type: "string" as const },
133
+ verbose: { type: "boolean" as const },
134
+ },
135
+ required: ["name"],
136
+ };
137
+
138
+ const result = interpolateActionCommand(
139
+ ["echo", "{{name}}", "{{verbose}}"],
140
+ { name: "Alice" },
141
+ { inputSchema: schema }
142
+ );
143
+ // "verbose" is optional with no value → omitted
144
+ expect(result).toEqual(["echo", "Alice"]);
145
+ });
146
+
147
+ test("uses default value for optional param", () => {
148
+ const schema = {
149
+ type: "object" as const,
150
+ properties: {
151
+ format: { type: "string" as const, default: "text" },
152
+ },
153
+ };
154
+
155
+ const result = interpolateActionCommand(["echo", "{{format}}"], {}, { inputSchema: schema });
156
+ expect(result).toEqual(["echo", "text"]);
157
+ });
158
+
159
+ test("does not split values with spaces into multiple args", () => {
160
+ const result = interpolateActionCommand(["echo", "{{msg}}"], {
161
+ msg: "hello world with spaces",
162
+ });
163
+ // Must remain a single argument — security property
164
+ expect(result).toEqual(["echo", "hello world with spaces"]);
165
+ });
166
+ });
167
+
168
+ describe("getMissingRequiredParams", () => {
169
+ test("returns empty when all required params provided", () => {
170
+ const schema = {
171
+ type: "object" as const,
172
+ required: ["name"],
173
+ properties: { name: { type: "string" as const } },
174
+ };
175
+ const missing = getMissingRequiredParams(["echo", "{{name}}"], { name: "hi" }, schema);
176
+ expect(missing).toHaveLength(0);
177
+ });
178
+
179
+ test("returns missing required param names", () => {
180
+ const schema = {
181
+ type: "object" as const,
182
+ required: ["name", "age"],
183
+ properties: {
184
+ name: { type: "string" as const },
185
+ age: { type: "number" as const },
186
+ },
187
+ };
188
+ const missing = getMissingRequiredParams(
189
+ ["echo", "{{name}}", "{{age}}"],
190
+ { name: "Alice" },
191
+ schema
192
+ );
193
+ expect(missing).toEqual(["age"]);
194
+ });
195
+
196
+ test("does not report optional params as missing", () => {
197
+ const schema = {
198
+ type: "object" as const,
199
+ required: [],
200
+ properties: { verbose: { type: "boolean" as const } },
201
+ };
202
+ const missing = getMissingRequiredParams(["echo", "{{verbose}}"], {}, schema);
203
+ expect(missing).toHaveLength(0);
204
+ });
205
+ });
206
+
207
+ describe("getActionCommandParams", () => {
208
+ test("returns all parameter names from command", () => {
209
+ expect(getActionCommandParams(["cmd", "{{a}}", "{{b}}"])).toEqual(["a", "b"]);
210
+ });
211
+
212
+ test("returns empty for command without templates", () => {
213
+ expect(getActionCommandParams(["echo", "hello"])).toEqual([]);
214
+ });
215
+ });
216
+
217
+ describe("prepareActionCommand", () => {
218
+ test("interpolates and returns command array", () => {
219
+ const schema = {
220
+ type: "object" as const,
221
+ required: ["url"],
222
+ properties: { url: { type: "string" as const } },
223
+ };
224
+ const result = prepareActionCommand(
225
+ ["python", "main.py", "{{url}}"],
226
+ { url: "https://example.com" },
227
+ schema
228
+ );
229
+ expect(result).toEqual(["python", "main.py", "https://example.com"]);
230
+ });
231
+
232
+ test("throws when required parameters are missing", () => {
233
+ const schema = {
234
+ type: "object" as const,
235
+ required: ["url"],
236
+ properties: { url: { type: "string" as const } },
237
+ };
238
+ expect(() => prepareActionCommand(["python", "main.py", "{{url}}"], {}, schema)).toThrow(
239
+ "Missing required parameters: url"
240
+ );
241
+ });
242
+
243
+ test("works without schema (all params treated conservatively)", () => {
244
+ // Without schema, all params are treated as required
245
+ expect(() => prepareActionCommand(["echo", "{{name}}"], {})).toThrow(
246
+ "Missing required parameters: name"
247
+ );
248
+ });
249
+ });