imtoagent 0.3.7 → 0.3.9

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 CHANGED
@@ -9,7 +9,7 @@ One gateway, multiple IMs, multiple agents, unified port proxy.
9
9
  ### Step 1: Install (One Command)
10
10
 
11
11
  ```bash
12
- curl -fsSL https://raw.githubusercontent.com/imtoagent/imtoagent/main/install.sh | bash
12
+ curl -fsSL https://imtoagent.pages.dev/install.sh | bash
13
13
  ```
14
14
 
15
15
  This script detects your environment, installs bun if needed, installs imtoagent, and guides you through setup.
@@ -86,7 +86,7 @@ IM Platform (Feishu/Telegram/WeChat/WeCom)
86
86
  ### Method 1: One-Click Install (Recommended)
87
87
 
88
88
  ```bash
89
- curl -fsSL https://raw.githubusercontent.com/imtoagent/imtoagent/main/install.sh | bash
89
+ curl -fsSL https://imtoagent.pages.dev/install.sh | bash
90
90
  ```
91
91
 
92
92
  This script does everything automatically:
@@ -64,6 +64,12 @@ switch (command) {
64
64
  }
65
65
  break;
66
66
  }
67
+ case 'version':
68
+ case '--version':
69
+ case '-v':
70
+ const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
71
+ console.log(pkg.version);
72
+ break;
67
73
  case 'help':
68
74
  case '--help':
69
75
  case '-h':
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 { getProxyUsage, resetProxyUsage, initCodexProxyConfig } from './modules/proxy/codex-proxy';
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
- }
@@ -59,7 +59,7 @@ export function getDataDir(): string {
59
59
  // 有现成配置,直接用最优先的那个
60
60
  const chosen = candidates[0];
61
61
  _dataDir = chosen.dir;
62
- console.log(`[Paths] Data directory: ${_dataDir} (${chosen.label})`);
62
+ console.error(`[Paths] Data directory: ${_dataDir} (${chosen.label})`);
63
63
  return _dataDir;
64
64
  }
65
65
 
@@ -119,8 +119,8 @@ function initDataDir(dotDir: string, envHome: string): string {
119
119
  }
120
120
  }
121
121
 
122
- console.log(`[Paths] ✨ First-time data directory initialized: ${target} (source: ${sourceLabel || 'default template'})`);
123
- console.log(`[Paths] Please edit ${path.join(target, 'config.json')} to configure your credentials`);
122
+ console.error(`[Paths] ✨ First-time data directory initialized: ${target} (source: ${sourceLabel || 'default template'})`);
123
+ console.error(`[Paths] Please edit ${path.join(target, 'config.json')} to configure your credentials`);
124
124
 
125
125
  return target;
126
126
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imtoagent",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
4
4
  "description": "IM ↔ Agent 统一网关 — 飞书/Telegram/微信/企业微信对接 Claude Code/Codex/OpenCode",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,7 +13,6 @@
13
13
  "templates/",
14
14
  "scripts/",
15
15
  "README.md",
16
- "install.sh",
17
16
  "bin/imtoagent.ts",
18
17
  "bin/imtoagent.cjs",
19
18
  "bin/imtoagent-real"
