agentikit 0.0.7 → 0.0.8

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 (80) hide show
  1. package/README.md +113 -77
  2. package/dist/index.d.ts +13 -3
  3. package/dist/index.js +7 -2
  4. package/dist/src/asset-spec.d.ts +14 -0
  5. package/dist/src/asset-spec.js +46 -0
  6. package/dist/src/cli.js +154 -52
  7. package/dist/src/common.d.ts +8 -0
  8. package/dist/src/common.js +46 -0
  9. package/dist/src/config.d.ts +31 -0
  10. package/dist/src/config.js +74 -0
  11. package/dist/src/embedder.d.ts +10 -0
  12. package/dist/src/embedder.js +87 -0
  13. package/dist/src/frontmatter.d.ts +30 -0
  14. package/dist/src/frontmatter.js +86 -0
  15. package/dist/src/indexer.d.ts +20 -2
  16. package/dist/src/indexer.js +212 -80
  17. package/dist/src/init.d.ts +19 -0
  18. package/dist/src/init.js +87 -0
  19. package/dist/src/llm.d.ts +15 -0
  20. package/dist/src/llm.js +91 -0
  21. package/dist/src/markdown.d.ts +18 -0
  22. package/dist/src/markdown.js +77 -0
  23. package/dist/src/metadata.d.ts +10 -2
  24. package/dist/src/metadata.js +146 -30
  25. package/dist/src/ripgrep-install.d.ts +12 -0
  26. package/dist/src/ripgrep-install.js +169 -0
  27. package/dist/src/ripgrep-resolve.d.ts +13 -0
  28. package/dist/src/ripgrep-resolve.js +68 -0
  29. package/dist/src/ripgrep.d.ts +3 -36
  30. package/dist/src/ripgrep.js +2 -262
  31. package/dist/src/similarity.d.ts +1 -2
  32. package/dist/src/similarity.js +11 -0
  33. package/dist/src/stash-ref.d.ts +7 -0
  34. package/dist/src/stash-ref.js +33 -0
  35. package/dist/src/stash-resolve.d.ts +2 -0
  36. package/dist/src/stash-resolve.js +45 -0
  37. package/dist/src/stash-search.d.ts +6 -0
  38. package/dist/src/stash-search.js +269 -0
  39. package/dist/src/stash-show.d.ts +5 -0
  40. package/dist/src/stash-show.js +107 -0
  41. package/dist/src/stash-types.d.ts +53 -0
  42. package/dist/src/stash-types.js +1 -0
  43. package/dist/src/stash.d.ts +8 -63
  44. package/dist/src/stash.js +4 -633
  45. package/dist/src/tool-runner.d.ts +35 -0
  46. package/dist/src/tool-runner.js +100 -0
  47. package/dist/src/walker.d.ts +19 -0
  48. package/dist/src/walker.js +47 -0
  49. package/package.json +8 -14
  50. package/src/asset-spec.ts +69 -0
  51. package/src/cli.ts +164 -48
  52. package/src/common.ts +58 -0
  53. package/src/config.ts +124 -0
  54. package/src/embedder.ts +117 -0
  55. package/src/frontmatter.ts +95 -0
  56. package/src/indexer.ts +244 -84
  57. package/src/init.ts +106 -0
  58. package/src/llm.ts +124 -0
  59. package/src/markdown.ts +106 -0
  60. package/src/metadata.ts +157 -29
  61. package/src/ripgrep-install.ts +200 -0
  62. package/src/ripgrep-resolve.ts +72 -0
  63. package/src/ripgrep.ts +3 -315
  64. package/src/similarity.ts +13 -1
  65. package/src/stash-ref.ts +41 -0
  66. package/src/stash-resolve.ts +47 -0
  67. package/src/stash-search.ts +343 -0
  68. package/src/stash-show.ts +104 -0
  69. package/src/stash-types.ts +46 -0
  70. package/src/stash.ts +16 -760
  71. package/src/tool-runner.ts +129 -0
  72. package/src/walker.ts +53 -0
  73. package/.claude-plugin/plugin.json +0 -21
  74. package/commands/open.md +0 -11
  75. package/commands/run.md +0 -11
  76. package/commands/search.md +0 -11
  77. package/dist/src/plugin.d.ts +0 -2
  78. package/dist/src/plugin.js +0 -55
  79. package/skills/stash/SKILL.md +0 -73
  80. package/src/plugin.ts +0 -56
