@evomap/evolver 1.90.0 → 2.0.0-beta.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 (231) hide show
  1. package/README.md +27 -564
  2. package/bin/evolver-llm-proxy.js +3 -0
  3. package/bin/evolver-mcp.js +2 -0
  4. package/bin/evolver-proxy.js +3 -0
  5. package/bin/evolver.js +4 -0
  6. package/index.js +1 -3670
  7. package/package.json +32 -62
  8. package/CONTRIBUTING.md +0 -19
  9. package/LICENSE +0 -641
  10. package/README.ja-JP.md +0 -521
  11. package/README.ko-KR.md +0 -520
  12. package/README.zh-CN.md +0 -531
  13. package/SKILL.md +0 -365
  14. package/assets/cover.png +0 -0
  15. package/assets/gep/genes.seed.json +0 -496
  16. package/conformance/savings-core/constants.json +0 -30
  17. package/conformance/savings-core/golden-vectors.json +0 -333
  18. package/scripts/a2a_export.js +0 -63
  19. package/scripts/a2a_ingest.js +0 -79
  20. package/scripts/a2a_promote.js +0 -118
  21. package/scripts/analyze_by_skill.js +0 -121
  22. package/scripts/build_binaries.js +0 -479
  23. package/scripts/check-changelog.js +0 -166
  24. package/scripts/extract_log.js +0 -85
  25. package/scripts/generate_history.js +0 -75
  26. package/scripts/gep_append_event.js +0 -96
  27. package/scripts/gep_personality_report.js +0 -234
  28. package/scripts/human_report.js +0 -147
  29. package/scripts/recall-verify-report.js +0 -234
  30. package/scripts/recover_loop.js +0 -61
  31. package/scripts/refresh_stars_badge.js +0 -168
  32. package/scripts/seed-merchants.js +0 -91
  33. package/scripts/skill2recipes.js +0 -118
  34. package/scripts/suggest_version.js +0 -89
  35. package/scripts/validate-modules.js +0 -38
  36. package/scripts/validate-suite.js +0 -78
  37. package/src/adapters/claudeCode.js +0 -194
  38. package/src/adapters/codex.js +0 -216
  39. package/src/adapters/cursor.js +0 -91
  40. package/src/adapters/hookAdapter.js +0 -469
  41. package/src/adapters/kiro.js +0 -195
  42. package/src/adapters/opencode.js +0 -326
  43. package/src/adapters/scripts/_lockPaths.js +0 -74
  44. package/src/adapters/scripts/_memoryFiltering.js +0 -35
  45. package/src/adapters/scripts/_runtimePaths.js +0 -440
  46. package/src/adapters/scripts/evolver-session-end.js +0 -321
  47. package/src/adapters/scripts/evolver-session-start.js +0 -587
  48. package/src/adapters/scripts/evolver-signal-detect.js +0 -98
  49. package/src/adapters/scripts/evolver-task-recall.js +0 -173
  50. package/src/atp/atpExecute.js +0 -283
  51. package/src/atp/atpTaskPickup.js +0 -233
  52. package/src/atp/autoBuyer.js +0 -382
  53. package/src/atp/autoDeliver.js +0 -215
  54. package/src/atp/cli.js +0 -354
  55. package/src/atp/cliAutobuyPrompt.js +0 -154
  56. package/src/atp/consumerAgent.js +0 -157
  57. package/src/atp/defaultHandler.js +0 -69
  58. package/src/atp/heartbeatSignalsHandler.js +0 -254
  59. package/src/atp/hubClient.js +0 -317
  60. package/src/atp/index.js +0 -38
  61. package/src/atp/merchantAgent.js +0 -118
  62. package/src/atp/protocol.js +0 -41
  63. package/src/atp/questionComposer.js +0 -133
  64. package/src/atp/serviceHelper.js +0 -92
  65. package/src/canary.js +0 -13
  66. package/src/config.js +0 -351
  67. package/src/evolve/guards.js +0 -1
  68. package/src/evolve/pipeline/collect.js +0 -1
  69. package/src/evolve/pipeline/dispatch.js +0 -1
  70. package/src/evolve/pipeline/enrich.js +0 -1
  71. package/src/evolve/pipeline/hub.js +0 -1
  72. package/src/evolve/pipeline/select.js +0 -1
  73. package/src/evolve/pipeline/signals.js +0 -1
  74. package/src/evolve/utils.js +0 -1
  75. package/src/evolve.js +0 -1
  76. package/src/experiment/agentRunner.js +0 -229
  77. package/src/experiment/cli.js +0 -159
  78. package/src/experiment/comparison.js +0 -233
  79. package/src/experiment/metrics.js +0 -75
  80. package/src/forceUpdate.js +0 -991
  81. package/src/gep/a2a.js +0 -173
  82. package/src/gep/a2aProtocol.js +0 -1
  83. package/src/gep/analyzer.js +0 -35
  84. package/src/gep/antiAbuseTelemetry.js +0 -1
  85. package/src/gep/assetCallLog.js +0 -197
  86. package/src/gep/assetStore.js +0 -723
  87. package/src/gep/assets.js +0 -36
  88. package/src/gep/autoDistillConv.js +0 -1
  89. package/src/gep/autoDistillLlm.js +0 -1
  90. package/src/gep/bridge.js +0 -138
  91. package/src/gep/candidateEval.js +0 -1
  92. package/src/gep/candidates.js +0 -1
  93. package/src/gep/claimNudge.js +0 -121
  94. package/src/gep/cliContracts.js +0 -1190
  95. package/src/gep/contentHash.js +0 -1
  96. package/src/gep/conversationDistiller.js +0 -1
  97. package/src/gep/conversationSniffer.js +0 -1
  98. package/src/gep/crypto.js +0 -1
  99. package/src/gep/curriculum.js +0 -1
  100. package/src/gep/deviceId.js +0 -1
  101. package/src/gep/directoryClient.js +0 -115
  102. package/src/gep/envFingerprint.js +0 -1
  103. package/src/gep/epigenetics.js +0 -1
  104. package/src/gep/execBridge.js +0 -1
  105. package/src/gep/executionTrace.js +0 -291
  106. package/src/gep/explore.js +0 -1
  107. package/src/gep/featureFlags.js +0 -121
  108. package/src/gep/gitOps.js +0 -265
  109. package/src/gep/hash.js +0 -1
  110. package/src/gep/hostErrorClassifier.js +0 -34
  111. package/src/gep/hubFetch.js +0 -1
  112. package/src/gep/hubReview.js +0 -1
  113. package/src/gep/hubSearch.js +0 -1
  114. package/src/gep/hubVerify.js +0 -1
  115. package/src/gep/idleScheduler.js +0 -400
  116. package/src/gep/issueReporter.js +0 -416
  117. package/src/gep/learningSignals.js +0 -1
  118. package/src/gep/llmReview.js +0 -92
  119. package/src/gep/localStateAwareness.js +0 -243
  120. package/src/gep/mailboxTransport.js +0 -119
  121. package/src/gep/memoryGraph.js +0 -1
  122. package/src/gep/memoryGraphAdapter.js +0 -1
  123. package/src/gep/mutation.js +0 -1
  124. package/src/gep/narrativeMemory.js +0 -1
  125. package/src/gep/oauthLogin.js +0 -181
  126. package/src/gep/openPRRegistry.js +0 -1
  127. package/src/gep/paths.js +0 -522
  128. package/src/gep/personality.js +0 -1
  129. package/src/gep/policyCheck.js +0 -1
  130. package/src/gep/portable.js +0 -103
  131. package/src/gep/privacyClient.js +0 -235
  132. package/src/gep/prompt.js +0 -1
  133. package/src/gep/questionGenerator.js +0 -518
  134. package/src/gep/recallInject.js +0 -1
  135. package/src/gep/recallVerifier.js +0 -1
  136. package/src/gep/reflection.js +0 -1
  137. package/src/gep/sanitize.js +0 -264
  138. package/src/gep/savingsCore.js +0 -1
  139. package/src/gep/schemas/capsule.js +0 -170
  140. package/src/gep/schemas/gene.js +0 -154
  141. package/src/gep/schemas/index.js +0 -8
  142. package/src/gep/schemas/protocol.js +0 -51
  143. package/src/gep/schemas/task.js +0 -74
  144. package/src/gep/selector.js +0 -1
  145. package/src/gep/selfPR.js +0 -469
  146. package/src/gep/signals.js +0 -776
  147. package/src/gep/skill2gep.js +0 -1056
  148. package/src/gep/skill2gepAudit.js +0 -303
  149. package/src/gep/skill2recipes.js +0 -511
  150. package/src/gep/skillDistiller.js +0 -1
  151. package/src/gep/skillPublisher.js +0 -358
  152. package/src/gep/solidify.js +0 -1
  153. package/src/gep/strategy.js +0 -1
  154. package/src/gep/taskReceiver.js +0 -575
  155. package/src/gep/tokenSavings.js +0 -1
  156. package/src/gep/trajectoryExport.js +0 -1
  157. package/src/gep/validationReport.js +0 -55
  158. package/src/gep/validator/index.js +0 -411
  159. package/src/gep/validator/reporter.js +0 -210
  160. package/src/gep/validator/sandboxExecutor.js +0 -480
  161. package/src/gep/validator/stakeBootstrap.js +0 -357
  162. package/src/gep/workspaceKeychain.js +0 -1
  163. package/src/ops/cleanup.js +0 -80
  164. package/src/ops/commentary.js +0 -60
  165. package/src/ops/health_check.js +0 -104
  166. package/src/ops/index.js +0 -11
  167. package/src/ops/innovation.js +0 -67
  168. package/src/ops/lifecycle.js +0 -798
  169. package/src/ops/self_repair.js +0 -76
  170. package/src/ops/skills_monitor.js +0 -147
  171. package/src/ops/trigger.js +0 -33
  172. package/src/proxy/clientSettings.js +0 -405
  173. package/src/proxy/envelope.js +0 -59
  174. package/src/proxy/extensions/dmHandler.js +0 -45
  175. package/src/proxy/extensions/sessionHandler.js +0 -141
  176. package/src/proxy/extensions/skillUpdater.js +0 -64
  177. package/src/proxy/extensions/traceControl.js +0 -1
  178. package/src/proxy/index.js +0 -1395
  179. package/src/proxy/inject.js +0 -1
  180. package/src/proxy/lifecycle/manager.js +0 -1568
  181. package/src/proxy/mailbox/state.js +0 -207
  182. package/src/proxy/mailbox/store.js +0 -602
  183. package/src/proxy/router/cache_passthrough.js +0 -26
  184. package/src/proxy/router/features.js +0 -84
  185. package/src/proxy/router/gemini_route.js +0 -154
  186. package/src/proxy/router/messages_route.js +0 -535
  187. package/src/proxy/router/model_router.js +0 -113
  188. package/src/proxy/router/models_route.js +0 -52
  189. package/src/proxy/router/ollama_route.js +0 -103
  190. package/src/proxy/router/responses_route.js +0 -170
  191. package/src/proxy/router/vertex_route.js +0 -110
  192. package/src/proxy/server/http.js +0 -363
  193. package/src/proxy/server/routes.js +0 -558
  194. package/src/proxy/server/settings.js +0 -115
  195. package/src/proxy/sync/engine.js +0 -179
  196. package/src/proxy/sync/inbound.js +0 -211
  197. package/src/proxy/sync/outbound.js +0 -361
  198. package/src/proxy/task/monitor.js +0 -131
  199. package/src/proxy/trace/extractor.js +0 -1
  200. package/src/proxy/trace/usage.js +0 -1
  201. package/src/solo/breaker.js +0 -25
  202. package/src/solo/gitGuard.js +0 -65
  203. package/src/webui/client/clientJs/assets.js +0 -111
  204. package/src/webui/client/clientJs/bootstrap.js +0 -92
  205. package/src/webui/client/clientJs/common.js +0 -77
  206. package/src/webui/client/clientJs/i18n.js +0 -366
  207. package/src/webui/client/clientJs/index.js +0 -35
  208. package/src/webui/client/clientJs/interactions.js +0 -351
  209. package/src/webui/client/clientJs/overview.js +0 -152
  210. package/src/webui/client/clientJs/personality.js +0 -285
  211. package/src/webui/client/clientJs/pipelines.js +0 -330
  212. package/src/webui/client/indexHtml.js +0 -221
  213. package/src/webui/client/static.js +0 -23
  214. package/src/webui/client/stylesCss.js +0 -639
  215. package/src/webui/client/vendor/README.md +0 -15
  216. package/src/webui/client/vendor/echarts.min.js +0 -45
  217. package/src/webui/index.js +0 -14
  218. package/src/webui/observer/assets.js +0 -146
  219. package/src/webui/observer/index.js +0 -37
  220. package/src/webui/observer/interactions.js +0 -127
  221. package/src/webui/observer/jsonl.js +0 -75
  222. package/src/webui/observer/paths.js +0 -46
  223. package/src/webui/observer/personality.js +0 -43
  224. package/src/webui/observer/pipelineEvents.js +0 -58
  225. package/src/webui/observer/redact.js +0 -63
  226. package/src/webui/observer/runs.js +0 -356
  227. package/src/webui/observer/safety.js +0 -57
  228. package/src/webui/observer/skills.js +0 -70
  229. package/src/webui/observer/status.js +0 -71
  230. package/src/webui/server/http.js +0 -138
  231. package/src/webui/server/routes.js +0 -41
