agentikit 0.0.13 → 0.0.15

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 (156) hide show
  1. package/LICENSE +385 -0
  2. package/README.md +187 -110
  3. package/dist/{src/asset-spec.js → asset-spec.js} +11 -2
  4. package/dist/{src/asset-type-handler.js → asset-type-handler.js} +4 -3
  5. package/dist/cli.js +709 -0
  6. package/dist/common.js +192 -0
  7. package/dist/{src/config-cli.js → config-cli.js} +36 -30
  8. package/dist/{src/config.js → config.js} +95 -25
  9. package/dist/{src/db.js → db.js} +123 -51
  10. package/dist/{src/embedder.js → embedder.js} +57 -2
  11. package/dist/errors.js +28 -0
  12. package/dist/file-context.js +188 -0
  13. package/dist/{src/frontmatter.js → frontmatter.js} +1 -1
  14. package/dist/{src/github.js → github.js} +1 -3
  15. package/dist/handlers/agent-handler.js +19 -0
  16. package/dist/handlers/command-handler.js +20 -0
  17. package/dist/handlers/handler-bridge.js +51 -0
  18. package/dist/handlers/index.js +19 -0
  19. package/dist/handlers/knowledge-handler.js +32 -0
  20. package/dist/handlers/script-handler.js +42 -0
  21. package/dist/{src/handlers → handlers}/skill-handler.js +5 -6
  22. package/dist/{src/handlers → handlers}/tool-handler.js +8 -24
  23. package/dist/{src/indexer.js → indexer.js} +50 -26
  24. package/dist/init.js +43 -0
  25. package/dist/{src/llm.js → llm.js} +6 -11
  26. package/dist/lockfile.js +60 -0
  27. package/dist/matchers.js +163 -0
  28. package/dist/{src/metadata.js → metadata.js} +36 -16
  29. package/dist/{src/origin-resolve.js → origin-resolve.js} +10 -9
  30. package/dist/paths.js +83 -0
  31. package/dist/{src/registry-install.js → registry-install.js} +151 -19
  32. package/dist/{src/registry-resolve.js → registry-resolve.js} +190 -26
  33. package/dist/{src/registry-search.js → registry-search.js} +13 -21
  34. package/dist/renderers.js +286 -0
  35. package/dist/{src/ripgrep-install.js → ripgrep-install.js} +8 -27
  36. package/dist/{src/ripgrep-resolve.js → ripgrep-resolve.js} +21 -11
  37. package/dist/ripgrep.js +2 -0
  38. package/dist/self-update.js +226 -0
  39. package/dist/{src/stash-add.js → stash-add.js} +14 -4
  40. package/dist/stash-clone.js +115 -0
  41. package/dist/{src/stash-ref.js → stash-ref.js} +10 -9
  42. package/dist/{src/stash-registry.js → stash-registry.js} +21 -46
  43. package/dist/{src/stash-resolve.js → stash-resolve.js} +10 -9
  44. package/dist/{src/stash-search.js → stash-search.js} +89 -74
  45. package/dist/stash-show.js +74 -0
  46. package/dist/stash-source.js +127 -0
  47. package/dist/submit.js +557 -0
  48. package/dist/{src/tool-runner.js → tool-runner.js} +1 -5
  49. package/dist/{src/walker.js → walker.js} +38 -0
  50. package/dist/warn.js +20 -0
  51. package/package.json +13 -18
  52. package/dist/index.d.ts +0 -28
  53. package/dist/index.js +0 -15
  54. package/dist/src/asset-spec.d.ts +0 -16
  55. package/dist/src/asset-type-handler.d.ts +0 -27
  56. package/dist/src/cli.d.ts +0 -2
  57. package/dist/src/cli.js +0 -399
  58. package/dist/src/common.d.ts +0 -13
  59. package/dist/src/common.js +0 -60
  60. package/dist/src/config-cli.d.ts +0 -9
  61. package/dist/src/config.d.ts +0 -50
  62. package/dist/src/db.d.ts +0 -46
  63. package/dist/src/embedder.d.ts +0 -10
  64. package/dist/src/frontmatter.d.ts +0 -30
  65. package/dist/src/github.d.ts +0 -4
  66. package/dist/src/handlers/agent-handler.d.ts +0 -2
  67. package/dist/src/handlers/agent-handler.js +0 -26
  68. package/dist/src/handlers/command-handler.d.ts +0 -2
  69. package/dist/src/handlers/command-handler.js +0 -23
  70. package/dist/src/handlers/index.d.ts +0 -6
  71. package/dist/src/handlers/index.js +0 -23
  72. package/dist/src/handlers/knowledge-handler.d.ts +0 -2
  73. package/dist/src/handlers/knowledge-handler.js +0 -56
  74. package/dist/src/handlers/markdown-helpers.d.ts +0 -7
  75. package/dist/src/handlers/script-handler.d.ts +0 -2
  76. package/dist/src/handlers/script-handler.js +0 -78
  77. package/dist/src/handlers/skill-handler.d.ts +0 -2
  78. package/dist/src/handlers/tool-handler.d.ts +0 -2
  79. package/dist/src/indexer.d.ts +0 -22
  80. package/dist/src/init.d.ts +0 -19
  81. package/dist/src/init.js +0 -99
  82. package/dist/src/llm.d.ts +0 -15
  83. package/dist/src/markdown.d.ts +0 -18
  84. package/dist/src/metadata.d.ts +0 -41
  85. package/dist/src/origin-resolve.d.ts +0 -19
  86. package/dist/src/registry-install.d.ts +0 -11
  87. package/dist/src/registry-resolve.d.ts +0 -3
  88. package/dist/src/registry-search.d.ts +0 -27
  89. package/dist/src/registry-types.d.ts +0 -62
  90. package/dist/src/ripgrep-install.d.ts +0 -12
  91. package/dist/src/ripgrep-resolve.d.ts +0 -13
  92. package/dist/src/ripgrep.d.ts +0 -3
  93. package/dist/src/ripgrep.js +0 -2
  94. package/dist/src/stash-add.d.ts +0 -4
  95. package/dist/src/stash-clone.d.ts +0 -22
  96. package/dist/src/stash-clone.js +0 -83
  97. package/dist/src/stash-ref.d.ts +0 -31
  98. package/dist/src/stash-registry.d.ts +0 -18
  99. package/dist/src/stash-resolve.d.ts +0 -2
  100. package/dist/src/stash-search.d.ts +0 -8
  101. package/dist/src/stash-show.d.ts +0 -5
  102. package/dist/src/stash-show.js +0 -46
  103. package/dist/src/stash-source.d.ts +0 -24
  104. package/dist/src/stash-source.js +0 -81
  105. package/dist/src/stash-types.d.ts +0 -227
  106. package/dist/src/stash.d.ts +0 -16
  107. package/dist/src/stash.js +0 -9
  108. package/dist/src/tool-runner.d.ts +0 -35
  109. package/dist/src/walker.d.ts +0 -19
  110. package/src/asset-spec.ts +0 -85
  111. package/src/asset-type-handler.ts +0 -77
  112. package/src/cli.ts +0 -427
  113. package/src/common.ts +0 -76
  114. package/src/config-cli.ts +0 -499
  115. package/src/config.ts +0 -305
  116. package/src/db.ts +0 -411
  117. package/src/embedder.ts +0 -128
  118. package/src/frontmatter.ts +0 -95
  119. package/src/github.ts +0 -21
  120. package/src/handlers/agent-handler.ts +0 -32
  121. package/src/handlers/command-handler.ts +0 -29
  122. package/src/handlers/index.ts +0 -25
  123. package/src/handlers/knowledge-handler.ts +0 -62
  124. package/src/handlers/markdown-helpers.ts +0 -19
  125. package/src/handlers/script-handler.ts +0 -92
  126. package/src/handlers/skill-handler.ts +0 -37
  127. package/src/handlers/tool-handler.ts +0 -71
  128. package/src/indexer.ts +0 -392
  129. package/src/init.ts +0 -114
  130. package/src/llm.ts +0 -125
  131. package/src/markdown.ts +0 -106
  132. package/src/metadata.ts +0 -333
  133. package/src/origin-resolve.ts +0 -67
  134. package/src/registry-install.ts +0 -361
  135. package/src/registry-resolve.ts +0 -341
  136. package/src/registry-search.ts +0 -335
  137. package/src/registry-types.ts +0 -72
  138. package/src/ripgrep-install.ts +0 -200
  139. package/src/ripgrep-resolve.ts +0 -72
  140. package/src/ripgrep.ts +0 -3
  141. package/src/stash-add.ts +0 -63
  142. package/src/stash-clone.ts +0 -127
  143. package/src/stash-ref.ts +0 -99
  144. package/src/stash-registry.ts +0 -259
  145. package/src/stash-resolve.ts +0 -50
  146. package/src/stash-search.ts +0 -613
  147. package/src/stash-show.ts +0 -55
  148. package/src/stash-source.ts +0 -103
  149. package/src/stash-types.ts +0 -231
  150. package/src/stash.ts +0 -39
  151. package/src/tool-runner.ts +0 -142
  152. package/src/walker.ts +0 -53
  153. /package/dist/{src/handlers → handlers}/markdown-helpers.js +0 -0
  154. /package/dist/{src/markdown.js → markdown.js} +0 -0
  155. /package/dist/{src/registry-types.js → registry-types.js} +0 -0
  156. /package/dist/{src/stash-types.js → stash-types.js} +0 -0
