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.
- package/README.md +113 -77
- package/dist/index.d.ts +15 -3
- package/dist/index.js +8 -2
- package/dist/src/asset-spec.d.ts +14 -0
- package/dist/src/asset-spec.js +46 -0
- package/dist/src/cli.js +154 -52
- package/dist/src/common.d.ts +8 -0
- package/dist/src/common.js +46 -0
- package/dist/src/config.d.ts +31 -0
- package/dist/src/config.js +74 -0
- package/dist/src/embedder.d.ts +10 -0
- package/dist/src/embedder.js +87 -0
- package/dist/src/frontmatter.d.ts +30 -0
- package/dist/src/frontmatter.js +86 -0
- package/dist/src/indexer.d.ts +20 -2
- package/dist/src/indexer.js +212 -80
- package/dist/src/init.d.ts +19 -0
- package/dist/src/init.js +87 -0
- package/dist/src/llm.d.ts +15 -0
- package/dist/src/llm.js +91 -0
- package/dist/src/markdown.d.ts +18 -0
- package/dist/src/markdown.js +77 -0
- package/dist/src/metadata.d.ts +10 -2
- package/dist/src/metadata.js +146 -30
- package/dist/src/ripgrep-install.d.ts +12 -0
- package/dist/src/ripgrep-install.js +169 -0
- package/dist/src/ripgrep-resolve.d.ts +13 -0
- package/dist/src/ripgrep-resolve.js +68 -0
- package/dist/src/ripgrep.d.ts +3 -0
- package/dist/src/ripgrep.js +2 -0
- package/dist/src/similarity.d.ts +1 -2
- package/dist/src/similarity.js +35 -9
- package/dist/src/stash-ref.d.ts +7 -0
- package/dist/src/stash-ref.js +33 -0
- package/dist/src/stash-resolve.d.ts +2 -0
- package/dist/src/stash-resolve.js +45 -0
- package/dist/src/stash-search.d.ts +6 -0
- package/dist/src/stash-search.js +269 -0
- package/dist/src/stash-show.d.ts +5 -0
- package/dist/src/stash-show.js +107 -0
- package/dist/src/stash-types.d.ts +53 -0
- package/dist/src/stash-types.js +1 -0
- package/dist/src/stash.d.ts +8 -58
- package/dist/src/stash.js +4 -580
- package/dist/src/tool-runner.d.ts +35 -0
- package/dist/src/tool-runner.js +100 -0
- package/dist/src/walker.d.ts +19 -0
- package/dist/src/walker.js +47 -0
- package/package.json +8 -14
- package/src/asset-spec.ts +69 -0
- package/src/cli.ts +164 -48
- package/src/common.ts +58 -0
- package/src/config.ts +124 -0
- package/src/embedder.ts +117 -0
- package/src/frontmatter.ts +95 -0
- package/src/indexer.ts +244 -84
- package/src/init.ts +106 -0
- package/src/llm.ts +124 -0
- package/src/markdown.ts +106 -0
- package/src/metadata.ts +157 -29
- package/src/ripgrep-install.ts +200 -0
- package/src/ripgrep-resolve.ts +72 -0
- package/src/ripgrep.ts +3 -0
- package/src/similarity.ts +33 -9
- package/src/stash-ref.ts +41 -0
- package/src/stash-resolve.ts +47 -0
- package/src/stash-search.ts +343 -0
- package/src/stash-show.ts +104 -0
- package/src/stash-types.ts +46 -0
- package/src/stash.ts +16 -695
- package/src/tool-runner.ts +129 -0
- package/src/walker.ts +53 -0
- package/.claude-plugin/plugin.json +0 -21
- package/commands/open.md +0 -11
- package/commands/run.md +0 -11
- package/commands/search.md +0 -11
- package/dist/src/plugin.d.ts +0 -2
- package/dist/src/plugin.js +0 -55
- package/skills/stash/SKILL.md +0 -68
- 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
|
-
.
|
|
213
|
-
if (typeFilter && typeFilter !== "any" && d.entry.entry.type !== typeFilter) return
|
|
214
|
-
|
|
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
|
|
package/src/stash-ref.ts
ADDED
|
@@ -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 }
|