agentikit 0.0.7 → 0.0.9

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 (98) hide show
  1. package/README.md +215 -76
  2. package/dist/index.d.ts +17 -3
  3. package/dist/index.js +10 -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 +268 -57
  7. package/dist/src/common.d.ts +8 -0
  8. package/dist/src/common.js +46 -0
  9. package/dist/src/config.d.ts +37 -0
  10. package/dist/src/config.js +124 -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 +11 -2
  24. package/dist/src/metadata.js +161 -29
  25. package/dist/src/registry-install.d.ts +11 -0
  26. package/dist/src/registry-install.js +208 -0
  27. package/dist/src/registry-resolve.d.ts +3 -0
  28. package/dist/src/registry-resolve.js +231 -0
  29. package/dist/src/registry-search.d.ts +5 -0
  30. package/dist/src/registry-search.js +129 -0
  31. package/dist/src/registry-types.d.ts +55 -0
  32. package/dist/src/registry-types.js +1 -0
  33. package/dist/src/ripgrep-install.d.ts +12 -0
  34. package/dist/src/ripgrep-install.js +169 -0
  35. package/dist/src/ripgrep-resolve.d.ts +13 -0
  36. package/dist/src/ripgrep-resolve.js +68 -0
  37. package/dist/src/ripgrep.d.ts +3 -36
  38. package/dist/src/ripgrep.js +2 -262
  39. package/dist/src/similarity.d.ts +1 -2
  40. package/dist/src/similarity.js +11 -0
  41. package/dist/src/stash-add.d.ts +4 -0
  42. package/dist/src/stash-add.js +59 -0
  43. package/dist/src/stash-ref.d.ts +7 -0
  44. package/dist/src/stash-ref.js +33 -0
  45. package/dist/src/stash-registry.d.ts +18 -0
  46. package/dist/src/stash-registry.js +221 -0
  47. package/dist/src/stash-resolve.d.ts +2 -0
  48. package/dist/src/stash-resolve.js +45 -0
  49. package/dist/src/stash-search.d.ts +8 -0
  50. package/dist/src/stash-search.js +484 -0
  51. package/dist/src/stash-show.d.ts +5 -0
  52. package/dist/src/stash-show.js +114 -0
  53. package/dist/src/stash-types.d.ts +217 -0
  54. package/dist/src/stash-types.js +1 -0
  55. package/dist/src/stash.d.ts +10 -63
  56. package/dist/src/stash.js +6 -633
  57. package/dist/src/tool-runner.d.ts +35 -0
  58. package/dist/src/tool-runner.js +100 -0
  59. package/dist/src/walker.d.ts +19 -0
  60. package/dist/src/walker.js +47 -0
  61. package/package.json +8 -14
  62. package/src/asset-spec.ts +69 -0
  63. package/src/cli.ts +282 -46
  64. package/src/common.ts +58 -0
  65. package/src/config.ts +183 -0
  66. package/src/embedder.ts +117 -0
  67. package/src/frontmatter.ts +95 -0
  68. package/src/indexer.ts +244 -84
  69. package/src/init.ts +106 -0
  70. package/src/llm.ts +124 -0
  71. package/src/markdown.ts +106 -0
  72. package/src/metadata.ts +171 -27
  73. package/src/registry-install.ts +245 -0
  74. package/src/registry-resolve.ts +272 -0
  75. package/src/registry-search.ts +145 -0
  76. package/src/registry-types.ts +64 -0
  77. package/src/ripgrep-install.ts +200 -0
  78. package/src/ripgrep-resolve.ts +72 -0
  79. package/src/ripgrep.ts +3 -315
  80. package/src/similarity.ts +13 -1
  81. package/src/stash-add.ts +66 -0
  82. package/src/stash-ref.ts +41 -0
  83. package/src/stash-registry.ts +259 -0
  84. package/src/stash-resolve.ts +47 -0
  85. package/src/stash-search.ts +595 -0
  86. package/src/stash-show.ts +112 -0
  87. package/src/stash-types.ts +221 -0
  88. package/src/stash.ts +31 -760
  89. package/src/tool-runner.ts +129 -0
  90. package/src/walker.ts +53 -0
  91. package/.claude-plugin/plugin.json +0 -21
  92. package/commands/open.md +0 -11
  93. package/commands/run.md +0 -11
  94. package/commands/search.md +0 -11
  95. package/dist/src/plugin.d.ts +0 -2
  96. package/dist/src/plugin.js +0 -55
  97. package/skills/stash/SKILL.md +0 -73
  98. package/src/plugin.ts +0 -56
