@wrongstack/core 0.310.1 → 0.313.1

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 (187) hide show
  1. package/dist/chronicle/index.js +84 -6391
  2. package/dist/chronicle/project-server.js +84 -4868
  3. package/dist/chunk-2BUUIMQB.js +225 -0
  4. package/dist/chunk-2FK4WTFF.js +207 -0
  5. package/dist/chunk-2FQL4PDC.js +292 -0
  6. package/dist/chunk-2KKODIQU.js +2256 -0
  7. package/dist/chunk-2YU4WVBW.js +134 -0
  8. package/dist/chunk-3E244UDA.js +356 -0
  9. package/dist/chunk-3XPVB7BA.js +135 -0
  10. package/dist/chunk-4LJJYBAU.js +388 -0
  11. package/dist/chunk-4VWO3ZXL.js +342 -0
  12. package/dist/chunk-4XV6IR37.js +187 -0
  13. package/dist/chunk-4ZHTKJHM.js +183 -0
  14. package/dist/chunk-52RTVH4M.js +361 -0
  15. package/dist/chunk-57GEWK5X.js +26 -0
  16. package/dist/chunk-5DSMEVA4.js +931 -0
  17. package/dist/chunk-5RP3QTRL.js +1223 -0
  18. package/dist/chunk-5S437PRC.js +50 -0
  19. package/dist/chunk-5XDUGTKN.js +511 -0
  20. package/dist/chunk-62V2AFVG.js +422 -0
  21. package/dist/chunk-65RDIOR2.js +2470 -0
  22. package/dist/chunk-6AKOTGLM.js +597 -0
  23. package/dist/chunk-7ZHZE5EO.js +28 -0
  24. package/dist/chunk-A455ATZB.js +140 -0
  25. package/dist/chunk-AD7ZFZCS.js +46 -0
  26. package/dist/chunk-AI4LLQ2U.js +856 -0
  27. package/dist/chunk-ASX6AW36.js +789 -0
  28. package/dist/chunk-AT4V4XGI.js +3522 -0
  29. package/dist/chunk-AWNQ6ED6.js +311 -0
  30. package/dist/chunk-B4AMLZPB.js +9 -0
  31. package/dist/chunk-BAKVJLEV.js +417 -0
  32. package/dist/chunk-BJHVULHO.js +55 -0
  33. package/dist/chunk-CB2HFKTP.js +9 -0
  34. package/dist/chunk-CE4ZOCOI.js +1640 -0
  35. package/dist/chunk-CQQL4ZOQ.js +151 -0
  36. package/dist/chunk-DEIQMBD2.js +65 -0
  37. package/dist/chunk-E6I5RXVK.js +58 -0
  38. package/dist/chunk-EFEHDQGY.js +1589 -0
  39. package/dist/chunk-EODDVJBM.js +82 -0
  40. package/dist/chunk-EPC3GZD4.js +308 -0
  41. package/dist/chunk-FAYRX2AD.js +13 -0
  42. package/dist/chunk-FFSX2QDQ.js +362 -0
  43. package/dist/chunk-FHAMQS7Q.js +5856 -0
  44. package/dist/chunk-FIV4IJL7.js +2263 -0
  45. package/dist/chunk-FNXVT7HF.js +52 -0
  46. package/dist/chunk-FSWCWSTS.js +395 -0
  47. package/dist/chunk-FW5LDDMJ.js +16464 -0
  48. package/dist/chunk-FXQLIX3K.js +211 -0
  49. package/dist/chunk-G5AAKEEZ.js +228 -0
  50. package/dist/chunk-G5S3S5ZN.js +38 -0
  51. package/dist/chunk-GAZEWQEZ.js +231 -0
  52. package/dist/chunk-GFJOMSHG.js +125 -0
  53. package/dist/chunk-GFXZTHNI.js +158 -0
  54. package/dist/chunk-GM54GJBU.js +960 -0
  55. package/dist/chunk-GPQ3NJPR.js +900 -0
  56. package/dist/chunk-GTCU6DZ3.js +1 -0
  57. package/dist/chunk-H5E77KUJ.js +15 -0
  58. package/dist/chunk-H72H4XDU.js +1023 -0
  59. package/dist/chunk-HA4ZYA6T.js +178 -0
  60. package/dist/chunk-I45KAW2N.js +5020 -0
  61. package/dist/chunk-IQ35HMQL.js +1396 -0
  62. package/dist/chunk-IYFBFX34.js +86 -0
  63. package/dist/chunk-JJPMWPB6.js +109 -0
  64. package/dist/chunk-JTHKOYWU.js +663 -0
  65. package/dist/chunk-JVGPR3TS.js +365 -0
  66. package/dist/chunk-JVKB2TBL.js +48 -0
  67. package/dist/chunk-JX7VBZDK.js +31 -0
  68. package/dist/chunk-K3G6JBUC.js +5232 -0
  69. package/dist/chunk-K4YUNKU7.js +589 -0
  70. package/dist/chunk-KBZGE6LN.js +710 -0
  71. package/dist/chunk-KDQJCT2J.js +45 -0
  72. package/dist/chunk-KI4L2DUL.js +560 -0
  73. package/dist/chunk-KJ2ZTQSL.js +47 -0
  74. package/dist/chunk-KSJ5UTP7.js +154 -0
  75. package/dist/chunk-LHOA5DWA.js +28 -0
  76. package/dist/chunk-LX5QWLBL.js +27 -0
  77. package/dist/chunk-LZDDLNCZ.js +279 -0
  78. package/dist/chunk-M2WF6AXG.js +115 -0
  79. package/dist/chunk-M7BS2K6U.js +1 -0
  80. package/dist/chunk-NDBIPFNY.js +102 -0
  81. package/dist/chunk-NEMIWEXC.js +1199 -0
  82. package/dist/chunk-NFAYHPRH.js +323 -0
  83. package/dist/chunk-O37BQ65S.js +21 -0
  84. package/dist/chunk-O74MJHXU.js +187 -0
  85. package/dist/chunk-OJZEX73F.js +112 -0
  86. package/dist/chunk-PAAWLLZX.js +74 -0
  87. package/dist/chunk-PFXFQQRT.js +339 -0
  88. package/dist/chunk-PFZO3C6H.js +14 -0
  89. package/dist/chunk-PHH7LD45.js +113 -0
  90. package/dist/chunk-QRTG4XTI.js +188 -0
  91. package/dist/chunk-QVOVMXJR.js +1351 -0
  92. package/dist/chunk-RCQYT2I4.js +757 -0
  93. package/dist/chunk-RD6DEE36.js +1178 -0
  94. package/dist/chunk-RJIZEJ42.js +30 -0
  95. package/dist/chunk-RPOA7IAE.js +150 -0
  96. package/dist/chunk-SN2DQRL6.js +1172 -0
  97. package/dist/chunk-SQJVFEBV.js +41 -0
  98. package/dist/chunk-SQP64RCS.js +5391 -0
  99. package/dist/chunk-SVT4RNTR.js +49 -0
  100. package/dist/chunk-SWUFN7XM.js +43 -0
  101. package/dist/chunk-SXMVYC5H.js +174 -0
  102. package/dist/chunk-SZ6AYUIZ.js +23 -0
  103. package/dist/chunk-T3KGZBU5.js +3171 -0
  104. package/dist/chunk-TBRL2RCC.js +9 -0
  105. package/dist/chunk-TRT2HRWZ.js +18 -0
  106. package/dist/chunk-TYW2GI5I.js +1077 -0
  107. package/dist/chunk-UIWONWVG.js +4959 -0
  108. package/dist/chunk-VB3H5WWB.js +151 -0
  109. package/dist/chunk-W4A6QA3D.js +258 -0
  110. package/dist/chunk-WBPVKEEE.js +227 -0
  111. package/dist/chunk-WEBMSQP2.js +123 -0
  112. package/dist/chunk-WOEZV3Q4.js +333 -0
  113. package/dist/chunk-WQQON3SV.js +473 -0
  114. package/dist/chunk-WQTOVMGV.js +119 -0
  115. package/dist/chunk-WZO23AWO.js +2779 -0
  116. package/dist/chunk-XBDP4QPW.js +7 -0
  117. package/dist/chunk-XEBHSJLT.js +103 -0
  118. package/dist/chunk-XQ2ECNK3.js +1 -0
  119. package/dist/chunk-XQ2XGIBF.js +2086 -0
  120. package/dist/chunk-Y2GE5TSI.js +355 -0
  121. package/dist/chunk-YBUJCZ57.js +351 -0
  122. package/dist/chunk-YNCVSFGF.js +149 -0
  123. package/dist/chunk-YUPMXOGP.js +282 -0
  124. package/dist/chunk-YY6APYUM.js +49 -0
  125. package/dist/chunk-Z2IJUO45.js +691 -0
  126. package/dist/chunk-ZEHUCJHY.js +129 -0
  127. package/dist/chunk-ZYU2YOJ6.js +1496 -0
  128. package/dist/chunk-ZZ4V3WIZ.js +163 -0
  129. package/dist/coordination/agents/index.js +145 -6260
  130. package/dist/coordination/index.js +365 -34323
  131. package/dist/coordination/mailbox-project-server.js +48 -3220
  132. package/dist/coordination/mailbox-types.d.ts +13 -1
  133. package/dist/coordination/provider-status-tracker.d.ts +4 -0
  134. package/dist/coordination/sqlite-mailbox.d.ts +2 -1
  135. package/dist/core/agent.d.ts +14 -1
  136. package/dist/core/index.js +267 -12479
  137. package/dist/core/model-ref.js +5 -23
  138. package/dist/core/provider-runner.d.ts +36 -1
  139. package/dist/design/index.js +50 -1087
  140. package/dist/execution/index.d.ts +1 -1
  141. package/dist/execution/index.js +1599 -21887
  142. package/dist/execution/prompt-enhancer.js +16 -535
  143. package/dist/execution/retry-policy.d.ts +31 -1
  144. package/dist/extension/index.js +7 -311
  145. package/dist/goal/index.js +33 -3435
  146. package/dist/hooks/index.js +14 -1138
  147. package/dist/hq/index.js +112 -5139
  148. package/dist/hq/protocol.js +21 -668
  149. package/dist/index.js +1410 -92655
  150. package/dist/infrastructure/index.js +64 -4811
  151. package/dist/kernel/events/provider-events.d.ts +15 -0
  152. package/dist/kernel/events/wrongtrace-events.d.ts +26 -0
  153. package/dist/kernel/events.d.ts +16 -7
  154. package/dist/kernel/index.js +14 -992
  155. package/dist/models/index.js +40 -1117
  156. package/dist/notifications/index.js +3 -108
  157. package/dist/observability/index.js +17 -279
  158. package/dist/plugin/index.js +584 -14835
  159. package/dist/prompts/index.js +14 -948
  160. package/dist/registry/index.js +9 -822
  161. package/dist/registry-R23UBE7H.js +14 -0
  162. package/dist/replay/index.js +6 -84
  163. package/dist/review-finding-store-DKAV3AS6.js +21 -0
  164. package/dist/review-report-store-OWY3EC3N.js +19 -0
  165. package/dist/security/index.js +114 -2202
  166. package/dist/session-catalog/index.js +36 -2103
  167. package/dist/session-catalog/project-server.js +42 -1435
  168. package/dist/skills/index.js +35 -1578
  169. package/dist/storage/index.js +182 -15600
  170. package/dist/tasking/index.js +10 -676
  171. package/dist/tools/index.js +52 -9589
  172. package/dist/types/config/autonomy.d.ts +5 -0
  173. package/dist/types/config/tools.d.ts +2 -2
  174. package/dist/types/config/ui.d.ts +1 -1
  175. package/dist/types/errors.d.ts +18 -0
  176. package/dist/types/index.js +121 -1341
  177. package/dist/types/provider-runner.d.ts +8 -0
  178. package/dist/types/session-markers.js +8 -144
  179. package/dist/utils/error.js +3 -4
  180. package/dist/utils/expect-defined.js +3 -9
  181. package/dist/utils/heap-watchdog.js +6 -706
  182. package/dist/utils/index.js +269 -5685
  183. package/dist/utils/sage-output-block.js +5 -42
  184. package/dist/wiring/proxy-rewrite.d.ts +92 -4
  185. package/dist/wiring/proxy-rewrite.js +20 -61
  186. package/dist/worktree/index.js +6 -887
  187. package/package.json +4 -4
