agentikit 0.0.9 → 0.0.12

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 (107) hide show
  1. package/README.md +129 -214
  2. package/dist/index.d.ts +8 -2
  3. package/dist/index.js +4 -1
  4. package/dist/src/asset-spec.d.ts +2 -0
  5. package/dist/src/asset-spec.js +22 -3
  6. package/dist/src/asset-type-handler.d.ts +27 -0
  7. package/dist/src/asset-type-handler.js +33 -0
  8. package/dist/src/cli.js +201 -75
  9. package/dist/src/common.d.ts +6 -1
  10. package/dist/src/common.js +18 -4
  11. package/dist/src/config-cli.d.ts +9 -0
  12. package/dist/src/config-cli.js +473 -0
  13. package/dist/src/config.d.ts +19 -6
  14. package/dist/src/config.js +139 -29
  15. package/dist/src/db.d.ts +46 -0
  16. package/dist/src/db.js +299 -0
  17. package/dist/src/embedder.js +12 -7
  18. package/dist/src/github.d.ts +4 -0
  19. package/dist/src/github.js +19 -0
  20. package/dist/src/handlers/agent-handler.d.ts +2 -0
  21. package/dist/src/handlers/agent-handler.js +26 -0
  22. package/dist/src/handlers/command-handler.d.ts +2 -0
  23. package/dist/src/handlers/command-handler.js +23 -0
  24. package/dist/src/handlers/index.d.ts +6 -0
  25. package/dist/src/handlers/index.js +23 -0
  26. package/dist/src/handlers/knowledge-handler.d.ts +2 -0
  27. package/dist/src/handlers/knowledge-handler.js +56 -0
  28. package/dist/src/handlers/markdown-helpers.d.ts +7 -0
  29. package/dist/src/handlers/markdown-helpers.js +15 -0
  30. package/dist/src/handlers/script-handler.d.ts +2 -0
  31. package/dist/src/handlers/script-handler.js +78 -0
  32. package/dist/src/handlers/skill-handler.d.ts +2 -0
  33. package/dist/src/handlers/skill-handler.js +30 -0
  34. package/dist/src/handlers/tool-handler.d.ts +2 -0
  35. package/dist/src/handlers/tool-handler.js +58 -0
  36. package/dist/src/indexer.d.ts +1 -23
  37. package/dist/src/indexer.js +162 -155
  38. package/dist/src/init.d.ts +2 -2
  39. package/dist/src/init.js +21 -9
  40. package/dist/src/llm.js +4 -3
  41. package/dist/src/metadata.d.ts +0 -1
  42. package/dist/src/metadata.js +6 -64
  43. package/dist/src/origin-resolve.d.ts +19 -0
  44. package/dist/src/origin-resolve.js +53 -0
  45. package/dist/src/registry-install.d.ts +2 -2
  46. package/dist/src/registry-install.js +142 -35
  47. package/dist/src/registry-resolve.js +90 -22
  48. package/dist/src/registry-search.d.ts +22 -0
  49. package/dist/src/registry-search.js +231 -97
  50. package/dist/src/registry-types.d.ts +9 -2
  51. package/dist/src/stash-add.js +4 -4
  52. package/dist/src/stash-clone.d.ts +22 -0
  53. package/dist/src/stash-clone.js +83 -0
  54. package/dist/src/stash-ref.d.ts +27 -3
  55. package/dist/src/stash-ref.js +63 -24
  56. package/dist/src/stash-registry.js +12 -12
  57. package/dist/src/stash-resolve.js +3 -0
  58. package/dist/src/stash-search.js +168 -164
  59. package/dist/src/stash-show.d.ts +1 -1
  60. package/dist/src/stash-show.js +28 -96
  61. package/dist/src/stash-source.d.ts +24 -0
  62. package/dist/src/stash-source.js +81 -0
  63. package/dist/src/stash-types.d.ts +14 -4
  64. package/dist/src/stash.d.ts +6 -0
  65. package/dist/src/stash.js +3 -0
  66. package/dist/src/tool-runner.d.ts +1 -1
  67. package/dist/src/tool-runner.js +18 -5
  68. package/package.json +7 -2
  69. package/src/asset-spec.ts +20 -4
  70. package/src/asset-type-handler.ts +77 -0
  71. package/src/cli.ts +213 -82
  72. package/src/common.ts +23 -5
  73. package/src/config-cli.ts +499 -0
  74. package/src/config.ts +160 -38
  75. package/src/db.ts +411 -0
  76. package/src/embedder.ts +22 -11
  77. package/src/github.ts +21 -0
  78. package/src/handlers/agent-handler.ts +32 -0
  79. package/src/handlers/command-handler.ts +29 -0
  80. package/src/handlers/index.ts +25 -0
  81. package/src/handlers/knowledge-handler.ts +62 -0
  82. package/src/handlers/markdown-helpers.ts +19 -0
  83. package/src/handlers/script-handler.ts +92 -0
  84. package/src/handlers/skill-handler.ts +37 -0
  85. package/src/handlers/tool-handler.ts +71 -0
  86. package/src/indexer.ts +208 -187
  87. package/src/init.ts +17 -9
  88. package/src/llm.ts +4 -3
  89. package/src/metadata.ts +5 -65
  90. package/src/origin-resolve.ts +67 -0
  91. package/src/registry-install.ts +158 -42
  92. package/src/registry-resolve.ts +92 -23
  93. package/src/registry-search.ts +288 -98
  94. package/src/registry-types.ts +10 -2
  95. package/src/stash-add.ts +14 -17
  96. package/src/stash-clone.ts +127 -0
  97. package/src/stash-ref.ts +84 -26
  98. package/src/stash-registry.ts +12 -12
  99. package/src/stash-resolve.ts +3 -0
  100. package/src/stash-search.ts +202 -184
  101. package/src/stash-show.ts +33 -90
  102. package/src/stash-source.ts +103 -0
  103. package/src/stash-types.ts +14 -4
  104. package/src/stash.ts +8 -0
  105. package/src/tool-runner.ts +18 -5
  106. package/dist/src/similarity.d.ts +0 -34
  107. package/src/similarity.ts +0 -271
