cortad 0.1.4 → 0.1.6

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 (2) hide show
  1. package/local.mjs +76 -6
  2. package/package.json +1 -1
package/local.mjs CHANGED
@@ -149,8 +149,13 @@ function envOrigins(envFiles) {
149
149
  const THROUGHPUT = /(RATE_?LIMIT|DAILY_LIMIT|HOURLY_LIMIT|MINUTE_LIMIT|REQUESTS_PER|TOKEN_BUDGET|MAX_SSE|MAX_CONCURRENT|THROTTLE|_RPM$|_RPS$|_QPS$)/;
150
150
  const GUARDED = /(AUTH|LOGIN|PASSWORD|BREAKER|LOCKOUT|ATTEMPT|FAIL|BAN|BLOCK)/;
151
151
  const SWITCH = /_(?:ENABLED|DISABLED|ENABLE|DISABLE)$|^(?:ENABLE|DISABLE)_/;
152
- function liftedLimits(envFiles) {
153
- const lifted = {};
152
+ // Limit-shaped names the code itself reads (process.env.GUEST_DAILY_LIMIT, os.environ.get("RATE_LIMIT_RPM"))
153
+ // count too: a limit with a default in code and no line in .env is the one that closes on a run.
154
+ const ENV_READ = /(?:process\.env(?:\.|\[\s*['"])|os\.(?:environ\.get|getenv)\(\s*['"]|os\.environ\[\s*['"]|\benv\(\s*['"]|Deno\.env\.get\(\s*['"])([A-Z][A-Z0-9_]*)/g;
155
+ const lifted = (name) => (/(TOKEN_BUDGET|TOKENS)/.test(name) ? "1000000000" : "1000000");
156
+ const liftable = (name) => THROUGHPUT.test(name) && !GUARDED.test(name) && !SWITCH.test(name);
157
+ function liftedLimits(envFiles, sources = []) {
158
+ const out = {};
154
159
  for (const file of envFiles) {
155
160
  let text = "";
156
161
  try { text = readFileSync(file, "utf8"); } catch { continue; }
@@ -159,10 +164,16 @@ function liftedLimits(envFiles) {
159
164
  if (!m) continue;
160
165
  const [, name, raw] = m;
161
166
  const value = raw.trim().replace(/^(['"])(.*)\1$/, "$2");
162
- if (THROUGHPUT.test(name) && !GUARDED.test(name) && !SWITCH.test(name) && /^\d+$/.test(value)) lifted[name] = /(TOKEN_BUDGET|TOKENS)/.test(name) ? "1000000000" : "1000000";
167
+ if (liftable(name) && /^\d+$/.test(value)) out[name] = lifted(name);
163
168
  }
164
169
  }
165
- return lifted;
170
+ for (const file of sources) {
171
+ if (!/\.(?:[cm]?[jt]sx?|py|go|rb|php|rs)$/.test(file)) continue;
172
+ let text = "";
173
+ try { if (statSync(file).size > 512_000) continue; text = readFileSync(file, "utf8"); } catch { continue; }
174
+ for (const m of text.matchAll(ENV_READ)) if (liftable(m[1]) && !(m[1] in out)) out[m[1]] = lifted(m[1]);
175
+ }
176
+ return out;
166
177
  }
167
178
  let secrets = [];
168
179
  let identities = null;
@@ -335,6 +346,9 @@ async function startApp() {
335
346
  const wanted = Number(flag("--port"));
336
347
  if (wanted) {
337
348
  if (!(await answers(wanted))) fail(`nothing is answering on port ${wanted}. Start your app first, then run this again.`);
349
+ const took = await takeOver(wanted);
350
+ if (took) return took;
351
+ say("your app was already running, so its request limits stay as they are; if it answers 429, stop it and run this without --port and they are raised for the session");
338
352
  return { port: wanted, cmd: null };
339
353
  }
340
354
  const plan = startPlan({ root, typed: flag("--start"), onPath });
@@ -343,7 +357,7 @@ async function startApp() {
343
357
  appDir = plan?.cwd ?? root;
344
358
  pinned = pinnedNode();
345
359
  if (plan?.within) say(`your app is in ${plan.within}, started there with: ${cmd}`);
346
- const lifted = liftedLimits(envFiles);
360
+ const lifted = liftedLimits(envFiles, files.map((f) => join(root, f)));
347
361
  if (Object.keys(lifted).length) say(`higher request limits for this session: ${Object.keys(lifted).join(", ")}`);
348
362
  launched = { cmd, lifted };
349
363
  step(`starting your app: ${cmd}`);
@@ -354,7 +368,13 @@ async function startApp() {
354
368
  if (up.exited !== null && /EADDRINUSE|address already in use|port.{0,40}(?:in use|already used|is taken|unavailable)/i.test(up.tail)) {
355
369
  const ports = [...up.tail.matchAll(/(?::|port\s*[:=]?\s*)(\d{4,5})\b/gi)].map((m) => Number(m[1]));
356
370
  for (const port of new Set(ports)) {
357
- if (await answers(port)) { launched = null; child = null; say(`your app is already running on port ${port}, so that one is used`); return { port, cmd: null }; }
371
+ if (await answers(port)) {
372
+ launched = null; child = null;
373
+ const took = await takeOver(port);
374
+ if (took) return took;
375
+ say(`your app is already running on port ${port}, so that one is used; its request limits stay as they are`);
376
+ return { port, cmd: null };
377
+ }
358
378
  }
359
379
  }
360
380
  if (up.tail) console.error(up.tail);
@@ -362,6 +382,55 @@ async function startApp() {
362
382
  return { port: null, said: up.tail, why: `${up.exited !== null ? "your app stopped before it answered" : "your app did not answer within three minutes"}; what it said is above.${wrongNode}` };
363
383
  }
364
384
 
385
+ // An app the person started keeps the limits it started with, and a run needs more than a person
386
+ // uses in a month. With their yes, the command stops that app and starts it the way it would have,
387
+ // limits raised for the session. Only in a terminal, only when the code reads a limit, and only
388
+ // when the command knows how to start the app.
389
+ async function takeOver(port) {
390
+ const lifted = liftedLimits(envFiles, files.map((f) => join(root, f)));
391
+ const names = Object.keys(lifted);
392
+ if (!names.length || !process.stdin.isTTY) return null;
393
+ const plan = startPlan({ root, typed: flag("--start"), onPath });
394
+ if (!plan?.cmd) return null;
395
+ const pid = await listenerOn(port);
396
+ if (!pid) return null;
397
+ const answer = await ask(`your app is running with its own request limits (${names.join(", ")}). Restart it with them raised for this session? [Y/n] `);
398
+ if (answer && !/^y(?:es)?$/i.test(answer)) return null;
399
+ const top = await supervisorOf(pid);
400
+ step(`stopping your app (pid ${top}) to start it with higher limits`);
401
+ await stopApp(top);
402
+ for (let i = 0; i < 50 && (await answers(port)); i++) await new Promise((r) => setTimeout(r, 200));
403
+ if (await answers(port)) { say("your app did not stop, so it is used as it is"); return null; }
404
+ appDir = plan.cwd ?? root;
405
+ pinned = pinnedNode();
406
+ if (plan.within) say(`your app is in ${plan.within}, started there with: ${plan.cmd}`);
407
+ say(`higher request limits for this session: ${names.join(", ")}`);
408
+ launched = { cmd: plan.cmd, lifted };
409
+ step(`starting your app: ${plan.cmd}`);
410
+ const up = await launch(180_000);
411
+ if (up.port) return { port: up.port, cmd: plan.cmd, lifted: names };
412
+ if (up.tail) console.error(up.tail);
413
+ fail("your app did not come back after the restart. Start it yourself, then run this again with --port.");
414
+ }
415
+ // The process listening on a port.
416
+ async function listenerOn(port) {
417
+ try { return Number((await exec("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"])).stdout.trim().split("\n")[0]) || null; } catch { return null; }
418
+ }
419
+ // The top of the chain that runs the app: nodemon, npm, the sh -c under it. Climbs from the listener
420
+ // while the parent is a runner, never into the person's own shell or terminal.
421
+ const RUNNER = /^(?:\S*\/)?(?:node|npm|npx|pnpm|yarn|bun|deno|python[\d.]*|uvicorn|gunicorn|flask|tsx|ts-node|nodemon|concurrently|pm2)(?:\s|$)|^(?:\/bin\/)?sh -c\b/;
422
+ async function supervisorOf(pid) {
423
+ const rows = (await exec("ps", ["-axo", "pid=,ppid=,args="])).stdout.trim().split("\n").map((l) => l.trim());
424
+ const table = new Map(rows.map((l) => { const m = /^(\d+)\s+(\d+)\s+(.*)$/.exec(l); return m ? [Number(m[1]), { ppid: Number(m[2]), args: m[3] }] : [0, null]; }));
425
+ let top = pid;
426
+ for (let i = 0; i < 8; i++) {
427
+ const parent = table.get(table.get(top)?.ppid ?? 0);
428
+ if (!parent || !RUNNER.test(parent.args)) break;
429
+ top = table.get(top).ppid;
430
+ }
431
+ return top;
432
+ }
433
+
365
434
  // Your app, started the way you start it, and watched until one of its own ports answers.
366
435
  let launched = null;
367
436
  // The Node this project pins, when the shell that ran this command has another. Strapi refuses
@@ -480,6 +549,7 @@ if (explain) {
480
549
  `not sent anything git ignores, env files (${envFiles.length} here: ${envFiles.slice(0, 6).map(rel).join(", ") || "none"}), key files, data files, node_modules, .git`,
481
550
  `env files read here only: to hide their values in replies, and to sign in a test account`,
482
551
  `would start ${flag("--port") ? `nothing: uses your app on port ${flag("--port")}` : plan ? `${plan.cmd} (in ${rel(plan.cwd)})` : "asks you how your app starts"}`,
552
+ `would raise ${flag("--port") ? "nothing: your app's own request limits stay as they are" : `${Object.keys(liftedLimits(envFiles, files.map((f) => join(root, f)))).join(", ") || "no request limits found"} (for this session only)`}`,
483
553
  `loads into app lib/trace.cjs (Node) or lib/pyhook/sitecustomize.py (Python): records the one request during which your app calls a model`,
484
554
  `agent edits in your files, each with an undo kept in ~/.cortad/checkpoints; git is never touched`,
485
555
  `agent shell confined by the OS: your project and toolchains only, writes to temp and build folders, localhost only`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cortad",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Connects the AI app on your machine to Cortad for test conversations. No dependencies.",
5
5
  "bin": {
6
6
  "cortad": "local.mjs"