@@ -1,262 +1,2 @@
1
- import { spawnSync } from "node:child_process";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- // ── ripgrep Resolution ──────────────────────────────────────────────────────
5
- const IS_WINDOWS = process.platform === "win32";
6
- const RG_BINARY = IS_WINDOWS ? "rg.exe" : "rg";
7
- function canExecute(filePath) {
8
- if (!fs.existsSync(filePath))
9
- return false;
10
- if (IS_WINDOWS)
11
- return true;
12
- try {
13
- fs.accessSync(filePath, fs.constants.X_OK);
14
- return true;
15
- }
16
- catch {
17
- return false;
18
- }
19
- }
20
- function resolveFromPath() {
21
- const rawPath = process.env.PATH;
22
- if (!rawPath)
23
- return null;
24
- const pathEntries = rawPath.split(path.delimiter).filter(Boolean);
25
- if (IS_WINDOWS) {
26
- const pathext = (process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM")
27
- .split(";")
28
- .filter(Boolean)
29
- .map((ext) => ext.toLowerCase());
30
- for (const entry of pathEntries) {
31
- const directCandidate = path.join(entry, "rg");
32
- if (canExecute(directCandidate))
33
- return directCandidate;
34
- for (const ext of pathext) {
35
- const candidate = path.join(entry, `rg${ext}`);
36
- if (canExecute(candidate))
37
- return candidate;
38
- }
39
- }
40
- return null;
41
- }
42
- for (const entry of pathEntries) {
43
- const candidate = path.join(entry, "rg");
44
- if (canExecute(candidate))
45
- return candidate;
46
- }
47
- return null;
48
- }
49
- /**
50
- * Resolve the path to a usable ripgrep binary.
51
- * Checks in order:
52
- * 1. stashDir/bin/rg
53
- * 2. system PATH (rg)
54
- * Returns null if ripgrep is not available.
55
- */
56
- export function resolveRg(stashDir) {
57
- // Check stash bin directory first
58
- if (stashDir) {
59
- const stashRg = path.join(stashDir, "bin", RG_BINARY);
60
- if (canExecute(stashRg))
61
- return stashRg;
62
- }
63
- return resolveFromPath();
64
- }
65
- /**
66
- * Check if ripgrep is available (either in stash/bin or system PATH).
67
- */
68
- export function isRgAvailable(stashDir) {
69
- return resolveRg(stashDir) !== null;
70
- }
71
- /**
72
- * Use ripgrep to find .stash.json files that match query tokens.
73
- * Returns paths to matching .stash.json files.
74
- *
75
- * If ripgrep is not available or the query is empty, returns null
76
- * to signal that the caller should skip pre-filtering.
77
- */
78
- export function rgFilterCandidates(query, searchDir, stashDir) {
79
- if (!query.trim())
80
- return null;
81
- const rgPath = resolveRg(stashDir);
82
- if (!rgPath)
83
- return null;
84
- // Tokenize the query into an OR pattern for ripgrep
85
- const tokens = query
86
- .toLowerCase()
87
- .replace(/[^a-z0-9\s]/g, " ")
88
- .split(/\s+/)
89
- .filter((t) => t.length > 1);
90
- if (tokens.length === 0)
91
- return null;
92
- const pattern = tokens.join("|");
93
- const result = spawnSync(rgPath, [
94
- "-i", // case insensitive
95
- "-l", // files-with-matches only
96
- "--hidden", // include hidden files such as .stash.json
97
- "--no-ignore", // include ignored files to ensure metadata is searchable
98
- "--glob", ".stash.json", // only search .stash.json files
99
- pattern,
100
- searchDir,
101
- ], {
102
- encoding: "utf8",
103
- timeout: 10_000,
104
- });
105
- if (result.status !== 0 && result.status !== 1) {
106
- // rg exit code 1 = no matches (normal), anything else = error
107
- return null;
108
- }
109
- const files = (result.stdout || "")
110
- .trim()
111
- .split(/\r?\n/)
112
- .filter((f) => f.length > 0);
113
- return { matchedFiles: files, usedRg: true };
114
- }
115
- // ── ripgrep Installation ────────────────────────────────────────────────────
116
- /**
117
- * Platform and architecture detection for ripgrep binary downloads.
118
- */
119
- function getRgPlatformTarget() {
120
- const platform = process.platform;
121
- const arch = process.arch;
122
- if (platform === "linux" && arch === "x64") {
123
- return { platform: "x86_64-unknown-linux-musl", arch: "x64", ext: ".tar.gz" };
124
- }
125
- if (platform === "linux" && arch === "arm64") {
126
- return { platform: "aarch64-unknown-linux-gnu", arch: "arm64", ext: ".tar.gz" };
127
- }
128
- if (platform === "darwin" && arch === "x64") {
129
- return { platform: "x86_64-apple-darwin", arch: "x64", ext: ".tar.gz" };
130
- }
131
- if (platform === "darwin" && arch === "arm64") {
132
- return { platform: "aarch64-apple-darwin", arch: "arm64", ext: ".tar.gz" };
133
- }
134
- if (platform === "win32" && arch === "x64") {
135
- return { platform: "x86_64-pc-windows-msvc", arch: "x64", ext: ".zip" };
136
- }
137
- return null;
138
- }
139
- const RG_VERSION = "14.1.1";
140
- /**
141
- * Ensure ripgrep is available. If not found on PATH or in stash/bin,
142
- * download and install it to stash/bin.
143
- *
144
- * Returns the path to the ripgrep binary and whether it was newly installed.
145
- */
146
- export function ensureRg(stashDir) {
147
- // Already available?
148
- const existing = resolveRg(stashDir);
149
- if (existing) {
150
- return { rgPath: existing, installed: false, version: getRgVersion(existing) };
151
- }
152
- // Determine platform
153
- const target = getRgPlatformTarget();
154
- if (!target) {
155
- throw new Error(`Unsupported platform for ripgrep auto-install: ${process.platform}/${process.arch}. ` +
156
- `Install ripgrep manually: https://github.com/BurntSushi/ripgrep#installation`);
157
- }
158
- const binDir = path.join(stashDir, "bin");
159
- if (!fs.existsSync(binDir)) {
160
- fs.mkdirSync(binDir, { recursive: true });
161
- }
162
- const archiveName = `ripgrep-${RG_VERSION}-${target.platform}`;
163
- const url = `https://github.com/BurntSushi/ripgrep/releases/download/${RG_VERSION}/${archiveName}${target.ext}`;
164
- const destBinary = path.join(binDir, RG_BINARY);
165
- if (target.ext === ".tar.gz") {
166
- downloadAndExtractTarGz(url, archiveName, destBinary);
167
- }
168
- else {
169
- downloadAndExtractZip(url, archiveName, destBinary);
170
- }
171
- // Make executable
172
- if (!IS_WINDOWS) {
173
- fs.chmodSync(destBinary, 0o755);
174
- }
175
- return { rgPath: destBinary, installed: true, version: RG_VERSION };
176
- }
177
- function downloadAndExtractTarGz(url, archiveName, destBinary) {
178
- const destDir = path.dirname(destBinary);
179
- const tmpTarGz = path.join(destDir, "rg-download.tar.gz");
180
- try {
181
- // Download archive to a temporary file without using a shell
182
- const curlResult = spawnSync("curl", ["-fsSL", "-o", tmpTarGz, url], {
183
- encoding: "utf8",
184
- timeout: 60_000,
185
- });
186
- if (curlResult.status !== 0) {
187
- const err = curlResult.stderr?.trim() || curlResult.error?.message || "unknown error";
188
- throw new Error(`Failed to download ripgrep from ${url}: ${err}`);
189
- }
190
- // Extract the specific binary from the archive into destDir
191
- const tarResult = spawnSync("tar", [
192
- "xzf",
193
- tmpTarGz,
194
- "--strip-components=1",
195
- "-C",
196
- destDir,
197
- `${archiveName}/rg`,
198
- ], {
199
- encoding: "utf8",
200
- timeout: 60_000,
201
- });
202
- if (tarResult.status !== 0) {
203
- const err = tarResult.stderr?.trim() || tarResult.error?.message || "unknown error";
204
- throw new Error(`Failed to extract ripgrep from ${url}: ${err}`);
205
- }
206
- if (!fs.existsSync(destBinary)) {
207
- throw new Error(`ripgrep binary not found at ${destBinary} after extraction`);
208
- }
209
- }
210
- finally {
211
- // Best-effort cleanup of temporary archive
212
- try {
213
- if (fs.existsSync(tmpTarGz)) {
214
- fs.unlinkSync(tmpTarGz);
215
- }
216
- }
217
- catch {
218
- // ignore cleanup errors
219
- }
220
- }
221
- }
222
- function downloadAndExtractZip(url, archiveName, destBinary) {
223
- const destDir = path.dirname(destBinary);
224
- const tmpZip = path.join(destDir, "rg-download.zip");
225
- const expandedDir = path.join(destDir, archiveName);
226
- try {
227
- // Download
228
- const dlResult = spawnSync("curl", ["-fsSL", "-o", tmpZip, url], {
229
- encoding: "utf8",
230
- timeout: 60_000,
231
- });
232
- if (dlResult.status !== 0) {
233
- throw new Error(dlResult.stderr?.trim() || "download failed");
234
- }
235
- // Extract just the rg.exe
236
- const extractResult = spawnSync("powershell", [
237
- "-Command",
238
- `Expand-Archive -Path "${tmpZip}" -DestinationPath "${destDir}" -Force; ` +
239
- `Move-Item -Force "${path.join(destDir, archiveName, "rg.exe")}" "${destBinary}"`,
240
- ], {
241
- encoding: "utf8",
242
- timeout: 60_000,
243
- });
244
- if (extractResult.status !== 0) {
245
- throw new Error(extractResult.stderr?.trim() || "extraction failed");
246
- }
247
- }
248
- finally {
249
- if (fs.existsSync(tmpZip))
250
- fs.unlinkSync(tmpZip);
251
- if (fs.existsSync(expandedDir))
252
- fs.rmSync(expandedDir, { recursive: true, force: true });
253
- }
254
- }
255
- function getRgVersion(rgPath) {
256
- const result = spawnSync(rgPath, ["--version"], { encoding: "utf8", timeout: 5_000 });
257
- if (result.status === 0 && result.stdout) {
258
- const match = result.stdout.match(/ripgrep\s+([\d.]+)/);
259
- return match ? match[1] : "unknown";
260
- }
261
- return "unknown";
262
- }
1
+ export { resolveRg, isRgAvailable } from "./ripgrep-resolve";
2
+ export { ensureRg } from "./ripgrep-install";
@@ -14,7 +14,7 @@ export interface SearchAdapter {
14
14
  buildIndex(entries: ScoredEntry[]): void;
15
15
  search(query: string, limit: number, typeFilter?: string): ScoredResult[];
16
16
  }
17
- interface SerializedTfIdf {
17
+ export interface SerializedTfIdf {
18
18
  idf: Record<string, number>;
19
19
  docs: Array<{
20
20
  id: string;
@@ -32,4 +32,3 @@ export declare class TfIdfAdapter implements SearchAdapter {
32
32
  static deserialize(data: SerializedTfIdf, entries: ScoredEntry[]): TfIdfAdapter;
33
33
  private substringFallback;
34
34
  }
35
- export {};
@@ -94,6 +94,17 @@ export class TfIdfAdapter {
94
94
  score += 0.15;
95
95
  }
96
96
  }
97
+ // Boost: intent phrase contains query token
98
+ const intents = doc.entry.entry.intents || [];
99
+ for (const intent of intents) {
100
+ const intentLower = intent.toLowerCase();
101
+ for (const token of queryTokens) {
102
+ if (intentLower.includes(token)) {
103
+ score += 0.12;
104
+ break; // one boost per intent phrase
105
+ }
106
+ }
107
+ }
97
108
  // Boost: name contains query token
98
109
  const nameLower = doc.entry.entry.name.toLowerCase().replace(/[-_]/g, " ");
99
110
  for (const token of queryTokens) {
@@ -0,0 +1,7 @@
1
+ import { type AgentikitAssetType } from "./common";
2
+ export interface OpenRef {
3
+ type: AgentikitAssetType;
4
+ name: string;
5
+ }
6
+ export declare function parseOpenRef(ref: string): OpenRef;
7
+ export declare function makeOpenRef(type: AgentikitAssetType, name: string): string;
@@ -0,0 +1,33 @@
1
+ import path from "node:path";
2
+ import { isAssetType } from "./common";
3
+ export function parseOpenRef(ref) {
4
+ const separator = ref.indexOf(":");
5
+ if (separator <= 0) {
6
+ throw new Error("Invalid open ref. Expected format '<type>:<name>'.");
7
+ }
8
+ const rawType = ref.slice(0, separator);
9
+ const rawName = ref.slice(separator + 1);
10
+ if (!isAssetType(rawType)) {
11
+ throw new Error(`Invalid open ref type: "${rawType}".`);
12
+ }
13
+ let name;
14
+ try {
15
+ name = decodeURIComponent(rawName);
16
+ }
17
+ catch {
18
+ throw new Error("Invalid open ref encoding.");
19
+ }
20
+ const normalized = path.posix.normalize(name.replace(/\\/g, "/"));
21
+ if (!name
22
+ || name.includes("\0")
23
+ || /^[A-Za-z]:/.test(name)
24
+ || path.posix.isAbsolute(normalized)
25
+ || normalized === ".."
26
+ || normalized.startsWith("../")) {
27
+ throw new Error("Invalid open ref name.");
28
+ }
29
+ return { type: rawType, name: normalized };
30
+ }
31
+ export function makeOpenRef(type, name) {
32
+ return `${type}:${encodeURIComponent(name)}`;
33
+ }
@@ -0,0 +1,2 @@
1
+ import { type AgentikitAssetType } from "./common";
2
+ export declare function resolveAssetPath(stashDir: string, type: AgentikitAssetType, name: string): string;
@@ -0,0 +1,45 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { hasErrnoCode, isWithin } from "./common";
4
+ import { TYPE_DIRS, isRelevantAssetFile, resolveAssetPathFromName } from "./asset-spec";
5
+ export function resolveAssetPath(stashDir, type, name) {
6
+ const root = path.join(stashDir, TYPE_DIRS[type]);
7
+ const target = resolveAssetPathFromName(type, root, name);
8
+ const resolvedRoot = resolveAndValidateTypeRoot(root, type, name);
9
+ const resolvedTarget = path.resolve(target);
10
+ if (!isWithin(resolvedTarget, resolvedRoot)) {
11
+ throw new Error("Ref resolves outside the stash root.");
12
+ }
13
+ if (!fs.existsSync(resolvedTarget) || !fs.statSync(resolvedTarget).isFile()) {
14
+ throw new Error(`Stash asset not found for ref: ${type}:${name}`);
15
+ }
16
+ const realTarget = fs.realpathSync(resolvedTarget);
17
+ if (!isWithin(realTarget, resolvedRoot)) {
18
+ throw new Error("Ref resolves outside the stash root.");
19
+ }
20
+ if (!isRelevantAssetFile(type, path.basename(resolvedTarget))) {
21
+ if (type === "tool") {
22
+ throw new Error("Tool ref must resolve to a .sh, .ts, .js, .ps1, .cmd, or .bat file.");
23
+ }
24
+ throw new Error(`Stash asset not found for ref: ${type}:${name}`);
25
+ }
26
+ return realTarget;
27
+ }
28
+ function resolveAndValidateTypeRoot(root, type, name) {
29
+ const rootStat = readTypeRootStat(root, type, name);
30
+ if (!rootStat.isDirectory()) {
31
+ throw new Error(`Stash type root is not a directory for ref: ${type}:${name}`);
32
+ }
33
+ return fs.realpathSync(root);
34
+ }
35
+ function readTypeRootStat(root, type, name) {
36
+ try {
37
+ return fs.statSync(root);
38
+ }
39
+ catch (error) {
40
+ if (hasErrnoCode(error, "ENOENT")) {
41
+ throw new Error(`Stash type root not found for ref: ${type}:${name}`);
42
+ }
43
+ throw error;
44
+ }
45
+ }
@@ -0,0 +1,6 @@
1
+ import type { AgentikitSearchType, SearchResponse } from "./stash-types";
2
+ export declare function agentikitSearch(input: {
3
+ query: string;
4
+ type?: AgentikitSearchType;
5
+ limit?: number;
6
+ }): Promise<SearchResponse>;
@@ -0,0 +1,269 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { hasErrnoCode, resolveStashDir } from "./common";
4
+ import { ASSET_TYPES, TYPE_DIRS, deriveCanonicalAssetName } from "./asset-spec";
5
+ import { loadSearchIndex, buildSearchText } from "./indexer";
6
+ import { TfIdfAdapter } from "./similarity";
7
+ import { buildToolInfo } from "./tool-runner";
8
+ import { walkStash } from "./walker";
9
+ import { makeOpenRef } from "./stash-ref";
10
+ import { loadConfig } from "./config";
11
+ const DEFAULT_LIMIT = 20;
12
+ export async function agentikitSearch(input) {
13
+ const t0 = Date.now();
14
+ const query = input.query.trim().toLowerCase();
15
+ const searchType = input.type ?? "any";
16
+ const limit = normalizeLimit(input.limit);
17
+ const stashDir = resolveStashDir();
18
+ const config = loadConfig(stashDir);
19
+ const allStashDirs = [
20
+ stashDir,
21
+ ...config.additionalStashDirs.filter((d) => {
22
+ try {
23
+ return fs.statSync(d).isDirectory();
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ }),
29
+ ];
30
+ // Try indexed search (single unified pipeline: embedding + TF-IDF as weighted features)
31
+ const index = loadSearchIndex();
32
+ if (index && index.entries && index.entries.length > 0 && index.stashDir === stashDir) {
33
+ const { hits, embedMs, rankMs } = await searchIndex(index, query, searchType, limit, stashDir, allStashDirs, config);
34
+ return {
35
+ stashDir,
36
+ hits,
37
+ tip: hits.length === 0 ? "No matching stash assets were found. Try running 'akm index' to rebuild." : undefined,
38
+ timing: { totalMs: Date.now() - t0, rankMs, embedMs },
39
+ };
40
+ }
41
+ // No index: fall back to filesystem walk + substring match across all stash dirs
42
+ const hits = allStashDirs
43
+ .flatMap((dir) => substringSearch(query, searchType, limit, dir))
44
+ .slice(0, limit);
45
+ return {
46
+ stashDir,
47
+ hits,
48
+ tip: hits.length === 0 ? "No matching stash assets were found. Try running 'akm index' to rebuild." : undefined,
49
+ timing: { totalMs: Date.now() - t0 },
50
+ };
51
+ }
52
+ // ── Unified indexed search ──────────────────────────────────────────────────
53
+ async function searchIndex(index, query, searchType, limit, stashDir, allStashDirs, config) {
54
+ // Filter candidates by type
55
+ let candidates = index.entries;
56
+ if (searchType !== "any") {
57
+ candidates = candidates.filter((ie) => ie.entry.type === searchType);
58
+ }
59
+ if (candidates.length === 0)
60
+ return { hits: [] };
61
+ // Empty query: return all entries (no scoring needed)
62
+ if (!query) {
63
+ return { hits: candidates.slice(0, limit).map((ie) => buildIndexedHit({ entry: ie.entry, path: ie.path, score: 1, query, rankingMode: "tfidf", defaultStashDir: stashDir, allStashDirs })) };
64
+ }
65
+ // Score each candidate using available signals
66
+ const tEmbed0 = Date.now();
67
+ const embeddingScores = await tryEmbeddingScores(candidates, query, config);
68
+ const embedMs = Date.now() - tEmbed0;
69
+ const tRank0 = Date.now();
70
+ const tfidfScores = computeTfidfScores(index, candidates, query, searchType);
71
+ const scored = [];
72
+ for (const ie of candidates) {
73
+ const key = ie.path;
74
+ const embScore = embeddingScores?.get(key);
75
+ const tfidfScore = tfidfScores.get(key) ?? 0;
76
+ if (embScore !== undefined) {
77
+ // Weighted blend: embedding dominates when available, TF-IDF boosts lexical matches
78
+ const blended = embScore * 0.7 + tfidfScore * 0.3;
79
+ if (blended > 0)
80
+ scored.push({ ie, score: blended, rankingMode: "semantic" });
81
+ }
82
+ else if (tfidfScore > 0) {
83
+ scored.push({ ie, score: tfidfScore, rankingMode: "tfidf" });
84
+ }
85
+ }
86
+ scored.sort((a, b) => b.score - a.score);
87
+ const rankMs = Date.now() - tRank0;
88
+ return { embedMs, rankMs, hits: scored.slice(0, limit).map(({ ie, score, rankingMode }) => buildIndexedHit({
89
+ entry: ie.entry,
90
+ path: ie.path,
91
+ score: Math.round(score * 1000) / 1000,
92
+ query,
93
+ rankingMode,
94
+ defaultStashDir: stashDir,
95
+ allStashDirs,
96
+ })) };
97
+ }
98
+ // ── Embedding scorer ────────────────────────────────────────────────────────
99
+ async function tryEmbeddingScores(candidates, query, config) {
100
+ if (!config.semanticSearch)
101
+ return null;
102
+ const withEmbeddings = candidates.filter((ie) => ie.embedding && ie.embedding.length > 0);
103
+ if (withEmbeddings.length === 0)
104
+ return null;
105
+ try {
106
+ const { embed, cosineSimilarity } = await import("./embedder.js");
107
+ const queryEmbedding = await embed(query, config.embedding);
108
+ const scores = new Map();
109
+ for (const ie of withEmbeddings) {
110
+ scores.set(ie.path, cosineSimilarity(queryEmbedding, ie.embedding));
111
+ }
112
+ return scores;
113
+ }
114
+ catch {
115
+ return null;
116
+ }
117
+ }
118
+ // ── TF-IDF scorer ───────────────────────────────────────────────────────────
119
+ function computeTfidfScores(index, candidates, query, searchType) {
120
+ const candidateScoredEntries = toScoredEntries(candidates);
121
+ let adapter;
122
+ if (index.tfidf) {
123
+ const allScored = toScoredEntries(index.entries);
124
+ adapter = TfIdfAdapter.deserialize(index.tfidf, allScored);
125
+ }
126
+ else {
127
+ adapter = new TfIdfAdapter();
128
+ adapter.buildIndex(candidateScoredEntries);
129
+ }
130
+ const typeFilter = searchType === "any" ? undefined : searchType;
131
+ const results = adapter.search(query, candidates.length, typeFilter);
132
+ const scores = new Map();
133
+ for (const r of results) {
134
+ scores.set(r.path, r.score);
135
+ }
136
+ return scores;
137
+ }
138
+ // ── Substring fallback (no index) ───────────────────────────────────────────
139
+ function substringSearch(query, searchType, limit, stashDir) {
140
+ const assets = indexAssets(stashDir, searchType);
141
+ return assets
142
+ .filter((asset) => asset.name.toLowerCase().includes(query))
143
+ .sort(compareAssets)
144
+ .slice(0, limit)
145
+ .map((asset) => assetToSearchHit(asset, stashDir));
146
+ }
147
+ // ── Hit building ────────────────────────────────────────────────────────────
148
+ function findStashDirForPath(filePath, stashDirs) {
149
+ const resolved = path.resolve(filePath);
150
+ for (const dir of stashDirs) {
151
+ if (resolved.startsWith(path.resolve(dir) + path.sep))
152
+ return dir;
153
+ }
154
+ return undefined;
155
+ }
156
+ function buildIndexedHit(input) {
157
+ const entryStashDir = findStashDirForPath(input.path, input.allStashDirs) ?? input.defaultStashDir;
158
+ const typeRoot = path.join(entryStashDir, TYPE_DIRS[input.entry.type]);
159
+ const openRefName = deriveCanonicalAssetName(input.entry.type, typeRoot, input.path)
160
+ ?? input.entry.name;
161
+ const qualityBoost = input.entry.generated === true ? 0 : 0.05;
162
+ const confidenceBoost = typeof input.entry.confidence === "number" ? Math.min(0.05, Math.max(0, input.entry.confidence) * 0.05) : 0;
163
+ const score = Math.round((input.score + qualityBoost + confidenceBoost) * 1000) / 1000;
164
+ const whyMatched = buildWhyMatched(input.entry, input.query, input.rankingMode, qualityBoost, confidenceBoost);
165
+ const hit = {
166
+ type: input.entry.type,
167
+ name: input.entry.name,
168
+ path: input.path,
169
+ openRef: makeOpenRef(input.entry.type, openRefName),
170
+ description: input.entry.description,
171
+ tags: input.entry.tags,
172
+ score,
173
+ whyMatched,
174
+ };
175
+ if (input.entry.type === "tool") {
176
+ try {
177
+ const toolInfo = buildToolInfo(entryStashDir, input.path);
178
+ hit.runCmd = toolInfo.runCmd;
179
+ hit.kind = toolInfo.kind;
180
+ }
181
+ catch (error) {
182
+ if (!hasErrnoCode(error, "ENOENT"))
183
+ throw error;
184
+ }
185
+ }
186
+ return hit;
187
+ }
188
+ function buildWhyMatched(entry, query, rankingMode, qualityBoost, confidenceBoost) {
189
+ const reasons = [rankingMode === "semantic" ? "semantic similarity" : "tf-idf lexical relevance"];
190
+ const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
191
+ const name = entry.name.toLowerCase();
192
+ const tags = entry.tags?.join(" ").toLowerCase() ?? "";
193
+ const intents = entry.intents?.join(" ").toLowerCase() ?? "";
194
+ const aliases = entry.aliases?.join(" ").toLowerCase() ?? "";
195
+ if (tokens.some((t) => name.includes(t)))
196
+ reasons.push("matched name tokens");
197
+ if (tokens.some((t) => tags.includes(t)))
198
+ reasons.push("matched tags");
199
+ if (tokens.some((t) => intents.includes(t)))
200
+ reasons.push("matched intents");
201
+ if (tokens.some((t) => aliases.includes(t)))
202
+ reasons.push("matched aliases");
203
+ if (qualityBoost > 0)
204
+ reasons.push("curated metadata boost");
205
+ if (confidenceBoost > 0)
206
+ reasons.push("metadata confidence boost");
207
+ return reasons;
208
+ }
209
+ // ── Helpers ─────────────────────────────────────────────────────────────────
210
+ function toScoredEntries(entries) {
211
+ return entries.map((ie) => ({
212
+ id: `${ie.entry.type}:${ie.entry.name}`,
213
+ text: buildSearchText(ie.entry),
214
+ entry: ie.entry,
215
+ path: ie.path,
216
+ }));
217
+ }
218
+ function assetToSearchHit(asset, stashDir) {
219
+ if (asset.type !== "tool") {
220
+ return {
221
+ type: asset.type,
222
+ name: asset.name,
223
+ path: asset.path,
224
+ openRef: makeOpenRef(asset.type, asset.name),
225
+ };
226
+ }
227
+ const toolInfo = buildToolInfo(stashDir, asset.path);
228
+ return {
229
+ type: "tool",
230
+ name: asset.name,
231
+ path: asset.path,
232
+ openRef: makeOpenRef("tool", asset.name),
233
+ runCmd: toolInfo.runCmd,
234
+ kind: toolInfo.kind,
235
+ };
236
+ }
237
+ function normalizeLimit(limit) {
238
+ if (typeof limit !== "number" || Number.isNaN(limit) || limit <= 0) {
239
+ return DEFAULT_LIMIT;
240
+ }
241
+ return Math.min(Math.floor(limit), 200);
242
+ }
243
+ function fileToAsset(assetType, root, file) {
244
+ const name = deriveCanonicalAssetName(assetType, root, file);
245
+ if (!name)
246
+ return undefined;
247
+ return { type: assetType, name, path: file };
248
+ }
249
+ function indexAssets(stashDir, type) {
250
+ const assets = [];
251
+ const types = type === "any" ? ASSET_TYPES : [type];
252
+ for (const assetType of types) {
253
+ const root = path.join(stashDir, TYPE_DIRS[assetType]);
254
+ const groups = walkStash(root, assetType);
255
+ for (const { files } of groups) {
256
+ for (const file of files) {
257
+ const asset = fileToAsset(assetType, root, file);
258
+ if (asset)
259
+ assets.push(asset);
260
+ }
261
+ }
262
+ }
263
+ return assets;
264
+ }
265
+ function compareAssets(a, b) {
266
+ if (a.type !== b.type)
267
+ return a.type.localeCompare(b.type);
268
+ return a.name.localeCompare(b.name);
269
+ }
@@ -0,0 +1,5 @@
1
+ import type { KnowledgeView, ShowResponse } from "./stash-types";
2
+ export declare function agentikitShow(input: {
3
+ ref: string;
4
+ view?: KnowledgeView;
5
+ }): ShowResponse;