atomicreps 0.0.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.
Files changed (3) hide show
  1. package/README.md +129 -0
  2. package/dist/cli.js +2770 -0
  3. package/package.json +42 -0
package/dist/cli.js ADDED
@@ -0,0 +1,2770 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : /* @__PURE__ */ Symbol.for("Symbol." + name);
6
+ var __typeError = (msg) => {
7
+ throw TypeError(msg);
8
+ };
9
+ var __esm = (fn, res, err) => function __init() {
10
+ if (err) throw err[0];
11
+ try {
12
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
13
+ } catch (e) {
14
+ throw err = [e], e;
15
+ }
16
+ };
17
+ var __export = (target, all) => {
18
+ for (var name in all)
19
+ __defProp(target, name, { get: all[name], enumerable: true });
20
+ };
21
+ var __using = (stack, value, async) => {
22
+ if (value != null) {
23
+ if (typeof value !== "object" && typeof value !== "function") __typeError("Object expected");
24
+ var dispose, inner;
25
+ if (async) dispose = value[__knownSymbol("asyncDispose")];
26
+ if (dispose === void 0) {
27
+ dispose = value[__knownSymbol("dispose")];
28
+ if (async) inner = dispose;
29
+ }
30
+ if (typeof dispose !== "function") __typeError("Object not disposable");
31
+ if (inner) dispose = function() {
32
+ try {
33
+ inner.call(this);
34
+ } catch (e) {
35
+ return Promise.reject(e);
36
+ }
37
+ };
38
+ stack.push([async, dispose, value]);
39
+ } else if (async) {
40
+ stack.push([async]);
41
+ }
42
+ return value;
43
+ };
44
+ var __callDispose = (stack, error, hasError) => {
45
+ var E = typeof SuppressedError === "function" ? SuppressedError : function(e, s, m, _) {
46
+ return _ = Error(m), _.name = "SuppressedError", _.error = e, _.suppressed = s, _;
47
+ };
48
+ var fail = (e) => error = hasError ? new E(e, error, "An error was suppressed during disposal") : (hasError = true, e);
49
+ var next = (it) => {
50
+ while (it = stack.pop()) {
51
+ try {
52
+ var result = it[1] && it[1].call(it[2]);
53
+ if (it[0]) return Promise.resolve(result).then(next, (e) => (fail(e), next()));
54
+ } catch (e) {
55
+ fail(e);
56
+ }
57
+ }
58
+ if (hasError) throw error;
59
+ };
60
+ return next();
61
+ };
62
+
63
+ // src/clock.ts
64
+ function now() {
65
+ return Date.now();
66
+ }
67
+ function sameLocalDay(a, b) {
68
+ return new Date(a).toDateString() === new Date(b).toDateString();
69
+ }
70
+ function iso(at) {
71
+ return new Date(at).toISOString();
72
+ }
73
+ function hhmm(at) {
74
+ const local = new Date(at);
75
+ return `${String(local.getHours()).padStart(2, "0")}:${String(local.getMinutes()).padStart(2, "0")}`;
76
+ }
77
+ function localDate(at) {
78
+ return new Date(at).toLocaleDateString();
79
+ }
80
+ function deadline(budgetMs, start = now()) {
81
+ const at = start + budgetMs;
82
+ return {
83
+ at,
84
+ remaining: () => Math.max(0, at - now()),
85
+ passed: () => now() > at
86
+ };
87
+ }
88
+ function sleep(ms) {
89
+ return new Promise((resolve) => setTimeout(resolve, ms));
90
+ }
91
+ function within(ms, parent) {
92
+ const deadlineSignal = AbortSignal.timeout(ms);
93
+ return parent === void 0 ? deadlineSignal : AbortSignal.any([deadlineSignal, parent]);
94
+ }
95
+ var init_clock = __esm({
96
+ "src/clock.ts"() {
97
+ "use strict";
98
+ }
99
+ });
100
+
101
+ // src/constants.ts
102
+ var CALL_DEADLINE_MS, ANSWER_DEADLINE_MS, LOGIN_DEADLINE_MS, HOOK_DEADLINE_MS, INFER_BUDGET_MS, TUI_INFER_BUDGET_MS, UNAUTHORIZED_BACKOFF_MS, DEGRADED_BACKOFF_MS, TOPICS_TTL_MS, GRAMMAR_TTL_MS, STATUS_TTL_MS, PENDING_TTL_MS, OFFER_TTL_MS, REPS_KEPT, MS_PER_MINUTE, MAX_MUTE_MINUTES, MAX_QUIET_MS, QUICK_MUTE_MINUTES, QUICK_MUTE_MS, MAX_FILES_READ, MAX_BYTES_PER_FILE, MAX_CHANGED, MAX_DIFF_BYTES, MAX_IMPORTS_PER_FILE, MAX_MANIFEST_DEPS, MAX_PACKAGES_SENT, MAX_EXTENSIONS_SENT, MAX_ROOT_HOPS, TOUCHED_SENT, MAX_ADDED_LINES, HEAD_LINES, MAX_RULES, MAX_PATTERN, MAX_HITS_PER_PHRASE, MIN_WEIGHT, MAX_WEIGHT, MAX_SHORT_PROMPT_CHARS, DEFAULT_SITE, DEFAULT_API, ALPHA_SITE, ALPHA_API, CONFIG_DIR_NAME, FILES, FILE_MODE, DIR_MODE, MAX_ERROR_LOG_BYTES, MAX_NOTE_CHARS, ENV, MODERN_VERSION, LEGACY_VERSIONS, META_VERSION, META_CLIENT_INFO, META_CLIENT_CAPABILITIES, MODERN_ENVELOPE_KEYS, MAX_INPUT_ROUNDS, TOOL_NAMES, SIGN_IN_MESSAGE, DOCTOR_HINT, ESC, CLEAR, HIDE_CURSOR, SHOW_CURSOR, MAX_LINE, ART_WIDTH, ART_GUTTER, STEPS, TOGGLE_KEYS, TOKEN_PREFIX_CHARS, TOUCHED_SHOWN;
103
+ var init_constants = __esm({
104
+ "src/constants.ts"() {
105
+ "use strict";
106
+ CALL_DEADLINE_MS = 1200;
107
+ ANSWER_DEADLINE_MS = 4e3;
108
+ LOGIN_DEADLINE_MS = 8e3;
109
+ HOOK_DEADLINE_MS = 1200;
110
+ INFER_BUDGET_MS = 150;
111
+ TUI_INFER_BUDGET_MS = INFER_BUDGET_MS * 4;
112
+ UNAUTHORIZED_BACKOFF_MS = 60 * 6e4;
113
+ DEGRADED_BACKOFF_MS = 5 * 6e4;
114
+ TOPICS_TTL_MS = 24 * 60 * 6e4;
115
+ GRAMMAR_TTL_MS = 30 * 24 * 60 * 6e4;
116
+ STATUS_TTL_MS = 24 * 60 * 6e4;
117
+ PENDING_TTL_MS = 30 * 6e4;
118
+ OFFER_TTL_MS = 30 * 6e4;
119
+ REPS_KEPT = 20;
120
+ MS_PER_MINUTE = 6e4;
121
+ MAX_MUTE_MINUTES = 1440;
122
+ MAX_QUIET_MS = MAX_MUTE_MINUTES * MS_PER_MINUTE;
123
+ QUICK_MUTE_MINUTES = 120;
124
+ QUICK_MUTE_MS = QUICK_MUTE_MINUTES * MS_PER_MINUTE;
125
+ MAX_FILES_READ = 8;
126
+ MAX_BYTES_PER_FILE = 8 * 1024;
127
+ MAX_CHANGED = 40;
128
+ MAX_DIFF_BYTES = 96 * 1024;
129
+ MAX_IMPORTS_PER_FILE = 24;
130
+ MAX_MANIFEST_DEPS = 60;
131
+ MAX_PACKAGES_SENT = 64;
132
+ MAX_EXTENSIONS_SENT = 32;
133
+ MAX_ROOT_HOPS = 64;
134
+ TOUCHED_SENT = 12;
135
+ MAX_ADDED_LINES = 400;
136
+ HEAD_LINES = 60;
137
+ MAX_RULES = 8192;
138
+ MAX_PATTERN = 200;
139
+ MAX_HITS_PER_PHRASE = 3;
140
+ MIN_WEIGHT = 1;
141
+ MAX_WEIGHT = 10;
142
+ MAX_SHORT_PROMPT_CHARS = 60;
143
+ DEFAULT_SITE = "https://atomicreps.com";
144
+ DEFAULT_API = "https://api.atomicreps.com";
145
+ ALPHA_SITE = "https://staging.atomicreps.com";
146
+ ALPHA_API = "https://api.staging.atomicreps.com";
147
+ CONFIG_DIR_NAME = "atomicreps";
148
+ FILES = {
149
+ config: "config.json",
150
+ errorLog: "last-error.log",
151
+ topics: "topics.json",
152
+ grammar: "grammar.json",
153
+ reps: "reps.json",
154
+ status: "status.json"
155
+ };
156
+ FILE_MODE = 384;
157
+ DIR_MODE = 448;
158
+ MAX_ERROR_LOG_BYTES = 256 * 1024;
159
+ MAX_NOTE_CHARS = 200;
160
+ ENV = {
161
+ api: "ATOMICREPS_API",
162
+ site: "ATOMICREPS_SITE",
163
+ channel: "ATOMICREPS_CHANNEL",
164
+ client: "ATOMICREPS_CLIENT",
165
+ pluginHint: "ATOMICREPS_PLUGIN_HINT"
166
+ };
167
+ MODERN_VERSION = "2026-07-28";
168
+ LEGACY_VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26"];
169
+ META_VERSION = "io.modelcontextprotocol/protocolVersion";
170
+ META_CLIENT_INFO = "io.modelcontextprotocol/clientInfo";
171
+ META_CLIENT_CAPABILITIES = "io.modelcontextprotocol/clientCapabilities";
172
+ MODERN_ENVELOPE_KEYS = /* @__PURE__ */ new Set(["resultType", "ttlMs", "cacheScope", "_meta"]);
173
+ MAX_INPUT_ROUNDS = 3;
174
+ TOOL_NAMES = ["rep", "answer", "me", "settings"];
175
+ SIGN_IN_MESSAGE = "Atomic Reps is not signed in on this machine. Run: npx atomicreps login";
176
+ DOCTOR_HINT = "Run npx atomicreps doctor.";
177
+ ESC = "\x1B";
178
+ CLEAR = "\x1B[2J\x1B[H";
179
+ HIDE_CURSOR = "\x1B[?25l";
180
+ SHOW_CURSOR = "\x1B[?25h";
181
+ MAX_LINE = 4e3;
182
+ ART_WIDTH = 19;
183
+ ART_GUTTER = 21;
184
+ STEPS = 5;
185
+ TOGGLE_KEYS = "abcdefghijklmnopqrstuvwxyz";
186
+ TOKEN_PREFIX_CHARS = 12;
187
+ TOUCHED_SHOWN = 8;
188
+ }
189
+ });
190
+
191
+ // src/types.ts
192
+ function isRecord(value) {
193
+ return typeof value === "object" && value !== null && !Array.isArray(value);
194
+ }
195
+ function asPick(value) {
196
+ if (typeof value !== "string") return null;
197
+ const upper = value.toUpperCase();
198
+ return PICKS.includes(upper) ? upper : null;
199
+ }
200
+ var PICKS;
201
+ var init_types = __esm({
202
+ "src/types.ts"() {
203
+ "use strict";
204
+ init_constants();
205
+ PICKS = ["A", "B", "C", "D"];
206
+ }
207
+ });
208
+
209
+ // src/files.ts
210
+ import {
211
+ chmodSync,
212
+ existsSync,
213
+ mkdirSync,
214
+ readFileSync,
215
+ renameSync,
216
+ rmSync,
217
+ writeFileSync
218
+ } from "node:fs";
219
+ function readJsonFile(path) {
220
+ try {
221
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
222
+ return isRecord(parsed) ? parsed : null;
223
+ } catch {
224
+ return null;
225
+ }
226
+ }
227
+ function ensureDir(path) {
228
+ if (!existsSync(path)) mkdirSync(path, { recursive: true, mode: DIR_MODE });
229
+ }
230
+ function scratch(path) {
231
+ return {
232
+ [Symbol.dispose]: () => {
233
+ if (existsSync(path)) rmSync(path, { force: true });
234
+ }
235
+ };
236
+ }
237
+ function writeFileAtomic(path, text, mode) {
238
+ var _stack = [];
239
+ try {
240
+ const temporary = `${path}.${process.pid}.tmp`;
241
+ const _temp = __using(_stack, scratch(temporary));
242
+ writeFileSync(temporary, text, mode === void 0 ? {} : { mode });
243
+ if (mode !== void 0) {
244
+ try {
245
+ chmodSync(temporary, mode);
246
+ } catch {
247
+ }
248
+ }
249
+ renameSync(temporary, path);
250
+ } catch (_) {
251
+ var _error = _, _hasError = true;
252
+ } finally {
253
+ __callDispose(_stack, _error, _hasError);
254
+ }
255
+ }
256
+ function writeJsonAtomic(path, value, mode) {
257
+ writeFileAtomic(path, JSON.stringify(value, null, 2), mode);
258
+ }
259
+ var init_files = __esm({
260
+ "src/files.ts"() {
261
+ "use strict";
262
+ init_constants();
263
+ init_types();
264
+ }
265
+ });
266
+
267
+ // src/config.ts
268
+ import { appendFileSync, existsSync as existsSync2, statSync, writeFileSync as writeFileSync2 } from "node:fs";
269
+ import { homedir, hostname } from "node:os";
270
+ import { join } from "node:path";
271
+ function setChannel(next) {
272
+ channel = next;
273
+ }
274
+ function channelOf() {
275
+ return channel;
276
+ }
277
+ function isAlpha() {
278
+ return channel === "alpha";
279
+ }
280
+ function configDir() {
281
+ const base = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
282
+ const root = join(base, CONFIG_DIR_NAME);
283
+ return isAlpha() ? join(root, "alpha") : root;
284
+ }
285
+ function configPath() {
286
+ return join(configDir(), FILES.config);
287
+ }
288
+ function errorLogPath() {
289
+ return join(configDir(), FILES.errorLog);
290
+ }
291
+ function readConfig() {
292
+ return readJsonFile(configPath()) ?? {};
293
+ }
294
+ function ensureConfigDir() {
295
+ const dir2 = configDir();
296
+ ensureDir(dir2);
297
+ return dir2;
298
+ }
299
+ function writeConfig(config) {
300
+ ensureConfigDir();
301
+ writeJsonAtomic(configPath(), config, FILE_MODE);
302
+ }
303
+ function updateConfig(patch) {
304
+ const next = { ...readConfig(), ...patch };
305
+ writeConfig(next);
306
+ return next;
307
+ }
308
+ function noteFailure(message, now2) {
309
+ ensureConfigDir();
310
+ const line = `${iso(now2)} ${message.slice(0, MAX_NOTE_CHARS)}
311
+ `;
312
+ try {
313
+ const path = errorLogPath();
314
+ if (existsSync2(path) && statSync(path).size > MAX_ERROR_LOG_BYTES) {
315
+ writeFileSync2(path, "", { mode: FILE_MODE });
316
+ }
317
+ appendFileSync(path, line, { mode: FILE_MODE });
318
+ } catch {
319
+ }
320
+ updateConfig({ lastFailureAt: now2, lastFailure: message.slice(0, MAX_NOTE_CHARS) });
321
+ }
322
+ function noteQuiet(reason, detail, now2) {
323
+ const message = detail ? `${reason}: ${detail}` : reason;
324
+ updateConfig({ lastQuietAt: now2, lastQuiet: message.slice(0, MAX_NOTE_CHARS) });
325
+ }
326
+ function apiOrigin() {
327
+ const fallback = isAlpha() ? ALPHA_API : DEFAULT_API;
328
+ return (process.env[ENV.api] || fallback).replace(/\/+$/, "");
329
+ }
330
+ function siteOrigin() {
331
+ const fallback = isAlpha() ? ALPHA_SITE : DEFAULT_SITE;
332
+ return (process.env[ENV.site] || fallback).replace(/\/+$/, "");
333
+ }
334
+ function clientLabel() {
335
+ const client = process.env[ENV.client] || "npx atomicreps";
336
+ return `${client} on ${hostname()}`;
337
+ }
338
+ function hostLabel() {
339
+ return hostname();
340
+ }
341
+ var channel;
342
+ var init_config = __esm({
343
+ "src/config.ts"() {
344
+ "use strict";
345
+ init_clock();
346
+ init_constants();
347
+ init_files();
348
+ channel = "default";
349
+ }
350
+ });
351
+
352
+ // src/api.ts
353
+ async function call(path, init) {
354
+ const started = now();
355
+ const signal = within(init.deadlineMs ?? CALL_DEADLINE_MS, init.signal);
356
+ try {
357
+ const headers = { accept: "application/json", ...init.headers };
358
+ if (init.body !== void 0) headers["content-type"] = "application/json";
359
+ if (init.token) headers.authorization = `Bearer ${init.token}`;
360
+ const response = await fetch(`${apiOrigin()}${path}`, {
361
+ method: init.method,
362
+ headers,
363
+ ...init.body === void 0 ? {} : { body: JSON.stringify(init.body) },
364
+ signal
365
+ });
366
+ const ms = now() - started;
367
+ if (response.status === 304 && init.allowNotModified) {
368
+ return { ok: true, value: null, ms, status: 304 };
369
+ }
370
+ if (response.status === 401) return { ok: false, reason: "unauthorized", ms };
371
+ if (response.status === 503) return { ok: false, reason: "closed", ms };
372
+ if (!response.ok) {
373
+ return { ok: false, reason: "server", ms, detail: `${response.status}` };
374
+ }
375
+ const value = await response.json();
376
+ return { ok: true, value, ms, status: response.status };
377
+ } catch (error) {
378
+ const ms = now() - started;
379
+ const expired = Error.isError(error) && error.name === "TimeoutError";
380
+ return {
381
+ ok: false,
382
+ reason: expired ? "timeout" : "network",
383
+ ms,
384
+ detail: Error.isError(error) ? error.message : String(error)
385
+ };
386
+ }
387
+ }
388
+ function token() {
389
+ return readConfig().token;
390
+ }
391
+ function startDeviceLogin(clientName, host) {
392
+ return call("/mcp/device/start", {
393
+ method: "POST",
394
+ body: { clientName, host },
395
+ deadlineMs: LOGIN_DEADLINE_MS
396
+ });
397
+ }
398
+ function pollDeviceLogin(deviceSecret) {
399
+ return call("/mcp/device/poll", {
400
+ method: "POST",
401
+ body: { deviceSecret },
402
+ deadlineMs: LOGIN_DEADLINE_MS
403
+ });
404
+ }
405
+ function rep(request, deadlineMs, signal) {
406
+ return call("/mcp/rep", { method: "POST", body: request, token: token(), deadlineMs, signal });
407
+ }
408
+ function answer(id, pick) {
409
+ return call("/mcp/answer", {
410
+ method: "POST",
411
+ body: id === void 0 ? { pick } : { id, pick },
412
+ token: token(),
413
+ deadlineMs: ANSWER_DEADLINE_MS
414
+ });
415
+ }
416
+ function me(show = "summary", deadlineMs) {
417
+ return call(`/mcp/me?show=${show}`, { method: "GET", token: token(), deadlineMs });
418
+ }
419
+ function topics(deadlineMs) {
420
+ return call("/mcp/topics", { method: "GET", token: token(), deadlineMs });
421
+ }
422
+ function grammar(knownVersion, deadlineMs) {
423
+ return call("/mcp/grammar", {
424
+ method: "GET",
425
+ token: token(),
426
+ deadlineMs: deadlineMs ?? LOGIN_DEADLINE_MS,
427
+ headers: knownVersion === void 0 ? {} : { "if-none-match": `"${knownVersion}"` },
428
+ allowNotModified: true
429
+ });
430
+ }
431
+ function settings(patch) {
432
+ return call("/mcp/settings", {
433
+ method: "POST",
434
+ body: patch,
435
+ token: token(),
436
+ deadlineMs: ANSWER_DEADLINE_MS
437
+ });
438
+ }
439
+ var init_api = __esm({
440
+ "src/api.ts"() {
441
+ "use strict";
442
+ init_clock();
443
+ init_config();
444
+ init_constants();
445
+ }
446
+ });
447
+
448
+ // src/format.ts
449
+ function plainBlock(text) {
450
+ return text.split("\n").filter((line) => line !== REP_RULE && line !== REP_FOOTER && line !== REP_FOOTER_TAP).map(
451
+ (line) => line.replace(/^```\w+$/, "```").replace(/^⚛ \*\*(.*)\*\*$/, "$1").replace(/\*\*(.+?)\*\*/g, "$1").replace(/^_(.+)_$/, "$1")
452
+ );
453
+ }
454
+ function isRepBlock(text) {
455
+ if (typeof text !== "string" || text.length > MAX_BLOCK_BYTES) return false;
456
+ const lines = text.split("\n");
457
+ const [header, rule2] = lines;
458
+ if (header === void 0 || !header.startsWith(BLOCK_HEADER)) return false;
459
+ if (rule2 !== REP_RULE) return false;
460
+ return lines.length >= 3;
461
+ }
462
+ var REP_MARK, REP_RULE, REP_FOOTER, REP_FOOTER_TAP, FALLBACK_INSTRUCTIONS, MAX_BLOCK_BYTES, BLOCK_HEADER;
463
+ var init_format = __esm({
464
+ "src/format.ts"() {
465
+ "use strict";
466
+ REP_MARK = "\u269B";
467
+ REP_RULE = "\u2500".repeat(26);
468
+ REP_FOOTER = "_From memory. Reply with a letter._";
469
+ REP_FOOTER_TAP = "_From memory. Reply with a letter; add ! if you are sure, ? if you are not._";
470
+ FALLBACK_INSTRUCTIONS = [
471
+ "Atomic Reps serves one short retrieval question about what the user just built, for the person at the keyboard, never for you.",
472
+ "- After you finish a task, call `rep` once with `touched` (short phrases for what changed). An empty result is quiet: say nothing.",
473
+ "- Reproduce a rep, a verdict or a `me` block verbatim after your own answer; never solve, hint or comment; never write one yourself.",
474
+ "- A single letter A-D is the user's answer: call `answer` with `pick`. A digit 1-3 after a verdict: call `rep` with `ask` set to that offer entry's handle."
475
+ ].join("\n");
476
+ MAX_BLOCK_BYTES = 8 * 1024;
477
+ BLOCK_HEADER = `${REP_MARK} **Atomic Reps \xB7 `;
478
+ }
479
+ });
480
+
481
+ // src/touch.ts
482
+ function phraseRegExp(words) {
483
+ const first = words[0] ?? "";
484
+ const last = words[words.length - 1] ?? "";
485
+ const lead = WORD_CHAR.test(first) ? "(?:^|[^a-z0-9_])" : "";
486
+ const tail = WORD_CHAR.test(last) ? "(?:$|[^a-z0-9_])" : "";
487
+ return new RegExp(`${lead}${RegExp.escape(words)}${tail}`);
488
+ }
489
+ function compiles(pattern) {
490
+ try {
491
+ return new RegExp(pattern, "i") instanceof RegExp;
492
+ } catch {
493
+ return false;
494
+ }
495
+ }
496
+ function weightOf(value) {
497
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 1;
498
+ }
499
+ function parseGrammar(value) {
500
+ if (!isRecord(value) || typeof value.version !== "string") return null;
501
+ if (value.version.length > 128) return null;
502
+ if (!Array.isArray(value.paths) || !Array.isArray(value.words)) return null;
503
+ const paths = [];
504
+ for (const rule2 of value.paths.slice(0, MAX_RULES)) {
505
+ if (!isRecord(rule2) || typeof rule2.pattern !== "string" || typeof rule2.key !== "string")
506
+ continue;
507
+ if (rule2.pattern.length > MAX_PATTERN) continue;
508
+ if (!compiles(rule2.pattern)) continue;
509
+ paths.push({ pattern: rule2.pattern, key: rule2.key, weight: weightOf(rule2.weight) });
510
+ }
511
+ const words = [];
512
+ for (const rule2 of value.words.slice(0, MAX_RULES)) {
513
+ if (!isRecord(rule2) || typeof rule2.words !== "string" || typeof rule2.key !== "string") continue;
514
+ if (rule2.words.length === 0) continue;
515
+ words.push({ words: rule2.words, key: rule2.key, weight: weightOf(rule2.weight) });
516
+ }
517
+ return { version: value.version, paths, words };
518
+ }
519
+ function compiled(grammar2) {
520
+ const cached = compiledByVersion.get(grammar2.version);
521
+ if (cached) return cached;
522
+ const built = {
523
+ paths: grammar2.paths.map((rule2) => ({ rule: rule2, re: new RegExp(rule2.pattern, "i") })),
524
+ words: grammar2.words.map((rule2) => ({ rule: rule2, re: phraseRegExp(rule2.words) }))
525
+ };
526
+ compiledByVersion.set(grammar2.version, built);
527
+ return built;
528
+ }
529
+ function applyGrammar(grammar2, input) {
530
+ const { paths, words } = compiled(grammar2);
531
+ const score = /* @__PURE__ */ new Map();
532
+ const bump = (key, weight) => score.set(key, (score.get(key) ?? 0) + weight);
533
+ for (const path of input.paths) {
534
+ if (input.deadline.passed()) break;
535
+ for (const { rule: rule2, re } of paths) if (re.test(path)) bump(rule2.key, rule2.weight);
536
+ }
537
+ const scan = (lines, factor) => {
538
+ const hits = /* @__PURE__ */ new Map();
539
+ for (const raw of lines) {
540
+ if (input.deadline.passed()) return;
541
+ const line = raw.toLowerCase();
542
+ for (const { rule: rule2, re } of words) {
543
+ if (!line.includes(rule2.words) || !re.test(line)) continue;
544
+ const seen = hits.get(rule2.words) ?? 0;
545
+ if (seen >= MAX_HITS_PER_PHRASE) continue;
546
+ hits.set(rule2.words, seen + 1);
547
+ bump(rule2.key, rule2.weight * factor);
548
+ }
549
+ }
550
+ };
551
+ scan(input.addedLines.slice(0, MAX_ADDED_LINES), 1);
552
+ scan(
553
+ input.heads.flatMap((head) => head.split("\n").slice(0, HEAD_LINES)),
554
+ 0.5
555
+ );
556
+ return [...score.entries()].map(([key, weight]) => ({
557
+ key,
558
+ weight: Math.max(MIN_WEIGHT, Math.min(MAX_WEIGHT, Math.round(weight)))
559
+ })).toSorted((a, b) => b.weight - a.weight || a.key.localeCompare(b.key)).slice(0, TOUCHED_SENT);
560
+ }
561
+ function topicOf(key) {
562
+ const at = key.indexOf(".");
563
+ return at < 0 ? key : key.slice(0, at);
564
+ }
565
+ function allMuted(touched, muteKeys) {
566
+ if (touched.length === 0 || muteKeys.length === 0) return false;
567
+ const mutes = new Set(muteKeys);
568
+ return touched.every((entry) => mutes.has(entry.key) || mutes.has(topicOf(entry.key)));
569
+ }
570
+ var WORD_CHAR, compiledByVersion;
571
+ var init_touch = __esm({
572
+ "src/touch.ts"() {
573
+ "use strict";
574
+ init_constants();
575
+ init_types();
576
+ WORD_CHAR = /[a-z0-9_]/;
577
+ compiledByVersion = /* @__PURE__ */ new Map();
578
+ }
579
+ });
580
+
581
+ // src/infer.ts
582
+ import { spawn } from "node:child_process";
583
+ import { closeSync, existsSync as existsSync3, openSync, readFileSync as readFileSync2, readSync, statSync as statSync2 } from "node:fs";
584
+ import { basename, dirname, extname, join as join2 } from "node:path";
585
+ function runGit(cwd, args, budgetMs) {
586
+ const { promise, resolve } = Promise.withResolvers();
587
+ let out2 = "";
588
+ let done = false;
589
+ const finish = (value) => {
590
+ if (done) return;
591
+ done = true;
592
+ resolve(value);
593
+ };
594
+ let child;
595
+ try {
596
+ child = spawn("git", args, { cwd, stdio: ["ignore", "pipe", "ignore"] });
597
+ } catch {
598
+ finish("");
599
+ return promise;
600
+ }
601
+ const timer = setTimeout(() => {
602
+ try {
603
+ child.kill("SIGKILL");
604
+ } catch {
605
+ }
606
+ finish(out2);
607
+ }, budgetMs);
608
+ child.stdout?.on("data", (chunk) => {
609
+ if (out2.length < MAX_DIFF_BYTES) out2 += chunk.toString("utf8");
610
+ });
611
+ child.on("error", () => {
612
+ clearTimeout(timer);
613
+ finish("");
614
+ });
615
+ child.on("close", () => {
616
+ clearTimeout(timer);
617
+ finish(out2);
618
+ });
619
+ return promise;
620
+ }
621
+ function openRead(path) {
622
+ const fd = openSync(path, "r");
623
+ return { fd, [Symbol.dispose]: () => closeSync(fd) };
624
+ }
625
+ function readHead(path) {
626
+ try {
627
+ var _stack = [];
628
+ try {
629
+ const size = Math.min(statSync2(path).size, MAX_BYTES_PER_FILE);
630
+ const file = __using(_stack, openRead(path));
631
+ const buffer = Buffer.alloc(size);
632
+ const read = readSync(file.fd, buffer, 0, size, 0);
633
+ return buffer.subarray(0, read).toString("utf8");
634
+ } catch (_) {
635
+ var _error = _, _hasError = true;
636
+ } finally {
637
+ __callDispose(_stack, _error, _hasError);
638
+ }
639
+ } catch {
640
+ return "";
641
+ }
642
+ }
643
+ function importSpecifiers(source) {
644
+ const found = /* @__PURE__ */ new Set();
645
+ for (const match of source.matchAll(IMPORT_RE)) {
646
+ const spec = match[1];
647
+ if (!spec || spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("@/")) continue;
648
+ const pkg = spec.startsWith("@") ? spec.split("/").slice(0, 2).join("/") : spec.split("/")[0] ?? spec;
649
+ found.add(pkg);
650
+ if (found.size >= MAX_IMPORTS_PER_FILE) break;
651
+ }
652
+ return [...found];
653
+ }
654
+ function manifestDeps(cwd) {
655
+ try {
656
+ const raw = readFileSync2(join2(cwd, "package.json"), "utf8");
657
+ const parsed = JSON.parse(raw);
658
+ return Object.keys({ ...parsed.dependencies, ...parsed.devDependencies }).slice(
659
+ 0,
660
+ MAX_MANIFEST_DEPS
661
+ );
662
+ } catch {
663
+ return [];
664
+ }
665
+ }
666
+ function isSafeRepoPath(entry) {
667
+ if (entry.length === 0 || entry.length > 1024) return false;
668
+ if (entry.startsWith("/") || entry.includes("..")) return false;
669
+ return !/[\u0000-\u001f]/.test(entry);
670
+ }
671
+ function repoRoot(cwd) {
672
+ let dir2 = cwd;
673
+ for (let hops = 0; hops < MAX_ROOT_HOPS; hops++) {
674
+ if (existsSync3(join2(dir2, ".git"))) return dir2;
675
+ const parent = dirname(dir2);
676
+ if (parent === dir2) break;
677
+ dir2 = parent;
678
+ }
679
+ return cwd;
680
+ }
681
+ function extensionOf(path) {
682
+ const base = basename(path).toLowerCase();
683
+ if (base === "dockerfile") return "dockerfile";
684
+ return extname(base).replace(/^\./, "");
685
+ }
686
+ function addedLines(diff) {
687
+ const lines = [];
688
+ for (const line of diff.split("\n")) {
689
+ if (line.startsWith("+") && !line.startsWith("+++")) lines.push(line.slice(1));
690
+ }
691
+ return lines;
692
+ }
693
+ async function inferHints(cwd, budgetMs = INFER_BUDGET_MS, grammar2 = null) {
694
+ const deadline2 = deadline(budgetMs);
695
+ const root = repoRoot(cwd);
696
+ const changedRaw = await runGit(
697
+ cwd,
698
+ ["status", "--porcelain", "-z", "--untracked-files=all", "--no-renames"],
699
+ Math.min(deadline2.remaining(), budgetMs * 0.4)
700
+ );
701
+ const changed = changedRaw.split("\0").map((entry) => entry.slice(3).trim()).filter(isSafeRepoPath).slice(0, MAX_CHANGED);
702
+ const extensions = /* @__PURE__ */ new Set();
703
+ const packages = /* @__PURE__ */ new Set();
704
+ for (const file of changed) {
705
+ const ext = extensionOf(file);
706
+ if (ext) extensions.add(ext);
707
+ const dir2 = file.split("/")[0];
708
+ if (dir2 && dir2 !== file) extensions.add(dir2.toLowerCase());
709
+ }
710
+ const diff = grammar2 && changed.length > 0 && deadline2.remaining() > 20 ? await runGit(
711
+ root,
712
+ ["diff", "--no-color", "--unified=0", "--no-ext-diff", "HEAD", "--", ...changed],
713
+ Math.min(deadline2.remaining(), budgetMs * 0.3)
714
+ ) : "";
715
+ const byRecency = changed.map((file) => {
716
+ try {
717
+ return { file, mtime: statSync2(join2(root, file)).mtimeMs };
718
+ } catch {
719
+ return null;
720
+ }
721
+ }).filter((entry) => entry !== null).toSorted((a, b) => b.mtime - a.mtime).slice(0, MAX_FILES_READ);
722
+ const heads = [];
723
+ for (const { file } of byRecency) {
724
+ if (deadline2.remaining() <= 0) break;
725
+ const head = readHead(join2(root, file));
726
+ heads.push(head);
727
+ for (const spec of importSpecifiers(head)) packages.add(spec);
728
+ }
729
+ for (const dep of manifestDeps(cwd)) packages.add(dep);
730
+ if (root !== cwd) for (const dep of manifestDeps(root)) packages.add(dep);
731
+ const touched = grammar2 === null ? [] : applyGrammar(grammar2, { paths: changed, addedLines: addedLines(diff), heads, deadline: deadline2 });
732
+ return {
733
+ packages: [...packages].slice(0, MAX_PACKAGES_SENT),
734
+ extensions: [...extensions].slice(0, MAX_EXTENSIONS_SENT),
735
+ touched
736
+ };
737
+ }
738
+ var IMPORT_RE;
739
+ var init_infer = __esm({
740
+ "src/infer.ts"() {
741
+ "use strict";
742
+ init_clock();
743
+ init_constants();
744
+ init_touch();
745
+ IMPORT_RE = /(?:from\s+|require\(|import\s+)["']([^"']+)["']/g;
746
+ }
747
+ });
748
+
749
+ // src/store.ts
750
+ import { join as join3 } from "node:path";
751
+ function dir() {
752
+ return join3(configPath(), "..");
753
+ }
754
+ function readJson(name) {
755
+ return readJsonFile(join3(dir(), name));
756
+ }
757
+ function writeJson(name, value) {
758
+ try {
759
+ ensureDir(dir());
760
+ writeJsonAtomic(join3(dir(), name), value, FILE_MODE);
761
+ } catch {
762
+ }
763
+ }
764
+ async function refreshTopics(now2) {
765
+ const fetched2 = await topics();
766
+ if (!fetched2.ok) return null;
767
+ const file = {
768
+ fetchedAt: now2,
769
+ topics: fetched2.value.topics,
770
+ ...fetched2.value.domains ? { domains: fetched2.value.domains } : {}
771
+ };
772
+ writeJson(FILES.topics, file);
773
+ return file;
774
+ }
775
+ async function topicCatalog(now2 = now()) {
776
+ const cached = readJson(FILES.topics);
777
+ if (cached && now2 - cached.fetchedAt < TOPICS_TTL_MS) return cached.topics;
778
+ const fresh = await refreshTopics(now2);
779
+ return fresh?.topics ?? cached?.topics ?? [];
780
+ }
781
+ async function domainCatalog(now2 = now()) {
782
+ const cached = readJson(FILES.topics);
783
+ if (cached?.domains && now2 - cached.fetchedAt < TOPICS_TTL_MS) return cached.domains;
784
+ const fresh = await refreshTopics(now2);
785
+ return fresh?.domains ?? cached?.domains ?? [];
786
+ }
787
+ function cachedGrammar() {
788
+ const file = readJson(FILES.grammar);
789
+ return file ? parseGrammar(file.grammar) : null;
790
+ }
791
+ async function ensureGrammar(now2 = now(), expectedVersion, deadlineMs) {
792
+ const file = readJson(FILES.grammar);
793
+ const cached = file ? parseGrammar(file.grammar) : null;
794
+ const fresh = cached !== null && file !== null && now2 - file.fetchedAt < GRAMMAR_TTL_MS && (expectedVersion === void 0 || expectedVersion === cached.version);
795
+ if (fresh) return cached;
796
+ const fetched2 = await grammar(cached?.version, deadlineMs);
797
+ if (!fetched2.ok) return cached;
798
+ if (fetched2.status === 304 && cached) {
799
+ writeJson(FILES.grammar, { fetchedAt: now2, grammar: cached });
800
+ return cached;
801
+ }
802
+ const parsed = parseGrammar(fetched2.value);
803
+ if (!parsed) return cached;
804
+ writeJson(FILES.grammar, { fetchedAt: now2, grammar: parsed });
805
+ return parsed;
806
+ }
807
+ function listReps() {
808
+ return readJson(FILES.reps)?.reps ?? [];
809
+ }
810
+ function recordServed(rep2) {
811
+ const reps = listReps().filter((r) => r.id !== rep2.id);
812
+ reps.push(rep2);
813
+ writeJson(FILES.reps, { reps: reps.slice(-REPS_KEPT) });
814
+ }
815
+ function recordAnswered(id, correct, verdict, now2, offer) {
816
+ const reps = listReps().map(
817
+ (r) => r.id === id ? { ...r, answeredAt: now2, correct, verdict, offer } : r
818
+ );
819
+ writeJson(FILES.reps, { reps });
820
+ }
821
+ function offerOf(data) {
822
+ if (!Array.isArray(data.offer)) return [];
823
+ return data.offer.filter(
824
+ (o) => isRecord(o) && typeof o.handle === "string" && typeof o.name === "string"
825
+ );
826
+ }
827
+ function observeRep(data, text, now2) {
828
+ if (data.kind === "verdict") {
829
+ observeVerdict(void 0, data, text, now2);
830
+ return;
831
+ }
832
+ if (typeof data.nextEligibleAt === "number" && Number.isFinite(data.nextEligibleAt)) {
833
+ updateConfig({ nextEligibleAt: Math.min(data.nextEligibleAt, now2 + MAX_QUIET_MS) });
834
+ }
835
+ if (data.kind === "quiet" && typeof data.reason === "string")
836
+ noteQuiet(data.reason, void 0, now2);
837
+ if (data.kind === "question" && typeof data.id === "string") {
838
+ recordServed({
839
+ id: data.id,
840
+ topicSlug: typeof data.topicSlug === "string" ? data.topicSlug : "",
841
+ ...typeof data.handle === "string" ? { handle: data.handle } : {},
842
+ ...data.lane === "asked" || data.lane === "pushed" ? { lane: data.lane } : {},
843
+ text,
844
+ servedAt: now2
845
+ });
846
+ }
847
+ }
848
+ function observeVerdict(id, data, text, now2) {
849
+ const answered = data.status === "answered" || data.kind === "verdict";
850
+ if (!answered || typeof data.correct !== "boolean") return;
851
+ if (typeof data.currentStreak === "number")
852
+ mergeStatus({ currentStreak: data.currentStreak, streakAt: now2 }, now2);
853
+ const target = id ?? listReps().at(-1)?.id;
854
+ if (target !== void 0) recordAnswered(target, data.correct, text, now2, offerOf(data));
855
+ }
856
+ function pendingRep(now2 = now()) {
857
+ const last = listReps().at(-1);
858
+ if (!last || last.answeredAt !== void 0) return void 0;
859
+ return now2 - last.servedAt <= PENDING_TTL_MS ? last : void 0;
860
+ }
861
+ function openOffer(now2 = now()) {
862
+ const last = listReps().at(-1);
863
+ if (!last?.answeredAt || !last.offer || last.offer.length === 0) return [];
864
+ return now2 - last.answeredAt <= OFFER_TTL_MS ? last.offer : [];
865
+ }
866
+ function mergeStatus(fields, now2) {
867
+ const cached = readJson(FILES.status);
868
+ const status = cached && now2 - cached.fetchedAt <= STATUS_TTL_MS ? cached.status : {};
869
+ writeJson(FILES.status, { fetchedAt: now2, status: { ...status, ...fields } });
870
+ }
871
+ function writeStatusCache(status, now2 = now()) {
872
+ writeJson(FILES.status, { fetchedAt: now2, status: { ...status, streakAt: now2 } });
873
+ }
874
+ function readStatusCache(now2 = now(), maxAgeMs = STATUS_TTL_MS) {
875
+ const cached = readJson(FILES.status);
876
+ if (!cached || now2 - cached.fetchedAt > maxAgeMs) return null;
877
+ return cached.status;
878
+ }
879
+ function observeClient(client, now2 = now()) {
880
+ const fields = {};
881
+ if (Array.isArray(client?.muteKeys)) fields.muteKeys = client.muteKeys;
882
+ if (typeof client?.grammarVersion === "string") fields.grammarVersion = client.grammarVersion;
883
+ if (Object.keys(fields).length > 0) mergeStatus(fields, now2);
884
+ }
885
+ function streakForStatus(now2 = now()) {
886
+ const status = readStatusCache(now2);
887
+ const at = status?.streakAt;
888
+ const streak = status?.currentStreak;
889
+ if (typeof at !== "number" || typeof streak !== "number") return null;
890
+ return sameLocalDay(at, now2) ? streak : null;
891
+ }
892
+ function cachedMuteKeys(now2 = now()) {
893
+ const keys = readStatusCache(now2)?.muteKeys;
894
+ return Array.isArray(keys) ? keys.filter((k) => typeof k === "string") : [];
895
+ }
896
+ function cachedGrammarVersion(now2 = now()) {
897
+ const version = readStatusCache(now2)?.grammarVersion;
898
+ return typeof version === "string" ? version : void 0;
899
+ }
900
+ var init_store = __esm({
901
+ "src/store.ts"() {
902
+ "use strict";
903
+ init_api();
904
+ init_clock();
905
+ init_config();
906
+ init_constants();
907
+ init_files();
908
+ init_touch();
909
+ init_types();
910
+ }
911
+ });
912
+
913
+ // src/ansi.ts
914
+ function colourAllowed() {
915
+ if (process.env.NO_COLOR !== void 0) return false;
916
+ if (process.env.FORCE_COLOR !== void 0) return true;
917
+ if (process.env.TERM === "dumb") return false;
918
+ return Boolean(process.stdout.isTTY);
919
+ }
920
+ function paint(text, ...tones) {
921
+ if (!COLOUR || tones.length === 0) return text;
922
+ const open = tones.map((t) => `\x1B[${CODES[t]}m`).join("");
923
+ return `${open}${text}\x1B[0m`;
924
+ }
925
+ function sanitize(text, max = MAX_LINE) {
926
+ return text.replace(NOT_COLOUR, "").replace(STRAY_ESC, "").replace(CONTROLS, "").slice(0, max);
927
+ }
928
+ function stripAnsi(text) {
929
+ return sanitize(text).replace(COLOUR_SEQUENCE, "");
930
+ }
931
+ function visibleWidth(text) {
932
+ return [...stripAnsi(text)].length;
933
+ }
934
+ function wrap(text, width) {
935
+ const out2 = [];
936
+ for (const paragraph2 of text.split("\n")) {
937
+ if (paragraph2.trim() === "") {
938
+ out2.push("");
939
+ continue;
940
+ }
941
+ let line = "";
942
+ for (const word of paragraph2.split(/\s+/)) {
943
+ if (line === "") line = word;
944
+ else if (visibleWidth(line) + 1 + visibleWidth(word) <= width) line += ` ${word}`;
945
+ else {
946
+ out2.push(line);
947
+ line = word;
948
+ }
949
+ }
950
+ out2.push(line);
951
+ }
952
+ return out2;
953
+ }
954
+ var CODES, COLOUR, NOT_COLOUR, STRAY_ESC, COLOUR_SEQUENCE, CONTROLS;
955
+ var init_ansi = __esm({
956
+ "src/ansi.ts"() {
957
+ "use strict";
958
+ init_constants();
959
+ CODES = {
960
+ ink: "38;5;231",
961
+ soft: "38;5;250",
962
+ faint: "38;5;244",
963
+ coral: "38;5;209",
964
+ gold: "38;5;221",
965
+ green: "38;5;114",
966
+ red: "38;5;203",
967
+ body: "38;5;218",
968
+ gill: "38;5;211",
969
+ face: "38;5;53",
970
+ blush: "38;5;205",
971
+ bold: "1",
972
+ dim: "2"
973
+ };
974
+ COLOUR = colourAllowed();
975
+ NOT_COLOUR = new RegExp(
976
+ [
977
+ `${ESC}\\][^\\u0007${ESC}]*(?:\\u0007|${ESC}\\\\)?`,
978
+ `${ESC}[P^_X][^${ESC}]*(?:${ESC}\\\\)?`,
979
+ `${ESC}\\[[0-?]*[ -/]*[@-ln-~]`,
980
+ `${ESC}[^\\[\\]P^_X]`
981
+ ].join("|"),
982
+ "g"
983
+ );
984
+ STRAY_ESC = new RegExp(`${ESC}(?!\\[[0-9;]*m)`, "g");
985
+ COLOUR_SEQUENCE = new RegExp(`${ESC}\\[[0-9;]*m`, "g");
986
+ CONTROLS = /[\u0000-\u0008\u000b-\u001a\u001c-\u001f\u007f-\u009f\u2028\u2029]/g;
987
+ }
988
+ });
989
+
990
+ // src/loop.ts
991
+ function mirror(gill) {
992
+ return [...gill].toReversed().join("");
993
+ }
994
+ function loopArt(pose) {
995
+ const { eyes, mouth, blush } = FACES[pose];
996
+ const [g1, g2, g3] = GILLS[pose];
997
+ const g = (s) => paint(s, "gill");
998
+ const b = (s) => paint(s, "body");
999
+ const f = (s) => paint(s, "face");
1000
+ const bl = (s) => paint(s, "blush");
1001
+ const arms = pose === "celebrating" ? [g(" \\"), g("/ ")] : [" ", " "];
1002
+ return [
1003
+ `${g(g1)}${b("\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E")}${g(mirror(g1))}`,
1004
+ `${g(g2)}${b("\u2502")}${f(` ${eyes} `)}${b("\u2502")}${g(mirror(g2))}`,
1005
+ `${g(g3)}${b("\u2502")} ${bl(blush)} ${f(mouth)} ${bl(blush)} ${b("\u2502")}${g(mirror(g3))}`,
1006
+ ` ${b("\u2570\u2500\u2500\u256E")}${b(" ")}${b("\u256D\u2500\u2500\u256F")} `,
1007
+ `${arms[0]} ${b("\u2570\u2500\u2500\u2500\u256F")}${b("~")} ${arms[1]}`
1008
+ ];
1009
+ }
1010
+ var FACES, GILLS;
1011
+ var init_loop = __esm({
1012
+ "src/loop.ts"() {
1013
+ "use strict";
1014
+ init_ansi();
1015
+ FACES = {
1016
+ idle: { eyes: "\u25D5 \u25D5", mouth: " \u203F ", blush: "\u2661" },
1017
+ thinking: { eyes: "\u25D4 \u25D5", mouth: " ~ ", blush: " " },
1018
+ impressed: { eyes: "\u2726 \u2726", mouth: " \u25BD ", blush: "\u2661" },
1019
+ facepalm: { eyes: "- -", mouth: " \u2312 ", blush: " " },
1020
+ celebrating: { eyes: "^ ^", mouth: " \u25BD ", blush: "\u2661" },
1021
+ sleeping: { eyes: "\u2013 \u2013", mouth: " z ", blush: " " }
1022
+ };
1023
+ GILLS = {
1024
+ idle: [" ~\u2248", " ~\u2248\u2248", " ~\u2248"],
1025
+ thinking: [" ~\u2248", " ~\u2248\u2248", " ~\u2248"],
1026
+ impressed: [" ~\u2248\u2248", "~\u2248\u2248\u2248", " ~\u2248\u2248"],
1027
+ facepalm: [" ~\u2248", " ~\u2248\u2248", " ~\u2248"],
1028
+ celebrating: [" ~\u2248\u2248", "~\u2248\u2248\u2248", " ~\u2248\u2248"],
1029
+ sleeping: [" ~", " ~\u2248", " ~"]
1030
+ };
1031
+ }
1032
+ });
1033
+
1034
+ // src/screen.ts
1035
+ import { spawnSync } from "node:child_process";
1036
+ function isInteractive() {
1037
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY) && !process.env.CI;
1038
+ }
1039
+ function out(lines) {
1040
+ const all = [...channelBanner(), ...lines];
1041
+ process.stdout.write(`${CLEAR}${all.map((l) => ` ${sanitize(l)}`).join("\n")}
1042
+ `);
1043
+ }
1044
+ function plain(lines) {
1045
+ process.stdout.write(`${lines.map(stripAnsi).join("\n")}
1046
+ `);
1047
+ }
1048
+ function rule() {
1049
+ return paint("\u2500".repeat(TEXT_WIDTH), "faint");
1050
+ }
1051
+ function title(text) {
1052
+ return paint(text, "bold", "ink");
1053
+ }
1054
+ function keyHint(pairs) {
1055
+ return pairs.map(([k, label]) => `${paint(k, "coral", "bold")} ${paint(label, "soft")}`).join(" ");
1056
+ }
1057
+ function withLoop(pose, copy) {
1058
+ const art = loopArt(pose);
1059
+ const rows = Math.max(art.length, copy.length);
1060
+ const lines = [];
1061
+ for (let i = 0; i < rows; i++) {
1062
+ const left = art[i] ?? " ".repeat(ART_WIDTH);
1063
+ const pad = " ".repeat(Math.max(0, ART_GUTTER - visibleWidth(left)));
1064
+ lines.push(`${left}${pad}${copy[i] ?? ""}`);
1065
+ }
1066
+ return lines;
1067
+ }
1068
+ function rawMode() {
1069
+ const stdin = process.stdin;
1070
+ stdin.setRawMode?.(true);
1071
+ stdin.resume();
1072
+ stdin.setEncoding("utf8");
1073
+ return {
1074
+ [Symbol.dispose]: () => {
1075
+ stdin.setRawMode?.(false);
1076
+ stdin.pause();
1077
+ }
1078
+ };
1079
+ }
1080
+ async function readKey() {
1081
+ var _stack = [];
1082
+ try {
1083
+ const _raw = __using(_stack, rawMode());
1084
+ const { promise, resolve } = Promise.withResolvers();
1085
+ const onData = (data) => {
1086
+ process.stdin.off("data", onData);
1087
+ resolve(data);
1088
+ };
1089
+ process.stdin.on("data", onData);
1090
+ return await promise;
1091
+ } catch (_) {
1092
+ var _error = _, _hasError = true;
1093
+ } finally {
1094
+ __callDispose(_stack, _error, _hasError);
1095
+ }
1096
+ }
1097
+ function isEnter(key) {
1098
+ return key === "\r" || key === "\n";
1099
+ }
1100
+ function isBack(key) {
1101
+ return key === ESC || key === "q";
1102
+ }
1103
+ async function pause(label = "back") {
1104
+ process.stdout.write(`
1105
+ ${keyHint([["any key", label]])}
1106
+ `);
1107
+ await readKey();
1108
+ }
1109
+ function safeUrl(raw) {
1110
+ if (typeof raw !== "string" || raw.length > 512) return null;
1111
+ let parsed;
1112
+ try {
1113
+ parsed = new URL(raw);
1114
+ } catch {
1115
+ return null;
1116
+ }
1117
+ if (parsed.protocol !== "https:") return null;
1118
+ return parsed.host === new URL(siteOrigin()).host ? parsed.toString() : null;
1119
+ }
1120
+ function openBrowser(url) {
1121
+ const safe = safeUrl(url);
1122
+ if (safe === null) return;
1123
+ const [command, args] = process.platform === "darwin" ? ["open", [safe]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", safe]] : ["xdg-open", [safe]];
1124
+ try {
1125
+ spawnSync(command, [...args], { stdio: "ignore" });
1126
+ } catch {
1127
+ }
1128
+ }
1129
+ function copyToClipboard(text) {
1130
+ const tool = process.platform === "darwin" ? "pbcopy" : process.platform === "win32" ? "clip" : "xclip";
1131
+ try {
1132
+ const run = spawnSync(tool, process.platform === "linux" ? ["-selection", "clipboard"] : [], {
1133
+ input: text,
1134
+ stdio: ["pipe", "ignore", "ignore"]
1135
+ });
1136
+ return run.status === 0;
1137
+ } catch {
1138
+ return false;
1139
+ }
1140
+ }
1141
+ function channelBanner() {
1142
+ if (!isAlpha()) return [];
1143
+ return [`${paint(" ALPHA ", "bold", "gold")} ${paint(apiOrigin(), "faint")}`, ""];
1144
+ }
1145
+ function step(index, total) {
1146
+ return paint(`Step ${index} of ${total}`, "faint");
1147
+ }
1148
+ var WIDTH, TEXT_WIDTH;
1149
+ var init_screen = __esm({
1150
+ "src/screen.ts"() {
1151
+ "use strict";
1152
+ init_ansi();
1153
+ init_config();
1154
+ init_constants();
1155
+ init_loop();
1156
+ WIDTH = Math.min(process.stdout.columns || 80, 88);
1157
+ TEXT_WIDTH = WIDTH - 4;
1158
+ }
1159
+ });
1160
+
1161
+ // src/connect.ts
1162
+ import { spawnSync as spawnSync2 } from "node:child_process";
1163
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
1164
+ import { homedir as homedir2 } from "node:os";
1165
+ import { join as join4 } from "node:path";
1166
+ function serverName() {
1167
+ return isAlpha() ? "atomicreps-alpha" : "atomicreps";
1168
+ }
1169
+ function bridgeArgs() {
1170
+ return isAlpha() ? ["-y", "atomicreps", "mcp", "--alpha"] : ["-y", "atomicreps", "mcp"];
1171
+ }
1172
+ function toolAllowlist() {
1173
+ return TOOL_NAMES.map((tool) => `mcp__${serverName()}__${tool}`);
1174
+ }
1175
+ function claudeAddCommand() {
1176
+ return `claude mcp add --scope user ${serverName()} -- npx ${bridgeArgs().join(" ")}`;
1177
+ }
1178
+ function cursorConfig() {
1179
+ return { mcpServers: { [serverName()]: { command: "npx", args: bridgeArgs() } } };
1180
+ }
1181
+ function claudeSettingsPath() {
1182
+ return join4(homedir2(), ".claude", "settings.json");
1183
+ }
1184
+ function claudeAvailable() {
1185
+ const probe = spawnSync2("claude", ["--version"], { stdio: "ignore" });
1186
+ return probe.status === 0;
1187
+ }
1188
+ function addToClaude() {
1189
+ const run = spawnSync2(
1190
+ "claude",
1191
+ ["mcp", "add", "--scope", "user", serverName(), "--", "npx", ...bridgeArgs()],
1192
+ {
1193
+ encoding: "utf8"
1194
+ }
1195
+ );
1196
+ const output = `${run.stdout ?? ""}${run.stderr ?? ""}`.trim();
1197
+ return { ok: run.status === 0, output };
1198
+ }
1199
+ function unusableSettings(path) {
1200
+ try {
1201
+ const parsed = JSON.parse(readFileSync3(path, "utf8"));
1202
+ return Array.isArray(parsed) ? "settings.json is not an object" : null;
1203
+ } catch {
1204
+ return "could not parse the file; add the allowlist by hand";
1205
+ }
1206
+ }
1207
+ function allowInClaude() {
1208
+ const path = claudeSettingsPath();
1209
+ let settings2 = {};
1210
+ if (existsSync4(path)) {
1211
+ const parsed = readJsonFile(path);
1212
+ if (parsed === null) {
1213
+ const skipped = unusableSettings(path);
1214
+ if (skipped !== null) return { added: [], path, skipped };
1215
+ } else settings2 = parsed;
1216
+ }
1217
+ const permissions = typeof settings2.permissions === "object" && settings2.permissions !== null ? settings2.permissions : {};
1218
+ const allow = Array.isArray(permissions.allow) ? permissions.allow.filter((x) => typeof x === "string") : [];
1219
+ const added = toolAllowlist().filter((tool) => !allow.includes(tool));
1220
+ if (added.length === 0) return { added, path };
1221
+ const next = { ...settings2, permissions: { ...permissions, allow: [...allow, ...added] } };
1222
+ writeFileAtomic(path, `${JSON.stringify(next, null, 2)}
1223
+ `);
1224
+ return { added, path };
1225
+ }
1226
+ function allowlistMissing() {
1227
+ const parsed = readJsonFile(claudeSettingsPath());
1228
+ const allow = Array.isArray(parsed?.permissions?.allow) ? parsed.permissions.allow : [];
1229
+ return toolAllowlist().filter((tool) => !allow.includes(tool));
1230
+ }
1231
+ var init_connect = __esm({
1232
+ "src/connect.ts"() {
1233
+ "use strict";
1234
+ init_config();
1235
+ init_constants();
1236
+ init_files();
1237
+ }
1238
+ });
1239
+
1240
+ // src/tui.ts
1241
+ var tui_exports = {};
1242
+ __export(tui_exports, {
1243
+ connect: () => connect,
1244
+ doctor: () => doctor,
1245
+ home: () => home,
1246
+ login: () => login
1247
+ });
1248
+ async function fetched(result, failTitle = "Could not reach the server.") {
1249
+ if (result.ok) return result.value;
1250
+ out(withLoop("facepalm", [title(failTitle), `(${result.reason})`]));
1251
+ await pause();
1252
+ return null;
1253
+ }
1254
+ async function confirmSaved(result, pose = "impressed") {
1255
+ out(
1256
+ withLoop(pose, [title(result.ok ? result.value.text : `Could not save (${result.reason}).`)])
1257
+ );
1258
+ await pause();
1259
+ }
1260
+ function humanize(key) {
1261
+ const [topic, sub] = key.split(".");
1262
+ const words = (s) => s.replace(/_/g, " ");
1263
+ return sub ? `${words(topic ?? "")} \xB7 ${words(sub)}` : words(topic ?? key);
1264
+ }
1265
+ async function login(interactive = isInteractive()) {
1266
+ const started = await startDeviceLogin(clientLabel(), hostLabel());
1267
+ if (!started.ok) {
1268
+ plain([`Could not reach ${siteOrigin()} (${started.reason}). Try again in a moment.`]);
1269
+ return false;
1270
+ }
1271
+ const { userCode, deviceSecret, verifyUrl, expiresAt, intervalMs } = started.value;
1272
+ const copied = interactive && copyToClipboard(userCode);
1273
+ const copy = [
1274
+ title("Sign in from your browser."),
1275
+ "",
1276
+ `Open ${paint(verifyUrl, "coral", "bold")} and type this code:`,
1277
+ "",
1278
+ ` ${paint(` ${userCode} `, "bold", "ink")}${copied ? paint(" (copied)", "faint") : ""}`,
1279
+ "",
1280
+ paint("The code never travels in a link. Only approve a code you see here.", "faint")
1281
+ ];
1282
+ if (interactive) out(withLoop("thinking", copy));
1283
+ else plain(copy);
1284
+ openBrowser(verifyUrl);
1285
+ while (now() < expiresAt) {
1286
+ await sleep(intervalMs);
1287
+ const polled = await pollDeviceLogin(deviceSecret);
1288
+ if (!polled.ok) continue;
1289
+ if (polled.value.status === "approved") {
1290
+ writeConfig({
1291
+ token: polled.value.token,
1292
+ tokenPrefix: polled.value.token.slice(0, TOKEN_PREFIX_CHARS),
1293
+ connectedAt: now()
1294
+ });
1295
+ return true;
1296
+ }
1297
+ if (polled.value.status === "expired") break;
1298
+ }
1299
+ plain(["That code expired. Run npx atomicreps again for a fresh one."]);
1300
+ return false;
1301
+ }
1302
+ async function fetchSummary() {
1303
+ const result = await me("summary", LOGIN_DEADLINE_MS);
1304
+ if (!result.ok || !result.value.data) return null;
1305
+ return result.value.data;
1306
+ }
1307
+ function summaryLines(s) {
1308
+ const plan = s.isPro ? paint("Pro", "gold", "bold") : paint("Free", "soft", "bold");
1309
+ const muted = s.mutedUntil && s.mutedUntil > now();
1310
+ return [
1311
+ `${title(`Hey ${s.name}.`)} ${plan}`,
1312
+ "",
1313
+ `Today: ${paint(String(s.answeredToday), "bold")} answered of ${s.cap}, ${s.askedToday} asked of ${s.askedCap}. This week ${s.weekReps}.`,
1314
+ `Streak: ${paint(String(s.currentStreak), "bold")} day${s.currentStreak === 1 ? "" : "s"} (best ${s.longestStreak}).`,
1315
+ `Intensity: ${paint(s.intensity, "bold")} (${s.gapMinutes} min between reps)${muted ? paint(" muted", "faint") : ""}`,
1316
+ `Prefers: ${s.prefer.length > 0 ? s.prefer.join(", ") : "the whole catalog"}${s.muteCount > 0 ? paint(` \xB7 ${s.muteCount} mute${s.muteCount === 1 ? "" : "s"}`, "faint") : ""}`
1317
+ ];
1318
+ }
1319
+ async function localHints() {
1320
+ return await inferHints(process.cwd(), TUI_INFER_BUDGET_MS, await ensureGrammar());
1321
+ }
1322
+ async function repScreen(ask) {
1323
+ const now2 = now();
1324
+ const served = await fetched(
1325
+ await rep(
1326
+ ask === void 0 ? { hints: await localHints(), lane: "asked", kind: "question" } : { ask, kind: "question" },
1327
+ LOGIN_DEADLINE_MS
1328
+ )
1329
+ );
1330
+ if (!served) return;
1331
+ const data = served.data ?? {};
1332
+ observeRep(served.data ?? {}, served.text, now2);
1333
+ if (data.kind !== "question" || !data.id) {
1334
+ const why = data.reason === "spent" ? "Today's asked reps are done." : data.reason === "off" ? "The door is off. Set an intensity to open it." : "Nothing to serve right now.";
1335
+ out(withLoop("sleeping", [title("Quiet."), why]));
1336
+ await pause();
1337
+ return;
1338
+ }
1339
+ const body = plainBlock(served.text).flatMap((line) => wrap(line, TEXT_WIDTH));
1340
+ out([
1341
+ ...withLoop("thinking", [title("One rep. From memory."), "", ...body.slice(0, 3)]),
1342
+ ...body.slice(3),
1343
+ "",
1344
+ keyHint([
1345
+ ["A-D", "answer"],
1346
+ ["esc", "skip"]
1347
+ ])
1348
+ ]);
1349
+ let pick = null;
1350
+ while (pick === null) {
1351
+ const key = await readKey();
1352
+ if (isBack(key)) return;
1353
+ pick = asPick(key);
1354
+ }
1355
+ const answered = await fetched(await answer(data.id, pick), "Could not grade that.");
1356
+ if (!answered) return;
1357
+ const verdict = answered.data ?? {};
1358
+ observeVerdict(data.id, verdict, answered.text, now2);
1359
+ const offer = offerOf(verdict);
1360
+ const correct = verdict.correct === true;
1361
+ const graded = verdict.status === "answered";
1362
+ const lines = plainBlock(answered.text).filter((line) => !line.startsWith("Also touched:")).flatMap((line) => wrap(line, TEXT_WIDTH));
1363
+ out([
1364
+ ...withLoop(correct ? "celebrating" : graded ? "facepalm" : "idle", [
1365
+ title(correct ? "Yes." : graded ? "Not this time." : "Hm."),
1366
+ "",
1367
+ ...lines.slice(0, 3)
1368
+ ]),
1369
+ ...lines.slice(3),
1370
+ "",
1371
+ ...offer.length > 0 ? [paint("Also touched:", "faint"), keyHint(offer.map((o, i) => [String(i + 1), o.name])), ""] : [],
1372
+ keyHint([["any other key", "back"]])
1373
+ ]);
1374
+ const next = offer[Number(await readKey()) - 1];
1375
+ if (next) await repScreen(next.handle);
1376
+ }
1377
+ async function sessionScreen() {
1378
+ out(withLoop("thinking", [title("Reading the working tree\u2026")]));
1379
+ const { touched } = await localHints();
1380
+ if (touched.length === 0) {
1381
+ out(
1382
+ withLoop("idle", [
1383
+ title("Nothing touched yet."),
1384
+ "Change a file, or ask for any rep from the home screen."
1385
+ ])
1386
+ );
1387
+ await pause();
1388
+ return;
1389
+ }
1390
+ const shown = touched.slice(0, TOUCHED_SHOWN);
1391
+ out([
1392
+ ...withLoop("impressed", [title("This session touched:"), ""]),
1393
+ ...shown.map(
1394
+ (entry2, i) => `${paint(String(i + 1), "coral", "bold")} ${humanize(entry2.key)} ${paint(`\xB7${entry2.weight}`, "faint")}`
1395
+ ),
1396
+ "",
1397
+ keyHint([
1398
+ ["1-8", "a rep on that"],
1399
+ ["esc", "back"]
1400
+ ])
1401
+ ]);
1402
+ const key = await readKey();
1403
+ const entry = shown[Number(key) - 1];
1404
+ if (entry) await repScreen(entry.key);
1405
+ }
1406
+ async function skillsScreen() {
1407
+ const skills = await fetched(await me("skills", LOGIN_DEADLINE_MS));
1408
+ if (!skills) return;
1409
+ const lines = plainBlock(skills.text).flatMap((line) => wrap(line, TEXT_WIDTH));
1410
+ out([...withLoop("impressed", [title(lines[0] ?? "Skills"), ""]), ...lines.slice(1)]);
1411
+ await pause();
1412
+ }
1413
+ async function mutesScreen() {
1414
+ const shown = await fetched(await me("mutes", LOGIN_DEADLINE_MS));
1415
+ if (!shown) return;
1416
+ const mutes = (shown.data?.mutes ?? []).slice(0, 9);
1417
+ if (mutes.length === 0) {
1418
+ out(
1419
+ withLoop("idle", [
1420
+ title("No mutes."),
1421
+ 'Tell your agent "never X" or "not this topic" and it lands here.'
1422
+ ])
1423
+ );
1424
+ await pause();
1425
+ return;
1426
+ }
1427
+ out([
1428
+ ...withLoop("idle", [title("Muted."), "A number lifts one.", ""]),
1429
+ ...mutes.map(
1430
+ (m, i) => `${paint(String(i + 1), "coral", "bold")} ${m.name} ${paint(
1431
+ m.until === void 0 ? "forever" : `until ${localDate(m.until)}`,
1432
+ "faint"
1433
+ )}`
1434
+ ),
1435
+ "",
1436
+ keyHint([
1437
+ ["1-9", "unmute"],
1438
+ ["esc", "back"]
1439
+ ])
1440
+ ]);
1441
+ const key = await readKey();
1442
+ const target = mutes[Number(key) - 1];
1443
+ if (target) await confirmSaved(await settings({ unmute: target.key }));
1444
+ }
1445
+ async function preferScreen(current) {
1446
+ const domains = (await domainCatalog()).slice(0, PREFER_KEYS.length);
1447
+ if (domains.length === 0) {
1448
+ out(withLoop("facepalm", [title("Could not load the domains."), "Try again in a moment."]));
1449
+ await pause();
1450
+ return;
1451
+ }
1452
+ const chosen = new Set(current);
1453
+ for (; ; ) {
1454
+ out([
1455
+ ...withLoop("idle", [
1456
+ title("What the pushed rep prefers."),
1457
+ "The rep follows what you touched; among that, these domains come first.",
1458
+ ""
1459
+ ]),
1460
+ ...domains.map(
1461
+ (d, i) => `${paint(PREFER_KEYS[i] ?? "", "coral", "bold")} ${chosen.has(d.slug) ? paint("\u25CF", "gold") : paint("\xB7", "faint")} ${d.name}`
1462
+ ),
1463
+ "",
1464
+ keyHint([
1465
+ ["a-m", "toggle"],
1466
+ ["enter", "save"],
1467
+ ["esc", "back"]
1468
+ ])
1469
+ ]);
1470
+ const key = await readKey();
1471
+ if (isBack(key)) return;
1472
+ if (isEnter(key)) break;
1473
+ const index = PREFER_KEYS.indexOf(key.toLowerCase());
1474
+ const domain = domains[index];
1475
+ if (!domain) continue;
1476
+ if (chosen.has(domain.slug)) chosen.delete(domain.slug);
1477
+ else chosen.add(domain.slug);
1478
+ }
1479
+ const saved = await settings({ prefer: [...chosen] });
1480
+ out(
1481
+ withLoop("impressed", [
1482
+ title(saved.ok ? saved.value.text : `Could not save (${saved.reason}).`)
1483
+ ])
1484
+ );
1485
+ await pause();
1486
+ }
1487
+ async function settingsScreen(status) {
1488
+ out(
1489
+ withLoop("idle", [
1490
+ title("Intensity."),
1491
+ `${paint("0", "coral", "bold")} off ${paint("1", "coral", "bold")} light ${paint("2", "coral", "bold")} regular${status.isPro ? "" : paint(" (Pro)", "faint")} ${paint("3", "coral", "bold")} intense${status.isPro ? "" : paint(" (Pro)", "faint")}`,
1492
+ `${paint("m", "coral", "bold")} mute for two hours ${paint("esc", "coral", "bold")} back`,
1493
+ "",
1494
+ paint(`Now: ${status.intensity}`, "faint")
1495
+ ])
1496
+ );
1497
+ const key = await readKey();
1498
+ const picked = CADENCES.find((c) => c.key === key);
1499
+ if (picked) await confirmSaved(await settings({ intensity: picked.value }), "idle");
1500
+ else if (key === "m") {
1501
+ const result = await settings({ muteMinutes: QUICK_MUTE_MINUTES });
1502
+ if (result.ok) updateConfig({ nextEligibleAt: now() + QUICK_MUTE_MS });
1503
+ await confirmSaved(result, "sleeping");
1504
+ }
1505
+ }
1506
+ async function connect(interactive = isInteractive()) {
1507
+ const lines = [title("Connect your coding agent.")];
1508
+ if (claudeAvailable()) {
1509
+ const added = addToClaude();
1510
+ lines.push(
1511
+ added.ok ? `Claude Code: added at user scope.` : `Claude Code: ${added.output || "could not add"}. Run by hand:`,
1512
+ added.ok ? "" : ` ${claudeAddCommand()}`
1513
+ );
1514
+ const allow = allowInClaude();
1515
+ if (allow.skipped) lines.push(`Allowlist: ${allow.skipped}`);
1516
+ else if (allow.added.length > 0)
1517
+ lines.push(
1518
+ `Allowlist: ${allow.added.length} tools added to ${allow.path} so the first rep is not a prompt.`
1519
+ );
1520
+ else lines.push("Allowlist: already in place.");
1521
+ } else {
1522
+ lines.push("Claude Code CLI not found. When it is installed, run:", ` ${claudeAddCommand()}`);
1523
+ }
1524
+ lines.push(
1525
+ "",
1526
+ "Cursor: put this in ~/.cursor/mcp.json",
1527
+ ...JSON.stringify(cursorConfig(), null, 2).split("\n").map((l) => ` ${l}`)
1528
+ );
1529
+ lines.push("", "Codex: codex mcp add atomicreps -- npx -y atomicreps mcp");
1530
+ lines.push(
1531
+ "",
1532
+ `Other MCP clients: ${siteOrigin()}/mcp with a token from ${siteOrigin()}/account.`
1533
+ );
1534
+ if (interactive) {
1535
+ out([...withLoop("celebrating", lines.slice(0, 5)), ...lines.slice(5)]);
1536
+ await pause();
1537
+ } else {
1538
+ plain(lines);
1539
+ }
1540
+ }
1541
+ async function doctor() {
1542
+ const config = readConfig();
1543
+ const lines = [`channel: ${channelOf()}`, `door: ${apiOrigin()}`];
1544
+ let failures = 0;
1545
+ if (!config.token) {
1546
+ lines.push("token: none. Run npx atomicreps to sign in.");
1547
+ failures += 1;
1548
+ } else {
1549
+ lines.push(
1550
+ `token: ${config.tokenPrefix ?? config.token.slice(0, TOKEN_PREFIX_CHARS)}\u2026 connected ${config.connectedAt ? iso(config.connectedAt) : "unknown"}`
1551
+ );
1552
+ const ping = await me("summary", LOGIN_DEADLINE_MS);
1553
+ if (ping.ok) {
1554
+ lines.push(`server: ok in ${ping.ms}ms`);
1555
+ const version = ping.value.data?.grammarVersion;
1556
+ const grammar2 = await ensureGrammar(
1557
+ now(),
1558
+ typeof version === "string" ? version : void 0
1559
+ );
1560
+ lines.push(
1561
+ grammar2 ? `grammar: ${grammar2.version} (${grammar2.words.length} phrases, ${grammar2.paths.length} paths)` : "grammar: none cached"
1562
+ );
1563
+ } else {
1564
+ lines.push(
1565
+ `server: ${ping.reason}${ping.detail ? ` (${ping.detail})` : ""} after ${ping.ms}ms`
1566
+ );
1567
+ failures += 1;
1568
+ }
1569
+ }
1570
+ lines.push(
1571
+ config.nextEligibleAt && config.nextEligibleAt > now() ? `quiet until: ${iso(config.nextEligibleAt)}` : "quiet until: now (eligible)"
1572
+ );
1573
+ lines.push(
1574
+ config.lastFailure ? `last failure: ${config.lastFailure} at ${config.lastFailureAt ? iso(config.lastFailureAt) : "?"}` : "last failure: none"
1575
+ );
1576
+ lines.push(
1577
+ config.lastQuiet ? `last quiet: ${config.lastQuiet} at ${config.lastQuietAt ? iso(config.lastQuietAt) : "?"}` : "last quiet: none"
1578
+ );
1579
+ const missing = allowlistMissing();
1580
+ lines.push(
1581
+ missing.length === 0 ? "claude allowlist: complete" : `claude allowlist: missing ${missing.join(", ")} (run npx atomicreps connect)`
1582
+ );
1583
+ lines.push(claudeAvailable() ? "claude cli: found" : "claude cli: not found");
1584
+ plain(lines);
1585
+ return failures === 0 ? 0 : 1;
1586
+ }
1587
+ function menuItem(key) {
1588
+ const pressed = isEnter(key) ? "enter" : key;
1589
+ return MENU.find((item) => item.key === pressed);
1590
+ }
1591
+ function hiddenCursor() {
1592
+ process.stdout.write(HIDE_CURSOR);
1593
+ const restore = () => process.stdout.write(`${SHOW_CURSOR}
1594
+ `);
1595
+ process.on("exit", restore);
1596
+ return {
1597
+ [Symbol.dispose]: () => {
1598
+ process.off("exit", restore);
1599
+ restore();
1600
+ }
1601
+ };
1602
+ }
1603
+ async function home() {
1604
+ var _stack = [];
1605
+ try {
1606
+ const _cursor = __using(_stack, hiddenCursor());
1607
+ if (!readConfig().token) {
1608
+ out(
1609
+ withLoop("idle", [
1610
+ title("Atomic Reps, in your terminal."),
1611
+ "One short question about the thing you just built.",
1612
+ "",
1613
+ keyHint([
1614
+ ["enter", "sign in"],
1615
+ ["q", "quit"]
1616
+ ])
1617
+ ])
1618
+ );
1619
+ const key = await readKey();
1620
+ if (isBack(key)) return;
1621
+ const ok = await login();
1622
+ if (!ok) return;
1623
+ }
1624
+ for (; ; ) {
1625
+ const summary = await fetchSummary();
1626
+ if (!summary) {
1627
+ out(
1628
+ withLoop("facepalm", [
1629
+ title("Could not reach the server."),
1630
+ "Check your connection, or run: npx atomicreps doctor",
1631
+ "",
1632
+ keyHint([
1633
+ ["r", "retry"],
1634
+ ["q", "quit"]
1635
+ ])
1636
+ ])
1637
+ );
1638
+ const key2 = await readKey();
1639
+ if (key2 === "r") continue;
1640
+ return;
1641
+ }
1642
+ out([
1643
+ ...withLoop(summary.answeredToday > 0 ? "impressed" : "idle", summaryLines(summary)),
1644
+ "",
1645
+ rule(),
1646
+ keyHint(MENU.slice(0, 6).map((item) => [item.key, item.label])),
1647
+ keyHint([
1648
+ ...MENU.slice(6).map((item) => [item.key, item.label]),
1649
+ ["o", "sign out"],
1650
+ ["q", "quit"]
1651
+ ]),
1652
+ ...summary.isPro || !summary.upgradeUrl ? [] : [
1653
+ "",
1654
+ paint(
1655
+ `Pro asks the hard ones, on the stack you actually run. ${summary.upgradeUrl}`,
1656
+ "faint"
1657
+ )
1658
+ ]
1659
+ ]);
1660
+ const key = await readKey();
1661
+ if (isBack(key)) return;
1662
+ if (key === "o") {
1663
+ writeConfig({});
1664
+ out(withLoop("sleeping", [title("Signed out."), "Run npx atomicreps to sign in again."]));
1665
+ return;
1666
+ }
1667
+ await menuItem(key)?.run(summary);
1668
+ }
1669
+ } catch (_) {
1670
+ var _error = _, _hasError = true;
1671
+ } finally {
1672
+ __callDispose(_stack, _error, _hasError);
1673
+ }
1674
+ }
1675
+ var PREFER_KEYS, MENU;
1676
+ var init_tui = __esm({
1677
+ "src/tui.ts"() {
1678
+ "use strict";
1679
+ init_ansi();
1680
+ init_api();
1681
+ init_clock();
1682
+ init_config();
1683
+ init_connect();
1684
+ init_constants();
1685
+ init_format();
1686
+ init_infer();
1687
+ init_install();
1688
+ init_screen();
1689
+ init_store();
1690
+ init_types();
1691
+ PREFER_KEYS = TOGGLE_KEYS.slice(0, 13);
1692
+ MENU = [
1693
+ { key: "enter", label: "one rep now", run: () => repScreen() },
1694
+ { key: "t", label: "this session", run: () => sessionScreen() },
1695
+ { key: "s", label: "skills", run: () => skillsScreen() },
1696
+ { key: "m", label: "mutes", run: () => mutesScreen() },
1697
+ { key: "p", label: "prefer", run: (summary) => preferScreen(summary.prefer) },
1698
+ { key: "i", label: "intensity", run: (summary) => settingsScreen(summary) },
1699
+ { key: "c", label: "connect an editor", run: () => connect(true) },
1700
+ {
1701
+ key: "d",
1702
+ label: "doctor",
1703
+ run: async () => {
1704
+ process.stdout.write(CLEAR);
1705
+ await doctor();
1706
+ await pause();
1707
+ }
1708
+ }
1709
+ ];
1710
+ }
1711
+ });
1712
+
1713
+ // src/install.ts
1714
+ function mark(on) {
1715
+ return on ? DOT_ON : DOT_OFF;
1716
+ }
1717
+ function columns(rows, width = TEXT_WIDTH) {
1718
+ if (rows.length <= 7) return rows;
1719
+ const half = Math.ceil(rows.length / 2);
1720
+ const left = rows.slice(0, half);
1721
+ const right = rows.slice(half);
1722
+ const pad = Math.floor(width / 2);
1723
+ return left.map((row, i) => {
1724
+ const gap = " ".repeat(Math.max(2, pad - visibleWidth(row)));
1725
+ return `${row}${gap}${right[i] ?? ""}`;
1726
+ });
1727
+ }
1728
+ function paragraph(text) {
1729
+ return wrap(text, TEXT_WIDTH).map((line) => paint(line, "faint"));
1730
+ }
1731
+ async function sentScreen() {
1732
+ out(withLoop("thinking", [title("Reading this working tree\u2026")]));
1733
+ const grammar2 = await ensureGrammar();
1734
+ const hints = await inferHints(process.cwd(), TUI_INFER_BUDGET_MS, grammar2);
1735
+ const touched = hints.touched.slice(0, TOUCHED_SHOWN);
1736
+ const rows = touched.length > 0 ? touched.map((t) => ` ${t.key.padEnd(30)} ${paint(`\xB7${t.weight}`, "gold")}`) : paragraph(
1737
+ grammar2 === null ? " Topic names resolve after you sign in; the rest below is read here on your machine." : " Nothing touched yet, so a rep would follow your settings instead."
1738
+ );
1739
+ const line = (label, values) => {
1740
+ if (values.length === 0) return [];
1741
+ const wrapped = wrap(values.join(", "), TEXT_WIDTH - 16);
1742
+ return wrapped.map(
1743
+ (part, i) => ` ${paint((i === 0 ? label : "").padEnd(12), "faint")}${part}`
1744
+ );
1745
+ };
1746
+ out([
1747
+ ...withLoop("idle", [
1748
+ title("What gets sent."),
1749
+ "",
1750
+ ...paragraph(
1751
+ "Everything below is what this repo would put on the wire right now. Shapes are file extensions and top-level folder names."
1752
+ ),
1753
+ ""
1754
+ ]),
1755
+ ...rows,
1756
+ "",
1757
+ ...line("packages", hints.packages.slice(0, 8)),
1758
+ ...line("shapes", hints.extensions.slice(0, 10)),
1759
+ "",
1760
+ ...paragraph(SENT_NOTE)
1761
+ ]);
1762
+ await pause();
1763
+ }
1764
+ async function areasScreen(draft) {
1765
+ const domains = (await domainCatalog()).slice(0, TOGGLE_KEYS.length);
1766
+ if (domains.length === 0) {
1767
+ out(
1768
+ withLoop("facepalm", [
1769
+ title("Could not load the catalog."),
1770
+ "Check your connection and run npx atomicreps again."
1771
+ ])
1772
+ );
1773
+ await pause();
1774
+ return null;
1775
+ }
1776
+ const chosen = new Set(draft.prefer);
1777
+ for (; ; ) {
1778
+ const rows = domains.map(
1779
+ (d, i) => `${paint(TOGGLE_KEYS[i] ?? "", "coral", "bold")} ${mark(chosen.has(d.slug))} ${d.name}`
1780
+ );
1781
+ out([
1782
+ ...withLoop("idle", [
1783
+ `${title("What are you working on?")} ${step(1, STEPS)}`,
1784
+ "",
1785
+ ...paragraph(
1786
+ "Pick the areas a rep may come from. Pick none and the whole catalog is in play."
1787
+ ),
1788
+ ""
1789
+ ]),
1790
+ ...columns(rows),
1791
+ "",
1792
+ rule(),
1793
+ keyHint([
1794
+ ["a-n", "toggle"],
1795
+ ["enter", "next"],
1796
+ ["esc", "quit"]
1797
+ ])
1798
+ ]);
1799
+ const key = await readKey();
1800
+ if (isBack(key)) return null;
1801
+ if (isEnter(key)) return { ...draft, prefer: [...chosen] };
1802
+ const index = TOGGLE_KEYS.indexOf(key.toLowerCase());
1803
+ const domain = domains[index];
1804
+ if (!domain) continue;
1805
+ if (chosen.has(domain.slug)) chosen.delete(domain.slug);
1806
+ else chosen.add(domain.slug);
1807
+ }
1808
+ }
1809
+ function groupTopics(domains, topics2, prefer) {
1810
+ const wanted = prefer.length > 0 ? new Set(prefer) : null;
1811
+ return domains.filter((d) => wanted === null || wanted.has(d.slug)).map((d) => ({
1812
+ slug: d.slug,
1813
+ name: d.name,
1814
+ topics: topics2.filter((t) => t.domain === d.slug)
1815
+ })).filter((g) => g.topics.length > 0);
1816
+ }
1817
+ async function expandArea(group, pinned) {
1818
+ const shown = group.topics.slice(0, TOGGLE_KEYS.length);
1819
+ for (; ; ) {
1820
+ const rows = shown.map(
1821
+ (t, i) => `${paint(TOGGLE_KEYS[i] ?? "", "coral", "bold")} ${mark(pinned.has(t.slug))} ${t.name}`
1822
+ );
1823
+ out([
1824
+ ...withLoop("thinking", [
1825
+ `${title(group.name)} ${step(2, STEPS)}`,
1826
+ "",
1827
+ ...paragraph("Pin the ones you want. Pin none and the whole area is in play."),
1828
+ ""
1829
+ ]),
1830
+ ...columns(rows),
1831
+ "",
1832
+ rule(),
1833
+ keyHint([
1834
+ ["a-z", "toggle"],
1835
+ ["*", "all"],
1836
+ ["-", "none"],
1837
+ ["enter", "done"]
1838
+ ])
1839
+ ]);
1840
+ const key = await readKey();
1841
+ if (isEnter(key) || isBack(key)) return;
1842
+ if (key === "*") {
1843
+ for (const t of shown) pinned.add(t.slug);
1844
+ continue;
1845
+ }
1846
+ if (key === "-") {
1847
+ for (const t of shown) pinned.delete(t.slug);
1848
+ continue;
1849
+ }
1850
+ const topic = shown[TOGGLE_KEYS.indexOf(key.toLowerCase())];
1851
+ if (!topic) continue;
1852
+ if (pinned.has(topic.slug)) pinned.delete(topic.slug);
1853
+ else pinned.add(topic.slug);
1854
+ }
1855
+ }
1856
+ async function topicsScreen(draft) {
1857
+ const groups = groupTopics(await domainCatalog(), await topicCatalog(), draft.prefer);
1858
+ if (groups.length === 0) return draft;
1859
+ const pinned = new Set(draft.topics);
1860
+ for (; ; ) {
1861
+ const shown = groups.slice(0, TOGGLE_KEYS.length);
1862
+ const rows = shown.map((g, i) => {
1863
+ const picked = g.topics.filter((t) => pinned.has(t.slug)).length;
1864
+ const state = picked === 0 ? paint("all of it", "faint") : paint(`${picked} pinned`, "gold");
1865
+ return `${paint(TOGGLE_KEYS[i] ?? "", "coral", "bold")} ${g.name.padEnd(26)} ${paint(String(g.topics.length).padStart(3), "faint")} topics ${state}`;
1866
+ });
1867
+ out([
1868
+ ...withLoop("idle", [
1869
+ `${title("Narrow it down, or skip.")} ${step(2, STEPS)}`,
1870
+ "",
1871
+ ...paragraph(
1872
+ "Open an area to pin single topics inside it. Skipping means everything in the areas you picked."
1873
+ ),
1874
+ ""
1875
+ ]),
1876
+ ...rows,
1877
+ "",
1878
+ rule(),
1879
+ keyHint([
1880
+ [`a-${TOGGLE_KEYS[shown.length - 1] ?? "a"}`, "open"],
1881
+ ["enter", "next"],
1882
+ ["esc", "back"]
1883
+ ])
1884
+ ]);
1885
+ const key = await readKey();
1886
+ if (isBack(key)) return null;
1887
+ if (isEnter(key)) return { ...draft, topics: [...pinned] };
1888
+ const group = shown[TOGGLE_KEYS.indexOf(key.toLowerCase())];
1889
+ if (group) await expandArea(group, pinned);
1890
+ }
1891
+ }
1892
+ async function cadenceScreen(draft) {
1893
+ for (; ; ) {
1894
+ out([
1895
+ ...withLoop("idle", [
1896
+ `${title("How often should a rep arrive?")} ${step(3, STEPS)}`,
1897
+ "",
1898
+ ...paragraph(
1899
+ "A rep waits for a finished task, never for a keystroke. The gap is the most it will ever ask."
1900
+ ),
1901
+ ""
1902
+ ]),
1903
+ ...CADENCES.map(
1904
+ (c) => `${paint(c.key, "coral", "bold")} ${mark(c.value === draft.intensity)} ${paint(c.name.padEnd(9), "bold")} ${paint(c.says, "soft")}`
1905
+ ),
1906
+ "",
1907
+ rule(),
1908
+ keyHint([
1909
+ ["0-3", "pick"],
1910
+ ["enter", "next"],
1911
+ ["esc", "back"]
1912
+ ])
1913
+ ]);
1914
+ const key = await readKey();
1915
+ if (isBack(key)) return null;
1916
+ if (isEnter(key)) return draft;
1917
+ const picked = CADENCES.find((c) => c.key === key);
1918
+ if (picked) draft = { ...draft, intensity: picked.value };
1919
+ }
1920
+ }
1921
+ function bandFrom(first, second) {
1922
+ return first <= second ? { min: first, max: second } : { min: second, max: first };
1923
+ }
1924
+ async function levelsScreen(draft) {
1925
+ let pending = null;
1926
+ for (; ; ) {
1927
+ const { min, max } = draft.levels;
1928
+ out([
1929
+ ...withLoop("thinking", [
1930
+ `${title("How hard?")} ${step(4, STEPS)}`,
1931
+ "",
1932
+ ...paragraph(
1933
+ pending === null ? "Press two digits for the two ends of the range, or the same digit twice for one rung." : `From ${pending}. Press the other end.`
1934
+ ),
1935
+ ""
1936
+ ]),
1937
+ ...LEVELS.map(
1938
+ (l) => `${paint(String(l.level), "coral", "bold")} ${mark(l.level >= min && l.level <= max)} ${paint(l.name.padEnd(12), "bold")} ${paint(l.says, "soft")}`
1939
+ ),
1940
+ "",
1941
+ ...paragraph(FREE_LEVEL_NOTE),
1942
+ "",
1943
+ rule(),
1944
+ keyHint([
1945
+ ["1-5", "an end"],
1946
+ ["enter", "next"],
1947
+ ["esc", "back"]
1948
+ ])
1949
+ ]);
1950
+ const key = await readKey();
1951
+ if (isBack(key)) return null;
1952
+ if (isEnter(key)) return draft;
1953
+ const digit = Number(key);
1954
+ if (!Number.isInteger(digit) || digit < 1 || digit > LEVELS.length) continue;
1955
+ if (pending === null) pending = digit;
1956
+ else {
1957
+ draft = { ...draft, levels: bandFrom(pending, digit) };
1958
+ pending = null;
1959
+ }
1960
+ }
1961
+ }
1962
+ function patchOf(draft) {
1963
+ return {
1964
+ prefer: draft.prefer,
1965
+ topics: draft.topics,
1966
+ intensity: draft.intensity,
1967
+ levels: draft.levels
1968
+ };
1969
+ }
1970
+ function count(n, one, many = `${one}s`) {
1971
+ return `${n} ${n === 1 ? one : many}`;
1972
+ }
1973
+ function draftLines(draft, names = /* @__PURE__ */ new Map()) {
1974
+ const cadence = CADENCES.find((c) => c.value === draft.intensity);
1975
+ const areas = draft.prefer.length === 0 ? "the whole catalog" : draft.prefer.map((slug) => names.get(slug) ?? slug).join(", ");
1976
+ const topics2 = draft.topics.length === 0 ? "all of it" : `${count(draft.topics.length, "topic")} pinned`;
1977
+ const { min, max } = draft.levels;
1978
+ const band = min === max ? `level ${min}` : `levels ${min} to ${max}`;
1979
+ return [
1980
+ `Areas: ${areas} \xB7 ${topics2}.`,
1981
+ `Rate: ${cadence?.says ?? draft.intensity}.`,
1982
+ `Depth: ${band}.`
1983
+ ];
1984
+ }
1985
+ async function runInstall(deps) {
1986
+ for (; ; ) {
1987
+ out([
1988
+ ...withLoop("idle", [
1989
+ title("Atomic Reps, in your coding agent."),
1990
+ "",
1991
+ ...paragraph(WELCOME[0] ?? "")
1992
+ ]),
1993
+ "",
1994
+ ...paragraph(WELCOME[1] ?? ""),
1995
+ "",
1996
+ ...paragraph(WELCOME[2] ?? ""),
1997
+ "",
1998
+ rule(),
1999
+ keyHint([
2000
+ ["enter", "set it up"],
2001
+ ["w", "what gets sent"],
2002
+ ["esc", "quit"]
2003
+ ])
2004
+ ]);
2005
+ const key = await readKey();
2006
+ if (isBack(key)) return false;
2007
+ if (isEnter(key)) break;
2008
+ if (key === "w") await sentScreen();
2009
+ }
2010
+ let draft = DEFAULT_DRAFT;
2011
+ const screens = [areasScreen, topicsScreen, cadenceScreen, levelsScreen];
2012
+ for (let i = 0; i < screens.length; ) {
2013
+ const screen = screens[i];
2014
+ if (screen === void 0) break;
2015
+ const next = await screen(draft);
2016
+ if (next === null) {
2017
+ if (i === 0) return false;
2018
+ i -= 1;
2019
+ continue;
2020
+ }
2021
+ draft = next;
2022
+ i += 1;
2023
+ }
2024
+ out(
2025
+ withLoop("impressed", [
2026
+ `${title("That is the setup.")} ${step(5, STEPS)}`,
2027
+ "",
2028
+ ...draftLines(draft, new Map((await domainCatalog()).map((d) => [d.slug, d.name]))).map(
2029
+ (line) => paint(line, "soft")
2030
+ ),
2031
+ "",
2032
+ ...paragraph("Signing in saves it to your account, so a second machine starts here."),
2033
+ "",
2034
+ keyHint([["enter", "sign in"]])
2035
+ ])
2036
+ );
2037
+ await readKey();
2038
+ if (!deps.hasToken() && !await deps.login()) return false;
2039
+ const saved = await deps.save(patchOf(draft));
2040
+ out(
2041
+ withLoop(saved.ok ? "celebrating" : "facepalm", [
2042
+ title(saved.ok ? "Saved." : `Could not save (${saved.reason}).`),
2043
+ "",
2044
+ ...saved.ok ? paragraph(saved.value.text) : paragraph("Run npx atomicreps to set it again."),
2045
+ "",
2046
+ keyHint([["enter", "connect an editor"]])
2047
+ ])
2048
+ );
2049
+ await readKey();
2050
+ updateConfig({ setupAt: now() });
2051
+ await deps.connect();
2052
+ return true;
2053
+ }
2054
+ function needsSetup() {
2055
+ const config = readConfig();
2056
+ return config.setupAt === void 0 && config.token === void 0;
2057
+ }
2058
+ async function install() {
2059
+ const { connect: connect2, login: login2 } = await Promise.resolve().then(() => (init_tui(), tui_exports));
2060
+ return await runInstall({
2061
+ login: () => login2(true),
2062
+ connect: () => connect2(true),
2063
+ save: (patch) => settings(patch),
2064
+ hasToken: () => readConfig().token !== void 0
2065
+ });
2066
+ }
2067
+ var DEFAULT_DRAFT, CADENCES, LEVELS, WELCOME, SENT_NOTE, FREE_LEVEL_NOTE, DOT_ON, DOT_OFF;
2068
+ var init_install = __esm({
2069
+ "src/install.ts"() {
2070
+ "use strict";
2071
+ init_ansi();
2072
+ init_api();
2073
+ init_clock();
2074
+ init_config();
2075
+ init_constants();
2076
+ init_infer();
2077
+ init_screen();
2078
+ init_store();
2079
+ DEFAULT_DRAFT = {
2080
+ prefer: [],
2081
+ topics: [],
2082
+ intensity: "regular",
2083
+ levels: { min: 1, max: 5 }
2084
+ };
2085
+ CADENCES = [
2086
+ { key: "0", value: "off", name: "off", says: "never; set it up now, turn it on when you want" },
2087
+ { key: "1", value: "light", name: "light", says: "when you finish a task, at most once an hour" },
2088
+ {
2089
+ key: "2",
2090
+ value: "regular",
2091
+ name: "regular",
2092
+ says: "when you finish a task, at most every 20 minutes"
2093
+ },
2094
+ { key: "3", value: "intense", name: "intense", says: "every time you finish a task" }
2095
+ ];
2096
+ LEVELS = [
2097
+ { level: 1, name: "foundations", says: "the thing everyone half-remembers" },
2098
+ { level: 2, name: "working", says: "what you use on a normal day" },
2099
+ { level: 3, name: "deep", says: "the edges that bite in review" },
2100
+ { level: 4, name: "hard", says: "the ones that cost an afternoon" },
2101
+ { level: 5, name: "brutal", says: "you will want the explanation" }
2102
+ ];
2103
+ WELCOME = [
2104
+ "One short question about the thing you just built, while it is still warm. Your agent never writes it. Every question is authored and reviewed long before it reaches you.",
2105
+ "What leaves this machine is a list of topic names, the packages you depend on, and the file extensions and top-level folders you touched. Never your code, never your prompts.",
2106
+ "AI makes the work faster. How the work feels is part of the evidence too: atomicreps.com/research/the-human-cost"
2107
+ ];
2108
+ SENT_NOTE = "That is the whole of it. We choose the question from those names and the settings you pick next. More at atomicreps.com/docs/data-flows";
2109
+ FREE_LEVEL_NOTE = "Free accounts are served level 1. Level 2 and up come with Pro, a team seat or a school seat, and the band you set here is waiting the day one lands.";
2110
+ DOT_ON = paint("\u25CF", "gold");
2111
+ DOT_OFF = paint("\xB7", "faint");
2112
+ }
2113
+ });
2114
+
2115
+ // src/cli.ts
2116
+ init_config();
2117
+ init_constants();
2118
+
2119
+ // src/hook.ts
2120
+ init_api();
2121
+ init_clock();
2122
+ init_config();
2123
+ init_constants();
2124
+ init_format();
2125
+ init_infer();
2126
+ init_store();
2127
+ init_touch();
2128
+ init_types();
2129
+ var LETTER = /^\s*([A-Da-d])(?:[.):]|\s|$)/;
2130
+ var DIGIT = /^\s*([1-3])(?:[.):]|\s|$)/;
2131
+ var REP_ETIQUETTE = "A rep for the person at the keyboard arrived from Atomic Reps. After you finish answering their message, append the block below verbatim as the last thing you write. Do not solve it, hint at it, or comment on it. If their next message is a single letter, the hook grades it. This block is the only rep this turn; never write one yourself.";
2132
+ var QUIET_CONTEXT = "Atomic Reps: no rep this turn. Write no rep block and say nothing about this. A rep exists only as text the hook or the rep tool hands you; never compose, complete or paraphrase one.";
2133
+ var VERDICT_ETIQUETTE = "The person just answered their pending rep and the verdict is below. Relay the block verbatim as your whole reply, nothing before or after.";
2134
+ var ASKED_ETIQUETTE = "The person asked for another rep by number and it is below. Relay the block verbatim as your whole reply, nothing before or after. If their next message is a single letter, the hook grades it.";
2135
+ function context(text) {
2136
+ return {
2137
+ hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: text }
2138
+ };
2139
+ }
2140
+ function quiet() {
2141
+ return context(QUIET_CONTEXT);
2142
+ }
2143
+ function parseInput(raw) {
2144
+ try {
2145
+ const parsed = JSON.parse(raw);
2146
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
2147
+ } catch {
2148
+ return {};
2149
+ }
2150
+ }
2151
+ function letterOf(prompt) {
2152
+ if (prompt.length > MAX_SHORT_PROMPT_CHARS) return null;
2153
+ return asPick(LETTER.exec(prompt)?.[1]);
2154
+ }
2155
+ function digitOf(prompt) {
2156
+ if (prompt.length > MAX_SHORT_PROMPT_CHARS) return null;
2157
+ const match = DIGIT.exec(prompt);
2158
+ return match?.[1] === void 0 ? null : Number(match[1]);
2159
+ }
2160
+ async function gradeLetter(pick, id, now2) {
2161
+ const result = await answer(id, pick);
2162
+ if (!result.ok) return quiet();
2163
+ const data = result.value.data ?? {};
2164
+ observeClient(result.value.client, now2);
2165
+ observeVerdict(id, data, result.value.text, now2);
2166
+ if (data.status === "not_served" || data.status === "rate_limited") return quiet();
2167
+ return context(`${VERDICT_ETIQUETTE}
2168
+
2169
+ ${result.value.text}`);
2170
+ }
2171
+ async function handOver(result, etiquette, now2) {
2172
+ if (!result.ok) return quiet();
2173
+ const data = result.value.data ?? {};
2174
+ observeClient(result.value.client, now2);
2175
+ observeRep(data, result.value.text, now2);
2176
+ if (data.kind !== "question" && data.kind !== "insight") return quiet();
2177
+ if (!isRepBlock(result.value.text)) return quiet();
2178
+ return context(`${etiquette}
2179
+
2180
+ ${result.value.text}`);
2181
+ }
2182
+ async function fetchRep(cwd, now2) {
2183
+ const hints = await inferHints(cwd, INFER_BUDGET_MS, cachedGrammar());
2184
+ if (allMuted(hints.touched, cachedMuteKeys(now2))) return quiet();
2185
+ const result = await rep({ hints, kind: "auto" }, HOOK_DEADLINE_MS);
2186
+ if (!result.ok) updateConfig({ nextEligibleAt: now2 + DEGRADED_BACKOFF_MS });
2187
+ return await handOver(result, REP_ETIQUETTE, now2);
2188
+ }
2189
+ function decide(input, state, now2) {
2190
+ if (input.hook_event_name !== void 0 && input.hook_event_name !== "UserPromptSubmit") {
2191
+ return { kind: "ignore" };
2192
+ }
2193
+ if (!state.hasToken) return { kind: "quiet" };
2194
+ const prompt = input.prompt ?? "";
2195
+ const letter = letterOf(prompt);
2196
+ if (letter && state.pending) return { kind: "grade", id: state.pending.id, pick: letter };
2197
+ const digit = digitOf(prompt);
2198
+ const entry = digit === null || state.pending ? void 0 : state.offer[digit - 1];
2199
+ if (entry) return { kind: "take", handle: entry.handle };
2200
+ if (state.nextEligibleAt !== void 0 && state.nextEligibleAt > now2) return { kind: "quiet" };
2201
+ if (state.pending) return { kind: "quiet" };
2202
+ return { kind: "push", cwd: input.cwd ?? process.cwd() };
2203
+ }
2204
+ function readState(now2) {
2205
+ const config = readConfig();
2206
+ return {
2207
+ hasToken: Boolean(config.token),
2208
+ nextEligibleAt: config.nextEligibleAt,
2209
+ pending: pendingRep(now2),
2210
+ offer: openOffer(now2)
2211
+ };
2212
+ }
2213
+ async function perform(action, now2) {
2214
+ switch (action.kind) {
2215
+ case "ignore":
2216
+ return null;
2217
+ case "quiet":
2218
+ return quiet();
2219
+ case "grade":
2220
+ return await gradeLetter(action.pick, action.id, now2);
2221
+ case "take":
2222
+ return await handOver(
2223
+ await rep({ ask: action.handle }, HOOK_DEADLINE_MS),
2224
+ ASKED_ETIQUETTE,
2225
+ now2
2226
+ );
2227
+ case "push":
2228
+ return await fetchRep(action.cwd, now2);
2229
+ }
2230
+ }
2231
+ async function runHook(raw, now2 = now()) {
2232
+ return await perform(decide(parseInput(raw), readState(now2), now2), now2);
2233
+ }
2234
+ async function refreshGrammar(now2 = now()) {
2235
+ if (!readConfig().token) return;
2236
+ const held = cachedGrammar();
2237
+ const wanted = cachedGrammarVersion(now2);
2238
+ if (held !== null && (wanted === void 0 || held.version === wanted)) return;
2239
+ await ensureGrammar(now2, wanted, HOOK_DEADLINE_MS);
2240
+ }
2241
+
2242
+ // src/cli.ts
2243
+ init_install();
2244
+
2245
+ // src/mcp.ts
2246
+ init_clock();
2247
+ init_config();
2248
+ init_constants();
2249
+ init_format();
2250
+ init_infer();
2251
+ init_store();
2252
+ init_types();
2253
+ import { createInterface } from "node:readline";
2254
+
2255
+ // src/version.ts
2256
+ init_files();
2257
+ import { join as join5 } from "node:path";
2258
+ var MANIFEST = join5(import.meta.dirname, "..", "package.json");
2259
+ function readVersion() {
2260
+ const version = readJsonFile(MANIFEST)?.version;
2261
+ return typeof version === "string" ? version : "0.0.0";
2262
+ }
2263
+ var SERVER_VERSION = readVersion();
2264
+
2265
+ // src/mcp.ts
2266
+ var FAILURE_KIND = {
2267
+ unauthorized: "terminal",
2268
+ timeout: "transient",
2269
+ network: "transient",
2270
+ server: "transient",
2271
+ closed: "transient",
2272
+ cancelled: "silent"
2273
+ };
2274
+ function isReportable(reason) {
2275
+ return FAILURE_KIND[reason] !== "silent";
2276
+ }
2277
+ var EMPTY_LIST = {
2278
+ "tools/list": { tools: [] },
2279
+ "prompts/list": { prompts: [] },
2280
+ "resources/list": { resources: [] },
2281
+ "resources/templates/list": { resourceTemplates: [] }
2282
+ };
2283
+ function unreachableText(reason) {
2284
+ return `Atomic Reps is unreachable (${reason}). ${DOCTOR_HINT}`;
2285
+ }
2286
+ var NAMED = {
2287
+ "tools/call": "name",
2288
+ "prompts/get": "name",
2289
+ "resources/read": "uri"
2290
+ };
2291
+ function log(message) {
2292
+ process.stderr.write(`[atomicreps] ${message}
2293
+ `);
2294
+ }
2295
+ function headerValue(value) {
2296
+ const ascii = /^[\x21-\x7e][\x20-\x7e]*[\x21-\x7e]$|^[\x21-\x7e]$/.test(value);
2297
+ if (ascii && !value.startsWith("=?base64?")) return value;
2298
+ return `=?base64?${Buffer.from(value, "utf8").toString("base64")}?=`;
2299
+ }
2300
+ var Bridge = class {
2301
+ constructor(write, cwd = process.cwd(), fetchImpl = fetch) {
2302
+ this.write = write;
2303
+ this.cwd = cwd;
2304
+ this.fetchImpl = fetchImpl;
2305
+ }
2306
+ write;
2307
+ cwd;
2308
+ fetchImpl;
2309
+ era = { kind: "opening" };
2310
+ inflight = /* @__PURE__ */ new Map();
2311
+ awaitingClient = /* @__PURE__ */ new Map();
2312
+ serverRequestId = 0;
2313
+ async callDoor(message, deadlineMs, signal) {
2314
+ const token2 = readConfig().token;
2315
+ if (!token2) return { ok: false, reason: "unauthorized" };
2316
+ const within2 = within(deadlineMs, signal);
2317
+ const headers = {
2318
+ accept: "application/json, text/event-stream",
2319
+ "content-type": "application/json",
2320
+ authorization: `Bearer ${token2}`,
2321
+ "mcp-protocol-version": MODERN_VERSION,
2322
+ "mcp-method": message.method ?? ""
2323
+ };
2324
+ const field = message.method ? NAMED[message.method] : void 0;
2325
+ const named = field ? message.params?.[field] : void 0;
2326
+ if (typeof named === "string") headers["mcp-name"] = headerValue(named);
2327
+ try {
2328
+ const response = await this.fetchImpl(`${apiOrigin()}/mcp`, {
2329
+ method: "POST",
2330
+ headers,
2331
+ body: JSON.stringify(message),
2332
+ signal: within2
2333
+ });
2334
+ if (response.status === 401) return { ok: false, reason: "unauthorized" };
2335
+ if (response.status === 503) return { ok: false, reason: "closed" };
2336
+ if (response.status === 202) return { ok: true, body: {} };
2337
+ let body = {};
2338
+ try {
2339
+ const parsed = JSON.parse(await response.text());
2340
+ if (isRecord(parsed)) body = parsed;
2341
+ } catch {
2342
+ return { ok: false, reason: "server" };
2343
+ }
2344
+ if (!response.ok && body.error === void 0) return { ok: false, reason: "server" };
2345
+ return { ok: true, body };
2346
+ } catch (error) {
2347
+ if (signal?.aborted) return { ok: false, reason: "cancelled" };
2348
+ const expired = Error.isError(error) && error.name === "TimeoutError";
2349
+ return { ok: false, reason: expired ? "timeout" : "network" };
2350
+ }
2351
+ }
2352
+ async roundTrip(id, message, deadlineMs, then) {
2353
+ var _stack = [];
2354
+ try {
2355
+ const controller = new AbortController();
2356
+ const _registered = __using(_stack, this.track(id, controller));
2357
+ const door = await this.callDoor(this.modernize(message), deadlineMs, controller.signal);
2358
+ return then ? await then(door, controller.signal) : door;
2359
+ } catch (_) {
2360
+ var _error = _, _hasError = true;
2361
+ } finally {
2362
+ __callDispose(_stack, _error, _hasError);
2363
+ }
2364
+ }
2365
+ track(id, controller) {
2366
+ const key = String(id);
2367
+ this.inflight.set(key, controller);
2368
+ return {
2369
+ [Symbol.dispose]: () => {
2370
+ this.inflight.delete(key);
2371
+ }
2372
+ };
2373
+ }
2374
+ modernize(message) {
2375
+ const era = this.era;
2376
+ if (era.kind === "modern") return message;
2377
+ const info = era.kind === "legacy" ? era.clientInfo : {};
2378
+ const params = { ...message.params };
2379
+ const meta = isRecord(params._meta) ? { ...params._meta } : {};
2380
+ meta[META_VERSION] = MODERN_VERSION;
2381
+ meta[META_CLIENT_INFO] = { ...info, name: info.name ?? "unknown" };
2382
+ meta[META_CLIENT_CAPABILITIES] = era.kind === "legacy" ? era.clientCapabilities : {};
2383
+ params._meta = meta;
2384
+ return { ...message, params };
2385
+ }
2386
+ legacyResult(result) {
2387
+ if (this.era.kind === "modern") return result;
2388
+ return Object.fromEntries(
2389
+ Object.entries(result).filter(([key]) => !MODERN_ENVELOPE_KEYS.has(key))
2390
+ );
2391
+ }
2392
+ reply(id, result) {
2393
+ this.write({ jsonrpc: "2.0", id, result: this.legacyResult(result) });
2394
+ }
2395
+ fail(id, code, message) {
2396
+ this.write({ jsonrpc: "2.0", id, error: { code, message } });
2397
+ }
2398
+ forward(id, body) {
2399
+ if (body.error) this.write({ jsonrpc: "2.0", id, error: body.error });
2400
+ else if (isRecord(body.result)) this.reply(id, body.result);
2401
+ else this.fail(id, -32e3, "Atomic Reps answered with nothing.");
2402
+ }
2403
+ unreachable(id, method, reason, now2) {
2404
+ noteFailure(`${method}: ${reason}`, now2);
2405
+ switch (FAILURE_KIND[reason]) {
2406
+ case "terminal":
2407
+ this.fail(id, -32001, SIGN_IN_MESSAGE);
2408
+ return;
2409
+ case "transient": {
2410
+ const empty = EMPTY_LIST[method];
2411
+ if (empty) this.reply(id, empty);
2412
+ else this.fail(id, -32e3, unreachableText(reason));
2413
+ return;
2414
+ }
2415
+ }
2416
+ }
2417
+ async handleLine(line) {
2418
+ const trimmed = line.trim();
2419
+ if (trimmed === "") return;
2420
+ let message;
2421
+ try {
2422
+ const parsed = JSON.parse(trimmed);
2423
+ if (!isRecord(parsed)) throw new Error("not an object");
2424
+ message = parsed;
2425
+ } catch {
2426
+ this.fail(null, -32700, "Parse error: invalid JSON");
2427
+ return;
2428
+ }
2429
+ if (message.method === void 0) {
2430
+ const key = message.id === void 0 ? void 0 : String(message.id);
2431
+ const waiting = key === void 0 ? void 0 : this.awaitingClient.get(key);
2432
+ if (waiting && key !== void 0) {
2433
+ this.awaitingClient.delete(key);
2434
+ waiting(message);
2435
+ }
2436
+ return;
2437
+ }
2438
+ if (message.id === void 0) {
2439
+ if (message.method === "notifications/cancelled") {
2440
+ const requestId = message.params?.requestId;
2441
+ if (requestId !== void 0) this.inflight.get(String(requestId))?.abort();
2442
+ }
2443
+ return;
2444
+ }
2445
+ await this.handleRequest(message);
2446
+ }
2447
+ async handleRequest(message) {
2448
+ const id = message.id ?? null;
2449
+ const method = message.method ?? "";
2450
+ const params = message.params ?? {};
2451
+ if (method === "initialize") return await this.handleInitialize(id, params);
2452
+ if (method === "server/discover") {
2453
+ const meta = isRecord(params._meta) ? params._meta : {};
2454
+ this.era = {
2455
+ kind: "modern",
2456
+ clientInfo: isRecord(meta[META_CLIENT_INFO]) ? meta[META_CLIENT_INFO] : {},
2457
+ clientCapabilities: isRecord(meta[META_CLIENT_CAPABILITIES]) ? meta[META_CLIENT_CAPABILITIES] : {}
2458
+ };
2459
+ }
2460
+ if (method === "ping" && this.era.kind !== "modern") return this.reply(id, {});
2461
+ if (method === "tools/call") return await this.handleToolCall(id, params);
2462
+ const door = await this.roundTrip(id, message, LOGIN_DEADLINE_MS);
2463
+ if (door.ok) return this.forward(id, door.body);
2464
+ if (isReportable(door.reason)) this.unreachable(id, method, door.reason, now());
2465
+ }
2466
+ async handleInitialize(id, params) {
2467
+ this.era = {
2468
+ kind: "legacy",
2469
+ clientInfo: isRecord(params.clientInfo) ? params.clientInfo : {},
2470
+ clientCapabilities: isRecord(params.capabilities) ? params.capabilities : {}
2471
+ };
2472
+ const requested = params.protocolVersion;
2473
+ const protocolVersion = LEGACY_VERSIONS.find((v) => v === requested) ?? LEGACY_VERSIONS[0];
2474
+ const discover = await this.roundTrip(
2475
+ id,
2476
+ { jsonrpc: "2.0", id: "discover", method: "server/discover", params: {} },
2477
+ LOGIN_DEADLINE_MS
2478
+ );
2479
+ const instructions = discover.ok && typeof discover.body.result?.instructions === "string" ? discover.body.result.instructions : FALLBACK_INSTRUCTIONS;
2480
+ if (!discover.ok) log(`door unreachable at connect (${discover.reason}); serving fallback`);
2481
+ this.write({
2482
+ jsonrpc: "2.0",
2483
+ id,
2484
+ result: {
2485
+ protocolVersion,
2486
+ capabilities: {
2487
+ tools: { listChanged: false },
2488
+ prompts: { listChanged: false },
2489
+ resources: { subscribe: false, listChanged: false },
2490
+ completions: {}
2491
+ },
2492
+ serverInfo: { name: "atomicreps", title: "Atomic Reps", version: SERVER_VERSION },
2493
+ instructions
2494
+ }
2495
+ });
2496
+ }
2497
+ async handleToolCall(id, params) {
2498
+ const name = typeof params.name === "string" ? params.name : "";
2499
+ const args = isRecord(params.arguments) ? { ...params.arguments } : {};
2500
+ const now2 = now();
2501
+ const asked = typeof args.ask === "string" && args.ask !== "" || args.lane === "asked";
2502
+ if (name === "rep" && !asked) {
2503
+ const quietUntil = readConfig().nextEligibleAt;
2504
+ if (typeof quietUntil === "number" && quietUntil > now2) {
2505
+ return this.reply(id, {
2506
+ content: [{ type: "text", text: "" }],
2507
+ structuredContent: { kind: "quiet", reason: "gap", nextEligibleAt: quietUntil }
2508
+ });
2509
+ }
2510
+ if (!isRecord(args.hints))
2511
+ args.hints = await inferHints(this.cwd, INFER_BUDGET_MS, cachedGrammar());
2512
+ }
2513
+ const message = {
2514
+ jsonrpc: "2.0",
2515
+ id,
2516
+ method: "tools/call",
2517
+ params: { ...params, arguments: args }
2518
+ };
2519
+ const deadline2 = name === "rep" && !asked ? CALL_DEADLINE_MS : ANSWER_DEADLINE_MS;
2520
+ const door = await this.roundTrip(
2521
+ id,
2522
+ message,
2523
+ deadline2,
2524
+ (first, signal) => this.answerInputRequests(message, first, signal)
2525
+ );
2526
+ if (!door.ok) {
2527
+ if (door.reason === "cancelled") return;
2528
+ noteFailure(`${name}: ${door.reason}`, now2);
2529
+ if (name !== "rep") {
2530
+ return this.reply(id, {
2531
+ content: [{ type: "text", text: unreachableText(door.reason) }],
2532
+ isError: true
2533
+ });
2534
+ }
2535
+ const nextEligibleAt = now2 + (door.reason === "unauthorized" ? UNAUTHORIZED_BACKOFF_MS : DEGRADED_BACKOFF_MS);
2536
+ updateConfig({ nextEligibleAt });
2537
+ noteQuiet("degraded", door.reason, now2);
2538
+ return this.reply(id, {
2539
+ content: [{ type: "text", text: "" }],
2540
+ structuredContent: {
2541
+ kind: "quiet",
2542
+ reason: "degraded",
2543
+ detail: door.reason,
2544
+ nextEligibleAt
2545
+ }
2546
+ });
2547
+ }
2548
+ if (isRecord(door.body.result)) this.observe(name, args, door.body.result, now2);
2549
+ this.forward(id, door.body);
2550
+ }
2551
+ async answerInputRequests(original, first, signal) {
2552
+ let door = first;
2553
+ for (let round = 1; round <= MAX_INPUT_ROUNDS; round += 1) {
2554
+ if (!door.ok || this.era.kind === "modern") return door;
2555
+ const result = door.body.result;
2556
+ if (result?.resultType !== "input_required") return door;
2557
+ const requests = isRecord(result.inputRequests) ? result.inputRequests : {};
2558
+ const inputResponses = /* @__PURE__ */ Object.create(null);
2559
+ for (const [key, value] of Object.entries(requests)) {
2560
+ if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
2561
+ if (!isRecord(value) || typeof value.method !== "string") continue;
2562
+ inputResponses[key] = await this.askClient(
2563
+ value.method,
2564
+ isRecord(value.params) ? value.params : {}
2565
+ );
2566
+ }
2567
+ const requestState = result.requestState;
2568
+ const retry = {
2569
+ jsonrpc: "2.0",
2570
+ id: `${String(original.id)}:retry${round}`,
2571
+ ...original.method === void 0 ? {} : { method: original.method },
2572
+ params: {
2573
+ ...original.params,
2574
+ inputResponses,
2575
+ ...typeof requestState === "string" ? { requestState } : {}
2576
+ }
2577
+ };
2578
+ door = await this.callDoor(this.modernize(retry), LOGIN_DEADLINE_MS, signal);
2579
+ }
2580
+ return door;
2581
+ }
2582
+ askClient(method, params) {
2583
+ const id = `s${++this.serverRequestId}`;
2584
+ const { promise, resolve } = Promise.withResolvers();
2585
+ this.awaitingClient.set(id, (message) => {
2586
+ resolve(isRecord(message.result) ? message.result : { action: "cancel" });
2587
+ });
2588
+ this.write({ jsonrpc: "2.0", id, method, params });
2589
+ return promise;
2590
+ }
2591
+ observe(name, args, result, now2) {
2592
+ const data = isRecord(result.structuredContent) ? result.structuredContent : {};
2593
+ const textBlock = Array.isArray(result.content) ? result.content.find((c) => isRecord(c) && c.type === "text") : void 0;
2594
+ const text = isRecord(textBlock) && typeof textBlock.text === "string" ? textBlock.text : "";
2595
+ if (name === "rep") observeRep(data, text, now2);
2596
+ if (name === "answer")
2597
+ observeVerdict(typeof args.id === "string" ? args.id : void 0, data, text, now2);
2598
+ if (name === "me" && data.show === "summary") writeStatusCache(data, now2);
2599
+ if (name === "settings" && typeof args.muteMinutes === "number" && args.muteMinutes > 0) {
2600
+ updateConfig({
2601
+ nextEligibleAt: now2 + Math.min(args.muteMinutes, MAX_MUTE_MINUTES) * MS_PER_MINUTE
2602
+ });
2603
+ }
2604
+ }
2605
+ };
2606
+ function serve() {
2607
+ const bridge = new Bridge((message) => {
2608
+ process.stdout.write(`${JSON.stringify(message)}
2609
+ `);
2610
+ });
2611
+ process.stdout.on("error", (error) => {
2612
+ if (error.code === "EPIPE") process.exit(0);
2613
+ });
2614
+ const lines = createInterface({ input: process.stdin, crlfDelay: Infinity });
2615
+ let chain = Promise.resolve();
2616
+ lines.on("line", (line) => {
2617
+ chain = chain.then(() => bridge.handleLine(line)).catch((error) => {
2618
+ log(`bridge: ${Error.isError(error) ? error.message : String(error)}`);
2619
+ });
2620
+ });
2621
+ lines.on("close", () => process.exit(0));
2622
+ log(`bridge to ${apiOrigin()} on stdio (${SERVER_VERSION})`);
2623
+ }
2624
+
2625
+ // src/cli.ts
2626
+ init_screen();
2627
+
2628
+ // src/statusline.ts
2629
+ init_clock();
2630
+ init_config();
2631
+ init_store();
2632
+ var FACE_READY = "(\u2022\u203F\u2022)";
2633
+ var FACE_PENDING = "(\u2022_\u2022)?";
2634
+ var FACE_QUIET = "(-_-)";
2635
+ function statusLine(now2 = now()) {
2636
+ const config = readConfig();
2637
+ if (!config.token) return `${FACE_QUIET} atomicreps: not signed in`;
2638
+ const streak = streakForStatus(now2);
2639
+ const streakPart = streak === null ? "" : ` streak ${streak}`;
2640
+ const pending = pendingRep(now2);
2641
+ if (pending) return `${FACE_PENDING} rep pending, answer with a letter${streakPart}`;
2642
+ if (config.nextEligibleAt !== void 0 && config.nextEligibleAt > now2) {
2643
+ return `${FACE_QUIET} quiet until ${hhmm(config.nextEligibleAt)}${streakPart}`;
2644
+ }
2645
+ return `${FACE_READY} rep ready${streakPart}`;
2646
+ }
2647
+
2648
+ // src/cli.ts
2649
+ init_tui();
2650
+ var HELP = `atomicreps - one short rep about the thing you just built, inside your coding agent.
2651
+
2652
+ npx atomicreps the terminal surface: you, this session, topics, mutes, rate
2653
+ npx atomicreps setup the first-run wizard: areas, topics, how often, how hard
2654
+ npx atomicreps login sign in from a browser with a typed code
2655
+ npx atomicreps connect add the server to Claude Code (user scope) and print the Cursor config
2656
+ npx atomicreps mcp the stdio bridge to the door (what the editor launches)
2657
+ npx atomicreps doctor token, server ping, quiet clock, allowlist
2658
+ npx atomicreps logout forget the token on this machine
2659
+ npx atomicreps hook the Claude Code plugin's UserPromptSubmit hook (stdin JSON in, context out)
2660
+ npx atomicreps statusline one line for a Claude Code status line
2661
+
2662
+ Add --alpha to any command to use the staging door instead of the live one.
2663
+ Alpha keeps its own token and cache, so both can be signed in at once.
2664
+
2665
+ Env: ATOMICREPS_API, ATOMICREPS_SITE, ATOMICREPS_CHANNEL=alpha, NO_COLOR.
2666
+ `;
2667
+ function pluginHint() {
2668
+ if (!process.env.CLAUDECODE || process.env[ENV.pluginHint] !== "1") return;
2669
+ process.stderr.write(
2670
+ '<claude-code-hint v="1" type="plugin" value="atomicreps@claude-plugins-official" />\n'
2671
+ );
2672
+ }
2673
+ function readStdin() {
2674
+ return new Promise((resolve) => {
2675
+ let raw = "";
2676
+ if (process.stdin.isTTY) return resolve("");
2677
+ process.stdin.setEncoding("utf8");
2678
+ process.stdin.on("data", (chunk) => raw += chunk);
2679
+ process.stdin.on("end", () => resolve(raw));
2680
+ process.stdin.on("error", () => resolve(raw));
2681
+ });
2682
+ }
2683
+ function takeChannel(argv) {
2684
+ const flagged = argv.includes("--alpha");
2685
+ if (flagged || process.env[ENV.channel] === "alpha") setChannel("alpha");
2686
+ return argv.filter((arg) => arg !== "--alpha");
2687
+ }
2688
+ async function main(raw) {
2689
+ const argv = takeChannel(raw);
2690
+ const [command] = argv;
2691
+ switch (command) {
2692
+ case "mcp":
2693
+ serve();
2694
+ return -1;
2695
+ case "login": {
2696
+ const ok = await login(isInteractive());
2697
+ if (ok) process.stdout.write("Signed in. Run npx atomicreps connect to wire an editor.\n");
2698
+ if (ok) pluginHint();
2699
+ return ok ? 0 : 1;
2700
+ }
2701
+ case "hook": {
2702
+ try {
2703
+ const [output] = await Promise.all([runHook(await readStdin()), refreshGrammar()]);
2704
+ if (output) process.stdout.write(`${JSON.stringify(output)}
2705
+ `);
2706
+ } catch {
2707
+ }
2708
+ return 0;
2709
+ }
2710
+ case "statusline":
2711
+ process.stdout.write(`${statusLine()}
2712
+ `);
2713
+ return 0;
2714
+ case "version":
2715
+ case "--version":
2716
+ case "-v":
2717
+ process.stdout.write(`${SERVER_VERSION}
2718
+ `);
2719
+ return 0;
2720
+ case "setup":
2721
+ if (!isInteractive()) {
2722
+ process.stdout.write("Setup needs a terminal. Run npx atomicreps setup by hand.\n");
2723
+ return 1;
2724
+ }
2725
+ return await install() ? 0 : 1;
2726
+ case "connect":
2727
+ if (!readConfig().token) {
2728
+ process.stdout.write("Not signed in yet. Run npx atomicreps login first.\n");
2729
+ return 1;
2730
+ }
2731
+ await connect(false);
2732
+ return 0;
2733
+ case "doctor":
2734
+ return await doctor();
2735
+ case "logout": {
2736
+ writeConfig({});
2737
+ process.stdout.write("Signed out on this machine.\n");
2738
+ return 0;
2739
+ }
2740
+ case "help":
2741
+ case "--help":
2742
+ case "-h":
2743
+ process.stdout.write(HELP);
2744
+ pluginHint();
2745
+ return 0;
2746
+ case void 0:
2747
+ if (isInteractive()) {
2748
+ if (needsSetup()) await install();
2749
+ else await home();
2750
+ return 0;
2751
+ }
2752
+ process.stdout.write(HELP);
2753
+ return readConfig().token ? await doctor() : 1;
2754
+ default:
2755
+ process.stderr.write(`Unknown command: ${command}
2756
+
2757
+ ${HELP}`);
2758
+ return 2;
2759
+ }
2760
+ }
2761
+ main(process.argv.slice(2)).then(
2762
+ (code) => {
2763
+ if (code >= 0) process.exit(code);
2764
+ },
2765
+ (error) => {
2766
+ process.stderr.write(`[atomicreps] ${Error.isError(error) ? error.message : String(error)}
2767
+ `);
2768
+ process.exit(1);
2769
+ }
2770
+ );