@profullstack/qryptchat 0.5.1 → 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/bin/qc.js +2 -2
- package/dist/{chunk-byfvkvkr.js → chunk-0f0frsar.js} +131 -14
- package/dist/{chunk-vfahhydf.js → chunk-menzxtw0.js} +1 -1
- package/dist/commands.js +4 -2
- package/dist/mcp.js +2 -2
- package/dist/tui.js +1 -1
- package/package.json +1 -1
package/bin/qc.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// qc: qrypt.chat in the terminal. `qc` opens the chat client; `qc --help` lists the rest.
|
|
3
3
|
import { readFileSync } from 'node:fs';
|
|
4
|
-
import { main } from '../dist/commands.js';
|
|
4
|
+
import { errorLine, main } from '../dist/commands.js';
|
|
5
5
|
|
|
6
6
|
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
7
7
|
|
|
8
8
|
main(process.argv.slice(2), { version: pkg.version }).then(
|
|
9
9
|
() => process.exit(0),
|
|
10
10
|
(err) => {
|
|
11
|
-
process.stderr.write(
|
|
11
|
+
process.stderr.write(`${errorLine(err)}\n`);
|
|
12
12
|
process.exit(1);
|
|
13
13
|
},
|
|
14
14
|
);
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DEFAULT_URL,
|
|
2
3
|
sessionPath,
|
|
3
4
|
baseUrl,
|
|
4
5
|
loadSession,
|
|
5
6
|
unlockSession,
|
|
6
7
|
storeSession,
|
|
7
8
|
clearSession
|
|
8
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-menzxtw0.js";
|
|
9
10
|
|
|
10
11
|
// src/cli/crypto.js
|
|
11
12
|
import { MlKem1024 as MlKem10242 } from "mlkem";
|
|
@@ -1090,11 +1091,79 @@ async function login(options = {}) {
|
|
|
1090
1091
|
return { session, save };
|
|
1091
1092
|
}
|
|
1092
1093
|
|
|
1094
|
+
// src/cli/agent.js
|
|
1095
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
1096
|
+
function parseInvite(input) {
|
|
1097
|
+
const text = String(input ?? "").trim();
|
|
1098
|
+
let base = DEFAULT_URL;
|
|
1099
|
+
let token = text;
|
|
1100
|
+
if (/^https?:\/\//i.test(text)) {
|
|
1101
|
+
const url = new URL(text);
|
|
1102
|
+
base = url.origin;
|
|
1103
|
+
token = url.hash.replace(/^#/, "") || url.searchParams.get("token") || "";
|
|
1104
|
+
}
|
|
1105
|
+
if (!/^[A-Za-z0-9_-]{40,64}$/.test(token))
|
|
1106
|
+
throw new Error("That is not an agent invite link (https://qrypt.chat/agents/join#...).");
|
|
1107
|
+
return { base, token };
|
|
1108
|
+
}
|
|
1109
|
+
function usernameFor(name) {
|
|
1110
|
+
const stem = String(name || "agent").toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 20) || "agent";
|
|
1111
|
+
return `${stem.length >= 3 ? stem : `${stem}_agent`}_${randomBytes2(3).toString("hex")}`.slice(0, 30);
|
|
1112
|
+
}
|
|
1113
|
+
async function joinAsAgent(invite, { name, username, env = process.env, fetchImpl = fetch, vault } = {}) {
|
|
1114
|
+
const { base, token } = parseInvite(invite);
|
|
1115
|
+
const keys1024 = await ephemeralKeypair();
|
|
1116
|
+
const res = await fetchImpl(`${base}/api/agents/redeem`, {
|
|
1117
|
+
method: "POST",
|
|
1118
|
+
headers: { "Content-Type": "application/json" },
|
|
1119
|
+
body: JSON.stringify({ token, username: username || usernameFor(name), displayName: name, publicKey: keys1024.publicKey })
|
|
1120
|
+
});
|
|
1121
|
+
const body = await res.json().catch(() => ({}));
|
|
1122
|
+
if (!res.ok)
|
|
1123
|
+
throw new Error(body.error || `Could not join (HTTP ${res.status})`);
|
|
1124
|
+
const session = {
|
|
1125
|
+
base,
|
|
1126
|
+
access_token: body.access_token,
|
|
1127
|
+
refresh_token: body.refresh_token,
|
|
1128
|
+
expires_at: body.expires_at,
|
|
1129
|
+
user: body.user,
|
|
1130
|
+
keys: { keys1024 },
|
|
1131
|
+
agent: true,
|
|
1132
|
+
created_at: new Date().toISOString()
|
|
1133
|
+
};
|
|
1134
|
+
await storeSession(session, env, vault);
|
|
1135
|
+
return { session, conversationId: body.conversation_id, operator: body.operator };
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// src/cli/style.js
|
|
1139
|
+
import { emojify, icon } from "@profullstack/hqtui";
|
|
1140
|
+
var mark = (name) => {
|
|
1141
|
+
const glyph = icon(name);
|
|
1142
|
+
return glyph ? `${glyph} ` : "";
|
|
1143
|
+
};
|
|
1144
|
+
var em = (text) => emojify(String(text ?? ""), { mode: "emoji" });
|
|
1145
|
+
var ok = (text) => `${mark("check-circle")}${em(text)}`;
|
|
1146
|
+
var info = (text) => `${mark("info")}${em(text)}`;
|
|
1147
|
+
var warn = (text) => `${mark("warning")}${em(text)}`;
|
|
1148
|
+
var fail = (text) => `${mark("error")}${em(text)}`;
|
|
1149
|
+
function who(user, { agent = false } = {}) {
|
|
1150
|
+
if (!user)
|
|
1151
|
+
return "";
|
|
1152
|
+
const face = user.emoji ? `${user.emoji} ` : agent || user.account_type === "agent" ? "\uD83E\uDD16 " : mark("user");
|
|
1153
|
+
const name = user.display_name || user.displayName || user.username || "";
|
|
1154
|
+
const handle = user.username && name !== user.username ? ` @${user.username}` : "";
|
|
1155
|
+
const pronouns = user.pronouns ? ` (${user.pronouns})` : "";
|
|
1156
|
+
return `${face}${name}${handle}${pronouns}`;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1093
1159
|
// src/cli/commands.js
|
|
1160
|
+
var errorLine2 = (err) => `qc: ${fail(err?.message ?? String(err))}`;
|
|
1094
1161
|
var HELP2 = `qc: qrypt.chat in your terminal (end-to-end encrypted, ML-KEM-1024)
|
|
1095
1162
|
|
|
1096
1163
|
Usage:
|
|
1097
1164
|
qc open the chat client (signs you in first if needed)
|
|
1165
|
+
qc fonts install|status|remove our OpenEmoji colour font as the terminal's emoji font
|
|
1166
|
+
qc agent join <link> join as an AI agent from an invite (makes its own keys) [--name --username]
|
|
1098
1167
|
qc login [--oob] sign in through your browser; --oob to paste a code (SSH)
|
|
1099
1168
|
qc logout forget this terminal's session and keys
|
|
1100
1169
|
qc whoami who this terminal is signed in as
|
|
@@ -1127,7 +1196,7 @@ function parseArgs2(argv) {
|
|
|
1127
1196
|
const [k, v] = a.slice(2).split("=", 2);
|
|
1128
1197
|
if (v !== undefined)
|
|
1129
1198
|
args.flags[k] = v;
|
|
1130
|
-
else if (["url", "n", "limit", "reply", "emoji", "pronouns", "website", "bio"].includes(k) && argv[i + 1] !== undefined && !argv[i + 1].startsWith("--"))
|
|
1199
|
+
else if (["url", "n", "limit", "reply", "emoji", "pronouns", "website", "bio", "name", "username"].includes(k) && argv[i + 1] !== undefined && !argv[i + 1].startsWith("--"))
|
|
1131
1200
|
args.flags[k] = argv[++i];
|
|
1132
1201
|
else
|
|
1133
1202
|
args.flags[k] = true;
|
|
@@ -1196,9 +1265,55 @@ async function main2(argv, { version = "0.0.0" } = {}) {
|
|
|
1196
1265
|
const { runTui } = await import("./tui.js");
|
|
1197
1266
|
return runTui(c, { initialChat: rest[0] });
|
|
1198
1267
|
}
|
|
1268
|
+
case "agent": {
|
|
1269
|
+
if (rest[0] !== "join" || !rest[1])
|
|
1270
|
+
throw new Error("Usage: qc agent join <invite link> [--name NAME] [--username NAME]");
|
|
1271
|
+
const { session, conversationId, operator } = await joinAsAgent(rest[1], {
|
|
1272
|
+
name: typeof flags.name === "string" ? flags.name : undefined,
|
|
1273
|
+
username: typeof flags.username === "string" ? flags.username : undefined
|
|
1274
|
+
});
|
|
1275
|
+
if (flags.json)
|
|
1276
|
+
return json({ user: session.user, conversation_id: conversationId, operator });
|
|
1277
|
+
out(ok(`Joined qrypt.chat as ${who(session.user, { agent: true })}, operated by ${who(operator)}.`));
|
|
1278
|
+
out(`${mark("key")}Keys made here and sealed in ${sessionPath()}; only the public key left this machine.`);
|
|
1279
|
+
const chatName = operator?.display_name || operator?.username || "<chat>";
|
|
1280
|
+
out(`${mark("chat")}Your chat with ${chatName} is open. Try: qc listen or qc send "${chatName}" "hello" or qc mcp`);
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
case "fonts": {
|
|
1284
|
+
const { installEmojiFont, emojiFontStatus, removeEmojiFont } = await import("@profullstack/hqtui");
|
|
1285
|
+
const action = rest[0] || "status";
|
|
1286
|
+
if (action === "install") {
|
|
1287
|
+
const result = await installEmojiFont();
|
|
1288
|
+
for (const note of result.notes)
|
|
1289
|
+
out(info(note));
|
|
1290
|
+
if (result.snippets?.length) {
|
|
1291
|
+
out(`
|
|
1292
|
+
${mark("info")}Terminals that pick their own fonts need one line each:`);
|
|
1293
|
+
for (const sn of result.snippets)
|
|
1294
|
+
out(` ${sn.terminal} (${sn.file}):
|
|
1295
|
+
${sn.snippet}`);
|
|
1296
|
+
}
|
|
1297
|
+
return out(`
|
|
1298
|
+
${ok("Installed OpenEmoji. Restart the terminal, then run: qc fonts status")}`);
|
|
1299
|
+
}
|
|
1300
|
+
if (action === "status") {
|
|
1301
|
+
const st = await emojiFontStatus();
|
|
1302
|
+
out(`${st.installed ? ok("OpenEmoji installed") : warn("OpenEmoji not installed (qc fonts install)")}`);
|
|
1303
|
+
if (st.emojiFont !== undefined)
|
|
1304
|
+
out(`${mark("info")}Emoji font in use: ${st.emojiFont || "none"}`);
|
|
1305
|
+
return out(st.active ? ok("Active: emoji draw as OpenEmoji") : warn("Not active yet (restart the terminal)"));
|
|
1306
|
+
}
|
|
1307
|
+
if (action === "remove") {
|
|
1308
|
+
const done = await removeEmojiFont();
|
|
1309
|
+
return out(done.length ? ok("Removed the OpenEmoji font setup.") : info("Nothing to remove."));
|
|
1310
|
+
}
|
|
1311
|
+
throw new Error("Usage: qc fonts install|status|remove");
|
|
1312
|
+
}
|
|
1199
1313
|
case "login": {
|
|
1200
1314
|
const { session } = await login({ oob: !!flags.oob });
|
|
1201
|
-
out(`Signed in as
|
|
1315
|
+
out(ok(`Signed in as ${who(session.user)} on ${session.base}.`));
|
|
1316
|
+
out(`${mark("lock")}Keys sealed (ChaCha20-Poly1305) in ${sessionPath()}.`);
|
|
1202
1317
|
if (await new QcClient(session).keyCheck() === "mismatch") {
|
|
1203
1318
|
out("Warning: the browser handed over keys that are not your account's current keys, so messages will not decrypt here. Restore your keys in that browser (Settings > Keys) or approve from the device you chat on, then run qc login again.");
|
|
1204
1319
|
}
|
|
@@ -1206,12 +1321,12 @@ async function main2(argv, { version = "0.0.0" } = {}) {
|
|
|
1206
1321
|
}
|
|
1207
1322
|
case "logout":
|
|
1208
1323
|
clearSession();
|
|
1209
|
-
return out("Signed out. This terminal no longer holds your keys.");
|
|
1324
|
+
return out(ok("Signed out. This terminal no longer holds your keys."));
|
|
1210
1325
|
case "whoami": {
|
|
1211
1326
|
const session = loadSession();
|
|
1212
1327
|
if (!session)
|
|
1213
1328
|
throw new Error("Not signed in. Run qc login.");
|
|
1214
|
-
return flags.json ? json({ user: session.user, base: session.base }) : out(
|
|
1329
|
+
return flags.json ? json({ user: session.user, base: session.base }) : out(`${who(session.user)} on ${session.base}`);
|
|
1215
1330
|
}
|
|
1216
1331
|
case "profile": {
|
|
1217
1332
|
const c = await client(flags);
|
|
@@ -1224,14 +1339,14 @@ async function main2(argv, { version = "0.0.0" } = {}) {
|
|
|
1224
1339
|
const p = Object.keys(fields).length ? await c.updateProfile(fields) : await c.profile();
|
|
1225
1340
|
if (flags.json)
|
|
1226
1341
|
return json({ username: p.username, emoji: p.emoji ?? null, pronouns: p.pronouns ?? null, website: p.website ?? null, bio: p.bio ?? null });
|
|
1227
|
-
out(
|
|
1342
|
+
out(who(p));
|
|
1228
1343
|
if (p.website)
|
|
1229
|
-
out(p.website);
|
|
1344
|
+
out(`${mark("link")}${p.website}`);
|
|
1230
1345
|
if (p.bio)
|
|
1231
1346
|
out(p.bio);
|
|
1232
1347
|
if (!Object.keys(fields).length)
|
|
1233
1348
|
out(`
|
|
1234
|
-
Set: qc profile --emoji
|
|
1349
|
+
${info("Set: qc profile --emoji :telescope: --pronouns she/her --website https://you.example (an empty value clears)")}`);
|
|
1235
1350
|
return;
|
|
1236
1351
|
}
|
|
1237
1352
|
case "chats":
|
|
@@ -1240,7 +1355,7 @@ Set: qc profile --emoji \uD83D\uDD2D --pronouns she/her --website https://you.ex
|
|
|
1240
1355
|
if (flags.json)
|
|
1241
1356
|
return json(chats.map((c) => ({ id: c.id, title: c.title, type: c.type, updated_at: c.updated_at, participants: c.participants?.length ?? 0 })));
|
|
1242
1357
|
for (const c of chats)
|
|
1243
|
-
out(`${c.id} ${c.title}`);
|
|
1358
|
+
out(`${c.id} ${mark(c.type === "group" ? "users" : "chat")}${c.title}`);
|
|
1244
1359
|
return;
|
|
1245
1360
|
}
|
|
1246
1361
|
case "read": {
|
|
@@ -1256,7 +1371,7 @@ Set: qc profile --emoji \uD83D\uDD2D --pronouns she/her --website https://you.ex
|
|
|
1256
1371
|
if (m.replyTo)
|
|
1257
1372
|
out(` ┃ ${m.replyTo.name ? `${m.replyTo.name}: ` : ""}${m.replyTo.snippet}`);
|
|
1258
1373
|
const reactions = m.reactions?.length ? ` [${m.reactions.map((r) => `${r.emoji}${r.count > 1 ? r.count : ""}`).join(" ")}]` : "";
|
|
1259
|
-
out(`[${stamp(m.at)}] ${m.mine ? "you" : m.sender}: ${m.text}${reactions} (${m.id.slice(0, 8)})`);
|
|
1374
|
+
out(`[${stamp(m.at)}] ${m.emoji ? `${m.emoji} ` : ""}${m.mine ? "you" : m.sender}: ${em(m.text)}${reactions} (${m.id.slice(0, 8)})`);
|
|
1260
1375
|
}
|
|
1261
1376
|
return;
|
|
1262
1377
|
}
|
|
@@ -1271,7 +1386,7 @@ Set: qc profile --emoji \uD83D\uDD2D --pronouns she/her --website https://you.ex
|
|
|
1271
1386
|
if (target.length !== 1)
|
|
1272
1387
|
throw new Error(target.length ? `"${messageId}" matches ${target.length} messages` : `No message "${messageId}" in ${chat.title}`);
|
|
1273
1388
|
await c.react(chat.id, target[0].id, emoji, !!flags.remove);
|
|
1274
|
-
return out(`${flags.remove ? "Removed" : "Reacted"} ${emoji} on ${chat.title}.`);
|
|
1389
|
+
return out(ok(`${flags.remove ? "Removed" : "Reacted"} ${emoji} on ${chat.title}.`));
|
|
1275
1390
|
}
|
|
1276
1391
|
case "send": {
|
|
1277
1392
|
if (!rest[0] || rest.length < 2)
|
|
@@ -1284,7 +1399,9 @@ Set: qc profile --emoji \uD83D\uDD2D --pronouns she/her --website https://you.ex
|
|
|
1284
1399
|
const { message, skipped } = await c.send(chat.id, text, { replyTo: flags.reply || undefined });
|
|
1285
1400
|
if (flags.json)
|
|
1286
1401
|
return json({ id: message?.id, conversation: chat.id, skipped });
|
|
1287
|
-
|
|
1402
|
+
if (skipped)
|
|
1403
|
+
out(warn(`${skipped} participant(s) have no key yet and will not see it.`));
|
|
1404
|
+
return out(`${mark("send")}Sent to ${chat.title}.`);
|
|
1288
1405
|
}
|
|
1289
1406
|
case "listen": {
|
|
1290
1407
|
const c = await client(flags);
|
|
@@ -1305,7 +1422,7 @@ Set: qc profile --emoji \uD83D\uDD2D --pronouns she/her --website https://you.ex
|
|
|
1305
1422
|
if (flags.json)
|
|
1306
1423
|
out(JSON.stringify({ chat: id, title: titles.get(id), ...m }));
|
|
1307
1424
|
else
|
|
1308
|
-
out(`[${stamp(m.at)}] ${titles.get(id) ?? id} · ${m.mine ? "you" : m.sender}: ${m.text}`);
|
|
1425
|
+
out(`[${stamp(m.at)}] ${mark("chat")}${titles.get(id) ?? id} · ${m.emoji ? `${m.emoji} ` : ""}${m.mine ? "you" : m.sender}: ${em(m.text)}`);
|
|
1309
1426
|
}
|
|
1310
1427
|
}, { onStatus: (s) => process.stderr.write(`qc: ${s}
|
|
1311
1428
|
`) });
|
|
@@ -1322,4 +1439,4 @@ ${HELP2}`);
|
|
|
1322
1439
|
}
|
|
1323
1440
|
}
|
|
1324
1441
|
|
|
1325
|
-
export { HELP2, parseArgs2, findChat2, main2 };
|
|
1442
|
+
export { errorLine2, HELP2, parseArgs2, findChat2, main2 };
|
|
@@ -187,4 +187,4 @@ function clearSession(env = process.env) {
|
|
|
187
187
|
rmSync(file);
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
export { configDir, sessionPath, baseUrl, loadSession, unlockSession, storeSession, clearSession };
|
|
190
|
+
export { DEFAULT_URL, configDir, sessionPath, baseUrl, loadSession, unlockSession, storeSession, clearSession };
|
package/dist/commands.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
|
+
errorLine2,
|
|
2
3
|
HELP2,
|
|
3
4
|
parseArgs2,
|
|
4
5
|
findChat2,
|
|
5
6
|
main2
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import"./chunk-
|
|
7
|
+
} from "./chunk-0f0frsar.js";
|
|
8
|
+
import"./chunk-menzxtw0.js";
|
|
8
9
|
export {
|
|
9
10
|
HELP2 as HELP,
|
|
11
|
+
errorLine2 as errorLine,
|
|
10
12
|
findChat2 as findChat,
|
|
11
13
|
main2 as main,
|
|
12
14
|
parseArgs2 as parseArgs
|
package/dist/mcp.js
CHANGED
package/dist/tui.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
configDir
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-menzxtw0.js";
|
|
4
4
|
|
|
5
5
|
// src/cli/tui.js
|
|
6
6
|
import { createApp, createImageStore, drawIcon, drawRichText, editText, emojify, insertText, stringWidth, truncate, widgets, wrap } from "@profullstack/hqtui";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@profullstack/qryptchat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "qc: qrypt.chat in your terminal. A full-screen end-to-end encrypted chat client (ML-KEM-1024), plus a scriptable CLI and an MCP server.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|