@herbertgao/pi-extensions 2026.8.12 → 2026.8.13

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 (112) hide show
  1. package/README.md +8 -3
  2. package/THIRD_PARTY_NOTICES.md +4 -0
  3. package/node_modules/@luxusai/pi-hindsight/CHANGELOG.md +376 -0
  4. package/node_modules/@luxusai/pi-hindsight/README.md +92 -0
  5. package/node_modules/@luxusai/pi-hindsight/docs/adr/001-memory-lifecycle-and-scope.md +56 -0
  6. package/node_modules/@luxusai/pi-hindsight/docs/adr/002-explicit-routing-strategy-seam.md +169 -0
  7. package/node_modules/@luxusai/pi-hindsight/docs/adr/003-tui-memory-mode-vocabulary.md +107 -0
  8. package/node_modules/@luxusai/pi-hindsight/docs/adr/004-lifeos-dual-bank-design.md +89 -0
  9. package/node_modules/@luxusai/pi-hindsight/docs/adr/005-domain-banks-and-agent-first-surface.md +189 -0
  10. package/node_modules/@luxusai/pi-hindsight/docs/assets/logos/pi-hindsight-logo-dark.webp +0 -0
  11. package/node_modules/@luxusai/pi-hindsight/docs/assets/logos/pi-hindsight-logo-dark@2x.webp +0 -0
  12. package/node_modules/@luxusai/pi-hindsight/docs/coding-memory-evaluation.md +54 -0
  13. package/node_modules/@luxusai/pi-hindsight/docs/compatibility.md +75 -0
  14. package/node_modules/@luxusai/pi-hindsight/docs/hindsight-core-functions.md +300 -0
  15. package/node_modules/@luxusai/pi-hindsight/docs/mission-and-mental-model-quality.md +158 -0
  16. package/node_modules/@luxusai/pi-hindsight/docs/next-opt-out-design.md +164 -0
  17. package/node_modules/@luxusai/pi-hindsight/docs/risky-memory-modes.md +139 -0
  18. package/node_modules/@luxusai/pi-hindsight/docs/starter-mental-model-suggestions.md +74 -0
  19. package/node_modules/@luxusai/pi-hindsight/docs/surface-reference.md +261 -0
  20. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-operations.ts +151 -0
  21. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-selection.ts +18 -0
  22. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-settings-presenter.ts +46 -0
  23. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-templates.ts +383 -0
  24. package/node_modules/@luxusai/pi-hindsight/extensions/banks/banking.ts +240 -0
  25. package/node_modules/@luxusai/pi-hindsight/extensions/banks/knowledge-page-seed.ts +176 -0
  26. package/node_modules/@luxusai/pi-hindsight/extensions/banks/retain-strategies.ts +178 -0
  27. package/node_modules/@luxusai/pi-hindsight/extensions/client/client-retry.ts +41 -0
  28. package/node_modules/@luxusai/pi-hindsight/extensions/client/client.ts +386 -0
  29. package/node_modules/@luxusai/pi-hindsight/extensions/client/fetch-compat.ts +39 -0
  30. package/node_modules/@luxusai/pi-hindsight/extensions/client/timeout.ts +34 -0
  31. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-defaults.ts +140 -0
  32. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-editing-model.ts +61 -0
  33. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-editing-registry.ts +925 -0
  34. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-field-paths.ts +247 -0
  35. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-normalize.ts +547 -0
  36. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-writer.ts +480 -0
  37. package/node_modules/@luxusai/pi-hindsight/extensions/config/config.ts +182 -0
  38. package/node_modules/@luxusai/pi-hindsight/extensions/config/setup-gate.ts +86 -0
  39. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-execute.ts +1010 -0
  40. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-parse.ts +175 -0
  41. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-plan.ts +425 -0
  42. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-presentation.ts +210 -0
  43. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-sessions.ts +817 -0
  44. package/node_modules/@luxusai/pi-hindsight/extensions/index.ts +25 -0
  45. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/git-seed.ts +319 -0
  46. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-recall.ts +196 -0
  47. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-retain.ts +189 -0
  48. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-runtime.ts +65 -0
  49. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle.ts +260 -0
  50. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/mental-models.ts +244 -0
  51. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/observation-scopes.ts +59 -0
  52. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall-cleanup.ts +82 -0
  53. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall-visibility.ts +45 -0
  54. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall.ts +331 -0
  55. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-cursor.ts +354 -0
  56. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-job-builder.ts +66 -0
  57. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-receipts.ts +132 -0
  58. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain.ts +228 -0
  59. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-bank-template-operations.ts +74 -0
  60. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-config-operations.ts +16 -0
  61. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-control-operations.ts +648 -0
  62. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-diagnostics-operations.ts +105 -0
  63. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-identity.ts +68 -0
  64. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-operation-service.ts +71 -0
  65. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-operation-types.ts +13 -0
  66. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-recall-operations.ts +162 -0
  67. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-retain-operations.ts +131 -0
  68. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-scope.ts +104 -0
  69. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-session-operations.ts +37 -0
  70. package/node_modules/@luxusai/pi-hindsight/extensions/operations/operation-catalog.ts +893 -0
  71. package/node_modules/@luxusai/pi-hindsight/extensions/operations/reflect-presenter.ts +35 -0
  72. package/node_modules/@luxusai/pi-hindsight/extensions/operations/scope-migrate.ts +177 -0
  73. package/node_modules/@luxusai/pi-hindsight/extensions/operations/tools.ts +7 -0
  74. package/node_modules/@luxusai/pi-hindsight/extensions/queue/flush-presenter.ts +21 -0
  75. package/node_modules/@luxusai/pi-hindsight/extensions/queue/jsonl-queue-store.ts +112 -0
  76. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-delivery.ts +111 -0
  77. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-lock.ts +214 -0
  78. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-operations.ts +70 -0
  79. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue.ts +392 -0
  80. package/node_modules/@luxusai/pi-hindsight/extensions/tui/bank-template-presentation.ts +45 -0
  81. package/node_modules/@luxusai/pi-hindsight/extensions/tui/commands.ts +9 -0
  82. package/node_modules/@luxusai/pi-hindsight/extensions/tui/guided-setup.ts +860 -0
  83. package/node_modules/@luxusai/pi-hindsight/extensions/tui/prefill-input.ts +105 -0
  84. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-flow.ts +189 -0
  85. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-server-probe.ts +299 -0
  86. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-actions.ts +201 -0
  87. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-facts.ts +47 -0
  88. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-render.ts +239 -0
  89. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-types.ts +50 -0
  90. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui.ts +259 -0
  91. package/node_modules/@luxusai/pi-hindsight/extensions/tui/tool-presenters.ts +77 -0
  92. package/node_modules/@luxusai/pi-hindsight/extensions/types.ts +534 -0
  93. package/node_modules/@luxusai/pi-hindsight/extensions/utils/diagnostics.ts +319 -0
  94. package/node_modules/@luxusai/pi-hindsight/extensions/utils/messages.ts +325 -0
  95. package/node_modules/@luxusai/pi-hindsight/extensions/utils/sanitize.ts +42 -0
  96. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session-memory-meta.ts +244 -0
  97. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session-operations.ts +56 -0
  98. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session.ts +50 -0
  99. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status-fields.ts +167 -0
  100. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status-health.ts +207 -0
  101. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status.ts +120 -0
  102. package/node_modules/@luxusai/pi-hindsight/extensions/version.ts +8 -0
  103. package/node_modules/@luxusai/pi-hindsight/package.json +118 -0
  104. package/node_modules/@luxusai/pi-hindsight/skills/hindsight-memory-doctor/SKILL.md +75 -0
  105. package/node_modules/@narumitw/pi-btw/package.json +2 -2
  106. package/node_modules/pi-mcp-adapter/CHANGELOG.md +13 -0
  107. package/node_modules/pi-mcp-adapter/config.ts +10 -0
  108. package/node_modules/pi-mcp-adapter/dist/config.js +10 -0
  109. package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
  110. package/node_modules/pi-mcp-adapter/init.ts +5 -1
  111. package/node_modules/pi-mcp-adapter/package.json +1 -1
  112. package/package.json +10 -4
