@logicseek/hello-cc 1.1.0-rc.4 → 1.1.0-rc.5
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.
- package/CHANGELOG.md +39 -0
- package/docs/README.md +2 -0
- package/docs/README.zh-CN.md +2 -0
- package/docs/app-bridge.md +110 -0
- package/docs/app-bridge.zh-CN.md +80 -0
- package/docs/dsh.md +1 -1
- package/docs/dsh.zh-CN.md +1 -1
- package/lib/cli/app.mjs +3 -0
- package/lib/cli/commands/app-bridge.mjs +89 -0
- package/lib/cli/commands/hook.mjs +21 -2
- package/lib/coordination-state.mjs +5 -2
- package/lib/core/coordination/messages.mjs +7 -2
- package/lib/integrations/claude-app-collaboration.mjs +222 -0
- package/lib/integrations/claude-app.mjs +289 -0
- package/lib/integrations/claude-mod/register.mjs +141 -0
- package/lib/integrations/codex-app-probe.mjs +213 -0
- package/lib/integrations/dsh-collaboration.mjs +19 -2
- package/lib/integrations/dsh-cordis.d.ts +2 -0
- package/lib/integrations/dsh-cordis.mjs +26 -33
- package/lib/integrations/dsh-inbox.mjs +251 -0
- package/lib/ui/help.mjs +1 -0
- package/package.json +1 -1
- package/scripts/dsh-inbox-acceptance.mjs +210 -0
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,45 @@ the release description from the current changelog section. Use
|
|
|
16
16
|
`npm run release:github` with `GH_TOKEN` or `GITHUB_TOKEN` only for local
|
|
17
17
|
backfills.
|
|
18
18
|
|
|
19
|
+
## 1.1.0-rc.5
|
|
20
|
+
|
|
21
|
+
### Summary
|
|
22
|
+
|
|
23
|
+
This preview adds desktop Agent communication adapters: idle inbox wakeup for
|
|
24
|
+
existing DeepSeek Harness Agents, an opt-in bridge for one Claude Desktop Code
|
|
25
|
+
session, and a read-only probe for an explicitly selected Codex endpoint.
|
|
26
|
+
It continues the existing preview channel.
|
|
27
|
+
|
|
28
|
+
### Highlights
|
|
29
|
+
|
|
30
|
+
- Wake an idle live Harness Agent with external HCC messages through the official
|
|
31
|
+
followup API. Poll every second by default; set `inboxPollMs` to `0` to disable
|
|
32
|
+
idle wakeup. Preserve admission decisions and acknowledge only exact committed
|
|
33
|
+
context. Self messages, rejected input and reload gaps cannot create retry loops.
|
|
34
|
+
- Generate a private Claude Desktop Mod with `hcc app claude serve`. Bind it to
|
|
35
|
+
one project and existing session; record replies and acknowledgements together
|
|
36
|
+
only after the matching main turn completes. Requires an embedded Claude Code
|
|
37
|
+
engine of at least 2.1.287 and explicit plugin loading in that session.
|
|
38
|
+
- Preserve uncertain Claude deliveries across disconnects and restarts. Ordinary
|
|
39
|
+
hooks cannot replay those inputs or replace an active Mod owner. Read-only
|
|
40
|
+
inbox queries retain the unresolved messages for inspection.
|
|
41
|
+
- Inspect an explicitly supplied Codex Unix socket with `hcc app codex probe`.
|
|
42
|
+
The probe does not start, resume or send to a thread, and does not establish
|
|
43
|
+
that Desktop owns that endpoint. Codex Desktop sending remains unavailable.
|
|
44
|
+
- Include Chinese and English capability, setup and acceptance guidance. Keep
|
|
45
|
+
the existing native CLI workers and their ownership boundaries.
|
|
46
|
+
|
|
47
|
+
### Validation
|
|
48
|
+
|
|
49
|
+
The implementation passed 1,825 local unit tests (two platform/privilege skips),
|
|
50
|
+
all 13 regression stages and five checks against the official Harness runtime
|
|
51
|
+
with deterministic localhost model responses. Merged-source CI also passed
|
|
52
|
+
Linux/macOS regression, Web and installed Harness package acceptance. These
|
|
53
|
+
checks do not establish real-model Desktop interoperability across all three
|
|
54
|
+
Apps. The generated Claude plugin and marketplace pass the official 2.1.289
|
|
55
|
+
strict validator. Device-specific results have separate receipts; the minimum
|
|
56
|
+
Desktop engine version remains a prerequisite.
|
|
57
|
+
|
|
19
58
|
## 1.1.0-rc.4
|
|
20
59
|
|
|
21
60
|
### Summary
|
package/docs/README.md
CHANGED
|
@@ -41,3 +41,5 @@ authenticated selection of any existing server directory are accepted risks.
|
|
|
41
41
|
surface, stack, shim behavior, and implementation plan.
|
|
42
42
|
- [Architecture](architecture.md): target project layout, module boundaries,
|
|
43
43
|
dependency direction, and staged migration plan.
|
|
44
|
+
|
|
45
|
+
- [Desktop Agent communication](app-bridge.md): DSH idle wakeup, Claude Desktop Mod and read-only Codex probes.
|
package/docs/README.zh-CN.md
CHANGED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Desktop Agent communication
|
|
2
|
+
|
|
3
|
+
Message persistence, model context admission and completed replies are separate
|
|
4
|
+
states. HCC-owned native workers do not take over existing Desktop chats.
|
|
5
|
+
|
|
6
|
+
These commands are introduced in preview version `1.1.0-rc.5` and require
|
|
7
|
+
Node.js 24+. Use the existing `preview` installation channel. From this source
|
|
8
|
+
checkout, replace `hcc` below with `node ./bin/hcc.mjs`; older installed versions
|
|
9
|
+
do not include these commands.
|
|
10
|
+
|
|
11
|
+
| Surface | Implementation | Evidence boundary |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| DeepSeek Harness App / Web | Cordis tools and idle inbox wakeup on live Agents | Pinned official runtime with a deterministic localhost model, not installed Desktop/model acceptance |
|
|
14
|
+
| Claude Desktop Code tab | Opt-in Mod for one selected existing session, bus replies and correlated completion | Embedded Claude Code 2.1.287+; protocol tests and official 2.1.289 strict static validation passed; Desktop acceptance pending |
|
|
15
|
+
| Codex App | Read-only explicit app-server socket probe | No prompt/resume or verified Desktop endpoint ownership |
|
|
16
|
+
|
|
17
|
+
## DeepSeek Harness
|
|
18
|
+
|
|
19
|
+
Use the existing [Cordis integration](dsh.md). `inboxPollMs` defaults to 1000,
|
|
20
|
+
accepts 100–60000, or 0 to disable idle polling. Set it in the Harness plugin
|
|
21
|
+
configuration, not HCC's integrity-checked managed files. Active-step tools and
|
|
22
|
+
context remain enabled.
|
|
23
|
+
|
|
24
|
+
Only complete unread messages from other peers trigger `Agent.followup()`.
|
|
25
|
+
Self messages cannot wake the Agent or starve later external input. Admission
|
|
26
|
+
and tool policies remain effective. Only exact committed context is ACKed.
|
|
27
|
+
Cancelled/rejected batches remain unread without automatic retry loops.
|
|
28
|
+
Unload removes only this plugin's queued input; reload rediscovers live Agents.
|
|
29
|
+
Ambiguous commits across a reload gap are not automatically replayed.
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
node scripts/dsh-inbox-acceptance.mjs --dsh-install /absolute/path/to/official-install
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
This isolated test uses ACP for Agent creation/cleanup but never calls
|
|
36
|
+
`session/prompt`. It uses localhost deterministic responses, no credentials,
|
|
37
|
+
and does not install or modify the user's App.
|
|
38
|
+
|
|
39
|
+
## Claude Desktop Code
|
|
40
|
+
|
|
41
|
+
Ordinary Chat and CLI Channels are different integrations. Check the embedded
|
|
42
|
+
Desktop engine, not only the independently installed CLI:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
hcc app claude capability --version 2.1.287
|
|
46
|
+
hcc --root /absolute/project app claude serve --session-id EXISTING_SESSION_ID
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The first command only checks the version prerequisite. The foreground bridge
|
|
50
|
+
prints a new private marketplace directory, plugin name, marketplace name and
|
|
51
|
+
peer ID. Explicitly load it in the original Desktop Code session:
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
/plugin marketplace add <marketplaceDirectory>
|
|
55
|
+
/plugin install <pluginName>@<marketplace>
|
|
56
|
+
/reload-plugins
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Desktop also supports **+ → Plugins → Add plugin**. Each generated marketplace
|
|
60
|
+
has a unique name/version. HCC does not install/upgrade plugins or modify global
|
|
61
|
+
settings/accounts. The generated `hcc-session-link` plugin and marketplace
|
|
62
|
+
both passed official Claude Code 2.1.289 `claude plugin validate <directory>
|
|
63
|
+
--strict`. This static check does not establish target Desktop compatibility
|
|
64
|
+
or replace device acceptance.
|
|
65
|
+
|
|
66
|
+
The target must not already have another HCC hooks/native/Mod owner. Once it
|
|
67
|
+
connects, send through the existing bus:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
hcc --root /absolute/project msg send --from coordinator --to PRINTED_PEER_ID --body 'Review the current task results'
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The Mod uses `$.prompt.submit()` with Mod provenance. Only the matching main
|
|
74
|
+
turn can complete the message; human turns and subagents cannot acknowledge
|
|
75
|
+
it. Reply and ACK commit atomically. Incoming replies never generate another
|
|
76
|
+
automatic reply. Queued/claimed/started do not mean completed. Aborts,
|
|
77
|
+
disconnects and uncertain restarts remain unread with durable intent records;
|
|
78
|
+
ordinary hooks also exclude those prior submissions from automatic context.
|
|
79
|
+
Normal inbox queries retain them for explicit inspection before another send.
|
|
80
|
+
|
|
81
|
+
Keep the command running. Ctrl-C removes this generated plugin and closes HCC's
|
|
82
|
+
bridge, preserving the App session. The 0700 directory/0600 files contain a
|
|
83
|
+
temporary local capability, not a Claude account credential: never publish or
|
|
84
|
+
commit them. Same-OS-user access is inside the local trust boundary, not
|
|
85
|
+
provider-attested identity.
|
|
86
|
+
|
|
87
|
+
References: [Mods](https://code.claude.com/docs/en/plugins/mods/overview),
|
|
88
|
+
[API](https://code.claude.com/docs/en/plugins/mods/api),
|
|
89
|
+
[installation](https://code.claude.com/docs/en/plugins/install).
|
|
90
|
+
|
|
91
|
+
## Codex App
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
hcc --root /absolute/project --json app codex probe \
|
|
95
|
+
--socket /absolute/path/to/known-control.sock --thread EXISTING_THREAD_ID
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The probe only initializes, reads thread metadata without turns, and performs
|
|
99
|
+
bounded loaded-thread queries. It never scans sockets, starts a daemon,
|
|
100
|
+
resumes/starts/interrupts threads, or reads accounts. Closing affects only its
|
|
101
|
+
own connection. Provider preview/name/error contents are not reported.
|
|
102
|
+
|
|
103
|
+
`loaded: null` means unestablished. `writable` and `desktopEndpointVerified`
|
|
104
|
+
remain false even when loaded. Public `thread/resume` can cold-restore a thread
|
|
105
|
+
and has no atomic only-loaded precondition. An in-app tool is not automatically
|
|
106
|
+
a public external HCC API. References are the installed official CLI 0.144.6
|
|
107
|
+
help/schema snapshot and the official local source tree at
|
|
108
|
+
`d109393270432531ac0010542ae7973801e0d9d7`. An exact CLI-to-source build match
|
|
109
|
+
and online documentation recheck are not established. `app-server proxy`
|
|
110
|
+
tunnels WebSocket frames, not native stdio JSONL.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# 桌面 Agent 通信
|
|
2
|
+
|
|
3
|
+
消息入库、进入模型上下文和完成回复是不同状态。`hcc native` 启动的自有 worker 与桌面已有会话也是不同连接。
|
|
4
|
+
|
|
5
|
+
以下入口从 `1.1.0-rc.5` 预览版本加入,要求 Node.js 24+。使用现有 `preview` 渠道安装;从源码运行时,可将下文的 `hcc` 替换为 `node ./bin/hcc.mjs`。较早的已安装版本不包含这些命令。
|
|
6
|
+
|
|
7
|
+
| 对象 | 当前实现 | 验证边界 |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| DeepSeek Harness App / Web | Cordis 插件在原 live Agent 中收件并唤醒 | 固定官方运行时+本地确定性模型,尚不代表已安装桌面真实模型验收 |
|
|
10
|
+
| Claude Desktop Code | 绑定一个已有会话的可选 Mod,以及 HCC 收件/自动回复 | 需要内嵌引擎 2.1.287+;协议测试和官方 2.1.289 严格静态校验已通过,桌面实测待完成 |
|
|
11
|
+
| Codex App | 显式 app-server socket 的只读检查 | 不发 prompt、不恢复会话,尚未证明端点归属桌面 |
|
|
12
|
+
|
|
13
|
+
## DeepSeek Harness
|
|
14
|
+
|
|
15
|
+
沿用 [Cordis 安装方式](dsh.zh-CN.md)。插件默认每 1000ms 检查真实存活 Agent,通过官方 `followup()` 唤醒有外来未读消息的空闲会话。活动会话仍在步骤及停止边界处理。
|
|
16
|
+
|
|
17
|
+
在 Harness 插件配置中设置 `inboxPollMs`:允许 `100..60000`,默认 `1000`;`0` 关闭自动空闲唤醒,保留活动步骤的协作工具与上下文。不要手改 HCC 带完整性校验的托管文件。
|
|
18
|
+
|
|
19
|
+
- 自身消息不触发空闲唤醒,也不会挡住排在后面的外来消息。
|
|
20
|
+
- 同一批消息只排队一次,准确的 `user/message` 提交后才 ACK。
|
|
21
|
+
- 准入拒绝、取消或准备失败后保持未读,不自动重试同一批外来输入;新外来消息或人工后续步骤可继续处理。
|
|
22
|
+
- 卸载只移除本插件未被领取的输入,不删除用户输入;热重载重新发现 live Agent。
|
|
23
|
+
- 热重载监听空档内的提交结果若不明,保持未读且不自动重放。
|
|
24
|
+
- 原有准入、工具审批、项目与 peer 身份约束继续生效。
|
|
25
|
+
|
|
26
|
+
隔离测试命令:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
node scripts/dsh-inbox-acceptance.mjs --dsh-install /absolute/path/to/official-install
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
测试使用私有临时目录和 localhost 模型响应。ACP 只创建/清理隔离 Agent,**不调用 `session/prompt`**,不读取账号凭据或修改已安装 App。
|
|
33
|
+
|
|
34
|
+
## Claude Desktop Code
|
|
35
|
+
|
|
36
|
+
普通 Chat 和 CLI Channels 是其他入口。先检查目标 **Desktop 内嵌引擎**,单独升级 CLI 不代表桌面也已更新:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
hcc app claude capability --version 2.1.287
|
|
40
|
+
hcc --root /absolute/project app claude serve --session-id EXISTING_SESSION_ID
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
第一条只检查版本前提;第二条为明确选定的已有 session ID 启动前台桥接,并输出 `marketplaceDirectory`、`pluginName`、`marketplace` 和 peer ID。随后由用户在原 Desktop Code 会话显式加载:
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
/plugin marketplace add <marketplaceDirectory>
|
|
47
|
+
/plugin install <pluginName>@<marketplace>
|
|
48
|
+
/reload-plugins
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Desktop 也提供 **+ → Plugins → Add plugin**。生成器使用每次唯一的本地 marketplace 名称,防止复用旧桥接缓存。HCC 不自动安装、升级或改账号/全局配置。生成的 `hcc-session-link` 插件及其 marketplace 已通过官方 Claude Code 2.1.289 的 `claude plugin validate <directory> --strict` 校验。该静态校验不代替目标 Desktop 的版本检查和实机验收。
|
|
52
|
+
|
|
53
|
+
原会话不能同时由其他 HCC hooks/native/Mod 适配器持有。连接成功后,从其他 peer 发送:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
hcc --root /absolute/project msg send --from coordinator --to PRINTED_PEER_ID --body '请检查当前任务的测试结果'
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Mod 使用官方 `$.prompt.submit()` 并保留 Mod 来源。只有精确匹配输入的主回合完成才能确认消息,其他人工回合和子代理结果不能误确认。回复与 ACK 在一个数据库事务内提交,收到 `reply` 不再自动回复,避免循环。
|
|
60
|
+
|
|
61
|
+
排队、领取、开始均不等于完成。取消、断线或重启后的不确定投递保留未读与持久记录,不盲目重发。关闭桥接后,普通 Hooks 也不会重放这些旧输入;普通 inbox 查询仍可查看它们。先检查 App 的实际结果,再明确决定是否另发消息。
|
|
62
|
+
|
|
63
|
+
保持前台命令运行。Ctrl-C 清理这次生成的插件并关闭桥接,不结束 App 会话。插件目录含此次桥接的临时凭据(目录 0700、文件 0600),不能分享、提交或发布到公共市场。它不是 Claude 账号凭据;同一 OS 用户能读取该目录的代码属于本地信任范围,不提供 provider 签名身份。
|
|
64
|
+
|
|
65
|
+
依据:[Mods 概览](https://code.claude.com/docs/en/plugins/mods/overview)、[API](https://code.claude.com/docs/en/plugins/mods/api)、[插件安装](https://code.claude.com/docs/en/plugins/install)。
|
|
66
|
+
|
|
67
|
+
## Codex App
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
hcc --root /absolute/project --json app codex probe \
|
|
71
|
+
--socket /absolute/path/to/known-control.sock --thread EXISTING_THREAD_ID
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
只连接给定 Unix socket,初始化后调用 `thread/read(includeTurns:false)` 和有界的 `thread/loaded/list`。不扫描 socket、不启动 daemon、不读取账号或完整聊天历史、不调用 resume/start/interrupt。结束时仅断开自己的连接,原始 preview/name/error 不写进报告。
|
|
75
|
+
|
|
76
|
+
`loaded: null` 表示尚未确认。即使 loaded 为 true,`writable` 和 `desktopEndpointVerified` 也保持 false,因为该结果不能证明桌面正在使用同一执行器。
|
|
77
|
+
|
|
78
|
+
公开 `thread/resume` 既可加入 live thread,也可冷恢复会话,没有“只能附着已加载会话”的原子条件。当前实现不把先检查再 resume 的竞争窗口当成纯附着。应用内聊天工具也不自动构成 HCC 外部程序的公开入口。
|
|
79
|
+
|
|
80
|
+
参考依据为本机官方 CLI 0.144.6 的帮助与 schema 快照,以及本机官方源码树 `d109393270432531ac0010542ae7973801e0d9d7` 的 app-server README/协议定义。未证明源码树与 CLI 构建版本完全对应,也未完成官方网页在线复核。`app-server proxy` 传输 WebSocket 握手和帧,不是 native adapter 的 stdio JSONL。
|
package/docs/dsh.md
CHANGED
|
@@ -57,7 +57,7 @@ The Cordis plugin awaits `agent/created`, delegates the `agent/pre-step` decisio
|
|
|
57
57
|
|
|
58
58
|
The plugin reuses CLI/MCP transactions and task/lock ownership rules. Context and tool output are each bounded to 16,000 characters by default. Truncation is explicit; incompletely delivered messages remain unread. The shared service also validates argument bounds and authority. ACK occurs only when Harness commits the exact context as a `user/message`; rejected admission, preparation failure and cancellation do not ACK early.
|
|
59
59
|
|
|
60
|
-
`next()` delegation preserves deny/ask policy. Closing an Agent expires only its own peer. Unloading removes listeners/tool registrations and revokes stale authority;
|
|
60
|
+
`next()` delegation preserves deny/ask policy. Closing an Agent expires only its own peer. Unloading removes listeners/tool registrations and revokes stale authority; after reload, polling rediscovers existing live Agents; an awaited pre-step can also initialize them. After an unclean exit, the same session can recover its Cordis owner only when the recorded process is verified dead or its PID has been reused. Live or unverifiable owners and conflicting hooks/native bindings remain protected. The plugin polls live Agents every 1000ms by default and uses official followup to wake idle Agents with external unread messages. Set inboxPollMs to 0 to disable idle polling; active steps and stop boundaries still process context. Self messages do not wake an Agent, rejected/cancelled batches are not repeatedly retried, and ACK follows exact context commit. See [Desktop Agent communication](app-bridge.md).
|
|
61
61
|
|
|
62
62
|
## Modes, Arguments And Routing
|
|
63
63
|
|
package/docs/dsh.zh-CN.md
CHANGED
|
@@ -57,7 +57,7 @@ Cordis 插件使用 `agent/created` 的等待式初始化、`agent/pre-step` 的
|
|
|
57
57
|
|
|
58
58
|
底层复用 CLI/MCP 的事务、任务归属和锁规则。上下文与工具输出默认各限制 16,000 字符;过长输出明确标记截断,未完整投递的消息保留未读。工具参数的长度、数量和归属由服务再次验证。消息只在 Harness 提交对应 `user/message` 记录后 ACK;拒绝首轮、准备失败和取消不会提前 ACK。
|
|
59
59
|
|
|
60
|
-
插件委托 `next()`,保留现有 deny/ask 权限决定。关闭 Agent 只更新其自身 peer
|
|
60
|
+
插件委托 `next()`,保留现有 deny/ask 权限决定。关闭 Agent 只更新其自身 peer;卸载时移除监听与工具注册,并使旧的执行权限失效。重新加载时,轮询会重新发现既有 live Agent;等待式 pre-step 也可重新初始化其身份。异常退出后,只有核实原进程已退出或 PID 已被复用时,同一会话才能恢复 Cordis 归属;活跃或无法验证的 owner,以及 hooks/native 冲突绑定仍会被拒绝。插件默认每 1000ms 检查 live Agent,并用官方 followup 唤醒有外来未读消息的空闲会话;活动步骤和停止边界仍继续处理。inboxPollMs: 0 可关闭自动空闲唤醒。自身消息不触发唤醒,拒绝或取消不导致同批输入反复重试;上下文提交后才 ACK。详见 [桌面 Agent 通信](app-bridge.zh-CN.md)。
|
|
61
61
|
|
|
62
62
|
## 配置模式与参数
|
|
63
63
|
|
package/lib/cli/app.mjs
CHANGED
|
@@ -34,6 +34,7 @@ import { createMiscCommands } from '../cli/commands/misc.mjs';
|
|
|
34
34
|
import { createInstallCommands } from '../cli/commands/install.mjs';
|
|
35
35
|
import { createHookCommand } from '../cli/commands/hook.mjs';
|
|
36
36
|
import { createDshCommands } from '../cli/commands/dsh.mjs';
|
|
37
|
+
import { createAppBridgeCommands } from '../cli/commands/app-bridge.mjs';
|
|
37
38
|
import { createNativeCommands } from '../cli/commands/native.mjs';
|
|
38
39
|
import { createMcpCommands } from '../cli/commands/mcp.mjs';
|
|
39
40
|
import { createPeerCommands } from '../cli/commands/peer.mjs';
|
|
@@ -354,6 +355,7 @@ const { cmdHook } = createHookCommand({
|
|
|
354
355
|
});
|
|
355
356
|
|
|
356
357
|
const { cmdDsh } = createDshCommands();
|
|
358
|
+
const { cmdAppBridge } = createAppBridgeCommands();
|
|
357
359
|
|
|
358
360
|
const { cmdPeer } = createPeerCommands({
|
|
359
361
|
connect, now, tx, addEvent, auditPayload, printResult, CliError, parseOpts,
|
|
@@ -659,6 +661,7 @@ async function dispatch(ctx, rest) {
|
|
|
659
661
|
if (command === 'web') return cmdWeb(ctx, args);
|
|
660
662
|
if (command === 'hook') return cmdHook(ctx, args);
|
|
661
663
|
if (command === 'dsh') return cmdDsh(ctx, args);
|
|
664
|
+
if (command === 'app') return cmdAppBridge(ctx, args);
|
|
662
665
|
if (command === 'native') return cmdNative(ctx, args);
|
|
663
666
|
if (command === 'mcp') return cmdMcp(ctx, args);
|
|
664
667
|
if (command === 'install-hooks') return cmdInstallHooks(ctx, args);
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import { CliError } from '../../shared/errors.mjs';
|
|
3
|
+
import { parseOpts, required, validateOpts, wantsHelp, intOpt } from '../../cli-args.mjs';
|
|
4
|
+
import { printResult } from '../../format.mjs';
|
|
5
|
+
import { projectDbPath } from '../../runtime/paths.mjs';
|
|
6
|
+
import { assertSelectedCwdSnapshot } from '../../process/selected-cwd-identity.mjs';
|
|
7
|
+
|
|
8
|
+
export function appBridgeHelp() {
|
|
9
|
+
console.log(`Usage:
|
|
10
|
+
hcc --root PROJECT app claude serve --session-id ID [--plugin-dir NEW_DIRECTORY]
|
|
11
|
+
hcc --root PROJECT app codex probe --socket ABSOLUTE_PATH --thread ID [--timeout-ms 5000]
|
|
12
|
+
hcc app claude capability --version VERSION
|
|
13
|
+
|
|
14
|
+
Claude: generate an opt-in private Desktop Mod and run its local bridge in the
|
|
15
|
+
foreground. Requires Claude Code 2.1.287+ in Desktop and the selected session.
|
|
16
|
+
No plugin is installed and no account or global configuration is changed.
|
|
17
|
+
Ctrl-C closes this bridge, removes its generated plugin and preserves the App.
|
|
18
|
+
Queued/submitted input is not acknowledged until its own turn completes.
|
|
19
|
+
|
|
20
|
+
Codex: read-only probe of an explicitly supplied existing Unix socket. It never
|
|
21
|
+
starts/resumes a thread or sends a prompt. A loaded thread does not establish
|
|
22
|
+
that the desktop App owns the endpoint; writable remains false.
|
|
23
|
+
|
|
24
|
+
DSH App: use the Cordis integration (hcc dsh setup --mode cordis).
|
|
25
|
+
Its inboxPollMs setting controls idle wakeup; 0 disables automatic polling.`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createAppBridgeCommands() {
|
|
29
|
+
async function cmdAppBridge(ctx, args) {
|
|
30
|
+
if (!args.length || wantsHelp(args)) return appBridgeHelp();
|
|
31
|
+
const [provider, action, ...rest] = args;
|
|
32
|
+
const opts = parseOpts(rest);
|
|
33
|
+
if (provider === 'codex' && action === 'probe') {
|
|
34
|
+
validateOpts('app codex probe', opts, ['socket', 'thread', 'timeout-ms']);
|
|
35
|
+
const { probeCodexEndpoint } = await import('../../integrations/codex-app-probe.mjs');
|
|
36
|
+
if (ctx.initialRootIdentity) assertSelectedCwdSnapshot(ctx.initialRootIdentity);
|
|
37
|
+
const result = await probeCodexEndpoint({ socketPath: required(opts, 'socket'),
|
|
38
|
+
threadId: required(opts, 'thread'), expectedCwd: ctx.root, timeoutMs: intOpt(opts, 'timeout-ms', 5000) });
|
|
39
|
+
if (ctx.initialRootIdentity) assertSelectedCwdSnapshot(ctx.initialRootIdentity);
|
|
40
|
+
printResult(ctx, result, value => [
|
|
41
|
+
`Endpoint connected: ${value.transportConnected}; target loaded: ${value.loaded ?? 'unknown'}`,
|
|
42
|
+
'Desktop ownership unverified; message sending disabled.',
|
|
43
|
+
...value.unmetConditions.map(item => `${item.code}: ${item.message}`)
|
|
44
|
+
].join('\n'));
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
if (provider === 'claude' && action === 'capability') {
|
|
48
|
+
validateOpts('app claude capability', opts, ['version']);
|
|
49
|
+
const { claudeAppCapability } = await import('../../integrations/claude-app.mjs');
|
|
50
|
+
const result = claudeAppCapability(required(opts, 'version'));
|
|
51
|
+
const prerequisite = result.supported ? 'met' : 'not met';
|
|
52
|
+
printResult(ctx, result, value => `Desktop Mod version prerequisite: ${prerequisite} (minimum ${value.minimumVersion}); this does not test a connected session.`);
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
if (provider !== 'claude' || action !== 'serve') throw new CliError('BAD_ARGS', 'Unknown App bridge command');
|
|
56
|
+
validateOpts('app claude serve', opts, ['session-id', 'plugin-dir']);
|
|
57
|
+
// This service always uses the selected project's canonical managed bus.
|
|
58
|
+
if (ctx.dbPath !== projectDbPath(ctx.root)) throw new CliError('BAD_ARGS', 'App bridges use the project managed database; omit --db');
|
|
59
|
+
const sessionId = required(opts, 'session-id');
|
|
60
|
+
const { createClaudeAppBridge, writeClaudeAppPlugin } = await import('../../integrations/claude-app.mjs');
|
|
61
|
+
const { createClaudeAppCoordination } = await import('../../integrations/claude-app-collaboration.mjs');
|
|
62
|
+
let bridge, plugin, coordination, stop;
|
|
63
|
+
const finished = new Promise(resolve => { stop = resolve; });
|
|
64
|
+
const signal = () => stop();
|
|
65
|
+
try {
|
|
66
|
+
bridge = await createClaudeAppBridge({ root: ctx.root, sessionId, initialRootIdentity: ctx.initialRootIdentity });
|
|
67
|
+
plugin = writeClaudeAppPlugin({ directory: opts['plugin-dir'], bridge });
|
|
68
|
+
coordination = createClaudeAppCoordination({ root: ctx.root, sessionId, bridge, initialRootIdentity: ctx.initialRootIdentity,
|
|
69
|
+
onStatus: status => {
|
|
70
|
+
const message = status.error ? `Claude App bridge: ${status.error}` : `Claude App peer ${status.peer} connected`;
|
|
71
|
+
process.stderr.write(message + '\n');
|
|
72
|
+
} });
|
|
73
|
+
process.once('SIGINT', signal); process.once('SIGTERM', signal);
|
|
74
|
+
printResult(ctx, { sessionId, peer: coordination.peer, pluginDirectory: plugin.directory,
|
|
75
|
+
marketplaceDirectory: plugin.marketplaceDirectory, marketplace: plugin.marketplace, pluginName: plugin.name,
|
|
76
|
+
minimumVersion: plugin.minimumVersion, connected: false, foreground: true }, value =>
|
|
77
|
+
`Waiting for the selected Claude Desktop Code session.\nPeer: ${value.peer}\n` +
|
|
78
|
+
`Private marketplace: ${value.marketplaceDirectory}\nPlugin: ${value.pluginName}@${value.marketplace}\n` +
|
|
79
|
+
`Requires Claude Code ${value.minimumVersion}+. Add this local marketplace and plugin explicitly, then /reload-plugins in the selected session.\n` +
|
|
80
|
+
'Keep this command running; Ctrl-C removes the generated plugin and disconnects HCC.');
|
|
81
|
+
await finished;
|
|
82
|
+
} finally {
|
|
83
|
+
process.removeListener('SIGINT', signal); process.removeListener('SIGTERM', signal);
|
|
84
|
+
try { coordination?.close(); }
|
|
85
|
+
finally { try { await bridge?.close(); } finally { plugin?.dispose(); } }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return { cmdAppBridge };
|
|
89
|
+
}
|
|
@@ -158,11 +158,30 @@ async function cmdHook(ctx, args) {
|
|
|
158
158
|
process.exitCode = 0;
|
|
159
159
|
return;
|
|
160
160
|
}
|
|
161
|
+
// A Desktop Mod already drives this exact session. Ordinary hooks must
|
|
162
|
+
// neither replace its transport binding nor consume its inbox a second time.
|
|
163
|
+
const appOwnsSession = () => {
|
|
164
|
+
const appBinding = db.prepare(`SELECT * FROM peer_bindings WHERE transport='claude-mod'
|
|
165
|
+
AND runtime_target IS NOT NULL AND (peer=? OR (provider='claude' AND provider_session_id=?)) LIMIT 1`).get(peerId, sessionId);
|
|
166
|
+
if (!appBinding) return false;
|
|
167
|
+
if (kind !== 'claude' || appBinding.provider !== 'claude' ||
|
|
168
|
+
appBinding.provider_session_id !== sessionId || appBinding.runtime_session_id !== sessionId) {
|
|
169
|
+
throw new CliError('CLAUDE_APP_OWNERSHIP_CONFLICT', 'Hook does not match the existing Desktop Mod session');
|
|
170
|
+
}
|
|
171
|
+
return true;
|
|
172
|
+
};
|
|
173
|
+
if (appOwnsSession()) {
|
|
174
|
+
process.exitCode = 0;
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
161
177
|
const status = hookKey === 'stop' ? 'idle' : 'working';
|
|
162
178
|
const providerAncestor = readAncestorCliInfo(isDsh ? 'dsh' : undefined);
|
|
163
179
|
const providerPid = providerAncestor?.kind === kind ? Number(providerAncestor.pid) : null;
|
|
164
180
|
const providerIdentity = providerPid ? liveProcessIdentity(providerPid) : null;
|
|
165
181
|
const registerHookIdentity = () => {
|
|
182
|
+
// Recheck after obtaining the write transaction: a Mod may have
|
|
183
|
+
// registered while this hook was collecting process evidence.
|
|
184
|
+
if (kind === 'claude' && appOwnsSession()) return false;
|
|
166
185
|
if (isDsh) assertDshHookBinding(db, peerId, sessionId);
|
|
167
186
|
const existing = db.prepare('SELECT id FROM peers WHERE id = ?').get(peerId);
|
|
168
187
|
if (!existing) {
|
|
@@ -224,8 +243,8 @@ async function cmdHook(ctx, args) {
|
|
|
224
243
|
};
|
|
225
244
|
// Keep the dsh conflict check and identity/binding writes atomic, so a
|
|
226
245
|
// concurrent service/session registration cannot change the inspected row.
|
|
227
|
-
|
|
228
|
-
|
|
246
|
+
const registered = isDsh || kind === 'claude' ? tx(db, registerHookIdentity) : registerHookIdentity();
|
|
247
|
+
if (registered === false) { process.exitCode = 0; return; }
|
|
229
248
|
// hb-06: an active hook is proof the peer is working. During clock grace,
|
|
230
249
|
// renew retained locks even when the wall-clock jump made them look expired.
|
|
231
250
|
const hookNow = now();
|
|
@@ -92,7 +92,7 @@ export function createCoordinationState(deps = {}) {
|
|
|
92
92
|
ORDER BY resource ASC
|
|
93
93
|
`).all();
|
|
94
94
|
const messages = peer
|
|
95
|
-
? queryInbox(db, peer, false, 50)
|
|
95
|
+
? queryInbox(db, peer, false, 50, { excludeAppDeliveries: opts.excludeAppDeliveries === true })
|
|
96
96
|
: db.prepare(`
|
|
97
97
|
SELECT id, sender, recipient, task_id, kind, body, reply_to, thread_id, created_at
|
|
98
98
|
FROM messages
|
|
@@ -142,7 +142,10 @@ export function createCoordinationState(deps = {}) {
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
function buildHookCoordinationContext(db, ctx, peerId) {
|
|
145
|
-
|
|
145
|
+
// A prior App submission may have run even when its final reply was lost.
|
|
146
|
+
// Returning to ordinary hooks must not silently replay that input. Normal
|
|
147
|
+
// inbox/state queries keep it visible and unread for explicit inspection.
|
|
148
|
+
const snapshot = collectStateSnapshot(db, ctx, peerId, { excludeAppDeliveries: true });
|
|
146
149
|
const openTasks = snapshot.tasks.slice(0, 8);
|
|
147
150
|
const unread = snapshot.messages.slice(0, 5);
|
|
148
151
|
const peers = snapshot.peers.slice(0, 8);
|
|
@@ -31,7 +31,10 @@ export function createMessageStore(deps = {}) {
|
|
|
31
31
|
return messageId;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
function queryInbox(db, peer, includeAll, limit) {
|
|
34
|
+
function queryInbox(db, peer, includeAll, limit, options = {}) {
|
|
35
|
+
const excludeAppDeliveries = options.excludeAppDeliveries === true;
|
|
36
|
+
const appDeliveryFilter = excludeAppDeliveries
|
|
37
|
+
? 'AND NOT EXISTS (SELECT 1 FROM meta WHERE key = ? || m.id)' : '';
|
|
35
38
|
return db.prepare(`
|
|
36
39
|
SELECT
|
|
37
40
|
m.id, m.sender, m.recipient, m.task_id, m.kind, m.body,
|
|
@@ -42,9 +45,11 @@ export function createMessageStore(deps = {}) {
|
|
|
42
45
|
WHERE
|
|
43
46
|
(m.recipient IS NULL OR m.recipient = '' OR m.recipient = 'all' OR m.recipient = ?)
|
|
44
47
|
AND (? = 1 OR r.read_at IS NULL)
|
|
48
|
+
${appDeliveryFilter}
|
|
45
49
|
ORDER BY m.id ASC
|
|
46
50
|
LIMIT ?
|
|
47
|
-
`).all(peer, peer, includeAll ? 1 : 0,
|
|
51
|
+
`).all(peer, peer, includeAll ? 1 : 0,
|
|
52
|
+
...(excludeAppDeliveries ? [`claude-app.delivery.${peer}.`] : []), limit);
|
|
48
53
|
}
|
|
49
54
|
|
|
50
55
|
function queryTimelineMessages(db, peer, limit) {
|