@profullstack/qryptchat 0.1.0 → 0.2.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/dist/{chunk-9kkxje88.js → chunk-c8qkdept.js} +175 -32
- package/dist/commands.js +1 -1
- package/dist/mcp.js +37 -4
- package/dist/tui.js +8 -0
- package/package.json +1 -1
|
@@ -822,6 +822,81 @@ async function openKeyBlob(blob, ephemeral) {
|
|
|
822
822
|
return { keys1024: keys.keys1024, keys768: keys.keys768 };
|
|
823
823
|
}
|
|
824
824
|
|
|
825
|
+
// src/lib/chat/reactions.js
|
|
826
|
+
var REACTION_TYPE = "reaction";
|
|
827
|
+
var MAX_EMOJI_LENGTH = 32;
|
|
828
|
+
function reactionEnvelope(targetId, emoji, remove = false) {
|
|
829
|
+
return JSON.stringify({ v: 1, type: REACTION_TYPE, target: String(targetId), emoji: String(emoji), remove: !!remove });
|
|
830
|
+
}
|
|
831
|
+
function parseReaction(content) {
|
|
832
|
+
try {
|
|
833
|
+
const d = JSON.parse(content);
|
|
834
|
+
if (d?.type !== REACTION_TYPE || typeof d.target !== "string" || typeof d.emoji !== "string")
|
|
835
|
+
return null;
|
|
836
|
+
if (!d.emoji || d.emoji.length > MAX_EMOJI_LENGTH)
|
|
837
|
+
return null;
|
|
838
|
+
return { target: d.target, emoji: d.emoji, remove: !!d.remove };
|
|
839
|
+
} catch {
|
|
840
|
+
return null;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
var senderName = (m) => m?.sender?.display_name || m?.sender?.username || "Someone";
|
|
844
|
+
function snippet(m, max = 90) {
|
|
845
|
+
if (!m)
|
|
846
|
+
return "";
|
|
847
|
+
if (m.message_type === "file" || m.has_attachments)
|
|
848
|
+
return "\uD83D\uDCCE Attachment";
|
|
849
|
+
const text = String(m.content ?? "").replace(/\s+/g, " ").trim();
|
|
850
|
+
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
851
|
+
}
|
|
852
|
+
function foldMessages(messages, myUserId) {
|
|
853
|
+
const visible = [];
|
|
854
|
+
const byId = new Map;
|
|
855
|
+
const picks = new Map;
|
|
856
|
+
for (const m of messages) {
|
|
857
|
+
if (m.message_type === REACTION_TYPE) {
|
|
858
|
+
const r = parseReaction(m.content);
|
|
859
|
+
if (!r)
|
|
860
|
+
continue;
|
|
861
|
+
const forTarget = picks.get(r.target) ?? new Map;
|
|
862
|
+
const current = forTarget.get(m.sender_id);
|
|
863
|
+
if (r.remove) {
|
|
864
|
+
if (current && current.emoji === r.emoji)
|
|
865
|
+
forTarget.delete(m.sender_id);
|
|
866
|
+
} else {
|
|
867
|
+
forTarget.set(m.sender_id, { emoji: r.emoji, name: senderName(m), at: m.created_at });
|
|
868
|
+
}
|
|
869
|
+
picks.set(r.target, forTarget);
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
visible.push(m);
|
|
873
|
+
byId.set(m.id, m);
|
|
874
|
+
}
|
|
875
|
+
return visible.map((m) => {
|
|
876
|
+
const out = { ...m, reactions: [] };
|
|
877
|
+
const forTarget = picks.get(m.id);
|
|
878
|
+
if (forTarget?.size) {
|
|
879
|
+
const groups = new Map;
|
|
880
|
+
for (const [senderId, p] of forTarget) {
|
|
881
|
+
const g = groups.get(p.emoji) ?? { emoji: p.emoji, count: 0, mine: false, names: [], first: p.at };
|
|
882
|
+
g.count += 1;
|
|
883
|
+
g.names.push(senderId === myUserId ? "You" : p.name);
|
|
884
|
+
if (senderId === myUserId)
|
|
885
|
+
g.mine = true;
|
|
886
|
+
if (p.at < g.first)
|
|
887
|
+
g.first = p.at;
|
|
888
|
+
groups.set(p.emoji, g);
|
|
889
|
+
}
|
|
890
|
+
out.reactions = [...groups.values()].sort((a, b) => b.count - a.count || String(a.first).localeCompare(String(b.first)));
|
|
891
|
+
}
|
|
892
|
+
if (m.reply_to_id) {
|
|
893
|
+
const target = byId.get(m.reply_to_id);
|
|
894
|
+
out.replyTo = target ? { id: target.id, name: target.sender_id === myUserId ? "You" : senderName(target), snippet: snippet(target), missing: false } : { id: m.reply_to_id, name: "", snippet: "Original message not loaded", missing: true };
|
|
895
|
+
}
|
|
896
|
+
return out;
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
|
|
825
900
|
// src/cli/api.js
|
|
826
901
|
class QcError extends Error {
|
|
827
902
|
constructor(message, status) {
|
|
@@ -889,34 +964,37 @@ class QcClient {
|
|
|
889
964
|
const { conversations = [] } = await this.request("/api/conversations/load", { method: "POST", body: {} });
|
|
890
965
|
return conversations.map((c) => ({ ...c, title: conversationTitle(c, this.me) }));
|
|
891
966
|
}
|
|
892
|
-
async messages(conversationId, { limit =
|
|
967
|
+
async messages(conversationId, { limit = 200, before } = {}) {
|
|
893
968
|
const { messages = [], hasMore = false } = await this.request("/api/messages/load", {
|
|
894
969
|
method: "POST",
|
|
895
970
|
body: { conversationId, limit, ...before ? { before } : {} }
|
|
896
971
|
});
|
|
897
|
-
const
|
|
972
|
+
const raw = [];
|
|
898
973
|
for (const m of messages) {
|
|
899
|
-
let
|
|
974
|
+
let content = m.encrypted_content ? await this.ring.decrypt(m.encrypted_content) : "";
|
|
900
975
|
if (m.message_type === "file" || m.has_attachments) {
|
|
901
|
-
|
|
902
|
-
text = `\uD83D\uDCCE ${text && text !== "[File attachment]" ? `${text} ` : ""}(attachment: open qrypt.chat to download)`;
|
|
903
|
-
} else {
|
|
904
|
-
text = m.encrypted_content ? await this.ring.decrypt(m.encrypted_content) : "";
|
|
976
|
+
content = `\uD83D\uDCCE ${content && content !== "[File attachment]" ? `${content} ` : ""}(attachment: open qrypt.chat to download)`;
|
|
905
977
|
}
|
|
906
|
-
|
|
907
|
-
id: m.id,
|
|
908
|
-
conversationId: m.conversation_id,
|
|
909
|
-
senderId: m.sender_id,
|
|
910
|
-
sender: m.sender?.display_name || m.sender?.username || "unknown",
|
|
911
|
-
username: m.sender?.username || "",
|
|
912
|
-
mine: m.sender_id === this.me?.id,
|
|
913
|
-
text,
|
|
914
|
-
at: m.created_at
|
|
915
|
-
});
|
|
978
|
+
raw.push({ ...m, content });
|
|
916
979
|
}
|
|
980
|
+
const out = foldMessages(raw, this.me?.id).map((m) => ({
|
|
981
|
+
id: m.id,
|
|
982
|
+
conversationId: m.conversation_id,
|
|
983
|
+
senderId: m.sender_id,
|
|
984
|
+
sender: m.sender?.display_name || m.sender?.username || "unknown",
|
|
985
|
+
username: m.sender?.username || "",
|
|
986
|
+
mine: m.sender_id === this.me?.id,
|
|
987
|
+
text: m.content,
|
|
988
|
+
at: m.created_at,
|
|
989
|
+
reactions: m.reactions.map(({ emoji, count, mine, names }) => ({ emoji, count, mine, names })),
|
|
990
|
+
...m.replyTo ? { replyTo: m.replyTo } : {}
|
|
991
|
+
}));
|
|
917
992
|
return { messages: out, hasMore };
|
|
918
993
|
}
|
|
919
|
-
async
|
|
994
|
+
async react(conversationId, messageId, emoji, remove = false) {
|
|
995
|
+
return this.send(conversationId, reactionEnvelope(messageId, emoji, remove), { messageType: REACTION_TYPE });
|
|
996
|
+
}
|
|
997
|
+
async send(conversationId, text, { replyTo, messageType = "text" } = {}) {
|
|
920
998
|
const { participants = [] } = await this.request(`/api/chat/conversations/${encodeURIComponent(conversationId)}/participants`);
|
|
921
999
|
const userIds = participants.map((p) => p.user_id);
|
|
922
1000
|
if (!userIds.length)
|
|
@@ -935,7 +1013,7 @@ class QcClient {
|
|
|
935
1013
|
throw new QcError("No participant has a public key yet");
|
|
936
1014
|
const { message } = await this.request("/api/messages/send", {
|
|
937
1015
|
method: "POST",
|
|
938
|
-
body: { conversationId, encryptedContents, messageType:
|
|
1016
|
+
body: { conversationId, encryptedContents, messageType, ...replyTo ? { replyToId: replyTo } : {} }
|
|
939
1017
|
});
|
|
940
1018
|
return { message, skipped: missing.length };
|
|
941
1019
|
}
|
|
@@ -1049,10 +1127,32 @@ var TIMEOUT_MS = 5 * 60 * 1000;
|
|
|
1049
1127
|
var PAGE = (title, body) => `<!doctype html><meta charset="utf-8"><title>${title}</title>
|
|
1050
1128
|
<style>body{font:16px system-ui,sans-serif;display:grid;place-items:center;min-height:90vh;background:#0b0d12;color:#e6e8ee}main{max-width:28rem;text-align:center}</style>
|
|
1051
1129
|
<main><h1>${title}</h1><p>${body}</p></main>`;
|
|
1130
|
+
function isRemote({ platform = process.platform, env = process.env } = {}) {
|
|
1131
|
+
if (env.QC_NO_BROWSER)
|
|
1132
|
+
return true;
|
|
1133
|
+
return !!(env.SSH_CONNECTION || env.SSH_TTY) && !env.DISPLAY && !env.WAYLAND_DISPLAY && platform !== "darwin" && platform !== "win32";
|
|
1134
|
+
}
|
|
1135
|
+
function parsePasted(input, state) {
|
|
1136
|
+
const text = String(input || "").trim();
|
|
1137
|
+
if (!text)
|
|
1138
|
+
return null;
|
|
1139
|
+
if (/^https?:\/\//i.test(text)) {
|
|
1140
|
+
let url;
|
|
1141
|
+
try {
|
|
1142
|
+
url = new URL(text);
|
|
1143
|
+
} catch {
|
|
1144
|
+
return null;
|
|
1145
|
+
}
|
|
1146
|
+
const got = url.searchParams.get("state");
|
|
1147
|
+
if (got && got !== state)
|
|
1148
|
+
throw new Error("That link belongs to a different login. Run qc login again.");
|
|
1149
|
+
return url.searchParams.get("code");
|
|
1150
|
+
}
|
|
1151
|
+
return /^[A-Za-z0-9_-]{20,}$/.test(text) ? text : null;
|
|
1152
|
+
}
|
|
1052
1153
|
function openBrowser(url, { platform = process.platform, env = process.env } = {}) {
|
|
1053
|
-
if (
|
|
1154
|
+
if (isRemote({ platform, env }))
|
|
1054
1155
|
return false;
|
|
1055
|
-
}
|
|
1056
1156
|
const [cmd, args] = platform === "darwin" ? ["open", [url]] : platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
1057
1157
|
try {
|
|
1058
1158
|
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
@@ -1102,7 +1202,8 @@ async function login(options = {}) {
|
|
|
1102
1202
|
const ephemeral = await ephemeralKeypair();
|
|
1103
1203
|
let server;
|
|
1104
1204
|
let redirectUri = "oob";
|
|
1105
|
-
|
|
1205
|
+
const oob = options.oob || isRemote({ env });
|
|
1206
|
+
if (!oob) {
|
|
1106
1207
|
server = createServer();
|
|
1107
1208
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
1108
1209
|
redirectUri = `http://127.0.0.1:${server.address().port}/callback`;
|
|
@@ -1124,20 +1225,44 @@ async function login(options = {}) {
|
|
|
1124
1225
|
print(` Confirmation code: ${match}`);
|
|
1125
1226
|
print(" Check that the browser shows the same code before you approve.");
|
|
1126
1227
|
print("");
|
|
1127
|
-
const opened = !
|
|
1228
|
+
const opened = !oob && openBrowser(url.toString(), { env });
|
|
1128
1229
|
print(opened ? " Opened your browser. If it did not, open:" : " Open this link in a browser where you are signed in to qrypt.chat:");
|
|
1129
1230
|
print(` ${url.toString()}`);
|
|
1130
1231
|
print("");
|
|
1131
1232
|
let code;
|
|
1233
|
+
let rl;
|
|
1132
1234
|
try {
|
|
1133
1235
|
if (server) {
|
|
1134
|
-
|
|
1236
|
+
const waits = [waitForCallback(server, state)];
|
|
1237
|
+
if (process.stdin.isTTY) {
|
|
1238
|
+
rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
1239
|
+
waits.push((async () => {
|
|
1240
|
+
for (;; ) {
|
|
1241
|
+
let line;
|
|
1242
|
+
try {
|
|
1243
|
+
line = await rl.question(" Waiting for the browser… or paste the URL it ended up on: ");
|
|
1244
|
+
} catch {
|
|
1245
|
+
return new Promise(() => {});
|
|
1246
|
+
}
|
|
1247
|
+
const pasted = parsePasted(line, state);
|
|
1248
|
+
if (pasted)
|
|
1249
|
+
return pasted;
|
|
1250
|
+
print(" That is not a qc login code or callback URL.");
|
|
1251
|
+
}
|
|
1252
|
+
})());
|
|
1253
|
+
}
|
|
1254
|
+
code = await Promise.race(waits);
|
|
1135
1255
|
} else {
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1256
|
+
rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
1257
|
+
for (;; ) {
|
|
1258
|
+
code = parsePasted(await rl.question(" Paste the code (or the URL) from the browser: "), state);
|
|
1259
|
+
if (code)
|
|
1260
|
+
break;
|
|
1261
|
+
print(" That is not a qc login code.");
|
|
1262
|
+
}
|
|
1139
1263
|
}
|
|
1140
1264
|
} finally {
|
|
1265
|
+
rl?.close();
|
|
1141
1266
|
server?.close();
|
|
1142
1267
|
}
|
|
1143
1268
|
if (!code)
|
|
@@ -1174,7 +1299,8 @@ Usage:
|
|
|
1174
1299
|
qc whoami who this terminal is signed in as
|
|
1175
1300
|
qc chats list your chats
|
|
1176
1301
|
qc read <chat> [-n 20] print the last messages of a chat
|
|
1177
|
-
qc send <chat> <text> send a message (text "-" reads stdin)
|
|
1302
|
+
qc send <chat> <text> send a message (text "-" reads stdin; --reply <message-id> to reply)
|
|
1303
|
+
qc react <chat> <message-id> <emoji> [--remove] react to a message
|
|
1178
1304
|
qc listen print new messages as they arrive (NDJSON with --json)
|
|
1179
1305
|
qc mcp run as an MCP server on stdio (list_chats, read_chat, send_message)
|
|
1180
1306
|
|
|
@@ -1192,7 +1318,7 @@ function parseArgs2(argv) {
|
|
|
1192
1318
|
const [k, v] = a.slice(2).split("=", 2);
|
|
1193
1319
|
if (v !== undefined)
|
|
1194
1320
|
args.flags[k] = v;
|
|
1195
|
-
else if (["url", "n", "limit"].includes(k) && argv[i + 1] !== undefined)
|
|
1321
|
+
else if (["url", "n", "limit", "reply"].includes(k) && argv[i + 1] !== undefined)
|
|
1196
1322
|
args.flags[k] = argv[++i];
|
|
1197
1323
|
else
|
|
1198
1324
|
args.flags[k] = true;
|
|
@@ -1292,10 +1418,27 @@ async function main2(argv, { version = "0.0.0" } = {}) {
|
|
|
1292
1418
|
const last = messages.slice(-Math.max(1, Number(flags.n || flags.limit || 20)));
|
|
1293
1419
|
if (flags.json)
|
|
1294
1420
|
return json(last);
|
|
1295
|
-
for (const m of last)
|
|
1296
|
-
|
|
1421
|
+
for (const m of last) {
|
|
1422
|
+
if (m.replyTo)
|
|
1423
|
+
out(` ┃ ${m.replyTo.name ? `${m.replyTo.name}: ` : ""}${m.replyTo.snippet}`);
|
|
1424
|
+
const reactions = m.reactions?.length ? ` [${m.reactions.map((r) => `${r.emoji}${r.count > 1 ? r.count : ""}`).join(" ")}]` : "";
|
|
1425
|
+
out(`[${stamp(m.at)}] ${m.mine ? "you" : m.sender}: ${m.text}${reactions} (${m.id.slice(0, 8)})`);
|
|
1426
|
+
}
|
|
1297
1427
|
return;
|
|
1298
1428
|
}
|
|
1429
|
+
case "react": {
|
|
1430
|
+
const [chatQuery, messageId, emoji] = rest;
|
|
1431
|
+
if (!chatQuery || !messageId || !emoji)
|
|
1432
|
+
throw new Error("Usage: qc react <chat> <message-id> <emoji> [--remove]");
|
|
1433
|
+
const c = await client(flags);
|
|
1434
|
+
const chat = findChat2(await c.conversations(), chatQuery);
|
|
1435
|
+
const { messages } = await c.messages(chat.id);
|
|
1436
|
+
const target = messages.filter((m) => m.id === messageId || m.id.startsWith(messageId));
|
|
1437
|
+
if (target.length !== 1)
|
|
1438
|
+
throw new Error(target.length ? `"${messageId}" matches ${target.length} messages` : `No message "${messageId}" in ${chat.title}`);
|
|
1439
|
+
await c.react(chat.id, target[0].id, emoji, !!flags.remove);
|
|
1440
|
+
return out(`${flags.remove ? "Removed" : "Reacted"} ${emoji} on ${chat.title}.`);
|
|
1441
|
+
}
|
|
1299
1442
|
case "send": {
|
|
1300
1443
|
if (!rest[0] || rest.length < 2)
|
|
1301
1444
|
throw new Error('Usage: qc send <chat> <text> (text "-" reads stdin)');
|
|
@@ -1304,7 +1447,7 @@ async function main2(argv, { version = "0.0.0" } = {}) {
|
|
|
1304
1447
|
throw new Error("Nothing to send.");
|
|
1305
1448
|
const c = await client(flags);
|
|
1306
1449
|
const chat = findChat2(await c.conversations(), rest[0]);
|
|
1307
|
-
const { message, skipped } = await c.send(chat.id, text);
|
|
1450
|
+
const { message, skipped } = await c.send(chat.id, text, { replyTo: flags.reply || undefined });
|
|
1308
1451
|
if (flags.json)
|
|
1309
1452
|
return json({ id: message?.id, conversation: chat.id, skipped });
|
|
1310
1453
|
return out(`Sent to ${chat.title}${skipped ? ` (${skipped} participant(s) have no key yet)` : ""}.`);
|
package/dist/commands.js
CHANGED
package/dist/mcp.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
findChat2
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-c8qkdept.js";
|
|
4
4
|
|
|
5
5
|
// src/cli/mcp.js
|
|
6
6
|
import { createInterface } from "node:readline";
|
|
@@ -30,11 +30,27 @@ var TOOLS = [
|
|
|
30
30
|
type: "object",
|
|
31
31
|
properties: {
|
|
32
32
|
chat: { type: "string", description: "Conversation id or part of its name" },
|
|
33
|
-
text: { type: "string", description: "The message" }
|
|
33
|
+
text: { type: "string", description: "The message" },
|
|
34
|
+
reply_to: { type: "string", description: "Optional: id of the message this replies to (from read_chat)" }
|
|
34
35
|
},
|
|
35
36
|
required: ["chat", "text"],
|
|
36
37
|
additionalProperties: false
|
|
37
38
|
}
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "react_to_message",
|
|
42
|
+
description: "React to a message with an emoji (Signal-style: one reaction per person; a new one replaces yours). End-to-end encrypted.",
|
|
43
|
+
inputSchema: {
|
|
44
|
+
type: "object",
|
|
45
|
+
properties: {
|
|
46
|
+
chat: { type: "string", description: "Conversation id or part of its name" },
|
|
47
|
+
message_id: { type: "string", description: "The message id, from read_chat" },
|
|
48
|
+
emoji: { type: "string", description: "The emoji, e.g. ❤️" },
|
|
49
|
+
remove: { type: "boolean", description: "Withdraw this reaction instead" }
|
|
50
|
+
},
|
|
51
|
+
required: ["chat", "message_id", "emoji"],
|
|
52
|
+
additionalProperties: false
|
|
53
|
+
}
|
|
38
54
|
}
|
|
39
55
|
];
|
|
40
56
|
async function callTool(client, name, args = {}) {
|
|
@@ -46,15 +62,32 @@ async function callTool(client, name, args = {}) {
|
|
|
46
62
|
const chat = findChat2(await client.conversations(), args.chat);
|
|
47
63
|
const { messages } = await client.messages(chat.id, { limit: 100 });
|
|
48
64
|
const limit = Math.min(100, Math.max(1, Number(args.limit) || 20));
|
|
49
|
-
return {
|
|
65
|
+
return {
|
|
66
|
+
chat: { id: chat.id, title: chat.title },
|
|
67
|
+
messages: messages.slice(-limit).map(({ id, sender, mine, text, at, reactions, replyTo }) => ({
|
|
68
|
+
id,
|
|
69
|
+
from: mine ? "me" : sender,
|
|
70
|
+
text,
|
|
71
|
+
at,
|
|
72
|
+
...reactions?.length ? { reactions: reactions.map(({ emoji, count, mine: m }) => ({ emoji, count, mine: m })) } : {},
|
|
73
|
+
...replyTo ? { reply_to: { id: replyTo.id, from: replyTo.name, text: replyTo.snippet } } : {}
|
|
74
|
+
}))
|
|
75
|
+
};
|
|
50
76
|
}
|
|
51
77
|
if (name === "send_message") {
|
|
52
78
|
if (!args.text || typeof args.text !== "string")
|
|
53
79
|
throw new Error("text is required");
|
|
54
80
|
const chat = findChat2(await client.conversations(), args.chat);
|
|
55
|
-
const { message, skipped } = await client.send(chat.id, args.text);
|
|
81
|
+
const { message, skipped } = await client.send(chat.id, args.text, { replyTo: typeof args.reply_to === "string" ? args.reply_to : undefined });
|
|
56
82
|
return { sent: true, id: message?.id, chat: chat.title, skipped };
|
|
57
83
|
}
|
|
84
|
+
if (name === "react_to_message") {
|
|
85
|
+
if (!args.emoji || typeof args.emoji !== "string")
|
|
86
|
+
throw new Error("emoji is required");
|
|
87
|
+
const chat = findChat2(await client.conversations(), args.chat);
|
|
88
|
+
await client.react(chat.id, String(args.message_id), args.emoji, !!args.remove);
|
|
89
|
+
return { reacted: !args.remove, emoji: args.emoji, chat: chat.title };
|
|
90
|
+
}
|
|
58
91
|
throw new Error(`Unknown tool ${name}`);
|
|
59
92
|
}
|
|
60
93
|
async function handle(client, msg, { version }) {
|
package/dist/tui.js
CHANGED
|
@@ -61,11 +61,19 @@ function transcriptLines(messages, width, theme) {
|
|
|
61
61
|
]
|
|
62
62
|
});
|
|
63
63
|
}
|
|
64
|
+
if (m.replyTo) {
|
|
65
|
+
const who = m.replyTo.name ? `${m.replyTo.name}: ` : "";
|
|
66
|
+
lines.push({ text: truncate(` ┃ ${who}${m.replyTo.snippet}`, width), fg: theme.muted });
|
|
67
|
+
}
|
|
64
68
|
for (const part of String(m.text ?? "").split(`
|
|
65
69
|
`)) {
|
|
66
70
|
for (const line of wrap(part, Math.max(4, width - 2)))
|
|
67
71
|
lines.push({ text: ` ${line}`, fg: theme.foreground });
|
|
68
72
|
}
|
|
73
|
+
if (m.reactions?.length) {
|
|
74
|
+
const summary = m.reactions.map((r) => `${r.emoji}${r.count > 1 ? ` ${r.count}` : ""}`).join(" ");
|
|
75
|
+
lines.push({ text: truncate(` ${summary}`, width), fg: m.reactions.some((r) => r.mine) ? theme.accent : theme.muted });
|
|
76
|
+
}
|
|
69
77
|
last = m;
|
|
70
78
|
}
|
|
71
79
|
return lines;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@profullstack/qryptchat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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": {
|