@profullstack/qryptchat 0.5.1 → 0.6.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/bin/qc.js CHANGED
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  // qc: qrypt.chat in the terminal. `qc` opens the chat client; `qc --help` lists the rest.
3
3
  import { readFileSync } from 'node:fs';
4
- import { main } from '../dist/commands.js';
4
+ import { errorLine, main } from '../dist/commands.js';
5
5
 
6
6
  const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
7
7
 
8
8
  main(process.argv.slice(2), { version: pkg.version }).then(
9
9
  () => process.exit(0),
10
10
  (err) => {
11
- process.stderr.write(`qc: ${err?.message ?? err}\n`);
11
+ process.stderr.write(`${errorLine(err)}\n`);
12
12
  process.exit(1);
13
13
  },
14
14
  );
@@ -1,11 +1,12 @@
1
1
  import {
2
+ DEFAULT_URL,
2
3
  sessionPath,
3
4
  baseUrl,
4
5
  loadSession,
5
6
  unlockSession,
6
7
  storeSession,
7
8
  clearSession
8
- } from "./chunk-vfahhydf.js";
9
+ } from "./chunk-menzxtw0.js";
9
10
 
10
11
  // src/cli/crypto.js
11
12
  import { MlKem1024 as MlKem10242 } from "mlkem";
@@ -1090,11 +1091,79 @@ async function login(options = {}) {
1090
1091
  return { session, save };
1091
1092
  }
1092
1093
 
1094
+ // src/cli/agent.js
1095
+ import { randomBytes as randomBytes2 } from "node:crypto";
1096
+ function parseInvite(input) {
1097
+ const text = String(input ?? "").trim();
1098
+ let base = DEFAULT_URL;
1099
+ let token = text;
1100
+ if (/^https?:\/\//i.test(text)) {
1101
+ const url = new URL(text);
1102
+ base = url.origin;
1103
+ token = url.hash.replace(/^#/, "") || url.searchParams.get("token") || "";
1104
+ }
1105
+ if (!/^[A-Za-z0-9_-]{40,64}$/.test(token))
1106
+ throw new Error("That is not an agent invite link (https://qrypt.chat/agents/join#...).");
1107
+ return { base, token };
1108
+ }
1109
+ function usernameFor(name) {
1110
+ const stem = String(name || "agent").toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 20) || "agent";
1111
+ return `${stem.length >= 3 ? stem : `${stem}_agent`}_${randomBytes2(3).toString("hex")}`.slice(0, 30);
1112
+ }
1113
+ async function joinAsAgent(invite, { name, username, env = process.env, fetchImpl = fetch, vault } = {}) {
1114
+ const { base, token } = parseInvite(invite);
1115
+ const keys1024 = await ephemeralKeypair();
1116
+ const res = await fetchImpl(`${base}/api/agents/redeem`, {
1117
+ method: "POST",
1118
+ headers: { "Content-Type": "application/json" },
1119
+ body: JSON.stringify({ token, username: username || usernameFor(name), displayName: name, publicKey: keys1024.publicKey })
1120
+ });
1121
+ const body = await res.json().catch(() => ({}));
1122
+ if (!res.ok)
1123
+ throw new Error(body.error || `Could not join (HTTP ${res.status})`);
1124
+ const session = {
1125
+ base,
1126
+ access_token: body.access_token,
1127
+ refresh_token: body.refresh_token,
1128
+ expires_at: body.expires_at,
1129
+ user: body.user,
1130
+ keys: { keys1024 },
1131
+ agent: true,
1132
+ created_at: new Date().toISOString()
1133
+ };
1134
+ await storeSession(session, env, vault);
1135
+ return { session, conversationId: body.conversation_id, operator: body.operator };
1136
+ }
1137
+
1138
+ // src/cli/style.js
1139
+ import { emojify, icon } from "@profullstack/hqtui";
1140
+ var mark = (name) => {
1141
+ const glyph = icon(name);
1142
+ return glyph ? `${glyph} ` : "";
1143
+ };
1144
+ var em = (text) => emojify(String(text ?? ""), { mode: "emoji" });
1145
+ var ok = (text) => `${mark("check-circle")}${em(text)}`;
1146
+ var info = (text) => `${mark("info")}${em(text)}`;
1147
+ var warn = (text) => `${mark("warning")}${em(text)}`;
1148
+ var fail = (text) => `${mark("error")}${em(text)}`;
1149
+ function who(user, { agent = false } = {}) {
1150
+ if (!user)
1151
+ return "";
1152
+ const face = user.emoji ? `${user.emoji} ` : agent || user.account_type === "agent" ? "\uD83E\uDD16 " : mark("user");
1153
+ const name = user.display_name || user.displayName || user.username || "";
1154
+ const handle = user.username && name !== user.username ? ` @${user.username}` : "";
1155
+ const pronouns = user.pronouns ? ` (${user.pronouns})` : "";
1156
+ return `${face}${name}${handle}${pronouns}`;
1157
+ }
1158
+
1093
1159
  // src/cli/commands.js
