@enactprotocol/cli 2.1.6 → 2.1.10

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 (47) 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 +1 -1
  4. package/dist/commands/index.d.ts.map +1 -1
  5. package/dist/commands/index.js +1 -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 +92 -61
  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.map +1 -1
  23. package/dist/commands/publish/index.js +14 -7
  24. package/dist/commands/publish/index.js.map +1 -1
  25. package/dist/commands/sign/index.d.ts +2 -1
  26. package/dist/commands/sign/index.d.ts.map +1 -1
  27. package/dist/commands/sign/index.js +75 -17
  28. package/dist/commands/sign/index.js.map +1 -1
  29. package/dist/index.d.ts +1 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +4 -4
  32. package/dist/index.js.map +1 -1
  33. package/package.json +5 -5
  34. package/src/commands/auth/index.ts +1 -1
  35. package/src/commands/index.ts +1 -1
  36. package/src/commands/{get → info}/index.ts +103 -16
  37. package/src/commands/init/index.ts +104 -65
  38. package/src/commands/learn/index.ts +228 -5
  39. package/src/commands/publish/index.ts +14 -7
  40. package/src/commands/sign/index.ts +93 -18
  41. package/src/index.ts +3 -3
  42. package/tests/commands/{get.test.ts → info.test.ts} +35 -33
  43. package/tests/commands/init.test.ts +204 -17
  44. package/tests/commands/learn.test.ts +2 -2
  45. package/tests/e2e.test.ts +1 -1
  46. package/tsconfig.tsbuildinfo +1 -1
  47. /package/tests/fixtures/echo-tool/{enact.md → SKILL.md} +0 -0
@@ -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
@@ -430,6 +432,30 @@ function loadTemplate(templateName: string, replacements: Record<string, string>
430
432
  return content;
431
433
  }
432
434
 
435
+ /**
436
+ * Create .enact/tools.json for project tool tracking
437
+ */
438
+ function createEnactProjectDir(targetDir: string, force: boolean): boolean {
439
+ const enactDir = join(targetDir, ".enact");
440
+ const toolsJsonPath = join(enactDir, "tools.json");
441
+
442
+ // Check if tools.json already exists
443
+ if (existsSync(toolsJsonPath) && !force) {
444
+ info(".enact/tools.json already exists, skipping");
445
+ return false;
446
+ }
447
+
448
+ // Create .enact directory if it doesn't exist
449
+ if (!existsSync(enactDir)) {
450
+ mkdirSync(enactDir, { recursive: true });
451
+ }
452
+
453
+ // Write empty tools.json
454
+ const toolsJson = { tools: {} };
455
+ writeFileSync(toolsJsonPath, `${JSON.stringify(toolsJson, null, 2)}\n`, "utf-8");
456
+ return true;
457
+ }
458
+
433
459
  /**
434
460
  * Get the current logged-in username
435
461
  */
