@profullstack/qryptchat 0.1.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.
@@ -0,0 +1,40 @@
1
+ // src/cli/config.js
2
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ var DEFAULT_URL = "https://qrypt.chat";
6
+ function configDir(env = process.env) {
7
+ if (env.QC_HOME)
8
+ return env.QC_HOME;
9
+ return join(env.XDG_CONFIG_HOME || join(homedir(), ".config"), "qc");
10
+ }
11
+ var sessionPath = (env = process.env) => join(configDir(env), "session.json");
12
+ function baseUrl(session, env = process.env) {
13
+ return (env.QC_URL || session?.base || DEFAULT_URL).replace(/\/+$/, "");
14
+ }
15
+ function loadSession(env = process.env) {
16
+ try {
17
+ const session = JSON.parse(readFileSync(sessionPath(env), "utf8"));
18
+ return session && session.access_token && session.keys ? session : null;
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+ function saveSession(session, env = process.env) {
24
+ const dir = configDir(env);
25
+ mkdirSync(dir, { recursive: true, mode: 448 });
26
+ try {
27
+ chmodSync(dir, 448);
28
+ } catch {}
29
+ const file = sessionPath(env);
30
+ const tmp = `${file}.${process.pid}.tmp`;
31
+ writeFileSync(tmp, JSON.stringify(session, null, 2), { mode: 384 });
32
+ renameSync(tmp, file);
33
+ }
34
+ function clearSession(env = process.env) {
35
+ const file = sessionPath(env);
36
+ if (existsSync(file))
37
+ rmSync(file);
38
+ }
39
+
40
+ export { configDir, sessionPath, baseUrl, loadSession, saveSession, clearSession };
@@ -0,0 +1,12 @@
1
+ import {
2
+ HELP2,
3
+ parseArgs2,
4
+ findChat2,
5
+ main2
6
+ } from "./chunk-9kkxje88.js";
7
+ export {
8
+ HELP2 as HELP,
9
+ findChat2 as findChat,
10
+ main2 as main,
11
+ parseArgs2 as parseArgs
12
+ };
package/dist/mcp.js ADDED
@@ -0,0 +1,118 @@
1
+ import {
2
+ findChat2
3
+ } from "./chunk-9kkxje88.js";
4
+
5
+ // src/cli/mcp.js
6
+ import { createInterface } from "node:readline";
7
+ var TOOLS = [
8
+ {
9
+ name: "list_chats",
10
+ description: "List your qrypt.chat conversations (id, title, participant count), most recent first.",
11
+ inputSchema: { type: "object", properties: {}, additionalProperties: false }
12
+ },
13
+ {
14
+ name: "read_chat",
15
+ description: "Read the latest decrypted messages of a conversation.",
16
+ inputSchema: {
17
+ type: "object",
18
+ properties: {
19
+ chat: { type: "string", description: "Conversation id or part of its name" },
20
+ limit: { type: "number", description: "How many of the latest messages (default 20, max 100)" }
21
+ },
22
+ required: ["chat"],
23
+ additionalProperties: false
24
+ }
25
+ },
26
+ {
27
+ name: "send_message",
28
+ description: "Send an end-to-end encrypted message (ML-KEM-1024) to a conversation.",
29
+ inputSchema: {
30
+ type: "object",
31
+ properties: {
32
+ chat: { type: "string", description: "Conversation id or part of its name" },
33
+ text: { type: "string", description: "The message" }
34
+ },
35
+ required: ["chat", "text"],
36
+ additionalProperties: false
37
+ }
38
+ }
39
+ ];
40
+ async function callTool(client, name, args = {}) {
41
+ if (name === "list_chats") {
42
+ const chats = await client.conversations();
43
+ return chats.map((c) => ({ id: c.id, title: c.title, participants: c.participants?.length ?? 0, updated_at: c.updated_at }));
44
+ }
45
+ if (name === "read_chat") {
46
+ const chat = findChat2(await client.conversations(), args.chat);
47
+ const { messages } = await client.messages(chat.id, { limit: 100 });
48
+ const limit = Math.min(100, Math.max(1, Number(args.limit) || 20));
49
+ return { chat: { id: chat.id, title: chat.title }, messages: messages.slice(-limit).map(({ sender, mine, text, at }) => ({ from: mine ? "me" : sender, text, at })) };
50
+ }
51
+ if (name === "send_message") {
52
+ if (!args.text || typeof args.text !== "string")
53
+ throw new Error("text is required");
54
+ const chat = findChat2(await client.conversations(), args.chat);
55
+ const { message, skipped } = await client.send(chat.id, args.text);
56
+ return { sent: true, id: message?.id, chat: chat.title, skipped };
57
+ }
58
+ throw new Error(`Unknown tool ${name}`);
59
+ }
60
+ async function handle(client, msg, { version }) {
61
+ const { id, method, params } = msg;
62
+ switch (method) {
63
+ case "initialize":
64
+ return {
65
+ protocolVersion: params?.protocolVersion ?? "2025-06-18",
66
+ capabilities: { tools: {} },
67
+ serverInfo: { name: "qc", title: "qrypt.chat", version }
68
+ };
69
+ case "ping":
70
+ return {};
71
+ case "tools/list":
72
+ return { tools: TOOLS };
73
+ case "tools/call":
74
+ try {
75
+ const result = await callTool(client, params?.name, params?.arguments);
76
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], structuredContent: result };
77
+ } catch (err) {
78
+ return { content: [{ type: "text", text: err.message }], isError: true };
79
+ }
80
+ default:
81
+ if (id === undefined)
82
+ return;
83
+ throw Object.assign(new Error(`Method not found: ${method}`), { code: -32601 });
84
+ }
85
+ }
86
+ function serveMcp(client, { version = "0.0.0", input = process.stdin, output = process.stdout } = {}) {
87
+ for (const m of ["log", "info", "debug"])
88
+ console[m] = (...a) => process.stderr.write(`${a.join(" ")}
89
+ `);
90
+ const write = (obj) => output.write(`${JSON.stringify(obj)}
91
+ `);
92
+ const rl = createInterface({ input });
93
+ rl.on("line", async (line) => {
94
+ if (!line.trim())
95
+ return;
96
+ let msg;
97
+ try {
98
+ msg = JSON.parse(line);
99
+ } catch {
100
+ write({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } });
101
+ return;
102
+ }
103
+ try {
104
+ const result = await handle(client, msg, { version });
105
+ if (msg.id !== undefined && result !== undefined)
106
+ write({ jsonrpc: "2.0", id: msg.id, result });
107
+ } catch (err) {
108
+ if (msg.id !== undefined)
109
+ write({ jsonrpc: "2.0", id: msg.id, error: { code: err.code ?? -32603, message: err.message } });
110
+ }
111
+ });
112
+ return new Promise((resolve) => rl.on("close", resolve));
113
+ }
114
+ export {
115
+ callTool,
116
+ handle,
117
+ serveMcp
118
+ };
package/dist/tui.js ADDED
@@ -0,0 +1,426 @@
1
+ import {
2
+ configDir
3
+ } from "./chunk-fb6zrz6m.js";
4
+
5
+ // src/cli/tui.js
6
+ import { createApp, editText, emojify, insertText, stringWidth, truncate, widgets, wrap } from "@profullstack/hqtui";
7
+ import { readFileSync, writeFileSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ var SIDEBAR = 30;
10
+ function initialState(me) {
11
+ return {
12
+ me,
13
+ conversations: [],
14
+ selected: 0,
15
+ activeId: null,
16
+ messages: {},
17
+ unread: {},
18
+ typing: {},
19
+ focus: "compose",
20
+ field: { value: "", cursor: 0 },
21
+ picker: null,
22
+ recent: [],
23
+ scrollBack: 0,
24
+ status: "connecting",
25
+ notice: "",
26
+ loading: true
27
+ };
28
+ }
29
+ var pad2 = (n) => String(n).padStart(2, "0");
30
+ var timeOf = (iso) => {
31
+ const d = new Date(iso);
32
+ return `${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
33
+ };
34
+ var dayOf = (iso) => new Date(iso).toLocaleDateString(undefined, { weekday: "short", day: "numeric", month: "short" });
35
+ function nameColor(theme, key) {
36
+ const palette = [theme.accent, theme.info, theme.success, theme.warning, theme.secondary, theme.primary];
37
+ let h = 0;
38
+ for (const ch of key)
39
+ h = h * 31 + ch.codePointAt(0) >>> 0;
40
+ return palette[h % palette.length];
41
+ }
42
+ function transcriptLines(messages, width, theme) {
43
+ const lines = [];
44
+ let day = "";
45
+ let last = null;
46
+ for (const m of messages) {
47
+ const d = dayOf(m.at);
48
+ if (d !== day) {
49
+ day = d;
50
+ const label = ` ${d} `;
51
+ const side = Math.max(2, Math.floor((width - stringWidth(label)) / 2));
52
+ lines.push({ text: `${"─".repeat(side)}${label}${"─".repeat(Math.max(0, width - side - stringWidth(label)))}`, fg: theme.border });
53
+ last = null;
54
+ }
55
+ const sameRun = last && last.senderId === m.senderId && new Date(m.at) - new Date(last.at) < 5 * 60 * 1000;
56
+ if (!sameRun) {
57
+ lines.push({
58
+ spans: [
59
+ { text: m.mine ? "you" : m.sender, fg: m.mine ? theme.primary : nameColor(theme, m.senderId || m.sender), bold: true },
60
+ { text: ` ${timeOf(m.at)}`, fg: theme.muted }
61
+ ]
62
+ });
63
+ }
64
+ for (const part of String(m.text ?? "").split(`
65
+ `)) {
66
+ for (const line of wrap(part, Math.max(4, width - 2)))
67
+ lines.push({ text: ` ${line}`, fg: theme.foreground });
68
+ }
69
+ last = m;
70
+ }
71
+ return lines;
72
+ }
73
+ function drawTranscript(surface, state) {
74
+ const theme = surface.theme;
75
+ const messages = state.messages[state.activeId] || [];
76
+ if (!state.activeId) {
77
+ surface.text(1, 1, state.loading ? "Loading your chats…" : "Pick a chat on the left.", { fg: theme.muted });
78
+ return;
79
+ }
80
+ if (!messages.length) {
81
+ surface.text(1, 1, "No messages yet. Say hello \uD83D\uDC4B", { fg: theme.muted });
82
+ return;
83
+ }
84
+ const lines = transcriptLines(messages, surface.width, theme);
85
+ const start = Math.max(0, lines.length - surface.height - state.scrollBack);
86
+ const top = Math.max(0, surface.height - (lines.length - start));
87
+ for (let y = top;y < surface.height; y++) {
88
+ const line = lines[start + y - top];
89
+ if (!line)
90
+ break;
91
+ if (line.spans) {
92
+ let x = 0;
93
+ for (const span of line.spans)
94
+ x += surface.text(x, y, span.text, { fg: span.fg, attrs: span.bold ? 1 : 0 });
95
+ } else {
96
+ surface.text(0, y, truncate(line.text, surface.width), { fg: line.fg });
97
+ }
98
+ }
99
+ if (state.scrollBack > 0) {
100
+ const hint = ` ↓ ${state.scrollBack} more `;
101
+ surface.text(Math.max(0, surface.width - stringWidth(hint)), surface.height - 1, hint, { fg: theme.background, bg: theme.accent });
102
+ }
103
+ }
104
+ function render({ ui, width, height, theme }, state, on = {}) {
105
+ const active = state.conversations.find((c) => c.id === state.activeId);
106
+ const typingNames = Object.values(state.typing[state.activeId] || {}).map((t) => t.name);
107
+ ui.row({ size: 1 }, (bar) => {
108
+ bar.draw((s) => {
109
+ const t = s.theme;
110
+ s.fillRect(0, 0, s.width, 1, { bg: t.surface ?? t.background });
111
+ let x = s.text(0, 0, " \uD83D\uDD12 qrypt.chat ", { fg: t.background, bg: t.primary, attrs: 1 });
112
+ x += s.text(x, 0, ` @${state.me?.username ?? "?"}`, { fg: t.foreground, bg: t.surface ?? t.background });
113
+ if (state.notice)
114
+ s.text(x + 2, 0, truncate(state.notice, Math.max(0, s.width - x - 16)), { fg: t.warning, bg: t.surface ?? t.background });
115
+ const live = { live: ["●", "live", t.success], connecting: ["◌", "connecting", t.warning], offline: ["○", "offline", t.danger] }[state.status] ?? ["○", state.status, t.muted];
116
+ const label = `${live[0]} ${live[1]} `;
117
+ s.text(s.width - stringWidth(label), 0, label, { fg: live[2], bg: t.surface ?? t.background });
118
+ });
119
+ });
120
+ ui.row({ size: "fill" }, (row) => {
121
+ const listWidth = Math.min(SIDEBAR, Math.max(18, Math.floor(width * 0.3)));
122
+ row.panel({ title: "Chats", size: listWidth, borderColor: state.focus === "list" ? theme?.borderFocused : undefined }, (p) => {
123
+ const inner = Math.max(4, listWidth - 5);
124
+ p.list({
125
+ items: state.conversations.map((c) => {
126
+ const unread = state.unread[c.id] || 0;
127
+ const tag = unread ? ` ●${unread > 99 ? "99+" : unread}` : "";
128
+ const name = truncate(c.title, inner - stringWidth(tag));
129
+ return {
130
+ label: `${name}${" ".repeat(Math.max(0, inner - stringWidth(name) - stringWidth(tag)))}${tag}`,
131
+ color: unread ? theme?.accent : undefined
132
+ };
133
+ }),
134
+ selected: state.selected,
135
+ followSelection: true,
136
+ scrollbar: true,
137
+ onSelectRow: (i) => on.open?.(i),
138
+ onActivateRow: (i) => on.open?.(i)
139
+ });
140
+ });
141
+ const people = active?.participants?.length ?? 0;
142
+ row.panel({
143
+ title: active?.title ?? "qc",
144
+ subtitle: active ? `${people} ${people === 1 ? "person" : "people"} · ML-KEM-1024` : "end-to-end encrypted",
145
+ borderColor: state.focus === "compose" ? theme?.borderFocused : undefined
146
+ }, (p) => {
147
+ p.draw((s) => {
148
+ drawTranscript(s, state);
149
+ p.ctx.hit({ rect: s.hitRect(), onScroll: (delta) => on.scroll?.(-delta) });
150
+ }, { size: "fill" });
151
+ p.label(typingNames.length ? `${typingNames.join(", ")} ${typingNames.length === 1 ? "is" : "are"} typing…` : " ", { size: 1 });
152
+ p.textInput({
153
+ value: state.field.value,
154
+ cursor: state.field.cursor,
155
+ placeholder: active ? `Message ${active.title} (Ctrl+E emoji, :rocket: works too)` : "Pick a chat first",
156
+ focused: state.focus === "compose" && !state.picker,
157
+ size: 1
158
+ });
159
+ });
160
+ });
161
+ ui.statusBar({
162
+ items: [
163
+ { key: "Enter", label: state.focus === "list" ? "open" : "send" },
164
+ { key: "Ctrl+E", label: "emoji" },
165
+ { key: "Tab", label: state.focus === "list" ? "compose" : "chats" },
166
+ { key: "PgUp", label: "scroll" },
167
+ { key: "Ctrl+C", label: "quit" }
168
+ ],
169
+ size: 1
170
+ });
171
+ if (state.picker) {
172
+ const size = widgets.emojiPickerSize(10, 7);
173
+ ui.emojiPicker({
174
+ state: state.picker,
175
+ recent: state.recent,
176
+ x: Math.min(Math.max(0, width - size.width - 1), Math.min(SIDEBAR, Math.floor(width * 0.3)) + 1),
177
+ y: Math.max(0, height - size.height - 3),
178
+ onChange: (s) => on.pickerChange?.(s),
179
+ onPick: (e) => on.pick?.(e),
180
+ onClose: () => on.pickerClose?.()
181
+ });
182
+ }
183
+ }
184
+ var recentFile = () => join(configDir(), "recent-emoji.json");
185
+ function loadRecent() {
186
+ try {
187
+ const list = JSON.parse(readFileSync(recentFile(), "utf8"));
188
+ return Array.isArray(list) ? list.filter((c) => typeof c === "string").slice(0, 30) : [];
189
+ } catch {
190
+ return [];
191
+ }
192
+ }
193
+ function saveRecent(list) {
194
+ try {
195
+ writeFileSync(recentFile(), JSON.stringify(list), { mode: 384 });
196
+ } catch {}
197
+ }
198
+ async function runTui(client, { initialChat } = {}) {
199
+ const state = initialState(client.me);
200
+ state.recent = loadRecent();
201
+ const abort = new AbortController;
202
+ const app = await createApp({ quitKeys: ["ctrl+c"], focusNavigation: false });
203
+ const redraw = () => app.invalidate();
204
+ const note = (msg) => {
205
+ state.notice = msg;
206
+ redraw();
207
+ if (msg)
208
+ setTimeout(() => {
209
+ if (state.notice === msg) {
210
+ state.notice = "";
211
+ redraw();
212
+ }
213
+ }, 6000);
214
+ };
215
+ for (const m of ["log", "info", "warn", "error", "debug"])
216
+ console[m] = () => {};
217
+ async function loadMessages(id) {
218
+ try {
219
+ const { messages } = await client.messages(id, { limit: 100 });
220
+ state.messages[id] = messages;
221
+ redraw();
222
+ } catch (err) {
223
+ note(`Could not load messages: ${err.message}`);
224
+ }
225
+ }
226
+ async function loadConversations() {
227
+ try {
228
+ const before = state.activeId;
229
+ state.conversations = await client.conversations();
230
+ state.loading = false;
231
+ if (!state.activeId && state.conversations.length) {
232
+ const want = initialChat ? state.conversations.findIndex((c) => c.id === initialChat || c.title.toLowerCase().includes(String(initialChat).toLowerCase())) : 0;
233
+ openAt(Math.max(0, want));
234
+ } else {
235
+ state.selected = Math.max(0, state.conversations.findIndex((c) => c.id === before));
236
+ }
237
+ redraw();
238
+ for (const c of state.conversations.slice(0, 40)) {
239
+ if (c.id !== state.activeId && !state.messages[c.id])
240
+ await loadMessages(c.id);
241
+ }
242
+ } catch (err) {
243
+ state.loading = false;
244
+ note(err.status === 401 ? "Session ended: run qc login" : `Could not load chats: ${err.message}`);
245
+ }
246
+ }
247
+ function openAt(i) {
248
+ const c = state.conversations[i];
249
+ if (!c)
250
+ return;
251
+ state.selected = i;
252
+ state.activeId = c.id;
253
+ state.unread[c.id] = 0;
254
+ state.scrollBack = 0;
255
+ if (!state.messages[c.id])
256
+ loadMessages(c.id);
257
+ redraw();
258
+ }
259
+ let typingSent = 0;
260
+ function typed() {
261
+ if (!state.activeId)
262
+ return;
263
+ const now = Date.now();
264
+ if (now - typingSent > 3000) {
265
+ typingSent = now;
266
+ client.typing(state.activeId, true);
267
+ }
268
+ }
269
+ async function send() {
270
+ const text = emojify(state.field.value.trim(), { mode: "emoji" });
271
+ if (!text || !state.activeId)
272
+ return;
273
+ const id = state.activeId;
274
+ state.field = { value: "", cursor: 0 };
275
+ state.scrollBack = 0;
276
+ const pending = { id: `pending-${Date.now()}`, senderId: client.me?.id, sender: "you", mine: true, text, at: new Date().toISOString() };
277
+ state.messages[id] = [...state.messages[id] || [], pending];
278
+ redraw();
279
+ try {
280
+ const { skipped } = await client.send(id, text);
281
+ if (skipped)
282
+ note(`${skipped} participant(s) have no key yet and will not see this`);
283
+ client.typing(id, false);
284
+ typingSent = 0;
285
+ await loadMessages(id);
286
+ } catch (err) {
287
+ state.messages[id] = (state.messages[id] || []).filter((m) => m !== pending);
288
+ state.field = { value: text, cursor: text.length };
289
+ note(`Not sent: ${err.message}`);
290
+ }
291
+ }
292
+ const handlers = {
293
+ select: (i) => {
294
+ state.selected = i;
295
+ redraw();
296
+ },
297
+ open: (i) => {
298
+ openAt(i);
299
+ state.focus = "compose";
300
+ },
301
+ scroll: (delta) => {
302
+ state.scrollBack = Math.max(0, state.scrollBack + delta * 3);
303
+ redraw();
304
+ },
305
+ pickerChange: (s) => {
306
+ state.picker = s;
307
+ redraw();
308
+ },
309
+ pick: (e) => {
310
+ state.field = insertText(state.field, e.char);
311
+ state.recent = [e.char, ...state.recent.filter((c) => c !== e.char)].slice(0, 30);
312
+ saveRecent(state.recent);
313
+ state.picker = null;
314
+ typed();
315
+ redraw();
316
+ },
317
+ pickerClose: () => {
318
+ state.picker = null;
319
+ redraw();
320
+ }
321
+ };
322
+ app.on("key", (event) => {
323
+ if (state.picker)
324
+ return;
325
+ if (event.key === "ctrl+e") {
326
+ state.picker = widgets.createEmojiPicker(state.recent);
327
+ } else if (event.key === "tab" || event.key === "shift+tab" || event.key === "escape" && state.focus === "compose") {
328
+ state.focus = state.focus === "list" ? "compose" : "list";
329
+ } else if (event.key === "pageup") {
330
+ handlers.scroll(4);
331
+ } else if (event.key === "pagedown") {
332
+ handlers.scroll(-4);
333
+ } else if (event.key === "ctrl+n" || event.key === "alt+down") {
334
+ openAt(Math.min(state.conversations.length - 1, state.selected + 1));
335
+ } else if (event.key === "ctrl+p" || event.key === "alt+up") {
336
+ openAt(Math.max(0, state.selected - 1));
337
+ } else if (event.key === "ctrl+r") {
338
+ loadConversations();
339
+ if (state.activeId)
340
+ loadMessages(state.activeId);
341
+ } else if (state.focus === "list") {
342
+ if (event.key === "up" || event.key === "k")
343
+ state.selected = Math.max(0, state.selected - 1);
344
+ else if (event.key === "down" || event.key === "j")
345
+ state.selected = Math.min(state.conversations.length - 1, state.selected + 1);
346
+ else if (event.key === "enter" || event.key === "right" || event.key === "l")
347
+ handlers.open(state.selected);
348
+ } else if (event.key === "enter") {
349
+ send();
350
+ } else if (event.key === "up" && !state.field.value) {
351
+ handlers.scroll(1);
352
+ } else if (event.key === "down" && !state.field.value) {
353
+ handlers.scroll(-1);
354
+ } else {
355
+ const next = editText(state.field, event);
356
+ if (next) {
357
+ if (next.value !== state.field.value)
358
+ typed();
359
+ state.field = next;
360
+ }
361
+ }
362
+ redraw();
363
+ });
364
+ app.on("paste", (event) => {
365
+ if (state.picker || state.focus !== "compose")
366
+ return;
367
+ state.field = insertText(state.field, event.text);
368
+ redraw();
369
+ });
370
+ app.on("exit", () => abort.abort());
371
+ app.render((args) => render(args, state, handlers));
372
+ loadConversations();
373
+ client.events(({ type, data }) => {
374
+ if (type === "NEW_MESSAGE") {
375
+ const id = data?.message?.conversation_id;
376
+ if (!id)
377
+ return;
378
+ if (id === state.activeId)
379
+ loadMessages(id);
380
+ else {
381
+ if (data.message.sender_id !== client.me?.id)
382
+ state.unread[id] = (state.unread[id] || 0) + 1;
383
+ delete state.messages[id];
384
+ }
385
+ const i = state.conversations.findIndex((c) => c.id === id);
386
+ if (i > 0) {
387
+ const [c] = state.conversations.splice(i, 1);
388
+ state.conversations.unshift(c);
389
+ state.selected = state.conversations.findIndex((x) => x.id === state.activeId);
390
+ }
391
+ if (i === -1)
392
+ loadConversations();
393
+ if (data.message.sender_id !== client.me?.id)
394
+ delete (state.typing[id] || {})[data.message.sender_id];
395
+ } else if (type === "USER_TYPING" && data?.conversationId) {
396
+ const room = state.typing[data.conversationId] ||= {};
397
+ if (data.isTyping) {
398
+ room[data.userId] = { name: data.displayName || data.username || "someone", until: Date.now() + 6000 };
399
+ setTimeout(() => {
400
+ if (room[data.userId]?.until <= Date.now()) {
401
+ delete room[data.userId];
402
+ redraw();
403
+ }
404
+ }, 6100);
405
+ } else
406
+ delete room[data.userId];
407
+ } else if (type === "CONVERSATION_CREATED") {
408
+ loadConversations();
409
+ }
410
+ redraw();
411
+ }, {
412
+ signal: abort.signal,
413
+ onStatus: (s) => {
414
+ state.status = s;
415
+ redraw();
416
+ }
417
+ }).catch((err) => note(err.message));
418
+ await app.start();
419
+ abort.abort();
420
+ }
421
+ export {
422
+ initialState,
423
+ render,
424
+ runTui,
425
+ transcriptLines
426
+ };
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@profullstack/qryptchat",
3
+ "version": "0.1.0",
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
+ "type": "module",
6
+ "bin": {
7
+ "qc": "./bin/qc.js",
8
+ "qryptchat": "./bin/qc.js"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "dist",
13
+ "README.md"
14
+ ],
15
+ "engines": {
16
+ "node": ">=22"
17
+ },
18
+ "dependencies": {
19
+ "@noble/ciphers": "^2.0.0",
20
+ "@profullstack/hqtui": "^0.8.0",
21
+ "mlkem": "^2.5.0"
22
+ },
23
+ "keywords": [
24
+ "qrypt.chat",
25
+ "chat",
26
+ "e2ee",
27
+ "ml-kem",
28
+ "post-quantum",
29
+ "tui",
30
+ "cli",
31
+ "mcp"
32
+ ],
33
+ "homepage": "https://qrypt.chat",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/profullstack/qryptchat-web.git",
37
+ "directory": "packages/qryptchat"
38
+ },
39
+ "license": "MIT",
40
+ "author": "Profullstack, Inc."
41
+ }