@enactprotocol/cli 2.1.7 → 2.1.14

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 (55) hide show
  1. package/dist/commands/auth/index.js +1 -1
  2. package/dist/commands/auth/index.js.map +1 -1
  3. package/dist/commands/index.d.ts +2 -1
  4. package/dist/commands/index.d.ts.map +1 -1
  5. package/dist/commands/index.js +3 -1
  6. package/dist/commands/index.js.map +1 -1
  7. package/dist/commands/info/index.d.ts +11 -0
  8. package/dist/commands/info/index.d.ts.map +1 -0
  9. package/dist/commands/info/index.js +232 -0
  10. package/dist/commands/info/index.js.map +1 -0
  11. package/dist/commands/init/index.d.ts.map +1 -1
  12. package/dist/commands/init/index.js +17 -14
  13. package/dist/commands/init/index.js.map +1 -1
  14. package/dist/commands/learn/index.d.ts +4 -0
  15. package/dist/commands/learn/index.d.ts.map +1 -1
  16. package/dist/commands/learn/index.js +159 -5
  17. package/dist/commands/learn/index.js.map +1 -1
  18. package/dist/commands/mcp/index.d.ts +20 -0
  19. package/dist/commands/mcp/index.d.ts.map +1 -0
  20. package/dist/commands/mcp/index.js +460 -0
  21. package/dist/commands/mcp/index.js.map +1 -0
  22. package/dist/commands/publish/index.d.ts +2 -0
  23. package/dist/commands/publish/index.d.ts.map +1 -1
  24. package/dist/commands/publish/index.js +32 -8
  25. package/dist/commands/publish/index.js.map +1 -1
  26. package/dist/commands/sign/index.d.ts +2 -1
  27. package/dist/commands/sign/index.d.ts.map +1 -1
  28. package/dist/commands/sign/index.js +75 -17
  29. package/dist/commands/sign/index.js.map +1 -1
  30. package/dist/commands/visibility/index.d.ts +11 -0
  31. package/dist/commands/visibility/index.d.ts.map +1 -0
  32. package/dist/commands/visibility/index.js +117 -0
  33. package/dist/commands/visibility/index.js.map +1 -0
  34. package/dist/index.d.ts +1 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +5 -3
  37. package/dist/index.js.map +1 -1
  38. package/package.json +5 -5
  39. package/src/commands/auth/index.ts +1 -1
  40. package/src/commands/index.ts +4 -1
  41. package/src/commands/{get → info}/index.ts +103 -16
  42. package/src/commands/init/index.ts +17 -14
  43. package/src/commands/learn/index.ts +228 -5
  44. package/src/commands/publish/index.ts +37 -8
  45. package/src/commands/sign/index.ts +93 -18
  46. package/src/commands/visibility/index.ts +154 -0
  47. package/src/index.ts +7 -3
  48. package/tests/commands/{get.test.ts → info.test.ts} +35 -33
  49. package/tests/commands/init.test.ts +6 -6
  50. package/tests/commands/learn.test.ts +2 -2
  51. package/tests/commands/publish.test.ts +50 -0
  52. package/tests/commands/visibility.test.ts +156 -0
  53. package/tests/e2e.test.ts +1 -1
  54. package/tsconfig.tsbuildinfo +1 -1
  55. /package/tests/fixtures/echo-tool/{enact.md → SKILL.md} +0 -0
@@ -1,7 +1,7 @@
1
1
  /**
2
- * enact get command
2
+ * enact info command
3
3
  *
4
- * Show detailed information about a tool from the registry.
4
+ * Show detailed information about a tool from the registry or local path.
5
5
  */
6
6
 
