SLOPSHOPPER

ccmsg-v1

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

newpromptprocesstimer
v0.158.0MITupdated 2026-10-09kawaz/claude-ccmsg
A shopper browsing a rack in a slop shop
README

claude-ccmsg

🇯🇵 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.

Status

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.

Install

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).

Web UI

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.

Speaking with say

ccmsg 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).

Why a rewrite?

The p2p approach in cmux-msg worked for 1:1 messaging but exposed five structural problems during multi-session dogfooding:

  1. Cross-explosion — adding a 4th or 5th peer multiplies pair-wise sends.
  2. Repeated instructions — same prompt to N peers means N copy-pastes for the user and N duplicated actions from peers.
  3. AI-to-AI noise — peers waste context comparing notes about a third peer.
  4. Mail-bloat — msg/send/reply framing pushes models toward formal long messages.
  5. User-mixing overhead — the human user can only target one peer at a time, while AIs gossip about what the user said.

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.

Architecture (see DR-0001)

  • Single host — laptop or workstation, no federation. Mobile access via tailscale over LAN.
  • Central daemon (bun) — the only writer. Issues room IDs, serializes and deduplicates concurrent room creation, and assigns per-room monotonic message IDs (mid).
  • Storage — one append-only 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).
  • Delivery — 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.
  • Transport — UNIX Domain Socket (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).
  • Clients — a standalone 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.

Repository layout

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

License

MIT — see LICENSE. Copyright (c) Yoshiaki Kawazu.

Source 1 files
hooks/composer-fill.bundle.js 1093 lines
1// 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