@profullstack/qryptchat 0.10.0 → 0.11.0

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/README.md CHANGED
@@ -35,7 +35,8 @@ The first run signs you in through your browser, then opens your chats.
35
35
  | Enter | send (or open the highlighted chat) |
36
36
  | Ctrl+E | the emoji picker: search, recents and every group, drawn with [OpenEmoji](https://github.com/profullstack/openemoji) |
37
37
  | `:rocket:` | shortcodes turn into emoji when you send |
38
- | Tab / Esc | move between the chat list and the composer |
38
+ | `@` `@ali` + Tab | complete a mention, like x.com and IRC: Tab again cycles the matches, Shift+Tab goes back; the line above the composer lists who matches |
39
+ | Tab / Esc | move between the chat list and the composer (Tab completes instead when the cursor is in an `@name`) |
39
40
  | Ctrl+N / Ctrl+P | next / previous chat |
40
41
  | PgUp / PgDn, wheel | scroll back through the conversation |
41
42
  | Ctrl+R | reload |
@@ -49,6 +50,8 @@ The mouse works too: click a chat, scroll the transcript, click an emoji. Unread
49
50
  qc chats # id and name of every chat
50
51
  qc read alice -n 20 # the last 20 messages, decrypted
51
52
  qc send alice "on my way" # encrypted to every participant
53
+ qc send ops "@ri deploy is done" # an @partial matching one person is sent as @riotcoder
54
+ qc mentions ops @al # who @al completes to: @alice, @alina
52
55
  echo "deploy done" | qc send ops -
53
56
  qc listen --json # new messages as NDJSON, until you stop it
54
57
  qc whoami
@@ -62,7 +65,7 @@ qc whoami
62
65
  qc mcp
63
66
  ```
64
67
 
65
- runs an MCP server on stdio with three tools: `list_chats`, `read_chat` and `send_message`. Encryption and decryption happen on this machine; the server only ever sees ciphertext. For Claude Code:
68
+ runs an MCP server on stdio with `list_chats`, `read_chat`, `send_message`, `list_mentions` (who an `@partial` completes to) and `react_to_message`; `send_message` writes out an `@partial` that matches exactly one person. Encryption and decryption happen on this machine; the server only ever sees ciphertext. For Claude Code:
66
69
 
67
70
  ```sh
68
71
  claude mcp add qryptchat -- qc mcp
@@ -264,4 +264,108 @@ function listLines(conversations, titleOf = (c) => c.name || "Chat") {
264
264
  return [`${live.length} ${live.length === 1 ? "chat" : "chats"}${archived ? ` (+${archived} archived)` : ""}`, ...lines];
265
265
  }
266
266
 
267
- export { DEFAULT_URL, configDir, sessionPath, baseUrl, loadSession, unlockSession, storeSession, clearSession, notify, PAIRUX_URL, newVoiceRoomLink, voiceMessage, parseSlashCommand, namesLines, listLines };
267
+ // src/lib/chat/mentions.js
268
+ var NAME_CHAR = /[\p{L}\p{N}_.-]/u;
269
+ var BEFORE_AT = /[\s([{"'“‘]/u;
270
+ function mentionCandidates(participants, selfId) {
271
+ const seen = new Set;
272
+ const out = [];
273
+ for (const p of participants || []) {
274
+ const u = p.user || p;
275
+ const id = p.user_id ?? u.id;
276
+ if (!u.username || selfId && id === selfId || seen.has(u.username.toLowerCase()))
277
+ continue;
278
+ seen.add(u.username.toLowerCase());
279
+ out.push({ id, username: u.username, name: u.display_name || "", agent: u.account_type === "agent" });
280
+ }
281
+ return out.sort((a, b) => a.username.localeCompare(b.username, undefined, { sensitivity: "base" }));
282
+ }
283
+ function mentionAt(value, cursor = value.length) {
284
+ let i = cursor;
285
+ while (i > 0 && NAME_CHAR.test(value[i - 1]))
286
+ i--;
287
+ if (value[i - 1] !== "@")
288
+ return null;
289
+ const start = i - 1;
290
+ if (start > 0 && !BEFORE_AT.test(value[start - 1]))
291
+ return null;
292
+ let end = cursor;
293
+ while (end < value.length && NAME_CHAR.test(value[end]))
294
+ end++;
295
+ return { start, end, query: value.slice(i, cursor) };
296
+ }
297
+ function matchMentions(candidates, query = "", { anywhere = true } = {}) {
298
+ const q = query.toLowerCase();
299
+ if (!q)
300
+ return [...candidates];
301
+ const rank = (c) => {
302
+ const u = c.username.toLowerCase();
303
+ if (u === q)
304
+ return 0;
305
+ if (u.startsWith(q))
306
+ return 1;
307
+ if (c.name && c.name.toLowerCase().split(/\s+/).some((w) => w.startsWith(q)))
308
+ return 2;
309
+ if (anywhere && u.includes(q))
310
+ return 3;
311
+ return -1;
312
+ };
313
+ return candidates.map((c) => ({ c, r: rank(c) })).filter((x) => x.r >= 0).sort((a, b) => a.r - b.r).map((x) => x.c);
314
+ }
315
+ function insertMention(value, { start, end }, username) {
316
+ const after = value.slice(end);
317
+ const space = after.startsWith(" ") ? "" : " ";
318
+ const next = `${value.slice(0, start)}@${username}${space}${after}`;
319
+ return { value: next, cursor: start + username.length + 2 };
320
+ }
321
+ function completeMention(value, cursor, candidates, cycle = null, dir = 1) {
322
+ if (cycle && cycle.value === value && cycle.cursor === cursor && cycle.matches.length) {
323
+ const n = cycle.matches.length;
324
+ const index = (cycle.index + dir + n) % n;
325
+ const at = { start: cycle.start, end: cycle.start + cycle.matches[cycle.index].username.length + 1 };
326
+ const done = insertMention(value, at, cycle.matches[index].username);
327
+ return { ...done, cycle: { ...cycle, index, value: done.value, cursor: done.cursor } };
328
+ }
329
+ const at = mentionAt(value, cursor);
330
+ if (!at)
331
+ return null;
332
+ const matches = matchMentions(candidates, at.query);
333
+ if (!matches.length)
334
+ return null;
335
+ const index = dir < 0 ? matches.length - 1 : 0;
336
+ const done = insertMention(value, at, matches[index].username);
337
+ return { ...done, cycle: { start: at.start, query: at.query, matches, index, value: done.value, cursor: done.cursor } };
338
+ }
339
+ function expandMentions(text, candidates) {
340
+ const mentions = [];
341
+ const ambiguous = [];
342
+ const unknown = [];
343
+ const out = String(text).replace(/(^|[\s([{"'“‘])@([\p{L}\p{N}_.-]+)/gu, (whole, before, raw) => {
344
+ const query = raw.replace(/[.-]+$/, "");
345
+ const tail = raw.slice(query.length);
346
+ if (!query)
347
+ return whole;
348
+ const exact = candidates.find((c) => c.username.toLowerCase() === query.toLowerCase());
349
+ const pick = exact ? [exact] : matchMentions(candidates, query, { anywhere: false });
350
+ if (pick.length === 1) {
351
+ if (!mentions.includes(pick[0].username))
352
+ mentions.push(pick[0].username);
353
+ return `${before}@${pick[0].username}${tail}`;
354
+ }
355
+ if (pick.length > 1)
356
+ ambiguous.push({ query, matches: pick.map((c) => c.username) });
357
+ else
358
+ unknown.push(query);
359
+ return whole;
360
+ });
361
+ return { text: out, mentions, ambiguous, unknown };
362
+ }
363
+ function mentionHint(matches, query, index = -1, max = 6) {
364
+ if (!matches.length)
365
+ return `@${query}: nobody here by that name`;
366
+ const shown = matches.slice(0, max).map((c, i) => i === index ? `[${c.username}]` : c.username);
367
+ const more = matches.length > max ? ` +${matches.length - max}` : "";
368
+ return `@${query} → ${shown.join(" ")}${more} (Tab)`;
369
+ }
370
+
371
+ export { DEFAULT_URL, configDir, sessionPath, baseUrl, loadSession, unlockSession, storeSession, clearSession, notify, PAIRUX_URL, newVoiceRoomLink, voiceMessage, parseSlashCommand, namesLines, listLines, mentionCandidates, mentionAt, matchMentions, completeMention, expandMentions, mentionHint };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  openBrowser2,
3
3
  login2
4
- } from "./chunk-vj54b54e.js";
4
+ } from "./chunk-ywyhy3sp.js";
5
5
  export {
6
6
  login2 as login,
7
7
  openBrowser2 as openBrowser
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  CALL_TYPE,
3
3
  parseCall
4
- } from "./chunk-vj54b54e.js";
4
+ } from "./chunk-ywyhy3sp.js";
5
5
 
6
6
  // src/cli/voice-agent.js
7
7
  import { spawn } from "node:child_process";
@@ -10,8 +10,11 @@ import {
10
10
  PAIRUX_URL,
11
11
  newVoiceRoomLink,
12
12
  voiceMessage,
13
- namesLines
14
- } from "./chunk-8gpysrd6.js";
13
+ namesLines,
14
+ mentionCandidates,
15
+ matchMentions,
16
+ expandMentions
17
+ } from "./chunk-4ea1xy2c.js";
15
18
 
16
19
  // src/cli/crypto.js
17
20
  import { MlKem1024 as MlKem10242 } from "mlkem";
@@ -1271,10 +1274,12 @@ Usage:
1271
1274
  qc call <number> call a phone (US/Canada free 10 min a day; +country code for others):
1272
1275
  opens qrypt.chat's dialer with the number filled in
1273
1276
  qc read <chat> [-n 20] print the last messages of a chat
1274
- qc send <chat> <text> send a message (text "-" reads stdin; --reply <message-id> to reply)
1277
+ qc send <chat> <text> send a message (text "-" reads stdin; --reply <message-id> to reply);
1278
+ an @partial that matches one person becomes their @username
1279
+ qc mentions <chat> [@partial] who @ completes to in a chat, best match first (one per line)
1275
1280
  qc react <chat> <message-id> <emoji> [--remove] react to a message
1276
1281
  qc listen print new messages as they arrive (NDJSON with --json; --notify alerts with it)
1277
- qc mcp run as an MCP server on stdio (list_chats, read_chat, send_message)
1282
+ qc mcp run as an MCP server on stdio (list_chats, read_chat, send_message, list_mentions)
1278
1283
  qc join <link> join a group chat from its invite link (https://qrypt.chat/join/...)
1279
1284
  qc invite <chat> print a group's invite link (group admins) [--reset makes a new one, --off turns it off]
1280
1285
 
@@ -1330,6 +1335,8 @@ function findChat2(chats, query) {
1330
1335
  throw new Error(`"${query}" matches ${some.length} chats: ${some.map((c) => c.title).join(", ")}`);
1331
1336
  throw new Error(`No chat matches "${query}". Try qc chats.`);
1332
1337
  }
1338
+ var mentionMatches2 = (chat, me, partial = "") => matchMentions(mentionCandidates(chat.participants, me?.id), String(partial).replace(/^@/, ""));
1339
+ var withMentions2 = (chat, me, text) => expandMentions(text, mentionCandidates(chat.participants, me?.id));
1333
1340
  async function client(flags, { interactive = false } = {}) {
1334
1341
  if (flags.url)
1335
1342
  process.env.QC_URL = flags.url;
@@ -1372,7 +1379,7 @@ async function main2(argv, { version = "0.0.0" } = {}) {
1372
1379
  }
1373
1380
  case "agent": {
1374
1381
  if (rest[0] === "call") {
1375
- const { runVoiceAgent } = await import("./chunk-ckgythyp.js");
1382
+ const { runVoiceAgent } = await import("./chunk-wkjs9anq.js");
1376
1383
  return runVoiceAgent(await client(flags), {
1377
1384
  brain: typeof flags.brain === "string" ? flags.brain : undefined,
1378
1385
  voice: typeof flags.voice === "string" ? flags.voice : undefined,
@@ -1507,7 +1514,7 @@ ${info("Set: qc profile --emoji :telescope: --pronouns she/her --website https:/
1507
1514
  throw new Error('Usage: qc call <number> (e.g. qc call "(415) 555-1234" or qc call +442079460958)');
1508
1515
  const c = await client(flags);
1509
1516
  const url = `${c.base}/phone?to=${encodeURIComponent(rest.join(" "))}`;
1510
- const { openBrowser } = await import("./chunk-kycg804s.js");
1517
+ const { openBrowser } = await import("./chunk-nsn86c8y.js");
1511
1518
  const opened = openBrowser(url);
1512
1519
  return out(`${mark("send")}${opened === false ? "Open" : "Opened"} ${url}
1513
1520
  Calls to phones need your browser for the audio; tap Call there.`);
@@ -1523,6 +1530,18 @@ ${info("Set: qc profile --emoji :telescope: --pronouns she/her --website https:/
1523
1530
  out(line);
1524
1531
  return;
1525
1532
  }
1533
+ case "mentions": {
1534
+ if (!rest[0])
1535
+ throw new Error("Usage: qc mentions <chat> [@partial]");
1536
+ const c = await client(flags);
1537
+ const chat = findChat2(await c.conversations(), rest[0]);
1538
+ const matches = mentionMatches2(chat, c.me, rest[1] ?? "");
1539
+ if (flags.json)
1540
+ return json(matches);
1541
+ for (const p of matches)
1542
+ out(`@${p.username}${p.agent ? " \uD83E\uDD16" : ""}${p.name ? ` ${p.name}` : ""}`);
1543
+ return;
1544
+ }
1526
1545
  case "read": {
1527
1546
  if (!rest[0])
1528
1547
  throw new Error("Usage: qc read <chat> [-n 20]");
@@ -1586,11 +1605,14 @@ ${info("Set: qc profile --emoji :telescope: --pronouns she/her --website https:/
1586
1605
  throw new Error("Nothing to send.");
1587
1606
  const c = await client(flags);
1588
1607
  const chat = findChat2(await c.conversations(), rest[0]);
1589
- const { message, skipped } = await c.send(chat.id, text, { replyTo: flags.reply || undefined });
1608
+ const m = withMentions2(chat, c.me, text);
1609
+ const { message, skipped } = await c.send(chat.id, m.text, { replyTo: flags.reply || undefined });
1590
1610
  if (flags.json)
1591
- return json({ id: message?.id, conversation: chat.id, skipped });
1611
+ return json({ id: message?.id, conversation: chat.id, skipped, mentions: m.mentions, ambiguous: m.ambiguous });
1592
1612
  if (skipped)
1593
1613
  out(warn(`${skipped} participant(s) have no key yet and will not see it.`));
1614
+ for (const a of m.ambiguous)
1615
+ out(warn(`@${a.query} could be ${a.matches.join(", ")}: sent as typed.`));
1594
1616
  return out(`${mark("send")}Sent to ${chat.title}.`);
1595
1617
  }
1596
1618
  case "listen": {
@@ -1666,4 +1688,4 @@ async function voiceCommand(argv, { out, json }) {
1666
1688
  }
1667
1689
  }
1668
1690
 
1669
- export { CALL_TYPE, parseCall, openBrowser2, login2, exitWhenFlushed2, errorLine2, HELP2, parseArgs2, findChat2, main2 };
1691
+ export { CALL_TYPE, parseCall, openBrowser2, login2, exitWhenFlushed2, errorLine2, HELP2, parseArgs2, findChat2, mentionMatches2, withMentions2, main2 };
package/dist/commands.js CHANGED
@@ -4,14 +4,18 @@ import {
4
4
  HELP2,
5
5
  parseArgs2,
6
6
  findChat2,
7
+ mentionMatches2,
8
+ withMentions2,
7
9
  main2
8
- } from "./chunk-vj54b54e.js";
9
- import"./chunk-8gpysrd6.js";
10
+ } from "./chunk-ywyhy3sp.js";
11
+ import"./chunk-4ea1xy2c.js";
10
12
  export {
11
13
  HELP2 as HELP,
12
14
  errorLine2 as errorLine,
13
15
  exitWhenFlushed2 as exitWhenFlushed,
14
16
  findChat2 as findChat,
15
17
  main2 as main,
16
- parseArgs2 as parseArgs
18
+ mentionMatches2 as mentionMatches,
19
+ parseArgs2 as parseArgs,
20
+ withMentions2 as withMentions
17
21
  };
package/dist/mcp.js CHANGED
@@ -1,7 +1,9 @@
1
- import"./chunk-8gpysrd6.js";
1
+ import"./chunk-4ea1xy2c.js";
2
2
  import {
3
- findChat2
4
- } from "./chunk-vj54b54e.js";
3
+ findChat2,
4
+ mentionMatches2,
5
+ withMentions2
6
+ } from "./chunk-ywyhy3sp.js";
5
7
 
6
8
  // src/cli/mcp.js
7
9
  import { createInterface } from "node:readline";
@@ -26,7 +28,7 @@ var TOOLS = [
26
28
  },
27
29
  {
28
30
  name: "send_message",
29
- description: "Send an end-to-end encrypted message (ML-KEM-1024) to a conversation.",
31
+ description: "Send an end-to-end encrypted message (ML-KEM-1024) to a conversation. An @partial in the text that matches exactly one participant is written out as their full @username (list_mentions shows who matches).",
30
32
  inputSchema: {
31
33
  type: "object",
32
34
  properties: {
@@ -38,6 +40,19 @@ var TOOLS = [
38
40
  additionalProperties: false
39
41
  }
40
42
  },
43
+ {
44
+ name: "list_mentions",
45
+ description: "Who an @mention can complete to in a conversation, best match first (like typing @ali then Tab). Without partial: everyone you can mention.",
46
+ inputSchema: {
47
+ type: "object",
48
+ properties: {
49
+ chat: { type: "string", description: "Conversation id or part of its name" },
50
+ partial: { type: "string", description: "Optional: the start of a name, with or without the @" }
51
+ },
52
+ required: ["chat"],
53
+ additionalProperties: false
54
+ }
55
+ },
41
56
  {
42
57
  name: "react_to_message",
43
58
  description: "React to a message with an emoji (Signal-style: one reaction per person; a new one replaces yours). End-to-end encrypted.",
@@ -79,8 +94,14 @@ async function callTool(client, name, args = {}) {
79
94
  if (!args.text || typeof args.text !== "string")
80
95
  throw new Error("text is required");
81
96
  const chat = findChat2(await client.conversations(), args.chat);
82
- const { message, skipped } = await client.send(chat.id, args.text, { replyTo: typeof args.reply_to === "string" ? args.reply_to : undefined });
83
- return { sent: true, id: message?.id, chat: chat.title, skipped };
97
+ const m = withMentions2(chat, client.me, args.text);
98
+ const { message, skipped } = await client.send(chat.id, m.text, { replyTo: typeof args.reply_to === "string" ? args.reply_to : undefined });
99
+ return { sent: true, id: message?.id, chat: chat.title, skipped, ...m.mentions.length ? { mentions: m.mentions } : {}, ...m.ambiguous.length ? { ambiguous: m.ambiguous } : {} };
100
+ }
101
+ if (name === "list_mentions") {
102
+ const chat = findChat2(await client.conversations(), args.chat);
103
+ const matches = mentionMatches2(chat, client.me, typeof args.partial === "string" ? args.partial : "");
104
+ return { chat: chat.title, mentions: matches.map(({ username, name: display_name, agent }) => ({ mention: `@${username}`, username, ...display_name ? { display_name } : {}, agent })) };
84
105
  }
85
106
  if (name === "react_to_message") {
86
107
  if (!args.emoji || typeof args.emoji !== "string")
package/dist/tui.js CHANGED
@@ -6,8 +6,13 @@ import {
6
6
  voiceMessage,
7
7
  parseSlashCommand,
8
8
  namesLines,
9
- listLines
10
- } from "./chunk-8gpysrd6.js";
9
+ listLines,
10
+ mentionCandidates,
11
+ mentionAt,
12
+ matchMentions,
13
+ completeMention,
14
+ mentionHint
15
+ } from "./chunk-4ea1xy2c.js";
11
16
 
12
17
  // src/cli/tui.js
13
18
  import { createApp, createImageStore, drawIcon, drawRichText, editText, emojify, insertText, stringWidth, truncate, widgets, wrap } from "@profullstack/hqtui";
@@ -41,9 +46,28 @@ function initialState(me) {
41
46
  status: "connecting",
42
47
  notice: "",
43
48
  loading: true,
44
- showArchived: false
49
+ showArchived: false,
50
+ mention: null
45
51
  };
46
52
  }
53
+ var mentionPeople = (state) => mentionCandidates(state.conversations.find((c) => c.id === state.activeId)?.participants, state.me?.id);
54
+ function mentionSuggestions(state) {
55
+ if (state.focus !== "compose" || !state.activeId)
56
+ return null;
57
+ const cycle = state.mention;
58
+ if (cycle && cycle.value === state.field.value && cycle.cursor === state.field.cursor)
59
+ return { query: cycle.query, matches: cycle.matches, index: cycle.index };
60
+ const at = mentionAt(state.field.value, state.field.cursor);
61
+ return at ? { query: at.query, matches: matchMentions(mentionPeople(state), at.query), index: -1 } : null;
62
+ }
63
+ function tabComplete(state, dir = 1) {
64
+ const done = completeMention(state.field.value, state.field.cursor, mentionPeople(state), state.mention, dir);
65
+ if (!done)
66
+ return false;
67
+ state.field = { value: done.value, cursor: done.cursor };
68
+ state.mention = done.cycle;
69
+ return true;
70
+ }
47
71
  var listed = (state) => state.conversations.filter((c) => !!c.is_archived === !!state.showArchived);
48
72
  var CHAT_LIST_REFRESH_MS = 15000;
49
73
  function chatsWithNews(prev, next) {
@@ -138,6 +162,7 @@ function drawTranscript(surface, state) {
138
162
  function render({ ui, width, height, theme }, state, on = {}) {
139
163
  const active = state.conversations.find((c) => c.id === state.activeId);
140
164
  const typingNames = Object.values(state.typing[state.activeId] || {}).map((t) => t.name);
165
+ const mention = mentionSuggestions(state);
141
166
  ui.row({ size: 1 }, (bar) => {
142
167
  bar.draw((s) => {
143
168
  const t = s.theme;
@@ -188,11 +213,11 @@ function render({ ui, width, height, theme }, state, on = {}) {
188
213
  drawTranscript(s, state);
189
214
  p.ctx.hit({ rect: s.hitRect(), onScroll: (delta) => on.scroll?.(-delta) });
190
215
  }, { size: "fill" });
191
- p.label(typingNames.length ? `${typingNames.join(", ")} ${typingNames.length === 1 ? "is" : "are"} typing…` : " ", { size: 1 });
216
+ p.label(mention ? mentionHint(mention.matches, mention.query, mention.index) : typingNames.length ? `${typingNames.join(", ")} ${typingNames.length === 1 ? "is" : "are"} typing…` : " ", { size: 1 });
192
217
  p.textInput({
193
218
  value: state.field.value,
194
219
  cursor: state.field.cursor,
195
- placeholder: active ? `Message ${active.title} (Ctrl+E emoji, :rocket: works too)` : "Pick a chat first",
220
+ placeholder: active ? `Message ${active.title} (@name Tab, Ctrl+E emoji, :rocket: works too)` : "Pick a chat first",
196
221
  focused: state.focus === "compose" && !state.picker,
197
222
  size: 1
198
223
  });
@@ -202,7 +227,7 @@ function render({ ui, width, height, theme }, state, on = {}) {
202
227
  items: [
203
228
  { key: "Enter", label: state.focus === "list" ? "open" : "send" },
204
229
  { key: "Ctrl+E", label: "emoji" },
205
- { key: "Tab", label: state.focus === "list" ? "compose" : "chats" },
230
+ { key: "Tab", label: state.focus === "list" ? "compose" : mention?.matches.length ? "complete @" : "chats" },
206
231
  { key: "PgUp", label: "scroll" },
207
232
  ...state.focus === "list" ? [{ key: "a", label: state.showArchived ? "unarchive" : "archive" }] : [],
208
233
  { key: "Ctrl+O", label: state.showArchived ? "chats" : "archived" },
@@ -442,6 +467,8 @@ async function runTui(client, { initialChat } = {}) {
442
467
  return;
443
468
  if (event.key === "ctrl+e") {
444
469
  state.picker = widgets.createEmojiPicker(state.recent);
470
+ } else if ((event.key === "tab" || event.key === "shift+tab") && state.focus === "compose" && tabComplete(state, event.key === "tab" ? 1 : -1)) {
471
+ typed();
445
472
  } else if (event.key === "tab" || event.key === "shift+tab" || event.key === "escape" && state.focus === "compose") {
446
473
  state.focus = state.focus === "list" ? "compose" : "list";
447
474
  } else if (event.key === "pageup") {
@@ -562,7 +589,9 @@ export {
562
589
  chatsWithNews,
563
590
  initialState,
564
591
  listed,
592
+ mentionSuggestions,
565
593
  render,
566
594
  runTui,
595
+ tabComplete,
567
596
  transcriptLines
568
597
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@profullstack/qryptchat",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
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": {