package/src/indexer.ts CHANGED
@@ -9,33 +9,27 @@ import {
9
9
  writeStashFile,
10
10
  generateMetadata,
11
11
  } from "./metadata"
12
- import { TfIdfAdapter, type ScoredEntry, type SerializedTfIdf } from "./similarity"
13
12
  import { walkStash } from "./walker"
14
- import type { EmbeddingVector } from "./embedder"
15
13
  import type { LlmConnectionConfig } from "./config"
14
+ import {
15
+ openDatabase,
16
+ closeDatabase,
17
+ getDbPath,
18
+ getMeta,
19
+ setMeta,
20
+ upsertEntry,
21
+ deleteEntriesByDir,
22
+ rebuildFts,
23
+ upsertEmbedding,
24
+ getEntriesByDir,
25
+ getEntryCount,
26
+ isVecAvailable,
27
+ DB_VERSION,
28
+ type DbIndexedEntry,
29
+ } from "./db"
16
30
 
17
31
  // ── Types ───────────────────────────────────────────────────────────────────
18
32
 
19
- export interface IndexedEntry {
20
- entry: StashEntry
21
- path: string
22
- dirPath: string
23
- embedding?: EmbeddingVector
24
- }
25
-
26
- export interface SearchIndex {
27
- version: number
28
- builtAt: string
29
- stashDir: string
30
- /** All stash directories that were indexed (primary + additional) */
31
- stashDirs?: string[]
32
- entries: IndexedEntry[]
33
- /** Serialized TF-IDF state (term frequencies, idf values) */
34
- tfidf?: SerializedTfIdf
35
- /** Whether embeddings are included in entries */
36
- hasEmbeddings?: boolean
37
- }
38
-
39
33
  export interface IndexResponse {
40
34
  stashDir: string
41
35
  totalEntries: number
@@ -45,211 +39,240 @@ export interface IndexResponse {
45
39
  directoriesScanned: number
46
40
  directoriesSkipped: number
47
41
  /** Timing counters in milliseconds */
48
- timing?: { totalMs: number; walkMs: number; embedMs: number; tfidfMs: number }
42
+ timing?: { totalMs: number; walkMs: number; embedMs: number; ftsMs: number }
49
43
  }
50
44
 
51
- // ── Constants ───────────────────────────────────────────────────────────────
45
+ // ── Indexer ──────────────────────────────────────────────────────────────────
52
46
 
53
- const INDEX_VERSION = 4
47
+ export async function agentikitIndex(options?: { stashDir?: string; full?: boolean }): Promise<IndexResponse> {
48
+ const stashDir = options?.stashDir || resolveStashDir()
54
49
 
55
- // ── Index Path ──────────────────────────────────────────────────────────────
50
+ // Load config and resolve all stash sources
51
+ const { loadConfig } = await import("./config.js")
52
+ const config = loadConfig()
53
+ const { resolveAllStashDirs } = await import("./stash-source.js")
54
+ const allStashDirs = resolveAllStashDirs(stashDir)
56
55
 
57
- export function getIndexPath(): string {
58
- const cacheDir = process.env.XDG_CACHE_HOME
59
- || path.join(process.env.HOME || process.env.USERPROFILE || "", ".cache")
60
- return path.join(cacheDir, "agentikit", "index.json")
61
- }
56
+ const t0 = Date.now()
57
+
58
+ // Open database pass embedding dimension from config if available
59
+ const dbPath = getDbPath()
60
+ const embeddingDim = config.embedding?.dimension
61
+ const db = openDatabase(dbPath, embeddingDim ? { embeddingDim } : undefined)
62
62
 
63
- export function loadSearchIndex(): SearchIndex | null {
64
- const indexPath = getIndexPath()
65
- if (!fs.existsSync(indexPath)) return null
66
63
  try {
67
- const raw = JSON.parse(fs.readFileSync(indexPath, "utf8"))
68
- if (raw?.version !== INDEX_VERSION) return null
69
- return raw as SearchIndex
70
- } catch {
71
- return null
72
- }
73
- }
64
+ // Check if we should do incremental
65
+ const prevStashDir = getMeta(db, "stashDir")
66
+ const prevBuiltAt = getMeta(db, "builtAt")
67
+ const isIncremental = !options?.full && prevStashDir === stashDir && !!prevBuiltAt
68
+ const builtAtMs = isIncremental ? new Date(prevBuiltAt!).getTime() : 0
69
+
70
+ if (options?.full || !isIncremental) {
71
+ // Wipe all entries for full rebuild or stashDir change
72
+ db.exec("DELETE FROM entries")
73
+ db.exec("DELETE FROM entries_fts")
74
+ if (isVecAvailable()) {
75
+ try { db.exec("DELETE FROM entries_vec") } catch { /* ignore */ }
76
+ }
77
+ }
74
78
 
75
- // ── Indexer ──────────────────────────────────────────────────────────────────
79
+ const tWalkStart = Date.now()
76
80
 
77
- export async function agentikitIndex(options?: { stashDir?: string; full?: boolean }): Promise<IndexResponse> {
78
- const stashDir = options?.stashDir || resolveStashDir()
81
+ // Walk stash dirs and index entries
82
+ const { scannedDirs, skippedDirs, generatedCount, dirsNeedingLlm } = indexEntries(db, allStashDirs, stashDir, isIncremental, builtAtMs)
79
83
 
80
- // Load config to get additional stash dirs and semantic search setting
81
- const { loadConfig } = await import("./config.js")
82
- const config = loadConfig(stashDir)
84
+ // Enhance entries with LLM if configured
85
+ await enhanceDirsWithLlm(db, config, dirsNeedingLlm)
83
86
 
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
- }
87
+ const tWalkEnd = Date.now()
92
88
 
93
- const t0 = Date.now()
94
- const allEntries: IndexedEntry[] = []
95
- let generatedCount = 0
96
- let scannedDirs = 0
97
- let skippedDirs = 0
89
+ // Rebuild FTS after all inserts
90
+ rebuildFts(db)
91
+ const tFtsEnd = Date.now()
92
+
93
+ // Generate embeddings if semantic search is enabled
94
+ const hasEmbeddings = await generateEmbeddingsForDb(db, config)
95
+
96
+ const tEmbedEnd = Date.now()
97
+
98
+ // Update metadata
99
+ setMeta(db, "version", String(DB_VERSION))
100
+ setMeta(db, "builtAt", new Date().toISOString())
101
+ setMeta(db, "stashDir", stashDir)
102
+ setMeta(db, "stashDirs", JSON.stringify(allStashDirs))
103
+ setMeta(db, "hasEmbeddings", hasEmbeddings ? "1" : "0")
104
+
105
+ const totalEntries = getEntryCount(db)
98
106
 
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
103
-
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)
107
+ const tEnd = Date.now()
108
+
109
+ return {
110
+ stashDir,
111
+ totalEntries,
112
+ generatedMetadata: generatedCount,
113
+ indexPath: dbPath,
114
+ mode: isIncremental ? "incremental" : "full",
115
+ directoriesScanned: scannedDirs,
116
+ directoriesSkipped: skippedDirs,
117
+ timing: {
118
+ totalMs: tEnd - t0,
119
+ walkMs: tWalkEnd - tWalkStart,
120
+ embedMs: tEmbedEnd - tFtsEnd,
121
+ ftsMs: tFtsEnd - tWalkEnd,
122
+ },
111
123
  }
124
+ } finally {
125
+ closeDatabase(db)
112
126
  }
