grix-connector 1.0.2
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/README.md +149 -0
- package/dist/adapter/acp/acp-adapter.js +13 -0
- package/dist/adapter/acp/index.js +1 -0
- package/dist/adapter/acp/usage-parser.js +1 -0
- package/dist/adapter/claude/activity-status-manager.js +1 -0
- package/dist/adapter/claude/channel-notification.js +1 -0
- package/dist/adapter/claude/claude-adapter.js +15 -0
- package/dist/adapter/claude/claude-bridge-server.js +1 -0
- package/dist/adapter/claude/claude-tools.js +1 -0
- package/dist/adapter/claude/claude-worker-client.js +1 -0
- package/dist/adapter/claude/index.js +1 -0
- package/dist/adapter/claude/interaction-protocol.js +1 -0
- package/dist/adapter/claude/mcp-http-launcher.js +2 -0
- package/dist/adapter/claude/model-list.js +1 -0
- package/dist/adapter/claude/protocol-contract.js +1 -0
- package/dist/adapter/claude/result-timeout.js +1 -0
- package/dist/adapter/claude/skill-scanner.js +2 -0
- package/dist/adapter/claude/usage-parser.js +3 -0
- package/dist/adapter/codewhale/codewhale-adapter.js +6 -0
- package/dist/adapter/codewhale/index.js +1 -0
- package/dist/adapter/codex/codex-bridge.js +10 -0
- package/dist/adapter/codex/codex-trust.js +8 -0
- package/dist/adapter/codex/index.js +1 -0
- package/dist/adapter/codex/usage-parser.js +1 -0
- package/dist/adapter/cursor/cursor-adapter.js +8 -0
- package/dist/adapter/cursor/index.js +1 -0
- package/dist/adapter/deepseek/deepseek-adapter.js +6 -0
- package/dist/adapter/deepseek/index.js +1 -0
- package/dist/adapter/index.js +1 -0
- package/dist/adapter/opencode/index.js +1 -0
- package/dist/adapter/opencode/opencode-adapter.js +8 -0
- package/dist/adapter/opencode/opencode-transport.js +5 -0
- package/dist/adapter/opencode/opencode-types.js +0 -0
- package/dist/adapter/openhuman/index.js +1 -0
- package/dist/adapter/openhuman/openhuman-adapter.js +7 -0
- package/dist/adapter/openhuman/openhuman-transport.js +1 -0
- package/dist/adapter/openhuman/openhuman-types.js +0 -0
- package/dist/adapter/pi/index.js +1 -0
- package/dist/adapter/pi/pi-adapter.js +10 -0
- package/dist/adapter/pi/pi-transport.js +4 -0
- package/dist/adapter/pi/pi-types.js +0 -0
- package/dist/adapter/pi/usage-parser.js +1 -0
- package/dist/adapter/qwen/index.js +1 -0
- package/dist/adapter/qwen/qwen-adapter.js +4 -0
- package/dist/adapter/types.js +1 -0
- package/dist/agent/index.js +1 -0
- package/dist/agent/process.js +2 -0
- package/dist/aibot/client.js +1 -0
- package/dist/aibot/index.js +1 -0
- package/dist/aibot/types.js +0 -0
- package/dist/bridge/adapter-pool.js +1 -0
- package/dist/bridge/bridge.js +10 -0
- package/dist/bridge/deferred-events.js +1 -0
- package/dist/bridge/event-queue.js +1 -0
- package/dist/bridge/index.js +1 -0
- package/dist/bridge/respawn-manager.js +1 -0
- package/dist/bridge/revoke-handler.js +1 -0
- package/dist/bridge/runtime-config.js +1 -0
- package/dist/bridge/send-controller.js +1 -0
- package/dist/bridge/session-controller.js +9 -0
- package/dist/bridge/tool-card-utils.js +1 -0
- package/dist/core/access/allowlist-gate.js +1 -0
- package/dist/core/access/allowlist-store.js +1 -0
- package/dist/core/access/index.js +1 -0
- package/dist/core/aibot/client.js +1 -0
- package/dist/core/aibot/connection-handle.js +1 -0
- package/dist/core/aibot/connection-manager.js +1 -0
- package/dist/core/aibot/event-lifecycle-types.js +0 -0
- package/dist/core/aibot/index.js +1 -0
- package/dist/core/aibot/types.js +0 -0
- package/dist/core/config/index.js +1 -0
- package/dist/core/config/paths.js +1 -0
- package/dist/core/context/channel-context-resolution.js +1 -0
- package/dist/core/context/channel-context-store.js +1 -0
- package/dist/core/context/index.js +1 -0
- package/dist/core/context/transcript-channel-context.js +1 -0
- package/dist/core/file-ops/handler.js +1 -0
- package/dist/core/file-ops/list-files.js +1 -0
- package/dist/core/file-ops/types.js +0 -0
- package/dist/core/files/create-folder.js +1 -0
- package/dist/core/files/index.js +1 -0
- package/dist/core/files/list-files.js +1 -0
- package/dist/core/files/list-handler.js +1 -0
- package/dist/core/files/types.js +0 -0
- package/dist/core/files/utils.js +1 -0
- package/dist/core/hooks/hook-signal-store.js +2 -0
- package/dist/core/hooks/index.js +1 -0
- package/dist/core/log/bridge-event-log.js +2 -0
- package/dist/core/log/conversation-log.js +3 -0
- package/dist/core/log/index.js +1 -0
- package/dist/core/log/logger.js +6 -0
- package/dist/core/log/packet-log.js +2 -0
- package/dist/core/log/rotation.js +2 -0
- package/dist/core/mcp/event-tool-executor.js +1 -0
- package/dist/core/mcp/index.js +1 -0
- package/dist/core/mcp/internal-api-server.js +1 -0
- package/dist/core/mcp/tool-schemas.js +1 -0
- package/dist/core/mcp/tools.js +1 -0
- package/dist/core/persistence/active-event-store.js +1 -0
- package/dist/core/persistence/agent-global-config-store.js +1 -0
- package/dist/core/persistence/elicitation-store.js +1 -0
- package/dist/core/persistence/event-results-store.js +1 -0
- package/dist/core/persistence/permission-store.js +1 -0
- package/dist/core/persistence/question-store.js +1 -0
- package/dist/core/persistence/session-binding-store.js +1 -0
- package/dist/core/protocol/agent-api-media.js +1 -0
- package/dist/core/protocol/attachment-file.js +1 -0
- package/dist/core/protocol/index.js +1 -0
- package/dist/core/protocol/interaction-parser.js +1 -0
- package/dist/core/protocol/message-metadata.js +2 -0
- package/dist/core/protocol/message-reference.js +2 -0
- package/dist/core/protocol/payload-parser.js +11 -0
- package/dist/core/protocol/protocol-descriptor.js +1 -0
- package/dist/core/protocol/protocol-text.js +1 -0
- package/dist/core/provider-quota/index.js +1 -0
- package/dist/core/provider-quota/kiro.js +1 -0
- package/dist/core/provider-quota/providers.js +1 -0
- package/dist/core/provider-quota/types.js +0 -0
- package/dist/core/runtime/health.js +1 -0
- package/dist/core/runtime/index.js +1 -0
- package/dist/core/runtime/pidfile.js +2 -0
- package/dist/core/runtime/spawn.js +1 -0
- package/dist/core/text-segmentation/index.js +1 -0
- package/dist/core/text-segmentation/safe-markdown-stream-segmenter.js +6 -0
- package/dist/core/transport/index.js +1 -0
- package/dist/core/transport/json-rpc.js +3 -0
- package/dist/core/upgrade/npm-upgrader.js +2 -0
- package/dist/core/upgrade/upgrade-checker.js +1 -0
- package/dist/core/util/client-version.js +1 -0
- package/dist/core/util/codex-output-policy.js +1 -0
- package/dist/core/util/event-buffer.js +1 -0
- package/dist/core/util/index.js +1 -0
- package/dist/core/util/json-file.js +2 -0
- package/dist/core/util/normalize-string.js +1 -0
- package/dist/core/util/quoted-message-stream.js +3 -0
- package/dist/grix.js +28 -0
- package/dist/index.js +1 -0
- package/dist/log.js +3 -0
- package/dist/main.js +31 -0
- package/dist/manager.js +1 -0
- package/dist/mcp/acp-mcp-server.js +5 -0
- package/dist/mcp/stdio/server.js +10 -0
- package/dist/mcp/stream-http/config.js +1 -0
- package/dist/mcp/stream-http/connection-binding.js +1 -0
- package/dist/mcp/stream-http/event-tool-executor.js +1 -0
- package/dist/mcp/stream-http/gateway.js +1 -0
- package/dist/mcp/stream-http/index.js +1 -0
- package/dist/mcp/stream-http/security.js +1 -0
- package/dist/mcp/stream-http/session-manager.js +1 -0
- package/dist/mcp/stream-http/tool-executor.js +1 -0
- package/dist/mcp/stream-http/tool-registry.js +1 -0
- package/dist/mcp/stream-http/tool-schemas.js +1 -0
- package/dist/protocol/acp-client.js +1 -0
- package/dist/protocol/event-mapper.js +5 -0
- package/dist/protocol/index.js +1 -0
- package/dist/runtime/daemon-lock.js +2 -0
- package/dist/runtime/service-state.js +2 -0
- package/dist/scripts/approve-plan-hook.js +2 -0
- package/dist/scripts/elicitation-hook.js +6 -0
- package/dist/scripts/lib/read-stdin.js +1 -0
- package/dist/scripts/lifecycle-hook.js +2 -0
- package/dist/scripts/notification-hook.js +4 -0
- package/dist/scripts/permission-hook.js +5 -0
- package/dist/scripts/status-line-forwarder.js +2 -0
- package/dist/scripts/user-prompt-submit-hook.js +2 -0
- package/dist/service/platform-adapter.js +45 -0
- package/dist/service/process-control.js +1 -0
- package/dist/service/service-install-store.js +1 -0
- package/dist/service/service-manager.js +1 -0
- package/dist/service/service-paths.js +1 -0
- package/dist/session/index.js +1 -0
- package/dist/session/manager.js +1 -0
- package/dist/transport/index.js +1 -0
- package/dist/transport/json-rpc.js +3 -0
- package/dist/types/events.js +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/protocol.js +0 -0
- package/dist/types/session-state.js +0 -0
- package/dist/types/usage.js +0 -0
- package/openclaw-plugin/index.js +11271 -0
- package/openclaw-plugin/skills/grix-admin/SKILL.md +202 -0
- package/openclaw-plugin/skills/grix-admin/references/api-contract.md +210 -0
- package/openclaw-plugin/skills/grix-egg/SKILL.md +81 -0
- package/openclaw-plugin/skills/grix-egg/references/api-contract.md +40 -0
- package/openclaw-plugin/skills/grix-group/SKILL.md +164 -0
- package/openclaw-plugin/skills/grix-group/references/api-contract.md +97 -0
- package/openclaw-plugin/skills/grix-query/SKILL.md +247 -0
- package/openclaw-plugin/skills/grix-register/SKILL.md +86 -0
- package/openclaw-plugin/skills/grix-register/references/api-contract.md +76 -0
- package/openclaw-plugin/skills/grix-register/references/grix-concepts.md +26 -0
- package/openclaw-plugin/skills/grix-register/references/handoff-contract.md +24 -0
- package/openclaw-plugin/skills/grix-register/references/openclaw-setup.md +6 -0
- package/openclaw-plugin/skills/grix-register/references/user-replies.md +25 -0
- package/openclaw-plugin/skills/grix-register/scripts/grix_auth.ts +599 -0
- package/openclaw-plugin/skills/grix-update/SKILL.md +310 -0
- package/openclaw-plugin/skills/grix-update/references/cron-setup.md +56 -0
- package/openclaw-plugin/skills/grix-update/references/update-contract.md +149 -0
- package/openclaw-plugin/skills/message-send/SKILL.md +197 -0
- package/openclaw-plugin/skills/message-unsend/SKILL.md +186 -0
- package/openclaw-plugin/skills/message-unsend/flowchart.mermaid +27 -0
- package/openclaw-plugin/skills/openclaw-memory-setup/SKILL.md +282 -0
- package/openclaw-plugin/skills/openclaw-memory-setup/references/case-study-macpro.md +52 -0
- package/openclaw-plugin/skills/openclaw-memory-setup/references/host-readiness.md +147 -0
- package/openclaw-plugin/skills/openclaw-memory-setup/scripts/bench_ollama_embeddings.ts +326 -0
- package/openclaw-plugin/skills/openclaw-memory-setup/scripts/set_openclaw_memory_model.ts +385 -0
- package/openclaw-plugin/skills/openclaw-memory-setup/scripts/survey_host_readiness.ts +294 -0
- package/openclaw.plugin.json +24 -0
- package/package.json +114 -0
- package/scripts/install-guardian.mjs +30 -0
- package/scripts/install-guardian.sh +30 -0
- package/scripts/upgrade-guardian.sh +98 -0
package/README.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# grix-connector
|
|
2
|
+
|
|
3
|
+
A command-line daemon that connects your local AI coding agents to the [Grix](https://grix.dhf.pub) platform.
|
|
4
|
+
|
|
5
|
+
## What is Grix?
|
|
6
|
+
|
|
7
|
+
Grix is an AI Agent scheduling platform. It lets you manage and interact with multiple AI coding agents through a unified chat interface. Register at [grix.dhf.pub](https://grix.dhf.pub) to get started.
|
|
8
|
+
|
|
9
|
+
## Supported Agents
|
|
10
|
+
|
|
11
|
+
| `client_type` | Agent | Required CLI |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `claude` | Claude Code (Anthropic) | `claude` |
|
|
14
|
+
| `codex` | Codex (OpenAI) | `codex` |
|
|
15
|
+
| `gemini` | Gemini (Google) | `gemini` |
|
|
16
|
+
| `qwen` | Qwen (Alibaba) | `qwen` |
|
|
17
|
+
| `codewhale` | CodeWhale | `codewhale` |
|
|
18
|
+
| `cursor` | Cursor Agent | `agent` |
|
|
19
|
+
| `opencode` | OpenCode | `opencode` |
|
|
20
|
+
| `pi` | Pi | `pi` |
|
|
21
|
+
| `openhuman` | OpenHuman | `openhuman-core` |
|
|
22
|
+
| `reasonix` | Reasonix | `reasonix` |
|
|
23
|
+
|
|
24
|
+
You need to have the corresponding CLI tool installed locally before connecting an agent.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install -g grix-connector
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Requires Node.js >= 18.
|
|
33
|
+
|
|
34
|
+
On Windows, `grix-connector` uses the built-in Task Scheduler with a hidden WScript launcher (no extra dependency required).
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
### 1. Register a Grix account
|
|
39
|
+
|
|
40
|
+
Go to [grix.dhf.pub](https://grix.dhf.pub), sign up and get your API key.
|
|
41
|
+
|
|
42
|
+
### 2. Create agent config
|
|
43
|
+
|
|
44
|
+
Create `~/.grix/config/agents.json`:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"agents": [
|
|
49
|
+
{
|
|
50
|
+
"name": "my-agent",
|
|
51
|
+
"ws_url": "wss://grix.dhf.pub/v1/agent-api/ws",
|
|
52
|
+
"agent_id": "your-agent-id",
|
|
53
|
+
"api_key": "your-grix-api-key",
|
|
54
|
+
"client_type": "claude"
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Change `client_type` to match the agent you want to connect (see table above). You can define multiple agents in one file, or use separate files under `~/.grix/config/`.
|
|
61
|
+
|
|
62
|
+
### Config Reference
|
|
63
|
+
|
|
64
|
+
Each agent entry uses one flat structure:
|
|
65
|
+
|
|
66
|
+
| Field | Required | Description |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| `name` | yes | Display name for this agent |
|
|
69
|
+
| `ws_url` | yes | WebSocket endpoint URL, e.g. `wss://grix.dhf.pub/v1/agent-api/ws` |
|
|
70
|
+
| `agent_id` | yes | Agent ID from Grix platform |
|
|
71
|
+
| `api_key` | yes | API key for authentication |
|
|
72
|
+
| `client_type` | yes | See Supported Agents table above |
|
|
73
|
+
| `prompt_timeout_ms` | no | Prompt execution timeout (ms) |
|
|
74
|
+
| `pool.maxSize` | no | Max adapter pool size (default 20) |
|
|
75
|
+
| `pool.idleTimeoutMs` | no | Idle adapter eviction timeout (default 300000 = 5 min) |
|
|
76
|
+
|
|
77
|
+
Adapter command/args/options are built in and resolved from `client_type`. To connect a different agent, simply change `client_type` — no other config changes needed.
|
|
78
|
+
|
|
79
|
+
### Multi-agent Example
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"agents": [
|
|
84
|
+
{
|
|
85
|
+
"name": "my-claude",
|
|
86
|
+
"ws_url": "wss://grix.dhf.pub/v1/agent-api/ws",
|
|
87
|
+
"agent_id": "your-agent-id",
|
|
88
|
+
"api_key": "your-grix-api-key",
|
|
89
|
+
"client_type": "claude"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"name": "my-gemini",
|
|
93
|
+
"ws_url": "wss://grix.dhf.pub/v1/agent-api/ws",
|
|
94
|
+
"agent_id": "another-agent-id",
|
|
95
|
+
"api_key": "your-grix-api-key",
|
|
96
|
+
"client_type": "gemini"
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 3. Start the daemon
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
grix-connector start
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The daemon connects to Grix via WebSocket and starts routing chat messages to your agents.
|
|
109
|
+
|
|
110
|
+
### Commands
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
grix-connector start # Start as system service (auto-installs on first run)
|
|
114
|
+
grix-connector stop # Stop the service
|
|
115
|
+
grix-connector restart # Restart the service
|
|
116
|
+
grix-connector status # Check service status
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## OpenClaw Plugin
|
|
120
|
+
|
|
121
|
+
grix-connector can also be installed as an [OpenClaw](https://openclaw.io) plugin, providing a Grix channel transport with admin tools and operator CLI.
|
|
122
|
+
|
|
123
|
+
### Install
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
openclaw plugin install grix-connector
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Or manually add to your OpenClaw project:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
npm install grix-connector
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Plugin Features
|
|
136
|
+
|
|
137
|
+
- **Channel**: Grix chat transport — routes messages between OpenClaw and your Grix deployment
|
|
138
|
+
- **Tools**: `grix_query`, `grix_group`, `grix_admin`, `grix_egg`, `grix_register`, `grix_update`, `grix_message_send`, `grix_message_unsend`, `openclaw_memory_setup`
|
|
139
|
+
- **CLI**: `openclaw grix` — agent management and admin commands
|
|
140
|
+
- **Skills**: 9 bundled skills for admin, group, query, registration, update, messaging, memory setup, and egg orchestration
|
|
141
|
+
|
|
142
|
+
### Requirements
|
|
143
|
+
|
|
144
|
+
- OpenClaw >= 2026.4.8
|
|
145
|
+
- A Grix account with agent ID and API key
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
MIT
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import{fileURLToPath as C}from"node:url";import p from"node:path";import{homedir as I}from"node:os";import{promises as g}from"node:fs";import{EventEmitter as S}from"node:events";import{spawn as R}from"node:child_process";import{AgentProcess as _}from"../../agent/process.js";import{AcpClient as T,AcpAuthRequiredError as A,isAuthRequiredError as w}from"../../protocol/acp-client.js";import{AgentEventType as m}from"../../types/events.js";import{InternalApiServer as y}from"../../core/mcp/internal-api-server.js";import{EventResultsStore as k}from"../../core/persistence/event-results-store.js";import{QuotedMessageStream as $}from"../../core/util/quoted-message-stream.js";import{SafeMarkdownStreamSegmenter as E}from"../../core/text-segmentation/index.js";import{extractAcpTurnInput as M}from"../../core/protocol/payload-parser.js";import{injectMessageMetadata as P}from"../../core/protocol/message-metadata.js";import{log as r}from"../../core/log/index.js";import{scanSkills as x}from"../claude/skill-scanner.js";const v=p.dirname(C(import.meta.url)),q=200,j=60*1e3,N=600*1e3,B=2e3;function b(l){return!(!(l instanceof Error)||l.code!==-32603||w(l))}function f(l){if(!(l instanceof Error))return String(l);let e=l.message;const t=l.data;return t&&(e+=` data=${JSON.stringify(t)}`),e}function D(l){return l.replace(/\u001b\[[0-9;?]*[ -/]*[@-~]/g,"")}class ie extends S{type="acp";config;callbacks;agentProcess=null;acpClient=null;internalApi=null;activeRun=null;eventResults=null;pendingApprovals=new Map;clientMsgSeq=0;stopped=!1;acpAuthMethod;acpInitialMode;acpMcpTools;rawTransport;approvalMode;autoInjectArgs;bindingStore;sessionBindings=new Map;deferredEvents=new Map;currentAibotSessionId;sessionConnected=!1;rawEventSeq=0;recoveryContextBySessionId=new Map;constructor(e,t,s){if(super(),this.config=e,this.callbacks=t,this.acpAuthMethod=s?.acpAuthMethod,this.acpInitialMode=s?.acpInitialMode,this.acpMcpTools=!1,this.rawTransport=s?.rawTransport??!1,this.approvalMode=s?.approvalMode??"default",this.autoInjectArgs=s?.autoInjectArgs,this.bindingStore=s?.bindingStore??null,this.currentAibotSessionId=s?.aibotSessionId?String(s.aibotSessionId).trim():void 0,s?.eventResultsPath&&(this.eventResults=new k(s.eventResultsPath)),this.bindingStore&&this.currentAibotSessionId){const i=this.bindingStore.get(this.currentAibotSessionId);i?.cwd&&this.sessionBindings.set(this.currentAibotSessionId,i.cwd)}}async start(){if(await this.spawnProcess(),!this.bindingStore){await this.connectSession(this.resolveCwd());return}const e=this.currentAibotSessionId?this.sessionBindings.get(this.currentAibotSessionId):void 0;e&&await this.connectSession(e)}async stop(){this.stopped=!0,this.rejectDeferredEvents("adapter stopped"),this.cancelAllDeferredTimers(),this.activeRun&&(this.callbacks.sendSessionComposing(this.activeRun.sessionId,!1),this.activeRun.flushTimer&&(clearTimeout(this.activeRun.flushTimer),this.activeRun.flushTimer=null),this.activeRun.idleTimer&&(clearTimeout(this.activeRun.idleTimer),this.activeRun.idleTimer=null),this.activeRun.composingTimer&&(clearInterval(this.activeRun.composingTimer),this.activeRun.composingTimer=null),this.activeRun=null),this.acpClient&&(this.acpClient.removeAllListeners(),this.acpClient=null),this.agentProcess&&(await this.agentProcess.close(),this.agentProcess=null),this.internalApi&&(await this.internalApi.stop(),this.internalApi=null)}isAlive(){return this.agentProcess?.alive??!1}async createSession(e){return this.acpClient?.sessionId??""}async resumeSession(e,t){}async destroySession(e){}sendPrompt(e){const t=new L(e.adapterSessionId);return this.acpClient?.isAlive&&this.acpClient.send(e.text).catch(s=>{t.emitError(s instanceof Error?s:new Error(String(s)))}),t}async cancel(e){this.activeRun&&this.acpClient&&(await this.acpClient.cancel(),this.flushStream(),this.finishRun("canceled","stopped by user"))}setPermissionHandler(e){}async ping(e){return this.acpClient?.isAlive?this.acpClient.ping(e):this.agentProcess?.alive??!1}getStatus(){return{alive:this.agentProcess?.alive??!1,busy:this.activeRun!==null,sessions:this.sessionConnected?1:0}}getActiveEventIds(){return this.activeRun?[this.activeRun.eventId]:[]}clearActiveEventForShutdown(){this.activeRun&&(this.activeRun.flushTimer&&(clearTimeout(this.activeRun.flushTimer),this.activeRun.flushTimer=null),this.activeRun.idleTimer&&(clearTimeout(this.activeRun.idleTimer),this.activeRun.idleTimer=null),this.activeRun.composingTimer&&(clearInterval(this.activeRun.composingTimer),this.activeRun.composingTimer=null),this.activeRun=null)}getMcpConfig(){if(!this.internalApi)return null;const e=p.resolve(v,"../../mcp/acp-mcp-server.js");return{name:"grix-connector-tools",command:process.execPath,args:[e,"--api-url",this.internalApi.url]}}getSupportedCommands(){return[{name:"model",description:"List or set model",args:"[model_id]"},{name:"mode",description:"List or set collaboration mode",args:"[mode_id]"},{name:"interrupt",description:"Interrupt current run"},{name:"status",description:"Show session status"},{name:"skills",description:"List available skills"}]}async execCommand(e,t,s){try{switch(e){case"model":{const i=t.trim();if(i)return await this.setModel(i)?{status:"ok",message:`Model set to ${i}`}:{status:"failed",message:`Failed to set model: ${i}`};const n=this.buildToolbarContext("model_list");return{status:"ok",message:`Current: ${n.currentModelId||"unknown"}`,data:n}}case"mode":{const i=t.trim();if(i)return await this.setMode(i)?{status:"ok",message:`Mode set to ${i}`}:{status:"failed",message:`Failed to set mode: ${i}`};const n=this.buildToolbarContext("mode_list");return{status:"ok",message:`Current: ${n.currentModeId||"unknown"}`,data:n}}case"interrupt":return this.activeRun?this.acpClient?(await this.cancel(this.activeRun.sessionId),{status:"ok",message:"Run interrupted"}):{status:"failed",message:"Not connected"}:{status:"failed",message:"No active run to interrupt"};case"status":{const i=this.getStatus(),n=this.acpClient?.sessionOptions;return{status:"ok",message:`Alive: ${i.alive}, Busy: ${i.busy}, Model: ${n?.currentModelId??"unknown"}, Mode: ${n?.currentModeId??"unknown"}`,data:{alive:i.alive,busy:i.busy,sessions:i.sessions,model:n?.currentModelId??"",mode:n?.currentModeId??""}}}case"skills":{const i=x({mode:this.config.command==="kiro-cli"?"kiro":"gemini",projectDir:process.cwd()}),n=i.map(a=>`- ${a.name}${a.trigger?` (${a.trigger})`:""} [${a.source}]: ${a.description}`);return{status:"ok",message:n.length>0?n.join(`
|
|
2
|
+
`):"No skills found",data:i}}default:return{status:"unsupported",message:`Unknown command: ${e}`}}}catch(i){return{status:"failed",message:i instanceof Error?i.message:String(i)}}}get acpSessionOptions(){return this.acpClient?.sessionOptions??null}buildToolbarContext(e,t){const s=this.acpClient?.sessionOptions,i=s?.currentModeId??"",n=s?.currentModelId??"",a=s?.modes.map(d=>({id:d.id,name:d.name}))??[],o=s?.models.map(d=>({modelId:d.modelId,name:d.name}))??[],c=s?.models.map(d=>({id:d.modelId,displayName:d.name}))??[],u=s?.modes.map(d=>({id:d.id,displayName:d.name}))??[],h={outcome:e,...t?{cwd:t}:{},model_id:n,mode_id:i,currentModelId:n,currentModeId:i,available_models:c,available_modes:u,availableModels:c,availableModes:u,models:o,modes:a};return r.info("acp-adapter",`[toolbar] buildToolbarContext outcome=${e} model_id="${n}" available_models=${JSON.stringify(c.map(d=>d.id))} currentModelId=${n}`),h}get pendingApprovalEntries(){return this.pendingApprovals}get hasSessionBinding(){return this.bindingStore!==null}setMode(e){return this.acpClient?this.acpClient.setLiveMode(e):Promise.resolve(!1)}setModel(e){return this.acpClient?this.acpClient.setModel(e):Promise.resolve(!1)}handleAcpApprovalAction(e,t){const s=this.pendingApprovals.get(e);return s?(this.pendingApprovals.delete(e),this.acpClient&&this.acpClient.respondPermission(s,{behavior:t}).catch(i=>{r.error("acp-adapter",`Failed to respond to permission: ${i}`)}),this.activeRun&&this.resetIdleTimer(this.activeRun),!0):!1}respondToPermission(e,t){this.acpClient&&this.acpClient.respondPermission(e,t).catch(s=>{r.error("acp-adapter",`Failed to respond to permission: ${s}`)}),this.activeRun&&this.resetIdleTimer(this.activeRun)}async handleLocalAction(e){const t=e.action_type??"",s=e.params??{};if(t==="exec_approve"||t==="exec_reject"||t==="permission_approve"||t==="permission_reject"){const i=String(s.tool_call_id??s.approval_command_id??s.approval_id??s.exec_context_id??""),n=t==="exec_approve"||t==="permission_approve",a=String(s.decision??"");let o;return n&&(a==="allow-once"||a==="allow-always")?o=a:n?o="allow":o="deny",i?this.handleAcpApprovalAction(i,o)?(this.callbacks.sendLocalActionResult(e.action_id,"ok"),{handled:!0,kind:"approval"}):(this.callbacks.sendLocalActionResult(e.action_id,"failed",void 0,"approval_not_found",`no pending approval for tool_call_id: ${i}`),{handled:!0,kind:"approval"}):(this.callbacks.sendLocalActionResult(e.action_id,"failed",void 0,"tool_call_id_required","tool_call_id is required"),{handled:!0,kind:"approval"})}return{handled:!1,kind:""}}resolveCwd(){if(this.currentAibotSessionId){const e=this.sessionBindings.get(this.currentAibotSessionId);if(e)return e}if(this.bindingStore&&this.currentAibotSessionId){const e=this.bindingStore.get(this.currentAibotSessionId);if(e?.cwd)return e.cwd}return process.cwd()}bindSession(e,t){return this.sessionBindings.get(e)?Promise.resolve(!1):(this.sessionBindings.set(e,t),this.bindingStore&&this.bindingStore.set(e,t),!this.sessionConnected&&this.agentProcess?.alive?this.connectSession(t).then(()=>!0).catch(i=>(r.error("acp-adapter",`Failed to create session on bind: ${f(i)}`),this.callbacks.sendUpdateBindingCard(e,"failed",t),!0)):(this.acpClient?.isAlive&&(this.bindingStore&&this.acpClient.sessionId&&this.bindingStore.setAcpSessionId(e,this.acpClient.sessionId),this.callbacks.sendUpdateBindingCard(e,"connected",t,this.buildToolbarContext("binding_ready",t))),Promise.resolve(!0)))}getSessionCwd(e){return this.sessionBindings.get(e)}getSessionBindings(){return this.sessionBindings}replayDeferredEvents(e){const t=this.deferredEvents.get(e);if(!(!t||t.length===0)){this.deferredEvents.delete(e),this.cancelDeferredTimer(e),r.info("acp-adapter",`Replaying ${t.length} deferred events for session ${e}`);for(const{event:s}of t){if(this.activeRun){r.info("acp-adapter",`Cannot replay ${s.event_id}: agent busy, dropping`);continue}this.startRun(s,!1)}}}announceDeferredComposing(e){const t=this.deferredEvents.get(e);!t||t.length===0||this.callbacks.sendSessionComposing(e,!0)}deliverInboundEvent(e){if(this.eventResults?.has(e.session_id,e.event_id)){const t=this.eventResults.get(e.session_id,e.event_id);r.info("acp-adapter",`Deduplicating event ${e.event_id} (cached: ${t.status})`),this.callbacks.sendEventResult(e.event_id,t.status,t.msg);return}if(this.activeRun){r.info("acp-adapter",`Event ${e.event_id} rejected: busy`),this.callbacks.sendEventResult(e.event_id,"failed","agent busy");return}if(!this.agentProcess?.alive){this.callbacks.sendEventResult(e.event_id,"failed","agent not alive");return}e.session_id&&e.session_id!==this.currentAibotSessionId&&(this.currentAibotSessionId=e.session_id),this.startRun(e,!1)}deliverStopEvent(e,t){this.acpClient?(this.acpClient.cancel().catch(()=>{}),this.flushStream()):this.flushStream(),this.activeRun?.eventId===e&&this.finishRun("canceled","stopped by user")}deferEvent(e){const t=this.deferredEvents.get(e.session_id)??[];t.some(s=>s.event.event_id===e.event_id)||(t.push({event:e,queuedAt:Date.now()}),this.deferredEvents.set(e.session_id,t),r.info("acp-adapter",`Deferred event ${e.event_id} for session ${e.session_id} (queue: ${t.length})`),this.scheduleDeferredTimeout(e.session_id))}deferredTimers=new Map;rejectDeferredEvents(e){for(const[t,s]of this.deferredEvents)for(const{event:i}of s)r.info("acp-adapter",`Rejecting deferred event ${i.event_id}: ${e}`),this.callbacks.sendEventResult(i.event_id,"failed",e);this.deferredEvents.clear(),this.cancelAllDeferredTimers()}cancelDeferredTimer(e){const t=this.deferredTimers.get(e);t&&(clearTimeout(t),this.deferredTimers.delete(e))}cancelAllDeferredTimers(){for(const e of this.deferredTimers.values())clearTimeout(e);this.deferredTimers.clear()}scheduleDeferredTimeout(e){if(this.deferredTimers.has(e))return;const t=3e4,s=setTimeout(()=>{this.deferredTimers.delete(e);const i=this.deferredEvents.get(e);if(!(!i||i.length===0)&&!this.acpClient?.isAlive){r.error("acp-adapter",`Deferred events for session ${e} timed out after ${t/1e3}s (no ACP client)`),this.deferredEvents.delete(e);for(const{event:n}of i)this.callbacks.sendEventResult(n.event_id,"failed","agent initialization timed out")}},t);this.deferredTimers.set(e,s)}startRun(e,t){if(!this.acpClient?.isAlive){this.deferEvent(e);return}const s=`acp_${++this.clientMsgSeq}_${Date.now()}`;this.activeRun={eventId:e.event_id,sessionId:e.session_id,threadId:e.thread_id,clientMsgIdBase:s,currentClientMsgId:s,currentSegmentIndex:0,chunkSeq:0,buffer:"",quotedStream:new $,markdownSegmenter:new E,quotedMessageId:void 0,flushTimer:null,idleTimer:null,composingTimer:null,awaitingToolResult:!1,responded:!1,silent:t};const i=this.activeRun,n=M({event_id:e.event_id,event_type:"delegate",session_id:e.session_id,content:e.content,msg_id:e.msg_id,msg_type:e.msg_type,quoted_message_id:e.quoted_message_id,session_type:e.session_type,context_messages:e.context_messages_json?JSON.parse(e.context_messages_json):void 0,extra:e.extra_json?JSON.parse(e.extra_json):void 0},this.resolveCwd());this.callbacks.sendSessionComposing(i.sessionId,!0),this.resetIdleTimer(i),n.modeId&&this.acpClient&&this.acpClient.setLiveMode(n.modeId).catch(()=>{}),n.modelId&&this.acpClient&&this.acpClient.setModel(n.modelId).catch(()=>{});const a=P(n.prompt,{messageId:e.msg_id,quotedMessageId:e.quoted_message_id}),o=this.injectRecoveryContext(i.sessionId,a);this.sendPromptWithRetry(i,o)}injectRecoveryContext(e,t){const s=this.recoveryContextBySessionId.get(e);return s?(this.recoveryContextBySessionId.delete(e),r.info("acp-adapter",`Injecting recovery context for session ${e} (chars=${s.length})`),`${s}
|
|
3
|
+
|
|
4
|
+
[\u5F53\u524D\u7528\u6237\u6D88\u606F]
|
|
5
|
+
${t}`):t}sendPromptWithRetry(e,t,s=0){this.acpClient.send(t).catch(i=>{if(r.error("acp-adapter",`Prompt failed (attempt ${s+1}): ${f(i)}`),s===0&&b(i)){r.info("acp-adapter",`Retrying prompt for event ${e.eventId} after retryable error`),e.buffer="",e.chunkSeq=0,e.currentSegmentIndex=0,e.currentClientMsgId=e.clientMsgIdBase,e.markdownSegmenter.reset(),e.responded=!1,e.awaitingToolResult=!1,e.flushTimer&&(clearTimeout(e.flushTimer),e.flushTimer=null),setTimeout(()=>{this.activeRun?.eventId===e.eventId&&this.acpClient?.isAlive?(this.resetIdleTimer(e),this.sendPromptWithRetry(e,t,1)):this.finishRun("failed",i instanceof Error?i.message:String(i))},B);return}const n=b(i)?"Agent \u9047\u5230\u5185\u90E8\u9519\u8BEF\uFF0C\u81EA\u52A8\u91CD\u8BD5\u540E\u4ECD\u7136\u5931\u8D25\u3002\u8BF7\u91CD\u65B0\u53D1\u9001\u6D88\u606F\uFF0C\u6216\u53D1\u9001 /grix restart \u91CD\u542F agent\u3002":i instanceof Error?i.message:String(i);this.finishRun("failed",n)})}async spawnProcess(){const e=[...this.config.args??[]];this.autoInjectArgs?.acp&&e.push("--acp"),this.autoInjectArgs?.model&&e.push("--model",this.autoInjectArgs.model),this.config.command==="gemini"&&!e.includes("--skip-trust")&&e.push("--skip-trust"),this.config.command==="kiro-cli"&&!e.includes("--trust-all-tools")&&e.push("--trust-all-tools"),this.agentProcess=new _;try{await this.agentProcess.start({command:this.config.command,args:e.length>0?e:void 0,cwd:this.resolveCwd(),env:this.config.env})}catch(t){throw r.error("acp-adapter",`Failed to spawn agent process: ${t}`),this.rejectDeferredEvents(`agent spawn failed: ${t instanceof Error?t.message:String(t)}`),this.emit("exit",null),t}r.info("acp-adapter","ACP agent process started"),this.agentProcess.on("error",t=>{this.stopped||(r.error("acp-adapter",`Agent process error: ${t.message}`),this.activeRun&&this.finishRun("failed",`agent process error: ${t.message}`),this.rejectDeferredEvents(`agent process error: ${t.message}`),this.emit("exit",null))}),this.agentProcess.on("exit",t=>{this.stopped||(r.error("acp-adapter",`Agent process exited unexpectedly (code=${t})`),this.activeRun&&this.finishRun("failed",`agent process exited (code=${t})`),this.rejectDeferredEvents(`agent process exited (code=${t})`),this.emit("exit",t))})}async connectSession(e){if(this.sessionConnected||!this.agentProcess?.transport)return;let t;this.acpMcpTools&&(t=await this.startInternalApiAndMcp()),this.acpClient=new T,this.acpClient.on("event",o=>this.handleAcpEvent(o)),this.acpClient.on("session-lost",()=>{this.stopped||this.agentProcess?.alive&&(r.error("acp-adapter","ACP transport closed while agent process still alive, triggering respawn"),this.emit("exit",null))});const s=this.currentAibotSessionId&&this.bindingStore?this.bindingStore.getAcpSessionId(this.currentAibotSessionId):void 0,i=async o=>{await this.acpClient.connect({transport:this.agentProcess.transport,authMethod:this.acpAuthMethod,initialMode:this.acpInitialMode,cwd:e||this.resolveCwd(),mcpServers:t,sessionId:o})};let n=!1,a;try{await i(s)}catch(o){if(o instanceof A){await this.handleAuthRequired(o);return}if(!s)throw this.handleConnectFailure(o),o;if(r.warn("acp-adapter",`Failed to load persisted session ${s}, fallback to session/new: ${f(o)}`),n=!0,a=s,this.config.command==="kiro-cli"&&this.currentAibotSessionId){r.info("acp-adapter",`Building kiro recovery summary: aibotSession=${this.currentAibotSessionId} acpSession=${s}`);const c=await this.buildKiroRecoveryContext(this.currentAibotSessionId,s,e||this.resolveCwd());c?(this.recoveryContextBySessionId.set(this.currentAibotSessionId,c),r.info("acp-adapter",`Recovery context prepared for session ${this.currentAibotSessionId} (chars=${c.length})`)):r.warn("acp-adapter",`Recovery context skipped: no summary generated for acpSession=${s}`)}await i(void 0)}this.sessionConnected=!0,r.info("acp-adapter",`ACP session ready: ${this.acpClient.sessionId}`),this.emit("acpSessionReady",this.acpClient.sessionId),this.currentAibotSessionId&&this.bindingStore&&this.bindingStore.setAcpSessionId(this.currentAibotSessionId,this.acpClient.sessionId);for(const[o,c]of this.sessionBindings){this.bindingStore&&this.acpClient.sessionId&&this.bindingStore.setAcpSessionId(o,this.acpClient.sessionId);const u=n?"binding_recreated":"binding_ready",h=this.buildToolbarContext(u,c);n&&a&&(h.previous_session_id=a,h.recreated_session_id=this.acpClient.sessionId),this.callbacks.sendUpdateBindingCard(o,"ready",c,h)}}async buildKiroRecoveryContext(e,t,s){try{const i=p.join(I(),".kiro","sessions","cli",`${t}.json`);await g.access(i),r.info("acp-adapter",`Reading kiro source session file: ${i}`);const n=await this.generateSummaryByKiroCli(i,s);if(!n)return;const a=p.join(I(),".grix","data","session-summaries");await g.mkdir(a,{recursive:!0});const o=p.join(a,`${e}.md`),c=[`SOURCE_KIRO_SESSION_FILE: ${i}`,"",n.trim(),""].join(`
|
|
6
|
+
`);return await g.writeFile(o,c,"utf8"),r.info("acp-adapter",`Recovery summary saved: ${o} (chars=${n.length})`),["[\u5386\u53F2\u4E0A\u4E0B\u6587\u56DE\u704C\u8BF4\u660E]","\u4EE5\u4E0B\u5185\u5BB9\u6765\u81EA\u65E7\u4F1A\u8BDD\u81EA\u52A8\u6458\u8981\uFF0C\u8BF7\u5728\u540E\u7EED\u56DE\u7B54\u4E2D\u5EF6\u7EED\u5176\u4E2D\u7684\u76EE\u6807\u3001\u7EA6\u675F\u548C\u7ED3\u8BBA\u3002",`SOURCE_KIRO_SESSION_FILE: ${i}`,`SUMMARY_FILE: ${o}`,"",n.trim()].join(`
|
|
7
|
+
`)}catch(i){r.warn("acp-adapter",`Failed to build kiro recovery context: ${f(i)}`);return}}async generateSummaryByKiroCli(e,t){const i=["chat","--no-interactive","--trust-tools=read,grep",["\u8BF7\u57FA\u4E8E\u4E0B\u9762\u5F15\u7528\u7684 Kiro \u539F\u59CB\u4F1A\u8BDD\u6587\u4EF6\u751F\u6210\u6458\u8981\uFF0C\u8F93\u51FA\u5FC5\u987B\u4E3A Markdown\u3002","\u8981\u6C42\uFF1A1) \u7B80\u660E\u51C6\u786E\uFF1B2) \u5305\u542B goals/constraints/decisions/open_items \u56DB\u90E8\u5206\uFF1B3) \u4E0D\u8981\u8F93\u51FA\u4EE3\u7801\u5757\u56F4\u680F\u3002",`\u4F1A\u8BDD\u6587\u4EF6\uFF1A@"${e}"`].join(`
|
|
8
|
+
`)],n=await this.runCommandCapture("kiro-cli",i,t,45e3);if(!n)return;const a=D(n).trim(),o=a.indexOf("> "),c=o>=0?a.slice(o+2).trim():a;if(c)return r.info("acp-adapter",`Kiro summary generated from ${e} (raw_chars=${a.length}, body_chars=${c.length})`),c.split(`
|
|
9
|
+
`).filter(u=>!u.includes("Credits:")&&!u.includes("Time:")).join(`
|
|
10
|
+
`).trim()}runCommandCapture(e,t,s,i){return new Promise((n,a)=>{const o=R(e,t,{cwd:s,stdio:["ignore","pipe","pipe"]});let c="",u="";const h=setTimeout(()=>{o.kill("SIGTERM"),a(new Error(`${e} timed out after ${i}ms`))},i);o.stdout.on("data",d=>{c+=String(d)}),o.stderr.on("data",d=>{u+=String(d)}),o.on("error",d=>{clearTimeout(h),a(d)}),o.on("close",d=>{if(clearTimeout(h),d===0){n(c||u);return}a(new Error(`${e} exited with code ${d}: ${u||c}`))})})}handleConnectFailure(e){r.error("acp-adapter",`ACP session creation failed: ${f(e)}`),this.acpClient?.removeAllListeners(),this.acpClient=null,this.emit("exit",null)}async startInternalApiAndMcp(){try{this.internalApi||(this.internalApi=new y,this.internalApi.setInvokeHandler(async(t,s)=>this.callbacks.agentInvoke(t,s)),await this.internalApi.start(0),r.info("acp-adapter",`Internal API started at ${this.internalApi.url}`));const e=p.resolve(v,"../../mcp/acp-mcp-server.js");return[{name:"grix-connector-tools",command:process.execPath,args:[e,"--api-url",this.internalApi.url],env:{GRIX_CONNECTOR_INTERNAL_API:this.internalApi.url}}]}catch(e){r.error("acp-adapter",`Failed to start MCP tools: ${e}`);return}}cleanup(){this.activeRun&&this.callbacks.sendSessionComposing(this.activeRun.sessionId,!1),this.pendingApprovals.clear(),this.rejectDeferredEvents("adapter cleaned up"),this.cancelAllDeferredTimers(),this.sessionConnected=!1,this.acpClient&&(this.acpClient.removeAllListeners(),this.acpClient=null),this.agentProcess&&(this.agentProcess.removeAllListeners(),this.agentProcess=null)}async handleAuthRequired(e){r.info("acp-adapter",`Auth required, methods: ${e.authMethods.map(n=>n.id).join(", ")}`);const t=e.authMethods.find(n=>/oauth|browser/i.test(n.id))??e.authMethods[0];if(!t)throw e;const s=this.currentAibotSessionId??"";this.callbacks.sendAuthNotification(s,`Authentication required (${t.id}). Initiating auth flow...`);for(const[n,a]of this.sessionBindings)this.callbacks.sendUpdateBindingCard(n,"failed",a);const i=await this.captureAuthUrl();i&&this.callbacks.sendAuthNotification(s,`Please open this URL to authenticate:
|
|
11
|
+
${i}
|
|
12
|
+
|
|
13
|
+
Waiting for authentication to complete...`);try{await this.acpClient.authenticate(t.id),r.info("acp-adapter","Authentication successful"),this.callbacks.sendAuthNotification(s,"Authentication successful. Resuming...");const n=this.internalApi?[{name:"grix-connector-tools",command:process.execPath,args:[p.resolve(v,"../../mcp/acp-mcp-server.js"),"--api-url",this.internalApi.url],env:{GRIX_CONNECTOR_INTERNAL_API:this.internalApi.url}}]:void 0;await this.acpClient.connect({transport:this.agentProcess.transport,initialMode:this.acpInitialMode,cwd:this.resolveCwd(),mcpServers:n}),r.info("acp-adapter",`ACP session ready after auth: ${this.acpClient.sessionId}`),this.emit("acpSessionReady",this.acpClient.sessionId);for(const[a,o]of this.sessionBindings)this.callbacks.sendUpdateBindingCard(a,"ready",o,this.buildToolbarContext("binding_ready",o))}catch(n){throw r.error("acp-adapter",`Auth retry failed: ${n}`),n}}captureAuthUrl(){return new Promise(e=>{if(!this.agentProcess){e(null);return}const t=/https?:\/\/[^\s"')\]]+/;let s=!1;const i=n=>{if(s)return;const o=n.toString().replace(/\x1b\[[0-9;]*m/g,"").match(t);o&&(s=!0,this.agentProcess.removeListener("stderr",i),e(o[0]))};this.agentProcess.on("stderr",i),setTimeout(()=>{s||(s=!0,this.agentProcess.removeListener("stderr",i),e(null))},3e4)})}handleAcpEvent(e){if(e.type===m.PermissionRequest){this.activeRun&&this.resetIdleTimer(this.activeRun),this.handlePermissionRequest(e);return}const t=this.activeRun;if(t)switch(this.resetIdleTimer(t),t.responded||(t.responded=!0),e.type){case m.Text:{if(e.content){const s=t.quotedStream.consume(e.content);s.quotedMessageId&&(t.quotedMessageId=s.quotedMessageId),s.deltaContent&&this.appendToStream(t,s.deltaContent)}break}case m.ToolUse:{t.awaitingToolResult=!0,e.toolName&&(this.emitRawEventEnvelope(t,{type:"tool_use",payload:{tool_name:e.toolName,tool_input:e.toolInput??""}})||this.callbacks.sendToolUse(t.eventId,t.sessionId,e.toolName,e.toolInput??""));break}case m.ToolResult:{t.awaitingToolResult=!1,e.content&&(this.emitRawEventEnvelope(t,{type:"tool_result",payload:{tool_name:e.toolName??"",content:e.content}})||this.callbacks.sendToolResult(t.eventId,t.sessionId,e.toolName??"",e.content));break}case m.Thinking:{e.content&&(this.emitRawEventEnvelope(t,{type:"thinking",payload:{content:e.content}})||this.callbacks.sendThinking(t.eventId,t.sessionId,e.content));break}case m.Error:{this.emitRawEventEnvelope(t,{type:"error",payload:{message:String(e.error??"unknown error")}}),r.error("acp-adapter",`ACP error: ${e.error}`);break}case m.Result:{this.emitRawEventEnvelope(t,{type:"result",payload:{done:e.done??!0}});const s=t.quotedStream.flush();s.deltaContent&&this.appendToStream(t,s.deltaContent),s.quotedMessageId&&(t.quotedMessageId=s.quotedMessageId),this.flushStream(),this.finishRun("responded");break}}}handlePermissionRequest(e){const t=e.permissionRequest;if(!t||!e.requestId||!this.acpClient)return;if(this.approvalMode==="yolo"||this.approvalMode==="autoEdit"){r.info("acp-adapter",`Auto-approving (${this.approvalMode}): ${t.toolName}`),this.acpClient.respondPermission(t.requestId,{behavior:"allow"}).catch(()=>{});return}const s=t.toolCallId;this.pendingApprovals.set(s,e.requestId);const i=this.activeRun;i?(i.idleTimer&&(clearTimeout(i.idleTimer),i.idleTimer=null),i.composingTimer&&(clearInterval(i.composingTimer),i.composingTimer=null),this.emitRawEventEnvelope(i,{type:"permission_request",payload:{request_id:t.requestId,tool_call_id:s,tool_name:t.toolName,tool_title:t.toolTitle,options:t.options}})||this.callbacks.sendPermissionCard({eventId:i.eventId,sessionId:i.sessionId,toolCallId:s,toolName:t.toolName,toolTitle:t.toolTitle,options:t.options})):(r.info("acp-adapter",`Permission request without active run, auto-approving: ${t.toolName}`),this.acpClient.respondPermission(e.requestId,{behavior:"allow"}),this.pendingApprovals.delete(s))}emitRawEventEnvelope(e,t){return!e||!this.rawTransport||!this.callbacks.sendRawEventEnvelope?!1:(this.callbacks.sendRawEventEnvelope(e.eventId,e.sessionId,{type:t.type,payload:t.payload,seq:++this.rawEventSeq,at:new Date().toISOString()}),!0)}resetIdleTimer(e){e.idleTimer&&clearTimeout(e.idleTimer);const t=e.awaitingToolResult?N:j;e.idleTimer=setTimeout(()=>{this.activeRun?.eventId===e.eventId&&(r.error("acp-adapter",`Agent idle for ${t/1e3}s, declaring stuck: ${e.eventId}`),this.finishRun("failed",`agent idle for ${t/1e3}s`),this.emit("stuck"))},t),e.awaitingToolResult&&!e.composingTimer?e.composingTimer=setInterval(()=>{this.activeRun?.eventId===e.eventId?this.callbacks.sendSessionComposing(e.sessionId,!0):(clearInterval(e.composingTimer),e.composingTimer=null)},25e3):!e.awaitingToolResult&&e.composingTimer&&(clearInterval(e.composingTimer),e.composingTimer=null)}appendToStream(e,t){e.buffer+=t,e.flushTimer||(e.flushTimer=setTimeout(()=>this.flushStream(),q))}emitSegmentedStream(e,t){if(!t)return;const s=e.markdownSegmenter.push(t);for(const i of s)i.text&&(this.callbacks.sendStreamChunk(e.eventId,e.sessionId,i.text,++e.chunkSeq,!1,e.currentClientMsgId),i.closeAfter&&(this.callbacks.sendStreamChunk(e.eventId,e.sessionId,"",++e.chunkSeq,!0,e.currentClientMsgId),e.currentSegmentIndex+=1,e.currentClientMsgId=`${e.clientMsgIdBase}_seg_${e.currentSegmentIndex}`,r.info("acp-adapter",`stream segment rollover event=${e.eventId} segment=${e.currentSegmentIndex} reason=${i.reason??"threshold"}`)))}flushStream(){const e=this.activeRun;if(!e||!e.buffer)return;e.flushTimer&&(clearTimeout(e.flushTimer),e.flushTimer=null);const t=e.buffer;e.buffer="",this.emitSegmentedStream(e,t)}finishRun(e,t){const s=this.activeRun;if(!s)return;this.activeRun=null,this.callbacks.sendSessionComposing(s.sessionId,!1),s.flushTimer&&(clearTimeout(s.flushTimer),s.flushTimer=null),s.idleTimer&&(clearTimeout(s.idleTimer),s.idleTimer=null),s.composingTimer&&(clearInterval(s.composingTimer),s.composingTimer=null);const i=s.quotedStream.flush();i.deltaContent&&(s.buffer+=i.deltaContent),i.quotedMessageId&&(s.quotedMessageId=i.quotedMessageId),s.buffer&&(this.emitSegmentedStream(s,s.buffer),s.buffer=""),t&&this.callbacks.sendRunError(s.eventId,s.sessionId,t);const n=++s.chunkSeq;this.callbacks.sendFinalStreamChunkReliable?this.callbacks.sendFinalStreamChunkReliable(s.eventId,s.sessionId,n,s.currentClientMsgId).then(()=>{s.silent||this.callbacks.sendEventResult(s.eventId,e,t),this.persistEventResult(s,e,t)}).catch(a=>{r.error("acp-adapter",`finalStreamChunk ACK failed event=${s.eventId}: ${a}`),this.callbacks.sendStreamChunk(s.eventId,s.sessionId,"",n,!0,s.currentClientMsgId),s.silent||this.callbacks.sendEventResult(s.eventId,e,t),this.persistEventResult(s,e,t)}):(this.callbacks.sendStreamChunk(s.eventId,s.sessionId,"",n,!0,s.currentClientMsgId),s.silent||this.callbacks.sendEventResult(s.eventId,e,t),this.persistEventResult(s,e,t))}persistEventResult(e,t,s){this.eventResults&&!e.silent&&this.eventResults.set({sessionId:e.sessionId,eventId:e.eventId,status:t,msg:s,updatedAt:Date.now()})}}class L extends S{adapterSessionId;constructor(e){super(),this.adapterSessionId=e}emitDone(e){this.emit("done",e)}emitError(e){if(this.listenerCount("error")===0){r.warn("acp-adapter",`Prompt handle error (no listeners): ${e.message}`);return}this.emit("error",e)}async cancel(){}}export{ie as AcpAdapter};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{AcpAdapter as e}from"./acp-adapter.js";export{e as AcpAdapter};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createReadStream as T,readFileSync as y,readdirSync as w,existsSync as j}from"node:fs";import{createInterface as I}from"node:readline";import p from"node:path";import g from"node:os";import{log as f}from"../../core/log/index.js";function m(){return{inputTokens:0,outputTokens:0,cacheReadInputTokens:0,cacheCreationInputTokens:0}}function d(e,n){e.inputTokens+=n.inputTokens,e.outputTokens+=n.outputTokens,e.cacheReadInputTokens+=n.cacheReadInputTokens,e.cacheCreationInputTokens+=n.cacheCreationInputTokens}function C(e,n){const s=p.join(g.homedir(),".gemini","tmp"),r=p.basename(n),o=e.slice(0,8),u=p.join(s,r,"chats");try{const l=w(u);for(const c of[".jsonl",".json"]){const a=l.find(t=>t.startsWith("session-")&&t.includes(o)&&t.endsWith(c));if(a)return p.join(u,a)}}catch{}return null}function h(e){if(e.type!=="gemini"||!e.tokens)return null;const n=e.tokens;return{model:e.model??"unknown",usage:{inputTokens:n.input??0,outputTokens:n.output??0,cacheReadInputTokens:n.cached??0,cacheCreationInputTokens:0}}}function M(e,n){const s=p.join(g.homedir(),".qwen","projects"),r=p.join(s,n.replace(/[/\\]/g,"-")),o=p.join(r,"chats",`${e}.jsonl`);return j(o)?o:null}function S(e){if(e.type!=="assistant"||!e.usageMetadata)return null;const n=e.usageMetadata;return{model:e.model??"unknown",usage:{inputTokens:n.promptTokenCount??0,outputTokens:n.candidatesTokenCount??0,cacheReadInputTokens:n.cachedContentTokenCount??0,cacheCreationInputTokens:0}}}async function k(e,n){const s=new Map,r=m();let o=0;const u=I({input:T(e,"utf8"),crlfDelay:1/0});for await(const c of u)if(c.trim())try{const a=JSON.parse(c),t=n(a);if(!t)continue;o++,d(r,t.usage);let i=s.get(t.model);i||(i={turns:0,usage:m()},s.set(t.model,i)),i.turns++,d(i.usage,t.usage)}catch{}return o===0?null:{models:[...s.entries()].map(([c,a])=>({model:c,turns:a.turns,total:a.usage})),total:r,turns:o}}function $(e){const n=y(e,"utf8"),s=JSON.parse(n),r=Array.isArray(s)?s:s.messages??[],o=new Map,u=m();let l=0;for(const a of r){const t=h(a);if(!t)continue;l++,d(u,t.usage);let i=o.get(t.model);i||(i={turns:0,usage:m()},o.set(t.model,i)),i.turns++,d(i.usage,t.usage)}return l===0?null:{models:[...o.entries()].map(([a,t])=>({model:a,turns:t.turns,total:t.usage})),total:u,turns:l}}async function Q(e,n){const s=C(e,n);if(s){f.info("acp-usage-parser",`Parsing Gemini session from ${s}`);try{return s.endsWith(".jsonl")?await k(s,h):$(s)}catch(o){f.error("acp-usage-parser",`Gemini parse failed: ${o}`)}}const r=M(e,n);if(r){f.info("acp-usage-parser",`Parsing Qwen session from ${r}`);try{return await k(r,S)}catch(o){f.error("acp-usage-parser",`Qwen parse failed: ${o}`)}}return f.info("acp-usage-parser",`No session file found for ${e} in ${n}`),null}export{Q as parseAcpSessionUsage};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{watch as o}from"node:fs";class l{store;onActivity;onStop;onStopFailure;onCompactResult;onConfigChange;onPermissionRequest;debounceMs;pollIntervalMs;watcher=null;pollTimer=null;debounceTimer=null;lastObservedAt=0;currentActivity=null;constructor(t){this.store=t.hookSignalStore,this.onActivity=t.onActivity,this.onStop=t.onStop,this.onStopFailure=t.onStopFailure,this.onCompactResult=t.onCompactResult,this.onConfigChange=t.onConfigChange,this.onPermissionRequest=t.onPermissionRequest,this.debounceMs=t.debounceMs??200,this.pollIntervalMs=t.watchIntervalMs??500}start(){this.stop(),this.lastObservedAt=Date.now();try{this.watcher=o(this.store.filePath,()=>{this.onFileChange().catch(()=>{})}),this.watcher.on("error",()=>{})}catch{}this.pollTimer=setInterval(()=>{this.onFileChange().catch(()=>{})},this.pollIntervalMs)}stop(){this.watcher&&(this.watcher.close(),this.watcher=null),this.pollTimer&&(clearInterval(this.pollTimer),this.pollTimer=null),this.debounceTimer&&(clearTimeout(this.debounceTimer),this.debounceTimer=null)}async onFileChange(){const e=(await this.store.readState()).recent_events.filter(i=>i.event_at>this.lastObservedAt).sort((i,s)=>i.event_at-s.event_at);if(e.length!==0){this.lastObservedAt=e[e.length-1].event_at;for(const i of e)this.processEvent(i)}}processEvent(t){switch(t.hook_event_name){case"Stop":this.clearActivity(),this.onStop?.();break;case"StopFailure":this.clearActivity(),this.onStopFailure?.();break;case"PreToolUse":case"PostToolUse":case"PostToolUseFailure":{const e={event_name:t.hook_event_name,tool_name:t.detail};t.tool_input&&(e.tool_input=t.tool_input),this.applyActivity(e);break}case"PostCompact":this.onCompactResult?.(t.detail,t.tool_input??"");break;case"ConfigChange":this.onConfigChange?.(t.detail);break;case"PermissionRequest":this.onPermissionRequest?.(t.detail,t.tool_input);break;default:break}}applyActivity(t){this.currentActivity=t,this.onActivity(t)}clearActivity(){this.debounceTimer&&(clearTimeout(this.debounceTimer),this.debounceTimer=null),this.currentActivity=null,this.onActivity(null)}}export{l as ActivityStatusManager};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{formatInboundMessageReferenceText as r,resolveOutboundQuotedMessageId as s}from"../../core/protocol/message-reference.js";export{r as formatInboundMessageReferenceText,s as resolveOutboundQuotedMessageId};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import{spawn as U,spawnSync as fe,execSync as H}from"node:child_process";import{randomUUID as O}from"node:crypto";import{killProcessGroup as M}from"../../core/runtime/spawn.js";import{readdirSync as me,readFileSync as Q,rmSync as ve,statSync as B,existsSync as L,watchFile as ge,unwatchFile as Z}from"node:fs";import{mkdir as $,readFile as C,rm as ee,stat as te,writeFile as P}from"node:fs/promises";import{join as h,resolve as Ee}from"node:path";import{homedir as R,tmpdir as _e}from"node:os";import G from"node:net";import{EventEmitter as ie}from"node:events";import{fileURLToPath as Se}from"node:url";let q=null;if(process.platform==="win32")try{q=await import("node-pty")}catch{q=null}import{resolveRuntimePaths as w}from"../../core/config/index.js";import{SESSION_MODE_IDS as D}from"./protocol-contract.js";import{log as o}from"../../core/log/index.js";import{ActivityStatusManager as we}from"./activity-status-manager.js";import{HookSignalStore as ye}from"../../core/hooks/hook-signal-store.js";import{QuestionStore as se}from"../../core/persistence/question-store.js";import{PermissionStore as F}from"../../core/persistence/permission-store.js";import{InternalApiServer as Ce}from"../../core/mcp/internal-api-server.js";import{executeEventTool as ke,isEventTool as Pe}from"../../core/mcp/event-tool-executor.js";import{validateToolArgs as Ie}from"../../core/mcp/tool-schemas.js";import{ACCESS_CONTROL_ACTION_MAP as Ae,isGrixInternalToolName as be,normalizeEventToolArgs as Te}from"../../core/mcp/tools.js";import{scanSkills as $e}from"./skill-scanner.js";import{extractLastAssistantText as Re}from"./usage-parser.js";const x="grix";function xe(d){if(!Array.isArray(d))return;const e=d.map(i=>{if(typeof i=="string")return i.trim();if(!i||typeof i!="object")return"";const t=i,s=String(t.label??"").trim();if(s)return s;const n=String(t.value??"").trim();if(n)return n;const r=String(t.text??"").trim();return r||""}).filter(i=>i.length>0);return e.length>0?e:void 0}function ne(d){let e;try{e=JSON.parse(d)}catch{return null}const i=e.questions;if(!Array.isArray(i)||i.length===0)return null;const t=[];for(let s=0;s<i.length;s++){const n=i[s];if(!n||typeof n!="object")continue;const r=n,a=`Question ${s+1}`,l=String(r.header??r.question??a).trim()||a,u=String(r.prompt??r.question??"").trim();if(!u)continue;const c=xe(r.options),p=r.multiSelect===!0||r.multi_select===!0;t.push({header:l,prompt:u,...c?{options:c}:{},...p?{multi_select:!0}:{}})}return t.length>0?t:null}function Me(d){if(!d||typeof d!="string")return null;try{const e=JSON.parse(d);return(typeof e.plan=="string"?e.plan.trim():"")||null}catch{return null}}const De=[/Please run \/login/,/API Error:\s*401/,/authentication_error/,/OAuth token has expired/],Le=[/You're out of extra usage/i,/Stop and wait for limit to reset/i,/Add funds to continue with extra usage/i],re=300*1e3,qe=1800*1e3,z=60*1e3,J=600*1e3,Fe=12e4,je=3e4,Ne=3e4,Ue=3e3,j=1e3,oe=45e3,He=18e3,W=8192,ae=50;let le=!1;const V=new Set;async function Oe(d=[]){const e=new Set(d.filter(i=>Number.isInteger(i)&&i>0));for(let i=0;i<20;i++){const t=await new Promise((s,n)=>{const r=G.createServer();r.once("error",n),r.listen(0,"127.0.0.1",()=>{const a=r.address(),l=typeof a=="object"&&a?a.port:0;r.close(u=>{if(u){n(u);return}s(l)})})});if(t>0&&!e.has(t)&&!V.has(t))return V.add(t),t}throw new Error("\u65E0\u6CD5\u5206\u914D MCP \u901A\u77E5\u7AEF\u53E3")}function Qe(d){d>0&&V.delete(d)}const ce=["You are connected to a chat via the grix-claude MCP server.",'Messages arrive as <channel source="grix-claude" chat_id="..." event_id="..." message_id="..." user_id="...">text</channel>.',"IMPORTANT: You MUST use the reply tool to send any response to the user.","Your plain text output is NOT delivered to the chat \u2014 only the reply tool delivers messages.","Always call the reply tool with chat_id, event_id, and your response text.","If you intentionally do not want to send a visible reply, you must call the complete tool with event_id and a final status."].join(" ");function de(d){return String(d??"").trim().toLowerCase()===D.approval?D.approval:D.fullAuto}class he extends ie{type="claude";config;bridgeCallbacks;mcpServerProcess=null;internalApi=null;claudeProcess=null;claudePty=null;spawnPromise=null;lifecycleVersion=0;sessionId="";alive=!1;stopped=!1;activeEvent=null;activeEventIdleTimer=null;activeEventHardTimer=null;queuedEvents=[];queuedEventIds=new Set;stopHookBarrierSessionId=null;stopHookBarrierTimer=null;composingSessionId=null;composingTimer=null;mcpServerReady=!1;mcpStartupFailureHandled=!1;mcpChannelBroken=!1;startupChannelListening=!1;startupChannelListeningAt=0;pendingMcpFailureTimer=null;sessionIdConflictDetected=!1;sessionIdConflictRetriedEventIds=new Set;runtimeResolver=null;activityManager=null;lastPreToolInput="";deferredModelId=null;claudeChildPid=0;claudeCliSessionId="";claudeSessionCwd="";claudeMcpConfigPath="";sessionState=null;authFailureUntil=0;usageLimitUntil=0;completedEventIds=new Map;pendingQuestion=new Map;pendingPermissions=new Map;lastClearedEvent=null;constructor(e,i){super(),this.config=e,this.bridgeCallbacks=i;const s=(e.options??{}).sessionRuntimeResolver;typeof s=="function"&&(this.runtimeResolver=s)}async start(){this.lifecycleVersion+=1,this.stopped=!1,this.alive=!0,this.internalApi=new Ce,this.internalApi.setInvokeHandler(async(e,i,t)=>this.handleInternalInvoke(e,i,t)),this.internalApi.setStatusLineHandler(async e=>{this.handleStatusLineUpdate(e)}),await this.internalApi.start(0),this.internalApiPort=parseInt(new URL(this.internalApi.baseUrl).port,10),this.notifyPort=0,o.info("claude-adapter",`Adapter started (stdio MCP mode, internal API at ${this.internalApi.baseUrl})`)}async stop(){if(o.info("claude-adapter",`Stopping adapter (sessionId=${this.sessionId}, alive=${this.alive}, activeEvent=${this.activeEvent?.eventId??"none"})`),this.lifecycleVersion+=1,this.stopped=!0,this.alive=!1,this.activeEvent&&(this.bridgeCallbacks.sendEventResult(this.activeEvent.eventId,"canceled","adapter stopped"),this.clearActiveEvent()),this.stopComposing(),this.rejectQueuedEvents("adapter stopped","canceled"),this.clearStopHookBarrier(),this.mcpChannelBroken=!1,this.startupChannelListening=!1,this.startupChannelListeningAt=0,this.deferredModelId=null,this.clearPendingMcpFailureTimer(),this.ptyAutoConfirmTimer&&(clearInterval(this.ptyAutoConfirmTimer),this.ptyAutoConfirmTimer=null),this.pendingPermissions.size>0){const s=w(),n=new F(s.permissionRequestsDir);for(const[r]of this.pendingPermissions)n.resolveRequest(r,"deny").catch(()=>{});this.pendingPermissions.clear()}if(this.activityManager&&(this.activityManager.stop(),this.activityManager=null),this.sessionId){const s=this.resolveHookSignalsPath();ee(s).catch(()=>{})}this.stopMcpServer();const e=this.claudeChildPid;this.claudeChildPid=0;const i=this.claudeProcess,t=this.claudePty;if(this.claudeProcess=null,this.claudePty=null,this.spawnPromise=null,t)try{t.kill()}catch{}if(e>0)try{process.kill(e,"SIGTERM")}catch{}if(i?.pid&&(M(i,"SIGTERM"),!await Promise.race([new Promise(n=>{i.once("exit",()=>n(!0))}),new Promise(n=>{setTimeout(()=>n(!1),5e3)})]))){if(e>0)try{process.kill(e,"SIGKILL")}catch{}M(i,"SIGKILL")}if(e>0){const s=Date.now();for(;Date.now()-s<5e3;)try{process.kill(e,0),await new Promise(n=>setTimeout(n,100))}catch{break}try{process.kill(e,"SIGKILL")}catch{}}this.releaseNotifyPortReservation(),this.claudeCliSessionId&&N(this.claudeCliSessionId),this.internalApi&&(await this.internalApi.stop(),this.internalApi=null)}isAlive(){return this.alive}async createSession(e){return`claude-session-${Date.now()}`}async resumeSession(e,i){}async destroySession(e){}sendPrompt(e){const i=new Be(e.adapterSessionId),t={event_id:e.adapterSessionId,session_id:e.adapterSessionId,content:e.text,context_messages_json:e.contextMessages?JSON.stringify(e.contextMessages):void 0};return this.deliverInboundEvent(t),this.once(`reply:${e.adapterSessionId}`,s=>{i.emitDone(s.status==="completed"?{status:"completed"}:{status:"failed",error:"failed"})}),i}async cancel(e){}setPermissionHandler(e){}async ping(e){return this.stopped?!1:!this.claudeProcess&&!this.claudePty?this.alive&&this.internalApi!==null:this.alive&&this.mcpServerReady&&(this.startupChannelListening||process.platform==="win32")}getStatus(){return{alive:this.alive,busy:this.activeEvent!==null,sessions:0,details:this.sessionState?{sessionState:this.sessionState}:void 0}}getActiveEventIds(){return this.activeEvent?[this.activeEvent.eventId]:[]}clearActiveEventForShutdown(){this.activeEvent&&this.clearActiveEvent()}getSessionState(){return this.sessionState}getMcpConfig(){return null}getSupportedCommands(){return[{name:"compact",description:"\u538B\u7F29\u4E0A\u4E0B\u6587",args:"[instructions]"},{name:"clear",description:"\u6E05\u9664\u5BF9\u8BDD"},{name:"model",description:"\u5207\u6362\u6A21\u578B",args:"<model-id>"},{name:"cost",description:"\u663E\u793A\u8D39\u7528"},{name:"rewind",description:"\u56DE\u9000\u5BF9\u8BDD"},{name:"memory",description:"\u8BB0\u5FC6\u7BA1\u7406"},{name:"doctor",description:"\u8BCA\u65AD"},{name:"status",description:"\u72B6\u6001\u663E\u793A"},{name:"skills",description:"\u83B7\u53D6 skills \u6E05\u5355"}]}async execCommand(e,i,t){if(e==="skills")try{const n=$e({mode:"claude",projectDir:this.claudeSessionCwd}),r=n.map(a=>{const l=a.trigger?` (${a.trigger})`:"",u=a.pluginName?` [plugin:${a.pluginName}]`:` [${a.source}]`;return`- ${a.name}${l}${u}: ${a.description}`});return{status:"ok",message:r.length>0?r.join(`
|
|
2
|
+
`):"No skills found",data:n}}catch(n){return{status:"failed",message:`Failed to scan skills: ${n instanceof Error?n.message:n}`}}if(!this.claudeProcess&&!this.claudePty)return{status:"failed",message:"Claude process is not running"};if(this.getStatus().busy)return{status:"failed",message:"Claude is busy, try again later"};const s=i?`/${e} ${i}`:`/${e}`;try{return this.claudePty?this.claudePty.write(`${s}\r`):this.claudeProcess?.stdin?.write(`${s}\r`),{status:"ok",message:`Sent: ${s}`}}catch(n){return{status:"failed",message:`Failed to send: ${n instanceof Error?n.message:n}`}}}handleStatusLineUpdate(e){try{const i=e.context_window,t=e.cost,s=e.model,n=e.rate_limits??e.rateLimits,r=i?.current_usage;if(this.sessionState={contextWindow:{usedPercentage:i?.used_percentage!=null?g(i.used_percentage):null,remainingPercentage:i?.remaining_percentage!=null?g(i.remaining_percentage):null,totalInputTokens:g(i?.total_input_tokens),totalOutputTokens:g(i?.total_output_tokens),contextWindowSize:g(i?.context_window_size)||2e5,currentUsage:r?{inputTokens:g(r.input_tokens),outputTokens:g(r.output_tokens),cacheCreationInputTokens:g(r.cache_creation_input_tokens),cacheReadInputTokens:g(r.cache_read_input_tokens)}:null},cost:{totalCostUsd:g(t?.total_cost_usd),totalDurationMs:g(t?.total_duration_ms),totalApiDurationMs:g(t?.total_api_duration_ms),totalLinesAdded:g(t?.total_lines_added),totalLinesRemoved:g(t?.total_lines_removed)},rateLimits:this.parseStatusRateLimits(n),model:{id:String(s?.id??""),displayName:String(s?.display_name??"")},fastMode:e.fast_mode===!0,effort:e.effort!=null?String(e.effort):void 0,thinkingEnabled:e.thinking_enabled===!0,exceeds200kTokens:e.exceeds_200k_tokens===!0,version:String(e.version??""),sampledAt:Date.now()},n){const a=this.sessionState.rateLimits?.fiveHour,l=this.sessionState.rateLimits?.sevenDay;o.info("claude-adapter",`[rate-limits] statusLine parsed: fiveHour=${a?`${a.usedPercentage}% resetsAt=${a.resetsAt}`:"n/a"} sevenDay=${l?`${l.usedPercentage}% resetsAt=${l.resetsAt}`:"n/a"}`)}else o.debug("claude-adapter","[rate-limits] statusLine: no rate_limits in payload");try{this.bridgeCallbacks.onStatusLineUpdated?.(this.sessionState)}catch(a){o.warn("claude-adapter",`onStatusLineUpdated callback error: ${a instanceof Error?a.message:a}`)}}catch(i){o.warn("claude-adapter",`Failed to parse statusLine payload: ${i instanceof Error?i.message:i}`)}}parseStatusRateLimits(e){if(!e)return;const i=n=>{if(!n||typeof n!="object")return;const r=n,a=g(r.used_percentage??r.usedPercent),l=g(r.resets_at??r.resetsAt);if(!(a<=0&&l<=0))return{usedPercentage:a,resetsAt:l}},t=i(e.five_hour??e.fiveHour),s=i(e.seven_day??e.sevenDay);if(!(!t&&!s))return{...t?{fiveHour:t}:{},...s?{sevenDay:s}:{}}}deliverInboundEvent(e){if(this.sessionId||(this.sessionId=e.session_id),this.pruneCompletedEvents(),this.completedEventIds.has(e.event_id)){o.info("claude-adapter",`Event ${e.event_id} rejected: duplicate`),this.bridgeCallbacks.sendEventResult(e.event_id,"responded","duplicate event");return}if(Date.now()<this.authFailureUntil){o.info("claude-adapter",`Event ${e.event_id} rejected: auth cooldown`),this.bridgeCallbacks.sendEventResult(e.event_id,"failed","Claude authentication failed \u2014 please re-login");return}if(Date.now()<this.usageLimitUntil){o.info("claude-adapter",`Event ${e.event_id} rejected: usage limit cooldown`),this.bridgeCallbacks.sendEventResult(e.event_id,"failed","Claude usage limit reached \u2014 waiting for reset");return}if(this.activeEvent){if(this.activeEvent.eventId===e.event_id){o.info("claude-adapter",`Event ${e.event_id} ignored: same as active event`);return}if(this.queuedEventIds.has(e.event_id))return;if(this.queuedEvents.length>=ae){o.warn("claude-adapter",`Event ${e.event_id} rejected: queue full (size=${this.queuedEvents.length})`),this.bridgeCallbacks.sendEventResult(e.event_id,"failed","agent queue full");return}this.queuedEvents.push(e),this.queuedEventIds.add(e.event_id),o.info("claude-adapter",`Queue event ${e.event_id} (active=${this.activeEvent.eventId}, size=${this.queuedEvents.length})`);return}if(this.stopHookBarrierSessionId===e.session_id){if(this.queuedEventIds.has(e.event_id))return;if(this.queuedEvents.length>=ae){o.warn("claude-adapter",`Event ${e.event_id} rejected: queue full during barrier (size=${this.queuedEvents.length})`),this.bridgeCallbacks.sendEventResult(e.event_id,"failed","agent queue full");return}this.queuedEvents.push(e),this.queuedEventIds.add(e.event_id),o.info("claude-adapter",`Queue event ${e.event_id} (barrier session=${e.session_id})`);return}this.activeEvent={eventId:e.event_id,sessionId:e.session_id,rawEvent:e},this.lastClearedEvent=null,this.markActiveEventActivity(e.event_id,e.session_id),this.resetActiveEventHardTimer(e.event_id),this.ensureClaudeAndPushEvent(e)}deliverStopEvent(e,i){if(this.activeEvent?.eventId!==e)return;const t=this.activeEvent.sessionId;o.info("claude-adapter",`Stop requested for active event=${e} \u2014 killing Claude process`),this.mcpServerReady&&this.pushNotification("notifications/event_stop",{event_id:e,session_id:t,stop_id:O()}),this.bridgeCallbacks.sendEventResult(e,"canceled","stopped by user"),this.clearActiveEvent(),this.killClaudeProcess("stop")}async handleLocalAction(e){if(!this.mcpServerReady)return{handled:!1,kind:""};const i=String(e.action_type??"");return i==="claude_interaction_reply"?this.handleQuestionReply(e):i==="exec_approve"||i==="exec_reject"?this.handlePermissionApproval(e):(this.pushNotification("notifications/local_action",e),{handled:!0,kind:""})}async handleQuestionReply(e){const i=e.params??{},t=String(i.request_id??"");if(!t)return o.warn("claude-adapter","Question reply missing request_id"),this.bridgeCallbacks.sendLocalActionResult(e.action_id,"failed",void 0,"request_id_required","request_id is required"),{handled:!0,kind:"question_reply_no_id"};const s=w(),n=new se(s.questionRequestsDir),r=i.resolution??{},a=String(r.type??""),l=r;if(a==="action"&&String(r.value??"")==="cancel"){o.info("claude-adapter",`Question cancelled by user: request_id=${t}`),await n.resolveRequest(t,"cancel",void 0,l);const f=this.pendingQuestion.get(t);return f&&this.activeEvent?.eventId===f.eventId&&(this.activeEvent.awaitingUserQuestion=!1),this.pendingQuestion.delete(t),this.bridgeCallbacks.sendLocalActionResult(e.action_id,"ok",{request_id:t,resolution:"cancel"}),{handled:!0,kind:"question_reply_cancel"}}let u="";if(a==="text"?u=String(r.value??""):a==="map"&&(u=(Array.isArray(r.entries)?r.entries:[]).map(f=>f.value).join(", ")),!u){o.warn("claude-adapter",`Empty answer for question reply request_id=${t}`),await n.resolveRequest(t,"cancel",void 0,l);const p=this.pendingQuestion.get(t);return p&&this.activeEvent?.eventId===p.eventId&&(this.activeEvent.awaitingUserQuestion=!1),this.pendingQuestion.delete(t),this.bridgeCallbacks.sendLocalActionResult(e.action_id,"ok",{request_id:t,resolution:"cancel"}),{handled:!0,kind:"question_reply_empty"}}o.info("claude-adapter",`Resolving question in store: request_id=${t} answer=${u.slice(0,80)}`),await n.resolveRequest(t,"answer",u,l);const c=this.pendingQuestion.get(t);return c&&this.activeEvent?.eventId===c.eventId&&(this.activeEvent.awaitingUserQuestion=!1),this.pendingQuestion.delete(t),this.bridgeCallbacks.sendLocalActionResult(e.action_id,"ok",{request_id:t,resolution:"answer"}),{handled:!0,kind:"question_reply"}}async handlePermissionApproval(e){const i=e.params??{},t=String(i.approval_id??i.approval_command_id??i.tool_call_id??""),s=String(e.action_type??"")==="exec_approve";if(!t)return this.bridgeCallbacks.sendLocalActionResult(e.action_id,"failed",void 0,"approval_id_required","approval_id is required"),{handled:!0,kind:"permission_approval"};if(!this.pendingPermissions.get(t))return this.bridgeCallbacks.sendLocalActionResult(e.action_id,"failed",void 0,"approval_not_found",`no pending permission for approval_id: ${t}`),{handled:!0,kind:"permission_approval"};const r=w(),a=new F(r.permissionRequestsDir),l=s?"allow":"deny";return await a.resolveRequest(t,l),this.pendingPermissions.delete(t),this.bridgeCallbacks.sendLocalActionResult(e.action_id,"ok"),o.info("claude-adapter",`Permission ${l}: approvalId=${t}`),{handled:!0,kind:"permission_approval"}}async ensureClaudeAndPushEvent(e,i=0){i===0&&!this.claudeProcess&&this.startComposing(e.session_id,"preparing");try{if(this.mcpChannelBroken){await this.runSingleTurnFallback(e,"mcp channel unavailable");return}if(await this.ensureClaudeProcessReady(),!this.mcpServerReady)throw new Error("MCP stdio server not available");if(await this.waitForNotifyPort(3e4),process.platform==="win32"?await this.waitForWindowsChannelReady(oe):await this.waitForChannelListening(oe),this.activeEvent?.eventId!==e.event_id||this.mcpChannelBroken)return;const s={},n=[["chat_id",e.session_id],["event_id",e.event_id],["event_type","user_chat"],["message_id",e.msg_id],["sender_id",e.sender_id],["user",e.sender_id],["user_id",e.sender_id],["msg_id",e.msg_id],["session_type",e.session_type!=null?String(e.session_type):void 0],["msg_type",e.msg_type!=null?String(e.msg_type):void 0],["quoted_message_id",e.quoted_message_id],["ts",e.created_at!=null?new Date(Number(e.created_at)).toISOString():void 0],["owner_id",e.owner_id],["agent_id",e.agent_id],["attachments_json",e.attachments_json],["context_messages_json",e.context_messages_json]];for(const[r,a]of n)a!=null&&a!==""&&(s[r]=a);this.pushNotification("notifications/claude/channel",{content:e.content??"",meta:s}),o.info("claude-adapter",`Event ${e.event_id} pushed to MCP stdio server`)}catch(s){if(this.stopped){this.activeEvent?.eventId===e.event_id&&(this.bridgeCallbacks.sendEventResult(e.event_id,"canceled","adapter stopped"),this.clearActiveEvent());return}if(s instanceof Error&&(s.message.includes("exited before")||s.message.includes("timeout")||s.message.includes("spawn failed"))&&i<3)return o.warn("claude-adapter",`Spawn failed (attempt ${i+1}/4), retrying: ${s}`),this.claudeProcess=null,this.claudeChildPid=0,this.alive=!1,this.releaseNotifyPortReservation(),this.stopMcpServer(),N(this.claudeCliSessionId),K(this.claudeCliSessionId,[]),await new Promise(r=>setTimeout(r,(i+1)*2e3)),this.ensureClaudeAndPushEvent(e,i+1);if(o.error("claude-adapter",`Failed to deliver event ${e.event_id}: ${s}`),this.shouldFallbackToSingleTurn(s)){o.warn("claude-adapter",`Channel path failed, switching to single-turn fallback: ${s instanceof Error?s.message:String(s)}`);try{await this.runSingleTurnFallback(e,s instanceof Error?s.message:String(s));return}catch(r){o.error("claude-adapter",`Single-turn fallback failed for ${e.event_id}: ${r instanceof Error?r.message:String(r)}`)}}this.activeEvent?.eventId===e.event_id&&(this.bridgeCallbacks.sendEventResult(e.event_id,"failed",s instanceof Error?s.message:String(s)),this.clearActiveEvent())}}shouldFallbackToSingleTurn(e){const i=(e instanceof Error?e.message:String(e)).toLowerCase();return i?i.includes("mcp server startup failed")||i.includes("claude channel listener not ready")||i.includes("channels are not currently available")||i.includes("mcp stdio server not available")||i.includes("notify port not ready"):!1}buildSingleTurnPrompt(e){const i=e.context_messages_json?String(e.context_messages_json):"",t=["Reply to the latest user message directly and concisely."];return i&&(t.push("Conversation context (JSON):"),t.push(i)),t.push("Latest user message:"),t.push(e.content??""),t.join(`
|
|
3
|
+
`)}extractSingleTurnDelta(e){const i=e.trim();if(!i)return"";try{const t=JSON.parse(i),s=String(t.type??"");if(s==="content_block_delta"){const n=t.delta;if(n&&typeof n=="object"){const r=String(n.text??"");if(r)return r}}return s==="result"?String(t.result??""):typeof t.text=="string"&&t.text?t.text:""}catch{return i}}buildClaudeRuntimeEnv(){const e={...process.env,...this.config.env};return delete e.CLAUDECODE,e.CLAUDE_PLUGIN_DATA=w().dataDir,e.GRIX_HOOK_SIGNALS_PATH=this.resolveHookSignalsPath(),this.internalApi&&(e.GRIX_CLAUDE_INTERNAL_API_URL=this.internalApi.baseUrl,e.GRIX_CLAUDE_INTERNAL_API_TOKEN=this.internalApi.token),e}async runSingleTurnFallback(e,i){const t=this.resolveSessionRuntime(),s=this.config.options??{},n=this.config.command||"claude",r=(this.config.args??[]).filter(I=>{const m=String(I).trim().toLowerCase();return m!=="--dangerously-load-development-channels"&&m!=="--session-id"&&m!=="--resume"}),a=String(t.cwd??"").trim(),l=String(t.modelId??"").trim(),u=de(t.modeId);await this.ensureWorkspaceTrust(a),await this.ensureGrowthBookFeatures(a),await this.injectStatusLineSettings(a),await this.ensureStdioMcpServer();const c=this.buildClaudeRuntimeEnv(),p=t.pluginDir??s.pluginDir??await this.ensureClaudePluginDir(),f=this.buildSingleTurnPrompt(e),y=[...r,"-p","--output-format","stream-json","--include-partial-messages","--no-session-persistence","--tools","","--plugin-dir",p,"--strict-mcp-config","--mcp-config",this.claudeMcpConfigPath,"--permission-mode",u===D.fullAuto?"bypassPermissions":"default"];l&&y.push("--model",l),y.push(f),o.warn("claude-adapter",`Running single-turn fallback for event ${e.event_id}: ${i}`),this.bridgeCallbacks.sendEventAck(e.event_id,e.session_id),this.startComposing(e.session_id,"fallback_single_turn"),await new Promise((I,m)=>{const b=U(n,y,{cwd:a||process.cwd(),env:c,stdio:["ignore","pipe","pipe"],detached:!0,windowsHide:!0});let k="",T=0,E=!1;const v=`fallback_${e.event_id}_${Date.now()}`;let _="";b.stdout?.setEncoding("utf8"),b.stdout?.on("data",S=>{_+=S;let A=_.indexOf(`
|
|
4
|
+
`);for(;A>=0;){const pe=_.slice(0,A);_=_.slice(A+1);const X=this.extractSingleTurnDelta(pe);X&&(T+=1,E=!0,this.bridgeCallbacks.sendStreamChunk(e.event_id,e.session_id,X,T,!1,v,e.quoted_message_id)),A=_.indexOf(`
|
|
5
|
+
`)}}),b.stderr?.setEncoding("utf8"),b.stderr?.on("data",S=>{k.length>=W||(k+=S,k.length>W&&(k=k.slice(0,W)))}),b.on("error",S=>m(S)),b.on("close",S=>{if(_.trim()){const A=this.extractSingleTurnDelta(_);A&&(T+=1,E=!0,this.bridgeCallbacks.sendStreamChunk(e.event_id,e.session_id,A,T,!1,v,e.quoted_message_id))}if(S!==0&&!E){const A=k.trim();m(new Error(A||`claude single-turn exited with code ${S}`));return}T+=1,this.bridgeCallbacks.sendStreamChunk(e.event_id,e.session_id,"",T,!0,v,e.quoted_message_id),I()})}),this.activeEvent?.eventId===e.event_id&&(this.bridgeCallbacks.sendEventResult(e.event_id,"responded"),this.clearActiveEvent())}async ensureClaudeProcessReady(){this.claudeProcess||this.claudePty||(this.spawnPromise||(this.spawnPromise=this.spawnClaude().finally(()=>{this.spawnPromise=null})),await this.spawnPromise)}async spawnClaude(){if(this.claudeProcess)return;const e=this.lifecycleVersion,i=()=>{if(this.stopped||e!==this.lifecycleVersion)throw new Error("adapter stopped")},t=this.config.options??{};this.sessionIdConflictDetected=!1;const s=this.resolveSessionRuntime(),n=this.config.command||"claude",r=this.config.args??[],a=this.buildClaudeRuntimeEnv(),l=de(s.modeId),u=String(s.modelId??"").trim(),c=String(s.cwd??"").trim();this.claudeSessionCwd=c;const p=String(s.claudeSessionId??"").trim()||O();this.claudeCliSessionId=p,!s.claudeSessionId&&s.onSessionIdAssigned&&s.onSessionIdAssigned(p);const f=Xe(p,c||void 0);if(f||(K(p,[]),N(p)),this.runtimeResolver&&!c)throw new Error("Claude session binding missing cwd \u2014 run /grix open <working-directory> first");if(!c)throw new Error("Claude runtime cwd is required");await this.ensureWorkspaceTrust(c),await this.ensureGrowthBookFeatures(c),await this.injectStatusLineSettings(c);let y=!1,I="",m=null;try{i(),this.notifyPort=await Oe([this.internalApiPort]),y=!0,i(),o.info("claude-adapter",`Allocated MCP notify port ${this.notifyPort} (internal API ${this.internalApiPort})`),await this.ensureStdioMcpServer(),i();const k=t.pluginDir||await this.ensureClaudePluginDir(),T=f?["--resume",p]:["--session-id",p],E=[...r,"--name",`grix-${this.sessionId}`,...T,"--plugin-dir",k,"--strict-mcp-config","--mcp-config",this.claudeMcpConfigPath];if(u&&(/^claude/i.test(u)?E.push("--model",u):(o.warn("claude-adapter",`Skipping --model ${u}: non-Anthropic models are incompatible with development channels. Using default model so channels can load. Switch models after startup via /model if needed.`),this.deferredModelId=u)),l===D.fullAuto&&E.push("--dangerously-skip-permissions"),E.push("--dangerously-load-development-channels",`server:${x}`),process.platform==="win32"){const v=ce.replace(/</g,"[").replace(/>/g,"]").replace(/"/g,"'"),_=Ve(this.claudeCliSessionId);await Ke(_,v),E.push("--append-system-prompt-file",_)}else E.push("--append-system-prompt",ce);if(process.platform==="win32")if(q){const v="cmd.exe",_=["/c",n,...E];o.info("claude-adapter",`Spawning Claude via PTY on win32: cwd=${c} mode=${l} ${f?"resume":"new"} ${n} ${E.join(" ")}`);const S=q.spawn(v,_,{name:"xterm-256color",cols:120,rows:30,cwd:c,env:a,useConpty:!0,conptyInheritCursor:!1});this.claudePty=S,this.claudeChildPid=S.pid,this.startPtyAutoConfirm(S),m=null}else o.info("claude-adapter",`Spawning Claude via shell on win32 (node-pty unavailable): cwd=${c} mode=${l} ${f?"resume":"new"} ${n} ${E.join(" ")}`),m=U(n,E,{cwd:c,env:a,stdio:["pipe","pipe","pipe"],detached:!0,shell:!0,windowsHide:!0});else{o.info("claude-adapter",`Spawning via expect PTY: cwd=${c} mode=${l} ${f?"resume":"new"} ${n} ${E.join(" ")}`);try{B("/usr/bin/expect")}catch{throw new Error("/usr/bin/expect not found. Install it with: apt install expect / dnf install expect / brew install expect")}const v=h(_e(),`grix-claude-${O()}`);await $(v,{recursive:!0});const{expectPath:_,pidPath:S}=await Ge(v,n,E);I=S,i(),m=U("/usr/bin/expect",[_],{cwd:c,env:a,stdio:["pipe","pipe","pipe"],detached:!0})}if(this.claudeProcess=m,this.clearPendingMcpFailureTimer(),this.mcpStartupFailureHandled=!1,this.mcpChannelBroken=!1,this.startupChannelListening=!1,this.startupChannelListeningAt=0,m&&m.on("error",v=>{o.error("claude-adapter","Claude process spawn error: "+v),this.claudeProcess=null,this.claudeChildPid=0,this.alive=!1,this.releaseNotifyPortReservation(),this.stopMcpServer(),this.clearPendingMcpFailureTimer(),this.activeEvent&&(this.bridgeCallbacks.sendEventResult(this.activeEvent.eventId,"failed","Claude process spawn failed"),this.clearActiveEvent())}),i(),!this.claudePty)if(process.platform==="win32")this.claudeChildPid=m?.pid??0;else{const v=await Je(m,I);if(i(),!Number.isFinite(v)||v<=0)throw new Error("failed to determine spawned Claude pid");this.claudeChildPid=v}if(!this.claudeChildPid||this.claudeChildPid<=0)throw new Error("failed to determine spawned Claude pid")}catch(k){if(m?.pid&&M(m,"SIGTERM"),this.claudePty){try{this.claudePty.kill()}catch{}this.claudePty=null}throw this.claudeProcess===m&&(this.claudeProcess=null),this.claudeChildPid=0,y&&this.releaseNotifyPortReservation(),this.stopMcpServer(),k}const b=this.claudeChildPid;o.info("claude-adapter","Claude child PID: "+b+(this.claudePty?" (via PTY)":"")),this.setupProcessOrPtyHandlers(),this.alive=!0}setupProcessOrPtyHandlers(){this.claudeProcess&&(this.claudeProcess.on("exit",(e,i)=>{if(o.info("claude-adapter",`Claude process exited (code=${e}, signal=${i})`),this.claudeProcess=null,this.claudeChildPid=0,this.alive=!1,this.releaseNotifyPortReservation(),this.stopMcpServer(),this.clearPendingMcpFailureTimer(),!this.tryRecoverSessionIdConflict()){if(this.activeEvent&&!this.stopped){const t=this.activeEvent.replied?"responded":"failed",s=this.activeEvent.replied?void 0:"Claude process exited";o.error("claude-adapter",`Claude process exited with active event ${this.activeEvent.eventId}, replied=${!!this.activeEvent.replied}, sending ${t}`),this.bridgeCallbacks.sendEventResult(this.activeEvent.eventId,t,s),this.clearActiveEvent()}this.stopped||this.emit("exit",e)}}),this.claudeProcess.stdout?.on("data",e=>{this.handleClaudeOutput(e.toString())}),this.claudeProcess.stderr?.on("data",e=>{const i=e.toString().trim();i&&o.info("claude-adapter",`[claude stderr] ${i}`),this.checkFailurePatterns(i)})),this.claudePty&&(this.claudePty.onExit(({exitCode:e,signal:i})=>{if(o.info("claude-adapter",`Claude PTY process exited (code=${e}, signal=${i})`),this.claudePty=null,this.claudeChildPid=0,this.alive=!1,this.releaseNotifyPortReservation(),this.stopMcpServer(),this.clearPendingMcpFailureTimer(),!this.tryRecoverSessionIdConflict()){if(this.activeEvent&&!this.stopped){const t=this.activeEvent.replied?"responded":"failed",s=this.activeEvent.replied?void 0:"Claude process exited";o.error("claude-adapter",`Claude PTY exited with active event ${this.activeEvent.eventId}, replied=${!!this.activeEvent.replied}, sending ${t}`),this.bridgeCallbacks.sendEventResult(this.activeEvent.eventId,t,s),this.clearActiveEvent()}this.stopped||this.emit("exit",e)}}),this.claudePty.onData(e=>{this.handleClaudeOutput(e),this.checkFailurePatterns(e)})),this.watchGrowthBookCache()}handleClaudeOutput(e){const i=e.trim();if(i){const a=i.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g,"").replace(/[\x00-\x1f\x7f-\x9f\u200b-\u200f\u2028-\u202f\ufeff]/g,"").trim();a?a.length<=3||/^[✳✶✻✽✵❋✺✴❈❖✦✧✢◉◎⬥⬦◇◆▸▹►▻→←↑↓⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏·…\d]*still\s+thinking/i.test(a)?o.debug("claude-adapter",`[claude output] ${a}`):o.info("claude-adapter",`[claude output] ${a.slice(0,500)}`):o.debug("claude-adapter",`[claude output] ${i.slice(0,200).replace(/[^\x20-\x7e]/g,l=>`\\x${l.charCodeAt(0).toString(16).padStart(2,"0")}`)}`)}this.activeEvent&&this.markActiveEventActivity(this.activeEvent.eventId,this.activeEvent.sessionId);const t=i.match(/Session ID (\S+) is already in use/i);if(t){const a=t[1];this.sessionIdConflictDetected=!0;const l=[this.claudeProcess?.pid,this.claudeChildPid,this.claudePty?.pid].filter(u=>!!u&&u>0);K(a,l)}const s=i.replace(/\[[0-9;?]*[ -/]*[@-~]/g," ").replace(/[^a-zA-Z]+/g," ").toLowerCase(),n=s.replace(/ /g,"");if(this.claudePty&&!this.startupChannelListening&&/trust.*folder|quick.*safety.*check/.test(n)&&(o.info("claude-adapter","Auto-accepting workspace trust dialog"),this.claudePty.write("1\r")),(/listeningforch\w*nelmessages/.test(n)||/nnelmessagesfrom/.test(n)||/inboundmessageswillbepushedintothissession/.test(n))&&(this.startupChannelListening||(this.startupChannelListeningAt=Date.now(),this.startupChannelListening=!0,this.sendDeferredModelSwitch()),this.clearPendingMcpFailureTimer()),!this.startupChannelListening&&/channels?arenotcurrentlyavailable/.test(n)&&o.warn("claude-adapter",'Claude reports "Channels are not currently available" \u2014 development channels require an Anthropic model (e.g. claude-*). If using a non-Anthropic model, the --model flag was skipped to allow channels to load.'),!this.mcpStartupFailureHandled&&s.includes("mcp server failed")){if(this.startupChannelListening){o.warn("claude-adapter","Claude reported MCP server failed for one channel, but channel listening is active; ignoring non-blocking failure");return}this.pendingMcpFailureTimer||(this.pendingMcpFailureTimer=setTimeout(()=>{this.pendingMcpFailureTimer=null,!(this.startupChannelListening||this.mcpStartupFailureHandled)&&this.markMcpStartupFailure()},Ue))}}checkFailurePatterns(e){/Session ID (\S+) is already in use/i.test(e)&&(this.sessionIdConflictDetected=!0),De.some(i=>i.test(e))&&(o.error("claude-adapter",`Auth failure: ${e}`),this.authFailureUntil=Date.now()+re),Le.some(i=>i.test(e))&&(o.error("claude-adapter",`Usage limit: ${e}`),this.usageLimitUntil=Date.now()+re)}async ensureClaudePluginDir(){const e=this.resolveProjectRoot(),i=h(w().dataDir,"claude-plugin"),t=h(i,".claude-plugin"),s=h(t,"plugin.json"),n=h(e,".claude-plugin","plugin.json");await $(t,{recursive:!0});let r="";try{r=await C(n,"utf8")}catch{r=`${JSON.stringify({name:"grix-connector",version:"0.1.0",description:"Claude Code channel plugin for grix-connector.",license:"MIT"},null,2)}
|
|
6
|
+
`}let a="";try{a=await C(s,"utf8")}catch{}a!==r&&(await P(s,r,"utf8"),o.info("claude-adapter",`Wrote Claude plugin manifest: ${s}`));const l=h(e,"dist","scripts"),u=m=>`"${String(m).replace(/"/g,'\\"')}"`,c=m=>`${u(process.execPath)} ${u(h(l,m))}`,p=`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",command:c("lifecycle-hook.js")}]}],Elicitation:[{hooks:[{type:"command",command:c("elicitation-hook.js")}]}],ElicitationResult:[{matcher:"",hooks:[{type:"command",command:c("lifecycle-hook.js")}]}],UserPromptSubmit:[{hooks:[{type:"command",command:c("user-prompt-submit-hook.js")}]}],PreToolUse:[{matcher:"ExitPlanMode",hooks:[{type:"command",command:c("approve-plan-hook.js")}]},{matcher:"",hooks:[{type:"command",command:c("lifecycle-hook.js")}]}],PostToolUse:[{matcher:"",hooks:[{type:"command",command:c("lifecycle-hook.js")}]}],PostToolUseFailure:[{matcher:"",hooks:[{type:"command",command:c("lifecycle-hook.js")}]}],PermissionRequest:[{matcher:"",hooks:[{type:"command",command:c("permission-hook.js")}]}],Notification:[{matcher:"idle_prompt",hooks:[{type:"command",command:c("notification-hook.js")}]}],PermissionDenied:[{matcher:"",hooks:[{type:"command",command:c("lifecycle-hook.js")}]}],Stop:[{hooks:[{type:"command",command:c("lifecycle-hook.js")}]}],StopFailure:[{hooks:[{type:"command",command:c("lifecycle-hook.js")}]}],PreCompact:[{hooks:[{type:"command",command:c("lifecycle-hook.js")}]}],PostCompact:[{hooks:[{type:"command",command:c("lifecycle-hook.js")}]}],ConfigChange:[{hooks:[{type:"command",command:c("lifecycle-hook.js")}]}]}},null,2)}
|
|
7
|
+
`,f=h(i,"hooks"),y=h(f,"hooks.json");await $(f,{recursive:!0});let I="";try{I=await C(y,"utf8")}catch{}return I!==p&&(await P(y,p,"utf8"),o.info("claude-adapter",`Wrote Claude hooks config: ${y}`)),i}async ensureStdioMcpServer(){const e=this.resolveProjectRoot(),i=this.resolveStdioServerPath(e);if(this.ensureStdioServerArtifact(e,i),!L(i))throw new Error(`MCP stdio server entry point not found: ${i}`);const t=this.getInternalApiUrl(),s=this.notifyPort,n=[i,"--handle-url",t,"--notify-port",String(s)],r=We(this.claudeCliSessionId);await Ye(r,n),this.claudeMcpConfigPath=r,this.mcpServerReady=!0,this.startActivityTracking()}resolveProjectRoot(){const e=Se(import.meta.url);return Ee(e,"..","..","..","..")}resolveStdioServerPath(e=this.resolveProjectRoot()){return h(e,"dist","mcp","stdio","server.js")}ensureStdioServerArtifact(e,i){if(L(i)||le)return;le=!0;const t=h(e,"node_modules","typescript","bin","tsc"),s=h(e,"tsconfig.json");if(!L(t)||!L(s))return;o.warn("claude-adapter",`MCP stdio server artifact missing, attempting build: ${i}`);const n=fe(process.execPath,[t,"-p",s],{cwd:e,env:process.env,encoding:"utf8",timeout:6e4});if(n.status!==0){const r=`${n.stderr??""}${n.stdout??""}`.trim();throw new Error(`MCP stdio server build failed: ${r||`exit ${n.status??"unknown"}`}`)}}getInternalApiUrl(){return this.internalApi?this.internalApi.url:process.env.GRIX_CONNECTOR_INTERNAL_API?process.env.GRIX_CONNECTOR_INTERNAL_API:`http://127.0.0.1:${this.internalApiPort}`}internalApiPort=0;notifyPort=0;notifySocket=null;async waitForNotifyPort(e){if(this.notifyPort<=0)return;const i=Date.now();for(;Date.now()-i<e;)try{await new Promise((t,s)=>{const n=G.createConnection({host:"127.0.0.1",port:this.notifyPort},()=>{n.destroy(),t()});n.on("error",s),setTimeout(()=>{n.destroy(),s(new Error("probe timeout"))},2e3)});return}catch{await new Promise(t=>setTimeout(t,500))}throw new Error(`Notify port ${this.notifyPort} not ready within ${e}ms`)}async waitForChannelListening(e){const i=Date.now();for(;Date.now()-i<e;){if(this.startupChannelListening){const t=this.startupChannelListeningAt||Date.now(),s=Date.now()-t;if(s>=j)return;await new Promise(n=>setTimeout(n,j-s));return}await new Promise(t=>setTimeout(t,200))}throw new Error(`Claude channel listener not ready within ${e}ms`)}ptyAutoConfirmTimer=null;startPtyAutoConfirm(e){this.ptyAutoConfirmTimer&&clearInterval(this.ptyAutoConfirmTimer);let i=!1;const t=()=>{if(this.startupChannelListening||this.stopped){this.ptyAutoConfirmTimer&&(clearInterval(this.ptyAutoConfirmTimer),this.ptyAutoConfirmTimer=null);return}try{e.write("\r"),i||(o.info("claude-adapter","PTY auto-confirm: sending Enter for dev channels dialog"),i=!0)}catch{}};setTimeout(t,1e3),this.ptyAutoConfirmTimer=setInterval(t,2e3),setTimeout(()=>{this.ptyAutoConfirmTimer&&(clearInterval(this.ptyAutoConfirmTimer),this.ptyAutoConfirmTimer=null)},3e4).unref()}sendDeferredModelSwitch(){if(!this.deferredModelId)return;const e=this.deferredModelId;this.deferredModelId=null,setTimeout(()=>{const i=this.claudePty??this.claudeProcess?.stdin;if(!(!i||this.stopped))try{const t=`/model ${e}
|
|
8
|
+
`;"write"in i?i.write(t):i.write(t,()=>{}),o.info("claude-adapter",`Deferred model switch: /model ${e}`)}catch{}},3e3)}async waitForWindowsChannelReady(e){const i=Date.now();let t=!1;const s=setInterval(()=>{if(this.startupChannelListening){clearInterval(s);return}try{this.claudePty?(this.claudePty.write("\r"),t||(o.info("claude-adapter","Windows PTY: sending Enter to auto-confirm dev channels dialog"),t=!0)):this.claudeProcess?.stdin?.writable&&(this.claudeProcess.stdin.write("\r"),t||(o.info("claude-adapter","Windows shell: sending Enter to auto-confirm dev channels dialog"),t=!0))}catch{}},3e3);try{for(;Date.now()-i<e;){if(this.startupChannelListening){const a=this.startupChannelListeningAt||Date.now(),l=Date.now()-a;if(l>=j)return;await new Promise(u=>setTimeout(u,j-l));return}const n=Date.now()-i,r=this.claudePty?He:12e3;if(this.alive&&this.mcpServerReady&&n>r){o.info("claude-adapter",`Windows ${this.claudePty?"PTY":"shell"} fallback: assuming channel ready after ${n}ms (no stdout detection, MCP server connected)`),this.startupChannelListeningAt=Date.now(),this.startupChannelListening=!0,this.clearPendingMcpFailureTimer(),this.sendDeferredModelSwitch();return}await new Promise(a=>setTimeout(a,500))}}finally{clearInterval(s)}throw new Error(`Windows channel ready check timed out within ${e}ms`)}pushNotification(e,i){if(!this.mcpServerReady||this.notifyPort<=0)return;const t=JSON.stringify({jsonrpc:"2.0",method:e,params:i});!this.notifySocket||this.notifySocket.destroyed?(this.notifySocket=G.createConnection({host:"127.0.0.1",port:this.notifyPort},()=>{this.notifySocket.write(t+`
|
|
9
|
+
`)}),this.notifySocket.on("error",s=>{o.error("claude-adapter",`Notify socket error: ${s.message}`),this.notifySocket=null}),this.notifySocket.on("close",()=>{this.notifySocket=null})):this.notifySocket.write(t+`
|
|
10
|
+
`)}stopMcpServer(){if(this.mcpServerReady=!1,this.notifySocket){try{this.notifySocket.destroy()}catch{}this.notifySocket=null}if(this.mcpServerProcess){try{this.mcpServerProcess.kill("SIGTERM")}catch{}this.mcpServerProcess=null}}releaseNotifyPortReservation(){Qe(this.notifyPort),this.notifyPort=0}killClaudeProcess(e){const i=this.claudeProcess,t=this.claudePty,s=this.claudeChildPid;if(this.claudeProcess=null,this.claudePty=null,this.claudeChildPid=0,this.spawnPromise=null,this.alive=!1,this.stopMcpServer(),this.releaseNotifyPortReservation(),this.mcpChannelBroken=!1,this.startupChannelListening=!1,this.startupChannelListeningAt=0,this.deferredModelId=null,this.clearPendingMcpFailureTimer(),this.sessionIdConflictDetected=!1,this.pendingPermissions.size>0){const n=w(),r=new F(n.permissionRequestsDir);for(const[a]of this.pendingPermissions)r.resolveRequest(a,"deny").catch(()=>{});this.pendingPermissions.clear()}if(t)try{t.kill()}catch{}if(i?.pid&&M(i,"SIGTERM"),s>0)try{process.kill(s,"SIGTERM")}catch{}if(i?.pid||t||s>0){const n=s,r=i,a=t;setTimeout(()=>{if(a)try{a.kill()}catch{}if(r?.pid&&M(r,"SIGKILL"),n>0)try{process.kill(n,"SIGKILL")}catch{}},5e3).unref()}o.info("claude-adapter",`Claude process killed (reason=${e}, pid=${s}, expectPid=${i?.pid})`)}tryRecoverSessionIdConflict(){if(!this.sessionIdConflictDetected||this.stopped||!this.activeEvent||this.activeEvent.replied)return!1;const e=this.activeEvent.eventId;if(this.sessionIdConflictRetriedEventIds.has(e))return this.sessionIdConflictDetected=!1,!1;this.sessionIdConflictRetriedEventIds.add(e),this.sessionIdConflictDetected=!1;const i=this.activeEvent.rawEvent;return o.warn("claude-adapter",`Detected Claude session-id conflict, auto-retrying event once: ${e}`),this.clearActiveEventIdleTimer(),this.clearActiveEventHardTimer(),this.activeEvent=null,this.stopComposing(),this.deliverInboundEvent(i),!0}async handleInternalInvoke(e,i,t){if(e==="event_tool_call"){const s=String(i.tool_name??""),n=Te(s,{...i.arguments??{}}),r=this.getEventToolHandle();if(this.activeEvent){const l=this.activeEvent;String(n.event_id??"").trim()===""&&(n.event_id=l.eventId),String(n.session_id??"").trim()===""&&(n.session_id=l.sessionId)}else if(this.lastClearedEvent&&s==="grix_reply"){const l=this.lastClearedEvent;Date.now()-l.ts<Fe&&(String(n.session_id??"").trim()===""&&(n.session_id=l.sessionId),n.event_id="",o.info("claude-adapter",`Late grix_reply fallback: sending direct message for cleared event ${l.eventId} (cleared ${(Date.now()-l.ts)/1e3}s ago)`))}s==="grix_reply"&&String(n.event_id??"").trim()!==""&&this.completedEventIds.has(String(n.event_id??"").trim())&&(o.info("claude-adapter",`Late grix_reply fallback: sending direct message for completed event ${String(n.event_id??"").trim()}`),n.event_id="");const a=Ie(s,n);if(!a.valid)throw new Error(`\u53C2\u6570\u6821\u9A8C\u5931\u8D25: ${a.error}`);if(r.status!=="ready")throw new Error(`\u8FDE\u63A5\u4E0D\u53EF\u7528: \u5F53\u524D\u72B6\u6001\u4E3A ${r.status}`);if(s==="grix_access_control")return this.executeAccessControl(r,n);if(Pe(s)){const l=this.activeEvent;l&&(l.toolCallInFlight=!0);try{if(t?.aborted)throw new Error("invoke aborted by timeout");const u=await ke(r,s,n);if(u.isError)throw new Error(u.content[0]?.text??"event tool failed");if(t?.aborted)throw new Error("invoke aborted by timeout after send");return this.postProcessEventToolCall(s,n),JSON.parse(u.content[0]?.text??"null")}finally{if(l&&(l.toolCallInFlight=!1,l.pendingStopHook&&this.activeEvent===l)){const u=l.pendingStopHook;l.pendingStopHook=void 0,this.finalizeActiveEvent(u)}}}throw new Error(`\u672A\u77E5\u4E8B\u4EF6\u5DE5\u5177: ${s}`)}return this.bridgeCallbacks.agentInvoke(e,i)}getEventToolHandle(){if(typeof this.bridgeCallbacks.getHandle=="function")try{const e=this.bridgeCallbacks.getHandle(),i=this;return{status:e.status,getStatusSnapshot:()=>e.getStatusSnapshot(),sendEventAck:t=>e.sendEventAck(t),sendStreamChunk(t){i.bridgeCallbacks.sendStreamChunk(t.event_id??"",t.session_id,t.delta_content??"",Number(t.chunk_seq??0)||1,t.is_finish===!0,t.client_msg_id,t.quoted_message_id)},sendMsg:t=>e.sendMsg(t),sendMedia:(t,s)=>e.sendMedia(t,s),sendEventResult:t=>e.sendEventResult(t),sendLocalActionResult:t=>e.sendLocalActionResult(t),sendEventStopAck:t=>e.sendEventStopAck(t),sendEventStopResult:t=>e.sendEventStopResult(t),sendSessionActivitySet:t=>e.sendSessionActivitySet(t),sendPing:()=>e.sendPing(),sendCodexEvent:t=>e.sendCodexEvent(t),sendCodexEventReliable:(t,s)=>e.sendCodexEventReliable(t,s),sendUpdateBindingCard:t=>e.sendUpdateBindingCard(t),agentInvoke:(t,s,n)=>e.agentInvoke(t,s,n),sendStreamChunkRequest:(t,s)=>e.sendStreamChunkRequest(t,s),sendText:(t,s)=>e.sendText(t,s),deleteMessage:(t,s,n)=>e.deleteMessage(t,s,n),sendEventResultRequest:(t,s)=>e.sendEventResultRequest(t,s),onEvent:t=>e.onEvent(t),onLocalAction:t=>e.onLocalAction(t),onStop:t=>e.onStop(t),onRevoke:t=>e.onRevoke(t),onKicked:t=>e.onKicked(t),onError:t=>e.onError(t),onClose:t=>e.onClose(t),onDisconnected:t=>e.onDisconnected(t),disconnect:()=>e.disconnect()}}catch(e){o.warn("claude-adapter",`bridge getHandle failed, using fallback handle: ${e instanceof Error?e.message:String(e)}`)}return this.createFallbackEventToolHandle()}createFallbackEventToolHandle(){const e=this;return{status:"ready",getStatusSnapshot:()=>({status:"ready",connectedAt:Date.now(),reconnectAttempts:0}),sendEventAck:t=>{e.bridgeCallbacks.sendEventAck(t.event_id,t.session_id??"")},sendStreamChunk:t=>{e.bridgeCallbacks.sendStreamChunk(t.event_id??"",t.session_id,t.delta_content??"",Number(t.chunk_seq??0)||1,t.is_finish===!0,t.client_msg_id,t.quoted_message_id)},sendMsg:()=>{},sendEventResult:t=>{e.bridgeCallbacks.sendEventResult(t.event_id,t.status,t.msg,t.code)},sendLocalActionResult:()=>{},sendEventStopAck:()=>{},sendEventStopResult:()=>{},sendSessionActivitySet:t=>{e.bridgeCallbacks.sendSessionComposing(t.session_id,t.active===!0)},sendCodexEvent:()=>{},sendUpdateBindingCard:()=>{},sendPing:()=>{},sendEventState:()=>{},sendEventCancelResult:()=>{},sendQueueClearResult:()=>{},sendQueueSnapshot:()=>{},agentInvoke:(t,s,n)=>e.bridgeCallbacks.agentInvoke(t,s,n),sendStreamChunkRequest:async()=>{throw new Error("fallback handle: no active connection for sendStreamChunkRequest")},sendText:async()=>{throw new Error("fallback handle: no active connection for sendText")},sendMedia:async()=>{throw new Error("fallback handle: no active connection for sendMedia")},deleteMessage:async()=>{throw new Error("fallback handle: no active connection for deleteMessage")},sendEventResultRequest:async()=>{throw new Error("fallback handle: no active connection for sendEventResultRequest")},sendCodexEventReliable:async()=>{throw new Error("fallback handle: no active connection for sendCodexEventReliable")},onEvent:()=>()=>{},onLocalAction:()=>()=>{},onStop:()=>()=>{},onRevoke:()=>()=>{},onEventCancel:()=>()=>{},onQueueClear:()=>()=>{},onKicked:()=>()=>{},onError:()=>()=>{},onClose:()=>()=>{},onDisconnected:()=>()=>{},disconnect:()=>{}}}postProcessEventToolCall(e,i){const t=String(i.event_id??"").trim();if(!t||this.activeEvent?.eventId!==t){o.warn("claude-adapter",`postProcessEventToolCall: event_id mismatch (tool=${e}, eventId=${t}, activeEventId=${this.activeEvent?.eventId??"none"})`);return}if(e==="grix_complete"){this.completedEventIds.set(t,Date.now()),this.clearActiveEvent();return}if(e==="grix_reply"){this.activeEvent.replied=!0;const s=String(i.text??"");s.length>0&&(this.activeEvent.lastReplyTextLen=s.length),this.markActiveEventActivity(t,String(i.session_id??"").trim()||void 0)}}async executeAccessControl(e,i){const t=String(i.action??""),s=Ae[t];if(!s)throw new Error(`\u672A\u77E5 access_control action: ${t}`);const n={};return i.code!=null&&(n.code=i.code),i.sender_id!=null&&(n.sender_id=i.sender_id),i.policy!=null&&(n.policy=i.policy),e.agentInvoke("claude_access_control",{verb:s,payload:n},3e4)}resolveSessionRuntime(){if(!this.runtimeResolver)return{};try{return this.runtimeResolver(this.sessionId)??{}}catch(e){throw new Error(`resolve session runtime failed: ${e instanceof Error?e.message:String(e)}`)}}async validatePluginDir(e){const i=String(e??"").trim();if(!i)return;let t;try{t=await te(i)}catch{throw new Error(`pluginDir is not accessible: ${i}`)}if(!t.isDirectory())throw new Error(`pluginDir is not a directory: ${i}`);const s=h(i,".mcp.json");try{(await te(s)).isFile()&&(await ee(s),o.info("claude-adapter",`Removed conflicting .mcp.json from pluginDir: ${s}`))}catch{}}async ensureWorkspaceTrust(e){const i=h(R(),".claude.json");try{const t=await C(i,"utf8"),s=JSON.parse(t);if(s.projects?.[e]?.hasTrustDialogAccepted===!0)return;s.projects||(s.projects={}),s.projects[e]||(s.projects[e]={}),s.projects[e].hasTrustDialogAccepted=!0,await P(i,JSON.stringify(s),"utf8"),o.info("claude-adapter",`Pre-trusted workspace: ${e}`)}catch(t){o.warn("claude-adapter",`Failed to pre-trust workspace ${e}: ${t}`)}}static REQUIRED_GROWTHBOOK_FEATURES={tengu_harbor:!0,tengu_harbor_permissions:!0,tengu_harbor_prism:!0,tengu_harbor_ledger:[{marketplace:"claude-plugins-official",plugin:"discord"},{marketplace:"claude-plugins-official",plugin:"telegram"},{marketplace:"claude-plugins-official",plugin:"fakechat"},{marketplace:"claude-plugins-official",plugin:"imessage"},{marketplace:"claude-plugins-official",plugin:"grix"}],tengu_flint_harbor:!0,tengu_quill_harbor:"acceptEdits"};async ensureGrowthBookFeatures(e){const i=h(R(),".claude.json");try{let t;try{const a=await C(i,"utf8");t=JSON.parse(a)}catch{t={}}t.cachedGrowthBookFeatures||(t.cachedGrowthBookFeatures={});const s=t.cachedGrowthBookFeatures;let n=!1;for(const[a,l]of Object.entries(he.REQUIRED_GROWTHBOOK_FEATURES))s[a]!==l&&(s[a]=l,n=!0);t.hasCompletedOnboarding||(t.hasCompletedOnboarding=!0,t.lastOnboardingVersion||(t.lastOnboardingVersion="2.1.31"),n=!0),t.projects||(t.projects={});const r=t.projects;if(r[e]||(r[e]={}),r[e].hasCompletedProjectOnboarding||(r[e].hasCompletedProjectOnboarding=!0,n=!0),r[e]?.hasTrustDialogAccepted&&(delete r[e].hasTrustDialogAccepted,n=!0),!n)return;await P(i,JSON.stringify(t,null,2),"utf8"),o.info("claude-adapter",`Injected GrowthBook features, onboarding flags, and deferred trust: ${e}`)}catch(t){o.warn("claude-adapter",`Failed to inject GrowthBook features: ${t}`)}}watchGrowthBookCache(){const e=h(R(),".claude.json"),t=Object.entries({tengu_harbor:!0,tengu_flint_harbor:!0,tengu_harbor_prism:!0});let s=0;const n=async()=>{try{const r=await C(e,"utf8"),a=JSON.parse(r),l=a.cachedGrowthBookFeatures;if(!l)return;let u=!1;for(const[c,p]of t)l[c]!==p&&(l[c]=p,u=!0);if(!u)return;await P(e,JSON.stringify(a),"utf8"),s++,o.info("claude-adapter",`Re-injected GrowthBook features after Claude cache update (attempt ${s})`)}catch{}};ge(e,{interval:500},()=>{if(this.startupChannelListening||this.stopped||s>=5){Z(e);return}n()}),setTimeout(()=>Z(e),3e4).unref()}async injectStatusLineSettings(e){try{const i=this.resolveProjectRoot(),t=h(i,"dist","scripts","status-line-forwarder.js"),s=h(e,".claude"),n=h(s,"settings.json");await $(s,{recursive:!0});let r={};try{r=JSON.parse(await C(n,"utf8"))}catch{}const a={type:"command",command:`node ${t}`,refreshInterval:10};r.statusLine=a;const l=`${JSON.stringify(r,null,2)}
|
|
11
|
+
`;await P(n,l,"utf8"),o.info("claude-adapter",`Injected statusLine settings: ${n}`)}catch(i){o.warn("claude-adapter",`Failed to inject statusLine settings: ${i instanceof Error?i.message:i}`)}}async ensureUserMcpServer(e,i,t){const s=this.resolveServerEntryPath(i),n=process.execPath,r=[s],a=h(R(),".claude.json");let l=null;try{const f=await C(a,"utf8");l=JSON.parse(f)?.mcpServers?.[x]??null}catch{}if(l&&String(l.type||"stdio").trim()==="stdio"&&String(l.command??"").trim()===n&&Array.isArray(l.args)&&l.args.length===r.length&&l.args.every((f,y)=>f===r[y]))return;o.info("claude-adapter",`Registering user-scoped MCP server: ${x} -> ${n} ${r.join(" ")}`);try{H(`${e} mcp remove -s user ${x}`,{encoding:"utf8",timeout:1e4,env:t,stdio:"pipe"})}catch{}const u=["mcp","add","--scope","user",x,"--",n,...r],c=process.platform==="win32"?'"':"'",p=H(`${e} ${u.map(f=>`${c}${f}${c}`).join(" ")}`,{encoding:"utf8",timeout:1e4,env:t,stdio:"pipe"});o.info("claude-adapter",`MCP server registered: ${p.trim()||"ok"}`)}resolveServerEntryPath(e){const i=h(e,"server","main.js");try{if(Q(i))return i}catch{}const t=h(e,"dist","index.js");try{if(Q(t))return t}catch{}throw new Error(`Cannot find grix-claude server entry in pluginDir: ${e}`)}clearActiveEvent(){const e=this.activeEvent;this.clearActiveEventIdleTimer(),this.clearActiveEventHardTimer(),e&&(this.sessionIdConflictRetriedEventIds.delete(e.eventId),this.completedEventIds.set(e.eventId,Date.now()),this.lastClearedEvent={eventId:e.eventId,sessionId:e.sessionId,ts:Date.now()},this.emit(`reply:${e.sessionId}`,{status:"completed"}),this.emit("eventDone",e.eventId)),this.activeEvent=null,this.stopComposing(),this.tryDeliverQueuedEvent()}startComposing(e,i){const t=this.activeEvent&&this.activeEvent.sessionId===e?this.activeEvent.eventId:void 0;this.composingSessionId===e?this.clearComposingTimer():(this.stopComposing(),this.composingSessionId=e),this.bridgeCallbacks.sendSessionComposing(e,!0,i,t),this.composingTimer=setInterval(()=>{this.bridgeCallbacks.sendSessionComposing(e,!0,i,t)},je)}stopComposing(){if(this.clearComposingTimer(),this.composingSessionId){const e=this.composingSessionId;this.composingSessionId=null,this.bridgeCallbacks.sendSessionComposing(e,!1)}}clearComposingTimer(){this.composingTimer&&(clearInterval(this.composingTimer),this.composingTimer=null)}resolveHookSignalsPath(){const e=w();return h(e.dataDir,`hook-signals-${this.sessionId}.json`)}startActivityTracking(){this.activityManager&&this.activityManager.stop();const e=this.resolveHookSignalsPath(),i=w().hookSignalsLogPath,t=new ye(e,i);o.info("claude-adapter",`Activity tracking started: watching ${e}`),this.activityManager=new we({hookSignalStore:t,onActivity:s=>this.onHookActivity(s),onStop:()=>this.onClaudeTurnEnd("Stop"),onStopFailure:()=>this.onClaudeTurnEnd("StopFailure"),onPermissionRequest:(s,n)=>this.handlePermissionHookEvent(s,n)}),this.activityManager.start()}onClaudeTurnEnd(e){if(o.info("claude-adapter",`Hook activity: ${e}`),!!this.activeEvent){if(this.activeEvent.toolCallInFlight){o.info("claude-adapter",`Stop hook deferred: toolCallInFlight for ${this.activeEvent.eventId}`),this.activeEvent.pendingStopHook=e;return}this.finalizeActiveEvent(e)}}attemptRescueFromJsonl(e,i){if(!this.claudeCliSessionId||!this.claudeSessionCwd)return o.info("claude-adapter",`Rescue skipped: no claudeCliSessionId or claudeSessionCwd for ${e}`),!1;const t=Re(this.claudeCliSessionId,this.claudeSessionCwd);if(!t)return o.info("claude-adapter",`Rescue failed: no assistant text found in JSONL for ${e}`),!1;const s=`rescue_${e}_${Date.now()}`;return this.bridgeCallbacks.sendStreamChunk(e,i,t,1,!1,s),this.bridgeCallbacks.sendStreamChunk(e,i,"",2,!0,s),o.info("claude-adapter",`Rescue succeeded for event ${e}: sent ${t.length} chars from JSONL`),!0}finalizeActiveEvent(e){if(!this.activeEvent)return;const i=this.activeEvent.eventId,t=this.activeEvent.sessionId,s=!this.activeEvent.replied&&this.attemptRescueFromJsonl(i,t);this.activeEvent.replied||s?(o.info("claude-adapter",`Stop hook received, finalizing event ${i} as responded (replied=${this.activeEvent.replied}, rescued=${s})`),this.bridgeCallbacks.sendEventResult(i,"responded")):(o.warn("claude-adapter",`Active event not confirmed when ${e}: ${i}, sending failed result`),this.bridgeCallbacks.sendEventResult(i,"failed",`agent completed without confirming reply (${e})`,"reply_unconfirmed")),this.clearActiveEvent(),this.armStopHookBarrier(t)}onHookActivity(e){const i=this.activeEvent?.sessionId||this.composingSessionId;if(o.info("claude-adapter",`Hook activity: tool=${e?.tool_name??"(clear)"} session=${i??"(none)"}`),e&&this.activeEvent&&this.markActiveEventActivity(this.activeEvent.eventId,this.activeEvent.sessionId),!!i)if(e){this.startComposing(i,e);const t=this.activeEvent;if(t){const s=e.event_name,n=e.tool_name,r=e.tool_input??"",a=n?be(n):!1;if(n==="ExitPlanMode"&&s==="PreToolUse")o.info("claude-adapter","ExitPlanMode detected; waiting for user decision via permission card");else if(n==="AskUserQuestion"&&s==="PreToolUse")this.handleAskUserQuestion(t,r);else if(s==="PreToolUse"&&n)r&&(this.lastPreToolInput=r),a||this.bridgeCallbacks.sendToolUse(t.eventId,t.sessionId,n,r);else if((s==="PostToolUse"||s==="PostToolUseFailure")&&n){if(n==="AskUserQuestion")return;const l=r||this.lastPreToolInput;if(this.lastPreToolInput="",!a){const u=s==="PostToolUseFailure"?`(failed) ${l}`:l;this.bridgeCallbacks.sendToolResult(t.eventId,t.sessionId,n,u)}}}}else this.startComposing(i)}handlePermissionHookEvent(e,i){if(!this.activeEvent){o.warn("claude-adapter","PermissionRequest without active event, ignoring");return}if(e==="AskUserQuestion"){o.info("claude-adapter","Skip permission card for AskUserQuestion; handled by agent_question card flow");return}const t=this.activeEvent,s=w();new F(s.permissionRequestsDir).listPending().then(r=>{const a=r.length>0?r[r.length-1]:null;if(!a){o.warn("claude-adapter","No pending permission request found in store");return}const l=a.request_id;this.pendingPermissions.set(l,{eventId:t.eventId,sessionId:t.sessionId});const u=typeof i=="string"?i:"";if(e==="ExitPlanMode"){const f=Me(u);f&&this.bridgeCallbacks.sendReply(t.eventId,t.sessionId,f)}const c=e==="ExitPlanMode"?"":u.slice(0,100),p=c?`${e}: ${c}`:e;this.bridgeCallbacks.sendPermissionCard({eventId:t.eventId,sessionId:t.sessionId,approvalId:l,toolName:e,toolTitle:p}),o.info("claude-adapter",`Sent permission card: approvalId=${l} tool=${e}`)}).catch(r=>{o.warn("claude-adapter",`Failed to send permission card: ${r}`)})}handleAskUserQuestion(e,i){const t=ne(i);if(!t){o.warn("claude-adapter","Failed to parse AskUserQuestion input, skipping agent_question card");return}const s=w(),n=new se(s.questionRequestsDir);n.listPending().then(async r=>{const a=[...r].reverse().filter(c=>String(c.session_id??"").trim()===this.claudeCliSessionId),l=a.find(c=>String(c.event_id??"").trim()===e.eventId)??a.find(c=>String(c.event_id??"").trim()==="")??a[0]??null,u=l?.request_id??`fallback-${Date.now()}`;l&&String(l.event_id??"").trim()===""&&await n.updateRequest(l.request_id,{event_id:e.eventId}),this.pendingQuestion.set(u,{eventId:e.eventId,sessionId:e.sessionId}),e.awaitingUserQuestion=!0,this.bridgeCallbacks.sendAgentQuestionCard(e.eventId,e.sessionId,{request_id:u,mode:"form",questions:t}),o.info("claude-adapter",`Sent agent_question card: request_id=${u} questions=${t.length}`)}).catch(()=>{o.warn("claude-adapter","Failed to list pending questions from store")})}parseAskUserQuestions(e){return ne(e)}parseAskUserQuestionInput(e){let i;try{i=JSON.parse(e)}catch{return null}const t=[],s=i.questions;if(!Array.isArray(s)||s.length===0)return null;for(let n=0;n<s.length;n++){const r=s[n];if(!r||typeof r!="object")continue;const a=String(r.header??r.question??`Question ${n+1}`),l=String(r.prompt??""),u=Array.isArray(r.options)?r.options:void 0,c=r.multiSelect===!0;t.push({header:a,prompt:l,...u&&u.length>0?{options:u}:{},...c?{multi_select:!0}:{}})}return t.length>0?{questions:t}:null}pruneCompletedEvents(){const e=Date.now()-qe;for(const[i,t]of this.completedEventIds.entries())t<e&&this.completedEventIds.delete(i)}markActiveEventActivity(e,i){const t=this.activeEvent;t&&(e&&t.eventId!==e||i&&t.sessionId!==i||(this.resetActiveEventIdleTimer(t.eventId),this.resetActiveEventHardTimer(t.eventId)))}clearActiveEventIdleTimer(){this.activeEventIdleTimer&&(clearTimeout(this.activeEventIdleTimer),this.activeEventIdleTimer=null)}clearActiveEventHardTimer(){this.activeEventHardTimer&&(clearTimeout(this.activeEventHardTimer),this.activeEventHardTimer=null)}resetActiveEventIdleTimer(e){this.clearActiveEventIdleTimer(),this.activeEventIdleTimer=setTimeout(()=>{if(this.stopped||this.activeEvent?.eventId!==e)return;if(this.activeEvent?.toolCallInFlight){o.info("claude-adapter",`Idle timeout skipped: toolCallInFlight for ${e}, resetting timer`),this.resetActiveEventIdleTimer(e);return}if(this.activeEvent?.awaitingUserQuestion){o.info("claude-adapter",`Idle timeout skipped: awaitingUserQuestion for ${e}, resetting timer`),this.resetActiveEventIdleTimer(e);return}if(this.pendingPermissions.size>0){o.info("claude-adapter",`Idle timeout skipped: pendingPermissions=${this.pendingPermissions.size} for ${e}, resetting timer`),this.resetActiveEventIdleTimer(e);return}const i=this.activeEvent?.replied?"responded":"failed",t=this.activeEvent?.replied?void 0:`agent idle for ${z/1e3}s`,s=this.activeEvent?.replied?void 0:"agent_idle_timeout";o.error("claude-adapter",`Active event idle timeout (${z/1e3}s): ${e}, replied=${!!this.activeEvent?.replied}, sending ${i}`),this.completedEventIds.set(e,Date.now()),this.bridgeCallbacks.sendEventResult(e,i,t,s),this.clearActiveEvent()},z)}resetActiveEventHardTimer(e){this.clearActiveEventHardTimer(),this.activeEventHardTimer=setTimeout(()=>{if(this.stopped||this.activeEvent?.eventId!==e)return;const i=this.activeEvent?.replied?"responded":"failed",t=this.activeEvent?.replied?void 0:`agent exceeded max duration ${J/1e3}s`,s=this.activeEvent?.replied?void 0:"agent_hard_timeout";o.error("claude-adapter",`Active event hard timeout (${J/1e3}s): ${e}, replied=${!!this.activeEvent?.replied}, sending ${i}`),this.completedEventIds.set(e,Date.now()),this.bridgeCallbacks.sendEventResult(e,i,t,s),this.clearActiveEvent()},J)}tryDeliverQueuedEvent(){if(this.activeEvent||this.queuedEvents.length===0)return;const e=this.queuedEvents.shift();this.queuedEventIds.delete(e.event_id),this.deliverInboundEvent(e)}rejectQueuedEvents(e,i="failed"){if(this.queuedEvents.length===0)return;const t=this.queuedEvents.splice(0);this.queuedEventIds.clear();for(const s of t)this.bridgeCallbacks.sendEventResult(s.event_id,i,e)}armStopHookBarrier(e){this.stopHookBarrierSessionId=e,this.stopHookBarrierTimer=setTimeout(()=>{this.stopHookBarrierSessionId===e&&(o.warn("claude-adapter",`Stop hook barrier timeout for session=${e}`),this.clearStopHookBarrier(),this.tryDeliverQueuedEvent())},Ne)}clearStopHookBarrier(){this.stopHookBarrierSessionId=null,this.stopHookBarrierTimer&&(clearTimeout(this.stopHookBarrierTimer),this.stopHookBarrierTimer=null)}clearPendingMcpFailureTimer(){this.pendingMcpFailureTimer&&(clearTimeout(this.pendingMcpFailureTimer),this.pendingMcpFailureTimer=null)}markMcpStartupFailure(){if(!this.mcpStartupFailureHandled&&(this.mcpStartupFailureHandled=!0,this.mcpChannelBroken=!0,o.error("claude-adapter","Claude reported blocking MCP server startup failure"),this.activeEvent)){const e=this.activeEvent.eventId;this.completedEventIds.set(e,Date.now()),this.clearActiveEvent()}}}class Be extends ie{adapterSessionId;constructor(e){super(),this.adapterSessionId=e}emitDone(e){this.emit("done",e)}emitError(e){if(this.listenerCount("error")===0){o.warn("claude-adapter",`Prompt handle error (no listeners): ${e.message}`);return}this.emit("error",e)}async cancel(){}}function g(d){if(d==null)return 0;const e=Number(d);return Number.isFinite(e)?e:0}function Y(d){return String(d).replace(/\\/g,"\\\\").replace(/\{/g,"\\{").replace(/\}/g,"\\}")}async function Ge(d,e,i){const t=h(d,"claude.expect"),s=h(d,"claude.pid"),n=["log_user 1","set timeout -1","set startup_prompt_armed 1",`set claude_command [list {${Y(e)}}${i.map(r=>` {${Y(r)}}`).join("")}]`,"spawn -noecho {*}$claude_command",`set pid_file [open {${Y(s)}} w]`,"puts $pid_file [exp_pid -i $spawn_id]","close $pid_file","after 500",'send -- "\\r"',"expect {"," -re {(?i)(Quick.*safety.*check|trust.*folder)} {",' if {$startup_prompt_armed} { send -- "1\\r"; after 300 }; exp_continue'," }"," -re {(?i)(I am using this for local development|Please use --channels|dangerously-load-development-channels)} {",' if {$startup_prompt_armed} { send -- "\\r"; after 300 }; exp_continue'," }"," -re {(?i)(Enter.*confirm|Press.*Enter|Hit.*Enter|Continue.*Enter)} {",' if {$startup_prompt_armed} { send -- "\\r"; after 300 }; exp_continue'," }"," -re {(?i)Listening.*channel messages.*server:grix} {"," set startup_prompt_armed 0"," }"," eof {}","}","interact",""];return await P(s,"","utf8"),await P(t,n.join(`
|
|
12
|
+
`),"utf8"),{expectPath:t,pidPath:s}}async function ze(d,e=50,i=100){for(let t=0;t<e;t++){try{const s=await C(d,"utf8"),n=parseInt(String(s).trim(),10);if(Number.isFinite(n)&&n>0)return n}catch{}await new Promise(s=>setTimeout(s,i))}return 0}function Je(d,e){return new Promise((i,t)=>{let s=!1;const n=a=>{s||(s=!0,d.off("error",r),a())},r=a=>n(()=>t(a));d.once("error",r),ze(e).then(a=>n(()=>i(a))).catch(a=>n(()=>t(a)))})}function ue(d){return d.replace(/[/\\]/g,"-")}function We(d){const e=w();return h(e.dataDir,"claude-mcp-configs",`${ue(d)}.json`)}function Ve(d){const e=w();return h(e.dataDir,"claude-system-prompts",`${ue(d)}.txt`)}async function Ye(d,e){await $(h(w().dataDir,"claude-mcp-configs"),{recursive:!0});const i={mcpServers:{[x]:{type:"stdio",command:process.execPath,args:e}}},t=`${JSON.stringify(i,null,2)}
|
|
13
|
+
`;let s="";try{s=await C(d,"utf8")}catch{}s!==t&&(await P(d,t,"utf8"),o.info("claude-adapter",`Wrote MCP config: ${d}`))}async function Ke(d,e){await $(h(w().dataDir,"claude-system-prompts"),{recursive:!0});const i=`${e}
|
|
14
|
+
`;let t="";try{t=await C(d,"utf8")}catch{}t!==i&&await P(d,i,"utf8")}function K(d,e){if(process.platform==="win32")return;let i=!1;try{const t=H(`ps ax -o pid,command | grep -E -- '--(session-id|resume) ${d}' | grep -v grep`,{encoding:"utf8",timeout:5e3}).trim();if(t)for(const s of t.split(`
|
|
15
|
+
`)){const n=parseInt(s.trim(),10);if(n>0&&n!==process.pid&&!e.includes(n)){o.info("claude-adapter",`Killing stale Claude process PID=${n} holding session ${d}`);try{process.kill(n,"SIGTERM"),i=!0}catch{}}}}catch{}i||N(d)}function N(d){const e=h(R(),".claude"),i=[h(e,"session-env",d)];try{const t=h(e,"sessions");for(const s of me(t))if(s.endsWith(".json"))try{const n=h(t,s),r=JSON.parse(Q(n,"utf8"));r&&r.sessionId===d&&i.push(n)}catch{}}catch{}for(const t of i)try{B(t),ve(t,{recursive:!0,force:!0}),o.info("claude-adapter",`Removed stale session file: ${t}`)}catch{}}function Xe(d,e){if(!e)return!1;const i=h(R(),".claude"),t=Ze(e),s=h(i,"projects",t,`${d}.jsonl`);try{return B(s),!0}catch{return!1}}function Ze(d){const e=d.replace(/\\/g,"/").replace(/:\/\//g,"/");return/^[a-zA-Z]:\//.test(e)||e.startsWith("//")?e.toLowerCase().replace(/:/g,"-").replace(/^\//,"").replace(/\//g,"-"):e.replace(/:/g,"-").replace(/\//g,"-")}export{he as ClaudeAdapter,Ze as deriveClaudeProjectDirName,ne as parseAskUserQuestionPayload};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import c from"node:http";import{randomUUID as d}from"node:crypto";import{log as o}from"../../core/log/index.js";function l(t){t.writeHead(401,{"content-type":"application/json"}),t.end(JSON.stringify({error:"unauthorized"}))}function u(t){t.writeHead(404,{"content-type":"application/json"}),t.end(JSON.stringify({error:"not_found"}))}function h(t,e){t.writeHead(400,{"content-type":"application/json"}),t.end(JSON.stringify({error:e}))}function p(t,e={ok:!0}){t.writeHead(200,{"content-type":"application/json"}),t.end(JSON.stringify(e))}async function v(t){const e=[];for await(const r of t)e.push(r);const n=Buffer.concat(e).toString("utf8").trim();return n?JSON.parse(n):{}}function k(t){const e=(t.headers.authorization??"").trim();return e.toLowerCase().startsWith("bearer ")?e.slice(7).trim():""}class f{host="127.0.0.1";port=0;token;callbacks;server=null;address=null;constructor(e){this.token=d(),this.callbacks=e}getToken(){return this.token}getURL(){return this.address?`http://${this.address.address}:${this.address.port}`:""}async start(){this.server||(this.server=c.createServer(async(e,n)=>{try{await this.handleRequest(e,n)}catch(r){h(n,r instanceof Error?r.message:String(r))}}),await new Promise((e,n)=>{this.server.once("error",n),this.server.listen(this.port,this.host,()=>{this.server.off("error",n),e()})}),this.address=this.server.address(),o.info("claude-bridge",`Bridge server listening on ${this.getURL()}`))}async stop(){if(!this.server)return;const e=this.server;this.server=null,this.address=null,e.closeIdleConnections?.(),e.closeAllConnections?.(),await new Promise((n,r)=>{e.close(s=>s?r(s):n())})}async handleRequest(e,n){if(k(e)!==this.token){l(n);return}if(e.method!=="POST"){n.writeHead(405,{"content-type":"application/json"}),n.end(JSON.stringify({error:"method_not_allowed"}));return}const r=new URL(e.url,"http://localhost").pathname,s=await v(e),i=w.get(r);if(!i){u(n);return}const a=await i(this.callbacks,s);p(n,a??{ok:!0})}}const w=new Map([["/v1/worker/register",async(t,e)=>(o.info("claude-bridge",`Worker registered: ${e.worker_id} (pid=${e.pid})`),t.onRegisterWorker(e))],["/v1/worker/status",async(t,e)=>(o.info("claude-bridge",`Worker status: ${e.status}`),t.onStatusUpdate(e))],["/v1/worker/send-text",async(t,e)=>t.onSendText(e)],["/v1/worker/send-stream-chunk",async(t,e)=>t.onSendStreamChunk(e)],["/v1/worker/send-media",async(t,e)=>t.onSendMedia(e)],["/v1/worker/delete-message",async(t,e)=>t.onDeleteMessage(e)],["/v1/worker/ack-event",async(t,e)=>t.onAckEvent(e)],["/v1/worker/event-result",async(t,e)=>t.onSendEventResult(e)],["/v1/worker/event-stop-ack",async(t,e)=>t.onSendEventStopAck(e)],["/v1/worker/event-stop-result",async(t,e)=>t.onSendEventStopResult(e)],["/v1/worker/session-composing",async(t,e)=>t.onSetSessionComposing(e)],["/v1/worker/agent-invoke",async(t,e)=>t.onAgentInvoke(e)],["/v1/worker/local-action-result",async(t,e)=>t.onLocalActionResult(e)]]);export{f as ClaudeBridgeServer};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{randomUUID as x}from"node:crypto";import{CallToolRequestSchema as S,ListToolsRequestSchema as w}from"@modelcontextprotocol/sdk/types.js";import{log as f}from"../../core/log/index.js";import{toolCallToInvoke as k}from"../../core/mcp/tools.js";const E=new Set(["contact_search","session_search","message_history","message_search","group_create","group_detail_read","group_leave_self","group_member_add","group_member_remove","group_member_role_update","group_all_members_muted_update","group_member_speaking_update","group_dissolve","send_msg","delete_msg","agent_api_create","agent_category_list","agent_category_create","agent_category_update","agent_category_assign","agent_api_key_rotate"]),y=3e4,I=[{name:"reply",description:"Send a visible message back to the chat for this grix-claude event.",inputSchema:{type:"object",properties:{text:{type:"string",description:"The visible reply text to send."},chat_id:{type:"string",description:"The target chat/session id from the <channel> tag."},event_id:{type:"string",description:"The Aibot event_id from the <channel> tag."},reply_to:{type:"string",description:"Optional message_id to quote instead of the inbound trigger message."},files:{type:"array",items:{type:"string"},description:"Optional absolute local file paths. Each file is uploaded through Agent API OSS presign before sending."},final:{type:"boolean",description:"Whether this is the final reply for the event. Defaults to false \u2014 the event stays open while Claude continues working, and auto-completes after inactivity. Set true only when this is definitively the last message for the event."}},required:["chat_id","event_id"]}},{name:"complete",description:"Finish an event without sending a visible reply so the backend does not time out.",inputSchema:{type:"object",properties:{event_id:{type:"string",description:"The Aibot event_id from the <channel> tag."},status:{type:"string",enum:["responded","canceled","failed"]},code:{type:"string"},msg:{type:"string"}},required:["event_id","status"]}},{name:"delete_message",description:"Delete a previously sent message in the same grix-claude chat.",inputSchema:{type:"object",properties:{chat_id:{type:"string"},message_id:{type:"string"}},required:["chat_id","message_id"]}},{name:"status",description:"Show grix-claude runtime status, upstream access state, bridge health, and startup hints.",inputSchema:{type:"object",properties:{}}},{name:"send",description:"Send a message to a chat session proactively, without requiring an inbound event. Use for notifications or scheduled reports.",inputSchema:{type:"object",properties:{chat_id:{type:"string",description:"The target chat/session id."},text:{type:"string",description:"The message text to send."}},required:["chat_id","text"]}},{name:"access_pair",description:"Forward a sender pairing approval code to upstream access control.",inputSchema:{type:"object",properties:{code:{type:"string"}},required:["code"]}},{name:"access_deny",description:"Forward a sender pairing denial code to upstream access control.",inputSchema:{type:"object",properties:{code:{type:"string"}},required:["code"]}},{name:"allow_sender",description:"Ask upstream access control to allow a sender_id.",inputSchema:{type:"object",properties:{sender_id:{type:"string"}},required:["sender_id"]}},{name:"remove_sender",description:"Ask upstream access control to remove a sender_id.",inputSchema:{type:"object",properties:{sender_id:{type:"string"}},required:["sender_id"]}},{name:"access_policy",description:"Ask upstream access control to update the sender access policy.",inputSchema:{type:"object",properties:{policy:{type:"string",enum:["allowlist","open","disabled"]}},required:["policy"]}},{name:"grix_query",description:"Search contacts, sessions, message history, or messages by keyword in the Grix/AIBot platform.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["contact_search","session_search","message_history","message_search"]},keyword:{type:"string"},id:{type:"string"},sessionId:{type:"string"},limit:{type:"number"},offset:{type:"number"},beforeId:{type:"string"}},required:["action"]}},{name:"grix_group",description:"Manage groups in the Grix/AIBot platform: create, get details, leave, dissolve, manage members and permissions.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["create","detail","leave","add_members","remove_members","update_member_role","update_all_members_muted","update_member_speaking","dissolve"]},sessionId:{type:"string"},name:{type:"string"},memberIds:{type:"array",items:{type:"string"}},memberTypes:{type:"array",items:{type:"integer",enum:[1,2]}},memberId:{type:"string"},role:{type:"integer",enum:[1,2]},memberType:{type:"integer",description:"Member type (for update_member_role / update_member_speaking)."},allMembersMuted:{type:"boolean"},isSpeakMuted:{type:"boolean"},canSpeakWhenAllMuted:{type:"boolean",description:"Allow speaking when all muted (for update_member_speaking)."}},required:["action"]}},{name:"grix_message_send",description:"Send a message to a session in the Grix/AIBot platform.",inputSchema:{type:"object",properties:{sessionId:{type:"string"},content:{type:"string"},msgType:{type:"number"},quotedMessageId:{type:"string"},threadId:{type:"string"}},required:["sessionId","content"]}},{name:"grix_message_unsend",description:"Recall/unsend a message in the Grix/AIBot platform.",inputSchema:{type:"object",properties:{sessionId:{type:"string"},msgId:{type:"string"}},required:["sessionId","msgId"]}},{name:"grix_admin",description:"Agent and category management in the Grix/AIBot platform: create agents, manage categories, rotate API keys.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["create_agent","list_categories","create_category","update_category","assign_category","rotate_api_key"]},agentId:{type:"string"},agentName:{type:"string"},introduction:{type:"string"},isMain:{type:"boolean"},categoryId:{type:"string"},name:{type:"string"},parentId:{type:"string"},sortOrder:{type:"number"}},required:["action"]}}];function q(r,e){r.setRequestHandler(w,async()=>({tools:I})),r.setRequestHandler(S,async i=>{const{name:n,arguments:t}=i.params,s=t??{};try{switch(n){case"reply":return await A(s,e);case"complete":return await $(s,e);case"delete_message":return await T(s,e);case"status":return j(e);case"send":return await M(s,e);case"access_pair":case"access_deny":case"allow_sender":case"remove_sender":case"access_policy":return await R(n,s,e);case"grix_query":case"grix_group":case"grix_message_send":case"grix_message_unsend":case"grix_admin":return await O(n,s,e);default:return{content:[{type:"text",text:`Unknown tool: ${n}`}],isError:!0}}}catch(a){return f.error("claude-tools",`Tool ${n} error: ${a}`),{content:[{type:"text",text:`Error: ${a instanceof Error?a.message:String(a)}`}],isError:!0}}})}async function A(r,e){const i=e.getActiveEvent();if(!i)return{content:[{type:"text",text:"No active event to reply to"}],isError:!0};const n=String(r.text??""),t=String(r.chat_id??""),s=String(r.event_id??i.eventId),a=r.reply_to,d=r.files,_=r.final===!0;if(!t||!s)return{content:[{type:"text",text:"reply requires chat_id and event_id"}],isError:!0};if(!n.trim()&&(!d||d.length===0))return{content:[{type:"text",text:"reply requires at least one of text or files"}],isError:!0};const{text:g,quotedMessageId:v}=e.resolveQuotedMessageId(a,n),b=[];let m=0;const h=`reply_${s}_${Date.now()}`;try{if(g){const p=e.splitText(g);for(let o=0;o<p.length;o++){if(!e.isEventActive(s))return c("ignored: event no longer active");m++,e.bridge.sendStreamChunk(s,t,p[o],++i.chunkSeq,!1,h)}}if(d&&d.length>0)for(const p of d){if(!e.isEventActive(s))return c("ignored: event no longer active");m++;const o=await e.uploadFile(p,t),l=`${x()}_${m}`;e.bridge.sendMedia(s,t,o.access_url,o.file_name,v,l,o.extra),f.info("claude-tools",`File sent: ${o.file_name}`)}e.bridge.sendStreamChunk(s,t,"",++i.chunkSeq,!0,h)}catch(p){if(g&&b.length===0)try{const o=`fallback_${s}_${Date.now()}`,l=e.splitText(g);for(let u=0;u<l.length;u++)e.bridge.sendStreamChunk("",t,l[u],u+1,!1,o);return e.bridge.sendStreamChunk("",t,"",l.length+1,!0,o),e.markReplySent(s),_&&e.finalizeEvent(s,"responded"),c("sent via fallback")}catch{}if(!e.isEventActive(s))return c("ignored: event no longer active");throw e.bridge.sendEventResult(s,"failed",String(p),"send_msg_failed"),p}return e.markReplySent(s),_?e.finalizeEvent(s,"responded"):(i.responded=!0,e.clearActiveEvent("completed")),c("Reply sent")}async function $(r,e){const i=e.getActiveEvent(),n=String(r.event_id??""),t=r.status??"",s=r.code,a=r.msg;if(!n||!t)return{content:[{type:"text",text:"complete requires event_id and status"}],isError:!0};const d=["responded","canceled","failed"];return d.includes(t)?e.isEventActive(n)?(e.bridge.sendEventResult(n,t,a,s),e.clearActiveEvent(t),c("Event completed")):c("ignored: event no longer active"):{content:[{type:"text",text:`status must be one of: ${d.join(", ")}`}],isError:!0}}async function T(r,e){const i=String(r.chat_id??""),n=String(r.message_id??"");if(!i||!n)return{content:[{type:"text",text:"chat_id and message_id are required"}],isError:!0};try{return await e.bridge.agentInvoke("grix_message_unsend",{sessionId:i,msgId:n},y),c(`deleted (${n})`)}catch(t){return{content:[{type:"text",text:`Delete failed: ${t}`}],isError:!0}}}function j(r){const e=r.getStatusInfo();return{content:[{type:"text",text:JSON.stringify({alive:e.alive,active_event:e.activeEvent,pending_approvals:e.pendingPermissions,pending_questions:e.pendingElicitations})}]}}async function M(r,e){const i=String(r.chat_id??""),n=String(r.text??"");if(!i||!n)return{content:[{type:"text",text:"chat_id and text are required"}],isError:!0};try{const t=e.splitText(n),s=`send_${i}_${Date.now()}`;for(let a=0;a<t.length;a++)e.bridge.sendStreamChunk("",i,t[a],a+1,!1,s);return e.bridge.sendStreamChunk("",i,"",t.length+1,!0,s),c("sent")}catch(t){return{content:[{type:"text",text:`Send failed: ${t}`}],isError:!0}}}const C={access_pair:{verb:"pair_approve",payloadKey:"code"},access_deny:{verb:"pair_deny",payloadKey:"code"},allow_sender:{verb:"sender_allow",payloadKey:"sender_id"},remove_sender:{verb:"sender_remove",payloadKey:"sender_id"},access_policy:{verb:"policy_set",payloadKey:"policy"}};async function R(r,e,i){try{const n=C[r];if(!n)throw new Error(`Unknown access control tool: ${r}`);const t={};e.code!=null&&(t.code=e.code),e.sender_id!=null&&(t.sender_id=e.sender_id),e.policy!=null&&(t.policy=e.policy);const s=await i.bridge.agentInvoke("claude_access_control",{verb:n.verb,payload:t},y);return{content:[{type:"text",text:typeof s=="string"?s:JSON.stringify(s)}]}}catch(n){return{content:[{type:"text",text:`${r} failed: ${n}`}],isError:!0}}}async function O(r,e,i){try{const n=k(r,e);if(!E.has(n.action))throw new Error(`Action not allowed: ${n.action}`);const t=await i.bridge.agentInvoke(n.action,n.params,y);if(t&&Number(t.code??0)!==0)throw new Error(String(t.msg??"invoke failed"));return{content:[{type:"text",text:t?.data!=null?typeof t.data=="string"?t.data:JSON.stringify(t.data):JSON.stringify(t)}]}}catch(n){return{content:[{type:"text",text:`${r} failed: ${n}`}],isError:!0}}}function c(r){return{content:[{type:"text",text:r}]}}export{q as registerClaudeTools};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{log as l}from"../../core/log/index.js";class c{controlURL="";token="";isConfigured(){return!!(this.controlURL&&this.token)}configure(r,e){this.controlURL=r.replace(/\/+$/,""),this.token=e.trim(),l.info("claude-worker-client",`Configured with control URL: ${this.controlURL}`)}async post(r,e,s){if(!this.isConfigured())throw new Error("worker control not configured");const i=new AbortController,o=setTimeout(()=>i.abort(),s);try{const t=await fetch(`${this.controlURL}${r}`,{method:"POST",headers:{"content-type":"application/json",authorization:`Bearer ${this.token}`},body:JSON.stringify(e),signal:i.signal}),n=await t.text(),a=n.trim()?JSON.parse(n):{};if(!t.ok)throw new Error(a.error||`worker control failed ${t.status}`);return a}finally{clearTimeout(o)}}isRetryableError(r){const e=r instanceof Error?r.message:String(r);return/fetch failed|network|ECONNRESET|ETIMEDOUT|EAI_AGAIN|socket hang up|aborted/i.test(e)}async postWithRetry(r,e,s,i=1){let o;for(let t=0;t<=i;t++)try{return t>0&&l.info("claude-worker-client",`Retrying ${r} attempt=${t+1}`),await this.post(r,e,s)}catch(n){if(o=n,t>=i||!this.isRetryableError(n))break;await new Promise(a=>setTimeout(a,150))}throw o instanceof Error?o:new Error(String(o))}async deliverEvent(r){return this.postWithRetry("/v1/worker/deliver-event",{payload:r},1e4,1)}async deliverStop(r){return this.postWithRetry("/v1/worker/deliver-stop",{payload:r},1e4,1)}async deliverLocalAction(r){return this.postWithRetry("/v1/worker/deliver-local-action",{payload:r},1e4,1)}async ping(){try{return await this.postWithRetry("/v1/worker/ping",{},5e3,1),!0}catch{return!1}}}export{c as ClaudeWorkerClient};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{ClaudeAdapter as o}from"./claude-adapter.js";import{CHANNEL_NAME as t}from"./protocol-contract.js";export{t as CHANNEL_NAME,o as ClaudeAdapter};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{INTERACTION_KINDS as c,LOCAL_ACTION_TYPES as m,INTERACTION_RESOLUTION_TYPES as d}from"./protocol-contract.js";import{normalizeString as r}from"../../core/util/normalize-string.js";function a(e){return!!e&&typeof e=="object"&&!Array.isArray(e)}function p(e){return JSON.parse(JSON.stringify(e))}function f(e){return Array.isArray(e)?e.map(t=>{if(!a(t))return null;const u=r(t.key),o=r(t.value);return!u||!o?null:{key:u,value:o}}).filter(t=>t!==null):[]}function y(e){const t=r(e.kind);if(!Object.values(c).includes(t))throw new Error(`interaction kind ${t} is invalid`);return{kind:t,request_id:r(e.requestID),session_id:r(e.sessionID),message_id:r(e.messageID),payload:a(e.payload)?e.payload:{}}}function C(e){return{tool_name:r(e.tool_name),description:r(e.description),input_preview:r(e.input_preview)}}function I(e){const t=a(e?.request_payload)?e.request_payload:{},u=r(t.mode).toLowerCase()==="url"?"url":"form",o={mode:u},i=r(t.message||t.prompt);return i&&(o.message=i),u==="url"?(o.url=r(t.url),o):(a(t.requested_schema)&&(o.requested_schema=p(t.requested_schema)),o)}function g(e){return{domain:"interaction_reply",kind:r(e.kind),request_id:r(e.requestID),outcome:r(e.outcome)||"resolved"}}function h(e){if(r(e?.action_type).toLowerCase()!==m.interactionReply)return{matched:!1,kind:"",requestID:"",resolution:null,errorCode:"",errorMsg:""};const u=a(e?.params)?e.params:{},o=r(u.kind).toLowerCase();if(!Object.values(c).includes(o))return{matched:!0,kind:"",requestID:r(u.request_id),resolution:null,errorCode:"resolution_invalid",errorMsg:"interaction kind is invalid"};const i=r(u.request_id),l=a(u.resolution)?u.resolution:null;if(!l)return{matched:!0,kind:o,requestID:i,resolution:null,errorCode:"resolution_invalid",errorMsg:"interaction resolution is required"};const s=r(l.type).toLowerCase();if(s===d.decision){const n=r(l.value).toLowerCase();return n==="allow"||n==="deny"?{matched:!0,kind:o,requestID:i,resolution:{type:s,value:n},errorCode:"",errorMsg:""}:{matched:!0,kind:o,requestID:i,resolution:null,errorCode:"resolution_invalid",errorMsg:`interaction decision ${n} is invalid`}}if(s===d.action){const n=r(l.value).toLowerCase();return["accept","decline","cancel"].includes(n)?{matched:!0,kind:o,requestID:i,resolution:{type:s,value:n},errorCode:"",errorMsg:""}:{matched:!0,kind:o,requestID:i,resolution:null,errorCode:"resolution_invalid",errorMsg:`interaction action ${n} is invalid`}}if(s===d.text){const n=r(l.value);return n?{matched:!0,kind:o,requestID:i,resolution:{type:s,value:n},errorCode:"",errorMsg:""}:{matched:!0,kind:o,requestID:i,resolution:null,errorCode:"resolution_invalid",errorMsg:"interaction text resolution is required"}}if(s===d.map){const n=f(l.entries);return n.length>0?{matched:!0,kind:o,requestID:i,resolution:{type:s,entries:n},errorCode:"",errorMsg:""}:{matched:!0,kind:o,requestID:i,resolution:null,errorCode:"resolution_invalid",errorMsg:"interaction map resolution is required"}}return{matched:!0,kind:o,requestID:i,resolution:null,errorCode:"resolution_invalid",errorMsg:`interaction resolution type ${s} is invalid`}}export{I as buildElicitationInteractionPayload,g as buildInteractionReplyResult,y as buildInteractionRequestInvokeParams,C as buildPermissionInteractionPayload,h as parseInteractionReplyAction};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{spawn as x,execSync as y}from"node:child_process";import{randomUUID as v}from"node:crypto";import{mkdir as S}from"node:fs/promises";import{readFileSync as C}from"node:fs";import{join as d}from"node:path";import{homedir as T,tmpdir as I}from"node:os";import{log as o}from"../../core/log/index.js";import{MCP_HTTP_CHANNEL_NAME as m}from"./protocol-contract.js";function P(t){let e=null,r=0,n=!1,i=!1;const a=v(),s=t.gatewayUrl??"http://127.0.0.1:19580/mcp";return{async start(){await M(t.command,s,t.env);const c=E(t.grix),u=[...t.args??[],"--name",`grix-mcp-${t.name}`,"--session-id",a];t.fullAuto&&u.push("--dangerously-skip-permissions"),u.push("--dangerously-load-development-channels",`server:${m}`,"--append-system-prompt",c);const f=d(I(),`grix-mcp-claude-${t.name}`);await S(f,{recursive:!0});const{expectPath:$,pidPath:g}=await k(f,t.command,u),_={...process.env,...t.env??{}};e=x("/usr/bin/expect",[$],{cwd:t.cwd,env:_,stdio:["ignore","pipe","pipe"],detached:!0}),o.info("mcp-http-launcher",`\u542F\u52A8 Claude: name=${t.name} cwd=${t.cwd} pid=${e.pid}`),r=await F(g),n=!0,o.info("mcp-http-launcher",`Claude \u5B50\u8FDB\u7A0B PID: ${r}`),e.on("exit",(l,p)=>{o.info("mcp-http-launcher",`Claude \u9000\u51FA: code=${l} signal=${p}`),n=!1,e=null,r=0,i||(o.info("mcp-http-launcher","3 \u79D2\u540E\u81EA\u52A8\u91CD\u542F..."),setTimeout(()=>{i||this.start().catch(w=>{o.error("mcp-http-launcher",`\u91CD\u542F\u5931\u8D25: ${w}`)})},3e3))}),e.stdout?.on("data",l=>{const p=l.toString().trim();p&&o.info("mcp-http-launcher",`[stdout] ${p.slice(0,300)}`)}),e.stderr?.on("data",l=>{const p=l.toString().trim();p&&o.info("mcp-http-launcher",`[stderr] ${p.slice(0,300)}`)})},async stop(){if(i=!0,n=!1,r>0)try{process.kill(r,"SIGTERM")}catch{}if(e?.pid){try{process.kill(-e.pid,"SIGTERM")}catch{}await new Promise(c=>{const u=setTimeout(()=>{if(r>0)try{process.kill(r,"SIGKILL")}catch{}if(e?.pid)try{process.kill(-e.pid,"SIGKILL")}catch{}c()},5e3);e?.once("exit",()=>{clearTimeout(u),c()})})}e=null,r=0},getStatus(){return{name:t.name,alive:n,pid:r}}}}function E(t){return["You are connected to a chat via the grix MCP server.",`On startup, immediately call grix_authorize with: agentId="${t.agentId}", apiKey="${t.apiKey}", wsUrl="${t.wsUrl}", clientType="${t.clientType}".`,"When you receive a <channel> message, you MUST respond by calling the grix_reply tool (or the grix_complete tool if no response is needed).","Never write your reply as plain text \u2014 it will NOT reach the user. Only the grix_reply tool delivers your response to the chat.","The <channel> message contains event_id and session_id \u2014 pass them to grix_reply."].join(" ")}async function M(t,e,r){const n=d(T(),".claude.json");let i=null;try{const s=C(n,"utf8");i=JSON.parse(s)?.mcpServers?.[m]??null}catch{}if(i&&String(i.type??"").trim()==="http"&&String(i.url??"").trim()===e)return;o.info("mcp-http-launcher",`\u6CE8\u518C MCP Server: ${m} -> ${e}`);const a={...process.env,...r??{}};try{y(`${t} mcp remove -s user ${m}`,{encoding:"utf8",timeout:1e4,env:a,stdio:"pipe"})}catch{}y(`${t} mcp add --scope user --transport http ${m} ${e}`,{encoding:"utf8",timeout:1e4,env:a,stdio:"pipe"})}async function k(t,e,r){const{writeFile:n}=await import("node:fs/promises"),i=d(t,"claude.pid"),a=d(t,"claude.expect"),s=["log_user 1","set timeout -1","set startup_prompt_armed 1",`set claude_command [list {${h(e)}}${r.map(c=>` {${h(c)}}`).join("")}]`,"spawn -noecho {*}$claude_command",`set pid_file [open {${h(i)}} w]`,"puts $pid_file [exp_pid -i $spawn_id]","close $pid_file","expect {"," -re {(?i)(Quick.*safety.*check|trust.*folder)} {",' if {$startup_prompt_armed} { send -- "1\\r"; after 300 }; exp_continue'," }"," -re {(?i)I am using this for local development} {",' if {$startup_prompt_armed} { send -- "1\\r"; after 300 }; exp_continue'," }"," -re {(?i)(Enter.*confirm|Press.*Enter|Hit.*Enter)} {",' if {$startup_prompt_armed} { send -- "\\r"; after 300 }; exp_continue'," }"," -re {Listening for channel} {"," set startup_prompt_armed 0"," after 1000",' send -- "Call grix_authorize now as instructed in your system prompt.\\r"'," }"," -re {bypass permissions} {"," set startup_prompt_armed 0"," after 1000",' send -- "Call grix_authorize now as instructed in your system prompt.\\r"'," }"," eof {}","}","expect eof",""];return await n(i,"","utf8"),await n(a,s.join(`
|
|
2
|
+
`),"utf8"),{expectPath:a,pidPath:i}}function h(t){return t.replace(/[\\{}$\[\]"]/g,"\\$&")}async function F(t,e=1e4){const{readFile:r}=await import("node:fs/promises"),n=Math.ceil(e/100);for(let i=0;i<n;i++){try{const a=await r(t,"utf8"),s=parseInt(String(a).trim(),10);if(Number.isFinite(s)&&s>0)return s}catch{}await new Promise(a=>setTimeout(a,100))}return 0}export{P as createMcpHttpLauncher};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{readJSONFile as A}from"../../core/util/json-file.js";import{log as i}from"../../core/log/index.js";import{homedir as h}from"node:os";import{join as f}from"node:path";const s=[{id:"opus",displayName:"Opus"},{id:"sonnet",displayName:"Sonnet"},{id:"haiku",displayName:"Haiku"},{id:"claude-opus-4-7",displayName:"Claude Opus 4.7"},{id:"claude-opus-4-6",displayName:"Claude Opus 4.6"},{id:"claude-sonnet-4-6",displayName:"Claude Sonnet 4.6"},{id:"claude-sonnet-4-5",displayName:"Claude Sonnet 4.5"},{id:"claude-haiku-4-5",displayName:"Claude Haiku 4.5"}],g=1440*60*1e3;let e=null,t=0;function N(){const n=f(h(),".claude");return A(f(n,"settings.json"))?.env??{}}async function I(){if(e&&Date.now()-t<g)return e;const n=N(),l=(n.ANTHROPIC_BASE_URL??"").trim(),r=(n.ANTHROPIC_AUTH_TOKEN??"").trim(),m=(n.ANTHROPIC_API_KEY??"").trim();if(!l||!(r||m))return i.info("model-list","No API credentials found in settings, using fallback model list"),e=s,t=Date.now(),e;try{const a=`${l.replace(/\/+$/,"")}/v1/models`,d={"anthropic-version":"2023-06-01"};r?d.Authorization=`Bearer ${r}`:d["x-api-key"]=m;const p=new AbortController,y=setTimeout(()=>p.abort(),1e4),c=await fetch(a,{headers:d,signal:p.signal});if(clearTimeout(y),!c.ok)return i.warn("model-list",`API returned ${c.status}, using fallback model list`),e=s,t=Date.now(),e;const u=(await c.json()).data;return!Array.isArray(u)||u.length===0?(i.warn("model-list","API returned empty model list, using fallback"),e=s,t=Date.now(),e):(e=u.map(o=>({id:o.id,displayName:o.display_name||o.name||o.id})),t=Date.now(),i.info("model-list",`Fetched ${e.length} models from API: ${e.map(o=>o.id).join(", ")}`),e)}catch(a){return i.warn("model-list",`Failed to fetch models: ${a instanceof Error?a.message:a}, using fallback`),e=s,t=Date.now(),e}}function O(){return e??s}function H(){e=null,t=0}export{I as fetchAvailableModels,O as getCachedModels,N as readSettingsEnv,H as resetModelCache};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const e="claude/base",t="grix-claude",o={sessionControl:"session_control",setMode:"set_mode",setModel:"set_model",interactionReply:"claude_interaction_reply",fileList:"file_list",createFolder:"create_folder",getSessionUsage:"get_session_usage",getRateLimits:"get_rate_limits",threadCompact:"thread_compact",getAgentGlobalConfig:"get_agent_global_config"},i={interactionRequestCreate:"claude_interaction_request_create",accessControl:"claude_access_control"},n={permission:"permission",elicitation:"elicitation"},s={decision:"decision",action:"action",text:"text",map:"map"},r={open:"open",status:"status",where:"where",stop:"stop",restart:"restart",setMode:"set_mode",setModel:"set_model",listOptions:"list_options",approve:"approve",reject:"reject",exec:"exec"},_={approval:"approval",fullAuto:"full_auto"},a={responded:"responded",failed:"failed",canceled:"canceled"},d={ok:"ok",failed:"failed"},c={requestIDRequired:"interaction_request_id_required",requestNotFound:"interaction_request_not_found",requestNotPending:"interaction_request_not_pending",resolutionInvalid:"interaction_resolution_invalid",forwardFailed:"interaction_forward_failed"},l={cwdRequired:"session_cwd_required",invalidCwd:"session_invalid_cwd",bindingMissing:"session_binding_missing",rebindForbidden:"session_rebind_forbidden",verbInvalid:"session_verb_invalid",runtimeError:"session_runtime_error"},p={unsupportedLocalAction:"unsupported_local_action",localActionRouteMissing:"local_action_route_missing",modeInvalid:"set_mode_invalid"};export{e as ADAPTER_HINT,i as AGENT_INVOKE_ACTIONS,t as CHANNEL_NAME,a as EVENT_RESULT_STATUSES,n as INTERACTION_KINDS,c as INTERACTION_REPLY_ERROR_CODES,s as INTERACTION_RESOLUTION_TYPES,p as LOCAL_ACTION_ERROR_CODES,d as LOCAL_ACTION_RESULT_STATUSES,o as LOCAL_ACTION_TYPES,l as SESSION_CONTROL_ERROR_CODES,r as SESSION_CONTROL_VERBS,_ as SESSION_MODE_IDS};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
class m{defaultTimeoutMs;onTimeout;timers=new Map;constructor(e){this.defaultTimeoutMs=e.defaultTimeoutMs??9e4,this.onTimeout=e.onTimeout}arm(e,t){this.cancel(e);const s=t?.timeoutMs??this.defaultTimeoutMs,i=Date.now()+s,o=setTimeout(()=>{this.timers.delete(e),this.onTimeout(e).catch(()=>{})},s);return this.timers.set(e,o),i}cancel(e){const t=this.timers.get(e);t&&(clearTimeout(t),this.timers.delete(e))}has(e){return this.timers.has(e)}close(){for(const e of this.timers.values())clearTimeout(e);this.timers.clear()}}export{m as ResultTimeoutManager};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{readdirSync as y,readFileSync as g,existsSync as d,statSync as S}from"node:fs";import{dirname as w,join as s,resolve as b}from"node:path";import{homedir as F}from"node:os";import{log as L}from"../../core/log/index.js";function D(t){const e=t.trim();if(!e.startsWith("---"))return{name:"",description:""};const i=e.indexOf("---",3);if(i===-1)return{name:"",description:""};const r=e.slice(3,i).trim();let n="",c="",f;for(const a of r.split(`
|
|
2
|
+
`)){const u=a.indexOf(":");if(u===-1)continue;const p=a.slice(0,u).trim();let o=a.slice(u+1).trim();(o.startsWith('"')&&o.endsWith('"')||o.startsWith("'")&&o.endsWith("'"))&&(o=o.slice(1,-1)),p==="name"?n=o:p==="description"?c=o:p==="trigger"&&(f=o)}return{name:n,description:c,trigger:f}}function O(t,e,i){if(!d(t))return[];const r=[];try{for(const n of y(t,{withFileTypes:!0})){const c=s(t,n.name);if(!j(n,c)||n.name.startsWith("."))continue;const f=s(c,"SKILL.md");if(d(f))try{const a=g(f,"utf-8"),u=D(a);u.name&&r.push({name:u.name,description:u.description,trigger:u.trigger,source:e,pluginName:i})}catch{}}}catch{}return r}function l(t,e,i){if(!d(t))return[];const r=i?.maxDepth??6,n=i?.includeHiddenDirs??!1,c=[],f=(a,u)=>{if(u>r)return;let p;try{p=y(a,{withFileTypes:!0,encoding:"utf8"})}catch{return}for(const o of p){const m=s(a,o.name);if(!j(o,m)||!n&&o.name.startsWith("."))continue;const h=s(m,"SKILL.md");if(d(h))try{const x=g(h,"utf-8"),k=D(x);k.name&&c.push({name:k.name,description:k.description,trigger:k.trigger,source:e,pluginName:i?.pluginName})}catch{}f(m,u+1)}};return f(t,0),c}function j(t,e){if(t.isDirectory())return!0;if(!t.isSymbolicLink())return!1;try{return S(e).isDirectory()}catch{return!1}}function W(t){const e=[],i=new Set;for(const r of t){const n=`${r.name.trim().toLowerCase()}|${r.source}|${(r.pluginName??"").trim().toLowerCase()}`;i.has(n)||(i.add(n),e.push(r))}return e}function N(t){const e=[],i=new Set;let r=b(t);for(;;){i.has(r)||(e.push(r),i.add(r));const n=w(r);if(n===r)break;r=n}return e}function $(t){const e=s(t,".claude","plugins","installed_plugins.json");if(!d(e))return[];const i=[];try{const r=g(e,"utf-8"),c=JSON.parse(r)?.plugins;if(!c||typeof c!="object")return i;for(const[f,a]of Object.entries(c))if(Array.isArray(a))for(const u of a){const p=u?.installPath;if(!p||!d(p))continue;const o=s(p,"skills"),m=f.split("@")[0],h=O(o,"plugin",m);i.push(...h)}}catch{}return i}function A(t){const e=[],i=t.homeDir??F();switch(t.mode){case"claude":{const n=s(i,".claude","skills");e.push(...l(n,"global")),t.projectDir&&e.push(...l(s(t.projectDir,".claude","skills"),"project")),e.push(...$(i));break}case"codex":{const n=process.env.CODEX_HOME?.trim()||s(i,".codex");if(t.projectDir)for(const c of N(t.projectDir))e.push(...l(s(c,".agents","skills"),"project")),e.push(...l(s(c,".codex","skills"),"project"));e.push(...l(s(i,".agents","skills"),"codex")),e.push(...l(s(n,"skills"),"codex")),e.push(...l(s(n,"skills",".system"),"codex"));break}case"gemini":{e.push(...l(s(i,".gemini","skills"),"gemini")),e.push(...l(s(i,".agents","skills"),"gemini")),t.projectDir&&(e.push(...l(s(t.projectDir,".gemini","skills"),"project")),e.push(...l(s(t.projectDir,".agents","skills"),"project")));break}case"pi":{e.push(...l(s(i,".pi","agent","skills"),"pi")),t.projectDir&&e.push(...l(s(t.projectDir,".pi","skills"),"project"));break}case"kiro":{e.push(...l(s(i,".kiro","skills"),"kiro")),t.projectDir&&e.push(...l(s(t.projectDir,".kiro","skills"),"project"));break}}const r=W(e);return L.info("skill-scanner",`Scanned skills: mode=${t.mode} count=${r.length}`),r}export{W as dedupeSkills,D as parseSkillFrontmatter,l as scanSkillTree,A as scanSkills};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{createReadStream as T,readFileSync as _}from"node:fs";import{createInterface as j}from"node:readline";import m from"node:path";import I from"node:os";import{log as d}from"../../core/log/index.js";function b(t){return t.replace(/[/\\]/g,"-").replace(/:/g,"-")}function h(t,e){const i=m.join(I.homedir(),".claude"),n=m.join(i,"projects",b(e));return m.join(n,`${t}.jsonl`)}function y(){return{inputTokens:0,outputTokens:0,cacheReadInputTokens:0,cacheCreationInputTokens:0}}function k(t,e){t.inputTokens+=e.input_tokens??0,t.outputTokens+=e.output_tokens??0,t.cacheReadInputTokens+=e.cache_read_input_tokens??0,t.cacheCreationInputTokens+=e.cache_creation_input_tokens??0}async function J(t,e){const i=h(t,e);d.info("usage-parser",`Parsing session usage from ${i}`);const n=new Map,p=y();let u=0;try{const o=j({input:T(i,"utf8"),crlfDelay:1/0});for await(const s of o)if(s.trim())try{const c=JSON.parse(s);if(c.type!=="assistant")continue;const a=c.message?.usage;if(!a)continue;const l=c.message?.model??"unknown";u++,k(p,a);let r=n.get(l);r||(r={turns:0,usage:y()},n.set(l,r)),r.turns++,k(r.usage,a)}catch{}}catch(o){return o.code==="ENOENT"?(d.info("usage-parser",`Session JSONL not found: ${i}`),null):(d.error("usage-parser",`Failed to parse session usage: ${o}`),null)}return u===0?null:{models:[...n.entries()].map(([o,s])=>({model:o,turns:s.turns,total:s.usage})),total:p,turns:u}}const x=64*1024;function O(t,e){const i=h(t,e);try{const n=_(i),f=(n.length>x?n.subarray(n.length-x):n).toString("utf8").split(`
|
|
2
|
+
`);for(let o=f.length-1;o>=0;o--){const s=f[o].trim();if(!s)continue;let c;try{c=JSON.parse(s)}catch{continue}if(c.type!=="assistant")continue;const a=c.message?.content;if(!a)continue;const l=Array.isArray(a)?a:[{type:"text",text:String(a)}],r=[];for(const S of l){const g=S;g.type==="text"&&g.text?.trim()&&r.push(g.text)}if(r.length>0)return r.join(`
|
|
3
|
+
`)}return null}catch{return null}}export{O as extractLastAssistantText,J as parseClaudeSessionUsage,h as resolveSessionJsonlPath};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import{resolveCommandPath as b,spawnCommand as T}from"../../core/runtime/spawn.js";import{createInterface as w}from"node:readline";import{EventEmitter as I}from"node:events";import{formatInboundMessageReferenceText as _}from"../../core/protocol/message-reference.js";import{log as n}from"../../core/log/index.js";import{SessionBindingStore as C}from"../../core/persistence/session-binding-store.js";const S=120*1e3;class L extends I{type="codewhale";config;callbacks;alive=!1;stopped=!1;deepSeekSessionId=null;activeEventId=null;activeSessionId=null;chunkSeq=0;activeClientMsgId=null;idleTimer=null;activeProcess=null;composingTimer=null;composingTTLClear=null;composingTTL=12e4;composingRefreshInterval=3e4;bindingStore=null;aibotSessionId="";cwd;lastUsage=null;currentModel=null;constructor(e,s){super(),this.config=e,this.callbacks=s;const i=e.options??{};if(this.aibotSessionId=String(i.aibotSessionId??"").trim(),this.bindingStore=i.bindingStore instanceof C?i.bindingStore:null,this.cwd=this.resolveCwd(),this.bindingStore&&this.aibotSessionId){const o=this.bindingStore.getCodeWhaleThreadId(this.aibotSessionId);o&&(this.deepSeekSessionId=o)}}resolveCwd(){if(this.bindingStore&&this.aibotSessionId){const e=this.bindingStore.get(this.aibotSessionId);if(e?.cwd)return e.cwd}return process.cwd()}async start(){this.alive=!0,this.notifyBindingReady(),n.info("codewhale-adapter","Ready (exec mode)")}async stop(){this.stopped=!0,this.alive=!1,this.stopComposing(),this.clearIdleTimer(),this.killActiveProcess()}isAlive(){return this.alive}async createSession(e){const s=this.deepSeekSessionId??`ds-${Date.now()}`;return this.notifyBindingReady(),s}async resumeSession(e,s){}async destroySession(e){this.deepSeekSessionId=null,this.persistSessionId(void 0)}sendPrompt(e){const s=new v(e.adapterSessionId);return this.runMessage(e,s).catch(i=>{s.emitError(i instanceof Error?i:new Error(String(i)))}),s}async cancel(e){this.killActiveProcess()}setPermissionHandler(e){}async ping(e){return this.alive}getStatus(){return{alive:this.alive,busy:this.activeEventId!==null,sessions:this.deepSeekSessionId?1:0}}getActiveEventIds(){return this.activeEventId?[this.activeEventId]:[]}clearActiveEventForShutdown(){this.clearIdleTimer(),this.killActiveProcess(),this.activeEventId=null}getMcpConfig(){return null}getUsageSnapshot(){return this.lastUsage}getSupportedCommands(){return[{name:"status",description:"Show session and working directory status"}]}async execCommand(e,s,i){return e==="status"?{status:"ok",message:`Session: ${this.deepSeekSessionId??"none"}, CWD: ${this.cwd}`,data:{sessionId:this.deepSeekSessionId,cwd:this.cwd,alive:this.alive}}:{status:"unsupported",message:`Unknown command: ${e}`}}async handleLocalAction(e){const s=e.action_type??"",i=e.params??{};switch(s){case"get_context":return this.callbacks.sendLocalActionResult(e.action_id,"ok",{sessionId:this.deepSeekSessionId,cwd:this.cwd,model:this.currentModel}),{handled:!0,kind:"get_context"};case"set_model":{const o=String(i.model_id??"").trim();return o?(this.currentModel=o,this.callbacks.sendLocalActionResult(e.action_id,"ok",{outcome:"model_set",modelId:o}),n.info("codewhale-adapter",`Model set to: ${o}`),{handled:!0,kind:"set_model"}):(this.callbacks.sendLocalActionResult(e.action_id,"failed",void 0,"invalid_params","model_id is required"),{handled:!0,kind:"set_model"})}default:return{handled:!1,kind:""}}}deliverInboundEvent(e){const s=_(e.content,{messageId:e.msg_id,quotedMessageId:e.quoted_message_id});if(this.activeEventId){n.info("codewhale-adapter",`Event ${e.event_id}: rejected, busy with ${this.activeEventId}`),this.callbacks.sendEventResult(e.event_id,"failed","agent busy");return}this.startNewMessage(e,s)}deliverStopEvent(e,s){this.activeEventId===e&&(this.callbacks.sendEventResult(e,"canceled","stopped by user"),this.clearActive())}startNewMessage(e,s){this.activeEventId=e.event_id,this.activeSessionId=e.session_id,this.chunkSeq=0,this.activeClientMsgId=`ds-${Date.now()}-${Math.random().toString(36).slice(2,8)}`,this.startComposing();const i={adapterSessionId:this.deepSeekSessionId??"",text:s,contextMessages:e.context_messages_json?JSON.parse(e.context_messages_json).map(t=>({senderId:t.sender_id??"unknown",content:t.content})):void 0},o=new v(this.deepSeekSessionId??"");this.runMessage(i,o,e.event_id,e.session_id).catch(t=>{n.error("codewhale-adapter",`Message failed: ${t}`),this.callbacks.sendEventResult(e.event_id,"failed",t instanceof Error?t.message:String(t)),this.clearActive()}),this.resetIdleTimer(e.event_id)}buildExecArgs(e){const s=["exec","--output-format","stream-json"];return this.currentModel&&s.push("--model",this.currentModel),this.deepSeekSessionId&&s.push("--resume",this.deepSeekSessionId),s.push("--",e),s}async runMessage(e,s,i,o){let t=e.text;e.contextMessages&&e.contextMessages.length>0&&(t=`Conversation context:
|
|
2
|
+
${e.contextMessages.map(r=>`[${r.senderId??"unknown"}]: ${r.content}`).join(`
|
|
3
|
+
`)}
|
|
4
|
+
|
|
5
|
+
Latest user message:
|
|
6
|
+
${t}`);const a=this.config.command||"codewhale",h=this.buildExecArgs(t),d={...process.env,...this.config.env},f=b(a,typeof d.PATH=="string"?d.PATH:void 0);n.info("codewhale-adapter",`Spawning: ${f} ${h.slice(0,5).join(" ")}...`);const l=T(f,h,{cwd:this.cwd,env:d}).process;return this.activeProcess=l,l.stderr?.on("data",u=>{const r=u.toString().trim();r&&n.info("codewhale-adapter",`[codewhale stderr] ${r}`)}),new Promise((u,r)=>{let p=!1,m="";const g=()=>{this.activeProcess=null};l.on("error",c=>{p||(p=!0,g(),r(c))}),l.on("exit",c=>{if(m.trim()&&this.handleOutputLine(m.trim(),i),m="",p){g();return}if(p=!0,g(),c!==0&&i&&this.activeEventId===i){r(new Error(`codewhale exec exited with code ${c}`));return}s.emitDone({status:"completed"}),u()}),w({input:l.stdout}).on("line",c=>{c.trim()&&this.handleOutputLine(c.trim(),i)}),l.stdin?.end()})}handleOutputLine(e,s){let i;try{i=JSON.parse(e)}catch{n.error("codewhale-adapter",`Invalid JSON: ${e.slice(0,200)}`);return}switch(i.type){case"content":{const t=i.content;t&&s&&this.activeEventId===s&&this.activeSessionId&&(this.chunkSeq++,this.callbacks.sendStreamChunk(s,this.activeSessionId,t,this.chunkSeq,!1,this.activeClientMsgId??void 0),this.startComposing(),this.resetIdleTimer(s));break}case"session_capture":{const t=i.content;t&&(this.deepSeekSessionId=t,this.persistSessionId(t),n.info("codewhale-adapter",`Session captured: ${t}`));break}case"metadata":{const t=i.meta;if(t){n.info("codewhale-adapter",`Metadata: model=${t.model}, tokens_in=${t.input_tokens}, tokens_out=${t.output_tokens}`);const a=Number(t.input_tokens??0),h=Number(t.output_tokens??0);if(a>0||h>0){const d=this.lastUsage;this.lastUsage={sampledAt:new Date().toISOString(),turns:(d?.turns??0)+1,total:{input:(d?.total.input??0)+a,output:(d?.total.output??0)+h}}}}break}case"tool_use":{const t=i.name,a=typeof i.input=="string"?i.input:JSON.stringify(i.input??{});t&&s&&this.activeEventId===s&&this.activeSessionId&&(n.info("codewhale-adapter",`Tool use: ${t}`),this.callbacks.sendToolUse(s,this.activeSessionId,t,a),this.resetIdleTimer(s));break}case"tool_result":{const t=i.name,a=i.output;s&&this.activeEventId===s&&this.activeSessionId&&(this.callbacks.sendToolResult(s,this.activeSessionId,t??"unknown",a??""),this.resetIdleTimer(s));break}case"done":{this.handleMessageCompleted(s);break}default:break}}handleMessageCompleted(e){if(this.stopComposing(),e&&this.activeEventId===e){const s=this.activeSessionId??"",i=this.activeClientMsgId??void 0;s&&(this.chunkSeq++,this.callbacks.sendStreamChunk(e,s,"",this.chunkSeq,!0,i)),this.callbacks.sendEventResult(e,"responded"),this.clearActive()}}killActiveProcess(){const e=this.activeProcess;if(this.activeProcess=null,e?.pid)try{e.kill("SIGTERM")}catch{}}notifyBindingReady(){!this.aibotSessionId||!this.cwd||this.callbacks.sendUpdateBindingCard(this.aibotSessionId,"ready",this.cwd)}persistSessionId(e){!this.bindingStore||!this.aibotSessionId||this.bindingStore.setCodeWhaleThreadId(this.aibotSessionId,e)}startComposing(){if(!this.activeSessionId||this.composingTimer)return;this.stopComposing();const e=this.activeSessionId,s={ttl_ms:this.composingTTL};this.callbacks.sendSessionActivitySet(e,"composing",!0,s),this.composingTimer=setInterval(()=>{this.callbacks.sendSessionActivitySet(e,"composing",!0,s)},this.composingRefreshInterval),this.composingTTLClear=setTimeout(()=>{this.stopComposing()},this.composingTTL)}stopComposing(){this.composingTimer&&(clearInterval(this.composingTimer),this.composingTimer=null),this.composingTTLClear&&(clearTimeout(this.composingTTLClear),this.composingTTLClear=null),this.activeSessionId&&this.callbacks.sendSessionActivitySet(this.activeSessionId,"composing",!1)}resetIdleTimer(e){this.clearIdleTimer(),this.idleTimer=setTimeout(()=>{this.activeEventId===e&&(n.error("codewhale-adapter",`Agent idle for ${S/1e3}s: ${e}`),this.killActiveProcess(),this.callbacks.sendEventResult(e,"failed",`agent idle for ${S/1e3}s`),this.clearActive(),this.emit("stuck"))},S)}clearIdleTimer(){this.idleTimer&&(clearTimeout(this.idleTimer),this.idleTimer=null)}clearActive(){const e=this.activeEventId;this.stopComposing(),this.activeEventId=null,this.activeSessionId=null,this.chunkSeq=0,this.activeClientMsgId=null,this.clearIdleTimer(),e&&this.emit("eventDone",e)}}class v extends I{adapterSessionId;constructor(e){super(),this.adapterSessionId=e}emitDone(e){this.emit("done",e)}emitError(e){if(this.listenerCount("error")===0){n.warn("codewhale-adapter",`Prompt handle error (no listeners): ${e.message}`);return}this.emit("error",e)}async cancel(){}}export{L as CodeWhaleAdapter};
|