agentikit 0.0.13 → 0.0.15

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 (156) hide show
  1. package/LICENSE +385 -0
  2. package/README.md +187 -110
  3. package/dist/{src/asset-spec.js → asset-spec.js} +11 -2
  4. package/dist/{src/asset-type-handler.js → asset-type-handler.js} +4 -3
  5. package/dist/cli.js +709 -0
  6. package/dist/common.js +192 -0
  7. package/dist/{src/config-cli.js → config-cli.js} +36 -30
  8. package/dist/{src/config.js → config.js} +95 -25
  9. package/dist/{src/db.js → db.js} +123 -51
  10. package/dist/{src/embedder.js → embedder.js} +57 -2
  11. package/dist/errors.js +28 -0
  12. package/dist/file-context.js +188 -0
  13. package/dist/{src/frontmatter.js → frontmatter.js} +1 -1
  14. package/dist/{src/github.js → github.js} +1 -3
  15. package/dist/handlers/agent-handler.js +19 -0
  16. package/dist/handlers/command-handler.js +20 -0
  17. package/dist/handlers/handler-bridge.js +51 -0
  18. package/dist/handlers/index.js +19 -0
  19. package/dist/handlers/knowledge-handler.js +32 -0
  20. package/dist/handlers/script-handler.js +42 -0
  21. package/dist/{src/handlers → handlers}/skill-handler.js +5 -6
  22. package/dist/{src/handlers → handlers}/tool-handler.js +8 -24
  23. package/dist/{src/indexer.js → indexer.js} +50 -26
  24. package/dist/init.js +43 -0
  25. package/dist/{src/llm.js → llm.js} +6 -11
  26. package/dist/lockfile.js +60 -0
  27. package/dist/matchers.js +163 -0
  28. package/dist/{src/metadata.js → metadata.js} +36 -16
  29. package/dist/{src/origin-resolve.js → origin-resolve.js} +10 -9
  30. package/dist/paths.js +83 -0
  31. package/dist/{src/registry-install.js → registry-install.js} +151 -19
  32. package/dist/{src/registry-resolve.js → registry-resolve.js} +190 -26
  33. package/dist/{src/registry-search.js → registry-search.js} +13 -21
  34. package/dist/renderers.js +286 -0
  35. package/dist/{src/ripgrep-install.js → ripgrep-install.js} +8 -27
  36. package/dist/{src/ripgrep-resolve.js → ripgrep-resolve.js} +21 -11
  37. package/dist/ripgrep.js +2 -0
  38. package/dist/self-update.js +226 -0
  39. package/dist/{src/stash-add.js → stash-add.js} +14 -4
  40. package/dist/stash-clone.js +115 -0
  41. package/dist/{src/stash-ref.js → stash-ref.js} +10 -9
  42. package/dist/{src/stash-registry.js → stash-registry.js} +21 -46
  43. package/dist/{src/stash-resolve.js → stash-resolve.js} +10 -9
  44. package/dist/{src/stash-search.js → stash-search.js} +89 -74
  45. package/dist/stash-show.js +74 -0
  46. package/dist/stash-source.js +127 -0
  47. package/dist/submit.js +557 -0
  48. package/dist/{src/tool-runner.js → tool-runner.js} +1 -5
  49. package/dist/{src/walker.js → walker.js} +38 -0
  50. package/dist/warn.js +20 -0
  51. package/package.json +13 -18
  52. package/dist/index.d.ts +0 -28
  53. package/dist/index.js +0 -15
  54. package/dist/src/asset-spec.d.ts +0 -16
  55. package/dist/src/asset-type-handler.d.ts +0 -27
  56. package/dist/src/cli.d.ts +0 -2
  57. package/dist/src/cli.js +0 -399
  58. package/dist/src/common.d.ts +0 -13
  59. package/dist/src/common.js +0 -60
  60. package/dist/src/config-cli.d.ts +0 -9
  61. package/dist/src/config.d.ts +0 -50
  62. package/dist/src/db.d.ts +0 -46
  63. package/dist/src/embedder.d.ts +0 -10
  64. package/dist/src/frontmatter.d.ts +0 -30
  65. package/dist/src/github.d.ts +0 -4
  66. package/dist/src/handlers/agent-handler.d.ts +0 -2
  67. package/dist/src/handlers/agent-handler.js +0 -26
  68. package/dist/src/handlers/command-handler.d.ts +0 -2
  69. package/dist/src/handlers/command-handler.js +0 -23
  70. package/dist/src/handlers/index.d.ts +0 -6
  71. package/dist/src/handlers/index.js +0 -23
  72. package/dist/src/handlers/knowledge-handler.d.ts +0 -2
  73. package/dist/src/handlers/knowledge-handler.js +0 -56
  74. package/dist/src/handlers/markdown-helpers.d.ts +0 -7
  75. package/dist/src/handlers/script-handler.d.ts +0 -2
  76. package/dist/src/handlers/script-handler.js +0 -78
  77. package/dist/src/handlers/skill-handler.d.ts +0 -2
  78. package/dist/src/handlers/tool-handler.d.ts +0 -2
  79. package/dist/src/indexer.d.ts +0 -22
  80. package/dist/src/init.d.ts +0 -19
  81. package/dist/src/init.js +0 -99
  82. package/dist/src/llm.d.ts +0 -15
  83. package/dist/src/markdown.d.ts +0 -18
  84. package/dist/src/metadata.d.ts +0 -41
  85. package/dist/src/origin-resolve.d.ts +0 -19
  86. package/dist/src/registry-install.d.ts +0 -11
  87. package/dist/src/registry-resolve.d.ts +0 -3
  88. package/dist/src/registry-search.d.ts +0 -27
  89. package/dist/src/registry-types.d.ts +0 -62
  90. package/dist/src/ripgrep-install.d.ts +0 -12
  91. package/dist/src/ripgrep-resolve.d.ts +0 -13
  92. package/dist/src/ripgrep.d.ts +0 -3
  93. package/dist/src/ripgrep.js +0 -2
  94. package/dist/src/stash-add.d.ts +0 -4
  95. package/dist/src/stash-clone.d.ts +0 -22
  96. package/dist/src/stash-clone.js +0 -83
  97. package/dist/src/stash-ref.d.ts +0 -31
  98. package/dist/src/stash-registry.d.ts +0 -18
  99. package/dist/src/stash-resolve.d.ts +0 -2
  100. package/dist/src/stash-search.d.ts +0 -8
  101. package/dist/src/stash-show.d.ts +0 -5
  102. package/dist/src/stash-show.js +0 -46
  103. package/dist/src/stash-source.d.ts +0 -24
  104. package/dist/src/stash-source.js +0 -81
  105. package/dist/src/stash-types.d.ts +0 -227
  106. package/dist/src/stash.d.ts +0 -16
  107. package/dist/src/stash.js +0 -9
  108. package/dist/src/tool-runner.d.ts +0 -35
  109. package/dist/src/walker.d.ts +0 -19
  110. package/src/asset-spec.ts +0 -85
  111. package/src/asset-type-handler.ts +0 -77
  112. package/src/cli.ts +0 -427
  113. package/src/common.ts +0 -76
  114. package/src/config-cli.ts +0 -499
  115. package/src/config.ts +0 -305
  116. package/src/db.ts +0 -411
  117. package/src/embedder.ts +0 -128
  118. package/src/frontmatter.ts +0 -95
  119. package/src/github.ts +0 -21
  120. package/src/handlers/agent-handler.ts +0 -32
  121. package/src/handlers/command-handler.ts +0 -29
  122. package/src/handlers/index.ts +0 -25
  123. package/src/handlers/knowledge-handler.ts +0 -62
  124. package/src/handlers/markdown-helpers.ts +0 -19
  125. package/src/handlers/script-handler.ts +0 -92
  126. package/src/handlers/skill-handler.ts +0 -37
  127. package/src/handlers/tool-handler.ts +0 -71
  128. package/src/indexer.ts +0 -392
  129. package/src/init.ts +0 -114
  130. package/src/llm.ts +0 -125
  131. package/src/markdown.ts +0 -106
  132. package/src/metadata.ts +0 -333
  133. package/src/origin-resolve.ts +0 -67
  134. package/src/registry-install.ts +0 -361
  135. package/src/registry-resolve.ts +0 -341
  136. package/src/registry-search.ts +0 -335
  137. package/src/registry-types.ts +0 -72
  138. package/src/ripgrep-install.ts +0 -200
  139. package/src/ripgrep-resolve.ts +0 -72
  140. package/src/ripgrep.ts +0 -3
  141. package/src/stash-add.ts +0 -63
  142. package/src/stash-clone.ts +0 -127
  143. package/src/stash-ref.ts +0 -99
  144. package/src/stash-registry.ts +0 -259
  145. package/src/stash-resolve.ts +0 -50
  146. package/src/stash-search.ts +0 -613
  147. package/src/stash-show.ts +0 -55
  148. package/src/stash-source.ts +0 -103
  149. package/src/stash-types.ts +0 -231
  150. package/src/stash.ts +0 -39
  151. package/src/tool-runner.ts +0 -142
  152. package/src/walker.ts +0 -53
  153. /package/dist/{src/handlers → handlers}/markdown-helpers.js +0 -0
  154. /package/dist/{src/markdown.js → markdown.js} +0 -0
  155. /package/dist/{src/registry-types.js → registry-types.js} +0 -0
  156. /package/dist/{src/stash-types.js → stash-types.js} +0 -0