127
+ }
128
+
129
+ // ── Extracted helpers for agentikitIndex ─────────────────────────────────────
113
130
 
131
+ function indexEntries(
132
+ db: import("bun:sqlite").Database,
133
+ allStashDirs: string[],
134
+ stashDir: string,
135
+ isIncremental: boolean,
136
+ builtAtMs: number,
137
+ ): { scannedDirs: number; skippedDirs: number; generatedCount: number; dirsNeedingLlm: Array<{ dirPath: string; files: string[]; assetType: AgentikitAssetType; currentStashDir: string }> } {
138
+ let scannedDirs = 0
139
+ let skippedDirs = 0
140
+ let generatedCount = 0
114
141
  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)
126
-
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))
131
-
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
138
- }
142
+ const dirsNeedingLlm: Array<{ dirPath: string; files: string[]; assetType: AgentikitAssetType; currentStashDir: string }> = []
139
143
 
140
- scannedDirs++
144
+ const insertTransaction = db.transaction(() => {
145
+ for (const currentStashDir of allStashDirs) {
146
+ for (const assetType of ASSET_TYPES as AgentikitAssetType[]) {
147
+ const typeRoot = path.join(currentStashDir, TYPE_DIRS[assetType])
148
+ try {
149
+ if (!fs.statSync(typeRoot).isDirectory()) continue
150
+ } catch { continue }
151
+
152
+ const dirGroups = walkStash(typeRoot, assetType)
153
+
154
+ for (const { dirPath, files } of dirGroups) {
155
+ if (seenPaths.has(path.resolve(dirPath))) continue
156
+ seenPaths.add(path.resolve(dirPath))
157
+
158
+ // Incremental: skip directories that haven't changed
159
+ if (isIncremental) {
160
+ const prevEntries = getEntriesByDir(db, dirPath)
161
+ if (prevEntries.length > 0 && !isDirStale(dirPath, files, prevEntries, builtAtMs)) {
162
+ skippedDirs++
163
+ continue
164
+ }
165
+ }
141
166
 
142
- // Try loading existing .stash.json
143
- let stash = loadStashFile(dirPath)
167
+ scannedDirs++
144
168
 
145
- if (stash) {
146
- const migration = migrateGeneratedSkillMetadata(stash, files, typeRoot)
147
- if (migration.changed) {
148
- stash = migration.stash
149
- writeStashFile(dirPath, stash)
150
- }
151
- }
169
+ // Delete old entries for this dir (will be re-inserted)
170
+ deleteEntriesByDir(db, dirPath)
152
171
 
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
172
+ // Try loading existing .stash.json (user metadata overrides)
173
+ let stash = loadStashFile(dirPath)
174
+
175
+ if (stash) {
176
+ const migration = migrateGeneratedSkillMetadata(stash, files, typeRoot)
177
+ if (migration.changed) {
178
+ stash = migration.stash
179
+ writeStashFile(dirPath, stash)
180
+ }
163
181
  }
164
- }
165
182
 
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 })
183
+ if (!stash) {
184
+ // Generate metadata heuristically
185
+ stash = generateMetadata(dirPath, assetType, files, typeRoot)
186
+ if (stash.entries.length > 0) {
187
+ writeStashFile(dirPath, stash)
188
+ generatedCount += stash.entries.length
189
+ }
172
190
  }
173
- }
174
- }
175
- }
176
- }
177
191
 