1160
+ var errorLine2 = (err) => `qc: ${fail(err?.message ?? String(err))}`;
1094
1161
  var HELP2 = `qc: qrypt.chat in your terminal (end-to-end encrypted, ML-KEM-1024)
1095
1162
 
1096
1163
  Usage:
1097
1164
  qc open the chat client (signs you in first if needed)
1165
+ qc fonts install|status|remove our OpenEmoji colour font as the terminal's emoji font
1166
+ qc agent join <link> join as an AI agent from an invite (makes its own keys) [--name --username]
1098
1167
  qc login [--oob] sign in through your browser; --oob to paste a code (SSH)
1099
1168
  qc logout forget this terminal's session and keys
1100
1169
  qc whoami who this terminal is signed in as
@@ -1127,7 +1196,7 @@ function parseArgs2(argv) {
1127
1196
  const [k, v] = a.slice(2).split("=", 2);
1128
1197
  if (v !== undefined)
1129
1198
  args.flags[k] = v;
1130
- else if (["url", "n", "limit", "reply", "emoji", "pronouns", "website", "bio"].includes(k) && argv[i + 1] !== undefined && !argv[i + 1].startsWith("--"))
1199
+ else if (["url", "n", "limit", "reply", "emoji", "pronouns", "website", "bio", "name", "username"].includes(k) && argv[i + 1] !== undefined && !argv[i + 1].startsWith("--"))
1131
1200
  args.flags[k] = argv[++i];
1132
1201
  else
1133
1202
  args.flags[k] = true;
@@ -1196,9 +1265,55 @@ async function main2(argv, { version = "0.0.0" } = {}) {
1196
1265
  const { runTui } = await import("./tui.js");
1197
1266
  return runTui(c, { initialChat: rest[0] });
1198
1267
  }
1268
+ case "agent": {
1269
+ if (rest[0] !== "join" || !rest[1])
1270
+ throw new Error("Usage: qc agent join <invite link> [--name NAME] [--username NAME]");
1271
+ const { session, conversationId, operator } = await joinAsAgent(rest[1], {
1272
+ name: typeof flags.name === "string" ? flags.name : undefined,
1273
+ username: typeof flags.username === "string" ? flags.username : undefined
1274
+ });
1275
+ if (flags.json)
1276
+ return json({ user: session.user, conversation_id: conversationId, operator });
1277
+ out(ok(`Joined qrypt.chat as ${who(session.user, { agent: true })}, operated by ${who(operator)}.`));
1278
+ out(`${mark("key")}Keys made here and sealed in ${sessionPath()}; only the public key left this machine.`);
1279
+ const chatName = operator?.display_name || operator?.username || "<chat>";
1280
+ out(`${mark("chat")}Your chat with ${chatName} is open. Try: qc listen or qc send "${chatName}" "hello" or qc mcp`);
1281
+ return;
1282
+ }
1283
+ case "fonts": {
1284
+ const { installEmojiFont, emojiFontStatus, removeEmojiFont } = await import("@profullstack/hqtui");
1285
+ const action = rest[0] || "status";
1286
+ if (action === "install") {
1287
+ const result = await installEmojiFont();
1288
+ for (const note of result.notes)
1289
+ out(info(note));
1290
+ if (result.snippets?.length) {
1291
+ out(`
1292
+ ${mark("info")}Terminals that pick their own fonts need one line each:`);
1293
+ for (const sn of result.snippets)
1294
+ out(` ${sn.terminal} (${sn.file}):
1295
+ ${sn.snippet}`);
1296
+ }
1297
+ return out(`
1298
+ ${ok("Installed OpenEmoji. Restart the terminal, then run: qc fonts status")}`);
1299
+ }
1300
+ if (action === "status") {
1301
+ const st = await emojiFontStatus();
1302
+ out(`${st.installed ? ok("OpenEmoji installed") : warn("OpenEmoji not installed (qc fonts install)")}`);
1303
+ if (st.emojiFont !== undefined)
1304
+ out(`${mark("info")}Emoji font in use: ${st.emojiFont || "none"}`);
1305
+ return out(st.active ? ok("Active: emoji draw as OpenEmoji") : warn("Not active yet (restart the terminal)"));
1306
+ }
1307
+ if (action === "remove") {
1308
+ const done = await removeEmojiFont();
1309
+ return out(done.length ? ok("Removed the OpenEmoji font setup.") : info("Nothing to remove."));
1310
+ }
1311
+ throw new Error("Usage: qc fonts install|status|remove");
1312
+ }
1199
1313
  case "login": {
1200
1314
  const { session } = await login({ oob: !!flags.oob });
1201
- out(`Signed in as @${session.user?.username ?? "unknown"} on ${session.base}. Keys sealed (ChaCha20-Poly1305) in ${sessionPath()}.`);
1315
+ out(ok(`Signed in as ${who(session.user)} on ${session.base}.`));
1316
+ out(`${mark("lock")}Keys sealed (ChaCha20-Poly1305) in ${sessionPath()}.`);
1202
1317
  if (await new QcClient(session).keyCheck() === "mismatch") {
1203
1318
  out("Warning: the browser handed over keys that are not your account's current keys, so messages will not decrypt here. Restore your keys in that browser (Settings > Keys) or approve from the device you chat on, then run qc login again.");
1204
1319
  }
@@ -1206,12 +1321,12 @@ async function main2(argv, { version = "0.0.0" } = {}) {
1206
1321
  }
1207
1322
  case "logout":
1208
1323
  clearSession();
1209
- return out("Signed out. This terminal no longer holds your keys.");
1324
+ return out(ok("Signed out. This terminal no longer holds your keys."));
1210
1325
  case "whoami": {
1211
1326
  const session = loadSession();
1212
1327
  if (!session)
1213
1328
  throw new Error("Not signed in. Run qc login.");
1214
- return flags.json ? json({ user: session.user, base: session.base }) : out(`@${session.user?.username} (${session.user?.display_name ?? ""}) on ${session.base}`);
1329
+ return flags.json ? json({ user: session.user, base: session.base }) : out(`${who(session.user)} on ${session.base}`);
1215
1330
  }
1216
1331
  case "profile": {
1217
1332
  const c = await client(flags);
@@ -1224,14 +1339,14 @@ async function main2(argv, { version = "0.0.0" } = {}) {
1224
1339
  const p = Object.keys(fields).length ? await c.updateProfile(fields) : await c.profile();
1225
1340
  if (flags.json)
1226
1341
  return json({ username: p.username, emoji: p.emoji ?? null, pronouns: p.pronouns ?? null, website: p.website ?? null, bio: p.bio ?? null });
1227
- out(`${p.emoji ? `${p.emoji} ` : ""}${p.displayName ?? p.display_name ?? p.username} @${p.username}${p.pronouns ? ` (${p.pronouns})` : ""}`);
1342
+ out(who(p));
1228
1343
  if (p.website)
1229
- out(p.website);
1344
+ out(`${mark("link")}${p.website}`);
1230
1345
  if (p.bio)
1231
1346
  out(p.bio);
1232
1347
  if (!Object.keys(fields).length)
1233
1348
  out(`