@@ -514,24 +540,64 @@ async function getCurrentUsername(): Promise<string | null> {
514
540
  async function initHandler(options: InitOptions, ctx: CommandContext): Promise<void> {
515
541
  const targetDir = ctx.cwd;
516
542
 
517
- // Determine mode: --agent, --claude, or --tool (default)
518
- const isAgentMode = options.agent;
543
+ // Determine mode: --tool, --claude, or --agent (default)
544
+ const isToolMode = options.tool;
519
545
  const isClaudeMode = options.claude;
520
- // Default to tool mode if no flag specified
546
+ // Default to agent mode if no flag specified
521
547
 
522
- // Handle --agent mode: create AGENTS.md for projects using Enact tools
523
- if (isAgentMode) {
548
+ // Handle --tool mode: create SKILL.md + AGENTS.md for tool development
549
+ if (isToolMode) {
550
+ const manifestPath = join(targetDir, "SKILL.md");
524
551
  const agentsPath = join(targetDir, "AGENTS.md");
525
- if (existsSync(agentsPath) && !options.force) {
526
- warning(`AGENTS.md already exists at: ${agentsPath}`);
552
+
553
+ if (existsSync(manifestPath) && !options.force) {
554
+ warning(`Tool manifest already exists at: ${manifestPath}`);
527
555
  info("Use --force to overwrite");
528
556
  return;
529
557
  }
530
- writeFileSync(agentsPath, loadTemplate("agent-agents.md"), "utf-8");
531
- success(`Created AGENTS.md: ${agentsPath}`);
558
+
559
+ // Get username for the tool name
560
+ let toolName = options.name;
561
+
562
+ if (!toolName) {
563
+ const username = await getCurrentUsername();
564
+ if (username) {
565
+ toolName = `${username}/my-tool`;
566
+ info(`Using logged-in username: ${username}`);
567
+ } else {
568
+ toolName = "my-tool";
569
+ info("Not logged in - using generic tool name");
570
+ info("Run 'enact auth login' to use your username in tool names");
571
+ }
572
+ }
573
+
574
+ // Load templates with placeholder replacement
575
+ const replacements = { TOOL_NAME: toolName };
576
+ const manifestContent = loadTemplate("tool-skill.md", replacements);
577
+ const agentsContent = loadTemplate("tool-agents.md", replacements);
578
+
579
+ // Ensure directory exists
580
+ if (!existsSync(targetDir)) {
581
+ mkdirSync(targetDir, { recursive: true });
582
+ }
583
+
584
+ // Write SKILL.md
585
+ writeFileSync(manifestPath, manifestContent, "utf-8");
586
+ success(`Created tool manifest: ${manifestPath}`);
587
+
588
+ // Write AGENTS.md (only if it doesn't exist or --force is used)
589
+ if (!existsSync(agentsPath) || options.force) {
590
+ writeFileSync(agentsPath, agentsContent, "utf-8");
591
+ success(`Created AGENTS.md: ${agentsPath}`);
592
+ } else {
593
+ info("AGENTS.md already exists, skipping (use --force to overwrite)");
594
+ }
595
+
532
596
  info("");
533
- info("This file helps AI agents understand how to use Enact tools in your project.");
534
- info("Run 'enact search <query>' to find tools, 'enact install <tool>' to add them.");
597
+ info("Next steps:");
598
+ info(" 1. Edit SKILL.md to customize your tool");
599
+ info(" 2. Run 'enact run ./' to test your tool");
600
+ info(" 3. Run 'enact publish' to share your tool");
535
601
  return;
536
602
  }
537
603
 
@@ -545,63 +611,36 @@ async function initHandler(options: InitOptions, ctx: CommandContext): Promise<v
545
611
  }
546
612
  writeFileSync(claudePath, loadTemplate("claude.md"), "utf-8");
547
613
  success(`Created CLAUDE.md: ${claudePath}`);
614
+
615
+ // Create .enact/tools.json
616
+ if (createEnactProjectDir(targetDir, options.force ?? false)) {
617
+ success("Created .enact/tools.json");
618
+ }
619
+
548
620
  info("");
549
621
  info("This file helps Claude understand how to use Enact tools in your project.");
550
622
  return;
551
623
  }
552
624
 
553
- // Handle --tool mode (default): create enact.md + AGENTS.md for tool development
554
- const manifestPath = join(targetDir, "enact.md");
625
+ // Handle default (agent) mode: create AGENTS.md for projects using Enact tools
555
626
  const agentsPath = join(targetDir, "AGENTS.md");
556
-
557
- if (existsSync(manifestPath) && !options.force) {
558
- warning(`Tool manifest already exists at: ${manifestPath}`);
627
+ if (existsSync(agentsPath) && !options.force) {
628
+ warning(`AGENTS.md already exists at: ${agentsPath}`);
559
629
  info("Use --force to overwrite");
560
630
  return;
561
631
  }
632
+ writeFileSync(agentsPath, loadTemplate("agent-agents.md"), "utf-8");
633
+ success(`Created AGENTS.md: ${agentsPath}`);
562
634
 
563
- // Get username for the tool name
564
- let toolName = options.name;
565
-
566
- if (!toolName) {
567
- const username = await getCurrentUsername();
568
- if (username) {
569
- toolName = `${username}/my-tool`;
570
- info(`Using logged-in username: ${username}`);
571
- } else {
572
- toolName = "my-tool";
573
- info("Not logged in - using generic tool name");
574
- info("Run 'enact auth login' to use your username in tool names");
575
- }
576
- }
577
-
578
- // Load templates with placeholder replacement
579
- const replacements = { TOOL_NAME: toolName };
580
- const manifestContent = loadTemplate("tool-enact.md", replacements);
581
- const agentsContent = loadTemplate("tool-agents.md", replacements);
582
-
583
- // Ensure directory exists
584
- if (!existsSync(targetDir)) {
585
- mkdirSync(targetDir, { recursive: true });
586
- }
587
-
588
- // Write enact.md
589
- writeFileSync(manifestPath, manifestContent, "utf-8");
590
- success(`Created tool manifest: ${manifestPath}`);
591
-
592
- // Write AGENTS.md (only if it doesn't exist or --force is used)
593
- if (!existsSync(agentsPath) || options.force) {
594
- writeFileSync(agentsPath, agentsContent, "utf-8");
595
- success(`Created AGENTS.md: ${agentsPath}`);
596
- } else {
597
- info("AGENTS.md already exists, skipping (use --force to overwrite)");
635
+ // Create .enact/tools.json
636
+ if (createEnactProjectDir(targetDir, options.force ?? false)) {
637
+ success("Created .enact/tools.json");
598
638
  }
599
639
 
600
640
  info("");
601
- info("Next steps:");
602
- info(" 1. Edit enact.md to customize your tool");
603
- info(" 2. Run 'enact run ./' to test your tool");
604
- info(" 3. Run 'enact publish' to share your tool");
641
+ info("This file helps AI agents understand how to use Enact tools in your project.");
642
+ info("Run 'enact search <query>' to find tools, 'enact learn <tool>' to view docs,");
643
+ info("and 'enact install <tool>' to add them.");
605
644
  }
606
645
 
607
646
  /**
@@ -613,9 +652,9 @@ export function configureInitCommand(program: Command): void {
613
652
  .description("Initialize Enact in the current directory")
614
653
  .option("-n, --name <name>", "Tool name (default: username/my-tool)")
615
654
  .option("-f, --force", "Overwrite existing files")
616
- .option("--tool", "Create a new Enact tool (default)")
617
- .option("--agent", "Create AGENTS.md for projects that use Enact tools")
618
- .option("--claude", "Create CLAUDE.md with Claude-specific instructions")
655
+ .option("--tool", "Create a new Enact tool (SKILL.md + AGENTS.md)")
656
+ .option("--agent", "Create AGENTS.md + .enact/tools.json (default)")
657
+ .option("--claude", "Create CLAUDE.md + .enact/tools.json")
619
658
  .option("-v, --verbose", "Show detailed output")
620
659
  .action(async (options: InitOptions) => {
621
660
  const ctx: CommandContext = {
@@ -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(),
@@ -130,9 +130,16 @@ async function createBundleFromDir(toolDir: string): Promise<Uint8Array> {
130
130
  }
131
131
 
132
132
  /**
133
- * Load the raw enact.md file content (full documentation with frontmatter)
133
+ * Load the raw markdown manifest file content (full documentation with frontmatter)
134
+ * Checks for SKILL.md first (preferred), then falls back to enact.md
134
135
  */
135
- function loadEnactMd(toolDir: string): string | undefined {
136
+ function loadRawManifest(toolDir: string): string | undefined {
137
+ // Check SKILL.md first (preferred format)
138
+ const skillMdPath = join(toolDir, "SKILL.md");
139
+ if (existsSync(skillMdPath)) {
140
+ return readFileSync(skillMdPath, "utf-8");
141
+ }
142
+ // Fall back to enact.md
136
143
  const enactMdPath = join(toolDir, "enact.md");
137
144
  if (existsSync(enactMdPath)) {
138
145
  return readFileSync(enactMdPath, "utf-8");
@@ -299,10 +306,10 @@ async function publishHandler(
299
306
  return;
300
307
  }
301
308
 
302
- // Load the full enact.md content (frontmatter + documentation)
303
- const enactMdContent = loadEnactMd(toolDir);
304
- if (enactMdContent) {
305
- info("Found enact.md documentation");
309
+ // Load the full markdown manifest content (SKILL.md or enact.md)
310
+ const rawManifestContent = loadRawManifest(toolDir);
311
+ if (rawManifestContent) {
312
+ info("Found markdown documentation (SKILL.md or enact.md)");
306
313
  }
307
314
 
308
315
  // Create bundle
@@ -318,7 +325,7 @@ async function publishHandler(
318
325
  name: toolName,
319
326
  manifest: manifest as unknown as Record<string, unknown>,
320
327
  bundle,
321
- rawManifest: enactMdContent,
328
+ rawManifest: rawManifestContent,
322
329
  });
323
330
  });
324
331