agentikit 0.0.9 → 0.0.13

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 (108) hide show
  1. package/README.md +139 -208
  2. package/dist/index.d.ts +8 -2
  3. package/dist/index.js +4 -1
  4. package/dist/src/asset-spec.d.ts +2 -0
  5. package/dist/src/asset-spec.js +22 -3
  6. package/dist/src/asset-type-handler.d.ts +27 -0
  7. package/dist/src/asset-type-handler.js +33 -0
  8. package/dist/src/cli.js +201 -75
  9. package/dist/src/common.d.ts +6 -1
  10. package/dist/src/common.js +18 -4
  11. package/dist/src/config-cli.d.ts +9 -0
  12. package/dist/src/config-cli.js +473 -0
  13. package/dist/src/config.d.ts +19 -6
  14. package/dist/src/config.js +139 -29
  15. package/dist/src/db.d.ts +46 -0
  16. package/dist/src/db.js +299 -0
  17. package/dist/src/embedder.js +12 -7
  18. package/dist/src/github.d.ts +4 -0
  19. package/dist/src/github.js +19 -0
  20. package/dist/src/handlers/agent-handler.d.ts +2 -0
  21. package/dist/src/handlers/agent-handler.js +26 -0
  22. package/dist/src/handlers/command-handler.d.ts +2 -0
  23. package/dist/src/handlers/command-handler.js +23 -0
  24. package/dist/src/handlers/index.d.ts +6 -0
  25. package/dist/src/handlers/index.js +23 -0
  26. package/dist/src/handlers/knowledge-handler.d.ts +2 -0
  27. package/dist/src/handlers/knowledge-handler.js +56 -0
  28. package/dist/src/handlers/markdown-helpers.d.ts +7 -0
  29. package/dist/src/handlers/markdown-helpers.js +15 -0
  30. package/dist/src/handlers/script-handler.d.ts +2 -0
  31. package/dist/src/handlers/script-handler.js +78 -0
  32. package/dist/src/handlers/skill-handler.d.ts +2 -0
  33. package/dist/src/handlers/skill-handler.js +30 -0
  34. package/dist/src/handlers/tool-handler.d.ts +2 -0
  35. package/dist/src/handlers/tool-handler.js +58 -0
  36. package/dist/src/indexer.d.ts +1 -23
  37. package/dist/src/indexer.js +162 -155
  38. package/dist/src/init.d.ts +2 -2
  39. package/dist/src/init.js +21 -9
  40. package/dist/src/llm.js +4 -3
  41. package/dist/src/metadata.d.ts +0 -1
  42. package/dist/src/metadata.js +6 -64
  43. package/dist/src/origin-resolve.d.ts +19 -0
  44. package/dist/src/origin-resolve.js +53 -0
  45. package/dist/src/registry-install.d.ts +2 -2
  46. package/dist/src/registry-install.js +142 -35
  47. package/dist/src/registry-resolve.js +90 -22
  48. package/dist/src/registry-search.d.ts +22 -0
  49. package/dist/src/registry-search.js +231 -97
  50. package/dist/src/registry-types.d.ts +9 -2
  51. package/dist/src/stash-add.js +4 -4
  52. package/dist/src/stash-clone.d.ts +22 -0
  53. package/dist/src/stash-clone.js +83 -0
  54. package/dist/src/stash-ref.d.ts +27 -3
  55. package/dist/src/stash-ref.js +63 -24
  56. package/dist/src/stash-registry.js +12 -12
  57. package/dist/src/stash-resolve.js +3 -0
  58. package/dist/src/stash-search.js +168 -164
  59. package/dist/src/stash-show.d.ts +1 -1
  60. package/dist/src/stash-show.js +28 -96
  61. package/dist/src/stash-source.d.ts +24 -0
  62. package/dist/src/stash-source.js +81 -0
  63. package/dist/src/stash-types.d.ts +14 -4
  64. package/dist/src/stash.d.ts +6 -0
  65. package/dist/src/stash.js +3 -0
  66. package/dist/src/tool-runner.d.ts +1 -1
  67. package/dist/src/tool-runner.js +18 -5
  68. package/package.json +7 -2
  69. package/src/asset-spec.ts +20 -4
  70. package/src/asset-type-handler.ts +77 -0
  71. package/src/cli.ts +213 -82
  72. package/src/common.ts +23 -5
  73. package/src/config-cli.ts +499 -0
  74. package/src/config.ts +160 -38
  75. package/src/db.ts +411 -0
  76. package/src/embedder.ts +22 -11
  77. package/src/github.ts +21 -0
  78. package/src/handlers/agent-handler.ts +32 -0
  79. package/src/handlers/command-handler.ts +29 -0
  80. package/src/handlers/index.ts +25 -0
  81. package/src/handlers/knowledge-handler.ts +62 -0
  82. package/src/handlers/markdown-helpers.ts +19 -0
  83. package/src/handlers/script-handler.ts +92 -0
  84. package/src/handlers/skill-handler.ts +37 -0
  85. package/src/handlers/tool-handler.ts +71 -0
  86. package/src/indexer.ts +208 -187
  87. package/src/init.ts +17 -9
  88. package/src/llm.ts +4 -3
  89. package/src/metadata.ts +5 -65
  90. package/src/origin-resolve.ts +67 -0
  91. package/src/registry-install.ts +158 -42
  92. package/src/registry-resolve.ts +92 -23
  93. package/src/registry-search.ts +288 -98
  94. package/src/registry-types.ts +10 -2
  95. package/src/stash-add.ts +14 -17
  96. package/src/stash-clone.ts +127 -0
  97. package/src/stash-ref.ts +84 -26
  98. package/src/stash-registry.ts +12 -12
  99. package/src/stash-resolve.ts +3 -0
  100. package/src/stash-search.ts +202 -184
  101. package/src/stash-show.ts +33 -90
  102. package/src/stash-source.ts +103 -0
  103. package/src/stash-types.ts +14 -4
  104. package/src/stash.ts +8 -0
  105. package/src/tool-runner.ts +18 -5
  106. package/dist/src/similarity.d.ts +0 -34
  107. package/dist/src/similarity.js +0 -211
  108. package/src/similarity.ts +0 -271
