akm-cli 0.0.21 → 0.0.23

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 (46) hide show
  1. package/README.md +8 -5
  2. package/dist/asset-spec.js +91 -10
  3. package/dist/cli.js +172 -57
  4. package/dist/common.js +15 -2
  5. package/dist/config-cli.js +55 -6
  6. package/dist/config.js +118 -22
  7. package/dist/create-provider-registry.js +18 -0
  8. package/dist/db.js +156 -53
  9. package/dist/embedder.js +36 -18
  10. package/dist/errors.js +6 -0
  11. package/dist/file-context.js +18 -19
  12. package/dist/frontmatter.js +19 -3
  13. package/dist/indexer.js +126 -89
  14. package/dist/{stash-registry.js → installed-kits.js} +16 -24
  15. package/dist/kit-include.js +108 -0
  16. package/dist/local-search.js +429 -0
  17. package/dist/lockfile.js +47 -5
  18. package/dist/matchers.js +6 -0
  19. package/dist/metadata.js +20 -10
  20. package/dist/paths.js +4 -0
  21. package/dist/providers/skills-sh.js +3 -2
  22. package/dist/providers/static-index.js +4 -9
  23. package/dist/registry-build-index.js +356 -0
  24. package/dist/registry-factory.js +19 -0
  25. package/dist/registry-install.js +114 -109
  26. package/dist/registry-resolve.js +44 -9
  27. package/dist/registry-search.js +14 -9
  28. package/dist/renderers.js +23 -7
  29. package/dist/ripgrep-install.js +9 -4
  30. package/dist/self-update.js +31 -4
  31. package/dist/stash-add.js +75 -6
  32. package/dist/stash-clone.js +1 -1
  33. package/dist/stash-provider-factory.js +37 -0
  34. package/dist/stash-provider.js +1 -0
  35. package/dist/stash-providers/filesystem.js +42 -0
  36. package/dist/stash-providers/index.js +9 -0
  37. package/dist/stash-providers/openviking.js +337 -0
  38. package/dist/stash-resolve.js +4 -4
  39. package/dist/stash-search.js +70 -401
  40. package/dist/stash-show.js +24 -5
  41. package/dist/stash-source-manage.js +82 -0
  42. package/dist/stash-source.js +19 -11
  43. package/dist/walker.js +15 -10
  44. package/dist/warn.js +7 -0
  45. package/package.json +1 -1
  46. package/dist/provider-registry.js +0 -8
