clawvault 2.4.6 → 2.5.0

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 (69) hide show
  1. package/bin/clawvault.js +5 -0
  2. package/bin/command-registration.test.js +1 -1
  3. package/bin/help-contract.test.js +1 -0
  4. package/bin/register-config-route-commands.test.js +8 -1
  5. package/bin/register-core-commands.js +3 -3
  6. package/bin/register-project-commands.js +209 -0
  7. package/bin/register-project-commands.test.js +201 -0
  8. package/bin/register-query-commands.js +40 -0
  9. package/bin/register-task-commands.js +2 -18
  10. package/bin/register-task-commands.test.js +3 -4
  11. package/bin/test-helpers/cli-command-fixtures.js +5 -0
  12. package/dist/{chunk-3PJIGGWV.js → chunk-2CDEETQN.js} +1 -0
  13. package/dist/{chunk-FD2ZA65C.js → chunk-2RK2AG32.js} +5 -5
  14. package/dist/chunk-5GZFTAL7.js +340 -0
  15. package/dist/{chunk-P2ZH6AN5.js → chunk-6RQPD7X6.js} +3 -4
  16. package/dist/{chunk-HNMFXFYP.js → chunk-7OHQFMJK.js} +2 -1
  17. package/dist/{chunk-FKQJB6XC.js → chunk-C3PF7WBA.js} +2 -2
  18. package/dist/{chunk-JXY6T5R7.js → chunk-FW465EEA.js} +1 -1
  19. package/dist/{chunk-BI6SGGZP.js → chunk-G3OQJ2NQ.js} +1 -1
  20. package/dist/chunk-GSD4ALSI.js +724 -0
  21. package/dist/{chunk-6QLRSPLZ.js → chunk-IOALNTAN.js} +268 -47
  22. package/dist/chunk-ITPEXLHA.js +528 -0
  23. package/dist/{chunk-LLN5SPGL.js → chunk-J5EMBUPK.js} +1 -1
  24. package/dist/chunk-K3CDT7IH.js +122 -0
  25. package/dist/{chunk-AHGUJG76.js → chunk-KCCHROBR.js} +13 -69
  26. package/dist/{chunk-JTO7NZLS.js → chunk-LMCC5OC7.js} +2 -2
  27. package/dist/{chunk-QALB2V3E.js → chunk-MQUJNOHK.js} +1 -1
  28. package/dist/{chunk-H6WQUUNK.js → chunk-TMZMN7OS.js} +334 -457
  29. package/dist/{chunk-HVTTYDCJ.js → chunk-VR5NE7PZ.js} +1 -1
  30. package/dist/{chunk-22WE3J4F.js → chunk-WIICLBNF.js} +35 -4
  31. package/dist/chunk-YCVDVI5B.js +273 -0
  32. package/dist/{chunk-NAMFB7ZA.js → chunk-Z2XBWN7A.js} +0 -2
  33. package/dist/commands/archive.js +3 -3
  34. package/dist/commands/backlog.js +1 -1
  35. package/dist/commands/blocked.js +1 -1
  36. package/dist/commands/canvas.d.ts +1 -14
  37. package/dist/commands/canvas.js +123 -1543
  38. package/dist/commands/context.js +5 -6
  39. package/dist/commands/doctor.js +2 -2
  40. package/dist/commands/inject.d.ts +2 -0
  41. package/dist/commands/inject.js +14 -0
  42. package/dist/commands/kanban.js +2 -2
  43. package/dist/commands/migrate-observations.js +2 -2
  44. package/dist/commands/observe.js +8 -6
  45. package/dist/commands/project.d.ts +85 -0
  46. package/dist/commands/project.js +411 -0
  47. package/dist/commands/rebuild.js +7 -5
  48. package/dist/commands/reflect.js +5 -4
  49. package/dist/commands/replay.js +10 -7
  50. package/dist/commands/setup.d.ts +1 -1
  51. package/dist/commands/setup.js +2 -2
  52. package/dist/commands/sleep.d.ts +1 -1
  53. package/dist/commands/sleep.js +11 -8
  54. package/dist/commands/status.js +2 -2
  55. package/dist/commands/task.d.ts +2 -2
  56. package/dist/commands/task.js +11 -301
  57. package/dist/commands/wake.d.ts +1 -1
  58. package/dist/commands/wake.js +4 -4
  59. package/dist/index.d.ts +75 -107
  60. package/dist/index.js +78 -36
  61. package/dist/inject-x65KXWPk.d.ts +137 -0
  62. package/dist/lib/project-utils.d.ts +97 -0
  63. package/dist/lib/project-utils.js +19 -0
  64. package/dist/lib/task-utils.d.ts +8 -3
  65. package/dist/lib/task-utils.js +1 -1
  66. package/dist/{types-DMU3SuAV.d.ts → types-jjuYN2Xn.d.ts} +1 -1
  67. package/package.json +2 -2
  68. package/dist/chunk-L3DJ36BZ.js +0 -40
  69. package/dist/chunk-UMMCYTJV.js +0 -105
