cortad 0.1.6 → 0.1.8
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.
- package/lib/door.mjs +5 -2
- package/lib/identities.mjs +15 -3
- package/lib/mint.mjs +18 -2
- package/lib/pyhook/sitecustomize.py +211 -16
- package/lib/replay.mjs +72 -6
- package/lib/sample.mjs +218 -0
- package/lib/start.mjs +69 -14
- package/lib/trace.cjs +184 -12
- package/local.mjs +179 -22
- package/package.json +2 -1
package/lib/sample.mjs
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// Reading your own material from your own stores, here on this machine.
|
|
2
|
+
//
|
|
3
|
+
// A world's shell cannot do this and must not: it runs inside the OS sandbox, which denies every
|
|
4
|
+
// env file and every host but this one. A reader written into that shell read nothing on a real
|
|
5
|
+
// laptop: no address, no key, no network. The address and the key are in your environment file,
|
|
6
|
+
// which this program already opened to mask what your commands print, so the read happens here and
|
|
7
|
+
// only rows of short fields go back.
|
|
8
|
+
//
|
|
9
|
+
// Nothing runnable arrives from the outside: the plan names store kinds and variable NAMES, and
|
|
10
|
+
// this file decides what is asked and what is kept. Values are never sent; the rows are.
|
|
11
|
+
|
|
12
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
13
|
+
import { join, relative } from "node:path";
|
|
14
|
+
|
|
15
|
+
const ROAD = /^[\w.:/-]+$/;
|
|
16
|
+
const NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
17
|
+
const KINDS = new Set(["qdrant", "postgres", "files"]);
|
|
18
|
+
const STORES_MAX = 12;
|
|
19
|
+
const PER_STORE = 6;
|
|
20
|
+
const PER_STORE_MAX = 60;
|
|
21
|
+
// A few rows from MANY collections, never many rows from few: a store whose collection names are
|
|
22
|
+
// the product's own grid (jordan_grade-10_sem-2_math) is only read when every cell is read. Forty
|
|
23
|
+
// eight of them, twenty two rows apiece, filled the whole answer with seven of ulaim's cells and
|
|
24
|
+
// left grade 10 semester 2 maths unread, so the depth follows the breadth: how many rows a
|
|
25
|
+
// collection gets is the answer's room divided by how many collections there are.
|
|
26
|
+
const COLLECTIONS = 200;
|
|
27
|
+
// One row of theirs as this store gave it: a page and its short fields, measured on a real store.
|
|
28
|
+
const ROW_BYTES = 800;
|
|
29
|
+
const PER_MIN = 2;
|
|
30
|
+
const ROWS = 200;
|
|
31
|
+
const FIELDS = 12;
|
|
32
|
+
const STRING_MAX = 120;
|
|
33
|
+
// The one long prose field a row carries is the page itself, kept to what the control plane keeps.
|
|
34
|
+
const PAGE_MAX = 700;
|
|
35
|
+
const BUDGET = 540_000;
|
|
36
|
+
const READ_MS = 50_000;
|
|
37
|
+
const ASK_MS = 10_000;
|
|
38
|
+
|
|
39
|
+
const PII = /(^|_)(e?mail|phone|mobile|tel|fax|password|passwd|pwd|secret|token|otp|ssn|iban|card|address|street|zip|postal|birth|dob|salt|hash|ip)(_|$)|(mail|phone|password|token|secret|hash)$/i;
|
|
40
|
+
const CORPUS = /grade|sem|subject|book|lesson|chunk|doc|page|content|unit|chapter|curricul|textbook|knowledge|embed|vector|material|note|article/i;
|
|
41
|
+
const RECORDS = /histor|attempt|session|log|event|message|user|profile|audit|analytic|trace|queue|cache|token|auth|billing|payment|invoice/i;
|
|
42
|
+
// The narrow security floor: names that are credentials, never a corpus in any product.
|
|
43
|
+
const SENSITIVE = /(?:^|[_\-/.])(?:password|passwd|pwd|credential|secret|apikey|api[_-]?key|access[_-]?token|refresh[_-]?token|jwt|session|oauth)s?(?:$|[_\-/.])|^(?:passwords|credentials|api_keys|secrets)$/i;
|
|
44
|
+
const DOC = /\.(?:md|mdx|txt|rst|html?|pdf|docx?|csv|json)$/i;
|
|
45
|
+
const SKIP_DIR = new Set(["node_modules", ".git", "dist", "build", ".venv", "venv", "__pycache__", ".next"]);
|
|
46
|
+
|
|
47
|
+
const store = (plan, status, rows, note = "") => ({ road: plan.road, kind: plan.kind, status, rows, ...(note ? { note } : {}) });
|
|
48
|
+
const reason = (err) => String(err?.cause?.code ?? err?.code ?? err?.message ?? err).slice(0, 120);
|
|
49
|
+
const valueFor = (names, env, pattern) => names.map((n) => (pattern.test(n) ? env[n] : "")).find(Boolean) ?? "";
|
|
50
|
+
|
|
51
|
+
// The short fields of one row: numbers, short strings, and at most one page of prose. The page is
|
|
52
|
+
// looked for in every column, never in the first twelve alone: a row of theirs carries a dozen ids,
|
|
53
|
+
// dates and flags before its text, so a cap that stopped early kept the bookkeeping and dropped the
|
|
54
|
+
// page, and the cell had nothing of theirs to ask about.
|
|
55
|
+
function short(fields) {
|
|
56
|
+
const out = {};
|
|
57
|
+
let page = null;
|
|
58
|
+
for (const [key, value] of Object.entries(fields ?? {})) {
|
|
59
|
+
if (typeof key !== "string" || PII.test(key) || typeof value === "boolean") continue;
|
|
60
|
+
if (typeof value === "string" && value.trim().length > STRING_MAX) {
|
|
61
|
+
const text = value.trim();
|
|
62
|
+
if (!page || text.length > page[1].length) page = [key, text];
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (Object.keys(out).length >= FIELDS) continue;
|
|
66
|
+
if (typeof value === "number" && Number.isFinite(value)) out[key] = value;
|
|
67
|
+
else if (typeof value === "string" && value.trim()) out[key] = value.trim();
|
|
68
|
+
}
|
|
69
|
+
if (page) out[page[0]] = page[1].slice(0, PAGE_MAX);
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const rank = (name) => `${RECORDS.test(name) ? 1 : 0}${CORPUS.test(name) ? 0 : 1}${name}`;
|
|
74
|
+
|
|
75
|
+
// Round robin over the cell a name addresses, which is every segment but the last:
|
|
76
|
+
// jordan_grade-10_sem-2 holds math, physics and the rest. Keyed on the first two segments instead,
|
|
77
|
+
// the semester was never part of the key, the names sorted with every sem-1 collection first, and a
|
|
78
|
+
// cap of forty-eight read semester 1 of everything and semester 2 of nothing.
|
|
79
|
+
export function spread(names) {
|
|
80
|
+
const groups = new Map();
|
|
81
|
+
for (const name of names) {
|
|
82
|
+
const parts = name.split(/[_/.]/);
|
|
83
|
+
const key = parts.length > 1 ? parts.slice(0, -1).join("_") : parts[0];
|
|
84
|
+
groups.set(key, [...(groups.get(key) ?? []), name]);
|
|
85
|
+
}
|
|
86
|
+
const out = [];
|
|
87
|
+
for (let i = 0; out.length < names.length; i += 1) {
|
|
88
|
+
for (const key of [...groups.keys()].sort()) {
|
|
89
|
+
const list = groups.get(key);
|
|
90
|
+
if (list.length > i) out.push(list[i]);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function ask(url, key, body) {
|
|
97
|
+
const res = await fetch(url, {
|
|
98
|
+
method: body ? "POST" : "GET",
|
|
99
|
+
headers: { "content-type": "application/json", ...(key ? { "api-key": key } : {}) },
|
|
100
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
101
|
+
signal: AbortSignal.timeout(ASK_MS),
|
|
102
|
+
});
|
|
103
|
+
if (!res.ok) throw new Error(`answered ${res.status}`);
|
|
104
|
+
return res.json();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function qdrant(plan, names, env, per, until) {
|
|
108
|
+
let url = valueFor(names, env, /URL|HOST|ENDPOINT/);
|
|
109
|
+
const key = valueFor(names, env, /KEY|TOKEN/);
|
|
110
|
+
if (!url) return store(plan, "no-store", []);
|
|
111
|
+
if (!/^https?:\/\//.test(url)) url = `http://${url}`;
|
|
112
|
+
const base = url.replace(/\/+$/, "");
|
|
113
|
+
let every;
|
|
114
|
+
try {
|
|
115
|
+
every = ((await ask(`${base}/collections`, key)).result?.collections ?? []).map((c) => String(c?.name ?? "")).filter(Boolean);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
return store(plan, "unreachable", [], reason(err));
|
|
118
|
+
}
|
|
119
|
+
const corpus = every.filter((n) => CORPUS.test(n) && !SENSITIVE.test(n));
|
|
120
|
+
const named = (corpus.length ? corpus : every.filter((n) => !SENSITIVE.test(n))).sort((a, b) => rank(a).localeCompare(rank(b)));
|
|
121
|
+
const asking = spread(named).slice(0, COLLECTIONS);
|
|
122
|
+
const each = Math.max(PER_MIN, Math.min(per, Math.floor(BUDGET / ROW_BYTES / Math.max(1, asking.length))));
|
|
123
|
+
const rows = [];
|
|
124
|
+
let read = 0;
|
|
125
|
+
// Where each collection that had rows left off. On ulaim a third of the 177 collections are
|
|
126
|
+
// empty, so the even split left half the answer unspent at three pages a cell.
|
|
127
|
+
const more = new Map();
|
|
128
|
+
const scroll = async (name, limit, offset) => {
|
|
129
|
+
const got = await ask(`${base}/collections/${encodeURIComponent(name)}/points/scroll`, key, { limit, with_payload: true, with_vector: false, ...(offset != null ? { offset } : {}) });
|
|
130
|
+
let kept = 0;
|
|
131
|
+
for (const point of got.result?.points ?? []) {
|
|
132
|
+
const fields = short(point?.payload ?? {});
|
|
133
|
+
if (Object.keys(fields).length) { rows.push({ where: `${name}#${point?.id}`, fields }); kept += 1; }
|
|
134
|
+
}
|
|
135
|
+
if (kept && got.result?.next_page_offset != null) more.set(name, got.result.next_page_offset);
|
|
136
|
+
};
|
|
137
|
+
for (const name of asking) {
|
|
138
|
+
if (Date.now() > until) break;
|
|
139
|
+
try { await scroll(name, each); } catch { continue; }
|
|
140
|
+
read += 1;
|
|
141
|
+
}
|
|
142
|
+
// The second sweep spends what the first left, evenly over the collections that had more.
|
|
143
|
+
const rowBytes = rows.length ? JSON.stringify(rows).length / rows.length : ROW_BYTES;
|
|
144
|
+
const extra = Math.min(PER_STORE_MAX - each, Math.floor((BUDGET - JSON.stringify(rows).length) / rowBytes / Math.max(1, more.size)));
|
|
145
|
+
if (extra > 0) {
|
|
146
|
+
for (const [name, offset] of more) {
|
|
147
|
+
if (Date.now() > until) break;
|
|
148
|
+
try { await scroll(name, extra, offset); } catch { /* the first sweep's rows stand */ }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return store(plan, rows.length ? "sampled" : "empty", rows, `${read} of ${named.length} collections`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// A document folder their own code ingests from, named by its variable and walked from your root.
|
|
155
|
+
function files(plan, names, env, root) {
|
|
156
|
+
const name = names.find((n) => env[n]);
|
|
157
|
+
const set = name ? env[name] : "";
|
|
158
|
+
const dir = set && !set.startsWith("/") ? join(root, set) : set;
|
|
159
|
+
if (!dir || !existsSync(dir) || !statSync(dir).isDirectory()) return store(plan, "no-store", []);
|
|
160
|
+
const rows = [];
|
|
161
|
+
const walk = (at, depth) => {
|
|
162
|
+
if (rows.length >= ROWS || depth > 8) return;
|
|
163
|
+
let entries;
|
|
164
|
+
try { entries = readdirSync(at, { withFileTypes: true }); } catch { return; }
|
|
165
|
+
for (const entry of entries) {
|
|
166
|
+
if (rows.length >= ROWS) return;
|
|
167
|
+
if (entry.isDirectory()) { if (!SKIP_DIR.has(entry.name) && !entry.name.startsWith(".")) walk(join(at, entry.name), depth + 1); continue; }
|
|
168
|
+
if (!DOC.test(entry.name)) continue;
|
|
169
|
+
rows.push({ where: `${name}#${relative(dir, join(at, entry.name))}`, fields: { file: entry.name, heading: heading(join(at, entry.name)) } });
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
walk(dir, 0);
|
|
173
|
+
return store(plan, rows.length ? "sampled" : "empty", rows);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// What the file calls itself: its first line that says something.
|
|
177
|
+
function heading(path) {
|
|
178
|
+
try {
|
|
179
|
+
for (const line of readFileSync(path, "utf8").slice(0, 4000).split("\n").slice(0, 60)) {
|
|
180
|
+
const said = line.trim().replace(/^#+/, "").trim();
|
|
181
|
+
if (said) return said.slice(0, STRING_MAX);
|
|
182
|
+
}
|
|
183
|
+
} catch { /* unreadable is no heading */ }
|
|
184
|
+
return "";
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Halved until the report fits what one answer carries, pages first and every store together, so a
|
|
188
|
+
// store read late is not the only one cut.
|
|
189
|
+
function fitted(report) {
|
|
190
|
+
while (JSON.stringify(report).length > BUDGET && report.stores.some((s) => s.rows.length > 1)) {
|
|
191
|
+
for (const s of report.stores) s.rows = s.rows.slice(0, Math.max(1, Math.floor(s.rows.length / 2)));
|
|
192
|
+
}
|
|
193
|
+
return report;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ponytail: a database on this machine is not read yet; the table choice needs the read's own list
|
|
197
|
+
// of the tables their AI code names, which the control plane holds and this program does not.
|
|
198
|
+
const NO_DATABASE = "we do not read a database from your own machine yet, so nothing here is written from its rows";
|
|
199
|
+
|
|
200
|
+
export async function sampleHere(plan, env, root) {
|
|
201
|
+
const until = Date.now() + READ_MS;
|
|
202
|
+
const wanted = Number(plan?.perStore);
|
|
203
|
+
const per = Number.isInteger(wanted) && wanted > 0 ? Math.min(wanted, PER_STORE_MAX) : PER_STORE;
|
|
204
|
+
const stores = [];
|
|
205
|
+
for (const asked of (plan?.stores ?? []).slice(0, STORES_MAX)) {
|
|
206
|
+
if (!ROAD.test(String(asked?.road ?? "")) || !KINDS.has(asked?.kind)) continue;
|
|
207
|
+
const named = { road: asked.road, kind: asked.kind };
|
|
208
|
+
const names = (asked.env ?? []).filter((n) => typeof n === "string" && NAME.test(n));
|
|
209
|
+
try {
|
|
210
|
+
if (asked.kind === "qdrant") stores.push(await qdrant(named, names, env, per, until));
|
|
211
|
+
else if (asked.kind === "files") stores.push(files(named, names, env, root));
|
|
212
|
+
else stores.push(store(named, "no-reader", [], NO_DATABASE));
|
|
213
|
+
} catch (err) {
|
|
214
|
+
stores.push(store(named, "unreachable", [], reason(err)));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return fitted({ stores });
|
|
218
|
+
}
|
package/lib/start.mjs
CHANGED
|
@@ -10,36 +10,55 @@ const SERVER = /^(?:express|fastify|hono|koa|@nestjs\/core|@strapi\/strapi|next|
|
|
|
10
10
|
const PY_MODEL = /^\s*["']?(?:openai|anthropic|langchain|langgraph|litellm|llama[-_]index|google-generativeai|google-genai|groq|cohere|mistralai|ollama|fireworks-ai|together)\b/im;
|
|
11
11
|
const PY_SERVER = /^\s*["']?(?:fastapi|flask|django|starlette|quart|litestar|sanic|aiohttp|uvicorn|gunicorn)\b/im;
|
|
12
12
|
const ENTRIES = ["main.py", "app.py", "server.py", "run.py", "api.py", "wsgi.py", "asgi.py", "src/main.py", "app/main.py", "src/app.py", "backend/main.py", "api/main.py"];
|
|
13
|
+
// A twelve-name list is not how apps name their entry file: agent-service-toolkit's is
|
|
14
|
+
// src/run_service.py, and asking a person how their app starts because of a filename is asking
|
|
15
|
+
// them to do our job. Every plausibly named file beside the manifest is a candidate.
|
|
16
|
+
const ENTRY_NAME = /^(?:main|app|server|serve|api|run|start|service|web|backend|wsgi|asgi)[\w-]*\.py$/i;
|
|
17
|
+
const entriesIn = (dir) => ["", "src", "app", "backend", "api"].flatMap((sub) => {
|
|
18
|
+
try { return readdirSync(join(dir, sub)).filter((f) => ENTRY_NAME.test(f)).map((f) => (sub ? `${sub}/${f}` : f)); }
|
|
19
|
+
catch { return []; }
|
|
20
|
+
});
|
|
21
|
+
// What tells an entry file from a script that also runs itself: this one brings a server up.
|
|
22
|
+
const SERVES = /uvicorn\.run|FastAPI\(|Flask\(|Starlette\(|Litestar\(|Quart\(|app\.run\(|run_server|serve\(/;
|
|
23
|
+
// The same question of a package script: does this command put something on a port, or run a task
|
|
24
|
+
// and exit. `crewai run` and `python main.py` are tasks; `next dev` and `nodemon server.js` serve.
|
|
25
|
+
const SERVES_JS = /\b(?:next|nuxt|vite|nest|remix|astro|sveltekit|serve|nodemon|ts-node-dev|uvicorn|gunicorn|rails|strapi)\b/;
|
|
26
|
+
// The manager this repository was installed with, named by its lockfile.
|
|
27
|
+
const manager = (dir) => (existsSync(join(dir, "pnpm-lock.yaml")) ? "pnpm" : existsSync(join(dir, "yarn.lock")) ? "yarn" : existsSync(join(dir, "bun.lockb")) || existsSync(join(dir, "bun.lock")) ? "bun" : "npm");
|
|
13
28
|
|
|
14
29
|
function nodeStart(dir, onPath) {
|
|
15
30
|
const pkg = json(join(dir, "package.json"));
|
|
16
31
|
if (!pkg) return null;
|
|
17
32
|
const script = ["dev", "develop", "start:dev", "serve", "start"].find((s) => pkg.scripts?.[s]);
|
|
18
33
|
if (!script) return null;
|
|
19
|
-
const locked =
|
|
34
|
+
const locked = manager(dir);
|
|
35
|
+
const deps = Object.keys({ ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) });
|
|
20
36
|
// A lockfile names the manager the repository was installed with, not one this machine has.
|
|
21
|
-
return `${onPath(locked) ? locked : "npm"} run ${script}
|
|
37
|
+
return { cmd: `${onPath(locked) ? locked : "npm"} run ${script}`, serves: deps.some((d) => SERVER.test(d)) || SERVES_JS.test(pkg.scripts[script]) };
|
|
22
38
|
}
|
|
23
39
|
|
|
24
40
|
function pythonStart(dir, onPath) {
|
|
25
41
|
if (!["requirements.txt", "pyproject.toml", "manage.py", "Pipfile", "uv.lock"].some((f) => existsSync(join(dir, f)))) return null;
|
|
26
42
|
const venv = [".venv/bin/python", "venv/bin/python", "env/bin/python"].map((p) => join(dir, p)).find(existsSync);
|
|
27
43
|
const py = venv ? JSON.stringify(venv) : existsSync(join(dir, "uv.lock")) && onPath("uv") ? "uv run python" : existsSync(join(dir, "poetry.lock")) && onPath("poetry") ? "poetry run python" : "python3";
|
|
28
|
-
if (existsSync(join(dir, "manage.py"))) return `${py} manage.py runserver
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
44
|
+
if (existsSync(join(dir, "manage.py"))) return { cmd: `${py} manage.py runserver`, serves: true };
|
|
45
|
+
const named = [...new Set([...ENTRIES, ...entriesIn(dir)])].map((entry) => [entry, read(join(dir, entry))]).filter(([, text]) => text);
|
|
46
|
+
// The file that serves before the file that merely runs: src/run_agent.py runs a conversation in
|
|
47
|
+
// the terminal and src/run_service.py is the app, and they sit in one folder.
|
|
48
|
+
for (const [entry, text] of [...named.filter(([, text]) => SERVES.test(text)), ...named]) {
|
|
32
49
|
// Their own way of starting it carries their own port and settings.
|
|
33
|
-
if (/if\s+__name__\s*==\s*["']__main__["']/.test(text)) return `${py} ${entry}
|
|
50
|
+
if (/if\s+__name__\s*==\s*["']__main__["']/.test(text)) return { cmd: `${py} ${entry}`, serves: SERVES.test(text) };
|
|
34
51
|
const module = entry.replace(/\.py$/, "").split("/").join(".");
|
|
35
52
|
const fast = /^(\w+)\s*=\s*(?:FastAPI|Starlette|Litestar|Quart)\(/m.exec(text);
|
|
36
|
-
if (fast) return `${py} -m uvicorn ${module}:${fast[1]} --host 127.0.0.1 --port 8000
|
|
53
|
+
if (fast) return { cmd: `${py} -m uvicorn ${module}:${fast[1]} --host 127.0.0.1 --port 8000`, serves: true };
|
|
37
54
|
const flask = /^(\w+)\s*=\s*Flask\(/m.exec(text);
|
|
38
|
-
if (flask) return `${py} -m flask --app ${module} run --port 5000
|
|
55
|
+
if (flask) return { cmd: `${py} -m flask --app ${module} run --port 5000`, serves: true };
|
|
39
56
|
}
|
|
40
57
|
return null;
|
|
41
58
|
}
|
|
42
59
|
|
|
60
|
+
const KNOWN = ["package.json", "pyproject.toml", "requirements.txt", "manage.py", "Pipfile", "uv.lock"];
|
|
61
|
+
|
|
43
62
|
const startOf = (dir, onPath) => nodeStart(dir, onPath) ?? pythonStart(dir, onPath);
|
|
44
63
|
|
|
45
64
|
// How much a folder looks like the thing that serves the AI.
|
|
@@ -53,7 +72,7 @@ function weight(dir, name) {
|
|
|
53
72
|
}
|
|
54
73
|
|
|
55
74
|
const SKIP = /^(?:node_modules|\.git|dist|build|\.next|coverage|\.venv|venv|__pycache__|\.turbo|\.cache)$/;
|
|
56
|
-
function workspaces(root) {
|
|
75
|
+
export function workspaces(root) {
|
|
57
76
|
const found = [];
|
|
58
77
|
const visit = (dir, depth) => {
|
|
59
78
|
let names = [];
|
|
@@ -75,11 +94,47 @@ export function startPlan({ root, typed, onPath }) {
|
|
|
75
94
|
const pkg = json(join(root, "package.json"));
|
|
76
95
|
const mono = Boolean(pkg?.workspaces) || existsSync(join(root, "pnpm-workspace.yaml")) || existsSync(join(root, "turbo.json")) || existsSync(join(root, "lerna.json"));
|
|
77
96
|
const own = mono ? null : startOf(root, onPath);
|
|
78
|
-
if (own) return { cmd: own, cwd: root };
|
|
79
|
-
const ranked = workspaces(root).map((dir) => ({ dir,
|
|
97
|
+
if (own) return { cmd: own.cmd, cwd: root };
|
|
98
|
+
const ranked = workspaces(root).map((dir) => ({ dir, start: startOf(dir, onPath), weight: weight(dir, relative(root, dir)) })).filter((w) => w.start).sort((a, b) => b.weight - a.weight);
|
|
80
99
|
const best = ranked[0];
|
|
81
|
-
|
|
100
|
+
const at = (w) => ({ cmd: w.start.cmd, cwd: w.dir, within: relative(root, w.dir) });
|
|
101
|
+
if (best && best.weight > 0 && (ranked.length === 1 || best.weight > ranked[1].weight)) return at(best);
|
|
82
102
|
// A root that does have a start of its own after all (a monorepo whose root script runs everything).
|
|
83
103
|
const rootStart = startOf(root, onPath);
|
|
84
|
-
|
|
104
|
+
if (rootStart) return { cmd: rootStart.cmd, cwd: root };
|
|
105
|
+
// Nothing at all that we know how to start. A repository written in a language we read, holding
|
|
106
|
+
// no way to serve, is a package rather than an app: camel is one. Anything else is a start
|
|
107
|
+
// command we have not learned.
|
|
108
|
+
if (!best) return KNOWN.some((f) => existsSync(join(root, f))) ? { cmd: null, cwd: root, noServer: true } : null;
|
|
109
|
+
// Nothing here stands out, so the one that serves wins. A file that only runs in a terminal is
|
|
110
|
+
// not an app: camel is a package people import, swarm and the crewai examples are scripts that
|
|
111
|
+
// print to a terminal, and starting one of them puts a chat loop where nothing can knock on it.
|
|
112
|
+
const serving = ranked.find((w) => w.start.serves);
|
|
113
|
+
return serving ? { ...at(serving), unsure: true } : { cmd: null, cwd: root, noServer: true };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// What an app says when its dependencies are not installed on this machine, in every runtime we
|
|
117
|
+
// start, and the name of the one it named first.
|
|
118
|
+
const MISSING = /Cannot find module ['"]?([@\w./-]+)|Cannot find package ['"]?([@\w./-]+)|ModuleNotFoundError: No module named ['"]?([\w.]+)|ImportError: No module named ['"]?([\w.]+)|(?:^|[:/ ])([\w.-]+): (?:command )?not found|command not found: ?([\w.-]+)/m;
|
|
119
|
+
export const missingDependency = (said) => MISSING.exec(String(said ?? ""))?.slice(1).find(Boolean) ?? "";
|
|
120
|
+
|
|
121
|
+
// One install, with the manager the project locked. Never a global one: a Python project without an
|
|
122
|
+
// interpreter of its own gets a virtual environment beside its code, so nothing installed here
|
|
123
|
+
// reaches the rest of the machine.
|
|
124
|
+
export function installPlan(dir, onPath) {
|
|
125
|
+
if (json(join(dir, "package.json"))) {
|
|
126
|
+
// The manager the repository locked, fetched for the job when this machine has not got it: npm
|
|
127
|
+
// refuses vercel's ai-chatbot outright over a peer range pnpm resolves without a word.
|
|
128
|
+
const locked = manager(dir);
|
|
129
|
+
if (locked !== "npm") return onPath(locked) ? `${locked} install` : `npx --yes ${locked} install`;
|
|
130
|
+
return existsSync(join(dir, "package-lock.json")) ? "npm ci || npm install --legacy-peer-deps" : "npm install || npm install --legacy-peer-deps";
|
|
131
|
+
}
|
|
132
|
+
if (existsSync(join(dir, "uv.lock")) && onPath("uv")) return "uv sync";
|
|
133
|
+
if (existsSync(join(dir, "poetry.lock")) && onPath("poetry")) return "poetry install";
|
|
134
|
+
const venv = [".venv/bin/python", "venv/bin/python", "env/bin/python"].map((p) => join(dir, p)).find(existsSync);
|
|
135
|
+
const py = venv ? JSON.stringify(venv) : null;
|
|
136
|
+
const into = (what) => (py ? `${py} -m pip install ${what}` : `python3 -m venv .venv && .venv/bin/python -m pip install --upgrade pip && .venv/bin/python -m pip install ${what}`);
|
|
137
|
+
if (existsSync(join(dir, "requirements.txt"))) return into("-r requirements.txt");
|
|
138
|
+
if (existsSync(join(dir, "pyproject.toml"))) return into("-e .");
|
|
139
|
+
return null;
|
|
85
140
|
}
|
package/lib/trace.cjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// Loaded into your app by the command that started it (node --require), and only then. It watches
|
|
2
|
-
// for
|
|
3
|
-
// AI's door, with the exact body it takes and the sign-in it carried, learned from a message you
|
|
4
|
-
// yourself rather than guessed from code.
|
|
5
|
-
//
|
|
2
|
+
// for two things. First, a request to your app during which your app called a model: that request is
|
|
3
|
+
// your AI's door, with the exact body it takes and the sign-in it carried, learned from a message you
|
|
4
|
+
// sent yourself rather than guessed from code. Second, every model call your app makes: the host, the
|
|
5
|
+
// model it asked for, the status and the token counts the provider sent back, so a run can say which
|
|
6
|
+
// model answered and what it cost you. Reply text is never written down. Both go to a file only you
|
|
7
|
+
// can read, in the command's own folder on this machine. Nothing here talks to a network.
|
|
6
8
|
"use strict";
|
|
7
9
|
const FILE = process.env.CORTAD_TRACE_FILE;
|
|
8
10
|
if (FILE) {
|
|
@@ -13,10 +15,15 @@ if (FILE) {
|
|
|
13
15
|
const https = require("node:https");
|
|
14
16
|
const als = new AsyncLocalStorage();
|
|
15
17
|
// Said once, so the command knows a message sent to this app can be seen arriving.
|
|
16
|
-
try { fs.appendFileSync(FILE, JSON.stringify(
|
|
18
|
+
const row = (value) => { try { fs.appendFileSync(FILE, JSON.stringify(value) + "\n", { mode: 0o600 }); } catch { /* the command is gone */ } };
|
|
19
|
+
const isBun = typeof Bun !== "undefined" && typeof Bun.serve === "function";
|
|
20
|
+
row({ hello: isBun ? "bun" : "node", pid: process.pid });
|
|
21
|
+
// Which port this process serves. Every process the start command spawns loads this file, turbo's
|
|
22
|
+
// own launcher included, so "loaded" is not "watching your app": only a listener on the app's port is.
|
|
23
|
+
const listening = (port) => { if (Number.isInteger(port) && port > 0) row({ listen: port, pid: process.pid }); };
|
|
17
24
|
const MAX = 65536;
|
|
18
25
|
// Where models are served, by host, and the paths every OpenAI-shaped or vendor endpoint ends with.
|
|
19
|
-
const MODEL_HOST = /(?:^|\.)(?:openai\.com|anthropic\.com|fireworks\.ai|openrouter\.ai|groq\.com|mistral\.ai|together\.xyz|together\.ai|deepseek\.com|cohere\.ai|cohere\.com|perplexity\.ai|x\.ai|googleapis\.com|openai\.azure\.com|cognitiveservices\.azure\.com|amazonaws\.com|replicate\.com|huggingface\.co|cerebras\.ai|deepinfra\.com|novita\.ai|moonshot\.cn|dashscope\.aliyuncs\.com|bigmodel\.cn)$/i;
|
|
26
|
+
const MODEL_HOST = /(?:^|\.)(?:openai\.com|anthropic\.com|fireworks\.ai|openrouter\.ai|groq\.com|mistral\.ai|together\.xyz|together\.ai|deepseek\.com|cohere\.ai|cohere\.com|perplexity\.ai|x\.ai|googleapis\.com|openai\.azure\.com|cognitiveservices\.azure\.com|amazonaws\.com|replicate\.com|huggingface\.co|cerebras\.ai|deepinfra\.com|novita\.ai|moonshot\.cn|dashscope\.aliyuncs\.com|bigmodel\.cn|ai-gateway\.vercel\.sh|gateway\.ai\.cloudflare\.com|helicone\.ai|portkey\.ai)$/i;
|
|
20
27
|
const MODEL_PATH = /\/(?:chat\/completions|completions|responses|messages|embeddings)$|:(?:generateContent|streamGenerateContent)|\/invoke(?:-with-response-stream)?$|\/api\/(?:chat|generate)$/i;
|
|
21
28
|
const isModelCall = (host, path) => {
|
|
22
29
|
const h = String(host || "").replace(/:\d+$/, "");
|
|
@@ -32,14 +39,16 @@ if (FILE) {
|
|
|
32
39
|
const key = `${ctx.method} ${ctx.path.split("?")[0]}`;
|
|
33
40
|
if (said.has(key)) return;
|
|
34
41
|
said.add(key);
|
|
35
|
-
const
|
|
36
|
-
|
|
42
|
+
const write = (chunks) => row({ at: Date.now(), method: ctx.method, path: ctx.path, headers: ctx.headers, body: Buffer.concat(chunks).toString("utf8").slice(0, MAX), sent: String(sent || "").slice(0, MAX) });
|
|
43
|
+
if (ctx.body) ctx.body.then((b) => write([b]));
|
|
44
|
+
else write(ctx.chunks);
|
|
37
45
|
};
|
|
38
46
|
|
|
39
47
|
// Inbound: each request is handled inside its own context, and its body is seen as it arrives
|
|
40
48
|
// without reading it, so your own body parser is untouched.
|
|
41
49
|
const emit = http.Server.prototype.emit;
|
|
42
50
|
http.Server.prototype.emit = function (type, req, ...rest) {
|
|
51
|
+
if (type === "listening") { try { listening(this.address()?.port); } catch { /* not a TCP server */ } }
|
|
43
52
|
if (type !== "request" || !req || !req.method || /^(?:GET|HEAD|OPTIONS)$/.test(req.method)) return emit.call(this, type, req, ...rest);
|
|
44
53
|
const ctx = { method: req.method, path: req.url || "/", headers: { ...req.headers }, chunks: [], size: 0, noted: false };
|
|
45
54
|
const push = req.push;
|
|
@@ -67,16 +76,160 @@ if (FILE) {
|
|
|
67
76
|
return als.run(ctx, () => emit.call(this, type, req, ...rest));
|
|
68
77
|
};
|
|
69
78
|
|
|
79
|
+
// Bun serves through Bun.serve, never node:http, so the patch above saw no request at all: databuddy's
|
|
80
|
+
// Elysia API on Bun took the customer's messages and nothing was written. Bun.serve is wrapped
|
|
81
|
+
// instead, both the fetch handler and the per-route handlers Bun can dispatch to directly. The body
|
|
82
|
+
// is read from a clone, so the app's own reader is untouched.
|
|
83
|
+
if (isBun) {
|
|
84
|
+
const inside = (handler, self) => function (req, server) {
|
|
85
|
+
if (!req || /^(?:GET|HEAD|OPTIONS)$/.test(req.method)) return handler.call(self, req, server);
|
|
86
|
+
let path = "/";
|
|
87
|
+
try { const u = new URL(req.url); path = u.pathname + u.search; } catch { /* keep "/" */ }
|
|
88
|
+
const ctx = { method: req.method, path, headers: Object.fromEntries(req.headers), chunks: [], size: 0, noted: false };
|
|
89
|
+
// ponytail: a body is copied only when it is small or says it is text; an upload is left alone.
|
|
90
|
+
const size = Number(req.headers.get("content-length") || 0);
|
|
91
|
+
if (size <= MAX || /json|text|form/i.test(req.headers.get("content-type") || "")) {
|
|
92
|
+
try { ctx.body = req.clone().arrayBuffer().then((b) => Buffer.from(b).subarray(0, MAX), () => Buffer.alloc(0)); } catch { /* unread */ }
|
|
93
|
+
}
|
|
94
|
+
return als.run(ctx, () => handler.call(self, req, server));
|
|
95
|
+
};
|
|
96
|
+
const routes = (table) => {
|
|
97
|
+
if (!table || typeof table !== "object") return table;
|
|
98
|
+
const out = {};
|
|
99
|
+
for (const [route, value] of Object.entries(table)) {
|
|
100
|
+
if (typeof value === "function") out[route] = inside(value, table);
|
|
101
|
+
else if (value && typeof value === "object" && !(value instanceof Response)) {
|
|
102
|
+
out[route] = {};
|
|
103
|
+
for (const [verb, fn] of Object.entries(value)) out[route][verb] = typeof fn === "function" ? inside(fn, value) : fn;
|
|
104
|
+
} else out[route] = value;
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
};
|
|
108
|
+
const serve = Bun.serve;
|
|
109
|
+
Bun.serve = function (options, ...rest) {
|
|
110
|
+
let wrapped = options;
|
|
111
|
+
try {
|
|
112
|
+
if (options && typeof options === "object") {
|
|
113
|
+
wrapped = Object.assign(Object.create(Object.getPrototypeOf(options)), options);
|
|
114
|
+
if (typeof options.fetch === "function") wrapped.fetch = inside(options.fetch, options);
|
|
115
|
+
if (options.routes) wrapped.routes = routes(options.routes);
|
|
116
|
+
}
|
|
117
|
+
} catch { wrapped = options; }
|
|
118
|
+
const server = serve.call(this, wrapped, ...rest);
|
|
119
|
+
try { listening(server && server.port); } catch { /* not ours to fail */ }
|
|
120
|
+
return server;
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// The meter. One row per model call: host, model id, status, and the provider's own token counts.
|
|
125
|
+
const zlib = require("node:zlib");
|
|
126
|
+
const REPLY_MAX = 8 * 1024 * 1024;
|
|
127
|
+
const decoded = (buf, encoding) => {
|
|
128
|
+
try {
|
|
129
|
+
const e = String(encoding || "").toLowerCase();
|
|
130
|
+
return (e === "gzip" ? zlib.gunzipSync(buf) : e === "br" ? zlib.brotliDecompressSync(buf) : e === "deflate" ? zlib.inflateSync(buf) : buf).toString("utf8");
|
|
131
|
+
} catch { return ""; }
|
|
132
|
+
};
|
|
133
|
+
const parse = (text) => { try { return JSON.parse(text); } catch { return null; } };
|
|
134
|
+
const n = (v) => (Number.isInteger(v) && v > 0 ? v : 0);
|
|
135
|
+
// The shapes the providers answer in. promptTokens is the whole input, cache included, so totals add up.
|
|
136
|
+
const tokensOf = (u) => {
|
|
137
|
+
if (!u || typeof u !== "object") return null;
|
|
138
|
+
if (u.tokens && typeof u.tokens === "object") return tokensOf(u.tokens);
|
|
139
|
+
if ("prompt_tokens" in u) return { promptTokens: n(u.prompt_tokens), cachedTokens: n(u.prompt_tokens_details && u.prompt_tokens_details.cached_tokens), completionTokens: n(u.completion_tokens) };
|
|
140
|
+
if ("input_tokens" in u && u.input_tokens_details) return { promptTokens: n(u.input_tokens), cachedTokens: n(u.input_tokens_details.cached_tokens), completionTokens: n(u.output_tokens) };
|
|
141
|
+
if ("input_tokens" in u) { const read = n(u.cache_read_input_tokens); return { promptTokens: n(u.input_tokens) + read + n(u.cache_creation_input_tokens), cachedTokens: read, completionTokens: n(u.output_tokens) }; }
|
|
142
|
+
if ("inputTokens" in u) { const read = n(u.cacheReadInputTokens); return { promptTokens: n(u.inputTokens) + read + n(u.cacheWriteInputTokens), cachedTokens: read, completionTokens: n(u.outputTokens) }; }
|
|
143
|
+
if ("promptTokenCount" in u) return { promptTokens: n(u.promptTokenCount), cachedTokens: n(u.cachedContentTokenCount), completionTokens: n(u.candidatesTokenCount) };
|
|
144
|
+
return null;
|
|
145
|
+
};
|
|
146
|
+
// A stream spreads its counts over events (Anthropic's input in the first, output in the last);
|
|
147
|
+
// later values win, so the fold ends on the final figures.
|
|
148
|
+
const readReply = (type, text) => {
|
|
149
|
+
const events = /event-stream/i.test(type || "")
|
|
150
|
+
? text.split("\n").filter((l) => l.startsWith("data:")).map((l) => parse(l.slice(5).trim()))
|
|
151
|
+
: [].concat(parse(text));
|
|
152
|
+
const usage = {};
|
|
153
|
+
let model = null;
|
|
154
|
+
for (const e of events) {
|
|
155
|
+
if (!e || typeof e !== "object") continue;
|
|
156
|
+
const part = e.usage || (e.message && e.message.usage) || (e.response && e.response.usage) || e.usageMetadata;
|
|
157
|
+
if (part && typeof part === "object") Object.assign(usage, part);
|
|
158
|
+
model = e.model || (e.message && e.message.model) || (e.response && e.response.model) || e.modelVersion || model;
|
|
159
|
+
}
|
|
160
|
+
return { tokens: tokensOf(Object.keys(usage).length ? usage : null), model };
|
|
161
|
+
};
|
|
162
|
+
// The model asked for, from what was sent; Gemini and Bedrock put it in the path instead.
|
|
163
|
+
const askedFor = (path, sent) => {
|
|
164
|
+
const body = parse(sent);
|
|
165
|
+
if (body && typeof body.model === "string") return body.model;
|
|
166
|
+
const m = /\/models\/([^/:]+):|\/model\/([^/]+)\/(?:invoke|converse)/.exec(path || "");
|
|
167
|
+
return m ? decodeURIComponent(m[1] || m[2]) : "";
|
|
168
|
+
};
|
|
169
|
+
const meter = ({ host, path, sent, status, type, body }) => {
|
|
170
|
+
try {
|
|
171
|
+
const reply = readReply(type, String(body || "").slice(0, REPLY_MAX));
|
|
172
|
+
const row = { at: Date.now(), host: String(host).replace(/:443$/, ""), model: String(askedFor(path, sent) || reply.model || "").slice(0, 160), status, ...(reply.tokens || { promptTokens: 0, cachedTokens: 0, completionTokens: 0 }), usage: Boolean(reply.tokens) };
|
|
173
|
+
fs.appendFileSync(FILE, JSON.stringify({ call: row }) + "\n", { mode: 0o600 });
|
|
174
|
+
} catch { /* the command is gone */ }
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// Outbound to a service their own settings name (a vector store, a database API, a search
|
|
178
|
+
// host): which setting, which host and whether it answered. No body, no headers, no query. A
|
|
179
|
+
// run that tested ulaim while every curriculum lookup failed inside Node never knew; this is
|
|
180
|
+
// the witness that lets the run say retrieval was not tested instead of grading without it.
|
|
181
|
+
let depHosts = null, depSeen = -1;
|
|
182
|
+
const depOf = (input) => {
|
|
183
|
+
try {
|
|
184
|
+
const keys = Object.keys(process.env);
|
|
185
|
+
if (keys.length !== depSeen) {
|
|
186
|
+
depSeen = keys.length; depHosts = new Map();
|
|
187
|
+
for (const k of keys) {
|
|
188
|
+
const v = process.env[k];
|
|
189
|
+
if (!v || !/^https?:\/\//i.test(v) || /(^|_)(KEY|TOKEN|SECRET|PASSWORD)$/i.test(k)) continue;
|
|
190
|
+
try { depHosts.set(new URL(v).host.replace(/:443$/, ""), k); } catch { /* not a URL */ }
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const u = new URL(typeof input === "string" ? input : input && input.url ? input.url : String(input));
|
|
194
|
+
const name = depHosts.get(u.host.replace(/:443$/, "")) || depHosts.get(u.hostname);
|
|
195
|
+
return name ? { env: name, host: u.hostname } : null;
|
|
196
|
+
} catch { return null; }
|
|
197
|
+
};
|
|
198
|
+
const depRow = (dep, status, code) => {
|
|
199
|
+
try { fs.appendFileSync(FILE, JSON.stringify({ dep: { at: Date.now(), env: dep.env, host: dep.host, status, code: code ? String(code).slice(0, 40) : undefined } }) + "\n", { mode: 0o600 }); } catch { /* the command is gone */ }
|
|
200
|
+
};
|
|
201
|
+
|
|
70
202
|
// Outbound: the model call your app makes while it handles that request.
|
|
71
203
|
const bodyText = (body) => (typeof body === "string" ? body : Buffer.isBuffer(body) ? body.toString("utf8") : body instanceof Uint8Array ? Buffer.from(body).toString("utf8") : "");
|
|
72
204
|
if (typeof globalThis.fetch === "function") {
|
|
73
205
|
const realFetch = globalThis.fetch;
|
|
74
206
|
globalThis.fetch = function (input, init) {
|
|
207
|
+
let url = null;
|
|
75
208
|
try {
|
|
76
|
-
const
|
|
77
|
-
if (isModelCall(
|
|
209
|
+
const u = new URL(typeof input === "string" ? input : input && input.url ? input.url : String(input));
|
|
210
|
+
if (isModelCall(u.host, u.pathname)) url = u;
|
|
78
211
|
} catch { /* not a URL we can read */ }
|
|
79
|
-
|
|
212
|
+
if (!url) {
|
|
213
|
+
const dep = depOf(input);
|
|
214
|
+
if (!dep) return realFetch.apply(this, arguments);
|
|
215
|
+
return realFetch.apply(this, arguments).then(
|
|
216
|
+
(res) => { depRow(dep, res.status); return res; },
|
|
217
|
+
(err) => { depRow(dep, 0, err && err.cause && err.cause.code); throw err; },
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
const sent = bodyText(init && init.body);
|
|
221
|
+
note(als.getStore(), sent);
|
|
222
|
+
const call = { host: url.host, path: url.pathname, sent };
|
|
223
|
+
return realFetch.apply(this, arguments).then((res) => {
|
|
224
|
+
try {
|
|
225
|
+
// A clone is a tee: your app's branch gets every byte as fast as it reads, this one is read to the end.
|
|
226
|
+
res.clone().text().then(
|
|
227
|
+
(text) => meter({ ...call, status: res.status, type: res.headers.get("content-type"), body: text }),
|
|
228
|
+
() => meter({ ...call, status: res.status, type: "", body: "" }),
|
|
229
|
+
);
|
|
230
|
+
} catch { meter({ ...call, status: res.status, type: "", body: "" }); }
|
|
231
|
+
return res;
|
|
232
|
+
}, (err) => { meter({ ...call, status: 0, type: "", body: "" }); throw err; });
|
|
80
233
|
};
|
|
81
234
|
}
|
|
82
235
|
for (const mod of [http, https]) {
|
|
@@ -90,10 +243,29 @@ if (FILE) {
|
|
|
90
243
|
if (isModelCall(host, path)) {
|
|
91
244
|
const ctx = als.getStore();
|
|
92
245
|
const parts = [];
|
|
246
|
+
let sent = "";
|
|
93
247
|
const write = req.write;
|
|
94
248
|
const end = req.end;
|
|
95
249
|
req.write = function (chunk, ...r) { if (chunk && typeof chunk !== "function") parts.push(Buffer.from(chunk)); return write.call(this, chunk, ...r); };
|
|
96
|
-
req.end = function (chunk, ...r) { if (chunk && typeof chunk !== "function") parts.push(Buffer.from(chunk));
|
|
250
|
+
req.end = function (chunk, ...r) { if (chunk && typeof chunk !== "function") parts.push(Buffer.from(chunk)); sent = Buffer.concat(parts).toString("utf8"); note(ctx, sent); return end.call(this, chunk, ...r); };
|
|
251
|
+
const call = { host: String(host || ""), path: String(path || "").split("?")[0] };
|
|
252
|
+
let done = false;
|
|
253
|
+
const once = (row) => { if (!done) { done = true; meter({ ...call, sent, ...row }); } };
|
|
254
|
+
req.once("error", () => once({ status: 0, type: "", body: "" }));
|
|
255
|
+
req.once("response", (res) => {
|
|
256
|
+
// The reply's bytes as the parser hands them over, without reading the stream: your
|
|
257
|
+
// app's own listeners and pipes see exactly what they would have.
|
|
258
|
+
const got = [];
|
|
259
|
+
let size = 0;
|
|
260
|
+
const push = res.push;
|
|
261
|
+
res.push = function (chunk, encoding) {
|
|
262
|
+
if (chunk && size < REPLY_MAX) { const b = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding); got.push(b); size += b.length; }
|
|
263
|
+
return push.call(this, chunk, encoding);
|
|
264
|
+
};
|
|
265
|
+
const finish = () => once({ status: res.statusCode || 0, type: res.headers["content-type"] || "", body: decoded(Buffer.concat(got), res.headers["content-encoding"]) });
|
|
266
|
+
res.once("end", finish);
|
|
267
|
+
res.once("close", finish);
|
|
268
|
+
});
|
|
97
269
|
}
|
|
98
270
|
} catch { /* leave the request alone */ }
|
|
99
271
|
return req;
|