agentikit 0.0.13 → 0.0.14

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 (145) hide show
  1. package/LICENSE +385 -0
  2. package/README.md +180 -110
  3. package/dist/cli.js +671 -0
  4. package/dist/common.js +192 -0
  5. package/dist/{src/config-cli.js → config-cli.js} +14 -6
  6. package/dist/{src/config.js → config.js} +92 -24
  7. package/dist/{src/db.js → db.js} +109 -35
  8. package/dist/{src/embedder.js → embedder.js} +57 -2
  9. package/dist/file-context.js +158 -0
  10. package/dist/{src/handlers → handlers}/command-handler.js +2 -0
  11. package/dist/{src/handlers → handlers}/index.js +0 -6
  12. package/dist/{src/indexer.js → indexer.js} +34 -10
  13. package/dist/init.js +43 -0
  14. package/dist/lockfile.js +55 -0
  15. package/dist/matchers.js +157 -0
  16. package/dist/{src/metadata.js → metadata.js} +12 -1
  17. package/dist/{src/origin-resolve.js → origin-resolve.js} +10 -9
  18. package/dist/paths.js +82 -0
  19. package/dist/{src/registry-install.js → registry-install.js} +145 -17
  20. package/dist/{src/registry-resolve.js → registry-resolve.js} +178 -18
  21. package/dist/{src/registry-search.js → registry-search.js} +8 -16
  22. package/dist/renderers.js +276 -0
  23. package/dist/{src/ripgrep-install.js → ripgrep-install.js} +5 -5
  24. package/dist/{src/ripgrep-resolve.js → ripgrep-resolve.js} +21 -11
  25. package/dist/self-update.js +220 -0
  26. package/dist/{src/stash-add.js → stash-add.js} +11 -2
  27. package/dist/stash-clone.js +115 -0
  28. package/dist/{src/stash-registry.js → stash-registry.js} +15 -41
  29. package/dist/{src/stash-search.js → stash-search.js} +67 -55
  30. package/dist/{src/stash-show.js → stash-show.js} +30 -3
  31. package/dist/{src/stash-source.js → stash-source.js} +56 -9
  32. package/dist/submit.js +552 -0
  33. package/dist/{src/walker.js → walker.js} +38 -0
  34. package/package.json +7 -16
  35. package/dist/index.d.ts +0 -28
  36. package/dist/index.js +0 -15
  37. package/dist/src/asset-spec.d.ts +0 -16
  38. package/dist/src/asset-type-handler.d.ts +0 -27
  39. package/dist/src/cli.d.ts +0 -2
  40. package/dist/src/cli.js +0 -399
  41. package/dist/src/common.d.ts +0 -13
  42. package/dist/src/common.js +0 -60
  43. package/dist/src/config-cli.d.ts +0 -9
  44. package/dist/src/config.d.ts +0 -50
  45. package/dist/src/db.d.ts +0 -46
  46. package/dist/src/embedder.d.ts +0 -10
  47. package/dist/src/frontmatter.d.ts +0 -30
  48. package/dist/src/github.d.ts +0 -4
  49. package/dist/src/handlers/agent-handler.d.ts +0 -2
  50. package/dist/src/handlers/command-handler.d.ts +0 -2
  51. package/dist/src/handlers/index.d.ts +0 -6
  52. package/dist/src/handlers/knowledge-handler.d.ts +0 -2
  53. package/dist/src/handlers/markdown-helpers.d.ts +0 -7
  54. package/dist/src/handlers/script-handler.d.ts +0 -2
  55. package/dist/src/handlers/skill-handler.d.ts +0 -2
  56. package/dist/src/handlers/tool-handler.d.ts +0 -2
  57. package/dist/src/indexer.d.ts +0 -22
  58. package/dist/src/init.d.ts +0 -19
  59. package/dist/src/init.js +0 -99
  60. package/dist/src/llm.d.ts +0 -15
  61. package/dist/src/markdown.d.ts +0 -18
  62. package/dist/src/metadata.d.ts +0 -41
  63. package/dist/src/origin-resolve.d.ts +0 -19
  64. package/dist/src/registry-install.d.ts +0 -11
  65. package/dist/src/registry-resolve.d.ts +0 -3
  66. package/dist/src/registry-search.d.ts +0 -27
  67. package/dist/src/registry-types.d.ts +0 -62
  68. package/dist/src/ripgrep-install.d.ts +0 -12
  69. package/dist/src/ripgrep-resolve.d.ts +0 -13
  70. package/dist/src/ripgrep.d.ts +0 -3
  71. package/dist/src/stash-add.d.ts +0 -4
  72. package/dist/src/stash-clone.d.ts +0 -22
  73. package/dist/src/stash-clone.js +0 -83
  74. package/dist/src/stash-ref.d.ts +0 -31
  75. package/dist/src/stash-registry.d.ts +0 -18
  76. package/dist/src/stash-resolve.d.ts +0 -2
  77. package/dist/src/stash-search.d.ts +0 -8
  78. package/dist/src/stash-show.d.ts +0 -5
  79. package/dist/src/stash-source.d.ts +0 -24
  80. package/dist/src/stash-types.d.ts +0 -227
  81. package/dist/src/stash.d.ts +0 -16
  82. package/dist/src/stash.js +0 -9
  83. package/dist/src/tool-runner.d.ts +0 -35
  84. package/dist/src/walker.d.ts +0 -19
  85. package/src/asset-spec.ts +0 -85
  86. package/src/asset-type-handler.ts +0 -77
  87. package/src/cli.ts +0 -427
  88. package/src/common.ts +0 -76
  89. package/src/config-cli.ts +0 -499
  90. package/src/config.ts +0 -305
  91. package/src/db.ts +0 -411
  92. package/src/embedder.ts +0 -128
  93. package/src/frontmatter.ts +0 -95
  94. package/src/github.ts +0 -21
  95. package/src/handlers/agent-handler.ts +0 -32
  96. package/src/handlers/command-handler.ts +0 -29
  97. package/src/handlers/index.ts +0 -25
  98. package/src/handlers/knowledge-handler.ts +0 -62
  99. package/src/handlers/markdown-helpers.ts +0 -19
  100. package/src/handlers/script-handler.ts +0 -92
  101. package/src/handlers/skill-handler.ts +0 -37
  102. package/src/handlers/tool-handler.ts +0 -71
  103. package/src/indexer.ts +0 -392
  104. package/src/init.ts +0 -114
  105. package/src/llm.ts +0 -125
  106. package/src/markdown.ts +0 -106
  107. package/src/metadata.ts +0 -333
  108. package/src/origin-resolve.ts +0 -67
  109. package/src/registry-install.ts +0 -361
  110. package/src/registry-resolve.ts +0 -341
  111. package/src/registry-search.ts +0 -335
  112. package/src/registry-types.ts +0 -72
  113. package/src/ripgrep-install.ts +0 -200
  114. package/src/ripgrep-resolve.ts +0 -72
  115. package/src/ripgrep.ts +0 -3
  116. package/src/stash-add.ts +0 -63
  117. package/src/stash-clone.ts +0 -127
  118. package/src/stash-ref.ts +0 -99
  119. package/src/stash-registry.ts +0 -259
  120. package/src/stash-resolve.ts +0 -50
  121. package/src/stash-search.ts +0 -613
  122. package/src/stash-show.ts +0 -55
  123. package/src/stash-source.ts +0 -103
  124. package/src/stash-types.ts +0 -231
  125. package/src/stash.ts +0 -39
  126. package/src/tool-runner.ts +0 -142
  127. package/src/walker.ts +0 -53
  128. /package/dist/{src/asset-spec.js → asset-spec.js} +0 -0
  129. /package/dist/{src/asset-type-handler.js → asset-type-handler.js} +0 -0
  130. /package/dist/{src/frontmatter.js → frontmatter.js} +0 -0
  131. /package/dist/{src/github.js → github.js} +0 -0
  132. /package/dist/{src/handlers → handlers}/agent-handler.js +0 -0
  133. /package/dist/{src/handlers → handlers}/knowledge-handler.js +0 -0
  134. /package/dist/{src/handlers → handlers}/markdown-helpers.js +0 -0
  135. /package/dist/{src/handlers → handlers}/script-handler.js +0 -0
  136. /package/dist/{src/handlers → handlers}/skill-handler.js +0 -0
  137. /package/dist/{src/handlers → handlers}/tool-handler.js +0 -0
  138. /package/dist/{src/llm.js → llm.js} +0 -0
  139. /package/dist/{src/markdown.js → markdown.js} +0 -0
  140. /package/dist/{src/registry-types.js → registry-types.js} +0 -0
  141. /package/dist/{src/ripgrep.js → ripgrep.js} +0 -0
  142. /package/dist/{src/stash-ref.js → stash-ref.js} +0 -0
  143. /package/dist/{src/stash-resolve.js → stash-resolve.js} +0 -0
  144. /package/dist/{src/stash-types.js → stash-types.js} +0 -0
  145. /package/dist/{src/tool-runner.js → tool-runner.js} +0 -0