@@ -0,0 +1,528 @@
1
+ import {
2
+ DEFAULT_CATEGORIES
3
+ } from "./chunk-2CDEETQN.js";
4
+
5
+ // src/lib/config-manager.ts
6
+ import * as fs from "fs";
7
+ import * as path from "path";
8
+ var CONFIG_FILE = ".clawvault.json";
9
+ var OBSERVE_PROVIDERS = ["anthropic", "openai", "gemini"];
10
+ var OBSERVER_COMPRESSION_PROVIDERS = [
11
+ "anthropic",
12
+ "openai",
13
+ "gemini",
14
+ "openai-compatible",
15
+ "ollama"
16
+ ];
17
+ var THEMES = ["neural", "minimal", "none"];
18
+ var CONTEXT_PROFILES = ["default", "planning", "incident", "handoff", "auto"];
19
+ var SUPPORTED_CONFIG_KEYS = [
20
+ "name",
21
+ "categories",
22
+ "theme",
23
+ "observe.model",
24
+ "observe.provider",
25
+ "observer.compression.provider",
26
+ "observer.compression.model",
27
+ "observer.compression.baseUrl",
28
+ "observer.compression.apiKey",
29
+ "context.maxResults",
30
+ "context.defaultProfile",
31
+ "graph.maxHops",
32
+ "inject.maxResults",
33
+ "inject.useLlm",
34
+ "inject.scope"
35
+ ];
36
+ var DEFAULT_THEME = "none";
37
+ var DEFAULT_OBSERVE_MODEL = "gemini-2.0-flash";
38
+ var DEFAULT_OBSERVE_PROVIDER = "gemini";
39
+ var DEFAULT_CONTEXT_MAX_RESULTS = 5;
40
+ var DEFAULT_CONTEXT_PROFILE = "default";
41
+ var DEFAULT_GRAPH_MAX_HOPS = 2;
42
+ var DEFAULT_INJECT_MAX_RESULTS = 8;
43
+ var DEFAULT_INJECT_USE_LLM = true;
44
+ var DEFAULT_INJECT_SCOPE = ["global"];
45
+ function configPathFor(vaultPath) {
46
+ return path.join(path.resolve(vaultPath), CONFIG_FILE);
47
+ }
48
+ function readConfigDocument(vaultPath) {
49
+ const configPath = configPathFor(vaultPath);
50
+ if (!fs.existsSync(configPath)) {
51
+ throw new Error(`No ClawVault config found at ${configPath}`);
52
+ }
53
+ try {
54
+ const parsed = JSON.parse(fs.readFileSync(configPath, "utf-8"));
55
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
56
+ throw new Error("Config root must be a JSON object.");
57
+ }
58
+ return { ...parsed };
59
+ } catch (error) {
60
+ if (error instanceof Error) {
61
+ throw new Error(`Failed to parse ${CONFIG_FILE}: ${error.message}`);
62
+ }
63
+ throw new Error(`Failed to parse ${CONFIG_FILE}.`);
64
+ }
65
+ }
66
+ function writeConfigDocument(vaultPath, config) {
67
+ const configPath = configPathFor(vaultPath);
68
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
69
+ }
70
+ function asStringArray(value) {
71
+ if (!Array.isArray(value)) {
72
+ return null;
73
+ }
74
+ const output = value.map((entry) => typeof entry === "string" ? entry.trim() : "").filter(Boolean);
75
+ return output.length > 0 ? output : null;
76
+ }
77
+ function asPositiveInteger(value) {
78
+ if (typeof value === "number" && Number.isInteger(value) && value > 0) {
79
+ return value;
80
+ }
81
+ if (typeof value === "string") {
82
+ const parsed = Number.parseInt(value, 10);
83
+ if (Number.isInteger(parsed) && parsed > 0) {
84
+ return parsed;
85
+ }
86
+ }
87
+ return null;
88
+ }
89
+ function asBoolean(value) {
90
+ if (typeof value === "boolean") {
91
+ return value;
92
+ }
93
+ if (typeof value === "string") {
94
+ const normalized = value.trim().toLowerCase();
95
+ if (["true", "1", "yes", "on"].includes(normalized)) {
96
+ return true;
97
+ }
98
+ if (["false", "0", "no", "off"].includes(normalized)) {
99
+ return false;
100
+ }
101
+ }
102
+ return null;
103
+ }
104
+ function isObserveProvider(value) {
105
+ return typeof value === "string" && OBSERVE_PROVIDERS.includes(value);
106
+ }
107
+ function isObserverCompressionProvider(value) {
108
+ return typeof value === "string" && OBSERVER_COMPRESSION_PROVIDERS.includes(value);
109
+ }
110
+ function isTheme(value) {
111
+ return typeof value === "string" && THEMES.includes(value);
112
+ }
113
+ function isContextProfile(value) {
114
+ return typeof value === "string" && CONTEXT_PROFILES.includes(value);
115
+ }
116
+ function normalizeRouteTarget(target) {
117
+ const trimmed = target.trim().replace(/^\/+/, "").replace(/\/+$/, "");
118
+ if (!trimmed) {
119
+ throw new Error("Route target cannot be empty.");
120
+ }
121
+ const segments = trimmed.split("/").map((segment) => segment.trim()).filter(Boolean);
122
+ if (segments.length === 0) {
123
+ throw new Error("Route target cannot be empty.");
124
+ }
125
+ if (segments.some((segment) => segment === "." || segment === "..")) {
126
+ throw new Error(`Route target cannot contain relative path segments: ${target}`);
127
+ }
128
+ return segments.join("/");
129
+ }
130
+ function normalizeRouteRule(raw) {
131
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
132
+ return null;
133
+ }
134
+ const record = raw;
135
+ const pattern = typeof record.pattern === "string" ? record.pattern.trim() : "";
136
+ const target = typeof record.target === "string" ? record.target.trim() : "";
137
+ const priority = asPositiveInteger(record.priority);
138
+ if (!pattern || !target || priority === null) {
139
+ return null;
140
+ }
141
+ return {
142
+ pattern,
143
+ target: normalizeRouteTarget(target),
144
+ priority
145
+ };
146
+ }
147
+ function normalizeRoutes(value) {
148
+ if (!Array.isArray(value)) {
149
+ return [];
150
+ }
151
+ return value.map((entry) => normalizeRouteRule(entry)).filter((entry) => entry !== null).sort((left, right) => right.priority - left.priority || left.pattern.localeCompare(right.pattern));
152
+ }
153
+ function getNestedValue(source, dottedPath) {
154
+ const parts = dottedPath.split(".");
155
+ let cursor = source;
156
+ for (const part of parts) {
157
+ if (!cursor || typeof cursor !== "object" || Array.isArray(cursor)) {
158
+ return void 0;
159
+ }
160
+ cursor = cursor[part];
161
+ }
162
+ return cursor;
163
+ }
164
+ function setNestedValue(source, dottedPath, value) {
165
+ const parts = dottedPath.split(".");
166
+ let cursor = source;
167
+ for (let index = 0; index < parts.length - 1; index += 1) {
168
+ const part = parts[index];
169
+ const current = cursor[part];
170
+ if (!current || typeof current !== "object" || Array.isArray(current)) {
171
+ cursor[part] = {};
172
+ }
173
+ cursor = cursor[part];
174
+ }
175
+ cursor[parts[parts.length - 1]] = value;
176
+ }
177
+ function parseRegexLiteral(pattern) {
178
+ const match = pattern.match(/^\/(.+)\/([a-z]*)$/i);
179
+ if (!match) {
180
+ return null;
181
+ }
182
+ try {
183
+ return new RegExp(match[1], match[2]);
184
+ } catch (error) {
185
+ throw new Error(`Invalid route regex pattern "${pattern}": ${error instanceof Error ? error.message : "parse error"}`);
186
+ }
187
+ }
188
+ function withDefaults(vaultPath, config) {
189
+ const resolvedPath = path.resolve(vaultPath);
190
+ const defaults = {
191
+ name: path.basename(resolvedPath),
192
+ categories: [...DEFAULT_CATEGORIES],
193
+ theme: DEFAULT_THEME,
194
+ observe: {
195
+ model: DEFAULT_OBSERVE_MODEL,
196
+ provider: DEFAULT_OBSERVE_PROVIDER
197
+ },
198
+ observer: {
199
+ compression: {}
200
+ },
201
+ context: {
202
+ maxResults: DEFAULT_CONTEXT_MAX_RESULTS,
203
+ defaultProfile: DEFAULT_CONTEXT_PROFILE
204
+ },
205
+ graph: {
206
+ maxHops: DEFAULT_GRAPH_MAX_HOPS
207
+ },
208
+ inject: {
209
+ maxResults: DEFAULT_INJECT_MAX_RESULTS,
210
+ useLlm: DEFAULT_INJECT_USE_LLM,
211
+ scope: [...DEFAULT_INJECT_SCOPE]
212
+ },
213
+ routes: []
214
+ };
215
+ const observeRecord = config.observe && typeof config.observe === "object" && !Array.isArray(config.observe) ? config.observe : {};
216
+ const contextRecord = config.context && typeof config.context === "object" && !Array.isArray(config.context) ? config.context : {};
217
+ const observerRecord = config.observer && typeof config.observer === "object" && !Array.isArray(config.observer) ? config.observer : {};
218
+ const compressionRecord = observerRecord.compression && typeof observerRecord.compression === "object" && !Array.isArray(observerRecord.compression) ? observerRecord.compression : {};
219
+ const graphRecord = config.graph && typeof config.graph === "object" && !Array.isArray(config.graph) ? config.graph : {};
220
+ const compressionProvider = isObserverCompressionProvider(compressionRecord.provider) ? compressionRecord.provider : void 0;
221
+ const compressionModel = typeof compressionRecord.model === "string" && compressionRecord.model.trim() ? compressionRecord.model.trim() : void 0;
222
+ const compressionBaseUrl = typeof compressionRecord.baseUrl === "string" && compressionRecord.baseUrl.trim() ? compressionRecord.baseUrl.trim() : void 0;
223
+ const compressionApiKey = typeof compressionRecord.apiKey === "string" && compressionRecord.apiKey.trim() ? compressionRecord.apiKey.trim() : void 0;
224
+ const normalizedCompression = {};
225
+ if (compressionProvider) {
226
+ normalizedCompression.provider = compressionProvider;
227
+ }
228
+ if (compressionModel) {
229
+ normalizedCompression.model = compressionModel;
230
+ }
231
+ if (compressionBaseUrl) {
232
+ normalizedCompression.baseUrl = compressionBaseUrl;
233
+ }
234
+ if (compressionApiKey) {
235
+ normalizedCompression.apiKey = compressionApiKey;
236
+ }
237
+ const injectRecord = config.inject && typeof config.inject === "object" && !Array.isArray(config.inject) ? config.inject : {};
238
+ return {
239
+ ...config,
240
+ name: typeof config.name === "string" && config.name.trim() ? config.name.trim() : defaults.name,
241
+ categories: asStringArray(config.categories) ?? defaults.categories,
242
+ theme: isTheme(config.theme) ? config.theme : defaults.theme,
243
+ observe: {
244
+ ...observeRecord,
245
+ model: typeof observeRecord.model === "string" && observeRecord.model.trim() ? observeRecord.model.trim() : defaults.observe.model,
246
+ provider: isObserveProvider(observeRecord.provider) ? observeRecord.provider : defaults.observe.provider
247
+ },
248
+ observer: {
249
+ ...observerRecord,
250
+ compression: normalizedCompression
251
+ },
252
+ context: {
253
+ ...contextRecord,
254
+ maxResults: asPositiveInteger(contextRecord.maxResults) ?? defaults.context.maxResults,
255
+ defaultProfile: isContextProfile(contextRecord.defaultProfile) ? contextRecord.defaultProfile : defaults.context.defaultProfile
256
+ },
257
+ graph: {
258
+ ...graphRecord,
259
+ maxHops: asPositiveInteger(graphRecord.maxHops) ?? defaults.graph.maxHops
260
+ },
261
+ inject: {
262
+ ...injectRecord,
263
+ maxResults: asPositiveInteger(injectRecord.maxResults) ?? defaults.inject.maxResults,
264
+ useLlm: asBoolean(injectRecord.useLlm) ?? defaults.inject.useLlm,
265
+ scope: asStringArray(injectRecord.scope) ?? (typeof injectRecord.scope === "string" ? injectRecord.scope.split(",").map((entry) => entry.trim()).filter(Boolean) : null) ?? [...defaults.inject.scope]
266
+ },
267
+ routes: normalizeRoutes(config.routes)
268
+ };
269
+ }
270
+ function coerceManagedValue(key, value) {
271
+ if (key === "name") {
272
+ if (typeof value !== "string" || !value.trim()) {
273
+ throw new Error('Config key "name" must be a non-empty string.');
274
+ }
275
+ return value.trim();
276
+ }
277
+ if (key === "categories") {
278
+ if (Array.isArray(value)) {
279
+ const normalized = value.map((entry) => typeof entry === "string" ? entry.trim() : "").filter(Boolean);
280
+ if (normalized.length === 0) {
281
+ throw new Error('Config key "categories" must include at least one category.');
282
+ }
283
+ return normalized;
284
+ }
285
+ if (typeof value !== "string") {
286
+ throw new Error('Config key "categories" must be a comma-separated string.');
287
+ }
288
+ const categories = value.split(",").map((entry) => entry.trim()).filter(Boolean);
289
+ if (categories.length === 0) {
290
+ throw new Error('Config key "categories" must include at least one category.');
291
+ }
292
+ return categories;
293
+ }
294
+ if (key === "theme") {
295
+ if (!isTheme(value)) {
296
+ throw new Error(`Config key "theme" must be one of: ${THEMES.join(", ")}`);
297
+ }
298
+ return value;
299
+ }
300
+ if (key === "observe.provider") {
301
+ if (!isObserveProvider(value)) {
302
+ throw new Error(`Config key "observe.provider" must be one of: ${OBSERVE_PROVIDERS.join(", ")}`);
303
+ }
304
+ return value;
305
+ }
306
+ if (key === "observe.model") {
307
+ if (typeof value !== "string" || !value.trim()) {
308
+ throw new Error('Config key "observe.model" must be a non-empty string.');
309
+ }
310
+ return value.trim();
311
+ }
312
+ if (key === "observer.compression.provider") {
313
+ if (!isObserverCompressionProvider(value)) {
314
+ throw new Error(
315
+ `Config key "observer.compression.provider" must be one of: ${OBSERVER_COMPRESSION_PROVIDERS.join(", ")}`
316
+ );
317
+ }
318
+ return value;
319
+ }
320
+ if (key === "observer.compression.model") {
321
+ if (typeof value !== "string" || !value.trim()) {
322
+ throw new Error('Config key "observer.compression.model" must be a non-empty string.');
323
+ }
324
+ return value.trim();
325
+ }
326
+ if (key === "observer.compression.baseUrl") {
327
+ if (typeof value !== "string" || !value.trim()) {
328
+ throw new Error('Config key "observer.compression.baseUrl" must be a non-empty string.');
329
+ }
330
+ return value.trim();
331
+ }
332
+ if (key === "observer.compression.apiKey") {
333
+ if (typeof value !== "string") {
334
+ throw new Error('Config key "observer.compression.apiKey" must be a string.');
335
+ }
336
+ return value.trim();
337
+ }
338
+ if (key === "context.maxResults") {
339
+ const parsed = asPositiveInteger(value);
340
+ if (parsed === null) {
341
+ throw new Error('Config key "context.maxResults" must be a positive integer.');
342
+ }
343
+ return parsed;
344
+ }
345
+ if (key === "context.defaultProfile") {
346
+ if (!isContextProfile(value)) {
347
+ throw new Error(`Config key "context.defaultProfile" must be one of: ${CONTEXT_PROFILES.join(", ")}`);
348
+ }
349
+ return value;
350
+ }
351
+ if (key === "graph.maxHops") {
352
+ const parsed = asPositiveInteger(value);
353
+ if (parsed === null) {
354
+ throw new Error('Config key "graph.maxHops" must be a positive integer.');
355
+ }
356
+ return parsed;
357
+ }
358
+ if (key === "inject.maxResults") {
359
+ const parsed = asPositiveInteger(value);
360
+ if (parsed === null) {
361
+ throw new Error('Config key "inject.maxResults" must be a positive integer.');
362
+ }
363
+ return parsed;
364
+ }
365
+ if (key === "inject.useLlm") {
366
+ const parsed = asBoolean(value);
367
+ if (parsed === null) {
368
+ throw new Error('Config key "inject.useLlm" must be a boolean.');
369
+ }
370
+ return parsed;
371
+ }
372
+ if (key === "inject.scope") {
373
+ const normalized = Array.isArray(value) ? value.map((entry) => typeof entry === "string" ? entry.trim() : "").filter(Boolean) : typeof value === "string" ? value.split(",").map((entry) => entry.trim()).filter(Boolean) : [];
374
+ if (normalized.length === 0) {
375
+ throw new Error('Config key "inject.scope" must be a non-empty string list.');
376
+ }
377
+ return normalized;
378
+ }
379
+ throw new Error(`Unsupported config key: ${key}`);
380
+ }
381
+ function toComparablePattern(pattern) {
382
+ return pattern.trim().toLowerCase();
383
+ }
384
+ function listConfig(vaultPath) {
385
+ const config = readConfigDocument(vaultPath);
386
+ return withDefaults(vaultPath, config);
387
+ }
388
+ function getConfig(vaultPath) {
389
+ return listConfig(vaultPath);
390
+ }
391
+ function getConfigValue(vaultPath, key) {
392
+ if (!SUPPORTED_CONFIG_KEYS.includes(key)) {
393
+ throw new Error(`Unsupported config key: ${key}`);
394
+ }
395
+ const config = listConfig(vaultPath);
396
+ return getNestedValue(config, key);
397
+ }
398
+ function setConfigValue(vaultPath, key, value) {
399
+ if (!SUPPORTED_CONFIG_KEYS.includes(key)) {
400
+ throw new Error(`Unsupported config key: ${key}`);
401
+ }
402
+ const document = readConfigDocument(vaultPath);
403
+ const coerced = coerceManagedValue(key, value);
404
+ setNestedValue(document, key, coerced);
405
+ if (typeof document.lastUpdated === "string") {
406
+ document.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
407
+ }
408
+ writeConfigDocument(vaultPath, document);
409
+ return {
410
+ value: coerced,
411
+ config: withDefaults(vaultPath, document)
412
+ };
413
+ }
414
+ function resetConfig(vaultPath) {
415
+ const document = readConfigDocument(vaultPath);
416
+ const defaultName = path.basename(path.resolve(vaultPath));
417
+ document.name = defaultName;
418
+ document.categories = [...DEFAULT_CATEGORIES];
419
+ document.theme = DEFAULT_THEME;
420
+ document.observe = {
421
+ model: DEFAULT_OBSERVE_MODEL,
422
+ provider: DEFAULT_OBSERVE_PROVIDER
423
+ };
424
+ const observerRecord = document.observer && typeof document.observer === "object" && !Array.isArray(document.observer) ? document.observer : {};
425
+ document.observer = {
426
+ ...observerRecord,
427
+ compression: {}
428
+ };
429
+ document.context = {
430
+ maxResults: DEFAULT_CONTEXT_MAX_RESULTS,
431
+ defaultProfile: DEFAULT_CONTEXT_PROFILE
432
+ };
433
+ document.graph = {
434
+ maxHops: DEFAULT_GRAPH_MAX_HOPS
435
+ };
436
+ document.inject = {
437
+ maxResults: DEFAULT_INJECT_MAX_RESULTS,
438
+ useLlm: DEFAULT_INJECT_USE_LLM,
439
+ scope: [...DEFAULT_INJECT_SCOPE]
440
+ };
441
+ document.routes = [];
442
+ if (typeof document.lastUpdated === "string") {
443
+ document.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
444
+ }
445
+ writeConfigDocument(vaultPath, document);
446
+ return withDefaults(vaultPath, document);
447
+ }
448
+ function listRouteRules(vaultPath) {
449
+ const config = listConfig(vaultPath);
450
+ return normalizeRoutes(config.routes);
451
+ }
452
+ function addRouteRule(vaultPath, pattern, target) {
453
+ const normalizedPattern = pattern.trim();
454
+ if (!normalizedPattern) {
455
+ throw new Error("Route pattern cannot be empty.");
456
+ }
457
+ const normalizedTarget = normalizeRouteTarget(target);
458
+ const document = readConfigDocument(vaultPath);
459
+ const existingRoutes = normalizeRoutes(document.routes);
460
+ const duplicate = existingRoutes.find(
461
+ (rule) => toComparablePattern(rule.pattern) === toComparablePattern(normalizedPattern)
462
+ );
463
+ if (duplicate) {
464
+ throw new Error(`Route pattern already exists: ${pattern}`);
465
+ }
466
+ const maxPriority = existingRoutes.reduce((max, rule) => Math.max(max, rule.priority), 0);
467
+ const nextRule = {
468
+ pattern: normalizedPattern,
469
+ target: normalizedTarget,
470
+ priority: maxPriority + 1
471
+ };
472
+ document.routes = [...existingRoutes, nextRule];
473
+ if (typeof document.lastUpdated === "string") {
474
+ document.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
475
+ }
476
+ writeConfigDocument(vaultPath, document);
477
+ return nextRule;
478
+ }
479
+ function removeRouteRule(vaultPath, pattern) {
480
+ const normalizedPattern = toComparablePattern(pattern);
481
+ const document = readConfigDocument(vaultPath);
482
+ const existingRoutes = normalizeRoutes(document.routes);
483
+ const nextRoutes = existingRoutes.filter(
484
+ (rule) => toComparablePattern(rule.pattern) !== normalizedPattern
485
+ );
486
+ if (nextRoutes.length === existingRoutes.length) {
487
+ return false;
488
+ }
489
+ document.routes = nextRoutes;
490
+ if (typeof document.lastUpdated === "string") {
491
+ document.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
492
+ }
493
+ writeConfigDocument(vaultPath, document);
494
+ return true;
495
+ }
496
+ function matchRouteRule(text, routes) {
497
+ for (const route of routes) {
498
+ const regex = parseRegexLiteral(route.pattern);
499
+ if (regex) {
500
+ if (regex.test(text)) {
501
+ return route;
502
+ }
503
+ continue;
504
+ }
505
+ if (text.toLowerCase().includes(route.pattern.toLowerCase())) {
506
+ return route;
507
+ }
508
+ }
509
+ return null;
510
+ }
511
+ function testRouteRule(vaultPath, text) {
512
+ const routes = listRouteRules(vaultPath);
513
+ return matchRouteRule(text, routes);
514
+ }
515
+
516
+ export {
517
+ SUPPORTED_CONFIG_KEYS,
518
+ listConfig,
519
+ getConfig,
520
+ getConfigValue,
521
+ setConfigValue,
522
+ resetConfig,
523
+ listRouteRules,
524
+ addRouteRule,
525
+ removeRouteRule,
526
+ matchRouteRule,
527
+ testRouteRule
528
+ };
@@ -2,7 +2,7 @@ import {
2
2
  listTasks,
3
3
  readTask,
4
4
  updateTask
5
- } from "./chunk-6QLRSPLZ.js";
5
+ } from "./chunk-IOALNTAN.js";
6
6
 
