@enactprotocol/cli 2.1.24 → 2.1.29

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 (57) hide show
  1. package/dist/commands/index.d.ts +2 -0
  2. package/dist/commands/index.d.ts.map +1 -1
  3. package/dist/commands/index.js +4 -0
  4. package/dist/commands/index.js.map +1 -1
  5. package/dist/commands/init/templates/claude.d.ts +1 -1
  6. package/dist/commands/init/templates/claude.d.ts.map +1 -1
  7. package/dist/commands/init/templates/claude.js +268 -28
  8. package/dist/commands/init/templates/claude.js.map +1 -1
  9. package/dist/commands/init/templates/tool-agents.d.ts +1 -1
  10. package/dist/commands/init/templates/tool-agents.d.ts.map +1 -1
  11. package/dist/commands/init/templates/tool-agents.js +90 -15
  12. package/dist/commands/init/templates/tool-agents.js.map +1 -1
  13. package/dist/commands/install/index.d.ts.map +1 -1
  14. package/dist/commands/install/index.js +9 -1
  15. package/dist/commands/install/index.js.map +1 -1
  16. package/dist/commands/learn/index.d.ts.map +1 -1
  17. package/dist/commands/learn/index.js +4 -11
  18. package/dist/commands/learn/index.js.map +1 -1
  19. package/dist/commands/mcp/index.d.ts.map +1 -1
  20. package/dist/commands/mcp/index.js +204 -53
  21. package/dist/commands/mcp/index.js.map +1 -1
  22. package/dist/commands/run/index.d.ts.map +1 -1
  23. package/dist/commands/run/index.js +380 -39
  24. package/dist/commands/run/index.js.map +1 -1
  25. package/dist/commands/validate/index.d.ts +11 -0
  26. package/dist/commands/validate/index.d.ts.map +1 -0
  27. package/dist/commands/validate/index.js +299 -0
  28. package/dist/commands/validate/index.js.map +1 -0
  29. package/dist/index.d.ts +1 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +6 -2
  32. package/dist/index.js.map +1 -1
  33. package/dist/types.d.ts +2 -0
  34. package/dist/types.d.ts.map +1 -1
  35. package/dist/types.js.map +1 -1
  36. package/dist/utils/errors.d.ts +8 -1
  37. package/dist/utils/errors.d.ts.map +1 -1
  38. package/dist/utils/errors.js +13 -2
  39. package/dist/utils/errors.js.map +1 -1
  40. package/package.json +5 -5
  41. package/src/commands/index.ts +5 -0
  42. package/src/commands/init/templates/claude.ts +268 -28
  43. package/src/commands/init/templates/tool-agents.ts +90 -15
  44. package/src/commands/install/index.ts +11 -0
  45. package/src/commands/learn/index.ts +6 -11
  46. package/src/commands/mcp/index.ts +768 -0
  47. package/src/commands/run/README.md +68 -1
  48. package/src/commands/run/index.ts +475 -35
  49. package/src/commands/validate/index.ts +344 -0
  50. package/src/index.ts +8 -1
  51. package/src/types.ts +2 -0
  52. package/src/utils/errors.ts +26 -6
  53. package/tests/commands/init.test.ts +2 -2
  54. package/tests/commands/run.test.ts +260 -0
  55. package/tests/commands/validate.test.ts +81 -0
  56. package/tests/utils/errors.test.ts +36 -0
  57. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,344 @@
