agentikit 0.0.3 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +113 -77
  2. package/dist/index.d.ts +15 -3
  3. package/dist/index.js +8 -2
  4. package/dist/src/asset-spec.d.ts +14 -0
  5. package/dist/src/asset-spec.js +46 -0
  6. package/dist/src/cli.js +154 -52
  7. package/dist/src/common.d.ts +8 -0
  8. package/dist/src/common.js +46 -0
  9. package/dist/src/config.d.ts +31 -0
  10. package/dist/src/config.js +74 -0
  11. package/dist/src/embedder.d.ts +10 -0
  12. package/dist/src/embedder.js +87 -0
  13. package/dist/src/frontmatter.d.ts +30 -0
  14. package/dist/src/frontmatter.js +86 -0
  15. package/dist/src/indexer.d.ts +20 -2
  16. package/dist/src/indexer.js +212 -80
  17. package/dist/src/init.d.ts +19 -0
  18. package/dist/src/init.js +87 -0
  19. package/dist/src/llm.d.ts +15 -0
  20. package/dist/src/llm.js +91 -0
  21. package/dist/src/markdown.d.ts +18 -0
  22. package/dist/src/markdown.js +77 -0
  23. package/dist/src/metadata.d.ts +10 -2
  24. package/dist/src/metadata.js +146 -30
  25. package/dist/src/ripgrep-install.d.ts +12 -0
  26. package/dist/src/ripgrep-install.js +169 -0
  27. package/dist/src/ripgrep-resolve.d.ts +13 -0
  28. package/dist/src/ripgrep-resolve.js +68 -0
  29. package/dist/src/ripgrep.d.ts +3 -0
  30. package/dist/src/ripgrep.js +2 -0
  31. package/dist/src/similarity.d.ts +1 -2
  32. package/dist/src/similarity.js +35 -9
  33. package/dist/src/stash-ref.d.ts +7 -0
  34. package/dist/src/stash-ref.js +33 -0
  35. package/dist/src/stash-resolve.d.ts +2 -0
  36. package/dist/src/stash-resolve.js +45 -0
  37. package/dist/src/stash-search.d.ts +6 -0
  38. package/dist/src/stash-search.js +269 -0
  39. package/dist/src/stash-show.d.ts +5 -0
  40. package/dist/src/stash-show.js +107 -0
  41. package/dist/src/stash-types.d.ts +53 -0
  42. package/dist/src/stash-types.js +1 -0
  43. package/dist/src/stash.d.ts +8 -58
  44. package/dist/src/stash.js +4 -580
  45. package/dist/src/tool-runner.d.ts +35 -0
  46. package/dist/src/tool-runner.js +100 -0
  47. package/dist/src/walker.d.ts +19 -0
  48. package/dist/src/walker.js +47 -0
  49. package/package.json +8 -14
  50. package/src/asset-spec.ts +69 -0
  51. package/src/cli.ts +164 -48
  52. package/src/common.ts +58 -0
  53. package/src/config.ts +124 -0
  54. package/src/embedder.ts +117 -0
  55. package/src/frontmatter.ts +95 -0
  56. package/src/indexer.ts +244 -84
  57. package/src/init.ts +106 -0
  58. package/src/llm.ts +124 -0
  59. package/src/markdown.ts +106 -0
  60. package/src/metadata.ts +157 -29
  61. package/src/ripgrep-install.ts +200 -0
  62. package/src/ripgrep-resolve.ts +72 -0
  63. package/src/ripgrep.ts +3 -0
  64. package/src/similarity.ts +33 -9
  65. package/src/stash-ref.ts +41 -0
  66. package/src/stash-resolve.ts +47 -0
  67. package/src/stash-search.ts +343 -0
  68. package/src/stash-show.ts +104 -0
  69. package/src/stash-types.ts +46 -0
  70. package/src/stash.ts +16 -695
  71. package/src/tool-runner.ts +129 -0
  72. package/src/walker.ts +53 -0
  73. package/.claude-plugin/plugin.json +0 -21
  74. package/commands/open.md +0 -11
  75. package/commands/run.md +0 -11
  76. package/commands/search.md +0 -11
  77. package/dist/src/plugin.d.ts +0 -2
  78. package/dist/src/plugin.js +0 -55
  79. package/skills/stash/SKILL.md +0 -68
  80. package/src/plugin.ts +0 -56
