@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 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(`qc: ${err?.message ?? err}\n`);
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
- export { DEFAULT_URL, configDir, sessionPath, baseUrl, loadSession, unlockSession, storeSession, clearSession };
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
- } from "./chunk-menzxtw0.js";
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 @${session.user?.username} (agent), operated by @${operator?.username ?? "?"}.`);
1255
- out(`Keys made here and sealed in ${sessionPath()}; only the public key left this machine.`);
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(`Your chat with ${chatName} is open. Try: qc listen or qc send "${chatName}" "hello" or qc mcp`);
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 @${session.user?.username ?? "unknown"} on ${session.base}. Keys sealed (ChaCha20-Poly1305) in ${sessionPath()}.`);
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(`@${session.user?.username} (${session.user?.display_name ?? ""}) on ${session.base}`);
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(`${p.emoji ? `${p.emoji} ` : ""}${p.displayName ?? p.display_name ?? p.username} @${p.username}${p.pronouns ? ` (${p.pronouns})` : ""}`);
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 \uD83D\uDD2D --pronouns she/her --website https://you.example (an empty value clears)`);
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
- return out(`Sent to ${chat.title}${skipped ? ` (${skipped} participant(s) have no key yet)` : ""}.`);
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-074nrnc8.js";
7
- import"./chunk-menzxtw0.js";
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
@@ -1,7 +1,7 @@
1
- import"./chunk-menzxtw0.js";
1
+ import"./chunk-g9xn5036.js";
2
2
  import {
3
3
  findChat2
4
- } from "./chunk-074nrnc8.js";
4
+ } from "./chunk-s0qccr26.js";
5
5
 
6
6
  // src/cli/mcp.js
7
7
  import { createInterface } from "node:readline";
package/dist/tui.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import {
2
- configDir
3
- } from "./chunk-menzxtw0.js";
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.0",
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": {