imtoagent 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +234 -0
  2. package/bin/imtoagent +453 -0
  3. package/index.ts +1129 -0
  4. package/modules/agent/claude-adapter.ts +258 -0
  5. package/modules/agent/claude.ts +160 -0
  6. package/modules/agent/codex-adapter.ts +232 -0
  7. package/modules/agent/codex-exec-server.ts +513 -0
  8. package/modules/agent/codex.ts +275 -0
  9. package/modules/agent/opencode-adapter.ts +308 -0
  10. package/modules/agent/opencode.ts +247 -0
  11. package/modules/bot-context.ts +26 -0
  12. package/modules/capabilities.ts +189 -0
  13. package/modules/cli/setup.ts +424 -0
  14. package/modules/core/config.ts +275 -0
  15. package/modules/core/error.ts +124 -0
  16. package/modules/core/index.ts +39 -0
  17. package/modules/core/runtime.ts +282 -0
  18. package/modules/core/session.ts +256 -0
  19. package/modules/core/stats.ts +92 -0
  20. package/modules/core/types.ts +250 -0
  21. package/modules/im/feishu.ts +731 -0
  22. package/modules/im/telegram.ts +639 -0
  23. package/modules/im/wechat.ts +1094 -0
  24. package/modules/im/wecom.ts +603 -0
  25. package/modules/media/feishu-inbound-adapter.ts +108 -0
  26. package/modules/media/index.ts +27 -0
  27. package/modules/media/media-store.ts +273 -0
  28. package/modules/media/resolver.ts +178 -0
  29. package/modules/media/telegram-inbound-adapter.ts +124 -0
  30. package/modules/media/types.ts +76 -0
  31. package/modules/prompt-builder.ts +123 -0
  32. package/modules/proxy/anthropic-proxy.ts +1083 -0
  33. package/modules/proxy/codex-proxy.ts +657 -0
  34. package/modules/rate-limiter.ts +58 -0
  35. package/modules/types.ts +144 -0
  36. package/modules/utils/backend-check.ts +121 -0
  37. package/modules/utils/paths.ts +218 -0
  38. package/package.json +53 -0
  39. package/scripts/postinstall.ts +70 -0
  40. package/templates/config.template.json +57 -0
  41. package/templates/opencode.template.json +28 -0
  42. package/templates/providers.template.json +19 -0
  43. package/templates/soul.template/identity.md +6 -0
  44. package/templates/soul.template/profile.md +11 -0
  45. package/templates/soul.template/rules.md +7 -0
  46. package/templates/soul.template/skills.md +3 -0
  47. package/templates/soul.template/workspace.md +4 -0