1234
- Set: qc profile --emoji \uD83D\uDD2D --pronouns she/her --website https://you.example (an empty value clears)`);
1349
+ ${info("Set: qc profile --emoji :telescope: --pronouns she/her --website https://you.example (an empty value clears)")}`);
1235
1350
  return;
1236
1351
  }
1237
1352
  case "chats":
@@ -1240,7 +1355,7 @@ Set: qc profile --emoji \uD83D\uDD2D --pronouns she/her --website https://you.ex
1240
1355
  if (flags.json)
1241
1356
  return json(chats.map((c) => ({ id: c.id, title: c.title, type: c.type, updated_at: c.updated_at, participants: c.participants?.length ?? 0 })));
1242
1357
  for (const c of chats)
1243
- out(`${c.id} ${c.title}`);
1358
+ out(`${c.id} ${mark(c.type === "group" ? "users" : "chat")}${c.title}`);
1244
1359
  return;
1245
1360
  }
1246
1361
  case "read": {
@@ -1256,7 +1371,7 @@ Set: qc profile --emoji \uD83D\uDD2D --pronouns she/her --website https://you.ex
1256
1371
  if (m.replyTo)
1257
1372
  out(` ┃ ${m.replyTo.name ? `${m.replyTo.name}: ` : ""}${m.replyTo.snippet}`);
1258
1373
  const reactions = m.reactions?.length ? ` [${m.reactions.map((r) => `${r.emoji}${r.count > 1 ? r.count : ""}`).join(" ")}]` : "";
1259
- out(`[${stamp(m.at)}] ${m.mine ? "you" : m.sender}: ${m.text}${reactions} (${m.id.slice(0, 8)})`);
1374
+ out(`[${stamp(m.at)}] ${m.emoji ? `${m.emoji} ` : ""}${m.mine ? "you" : m.sender}: ${em(m.text)}${reactions} (${m.id.slice(0, 8)})`);
1260
1375
  }
1261
1376
  return;
1262
1377
  }
@@ -1271,7 +1386,7 @@ Set: qc profile --emoji \uD83D\uDD2D --pronouns she/her --website https://you.ex
1271
1386
  if (target.length !== 1)
1272
1387
  throw new Error(target.length ? `"${messageId}" matches ${target.length} messages` : `No message "${messageId}" in ${chat.title}`);
1273
1388
  await c.react(chat.id, target[0].id, emoji, !!flags.remove);
1274
- return out(`${flags.remove ? "Removed" : "Reacted"} ${emoji} on ${chat.title}.`);
1389
+ return out(ok(`${flags.remove ? "Removed" : "Reacted"} ${emoji} on ${chat.title}.`));
1275
1390
  }
1276
1391
  case "send": {
1277
1392
  if (!rest[0] || rest.length < 2)
@@ -1284,7 +1399,9 @@ Set: qc profile --emoji \uD83D\uDD2D --pronouns she/her --website https://you.ex
1284
1399
  const { message, skipped } = await c.send(chat.id, text, { replyTo: flags.reply || undefined });
1285
1400
  if (flags.json)
1286
1401
  return json({ id: message?.id, conversation: chat.id, skipped });
1287
- return out(`Sent to ${chat.title}${skipped ? ` (${skipped} participant(s) have no key yet)` : ""}.`);
1402
+ if (skipped)
1403
+ out(warn(`${skipped} participant(s) have no key yet and will not see it.`));
1404
+ return out(`${mark("send")}Sent to ${chat.title}.`);
1288
1405
  }
1289
1406
  case "listen": {
1290
1407
  const c = await client(flags);
@@ -1305,7 +1422,7 @@ Set: qc profile --emoji \uD83D\uDD2D --pronouns she/her --website https://you.ex
1305
1422
  if (flags.json)
1306
1423
  out(JSON.stringify({ chat: id, title: titles.get(id), ...m }));
1307
1424
  else
1308
- out(`[${stamp(m.at)}] ${titles.get(id) ?? id} · ${m.mine ? "you" : m.sender}: ${m.text}`);
1425
+ out(`[${stamp(m.at)}] ${mark("chat")}${titles.get(id) ?? id} · ${m.emoji ? `${m.emoji} ` : ""}${m.mine ? "you" : m.sender}: ${em(m.text)}`);
1309
1426
  }
1310
1427
  }, { onStatus: (s) => process.stderr.write(`qc: ${s}
