@cortad/connector 0.6.4 → 0.6.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.
package/README.md CHANGED
@@ -35,8 +35,6 @@ cortad intelligence site <address> the one site the page key answers
35
35
  cortad intelligence --wait return when the history is read, with what it found
36
36
  ```
37
37
 
38
- The ingest key only sends conversations to Cortad: it reads nothing and spends nothing. It is never printed and never copied to a clipboard; the command names its line in your env file.
39
-
40
38
  ## Your coding agent
41
39
 
42
40
  After the connect, your agent has six tools, one for each thing it needs from Cortad:
@@ -77,7 +75,6 @@ The hooks run after an edit and on your next message: the first names the prompt
77
75
  -- <command> how your app starts; everything after -- is run as typed (--start "cmd" says the same)
78
76
  --port 3000 use an app that is already running, as it is
79
77
  --port 3000 -- <command> start a container and watch the port it publishes; the model base URL to set inside it is printed
80
- --explain show what would be sent and started, then exit
81
78
  --verbose print your app's output
82
79
  --from <tool> intelligence history: the tool your conversations are read from
83
80
  --stdin intelligence history: read your own export from standard input
@@ -88,23 +85,15 @@ The hooks run after an edit and on your next message: the first names the prompt
88
85
  --wait intelligence: return when the history is read
89
86
  ```
90
87
 
91
- Nothing about your environment is guessed or changed. Your app gets this terminal's environment and its own request limits raised for the session, by name; its env files are read only to hide their values in what leaves this machine. Nothing is copied or started beside it: test conversations write where your app writes, after your yes in the browser.
92
-
93
- ## What leaves this machine
94
-
95
- The part of the code that is its AI, chosen by the same parsers that read it: the prompts, the evals, the code that calls the model, the routes that reach it and the shapes they read, with the README, the manifests and the schema. The commit history since the last connect goes too, each commit with its message, author, dates and a diff of those same files, with any line that carries a key replaced first. Env files are read only to hide their values; keys never leave. A project that connected before 2026-10-23 sent its whole tree and goes on doing so, so its read and its history stand.
96
-
97
- After the connect, the command sends each AI endpoint one test request built from what was read, from this machine to your app on its port, and the conversations of a run go the same way. The exchanges, the model calls your app made and the replies are what Cortad judges; `--explain` prints what would be sent and started without sending anything.
98
-
99
- With `intelligence`, the conversations you chose: the person's and the app's messages with their times, keys, card numbers, emails and phone numbers replaced on this machine first, the app's conversation and user ids hashed. Exactly what is sent is written first to `~/.cortad/<project>/history.json`. It talks to cortad.com and, when you choose one, to that tool's own address with the keys already in your env; those keys go nowhere else. It never connects to a database itself and never sends a connection string.
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.
100
89
 
101
90
  ## Files it creates
102
91
 
103
- - `~/.cortad/<project>/` the key, the start command, the last tree digest, which process holds your app up, your app's output in `app.log`, the sign-in your own request carried, the run it asked for last, in `news.txt` the line a finished run left for your agent, removed once the agent has it, in `heard.txt` the ids of the runs your agent has read, in `history.json` the conversations `intelligence` sent, and in `pairing.json` a link waiting for your approval
92
+ - `~/.cortad/<project>/` the key, the start command, the last tree digest, which process holds your app up, your app's output in `app.log`, the sign-in your own request carried, the run it asked for last, in `news.txt` the line a finished run left for your agent, removed once the agent has it, in `heard.txt` the ids of the runs your agent has read, and in `pairing.json` a link waiting for your approval
104
93
  - your app's env file (`.env.local`, `.env` or `.dev.vars`, whichever it loads) Cortad's lines alone: `CORTAD_INGEST_KEY`, `CORTAD_PUBLIC_KEY` and `CORTAD_ENV=development`; only when git ignores the file
105
94
  - `$TMPDIR/cortad-<pid>/` removed on exit
106
95
 
