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
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 { ConfigError } from "./errors";
5
+ import { getConfigPath, getDefaultStashDir } from "./paths";
6
+ // ── Constants ───────────────────────────────────────────────────────────────
7
+ export const IS_WINDOWS = process.platform === "win32";
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 ConfigError(`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 ConfigError(`Unable to read stash directory at "${stashDir}".`);
50
+ }
51
+ if (!stat.isDirectory()) {
52
+ throw new ConfigError(`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 * 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 * 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
+ }
@@ -1,5 +1,6 @@
1
1
  import { DEFAULT_CONFIG, } from "./config";
2
2
  import { EMBEDDING_DIM } from "./db";
3
+ import { ConfigError, UsageError } from "./errors";
3
4
  const LOCAL_EMBEDDING_MODEL = "Xenova/all-MiniLM-L6-v2";
4
5
  const DEFAULT_LLM_TEMPERATURE = 0.3;
5
6
  const DEFAULT_LLM_MAX_TOKENS = 512;
@@ -59,35 +60,39 @@ const LLM_PROVIDER_PRESETS = {
59
60
  };
60
61
  export function parseConfigValue(key, value) {
61
62
  switch (key) {
63
+ case "stashDir":
64
+ return { stashDir: requireNonEmptyString(value, key) };
62
65
  case "semanticSearch":
63
66
  if (value !== "true" && value !== "false") {
64
- throw new Error(`Invalid value for semanticSearch: expected "true" or "false"`);
67
+ throw new UsageError(`Invalid value for semanticSearch: expected "true" or "false"`);
65
68
  }
66
69
  return { semanticSearch: value === "true" };
67
- case "mountedStashDirs":
70
+ case "searchPaths":
68
71
  try {
69
72
  const parsed = JSON.parse(value);
70
73
  if (!Array.isArray(parsed))
71
- throw new Error("expected JSON array");
72
- return { mountedStashDirs: parsed.filter((d) => typeof d === "string") };
74
+ throw new UsageError("expected JSON array");
75
+ return { searchPaths: parsed.filter((d) => typeof d === "string") };
73
76
  }
74
77
  catch {
75
- throw new Error(`Invalid value for mountedStashDirs: expected JSON array (e.g. '["/path/a","/path/b"]')`);
78
+ throw new UsageError(`Invalid value for searchPaths: expected JSON array (e.g. '["/path/a","/path/b"]')`);
76
79
  }
77
80
  case "embedding":
78
81
  return { embedding: parseEmbeddingConnectionValue(value) };
79
82
  case "llm":
80
83
  return { llm: parseLlmConnectionValue(value) };
81
84
  default:
82
- throw new Error(`Unknown config key: ${key}`);
85
+ throw new UsageError(`Unknown config key: ${key}`);
83
86
  }
84
87
  }
85
88
  export function getConfigValue(config, key) {
86
89
  switch (key) {
90
+ case "stashDir":
91
+ return config.stashDir ?? null;
87
92
  case "semanticSearch":
88
93
  return config.semanticSearch;
89
- case "mountedStashDirs":
90
- return [...config.mountedStashDirs];
94
+ case "searchPaths":
95
+ return [...config.searchPaths];
91
96
  case "embedding":
92
97
  return maskSecrets(getEmbeddingDisplayConfig(config));
93
98
  case "embedding.provider":
@@ -115,13 +120,14 @@ export function getConfigValue(config, key) {
115
120
  case "llm.apiKey":
116
121
  return maskSecret(getLlmDisplayConfig(config).apiKey) ?? null;
117
122
  default:
118
- throw new Error(`Unknown config key: ${key}`);
123
+ throw new UsageError(`Unknown config key: ${key}`);
119
124
  }
120
125
  }
121
126
  export function setConfigValue(config, key, rawValue) {
122
127
  switch (key) {
128
+ case "stashDir":
123
129
  case "semanticSearch":
124
- case "mountedStashDirs":
130
+ case "searchPaths":
125
131
  case "embedding":
126
132
  case "llm":
127
133
  return { ...config, ...parseConfigValue(key, rawValue) };
@@ -202,11 +208,13 @@ export function setConfigValue(config, key, rawValue) {
202
208
  },
203
209
  };
204
210
  default:
205
- throw new Error(`Unknown config key: ${key}`);
211
+ throw new UsageError(`Unknown config key: ${key}`);
206
212
  }
207
213
  }