@@ -1,1269 +1,57 @@
1
- var __defProp = Object.defineProperty;
2
- var __getOwnPropNames = Object.getOwnPropertyNames;
3
- var __esm = (fn, res, err) => function __init() {
4
- if (err) throw err[0];
5
- try {
6
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
7
- } catch (e) {
8
- throw err = [e], e;
9
- }
10
- };
11
- var __export = (target, all) => {
12
- for (var name in all)
13
- __defProp(target, name, { get: all[name], enumerable: true });
14
- };
15
-
16
- // src/session-catalog/protocol.ts
17
- function encodeSessionCatalogMessage(message) {
18
- return `${JSON.stringify(message)}
19
- `;
20
- }
21
- var SESSION_CATALOG_PROTOCOL_VERSION, SESSION_CATALOG_MAX_FRAME_CHARS;
22
- var init_protocol = __esm({
23
- "src/session-catalog/protocol.ts"() {
24
- "use strict";
25
- SESSION_CATALOG_PROTOCOL_VERSION = 1;
26
- SESSION_CATALOG_MAX_FRAME_CHARS = 4 * 1024 * 1024;
27
- }
28
- });
29
-
30
- // src/session-catalog/endpoint.ts
31
- import { createHash as createHash2 } from "node:crypto";
32
- import * as os3 from "node:os";
33
- import * as path9 from "node:path";
34
- function normalizedPath(value) {
35
- const resolved = path9.resolve(value);
36
- return process.platform === "win32" ? resolved.toLowerCase() : resolved;
37
- }
38
- function sessionCatalogProjectServerKey(projectDir) {
39
- return createHash2("sha256").update(normalizedPath(projectDir)).digest("hex").slice(0, 24);
40
- }
41
- function sessionCatalogProjectServerEndpoint(projectDir) {
42
- const key = sessionCatalogProjectServerKey(projectDir);
43
- if (process.platform === "win32") {
44
- return `\\\\.\\pipe\\wrongstack-session-catalog-v${SESSION_CATALOG_PROTOCOL_VERSION}-${key}`;
45
- }
46
- return path9.join(os3.tmpdir(), `wssc-v${SESSION_CATALOG_PROTOCOL_VERSION}`, `${key}.sock`);
47
- }
48
- function sessionCatalogProjectServerMetadataPath(projectDir) {
49
- return path9.join(projectDir, SESSION_CATALOG_METADATA_FILE);
50
- }
51
- var SESSION_CATALOG_METADATA_FILE;
52
- var init_endpoint = __esm({
53
- "src/session-catalog/endpoint.ts"() {
54
- "use strict";
55
- init_protocol();
56
- SESSION_CATALOG_METADATA_FILE = ".session-catalog-server.json";
57
- }
58
- });
59
-
60
- // src/session-catalog/client.ts
61
- import { spawn } from "node:child_process";
62
- import * as fs7 from "node:fs";
63
- import * as net from "node:net";
64
- import * as path10 from "node:path";
65
- import { fileURLToPath } from "node:url";
66
- function locateServer(moduleUrl, exists) {
67
- for (const candidate of [
68
- "./project-server.js",
69
- "../session-catalog/project-server.js",
70
- "./session-catalog/project-server.js",
71
- "../../dist/session-catalog/project-server.js"
72
- ]) {
73
- try {
74
- const url = new URL(candidate, moduleUrl);
75
- if (url.protocol === "file:" && exists(fileURLToPath(url))) return url;
76
- } catch {
77
- }
78
- }
79
- return null;
80
- }
81
- function resolveSessionCatalogDaemonAvailability(moduleUrl = import.meta.url, exists = fs7.existsSync) {
82
- if (process.env["WRONGSTACK_SESSION_CATALOG_INLINE"] || process.env["WRONGSTACK_SESSION_CATALOG_SERVER"] === "0")
83
- return { kind: "inline-requested" };
84
- const url = locateServer(moduleUrl, exists);
85
- return url ? { kind: "available", url } : { kind: "missing-build" };
86
- }
87
- function resolveSessionCatalogProjectServerUrl(moduleUrl = import.meta.url, exists = fs7.existsSync) {
88
- const availability = resolveSessionCatalogDaemonAvailability(moduleUrl, exists);
89
- return availability.kind === "available" ? availability.url : null;
90
- }
91
- function normalize2(value) {
92
- const resolved = path10.resolve(value);
93
- return process.platform === "win32" ? resolved.toLowerCase() : resolved;
94
- }
95
- function delay(ms) {
96
- return new Promise((resolve9) => setTimeout(resolve9, ms));
97
- }
98
- var CONNECT_TIMEOUT_MS, START_TIMEOUT_MS, CALL_TIMEOUT_MS, MAX_PENDING_REQUESTS, MAX_EVENT_LISTENERS, SessionCatalogProjectClient;
99
- var init_client = __esm({
100
- "src/session-catalog/client.ts"() {
101
- "use strict";
102
- init_endpoint();
103
- init_protocol();
104
- CONNECT_TIMEOUT_MS = 750;
105
- START_TIMEOUT_MS = 1e4;
106
- CALL_TIMEOUT_MS = 3e4;
107
- MAX_PENDING_REQUESTS = 1024;
108
- MAX_EVENT_LISTENERS = 64;
109
- SessionCatalogProjectClient = class {
110
- constructor(options) {
111
- this.options = options;
112
- this.endpoint = sessionCatalogProjectServerEndpoint(options.projectDir);
113
- }
114
- options;
115
- endpoint;
116
- socket = null;
117
- info = null;
118
- buffer = "";
119
- connecting = null;
120
- connectResolve = null;
121
- connectReject = null;
122
- authToken;
123
- nextId = 1;
124
- pending = /* @__PURE__ */ new Map();
125
- eventListeners = /* @__PURE__ */ new Set();
126
- reconnectTimer;
127
- explicitlyClosed = false;
128
- async call(op, args, options = {}) {
129
- await this.ensureConnected(true);
130
- return this.request({ type: "request", op, args }, options.timeoutMs ?? CALL_TIMEOUT_MS);
131
- }
132
- /**
133
- * Call an already-running project daemon without starting one.
134
- *
135
- * Cross-project discovery must use this path: observing another project is
136
- * never sufficient authority to wake that project's IPC owner.
137
- */
138
- async callExisting(op, args, options = {}) {
139
- await this.ensureConnected(false);
140
- return this.request({ type: "request", op, args }, options.timeoutMs ?? CALL_TIMEOUT_MS);
141
- }
142
- ping() {
143
- return this.call("ping", {}, { timeoutMs: 3e3 });
144
- }
145
- async subscribe(listener) {
146
- if (!this.eventListeners.has(listener) && this.eventListeners.size >= MAX_EVENT_LISTENERS) {
147
- throw new Error(`Session Catalog listener limit reached (${MAX_EVENT_LISTENERS})`);
148
- }
149
- this.explicitlyClosed = false;
150
- this.eventListeners.add(listener);
151
- try {
152
- await this.call("subscribe", {});
153
- } catch (error) {
154
- this.eventListeners.delete(listener);
155
- throw error;
156
- }
157
- return async () => {
158
- this.eventListeners.delete(listener);
159
- if (this.eventListeners.size === 0)
160
- await this.callExisting("unsubscribe", {}).catch(() => void 0);
161
- };
162
- }
163
- async shutdown(reason) {
164
- try {
165
- await this.ensureConnected(false);
166
- } catch {
167
- return { stopped: false };
168
- }
169
- const result = await this.request({ type: "shutdown", ...reason !== void 0 ? { reason } : {} }, 5e3).catch(
170
- () => ({ stopped: false })
171
- );
172
- if (result.stopped) {
173
- const metadataPath = sessionCatalogProjectServerMetadataPath(this.options.projectDir);
174
- const deadline = Date.now() + 5e3;
175
- while (fs7.existsSync(metadataPath) && Date.now() < deadline) await delay(20);
176
- if (result.pid && result.pid !== process.pid) {
177
- while (Date.now() < deadline) {
178
- try {
179
- process.kill(result.pid, 0);
180
- await delay(20);
181
- } catch {
182
- break;
183
- }
184
- }
185
- }
186
- }
187
- return result;
188
- }
189
- async close() {
190
- this.explicitlyClosed = true;
191
- if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
192
- this.reconnectTimer = void 0;
193
- const socket = this.socket;
194
- this.socket = null;
195
- this.info = null;
196
- if (socket && !socket.destroyed)
197
- await new Promise((resolve9) => {
198
- socket.once("close", resolve9);
199
- socket.end();
200
- });
201
- }
202
- async ensureConnected(spawnIfMissing) {
203
- if (this.socket && !this.socket.destroyed && this.info) return;
204
- if (this.connecting) return this.connecting;
205
- this.connecting = this.connectWithElection(spawnIfMissing).finally(() => {
206
- this.connecting = null;
207
- });
208
- return this.connecting;
209
- }
210
- async connectWithElection(spawnIfMissing) {
211
- const deadline = Date.now() + (spawnIfMissing ? START_TIMEOUT_MS : CONNECT_TIMEOUT_MS);
212
- let spawned = false;
213
- let lastError = new Error("Session Catalog project server unavailable");
214
- while (Date.now() < deadline) {
215
- try {
216
- await this.connectOnce();
217
- return;
218
- } catch (error) {
219
- lastError = error;
220
- }
221
- if (!spawnIfMissing) break;
222
- if (!spawned) {
223
- this.spawnDetached();
224
- spawned = true;
225
- }
226
- await delay(75);
227
- }
228
- throw lastError;
229
- }
230
- connectOnce() {
231
- this.socket?.destroy();
232
- this.socket = null;
233
- this.info = null;
234
- this.authToken = void 0;
235
- this.buffer = "";
236
- return new Promise((resolve9, reject) => {
237
- const socket = net.createConnection(this.endpoint);
238
- this.socket = socket;
239
- socket.setEncoding("utf8");
240
- const timer = setTimeout(() => {
241
- reject(new Error("Session Catalog handshake timed out"));
242
- socket.destroy();
243
- }, CONNECT_TIMEOUT_MS);
244
- timer.unref?.();
245
- this.connectResolve = () => {
246
- clearTimeout(timer);
247
- this.connectResolve = null;
248
- this.connectReject = null;
249
- resolve9();
250
- };
251
- this.connectReject = (error) => {
252
- clearTimeout(timer);
253
- this.connectResolve = null;
254
- this.connectReject = null;
255
- reject(error);
256
- };
257
- socket.on("data", (chunk) => this.onData(socket, chunk));
258
- socket.on("error", (error) => {
259
- if (!this.info) this.connectReject?.(error);
260
- });
261
- socket.on("close", () => this.onClose(socket));
262
- });
263
- }
264
- currentAuthToken() {
265
- if (this.authToken === void 0) {
266
- try {
267
- const parsed = JSON.parse(
268
- fs7.readFileSync(sessionCatalogProjectServerMetadataPath(this.options.projectDir), "utf8")
269
- );
270
- if (typeof parsed.authToken === "string" && parsed.authToken)
271
- this.authToken = parsed.authToken;
272
- } catch {
273
- }
274
- }
275
- return this.authToken;
276
- }
277
- request(message, timeoutMs) {
278
- const socket = this.socket;
279
- if (!socket || socket.destroyed)
280
- return Promise.reject(new Error("Session Catalog connection is unavailable"));
281
- const id = this.nextId++;
282
- if (this.pending.size >= MAX_PENDING_REQUESTS) {
283
- return Promise.reject(
284
- new Error(`Session Catalog pending request limit reached (${MAX_PENDING_REQUESTS})`)
285
- );
286
- }
287
- const encoded = encodeSessionCatalogMessage({
288
- ...message,
289
- id,
290
- authToken: this.currentAuthToken()
291
- });
292
- if (encoded.length > SESSION_CATALOG_MAX_FRAME_CHARS)
293
- return Promise.reject(new Error("Session Catalog request exceeded frame limit"));
294
- return new Promise((resolve9, reject) => {
295
- const timer = setTimeout(() => {
296
- const pending = this.pending.get(id);
297
- if (!pending) return;
298
- this.pending.delete(id);
299
- pending.reject(
300
- new Error(
301
- `Session Catalog ${message.type === "request" ? message.op : message.type} timed out`
302
- )
303
- );
304
- }, timeoutMs);
305
- timer.unref?.();
306
- this.pending.set(id, { resolve: resolve9, reject, timer });
307
- socket.write(encoded);
308
- });
309
- }
310
- onData(socket, chunk) {
311
- if (socket !== this.socket) return;
312
- this.buffer += chunk;
313
- if (this.buffer.length > SESSION_CATALOG_MAX_FRAME_CHARS) {
314
- socket.destroy(new Error("Session Catalog response exceeded frame limit"));
315
- return;
316
- }
317
- while (true) {
318
- const newline = this.buffer.indexOf("\n");
319
- if (newline < 0) return;
320
- const line = this.buffer.slice(0, newline);
321
- this.buffer = this.buffer.slice(newline + 1);
322
- if (!line) continue;
323
- try {
324
- this.onMessage(JSON.parse(line));
325
- } catch {
326
- socket.destroy(new Error("Invalid Session Catalog response"));
327
- return;
328
- }
329
- }
330
- }
331
- onMessage(message) {
332
- if (message.type === "hello") {
333
- if (message.protocolVersion !== SESSION_CATALOG_PROTOCOL_VERSION) {
334
- this.connectReject?.(
335
- new Error(
336
- `Session Catalog protocol mismatch: client=${SESSION_CATALOG_PROTOCOL_VERSION}, server=${message.protocolVersion}`
337
- )
338
- );
339
- this.socket?.destroy();
340
- return;
341
- }
342
- if (normalize2(message.projectDir) !== normalize2(this.options.projectDir) || normalize2(message.projectRoot) !== normalize2(this.options.projectRoot)) {
343
- this.connectReject?.(new Error("Session Catalog project identity mismatch"));
344
- this.socket?.destroy();
345
- return;
346
- }
347
- this.info = message;
348
- this.connectResolve?.();
349
- return;
350
- }
351
- if (message.type === "event") {
352
- for (const listener of this.eventListeners) {
353
- try {
354
- listener(message.event);
355
- } catch {
356
- }
357
- }
358
- return;
359
- }
360
- const pending = this.pending.get(message.id);
361
- if (!pending) return;
362
- this.pending.delete(message.id);
363
- clearTimeout(pending.timer);
364
- if (message.ok) pending.resolve(message.result);
365
- else {
366
- const error = new Error(message.error);
367
- if (message.errorName) error.name = message.errorName;
368
- pending.reject(error);
369
- }
370
- }
371
- onClose(socket) {
372
- if (socket !== this.socket) return;
373
- this.socket = null;
374
- this.info = null;
375
- this.authToken = void 0;
376
- const error = new Error("Session Catalog connection closed");
377
- this.connectReject?.(error);
378
- this.connectResolve = null;
379
- this.connectReject = null;
380
- for (const pending of this.pending.values()) {
381
- clearTimeout(pending.timer);
382
- pending.reject(error);
383
- }
384
- this.pending.clear();
385
- this.scheduleSubscriptionReconnect();
386
- }
387
- scheduleSubscriptionReconnect() {
388
- if (this.explicitlyClosed || this.eventListeners.size === 0 || this.reconnectTimer) return;
389
- this.reconnectTimer = setTimeout(() => {
390
- this.reconnectTimer = void 0;
391
- void this.call("subscribe", {}).catch(() => this.scheduleSubscriptionReconnect());
392
- }, 250);
393
- this.reconnectTimer.unref?.();
394
- }
395
- spawnDetached() {
396
- const url = resolveSessionCatalogProjectServerUrl();
397
- if (!url) throw new Error("Built Session Catalog project server is unavailable");
398
- const child = spawn(
399
- process.execPath,
400
- [
401
- fileURLToPath(url),
402
- "--project-dir",
403
- this.options.projectDir,
404
- "--project-root",
405
- this.options.projectRoot
406
- ],
407
- { detached: true, stdio: "ignore", windowsHide: true, env: process.env }
408
- );
409
- child.unref();
410
- }
411
- };
412
- }
413
- });
414
-
415
- // src/session-catalog/registry.ts
416
- var registry_exports = {};
417
- __export(registry_exports, {
418
- ProjectSessionRegistry: () => ProjectSessionRegistry,
419
- getProjectSessionRegistry: () => getProjectSessionRegistry,
420
- hasProjectSessionRegistry: () => hasProjectSessionRegistry
421
- });
422
- import { randomUUID } from "node:crypto";
423
- import * as fs8 from "node:fs/promises";
424
- import * as path11 from "node:path";
425
- function getProjectSessionRegistry(globalRoot) {
426
- const key = globalRoot !== void 0 ? path11.resolve(globalRoot) : lastRegistryKey;
427
- if (!key)
428
- throw new Error("SessionRegistry not initialized. Call getSessionRegistry(globalRoot) first.");
429
- let registry = registries.get(key);
430
- if (!registry) {
431
- if (registries.size >= MAX_GLOBAL_ROOTS) {
432
- const idle = [...registries.entries()].find(([, candidate]) => !candidate.ownsSession());
433
- if (!idle)
434
- throw new Error(`Session Registry global-root limit reached (${MAX_GLOBAL_ROOTS})`);
435
- registries.delete(idle[0]);
436
- void idle[1].dispose();
437
- }
438
- registry = new ProjectSessionRegistry(key);
439
- registries.set(key, registry);
440
- } else {
441
- registries.delete(key);
442
- registries.set(key, registry);
443
- }
444
- lastRegistryKey = key;
445
- return registry;
446
- }
447
- function hasProjectSessionRegistry(globalRoot) {
448
- if (globalRoot === void 0) return registries.size > 0;
449
- return registries.has(path11.resolve(globalRoot));
450
- }
451
- var HEARTBEAT_INTERVAL_MS, AGENT_WRITE_THROTTLE_MS, MAX_PROJECT_CLIENTS, MAX_GLOBAL_ROOTS, ProjectSessionRegistry, registries, lastRegistryKey;
452
- var init_registry = __esm({
453
- "src/session-catalog/registry.ts"() {
454
- "use strict";
455
- init_client();
456
- init_endpoint();
457
- HEARTBEAT_INTERVAL_MS = 5e3;
458
- AGENT_WRITE_THROTTLE_MS = 300;
459
- MAX_PROJECT_CLIENTS = 128;
460
- MAX_GLOBAL_ROOTS = 16;
461
- ProjectSessionRegistry = class {
462
- constructor(globalRoot) {
463
- this.globalRoot = globalRoot;
464
- }
465
- globalRoot;
466
- instanceId = randomUUID();
467
- clients = /* @__PURE__ */ new Map();
468
- current;
469
- heartbeatTimer;
470
- agentRevision = 0;
471
- pendingAgents;
472
- agentTimer;
473
- lastAgentWriteAt = 0;
474
- bindingKey(projectDir) {
475
- const resolved = path11.resolve(projectDir);
476
- return process.platform === "win32" ? resolved.toLowerCase() : resolved;
477
- }
478
- async closeBinding(binding) {
479
- if (this.current?.binding === binding) return;
480
- const key = this.bindingKey(binding.projectDir);
481
- if (this.clients.get(key) === binding) this.clients.delete(key);
482
- await binding.client.close();
483
- }
484
- binding(projectSlug2, projectRoot) {
485
- const projectDir = path11.join(this.globalRoot, "projects", projectSlug2);
486
- const key = this.bindingKey(projectDir);
487
- let binding = this.clients.get(key);
488
- if (!binding) {
489
- if (this.clients.size >= MAX_PROJECT_CLIENTS) {
490
- const oldest = [...this.clients.entries()].find(
491
- ([, candidate]) => candidate !== this.current?.binding
492
- );
493
- if (oldest) {
494
- this.clients.delete(oldest[0]);
495
- void oldest[1].client.close();
496
- }
497
- }
498
- binding = {
499
- projectDir,
500
- projectRoot: path11.resolve(projectRoot),
501
- client: new SessionCatalogProjectClient({ projectDir, projectRoot })
502
- };
503
- this.clients.set(key, binding);
504
- } else {
505
- this.clients.delete(key);
506
- this.clients.set(key, binding);
507
- }
508
- return binding;
509
- }
510
- fullEntry(entry) {
511
- const agents = entry.agents ?? [];
512
- return {
513
- ...entry,
514
- status: agents.some((agent) => agent.status !== "idle") ? "active" : "idle",
515
- lastHeartbeatAt: (/* @__PURE__ */ new Date()).toISOString(),
516
- agentCount: agents.length,
517
- agents
518
- };
519
- }
520
- async register(entry) {
521
- const full = this.fullEntry(entry);
522
- if (this.current?.entry.sessionId === full.sessionId && this.current.entry.pid === full.pid) {
523
- this.current.entry = full;
524
- this.current.credential = await this.current.binding.client.call("heartbeat", {
525
- credential: this.current.credential,
526
- status: full.status
527
- });
528
- return;
529
- }
530
- const nextBinding = this.binding(full.projectSlug, full.projectRoot);
531
- let nextCredential;
532
- try {
533
- nextCredential = await nextBinding.client.call("claim_new", {
534
- entry: full,
535
- ownerInstanceId: this.instanceId
536
- });
537
- } catch (error) {
538
- await this.closeBinding(nextBinding).catch(() => void 0);
539
- throw error;
540
- }
541
- const previous = this.current;
542
- this.current = { binding: nextBinding, credential: nextCredential, entry: full };
543
- this.agentRevision = 0;
544
- this.cancelAgentTimer();
545
- this.startHeartbeat();
546
- if (previous) {
547
- await previous.binding.client.call("release", { credential: previous.credential }).catch(() => void 0);
548
- if (previous.binding !== nextBinding) await this.closeBinding(previous.binding);
549
- }
550
- }
551
- /** Reserve before transcript hydration; activation swaps ownership only after the writer opened. */
552
- async reserveResume(target) {
553
- const binding = this.binding(target.projectSlug, target.projectRoot);
554
- let reservation;
555
- try {
556
- reservation = await binding.client.call("reserve_resume", {
557
- targetSessionId: target.sessionId,
558
- requesterInstanceId: this.instanceId,
559
- ...this.current ? { currentSessionId: this.current.entry.sessionId } : {}
560
- });
561
- } catch (error) {
562
- await this.closeBinding(binding).catch(() => void 0);
563
- throw error;
564
- }
565
- let settled = false;
566
- return {
567
- reservation,
568
- activate: async (registration) => {
569
- if (settled) throw new Error("Resume reservation is already settled");
570
- const entry = this.fullEntry(registration);
571
- const credential = await binding.client.call("activate_reservation", {
572
- reservation,
573
- entry
574
- });
575
- const previous = this.current;
576
- this.current = { binding, credential, entry };
577
- settled = true;
578
- this.agentRevision = 0;
579
- this.cancelAgentTimer();
580
- this.startHeartbeat();
581
- if (previous) {
582
- await previous.binding.client.call("release", { credential: previous.credential }).catch(() => void 0);
583
- if (previous.binding !== binding) await this.closeBinding(previous.binding);
584
- }
585
- },
586
- cancel: async () => {
587
- if (settled) return;
588
- settled = true;
589
- try {
590
- await binding.client.call("cancel_reservation", {
591
- reservationId: reservation.reservationId,
592
- requesterInstanceId: this.instanceId
593
- }).catch(() => void 0);
594
- } finally {
595
- await this.closeBinding(binding).catch(() => void 0);
596
- }
597
- }
598
- };
599
- }
600
- async updateAgents(agents) {
601
- if (!this.current) return;
602
- this.pendingAgents = agents;
603
- this.current.entry = {
604
- ...this.current.entry,
605
- agents,
606
- agentCount: agents.length,
607
- status: agents.some((agent) => agent.status !== "idle") ? "active" : "idle",
608
- lastHeartbeatAt: (/* @__PURE__ */ new Date()).toISOString()
609
- };
610
- const elapsed = Date.now() - this.lastAgentWriteAt;
611
- if (!this.agentTimer && elapsed >= AGENT_WRITE_THROTTLE_MS) {
612
- await this.flushAgents();
613
- return;
614
- }
615
- if (!this.agentTimer) {
616
- this.agentTimer = setTimeout(
617
- () => {
618
- this.agentTimer = void 0;
619
- void this.flushAgents();
620
- },
621
- Math.max(0, AGENT_WRITE_THROTTLE_MS - elapsed)
622
- );
623
- this.agentTimer.unref?.();
624
- }
625
- }
626
- async flushAgents() {
627
- const agents = this.pendingAgents;
628
- const current = this.current;
629
- if (!agents || !current) return;
630
- this.pendingAgents = void 0;
631
- this.lastAgentWriteAt = Date.now();
632
- const revision = ++this.agentRevision;
633
- await current.binding.client.call("publish_agents", {
634
- credential: current.credential,
635
- revision,
636
- agents
637
- });
638
- }
639
- async markClosing() {
640
- this.stopHeartbeat();
641
- this.cancelAgentTimer();
642
- if (this.current)
643
- await this.current.binding.client.call("mark_closing", {
644
- credential: this.current.credential
645
- });
646
- }
647
- async unregister() {
648
- this.stopHeartbeat();
649
- this.cancelAgentTimer();
650
- const current = this.current;
651
- this.current = void 0;
652
- if (current) {
653
- try {
654
- await current.binding.client.call("release", { credential: current.credential });
655
- } finally {
656
- await this.closeBinding(current.binding);
657
- }
658
- }
659
- }
660
- async list() {
661
- const projectsDir = path11.join(this.globalRoot, "projects");
662
- let directories = [];
663
- try {
664
- directories = (await fs8.readdir(projectsDir, { withFileTypes: true })).filter((entry) => entry.isDirectory()).slice(0, 1e3).map((entry) => entry.name);
665
- } catch {
666
- return [];
667
- }
668
- const snapshots = await Promise.all(
669
- directories.map(async (slug) => {
670
- const projectDir = path11.join(projectsDir, slug);
671
- let client;
672
- try {
673
- const metadata = JSON.parse(
674
- await fs8.readFile(sessionCatalogProjectServerMetadataPath(projectDir), "utf8")
675
- );
676
- if (typeof metadata.projectRoot !== "string" || !metadata.projectRoot) return [];
677
- client = new SessionCatalogProjectClient({
678
- projectDir,
679
- projectRoot: metadata.projectRoot
680
- });
681
- return await client.callExisting("list_live", {});
682
- } catch {
683
- return [];
684
- } finally {
685
- await client?.close().catch(() => void 0);
686
- }
687
- })
688
- );
689
- return snapshots.flat();
690
- }
691
- async listByProject(projectSlug2) {
692
- const current = this.current;
693
- if (current && path11.basename(current.binding.projectDir) === projectSlug2) {
694
- return current.binding.client.call("list_live", {}).catch(() => []);
695
- }
696
- const projectDir = path11.join(this.globalRoot, "projects", projectSlug2);
697
- let client;
698
- try {
699
- const metadata = JSON.parse(
700
- await fs8.readFile(sessionCatalogProjectServerMetadataPath(projectDir), "utf8")
701
- );
702
- if (typeof metadata.projectRoot !== "string") return [];
703
- client = new SessionCatalogProjectClient({ projectDir, projectRoot: metadata.projectRoot });
704
- return await client.callExisting("list_live", {}).catch(() => []);
705
- } catch {
706
- return [];
707
- } finally {
708
- await client?.close().catch(() => void 0);
709
- }
710
- }
711
- async get(sessionId) {
712
- return (await this.list()).find((entry) => entry.sessionId === sessionId);
713
- }
714
- subscribeProject(projectSlug2, projectRoot, listener) {
715
- const binding = this.binding(projectSlug2, projectRoot);
716
- return binding.client.subscribe(listener).then((unsubscribe) => async () => {
717
- await unsubscribe();
718
- await this.closeBinding(binding);
719
- });
720
- }
721
- get registryPath() {
722
- return path11.join(this.globalRoot, "projects");
723
- }
724
- async dispose() {
725
- await this.unregister().catch(() => void 0);
726
- await Promise.all([...this.clients.values()].map((binding) => binding.client.close()));
727
- this.clients.clear();
728
- }
729
- /** Whether this facade currently owns a live session lease. */
730
- ownsSession() {
731
- return this.current !== void 0;
732
- }
733
- startHeartbeat() {
734
- this.stopHeartbeat();
735
- this.heartbeatTimer = setInterval(() => {
736
- void this.heartbeat();
737
- }, HEARTBEAT_INTERVAL_MS);
738
- this.heartbeatTimer.unref?.();
739
- }
740
- stopHeartbeat() {
741
- if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
742
- this.heartbeatTimer = void 0;
743
- }
744
- cancelAgentTimer() {
745
- if (this.agentTimer) clearTimeout(this.agentTimer);
746
- this.agentTimer = void 0;
747
- this.pendingAgents = void 0;
748
- }
749
- async heartbeat() {
750
- const current = this.current;
751
- if (!current) return;
752
- try {
753
- const credential = await current.binding.client.call("heartbeat", {
754
- credential: current.credential,
755
- status: current.entry.status
756
- });
757
- if (this.current === current) current.credential = credential;
758
- } catch {
759
- }
760
- }
761
- };
762
- registries = /* @__PURE__ */ new Map();
763
- }
764
- });
765
-
766
- // src/infrastructure/logger.ts
767
- import * as fsp from "node:fs/promises";
768
- import * as path from "node:path";
769
-
770
- // src/utils/term.ts
771
- var hasStdout = () => typeof process !== "undefined" && !!process.stdout;
772
- function isStdoutTTY() {
773
- return hasStdout() && Boolean(process.stdout.isTTY);
774
- }
775
- var activeOutputGuard = null;
776
- function writeTo(s, stream) {
777
- if (!stream || typeof stream.write !== "function") return false;
778
- const guard = activeOutputGuard;
779
- if (!guard) {
780
- stream.write(s);
781
- return true;
782
- }
783
- guard.suspend();
784
- stream.write(s);
785
- guard.resume();
786
- return true;
787
- }
788
- function writeErr(s, stream = process.stderr) {
789
- return writeTo(s, stream);
790
- }
791
- var ESCAPE_TERMINATOR = Object.freeze({
792
- BEL: "\x07",
793
- // OSC 0 / OSC 2 (title)
794
- ST: "\x1B\\"
795
- // DCS / strict OSC
796
- });
797
-
798
- // src/utils/color.ts
799
- var isColorTty = () => {
800
- if (envFlag(process.env.NO_COLOR)) return false;
801
- if (envFlag(process.env.FORCE_COLOR)) return true;
802
- return isStdoutTTY();
803
- };
804
- function envFlag(value) {
805
- if (value === void 0) return false;
806
- if (value.trim() === "") return false;
807
- return !/^(0|false|no|off)$/i.test(value.trim());
808
- }
809
- var COLOR = isColorTty();
810
- var wrap = (open, close) => (s) => COLOR ? `\x1B[${open}m${s}\x1B[${close}m` : s;
811
- var color = {
812
- reset: wrap("0", "0"),
813
- bold: wrap("1", "22"),
814
- dim: wrap("2", "22"),
815
- italic: wrap("3", "23"),
816
- underline: wrap("4", "24"),
817
- red: wrap("31", "39"),
818
- green: wrap("32", "39"),
819
- yellow: wrap("33", "39"),
820
- blue: wrap("34", "39"),
821
- magenta: wrap("35", "39"),
822
- cyan: wrap("36", "39"),
823
- gray: wrap("90", "39"),
824
- amber: wrap("38;5;214", "39"),
825
- pink: wrap("38;5;205", "39"),
826
- bgRed: wrap("41", "49"),
827
- bgGreen: wrap("42", "49")
828
- };
829
-
830
- // src/infrastructure/logger.ts
831
- var LEVEL_RANK = {
832
- error: 0,
833
- warn: 1,
834
- info: 2,
835
- debug: 3,
836
- trace: 4
837
- };
838
- var COLORS = {
839
- error: color.red,
840
- warn: color.yellow,
841
- info: color.cyan,
842
- debug: color.gray,
843
- trace: color.dim
844
- };
845
- var LOG_LEVELS = /* @__PURE__ */ new Set(["error", "warn", "info", "debug", "trace"]);
846
- var LOG_FORMATS = /* @__PURE__ */ new Set(["pretty", "json"]);
847
- var DefaultLogger = class _DefaultLogger {
848
- /** How many file writes between rotation size checks (statSync is not free). */
849
- static ROTATE_CHECK_EVERY = 100;
850
- static MAX_PENDING_FILE_WRITES = 2e3;
851
- static MAX_PENDING_FILE_BYTES = 8 * 1024 * 1024;
852
- level;
853
- file;
854
- bindings;
855
- format;
856
- stderr;
857
- maxFileBytes;
858
- writesSinceRotateCheck = 0;
859
- /**
860
- * Serialized async tail for file writes. Every appendFile (and any
861
- * chained rotation) is awaited through this promise so file I/O
862
- * never overlaps itself — preserving the per-line ordering the
863
- * sync version had, but without blocking the caller thread. Any
864
- * rejection is swallowed (`catch(() => {})`) because logging must
865
- * never crash the host.
866
- *
867
- * Children share the parent's tail: `child.tail === parent.tail`
868
- * for the lifetime of the chain. Read/write access goes through
869
- * `_tail` so that, when a child has been wired to a parent, both
870
- * `enqueueRotate` and `log` always observe the parent's current tail
871
- * rather than a stale snapshot taken at `child()` time.
872
- */
873
- tail = Promise.resolve();
874
- parent = null;
875
- pendingFileWrites = 0;
876
- pendingFileBytes = 0;
877
- get root() {
878
- return this.parent ? this.parent.root : this;
879
- }
880
- /**
881
- * Resolve the current tail. For the root logger this is the field;
882
- * for a child logger we always read through the parent so that a
883
- * child's appends land on the parent's most recent tail, and a
884
- * parent's `flush()` waits for everything the child chained.
885
- */
886
- get _tail() {
887
- return this.parent ? this.parent._tail : this.tail;
888
- }
889
- set _tail(next) {
890
- if (this.parent) this.parent.tail = next;
891
- else this.tail = next;
892
- }
893
- constructor(opts = {}) {
894
- this.level = opts.level ?? parseLogLevel(process.env.WRONGSTACK_LOG_LEVEL);
895
- this.file = opts.file;
896
- this.bindings = opts.bindings ?? {};
897
- this.format = opts.format ?? parseLogFormat(process.env.WRONGSTACK_LOG_FORMAT);
898
- this.stderr = opts.stderr !== false;
899
- this.maxFileBytes = opts.maxFileBytes ?? 10 * 1024 * 1024;
900
- if (this.file) {
901
- const dir = path.dirname(this.file);
902
- this._tail = this._tail.then(async () => {
903
- await fsp.mkdir(dir, { recursive: true });
904
- }).catch(() => void 0);
905
- }
906
- }
907
- error(msg, ctx) {
908
- this.log("error", msg, ctx);
909
- }
910
- warn(msg, ctx) {
911
- this.log("warn", msg, ctx);
912
- }
913
- info(msg, ctx) {
914
- this.log("info", msg, ctx);
915
- }
916
- debug(msg, ctx) {
917
- this.log("debug", msg, ctx);
918
- }
919
- trace(msg, ctx) {
920
- this.log("trace", msg, ctx);
921
- }
922
- child(bindings) {
923
- const child = Object.create(_DefaultLogger.prototype);
924
- child.level = this.level;
925
- child.file = this.file;
926
- child.bindings = { ...this.bindings, ...bindings };
927
- child.format = this.format;
928
- child.stderr = this.stderr;
929
- child.maxFileBytes = this.maxFileBytes;
930
- child.parent = this;
931
- child.writesSinceRotateCheck = this.writesSinceRotateCheck;
932
- return child;
933
- }
934
- /**
935
- * Wait until all queued file writes (and any pending rotation) have
936
- * completed. `log()` is fire-and-forget by design — the caller never
937
- * blocks on disk — so tests, shutdown handlers, and processes that
938
- * need a deterministic "everything is on disk now" guarantee should
939
- * `await logger.flush()` before reading the file or exiting.
940
- */
941
- flush() {
942
- return this._tail;
943
- }
944
- /**
945
- * Size-based rotation: when the file outgrows `maxFileBytes`, rename it to
946
- * `<file>.1` (dropping the previous `.1`) so the live file restarts empty.
947
- * Checked on the first write and every ROTATE_CHECK_EVERY writes after.
948
- * Best-effort: a rename can fail on Windows while another process holds
949
- * the file — the next check retries. Multiple processes appending to the
950
- * same log all run this check; whoever crosses the threshold first wins.
951
- *
952
- * Async: the rotation runs on the file-write tail (so its writes don't
953
- * interleave with the next append), and the caller never blocks on a
954
- * statSync / renameSync syscall on the hot log path.
955
- */
956
- enqueueRotate(file) {
957
- if (this.writesSinceRotateCheck++ % _DefaultLogger.ROTATE_CHECK_EVERY !== 0) return;
958
- this._tail = this._tail.then(async () => {
959
- let st;
960
- try {
961
- st = await fsp.stat(file);
962
- } catch {
963
- return;
964
- }
965
- if (st.size < this.maxFileBytes) return;
966
- try {
967
- await fsp.rm(`${file}.1`, { force: true });
968
- await fsp.rename(file, `${file}.1`);
969
- } catch {
970
- }
971
- }).catch(() => void 0);
972
- }
973
- log(level, msg, ctx) {
974
- const r = LEVEL_RANK[level];
975
- const allowed = LEVEL_RANK[this.level];
976
- if (r > allowed) return;
977
- const ts = (/* @__PURE__ */ new Date()).toISOString();
978
- const entry = { ts, level, msg, ...this.bindings };
979
- if (ctx !== void 0) {
980
- entry.ctx = ctx instanceof Error ? { message: ctx.message, stack: ctx.stack } : ctx;
981
- }
982
- if (this.file) {
983
- const line = `${JSON.stringify(entry)}
984
- `;
985
- const bytes = Buffer.byteLength(line, "utf8");
986
- const root = this.root;
987
- if (root.pendingFileWrites < _DefaultLogger.MAX_PENDING_FILE_WRITES && bytes <= _DefaultLogger.MAX_PENDING_FILE_BYTES && root.pendingFileBytes + bytes <= _DefaultLogger.MAX_PENDING_FILE_BYTES) {
988
- root.pendingFileWrites += 1;
989
- root.pendingFileBytes += bytes;
990
- this.enqueueRotate(this.file);
991
- this._tail = this._tail.then(() => fsp.appendFile(this.file, line, { mode: 384 })).catch(() => void 0).finally(() => {
992
- root.pendingFileWrites = Math.max(0, root.pendingFileWrites - 1);
993
- root.pendingFileBytes = Math.max(0, root.pendingFileBytes - bytes);
994
- });
995
- }
996
- }
997
- if (!this.stderr) return;
998
- if (this.format === "json") {
999
- writeErr(`${JSON.stringify(entry)}
1000
- `);
1001
- } else {
1002
- const head = `${color.dim(ts)} ${COLORS[level](level.toUpperCase().padEnd(5))} ${msg}`;
1003
- if (ctx !== void 0) {
1004
- writeErr(`${head} ${formatCtx(ctx)}
1005
- `);
1006
- } else {
1007
- writeErr(`${head}
1008
- `);
1009
- }
1010
- }
1011
- }
1012
- };
1013
- function parseLogLevel(raw) {
1014
- return raw && LOG_LEVELS.has(raw) ? raw : "info";
1015
- }
1016
- function parseLogFormat(raw) {
1017
- return raw && LOG_FORMATS.has(raw) ? raw : "pretty";
1018
- }
1019
- function formatCtx(ctx) {
1020
- if (ctx instanceof Error) return color.dim(ctx.message);
1021
- if (typeof ctx === "string") return color.dim(ctx);
1022
- try {
1023
- return color.dim(JSON.stringify(ctx));
1024
- } catch {
1025
- return color.dim(String(ctx));
1026
- }
1027
- }
1028
- var noOpLogger = {
1029
- // 'error' is the quietest level the Logger contract offers; the methods
1030
- // discard everything regardless, this only matters to level checks.
1031
- level: "error",
1032
- error: () => {
1033
- },
1034
- warn: () => {
1035
- },
1036
- info: () => {
1037
- },
1038
- debug: () => {
1039
- },
1040
- trace: () => {
1041
- },
1042
- child: () => noOpLogger
1043
- };
1044
-
1045
- // src/infrastructure/path-resolver.ts
1046
- import * as fs from "node:fs";
1047
- import * as os from "node:os";
1048
- import * as path2 from "node:path";
1049
- var PROJECT_MARKERS = [
1050
- ".git",
1051
- "package.json",
1052
- "pnpm-workspace.yaml",
1053
- "go.mod",
1054
- "Cargo.toml",
1055
- "pyproject.toml",
1056
- // Use AGENTS.md, not the bare .wrongstack directory. A bare .wrongstack/
1057
- // directory can be the global config directory (~/.wrongstack), which is
1058
- // NOT a project marker. Only .wrongstack/AGENTS.md signals a real
1059
- // WrongStack project.
1060
- ".wrongstack/AGENTS.md"
1061
- ];
1062
- var DefaultPathResolver = class {
1063
- projectRoot;
1064
- cwd;
1065
- constructor(cwd = process.cwd()) {
1066
- this.cwd = path2.resolve(cwd);
1067
- this.projectRoot = this.detectProjectRoot(this.cwd);
1068
- }
1069
- detectProjectRoot(start) {
1070
- let dir = path2.resolve(start);
1071
- const root = path2.parse(dir).root;
1072
- const home = path2.resolve(os.homedir());
1073
- const startPath = path2.resolve(start);
1074
- while (dir !== root) {
1075
- if (dir === home && dir !== startPath) {
1076
- break;
1077
- }
1078
- for (const marker of PROJECT_MARKERS) {
1079
- try {
1080
- fs.accessSync(path2.join(dir, marker));
1081
- return dir;
1082
- } catch {
1083
- }
1084
- }
1085
- const parent = path2.dirname(dir);
1086
- if (parent === dir) break;
1087
- dir = parent;
1088
- }
1089
- return startPath;
1090
- }
1091
- resolve(input) {
1092
- const abs = path2.isAbsolute(input) ? input : path2.resolve(this.cwd, input);
1093
- let real;
1094
- try {
1095
- real = fs.realpathSync(abs);
1096
- } catch {
1097
- real = path2.normalize(abs);
1098
- }
1099
- return real;
1100
- }
1101
- isInsideRoot(absPath) {
1102
- const normalized = path2.normalize(absPath);
1103
- const root = path2.normalize(this.projectRoot);
1104
- if (normalized === root) return true;
1105
- const rel = path2.relative(root, normalized);
1106
- return !rel.startsWith("..") && !path2.isAbsolute(rel);
1107
- }
1108
- ensureInsideRoot(absPath) {
1109
- const resolved = this.resolve(absPath);
1110
- if (!this.isInsideRoot(resolved)) {
1111
- const display = path2.isAbsolute(absPath) ? path2.basename(absPath) : absPath;
1112
- const err = new Error(`Path "${display}" resolves outside the project root`);
1113
- err.fullPath = absPath;
1114
- err.projectRoot = this.projectRoot;
1115
- throw err;
1116
- }
1117
- return resolved;
1118
- }
1119
- };
1120
-
1121
- // src/utils/error.ts
1122
- function toErrorMessage(err) {
1123
- return err instanceof Error ? err.message : String(err);
1124
- }
1125
-
1126
- // src/types/errors.ts
1127
- var ERROR_CODES = {
1128
- // Provider
1129
- PROVIDER_RATE_LIMITED: "PROVIDER_RATE_LIMITED",
1130
- PROVIDER_AUTH_FAILED: "PROVIDER_AUTH_FAILED",
1131
- PROVIDER_OVERLOADED: "PROVIDER_OVERLOADED",
1132
- PROVIDER_INVALID_REQUEST: "PROVIDER_INVALID_REQUEST",
1133
- PROVIDER_SERVER_ERROR: "PROVIDER_SERVER_ERROR",
1134
- PROVIDER_NETWORK_ERROR: "PROVIDER_NETWORK_ERROR",
1135
- PROVIDER_CONTEXT_OVERFLOW: "PROVIDER_CONTEXT_OVERFLOW",
1136
- // Tool
1137
- TOOL_NOT_FOUND: "TOOL_NOT_FOUND",
1138
- TOOL_PERMISSION_DENIED: "TOOL_PERMISSION_DENIED",
1139
- TOOL_EXECUTION_FAILED: "TOOL_EXECUTION_FAILED",
1140
- TOOL_TIMEOUT: "TOOL_TIMEOUT",
1141
- TOOL_INPUT_INVALID: "TOOL_INPUT_INVALID",
1142
- // Config
1143
- CONFIG_INVALID: "CONFIG_INVALID",
1144
- CONFIG_NOT_FOUND: "CONFIG_NOT_FOUND",
1145
- CONFIG_PARSE_FAILED: "CONFIG_PARSE_FAILED",
1146
- CONFIG_MIGRATION_NEEDED: "CONFIG_MIGRATION_NEEDED",
1147
- // Plugin
1148
- PLUGIN_LOAD_FAILED: "PLUGIN_LOAD_FAILED",
1149
- PLUGIN_API_MISMATCH: "PLUGIN_API_MISMATCH",
1150
- PLUGIN_MISSING_DEPENDENCY: "PLUGIN_MISSING_DEPENDENCY",
1151
- // Agent
1152
- AGENT_ITERATION_LIMIT: "AGENT_ITERATION_LIMIT",
1153
- AGENT_CONTEXT_OVERFLOW: "AGENT_CONTEXT_OVERFLOW",
1154
- AGENT_ABORTED: "AGENT_ABORTED",
1155
- AGENT_RUN_FAILED: "AGENT_RUN_FAILED",
1156
- // Session
1157
- SESSION_NOT_FOUND: "SESSION_NOT_FOUND",
1158
- SESSION_CORRUPTED: "SESSION_CORRUPTED",
1159
- SESSION_WRITE_FAILED: "SESSION_WRITE_FAILED",
1160
- // Container / Registry
1161
- CONTAINER_TOKEN_ALREADY_BOUND: "CONTAINER_TOKEN_ALREADY_BOUND",
1162
- CONTAINER_TOKEN_NOT_BOUND: "CONTAINER_TOKEN_NOT_BOUND",
1163
- CONTAINER_CIRCULAR_DEPENDENCY: "CONTAINER_CIRCULAR_DEPENDENCY",
1164
- REGISTRY_DUPLICATE: "REGISTRY_DUPLICATE",
1165
- REGISTRY_NOT_FOUND: "REGISTRY_NOT_FOUND",
1166
- REGISTRY_INVALID: "REGISTRY_INVALID",
1167
- // File system
1168
- FS_READ_FAILED: "FS_READ_FAILED",
1169
- FS_WRITE_FAILED: "FS_WRITE_FAILED",
1170
- FS_MKDIR_FAILED: "FS_MKDIR_FAILED",
1171
- FS_DELETE_FAILED: "FS_DELETE_FAILED",
1172
- FS_ATOMIC_WRITE_FAILED: "FS_ATOMIC_WRITE_FAILED",
1173
- // SDD (Spec-Driven Development)
1174
- SDD_VALIDATION_FAILED: "SDD_VALIDATION_FAILED",
1175
- SDD_PARSE_FAILED: "SDD_PARSE_FAILED",
1176
- SDD_INVALID_STATE: "SDD_INVALID_STATE",
1177
- SDD_NOT_READY: "SDD_NOT_READY",
1178
- // General
1179
- VALIDATION_ERROR: "VALIDATION_ERROR",
1180
- PARSE_FAILED: "PARSE_FAILED",
1181
- UNKNOWN: "UNKNOWN"
1182
- };
1183
- var WrongStackError = class extends Error {
1184
- code;
1185
- subsystem;
1186
- severity;
1187
- recoverable;
1188
- context;
1189
- constructor(opts) {
1190
- super(opts.message, { cause: opts.cause });
1191
- this.name = "WrongStackError";
1192
- this.code = opts.code;
1193
- this.subsystem = opts.subsystem;
1194
- this.severity = opts.severity ?? "error";
1195
- this.recoverable = opts.recoverable ?? false;
1196
- this.context = opts.context;
1197
- }
1198
- /**
1199
- * Render a one-line user-facing description.
1200
- * Subclasses should override for domain-specific formatting.
1201
- */
1202
- describe() {
1203
- const ctx = this.context ? ` ${formatContext(this.context)}` : "";
1204
- return `${this.code}: ${this.message}${ctx}`;
1205
- }
1206
- };
1207
- function formatContext(ctx) {
1208
- const parts = Object.entries(ctx).filter(([, v]) => v !== void 0).slice(0, 3).map(([k, v]) => `${k}=${String(v)}`);
1209
- return parts.length > 0 ? `[${parts.join(" ")}]` : "";
1210
- }
1211
- var ConfigError = class extends WrongStackError {
1212
- constructor(opts) {
1213
- super({
1214
- message: opts.message,
1215
- code: opts.code,
1216
- subsystem: "config",
1217
- severity: "fatal",
1218
- recoverable: false,
1219
- context: opts.context,
1220
- cause: opts.cause
1221
- });
1222
- this.name = "ConfigError";
1223
- }
1224
- };
1225
- var FsError = class extends WrongStackError {
1226
- path;
1227
- constructor(opts) {
1228
- super({
1229
- message: opts.message,
1230
- code: opts.code,
1231
- subsystem: "fs",
1232
- severity: "error",
1233
- recoverable: opts.code !== ERROR_CODES.FS_READ_FAILED,
1234
- context: { path: opts.path, ...opts.context },
1235
- cause: opts.cause
1236
- });
1237
- this.name = "FsError";
1238
- this.path = opts.path;
1239
- }
1240
- };
1241
-
1242
- // src/types/provider.ts
1243
- function effectiveInputTokens(usage) {
1244
- return usage.input + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
1245
- }
1246
- function promptCacheHitRatio(usage) {
1247
- const total = effectiveInputTokens(usage);
1248
- if (!Number.isFinite(total) || total <= 0) return 0;
1249
- const cached = Number.isFinite(usage.cacheRead) ? Math.max(0, usage.cacheRead ?? 0) : 0;
1250
- return Math.min(1, cached / total);
1251
- }
1252
- var MAX_RESET_HINT_MS = 7 * 24 * 60 * 60 * 1e3;
1253
- var KIND_TO_CODE = {
1254
- network: ERROR_CODES.PROVIDER_NETWORK_ERROR,
1255
- timeout: ERROR_CODES.PROVIDER_NETWORK_ERROR,
1256
- rate_limit: ERROR_CODES.PROVIDER_RATE_LIMITED,
1257
- quota_exhausted: ERROR_CODES.PROVIDER_RATE_LIMITED,
1258
- auth: ERROR_CODES.PROVIDER_AUTH_FAILED,
1259
- overloaded: ERROR_CODES.PROVIDER_OVERLOADED,
1260
- context_overflow: ERROR_CODES.PROVIDER_CONTEXT_OVERFLOW,
1261
- server: ERROR_CODES.PROVIDER_SERVER_ERROR,
1262
- stream_hang: ERROR_CODES.PROVIDER_SERVER_ERROR,
1263
- content_filter: ERROR_CODES.PROVIDER_INVALID_REQUEST,
1264
- invalid_request: ERROR_CODES.PROVIDER_INVALID_REQUEST,
1265
- unknown: ERROR_CODES.PROVIDER_INVALID_REQUEST
1266
- };
1
+ import {
2
+ DefaultPathResolver,
3
+ ProviderCacheLedger,
4
+ assertProjectRootOutsideStateDir,
5
+ bootConfig,
6
+ flagsToConfigPatch
7
+ } from "../chunk-JTHKOYWU.js";
8
+ import "../chunk-IQ35HMQL.js";
9
+ import "../chunk-6AKOTGLM.js";
10
+ import {
11
+ allServers,
12
+ awsServer,
13
+ blockServer,
14
+ braveSearchServer,
15
+ context7Server,
16
+ everArtServer,
17
+ filesystemServer,
18
+ githubServer,
19
+ googleMapsServer,
20
+ miniMaxVisionServer,
21
+ playwrightServer,
22
+ sentinelServer,
23
+ slackServer,
24
+ sshManagerServer,
25
+ zaiVisionServer
26
+ } from "../chunk-FXQLIX3K.js";
27
+ import "../chunk-YY6APYUM.js";
28
+ import {
29
+ DefaultLogger
30
+ } from "../chunk-WBPVKEEE.js";
31
+ import "../chunk-H5E77KUJ.js";
32
+ import "../chunk-ZZ4V3WIZ.js";
33
+ import {
34
+ promptCacheHitRatio
35
+ } from "../chunk-NFAYHPRH.js";
36
+ import "../chunk-CQQL4ZOQ.js";
37
+ import "../chunk-RPOA7IAE.js";
38
+ import "../chunk-DEIQMBD2.js";
39
+ import {
40
+ repairToolUseAdjacency
41
+ } from "../chunk-M2WF6AXG.js";
42
+ import "../chunk-4XV6IR37.js";
43
+ import "../chunk-4VWO3ZXL.js";
44
+ import {
45
+ estimateMessageTokens,
46
+ estimateRequestTokens
47
+ } from "../chunk-FSWCWSTS.js";
48
+ import "../chunk-7ZHZE5EO.js";
49
+ import "../chunk-YNCVSFGF.js";
50
+ import "../chunk-KJ2ZTQSL.js";
51
+ import "../chunk-B4AMLZPB.js";
52
+ import "../chunk-WOEZV3Q4.js";
53
+ import "../chunk-TBRL2RCC.js";
54
+ import "../chunk-PFZO3C6H.js";
1267
55
 