package/dist/common.js ADDED
@@ -0,0 +1,192 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { TYPE_DIRS } from "./asset-spec";
4
+ import { getConfigPath, getDefaultStashDir } from "./paths";
5
+ // ── Constants ───────────────────────────────────────────────────────────────
6
+ export const IS_WINDOWS = process.platform === "win32";
7
+ export { SCRIPT_EXTENSIONS, TYPE_DIRS } from "./asset-spec";
8
+ // ── Validators ──────────────────────────────────────────────────────────────
9
+ export function isAssetType(type) {
10
+ return type in TYPE_DIRS;
11
+ }
12
+ // ── Utilities ───────────────────────────────────────────────────────────────
13
+ /**
14
+ * Resolve the stash directory using a three-level fallback chain:
15
+ * 1. AKM_STASH_DIR environment variable (override for CI/scripts)
16
+ * 2. stashDir field in config.json
17
+ * 3. Platform default (~/agentikit or ~/Documents/agentikit on Windows)
18
+ *
19
+ * Throws if no valid stash directory is found.
20
+ */
21
+ export function resolveStashDir(options) {
22
+ // 1. Env var override (for CI, scripts, testing)
23
+ const envDir = process.env.AKM_STASH_DIR?.trim();
24
+ if (envDir) {
25
+ const resolved = validateStashDir(envDir);
26
+ if (!options?.readOnly)
27
+ persistStashDirToConfig(resolved);
28
+ return resolved;
29
+ }
30
+ // 2. Config file stashDir field
31
+ const configStashDir = readStashDirFromConfig();
32
+ if (configStashDir)
33
+ return validateStashDir(configStashDir);
34
+ // 3. Platform default — use it if it exists
35
+ const defaultDir = getDefaultStashDir();
36
+ if (isValidDirectory(defaultDir)) {
37
+ return defaultDir;
38
+ }
39
+ throw new Error(`No stash directory found. Run "akm init" to create one at ${defaultDir}, ` +
40
+ `or set stashDir in ${getConfigPath()}.`);
41
+ }
42
+ function validateStashDir(raw) {
43
+ const stashDir = path.resolve(raw);
44
+ let stat;
45
+ try {
46
+ stat = fs.statSync(stashDir);
47
+ }
48
+ catch {
49
+ throw new Error(`Unable to read stash directory at "${stashDir}".`);
50
+ }
51
+ if (!stat.isDirectory()) {
52
+ throw new Error(`Stash path must point to a directory: "${stashDir}".`);
53
+ }
54
+ return stashDir;
55
+ }
56
+ function isValidDirectory(dir) {
57
+ try {
58
+ return fs.statSync(dir).isDirectory();
59
+ }
60
+ catch {
61
+ return false;
62
+ }
63
+ }
64
+ /**
65
+ * Read stashDir directly from config.json without pulling in the full config
66
+ * module, to avoid circular dependencies.
67
+ */
68
+ function readStashDirFromConfig() {
69
+ try {
70
+ const configPath = getConfigPath();
71
+ const text = fs.readFileSync(configPath, "utf8");
72
+ const raw = JSON.parse(text);
73
+ if (typeof raw === "object" && raw !== null && typeof raw.stashDir === "string" && raw.stashDir.trim()) {
74
+ return raw.stashDir.trim();
75
+ }
76
+ }
77
+ catch {
78
+ // Config doesn't exist or is invalid — fall through
79
+ }
80
+ return undefined;
81
+ }
82
+ /**
83
+ * Persist stashDir to config.json if not already set, so users can
84
+ * transition away from relying on the AKM_STASH_DIR env var.
85
+ */
86
+ function persistStashDirToConfig(stashDir) {
87
+ try {
88
+ const configPath = getConfigPath();
89
+ let raw = {};
90
+ try {
91
+ const text = fs.readFileSync(configPath, "utf8");
92
+ const parsed = JSON.parse(text);
93
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
94
+ raw = parsed;
95
+ }
96
+ }
97
+ catch {
98
+ // No existing config or invalid — start fresh
99
+ }
100
+ if (!raw.stashDir) {
101
+ raw.stashDir = stashDir;
102
+ const dir = path.dirname(configPath);
103
+ fs.mkdirSync(dir, { recursive: true });
104
+ const tmpPath = configPath + `.tmp.${process.pid}`;
105
+ fs.writeFileSync(tmpPath, JSON.stringify(raw, null, 2) + "\n", "utf8");
106
+ fs.renameSync(tmpPath, configPath);
107
+ }
108
+ }
109
+ catch {
110
+ // Non-fatal: best-effort persistence
111
+ }
112
+ }
113
+ export function toPosix(input) {
114
+ return input.replace(/\\/g, "/");
115
+ }
116
+ export function hasErrnoCode(error, code) {
117
+ if (typeof error !== "object" || error === null || !("code" in error))
118
+ return false;
119
+ return error.code === code;
120
+ }
121
+ export function isWithin(candidate, root) {
122
+ const resolvedRoot = safeRealpath(root);
123
+ const resolvedCandidate = safeRealpath(candidate);
124
+ const normalizedRoot = normalizeFsPathForComparison(resolvedRoot);
125
+ const normalizedCandidate = normalizeFsPathForComparison(resolvedCandidate);
126
+ const rel = path.relative(normalizedRoot, normalizedCandidate);
127
+ return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
128
+ }
129
+ function safeRealpath(p) {
130
+ try {
131
+ return fs.realpathSync(path.resolve(p));
132
+ }
133
+ catch {
134
+ return path.resolve(p);
135
+ }
136
+ }
137
+ function normalizeFsPathForComparison(value) {
138
+ return process.platform === "win32" ? value.toLowerCase() : value;
139
+ }
140
+ /**
141
+ * Fetch with an AbortController timeout.
142
+ * Defaults to 30 seconds if no timeout is specified.
143
+ */
144
+ export async function fetchWithTimeout(url, opts, timeoutMs = 30_000) {
145
+ const controller = new AbortController();
146
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
147
+ try {
148
+ return await fetch(url, { ...opts, signal: controller.signal });
149
+ }
150
+ finally {
151
+ clearTimeout(timer);
152
+ }
153
+ }
154
+ /**
155
+ * Fetch with retry and exponential backoff.
156
+ * Retries on network errors, 429, and 5xx responses.
157
+ * Honors Retry-After header for 429 responses.
158
+ */
159
+ export async function fetchWithRetry(url, init, options) {
160
+ const maxRetries = options?.retries ?? 3;
161
+ const baseDelay = options?.baseDelay ?? 500;
162
+ const timeout = options?.timeout ?? 30_000;
163
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
164
+ try {
165
+ const response = await fetchWithTimeout(url, init, timeout);
166
+ if (attempt < maxRetries && shouldRetry(response.status)) {
167
+ const retryAfter = parseRetryAfter(response);
168
+ const delay = retryAfter ?? baseDelay * Math.pow(2, attempt) * (0.5 + Math.random() * 0.5);
169
+ await new Promise((r) => setTimeout(r, delay));
170
+ continue;
171
+ }
172
+ return response;
173
+ }
174
+ catch (err) {
175
+ if (attempt >= maxRetries)
176
+ throw err;
177
+ const delay = baseDelay * Math.pow(2, attempt) * (0.5 + Math.random() * 0.5);
178
+ await new Promise((r) => setTimeout(r, delay));
179
+ }
180
+ }
181
+ throw new Error("fetchWithRetry: unreachable");
182
+ }
183
+ function shouldRetry(status) {
184
+ return status === 429 || status >= 500;
185
+ }
186
+ function parseRetryAfter(response) {
187
+ const header = response.headers.get("retry-after");
188
+ if (!header)
189
+ return undefined;
190
+ const seconds = parseInt(header, 10);
191
+ return isNaN(seconds) ? undefined : seconds * 1000;
192
+ }
@@ -59,20 +59,22 @@ const LLM_PROVIDER_PRESETS = {
59
59
  };