178
- const tWalkEnd = Date.now()
179
-
180
- // Build TF-IDF index
181
- const adapter = new TfIdfAdapter()
182
- const scoredEntries: ScoredEntry[] = allEntries.map((ie) => ({
183
- id: `${ie.entry.type}:${ie.entry.name}`,
184
- text: buildSearchText(ie.entry),
185
- entry: ie.entry,
186
- path: ie.path,
187
- }))
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)
192
+ if (stash) {
193
+ for (const entry of stash.entries) {
194
+ const entryPath = entry.entry
195
+ ? path.join(dirPath, entry.entry)
196
+ : files[0] || dirPath
197
+ const entryKey = `${currentStashDir}:${entry.type}:${entry.name}`
198
+ const searchText = buildSearchText(entry)
199
+
200
+ upsertEntry(db, entryKey, dirPath, entryPath, currentStashDir, entry, searchText)
201
+ }
202
+
203
+ // Collect dirs needing LLM enhancement during the first walk
204
+ if (stash.entries.some((e) => e.generated)) {
205
+ dirsNeedingLlm.push({ dirPath, files, assetType, currentStashDir })
206
+ }
207
+ }
200
208
  }
201
209
  }
202
- hasEmbeddings = true
203
- } catch {
204
- // Embedding provider not available, continue without embeddings
205
210
  }
