@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.
- package/dist/commands/auth/index.js +1 -1
- package/dist/commands/auth/index.js.map +1 -1
- package/dist/commands/index.d.ts +1 -1
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +1 -1
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/info/index.d.ts +11 -0
- package/dist/commands/info/index.d.ts.map +1 -0
- package/dist/commands/info/index.js +232 -0
- package/dist/commands/info/index.js.map +1 -0
- package/dist/commands/init/index.d.ts.map +1 -1
- package/dist/commands/init/index.js +92 -61
- package/dist/commands/init/index.js.map +1 -1
- package/dist/commands/learn/index.d.ts +4 -0
- package/dist/commands/learn/index.d.ts.map +1 -1
- package/dist/commands/learn/index.js +159 -5
- package/dist/commands/learn/index.js.map +1 -1
- package/dist/commands/mcp/index.d.ts +20 -0
- package/dist/commands/mcp/index.d.ts.map +1 -0
- package/dist/commands/mcp/index.js +460 -0
- package/dist/commands/mcp/index.js.map +1 -0
- package/dist/commands/publish/index.d.ts.map +1 -1
- package/dist/commands/publish/index.js +14 -7
- package/dist/commands/publish/index.js.map +1 -1
- package/dist/commands/sign/index.d.ts +2 -1
- package/dist/commands/sign/index.d.ts.map +1 -1
- package/dist/commands/sign/index.js +75 -17
- package/dist/commands/sign/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/commands/auth/index.ts +1 -1
- package/src/commands/index.ts +1 -1
- package/src/commands/{get → info}/index.ts +103 -16
- package/src/commands/init/index.ts +104 -65
- package/src/commands/learn/index.ts +228 -5
- package/src/commands/publish/index.ts +14 -7
- package/src/commands/sign/index.ts +93 -18
- package/src/index.ts +3 -3
- package/tests/commands/{get.test.ts → info.test.ts} +35 -33
- package/tests/commands/init.test.ts +204 -17
- package/tests/commands/learn.test.ts +2 -2
- package/tests/e2e.test.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
- /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-
|
|
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
|
|
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
|
-
##
|
|
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 \`
|
|
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>/
|
|
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 \`
|
|
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
|
|
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: --
|
|
518
|
-
const
|
|
543
|
+
// Determine mode: --tool, --claude, or --agent (default)
|
|
544
|
+
const isToolMode = options.tool;
|
|
519
545
|
const isClaudeMode = options.claude;
|
|
520
|
-
// Default to
|
|
546
|
+
// Default to agent mode if no flag specified
|
|
521
547
|
|
|
522
|
-
// Handle --
|
|
523
|
-
if (
|
|
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
|
-
|
|
526
|
-
|
|
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
|
-
|
|
531
|
-
|
|
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("
|
|
534
|
-
info("
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
564
|
-
|
|
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("
|
|
602
|
-
info("
|
|
603
|
-
info("
|
|
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 (
|
|
617
|
-
.option("--agent", "Create AGENTS.md
|
|
618
|
-
.option("--claude", "Create CLAUDE.md
|
|
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 {
|
|
9
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
303
|
-
const
|
|
304
|
-
if (
|
|
305
|
-
info("Found enact.md
|
|
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:
|
|
328
|
+
rawManifest: rawManifestContent,
|
|
322
329
|
});
|
|
323
330
|
});
|
|
324
331
|
|