1268
56
  // src/infrastructure/token-counter.ts
1269
57
  var PRICE_CACHE_MAX_SIZE = 100;
@@ -1507,640 +295,17 @@ function round4(n) {
1507
295
  return Math.round(n * 1e4) / 1e4;
1508
296
  }
1509
297
 
1510
- // src/infrastructure/provider-cache-ledger.ts
1511
- var ProviderCacheLedger = class {
1512
- byProvider = /* @__PURE__ */ new Map();
1513
- last = { input: 0, cacheRead: 0, cacheWrite: 0 };
1514
- off;
1515
- constructor(events) {
1516
- this.off = events.on(
1517
- "token.accounted",
1518
- (p) => {
1519
- this.record(p.usage, p.provider, p.deltaUsage);
1520
- }
1521
- );
1522
- }
1523
- record(usage, provider, deltaUsage) {
1524
- const cur = {
1525
- input: usage.input,
1526
- cacheRead: usage.cacheRead ?? 0,
1527
- cacheWrite: usage.cacheWrite ?? 0
1528
- };
1529
- const delta = deltaUsage ? {
1530
- input: deltaUsage.input,
1531
- cacheRead: deltaUsage.cacheRead ?? 0,
1532
- cacheWrite: deltaUsage.cacheWrite ?? 0
1533
- } : {
1534
- input: Math.max(0, cur.input - this.last.input),
1535
- cacheRead: Math.max(0, cur.cacheRead - this.last.cacheRead),
1536
- cacheWrite: Math.max(0, cur.cacheWrite - this.last.cacheWrite)
1537
- };
1538
- this.last = cur;
1539
- const key = provider ?? "unknown";
1540
- const acc = this.byProvider.get(key) ?? { input: 0, cacheRead: 0, cacheWrite: 0 };
1541
- acc.input += delta.input;
1542
- acc.cacheRead += delta.cacheRead;
1543
- acc.cacheWrite += delta.cacheWrite;
1544
- this.byProvider.set(key, acc);
1545
- }
1546
- /** Distinct providers seen this session. */
1547
- providers() {
1548
- return [...this.byProvider.keys()];
1549
- }
1550
- /** Per-provider cache stats, most-cached first. */
1551
- perProvider() {
1552
- const out = [];
1553
- for (const [provider, a] of this.byProvider) {
1554
- out.push({
1555
- provider,
1556
- input: a.input,
1557
- cacheRead: a.cacheRead,
1558
- cacheWrite: a.cacheWrite,
1559
- hitRatio: promptCacheHitRatio({
1560
- input: a.input,
1561
- output: 0,
1562
- cacheRead: a.cacheRead,
1563
- cacheWrite: a.cacheWrite
1564
- })
1565
- });
1566
- }
1567
- return out.sort((x, y) => y.cacheRead - x.cacheRead);
1568
- }
1569
- /**
1570
- * Unsubscribe from the event bus and release the per-session listener.
1571
- *
1572
- * **The caller MUST invoke `dispose()` on session teardown** — see the
1573
- * constructor's lifecycle warning. Failure to call this method leaves a
1574
- * permanent `token.accounted` listener on the shared EventBus that
1575
- * accumulates unboundedly across session restarts/resumes.
1576
- */
1577
- dispose() {
1578
- this.off();
1579
- }
1580
- };
1581
-
1582
- // src/infrastructure/mcp-servers.ts
1583
- var filesystemServer = () => ({
1584
- name: "filesystem",
1585
- description: "Read, write, and navigate the local filesystem (read-heavy tools)",
1586
- transport: "stdio",
1587
- command: "npx",
1588
- args: ["-y", "@modelcontextprotocol/server-filesystem", "."],
1589
- permission: "confirm"
1590
- });
1591
- var githubServer = () => ({
1592
- name: "github",
1593
- description: "GitHub API \u2014 issues, PRs, repos, search, file ops (requires GITHUB_PERSONAL_ACCESS_TOKEN)",
1594
- transport: "stdio",
1595
- command: "npx",
1596
- args: ["-y", "@modelcontextprotocol/server-github"],
1597
- passthroughEnv: ["GITHUB_PERSONAL_ACCESS_TOKEN", "GITHUB_TOKEN"],
1598
- permission: "confirm"
1599
- });
1600
- var context7Server = () => ({
1601
- name: "context7",
1602
- description: "Codebase-aware documentation and Q&A (context7.ai)",
1603
- transport: "streamable-http",
1604
- url: "https://mcp.context7.com/mcp",
1605
- permission: "confirm"
1606
- });
1607
- var braveSearchServer = () => ({
1608
- name: "brave-search",
1609
- description: "Web search (Brave). Requires BRAVE_SEARCH_API_KEY \u2014 free tier 2k queries/month",
1610
- transport: "stdio",
1611
- command: "npx",
1612
- args: ["-y", "@modelcontextprotocol/server-brave-search"],
1613
- passthroughEnv: ["BRAVE_SEARCH_API_KEY"],
1614
- permission: "confirm"
1615
- });
1616
- var blockServer = () => ({
1617
- name: "block",
1618
- description: "Postgres database access via SQL (Block MCP server)",
1619
- transport: "stdio",
1620
- command: "npx",
1621
- args: ["-y", "@modelcontextprotocol/server-block"],
1622
- permission: "confirm"
1623
- });
1624
- var everArtServer = () => ({
1625
- name: "everart",
1626
- description: "AI image generation (EverArt). Requires EVERART_API_KEY",
1627
- transport: "stdio",
1628
- command: "npx",
1629
- args: ["-y", "@modelcontextprotocol/server-everart"],
1630
- passthroughEnv: ["EVERART_API_KEY"],
1631
- permission: "confirm"
1632
- });
1633
- var slackServer = () => ({
1634
- name: "slack",
1635
- description: "Slack \u2014 messaging, channels, search. Requires SLACK_BOT_TOKEN + SLACK_TEAM_ID",
1636
- transport: "stdio",
1637
- command: "npx",
1638
- args: ["-y", "@modelcontextprotocol/server-slack"],
1639
- passthroughEnv: ["SLACK_BOT_TOKEN", "SLACK_TEAM_ID"],
1640
- permission: "confirm"
1641
- });
1642
- var awsServer = () => ({
1643
- name: "aws",
1644
- description: "AWS \u2014 EC2, S3, Lambda, IAM, CloudFormation, costs. Requires AWS credentials",
1645
- transport: "stdio",
1646
- command: "npx",
1647
- args: ["-y", "@modelcontextprotocol/server-aws"],
1648
- passthroughEnv: ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_REGION", "AWS_SESSION_TOKEN"],
1649
- permission: "confirm"
1650
- });
1651
- var googleMapsServer = () => ({
1652
- name: "google-maps",
1653
- description: "Google Maps \u2014 directions, geocoding, places. Requires GOOGLE_MAPS_API_KEY",
1654
- transport: "stdio",
1655
- command: "npx",
1656
- args: ["-y", "@modelcontextprotocol/server-google-maps"],
1657
- passthroughEnv: ["GOOGLE_MAPS_API_KEY"],
1658
- permission: "confirm"
1659
- });
1660
- var sentinelServer = () => ({
1661
- name: "sentinel",
1662
- description: "Security vulnerability scanning (Sentinel)",
1663
- transport: "streamable-http",
1664
- url: "https://mcp.sentinel.ai",
1665
- permission: "deny"
1666
- // security tool — require explicit confirmation
1667
- });
1668
- var zaiVisionServer = () => ({
1669
- name: "zai-vision",
1670
- description: "Z.AI Vision MCP \u2014 image analysis and screenshot understanding",
1671
- transport: "stdio",
1672
- command: "npx",
1673
- args: ["-y", "@z_ai/mcp-server@latest"],
1674
- env: { Z_AI_MODE: "ZAI" },
1675
- passthroughEnv: ["Z_AI_API_KEY"],
1676
- allowedTools: [
1677
- "image_analysis",
1678
- "extract_text_from_screenshot",
1679
- "diagnose_error_screenshot",
1680
- "understand_technical_diagram",
1681
- "analyze_data_visualization",
1682
- "ui_diff_check"
1683
- ],
1684
- permission: "auto"
1685
- });
1686
- var playwrightServer = () => ({
1687
- name: "playwright",
1688
- description: "Browser automation \u2014 navigate, screenshot, click, type, evaluate JS (headless Chromium)",
1689
- transport: "stdio",
1690
- command: "npx",
1691
- args: ["-y", "@modelcontextprotocol/server-playwright"],
1692
- permission: "confirm"
1693
- });
1694
- var miniMaxVisionServer = () => ({
1695
- name: "minimax-vision",
1696
- description: "MiniMax MCP \u2014 image understanding via understand_image",
1697
- transport: "stdio",
1698
- command: "uvx",
1699
- args: ["minimax-coding-plan-mcp", "-y"],
1700
- env: {
1701
- MINIMAX_MCP_BASE_PATH: "./.wrongstack/minimax-output",
1702
- MINIMAX_API_HOST: "https://api.minimax.io",
1703
- MINIMAX_API_RESOURCE_MODE: "url"
1704
- },
1705
- passthroughEnv: ["MINIMAX_API_KEY"],
1706
- allowedTools: ["understand_image"],
1707
- permission: "auto"
1708
- });
1709
- var sshManagerServer = () => ({
1710
- name: "ssh",
1711
- description: "Remote SSH management \u2014 execute commands, transfer files, tunnels, health checks (mcp-ssh-manager)",
1712
- transport: "stdio",
1713
- command: "npx",
1714
- args: ["-y", "mcp-ssh-manager"],
1715
- env: {
1716
- MCP_SSH_COMPACT_JSON: "true",
1717
- MCP_SSH_DEFAULT_TIMEOUT: "120000"
1718
- },
1719
- permission: "confirm",
1720
- requestTimeoutMs: 18e4
1721
- });
1722
- var requirementIntakeServer = () => ({
1723
- name: "requirement-intake",
1724
- description: "WrongStack Requirements Intake \u2014 list intake records and file new ones (project-scoped, --writable)",
1725
- transport: "stdio",
1726
- command: "wstack-requirement-intake-mcp",
1727
- args: ["--project-root", ".", "--writable"],
1728
- permission: "auto"
1729
- });
1730
- var kanbanServer = () => ({
1731
- name: "kanban",
1732
- description: "WrongStack Kanban \u2014 inspect and manage project work boards (project-scoped, manage tier, no destructive ops)",
1733
- transport: "stdio",
1734
- command: "wstack-kanban-mcp",
1735
- args: ["--project-root", ".", "--writable"],
1736
- permission: "confirm"
1737
- });
1738
- var mailboxServer = () => ({
1739
- name: "mailbox",
1740
- description: "WrongStack Mailbox \u2014 read and send project agent mail (project-scoped, no admin/credentials)",
1741
- transport: "stdio",
1742
- command: "wstack-mailbox-mcp",
1743
- args: ["--project-root", ".", "--actor", "external-agent", "--writable"],
1744
- permission: "auto"
1745
- });
1746
- var codebaseIndexServer = () => ({
1747
- name: "codebase-index",
1748
- description: "WrongStack Codebase Index \u2014 symbol search and dependency graphs (project-scoped, --writable)",
1749
- transport: "stdio",
1750
- command: "wstack-codebase-index-mcp",
1751
- args: ["--project-root", ".", "--writable"],
1752
- permission: "auto"
1753
- });
1754
- var allServers = () => ({
1755
- filesystem: { ...filesystemServer(), enabled: false },
1756
- github: { ...githubServer(), enabled: false },
1757
- context7: { ...context7Server(), enabled: false },
1758
- "brave-search": { ...braveSearchServer(), enabled: false },
1759
- block: { ...blockServer(), enabled: false },
1760
- everart: { ...everArtServer(), enabled: false },
1761
- slack: { ...slackServer(), enabled: false },
1762
- aws: { ...awsServer(), enabled: false },
1763
- "google-maps": { ...googleMapsServer(), enabled: false },
1764
- sentinel: { ...sentinelServer(), enabled: false },
1765
- "zai-vision": { ...zaiVisionServer(), enabled: false },
1766
- "minimax-vision": { ...miniMaxVisionServer(), enabled: false },
1767
- playwright: { ...playwrightServer(), enabled: false },
1768
- ssh: { ...sshManagerServer(), enabled: false },
1769
- kanban: { ...kanbanServer(), enabled: false },
1770
- mailbox: { ...mailboxServer(), enabled: false },
1771
- "codebase-index": { ...codebaseIndexServer(), enabled: false },
1772
- "requirement-intake": { ...requirementIntakeServer(), enabled: false }
1773
- });
1774
-
1775
- // src/utils/expect-defined.ts
1776
- function expectDefined(value, label) {
1777
- if (value === null || value === void 0) {
1778
- const err = new Error(label ? `Expected ${label} to be defined` : "Expected value to be defined");
1779
- err.name = "ExpectDefinedError";
1780
- throw err;
1781
- }
1782
- return value;
298
+ // src/infrastructure/context-manager.ts
299
+ var CONTEXT_MANAGER_TOOL_NAME = "context_manager";
300
+ function roughEstimate(messages) {
301
+ return estimateMessageTokens(messages);
1783
302
  }
1784
-
1785
- // src/utils/message-invariants.ts
1786
- function repairToolUseAdjacency(messages) {
1787
- const removedToolUses = [];
1788
- const removedToolResults = [];
1789
- let removedMessages = 0;
1790
- let changed = false;
1791
- const out = [];
1792
- for (let i = 0; i < messages.length; i++) {
1793
- const original = expectDefined(messages[i]);
1794
- let msg = original;
1795
- if (hasToolUse(msg)) {
1796
- const nextIds = toolResultIds(messages[i + 1]);
1797
- const filtered = mapContent(msg, (blocks) => {
1798
- const next = [];
1799
- for (const block of blocks) {
1800
- if (block.type === "tool_use" && !nextIds.has(block.id)) {
1801
- removedToolUses.push(block.id);
1802
- changed = true;
1803
- continue;
1804
- }
1805
- next.push(block);
1806
- }
1807
- return next;
1808
- });
1809
- msg = filtered ?? msg;
1810
- }
1811
- if (hasToolResult(msg)) {
1812
- const allowed = toolUseIds(out[out.length - 1]);
1813
- const filtered = mapContent(msg, (blocks) => {
1814
- const next = [];
1815
- for (const block of blocks) {
1816
- if (block.type === "tool_result" && !allowed.has(block.tool_use_id)) {
1817
- removedToolResults.push(block.tool_use_id);
1818
- changed = true;
1819
- continue;
1820
- }
1821
- next.push(block);
1822
- }
1823
- return next;
1824
- });
1825
- msg = filtered ?? msg;
1826
- }
1827
- if (isEmptyMessage(msg)) {
1828
- removedMessages++;
1829
- changed = true;
1830
- continue;
1831
- }
1832
- out.push(msg);
1833
- }
1834
- return {
1835
- messages: changed ? out : messages,
1836
- report: { changed, removedToolUses, removedToolResults, removedMessages }
1837
- };
1838
- }
1839
- function hasToolUse(msg) {
1840
- return contentBlocks(msg).some((b) => b.type === "tool_use");
1841
- }
1842
- function hasToolResult(msg) {
1843
- return contentBlocks(msg).some((b) => b.type === "tool_result");
1844
- }
1845
- function toolUseIds(msg) {
1846
- const ids = /* @__PURE__ */ new Set();
1847
- if (msg?.role !== "assistant") return ids;
1848
- for (const block of contentBlocks(msg)) {
1849
- if (block.type === "tool_use") ids.add(block.id);
1850
- }
1851
- return ids;
1852
- }
1853
- function toolResultIds(msg) {
1854
- const ids = /* @__PURE__ */ new Set();
1855
- if (msg?.role !== "user") return ids;
1856
- for (const block of contentBlocks(msg)) {
1857
- if (block.type === "tool_result") ids.add(block.tool_use_id);
1858
- }
1859
- return ids;
1860
- }
1861
- function contentBlocks(msg) {
1862
- return msg && Array.isArray(msg.content) ? msg.content : [];
1863
- }
1864
- function mapContent(msg, fn) {
1865
- if (!Array.isArray(msg.content)) return msg;
1866
- const next = fn(msg.content);
1867
- if (next.length === msg.content.length && next.every((b, idx) => b === msg.content[idx])) {
1868
- return msg;
1869
- }
1870
- return { ...msg, content: next };
1871
- }
1872
- function hasMeaningfulContent(content) {
1873
- if (typeof content === "string") return content.trim().length > 0;
1874
- for (const block of content) {
1875
- if (block.type === "text") {
1876
- if (block.text.trim().length > 0) return true;
1877
- continue;
1878
- }
1879
- if (block.type === "thinking") {
1880
- if (block.thinking.trim().length > 0 || block.signature) return true;
1881
- continue;
1882
- }
1883
- return true;
1884
- }
1885
- return false;
1886
- }
1887
- function isEmptyMessage(msg) {
1888
- return !hasMeaningfulContent(msg.content);
1889
- }
1890
-
1891
- // src/utils/tool-wire-compact.ts
1892
- var TOOL_DESCRIPTION_MAX_CHARS = 400;
1893
- var SCHEMA_DESCRIPTION_MAX_CHARS = 120;
1894
- var compactCache = /* @__PURE__ */ new WeakMap();
1895
- function compactToolDefinitionForWire(tool, opts = {}) {
1896
- const useDefaultOptions = opts.descriptionMaxChars === void 0 && opts.schemaDescriptionMaxChars === void 0;
1897
- if (useDefaultOptions && typeof tool === "object" && tool !== null) {
1898
- const cached = compactCache.get(tool);
1899
- if (cached) return cached;
1900
- }
1901
- const compact = {
1902
- name: tool.name,
1903
- description: compactDescription(
1904
- tool.description ?? "",
1905
- opts.descriptionMaxChars ?? TOOL_DESCRIPTION_MAX_CHARS
1906
- ),
1907
- inputSchema: normalizeTopLevelToolSchema(
1908
- compactSchemaDescriptions(
1909
- tool.inputSchema,
1910
- opts.schemaDescriptionMaxChars ?? SCHEMA_DESCRIPTION_MAX_CHARS
1911
- )
1912
- )
1913
- };
1914
- if (useDefaultOptions && typeof tool === "object" && tool !== null) {
1915
- compactCache.set(tool, compact);
1916
- }
1917
- return compact;
1918
- }
1919
- function normalizeTopLevelToolSchema(schema) {
1920
- const combinators = ["oneOf", "anyOf", "allOf"].map((keyword) => ({ keyword, branches: schema[keyword] })).filter(
1921
- (entry) => Array.isArray(entry.branches)
1922
- );
1923
- if (combinators.length === 0) return schema;
1924
- const out = { ...schema, type: "object" };
1925
- delete out["oneOf"];
1926
- delete out["anyOf"];
1927
- delete out["allOf"];
1928
- const properties = isRecord(schema["properties"]) ? { ...schema["properties"] } : {};
1929
- let required = stringSet(schema["required"]);
1930
- for (const { keyword, branches } of combinators) {
1931
- const objectBranches = branches.filter(isRecord);
1932
- for (const branch of objectBranches) {
1933
- if (isRecord(branch["properties"])) Object.assign(properties, branch["properties"]);
1934
- }
1935
- const branchRequired = objectBranches.map((branch) => stringSet(branch["required"]));
1936
- if (keyword === "allOf") {
1937
- for (const fields of branchRequired) for (const field of fields) required.add(field);
1938
- } else if (branchRequired.length > 0) {
1939
- const common = new Set(
1940
- [...branchRequired[0]].filter(
1941
- (field) => branchRequired.slice(1).every((fields) => fields.has(field))
1942
- )
1943
- );
1944
- required = /* @__PURE__ */ new Set([...required, ...common]);
1945
- }
1946
- }
1947
- out["properties"] = properties;
1948
- if (required.size > 0) out["required"] = [...required];
1949
- else delete out["required"];
1950
- return out;
1951
- }
1952
- function stringSet(value) {
1953
- return new Set(
1954
- Array.isArray(value) ? value.filter((item) => typeof item === "string") : []
1955
- );
1956
- }
1957
- function compactSchemaDescriptions(schema, maxDescriptionChars = SCHEMA_DESCRIPTION_MAX_CHARS) {
1958
- const compact = compactSchemaNode(schema, maxDescriptionChars);
1959
- return isRecord(compact) ? compact : { type: "object", properties: {} };
1960
- }
1961
- function compactSchemaNode(node, maxDescriptionChars) {
1962
- if (Array.isArray(node)) {
1963
- return node.map((item) => compactSchemaNode(item, maxDescriptionChars));
1964
- }
1965
- if (!isRecord(node)) return node;
1966
- const out = {};
1967
- for (const [key, value] of Object.entries(node)) {
1968
- if (key === "description" && typeof value === "string") {
1969
- out[key] = compactDescription(value, maxDescriptionChars);
1970
- } else {
1971
- out[key] = compactSchemaNode(value, maxDescriptionChars);
1972
- }
1973
- }
1974
- return out;
1975
- }
1976
- function compactDescription(text, maxChars) {
1977
- const normalized = text.replace(/\s+/g, " ").trim();
1978
- if (normalized.length <= maxChars) return normalized;
1979
- if (maxChars <= 20) return normalized.slice(0, maxChars);
1980
- const hardLimit = maxChars - 12;
1981
- const boundary = findSemanticBoundary(normalized, hardLimit);
1982
- const head = normalized.slice(0, boundary > 0 ? boundary : hardLimit).trimEnd();
1983
- return `${head} ...`;
1984
- }
1985
- function findSemanticBoundary(text, limit) {
1986
- const punctuation = Math.max(
1987
- text.lastIndexOf(". ", limit),
1988
- text.lastIndexOf("; ", limit),
1989
- text.lastIndexOf(": ", limit)
1990
- );
1991
- if (punctuation >= Math.floor(limit * 0.45)) return punctuation + 1;
1992
- const comma = text.lastIndexOf(", ", limit);
1993
- if (comma >= Math.floor(limit * 0.6)) return comma + 1;
1994
- const space = text.lastIndexOf(" ", limit);
1995
- return space >= Math.floor(limit * 0.6) ? space : limit;
1996
- }
1997
- function isRecord(value) {
1998
- return !!value && typeof value === "object" && !Array.isArray(value);
1999
- }
2000
-
2001
- // src/utils/token-estimate.ts
2002
- var RoughTokenEstimate = (text, charsPerToken = 3.5) => Math.max(1, Math.ceil(text.length / charsPerToken));
2003
- var CALIBRATION_GLOBAL_KEY = "__global__";
2004
- var _cals = /* @__PURE__ */ new Map();
2005
- function calState(key) {
2006
- let state = _cals.get(key);
2007
- if (!state) {
2008
- state = { ratio: 1, count: 0, prevEst: 0 };
2009
- _cals.set(key, state);
2010
- }
2011
- return state;
2012
- }
2013
- var ESTIMATE_CACHE = /* @__PURE__ */ new Map();
2014
- var _estimateCacheOrder = [];
2015
- var ESTIMATE_CACHE_MAX_SIZE = 5e4;
2016
- function estimateCacheKey(source) {
2017
- let h = 5381;
2018
- for (let i = 0; i < source.length; i++) {
2019
- h = (h << 5) + h + source.charCodeAt(i) | 0;
2020
- }
2021
- return `${source.length}:${h >>> 0}`;
2022
- }
2023
- function getCachedEstimate(source, compute) {
2024
- const key = estimateCacheKey(source);
2025
- const existing = ESTIMATE_CACHE.get(key);
2026
- if (existing !== void 0) return existing;
2027
- if (ESTIMATE_CACHE.size >= ESTIMATE_CACHE_MAX_SIZE) {
2028
- while (ESTIMATE_CACHE.size > Math.floor(ESTIMATE_CACHE_MAX_SIZE / 2)) {
2029
- const oldest = _estimateCacheOrder.shift();
2030
- if (oldest !== void 0) ESTIMATE_CACHE.delete(oldest);
2031
- }
2032
- }
2033
- const estimate = compute(source);
2034
- ESTIMATE_CACHE.set(key, estimate);
2035
- _estimateCacheOrder.push(key);
2036
- return estimate;
2037
- }
2038
- function estimateToolInputTokens(input) {
2039
- if (typeof input === "string") return RoughTokenEstimate(input);
2040
- if (input === null || typeof input !== "object") {
2041
- return RoughTokenEstimate(String(input));
2042
- }
2043
- return getCachedEstimate(JSON.stringify(input), (key) => RoughTokenEstimate(key));
2044
- }
2045
- function estimateToolResultTokens(content) {
2046
- if (typeof content === "string") return RoughTokenEstimate(content);
2047
- return getCachedEstimate(JSON.stringify(content), (key) => RoughTokenEstimate(key));
2048
- }
2049
- function estimateTextTokens(text) {
2050
- return RoughTokenEstimate(text);
2051
- }
2052
- function computeMessageTokens(msg) {
2053
- if (typeof msg.content === "string") return estimateTextTokens(msg.content);
2054
- let total = 0;
2055
- for (const b of msg.content) {
2056
- if (b.type === "text") total += estimateTextTokens(b.text);
2057
- else if (b.type === "tool_use") total += estimateToolInputTokens(b.input);
2058
- else if (b.type === "tool_result") total += estimateToolResultTokens(b.content);
2059
- else total += RoughTokenEstimate(JSON.stringify(b));
2060
- }
2061
- return total;
2062
- }
2063
- function estimateMessageTokens(messages) {
2064
- let total = 0;
2065
- for (const m of messages) {
2066
- if (typeof m._estTokens === "number" && m._estTokens > 0) {
2067
- total += m._estTokens;
2068
- continue;
2069
- }
2070
- total += computeMessageTokens(m);
2071
- }
2072
- return total;
2073
- }
2074
- function estimateToolDefTokens(tool) {
2075
- const cached = tool._estDefTokens;
2076
- if (typeof cached === "number" && cached > 0) return cached;
2077
- const compact = compactToolDefinitionForWire(tool);
2078
- return RoughTokenEstimate(tool.name) + RoughTokenEstimate(compact.description) + RoughTokenEstimate(JSON.stringify(compact.inputSchema));
2079
- }
2080
- function estimateRequestTokens(messages, systemPrompt, tools, calibrationKey = CALIBRATION_GLOBAL_KEY) {
2081
- let messagesTokens = 0;
2082
- if (typeof messages === "string") {
2083
- messagesTokens = RoughTokenEstimate(messages);
2084
- } else if (Array.isArray(messages)) {
2085
- for (const m of messages) {
2086
- if (typeof m === "object" && m !== null && "content" in m) {
2087
- const cached = m._estTokens;
2088
- if (typeof cached === "number" && cached > 0) {
2089
- messagesTokens += cached;
2090
- continue;
2091
- }
2092
- const content = m.content;
2093
- if (typeof content === "string") {
2094
- messagesTokens += RoughTokenEstimate(content);
2095
- } else if (Array.isArray(content)) {
2096
- for (const b of content) {
2097
- if (typeof b === "object" && b !== null) {
2098
- if (b.type === "text") {
2099
- messagesTokens += RoughTokenEstimate(b.text);
2100
- } else {
2101
- messagesTokens += RoughTokenEstimate(JSON.stringify(b));
2102
- }
2103
- }
2104
- }
2105
- }
2106
- }
2107
- }
2108
- }
2109
- let systemTokens = 0;
2110
- if (typeof systemPrompt === "string") {
2111
- systemTokens = RoughTokenEstimate(systemPrompt);
2112
- } else if (Array.isArray(systemPrompt)) {
2113
- for (const b of systemPrompt) {
2114
- if (typeof b === "object" && b !== null && b.type === "text") {
2115
- systemTokens += RoughTokenEstimate(b.text);
2116
- }
2117
- }
2118
- }
2119
- let toolsTokens = 0;
2120
- for (const t of tools) {
2121
- toolsTokens += estimateToolDefTokens(t);
2122
- }
2123
- const total = messagesTokens + systemTokens + toolsTokens;
2124
- calState(calibrationKey).prevEst = total;
2125
- return {
2126
- messages: messagesTokens,
2127
- systemPrompt: systemTokens,
2128
- tools: toolsTokens,
2129
- total
2130
- };
2131
- }
2132
-
2133
- // src/infrastructure/context-manager.ts
2134
- var CONTEXT_MANAGER_TOOL_NAME = "context_manager";
2135
- function roughEstimate(messages) {
2136
- return estimateMessageTokens(messages);
2137
- }
2138
- function createContextManagerTool(opts = {}) {
2139
- const minCompactThreshold = opts.minCompactThreshold ?? 0;
2140
- const noopRetryDeltaTokens = opts.noopRetryDeltaTokens ?? 1e3;
2141
- const configuredMaxContext = opts.maxContext;
2142
- const compactThresholdFraction = opts.compactThresholdFraction ?? 0.4;
2143
- let lastNoopTokens = 0;
303
+ function createContextManagerTool(opts = {}) {
304
+ const minCompactThreshold = opts.minCompactThreshold ?? 0;
305
+ const noopRetryDeltaTokens = opts.noopRetryDeltaTokens ?? 1e3;
306
+ const configuredMaxContext = opts.maxContext;
307
+ const compactThresholdFraction = opts.compactThresholdFraction ?? 0.4;
308
+ let lastNoopTokens = 0;
2144
309
  return {
2145
310
  name: CONTEXT_MANAGER_TOOL_NAME,
2146
311
  description: 'Inspect or reorganize the conversation context window. Use "check" to see token budget. Use "summary" to collapse a message range into a concise note (provide "text" for custom summary). Use "prune" to remove specific messages by index. Use "add_note" to inject a summary note. Use "compact" to run aggressive compaction. Use "repair" to remove orphan tool_use/tool_result blocks after manual context surgery.',
@@ -2389,2918 +554,6 @@ function createContextManagerTool(opts = {}) {
2389
554
  };
2390
555
  }
2391
556
  var contextManagerTool = createContextManagerTool();
2392
-
2393
- // src/boot.ts
2394
- import * as fs9 from "node:fs/promises";
2395
- import * as os4 from "node:os";
2396
- import * as path12 from "node:path";
2397
-
2398
- // src/security/secret-vault.ts
2399
- import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "node:crypto";
2400
- import * as fs2 from "node:fs";
2401
- import * as fsp2 from "node:fs/promises";
2402
- import * as path3 from "node:path";
2403
-
2404
- // src/types/secret-vault.ts
2405
- var ENCRYPTED_PREFIX_PATTERN = /^enc:v(\d+):/;
2406
- function encryptedPrefixForVersion(version) {
2407
- return `enc:v${version}:`;
2408
- }
2409
-
2410
- // src/utils/atomic-write.ts
2411
- import {
2412
- createPersistencePrimitives
2413
- } from "@wrongstack/persistence";
2414
- var primitives = createPersistencePrimitives({
2415
- createLockTimeoutError: ({ targetPath, timeoutMs }) => new FsError({
2416
- message: `Timed out waiting for file lock: ${targetPath}`,
2417
- code: "FS_ATOMIC_WRITE_FAILED",
2418
- path: targetPath,
2419
- context: { timeoutMs }
2420
- })
2421
- });
2422
- var atomicWrite = primitives.atomicWrite;
2423
- var atomicReplaceWithWriter = primitives.atomicReplaceWithWriter;
2424
- var ensureDir = primitives.ensureDir;
2425
- var withFileLock = primitives.withFileLock;
2426
-
2427
- // src/security/config-secrets.ts
2428
- function decryptConfigSecrets(cfg, vault, opts) {
2429
- const warn = opts?.warn ?? ((msg) => console.warn(msg));
2430
- return walk(cfg, vault, (v, key) => {
2431
- try {
2432
- return vault.decrypt(v);
2433
- } catch (err) {
2434
- warn(
2435
- `[secret-vault] Failed to decrypt "${key}": ${err instanceof Error ? err.message : err}`
2436
- );
2437
- return "";
2438
- }
2439
- });
2440
- }
2441
- function walk(node, vault, transform) {
2442
- if (node === null || node === void 0) return node;
2443
- if (typeof node !== "object") return node;
2444
- if (Array.isArray(node)) {
2445
- return node.map((item) => walk(item, vault, transform));
2446
- }
2447
- const out = /* @__PURE__ */ Object.create(null);
2448
- for (const [k, v] of Object.entries(node)) {
2449
- if (typeof v === "string" && isSecretField(k)) {
2450
- out[k] = transform(v, k);
2451
- } else if (typeof v === "object" && v !== null) {
2452
- out[k] = walk(v, vault, transform);
2453
- } else {
2454
- out[k] = v;
2455
- }
2456
- }
2457
- return out;
2458
- }
2459
- var SECRET_KEY_PATTERN = /(?:api[-_]?key|auth[-_]?token|authorization|proxy-authorization|cookie|bearer|secret|password|passwd|pwd|refresh[-_]?token|session[-_]?key|access[_-]?token|private[_-]?key|token\b)/i;
2460
- var NON_SECRET_OVERRIDES = /* @__PURE__ */ new Set(["publickey", "public_key"]);
2461
- function isSecretField(name) {
2462
- const lc = name.toLowerCase();
2463
- if (NON_SECRET_OVERRIDES.has(lc)) return false;
2464
- return SECRET_KEY_PATTERN.test(lc);
2465
- }
2466
-
2467
- // src/security/file-permissions.ts
2468
- import {
2469
- restrictDirPermissions,
2470
- restrictFilePermissions,
2471
- SECRET_DIR_MODE,
2472
- SECRET_FILE_MODE
2473
- } from "@wrongstack/persistence";
2474
-
2475
- // src/utils/deep-merge.ts
2476
- var FORBIDDEN_PROTO_KEYS = /* @__PURE__ */ new Set([
2477
- "__proto__",
2478
- "constructor",
2479
- "prototype",
2480
- "__defineGetter__",
2481
- "__defineSetter__",
2482
- "__lookupGetter__",
2483
- "__lookupSetter__"
2484
- ]);
2485
- function isPrimitiveArray(a) {
2486
- return a.every((v) => v === null || typeof v !== "object" && typeof v !== "function");
2487
- }
2488
- function deepMerge(base, patch, options = {}) {
2489
- const {
2490
- conflictResolution = "prefer-patch",
2491
- arrayMode = "replace",
2492
- protectProto = true,
2493
- onNonPrimitiveArrayReplace
2494
- } = options;
2495
- if (typeof base !== "object" || base === null) {
2496
- return conflictResolution === "prefer-patch" ? patch : base;
2497
- }
2498
- if (typeof patch !== "object" || patch === null) {
2499
- return conflictResolution === "prefer-patch" ? patch : base;
2500
- }
2501
- if (Array.isArray(base) && Array.isArray(patch)) {
2502
- if (arrayMode === "concat-primitives" && isPrimitiveArray(base) && isPrimitiveArray(patch)) {
2503
- return [.../* @__PURE__ */ new Set([...base, ...patch])];
2504
- }
2505
- return conflictResolution === "prefer-patch" ? patch : base;
2506
- }
2507
- if (Array.isArray(base) || Array.isArray(patch)) {
2508
- return conflictResolution === "prefer-patch" ? patch : base;
2509
- }
2510
- const baseObj = base;
2511
- const patchObj = patch;
2512
- const out = { ...baseObj };
2513
- for (const [k, v] of Object.entries(patchObj)) {
2514
- if (protectProto && FORBIDDEN_PROTO_KEYS.has(k)) continue;
2515
- const existing = out[k];
2516
- if (v !== null && typeof v === "object" && !Array.isArray(v) && existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
2517
- out[k] = deepMerge(existing, v, options);
2518
- } else if (Array.isArray(v) && Array.isArray(existing)) {
2519
- if (onNonPrimitiveArrayReplace && !isPrimitiveArray(v)) {
2520
- onNonPrimitiveArrayReplace(k, existing.length, v.length);
2521
- }
2522
- out[k] = deepMerge(existing, v, options);
2523
- } else if (v !== void 0) {
2524
- if (onNonPrimitiveArrayReplace && Array.isArray(v) && !isPrimitiveArray(v)) {
2525
- const existingLen = Array.isArray(existing) ? existing.length : 0;
2526
- onNonPrimitiveArrayReplace(k, existingLen, v.length);
2527
- }
2528
- out[k] = v;
2529
- }
2530
- }
2531
- return out;
2532
- }
2533
-
2534
- // src/security/secret-vault.ts
2535
- var KEY_BYTES = 32;
2536
- var IV_BYTES = 12;
2537
- var TAG_BYTES = 16;
2538
- var ALGO = "aes-256-gcm";
2539
- var KEY_FILE_MODE = 384;
2540
- var KEY_FILE_MAGIC = Buffer.from("WSKV", "ascii");
2541
- var VERSIONED_KEY_FILE_SIZE = KEY_FILE_MAGIC.length + 1 + KEY_BYTES;
2542
- var KEK_MAGIC = Buffer.from("WSKW", "ascii");
2543
- var KEK_SALT_BYTES = 16;
2544
- var WRAPPED_KEY_FILE_SIZE = KEK_MAGIC.length + 1 + KEK_SALT_BYTES + IV_BYTES + TAG_BYTES + KEY_BYTES;
2545
- var SCRYPT_N = 1 << 15;
2546
- var SCRYPT_R = 8;
2547
- var SCRYPT_P = 1;
2548
- var SCRYPT_MAXMEM = 64 * 1024 * 1024;
2549
- function getVaultPassphrase() {
2550
- const v = process.env["WRONGSTACK_VAULT_PASSPHRASE"];
2551
- return v && v.length > 0 ? v : void 0;
2552
- }
2553
- function isWrappedKeyFile(buf) {
2554
- return buf.length === WRAPPED_KEY_FILE_SIZE && buf.subarray(0, KEK_MAGIC.length).equals(KEK_MAGIC);
2555
- }
2556
- function deriveKEK(passphrase, salt) {
2557
- return scryptSync(passphrase, salt, KEY_BYTES, {
2558
- N: SCRYPT_N,
2559
- r: SCRYPT_R,
2560
- p: SCRYPT_P,
2561
- maxmem: SCRYPT_MAXMEM
2562
- });
2563
- }
2564
- function wrapDataKey(dataKey, keyVersion, passphrase) {
2565
- const salt = randomBytes(KEK_SALT_BYTES);
2566
- const iv = randomBytes(IV_BYTES);
2567
- const kek = deriveKEK(passphrase, salt);
2568
- const cipher = createCipheriv(ALGO, kek, iv);
2569
- const ct = Buffer.concat([cipher.update(dataKey), cipher.final()]);
2570
- const tag = cipher.getAuthTag();
2571
- const out = Buffer.alloc(WRAPPED_KEY_FILE_SIZE);
2572
- let off = 0;
2573
- KEK_MAGIC.copy(out, off);
2574
- off += KEK_MAGIC.length;
2575
- out[off] = keyVersion & 255;
2576
- off += 1;
2577
- salt.copy(out, off);
2578
- off += KEK_SALT_BYTES;
2579
- iv.copy(out, off);
2580
- off += IV_BYTES;
2581
- tag.copy(out, off);
2582
- off += TAG_BYTES;
2583
- ct.copy(out, off);
2584
- return out;
2585
- }
2586
- function unwrapDataKey(buf, keyFile) {
2587
- const passphrase = getVaultPassphrase();
2588
- if (!passphrase) {
2589
- throw new ConfigError({
2590
- message: `SecretVault: key file ${keyFile} is passphrase-protected \u2014 set the WRONGSTACK_VAULT_PASSPHRASE environment variable to unlock it.`,
2591
- code: ERROR_CODES.CONFIG_INVALID,
2592
- context: { keyFile }
2593
- });
2594
- }
2595
- let off = KEK_MAGIC.length;
2596
- const version = buf[off];
2597
- off += 1;
2598
- const salt = buf.subarray(off, off + KEK_SALT_BYTES);
2599
- off += KEK_SALT_BYTES;
2600
- const iv = buf.subarray(off, off + IV_BYTES);
2601
- off += IV_BYTES;
2602
- const tag = buf.subarray(off, off + TAG_BYTES);
2603
- off += TAG_BYTES;
2604
- const ct = buf.subarray(off, off + KEY_BYTES);
2605
- const kek = deriveKEK(passphrase, salt);
2606
- const decipher = createDecipheriv(ALGO, kek, iv);
2607
- decipher.setAuthTag(tag);
2608
- try {
2609
- const key = Buffer.concat([decipher.update(ct), decipher.final()]);
2610
- return { key: Buffer.from(key), version };
2611
- } catch {
2612
- throw new ConfigError({
2613
- message: `SecretVault: failed to unlock key file ${keyFile} \u2014 wrong WRONGSTACK_VAULT_PASSPHRASE (key unwrap authentication failed).`,
2614
- code: ERROR_CODES.CONFIG_INVALID,
2615
- context: { keyFile }
2616
- });
2617
- }
2618
- }
2619
- function keyFileNeedsHardening(keyFile, opts) {
2620
- if (process.platform === "win32") return false;
2621
- const warn = opts?.warn ?? ((msg) => console.warn(msg));
2622
- try {
2623
- const stat5 = fs2.statSync(keyFile);
2624
- const actualMode = stat5.mode & 511;
2625
- if (actualMode !== KEY_FILE_MODE) {
2626
- warn(
2627
- `Key file ${keyFile} has mode ${actualMode.toString(8)} \u2014 expected ${KEY_FILE_MODE.toString(8)}. Hardening\u2026`
2628
- );
2629
- return true;
2630
- }
2631
- } catch {
2632
- }
2633
- return false;
2634
- }
2635
- function mkdirSecretDirSync(dir) {
2636
- fs2.mkdirSync(dir, { recursive: true, mode: 448 });
2637
- if (process.platform === "win32") return;
2638
- try {
2639
- fs2.chmodSync(dir, 448);
2640
- } catch {
2641
- }
2642
- }
2643
- function writeKeyFileAtomicSync(keyFile, content) {
2644
- const tmp = `${keyFile}.${randomBytes(4).toString("hex")}.tmp`;
2645
- const fd = fs2.openSync(tmp, "w", 384);
2646
- try {
2647
- fs2.writeFileSync(fd, content);
2648
- try {
2649
- fs2.fsyncSync(fd);
2650
- } catch {
2651
- }
2652
- } finally {
2653
- fs2.closeSync(fd);
2654
- }
2655
- try {
2656
- fs2.renameSync(tmp, keyFile);
2657
- } catch (err) {
2658
- try {
2659
- fs2.unlinkSync(tmp);
2660
- } catch {
2661
- }
2662
- throw err;
2663
- }
2664
- }
2665
- var DefaultSecretVault = class {
2666
- keyFile;
2667
- logger;
2668
- key;
2669
- _keyVersion = 1;
2670
- /**
2671
- * Pending key-file hardening promises (Windows ACL restrictions via icacls).
2672
- * Tracked so async callers can `flushHardening()` before returning, ensuring
2673
- * a short-lived CLI process does not exit before the hardening resolves.
2674
- * Sync-only callers (encrypt/decrypt lazy-load) leave these pending —
2675
- * hardening remains best-effort in that case.
2676
- */
2677
- pendingHardening = [];
2678
- constructor(opts) {
2679
- this.keyFile = opts.keyFile;
2680
- this.logger = opts.logger;
2681
- }
2682
- /**
2683
- * Emit a structured warning. Uses the configured Logger when available;
2684
- * falls back to console.warn(JSON) so warnings are never silently dropped
2685
- * during early boot.
2686
- */
2687
- logWarn(msg, ctx) {
2688
- if (this.logger) {
2689
- this.logger.warn(msg, ctx);
2690
- } else {
2691
- console.warn(JSON.stringify({ ...ctx, message: msg, timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
2692
- }
2693
- }
2694
- /**
2695
- * Schedule best-effort key-file hardening and track the promise so async
2696
- * callers can await it via `flushHardening()` before returning. Rejections
2697
- * are swallowed here (the helper already warns on failure) so a rejection
2698
- * can never become an unhandled one.
2699
- */
2700
- scheduleKeyHardening() {
2701
- const p = restrictFilePermissions(this.keyFile, {
2702
- label: "secret-vault-key",
2703
- warn: (msg) => this.logWarn(msg)
2704
- }).catch(() => void 0);
2705
- this.pendingHardening.push(p);
2706
- }
2707
- /** Detect and schedule repair of a pre-existing POSIX key with loose mode bits. */
2708
- checkKeyFilePermissions() {
2709
- if (keyFileNeedsHardening(this.keyFile, {
2710
- warn: (msg) => this.logWarn(msg)
2711
- })) {
2712
- this.scheduleKeyHardening();
2713
- }
2714
- }
2715
- /** Flush all pending key-file hardening promises. */
2716
- flushHardening() {
2717
- if (this.pendingHardening.length === 0) return Promise.resolve();
2718
- const all = Promise.all(this.pendingHardening).then(() => void 0);
2719
- this.pendingHardening = [];
2720
- return all;
2721
- }
2722
- /** Current key version. Starts at 1; incremented by rotateKey(). */
2723
- get keyVersion() {
2724
- if (!this.key) this.loadOrCreateKey();
2725
- return this._keyVersion;
2726
- }
2727
- isEncrypted(value) {
2728
- return typeof value === "string" && ENCRYPTED_PREFIX_PATTERN.test(value);
2729
- }
2730
- encrypt(plaintext) {
2731
- if (this.isEncrypted(plaintext)) return plaintext;
2732
- const key = this.loadOrCreateKey();
2733
- const iv = randomBytes(IV_BYTES);
2734
- const cipher = createCipheriv(ALGO, key, iv);
2735
- const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
2736
- const tag = cipher.getAuthTag();
2737
- const prefix = encryptedPrefixForVersion(this._keyVersion);
2738
- return `${prefix}${iv.toString("base64")}:${tag.toString("base64")}:${ct.toString("base64")}`;
2739
- }
2740
- decrypt(value) {
2741
- if (!this.isEncrypted(value)) return value;
2742
- const prefixMatch = value.match(ENCRYPTED_PREFIX_PATTERN);
2743
- if (!prefixMatch) {
2744
- throw new ConfigError({
2745
- message: "SecretVault: malformed encrypted value",
2746
- code: ERROR_CODES.CONFIG_PARSE_FAILED,
2747
- context: { field: "encrypted_value" }
2748
- });
2749
- }
2750
- const rest = value.slice(prefixMatch[0].length);
2751
- const parts = rest.split(":");
2752
- if (parts.length !== 3) {
2753
- throw new ConfigError({
2754
- message: "SecretVault: malformed encrypted value",
2755
- code: ERROR_CODES.CONFIG_PARSE_FAILED,
2756
- context: { field: "encrypted_value" }
2757
- });
2758
- }
2759
- const [ivB64, tagB64, ctB64] = parts;
2760
- const iv = Buffer.from(ivB64, "base64");
2761
- const tag = Buffer.from(tagB64, "base64");
2762
- const ct = Buffer.from(ctB64, "base64");
2763
- if (iv.length !== IV_BYTES)
2764
- throw new ConfigError({
2765
- message: "SecretVault: bad IV length",
2766
- code: ERROR_CODES.CONFIG_PARSE_FAILED,
2767
- context: { expected: IV_BYTES, actual: iv.length }
2768
- });
2769
- if (tag.length !== TAG_BYTES)
2770
- throw new ConfigError({
2771
- message: "SecretVault: bad tag length",
2772
- code: ERROR_CODES.CONFIG_PARSE_FAILED,
2773
- context: { expected: TAG_BYTES, actual: tag.length }
2774
- });
2775
- const key = this.loadOrCreateKey();
2776
- const decipher = createDecipheriv(ALGO, key, iv);
2777
- decipher.setAuthTag(tag);
2778
- const pt = Buffer.concat([decipher.update(ct), decipher.final()]);
2779
- return pt.toString("utf8");
2780
- }
2781
- /**
2782
- * Generate a new encryption key, write it to disk, and increment the key version.
2783
- * After rotation, encrypt() emits the new version prefix (e.g. enc:v2:).
2784
- * The caller must re-encrypt existing config values (see rotateConfigKeys()).
2785
- */
2786
- rotateKey() {
2787
- const oldVersion = this._keyVersion;
2788
- const newKey = randomBytes(KEY_BYTES);
2789
- const newVersion = oldVersion + 1;
2790
- mkdirSecretDirSync(path3.dirname(this.keyFile));
2791
- const passphrase = getVaultPassphrase();
2792
- if (passphrase) {
2793
- writeKeyFileAtomicSync(this.keyFile, wrapDataKey(newKey, newVersion, passphrase));
2794
- } else {
2795
- const keyFileBuf = Buffer.alloc(VERSIONED_KEY_FILE_SIZE);
2796
- KEY_FILE_MAGIC.copy(keyFileBuf, 0);
2797
- keyFileBuf[KEY_FILE_MAGIC.length] = newVersion;
2798
- newKey.copy(keyFileBuf, KEY_FILE_MAGIC.length + 1);
2799
- writeKeyFileAtomicSync(this.keyFile, keyFileBuf);
2800
- }
2801
- this.scheduleKeyHardening();
2802
- this.checkKeyFilePermissions();
2803
- this.key = newKey;
2804
- this._keyVersion = newVersion;
2805
- return { oldVersion, newVersion };
2806
- }
2807
- /**
2808
- * If WRONGSTACK_VAULT_PASSPHRASE is set but the key on disk is still stored
2809
- * unwrapped (legacy v1 / versioned v2), re-write it in passphrase-wrapped (v3)
2810
- * form. The data key is preserved, so all existing ciphertext keeps
2811
- * decrypting. Best-effort: a write failure leaves the working unwrapped file
2812
- * in place and is not fatal to load.
2813
- */
2814
- migrateToWrappedIfPassphrase() {
2815
- const passphrase = getVaultPassphrase();
2816
- if (!passphrase || !this.key) return;
2817
- try {
2818
- writeKeyFileAtomicSync(this.keyFile, wrapDataKey(this.key, this._keyVersion, passphrase));
2819
- this.scheduleKeyHardening();
2820
- this.checkKeyFilePermissions();
2821
- } catch {
2822
- }
2823
- }
2824
- loadOrCreateKey() {
2825
- if (this.key) return this.key;
2826
- try {
2827
- const buf = fs2.readFileSync(this.keyFile);
2828
- if (isWrappedKeyFile(buf)) {
2829
- const { key: key2, version } = unwrapDataKey(buf, this.keyFile);
2830
- this.key = key2;
2831
- this._keyVersion = version;
2832
- this.checkKeyFilePermissions();
2833
- return this.key;
2834
- }
2835
- if (buf.length === KEY_BYTES) {
2836
- this.key = buf;
2837
- this._keyVersion = 1;
2838
- this.checkKeyFilePermissions();
2839
- this.migrateToWrappedIfPassphrase();
2840
- return this.key;
2841
- }
2842
- if (buf.length === VERSIONED_KEY_FILE_SIZE) {
2843
- const magic = buf.subarray(0, KEY_FILE_MAGIC.length);
2844
- if (!magic.equals(KEY_FILE_MAGIC)) {
2845
- throw new ConfigError({
2846
- message: `SecretVault: key file ${this.keyFile} has invalid magic header`,
2847
- code: ERROR_CODES.CONFIG_INVALID,
2848
- context: { keyFile: this.keyFile }
2849
- });
2850
- }
2851
- const version = buf[KEY_FILE_MAGIC.length];
2852
- const key2 = buf.subarray(KEY_FILE_MAGIC.length + 1);
2853
- if (key2.length !== KEY_BYTES) {
2854
- throw new ConfigError({
2855
- message: `SecretVault: key file ${this.keyFile} has wrong key size (${key2.length} bytes, expected ${KEY_BYTES})`,
2856
- code: ERROR_CODES.CONFIG_INVALID,
2857
- context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: key2.length }
2858
- });
2859
- }
2860
- this.key = Buffer.from(key2);
2861
- this._keyVersion = version;
2862
- this.checkKeyFilePermissions();
2863
- this.migrateToWrappedIfPassphrase();
2864
- return this.key;
2865
- }
2866
- throw new ConfigError({
2867
- message: `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES} for v1 or ${VERSIONED_KEY_FILE_SIZE} for v2+). Remove it manually to generate a new key.`,
2868
- code: ERROR_CODES.CONFIG_INVALID,
2869
- context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: buf.length }
2870
- });
2871
- } catch (err) {
2872
- if (err.code !== "ENOENT") throw err;
2873
- }
2874
- mkdirSecretDirSync(path3.dirname(this.keyFile));
2875
- const key = randomBytes(KEY_BYTES);
2876
- const passphrase = getVaultPassphrase();
2877
- const initialBytes = passphrase ? wrapDataKey(key, 1, passphrase) : key;
2878
- try {
2879
- fs2.writeFileSync(this.keyFile, initialBytes, { mode: 384, flag: "wx" });
2880
- this.scheduleKeyHardening();
2881
- } catch (err) {
2882
- if (err.code !== "EEXIST") throw err;
2883
- this.scheduleKeyHardening();
2884
- const buf = fs2.readFileSync(this.keyFile);
2885
- if (isWrappedKeyFile(buf)) {
2886
- const { key: winnerKey, version } = unwrapDataKey(buf, this.keyFile);
2887
- this.key = winnerKey;
2888
- this._keyVersion = version;
2889
- this.checkKeyFilePermissions();
2890
- return this.key;
2891
- }
2892
- if (buf.length === KEY_BYTES) {
2893
- this.key = buf;
2894
- this._keyVersion = 1;
2895
- this.checkKeyFilePermissions();
2896
- return this.key;
2897
- }
2898
- if (buf.length === VERSIONED_KEY_FILE_SIZE) {
2899
- const magic = buf.subarray(0, KEY_FILE_MAGIC.length);
2900
- if (!magic.equals(KEY_FILE_MAGIC)) {
2901
- throw new ConfigError({
2902
- message: `SecretVault: key file ${this.keyFile} has invalid magic header`,
2903
- code: ERROR_CODES.CONFIG_INVALID,
2904
- context: { keyFile: this.keyFile }
2905
- });
2906
- }
2907
- const version = buf[KEY_FILE_MAGIC.length];
2908
- const winnerKey = buf.subarray(KEY_FILE_MAGIC.length + 1);
2909
- this.key = Buffer.from(winnerKey);
2910
- this._keyVersion = version;
2911
- this.checkKeyFilePermissions();
2912
- return this.key;
2913
- }
2914
- throw new ConfigError({
2915
- message: `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES} for v1 or ${VERSIONED_KEY_FILE_SIZE} for v2+). Remove it manually to generate a new key.`,
2916
- code: ERROR_CODES.CONFIG_INVALID,
2917
- context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: buf.length }
2918
- });
2919
- }
2920
- this.key = key;
2921
- this._keyVersion = 1;
2922
- return key;
2923
- }
2924
- };
2925
- async function migratePlaintextSecrets(configPath, vault, logger) {
2926
- let raw;
2927
- try {
2928
- raw = await fsp2.readFile(configPath, "utf8");
2929
- } catch {
2930
- return { migrated: 0, file: configPath };
2931
- }
2932
- let parsed;
2933
- try {
2934
- parsed = JSON.parse(raw);
2935
- } catch {
2936
- return { migrated: 0, file: configPath };
2937
- }
2938
- const counter = { n: 0 };
2939
- const migrated = walkCount(parsed, vault, counter);
2940
- if (counter.n === 0) return { migrated: 0, file: configPath };
2941
- await atomicWrite(configPath, JSON.stringify(migrated, null, 2), { mode: 384 });
2942
- await restrictFilePermissions2(
2943
- configPath,
2944
- logger ? { warn: (msg) => logger.warn(msg) } : void 0
2945
- );
2946
- await vault.flushHardening?.();
2947
- return { migrated: counter.n, file: configPath };
2948
- }
2949
- async function restrictFilePermissions2(filePath, opts) {
2950
- await restrictFilePermissions(filePath, { label: "secret-vault", warn: opts?.warn });
2951
- }
2952
- function walkCount(node, vault, counter) {
2953
- if (node === null || node === void 0) return node;
2954
- if (typeof node !== "object") return node;
2955
- if (Array.isArray(node)) {
2956
- return node.map((item) => walkCount(item, vault, counter));
2957
- }
2958
- const out = /* @__PURE__ */ Object.create(null);
2959
- for (const [k, v] of Object.entries(node)) {
2960
- if (typeof v === "string" && isSecretField(k) && !vault.isEncrypted(v) && v.length > 0) {
2961
- out[k] = vault.encrypt(v);
2962
- counter.n++;
2963
- } else if (typeof v === "object" && v !== null) {
2964
- out[k] = walkCount(v, vault, counter);
2965
- } else {
2966
- out[k] = v;
2967
- }
2968
- }
2969
- return out;
2970
- }
2971
-
2972
- // src/storage/config-loader.ts
2973
- import * as fs5 from "node:fs/promises";
2974
-
2975
- // src/types/context-window.ts
2976
- var DEFAULT_CONTEXT_WINDOW_MODE_ID = "balanced";
2977
- var DEPRECATED_CONTEXT_WINDOW_MODE_ALIASES = Object.freeze({
2978
- archival: "balanced"
2979
- });
2980
- var CONTEXT_WINDOW_MODES = Object.freeze([
2981
- {
2982
- id: "balanced",
2983
- name: "Balanced",
2984
- description: "Default rolling compaction: recent work stays verbatim, old tool output is trimmed. Windows of 1M+ tokens default to Deep.",
2985
- thresholds: { warn: 0.55, soft: 0.7, hard: 0.85 },
2986
- aggressiveOn: "soft",
2987
- preserveK: 8,
2988
- eliseThreshold: 1200,
2989
- targetLoad: 0.65
2990
- },
2991
- {
2992
- id: "frugal",
2993
- name: "Frugal",
2994
- description: "Token-saver mode: compacts early and keeps a tight verbatim tail.",
2995
- thresholds: { warn: 0.4, soft: 0.55, hard: 0.7 },
2996
- aggressiveOn: "warn",
2997
- preserveK: 4,
2998
- eliseThreshold: 500,
2999
- targetLoad: 0.45
3000
- },
3001
- {
3002
- id: "deep",
3003
- name: "Deep",
3004
- description: "Long-reasoning mode: delays compaction and keeps more recent turns intact.",
3005
- thresholds: { warn: 0.72, soft: 0.86, hard: 0.96 },
3006
- aggressiveOn: "hard",
3007
- preserveK: 18,
3008
- eliseThreshold: 5e3,
3009
- targetLoad: 0.82
3010
- }
3011
- ]);
3012
- function listContextWindowModes() {
3013
- return CONTEXT_WINDOW_MODES.map((m) => ({ ...m, thresholds: { ...m.thresholds } }));
3014
- }
3015
- function normalizeContextWindowModeId(id) {
3016
- if (!id) return null;
3017
- if (isContextWindowModeId(id)) return id;
3018
- return DEPRECATED_CONTEXT_WINDOW_MODE_ALIASES[id] ?? null;
3019
- }
3020
- function isDeprecatedContextWindowModeId(id) {
3021
- return Object.hasOwn(DEPRECATED_CONTEXT_WINDOW_MODE_ALIASES, id);
3022
- }
3023
- function isContextWindowModeSelectionId(id) {
3024
- return isContextWindowModeId(id) || isDeprecatedContextWindowModeId(id);
3025
- }
3026
- function isContextWindowModeId(id) {
3027
- return CONTEXT_WINDOW_MODES.some((m) => m.id === id);
3028
- }
3029
-
3030
- // src/utils/config-backup.ts
3031
- import * as fs3 from "node:fs/promises";
3032
- import * as path4 from "node:path";
3033
- function configHistoryDir(globalRoot) {
3034
- return path4.join(globalRoot, "config-history");
3035
- }
3036
- function configSlug(absolutePath, globalRoot) {
3037
- const rel = path4.relative(globalRoot, absolutePath);
3038
- const normalized = rel.replace(/\\/g, "/").replace(/\.json$/i, "");
3039
- return normalized.replace(/\//g, "-");
3040
- }
3041
- async function backupConfigFile(filePath, paths) {
3042
- let currentContent;
3043
- try {
3044
- currentContent = await fs3.readFile(filePath, "utf8");
3045
- if (!currentContent.trim()) return;
3046
- } catch {
3047
- return;
3048
- }
3049
- const now = /* @__PURE__ */ new Date();
3050
- const ts = now.toISOString().replace(/[:.]/g, "-").replace(/Z$/, "");
3051
- const slug = configSlug(filePath, paths.globalRoot);
3052
- const backupDir = configHistoryDir(paths.globalRoot);
3053
- const backupFile = path4.join(backupDir, `${slug}-${ts}.json`);
3054
- try {
3055
- await fs3.mkdir(backupDir, { recursive: true });
3056
- await fs3.writeFile(backupFile, currentContent, { mode: 384, encoding: "utf8" });
3057
- } catch {
3058
- }
3059
- }
3060
-
3061
- // src/utils/safe-json.ts
3062
- function safeParse(input, maxBytes = 5e6) {
3063
- if (Buffer.byteLength(input, "utf8") > maxBytes) {
3064
- return { ok: false, error: `Input exceeds limit (${maxBytes} bytes)` };
3065
- }
3066
- try {
3067
- return { ok: true, value: JSON.parse(input) };
3068
- } catch (err) {
3069
- return {
3070
- ok: false,
3071
- error: toErrorMessage(err)
3072
- };
3073
- }
3074
- }
3075
-
3076
- // src/utils/wstack-paths.ts
3077
- import { createHash } from "node:crypto";
3078
- import * as fs4 from "node:fs";
3079
- import * as os2 from "node:os";
3080
- import * as path5 from "node:path";
3081
- function canonicalProjectRoot(absRoot) {
3082
- const checkoutRoot = path5.resolve(absRoot);
3083
- const dotGit = path5.join(checkoutRoot, ".git");
3084
- try {
3085
- if (!fs4.statSync(dotGit).isFile()) return checkoutRoot;
3086
- const gitDirLine = fs4.readFileSync(dotGit, "utf8").trim();
3087
- const match = /^gitdir:\s*(.+)$/i.exec(gitDirLine);
3088
- if (!match?.[1]) return checkoutRoot;
3089
- const gitDir = path5.resolve(checkoutRoot, match[1].trim());
3090
- const commonDirFile = path5.join(gitDir, "commondir");
3091
- if (!fs4.statSync(commonDirFile).isFile()) return checkoutRoot;
3092
- const commonDir = path5.resolve(gitDir, fs4.readFileSync(commonDirFile, "utf8").trim());
3093
- const worktreesDir = path5.dirname(gitDir);
3094
- if (path5.basename(worktreesDir).toLowerCase() !== "worktrees") return checkoutRoot;
3095
- if (path5.resolve(worktreesDir, "..") !== commonDir) return checkoutRoot;
3096
- if (path5.basename(commonDir).toLowerCase() !== ".git") return checkoutRoot;
3097
- return path5.dirname(commonDir);
3098
- } catch {
3099
- return checkoutRoot;
3100
- }
3101
- }
3102
- function projectHash(absRoot) {
3103
- return createHash("sha256").update(canonicalProjectRoot(absRoot)).digest("hex").slice(0, 12);
3104
- }
3105
- function projectSlug(absRoot) {
3106
- const identityRoot = canonicalProjectRoot(absRoot);
3107
- const base = slugify(path5.basename(identityRoot));
3108
- const hash = createHash("sha256").update(identityRoot).digest("hex").slice(0, 6);
3109
- return `${base}-${hash}`;
3110
- }
3111
- function slugify(name) {
3112
- return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "project";
3113
- }
3114
- function safeProfileName(name) {
3115
- const safe = (name ?? "").replace(/[/\\:]/g, "_").replace(/\.\./g, "_").trim();
3116
- return safe || "default";
3117
- }
3118
- function bootstrapProfileName(globalRoot) {
3119
- try {
3120
- const parsed = JSON.parse(fs4.readFileSync(path5.join(globalRoot, "config.json"), "utf8"));
3121
- return safeProfileName(
3122
- typeof parsed.activeProfile === "string" ? parsed.activeProfile : void 0
3123
- );
3124
- } catch {
3125
- return "default";
3126
- }
3127
- }
3128
- function wstackGlobalRoot() {
3129
- const fromEnv = process.env["WRONGSTACK_HOME"];
3130
- if (fromEnv && fromEnv.trim().length > 0) return path5.resolve(fromEnv);
3131
- return path5.join(os2.homedir(), ".wrongstack");
3132
- }
3133
- function resolveWstackPaths(opts) {
3134
- const globalRoot = opts.globalRoot ?? (opts.userHome ? path5.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
3135
- const homeDir = opts.userHome ?? os2.homedir();
3136
- const profileName = safeProfileName(opts.profileName ?? bootstrapProfileName(globalRoot));
3137
- const profileDir = path5.join(globalRoot, "profiles", profileName);
3138
- const hash = projectHash(opts.projectRoot);
3139
- const slug = projectSlug(opts.projectRoot);
3140
- const projectDir = path5.join(globalRoot, "projects", slug);
3141
- return {
3142
- globalRoot,
3143
- profileName,
3144
- profileDir,
3145
- projectRoot: opts.projectRoot,
3146
- homeDir,
3147
- configDir: profileDir,
3148
- globalConfig: path5.join(globalRoot, "config.json"),
3149
- profilesDir: path5.join(globalRoot, "profiles"),
3150
- profileConfig: (name) => {
3151
- return path5.join(globalRoot, "profiles", safeProfileName(name), "config.json");
3152
- },
3153
- profileStatuslineConfig: (name) => {
3154
- return path5.join(globalRoot, "profiles", safeProfileName(name), "statusline.json");
3155
- },
3156
- profileModeConfig: (name) => {
3157
- return path5.join(globalRoot, "profiles", safeProfileName(name), "mode.json");
3158
- },
3159
- profileProviderStatus: (name) => {
3160
- return path5.join(globalRoot, "profiles", safeProfileName(name), "provider-status.json");
3161
- },
3162
- profileUpdateCache: (name) => {
3163
- return path5.join(globalRoot, "profiles", safeProfileName(name), "update-cache.json");
3164
- },
3165
- secretsKey: path5.join(globalRoot, ".key"),
3166
- globalMemory: path5.join(profileDir, "memory.md"),
3167
- globalSkills: path5.join(profileDir, "skills"),
3168
- globalClaudeSkills: path5.join(homeDir, ".claude", "skills"),
3169
- globalDesignKits: path5.join(profileDir, "design-kits"),
3170
- globalPrompts: path5.join(profileDir, "prompts"),
3171
- globalInstructions: path5.join(profileDir, "instructions"),
3172
- promptUsage: path5.join(profileDir, "prompt-usage.json"),
3173
- cacheDir: path5.join(globalRoot, "cache"),
3174
- modelsCache: path5.join(globalRoot, "cache", "models.dev.json"),
3175
- modelsOverlayCache: path5.join(globalRoot, "cache", "models-overlay.json"),
3176
- historyFile: path5.join(profileDir, "history"),
3177
- logFile: path5.join(globalRoot, "logs", "wrongstack.log"),
3178
- projectDir,
3179
- projectCodebaseIndex: path5.join(projectDir, "codebase-index"),
3180
- projectMemory: path5.join(projectDir, "memory.md"),
3181
- projectSessions: path5.join(projectDir, "sessions"),
3182
- projectTrust: path5.join(projectDir, "trust.json"),
3183
- projectMeta: path5.join(projectDir, "meta.json"),
3184
- projectLocalConfig: path5.join(projectDir, "config.local.json"),
3185
- inProjectConfig: path5.join(opts.projectRoot, ".wrongstack", "config.json"),
3186
- inProjectAgentsFile: path5.join(opts.projectRoot, ".wrongstack", "AGENTS.md"),
3187
- inProjectSkills: path5.join(opts.projectRoot, ".wrongstack", "skills"),
3188
- inProjectClaudeSkills: path5.join(opts.projectRoot, ".claude", "skills"),
3189
- inProjectPrompts: path5.join(opts.projectRoot, ".wrongstack", "prompts"),
3190
- inProjectInstructions: path5.join(opts.projectRoot, ".wrongstack", "instructions"),
3191
- inProjectDesignKits: path5.join(opts.projectRoot, ".wrongstack", "design-kits"),
3192
- inProjectWorktrees: path5.join(opts.projectRoot, ".wrongstack", "worktrees"),
3193
- projectHash: hash,
3194
- projectSlug: slug,
3195
- projectGoal: path5.join(projectDir, "goal.json"),
3196
- projectInputHistory: path5.join(projectDir, "input-history.json"),
3197
- projectSpecs: path5.join(projectDir, "specs"),
3198
- projectTaskGraphs: path5.join(projectDir, "task-graphs"),
3199
- projectSddSession: path5.join(projectDir, "sdd-session.json"),
3200
- projectPlan: path5.join(projectDir, "plan.json"),
3201
- projectAutophase: path5.join(projectDir, "autophase"),
3202
- projectSddBoards: path5.join(projectDir, "sdd-boards"),
3203
- projectRequirementIntakes: path5.join(projectDir, "requirement-intakes"),
3204
- syncConfig: path5.join(profileDir, "sync.json"),
3205
- configHistoryDir: path5.join(globalRoot, "config-history"),
3206
- projectStatus: (statusProjectSlug) => {
3207
- if (!/^[a-z0-9](?:[a-z0-9-]{0,39})-[a-f0-9]{6}$/.test(statusProjectSlug)) {
3208
- throw new Error(`Invalid project slug: ${statusProjectSlug}`);
3209
- }
3210
- return path5.join(globalRoot, "projects", statusProjectSlug, "status.json");
3211
- }
3212
- };
3213
- }
3214
-
3215
- // src/storage/config-loader/bootstrap.ts
3216
- var BOOTSTRAP_KEYS = /* @__PURE__ */ new Set(["version", "activeProfile"]);
3217
- function isBootstrapOnly(config) {
3218
- return Object.keys(config).every((k) => BOOTSTRAP_KEYS.has(k));
3219
- }
3220
-
3221
- // src/types/config/runtime.ts
3222
- function normalizeTokenSavingTier(val) {
3223
- if (val === void 0) return "off";
3224
- if (typeof val === "boolean") return val ? "medium" : "off";
3225
- if (val === "auto") return "medium";
3226
- const validTiers = /* @__PURE__ */ new Set([
3227
- "off",
3228
- "minimal",
3229
- "light",
3230
- "medium",
3231
- "aggressive"
3232
- ]);
3233
- return validTiers.has(val) ? val : "off";
3234
- }
3235
- var DEFAULT_TUI_THINKING_WORD = "thinking";
3236
-
3237
- // src/types/default-config.ts
3238
- var DEFAULT_TOOLS_CONFIG = Object.freeze({
3239
- defaultExecutionStrategy: "smart",
3240
- maxIterations: 100,
3241
- iterationTimeoutMs: 3e5,
3242
- maxToolTimeoutMs: 3e5,
3243
- sessionTimeoutMs: 18e5,
3244
- perIterationOutputCapBytes: 1e5,
3245
- descriptionMode: Object.freeze({}),
3246
- disabledTools: Object.freeze([]),
3247
- // Extension requires an explicit host/user policy. Without that boundary a
3248
- // nominal maxIterations cap can grow forever in autonomous sessions.
3249
- autoExtendLimit: false,
3250
- restrictToProjectRoot: true,
3251
- // Off by default: the board is a record of the work, not a permit for it.
3252
- // See ToolsConfig.kanbanGovernance for what turning it on costs and gates.
3253
- kanbanGovernance: false,
3254
- loopDetection: Object.freeze({
3255
- mode: "steer-then-cut",
3256
- steerThreshold: 3,
3257
- cutThreshold: 5,
3258
- windowSize: 12,
3259
- callRepeatThreshold: 4
3260
- })
3261
- });
3262
- var DEFAULT_CONTEXT_CONFIG = Object.freeze({
3263
- warnThreshold: 0.55,
3264
- softThreshold: 0.7,
3265
- hardThreshold: 0.85,
3266
- targetLoad: 0.65,
3267
- preserveK: 8,
3268
- eliseThreshold: 1200
3269
- });
3270
- var DEFAULT_AUTONOMY_CONFIG = Object.freeze({
3271
- autoProceedDelayMs: 45e3
3272
- });
3273
- var DEFAULT_CIRCUIT_BREAKER_CONFIG = Object.freeze({
3274
- enabled: false,
3275
- autoKillResetMs: 6e4
3276
- });
3277
- var DEFAULT_SESSION_LOGGING_CONFIG = Object.freeze({
3278
- auditLevel: "standard",
3279
- sampling: {
3280
- toolProgress: {
3281
- sampleRate: 8
3282
- }
3283
- }
3284
- });
3285
-
3286
- // src/storage/config-loader/defaults.ts
3287
- var CONFIG_BEHAVIOR_DEFAULTS = {
3288
- version: 1,
3289
- context: {
3290
- mode: DEFAULT_CONTEXT_WINDOW_MODE_ID,
3291
- warnThreshold: DEFAULT_CONTEXT_CONFIG.warnThreshold,
3292
- softThreshold: DEFAULT_CONTEXT_CONFIG.softThreshold,
3293
- hardThreshold: DEFAULT_CONTEXT_CONFIG.hardThreshold,
3294
- targetLoad: DEFAULT_CONTEXT_CONFIG.targetLoad,
3295
- autoCompact: true,
3296
- preserveK: DEFAULT_CONTEXT_CONFIG.preserveK,
3297
- eliseThreshold: DEFAULT_CONTEXT_CONFIG.eliseThreshold,
3298
- strategy: "hybrid"
3299
- },
3300
- tools: {
3301
- defaultExecutionStrategy: DEFAULT_TOOLS_CONFIG.defaultExecutionStrategy,
3302
- maxIterations: DEFAULT_TOOLS_CONFIG.maxIterations,
3303
- iterationTimeoutMs: DEFAULT_TOOLS_CONFIG.iterationTimeoutMs,
3304
- maxToolTimeoutMs: DEFAULT_TOOLS_CONFIG.maxToolTimeoutMs,
3305
- sessionTimeoutMs: DEFAULT_TOOLS_CONFIG.sessionTimeoutMs,
3306
- perIterationOutputCapBytes: DEFAULT_TOOLS_CONFIG.perIterationOutputCapBytes,
3307
- descriptionMode: DEFAULT_TOOLS_CONFIG.descriptionMode,
3308
- disabledTools: DEFAULT_TOOLS_CONFIG.disabledTools,
3309
- autoExtendLimit: DEFAULT_TOOLS_CONFIG.autoExtendLimit,
3310
- restrictToProjectRoot: DEFAULT_TOOLS_CONFIG.restrictToProjectRoot,
3311
- kanbanGovernance: DEFAULT_TOOLS_CONFIG.kanbanGovernance,
3312
- loopDetection: DEFAULT_TOOLS_CONFIG.loopDetection
3313
- },
3314
- log: { level: "info" },
3315
- features: {
3316
- mcp: true,
3317
- plugins: true,
3318
- memory: true,
3319
- modelsRegistry: true,
3320
- skills: true,
3321
- prompts: true,
3322
- // 'auto' → resolveTokenSavingTier picks a concrete tier from the model's
3323
- // context window ONCE per session (cache-safe): lean prompt on small
3324
- // windows (<32k medium, <128k light) where the fixed identity+tool prose
3325
- // is a big fraction; minimal trimming on >=128k so modern large-window
3326
- // models still get cost savings without capability loss. Explicit tiers
3327
- // are respected verbatim.
3328
- tokenSavingMode: "auto",
3329
- // Derived, never written by hand: this and `tools.restrictToProjectRoot`
3330
- // (line 41) are the two halves of one switch, and they used to be declared
3331
- // independently in this same object — one `true`, one `true` — with
3332
- // session.ts resolving the contradiction via `??` in favour of the
3333
- // permissive half. The confinement guard therefore shipped disabled on a
3334
- // fresh install even though the other half asked for it (WS-075).
3335
- allowOutsideProjectRoot: !DEFAULT_TOOLS_CONFIG.restrictToProjectRoot
3336
- },
3337
- Sage: {
3338
- enabled: true,
3339
- storage: {
3340
- projectLocal: true,
3341
- directory: ".wrongstack/memories"
3342
- },
3343
- inject: {
3344
- // Keep the ordinary turn prompt clean. Project memory is surfaced
3345
- // on-demand beside relevant tool results, where the path/query that
3346
- // caused retrieval is known and the hint can be independently capped.
3347
- turnContext: false,
3348
- toolResults: true,
3349
- // Off: task text searches for what the operator is doing, not for the
3350
- // file the tool touched, and every match it adds is unrelated to the
3351
- // result it lands on.
3352
- taskAware: false,
3353
- maxHintsPerTool: 8,
3354
- maxCharsPerTool: 2800,
3355
- maxTurnMemories: 8,
3356
- maxCharsPerTurn: 2400,
3357
- minScore: 0.72,
3358
- minImportance: 0.5,
3359
- // 0 = once per session. Re-showing a memory the model has already been
3360
- // given spends context and adds nothing.
3361
- repeatCooldownMs: 0
3362
- },
3363
- hygiene: {
3364
- autoAfterSession: true,
3365
- autoOnFileChange: true,
3366
- retentionDays: 90,
3367
- archiveLowConfidenceAfterDays: 30
3368
- },
3369
- embeddings: {
3370
- enabled: false
3371
- }
3372
- },
3373
- skills: { readClaudeSkills: true, mode: "progressive" },
3374
- mcpServers: {},
3375
- fallbackAuto: true,
3376
- maxConcurrent: 4,
3377
- // YOLO opt-in: users who want auto-approve behaviour set `yolo: true`.
3378
- // Off by default so prompt injection in repository content, fetched
3379
- // content, or mailbox messages cannot induce arbitrary shell calls or
3380
- // write operations without human confirmation.
3381
- yolo: false,
3382
- nextPrediction: false,
3383
- hints: true,
3384
- debugStream: false,
3385
- configScope: "global",
3386
- indexing: {
3387
- onSessionStart: true,
3388
- onEdit: true,
3389
- watchExternal: true,
3390
- debounceMs: 400
3391
- },
3392
- session: { ...DEFAULT_SESSION_LOGGING_CONFIG },
3393
- autonomy: {
3394
- // 'auto' by default: the agent self-drives (picks the top next-step after
3395
- // each turn). This is a startup default, not a runtime flip — the
3396
- // autoProceedMaxIterations cap + autoProceedDelayMs cooldown + Ctrl+C /
3397
- // [GOAL_COMPLETE] stops remain in force. Explicit 'off'
3398
- // in a config file is preserved (opt-out respected).
3399
- defaultMode: "auto",
3400
- autoProceedDelayMs: DEFAULT_AUTONOMY_CONFIG.autoProceedDelayMs,
3401
- autoProceedMaxIterations: 50,
3402
- autonomyNextPrompt: "auto {{suggestion}}",
3403
- terminalTitleAnimation: true,
3404
- // Mirrored from the top-level yolo default so the autonomy subsystem
3405
- // (which reads autonomy.yolo) stays consistent with config.yolo.
3406
- yolo: false,
3407
- fleetChatVerbosity: "off",
3408
- chime: false,
3409
- confirmExit: true,
3410
- mouseMode: false,
3411
- enhance: true,
3412
- enhanceDelayMs: 6e4,
3413
- enhanceLanguage: "original",
3414
- // Product-wide statusline density default. Mirrored by the TUI's
3415
- // DEFAULT_STATUSLINE_MODE (packages/tui/src/components/settings-picker-model.ts).
3416
- statuslineMode: "minimum",
3417
- thinkingWord: DEFAULT_TUI_THINKING_WORD,
3418
- showAgentSwarmPanel: "bottom"
3419
- },
3420
- circuitBreaker: { ...DEFAULT_CIRCUIT_BREAKER_CONFIG },
3421
- modelRuntime: {
3422
- // `effort` is intentionally undefined by default. Leaving it unset lets
3423
- // each model use its provider-recommended reasoning effort (or none at
3424
- // all) instead of forcing an opinionated value that may be unsupported,
3425
- // silently omitted, and surfaced as a per-request warning. Users who
3426
- // want a specific effort can opt in via `/settings` or the WebUI panel.
3427
- reasoning: { mode: "auto" },
3428
- cache: { ttl: "1h" }
3429
- },
3430
- systemPrompt: { variant: "default" }
3431
- };
3432
-
3433
- // src/storage/config-loader/default-repair.ts
3434
- function isPlainRecord(value) {
3435
- return typeof value === "object" && value !== null && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
3436
- }
3437
- function cloneJsonValue(value) {
3438
- return structuredClone(value);
3439
- }
3440
- function fillMissingDefaults(target, defaults) {
3441
- const value = cloneJsonValue(target);
3442
- const changed = fillMissingDefaultsInPlace(value, defaults);
3443
- return { value, changed };
3444
- }
3445
- function fillMissingDefaultsInPlace(target, defaults) {
3446
- let changed = false;
3447
- for (const [key, defaultValue] of Object.entries(defaults)) {
3448
- if (!Object.hasOwn(target, key)) {
3449
- target[key] = cloneJsonValue(defaultValue);
3450
- changed = true;
3451
- continue;
3452
- }
3453
- const current = target[key];
3454
- if (isPlainRecord(current) && isPlainRecord(defaultValue)) {
3455
- changed = fillMissingDefaultsInPlace(current, defaultValue) || changed;
3456
- }
3457
- }
3458
- return changed;
3459
- }
3460
-
3461
- // src/storage/config-loader/diagnostics.ts
3462
- function storageErrorString(err) {
3463
- if (err instanceof Error) {
3464
- const code = err.code;
3465
- return code ? `${code}: ${err.message}` : err.message;
3466
- }
3467
- return String(err);
3468
- }
3469
-
3470
- // src/storage/config-loader/env-overrides.ts
3471
- function envBool(v) {
3472
- return !/^(0|false|no|off)$/i.test(v.trim());
3473
- }
3474
- function envBoolOptional(v) {
3475
- return v !== void 0 && envBool(v);
3476
- }
3477
- var LOG_LEVELS2 = /* @__PURE__ */ new Set(["error", "warn", "info", "debug", "trace"]);
3478
- function envLogLevel(v) {
3479
- return LOG_LEVELS2.has(v) ? v : "info";
3480
- }
3481
- var defaultIndexing = {
3482
- onSessionStart: true,
3483
- onEdit: true,
3484
- watchExternal: true,
3485
- debounceMs: 400
3486
- };
3487
- var ENV_MAP = {
3488
- WRONGSTACK_PROVIDER: (c, v) => {
3489
- c.provider = v;
3490
- if (c._envSource === void 0) c._envSource = /* @__PURE__ */ new Set();
3491
- c._envSource.add("provider");
3492
- },
3493
- WRONGSTACK_MODEL: (c, v) => {
3494
- c.model = v;
3495
- if (c._envSource === void 0) c._envSource = /* @__PURE__ */ new Set();
3496
- c._envSource.add("model");
3497
- },
3498
- WRONGSTACK_API_KEY: (c, v) => {
3499
- c.apiKey = v;
3500
- if (c._envSource === void 0) c._envSource = /* @__PURE__ */ new Set();
3501
- c._envSource.add("apiKey");
3502
- },
3503
- WRONGSTACK_BASE_URL: (c, v) => {
3504
- c.baseUrl = v;
3505
- if (c._envSource === void 0) c._envSource = /* @__PURE__ */ new Set();
3506
- c._envSource.add("baseUrl");
3507
- },
3508
- WRONGSTACK_LOG_LEVEL: (c, v) => {
3509
- if (!c.log) c.log = { level: "info" };
3510
- c.log.level = envLogLevel(v);
3511
- },
3512
- WRONGSTACK_INDEX_ON_START: (c, v) => {
3513
- c.indexing = { ...defaultIndexing, ...c.indexing, onSessionStart: envBool(v) };
3514
- },
3515
- WRONGSTACK_INDEX_ON_EDIT: (c, v) => {
3516
- c.indexing = { ...defaultIndexing, ...c.indexing, onEdit: envBool(v) };
3517
- },
3518
- WRONGSTACK_INDEX_WATCH: (c, v) => {
3519
- c.indexing = { ...defaultIndexing, ...c.indexing, watchExternal: envBool(v) };
3520
- }
3521
- };
3522
-
3523
- // src/storage/config-loader/in-project-policy.ts
3524
- var IN_PROJECT_ALLOWED_KEYS = /* @__PURE__ */ new Set([
3525
- "version",
3526
- "model",
3527
- "cwd",
3528
- "context",
3529
- "tools",
3530
- "features",
3531
- "Sage",
3532
- "skills",
3533
- "autonomy",
3534
- "indexing",
3535
- "session",
3536
- "log",
3537
- "launch",
3538
- "nextPrediction",
3539
- "hints",
3540
- "debugStream",
3541
- "configScope",
3542
- "maxConcurrent",
3543
- "uiLocale",
3544
- "fallbackModels",
3545
- "fallbackBridge",
3546
- "fallbackProfiles",
3547
- // The profile SELECTOR. No broader than its siblings: a repo that can write
3548
- // `fallbackModels` and `fallbackProfiles` already controls the chain outright,
3549
- // and this one can only name a profile the user already defined.
3550
- "fallbackProfile",
3551
- "favoriteModels",
3552
- "favoriteModelsOnly",
3553
- "modelAvailabilitySchedule",
3554
- "fallbackAuto",
3555
- "fallbackStickiness",
3556
- "models",
3557
- "modelMatrix",
3558
- "circuitBreaker",
3559
- "adaptiveConcurrency",
3560
- "modelRuntime"
3561
- ]);
3562
- var KNOWN_DENIED_IN_PROJECT = [
3563
- {
3564
- key: "activeProfile",
3565
- reason: "Only the trusted root bootstrap may select the active profile."
3566
- },
3567
- { key: "provider", reason: "Provider id override; can intercept prompts/responses." },
3568
- { key: "apiKey", reason: "Overrides user API key; exfiltrates prompts." },
3569
- { key: "baseUrl", reason: "Redirects provider endpoint; leaks real API key." },
3570
- { key: "providers", reason: "Per-provider apiKey/baseUrl/oauthConfig; same redirect/exfil." },
3571
- { key: "mcpServers", reason: "Arbitrary command/args/env spawned at boot (RCE)." },
3572
- { key: "hooks", reason: "Shell command arrays on lifecycle events (RCE)." },
3573
- { key: "plugins", reason: "Dynamic npm package load at boot (RCE)." },
3574
- {
3575
- key: "pluginManager",
3576
- reason: "Controls which plugin boot states the LLM may mutate; user-owned trust policy."
3577
- },
3578
- { key: "sync", reason: "Carries githubToken credential and target repo." },
3579
- {
3580
- key: "cloudSync",
3581
- reason: "Carries the my.wrongstack.com machine token credential and endpoint URL."
3582
- },
3583
- { key: "yolo", reason: "Disables all permission confirmation prompts." },
3584
- { key: "extensions", reason: "Per-plugin config can carry command/credential fields." },
3585
- { key: "hq", reason: "Carries HQ client token credential and endpoint URL." },
3586
- { key: "acp", reason: "Per-agent ACP command/args/env override \u2192 arbitrary command exec (RCE)." },
3587
- {
3588
- key: "fleet",
3589
- reason: "Fleet supervision knobs: a repo-committed config could enable autonomous subagent spawning/steering/termination and mailbox traffic on the victim machine."
3590
- },
3591
- {
3592
- key: "brain",
3593
- reason: "Brain decision-layer knobs: a repo-committed config could raise the autonomy risk ceiling, switch the Brain to headless (removing the human tier), or reroute Brain decisions to an attacker-chosen provider/model."
3594
- },
3595
- {
3596
- key: "git",
3597
- reason: "Carries git.identity (GIT_AUTHOR_*/GIT_COMMITTER_* injection): a repo-committed config could spoof the author identity written into the victim's commit history (impersonation)."
3598
- },
3599
- {
3600
- key: "fallbackMaxLastResortCandidates",
3601
- reason: "Bounds how many of the user's OTHER configured providers may be swept in as last-resort failover. Setting it to 0 from a repo-committed config would silently strip that depth during an outage. It was already stripped in practice (absent from the allow-list) but was missing from the key registry, so this gate never checked it."
3602
- }
3603
- ];
3604
- var KNOWN_CONFIG_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
3605
- "version",
3606
- "activeProfile",
3607
- "provider",
3608
- "model",
3609
- "apiKey",
3610
- "baseUrl",
3611
- "maxConcurrent",
3612
- "uiLocale",
3613
- "providers",
3614
- "models",
3615
- "modelMatrix",
3616
- "context",
3617
- "tools",
3618
- "mcpServers",
3619
- "fallbackModels",
3620
- "fallbackBridge",
3621
- "fallbackProfiles",
3622
- "fallbackProfile",
3623
- "favoriteModels",
3624
- "favoriteModelsOnly",
3625
- "modelAvailabilitySchedule",
3626
- "fallbackAuto",
3627
- "fallbackStickiness",
3628
- "fallbackMaxLastResortCandidates",
3629
- "hooks",
3630
- "plugins",
3631
- "pluginManager",
3632
- "log",
3633
- "features",
3634
- "Sage",
3635
- "skills",
3636
- "yolo",
3637
- "nextPrediction",
3638
- "cwd",
3639
- "autonomy",
3640
- "hints",
3641
- "debugStream",
3642
- "configScope",
3643
- "indexing",
3644
- "circuitBreaker",
3645
- "adaptiveConcurrency",
3646
- "launch",
3647
- "session",
3648
- "modelRuntime",
3649
- "hq",
3650
- "sync",
3651
- "cloudSync",
3652
- "extensions",
3653
- "acp",
3654
- "fleet",
3655
- "brain",
3656
- "git"
3657
- ]);
3658
- var IN_PROJECT_DENIED_PATHS = [
3659
- {
3660
- path: "tools.exec.allow",
3661
- reason: "Extends the exec allow-list; a repo could authorise its own binaries."
3662
- },
3663
- { path: "tools.exec.danger", reason: "Weakens the destructive-command banner." },
3664
- {
3665
- // The whole subtree, not just the dangerous leaves: a persona's
3666
- // `instruction` is rendered into the voter SYSTEM prompt, a profile seat
3667
- // may pin providerId/model, and `defaultProfile` selects which of those
3668
- // runs when the agent names no profile. Denying the parent leaves no leaf
3669
- // to reclassify wrongly later.
3670
- path: "tools.council",
3671
- reason: "Council tool panel definitions: persona instructions are injected into the voter SYSTEM prompt and profile seats can pin an attacker-chosen providerId/model."
3672
- },
3673
- { path: "skills.extraDirs", reason: "Loads skill definitions from repo-chosen directories." },
3674
- { path: "skills.registryUrl", reason: "Redirects skill installs to a repo-chosen host." },
3675
- // Deliberately NOT denied: skills.mode and skills.eagerMaxChars. The audit
3676
- // suggested stripping both, but config-loader-extra.test.ts classifies `mode`
3677
- // as a safe preference with a stated rationale, and a repo can only use these
3678
- // to move injection volume in either direction — the trust problem was that
3679
- // repo-supplied skill bodies were indistinguishable from the user's own, which
3680
- // the provenance tag in system-prompt-skill-bodies.ts now fixes. Re-classifying
3681
- // them is a product call.
3682
- { path: "Sage.storage.directory", reason: "Redirects memory storage outside the profile." },
3683
- {
3684
- path: "autonomy.yolo",
3685
- reason: "Alias for the denied top-level `yolo`, and it wins over the user setting \u2014 a repo-committed config could disable every permission prompt."
3686
- },
3687
- {
3688
- path: "autonomy.defaultMode",
3689
- reason: "Sets the startup autonomy mode; autonomy is user-owned, never repo-owned."
3690
- },
3691
- // Deliberately NOT denied: autonomy.autoProceedDelayMs and
3692
- // autoProceedMaxIterations. They tune a mode the user has already switched on
3693
- // rather than granting it, and config-loader-extra.test.ts classifies the
3694
- // delay as a benign project preference. Re-classifying them is a product call.
3695
- {
3696
- path: "launch.autonomy",
3697
- reason: "Launch-time autonomy mode; same user-owned boundary as autonomy.defaultMode."
3698
- },
3699
- {
3700
- path: "features.allowOutsideProjectRoot",
3701
- reason: "Short-circuits both the project-root containment check and the symlink realpath check in the file tools."
3702
- },
3703
- {
3704
- path: "tools.restrictToProjectRoot",
3705
- reason: "The other half of the filesystem confinement switch."
3706
- },
3707
- {
3708
- // Denied for the direction that loosens: the flag defaults to false, so a
3709
- // repo can only ever use it to turn OFF a gate the user deliberately
3710
- // enabled. Same class as `tools.restrictToProjectRoot` — a control the
3711
- // operator owns, not the checked-out repository.
3712
- path: "tools.kanbanGovernance",
3713
- reason: "Repo-committed config could disable a Kanban governance gate the operator switched on, letting product mutations run outside any managed card."
3714
- },
3715
- {
3716
- // The bridge spawn path resolves the CLI entry by walking UP from the
3717
- // project root, so a repo that ships its own `packages/cli/dist/index.js`
3718
- // gets that file spawned with `process.execPath` on WebUI boot — no
3719
- // prompt, no banner. Turning the feature on is therefore equivalent to
3720
- // arbitrary code execution for a hostile checkout, which makes this an
3721
- // operator-owned switch and never a repo-owned one.
3722
- // See discover-mailbox-bridge.ts:findWorkspaceCliEntry.
3723
- path: "features.mailboxBridge",
3724
- reason: "Enables the mailbox bridge, whose CLI-entry resolution walks up from the project root \u2014 a repo-supplied packages/cli/dist/index.js would be spawned on WebUI boot."
3725
- },
3726
- {
3727
- // `plugins` is already denied above, so a repo cannot ADD a plugin. This
3728
- // closes the other half: a repo could previously ship
3729
- // `{"features":{"pluginsTrust":false}}` and switch off the integrity gate
3730
- // for plugins the user had ALREADY installed globally — disarming the
3731
- // trust-on-first-use pin that exists to catch a supply-chain update
3732
- // rewriting a plugin's entry file. Same operator-owned class as the
3733
- // switches above.
3734
- path: "features.pluginsTrust",
3735
- reason: "Disables the plugin trust-on-first-use integrity gate, re-trusting changed code in already-installed global plugins."
3736
- }
3737
- ];
3738
- function deleteNestedPath(target, path13) {
3739
- const segments = path13.split(".");
3740
- const last = segments[segments.length - 1];
3741
- if (last === void 0) return false;
3742
- let cursor = target;
3743
- const chain = [];
3744
- for (const segment of segments.slice(0, -1)) {
3745
- const next = cursor[segment];
3746
- if (!next || typeof next !== "object" || Array.isArray(next)) return false;
3747
- chain.push({ parent: cursor, key: segment });
3748
- cursor = next;
3749
- }
3750
- if (!(last in cursor)) return false;
3751
- const clonedLeaf = { ...cursor };
3752
- delete clonedLeaf[last];
3753
- let replacement = clonedLeaf;
3754
- for (let i = chain.length - 1; i >= 0; i--) {
3755
- const link = chain[i];
3756
- if (!link) continue;
3757
- replacement = { ...link.parent, [link.key]: replacement };
3758
- if (i === 0) {
3759
- for (const [k, v] of Object.entries(replacement)) target[k] = v;
3760
- return true;
3761
- }
3762
- }
3763
- for (const [k, v] of Object.entries(replacement)) target[k] = v;
3764
- return true;
3765
- }
3766
- function assertInProjectAllowListComplete() {
3767
- const missingFromBoth = [];
3768
- for (const key of KNOWN_CONFIG_TOP_LEVEL_KEYS) {
3769
- if (IN_PROJECT_ALLOWED_KEYS.has(key)) continue;
3770
- const denied = KNOWN_DENIED_IN_PROJECT.find((d) => d.key === key);
3771
- if (!denied) missingFromBoth.push(key);
3772
- }
3773
- const staleDenials = KNOWN_DENIED_IN_PROJECT.filter(
3774
- (d) => !KNOWN_CONFIG_TOP_LEVEL_KEYS.has(d.key)
3775
- ).map((d) => d.key);
3776
- const duplicate = KNOWN_DENIED_IN_PROJECT.filter((d) => IN_PROJECT_ALLOWED_KEYS.has(d.key)).map(
3777
- (d) => d.key
3778
- );
3779
- const problems = [];
3780
- if (missingFromBoth.length > 0) {
3781
- problems.push(
3782
- `new Config field(s) not classified as allowed or denied for in-project config: ` + missingFromBoth.join(", ") + ". Add each to IN_PROJECT_ALLOWED_KEYS (if safe) or KNOWN_DENIED_IN_PROJECT (with a reason)."
3783
- );
3784
- }
3785
- if (staleDenials.length > 0) {
3786
- problems.push(
3787
- `KNOWN_DENIED_IN_PROJECT references keys that no longer exist on Config: ` + staleDenials.join(", ") + ". Remove them or restore the field on Config."
3788
- );
3789
- }
3790
- if (duplicate.length > 0) {
3791
- problems.push(
3792
- `field(s) appear in BOTH IN_PROJECT_ALLOWED_KEYS and KNOWN_DENIED_IN_PROJECT: ` + duplicate.join(", ") + ". The allow-list wins at runtime; remove from one of the two."
3793
- );
3794
- }
3795
- const orphanedPaths = IN_PROJECT_DENIED_PATHS.filter(
3796
- ({ path: path13 }) => !IN_PROJECT_ALLOWED_KEYS.has(path13.split(".")[0] ?? "")
3797
- ).map(({ path: path13 }) => path13);
3798
- if (orphanedPaths.length > 0) {
3799
- problems.push(
3800
- `IN_PROJECT_DENIED_PATHS entr(ies) whose top-level parent is not allowed: ` + orphanedPaths.join(", ") + ". The parent is already stripped wholesale, so the nested denial is dead \u2014 remove it."
3801
- );
3802
- }
3803
- const malformedPaths = IN_PROJECT_DENIED_PATHS.filter(
3804
- ({ path: path13 }) => path13.split(".").length < 2 || path13.split(".").some((s) => s.length === 0)
3805
- ).map(({ path: path13 }) => path13);
3806
- if (malformedPaths.length > 0) {
3807
- problems.push(
3808
- `IN_PROJECT_DENIED_PATHS entr(ies) are not dotted nested paths: ` + malformedPaths.join(", ") + ". Top-level keys belong in KNOWN_DENIED_IN_PROJECT instead."
3809
- );
3810
- }
3811
- if (problems.length > 0) {
3812
- throw new Error(
3813
- `stripUnsafeInProjectFields drift check failed:
3814
- - ${problems.join("\n - ")}`
3815
- );
3816
- }
3817
- }
3818
- var driftChecked = false;
3819
- function stripUnsafeInProjectFields(inProject, sourcePath, warn = (msg) => console.warn(msg)) {
3820
- if (!driftChecked) {
3821
- assertInProjectAllowListComplete();
3822
- driftChecked = true;
3823
- }
3824
- const stripped = [];
3825
- const out = {};
3826
- for (const [k, v] of Object.entries(inProject)) {
3827
- if (IN_PROJECT_ALLOWED_KEYS.has(k)) {
3828
- out[k] = v;
3829
- continue;
3830
- }
3831
- stripped.push(k);
3832
- }
3833
- for (const { path: path13 } of IN_PROJECT_DENIED_PATHS) {
3834
- if (deleteNestedPath(out, path13)) stripped.push(path13);
3835
- }
3836
- if (stripped.length > 0) {
3837
- warn(
3838
- JSON.stringify({
3839
- level: "warn",
3840
- event: "config.in_project_unsafe_fields_ignored",
3841
- path: sourcePath,
3842
- ignoredKeys: stripped,
3843
- message: `Ignored ${stripped.length} field(s) from the repo-committed config "${sourcePath}": ${stripped.join(", ")}. Only a small allow-list of benign preferences (model, context, tools limits, features, \u2026) may be set by <project>/.wrongstack/config.json. Everything else must live in your active profile config.`,
3844
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
3845
- })
3846
- );
3847
- }
3848
- return out;
3849
- }
3850
-
3851
- // src/storage/config-loader/inline-provider-models.ts
3852
- var BOOLEAN_CAPABILITIES = [
3853
- "tools",
3854
- "parallelTools",
3855
- "vision",
3856
- "streaming",
3857
- "promptCache",
3858
- "systemPrompt",
3859
- "jsonMode",
3860
- "reasoning",
3861
- "topK",
3862
- "frequencyPenalty",
3863
- "presencePenalty",
3864
- "seed",
3865
- "structuredOutput",
3866
- "logprobs",
3867
- "audio",
3868
- "multipleCompletions"
3869
- ];
3870
- function isRecord2(value) {
3871
- return typeof value === "object" && value !== null && !Array.isArray(value);
3872
- }
3873
- function positiveNumber(value) {
3874
- return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
3875
- }
3876
- function readCapabilities(model) {
3877
- const raw = isRecord2(model["capabilities"]) ? model["capabilities"] : void 0;
3878
- const capabilities = {};
3879
- if (raw) {
3880
- for (const key of BOOLEAN_CAPABILITIES) {
3881
- const value = raw[key];
3882
- if (typeof value === "boolean") capabilities[key] = value;
3883
- }
3884
- const cacheControl = raw["cacheControl"];
3885
- if (cacheControl === "native" || cacheControl === "auto" || cacheControl === "none") {
3886
- capabilities.cacheControl = cacheControl;
3887
- }
3888
- }
3889
- const limit = isRecord2(model["limit"]) ? model["limit"] : void 0;
3890
- const maxContext = positiveNumber(raw?.["maxContext"]) ?? positiveNumber(model["maxContext"]) ?? positiveNumber(model["contextWindow"]) ?? positiveNumber(limit?.["context"]);
3891
- if (maxContext !== void 0) capabilities.maxContext = maxContext;
3892
- const capabilityMaxOutput = positiveNumber(raw?.["maxOutput"]);
3893
- if (capabilityMaxOutput !== void 0) capabilities.maxOutput = capabilityMaxOutput;
3894
- if (typeof model["tool_call"] === "boolean" && capabilities.tools === void 0) {
3895
- capabilities.tools = model["tool_call"];
3896
- }
3897
- if (typeof model["reasoning"] === "boolean" && capabilities.reasoning === void 0) {
3898
- capabilities.reasoning = model["reasoning"];
3899
- }
3900
- const modalities = isRecord2(model["modalities"]) ? model["modalities"] : void 0;
3901
- const inputModalities = modalities?.["input"];
3902
- if (Array.isArray(inputModalities) && capabilities.vision === void 0) {
3903
- capabilities.vision = inputModalities.includes("image");
3904
- }
3905
- return Object.keys(capabilities).length > 0 ? capabilities : void 0;
3906
- }
3907
- function inlineDefinition(model) {
3908
- const definition = {};
3909
- const name = model["name"];
3910
- if (typeof name === "string" && name.trim().length > 0) definition.name = name;
3911
- const provider = model["provider"];
3912
- if (typeof provider === "string" && provider.trim().length > 0) {
3913
- definition.provider = provider;
3914
- }
3915
- const limit = isRecord2(model["limit"]) ? model["limit"] : void 0;
3916
- const maxOutput = positiveNumber(model["maxOutput"]) ?? positiveNumber(limit?.["output"]) ?? positiveNumber(
3917
- isRecord2(model["capabilities"]) ? model["capabilities"]["maxOutput"] : void 0
3918
- );
3919
- if (maxOutput !== void 0) definition.maxOutput = maxOutput;
3920
- const capabilities = readCapabilities(model);
3921
- if (capabilities) definition.capabilities = capabilities;
3922
- const { id: _id, ...rest } = model;
3923
- if (Object.keys(rest).length > 0) {
3924
- definition.modelsDev = rest;
3925
- }
3926
- return definition;
3927
- }
3928
- function mergeDefinitions(base, patch) {
3929
- const mergedModelsDev = (() => {
3930
- if (!base?.modelsDev && !patch.modelsDev) return void 0;
3931
- const baseMd = base?.modelsDev ?? {};
3932
- const patchMd = patch.modelsDev ?? {};
3933
- const result = { ...baseMd, ...patchMd };
3934
- for (const nestedKey of ["limit", "cost", "modalities"]) {
3935
- const bn = baseMd?.[nestedKey];
3936
- const pn = patchMd?.[nestedKey];
3937
- if (bn || pn) {
3938
- result[nestedKey] = { ...bn ?? {}, ...pn ?? {} };
3939
- }
3940
- }
3941
- return result;
3942
- })();
3943
- return {
3944
- ...base,
3945
- ...patch,
3946
- ...base?.capabilities || patch.capabilities ? { capabilities: { ...base?.capabilities, ...patch.capabilities } } : {},
3947
- ...mergedModelsDev ? { modelsDev: mergedModelsDev } : {}
3948
- };
3949
- }
3950
- function normalizeInlineProviderModels(config, warn) {
3951
- const providers = config["providers"];
3952
- if (!isRecord2(providers)) return;
3953
- for (const [providerId, rawProvider] of Object.entries(providers)) {
3954
- if (!isRecord2(rawProvider) || !Array.isArray(rawProvider["models"])) continue;
3955
- const ids = [];
3956
- const seen = /* @__PURE__ */ new Set();
3957
- const derived = /* @__PURE__ */ new Map();
3958
- for (const [index, entry] of rawProvider["models"].entries()) {
3959
- let id;
3960
- let definition;
3961
- if (typeof entry === "string" && entry.length > 0) {
3962
- id = entry;
3963
- } else if (isRecord2(entry)) {
3964
- const rawId = entry["id"];
3965
- if (typeof rawId === "string" && rawId.trim().length > 0) {
3966
- id = rawId.trim();
3967
- if (!FORBIDDEN_PROTO_KEYS.has(id)) definition = inlineDefinition(entry);
3968
- }
3969
- }
3970
- if (!id || FORBIDDEN_PROTO_KEYS.has(id)) {
3971
- warn?.("Ignoring invalid inline provider model entry", {
3972
- event: "config.invalid_inline_provider_model",
3973
- provider: providerId,
3974
- index
3975
- });
3976
- continue;
3977
- }
3978
- if (!seen.has(id)) {
3979
- seen.add(id);
3980
- ids.push(id);
3981
- }
3982
- if (definition) derived.set(id, mergeDefinitions(derived.get(id), definition));
3983
- }
3984
- rawProvider["models"] = ids;
3985
- if (derived.size === 0) continue;
3986
- const explicit = isRecord2(rawProvider["customModels"]) ? rawProvider["customModels"] : void 0;
3987
- const normalized = /* @__PURE__ */ Object.create(null);
3988
- for (const [id, definition] of derived) normalized[id] = definition;
3989
- if (explicit) {
3990
- for (const [id, value] of Object.entries(explicit)) {
3991
- if (FORBIDDEN_PROTO_KEYS.has(id) || !isRecord2(value)) continue;
3992
- normalized[id] = mergeDefinitions(normalized[id], value);
3993
- }
3994
- }
3995
- rawProvider["customModels"] = normalized;
3996
- }
3997
- }
3998
-
3999
- // src/storage/config-loader/legacy-sage-migration.ts
4000
- function migrateLegacySuperMemoryKey(config) {
4001
- const legacy = config["superMemory"];
4002
- if (legacy === void 0) return false;
4003
- if (isPlainRecord(legacy)) {
4004
- const current = config["Sage"];
4005
- config["Sage"] = isPlainRecord(current) ? { ...legacy, ...current } : { ...legacy };
4006
- }
4007
- delete config["superMemory"];
4008
- return true;
4009
- }
4010
- function removeLegacySageEngine(config) {
4011
- const migratedKey = migrateLegacySuperMemoryKey(config);
4012
- const Sage = config["Sage"];
4013
- if (!isPlainRecord(Sage)) return migratedKey;
4014
- const storage = Sage["storage"];
4015
- if (!isPlainRecord(storage) || !Object.hasOwn(storage, "engine")) {
4016
- return migratedKey;
4017
- }
4018
- delete storage["engine"];
4019
- return true;
4020
- }
4021
-
4022
- // src/storage/config-loader/path-identity.ts
4023
- import * as path6 from "node:path";
4024
- function samePath(a, b) {
4025
- let ra = path6.resolve(a);
4026
- let rb = path6.resolve(b);
4027
- if (process.platform === "win32" || process.platform === "darwin") {
4028
- ra = ra.toLowerCase();
4029
- rb = rb.toLowerCase();
4030
- }
4031
- return ra === rb;
4032
- }
4033
-
4034
- // src/storage/config-loader.ts
4035
- function deepMerge2(base, patch) {
4036
- const opts = { arrayMode: "concat-primitives" };
4037
- if (envBoolOptional(process.env.WRONGSTACK_DEBUG_CONFIG)) {
4038
- opts.onNonPrimitiveArrayReplace = (key, existingLen, patchLen) => {
4039
- console.warn(
4040
- `[config] Non-primitive array for "${key}" replaced (global + local config merge). Global entries: ${existingLen}, local entries: ${patchLen}.`
4041
- );
4042
- };
4043
- }
4044
- return deepMerge(
4045
- base,
4046
- patch,
4047
- opts
4048
- );
4049
- }
4050
- var DefaultConfigLoader = class _DefaultConfigLoader {
4051
- paths;
4052
- strict;
4053
- vault;
4054
- extraSources;
4055
- events;
4056
- traceId;
4057
- logger;
4058
- jsonCache = /* @__PURE__ */ new Map();
4059
- constructor(opts) {
4060
- this.paths = opts.paths;
4061
- this.strict = opts.strict ?? false;
4062
- this.vault = opts.vault;
4063
- this.extraSources = opts.sources ?? [];
4064
- this.events = opts.events;
4065
- this.traceId = opts.traceId;
4066
- this.logger = opts.logger;
4067
- }
4068
- /**
4069
- * Emit a structured warning. Uses the configured Logger when available;
4070
- * falls back to console.warn(JSON) so warnings are never silently dropped
4071
- * during early boot (before a Logger is constructed).
4072
- */
4073
- logWarn(msg, ctx) {
4074
- if (this.logger) {
4075
- this.logger.warn(msg, ctx);
4076
- } else {
4077
- console.warn(JSON.stringify({ ...ctx, message: msg, timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
4078
- }
4079
- }
4080
- async load(opts = {}) {
4081
- let cfg = { ...CONFIG_BEHAVIOR_DEFAULTS };
4082
- await this.ensureGlobalDefaults();
4083
- const inProjectCollides = samePath(this.paths.inProjectConfig, this.paths.globalConfig) || samePath(this.paths.inProjectConfig, this.paths.projectLocalConfig);
4084
- const [bootstrap, local, inProject] = await Promise.all([
4085
- this.readJson(this.paths.globalConfig),
4086
- this.readJson(this.paths.projectLocalConfig),
4087
- inProjectCollides ? Promise.resolve({}) : this.readJson(this.paths.inProjectConfig)
4088
- ]);
4089
- const bootstrapMetadata = {
4090
- ...typeof bootstrap.version === "number" ? { version: bootstrap.version } : {},
4091
- ...typeof bootstrap.activeProfile === "string" ? { activeProfile: safeProfileName(bootstrap.activeProfile) } : {}
4092
- };
4093
- cfg = deepMerge2(cfg, bootstrapMetadata);
4094
- const profileName = safeProfileName(
4095
- bootstrap.activeProfile
4096
- );
4097
- const profileCfg = await this.readJson(this.paths.profileConfig(profileName));
4098
- if (Object.keys(profileCfg).length > 0) {
4099
- const profileSettings = { ...profileCfg };
4100
- delete profileSettings["version"];
4101
- delete profileSettings["activeProfile"];
4102
- cfg = deepMerge2(cfg, profileSettings);
4103
- }
4104
- const localSettings = { ...local };
4105
- delete localSettings["version"];
4106
- delete localSettings["activeProfile"];
4107
- cfg = deepMerge2(cfg, localSettings);
4108
- cfg = deepMerge2(
4109
- cfg,
4110
- stripUnsafeInProjectFields(
4111
- inProject,
4112
- this.paths.inProjectConfig,
4113
- this.logger ? (msg) => this.logger.warn(msg) : void 0
4114
- )
4115
- );
4116
- for (const [key, fn] of Object.entries(ENV_MAP)) {
4117
- const v = process.env[key];
4118
- if (v) fn(cfg, v);
4119
- }
4120
- const sorted = [...this.extraSources].sort((a, b) => {
4121
- const pd = (a.priority ?? 50) - (b.priority ?? 50);
4122
- if (pd !== 0) return pd;
4123
- return a.name.localeCompare(b.name);
4124
- });
4125
- for (const src of sorted) {
4126
- try {
4127
- const patch = await src.read();
4128
- if (patch && Object.keys(patch).length > 0) {
4129
- cfg = deepMerge2(cfg, patch);
4130
- }
4131
- } catch (err) {
4132
- this.logWarn("Config source load failed", {
4133
- event: "config.source_load_failed",
4134
- source: src.name,
4135
- message: toErrorMessage(err)
4136
- });
4137
- }
4138
- }
4139
- if (opts.cliFlags) {
4140
- cfg = deepMerge2(cfg, opts.cliFlags);
4141
- }
4142
- normalizeInlineProviderModels(
4143
- cfg,
4144
- (message, context) => this.logWarn(message, context)
4145
- );
4146
- if (this.vault) {
4147
- cfg = decryptConfigSecrets(cfg, this.vault);
4148
- }
4149
- if (cfg.providers) {
4150
- for (const pcfg of Object.values(cfg.providers)) {
4151
- if (!pcfg || typeof pcfg !== "object") continue;
4152
- const rawKeys = pcfg.apiKeys;
4153
- if (!Array.isArray(rawKeys) || rawKeys.length === 0) continue;
4154
- const keys = rawKeys.filter(
4155
- (k) => !!k && typeof k === "object" && typeof k.label === "string" && typeof k.apiKey === "string"
4156
- );
4157
- if (keys.length === 0) continue;
4158
- const existing = pcfg.apiKey;
4159
- if (existing && existing.length > 0) continue;
4160
- const activeLabel = pcfg.activeKey;
4161
- const chosen = activeLabel ? keys.find((k) => k.label === activeLabel) ?? keys[0] : keys[0];
4162
- if (chosen?.apiKey) {
4163
- pcfg.apiKey = chosen.apiKey;
4164
- }
4165
- }
4166
- }
4167
- removeLegacySageEngine(cfg);
4168
- this.validateBehavior(cfg);
4169
- if (this.strict && !opts.skipIdentityValidation) {
4170
- this.validateIdentity(cfg);
4171
- }
4172
- return Object.freeze(cfg);
4173
- }
4174
- /** Check whether a config object contains only the two bootstrap keys. */
4175
- static isBootstrapOnly(config) {
4176
- return isBootstrapOnly(config);
4177
- }
4178
- async ensureGlobalDefaults() {
4179
- const fp = this.paths.globalConfig;
4180
- const t0 = Date.now();
4181
- try {
4182
- await withFileLock(fp, async () => {
4183
- let parsed;
4184
- let fileExisted = true;
4185
- try {
4186
- const raw = await fs5.readFile(fp, "utf8");
4187
- const result = safeParse(raw);
4188
- if (!result.ok || !isPlainRecord(result.value)) {
4189
- return;
4190
- }
4191
- parsed = result.value;
4192
- } catch (err) {
4193
- if (err.code !== "ENOENT") {
4194
- this.events?.emit("storage.error", {
4195
- sessionId: "~config~",
4196
- store: "config",
4197
- filePath: fp,
4198
- operation: "ensure_defaults",
4199
- outcome: "failure",
4200
- error: storageErrorString(err),
4201
- recoverable: false,
4202
- durationMs: Date.now() - t0,
4203
- ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
4204
- });
4205
- this.logWarn("Config defaults read failed", {
4206
- event: "config.defaults_read_failed",
4207
- path: fp,
4208
- message: toErrorMessage(err)
4209
- });
4210
- return;
4211
- }
4212
- fileExisted = false;
4213
- parsed = {};
4214
- }
4215
- if (parsed["version"] !== void 0 && parsed["version"] !== 1) return;
4216
- const rawProfileName = parsed.activeProfile;
4217
- const profileName = safeProfileName(rawProfileName);
4218
- const profileFp = this.paths.profileConfig(profileName);
4219
- await this.ensureProfileConfig(profileFp, parsed, fileExisted);
4220
- const bootstrap = {
4221
- version: 1,
4222
- activeProfile: profileName
4223
- };
4224
- let needsBootstrapWrite = false;
4225
- if (parsed.version !== 1) {
4226
- needsBootstrapWrite = true;
4227
- }
4228
- if (parsed.activeProfile === void 0 || parsed.activeProfile !== profileName) {
4229
- needsBootstrapWrite = true;
4230
- }
4231
- if (!_DefaultConfigLoader.isBootstrapOnly(parsed)) {
4232
- needsBootstrapWrite = true;
4233
- }
4234
- if (needsBootstrapWrite) {
4235
- await backupConfigFile(fp, this.paths);
4236
- await atomicWrite(fp, JSON.stringify(bootstrap, null, 2), { mode: 384 });
4237
- this.events?.emit("storage.write", {
4238
- sessionId: "~config~",
4239
- store: "config",
4240
- filePath: fp,
4241
- operation: "ensure_defaults",
4242
- outcome: "success",
4243
- durationMs: Date.now() - t0,
4244
- ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
4245
- });
4246
- }
4247
- });
4248
- } catch (err) {
4249
- this.events?.emit("storage.error", {
4250
- sessionId: "~config~",
4251
- store: "config",
4252
- filePath: fp,
4253
- operation: "ensure_defaults",
4254
- outcome: "failure",
4255
- error: storageErrorString(err),
4256
- recoverable: false,
4257
- durationMs: Date.now() - t0,
4258
- ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
4259
- });
4260
- this.logWarn("Config defaults write failed", {
4261
- event: "config.defaults_write_failed",
4262
- path: fp,
4263
- message: toErrorMessage(err)
4264
- });
4265
- }
4266
- }
4267
- /**
4268
- * Ensure a profile config file exists at the given path.
4269
- * On first boot: migrate content from the old flat global config, or seed
4270
- * with behavior defaults. Subsequent boots: fill any missing keys from
4271
- * CONFIG_BEHAVIOR_DEFAULTS (keeping user settings intact).
4272
- *
4273
- * Once a profile file exists, settings found in the root bootstrap are
4274
- * ignored. This prevents stale or accidentally-written root values from
4275
- * becoming a hidden settings source alongside profiles.
4276
- */
4277
- async ensureProfileConfig(profileFp, oldGlobalParsed, oldFileExisted) {
4278
- const t0 = Date.now();
4279
- let parsed;
4280
- let existed = true;
4281
- try {
4282
- const raw = await fs5.readFile(profileFp, "utf8");
4283
- const result = safeParse(raw);
4284
- if (!result.ok || !isPlainRecord(result.value)) {
4285
- this.logWarn("Profile config parse failed \u2014 falling back to defaults", {
4286
- event: "config.profile_parse_failed",
4287
- path: profileFp
4288
- });
4289
- parsed = {};
4290
- } else {
4291
- parsed = result.value;
4292
- }
4293
- } catch (err) {
4294
- if (err.code !== "ENOENT") {
4295
- this.logWarn("Profile config read failed", {
4296
- event: "config.profile_read_failed",
4297
- path: profileFp,
4298
- message: toErrorMessage(err)
4299
- });
4300
- return;
4301
- }
4302
- existed = false;
4303
- parsed = {};
4304
- }
4305
- const removedLegacyEngine = removeLegacySageEngine(parsed);
4306
- let seed;
4307
- if (!existed && oldFileExisted && Object.keys(oldGlobalParsed).length >= 1) {
4308
- seed = { ...oldGlobalParsed };
4309
- delete seed["activeProfile"];
4310
- removeLegacySageEngine(seed);
4311
- const filled = fillMissingDefaults(seed, CONFIG_BEHAVIOR_DEFAULTS);
4312
- seed = filled.value;
4313
- } else {
4314
- const filled = fillMissingDefaults(
4315
- parsed,
4316
- CONFIG_BEHAVIOR_DEFAULTS
4317
- );
4318
- if (!filled.changed && !removedLegacyEngine) return;
4319
- seed = filled.value;
4320
- }
4321
- await backupConfigFile(profileFp, this.paths);
4322
- await atomicWrite(profileFp, JSON.stringify(seed, null, 2), { mode: 384 });
4323
- this.events?.emit("storage.write", {
4324
- sessionId: "~config~",
4325
- store: "config",
4326
- filePath: profileFp,
4327
- operation: "ensure_profile_defaults",
4328
- outcome: "success",
4329
- durationMs: Date.now() - t0,
4330
- ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
4331
- });
4332
- }
4333
- /**
4334
- * Persist sync config to the active profile's sync.json, with the token encrypted
4335
- * by the vault (if provided). The file is isolated from the main config
4336
- * hierarchy to prevent accidental commits.
4337
- */
4338
- async persistSyncConfig(cfg) {
4339
- let toWrite = { ...cfg };
4340
- if (this.vault && toWrite.githubToken && !toWrite.githubToken.startsWith("enc:")) {
4341
- toWrite = { ...toWrite, githubToken: this.vault.encrypt(toWrite.githubToken) };
4342
- }
4343
- const fp = this.paths.syncConfig;
4344
- const t0 = Date.now();
4345
- try {
4346
- await atomicWrite(fp, JSON.stringify(toWrite, null, 2), { mode: 384 });
4347
- this.events?.emit("storage.write", {
4348
- sessionId: "~config~",
4349
- store: "config",
4350
- filePath: fp,
4351
- operation: "persist_sync",
4352
- outcome: "success",
4353
- durationMs: Date.now() - t0,
4354
- ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
4355
- });
4356
- } catch (err) {
4357
- this.events?.emit("storage.error", {
4358
- sessionId: "~config~",
4359
- store: "config",
4360
- filePath: fp,
4361
- operation: "persist_sync",
4362
- outcome: "failure",
4363
- error: storageErrorString(err),
4364
- recoverable: false,
4365
- durationMs: Date.now() - t0,
4366
- ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
4367
- });
4368
- throw err;
4369
- }
4370
- }
4371
- /**
4372
- * Read the active profile's sync.json (encrypted GitHub token storage) and decrypt
4373
- * the token if a vault is available. Returns null if the file doesn't exist.
4374
- * This is separate from main config loading because sync.json is intentionally
4375
- * isolated — it should never be part of project-local or env-driven config.
4376
- */
4377
- async loadSyncConfig() {
4378
- const fp = this.paths.syncConfig;
4379
- const t0 = Date.now();
4380
- try {
4381
- const raw = await fs5.readFile(fp, "utf8");
4382
- const parsed = safeParse(raw);
4383
- if (!parsed.ok || !parsed.value) {
4384
- this.events?.emit("storage.read", {
4385
- sessionId: "~config~",
4386
- store: "config",
4387
- filePath: fp,
4388
- operation: "load_sync",
4389
- outcome: "failure",
4390
- durationMs: Date.now() - t0,
4391
- error: "parse error or empty file",
4392
- ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
4393
- });
4394
- return null;
4395
- }
4396
- if (this.vault) {
4397
- const decrypted = decryptConfigSecrets({ sync: parsed.value }, this.vault);
4398
- const result = decrypted.sync ?? null;
4399
- this.events?.emit("storage.read", {
4400
- sessionId: "~config~",
4401
- store: "config",
4402
- filePath: fp,
4403
- operation: "load_sync",
4404
- outcome: "success",
4405
- durationMs: Date.now() - t0,
4406
- ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
4407
- });
4408
- return result;
4409
- }
4410
- this.events?.emit("storage.read", {
4411
- sessionId: "~config~",
4412
- store: "config",
4413
- filePath: fp,
4414
- operation: "load_sync",
4415
- outcome: "success",
4416
- durationMs: Date.now() - t0,
4417
- ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
4418
- });
4419
- return parsed.value;
4420
- } catch (err) {
4421
- if (err.code === "ENOENT") return null;
4422
- this.events?.emit("storage.read", {
4423
- sessionId: "~config~",
4424
- store: "config",
4425
- filePath: fp,
4426
- operation: "load_sync",
4427
- outcome: "failure",
4428
- durationMs: Date.now() - t0,
4429
- error: storageErrorString(err),
4430
- ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
4431
- });
4432
- this.logWarn("Config sync load failed", {
4433
- event: "config.sync_load_failed",
4434
- message: toErrorMessage(err)
4435
- });
4436
- return null;
4437
- }
4438
- }
4439
- async readJson(file) {
4440
- const t0 = Date.now();
4441
- let mtimeMs = null;
4442
- try {
4443
- const stat5 = await fs5.stat(file);
4444
- mtimeMs = stat5.mtimeMs;
4445
- const cached = this.jsonCache.get(file);
4446
- if (cached && cached.mtimeMs === mtimeMs) {
4447
- return structuredClone(cached.value);
4448
- }
4449
- } catch (err) {
4450
- if (err.code === "ENOENT") {
4451
- this.jsonCache.set(file, { mtimeMs: null, value: {} });
4452
- return {};
4453
- }
4454
- this.events?.emit("storage.read", {
4455
- sessionId: "~config~",
4456
- store: "config",
4457
- filePath: file,
4458
- operation: "read_json",
4459
- outcome: "failure",
4460
- durationMs: Date.now() - t0,
4461
- error: storageErrorString(err),
4462
- ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
4463
- });
4464
- this.logWarn("Config read failed (stat)", {
4465
- event: "config.read_failed",
4466
- path: file,
4467
- message: toErrorMessage(err)
4468
- });
4469
- return {};
4470
- }
4471
- let raw;
4472
- try {
4473
- raw = await fs5.readFile(file, "utf8");
4474
- } catch (err) {
4475
- if (err.code !== "ENOENT") {
4476
- this.events?.emit("storage.read", {
4477
- sessionId: "~config~",
4478
- store: "config",
4479
- filePath: file,
4480
- operation: "read_json",
4481
- outcome: "failure",
4482
- durationMs: Date.now() - t0,
4483
- error: storageErrorString(err),
4484
- ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
4485
- });
4486
- this.logWarn("Config read failed (read)", {
4487
- event: "config.read_failed",
4488
- path: file,
4489
- message: toErrorMessage(err)
4490
- });
4491
- }
4492
- this.jsonCache.set(file, { mtimeMs: null, value: {} });
4493
- return {};
4494
- }
4495
- const parsed = safeParse(raw);
4496
- if (!parsed.ok || !parsed.value) {
4497
- this.events?.emit("storage.read", {
4498
- sessionId: "~config~",
4499
- store: "config",
4500
- filePath: file,
4501
- operation: "read_json",
4502
- outcome: "failure",
4503
- durationMs: Date.now() - t0,
4504
- error: "parse error or empty file",
4505
- ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
4506
- });
4507
- this.logWarn("Config parse failed \u2014 falling back to defaults", {
4508
- event: "config.parse_failed",
4509
- path: file
4510
- });
4511
- return {};
4512
- }
4513
- migrateLegacySuperMemoryKey(parsed.value);
4514
- this.jsonCache.set(file, { mtimeMs, value: structuredClone(parsed.value) });
4515
- return parsed.value;
4516
- }
4517
- validateBehavior(cfg) {
4518
- if (cfg.version === void 0)
4519
- throw new ConfigError({
4520
- message: "Config: missing version field",
4521
- code: ERROR_CODES.CONFIG_INVALID,
4522
- context: { field: "version" }
4523
- });
4524
- if (cfg.version !== 1)
4525
- throw new ConfigError({
4526
- message: `Config: unsupported version ${cfg.version}`,
4527
- code: ERROR_CODES.CONFIG_INVALID,
4528
- context: { field: "version", actual: cfg.version }
4529
- });
4530
- const c = cfg.context;
4531
- if (!c)
4532
- throw new ConfigError({
4533
- message: "Config: missing context section",
4534
- code: ERROR_CODES.CONFIG_INVALID,
4535
- context: { field: "context" }
4536
- });
4537
- const fields = ["warnThreshold", "softThreshold", "hardThreshold"];
4538
- for (const f of fields) {
4539
- const v = c[f];
4540
- if (typeof v !== "number" || !Number.isFinite(v)) {
4541
- throw new ConfigError({
4542
- message: `Config: context.${String(f)} must be a finite number (got ${typeof v})`,
4543
- code: ERROR_CODES.CONFIG_INVALID,
4544
- context: { field: `context.${String(f)}`, actualType: typeof v }
4545
- });
4546
- }
4547
- }
4548
- if (c.warnThreshold >= c.softThreshold || c.softThreshold >= c.hardThreshold) {
4549
- throw new ConfigError({
4550
- message: "Config: context thresholds must satisfy warn < soft < hard",
4551
- code: ERROR_CODES.CONFIG_INVALID,
4552
- context: { warn: c.warnThreshold, soft: c.softThreshold, hard: c.hardThreshold }
4553
- });
4554
- }
4555
- if (c.mode !== void 0 && !isContextWindowModeSelectionId(c.mode)) {
4556
- const known = listContextWindowModes().map((m) => m.id).join(", ");
4557
- this.logWarn(
4558
- `Ignoring unknown context.mode "${c.mode}" \u2014 falling back to "${DEFAULT_CONTEXT_WINDOW_MODE_ID}"`,
4559
- { event: "config.unknown_context_mode", mode: c.mode, known }
4560
- );
4561
- c.mode = DEFAULT_CONTEXT_WINDOW_MODE_ID;
4562
- } else if (c.mode !== void 0) {
4563
- c.mode = normalizeContextWindowModeId(c.mode) ?? DEFAULT_CONTEXT_WINDOW_MODE_ID;
4564
- }
4565
- }
4566
- validateIdentity(cfg) {
4567
- if (!cfg.provider) {
4568
- throw new ConfigError({
4569
- message: "Config: no provider configured. Run `wstack init` or set WRONGSTACK_PROVIDER.",
4570
- code: ERROR_CODES.CONFIG_INVALID,
4571
- context: { field: "provider" }
4572
- });
4573
- }
4574
- if (!cfg.model) {
4575
- throw new ConfigError({
4576
- message: "Config: no model configured. Run `wstack init` or set WRONGSTACK_MODEL.",
4577
- code: ERROR_CODES.CONFIG_INVALID,
4578
- context: { field: "model" }
4579
- });
4580
- }
4581
- }
4582
- };
4583
-
4584
- // src/utils/project-identity.ts
4585
- import * as fsPromises from "node:fs/promises";
4586
- import * as path7 from "node:path";
4587
-
4588
- // src/utils/ulid.ts
4589
- import { randomBytes as randomBytes2 } from "node:crypto";
4590
- var ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
4591
- var ENCODING_LEN = ENCODING.length;
4592
- var TIME_LEN = 10;
4593
- var RANDOM_LEN = 16;
4594
- function encodeTime(now, len) {
4595
- let mod;
4596
- let str = "";
4597
- for (let i = len - 1; i >= 0; i--) {
4598
- mod = now % ENCODING_LEN;
4599
- str = ENCODING[mod] + str;
4600
- now = (now - mod) / ENCODING_LEN;
4601
- }
4602
- return str;
4603
- }
4604
- function encodeRandom(len) {
4605
- const bytes = randomBytes2(len);
4606
- let str = "";
4607
- for (let i = 0; i < len; i++) {
4608
- str += ENCODING[bytes[i] % ENCODING_LEN];
4609
- }
4610
- return str;
4611
- }
4612
- function ulid(seedTime = Date.now()) {
4613
- return encodeTime(seedTime, TIME_LEN) + encodeRandom(RANDOM_LEN);
4614
- }
4615
- function isUlid(value) {
4616
- if (value.length !== TIME_LEN + RANDOM_LEN) return false;
4617
- for (const ch of value) {
4618
- if (!ENCODING.includes(ch)) return false;
4619
- }
4620
- return true;
4621
- }
4622
-
4623
- // src/utils/project-identity.ts
4624
- var PROJECT_IDENTITY_VERSION = 1;
4625
- var PROJECT_ID_PREFIX = "proj_";
4626
- var PROJECT_IDENTITY_RELATIVE_PATH = path7.join(".wrongstack", "project.json");
4627
- var PROJECT_GITIGNORE_RULES = [
4628
- ".wrongstack/",
4629
- "!/.wrongstack/",
4630
- "/.wrongstack/*",
4631
- "!/.wrongstack/project.json"
4632
- ];
4633
- function projectIdentityPath(projectRoot) {
4634
- return path7.join(projectRoot, PROJECT_IDENTITY_RELATIVE_PATH);
4635
- }
4636
- function createProjectId(seedTime = Date.now()) {
4637
- return `${PROJECT_ID_PREFIX}${ulid(seedTime)}`;
4638
- }
4639
- function isProjectId(value) {
4640
- return value.startsWith(PROJECT_ID_PREFIX) && isUlid(value.slice(PROJECT_ID_PREFIX.length));
4641
- }
4642
- async function readProjectIdentity(projectRoot) {
4643
- const filePath = projectIdentityPath(projectRoot);
4644
- try {
4645
- return parseProjectIdentity(await fsPromises.readFile(filePath, "utf8"), filePath);
4646
- } catch (error) {
4647
- if (isMissingFileError(error)) return void 0;
4648
- throw error;
4649
- }
4650
- }
4651
- async function ensureProjectIdentity(projectRoot, idFactory = createProjectId) {
4652
- const existing = await readProjectIdentity(projectRoot);
4653
- if (existing) return { identity: existing, created: false };
4654
- const filePath = projectIdentityPath(projectRoot);
4655
- const identity = makeIdentity(idFactory());
4656
- await fsPromises.mkdir(path7.dirname(filePath), { recursive: true });
4657
- try {
4658
- await fsPromises.writeFile(filePath, serializeProjectIdentity(identity), {
4659
- encoding: "utf8",
4660
- flag: "wx"
4661
- });
4662
- return { identity, created: true };
4663
- } catch (error) {
4664
- if (!isAlreadyExistsError(error)) throw error;
4665
- const winner = await readProjectIdentity(projectRoot);
4666
- if (!winner) throw error;
4667
- return { identity: winner, created: false };
4668
- }
4669
- }
4670
- async function ensureProjectGitignore(projectRoot) {
4671
- const filePath = path7.join(projectRoot, ".gitignore");
4672
- let content = "";
4673
- try {
4674
- content = await fsPromises.readFile(filePath, "utf8");
4675
- } catch (error) {
4676
- if (!isMissingFileError(error)) throw error;
4677
- }
4678
- const hadTrailingNewline = content.endsWith("\n");
4679
- const lines = content.length === 0 ? [] : content.split(/\r?\n/);
4680
- if (hadTrailingNewline) lines.pop();
4681
- let changed = false;
4682
- for (let index = 0; index < lines.length; index++) {
4683
- if (lines[index]?.trim() === ".wrongstack/*") {
4684
- lines[index] = ".wrongstack/";
4685
- changed = true;
4686
- }
4687
- }
4688
- for (const entry of PROJECT_GITIGNORE_RULES) {
4689
- if (!lines.some((line) => line.trim() === entry)) {
4690
- if (entry === ".wrongstack/" && lines.length > 0 && lines.at(-1) !== "") lines.push("");
4691
- lines.push(entry);
4692
- changed = true;
4693
- }
4694
- }
4695
- if (!changed) return;
4696
- await fsPromises.writeFile(filePath, `${lines.join("\n")}
4697
- `, "utf8");
4698
- }
4699
- function makeIdentity(projectId) {
4700
- if (!isProjectId(projectId)) {
4701
- throw new Error(`Invalid WrongStack project id: ${projectId}`);
4702
- }
4703
- return { version: PROJECT_IDENTITY_VERSION, projectId };
4704
- }
4705
- function parseProjectIdentity(raw, filePath) {
4706
- let value;
4707
- try {
4708
- value = JSON.parse(raw);
4709
- } catch {
4710
- throw new Error(`Invalid JSON in WrongStack project identity file: ${filePath}`);
4711
- }
4712
- if (typeof value !== "object" || value === null) {
4713
- throw new Error(`Invalid WrongStack project identity file: ${filePath}`);
4714
- }
4715
- const record = value;
4716
- if (record["version"] !== PROJECT_IDENTITY_VERSION) {
4717
- throw new Error(`Unsupported WrongStack project identity version in ${filePath}`);
4718
- }
4719
- if (typeof record["projectId"] !== "string" || !isProjectId(record["projectId"])) {
4720
- throw new Error(`Invalid projectId in WrongStack project identity file: ${filePath}`);
4721
- }
4722
- return { version: PROJECT_IDENTITY_VERSION, projectId: record["projectId"] };
4723
- }
4724
- function serializeProjectIdentity(identity) {
4725
- return `${JSON.stringify(identity, null, 2)}
4726
- `;
4727
- }
4728
- function isMissingFileError(error) {
4729
- return error?.code === "ENOENT";
4730
- }
4731
- function isAlreadyExistsError(error) {
4732
- return error?.code === "EEXIST";
4733
- }
4734
-
4735
- // src/utils/project-state-guard.ts
4736
- import * as fs6 from "node:fs";
4737
- import * as fsp3 from "node:fs/promises";
4738
- import * as path8 from "node:path";
4739
- var PROJECT_STATE_DIRECTORY = ".wrongstack";
4740
- var DEFAULT_POLL_INTERVAL_MS = 1e3;
4741
- var REPAIR_DEBOUNCE_MS = 25;
4742
- var ACTIVE_GUARD_KEY = /* @__PURE__ */ Symbol.for("wrongstack.activeProjectStateGuard");
4743
- var globalScope = globalThis;
4744
- globalScope[ACTIVE_GUARD_KEY] ??= {};
4745
- var activeGuard = globalScope[ACTIVE_GUARD_KEY];
4746
- function normalizedRoot(root) {
4747
- const resolved = path8.resolve(root);
4748
- return process.platform === "win32" ? resolved.toLowerCase() : resolved;
4749
- }
4750
- function concernsProjectState(filename) {
4751
- if (filename === null) return true;
4752
- const firstSegment = String(filename).replaceAll("\\", "/").split("/")[0];
4753
- return firstSegment?.toLowerCase() === PROJECT_STATE_DIRECTORY;
4754
- }
4755
- async function startProjectStateGuard(projectRoot, options = {}) {
4756
- const root = path8.resolve(projectRoot);
4757
- const directory = path8.join(root, PROJECT_STATE_DIRECTORY);
4758
- const pollIntervalMs = Math.max(25, options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
4759
- let closed = false;
4760
- let repairPending = false;
4761
- let repairInFlight;
4762
- let repairTimer;
4763
- const ensureDirectory = async () => {
4764
- await fsp3.mkdir(directory, { recursive: true });
4765
- };
4766
- const ensureInvariant = async () => {
4767
- await ensureDirectory();
4768
- try {
4769
- await ensureProjectGitignore(root);
4770
- } catch (error) {
4771
- options.onError?.(error);
4772
- }
4773
- };
4774
- const scheduleRepair = () => {
4775
- if (closed) return;
4776
- repairPending = true;
4777
- if (repairInFlight || repairTimer) return;
4778
- repairTimer = setTimeout(() => {
4779
- repairTimer = void 0;
4780
- repairInFlight = (async () => {
4781
- while (!closed && repairPending) {
4782
- repairPending = false;
4783
- try {
4784
- const rootState = await fsp3.stat(root).catch(() => void 0);
4785
- if (!rootState?.isDirectory()) return;
4786
- await ensureInvariant();
4787
- } catch (error) {
4788
- options.onError?.(error);
4789
- }
4790
- }
4791
- })().finally(() => {
4792
- repairInFlight = void 0;
4793
- if (repairPending && !closed) scheduleRepair();
4794
- });
4795
- }, REPAIR_DEBOUNCE_MS);
4796
- repairTimer.unref?.();
4797
- };
4798
- await ensureDirectory();
4799
- await ensureProjectGitignore(root).catch((error) => options.onError?.(error));
4800
- let watcher;
4801
- try {
4802
- watcher = fs6.watch(root, { persistent: false }, (_eventType, filename) => {
4803
- if (concernsProjectState(filename) || String(filename).toLowerCase() === ".gitignore") {
4804
- scheduleRepair();
4805
- }
4806
- });
4807
- watcher.on("error", (error) => {
4808
- options.onError?.(error);
4809
- watcher = void 0;
4810
- scheduleRepair();
4811
- });
4812
- } catch (error) {
4813
- options.onError?.(error);
4814
- }
4815
- const poll = setInterval(scheduleRepair, pollIntervalMs);
4816
- poll.unref?.();
4817
- return {
4818
- directory,
4819
- close() {
4820
- if (closed) return;
4821
- closed = true;
4822
- clearInterval(poll);
4823
- if (repairTimer) clearTimeout(repairTimer);
4824
- repairTimer = void 0;
4825
- try {
4826
- watcher?.close();
4827
- } catch {
4828
- }
4829
- watcher = void 0;
4830
- }
4831
- };
4832
- }
4833
- async function activateProjectStateGuard(projectRoot, options = {}) {
4834
- const root = normalizedRoot(projectRoot);
4835
- if (activeGuard.root === root && activeGuard.guard) return activeGuard.guard;
4836
- const next = await startProjectStateGuard(projectRoot, options);
4837
- const previous = activeGuard.guard;
4838
- activeGuard.root = root;
4839
- activeGuard.guard = next;
4840
- previous?.close();
4841
- return next;
4842
- }
4843
-
4844
- // src/boot.ts
4845
- async function bootConfig(options = {}) {
4846
- const {
4847
- flags = {},
4848
- appLabel = "wstack",
4849
- loadSyncConfig = true,
4850
- skipIdentityValidation = false
4851
- } = options;
4852
- const cwd = typeof flags["cwd"] === "string" ? path12.resolve(flags["cwd"]) : process.cwd();
4853
- const pathResolver = new DefaultPathResolver(cwd);
4854
- const projectRoot = pathResolver.projectRoot;
4855
- const projectIdentityRoot = canonicalProjectRoot(projectRoot);
4856
- const userHome = os4.homedir();
4857
- const wpaths = resolveWstackPaths({ projectRoot });
4858
- assertProjectRootOutsideStateDir(projectRoot, wpaths.globalRoot);
4859
- await activateProjectStateGuard(projectRoot);
4860
- await fs9.mkdir(wpaths.globalRoot, { recursive: true });
4861
- await fs9.mkdir(wpaths.profilesDir, { recursive: true });
4862
- await fs9.mkdir(wpaths.projectDir, { recursive: true });
4863
- await fs9.mkdir(wpaths.projectSessions, { recursive: true });
4864
- let registeredProjectId;
4865
- try {
4866
- registeredProjectId = (await readProjectIdentity(projectRoot))?.projectId;
4867
- } catch {
4868
- }
4869
- await writeProjectMeta(wpaths, projectIdentityRoot, registeredProjectId);
4870
- await registerProjectInManifest(
4871
- wpaths,
4872
- projectIdentityRoot,
4873
- void 0,
4874
- cwd !== projectIdentityRoot ? cwd : void 0,
4875
- registeredProjectId
4876
- );
4877
- await ensureProjectGitignore(projectRoot).catch(() => void 0);
4878
- await migrateLegacyConfig(wpaths);
4879
- await migrateProfileFiles(wpaths);
4880
- cleanupStaleProjects(wpaths).catch((err) => {
4881
- noOpLogger.debug("cleanupStaleProjects failed", { err });
4882
- });
4883
- const bootLogger = new DefaultLogger({ stderr: true });
4884
- const vault = new DefaultSecretVault({ keyFile: wpaths.secretsKey, logger: bootLogger });
4885
- const bootstrapProfilePath = typeof wpaths.profileConfig === "function" ? wpaths.profileConfig(wpaths.profileName) : null;
4886
- const secretFiles = [wpaths.projectLocalConfig].filter(Boolean);
4887
- if (bootstrapProfilePath) secretFiles.push(bootstrapProfilePath);
4888
- for (const file of secretFiles) {
4889
- try {
4890
- const { migrated } = await migratePlaintextSecrets(file, vault, noOpLogger);
4891
- if (migrated > 0) {
4892
- writeErr(`[${appLabel}] Encrypted ${migrated} plaintext secret(s) in ${file}
4893
- `);
4894
- }
4895
- } catch {
4896
- }
4897
- }
4898
- const configLoader = new DefaultConfigLoader({ paths: wpaths, vault, logger: bootLogger });
4899
- let config;
4900
- try {
4901
- config = await configLoader.load({ cliFlags: flagsToConfigPatch(flags) });
4902
- } catch (err) {
4903
- if (skipIdentityValidation && err instanceof Error && err.message.includes("no provider configured")) {
4904
- console.warn(
4905
- JSON.stringify({
4906
- level: "warn",
4907
- event: "boot.no_provider_configured",
4908
- app: appLabel,
4909
- message: "No provider configured \u2014 setup screen will be shown",
4910
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
4911
- })
4912
- );
4913
- try {
4914
- config = await configLoader.load({
4915
- cliFlags: flagsToConfigPatch(flags),
4916
- skipIdentityValidation: true
4917
- });
4918
- } catch (fallbackErr) {
4919
- console.warn(
4920
- JSON.stringify({
4921
- level: "warn",
4922
- event: "boot.config_fallback",
4923
- message: `Config load fallback triggered: ${toErrorMessage(fallbackErr)}`,
4924
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
4925
- })
4926
- );
4927
- config = Object.freeze({});
4928
- }
4929
- } else {
4930
- throw err;
4931
- }
4932
- }
4933
- if (loadSyncConfig) {
4934
- const syncConfig = await configLoader.loadSyncConfig();
4935
- if (syncConfig) {
4936
- config = Object.freeze({ ...config, sync: syncConfig });
4937
- }
4938
- }
4939
- if (!config.hq?.projectAlias) {
4940
- try {
4941
- await ensureProjectIdentity(projectRoot);
4942
- } catch (error) {
4943
- bootLogger.warn?.("Could not initialize .wrongstack/project.json", {
4944
- event: "project.identity_init_failed",
4945
- error: toErrorMessage(error)
4946
- });
4947
- }
4948
- }
4949
- let committedProjectId;
4950
- try {
4951
- committedProjectId = (await readProjectIdentity(projectRoot))?.projectId;
4952
- } catch {
4953
- }
4954
- if (committedProjectId !== registeredProjectId) {
4955
- await writeProjectMeta(wpaths, projectIdentityRoot, committedProjectId);
4956
- await registerProjectInManifest(
4957
- wpaths,
4958
- projectIdentityRoot,
4959
- void 0,
4960
- cwd !== projectIdentityRoot ? cwd : void 0,
4961
- committedProjectId
4962
- );
4963
- }
4964
- const logger = new DefaultLogger({ level: config.log?.level ?? "info", file: wpaths.logFile });
4965
- try {
4966
- const { getProjectSessionRegistry: getProjectSessionRegistry2 } = await Promise.resolve().then(() => (init_registry(), registry_exports));
4967
- getProjectSessionRegistry2(wpaths.globalRoot);
4968
- } catch {
4969
- }
4970
- return {
4971
- cwd,
4972
- projectRoot,
4973
- userHome,
4974
- wpaths,
4975
- pathResolver,
4976
- config,
4977
- vault,
4978
- logger,
4979
- globalConfigPath: wpaths.globalConfig
4980
- };
4981
- }
4982
- function isEnabledFlag(value) {
4983
- if (value === true) return true;
4984
- if (typeof value !== "string") return false;
4985
- const normalized = value.trim().toLowerCase();
4986
- return normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "on";
4987
- }
4988
- function flagsToConfigPatch(flags) {
4989
- const patch = {};
4990
- if (typeof flags["provider"] === "string") patch.provider = flags["provider"];
4991
- if (typeof flags["model"] === "string") patch.model = flags["model"];
4992
- if (typeof flags["fallback-model"] === "string") {
4993
- const list = flags["fallback-model"].split(",").map((s) => s.trim()).filter(Boolean);
4994
- if (list.length > 0) patch.fallbackModels = list;
4995
- }
4996
- if (typeof flags["cwd"] === "string") patch.cwd = flags["cwd"];
4997
- if (typeof flags["log-level"] === "string") {
4998
- patch.log = { level: flags["log-level"] };
4999
- } else if (flags["verbose"]) {
5000
- patch.log = { level: "debug" };
5001
- } else if (flags["trace"]) {
5002
- patch.log = { level: "trace" };
5003
- }
5004
- if (flags["no-yolo"] === true) patch.yolo = false;
5005
- else if (flags["yolo"]) patch.yolo = true;
5006
- if (flags["no-features"]) {
5007
- patch.features = {
5008
- mcp: false,
5009
- plugins: false,
5010
- memory: false,
5011
- modelsRegistry: false,
5012
- skills: false
5013
- };
5014
- }
5015
- if (flags["token-saving-mode"]) {
5016
- patch.features ??= {};
5017
- patch.features.tokenSavingMode = true;
5018
- }
5019
- if (isEnabledFlag(flags["system-pro"]) || flags["system-prompt"] === "pro") {
5020
- patch.systemPrompt = { variant: "pro" };
5021
- } else if (isEnabledFlag(flags["system-lite"]) || flags["system-prompt"] === "lite") {
5022
- patch.systemPrompt = { variant: "lite" };
5023
- } else if (flags["system-prompt"] === "default") {
5024
- patch.systemPrompt = { variant: "default" };
5025
- }
5026
- if (typeof flags["token-saving-tier"] === "string") {
5027
- patch.features ??= {};
5028
- patch.features.tokenSavingMode = normalizeTokenSavingTier(
5029
- flags["token-saving-tier"]
5030
- );
5031
- }
5032
- return patch;
5033
- }
5034
- async function migrateLegacyConfig(wpaths) {
5035
- const rootFp = wpaths.globalConfig;
5036
- let legacyRaw;
5037
- try {
5038
- legacyRaw = await fs9.readFile(rootFp, "utf8");
5039
- } catch {
5040
- return;
5041
- }
5042
- const result = safeParse(legacyRaw);
5043
- if (!result.ok || !result.value || typeof result.value !== "object" || Array.isArray(result.value)) {
5044
- return;
5045
- }
5046
- const activeProfile = safeProfileName(
5047
- typeof result.value["activeProfile"] === "string" ? result.value["activeProfile"] : void 0
5048
- );
5049
- const profileFp = wpaths.profileConfig(activeProfile);
5050
- try {
5051
- await fs9.access(profileFp);
5052
- return;
5053
- } catch (err) {
5054
- if (err.code !== "ENOENT") return;
5055
- }
5056
- await fs9.mkdir(path12.dirname(profileFp), { recursive: true });
5057
- const profileContent = { ...result.value };
5058
- delete profileContent["activeProfile"];
5059
- await atomicWrite(profileFp, JSON.stringify(profileContent, null, 2), { mode: 384 });
5060
- const bootstrap = { version: 1, activeProfile };
5061
- try {
5062
- await atomicWrite(rootFp, JSON.stringify(bootstrap, null, 2), { mode: 384 });
5063
- } catch {
5064
- }
5065
- }
5066
- var PROFILE_STATE_PAIRS = [
5067
- {
5068
- globalSrc: (w) => path12.join(w.globalRoot, "statusline.json"),
5069
- profileDst: (w, n) => w.profileStatuslineConfig(n)
5070
- },
5071
- {
5072
- globalSrc: (w) => path12.join(w.globalRoot, "mode.json"),
5073
- profileDst: (w, n) => w.profileModeConfig(n)
5074
- },
5075
- {
5076
- globalSrc: (w) => path12.join(w.globalRoot, "provider-status.json"),
5077
- profileDst: (w, n) => w.profileProviderStatus(n)
5078
- },
5079
- {
5080
- globalSrc: (w) => path12.join(w.globalRoot, "update-cache.json"),
5081
- profileDst: (w, n) => w.profileUpdateCache(n)
5082
- },
5083
- {
5084
- globalSrc: (w) => path12.join(w.globalRoot, "memory.md"),
5085
- profileDst: (w, n) => path12.join(w.profilesDir, n, "memory.md")
5086
- },
5087
- {
5088
- globalSrc: (w) => path12.join(w.globalRoot, "history"),
5089
- profileDst: (w, n) => path12.join(w.profilesDir, n, "history")
5090
- },
5091
- {
5092
- globalSrc: (w) => path12.join(w.globalRoot, "sync.json"),
5093
- profileDst: (w, n) => path12.join(w.profilesDir, n, "sync.json")
5094
- },
5095
- {
5096
- globalSrc: (w) => path12.join(w.globalRoot, "sync-state.json"),
5097
- profileDst: (w, n) => path12.join(w.profilesDir, n, "sync-state.json")
5098
- },
5099
- {
5100
- globalSrc: (w) => path12.join(w.globalRoot, "prompt-usage.json"),
5101
- profileDst: (w, n) => path12.join(w.profilesDir, n, "prompt-usage.json")
5102
- },
5103
- {
5104
- globalSrc: (w) => path12.join(w.globalRoot, "custom-context-modes.json"),
5105
- profileDst: (w, n) => path12.join(w.profilesDir, n, "custom-context-modes.json")
5106
- },
5107
- {
5108
- globalSrc: (w) => path12.join(w.globalRoot, "desktop.json"),
5109
- profileDst: (w, n) => path12.join(w.profilesDir, n, "desktop.json")
5110
- },
5111
- {
5112
- globalSrc: (w) => path12.join(w.globalRoot, "installed-skills.json"),
5113
- profileDst: (w, n) => path12.join(w.profilesDir, n, "installed-skills.json")
5114
- },
5115
- ...["skills", "prompts", "instructions", "design-kits", "desktop", "settings"].map(
5116
- (directory) => ({
5117
- globalSrc: (w) => path12.join(w.globalRoot, directory),
5118
- profileDst: (w, n) => path12.join(w.profilesDir, n, directory)
5119
- })
5120
- )
5121
- ];
5122
- async function migrateProfileFiles(wpaths) {
5123
- let profileName = wpaths.profileName;
5124
- try {
5125
- const raw = await fs9.readFile(wpaths.globalConfig, "utf8");
5126
- const parsed = safeParse(raw);
5127
- if (parsed.ok && parsed.value && typeof parsed.value.activeProfile === "string") {
5128
- profileName = safeProfileName(parsed.value.activeProfile);
5129
- }
5130
- } catch {
5131
- }
5132
- try {
5133
- await fs9.mkdir(path12.join(wpaths.profilesDir, profileName), { recursive: true });
5134
- } catch {
5135
- return;
5136
- }
5137
- for (const pair of PROFILE_STATE_PAIRS) {
5138
- const src = pair.globalSrc(wpaths);
5139
- const dst = pair.profileDst(wpaths, profileName);
5140
- let srcIsDir;
5141
- try {
5142
- srcIsDir = (await fs9.stat(src)).isDirectory();
5143
- } catch {
5144
- continue;
5145
- }
5146
- try {
5147
- const dstStat = await fs9.stat(dst);
5148
- if (dstStat.isDirectory() === srcIsDir) {
5149
- continue;
5150
- }
5151
- await fs9.rm(dst, { recursive: true, force: true });
5152
- } catch (err) {
5153
- const code = err.code;
5154
- if (code !== "ENOENT") {
5155
- writeErr(`migrateProfileFiles: skipping ${dst} (stat error: ${code ?? "unknown"})
5156
- `);
5157
- continue;
5158
- }
5159
- }
5160
- try {
5161
- if (srcIsDir) {
5162
- await fs9.cp(src, dst, { recursive: true, errorOnExist: false, force: false });
5163
- } else {
5164
- const content = await fs9.readFile(src);
5165
- await fs9.writeFile(dst, content, { mode: 384 });
5166
- }
5167
- } catch {
5168
- }
5169
- }
5170
- }
5171
- function assertProjectRootOutsideStateDir(projectRoot, globalRoot) {
5172
- const stateNamespace = path12.resolve(globalRoot, "projects");
5173
- const rel = path12.relative(stateNamespace, path12.resolve(projectRoot));
5174
- if (rel.startsWith("..") || path12.isAbsolute(rel)) return;
5175
- throw new Error(
5176
- `Refusing to start: the resolved project root is inside WrongStack's per-project state directory.
5177
- project root: ${projectRoot}
5178
- state dir: ${stateNamespace}
5179
- This usually means a WrongStack process was spawned with a state path as its working directory. Start from a real project directory, or pass --cwd <path>.`
5180
- );
5181
- }
5182
- async function writeProjectMeta(paths, projectRoot, projectId) {
5183
- try {
5184
- await fs9.mkdir(paths.projectDir, { recursive: true });
5185
- const meta = {
5186
- hash: paths.projectHash,
5187
- slug: paths.projectSlug,
5188
- root: projectRoot,
5189
- ...projectId ? { projectId } : {},
5190
- lastSeen: (/* @__PURE__ */ new Date()).toISOString()
5191
- };
5192
- await atomicWrite(paths.projectMeta, JSON.stringify(meta, null, 2));
5193
- } catch {
5194
- }
5195
- }
5196
- async function registerProjectInManifest(paths, projectRoot, events, workingDir, projectId) {
5197
- const manifestPath = path12.join(paths.globalRoot, "projects.json");
5198
- try {
5199
- const t0 = Date.now();
5200
- const raw = await fs9.readFile(manifestPath, "utf8");
5201
- events?.emit("storage.read", {
5202
- sessionId: "~boot~",
5203
- store: "project",
5204
- filePath: manifestPath,
5205
- operation: "manifest_read",
5206
- outcome: "success",
5207
- durationMs: Date.now() - t0
5208
- });
5209
- try {
5210
- JSON.parse(raw);
5211
- } catch {
5212
- }
5213
- } catch (err) {
5214
- events?.emit("storage.error", {
5215
- sessionId: "~boot~",
5216
- store: "project",
5217
- filePath: manifestPath,
5218
- operation: "manifest_read",
5219
- error: toErrorMessage(err),
5220
- recoverable: true
5221
- });
5222
- }
5223
- try {
5224
- let manifest;
5225
- try {
5226
- const raw = await fs9.readFile(manifestPath, "utf8");
5227
- manifest = JSON.parse(raw);
5228
- } catch {
5229
- manifest = { projects: [] };
5230
- }
5231
- const now = (/* @__PURE__ */ new Date()).toISOString();
5232
- const existing = manifest.projects.find((p) => p.root === projectRoot);
5233
- if (existing) {
5234
- existing.lastSeen = now;
5235
- if (projectId) existing.projectId = projectId;
5236
- if (workingDir) existing.lastWorkingDir = workingDir;
5237
- } else {
5238
- const slug = paths.projectSlug;
5239
- const name = path12.basename(projectRoot);
5240
- const entry = {
5241
- name,
5242
- root: projectRoot,
5243
- slug,
5244
- projectId,
5245
- lastSeen: now,
5246
- createdAt: now
5247
- };
5248
- if (workingDir) entry.lastWorkingDir = workingDir;
5249
- manifest.projects.push(entry);
5250
- }
5251
- const writeT0 = Date.now();
5252
- await atomicWrite(manifestPath, JSON.stringify(manifest, null, 2));
5253
- events?.emit("storage.write", {
5254
- sessionId: "~boot~",
5255
- store: "project",
5256
- filePath: manifestPath,
5257
- operation: "manifest_write",
5258
- outcome: "success",
5259
- durationMs: Date.now() - writeT0
5260
- });
5261
- } catch (err) {
5262
- events?.emit("storage.error", {
5263
- sessionId: "~boot~",
5264
- store: "project",
5265
- filePath: manifestPath,
5266
- operation: "manifest_write",
5267
- error: toErrorMessage(err),
5268
- recoverable: false
5269
- });
5270
- }
5271
- }
5272
- async function cleanupStaleProjects(wpaths) {
5273
- const projectsRoot = path12.dirname(wpaths.projectDir);
5274
- let entries;
5275
- try {
5276
- entries = await fs9.readdir(projectsRoot, { withFileTypes: true });
5277
- } catch {
5278
- return;
5279
- }
5280
- const stateNamespace = path12.resolve(projectsRoot);
5281
- for (const entry of entries) {
5282
- if (!entry.isDirectory()) continue;
5283
- const projectPath = path12.join(projectsRoot, entry.name);
5284
- const metaPath = path12.join(projectPath, "meta.json");
5285
- try {
5286
- const raw = await fs9.readFile(metaPath, "utf8");
5287
- const meta = JSON.parse(raw);
5288
- if (typeof meta.root !== "string") continue;
5289
- const rel = path12.relative(stateNamespace, path12.resolve(meta.root));
5290
- const nested = rel !== "" && !rel.startsWith("..") && !path12.isAbsolute(rel);
5291
- if (nested) {
5292
- await fs9.rm(projectPath, { recursive: true, force: true });
5293
- continue;
5294
- }
5295
- try {
5296
- await fs9.access(meta.root);
5297
- } catch {
5298
- await fs9.rm(projectPath, { recursive: true, force: true });
5299
- }
5300
- } catch {
5301
- }
5302
- }
5303
- }
5304
557
  export {
5305
558
  DefaultLogger,
5306
559
  DefaultPathResolver,