206
- }
211
+ })
207
212
 
208
- const tEmbedEnd = Date.now()
213
+ insertTransaction()
209
214
 
210
- // Persist index
211
- const indexPath = getIndexPath()
212
- const indexDir = path.dirname(indexPath)
213
- if (!fs.existsSync(indexDir)) {
214
- fs.mkdirSync(indexDir, { recursive: true })
215
- }
215
+ return { scannedDirs, skippedDirs, generatedCount, dirsNeedingLlm }
216
+ }
216
217
 
217
- const index: SearchIndex = {
218
- version: INDEX_VERSION,
219
- builtAt: new Date().toISOString(),
220
- stashDir,
221
- stashDirs: allStashDirs,
222
- entries: allEntries,
223
- tfidf: adapter.serialize(),
224
- hasEmbeddings,
218
+ async function enhanceDirsWithLlm(
219
+ db: import("bun:sqlite").Database,
220
+ config: import("./config").AgentikitConfig,
221
+ dirsNeedingLlm: Array<{ dirPath: string; files: string[]; assetType: AgentikitAssetType; currentStashDir: string }>,
222
+ ): Promise<void> {
223
+ if (!config.llm || dirsNeedingLlm.length === 0) return
224
+
225
+ for (const { dirPath, files, currentStashDir } of dirsNeedingLlm) {
226
+ let stash = loadStashFile(dirPath)
227
+ if (!stash) continue
228
+ stash = await enhanceStashWithLlm(config.llm, stash, dirPath, files)
229
+ writeStashFile(dirPath, stash)
230
+
231
+ // Re-upsert enhanced entries
232
+ for (const entry of stash.entries) {
233
+ const entryPath = entry.entry ? path.join(dirPath, entry.entry) : files[0] || dirPath
234
+ const entryKey = `${currentStashDir}:${entry.type}:${entry.name}`
235
+ const searchText = buildSearchText(entry)
236
+ upsertEntry(db, entryKey, dirPath, entryPath, currentStashDir, entry, searchText)
237
+ }
225
238
  }
226
- fs.writeFileSync(indexPath, JSON.stringify(index) + "\n", "utf8")
239
+ }
227
240
 
228
- const tEnd = Date.now()
241
+ async function generateEmbeddingsForDb(
242
+ db: import("bun:sqlite").Database,
243
+ config: import("./config").AgentikitConfig,
244
+ ): Promise<boolean> {
245
+ if (!config.semanticSearch || !isVecAvailable()) return false
229
246
 
230
- return {
231
- stashDir,
232
- totalEntries: allEntries.length,
233
- generatedMetadata: generatedCount,
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
- },
247
+ try {
248
+ const { embed } = await import("./embedder.js")
249
+ const allEntries = getAllEntriesForEmbedding(db)
250
+ for (const { id, searchText } of allEntries) {
251
+ const embedding = await embed(searchText, config.embedding)
252
+ upsertEmbedding(db, id, embedding)
253
+ }
254
+ return true
255
+ } catch (error) {
256
+ console.warn("Embedding generation failed, continuing without:", error instanceof Error ? error.message : String(error))
257
+ return false
244
258
  }
245
259
  }
246
260
 
247
261
  // ── Helpers ─────────────────────────────────────────────────────────────────
248
262
 
263
+ function getAllEntriesForEmbedding(db: import("bun:sqlite").Database): Array<{ id: number; searchText: string }> {
264
+ return db
265
+ .prepare(`
266
+ SELECT e.id, e.search_text AS searchText FROM entries e
267
+ WHERE NOT EXISTS (SELECT 1 FROM entries_vec v WHERE v.id = e.id)
268
+ `)
269
+ .all() as Array<{ id: number; searchText: string }>
270
+ }
271
+
249
272
  function isDirStale(
250
273
  dirPath: string,
251
274
  currentFiles: string[],
252
- previousEntries: IndexedEntry[],
275
+ previousEntries: DbIndexedEntry[],
253
276
  builtAtMs: number,
254
277
  ): boolean {
255
278
  // Check if file set changed (additions or deletions)
@@ -327,7 +350,6 @@ async function enhanceStashWithLlm(
327
350
  const enhanced: StashEntry[] = []
328
351
  for (const entry of stash.entries) {
329
352
  try {
330
- // Find the file matching this entry for content context
331
353
  const entryFile = entry.entry
332
354
  ? files.find((f) => path.basename(f) === entry.entry) ?? files[0]
333
355
  : files[0]
@@ -345,7 +367,6 @@ async function enhanceStashWithLlm(
345
367
  if (improvements.tags?.length) updated.tags = improvements.tags
346
368
  enhanced.push(updated)
347
369
  } catch {
348
- // LLM enhancement failed for this entry, keep original
349
370
  enhanced.push(entry)
350
371
  }
351
372
  }
package/src/init.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Agentikit initialization logic.
3
3
  *
4
- * Creates the stash directory structure, sets the AGENTIKIT_STASH_DIR
4
+ * Creates the working stash directory structure, sets the AKM_STASH_DIR
5
5
  * environment variable, and ensures ripgrep is available.
6
6
  */
7
7
 
@@ -25,7 +25,7 @@ export interface InitResponse {
25
25
  }
26
26
  }
27
27
 
28
- export function agentikitInit(): InitResponse {
28
+ export async function agentikitInit(): Promise<InitResponse> {
29
29
  let stashDir: string
30
30
  if (IS_WINDOWS) {
31
31
  const userProfile = process.env.USERPROFILE?.trim()
@@ -59,7 +59,7 @@ export function agentikitInit(): InitResponse {
59
59
  let profileUpdated: string | undefined
60
60
 
61
61
  if (IS_WINDOWS) {
62
- const result = spawnSync("setx", ["AGENTIKIT_STASH_DIR", stashDir], {
62
+ const result = spawnSync("setx", ["AKM_STASH_DIR", stashDir], {
63
63
  encoding: "utf8",
64
64
  timeout: 10_000,
65
65
  })
@@ -76,22 +76,30 @@ export function agentikitInit(): InitResponse {
76
76
  profile = path.join(homeDir, ".profile")
77
77
  }
78
78
 
79
- const exportLine = `export AGENTIKIT_STASH_DIR="${stashDir}"`
79
+ const exportLine = `export AKM_STASH_DIR="${stashDir}"`
80
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`)
81
+ if (!existing.includes("AKM_STASH_DIR")) {
82
+ const updated = existing + `\n# Agentikit working stash directory\n${exportLine}\n`
83
+ const tmpPath = profile + `.tmp.${process.pid}`
84
+ try {
85
+ fs.writeFileSync(tmpPath, updated, "utf8")
86
+ fs.renameSync(tmpPath, profile)
87
+ } catch (err) {
88
+ try { fs.unlinkSync(tmpPath) } catch { /* ignore */ }
89
+ throw err
90
+ }
83
91
  envSet = true
84
92
  profileUpdated = profile
85
93
  }
86
94
  }
87
95
 
88
96
  // Create default config.json if it doesn't exist
89
- const configPath = getConfigPath(stashDir)
97
+ const configPath = getConfigPath()
90
98
  if (!fs.existsSync(configPath)) {
91
- saveConfig(DEFAULT_CONFIG, stashDir)
99
+ saveConfig(DEFAULT_CONFIG)
92
100
  }
93
101
 
94
- process.env.AGENTIKIT_STASH_DIR = stashDir
102
+ process.env.AKM_STASH_DIR = stashDir
95
103
 
96
104
  // Ensure ripgrep is available (install to stash/bin if needed)
97
105
  let ripgrep: InitResponse["ripgrep"]
package/src/llm.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { LlmConnectionConfig } from "./config"
2
+ import { fetchWithTimeout } from "./common"
2
3
  import type { StashEntry } from "./metadata"
3
4
 
4
5
  // ── OpenAI-compatible chat completions ──────────────────────────────────────
@@ -21,14 +22,14 @@ async function chatCompletion(
21
22
  headers["Authorization"] = `Bearer ${config.apiKey}`
22
23
  }
23
24
 
24
- const response = await fetch(config.endpoint, {
25
+ const response = await fetchWithTimeout(config.endpoint, {
25
26
  method: "POST",
26
27
  headers,
27
28
  body: JSON.stringify({
28
29
  model: config.model,
29
30
  messages,
30
- temperature: 0.3,
31
- max_tokens: 512,
31
+ temperature: config.temperature ?? 0.3,
32
+ max_tokens: config.maxTokens ?? 512,
32
33
  }),
33
34
  })
34
35
 
package/src/metadata.ts CHANGED
@@ -4,6 +4,7 @@ import { type AgentikitAssetType, isAssetType } from "./common"
4
4
  import { SCRIPT_EXTENSIONS, isRelevantAssetFile, deriveCanonicalAssetName } from "./asset-spec"
5
5
  import { parseFrontmatter, toStringOrUndefined } from "./frontmatter"
6
6
  import { parseMarkdownToc, type TocHeading } from "./markdown"
7
+ import { tryGetHandler } from "./asset-type-handler"
7
8
 
8
9
  // ── Schema ──────────────────────────────────────────────────────────────────
9
10
 
@@ -183,25 +184,10 @@ export function generateMetadata(
183
184
  }
184
185
  }
185
186
 
186
- // Knowledge entries: generate TOC from headings
187
- if (assetType === "knowledge") {
188
- try {
189
- const mdContent = fs.readFileSync(file, "utf8")
190
- const toc = parseMarkdownToc(mdContent)
191
- if (toc.headings.length > 0) entry.toc = toc.headings
192
- } catch {
193
- // Non-fatal: skip TOC if file can't be read
194
- }
195
- }
196
-
197
- // Priority 3: Code comments (for script files)
198
- if (SCRIPT_EXTENSIONS.has(ext) && ext !== ".md") {
199
- const commentDesc = extractDescriptionFromComments(file)
200
- if (commentDesc && !entry.description) {
201
- entry.description = commentDesc
202
- entry.source = "comments"
203
- entry.confidence = 0.7
204
- }
187
+ // Type-specific metadata extraction (e.g. TOC for knowledge, comments for tools/scripts)
188
+ const handler = tryGetHandler(assetType)
189
+ if (handler?.extractTypeMetadata) {
190
+ handler.extractTypeMetadata(entry, file, ext)
205
191
  }
206
192
 
207
193
  // Priority 4: Filename heuristics (fallback)
@@ -249,52 +235,6 @@ function buildAliases(name: string, tags: string[]): string[] {
249
235
  return Array.from(aliases)
250
236
  }
251
237
 
252
- // ── Intent Generation ────────────────────────────────────────────────────────
253
-
254
- export function generateIntents(description: string, tags: string[], name: string): string[] {
255
- const intents = new Set<string>()
256
-
257
- // Split name on separators to extract tokens and potential verb
258
- const nameTokens = name
259
- .replace(/[-_]+/g, " ")
260
- .replace(/([a-z])([A-Z])/g, "$1 $2")
261
- .toLowerCase()
262
- .trim()
263
- .split(/\s+/)
264
- .filter((t) => t.length > 1)
265
-
266
- // Intent from name as phrase (e.g. "summarize diff")
267
- const namePhrase = nameTokens.join(" ")
268
- if (namePhrase.length > 2) intents.add(namePhrase)
269
-
270
- // Intent from description (lowercased)
271
- const desc = description.toLowerCase().trim()
272
- if (desc.length > 2) intents.add(desc)
273
-
274
- // Combine first name token (potential verb) with tags
275
- // e.g. name "summarize-diff", tags ["git"] → "summarize git diff"
276
- if (nameTokens.length >= 1 && tags.length > 0) {
277
- const verb = nameTokens[0]
278
- const rest = nameTokens.slice(1).join(" ")
279
- for (const tag of tags) {
280
- const tagLower = tag.toLowerCase()
281
- // verb + tag + rest (e.g. "summarize git diff")
282
- const parts = [verb, tagLower, rest].filter((p) => p.length > 0)
283
- const phrase = parts.join(" ")
284
- if (phrase !== namePhrase && phrase.length > 2) intents.add(phrase)
285
- }
286
- }
287
-
288
- // Join tag pairs (e.g. ["git", "diff"] → "git diff")
289
- if (tags.length >= 2) {
290
- const tagPhrase = tags.map((t) => t.toLowerCase()).join(" ")
291
- if (tagPhrase.length > 2) intents.add(tagPhrase)
292
- }
293
-
294
- // Cap at 8 intents
295
- return Array.from(intents).slice(0, 8)
296
- }
297
-
298
238
  export function extractDescriptionFromComments(filePath: string): string | null {
299
239
  let content: string
300
240
  try {