@enactprotocol/cli 2.0.4 → 2.0.5

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.
@@ -32,6 +32,7 @@ interface SearchOptions extends GlobalOptions {
32
32
  tags?: string;
33
33
  limit?: string;
34
34
  offset?: string;
35
+ threshold?: string;
35
36
  local?: boolean;
36
37
  global?: boolean;
37
38
  }
@@ -229,7 +230,15 @@ async function searchHandler(
229
230
  process.env.ENACT_REGISTRY_URL ??
230
231
  config.registry?.url ??
231
232
  "https://siikwkfgsmouioodghho.supabase.co/functions/v1";
232
- const authToken = config.registry?.authToken;
233
+
234
+ // Get auth token - use user token if available, otherwise use anon key for public access
235
+ let authToken = config.registry?.authToken ?? process.env.ENACT_AUTH_TOKEN;
236
+ if (!authToken && registryUrl.includes("siikwkfgsmouioodghho.supabase.co")) {
237
+ // Use the official Supabase anon key for unauthenticated access
238
+ authToken =
239
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InNpaWt3a2Znc21vdWlvb2RnaGhvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjQ2MTkzMzksImV4cCI6MjA4MDE5NTMzOX0.kxnx6-IPFhmGx6rzNx36vbyhFMFZKP_jFqaDbKnJ_E0";
240
+ }
241
+
233
242
  const client = createApiClient({
234
243
  baseUrl: registryUrl,
235
244
  authToken: authToken,
@@ -237,12 +246,16 @@ async function searchHandler(
237
246
 
238
247
  const limit = options.limit ? Number.parseInt(options.limit, 10) : 20;
239
248
  const offset = options.offset ? Number.parseInt(options.offset, 10) : 0;
249
+ const threshold = options.threshold ? Number.parseFloat(options.threshold) : undefined;
240
250
 
241
251
  if (ctx.options.verbose) {
242
252
  info(`Searching for: "${query}"`);
243
253
  if (options.tags) {
244
254
  info(`Tags: ${options.tags}`);
245
255
  }
256
+ if (threshold !== undefined) {
257
+ info(`Similarity threshold: ${threshold}`);
258
+ }
246
259
  }
247
260
 
248
261
  try {
@@ -251,8 +264,16 @@ async function searchHandler(
251
264
  tags: options.tags,
252
265
  limit,
253
266
  offset,
267
+ threshold,
254
268
  });
255
269
 
270
+ // Show search type in verbose mode
271
+ if (ctx.options.verbose && response.searchType) {
272
+ const searchTypeLabel =
273
+ response.searchType === "hybrid" ? "semantic + text (hybrid)" : "text only (no OpenAI key)";
274
+ info(`Search mode: ${searchTypeLabel}`);
275
+ }
276
+
256
277
  // JSON output
257
278
  if (options.json) {
258
279
  json({
@@ -262,6 +283,7 @@ async function searchHandler(
262
283
  limit: response.limit,
263
284
  offset: response.offset,
264
285
  hasMore: response.hasMore,
286
+ searchType: response.searchType,
265
287
  });
266
288
  return;
267
289
  }
@@ -269,6 +291,9 @@ async function searchHandler(
269
291
  // No results
270
292
  if (response.results.length === 0) {
271
293
  info(`No tools found matching "${query}"`);
294
+ if (response.searchType === "text") {
295
+ dim("Note: Semantic search unavailable (OpenAI key not configured on server)");
296
+ }
272
297
  dim("Try a different search term or remove tag filters");
273
298
  return;
274
299
  }
@@ -348,6 +373,10 @@ export function configureSearchCommand(program: Command): void {
348
373
  .option("-t, --tags <tags>", "Filter by tags (comma-separated, registry only)")
349
374
  .option("-l, --limit <number>", "Maximum results to return (default: 20, registry only)")
350
375
  .option("-o, --offset <number>", "Pagination offset (default: 0, registry only)")
376
+ .option(
377
+ "--threshold <number>",
378
+ "Similarity threshold for semantic search (0.0-1.0, default: 0.1)"
379
+ )
351
380
  .option("-v, --verbose", "Show detailed output")
352
381
  .option("--json", "Output as JSON")
353
382
  .action(async (query: string, options: SearchOptions) => {
@@ -12,11 +12,7 @@
12
12
 
13
13
  import { readFileSync, writeFileSync } from "node:fs";
14
14
  import { dirname, join, resolve } from "node:path";
15
- import {
16
- createApiClient,
17
- getToolVersion,
18
- submitAttestation as submitAttestationToRegistry,
19
- } from "@enactprotocol/api";
15
+ import { createApiClient, getToolVersion, submitAttestationToRegistry } from "@enactprotocol/api";
20
16
  import { getSecret } from "@enactprotocol/secrets";
21
17
  import {
22
18
  addTrustedAuditor,
@@ -31,6 +27,7 @@ import {
31
27
  type EnactToolAttestationOptions,
32
28
  type SigstoreBundle,
33
29
  createEnactToolStatement,
30
+ extractCertificateFromBundle,
34
31
  signAttestation,
35
32
  } from "@enactprotocol/trust";
36
33
  import type { Command } from "commander";
@@ -160,7 +157,8 @@ function displayDryRun(
160
157
  */
161
158
  async function promptAddToTrustList(
162
159
  auditorEmail: string,
163
- isInteractive: boolean
160
+ isInteractive: boolean,
161
+ issuer?: string
164
162
  ): Promise<boolean> {
165
163
  if (!isInteractive) {
166
164
  return false;
@@ -168,7 +166,8 @@ async function promptAddToTrustList(
168
166
 
169
167
  try {
170
168
  // Convert email to provider:identity format (e.g., github:alice)
171
- const providerIdentity = emailToProviderIdentity(auditorEmail);
169
+ // Pass the issuer so we can correctly determine the provider
170
+ const providerIdentity = emailToProviderIdentity(auditorEmail, issuer);
172
171
 
173
172
  // Check if already in local trust list
174
173
  const trustedAuditors = getTrustedAuditors();
@@ -387,11 +386,12 @@ async function signRemoteTool(
387
386
  const attestationResult = await withSpinner(
388
387
  "Submitting attestation to registry...",
389
388
  async () => {
390
- return await submitAttestationToRegistry(client, {
391
- name: toolInfo.name,
392
- version: toolInfo.version,
393
- sigstoreBundle: result.bundle as unknown as Record<string, unknown>,
394
- });
389
+ return await submitAttestationToRegistry(
390
+ client,
391
+ toolInfo.name,
392
+ toolInfo.version,
393
+ result.bundle as unknown as Record<string, unknown>
394
+ );
395
395
  }
396
396
  );
397
397
 
@@ -402,9 +402,11 @@ async function signRemoteTool(
402
402
  keyValue("Rekor log index", String(attestationResult.rekorLogIndex));
403
403
  }
404
404
 
405
- // Prompt to add to trust list
405
+ // Prompt to add to trust list - extract issuer from bundle for correct identity format
406
406
  if (_ctx.isInteractive && !options.json) {
407
- await promptAddToTrustList(attestationResult.auditor, _ctx.isInteractive);
407
+ const certificate = extractCertificateFromBundle(result.bundle);
408
+ const issuer = certificate?.identity?.issuer;
409
+ await promptAddToTrustList(attestationResult.auditor, _ctx.isInteractive, issuer);
408
410
  }
409
411
 
410
412
  if (options.json) {
@@ -446,6 +448,28 @@ async function signLocalTool(
446
448
 
447
449
  const manifest = loaded.manifest;
448
450
 
451
+ // Warn about local signing workflow - attestation hash won't match published bundle
452
+ if (_ctx.isInteractive && !options.dryRun) {
453
+ newline();
454
+ warning("Local signing creates an attestation for the manifest content hash.");
455
+ dim("If you plan to publish this tool, the published bundle will have a different hash.");
456
+ dim("The attestation won't match and verification will fail.");
457
+ newline();
458
+ info("Recommended workflow:");
459
+ dim(` 1. ${colors.command(`enact publish ${pathArg}`)} # Publish first`);
460
+ dim(
461
+ ` 2. ${colors.command(`enact sign ${manifest.name}@${manifest.version ?? "1.0.0"}`)} # Then sign the published version`
462
+ );
463
+ newline();
464
+
465
+ const shouldContinue = await confirm("Continue with local signing anyway?", false);
466
+ if (!shouldContinue) {
467
+ info("Signing cancelled. Use the recommended workflow above.");
468
+ return;
469
+ }
470
+ newline();
471
+ }
472
+
449
473
  // Validate manifest
450
474
  const validation = validateManifest(manifest);
451
475
  if (!validation.valid && validation.errors) {
@@ -562,11 +586,12 @@ async function signLocalTool(
562
586
  "Submitting attestation to registry...",
563
587
  async () => {
564
588
  // Submit the Sigstore bundle directly (v2 API)
565
- return await submitAttestationToRegistry(client, {
566
- name: manifest.name,
567
- version: manifest.version ?? "1.0.0",
568
- sigstoreBundle: result.bundle as unknown as Record<string, unknown>,
569
- });
589
+ return await submitAttestationToRegistry(
590
+ client,
591
+ manifest.name,
592
+ manifest.version ?? "1.0.0",
593
+ result.bundle as unknown as Record<string, unknown>
594
+ );
570
595
  }
571
596
  );
572
597
 
@@ -576,8 +601,11 @@ async function signLocalTool(
576
601
  };
577
602
 
578
603
  // Prompt to add auditor to trust list (if interactive and not in JSON mode)
604
+ // Extract issuer from bundle for correct identity format
579
605
  if (!options.json && _ctx.isInteractive) {
580
- await promptAddToTrustList(attestationResult.auditor, _ctx.isInteractive);
606
+ const certificate = extractCertificateFromBundle(result.bundle);
607
+ const issuer = certificate?.identity?.issuer;
608
+ await promptAddToTrustList(attestationResult.auditor, _ctx.isInteractive, issuer);
581
609
  }
582
610
  } catch (err) {
583
611
  warning("Failed to submit attestation to registry");
package/src/index.ts CHANGED
@@ -32,7 +32,7 @@ import {
32
32
  } from "./commands";
33
33
  import { error, formatError } from "./utils";
34
34
 
35
- export const version = "2.0.4";
35
+ export const version = "2.0.5";
36
36
 
37
37
  // Export types for external use
38
38
  export type { GlobalOptions, CommandContext } from "./types";
@@ -47,9 +47,7 @@ async function main() {
47
47
  program
48
48
  .name("enact")
49
49
  .description("Enact - Verified, portable protocol for AI-executable tools")
50
- .version(version)
51
- .option("--json", "Output as JSON")
52
- .option("-v, --verbose", "Enable verbose output");
50
+ .version(version);
53
51
 
54
52
  // Configure all commands
55
53
  configureSetupCommand(program);
@@ -42,24 +42,26 @@ describe("get command", () => {
42
42
  expect(args[0]?.name()).toBe("tool");
43
43
  });
44
44
 
45
- test("has --version option", () => {
45
+ test("has --ver option for specifying version", () => {
46
46
  const program = new Command();
47
47
  configureGetCommand(program);
48
48
 
49
49
  const getCmd = program.commands.find((cmd) => cmd.name() === "get");
50
50
  const opts = getCmd?.options ?? [];
51
- const versionOpt = opts.find((o) => o.long === "--version");
52
- expect(versionOpt).toBeDefined();
51
+ const verOpt = opts.find((o) => o.long === "--ver");
52
+ expect(verOpt).toBeDefined();
53
53
  });
54
54
 
55
- test("has -v short option for version", () => {
55
+ test("has -v short option for verbose (not version)", () => {
56
56
  const program = new Command();
57
57
  configureGetCommand(program);
58
58
 
59
59
  const getCmd = program.commands.find((cmd) => cmd.name() === "get");
60
60
  const opts = getCmd?.options ?? [];
61
- const versionOpt = opts.find((o) => o.short === "-v");
62
- expect(versionOpt).toBeDefined();
61
+ // -v is for verbose, not version (--ver is for version)
62
+ const verboseOpt = opts.find((o) => o.short === "-v");
63
+ expect(verboseOpt).toBeDefined();
64
+ expect(verboseOpt?.long).toBe("--verbose");
63
65
  });
64
66
 
65
67
  test("has --json option", () => {
@@ -161,4 +161,63 @@ command: "echo hello"
161
161
  expect(predicateType).toContain("tool");
162
162
  });
163
163
  });
164
+
165
+ describe("remote tool reference parsing", () => {
166
+ test("parses simple remote tool reference with version", () => {
167
+ const ref = "alice/greeter@1.2.0";
168
+ const atIndex = ref.lastIndexOf("@");
169
+ const name = ref.slice(0, atIndex);
170
+ const version = ref.slice(atIndex + 1);
171
+
172
+ expect(name).toBe("alice/greeter");
173
+ expect(version).toBe("1.2.0");
174
+ });
175
+
176
+ test("parses namespaced tool reference with version", () => {
177
+ const ref = "org/utils/greeter@2.0.0";
178
+ const atIndex = ref.lastIndexOf("@");
179
+ const name = ref.slice(0, atIndex);
180
+ const version = ref.slice(atIndex + 1);
181
+
182
+ expect(name).toBe("org/utils/greeter");
183
+ expect(version).toBe("2.0.0");
184
+ });
185
+
186
+ test("detects remote vs local tool reference", () => {
187
+ const isRemoteToolRef = (path: string): boolean => {
188
+ // Remote refs contain @ for version and don't start with . or /
189
+ return (
190
+ !path.startsWith(".") && !path.startsWith("/") && path.includes("@") && path.includes("/")
191
+ );
192
+ };
193
+
194
+ expect(isRemoteToolRef("alice/greeter@1.0.0")).toBe(true);
195
+ expect(isRemoteToolRef("./local-tool")).toBe(false);
196
+ expect(isRemoteToolRef("/absolute/path/tool")).toBe(false);
197
+ expect(isRemoteToolRef("examples/hello-python")).toBe(false);
198
+ });
199
+
200
+ test("requires version in remote tool reference", () => {
201
+ const hasVersion = (ref: string): boolean => {
202
+ return ref.includes("@") && ref.lastIndexOf("@") > 0;
203
+ };
204
+
205
+ expect(hasVersion("alice/greeter@1.0.0")).toBe(true);
206
+ expect(hasVersion("alice/greeter")).toBe(false);
207
+ });
208
+
209
+ test("validates semver version format", () => {
210
+ const isValidSemver = (version: string): boolean => {
211
+ const semverRegex = /^\d+\.\d+\.\d+(-[\w.]+)?(\+[\w.]+)?$/;
212
+ return semverRegex.test(version);
213
+ };
214
+
215
+ expect(isValidSemver("1.0.0")).toBe(true);
216
+ expect(isValidSemver("2.1.3")).toBe(true);
217
+ expect(isValidSemver("1.0.0-alpha")).toBe(true);
218
+ expect(isValidSemver("1.0.0-beta.1")).toBe(true);
219
+ expect(isValidSemver("invalid")).toBe(false);
220
+ expect(isValidSemver("1.0")).toBe(false);
221
+ });
222
+ });
164
223
  });