@@ -1,363 +0,0 @@
1
- 'use strict';
2
-
3
- const crypto = require('crypto');
4
- const http = require('http');
5
- const { writeSettings, readSettings, clearSettings, clearIfStale } = require('./settings');
6
- const {
7
- isValidReusableProxyToken,
8
- readReusableClientProxyToken,
9
- syncClaudeProxySettings,
10
- } = require('../clientSettings');
11
-
12
- const MAX_PORT_ATTEMPTS = 100;
13
- const DEFAULT_PORT = 19820;
14
-
15
- // GHSA-7xp7-m392-h92c: cap request body at 1 MiB. The proxy's HTTP surface is
16
- // bound to 127.0.0.1 but still reachable by any local process (other users on
17
- // a shared dev host, container neighbors sharing the host netns, malicious
18
- // postinstall scripts). Without a cap, /asset/submit and /mailbox/send write
19
- // the full body verbatim into messages.jsonl, so an attacker can fill the
20
- // disk and make the daemon OOM on every restart (readFileSync over a multi-
21
- // GB JSONL). Tune via EVOMAP_PROXY_MAX_BODY_BYTES if a legitimate workload
22
- // truly needs bigger bodies.
23
- const DEFAULT_MAX_BODY_BYTES = 1 * 1024 * 1024;
24
- function resolveMaxBodyBytes() {
25
- const raw = Number(process.env.EVOMAP_PROXY_MAX_BODY_BYTES);
26
- if (Number.isFinite(raw) && raw > 0) return Math.floor(raw);
27
- return DEFAULT_MAX_BODY_BYTES;
28
- }
29
-
30
- function parseBody(req, opts) {
31
- const maxBytes = (opts && Number.isFinite(opts.maxBytes) && opts.maxBytes > 0)
32
- ? opts.maxBytes
33
- : resolveMaxBodyBytes();
34
- return new Promise((resolve, reject) => {
35
- const declared = Number(req.headers['content-length']);
36
- if (Number.isFinite(declared) && declared > maxBytes) {
37
- const err = new Error('Request body too large');
38
- err.statusCode = 413;
39
- return reject(err);
40
- }
41
- const chunks = [];
42
- let received = 0;
43
- let settled = false;
44
- const fail = (err) => {
45
- if (settled) return;
46
- settled = true;
47
- try { req.destroy(); } catch { /* ignore */ }
48
- reject(err);
49
- };
50
- req.on('data', (c) => {
51
- if (settled) return;
52
- received += c.length;
53
- if (received > maxBytes) {
54
- const err = new Error('Request body too large');
55
- err.statusCode = 413;
56
- return fail(err);
57
- }
58
- chunks.push(c);
59
- });
60
- req.on('end', () => {
61
- if (settled) return;
62
- settled = true;
63
- const raw = Buffer.concat(chunks).toString();
64
- if (!raw) return resolve({});
65
- try { resolve(JSON.parse(raw)); }
66
- catch (e) { reject(new Error('Invalid JSON body')); }
67
- });
68
- req.on('error', fail);
69
- });
70
- }
71
-
72
- function sendJson(res, status, body, extraHeaders = {}) {
73
- const payload = JSON.stringify(body);
74
- const headers = {
75
- 'Content-Type': 'application/json',
76
- 'Content-Length': Buffer.byteLength(payload),
77
- };
78
- for (const [name, value] of Object.entries(extraHeaders || {})) {
79
- if (value === undefined || value === null) continue;
80
- const lower = String(name).toLowerCase();
81
- if (lower === 'content-length' || lower === 'transfer-encoding' || lower === 'connection') continue;
82
- const headerValue = Array.isArray(value) ? value.join(', ') : String(value);
83
- if (/[\r\n]/.test(headerValue)) continue;
84
- headers[name] = headerValue;
85
- }
86
- res.writeHead(status, headers);
87
- res.end(payload);
88
- }
89
-
90
- function tryListen(server, port) {
91
- return new Promise((resolve, reject) => {
92
- server.once('error', (err) => {
93
- if (err.code === 'EADDRINUSE') return resolve(false);
94
- reject(err);
95
- });
96
- server.listen(port, '127.0.0.1', () => resolve(true));
97
- });
98
- }
99
-
100
- class ProxyHttpServer {
101
- constructor(routes, { port, logger, clientSettings } = {}) {
102
- this.routes = routes;
103
- this.basePort = port || Number(process.env.EVOMAP_PROXY_PORT) || DEFAULT_PORT;
104
- this.actualPort = null;
105
- this.logger = logger || console;
106
- this.server = null;
107
- this.token = null;
108
- this.clientSettings = clientSettings || null;
109
- }
110
-
111
- async start() {
112
- // Capture the prior token before clearIfStale wipes it. Daemon restarts
113
- // routinely fall into the stale branch (the previous PID is gone), and
114
- // rotating `proxy.token` on every restart invalidates ANTHROPIC_AUTH_TOKEN
115
- // already exported into long-lived shells (the .bashrc auto-source only
116
- // runs once per terminal).
117
- const priorProxy = readSettings().proxy || {};
118
- const previous = isValidReusableProxyToken(priorProxy.token) ? priorProxy.token.trim() : null;
119
- // settings.json is operator-edited; previous_tokens may contain non-strings
120
- // (numbers, booleans, objects) that would later crash Buffer.from(cand, 'utf8')
121
- // in _handleRequest as ERR_INVALID_ARG_TYPE — an unhandled rejection that
122
- // takes the daemon down under default --unhandled-rejections=throw.
123
- const priorPreviousTokens = Array.isArray(priorProxy.previous_tokens)
124
- ? priorProxy.previous_tokens
125
- .map((t) => (typeof t === 'string' ? t.trim() : ''))
126
- .filter((t) => isValidReusableProxyToken(t))
127
- : [];
128
- clearIfStale();
129
- const clientSettingsOpts = this.clientSettings || {};
130
- const clientToken = this.clientSettings
131
- ? readReusableClientProxyToken({ ...clientSettingsOpts, port: this.basePort })
132
- : null;
133
- this.token = previous || clientToken || crypto.randomBytes(32).toString('hex');
134
- this._priorPreviousTokens = priorPreviousTokens;
135
- this.server = http.createServer((req, res) => this._handleRequest(req, res));
136
-
137
- let port = this.basePort;
138
- for (let i = 0; i < MAX_PORT_ATTEMPTS; i++) {
139
- const ok = await tryListen(this.server, port);
140
- if (ok) {
141
- this.actualPort = port;
142
- const url = `http://127.0.0.1:${port}`;
143
- const proxyBlock = {
144
- url,
145
- pid: process.pid,
146
- started_at: new Date().toISOString(),
147
- token: this.token,
148
- };
149
- if (this._priorPreviousTokens && this._priorPreviousTokens.length) {
150
- proxyBlock.previous_tokens = this._priorPreviousTokens;
151
- }
152
- writeSettings({ proxy: proxyBlock });
153
- if (this.clientSettings) {
154
- try {
155
- const syncResult = syncClaudeProxySettings({
156
- ...clientSettingsOpts,
157
- url,
158
- port: this.basePort,
159
- token: this.token,
160
- runtimeEnv: process.env,
161
- });
162
- if (syncResult.synced && syncResult.changed) {
163
- this.logger.log(`[proxy] Synced Claude client settings at ${syncResult.file}`);
164
- } else if (syncResult.reason === 'invalid_settings_json') {
165
- this.logger.warn?.(`[proxy] Skipped Claude client settings sync because ${syncResult.file} is not valid JSON`);
166
- }
167
- } catch (err) {
168
- this.logger.warn?.('[proxy] Claude client settings sync failed: ' + (err && err.message ? err.message : err));
169
- }
170
- }
171
- this.logger.log(`[proxy] HTTP server listening on ${url}`);
172
- return { port, url, token: this.token };
173
- }
174
- port++;
175
- }
176
- throw new Error(`Could not find free port after ${MAX_PORT_ATTEMPTS} attempts starting from ${this.basePort}`);
177
- }
178
-
179
- async stop() {
180
- if (this.server) {
181
- await new Promise((resolve) => this.server.close(resolve));
182
- this.server = null;
183
- }
184
- clearSettings();
185
- }
186
-
187
- async _handleRequest(req, res) {
188
- const authHeader = req.headers['authorization'] || '';
189
- const provided = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : '';
190
- const provBuf = Buffer.from(provided, 'utf8');
191
- // Primary token plus any grace tokens from settings.json::proxy.previous_tokens.
192
- // The grace list is the recovery path for the rare case where settings.json was
193
- // wiped externally (logout, manual rm) while long-lived CC sessions still hold
194
- // the pre-wipe token in their fork-time env. Operator writes the lost token into
195
- // previous_tokens; once those sessions close, the operator can clear the array.
196
- // Reading directly from settings.json (instead of an env shim) keeps the
197
- // single source of truth on disk — no python bridge in the daemon hook.
198
- // Defense in depth: even though start() filters non-strings before persisting,
199
- // settings.json can be hand-edited between requests, so re-validate every read.
200
- // A non-string slipping into Buffer.from below would throw ERR_INVALID_ARG_TYPE
201
- // and unhandled-reject through the auth path.
202
- const previous = readSettings().proxy?.previous_tokens;
203
- const extras = Array.isArray(previous)
204
- ? previous
205
- .map((t) => (typeof t === 'string' ? t.trim() : ''))
206
- .filter((t) => isValidReusableProxyToken(t))
207
- : [];
208
- const candidates = [this.token, ...extras]
209
- .filter((t) => isValidReusableProxyToken(t))
210
- .map((t) => t.trim());
211
- let valid = false;
212
- for (const cand of candidates) {
213
- const expBuf = Buffer.from(cand, 'utf8');
214
- if (provBuf.length === expBuf.length && crypto.timingSafeEqual(provBuf, expBuf)) {
215
- valid = true;
216
- break;
217
- }
218
- }
219
- if (!valid) {
220
- return sendJson(res, 401, { error: 'Unauthorized' });
221
- }
222
-
223
- const url = new URL(req.url, `http://127.0.0.1:${this.actualPort}`);
224
- const routeKey = `${req.method} ${url.pathname}`;
225
-
226
- const paramMatch = this._matchRoute(req.method, url.pathname);
227
-
228
- if (!paramMatch) {
229
- return sendJson(res, 404, { error: 'Not found', path: url.pathname });
230
- }
231
-
232
- const { handler, params } = paramMatch;
233
-
234
- try {
235
- const body = (req.method === 'POST' || req.method === 'PUT') ? await parseBody(req) : {};
236
- const query = Object.fromEntries(url.searchParams);
237
- const headers = req.headers;
238
- const result = await handler({ body, query, params, headers });
239
- if (result && result.stream) {
240
- await this._streamResponse(res, result);
241
- } else {
242
- sendJson(res, result.status || 200, result.body || result, result.headers);
243
- }
244
- } catch (err) {
245
- this.logger.error(`[proxy] ${routeKey} error:`, err.message);
246
- if (res.headersSent) {
247
- try { res.end(); } catch { /* ignore */ }
248
- } else {
249
- sendJson(res, err.statusCode || 500, {
250
- error: err.message || 'Internal error',
251
- });
252
- }
253
- }
254
- }
255
-
256
- // SSE / pass-through streaming path used by /v1/messages (slice 5).
257
- // `result.stream` may be any async iterable yielding Buffer or string
258
- // chunks (Web ReadableStream from fetch, or a Node Readable). Headers
259
- // default to text/event-stream so Anthropic-style SSE bytes piped
260
- // through reach the client unmodified. The caller owns producing
261
- // correctly-framed SSE; this method only relays bytes.
262
- async _streamResponse(res, result) {
263
- const headers = Object.assign({
264
- 'Content-Type': 'text/event-stream',
265
- 'Cache-Control': 'no-cache',
266
- 'Connection': 'keep-alive',
267
- }, result.headers || {});
268
- res.writeHead(result.status || 200, headers);
269
-
270
- // Browsers, network blips, and Ctrl-C all close the SSE socket mid-stream.
271
- // If we await `drain` without watching for `close`, the drain event never
272
- // fires on a destroyed socket and this coroutine hangs forever — which
273
- // also pins the upstream fetch body open (real Anthropic socket leak, not
274
- // just coroutine leak). For Web ReadableStream upstreams we read via an
275
- // explicit reader so cancellation can go through the same lock; for sync
276
- // generators / Node Readables we fall back to for-await.
277
- const stream = result.stream;
278
- const reader = stream && typeof stream.getReader === 'function' ? stream.getReader() : null;
279
-
280
- let clientGone = false;
281
- const onClose = () => {
282
- clientGone = true;
283
- if (reader) {
284
- reader.cancel().catch(() => { /* upstream already settled */ });
285
- } else if (stream && typeof stream.destroy === 'function') {
286
- try { stream.destroy(); } catch { /* ignore */ }
287
- }
288
- };
289
- res.once('close', onClose);
290
-
291
- const awaitBackpressure = () => new Promise((resolve) => {
292
- let settled = false;
293
- const onDrain = () => {
294
- if (settled) return;
295
- settled = true;
296
- res.off('close', onCloseInner);
297
- resolve();
298
- };
299
- const onCloseInner = () => {
300
- if (settled) return;
301
- settled = true;
302
- res.off('drain', onDrain);
303
- resolve();
304
- };
305
- res.once('drain', onDrain);
306
- res.once('close', onCloseInner);
307
- });
308
-
309
- try {
310
- if (reader) {
311
- for (;;) {
312
- const { value, done } = await reader.read();
313
- if (done || clientGone) break;
314
- if (!res.write(value)) {
315
- await awaitBackpressure();
316
- if (clientGone) break;
317
- }
318
- }
319
- } else {
320
- for await (const chunk of stream) {
321
- if (clientGone) break;
322
- if (!res.write(chunk)) {
323
- await awaitBackpressure();
324
- if (clientGone) break;
325
- }
326
- }
327
- }
328
- } finally {
329
- res.off('close', onClose);
330
- try { res.end(); } catch { /* socket may already be destroyed */ }
331
- }
332
- }
333
-
334
- _matchRoute(method, pathname) {
335
- for (const [pattern, handler] of Object.entries(this.routes)) {
336
- const [routeMethod, routePath] = pattern.split(' ');
337
- if (routeMethod !== method) continue;
338
-
339
- const params = matchPath(routePath, pathname);
340
- if (params !== null) return { handler, params };
341
- }
342
- return null;
343
- }
344
- }
345
-
346
- function matchPath(pattern, pathname) {
347
- const patternParts = pattern.split('/');
348
- const pathParts = pathname.split('/');
349
-
350
- if (patternParts.length !== pathParts.length) return null;
351
-
352
- const params = {};
353
- for (let i = 0; i < patternParts.length; i++) {
354
- if (patternParts[i].startsWith(':')) {
355
- params[patternParts[i].slice(1)] = pathParts[i];
356
- } else if (patternParts[i] !== pathParts[i]) {
357
- return null;
358
- }
359
- }
360
- return params;
361
- }
362
-
363
- module.exports = { ProxyHttpServer, parseBody, sendJson, DEFAULT_PORT, DEFAULT_MAX_BODY_BYTES, resolveMaxBodyBytes };