opencode-engram 0.1.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.
@@ -0,0 +1,963 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ import type { createOpencodeClient } from "@opencode-ai/sdk";
6
+
7
+ export type DebugModeConfig = {
8
+ enable?: boolean;
9
+ log_tool_calls?: boolean;
10
+ };
11
+
12
+ export type BrowseConfig = {
13
+ message_limit?: number;
14
+ user_preview_length?: number;
15
+ assistant_preview_length?: number;
16
+ };
17
+
18
+ export type OverviewConfig = {
19
+ user_preview_length?: number;
20
+ assistant_preview_length?: number;
21
+ };
22
+
23
+ export type SearchConfig = {
24
+ max_hits_per_message?: number;
25
+ max_snippets_per_hit?: number;
26
+ snippet_length?: number;
27
+ message_limit?: number;
28
+ };
29
+
30
+ export type PullConfig = {
31
+ text_length?: number;
32
+ reasoning_length?: number;
33
+ tool_output_length?: number;
34
+ tool_input_length?: number;
35
+ };
36
+
37
+ export type UpstreamHistoryConfig = {
38
+ enable?: boolean;
39
+ disable_for_agents?: string[];
40
+ };
41
+
42
+ export type ChartingConfig = {
43
+ enable?: boolean;
44
+ recent_turns?: number;
45
+ recent_messages?: number;
46
+ };
47
+
48
+ type RawDebugModeConfig = DebugModeConfig | undefined;
49
+ type RawUpstreamHistoryConfig = UpstreamHistoryConfig | undefined;
50
+ type RawChartingConfig = ChartingConfig | undefined;
51
+ type RawBrowseConfig = BrowseConfig | undefined;
52
+ type RawOverviewConfig = OverviewConfig | undefined;
53
+ type RawSearchConfig = SearchConfig | undefined;
54
+ type RawPullConfig = PullConfig | undefined;
55
+
56
+ type RawConfig = {
57
+ debug_mode?: RawDebugModeConfig;
58
+ upstream_history?: RawUpstreamHistoryConfig;
59
+ context_charting?: RawChartingConfig;
60
+ browse_turns?: RawOverviewConfig;
61
+ browse_messages?: RawBrowseConfig;
62
+ search?: RawSearchConfig;
63
+ pull_message?: RawPullConfig;
64
+ show_tool_input?: string[];
65
+ show_tool_output?: string[];
66
+ };
67
+
68
+ type ConfigIssueReporter = (message: string) => void;
69
+
70
+ type SdkClient = ReturnType<typeof createOpencodeClient>;
71
+
72
+ export type ResolvedDebugModeConfig = {
73
+ enable: boolean;
74
+ log_tool_calls: boolean;
75
+ };
76
+
77
+ export type ResolvedBrowseConfig = {
78
+ message_limit: number;
79
+ user_preview_length: number;
80
+ assistant_preview_length: number;
81
+ };
82
+
83
+ export type ResolvedOverviewConfig = {
84
+ user_preview_length: number;
85
+ assistant_preview_length: number;
86
+ };
87
+
88
+ export type ResolvedSearchConfig = {
89
+ max_hits_per_message: number;
90
+ max_snippets_per_hit: number;
91
+ snippet_length: number;
92
+ message_limit: number;
93
+ };
94
+
95
+ export type ResolvedPullConfig = {
96
+ text_length: number;
97
+ reasoning_length: number;
98
+ tool_output_length: number;
99
+ tool_input_length: number;
100
+ };
101
+
102
+ export type ResolvedUpstreamHistoryConfig = {
103
+ enable: boolean;
104
+ disable_for_agents: string[];
105
+ };
106
+
107
+ export type ResolvedChartingConfig = {
108
+ enable: boolean;
109
+ recent_turns: number;
110
+ recent_messages: number;
111
+ };
112
+
113
+ export type EngramConfig = {
114
+ debug_mode: ResolvedDebugModeConfig;
115
+ upstream_history: ResolvedUpstreamHistoryConfig;
116
+ context_charting: ResolvedChartingConfig;
117
+ browse_turns: ResolvedOverviewConfig;
118
+ browse_messages: ResolvedBrowseConfig;
119
+ pull_message: ResolvedPullConfig;
120
+ search: ResolvedSearchConfig;
121
+ show_tool_input: string[];
122
+ show_tool_output: string[];
123
+ };
124
+
125
+ const configNames = ["opencode-engram.json", "opencode-engram.jsonc"];
126
+ const SHOW_TOOL_INPUT_BUILTINS = [
127
+ "bash",
128
+ "grep",
129
+ "glob",
130
+ "task",
131
+ "websearch",
132
+ "webfetch",
133
+ "question",
134
+ "skill",
135
+ ] as const;
136
+ const SHOW_TOOL_OUTPUT_BUILTINS = [
137
+ "question",
138
+ "bash",
139
+ "task",
140
+ "apply_patch",
141
+ "grep",
142
+ "todowrite",
143
+ "edit",
144
+ "glob",
145
+ ] as const;
146
+ const noopConfigIssueReporter: ConfigIssueReporter = () => undefined;
147
+
148
+ const supportedTopLevelKeys = [
149
+ "debug_mode",
150
+ "upstream_history",
151
+ "context_charting",
152
+ "browse_turns",
153
+ "browse_messages",
154
+ "pull_message",
155
+ "search",
156
+ "show_tool_input",
157
+ "show_tool_output",
158
+ ];
159
+ const supportedDebugModeKeys = ["enable", "log_tool_calls"];
160
+ const supportedUpstreamHistoryKeys = ["enable", "disable_for_agents"];
161
+ const supportedChartingKeys = ["enable", "recent_turns", "recent_messages"];
162
+ const supportedBrowseKeys = ["message_limit", "user_preview_length", "assistant_preview_length"];
163
+ const supportedOverviewKeys = ["user_preview_length", "assistant_preview_length"];
164
+ const supportedSearchKeys = ["max_hits_per_message", "max_snippets_per_hit", "snippet_length", "message_limit"];
165
+ const supportedPullKeys = [
166
+ "text_length",
167
+ "reasoning_length",
168
+ "tool_output_length",
169
+ "tool_input_length",
170
+ ];
171
+
172
+ function errorMessage(error: unknown) {
173
+ return error instanceof Error ? error.message : String(error);
174
+ }
175
+
176
+ function resolveWithFallback<T>(
177
+ fallback: T,
178
+ report: ConfigIssueReporter,
179
+ resolve: () => T,
180
+ ) {
181
+ try {
182
+ return resolve();
183
+ } catch (error) {
184
+ report(errorMessage(error));
185
+ return fallback;
186
+ }
187
+ }
188
+
189
+ function toConfigObject(
190
+ value: unknown,
191
+ label: string,
192
+ report: ConfigIssueReporter,
193
+ ): Record<string, unknown> | undefined {
194
+ if (value === undefined) {
195
+ return undefined;
196
+ }
197
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
198
+ report(`${label} must be a config object`);
199
+ return undefined;
200
+ }
201
+ return value as Record<string, unknown>;
202
+ }
203
+
204
+ function reportUnsupportedKeys(
205
+ value: Record<string, unknown>,
206
+ label: string,
207
+ supportedKeys: readonly string[],
208
+ report: ConfigIssueReporter,
209
+ ) {
210
+ for (const key of Object.keys(value)) {
211
+ if (!supportedKeys.includes(key)) {
212
+ report(`${label}.${key} is not supported`);
213
+ }
214
+ }
215
+ }
216
+
217
+ function reportUnsupportedTopLevelKeys(
218
+ patch: RawConfig,
219
+ source: string,
220
+ report: ConfigIssueReporter,
221
+ ) {
222
+ for (const key of Object.keys(patch)) {
223
+ if (!supportedTopLevelKeys.includes(key)) {
224
+ report(`${source}: '${key}' is not supported`);
225
+ }
226
+ }
227
+ }
228
+
229
+ function decodeConfigText(bytes: Uint8Array) {
230
+ if (
231
+ bytes.length >= 3 &&
232
+ bytes[0] === 0xef &&
233
+ bytes[1] === 0xbb &&
234
+ bytes[2] === 0xbf
235
+ ) {
236
+ return Buffer.from(bytes.subarray(3)).toString("utf8");
237
+ }
238
+
239
+ if (bytes.length >= 2 && bytes[0] === 0xff && bytes[1] === 0xfe) {
240
+ return Buffer.from(bytes.subarray(2)).toString("utf16le");
241
+ }
242
+
243
+ if (bytes.length >= 2 && bytes[0] === 0xfe && bytes[1] === 0xff) {
244
+ const body = bytes.subarray(2);
245
+ const length = body.length - (body.length % 2);
246
+ const swapped = Buffer.alloc(length);
247
+
248
+ for (let i = 0; i < length; i += 2) {
249
+ swapped[i] = body[i + 1];
250
+ swapped[i + 1] = body[i];
251
+ }
252
+
253
+ return swapped.toString("utf16le");
254
+ }
255
+
256
+ return Buffer.from(bytes).toString("utf8");
257
+ }
258
+
259
+ function localGlobalConfigRoots() {
260
+ const roots = [join(process.env.XDG_CONFIG_HOME?.trim() || join(homedir(), ".config"), "opencode")];
261
+
262
+ if (process.platform === "win32") {
263
+ const appData = process.env.APPDATA;
264
+ const localAppData = process.env.LOCALAPPDATA;
265
+
266
+ if (appData) roots.push(join(appData, "opencode"));
267
+ if (localAppData && localAppData !== appData) {
268
+ roots.push(join(localAppData, "opencode"));
269
+ }
270
+ }
271
+
272
+ return roots;
273
+ }
274
+
275
+ async function sdkGlobalConfigRoot(client?: SdkClient) {
276
+ if (!client) {
277
+ return;
278
+ }
279
+
280
+ try {
281
+ const result = await client.path.get({ throwOnError: false });
282
+ const config = result.data?.config?.trim();
283
+ if (config) {
284
+ return config;
285
+ }
286
+ } catch {
287
+ return;
288
+ }
289
+ }
290
+
291
+ async function globalConfigRoots(client?: SdkClient) {
292
+ const roots = [] as string[];
293
+ const sdkRoot = await sdkGlobalConfigRoot(client);
294
+
295
+ if (sdkRoot) {
296
+ roots.push(sdkRoot);
297
+ }
298
+
299
+ roots.push(...localGlobalConfigRoots());
300
+
301
+ return [...new Set(roots)];
302
+ }
303
+
304
+ function defaultDebugModeConfig(): ResolvedDebugModeConfig {
305
+ return {
306
+ enable: false,
307
+ log_tool_calls: true,
308
+ };
309
+ }
310
+
311
+ function defaultShowToolInputTools(): string[] {
312
+ return [...SHOW_TOOL_INPUT_BUILTINS];
313
+ }
314
+
315
+ function defaultShowToolOutputTools(): string[] {
316
+ return [...SHOW_TOOL_OUTPUT_BUILTINS];
317
+ }
318
+
319
+ function defaults(): EngramConfig {
320
+ return {
321
+ debug_mode: defaultDebugModeConfig(),
322
+ upstream_history: {
323
+ enable: true,
324
+ disable_for_agents: [],
325
+ },
326
+ context_charting: {
327
+ enable: true,
328
+ recent_turns: 10,
329
+ recent_messages: 5,
330
+ },
331
+ browse_turns: {
332
+ user_preview_length: 280,
333
+ assistant_preview_length: 140,
334
+ },
335
+ browse_messages: {
336
+ message_limit: 100,
337
+ user_preview_length: 280,
338
+ assistant_preview_length: 140,
339
+ },
340
+ pull_message: {
341
+ text_length: 400,
342
+ reasoning_length: 200,
343
+ tool_output_length: 400,
344
+ tool_input_length: 140,
345
+ },
346
+ search: {
347
+ max_hits_per_message: 5,
348
+ max_snippets_per_hit: 5,
349
+ snippet_length: 140,
350
+ message_limit: 5,
351
+ },
352
+ show_tool_input: defaultShowToolInputTools(),
353
+ show_tool_output: defaultShowToolOutputTools(),
354
+ };
355
+ }
356
+
357
+ function stripJsonComments(input: string) {
358
+ let out = "";
359
+ let inString = false;
360
+ let escaped = false;
361
+
362
+ for (let i = 0; i < input.length; i += 1) {
363
+ const char = input[i];
364
+ const next = input[i + 1];
365
+
366
+ if (inString) {
367
+ out += char;
368
+ if (escaped) {
369
+ escaped = false;
370
+ } else if (char === "\\") {
371
+ escaped = true;
372
+ } else if (char === '"') {
373
+ inString = false;
374
+ }
375
+ continue;
376
+ }
377
+
378
+ if (char === '"') {
379
+ inString = true;
380
+ out += char;
381
+ continue;
382
+ }
383
+
384
+ if (char === "/" && next === "/") {
385
+ i += 2;
386
+ while (i < input.length && input[i] !== "\n") i += 1;
387
+ if (i < input.length) out += "\n";
388
+ continue;
389
+ }
390
+
391
+ if (char === "/" && next === "*") {
392
+ i += 2;
393
+ while (i < input.length - 1) {
394
+ if (input[i] === "*" && input[i + 1] === "/") {
395
+ i += 1;
396
+ break;
397
+ }
398
+ i += 1;
399
+ }
400
+ continue;
401
+ }
402
+
403
+ out += char;
404
+ }
405
+
406
+ return out;
407
+ }
408
+
409
+ function stripTrailingCommas(input: string) {
410
+ let out = "";
411
+ let inString = false;
412
+ let escaped = false;
413
+
414
+ for (let i = 0; i < input.length; i += 1) {
415
+ const char = input[i];
416
+
417
+ if (inString) {
418
+ out += char;
419
+ if (escaped) {
420
+ escaped = false;
421
+ } else if (char === "\\") {
422
+ escaped = true;
423
+ } else if (char === '"') {
424
+ inString = false;
425
+ }
426
+ continue;
427
+ }
428
+
429
+ if (char === '"') {
430
+ inString = true;
431
+ out += char;
432
+ continue;
433
+ }
434
+
435
+ if (char === ",") {
436
+ let j = i + 1;
437
+ while (j < input.length && /\s/.test(input[j])) j += 1;
438
+ if (input[j] === "}" || input[j] === "]") continue;
439
+ }
440
+
441
+ out += char;
442
+ }
443
+
444
+ return out;
445
+ }
446
+
447
+ function parseJsonc(text: string, filePath: string) {
448
+ const normalized = stripTrailingCommas(stripJsonComments(text));
449
+ try {
450
+ return JSON.parse(normalized) as RawConfig;
451
+ } catch (error) {
452
+ const message = error instanceof Error ? error.message : String(error);
453
+ throw new Error(`Invalid engram config at ${filePath}: ${message}`);
454
+ }
455
+ }
456
+
457
+ function validateBoolean(value: unknown, label: string, fallback: boolean) {
458
+ if (value === undefined) return fallback;
459
+ if (typeof value !== "boolean") {
460
+ throw new Error(`${label} must be a boolean`);
461
+ }
462
+ return value;
463
+ }
464
+
465
+ function validatePositiveInt(value: unknown, label: string, fallback: number) {
466
+ if (value === undefined) return fallback;
467
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
468
+ throw new Error(`${label} must be a positive integer`);
469
+ }
470
+ return value;
471
+ }
472
+
473
+ function validateNonNegativeInt(value: unknown, label: string, fallback: number) {
474
+ if (value === undefined) return fallback;
475
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
476
+ throw new Error(`${label} must be a non-negative integer`);
477
+ }
478
+ return value;
479
+ }
480
+
481
+ function validatePreviewLength(value: unknown, label: string, fallback: number) {
482
+ if (value === undefined) return fallback;
483
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
484
+ throw new Error(`${label} must be a positive integer`);
485
+ }
486
+ if (value > 10000) {
487
+ throw new Error(`${label} must not exceed 10000`);
488
+ }
489
+ return value;
490
+ }
491
+
492
+ function validateStringArray(value: unknown, label: string, fallback: string[]) {
493
+ if (value === undefined) return fallback;
494
+ if (!Array.isArray(value)) {
495
+ throw new Error(`${label} must be an array of strings`);
496
+ }
497
+
498
+ const resolved: string[] = [];
499
+ const seen = new Set<string>();
500
+
501
+ for (let i = 0; i < value.length; i += 1) {
502
+ const item = value[i];
503
+ if (typeof item !== "string") {
504
+ throw new Error(`${label}[${i}] must be a string`);
505
+ }
506
+ const normalized = item.trim();
507
+ if (!normalized) {
508
+ throw new Error(`${label}[${i}] must not be empty`);
509
+ }
510
+ if (!seen.has(normalized)) {
511
+ seen.add(normalized);
512
+ resolved.push(normalized);
513
+ }
514
+ }
515
+
516
+ return resolved;
517
+ }
518
+
519
+ function validateAgentNameArray(value: unknown, label: string, fallback: string[]) {
520
+ if (value === undefined) return fallback;
521
+ if (!Array.isArray(value)) {
522
+ throw new Error(`${label} must be an array of strings`);
523
+ }
524
+
525
+ const resolved: string[] = [];
526
+
527
+ for (let i = 0; i < value.length; i += 1) {
528
+ const item = value[i];
529
+ if (typeof item !== "string") {
530
+ throw new Error(`${label}[${i}] must be a string`);
531
+ }
532
+ resolved.push(item);
533
+ }
534
+
535
+ return resolved;
536
+ }
537
+
538
+ function validateOptionalPathString(value: unknown, label: string, fallback: string) {
539
+ if (value === undefined) {
540
+ return fallback;
541
+ }
542
+ if (typeof value !== "string") {
543
+ throw new Error(`${label} must be a string`);
544
+ }
545
+ return value.trim();
546
+ }
547
+
548
+ function selectorPatternToRegex(pattern: string) {
549
+ const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*");
550
+ return new RegExp(`^${escaped}$`);
551
+ }
552
+
553
+ function matchesToolSelector(toolName: string, selector: string) {
554
+ if (selector === "*") {
555
+ return true;
556
+ }
557
+ return selectorPatternToRegex(selector).test(toolName);
558
+ }
559
+
560
+ function mergeToolVisibilitySelectors(
561
+ builtins: readonly string[],
562
+ selectors: readonly string[],
563
+ ): string[] {
564
+ const merged: string[] = [];
565
+ const seen = new Set<string>();
566
+
567
+ for (const rawSelector of [...builtins, ...selectors]) {
568
+ if (rawSelector === "!" || rawSelector === "!*") {
569
+ return ["!"];
570
+ }
571
+
572
+ const denied = rawSelector.startsWith("!");
573
+ const selector = denied ? rawSelector.slice(1) : rawSelector;
574
+
575
+ if (!selector) {
576
+ throw new Error("tool selector '!' must be used by itself");
577
+ }
578
+
579
+ if (!seen.has(rawSelector)) {
580
+ seen.add(rawSelector);
581
+ merged.push(rawSelector);
582
+ }
583
+ }
584
+
585
+ return merged;
586
+ }
587
+
588
+ export function resolveVisibleToolNames(
589
+ toolNames: Iterable<string>,
590
+ selectors: readonly string[],
591
+ ): string[] {
592
+ if (selectors.includes("!") || selectors.includes("!*")) {
593
+ return [];
594
+ }
595
+
596
+ let allowAll = false;
597
+ const allowPatterns: string[] = [];
598
+ const denyPatterns: string[] = [];
599
+
600
+ for (const rawSelector of selectors) {
601
+ const denied = rawSelector.startsWith("!");
602
+ const selector = denied ? rawSelector.slice(1) : rawSelector;
603
+
604
+ if (!selector) {
605
+ throw new Error("tool selector '!' must be used by itself");
606
+ }
607
+
608
+ if (selector === "*") {
609
+ if (denied) {
610
+ return [];
611
+ }
612
+ allowAll = true;
613
+ continue;
614
+ }
615
+
616
+ if (denied) {
617
+ denyPatterns.push(selector);
618
+ continue;
619
+ }
620
+
621
+ allowPatterns.push(selector);
622
+ }
623
+
624
+ const resolved: string[] = [];
625
+ const seen = new Set<string>();
626
+
627
+ for (const toolName of toolNames) {
628
+ if (seen.has(toolName)) {
629
+ continue;
630
+ }
631
+ seen.add(toolName);
632
+
633
+ const allowed = allowAll || allowPatterns.some((pattern) => matchesToolSelector(toolName, pattern));
634
+ if (!allowed) {
635
+ continue;
636
+ }
637
+ if (denyPatterns.some((pattern) => matchesToolSelector(toolName, pattern))) {
638
+ continue;
639
+ }
640
+ resolved.push(toolName);
641
+ }
642
+
643
+ return resolved;
644
+ }
645
+
646
+ function mergeConfig(
647
+ base: EngramConfig,
648
+ rawPatch: unknown,
649
+ source: string,
650
+ report: ConfigIssueReporter,
651
+ ) {
652
+ if (typeof rawPatch !== "object" || rawPatch === null || Array.isArray(rawPatch)) {
653
+ report(`${source}: root config must be an object`);
654
+ return base;
655
+ }
656
+
657
+ const patch = rawPatch as RawConfig;
658
+ reportUnsupportedTopLevelKeys(patch, source, report);
659
+
660
+ const debugMode = toConfigObject(patch.debug_mode, `${source}: debug_mode`, report);
661
+ if (debugMode) {
662
+ reportUnsupportedKeys(debugMode, `${source}: debug_mode`, supportedDebugModeKeys, report);
663
+ }
664
+
665
+ const upstreamHistory = toConfigObject(
666
+ patch.upstream_history,
667
+ `${source}: upstream_history`,
668
+ report,
669
+ );
670
+ if (upstreamHistory) {
671
+ reportUnsupportedKeys(
672
+ upstreamHistory,
673
+ `${source}: upstream_history`,
674
+ supportedUpstreamHistoryKeys,
675
+ report,
676
+ );
677
+ }
678
+
679
+ const browseTurns = toConfigObject(patch.browse_turns, `${source}: browse_turns`, report);
680
+ if (browseTurns) {
681
+ reportUnsupportedKeys(browseTurns, `${source}: browse_turns`, supportedOverviewKeys, report);
682
+ }
683
+
684
+ const browseMessages = toConfigObject(
685
+ patch.browse_messages,
686
+ `${source}: browse_messages`,
687
+ report,
688
+ );
689
+ if (browseMessages) {
690
+ reportUnsupportedKeys(
691
+ browseMessages,
692
+ `${source}: browse_messages`,
693
+ supportedBrowseKeys,
694
+ report,
695
+ );
696
+ }
697
+
698
+ const contextCharting = toConfigObject(
699
+ patch.context_charting,
700
+ `${source}: context_charting`,
701
+ report,
702
+ );
703
+ if (contextCharting) {
704
+ reportUnsupportedKeys(
705
+ contextCharting,
706
+ `${source}: context_charting`,
707
+ supportedChartingKeys,
708
+ report,
709
+ );
710
+ }
711
+
712
+ const search = toConfigObject(patch.search, `${source}: search`, report);
713
+ if (search) {
714
+ reportUnsupportedKeys(search, `${source}: search`, supportedSearchKeys, report);
715
+ }
716
+
717
+ const pullMessage = toConfigObject(patch.pull_message, `${source}: pull_message`, report);
718
+ if (pullMessage) {
719
+ reportUnsupportedKeys(pullMessage, `${source}: pull_message`, supportedPullKeys, report);
720
+ }
721
+
722
+ const hasShowToolInput = Object.hasOwn(patch, "show_tool_input");
723
+ const hasShowToolOutput = Object.hasOwn(patch, "show_tool_output");
724
+
725
+ return {
726
+ debug_mode: {
727
+ enable: resolveWithFallback(base.debug_mode.enable, report, () =>
728
+ validateBoolean(
729
+ debugMode?.enable,
730
+ `${source}: debug_mode.enable`,
731
+ base.debug_mode.enable,
732
+ )
733
+ ),
734
+ log_tool_calls: resolveWithFallback(base.debug_mode.log_tool_calls, report, () =>
735
+ validateBoolean(
736
+ debugMode?.log_tool_calls,
737
+ `${source}: debug_mode.log_tool_calls`,
738
+ base.debug_mode.log_tool_calls,
739
+ )
740
+ ),
741
+ },
742
+ upstream_history: {
743
+ enable: resolveWithFallback(base.upstream_history.enable, report, () =>
744
+ validateBoolean(
745
+ upstreamHistory?.enable,
746
+ `${source}: upstream_history.enable`,
747
+ base.upstream_history.enable,
748
+ )
749
+ ),
750
+ disable_for_agents: resolveWithFallback(base.upstream_history.disable_for_agents, report, () =>
751
+ validateAgentNameArray(
752
+ upstreamHistory?.disable_for_agents,
753
+ `${source}: upstream_history.disable_for_agents`,
754
+ base.upstream_history.disable_for_agents,
755
+ )
756
+ ),
757
+ },
758
+ context_charting: {
759
+ enable: resolveWithFallback(base.context_charting.enable, report, () =>
760
+ validateBoolean(
761
+ contextCharting?.enable,
762
+ `${source}: context_charting.enable`,
763
+ base.context_charting.enable,
764
+ )
765
+ ),
766
+ recent_turns: resolveWithFallback(base.context_charting.recent_turns, report, () =>
767
+ validateNonNegativeInt(
768
+ contextCharting?.recent_turns,
769
+ `${source}: context_charting.recent_turns`,
770
+ base.context_charting.recent_turns,
771
+ )
772
+ ),
773
+ recent_messages: resolveWithFallback(base.context_charting.recent_messages, report, () =>
774
+ validateNonNegativeInt(
775
+ contextCharting?.recent_messages,
776
+ `${source}: context_charting.recent_messages`,
777
+ base.context_charting.recent_messages,
778
+ )
779
+ ),
780
+ },
781
+ browse_turns: {
782
+ user_preview_length: resolveWithFallback(
783
+ base.browse_turns.user_preview_length,
784
+ report,
785
+ () =>
786
+ validatePreviewLength(
787
+ browseTurns?.user_preview_length,
788
+ `${source}: browse_turns.user_preview_length`,
789
+ base.browse_turns.user_preview_length,
790
+ ),
791
+ ),
792
+ assistant_preview_length: resolveWithFallback(
793
+ base.browse_turns.assistant_preview_length,
794
+ report,
795
+ () =>
796
+ validatePreviewLength(
797
+ browseTurns?.assistant_preview_length,
798
+ `${source}: browse_turns.assistant_preview_length`,
799
+ base.browse_turns.assistant_preview_length,
800
+ ),
801
+ ),
802
+ },
803
+ browse_messages: {
804
+ message_limit: resolveWithFallback(base.browse_messages.message_limit, report, () =>
805
+ validatePositiveInt(
806
+ browseMessages?.message_limit,
807
+ `${source}: browse_messages.message_limit`,
808
+ base.browse_messages.message_limit,
809
+ )
810
+ ),
811
+ user_preview_length: resolveWithFallback(
812
+ base.browse_messages.user_preview_length,
813
+ report,
814
+ () =>
815
+ validatePreviewLength(
816
+ browseMessages?.user_preview_length,
817
+ `${source}: browse_messages.user_preview_length`,
818
+ base.browse_messages.user_preview_length,
819
+ ),
820
+ ),
821
+ assistant_preview_length: resolveWithFallback(
822
+ base.browse_messages.assistant_preview_length,
823
+ report,
824
+ () =>
825
+ validatePreviewLength(
826
+ browseMessages?.assistant_preview_length,
827
+ `${source}: browse_messages.assistant_preview_length`,
828
+ base.browse_messages.assistant_preview_length,
829
+ ),
830
+ ),
831
+ },
832
+ pull_message: {
833
+ text_length: resolveWithFallback(base.pull_message.text_length, report, () =>
834
+ validatePreviewLength(
835
+ pullMessage?.text_length,
836
+ `${source}: pull_message.text_length`,
837
+ base.pull_message.text_length,
838
+ )
839
+ ),
840
+ reasoning_length: resolveWithFallback(base.pull_message.reasoning_length, report, () =>
841
+ validatePreviewLength(
842
+ pullMessage?.reasoning_length,
843
+ `${source}: pull_message.reasoning_length`,
844
+ base.pull_message.reasoning_length,
845
+ )
846
+ ),
847
+ tool_output_length: resolveWithFallback(base.pull_message.tool_output_length, report, () =>
848
+ validatePreviewLength(
849
+ pullMessage?.tool_output_length,
850
+ `${source}: pull_message.tool_output_length`,
851
+ base.pull_message.tool_output_length,
852
+ )
853
+ ),
854
+ tool_input_length: resolveWithFallback(base.pull_message.tool_input_length, report, () =>
855
+ validatePreviewLength(
856
+ pullMessage?.tool_input_length,
857
+ `${source}: pull_message.tool_input_length`,
858
+ base.pull_message.tool_input_length,
859
+ )
860
+ ),
861
+ },
862
+ search: {
863
+ max_hits_per_message: resolveWithFallback(base.search.max_hits_per_message, report, () =>
864
+ validatePositiveInt(
865
+ search?.max_hits_per_message,
866
+ `${source}: search.max_hits_per_message`,
867
+ base.search.max_hits_per_message,
868
+ )
869
+ ),
870
+ max_snippets_per_hit: resolveWithFallback(base.search.max_snippets_per_hit, report, () =>
871
+ validatePositiveInt(
872
+ search?.max_snippets_per_hit,
873
+ `${source}: search.max_snippets_per_hit`,
874
+ base.search.max_snippets_per_hit,
875
+ )
876
+ ),
877
+ snippet_length: resolveWithFallback(base.search.snippet_length, report, () =>
878
+ validatePreviewLength(
879
+ search?.snippet_length,
880
+ `${source}: search.snippet_length`,
881
+ base.search.snippet_length,
882
+ )
883
+ ),
884
+ message_limit: resolveWithFallback(base.search.message_limit, report, () =>
885
+ validatePositiveInt(
886
+ search?.message_limit,
887
+ `${source}: search.message_limit`,
888
+ base.search.message_limit,
889
+ )
890
+ ),
891
+ },
892
+ show_tool_input: hasShowToolInput
893
+ ? resolveWithFallback(base.show_tool_input, report, () =>
894
+ mergeToolVisibilitySelectors(
895
+ SHOW_TOOL_INPUT_BUILTINS,
896
+ validateStringArray(
897
+ patch.show_tool_input,
898
+ `${source}: show_tool_input`,
899
+ [],
900
+ ),
901
+ )
902
+ )
903
+ : base.show_tool_input,
904
+ show_tool_output: hasShowToolOutput
905
+ ? resolveWithFallback(base.show_tool_output, report, () =>
906
+ mergeToolVisibilitySelectors(
907
+ SHOW_TOOL_OUTPUT_BUILTINS,
908
+ validateStringArray(
909
+ patch.show_tool_output,
910
+ `${source}: show_tool_output`,
911
+ [],
912
+ ),
913
+ )
914
+ )
915
+ : base.show_tool_output,
916
+ } satisfies EngramConfig;
917
+ }
918
+
919
+ async function readConfigFile(filePath: string) {
920
+ try {
921
+ const text = decodeConfigText(await readFile(filePath));
922
+ return parseJsonc(text, filePath);
923
+ } catch (error) {
924
+ if (
925
+ error &&
926
+ typeof error === "object" &&
927
+ "code" in error &&
928
+ error.code === "ENOENT"
929
+ ) {
930
+ return;
931
+ }
932
+
933
+ if (error instanceof Error) {
934
+ throw new Error(`Failed to read engram config at ${filePath}: ${error.message}`);
935
+ }
936
+
937
+ throw new Error(`Failed to read engram config at ${filePath}: ${String(error)}`);
938
+ }
939
+ }
940
+
941
+ export async function loadEngramConfig(
942
+ projectRoot = process.cwd(),
943
+ reportIssue: ConfigIssueReporter = noopConfigIssueReporter,
944
+ client?: SdkClient,
945
+ ) {
946
+ let resolved = defaults();
947
+ const roots = [...(await globalConfigRoots(client)), projectRoot];
948
+
949
+ for (const root of roots) {
950
+ for (const name of configNames) {
951
+ const filePath = join(root, name);
952
+ try {
953
+ const patch = await readConfigFile(filePath);
954
+ if (!patch) continue;
955
+ resolved = mergeConfig(resolved, patch, filePath, reportIssue);
956
+ } catch (error) {
957
+ reportIssue(errorMessage(error));
958
+ }
959
+ }
960
+ }
961
+
962
+ return resolved;
963
+ }