@profullstack/qryptchat 0.8.0 → 0.8.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/README.md CHANGED
@@ -3,10 +3,15 @@
3
3
  [qrypt.chat](https://qrypt.chat) in your terminal: a full-screen, end-to-end encrypted chat client, a scriptable CLI and an MCP server, all in one command.
4
4
 
5
5
  ```sh
6
- npm install -g @profullstack/qryptchat # or: bun add -g @profullstack/qryptchat
6
+ curl -fsSL https://qrypt.chat/install.sh | sh
7
7
  qc
8
8
  ```
9
9
 
10
+ The installer puts `qc` (and its alias `qryptchat`) under `~/.local`, never asks for sudo, and
11
+ fetches a private official Node if this machine has no Node 22+. Run it again to update; remove
12
+ with `sh ~/.local/share/qryptchat/uninstall.sh`. Or install it with a package manager:
13
+ `npm install -g @profullstack/qryptchat` (or `bun add -g @profullstack/qryptchat`).
14
+
10
15
  The first run signs you in through your browser, then opens your chats.
11
16
 
12
17
  ## The client
@@ -235,4 +235,33 @@ function parseSlashCommand(text, names) {
235
235
  return { name: m[1], args: (m[2] ?? "").trim() };
236
236
  }
237
237
 
238
- export { DEFAULT_URL, configDir, sessionPath, baseUrl, loadSession, unlockSession, storeSession, clearSession, notify, PAIRUX_URL, newVoiceRoomLink, voiceMessage, parseSlashCommand };
238
+ // src/lib/chat/irc.js
239
+ var nameOf = (p) => {
240
+ const u = p.user || p;
241
+ return u.username || u.display_name || "unknown";
242
+ };
243
+ function namesLines(conversation, title = conversation?.name || "this chat") {
244
+ const people = [...conversation?.participants || []].sort((a, b) => (b.role === "admin") - (a.role === "admin") || nameOf(a).localeCompare(nameOf(b)));
245
+ const names = people.map((p) => {
246
+ const u = p.user || p;
247
+ const op = p.role === "admin" ? "@" : p.role === "moderator" ? "+" : "";
248
+ const agent = u.account_type === "agent" ? " \uD83E\uDD16" : "";
249
+ return `${op}${nameOf(p)}${agent}`;
250
+ });
251
+ const n = names.length;
252
+ return [`${title}: ${n} ${n === 1 ? "person" : "people"}`, names.join(" ") || "(nobody)"];
253
+ }
254
+ function listLines(conversations, titleOf = (c) => c.name || "Chat") {
255
+ const live = (conversations || []).filter((c) => !c.is_archived);
256
+ const rank = { group: 0, direct: 1, note_to_self: 2 };
257
+ const sorted = [...live].sort((a, b) => (rank[a.type] ?? 1) - (rank[b.type] ?? 1) || titleOf(a).localeCompare(titleOf(b)));
258
+ const lines = sorted.map((c) => {
259
+ const n = c.participants?.length ?? 0;
260
+ const kind = c.type === "group" ? "group" : c.type === "note_to_self" ? "note" : "direct";
261
+ return `${c.type === "group" ? "#" : " "}${titleOf(c)} ${n} ${n === 1 ? "person" : "people"} · ${kind}`;
262
+ });
263
+ const archived = (conversations || []).length - live.length;
264
+ return [`${live.length} ${live.length === 1 ? "chat" : "chats"}${archived ? ` (+${archived} archived)` : ""}`, ...lines];
265
+ }
266
+
267
+ export { DEFAULT_URL, configDir, sessionPath, baseUrl, loadSession, unlockSession, storeSession, clearSession, notify, PAIRUX_URL, newVoiceRoomLink, voiceMessage, parseSlashCommand, namesLines, listLines };
@@ -9,8 +9,9 @@ import {
9
9
  notify,
10
10
  PAIRUX_URL,
11
11
  newVoiceRoomLink,
12
- voiceMessage
13
- } from "./chunk-zc4n789c.js";
12
+ voiceMessage,
13
+ namesLines
14
+ } from "./chunk-8gpysrd6.js";
14
15
 
15
16
  // src/cli/crypto.js
16
17
  import { MlKem1024 as MlKem10242 } from "mlkem";
@@ -1257,7 +1258,8 @@ Usage:
1257
1258
  qc profile [--emoji \uD83D\uDD2D] [--pronouns she/her] [--website URL] [--bio TEXT]
1258
1259
  show or set your public profile (an empty value clears it);
1259
1260
  --openprofile prints it as OpenProfile.md
1260
- qc chats [--archived] list your chats (archived ones only with --archived)
1261
+ qc chats [--archived] list your chats (archived ones only with --archived); qc list is the same
1262
+ qc names <chat> who is in a chat (@ = admin, \uD83E\uDD16 = agent), like IRC /names
1261
1263
  qc archive <chat> archive a chat for you (qc unarchive <chat> brings it back)
1262
1264
  qc voice [--send <chat>] a link to a fresh end-to-end encrypted PairUX voice room (no account
1263
1265
  needed to join); qc voice login|join|... runs the pairux CLI (qc voice help)