208
214
  export function unsetConfigValue(config, key) {
209
215
  switch (key) {
216
+ case "stashDir":
217
+ return { ...config, stashDir: undefined };
210
218
  case "embedding":
211
219
  return { ...config, embedding: undefined };
212
220
  case "embedding.apiKey":
@@ -240,13 +248,14 @@ export function unsetConfigValue(config, key) {
240
248
  return config;
241
249
  return { ...config, llm: omitKey(config.llm, "provider") };
242
250
  default:
243
- throw new Error(`Unknown or unsupported unset key: ${key}`);
251
+ throw new UsageError(`Unknown or unsupported unset key: ${key}`);
244
252
  }
245
253
  }
246
254
  export function listConfig(config) {
247
255
  return {
248
256
  ...DEFAULT_CONFIG,
249
257
  ...maskSecrets(config),
258
+ stashDir: config.stashDir ?? null,
250
259
  embedding: maskSecrets(getEmbeddingDisplayConfig(config)),
251
260
  llm: maskSecrets(getLlmDisplayConfig(config)),
252
261
  };
@@ -265,7 +274,7 @@ export function useProvider(config, scope, providerName) {
265
274
  if (scope === "embedding") {
266
275
  const preset = EMBEDDING_PROVIDER_PRESETS[providerName];
267
276
  if (!preset) {
268
- throw new Error(`Unknown embedding provider: ${providerName}`);
277
+ throw new UsageError(`Unknown embedding provider: ${providerName}`);
269
278
  }
270
279
  if (!preset.config) {
271
280
  return { ...config, embedding: undefined };
@@ -274,7 +283,7 @@ export function useProvider(config, scope, providerName) {
274
283
  }
275
284
  const preset = LLM_PROVIDER_PRESETS[providerName];
276
285
  if (!preset) {
277
- throw new Error(`Unknown llm provider: ${providerName}`);
286
+ throw new UsageError(`Unknown llm provider: ${providerName}`);
278
287
  }
279
288
  if (!preset.config) {
280
289
  return { ...config, llm: undefined };
@@ -380,68 +389,65 @@ function parseJsonObject(value, key, example) {
380
389
  parsed = JSON.parse(value);
381
390
  }
382
391
  catch {
383
- throw new Error(`Invalid value for ${key}: expected JSON object with endpoint and model`
384
- + ` (e.g. '{"endpoint":"${example.endpoint}","model":"${example.model}"}')`);
392
+ throw new UsageError(`Invalid value for ${key}: expected JSON object with endpoint and model` +
393
+ ` (e.g. '{"endpoint":"${example.endpoint}","model":"${example.model}"}')`);
385
394
  }
386
395
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
387
- throw new Error(`Invalid value for ${key}: expected a JSON object`);
396
+ throw new UsageError(`Invalid value for ${key}: expected a JSON object`);
388
397
  }
389
398
  return parsed;
390
399
  }
391
400
  function asRequiredString(value, key, field) {
392
401
  if (typeof value !== "string" || !value) {
393
- throw new Error(`Invalid value for ${key}: "${field}" is a required string field`);
402
+ throw new UsageError(`Invalid value for ${key}: "${field}" is a required string field`);
394
403
  }
395
404
  return value;
396
405
  }
397
406
  function requireEmbeddingConfig(config) {
398
407
  if (!config.embedding) {
399
- throw new Error("Embedding provider is using the built-in local default. Run `akm config use embedding <provider>` first.");
408
+ throw new ConfigError("Embedding provider is using the built-in local default. Run `akm config use embedding <provider>` first.");
400
409
  }
401
410
  return config.embedding;
402
411
  }
403
412
  function requireLlmConfig(config) {
404
413
  if (!config.llm) {
405
- throw new Error("LLM provider is disabled. Run `akm config use llm <provider>` first.");
414
+ throw new ConfigError("LLM provider is disabled. Run `akm config use llm <provider>` first.");
406
415
  }
407
416
  return config.llm;
408
417
  }
409
418
  function requireNonEmptyString(value, key) {
410
419
  if (!value) {
411
- throw new Error(`Invalid value for ${key}: expected a non-empty string`);
420
+ throw new UsageError(`Invalid value for ${key}: expected a non-empty string`);
412
421
  }
413
422
  return value;
414
423
  }
415
424
  function parseNumber(value, key) {
416
425
  const parsed = Number(value);
417
426
  if (!Number.isFinite(parsed)) {
418
- throw new Error(`Invalid value for ${key}: expected a number`);
427
+ throw new UsageError(`Invalid value for ${key}: expected a number`);
419
428
  }
420
429
  return parsed;
421
430
  }
422
431
  function parsePositiveInteger(value, key) {
423
432
  const trimmed = value.trim();
424
433
  if (!/^[1-9]\d*$/.test(trimmed)) {
425
- throw new Error(`Invalid value for ${key}: expected a positive integer`);
434
+ throw new UsageError(`Invalid value for ${key}: expected a positive integer`);
426
435
  }
427
436
  const parsed = Number(trimmed);
428
437
  if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed <= 0) {
429
- throw new Error(`Invalid value for ${key}: expected a positive integer`);
438
+ throw new UsageError(`Invalid value for ${key}: expected a positive integer`);
430
439
  }
431
440
  return parsed;
432
441
  }
433
442
  function parseUnknownNumber(value, key) {
434
443
  if (typeof value !== "number" || !Number.isFinite(value)) {
435
- throw new Error(`Invalid value for ${key}: expected a number`);
444
+ throw new UsageError(`Invalid value for ${key}: expected a number`);
436
445
  }
437
446
  return value;
438
447
  }
439
448
  function parseUnknownPositiveInteger(value, key) {
440
- if (typeof value !== "number" ||
441
- !Number.isFinite(value) ||
442
- !Number.isInteger(value) ||
443
- value <= 0) {
444
- throw new Error(`Invalid value for ${key}: expected a positive integer`);
449
+ if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
450
+ throw new UsageError(`Invalid value for ${key}: expected a positive integer`);
445
451
  }
446
452
  return value;
447
453
  }
@@ -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 });
@@ -63,7 +68,9 @@ export function saveConfig(config) {
63
68
  try {
64
69
  fs.unlinkSync(tmpPath);
65
70
  }
66
- catch { /* ignore cleanup failure */ }
71
+ catch {
72
+ /* ignore cleanup failure */
73
+ }
67
74
  throw err;
68
75
  }
69
76
  }
