imtoagent 0.3.6 โ 0.3.8
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 +203 -56
- package/index.ts +1 -7
- package/modules/agent/codex-exec-server.ts +0 -4
- package/modules/proxy/codex-proxy.ts +1 -19
- package/package.json +5 -4
- package/modules/agent/claude.ts +0 -160
- package/modules/agent/codex.ts +0 -275
- package/modules/agent/opencode.ts +0 -247
- package/scripts/postinstall.ts +0 -70
package/README.md
CHANGED
|
@@ -4,12 +4,50 @@ Connect Feishu, Telegram, personal WeChat, and WeCom to AI coding agents like Cl
|
|
|
4
4
|
|
|
5
5
|
One gateway, multiple IMs, multiple agents, unified port proxy.
|
|
6
6
|
|
|
7
|
+
## ๐ Quick Start (5 Minutes)
|
|
8
|
+
|
|
9
|
+
### Step 1: Install (One Command)
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
curl -fsSL https://raw.githubusercontent.com/imtoagent/imtoagent/main/install.sh | bash
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This script detects your environment, installs bun if needed, installs imtoagent, and guides you through setup.
|
|
16
|
+
|
|
17
|
+
### Step 2: Start
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
imtoagent setup
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The interactive wizard guides you through:
|
|
24
|
+
1. Select IM platform (Feishu/Telegram/WeChat/WeCom)
|
|
25
|
+
2. Enter Bot credentials
|
|
26
|
+
3. Choose Agent backend (Claude Code/Codex/OpenCode)
|
|
27
|
+
4. Configure model providers (API keys)
|
|
28
|
+
5. Generate soul files for personality injection
|
|
29
|
+
|
|
30
|
+
### Step 2: Verify
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
imtoagent status # check it's running
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
That's it! Send `/help` to your Bot in the IM to see available commands.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
**Alternative install methods:** See [Installation Methods](#installation-methods) below for npm global install or source install.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
7
44
|
## Architecture
|
|
8
45
|
|
|
9
46
|
```
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
47
|
+
IM Platform (Feishu/Telegram/WeChat/WeCom)
|
|
48
|
+
โ IM Registry Factory โ Bot Instance
|
|
49
|
+
โ AgentRuntime SDK โ Agent Adapter
|
|
50
|
+
โ Unified Proxy (:18899) โ Upstream Models
|
|
13
51
|
```
|
|
14
52
|
|
|
15
53
|
### Supported IM Adapters
|
|
@@ -43,63 +81,115 @@ One gateway, multiple IMs, multiple agents, unified port proxy.
|
|
|
43
81
|
| Codex | `npm install -g @openai/codex` |
|
|
44
82
|
| OpenCode | `npm install -g opencode` |
|
|
45
83
|
|
|
46
|
-
|
|
84
|
+
## Installation Methods
|
|
47
85
|
|
|
48
|
-
|
|
86
|
+
### Method 1: One-Click Install (Recommended)
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
curl -fsSL https://raw.githubusercontent.com/imtoagent/imtoagent/main/install.sh | bash
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
This script does everything automatically:
|
|
93
|
+
- Detects your OS and environment
|
|
94
|
+
- Installs bun if missing
|
|
95
|
+
- Installs or upgrades imtoagent
|
|
96
|
+
- Runs the setup wizard if no configuration exists
|
|
97
|
+
- Starts the gateway and verifies it's running
|
|
98
|
+
|
|
99
|
+
**Flags:**
|
|
100
|
+
- `--non-interactive` โ Skip all prompts, auto-install and auto-start
|
|
101
|
+
- `--skip-bun` โ Skip bun installation check
|
|
102
|
+
- `--skip-start` โ Don't start the gateway after install
|
|
103
|
+
|
|
104
|
+
### Method 2: npm Global Install
|
|
49
105
|
|
|
50
106
|
```bash
|
|
51
107
|
npm install -g imtoagent
|
|
52
108
|
```
|
|
53
109
|
|
|
54
|
-
|
|
110
|
+
This is the simplest approach. After installation, the post-install script checks if you need initial configuration.
|
|
55
111
|
|
|
56
|
-
|
|
112
|
+
### Method 3: Source Install
|
|
57
113
|
|
|
58
114
|
```bash
|
|
59
|
-
git clone https://github.com/
|
|
115
|
+
git clone https://github.com/imtoagent/imtoagent.git
|
|
60
116
|
cd imtoagent
|
|
61
117
|
bun install
|
|
62
118
|
bun run bin/imtoagent setup
|
|
63
119
|
```
|
|
64
120
|
|
|
65
|
-
|
|
121
|
+
Use this for development or if you want to modify the source code.
|
|
122
|
+
|
|
123
|
+
### Prerequisites
|
|
124
|
+
|
|
125
|
+
- **Bun** runtime (โฅ1.0.0): `brew install oven-sh/bun/bun`
|
|
126
|
+
- **macOS or Linux**
|
|
127
|
+
- **At least one Agent backend** installed (see below)
|
|
128
|
+
|
|
129
|
+
### Agent Backend Installation
|
|
130
|
+
|
|
131
|
+
| Backend | Install Command |
|
|
132
|
+
|---------|----------------|
|
|
133
|
+
| Claude Code | `npm install -g @anthropic-ai/claude-agent-sdk` |
|
|
134
|
+
| Codex | `npm install -g @openai/codex` |
|
|
135
|
+
| OpenCode | `npm install -g opencode` |
|
|
136
|
+
|
|
137
|
+
You can install backends before or after installing imtoagent.
|
|
138
|
+
|
|
139
|
+
## Configuration
|
|
140
|
+
|
|
141
|
+
### First-Time Setup
|
|
66
142
|
|
|
67
143
|
```bash
|
|
68
144
|
imtoagent setup
|
|
69
145
|
```
|
|
70
146
|
|
|
71
|
-
The interactive
|
|
147
|
+
The interactive wizard will guide you through:
|
|
72
148
|
|
|
73
|
-
1. **Configure Bot** โ Select IM platform + Agent backend
|
|
149
|
+
1. **Configure Bot** โ Select IM platform + Agent backend combination
|
|
74
150
|
2. **Configure Model Providers** โ Add API credentials (DeepSeek, Dashscope, etc.)
|
|
75
|
-
3. **Generate Soul Files** โ Create rules.md
|
|
151
|
+
3. **Generate Soul Files** โ Create personality files (rules.md, identity.md, etc.)
|
|
76
152
|
4. **Write Config Files** โ Auto-generate `~/.imtoagent/config.json`
|
|
77
153
|
|
|
78
|
-
|
|
154
|
+
### Platform-Specific Requirements
|
|
79
155
|
|
|
80
|
-
|
|
81
|
-
-
|
|
82
|
-
- Feishu app
|
|
156
|
+
#### Feishu
|
|
157
|
+
- **App ID** (`cli_...`) and **App Secret**
|
|
158
|
+
- Enable in Feishu app console: Bot, Event Subscription, Message Send/Receive permissions
|
|
83
159
|
|
|
84
|
-
#### Telegram
|
|
85
|
-
|
|
86
|
-
-
|
|
87
|
-
- Optional: Proxy address (e.g., `http://127.0.0.1:7890`)
|
|
160
|
+
#### Telegram
|
|
161
|
+
- **Bot Token** from @BotFather
|
|
162
|
+
- Optional: HTTP proxy (e.g., `http://127.0.0.1:7890`)
|
|
88
163
|
|
|
89
164
|
#### Personal WeChat
|
|
165
|
+
- QR code automatically appears on first `imtoagent start`
|
|
166
|
+
- Scan with WeChat on your phone to complete binding
|
|
90
167
|
|
|
91
|
-
|
|
92
|
-
-
|
|
168
|
+
#### WeCom (Enterprise WeChat)
|
|
169
|
+
- Webhook callback URL configuration
|
|
170
|
+
- REST API credentials
|
|
93
171
|
|
|
94
|
-
|
|
172
|
+
## Running the Gateway
|
|
95
173
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
174
|
+
### Basic Commands
|
|
175
|
+
|
|
176
|
+
| Command | Description |
|
|
177
|
+
|---------|-------------|
|
|
178
|
+
| `imtoagent start` | Start gateway in background |
|
|
179
|
+
| `imtoagent stop` | Stop the gateway |
|
|
180
|
+
| `imtoagent status` | Check running status |
|
|
181
|
+
| `imtoagent restore` | Hot reload recovery |
|
|
182
|
+
| `imtoagent daemon` | Foreground daemon mode (crash auto-restart) |
|
|
183
|
+
|
|
184
|
+
### Running Modes
|
|
101
185
|
|
|
102
|
-
|
|
186
|
+
- **`start`** โ Background mode, terminal returns immediately
|
|
187
|
+
- **`run`** โ Foreground mode, real-time logs, Ctrl+C to stop
|
|
188
|
+
- **`daemon`** โ Foreground with auto-restart on crash
|
|
189
|
+
|
|
190
|
+
### Auto-Start on Boot
|
|
191
|
+
|
|
192
|
+
#### macOS (launchd)
|
|
103
193
|
|
|
104
194
|
```bash
|
|
105
195
|
# Create launchd configuration
|
|
@@ -134,36 +224,54 @@ cat > ~/Library/LaunchAgents/com.imtoagent.plist << 'EOF'
|
|
|
134
224
|
</plist>
|
|
135
225
|
EOF
|
|
136
226
|
|
|
137
|
-
# Load
|
|
227
|
+
# Load the service
|
|
138
228
|
launchctl load ~/Library/LaunchAgents/com.imtoagent.plist
|
|
139
229
|
```
|
|
140
230
|
|
|
141
|
-
|
|
231
|
+
#### Linux (systemd)
|
|
142
232
|
|
|
143
|
-
|
|
144
|
-
|------|------|
|
|
145
|
-
| `imtoagent setup` | Interactive setup wizard |
|
|
146
|
-
| `imtoagent start` | Start gateway in background |
|
|
147
|
-
| `imtoagent stop` | Stop the gateway |
|
|
148
|
-
| `imtoagent status` | Check running status |
|
|
149
|
-
| `imtoagent restore` | Hot reload recovery |
|
|
150
|
-
| `imtoagent daemon` | Foreground daemon mode (suitable for launchd/systemd) |
|
|
233
|
+
Create `/etc/systemd/system/imtoagent.service`:
|
|
151
234
|
|
|
152
|
-
|
|
235
|
+
```ini
|
|
236
|
+
[Unit]
|
|
237
|
+
Description=IMtoAgent Gateway
|
|
238
|
+
After=network.target
|
|
153
239
|
|
|
154
|
-
|
|
240
|
+
[Service]
|
|
241
|
+
Type=simple
|
|
242
|
+
User=youruser
|
|
243
|
+
WorkingDirectory=/home/youruser/.imtoagent
|
|
244
|
+
ExecStart=/usr/bin/bun run /usr/lib/node_modules/imtoagent/index.ts daemon
|
|
245
|
+
Restart=on-failure
|
|
246
|
+
RestartSec=5
|
|
247
|
+
|
|
248
|
+
[Install]
|
|
249
|
+
WantedBy=multi-user.target
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
systemctl daemon-reload
|
|
254
|
+
systemctl enable imtoagent
|
|
255
|
+
systemctl start imtoagent
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Using the Gateway
|
|
259
|
+
|
|
260
|
+
### Built-In Commands
|
|
261
|
+
|
|
262
|
+
Send these to your Bot in the IM chat:
|
|
155
263
|
|
|
156
264
|
| Command | Description |
|
|
157
|
-
|
|
158
|
-
| `/help` |
|
|
265
|
+
|---------|-------------|
|
|
266
|
+
| `/help` | Show available commands |
|
|
159
267
|
| `/status` | Gateway status |
|
|
160
268
|
| `/stats` | Usage statistics |
|
|
161
|
-
| `/model` | Switch model |
|
|
162
|
-
| `/providers` | View providers |
|
|
163
|
-
| `/memory` | View memory |
|
|
269
|
+
| `/model` | Switch AI model |
|
|
270
|
+
| `/providers` | View model providers |
|
|
271
|
+
| `/memory` | View memory status |
|
|
164
272
|
| `/soul` | Soul management |
|
|
165
|
-
| `/reload` | Reload |
|
|
166
|
-
| `/clear` | Clear session |
|
|
273
|
+
| `/reload` | Reload configuration |
|
|
274
|
+
| `/clear` | Clear conversation session |
|
|
167
275
|
| `/mode` | Switch mode (permission/auto/plan) |
|
|
168
276
|
| `/dir` | Switch working directory |
|
|
169
277
|
|
|
@@ -206,27 +314,66 @@ imtoagent/
|
|
|
206
314
|
|
|
207
315
|
## Data Directory
|
|
208
316
|
|
|
209
|
-
All runtime data is stored
|
|
317
|
+
All runtime data is stored in `~/.imtoagent/`:
|
|
210
318
|
|
|
211
319
|
```
|
|
212
320
|
~/.imtoagent/
|
|
213
321
|
โโโ config.json # Main config (Bot + providers + system)
|
|
214
|
-
โโโ providers.json # Model provider
|
|
215
|
-
โโโ opencode.json # OpenCode config
|
|
216
|
-
โโโ sessions/ #
|
|
322
|
+
โโโ providers.json # Model provider configurations
|
|
323
|
+
โโโ opencode.json # OpenCode-specific config
|
|
324
|
+
โโโ sessions/ # Conversation persistence
|
|
217
325
|
โโโ logs/ # Runtime logs
|
|
218
|
-
โโโ soul/ #
|
|
326
|
+
โโโ soul/ # Bot personality files
|
|
219
327
|
โโโ ClaudeBot/
|
|
220
328
|
โโโ CodexBot/
|
|
221
|
-
โโโ ...
|
|
329
|
+
โโโ ... # One directory per Bot
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
## Troubleshooting
|
|
333
|
+
|
|
334
|
+
### Common Issues
|
|
335
|
+
|
|
336
|
+
**Gateway won't start**
|
|
337
|
+
- Check `imtoagent status` for details
|
|
338
|
+
- Verify config with `cat ~/.imtoagent/config.json`
|
|
339
|
+
- Check logs: `cat ~/.imtoagent/logs/*.log`
|
|
340
|
+
|
|
341
|
+
**Setup wizard stuck**
|
|
342
|
+
- Ensure terminal supports interactive input
|
|
343
|
+
- Try running in a standard terminal (not IDE integrated)
|
|
344
|
+
|
|
345
|
+
**Bot not responding in IM**
|
|
346
|
+
- Verify credentials in config.json
|
|
347
|
+
- Check IM platform permissions (Feishu events, Telegram webhook, etc.)
|
|
348
|
+
- Ensure the gateway is running: `imtoagent status`
|
|
349
|
+
|
|
350
|
+
**Port 18899 already in use**
|
|
351
|
+
- Another service is using the proxy port
|
|
352
|
+
- Kill the existing process or change port in config
|
|
353
|
+
|
|
354
|
+
### Getting Help
|
|
355
|
+
|
|
356
|
+
- Check logs: `~/.imtoagent/logs/`
|
|
357
|
+
- Run `imtoagent status` for runtime information
|
|
358
|
+
- Open an issue on GitHub with logs attached
|
|
222
359
|
```
|
|
223
360
|
|
|
224
361
|
## Development
|
|
225
362
|
|
|
226
363
|
```bash
|
|
364
|
+
# Clone and setup development environment
|
|
365
|
+
git clone https://github.com/imtoagent/imtoagent.git
|
|
366
|
+
cd imtoagent
|
|
227
367
|
bun install
|
|
228
|
-
|
|
229
|
-
|
|
368
|
+
|
|
369
|
+
# Run directly
|
|
370
|
+
bun run index.ts
|
|
371
|
+
|
|
372
|
+
# Run setup wizard
|
|
373
|
+
bun run bin/imtoagent setup
|
|
374
|
+
|
|
375
|
+
# Run CLI from source
|
|
376
|
+
bun run bin/imtoagent status
|
|
230
377
|
```
|
|
231
378
|
|
|
232
379
|
## License
|
package/index.ts
CHANGED
|
@@ -72,8 +72,7 @@ registerIM('wechat', {
|
|
|
72
72
|
},
|
|
73
73
|
});
|
|
74
74
|
import { startAnthropicProxy, stopAnthropicProxy } from './modules/proxy/anthropic-proxy';
|
|
75
|
-
import {
|
|
76
|
-
import { initOpenCodeConfig } from './modules/agent/opencode';
|
|
75
|
+
import { initCodexProxyConfig } from './modules/proxy/codex-proxy';
|
|
77
76
|
import { checkRateLimit, setRateLimitConfig } from './modules/rate-limiter';
|
|
78
77
|
import { setCurrentBot } from './modules/bot-context';
|
|
79
78
|
import { getDataDir, getSessionsDir, getSoulDir, getBotKey, getRestoreMarkerPath } from './modules/utils/paths';
|
|
@@ -904,11 +903,6 @@ async function main() {
|
|
|
904
903
|
upstream: codexCfg.upstream || 'https://api.deepseek.com/v1/chat/completions',
|
|
905
904
|
apiKey,
|
|
906
905
|
});
|
|
907
|
-
const ocCfg = config.opencode || {};
|
|
908
|
-
initOpenCodeConfig({
|
|
909
|
-
serverUrl: ocCfg.serverUrl || 'http://localhost:4096',
|
|
910
|
-
defaultModel: ocCfg.defaultModel || { providerID: 'anthropic', modelID: 'claude-sonnet-4-5' },
|
|
911
|
-
});
|
|
912
906
|
const rlCfg = config.rateLimit || {};
|
|
913
907
|
if (rlCfg.enabled !== false) {
|
|
914
908
|
setRateLimitConfig({
|
|
@@ -22,9 +22,6 @@ let _config: ExecServerConfig = {
|
|
|
22
22
|
maxToolCallsPerTurn: 80,
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
-
export function setExecServerConfig(cfg: Partial<ExecServerConfig>) {
|
|
26
|
-
_config = { ..._config, ...cfg };
|
|
27
|
-
}
|
|
28
25
|
|
|
29
26
|
// ================================================================
|
|
30
27
|
// ไบไปถ็ฑปๅ
|
|
@@ -510,4 +507,3 @@ export async function shutdownAppServer(): Promise<void> {
|
|
|
510
507
|
export const shutdownExecServer = shutdownAppServer;
|
|
511
508
|
|
|
512
509
|
// ๅๅๅ
ผๅฎน็็ฑปๅๅซๅ
|
|
513
|
-
export type CodexExecServerClient = CodexAppServerClient;
|
|
@@ -525,19 +525,10 @@ async function streamResponse(upstreamRes: Response, resWriter: WritableStreamDe
|
|
|
525
525
|
}
|
|
526
526
|
|
|
527
527
|
// ================================================================
|
|
528
|
-
//
|
|
529
|
-
// usage ็ดฏๅ ๅจ โ ไพ็ฝๅ
ณ่ฏปๅ Codex ็ Token/ๆๆฌ็ป่ฎก
|
|
528
|
+
// usage ็ดฏๅ ๅจ โ ๅ
้จ็ป่ฎก๏ผไพ accumulateProxyUsage ่ฎฐๅฝ
|
|
530
529
|
// ================================================================
|
|
531
530
|
let _proxyUsage = { inputTokens: 0, outputTokens: 0 };
|
|
532
531
|
|
|
533
|
-
export function getProxyUsage() {
|
|
534
|
-
return { ..._proxyUsage };
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
export function resetProxyUsage() {
|
|
538
|
-
_proxyUsage = { inputTokens: 0, outputTokens: 0 };
|
|
539
|
-
}
|
|
540
|
-
|
|
541
532
|
export function accumulateProxyUsage(inputTokens: number, outputTokens: number) {
|
|
542
533
|
_proxyUsage.inputTokens += inputTokens;
|
|
543
534
|
_proxyUsage.outputTokens += outputTokens;
|
|
@@ -646,12 +637,3 @@ export async function handleCodexRequest(
|
|
|
646
637
|
res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'internal error' })); return;
|
|
647
638
|
}
|
|
648
639
|
}
|
|
649
|
-
|
|
650
|
-
// ๅ
ผๅฎนๆงๅผ็จ๏ผไธๅๅฏๅจ็ฌ็ซๆๅกๅจ๏ผ
|
|
651
|
-
export function startCodexProxy(_port?: number): Promise<number> {
|
|
652
|
-
console.log('[Codex Proxy] Merged into port 18899');
|
|
653
|
-
return Promise.resolve(18899);
|
|
654
|
-
}
|
|
655
|
-
export function stopCodexProxy(): Promise<void> {
|
|
656
|
-
return Promise.resolve();
|
|
657
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "imtoagent",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
4
4
|
"description": "IM โ Agent ็ปไธ็ฝๅ
ณ โ ้ฃไนฆ/Telegram/ๅพฎไฟก/ไผไธๅพฎไฟกๅฏนๆฅ Claude Code/Codex/OpenCode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
},
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|
|
35
|
-
"url": "
|
|
35
|
+
"url": "https://github.com/imtoagent/imtoagent.git",
|
|
36
|
+
"directory": ""
|
|
36
37
|
},
|
|
37
38
|
"keywords": [
|
|
38
39
|
"im",
|
|
@@ -50,7 +51,7 @@
|
|
|
50
51
|
"author": "Keyi",
|
|
51
52
|
"license": "MIT",
|
|
52
53
|
"bugs": {
|
|
53
|
-
"url": "https://github.com/
|
|
54
|
+
"url": "https://github.com/imtoagent/imtoagent/issues"
|
|
54
55
|
},
|
|
55
|
-
"homepage": "https://github.com/
|
|
56
|
+
"homepage": "https://github.com/imtoagent/imtoagent#readme"
|
|
56
57
|
}
|
package/modules/agent/claude.ts
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
// Claude Agent ๆจกๅ
|
|
2
|
-
// ๅฏนๆฅ Claude Agent SDK๏ผ้่ฟ :18899 Proxy ่ฐ็จ Provider
|
|
3
|
-
|
|
4
|
-
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
5
|
-
import type { AgentContext } from '../types';
|
|
6
|
-
import { parseToBlocks, type UnifiedBlock } from '../capabilities';
|
|
7
|
-
import { buildSystemPrompt, resolveCapabilities } from '../prompt-builder';
|
|
8
|
-
import type { SDKMessage } from '@anthropic-ai/claude-agent-sdk';
|
|
9
|
-
|
|
10
|
-
// ================================================================
|
|
11
|
-
// ๅทฅๅ
ทๅฝๆฐ
|
|
12
|
-
// ================================================================
|
|
13
|
-
function resolveAlias(modelSpec: string): string {
|
|
14
|
-
const i = modelSpec.indexOf('/');
|
|
15
|
-
return i >= 0 ? modelSpec.slice(i + 1) : modelSpec;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function extractText(msg: SDKMessage): string | null {
|
|
19
|
-
if (msg.type !== 'assistant') return null;
|
|
20
|
-
const content = (msg as any).message?.content;
|
|
21
|
-
if (!Array.isArray(content)) return null;
|
|
22
|
-
return content.filter((b: any) => b.type === 'text').map((b: any) => b.text).join('') || null;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function extractToolInfo(msg: SDKMessage): { name: string; summary: string } | null {
|
|
26
|
-
if (msg.type !== 'assistant') return null;
|
|
27
|
-
const content = (msg as any).message?.content;
|
|
28
|
-
if (!Array.isArray(content)) return null;
|
|
29
|
-
const tool = content.find((b: any) => b.type === 'tool_use');
|
|
30
|
-
if (!tool?.name) return null;
|
|
31
|
-
const input = tool.input || {};
|
|
32
|
-
let summary = '';
|
|
33
|
-
if (['Read','Edit','Write'].includes(tool.name) && input.file_path) {
|
|
34
|
-
const p = String(input.file_path);
|
|
35
|
-
summary = p.includes('/') ? p.split('/').pop()! : p;
|
|
36
|
-
} else if (tool.name === 'Bash' && input.command) {
|
|
37
|
-
summary = String(input.command).trim().slice(0, 60);
|
|
38
|
-
}
|
|
39
|
-
return { name: tool.name, summary };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// ================================================================
|
|
43
|
-
// Claude ๆจกๅ็ฑป
|
|
44
|
-
// ================================================================
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* ๆณจๆ๏ผๆญคๆจกๅ็ฎๅไพ่ตๅฎฟไธป Bot ๅฎไพ็ๆนๆณ๏ผreply/sendProgress/addToolLog ็ญ๏ผใ
|
|
48
|
-
* Phase 2 ็ฌฌไธๆญฅๅ
ๅๆ ทๆๅ๏ผๅ็ปญ้ๆญฅ่งฃ่ฆไธบๅนฒๅๆฅๅฃใ
|
|
49
|
-
*/
|
|
50
|
-
export class ClaudeAgentModule {
|
|
51
|
-
private ctx: AgentContext;
|
|
52
|
-
|
|
53
|
-
constructor(ctx: AgentContext) {
|
|
54
|
-
this.ctx = ctx;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async handleMessage(chatId: string, text: string, session: any) {
|
|
58
|
-
session.generator.push({
|
|
59
|
-
type: 'user', message: { role: 'user', content: [{ type: 'text', text }] },
|
|
60
|
-
});
|
|
61
|
-
if (!session.running) this._startLoop(chatId);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
private async _startLoop(chatId: string) {
|
|
65
|
-
const ctx = this.ctx;
|
|
66
|
-
const session = ctx.sessions.get(chatId);
|
|
67
|
-
if (!session || session.running) return;
|
|
68
|
-
session.running = true;
|
|
69
|
-
console.log(`[${ctx.name}] Claude loop started chat=${chatId.slice(-8)}`);
|
|
70
|
-
|
|
71
|
-
try {
|
|
72
|
-
const modelSpec = ctx.activeModel;
|
|
73
|
-
const modelName = modelSpec.slice(modelSpec.indexOf('/') + 1);
|
|
74
|
-
const aliases = ctx.modelAliases;
|
|
75
|
-
const customEnv: Record<string, string> = {
|
|
76
|
-
...process.env,
|
|
77
|
-
ANTHROPIC_BASE_URL: 'http://localhost:18899',
|
|
78
|
-
ANTHROPIC_API_KEY: '', ANTHROPIC_AUTH_TOKEN: '', ANTHROPIC_MODEL: '',
|
|
79
|
-
ANTHROPIC_DEFAULT_SONNET_MODEL: resolveAlias(aliases.sonnet),
|
|
80
|
-
ANTHROPIC_DEFAULT_OPUS_MODEL: resolveAlias(aliases.opus),
|
|
81
|
-
ANTHROPIC_DEFAULT_HAIKU_MODEL: resolveAlias(aliases.haiku),
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
const queryOptions: any = {
|
|
85
|
-
cwd: session.cwd || ctx.defaultCwd,
|
|
86
|
-
maxTurns: 50, model: modelName,
|
|
87
|
-
permissionMode: session.permissionMode || 'bypassPermissions',
|
|
88
|
-
persistSession: true,
|
|
89
|
-
};
|
|
90
|
-
if (session.sdkSessionId) {
|
|
91
|
-
queryOptions.resume = session.sdkSessionId;
|
|
92
|
-
} else {
|
|
93
|
-
queryOptions.sessionId = crypto.randomUUID();
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const botName = ctx.name;
|
|
97
|
-
const systemPrompt = buildSystemPrompt({
|
|
98
|
-
imModule: ctx.imModule || null,
|
|
99
|
-
botName,
|
|
100
|
-
});
|
|
101
|
-
console.log(`[Claude] ๐ system prompt built (${systemPrompt.length} chars, bot=${botName})`);
|
|
102
|
-
queryOptions.systemPrompt = systemPrompt;
|
|
103
|
-
|
|
104
|
-
const q = query({
|
|
105
|
-
prompt: session.generator.generate(),
|
|
106
|
-
options: queryOptions, env: customEnv,
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
let fullResponse = '', toolCalls = 0;
|
|
110
|
-
let callInput = 0, callOutput = 0, callCost = 0, callDur = 0;
|
|
111
|
-
|
|
112
|
-
for await (const msg of q) {
|
|
113
|
-
const msgAny = msg as any;
|
|
114
|
-
if (msgAny.session_id && !session.sdkSessionId) {
|
|
115
|
-
session.sdkSessionId = msgAny.session_id;
|
|
116
|
-
ctx.persistSession(chatId, session);
|
|
117
|
-
}
|
|
118
|
-
const text = extractText(msg);
|
|
119
|
-
if (text) fullResponse += text;
|
|
120
|
-
const toolInfo = extractToolInfo(msg);
|
|
121
|
-
if (toolInfo) { toolCalls++; ctx.addToolLog(chatId, toolInfo); }
|
|
122
|
-
|
|
123
|
-
if (msg.type === 'result') {
|
|
124
|
-
const result = msg as any;
|
|
125
|
-
callInput = result.usage?.input_tokens || 0;
|
|
126
|
-
callOutput = result.usage?.output_tokens || 0;
|
|
127
|
-
callCost = result.total_cost_usd || 0;
|
|
128
|
-
callDur = result.duration_ms || 0;
|
|
129
|
-
|
|
130
|
-
ctx.accumulateStats(session, {
|
|
131
|
-
inputTokens: callInput, outputTokens: callOutput,
|
|
132
|
-
costUSD: callCost, durationMs: callDur,
|
|
133
|
-
numTurns: result.num_turns || 0,
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
if (result.subtype === 'error' || result.subtype === 'cancelled') {
|
|
137
|
-
await ctx.reply(chatId, `โ ${result.error || result.result || 'Unknown error'}`);
|
|
138
|
-
} else if (fullResponse) {
|
|
139
|
-
await ctx.sendFormattedReply(chatId, fullResponse);
|
|
140
|
-
} else {
|
|
141
|
-
await ctx.reply(chatId, `โ
Completed (${toolCalls} steps)`);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
ctx.flushToolLog(chatId);
|
|
145
|
-
const costStr = callCost > 0 ? `Cost $${callCost.toFixed(4)}\n` : '';
|
|
146
|
-
await ctx.sendProgress(chatId,
|
|
147
|
-
`โ
Completed (${toolCalls} steps)\nInput ${callInput.toLocaleString()} Token\nOutput ${callOutput.toLocaleString()} Token\n${costStr}Duration ${(callDur/1000).toFixed(1)}s`);
|
|
148
|
-
fullResponse = ''; toolCalls = 0;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
} catch (e: any) {
|
|
152
|
-
console.error(`[${ctx.name}] Claude error: ${e.message}`);
|
|
153
|
-
await ctx.reply(chatId, `โ ${e.message}`);
|
|
154
|
-
} finally {
|
|
155
|
-
session.running = false;
|
|
156
|
-
session.generator.close();
|
|
157
|
-
ctx.persistSession(chatId, session);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
package/modules/agent/codex.ts
DELETED
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
// Codex Agent ๆจกๅ
|
|
2
|
-
// ๅฏนๆฅ Codex CLI๏ผ้่ฟ :18899 Proxy ่ฐ็จ Provider
|
|
3
|
-
|
|
4
|
-
import { getProxyUsage, resetProxyUsage } from '../proxy/codex-proxy';
|
|
5
|
-
import { calculateCost } from '../proxy/anthropic-proxy';
|
|
6
|
-
import type { AgentContext } from '../types';
|
|
7
|
-
import { getAppServerManager, type AgentEvent } from './codex-exec-server';
|
|
8
|
-
// ================================================================
|
|
9
|
-
// ็ฑปๅ
|
|
10
|
-
// ================================================================
|
|
11
|
-
interface CodexJsonEvent {
|
|
12
|
-
type: string;
|
|
13
|
-
thread_id?: string;
|
|
14
|
-
item?: { type: string; text?: string; name?: string; arguments?: string; output?: string };
|
|
15
|
-
text?: string;
|
|
16
|
-
delta?: string;
|
|
17
|
-
message?: { content?: { type: string; text?: string }[] };
|
|
18
|
-
error?: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const TOOL_NAMES: Record<string, string> = {
|
|
22
|
-
Bash: 'Execute command', Read: 'Read file', Edit: 'Edit file', Write: 'Write file',
|
|
23
|
-
Glob: 'Search files', Grep: 'Search content', WebSearch: 'Web search', WebFetch: 'Fetch webpage',
|
|
24
|
-
NotebookEdit: 'Edit Notebook',
|
|
25
|
-
// Codex tool names
|
|
26
|
-
command_execution: 'Execute command', exec_command: 'Execute command', write_stdin: 'Write to stdin', update_plan: 'Update plan',
|
|
27
|
-
request_user_input: 'Request input', apply_patch: 'Apply patch', view_image: 'View image',
|
|
28
|
-
spawn_agent: 'Spawn agent', send_input: 'Send input', resume_agent: 'Resume agent',
|
|
29
|
-
wait_agent: 'Wait agent', close_agent: 'Close agent',
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
// ================================================================
|
|
33
|
-
// Codex CLI ่ฐ็จ
|
|
34
|
-
// ================================================================
|
|
35
|
-
function processCodexStream(stdout: string, onTool?: (name: string, args: Record<string, any>) => void): { threadId: string; response: string } {
|
|
36
|
-
let threadId = '', response = '';
|
|
37
|
-
for (const line of stdout.split('\n')) {
|
|
38
|
-
if (!line.trim()) continue;
|
|
39
|
-
try {
|
|
40
|
-
const evt: CodexJsonEvent = JSON.parse(line);
|
|
41
|
-
if (evt.type === 'thread.started' && evt.thread_id) {
|
|
42
|
-
threadId = evt.thread_id;
|
|
43
|
-
} else if (evt.type === 'item.completed') {
|
|
44
|
-
if (evt.item?.type === 'agent_message') {
|
|
45
|
-
response = (response ? response + '\n' : '') + (evt.item.text || '');
|
|
46
|
-
} else if (TOOL_NAMES[evt.item?.type || ''] && onTool) {
|
|
47
|
-
onTool(evt.item.type || 'unknown', { command: (evt.item as any).command || '' });
|
|
48
|
-
}
|
|
49
|
-
} else if (evt.type === 'error' || evt.type === 'thread.error') {
|
|
50
|
-
console.error(`[Codex] event error: ${(evt as any).message || (evt as any).error || JSON.stringify(evt)}`);
|
|
51
|
-
}
|
|
52
|
-
} catch {}
|
|
53
|
-
}
|
|
54
|
-
return { threadId, response };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async function spawnCodexExec(
|
|
58
|
-
cwd: string, prompt: string,
|
|
59
|
-
onTool?: (name: string, args: Record<string, any>) => void
|
|
60
|
-
): Promise<{ threadId: string; response: string }> {
|
|
61
|
-
console.error(`[Codex] spawnExec cwd=${cwd} prompt_len=${prompt.length}`);
|
|
62
|
-
const child = Bun.spawn(['codex', 'exec', '-p', 'imtoagent', '-s', 'danger-full-access',
|
|
63
|
-
'--skip-git-repo-check', '--json', prompt], {
|
|
64
|
-
cwd, stdout: 'pipe', stderr: 'pipe',
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
// Safe read stdout/stderr: catch subprocess kill exceptions, ensure reject carries Error object
|
|
68
|
-
let stdout = '', stderr = '';
|
|
69
|
-
try {
|
|
70
|
-
[stdout, stderr] = await Promise.all([
|
|
71
|
-
new Response(child.stdout).text().catch((e: any) => { throw new Error(`stdout read failed: ${e?.message || e}`); }),
|
|
72
|
-
new Response(child.stderr).text().catch((e: any) => { throw new Error(`stderr read failed: ${e?.message || e}`); }),
|
|
73
|
-
]);
|
|
74
|
-
} catch (ioErr: any) {
|
|
75
|
-
// Subprocess may have been killed, try to get exit code
|
|
76
|
-
try { child.kill('SIGKILL'); } catch {}
|
|
77
|
-
throw new Error(`codex exec I/O error: ${ioErr.message}`);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const code = await child.exited.catch(() => -1);
|
|
81
|
-
console.error(`[Codex] exec exit=${code} stdout_len=${stdout.length} stderr_len=${stderr.length}`);
|
|
82
|
-
const { threadId, response } = processCodexStream(stdout, onTool);
|
|
83
|
-
if (code !== 0 || !threadId) throw new Error(`codex exec exit ${code}: ${stderr.slice(0, 300)}`);
|
|
84
|
-
return { threadId, response };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async function spawnCodexResume(
|
|
88
|
-
cwd: string, threadId: string, prompt: string,
|
|
89
|
-
onTool?: (name: string, args: Record<string, any>) => void
|
|
90
|
-
): Promise<{ response: string }> {
|
|
91
|
-
console.error(`[Codex] spawnResume cwd=${cwd} threadId=${threadId.slice(-8)} prompt_len=${prompt.length}`);
|
|
92
|
-
const child = Bun.spawn(['codex', 'exec', 'resume', threadId,
|
|
93
|
-
'--dangerously-bypass-approvals-and-sandbox', '-c', 'model_provider=imtoagent', '-c', 'model=gpt-5.5', '--json', '--skip-git-repo-check', prompt], {
|
|
94
|
-
cwd, stdout: 'pipe', stderr: 'pipe',
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
let stdout = '', stderr = '';
|
|
98
|
-
try {
|
|
99
|
-
[stdout, stderr] = await Promise.all([
|
|
100
|
-
new Response(child.stdout).text().catch((e: any) => { throw new Error(`stdout read failed: ${e?.message || e}`); }),
|
|
101
|
-
new Response(child.stderr).text().catch((e: any) => { throw new Error(`stderr read failed: ${e?.message || e}`); }),
|
|
102
|
-
]);
|
|
103
|
-
} catch (ioErr: any) {
|
|
104
|
-
try { child.kill('SIGKILL'); } catch {}
|
|
105
|
-
throw new Error(`codex exec resume I/O error: ${ioErr.message}`);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const code = await child.exited.catch(() => -1);
|
|
109
|
-
console.error(`[Codex] resume exit=${code} stdout_len=${stdout.length} stderr_len=${stderr.length}`);
|
|
110
|
-
if (code !== 0) throw new Error(`codex exec resume exit ${code}: ${stderr.slice(0, 300)}`);
|
|
111
|
-
return { response: processCodexStream(stdout, onTool).response };
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// ================================================================
|
|
115
|
-
// App-Server ่ทฏๅพ๏ผไผๅ
ไฝฟ็จโโๆตๅผ่พๅบ + ้ฟ่ฎฐๅฟ + ๅดฉๆบไธไธขไธไธๆ๏ผ
|
|
116
|
-
// ================================================================
|
|
117
|
-
async function runViaAppServer(
|
|
118
|
-
cwd: string, prompt: string, chatId: string, session: any,
|
|
119
|
-
onTool: (name: string, args: Record<string, any>) => void,
|
|
120
|
-
isFresh: boolean
|
|
121
|
-
): Promise<{ threadId: string; response: string; usage: { inputTokens: number; outputTokens: number } }> {
|
|
122
|
-
const manager = getAppServerManager();
|
|
123
|
-
const client = await manager.getClient(chatId);
|
|
124
|
-
|
|
125
|
-
// app-server ๅ่ฟ็จๅ
็บฟ็จๅญๆดป
|
|
126
|
-
// ไฝ่ฟ็จ้ๅฏๅๆง thread ่ฟๆ๏ผ้่ฆๅคๆญไปฃ้
|
|
127
|
-
const currentGen = manager.generation;
|
|
128
|
-
const threadExpired = session._appServerGen !== currentGen;
|
|
129
|
-
if (isFresh || !session.codexThreadId || threadExpired) {
|
|
130
|
-
session.codexThreadId = await client.startThread(cwd);
|
|
131
|
-
session._appServerGen = currentGen;
|
|
132
|
-
console.log(`[Codex] app-server new thread=${session.codexThreadId.slice(-8)}${threadExpired ? ' (process restarted)' : ''}`);
|
|
133
|
-
}
|
|
134
|
-
// ๅ็ปญๆถๆฏ็ดๆฅ turn/start๏ผๅ็บฟ็จๅปถ็ปญไธไธๆ๏ผ
|
|
135
|
-
|
|
136
|
-
await client.sendPrompt(session.codexThreadId, prompt, cwd);
|
|
137
|
-
|
|
138
|
-
let response = '';
|
|
139
|
-
let totalUsage = { inputTokens: 0, outputTokens: 0 };
|
|
140
|
-
const startTime = Date.now();
|
|
141
|
-
const MAX_DURATION = 600_000; // 10 ๅ้ๆป่ถ
ๆถ
|
|
142
|
-
|
|
143
|
-
for await (const event of client.receiveEvents()) {
|
|
144
|
-
// ่ถ
ๆถไฟๆค
|
|
145
|
-
if (Date.now() - startTime > MAX_DURATION) {
|
|
146
|
-
console.error('[Codex] app-server task timed out (10min)');
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
switch (event.type) {
|
|
151
|
-
case 'text_delta':
|
|
152
|
-
response += event.textDelta || '';
|
|
153
|
-
break;
|
|
154
|
-
case 'tool_call':
|
|
155
|
-
onTool(event.toolName || 'unknown', event.toolInput || {});
|
|
156
|
-
break;
|
|
157
|
-
case 'turn_result':
|
|
158
|
-
// ็ดฏๅ ๅค่ฝฎ token๏ผ้็ป็ซฏ turn_result ๆฅ่ชๆฏ่ฝฎ็ turn/completed๏ผ
|
|
159
|
-
totalUsage.inputTokens += event.usage?.inputTokens || 0;
|
|
160
|
-
totalUsage.outputTokens += event.usage?.outputTokens || 0;
|
|
161
|
-
break;
|
|
162
|
-
case 'error':
|
|
163
|
-
throw new Error(`app-server error: ${event.error}`);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return { threadId: session.codexThreadId, response, usage: totalUsage };
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// ================================================================
|
|
171
|
-
// Codex ๆจกๅ็ฑป
|
|
172
|
-
// ================================================================
|
|
173
|
-
export class CodexAgentModule {
|
|
174
|
-
private ctx: AgentContext;
|
|
175
|
-
|
|
176
|
-
constructor(ctx: AgentContext) {
|
|
177
|
-
this.ctx = ctx;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
async handleMessage(chatId: string, text: string, session: any) {
|
|
181
|
-
const ctx = this.ctx;
|
|
182
|
-
const cwd = session.cwd || ctx.defaultCwd;
|
|
183
|
-
console.log(`[${ctx.name}] Codex chat=${chatId.slice(-8)} startFresh=${session.startFresh || false}`);
|
|
184
|
-
|
|
185
|
-
const onTool = (name: string, args: Record<string, any>) => {
|
|
186
|
-
const cmd = args.cmd || args.command || '';
|
|
187
|
-
const justification = args.justification || args.description || '';
|
|
188
|
-
const summary = cmd ? cmd.slice(0, 80) : justification.slice(0, 80);
|
|
189
|
-
ctx.addToolLog(chatId, { name, summary });
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
resetProxyUsage();
|
|
193
|
-
try {
|
|
194
|
-
let effectiveText = text;
|
|
195
|
-
if (session.codexMode === 'plan') {
|
|
196
|
-
effectiveText = `[Mode: Plan then execute] Please create a clear plan first, wait for my confirmation before executing. User request: ${text}`;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const isFresh = session.startFresh || !session.codexThreadId;
|
|
200
|
-
let response: string;
|
|
201
|
-
let execServerUsage: { inputTokens: number; outputTokens: number } | null = null;
|
|
202
|
-
|
|
203
|
-
session.startFresh = false;
|
|
204
|
-
await ctx.sendProgress(chatId, '๐ญ Thinking...');
|
|
205
|
-
|
|
206
|
-
// ไผๅ
ๅฐ่ฏ app-server
|
|
207
|
-
console.error(`[${ctx.name}] DEBUG entering app-server branch, isFresh=${isFresh}, threadId=${session.codexThreadId?.slice(-8)}`);
|
|
208
|
-
let useExecFallback = false;
|
|
209
|
-
try {
|
|
210
|
-
const r = await runViaAppServer(cwd, effectiveText, chatId, session, onTool, isFresh);
|
|
211
|
-
response = r.response;
|
|
212
|
-
execServerUsage = r.usage;
|
|
213
|
-
} catch (appErr: any) {
|
|
214
|
-
const errMsg = appErr.message || '';
|
|
215
|
-
console.error(`[${ctx.name}] app-server failed: ${errMsg}`);
|
|
216
|
-
|
|
217
|
-
// thread not found โ app-server ่ฟ็จๅ
็บฟ็จไธขไบ๏ผๅฐ่ฏ้ๆฐๅๅปบ
|
|
218
|
-
if (errMsg.includes('thread not found') || errMsg.includes('Thread not found')) {
|
|
219
|
-
try {
|
|
220
|
-
session.codexThreadId = undefined;
|
|
221
|
-
const r2 = await runViaAppServer(cwd, effectiveText, chatId, session, onTool, true);
|
|
222
|
-
response = r2.response;
|
|
223
|
-
execServerUsage = r2.usage;
|
|
224
|
-
console.error(`[${ctx.name}] app-server thread rebuilt successfully`);
|
|
225
|
-
} catch {
|
|
226
|
-
useExecFallback = true;
|
|
227
|
-
}
|
|
228
|
-
} else {
|
|
229
|
-
useExecFallback = true;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (useExecFallback) {
|
|
234
|
-
getAppServerManager().removeClient(chatId);
|
|
235
|
-
if (isFresh || !session.codexThreadId) {
|
|
236
|
-
const r = await spawnCodexExec(cwd, effectiveText, onTool);
|
|
237
|
-
session.codexThreadId = r.threadId;
|
|
238
|
-
response = r.response;
|
|
239
|
-
console.log(`[${ctx.name}] Fresh session thread=${r.threadId.slice(-8)}`);
|
|
240
|
-
} else {
|
|
241
|
-
const r = await spawnCodexResume(cwd, session.codexThreadId, effectiveText, onTool);
|
|
242
|
-
response = r.response;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
ctx.flushToolLog(chatId);
|
|
246
|
-
|
|
247
|
-
// ไผๅ
ไฝฟ็จ app-server ่ฟๅ็ usage๏ผๅฆๅไป proxy ่ทๅ
|
|
248
|
-
const usage = execServerUsage || getProxyUsage();
|
|
249
|
-
if (usage.inputTokens > 0 || usage.outputTokens > 0) {
|
|
250
|
-
const cost = calculateCost(ctx.activeModel, usage.inputTokens, usage.outputTokens);
|
|
251
|
-
ctx.accumulateStats(session, { ...usage, costUSD: cost });
|
|
252
|
-
await ctx.sendProgress(chatId,
|
|
253
|
-
`Input ${usage.inputTokens.toLocaleString()} Token\nOutput ${usage.outputTokens.toLocaleString()} Token\nCost $${cost.toFixed(4)}`);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
if (response) {
|
|
257
|
-
await ctx.sendFormattedReply(chatId, response);
|
|
258
|
-
}
|
|
259
|
-
else await ctx.reply(chatId, 'โ
Completed');
|
|
260
|
-
ctx.persistSession(chatId, session);
|
|
261
|
-
} catch (e: any) {
|
|
262
|
-
console.error(`[${ctx.name}] Codex error: ${e.message}`);
|
|
263
|
-
session.codexThreadId = undefined;
|
|
264
|
-
try {
|
|
265
|
-
const r = await spawnCodexExec(cwd, text, onTool);
|
|
266
|
-
session.codexThreadId = r.threadId;
|
|
267
|
-
if (r.response) {
|
|
268
|
-
await ctx.sendFormattedReply(chatId, r.response);
|
|
269
|
-
}
|
|
270
|
-
} catch (e2: any) {
|
|
271
|
-
await ctx.reply(chatId, `โ Processing failed: ${e2.message}`);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
// OpenCode Agent ๆจกๅ
|
|
2
|
-
// ๅฏนๆฅ opencode serve HTTP API๏ผ้่ฟ :18899 Anthropic Proxy ่ฐ Provider
|
|
3
|
-
//
|
|
4
|
-
// ่ฎพ่ฎก๏ผ่ๆจกๅ โ oc serve ็ฎก็ session/ๅทฅๅ
ท/provider๏ผGateway ๅชๅ IM ็ฟป่ฏ
|
|
5
|
-
|
|
6
|
-
import type { AgentContext, SessionData } from '../types';
|
|
7
|
-
import { parseToBlocks } from '../capabilities';
|
|
8
|
-
import { resolveCapabilities, buildSystemPrompt } from '../prompt-builder';
|
|
9
|
-
import { calculateCost } from '../proxy/anthropic-proxy';
|
|
10
|
-
import * as path from 'path';
|
|
11
|
-
import * as fs from 'fs';
|
|
12
|
-
import { getDataDir } from '../utils/paths';
|
|
13
|
-
|
|
14
|
-
// ================================================================
|
|
15
|
-
// ้
็ฝฎ๏ผไป config.json ่ฏปๅ๏ผ
|
|
16
|
-
// ================================================================
|
|
17
|
-
|
|
18
|
-
interface OpenCodeConfig {
|
|
19
|
-
serverUrl: string;
|
|
20
|
-
defaultModel: { providerID: string; modelID: string };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
let _ocConfig: OpenCodeConfig | null = null;
|
|
24
|
-
|
|
25
|
-
export function initOpenCodeConfig(cfg: OpenCodeConfig) {
|
|
26
|
-
_ocConfig = cfg;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function getOcConfig(): OpenCodeConfig {
|
|
30
|
-
if (!_ocConfig) {
|
|
31
|
-
try {
|
|
32
|
-
|
|
33
|
-
const raw = JSON.parse(fs.readFileSync(path.join(getDataDir(), 'config.json'), 'utf-8'));
|
|
34
|
-
const oc = raw.opencode || {};
|
|
35
|
-
_ocConfig = {
|
|
36
|
-
serverUrl: oc.serverUrl || 'http://localhost:4096',
|
|
37
|
-
defaultModel: oc.defaultModel || { providerID: 'anthropic', modelID: 'claude-sonnet-4-5' },
|
|
38
|
-
};
|
|
39
|
-
} catch {
|
|
40
|
-
_ocConfig = {
|
|
41
|
-
serverUrl: 'http://localhost:4096',
|
|
42
|
-
defaultModel: { providerID: 'anthropic', modelID: 'claude-sonnet-4-5' },
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return _ocConfig;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const OC_SERVER_URL = () => getOcConfig().serverUrl;
|
|
50
|
-
const OC_DEFAULT_MODEL = () => getOcConfig().defaultModel;
|
|
51
|
-
|
|
52
|
-
// ================================================================
|
|
53
|
-
// OpenCode Server HTTP ๅฎขๆท็ซฏ
|
|
54
|
-
// ================================================================
|
|
55
|
-
|
|
56
|
-
interface OcMessagePart {
|
|
57
|
-
type: string;
|
|
58
|
-
text?: string;
|
|
59
|
-
tool_call?: { name: string; arguments: Record<string, any> };
|
|
60
|
-
tool_result?: { content: string };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
interface OcMessage {
|
|
64
|
-
info: { id: string; role: string; model?: string };
|
|
65
|
-
parts: OcMessagePart[];
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async function ocCreateSession(title: string): Promise<string> {
|
|
69
|
-
const ac = new AbortController();
|
|
70
|
-
const timer = setTimeout(() => ac.abort(), 300_000);
|
|
71
|
-
const res = await fetch(`${OC_SERVER_URL()}/session`, {
|
|
72
|
-
method: 'POST',
|
|
73
|
-
headers: { 'Content-Type': 'application/json' },
|
|
74
|
-
body: JSON.stringify({ title }),
|
|
75
|
-
signal: ac.signal,
|
|
76
|
-
}).finally(() => clearTimeout(timer));
|
|
77
|
-
if (!res.ok) throw new Error(`oc create session: ${res.status} ${await res.text()}`);
|
|
78
|
-
const data = await res.json();
|
|
79
|
-
return data.id;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async function ocSendPrompt(
|
|
83
|
-
sessionId: string,
|
|
84
|
-
initialText: string,
|
|
85
|
-
system?: string,
|
|
86
|
-
onTool?: (name: string, args: Record<string, any>) => void
|
|
87
|
-
): Promise<{ response: string }> {
|
|
88
|
-
const MAX_TURNS = 50;
|
|
89
|
-
const TURN_TIMEOUT = 300_000;
|
|
90
|
-
const startTime = Date.now();
|
|
91
|
-
const MAX_DURATION = 600_000; // 10 ๅ้ๆป่ถ
ๆถ
|
|
92
|
-
|
|
93
|
-
let promptText = initialText;
|
|
94
|
-
let accumulatedResponse = '';
|
|
95
|
-
let turn = 0;
|
|
96
|
-
|
|
97
|
-
while (turn < MAX_TURNS) {
|
|
98
|
-
if (Date.now() - startTime > MAX_DURATION) {
|
|
99
|
-
console.error('[OpenCode] Task timed out (10min)');
|
|
100
|
-
break;
|
|
101
|
-
}
|
|
102
|
-
turn++;
|
|
103
|
-
|
|
104
|
-
const body: any = {
|
|
105
|
-
model: OC_DEFAULT_MODEL(),
|
|
106
|
-
parts: [{ type: 'text', text: promptText }],
|
|
107
|
-
};
|
|
108
|
-
if (turn === 1 && system) body.system = system;
|
|
109
|
-
|
|
110
|
-
const ac = new AbortController();
|
|
111
|
-
const timer = setTimeout(() => ac.abort(), TURN_TIMEOUT);
|
|
112
|
-
const res = await fetch(`${OC_SERVER_URL()}/session/${sessionId}/message`, {
|
|
113
|
-
method: 'POST',
|
|
114
|
-
headers: { 'Content-Type': 'application/json' },
|
|
115
|
-
body: JSON.stringify(body),
|
|
116
|
-
signal: ac.signal,
|
|
117
|
-
}).finally(() => clearTimeout(timer));
|
|
118
|
-
if (!res.ok) throw new Error(`oc send prompt: ${res.status} ${await res.text()}`);
|
|
119
|
-
|
|
120
|
-
const data: OcMessage = await res.json();
|
|
121
|
-
let hasToolCall = false;
|
|
122
|
-
|
|
123
|
-
for (const part of data.parts || []) {
|
|
124
|
-
if (part.type === 'text' && part.text) {
|
|
125
|
-
accumulatedResponse += (accumulatedResponse ? '\n' : '') + part.text;
|
|
126
|
-
} else if (part.type === 'tool_call' && part.tool_call) {
|
|
127
|
-
hasToolCall = true;
|
|
128
|
-
if (onTool) onTool(part.tool_call.name, part.tool_call.arguments);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// ็บฏๆๆฌๅๅคๆๆฒกๆ tool_call โ ไปปๅกๅฎๆ
|
|
133
|
-
if (!hasToolCall) break;
|
|
134
|
-
|
|
135
|
-
// ๆ tool_call๏ผ็ปง็ปญๆจ่ฟ๏ผ็ฉบ prompt๏ผ
|
|
136
|
-
promptText = 'Continue';
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return { response: accumulatedResponse };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async function ocDeleteSession(sessionId: string): Promise<void> {
|
|
143
|
-
await fetch(`${OC_SERVER_URL()}/session/${sessionId}`, { method: 'DELETE' }).catch(() => {});
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
async function ocHealthCheck(): Promise<boolean> {
|
|
147
|
-
try {
|
|
148
|
-
const res = await fetch(`${OC_SERVER_URL()}/global/health`);
|
|
149
|
-
return res.ok;
|
|
150
|
-
} catch {
|
|
151
|
-
return false;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// ================================================================
|
|
156
|
-
// Agent ๆจกๅ็ฑป
|
|
157
|
-
// ================================================================
|
|
158
|
-
export class OpenCodeAgentModule {
|
|
159
|
-
private ctx: AgentContext;
|
|
160
|
-
|
|
161
|
-
constructor(ctx: AgentContext) {
|
|
162
|
-
this.ctx = ctx;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
async handleMessage(chatId: string, text: string, session: SessionData) {
|
|
166
|
-
const ctx = this.ctx;
|
|
167
|
-
console.log(`[${ctx.name}] OpenCode chat=${chatId.slice(-8)}`);
|
|
168
|
-
|
|
169
|
-
// ๅทฅๅ
ทๅ่ฐ
|
|
170
|
-
const onTool = (name: string, args: Record<string, any>) => {
|
|
171
|
-
const summary = args.command || args.cmd || args.query || JSON.stringify(args).slice(0, 80);
|
|
172
|
-
ctx.addToolLog(chatId, { name, summary });
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
try {
|
|
176
|
-
// โ Plan ๆจกๅผๅค็
|
|
177
|
-
let effectiveText = text;
|
|
178
|
-
if (session.codexMode === 'plan') {
|
|
179
|
-
effectiveText = `[Mode: Plan then execute] Please create a clear plan first, wait for my confirmation before executing. User request: ${text}`;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// โก ๆธ
็ๆ ่ฎฐ
|
|
183
|
-
const shouldClear = session.startFresh;
|
|
184
|
-
session.startFresh = false;
|
|
185
|
-
|
|
186
|
-
// โข ่ทๅๆๅๅปบ OpenCode session
|
|
187
|
-
if (shouldClear || !session.ocSessionId) {
|
|
188
|
-
if (session.ocSessionId) {
|
|
189
|
-
await ocDeleteSession(session.ocSessionId);
|
|
190
|
-
console.log(`[${ctx.name}] Cleared oc session=${session.ocSessionId.slice(-8)}`);
|
|
191
|
-
}
|
|
192
|
-
session.ocSessionId = await ocCreateSession(chatId);
|
|
193
|
-
console.log(`[${ctx.name}] Created oc session=${session.ocSessionId.slice(-8)}`);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// โฃ ๅ้่ฟๅบฆๆ็คบ
|
|
197
|
-
await ctx.sendProgress(chatId, '๐ญ Thinking...');
|
|
198
|
-
|
|
199
|
-
// โฃ.โค ๆๅปบ็ณป็ปๆ็คบ่ฏ
|
|
200
|
-
const systemPrompt = buildSystemPrompt({
|
|
201
|
-
imModule: ctx.imModule || null,
|
|
202
|
-
botName: ctx.name,
|
|
203
|
-
});
|
|
204
|
-
console.log(`[${ctx.name}] ๐ system prompt built (${systemPrompt.length} chars, bot=${ctx.name})`);
|
|
205
|
-
|
|
206
|
-
// โค ๅ้ prompt๏ผๅค่ฝฎๅพช็ฏ๏ผ
|
|
207
|
-
const { response } = await ocSendPrompt(
|
|
208
|
-
session.ocSessionId,
|
|
209
|
-
effectiveText,
|
|
210
|
-
systemPrompt,
|
|
211
|
-
onTool,
|
|
212
|
-
);
|
|
213
|
-
|
|
214
|
-
// โฅ ๅทๆฐๅทฅๅ
ทๆฅๅฟ
|
|
215
|
-
ctx.flushToolLog(chatId);
|
|
216
|
-
|
|
217
|
-
// โฆ ่พๅบ
|
|
218
|
-
if (response) {
|
|
219
|
-
await ctx.sendFormattedReply(chatId, response);
|
|
220
|
-
} else {
|
|
221
|
-
await ctx.reply(chatId, 'โ
Completed');
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// โง ็ป่ฎก
|
|
225
|
-
const { sharedState } = await import('../proxy/anthropic-proxy');
|
|
226
|
-
const lastUsage = (sharedState as any).lastCallUsage;
|
|
227
|
-
if (lastUsage && (lastUsage.inputTokens > 0 || lastUsage.outputTokens > 0)) {
|
|
228
|
-
const cost = calculateCost(ctx.activeModel, lastUsage.inputTokens, lastUsage.outputTokens);
|
|
229
|
-
ctx.accumulateStats(session, { ...lastUsage, costUSD: cost });
|
|
230
|
-
await ctx.sendProgress(chatId,
|
|
231
|
-
`Input ${lastUsage.inputTokens.toLocaleString()} Token\nOutput ${lastUsage.outputTokens.toLocaleString()} Token\nCost $${cost.toFixed(4)}`);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// โจ ๆไน
ๅไผ่ฏ
|
|
235
|
-
ctx.persistSession(chatId, session);
|
|
236
|
-
|
|
237
|
-
} catch (err: any) {
|
|
238
|
-
console.error(`[${ctx.name}] OpenCode error: ${err.message}`);
|
|
239
|
-
await ctx.reply(chatId, `โ ๏ธ OpenCode error: ${err.message}`);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/** ๅฅๅบทๆฃๆฅ */
|
|
244
|
-
async healthCheck(): Promise<boolean> {
|
|
245
|
-
return ocHealthCheck();
|
|
246
|
-
}
|
|
247
|
-
}
|
package/scripts/postinstall.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
// ================================================================
|
|
3
|
-
// postinstall.ts โ npm ๅฎ่ฃ
ๅๅผๅฏผ่ๆฌ
|
|
4
|
-
// ================================================================
|
|
5
|
-
// package.json ไธญ "scripts": { "postinstall": "bun run scripts/postinstall.ts" }
|
|
6
|
-
// ๅฎ่ฃ
ๅ่ชๅจ่ฟ่ก๏ผๆฃๆตๆฏๅฆ้่ฆๅๅงๅ้
็ฝฎ
|
|
7
|
-
// ๅฆๆๆฏๅ
จๆฐๅฎ่ฃ
ไธ็ป็ซฏไบคไบ๏ผ่ชๅจๅผๅฏผ่ฟๅ
ฅ setup
|
|
8
|
-
// ================================================================
|
|
9
|
-
|
|
10
|
-
import * as fs from 'fs';
|
|
11
|
-
import * as path from 'path';
|
|
12
|
-
import { execSync } from 'child_process';
|
|
13
|
-
|
|
14
|
-
const HOME = process.env.HOME || process.env.USERPROFILE || '';
|
|
15
|
-
const DATA_DIR = path.join(HOME, '.imtoagent');
|
|
16
|
-
|
|
17
|
-
try {
|
|
18
|
-
const configExists = fs.existsSync(path.join(DATA_DIR, 'config.json'));
|
|
19
|
-
|
|
20
|
-
if (configExists) {
|
|
21
|
-
console.log(`
|
|
22
|
-
โ
imtoagent upgraded successfully!
|
|
23
|
-
Data directory: ${DATA_DIR}
|
|
24
|
-
Configuration file kept as-is, no need to reconfigure.
|
|
25
|
-
Run "imtoagent start" to start the gateway.
|
|
26
|
-
`);
|
|
27
|
-
} else {
|
|
28
|
-
console.log(`
|
|
29
|
-
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
30
|
-
โ โ
|
|
31
|
-
โ ๐ imtoagent installed successfully! โ
|
|
32
|
-
โ โ
|
|
33
|
-
โ First-time use requires configuring IM credentials and a model provider โ
|
|
34
|
-
โ โ
|
|
35
|
-
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
36
|
-
`);
|
|
37
|
-
|
|
38
|
-
// ๆฃๆตๆฏๅฆไธบไบคไบๅผ็ป็ซฏ๏ผๆฏๅ่ชๅจๅผๅฏผ่ฟๅ
ฅ setup
|
|
39
|
-
if (process.stdin.isTTY) {
|
|
40
|
-
const readline = await import('readline');
|
|
41
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
42
|
-
const answer = await new Promise<string>(resolve => {
|
|
43
|
-
rl.question('Launch the configuration wizard now? [Y/n]: ', resolve);
|
|
44
|
-
});
|
|
45
|
-
rl.close();
|
|
46
|
-
|
|
47
|
-
const yes = answer.trim().toLowerCase();
|
|
48
|
-
if (yes === '' || yes === 'y' || yes === 'yes') {
|
|
49
|
-
console.log('\n๐ Launching configuration wizard...\n');
|
|
50
|
-
// ่ฐ็จ setup ๅๅฏผ
|
|
51
|
-
const pkgDir = path.resolve(import.meta.dirname, '..');
|
|
52
|
-
execSync('bun run bin/imtoagent setup', {
|
|
53
|
-
cwd: pkgDir,
|
|
54
|
-
stdio: 'inherit',
|
|
55
|
-
env: { ...process.env },
|
|
56
|
-
});
|
|
57
|
-
} else {
|
|
58
|
-
console.log('\nRun "imtoagent setup" later to configure.');
|
|
59
|
-
}
|
|
60
|
-
} else {
|
|
61
|
-
console.log(' Run "imtoagent setup" to start configuring');
|
|
62
|
-
console.log(' Then run "imtoagent start" to start the gateway\n');
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
} catch (e: any) {
|
|
66
|
-
// Silently fail, do not affect installation
|
|
67
|
-
if (e.message && !e.message.includes('readline')) {
|
|
68
|
-
console.error(`[postinstall] Failed to display message: ${e.message}`);
|
|
69
|
-
}
|
|
70
|
-
}
|