@@ -2,9 +2,37 @@ import { spawnSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
- import { pathToFileURL } from "node:url";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
6
  import { fetchWithRetry } from "./common";
7
+ import { UsageError } from "./errors";
7
8
  import { asRecord, asString, GITHUB_API_BASE, githubHeaders } from "./github";
9
+ /**
10
+ * Validate that a URL is safe to pass to git.
11
+ * Allowlists https:, http:, ssh:, git: schemes and git@ SSH shorthand.
12
+ * Rejects git protocol helpers (ext::, fd::) that can execute arbitrary commands.
13
+ */
14
+ export function validateGitUrl(url) {
15
+ // git@ SSH shorthand: git@host:path
16
+ if (/^git@[^:]+:.+$/.test(url))
17
+ return;
18
+ let parsed;
19
+ try {
20
+ parsed = new URL(url);
21
+ }
22
+ catch {
23
+ throw new UsageError(`Invalid git URL: ${url}`);
24
+ }
25
+ const allowed = ["https:", "http:", "ssh:", "git:"];
26
+ if (!allowed.includes(parsed.protocol)) {
27
+ throw new UsageError(`Unsafe git URL scheme "${parsed.protocol}" in "${url}". Allowed: https, http, ssh, git, git@host:path`);
28
+ }
29
+ }
30
+ /** Validate that a git ref (branch/tag/commit) contains only safe characters. */
31
+ export function validateGitRef(ref) {
32
+ if (!/^[a-zA-Z0-9._\-/]+$/.test(ref)) {
33
+ throw new UsageError(`Unsafe git ref "${ref}": only alphanumerics, '.', '_', '-', '/' are allowed`);
34
+ }
35
+ }
8
36
  export function parseRegistryRef(rawRef) {
9
37
  const ref = rawRef.trim();
10
38
  if (!ref)
@@ -285,7 +313,10 @@ async function resolveGithubArtifact(parsed) {
285
313
  };
286
314
  }
287
315
  async function resolveGitArtifact(parsed) {
316
+ validateGitUrl(parsed.url);
288
317
  const ref = parsed.requestedRef ?? "HEAD";
318
+ if (parsed.requestedRef)
319
+ validateGitRef(parsed.requestedRef);
289
320
  const result = spawnSync("git", ["ls-remote", parsed.url, ref], { encoding: "utf8", timeout: 30_000 });
290
321
  let resolvedRevision;
291
322
  if (result.status === 0) {
@@ -370,19 +401,23 @@ function stripGitTransport(ref) {
370
401
  }
371
402
  /**
372
403
  * Convert a `file:` URI to a local filesystem path.
373
- * Supports `file:./relative`, `file:../relative`, and `file:///absolute`.
404
+ *
405
+ * Standard `file:///absolute` forms are handled by Node's `fileURLToPath`.
406
+ * Non-standard `file:./relative` and `file:../relative` shorthand forms
407
+ * (not a valid RFC 8089 URL) are handled with a custom fallback.
374
408
  */
375
409
  function fileUriToPath(ref) {
376
410
  const after = ref.slice(5); // strip "file:"
377
- // file:///absolute/path or file:///C:/path
378
- if (after.startsWith("///")) {
379
- return after.slice(2); // keep one leading /
380
- }
381
- // file://hostname/path (rare, treat hostname/path as absolute)
411
+ // Standard file:///absolute/path delegate to Node's implementation
382
412
  if (after.startsWith("//")) {
383
- return after.slice(1);
413
+ try {
414
+ return fileURLToPath(ref);
415
+ }
416
+ catch {
417
+ // Fall through to custom handling
418
+ }
384
419
  }
385
- // file:./relative or file:../relative or file:/absolute
420
+ // Non-standard file:./relative or file:../relative or file:/absolute
386
421
  return after;
387
422
  }
388
423
  /**
@@ -1,5 +1,6 @@
1
+ import { toErrorMessage } from "./common";
1
2
  import { DEFAULT_CONFIG, loadConfig } from "./config";
2
- import { resolveProviderFactory } from "./provider-registry";
3
+ import { resolveProviderFactory } from "./registry-factory";
3
4
  // ── Eagerly import providers to trigger self-registration ───────────────────
4
5
  import "./providers/static-index";
5
6
  import "./providers/skills-sh";
@@ -63,11 +64,18 @@ export function resolveRegistries(configRegistries) {
63
64
  // Allow env var override (comma-separated URLs) — CI escape hatch
64
65
  const envUrls = process.env.AKM_REGISTRY_URL?.trim();
65
66
  if (envUrls) {
66
- return envUrls
67
- .split(",")
68
- .map((u) => u.trim())
69
- .filter(Boolean)
70
- .map((url) => ({ url }));
67
+ const entries = [];
68
+ for (const raw of envUrls.split(",")) {
69
+ const url = raw.trim();
70
+ if (!url)
71
+ continue;
72
+ if (!url.startsWith("http://") && !url.startsWith("https://")) {
73
+ console.warn(`[agentikit] Ignoring AKM_REGISTRY_URL entry: must start with http:// or https://, got "${url}"`);
74
+ continue;
75
+ }
76
+ entries.push({ url });
77
+ }
78
+ return entries;
71
79
  }
72
80
  const registries = configRegistries ?? loadConfig().registries ?? DEFAULT_CONFIG.registries ?? [];
73
81
  return registries.filter((r) => r.enabled !== false);
@@ -89,6 +97,3 @@ function clampLimit(limit) {
89
97
  return 20;
90
98
  return Math.min(100, Math.max(1, Math.trunc(limit)));
91
99
  }
92
- function toErrorMessage(error) {
93
- return error instanceof Error ? error.message : String(error);
94
- }
package/dist/renderers.js CHANGED
@@ -54,7 +54,7 @@ export function extractCommentTags(filePath) {
54
54
  catch {
55
55
  return {};
56
56
  }
57
- const lines = content.split(/\r?\n/).slice(0, 50);
57
+ const lines = content.split(/\r?\n/, 50);
58
58
  const hints = {};
59
59
  for (const line of lines) {
60
60
  const trimmed = line.trim();
@@ -89,10 +89,11 @@ export function extractCommentTags(filePath) {
89
89
  export function detectExecHints(filePath) {
90
90
  const ext = path.extname(filePath).toLowerCase();
91
91
  const hints = {};
92
- // Interpreter from extension
92
+ // Interpreter from extension — use basename so the run command is portable
93
+ // relative to the stash root (callers set cwd to the file's directory).
93
94
  const interpreter = INTERPRETER_MAP[ext];
94
95
  if (interpreter) {
95
- hints.run = `${interpreter} ${filePath}`;
96
+ hints.run = `${interpreter} ${path.basename(filePath)}`;
96
97
  }
97
98
  // Setup from nearby dependency files
98
99
  const dir = path.dirname(filePath);
@@ -206,7 +207,7 @@ const commandMdRenderer = {
206
207
  action: "Fill $ARGUMENTS placeholders in the template, then dispatch",
207
208
  description: toStringOrUndefined(parsedMd.data.description),
208
209
  template,
209
- modelHint: parsedMd.data.model,
210
+ modelHint: typeof parsedMd.data.model === "string" ? parsedMd.data.model : undefined,
210
211
  agent: toStringOrUndefined(parsedMd.data.agent),
211
212
  parameters: extractParameters(template),
212
213
  };
@@ -226,7 +227,7 @@ const agentMdRenderer = {
226
227
  description: toStringOrUndefined(parsedMd.data.description),
227
228
  prompt: parsedMd.content,
228
229
  toolPolicy: parsedMd.data.tools,
229
- modelHint: parsedMd.data.model,
230
+ modelHint: typeof parsedMd.data.model === "string" ? parsedMd.data.model : undefined,
230
231
  };
231
232
  },
232
233
  };
@@ -308,7 +309,21 @@ const knowledgeMdRenderer = {
308
309
  }
309
310
  },
310
311
  };
311
- // ── 5. script-source ─────────────────────────────────────────────────────────
312
+ // ── 5. memory-md ─────────────────────────────────────────────────────────────
313
+ const memoryMdRenderer = {
314
+ name: "memory-md",
315
+ buildShowResponse(ctx) {
316
+ const name = deriveName(ctx);
317
+ return {
318
+ type: "memory",
319
+ name,
320
+ path: ctx.absPath,
321
+ action: "Recall context — read the content below",
322
+ content: ctx.content(),
323
+ };
324
+ },
325
+ };
326
+ // ── 6. script-source ─────────────────────────────────────────────────────────
312
327
  const scriptSourceRenderer = {
313
328
  name: "script-source",
314
329
  buildShowResponse(ctx) {
@@ -371,6 +386,7 @@ const builtinRenderers = [
371
386
  commandMdRenderer,
372
387
  agentMdRenderer,
373
388
  knowledgeMdRenderer,
389
+ memoryMdRenderer,
374
390
  scriptSourceRenderer,
375
391
  ];
376
392
  /**
@@ -383,4 +399,4 @@ export function registerBuiltinRenderers() {
383
399
  }
384
400
  }
385
401
  // ── Named exports for testing ────────────────────────────────────────────────
386
- export { skillMdRenderer, commandMdRenderer, agentMdRenderer, knowledgeMdRenderer, scriptSourceRenderer, INTERPRETER_MAP, SETUP_SIGNALS, };
402
+ export { skillMdRenderer, commandMdRenderer, agentMdRenderer, knowledgeMdRenderer, memoryMdRenderer, scriptSourceRenderer, INTERPRETER_MAP, SETUP_SIGNALS, };
@@ -118,9 +118,13 @@ function downloadAndExtractZip(url, archiveName, destBinary) {
118
118
  if (dlResult.status !== 0) {
119
119
  throw new Error(dlResult.stderr?.trim() || "download failed");
120
120
  }
121
- // Extract the zip archive using separate spawnSync calls with argument arrays
122
- // to avoid shell injection via path interpolation in PowerShell -Command strings
123
- const expandResult = spawnSync("powershell", ["-Command", "Expand-Archive", "-Path", tmpZip, "-DestinationPath", destDir, "-Force"], {
121
+ // Extract the zip archive. Use a single-string -Command with quoted paths to
122
+ // prevent PowerShell from treating subsequent array elements as separate
123
+ // arguments to the interpreter itself (PowerShell -Command arg1 arg2 ... would
124
+ // concatenate them with spaces, causing unexpected evaluation on paths with
125
+ // backticks or semicolons).
126
+ const expandCmd = `Expand-Archive -Path '${tmpZip.replace(/'/g, "''")}' -DestinationPath '${destDir.replace(/'/g, "''")}' -Force`;
127
+ const expandResult = spawnSync("powershell", ["-NonInteractive", "-NoProfile", "-Command", expandCmd], {
124
128
  encoding: "utf8",
125
129
  timeout: 60_000,
126
130
  env: process.env,
@@ -129,7 +133,8 @@ function downloadAndExtractZip(url, archiveName, destBinary) {
129
133
  throw new Error(expandResult.stderr?.trim() || "extraction failed");
130
134
  }
131
135
  const srcRgExe = path.join(destDir, archiveName, "rg.exe");
132
- const moveResult = spawnSync("powershell", ["-Command", "Move-Item", "-Force", "-Path", srcRgExe, "-Destination", destBinary], {
136
+ const moveCmd = `Move-Item -Force -Path '${srcRgExe.replace(/'/g, "''")}' -Destination '${destBinary.replace(/'/g, "''")}'`;
137
+ const moveResult = spawnSync("powershell", ["-NonInteractive", "-NoProfile", "-Command", moveCmd], {
133
138
  encoding: "utf8",
134
139
  timeout: 60_000,
135
140
  env: process.env,
@@ -91,11 +91,21 @@ export async function performUpgrade(check, opts) {
91
91
  throw new Error(`Failed to download binary: ${binaryResponse.status} ${binaryResponse.statusText}`);
92
92
  }
93
93
  const binaryData = new Uint8Array(await binaryResponse.arrayBuffer());
94
- // Download and verify checksum
94
+ // Download and verify checksum (mandatory — upgrade is blocked if checksums cannot be fetched)
95
95
  let checksumVerified = false;
96
+ const skipChecksum = opts?.skipChecksum === true;
96
97
  try {
97
98
  const checksumsResponse = await fetchWithRetry(checksumsUrl);
98
- if (checksumsResponse.ok) {
99
+ if (!checksumsResponse.ok) {
100
+ if (skipChecksum) {
101
+ console.warn(`WARNING: checksums.txt fetch failed (HTTP ${checksumsResponse.status}). Proceeding without verification because --skip-checksum was provided.`);
102
+ }
103
+ else {
104
+ throw new Error(`Checksum verification failed: could not fetch ${checksumsUrl} (HTTP ${checksumsResponse.status}). ` +
105
+ `Use --skip-checksum to bypass (not recommended).`);
106
+ }
107
+ }
108
+ else {
99
109
  const checksumsText = await checksumsResponse.text();
100
110
  const expectedHash = parseChecksumForFile(checksumsText, binaryName);
101
111
  if (expectedHash) {
@@ -105,13 +115,30 @@ export async function performUpgrade(check, opts) {
105
115
  }
106
116
  checksumVerified = true;
107
117
  }
118
+ else {
119
+ if (skipChecksum) {
120
+ console.warn(`WARNING: ${binaryName} not found in checksums.txt. Proceeding without verification because --skip-checksum was provided.`);
121
+ }
122
+ else {
123
+ throw new Error(`Checksum verification failed: ${binaryName} not listed in checksums.txt. ` +
124
+ `Use --skip-checksum to bypass (not recommended).`);
125
+ }
126
+ }
108
127
  }
109
128
  }
110
129
  catch (err) {
111
- if (err instanceof Error && err.message.includes("Checksum mismatch")) {
130
+ if (err instanceof Error &&
131
+ (err.message.includes("Checksum mismatch") || err.message.includes("Checksum verification failed"))) {
112
132
  throw err;
113
133
  }
114
- // Non-fatal: checksum file missing or unparseable
134
+ // Network or parse failure
135
+ if (skipChecksum) {
136
+ console.warn(`WARNING: Could not fetch or parse checksums: ${err instanceof Error ? err.message : String(err)}. Proceeding because --skip-checksum was provided.`);
137
+ }
138
+ else {
139
+ throw new Error(`Checksum verification failed: ${err instanceof Error ? err.message : String(err)}. ` +
140
+ `Use --skip-checksum to bypass (not recommended).`);
141
+ }
115
142
  }
116
143
  const execPath = process.execPath;
117
144
  const execDir = path.dirname(execPath);
package/dist/stash-add.js CHANGED
@@ -1,15 +1,77 @@
1
1
  import fs from "node:fs";
2
+ import path from "node:path";
2
3
  import { resolveStashDir } from "./common";
3
- import { loadConfig } from "./config";
4
+ import { loadConfig, saveConfig } from "./config";
4
5
  import { UsageError } from "./errors";
5
6
  import { agentikitIndex } from "./indexer";
6
7
  import { upsertLockEntry } from "./lockfile";
7
- import { installRegistryRef, upsertInstalledRegistryEntry } from "./registry-install";
8
+ import { detectStashRoot, installRegistryRef, upsertInstalledRegistryEntry } from "./registry-install";
9
+ import { parseRegistryRef } from "./registry-resolve";
8
10
  export async function agentikitAdd(input) {
9
11
  const ref = input.ref.trim();
10
12
  if (!ref)
11
13
  throw new UsageError("Install ref or local directory is required.");
12
14
  const stashDir = resolveStashDir();
15
+ // Detect local directory refs and route them to stashes[] instead of installed[]
16
+ try {
17
+ const parsed = parseRegistryRef(ref);
18
+ if (parsed.source === "local") {
19
+ return addLocalStashSource(ref, parsed.sourcePath, stashDir);
20
+ }
21
+ }
22
+ catch {
23
+ // Not a local ref — fall through to registry install
24
+ }
25
+ return addRegistryKit(ref, stashDir);
26
+ }
27
+ /**
28
+ * Add a local directory as a filesystem stash source.
29
+ * Creates a stashes[] entry instead of an installed[] entry.
30
+ */
31
+ async function addLocalStashSource(ref, sourcePath, stashDir) {
32
+ const stashRoot = detectStashRoot(sourcePath);
33
+ const resolvedPath = path.resolve(stashRoot);
34
+ const config = loadConfig();
35
+ // Check for duplicates in stashes[]
36
+ const stashes = [...(config.stashes ?? [])];
37
+ const existing = stashes.find((s) => s.type === "filesystem" && s.path && path.resolve(s.path) === resolvedPath);
38
+ if (!existing) {
39
+ const entry = {
40
+ type: "filesystem",
41
+ path: resolvedPath,
42
+ name: toReadableId(resolvedPath),
43
+ };
44
+ stashes.push(entry);
45
+ saveConfig({ ...config, stashes });
46
+ }
47
+ const index = await agentikitIndex({ stashDir });
48
+ const updatedConfig = loadConfig();
49
+ return {
50
+ schemaVersion: 1,
51
+ stashDir,
52
+ ref,
53
+ stashSource: {
54
+ type: "filesystem",
55
+ path: resolvedPath,
56
+ name: toReadableId(resolvedPath),
57
+ stashRoot: resolvedPath,
58
+ },
59
+ config: {
60
+ searchPaths: updatedConfig.searchPaths,
61
+ installedKitCount: updatedConfig.installed?.length ?? 0,
62
+ },
63
+ index: {
64
+ mode: index.mode,
65
+ totalEntries: index.totalEntries,
66
+ directoriesScanned: index.directoriesScanned,
67
+ directoriesSkipped: index.directoriesSkipped,
68
+ },
69
+ };
70
+ }
71
+ /**
72
+ * Install a kit from a registry (npm, github, git).
73
+ */
74
+ async function addRegistryKit(ref, stashDir) {
13
75
  const installed = await installRegistryRef(ref);
14
76
  const replaced = (loadConfig().installed ?? []).find((entry) => entry.id === installed.id);
15
77
  const config = upsertInstalledRegistryEntry({
@@ -23,16 +85,16 @@ export async function agentikitAdd(input) {
23
85
  cacheDir: installed.cacheDir,
24
86
  installedAt: installed.installedAt,
25
87
  });
26
- upsertLockEntry({
88
+ await upsertLockEntry({
27
89
  id: installed.id,
28
90
  source: installed.source,
29
91
  ref: installed.ref,
30
92
  resolvedVersion: installed.resolvedVersion,
31
93
  resolvedRevision: installed.resolvedRevision,
32
- integrity: installed.integrity ?? (installed.source === "local" ? "local" : undefined),
94
+ integrity: installed.integrity,
33
95
  });
34
- // Clean up old cache directory on re-install (skip for local sources — no cache to clean)
35
- if (replaced && replaced.source !== "local" && replaced.cacheDir !== installed.cacheDir) {
96
+ // Clean up old cache directory on re-install
97
+ if (replaced && replaced.cacheDir !== installed.cacheDir) {
36
98
  try {
37
99
  fs.rmSync(replaced.cacheDir, { recursive: true, force: true });
38
100
  }
@@ -69,3 +131,10 @@ export async function agentikitAdd(input) {
69
131
  },
70
132
  };
71
133
  }
134
+ function toReadableId(resolvedPath) {
135
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
136
+ if (home && resolvedPath.startsWith(home + path.sep)) {
137
+ return `~${resolvedPath.slice(home.length)}`;
138
+ }
139
+ return resolvedPath;
140
+ }
@@ -47,7 +47,7 @@ export async function agentikitClone(options) {
47
47
  let lastError;
48
48
  for (const source of searchSources) {
49
49
  try {
50
- sourcePath = resolveAssetPath(source.path, parsed.type, parsed.name);
50
+ sourcePath = await resolveAssetPath(source.path, parsed.type, parsed.name);
51
51
  break;
52
52
  }
53
53
  catch (err) {
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Stash provider factory map.
3
+ *
4
+ * Maps stash source type identifiers (e.g. "filesystem", "openviking") to
5
+ * factory functions that create StashProvider instances from a StashConfigEntry.
6
+ *
7
+ * "Stash providers" are runtime data sources for the search and show commands —
8
+ * distinct from the kit-discovery registries (registry-factory.ts) and the
9
+ * installed-kit operations (installed-kits.ts).
10
+ */
11
+ import { createProviderRegistry } from "./create-provider-registry";
12
+ // ── Factory map ─────────────────────────────────────────────────────────────
13
+ const registry = createProviderRegistry();
14
+ export function registerStashProvider(type, factory) {
15
+ registry.register(type, factory);
16
+ }
17
+ export function resolveStashProviderFactory(type) {
18
+ return registry.resolve(type);
19
+ }
20
+ /**
21
+ * Resolve all non-filesystem stash providers from config.
22
+ * Filesystem entries are excluded — they are handled by resolveStashSources().
23
+ */
24
+ export function resolveStashProviders(config) {
25
+ const providers = [];
26
+ for (const entry of config.stashes ?? []) {
27
+ if (entry.enabled === false)
28
+ continue;
29
+ if (entry.type === "filesystem")
30
+ continue;
31
+ const factory = registry.resolve(entry.type);
32
+ if (factory) {
33
+ providers.push(factory(entry));
34
+ }
35
+ }
36
+ return providers;
37
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,42 @@
1
+ import { resolveStashDir } from "../common";
2
+ import { loadConfig } from "../config";
3
+ import { searchLocal } from "../local-search";
4
+ import { registerStashProvider } from "../stash-provider-factory";
5
+ import { showLocal } from "../stash-show";
6
+ import { resolveStashSources } from "../stash-source";
7
+ class FilesystemStashProvider {
8
+ type = "filesystem";
9
+ name;
10
+ stashDir;
11
+ config;
12
+ constructor(entry) {
13
+ this.config = loadConfig();
14
+ this.stashDir = entry.path ?? resolveStashDir();
15
+ this.name = entry.name ?? this.stashDir;
16
+ }
17
+ async search(options) {
18
+ const sources = resolveStashSources(this.stashDir, this.config);
19
+ const result = await searchLocal({
20
+ query: options.query.toLowerCase(),
21
+ searchType: options.type ?? "any",
22
+ limit: options.limit,
23
+ stashDir: this.stashDir,
24
+ sources,
25
+ config: this.config,
26
+ });
27
+ return {
28
+ hits: result.hits,
29
+ warnings: result.warnings,
30
+ embedMs: result.embedMs,
31
+ rankMs: result.rankMs,
32
+ };
33
+ }
34
+ async show(ref, view) {
35
+ return showLocal({ ref, view });
36
+ }
37
+ canShow(ref) {
38
+ return !ref.trim().startsWith("viking://");
39
+ }
40
+ }
41
+ // ── Self-register ───────────────────────────────────────────────────────────
42
+ registerStashProvider("filesystem", (config) => new FilesystemStashProvider(config));
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Centralized stash provider registration.
3
+ *
4
+ * Import this module (side-effect import) to register all built-in stash
5
+ * providers with the provider registry. This replaces the individual
6
+ * side-effect imports that were duplicated in stash-search.ts and stash-show.ts.
7
+ */
8
+ import "./filesystem";
9
+ import "./openviking";