baychat 0.23.1 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +120 -15
  2. package/dist/api.js +19 -2
  3. package/dist/claude-context-flush.js +219 -0
  4. package/dist/claude-context-state.js +191 -0
  5. package/dist/claude-context.js +311 -0
  6. package/dist/claude-settings.js +503 -0
  7. package/dist/codex-onboarding.js +33 -23
  8. package/dist/context-hooks-command.js +165 -0
  9. package/dist/context-report.js +307 -0
  10. package/dist/index.js +26 -497
  11. package/dist/main.js +511 -0
  12. package/dist/mcp-notes.js +110 -0
  13. package/dist/mcp.js +84 -5
  14. package/dist/protocol-content.js +1 -1
  15. package/dist/relay/acp/client.js +18 -0
  16. package/dist/relay/acp/prompt.js +54 -3
  17. package/dist/relay/acp/runner.js +100 -2
  18. package/dist/relay/acp/turn-queue.js +99 -3
  19. package/dist/relay/codex-context.js +242 -0
  20. package/dist/relay/compact-command.js +63 -0
  21. package/dist/relay/compact-focus.attacks.js +36 -0
  22. package/dist/relay/compact-focus.js +64 -0
  23. package/dist/relay/context-notice.js +144 -0
  24. package/dist/relay/daemon.js +429 -73
  25. package/dist/relay/fresh-start-verify.js +237 -0
  26. package/dist/relay/fresh-start.js +141 -0
  27. package/dist/relay/message-format.js +34 -15
  28. package/dist/relay/project-brief.js +195 -0
  29. package/dist/relay/project-notes.js +300 -0
  30. package/dist/relay/resume.js +25 -0
  31. package/dist/relay/server-registration.js +40 -0
  32. package/dist/relay/session-commands.js +2 -0
  33. package/dist/relay/terminal-text.js +13 -0
  34. package/dist/relay/transport.js +2 -1
  35. package/dist/relay/updates.js +46 -8
  36. package/dist/relay/ws.js +49 -6
  37. package/dist/runtimes.js +24 -6
  38. package/dist/tool-defs.js +93 -0
  39. package/package.json +1 -1
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ /**
3
+ * The cage a `/compact` focus goes through before anyone types it into a terminal.
4
+ *
5
+ * ── A TWIN, BYTE-IDENTICAL IN TWO PLACES ──────────────────────────────────────────────
6
+ *
7
+ * packages/cli/src/relay/compact-focus.ts — the relay, which TYPES the focus (send-keys -l)
8
+ * apps/api/src/lib/compact-focus.ts — the API, which refuses a bad focus at the door
9
+ * ("tidy up now" from the app)
10
+ *
11
+ * The API's check is not the safety: the relay re-checks everything it types, whoever sent
12
+ * it. It exists so the app can say WHY at once instead of the relay saying it in the chat
13
+ * later — and a server that judged a focus differently from the relay would accept what the
14
+ * relay then refuses, or refuse what it would type. So there is one rule, in one file, copied
15
+ * verbatim (it imports nothing) and held together by `apps/api/src/lib/compact-focus.parity.test.ts`.
16
+ * The attack strings both sides must refuse live beside it, twinned the same way
17
+ * (`compact-focus.attacks.ts`). Change one copy, change both.
18
+ *
19
+ * ── THE RULES ─────────────────────────────────────────────────────────────────────────
20
+ *
21
+ * - ONE line: no CR, LF, or Unicode line/paragraph separators. A newline would be Enter,
22
+ * and Enter followed by anything is a second command typed into someone's terminal.
23
+ * - PRINTABLE only: no control, format or invisible characters (`\p{C}` — escape
24
+ * sequences, tabs, zero-width joiners, right-to-left overrides that make a line read
25
+ * differently from what it types).
26
+ * - A SMALL ALPHABET: letters, marks and digits of any language, spaces, and
27
+ * . , ; : ' " ( ) - _ / ? ! + & % =. No `@` (Claude Code opens a file picker on it, and
28
+ * Enter would pick a file instead of submitting), no `\` or `^` (screen's `stuff` parses
29
+ * them), no `$` or backtick, no `#`, `<`, `>`, `|`, braces or brackets.
30
+ * - 200 characters at most, after runs of spaces are collapsed to one.
31
+ * - It cannot start with `/`, so the line can only ever be `/compact <words>`.
32
+ */
33
+ Object.defineProperty(exports, "__esModule", { value: true });
34
+ exports.COMPACT_FOCUS_PUNCTUATION = exports.COMPACT_FOCUS_MAX = void 0;
35
+ exports.sanitizeCompactFocus = sanitizeCompactFocus;
36
+ exports.COMPACT_FOCUS_MAX = 200;
37
+ exports.COMPACT_FOCUS_PUNCTUATION = `. , ; : ' " ( ) - _ / ? ! + & % =`;
38
+ const FOCUS_ALPHABET = /^[\p{L}\p{M}\p{N} .,;:'"()\-_/?!+&%=]+$/u;
39
+ /** Is this focus text safe to type? Returns it normalised (undefined when blank), or why not. */
40
+ function sanitizeCompactFocus(raw) {
41
+ if (/[\r\n\u2028\u2029]/.test(raw))
42
+ return { ok: false, code: "MULTILINE", reason: "the focus must be a single line." };
43
+ if (/\p{C}/u.test(raw)) {
44
+ return { ok: false, code: "CONTROL", reason: "the focus contains control or invisible characters." };
45
+ }
46
+ const focus = raw.replace(/\p{Zs}+/gu, " ").trim();
47
+ if (!focus)
48
+ return { ok: true };
49
+ if ([...focus].length > exports.COMPACT_FOCUS_MAX) {
50
+ return { ok: false, code: "TOO_LONG", reason: `the focus is longer than ${exports.COMPACT_FOCUS_MAX} characters.` };
51
+ }
52
+ if (focus.startsWith("/"))
53
+ return { ok: false, code: "LEADING_SLASH", reason: "the focus cannot start with /." };
54
+ if (!FOCUS_ALPHABET.test(focus)) {
55
+ const bad = [...focus].find((ch) => !FOCUS_ALPHABET.test(ch));
56
+ return {
57
+ ok: false,
58
+ code: "CHARACTER",
59
+ reason: `the focus may only use letters, numbers, spaces and ${exports.COMPACT_FOCUS_PUNCTUATION}${bad ? ` — "${bad}" is not allowed` : ""}.`,
60
+ ...(bad ? { character: bad } : {}),
61
+ };
62
+ }
63
+ return { ok: true, focus };
64
+ }
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.CarriedNotices = void 0;
37
+ exports.contextNoticeFrom = contextNoticeFrom;
38
+ exports.formatContextNotice = formatContextNotice;
39
+ exports.laterNotice = laterNotice;
40
+ exports.carryNotice = carryNotice;
41
+ exports.carriedNoticesPath = carriedNoticesPath;
42
+ const fs = __importStar(require("fs"));
43
+ const path = __importStar(require("path"));
44
+ const config_1 = require("../config");
45
+ const terminal_text_1 = require("./terminal-text");
46
+ /** The server's `notice` beside an event, parsed leniently; undefined when absent or unusable. */
47
+ function contextNoticeFrom(raw) {
48
+ if (!raw || typeof raw !== "object")
49
+ return undefined;
50
+ const { kind, percent, text } = raw;
51
+ if (kind !== "context_high" || typeof percent !== "number" || !Number.isFinite(percent) || typeof text !== "string")
52
+ return undefined;
53
+ return { kind, percent: Math.max(0, Math.min(100, Math.round(percent))), text };
54
+ }
55
+ function oneLine(text) {
56
+ return (0, terminal_text_1.cleanTerminalText)(text)
57
+ .replace(/[\u0080-\u009f‎‏‪-‮⁦-⁩]/g, "")
58
+ .replace(/\s+/g, " ")
59
+ .trim();
60
+ }
61
+ /** The block in front of the message, as lines (no trailing newline). */
62
+ function formatContextNotice(notice) {
63
+ return [`BayChat notice (memory ${notice.percent}% full): ${oneLine(notice.text)}`, "---"].join("\n");
64
+ }
65
+ /** The newer of two notices (a later crossing replaces an earlier one). */
66
+ function laterNotice(a, b) {
67
+ return b ?? a;
68
+ }
69
+ /**
70
+ * Hand a notice whose own message will not reach the session to the first of `later` (any chat:
71
+ * the notice is about the session, not a room).
72
+ *
73
+ * @returns true when it found a home.
74
+ */
75
+ function carryNotice(notice, later) {
76
+ const next = later[0];
77
+ if (!next)
78
+ return false;
79
+ next.notice = laterNotice(notice, next.notice);
80
+ return true;
81
+ }
82
+ function carriedNoticesPath() {
83
+ return path.join((0, config_1.configDir)(), "carried-notices.json");
84
+ }
85
+ /** Notices kept for a session until its next message goes out. ON DISK, 0600, like the carried briefs. */
86
+ class CarriedNotices {
87
+ file;
88
+ held;
89
+ constructor(file = carriedNoticesPath()) {
90
+ this.file = file;
91
+ }
92
+ keep(session, notice) {
93
+ this.load().set(session, notice);
94
+ this.save();
95
+ }
96
+ /** Put a kept notice on the first message of this batch (mutates the batch). */
97
+ apply(session, batch) {
98
+ const kept = this.load().get(session);
99
+ if (!kept || batch.length === 0)
100
+ return;
101
+ this.held.delete(session);
102
+ batch[0].notice = laterNotice(kept, batch[0].notice);
103
+ this.save();
104
+ }
105
+ /** Forget it: the session started fresh or ended, and the notice was about its old memory. */
106
+ forget(session) {
107
+ if (this.load().delete(session))
108
+ this.save();
109
+ }
110
+ load() {
111
+ if (this.held)
112
+ return this.held;
113
+ this.held = new Map();
114
+ let raw;
115
+ try {
116
+ raw = JSON.parse(fs.readFileSync(this.file, "utf8"));
117
+ }
118
+ catch (err) {
119
+ if (err.code !== "ENOENT") {
120
+ process.stderr.write(`baychat relay: could not read carried notices — ${err instanceof Error ? err.message : String(err)}\n`);
121
+ }
122
+ return this.held;
123
+ }
124
+ if (raw && typeof raw === "object") {
125
+ for (const [session, value] of Object.entries(raw)) {
126
+ const notice = contextNoticeFrom(value);
127
+ if (notice)
128
+ this.held.set(session, notice);
129
+ }
130
+ }
131
+ return this.held;
132
+ }
133
+ save() {
134
+ try {
135
+ fs.mkdirSync(path.dirname(this.file), { recursive: true });
136
+ fs.writeFileSync(this.file, JSON.stringify(Object.fromEntries(this.held ?? new Map())), { mode: 0o600 });
137
+ fs.chmodSync(this.file, 0o600);
138
+ }
139
+ catch (err) {
140
+ process.stderr.write(`baychat relay: could not persist carried notices — ${err instanceof Error ? err.message : String(err)}\n`);
141
+ }
142
+ }
143
+ }
144
+ exports.CarriedNotices = CarriedNotices;