package/src/similarity.ts CHANGED
@@ -28,7 +28,7 @@ interface TfIdfDocument {
28
28
  magnitude: number
29
29
  }
30
30
 
31
- interface SerializedTfIdf {
31
+ export interface SerializedTfIdf {
32
32
  idf: Record<string, number>
33
33
  docs: Array<{
34
34
  id: string
@@ -147,6 +147,18 @@ export class TfIdfAdapter implements SearchAdapter {
147
147
  }
148
148
  }
149
149
 
150
+ // Boost: intent phrase contains query token
151
+ const intents = doc.entry.entry.intents || []
152
+ for (const intent of intents) {
153
+ const intentLower = intent.toLowerCase()
154
+ for (const token of queryTokens) {
155
+ if (intentLower.includes(token)) {
156
+ score += 0.12
157
+ break // one boost per intent phrase
158
+ }
159
+ }
160
+ }
161
+
150
162
  // Boost: name contains query token
151
163
  const nameLower = doc.entry.entry.name.toLowerCase().replace(/[-_]/g, " ")
152
164
  for (const token of queryTokens) {
@@ -208,17 +220,29 @@ export class TfIdfAdapter implements SearchAdapter {
208
220
 
209
221
  private substringFallback(query: string, limit: number, typeFilter?: string): ScoredResult[] {
210
222
  const q = query.toLowerCase()
223
+ const tokens = tokenize(q)
211
224
  return this.documents
212
- .filter((d) => {
213
- if (typeFilter && typeFilter !== "any" && d.entry.entry.type !== typeFilter) return false
214
- return d.entry.text.includes(q) || d.entry.entry.name.toLowerCase().includes(q)
225
+ .map((d) => {
226
+ if (typeFilter && typeFilter !== "any" && d.entry.entry.type !== typeFilter) return null
227
+ // Check if any query token matches the document text or name
228
+ const text = d.entry.text
229
+ const name = d.entry.entry.name.toLowerCase()
230
+ let matchCount = 0
231
+ for (const token of tokens) {
232
+ if (text.includes(token) || name.includes(token)) matchCount++
233
+ }
234
+ // Also check full substring match
235
+ if (text.includes(q) || name.includes(q)) matchCount = Math.max(matchCount, tokens.length)
236
+ if (matchCount === 0) return null
237
+ return {
238
+ entry: d.entry.entry,
239
+ path: d.entry.path,
240
+ score: Math.round((matchCount / Math.max(tokens.length, 1)) * 500) / 1000,
241
+ }
215
242
  })
243
+ .filter((d): d is ScoredResult => d !== null)
244
+ .sort((a, b) => b.score - a.score)
216
245
  .slice(0, limit)
217
- .map((d) => ({
218
- entry: d.entry.entry,
219
- path: d.entry.path,
220
- score: 0.5,
221
- }))
222
246
  }
223
247
  }
224
248
 
@@ -0,0 +1,41 @@
1
+ import path from "node:path"
2
+ import { type AgentikitAssetType, isAssetType } from "./common"
3
+
4
+ export interface OpenRef {
5
+ type: AgentikitAssetType
6
+ name: string
7
+ }
8
+
9
+ export function parseOpenRef(ref: string): OpenRef {
10
+ const separator = ref.indexOf(":")
11
+ if (separator <= 0) {
12
+ throw new Error("Invalid open ref. Expected format '<type>:<name>'.")
13
+ }
14
+ const rawType = ref.slice(0, separator)
15
+ const rawName = ref.slice(separator + 1)
16
+ if (!isAssetType(rawType)) {
17
+ throw new Error(`Invalid open ref type: "${rawType}".`)
18
+ }
19
+ let name: string
20
+ try {
21
+ name = decodeURIComponent(rawName)
22
+ } catch {
23
+ throw new Error("Invalid open ref encoding.")
24
+ }
25
+ const normalized = path.posix.normalize(name.replace(/\\/g, "/"))
26
+ if (
27
+ !name
28
+ || name.includes("\0")
29
+ || /^[A-Za-z]:/.test(name)
30
+ || path.posix.isAbsolute(normalized)
31
+ || normalized === ".."
32
+ || normalized.startsWith("../")
33
+ ) {
34
+ throw new Error("Invalid open ref name.")
35
+ }
36
+ return { type: rawType, name: normalized }
37
+ }
38
+
39
+ export function makeOpenRef(type: AgentikitAssetType, name: string): string {
40
+ return `${type}:${encodeURIComponent(name)}`
41
+ }
@@ -0,0 +1,47 @@
1
+ import fs from "node:fs"
2
+ import path from "node:path"
3
+ import { type AgentikitAssetType, hasErrnoCode, isWithin } from "./common"
4
+ import { TYPE_DIRS, isRelevantAssetFile, resolveAssetPathFromName } from "./asset-spec"
5
+
6
+ export function resolveAssetPath(stashDir: string, type: AgentikitAssetType, name: string): string {
7
+ const root = path.join(stashDir, TYPE_DIRS[type])
8
+ const target = resolveAssetPathFromName(type, root, name)
9
+ const resolvedRoot = resolveAndValidateTypeRoot(root, type, name)
10
+ const resolvedTarget = path.resolve(target)
11
+ if (!isWithin(resolvedTarget, resolvedRoot)) {
12
+ throw new Error("Ref resolves outside the stash root.")
13
+ }
14
+ if (!fs.existsSync(resolvedTarget) || !fs.statSync(resolvedTarget).isFile()) {
15
+ throw new Error(`Stash asset not found for ref: ${type}:${name}`)
16
+ }
17
+ const realTarget = fs.realpathSync(resolvedTarget)
18
+ if (!isWithin(realTarget, resolvedRoot)) {
19
+ throw new Error("Ref resolves outside the stash root.")
20
+ }
21
+ if (!isRelevantAssetFile(type, path.basename(resolvedTarget))) {
22
+ if (type === "tool") {
23
+ throw new Error("Tool ref must resolve to a .sh, .ts, .js, .ps1, .cmd, or .bat file.")
24
+ }
25
+ throw new Error(`Stash asset not found for ref: ${type}:${name}`)
26
+ }
27
+ return realTarget
28
+ }
29
+
30
+ function resolveAndValidateTypeRoot(root: string, type: AgentikitAssetType, name: string): string {
31
+ const rootStat = readTypeRootStat(root, type, name)
32
+ if (!rootStat.isDirectory()) {
33
+ throw new Error(`Stash type root is not a directory for ref: ${type}:${name}`)
34
+ }
35
+ return fs.realpathSync(root)
36
+ }
37
+
38
+ function readTypeRootStat(root: string, type: AgentikitAssetType, name: string): fs.Stats {
39
+ try {
40
+ return fs.statSync(root)
41
+ } catch (error: unknown) {
42
+ if (hasErrnoCode(error, "ENOENT")) {
43
+ throw new Error(`Stash type root not found for ref: ${type}:${name}`)
44
+ }
45
+ throw error
46
+ }
47
+ }
@@ -0,0 +1,343 @@
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 { AgentikitSearchType, SearchHit, SearchResponse } from "./stash-types"
11
+ import { loadConfig } from "./config"
12
+
13
+ type IndexedAsset = {
14
+ type: AgentikitAssetType
15
+ name: string
16
+ path: string
17
+ }
18
+
19
+ const DEFAULT_LIMIT = 20
20
+
21
+ export async function agentikitSearch(input: {
22
+ query: string
23
+ type?: AgentikitSearchType
24
+ limit?: number
25
+ }): Promise<SearchResponse> {
26
+ const t0 = Date.now()
27
+ const query = input.query.trim().toLowerCase()
28
+ const searchType = input.type ?? "any"
29
+ const limit = normalizeLimit(input.limit)
30
+ const stashDir = resolveStashDir()
31
+ const config = loadConfig(stashDir)
32
+
33
+ const allStashDirs = [
34
+ stashDir,
35
+ ...config.additionalStashDirs.filter((d) => {
36
+ try { return fs.statSync(d).isDirectory() } catch { return false }
37
+ }),
38
+ ]
39
+
40
+ // Try indexed search (single unified pipeline: embedding + TF-IDF as weighted features)
41
+ const index = loadSearchIndex()
42
+ if (index && index.entries && index.entries.length > 0 && index.stashDir === stashDir) {
43
+ const { hits, embedMs, rankMs } = await searchIndex(index, query, searchType, limit, stashDir, allStashDirs, config)
44
+ return {
45
+ stashDir,
46
+ hits,
47
+ tip: hits.length === 0 ? "No matching stash assets were found. Try running 'akm index' to rebuild." : undefined,
48
+ timing: { totalMs: Date.now() - t0, rankMs, embedMs },
49
+ }
50
+ }
51
+
52
+ // No index: fall back to filesystem walk + substring match across all stash dirs
53
+ const hits = allStashDirs
54
+ .flatMap((dir) => substringSearch(query, searchType, limit, dir))
55
+ .slice(0, limit)
56
+
57
+ return {
58
+ stashDir,
59
+ hits,
60
+ tip: hits.length === 0 ? "No matching stash assets were found. Try running 'akm index' to rebuild." : undefined,
61
+ timing: { totalMs: Date.now() - t0 },
62
+ }
63
+ }
64
+
65
+ // ── Unified indexed search ──────────────────────────────────────────────────
66
+
67
+ async function searchIndex(
68
+ index: import("./indexer").SearchIndex,
69
+ query: string,
70
+ searchType: AgentikitSearchType,
71
+ limit: number,
72
+ stashDir: string,
73
+ allStashDirs: string[],
74
+ config: import("./config").AgentikitConfig,
75
+ ): Promise<{ hits: SearchHit[]; embedMs?: number; rankMs?: number }> {
76
+ // Filter candidates by type
77
+ let candidates = index.entries
78
+ if (searchType !== "any") {
79
+ candidates = candidates.filter((ie) => ie.entry.type === searchType)
80
+ }
81
+
82
+ if (candidates.length === 0) return { hits: [] }
83
+
84
+ // Empty query: return all entries (no scoring needed)
85
+ if (!query) {
86
+ return { hits: candidates.slice(0, limit).map((ie) =>
87
+ buildIndexedHit({ entry: ie.entry, path: ie.path, score: 1, query, rankingMode: "tfidf", defaultStashDir: stashDir, allStashDirs }),
88
+ ) }
89
+ }
90
+
91
+ // Score each candidate using available signals
92
+ const tEmbed0 = Date.now()
93
+ const embeddingScores = await tryEmbeddingScores(candidates, query, config)
94
+ const embedMs = Date.now() - tEmbed0
95
+
96
+ const tRank0 = Date.now()
97
+ const tfidfScores = computeTfidfScores(index, candidates, query, searchType)
98
+
99
+ const scored: Array<{ ie: IndexedEntry; score: number; rankingMode: "semantic" | "tfidf" }> = []
100
+
101
+ for (const ie of candidates) {
102
+ const key = ie.path
103
+ const embScore = embeddingScores?.get(key)
104
+ const tfidfScore = tfidfScores.get(key) ?? 0
105
+
106
+ if (embScore !== undefined) {
107
+ // Weighted blend: embedding dominates when available, TF-IDF boosts lexical matches
108
+ const blended = embScore * 0.7 + tfidfScore * 0.3
109
+ if (blended > 0) scored.push({ ie, score: blended, rankingMode: "semantic" })
110
+ } else if (tfidfScore > 0) {
111
+ scored.push({ ie, score: tfidfScore, rankingMode: "tfidf" })
112
+ }
113
+ }
114
+
115
+ scored.sort((a, b) => b.score - a.score)
116
+ const rankMs = Date.now() - tRank0
117
+
118
+ return { embedMs, rankMs, hits: scored.slice(0, limit).map(({ ie, score, rankingMode }) =>
119
+ buildIndexedHit({
120
+ entry: ie.entry,
121
+ path: ie.path,
122
+ score: Math.round(score * 1000) / 1000,
123
+ query,
124
+ rankingMode,
125
+ defaultStashDir: stashDir,
126
+ allStashDirs,
127
+ }),
128
+ ) }
129
+ }
130
+
131
+ // ── Embedding scorer ────────────────────────────────────────────────────────
132
+
133
+ async function tryEmbeddingScores(
134
+ candidates: IndexedEntry[],
135
+ query: string,
136
+ config: import("./config").AgentikitConfig,
137
+ ): Promise<Map<string, number> | null> {
138
+ if (!config.semanticSearch) return null
139
+
140
+ const withEmbeddings = candidates.filter((ie) => ie.embedding && ie.embedding.length > 0)
141
+ if (withEmbeddings.length === 0) return null
142
+
143
+ try {
144
+ const { embed, cosineSimilarity } = await import("./embedder.js")
145
+ const queryEmbedding = await embed(query, config.embedding)
146
+ const scores = new Map<string, number>()
147
+ for (const ie of withEmbeddings) {
148
+ scores.set(ie.path, cosineSimilarity(queryEmbedding, ie.embedding!))
149
+ }
150
+ return scores
151
+ } catch {
152
+ return null
153
+ }
154
+ }
155
+
156
+ // ── TF-IDF scorer ───────────────────────────────────────────────────────────
157
+
158
+ function computeTfidfScores(
159
+ index: import("./indexer").SearchIndex,
160
+ candidates: IndexedEntry[],
161
+ query: string,
162
+ searchType: AgentikitSearchType,
163
+ ): Map<string, number> {
164
+ const candidateScoredEntries = toScoredEntries(candidates)
165
+
166
+ let adapter: TfIdfAdapter
167
+ if (index.tfidf) {
168
+ const allScored = toScoredEntries(index.entries)
169
+ adapter = TfIdfAdapter.deserialize(index.tfidf, allScored)
170
+ } else {
171
+ adapter = new TfIdfAdapter()
172
+ adapter.buildIndex(candidateScoredEntries)
173
+ }
174
+
175
+ const typeFilter = searchType === "any" ? undefined : searchType
176
+ const results = adapter.search(query, candidates.length, typeFilter)
177
+
178
+ const scores = new Map<string, number>()
179
+ for (const r of results) {
180
+ scores.set(r.path, r.score)
181
+ }
182
+ return scores
183
+ }
184
+
185
+ // ── Substring fallback (no index) ───────────────────────────────────────────
186
+
187
+ function substringSearch(
188
+ query: string,
189
+ searchType: AgentikitSearchType,
190
+ limit: number,
191
+ stashDir: string,
192
+ ): SearchHit[] {
193
+ const assets = indexAssets(stashDir, searchType)
194
+ return assets
195
+ .filter((asset) => asset.name.toLowerCase().includes(query))
196
+ .sort(compareAssets)
197
+ .slice(0, limit)
198
+ .map((asset): SearchHit => assetToSearchHit(asset, stashDir))
199
+ }
200
+
201
+ // ── Hit building ────────────────────────────────────────────────────────────
202
+
203
+ function findStashDirForPath(filePath: string, stashDirs: string[]): string | undefined {
204
+ const resolved = path.resolve(filePath)
205
+ for (const dir of stashDirs) {
206
+ if (resolved.startsWith(path.resolve(dir) + path.sep)) return dir
207
+ }
208
+ return undefined
209
+ }
210
+
211
+ function buildIndexedHit(input: {
212
+ entry: IndexedEntry["entry"]
213
+ path: string
214
+ score: number
215
+ query: string
216
+ rankingMode: "semantic" | "tfidf"
217
+ defaultStashDir: string
218
+ allStashDirs: string[]
219
+ }): SearchHit {
220
+ const entryStashDir = findStashDirForPath(input.path, input.allStashDirs) ?? input.defaultStashDir
221
+ const typeRoot = path.join(entryStashDir, TYPE_DIRS[input.entry.type])
222
+ const openRefName = deriveCanonicalAssetName(input.entry.type, typeRoot, input.path)
223
+ ?? input.entry.name
224
+
225
+ const qualityBoost = input.entry.generated === true ? 0 : 0.05
226
+ const confidenceBoost = typeof input.entry.confidence === "number" ? Math.min(0.05, Math.max(0, input.entry.confidence) * 0.05) : 0
227
+ const score = Math.round((input.score + qualityBoost + confidenceBoost) * 1000) / 1000
228
+
229
+ const whyMatched = buildWhyMatched(input.entry, input.query, input.rankingMode, qualityBoost, confidenceBoost)
230
+
231
+ const hit: SearchHit = {
232
+ type: input.entry.type,
233
+ name: input.entry.name,
234
+ path: input.path,
235
+ openRef: makeOpenRef(input.entry.type, openRefName),
236
+ description: input.entry.description,
237
+ tags: input.entry.tags,
238
+ score,
239
+ whyMatched,
240
+ }
241
+
242
+ if (input.entry.type === "tool") {
243
+ try {
244
+ const toolInfo = buildToolInfo(entryStashDir, input.path)
245
+ hit.runCmd = toolInfo.runCmd
246
+ hit.kind = toolInfo.kind
247
+ } catch (error: unknown) {
248
+ if (!hasErrnoCode(error, "ENOENT")) throw error
249
+ }
250
+ }
251
+
252
+ return hit
253
+ }
254
+
255
+ function buildWhyMatched(
256
+ entry: IndexedEntry["entry"],
257
+ query: string,
258
+ rankingMode: "semantic" | "tfidf",
259
+ qualityBoost: number,
260
+ confidenceBoost: number,
261
+ ): string[] {
262
+ const reasons: string[] = [rankingMode === "semantic" ? "semantic similarity" : "tf-idf lexical relevance"]
263
+ const tokens = query.toLowerCase().split(/\s+/).filter(Boolean)
264
+
265
+ const name = entry.name.toLowerCase()
266
+ const tags = entry.tags?.join(" ").toLowerCase() ?? ""
267
+ const intents = entry.intents?.join(" ").toLowerCase() ?? ""
268
+ const aliases = entry.aliases?.join(" ").toLowerCase() ?? ""
269
+
270
+ if (tokens.some((t) => name.includes(t))) reasons.push("matched name tokens")
271
+ if (tokens.some((t) => tags.includes(t))) reasons.push("matched tags")
272
+ if (tokens.some((t) => intents.includes(t))) reasons.push("matched intents")
273
+ if (tokens.some((t) => aliases.includes(t))) reasons.push("matched aliases")
274
+ if (qualityBoost > 0) reasons.push("curated metadata boost")
275
+ if (confidenceBoost > 0) reasons.push("metadata confidence boost")
276
+
277
+ return reasons
278
+ }
279
+
280
+ // ── Helpers ─────────────────────────────────────────────────────────────────
281
+
282
+ function toScoredEntries(entries: IndexedEntry[]): ScoredEntry[] {
283
+ return entries.map((ie) => ({
284
+ id: `${ie.entry.type}:${ie.entry.name}`,
285
+ text: buildSearchText(ie.entry),
286
+ entry: ie.entry,
287
+ path: ie.path,
288
+ }))
289
+ }
290
+
291
+ function assetToSearchHit(asset: IndexedAsset, stashDir: string): SearchHit {
292
+ if (asset.type !== "tool") {
293
+ return {
294
+ type: asset.type,
295
+ name: asset.name,
296
+ path: asset.path,
297
+ openRef: makeOpenRef(asset.type, asset.name),
298
+ }
299
+ }
300
+ const toolInfo = buildToolInfo(stashDir, asset.path)
301
+ return {
302
+ type: "tool",
303
+ name: asset.name,
304
+ path: asset.path,
305
+ openRef: makeOpenRef("tool", asset.name),
306
+ runCmd: toolInfo.runCmd,
307
+ kind: toolInfo.kind,
308
+ }
309
+ }
310
+
311
+ function normalizeLimit(limit?: number): number {
312
+ if (typeof limit !== "number" || Number.isNaN(limit) || limit <= 0) {
313
+ return DEFAULT_LIMIT
314
+ }
315
+ return Math.min(Math.floor(limit), 200)
316
+ }
317
+
318
+ function fileToAsset(assetType: AgentikitAssetType, root: string, file: string): IndexedAsset | undefined {
319
+ const name = deriveCanonicalAssetName(assetType, root, file)
320
+ if (!name) return undefined
321
+ return { type: assetType, name, path: file }
322
+ }
323
+
324
+ function indexAssets(stashDir: string, type: AgentikitSearchType): IndexedAsset[] {
325
+ const assets: IndexedAsset[] = []
326
+ const types = type === "any" ? ASSET_TYPES : [type]
327
+ for (const assetType of types) {
328
+ const root = path.join(stashDir, TYPE_DIRS[assetType])
329
+ const groups = walkStash(root, assetType)
330
+ for (const { files } of groups) {
331
+ for (const file of files) {
332
+ const asset = fileToAsset(assetType, root, file)
333
+ if (asset) assets.push(asset)
334
+ }
335
+ }
336
+ }
337
+ return assets
338
+ }
339
+
340
+ function compareAssets(a: IndexedAsset, b: IndexedAsset): number {
341
+ if (a.type !== b.type) return a.type.localeCompare(b.type)
342
+ return a.name.localeCompare(b.name)
343
+ }
@@ -0,0 +1,104 @@
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: parsedMd.content,
64
+ toolPolicy: parsedMd.data.tools,
65
+ modelHint: parsedMd.data.model,
66
+ }
67
+ }
68
+ case "tool": {
69
+ const assetStashDir = allStashDirs.find((d) => path.resolve(assetPath!).startsWith(path.resolve(d) + path.sep)) ?? stashDir
70
+ const toolInfo = buildToolInfo(assetStashDir, assetPath)
71
+ return {
72
+ type: "tool",
73
+ name: parsed.name,
74
+ path: assetPath,
75
+ runCmd: toolInfo.runCmd,
76
+ kind: toolInfo.kind,
77
+ }
78
+ }
79
+ case "knowledge": {
80
+ const v = input.view ?? { mode: "full" }
81
+ switch (v.mode) {
82
+ case "toc": {
83
+ const toc = parseMarkdownToc(content)
84
+ return { type: "knowledge", name: parsed.name, path: assetPath, content: formatToc(toc) }
85
+ }
86
+ case "frontmatter": {
87
+ const fm = extractFrontmatterOnly(content)
88
+ return { type: "knowledge", name: parsed.name, path: assetPath, content: fm ?? "(no frontmatter)" }
89
+ }
90
+ case "section": {
91
+ const section = extractSection(content, v.heading)
92
+ if (!section) throw new Error(`Section "${v.heading}" not found in ${parsed.name}`)
93
+ return { type: "knowledge", name: parsed.name, path: assetPath, content: section.content }
94
+ }
95
+ case "lines": {
96
+ return { type: "knowledge", name: parsed.name, path: assetPath, content: extractLineRange(content, v.start, v.end) }
97
+ }
98
+ default: {
99
+ return { type: "knowledge", name: parsed.name, path: assetPath, content }
100
+ }
101
+ }
102
+ }
103
+ }
104
+ }
@@ -0,0 +1,46 @@
1
+ import type { AgentikitAssetType } from "./common"
2
+ import type { ToolKind } from "./tool-runner"
3
+
4
+ export type AgentikitSearchType = AgentikitAssetType | "any"
5
+
6
+ export interface SearchHit {
7
+ type: AgentikitAssetType
8
+ name: string
9
+ path: string
10
+ openRef: string
11
+ description?: string
12
+ tags?: string[]
13
+ score?: number
14
+ whyMatched?: string[]
15
+ runCmd?: string
16
+ kind?: ToolKind
17
+ }
18
+
19
+ export interface SearchResponse {
20
+ stashDir: string
21
+ hits: SearchHit[]
22
+ tip?: string
23
+ /** Timing counters in milliseconds */
24
+ timing?: { totalMs: number; rankMs?: number; embedMs?: number }
25
+ }
26
+
27
+ export interface ShowResponse {
28
+ type: AgentikitAssetType
29
+ name: string
30
+ path: string
31
+ content?: string
32
+ template?: string
33
+ prompt?: string
34
+ description?: string
35
+ toolPolicy?: unknown
36
+ modelHint?: unknown
37
+ runCmd?: string
38
+ kind?: ToolKind
39
+ }
40
+
41
+ export type KnowledgeView =
42
+ | { mode: "full" }
43
+ | { mode: "toc" }
44
+ | { mode: "frontmatter" }
45
+ | { mode: "section"; heading: string }
46
+ | { mode: "lines"; start: number; end: number }