7
7
  // src/commands/kanban.ts
8
8
  import * as fs from "fs";
@@ -0,0 +1,122 @@
1
+ // src/lib/llm-provider.ts
2
+ var DEFAULT_MODELS = {
3
+ anthropic: "claude-3-5-haiku-latest",
4
+ openai: "gpt-4o-mini",
5
+ gemini: "gemini-2.0-flash"
6
+ };
7
+ function resolveLlmProvider() {
8
+ if (process.env.CLAWVAULT_NO_LLM) {
9
+ return null;
10
+ }
11
+ if (process.env.ANTHROPIC_API_KEY) {
12
+ return "anthropic";
13
+ }
14
+ if (process.env.OPENAI_API_KEY) {
15
+ return "openai";
16
+ }
17
+ if (process.env.GEMINI_API_KEY) {
18
+ return "gemini";
19
+ }
20
+ return null;
21
+ }
22
+ async function requestLlmCompletion(options) {
23
+ const provider = options.provider ?? resolveLlmProvider();
24
+ if (!provider) {
25
+ return "";
26
+ }
27
+ if (provider === "anthropic") {
28
+ return callAnthropic(options, provider);
29
+ }
30
+ if (provider === "gemini") {
31
+ return callGemini(options, provider);
32
+ }
33
+ return callOpenAI(options, provider);
34
+ }
35
+ async function callAnthropic(options, provider) {
36
+ const apiKey = process.env.ANTHROPIC_API_KEY;
37
+ if (!apiKey) {
38
+ return "";
39
+ }
40
+ const fetchImpl = options.fetchImpl ?? fetch;
41
+ const response = await fetchImpl("https://api.anthropic.com/v1/messages", {
42
+ method: "POST",
43
+ headers: {
44
+ "content-type": "application/json",
45
+ "x-api-key": apiKey,
46
+ "anthropic-version": "2023-06-01"
47
+ },
48
+ body: JSON.stringify({
49
+ model: options.model ?? DEFAULT_MODELS[provider],
50
+ temperature: options.temperature ?? 0.1,
51
+ max_tokens: options.maxTokens ?? 1200,
52
+ messages: [{ role: "user", content: options.prompt }]
53
+ })
54
+ });
55
+ if (!response.ok) {
56
+ throw new Error(`Anthropic request failed (${response.status})`);
57
+ }
58
+ const payload = await response.json();
59
+ return payload.content?.filter((entry) => entry.type === "text" && entry.text).map((entry) => entry.text).join("\n").trim() ?? "";
60
+ }
61
+ async function callOpenAI(options, provider) {
62
+ const apiKey = process.env.OPENAI_API_KEY;
63
+ if (!apiKey) {
64
+ return "";
65
+ }
66
+ const fetchImpl = options.fetchImpl ?? fetch;
67
+ const messages = [];
68
+ if (options.systemPrompt?.trim()) {
69
+ messages.push({ role: "system", content: options.systemPrompt.trim() });
70
+ }
71
+ messages.push({ role: "user", content: options.prompt });
72
+ const response = await fetchImpl("https://api.openai.com/v1/chat/completions", {
73
+ method: "POST",
74
+ headers: {
75
+ "content-type": "application/json",
76
+ authorization: `Bearer ${apiKey}`
77
+ },
78
+ body: JSON.stringify({
79
+ model: options.model ?? DEFAULT_MODELS[provider],
80
+ temperature: options.temperature ?? 0.1,
81
+ max_tokens: options.maxTokens ?? 1200,
82
+ messages
83
+ })
84
+ });
85
+ if (!response.ok) {
86
+ throw new Error(`OpenAI request failed (${response.status})`);
87
+ }
88
+ const payload = await response.json();
89
+ return payload.choices?.[0]?.message?.content?.trim() ?? "";
90
+ }
91
+ async function callGemini(options, provider) {
92
+ const apiKey = process.env.GEMINI_API_KEY;
93
+ if (!apiKey) {
94
+ return "";
95
+ }
96
+ const fetchImpl = options.fetchImpl ?? fetch;
97
+ const model = options.model ?? DEFAULT_MODELS[provider];
98
+ const response = await fetchImpl(
99
+ `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`,
100
+ {
101
+ method: "POST",
102
+ headers: { "content-type": "application/json" },
103
+ body: JSON.stringify({
104
+ contents: [{ parts: [{ text: options.prompt }] }],
105
+ generationConfig: {
106
+ temperature: options.temperature ?? 0.1,
107
+ maxOutputTokens: options.maxTokens ?? 1200
108
+ }
109
+ })
110
+ }
111
+ );
112
+ if (!response.ok) {
113
+ throw new Error(`Gemini request failed (${response.status})`);
114
+ }
115
+ const payload = await response.json();
116
+ return payload.candidates?.[0]?.content?.parts?.[0]?.text?.trim() ?? "";
117
+ }
118
+
119
+ export {
120
+ resolveLlmProvider,
121
+ requestLlmCompletion
122
+ };