@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 +6 -1
- package/dist/{chunk-zc4n789c.js → chunk-8gpysrd6.js} +30 -1
- package/dist/{chunk-nrv1k5q2.js → chunk-wtbsdxe7.js} +17 -3
- package/dist/commands.js +2 -2
- package/dist/mcp.js +2 -2
- package/dist/tui.js +15 -5
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
package/dist/mcp.js
CHANGED
package/dist/tui.js
CHANGED
|
@@ -4,8 +4,10 @@ import {
|
|
|
4
4
|
PAIRUX_URL,
|
|
5
5
|
newVoiceRoomLink,
|
|
6
6
|
voiceMessage,
|
|
7
|
-
parseSlashCommand
|
|
8
|
-
|
|
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]
|
|
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 === "
|
|
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.
|
|
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": {
|