1311
1428
  `) });
@@ -1322,4 +1439,4 @@ ${HELP2}`);
1322
1439
  }
1323
1440
  }
1324
1441
 
1325
- export { HELP2, parseArgs2, findChat2, main2 };
1442
+ export { errorLine2, HELP2, parseArgs2, findChat2, main2 };
@@ -187,4 +187,4 @@ function clearSession(env = process.env) {
187
187
  rmSync(file);
188
188
  }
189
189
 
190
- export { configDir, sessionPath, baseUrl, loadSession, unlockSession, storeSession, clearSession };
190
+ export { DEFAULT_URL, configDir, sessionPath, baseUrl, loadSession, unlockSession, storeSession, clearSession };
package/dist/commands.js CHANGED
@@ -1,12 +1,14 @@
1
1
  import {
2
+ errorLine2,
2
3
  HELP2,
3
4
  parseArgs2,
4
5
  findChat2,
5
6
  main2
6
- } from "./chunk-byfvkvkr.js";
7
- import"./chunk-vfahhydf.js";
7
+ } from "./chunk-0f0frsar.js";
8
+ import"./chunk-menzxtw0.js";
8
9
  export {
9
10
  HELP2 as HELP,
11
+ errorLine2 as errorLine,
10
12
  findChat2 as findChat,
11
13
  main2 as main,
12
14
  parseArgs2 as parseArgs
package/dist/mcp.js CHANGED
@@ -1,7 +1,7 @@
1
- import"./chunk-vfahhydf.js";
1
+ import"./chunk-menzxtw0.js";
2
2
  import {
3
3
  findChat2
4
- } from "./chunk-byfvkvkr.js";
4
+ } from "./chunk-0f0frsar.js";
5
5
 
6
6
  // src/cli/mcp.js
7
7
  import { createInterface } from "node:readline";
package/dist/tui.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  configDir
3
- } from "./chunk-vfahhydf.js";
3
+ } from "./chunk-menzxtw0.js";
4
4
 
5
5
  // src/cli/tui.js
6
6
  import { createApp, createImageStore, drawIcon, drawRichText, editText, emojify, insertText, stringWidth, truncate, widgets, wrap } from "@profullstack/hqtui";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@profullstack/qryptchat",
3
- "version": "0.5.1",
3
+ "version": "0.6.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": {