@profullstack/qryptchat 0.12.0 → 0.13.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 +1 -0
- package/dist/{chunk-zh85gmtq.js → chunk-8frhjf08.js} +1 -1
- package/dist/{chunk-hmyhm26d.js → chunk-94qf0rg1.js} +49 -3
- package/dist/{chunk-ycvwj8wy.js → chunk-qcszcng8.js} +1 -1
- package/dist/commands.js +1 -1
- package/dist/mcp.js +21 -1
- package/dist/tui.js +49 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,6 +49,7 @@ The mouse works too: click a chat, scroll the transcript, click an emoji. Unread
|
|
|
49
49
|
```sh
|
|
50
50
|
qc chats # id and name of every chat
|
|
51
51
|
qc read alice -n 20 # the last 20 messages, decrypted
|
|
52
|
+
qc dm @alice "hey, it's me" # start (or reopen) a direct chat with anyone, by @username
|
|
52
53
|
qc send alice "on my way" # encrypted to every participant
|
|
53
54
|
qc send ops "@ri deploy is done" # an @partial matching one person is sent as @riotcoder
|
|
54
55
|
qc mentions ops @al # who @al completes to: @alice, @alina
|
|
@@ -795,6 +795,24 @@ class QcClient {
|
|
|
795
795
|
}));
|
|
796
796
|
return { messages: out, hasMore };
|
|
797
797
|
}
|
|
798
|
+
async searchUsers(query) {
|
|
799
|
+
const q = String(query ?? "").trim().replace(/^@/, "");
|
|
800
|
+
if (!q)
|
|
801
|
+
return [];
|
|
802
|
+
const { users = [] } = await this.request(`/api/users/search?q=${encodeURIComponent(q)}`);
|
|
803
|
+
return users;
|
|
804
|
+
}
|
|
805
|
+
async startDirect(who) {
|
|
806
|
+
const user = pickUser(await this.searchUsers(who), who);
|
|
807
|
+
const { conversation_id: id } = await this.request("/api/chat/conversations", {
|
|
808
|
+
method: "POST",
|
|
809
|
+
body: { type: "direct", participant_ids: [user.id] }
|
|
810
|
+
});
|
|
811
|
+
if (!id)
|
|
812
|
+
throw new QcError(`Could not start a chat with @${user.username}`);
|
|
813
|
+
const chat = (await this.conversations()).find((c) => c.id === id);
|
|
814
|
+
return chat ?? { id, title: user.display_name || user.username, participants: [] };
|
|
815
|
+
}
|
|
798
816
|
async joinGroup(code) {
|
|
799
817
|
return this.request("/api/conversations/join", { method: "POST", body: { code } });
|
|
800
818
|
}
|
|
@@ -922,6 +940,18 @@ function sleep(ms, signal) {
|
|
|
922
940
|
});
|
|
923
941
|
});
|
|
924
942
|
}
|
|
943
|
+
function pickUser(users, query) {
|
|
944
|
+
const q = String(query ?? "").trim().replace(/^@/, "").toLowerCase();
|
|
945
|
+
const exact = users.filter((u) => u.username?.toLowerCase() === q || u.unique_identifier?.toLowerCase() === q);
|
|
946
|
+
if (exact.length === 1)
|
|
947
|
+
return exact[0];
|
|
948
|
+
if (users.length === 1)
|
|
949
|
+
return users[0];
|
|
950
|
+
if (!users.length)
|
|
951
|
+
throw new QcError(`Nobody on qrypt.chat matches "${query}"`, 404);
|
|
952
|
+
const names = users.slice(0, 8).map((u) => `@${u.username}${u.display_name && u.display_name !== u.username ? ` (${u.display_name})` : ""}`);
|
|
953
|
+
throw new QcError(`"${query}" matches ${users.length} people: ${names.join(", ")}${users.length > 8 ? ", …" : ""}. Use their @username.`, 409);
|
|
954
|
+
}
|
|
925
955
|
function conversationTitle(c, me) {
|
|
926
956
|
if (c.name)
|
|
927
957
|
return c.name;
|
|
@@ -1285,10 +1315,12 @@ Usage:
|
|
|
1285
1315
|
qc read <chat> [-n 20] print the last messages of a chat
|
|
1286
1316
|
qc send <chat> <text> send a message (text "-" reads stdin; --reply <message-id> to reply);
|
|
1287
1317
|
an @partial that matches one person becomes their @username
|
|
1318
|
+
qc dm <@user> [text] start (or reopen) a direct chat with anyone on qrypt.chat and send
|
|
1319
|
+
text if given ("-" reads stdin); qc msg is the same. In the client: /msg @user
|
|
1288
1320
|
qc mentions <chat> [@partial] who @ completes to in a chat, best match first (one per line)
|
|
1289
1321
|
qc react <chat> <message-id> <emoji> [--remove] react to a message
|
|
1290
1322
|
qc listen print new messages as they arrive (NDJSON with --json; --notify alerts with it)
|
|
1291
|
-
qc mcp run as an MCP server on stdio (list_chats, read_chat, send_message, list_mentions)
|
|
1323
|
+
qc mcp run as an MCP server on stdio (list_chats, read_chat, send_message, start_direct_chat, list_mentions)
|
|
1292
1324
|
qc join <link> join a group chat from its invite link (https://qrypt.chat/join/...)
|
|
1293
1325
|
qc invite <chat> print a group's invite link (group admins) [--reset makes a new one, --off turns it off]
|
|
1294
1326
|
|
|
@@ -1388,7 +1420,7 @@ async function main2(argv, { version = "0.0.0" } = {}) {
|
|
|
1388
1420
|
}
|
|
1389
1421
|
case "agent": {
|
|
1390
1422
|
if (rest[0] === "call") {
|
|
1391
|
-
const { runVoiceAgent } = await import("./chunk-
|
|
1423
|
+
const { runVoiceAgent } = await import("./chunk-qcszcng8.js");
|
|
1392
1424
|
return runVoiceAgent(await client(flags), {
|
|
1393
1425
|
brain: typeof flags.brain === "string" ? flags.brain : undefined,
|
|
1394
1426
|
voice: typeof flags.voice === "string" ? flags.voice : undefined,
|
|
@@ -1552,7 +1584,7 @@ ${info("Set: qc profile --emoji :telescope: --pronouns she/her --website https:/
|
|
|
1552
1584
|
throw new Error('Usage: qc call <number> (e.g. qc call "(415) 555-1234" or qc call +442079460958)');
|
|
1553
1585
|
const c = await client(flags);
|
|
1554
1586
|
const url = `${c.base}/phone?to=${encodeURIComponent(rest.join(" "))}`;
|
|
1555
|
-
const { openBrowser } = await import("./chunk-
|
|
1587
|
+
const { openBrowser } = await import("./chunk-8frhjf08.js");
|
|
1556
1588
|
const opened = openBrowser(url);
|
|
1557
1589
|
return out(`${mark("send")}${opened === false ? "Open" : "Opened"} ${url}
|
|
1558
1590
|
Calls to phones need your browser for the audio; tap Call there.`);
|
|
@@ -1653,6 +1685,20 @@ ${info("Set: qc profile --emoji :telescope: --pronouns she/her --website https:/
|
|
|
1653
1685
|
out(warn(`@${a.query} could be ${a.matches.join(", ")}: sent as typed.`));
|
|
1654
1686
|
return out(`${mark("send")}Sent to ${chat.title}.`);
|
|
1655
1687
|
}
|
|
1688
|
+
case "dm":
|
|
1689
|
+
case "msg": {
|
|
1690
|
+
if (!rest[0])
|
|
1691
|
+
throw new Error(`Usage: qc ${cmd} <@user> [text] (a username, or a name only one person has)`);
|
|
1692
|
+
const text = rest[1] === "-" && rest.length === 2 ? (await readStdin()).trim() : rest.slice(1).join(" ");
|
|
1693
|
+
const c = await client(flags);
|
|
1694
|
+
const chat = await c.startDirect(rest[0]);
|
|
1695
|
+
const sent = text ? await c.send(chat.id, text) : null;
|
|
1696
|
+
if (flags.json)
|
|
1697
|
+
return json({ conversation: chat.id, title: chat.title, ...sent ? { id: sent.message?.id, skipped: sent.skipped } : {} });
|
|
1698
|
+
if (sent?.skipped)
|
|
1699
|
+
out(warn(`${chat.title} has no key yet and will not see it until they sign in.`));
|
|
1700
|
+
return out(`${mark("send")}${sent ? `Sent to ${chat.title}` : `Chat with ${chat.title} is open (qc send "${chat.title}" <text>)`}.`);
|
|
1701
|
+
}
|
|
1656
1702
|
case "listen": {
|
|
1657
1703
|
const c = await client(flags);
|
|
1658
1704
|
const chats = await c.conversations();
|
package/dist/commands.js
CHANGED
package/dist/mcp.js
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
findChat2,
|
|
4
4
|
mentionMatches2,
|
|
5
5
|
withMentions2
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-94qf0rg1.js";
|
|
7
7
|
|
|
8
8
|
// src/cli/mcp.js
|
|
9
9
|
import { createInterface } from "node:readline";
|
|
@@ -40,6 +40,19 @@ var TOOLS = [
|
|
|
40
40
|
additionalProperties: false
|
|
41
41
|
}
|
|
42
42
|
},
|
|
43
|
+
{
|
|
44
|
+
name: "start_direct_chat",
|
|
45
|
+
description: "Start (or reopen) an end-to-end encrypted direct chat with anyone on qrypt.chat, by @username or a name only one person has, and optionally send the first message. Returns the chat id for read_chat/send_message.",
|
|
46
|
+
inputSchema: {
|
|
47
|
+
type: "object",
|
|
48
|
+
properties: {
|
|
49
|
+
user: { type: "string", description: "Their @username (or a name that matches only them)" },
|
|
50
|
+
text: { type: "string", description: "Optional: the first message" }
|
|
51
|
+
},
|
|
52
|
+
required: ["user"],
|
|
53
|
+
additionalProperties: false
|
|
54
|
+
}
|
|
55
|
+
},
|
|
43
56
|
{
|
|
44
57
|
name: "list_mentions",
|
|
45
58
|
description: "Who an @mention can complete to in a conversation, best match first (like typing @ali then Tab). Without partial: everyone you can mention.",
|
|
@@ -98,6 +111,13 @@ async function callTool(client, name, args = {}) {
|
|
|
98
111
|
const { message, skipped } = await client.send(chat.id, m.text, { replyTo: typeof args.reply_to === "string" ? args.reply_to : undefined });
|
|
99
112
|
return { sent: true, id: message?.id, chat: chat.title, skipped, ...m.mentions.length ? { mentions: m.mentions } : {}, ...m.ambiguous.length ? { ambiguous: m.ambiguous } : {} };
|
|
100
113
|
}
|
|
114
|
+
if (name === "start_direct_chat") {
|
|
115
|
+
if (!args.user || typeof args.user !== "string")
|
|
116
|
+
throw new Error("user is required");
|
|
117
|
+
const chat = await client.startDirect(args.user);
|
|
118
|
+
const sent = typeof args.text === "string" && args.text.trim() ? await client.send(chat.id, args.text.trim()) : null;
|
|
119
|
+
return { chat: chat.title, id: chat.id, ...sent ? { sent: true, message_id: sent.message?.id, skipped: sent.skipped } : {} };
|
|
120
|
+
}
|
|
101
121
|
if (name === "list_mentions") {
|
|
102
122
|
const chat = findChat2(await client.conversations(), args.chat);
|
|
103
123
|
const matches = mentionMatches2(chat, client.me, typeof args.partial === "string" ? args.partial : "");
|
package/dist/tui.js
CHANGED
|
@@ -74,7 +74,12 @@ function chatsWithNews(prev, next) {
|
|
|
74
74
|
const seen = new Map(prev.map((c) => [c.id, c.updated_at ?? ""]));
|
|
75
75
|
return next.filter((c) => !seen.has(c.id) || (c.updated_at ?? "") > seen.get(c.id)).map((c) => c.id);
|
|
76
76
|
}
|
|
77
|
-
var SLASH_COMMANDS = ["voice", "archive", "unarchive", "archived", "names", "list"];
|
|
77
|
+
var SLASH_COMMANDS = ["voice", "archive", "unarchive", "archived", "names", "list", "msg", "dm", "query", "new"];
|
|
78
|
+
var NEW_CHAT_COMMANDS = ["msg", "dm", "query", "new"];
|
|
79
|
+
function parseNewChat(args) {
|
|
80
|
+
const m = /^(\S+)(?:\s+([\s\S]*))?$/.exec(String(args ?? "").trim());
|
|
81
|
+
return m ? { who: m[1], text: (m[2] ?? "").trim() } : null;
|
|
82
|
+
}
|
|
78
83
|
var pad2 = (n) => String(n).padStart(2, "0");
|
|
79
84
|
var timeOf = (iso) => {
|
|
80
85
|
const d = new Date(iso);
|
|
@@ -132,7 +137,8 @@ function drawTranscript(surface, state) {
|
|
|
132
137
|
const theme = surface.theme;
|
|
133
138
|
const messages = state.messages[state.activeId] || [];
|
|
134
139
|
if (!state.activeId) {
|
|
135
|
-
|
|
140
|
+
const hint = state.loading ? "Loading your chats…" : state.conversations.length ? "Pick a chat on the left, or type /msg @name to start one." : "No chats yet. Type /msg @name to message someone.";
|
|
141
|
+
surface.text(1, 1, hint, { fg: theme.muted });
|
|
136
142
|
return;
|
|
137
143
|
}
|
|
138
144
|
if (!messages.length) {
|
|
@@ -217,7 +223,7 @@ function render({ ui, width, height, theme }, state, on = {}) {
|
|
|
217
223
|
p.textInput({
|
|
218
224
|
value: state.field.value,
|
|
219
225
|
cursor: state.field.cursor,
|
|
220
|
-
placeholder: active ? `Message ${active.title} (@name Tab, Ctrl+E emoji,
|
|
226
|
+
placeholder: active ? `Message ${active.title} (@name Tab, Ctrl+E emoji, /msg @name for a new chat)` : "/msg @name to start a chat",
|
|
221
227
|
focused: state.focus === "compose" && !state.picker,
|
|
222
228
|
size: 1
|
|
223
229
|
});
|
|
@@ -229,7 +235,7 @@ function render({ ui, width, height, theme }, state, on = {}) {
|
|
|
229
235
|
{ key: "Ctrl+E", label: "emoji" },
|
|
230
236
|
{ key: "Tab", label: state.focus === "list" ? "compose" : mention?.matches.length ? "complete @" : "chats" },
|
|
231
237
|
{ key: "PgUp", label: "scroll" },
|
|
232
|
-
...state.focus === "list" ? [{ key: "a", label: state.showArchived ? "unarchive" : "archive" }] : [],
|
|
238
|
+
...state.focus === "list" ? [{ key: "n", label: "new chat" }, { key: "a", label: state.showArchived ? "unarchive" : "archive" }] : [],
|
|
233
239
|
{ key: "Ctrl+O", label: state.showArchived ? "chats" : "archived" },
|
|
234
240
|
{ key: "Ctrl+C", label: "quit" }
|
|
235
241
|
],
|
|
@@ -381,13 +387,43 @@ async function runTui(client, { initialChat } = {}) {
|
|
|
381
387
|
note(`Could not ${on ? "archive" : "unarchive"}: ${err.message}`);
|
|
382
388
|
}
|
|
383
389
|
}
|
|
390
|
+
function openById(id) {
|
|
391
|
+
const chat = state.conversations.find((c) => c.id === id);
|
|
392
|
+
if (!chat)
|
|
393
|
+
return;
|
|
394
|
+
state.showArchived = !!chat.is_archived;
|
|
395
|
+
openAt(listed(state).findIndex((c) => c.id === id));
|
|
396
|
+
state.focus = "compose";
|
|
397
|
+
}
|
|
398
|
+
async function startChat(args) {
|
|
399
|
+
const want = parseNewChat(args);
|
|
400
|
+
if (!want)
|
|
401
|
+
return note("Usage: /msg @name [message] (a username, or a name only one person has)");
|
|
402
|
+
note(`Looking up ${want.who}…`, { sticky: true });
|
|
403
|
+
try {
|
|
404
|
+
const chat = await client.startDirect(want.who);
|
|
405
|
+
if (!state.conversations.some((c) => c.id === chat.id))
|
|
406
|
+
state.conversations = [chat, ...state.conversations];
|
|
407
|
+
openById(chat.id);
|
|
408
|
+
note(`Chat with ${chat.title} is open`);
|
|
409
|
+
if (want.text) {
|
|
410
|
+
state.field = { value: want.text, cursor: want.text.length };
|
|
411
|
+
await send({ text: true });
|
|
412
|
+
}
|
|
413
|
+
} catch (err) {
|
|
414
|
+
note(err.message, { sticky: err.status === 409 });
|
|
415
|
+
}
|
|
416
|
+
redraw();
|
|
417
|
+
}
|
|
384
418
|
function runSlash(raw) {
|
|
385
419
|
const cmd = parseSlashCommand(raw, SLASH_COMMANDS);
|
|
386
420
|
if (!cmd)
|
|
387
421
|
return false;
|
|
388
422
|
const active = state.conversations.find((c) => c.id === state.activeId);
|
|
389
423
|
state.field = { value: "", cursor: 0 };
|
|
390
|
-
if (cmd.name
|
|
424
|
+
if (NEW_CHAT_COMMANDS.includes(cmd.name))
|
|
425
|
+
startChat(cmd.args);
|
|
426
|
+
else if (cmd.name === "archived")
|
|
391
427
|
toggleArchivedView();
|
|
392
428
|
else if (!active)
|
|
393
429
|
note("Pick a chat first");
|
|
@@ -407,8 +443,8 @@ async function runTui(client, { initialChat } = {}) {
|
|
|
407
443
|
redraw();
|
|
408
444
|
return true;
|
|
409
445
|
}
|
|
410
|
-
async function send({ raw = false } = {}) {
|
|
411
|
-
if (!raw && runSlash(state.field.value))
|
|
446
|
+
async function send({ raw = false, text: plain = false } = {}) {
|
|
447
|
+
if (!raw && !plain && runSlash(state.field.value))
|
|
412
448
|
return;
|
|
413
449
|
const text = raw ? state.field.value.trim() : emojify(state.field.value.trim(), { mode: "emoji" });
|
|
414
450
|
if (!text || !state.activeId)
|
|
@@ -492,7 +528,10 @@ async function runTui(client, { initialChat } = {}) {
|
|
|
492
528
|
state.selected = Math.min(listed(state).length - 1, state.selected + 1);
|
|
493
529
|
else if (event.key === "enter" || event.key === "right" || event.key === "l")
|
|
494
530
|
handlers.open(state.selected);
|
|
495
|
-
else if (event.key === "
|
|
531
|
+
else if (event.key === "n") {
|
|
532
|
+
state.field = { value: "/msg @", cursor: 6 };
|
|
533
|
+
state.focus = "compose";
|
|
534
|
+
} else if (event.key === "a") {
|
|
496
535
|
const c = listed(state)[state.selected];
|
|
497
536
|
if (c)
|
|
498
537
|
setArchived(c, !c.is_archived);
|
|
@@ -585,11 +624,13 @@ async function runTui(client, { initialChat } = {}) {
|
|
|
585
624
|
}
|
|
586
625
|
export {
|
|
587
626
|
CHAT_LIST_REFRESH_MS,
|
|
627
|
+
NEW_CHAT_COMMANDS,
|
|
588
628
|
SLASH_COMMANDS,
|
|
589
629
|
chatsWithNews,
|
|
590
630
|
initialState,
|
|
591
631
|
listed,
|
|
592
632
|
mentionSuggestions,
|
|
633
|
+
parseNewChat,
|
|
593
634
|
render,
|
|
594
635
|
runTui,
|
|
595
636
|
tabComplete,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@profullstack/qryptchat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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": {
|