107
- macOS and Linux, Node 20 or later. One dependency, playwright-core, loaded only to capture a request your app's own page makes. What is sent and what stays on your machine: `cortad --explain`, and cortad.com/privacy.
96
+ macOS and Linux, Node 20 or later. One dependency, playwright-core, loaded only to capture a request your app's own page makes.
108
97
 
109
98
  ## Licence
110
99
 
@@ -1,5 +1,4 @@
1
- // What was selected, sent to Cortad in sends under 1 MiB each, exactly as written to history.json
2
- // first. A send Cortad already has is taken once: running this again sends only what is missing.
1
+ // What was selected, sent to Cortad in sends under 1 MiB each. A send Cortad already has is taken once: running this again sends only what is missing.
3
2
  const MAX_BYTES = 900_000;
4
3
  const MAX_CONVERSATIONS = 500;
5
4
  const TRIES = 5;
@@ -8,10 +8,7 @@ export const lines = (...parts) => parts.flat().filter((part) => part !== null &
8
8
  const num = (n) => Number(n).toLocaleString("en-US");
9
9
  const day = (iso) => (iso ? new Date(iso).toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric", timeZone: "UTC" }) : "");
10
10
 
11
- export const header = (app) => [
12
- `Cortad Intelligence for ${app}`,
13
- "This command talks to cortad.com and, when you choose one, the tool your conversations are kept in. It writes Cortad's lines in your env file and files under ~/.cortad on this computer. It never edits your code or your coding agent's settings.",
14
- ];
11
+ export const header = (app) => [`Cortad Intelligence for ${app}`];
15
12
 
16
13
  export const approveFirst = ({ spec, link, words, machine }) => lines(
17
14
  "This computer needs the person's approval to connect to Cortad.",
@@ -26,7 +23,7 @@ export const keyRefused = "Cortad no longer accepts the key this computer kept f
26
23
 
27
24
  export const keysWritten = (file, at) => [
28
25
  `Wrote CORTAD_INGEST_KEY and CORTAD_PUBLIC_KEY to ${file}, lines ${at.ingest} and ${at.page}${at.env ? `, and CORTAD_ENV=development on line ${at.env} so chats on this computer are never counted as your users'` : ""}. ${file} is git-ignored.`,
29
- `CORTAD_INGEST_KEY is a secret, so this names its line instead of printing it; open ${file} to see it. It can only send conversations to Cortad: it reads nothing and spends nothing.`,
26
+ `CORTAD_INGEST_KEY is a secret, so this names its line instead of printing it; open ${file} to see it.`,
30
27
  ];
31
28
  export const keysKept = (file) => [`Cortad's keys are already in ${file} and still work; they stay as they are.`];
32
29
  export const keysManaged = (manager, path) => [
@@ -56,7 +53,6 @@ export function historyQuestion({ spec, sources, stores }) {
56
53
  `Ask the person, with your question tool: "Send our users' newest 2,500 messages from the last 30 days to Cortad, so it can show us our users?" Cortad counts your app's replies and takes each conversation whole.`,
57
54
  ...options.map((o, i) => ` ${i + 1}. ${o}`),
58
55
  ...sources.map((s) => `${s.name} is read from this computer at ${s.host} with the keys in your env; the keys go to ${s.name} only.`),
59
- "Before anything is sent, keys, card numbers, emails and phone numbers in the messages are replaced on this computer, and the app's conversation and user ids are hashed. What is sent goes to cortad.com, and a copy of exactly what was sent stays in ~/.cortad on this computer.",
60
56
  "",
61
57
  ...exportContract(spec),
62
58
  ];
@@ -82,9 +78,8 @@ export function historySent({ result, picked, span, source }) {
82
78
  "Cortad is reading them now. They count against nothing: the plan's messages are for your live app.",
83
79
  ];
84
80
  }
85
- export const historyDry = ({ picked, span, file }) => [
81
+ export const historyDry = ({ picked, span }) => [
86
82
  `Read ${num(picked.conversations.length)} conversations, ${day(span.from)} to ${day(span.to)}: ${num(picked.total)} messages, ${num(picked.messages)} of them your app's replies. Nothing was sent.`,
87
- `What would be sent is in ${file}.`,
88
83
  ];
89
84
  export const historyLooksLikeDevelopment = ({ why, spec, rest }) => [
90
85
  `Nothing was sent: this looks like a development copy, because ${why}.`,
@@ -163,16 +163,13 @@ export async function intelligence(args, ctx) {
163
163
  const picked = selected(raws, { key, cap: opts.cap, secrets: secretValues(ENV_FILES.map((f) => join(root, f)).filter(existsSync)).filter(credentialShaped) });
164
164
  if (picked.conversations.length === 0) return { text: say.lines(say.historyNothing(label)), code: 1 };
165
165
  const sends = packed(source, picked.conversations);
166
- const file = join(homeOf(ctx.project), "history.json");
167
- mkdirSync(homeOf(ctx.project), { recursive: true, mode: 0o700 });
168
- writeFileSync(file, `${JSON.stringify(sends, null, 1)}\n`, { mode: 0o600 });
169
166
  const span = spanOf(picked.conversations);
170
167
  const why = looksLikeDevelopment({ host, conversations: picked.conversations, users: picked.users });
171
168
  if (why && !opts.anyway) return { text: say.lines(say.historyLooksLikeDevelopment({ why, spec, rest: opts.from ? `--from ${opts.from}` : "--stdin" })) };
172
- if (opts.dry) return { text: say.lines(say.historyDry({ picked, span, file })) };
169
+ if (opts.dry) return { text: say.lines(say.historyDry({ picked, span })) };
173
170
  const result = await sendAll(sends, { api: ctx.apiBase, token: token() });
174
171
  if (result.refused) return { text: say.lines(`Cortad did not take the rest: ${result.refused}`, `${result.taken} conversations were taken before that.`), code: 1 };
175
- return { text: say.lines(say.historySent({ result, picked, span, source: label, file }), say.next(spec)), sent: true };
172
+ return { text: say.lines(say.historySent({ result, picked, span, source: label }), say.next(spec)), sent: true };
176
173
  }
177
174
 
178
175
  // The ingest key as this folder holds it: in its env file, or in keys.env when a manager holds its settings.
package/lib/read-text.mjs CHANGED
@@ -83,29 +83,22 @@ export function readLines(r) {
83
83
  }
84
84
 
85
85
  // Where each coding agent keeps the entry the connect command writes for Cortad, in the home folder.
86
- const listed = (names) => (names.length > 1 ? `${names.slice(0, -1).join(", ")} and ${names.at(-1)}` : names[0]);
87
86
 
88
87
  // What the connect command does on this machine, from the facts it keeps in the runner's record:
89
- // what it started and stops, the database copies, the settings its app runs with, what it loads,
90
- // what a run holds and puts back, and what it keeps outside this repository. The first line is the
91
- // one the terminal says at connect (connectedText); the rest is read when someone asks.
88
+ // what it started and stops, what it sees, and an outside service a run would really reach. The first
89
+ // line is the one the terminal says at connect (connectedText); the rest is read when someone asks.
92
90
  export function machineText(f) {
93
91
  if (!f) return ["No connect command is running for this repository now, so nothing Cortad started is running here."];
94
92
  const lines = [
95
93
  f.started
96
- ? `Cortad started your app in ${f.dir === "." ? "this folder" : f.dir} and stops it when this command stops. Your code and settings files are never edited.`
97
- : `Cortad uses your app already running on port ${f.port} and leaves it running. Your code and settings files are never edited.`,
94
+ ? `Cortad started your app in ${f.dir === "." ? "this folder" : f.dir} and stops it when this command stops.`
95
+ : `Cortad uses your app already running on port ${f.port} and leaves it running.`,
98
96
  ];
99
- // Env values reach only a process the command started itself.
100
- if (f.started) lines.push(`Your env values stay on this machine${f.changed?.length ? `, with ${listed(f.changed)} set for this session only` : ""}. None is sent to Cortad.`);
101
- lines.push(f.hooked ? "Cortad sees each request that reaches your model and the model calls it makes. Request headers stay on this machine; the one sign-in Cortad's test requests use is kept in ~/.cortad/<project>/sign-in.json, readable by you alone, until your app refuses it or you delete it."
102
- : f.proxy ? `A local proxy on port ${f.proxy} sees each request sent to it and the model calls your app makes through it. Request headers stay on this machine; the one sign-in Cortad's test requests use is kept in ~/.cortad/<project>/sign-in.json, readable by you alone, until your app refuses it or you delete it.`
97
+ lines.push(f.hooked ? "Cortad sees each request that reaches your model and the model calls it makes."
98
+ : f.proxy ? `A local proxy on port ${f.proxy} sees each request sent to it and the model calls your app makes through it.`
103
99
  : "Cortad did not start your app, so it does not see the calls your app makes to its model.");
104
- // Only the hook holds a write or records a file: through the proxy, or with nothing loaded, both go as the app does them.
105
- lines.push(f.hooked ? "During a run, anything your app would send to an outside service, such as a payment or an email, is answered inside your app and never sent, unless it carries a test key, goes to a provider's sandbox, or goes to a host you allowed on the card. A call that only reads goes out."
106
- : "During a run, what your app sends to an outside service, such as a payment or an email, is sent for real.");
107
- lines.push(f.hooked ? "Files your app writes in its own folder during a run are put back when the run ends. Its log files are left as they are." : "Files your app writes in its own folder stay as it writes them.");
108
- lines.push("Kept in ~/.cortad on this machine between connects: this repository's key, the key the test accounts' passwords are made from, what to put back after a run, and your app's output from the last session with your env values masked.");
100
+ // Only the hook answers an outside write itself: through the proxy, or with nothing loaded, a payment or an email goes out.
101
+ if (!f.hooked) lines.push("During a run, what your app sends to an outside service, such as a payment or an email, is sent for real.");
109
102
  return lines;
110
103
  }
111
104
  // What the terminal says once the app answers: the one line the person needs.
@@ -116,7 +109,7 @@ export const connectedText = (f) => machineText(f).slice(0, 1);
116
109
  // whole session was a terminal and a coding agent, which never had one open. Each opens on the state
117
110
  // it is reporting, and the one place that session can act on is the agent.
118
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.";
119
- 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.";
120
113
 
121
114
  // A payload with no read is not a read in progress, and saying so sent a customer's agent back to
122
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)" : ""}`);
@@ -979,33 +979,10 @@ secrets = secretValues(envFiles, shellSecrets);
979
979
  envKeys = [...secretEnvValues(envFiles, (f) => readFileSync(f, "utf8")), ...shellSecrets];
980
980
  const kept = [];
981
981
  for (let i = files.length - 1; i >= 0; i -= 1) if (carriesKey(files[i])) kept.unshift(...files.splice(i, 1));
982
- // --explain: what this would send and start, from this folder, and nothing else. No network, no
983
- // app started, nothing written. For the person (or the agent) who reads before running.
982
+ // --explain: the files this folder would give, one line, for the check that keys and ignored files
983
+ // stay out (lib/sent-files.test.mjs). No network, no app started, nothing written.
984
984
  if (explain) {
985
- const going = new Set([...files, ...kept]);
986
- const left = listed ? [...listed].filter((f) => !going.has(f) && !ENV_FILE.test(basename(f))).sort() : [];
987
- const bytes = files.reduce((n, f) => { try { return n + statSync(join(root, f)).size; } catch { return n; } }, 0);
988
- const rel = (f) => relative(root, f) || ".";
989
- console.log([
990
- `cortad --explain (nothing is sent or started by this)`,
991
- ``,
992
- `talks to ${origin.origin}, and localhost on your app's port only`,
993
- `would send ${files.length} source files, ${Math.round(bytes / 1024)} KB, once${listed ? " (what git would commit)" : ""}`,
994
- ...(kept.length ? [`kept here ${kept.length} file${kept.length === 1 ? " that holds" : "s that hold"} keys: ${kept.slice(0, 6).join(", ")}${kept.length > 6 ? ", ..." : ""}`] : []),
995
- ...(left.length ? [`also not sent ${left.length} tracked media or lock file${left.length === 1 ? "" : "s"}: ${left.slice(0, 6).join(", ")}${left.length > 6 ? ", ..." : ""}`] : []),
996
- `not sent anything git ignores, env files (${envFiles.length} here: ${envFiles.slice(0, 6).map(rel).join(", ") || "none"}), key files, node_modules`,
997
- `env files read here only, to hide their values in what leaves and to know where your app's model settings point; nothing in them is handed to your app or sent`,
998
- `would start ${wantedPort ? `nothing: uses your app on port ${wantedPort}, replies seen and model calls not` : typed ? `${typed} (your command, as typed, in ${rel(appDir)})` : "nothing: run again with your start command after --"}`,
999
- `would raise ${wantedPort || !typed ? "nothing" : `your app's own request limits, for this session only, so the simulated users are not turned away by them: ${Object.keys(liftedLimits(files.map((f) => join(root, f)))).join(", ") || "none found"}`}`,
1000
- `loads into app lib/trace.cjs (Node, Bun) or lib/pyhook/sitecustomize.py (Python): records each request during which your app calls a model, what it answered and the model calls on the way; header values stay here`,
1001
- `other apps a local proxy on 127.0.0.1 in front of your app's port and where its model settings point (OPENAI_BASE_URL and the others), recording the same; it passes each call on to the provider your settings name`,
1002
- `your files never written by this program; your own coding agent edits them`,
1003
- `your data nothing is copied, started or pointed elsewhere: your app uses the databases and services your command gives it, and test conversations write where it writes, after your yes on the card`,
1004
- `test shell confined by the OS: your project and toolchains only, writes to temp and build folders, localhost only`,
1005
- `for your agent an MCP entry and a skill in each coding agent's own home folder (Claude Code, Codex, Cursor), and a key in ~/.cortad for later runs`,
1006
- ``,
1007
- `first files ${files.slice(0, 8).join(", ")}${files.length > 8 ? ", ..." : ""}`,
1008
- ].join("\n"));
985
+ console.log(`first files ${files.slice(0, 8).join(", ")}${files.length > 8 ? ", ..." : ""}`);
1009
986
  process.exit(0);
1010
987
  }
1011
988
  const keepSecret = (v) => { if (v && v.length >= 12 && !secrets.includes(v)) secrets.push(v); };
@@ -1153,7 +1130,7 @@ if (head) void sendHistory().catch((err) => { if (verbose) say(`commit history n
1153
1130
  if (!viaToken && process.env.CORTAD_NO_REGISTER !== "1") {
1154
1131
  const got = await registerAll().catch((err) => { if (verbose) say(`could not register with your coding agents: ${err?.message ?? err}`); return { clients: [], changed: [] }; });
1155
1132
  const and = (list) => new Intl.ListFormat("en", { type: "conjunction" }).format(list);
1156
- 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.`);
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(", ")}.`);
1157
1134
  }
1158
1135
 
1159
1136
  lock = await makeLock({ root, work });
@@ -1215,7 +1192,7 @@ async function appLife(firstStart) {
1215
1192
  } : null);
1216
1193
  become("up", { ...upAt(), app: launched ? child?.pid ?? null : null });
1217
1194
  say(`your app is answering on port ${app.port}${app.cmd ? `, started with: ${shownCommand(app.cmd, mask)}` : ""}`);
1218
- if (launched) say(`your app's output for this session is kept in ${bootLog}, with the values from your env files masked`);
1195
+ if (launched) say(`your app's output is in ${bootLog}`);
1219
1196
  if (proxy) say(`send requests to port ${proxy.port}: it passes them to your app and Cortad sees each one. Requests sent straight to port ${app.port} are not seen`);
1220
1197
  for (const line of connectedText(machineOf())) say(line);
1221
1198
  say(CONNECTED);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortad/connector",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
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"