@@ -1,15 +1,13 @@
1
+ import { Database } from "bun:sqlite";
1
2
  import fs from "node:fs";
3
+ import { createRequire } from "node:module";
2
4
  import path from "node:path";
3
- import { Database } from "bun:sqlite";
5
+ import { cosineSimilarity } from "./embedder";
6
+ import { getDbPath } from "./paths";
7
+ import { warn } from "./warn";
4
8
  // ── Constants ───────────────────────────────────────────────────────────────
5
- export const DB_VERSION = 5;
9
+ export const DB_VERSION = 6;
6
10
  export const EMBEDDING_DIM = 384;
7
- // ── Path ────────────────────────────────────────────────────────────────────
8
- export function getDbPath() {
9
- const cacheDir = process.env.XDG_CACHE_HOME ||
10
- path.join(process.env.HOME || process.env.USERPROFILE || "", ".cache");
11
- return path.join(cacheDir, "agentikit", "index.db");
12
- }
13
11
  // ── Database lifecycle ──────────────────────────────────────────────────────
14
12
  export function openDatabase(dbPath, options) {
15
13
  const resolvedPath = dbPath ?? getDbPath();
@@ -23,26 +21,53 @@ export function openDatabase(dbPath, options) {
23
21
  // Try to load sqlite-vec extension
24
22
  loadVecExtension(db);
25
23
  ensureSchema(db, options?.embeddingDim ?? EMBEDDING_DIM);
24
+ // Warn once at init if using JS fallback with many entries
25
+ warnIfVecMissing(db, { once: true });
26
26
  return db;
27
27
  }
28
28
  export function closeDatabase(db) {
29
29
  db.close();
30
30
  }
31
31
  // ── sqlite-vec extension ────────────────────────────────────────────────────
32
- let vecAvailable = false;
32
+ const vecStatus = new WeakMap();
33
33
  function loadVecExtension(db) {
34
34
  try {
35
- const sqliteVec = require("sqlite-vec");
35
+ const esmRequire = createRequire(import.meta.url);
36
+ const sqliteVec = esmRequire("sqlite-vec");
36
37
  sqliteVec.load(db);
37
- vecAvailable = true;
38
+ vecStatus.set(db, true);
38
39
  }
39
40
  catch {
40
- console.warn("sqlite-vec extension not available, embeddings will be skipped");
41
- vecAvailable = false;
41
+ vecStatus.set(db, false);
42
42
  }
43
43
  }
44
- export function isVecAvailable() {
45
- return vecAvailable;
44
+ export function isVecAvailable(db) {
45
+ return vecStatus.get(db) ?? false;
46
+ }
47
+ const VEC_DOCS_URL = "https://github.com/itlackey/agentikit/blob/main/docs/configuration.md#sqlite-vec-extension";
48
+ const VEC_FALLBACK_THRESHOLD = 10_000;
49
+ let vecInitWarned = false;
50
+ /**
51
+ * Warn if sqlite-vec is unavailable and embedding count exceeds threshold.
52
+ * Called from openDatabase (once at init) and from indexer (each run).
53
+ */
54
+ export function warnIfVecMissing(db, { once } = { once: false }) {
55
+ if (isVecAvailable(db))
56
+ return;
57
+ if (once && vecInitWarned)
58
+ return;
59
+ try {
60
+ const row = db.prepare("SELECT COUNT(*) AS cnt FROM embeddings").get();
61
+ const count = row?.cnt ?? 0;
62
+ if (count >= VEC_FALLBACK_THRESHOLD) {
63
+ warn("Semantic search is using JS fallback for %d entries. Install sqlite-vec for faster performance.\n See: %s", count, VEC_DOCS_URL);
64
+ if (once)
65
+ vecInitWarned = true;
66
+ }
67
+ }
68
+ catch {
69
+ /* embeddings table may not exist yet during init */
70
+ }
46
71
  }
47
72
  // ── Schema ──────────────────────────────────────────────────────────────────
48
73
  function ensureSchema(db, embeddingDim) {
@@ -56,6 +81,7 @@ function ensureSchema(db, embeddingDim) {
56
81
  // Check stored version — if it differs from DB_VERSION, drop and recreate all tables
57
82
  const storedVersion = getMeta(db, "version");
58
83
  if (storedVersion && storedVersion !== String(DB_VERSION)) {
84
+ db.exec("DROP TABLE IF EXISTS embeddings");
59
85
  db.exec("DROP TABLE IF EXISTS entries_vec");
60
86
  db.exec("DROP TABLE IF EXISTS entries_fts");
61
87
  db.exec("DROP INDEX IF EXISTS idx_entries_dir");
@@ -77,11 +103,17 @@ function ensureSchema(db, embeddingDim) {
77
103
 
78
104
  CREATE INDEX IF NOT EXISTS idx_entries_dir ON entries(dir_path);
79
105
  CREATE INDEX IF NOT EXISTS idx_entries_type ON entries(entry_type);
106
+ `);
107
+ // BLOB-based embedding storage (always available, no sqlite-vec needed)
108
+ db.exec(`
109
+ CREATE TABLE IF NOT EXISTS embeddings (
110
+ id INTEGER PRIMARY KEY,
111
+ embedding BLOB NOT NULL,
112
+ FOREIGN KEY (id) REFERENCES entries(id)
113
+ );
80
114
  `);
81
115
  // FTS5 table — standalone with explicit entry_id for joining
82
- const ftsExists = db
83
- .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='entries_fts'")
84
- .get();
116
+ const ftsExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='entries_fts'").get();
85
117
  if (!ftsExists) {
86
118
  db.exec(`
87
119
  CREATE VIRTUAL TABLE entries_fts USING fts5(
@@ -92,18 +124,18 @@ function ensureSchema(db, embeddingDim) {
92
124
  `);
93
125
  }
94
126
  // sqlite-vec table
95
- if (vecAvailable) {
127
+ if (isVecAvailable(db)) {
96
128
  // Check if stored embedding dimension differs from configured one
97
129
  const storedDim = getMeta(db, "embeddingDim");
98
130
  if (storedDim && storedDim !== String(embeddingDim)) {
99
131
  try {
100
132
  db.exec("DROP TABLE IF EXISTS entries_vec");
101
133
  }
102
- catch { /* ignore */ }
134
+ catch {
135
+ /* ignore */
136
+ }
103
137
  }
104
- const vecExists = db
105
- .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='entries_vec'")
106
- .get();
138
+ const vecExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='entries_vec'").get();
107
139
  if (!vecExists) {
108
140
  db.exec(`
109
141
  CREATE VIRTUAL TABLE entries_vec USING vec0(
@@ -147,15 +179,21 @@ export function upsertEntry(db, entryKey, dirPath, filePath, stashDir, entry, se
147
179
  return row.id;
148
180
  }
149
181
  export function deleteEntriesByDir(db, dirPath) {
150
- if (vecAvailable) {
151
- const ids = db
152
- .prepare("SELECT id FROM entries WHERE dir_path = ?")
153
- .all(dirPath);
154
- for (const { id } of ids) {
182
+ const ids = db.prepare("SELECT id FROM entries WHERE dir_path = ?").all(dirPath);
183
+ for (const { id } of ids) {
184
+ try {
185
+ db.prepare("DELETE FROM embeddings WHERE id = ?").run(id);
186
+ }
187
+ catch {
188
+ /* ignore */
189
+ }
190
+ if (isVecAvailable(db)) {
155
191
  try {
156
192
  db.prepare("DELETE FROM entries_vec WHERE id = ?").run(id);
157
193
  }
158
- catch { /* ignore if vec table missing */ }
194
+ catch {
195
+ /* ignore */
196
+ }
159
197
  }
160
198
  }
161
199
  db.prepare("DELETE FROM entries WHERE dir_path = ?").run(dirPath);
@@ -166,32 +204,67 @@ export function rebuildFts(db) {
166
204
  }
167
205
  // ── Vector operations ───────────────────────────────────────────────────────
168
206
  export function upsertEmbedding(db, entryId, embedding) {
169
- if (!vecAvailable)
170
- return;
171
207
  const buf = float32Buffer(embedding);
172
- try {
173
- db.prepare("DELETE FROM entries_vec WHERE id = ?").run(entryId);
208
+ // Always write to BLOB table (works without sqlite-vec)
209
+ db.prepare("INSERT OR REPLACE INTO embeddings (id, embedding) VALUES (?, ?)").run(entryId, buf);
210
+ // Also write to sqlite-vec table when available (fast path)
211
+ if (isVecAvailable(db)) {
212
+ try {
213
+ db.prepare("DELETE FROM entries_vec WHERE id = ?").run(entryId);
214
+ }
215
+ catch {
216
+ /* ignore */
217
+ }
218
+ db.prepare("INSERT INTO entries_vec (id, embedding) VALUES (?, ?)").run(entryId, buf);
174
219
  }
175
- catch { /* ignore */ }
176
- db.prepare("INSERT INTO entries_vec (id, embedding) VALUES (?, ?)").run(entryId, buf);
177
220
  }
178
221
  export function searchVec(db, queryEmbedding, k) {
179
- if (!vecAvailable)
180
- return [];
181
- const buf = float32Buffer(queryEmbedding);
182
- try {
183
- return db
184
- .prepare("SELECT id, distance FROM entries_vec WHERE embedding MATCH ? AND k = ?")
185
- .all(buf, k);
186
- }
187
- catch {
188
- return [];
222
+ // Fast path: use sqlite-vec when available
223
+ if (isVecAvailable(db)) {
224
+ const buf = float32Buffer(queryEmbedding);
225
+ try {
226
+ return db
227
+ .prepare("SELECT id, distance FROM entries_vec WHERE embedding MATCH ? AND k = ?")
228
+ .all(buf, k);
229
+ }
230
+ catch {
231
+ return [];
232
+ }
189
233
  }
234
+ // Fallback: JS-based cosine similarity over BLOB table
235
+ return searchBlobVec(db, queryEmbedding, k);
190
236
  }
191
237
  function float32Buffer(vec) {
192
238
  const f32 = new Float32Array(vec);
193
239
  return Buffer.from(f32.buffer);
194
240
  }
241
+ function bufferToFloat32(buf) {
242
+ const f32 = new Float32Array(buf.buffer, buf.byteOffset, buf.byteLength / 4);
243
+ return Array.from(f32);
244
+ }
245
+ function searchBlobVec(db, queryEmbedding, k) {
246
+ try {
247
+ const rows = db.prepare("SELECT id, embedding FROM embeddings").all();
248
+ if (rows.length === 0)
249
+ return [];
250
+ const scored = [];
251
+ for (const row of rows) {
252
+ const embedding = bufferToFloat32(row.embedding);
253
+ const similarity = cosineSimilarity(queryEmbedding, embedding);
254
+ scored.push({ id: row.id, similarity });
255
+ }
256
+ scored.sort((a, b) => b.similarity - a.similarity);
257
+ // Convert cosine similarity to L2 distance for compatibility with sqlite-vec interface
258
+ // For normalized vectors: L2² = 2(1 - cos_sim)
259
+ return scored.slice(0, k).map(({ id, similarity }) => ({
260
+ id,
261
+ distance: Math.sqrt(2 * Math.max(0, 1 - similarity)),
262
+ }));
263
+ }
264
+ catch {
265
+ return [];
266
+ }
267
+ }
195
268
  // ── FTS5 search ─────────────────────────────────────────────────────────────
196
269
  export function searchFts(db, query, limit, entryType) {
197
270
  const ftsQuery = sanitizeFtsQuery(query);
@@ -242,18 +315,19 @@ function sanitizeFtsQuery(query) {
242
315
  const tokens = query
243
316
  .replace(/[^a-zA-Z0-9\s]/g, " ")
244
317
  .split(/\s+/)
245
- .filter((t) => t.length > 1);
318
+ .filter((t) => t.length >= 1);
246
319
  if (tokens.length === 0)
247
320
  return "";
248
321
  // Use unquoted tokens so the porter stemmer can normalize word forms
249
- return tokens.join(" OR ");
322
+ return tokens.join(" ");
250
323
  }
251
324
  // ── All entries ─────────────────────────────────────────────────────────────
252
325
  export function getAllEntries(db, entryType) {
253
326
  let sql;
254
327
  let params;
255
328
  if (entryType && entryType !== "any") {
256
- sql = "SELECT id, entry_key, dir_path, file_path, stash_dir, entry_json, search_text FROM entries WHERE entry_type = ?";
329
+ sql =
330
+ "SELECT id, entry_key, dir_path, file_path, stash_dir, entry_json, search_text FROM entries WHERE entry_type = ?";
257
331
  params = [entryType];
258
332
  }
259
333
  else {
@@ -276,9 +350,7 @@ export function getEntryCount(db) {
276
350
  return row.cnt;
277
351
  }
278
352
  export function getEntryById(db, id) {
279
- const row = db
280
- .prepare("SELECT file_path, entry_json FROM entries WHERE id = ?")
281
- .get(id);
353
+ const row = db.prepare("SELECT file_path, entry_json FROM entries WHERE id = ?").get(id);
282
354
  if (!row)
283
355
  return undefined;
284
356
  return { filePath: row.file_path, entry: JSON.parse(row.entry_json) };
@@ -60,16 +60,71 @@ export async function embed(text, embeddingConfig) {
60
60
  }
61
61
  return embedLocal(text);
62
62
  }
63
+ // ── Batch embedding ─────────────────────────────────────────────────────────
64
+ /**
65
+ * Generate embeddings for multiple texts in batch.
66
+ * Uses the OpenAI-compatible batch API for remote endpoints (batches of 100).
67
+ * Falls back to sequential embedding for local transformer pipeline.
68
+ */
69
+ export async function embedBatch(texts, embeddingConfig) {
70
+ if (texts.length === 0)
71
+ return [];
72
+ if (embeddingConfig) {
73
+ return embedRemoteBatch(texts, embeddingConfig);
74
+ }
75
+ // Local transformer: process sequentially (pipeline handles one at a time)
76
+ const results = [];
77
+ for (const text of texts) {
78
+ results.push(await embedLocal(text));
79
+ }
80
+ return results;
81
+ }
82
+ async function embedRemoteBatch(texts, config) {
83
+ const BATCH_SIZE = 100;
84
+ const results = [];
85
+ const headers = { "Content-Type": "application/json" };
86
+ if (config.apiKey) {
87
+ headers["Authorization"] = `Bearer ${config.apiKey}`;
88
+ }
89
+ for (let i = 0; i < texts.length; i += BATCH_SIZE) {
90
+ const batch = texts.slice(i, i + BATCH_SIZE);
91
+ const body = {
92
+ input: batch,
93
+ model: config.model,
94
+ };
95
+ if (config.dimension) {
96
+ body.dimensions = config.dimension;
97
+ }
98
+ const response = await fetchWithTimeout(config.endpoint, {
99
+ method: "POST",
100
+ headers,
101
+ body: JSON.stringify(body),
102
+ });
103
+ if (!response.ok) {
104
+ const respBody = await response.text().catch(() => "");
105
+ throw new Error(`Embedding batch request failed (${response.status}): ${respBody}`);
106
+ }
107
+ const json = (await response.json());
108
+ if (!json.data || json.data.length !== batch.length) {
109
+ throw new Error(`Unexpected embedding batch response: expected ${batch.length} embeddings, got ${json.data?.length ?? 0}`);
110
+ }
111
+ results.push(...json.data.map((d) => d.embedding));
112
+ }
113
+ return results;
114
+ }
63
115
  // ── Similarity ──────────────────────────────────────────────────────────────
64
116
  export function cosineSimilarity(a, b) {
65
117
  const len = Math.min(a.length, b.length);
66
118
  if (len === 0)
67
119
  return 0;
68
- let dot = 0;
120
+ let dot = 0, magA = 0, magB = 0;
69
121
  for (let i = 0; i < len; i++) {
70
122
  dot += a[i] * b[i];
123
+ magA += a[i] * a[i];
124
+ magB += b[i] * b[i];
71
125
  }
72
- return dot;
126
+ const denom = Math.sqrt(magA) * Math.sqrt(magB);
127
+ return denom === 0 ? 0 : dot / denom;
73
128
  }
74
129
  // ── Availability check ──────────────────────────────────────────────────────
75
130
  export async function isEmbeddingAvailable(embeddingConfig) {
package/dist/errors.js ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Typed error classes for structured exit code classification.
3
+ *
4
+ * - ConfigError -> exit 78 (configuration / environment problems)
5
+ * - UsageError -> exit 2 (bad CLI arguments or invalid input)
6
+ * - NotFoundError -> exit 1 (requested resource missing)
7
+ */
8
+ /** Raised when configuration or environment is invalid or missing. */
9
+ export class ConfigError extends Error {
10
+ constructor(msg) {
11
+ super(msg);
12
+ this.name = "ConfigError";
13
+ }
14
+ }
15
+ /** Raised when the user supplies invalid arguments or input. */
16
+ export class UsageError extends Error {
17
+ constructor(msg) {
18
+ super(msg);
19
+ this.name = "UsageError";
20
+ }
21
+ }
22
+ /** Raised when a requested resource (asset, entry, file) is not found. */
23
+ export class NotFoundError extends Error {
24
+ constructor(msg) {
25
+ super(msg);
26
+ this.name = "NotFoundError";
27
+ }
28
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Flexible asset resolution system.
3
+ *
4
+ * Provides a rich FileContext built once per file during walking, plus a
5
+ * matcher/renderer registry that decouples asset classification from rendering.
6
+ */
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import { toPosix } from "./common";
10
+ import { parseFrontmatter } from "./frontmatter";
11
+ /**
12
+ * Build a FileContext from a stash root and an absolute file path.
13
+ *
14
+ * Path-derived fields are computed eagerly. The content, frontmatter, and
15
+ * stat getters use lazy caching so the file is only read from disk when
16
+ * (and if) a matcher or renderer actually needs it.
17
+ */
18
+ export function buildFileContext(stashRoot, absPath) {
19
+ const relPath = toPosix(path.relative(stashRoot, absPath));
20
+ const ext = path.extname(absPath).toLowerCase();
21
+ const fileName = path.basename(absPath);
22
+ const parentDirAbs = path.dirname(absPath);
23
+ const parentDir = path.basename(parentDirAbs);
24
+ // Compute ancestor directory segments from the POSIX relPath's directory portion.
25
+ // For "tools/azure/deploy/run.sh" the dir portion is "tools/azure/deploy"
26
+ // which splits into ["tools", "azure", "deploy"].
27
+ const relDir = toPosix(path.dirname(relPath));
28
+ const ancestorDirs = relDir === "." ? [] : relDir.split("/").filter((seg) => seg.length > 0);
29
+ // Lazy caches
30
+ let cachedContent;
31
+ let cachedFrontmatter;
32
+ let frontmatterComputed = false;
33
+ let cachedStat;
34
+ return {
35
+ absPath,
36
+ relPath,
37
+ ext,
38
+ fileName,
39
+ parentDir,
40
+ parentDirAbs,
41
+ ancestorDirs,
42
+ stashRoot,
43
+ content() {
44
+ if (cachedContent === undefined) {
45
+ cachedContent = fs.readFileSync(absPath, "utf8");
46
+ }
47
+ return cachedContent;
48
+ },
49
+ frontmatter() {
50
+ if (!frontmatterComputed) {
51
+ const raw = this.content();
52
+ const parsed = parseFrontmatter(raw);
53
+ cachedFrontmatter = Object.keys(parsed.data).length > 0 ? parsed.data : null;
54
+ frontmatterComputed = true;
55
+ }
56
+ return cachedFrontmatter;
57
+ },
58
+ stat() {
59
+ if (cachedStat === undefined) {
60
+ cachedStat = fs.statSync(absPath);
61
+ }
62
+ return cachedStat;
63
+ },
64
+ };
65
+ }
66
+ // ── Registry ─────────────────────────────────────────────────────────────────
67
+ /** Ordered list of registered matchers. Later registrations win ties. */
68
+ const matchers = [];
69
+ /** Renderer lookup by name. */
70
+ const renderers = new Map();
71
+ let builtinsInitialized = false;
72
+ /** Pluggable initializer set via `setBuiltinRegistrar`. */
73
+ let builtinRegistrar = null;
74
+ /**
75
+ * Set the function that registers built-in matchers and renderers.
76
+ *
77
+ * This breaks the static import cycle: `file-context.ts` does not need to
78
+ * import `matchers.ts` or `renderers.ts` at the top level. Instead, a
79
+ * one-time call from outside (e.g. the test file or CLI entry) provides the
80
+ * registration callback.
81
+ *
82
+ * If no registrar is set by the time `ensureBuiltinsRegistered` runs, it
83
+ * falls back to a dynamic `require()` for backward compatibility.
84
+ */
85
+ export function setBuiltinRegistrar(fn) {
86
+ builtinRegistrar = fn;
87
+ }
88
+ /**
89
+ * Ensure that built-in matchers and renderers are registered.
90
+ * Called lazily on first use of runMatchers/getRenderer.
91
+ *
92
+ * Uses the registrar set via `setBuiltinRegistrar`, or falls back to
93
+ * a direct import of the registration modules. The dynamic import
94
+ * avoids a static circular dependency between file-context, renderers,
95
+ * and asset-spec.
96
+ */
97
+ function ensureBuiltinsRegistered() {
98
+ if (builtinsInitialized)
99
+ return;
100
+ builtinsInitialized = true;
101
+ if (builtinRegistrar) {
102
+ builtinRegistrar();
103
+ return;
104
+ }
105
+ // Lazy inline require avoids a top-level static import cycle:
106
+ // file-context -> renderers -> asset-spec -> asset-type-handler -> handlers -> file-context
107
+ // These are only evaluated once and only when no explicit registrar was set.
108
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
109
+ const { registerBuiltinMatchers } = require("./matchers");
110
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
111
+ const { registerBuiltinRenderers } = require("./renderers");
112
+ registerBuiltinMatchers();
113
+ registerBuiltinRenderers();
114
+ }
115
+ /**
116
+ * Register an AssetMatcher.
117
+ *
118
+ * Matchers are evaluated in registration order. When two matchers produce
119
+ * the same specificity score, the one registered later wins.
120
+ */
121
+ export function registerMatcher(matcher) {
122
+ matchers.push(matcher);
123
+ }
124
+ /**
125
+ * Register an AssetRenderer.
126
+ *
127
+ * If a renderer with the same name already exists it is silently replaced.
128
+ */
129
+ export function registerRenderer(renderer) {
130
+ renderers.set(renderer.name, renderer);
131
+ }
132
+ /**
133
+ * Look up a renderer by name.
134
+ */
135
+ export function getRenderer(name) {
136
+ ensureBuiltinsRegistered();
137
+ return renderers.get(name);
138
+ }
139
+ /**
140
+ * Return all registered renderers (snapshot, safe to iterate).
141
+ */
142
+ export function getAllRenderers() {
143
+ ensureBuiltinsRegistered();
144
+ return Array.from(renderers.values());
145
+ }
146
+ /**
147
+ * Run every registered matcher against a FileContext and return the
148
+ * highest-specificity result.
149
+ *
150
+ * Resolution rules:
151
+ * 1. Every matcher is invoked; null returns are discarded.
152
+ * 2. Results are ranked by specificity (descending).
153
+ * 3. Ties are broken by registration order: the matcher registered later wins
154
+ * (this lets user-registered matchers override built-in ones).
155
+ * 4. Returns null when no matcher claims the file.
156
+ */
157
+ export function runMatchers(ctx) {
158
+ ensureBuiltinsRegistered();
159
+ // Collect (result, registrationIndex) pairs from all matchers.
160
+ const hits = [];
161
+ for (let i = 0; i < matchers.length; i++) {
162
+ const result = matchers[i](ctx);
163
+ if (result !== null) {
164
+ hits.push({ result, index: i });
165
+ }
166
+ }
167
+ if (hits.length === 0)
168
+ return null;
169
+ // Sort by specificity descending, then by registration index descending (later wins ties).
170
+ hits.sort((a, b) => {
171
+ const specDiff = b.result.specificity - a.result.specificity;
172
+ if (specDiff !== 0)
173
+ return specDiff;
174
+ return b.index - a.index;
175
+ });
176
+ return hits[0].result;
177
+ }
178
+ /**
179
+ * Build a RenderContext by merging a FileContext with its winning MatchResult
180
+ * and the list of stash search paths.
181
+ */
182
+ export function buildRenderContext(ctx, match, stashDirs) {
183
+ return {
184
+ ...ctx,
185
+ matchResult: match,
186
+ stashDirs,
187
+ };
188
+ }
@@ -18,7 +18,7 @@ export function parseFrontmatter(raw) {
18
18
  let currentKey = null;
19
19
  let nested = null;
20
20
  for (const line of parsedBlock.frontmatter.split(/\r?\n/)) {
21
- const indented = line.match(/^ (\w[\w-]*):\s*(.+)$/);
21
+ const indented = line.match(/^ {2}(\w[\w-]*):\s*(.+)$/);
22
22
  if (indented && currentKey && nested) {
23
23
  nested[indented[1]] = parseYamlScalar(indented[2].trim());
24
24
  continue;
@@ -10,9 +10,7 @@ export function githubHeaders() {
10
10
  return headers;
11
11
  }
12
12
  export function asRecord(value) {
13
- return typeof value === "object" && value !== null && !Array.isArray(value)
14
- ? value
15
- : {};
13
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
16
14
  }
17
15
  export function asString(value) {
18
16
  return typeof value === "string" && value ? value : undefined;
@@ -0,0 +1,19 @@
1
+ import { getRenderer } from "../file-context";
2
+ import { showInputToRenderContext } from "./handler-bridge";
3
+ import { isMarkdownFile, markdownAssetPath, markdownCanonicalName } from "./markdown-helpers";
4
+ export const agentHandler = {
5
+ typeName: "agent",
6
+ stashDir: "agents",
7
+ isRelevantFile: isMarkdownFile,
8
+ toCanonicalName: markdownCanonicalName,
9
+ toAssetPath: markdownAssetPath,
10
+ buildShowResponse(input) {
11
+ const renderer = getRenderer("agent-md");
12
+ const ctx = showInputToRenderContext(input, "agent-md");
13
+ return renderer.buildShowResponse(ctx);
14
+ },
15
+ defaultUsageGuide: [
16
+ "Read the .md file and dispatch an agent using the content of the file. Use modelHint/toolPolicy when present to run the agent with compatible settings.",
17
+ "Use with `akm show <openRef>` to get the full prompt payload.",
18
+ ],
19
+ };
@@ -0,0 +1,20 @@
1
+ import { getRenderer } from "../file-context";
2
+ import { showInputToRenderContext } from "./handler-bridge";
3
+ import { isMarkdownFile, markdownAssetPath, markdownCanonicalName } from "./markdown-helpers";
4
+ export const commandHandler = {
5
+ typeName: "command",
6
+ stashDir: "commands",
7
+ isRelevantFile: isMarkdownFile,
8
+ toCanonicalName: markdownCanonicalName,
9
+ toAssetPath: markdownAssetPath,
10
+ buildShowResponse(input) {
11
+ const renderer = getRenderer("command-md");
12
+ const ctx = showInputToRenderContext(input, "command-md");
13
+ return renderer.buildShowResponse(ctx);
14
+ },
15
+ defaultUsageGuide: [
16
+ "Read the .md file, fill $ARGUMENTS placeholders, and run it in the current repo context.",
17
+ "Use `akm show <openRef>` to retrieve the command template body.",
18
+ "When `agent` is specified, dispatch the command to that agent.",
19
+ ],
20
+ };