@@ -93,11 +100,23 @@ export function updateConfig(partial) {
93
100
  // ── Helpers ─────────────────────────────────────────────────────────────────
94
101
  function pickKnownKeys(raw) {
95
102
  const config = { ...DEFAULT_CONFIG };
103
+ if (typeof raw.stashDir === "string" && raw.stashDir.trim()) {
104
+ config.stashDir = raw.stashDir.trim();
105
+ }
96
106
  if (typeof raw.semanticSearch === "boolean") {
97
107
  config.semanticSearch = raw.semanticSearch;
98
108
  }
109
+ if (Array.isArray(raw.searchPaths)) {
110
+ config.searchPaths = raw.searchPaths.filter((d) => typeof d === "string");
111
+ }
112
+ // Backward compat: merge legacy mountedStashDirs into searchPaths
99
113
  if (Array.isArray(raw.mountedStashDirs)) {
100
- config.mountedStashDirs = raw.mountedStashDirs.filter((d) => typeof d === "string");
114
+ const legacy = raw.mountedStashDirs.filter((d) => typeof d === "string");
115
+ const existing = new Set(config.searchPaths);
116
+ for (const d of legacy) {
117
+ if (!existing.has(d))
118
+ config.searchPaths.push(d);
119
+ }
101
120
  }
102
121
  const embedding = parseEmbeddingConfig(raw.embedding);
103
122
  if (embedding)
@@ -115,7 +134,8 @@ function pickKnownKeys(raw) {
115
134
  }
116
135
  function readConfigObject(configPath) {
117
136
  try {
118
- const raw = JSON.parse(fs.readFileSync(configPath, "utf8"));
137
+ const text = fs.readFileSync(configPath, "utf8");
138
+ const raw = JSON.parse(stripJsonComments(text));
119
139
  if (typeof raw !== "object" || raw === null || Array.isArray(raw))
120
140
  return undefined;
121
141
  return raw;
@@ -124,6 +144,54 @@ function readConfigObject(configPath) {
124
144
  return undefined;
125
145
  }
126
146
  }
147
+ /**
148
+ * Strip JavaScript-style comments from a JSON string (JSONC support).
149
+ * Handles // line comments and /* block comments while preserving
150
+ * comment-like sequences inside quoted strings.
151
+ */
152
+ export function stripJsonComments(text) {
153
+ let result = "";
154
+ let i = 0;
155
+ let inString = false;
156
+ let stringChar = "";
157
+ while (i < text.length) {
158
+ if (inString) {
159
+ if (text[i] === "\\") {
160
+ result += text[i] + (text[i + 1] ?? "");
161
+ i += 2;
162
+ continue;
163
+ }
164
+ if (text[i] === stringChar) {
165
+ inString = false;
166
+ }
167
+ result += text[i];
168
+ i++;
169
+ continue;
170
+ }
171
+ if (text[i] === '"' || text[i] === "'") {
172
+ inString = true;
173
+ stringChar = text[i];
174
+ result += text[i];
175
+ i++;
176
+ continue;
177
+ }
178
+ if (text[i] === "/" && text[i + 1] === "/") {
179
+ while (i < text.length && text[i] !== "\n")
180
+ i++;
181
+ continue;
182
+ }
183
+ if (text[i] === "/" && text[i + 1] === "*") {
184
+ i += 2;
185
+ while (i < text.length && !(text[i] === "*" && text[i + 1] === "/"))
186
+ i++;
187
+ i += 2;
188
+ continue;
189
+ }
190
+ result += text[i];
191
+ i++;
192
+ }
193
+ return result;
194
+ }
127
195
  function parseEmbeddingConfig(value) {
128
196
  if (typeof value !== "object" || value === null || Array.isArray(value))
129
197
  return undefined;
@@ -230,5 +298,7 @@ function asNonEmptyString(value) {
230
298
  return typeof value === "string" && value ? value : undefined;
231
299
  }
232
300
  function asRegistrySource(value) {
233
- return value === "npm" || value === "github" || value === "git" ? value : undefined;
301
+ if (value === "npm" || value === "github" || value === "git" || value === "local")
302
+ return value;
303
+ return undefined;
234
304
  }