1
+ /**
2
+ * enact validate command
3
+ *
4
+ * Validate a SKILL.md file for common issues and best practices.
5
+ */
6
+
7
+ import { existsSync } from "node:fs";
8
+ import { resolve } from "node:path";
9
+ import { type ToolManifest, tryResolveTool } from "@enactprotocol/shared";
10
+ import type { Command } from "commander";
11
+ import type { CommandContext, GlobalOptions } from "../../types";
12
+ import { colors, dim, error, formatError, info, newline, success, symbols } from "../../utils";
13
+
14
+ interface ValidateOptions extends GlobalOptions {
15
+ fix?: boolean;
16
+ }
17
+
18
+ interface ValidationIssue {
19
+ level: "error" | "warning" | "info";
20
+ message: string;
21
+ suggestion?: string;
22
+ }
23
+
24
+ /**
25
+ * Extract parameter names from a command template
26
+ */
27
+ function extractCommandParams(command: string): string[] {
28
+ const params: string[] = [];
29
+ const regex = /\$\{([^}:]+)(?::[^}]+)?\}/g;
30
+ let match: RegExpExecArray | null;
31
+ match = regex.exec(command);
32
+ while (match !== null) {
33
+ if (match[1] && !params.includes(match[1])) {
34
+ params.push(match[1]);
35
+ }
36
+ match = regex.exec(command);
37
+ }
38
+ return params;
39
+ }
40
+
41
+ /**
42
+ * Validate a tool manifest
43
+ */
44
+ function validateManifest(manifest: ToolManifest, sourceDir: string): ValidationIssue[] {
45
+ const issues: ValidationIssue[] = [];
46
+
47
+ // Check name format
48
+ if (!manifest.name) {
49
+ issues.push({
50
+ level: "error",
51
+ message: "Missing 'name' field",
52
+ suggestion: "Add a name in format: namespace/category/tool",
53
+ });
54
+ } else if (!manifest.name.includes("/")) {
55
+ issues.push({
56
+ level: "warning",
57
+ message: `Name '${manifest.name}' should follow hierarchical format`,
58
+ suggestion: "Use format: namespace/category/tool (e.g., alice/utils/formatter)",
59
+ });
60
+ }
61
+
62
+ // Check version
63
+ if (!manifest.version) {
64
+ issues.push({
65
+ level: "warning",
66
+ message: "Missing 'version' field",
67
+ suggestion: "Add a version using semver (e.g., 1.0.0)",
68
+ });
69
+ }
70
+
71
+ // Check description
72
+ if (!manifest.description) {
73
+ issues.push({
74
+ level: "warning",
75
+ message: "Missing 'description' field",
76
+ suggestion: "Add a clear description for discoverability",
77
+ });
78
+ }
79
+
80
+ // Check base image
81
+ if (manifest.from) {
82
+ if (manifest.from.endsWith(":latest")) {
83
+ issues.push({
84
+ level: "warning",
85
+ message: `Base image '${manifest.from}' uses :latest tag`,
86
+ suggestion: "Pin to a specific version (e.g., python:3.12-slim instead of python:latest)",
87
+ });
88
+ }
89
+ } else if (manifest.command) {
90
+ issues.push({
91
+ level: "info",
92
+ message: "No 'from' field specified, will use alpine:latest",
93
+ suggestion: "Consider specifying a base image for reproducibility",
94
+ });
95
+ }
96
+
97
+ // Check command vs instruction tool
98
+ if (!manifest.command) {
99
+ // Instruction-based tool
100
+ issues.push({
101
+ level: "info",
102
+ message: "No 'command' field - this is an LLM instruction tool",
103
+ });
104
+ } else {
105
+ // Command-based tool - validate parameters
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
+ }
148
+ }
149
+
150
+ // Check for double-quoting in command
151
+ if (
152
+ manifest.command.includes("'${") ||
153
+ manifest.command.includes('"${') ||
154
+ (manifest.command.includes("${") &&
155
+ (manifest.command.includes("}'") || manifest.command.includes('}"')))
156
+ ) {
157
+ issues.push({
158
+ level: "warning",
159
+ message: "Command may have manual quotes around parameters",
160
+ suggestion: "Enact auto-quotes parameters. Remove manual quotes around ${param}",
161
+ });
162
+ }
163
+ }
164
+
165
+ // Check timeout
166
+ if (!manifest.timeout && manifest.command) {
167
+ issues.push({
168
+ level: "info",
169
+ message: "No 'timeout' specified, using default (5 minutes)",
170
+ suggestion: "Consider setting an explicit timeout for long-running tools",
171
+ });
172
+ }
173
+
174
+ // Check for common file patterns
175
+ if (manifest.command) {
176
+ // Python tools
177
+ if (manifest.command.includes("python") && manifest.from?.includes("python")) {
178
+ const mainPy = resolve(sourceDir, "main.py");
179
+ if (manifest.command.includes("/workspace/main.py") && !existsSync(mainPy)) {
180
+ issues.push({
181
+ level: "error",
182
+ message: "Command references /workspace/main.py but main.py not found",
183
+ suggestion: "Create main.py or update the command path",
184
+ });
185
+ }
186
+ }
187
+
188
+ // Node tools
189
+ if (manifest.command.includes("node") && manifest.from?.includes("node")) {
190
+ const indexJs = resolve(sourceDir, "index.js");
191
+ const mainJs = resolve(sourceDir, "main.js");
192
+ if (
193
+ manifest.command.includes("/workspace/index.js") &&
194
+ !existsSync(indexJs) &&
195
+ !existsSync(mainJs)
196
+ ) {
197
+ issues.push({
198
+ level: "warning",
199
+ message: "Command references /workspace/index.js but index.js not found",
200
+ suggestion: "Create index.js or update the command path",
201
+ });
202
+ }
203
+ }
204
+ }
205
+
206
+ // Check env declarations
207
+ if (manifest.env) {
208
+ for (const [key, envDef] of Object.entries(manifest.env)) {
209
+ if (envDef.secret && !envDef.description) {
210
+ issues.push({
211
+ level: "info",
212
+ message: `Secret '${key}' has no description`,
213
+ suggestion: "Add a description to help users understand what this secret is for",
214
+ });
215
+ }
216
+ }
217
+ }
218
+
219
+ return issues;
220
+ }
221
+
222
+ /**
223
+ * Display validation results
224
+ */
225
+ function displayResults(issues: ValidationIssue[], toolPath: string): void {
226
+ const errors = issues.filter((i) => i.level === "error");
227
+ const warnings = issues.filter((i) => i.level === "warning");
228
+ const infos = issues.filter((i) => i.level === "info");
229
+
230
+ newline();
231
+
232
+ if (issues.length === 0) {
233
+ success(`${symbols.success} ${toolPath} - No issues found!`);
234
+ return;
235
+ }
236
+
237
+ info(`Validation results for ${toolPath}:`);
238
+ newline();
239
+
240
+ // Display errors
241
+ for (const issue of errors) {
242
+ console.log(` ${colors.error(`${symbols.error} ERROR:`)} ${issue.message}`);
243
+ if (issue.suggestion) {
244
+ dim(` → ${issue.suggestion}`);
245
+ }
246
+ }
247
+
248
+ // Display warnings
249
+ for (const issue of warnings) {
250
+ console.log(` ${colors.warning(`${symbols.warning} WARNING:`)} ${issue.message}`);
251
+ if (issue.suggestion) {
252
+ dim(` → ${issue.suggestion}`);
253
+ }
254
+ }
255
+
256
+ // Display info
257
+ for (const issue of infos) {
258
+ console.log(` ${colors.info(`${symbols.info} INFO:`)} ${issue.message}`);
259
+ if (issue.suggestion) {
260
+ dim(` → ${issue.suggestion}`);
261
+ }
262
+ }
263
+
264
+ newline();
265
+
266
+ // Summary
267
+ const summary: string[] = [];
268
+ if (errors.length > 0) summary.push(`${errors.length} error${errors.length > 1 ? "s" : ""}`);
269
+ if (warnings.length > 0)
270
+ summary.push(`${warnings.length} warning${warnings.length > 1 ? "s" : ""}`);
271
+ if (infos.length > 0) summary.push(`${infos.length} info`);
272
+
273
+ if (errors.length > 0) {
274
+ error(`Found ${summary.join(", ")}`);
275
+ } else if (warnings.length > 0) {
276
+ console.log(colors.warning(`Found ${summary.join(", ")}`));
277
+ } else {
278
+ success(`Found ${summary.join(", ")} - looking good!`);
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Validate command handler
284
+ */
285
+ function validateHandler(toolPath: string, _options: ValidateOptions, ctx: CommandContext): void {
286
+ const resolvedPath = resolve(ctx.cwd, toolPath);
287
+
288
+ // Check if path exists
289
+ if (!existsSync(resolvedPath)) {
290
+ error(`Path not found: ${resolvedPath}`);
291
+ process.exit(1);
292
+ }
293
+
294
+ // Resolve the tool
295
+ const resolution = tryResolveTool(resolvedPath);
296
+
297
+ if (!resolution) {
298
+ error(`Could not find SKILL.md in: ${resolvedPath}`);
299
+ dim("Make sure the directory contains a valid SKILL.md file.");
300
+ process.exit(1);
301
+ }
302
+
303
+ // Validate the manifest
304
+ const issues = validateManifest(resolution.manifest, resolution.sourceDir);
305
+
306
+ // Display results
307
+ displayResults(issues, toolPath);
308
+
309
+ // Exit with error if there are errors
310
+ const hasErrors = issues.some((i) => i.level === "error");
311
+ if (hasErrors) {
312
+ process.exit(1);
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Configure the validate command
318
+ */
319
+ export function configureValidateCommand(program: Command): void {
320
+ program
321
+ .command("validate")
322
+ .description("Validate a SKILL.md file for common issues")
323
+ .argument("[path]", "Path to tool directory", ".")
324
+ .option("-v, --verbose", "Show detailed output")
325
+ .option("--json", "Output result as JSON")
326
+ .action((toolPath: string, options: ValidateOptions) => {
327
+ const ctx: CommandContext = {
328
+ cwd: process.cwd(),
329
+ options,
330
+ isCI: Boolean(process.env.CI),
331
+ isInteractive: process.stdout.isTTY ?? false,
332
+ };
333
+
334
+ try {
335
+ validateHandler(toolPath, options, ctx);
336
+ } catch (err) {
337
+ error(formatError(err));
338
+ if (options.verbose && err instanceof Error && err.stack) {
339
+ dim(err.stack);
340
+ }
341
+ process.exit(1);
342
+ }
343
+ });
344
+ }
package/src/index.ts CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  configureInstallCommand,
22
22
  configureLearnCommand,
23
23
  configureListCommand,
24
+ configureMcpCommand,
24
25
  configurePublishCommand,
25
26
  configureReportCommand,
26
27
  configureRunCommand,
@@ -29,12 +30,13 @@ import {
29
30
  configureSignCommand,
30
31
  configureTrustCommand,
31
32
  configureUnyankCommand,
33
+ configureValidateCommand,
32
34
  configureVisibilityCommand,
33
35
  configureYankCommand,
34
36
  } from "./commands";
35
37
  import { error, formatError } from "./utils";
36
38
 
37
- export const version = "2.1.24";
39
+ export const version = "2.1.29";
38
40
 
39
41
  // Export types for external use
40
42
  export type { GlobalOptions, CommandContext } from "./types";
@@ -81,6 +83,11 @@ async function main() {
81
83
 
82
84
  // Private tools - visibility management
83
85
  configureVisibilityCommand(program);
86
+ // MCP integration commands
87
+ configureMcpCommand(program);
88
+
89
+ // Validation command
90
+ configureValidateCommand(program);
84
91
 
85
92
  // Global error handler - handle Commander's help/version exits gracefully
86
93
  program.exitOverride((err) => {
package/src/types.ts CHANGED
@@ -16,6 +16,8 @@ export interface GlobalOptions {
16
16
  quiet?: boolean;
17
17
  /** Run without making changes (preview mode) */
18
18
  dryRun?: boolean;
19
+ /** Enable debug mode - show detailed parameter and environment information */
20
+ debug?: boolean;
19
21
  }
20
22
 
21
23
  /**
@@ -45,12 +45,32 @@ export class CliError extends Error {
45
45
  * Tool not found error
46
46
  */
47
47
  export class ToolNotFoundError extends CliError {
48
- constructor(toolName: string) {
49
- super(
50
- `Tool not found: ${toolName}`,
51
- EXIT_TOOL_NOT_FOUND,
52
- "Check the tool name or provide a path to a local tool.\nFor registry tools, use the format: owner/namespace/tool[@version]"
53
- );
48
+ constructor(
49
+ toolName: string,
50
+ options?: {
51
+ /** Additional context about why the tool wasn't found */
52
+ reason?: string;
53
+ /** Locations that were searched */
54
+ searchedLocations?: string[];
55
+ /** Whether --local flag was set */
56
+ localOnly?: boolean;
57
+ }
58
+ ) {
59
+ let message = `Tool not found: ${toolName}`;
60
+ if (options?.reason) {
61
+ message += `\n${options.reason}`;
62
+ }
63
+ if (options?.searchedLocations && options.searchedLocations.length > 0) {
64
+ message += `\nSearched locations:\n${options.searchedLocations.map((l) => ` - ${l}`).join("\n")}`;
65
+ }
66
+
67
+ let suggestion =
68
+ "Check the tool name or provide a path to a local tool.\nFor registry tools, use the format: owner/namespace/tool[@version]";
69
+ if (options?.localOnly) {
70
+ suggestion = "Remove --local flag to search the registry, or check the tool path.";
71
+ }
72
+
73
+ super(message, EXIT_TOOL_NOT_FOUND, suggestion);
54
74
  this.name = "ToolNotFoundError";
55
75
  }
56
76
  }
@@ -456,7 +456,7 @@ describe("init command", () => {
456
456
  expect(content).toContain("enact publish");
457
457
  expect(content).toContain("${param}");
458
458
  expect(content).toContain("build:");
459
- expect(content).toContain("/work");
459
+ expect(content).toContain("/workspace");
460
460
  });
461
461
 
462
462
  test("AGENTS.md for agent projects contains usage instructions", async () => {
@@ -565,7 +565,7 @@ describe("init command", () => {
565
565
 
566
566
  // Should be comprehensive but not excessive
567
567
  const lines = content.split("\n").length;
568
- expect(lines).toBeLessThan(250); // Comprehensive guide under 250 lines
568
+ expect(lines).toBeLessThan(350); // Comprehensive guide under 350 lines (includes base image docs, troubleshooting)
569
569
  expect(lines).toBeGreaterThan(100); // But not too sparse
570
570
 
571
571
  // Clean up