@cortad/connector 0.6.0-rc.3 → 0.6.1
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/lib/history/contract.mjs
CHANGED
|
@@ -61,7 +61,11 @@ export function conversationOf(raw, count) {
|
|
|
61
61
|
|
|
62
62
|
// The conversations to send, newest first, whole, from the window, until the plan's messages; and
|
|
63
63
|
// why each one left out was left out.
|
|
64
|
-
|
|
64
|
+
// `key`: the app's ingest key. Ids and users are hashed with it exactly as the SDK hashes them, so a
|
|
65
|
+
// person brought in from history is the same person when they write to the live app, and a guessable
|
|
66
|
+
// id (a first name, an email) cannot be read back from its hash without the key.
|
|
67
|
+
export function selected(raws, { now = Date.now(), secrets = [], key }) {
|
|
68
|
+
const subject = (kind, value) => sha256(`${key}:${kind}:${value}`);
|
|
65
69
|
const left = new Map();
|
|
66
70
|
const count = (why) => { left.set(why, (left.get(why) ?? 0) + 1); return null; };
|
|
67
71
|
const all = raws.map((raw) => conversationOf(raw, count)).filter(Boolean);
|
|
@@ -77,8 +81,8 @@ export function selected(raws, { now = Date.now(), secrets = [] } = {}) {
|
|
|
77
81
|
if (replies > room) { count("past 2,500 messages"); continue; }
|
|
78
82
|
room -= replies;
|
|
79
83
|
out.push({
|
|
80
|
-
id:
|
|
81
|
-
...(c.user ? { user:
|
|
84
|
+
id: subject("session", c.id),
|
|
85
|
+
...(c.user ? { user: subject("user", c.user) } : {}),
|
|
82
86
|
messages: c.messages.map((m) => {
|
|
83
87
|
const clean = scrub(m.text, secrets);
|
|
84
88
|
scrubbed += clean.hits;
|
|
@@ -53,10 +53,10 @@ export function historyQuestion({ spec, sources, stores }) {
|
|
|
53
53
|
"Step 1 of 2: your users' conversations",
|
|
54
54
|
...sources.map((s) => `Found ${s.name} (${s.names.join(" and ")} in your env).`),
|
|
55
55
|
...(store ? [`Found where this app keeps its conversations: ${storeName(store)}.`] : []),
|
|
56
|
-
`Ask the person, with your question tool: "Send our
|
|
56
|
+
`Ask the person, with your question tool: "Send our newest whole conversations from the last 30 days to Cortad, until our app's replies reach 2,500, so it can show us our users?"`,
|
|
57
57
|
...options.map((o, i) => ` ${i + 1}. ${o}`),
|
|
58
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.`),
|
|
59
|
-
"
|
|
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
60
|
"",
|
|
61
61
|
...exportContract(spec),
|
|
62
62
|
];
|
|
@@ -65,7 +65,8 @@ const storeName = (s) => `${s.where ? `the ${s.where} ${s.kind === "postgres" ?
|
|
|
65
65
|
|
|
66
66
|
export const exportContract = (spec) => [
|
|
67
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.",
|
|
68
|
-
"Read
|
|
68
|
+
"Read the newest conversations from the last 30 days, at most 2,500 of them: pick their ids first, then their messages. Cortad keeps them whole, newest first, until your app's replies reach 2,500. 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>"}}`,
|
|
69
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"}]}`,
|
|
70
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.`,
|
|
71
72
|
];
|
package/lib/intelligence.mjs
CHANGED
|
@@ -67,7 +67,7 @@ export async function intelligence(args, ctx) {
|
|
|
67
67
|
}
|
|
68
68
|
if (connection.started) out.push(say.live(connection));
|
|
69
69
|
else if (connection.here) out.push(say.workingHere(connection.here.at));
|
|
70
|
-
if (!connection.started) out.push(...codeStepFor(keys));
|
|
70
|
+
if (!connection.started) out.push(...codeStepFor({ ...keys, site: keys.site ?? connection.site?.origin ?? null }));
|
|
71
71
|
const progress = await (wait ? waited(call) : call("/mcp/history"));
|
|
72
72
|
if (progress.ok) out.push("", ...say.findings({ ...progress.data, done: progress.data.history.read >= progress.data.history.conversations }));
|
|
73
73
|
if (progress.ok && progress.data.history.read < progress.data.history.conversations && !wait) out.push(say.next(spec, "--wait"));
|
|
@@ -135,7 +135,9 @@ export async function intelligence(args, ctx) {
|
|
|
135
135
|
return { text: say.lines("Name where the conversations come from: --from <tool>, --stdin for your own export, or --skip."), code: 1 };
|
|
136
136
|
}
|
|
137
137
|
const label = SOURCES.find((s) => s.id === source)?.name ?? "your export";
|
|
138
|
-
const
|
|
138
|
+
const key = ingestKeyHere();
|
|
139
|
+
if (!key) return { text: say.lines(`This folder has no CORTAD_INGEST_KEY yet. Run npx -y ${spec} intelligence first.`), code: 1 };
|
|
140
|
+
const picked = selected(raws, { key, secrets: secretValues(ENV_FILES.map((f) => join(root, f)).filter(existsSync)).filter(credentialShaped) });
|
|
139
141
|
if (picked.conversations.length === 0) return { text: say.lines(say.historyNothing(label)), code: 1 };
|
|
140
142
|
const sends = packed(source, picked.conversations);
|
|
141
143
|
const file = join(homeOf(ctx.project), "history.json");
|
|
@@ -150,10 +152,16 @@ export async function intelligence(args, ctx) {
|
|
|
150
152
|
return { text: say.lines(say.historySent({ result, picked, span, source: label, file }), say.next(spec)) };
|
|
151
153
|
}
|
|
152
154
|
|
|
153
|
-
|
|
155
|
+
// The ingest key as this folder holds it: in its env file, or in keys.env when a manager holds its settings.
|
|
156
|
+
function ingestKeyHere() {
|
|
154
157
|
const file = settingsOf(root).file ?? join(homeOf(ctx.project), "keys.env");
|
|
155
158
|
const path = file.startsWith("/") ? file : join(root, file);
|
|
156
|
-
|
|
159
|
+
return parseEnv(existsSync(path) ? readFileSync(path, "utf8") : "")["CORTAD_INGEST_KEY"] ?? null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function key(ctx_, host) {
|
|
163
|
+
const file = settingsOf(root).file ?? join(homeOf(ctx.project), "keys.env");
|
|
164
|
+
const value = ingestKeyHere();
|
|
157
165
|
if (!value) return { text: say.lines(`There is no CORTAD_INGEST_KEY in ${file} yet. Run npx -y ${spec} intelligence first.`), code: 1 };
|
|
158
166
|
if (!host || !HOSTS[host]) return { text: say.lines(`Name the host: --to ${Object.keys(HOSTS).filter((h) => h !== "other").join(", ")}.`), code: 1 };
|
|
159
167
|
if (!canSet(root, host)) return { text: say.lines(say.keyNotSet(host, `its command is not installed, signed in or linked to this folder`, `${file} (line ${lineOf(root, file, "CORTAD_INGEST_KEY")})`)), code: 1 };
|
package/package.json
CHANGED