@@ -0,0 +1,480 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { applyEdits, modify } from "jsonc-parser";
6
+
7
+ import { validEnvVarName } from "./config.js";
8
+ import { resetPathsForConfigKeys, type ConfigResetKey } from "./config-field-paths.js";
9
+ import { parseJsonWithComments } from "./config.js";
10
+
11
+ export type MemoryProfile = "project-only" | "project+global" | "global-only" | "recall-only";
12
+
13
+ export type ConfigScope = "project" | "global";
14
+
15
+ export type ConfigSource = ConfigScope | "env" | "default";
16
+ export type { ConfigResetKey } from "./config-field-paths.js";
17
+
18
+ export const DEFAULT_GLOBAL_BANK_ID = "pi-global";
19
+
20
+ function isRecord(value: unknown): value is Record<string, unknown> {
21
+ return typeof value === "object" && value !== null && !Array.isArray(value);
22
+ }
23
+
24
+ function mergeBankPatch(
25
+ patch: Record<string, unknown>,
26
+ bank: "project" | "user",
27
+ values: Record<string, unknown>,
28
+ ): void {
29
+ const banksPatch = isRecord(patch.banks) ? patch.banks : {};
30
+ patch.banks = {
31
+ ...banksPatch,
32
+ [bank]: {
33
+ ...(isRecord(banksPatch[bank]) ? banksPatch[bank] : {}),
34
+ ...values,
35
+ },
36
+ };
37
+ }
38
+
39
+ export function projectConfigPath(cwd: string): string {
40
+ return join(cwd, ".pi", "hindsight.json");
41
+ }
42
+
43
+ function projectJsoncConfigPath(cwd: string): string {
44
+ return join(cwd, ".pi", "hindsight.jsonc");
45
+ }
46
+
47
+ export function globalConfigPath(home = process.env.HOME || homedir()): string {
48
+ return join(home, ".pi", "agent", "hindsight.json");
49
+ }
50
+
51
+ function globalJsoncConfigPath(home = process.env.HOME || homedir()): string {
52
+ return join(home, ".pi", "agent", "hindsight.jsonc");
53
+ }
54
+
55
+ function activeConfigPath(jsonPath: string, jsoncPath: string): string {
56
+ if (existsSync(jsonPath)) return jsonPath;
57
+ if (existsSync(jsoncPath)) return jsoncPath;
58
+ return jsonPath;
59
+ }
60
+
61
+ function readConfig(path: string): Record<string, unknown> {
62
+ if (!existsSync(path)) return {};
63
+ const parsed = parseJsonWithComments(readFileSync(path, "utf8"), path);
64
+ if (!isRecord(parsed)) throw new Error(`${path} must contain a JSON object`);
65
+ return parsed;
66
+ }
67
+
68
+ export function readProjectConfig(cwd: string): Record<string, unknown> {
69
+ return readConfig(activeConfigPath(projectConfigPath(cwd), projectJsoncConfigPath(cwd)));
70
+ }
71
+
72
+ export function readGlobalConfig(home?: string): Record<string, unknown> {
73
+ return readConfig(activeConfigPath(globalConfigPath(home), globalJsoncConfigPath(home)));
74
+ }
75
+
76
+ export function deepMergeConfig(
77
+ base: Record<string, unknown>,
78
+ patch: Record<string, unknown>,
79
+ ): Record<string, unknown> {
80
+ const out: Record<string, unknown> = { ...base };
81
+ for (const [key, value] of Object.entries(patch)) {
82
+ out[key] = isRecord(value) && isRecord(out[key]) ? deepMergeConfig(out[key], value) : value;
83
+ }
84
+ return out;
85
+ }
86
+
87
+ export interface ProjectConfigPatchInput {
88
+ enabled?: boolean;
89
+ setupComplete?: boolean;
90
+ scopeMode?: "domain-tagged" | "isolated-bank";
91
+ projectId?: string;
92
+ projectIdStrategy?: "remote" | "basename";
93
+ /** Opt-in shared/untagged observation recall (ADR-005 / #492). */
94
+ includeSharedObservations?: boolean;
95
+ baseUrl?: string;
96
+ timeoutMs?: number;
97
+ apiKeyEnvVar?: string;
98
+ directApiKey?: string;
99
+ projectBankId?: string;
100
+ projectRetainMission?: string;
101
+ projectReflectMission?: string;
102
+ projectObservationsMission?: string;
103
+ globalBankId?: string;
104
+ globalRetainMission?: string;
105
+ globalReflectMission?: string;
106
+ globalObservationsMission?: string;
107
+ globalRetainMode?: "explicit-only";
108
+ enableGlobalBank?: boolean;
109
+ memoryProfile?: MemoryProfile;
110
+ agentUse?: "coding" | "conversation";
111
+ mentalModelsInject?: boolean;
112
+ recallEnabled?: boolean;
113
+ recallBudget?: "low" | "mid" | "high";
114
+ recallMaxTokens?: number;
115
+ recallUserMaxTokens?: number;
116
+ recallStoreLast?: boolean;
117
+ recallStoreFailures?: boolean;
118
+ recallPreferObservations?: boolean;
119
+ retainEnabled?: boolean;
120
+ retainAsync?: boolean;
121
+ retainDelivery?: "immediate" | "coalesced";
122
+ retainUpdateMode?: "append" | "replace";
123
+ queuePath?: string;
124
+ importMode?: "curated" | "raw" | "forensic";
125
+ importQualityProfile?: "compatible" | "strict";
126
+ importIncludeBranches?: "current-only" | "all-leaves";
127
+ importToolResults?: "errors-only" | "summary" | "content";
128
+ importToolResultSummaryMaxChars?: number;
129
+ importManifestPath?: string;
130
+ importCheckpointPath?: string;
131
+ importReplaceExistingDocs?: boolean;
132
+ importResume?: boolean;
133
+ statusStyle?: "off" | "text" | "emoji" | "nerdfont";
134
+ statusDetail?: "minimal" | "project" | "activity" | "verbose";
135
+ statusMaxLength?: number;
136
+ statusShowActivity?: boolean;
137
+ notifyStartup?: boolean;
138
+ notifyRecall?: boolean;
139
+ notifyRetain?: boolean;
140
+ resetDefaults?: ConfigResetKey[];
141
+ scope?: ConfigScope;
142
+ }
143
+
144
+ export function buildProjectConfigDeletes(input: ProjectConfigPatchInput): string[][] {
145
+ return resetPathsForConfigKeys(input.resetDefaults ?? []);
146
+ }
147
+
148
+ export function buildProjectConfigPatch(input: ProjectConfigPatchInput): Record<string, unknown> {
149
+ const patch: Record<string, unknown> = {};
150
+ if (input.enabled !== undefined) patch.enabled = input.enabled;
151
+ if (input.setupComplete !== undefined) patch.setupComplete = input.setupComplete;
152
+ if (
153
+ input.scopeMode !== undefined ||
154
+ input.projectId !== undefined ||
155
+ input.projectIdStrategy !== undefined ||
156
+ input.includeSharedObservations !== undefined
157
+ ) {
158
+ patch.scope = {
159
+ ...(input.scopeMode !== undefined ? { mode: input.scopeMode } : {}),
160
+ ...(input.projectId !== undefined ? { projectId: input.projectId } : {}),
161
+ ...(input.projectIdStrategy !== undefined
162
+ ? { projectIdStrategy: input.projectIdStrategy }
163
+ : {}),
164
+ ...(input.includeSharedObservations !== undefined
165
+ ? { includeSharedObservations: input.includeSharedObservations }
166
+ : {}),
167
+ };
168
+ }
169
+ if (input.apiKeyEnvVar && !validEnvVarName(input.apiKeyEnvVar)) {
170
+ throw new Error("apiKeyEnvVar must be an environment variable name");
171
+ }
172
+ if (input.directApiKey && input.scope !== "global") {
173
+ throw new Error(
174
+ "directApiKey can only be written to user config; use apiKeyEnvVar for project config",
175
+ );
176
+ }
177
+ if (input.baseUrl || input.timeoutMs !== undefined || input.apiKeyEnvVar || input.directApiKey) {
178
+ patch.hindsight = {
179
+ ...(input.baseUrl ? { baseUrl: input.baseUrl } : {}),
180
+ ...(input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs } : {}),
181
+ ...(input.apiKeyEnvVar ? { apiKey: { source: "env", name: input.apiKeyEnvVar } } : {}),
182
+ ...(input.directApiKey ? { apiKey: input.directApiKey } : {}),
183
+ };
184
+ }
185
+ if (input.agentUse) {
186
+ patch.agentUse = input.agentUse;
187
+ }
188
+ if (input.mentalModelsInject !== undefined) {
189
+ patch.mentalModels = { inject: input.mentalModelsInject };
190
+ }
191
+ if (input.memoryProfile) {
192
+ const globalBankId = input.globalBankId?.trim();
193
+ const userBankPatch = { enabled: true, ...(globalBankId ? { bankId: globalBankId } : {}) };
194
+ if (input.memoryProfile === "project-only") {
195
+ patch.banks = { project: { enabled: true }, user: { enabled: false } };
196
+ patch.userRetain = { mode: "explicit-only" };
197
+ patch.recall = { enabled: true };
198
+ patch.retain = { enabled: true };
199
+ } else if (input.memoryProfile === "project+global") {
200
+ patch.banks = {
201
+ project: { enabled: true },
202
+ user: userBankPatch,
203
+ };
204
+ patch.userRetain = { mode: "explicit-only" };
205
+ patch.recall = { enabled: true };
206
+ patch.retain = { enabled: true };
207
+ } else if (input.memoryProfile === "global-only") {
208
+ patch.banks = {
209
+ project: { enabled: false },
210
+ user: userBankPatch,
211
+ };
212
+ patch.userRetain = { mode: "explicit-only" };
213
+ patch.recall = { enabled: true };
214
+ patch.retain = { enabled: true };
215
+ } else {
216
+ patch.recall = { enabled: true };
217
+ patch.retain = { enabled: false };
218
+ }
219
+ }
220
+ if (input.projectBankId && input.memoryProfile !== "global-only") {
221
+ patch.banks = {
222
+ ...(isRecord(patch.banks) ? patch.banks : {}),
223
+ project: { enabled: true, derive: "manual", bankId: input.projectBankId },
224
+ };
225
+ }
226
+ if (input.projectRetainMission !== undefined) {
227
+ mergeBankPatch(patch, "project", { retainMission: input.projectRetainMission });
228
+ }
229
+ if (input.projectReflectMission !== undefined) {
230
+ mergeBankPatch(patch, "project", { reflectMission: input.projectReflectMission });
231
+ }
232
+ if (input.projectObservationsMission !== undefined) {
233
+ mergeBankPatch(patch, "project", { observationsMission: input.projectObservationsMission });
234
+ }
235
+ if (!input.memoryProfile && (input.globalBankId || input.enableGlobalBank !== undefined)) {
236
+ patch.banks = {
237
+ ...(isRecord(patch.banks) ? patch.banks : {}),
238
+ user: {
239
+ ...(input.enableGlobalBank !== undefined ? { enabled: input.enableGlobalBank } : {}),
240
+ ...(input.globalBankId ? { bankId: input.globalBankId, enabled: true } : {}),
241
+ },
242
+ };
243
+ }
244
+ if (input.globalRetainMission !== undefined) {
245
+ mergeBankPatch(patch, "user", { retainMission: input.globalRetainMission });
246
+ }
247
+ if (input.globalReflectMission !== undefined) {
248
+ mergeBankPatch(patch, "user", { reflectMission: input.globalReflectMission });
249
+ }
250
+ if (input.globalObservationsMission !== undefined) {
251
+ mergeBankPatch(patch, "user", { observationsMission: input.globalObservationsMission });
252
+ }
253
+ if (input.globalRetainMode) {
254
+ patch.userRetain = { mode: input.globalRetainMode };
255
+ }
256
+ if (
257
+ input.recallEnabled !== undefined ||
258
+ input.recallBudget ||
259
+ input.recallMaxTokens !== undefined ||
260
+ input.recallUserMaxTokens !== undefined ||
261
+ input.recallStoreLast !== undefined ||
262
+ input.recallStoreFailures !== undefined ||
263
+ input.recallPreferObservations !== undefined
264
+ ) {
265
+ patch.recall = {
266
+ ...(input.recallEnabled !== undefined ? { enabled: input.recallEnabled } : {}),
267
+ ...(input.recallBudget ? { budget: input.recallBudget } : {}),
268
+ ...(input.recallMaxTokens !== undefined ? { maxTokens: input.recallMaxTokens } : {}),
269
+ ...(input.recallUserMaxTokens !== undefined
270
+ ? { userMaxTokens: input.recallUserMaxTokens }
271
+ : {}),
272
+ ...(input.recallStoreLast !== undefined ? { storeLastRecall: input.recallStoreLast } : {}),
273
+ ...(input.recallStoreFailures !== undefined
274
+ ? { storeLastRecallFailures: input.recallStoreFailures }
275
+ : {}),
276
+ ...(input.recallPreferObservations !== undefined
277
+ ? { preferObservations: input.recallPreferObservations }
278
+ : {}),
279
+ };
280
+ }
281
+ if (
282
+ input.queuePath ||
283
+ input.retainEnabled !== undefined ||
284
+ input.retainAsync !== undefined ||
285
+ input.retainDelivery ||
286
+ input.retainUpdateMode
287
+ ) {
288
+ patch.retain = {
289
+ ...(input.retainEnabled !== undefined ? { enabled: input.retainEnabled } : {}),
290
+ ...(input.retainAsync !== undefined ? { async: input.retainAsync } : {}),
291
+ ...(input.retainDelivery ? { delivery: input.retainDelivery } : {}),
292
+ ...(input.retainUpdateMode ? { updateMode: input.retainUpdateMode } : {}),
293
+ ...(input.queuePath ? { queuePath: input.queuePath } : {}),
294
+ };
295
+ }
296
+ if (
297
+ input.importMode ||
298
+ input.importQualityProfile ||
299
+ input.importIncludeBranches ||
300
+ input.importToolResults ||
301
+ input.importToolResultSummaryMaxChars !== undefined ||
302
+ input.importManifestPath ||
303
+ input.importCheckpointPath ||
304
+ input.importReplaceExistingDocs !== undefined ||
305
+ input.importResume !== undefined
306
+ ) {
307
+ patch.import = {
308
+ ...(input.importMode ? { mode: input.importMode } : {}),
309
+ ...(input.importQualityProfile ? { qualityProfile: input.importQualityProfile } : {}),
310
+ ...(input.importIncludeBranches ? { includeBranches: input.importIncludeBranches } : {}),
311
+ ...(input.importToolResults ? { toolResults: input.importToolResults } : {}),
312
+ ...(input.importToolResultSummaryMaxChars !== undefined
313
+ ? { toolResultSummaryMaxChars: input.importToolResultSummaryMaxChars }
314
+ : {}),
315
+ ...(input.importManifestPath ? { manifestPath: input.importManifestPath } : {}),
316
+ ...(input.importCheckpointPath ? { checkpointPath: input.importCheckpointPath } : {}),
317
+ ...(input.importReplaceExistingDocs !== undefined
318
+ ? { replaceExistingImportedDocs: input.importReplaceExistingDocs }
319
+ : {}),
320
+ ...(input.importResume !== undefined ? { resume: input.importResume } : {}),
321
+ };
322
+ }
323
+ if (
324
+ input.statusStyle ||
325
+ input.statusDetail ||
326
+ input.statusMaxLength !== undefined ||
327
+ input.statusShowActivity !== undefined
328
+ ) {
329
+ patch.status = {
330
+ ...(input.statusStyle ? { style: input.statusStyle } : {}),
331
+ ...(input.statusDetail ? { detail: input.statusDetail } : {}),
332
+ ...(input.statusMaxLength !== undefined ? { maxLength: input.statusMaxLength } : {}),
333
+ ...(input.statusShowActivity !== undefined ? { showActivity: input.statusShowActivity } : {}),
334
+ };
335
+ }
336
+ if (
337
+ input.notifyStartup !== undefined ||
338
+ input.notifyRecall !== undefined ||
339
+ input.notifyRetain !== undefined
340
+ ) {
341
+ patch.notifications = {
342
+ ...(input.notifyStartup !== undefined ? { startup: input.notifyStartup } : {}),
343
+ ...(input.notifyRecall !== undefined ? { recall: input.notifyRecall } : {}),
344
+ ...(input.notifyRetain !== undefined ? { retain: input.notifyRetain } : {}),
345
+ };
346
+ }
347
+ return patch;
348
+ }
349
+
350
+ function deletePath(config: Record<string, unknown>, path: string[]): void {
351
+ const [head, ...rest] = path;
352
+ if (!head) return;
353
+ if (rest.length === 0) {
354
+ delete config[head];
355
+ return;
356
+ }
357
+ const child = config[head];
358
+ if (isRecord(child)) deletePath(child, rest);
359
+ }
360
+
361
+ async function writeConfig(
362
+ path: string,
363
+ base: Record<string, unknown>,
364
+ patch: Record<string, unknown>,
365
+ deletePaths: string[][] = [],
366
+ ): Promise<{ path: string; config: Record<string, unknown> }> {
367
+ for (const deletePathParts of deletePaths) deletePath(base, deletePathParts);
368
+ const next = deepMergeConfig(base, patch);
369
+ await mkdir(dirname(path), { recursive: true });
370
+ if (existsSync(path) && path.endsWith(".jsonc")) {
371
+ await writeJsoncConfig(path, base, patch, deletePaths);
372
+ } else {
373
+ await writeFile(path, `${JSON.stringify(next, null, 2)}\n`, "utf8");
374
+ }
375
+ return { path, config: next };
376
+ }
377
+
378
+ function getPathValue(value: unknown, path: string[]): unknown {
379
+ let current = value;
380
+ for (const part of path) {
381
+ if (!isRecord(current)) return undefined;
382
+ current = current[part];
383
+ }
384
+ return current;
385
+ }
386
+
387
+ function flattenPatchForJsonc(
388
+ value: unknown,
389
+ base: Record<string, unknown>,
390
+ prefix: string[] = [],
391
+ ): Array<{ path: string[]; value: unknown }> {
392
+ if (!isRecord(value)) return [{ path: prefix, value }];
393
+ if (prefix.length > 0 && !isRecord(getPathValue(base, prefix))) {
394
+ return [{ path: prefix, value }];
395
+ }
396
+ return Object.entries(value).flatMap(([key, child]) =>
397
+ flattenPatchForJsonc(child, base, [...prefix, key]),
398
+ );
399
+ }
400
+
401
+ async function writeJsoncConfig(
402
+ path: string,
403
+ base: Record<string, unknown>,
404
+ patch: Record<string, unknown>,
405
+ deletePaths: string[][],
406
+ ): Promise<void> {
407
+ let text = readFileSync(path, "utf8");
408
+ for (const pathParts of deletePaths) {
409
+ text = applyEdits(
410
+ text,
411
+ modify(text, pathParts, undefined, { formattingOptions: { insertSpaces: true, tabSize: 2 } }),
412
+ );
413
+ }
414
+ for (const item of flattenPatchForJsonc(patch, base)) {
415
+ text = applyEdits(
416
+ text,
417
+ modify(text, item.path, item.value, {
418
+ formattingOptions: { insertSpaces: true, tabSize: 2 },
419
+ isArrayInsertion: false,
420
+ }),
421
+ );
422
+ }
423
+ await writeFile(path, text.endsWith("\n") ? text : `${text}\n`, "utf8");
424
+ }
425
+
426
+ export async function writeProjectConfig(
427
+ cwd: string,
428
+ patch: Record<string, unknown>,
429
+ deletePaths: string[][] = [],
430
+ ): Promise<{ path: string; config: Record<string, unknown> }> {
431
+ const path = activeConfigPath(projectConfigPath(cwd), projectJsoncConfigPath(cwd));
432
+ return writeConfig(path, readProjectConfig(cwd), patch, deletePaths);
433
+ }
434
+
435
+ export async function writeGlobalConfig(
436
+ patch: Record<string, unknown>,
437
+ deletePaths: string[][] = [],
438
+ home?: string,
439
+ ): Promise<{ path: string; config: Record<string, unknown> }> {
440
+ const path = activeConfigPath(globalConfigPath(home), globalJsoncConfigPath(home));
441
+ return writeConfig(path, readGlobalConfig(home), patch, deletePaths);
442
+ }
443
+
444
+ export type ConfigOperationDeps = {
445
+ getProjectBankId(): string;
446
+ reloadConfig?(cwd: string): void;
447
+ };
448
+
449
+ export async function configureMemory(
450
+ cwd: string,
451
+ args: ProjectConfigPatchInput,
452
+ deps: ConfigOperationDeps,
453
+ ): Promise<{ path: string; config: Record<string, unknown>; projectBankId: string }> {
454
+ const projectBankId = args.projectBankId || deps.getProjectBankId();
455
+ const patch = buildProjectConfigPatch(args);
456
+ const deletes = buildProjectConfigDeletes(args);
457
+ const result =
458
+ args.scope === "global"
459
+ ? await writeGlobalConfig(patch, deletes)
460
+ : await writeProjectConfig(cwd, patch, deletes);
461
+ deps.reloadConfig?.(cwd);
462
+ return { projectBankId, ...result };
463
+ }
464
+
465
+ export async function initMemoryConfig(
466
+ cwd: string,
467
+ deps: Pick<ConfigOperationDeps, "getProjectBankId"> & {
468
+ getConfig(): { hindsight: { baseUrl: string } };
469
+ },
470
+ ): Promise<{ path: string; config: Record<string, unknown>; projectBankId: string }> {
471
+ const projectBankId = deps.getProjectBankId();
472
+ const result = await writeProjectConfig(
473
+ cwd,
474
+ buildProjectConfigPatch({
475
+ projectBankId,
476
+ baseUrl: deps.getConfig().hindsight.baseUrl,
477
+ }),
478
+ );
479
+ return { projectBankId, ...result };
480
+ }
@@ -0,0 +1,182 @@
1
+ import { copyFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { parse, printParseErrorCode, type ParseError } from "jsonc-parser";
4
+ import type { ResolvedConfig } from "../types.js";
5
+ import { DEFAULT_CONFIG } from "./config-defaults.js";
6
+ import { envBool, isRecord, merge, normalizeConfig, validEnvVarName } from "./config-normalize.js";
7
+
8
+ export { DEFAULT_CONFIG } from "./config-defaults.js";
9
+ export { normalizeConfig, validEnvVarName } from "./config-normalize.js";
10
+
11
+ export function parseJsonWithComments(text: string, source = "config"): unknown {
12
+ const errors: ParseError[] = [];
13
+ const value = parse(text, errors, { allowTrailingComma: true, disallowComments: false });
14
+ if (errors.length > 0) {
15
+ const first = errors[0]!;
16
+ throw new Error(
17
+ `Invalid JSONC in ${source} at offset ${first.offset}: ${printParseErrorCode(first.error)}`,
18
+ );
19
+ }
20
+ return value as unknown;
21
+ }
22
+
23
+ export function readJson(path: string): unknown {
24
+ if (!existsSync(path)) return undefined;
25
+ return parseJsonWithComments(readFileSync(path, "utf8"), path);
26
+ }
27
+
28
+ export function readConfigFile(basePath: string): unknown {
29
+ const json = readJson(`${basePath}.json`);
30
+ return json === undefined ? readJson(`${basePath}.jsonc`) : json;
31
+ }
32
+
33
+ function configPath(basePath: string): string | undefined {
34
+ const json = `${basePath}.json`;
35
+ if (existsSync(json)) return json;
36
+ const jsonc = `${basePath}.jsonc`;
37
+ if (existsSync(jsonc)) return jsonc;
38
+ return undefined;
39
+ }
40
+
41
+ function migrateUserMemoryKeys(config: Record<string, unknown>): boolean {
42
+ let changed = false;
43
+ if (isRecord(config.banks)) {
44
+ if ("global" in config.banks) {
45
+ if (isRecord(config.banks.user) && isRecord(config.banks.global)) {
46
+ config.banks.user = { ...config.banks.global, ...config.banks.user };
47
+ } else if (!("user" in config.banks)) config.banks.user = config.banks.global;
48
+ delete config.banks.global;
49
+ changed = true;
50
+ }
51
+ }
52
+ if ("globalRetain" in config) {
53
+ if (isRecord(config.userRetain) && isRecord(config.globalRetain)) {
54
+ config.userRetain = { ...config.globalRetain, ...config.userRetain };
55
+ } else if (!("userRetain" in config)) config.userRetain = config.globalRetain;
56
+ delete config.globalRetain;
57
+ changed = true;
58
+ }
59
+ if (isRecord(config.recall)) {
60
+ if ("globalQueryPreamble" in config.recall) {
61
+ if (!("userQueryPreamble" in config.recall)) {
62
+ config.recall.userQueryPreamble = config.recall.globalQueryPreamble;
63
+ }
64
+ delete config.recall.globalQueryPreamble;
65
+ changed = true;
66
+ }
67
+ }
68
+ return changed;
69
+ }
70
+
71
+ export interface ConfigMigrationResult {
72
+ path: string;
73
+ backupPath: string;
74
+ }
75
+
76
+ let lastConfigMigrationResults: ConfigMigrationResult[] = [];
77
+
78
+ export function consumeLastConfigMigrationResults(): ConfigMigrationResult[] {
79
+ const results = lastConfigMigrationResults;
80
+ lastConfigMigrationResults = [];
81
+ return results;
82
+ }
83
+
84
+ export function migrateUserMemoryConfigFile(
85
+ path: string,
86
+ now = new Date(),
87
+ ): ConfigMigrationResult | undefined {
88
+ if (!existsSync(path)) return undefined;
89
+ const parsed = parseJsonWithComments(readFileSync(path, "utf8"), path);
90
+ if (!isRecord(parsed)) return undefined;
91
+ if (!migrateUserMemoryKeys(parsed)) return undefined;
92
+ const stamp = now.toISOString().replace(/[:.]/g, "-");
93
+ const backupPath = `${path}.bak-${stamp}`;
94
+ copyFileSync(path, backupPath);
95
+ writeFileSync(path, `${JSON.stringify(parsed, null, 2)}\n`);
96
+ return { path, backupPath };
97
+ }
98
+
99
+ export function migrateUserMemoryConfigFiles(
100
+ cwd: string,
101
+ env: NodeJS.ProcessEnv = process.env,
102
+ ): ConfigMigrationResult[] {
103
+ const results: ConfigMigrationResult[] = [];
104
+ const home = env.HOME;
105
+ const paths = [
106
+ home ? configPath(join(home, ".pi", "agent", "hindsight")) : undefined,
107
+ configPath(join(cwd, ".pi", "hindsight")),
108
+ ].filter((path): path is string => Boolean(path));
109
+ const now = new Date();
110
+ for (const path of paths) {
111
+ const result = migrateUserMemoryConfigFile(path, now);
112
+ if (result) results.push(result);
113
+ }
114
+ if (results.length) lastConfigMigrationResults = results;
115
+ return results;
116
+ }
117
+
118
+ export function resolveConfig(cwd: string, env: NodeJS.ProcessEnv = process.env): ResolvedConfig {
119
+ migrateUserMemoryConfigFiles(cwd, env);
120
+ let rawConfig: Record<string, unknown> = {};
121
+ let config = DEFAULT_CONFIG;
122
+ const home = env.HOME;
123
+ const homeConfig = home ? readConfigFile(join(home, ".pi", "agent", "hindsight")) : undefined;
124
+ rawConfig = merge(rawConfig, homeConfig);
125
+ config = merge(config, homeConfig);
126
+ const projectConfig = readConfigFile(join(cwd, ".pi", "hindsight"));
127
+ rawConfig = merge(rawConfig, projectConfig);
128
+ config = merge(config, projectConfig);
129
+
130
+ const enabled = envBool(env, "PI_HINDSIGHT_ENABLED");
131
+ if (enabled !== undefined) {
132
+ rawConfig = merge(rawConfig, { enabled });
133
+ config = merge(config, { enabled });
134
+ }
135
+ if (env.HINDSIGHT_BASE_URL) {
136
+ rawConfig = merge(rawConfig, { hindsight: { baseUrl: env.HINDSIGHT_BASE_URL } });
137
+ config = merge(config, { hindsight: { baseUrl: env.HINDSIGHT_BASE_URL } });
138
+ }
139
+ if (env.HINDSIGHT_API_KEY_REF && validEnvVarName(env.HINDSIGHT_API_KEY_REF)) {
140
+ const ref = { source: "env", name: env.HINDSIGHT_API_KEY_REF };
141
+ rawConfig = merge(rawConfig, { hindsight: { apiKey: ref } });
142
+ config = merge(config, { hindsight: { apiKey: ref } });
143
+ }
144
+ // Prefer HINDSIGHT_API_TOKEN; keep HINDSIGHT_API_KEY as legacy fallback.
145
+ const apiKeyFromEnv = env.HINDSIGHT_API_TOKEN?.trim() || env.HINDSIGHT_API_KEY?.trim();
146
+ if (apiKeyFromEnv) {
147
+ rawConfig = merge(rawConfig, { hindsight: { apiKey: apiKeyFromEnv } });
148
+ config = merge(config, { hindsight: { apiKey: apiKeyFromEnv } });
149
+ }
150
+ if (env.PI_HINDSIGHT_PROJECT_BANK_ID) {
151
+ const patch = {
152
+ banks: { project: { bankId: env.PI_HINDSIGHT_PROJECT_BANK_ID, derive: "manual" } },
153
+ };
154
+ rawConfig = merge(rawConfig, patch);
155
+ config = merge(config, patch);
156
+ }
157
+ const userBankId = env.PI_HINDSIGHT_USER_BANK_ID || env.PI_HINDSIGHT_GLOBAL_BANK_ID;
158
+ if (userBankId) {
159
+ const patch = {
160
+ banks: { user: { enabled: true, bankId: userBankId } },
161
+ };
162
+ rawConfig = merge(rawConfig, patch);
163
+ config = merge(config, patch);
164
+ }
165
+ const minScores: Record<string, number> = {};
166
+ const semanticFloor =
167
+ env.PI_HINDSIGHT_MIN_SEMANTIC?.trim() === ""
168
+ ? Number.NaN
169
+ : Number(env.PI_HINDSIGHT_MIN_SEMANTIC);
170
+ if (Number.isFinite(semanticFloor)) minScores.semantic = semanticFloor;
171
+ const rerankerFloor =
172
+ env.PI_HINDSIGHT_MIN_RERANKER?.trim() === ""
173
+ ? Number.NaN
174
+ : Number(env.PI_HINDSIGHT_MIN_RERANKER);
175
+ if (Number.isFinite(rerankerFloor)) minScores.reranker = rerankerFloor;
176
+ if (Object.keys(minScores).length) {
177
+ const patch = { recall: { minScores } };
178
+ rawConfig = merge(rawConfig, patch);
179
+ config = merge(config, patch);
180
+ }
181
+ return normalizeConfig(config, rawConfig, env);
182
+ }