Skip to content

Commit 01ef258

Browse files
ralyodioclaude
andcommitted
qc: qrypt.chat in the terminal (hqtui client, CLI, MCP, browser login)
Bare `qc` opens a full-screen end-to-end encrypted chat client on hqtui: chats with unread counts, a bottom-anchored transcript grouped by sender, typing indicators, a live/connecting/offline badge, the OpenEmoji picker on Ctrl+E, :shortcode: emojify on send, and the mouse (one click opens a chat, the wheel scrolls). Scripts get `qc chats|read|send|listen` (--json), agents get `qc mcp` (list_chats, read_chat, send_message). Encryption is the web app's own ML-KEM-1024 code run in Node/Bun with the keys handed in: a message is encrypted per participant here, and the server only ever sees ciphertext. `qc login` is OAuth 2.1 authorization code + PKCE, started from the CLI: - /cli/authorize (web): the signed-in app approves, seals the account keypair to a one-time ML-KEM-1024 key qc generated, and stores a 5-minute single-use code (hash only) in cli_auth_codes. - /api/cli/token: code + verifier -> a fresh Supabase session of qc's own (never the browser's: refresh tokens rotate) + the sealed keys; grant_type=refresh_token rotates. Phone-only/anon accounts get a confirmed synthetic address for the magic-link bridge, like names do. - Loopback redirect, or --oob to paste a code over SSH. Both sides show a confirmation code derived from the challenge and the one-time key. /api/events now authenticates with authenticateRequest (Bearer or cookie), so a CLI can follow the live stream. Migration 20261006120000_cli_auth_codes: RLS on, no policies, service_role only. Applied on dev2. Publishable package: packages/qryptchat (@profullstack/qryptchat, bins qc + qryptchat), built by scripts/build-qc.js. The root `qryptchat` bin and `cli` script now point at qc; the old WebSocket-era blessed CLI is left in place, unused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent fb9aee1 commit 01ef258

27 files changed

Lines changed: 2294 additions & 11 deletions

File tree

‎.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,6 @@ supabase/.temp/
2727
# Claude Code: local-only, machine-specific (keep shared config committed)
2828
**/.claude/settings.local.json
2929
**/.claude/worktrees/
30+
31+
# qc package build output (scripts/build-qc.js)
32+
packages/qryptchat/dist/

‎bin/qc.js‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/env node
2+
// qc: qrypt.chat in the terminal. `qc` opens the chat client; `qc --help` lists the rest.
3+
import { readFileSync } from 'node:fs';
4+
import { main } from '../src/cli/commands.js';
5+
6+
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
7+
8+
// main resolves when the command is done (the TUI quit, stdin closed under
9+
// mcp); exit then, or the SSE socket and timers would hold the process open.
10+
main(process.argv.slice(2), { version: pkg.version }).then(
11+
() => process.exit(0),
12+
(err) => {
13+
process.stderr.write(`qc: ${err?.message ?? err}\n`);
14+
process.exit(1);
15+
},
16+
);

‎bun.lock‎

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,18 @@
44
"version": "0.0.3",
55
"type": "module",
66
"bin": {
7-
"qryptchat": "./bin/qryptchat-cli.js",
7+
"qc": "./bin/qc.js",
8+
"qryptchat": "./bin/qc.js",
89
"qryptchat-agent": "./bin/qryptchat-agent.js"
910
},
1011
"packageManager": "bun@1.4.0",
1112
"scripts": {
1213
"lint:oxlint": "oxlint",
1314
"lint:oxlint:fix": "oxlint --fix",
14-
"cli": "bun bin/qryptchat-cli.js",
15+
"cli": "bun bin/qc.js",
1516
"agent": "bun bin/qryptchat-agent.js",
17+
"qc": "bun bin/qc.js",
18+
"build:qc": "bun scripts/build-qc.js",
1619
"dev": "bun --bun next dev",
1720
"build": "bun --bun next build",
1821
"start": "bun --bun next start",
@@ -31,6 +34,7 @@
3134
"dependencies": "command -v pnpm && pnpm dlx @socketsecurity/socket-patch apply --silent --ecosystems npm || true"
3235
},
3336
"devDependencies": {
37+
"@profullstack/hqtui": "^0.8.0",
3438
"@testing-library/jest-dom": "^6.8.0",
3539
"@testing-library/react": "^16.0.0",
3640
"@testing-library/user-event": "^14.6.1",

‎packages/qryptchat/README.md‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# qc
2+
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+
5+
```sh
6+
npm install -g @profullstack/qryptchat # or: bun add -g @profullstack/qryptchat
7+
qc
8+
```
9+
10+
The first run signs you in through your browser, then opens your chats.
11+
12+
## The client
13+
14+
```
15+
🔒 qrypt.chat @you ● live
16+
┌ Chats ──────────┐┌ alice ───────────────────────── 2 people · ML-KEM-1024 ┐
17+
│▸ alice 2 ││ ───────────────────── Tue 6 Oct ───────────────────── │
18+
│ team chat ││ alice 14:02 │
19+
│ ops ││ shipped it 🚀 │
20+
│ ││ you 14:03 │
21+
│ ││ 🔥🔥 │
22+
│ ││ alice is typing… │
23+
│ ││ Message alice (Ctrl+E emoji, :rocket: works too) │
24+
└─────────────────┘└────────────────────────────────────────────────────────┘
25+
ENTER send CTRL+E emoji TAB chats PGUP scroll CTRL+C quit
26+
```
27+
28+
| Key | Does |
29+
|---|---|
30+
| Enter | send (or open the highlighted chat) |
31+
| Ctrl+E | the emoji picker: search, recents and every group, drawn with [OpenEmoji](https://github.com/profullstack/openemoji) |
32+
| `:rocket:` | shortcodes turn into emoji when you send |
33+
| Tab / Esc | move between the chat list and the composer |
34+
| Ctrl+N / Ctrl+P | next / previous chat |
35+
| PgUp / PgDn, wheel | scroll back through the conversation |
36+
| Ctrl+R | reload |
37+
| Ctrl+C | quit |
38+
39+
The mouse works too: click a chat, scroll the transcript, click an emoji. Unread counts, typing indicators and new messages arrive live.
40+
41+
## Scripts and agents
42+
43+
```sh
44+
qc chats # id and name of every chat
45+
qc read alice -n 20 # the last 20 messages, decrypted
46+
qc send alice "on my way" # encrypted to every participant
47+
echo "deploy done" | qc send ops -
48+
qc listen --json # new messages as NDJSON, until you stop it
49+
qc whoami
50+
```
51+
52+
`<chat>` is a chat id or any unique part of its name. Add `--json` for machine output.
53+
54+
### MCP
55+
56+
```sh
57+
qc mcp
58+
```
59+
60+
runs an MCP server on stdio with three tools: `list_chats`, `read_chat` and `send_message`. Encryption and decryption happen on this machine; the server only ever sees ciphertext. For Claude Code:
61+
62+
```sh
63+
claude mcp add qryptchat -- qc mcp
64+
```
65+
66+
## Signing in
67+
68+
`qc login` uses OAuth 2.1 (authorization code + PKCE), started from the terminal:
69+
70+
1. qc opens `qrypt.chat/cli/authorize` with a one-time ML-KEM-1024 public key and prints a confirmation code.
71+
2. You check the browser shows the same code, then approve.
72+
3. The browser seals your keys to that one-time key, and qc receives them plus a session of its own. The server only relays ciphertext.
73+
74+
Over SSH, `qc login --oob` shows a code in the browser for you to paste instead of redirecting back.
75+
76+
The session and keys are kept in `~/.config/qc/session.json` (or `$QC_HOME`), mode 0600. Sessions refresh themselves; `qc logout` deletes them. Point at another server with `--url` or `QC_URL`.
77+
78+
## Licence
79+
80+
MIT. Emoji: OpenEmoji by Profullstack, Inc. (CC BY 4.0).

‎packages/qryptchat/bin/qc.js‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/usr/bin/env node
2+
// qc: qrypt.chat in the terminal. `qc` opens the chat client; `qc --help` lists the rest.
3+
import { readFileSync } from 'node:fs';
4+
import { main } from '../dist/commands.js';
5+
6+
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
7+
8+
main(process.argv.slice(2), { version: pkg.version }).then(
9+
() => process.exit(0),
10+
(err) => {
11+
process.stderr.write(`qc: ${err?.message ?? err}\n`);
12+
process.exit(1);
13+
},
14+
);

‎packages/qryptchat/package.json‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

‎public/llms.txt‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ QryptChat uses NIST-approved post-quantum algorithms (ML-KEM-1024 / CRYSTALS-Kyb
1717
- [Terms of Service](https://qrypt.chat/terms) — Usage terms
1818
- [Warrant Canary](https://qrypt.chat/warrant-canary) — Transparency statement
1919

20+
## Terminal and agents
21+
22+
- [qc](https://www.npmjs.com/package/@profullstack/qryptchat) is the terminal client: `npm install -g @profullstack/qryptchat`, then run `qc`.
23+
- It is a full-screen chat app for people, and `qc chats|read|send|listen` for scripts.
24+
- `qc mcp` is an MCP server with the tools `list_chats`, `read_chat` and `send_message`.
25+
- Encryption happens on the machine running qc.
26+
2027
## Company
2128

2229
- **Name**: Profullstack, Inc.

‎public/skill.md‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ Quantum-resistant, end-to-end encrypted messaging.
1414

1515
Phone number + SMS verification code. No passwords.
1616

17-
## API
17+
## Terminal, scripts and agents: qc
1818

19-
No public API. Self-hostable via the open-source repo at https://github.com/profullstack/qryptchat-web
19+
`npm install -g @profullstack/qryptchat` installs `qc`:
20+
21+
- `qc` opens a full-screen chat client (emoji picker on Ctrl+E).
22+
- `qc chats`, `qc read <chat>`, `qc send <chat> <text>` and `qc listen --json` are for scripts.
23+
- `qc mcp` runs an MCP server on stdio with the tools `list_chats`, `read_chat` and `send_message`.
24+
25+
`qc login` signs in through the browser (OAuth 2.1, authorization code + PKCE). The browser seals the account's keys to a one-time ML-KEM-1024 key that qc generated, so messages are encrypted and decrypted on the machine running qc. The server only ever sees ciphertext.
26+
27+
Self-hostable via the open-source repo at https://github.com/profullstack/qryptchat-web

‎scripts/build-qc.js‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#!/usr/bin/env bun
2+
/**
3+
* Build the publishable qc package: `bun scripts/build-qc.js`.
4+
*
5+
* Bundles src/cli (and the web app's own crypto it imports) into
6+
* packages/qryptchat/dist, leaving the runtime dependencies external so npm
7+
* installs them. The TUI and MCP server stay separate chunks, loaded only by
8+
* the commands that need them.
9+
*/
10+
import { readFileSync, rmSync } from 'node:fs';
11+
import { join, resolve } from 'node:path';
12+
13+
const root = resolve(import.meta.dir, '..');
14+
const out = join(root, 'packages', 'qryptchat', 'dist');
15+
const pkg = JSON.parse(readFileSync(join(root, 'packages', 'qryptchat', 'package.json'), 'utf8'));
16+
17+
rmSync(out, { recursive: true, force: true });
18+
const result = await Bun.build({
19+
entrypoints: [join(root, 'src/cli/commands.js'), join(root, 'src/cli/tui.js'), join(root, 'src/cli/mcp.js')],
20+
outdir: out,
21+
target: 'node',
22+
format: 'esm',
23+
splitting: true,
24+
external: Object.keys(pkg.dependencies),
25+
naming: { entry: '[name].js', chunk: 'chunk-[hash].js' },
26+
});
27+
if (!result.success) {
28+
for (const log of result.logs) console.error(log);
29+
process.exit(1);
30+
}
31+
console.log(result.outputs.map((o) => `${o.path.replace(`${root}/`, '')} ${(o.size / 1024).toFixed(0)} KB`).join('\n'));

0 commit comments

Comments
 (0)