@@ -0,0 +1,513 @@
1
+ // Codex App-Server 模块(v2 协议)
2
+ // 管理 codex app-server 持久进程 + stdio JSON-RPC 通信
3
+ // app-server 与 TUI 交互模式共享底层协议 → 自动触发记忆整合
4
+
5
+ import type { Subprocess } from 'bun';
6
+
7
+ // ================================================================
8
+ // 配置
9
+ // ================================================================
10
+ export interface ExecServerConfig {
11
+ enabled: boolean;
12
+ startupTimeoutMs: number;
13
+ fallbackToExec: boolean;
14
+ /** 单 turn 最大 tool-call 次数,超限强制终止(防 loop 导致 OOM) */
15
+ maxToolCallsPerTurn: number;
16
+ }
17
+
18
+ let _config: ExecServerConfig = {
19
+ enabled: true,
20
+ startupTimeoutMs: 15000,
21
+ fallbackToExec: true,
22
+ maxToolCallsPerTurn: 80,
23
+ };
24
+
25
+ export function setExecServerConfig(cfg: Partial<ExecServerConfig>) {
26
+ _config = { ..._config, ...cfg };
27
+ }
28
+
29
+ // ================================================================
30
+ // 事件类型
31
+ // ================================================================
32
+ export interface AgentEvent {
33
+ type: 'text_delta' | 'tool_call' | 'turn_result' | 'error';
34
+ textDelta?: string;
35
+ toolName?: string;
36
+ toolInput?: Record<string, any>;
37
+ threadId?: string;
38
+ usage?: { inputTokens: number; outputTokens: number };
39
+ costUSD?: number;
40
+ durationMs?: number;
41
+ terminal?: boolean; // true = 任务真正完成,receiveEvents 停止
42
+ error?: string;
43
+ }
44
+
45
+ // ================================================================
46
+ // 客户端(每个 chat 一个实例)
47
+ // ================================================================
48
+ export class CodexAppServerClient {
49
+ private chatId: string;
50
+ private nextId = 1;
51
+ private pendingRequests: Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void }> = new Map();
52
+ private eventQueue: AgentEvent[] = [];
53
+ private resolveNext: ((v: IteratorResult<AgentEvent>) => void) | null = null;
54
+ private _active = false;
55
+
56
+ // 单 turn tool-call 计数(循环保护)
57
+ private _turnToolCallCount = 0;
58
+ private _turnActive = false;
59
+
60
+ constructor(chatId: string) {
61
+ this.chatId = chatId;
62
+ }
63
+
64
+ get connected(): boolean { return this._active; }
65
+ get turnToolCallCount(): number { return this._turnToolCallCount; }
66
+ setActive(active: boolean): void { this._active = active; }
67
+
68
+ /** 通知 client turn 已启动,重置计数器 */
69
+ notifyTurnStart(): void {
70
+ this._turnToolCallCount = 0;
71
+ this._turnActive = true;
72
+ }
73
+
74
+ /** 通知 turn 结束 */
75
+ notifyTurnEnd(): void {
76
+ this._turnActive = false;
77
+ }
78
+
79
+ /** 记录一次 tool-call,返回 true 表示未超限 */
80
+ recordToolCall(name: string): boolean {
81
+ if (!this._turnActive) return true;
82
+ this._turnToolCallCount++;
83
+ if (this._turnToolCallCount > _config.maxToolCallsPerTurn) {
84
+ console.error(`[app-server] ⚠️ tool-call loop 检测! chat=${this.chatId.slice(-8)}: ${this._turnToolCallCount} 次 > 上限 ${_config.maxToolCallsPerTurn}`);
85
+ return false;
86
+ }
87
+ return true;
88
+ }
89
+
90
+ // ================================================================
91
+ // 初始化 & 线程管理
92
+ // ================================================================
93
+
94
+ async initialize(): Promise<void> {
95
+ await this._sendRequest('initialize', {
96
+ clientInfo: { name: 'imtoagent', version: '1.0' },
97
+ });
98
+ console.log(`[app-server] initialized chat=${this.chatId.slice(-8)}`);
99
+ }
100
+
101
+ async startThread(cwd: string): Promise<string> {
102
+ const result: any = await this._sendRequest('thread/start', {
103
+ cwd,
104
+ model: 'gpt-5.5',
105
+ modelProvider: 'imtoagent',
106
+ sandbox: 'danger-full-access',
107
+ approvalPolicy: 'never',
108
+ });
109
+ const threadId = result?.thread?.id || '';
110
+ if (!threadId) throw new Error('thread/start 未返回 thread.id');
111
+ console.log(`[app-server] thread started=${threadId.slice(-8)} chat=${this.chatId.slice(-8)}`);
112
+ return threadId;
113
+ }
114
+
115
+ async resumeThread(threadId: string): Promise<void> {
116
+ // app-server v2: thread/resume 用于跨进程恢复
117
+ const result: any = await this._sendRequest('thread/resume', { threadId });
118
+ console.log(`[app-server] thread resumed=${threadId.slice(-8)} chat=${this.chatId.slice(-8)}`);
119
+ }
120
+
121
+ // ================================================================
122
+ // 发送消息
123
+ // ================================================================
124
+
125
+ async sendPrompt(threadId: string, prompt: string, cwd: string): Promise<void> {
126
+ this.notifyTurnStart();
127
+ await this._sendRequest('turn/start', {
128
+ threadId,
129
+ input: [{ type: 'text', text: prompt }],
130
+ cwd,
131
+ model: 'gpt-5.5',
132
+ effort: 'medium',
133
+ });
134
+ }
135
+
136
+ async *receiveEvents(): AsyncGenerator<AgentEvent> {
137
+ while (this._active) {
138
+ const event = await new Promise<AgentEvent>((resolve) => {
139
+ if (this.eventQueue.length > 0) {
140
+ resolve(this.eventQueue.shift()!);
141
+ } else {
142
+ this.resolveNext = (v: IteratorResult<AgentEvent>) => {
143
+ this.resolveNext = null;
144
+ if (v.done) resolve({ type: 'error', error: 'closed' });
145
+ else resolve(v.value);
146
+ };
147
+ }
148
+ });
149
+
150
+ yield event;
151
+
152
+ // 只在 agent 空闲(terminal)或出错时停止
153
+ if (event.type === 'error') break;
154
+ if (event.type === 'turn_result' && event.terminal) break;
155
+ }
156
+ }
157
+
158
+ close(): void {
159
+ this._active = false;
160
+ for (const [, p] of this.pendingRequests) {
161
+ p.reject(new Error('client closed'));
162
+ }
163
+ this.pendingRequests.clear();
164
+ if (this.resolveNext) {
165
+ this.resolveNext({ done: true, value: undefined });
166
+ this.resolveNext = null;
167
+ }
168
+ }
169
+
170
+ // ================================================================
171
+ // Manager 调用接口
172
+ // ================================================================
173
+
174
+ dispatchEvent(event: AgentEvent): void {
175
+ if (!this._active) return;
176
+ if (this.resolveNext) {
177
+ this.resolveNext({ done: false, value: event });
178
+ } else {
179
+ this.eventQueue.push(event);
180
+ }
181
+ }
182
+
183
+ dispatchResponse(id: number, result: unknown, error?: { code: number; message: string }): void {
184
+ const pending = this.pendingRequests.get(id);
185
+ if (pending) {
186
+ this.pendingRequests.delete(id);
187
+ if (error) {
188
+ pending.reject(new Error(`app-server error: ${error.message} (code=${error.code})`));
189
+ } else {
190
+ pending.resolve(result);
191
+ }
192
+ }
193
+ }
194
+
195
+ // ================================================================
196
+ // 内部
197
+ // ================================================================
198
+
199
+ _sendRequest(method: string, params: Record<string, unknown>): Promise<unknown> {
200
+ return new Promise((resolve, reject) => {
201
+ const id = this.nextId++;
202
+ const req = JSON.stringify({ jsonrpc: '2.0', id, method, params });
203
+ const timer = setTimeout(() => {
204
+ this.pendingRequests.delete(id);
205
+ reject(new Error(`app-server 请求超时: ${method}`));
206
+ }, 300000);
207
+ this.pendingRequests.set(id, { resolve, reject });
208
+
209
+ const ok = getAppServerManager()._writeStdin(req + '\n');
210
+ if (!ok) {
211
+ clearTimeout(timer);
212
+ this.pendingRequests.delete(id);
213
+ reject(new Error(`app-server stdin 写入失败: ${method}`));
214
+ }
215
+ });
216
+ }
217
+ }
218
+
219
+ // ================================================================
220
+ // 进程管理器(单例)
221
+ // ================================================================
222
+ class CodexAppServerManager {
223
+ private process: Subprocess | null = null;
224
+ private clients: Map<string, CodexAppServerClient> = new Map();
225
+ private activeChatId: string | null = null;
226
+ private _shuttingDown = false;
227
+ private _startPromise: Promise<void> | null = null;
228
+ private stdoutBuffer = '';
229
+ private readLoopRunning = false;
230
+ private _initialized = false; // app-server 只接受一次 initialize
231
+ private _generation = 0; // 每次进程重启递增,用于判断 thread 是否过期
232
+
233
+ async ensureRunning(): Promise<void> {
234
+ if (this._shuttingDown) throw new Error('app-server 正在关闭');
235
+ if (this.process && !this.process.killed) return;
236
+ if (this._startPromise) { await this._startPromise; return; }
237
+ this._startPromise = this._spawn();
238
+ try { await this._startPromise; } finally { this._startPromise = null; }
239
+ }
240
+
241
+ async getClient(chatId: string): Promise<CodexAppServerClient> {
242
+ await this.ensureRunning();
243
+
244
+ // 回收之前的活跃客户端
245
+ const prev = this.clients.get(this.activeChatId || '');
246
+ if (prev && this.activeChatId !== chatId) prev.setActive(false);
247
+
248
+ let client = this.clients.get(chatId);
249
+ if (!client) {
250
+ client = new CodexAppServerClient(chatId);
251
+ this.clients.set(chatId, client);
252
+ }
253
+
254
+ // 先设置 activeChatId,再发请求——防止 readLoop 先收到响应但找不到 client
255
+ this.activeChatId = chatId;
256
+
257
+ if (!this._initialized) {
258
+ client.setActive(true);
259
+ await client.initialize();
260
+ this._initialized = true;
261
+ } else {
262
+ client.setActive(true);
263
+ }
264
+
265
+ return client;
266
+ }
267
+
268
+ removeClient(chatId: string): void {
269
+ const client = this.clients.get(chatId);
270
+ if (client) { client.close(); this.clients.delete(chatId); }
271
+ if (this.activeChatId === chatId) this.activeChatId = null;
272
+ }
273
+
274
+ async shutdown(): Promise<void> {
275
+ this._shuttingDown = true;
276
+ for (const [, c] of this.clients) c.close();
277
+ this.clients.clear();
278
+ this.activeChatId = null;
279
+ if (this.process && !this.process.killed) {
280
+ try {
281
+ this.process.kill('SIGTERM');
282
+ await new Promise<void>((resolve) => {
283
+ const t = setTimeout(() => { try { this.process?.kill('SIGKILL'); } catch {} resolve(); }, 3000);
284
+ this.process!.exited.then(() => { clearTimeout(t); resolve(); });
285
+ });
286
+ } catch {}
287
+ }
288
+ this.process = null;
289
+ console.log('[app-server] 已关闭');
290
+ }
291
+ /** 健康检查:进程存活但 readLoop 已停止 */
292
+ needsRestart(): boolean {
293
+ return this.process !== null && !this.process.killed && !this.readLoopRunning;
294
+ }
295
+
296
+ /** 强制重启 app-server(用于健康检查自动恢复) */
297
+ async forceRestart(): Promise<void> {
298
+ console.warn('[app-server] 健康检查触发强制重启...');
299
+ await this.shutdown();
300
+ this._shuttingDown = false;
301
+ this._initialized = false;
302
+ }
303
+
304
+ _writeStdin(data: string): boolean {
305
+ if (!this.process || this.process.killed) return false;
306
+ try { this.process.stdin!.write(data); return true; } catch { return false; }
307
+ }
308
+
309
+ private async _spawn(): Promise<void> {
310
+ console.log('[app-server] 启动 codex app-server (stdio)...');
311
+ this.process = Bun.spawn(
312
+ ['codex', 'app-server',
313
+ '--listen', 'stdio://',
314
+ '-c', 'model_provider=imtoagent',
315
+ '-c', 'sandbox.mode=danger-full-access',
316
+ '--enable', 'memories',
317
+ ],
318
+ { stdin: 'pipe', stdout: 'pipe', stderr: 'pipe' },
319
+ );
320
+ this._readStderr().catch((err) => {
321
+ console.error(`[app-server] stderr reader error: ${err.message}`);
322
+ });
323
+ this.process.exited
324
+ .then(async (code: number | null) => {
325
+ console.error(`[app-server] 进程退出 code=${code}`);
326
+ this.process = null;
327
+ this.readLoopRunning = false;
328
+ this._initialized = false;
329
+ this._generation++;
330
+
331
+ // 先等待启动 promise 完成,避免 _spawn 还在等 _startReadLoop
332
+ if (this._startPromise) {
333
+ try { await this._startPromise; } catch {}
334
+ this._startPromise = null;
335
+ }
336
+
337
+ // 安全关闭所有客户端
338
+ for (const [, c] of this.clients) {
339
+ try { c.close(); } catch {}
340
+ }
341
+ this.clients.clear();
342
+ this.activeChatId = null;
343
+ })
344
+ .catch((err) => {
345
+ console.error(`[app-server] process.exited handler error: ${err.message}`);
346
+ });
347
+ // 给 app-server 短暂时间完成内部初始化
348
+ await new Promise(r => setTimeout(r, 500));
349
+ this._startReadLoop();
350
+ console.log('[app-server] 已就绪 (stdio)');
351
+ }
352
+
353
+ private _startReadLoop(): void {
354
+ if (this.readLoopRunning) return;
355
+ this.readLoopRunning = true;
356
+ this._readLoop();
357
+ }
358
+
359
+ private async _readLoop(): Promise<void> {
360
+ if (!this.process?.stdout) return;
361
+ const reader = this.process.stdout.getReader();
362
+ const decoder = new TextDecoder();
363
+ try {
364
+ while (true) {
365
+ const { done, value } = await reader.read();
366
+ if (done) break;
367
+ this.stdoutBuffer += decoder.decode(value, { stream: true });
368
+ let nl: number;
369
+ while ((nl = this.stdoutBuffer.indexOf('\n')) !== -1) {
370
+ const line = this.stdoutBuffer.slice(0, nl).trim();
371
+ this.stdoutBuffer = this.stdoutBuffer.slice(nl + 1);
372
+ if (line) this._processLine(line);
373
+ }
374
+ }
375
+ } catch (e: any) {
376
+ if (!this._shuttingDown) console.error(`[app-server] read error: ${e.message}`);
377
+ } finally {
378
+ try { reader.releaseLock(); } catch {}
379
+ this.readLoopRunning = false;
380
+ }
381
+ }
382
+
383
+ private _processLine(line: string): void {
384
+ let msg: any;
385
+ try { msg = JSON.parse(line); } catch { return; }
386
+
387
+ if ('id' in msg && msg.id != null) {
388
+ const active = this.clients.get(this.activeChatId || '');
389
+ if (active) active.dispatchResponse(msg.id, msg.result, msg.error);
390
+ } else if (msg.method) {
391
+ const active = this.clients.get(this.activeChatId || '');
392
+ if (active) this._handleNotification(active, msg.method, msg.params || {});
393
+ }
394
+ }
395
+
396
+ private _handleNotification(client: CodexAppServerClient, method: string, params: any): void {
397
+ try {
398
+ switch (method) {
399
+ case 'thread/started':
400
+ client.notifyTurnStart();
401
+ break;
402
+
403
+ case 'turn/started':
404
+ client.notifyTurnStart();
405
+ break;
406
+
407
+ case 'thread/status/changed':
408
+ // params: { threadId, status: { type: "idle" | "active" } }
409
+ if (params.status?.type === 'idle') {
410
+ // agent 真正停下来等用户输入 → 任务完成
411
+ client.notifyTurnEnd();
412
+ client.dispatchEvent({ type: 'turn_result', terminal: true, usage: { inputTokens: 0, outputTokens: 0 } });
413
+ }
414
+ break;
415
+
416
+ case 'item/started':
417
+ case 'item/completed': {
418
+ // 统计 tool-use / function_call 类型
419
+ const itemType = params.item?.type;
420
+ if (itemType === 'tool_use' || itemType === 'function_call') {
421
+ const toolName = params.item?.name || params.item?.function_name || 'unknown';
422
+ if (method === 'item/completed') {
423
+ const ok = client.recordToolCall(toolName);
424
+ if (!ok) {
425
+ // 超限,发送 error 事件强制终止本轮
426
+ client.dispatchEvent({
427
+ type: 'error',
428
+ error: `⚠️ Tool-call loop 检测:本 turn 已达 ${client.turnToolCallCount} 次工具调用(上限 ${_config.maxToolCallsPerTurn}),已强制终止。建议拆分为更小任务。`,
429
+ });
430
+ }
431
+ }
432
+ }
433
+ break;
434
+ }
435
+
436
+ case 'remoteControl/status/changed':
437
+ case 'account/rateLimits/updated':
438
+ case 'thread/tokenUsage/updated':
439
+ // 元数据通知,暂不处理
440
+ break;
441
+
442
+ case 'item/agentMessage/delta':
443
+ if (params.delta) client.dispatchEvent({ type: 'text_delta', textDelta: params.delta });
444
+ break;
445
+
446
+ case 'turn/completed':
447
+ client.notifyTurnEnd();
448
+ client.dispatchEvent({
449
+ type: 'turn_result',
450
+ terminal: false, // 非终点,agent 可能自动继续
451
+ usage: {
452
+ inputTokens: params.turn?.usage?.inputTokens || 0,
453
+ outputTokens: params.turn?.usage?.outputTokens || 0,
454
+ },
455
+ costUSD: params.turn?.usage?.costUSD || 0,
456
+ durationMs: params.turn?.durationMs || 0,
457
+ });
458
+ break;
459
+
460
+ case 'item/reasoning/textDelta':
461
+ case 'item/reasoning/summaryTextDelta':
462
+ break;
463
+
464
+ case 'turn/plan/updated':
465
+ case 'turn/diff/updated':
466
+ break;
467
+
468
+ case 'warning':
469
+ case 'deprecationNotice':
470
+ console.warn(`[app-server] ${method}:`, params);
471
+ break;
472
+
473
+ default:
474
+ // 静默忽略未知通知
475
+ break;
476
+ }
477
+ } catch (err) {
478
+ console.error(`[app-server] Error handling notification ${method}:`, err);
479
+ }
480
+ }
481
+
482
+ private async _readStderr(): Promise<void> {
483
+ if (!this.process) return;
484
+ try {
485
+ const stderr = await new Response(this.process.stderr).text();
486
+ if (stderr.trim()) console.error(`[app-server] stderr:`, stderr.slice(0, 500));
487
+ } catch {}
488
+ }
489
+
490
+ // 暴露当前进程代际,codex.ts 用于判断 thread 是否过期
491
+ get generation(): number { return this._generation; }
492
+ }
493
+
494
+ // 单例
495
+ let _manager: CodexAppServerManager | null = null;
496
+
497
+ export function getAppServerManager(): CodexAppServerManager {
498
+ if (!_manager) _manager = new CodexAppServerManager();
499
+ return _manager;
500
+ }
501
+
502
+ // 向后兼容别名
503
+ export const getExecServerManager = getAppServerManager;
504
+
505
+ export async function shutdownAppServer(): Promise<void> {
506
+ if (_manager) { await _manager.shutdown(); _manager = null; }
507
+ }
508
+
509
+ // 向后兼容别名
510
+ export const shutdownExecServer = shutdownAppServer;
511
+
512
+ // 向后兼容的类型别名
513
+ export type CodexExecServerClient = CodexAppServerClient;