@snack-kit/porygon 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,1963 @@
1
+ // src/porygon.ts
2
+ import { EventEmitter as EventEmitter2 } from "events";
3
+
4
+ // src/config/defaults.ts
5
+ var DEFAULT_CONFIG = {
6
+ defaultBackend: "claude",
7
+ backends: {},
8
+ defaults: {
9
+ timeoutMs: 3e5,
10
+ // 5 minutes
11
+ maxTurns: 50
12
+ },
13
+ proxy: void 0
14
+ };
15
+
16
+ // src/config/schema.ts
17
+ import { z } from "zod";
18
+
19
+ // src/errors/index.ts
20
+ var PorygonError = class extends Error {
21
+ code;
22
+ constructor(code, message, options) {
23
+ super(message, options);
24
+ this.name = "PorygonError";
25
+ this.code = code;
26
+ }
27
+ };
28
+ var AdapterNotFoundError = class extends PorygonError {
29
+ constructor(backend, options) {
30
+ super("ADAPTER_NOT_FOUND", `Adapter not found: ${backend}`, options);
31
+ this.name = "AdapterNotFoundError";
32
+ }
33
+ };
34
+ var AdapterNotAvailableError = class extends PorygonError {
35
+ constructor(backend, options) {
36
+ super("ADAPTER_NOT_AVAILABLE", `Adapter not available: ${backend}`, options);
37
+ this.name = "AdapterNotAvailableError";
38
+ }
39
+ };
40
+ var AdapterIncompatibleError = class extends PorygonError {
41
+ constructor(backend, version, options) {
42
+ super(
43
+ "ADAPTER_INCOMPATIBLE",
44
+ `Adapter incompatible: ${backend} version ${version}`,
45
+ options
46
+ );
47
+ this.name = "AdapterIncompatibleError";
48
+ }
49
+ };
50
+ var SessionNotFoundError = class extends PorygonError {
51
+ constructor(sessionId, options) {
52
+ super("SESSION_NOT_FOUND", `Session not found: ${sessionId}`, options);
53
+ this.name = "SessionNotFoundError";
54
+ }
55
+ };
56
+ var InterceptorRejectedError = class extends PorygonError {
57
+ constructor(reason, options) {
58
+ super("INTERCEPTOR_REJECTED", `Interceptor rejected: ${reason}`, options);
59
+ this.name = "InterceptorRejectedError";
60
+ }
61
+ };
62
+ var AgentExecutionError = class extends PorygonError {
63
+ constructor(message, options) {
64
+ super("AGENT_EXECUTION_ERROR", message, options);
65
+ this.name = "AgentExecutionError";
66
+ }
67
+ };
68
+ var AgentTimeoutError = class extends PorygonError {
69
+ constructor(timeoutMs, options) {
70
+ super("AGENT_TIMEOUT", `Agent timed out after ${timeoutMs}ms`, options);
71
+ this.name = "AgentTimeoutError";
72
+ }
73
+ };
74
+ var ConfigValidationError = class extends PorygonError {
75
+ constructor(message, options) {
76
+ super("CONFIG_VALIDATION_ERROR", message, options);
77
+ this.name = "ConfigValidationError";
78
+ }
79
+ };
80
+
81
+ // src/config/schema.ts
82
+ var ProxyConfigSchema = z.object({
83
+ url: z.string().url(),
84
+ noProxy: z.string().optional()
85
+ });
86
+ var BackendConfigSchema = z.object({
87
+ model: z.string().optional(),
88
+ appendSystemPrompt: z.string().optional(),
89
+ proxy: ProxyConfigSchema.optional(),
90
+ cwd: z.string().optional(),
91
+ options: z.record(z.string(), z.unknown()).optional()
92
+ });
93
+ var PorygonConfigSchema = z.object({
94
+ defaultBackend: z.string().optional(),
95
+ backends: z.record(z.string(), BackendConfigSchema).optional(),
96
+ defaults: z.object({
97
+ appendSystemPrompt: z.string().optional(),
98
+ timeoutMs: z.number().positive().optional(),
99
+ maxTurns: z.number().int().positive().optional()
100
+ }).optional(),
101
+ proxy: ProxyConfigSchema.optional()
102
+ });
103
+ function validateConfig(config) {
104
+ const result = PorygonConfigSchema.safeParse(config);
105
+ if (!result.success) {
106
+ const issues = result.error.issues.map((issue) => ({
107
+ path: issue.path.join("."),
108
+ message: issue.message
109
+ }));
110
+ throw new ConfigValidationError(
111
+ `\u914D\u7F6E\u6821\u9A8C\u5931\u8D25: ${issues.map((i) => `${i.path}: ${i.message}`).join("; ")}`
112
+ );
113
+ }
114
+ return result.data;
115
+ }
116
+
117
+ // src/config/config-loader.ts
118
+ function isPlainObject(value) {
119
+ return typeof value === "object" && value !== null && !Array.isArray(value);
120
+ }
121
+ function deepMerge(target, source) {
122
+ const result = { ...target };
123
+ for (const key of Object.keys(source)) {
124
+ const sourceVal = source[key];
125
+ const targetVal = result[key];
126
+ if (sourceVal === void 0) {
127
+ continue;
128
+ }
129
+ if (isPlainObject(targetVal) && isPlainObject(sourceVal)) {
130
+ result[key] = deepMerge(
131
+ targetVal,
132
+ sourceVal
133
+ );
134
+ } else {
135
+ result[key] = sourceVal;
136
+ }
137
+ }
138
+ return result;
139
+ }
140
+ function loadEnvOverrides() {
141
+ const config = {};
142
+ const defaultBackend = process.env["PORYGON_DEFAULT_BACKEND"];
143
+ if (defaultBackend) {
144
+ config.defaultBackend = defaultBackend;
145
+ }
146
+ const proxyUrl = process.env["PORYGON_PROXY_URL"];
147
+ if (proxyUrl) {
148
+ config.proxy = {
149
+ url: proxyUrl,
150
+ noProxy: process.env["PORYGON_PROXY_NO_PROXY"]
151
+ };
152
+ }
153
+ return config;
154
+ }
155
+ var ConfigLoader = class {
156
+ /**
157
+ * 加载并合并配置
158
+ * @param userConfig - 用户传入的配置(可选)
159
+ * @returns 校验通过的最终配置
160
+ * @throws ConfigValidationError 校验失败时抛出
161
+ */
162
+ static load(userConfig) {
163
+ let merged = { ...DEFAULT_CONFIG };
164
+ const envOverrides = loadEnvOverrides();
165
+ merged = deepMerge(merged, envOverrides);
166
+ if (userConfig) {
167
+ merged = deepMerge(merged, userConfig);
168
+ }
169
+ return validateConfig(merged);
170
+ }
171
+ };
172
+
173
+ // src/interceptor/interceptor.ts
174
+ var InterceptorManager = class {
175
+ inputInterceptors = [];
176
+ outputInterceptors = [];
177
+ /**
178
+ * 注册拦截器
179
+ * @param direction - 拦截方向
180
+ * @param fn - 拦截器函数
181
+ * @returns 取消注册的函数
182
+ */
183
+ use(direction, fn) {
184
+ const list = direction === "input" ? this.inputInterceptors : this.outputInterceptors;
185
+ list.push(fn);
186
+ return () => {
187
+ const idx = list.indexOf(fn);
188
+ if (idx !== -1) {
189
+ list.splice(idx, 1);
190
+ }
191
+ };
192
+ }
193
+ /**
194
+ * 执行输入拦截器流水线
195
+ * @param text - 原始输入文本
196
+ * @param context - 拦截器上下文(不含 direction)
197
+ * @returns 处理后的文本
198
+ * @throws InterceptorRejectedError 拦截器返回 false 时抛出
199
+ */
200
+ async processInput(text, context) {
201
+ return this.runPipeline(text, "input", context, this.inputInterceptors);
202
+ }
203
+ /**
204
+ * 执行输出拦截器流水线
205
+ * @param text - 原始输出文本
206
+ * @param context - 拦截器上下文(不含 direction)
207
+ * @returns 处理后的文本
208
+ * @throws InterceptorRejectedError 拦截器返回 false 时抛出
209
+ */
210
+ async processOutput(text, context) {
211
+ return this.runPipeline(text, "output", context, this.outputInterceptors);
212
+ }
213
+ /**
214
+ * 执行拦截器流水线
215
+ */
216
+ async runPipeline(text, direction, context, interceptors) {
217
+ const fullContext = { ...context, direction };
218
+ let current = text;
219
+ for (const fn of interceptors) {
220
+ const result = await fn(current, fullContext);
221
+ if (result === false) {
222
+ throw new InterceptorRejectedError(direction);
223
+ }
224
+ if (typeof result === "string") {
225
+ current = result;
226
+ }
227
+ }
228
+ return current;
229
+ }
230
+ };
231
+
232
+ // src/process/process-handle.ts
233
+ import { spawn } from "child_process";
234
+ import { EventEmitter } from "events";
235
+ import { createInterface } from "readline";
236
+ var GRACE_PERIOD_MS = 5e3;
237
+ var EphemeralProcess = class {
238
+ childProcess = null;
239
+ aborted = false;
240
+ /**
241
+ * 执行命令并收集输出。支持 AbortController 进行取消操作。
242
+ * @param options 进程启动选项
243
+ * @param abortSignal 可选的中止信号
244
+ * @returns 进程执行结果
245
+ */
246
+ async execute(options, abortSignal) {
247
+ return new Promise((resolve, reject) => {
248
+ if (abortSignal?.aborted) {
249
+ reject(new Error("Process aborted before start"));
250
+ return;
251
+ }
252
+ this.aborted = false;
253
+ const child = spawn(options.command, options.args, {
254
+ cwd: options.cwd,
255
+ env: options.env ? { ...process.env, ...options.env } : void 0,
256
+ stdio: ["pipe", "pipe", "pipe"]
257
+ });
258
+ this.childProcess = child;
259
+ const stdoutChunks = [];
260
+ const stderrChunks = [];
261
+ child.stdout?.on("data", (chunk) => {
262
+ stdoutChunks.push(chunk);
263
+ });
264
+ child.stderr?.on("data", (chunk) => {
265
+ stderrChunks.push(chunk);
266
+ });
267
+ let timeoutTimer;
268
+ if (options.timeoutMs !== void 0 && options.timeoutMs > 0) {
269
+ timeoutTimer = setTimeout(() => {
270
+ this.terminate();
271
+ reject(new Error(`Process timed out after ${options.timeoutMs}ms`));
272
+ }, options.timeoutMs);
273
+ }
274
+ const onAbort = () => {
275
+ this.terminate();
276
+ reject(new Error("Process aborted"));
277
+ };
278
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
279
+ child.on("error", (err) => {
280
+ if (timeoutTimer) clearTimeout(timeoutTimer);
281
+ abortSignal?.removeEventListener("abort", onAbort);
282
+ this.childProcess = null;
283
+ reject(err);
284
+ });
285
+ child.on("close", (code, signal) => {
286
+ if (timeoutTimer) clearTimeout(timeoutTimer);
287
+ abortSignal?.removeEventListener("abort", onAbort);
288
+ this.childProcess = null;
289
+ resolve({
290
+ exitCode: code,
291
+ signal,
292
+ stdout: Buffer.concat(stdoutChunks).toString("utf-8"),
293
+ stderr: Buffer.concat(stderrChunks).toString("utf-8")
294
+ });
295
+ });
296
+ });
297
+ }
298
+ /**
299
+ * 以流式方式执行命令,逐行输出 stdout 内容。
300
+ * @param options 进程启动选项
301
+ * @param abortSignal 可选的中止信号
302
+ */
303
+ async *executeStreaming(options, abortSignal) {
304
+ if (abortSignal?.aborted) {
305
+ throw new Error("Process aborted before start");
306
+ }
307
+ this.aborted = false;
308
+ const child = spawn(options.command, options.args, {
309
+ cwd: options.cwd,
310
+ env: options.env ?? void 0,
311
+ stdio: ["ignore", "pipe", "pipe"]
312
+ });
313
+ this.childProcess = child;
314
+ let timeoutTimer;
315
+ if (options.timeoutMs !== void 0 && options.timeoutMs > 0) {
316
+ timeoutTimer = setTimeout(() => {
317
+ this.terminate();
318
+ }, options.timeoutMs);
319
+ }
320
+ const onAbort = () => {
321
+ this.terminate();
322
+ };
323
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
324
+ const stderrChunks = [];
325
+ child.stderr?.on("data", (chunk) => {
326
+ stderrChunks.push(chunk);
327
+ });
328
+ let exitCode = null;
329
+ const exitPromise = new Promise((resolve) => {
330
+ child.on("close", (code) => {
331
+ exitCode = code;
332
+ resolve();
333
+ });
334
+ });
335
+ const rl = createInterface({ input: child.stdout });
336
+ let yieldedLines = 0;
337
+ try {
338
+ for await (const line of rl) {
339
+ yieldedLines++;
340
+ yield line;
341
+ }
342
+ await exitPromise;
343
+ if (exitCode !== 0 && yieldedLines === 0 && !this.aborted) {
344
+ const stderr = Buffer.concat(stderrChunks).toString("utf-8").trim();
345
+ throw new Error(
346
+ stderr || `Process exited with code ${exitCode}`
347
+ );
348
+ }
349
+ } finally {
350
+ if (timeoutTimer) clearTimeout(timeoutTimer);
351
+ abortSignal?.removeEventListener("abort", onAbort);
352
+ rl.close();
353
+ this.childProcess = null;
354
+ }
355
+ }
356
+ /** 获取底层子进程的 PID */
357
+ get pid() {
358
+ return this.childProcess?.pid;
359
+ }
360
+ /** 终止进程:先发送 SIGTERM,超时后发送 SIGKILL */
361
+ terminate() {
362
+ if (!this.childProcess || this.aborted) return;
363
+ this.aborted = true;
364
+ this.childProcess.kill("SIGTERM");
365
+ const ref = this.childProcess;
366
+ setTimeout(() => {
367
+ if (!ref.killed) {
368
+ ref.kill("SIGKILL");
369
+ }
370
+ }, GRACE_PERIOD_MS);
371
+ }
372
+ };
373
+ var PersistentProcess = class extends EventEmitter {
374
+ process = null;
375
+ restartCount = 0;
376
+ stopped = false;
377
+ healthCheckTimer = null;
378
+ options;
379
+ constructor(options) {
380
+ super();
381
+ this.options = options;
382
+ }
383
+ /** 启动持久进程 */
384
+ async start() {
385
+ this.stopped = false;
386
+ const child = spawn(this.options.command, this.options.args, {
387
+ cwd: this.options.cwd,
388
+ env: this.options.env ? { ...process.env, ...this.options.env } : void 0,
389
+ stdio: ["pipe", "pipe", "pipe"]
390
+ });
391
+ this.process = child;
392
+ child.on("error", (err) => {
393
+ this.emit("error", err);
394
+ });
395
+ child.on("exit", (code) => {
396
+ this.process = null;
397
+ this.stopHealthCheck();
398
+ this.handleCrash(code);
399
+ });
400
+ if (this.options.healthCheckFn && this.options.healthCheckIntervalMs > 0) {
401
+ this.healthCheckTimer = setInterval(() => {
402
+ this.options.healthCheckFn().catch((err) => {
403
+ this.emit("healthCheckFailed", err);
404
+ });
405
+ }, this.options.healthCheckIntervalMs);
406
+ }
407
+ this.emit("started", child.pid);
408
+ }
409
+ /** 处理进程崩溃,按指数退避策略重启 */
410
+ handleCrash(code) {
411
+ if (this.stopped) return;
412
+ this.emit("exit", code);
413
+ if (this.restartCount < this.options.maxRestarts) {
414
+ const delay = this.options.restartIntervalMs * Math.pow(2, this.restartCount);
415
+ this.restartCount++;
416
+ this.emit("restart", this.restartCount);
417
+ setTimeout(() => {
418
+ if (!this.stopped) {
419
+ this.start().catch((err) => {
420
+ this.emit("fatal", err);
421
+ });
422
+ }
423
+ }, delay);
424
+ } else {
425
+ this.emit(
426
+ "fatal",
427
+ new Error(
428
+ `Max restarts (${this.options.maxRestarts}) exceeded, last exit code: ${code}`
429
+ )
430
+ );
431
+ }
432
+ }
433
+ /** 停止健康检查定时器 */
434
+ stopHealthCheck() {
435
+ if (this.healthCheckTimer) {
436
+ clearInterval(this.healthCheckTimer);
437
+ this.healthCheckTimer = null;
438
+ }
439
+ }
440
+ /** 停止持久进程 */
441
+ async stop() {
442
+ this.stopped = true;
443
+ this.stopHealthCheck();
444
+ if (this.process) {
445
+ const child = this.process;
446
+ child.kill("SIGTERM");
447
+ await new Promise((resolve) => {
448
+ const timer = setTimeout(() => {
449
+ if (!child.killed) {
450
+ child.kill("SIGKILL");
451
+ }
452
+ resolve();
453
+ }, GRACE_PERIOD_MS);
454
+ child.on("exit", () => {
455
+ clearTimeout(timer);
456
+ resolve();
457
+ });
458
+ });
459
+ this.process = null;
460
+ }
461
+ }
462
+ /** 获取底层子进程的 PID */
463
+ get pid() {
464
+ return this.process?.pid;
465
+ }
466
+ /** 当前进程是否正在运行 */
467
+ get isRunning() {
468
+ return this.process !== null && !this.process.killed;
469
+ }
470
+ };
471
+
472
+ // src/process/process-manager.ts
473
+ var ProcessManager = class _ProcessManager {
474
+ ephemeral = /* @__PURE__ */ new Map();
475
+ persistent = /* @__PURE__ */ new Map();
476
+ /** 全局静态标记,确保信号处理器只注册一次 */
477
+ static cleanupRegistered = false;
478
+ /** 所有活跃的 ProcessManager 实例(弱引用避免阻止 GC) */
479
+ static instances = /* @__PURE__ */ new Set();
480
+ constructor() {
481
+ _ProcessManager.instances.add(this);
482
+ _ProcessManager.registerCleanup();
483
+ }
484
+ /** 注册进程退出清理钩子(仅清理资源,不劫持宿主退出行为) */
485
+ static registerCleanup() {
486
+ if (_ProcessManager.cleanupRegistered) return;
487
+ _ProcessManager.cleanupRegistered = true;
488
+ const cleanup = () => {
489
+ for (const instance of _ProcessManager.instances) {
490
+ instance.terminateAllSync();
491
+ }
492
+ };
493
+ process.on("exit", cleanup);
494
+ process.on("SIGTERM", cleanup);
495
+ process.on("SIGINT", cleanup);
496
+ }
497
+ /**
498
+ * 创建一次性进程实例并注册到管理器
499
+ * @param sessionId 会话标识
500
+ * @returns 一次性进程实例
501
+ */
502
+ createEphemeral(sessionId) {
503
+ const proc = new EphemeralProcess();
504
+ this.ephemeral.set(sessionId, proc);
505
+ return proc;
506
+ }
507
+ /**
508
+ * 从管理器中移除一次性进程
509
+ * @param sessionId 会话标识
510
+ */
511
+ removeEphemeral(sessionId) {
512
+ this.ephemeral.delete(sessionId);
513
+ }
514
+ /**
515
+ * 启动或获取持久进程
516
+ * @param backend 后端标识
517
+ * @param options 持久进程选项
518
+ * @returns 持久进程实例
519
+ */
520
+ async startPersistent(backend, options) {
521
+ const existing = this.persistent.get(backend);
522
+ if (existing?.isRunning) return existing;
523
+ const proc = new PersistentProcess(options);
524
+ this.persistent.set(backend, proc);
525
+ await proc.start();
526
+ return proc;
527
+ }
528
+ /**
529
+ * 获取指定后端的持久进程
530
+ * @param backend 后端标识
531
+ */
532
+ getPersistent(backend) {
533
+ return this.persistent.get(backend);
534
+ }
535
+ /**
536
+ * 获取指定会话的一次性进程
537
+ * @param sessionId 会话标识
538
+ */
539
+ getEphemeral(sessionId) {
540
+ return this.ephemeral.get(sessionId);
541
+ }
542
+ /** 异步终止所有托管进程 */
543
+ async terminateAll() {
544
+ const promises = [];
545
+ for (const [, proc] of this.ephemeral) {
546
+ proc.terminate();
547
+ }
548
+ for (const [, proc] of this.persistent) {
549
+ promises.push(proc.stop());
550
+ }
551
+ this.ephemeral.clear();
552
+ await Promise.allSettled(promises);
553
+ this.persistent.clear();
554
+ }
555
+ /** 同步终止所有托管进程(用于 exit 钩子) */
556
+ terminateAllSync() {
557
+ for (const [, proc] of this.ephemeral) {
558
+ proc.terminate();
559
+ }
560
+ for (const [, proc] of this.persistent) {
561
+ const pid = proc.pid;
562
+ if (pid) {
563
+ try {
564
+ process.kill(pid, "SIGTERM");
565
+ } catch {
566
+ }
567
+ }
568
+ }
569
+ }
570
+ /**
571
+ * 销毁实例,从全局实例列表中移除
572
+ */
573
+ destroy() {
574
+ _ProcessManager.instances.delete(this);
575
+ }
576
+ };
577
+
578
+ // src/session/session-manager.ts
579
+ var SessionManager = class {
580
+ cache = /* @__PURE__ */ new Map();
581
+ adapters = /* @__PURE__ */ new Map();
582
+ /**
583
+ * 注册适配器用于会话管理
584
+ * @param backend 后端标识
585
+ * @param adapter 适配器实例
586
+ */
587
+ registerAdapter(backend, adapter) {
588
+ this.adapters.set(backend, adapter);
589
+ }
590
+ /**
591
+ * 注销适配器
592
+ * @param backend 后端标识
593
+ */
594
+ unregisterAdapter(backend) {
595
+ this.adapters.delete(backend);
596
+ }
597
+ /**
598
+ * 获取指定后端的适配器,不存在时抛出错误
599
+ * @param backend 后端标识
600
+ */
601
+ getAdapter(backend) {
602
+ const adapter = this.adapters.get(backend);
603
+ if (!adapter) {
604
+ throw new AdapterNotFoundError(backend);
605
+ }
606
+ return adapter;
607
+ }
608
+ /**
609
+ * 列出指定后端的会话,结果会写入缓存
610
+ * @param backend 后端标识
611
+ * @param options 查询选项
612
+ */
613
+ async list(backend, options) {
614
+ const adapter = this.getAdapter(backend);
615
+ const sessions = await adapter.listSessions(options);
616
+ for (const session of sessions) {
617
+ this.cache.set(session.sessionId, session);
618
+ }
619
+ return sessions;
620
+ }
621
+ /**
622
+ * 从缓存中获取会话信息
623
+ * @param sessionId 会话 ID
624
+ */
625
+ get(sessionId) {
626
+ return this.cache.get(sessionId);
627
+ }
628
+ /**
629
+ * 恢复会话,委托适配器以 resume 标志执行查询
630
+ * @param backend 后端标识
631
+ * @param sessionId 要恢复的会话 ID
632
+ * @param prompt 提示词
633
+ */
634
+ async *resume(backend, sessionId, prompt) {
635
+ const adapter = this.getAdapter(backend);
636
+ yield* adapter.query({ prompt, resume: sessionId });
637
+ }
638
+ /**
639
+ * 从缓存中移除指定会话
640
+ * @param sessionId 会话 ID
641
+ */
642
+ evict(sessionId) {
643
+ this.cache.delete(sessionId);
644
+ }
645
+ /**
646
+ * 清空全部会话缓存
647
+ */
648
+ clearCache() {
649
+ this.cache.clear();
650
+ }
651
+ };
652
+
653
+ // src/adapters/claude/index.ts
654
+ import { readFile, writeFile } from "fs/promises";
655
+ import { homedir } from "os";
656
+ import { join } from "path";
657
+ import { randomUUID } from "crypto";
658
+
659
+ // src/adapters/base.ts
660
+ function satisfiesRange(version, range) {
661
+ const parseVersion = (v) => {
662
+ const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(v.trim());
663
+ if (!match) return null;
664
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
665
+ };
666
+ const trimmed = range.trim();
667
+ if (trimmed.startsWith(">=")) {
668
+ const rangeVersion2 = parseVersion(trimmed.slice(2));
669
+ const currentVersion2 = parseVersion(version);
670
+ if (!rangeVersion2 || !currentVersion2) return false;
671
+ for (let i = 0; i < 3; i++) {
672
+ if (currentVersion2[i] > rangeVersion2[i]) return true;
673
+ if (currentVersion2[i] < rangeVersion2[i]) return false;
674
+ }
675
+ return true;
676
+ }
677
+ if (trimmed.startsWith("<=")) {
678
+ const rangeVersion2 = parseVersion(trimmed.slice(2));
679
+ const currentVersion2 = parseVersion(version);
680
+ if (!rangeVersion2 || !currentVersion2) return false;
681
+ for (let i = 0; i < 3; i++) {
682
+ if (currentVersion2[i] < rangeVersion2[i]) return true;
683
+ if (currentVersion2[i] > rangeVersion2[i]) return false;
684
+ }
685
+ return true;
686
+ }
687
+ const rangeVersion = parseVersion(trimmed);
688
+ const currentVersion = parseVersion(version);
689
+ if (!rangeVersion || !currentVersion) return false;
690
+ return currentVersion[0] === rangeVersion[0] && currentVersion[1] === rangeVersion[1] && currentVersion[2] === rangeVersion[2];
691
+ }
692
+ var AbstractAgentAdapter = class {
693
+ /** 活跃的 AbortController 映射,按 sessionId 索引 */
694
+ abortControllers = /* @__PURE__ */ new Map();
695
+ processManager;
696
+ config;
697
+ constructor(processManager, config) {
698
+ this.processManager = processManager;
699
+ this.config = config;
700
+ }
701
+ /**
702
+ * 默认兼容性检查:获取当前版本并与 testedVersionRange 进行比较
703
+ */
704
+ async checkCompatibility() {
705
+ const warnings = [];
706
+ let version;
707
+ try {
708
+ version = await this.getVersion();
709
+ } catch {
710
+ return {
711
+ version: "unknown",
712
+ supported: false,
713
+ warnings: [`\u65E0\u6CD5\u83B7\u53D6 ${this.backend} \u7248\u672C\u4FE1\u606F`]
714
+ };
715
+ }
716
+ const capabilities = this.getCapabilities();
717
+ const supported = satisfiesRange(version, capabilities.testedVersionRange);
718
+ if (!supported) {
719
+ warnings.push(
720
+ `${this.backend} \u7248\u672C ${version} \u4E0D\u5728\u6D4B\u8BD5\u8303\u56F4 ${capabilities.testedVersionRange} \u5185\uFF0C\u53EF\u80FD\u5B58\u5728\u517C\u5BB9\u6027\u95EE\u9898`
721
+ );
722
+ }
723
+ return { version, supported, warnings };
724
+ }
725
+ /**
726
+ * 中止指定会话的执行
727
+ * @param sessionId 会话标识
728
+ */
729
+ abort(sessionId) {
730
+ const controller = this.abortControllers.get(sessionId);
731
+ if (controller) {
732
+ controller.abort();
733
+ this.abortControllers.delete(sessionId);
734
+ }
735
+ }
736
+ /**
737
+ * 创建并注册 AbortController
738
+ * @param sessionId 会话标识
739
+ * @returns AbortController 实例
740
+ */
741
+ createAbortController(sessionId) {
742
+ const existing = this.abortControllers.get(sessionId);
743
+ if (existing) {
744
+ existing.abort();
745
+ }
746
+ const controller = new AbortController();
747
+ this.abortControllers.set(sessionId, controller);
748
+ return controller;
749
+ }
750
+ /**
751
+ * 清理指定会话的 AbortController
752
+ * @param sessionId 会话标识
753
+ */
754
+ removeAbortController(sessionId) {
755
+ this.abortControllers.delete(sessionId);
756
+ }
757
+ };
758
+
759
+ // src/adapters/claude/types.ts
760
+ function isSystemEvent(event) {
761
+ if (typeof event !== "object" || event === null) return false;
762
+ return event["type"] === "system";
763
+ }
764
+ function isAssistantEvent(event) {
765
+ if (typeof event !== "object" || event === null) return false;
766
+ const obj = event;
767
+ return obj["type"] === "assistant" && typeof obj["message"] === "object" && obj["message"] !== null;
768
+ }
769
+ function isStreamEvent(event) {
770
+ if (typeof event !== "object" || event === null) return false;
771
+ const obj = event;
772
+ return obj["type"] === "stream_event" && typeof obj["event"] === "object" && obj["event"] !== null;
773
+ }
774
+ function isResultEvent(event) {
775
+ if (typeof event !== "object" || event === null) return false;
776
+ const obj = event;
777
+ return obj["type"] === "result" && "result" in obj;
778
+ }
779
+
780
+ // src/adapters/claude/message-mapper.ts
781
+ function mapClaudeEvent(event, sessionId) {
782
+ const timestamp = Date.now();
783
+ const baseFields = { timestamp, sessionId, raw: event };
784
+ if (isSystemEvent(event)) {
785
+ return {
786
+ ...baseFields,
787
+ type: "system",
788
+ model: event.model,
789
+ tools: event.tools,
790
+ cwd: event.cwd
791
+ };
792
+ }
793
+ if (isAssistantEvent(event)) {
794
+ const content = event.message.content;
795
+ return mapAssistantContent(content, baseFields);
796
+ }
797
+ if (isStreamEvent(event)) {
798
+ const delta = event.event.delta;
799
+ if (delta?.type === "text_delta" && delta.text) {
800
+ return {
801
+ ...baseFields,
802
+ type: "stream_chunk",
803
+ text: delta.text
804
+ };
805
+ }
806
+ return null;
807
+ }
808
+ if (isResultEvent(event)) {
809
+ return {
810
+ ...baseFields,
811
+ type: "result",
812
+ text: event.result,
813
+ costUsd: event.cost_usd,
814
+ durationMs: event.duration_ms,
815
+ inputTokens: event.input_tokens,
816
+ outputTokens: event.output_tokens
817
+ };
818
+ }
819
+ return null;
820
+ }
821
+ function mapAssistantContent(blocks, baseFields) {
822
+ if (!blocks || blocks.length === 0) return null;
823
+ const textParts = [];
824
+ let firstToolUse = null;
825
+ for (const block of blocks) {
826
+ if (block.type === "text" && block.text) {
827
+ textParts.push(block.text);
828
+ }
829
+ if (block.type === "tool_use" && !firstToolUse) {
830
+ firstToolUse = { name: block.name, input: block.input };
831
+ }
832
+ }
833
+ if (textParts.length > 0) {
834
+ return {
835
+ ...baseFields,
836
+ type: "assistant",
837
+ text: textParts.join("\n")
838
+ };
839
+ }
840
+ if (firstToolUse) {
841
+ return {
842
+ ...baseFields,
843
+ type: "tool_use",
844
+ toolName: firstToolUse.name,
845
+ input: firstToolUse.input
846
+ };
847
+ }
848
+ return null;
849
+ }
850
+
851
+ // src/adapters/claude/index.ts
852
+ var CLAUDE_SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
853
+ var CLAUDE_MODELS = [
854
+ { id: "sonnet", name: "Sonnet", provider: "anthropic" },
855
+ { id: "opus", name: "Opus", provider: "anthropic" },
856
+ { id: "haiku", name: "Haiku", provider: "anthropic" }
857
+ ];
858
+ var ClaudeAdapter = class extends AbstractAgentAdapter {
859
+ backend = "claude";
860
+ /**
861
+ * 检查 Claude CLI 是否可用
862
+ */
863
+ async isAvailable() {
864
+ try {
865
+ const proc = new EphemeralProcess();
866
+ const result = await proc.execute({
867
+ command: "which",
868
+ args: ["claude"]
869
+ });
870
+ return result.exitCode === 0;
871
+ } catch {
872
+ return false;
873
+ }
874
+ }
875
+ /**
876
+ * 获取 Claude CLI 版本号
877
+ */
878
+ async getVersion() {
879
+ const proc = new EphemeralProcess();
880
+ const result = await proc.execute({
881
+ command: "claude",
882
+ args: ["--version"]
883
+ });
884
+ if (result.exitCode !== 0) {
885
+ throw new Error(`\u83B7\u53D6 Claude \u7248\u672C\u5931\u8D25: ${result.stderr}`);
886
+ }
887
+ const output = result.stdout.trim();
888
+ const match = /(\d+\.\d+\.\d+)/.exec(output);
889
+ if (!match) {
890
+ throw new Error(`\u65E0\u6CD5\u89E3\u6790 Claude \u7248\u672C\u53F7: ${output}`);
891
+ }
892
+ return match[1];
893
+ }
894
+ /**
895
+ * 获取 Claude 适配器能力声明
896
+ */
897
+ getCapabilities() {
898
+ return {
899
+ features: /* @__PURE__ */ new Set([
900
+ "streaming",
901
+ "session-resume",
902
+ "system-prompt",
903
+ "tool-restriction",
904
+ "mcp",
905
+ "subagents",
906
+ "worktree"
907
+ ]),
908
+ outputFormats: ["text", "json", "stream-json"],
909
+ testedVersionRange: ">=1.0.0"
910
+ };
911
+ }
912
+ /**
913
+ * 向 Claude 发送提示并以流式方式接收响应
914
+ * @param request 提示请求参数
915
+ */
916
+ async *query(request) {
917
+ const sessionId = request.resume ?? randomUUID();
918
+ const controller = this.createAbortController(sessionId);
919
+ const proc = this.processManager.createEphemeral(sessionId);
920
+ try {
921
+ const args = this.buildArgs(request);
922
+ const cwd = request.cwd ?? this.config?.cwd;
923
+ const cleanEnv = {};
924
+ for (const [k, v] of Object.entries(process.env)) {
925
+ if (k !== "CLAUDECODE" && v !== void 0) {
926
+ cleanEnv[k] = v;
927
+ }
928
+ }
929
+ const proxyUrl = this.config?.proxy?.url;
930
+ if (proxyUrl) {
931
+ this.applyProxyEnv(cleanEnv, proxyUrl, this.config?.proxy?.noProxy);
932
+ }
933
+ if (request.envVars) {
934
+ for (const [k, v] of Object.entries(request.envVars)) {
935
+ if (v) cleanEnv[k] = v;
936
+ }
937
+ }
938
+ const streamOptions = {
939
+ command: "claude",
940
+ args,
941
+ ...cwd ? { cwd } : {},
942
+ env: cleanEnv,
943
+ timeoutMs: request.timeoutMs
944
+ };
945
+ const cmdStr = ["claude", ...args.map((a) => /[\s"']/.test(a) ? JSON.stringify(a) : a)].join(" ");
946
+ const debugCmd = cwd ? `cd ${JSON.stringify(cwd)} && ${cmdStr}` : cmdStr;
947
+ yield {
948
+ type: "system",
949
+ timestamp: Date.now(),
950
+ sessionId,
951
+ cwd,
952
+ raw: { debug_command: debugCmd }
953
+ };
954
+ for await (const line of proc.executeStreaming(
955
+ streamOptions,
956
+ controller.signal
957
+ )) {
958
+ const trimmed = line.trim();
959
+ if (!trimmed) continue;
960
+ let parsed;
961
+ try {
962
+ parsed = JSON.parse(trimmed);
963
+ } catch {
964
+ continue;
965
+ }
966
+ const message = mapClaudeEvent(parsed, sessionId);
967
+ if (message) {
968
+ yield message;
969
+ }
970
+ }
971
+ } finally {
972
+ this.removeAbortController(sessionId);
973
+ this.processManager.removeEphemeral(sessionId);
974
+ }
975
+ }
976
+ /**
977
+ * 列出 Claude 会话
978
+ * @param options 查询选项
979
+ */
980
+ async listSessions(options) {
981
+ const proc = new EphemeralProcess();
982
+ const args = ["sessions", "list", "--output", "json"];
983
+ if (options?.limit) {
984
+ args.push("--limit", String(options.limit));
985
+ }
986
+ const result = await proc.execute({
987
+ command: "claude",
988
+ args,
989
+ cwd: options?.cwd
990
+ });
991
+ if (result.exitCode !== 0) {
992
+ throw new Error(`\u83B7\u53D6 Claude \u4F1A\u8BDD\u5217\u8868\u5931\u8D25: ${result.stderr}`);
993
+ }
994
+ const output = result.stdout.trim();
995
+ if (!output) return [];
996
+ let rawSessions;
997
+ try {
998
+ rawSessions = JSON.parse(output);
999
+ } catch {
1000
+ throw new Error(`\u89E3\u6790 Claude \u4F1A\u8BDD\u5217\u8868\u5931\u8D25: ${output.slice(0, 200)}`);
1001
+ }
1002
+ if (!Array.isArray(rawSessions)) return [];
1003
+ return rawSessions.map((session) => {
1004
+ const s = session;
1005
+ return {
1006
+ sessionId: String(s["id"] ?? s["session_id"] ?? ""),
1007
+ backend: this.backend,
1008
+ summary: typeof s["summary"] === "string" ? s["summary"] : void 0,
1009
+ lastModified: typeof s["last_modified"] === "number" ? s["last_modified"] : typeof s["last_modified"] === "string" ? new Date(s["last_modified"]).getTime() : Date.now(),
1010
+ cwd: typeof s["cwd"] === "string" ? s["cwd"] : void 0,
1011
+ metadata: s
1012
+ };
1013
+ });
1014
+ }
1015
+ /**
1016
+ * 获取 Claude 可用模型列表。
1017
+ * Claude CLI 支持使用简短别名(sonnet/opus/haiku)自动选择当前最新版本。
1018
+ */
1019
+ async listModels() {
1020
+ return CLAUDE_MODELS;
1021
+ }
1022
+ /**
1023
+ * 读取 Claude 设置
1024
+ */
1025
+ async getSettings() {
1026
+ try {
1027
+ const content = await readFile(CLAUDE_SETTINGS_PATH, "utf-8");
1028
+ const parsed = JSON.parse(content);
1029
+ if (typeof parsed === "object" && parsed !== null) {
1030
+ return parsed;
1031
+ }
1032
+ return {};
1033
+ } catch {
1034
+ return {};
1035
+ }
1036
+ }
1037
+ /**
1038
+ * 更新 Claude 设置(读取-合并-写入)
1039
+ * @param settings 要合并的设置项
1040
+ */
1041
+ async updateSettings(settings) {
1042
+ const current = await this.getSettings();
1043
+ const merged = { ...current, ...settings };
1044
+ await writeFile(
1045
+ CLAUDE_SETTINGS_PATH,
1046
+ JSON.stringify(merged, null, 2),
1047
+ "utf-8"
1048
+ );
1049
+ }
1050
+ /**
1051
+ * 释放适配器资源
1052
+ */
1053
+ async dispose() {
1054
+ for (const [sessionId] of this.abortControllers) {
1055
+ this.abort(sessionId);
1056
+ }
1057
+ }
1058
+ /**
1059
+ * 将代理配置注入到环境变量中
1060
+ * @param env 环境变量对象
1061
+ * @param proxyUrl 代理地址
1062
+ * @param noProxy 不走代理的地址列表
1063
+ */
1064
+ applyProxyEnv(env, proxyUrl, noProxy) {
1065
+ env["http_proxy"] = proxyUrl;
1066
+ env["https_proxy"] = proxyUrl;
1067
+ env["HTTP_PROXY"] = proxyUrl;
1068
+ env["HTTPS_PROXY"] = proxyUrl;
1069
+ env["all_proxy"] = proxyUrl;
1070
+ env["ALL_PROXY"] = proxyUrl;
1071
+ if (noProxy) {
1072
+ env["no_proxy"] = noProxy;
1073
+ env["NO_PROXY"] = noProxy;
1074
+ }
1075
+ }
1076
+ buildArgs(request) {
1077
+ const args = [
1078
+ "-p",
1079
+ request.prompt,
1080
+ "--output-format",
1081
+ "stream-json",
1082
+ "--verbose"
1083
+ ];
1084
+ if (request.resume) {
1085
+ args.push("--resume", request.resume);
1086
+ }
1087
+ const model = request.model ?? this.config?.model;
1088
+ if (model) {
1089
+ args.push("--model", model);
1090
+ }
1091
+ if (request.systemPrompt) {
1092
+ args.push("--system-prompt", request.systemPrompt);
1093
+ }
1094
+ const appendPrompt = request.appendSystemPrompt ?? this.config?.appendSystemPrompt;
1095
+ if (appendPrompt) {
1096
+ args.push("--append-system-prompt", appendPrompt);
1097
+ }
1098
+ if (request.allowedTools && request.allowedTools.length > 0) {
1099
+ for (const tool of request.allowedTools) {
1100
+ args.push("--allowedTools", tool);
1101
+ }
1102
+ }
1103
+ if (request.disallowedTools && request.disallowedTools.length > 0) {
1104
+ for (const tool of request.disallowedTools) {
1105
+ args.push("--disallowedTools", tool);
1106
+ }
1107
+ }
1108
+ if (request.maxTurns !== void 0) {
1109
+ args.push("--max-turns", String(request.maxTurns));
1110
+ }
1111
+ if (request.mcpServers) {
1112
+ for (const [name, config] of Object.entries(request.mcpServers)) {
1113
+ const serverSpec = {
1114
+ command: config.command,
1115
+ ...config.args ? { args: config.args } : {},
1116
+ ...config.env ? { env: config.env } : {},
1117
+ ...config.url ? { url: config.url } : {}
1118
+ };
1119
+ args.push("--mcp-server", `${name}=${JSON.stringify(serverSpec)}`);
1120
+ }
1121
+ }
1122
+ return args;
1123
+ }
1124
+ };
1125
+
1126
+ // src/adapters/opencode/index.ts
1127
+ import { readFile as readFile2, writeFile as writeFile2, mkdir } from "fs/promises";
1128
+ import { homedir as homedir2 } from "os";
1129
+ import { join as join2 } from "path";
1130
+
1131
+ // src/adapters/opencode/types.ts
1132
+ var OpenCodeApiError = class extends Error {
1133
+ statusCode;
1134
+ responseBody;
1135
+ constructor(statusCode, responseBody) {
1136
+ super(`OpenCode API error (${statusCode}): ${responseBody}`);
1137
+ this.name = "OpenCodeApiError";
1138
+ this.statusCode = statusCode;
1139
+ this.responseBody = responseBody;
1140
+ }
1141
+ };
1142
+
1143
+ // src/adapters/opencode/api-client.ts
1144
+ var DEFAULT_TIMEOUT_MS = 3e4;
1145
+ var OpenCodeApiClient = class {
1146
+ baseUrl;
1147
+ /**
1148
+ * @param baseUrl opencode serve 服务的基础 URL,例如 http://localhost:39393
1149
+ */
1150
+ constructor(baseUrl) {
1151
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
1152
+ }
1153
+ /**
1154
+ * 创建新会话
1155
+ * @param cwd 可选的工作目录
1156
+ * @returns 包含会话 ID 的对象
1157
+ */
1158
+ async createSession(cwd) {
1159
+ const body = {};
1160
+ if (cwd) {
1161
+ body.cwd = cwd;
1162
+ }
1163
+ const response = await this.request(
1164
+ "/api/session",
1165
+ {
1166
+ method: "POST",
1167
+ body: JSON.stringify(body)
1168
+ }
1169
+ );
1170
+ return response;
1171
+ }
1172
+ /**
1173
+ * 获取所有会话列表
1174
+ * @returns 会话数组
1175
+ */
1176
+ async listSessions() {
1177
+ const response = await this.request(
1178
+ "/api/sessions",
1179
+ { method: "GET" }
1180
+ );
1181
+ return response.sessions;
1182
+ }
1183
+ /**
1184
+ * 向指定会话发送消息
1185
+ * @param sessionId 会话 ID
1186
+ * @param content 消息内容
1187
+ */
1188
+ async sendMessage(sessionId, content) {
1189
+ await this.request(
1190
+ `/api/session/${encodeURIComponent(sessionId)}/message`,
1191
+ {
1192
+ method: "POST",
1193
+ body: JSON.stringify({ content })
1194
+ }
1195
+ );
1196
+ }
1197
+ /**
1198
+ * 以 SSE 流式方式订阅会话事件
1199
+ * @param sessionId 会话 ID
1200
+ * @param abortSignal 可选的中止信号
1201
+ * @yields OpenCode SSE 事件
1202
+ */
1203
+ async *streamEvents(sessionId, abortSignal) {
1204
+ const url = `${this.baseUrl}/api/session/${encodeURIComponent(sessionId)}/events`;
1205
+ const response = await fetch(url, {
1206
+ method: "GET",
1207
+ headers: {
1208
+ Accept: "text/event-stream",
1209
+ "Cache-Control": "no-cache"
1210
+ },
1211
+ signal: abortSignal
1212
+ });
1213
+ if (!response.ok) {
1214
+ const body = await response.text().catch(() => "");
1215
+ throw new OpenCodeApiError(response.status, body);
1216
+ }
1217
+ if (!response.body) {
1218
+ throw new Error("Response body is null, SSE streaming unavailable");
1219
+ }
1220
+ const reader = response.body.getReader();
1221
+ const decoder = new TextDecoder();
1222
+ let buffer = "";
1223
+ try {
1224
+ while (true) {
1225
+ const { done, value } = await reader.read();
1226
+ if (done) break;
1227
+ buffer += decoder.decode(value, { stream: true });
1228
+ const parts = buffer.split("\n\n");
1229
+ buffer = parts.pop() ?? "";
1230
+ for (const part of parts) {
1231
+ const event = this.parseSseEvent(part);
1232
+ if (event) {
1233
+ yield event;
1234
+ if (event.type === "done") {
1235
+ return;
1236
+ }
1237
+ }
1238
+ }
1239
+ }
1240
+ if (buffer.trim()) {
1241
+ const event = this.parseSseEvent(buffer);
1242
+ if (event) {
1243
+ yield event;
1244
+ }
1245
+ }
1246
+ } finally {
1247
+ reader.releaseLock();
1248
+ }
1249
+ }
1250
+ /**
1251
+ * 健康检查:验证 opencode serve 服务是否可达
1252
+ * @returns 服务是否健康
1253
+ */
1254
+ async healthCheck() {
1255
+ try {
1256
+ const controller = new AbortController();
1257
+ const timer = setTimeout(() => controller.abort(), 5e3);
1258
+ const response = await fetch(`${this.baseUrl}/api/sessions`, {
1259
+ method: "GET",
1260
+ signal: controller.signal
1261
+ });
1262
+ clearTimeout(timer);
1263
+ return response.ok;
1264
+ } catch {
1265
+ return false;
1266
+ }
1267
+ }
1268
+ /**
1269
+ * 解析单个 SSE 事件文本块
1270
+ * @param raw 原始 SSE 文本块
1271
+ * @returns 解析后的事件对象,解析失败返回 null
1272
+ */
1273
+ parseSseEvent(raw) {
1274
+ const lines = raw.split("\n");
1275
+ let data = "";
1276
+ for (const line of lines) {
1277
+ if (line.startsWith(":")) continue;
1278
+ if (line.startsWith("data: ")) {
1279
+ data += line.slice(6);
1280
+ } else if (line.startsWith("data:")) {
1281
+ data += line.slice(5);
1282
+ }
1283
+ }
1284
+ if (!data) return null;
1285
+ try {
1286
+ return JSON.parse(data);
1287
+ } catch {
1288
+ return null;
1289
+ }
1290
+ }
1291
+ /**
1292
+ * 发送 HTTP 请求的通用方法
1293
+ * @param path API 路径
1294
+ * @param init fetch 请求选项
1295
+ * @returns 解析后的 JSON 响应
1296
+ */
1297
+ async request(path, init) {
1298
+ const controller = new AbortController();
1299
+ const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
1300
+ try {
1301
+ const response = await fetch(`${this.baseUrl}${path}`, {
1302
+ ...init,
1303
+ headers: {
1304
+ "Content-Type": "application/json",
1305
+ ...init.headers
1306
+ },
1307
+ signal: controller.signal
1308
+ });
1309
+ if (!response.ok) {
1310
+ const body = await response.text().catch(() => "");
1311
+ throw new OpenCodeApiError(response.status, body);
1312
+ }
1313
+ const text = await response.text();
1314
+ if (!text) return {};
1315
+ return JSON.parse(text);
1316
+ } finally {
1317
+ clearTimeout(timer);
1318
+ }
1319
+ }
1320
+ };
1321
+
1322
+ // src/adapters/opencode/message-mapper.ts
1323
+ function mapOpenCodeEventToMessage(event) {
1324
+ const base = {
1325
+ timestamp: event.timestamp ?? Date.now(),
1326
+ sessionId: event.sessionId,
1327
+ raw: event
1328
+ };
1329
+ switch (event.type) {
1330
+ case "session.created":
1331
+ return {
1332
+ ...base,
1333
+ type: "system",
1334
+ model: event.model,
1335
+ tools: event.tools,
1336
+ cwd: event.cwd
1337
+ };
1338
+ case "message.delta":
1339
+ return {
1340
+ ...base,
1341
+ type: "stream_chunk",
1342
+ text: event.text
1343
+ };
1344
+ case "message.complete":
1345
+ return {
1346
+ ...base,
1347
+ type: "result",
1348
+ text: event.text,
1349
+ durationMs: event.durationMs,
1350
+ costUsd: event.costUsd,
1351
+ inputTokens: event.inputTokens,
1352
+ outputTokens: event.outputTokens
1353
+ };
1354
+ case "tool_use.start":
1355
+ return {
1356
+ ...base,
1357
+ type: "tool_use",
1358
+ toolName: event.toolName,
1359
+ input: event.input
1360
+ };
1361
+ case "tool_use.complete":
1362
+ return {
1363
+ ...base,
1364
+ type: "tool_use",
1365
+ toolName: event.toolName,
1366
+ input: event.input,
1367
+ output: event.output
1368
+ };
1369
+ case "error":
1370
+ return {
1371
+ ...base,
1372
+ type: "error",
1373
+ message: event.message,
1374
+ code: event.code
1375
+ };
1376
+ // message.start / tool_use.delta / done 事件不需要映射为独立消息
1377
+ case "message.start":
1378
+ case "tool_use.delta":
1379
+ case "done":
1380
+ return null;
1381
+ default:
1382
+ return null;
1383
+ }
1384
+ }
1385
+
1386
+ // src/adapters/opencode/index.ts
1387
+ var DEFAULT_SERVE_PORT = 39393;
1388
+ var SERVE_READY_TIMEOUT_MS = 3e4;
1389
+ var SERVE_POLL_INTERVAL_MS = 500;
1390
+ var TESTED_VERSION_RANGE = ">=0.1.0";
1391
+ var MAX_RESTARTS = 3;
1392
+ var RESTART_INTERVAL_MS = 2e3;
1393
+ var HEALTH_CHECK_INTERVAL_MS = 3e4;
1394
+ var OpenCodeAdapter = class extends AbstractAgentAdapter {
1395
+ backend = "opencode";
1396
+ apiClient = null;
1397
+ servePort;
1398
+ /**
1399
+ * @param processManager 进程管理器实例
1400
+ * @param servePort 可选的 serve 服务端口
1401
+ */
1402
+ constructor(processManager, servePort) {
1403
+ super(processManager);
1404
+ this.servePort = servePort ?? DEFAULT_SERVE_PORT;
1405
+ }
1406
+ /**
1407
+ * 检查 opencode 命令是否可用
1408
+ */
1409
+ async isAvailable() {
1410
+ try {
1411
+ const proc = new EphemeralProcess();
1412
+ const result = await proc.execute({
1413
+ command: "opencode",
1414
+ args: ["--version"],
1415
+ timeoutMs: 1e4
1416
+ });
1417
+ return result.exitCode === 0;
1418
+ } catch {
1419
+ return false;
1420
+ }
1421
+ }
1422
+ /**
1423
+ * 获取 opencode 版本号
1424
+ */
1425
+ async getVersion() {
1426
+ const proc = new EphemeralProcess();
1427
+ const result = await proc.execute({
1428
+ command: "opencode",
1429
+ args: ["version"],
1430
+ timeoutMs: 1e4
1431
+ });
1432
+ if (result.exitCode !== 0) {
1433
+ throw new Error(`Failed to get opencode version: ${result.stderr}`);
1434
+ }
1435
+ return result.stdout.trim();
1436
+ }
1437
+ /**
1438
+ * 获取适配器能力声明
1439
+ */
1440
+ getCapabilities() {
1441
+ return {
1442
+ features: /* @__PURE__ */ new Set([
1443
+ "streaming",
1444
+ "session-resume",
1445
+ "system-prompt",
1446
+ "serve-mode"
1447
+ ]),
1448
+ outputFormats: ["text"],
1449
+ testedVersionRange: TESTED_VERSION_RANGE
1450
+ };
1451
+ }
1452
+ /**
1453
+ * 执行查询:启动 serve 进程(如未启动),创建/恢复会话,发送消息并流式返回结果
1454
+ * @param request 请求参数
1455
+ * @yields AgentMessage 统一消息流
1456
+ */
1457
+ async *query(request) {
1458
+ const client = await this.ensureServeRunning();
1459
+ let sessionId;
1460
+ if (request.resume) {
1461
+ sessionId = request.resume;
1462
+ } else {
1463
+ const session = await client.createSession(request.cwd);
1464
+ sessionId = session.id;
1465
+ }
1466
+ const controller = this.createAbortController(sessionId);
1467
+ try {
1468
+ await client.sendMessage(sessionId, request.prompt);
1469
+ for await (const event of client.streamEvents(
1470
+ sessionId,
1471
+ controller.signal
1472
+ )) {
1473
+ const message = mapOpenCodeEventToMessage(event);
1474
+ if (message) {
1475
+ yield message;
1476
+ }
1477
+ }
1478
+ } finally {
1479
+ this.removeAbortController(sessionId);
1480
+ }
1481
+ }
1482
+ /**
1483
+ * 列出历史会话
1484
+ * @param options 查询选项
1485
+ */
1486
+ async listSessions(options) {
1487
+ const client = await this.ensureServeRunning();
1488
+ const sessions = await client.listSessions();
1489
+ let filtered = sessions;
1490
+ if (options?.cwd) {
1491
+ filtered = filtered.filter((s) => s.cwd === options.cwd);
1492
+ }
1493
+ if (options?.limit !== void 0 && options.limit > 0) {
1494
+ filtered = filtered.slice(0, options.limit);
1495
+ }
1496
+ return filtered.map((s) => ({
1497
+ sessionId: s.id,
1498
+ backend: this.backend,
1499
+ summary: s.title,
1500
+ lastModified: s.updatedAt,
1501
+ cwd: s.cwd
1502
+ }));
1503
+ }
1504
+ /**
1505
+ * 获取 OpenCode 可用模型列表(通过 `opencode models` 命令)
1506
+ */
1507
+ async listModels() {
1508
+ try {
1509
+ const proc = new EphemeralProcess();
1510
+ const result = await proc.execute({
1511
+ command: "opencode",
1512
+ args: ["models"],
1513
+ timeoutMs: 15e3
1514
+ });
1515
+ if (result.exitCode !== 0) return [];
1516
+ const lines = result.stdout.trim().split("\n").filter(Boolean);
1517
+ return lines.map((line) => {
1518
+ const trimmed = line.trim();
1519
+ const slashIndex = trimmed.indexOf("/");
1520
+ if (slashIndex >= 0) {
1521
+ return {
1522
+ id: trimmed,
1523
+ name: trimmed.slice(slashIndex + 1),
1524
+ provider: trimmed.slice(0, slashIndex)
1525
+ };
1526
+ }
1527
+ return { id: trimmed, name: trimmed };
1528
+ });
1529
+ } catch {
1530
+ return [];
1531
+ }
1532
+ }
1533
+ /**
1534
+ * 读取 opencode 配置文件
1535
+ */
1536
+ async getSettings() {
1537
+ const configPath = this.getConfigPath();
1538
+ try {
1539
+ const content = await readFile2(configPath, "utf-8");
1540
+ return JSON.parse(content);
1541
+ } catch {
1542
+ return {};
1543
+ }
1544
+ }
1545
+ /**
1546
+ * 更新 opencode 配置文件(读取-合并-写入)
1547
+ * @param settings 要合并的配置项
1548
+ */
1549
+ async updateSettings(settings) {
1550
+ const configPath = this.getConfigPath();
1551
+ const configDir = join2(homedir2(), ".config", "opencode");
1552
+ let existing;
1553
+ try {
1554
+ const content = await readFile2(configPath, "utf-8");
1555
+ existing = JSON.parse(content);
1556
+ } catch {
1557
+ existing = {};
1558
+ }
1559
+ const merged = { ...existing, ...settings };
1560
+ await mkdir(configDir, { recursive: true });
1561
+ await writeFile2(configPath, JSON.stringify(merged, null, 2), "utf-8");
1562
+ }
1563
+ /**
1564
+ * 释放资源:停止 serve 进程,中止所有活跃流
1565
+ */
1566
+ async dispose() {
1567
+ for (const [sessionId] of this.abortControllers) {
1568
+ this.abort(sessionId);
1569
+ }
1570
+ const persistent = this.processManager.getPersistent(this.backend);
1571
+ if (persistent) {
1572
+ await persistent.stop();
1573
+ }
1574
+ this.apiClient = null;
1575
+ }
1576
+ /**
1577
+ * 确保 opencode serve 进程已启动并可用
1578
+ * @returns 可用的 API 客户端
1579
+ */
1580
+ async ensureServeRunning() {
1581
+ if (this.apiClient) {
1582
+ const healthy = await this.apiClient.healthCheck();
1583
+ if (healthy) return this.apiClient;
1584
+ }
1585
+ const client = new OpenCodeApiClient(
1586
+ `http://localhost:${this.servePort}`
1587
+ );
1588
+ const alreadyRunning = await client.healthCheck();
1589
+ if (alreadyRunning) {
1590
+ this.apiClient = client;
1591
+ return client;
1592
+ }
1593
+ await this.processManager.startPersistent(this.backend, {
1594
+ command: "opencode",
1595
+ args: ["serve", "--port", String(this.servePort)],
1596
+ maxRestarts: MAX_RESTARTS,
1597
+ restartIntervalMs: RESTART_INTERVAL_MS,
1598
+ healthCheckIntervalMs: HEALTH_CHECK_INTERVAL_MS,
1599
+ healthCheckFn: () => client.healthCheck().then((ok) => {
1600
+ if (!ok) throw new Error("Health check failed");
1601
+ return ok;
1602
+ })
1603
+ });
1604
+ await this.waitForServeReady(client);
1605
+ this.apiClient = client;
1606
+ return client;
1607
+ }
1608
+ /**
1609
+ * 轮询等待 serve 服务可达
1610
+ * @param client API 客户端
1611
+ */
1612
+ async waitForServeReady(client) {
1613
+ const deadline = Date.now() + SERVE_READY_TIMEOUT_MS;
1614
+ while (Date.now() < deadline) {
1615
+ const healthy = await client.healthCheck();
1616
+ if (healthy) return;
1617
+ await this.delay(SERVE_POLL_INTERVAL_MS);
1618
+ }
1619
+ throw new Error(
1620
+ `opencode serve did not become ready within ${SERVE_READY_TIMEOUT_MS}ms`
1621
+ );
1622
+ }
1623
+ /**
1624
+ * 延迟指定毫秒
1625
+ * @param ms 毫秒数
1626
+ */
1627
+ delay(ms) {
1628
+ return new Promise((resolve) => setTimeout(resolve, ms));
1629
+ }
1630
+ /**
1631
+ * 获取 opencode 配置文件路径
1632
+ */
1633
+ getConfigPath() {
1634
+ return join2(homedir2(), ".config", "opencode", "config.json");
1635
+ }
1636
+ };
1637
+
1638
+ // src/porygon.ts
1639
+ var Porygon = class extends EventEmitter2 {
1640
+ config;
1641
+ adapters = /* @__PURE__ */ new Map();
1642
+ interceptors;
1643
+ processManager;
1644
+ sessionManager;
1645
+ constructor(config) {
1646
+ super();
1647
+ this.config = ConfigLoader.load(config);
1648
+ this.interceptors = new InterceptorManager();
1649
+ this.processManager = new ProcessManager();
1650
+ this.sessionManager = new SessionManager();
1651
+ this.registerBuiltinAdapters();
1652
+ }
1653
+ /**
1654
+ * 注册内置适配器
1655
+ */
1656
+ registerBuiltinAdapters() {
1657
+ const claudeConfig = this.config.backends?.["claude"];
1658
+ const mergedClaudeConfig = {
1659
+ ...claudeConfig,
1660
+ proxy: claudeConfig?.proxy ?? this.config.proxy
1661
+ };
1662
+ const claudeAdapter = new ClaudeAdapter(this.processManager, mergedClaudeConfig);
1663
+ this.registerAdapter(claudeAdapter);
1664
+ const opencodeAdapter = new OpenCodeAdapter(this.processManager);
1665
+ this.registerAdapter(opencodeAdapter);
1666
+ }
1667
+ /**
1668
+ * 注册自定义适配器
1669
+ * @param adapter 适配器实例
1670
+ */
1671
+ registerAdapter(adapter) {
1672
+ this.adapters.set(adapter.backend, adapter);
1673
+ this.sessionManager.registerAdapter(adapter.backend, adapter);
1674
+ }
1675
+ /**
1676
+ * 获取指定后端的适配器
1677
+ * @param backend 后端名称,默认使用配置中的 defaultBackend
1678
+ */
1679
+ getAdapter(backend) {
1680
+ const name = backend ?? this.config.defaultBackend ?? "claude";
1681
+ const adapter = this.adapters.get(name);
1682
+ if (!adapter) throw new AdapterNotFoundError(name);
1683
+ return adapter;
1684
+ }
1685
+ /**
1686
+ * 流式查询,返回 AgentMessage 异步生成器。
1687
+ * 作为与 LLM 后端交互的主要入口。
1688
+ * @param request 提示请求参数
1689
+ * @yields AgentMessage 消息流
1690
+ */
1691
+ async *query(request) {
1692
+ const adapter = this.getAdapter(request.backend);
1693
+ const backendName = adapter.backend;
1694
+ const mergedRequest = this.mergeRequest(request, backendName);
1695
+ const processedPrompt = await this.interceptors.processInput(
1696
+ mergedRequest.prompt,
1697
+ { backend: backendName, sessionId: mergedRequest.resume }
1698
+ );
1699
+ mergedRequest.prompt = processedPrompt;
1700
+ for await (const message of adapter.query(mergedRequest)) {
1701
+ if (message.type === "assistant" || message.type === "result") {
1702
+ const processedText = await this.interceptors.processOutput(
1703
+ message.text,
1704
+ {
1705
+ backend: backendName,
1706
+ sessionId: message.sessionId,
1707
+ messageType: message.type
1708
+ }
1709
+ );
1710
+ yield { ...message, text: processedText };
1711
+ } else {
1712
+ yield message;
1713
+ }
1714
+ }
1715
+ }
1716
+ /**
1717
+ * 简单运行模式,收集所有消息并返回最终结果文本
1718
+ * @param request 提示请求参数
1719
+ * @returns 最终结果文本
1720
+ */
1721
+ async run(request) {
1722
+ let resultText = "";
1723
+ for await (const msg of this.query(request)) {
1724
+ if (msg.type === "result") {
1725
+ resultText = msg.text;
1726
+ } else if (msg.type === "assistant") {
1727
+ resultText = msg.text;
1728
+ }
1729
+ }
1730
+ return resultText;
1731
+ }
1732
+ /**
1733
+ * 注册拦截器
1734
+ * @param direction 拦截方向
1735
+ * @param fn 拦截器函数
1736
+ * @returns 取消注册的函数
1737
+ */
1738
+ use(direction, fn) {
1739
+ return this.interceptors.use(direction, fn);
1740
+ }
1741
+ /**
1742
+ * 获取后端能力声明
1743
+ * @param backend 后端名称
1744
+ */
1745
+ getCapabilities(backend) {
1746
+ return this.getAdapter(backend).getCapabilities();
1747
+ }
1748
+ /**
1749
+ * 获取指定后端的可用模型列表
1750
+ * @param backend 后端名称
1751
+ */
1752
+ async listModels(backend) {
1753
+ return this.getAdapter(backend).listModels();
1754
+ }
1755
+ /**
1756
+ * 对所有已注册后端进行健康检查
1757
+ */
1758
+ async healthCheck() {
1759
+ const entries = Array.from(this.adapters.entries());
1760
+ const checks = entries.map(async ([name, adapter]) => {
1761
+ try {
1762
+ const available = await adapter.isAvailable();
1763
+ let compatibility = null;
1764
+ if (available) {
1765
+ compatibility = await adapter.checkCompatibility();
1766
+ if (!compatibility.supported) {
1767
+ this.emit(
1768
+ "health:degraded",
1769
+ name,
1770
+ compatibility.warnings.join("; ")
1771
+ );
1772
+ }
1773
+ }
1774
+ return [name, { available, compatibility }];
1775
+ } catch (err) {
1776
+ const errorMsg = err instanceof Error ? err.message : String(err);
1777
+ return [name, { available: false, compatibility: null, error: errorMsg }];
1778
+ }
1779
+ });
1780
+ const settled = await Promise.allSettled(checks);
1781
+ const results = {};
1782
+ for (const item of settled) {
1783
+ if (item.status === "fulfilled") {
1784
+ const [name, result] = item.value;
1785
+ results[name] = result;
1786
+ }
1787
+ }
1788
+ return results;
1789
+ }
1790
+ /**
1791
+ * 读取或更新后端设置
1792
+ * @param backend 后端名称
1793
+ * @param newSettings 要更新的设置项(可选)
1794
+ * @returns 当前设置
1795
+ */
1796
+ async settings(backend, newSettings) {
1797
+ const adapter = this.getAdapter(backend);
1798
+ if (newSettings) {
1799
+ await adapter.updateSettings(newSettings);
1800
+ }
1801
+ return adapter.getSettings();
1802
+ }
1803
+ /**
1804
+ * 列出指定后端的会话
1805
+ * @param backend 后端名称
1806
+ * @param options 查询选项
1807
+ */
1808
+ async listSessions(backend, options) {
1809
+ const name = backend ?? this.config.defaultBackend ?? "claude";
1810
+ return this.sessionManager.list(name, options);
1811
+ }
1812
+ /**
1813
+ * 中止正在运行的查询
1814
+ * @param backend 后端名称
1815
+ * @param sessionId 会话 ID
1816
+ */
1817
+ abort(backend, sessionId) {
1818
+ this.getAdapter(backend).abort(sessionId);
1819
+ }
1820
+ /**
1821
+ * 释放所有资源
1822
+ */
1823
+ async dispose() {
1824
+ const promises = [];
1825
+ for (const [, adapter] of this.adapters) {
1826
+ promises.push(adapter.dispose());
1827
+ }
1828
+ await Promise.allSettled(promises);
1829
+ await this.processManager.terminateAll();
1830
+ this.adapters.clear();
1831
+ this.sessionManager.clearCache();
1832
+ }
1833
+ /**
1834
+ * 合并请求参数与配置默认值
1835
+ * @param request 原始请求
1836
+ * @param backend 后端名称
1837
+ * @returns 合并后的请求
1838
+ */
1839
+ mergeRequest(request, backend) {
1840
+ const backendConfig = this.config.backends?.[backend];
1841
+ const defaults = this.config.defaults;
1842
+ const appendParts = [];
1843
+ if (defaults?.appendSystemPrompt) {
1844
+ appendParts.push(defaults.appendSystemPrompt);
1845
+ }
1846
+ if (backendConfig?.appendSystemPrompt) {
1847
+ appendParts.push(backendConfig.appendSystemPrompt);
1848
+ }
1849
+ if (request.appendSystemPrompt) {
1850
+ appendParts.push(request.appendSystemPrompt);
1851
+ }
1852
+ return {
1853
+ ...request,
1854
+ model: request.model ?? backendConfig?.model,
1855
+ timeoutMs: request.timeoutMs ?? defaults?.timeoutMs,
1856
+ maxTurns: request.maxTurns ?? defaults?.maxTurns,
1857
+ cwd: request.cwd ?? backendConfig?.cwd,
1858
+ appendSystemPrompt: request.systemPrompt ? void 0 : appendParts.length > 0 ? appendParts.join("\n") : void 0
1859
+ };
1860
+ }
1861
+ };
1862
+ function createPorygon(config) {
1863
+ return new Porygon(config);
1864
+ }
1865
+
1866
+ // src/interceptor/guard.ts
1867
+ var BUILTIN_INPUT_PATTERNS = [
1868
+ // 中文
1869
+ /忽略.{0,10}(之前|上面|以上|所有).{0,10}(指令|规则|提示|约束)/i,
1870
+ /重复.{0,10}(系统|完整).{0,10}(提示|指令|prompt)/i,
1871
+ /输出.{0,10}(系统|完整).{0,10}(提示|指令|prompt)/i,
1872
+ /告诉我.{0,10}(系统|你的).{0,10}(提示|指令|prompt)/i,
1873
+ /显示.{0,10}(系统|隐藏|完整).{0,10}(提示|指令|prompt)/i,
1874
+ /你的(系统|初始).{0,10}(提示|指令|设定)/i,
1875
+ // English
1876
+ /ignore.{0,15}(previous|above|all|prior).{0,15}(instructions?|rules?|prompts?|constraints?)/i,
1877
+ /repeat.{0,15}(system|full|entire|complete).{0,15}(prompt|instructions?|message)/i,
1878
+ /reveal.{0,15}(system|hidden|full).{0,15}(prompt|instructions?)/i,
1879
+ /show.{0,15}(system|hidden|full).{0,15}(prompt|instructions?)/i,
1880
+ /what.{0,15}(is|are).{0,15}(your|the).{0,15}(system|initial).{0,15}(prompt|instructions?)/i,
1881
+ /print.{0,15}(system|full|entire).{0,15}(prompt|instructions?|message)/i,
1882
+ /disregard.{0,15}(previous|above|all|prior).{0,15}(instructions?|rules?)/i
1883
+ ];
1884
+ function createInputGuard(options) {
1885
+ const patterns = [
1886
+ ...BUILTIN_INPUT_PATTERNS,
1887
+ ...options?.blockedPatterns ?? []
1888
+ ];
1889
+ const action = options?.action ?? "reject";
1890
+ const backends = options?.backends;
1891
+ return (text, context) => {
1892
+ if (backends && !backends.includes(context.backend)) {
1893
+ return void 0;
1894
+ }
1895
+ for (const pattern of patterns) {
1896
+ if (pattern.test(text)) {
1897
+ if (action === "reject") {
1898
+ return false;
1899
+ }
1900
+ return text.replace(pattern, "[REDACTED]");
1901
+ }
1902
+ }
1903
+ if (options?.customCheck?.(text, context)) {
1904
+ if (action === "reject") {
1905
+ return false;
1906
+ }
1907
+ return "[REDACTED]";
1908
+ }
1909
+ return void 0;
1910
+ };
1911
+ }
1912
+ function createOutputGuard(options) {
1913
+ const keywords = options?.sensitiveKeywords ?? [];
1914
+ const action = options?.action ?? "redact";
1915
+ const backends = options?.backends;
1916
+ return (text, context) => {
1917
+ if (backends && !backends.includes(context.backend)) {
1918
+ return void 0;
1919
+ }
1920
+ let matched = false;
1921
+ let result = text;
1922
+ for (const keyword of keywords) {
1923
+ if (text.includes(keyword)) {
1924
+ matched = true;
1925
+ if (action === "redact") {
1926
+ result = result.split(keyword).join("[REDACTED]");
1927
+ }
1928
+ }
1929
+ }
1930
+ if (!matched && options?.customCheck?.(text, context)) {
1931
+ matched = true;
1932
+ if (action === "redact") {
1933
+ result = "[REDACTED]";
1934
+ }
1935
+ }
1936
+ if (!matched) {
1937
+ return void 0;
1938
+ }
1939
+ if (action === "reject") {
1940
+ return false;
1941
+ }
1942
+ return result;
1943
+ };
1944
+ }
1945
+ export {
1946
+ AbstractAgentAdapter,
1947
+ AdapterIncompatibleError,
1948
+ AdapterNotAvailableError,
1949
+ AdapterNotFoundError,
1950
+ AgentExecutionError,
1951
+ AgentTimeoutError,
1952
+ ClaudeAdapter,
1953
+ ConfigValidationError,
1954
+ InterceptorRejectedError,
1955
+ OpenCodeAdapter,
1956
+ Porygon,
1957
+ PorygonError,
1958
+ SessionNotFoundError,
1959
+ createInputGuard,
1960
+ createOutputGuard,
1961
+ createPorygon
1962
+ };
1963
+ //# sourceMappingURL=index.js.map