@snack-kit/porygon 0.1.0 → 0.3.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 CHANGED
@@ -88,6 +88,10 @@ var BackendConfigSchema = z.object({
88
88
  appendSystemPrompt: z.string().optional(),
89
89
  proxy: ProxyConfigSchema.optional(),
90
90
  cwd: z.string().optional(),
91
+ serverUrl: z.string().optional(),
92
+ apiKey: z.string().optional(),
93
+ interactive: z.boolean().optional(),
94
+ cliPath: z.string().optional(),
91
95
  options: z.record(z.string(), z.unknown()).optional()
92
96
  });
93
97
  var PorygonConfigSchema = z.object({
@@ -305,12 +309,18 @@ var EphemeralProcess = class {
305
309
  throw new Error("Process aborted before start");
306
310
  }
307
311
  this.aborted = false;
312
+ const useStdin = options.stdinData !== void 0;
308
313
  const child = spawn(options.command, options.args, {
309
314
  cwd: options.cwd,
310
315
  env: options.env ?? void 0,
311
- stdio: ["ignore", "pipe", "pipe"]
316
+ stdio: [useStdin ? "pipe" : "ignore", "pipe", "pipe"]
312
317
  });
313
318
  this.childProcess = child;
319
+ if (useStdin && child.stdin) {
320
+ child.stdin.write(options.stdinData, () => {
321
+ child.stdin.end();
322
+ });
323
+ }
314
324
  let timeoutTimer;
315
325
  if (options.timeoutMs !== void 0 && options.timeoutMs > 0) {
316
326
  timeoutTimer = setTimeout(() => {
@@ -780,15 +790,16 @@ function isResultEvent(event) {
780
790
  // src/adapters/claude/message-mapper.ts
781
791
  function mapClaudeEvent(event, sessionId) {
782
792
  const timestamp = Date.now();
783
- const baseFields = { timestamp, sessionId, raw: event };
793
+ const resolvedSessionId = event.session_id ?? sessionId;
794
+ const baseFields = { timestamp, sessionId: resolvedSessionId, raw: event };
784
795
  if (isSystemEvent(event)) {
785
- return {
796
+ return [{
786
797
  ...baseFields,
787
798
  type: "system",
788
799
  model: event.model,
789
800
  tools: event.tools,
790
801
  cwd: event.cwd
791
- };
802
+ }];
792
803
  }
793
804
  if (isAssistantEvent(event)) {
794
805
  const content = event.message.content;
@@ -797,16 +808,16 @@ function mapClaudeEvent(event, sessionId) {
797
808
  if (isStreamEvent(event)) {
798
809
  const delta = event.event.delta;
799
810
  if (delta?.type === "text_delta" && delta.text) {
800
- return {
811
+ return [{
801
812
  ...baseFields,
802
813
  type: "stream_chunk",
803
814
  text: delta.text
804
- };
815
+ }];
805
816
  }
806
- return null;
817
+ return [];
807
818
  }
808
819
  if (isResultEvent(event)) {
809
- return {
820
+ return [{
810
821
  ...baseFields,
811
822
  type: "result",
812
823
  text: event.result,
@@ -814,38 +825,52 @@ function mapClaudeEvent(event, sessionId) {
814
825
  durationMs: event.duration_ms,
815
826
  inputTokens: event.input_tokens,
816
827
  outputTokens: event.output_tokens
817
- };
828
+ }];
818
829
  }
819
- return null;
830
+ return [];
820
831
  }
821
832
  function mapAssistantContent(blocks, baseFields) {
822
- if (!blocks || blocks.length === 0) return null;
833
+ if (!blocks || blocks.length === 0) return [];
834
+ const messages = [];
823
835
  const textParts = [];
824
- let firstToolUse = null;
825
836
  for (const block of blocks) {
826
837
  if (block.type === "text" && block.text) {
827
838
  textParts.push(block.text);
828
- }
829
- if (block.type === "tool_use" && !firstToolUse) {
830
- firstToolUse = { name: block.name, input: block.input };
839
+ messages.push({
840
+ ...baseFields,
841
+ type: "stream_chunk",
842
+ text: block.text
843
+ });
831
844
  }
832
845
  }
833
846
  if (textParts.length > 0) {
834
- return {
847
+ messages.push({
835
848
  ...baseFields,
836
849
  type: "assistant",
837
- text: textParts.join("\n")
838
- };
850
+ text: textParts.join("\n"),
851
+ turnComplete: true
852
+ });
839
853
  }
840
- if (firstToolUse) {
841
- return {
842
- ...baseFields,
843
- type: "tool_use",
844
- toolName: firstToolUse.name,
845
- input: firstToolUse.input
846
- };
854
+ for (const block of blocks) {
855
+ if (block.type === "tool_use") {
856
+ messages.push({
857
+ ...baseFields,
858
+ type: "tool_use",
859
+ toolName: block.name,
860
+ input: block.input
861
+ });
862
+ }
863
+ if (block.type === "tool_result") {
864
+ messages.push({
865
+ ...baseFields,
866
+ type: "tool_use",
867
+ toolName: block.tool_use_id,
868
+ input: {},
869
+ output: block.content
870
+ });
871
+ }
847
872
  }
848
- return null;
873
+ return messages;
849
874
  }
850
875
 
851
876
  // src/adapters/claude/index.ts
@@ -857,6 +882,10 @@ var CLAUDE_MODELS = [
857
882
  ];
858
883
  var ClaudeAdapter = class extends AbstractAgentAdapter {
859
884
  backend = "claude";
885
+ /** CLI 命令名或路径 */
886
+ get cliCommand() {
887
+ return this.config?.cliPath ?? "claude";
888
+ }
860
889
  /**
861
890
  * 检查 Claude CLI 是否可用
862
891
  */
@@ -865,7 +894,7 @@ var ClaudeAdapter = class extends AbstractAgentAdapter {
865
894
  const proc = new EphemeralProcess();
866
895
  const result = await proc.execute({
867
896
  command: "which",
868
- args: ["claude"]
897
+ args: [this.cliCommand]
869
898
  });
870
899
  return result.exitCode === 0;
871
900
  } catch {
@@ -878,7 +907,7 @@ var ClaudeAdapter = class extends AbstractAgentAdapter {
878
907
  async getVersion() {
879
908
  const proc = new EphemeralProcess();
880
909
  const result = await proc.execute({
881
- command: "claude",
910
+ command: this.cliCommand,
882
911
  args: ["--version"]
883
912
  });
884
913
  if (result.exitCode !== 0) {
@@ -903,8 +932,10 @@ var ClaudeAdapter = class extends AbstractAgentAdapter {
903
932
  "tool-restriction",
904
933
  "mcp",
905
934
  "subagents",
906
- "worktree"
935
+ "worktree",
936
+ "interactive-session"
907
937
  ]),
938
+ streamingMode: "chunked",
908
939
  outputFormats: ["text", "json", "stream-json"],
909
940
  testedVersionRange: ">=1.0.0"
910
941
  };
@@ -935,14 +966,16 @@ var ClaudeAdapter = class extends AbstractAgentAdapter {
935
966
  if (v) cleanEnv[k] = v;
936
967
  }
937
968
  }
969
+ const stdinData = this.buildStdinData(request);
938
970
  const streamOptions = {
939
- command: "claude",
971
+ command: this.cliCommand,
940
972
  args,
941
973
  ...cwd ? { cwd } : {},
942
974
  env: cleanEnv,
943
- timeoutMs: request.timeoutMs
975
+ timeoutMs: request.timeoutMs,
976
+ stdinData
944
977
  };
945
- const cmdStr = ["claude", ...args.map((a) => /[\s"']/.test(a) ? JSON.stringify(a) : a)].join(" ");
978
+ const cmdStr = [this.cliCommand, ...args.map((a) => /[\s"']/.test(a) ? JSON.stringify(a) : a)].join(" ");
946
979
  const debugCmd = cwd ? `cd ${JSON.stringify(cwd)} && ${cmdStr}` : cmdStr;
947
980
  yield {
948
981
  type: "system",
@@ -963,8 +996,8 @@ var ClaudeAdapter = class extends AbstractAgentAdapter {
963
996
  } catch {
964
997
  continue;
965
998
  }
966
- const message = mapClaudeEvent(parsed, sessionId);
967
- if (message) {
999
+ const messages = mapClaudeEvent(parsed, sessionId);
1000
+ for (const message of messages) {
968
1001
  yield message;
969
1002
  }
970
1003
  }
@@ -984,7 +1017,7 @@ var ClaudeAdapter = class extends AbstractAgentAdapter {
984
1017
  args.push("--limit", String(options.limit));
985
1018
  }
986
1019
  const result = await proc.execute({
987
- command: "claude",
1020
+ command: this.cliCommand,
988
1021
  args,
989
1022
  cwd: options?.cwd
990
1023
  });
@@ -1073,10 +1106,16 @@ var ClaudeAdapter = class extends AbstractAgentAdapter {
1073
1106
  env["NO_PROXY"] = noProxy;
1074
1107
  }
1075
1108
  }
1109
+ /**
1110
+ * 构建通过 stdin 传递给 Claude CLI 的数据。
1111
+ * 使用 stdin 而非 CLI 参数传递 prompt,避免超长参数导致的 403 错误。
1112
+ */
1113
+ buildStdinData(request) {
1114
+ return request.prompt;
1115
+ }
1076
1116
  buildArgs(request) {
1077
1117
  const args = [
1078
- "-p",
1079
- request.prompt,
1118
+ "--print",
1080
1119
  "--output-format",
1081
1120
  "stream-json",
1082
1121
  "--verbose"
@@ -1108,6 +1147,11 @@ var ClaudeAdapter = class extends AbstractAgentAdapter {
1108
1147
  if (request.maxTurns !== void 0) {
1109
1148
  args.push("--max-turns", String(request.maxTurns));
1110
1149
  }
1150
+ const interactive = this.config?.interactive;
1151
+ const skipPerms = interactive === false || request.backendOptions?.dangerouslySkipPermissions || this.config?.options?.dangerouslySkipPermissions;
1152
+ if (skipPerms) {
1153
+ args.push("--dangerously-skip-permissions");
1154
+ }
1111
1155
  if (request.mcpServers) {
1112
1156
  for (const [name, config] of Object.entries(request.mcpServers)) {
1113
1157
  const serverSpec = {
@@ -1144,11 +1188,14 @@ var OpenCodeApiError = class extends Error {
1144
1188
  var DEFAULT_TIMEOUT_MS = 3e4;
1145
1189
  var OpenCodeApiClient = class {
1146
1190
  baseUrl;
1191
+ apiKey;
1147
1192
  /**
1148
1193
  * @param baseUrl opencode serve 服务的基础 URL,例如 http://localhost:39393
1194
+ * @param apiKey 可选的 API Key,用于 Basic Auth 认证(username: opencode, password: apiKey)
1149
1195
  */
1150
- constructor(baseUrl) {
1196
+ constructor(baseUrl, apiKey) {
1151
1197
  this.baseUrl = baseUrl.replace(/\/+$/, "");
1198
+ this.apiKey = apiKey;
1152
1199
  }
1153
1200
  /**
1154
1201
  * 创建新会话
@@ -1206,7 +1253,8 @@ var OpenCodeApiClient = class {
1206
1253
  method: "GET",
1207
1254
  headers: {
1208
1255
  Accept: "text/event-stream",
1209
- "Cache-Control": "no-cache"
1256
+ "Cache-Control": "no-cache",
1257
+ ...this.buildAuthHeaders()
1210
1258
  },
1211
1259
  signal: abortSignal
1212
1260
  });
@@ -1257,6 +1305,7 @@ var OpenCodeApiClient = class {
1257
1305
  const timer = setTimeout(() => controller.abort(), 5e3);
1258
1306
  const response = await fetch(`${this.baseUrl}/api/sessions`, {
1259
1307
  method: "GET",
1308
+ headers: this.buildAuthHeaders(),
1260
1309
  signal: controller.signal
1261
1310
  });
1262
1311
  clearTimeout(timer);
@@ -1294,6 +1343,17 @@ var OpenCodeApiClient = class {
1294
1343
  * @param init fetch 请求选项
1295
1344
  * @returns 解析后的 JSON 响应
1296
1345
  */
1346
+ /**
1347
+ * 构建包含认证信息的请求 headers
1348
+ */
1349
+ buildAuthHeaders() {
1350
+ const headers = {};
1351
+ if (this.apiKey) {
1352
+ const credentials = Buffer.from(`opencode:${this.apiKey}`).toString("base64");
1353
+ headers["Authorization"] = `Basic ${credentials}`;
1354
+ }
1355
+ return headers;
1356
+ }
1297
1357
  async request(path, init) {
1298
1358
  const controller = new AbortController();
1299
1359
  const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
@@ -1302,6 +1362,7 @@ var OpenCodeApiClient = class {
1302
1362
  ...init,
1303
1363
  headers: {
1304
1364
  "Content-Type": "application/json",
1365
+ ...this.buildAuthHeaders(),
1305
1366
  ...init.headers
1306
1367
  },
1307
1368
  signal: controller.signal
@@ -1395,18 +1456,36 @@ var OpenCodeAdapter = class extends AbstractAgentAdapter {
1395
1456
  backend = "opencode";
1396
1457
  apiClient = null;
1397
1458
  servePort;
1459
+ /** 远程服务器 URL(提供时跳过自启 serve) */
1460
+ remoteServerUrl;
1461
+ /** API Key(用于 Basic Auth) */
1462
+ apiKey;
1398
1463
  /**
1399
1464
  * @param processManager 进程管理器实例
1400
- * @param servePort 可选的 serve 服务端口
1465
+ * @param config 可选的后端配置
1466
+ * - serverUrl: 远程 OpenCode 服务地址,提供时跳过自启 serve
1467
+ * - apiKey: API Key,用于 Basic Auth 认证
1468
+ * - options.servePort: 本地 serve 端口(仅自启模式)
1401
1469
  */
1402
- constructor(processManager, servePort) {
1403
- super(processManager);
1404
- this.servePort = servePort ?? DEFAULT_SERVE_PORT;
1470
+ constructor(processManager, config) {
1471
+ super(processManager, config);
1472
+ const opts = config?.options ?? {};
1473
+ this.remoteServerUrl = config?.serverUrl ?? opts.serverUrl;
1474
+ this.apiKey = config?.apiKey ?? opts.apiKey;
1475
+ this.servePort = opts.servePort ?? DEFAULT_SERVE_PORT;
1405
1476
  }
1406
1477
  /**
1407
1478
  * 检查 opencode 命令是否可用
1408
1479
  */
1409
1480
  async isAvailable() {
1481
+ if (this.remoteServerUrl) {
1482
+ try {
1483
+ const client = new OpenCodeApiClient(this.remoteServerUrl, this.apiKey);
1484
+ return await client.healthCheck();
1485
+ } catch {
1486
+ return false;
1487
+ }
1488
+ }
1410
1489
  try {
1411
1490
  const proc = new EphemeralProcess();
1412
1491
  const result = await proc.execute({
@@ -1445,6 +1524,7 @@ var OpenCodeAdapter = class extends AbstractAgentAdapter {
1445
1524
  "system-prompt",
1446
1525
  "serve-mode"
1447
1526
  ]),
1527
+ streamingMode: "delta",
1448
1528
  outputFormats: ["text"],
1449
1529
  testedVersionRange: TESTED_VERSION_RANGE
1450
1530
  };
@@ -1582,8 +1662,20 @@ var OpenCodeAdapter = class extends AbstractAgentAdapter {
1582
1662
  const healthy = await this.apiClient.healthCheck();
1583
1663
  if (healthy) return this.apiClient;
1584
1664
  }
1665
+ if (this.remoteServerUrl) {
1666
+ const client2 = new OpenCodeApiClient(this.remoteServerUrl, this.apiKey);
1667
+ const healthy = await client2.healthCheck();
1668
+ if (!healthy) {
1669
+ throw new Error(
1670
+ `\u65E0\u6CD5\u8FDE\u63A5\u8FDC\u7A0B OpenCode \u670D\u52A1: ${this.remoteServerUrl}`
1671
+ );
1672
+ }
1673
+ this.apiClient = client2;
1674
+ return client2;
1675
+ }
1585
1676
  const client = new OpenCodeApiClient(
1586
- `http://localhost:${this.servePort}`
1677
+ `http://localhost:${this.servePort}`,
1678
+ this.apiKey
1587
1679
  );
1588
1680
  const alreadyRunning = await client.healthCheck();
1589
1681
  if (alreadyRunning) {
@@ -1635,6 +1727,54 @@ var OpenCodeAdapter = class extends AbstractAgentAdapter {
1635
1727
  }
1636
1728
  };
1637
1729
 
1730
+ // src/session/interactive-session.ts
1731
+ var InteractiveSession = class {
1732
+ initialSessionId;
1733
+ resolvedSessionId;
1734
+ adapter;
1735
+ baseRequest;
1736
+ firstSent = false;
1737
+ closed = false;
1738
+ constructor(initialSessionId, adapter, baseRequest) {
1739
+ this.initialSessionId = initialSessionId;
1740
+ this.adapter = adapter;
1741
+ this.baseRequest = baseRequest;
1742
+ }
1743
+ /** 当前生效的 sessionId(首次 send 后反映 CLI 返回的真实 ID) */
1744
+ get sessionId() {
1745
+ return this.resolvedSessionId ?? this.initialSessionId;
1746
+ }
1747
+ /** 会话是否仍然活跃 */
1748
+ get isActive() {
1749
+ return !this.closed;
1750
+ }
1751
+ /**
1752
+ * 发送一条消息,返回流式响应。
1753
+ * 首次调用使用 initialSessionId,后续自动附加 resume。
1754
+ */
1755
+ async *send(prompt) {
1756
+ if (this.closed) {
1757
+ throw new SessionNotFoundError(this.sessionId);
1758
+ }
1759
+ const request = {
1760
+ ...this.baseRequest,
1761
+ prompt,
1762
+ ...this.firstSent ? { resume: this.sessionId } : {}
1763
+ };
1764
+ for await (const msg of this.adapter.query(request)) {
1765
+ if (!this.firstSent && msg.sessionId) {
1766
+ this.resolvedSessionId = msg.sessionId;
1767
+ }
1768
+ yield msg;
1769
+ }
1770
+ this.firstSent = true;
1771
+ }
1772
+ /** 关闭会话(仅清理内部状态,无进程需要释放) */
1773
+ close() {
1774
+ this.closed = true;
1775
+ }
1776
+ };
1777
+
1638
1778
  // src/porygon.ts
1639
1779
  var Porygon = class extends EventEmitter2 {
1640
1780
  config;
@@ -1661,7 +1801,12 @@ var Porygon = class extends EventEmitter2 {
1661
1801
  };
1662
1802
  const claudeAdapter = new ClaudeAdapter(this.processManager, mergedClaudeConfig);
1663
1803
  this.registerAdapter(claudeAdapter);
1664
- const opencodeAdapter = new OpenCodeAdapter(this.processManager);
1804
+ const opencodeConfig = this.config.backends?.["opencode"];
1805
+ const mergedOpencodeConfig = {
1806
+ ...opencodeConfig,
1807
+ proxy: opencodeConfig?.proxy ?? this.config.proxy
1808
+ };
1809
+ const opencodeAdapter = new OpenCodeAdapter(this.processManager, mergedOpencodeConfig);
1665
1810
  this.registerAdapter(opencodeAdapter);
1666
1811
  }
1667
1812
  /**
@@ -1729,6 +1874,21 @@ var Porygon = class extends EventEmitter2 {
1729
1874
  }
1730
1875
  return resultText;
1731
1876
  }
1877
+ /**
1878
+ * 创建交互式多轮对话会话。
1879
+ * 自动管理 sessionId 和 resume,对调用方透明。
1880
+ */
1881
+ session(options) {
1882
+ const backend = options?.backend ?? this.config.defaultBackend ?? "claude";
1883
+ const adapter = this.getAdapter(backend);
1884
+ const merged = this.mergeRequest({ ...options, prompt: "" }, backend);
1885
+ const { prompt: _, ...baseRequest } = merged;
1886
+ return new InteractiveSession(
1887
+ crypto.randomUUID(),
1888
+ adapter,
1889
+ baseRequest
1890
+ );
1891
+ }
1732
1892
  /**
1733
1893
  * 注册拦截器
1734
1894
  * @param direction 拦截方向
@@ -1753,29 +1913,45 @@ var Porygon = class extends EventEmitter2 {
1753
1913
  return this.getAdapter(backend).listModels();
1754
1914
  }
1755
1915
  /**
1756
- * 对所有已注册后端进行健康检查
1916
+ * 检查单个后端的健康状态
1917
+ * @param backend 后端名称
1757
1918
  */
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 }];
1919
+ async checkBackend(backend) {
1920
+ const adapter = this.getAdapter(backend);
1921
+ try {
1922
+ const available = await adapter.isAvailable();
1923
+ if (!available) {
1924
+ return { available: false };
1778
1925
  }
1926
+ const compat = await adapter.checkCompatibility();
1927
+ const result = {
1928
+ available: true,
1929
+ version: compat.version,
1930
+ supported: compat.supported
1931
+ };
1932
+ if (compat.warnings.length > 0) {
1933
+ result.warnings = compat.warnings;
1934
+ }
1935
+ if (!compat.supported) {
1936
+ this.emit("health:degraded", backend, compat.warnings.join("; "));
1937
+ }
1938
+ return result;
1939
+ } catch (err) {
1940
+ return {
1941
+ available: false,
1942
+ error: err instanceof Error ? err.message : String(err)
1943
+ };
1944
+ }
1945
+ }
1946
+ /**
1947
+ * 对所有已注册后端进行健康检查。
1948
+ * 返回扁平化结构,包含 version/supported/warnings 等字段。
1949
+ */
1950
+ async healthCheck() {
1951
+ const entries = Array.from(this.adapters.keys());
1952
+ const checks = entries.map(async (name) => {
1953
+ const result = await this.checkBackend(name);
1954
+ return [name, result];
1779
1955
  });
1780
1956
  const settled = await Promise.allSettled(checks);
1781
1957
  const results = {};
@@ -1831,7 +2007,16 @@ var Porygon = class extends EventEmitter2 {
1831
2007
  this.sessionManager.clearCache();
1832
2008
  }
1833
2009
  /**
1834
- * 合并请求参数与配置默认值
2010
+ * 合并请求参数与配置默认值。
2011
+ *
2012
+ * 合并策略:
2013
+ * - model: request > backendConfig > 不设置(后端使用自身默认值)
2014
+ * - timeoutMs: request > defaults
2015
+ * - maxTurns: request > defaults
2016
+ * - cwd: request > backendConfig
2017
+ * - appendSystemPrompt: **追加模式** — defaults + backendConfig + request 三层拼接(换行分隔)
2018
+ * 但如果 request.systemPrompt 已设置(替换模式),则忽略所有 appendSystemPrompt
2019
+ *
1835
2020
  * @param request 原始请求
1836
2021
  * @param backend 后端名称
1837
2022
  * @returns 合并后的请求
@@ -1951,6 +2136,7 @@ export {
1951
2136
  AgentTimeoutError,
1952
2137
  ClaudeAdapter,
1953
2138
  ConfigValidationError,
2139
+ InteractiveSession,
1954
2140
  InterceptorRejectedError,
1955
2141
  OpenCodeAdapter,
1956
2142
  Porygon,