@@ -6,7 +6,7 @@ import { installRegistryRef, removeInstalledRegistryEntry, upsertInstalledRegist
6
6
  import { parseRegistryRef } from "./registry-resolve";
7
7
  export async function agentikitList(input) {
8
8
  const stashDir = input?.stashDir ?? resolveStashDir();
9
- const config = loadConfig(stashDir);
9
+ const config = loadConfig();
10
10
  const installed = config.registry?.installed ?? [];
11
11
  return {
12
12
  stashDir,
@@ -25,10 +25,10 @@ export async function agentikitRemove(input) {
25
25
  if (!target)
26
26
  throw new Error("Target is required.");
27
27
  const stashDir = input.stashDir ?? resolveStashDir();
28
- const config = loadConfig(stashDir);
28
+ const config = loadConfig();
29
29
  const installed = config.registry?.installed ?? [];
30
30
  const entry = resolveInstalledTarget(installed, target);
31
- const updatedConfig = removeInstalledRegistryEntry(entry.id, stashDir);
31
+ const updatedConfig = removeInstalledRegistryEntry(entry.id);
32
32
  cleanupDirectoryBestEffort(entry.cacheDir);
33
33
  const index = await agentikitIndex({ stashDir });
34
34
  return {
@@ -42,7 +42,7 @@ export async function agentikitRemove(input) {
42
42
  stashRoot: entry.stashRoot,
43
43
  },
44
44
  config: {
45
- additionalStashDirs: updatedConfig.additionalStashDirs,
45
+ mountedStashDirs: updatedConfig.mountedStashDirs,
46
46
  installedRegistryCount: updatedConfig.registry?.installed.length ?? 0,
47
47
  },
48
48
  index: {
@@ -57,12 +57,12 @@ export async function agentikitReinstall(input) {
57
57
  const stashDir = input?.stashDir ?? resolveStashDir();
58
58
  const target = input?.target?.trim();
59
59
  const all = input?.all === true;
60
- const installedEntries = loadConfig(stashDir).registry?.installed ?? [];
60
+ const installedEntries = loadConfig().registry?.installed ?? [];
61
61
  const selectedEntries = selectTargets(installedEntries, target, all);
62
62
  const processed = [];
63
63
  for (const entry of selectedEntries) {
64
64
  const installed = await installRegistryRef(entry.ref);
65
- upsertInstalledRegistryEntry(toInstalledEntry(installed), stashDir);
65
+ upsertInstalledRegistryEntry(toInstalledEntry(installed));
66
66
  if (entry.cacheDir !== installed.cacheDir) {
67
67
  cleanupDirectoryBestEffort(entry.cacheDir);
68
68
  }
@@ -75,14 +75,14 @@ export async function agentikitReinstall(input) {
75
75
  });
76
76
  }
77
77
  const index = await agentikitIndex({ stashDir });
78
- const config = loadConfig(stashDir);
78
+ const config = loadConfig();
79
79
  return {
80
80
  stashDir,
81
81
  target,
82
82
  all,
83
83
  processed,
84
84
  config: {
85
- additionalStashDirs: config.additionalStashDirs,
85
+ mountedStashDirs: config.mountedStashDirs,
86
86
  installedRegistryCount: config.registry?.installed.length ?? 0,
87
87
  },
88
88
  index: {
@@ -97,12 +97,12 @@ export async function agentikitUpdate(input) {
97
97
  const stashDir = input?.stashDir ?? resolveStashDir();
98
98
  const target = input?.target?.trim();
99
99
  const all = input?.all === true;
100
- const installedEntries = loadConfig(stashDir).registry?.installed ?? [];
100
+ const installedEntries = loadConfig().registry?.installed ?? [];
101
101
  const selectedEntries = selectTargets(installedEntries, target, all);
102
102
  const processed = [];
103
103
  for (const entry of selectedEntries) {
104
104
  const installed = await installRegistryRef(entry.ref);
105
- upsertInstalledRegistryEntry(toInstalledEntry(installed), stashDir);
105
+ upsertInstalledRegistryEntry(toInstalledEntry(installed));
106
106
  if (entry.cacheDir !== installed.cacheDir) {
107
107
  cleanupDirectoryBestEffort(entry.cacheDir);
108
108
  }
@@ -126,14 +126,14 @@ export async function agentikitUpdate(input) {
126
126
  });
127
127
  }
128
128
  const index = await agentikitIndex({ stashDir });
129
- const config = loadConfig(stashDir);
129
+ const config = loadConfig();
130
130
  return {
131
131
  stashDir,
132
132
  target,
133
133
  all,
134
134
  processed,
135
135
  config: {
136
- additionalStashDirs: config.additionalStashDirs,
136
+ mountedStashDirs: config.mountedStashDirs,
137
137
  installedRegistryCount: config.registry?.installed.length ?? 0,
138
138
  },
139
139
  index: {
@@ -21,6 +21,9 @@ export function resolveAssetPath(stashDir, type, name) {
21
21
  if (type === "tool") {
22
22
  throw new Error("Tool ref must resolve to a .sh, .ts, .js, .ps1, .cmd, or .bat file.");
23
23
  }
24
+ if (type === "script") {
25
+ throw new Error("Script ref must resolve to a file with a supported script extension. Refer to the Agentikit documentation for the complete list of supported script extensions.");
26
+ }
24
27
  throw new Error(`Stash asset not found for ref: ${type}:${name}`);
25
28
  }
26
29
  return realTarget;
@@ -1,37 +1,14 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { hasErrnoCode, resolveStashDir } from "./common";
4
3
  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
4
  import { walkStash } from "./walker";
9
- import { makeOpenRef } from "./stash-ref";
5
+ import { makeAssetRef } from "./stash-ref";
10
6
  import { loadConfig } from "./config";
11
7
  import { searchRegistry } from "./registry-search";
8
+ import { openDatabase, closeDatabase, getDbPath, getMeta, searchFts, searchVec, getAllEntries, getEntryCount, getEntryById, isVecAvailable, } from "./db";
9
+ import { tryGetHandler } from "./asset-type-handler";
10
+ import { resolveStashSources, findSourceForPath } from "./stash-source";
12
11
  const DEFAULT_LIMIT = 20;
13
- const DEFAULT_USAGE_GUIDE_BY_TYPE = {
14
- tool: [
15
- "Use the hit's runCmd for execution so runtime and working directory stay correct.",
16
- "Use `akm show <openRef>` to inspect the tool before running it.",
17
- ],
18
- skill: [
19
- "Read and apply the skill instructions as written, then adapt examples to your current repo state and task.",
20
- "Use `akm show <openRef>` to read the full SKILL.md for required steps and constraints.",
21
- ],
22
- command: [
23
- "Read the .md file, fill placeholders, and run it in the current repo context.",
24
- "Use `akm show <openRef>` to retrieve the command template body.",
25
- ],
26
- agent: [
27
- "Read the .md file and dispatch and agent using the content of the file. Use modelHint/toolPolicy when present to run the agent with compatible settings.",
28
- "Use with `akm show <openRef>` to get the full prompt payload.",
29
- ],
30
- knowledge: [
31
- "Use `akm show <openRef>` to read the document; start with `--view toc` for large files.",
32
- "Use `--view section` or `--view lines` to load only the part you need.",
33
- ],
34
- };
35
12
  export async function agentikitSearch(input) {
36
13
  const t0 = Date.now();
37
14
  const query = input.query.trim();
@@ -40,7 +17,8 @@ export async function agentikitSearch(input) {
40
17
  const limit = normalizeLimit(input.limit);
41
18
  const usageMode = parseSearchUsageMode(input.usage);
42
19
  const source = parseSearchSource(input.source);
43
- const stashDir = resolveStashDir();
20
+ const sources = resolveStashSources();
21
+ const stashDir = sources[0].path;
44
22
  const localResult = source === "registry"
45
23
  ? undefined
46
24
  : await searchLocal({
@@ -49,10 +27,12 @@ export async function agentikitSearch(input) {
49
27
  limit,
50
28
  usageMode,
51
29
  stashDir,
30
+ sources,
52
31
  });
32
+ const config = loadConfig();
53
33
  const registryResult = source === "local"
54
34
  ? undefined
55
- : await searchRegistry(query, { limit });
35
+ : await searchRegistry(query, { limit, registryUrls: config.registryUrls });
56
36
  if (source === "local") {
57
37
  return {
58
38
  stashDir,
@@ -76,6 +56,7 @@ export async function agentikitSearch(input) {
76
56
  homepage: hit.homepage,
77
57
  score: hit.score,
78
58
  metadata: hit.metadata,
59
+ curated: hit.curated,
79
60
  installRef,
80
61
  installCmd: `akm add ${installRef}`,
81
62
  };
@@ -103,32 +84,39 @@ export async function agentikitSearch(input) {
103
84
  };
104
85
  }
105
86
  async function searchLocal(input) {
106
- const { query, searchType, limit, usageMode, stashDir } = input;
107
- const config = loadConfig(stashDir);
108
- const allStashDirs = [
109
- stashDir,
110
- ...config.additionalStashDirs.filter((d) => {
87
+ const { query, searchType, limit, usageMode, stashDir, sources } = input;
88
+ const config = loadConfig();
89
+ const allStashDirs = sources.map((s) => s.path);
90
+ // Try to open the database
91
+ const dbPath = getDbPath();
92
+ try {
93
+ if (fs.existsSync(dbPath)) {
94
+ const embeddingDim = config.embedding?.dimension;
95
+ const db = openDatabase(dbPath, embeddingDim ? { embeddingDim } : undefined);
111
96
  try {
112
- return fs.statSync(d).isDirectory();
97
+ const entryCount = getEntryCount(db);
98
+ const storedStashDir = getMeta(db, "stashDir");
99
+ if (entryCount > 0 && storedStashDir === stashDir) {
100
+ const { hits, usageGuide, embedMs, rankMs } = await searchDatabase(db, query, searchType, limit, stashDir, allStashDirs, config, usageMode, sources);
101
+ return {
102
+ hits,
103
+ usageGuide,
104
+ tip: hits.length === 0 ? "No matching stash assets were found. Try running 'akm index' to rebuild." : undefined,
105
+ embedMs,
106
+ rankMs,
107
+ };
108
+ }
113
109
  }
114
- catch {
115
- return false;
110
+ finally {
111
+ closeDatabase(db);
116
112
  }
117
- }),
118
- ];
119
- const index = loadSearchIndex();
120
- if (index && index.entries && index.entries.length > 0 && index.stashDir === stashDir) {
121
- const { hits, usageGuide, embedMs, rankMs } = await searchIndex(index, query, searchType, limit, stashDir, allStashDirs, config, usageMode);
122
- return {
123
- hits,
124
- usageGuide,
125
- tip: hits.length === 0 ? "No matching stash assets were found. Try running 'akm index' to rebuild." : undefined,
126
- embedMs,
127
- rankMs,
128
- };
113
+ }
114
+ }
115
+ catch (error) {
116
+ console.warn("Search index unavailable, falling back to substring search:", error instanceof Error ? error.message : String(error));
129
117
  }
130
118
  const hits = allStashDirs
131
- .flatMap((dir) => substringSearch(query, searchType, limit, dir))
119
+ .flatMap((dir) => substringSearch(query, searchType, limit, dir, sources))
132
120
  .slice(0, limit);
133
121
  const usageGuide = shouldIncludeUsageGuide(usageMode) ? buildUsageGuide(hits.map((hit) => hit.type), searchType) : undefined;
134
122
  return {
@@ -137,71 +125,126 @@ async function searchLocal(input) {
137
125
  tip: hits.length === 0 ? "No matching stash assets were found. Try running 'akm index' to rebuild." : undefined,
138
126
  };
139
127
  }
140
- // ── Unified indexed search ──────────────────────────────────────────────────
141
- async function searchIndex(index, query, searchType, limit, stashDir, allStashDirs, config, usageMode) {
142
- // Filter candidates by type
143
- let candidates = index.entries;
144
- if (searchType !== "any") {
145
- candidates = candidates.filter((ie) => ie.entry.type === searchType);
146
- }
147
- if (candidates.length === 0) {
148
- return {
149
- hits: [],
150
- usageGuide: shouldIncludeUsageGuide(usageMode) ? buildUsageGuide([], searchType) : undefined,
151
- };
152
- }
153
- // Empty query: return all entries (no scoring needed)
128
+ // ── Database search ─────────────────────────────────────────────────────────
129
+ async function searchDatabase(db, query, searchType, limit, stashDir, allStashDirs, config, usageMode, sources) {
130
+ // Empty query: return all entries
154
131
  if (!query) {
155
- const selectedCandidates = candidates.slice(0, limit);
156
- const hits = selectedCandidates.map((ie) => buildIndexedHit({
132
+ const typeFilter = searchType === "any" ? undefined : searchType;
133
+ const allEntries = getAllEntries(db, typeFilter);
134
+ const selected = allEntries.slice(0, limit);
135
+ const hits = selected.map((ie) => buildDbHit({
157
136
  entry: ie.entry,
158
- path: ie.path,
137
+ path: ie.filePath,
159
138
  score: 1,
160
139
  query,
161
- rankingMode: "tfidf",
140
+ rankingMode: "fts",
162
141
  defaultStashDir: stashDir,
163
142
  allStashDirs,
143
+ sources,
164
144
  includeItemUsage: shouldIncludeItemUsage(usageMode),
165
145
  }));
166
146
  return {
167
147
  hits,
168
148
  usageGuide: shouldIncludeUsageGuide(usageMode)
169
- ? buildUsageGuideFromEntries(selectedCandidates.map((candidate) => candidate.entry), searchType)
149
+ ? buildUsageGuideFromEntries(selected.map((e) => e.entry), searchType)
170
150
  : undefined,
171
151
  };
172
152
  }
173
- // Score each candidate using available signals
153
+ // Score using FTS5 (BM25) and optionally sqlite-vec
174
154
  const tEmbed0 = Date.now();
175
- const embeddingScores = await tryEmbeddingScores(candidates, query, config);
155
+ const embeddingScores = await tryVecScores(db, query, limit * 3, config);
176
156
  const embedMs = Date.now() - tEmbed0;
177
157
  const tRank0 = Date.now();
178
- const tfidfScores = computeTfidfScores(index, candidates, query, searchType);
158
+ const typeFilter = searchType === "any" ? undefined : searchType;
159
+ const ftsResults = searchFts(db, query, limit * 3, typeFilter);
160
+ // Build score map from FTS results (normalize BM25 scores)
161
+ const ftsScoreMap = new Map();
162
+ for (const r of ftsResults) {
163
+ // BM25 returns negative scores (more negative = better match), normalize to 0-1
164
+ const absScore = Math.abs(r.bm25Score);
165
+ const normalized = absScore / (1 + absScore);
166
+ ftsScoreMap.set(r.id, { score: normalized, result: r });
167
+ }
168
+ // Blend scores
179
169
  const scored = [];
180
- for (const ie of candidates) {
181
- const key = ie.path;
182
- const embScore = embeddingScores?.get(key);
183
- const tfidfScore = tfidfScores.get(key) ?? 0;
170
+ const seenIds = new Set();
171
+ // Process FTS results
172
+ for (const [id, { score: ftsScore, result }] of ftsScoreMap) {
173
+ seenIds.add(id);
174
+ const embScore = embeddingScores?.get(id);
184
175
  if (embScore !== undefined) {
185
- // Weighted blend: embedding dominates when available, TF-IDF boosts lexical matches
186
- const blended = embScore * 0.7 + tfidfScore * 0.3;
176
+ const blended = embScore * 0.7 + ftsScore * 0.3;
187
177
  if (blended > 0)
188
- scored.push({ ie, score: blended, rankingMode: "semantic" });
178
+ scored.push({ id, entry: result.entry, filePath: result.filePath, score: blended, rankingMode: "semantic" });
179
+ }
180
+ else if (ftsScore > 0) {
181
+ scored.push({ id, entry: result.entry, filePath: result.filePath, score: ftsScore, rankingMode: "fts" });
189
182
  }
190
- else if (tfidfScore > 0) {
191
- scored.push({ ie, score: tfidfScore, rankingMode: "tfidf" });
183
+ }
184
+ // Add vec-only results not already in FTS results
185
+ if (embeddingScores) {
186
+ for (const [id, embScore] of embeddingScores) {
187
+ if (seenIds.has(id))
188
+ continue;
189
+ const found = getEntryById(db, id);
190
+ if (found) {
191
+ if (typeFilter && found.entry.type !== typeFilter)
192
+ continue;
193
+ scored.push({
194
+ id,
195
+ entry: found.entry,
196
+ filePath: found.filePath,
197
+ score: embScore,
198
+ rankingMode: "semantic",
199
+ });
200
+ }
192
201
  }
193
202
  }
203
+ // Apply boosts (tag, intent, name matches)
204
+ const queryTokens = query.toLowerCase().split(/\s+/).filter(Boolean);
205
+ for (const item of scored) {
206
+ const entry = item.entry;
207
+ // Tag boost
208
+ if (entry.tags) {
209
+ for (const tag of entry.tags) {
210
+ if (queryTokens.some((t) => tag.toLowerCase() === t)) {
211
+ item.score += 0.15;
212
+ }
213
+ }
214
+ }
215
+ // Intent boost
216
+ if (entry.intents) {
217
+ for (const intent of entry.intents) {
218
+ const intentLower = intent.toLowerCase();
219
+ for (const token of queryTokens) {
220
+ if (intentLower.includes(token)) {
221
+ item.score += 0.12;
222
+ break;
223
+ }
224
+ }
225
+ }
226
+ }
227
+ // Name boost
228
+ const nameLower = entry.name.toLowerCase().replace(/[-_]/g, " ");
229
+ if (queryTokens.some((t) => nameLower.includes(t))) {
230
+ item.score += 0.1;
231
+ }
232
+ }
233
+ for (const item of scored) {
234
+ item.score = Math.min(item.score, 1.0);
235
+ }
194
236
  scored.sort((a, b) => b.score - a.score);
195
237
  const rankMs = Date.now() - tRank0;
196
238
  const selected = scored.slice(0, limit);
197
- const hits = selected.map(({ ie, score, rankingMode }) => buildIndexedHit({
198
- entry: ie.entry,
199
- path: ie.path,
239
+ const hits = selected.map(({ entry, filePath, score, rankingMode }) => buildDbHit({
240
+ entry,
241
+ path: filePath,
200
242
  score: Math.round(score * 1000) / 1000,
201
243
  query,
202
244
  rankingMode,
203
245
  defaultStashDir: stashDir,
204
246
  allStashDirs,
247
+ sources,
205
248
  includeItemUsage: shouldIncludeItemUsage(usageMode),
206
249
  }));
207
250
  return {
@@ -209,83 +252,62 @@ async function searchIndex(index, query, searchType, limit, stashDir, allStashDi
209
252
  rankMs,
210
253
  hits,
211
254
  usageGuide: shouldIncludeUsageGuide(usageMode)
212
- ? buildUsageGuideFromEntries(selected.map((item) => item.ie.entry), searchType)
255
+ ? buildUsageGuideFromEntries(selected.map((item) => item.entry), searchType)
213
256
  : undefined,
214
257
  };
215
258
  }
216
- // ── Embedding scorer ────────────────────────────────────────────────────────
217
- async function tryEmbeddingScores(candidates, query, config) {
218
- if (!config.semanticSearch)
259
+ // ── Vector scorer ───────────────────────────────────────────────────────────
260
+ async function tryVecScores(db, query, k, config) {
261
+ if (!config.semanticSearch || !isVecAvailable())
219
262
  return null;
220
- const withEmbeddings = candidates.filter((ie) => ie.embedding && ie.embedding.length > 0);
221
- if (withEmbeddings.length === 0)
263
+ const hasEmbeddings = getMeta(db, "hasEmbeddings");
264
+ if (hasEmbeddings !== "1")
222
265
  return null;
223
266
  try {
224
- const { embed, cosineSimilarity } = await import("./embedder.js");
267
+ const { embed } = await import("./embedder.js");
225
268
  const queryEmbedding = await embed(query, config.embedding);
269
+ const vecResults = searchVec(db, queryEmbedding, k);
226
270
  const scores = new Map();
227
- for (const ie of withEmbeddings) {
228
- scores.set(ie.path, cosineSimilarity(queryEmbedding, ie.embedding));
271
+ for (const { id, distance } of vecResults) {
272
+ // Convert L2 distance to cosine similarity (vectors are normalized)
273
+ const cosineSim = 1 - (distance * distance) / 2;
274
+ scores.set(id, Math.max(0, cosineSim));
229
275
  }
230
276
  return scores;
231
277
  }
232
- catch {
278
+ catch (error) {
279
+ console.warn("Vector search failed, skipping:", error instanceof Error ? error.message : String(error));
233
280
  return null;
234
281
  }
235
282
  }
236
- // ── TF-IDF scorer ───────────────────────────────────────────────────────────
237
- function computeTfidfScores(index, candidates, query, searchType) {
238
- const candidateScoredEntries = toScoredEntries(candidates);
239
- let adapter;
240
- if (index.tfidf) {
241
- const allScored = toScoredEntries(index.entries);
242
- adapter = TfIdfAdapter.deserialize(index.tfidf, allScored);
243
- }
244
- else {
245
- adapter = new TfIdfAdapter();
246
- adapter.buildIndex(candidateScoredEntries);
247
- }
248
- const typeFilter = searchType === "any" ? undefined : searchType;
249
- const results = adapter.search(query, candidates.length, typeFilter);
250
- const scores = new Map();
251
- for (const r of results) {
252
- scores.set(r.path, r.score);
253
- }
254
- return scores;
255
- }
256
283
  // ── Substring fallback (no index) ───────────────────────────────────────────
257
- function substringSearch(query, searchType, limit, stashDir) {
284
+ function substringSearch(query, searchType, limit, stashDir, sources) {
258
285
  const assets = indexAssets(stashDir, searchType);
259
286
  return assets
260
287
  .filter((asset) => asset.name.toLowerCase().includes(query))
261
288
  .sort(compareAssets)
262
289
  .slice(0, limit)
263
- .map((asset) => assetToSearchHit(asset, stashDir));
290
+ .map((asset) => assetToSearchHit(asset, stashDir, sources));
264
291
  }
265
292
  // ── Hit building ────────────────────────────────────────────────────────────
266
- function findStashDirForPath(filePath, stashDirs) {
267
- const resolved = path.resolve(filePath);
268
- for (const dir of stashDirs) {
269
- if (resolved.startsWith(path.resolve(dir) + path.sep))
270
- return dir;
271
- }
272
- return undefined;
273
- }
274
- function buildIndexedHit(input) {
275
- const entryStashDir = findStashDirForPath(input.path, input.allStashDirs) ?? input.defaultStashDir;
293
+ function buildDbHit(input) {
294
+ const entryStashDir = findSourceForPath(input.path, input.sources)?.path ?? input.defaultStashDir;
276
295
  const typeRoot = path.join(entryStashDir, TYPE_DIRS[input.entry.type]);
277
296
  const openRefName = deriveCanonicalAssetName(input.entry.type, typeRoot, input.path)
278
297
  ?? input.entry.name;
279
298
  const qualityBoost = input.entry.generated === true ? 0 : 0.05;
280
299
  const confidenceBoost = typeof input.entry.confidence === "number" ? Math.min(0.05, Math.max(0, input.entry.confidence) * 0.05) : 0;
281
- const score = Math.round((input.score + qualityBoost + confidenceBoost) * 1000) / 1000;
300
+ const score = Math.min(Math.round((input.score + qualityBoost + confidenceBoost) * 1000) / 1000, 1.0);
282
301
  const whyMatched = buildWhyMatched(input.entry, input.query, input.rankingMode, qualityBoost, confidenceBoost);
302
+ const source = findSourceForPath(input.path, input.sources);
283
303
  const hit = {
284
304
  hitSource: "local",
285
305
  type: input.entry.type,
286
306
  name: input.entry.name,
287
307
  path: input.path,
288
- openRef: makeOpenRef(input.entry.type, openRefName),
308
+ openRef: makeAssetRef(input.entry.type, openRefName, source?.registryId),
309
+ registryId: source?.registryId,
310
+ editable: source?.writable ?? false,
289
311
  description: input.entry.description,
290
312
  tags: input.entry.tags,
291
313
  score,
@@ -294,21 +316,14 @@ function buildIndexedHit(input) {
294
316
  if (input.includeItemUsage && input.entry.usage && input.entry.usage.length > 0) {
295
317
  hit.usage = input.entry.usage;
296
318
  }
297
- if (input.entry.type === "tool") {
298
- try {
299
- const toolInfo = buildToolInfo(entryStashDir, input.path);
300
- hit.runCmd = toolInfo.runCmd;
301
- hit.kind = toolInfo.kind;
302
- }
303
- catch (error) {
304
- if (!hasErrnoCode(error, "ENOENT"))
305
- throw error;
306
- }
319
+ const handler = tryGetHandler(input.entry.type);
320
+ if (handler?.enrichSearchHit) {
321
+ handler.enrichSearchHit(hit, entryStashDir);
307
322
  }
308
323
  return hit;
309
324
  }
310
325
  function buildWhyMatched(entry, query, rankingMode, qualityBoost, confidenceBoost) {
311
- const reasons = [rankingMode === "semantic" ? "semantic similarity" : "tf-idf lexical relevance"];
326
+ const reasons = [rankingMode === "semantic" ? "semantic similarity" : "fts bm25 relevance"];
312
327
  const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
313
328
  const name = entry.name.toLowerCase();
314
329
  const tags = entry.tags?.join(" ").toLowerCase() ?? "";
@@ -329,34 +344,22 @@ function buildWhyMatched(entry, query, rankingMode, qualityBoost, confidenceBoos
329
344
  return reasons;
330
345
  }
331
346
  // ── Helpers ─────────────────────────────────────────────────────────────────
332
- function toScoredEntries(entries) {
333
- return entries.map((ie) => ({
334
- id: `${ie.entry.type}:${ie.entry.name}`,
335
- text: buildSearchText(ie.entry),
336
- entry: ie.entry,
337
- path: ie.path,
338
- }));
339
- }
340
- function assetToSearchHit(asset, stashDir) {
341
- if (asset.type !== "tool") {
342
- return {
343
- hitSource: "local",
344
- type: asset.type,
345
- name: asset.name,
346
- path: asset.path,
347
- openRef: makeOpenRef(asset.type, asset.name),
348
- };
349
- }
350
- const toolInfo = buildToolInfo(stashDir, asset.path);
351
- return {
347
+ function assetToSearchHit(asset, stashDir, sources) {
348
+ const source = findSourceForPath(asset.path, sources);
349
+ const hit = {
352
350
  hitSource: "local",
353
- type: "tool",
351
+ type: asset.type,
354
352
  name: asset.name,
355
353
  path: asset.path,
356
- openRef: makeOpenRef("tool", asset.name),
357
- runCmd: toolInfo.runCmd,
358
- kind: toolInfo.kind,
354
+ openRef: makeAssetRef(asset.type, asset.name, source?.registryId),
355
+ registryId: source?.registryId,
356
+ editable: source?.writable ?? false,
359
357
  };
358
+ const handler = tryGetHandler(asset.type);
359
+ if (handler?.enrichSearchHit) {
360
+ handler.enrichSearchHit(hit, stashDir);
361
+ }
362
+ return hit;
360
363
  }
361
364
  function normalizeLimit(limit) {
362
365
  if (typeof limit !== "number" || Number.isNaN(limit) || limit <= 0) {
@@ -453,7 +456,8 @@ function resolveGuideTypes(hitTypes, searchType) {
453
456
  return Array.from(new Set(hitTypes));
454
457
  }
455
458
  function usageGuideByType(type) {
456
- return DEFAULT_USAGE_GUIDE_BY_TYPE[type];
459
+ const handler = tryGetHandler(type);
460
+ return handler?.defaultUsageGuide ?? [];
457
461
  }
458
462
  function fileToAsset(assetType, root, file) {
459
463
  const name = deriveCanonicalAssetName(assetType, root, file);
@@ -2,4 +2,4 @@ import type { KnowledgeView, ShowResponse } from "./stash-types";
2
2
  export declare function agentikitShow(input: {
3
3
  ref: string;
4
4
  view?: KnowledgeView;
5
- }): ShowResponse;
5
+ }): Promise<ShowResponse>;