60
60
  export function parseConfigValue(key, value) {
61
61
  switch (key) {
62
+ case "stashDir":
63
+ return { stashDir: requireNonEmptyString(value, key) };
62
64
  case "semanticSearch":
63
65
  if (value !== "true" && value !== "false") {
64
66
  throw new Error(`Invalid value for semanticSearch: expected "true" or "false"`);
65
67
  }
66
68
  return { semanticSearch: value === "true" };
67
- case "mountedStashDirs":
69
+ case "searchPaths":
68
70
  try {
69
71
  const parsed = JSON.parse(value);
70
72
  if (!Array.isArray(parsed))
71
73
  throw new Error("expected JSON array");
72
- return { mountedStashDirs: parsed.filter((d) => typeof d === "string") };
74
+ return { searchPaths: parsed.filter((d) => typeof d === "string") };
73
75
  }
74
76
  catch {
75
- throw new Error(`Invalid value for mountedStashDirs: expected JSON array (e.g. '["/path/a","/path/b"]')`);
77
+ throw new Error(`Invalid value for searchPaths: expected JSON array (e.g. '["/path/a","/path/b"]')`);
76
78
  }
77
79
  case "embedding":
78
80
  return { embedding: parseEmbeddingConnectionValue(value) };
@@ -84,10 +86,12 @@ export function parseConfigValue(key, value) {
84
86
  }
85
87
  export function getConfigValue(config, key) {
86
88
  switch (key) {
89
+ case "stashDir":
90
+ return config.stashDir ?? null;
87
91
  case "semanticSearch":
88
92
  return config.semanticSearch;
89
- case "mountedStashDirs":
90
- return [...config.mountedStashDirs];
93
+ case "searchPaths":
94
+ return [...config.searchPaths];
91
95
  case "embedding":
92
96
  return maskSecrets(getEmbeddingDisplayConfig(config));
93
97
  case "embedding.provider":
@@ -120,8 +124,9 @@ export function getConfigValue(config, key) {
120
124
  }
121
125
  export function setConfigValue(config, key, rawValue) {
122
126
  switch (key) {
127
+ case "stashDir":
123
128
  case "semanticSearch":
124
- case "mountedStashDirs":
129
+ case "searchPaths":
125
130
  case "embedding":
126
131
  case "llm":
127
132
  return { ...config, ...parseConfigValue(key, rawValue) };
@@ -207,6 +212,8 @@ export function setConfigValue(config, key, rawValue) {
207
212
  }
208
213
  export function unsetConfigValue(config, key) {
209
214
  switch (key) {
215
+ case "stashDir":
216
+ return { ...config, stashDir: undefined };
210
217
  case "embedding":
211
218
  return { ...config, embedding: undefined };
212
219
  case "embedding.apiKey":
@@ -247,6 +254,7 @@ export function listConfig(config) {
247
254
  return {
248
255
  ...DEFAULT_CONFIG,
249
256
  ...maskSecrets(config),
257
+ stashDir: config.stashDir ?? null,
250
258
  embedding: maskSecrets(getEmbeddingDisplayConfig(config)),
251
259
  llm: maskSecrets(getLlmDisplayConfig(config)),
252
260
  };
@@ -1,37 +1,33 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { getConfigDir as _getConfigDir, getConfigPath as _getConfigPath } from "./paths";
3
4
  // ── Defaults ────────────────────────────────────────────────────────────────
4
5
  export const DEFAULT_CONFIG = {
5
6
  semanticSearch: true,
6
- mountedStashDirs: [],
7
+ searchPaths: [],
7
8
  };
8
9
  // ── Paths ───────────────────────────────────────────────────────────────────
9
- export function getConfigDir(env = process.env, platform = process.platform) {
10
- if (platform === "win32") {
11
- const appData = env.APPDATA?.trim();
12
- if (appData)
13
- return path.join(appData, "agentikit");
14
- const userProfile = env.USERPROFILE?.trim();
15
- if (!userProfile) {
16
- throw new Error("Unable to determine config directory. Set APPDATA or USERPROFILE.");
17
- }
18
- return path.join(userProfile, "AppData", "Roaming", "agentikit");
19
- }
20
- const xdgConfigHome = env.XDG_CONFIG_HOME?.trim();
21
- if (xdgConfigHome)
22
- return path.join(xdgConfigHome, "agentikit");
23
- const home = env.HOME?.trim();
24
- if (!home) {
25
- throw new Error("Unable to determine config directory. Set XDG_CONFIG_HOME or HOME.");
26
- }
27
- return path.join(home, ".config", "agentikit");
10
+ export function getConfigDir(env, platform) {
11
+ return _getConfigDir(env, platform);
28
12
  }
29
13
  export function getConfigPath() {
30
- return path.join(getConfigDir(), "config.json");
14
+ return _getConfigPath();
31
15
  }
32
16
  // ── Load / Save / Update ────────────────────────────────────────────────────
17
+ let cachedConfig;
33
18
  export function loadConfig() {
34
19
  const configPath = getConfigPath();
20
+ try {
21
+ const stat = fs.statSync(configPath);
22
+ if (cachedConfig && cachedConfig.path === configPath && cachedConfig.mtime === stat.mtimeMs) {
23
+ return cachedConfig.config;
24
+ }
25
+ }
26
+ catch {
27
+ // File doesn't exist — return defaults below
28
+ cachedConfig = undefined;
29
+ return { ...DEFAULT_CONFIG };
30
+ }
35
31
  const raw = readConfigObject(configPath);
36
32
  const config = raw ? pickKnownKeys(raw) : { ...DEFAULT_CONFIG };
37
33
  // Inject API keys from environment variables.
@@ -47,9 +43,18 @@ export function loadConfig() {
47
43
  if (envKey)
48
44
  config.llm.apiKey = envKey;
49
45
  }
46
+ // Cache the parsed config with its path and mtime for subsequent calls
47
+ try {
48
+ const stat = fs.statSync(configPath);
49
+ cachedConfig = { config, path: configPath, mtime: stat.mtimeMs };
50
+ }
51
+ catch {
52
+ // If we can't stat (unlikely since we just read it), skip caching
53
+ }
50
54
  return config;
51
55
  }
52
56
  export function saveConfig(config) {
57
+ cachedConfig = undefined;
53
58
  const configPath = getConfigPath();
54
59
  const dir = path.dirname(configPath);
55
60
  fs.mkdirSync(dir, { recursive: true });
@@ -93,11 +98,23 @@ export function updateConfig(partial) {
93
98
  // ── Helpers ─────────────────────────────────────────────────────────────────
94
99
  function pickKnownKeys(raw) {
95
100
  const config = { ...DEFAULT_CONFIG };
101
+ if (typeof raw.stashDir === "string" && raw.stashDir.trim()) {
102
+ config.stashDir = raw.stashDir.trim();
103
+ }
96
104
  if (typeof raw.semanticSearch === "boolean") {
97
105
  config.semanticSearch = raw.semanticSearch;
98
106
  }
107
+ if (Array.isArray(raw.searchPaths)) {
108
+ config.searchPaths = raw.searchPaths.filter((d) => typeof d === "string");
109
+ }
110
+ // Backward compat: merge legacy mountedStashDirs into searchPaths
99
111
  if (Array.isArray(raw.mountedStashDirs)) {
100
- config.mountedStashDirs = raw.mountedStashDirs.filter((d) => typeof d === "string");
112
+ const legacy = raw.mountedStashDirs.filter((d) => typeof d === "string");
113
+ const existing = new Set(config.searchPaths);
114
+ for (const d of legacy) {
115
+ if (!existing.has(d))
116
+ config.searchPaths.push(d);
117
+ }
101
118
  }
102
119
  const embedding = parseEmbeddingConfig(raw.embedding);
103
120
  if (embedding)
@@ -115,7 +132,8 @@ function pickKnownKeys(raw) {
115
132
  }
116
133
  function readConfigObject(configPath) {
117
134
  try {
118
- const raw = JSON.parse(fs.readFileSync(configPath, "utf8"));
135
+ const text = fs.readFileSync(configPath, "utf8");
136
+ const raw = JSON.parse(stripJsonComments(text));
119
137
  if (typeof raw !== "object" || raw === null || Array.isArray(raw))
120
138
  return undefined;
121
139
  return raw;
@@ -124,6 +142,54 @@ function readConfigObject(configPath) {
124
142
  return undefined;
125
143
  }
126
144
  }
145
+ /**
146
+ * Strip JavaScript-style comments from a JSON string (JSONC support).
147
+ * Handles // line comments and /* block comments while preserving
148
+ * comment-like sequences inside quoted strings.
149
+ */
150
+ export function stripJsonComments(text) {
151
+ let result = "";
152
+ let i = 0;
153
+ let inString = false;
154
+ let stringChar = "";
155
+ while (i < text.length) {
156
+ if (inString) {
157
+ if (text[i] === "\\") {
158
+ result += text[i] + (text[i + 1] ?? "");
159
+ i += 2;
160
+ continue;
161
+ }
162
+ if (text[i] === stringChar) {
163
+ inString = false;
164
+ }
165
+ result += text[i];
166
+ i++;
167
+ continue;
168
+ }
169
+ if (text[i] === '"' || text[i] === "'") {
170
+ inString = true;
171
+ stringChar = text[i];
172
+ result += text[i];
173
+ i++;
174
+ continue;
175
+ }
176
+ if (text[i] === "/" && text[i + 1] === "/") {
177
+ while (i < text.length && text[i] !== "\n")
178
+ i++;
179
+ continue;
180
+ }
181
+ if (text[i] === "/" && text[i + 1] === "*") {
182
+ i += 2;
183
+ while (i < text.length && !(text[i] === "*" && text[i + 1] === "/"))
184
+ i++;
185
+ i += 2;
186
+ continue;
187
+ }
188
+ result += text[i];
189
+ i++;
190
+ }
191
+ return result;
192
+ }
127
193
  function parseEmbeddingConfig(value) {
128
194
  if (typeof value !== "object" || value === null || Array.isArray(value))
129
195
  return undefined;
@@ -230,5 +296,7 @@ function asNonEmptyString(value) {
230
296
  return typeof value === "string" && value ? value : undefined;
231
297
  }
232
298
  function asRegistrySource(value) {
233
- return value === "npm" || value === "github" || value === "git" ? value : undefined;
299
+ if (value === "npm" || value === "github" || value === "git" || value === "local")
300
+ return value;
301
+ return undefined;
234
302
  }
@@ -1,14 +1,15 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { createRequire } from "node:module";
3
4
  import { Database } from "bun:sqlite";
5
+ import { cosineSimilarity } from "./embedder";
6
+ import { getDbPath as _getDbPath } from "./paths";
4
7
  // ── Constants ───────────────────────────────────────────────────────────────
5
- export const DB_VERSION = 5;
8
+ export const DB_VERSION = 6;
6
9
  export const EMBEDDING_DIM = 384;
7
10
  // ── Path ────────────────────────────────────────────────────────────────────
8
11
  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
+ return _getDbPath();
12
13
  }
13
14
  // ── Database lifecycle ──────────────────────────────────────────────────────
14
15
  export function openDatabase(dbPath, options) {
@@ -23,26 +24,51 @@ export function openDatabase(dbPath, options) {
23
24
  // Try to load sqlite-vec extension
24
25
  loadVecExtension(db);
25
26
  ensureSchema(db, options?.embeddingDim ?? EMBEDDING_DIM);
27
+ // Warn once at init if using JS fallback with many entries
28
+ warnIfVecMissing(db, { once: true });
26
29
  return db;
27
30
  }
28
31
  export function closeDatabase(db) {
29
32
  db.close();
30
33
  }
31
34
  // ── sqlite-vec extension ────────────────────────────────────────────────────
32
- let vecAvailable = false;
35
+ const vecStatus = new WeakMap();
33
36
  function loadVecExtension(db) {
34
37
  try {
35
- const sqliteVec = require("sqlite-vec");
38
+ const esmRequire = createRequire(import.meta.url);
39
+ const sqliteVec = esmRequire("sqlite-vec");
36
40
  sqliteVec.load(db);
37
- vecAvailable = true;
41
+ vecStatus.set(db, true);
38
42
  }
39
43
  catch {
40
- console.warn("sqlite-vec extension not available, embeddings will be skipped");
41
- vecAvailable = false;
44
+ vecStatus.set(db, false);
42
45
  }
43
46
  }
44
- export function isVecAvailable() {
45
- return vecAvailable;
47
+ export function isVecAvailable(db) {
48
+ return vecStatus.get(db) ?? false;
49
+ }
50
+ const VEC_DOCS_URL = "https://github.com/itlackey/agentikit/blob/main/docs/configuration.md#sqlite-vec-extension";
51
+ const VEC_FALLBACK_THRESHOLD = 10_000;
52
+ let vecInitWarned = false;
53
+ /**
54
+ * Warn if sqlite-vec is unavailable and embedding count exceeds threshold.
55
+ * Called from openDatabase (once at init) and from indexer (each run).
56
+ */
57
+ export function warnIfVecMissing(db, { once } = { once: false }) {
58
+ if (isVecAvailable(db))
59
+ return;
60
+ if (once && vecInitWarned)
61
+ return;
62
+ try {
63
+ const row = db.prepare("SELECT COUNT(*) AS cnt FROM embeddings").get();
64
+ const count = row?.cnt ?? 0;
65
+ if (count >= VEC_FALLBACK_THRESHOLD) {
66
+ console.warn("Semantic search is using JS fallback for %d entries. Install sqlite-vec for faster performance.\n See: %s", count, VEC_DOCS_URL);
67
+ if (once)
68
+ vecInitWarned = true;
69
+ }
70
+ }
71
+ catch { /* embeddings table may not exist yet during init */ }
46
72
  }
47
73
  // ── Schema ──────────────────────────────────────────────────────────────────
48
74
  function ensureSchema(db, embeddingDim) {
@@ -56,6 +82,7 @@ function ensureSchema(db, embeddingDim) {
56
82
  // Check stored version — if it differs from DB_VERSION, drop and recreate all tables
57
83
  const storedVersion = getMeta(db, "version");
58
84
  if (storedVersion && storedVersion !== String(DB_VERSION)) {
85
+ db.exec("DROP TABLE IF EXISTS embeddings");
59
86
  db.exec("DROP TABLE IF EXISTS entries_vec");
60
87
  db.exec("DROP TABLE IF EXISTS entries_fts");
61
88
  db.exec("DROP INDEX IF EXISTS idx_entries_dir");
@@ -77,6 +104,14 @@ function ensureSchema(db, embeddingDim) {
77
104
 
78
105
  CREATE INDEX IF NOT EXISTS idx_entries_dir ON entries(dir_path);
79
106
  CREATE INDEX IF NOT EXISTS idx_entries_type ON entries(entry_type);
107
+ `);
108
+ // BLOB-based embedding storage (always available, no sqlite-vec needed)
109
+ db.exec(`
110
+ CREATE TABLE IF NOT EXISTS embeddings (
111
+ id INTEGER PRIMARY KEY,
112
+ embedding BLOB NOT NULL,
113
+ FOREIGN KEY (id) REFERENCES entries(id)
114
+ );
80
115
  `);
81
116
  // FTS5 table — standalone with explicit entry_id for joining
82
117
  const ftsExists = db
@@ -92,7 +127,7 @@ function ensureSchema(db, embeddingDim) {
92
127
  `);
93
128
  }
94
129
  // sqlite-vec table
95
- if (vecAvailable) {
130
+ if (isVecAvailable(db)) {
96
131
  // Check if stored embedding dimension differs from configured one
97
132
  const storedDim = getMeta(db, "embeddingDim");
98
133
  if (storedDim && storedDim !== String(embeddingDim)) {
@@ -147,15 +182,19 @@ export function upsertEntry(db, entryKey, dirPath, filePath, stashDir, entry, se
147
182
  return row.id;
148
183
  }
149
184
  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) {
185
+ const ids = db
186
+ .prepare("SELECT id FROM entries WHERE dir_path = ?")
187
+ .all(dirPath);
188
+ for (const { id } of ids) {
189
+ try {
190
+ db.prepare("DELETE FROM embeddings WHERE id = ?").run(id);
191
+ }
192
+ catch { /* ignore */ }
193
+ if (isVecAvailable(db)) {
155
194
  try {
156
195
  db.prepare("DELETE FROM entries_vec WHERE id = ?").run(id);
157
196
  }
158
- catch { /* ignore if vec table missing */ }
197
+ catch { /* ignore */ }
159
198
  }
160
199
  }
161
200
  db.prepare("DELETE FROM entries WHERE dir_path = ?").run(dirPath);
@@ -166,32 +205,67 @@ export function rebuildFts(db) {
166
205
  }
167
206
  // ── Vector operations ───────────────────────────────────────────────────────
168
207
  export function upsertEmbedding(db, entryId, embedding) {
169
- if (!vecAvailable)
170
- return;
171
208
  const buf = float32Buffer(embedding);
172
- try {
173
- db.prepare("DELETE FROM entries_vec WHERE id = ?").run(entryId);
209
+ // Always write to BLOB table (works without sqlite-vec)
210
+ db.prepare("INSERT OR REPLACE INTO embeddings (id, embedding) VALUES (?, ?)").run(entryId, buf);
211
+ // Also write to sqlite-vec table when available (fast path)
212
+ if (isVecAvailable(db)) {
213
+ try {
214
+ db.prepare("DELETE FROM entries_vec WHERE id = ?").run(entryId);
215
+ }
216
+ catch { /* ignore */ }
217
+ db.prepare("INSERT INTO entries_vec (id, embedding) VALUES (?, ?)").run(entryId, buf);
174
218
  }
175
- catch { /* ignore */ }
176
- db.prepare("INSERT INTO entries_vec (id, embedding) VALUES (?, ?)").run(entryId, buf);
177
219
  }
178
220
  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 [];
221
+ // Fast path: use sqlite-vec when available
222
+ if (isVecAvailable(db)) {
223
+ const buf = float32Buffer(queryEmbedding);
224
+ try {
225
+ return db
226
+ .prepare("SELECT id, distance FROM entries_vec WHERE embedding MATCH ? AND k = ?")
227
+ .all(buf, k);
228
+ }
229
+ catch {
230
+ return [];
231
+ }
189
232
  }
233
+ // Fallback: JS-based cosine similarity over BLOB table
234
+ return searchBlobVec(db, queryEmbedding, k);
190
235
  }
191
236
  function float32Buffer(vec) {
192
237
  const f32 = new Float32Array(vec);
193
238
  return Buffer.from(f32.buffer);
194
239
  }
240
+ function bufferToFloat32(buf) {
241
+ const f32 = new Float32Array(buf.buffer, buf.byteOffset, buf.byteLength / 4);
242
+ return Array.from(f32);
243
+ }
244
+ function searchBlobVec(db, queryEmbedding, k) {
245
+ try {
246
+ const rows = db
247
+ .prepare("SELECT id, embedding FROM embeddings")
248
+ .all();
249
+ if (rows.length === 0)
250
+ return [];
251
+ const scored = [];
252
+ for (const row of rows) {
253
+ const embedding = bufferToFloat32(row.embedding);
254
+ const similarity = cosineSimilarity(queryEmbedding, embedding);
255
+ scored.push({ id: row.id, similarity });
256
+ }
257
+ scored.sort((a, b) => b.similarity - a.similarity);
258
+ // Convert cosine similarity to L2 distance for compatibility with sqlite-vec interface
259
+ // For normalized vectors: L2² = 2(1 - cos_sim)
260
+ return scored.slice(0, k).map(({ id, similarity }) => ({
261
+ id,
262
+ distance: Math.sqrt(2 * Math.max(0, 1 - similarity)),
263
+ }));
264
+ }
265
+ catch {
266
+ return [];
267
+ }
268
+ }
195
269
  // ── FTS5 search ─────────────────────────────────────────────────────────────
196
270
  export function searchFts(db, query, limit, entryType) {
197
271
  const ftsQuery = sanitizeFtsQuery(query);
@@ -246,7 +320,7 @@ function sanitizeFtsQuery(query) {
246
320
  if (tokens.length === 0)
247
321
  return "";
248
322
  // Use unquoted tokens so the porter stemmer can normalize word forms
249
- return tokens.join(" OR ");
323
+ return tokens.join(" ");
250
324
  }
251
325
  // ── All entries ─────────────────────────────────────────────────────────────
252
326
  export function getAllEntries(db, entryType) {