@@ -1446,6 +1448,7 @@ ${info("Set: qc profile --emoji :telescope: --pronouns she/her --website https:/
1446
1448
  return;
1447
1449
  }
1448
1450
  case "chats":
1451
+ case "list":
1449
1452
  case "ls": {
1450
1453
  const all = await (await client(flags)).conversations();
1451
1454
  const chats = all.filter((c) => !!c.is_archived === !!flags.archived);
@@ -1466,6 +1469,17 @@ ${info("Set: qc profile --emoji :telescope: --pronouns she/her --website https:/
1466
1469
  return json({ id: chat.id, archived: cmd === "archive" });
1467
1470
  return out(ok(cmd === "archive" ? `Archived ${chat.title}. qc chats --archived lists it; qc unarchive brings it back.` : `${chat.title} is back in your chats.`));
1468
1471
  }
1472
+ case "names": {
1473
+ if (!rest[0])
1474
+ throw new Error("Usage: qc names <chat>");
1475
+ const chat = findChat2(await (await client(flags)).conversations(), rest[0]);
1476
+ if (flags.json) {
1477
+ return json((chat.participants || []).map((p) => ({ id: p.user_id, username: p.user?.username, display_name: p.user?.display_name, role: p.role, agent: p.user?.account_type === "agent" })));
1478
+ }
1479
+ for (const line of namesLines(chat, chat.title))
1480
+ out(line);
1481
+ return;
1482
+ }
1469
1483
  case "read": {
1470
1484
  if (!rest[0])
1471
1485
  throw new Error("Usage: qc read <chat> [-n 20]");
package/dist/commands.js CHANGED
@@ -5,8 +5,8 @@ import {
5
5
  parseArgs2,
6
6
  findChat2,
7
7
  main2
8
- } from "./chunk-nrv1k5q2.js";
9
- import"./chunk-zc4n789c.js";
8
+ } from "./chunk-wtbsdxe7.js";
9
+ import"./chunk-8gpysrd6.js";
10
10
  export {
11
11
  HELP2 as HELP,
12
12
  errorLine2 as errorLine,
package/dist/mcp.js CHANGED
@@ -1,7 +1,7 @@
1
- import"./chunk-zc4n789c.js";
1
+ import"./chunk-8gpysrd6.js";
2
2
  import {
3
3
  findChat2
4
- } from "./chunk-nrv1k5q2.js";
4
+ } from "./chunk-wtbsdxe7.js";
5
5
 
6
6
  // src/cli/mcp.js
7
7
  import { createInterface } from "node:readline";
package/dist/tui.js CHANGED
@@ -4,8 +4,10 @@ import {
4
4
  PAIRUX_URL,
5
5
  newVoiceRoomLink,
6
6
  voiceMessage,
7
- parseSlashCommand
8
- } from "./chunk-zc4n789c.js";
7
+ parseSlashCommand,
8
+ namesLines,
9
+ listLines
10
+ } from "./chunk-8gpysrd6.js";
9
11
 
10
12
  // src/cli/tui.js
11
13
  import { createApp, createImageStore, drawIcon, drawRichText, editText, emojify, insertText, stringWidth, truncate, widgets, wrap } from "@profullstack/hqtui";
@@ -48,7 +50,7 @@ function chatsWithNews(prev, next) {
48
50
  const seen = new Map(prev.map((c) => [c.id, c.updated_at ?? ""]));
49
51
  return next.filter((c) => !seen.has(c.id) || (c.updated_at ?? "") > seen.get(c.id)).map((c) => c.id);
50
52
  }
51
- var SLASH_COMMANDS = ["voice", "archive", "unarchive", "archived"];
53
+ var SLASH_COMMANDS = ["voice", "archive", "unarchive", "archived", "names", "list"];
52
54
  var pad2 = (n) => String(n).padStart(2, "0");
53
55
  var timeOf = (iso) => {
54
56
  const d = new Date(iso);
@@ -261,7 +263,8 @@ async function runTui(client, { initialChat } = {}) {
261
263
  async function loadMessages(id) {
262
264
  try {
263
265
  const { messages } = await client.messages(id, { limit: 100 });
264
- state.messages[id] = messages;
266
+ const local = (state.messages[id] || []).filter((m) => m.local);
267
+ state.messages[id] = local.length ? [...messages, ...local].sort((a, b) => String(a.at).localeCompare(String(b.at))) : messages;
265
268
  redraw();
266
269
  } catch (err) {
267
270
  note(`Could not load messages: ${err.message}`);
@@ -363,7 +366,14 @@ async function runTui(client, { initialChat } = {}) {
363
366
  toggleArchivedView();
364
367
  else if (!active)
365
368
  note("Pick a chat first");
366
- else if (cmd.name === "voice") {
369
+ else if (cmd.name === "names" || cmd.name === "list") {
370
+ const lines = cmd.name === "names" ? namesLines(active, active.title) : listLines(state.conversations, (c) => c.title);
371
+ const at = new Date().toISOString();
372
+ const local = { id: `local-${Date.now()}`, senderId: "qc", sender: `qc /${cmd.name}`, local: true, text: lines.join(`
373
+ `), at };
374
+ state.messages[active.id] = [...state.messages[active.id] || [], local];
375
+ state.scrollBack = 0;
376
+ } else if (cmd.name === "voice") {
367
377
  const url = newVoiceRoomLink(process.env.PAIRUX_API_URL || PAIRUX_URL);
368
378
  state.field = { value: voiceMessage(url), cursor: 0 };
369
379
  send({ raw: true }).then(() => note("Voice room posted; open the link in a browser to join", { sticky: false }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@profullstack/qryptchat",
3
- "version": "0.8.0",
3
+ "version": "0.8.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": {