@@ -1,13 +1,16 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { ASSET_TYPES, TYPE_DIRS, deriveCanonicalAssetName } from "./asset-spec";
4
- import { walkStash } from "./walker";
5
- import { makeAssetRef } from "./stash-ref";
3
+ import { ASSET_TYPES, deriveCanonicalAssetName, TYPE_DIRS } from "./asset-spec";
4
+ import { tryGetHandler } from "./asset-type-handler";
6
5
  import { loadConfig } from "./config";
6
+ import { closeDatabase, getAllEntries, getEntryById, getEntryCount, getMeta, openDatabase, searchFts, searchVec, } from "./db";
7
+ import { UsageError } from "./errors";
8
+ import { getDbPath } from "./paths";
7
9
  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";
10
+ import { makeAssetRef } from "./stash-ref";
11
+ import { buildEditHint, findSourceForPath, isEditable, resolveStashSources } from "./stash-source";
12
+ import { walkStash } from "./walker";
13
+ import { warn } from "./warn";
11
14
  const DEFAULT_LIMIT = 20;
12
15
  export async function agentikitSearch(input) {
13
16
  const t0 = Date.now();
@@ -17,7 +20,17 @@ export async function agentikitSearch(input) {
17
20
  const limit = normalizeLimit(input.limit);
18
21
  const usageMode = parseSearchUsageMode(input.usage);
19
22
  const source = parseSearchSource(input.source);
20
- const sources = resolveStashSources();
23
+ const config = loadConfig();
24
+ const sources = resolveStashSources(undefined, config);
25
+ if (sources.length === 0) {
26
+ return {
27
+ stashDir: "",
28
+ source: source ?? "all",
29
+ hits: [],
30
+ warnings: ["No stash sources configured. Run `akm init` first."],
31
+ timing: { totalMs: Date.now() - t0 },
32
+ };
33
+ }
21
34
  const stashDir = sources[0].path;
22
35
  const localResult = source === "registry"
23
36
  ? undefined
@@ -28,11 +41,9 @@ export async function agentikitSearch(input) {
28
41
  usageMode,
29
42
  stashDir,
30
43
  sources,
44
+ config,
31
45
  });
32
- const config = loadConfig();
33
- const registryResult = source === "local"
34
- ? undefined
35
- : await searchRegistry(query, { limit, registryUrls: config.registryUrls });
46
+ const registryResult = source === "local" ? undefined : await searchRegistry(query, { limit, registryUrls: config.registryUrls });
36
47
  if (source === "local") {
37
48
  return {
38
49
  stashDir,
@@ -40,11 +51,12 @@ export async function agentikitSearch(input) {
40
51
  hits: localResult?.hits ?? [],
41
52
  usageGuide: localResult?.usageGuide,
42
53
  tip: localResult?.tip,
54
+ warnings: localResult?.warnings,
43
55
  timing: { totalMs: Date.now() - t0, rankMs: localResult?.rankMs, embedMs: localResult?.embedMs },
44
56
  };
45
57
  }
46
58
  const registryHits = (registryResult?.hits ?? []).map((hit) => {
47
- const installRef = hit.source === "npm" ? `npm:${hit.ref}` : `github:${hit.ref}`;
59
+ const installRef = hit.source === "npm" ? `npm:${hit.ref}` : hit.source === "git" ? `git+${hit.ref}` : `github:${hit.ref}`;
48
60
  return {
49
61
  hitSource: "registry",
50
62
  type: "registry",
@@ -73,19 +85,19 @@ export async function agentikitSearch(input) {
73
85
  };
74
86
  }
75
87
  const mergedHits = mergeSearchHits(localResult?.hits ?? [], registryHits, limit);
88
+ const warnings = [...(localResult?.warnings ?? []), ...(registryResult?.warnings ?? [])];
76
89
  return {
77
90
  stashDir,
78
91
  source,
79
92
  hits: mergedHits,
80
93
  usageGuide: localResult?.usageGuide,
81
94
  tip: mergedHits.length === 0 ? "No matching stash assets or registry entries were found." : undefined,
82
- warnings: registryResult?.warnings.length ? registryResult.warnings : undefined,
95
+ warnings: warnings.length ? warnings : undefined,
83
96
  timing: { totalMs: Date.now() - t0 },
84
97
  };
85
98
  }
86
99
  async function searchLocal(input) {
87
- const { query, searchType, limit, usageMode, stashDir, sources } = input;
88
- const config = loadConfig();
100
+ const { query, searchType, limit, usageMode, stashDir, sources, config } = input;
89
101
  const allStashDirs = sources.map((s) => s.path);
90
102
  // Try to open the database
91
103
  const dbPath = getDbPath();
@@ -101,7 +113,9 @@ async function searchLocal(input) {
101
113
  return {
102
114
  hits,
103
115
  usageGuide,
104
- tip: hits.length === 0 ? "No matching stash assets were found. Try running 'akm index' to rebuild." : undefined,
116
+ tip: hits.length === 0
117
+ ? "No matching stash assets were found. Try running 'akm index' to rebuild."
118
+ : undefined,
105
119
  embedMs,
106
120
  rankMs,
107
121
  };
@@ -113,12 +127,14 @@ async function searchLocal(input) {
113
127
  }
114
128
  }
115
129
  catch (error) {
116
- console.warn("Search index unavailable, falling back to substring search:", error instanceof Error ? error.message : String(error));
130
+ warn("Search index unavailable, falling back to substring search:", error instanceof Error ? error.message : String(error));
117
131
  }
118
132
  const hits = allStashDirs
119
- .flatMap((dir) => substringSearch(query, searchType, limit, dir, sources))
133
+ .flatMap((dir) => substringSearch(query, searchType, limit, dir, sources, config))
120
134
  .slice(0, limit);
121
- const usageGuide = shouldIncludeUsageGuide(usageMode) ? buildUsageGuide(hits.map((hit) => hit.type), searchType) : undefined;
135
+ const usageGuide = shouldIncludeUsageGuide(usageMode)
136
+ ? buildUsageGuide(hits.map((hit) => hit.type), searchType)
137
+ : undefined;
122
138
  return {
123
139
  hits,
124
140
  usageGuide,
@@ -142,6 +158,7 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allStashDi
142
158
  allStashDirs,
143
159
  sources,
144
160
  includeItemUsage: shouldIncludeItemUsage(usageMode),
161
+ config,
145
162
  }));
146
163
  return {
147
164
  hits,
@@ -157,58 +174,67 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allStashDi
157
174
  const tRank0 = Date.now();
158
175
  const typeFilter = searchType === "any" ? undefined : searchType;
159
176
  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 });
177
+ // Reciprocal Rank Fusion (RRF) constant
178
+ const RRF_K = 60;
179
+ // Build FTS rank map: rank 1 = best BM25, rank 2 = second best, etc.
180
+ // FTS results are already sorted by bm25Score (ascending, more negative = better)
181
+ const ftsRankMap = new Map();
182
+ for (let i = 0; i < ftsResults.length; i++) {
183
+ const r = ftsResults[i];
184
+ ftsRankMap.set(r.id, { rank: i + 1, result: r });
185
+ }
186
+ // Build embedding rank map: sort by cosine similarity descending
187
+ const embedRankMap = new Map();
188
+ if (embeddingScores) {
189
+ const sortedEmbeddings = [...embeddingScores.entries()].sort((a, b) => b[1] - a[1]);
190
+ for (let i = 0; i < sortedEmbeddings.length; i++) {
191
+ embedRankMap.set(sortedEmbeddings[i][0], i + 1);
192
+ }
167
193
  }
168
- // Blend scores
194
+ // Merge results using RRF
169
195
  const scored = [];
170
196
  const seenIds = new Set();
171
197
  // Process FTS results
172
- for (const [id, { score: ftsScore, result }] of ftsScoreMap) {
198
+ for (const [id, { rank, result }] of ftsRankMap) {
173
199
  seenIds.add(id);
174
- const embScore = embeddingScores?.get(id);
175
- if (embScore !== undefined) {
176
- const blended = embScore * 0.7 + ftsScore * 0.3;
177
- if (blended > 0)
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" });
182
- }
200
+ const ftsRrf = 1 / (RRF_K + rank);
201
+ const embedRank = embedRankMap.get(id);
202
+ const embedRrf = embedRank !== undefined ? 1 / (RRF_K + embedRank) : 0;
203
+ const rrfScore = ftsRrf + embedRrf;
204
+ const rankingMode = embedRrf > 0 ? "semantic" : "fts";
205
+ scored.push({ id, entry: result.entry, filePath: result.filePath, score: rrfScore, rankingMode });
183
206
  }
184
207
  // Add vec-only results not already in FTS results
185
208
  if (embeddingScores) {
186
- for (const [id, embScore] of embeddingScores) {
209
+ for (const [id] of embeddingScores) {
187
210
  if (seenIds.has(id))
188
211
  continue;
212
+ const embedRank = embedRankMap.get(id);
189
213
  const found = getEntryById(db, id);
190
214
  if (found) {
191
215
  if (typeFilter && found.entry.type !== typeFilter)
192
216
  continue;
217
+ const rrfScore = 1 / (RRF_K + embedRank);
193
218
  scored.push({
194
219
  id,
195
220
  entry: found.entry,
196
221
  filePath: found.filePath,
197
- score: embScore,
222
+ score: rrfScore,
198
223
  rankingMode: "semantic",
199
224
  });
200
225
  }
201
226
  }
202
227
  }
203
- // Apply boosts (tag, intent, name matches)
228
+ // Apply boosts as multiplicative factors
204
229
  const queryTokens = query.toLowerCase().split(/\s+/).filter(Boolean);
205
230
  for (const item of scored) {
206
231
  const entry = item.entry;
232
+ let boostSum = 0;
207
233
  // Tag boost
208
234
  if (entry.tags) {
209
235
  for (const tag of entry.tags) {
210
236
  if (queryTokens.some((t) => tag.toLowerCase() === t)) {
211
- item.score += 0.15;
237
+ boostSum += 0.15;
212
238
  }
213
239
  }
214
240
  }
@@ -218,7 +244,7 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allStashDi
218
244
  const intentLower = intent.toLowerCase();
219
245
  for (const token of queryTokens) {
220
246
  if (intentLower.includes(token)) {
221
- item.score += 0.12;
247
+ boostSum += 0.12;
222
248
  break;
223
249
  }
224
250
  }
@@ -227,11 +253,9 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allStashDi
227
253
  // Name boost
228
254
  const nameLower = entry.name.toLowerCase().replace(/[-_]/g, " ");
229
255
  if (queryTokens.some((t) => nameLower.includes(t))) {
230
- item.score += 0.1;
256
+ boostSum += 0.1;
231
257
  }
232
- }
233
- for (const item of scored) {
234
- item.score = Math.min(item.score, 1.0);
258
+ item.score = item.score * (1 + boostSum);
235
259
  }
236
260
  scored.sort((a, b) => b.score - a.score);
237
261
  const rankMs = Date.now() - tRank0;
@@ -246,6 +270,7 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allStashDi
246
270
  allStashDirs,
247
271
  sources,
248
272
  includeItemUsage: shouldIncludeItemUsage(usageMode),
273
+ config,
249
274
  }));
