@profullstack/qryptchat 0.4.2 → 0.4.4

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.
@@ -992,6 +992,20 @@ class QcClient {
992
992
  throw new QcError(data.error_description || data.error || `HTTP ${res.status}`, res.status);
993
993
  return data;
994
994
  }
995
+ async keyCheck() {
996
+ try {
997
+ const me = this.me?.id;
998
+ const mine = this.session.keys?.keys1024?.publicKey;
999
+ if (!me || !mine)
1000
+ return "unknown";
1001
+ const { public_keys: keys = {} } = await this.request("/api/crypto/public-keys", { method: "POST", body: { user_ids: [me] } });
1002
+ if (!keys[me])
1003
+ return "unknown";
1004
+ return keys[me] === mine ? "ok" : "mismatch";
1005
+ } catch {
1006
+ return "unknown";
1007
+ }
1008
+ }
995
1009
  async conversations() {
996
1010
  const { conversations = [] } = await this.request("/api/conversations/load", { method: "POST", body: {} });
997
1011
  return conversations.map((c) => ({ ...c, title: conversationTitle(c, this.me) }));
@@ -1356,7 +1370,9 @@ Usage:
1356
1370
 
1357
1371
  <chat> is a chat id or part of its name. Options: --json, --url <server> (or QC_URL).
1358
1372
  Keys and tokens are sealed (ChaCha20-Poly1305) in ${"$"}QC_HOME or ~/.config/qc; the key is in your
1359
- OS keychain, or comes from a passphrase (QC_PASSPHRASE for scripts and qc mcp).`;
1373
+ OS keychain, or comes from a passphrase (QC_PASSPHRASE for scripts and qc mcp).
1374
+ HD: emoji and icons are drawn as our OpenEmoji/OpenIcon images in Kitty and Ghostty. Over SSH or in
1375
+ tmux (set -g allow-passthrough on), run with QC_HD=1; QC_HD=0 turns it off.`;
1360
1376
  function parseArgs2(argv) {
1361
1377
  const args = { _: [], flags: {} };
1362
1378
  for (let i = 0;i < argv.length; i++) {
@@ -1440,7 +1456,11 @@ async function main2(argv, { version = "0.0.0" } = {}) {
1440
1456
  }
1441
1457
  case "login": {
1442
1458
  const { session } = await login({ oob: !!flags.oob });
1443
- return out(`Signed in as @${session.user?.username ?? "unknown"} on ${session.base}. Keys sealed (ChaCha20-Poly1305) in ${sessionPath()}.`);
1459
+ out(`Signed in as @${session.user?.username ?? "unknown"} on ${session.base}. Keys sealed (ChaCha20-Poly1305) in ${sessionPath()}.`);
1460
+ if (await new QcClient(session).keyCheck() === "mismatch") {
1461
+ 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.");
1462
+ }
1463
+ return;
1444
1464
  }
1445
1465
  case "logout":
1446
1466
  clearSession();
package/dist/commands.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  parseArgs2,
4
4
  findChat2,
5
5
  main2
6
- } from "./chunk-7mn96363.js";
6
+ } from "./chunk-jdt6p3k7.js";
7
7
  import"./chunk-vfahhydf.js";
8
8
  export {
9
9
  HELP2 as HELP,
package/dist/mcp.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import"./chunk-vfahhydf.js";
2
2
  import {
3
3
  findChat2
4
- } from "./chunk-7mn96363.js";
4
+ } from "./chunk-jdt6p3k7.js";
5
5
 
6
6
  // src/cli/mcp.js
7
7
  import { createInterface } from "node:readline";
package/dist/tui.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-vfahhydf.js";
4
4
 
5
5
  // src/cli/tui.js
6
- import { createApp, editText, emojify, insertText, stringWidth, truncate, widgets, wrap } from "@profullstack/hqtui";
6
+ import { createApp, createImageStore, drawIcon, drawRichText, editText, emojify, insertText, stringWidth, truncate, widgets, wrap } from "@profullstack/hqtui";
7
7
  import { readFileSync, writeFileSync } from "node:fs";
8
8
  import { join } from "node:path";
9
9
  var SIDEBAR = 30;
@@ -99,9 +99,9 @@ function drawTranscript(surface, state) {
99
99
  if (line.spans) {
100
100
  let x = 0;
101
101
  for (const span of line.spans)
102
- x += surface.text(x, y, span.text, { fg: span.fg, attrs: span.bold ? 1 : 0 });
102
+ x += drawRichText(surface, x, y, span.text, { fg: span.fg, attrs: span.bold ? 1 : 0 }, state.images);
103
103
  } else {
104
- surface.text(0, y, truncate(line.text, surface.width), { fg: line.fg });
104
+ drawRichText(surface, 0, y, truncate(line.text, surface.width), { fg: line.fg }, state.images);
105
105
  }
106
106
  }
107
107
  if (state.scrollBack > 0) {
@@ -116,7 +116,10 @@ function render({ ui, width, height, theme }, state, on = {}) {
116
116
  bar.draw((s) => {
117
117
  const t = s.theme;
118
118
  s.fillRect(0, 0, s.width, 1, { bg: t.surface ?? t.background });
119
- let x = s.text(0, 0, " \uD83D\uDD12 qrypt.chat ", { fg: t.background, bg: t.primary, attrs: 1 });
119
+ const brand = { fg: t.background, bg: t.primary, attrs: 1 };
120
+ let x = s.text(0, 0, " ", brand);
121
+ x += drawIcon(s, x, 0, "lock", state.images, "\uD83D\uDD12", brand);
122
+ x += s.text(x, 0, " qrypt.chat ", brand);
120
123
  x += s.text(x, 0, ` @${state.me?.username ?? "?"}`, { fg: t.foreground, bg: t.surface ?? t.background });
121
124
  if (state.notice)
122
125
  s.text(x + 2, 0, truncate(state.notice, Math.max(0, s.width - x - 16)), { fg: t.warning, bg: t.surface ?? t.background });
@@ -203,16 +206,19 @@ function saveRecent(list) {
203
206
  writeFileSync(recentFile(), JSON.stringify(list), { mode: 384 });
204
207
  } catch {}
205
208
  }
209
+ var KEY_MISMATCH = "Keys don't match your account, so messages can't decrypt. Run qc login and approve from a browser that can read your chats.";
206
210
  async function runTui(client, { initialChat } = {}) {
207
211
  const state = initialState(client.me);
208
212
  state.recent = loadRecent();
209
213
  const abort = new AbortController;
210
214
  const app = await createApp({ quitKeys: ["ctrl+c"], focusNavigation: false });
211
215
  const redraw = () => app.invalidate();
212
- const note = (msg) => {
216
+ const env = process.env.QC_HD && !process.env.HQTUI_IMAGES ? { ...process.env, HQTUI_IMAGES: process.env.QC_HD } : process.env;
217
+ state.images = createImageStore({ write: (seq) => app.terminal.write(seq), onReady: redraw, env });
218
+ const note = (msg, { sticky = false } = {}) => {
213
219
  state.notice = msg;
214
220
  redraw();
215
- if (msg)
221
+ if (msg && !sticky)
216
222
  setTimeout(() => {
217
223
  if (state.notice === msg) {
218
224
  state.notice = "";
@@ -231,6 +237,7 @@ async function runTui(client, { initialChat } = {}) {
231
237
  note(`Could not load messages: ${err.message}`);
232
238
  }
233
239
  }
240
+ let keysChecked = false;
234
241
  async function loadConversations() {
235
242
  try {
236
243
  const before = state.activeId;
@@ -243,6 +250,11 @@ async function runTui(client, { initialChat } = {}) {
243
250
  state.selected = Math.max(0, state.conversations.findIndex((c) => c.id === before));
244
251
  }
245
252
  redraw();
253
+ if (!keysChecked) {
254
+ keysChecked = true;
255
+ if (await client.keyCheck() === "mismatch")
256
+ note(KEY_MISMATCH, { sticky: true });
257
+ }
246
258
  } catch (err) {
247
259
  state.loading = false;
248
260
  note(err.status === 401 ? "Session ended: run qc login" : `Could not load chats: ${err.message}`);
@@ -371,7 +383,10 @@ async function runTui(client, { initialChat } = {}) {
371
383
  state.field = insertText(state.field, event.text);
372
384
  redraw();
373
385
  });
374
- app.on("exit", () => abort.abort());
386
+ app.on("exit", () => {
387
+ state.images?.clear();
388
+ abort.abort();
389
+ });
375
390
  app.render((args) => render(args, state, handlers));
376
391
  loadConversations();
377
392
  client.events(({ type, data }) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@profullstack/qryptchat",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
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": {
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "dependencies": {
19
19
  "@noble/ciphers": "^2.0.0",
20
- "@profullstack/hqtui": "^0.8.0",
20
+ "@profullstack/hqtui": "^0.9.0",
21
21
  "mlkem": "^2.5.0"
22
22
  },
23
23
  "keywords": [