@@ -0,0 +1,595 @@
1
+ import fs from "node:fs"
2
+ import path from "node:path"
3
+ import { type AgentikitAssetType, hasErrnoCode, resolveStashDir } from "./common"
4
+ import { ASSET_TYPES, TYPE_DIRS, deriveCanonicalAssetName } from "./asset-spec"
5
+ import { loadSearchIndex, buildSearchText, type IndexedEntry } from "./indexer"
6
+ import { TfIdfAdapter, type ScoredEntry } from "./similarity"
7
+ import { buildToolInfo } from "./tool-runner"
8
+ import { walkStash } from "./walker"
9
+ import { makeOpenRef } from "./stash-ref"
10
+ import type {
11
+ AgentikitSearchType,
12
+ LocalSearchHit,
13
+ RegistrySearchResultHit,
14
+ SearchHit,
15
+ SearchResponse,
16
+ SearchSource,
17
+ SearchUsageMode,
18
+ } from "./stash-types"
19
+ import { loadConfig } from "./config"
20
+ import { searchRegistry } from "./registry-search"
21
+
22
+ type IndexedAsset = {
23
+ type: AgentikitAssetType
24
+ name: string
25
+ path: string
26
+ }
27
+
28
+ const DEFAULT_LIMIT = 20
29
+
30
+ const DEFAULT_USAGE_GUIDE_BY_TYPE: Record<AgentikitAssetType, string[]> = {
31
+ tool: [
32
+ "Use the hit's runCmd for execution so runtime and working directory stay correct.",
33
+ "Use `akm show <openRef>` to inspect the tool before running it.",
34
+ ],
35
+ skill: [
36
+ "Read and apply the skill instructions as written, then adapt examples to your current repo state and task.",
37
+ "Use `akm show <openRef>` to read the full SKILL.md for required steps and constraints.",
38
+ ],
39
+ command: [
40
+ "Read the .md file, fill placeholders, and run it in the current repo context.",
41
+ "Use `akm show <openRef>` to retrieve the command template body.",
42
+ ],
43
+ agent: [
44
+ "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.",
45
+ "Use with `akm show <openRef>` to get the full prompt payload.",
46
+ ],
47
+ knowledge: [
48
+ "Use `akm show <openRef>` to read the document; start with `--view toc` for large files.",
49
+ "Use `--view section` or `--view lines` to load only the part you need.",
50
+ ],
51
+ }
52
+
53
+ export async function agentikitSearch(input: {
54
+ query: string
55
+ type?: AgentikitSearchType
56
+ limit?: number
57
+ usage?: SearchUsageMode
58
+ source?: SearchSource
59
+ }): Promise<SearchResponse> {
60
+ const t0 = Date.now()
61
+ const query = input.query.trim()
62
+ const normalizedQuery = query.toLowerCase()
63
+ const searchType = input.type ?? "any"
64
+ const limit = normalizeLimit(input.limit)
65
+ const usageMode = parseSearchUsageMode(input.usage)
66
+ const source = parseSearchSource(input.source)
67
+ const stashDir = resolveStashDir()
68
+ const localResult = source === "registry"
69
+ ? undefined
70
+ : await searchLocal({
71
+ query: normalizedQuery,
72
+ searchType,
73
+ limit,
74
+ usageMode,
75
+ stashDir,
76
+ })
77
+
78
+ const registryResult = source === "local"
79
+ ? undefined
80
+ : await searchRegistry(query, { limit })
81
+
82
+ if (source === "local") {
83
+ return {
84
+ stashDir,
85
+ source,
86
+ hits: localResult?.hits ?? [],
87
+ usageGuide: localResult?.usageGuide,
88
+ tip: localResult?.tip,
89
+ timing: { totalMs: Date.now() - t0, rankMs: localResult?.rankMs, embedMs: localResult?.embedMs },
90
+ }
91
+ }
92
+
93
+ const registryHits = (registryResult?.hits ?? []).map((hit): RegistrySearchResultHit => {
94
+ const installRef = hit.source === "npm" ? `npm:${hit.ref}` : `github:${hit.ref}`
95
+ return {
96
+ hitSource: "registry",
97
+ type: "registry",
98
+ name: hit.title,
99
+ id: hit.id,
100
+ registrySource: hit.source,
101
+ ref: hit.ref,
102
+ description: hit.description,
103
+ homepage: hit.homepage,
104
+ score: hit.score,
105
+ metadata: hit.metadata,
106
+ installRef,
107
+ installCmd: `akm add ${installRef}`,
108
+ }
109
+ })
110
+
111
+ if (source === "registry") {
112
+ const hits = registryHits.slice(0, limit)
113
+ return {
114
+ stashDir,
115
+ source,
116
+ hits,
117
+ tip: hits.length === 0 ? "No matching registry entries were found." : undefined,
118
+ warnings: registryResult?.warnings.length ? registryResult.warnings : undefined,
119
+ timing: { totalMs: Date.now() - t0 },
120
+ }
121
+ }
122
+
123
+ const mergedHits = mergeSearchHits(localResult?.hits ?? [], registryHits, limit)
124
+
125
+ return {
126
+ stashDir,
127
+ source,
128
+ hits: mergedHits,
129
+ usageGuide: localResult?.usageGuide,
130
+ tip: mergedHits.length === 0 ? "No matching stash assets or registry entries were found." : undefined,
131
+ warnings: registryResult?.warnings.length ? registryResult.warnings : undefined,
132
+ timing: { totalMs: Date.now() - t0 },
133
+ }
134
+ }
135
+
136
+ async function searchLocal(input: {
137
+ query: string
138
+ searchType: AgentikitSearchType
139
+ limit: number
140
+ usageMode: SearchUsageMode
141
+ stashDir: string
142
+ }): Promise<{ hits: LocalSearchHit[]; usageGuide?: Partial<Record<AgentikitAssetType, string[]>>; tip?: string; embedMs?: number; rankMs?: number }> {
143
+ const { query, searchType, limit, usageMode, stashDir } = input
144
+ const config = loadConfig(stashDir)
145
+ const allStashDirs = [
146
+ stashDir,
147
+ ...config.additionalStashDirs.filter((d) => {
148
+ try { return fs.statSync(d).isDirectory() } catch { return false }
149
+ }),
150
+ ]
151
+
152
+ const index = loadSearchIndex()
153
+ if (index && index.entries && index.entries.length > 0 && index.stashDir === stashDir) {
154
+ const { hits, usageGuide, embedMs, rankMs } = await searchIndex(index, query, searchType, limit, stashDir, allStashDirs, config, usageMode)
155
+ return {
156
+ hits,
157
+ usageGuide,
158
+ tip: hits.length === 0 ? "No matching stash assets were found. Try running 'akm index' to rebuild." : undefined,
159
+ embedMs,
160
+ rankMs,
161
+ }
162
+ }
163
+
164
+ const hits = allStashDirs
165
+ .flatMap((dir) => substringSearch(query, searchType, limit, dir))
166
+ .slice(0, limit)
167
+ const usageGuide = shouldIncludeUsageGuide(usageMode) ? buildUsageGuide(hits.map((hit) => hit.type), searchType) : undefined
168
+ return {
169
+ hits,
170
+ usageGuide,
171
+ tip: hits.length === 0 ? "No matching stash assets were found. Try running 'akm index' to rebuild." : undefined,
172
+ }
173
+ }
174
+
175
+ // ── Unified indexed search ──────────────────────────────────────────────────
176
+
177
+ async function searchIndex(
178
+ index: import("./indexer").SearchIndex,
179
+ query: string,
180
+ searchType: AgentikitSearchType,
181
+ limit: number,
182
+ stashDir: string,
183
+ allStashDirs: string[],
184
+ config: import("./config").AgentikitConfig,
185
+ usageMode: SearchUsageMode,
186
+ ): Promise<{ hits: LocalSearchHit[]; usageGuide?: Partial<Record<AgentikitAssetType, string[]>>; embedMs?: number; rankMs?: number }> {
187
+ // Filter candidates by type
188
+ let candidates = index.entries
189
+ if (searchType !== "any") {
190
+ candidates = candidates.filter((ie) => ie.entry.type === searchType)
191
+ }
192
+
193
+ if (candidates.length === 0) {
194
+ return {
195
+ hits: [],
196
+ usageGuide: shouldIncludeUsageGuide(usageMode) ? buildUsageGuide([], searchType) : undefined,
197
+ }
198
+ }
199
+
200
+ // Empty query: return all entries (no scoring needed)
201
+ if (!query) {
202
+ const selectedCandidates = candidates.slice(0, limit)
203
+ const hits = selectedCandidates.map((ie) =>
204
+ buildIndexedHit({
205
+ entry: ie.entry,
206
+ path: ie.path,
207
+ score: 1,
208
+ query,
209
+ rankingMode: "tfidf",
210
+ defaultStashDir: stashDir,
211
+ allStashDirs,
212
+ includeItemUsage: shouldIncludeItemUsage(usageMode),
213
+ }),
214
+ )
215
+ return {
216
+ hits,
217
+ usageGuide: shouldIncludeUsageGuide(usageMode)
218
+ ? buildUsageGuideFromEntries(selectedCandidates.map((candidate) => candidate.entry), searchType)
219
+ : undefined,
220
+ }
221
+ }
222
+
223
+ // Score each candidate using available signals
224
+ const tEmbed0 = Date.now()
225
+ const embeddingScores = await tryEmbeddingScores(candidates, query, config)
226
+ const embedMs = Date.now() - tEmbed0
227
+
228
+ const tRank0 = Date.now()
229
+ const tfidfScores = computeTfidfScores(index, candidates, query, searchType)
230
+
231
+ const scored: Array<{ ie: IndexedEntry; score: number; rankingMode: "semantic" | "tfidf" }> = []
232
+
233
+ for (const ie of candidates) {
234
+ const key = ie.path
235
+ const embScore = embeddingScores?.get(key)
236
+ const tfidfScore = tfidfScores.get(key) ?? 0
237
+
238
+ if (embScore !== undefined) {
239
+ // Weighted blend: embedding dominates when available, TF-IDF boosts lexical matches
240
+ const blended = embScore * 0.7 + tfidfScore * 0.3
241
+ if (blended > 0) scored.push({ ie, score: blended, rankingMode: "semantic" })
242
+ } else if (tfidfScore > 0) {
243
+ scored.push({ ie, score: tfidfScore, rankingMode: "tfidf" })
244
+ }
245
+ }
246
+
247
+ scored.sort((a, b) => b.score - a.score)
248
+ const rankMs = Date.now() - tRank0
249
+
250
+ const selected = scored.slice(0, limit)
251
+ const hits = selected.map(({ ie, score, rankingMode }) =>
252
+ buildIndexedHit({
253
+ entry: ie.entry,
254
+ path: ie.path,
255
+ score: Math.round(score * 1000) / 1000,
256
+ query,
257
+ rankingMode,
258
+ defaultStashDir: stashDir,
259
+ allStashDirs,
260
+ includeItemUsage: shouldIncludeItemUsage(usageMode),
261
+ }),
262
+ )
263
+
264
+ return {
265
+ embedMs,
266
+ rankMs,
267
+ hits,
268
+ usageGuide: shouldIncludeUsageGuide(usageMode)
269
+ ? buildUsageGuideFromEntries(selected.map((item) => item.ie.entry), searchType)
270
+ : undefined,
271
+ }
272
+ }
273
+
274
+ // ── Embedding scorer ────────────────────────────────────────────────────────
275
+
276
+ async function tryEmbeddingScores(
277
+ candidates: IndexedEntry[],
278
+ query: string,
279
+ config: import("./config").AgentikitConfig,
280
+ ): Promise<Map<string, number> | null> {
281
+ if (!config.semanticSearch) return null
282
+
283
+ const withEmbeddings = candidates.filter((ie) => ie.embedding && ie.embedding.length > 0)
284
+ if (withEmbeddings.length === 0) return null
285
+
286
+ try {
287
+ const { embed, cosineSimilarity } = await import("./embedder.js")
288
+ const queryEmbedding = await embed(query, config.embedding)
289
+ const scores = new Map<string, number>()
290
+ for (const ie of withEmbeddings) {
291
+ scores.set(ie.path, cosineSimilarity(queryEmbedding, ie.embedding!))
292
+ }
293
+ return scores
294
+ } catch {
295
+ return null
296
+ }
297
+ }
298
+
299
+ // ── TF-IDF scorer ───────────────────────────────────────────────────────────
300
+
301
+ function computeTfidfScores(
302
+ index: import("./indexer").SearchIndex,
303
+ candidates: IndexedEntry[],
304
+ query: string,
305
+ searchType: AgentikitSearchType,
306
+ ): Map<string, number> {
307
+ const candidateScoredEntries = toScoredEntries(candidates)
308
+
309
+ let adapter: TfIdfAdapter
310
+ if (index.tfidf) {
311
+ const allScored = toScoredEntries(index.entries)
312
+ adapter = TfIdfAdapter.deserialize(index.tfidf, allScored)
313
+ } else {
314
+ adapter = new TfIdfAdapter()
315
+ adapter.buildIndex(candidateScoredEntries)
316
+ }
317
+
318
+ const typeFilter = searchType === "any" ? undefined : searchType
319
+ const results = adapter.search(query, candidates.length, typeFilter)
320
+
321
+ const scores = new Map<string, number>()
322
+ for (const r of results) {
323
+ scores.set(r.path, r.score)
324
+ }
325
+ return scores
326
+ }
327
+
328
+ // ── Substring fallback (no index) ───────────────────────────────────────────
329
+
330
+ function substringSearch(
331
+ query: string,
332
+ searchType: AgentikitSearchType,
333
+ limit: number,
334
+ stashDir: string,
335
+ ): LocalSearchHit[] {
336
+ const assets = indexAssets(stashDir, searchType)
337
+ return assets
338
+ .filter((asset) => asset.name.toLowerCase().includes(query))
339
+ .sort(compareAssets)
340
+ .slice(0, limit)
341
+ .map((asset) => assetToSearchHit(asset, stashDir))
342
+ }
343
+
344
+ // ── Hit building ────────────────────────────────────────────────────────────
345
+
346
+ function findStashDirForPath(filePath: string, stashDirs: string[]): string | undefined {
347
+ const resolved = path.resolve(filePath)
348
+ for (const dir of stashDirs) {
349
+ if (resolved.startsWith(path.resolve(dir) + path.sep)) return dir
350
+ }
351
+ return undefined
352
+ }
353
+
354
+ function buildIndexedHit(input: {
355
+ entry: IndexedEntry["entry"]
356
+ path: string
357
+ score: number
358
+ query: string
359
+ rankingMode: "semantic" | "tfidf"
360
+ defaultStashDir: string
361
+ allStashDirs: string[]
362
+ includeItemUsage: boolean
363
+ }): LocalSearchHit {
364
+ const entryStashDir = findStashDirForPath(input.path, input.allStashDirs) ?? input.defaultStashDir
365
+ const typeRoot = path.join(entryStashDir, TYPE_DIRS[input.entry.type])
366
+ const openRefName = deriveCanonicalAssetName(input.entry.type, typeRoot, input.path)
367
+ ?? input.entry.name
368
+
369
+ const qualityBoost = input.entry.generated === true ? 0 : 0.05
370
+ const confidenceBoost = typeof input.entry.confidence === "number" ? Math.min(0.05, Math.max(0, input.entry.confidence) * 0.05) : 0
371
+ const score = Math.round((input.score + qualityBoost + confidenceBoost) * 1000) / 1000
372
+
373
+ const whyMatched = buildWhyMatched(input.entry, input.query, input.rankingMode, qualityBoost, confidenceBoost)
374
+
375
+ const hit: LocalSearchHit = {
376
+ hitSource: "local",
377
+ type: input.entry.type,
378
+ name: input.entry.name,
379
+ path: input.path,
380
+ openRef: makeOpenRef(input.entry.type, openRefName),
381
+ description: input.entry.description,
382
+ tags: input.entry.tags,
383
+ score,
384
+ whyMatched,
385
+ }
386
+
387
+ if (input.includeItemUsage && input.entry.usage && input.entry.usage.length > 0) {
388
+ hit.usage = input.entry.usage
389
+ }
390
+
391
+ if (input.entry.type === "tool") {
392
+ try {
393
+ const toolInfo = buildToolInfo(entryStashDir, input.path)
394
+ hit.runCmd = toolInfo.runCmd
395
+ hit.kind = toolInfo.kind
396
+ } catch (error: unknown) {
397
+ if (!hasErrnoCode(error, "ENOENT")) throw error
398
+ }
399
+ }
400
+
401
+ return hit
402
+ }
403
+
404
+ function buildWhyMatched(
405
+ entry: IndexedEntry["entry"],
406
+ query: string,
407
+ rankingMode: "semantic" | "tfidf",
408
+ qualityBoost: number,
409
+ confidenceBoost: number,
410
+ ): string[] {
411
+ const reasons: string[] = [rankingMode === "semantic" ? "semantic similarity" : "tf-idf lexical relevance"]
412
+ const tokens = query.toLowerCase().split(/\s+/).filter(Boolean)
413
+
414
+ const name = entry.name.toLowerCase()
415
+ const tags = entry.tags?.join(" ").toLowerCase() ?? ""
416
+ const intents = entry.intents?.join(" ").toLowerCase() ?? ""
417
+ const aliases = entry.aliases?.join(" ").toLowerCase() ?? ""
418
+
419
+ if (tokens.some((t) => name.includes(t))) reasons.push("matched name tokens")
420
+ if (tokens.some((t) => tags.includes(t))) reasons.push("matched tags")
421
+ if (tokens.some((t) => intents.includes(t))) reasons.push("matched intents")
422
+ if (tokens.some((t) => aliases.includes(t))) reasons.push("matched aliases")
423
+ if (qualityBoost > 0) reasons.push("curated metadata boost")
424
+ if (confidenceBoost > 0) reasons.push("metadata confidence boost")
425
+
426
+ return reasons
427
+ }
428
+
429
+ // ── Helpers ─────────────────────────────────────────────────────────────────
430
+
431
+ function toScoredEntries(entries: IndexedEntry[]): ScoredEntry[] {
432
+ return entries.map((ie) => ({
433
+ id: `${ie.entry.type}:${ie.entry.name}`,
434
+ text: buildSearchText(ie.entry),
435
+ entry: ie.entry,
436
+ path: ie.path,
437
+ }))
438
+ }
439
+
440
+ function assetToSearchHit(asset: IndexedAsset, stashDir: string): LocalSearchHit {
441
+ if (asset.type !== "tool") {
442
+ return {
443
+ hitSource: "local",
444
+ type: asset.type,
445
+ name: asset.name,
446
+ path: asset.path,
447
+ openRef: makeOpenRef(asset.type, asset.name),
448
+ }
449
+ }
450
+ const toolInfo = buildToolInfo(stashDir, asset.path)
451
+ return {
452
+ hitSource: "local",
453
+ type: "tool",
454
+ name: asset.name,
455
+ path: asset.path,
456
+ openRef: makeOpenRef("tool", asset.name),
457
+ runCmd: toolInfo.runCmd,
458
+ kind: toolInfo.kind,
459
+ }
460
+ }
461
+
462
+ function normalizeLimit(limit?: number): number {
463
+ if (typeof limit !== "number" || Number.isNaN(limit) || limit <= 0) {
464
+ return DEFAULT_LIMIT
465
+ }
466
+ return Math.min(Math.floor(limit), 200)
467
+ }
468
+
469
+ function parseSearchUsageMode(mode: SearchUsageMode | undefined): SearchUsageMode {
470
+ if (mode === "none" || mode === "both" || mode === "item" || mode === "guide") {
471
+ return mode
472
+ }
473
+ if (typeof mode === "undefined") return "both"
474
+ throw new Error(`Invalid usage mode: ${String(mode)}. Expected one of: none|both|item|guide`)
475
+ }
476
+
477
+ function parseSearchSource(source: SearchSource | undefined): SearchSource {
478
+ if (source === "local" || source === "registry" || source === "both") return source
479
+ if (typeof source === "undefined") return "local"
480
+ throw new Error(`Invalid search source: ${String(source)}. Expected one of: local|registry|both`)
481
+ }
482
+
483
+ function mergeSearchHits(localHits: LocalSearchHit[], registryHits: RegistrySearchResultHit[], limit: number): SearchHit[] {
484
+ const merged: SearchHit[] = []
485
+ let localIndex = 0
486
+ let registryIndex = 0
487
+
488
+ while (merged.length < limit && (localIndex < localHits.length || registryIndex < registryHits.length)) {
489
+ if (localIndex < localHits.length) {
490
+ merged.push(localHits[localIndex])
491
+ localIndex += 1
492
+ if (merged.length >= limit) break
493
+ }
494
+ if (registryIndex < registryHits.length) {
495
+ merged.push(registryHits[registryIndex])
496
+ registryIndex += 1
497
+ }
498
+ }
499
+
500
+ return merged
501
+ }
502
+
503
+ function shouldIncludeUsageGuide(mode: SearchUsageMode): boolean {
504
+ return mode === "both" || mode === "guide"
505
+ }
506
+
507
+ function shouldIncludeItemUsage(mode: SearchUsageMode): boolean {
508
+ return mode === "both" || mode === "item"
509
+ }
510
+
511
+ function buildUsageGuideFromEntries(
512
+ entries: IndexedEntry["entry"][],
513
+ searchType: AgentikitSearchType,
514
+ ): Partial<Record<AgentikitAssetType, string[]>> | undefined {
515
+ const types = entries.map((entry) => entry.type)
516
+ const fallbackGuide = buildUsageGuide(types, searchType)
517
+ const metadataByType = new Map<AgentikitAssetType, string[]>()
518
+
519
+ for (const entry of entries) {
520
+ if (!entry.usage || entry.usage.length === 0) continue
521
+ const current = metadataByType.get(entry.type) ?? []
522
+ for (const item of entry.usage) {
523
+ const trimmed = item.trim()
524
+ if (trimmed && !current.includes(trimmed)) current.push(trimmed)
525
+ }
526
+ if (current.length > 0) metadataByType.set(entry.type, current)
527
+ }
528
+
529
+ if (!fallbackGuide && metadataByType.size === 0) return undefined
530
+
531
+ const result: Partial<Record<AgentikitAssetType, string[]>> = {}
532
+ for (const assetType of resolveGuideTypes(types, searchType)) {
533
+ const lines: string[] = []
534
+ const metadataLines = metadataByType.get(assetType)
535
+ if (metadataLines && metadataLines.length > 0) {
536
+ lines.push(...metadataLines)
537
+ }
538
+ const fallbackLines = fallbackGuide?.[assetType]
539
+ if (fallbackLines && fallbackLines.length > 0) {
540
+ for (const line of fallbackLines) {
541
+ if (!lines.includes(line)) lines.push(line)
542
+ }
543
+ }
544
+ if (lines.length > 0) result[assetType] = lines
545
+ }
546
+
547
+ return Object.keys(result).length > 0 ? result : undefined
548
+ }
549
+
550
+ function buildUsageGuide(
551
+ hitTypes: AgentikitAssetType[],
552
+ searchType: AgentikitSearchType,
553
+ ): Partial<Record<AgentikitAssetType, string[]>> | undefined {
554
+ const result: Partial<Record<AgentikitAssetType, string[]>> = {}
555
+ for (const assetType of resolveGuideTypes(hitTypes, searchType)) {
556
+ result[assetType] = usageGuideByType(assetType)
557
+ }
558
+ return Object.keys(result).length > 0 ? result : undefined
559
+ }
560
+
561
+ function resolveGuideTypes(hitTypes: AgentikitAssetType[], searchType: AgentikitSearchType): AgentikitAssetType[] {
562
+ if (searchType !== "any") return [searchType]
563
+ return Array.from(new Set(hitTypes))
564
+ }
565
+
566
+ function usageGuideByType(type: AgentikitAssetType): string[] {
567
+ return DEFAULT_USAGE_GUIDE_BY_TYPE[type]
568
+ }
569
+
570
+ function fileToAsset(assetType: AgentikitAssetType, root: string, file: string): IndexedAsset | undefined {
571
+ const name = deriveCanonicalAssetName(assetType, root, file)
572
+ if (!name) return undefined
573
+ return { type: assetType, name, path: file }
574
+ }
575
+
576
+ function indexAssets(stashDir: string, type: AgentikitSearchType): IndexedAsset[] {
577
+ const assets: IndexedAsset[] = []
578
+ const types = type === "any" ? ASSET_TYPES : [type]
579
+ for (const assetType of types) {
580
+ const root = path.join(stashDir, TYPE_DIRS[assetType])
581
+ const groups = walkStash(root, assetType)
582
+ for (const { files } of groups) {
583
+ for (const file of files) {
584
+ const asset = fileToAsset(assetType, root, file)
585
+ if (asset) assets.push(asset)
586
+ }
587
+ }
588
+ }
589
+ return assets
590
+ }
591
+
592
+ function compareAssets(a: IndexedAsset, b: IndexedAsset): number {
593
+ if (a.type !== b.type) return a.type.localeCompare(b.type)
594
+ return a.name.localeCompare(b.name)
595
+ }
@@ -0,0 +1,112 @@
1
+ import fs from "node:fs"
2
+ import path from "node:path"
3
+ import { parseFrontmatter, toStringOrUndefined } from "./frontmatter"
4
+ import { resolveStashDir } from "./common"
5
+ import { parseOpenRef } from "./stash-ref"
6
+ import { resolveAssetPath } from "./stash-resolve"
7
+ import type { KnowledgeView, ShowResponse } from "./stash-types"
8
+ import { parseMarkdownToc, extractSection, extractLineRange, extractFrontmatterOnly, formatToc } from "./markdown"
9
+ import { buildToolInfo } from "./tool-runner"
10
+ import { loadConfig } from "./config"
11
+
12
+ export function agentikitShow(input: { ref: string; view?: KnowledgeView }): ShowResponse {
13
+ const parsed = parseOpenRef(input.ref)
14
+ const stashDir = resolveStashDir()
15
+ const config = loadConfig(stashDir)
16
+ const allStashDirs = [
17
+ stashDir,
18
+ ...config.additionalStashDirs.filter((d) => {
19
+ try { return fs.statSync(d).isDirectory() } catch { return false }
20
+ }),
21
+ ]
22
+
23
+ let assetPath: string | undefined
24
+ let lastError: Error | undefined
25
+ for (const dir of allStashDirs) {
26
+ try {
27
+ assetPath = resolveAssetPath(dir, parsed.type, parsed.name)
28
+ break
29
+ } catch (err) {
30
+ lastError = err instanceof Error ? err : new Error(String(err))
31
+ }
32
+ }
33
+ if (!assetPath) {
34
+ throw lastError ?? new Error(`Stash asset not found for ref: ${parsed.type}:${parsed.name}`)
35
+ }
36
+ const content = fs.readFileSync(assetPath, "utf8")
37
+
38
+ switch (parsed.type) {
39
+ case "skill":
40
+ return {
41
+ type: "skill",
42
+ name: parsed.name,
43
+ path: assetPath,
44
+ content,
45
+ }
46
+ case "command": {
47
+ const parsedMd = parseFrontmatter(content)
48
+ return {
49
+ type: "command",
50
+ name: parsed.name,
51
+ path: assetPath,
52
+ description: toStringOrUndefined(parsedMd.data.description),
53
+ template: parsedMd.content,
54
+ }
55
+ }
56
+ case "agent": {
57
+ const parsedMd = parseFrontmatter(content)
58
+ return {
59
+ type: "agent",
60
+ name: parsed.name,
61
+ path: assetPath,
62
+ description: toStringOrUndefined(parsedMd.data.description),
63
+ prompt: "Dispatching prompt must include the agent's full prompt content verbatim; summaries are non-compliant. \n\n"
64
+ + parsedMd.content,
65
+ toolPolicy: parsedMd.data.tools,
66
+ modelHint: parsedMd.data.model,
67
+ }
68
+ }
69
+ case "tool": {
70
+ const assetStashDir = allStashDirs.find((d) => path.resolve(assetPath!).startsWith(path.resolve(d) + path.sep)) ?? stashDir
71
+ const toolInfo = buildToolInfo(assetStashDir, assetPath)
72
+ return {
73
+ type: "tool",
74
+ name: parsed.name,
75
+ path: assetPath,
76
+ runCmd: toolInfo.runCmd,
77
+ kind: toolInfo.kind,
78
+ }
79
+ }
80
+ case "knowledge": {
81
+ const v = input.view ?? { mode: "full" }
82
+ switch (v.mode) {
83
+ case "toc": {
84
+ const toc = parseMarkdownToc(content)
85
+ return { type: "knowledge", name: parsed.name, path: assetPath, content: formatToc(toc) }
86
+ }
87
+ case "frontmatter": {
88
+ const fm = extractFrontmatterOnly(content)
89
+ return { type: "knowledge", name: parsed.name, path: assetPath, content: fm ?? "(no frontmatter)" }
90
+ }
91
+ case "section": {
92
+ const section = extractSection(content, v.heading)
93
+ if (!section) {
94
+ return {
95
+ type: "knowledge",
96
+ name: parsed.name,
97
+ path: assetPath,
98
+ content: `Section "${v.heading}" not found in ${parsed.name}. Try --view toc to discover available headings.`,
99
+ }
100
+ }
101
+ return { type: "knowledge", name: parsed.name, path: assetPath, content: section.content }
102
+ }
103
+ case "lines": {
104
+ return { type: "knowledge", name: parsed.name, path: assetPath, content: extractLineRange(content, v.start, v.end) }
105
+ }
106
+ default: {
107
+ return { type: "knowledge", name: parsed.name, path: assetPath, content }
108
+ }
109
+ }
110
+ }
111
+ }
112
+ }