agentikit 0.0.12 → 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.
- package/LICENSE +385 -0
- package/README.md +186 -100
- package/dist/cli.js +671 -0
- package/dist/common.js +192 -0
- package/dist/{src/config-cli.js → config-cli.js} +14 -6
- package/dist/{src/config.js → config.js} +92 -24
- package/dist/{src/db.js → db.js} +109 -35
- package/dist/{src/embedder.js → embedder.js} +57 -2
- package/dist/file-context.js +158 -0
- package/dist/{src/handlers → handlers}/command-handler.js +2 -0
- package/dist/{src/handlers → handlers}/index.js +0 -6
- package/dist/{src/indexer.js → indexer.js} +34 -10
- package/dist/init.js +43 -0
- package/dist/lockfile.js +55 -0
- package/dist/matchers.js +157 -0
- package/dist/{src/metadata.js → metadata.js} +12 -1
- package/dist/{src/origin-resolve.js → origin-resolve.js} +10 -9
- package/dist/paths.js +82 -0
- package/dist/{src/registry-install.js → registry-install.js} +145 -17
- package/dist/{src/registry-resolve.js → registry-resolve.js} +178 -18
- package/dist/{src/registry-search.js → registry-search.js} +8 -16
- package/dist/renderers.js +276 -0
- package/dist/{src/ripgrep-install.js → ripgrep-install.js} +5 -5
- package/dist/{src/ripgrep-resolve.js → ripgrep-resolve.js} +21 -11
- package/dist/self-update.js +220 -0
- package/dist/{src/stash-add.js → stash-add.js} +11 -2
- package/dist/stash-clone.js +115 -0
- package/dist/{src/stash-registry.js → stash-registry.js} +15 -41
- package/dist/{src/stash-search.js → stash-search.js} +67 -55
- package/dist/{src/stash-show.js → stash-show.js} +30 -3
- package/dist/{src/stash-source.js → stash-source.js} +56 -9
- package/dist/submit.js +552 -0
- package/dist/{src/walker.js → walker.js} +38 -0
- package/package.json +7 -16
- package/dist/index.d.ts +0 -28
- package/dist/index.js +0 -15
- package/dist/src/asset-spec.d.ts +0 -16
- package/dist/src/asset-type-handler.d.ts +0 -27
- package/dist/src/cli.d.ts +0 -2
- package/dist/src/cli.js +0 -399
- package/dist/src/common.d.ts +0 -13
- package/dist/src/common.js +0 -60
- package/dist/src/config-cli.d.ts +0 -9
- package/dist/src/config.d.ts +0 -50
- package/dist/src/db.d.ts +0 -46
- package/dist/src/embedder.d.ts +0 -10
- package/dist/src/frontmatter.d.ts +0 -30
- package/dist/src/github.d.ts +0 -4
- package/dist/src/handlers/agent-handler.d.ts +0 -2
- package/dist/src/handlers/command-handler.d.ts +0 -2
- package/dist/src/handlers/index.d.ts +0 -6
- package/dist/src/handlers/knowledge-handler.d.ts +0 -2
- package/dist/src/handlers/markdown-helpers.d.ts +0 -7
- package/dist/src/handlers/script-handler.d.ts +0 -2
- package/dist/src/handlers/skill-handler.d.ts +0 -2
- package/dist/src/handlers/tool-handler.d.ts +0 -2
- package/dist/src/indexer.d.ts +0 -22
- package/dist/src/init.d.ts +0 -19
- package/dist/src/init.js +0 -99
- package/dist/src/llm.d.ts +0 -15
- package/dist/src/markdown.d.ts +0 -18
- package/dist/src/metadata.d.ts +0 -41
- package/dist/src/origin-resolve.d.ts +0 -19
- package/dist/src/registry-install.d.ts +0 -11
- package/dist/src/registry-resolve.d.ts +0 -3
- package/dist/src/registry-search.d.ts +0 -27
- package/dist/src/registry-types.d.ts +0 -62
- package/dist/src/ripgrep-install.d.ts +0 -12
- package/dist/src/ripgrep-resolve.d.ts +0 -13
- package/dist/src/ripgrep.d.ts +0 -3
- package/dist/src/similarity.js +0 -211
- package/dist/src/stash-add.d.ts +0 -4
- package/dist/src/stash-clone.d.ts +0 -22
- package/dist/src/stash-clone.js +0 -83
- package/dist/src/stash-ref.d.ts +0 -31
- package/dist/src/stash-registry.d.ts +0 -18
- package/dist/src/stash-resolve.d.ts +0 -2
- package/dist/src/stash-search.d.ts +0 -8
- package/dist/src/stash-show.d.ts +0 -5
- package/dist/src/stash-source.d.ts +0 -24
- package/dist/src/stash-types.d.ts +0 -227
- package/dist/src/stash.d.ts +0 -16
- package/dist/src/stash.js +0 -9
- package/dist/src/tool-runner.d.ts +0 -35
- package/dist/src/walker.d.ts +0 -19
- package/src/asset-spec.ts +0 -85
- package/src/asset-type-handler.ts +0 -77
- package/src/cli.ts +0 -427
- package/src/common.ts +0 -76
- package/src/config-cli.ts +0 -499
- package/src/config.ts +0 -305
- package/src/db.ts +0 -411
- package/src/embedder.ts +0 -128
- package/src/frontmatter.ts +0 -95
- package/src/github.ts +0 -21
- package/src/handlers/agent-handler.ts +0 -32
- package/src/handlers/command-handler.ts +0 -29
- package/src/handlers/index.ts +0 -25
- package/src/handlers/knowledge-handler.ts +0 -62
- package/src/handlers/markdown-helpers.ts +0 -19
- package/src/handlers/script-handler.ts +0 -92
- package/src/handlers/skill-handler.ts +0 -37
- package/src/handlers/tool-handler.ts +0 -71
- package/src/indexer.ts +0 -392
- package/src/init.ts +0 -114
- package/src/llm.ts +0 -125
- package/src/markdown.ts +0 -106
- package/src/metadata.ts +0 -333
- package/src/origin-resolve.ts +0 -67
- package/src/registry-install.ts +0 -361
- package/src/registry-resolve.ts +0 -341
- package/src/registry-search.ts +0 -335
- package/src/registry-types.ts +0 -72
- package/src/ripgrep-install.ts +0 -200
- package/src/ripgrep-resolve.ts +0 -72
- package/src/ripgrep.ts +0 -3
- package/src/stash-add.ts +0 -63
- package/src/stash-clone.ts +0 -127
- package/src/stash-ref.ts +0 -99
- package/src/stash-registry.ts +0 -259
- package/src/stash-resolve.ts +0 -50
- package/src/stash-search.ts +0 -613
- package/src/stash-show.ts +0 -55
- package/src/stash-source.ts +0 -103
- package/src/stash-types.ts +0 -231
- package/src/stash.ts +0 -39
- package/src/tool-runner.ts +0 -142
- package/src/walker.ts +0 -53
- /package/dist/{src/asset-spec.js → asset-spec.js} +0 -0
- /package/dist/{src/asset-type-handler.js → asset-type-handler.js} +0 -0
- /package/dist/{src/frontmatter.js → frontmatter.js} +0 -0
- /package/dist/{src/github.js → github.js} +0 -0
- /package/dist/{src/handlers → handlers}/agent-handler.js +0 -0
- /package/dist/{src/handlers → handlers}/knowledge-handler.js +0 -0
- /package/dist/{src/handlers → handlers}/markdown-helpers.js +0 -0
- /package/dist/{src/handlers → handlers}/script-handler.js +0 -0
- /package/dist/{src/handlers → handlers}/skill-handler.js +0 -0
- /package/dist/{src/handlers → handlers}/tool-handler.js +0 -0
- /package/dist/{src/llm.js → llm.js} +0 -0
- /package/dist/{src/markdown.js → markdown.js} +0 -0
- /package/dist/{src/registry-types.js → registry-types.js} +0 -0
- /package/dist/{src/ripgrep.js → ripgrep.js} +0 -0
- /package/dist/{src/stash-ref.js → stash-ref.js} +0 -0
- /package/dist/{src/stash-resolve.js → stash-resolve.js} +0 -0
- /package/dist/{src/stash-types.js → stash-types.js} +0 -0
- /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 "
|
|
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 {
|
|
74
|
+
return { searchPaths: parsed.filter((d) => typeof d === "string") };
|
|
73
75
|
}
|
|
74
76
|
catch {
|
|
75
|
-
throw new Error(`Invalid value for
|
|
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 "
|
|
90
|
-
return [...config.
|
|
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 "
|
|
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
|
-
|
|
7
|
+
searchPaths: [],
|
|
7
8
|
};
|
|
8
9
|
// ── Paths ───────────────────────────────────────────────────────────────────
|
|
9
|
-
export function getConfigDir(env
|
|
10
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
299
|
+
if (value === "npm" || value === "github" || value === "git" || value === "local")
|
|
300
|
+
return value;
|
|
301
|
+
return undefined;
|
|
234
302
|
}
|
package/dist/{src/db.js → db.js}
RENAMED
|
@@ -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 =
|
|
8
|
+
export const DB_VERSION = 6;
|
|
6
9
|
export const EMBEDDING_DIM = 384;
|
|
7
10
|
// ── Path ────────────────────────────────────────────────────────────────────
|
|
8
11
|
export function getDbPath() {
|
|
9
|
-
|
|
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
|
-
|
|
35
|
+
const vecStatus = new WeakMap();
|
|
33
36
|
function loadVecExtension(db) {
|
|
34
37
|
try {
|
|
35
|
-
const
|
|
38
|
+
const esmRequire = createRequire(import.meta.url);
|
|
39
|
+
const sqliteVec = esmRequire("sqlite-vec");
|
|
36
40
|
sqliteVec.load(db);
|
|
37
|
-
|
|
41
|
+
vecStatus.set(db, true);
|
|
38
42
|
}
|
|
39
43
|
catch {
|
|
40
|
-
|
|
41
|
-
vecAvailable = false;
|
|
44
|
+
vecStatus.set(db, false);
|
|
42
45
|
}
|
|
43
46
|
}
|
|
44
|
-
export function isVecAvailable() {
|
|
45
|
-
return
|
|
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 (
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
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
|
-
|
|
173
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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("
|
|
323
|
+
return tokens.join(" ");
|
|
250
324
|
}
|
|
251
325
|
// ── All entries ─────────────────────────────────────────────────────────────
|
|
252
326
|
export function getAllEntries(db, entryType) {
|