@cortad/connector 0.6.4 → 0.6.5
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 +3 -14
- package/lib/history/send.mjs +1 -2
- package/lib/intelligence-text.mjs +3 -8
- package/lib/intelligence.mjs +2 -5
- package/lib/read-text.mjs +8 -15
- package/local.mjs +4 -27
- package/package.json +1 -1
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
|
-
|
|
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 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.
|
|
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,
|
|
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.
|
|
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
|
|
package/lib/history/send.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
// What was selected, sent to Cortad in sends under 1 MiB each
|
|
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
|
|
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
|
|
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}.`,
|
package/lib/intelligence.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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,
|
|
90
|
-
//
|
|
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
|
|
97
|
-
: `Cortad uses your app already running on port ${f.port} and leaves it running
|
|
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
|
-
|
|
100
|
-
|
|
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
|
|
105
|
-
|
|
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.
|
package/local.mjs
CHANGED
|
@@ -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:
|
|
983
|
-
//
|
|
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
|
-
|
|
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); };
|
|
@@ -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
|
|
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