@cortad/connector 0.6.0-rc.2 → 0.6.0

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.
@@ -86,7 +86,7 @@ export function selected(raws, { now = Date.now(), secrets = [] } = {}) {
86
86
  }),
87
87
  });
88
88
  }
89
- return { conversations: out, messages: HISTORY_MESSAGES - room, scrubbed, left: Object.fromEntries(left), users: new Set(recent.map((c) => c.user).filter(Boolean)).size };
89
+ return { conversations: out, messages: HISTORY_MESSAGES - room, total: out.reduce((n, c) => n + c.messages.length, 0), scrubbed, left: Object.fromEntries(left), users: new Set(recent.map((c) => c.user).filter(Boolean)).size };
90
90
  }
91
91
 
92
92
  // The span of what will be sent, oldest message to newest.
@@ -14,7 +14,7 @@ export const header = (app) => [
14
14
  ];
15
15
 
16
16
  export const approveFirst = ({ spec, link, words, machine }) => lines(
17
- "This computer is not connected to Cortad yet.",
17
+ "This computer needs the person's approval to connect to Cortad.",
18
18
  `Give the person this link and the four characters, once: ${link} (characters ${words}, computer ${machine})`,
19
19
  "They approve it in a browser where they are signed in to Cortad. The page shows the same four characters and this computer's name; they approve only if both match.",
20
20
  `Once they have approved it, run npx -y ${spec} intelligence again. It waits up to a minute and a half for the approval.`,
@@ -22,6 +22,7 @@ export const approveFirst = ({ spec, link, words, machine }) => lines(
22
22
  export const pairExpired = (spec) => lines(`The approval link expired before it was approved. Run npx -y ${spec} intelligence again for a new one.`);
23
23
  export const paired = (app) => `This computer is now connected to Cortad for ${app}.`;
24
24
  export const gone = () => lines("This folder's app is no longer connected to Cortad. Connect it again from cortad.com/sim, then run this command again.");
25
+ export const keyRefused = "Cortad no longer accepts the key this computer kept for this folder, so it asks for a new approval.";
25
26
 
26
27
  export const keysWritten = (file, at) => [
27
28
  `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.`,
@@ -55,7 +56,7 @@ export function historyQuestion({ spec, sources, stores }) {
55
56
  `Ask the person, with your question tool: "Send our most recent conversations to Cortad, up to 2,500 messages from the last 30 days, so it can show us our users?"`,
56
57
  ...options.map((o, i) => ` ${i + 1}. ${o}`),
57
58
  ...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.`),
58
- "What is sent goes to cortad.com and is written first to ~/.cortad on this computer, so you can read exactly what was sent.",
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.",
59
60
  "",
60
61
  ...exportContract(spec),
61
62
  ];
@@ -64,7 +65,8 @@ const storeName = (s) => `${s.where ? `the ${s.where} ${s.kind === "postgres" ?
64
65
 
65
66
  export const exportContract = (spec) => [
66
67
  "For our database: write a one-off script in this folder that reads the conversations with this app's own database client, through the connection this project's env on this computer already has. Do not commit it. Open a read-only transaction and look for no other credentials.",
67
- "Read at most the newest 2,500 conversations from the last 30 days: pick their ids first, then their messages. Cortad keeps whole conversations, newest first, up to 2,500 messages. Print one JSON line per conversation:",
68
+ "Read at most the newest 2,500 conversations from the last 30 days: pick their ids first, then their messages. Cortad keeps whole conversations, newest first, up to 2,500 messages. Print first one line naming the database host it read, then one JSON line per conversation:",
69
+ ` {"source": {"kind": "postgres", "host": "<the database host from the connection>"}}`,
68
70
  ` {"id": "<the app's conversation id>", "user": "<the app's user id, if it has one>", "messages": [{"role": "user" or "assistant", "text": "...", "at": "2026-11-08T14:02:11Z"}]}`,
69
71
  `Times need a zone (Z or +03:00) or epoch milliseconds. Tell the person which database host it reads. Pipe it to npx -y ${spec} intelligence history --stdin, then delete the script.`,
70
72
  ];
@@ -74,7 +76,7 @@ export function historySent({ result, picked, span, source, file }) {
74
76
  const left = Object.entries(picked.left).filter(([why]) => !shortened(why)).map(([why, n]) => `${num(n)} ${why}`);
75
77
  const cut = Object.entries(picked.left).filter(([why]) => shortened(why)).map(([why, n]) => `${num(n)} ${why}`);
76
78
  return [
77
- `Sent ${num(result.taken)} conversations from ${source}, ${num(result.messages)} messages, ${day(span.from)} to ${day(span.to)}.${result.already ? ` ${num(result.already)} were already in Cortad.` : ""}`,
79
+ `Sent ${num(result.taken)} conversations from ${source}, ${day(span.from)} to ${day(span.to)}: ${num(picked.total)} messages, ${num(result.messages)} of them your app's replies, which is what Cortad counts.${result.already ? ` ${num(result.already)} were already in Cortad.` : ""}`,
78
80
  ...(picked.scrubbed ? [`${num(picked.scrubbed)} keys, card numbers, emails or phone numbers were replaced on this computer before sending.`] : []),
79
81
  ...(left.length ? [`Left out: ${left.join(", ")}.`] : []),
80
82
  ...(cut.length ? [`Shortened: ${cut.join(", ")}.`] : []),
@@ -83,7 +85,7 @@ export function historySent({ result, picked, span, source, file }) {
83
85
  ];
84
86
  }
85
87
  export const historyDry = ({ picked, span, file }) => [
86
- `Read ${num(picked.conversations.length)} conversations, ${num(picked.messages)} messages, ${day(span.from)} to ${day(span.to)}. Nothing was sent.`,
88
+ `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
89
  `What would be sent is in ${file}.`,
88
90
  ];
89
91
  export const historyLooksLikeDevelopment = ({ why, spec, rest }) => [
@@ -125,7 +127,7 @@ export function codeStep({ spec, stack, page, file, site, consent, host, settabl
125
127
  ...secondQuestion({ spec, host, settable, file, site }),
126
128
  ];
127
129
  }
128
- export const SDK = "0.3.2";
130
+ export const SDK = "0.3.3";
129
131
 
130
132
  function secondQuestion({ spec, host, settable, file, site }) {
131
133
  const h = HOSTS[host] ?? HOSTS.other;
@@ -29,9 +29,13 @@ export async function intelligence(args, ctx) {
29
29
  const { root, spec, token } = ctx;
30
30
  const api = (path, init = {}) => request(ctx, path, init);
31
31
  const opening = [];
32
- if (!token()) {
32
+ // A key Cortad no longer takes (revoked, idle a month, or its account gone) is the same as none: the
33
+ // person approves this computer again and the new key replaces it.
34
+ const refusedKey = token() ? (await api("/mcp/intelligence/start")).status === 401 : false;
35
+ if (refusedKey) opening.push(say.keyRefused);
36
+ if (!token() || refusedKey) {
33
37
  const result = await pair(ctx, (path, body) => request({ ...ctx, token: () => "" }, path, { method: "POST", body }));
34
- if (result.waiting) return { text: say.approveFirst({ spec, ...result }) };
38
+ if (result.waiting) return { text: [say.lines(opening), say.approveFirst({ spec, ...result })].filter(Boolean).join("\n") };
35
39
  if (result.expired) return { text: say.pairExpired(spec) };
36
40
  if (result.failed) return { text: say.lines(result.failed), code: 1 };
37
41
  opening.push(say.paired(result.app));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortad/connector",
3
- "version": "0.6.0-rc.2",
3
+ "version": "0.6.0",
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"