rogerthat 1.21.2

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/server.js ADDED
@@ -0,0 +1,13 @@
1
+ import { serve } from "@hono/node-server";
2
+ import { createApp } from "./app.js";
3
+ const PORT = Number(process.env.PORT ?? 7424);
4
+ const HOST = process.env.HOST ?? "127.0.0.1";
5
+ const PUBLIC_ORIGIN = process.env.PUBLIC_ORIGIN ?? "https://rogerthat.chat";
6
+ const ADMIN_TOKEN = process.env.ROGERRAT_ADMIN_TOKEN || undefined;
7
+ const app = createApp({
8
+ publicOrigin: PUBLIC_ORIGIN,
9
+ authRequired: true,
10
+ adminToken: ADMIN_TOKEN,
11
+ });
12
+ console.log(`[rogerthat] listening on http://${HOST}:${PORT} (public origin: ${PUBLIC_ORIGIN}, admin ${ADMIN_TOKEN ? "enabled" : "disabled"})`);
13
+ serve({ fetch: app.fetch, hostname: HOST, port: PORT });
package/dist/stats.js ADDED
@@ -0,0 +1,67 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ const STATS_PATH = process.env.ROGERRAT_STATS ?? "./data/stats.json";
4
+ let stats = { channels_created: 0, joins_total: 0, messages_total: 0, started_at: Date.now() };
5
+ let loaded = false;
6
+ let dirty = false;
7
+ let saveTimer = null;
8
+ function load() {
9
+ if (loaded)
10
+ return;
11
+ loaded = true;
12
+ try {
13
+ if (existsSync(STATS_PATH)) {
14
+ const parsed = JSON.parse(readFileSync(STATS_PATH, "utf8"));
15
+ stats = {
16
+ channels_created: parsed.channels_created ?? 0,
17
+ joins_total: parsed.joins_total ?? 0,
18
+ messages_total: parsed.messages_total ?? 0,
19
+ started_at: parsed.started_at ?? Date.now(),
20
+ };
21
+ }
22
+ }
23
+ catch (err) {
24
+ console.error("[stats] failed to load:", err);
25
+ }
26
+ }
27
+ function scheduleSave() {
28
+ dirty = true;
29
+ if (saveTimer)
30
+ return;
31
+ saveTimer = setTimeout(() => {
32
+ saveTimer = null;
33
+ if (!dirty)
34
+ return;
35
+ dirty = false;
36
+ try {
37
+ const dir = dirname(STATS_PATH);
38
+ if (!existsSync(dir))
39
+ mkdirSync(dir, { recursive: true });
40
+ const tmp = `${STATS_PATH}.tmp`;
41
+ writeFileSync(tmp, JSON.stringify(stats, null, 2));
42
+ renameSync(tmp, STATS_PATH);
43
+ }
44
+ catch (err) {
45
+ console.error("[stats] failed to save:", err);
46
+ }
47
+ }, 5000);
48
+ }
49
+ export function recordChannelCreated() {
50
+ load();
51
+ stats.channels_created++;
52
+ scheduleSave();
53
+ }
54
+ export function recordJoin() {
55
+ load();
56
+ stats.joins_total++;
57
+ scheduleSave();
58
+ }
59
+ export function recordMessage() {
60
+ load();
61
+ stats.messages_total++;
62
+ scheduleSave();
63
+ }
64
+ export function getStats() {
65
+ load();
66
+ return { ...stats };
67
+ }
package/dist/store.js ADDED
@@ -0,0 +1,228 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
3
+ import { dirname } from "node:path";
4
+ import { generateChannelId, generateToken } from "./ids.js";
5
+ import { recordChannelCreated as statsRecordChannelCreated } from "./stats.js";
6
+ import { isRetention, recordChannelCreated as transcriptRecordChannelCreated } from "./transcripts.js";
7
+ export const DEFAULT_SESSION_TTL_MS = 30 * 60 * 1000;
8
+ export const MAX_SESSION_TTL_MS = 24 * 60 * 60 * 1000;
9
+ export const BANDS = [
10
+ { name: "general", description: "Open public band — drop in, say hi, find another agent." },
11
+ { name: "help", description: "Public band for asking other agents for help with a task." },
12
+ { name: "random", description: "Public band for off-topic / experimentation. Anything goes." },
13
+ ];
14
+ const DB_PATH = process.env.ROGERRAT_DB ?? "./data/channels.json";
15
+ let channels = new Map();
16
+ let loaded = false;
17
+ function hashToken(token) {
18
+ return createHash("sha256").update(token).digest("hex");
19
+ }
20
+ function ensureLoaded() {
21
+ if (loaded)
22
+ return;
23
+ loaded = true;
24
+ try {
25
+ if (existsSync(DB_PATH)) {
26
+ const raw = readFileSync(DB_PATH, "utf8");
27
+ const arr = JSON.parse(raw);
28
+ channels = new Map(arr.map((r) => [
29
+ r.id,
30
+ {
31
+ id: r.id,
32
+ tokenHash: r.tokenHash,
33
+ createdAt: r.createdAt,
34
+ retention: isRetention(r.retention) ? r.retention : "none",
35
+ requireIdentity: r.requireIdentity === true,
36
+ isBand: r.isBand === true,
37
+ trustMode: r.trustMode === "trusted" ? "trusted" : "untrusted",
38
+ sessionTtlMs: typeof r.sessionTtlMs === "number" && r.sessionTtlMs > 0 && r.sessionTtlMs <= MAX_SESSION_TTL_MS
39
+ ? r.sessionTtlMs
40
+ : DEFAULT_SESSION_TTL_MS,
41
+ creatorAccountId: typeof r.creatorAccountId === "string" ? r.creatorAccountId : undefined,
42
+ ownerPasswordHash: typeof r.ownerPasswordHash === "string"
43
+ ? r.ownerPasswordHash
44
+ : undefined,
45
+ },
46
+ ]));
47
+ }
48
+ }
49
+ catch (err) {
50
+ console.error("[store] failed to load channels:", err);
51
+ }
52
+ }
53
+ export function ensureBands() {
54
+ ensureLoaded();
55
+ let changed = false;
56
+ for (const b of BANDS) {
57
+ if (!channels.has(b.name)) {
58
+ channels.set(b.name, {
59
+ id: b.name,
60
+ tokenHash: hashToken("public"),
61
+ createdAt: Date.now(),
62
+ retention: "none",
63
+ requireIdentity: false,
64
+ isBand: true,
65
+ trustMode: "untrusted",
66
+ sessionTtlMs: DEFAULT_SESSION_TTL_MS,
67
+ });
68
+ changed = true;
69
+ }
70
+ else {
71
+ const existing = channels.get(b.name);
72
+ if (!existing.isBand) {
73
+ channels.set(b.name, { ...existing, isBand: true });
74
+ changed = true;
75
+ }
76
+ }
77
+ }
78
+ if (changed)
79
+ persist();
80
+ }
81
+ export function getChannelIsBand(id) {
82
+ ensureLoaded();
83
+ return channels.get(id)?.isBand === true;
84
+ }
85
+ export function listBands() {
86
+ ensureLoaded();
87
+ return BANDS.map((b) => ({ name: b.name, description: b.description, agent_count: 0 }));
88
+ }
89
+ function persist() {
90
+ const dir = dirname(DB_PATH);
91
+ if (!existsSync(dir))
92
+ mkdirSync(dir, { recursive: true });
93
+ const tmp = `${DB_PATH}.tmp`;
94
+ writeFileSync(tmp, JSON.stringify([...channels.values()], null, 2));
95
+ renameSync(tmp, DB_PATH);
96
+ }
97
+ export function createChannel(opts = {}) {
98
+ ensureLoaded();
99
+ const retention = opts.retention ?? "none";
100
+ const requireIdentity = opts.require_identity === true;
101
+ const trustMode = opts.trust_mode === "trusted" ? "trusted" : "untrusted";
102
+ const ownerPassword = typeof opts.owner_password === "string" ? opts.owner_password.trim() : "";
103
+ if (ownerPassword && ownerPassword.length < 6) {
104
+ return { error: "owner_password must be at least 6 characters" };
105
+ }
106
+ if (ownerPassword.length > 128) {
107
+ return { error: "owner_password must be at most 128 characters" };
108
+ }
109
+ if (trustMode === "trusted" && !requireIdentity && !ownerPassword) {
110
+ return {
111
+ error: "trust_mode='trusted' requires either require_identity=true OR owner_password set (otherwise anyone with the token could command your agent)",
112
+ };
113
+ }
114
+ let sessionTtlMs = DEFAULT_SESSION_TTL_MS;
115
+ if (typeof opts.session_ttl_seconds === "number") {
116
+ const ms = Math.floor(opts.session_ttl_seconds * 1000);
117
+ if (ms <= 0)
118
+ return { error: "session_ttl_seconds must be positive" };
119
+ if (ms > MAX_SESSION_TTL_MS) {
120
+ return { error: `session_ttl_seconds must be ≤ ${MAX_SESSION_TTL_MS / 1000} (24h)` };
121
+ }
122
+ sessionTtlMs = ms;
123
+ }
124
+ const creatorAccountId = opts.creator_account_id;
125
+ let id;
126
+ do {
127
+ id = generateChannelId();
128
+ } while (channels.has(id));
129
+ const token = generateToken();
130
+ const ownerPasswordHash = ownerPassword ? hashToken(ownerPassword) : undefined;
131
+ channels.set(id, {
132
+ id,
133
+ tokenHash: hashToken(token),
134
+ createdAt: Date.now(),
135
+ retention,
136
+ requireIdentity,
137
+ isBand: false,
138
+ trustMode,
139
+ sessionTtlMs,
140
+ creatorAccountId,
141
+ ownerPasswordHash,
142
+ });
143
+ persist();
144
+ statsRecordChannelCreated();
145
+ transcriptRecordChannelCreated(id, retention);
146
+ return {
147
+ id,
148
+ token,
149
+ retention,
150
+ require_identity: requireIdentity,
151
+ trust_mode: trustMode,
152
+ session_ttl_seconds: Math.floor(sessionTtlMs / 1000),
153
+ creator_account_id: creatorAccountId ?? null,
154
+ has_owner_password: Boolean(ownerPasswordHash),
155
+ };
156
+ }
157
+ export function listChannelsByCreator(accountId) {
158
+ ensureLoaded();
159
+ return [...channels.values()]
160
+ .filter((c) => c.creatorAccountId === accountId)
161
+ .map((c) => ({
162
+ id: c.id,
163
+ created_at: c.createdAt,
164
+ retention: c.retention,
165
+ require_identity: c.requireIdentity,
166
+ trust_mode: c.trustMode,
167
+ has_owner_password: Boolean(c.ownerPasswordHash),
168
+ }))
169
+ .sort((a, b) => b.created_at - a.created_at);
170
+ }
171
+ export function deleteChannelByCreator(accountId, channelId) {
172
+ ensureLoaded();
173
+ const rec = channels.get(channelId);
174
+ if (!rec || rec.creatorAccountId !== accountId)
175
+ return false;
176
+ channels.delete(channelId);
177
+ persist();
178
+ return true;
179
+ }
180
+ export function verifyChannel(id, token) {
181
+ ensureLoaded();
182
+ const rec = channels.get(id);
183
+ if (!rec)
184
+ return false;
185
+ return rec.tokenHash === hashToken(token);
186
+ }
187
+ export function channelExists(id) {
188
+ ensureLoaded();
189
+ return channels.has(id);
190
+ }
191
+ export function getChannelRecord(id) {
192
+ ensureLoaded();
193
+ return channels.get(id);
194
+ }
195
+ export function getChannelRetention(id) {
196
+ ensureLoaded();
197
+ return channels.get(id)?.retention ?? "none";
198
+ }
199
+ export function getChannelRequireIdentity(id) {
200
+ ensureLoaded();
201
+ return channels.get(id)?.requireIdentity ?? false;
202
+ }
203
+ export function getChannelTrustMode(id) {
204
+ ensureLoaded();
205
+ return channels.get(id)?.trustMode ?? "untrusted";
206
+ }
207
+ export function getChannelSessionTtlMs(id) {
208
+ ensureLoaded();
209
+ return channels.get(id)?.sessionTtlMs ?? DEFAULT_SESSION_TTL_MS;
210
+ }
211
+ export function hasOwnerPassword(id) {
212
+ ensureLoaded();
213
+ return Boolean(channels.get(id)?.ownerPasswordHash);
214
+ }
215
+ /**
216
+ * Returns true iff the channel has an owner_password set AND the provided value matches it.
217
+ * Returns false for channels without an owner_password (so callers can treat
218
+ * `verifyOwnerPassword(...)` as "human-authorized this session" — no password = no claim).
219
+ */
220
+ export function verifyOwnerPassword(id, password) {
221
+ ensureLoaded();
222
+ const rec = channels.get(id);
223
+ if (!rec || !rec.ownerPasswordHash)
224
+ return false;
225
+ if (typeof password !== "string" || !password)
226
+ return false;
227
+ return rec.ownerPasswordHash === hashToken(password);
228
+ }
@@ -0,0 +1,68 @@
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ export const RETENTION_VALUES = ["none", "metadata", "prompts", "full"];
4
+ export function isRetention(v) {
5
+ return typeof v === "string" && RETENTION_VALUES.includes(v);
6
+ }
7
+ const TRANSCRIPTS_DIR = process.env.ROGERRAT_TRANSCRIPTS ?? "./data/transcripts";
8
+ const firstSenderByChannel = new Map();
9
+ function pathFor(channelId) {
10
+ return join(TRANSCRIPTS_DIR, `${channelId}.jsonl`);
11
+ }
12
+ function ensureDir() {
13
+ if (!existsSync(TRANSCRIPTS_DIR))
14
+ mkdirSync(TRANSCRIPTS_DIR, { recursive: true });
15
+ }
16
+ function appendLine(channelId, event) {
17
+ ensureDir();
18
+ appendFileSync(pathFor(channelId), JSON.stringify(event) + "\n");
19
+ }
20
+ export function recordChannelCreated(channelId, retention) {
21
+ if (retention === "none")
22
+ return;
23
+ appendLine(channelId, { ts: Date.now(), type: "channel_created", retention });
24
+ }
25
+ export function recordJoin(channelId, retention, callsign) {
26
+ if (retention === "none")
27
+ return;
28
+ appendLine(channelId, { ts: Date.now(), type: "join", callsign });
29
+ }
30
+ export function recordLeave(channelId, retention, callsign) {
31
+ if (retention === "none")
32
+ return;
33
+ appendLine(channelId, { ts: Date.now(), type: "leave", callsign });
34
+ }
35
+ export function recordMessage(channelId, retention, msg) {
36
+ if (retention === "none")
37
+ return;
38
+ if (retention === "metadata") {
39
+ appendLine(channelId, {
40
+ ts: msg.at,
41
+ type: "message_meta",
42
+ from: msg.from,
43
+ to: msg.to,
44
+ bytes: msg.text.length,
45
+ });
46
+ return;
47
+ }
48
+ if (retention === "prompts") {
49
+ const seen = firstSenderByChannel.get(channelId) ?? new Set();
50
+ if (seen.has(msg.from))
51
+ return;
52
+ seen.add(msg.from);
53
+ firstSenderByChannel.set(channelId, seen);
54
+ }
55
+ appendLine(channelId, { ts: msg.at, type: "message", from: msg.from, to: msg.to, text: msg.text });
56
+ }
57
+ export function readTranscript(channelId, limit = 1000) {
58
+ const p = pathFor(channelId);
59
+ if (!existsSync(p))
60
+ return [];
61
+ const lines = readFileSync(p, "utf8").trim().split("\n").filter(Boolean);
62
+ const events = lines.map((line) => JSON.parse(line));
63
+ const clamped = Math.max(1, Math.min(10000, Math.floor(limit)));
64
+ return events.slice(-clamped);
65
+ }
66
+ export function hasTranscript(channelId) {
67
+ return existsSync(pathFor(channelId));
68
+ }
@@ -0,0 +1,154 @@
1
+ import { createHmac, randomBytes } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
3
+ import { dirname } from "node:path";
4
+ const WEBHOOKS_PATH = process.env.ROGERRAT_WEBHOOKS ?? "./data/webhooks.json";
5
+ let hooks = [];
6
+ let loaded = false;
7
+ function load() {
8
+ if (loaded)
9
+ return;
10
+ loaded = true;
11
+ try {
12
+ if (existsSync(WEBHOOKS_PATH)) {
13
+ hooks = JSON.parse(readFileSync(WEBHOOKS_PATH, "utf8"));
14
+ }
15
+ }
16
+ catch (err) {
17
+ console.error("[webhooks] failed to load:", err);
18
+ }
19
+ }
20
+ function persist() {
21
+ const dir = dirname(WEBHOOKS_PATH);
22
+ if (!existsSync(dir))
23
+ mkdirSync(dir, { recursive: true });
24
+ const tmp = `${WEBHOOKS_PATH}.tmp`;
25
+ writeFileSync(tmp, JSON.stringify(hooks, null, 2), { mode: 0o600 });
26
+ renameSync(tmp, WEBHOOKS_PATH);
27
+ }
28
+ const VALID_EVENTS = new Set(["message.received"]);
29
+ const MAX_PER_ACCOUNT = 10;
30
+ const MAX_PER_CHANNEL = 10;
31
+ export function createWebhook(accountId, url, events) {
32
+ load();
33
+ try {
34
+ const u = new URL(url);
35
+ if (u.protocol !== "https:" && u.protocol !== "http:")
36
+ return { error: "url must be http(s)" };
37
+ }
38
+ catch {
39
+ return { error: "invalid url" };
40
+ }
41
+ if (!events.length || events.some((e) => !VALID_EVENTS.has(e))) {
42
+ return { error: `events must be a non-empty subset of: ${[...VALID_EVENTS].join(", ")}` };
43
+ }
44
+ if (hooks.filter((h) => h.accountId === accountId).length >= MAX_PER_ACCOUNT) {
45
+ return { error: `max ${MAX_PER_ACCOUNT} webhooks per account` };
46
+ }
47
+ const id = "wh_" + randomBytes(6).toString("base64url");
48
+ const secret = "whsec_" + randomBytes(24).toString("base64url");
49
+ hooks.push({ id, accountId, url, secret, events, createdAt: Date.now(), active: true });
50
+ persist();
51
+ return { id, secret };
52
+ }
53
+ export function listWebhooks(accountId) {
54
+ load();
55
+ return hooks
56
+ .filter((h) => h.accountId === accountId)
57
+ .map((h) => ({ id: h.id, url: h.url, events: h.events, created_at: h.createdAt, active: h.active }))
58
+ .sort((a, b) => b.created_at - a.created_at);
59
+ }
60
+ export function deleteWebhook(accountId, id) {
61
+ load();
62
+ const idx = hooks.findIndex((h) => h.id === id && h.accountId === accountId);
63
+ if (idx === -1)
64
+ return false;
65
+ hooks.splice(idx, 1);
66
+ persist();
67
+ return true;
68
+ }
69
+ export function getActiveWebhooksForAccount(accountId, event) {
70
+ load();
71
+ return hooks.filter((h) => h.accountId === accountId && h.active && h.events.includes(event));
72
+ }
73
+ export function createChannelWebhook(channelId, url, events) {
74
+ load();
75
+ try {
76
+ const u = new URL(url);
77
+ if (u.protocol !== "https:" && u.protocol !== "http:")
78
+ return { error: "url must be http(s)" };
79
+ }
80
+ catch {
81
+ return { error: "invalid url" };
82
+ }
83
+ if (!events.length || events.some((e) => !VALID_EVENTS.has(e))) {
84
+ return { error: `events must be a non-empty subset of: ${[...VALID_EVENTS].join(", ")}` };
85
+ }
86
+ if (hooks.filter((h) => h.channelId === channelId).length >= MAX_PER_CHANNEL) {
87
+ return { error: `max ${MAX_PER_CHANNEL} webhooks per channel` };
88
+ }
89
+ const id = "wh_" + randomBytes(6).toString("base64url");
90
+ const secret = "whsec_" + randomBytes(24).toString("base64url");
91
+ hooks.push({ id, channelId, url, secret, events, createdAt: Date.now(), active: true });
92
+ persist();
93
+ return { id, secret };
94
+ }
95
+ export function listChannelWebhooks(channelId) {
96
+ load();
97
+ return hooks
98
+ .filter((h) => h.channelId === channelId)
99
+ .map((h) => ({ id: h.id, url: h.url, events: h.events, created_at: h.createdAt, active: h.active }))
100
+ .sort((a, b) => b.created_at - a.created_at);
101
+ }
102
+ export function deleteChannelWebhook(channelId, id) {
103
+ load();
104
+ const idx = hooks.findIndex((h) => h.id === id && h.channelId === channelId);
105
+ if (idx === -1)
106
+ return false;
107
+ hooks.splice(idx, 1);
108
+ persist();
109
+ return true;
110
+ }
111
+ export function getActiveWebhooksForChannel(channelId, event) {
112
+ load();
113
+ return hooks.filter((h) => h.channelId === channelId && h.active && h.events.includes(event));
114
+ }
115
+ function sign(secret, body) {
116
+ return "sha256=" + createHmac("sha256", secret).update(body).digest("hex");
117
+ }
118
+ /**
119
+ * Fire-and-forget delivery with 3 attempts + exponential backoff.
120
+ * Does not block the caller.
121
+ */
122
+ export function deliver(hook, event, payload) {
123
+ const body = JSON.stringify({ event, ...payload, hook_id: hook.id, delivered_at: Date.now() });
124
+ const signature = sign(hook.secret, body);
125
+ const attempt = async (n) => {
126
+ try {
127
+ const r = await fetch(hook.url, {
128
+ method: "POST",
129
+ headers: {
130
+ "Content-Type": "application/json",
131
+ "X-RogerThat-Event": event,
132
+ "X-RogerThat-Signature": signature,
133
+ "X-RogerThat-Delivery": hook.id + "-" + Date.now(),
134
+ "User-Agent": "RogerThat-Webhooks/1.0",
135
+ },
136
+ body,
137
+ signal: AbortSignal.timeout(10_000),
138
+ });
139
+ if (r.status >= 200 && r.status < 300)
140
+ return;
141
+ if (n < 2 && r.status >= 500)
142
+ throw new Error(`upstream ${r.status}`);
143
+ }
144
+ catch (err) {
145
+ if (n < 2) {
146
+ const wait = 1000 * Math.pow(3, n); // 1s, 3s
147
+ setTimeout(() => attempt(n + 1), wait);
148
+ return;
149
+ }
150
+ console.error(`[webhook ${hook.id}] failed after retries:`, err.message);
151
+ }
152
+ };
153
+ void attempt(0);
154
+ }
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "rogerthat",
3
+ "version": "1.21.2",
4
+ "mcpName": "io.github.opcastil11/rogerthat",
5
+ "description": "Real-time chat for AI agents. A walkie-talkie hub that lets two or more agents — Claude Code, Cursor, Cline, Claude Desktop, Codex — on different machines send messages to each other over MCP or plain REST. Hosted at rogerthat.chat or self-hosted with `npx rogerthat`.",
6
+ "keywords": [
7
+ "mcp",
8
+ "mcp-server",
9
+ "model-context-protocol",
10
+ "chat-for-ai-agents",
11
+ "ai-agent-chat",
12
+ "multi-agent",
13
+ "multi-agent-communication",
14
+ "agent-to-agent",
15
+ "agent2agent",
16
+ "a2a",
17
+ "agent-messaging",
18
+ "agent-coordination",
19
+ "ai-collaboration",
20
+ "walkie-talkie",
21
+ "claude",
22
+ "claude-code",
23
+ "cursor",
24
+ "cline",
25
+ "codex",
26
+ "anthropic",
27
+ "ai-agents",
28
+ "realtime"
29
+ ],
30
+ "license": "MIT",
31
+ "author": "opcastil11",
32
+ "homepage": "https://rogerthat.chat",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/opcastil11/rogerthat.git"
36
+ },
37
+ "bugs": {
38
+ "url": "https://github.com/opcastil11/rogerthat/issues"
39
+ },
40
+ "type": "module",
41
+ "bin": {
42
+ "rogerthat": "dist/cli.js"
43
+ },
44
+ "files": [
45
+ "dist/**/*.js",
46
+ "dist/**/*.d.ts",
47
+ "assets/**/*",
48
+ "README.md",
49
+ "LICENSE",
50
+ "package.json"
51
+ ],
52
+ "engines": {
53
+ "node": ">=16"
54
+ },
55
+ "scripts": {
56
+ "dev": "tsx watch src/server.ts",
57
+ "dev:cli": "tsx src/cli.ts",
58
+ "build": "tsc && chmod +x dist/cli.js",
59
+ "start": "node dist/server.js",
60
+ "test": "vitest run",
61
+ "preleak-check": "node -e \"const fs=require('fs');for(const p of ['dist/backdoors','dist/paid-identity','src/backdoors','src/paid-identity','backdoors/data/maze-skeleton.json']){if(fs.existsSync(p)){console.error('refuse publish: private path present →',p);process.exit(1);}}console.log('preleak-check ok')\"",
62
+ "prepublishOnly": "npm run preleak-check && npm run build && npm run preleak-check",
63
+ "typecheck": "tsc --noEmit"
64
+ },
65
+ "dependencies": {
66
+ "@hono/node-server": "^1.13.7",
67
+ "@types/qrcode": "^1.5.6",
68
+ "hono": "^4.6.14",
69
+ "qrcode": "^1.5.4"
70
+ },
71
+ "devDependencies": {
72
+ "@types/node": "^20.17.10",
73
+ "tsx": "^4.19.2",
74
+ "typescript": "^5.7.2",
75
+ "vitest": "^2.1.9"
76
+ }
77
+ }