@profullstack/qryptchat 0.6.0 → 0.6.2
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-menzxtw0.js → chunk-g9xn5036.js} +28 -1
- package/dist/{chunk-074nrnc8.js → chunk-s0qccr26.js} +80 -19
- package/dist/commands.js +4 -2
- package/dist/mcp.js +2 -2
- package/dist/tui.js +7 -2
- 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
|
);
|
|
@@ -187,4 +187,31 @@ function clearSession(env = process.env) {
|
|
|
187
187
|
rmSync(file);
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
|
|
190
|
+
// src/cli/notify.js
|
|
191
|
+
var ESC = "\x1B";
|
|
192
|
+
var BEL = "\x07";
|
|
193
|
+
var ST = `${ESC}\\`;
|
|
194
|
+
var clean = (s) => String(s ?? "").replace(/[\x00-\x1f\x7f;]/g, " ").trim();
|
|
195
|
+
function notifySequence(title, body, env = process.env) {
|
|
196
|
+
const mode = String(env.QC_NOTIFY ?? "1").toLowerCase();
|
|
197
|
+
if (["0", "off", "false", "no"].includes(mode))
|
|
198
|
+
return "";
|
|
199
|
+
if (mode === "bell")
|
|
200
|
+
return BEL;
|
|
201
|
+
const t = clean(title);
|
|
202
|
+
const b = clean(body);
|
|
203
|
+
const kitty = env.KITTY_WINDOW_ID || env.TERM === "xterm-kitty";
|
|
204
|
+
let osc = kitty ? `${ESC}]99;i=qc:d=0:o=unfocused;${t}${ST}${ESC}]99;i=qc:d=1:p=body;${b}${ST}` : `${ESC}]9;${t}: ${b}${BEL}`;
|
|
205
|
+
if (env.TMUX)
|
|
206
|
+
osc = `${ESC}Ptmux;${osc.replaceAll(ESC, ESC + ESC)}${ST}`;
|
|
207
|
+
return osc + BEL;
|
|
208
|
+
}
|
|
209
|
+
function notify(title, body, { env = process.env, streams = [process.stdout, process.stderr] } = {}) {
|
|
210
|
+
const seq = notifySequence(title, body, env);
|
|
211
|
+
const tty = streams.find((s) => s?.isTTY);
|
|
212
|
+
if (seq && tty)
|
|
213
|
+
tty.write(seq);
|
|
214
|
+
return Boolean(seq && tty);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export { DEFAULT_URL, configDir, sessionPath, baseUrl, loadSession, unlockSession, storeSession, clearSession, notify };
|
|
@@ -5,8 +5,9 @@ import {
|
|
|
5
5
|
loadSession,
|
|
6
6
|
unlockSession,
|
|
7
7
|
storeSession,
|
|
8
|
-
clearSession
|
|
9
|
-
|
|
8
|
+
clearSession,
|
|
9
|
+
notify
|
|
10
|
+
} from "./chunk-g9xn5036.js";
|
|
10
11
|
|
|
11
12
|
// src/cli/crypto.js
|
|
12
13
|
import { MlKem1024 as MlKem10242 } from "mlkem";
|
|
@@ -1135,11 +1136,34 @@ async function joinAsAgent(invite, { name, username, env = process.env, fetchImp
|
|
|
1135
1136
|
return { session, conversationId: body.conversation_id, operator: body.operator };
|
|
1136
1137
|
}
|
|
1137
1138
|
|
|
1139
|
+
// src/cli/style.js
|
|
1140
|
+
import { emojify, icon } from "@profullstack/hqtui";
|
|
1141
|
+
var mark = (name) => {
|
|
1142
|
+
const glyph = icon(name);
|
|
1143
|
+
return glyph ? `${glyph} ` : "";
|
|
1144
|
+
};
|
|
1145
|
+
var em = (text) => emojify(String(text ?? ""), { mode: "emoji" });
|
|
1146
|
+
var ok = (text) => `${mark("check-circle")}${em(text)}`;
|
|
1147
|
+
var info = (text) => `${mark("info")}${em(text)}`;
|
|
1148
|
+
var warn = (text) => `${mark("warning")}${em(text)}`;
|
|
1149
|
+
var fail = (text) => `${mark("error")}${em(text)}`;
|
|
1150
|
+
function who(user, { agent = false } = {}) {
|
|
1151
|
+
if (!user)
|
|
1152
|
+
return "";
|
|
1153
|
+
const face = user.emoji ? `${user.emoji} ` : agent || user.account_type === "agent" ? "\uD83E\uDD16 " : mark("user");
|
|
1154
|
+
const name = user.display_name || user.displayName || user.username || "";
|
|
1155
|
+
const handle = user.username && name !== user.username ? ` @${user.username}` : "";
|
|
1156
|
+
const pronouns = user.pronouns ? ` (${user.pronouns})` : "";
|
|
1157
|
+
return `${face}${name}${handle}${pronouns}`;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1138
1160
|
// src/cli/commands.js
|
|
1161
|
+
var errorLine2 = (err) => `qc: ${fail(err?.message ?? String(err))}`;
|
|
1139
1162
|
var HELP2 = `qc: qrypt.chat in your terminal (end-to-end encrypted, ML-KEM-1024)
|
|
1140
1163
|
|
|
1141
1164
|
Usage:
|
|
1142
1165
|
qc open the chat client (signs you in first if needed)
|
|
1166
|
+
qc fonts install|status|remove our OpenEmoji colour font as the terminal's emoji font
|
|
1143
1167
|
qc agent join <link> join as an AI agent from an invite (makes its own keys) [--name --username]
|
|
1144
1168
|
qc login [--oob] sign in through your browser; --oob to paste a code (SSH)
|
|
1145
1169
|
qc logout forget this terminal's session and keys
|
|
@@ -1151,7 +1175,7 @@ Usage:
|
|
|
1151
1175
|
qc read <chat> [-n 20] print the last messages of a chat
|
|
1152
1176
|
qc send <chat> <text> send a message (text "-" reads stdin; --reply <message-id> to reply)
|
|
1153
1177
|
qc react <chat> <message-id> <emoji> [--remove] react to a message
|
|
1154
|
-
qc listen print new messages as they arrive (NDJSON with --json)
|
|
1178
|
+
qc listen print new messages as they arrive (NDJSON with --json; --notify alerts with it)
|
|
1155
1179
|
qc mcp run as an MCP server on stdio (list_chats, read_chat, send_message)
|
|
1156
1180
|
|
|
1157
1181
|
<chat> is a chat id or part of its name. Options: --json, --url <server> (or QC_URL).
|
|
@@ -1160,7 +1184,9 @@ OS keychain, or comes from a passphrase (QC_PASSPHRASE for scripts and qc mcp).
|
|
|
1160
1184
|
HD: emoji and icons are drawn as our OpenEmoji/OpenIcon images in Kitty, Ghostty, WezTerm and iTerm2.
|
|
1161
1185
|
Over SSH or in tmux (set -g allow-passthrough on) say which: QC_HD=1 (Kitty/Ghostty) or
|
|
1162
1186
|
QC_HD=wezterm (WezTerm/iTerm2). QC_HD=0 turns it off. Mosh carries only text, so behind mosh
|
|
1163
|
-
emoji stay characters: for images use ssh or WezTerm's multiplexer (wezterm connect)
|
|
1187
|
+
emoji stay characters: for images use ssh or WezTerm's multiplexer (wezterm connect).
|
|
1188
|
+
New messages raise a terminal notification (OSC 9, Kitty OSC 99) and a bell, naming the chat
|
|
1189
|
+
but never the text; in tmux set -g allow-passthrough on. QC_NOTIFY=bell rings only, QC_NOTIFY=0 is quiet.`;
|
|
1164
1190
|
function parseArgs2(argv) {
|
|
1165
1191
|
const args = { _: [], flags: {} };
|
|
1166
1192
|
for (let i = 0;i < argv.length; i++) {
|
|
@@ -1251,15 +1277,46 @@ async function main2(argv, { version = "0.0.0" } = {}) {
|
|
|
1251
1277
|
});
|
|
1252
1278
|
if (flags.json)
|
|
1253
1279
|
return json({ user: session.user, conversation_id: conversationId, operator });
|
|
1254
|
-
out(`Joined qrypt.chat as
|
|
1255
|
-
out(
|
|
1280
|
+
out(ok(`Joined qrypt.chat as ${who(session.user, { agent: true })}, operated by ${who(operator)}.`));
|
|
1281
|
+
out(`${mark("key")}Keys made here and sealed in ${sessionPath()}; only the public key left this machine.`);
|
|
1256
1282
|
const chatName = operator?.display_name || operator?.username || "<chat>";
|
|
1257
|
-
out(
|
|
1283
|
+
out(`${mark("chat")}Your chat with ${chatName} is open. Try: qc listen or qc send "${chatName}" "hello" or qc mcp`);
|
|
1258
1284
|
return;
|
|
1259
1285
|
}
|
|
1286
|
+
case "fonts": {
|
|
1287
|
+
const { installEmojiFont, emojiFontStatus, removeEmojiFont } = await import("@profullstack/hqtui");
|
|
1288
|
+
const action = rest[0] || "status";
|
|
1289
|
+
if (action === "install") {
|
|
1290
|
+
const result = await installEmojiFont();
|
|
1291
|
+
for (const note of result.notes)
|
|
1292
|
+
out(info(note));
|
|
1293
|
+
if (result.snippets?.length) {
|
|
1294
|
+
out(`
|
|
1295
|
+
${mark("info")}Terminals that pick their own fonts need one line each:`);
|
|
1296
|
+
for (const sn of result.snippets)
|
|
1297
|
+
out(` ${sn.terminal} (${sn.file}):
|
|
1298
|
+
${sn.snippet}`);
|
|
1299
|
+
}
|
|
1300
|
+
return out(`
|
|
1301
|
+
${ok("Installed OpenEmoji. Restart the terminal, then run: qc fonts status")}`);
|
|
1302
|
+
}
|
|
1303
|
+
if (action === "status") {
|
|
1304
|
+
const st = await emojiFontStatus();
|
|
1305
|
+
out(`${st.installed ? ok("OpenEmoji installed") : warn("OpenEmoji not installed (qc fonts install)")}`);
|
|
1306
|
+
if (st.emojiFont !== undefined)
|
|
1307
|
+
out(`${mark("info")}Emoji font in use: ${st.emojiFont || "none"}`);
|
|
1308
|
+
return out(st.active ? ok("Active: emoji draw as OpenEmoji") : warn("Not active yet (restart the terminal)"));
|
|
1309
|
+
}
|
|
1310
|
+
if (action === "remove") {
|
|
1311
|
+
const done = await removeEmojiFont();
|
|
1312
|
+
return out(done.length ? ok("Removed the OpenEmoji font setup.") : info("Nothing to remove."));
|
|
1313
|
+
}
|
|
1314
|
+
throw new Error("Usage: qc fonts install|status|remove");
|
|
1315
|
+
}
|
|
1260
1316
|
case "login": {
|
|
1261
1317
|
const { session } = await login({ oob: !!flags.oob });
|
|
1262
|
-
out(`Signed in as
|
|
1318
|
+
out(ok(`Signed in as ${who(session.user)} on ${session.base}.`));
|
|
1319
|
+
out(`${mark("lock")}Keys sealed (ChaCha20-Poly1305) in ${sessionPath()}.`);
|
|
1263
1320
|
if (await new QcClient(session).keyCheck() === "mismatch") {
|
|
1264
1321
|
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.");
|
|
1265
1322
|
}
|
|
@@ -1267,12 +1324,12 @@ async function main2(argv, { version = "0.0.0" } = {}) {
|
|
|
1267
1324
|
}
|
|
1268
1325
|
case "logout":
|
|
1269
1326
|
clearSession();
|
|
1270
|
-
return out("Signed out. This terminal no longer holds your keys.");
|
|
1327
|
+
return out(ok("Signed out. This terminal no longer holds your keys."));
|
|
1271
1328
|
case "whoami": {
|
|
1272
1329
|
const session = loadSession();
|
|
1273
1330
|
if (!session)
|
|
1274
1331
|
throw new Error("Not signed in. Run qc login.");
|
|
1275
|
-
return flags.json ? json({ user: session.user, base: session.base }) : out(
|
|
1332
|
+
return flags.json ? json({ user: session.user, base: session.base }) : out(`${who(session.user)} on ${session.base}`);
|
|
1276
1333
|
}
|
|
1277
1334
|
case "profile": {
|
|
1278
1335
|
const c = await client(flags);
|
|
@@ -1285,14 +1342,14 @@ async function main2(argv, { version = "0.0.0" } = {}) {
|
|
|
1285
1342
|
const p = Object.keys(fields).length ? await c.updateProfile(fields) : await c.profile();
|
|
1286
1343
|
if (flags.json)
|
|
1287
1344
|
return json({ username: p.username, emoji: p.emoji ?? null, pronouns: p.pronouns ?? null, website: p.website ?? null, bio: p.bio ?? null });
|
|
1288
|
-
out(
|
|
1345
|
+
out(who(p));
|
|
1289
1346
|
if (p.website)
|
|
1290
|
-
out(p.website);
|
|
1347
|
+
out(`${mark("link")}${p.website}`);
|
|
1291
1348
|
if (p.bio)
|
|
1292
1349
|
out(p.bio);
|
|
1293
1350
|
if (!Object.keys(fields).length)
|
|
1294
1351
|
out(`
|
|
1295
|
-
Set: qc profile --emoji
|
|
1352
|
+
${info("Set: qc profile --emoji :telescope: --pronouns she/her --website https://you.example (an empty value clears)")}`);
|
|
1296
1353
|
return;
|
|
1297
1354
|
}
|
|
1298
1355
|
case "chats":
|
|
@@ -1301,7 +1358,7 @@ Set: qc profile --emoji \uD83D\uDD2D --pronouns she/her --website https://you.ex
|
|
|
1301
1358
|
if (flags.json)
|
|
1302
1359
|
return json(chats.map((c) => ({ id: c.id, title: c.title, type: c.type, updated_at: c.updated_at, participants: c.participants?.length ?? 0 })));
|
|
1303
1360
|
for (const c of chats)
|
|
1304
|
-
out(`${c.id} ${c.title}`);
|
|
1361
|
+
out(`${c.id} ${mark(c.type === "group" ? "users" : "chat")}${c.title}`);
|
|
1305
1362
|
return;
|
|
1306
1363
|
}
|
|
1307
1364
|
case "read": {
|
|
@@ -1317,7 +1374,7 @@ Set: qc profile --emoji \uD83D\uDD2D --pronouns she/her --website https://you.ex
|
|
|
1317
1374
|
if (m.replyTo)
|
|
1318
1375
|
out(` ┃ ${m.replyTo.name ? `${m.replyTo.name}: ` : ""}${m.replyTo.snippet}`);
|
|
1319
1376
|
const reactions = m.reactions?.length ? ` [${m.reactions.map((r) => `${r.emoji}${r.count > 1 ? r.count : ""}`).join(" ")}]` : "";
|
|
1320
|
-
out(`[${stamp(m.at)}] ${m.mine ? "you" : m.sender}: ${m.text}${reactions} (${m.id.slice(0, 8)})`);
|
|
1377
|
+
out(`[${stamp(m.at)}] ${m.emoji ? `${m.emoji} ` : ""}${m.mine ? "you" : m.sender}: ${em(m.text)}${reactions} (${m.id.slice(0, 8)})`);
|
|
1321
1378
|
}
|
|
1322
1379
|
return;
|
|
1323
1380
|
}
|
|
@@ -1332,7 +1389,7 @@ Set: qc profile --emoji \uD83D\uDD2D --pronouns she/her --website https://you.ex
|
|
|
1332
1389
|
if (target.length !== 1)
|
|
1333
1390
|
throw new Error(target.length ? `"${messageId}" matches ${target.length} messages` : `No message "${messageId}" in ${chat.title}`);
|
|
1334
1391
|
await c.react(chat.id, target[0].id, emoji, !!flags.remove);
|
|
1335
|
-
return out(`${flags.remove ? "Removed" : "Reacted"} ${emoji} on ${chat.title}.`);
|
|
1392
|
+
return out(ok(`${flags.remove ? "Removed" : "Reacted"} ${emoji} on ${chat.title}.`));
|
|
1336
1393
|
}
|
|
1337
1394
|
case "send": {
|
|
1338
1395
|
if (!rest[0] || rest.length < 2)
|
|
@@ -1345,7 +1402,9 @@ Set: qc profile --emoji \uD83D\uDD2D --pronouns she/her --website https://you.ex
|
|
|
1345
1402
|
const { message, skipped } = await c.send(chat.id, text, { replyTo: flags.reply || undefined });
|
|
1346
1403
|
if (flags.json)
|
|
1347
1404
|
return json({ id: message?.id, conversation: chat.id, skipped });
|
|
1348
|
-
|
|
1405
|
+
if (skipped)
|
|
1406
|
+
out(warn(`${skipped} participant(s) have no key yet and will not see it.`));
|
|
1407
|
+
return out(`${mark("send")}Sent to ${chat.title}.`);
|
|
1349
1408
|
}
|
|
1350
1409
|
case "listen": {
|
|
1351
1410
|
const c = await client(flags);
|
|
@@ -1363,10 +1422,12 @@ Set: qc profile --emoji \uD83D\uDD2D --pronouns she/her --website https://you.ex
|
|
|
1363
1422
|
if (seen.has(m.id) || m.id !== data.message.id)
|
|
1364
1423
|
continue;
|
|
1365
1424
|
seen.add(m.id);
|
|
1425
|
+
if (!m.mine && (flags.notify || !flags.json))
|
|
1426
|
+
notify("qrypt.chat", `${m.sender} in ${titles.get(id) ?? "a chat"}`);
|
|
1366
1427
|
if (flags.json)
|
|
1367
1428
|
out(JSON.stringify({ chat: id, title: titles.get(id), ...m }));
|
|
1368
1429
|
else
|
|
1369
|
-
out(`[${stamp(m.at)}] ${titles.get(id) ?? id} · ${m.mine ? "you" : m.sender}: ${m.text}`);
|
|
1430
|
+
out(`[${stamp(m.at)}] ${mark("chat")}${titles.get(id) ?? id} · ${m.emoji ? `${m.emoji} ` : ""}${m.mine ? "you" : m.sender}: ${em(m.text)}`);
|
|
1370
1431
|
}
|
|
1371
1432
|
}, { onStatus: (s) => process.stderr.write(`qc: ${s}
|
|
1372
1433
|
`) });
|
|
@@ -1383,4 +1444,4 @@ ${HELP2}`);
|
|
|
1383
1444
|
}
|
|
1384
1445
|
}
|
|
1385
1446
|
|
|
1386
|
-
export { HELP2, parseArgs2, findChat2, main2 };
|
|
1447
|
+
export { errorLine2, HELP2, parseArgs2, findChat2, main2 };
|
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-s0qccr26.js";
|
|
8
|
+
import"./chunk-g9xn5036.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,7 @@
|
|
|
1
1
|
import {
|
|
2
|
-
configDir
|
|
3
|
-
|
|
2
|
+
configDir,
|
|
3
|
+
notify
|
|
4
|
+
} from "./chunk-g9xn5036.js";
|
|
4
5
|
|
|
5
6
|
// src/cli/tui.js
|
|
6
7
|
import { createApp, createImageStore, drawIcon, drawRichText, editText, emojify, insertText, stringWidth, truncate, widgets, wrap } from "@profullstack/hqtui";
|
|
@@ -396,6 +397,10 @@ async function runTui(client, { initialChat } = {}) {
|
|
|
396
397
|
const id = data?.message?.conversation_id;
|
|
397
398
|
if (!id)
|
|
398
399
|
return;
|
|
400
|
+
if (data.message.sender_id !== client.me?.id) {
|
|
401
|
+
const chat = state.conversations.find((c) => c.id === id);
|
|
402
|
+
notify("qrypt.chat", `new message${chat ? ` in ${chat.title}` : ""}`);
|
|
403
|
+
}
|
|
399
404
|
if (id === state.activeId)
|
|
400
405
|
loadMessages(id);
|
|
401
406
|
else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@profullstack/qryptchat",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
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": {
|