7
7
  import {
@@ -11,7 +11,7 @@ import {
11
11
  getToolInfo,
12
12
  getToolVersion,
13
13
  } from "@enactprotocol/api";
14
- import { loadConfig } from "@enactprotocol/shared";
14
+ import { loadConfig, tryResolveTool } from "@enactprotocol/shared";
15
15
  import type { Command } from "commander";
16
16
  import type { CommandContext, GlobalOptions } from "../../types";
17
17
  import {
@@ -26,8 +26,9 @@ import {
26
26
  success,
27
27
  } from "../../utils";
28
28
 
29
- interface GetOptions extends GlobalOptions {
29
+ interface InfoOptions extends GlobalOptions {
30
30
  ver?: string;
31
+ local?: boolean;
31
32
  }
32
33
 
33
34
  /**
@@ -46,7 +47,7 @@ function formatDate(date: Date): string {
46
47
  */
47
48
  function displayToolInfo(
48
49
  tool: ToolInfo,
49
- options: GetOptions,
50
+ options: InfoOptions,
50
51
  rawManifest?: string | undefined
51
52
  ): void {
52
53
  header(tool.name);
@@ -83,7 +84,7 @@ function displayToolInfo(
83
84
  /**
84
85
  * Display version-specific info
85
86
  */
86
- function displayVersionInfo(version: ToolVersionInfo, options: GetOptions): void {
87
+ function displayVersionInfo(version: ToolVersionInfo, options: InfoOptions): void {
87
88
  header(`${version.name}@${version.version}`);
88
89
  newline();
89
90
 
@@ -119,13 +120,98 @@ function displayVersionInfo(version: ToolVersionInfo, options: GetOptions): void
119
120
  }
120
121
 
121
122
  /**
122
- * Get command handler
123
+ * Display local tool info
123
124
  */
124
- async function getHandler(
125
+ function displayLocalToolInfo(
126
+ name: string,
127
+ manifest: Record<string, unknown>,
128
+ manifestPath: string,
129
+ options: InfoOptions
130
+ ): void {
131
+ header(name);
132
+ dim("(local)");
133
+ newline();
134
+
135
+ if (manifest.description) {
136
+ info(String(manifest.description));
137
+ newline();
138
+ }
139
+
140
+ keyValue("Version", String(manifest.version ?? "unknown"));
141
+ if (manifest.license) {
142
+ keyValue("License", String(manifest.license));
143
+ }
144
+ if (manifest.command) {
145
+ keyValue("Command", String(manifest.command));
146
+ }
147
+
148
+ if (Array.isArray(manifest.tags) && manifest.tags.length > 0) {
149
+ keyValue("Tags", manifest.tags.join(", "));
150
+ }
151
+
152
+ if (Array.isArray(manifest.authors) && manifest.authors.length > 0) {
153
+ const authorNames = manifest.authors
154
+ .map((a: { name?: string }) => a.name ?? "unknown")
155
+ .join(", ");
156
+ keyValue("Authors", authorNames);
157
+ }
158
+
159
+ keyValue("Manifest", manifestPath);
160
+
161
+ // Show raw manifest when --verbose is used
162
+ if (options.verbose && manifestPath.endsWith(".md")) {
163
+ const { readFileSync } = require("node:fs");
164
+ const content = readFileSync(manifestPath, "utf-8");
165
+ newline();
166
+ header("Documentation");
167
+ newline();
168
+ console.log(content);
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Info command handler
174
+ */
175
+ async function infoHandler(
125
176
  toolName: string,
126
- options: GetOptions,
177
+ options: InfoOptions,
127
178
  ctx: CommandContext
128
179
  ): Promise<void> {
180
+ // First, try to resolve locally if it looks like a path
181
+ const resolution = tryResolveTool(toolName, { startDir: ctx.cwd });
182
+
183
+ if (resolution) {
184
+ // Tool found locally
185
+ if (options.json) {
186
+ json({
187
+ name: resolution.manifest.name ?? toolName,
188
+ version: resolution.manifest.version,
189
+ description: resolution.manifest.description,
190
+ command: resolution.manifest.command,
191
+ tags: resolution.manifest.tags,
192
+ authors: resolution.manifest.authors,
193
+ source: "local",
194
+ manifestPath: resolution.manifestPath,
195
+ });
196
+ return;
197
+ }
198
+
199
+ displayLocalToolInfo(
200
+ resolution.manifest.name ?? toolName,
201
+ resolution.manifest as unknown as Record<string, unknown>,
202
+ resolution.manifestPath,
203
+ options
204
+ );
205
+ return;
206
+ }
207
+
208
+ // If --local flag is set, don't fetch from registry
209
+ if (options.local) {
210
+ error(`Tool not found locally: ${toolName}`);
211
+ dim("The tool is not installed. Remove --local to fetch from registry.");
212
+ process.exit(1);
213
+ }
214
+
129
215
  const config = loadConfig();
130
216
  const registryUrl =
131
217
  process.env.ENACT_REGISTRY_URL ??
@@ -190,17 +276,18 @@ async function getHandler(
190
276
  }
191
277
 
192
278
  /**
193
- * Configure the get command
279
+ * Configure the info command
194
280
  */
195
- export function configureGetCommand(program: Command): void {
281
+ export function configureInfoCommand(program: Command): void {
196
282
  program
197
- .command("get <tool>")
198
- .alias("info")
199
- .description("Show detailed information about a tool")
283
+ .command("info <tool>")
284
+ .alias("get")
285
+ .description("Show detailed information about a tool (local path or registry)")
200
286
  .option("--ver <version>", "Show info for a specific version")
201
287
  .option("-v, --verbose", "Show detailed output")
288
+ .option("--local", "Only check locally installed tools")
202
289
  .option("--json", "Output as JSON")
203
- .action(async (toolName: string, options: GetOptions) => {
290
+ .action(async (toolName: string, options: InfoOptions) => {
204
291
  const ctx: CommandContext = {
205
292
  cwd: process.cwd(),
206
293
  options,
@@ -209,7 +296,7 @@ export function configureGetCommand(program: Command): void {
209
296
  };
210
297
 
211
298
  try {
212
- await getHandler(toolName, options, ctx);
299
+ await infoHandler(toolName, options, ctx);
213
300
  } catch (err) {
214
301
  error(formatError(err));
215
302
  process.exit(1);
@@ -26,7 +26,7 @@ const SUPABASE_ANON_KEY =
26
26
  * Embedded templates (for single-binary compatibility)
27
27
  */
28
28
  const TEMPLATES: Record<string, string> = {
29
- "tool-enact.md": `---
29
+ "tool-skill.md": `---
30
30
  name: {{TOOL_NAME}}
31
31
  description: A simple tool that echoes a greeting
32
32
  version: 0.1.0
@@ -74,7 +74,7 @@ Edit this file to create your own tool:
74
74
 
75
75
  "tool-agents.md": `# Enact Tool Development Guide
76
76
 
77
- Enact tools are containerized, cryptographically-signed executables. Each tool is defined by an \`enact.md\` file (YAML frontmatter + Markdown docs).
77
+ Enact tools are containerized, cryptographically-signed executables. Each tool is defined by a \`SKILL.md\` file (YAML frontmatter + Markdown docs).
78
78
 
79
79
  ## Quick Reference
80
80
 
@@ -85,7 +85,7 @@ Enact tools are containerized, cryptographically-signed executables. Each tool i
85
85
  | Dry run | \`enact run ./ --args '{}' --dry-run\` |
86
86
  | Sign & publish | \`enact sign ./ && enact publish ./\` |
87
87
 
88
- ## enact.md Structure
88
+ ## SKILL.md Structure
89
89
 
90
90
  \`\`\`yaml
91
91
  ---
@@ -225,7 +225,7 @@ Tools run in a container with \`/work\` as the working directory. All source fil
225
225
 
226
226
  ## Secrets
227
227
 
228
- Declare in \`enact.md\`:
228
+ Declare in \`SKILL.md\`:
229
229
  \`\`\`yaml
230
230
  env:
231
231
  API_KEY:
@@ -302,6 +302,7 @@ enact run ./path/to/tool --args '{}' # Run local tool
302
302
  \`\`\`bash
303
303
  enact search "pdf extraction" # Search registry
304
304
  enact get author/category/tool # View tool info
305
+ enact learn author/category/tool # View tool documentation
305
306
  enact install author/category/tool # Add to project (.enact/tools.json)
306
307
  enact install author/category/tool --global # Add globally
307
308
  enact list # List project tools
@@ -314,7 +315,7 @@ enact run tool --args '{}' | jq '.result'
314
315
  \`\`\`
315
316
 
316
317
  ## Creating Local Tools
317
- Create \`tools/<name>/enact.md\` with:
318
+ Create \`tools/<name>/SKILL.md\` with:
318
319
  \`\`\`yaml
319
320
  ---
320
321
  name: my-tool
@@ -345,6 +346,7 @@ This project uses Enact tools — containerized, signed executables you can run
345
346
  \`\`\`bash
346
347
  enact run <tool> --args '{"key": "value"}' # Run a tool
347
348
  enact search "keyword" # Find tools
349
+ enact learn author/tool # View tool documentation
348
350
  enact install author/tool # Install tool
349
351
  enact list # List installed tools
350
352
  \`\`\`
@@ -360,7 +362,7 @@ enact run tool --args '{}' | jq '.data'
360
362
  \`\`\`
361
363
 
362
364
  ## Creating Tools
363
- Create \`enact.md\` in a directory:
365
+ Create \`SKILL.md\` in a directory:
364
366
  \`\`\`yaml
365
367
  ---
366
368
  name: namespace/category/tool
@@ -394,7 +396,7 @@ enact sign ./ && enact publish ./ # Publish
394
396
  \`\`\`
395
397
 
396
398
  ## Secrets
397
- Declare in enact.md, set via CLI:
399
+ Declare in SKILL.md, set via CLI:
398
400
  \`\`\`yaml
399
401
  env:
400
402
  API_KEY: # Declared but not set
@@ -543,9 +545,9 @@ async function initHandler(options: InitOptions, ctx: CommandContext): Promise<v
543
545
  const isClaudeMode = options.claude;
544
546
  // Default to agent mode if no flag specified
545
547
 
546
- // Handle --tool mode: create enact.md + AGENTS.md for tool development
548
+ // Handle --tool mode: create SKILL.md + AGENTS.md for tool development
547
549
  if (isToolMode) {
548
- const manifestPath = join(targetDir, "enact.md");
550
+ const manifestPath = join(targetDir, "SKILL.md");
549
551
  const agentsPath = join(targetDir, "AGENTS.md");
550
552
 
551
553
  if (existsSync(manifestPath) && !options.force) {
@@ -571,7 +573,7 @@ async function initHandler(options: InitOptions, ctx: CommandContext): Promise<v
571
573
 
572
574
  // Load templates with placeholder replacement
573
575
  const replacements = { TOOL_NAME: toolName };
574
- const manifestContent = loadTemplate("tool-enact.md", replacements);
576
+ const manifestContent = loadTemplate("tool-skill.md", replacements);
575
577
  const agentsContent = loadTemplate("tool-agents.md", replacements);
576
578
 
577
579
  // Ensure directory exists
@@ -579,7 +581,7 @@ async function initHandler(options: InitOptions, ctx: CommandContext): Promise<v
579
581
  mkdirSync(targetDir, { recursive: true });
580
582
  }
581
583
 
582
- // Write enact.md
584
+ // Write SKILL.md
583
585
  writeFileSync(manifestPath, manifestContent, "utf-8");
584
586
  success(`Created tool manifest: ${manifestPath}`);
585
587
 
@@ -593,7 +595,7 @@ async function initHandler(options: InitOptions, ctx: CommandContext): Promise<v
593
595
 
594
596
  info("");
595
597
  info("Next steps:");
596
- info(" 1. Edit enact.md to customize your tool");
598
+ info(" 1. Edit SKILL.md to customize your tool");
597
599
  info(" 2. Run 'enact run ./' to test your tool");
598
600
  info(" 3. Run 'enact publish' to share your tool");
599
601
  return;
@@ -637,7 +639,8 @@ async function initHandler(options: InitOptions, ctx: CommandContext): Promise<v
637
639
 
638
640
  info("");
639
641
  info("This file helps AI agents understand how to use Enact tools in your project.");
640
- info("Run 'enact search <query>' to find tools, 'enact install <tool>' to add them.");
642
+ info("Run 'enact search <query>' to find tools, 'enact learn <tool>' to view docs,");
643
+ info("and 'enact install <tool>' to add them.");
641
644
  }
642
645
 
643
646
  /**
@@ -649,7 +652,7 @@ export function configureInitCommand(program: Command): void {
649
652
  .description("Initialize Enact in the current directory")
650
653
  .option("-n, --name <name>", "Tool name (default: username/my-tool)")
651
654
  .option("-f, --force", "Overwrite existing files")
652
- .option("--tool", "Create a new Enact tool (enact.md + AGENTS.md)")
655
+ .option("--tool", "Create a new Enact tool (SKILL.md + AGENTS.md)")
653
656
  .option("--agent", "Create AGENTS.md + .enact/tools.json (default)")
654
657
  .option("--claude", "Create CLAUDE.md + .enact/tools.json")
655
658
  .option("-v, --verbose", "Show detailed output")
@@ -3,16 +3,46 @@
3
3
  *
4
4
  * Display the documentation (enact.md) for a tool.
5
5
  * Fetches and displays the raw manifest content for easy reading.
6
+ *
7
+ * Security: For tools fetched from the registry, attestation checks are
8
+ * performed according to the trust policy. This prevents potentially
9
+ * malicious documentation from being displayed to LLMs or users.
6
10
  */
7
11
 
8
- import { createApiClient, getToolInfo, getToolVersion } from "@enactprotocol/api";
9
- import { loadConfig } from "@enactprotocol/shared";
12
+ import {
13
+ type AttestationListResponse,
14
+ createApiClient,
15
+ getAttestationList,
16
+ getToolInfo,
17
+ getToolVersion,
18
+ verifyAllAttestations,
19
+ } from "@enactprotocol/api";
20
+ import {
21
+ getMinimumAttestations,
22
+ getTrustPolicy,
23
+ getTrustedAuditors,
24
+ loadConfig,
25
+ tryResolveTool,
26
+ } from "@enactprotocol/shared";
10
27
  import type { Command } from "commander";
11
28
  import type { CommandContext, GlobalOptions } from "../../types";
12
- import { dim, error, formatError, header, json, newline } from "../../utils";
29
+ import {
30
+ TrustError,
31
+ confirm,
32
+ dim,
33
+ error,
34
+ formatError,
35
+ header,
36
+ info,
37
+ json,
38
+ newline,
39
+ success,
40
+ symbols,
41
+ } from "../../utils";
13
42
 
14
43
  interface LearnOptions extends GlobalOptions {
15
44
  ver?: string;
45
+ local?: boolean;
16
46
  }
17
47
 
18
48
  /**
@@ -21,14 +51,76 @@ interface LearnOptions extends GlobalOptions {
21
51
  async function learnHandler(
22
52
  toolName: string,
23
53
  options: LearnOptions,
24
- _ctx: CommandContext
54
+ ctx: CommandContext
25
55
  ): Promise<void> {
56
+ // First, try to resolve locally (project → user → cache)
57
+ // If the tool is already installed/cached, we trust it
58
+ const resolution = tryResolveTool(toolName, { startDir: ctx.cwd });
59
+
60
+ if (resolution) {
61
+ // Tool is installed locally - read documentation from the manifest file
62
+ if (resolution.manifestPath.endsWith(".md")) {
63
+ const { readFileSync } = await import("node:fs");
64
+ const content = readFileSync(resolution.manifestPath, "utf-8");
65
+
66
+ if (options.json) {
67
+ json({
68
+ name: toolName,
69
+ version: resolution.manifest.version,
70
+ documentation: content,
71
+ source: "local",
72
+ });
73
+ return;
74
+ }
75
+
76
+ header(`${toolName}@${resolution.manifest.version ?? "local"}`);
77
+ dim("(installed locally)");
78
+ newline();
79
+ console.log(content);
80
+ return;
81
+ }
82
+
83
+ // Fallback for non-.md manifests
84
+ if (options.json) {
85
+ json({
86
+ name: toolName,
87
+ version: resolution.manifest.version,
88
+ documentation: resolution.manifest.doc ?? resolution.manifest.description ?? null,
89
+ source: "local",
90
+ });
91
+ return;
92
+ }
93
+
94
+ header(`${toolName}@${resolution.manifest.version ?? "local"}`);
95
+ dim("(installed locally)");
96
+ newline();
97
+ console.log(
98
+ resolution.manifest.doc ?? resolution.manifest.description ?? "No documentation available."
99
+ );
100
+ return;
101
+ }
102
+
103
+ // If --local flag is set, don't fetch from registry
104
+ if (options.local) {
105
+ error(`Tool not found locally: ${toolName}`);
106
+ dim("The tool is not installed. Remove --local to fetch from registry.");
107
+ process.exit(1);
108
+ }
109
+
110
+ // Tool not installed - fetch from registry with attestation checks
26
111
  const config = loadConfig();
27
112
  const registryUrl =
28
113
  process.env.ENACT_REGISTRY_URL ??
29
114
  config.registry?.url ??
30
115
  "https://siikwkfgsmouioodghho.supabase.co/functions/v1";
31
- const authToken = config.registry?.authToken;
116
+
117
+ // Get auth token - use user token if available, otherwise use anon key for public access
118
+ let authToken = config.registry?.authToken ?? process.env.ENACT_AUTH_TOKEN;
119
+ if (!authToken && registryUrl.includes("siikwkfgsmouioodghho.supabase.co")) {
120
+ authToken =
121
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InNpaWt3a2Znc21vdWlvb2RnaGhvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjQ2MTkzMzksImV4cCI6MjA4MDE5NTMzOX0.kxnx6-IPFhmGx6rzNx36vbyhFMFZKP_jFqaDbKnJ_E0";
122
+ }
123
+
32
124
  const client = createApiClient({
33
125
  baseUrl: registryUrl,
34
126
  authToken: authToken,
@@ -42,14 +134,139 @@ async function learnHandler(
42
134
  version = toolInfo.latestVersion;
43
135
  }
44
136
 
137
+ if (!version) {
138
+ error(`No published versions for ${toolName}`);
139
+ process.exit(1);
140
+ }
141
+
45
142
  // Get the version info which includes rawManifest
46
143
  const versionInfo = await getToolVersion(client, toolName, version);
47
144
 
145
+ // ========================================
146
+ // TRUST VERIFICATION - same as run command
147
+ // ========================================
148
+ const trustPolicy = getTrustPolicy();
149
+ const minimumAttestations = getMinimumAttestations();
150
+ const trustedAuditors = getTrustedAuditors();
151
+
152
+ // Fetch attestations from registry
153
+ const attestationsResponse: AttestationListResponse = await getAttestationList(
154
+ client,
155
+ toolName,
156
+ version
157
+ );
158
+ const attestations = attestationsResponse.attestations;
159
+
160
+ if (attestations.length === 0) {
161
+ // No attestations found
162
+ info(`${symbols.warning} Tool ${toolName}@${version} has no attestations.`);
163
+
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);
175
+ }
176
+ } else if (!ctx.isInteractive && trustPolicy === "prompt") {
177
+ throw new TrustError(
178
+ "Cannot display documentation from unverified tools in non-interactive mode."
179
+ );
180
+ }
181
+ // trustPolicy === "allow" - continue without prompting
182
+ } else {
183
+ // Verify attestations locally (never trust registry's verification status)
184
+ const verifiedAuditors = await verifyAllAttestations(
185
+ client,
186
+ toolName,
187
+ version,
188
+ versionInfo.bundle.hash ?? ""
189
+ );
190
+
191
+ // Check verified auditors against trust config using provider:identity format
192
+ const trustedVerifiedAuditors = verifiedAuditors
193
+ .filter((auditor) => trustedAuditors.includes(auditor.providerIdentity))
194
+ .map((auditor) => auditor.providerIdentity);
195
+
196
+ if (trustedVerifiedAuditors.length > 0) {
197
+ // Check if we meet minimum attestations threshold
198
+ if (trustedVerifiedAuditors.length < minimumAttestations) {
199
+ info(
200
+ `${symbols.warning} Tool ${toolName}@${version} has ${trustedVerifiedAuditors.length} trusted attestation(s), but ${minimumAttestations} required.`
201
+ );
202
+ dim(`Trusted attestations: ${trustedVerifiedAuditors.join(", ")}`);
203
+
204
+ if (trustPolicy === "require_attestation") {
205
+ throw new TrustError(
206
+ `Trust policy requires at least ${minimumAttestations} attestation(s) from trusted identities.`
207
+ );
208
+ }
209
+ if (ctx.isInteractive && trustPolicy === "prompt") {
210
+ const proceed = await confirm(
211
+ "View documentation with fewer attestations than required?"
212
+ );
213
+ if (!proceed) {
214
+ info("Cancelled.");
215
+ process.exit(0);
216
+ }
217
+ } else if (!ctx.isInteractive && trustPolicy === "prompt") {
218
+ throw new TrustError(
219
+ "Cannot display documentation without meeting minimum attestation requirement in non-interactive mode."
220
+ );
221
+ }
222
+ // trustPolicy === "allow" - continue without prompting
223
+ } else {
224
+ // Tool meets or exceeds minimum attestations
225
+ if (options.verbose) {
226
+ success(
227
+ `Tool verified by ${trustedVerifiedAuditors.length} trusted identity(ies): ${trustedVerifiedAuditors.join(", ")}`
228
+ );
229
+ }
230
+ }
231
+ } else {
232
+ // Has attestations but none from trusted auditors
233
+ info(
234
+ `${symbols.warning} Tool ${toolName}@${version} has ${verifiedAuditors.length} attestation(s), but none from trusted auditors.`
235
+ );
236
+
237
+ if (trustPolicy === "require_attestation") {
238
+ dim(`Your trusted auditors: ${trustedAuditors.join(", ")}`);
239
+ dim(`Tool attested by: ${verifiedAuditors.map((a) => a.providerIdentity).join(", ")}`);
240
+ throw new TrustError(
241
+ "Trust policy requires attestations from trusted identities. Cannot display documentation."
242
+ );
243
+ }
244
+ if (ctx.isInteractive && trustPolicy === "prompt") {
245
+ dim(`Attested by: ${verifiedAuditors.map((a) => a.providerIdentity).join(", ")}`);
246
+ dim(`Your trusted auditors: ${trustedAuditors.join(", ")}`);
247
+ const proceed = await confirm("View documentation anyway?");
248
+ if (!proceed) {
249
+ info("Cancelled.");
250
+ process.exit(0);
251
+ }
252
+ } else if (!ctx.isInteractive && trustPolicy === "prompt") {
253
+ throw new TrustError(
254
+ "Cannot display documentation without trusted attestations in non-interactive mode."
255
+ );
256
+ }
257
+ // trustPolicy === "allow" - continue without prompting
258
+ }
259
+ }
260
+
261
+ // ========================================
262
+ // Display documentation (trust verified)
263
+ // ========================================
48
264
  if (options.json) {
49
265
  json({
50
266
  name: toolName,
51
267
  version: versionInfo.version,
52
268
  documentation: versionInfo.rawManifest ?? null,
269
+ source: "registry",
53
270
  });
54
271
  return;
55
272
  }
@@ -65,6 +282,10 @@ async function learnHandler(
65
282
  newline();
66
283
  console.log(versionInfo.rawManifest);
67
284
  } catch (err) {
285
+ if (err instanceof TrustError) {
286
+ error(err.message);
287
+ process.exit(1);
288
+ }
68
289
  if (err instanceof Error) {
69
290
  if (err.message.includes("not_found") || err.message.includes("404")) {
70
291
  error(`Tool not found: ${toolName}`);
@@ -88,7 +309,9 @@ export function configureLearnCommand(program: Command): void {
88
309
  .command("learn <tool>")
89
310
  .description("Display documentation (enact.md) for a tool")
90
311
  .option("--ver <version>", "Show documentation for a specific version")
312
+ .option("--local", "Only show documentation for locally installed tools")
91
313
  .option("--json", "Output as JSON")
314
+ .option("-v, --verbose", "Show detailed output")
92
315
  .action(async (toolName: string, options: LearnOptions) => {
93
316
  const ctx: CommandContext = {
94
317
  cwd: process.cwd(),