250
275
  return {
251
276
  embedMs,
@@ -258,7 +283,7 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allStashDi
258
283
  }
259
284
  // ── Vector scorer ───────────────────────────────────────────────────────────
260
285
  async function tryVecScores(db, query, k, config) {
261
- if (!config.semanticSearch || !isVecAvailable())
286
+ if (!config.semanticSearch)
262
287
  return null;
263
288
  const hasEmbeddings = getMeta(db, "hasEmbeddings");
264
289
  if (hasEmbeddings !== "1")
@@ -276,30 +301,30 @@ async function tryVecScores(db, query, k, config) {
276
301
  return scores;
277
302
  }
278
303
  catch (error) {
279
- console.warn("Vector search failed, skipping:", error instanceof Error ? error.message : String(error));
304
+ warn("Vector search failed, skipping:", error instanceof Error ? error.message : String(error));
280
305
  return null;
281
306
  }
282
307
  }
283
308
  // ── Substring fallback (no index) ───────────────────────────────────────────
284
- function substringSearch(query, searchType, limit, stashDir, sources) {
309
+ function substringSearch(query, searchType, limit, stashDir, sources, config) {
285
310
  const assets = indexAssets(stashDir, searchType);
286
311
  return assets
287
312
  .filter((asset) => asset.name.toLowerCase().includes(query))
288
313
  .sort(compareAssets)
289
314
  .slice(0, limit)
290
- .map((asset) => assetToSearchHit(asset, stashDir, sources));
315
+ .map((asset) => assetToSearchHit(asset, stashDir, sources, config));
291
316
  }
292
317
  // ── Hit building ────────────────────────────────────────────────────────────
293
318
  function buildDbHit(input) {
294
319
  const entryStashDir = findSourceForPath(input.path, input.sources)?.path ?? input.defaultStashDir;
295
320
  const typeRoot = path.join(entryStashDir, TYPE_DIRS[input.entry.type]);
296
- const openRefName = deriveCanonicalAssetName(input.entry.type, typeRoot, input.path)
297
- ?? input.entry.name;
321
+ const openRefName = deriveCanonicalAssetName(input.entry.type, typeRoot, input.path) ?? input.entry.name;
298
322
  const qualityBoost = input.entry.generated === true ? 0 : 0.05;
299
323
  const confidenceBoost = typeof input.entry.confidence === "number" ? Math.min(0.05, Math.max(0, input.entry.confidence) * 0.05) : 0;
300
- const score = Math.min(Math.round((input.score + qualityBoost + confidenceBoost) * 1000) / 1000, 1.0);
324
+ const score = Math.round(input.score * (1 + qualityBoost + confidenceBoost) * 1000) / 1000;
301
325
  const whyMatched = buildWhyMatched(input.entry, input.query, input.rankingMode, qualityBoost, confidenceBoost);
302
326
  const source = findSourceForPath(input.path, input.sources);
327
+ const editable = isEditable(input.path, input.config);
303
328
  const hit = {
304
329
  hitSource: "local",
305
330
  type: input.entry.type,
@@ -307,7 +332,8 @@ function buildDbHit(input) {
307
332
  path: input.path,
308
333
  openRef: makeAssetRef(input.entry.type, openRefName, source?.registryId),
309
334
  registryId: source?.registryId,
310
- editable: source?.writable ?? false,
335
+ editable,
336
+ ...(!editable ? { editHint: buildEditHint(input.path, input.entry.type, openRefName, source?.registryId) } : {}),
311
337
  description: input.entry.description,
312
338
  tags: input.entry.tags,
313
339
  score,
@@ -344,8 +370,9 @@ function buildWhyMatched(entry, query, rankingMode, qualityBoost, confidenceBoos
344
370
  return reasons;
345
371
  }
346
372
  // ── Helpers ─────────────────────────────────────────────────────────────────
347
- function assetToSearchHit(asset, stashDir, sources) {
373
+ function assetToSearchHit(asset, stashDir, sources, config) {
348
374
  const source = findSourceForPath(asset.path, sources);
375
+ const editable = isEditable(asset.path, config);
349
376
  const hit = {
350
377
  hitSource: "local",
351
378
  type: asset.type,
@@ -353,7 +380,8 @@ function assetToSearchHit(asset, stashDir, sources) {
353
380
  path: asset.path,
354
381
  openRef: makeAssetRef(asset.type, asset.name, source?.registryId),
355
382
  registryId: source?.registryId,
356
- editable: source?.writable ?? false,
383
+ editable,
384
+ ...(!editable ? { editHint: buildEditHint(asset.path, asset.type, asset.name, source?.registryId) } : {}),
357
385
  };
358
386
  const handler = tryGetHandler(asset.type);
359
387
  if (handler?.enrichSearchHit) {
@@ -373,32 +401,19 @@ function parseSearchUsageMode(mode) {
373
401
  }
374
402
  if (typeof mode === "undefined")
375
403
  return "both";
376
- throw new Error(`Invalid usage mode: ${String(mode)}. Expected one of: none|both|item|guide`);
404
+ throw new UsageError(`Invalid usage mode: ${String(mode)}. Expected one of: none|both|item|guide`);
377
405
  }
378
406
  function parseSearchSource(source) {
379
407
  if (source === "local" || source === "registry" || source === "both")
380
408
  return source;
381
409
  if (typeof source === "undefined")
382
410
  return "local";
383
- throw new Error(`Invalid search source: ${String(source)}. Expected one of: local|registry|both`);
411
+ throw new UsageError(`Invalid search source: ${String(source)}. Expected one of: local|registry|both`);
384
412
  }
385
413
  function mergeSearchHits(localHits, registryHits, limit) {
386
- const merged = [];
387
- let localIndex = 0;
388
- let registryIndex = 0;
389
- while (merged.length < limit && (localIndex < localHits.length || registryIndex < registryHits.length)) {
390
- if (localIndex < localHits.length) {
391
- merged.push(localHits[localIndex]);
392
- localIndex += 1;
393
- if (merged.length >= limit)
394
- break;
395
- }
396
- if (registryIndex < registryHits.length) {
397
- merged.push(registryHits[registryIndex]);
398
- registryIndex += 1;
399
- }
400
- }
401
- return merged;
414
+ const all = [...localHits, ...registryHits];
415
+ all.sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
416
+ return all.slice(0, limit);
402
417
  }
403
418
  function shouldIncludeUsageGuide(mode) {
404
419
  return mode === "both" || mode === "guide";
@@ -0,0 +1,74 @@
1
+ import fs from "node:fs";
2
+ import { getHandler } from "./asset-type-handler";
3
+ import { loadConfig } from "./config";
4
+ import { NotFoundError } from "./errors";
5
+ import { buildFileContext, buildRenderContext, getRenderer, runMatchers } from "./file-context";
6
+ import { resolveSourcesForOrigin } from "./origin-resolve";
7
+ import { parseAssetRef } from "./stash-ref";
8
+ import { resolveAssetPath } from "./stash-resolve";
9
+ import { buildEditHint, findSourceForPath, isEditable, resolveStashSources } from "./stash-source";
10
+ export async function agentikitShow(input) {
11
+ const parsed = parseAssetRef(input.ref);
12
+ const config = loadConfig();
13
+ const allSources = resolveStashSources();
14
+ const searchSources = resolveSourcesForOrigin(parsed.origin, allSources);
15
+ const allStashDirs = searchSources.map((s) => s.path);
16
+ let assetPath;
17
+ let lastError;
18
+ for (const dir of allStashDirs) {
19
+ try {
20
+ assetPath = resolveAssetPath(dir, parsed.type, parsed.name);
21
+ break;
22
+ }
23
+ catch (err) {
24
+ lastError = err instanceof Error ? err : new Error(String(err));
25
+ }
26
+ }
27
+ if (!assetPath && parsed.origin && searchSources.length === 0) {
28
+ const installCmd = `akm add ${parsed.origin}`;
29
+ throw new NotFoundError(`Stash asset not found for ref: ${parsed.type}:${parsed.name}. ` +
30
+ `Kit "${parsed.origin}" is not installed. Run: ${installCmd}`);
31
+ }
32
+ if (!assetPath) {
33
+ throw lastError ?? new NotFoundError(`Stash asset not found for ref: ${parsed.type}:${parsed.name}`);
34
+ }
35
+ const source = findSourceForPath(assetPath, allSources);
36
+ const sourceStashDir = source?.path ?? allStashDirs[0];
37
+ // Try new renderer pipeline first
38
+ if (sourceStashDir) {
39
+ const fileCtx = buildFileContext(sourceStashDir, assetPath);
40
+ const match = runMatchers(fileCtx);
41
+ if (match) {
42
+ match.meta = { ...match.meta, name: parsed.name, view: input.view };
43
+ const renderer = getRenderer(match.renderer);
44
+ if (renderer) {
45
+ const renderCtx = buildRenderContext(fileCtx, match, allStashDirs);
46
+ const response = renderer.buildShowResponse(renderCtx);
47
+ const editable = isEditable(assetPath, config);
48
+ return {
49
+ ...response,
50
+ registryId: source?.registryId,
51
+ editable,
52
+ ...(!editable ? { editHint: buildEditHint(assetPath, parsed.type, parsed.name, source?.registryId) } : {}),
53
+ };
54
+ }
55
+ }
56
+ }
57
+ // Fallback to legacy handler
58
+ const content = fs.readFileSync(assetPath, "utf8");
59
+ const handler = getHandler(parsed.type);
60
+ const response = handler.buildShowResponse({
61
+ name: parsed.name,
62
+ path: assetPath,
63
+ content,
64
+ view: input.view,
65
+ stashDirs: allStashDirs,
66
+ });
67
+ const editable = isEditable(assetPath, config);
68
+ return {
69
+ ...response,
70
+ registryId: source?.registryId,
71
+ editable,
72
+ ...(!editable ? { editHint: buildEditHint(assetPath, parsed.type, parsed.name, source?.registryId) } : {}),
73
+ };
74
+ }
@@ -0,0 +1,127 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { resolveStashDir } from "./common";
4
+ import { loadConfig } from "./config";
5
+ import { warn } from "./warn";
6
+ // ── Resolution ──────────────────────────────────────────────────────────────
7
+ /**
8
+ * Build the ordered list of stash sources (search paths):
9
+ * 1. Primary stash dir (user's own, destination for clone)
10
+ * 2. Additional search paths (user-configured)
11
+ * 3. Installed kit paths (cache-managed, from registry)
12
+ *
13
+ * The first entry is always the primary stash. Additional entries come
14
+ * from `searchPaths` config and `registry.installed` entries.
15
+ */
16
+ export function resolveStashSources(overrideStashDir, existingConfig) {
17
+ const stashDir = overrideStashDir ?? resolveStashDir();
18
+ const config = existingConfig ?? loadConfig();
19
+ const sources = [{ path: stashDir }];
20
+ for (const dir of config.searchPaths) {
21
+ if (isSuspiciousStashRoot(dir)) {
22
+ warn(`Warning: stash root "${dir}" appears to be a system directory. This may be unintentional.`);
23
+ }
24
+ if (isValidDirectory(dir)) {
25
+ sources.push({ path: dir });
26
+ }
27
+ }
28
+ for (const entry of config.registry?.installed ?? []) {
29
+ if (isSuspiciousStashRoot(entry.stashRoot)) {
30
+ warn(`Warning: stash root "${entry.stashRoot}" appears to be a system directory. This may be unintentional.`);
31
+ }
32
+ if (isValidDirectory(entry.stashRoot)) {
33
+ sources.push({
34
+ path: entry.stashRoot,
35
+ registryId: entry.id,
36
+ });
37
+ }
38
+ }
39
+ return sources;
40
+ }
41
+ /**
42
+ * Convenience: returns just the directory paths, preserving priority order.
43
+ */
44
+ export function resolveAllStashDirs(overrideStashDir) {
45
+ return resolveStashSources(overrideStashDir).map((s) => s.path);
46
+ }
47
+ /**
48
+ * Find which source a file path belongs to.
49
+ */
50
+ export function findSourceForPath(filePath, sources) {
51
+ const resolved = path.resolve(filePath);
52
+ for (const source of sources) {
53
+ if (resolved.startsWith(path.resolve(source.path) + path.sep))
54
+ return source;
55
+ }
56
+ return undefined;
57
+ }
58
+ /**
59
+ * Return the primary stash source (first entry in the list).
60
+ * This is the user's working stash and the default destination for clone.
61
+ */
62
+ export function getPrimarySource(sources) {
63
+ return sources[0];
64
+ }
65
+ // ── Editability ─────────────────────────────────────────────────────────────
66
+ /**
67
+ * Determine whether a file is safe to edit in place.
68
+ *
69
+ * The only files that are NOT editable are those inside a cache directory
70
+ * managed by the package manager (`registry.installed[].cacheDir`). These
71
+ * will be overwritten by `akm update` without warning.
72
+ *
73
+ * Everything else — working stash, search paths, local project dirs — is
74
+ * the user's domain to manage.
75
+ */
76
+ export function isEditable(filePath, config) {
77
+ const cfg = config ?? loadConfig();
78
+ const resolved = path.resolve(filePath);
79
+ const cacheManaged = cfg.registry?.installed ?? [];
80
+ const isWin = process.platform === "win32";
81
+ for (const entry of cacheManaged) {
82
+ const cacheRoot = path.resolve(entry.cacheDir);
83
+ if (isWin) {
84
+ // Windows paths are case-insensitive — normalize both sides
85
+ if (resolved.toLowerCase().startsWith(cacheRoot.toLowerCase() + path.sep))
86
+ return false;
87
+ }
88
+ else {
89
+ if (resolved.startsWith(cacheRoot + path.sep))
90
+ return false;
91
+ }
92
+ }
93
+ return true;
94
+ }
95
+ /**
96
+ * Build an actionable hint for the agent when a file is not editable.
97
+ * Callers must check `isEditable()` before calling — this function
98
+ * unconditionally returns the hint string.
99
+ */
100
+ export function buildEditHint(filePath, assetType, assetName, origin) {
101
+ const ref = origin ? `${origin}//${assetType}:${assetName}` : `${assetType}:${assetName}`;
102
+ return `This asset is managed by akm and may be overwritten on update. To edit, run: akm clone ${ref}`;
103
+ }
104
+ // ── Validation ──────────────────────────────────────────────────────────────
105
+ const SUSPICIOUS_ROOTS = new Set(["/", "/etc", "/bin", "/sbin", "/usr", "/var", "/tmp", "/dev", "/proc", "/sys"]);
106
+ function isSuspiciousStashRoot(dir) {
107
+ const resolved = path.resolve(dir);
108
+ const normalized = process.platform === "win32" ? resolved.toLowerCase() : resolved;
109
+ if (SUSPICIOUS_ROOTS.has(normalized))
110
+ return true;
111
+ if (process.platform === "win32") {
112
+ // Check for Windows system directories
113
+ const winDir = (process.env.SystemRoot || "C:\\Windows").toLowerCase();
114
+ if (normalized === winDir || normalized.startsWith(winDir + path.sep))
115
+ return true;
116
+ }
117
+ return false;
118
+ }
119
+ // ── Helpers ─────────────────────────────────────────────────────────────────
120
+ function isValidDirectory(dir) {
121
+ try {
122
+ return fs.statSync(dir).isDirectory();
123
+ }
124
+ catch {
125
+ return false;
126
+ }
127
+ }