package/install.sh DELETED
@@ -1,313 +0,0 @@
1
- #!/usr/bin/env bash
2
- #
3
- # imtoagent — One-Click Install Script
4
- # Usage: curl -fsSL https://raw.githubusercontent.com/imtoagent/imtoagent/main/scripts/install.sh | bash
5
- #
6
- # This script detects the environment, installs dependencies, installs/upgrades
7
- # imtoagent, and optionally runs the interactive setup wizard.
8
- #
9
- # Flags:
10
- # --non-interactive Skip interactive setup (for CI/automated installs)
11
- # --skip-bun Skip bun installation check
12
- # --skip-start Don't start the gateway after install
13
-
14
- set -euo pipefail
15
-
16
- # ============================================================
17
- # Colors & Helpers
18
- # ============================================================
19
- RED='\033[0;31m'
20
- GREEN='\033[0;32m'
21
- YELLOW='\033[1;33m'
22
- BLUE='\033[0;34m'
23
- CYAN='\033[0;36m'
24
- BOLD='\033[1m'
25
- NC='\033[0m'
26
-
27
- info() { echo -e "${BLUE}ℹ ${NC}$1"; }
28
- ok() { echo -e "${GREEN}✅ ${NC}$1"; }
29
- warn() { echo -e "${YELLOW}⚠️ ${NC}$1"; }
30
- error() { echo -e "${RED}❌ ${NC}$1"; }
31
- step() { echo -e "\n${BOLD}${CYAN}▸ $1${NC}"; }
32
- done_ok() { echo -e " ${GREEN}✓${NC} $1"; }
33
-
34
- # ============================================================
35
- # Parse flags
36
- # ============================================================
37
- NON_INTERACTIVE=false
38
- SKIP_BUN=false
39
- SKIP_START=false
40
-
41
- for arg in "$@"; do
42
- case "$arg" in
43
- --non-interactive) NON_INTERACTIVE=true ;;
44
- --skip-bun) SKIP_BUN=true ;;
45
- --skip-start) SKIP_START=true ;;
46
- esac
47
- done
48
-
49
- # ============================================================
50
- # Banner
51
- # ============================================================
52
- echo ""
53
- echo -e "${BOLD} ┌──────────────────────────────────────────┐${NC}"
54
- echo -e "${BOLD} │ ${CYAN}imtoagent${NC} — IM ↔ Agent Unified Gateway${BOLD} │${NC}"
55
- echo -e "${BOLD} └──────────────────────────────────────────┘${NC}"
56
- echo ""
57
-
58
- # ============================================================
59
- # 1. OS Detection
60
- # ============================================================
61
- step "1. Detecting environment"
62
-
63
- OS=""
64
- ARCH=""
65
-
66
- case "$(uname -s)" in
67
- Darwin*) OS="macos" ;;
68
- Linux*) OS="linux" ;;
69
- *) error "Unsupported OS: $(uname -s)"; exit 1 ;;
70
- esac
71
-
72
- case "$(uname -m)" in
73
- arm64|aarch64) ARCH="aarch64" ;;
74
- x86_64|amd64) ARCH="x64" ;;
75
- *) ARCH="unknown" ;;
76
- esac
77
-
78
- done_ok "OS: $OS ($(uname -m))"
79
-
80
- # ============================================================
81
- # 2. Bun Detection & Installation
82
- # ============================================================
83
- if [ "$SKIP_BUN" = true ]; then
84
- info "Skipping bun check (--skip-bun)"
85
- else
86
- step "2. Checking bun"
87
-
88
- BUN_BIN=""
89
-
90
- # Check environment variable first
91
- if [ -n "${BUN_BIN:-}" ] && [ -x "$BUN_BIN" ]; then
92
- BUN_BIN="$BUN_BIN"
93
- # Check common paths
94
- elif [ -x "$HOME/.bun/bin/bun" ]; then
95
- BUN_BIN="$HOME/.bun/bin/bun"
96
- elif [ -x "/opt/homebrew/bin/bun" ]; then
97
- BUN_BIN="/opt/homebrew/bin/bun"
98
- elif [ -x "/usr/local/bin/bun" ]; then
99
- BUN_BIN="/usr/local/bin/bun"
100
- elif command -v bun &>/dev/null; then
101
- BUN_BIN="$(command -v bun)"
102
- fi
103
-
104
- if [ -n "$BUN_BIN" ]; then
105
- BUN_VER=$("$BUN_BIN" --version 2>/dev/null || echo "unknown")
106
- done_ok "bun found: $BUN_BIN (v${BUN_VER})"
107
- else
108
- warn "bun not found — installing..."
109
- echo ""
110
-
111
- if [ "$OS" = "macos" ] || [ "$OS" = "linux" ]; then
112
- echo " Downloading and installing bun..."
113
- curl -fsSL https://bun.sh/install | bash 2>&1 | tail -5
114
-
115
- # Add bun to PATH for this session
116
- if [ -f "$HOME/.bun/bin/bun" ]; then
117
- BUN_BIN="$HOME/.bun/bin/bun"
118
- export PATH="$HOME/.bun/bin:$PATH"
119
- elif command -v bun &>/dev/null; then
120
- BUN_BIN="$(command -v bun)"
121
- fi
122
-
123
- if [ -n "$BUN_BIN" ] && [ -x "$BUN_BIN" ]; then
124
- BUN_VER=$("$BUN_BIN" --version 2>/dev/null || echo "unknown")
125
- done_ok "bun installed: v${BUN_VER}"
126
- else
127
- error "bun installation failed. Please install manually:"
128
- echo " curl -fsSL https://bun.sh/install | bash"
129
- echo " Then re-run this script."
130
- exit 1
131
- fi
132
- else
133
- error "Unsupported platform for automatic bun install."
134
- echo " Install bun manually: https://bun.sh"
135
- exit 1
136
- fi
137
- fi
138
- fi
139
-
140
- # ============================================================
141
- # 3. Node.js Check (npm requires node)
142
- # ============================================================
143
- step "3. Checking node/npm"
144
-
145
- if command -v node &>/dev/null; then
146
- NODE_VER=$(node --version)
147
- done_ok "node: ${NODE_VER}"
148
- else
149
- warn "node not found — npm install will fail"
150
- echo ""
151
- echo " You need Node.js installed. Options:"
152
- echo " macOS: brew install node"
153
- echo " Linux: apt install nodejs npm (or use nvm)"
154
- echo ""
155
- echo " Or install via nvm:"
156
- echo " curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash"
157
- echo " source ~/.bashrc # or ~/.zshrc"
158
- echo " nvm install 20"
159
- echo ""
160
- if [ "$NON_INTERACTIVE" = true ]; then
161
- error "Cannot continue in non-interactive mode without node."
162
- exit 1
163
- fi
164
- read -rp " Continue anyway? [y/N] " CONTINUE
165
- case "$CONTINUE" in
166
- [yY]*) info "Continuing..." ;;
167
- *) error "Aborted."; exit 1 ;;
168
- esac
169
- fi
170
-
171
- if command -v npm &>/dev/null; then
172
- NPM_VER=$(npm --version)
173
- done_ok "npm: v${NPM_VER}"
174
- else
175
- error "npm not found — required for installation"
176
- exit 1
177
- fi
178
-
179
- # ============================================================
180
- # 4. Install / Upgrade imtoagent
181
- # ============================================================
182
- step "4. Installing imtoagent"
183
-
184
- # Check if already installed
185
- EXISTING_VER=""
186
- if command -v imtoagent &>/dev/null; then
187
- EXISTING_VER=$(imtoagent --version 2>/dev/null || echo "unknown")
188
- warn "Already installed: v${EXISTING_VER}"
189
- echo ""
190
- if [ "$NON_INTERACTIVE" = true ]; then
191
- info "Non-interactive mode — upgrading"
192
- else
193
- read -rp " Upgrade to latest? [Y/n] " UPGRADE
194
- case "$UPGRADE" in
195
- [nN]*) info "Skipping upgrade."; exit 0 ;;
196
- esac
197
- fi
198
- fi
199
-
200
- echo " Running: npm install -g imtoagent"
201
- echo ""
202
- npm install -g imtoagent 2>&1 | tail -10
203
-
204
- INSTALLED_VER=$(imtoagent --version 2>/dev/null || echo "unknown")
205
- done_ok "imtoagent v${INSTALLED_VER} installed globally"
206
-
207
- # ============================================================
208
- # 5. Configuration Check & Setup
209
- # ============================================================
210
- step "5. Checking configuration"
211
-
212
- CONFIG_DIR="$HOME/.imtoagent"
213
- CONFIG_FILE="$CONFIG_DIR/config.json"
214
-
215
- NEED_SETUP=true
216
-
217
- if [ -f "$CONFIG_FILE" ]; then
218
- done_ok "Config found: $CONFIG_FILE"
219
-
220
- # Quick check if config has any bots (grep for botType field)
221
- if grep -q '"botType"' "$CONFIG_FILE" 2>/dev/null; then
222
- BOT_COUNT=$(grep -c '"botType"' "$CONFIG_FILE" 2>/dev/null || echo "0")
223
- done_ok "$BOT_COUNT bot(s) configured — skipping setup wizard"
224
- NEED_SETUP=false
225
- else
226
- warn "No bots configured in config"
227
- fi
228
- else
229
- warn "No configuration found"
230
- fi
231
-
232
- if [ "$NEED_SETUP" = true ]; then
233
- if [ "$NON_INTERACTIVE" = true ]; then
234
- info "Non-interactive mode — skipping setup wizard"
235
- echo ""
236
- echo " Run 'imtoagent setup' manually to configure."
237
- else
238
- echo ""
239
- echo " Starting interactive setup wizard..."
240
- echo " You'll need:"
241
- echo " • IM platform credentials (Feishu App ID/Secret, Telegram Token, etc.)"
242
- echo " • Agent backend (Claude Code / Codex / OpenCode)"
243
- echo " • Model provider API keys"
244
- echo ""
245
- read -rp " Start setup now? [Y/n] " START_SETUP
246
- case "$START_SETUP" in
247
- [nN]*)
248
- info "Skipping setup. Run 'imtoagent setup' later to configure."
249
- ;;
250
- *)
251
- echo ""
252
- imtoagent setup
253
- ;;
254
- esac
255
- fi
256
- fi
257
-
258
- # ============================================================
259
- # 6. Start Gateway (optional)
260
- # ============================================================
261
- if [ "$SKIP_START" = true ]; then
262
- info "Skipping gateway start (--skip-start)"
263
- else
264
- step "6. Starting gateway"
265
-
266
- # Check if already running
267
- if imtoagent status 2>/dev/null | grep -q "running"; then
268
- warn "Gateway already running"
269
- imtoagent status 2>/dev/null || true
270
- else
271
- if [ "$NON_INTERACTIVE" = true ]; then
272
- info "Non-interactive mode — starting gateway in background"
273
- imtoagent start
274
- sleep 3
275
- imtoagent status 2>/dev/null || true
276
- else
277
- read -rp " Start gateway now? [Y/n] " START_GATEWAY
278
- case "$START_GATEWAY" in
279
- [nN]*)
280
- info "Not starting. Run 'imtoagent start' when ready."
281
- ;;
282
- *)
283
- imtoagent start
284
- sleep 3
285
- imtoagent status 2>/dev/null || true
286
- ;;
287
- esac
288
- fi
289
- fi
290
- fi
291
-
292
- # ============================================================
293
- # Summary
294
- # ============================================================
295
- echo ""
296
- echo -e "${BOLD} ──────────────────────────────────────────${NC}"
297
- echo -e "${GREEN}${BOLD} Installation Complete!${NC}"
298
- echo -e "${BOLD} ──────────────────────────────────────────${NC}"
299
- echo ""
300
- echo " Version: v${INSTALLED_VER}"
301
- echo " Config: ${CONFIG_FILE}"
302
- echo " Logs: ${CONFIG_DIR}/logs/"
303
- echo ""
304
- echo " Quick commands:"
305
- echo " imtoagent status # Check gateway status"
306
- echo " imtoagent stop # Stop the gateway"
307
- echo " imtoagent setup # Configure bots"
308
- echo " imtoagent restore # Hot reload"
309
- echo ""
310
- echo " Send ${BOLD}/help${NC} to your Bot in IM to see available commands."
311
- echo ""
312
- echo " Docs: https://github.com/imtoagent/imtoagent"
313
- echo ""
@@ -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
- }
@@ -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
- }
@@ -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
- }