Central-daemon messenger for Claude Code sessions (rewrite of cmux-msg)

🇯🇵 README-ja.md
Central daemon behind the ccmsg web UI. The plugin's only job is to keep the daemon running: its SessionStart hook starts the daemon if it isn't up, and adds nothing to the session's context. The daemon stays resident for the web UI, where the human user browses rooms, sessions, and transcripts, and injects posts from the web UI directly into Claude Code sessions. Messaging between sessions is handled by kawaz/ccmsg.
MVP + web UI implemented. Architecture is captured in DR-0001 / DR-0002 / DR-0003 / DR-0004, grounded in the verbatim primary sources under docs/research/. The daemon, CLI, protocol, and webui packages under packages/ are implemented and tested.
Requires bun (the CLI and daemon run on bun).
claude plugin marketplace add kawaz/claude-ccmsg
claude plugin install ccmsg-v1@ccmsg-v1
Update:
claude plugin marketplace update ccmsg-v1
claude plugin update ccmsg-v1@ccmsg-v1
The plugin's bin/ccmsg lives under a versioned plugin-cache path, so it isn't on your shell PATH by default. To put it there, symlink it into a stable dir on PATH:
ln -sfn <plugin-cache>/bin/ccmsg ~/.local/bin/ccmsg
Once installed this way, the ccmsg launcher keeps that symlink pointed at the newest version on its own — every invocation from a versioned cache path re-points it if it's strictly newer than what the symlink currently targets (see DR-0007).
The daemon serves a web UI at http://127.0.0.1:8642 by default (for the human user: browse rooms, sessions, files, and transcripts, and post as u1 = User). Binding is loopback-only and browser access is gated by an Origin check: loopback origins pass by default, a tailscale serve front for this port is detected and allowed automatically (zero config for remote/phone access over your tailnet), and other reverse-proxy origins can be added with CCMSG_HTTP_ALLOW_ORIGIN (comma-separated). A source-IP allowlist (CCMSG_HTTP_ALLOW, default loopback) remains as defense in depth, and CCMSG_HTTP_BIND (comma-separated host:port, off to disable) overrides the bind. URL fragments are locators (/#rXXXX = room, /#rXXXX-mNN = message position, /#s<sid> = session files, /#t<sid> = session timeline). See DR-0004.
sayccmsg say forwards its arguments to macOS's /usr/bin/say unchanged, and records the speech in the calling session's 1on1 room first. With several Claude Code sessions running, a spoken notification is otherwise anonymous — the web UI's Sessions list marks the session that spoke with 📣, and its 1on1 room shows what was said with a 既読 (read) button that clears the marker. The 1on1 room is read-only; to type into a session, use the input box in its session view.
Speech never depends on the recording: no session id, no running daemon, or a daemon that refuses the event all still speak. bin/say is a PATH shim that routes an ordinary say through this command, so every say on the machine — yours, a script's, an agent's — becomes attributable. Install it as a copy next to the ccmsg on your PATH (not a symlink: its target is a versioned plugin-cache dir that each update removes).
The p2p approach in cmux-msg worked for 1:1 messaging but exposed five structural problems during multi-session dogfooding:
msg/send/reply framing pushes models toward formal long messages.Rooms solve (1) and (2) structurally: one post reaches every member. (3) loses its cause because history is shared, and the to delivery filter (DR-0011) now scopes noisy exchanges away from uninvolved peers on top of short-message culture. (4) is a hypothesis that the post short-message framing reduces bloat. (5) is addressed by the user posting directly — via CLI in the MVP, web UI later.
mid).jsonl file per room (member / leave / msg / thread-links next/prev / … events) as the only persistent state. No server-side read cursors — BBS model: each reader tracks its own position and reconnects with a since-seq cursor (seq = per-room sequence spanning every event type, DR-0016).to-less messages are pushed to all room members. A to-bearing message is delivered only to the listed member(s), the sender, and the admin User (u1, always exempt) — a delivery filter, not just an attention marker (DR-0011). Storage stays unfiltered: any member can read a message they weren't delivered, a skipped mid is a deliberate pull signal. No echo back of your own posts.0600 + UID check) for local clients. The web UI uses WebSocket (/ws) speaking the same protocol: the security layer is identity pinning to the User role, gated by loopback-only binds, a source-IP allowlist (loopback, CCMSG_HTTP_ALLOW) and browser Origin validation (loopback origins by default, extras via CCMSG_HTTP_ALLOW_ORIGIN, e.g. for tailscale serve).ccmsg binary that writes messages directly into a Claude Code session's peer messaging socket (no resident process needed), a subscribe command for non-Claude-Code consumers, a user-facing CLI (the human is reserved member u1 of every room), and a web UI. Every client silently health-checks and auto-starts the daemon.packages/
protocol/ # shared types (wire protocol / XDG paths / version)
daemon/ # central daemon (bun)
cli/ # CLI client (session sidecar + user CLI)
webui/ # web UI (hono + vanilla ESM, served by the daemon)
docs/
decisions/ # DR-NNNN decision records
research/ # primary sources (verbatim design statements)
issue/ # active issues (claude-local-issue plugin)
findings/ # confirmed facts
journal/ # chronological notes
runbooks/ # operational recipes
knowledge/ # static knowledge
design/ # design docs
MIT — see LICENSE. Copyright (c) Yoshiaki Kawazu.
hooks/composer-fill.bundle.js 1093 lines1// Generated by `just build-hooks` with bun 1.4.2 from composer-fill.ts; edit that file, not this one.
2var store$4;
3var DEFAULT_CONFIG = {
4 lang: undefined,
5 message: undefined,
6 abortEarly: undefined,
7 abortPipeEarly: undefined
8};
9function getGlobalConfig(config$1) {
10 if (!config$1 && !store$4)
11 return DEFAULT_CONFIG;
12 return {
13 lang: config$1?.lang ?? store$4?.lang,
14 message: config$1?.message,
15 abortEarly: config$1?.abortEarly ?? store$4?.abortEarly,
16 abortPipeEarly: config$1?.abortPipeEarly ?? store$4?.abortPipeEarly
17 };
18}
19var store$3;
20function getGlobalMessage(lang) {
21 return store$3?.get(lang);
22}
23var store$2;
24function getSchemaMessage(lang) {
25 return store$2?.get(lang);
26}
27var store$1;
28function getSpecificMessage(reference, lang) {
29 return store$1?.get(reference)?.get(lang);
30}
31function _stringify(input) {
32 const type = typeof input;
33 if (type === "string")
34 return `"${input}"`;
35 if (type === "number" || type === "bigint" || type === "boolean")
36 return `${input}`;
37 if (type === "object" || type === "function")
38 return (input && Object.getPrototypeOf(input)?.constructor?.name) ?? "null";
39 return type;
40}
41function _addIssue(context, label, dataset, config$1, other) {
42 const input = other && "input" in other ? other.input : dataset.value;
43 const expected = other?.expected ?? context.expects ?? null;
44 const received = other?.received ?? /* @__PURE__ */ _stringify(input);
45 const issue = {
46 kind: context.kind,
47 type: context.type,
48 input,
49 expected,
50 received,
51 message: `Invalid ${label}: ${expected ? `Expected ${expected} but r` : "R"}eceived ${received}`,
52 requirement: context.requirement,
53 path: other?.path,
54 issues: other?.issues,
55 lang: config$1.lang,
56 abortEarly: config$1.abortEarly,
57 abortPipeEarly: config$1.abortPipeEarly
58 };
59 const isSchema = context.kind === "schema";
60 const message$1 = other?.message ?? context.message ?? /* @__PURE__ */ getSpecificMessage(context.reference, issue.lang) ?? (isSchema ? /* @__PURE__ */ getSchemaMessage(issue.lang) : null) ?? config$1.message ?? /* @__PURE__ */ getGlobalMessage(issue.lang);
61 if (message$1 !== undefined)
62 issue.message = typeof message$1 === "function" ? message$1(issue) : message$1;
63 if (isSchema)
64 dataset.typed = false;
65 if (dataset.issues)
66 dataset.issues.push(issue);
67 else
68 dataset.issues = [issue];
69}
70function _isSameValueZero(value1, value2) {
71 return value1 === value2 || Number.isNaN(value1) && Number.isNaN(value2);
72}
73function _isValidObjectKey(object$1, key) {
74 return Object.prototype.hasOwnProperty.call(object$1, key) && key !== "__proto__" && key !== "prototype" && key !== "constructor";
75}
76function _joinExpects(values$1, separator) {
77 const list = [...new Set(values$1)];
78 if (list.length > 1)
79 return `(${list.join(` ${separator} `)})`;
80 return list[0] ?? "never";
81}
82function _standardSchema(schema) {
83 schema["~standard"] = {
84 version: 1,
85 vendor: "valibot",
86 validate: (value$1) => schema["~run"]({ value: value$1 }, /* @__PURE__ */ getGlobalConfig())
87 };
88 return schema;
89}
90function getDotPath(issue) {
91 if (issue.path) {
92 let key = "";
93 for (const item of issue.path)
94 if (typeof item.key === "string" || typeof item.key === "number")
95 if (key)
96 key += `.${item.key}`;
97 else
98 key += item.key;
99 else
100 return null;
101 return key;
102 }
103 return null;
104}
105function check(requirement, message$1) {
106 return {
107 kind: "validation",
108 type: "check",
109 reference: check,
110 async: false,
111 expects: null,
112 requirement,
113 message: message$1,
114 "~run"(dataset, config$1) {
115 if (dataset.typed && !this.requirement(dataset.value))
116 _addIssue(this, "input", dataset, config$1);
117 return dataset;
118 }
119 };
120}
121function nonEmpty(message$1) {
122 return {
123 kind: "validation",
124 type: "non_empty",
125 reference: nonEmpty,
126 async: false,
127 expects: "!0",
128 message: message$1,
129 "~run"(dataset, config$1) {
130 if (dataset.typed && dataset.value.length === 0)
131 _addIssue(this, "length", dataset, config$1, { received: "0" });
132 return dataset;
133 }
134 };
135}
136function parseJson(config$1, message$1) {
137 return {
138 kind: "transformation",
139 type: "parse_json",
140 reference: parseJson,
141 config: config$1,
142 message: message$1,
143 async: false,
144 "~run"(dataset, config$2) {
145 try {
146 dataset.value = JSON.parse(dataset.value, this.config?.reviver);
147 } catch (error) {
148 if (error instanceof Error) {
149 _addIssue(this, "JSON", dataset, config$2, { received: `"${error.message}"` });
150 dataset.typed = false;
151 } else
152 throw error;
153 }
154 return dataset;
155 }
156 };
157}
158var ABORT_EARLY_CONFIG = { abortEarly: true };
159function getFallback(schema, dataset, config$1) {
160 return typeof schema.fallback === "function" ? schema.fallback(dataset, config$1) : schema.fallback;
161}
162function getDefault(schema, dataset, config$1) {
163 return typeof schema.default === "function" ? schema.default(dataset, config$1) : schema.default;
164}
165function array(item, message$1) {
166 return _standardSchema({
167 kind: "schema",
168 type: "array",
169 reference: array,
170 expects: "Array",
171 async: false,
172 item,
173 message: message$1,
174 "~run"(dataset, config$1) {
175 const input = dataset.value;
176 if (Array.isArray(input)) {
177 dataset.typed = true;
178 dataset.value = [];
179 for (let key = 0;key < input.length; key++) {
180 const value$1 = input[key];
181 const itemDataset = this.item["~run"]({ value: value$1 }, config$1);
182 if (itemDataset.issues) {
183 const pathItem = {
184 type: "array",
185 origin: "value",
186 input,
187 key,
188 value: value$1
189 };
190 for (const issue of itemDataset.issues) {
191 if (issue.path)
192 issue.path.unshift(pathItem);
193 else
194 issue.path = [pathItem];
195 dataset.issues?.push(issue);
196 }
197 if (!dataset.issues)
198 dataset.issues = itemDataset.issues;
199 if (config$1.abortEarly) {
200 dataset.typed = false;
201 break;
202 }
203 }
204 if (!itemDataset.typed)
205 dataset.typed = false;
206 dataset.value.push(itemDataset.value);
207 }
208 } else
209 _addIssue(this, "type", dataset, config$1);
210 return dataset;
211 }
212 });
213}
214function boolean(message$1) {
215 return _standardSchema({
216 kind: "schema",
217 type: "boolean",
218 reference: boolean,
219 expects: "boolean",
220 async: false,
221 message: message$1,
222 "~run"(dataset, config$1) {
223 if (typeof dataset.value === "boolean")
224 dataset.typed = true;
225 else
226 _addIssue(this, "type", dataset, config$1);
227 return dataset;
228 }
229 });
230}
231function literal(literal_, message$1) {
232 return _standardSchema({
233 kind: "schema",
234 type: "literal",
235 reference: literal,
236 expects: /* @__PURE__ */ _stringify(literal_),
237 async: false,
238 literal: literal_,
239 message: message$1,
240 "~run"(dataset, config$1) {
241 if (/* @__PURE__ */ _isSameValueZero(dataset.value, this.literal))
242 dataset.typed = true;
243 else
244 _addIssue(this, "type", dataset, config$1);
245 return dataset;
246 }
247 });
248}
249function looseObject(entries$1, message$1) {
250 return _standardSchema({
251 kind: "schema",
252 type: "loose_object",
253 reference: looseObject,
254 expects: "Object",
255 async: false,
256 entries: entries$1,
257 message: message$1,
258 "~run"(dataset, config$1) {
259 const input = dataset.value;
260 if (input && typeof input === "object") {
261 dataset.typed = true;
262 dataset.value = {};
263 for (const key in this.entries) {
264 const valueSchema = this.entries[key];
265 if (key in input || (valueSchema.type === "exact_optional" || valueSchema.type === "optional" || valueSchema.type === "nullish") && valueSchema.default !== undefined) {
266 const value$1 = key in input ? input[key] : /* @__PURE__ */ getDefault(valueSchema);
267 const valueDataset = valueSchema["~run"]({ value: value$1 }, config$1);
268 if (valueDataset.issues) {
269 const pathItem = {
270 type: "object",
271 origin: "value",
272 input,
273 key,
274 value: value$1
275 };
276 for (const issue of valueDataset.issues) {
277 if (issue.path)
278 issue.path.unshift(pathItem);
279 else
280 issue.path = [pathItem];
281 dataset.issues?.push(issue);
282 }
283 if (!dataset.issues)
284 dataset.issues = valueDataset.issues;
285 if (config$1.abortEarly) {
286 dataset.typed = false;
287 break;
288 }
289 }
290 if (!valueDataset.typed)
291 dataset.typed = false;
292 dataset.value[key] = valueDataset.value;
293 } else if (valueSchema.fallback !== undefined)
294 dataset.value[key] = /* @__PURE__ */ getFallback(valueSchema);
295 else if (valueSchema.type !== "exact_optional" && valueSchema.type !== "optional" && valueSchema.type !== "nullish") {
296 _addIssue(this, "key", dataset, config$1, {
297 input: undefined,
298 expected: `"${key}"`,
299 path: [{
300 type: "object",
301 origin: "key",
302 input,
303 key,
304 value: input[key]
305 }]
306 });
307 if (config$1.abortEarly)
308 break;
309 }
310 }
311 if (!dataset.issues || !config$1.abortEarly) {
312 for (const key in input)
313 if (/* @__PURE__ */ _isValidObjectKey(input, key) && !Object.prototype.hasOwnProperty.call(this.entries, key))
314 dataset.value[key] = input[key];
315 }
316 } else
317 _addIssue(this, "type", dataset, config$1);
318 return dataset;
319 }
320 });
321}
322function nullable(wrapped, default_) {
323 return _standardSchema({
324 kind: "schema",
325 type: "nullable",
326 reference: nullable,
327 expects: `(${wrapped.expects} | null)`,
328 async: false,
329 wrapped,
330 default: default_,
331 "~run"(dataset, config$1) {
332 if (dataset.value === null) {
333 if (this.default !== undefined)
334 dataset.value = /* @__PURE__ */ getDefault(this, dataset, config$1);
335 if (dataset.value === null) {
336 dataset.typed = true;
337 return dataset;
338 }
339 }
340 return this.wrapped["~run"](dataset, config$1);
341 }
342 });
343}
344function number(message$1) {
345 return _standardSchema({
346 kind: "schema",
347 type: "number",
348 reference: number,
349 expects: "number",
350 async: false,
351 message: message$1,
352 "~run"(dataset, config$1) {
353 if (typeof dataset.value === "number" && !isNaN(dataset.value))
354 dataset.typed = true;
355 else
356 _addIssue(this, "type", dataset, config$1);
357 return dataset;
358 }
359 });
360}
361function object(entries$1, message$1) {
362 return _standardSchema({
363 kind: "schema",
364 type: "object",
365 reference: object,
366 expects: "Object",
367 async: false,
368 entries: entries$1,
369 message: message$1,
370 "~run"(dataset, config$1) {
371 const input = dataset.value;
372 if (input && typeof input === "object") {
373 dataset.typed = true;
374 dataset.value = {};
375 for (const key in this.entries) {
376 const valueSchema = this.entries[key];
377 if (key in input || (valueSchema.type === "exact_optional" || valueSchema.type === "optional" || valueSchema.type === "nullish") && valueSchema.default !== undefined) {
378 const value$1 = key in input ? input[key] : /* @__PURE__ */ getDefault(valueSchema);
379 const valueDataset = valueSchema["~run"]({ value: value$1 }, config$1);
380 if (valueDataset.issues) {
381 const pathItem = {
382 type: "object",
383 origin: "value",
384 input,
385 key,
386 value: value$1
387 };
388 for (const issue of valueDataset.issues) {
389 if (issue.path)
390 issue.path.unshift(pathItem);
391 else
392 issue.path = [pathItem];
393 dataset.issues?.push(issue);
394 }
395 if (!dataset.issues)
396 dataset.issues = valueDataset.issues;
397 if (config$1.abortEarly) {
398 dataset.typed = false;
399 break;
400 }
401 }
402 if (!valueDataset.typed)
403 dataset.typed = false;
404 dataset.value[key] = valueDataset.value;
405 } else if (valueSchema.fallback !== undefined)
406 dataset.value[key] = /* @__PURE__ */ getFallback(valueSchema);
407 else if (valueSchema.type !== "exact_optional" && valueSchema.type !== "optional" && valueSchema.type !== "nullish") {
408 _addIssue(this, "key", dataset, config$1, {
409 input: undefined,
410 expected: `"${key}"`,
411 path: [{
412 type: "object",
413 origin: "key",
414 input,
415 key,
416 value: input[key]
417 }]
418 });
419 if (config$1.abortEarly)
420 break;
421 }
422 }
423 } else
424 _addIssue(this, "type", dataset, config$1);
425 return dataset;
426 }
427 });
428}
429function optional(wrapped, default_) {
430 return _standardSchema({
431 kind: "schema",
432 type: "optional",
433 reference: optional,
434 expects: `(${wrapped.expects} | undefined)`,
435 async: false,
436 wrapped,
437 default: default_,
438 "~run"(dataset, config$1) {
439 if (dataset.value === undefined) {
440 if (this.default !== undefined)
441 dataset.value = /* @__PURE__ */ getDefault(this, dataset, config$1);
442 if (dataset.value === undefined) {
443 dataset.typed = true;
444 return dataset;
445 }
446 }
447 return this.wrapped["~run"](dataset, config$1);
448 }
449 });
450}
451function picklist(options, message$1) {
452 return _standardSchema({
453 kind: "schema",
454 type: "picklist",
455 reference: picklist,
456 expects: /* @__PURE__ */ _joinExpects(options.map(_stringify), "|"),
457 async: false,
458 options,
459 message: message$1,
460 "~run"(dataset, config$1) {
461 if (this.options.includes(dataset.value))
462 dataset.typed = true;
463 else
464 _addIssue(this, "type", dataset, config$1);
465 return dataset;
466 }
467 });
468}
469function strictObject(entries$1, message$1) {
470 return _standardSchema({
471 kind: "schema",
472 type: "strict_object",
473 reference: strictObject,
474 expects: "Object",
475 async: false,
476 entries: entries$1,
477 message: message$1,
478 "~run"(dataset, config$1) {
479 const input = dataset.value;
480 if (input && typeof input === "object") {
481 dataset.typed = true;
482 dataset.value = {};
483 for (const key in this.entries) {
484 const valueSchema = this.entries[key];
485 if (key in input || (valueSchema.type === "exact_optional" || valueSchema.type === "optional" || valueSchema.type === "nullish") && valueSchema.default !== undefined) {
486 const value$1 = key in input ? input[key] : /* @__PURE__ */ getDefault(valueSchema);
487 const valueDataset = valueSchema["~run"]({ value: value$1 }, config$1);
488 if (valueDataset.issues) {
489 const pathItem = {
490 type: "object",
491 origin: "value",
492 input,
493 key,
494 value: value$1
495 };
496 for (const issue of valueDataset.issues) {
497 if (issue.path)
498 issue.path.unshift(pathItem);
499 else
500 issue.path = [pathItem];
501 dataset.issues?.push(issue);
502 }
503 if (!dataset.issues)
504 dataset.issues = valueDataset.issues;
505 if (config$1.abortEarly) {
506 dataset.typed = false;
507 break;
508 }
509 }
510 if (!valueDataset.typed)
511 dataset.typed = false;
512 dataset.value[key] = valueDataset.value;
513 } else if (valueSchema.fallback !== undefined)
514 dataset.value[key] = /* @__PURE__ */ getFallback(valueSchema);
515 else if (valueSchema.type !== "exact_optional" && valueSchema.type !== "optional" && valueSchema.type !== "nullish") {
516 _addIssue(this, "key", dataset, config$1, {
517 input: undefined,
518 expected: `"${key}"`,
519 path: [{
520 type: "object",
521 origin: "key",
522 input,
523 key,
524 value: input[key]
525 }]
526 });
527 if (config$1.abortEarly)
528 break;
529 }
530 }
531 if (!dataset.issues || !config$1.abortEarly) {
532 for (const key in input)
533 if (!Object.prototype.hasOwnProperty.call(this.entries, key)) {
534 _addIssue(this, "key", dataset, config$1, {
535 input: key,
536 expected: "never",
537 path: [{
538 type: "object",
539 origin: "key",
540 input,
541 key,
542 value: input[key]
543 }]
544 });
545 break;
546 }
547 }
548 } else
549 _addIssue(this, "type", dataset, config$1);
550 return dataset;
551 }
552 });
553}
554function string(message$1) {
555 return _standardSchema({
556 kind: "schema",
557 type: "string",
558 reference: string,
559 expects: "string",
560 async: false,
561 message: message$1,
562 "~run"(dataset, config$1) {
563 if (typeof dataset.value === "string")
564 dataset.typed = true;
565 else
566 _addIssue(this, "type", dataset, config$1);
567 return dataset;
568 }
569 });
570}
571function unknown() {
572 return _standardSchema({
573 kind: "schema",
574 type: "unknown",
575 reference: unknown,
576 expects: "unknown",
577 async: false,
578 "~run"(dataset) {
579 dataset.typed = true;
580 return dataset;
581 }
582 });
583}
584function variant(key, options, message$1) {
585 return _standardSchema({
586 kind: "schema",
587 type: "variant",
588 reference: variant,
589 expects: "Object",
590 async: false,
591 key,
592 options,
593 message: message$1,
594 "~run"(dataset, config$1) {
595 const input = dataset.value;
596 if (input && typeof input === "object") {
597 let outputDataset;
598 let maxDiscriminatorPriority = 0;
599 let invalidDiscriminatorKey = this.key;
600 let expectedDiscriminators = [];
601 const parseOptions = (variant$1, allKeys) => {
602 for (const schema of variant$1.options) {
603 if (schema.type === "variant")
604 parseOptions(schema, new Set(allKeys).add(schema.key));
605 else {
606 let keysAreValid = true;
607 let currentPriority = 0;
608 for (const currentKey of allKeys) {
609 const discriminatorSchema = schema.entries[currentKey];
610 if (currentKey in input ? discriminatorSchema["~run"]({
611 typed: false,
612 value: input[currentKey]
613 }, ABORT_EARLY_CONFIG).issues : discriminatorSchema.type !== "exact_optional" && discriminatorSchema.type !== "optional" && discriminatorSchema.type !== "nullish") {
614 keysAreValid = false;
615 if (invalidDiscriminatorKey !== currentKey && (maxDiscriminatorPriority < currentPriority || maxDiscriminatorPriority === currentPriority && (currentKey in input) && !(invalidDiscriminatorKey in input))) {
616 maxDiscriminatorPriority = currentPriority;
617 invalidDiscriminatorKey = currentKey;
618 expectedDiscriminators = [];
619 }
620 if (invalidDiscriminatorKey === currentKey)
621 expectedDiscriminators.push(schema.entries[currentKey].expects);
622 break;
623 }
624 currentPriority++;
625 }
626 if (keysAreValid) {
627 const optionDataset = schema["~run"]({ value: input }, config$1);
628 if (!outputDataset || !outputDataset.typed && optionDataset.typed)
629 outputDataset = optionDataset;
630 }
631 }
632 if (outputDataset && !outputDataset.issues)
633 break;
634 }
635 };
636 parseOptions(this, new Set([this.key]));
637 if (outputDataset)
638 return outputDataset;
639 _addIssue(this, "type", dataset, config$1, {
640 input: input[invalidDiscriminatorKey],
641 expected: /* @__PURE__ */ _joinExpects(expectedDiscriminators, "|"),
642 path: [{
643 type: "object",
644 origin: "value",
645 input,
646 key: invalidDiscriminatorKey,
647 value: input[invalidDiscriminatorKey]
648 }]
649 });
650 } else
651 _addIssue(this, "type", dataset, config$1);
652 return dataset;
653 }
654 });
655}
656function pipe(...pipe$1) {
657 return _standardSchema({
658 ...pipe$1[0],
659 pipe: pipe$1,
660 "~run"(dataset, config$1) {
661 for (const item of pipe$1)
662 if (item.kind !== "metadata") {
663 if (dataset.issues && (item.kind === "schema" || item.kind === "transformation")) {
664 dataset.typed = false;
665 break;
666 }
667 if (!dataset.issues || !config$1.abortEarly && !config$1.abortPipeEarly)
668 dataset = item["~run"](dataset, config$1);
669 }
670 return dataset;
671 }
672 });
673}
674function safeParse(schema, input, config$1) {
675 const dataset = schema["~run"]({ value: input }, /* @__PURE__ */ getGlobalConfig(config$1));
676 return {
677 typed: dataset.typed,
678 success: !dataset.issues,
679 output: dataset.value,
680 issues: dataset.issues
681 };
682}
683
684function failureOf(issues) {
685 const read = issues.map((issue) => ({
686 path: getDotPath(issue) ?? "",
687 message: issue.message
688 }));
689 return {
690 issues: read,
691 summary: read.map((i) => i.path === "" ? i.message : `${i.path}: ${i.message}`).join("; ")
692 };
693}
694function parseWith(schema, input, options = {}) {
695 const read = safeParse(schema, input, options.firstIssue === true ? { abortEarly: true, abortPipeEarly: true } : undefined);
696 return read.success ? { ok: true, value: read.output } : { ok: false, failure: failureOf(read.issues) };
697}
698var JsonTextSchema = pipe(string(), parseJson());
699function jsonText(schema) {
700 return pipe(string(), parseJson(), schema);
701}
702function errorText(e) {
703 return e instanceof Error ? `${e.name}: ${e.message}` : `a thrown ${typeof e}`;
704}
705
706var SIDECAR_REQUEST_VERSION = 1;
707var SidecarRequestIdSchema = pipe(string(), nonEmpty("an id is not empty"));
708var PromptSendArgsSchema = object({
709 text: pipe(string(), check((text) => text.trim() !== "", "text holds something other than whitespace"))
710});
711var SessionInputModuleReasonSchema = picklist([
712 "dialog",
713 "no_composer",
714 "refused",
715 "no_terminal",
716 "not_submitted",
717 "enter_failed",
718 "session_changed"
719]);
720var PromptSendResultSchema = variant("outcome", [
721 object({ outcome: literal("accepted") }),
722 object({ outcome: literal("not_delivered"), reason: SessionInputModuleReasonSchema }),
723 object({ outcome: literal("unconfirmed") })
724]);
725var DescribeArgsSchema = pipe(unknown(), check((args) => !Array.isArray(args), "args is an object, not an array"), strictObject({}));
726var DescribeResultSchema = object({
727 v: array(number()),
728 ops: array(string()),
729 version: optional(string())
730});
731var SIDECAR_OPS = {
732 describe: { args: DescribeArgsSchema, result: DescribeResultSchema },
733 "prompt.send": { args: PromptSendArgsSchema, result: PromptSendResultSchema }
734};
735var SIDECAR_OP_NAMES = ["describe", "prompt.send"];
736function requestSchema(op, args) {
737 return object({
738 v: literal(SIDECAR_REQUEST_VERSION),
739 op: literal(op),
740 id: SidecarRequestIdSchema,
741 args
742 });
743}
744var SidecarRequestSchema = variant("op", [
745 requestSchema("describe", SIDECAR_OPS.describe.args),
746 requestSchema("prompt.send", SIDECAR_OPS["prompt.send"].args)
747]);
748var SidecarErrorCodeSchema = picklist(["internal"]);
749var SidecarErrorSchema = object({
750 code: SidecarErrorCodeSchema,
751 message: string()
752});
753var FailedReplySchema = object({
754 id: SidecarRequestIdSchema,
755 ok: literal(false),
756 error: SidecarErrorSchema
757});
758function sidecarReplySchema(result) {
759 return variant("ok", [
760 object({ id: SidecarRequestIdSchema, ok: literal(true), result }),
761 FailedReplySchema
762 ]);
763}
764var SidecarReplySchema = sidecarReplySchema(unknown());
765var SidecarReplyTextSchema = jsonText(SidecarReplySchema);
766var SidecarRequestLineSchema = jsonText(SidecarRequestSchema);
767function readSidecarRequest(line) {
768 return parseWith(SidecarRequestLineSchema, line);
769}
770
771var SessionStartInputSchema = looseObject({
772 isInteractive: optional(boolean()),
773 surface: optional(nullable(string()))
774});
775var SessionEndInputSchema = looseObject({ reason: optional(string()) });
776var ClassicSessionStartInputSchema = looseObject({});
777var UserPromptSubmitInputSchema = looseObject({ prompt: optional(string()) });
778var PromptSubmitInputSchema = looseObject({
779 text: optional(string()),
780 origin: optional(looseObject({ kind: optional(string()) }))
781});
782async function observe($, e, next, event, schema, observer) {
783 const read = parseWith(schema, e);
784 if (!read.ok)
785 $.ui.log(`ccmsg: ${event} not read: ${read.failure.summary}`, { to: "debug" });
786 const run = async (when, part) => {
787 if (!read.ok || part === undefined)
788 return;
789 try {
790 await part(read.value);
791 } catch (error) {
792 $.ui.log(`ccmsg: ${event} failed ${when} the event: ${errorText(error)}`, { to: "debug" });
793 }
794 };
795 await run("before", observer.before);
796 const result = await next(e);
797 await run("after", observer.after);
798 return result;
799}
800var HYOUI_TIMEOUT_MS = 3000;
801var ACCEPT_WAIT_MS = 3000;
802var REPLY_TIMEOUT_MS = 3000;
803var RESPAWN_MIN_MS = 500;
804var RESPAWN_MAX_MS = 30000;
805var STABLE_MS = 1e4;
806function failed(id, code, message) {
807 return { id, ok: false, error: { code, message } };
808}
809function hyouiEnterArgv(hyouiSessionId) {
810 return ["hyoui", "input", hyouiSessionId, "key:Enter"];
811}
812function samePrompt(submitted, filled) {
813 return submitted === filled || submitted === filled.trimEnd();
814}
815function lineSplitter(onLine) {
816 let rest = "";
817 return (piece) => {
818 const parts = (rest + piece).split(`
819`);
820 rest = parts.pop() ?? "";
821 for (const part of parts)
822 if (part.trim() !== "")
823 onLine(part);
824 };
825}
826var UNCONFIRMED = { outcome: "unconfirmed" };
827var SESSION_CHANGED = { outcome: "not_delivered", reason: "session_changed" };
828function newState() {
829 return {
830 generation: 0,
831 queue: Promise.resolve(),
832 delay: RESPAWN_MIN_MS,
833 starting: Promise.resolve()
834 };
835}
836function nextGeneration(state) {
837 state.generation += 1;
838 state.awaiting = undefined;
839}
840async function stillCurrent($, state, request) {
841 if (request.generation !== state.generation || request.sid !== state.sid)
842 return false;
843 return await $.session.id() === request.sid;
844}
845async function putBackUnsent($, text, draft) {
846 const box = await $.prompt.read();
847 if (box.text !== text)
848 return false;
849 const put = await $.prompt.fill({ text: draft, mode: "replace" });
850 if (!put.isFilled)
851 return false;
852 return !(await $.prompt.read()).text.includes(text);
853}
854async function handlePromptSend($, state, request) {
855 const hyouiSessionId = await $.env.get("HYOUI_SESSION_ID");
856 if (!hyouiSessionId)
857 return { outcome: "not_delivered", reason: "no_terminal" };
858 const draft = (await $.prompt.read()).text;
859 const filled = await $.prompt.fill({ text: request.text, mode: "replace" });
860 if (!filled.isFilled)
861 return { outcome: "not_delivered", reason: filled.refusal ?? "refused" };
862 if (!await stillCurrent($, state, request)) {
863 return await putBackUnsent($, request.text, draft) ? SESSION_CHANGED : UNCONFIRMED;
864 }
865 const accepted = Promise.withResolvers();
866 state.awaiting = { text: request.text, accept: () => accepted.resolve(true) };
867 state.restoring = { text: request.text, draft };
868 let entered;
869 try {
870 const run = await $.process.run(hyouiEnterArgv(hyouiSessionId), {
871 timeoutMs: HYOUI_TIMEOUT_MS
872 });
873 entered = run.exitCode === 0;
874 } catch {
875 entered = false;
876 }
877 const timer = $.clock.after(ACCEPT_WAIT_MS, () => accepted.resolve(false));
878 const isAccepted = await accepted.promise;
879 timer.cancel();
880 if (state.awaiting?.text === request.text)
881 state.awaiting = undefined;
882 if (request.generation !== state.generation)
883 return UNCONFIRMED;
884 if (isAccepted)
885 return { outcome: "accepted" };
886 if (state.restoring?.text === request.text)
887 state.restoring = undefined;
888 if (await putBackUnsent($, request.text, draft)) {
889 return { outcome: "not_delivered", reason: entered ? "not_submitted" : "enter_failed" };
890 }
891 return UNCONFIRMED;
892}
893async function putBackDraft($, submitted, draft) {
894 const box = await $.prompt.read();
895 if (box.text !== "" && box.text !== submitted) {
896 $.ui.log(`ccmsg: kept the newly typed text; the earlier draft was: ${draft}`, {
897 to: "debug"
898 });
899 return;
900 }
901 await $.prompt.fill({ text: draft, mode: "replace" });
902}
903function replyArgv(root, sid) {
904 return [`${root}/bin/ccmsg`, "sidecar", "reply", "--sid", sid];
905}
906async function sendReply($, sid, reply) {
907 try {
908 const done = await $.process.run(replyArgv($.plugin.root, sid), {
909 stdin: JSON.stringify(reply),
910 timeoutMs: REPLY_TIMEOUT_MS
911 });
912 if (done.exitCode !== 0) {
913 $.ui.log(`ccmsg: reply ${reply.id} exited ${done.exitCode}: ${done.stderr.trim()}`, {
914 to: "debug"
915 });
916 }
917 } catch (e) {
918 $.ui.log(`ccmsg: reply ${reply.id} failed: ${errorText(e)}`, { to: "debug" });
919 }
920}
921async function promptSend($, state, request) {
922 if (request.generation !== state.generation || request.sid !== state.sid) {
923 $.ui.log(`ccmsg: prompt.send ${request.id} not filled: the session changed its id`, {
924 to: "debug"
925 });
926 return { id: request.id, ok: true, result: SESSION_CHANGED };
927 }
928 return { id: request.id, ok: true, result: await handlePromptSend($, state, request) };
929}
930async function describe($) {
931 const described = { v: [SIDECAR_REQUEST_VERSION], ops: [...SIDECAR_OP_NAMES] };
932 try {
933 const run = await $.process.run([`${$.plugin.root}/bin/ccmsg`, "version"], {
934 env: { CCMSG_NO_SELF_EXEC: "1" },
935 timeoutMs: REPLY_TIMEOUT_MS
936 });
937 const version = run.stdout.trim();
938 if (run.exitCode === 0 && version !== "")
939 return { ...described, version };
940 } catch (e) {
941 $.ui.log(`ccmsg: describe could not read the version: ${errorText(e)}`, { to: "debug" });
942 }
943 return described;
944}
945async function replyTo($, state, sid, generation, request) {
946 const { id } = request;
947 try {
948 switch (request.op) {
949 case "describe":
950 return { id, ok: true, result: await describe($) };
951 case "prompt.send": {
952 const queued = { id, text: request.args.text, sid, generation };
953 const result = state.queue.then(() => promptSend($, state, queued));
954 state.queue = result.then(() => {
955 return;
956 }, () => {
957 return;
958 });
959 return await result;
960 }
961 }
962 } catch (e) {
963 $.ui.log(`ccmsg: request ${id} failed: ${errorText(e)}`, { to: "debug" });
964 return failed(id, "internal", errorText(e));
965 }
966}
967function answer($, state, sid, generation, line) {
968 const read = readSidecarRequest(line);
969 if (!read.ok) {
970 $.ui.log(`ccmsg sidecar: dropped a line that is not a request this module runs (${read.failure.summary}): ${line.slice(0, 200)}`, { to: "debug" });
971 return Promise.resolve();
972 }
973 const { id } = read.value;
974 return replyTo($, state, sid, generation, read.value).then((r) => sendReply($, sid, r)).catch((e) => {
975 $.ui.log(`ccmsg: request ${id} not answered: ${errorText(e)}`, { to: "debug" });
976 });
977}
978async function runSidecar($, state, sid) {
979 const child = $.process.spawn({
980 argv: [`${$.plugin.root}/bin/ccmsg`, "sidecar", "--sid", sid]
981 });
982 const mine = { sid, child };
983 state.sidecar = mine;
984 const startedAt = await $.clock.now();
985 const onLine = lineSplitter((line) => {
986 answer($, state, sid, state.generation, line);
987 });
988 try {
989 for await (const piece of child) {
990 if (piece.stream === "stdout")
991 onLine(piece.text);
992 else
993 $.ui.log(`ccmsg sidecar: ${piece.text.trimEnd()}`, { to: "debug" });
994 }
995 } catch (e) {
996 $.ui.log(`ccmsg sidecar: ${errorText(e)}`, { to: "debug" });
997 }
998 if (state.sidecar !== mine)
999 return;
1000 state.sidecar = undefined;
1001 const lived = await $.clock.now() - startedAt;
1002 if (lived >= STABLE_MS)
1003 state.delay = RESPAWN_MIN_MS;
1004 const wait = state.delay;
1005 state.delay = Math.min(state.delay * 2, RESPAWN_MAX_MS);
1006 $.clock.after(wait, () => {
1007 if (state.sidecar === undefined)
1008 startSidecar($, state);
1009 });
1010}
1011function startSidecar($, state) {
1012 state.starting = state.starting.then(() => replaceSidecar($, state)).catch((e) => {
1013 $.ui.log(`ccmsg sidecar: start failed: ${errorText(e)}`, { to: "debug" });
1014 });
1015}
1016async function replaceSidecar($, state) {
1017 state.interactive ??= (await $.session.surfaces()).length > 0;
1018 if (!state.interactive)
1019 return;
1020 const sid = await $.session.id();
1021 if (state.sid !== undefined && state.sid !== sid)
1022 nextGeneration(state);
1023 state.sid = sid;
1024 if (state.sidecar?.sid === sid)
1025 return;
1026 const old = state.sidecar;
1027 state.sidecar = undefined;
1028 if (old !== undefined)
1029 await old.child.return(undefined);
1030 runSidecar($, state, sid);
1031}
1032function register(on) {
1033 const state = newState();
1034 on("session.start", async ($, e, next) => observe($, e, next, "session.start", SessionStartInputSchema, {
1035 after: (input) => {
1036 state.interactive = input.isInteractive !== false && input.surface !== null;
1037 startSidecar($, state);
1038 }
1039 })).catch(async (_$, e, next) => next(e));
1040 on("session.end", async ($, e, next) => observe($, e, next, "session.end", SessionEndInputSchema, {
1041 before: (input) => {
1042 if (input.reason === "clear" && state.sid !== undefined) {
1043 nextGeneration(state);
1044 state.sid = undefined;
1045 }
1046 }
1047 })).catch(async (_$, e, next) => next(e));
1048 on("classic.SessionStart", async ($, e, next) => observe($, e, next, "classic.SessionStart", ClassicSessionStartInputSchema, {
1049 after: () => startSidecar($, state)
1050 })).catch(async (_$, e, next) => next(e));
1051 on("classic.UserPromptSubmit", async ($, e, next) => observe($, e, next, "classic.UserPromptSubmit", UserPromptSubmitInputSchema, {
1052 before: (input) => {
1053 const awaiting = state.awaiting;
1054 if (awaiting === undefined)
1055 return;
1056 if (input.prompt !== undefined && samePrompt(input.prompt, awaiting.text)) {
1057 state.awaiting = undefined;
1058 awaiting.accept();
1059 return;
1060 }
1061 $.ui.log(`ccmsg: UserPromptSubmit ${JSON.stringify(input.prompt ?? null)} is not the filled ${JSON.stringify(awaiting.text)}`, { to: "debug" });
1062 }
1063 })).catch(async (_$, e, next) => next(e));
1064 on("prompt.submit", async ($, e, next) => observe($, e, next, "prompt.submit", PromptSubmitInputSchema, {
1065 after: async (input) => {
1066 const restoring = state.restoring;
1067 if (restoring === undefined || input.origin?.kind !== "composer" || input.text === undefined || !samePrompt(input.text, restoring.text)) {
1068 return;
1069 }
1070 state.restoring = undefined;
1071 if (restoring.draft !== "")
1072 await putBackDraft($, restoring.text, restoring.draft);
1073 }
1074 })).catch(async (_$, e, next) => next(e));
1075 return state;
1076}
1077export {
1078 ACCEPT_WAIT_MS,
1079 HYOUI_TIMEOUT_MS,
1080 REPLY_TIMEOUT_MS,
1081 RESPAWN_MAX_MS,
1082 RESPAWN_MIN_MS,
1083 STABLE_MS,
1084 answer,
1085 handlePromptSend,
1086 hyouiEnterArgv,
1087 lineSplitter,
1088 newState,
1089 register,
1090 replyArgv,
1091 samePrompt
1092};
1093