cortad 0.2.2 → 0.3.0-rc.10
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/README.md +11 -2
- package/lib/cli.mjs +90 -38
- package/lib/home.mjs +9 -2
- package/lib/mcp.mjs +32 -12
- package/lib/pyhook/sitecustomize.py +396 -11
- package/lib/read-text.mjs +91 -0
- package/lib/register.mjs +64 -30
- package/lib/replay.mjs +43 -5
- package/lib/spec.mjs +20 -0
- package/lib/start.mjs +10 -0
- package/lib/stick.mjs +99 -0
- package/lib/text.mjs +268 -0
- package/lib/trace.cjs +303 -9
- package/lib/verbs.mjs +148 -103
- package/lib/words.mjs +37 -0
- package/local.mjs +58 -17
- package/package.json +6 -1
- package/skill/SKILL.md +75 -31
- package/skill/references/results.md +175 -34
package/lib/trace.cjs
CHANGED
|
@@ -20,8 +20,284 @@ if (FILE) {
|
|
|
20
20
|
row({ hello: isBun ? "bun" : "node", pid: process.pid });
|
|
21
21
|
// Which port this process serves. Every process the start command spawns loads this file, turbo's
|
|
22
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 }); };
|
|
23
|
+
const listening = (port) => { if (Number.isInteger(port) && port > 0) { row({ listen: port, pid: process.pid }); sayRoutes(port); } };
|
|
24
|
+
|
|
25
|
+
// The app's own route table, so the run knows every door it has before a message is sent.
|
|
26
|
+
// Express keeps no mount prefix once a router is built (Express 5 layers keep no path at all),
|
|
27
|
+
// so its registrations are watched as they happen; the others are read off the app.
|
|
28
|
+
const ROUTES_MAX = 400;
|
|
29
|
+
const pathOf = (p) => String(p)
|
|
30
|
+
.replace(/\{(\/?[:*][^{}]*)\}/g, "$1")
|
|
31
|
+
.replace(/:(\w+)(?:\{[^{}]*\})?\??/g, "{$1}")
|
|
32
|
+
.replace(/\*(\w+)/g, "{$1}");
|
|
33
|
+
const joined = (a, b) => ("/" + [a, b].map((s) => String(s).replace(/^\/+|\/+$/g, "")).filter(Boolean).join("/"));
|
|
34
|
+
const nodePath = require("node:path");
|
|
35
|
+
// The file that registered a route: the first frame outside this hook and outside node_modules.
|
|
36
|
+
const callerFile = () => {
|
|
37
|
+
for (const line of String(new Error().stack).split("\n").slice(2)) {
|
|
38
|
+
const m = /\(?(?:file:\/\/)?(\/[^()]+?):\d+:\d+\)?$/.exec(line.trim());
|
|
39
|
+
if (!m || m[1] === __filename || m[1].includes("/node_modules/")) continue;
|
|
40
|
+
const rel = nodePath.relative(process.cwd(), decodeURI(m[1]));
|
|
41
|
+
return rel.startsWith("..") ? "" : rel;
|
|
42
|
+
}
|
|
43
|
+
return "";
|
|
44
|
+
};
|
|
45
|
+
const seen = new Set();
|
|
46
|
+
let nest = false;
|
|
47
|
+
|
|
48
|
+
const expressRoutes = [];
|
|
49
|
+
const expressMounts = [];
|
|
50
|
+
const OWNER = Symbol("cortad.router");
|
|
51
|
+
const isApp = (o) => typeof o === "function" && typeof o.set === "function" && typeof o.handle === "function";
|
|
52
|
+
const isRouter = (o) => typeof o === "function" && Array.isArray(o.stack) && typeof o.handle === "function";
|
|
53
|
+
const useArgs = (args) => {
|
|
54
|
+
let first = args[0];
|
|
55
|
+
while (Array.isArray(first) && first.length) first = first[0];
|
|
56
|
+
const pathed = typeof first !== "function";
|
|
57
|
+
return { paths: pathed ? [].concat(args[0]).filter((p) => typeof p === "string") : ["/"], fns: args.slice(pathed ? 1 : 0).flat(Infinity) };
|
|
58
|
+
};
|
|
59
|
+
const patchExpress = (express) => {
|
|
60
|
+
seen.add("express");
|
|
61
|
+
const routerProto = typeof express.Router.prototype.route === "function" ? express.Router.prototype : express.Router;
|
|
62
|
+
const mounting = (proto, wanted) => {
|
|
63
|
+
const use = proto.use;
|
|
64
|
+
proto.use = function (...args) {
|
|
65
|
+
try {
|
|
66
|
+
const { paths, fns } = useArgs(args);
|
|
67
|
+
for (const fn of fns) if (wanted(fn)) for (const path of paths) expressMounts.push({ parent: this, child: fn, path });
|
|
68
|
+
} catch { /* not ours to fail */ }
|
|
69
|
+
return use.apply(this, args);
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
// Express 4 and 5 both pass a plain router through to the router's own use, and hide a
|
|
73
|
+
// mounted sub-app behind a wrapper there, so each level records only what it can see.
|
|
74
|
+
mounting(routerProto, isRouter);
|
|
75
|
+
mounting(express.application, isApp);
|
|
76
|
+
const route = routerProto.route;
|
|
77
|
+
routerProto.route = function (...args) {
|
|
78
|
+
const made = route.apply(this, args);
|
|
79
|
+
try { made[OWNER] = this; } catch { /* frozen */ }
|
|
80
|
+
return made;
|
|
81
|
+
};
|
|
82
|
+
for (const verb of ["get", "post", "put", "patch", "delete", "all"]) {
|
|
83
|
+
const add = express.Route.prototype[verb];
|
|
84
|
+
if (typeof add !== "function") continue;
|
|
85
|
+
express.Route.prototype[verb] = function (...args) {
|
|
86
|
+
try {
|
|
87
|
+
if (expressRoutes.length < ROUTES_MAX * 4) {
|
|
88
|
+
const fns = args.flat(Infinity);
|
|
89
|
+
expressRoutes.push({ owner: this[OWNER], verb, paths: [].concat(this.path).filter((p) => typeof p === "string"), handler: fns[fns.length - 1], file: callerFile() });
|
|
90
|
+
}
|
|
91
|
+
} catch { /* not ours to fail */ }
|
|
92
|
+
return add.apply(this, args);
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const expressTable = (add) => {
|
|
97
|
+
// An app is known by its router once it has one: routes are registered on the router.
|
|
98
|
+
// Express 4's app.router getter throws; its router is _router, made on the first route.
|
|
99
|
+
const own = (o) => {
|
|
100
|
+
if (!isApp(o) || o._router) return (o && o._router) || o;
|
|
101
|
+
try { return o.router || o; } catch { return o; }
|
|
102
|
+
};
|
|
103
|
+
const ups = new Map();
|
|
104
|
+
for (const m of expressMounts) {
|
|
105
|
+
const child = own(m.child);
|
|
106
|
+
if (!ups.has(child)) ups.set(child, []);
|
|
107
|
+
ups.get(child).push({ parent: own(m.parent), path: m.path });
|
|
108
|
+
}
|
|
109
|
+
const prefixes = (router, depth) => (!ups.has(router) || depth > 8 ? [""] : ups.get(router).flatMap((m) => prefixes(m.parent, depth + 1).map((p) => joined(p, m.path))));
|
|
110
|
+
for (const r of expressRoutes) {
|
|
111
|
+
for (const prefix of prefixes(own(r.owner), 0)) for (const path of r.paths) add(r.verb === "all" ? null : [r.verb], joined(prefix, path), r.handler, r.file);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Fastify says each instance it makes on a diagnostics channel, and a root onRoute hook sees
|
|
116
|
+
// every route with its plugin prefix already applied.
|
|
117
|
+
const fastifyRoutes = [];
|
|
118
|
+
try {
|
|
119
|
+
require("node:diagnostics_channel").subscribe("fastify.initialization", ({ fastify }) => {
|
|
120
|
+
seen.add("fastify");
|
|
121
|
+
try {
|
|
122
|
+
fastify.addHook("onRoute", (o) => {
|
|
123
|
+
if (fastifyRoutes.length < ROUTES_MAX * 2) fastifyRoutes.push({ methods: [].concat(o.method), path: o.url, handler: o.handler, file: callerFile() });
|
|
124
|
+
});
|
|
125
|
+
} catch { /* an instance that takes no hooks */ }
|
|
126
|
+
});
|
|
127
|
+
} catch { /* no diagnostics channel */ }
|
|
128
|
+
|
|
129
|
+
// Hono and Koa keep their tables on the app, so their apps are held as they are made. Only
|
|
130
|
+
// what is loaded through require is seen: Hono's ES module build never passes through here.
|
|
131
|
+
const honoApps = new Set();
|
|
132
|
+
const koaApps = new Set();
|
|
133
|
+
const holding = new WeakMap();
|
|
134
|
+
const heldHono = (exported) => {
|
|
135
|
+
const d = Object.getOwnPropertyDescriptor(exported, "Hono");
|
|
136
|
+
if (!d || ("value" in d && !d.writable && !d.configurable)) return exported;
|
|
137
|
+
seen.add("hono");
|
|
138
|
+
const Hono = new Proxy(exported.Hono, { construct(t, a, nt) { const app = Reflect.construct(t, a, nt); if (honoApps.size < 50) honoApps.add(app); return app; } });
|
|
139
|
+
return new Proxy(exported, { get: (t, k, r) => (k === "Hono" ? Hono : Reflect.get(t, k, r)) });
|
|
140
|
+
};
|
|
141
|
+
const patchKoa = (Koa) => {
|
|
142
|
+
seen.add("koa");
|
|
143
|
+
const callback = Koa.prototype.callback;
|
|
144
|
+
Koa.prototype.callback = function (...a) { if (koaApps.size < 50) koaApps.add(this); return callback.apply(this, a); };
|
|
145
|
+
};
|
|
146
|
+
const adopted = (request, exported) => {
|
|
147
|
+
if (!exported || !/(?:^|node_modules\/)(?:express|koa|hono|@nestjs\/core)(?:\/index\.js)?$|\/lib\/(?:express|application)(?:\.js)?$/.test(request)) return exported;
|
|
148
|
+
if (holding.has(exported)) return holding.get(exported);
|
|
149
|
+
let out = exported;
|
|
150
|
+
if (typeof exported === "function" && exported.application && typeof exported.Router === "function" && typeof exported.Route === "function") patchExpress(exported);
|
|
151
|
+
else if (typeof exported === "function" && exported.prototype && typeof exported.prototype.callback === "function" && typeof exported.prototype.createContext === "function") patchKoa(exported);
|
|
152
|
+
else if (typeof exported.Hono === "function") out = heldHono(exported);
|
|
153
|
+
else if (exported.NestFactory) nest = true;
|
|
154
|
+
holding.set(exported, out);
|
|
155
|
+
return out;
|
|
156
|
+
};
|
|
157
|
+
const Module = require("node:module");
|
|
158
|
+
const load = Module._load;
|
|
159
|
+
Module._load = function (request, ...rest) {
|
|
160
|
+
const exported = load.call(this, request, ...rest);
|
|
161
|
+
try { return adopted(String(request), exported); } catch { return exported; }
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
let bunRoutes = [];
|
|
165
|
+
const table = () => {
|
|
166
|
+
const out = new Map();
|
|
167
|
+
const from = new Set();
|
|
168
|
+
const add = (framework) => (methods, path, handler, file) => {
|
|
169
|
+
if (typeof path !== "string") return;
|
|
170
|
+
for (const m of methods || ["POST", "GET"]) {
|
|
171
|
+
const method = String(m).toUpperCase();
|
|
172
|
+
const at = pathOf(path.startsWith("/") ? path : "/" + path);
|
|
173
|
+
if (method === "HEAD" || method === "OPTIONS" || out.size >= ROUTES_MAX || out.has(`${method} ${at}`)) continue;
|
|
174
|
+
from.add(framework);
|
|
175
|
+
out.set(`${method} ${at}`, { method, path: at.slice(0, 1024), file: String(file || "").slice(0, 512), handler: String((handler && handler.name) || "").slice(0, 200) });
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
expressTable(add("express"));
|
|
179
|
+
for (const r of fastifyRoutes) add("fastify")(r.methods, r.path, r.handler, r.file);
|
|
180
|
+
// A sub-app's routes are copied into the app that mounts it, so the widest table is the app.
|
|
181
|
+
const hono = [...honoApps].reduce((a, b) => (Array.isArray(b.routes) && b.routes.length > ((a && a.routes.length) || 0) ? b : a), null);
|
|
182
|
+
for (const r of hono ? hono.routes : []) if (r.method !== "ALL" || !r.path.endsWith("*")) add("hono")(r.method === "ALL" ? null : [r.method], r.path, r.handler, "");
|
|
183
|
+
for (const app of koaApps) for (const mw of app.middleware || []) for (const layer of (mw.router && mw.router.stack) || []) {
|
|
184
|
+
if (layer.methods && layer.methods.length) add("koa")(layer.methods, layer.path, layer.stack && layer.stack[layer.stack.length - 1], "");
|
|
185
|
+
}
|
|
186
|
+
for (const r of bunRoutes) add(seen.has("elysia") ? "elysia" : "unknown")(r.methods, r.path, r.handler, "");
|
|
187
|
+
const framework = nest ? "nest" : [...from][0] || [...seen][0] || "unknown";
|
|
188
|
+
return { framework, routes: [...out.values()] };
|
|
189
|
+
};
|
|
190
|
+
let routesSaid = -1, lastPort = null;
|
|
191
|
+
function sayRoutes(port) {
|
|
192
|
+
try {
|
|
193
|
+
lastPort = port;
|
|
194
|
+
const { framework, routes } = table();
|
|
195
|
+
routesSaid = routes.length;
|
|
196
|
+
row({ routes: { framework, port, routes, openapi: null } });
|
|
197
|
+
} catch { /* the table is a courtesy; the app is not */ }
|
|
198
|
+
}
|
|
199
|
+
// Routes an app adds after it starts listening are there by its first request.
|
|
200
|
+
let recheck = true;
|
|
201
|
+
const recheckRoutes = () => {
|
|
202
|
+
if (!recheck || lastPort === null) return;
|
|
203
|
+
recheck = false;
|
|
204
|
+
setImmediate(() => { try { if (table().routes.length !== routesSaid) sayRoutes(lastPort); } catch { /* as above */ } });
|
|
205
|
+
};
|
|
24
206
|
const MAX = 65536;
|
|
207
|
+
// The turn a message came in under, when the run tagged it: one opaque id per request, so a
|
|
208
|
+
// model call and its prompt can be pinned to the reply they produced even while five turns are
|
|
209
|
+
// in flight. It is read here and never shown to your app's own code path beyond the header.
|
|
210
|
+
const TURN = /^[A-Za-z0-9:_.-]{1,80}$/;
|
|
211
|
+
const turnOf = (headers) => {
|
|
212
|
+
const t = headers && (typeof headers.get === "function" ? headers.get("x-cortad-turn") : headers["x-cortad-turn"]);
|
|
213
|
+
return typeof t === "string" && TURN.test(t) ? t : undefined;
|
|
214
|
+
};
|
|
215
|
+
// The customer's own rule sentences, written beside the trace by the run, so each model call can
|
|
216
|
+
// say which of them its prompt carried: a rule is then asked only of a reply whose call was told
|
|
217
|
+
// it. Absent file, nothing is claimed either way. Reloaded when the file changes.
|
|
218
|
+
const RULES = process.env.CORTAD_RULES_FILE;
|
|
219
|
+
let rules = null, rulesAt = -1;
|
|
220
|
+
const norm = (s) => String(s || "")
|
|
221
|
+
.replace(/\\u([0-9a-fA-F]{4})/g, (_, h) => String.fromCharCode(parseInt(h, 16)))
|
|
222
|
+
.replace(/\\[nrt]/g, " ").replace(/\\"/g, '"').replace(/\\\\/g, "\\")
|
|
223
|
+
.toLowerCase().replace(/\s+/g, " ").trim();
|
|
224
|
+
// A sentence with a slot in it ("answer in {language}") is matched by its literal parts.
|
|
225
|
+
const partsOf = (text) => text.split(/\{[^}]*\}|\$\{[^}]*\}|%[sd]|<[^>]{1,40}>/).map(norm).filter((p) => p.length >= 12);
|
|
226
|
+
const rulesNow = () => {
|
|
227
|
+
if (!RULES) return null;
|
|
228
|
+
try {
|
|
229
|
+
const at = fs.statSync(RULES).mtimeMs;
|
|
230
|
+
if (at !== rulesAt) {
|
|
231
|
+
rulesAt = at;
|
|
232
|
+
const list = JSON.parse(fs.readFileSync(RULES, "utf8"));
|
|
233
|
+
rules = Array.isArray(list)
|
|
234
|
+
? list.filter((r) => r && typeof r.id === "string" && typeof r.text === "string").map((r) => ({ id: r.id, parts: partsOf(r.text) })).filter((r) => r.parts.length)
|
|
235
|
+
: [];
|
|
236
|
+
}
|
|
237
|
+
} catch { /* not written yet, or gone */ }
|
|
238
|
+
return rules;
|
|
239
|
+
};
|
|
240
|
+
// What the app's tools answered, as the prompt of the next model call carries them: the tool
|
|
241
|
+
// messages of a chat body, the function outputs of a responses body, the tool_result blocks of
|
|
242
|
+
// an Anthropic one, the functionResponse parts of a Gemini one. They are the material a reply's
|
|
243
|
+
// facts rest on, and without them a real ticket id read as invented. Bounded per call.
|
|
244
|
+
const TOOL_TEXT = 3000, TOOLS_MAX = 12;
|
|
245
|
+
const textOf = (v) => (typeof v === "string" ? v : Array.isArray(v) ? v.map((p) => (p && typeof p === "object" ? (p.text || p.content || "") : String(p || ""))).filter(Boolean).join("\n") : v && typeof v === "object" ? JSON.stringify(v) : "");
|
|
246
|
+
const toolsIn = (sent) => {
|
|
247
|
+
let body; try { body = JSON.parse(sent); } catch { return undefined; }
|
|
248
|
+
if (!body || typeof body !== "object") return undefined;
|
|
249
|
+
const out = [];
|
|
250
|
+
const names = new Map();
|
|
251
|
+
const add = (name, text) => { const t = textOf(text).slice(0, TOOL_TEXT); if (t.trim() && out.length < TOOLS_MAX && !out.some((o) => o.text === t)) out.push({ name: String(name || "").slice(0, 80), text: t }); };
|
|
252
|
+
for (const m of Array.isArray(body.messages) ? body.messages : []) {
|
|
253
|
+
if (!m || typeof m !== "object") continue;
|
|
254
|
+
for (const c of Array.isArray(m.tool_calls) ? m.tool_calls : []) if (c && c.id && c.function) names.set(c.id, c.function.name);
|
|
255
|
+
if (Array.isArray(m.content)) for (const c of m.content) if (c && c.type === "tool_use" && c.id) names.set(c.id, c.name);
|
|
256
|
+
}
|
|
257
|
+
for (const m of Array.isArray(body.messages) ? body.messages : []) {
|
|
258
|
+
if (!m || typeof m !== "object") continue;
|
|
259
|
+
if (m.role === "tool" || m.role === "function") add(m.name || names.get(m.tool_call_id), m.content);
|
|
260
|
+
if (Array.isArray(m.content)) for (const c of m.content) if (c && c.type === "tool_result") add(names.get(c.tool_use_id), c.content);
|
|
261
|
+
}
|
|
262
|
+
const items = Array.isArray(body.input) ? body.input : [];
|
|
263
|
+
for (const it of items) if (it && it.type === "function_call" && it.call_id) names.set(it.call_id, it.name);
|
|
264
|
+
for (const it of items) if (it && it.type === "function_call_output") add(names.get(it.call_id), it.output);
|
|
265
|
+
for (const c of Array.isArray(body.contents) ? body.contents : []) for (const p of c && Array.isArray(c.parts) ? c.parts : []) if (p && p.functionResponse) add(p.functionResponse.name, p.functionResponse.response);
|
|
266
|
+
return out.length ? out : undefined;
|
|
267
|
+
};
|
|
268
|
+
// A framework that folds its tools' answers into the next prompt as plain text (an agent loop
|
|
269
|
+
// writing "Observation: ..." into a user message) sends no tool message to find. Within one
|
|
270
|
+
// turn, what the second call's prompt carries that the first one did not, outside the model's
|
|
271
|
+
// own words, is that material. The first call of a turn holds the customer's message and the
|
|
272
|
+
// system prompt, and both are in every later call, so neither is ever counted.
|
|
273
|
+
const textsOf = (body) => {
|
|
274
|
+
const out = new Set();
|
|
275
|
+
const take = (v) => { const t = textOf(v).trim(); if (t) out.add(t.slice(0, TOOL_TEXT)); };
|
|
276
|
+
for (const m of Array.isArray(body.messages) ? body.messages : []) if (m && typeof m === "object" && m.role !== "assistant") take(m.content);
|
|
277
|
+
for (const it of Array.isArray(body.input) ? body.input : []) if (it && typeof it === "object" && it.role !== "assistant" && it.type !== "function_call") take(it.content ?? it.output);
|
|
278
|
+
if (typeof body.system === "string") take(body.system);
|
|
279
|
+
for (const c of Array.isArray(body.contents) ? body.contents : []) if (c && c.role !== "model") for (const p of Array.isArray(c.parts) ? c.parts : []) if (p && typeof p.text === "string") take(p.text);
|
|
280
|
+
return out;
|
|
281
|
+
};
|
|
282
|
+
const seenByTurn = new Map();
|
|
283
|
+
const materialIn = (sent, turn) => {
|
|
284
|
+
if (!turn) return undefined;
|
|
285
|
+
let body; try { body = JSON.parse(sent); } catch { return undefined; }
|
|
286
|
+
if (!body || typeof body !== "object") return undefined;
|
|
287
|
+
const now = textsOf(body);
|
|
288
|
+
const before = seenByTurn.get(turn);
|
|
289
|
+
seenByTurn.set(turn, now);
|
|
290
|
+
if (seenByTurn.size > 200) seenByTurn.delete(seenByTurn.keys().next().value);
|
|
291
|
+
if (!before) return undefined;
|
|
292
|
+
const fresh = [...now].filter((t) => !before.has(t)).slice(0, TOOLS_MAX).map((t) => ({ name: "", text: t }));
|
|
293
|
+
return fresh.length ? fresh : undefined;
|
|
294
|
+
};
|
|
295
|
+
const rulesIn = (sent) => {
|
|
296
|
+
const list = rulesNow();
|
|
297
|
+
if (!list) return undefined;
|
|
298
|
+
const body = norm(sent);
|
|
299
|
+
return list.filter((r) => r.parts.every((p) => body.includes(p))).map((r) => r.id);
|
|
300
|
+
};
|
|
25
301
|
// Where models are served, by host, and the paths every OpenAI-shaped or vendor endpoint ends with.
|
|
26
302
|
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;
|
|
27
303
|
const MODEL_PATH = /\/(?:chat\/completions|completions|responses|messages|embeddings)$|:(?:generateContent|streamGenerateContent)|\/invoke(?:-with-response-stream)?$|\/api\/(?:chat|generate)$/i;
|
|
@@ -49,8 +325,10 @@ if (FILE) {
|
|
|
49
325
|
const emit = http.Server.prototype.emit;
|
|
50
326
|
http.Server.prototype.emit = function (type, req, ...rest) {
|
|
51
327
|
if (type === "listening") { try { listening(this.address()?.port); } catch { /* not a TCP server */ } }
|
|
328
|
+
if (type === "request") recheckRoutes();
|
|
52
329
|
if (type !== "request" || !req || !req.method || /^(?:GET|HEAD|OPTIONS)$/.test(req.method)) return emit.call(this, type, req, ...rest);
|
|
53
|
-
const
|
|
330
|
+
const { "x-cortad-turn": _turn, ...headers } = req.headers;
|
|
331
|
+
const ctx = { method: req.method, path: req.url || "/", headers, chunks: [], size: 0, noted: false, turn: turnOf(req.headers) };
|
|
54
332
|
const push = req.push;
|
|
55
333
|
req.push = function (chunk, encoding) {
|
|
56
334
|
if (chunk && ctx.size < MAX) { const b = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding); ctx.chunks.push(b); ctx.size += b.length; }
|
|
@@ -82,10 +360,11 @@ if (FILE) {
|
|
|
82
360
|
// is read from a clone, so the app's own reader is untouched.
|
|
83
361
|
if (isBun) {
|
|
84
362
|
const inside = (handler, self) => function (req, server) {
|
|
363
|
+
recheckRoutes();
|
|
85
364
|
if (!req || /^(?:GET|HEAD|OPTIONS)$/.test(req.method)) return handler.call(self, req, server);
|
|
86
365
|
let path = "/";
|
|
87
366
|
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 };
|
|
367
|
+
const ctx = { method: req.method, path, headers: Object.fromEntries([...req.headers].filter(([k]) => k !== "x-cortad-turn")), chunks: [], size: 0, noted: false, turn: turnOf(req.headers) };
|
|
89
368
|
// ponytail: a body is copied only when it is small or says it is text; an upload is left alone.
|
|
90
369
|
const size = Number(req.headers.get("content-length") || 0);
|
|
91
370
|
if (size <= MAX || /json|text|form/i.test(req.headers.get("content-type") || "")) {
|
|
@@ -105,6 +384,11 @@ if (FILE) {
|
|
|
105
384
|
}
|
|
106
385
|
return out;
|
|
107
386
|
};
|
|
387
|
+
// Bun's own route table: a handler per path, or a handler per method.
|
|
388
|
+
const bunTable = (table) => Object.entries(table && typeof table === "object" ? table : {}).map(([path, value]) => (
|
|
389
|
+
typeof value === "function" ? { methods: null, path, handler: value }
|
|
390
|
+
: value instanceof Response ? { methods: ["GET"], path, handler: null }
|
|
391
|
+
: { methods: Object.keys(value || {}), path, handler: null }));
|
|
108
392
|
const serve = Bun.serve;
|
|
109
393
|
Bun.serve = function (options, ...rest) {
|
|
110
394
|
let wrapped = options;
|
|
@@ -115,6 +399,10 @@ if (FILE) {
|
|
|
115
399
|
if (options.routes) wrapped.routes = routes(options.routes);
|
|
116
400
|
}
|
|
117
401
|
} catch { wrapped = options; }
|
|
402
|
+
try {
|
|
403
|
+
if (Object.keys(require.cache).some((k) => k.includes("/node_modules/elysia/"))) seen.add("elysia");
|
|
404
|
+
bunRoutes = bunTable(options && options.routes);
|
|
405
|
+
} catch { /* no table */ }
|
|
118
406
|
const server = serve.call(this, wrapped, ...rest);
|
|
119
407
|
try { listening(server && server.port); } catch { /* not ours to fail */ }
|
|
120
408
|
return server;
|
|
@@ -166,10 +454,12 @@ if (FILE) {
|
|
|
166
454
|
const m = /\/models\/([^/:]+):|\/model\/([^/]+)\/(?:invoke|converse)/.exec(path || "");
|
|
167
455
|
return m ? decodeURIComponent(m[1] || m[2]) : "";
|
|
168
456
|
};
|
|
169
|
-
const meter = ({ host, path, sent, status, type, body }) => {
|
|
457
|
+
const meter = ({ host, path, sent, status, type, body, turn }) => {
|
|
170
458
|
try {
|
|
171
459
|
const reply = readReply(type, String(body || "").slice(0, REPLY_MAX));
|
|
172
|
-
const
|
|
460
|
+
const rules = rulesIn(sent);
|
|
461
|
+
const tools = toolsIn(sent) ?? materialIn(sent, turn);
|
|
462
|
+
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), ...(turn ? { turn } : {}), ...(rules ? { rules } : {}), ...(tools ? { tools } : {}) };
|
|
173
463
|
fs.appendFileSync(FILE, JSON.stringify({ call: row }) + "\n", { mode: 0o600 });
|
|
174
464
|
} catch { /* the command is gone */ }
|
|
175
465
|
};
|
|
@@ -196,7 +486,8 @@ if (FILE) {
|
|
|
196
486
|
} catch { return null; }
|
|
197
487
|
};
|
|
198
488
|
const depRow = (dep, status, code) => {
|
|
199
|
-
|
|
489
|
+
const turn = (als.getStore() || {}).turn;
|
|
490
|
+
try { fs.appendFileSync(FILE, JSON.stringify({ dep: { at: Date.now(), ...(dep.env ? { env: dep.env } : {}), host: dep.host, status, code: code ? String(code).slice(0, 40) : undefined, ...(turn ? { turn } : {}) } }) + "\n", { mode: 0o600 }); } catch { /* the command is gone */ }
|
|
200
491
|
};
|
|
201
492
|
|
|
202
493
|
// Outbound: the model call your app makes while it handles that request.
|
|
@@ -210,7 +501,10 @@ if (FILE) {
|
|
|
210
501
|
if (isModelCall(u.host, u.pathname)) url = u;
|
|
211
502
|
} catch { /* not a URL we can read */ }
|
|
212
503
|
if (!url) {
|
|
213
|
-
|
|
504
|
+
// Every outbound host, named by the setting that points at it when one does, and by the
|
|
505
|
+
// host alone otherwise: the search provider behind TAVILY_API_KEY has no URL setting.
|
|
506
|
+
let dep = depOf(input);
|
|
507
|
+
if (!dep) { try { const u = new URL(typeof input === "string" ? input : input && input.url ? input.url : String(input)); if (/^https?:$/.test(u.protocol) && !/^(localhost|127\.0\.0\.1|\[::1\])$/.test(u.hostname)) dep = { host: u.hostname }; } catch { /* not a URL */ } }
|
|
214
508
|
if (!dep) return realFetch.apply(this, arguments);
|
|
215
509
|
return realFetch.apply(this, arguments).then(
|
|
216
510
|
(res) => { depRow(dep, res.status); return res; },
|
|
@@ -219,7 +513,7 @@ if (FILE) {
|
|
|
219
513
|
}
|
|
220
514
|
const sent = bodyText(init && init.body);
|
|
221
515
|
note(als.getStore(), sent);
|
|
222
|
-
const call = { host: url.host, path: url.pathname, sent };
|
|
516
|
+
const call = { host: url.host, path: url.pathname, sent, turn: (als.getStore() || {}).turn };
|
|
223
517
|
return realFetch.apply(this, arguments).then((res) => {
|
|
224
518
|
try {
|
|
225
519
|
// A clone is a tee: your app's branch gets every byte as fast as it reads, this one is read to the end.
|
|
@@ -248,7 +542,7 @@ if (FILE) {
|
|
|
248
542
|
const end = req.end;
|
|
249
543
|
req.write = function (chunk, ...r) { if (chunk && typeof chunk !== "function") parts.push(Buffer.from(chunk)); return write.call(this, chunk, ...r); };
|
|
250
544
|
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] };
|
|
545
|
+
const call = { host: String(host || ""), path: String(path || "").split("?")[0], turn: ctx && ctx.turn };
|
|
252
546
|
let done = false;
|
|
253
547
|
const once = (row) => { if (!done) { done = true; meter({ ...call, sent, ...row }); } };
|
|
254
548
|
req.once("error", () => once({ status: 0, type: "", body: "" }));
|