@oatelauser/crosschat 1.0.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 (59) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/LICENSE +25 -0
  3. package/README.md +244 -0
  4. package/dist/claude/deliver.js +33 -0
  5. package/dist/claude/deliver.js.map +1 -0
  6. package/dist/claude/frame.js +16 -0
  7. package/dist/claude/frame.js.map +1 -0
  8. package/dist/claude/key-file.js +45 -0
  9. package/dist/claude/key-file.js.map +1 -0
  10. package/dist/claude/registry.js +79 -0
  11. package/dist/claude/registry.js.map +1 -0
  12. package/dist/claude/resolve-exe.js +31 -0
  13. package/dist/claude/resolve-exe.js.map +1 -0
  14. package/dist/cli.js +270 -0
  15. package/dist/cli.js.map +1 -0
  16. package/dist/codex/client.js +130 -0
  17. package/dist/codex/client.js.map +1 -0
  18. package/dist/codex/deliver.js +162 -0
  19. package/dist/codex/deliver.js.map +1 -0
  20. package/dist/codex/discovery.js +45 -0
  21. package/dist/codex/discovery.js.map +1 -0
  22. package/dist/codex/queue.js +71 -0
  23. package/dist/codex/queue.js.map +1 -0
  24. package/dist/codex/rollout-meta.js +118 -0
  25. package/dist/codex/rollout-meta.js.map +1 -0
  26. package/dist/codex/rpc.js +153 -0
  27. package/dist/codex/rpc.js.map +1 -0
  28. package/dist/codex/transport.js +357 -0
  29. package/dist/codex/transport.js.map +1 -0
  30. package/dist/commands/claude-wrapper.js +28 -0
  31. package/dist/commands/claude-wrapper.js.map +1 -0
  32. package/dist/commands/doctor.js +169 -0
  33. package/dist/commands/doctor.js.map +1 -0
  34. package/dist/commands/install-skills.js +26 -0
  35. package/dist/commands/install-skills.js.map +1 -0
  36. package/dist/commands/send.js +122 -0
  37. package/dist/commands/send.js.map +1 -0
  38. package/dist/commands/status.js +77 -0
  39. package/dist/commands/status.js.map +1 -0
  40. package/dist/envelope.js +34 -0
  41. package/dist/envelope.js.map +1 -0
  42. package/dist/errors.js +15 -0
  43. package/dist/errors.js.map +1 -0
  44. package/dist/identity.js +42 -0
  45. package/dist/identity.js.map +1 -0
  46. package/dist/outbox.js +172 -0
  47. package/dist/outbox.js.map +1 -0
  48. package/dist/platform/pipe-transport.js +73 -0
  49. package/dist/platform/pipe-transport.js.map +1 -0
  50. package/dist/platform/process-inspector.js +15 -0
  51. package/dist/platform/process-inspector.js.map +1 -0
  52. package/dist/rate-limit.js +49 -0
  53. package/dist/rate-limit.js.map +1 -0
  54. package/dist/ref.js +64 -0
  55. package/dist/ref.js.map +1 -0
  56. package/dist/resolve.js +30 -0
  57. package/dist/resolve.js.map +1 -0
  58. package/package.json +47 -0
  59. package/skills/crosschat/SKILL.md +65 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ 本项目的全部显著变更记录于此。格式参照 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本遵循 [SemVer](https://semver.org/lang/zh-CN/)。
4
+
5
+ ## [1.0.0] - 2026-10-02
6
+
7
+ 首个公开版本。Windows 一期:本机 Claude Code ↔ Codex CLI 跨会话消息,无守护进程,原生投递。
8
+
9
+ ### 新增
10
+ - 四命令 CLI:`send`(同步投递/自包含引用/轮次计数)、`status`(双侧总览含目录时间)、`install-skills`、`claude`(接收许可包装启动)
11
+ - Claude 侧通道:sessions 注册表发现、named pipe + peerToken 鉴权、peer 协议 v1 帧注入
12
+ - Codex 侧通道:app-server proxy 桥接(ws-over-stdio)、resume→turn/start 投递、busy/locked 等待循环
13
+ - 溯源信封(回复命令自带教学 + 保留字中性化)与 agent 侧 skill(低入侵:题词只写角色)
14
+ - 防乒乓限流(30 条/60s/对端点)、16KiB 单条上限、身份冲突自纠指引
15
+ - 机会式发件箱:忙/锁超时 parked 落盘,任意 send/status 调用入口自动排涝补投
16
+ - daemon 0.160 开窗投递支持(同 daemon 多连接绕过进程级写者锁,实证)
17
+ - 双 bin 过渡别名(crosschat 主 / multichat 曾用名)
18
+
19
+ ### 过程档案
20
+ - 设计决策地图(wayfinder)、实测研究报告 ×4、联调实证 ×2 随仓库公开(已脱敏)
21
+
22
+ [1.0.0]: https://github.com/Oatelauser/crosschat/releases/tag/v1.0.0
package/LICENSE ADDED
@@ -0,0 +1,25 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Oatelauser
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ This project borrows module-level implementations (envelope framing, codex
16
+ app-server transport patterns) from embassy (https://github.com/YuanpingSong/embassy),
17
+ also MIT licensed.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,244 @@
1
+ <div align="center">
2
+
3
+ <img src="docs/assets/logo.svg" width="128" alt="crosschat logo"/>
4
+
5
+ # crosschat
6
+
7
+ **让本机的 Claude Code 与 Codex CLI 互相对话** —— 无守护进程、原生投递、双向实测。
8
+
9
+ *曾用名 multichat(`multichat` 命令作为过渡别名保留)*
10
+
11
+ ![CI](https://github.com/Oatelauser/crosschat/actions/workflows/ci.yml/badge.svg) ![version](https://img.shields.io/badge/version-1.0.0-blue) ![license](https://img.shields.io/badge/license-MIT-green) ![node](https://img.shields.io/badge/node-%3E%3D22-339933) ![platform](https://img.shields.io/badge/platform-Windows-blue) ![agents](https://img.shields.io/badge/agents-Claude%20Code%20%C2%B7%20Codex-blueviolet)
12
+
13
+ </div>
14
+
15
+ ---
16
+
17
+ ## ✨ 为什么用 crosschat
18
+
19
+ - **🔌 原生投递,零轮询**:消息经 Claude 的 named pipe / Codex 的 App Server daemon 直接注入运行中的会话——接收方像收到一条用户消息一样开始工作,不需要任何轮询或常驻服务
20
+ - **🪶 无守护进程**:整个工具就是一条无状态 CLI。没有后台进程要看护、没有崩溃丢状态、没有端口要占——`send` 就是发消息,`status` 就是看在线
21
+ - **🤖 教学内建,低入侵**:agent 侧装一次 skill,且**每条消息的信封自带回复命令**——照抄即可回话。题词只写角色,不写协议;长对话也不会忘
22
+ - **📮 忙时不丢**:对方正在跑长任务?消息自动进本地发件箱(outbox),对方空闲后任意一次调用自动补投——不需要你重发
23
+ - **🔐 同用户信任边界**:全部通道按 Windows 用户隔离,接收许可只授予 `crosschat claude` 启动的会话——你手敲开的会话不会被外部投递
24
+ - **🧪 每个结论都有实证**:通道可行性、写者锁、daemon 版本行为,全部真机联调验证(见 [📚 更多文档](#-更多文档))
25
+
26
+ > 灵感与部分模块实现来自 [embassy](https://github.com/YuanpingSong/embassy)(macOS-only,MIT)——crosschat 是它的 Windows 原生、无守护进程重实现。
27
+
28
+ ## 🧰 环境搭建
29
+
30
+ | 依赖 | 版本要求 | 说明 |
31
+ |---|---|---|
32
+ | Node.js | ≥ 22 | |
33
+ | Claude Code | 任意近期版 | 接收需 `crosschat claude` 启动(工具自动注入许可) |
34
+ | Codex CLI | ≥ 0.160 推荐 | 0.160 daemon 支持开窗投递;接收需 app-server daemon |
35
+ | OS | Windows(一期) | mac/linux 在路线图 |
36
+
37
+ ```powershell
38
+ git clone https://github.com/Oatelauser/crosschat.git
39
+ cd crosschat
40
+ npm install && npm run build && npm link # crosschat 上 PATH
41
+ crosschat install-skills # 协议教学装到两侧 agent
42
+ ```
43
+
44
+ **Codex daemon**(接收方向必需;从**干净终端**启动——勿在 Claude 会话内启动,否则派生 shell 身份污染):
45
+
46
+ ```powershell
47
+ codex app-server daemon start # 重启电脑后需重新执行
48
+ ```
49
+
50
+ ## 🚀 快速入门
51
+
52
+ ### 30 秒版:两终端互发一句问候
53
+
54
+ ```powershell
55
+ # 终端 A(claude,能收)
56
+ crosschat claude # 进入后 /rename alice
57
+
58
+ # 终端 B(任意终端,人手发)
59
+ crosschat status # 看到 alice
60
+ crosschat send --to alice --body "你好 alice!"
61
+ # → 终端 A 里几秒后出现这条消息
62
+ ```
63
+
64
+ ### 完整剧本:codex 当领导派活给 claude(多任务循环的经典形态)
65
+
66
+ **任务目标**:codex(领导)命令 claude(工人)在 `D:\workspace\demo` 创建 `notes.txt` 写三行待办,并验收。
67
+
68
+ **第 1 步 · 左窗启动 claude(接收方先开机;先启动 ≠ 先说话)**
69
+ ```
70
+ D:\workspace\demo> crosschat claude
71
+ ```
72
+ 进入后输入 `/rename worker`,待命。
73
+
74
+ **第 2 步 · 右窗启动 codex(领导)并贴题词**
75
+ ```
76
+ D:\workspace\demo> codex
77
+ ```
78
+ ```
79
+ 你是领导。用 crosschat(先 status 确认名字)给 claude 会话「worker」下发任务:
80
+ 在当前目录创建 notes.txt,内容三行:买牛奶、交电费、给妈妈打电话。
81
+ 收到完成报告后,亲自打开文件验证内容;通过则回复"验收通过,任务结束"并停止;
82
+ 不通过则下发返工任务。
83
+ ```
84
+
85
+ **第 3 步 · 右窗屏幕——任务由 codex 发出**
86
+ ```
87
+ ● exec: crosschat status
88
+ ● exec: crosschat send --to worker --body "任务1:在当前目录创建 notes.txt…"
89
+ ● delivered to worker (turn 1) ← 任务飞进左窗,发起方是 codex
90
+ ```
91
+
92
+ **第 4 步 ·(0.160+ 可跳过)关掉右窗**:旧版 daemon(≤0.157)下收报告需关窗;**0.160+ 开窗也能收**(headless 执行)。
93
+
94
+ **第 5 步 · 左窗屏幕——claude 收令干活**
95
+ ```
96
+ 📨 来自另一会话的消息:
97
+ <cross-session-message from-name="codex/01a…" turn="1">
98
+ 任务1:在当前目录创建 notes.txt,内容三行…
99
+ 回复请运行: crosschat send --conversation mc1_xxx --body "<你的回复>"
100
+ </cross-session-message>
101
+
102
+ ⏺ Write: notes.txt(三行待办)
103
+ ⏺ Bash: crosschat send --conversation mc1_xxx --body "已完成:notes.txt 已创建…"
104
+ ⏺ delivered (turn 2) ← 报告发回给 codex
105
+ ```
106
+
107
+ **第 6 步 · 自动发生**:报告落进 codex 线程 → codex 验证 → 下发下一个/验收结论 → 又出现在左窗。
108
+
109
+ **第 7 步 · 左窗收到"验收通过,任务结束"——收工**。全程在左窗直播;想看领导的验收细节:开右窗 `codex resume` 翻历史。
110
+
111
+ ## 📋 命令列表
112
+
113
+ ```
114
+ crosschat send --to <名字> --body "<正文>" # 新消息(名字含空格加引号)
115
+ crosschat send --conversation <ref> --body "<正文>" # 回复(ref 照抄收到的信封)
116
+ echo … | crosschat send --to <名字> # 正文走 stdin
117
+ crosschat status [--json] # 双侧总览(名字/目录/时间/状态)
118
+ crosschat doctor # 一键环境体检(有 ❌ 时退出码 1)
119
+ crosschat install-skills [--dir <根>] # 安装/更新 agent skill(幂等)
120
+ crosschat claude [任意 claude 参数…] # 带接收许可启动 claude(透传)
121
+ crosschat -v | --version | help # 版本 / 帮助
122
+ ```
123
+
124
+ 发送输出三种状态:`delivered`(已投递)/ `parked`(对方忙,已入发件箱待补投)/ 错误码(见排障)。
125
+
126
+ ## 📖 对话生命周期(规则总纲)
127
+
128
+ ### 三种发起方式
129
+
130
+ | 发起者 | 怎么发 | 特性 |
131
+ |---|---|---|
132
+ | **claude** | 会话内 agent 跑 `crosschat send --to <codex 线程名> --body "…"` | 最顺,推荐默认 |
133
+ | **codex** | 会话内 agent 跑 `crosschat send --to <claude 会话名> --body "…"` | 发送随意;回信开窗关窗都能收(daemon 0.160+) |
134
+ | **人** | 任意终端直接 `crosschat send --to <名字> --body "…"` | 身份是 human:**能发、不能被回复**(单向指令) |
135
+
136
+ 第一条消息永远用 `--to`(此刻生成对话引用 reply-ref);对方名字用 `status` 查。
137
+
138
+ ### 往复规则
139
+
140
+ 1. 收到方看到**信封消息**(`from-name` + `turn="N"` + 回复提示)
141
+ 2. 回复 = **逐字照抄回复提示里的命令**,换掉 `<你的回复>`——引用自动轮转,无需记任何历史
142
+ 3. 防失控:每对端点 30 条/60 秒限流
143
+
144
+ ### 轮次与终止
145
+
146
+ 信封显示 `turn="N"`;题词给预算(如"10 轮内完成");接近预算时 agent 按 skill 准则总结收尾;协议层不强制终止。
147
+
148
+ ## 📡 投递语义矩阵
149
+
150
+ | 接收方 | 状态 | 行为 |
151
+ |---|---|---|
152
+ | claude(`crosschat claude` 启动) | 窗口开 | ✅ 秒达,会话内出现信封消息 |
153
+ | claude(裸 `claude` 启动) | 任何 | ❌ 无接收许可(换 `crosschat claude` 重启) |
154
+ | codex(daemon ≥0.160) | 窗口开/关 | ✅ 送达(headless 执行;TUI 不实时刷新,翻历史可见) |
155
+ | codex(旧 daemon ≤0.157 / `--no-daemon`) | 窗口开 | ⏳ 等待 120s,关窗瞬间送达;超时 `parked` 入发件箱 |
156
+ | codex | turn 进行中(忙) | 📮 `parked` 入发件箱,对方空闲后任意一次 crosschat 调用自动补投 |
157
+
158
+ **不对称速记**:claude 收发都随意;codex 收信在 0.160+ 开窗关窗均可,关窗永远是最稳路径。
159
+
160
+ ## 🤝 Agent 兼容性
161
+
162
+ | Agent | 发送 | 接收 | 实测版本 |
163
+ |---|---|---|---|
164
+ | Claude Code | ✅ | ✅(需 `crosschat claude` 启动) | 2.1.287 |
165
+ | Codex CLI | ✅ | ✅(需 daemon;0.160+ 开窗可收) | 0.159.3 / 0.160.0 |
166
+ | 任意有 shell 的 agent | ✅(装 skill 后) | 🔜 按适配器扩展(GLM 等) | 路线图 |
167
+
168
+ ## 🗺️ 路线图
169
+
170
+ **已完成(v1.0.0)**
171
+ - [x] Claude ↔ Codex / Claude ↔ Claude / Codex ↔ Codex 双向消息(真机联调验证)
172
+ - [x] 无状态 CLI 四命令 + 自包含会话引用 + 轮次计数
173
+ - [x] 双侧自动发现(含目录/时间标注)、信封自带教学 skill
174
+ - [x] 16KiB 上限、30 条/60s 防乒乓限流、身份冲突自纠指引
175
+ - [x] 忙/锁超时 → 本地发件箱自动补投(outbox)
176
+ - [x] daemon 0.160 开窗投递(实证:同 daemon 多连接绕过写者锁)
177
+
178
+ **计划中**
179
+ - [ ] 🌐 跨机联邦(SSH,异构 win ↔ linux 互聊)
180
+ - [ ] 🐧 mac / linux 平台适配(平台接缝已预留:PipeTransport / ProcessInspector / PathLayout)
181
+ - [ ] 📦 常驻 broker(忙时持久队列、异步回执、投递状态机——embassy 完整集对齐)
182
+ - [ ] 🖼️ TUI 看板与服务安装(开机自启)
183
+ - [ ] 🤖 新 agent 适配器(GLM 等;有原生唤醒通道则原生,否则论证降级)
184
+ - [ ] 👀 status 标注 TUI 占用线程;专用信箱线程机制化
185
+
186
+ ## 🧯 错误码排障
187
+
188
+ **使用类**:`NAME_NOT_FOUND`(错误信息列出全部可用名)· `NAME_COLLISION`(重名,`status --json` 看 id)· `MESSAGE_TOO_LARGE`(>16KiB → 落盘发路径)· `RATE_LIMITED`(等待或收尾)· `TARGET_*`/`BODY_*`/`USAGE`(参数错误)
189
+
190
+ **身份类**:`CALLER_IDENTITY_CONFLICT`(环境双身份残留。临时:命令前缀 `env -u CLAUDE_CODE_MESSAGING_SOCKET -u CLAUDE_CODE_MESSAGING_TOKEN -u CLAUDE_CODE_SESSION_ID`;根治:干净终端重启 daemon)· `CALLER_NOT_IN_CONVERSATION` · `CANNOT_REPLY_TO_HUMAN`(对话由人发起)
191
+
192
+ **通道类**:`CODEX_PROXY_SPAWN_FAILED`(看 stderr 摘录;通常 daemon 未跑)· `CODEX_THREAD_LOCKED`/`CODEX_THREAD_BUSY_TIMEOUT`(已自动转 `parked` 入发件箱,无需重发)· `OUTBOX_FULL`(每线程 20 条积压上限,等待排涝或人工介入)· `CODEX_APPROVAL_REQUIRED`(**工具永不代答审批**)· `CLAUDE_PIPE_*`/`CODEX_*UNCERTAIN`(写入中途失败状态不明——**勿盲目重发**,先 `status` 核实)
193
+
194
+ ## ⚠️ 边界与限制
195
+
196
+ 单条 ≤16KiB;每对端点 30 条/60s;发件箱每线程 20 条;信任边界=同一 Windows 用户;接收许可仅 `crosschat claude` 启动的会话;重启电脑后需重新 `codex app-server daemon start`。
197
+
198
+ ## ❓ FAQ
199
+
200
+ **Q:codex 窗口开着收不到?** daemon ≥0.160 开窗也能收(headless,界面不实时刷新);旧 daemon/`--no-daemon` 下等待或已入发件箱,关窗后自动送达。
201
+
202
+ **Q:为什么必须 `crosschat claude`?** 它注入跨会话接收许可;裸 `claude` 的会话收不到。
203
+
204
+ **Q:`codex queue` 能用吗?** 仅当 TUI 附着 daemon(0.160+)时会被消费;旧形态下是黑洞,故 crosschat 不依赖它。
205
+
206
+ **Q:人能发消息吗?** 能(`--to`),身份 human,单向不能被回复。
207
+
208
+ **Q:消息历史在哪看?** claude 侧=会话 transcript;codex 侧=开窗 resume 线程。
209
+
210
+ ## 🔧 故障恢复
211
+
212
+ | 症状 | 动作 |
213
+ |---|---|
214
+ | 任何异常 | 先跑 `crosschat doctor` 一键体检(逐项 ✅/⚠️/❌ 定位) |
215
+ | status 的 codex 段 unavailable | 干净终端 `codex app-server daemon start` |
216
+ | codex 回信撞身份冲突 | 同上(重启 daemon 即根治) |
217
+ | skill 误删/过期 | `crosschat install-skills` |
218
+ | 升级 crosschat 代码后 | `npm run build`(skill 有变再 install-skills) |
219
+ | 消息发出对方没反应 | 先 `status` 确认在线;`parked` 的等对方空闲自动补投;UNCERTAIN 类勿重发先核实 |
220
+
221
+ ## 📚 更多文档
222
+
223
+ | 文档 | 内容 |
224
+ |---|---|
225
+ | [docs/architecture.md](docs/architecture.md) | **底层原理**:注册表/命名管道/daemon 通道、写者锁本质、信封与自包含引用、完整投递流程图 |
226
+ | [CHANGELOG.md](CHANGELOG.md) | 版本变更记录 |
227
+ | [docs/wayfinder/map.md](docs/wayfinder/map.md) | 设计决策地图(全部拍板过程与依据) |
228
+ | [docs/research/](docs/research/) | 实测研究报告(embassy 源码分析、两侧通道验证、0.160 inject_items 实验) |
229
+ | [docs/drill-reports/](docs/drill-reports/) | 联调实证记录 |
230
+
231
+ ## 🛠️ 开发说明
232
+
233
+ ```powershell
234
+ npm run check # lint + build + test
235
+ $env:CROSSCHAT_LIVE='1'; npx vitest run --dir test # 真机 live 测试
236
+ ```
237
+
238
+ - 目录:`src/claude`(注册表/管道/鉴权)· `src/codex`(proxy/RPC/投递)· `src/commands`(CLI)· `src/platform`(平台接缝)· `src/outbox.ts`(发件箱)· `skills/`(agent 教学)
239
+ - 铭谢:[embassy](https://github.com/YuanpingSong/embassy)(MIT)的信封格式与 codex 传输模式
240
+ - License:[MIT](LICENSE)
241
+
242
+ ---
243
+
244
+ <div align="center">Made with 🤝 between agents — crosschat 让它们自己对话</div>
@@ -0,0 +1,33 @@
1
+ import { getPipeTransport } from '../platform/pipe-transport.js';
2
+ import { MultichatError } from '../errors.js';
3
+ import { encodeAuthLine, encodeUserFrame } from './frame.js';
4
+ import { readPeerToken } from './key-file.js';
5
+ import { defaultClaudeSessionsDir } from './registry.js';
6
+ /**
7
+ * Deliver one user message to a running Claude session over its messaging
8
+ * pipe: auth line + user frame, written once, never retried. Transport
9
+ * failures surface as CLAUDE_PIPE_CONNECT_FAILED (never connected) or
10
+ * CLAUDE_PIPE_WRITE_UNCERTAIN (failed after the write started).
11
+ */
12
+ export async function deliverToClaudeSession(session, content, options = {}) {
13
+ const sessionsDir = options.sessionsDir ?? defaultClaudeSessionsDir();
14
+ const token = readPeerToken(sessionsDir, session.pid, session.messagingSocketPath);
15
+ const lines = [encodeAuthLine(token), encodeUserFrame(content)];
16
+ const transport = options.transport ?? getPipeTransport();
17
+ try {
18
+ await transport.sendLines(session.messagingSocketPath, lines);
19
+ }
20
+ catch (err) {
21
+ if (err instanceof MultichatError) {
22
+ if (err.code === 'PIPE_CONNECT_FAILED') {
23
+ throw new MultichatError('CLAUDE_PIPE_CONNECT_FAILED', `Cannot connect to the messaging pipe of Claude pid ${session.pid}.`, { cause: err });
24
+ }
25
+ if (err.code === 'PIPE_WRITE_UNCERTAIN') {
26
+ throw new MultichatError('CLAUDE_PIPE_WRITE_UNCERTAIN', `Write to the messaging pipe of Claude pid ${session.pid} failed mid-flight; delivery state unknown.`, { cause: err });
27
+ }
28
+ }
29
+ throw err;
30
+ }
31
+ return { status: 'delivered' };
32
+ }
33
+ //# sourceMappingURL=deliver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deliver.js","sourceRoot":"","sources":["../../src/claude/deliver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAsB,MAAM,+BAA+B,CAAC;AACrF,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AAczD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,OAA6B,EAC7B,OAAe,EACf,UAAiC,EAAE;IAEnC,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,wBAAwB,EAAE,CAAC;IACtE,MAAM,KAAK,GAAG,aAAa,CAAC,WAAW,EAAE,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,mBAAmB,CAAC,CAAC;IACnF,MAAM,KAAK,GAAG,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC;IAChE,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,gBAAgB,EAAE,CAAC;IAC1D,IAAI,CAAC;QACH,MAAM,SAAS,CAAC,SAAS,CAAC,OAAO,CAAC,mBAAmB,EAAE,KAAK,CAAC,CAAC;IAChE,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,cAAc,EAAE,CAAC;YAClC,IAAI,GAAG,CAAC,IAAI,KAAK,qBAAqB,EAAE,CAAC;gBACvC,MAAM,IAAI,cAAc,CACtB,4BAA4B,EAC5B,sDAAsD,OAAO,CAAC,GAAG,GAAG,EACpE,EAAE,KAAK,EAAE,GAAG,EAAE,CACf,CAAC;YACJ,CAAC;YACD,IAAI,GAAG,CAAC,IAAI,KAAK,sBAAsB,EAAE,CAAC;gBACxC,MAAM,IAAI,cAAc,CACtB,6BAA6B,EAC7B,6CAA6C,OAAO,CAAC,GAAG,6CAA6C,EACrG,EAAE,KAAK,EAAE,GAAG,EAAE,CACf,CAAC;YACJ,CAAC;QACH,CAAC;QACD,MAAM,GAAG,CAAC;IACZ,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;AACjC,CAAC"}
@@ -0,0 +1,16 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ export function encodeUserFrame(content, messageId = randomUUID()) {
3
+ const frame = {
4
+ msgV: 1,
5
+ msg_id: messageId,
6
+ type: 'user',
7
+ message: { role: 'user', content },
8
+ priority: 'next',
9
+ };
10
+ return JSON.stringify(frame);
11
+ }
12
+ /** Mandatory first line on Windows pipes (docs/research/claude-windows-pipe.md §3). */
13
+ export function encodeAuthLine(token) {
14
+ return JSON.stringify({ type: 'auth', token });
15
+ }
16
+ //# sourceMappingURL=frame.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frame.js","sourceRoot":"","sources":["../../src/claude/frame.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAczC,MAAM,UAAU,eAAe,CAAC,OAAe,EAAE,YAAoB,UAAU,EAAE;IAC/E,MAAM,KAAK,GAAoB;QAC7B,IAAI,EAAE,CAAC;QACP,MAAM,EAAE,SAAS;QACjB,IAAI,EAAE,MAAM;QACZ,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE;QAClC,QAAQ,EAAE,MAAM;KACjB,CAAC;IACF,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;AAC/B,CAAC;AAED,uFAAuF;AACvF,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,OAAO,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;AACjD,CAAC"}
@@ -0,0 +1,45 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { MultichatError } from '../errors.js';
5
+ import { PIPE_PREFIX } from '../platform/pipe-transport.js';
6
+ /**
7
+ * Claude Code normalizes the pipe path before hashing: keep the
8
+ * `\\.\pipe\` prefix, lowercase everything after it
9
+ * (docs/research/claude-windows-pipe.md §3, verified against live samples).
10
+ */
11
+ export function normalizePipePath(messagingSocketPath) {
12
+ if (!messagingSocketPath.startsWith(PIPE_PREFIX)) {
13
+ throw new MultichatError('CLAUDE_PIPE_PATH_INVALID', `Not a Windows named pipe path: ${JSON.stringify(messagingSocketPath)}`);
14
+ }
15
+ return PIPE_PREFIX + messagingSocketPath.slice(PIPE_PREFIX.length).toLowerCase();
16
+ }
17
+ /** Key file name: `<pid>.<sha256(normalized pipe path)>.key`. */
18
+ export function keyFileName(pid, messagingSocketPath) {
19
+ const digest = createHash('sha256')
20
+ .update(normalizePipePath(messagingSocketPath), 'utf8')
21
+ .digest('hex');
22
+ return `${pid}.${digest}.key`;
23
+ }
24
+ /** Read the peerToken bound to a session's messaging pipe. */
25
+ export function readPeerToken(sessionsDir, pid, messagingSocketPath) {
26
+ const filePath = join(sessionsDir, keyFileName(pid, messagingSocketPath));
27
+ let text;
28
+ try {
29
+ text = readFileSync(filePath, 'utf8');
30
+ }
31
+ catch (err) {
32
+ throw new MultichatError('CLAUDE_KEY_FILE_MISSING', `Cannot read key file for pid ${pid}: ${filePath}`, { cause: err });
33
+ }
34
+ try {
35
+ const parsed = JSON.parse(text);
36
+ if (typeof parsed.peerToken !== 'string' || parsed.peerToken === '') {
37
+ throw new Error('peerToken missing or empty');
38
+ }
39
+ return parsed.peerToken;
40
+ }
41
+ catch (err) {
42
+ throw new MultichatError('CLAUDE_KEY_FILE_INVALID', `Key file for pid ${pid} has no usable peerToken: ${filePath}`, { cause: err });
43
+ }
44
+ }
45
+ //# sourceMappingURL=key-file.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"key-file.js","sourceRoot":"","sources":["../../src/claude/key-file.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAE5D;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,mBAA2B;IAC3D,IAAI,CAAC,mBAAmB,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;QACjD,MAAM,IAAI,cAAc,CACtB,0BAA0B,EAC1B,kCAAkC,IAAI,CAAC,SAAS,CAAC,mBAAmB,CAAC,EAAE,CACxE,CAAC;IACJ,CAAC;IACD,OAAO,WAAW,GAAG,mBAAmB,CAAC,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,WAAW,EAAE,CAAC;AACnF,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,WAAW,CAAC,GAAW,EAAE,mBAA2B;IAClE,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,CAAC;SAChC,MAAM,CAAC,iBAAiB,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;SACtD,MAAM,CAAC,KAAK,CAAC,CAAC;IACjB,OAAO,GAAG,GAAG,IAAI,MAAM,MAAM,CAAC;AAChC,CAAC;AAED,8DAA8D;AAC9D,MAAM,UAAU,aAAa,CAC3B,WAAmB,EACnB,GAAW,EACX,mBAA2B;IAE3B,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,EAAE,WAAW,CAAC,GAAG,EAAE,mBAAmB,CAAC,CAAC,CAAC;IAC1E,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IACxC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,cAAc,CACtB,yBAAyB,EACzB,gCAAgC,GAAG,KAAK,QAAQ,EAAE,EAClD,EAAE,KAAK,EAAE,GAAG,EAAE,CACf,CAAC;IACJ,CAAC;IACD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAA4B,CAAC;QAC3D,IAAI,OAAO,MAAM,CAAC,SAAS,KAAK,QAAQ,IAAI,MAAM,CAAC,SAAS,KAAK,EAAE,EAAE,CAAC;YACpE,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;QAChD,CAAC;QACD,OAAO,MAAM,CAAC,SAAS,CAAC;IAC1B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,cAAc,CACtB,yBAAyB,EACzB,oBAAoB,GAAG,6BAA6B,QAAQ,EAAE,EAC9D,EAAE,KAAK,EAAE,GAAG,EAAE,CACf,CAAC;IACJ,CAAC;AACH,CAAC"}
@@ -0,0 +1,79 @@
1
+ import { readdirSync, readFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { MultichatError } from '../errors.js';
5
+ import { isProcessAlive } from '../platform/process-inspector.js';
6
+ /** Registry kinds crosschat can route to. daemon/daemon-worker are not user sessions. */
7
+ const ROUTABLE_KINDS = new Set(['interactive', 'bg']);
8
+ export function defaultClaudeSessionsDir() {
9
+ return join(homedir(), '.claude', 'sessions');
10
+ }
11
+ function parseEntry(text) {
12
+ let value;
13
+ try {
14
+ value = JSON.parse(text);
15
+ }
16
+ catch {
17
+ return undefined;
18
+ }
19
+ if (typeof value.pid !== 'number' || !Number.isInteger(value.pid))
20
+ return undefined;
21
+ if (typeof value.messagingSocketPath !== 'string' || value.messagingSocketPath === '') {
22
+ return undefined;
23
+ }
24
+ return {
25
+ pid: value.pid,
26
+ sessionId: typeof value.sessionId === 'string' ? value.sessionId : '',
27
+ kind: typeof value.kind === 'string' ? value.kind : '',
28
+ name: typeof value.name === 'string' && value.name !== '' ? value.name : undefined,
29
+ status: typeof value.status === 'string' ? value.status : 'unknown',
30
+ cwd: typeof value.cwd === 'string' ? value.cwd : undefined,
31
+ messagingSocketPath: value.messagingSocketPath,
32
+ };
33
+ }
34
+ /**
35
+ * Enumerate routable Claude sessions from the registry: kind interactive/bg
36
+ * with a live process. Malformed entries are skipped and counted, never thrown.
37
+ */
38
+ export function listClaudeSessions(dir = defaultClaudeSessionsDir()) {
39
+ let files;
40
+ try {
41
+ files = readdirSync(dir);
42
+ }
43
+ catch {
44
+ return { sessions: [], malformed: 0 };
45
+ }
46
+ const sessions = [];
47
+ let malformed = 0;
48
+ for (const file of files) {
49
+ if (!file.endsWith('.json'))
50
+ continue;
51
+ let entry;
52
+ try {
53
+ entry = parseEntry(readFileSync(join(dir, file), 'utf8'));
54
+ }
55
+ catch {
56
+ entry = undefined;
57
+ }
58
+ if (entry === undefined) {
59
+ malformed += 1;
60
+ continue;
61
+ }
62
+ if (!ROUTABLE_KINDS.has(entry.kind))
63
+ continue;
64
+ if (!isProcessAlive(entry.pid))
65
+ continue;
66
+ sessions.push(entry);
67
+ }
68
+ return { sessions, malformed };
69
+ }
70
+ /** Exact-name lookup over a scan's sessions; throws NAME_COLLISION on duplicates. */
71
+ export function findByExactName(name, sessions) {
72
+ const matches = sessions.filter((session) => session.name === name);
73
+ if (matches.length > 1) {
74
+ const candidates = matches.map((m) => `pid ${m.pid} (${m.sessionId})`).join(', ');
75
+ throw new MultichatError('NAME_COLLISION', `Multiple Claude sessions are named ${JSON.stringify(name)}: ${candidates}. Address one by pid instead.`);
76
+ }
77
+ return matches[0];
78
+ }
79
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/claude/registry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import process from 'node:process';
5
+ import { MultichatError } from '../errors.js';
6
+ export function defaultResolveExeDeps() {
7
+ return {
8
+ env: process.env,
9
+ where: (name) => spawnSync('where.exe', [name], { encoding: 'utf8' }),
10
+ exists: existsSync,
11
+ };
12
+ }
13
+ export function resolveClaudeExe(deps = defaultResolveExeDeps()) {
14
+ const candidates = [];
15
+ if (deps.env.APPDATA) {
16
+ candidates.push(join(deps.env.APPDATA, 'npm', 'node_modules', '@anthropic-ai', 'claude-code', 'node_modules', '@anthropic-ai', 'claude-code-win32-x64', 'claude.exe'));
17
+ }
18
+ const where = deps.where('claude');
19
+ if (where.status === 0) {
20
+ for (const line of where.stdout.split(/\r?\n/)) {
21
+ if (line.trim().toLowerCase().endsWith('.exe'))
22
+ candidates.push(line.trim());
23
+ }
24
+ }
25
+ const found = candidates.find((candidate) => deps.exists(candidate));
26
+ if (found === undefined) {
27
+ throw new MultichatError('CLAUDE_EXE_NOT_FOUND', `claude executable not found; tried: ${candidates.join(', ')}`);
28
+ }
29
+ return found;
30
+ }
31
+ //# sourceMappingURL=resolve-exe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-exe.js","sourceRoot":"","sources":["../../src/claude/resolve-exe.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,OAAO,MAAM,cAAc,CAAC;AACnC,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAe9C,MAAM,UAAU,qBAAqB;IACnC,OAAO;QACL,GAAG,EAAE,OAAO,CAAC,GAAG;QAChB,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,SAAS,CAAC,WAAW,EAAE,CAAC,IAAI,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;QACrE,MAAM,EAAE,UAAU;KACnB,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,OAAuB,qBAAqB,EAAE;IAC7E,MAAM,UAAU,GAAa,EAAE,CAAC;IAChC,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;QACrB,UAAU,CAAC,IAAI,CACb,IAAI,CACF,IAAI,CAAC,GAAG,CAAC,OAAO,EAChB,KAAK,EACL,cAAc,EACd,eAAe,EACf,aAAa,EACb,cAAc,EACd,eAAe,EACf,uBAAuB,EACvB,YAAY,CACb,CACF,CAAC;IACJ,CAAC;IACD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IACnC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;YAC/C,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC;gBAAE,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QAC/E,CAAC;IACH,CAAC;IACD,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC;IACrE,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,MAAM,IAAI,cAAc,CACtB,sBAAsB,EACtB,uCAAuC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAC/D,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}