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/indexer.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs"
|
|
2
2
|
import path from "node:path"
|
|
3
|
-
import type
|
|
3
|
+
import { type AgentikitAssetType, resolveStashDir } from "./common"
|
|
4
|
+
import { ASSET_TYPES, TYPE_DIRS, deriveCanonicalAssetName } from "./asset-spec"
|
|
4
5
|
import {
|
|
5
6
|
type StashFile,
|
|
6
7
|
type StashEntry,
|
|
@@ -8,7 +9,10 @@ import {
|
|
|
8
9
|
writeStashFile,
|
|
9
10
|
generateMetadata,
|
|
10
11
|
} from "./metadata"
|
|
11
|
-
import { TfIdfAdapter, type ScoredEntry } from "./similarity"
|
|
12
|
+
import { TfIdfAdapter, type ScoredEntry, type SerializedTfIdf } from "./similarity"
|
|
13
|
+
import { walkStash } from "./walker"
|
|
14
|
+
import type { EmbeddingVector } from "./embedder"
|
|
15
|
+
import type { LlmConnectionConfig } from "./config"
|
|
12
16
|
|
|
13
17
|
// ── Types ───────────────────────────────────────────────────────────────────
|
|
14
18
|
|
|
@@ -16,15 +20,20 @@ export interface IndexedEntry {
|
|
|
16
20
|
entry: StashEntry
|
|
17
21
|
path: string
|
|
18
22
|
dirPath: string
|
|
23
|
+
embedding?: EmbeddingVector
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
export interface SearchIndex {
|
|
22
27
|
version: number
|
|
23
28
|
builtAt: string
|
|
24
29
|
stashDir: string
|
|
30
|
+
/** All stash directories that were indexed (primary + additional) */
|
|
31
|
+
stashDirs?: string[]
|
|
25
32
|
entries: IndexedEntry[]
|
|
26
33
|
/** Serialized TF-IDF state (term frequencies, idf values) */
|
|
27
|
-
tfidf?:
|
|
34
|
+
tfidf?: SerializedTfIdf
|
|
35
|
+
/** Whether embeddings are included in entries */
|
|
36
|
+
hasEmbeddings?: boolean
|
|
28
37
|
}
|
|
29
38
|
|
|
30
39
|
export interface IndexResponse {
|
|
@@ -32,19 +41,16 @@ export interface IndexResponse {
|
|
|
32
41
|
totalEntries: number
|
|
33
42
|
generatedMetadata: number
|
|
34
43
|
indexPath: string
|
|
44
|
+
mode: "full" | "incremental"
|
|
45
|
+
directoriesScanned: number
|
|
46
|
+
directoriesSkipped: number
|
|
47
|
+
/** Timing counters in milliseconds */
|
|
48
|
+
timing?: { totalMs: number; walkMs: number; embedMs: number; tfidfMs: number }
|
|
35
49
|
}
|
|
36
50
|
|
|
37
51
|
// ── Constants ───────────────────────────────────────────────────────────────
|
|
38
52
|
|
|
39
|
-
const INDEX_VERSION =
|
|
40
|
-
const SCRIPT_EXTENSIONS = new Set([".sh", ".ts", ".js", ".ps1", ".cmd", ".bat"])
|
|
41
|
-
|
|
42
|
-
const TYPE_DIRS: Record<AgentikitAssetType, string> = {
|
|
43
|
-
tool: "tools",
|
|
44
|
-
skill: "skills",
|
|
45
|
-
command: "commands",
|
|
46
|
-
agent: "agents",
|
|
47
|
-
}
|
|
53
|
+
const INDEX_VERSION = 4
|
|
48
54
|
|
|
49
55
|
// ── Index Path ──────────────────────────────────────────────────────────────
|
|
50
56
|
|
|
@@ -68,42 +74,109 @@ export function loadSearchIndex(): SearchIndex | null {
|
|
|
68
74
|
|
|
69
75
|
// ── Indexer ──────────────────────────────────────────────────────────────────
|
|
70
76
|
|
|
71
|
-
export function agentikitIndex(options?: { stashDir?: string }): IndexResponse {
|
|
72
|
-
const stashDir = options?.stashDir ||
|
|
77
|
+
export async function agentikitIndex(options?: { stashDir?: string; full?: boolean }): Promise<IndexResponse> {
|
|
78
|
+
const stashDir = options?.stashDir || resolveStashDir()
|
|
79
|
+
|
|
80
|
+
// Load config to get additional stash dirs and semantic search setting
|
|
81
|
+
const { loadConfig } = await import("./config.js")
|
|
82
|
+
const config = loadConfig(stashDir)
|
|
83
|
+
|
|
84
|
+
const allStashDirs = [stashDir]
|
|
85
|
+
for (const d of config.additionalStashDirs) {
|
|
86
|
+
try {
|
|
87
|
+
if (fs.statSync(d).isDirectory() && !allStashDirs.includes(path.resolve(d))) {
|
|
88
|
+
allStashDirs.push(path.resolve(d))
|
|
89
|
+
}
|
|
90
|
+
} catch { /* skip nonexistent dirs */ }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const t0 = Date.now()
|
|
73
94
|
const allEntries: IndexedEntry[] = []
|
|
74
95
|
let generatedCount = 0
|
|
96
|
+
let scannedDirs = 0
|
|
97
|
+
let skippedDirs = 0
|
|
75
98
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
99
|
+
// Load previous index for incremental mode
|
|
100
|
+
const previousIndex = !options?.full ? loadSearchIndex() : null
|
|
101
|
+
const isIncremental = previousIndex !== null && previousIndex.stashDir === stashDir
|
|
102
|
+
const builtAtMs = isIncremental ? new Date(previousIndex.builtAt).getTime() : 0
|
|
79
103
|
|
|
80
|
-
|
|
81
|
-
|
|
104
|
+
// Build lookup of previous entries by dirPath
|
|
105
|
+
const previousEntriesByDir = new Map<string, IndexedEntry[]>()
|
|
106
|
+
if (isIncremental) {
|
|
107
|
+
for (const ie of previousIndex.entries) {
|
|
108
|
+
const list = previousEntriesByDir.get(ie.dirPath) || []
|
|
109
|
+
list.push(ie)
|
|
110
|
+
previousEntriesByDir.set(ie.dirPath, list)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const seenPaths = new Set<string>()
|
|
115
|
+
const tWalkStart = Date.now()
|
|
116
|
+
|
|
117
|
+
for (const currentStashDir of allStashDirs) {
|
|
118
|
+
for (const assetType of ASSET_TYPES as AgentikitAssetType[]) {
|
|
119
|
+
const typeRoot = path.join(currentStashDir, TYPE_DIRS[assetType])
|
|
120
|
+
try {
|
|
121
|
+
if (!fs.statSync(typeRoot).isDirectory()) continue
|
|
122
|
+
} catch { continue }
|
|
123
|
+
|
|
124
|
+
// Group files by their immediate parent directory
|
|
125
|
+
const dirGroups = walkStash(typeRoot, assetType)
|
|
82
126
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
127
|
+
for (const { dirPath, files } of dirGroups) {
|
|
128
|
+
// Deduplicate by dirPath across stash dirs
|
|
129
|
+
if (seenPaths.has(path.resolve(dirPath))) continue
|
|
130
|
+
seenPaths.add(path.resolve(dirPath))
|
|
86
131
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
132
|
+
// Incremental: skip directories that haven't changed
|
|
133
|
+
const prevEntries = previousEntriesByDir.get(dirPath)
|
|
134
|
+
if (isIncremental && prevEntries && !isDirStale(dirPath, files, prevEntries, builtAtMs)) {
|
|
135
|
+
allEntries.push(...prevEntries)
|
|
136
|
+
skippedDirs++
|
|
137
|
+
continue
|
|
93
138
|
}
|
|
94
|
-
}
|
|
95
139
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
140
|
+
scannedDirs++
|
|
141
|
+
|
|
142
|
+
// Try loading existing .stash.json
|
|
143
|
+
let stash = loadStashFile(dirPath)
|
|
144
|
+
|
|
145
|
+
if (stash) {
|
|
146
|
+
const migration = migrateGeneratedSkillMetadata(stash, files, typeRoot)
|
|
147
|
+
if (migration.changed) {
|
|
148
|
+
stash = migration.stash
|
|
149
|
+
writeStashFile(dirPath, stash)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!stash) {
|
|
154
|
+
// Generate metadata
|
|
155
|
+
stash = generateMetadata(dirPath, assetType, files, typeRoot)
|
|
156
|
+
// Enhance with LLM if configured
|
|
157
|
+
if (config.llm && stash.entries.length > 0) {
|
|
158
|
+
stash = await enhanceStashWithLlm(config.llm, stash, dirPath, files)
|
|
159
|
+
}
|
|
160
|
+
if (stash.entries.length > 0) {
|
|
161
|
+
writeStashFile(dirPath, stash)
|
|
162
|
+
generatedCount += stash.entries.length
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (stash) {
|
|
167
|
+
for (const entry of stash.entries) {
|
|
168
|
+
const entryPath = entry.entry
|
|
169
|
+
? path.join(dirPath, entry.entry)
|
|
170
|
+
: files[0] || dirPath
|
|
171
|
+
allEntries.push({ entry, path: entryPath, dirPath })
|
|
172
|
+
}
|
|
102
173
|
}
|
|
103
174
|
}
|
|
104
175
|
}
|
|
105
176
|
}
|
|
106
177
|
|
|
178
|
+
const tWalkEnd = Date.now()
|
|
179
|
+
|
|
107
180
|
// Build TF-IDF index
|
|
108
181
|
const adapter = new TfIdfAdapter()
|
|
109
182
|
const scoredEntries: ScoredEntry[] = allEntries.map((ie) => ({
|
|
@@ -113,6 +186,26 @@ export function agentikitIndex(options?: { stashDir?: string }): IndexResponse {
|
|
|
113
186
|
path: ie.path,
|
|
114
187
|
}))
|
|
115
188
|
adapter.buildIndex(scoredEntries)
|
|
189
|
+
const tTfidfEnd = Date.now()
|
|
190
|
+
|
|
191
|
+
// Generate embeddings if semantic search is enabled
|
|
192
|
+
let hasEmbeddings = false
|
|
193
|
+
if (config.semanticSearch) {
|
|
194
|
+
try {
|
|
195
|
+
const { embed } = await import("./embedder.js")
|
|
196
|
+
for (const ie of allEntries) {
|
|
197
|
+
if (!ie.embedding) {
|
|
198
|
+
const text = buildSearchText(ie.entry)
|
|
199
|
+
ie.embedding = await embed(text, config.embedding)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
hasEmbeddings = true
|
|
203
|
+
} catch {
|
|
204
|
+
// Embedding provider not available, continue without embeddings
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const tEmbedEnd = Date.now()
|
|
116
209
|
|
|
117
210
|
// Persist index
|
|
118
211
|
const indexPath = getIndexPath()
|
|
@@ -125,64 +218,138 @@ export function agentikitIndex(options?: { stashDir?: string }): IndexResponse {
|
|
|
125
218
|
version: INDEX_VERSION,
|
|
126
219
|
builtAt: new Date().toISOString(),
|
|
127
220
|
stashDir,
|
|
221
|
+
stashDirs: allStashDirs,
|
|
128
222
|
entries: allEntries,
|
|
129
223
|
tfidf: adapter.serialize(),
|
|
224
|
+
hasEmbeddings,
|
|
130
225
|
}
|
|
131
226
|
fs.writeFileSync(indexPath, JSON.stringify(index) + "\n", "utf8")
|
|
132
227
|
|
|
228
|
+
const tEnd = Date.now()
|
|
229
|
+
|
|
133
230
|
return {
|
|
134
231
|
stashDir,
|
|
135
232
|
totalEntries: allEntries.length,
|
|
136
233
|
generatedMetadata: generatedCount,
|
|
137
234
|
indexPath,
|
|
235
|
+
mode: isIncremental ? "incremental" : "full",
|
|
236
|
+
directoriesScanned: scannedDirs,
|
|
237
|
+
directoriesSkipped: skippedDirs,
|
|
238
|
+
timing: {
|
|
239
|
+
totalMs: tEnd - t0,
|
|
240
|
+
walkMs: tWalkEnd - tWalkStart, // includes metadata generation (interleaved)
|
|
241
|
+
embedMs: tEmbedEnd - tTfidfEnd,
|
|
242
|
+
tfidfMs: tTfidfEnd - tWalkEnd,
|
|
243
|
+
},
|
|
138
244
|
}
|
|
139
245
|
}
|
|
140
246
|
|
|
141
247
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
142
248
|
|
|
143
|
-
function
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
249
|
+
function isDirStale(
|
|
250
|
+
dirPath: string,
|
|
251
|
+
currentFiles: string[],
|
|
252
|
+
previousEntries: IndexedEntry[],
|
|
253
|
+
builtAtMs: number,
|
|
254
|
+
): boolean {
|
|
255
|
+
// Check if file set changed (additions or deletions)
|
|
256
|
+
const prevFileNames = new Set(
|
|
257
|
+
previousEntries
|
|
258
|
+
.map((ie) => ie.entry.entry)
|
|
259
|
+
.filter((e): e is string => !!e),
|
|
260
|
+
)
|
|
261
|
+
const currFileNames = new Set(currentFiles.map((f) => path.basename(f)))
|
|
262
|
+
if (prevFileNames.size !== currFileNames.size) return true
|
|
263
|
+
for (const name of currFileNames) {
|
|
264
|
+
if (!prevFileNames.has(name)) return true
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Check modification times of current files
|
|
268
|
+
for (const file of currentFiles) {
|
|
269
|
+
try {
|
|
270
|
+
if (fs.statSync(file).mtimeMs > builtAtMs) return true
|
|
271
|
+
} catch {
|
|
272
|
+
return true
|
|
166
273
|
}
|
|
167
274
|
}
|
|
168
275
|
|
|
169
|
-
|
|
170
|
-
|
|
276
|
+
// Check .stash.json modification time
|
|
277
|
+
const stashPath = path.join(dirPath, ".stash.json")
|
|
278
|
+
try {
|
|
279
|
+
if (fs.existsSync(stashPath) && fs.statSync(stashPath).mtimeMs > builtAtMs) return true
|
|
280
|
+
} catch {
|
|
281
|
+
// ignore
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return false
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function migrateGeneratedSkillMetadata(
|
|
288
|
+
stash: StashFile,
|
|
289
|
+
files: string[],
|
|
290
|
+
typeRoot: string,
|
|
291
|
+
): { stash: StashFile; changed: boolean } {
|
|
292
|
+
const fileByBaseName = new Map(files.map((filePath) => [path.basename(filePath), filePath]))
|
|
293
|
+
let changed = false
|
|
294
|
+
|
|
295
|
+
const entries = stash.entries.map((entry) => {
|
|
296
|
+
if (entry.type !== "skill" || entry.generated !== true) return entry
|
|
297
|
+
|
|
298
|
+
const hintedFilePath = entry.entry ? fileByBaseName.get(path.basename(entry.entry)) : undefined
|
|
299
|
+
const skillFilePath = hintedFilePath ?? fileByBaseName.get("SKILL.md")
|
|
300
|
+
if (!skillFilePath) return entry
|
|
301
|
+
|
|
302
|
+
const canonicalName = deriveCanonicalAssetName("skill", typeRoot, skillFilePath)
|
|
303
|
+
if (!canonicalName || canonicalName === entry.name) return entry
|
|
304
|
+
|
|
305
|
+
changed = true
|
|
306
|
+
return { ...entry, name: canonicalName }
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
if (!changed) {
|
|
310
|
+
return { stash, changed: false }
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
stash: { entries },
|
|
315
|
+
changed: true,
|
|
316
|
+
}
|
|
171
317
|
}
|
|
172
318
|
|
|
173
|
-
function
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
319
|
+
async function enhanceStashWithLlm(
|
|
320
|
+
llmConfig: LlmConnectionConfig,
|
|
321
|
+
stash: StashFile,
|
|
322
|
+
dirPath: string,
|
|
323
|
+
files: string[],
|
|
324
|
+
): Promise<StashFile> {
|
|
325
|
+
const { enhanceMetadata } = await import("./llm.js")
|
|
326
|
+
|
|
327
|
+
const enhanced: StashEntry[] = []
|
|
328
|
+
for (const entry of stash.entries) {
|
|
329
|
+
try {
|
|
330
|
+
// Find the file matching this entry for content context
|
|
331
|
+
const entryFile = entry.entry
|
|
332
|
+
? files.find((f) => path.basename(f) === entry.entry) ?? files[0]
|
|
333
|
+
: files[0]
|
|
334
|
+
let fileContent: string | undefined
|
|
335
|
+
if (entryFile) {
|
|
336
|
+
try {
|
|
337
|
+
fileContent = fs.readFileSync(entryFile, "utf8")
|
|
338
|
+
} catch { /* ignore unreadable files */ }
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const improvements = await enhanceMetadata(llmConfig, entry, fileContent)
|
|
342
|
+
const updated = { ...entry }
|
|
343
|
+
if (improvements.description) updated.description = improvements.description
|
|
344
|
+
if (improvements.intents?.length) updated.intents = improvements.intents
|
|
345
|
+
if (improvements.tags?.length) updated.tags = improvements.tags
|
|
346
|
+
enhanced.push(updated)
|
|
347
|
+
} catch {
|
|
348
|
+
// LLM enhancement failed for this entry, keep original
|
|
349
|
+
enhanced.push(entry)
|
|
350
|
+
}
|
|
185
351
|
}
|
|
352
|
+
return { entries: enhanced }
|
|
186
353
|
}
|
|
187
354
|
|
|
188
355
|
export function buildSearchText(entry: StashEntry): string {
|
|
@@ -190,22 +357,15 @@ export function buildSearchText(entry: StashEntry): string {
|
|
|
190
357
|
if (entry.description) parts.push(entry.description)
|
|
191
358
|
if (entry.tags) parts.push(entry.tags.join(" "))
|
|
192
359
|
if (entry.examples) parts.push(entry.examples.join(" "))
|
|
360
|
+
if (entry.aliases) parts.push(entry.aliases.join(" "))
|
|
361
|
+
if (entry.intents) parts.push(entry.intents.join(" "))
|
|
193
362
|
if (entry.intent) {
|
|
194
363
|
if (entry.intent.when) parts.push(entry.intent.when)
|
|
195
364
|
if (entry.intent.input) parts.push(entry.intent.input)
|
|
196
365
|
if (entry.intent.output) parts.push(entry.intent.output)
|
|
197
366
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
function resolveStashDirForIndex(): string {
|
|
202
|
-
const raw = process.env.AGENTIKIT_STASH_DIR?.trim()
|
|
203
|
-
if (!raw) {
|
|
204
|
-
throw new Error("AGENTIKIT_STASH_DIR is not set. Run 'agentikit init' first.")
|
|
205
|
-
}
|
|
206
|
-
const stashDir = path.resolve(raw)
|
|
207
|
-
if (!fs.existsSync(stashDir) || !fs.statSync(stashDir).isDirectory()) {
|
|
208
|
-
throw new Error(`AGENTIKIT_STASH_DIR does not exist or is not a directory: "${stashDir}"`)
|
|
367
|
+
if (entry.toc) {
|
|
368
|
+
parts.push(entry.toc.map((h) => h.text).join(" "))
|
|
209
369
|
}
|
|
210
|
-
return
|
|
370
|
+
return parts.join(" ").toLowerCase()
|
|
211
371
|
}
|
package/src/init.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agentikit initialization logic.
|
|
3
|
+
*
|
|
4
|
+
* Creates the stash directory structure, sets the AGENTIKIT_STASH_DIR
|
|
5
|
+
* environment variable, and ensures ripgrep is available.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { spawnSync } from "node:child_process"
|
|
9
|
+
import fs from "node:fs"
|
|
10
|
+
import path from "node:path"
|
|
11
|
+
import { IS_WINDOWS, TYPE_DIRS } from "./common"
|
|
12
|
+
import { ensureRg } from "./ripgrep-install"
|
|
13
|
+
import { getConfigPath, saveConfig, DEFAULT_CONFIG } from "./config"
|
|
14
|
+
|
|
15
|
+
export interface InitResponse {
|
|
16
|
+
stashDir: string
|
|
17
|
+
created: boolean
|
|
18
|
+
envSet: boolean
|
|
19
|
+
profileUpdated?: string
|
|
20
|
+
configPath: string
|
|
21
|
+
ripgrep?: {
|
|
22
|
+
rgPath: string
|
|
23
|
+
installed: boolean
|
|
24
|
+
version: string
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function agentikitInit(): InitResponse {
|
|
29
|
+
let stashDir: string
|
|
30
|
+
if (IS_WINDOWS) {
|
|
31
|
+
const userProfile = process.env.USERPROFILE?.trim()
|
|
32
|
+
if (!userProfile) {
|
|
33
|
+
throw new Error("Unable to determine Documents folder. Ensure USERPROFILE is set.")
|
|
34
|
+
}
|
|
35
|
+
const docs = path.join(userProfile, "Documents")
|
|
36
|
+
stashDir = path.join(docs, "agentikit")
|
|
37
|
+
} else {
|
|
38
|
+
const home = process.env.HOME?.trim()
|
|
39
|
+
if (!home) {
|
|
40
|
+
throw new Error("Unable to determine home directory. Set HOME.")
|
|
41
|
+
}
|
|
42
|
+
stashDir = path.join(home, "agentikit")
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let created = false
|
|
46
|
+
if (!fs.existsSync(stashDir)) {
|
|
47
|
+
fs.mkdirSync(stashDir, { recursive: true })
|
|
48
|
+
created = true
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const sub of Object.values(TYPE_DIRS)) {
|
|
52
|
+
const subDir = path.join(stashDir, sub)
|
|
53
|
+
if (!fs.existsSync(subDir)) {
|
|
54
|
+
fs.mkdirSync(subDir, { recursive: true })
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let envSet = false
|
|
59
|
+
let profileUpdated: string | undefined
|
|
60
|
+
|
|
61
|
+
if (IS_WINDOWS) {
|
|
62
|
+
const result = spawnSync("setx", ["AGENTIKIT_STASH_DIR", stashDir], {
|
|
63
|
+
encoding: "utf8",
|
|
64
|
+
timeout: 10_000,
|
|
65
|
+
})
|
|
66
|
+
envSet = result.status === 0
|
|
67
|
+
} else {
|
|
68
|
+
const shell = process.env.SHELL || ""
|
|
69
|
+
const homeDir = process.env.HOME! // already validated non-empty above
|
|
70
|
+
let profile: string
|
|
71
|
+
if (shell.endsWith("/zsh")) {
|
|
72
|
+
profile = path.join(homeDir, ".zshrc")
|
|
73
|
+
} else if (shell.endsWith("/bash")) {
|
|
74
|
+
profile = path.join(homeDir, ".bashrc")
|
|
75
|
+
} else {
|
|
76
|
+
profile = path.join(homeDir, ".profile")
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const exportLine = `export AGENTIKIT_STASH_DIR="${stashDir}"`
|
|
80
|
+
const existing = fs.existsSync(profile) ? fs.readFileSync(profile, "utf8") : ""
|
|
81
|
+
if (!existing.includes("AGENTIKIT_STASH_DIR")) {
|
|
82
|
+
fs.appendFileSync(profile, `\n# Agentikit stash directory\n${exportLine}\n`)
|
|
83
|
+
envSet = true
|
|
84
|
+
profileUpdated = profile
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Create default config.json if it doesn't exist
|
|
89
|
+
const configPath = getConfigPath(stashDir)
|
|
90
|
+
if (!fs.existsSync(configPath)) {
|
|
91
|
+
saveConfig(DEFAULT_CONFIG, stashDir)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
process.env.AGENTIKIT_STASH_DIR = stashDir
|
|
95
|
+
|
|
96
|
+
// Ensure ripgrep is available (install to stash/bin if needed)
|
|
97
|
+
let ripgrep: InitResponse["ripgrep"]
|
|
98
|
+
try {
|
|
99
|
+
const rgResult = ensureRg(stashDir)
|
|
100
|
+
ripgrep = rgResult
|
|
101
|
+
} catch {
|
|
102
|
+
// Non-fatal: ripgrep is optional, search works without it
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { stashDir, created, envSet, profileUpdated, configPath, ripgrep }
|
|
106
|
+
}
|
package/src/llm.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { LlmConnectionConfig } from "./config"
|
|
2
|
+
import type { StashEntry } from "./metadata"
|
|
3
|
+
|
|
4
|
+
// ── OpenAI-compatible chat completions ──────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
interface ChatMessage {
|
|
7
|
+
role: "system" | "user" | "assistant"
|
|
8
|
+
content: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface ChatCompletionResponse {
|
|
12
|
+
choices: Array<{ message: { content: string } }>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function chatCompletion(
|
|
16
|
+
config: LlmConnectionConfig,
|
|
17
|
+
messages: ChatMessage[],
|
|
18
|
+
): Promise<string> {
|
|
19
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" }
|
|
20
|
+
if (config.apiKey) {
|
|
21
|
+
headers["Authorization"] = `Bearer ${config.apiKey}`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const response = await fetch(config.endpoint, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers,
|
|
27
|
+
body: JSON.stringify({
|
|
28
|
+
model: config.model,
|
|
29
|
+
messages,
|
|
30
|
+
temperature: 0.3,
|
|
31
|
+
max_tokens: 512,
|
|
32
|
+
}),
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
const body = await response.text().catch(() => "")
|
|
37
|
+
throw new Error(`LLM request failed (${response.status}): ${body}`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const json = (await response.json()) as ChatCompletionResponse
|
|
41
|
+
return json.choices?.[0]?.message?.content?.trim() ?? ""
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── Metadata Enhancement ────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
const SYSTEM_PROMPT = `You are a metadata generator for a developer tool registry. Given a tool/skill/command/agent entry, generate improved metadata. Respond with ONLY valid JSON, no markdown fencing.`
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Use an LLM to enhance a stash entry's metadata: improve description,
|
|
50
|
+
* generate intents, and suggest tags.
|
|
51
|
+
*/
|
|
52
|
+
export async function enhanceMetadata(
|
|
53
|
+
config: LlmConnectionConfig,
|
|
54
|
+
entry: StashEntry,
|
|
55
|
+
fileContent?: string,
|
|
56
|
+
): Promise<{ description?: string; intents?: string[]; tags?: string[] }> {
|
|
57
|
+
const contextParts = [
|
|
58
|
+
`Name: ${entry.name}`,
|
|
59
|
+
`Type: ${entry.type}`,
|
|
60
|
+
]
|
|
61
|
+
if (entry.description) contextParts.push(`Current description: ${entry.description}`)
|
|
62
|
+
if (entry.tags?.length) contextParts.push(`Current tags: ${entry.tags.join(", ")}`)
|
|
63
|
+
if (fileContent) {
|
|
64
|
+
// Limit content to first 2000 chars to stay within token limits
|
|
65
|
+
const truncated = fileContent.length > 2000
|
|
66
|
+
? fileContent.slice(0, 2000) + "\n... (truncated)"
|
|
67
|
+
: fileContent
|
|
68
|
+
contextParts.push(`File content:\n${truncated}`)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const userPrompt = `${contextParts.join("\n")}
|
|
72
|
+
|
|
73
|
+
Generate improved metadata for this ${entry.type}. Return JSON with these fields:
|
|
74
|
+
- "description": a clear, concise one-sentence description of what this does
|
|
75
|
+
- "intents": an array of 3-6 natural language task phrases an agent might use to find this (e.g. "deploy a docker container", "run database migrations")
|
|
76
|
+
- "tags": an array of 3-8 relevant keyword tags
|
|
77
|
+
|
|
78
|
+
Return ONLY the JSON object, no explanation.`
|
|
79
|
+
|
|
80
|
+
const raw = await chatCompletion(config, [
|
|
81
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
82
|
+
{ role: "user", content: userPrompt },
|
|
83
|
+
])
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
// Strip markdown code fences if present
|
|
87
|
+
const cleaned = raw.replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/i, "")
|
|
88
|
+
const parsed = JSON.parse(cleaned) as Record<string, unknown>
|
|
89
|
+
const result: { description?: string; intents?: string[]; tags?: string[] } = {}
|
|
90
|
+
|
|
91
|
+
if (typeof parsed.description === "string" && parsed.description) {
|
|
92
|
+
result.description = parsed.description
|
|
93
|
+
}
|
|
94
|
+
if (Array.isArray(parsed.intents)) {
|
|
95
|
+
result.intents = parsed.intents.filter(
|
|
96
|
+
(s): s is string => typeof s === "string" && s.trim().length > 0,
|
|
97
|
+
).slice(0, 8)
|
|
98
|
+
}
|
|
99
|
+
if (Array.isArray(parsed.tags)) {
|
|
100
|
+
result.tags = parsed.tags.filter(
|
|
101
|
+
(s): s is string => typeof s === "string" && s.trim().length > 0,
|
|
102
|
+
).slice(0, 10)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return result
|
|
106
|
+
} catch {
|
|
107
|
+
// LLM returned unparseable output, return empty
|
|
108
|
+
return {}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Check if the LLM endpoint is reachable.
|
|
114
|
+
*/
|
|
115
|
+
export async function isLlmAvailable(config: LlmConnectionConfig): Promise<boolean> {
|
|
116
|
+
try {
|
|
117
|
+
const result = await chatCompletion(config, [
|
|
118
|
+
{ role: "user", content: "Respond with just the word: ok" },
|
|
119
|
+
])
|
|
120
|
+
return result.length > 0
|
|
121
|
+
} catch {
|
|
122
|
+
return false
|
|
123
|
+
}
|
|
124
|
+
}
|