@cortad/connector 0.6.5 → 0.6.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/README.md CHANGED
@@ -85,7 +85,7 @@ The hooks run after an edit and on your next message: the first names the prompt
85
85
  --wait intelligence: return when the history is read
86
86
  ```
87
87
 
88
- Your app gets this terminal's environment and its own request limits raised for the session, so simulated users are not turned away by them. Test conversations write where your app writes, after your yes in the browser.
88
+ Your app gets this terminal's environment and its request limits raised while Cortad tests it, so its simulated users are not blocked. Test conversations write where your app writes, after your yes in the browser.
89
89
 
90
90
  ## Files it creates
91
91
 
package/lib/origins.mjs CHANGED
@@ -16,6 +16,9 @@ const NEAR = /origin|cors|allow|client|frontend|web_?url|site_?url|app_?url|publ
16
16
  const ENV_ORIGIN = /ORIGIN|CORS|FRONTEND|CLIENT_URL|DOMAIN_CLIENT|WEB_URL|SITE_URL|APP_URL|PUBLIC_URL/;
17
17
  const ENV_KEY = /^(?:X_)?API_KEY$|^APP_API_KEY$|^INTERNAL_API_KEY$|^(?:X_)?ADMIN_KEY$|^API_TOKEN$|^AUTH_TOKEN$|^SERVICE_KEY$|^SERVER_KEY$|^ACCESS_CODE$/;
18
18
  const ENV_FILES = [".env", ".env.local", ".env.development", ".env.development.local"];
19
+ // The port the server's own code listens on when nothing overrides it (`PORT ?? '3100'`,
20
+ // `.listen(3100`): its own address is never its client, even when this session moved it elsewhere.
21
+ const OWN_PORT = /\bPORT\b['"\]]*\s*(?:\?\?|\|\|)\s*['"]?(\d{2,5})|\.listen\(\s*(\d{2,5})\b/g;
19
22
  const FILE_CAP = 512_000;
20
23
 
21
24
  export const envOf = (root) => {
@@ -33,8 +36,7 @@ export function clientOriginOf(root, { port = null, limit = 4000 } = {}) {
33
36
  const scores = new Map();
34
37
  const count = (url, by) => scores.set(url, (scores.get(url) ?? 0) + by);
35
38
  let seen = 0;
36
- const own = port ? Number(port) : null;
37
- const portOf = (m) => Number(m[1] ?? (m[0].startsWith("https") ? 443 : 80));
39
+ const own = new Set(port ? [Number(port)] : []);
38
40
  const walk = (dir, depth) => {
39
41
  if (depth > 6 || seen > limit) return;
40
42
  let entries;
@@ -46,19 +48,21 @@ export function clientOriginOf(root, { port = null, limit = 4000 } = {}) {
46
48
  const file = join(dir, e.name);
47
49
  let text;
48
50
  try { if (statSync(file).size > FILE_CAP) continue; text = readFileSync(file, "utf8"); } catch { continue; }
51
+ for (const m of text.matchAll(OWN_PORT)) own.add(Number(m[1] ?? m[2]));
49
52
  for (const m of text.matchAll(LOCAL)) {
50
- if (own && portOf(m) === own) continue;
51
53
  const around = text.slice(Math.max(0, m.index - 100), m.index);
52
54
  count(m[0], NEAR.test(around) ? 3 : 1);
53
55
  }
54
56
  }
55
57
  };
56
58
  walk(root, 0);
57
- for (const [name, value] of Object.entries(envOf(root))) {
59
+ const env = envOf(root);
60
+ if (/^\d{2,5}$/.test(String(env["PORT"] ?? ""))) own.add(Number(env["PORT"]));
61
+ for (const [name, value] of Object.entries(env)) {
58
62
  if (!ENV_ORIGIN.test(name)) continue;
59
- for (const m of String(value).matchAll(LOCAL)) if (!own || portOf(m) !== own) count(m[0], 4);
63
+ for (const m of String(value).matchAll(LOCAL)) count(m[0], 4);
60
64
  }
61
- const best = [...scores].sort((a, b) => b[1] - a[1])[0];
65
+ const best = [...scores].filter(([url]) => !own.has(Number(new URL(url).port || (url.startsWith("https") ? 443 : 80)))).sort((a, b) => b[1] - a[1])[0];
62
66
  return best ? best[0] : null;
63
67
  }
64
68
 
package/lib/read-text.mjs CHANGED
@@ -109,7 +109,7 @@ export const connectedText = (f) => machineText(f).slice(0, 1);
109
109
  // whole session was a terminal and a coding agent, which never had one open. Each opens on the state
110
110
  // it is reporting, and the one place that session can act on is the agent.
111
111
  export const CONNECTED = "connected: Cortad is watching your app. Next, your coding agent calls Cortad's status: it says what to tell you and what it needs. Ctrl-C disconnects and stops what your start command started.";
112
- export const NOT_RUNNING = "your app is not running, so Cortad has nothing to test. What stopped it is the line above, from starting your app, not from Cortad.";
112
+ export const NOT_RUNNING = "your app is not running, so Cortad has nothing to test. The line above is your app's own error.";
113
113
 
114
114
  // A payload with no read is not a read in progress, and saying so sent a customer's agent back to
115
115
  // poll a reading that had already ended: fifteen minutes on a repository that calls no model, where
package/local.mjs CHANGED
@@ -680,7 +680,7 @@ async function startApp() {
680
680
  // Only its own request limits: who the app lets in is never touched.
681
681
  const lifted = liftedLimits(sourceFiles);
682
682
  const raised = Object.keys(lifted);
683
- if (raised.length) say(`your app's own request limits are raised for this session only, so the simulated users are not turned away by them: ${raised.join(", ")}`);
683
+ if (raised.length) say(`raising your app's request limits while Cortad tests it, so its simulated users are not blocked: ${raised.join(", ")}`);
684
684
  launched = { cmd: typed, lifted };
685
685
  if (proxyAsked || !hookable(appDir, typed)) await useProxy();
686
686
  step(`starting your app with: ${shownCommand(typed, mask)}${fromSaved ? " (saved from your last connect)" : ""}`);
@@ -1064,7 +1064,7 @@ const head = (() => {
1064
1064
  const firstStart = startApp();
1065
1065
  firstStart.catch(() => { /* appLife reads the failure */ });
1066
1066
  const archive = join(work, "tree.tgz");
1067
- step("connecting");
1067
+ step("connecting to your Cortad account");
1068
1068
  // COPYFILE_DISABLE: a Mac's tar adds no "._name" entries.
1069
1069
  await exec("tar", ["-czf", archive, "-C", root, "-T", list], { env: { ...process.env, COPYFILE_DISABLE: "1" } });
1070
1070
  const bytes = readFileSync(archive);
@@ -1078,7 +1078,7 @@ const parts = Math.max(1, Math.ceil(bytes.length / PART));
1078
1078
  async function sendTree() {
1079
1079
  for (let off = 0; off < bytes.length; off += PART) {
1080
1080
  const last = off + PART >= bytes.length;
1081
- step(parts > 1 ? `connecting, ${Math.floor(off / PART) + 1} of ${parts}` : "connecting");
1081
+ step(parts > 1 ? `connecting to your Cortad account, ${Math.floor(off / PART) + 1} of ${parts}` : "connecting to your Cortad account");
1082
1082
  // The last part asks for this machine's key when none is stored yet: minted once the repository
1083
1083
  // row exists, kept in ~/.cortad for the runs a coding agent asks for on later days. A connect by a
1084
1084
  // code asks too, since the code may be another account's: the key kept from the account before
@@ -1121,7 +1121,9 @@ async function sendHistory() {
1121
1121
  }
1122
1122
  }
1123
1123
  // Unchanged code has already been read: coming back says so instead of claiming a second read.
1124
- stepDone(moved ? "connected: your change is in, and what Cortad read before still stands" : resumed ? "connected: nothing changed since last time" : "connected");
1124
+ // Said of the account, never of the app: a bare "connected" here, above an app that could not take its
1125
+ // port, was read as the app being up (2026-11-12).
1126
+ stepDone(moved ? "connected to your Cortad account: your change is in, and what Cortad read before still stands" : resumed ? "connected to your Cortad account: your code has not changed since last time" : "connected to your Cortad account");
1125
1127
  if (head) void sendHistory().catch((err) => { if (verbose) say(`commit history not sent: ${err?.message ?? err}`); });
1126
1128
  // The coding agents on this machine learn about Cortad now, once: an MCP entry and a skill in each
1127
1129
  // one's own home folder. A run started by an agent later comes back through lib/cli.mjs.
@@ -1130,7 +1132,7 @@ if (head) void sendHistory().catch((err) => { if (verbose) say(`commit history n
1130
1132
  if (!viaToken && process.env.CORTAD_NO_REGISTER !== "1") {
1131
1133
  const got = await registerAll().catch((err) => { if (verbose) say(`could not register with your coding agents: ${err?.message ?? err}`); return { clients: [], changed: [] }; });
1132
1134
  const and = (list) => new Intl.ListFormat("en", { type: "conjunction" }).format(list);
1133
- if (got.changed.length) say(`${and(got.clients)} can now use Cortad: its tools and skill were added to ${got.changed.map((f) => `~/${f}`).join(", ")}. This repository was not changed.`);
1135
+ if (got.changed.length) say(`${and(got.clients)} can now use Cortad: its tools and skill were added to ${got.changed.map((f) => `~/${f}`).join(", ")}.`);
1134
1136
  }
1135
1137
 
1136
1138
  lock = await makeLock({ root, work });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortad/connector",
3
- "version": "0.6.5",
3
+ "version": "0.6.8",
4
4
  "description": "Connects the AI app on your machine to Cortad for test conversations, and gives your coding agent the MCP and skill to run them.",
5
5
  "bin": {
6
6
  "cortad": "local.mjs"
package/skill/SKILL.md CHANGED
@@ -17,7 +17,7 @@ Seven tools:
17
17
  - `connect`: connect the live app, the same steps as `npx -y {{cortad}} intelligence`.
18
18
  - `tell`: a note to the Cortad team; that a problem's question reads this app wrong (`findingId` and `why`); what counts as a good ending; behavioral questions to propose; an address for incidents.
19
19
 
20
- Where the tools are missing, every command is `npx {{cortad}} <command>` in a shell with the same output, for example `npx {{cortad}} behavior --against previous` or `npx {{cortad}} run_status`. The person's own instructions come before this skill.
20
+ The tools load with the coding agent's next session after the first connect. Until then, and wherever they are missing, every command is `npx {{cortad}} <command>` in a shell with the same output, the normal way to work in that session and nothing to mention to the person, for example `npx {{cortad}} behavior --against previous` or `npx {{cortad}} run_status`. The person's own instructions come before this skill.
21
21
 
22
22
  ## Talking to the person
23
23