@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
@@ -2,7 +2,7 @@
2
2
  * enact install command
3
3
  *
4
4
  * Install a tool to the project or globally.
5
- * All tools are extracted to ~/.enact/cache/{tool}/{version}/
5
+ * All tools are extracted to ~/.agent/skills/{tool}/
6
6
  * - Project install: Adds entry to .enact/tools.json
7
7
  * - Global install: Adds entry to ~/.enact/tools.json
8
8
  *
@@ -25,7 +25,6 @@ import {
25
25
  addAlias,
26
26
  addMcpTool,
27
27
  addToolToRegistry,
28
- getCacheDir,
29
28
  getInstalledVersion,
30
29
  getMinimumAttestations,
31
30
  getProjectEnactDir,
@@ -110,7 +109,8 @@ function isLocalPath(toolName: string): boolean {
110
109
  */
111
110
  async function extractBundle(bundleData: ArrayBuffer, destPath: string): Promise<void> {
112
111
  // Create a temporary file for the bundle
113
- const tempFile = join(getCacheDir(), `bundle-${Date.now()}.tar.gz`);
112
+ const { tmpdir } = await import("node:os");
113
+ const tempFile = join(tmpdir(), `enact-bundle-${Date.now()}.tar.gz`);
114
114
  mkdirSync(dirname(tempFile), { recursive: true });
115
115
  writeFileSync(tempFile, Buffer.from(bundleData));
116
116
 
@@ -149,6 +149,40 @@ function formatBytes(bytes: number): string {
149
149
  return `${(bytes / k ** i).toFixed(1)} ${sizes[i]}`;
150
150
  }
151
151
 
152
+ /**
153
+ * Run postinstall hook commands in the tool directory.
154
+ * Runs each command sequentially; throws on first failure.
155
+ */
156
+ async function runPostinstallHook(
157
+ toolDir: string,
158
+ hookCommands: string | string[],
159
+ _toolName: string,
160
+ verbose?: boolean
161
+ ): Promise<void> {
162
+ const commands = Array.isArray(hookCommands) ? hookCommands : [hookCommands];
163
+
164
+ for (const cmd of commands) {
165
+ if (verbose) {
166
+ dim(` Running: ${cmd}`);
167
+ }
168
+
169
+ const proc = Bun.spawn(["sh", "-c", cmd], {
170
+ cwd: toolDir,
171
+ stdout: verbose ? "inherit" : "pipe",
172
+ stderr: "pipe",
173
+ env: { ...process.env },
174
+ });
175
+
176
+ const exitCode = await proc.exited;
177
+ if (exitCode !== 0) {
178
+ const stderr = await new Response(proc.stderr).text();
179
+ throw new Error(
180
+ `postinstall hook failed (exit ${exitCode}): ${cmd}${stderr ? `\n${stderr.trim()}` : ""}`
181
+ );
182
+ }
183
+ }
184
+ }
185
+
152
186
  /**
153
187
  * Install from the registry
154
188
  */
@@ -258,25 +292,29 @@ async function installFromRegistry(
258
292
  const attestations = attestationsResponse.attestations;
259
293
 
260
294
  if (attestations.length === 0) {
261
- // No attestations found
262
- info(`${symbols.warning} Tool ${toolName}@${targetVersion} has no attestations.`);
263
-
264
- if (trustPolicy === "require_attestation") {
265
- error("Trust policy requires attestations. Installation blocked.");
266
- info("Configure trust policy in ~/.enact/config.yaml");
267
- process.exit(EXIT_TRUST_ERROR);
268
- } else if (ctx.isInteractive && trustPolicy === "prompt") {
269
- const proceed = await confirm("Install unverified tool?");
270
- if (!proceed) {
271
- info("Installation cancelled.");
272
- process.exit(0);
295
+ // No attestations found — but if minimum_attestations is 0, that's fine
296
+ if (minimumAttestations === 0) {
297
+ // User explicitly configured zero required attestations — allow installation
298
+ } else {
299
+ info(`${symbols.warning} Tool ${toolName}@${targetVersion} has no attestations.`);
300
+
301
+ if (trustPolicy === "require_attestation") {
302
+ error("Trust policy requires attestations. Installation blocked.");
303
+ info("Configure trust policy in ~/.enact/config.yaml");
304
+ process.exit(EXIT_TRUST_ERROR);
305
+ } else if (ctx.isInteractive && trustPolicy === "prompt") {
306
+ const proceed = await confirm("Install unverified tool?");
307
+ if (!proceed) {
308
+ info("Installation cancelled.");
309
+ process.exit(0);
310
+ }
311
+ } else if (!ctx.isInteractive && trustPolicy === "prompt") {
312
+ error("Cannot install unverified tools in non-interactive mode.");
313
+ info("Run interactively to confirm installation.");
314
+ process.exit(EXIT_TRUST_ERROR);
273
315
  }
274
- } else if (!ctx.isInteractive && trustPolicy === "prompt") {
275
- error("Cannot install unverified tools in non-interactive mode.");
276
- info("Run interactively to confirm installation.");
277
- process.exit(EXIT_TRUST_ERROR);
278
316
  }
279
- // trustPolicy === "allow" - continue without prompting
317
+ // trustPolicy === "allow" or minimumAttestations === 0 - continue without prompting
280
318
  } else {
281
319
  // Verify attestations locally (never trust registry's verification status)
282
320
  const verifiedAuditors = await verifyAllAttestations(
@@ -438,6 +476,26 @@ async function installFromRegistry(
438
476
  throw new RegistryError(`Failed to extract bundle: ${formatError(err)}`);
439
477
  }
440
478
 
479
+ // Run postinstall hook if the manifest defines one
480
+ const extractedManifest = loadManifestFromDir(cachePath);
481
+ if (extractedManifest?.manifest.hooks?.postinstall) {
482
+ try {
483
+ await withSpinner(
484
+ "Running postinstall hook...",
485
+ async () =>
486
+ runPostinstallHook(
487
+ cachePath,
488
+ extractedManifest.manifest.hooks!.postinstall!,
489
+ toolName,
490
+ options.verbose
491
+ ),
492
+ `${symbols.success} postinstall complete`
493
+ );
494
+ } catch (err) {
495
+ info(`${symbols.warning} postinstall hook failed: ${formatError(err)}`);
496
+ }
497
+ }
498
+
441
499
  // Update tools.json for the appropriate scope
442
500
  addToolToRegistry(toolName, targetVersion!, scope, isGlobal ? undefined : ctx.cwd);
443
501
 
@@ -485,7 +543,7 @@ async function installFromRegistry(
485
543
  * Install from a local path
486
544
  *
487
545
  * Both global and project installs:
488
- * 1. Copy tool to cache (~/.enact/cache/{tool}/{version}/)
546
+ * 1. Copy tool to ~/.agent/skills/{tool}/
489
547
  * 2. Update tools.json (global: ~/.enact/tools.json, project: .enact/tools.json)
490
548
  */
491
549
  async function installFromPath(
@@ -547,6 +605,25 @@ async function installFromPath(
547
605
  `${symbols.success} Installed ${manifest.name}`
548
606
  );
549
607
 
608
+ // Run postinstall hook if the manifest defines one
609
+ if (manifest.hooks?.postinstall) {
610
+ try {
611
+ await withSpinner(
612
+ "Running postinstall hook...",
613
+ async () =>
614
+ runPostinstallHook(
615
+ cachePath,
616
+ manifest.hooks!.postinstall!,
617
+ manifest.name,
618
+ options.verbose
619
+ ),
620
+ `${symbols.success} postinstall complete`
621
+ );
622
+ } catch (err) {
623
+ info(`${symbols.warning} postinstall hook failed: ${formatError(err)}`);
624
+ }
625
+ }
626
+
550
627
  // Update tools.json for the appropriate scope
551
628
  addToolToRegistry(manifest.name, version, scope, isGlobal ? undefined : ctx.cwd);
552
629
 
@@ -18,6 +18,7 @@ import {
18
18
  verifyAllAttestations,
19
19
  } from "@enactprotocol/api";
20
20
  import {
21
+ type ActionsManifest,
21
22
  getMinimumAttestations,
22
23
  getTrustPolicy,
23
24
  getTrustedAuditors,
@@ -45,6 +46,48 @@ interface LearnOptions extends GlobalOptions {
45
46
  local?: boolean;
46
47
  }
47
48
 
49
+ /**
50
+ * Display available actions from an ActionsManifest
51
+ */
52
+ function displayActions(actionsManifest: ActionsManifest): void {
53
+ newline();
54
+ header("Available Actions");
55
+ newline();
56
+
57
+ // Iterate over action map (name is the key)
58
+ for (const [actionName, action] of Object.entries(actionsManifest.actions)) {
59
+ info(` ${actionName}`);
60
+ if (action.description) {
61
+ dim(` ${action.description}`);
62
+ }
63
+
64
+ // Show input parameters
65
+ if (action.inputSchema?.properties) {
66
+ const required = new Set(
67
+ Array.isArray(action.inputSchema.required) ? action.inputSchema.required : []
68
+ );
69
+ const props = action.inputSchema.properties as Record<
70
+ string,
71
+ { type?: string; description?: string }
72
+ >;
73
+ const paramNames = Object.keys(props);
74
+
75
+ if (paramNames.length > 0) {
76
+ dim(" Parameters:");
77
+ for (const paramName of paramNames) {
78
+ const prop = props[paramName];
79
+ const isRequired = required.has(paramName);
80
+ const reqLabel = isRequired ? " (required)" : "";
81
+ dim(` - ${paramName}: ${prop?.type ?? "any"}${reqLabel}`);
82
+ }
83
+ }
84
+ }
85
+ newline();
86
+ }
87
+
88
+ dim("Run an action with: enact run <skill>:<action> --args '{...}'");
89
+ }
90
+
48
91
  /**
49
92
  * Learn command handler
50
93
  */
@@ -69,6 +112,13 @@ async function learnHandler(
69
112
  version: resolution.manifest.version,
70
113
  documentation: content,
71
114
  source: "local",
115
+ actions: resolution.actionsManifest
116
+ ? Object.entries(resolution.actionsManifest.actions).map(([name, a]) => ({
117
+ name,
118
+ description: a.description,
119
+ inputSchema: a.inputSchema,
120
+ }))
121
+ : null,
72
122
  });
73
123
  return;
74
124
  }
@@ -77,6 +127,11 @@ async function learnHandler(
77
127
  dim("(installed locally)");
78
128
  newline();
79
129
  console.log(content);
130
+
131
+ // Display actions if available
132
+ if (resolution.actionsManifest) {
133
+ displayActions(resolution.actionsManifest);
134
+ }
80
135
  return;
81
136
  }
82
137
 
@@ -87,6 +142,13 @@ async function learnHandler(
87
142
  version: resolution.manifest.version,
88
143
  documentation: resolution.manifest.doc ?? resolution.manifest.description ?? null,
89
144
  source: "local",
145
+ actions: resolution.actionsManifest
146
+ ? Object.entries(resolution.actionsManifest.actions).map(([name, a]) => ({
147
+ name,
148
+ description: a.description,
149
+ inputSchema: a.inputSchema,
150
+ }))
151
+ : null,
90
152
  });
91
153
  return;
92
154
  }
@@ -97,6 +159,11 @@ async function learnHandler(
97
159
  console.log(
98
160
  resolution.manifest.doc ?? resolution.manifest.description ?? "No documentation available."
99
161
  );
162
+
163
+ // Display actions if available
164
+ if (resolution.actionsManifest) {
165
+ displayActions(resolution.actionsManifest);
166
+ }
100
167
  return;
101
168
  }
102
169
 
@@ -158,27 +225,31 @@ async function learnHandler(
158
225
  const attestations = attestationsResponse.attestations;
159
226
 
160
227
  if (attestations.length === 0) {
161
- // No attestations found
162
- info(`${symbols.warning} Tool ${toolName}@${version} has no attestations.`);
228
+ // No attestations found — but if minimum_attestations is 0, that's fine
229
+ if (minimumAttestations === 0) {
230
+ // User explicitly configured zero required attestations — allow access
231
+ } else {
232
+ info(`${symbols.warning} Tool ${toolName}@${version} has no attestations.`);
163
233
 
164
- if (trustPolicy === "require_attestation") {
165
- throw new TrustError(
166
- "Trust policy requires attestations. Cannot display documentation from unverified tools."
167
- );
168
- }
169
- if (ctx.isInteractive && trustPolicy === "prompt") {
170
- dim("Documentation from unverified tools may contain malicious content.");
171
- const proceed = await confirm("View documentation from unverified tool?");
172
- if (!proceed) {
173
- info("Cancelled.");
174
- process.exit(0);
234
+ if (trustPolicy === "require_attestation") {
235
+ throw new TrustError(
236
+ "Trust policy requires attestations. Cannot display documentation from unverified tools."
237
+ );
238
+ }
239
+ if (ctx.isInteractive && trustPolicy === "prompt") {
240
+ dim("Documentation from unverified tools may contain malicious content.");
241
+ const proceed = await confirm("View documentation from unverified tool?");
242
+ if (!proceed) {
243
+ info("Cancelled.");
244
+ process.exit(0);
245
+ }
246
+ } else if (!ctx.isInteractive && trustPolicy === "prompt") {
247
+ throw new TrustError(
248
+ "Cannot display documentation from unverified tools in non-interactive mode."
249
+ );
175
250
  }
176
- } else if (!ctx.isInteractive && trustPolicy === "prompt") {
177
- throw new TrustError(
178
- "Cannot display documentation from unverified tools in non-interactive mode."
179
- );
180
251
  }
181
- // trustPolicy === "allow" - continue without prompting
252
+ // trustPolicy === "allow" or minimumAttestations === 0 - continue without prompting
182
253
  } else {
183
254
  // Verify attestations locally (never trust registry's verification status)
184
255
  const verifiedAuditors = await verifyAllAttestations(
@@ -5,7 +5,7 @@
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
11
  import {
@@ -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