evolcore 0.0.2 → 0.0.4

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 (206) hide show
  1. package/CHANGELOG.md +93 -778
  2. package/README.md +45 -10
  3. package/bin/ec-safe-output.js +89 -24
  4. package/dist/agents/baseagent.js +46 -0
  5. package/dist/agents/claude-runner.js +198 -48
  6. package/dist/agents/codex-app-server-client.js +68 -0
  7. package/dist/agents/codex-runner.js +203 -8
  8. package/dist/agents/runner-types.js +2 -2
  9. package/dist/aun/aid/agentmd.js +79 -0
  10. package/dist/aun/aid/encryption-seed-policy.js +29 -0
  11. package/dist/aun/aid/index.js +1 -1
  12. package/dist/aun/aid/store.js +2 -5
  13. package/dist/aun/outbox.js +8 -0
  14. package/dist/channels/aun.js +241 -116
  15. package/dist/channels/daemon.js +18 -4
  16. package/dist/channels/dingtalk.js +1005 -260
  17. package/dist/channels/feishu.js +184 -56
  18. package/dist/channels/qqbot.js +23 -1
  19. package/dist/channels/wechat.js +303 -155
  20. package/dist/channels/wecom.js +1132 -164
  21. package/dist/cli/agent-command.js +2 -1
  22. package/dist/cli/aun-commands.js +98 -43
  23. package/dist/cli/bench.js +2 -2
  24. package/dist/cli/contact.js +71 -0
  25. package/dist/cli/ctl-command.js +2 -2
  26. package/dist/cli/daemon-commands.js +77 -214
  27. package/dist/cli/handoff-command.js +4 -3
  28. package/dist/cli/help.js +9 -5
  29. package/dist/cli/index.js +132 -116
  30. package/dist/cli/init-channel.js +242 -129
  31. package/dist/cli/init.js +70 -31
  32. package/dist/cli/model.js +2 -1
  33. package/dist/cli/net-check.js +2 -2
  34. package/dist/cli/queue-command.js +30 -6
  35. package/dist/cli/raw-key-input.js +25 -0
  36. package/dist/cli/response.js +5 -6
  37. package/dist/cli/restart-monitor.js +25 -1
  38. package/dist/cli/stats.js +6 -4
  39. package/dist/cli/trigger-command.js +146 -25
  40. package/dist/cli/version.js +6 -1
  41. package/dist/config/access-policy-domain.js +38 -0
  42. package/dist/config/access-policy.js +134 -0
  43. package/dist/config/builtin-role-templates.js +42 -17
  44. package/dist/config/builtin-roles.js +31 -6
  45. package/dist/config/config-field-policy.js +17 -5
  46. package/dist/config/config-manager.js +409 -29
  47. package/dist/config/config-operation-service.js +35 -38
  48. package/dist/config/contact-alias.js +68 -0
  49. package/dist/config/contact-bind-code.js +274 -0
  50. package/dist/config/contact-book-store.js +622 -0
  51. package/dist/config/contact-book-v2-startup.js +35 -0
  52. package/dist/config/contact-book.js +185 -300
  53. package/dist/config/contact-operation-service.js +116 -0
  54. package/dist/config/contact-request-service.js +331 -0
  55. package/dist/config/peer-role-resolver.js +136 -55
  56. package/dist/config/role-ranks.js +18 -0
  57. package/dist/config/role-service.js +16 -19
  58. package/dist/config/role-store.js +16 -5
  59. package/dist/config/roles.js +10 -1
  60. package/dist/config/schema-registry.js +49 -24
  61. package/dist/config-store.js +14 -4
  62. package/dist/core/auth/agent-delegation.js +105 -11
  63. package/dist/core/auth/authorization-audit.js +16 -1
  64. package/dist/core/auth/operation-authorizer.js +10 -0
  65. package/dist/core/auth/operation-catalog.js +105 -1
  66. package/dist/core/auth/trigger-authorization.js +15 -0
  67. package/dist/core/bootstrap-service.js +2 -9
  68. package/dist/core/channel-loader.js +6 -2
  69. package/dist/core/command/command-handler.js +113 -21
  70. package/dist/core/command/connect-menu.js +599 -0
  71. package/dist/core/command/evol-menu-version-gate.js +38 -0
  72. package/dist/core/command/group-menu.js +421 -0
  73. package/dist/core/command/menu-handler.js +382 -100
  74. package/dist/core/command/menu-protocol.js +8 -2
  75. package/dist/core/command/menu-token-store.js +102 -0
  76. package/dist/core/command/role-menu.js +128 -29
  77. package/dist/core/command/slash-gate.js +1 -1
  78. package/dist/core/command/slash-handler.js +186 -49
  79. package/dist/core/daemon-file-cache.js +40 -6
  80. package/dist/core/event-catalog.js +99 -0
  81. package/dist/core/evolagent-registry.js +4 -40
  82. package/dist/core/evolagent.js +20 -9
  83. package/dist/core/handoff/runtime.js +162 -37
  84. package/dist/core/handoff/store.js +46 -2
  85. package/dist/core/handoff/types.js +1 -0
  86. package/dist/core/inference/text-inference.js +16 -74
  87. package/dist/core/message/file-markers.js +7 -0
  88. package/dist/core/message/im-renderer.js +20 -14
  89. package/dist/core/message/inbound-admission.js +124 -0
  90. package/dist/core/message/message-bridge.js +670 -57
  91. package/dist/core/message/message-log.js +1 -0
  92. package/dist/core/message/message-queue.js +195 -10
  93. package/dist/core/message/message-utils.js +8 -2
  94. package/dist/core/message/peer-mode.js +7 -8
  95. package/dist/core/message/response-engine.js +642 -96
  96. package/dist/core/message/send-receipt.js +24 -0
  97. package/dist/core/message/stream-debouncer.js +11 -2
  98. package/dist/core/permission/approval-gateway.js +17 -10
  99. package/dist/core/permission/ec-command-parser.js +101 -46
  100. package/dist/core/permission/tool-policy.js +115 -38
  101. package/dist/core/protected-paths.js +38 -11
  102. package/dist/core/session/session-fs-store.js +44 -3
  103. package/dist/core/session/session-manager.js +86 -8
  104. package/dist/core/session/session-mapper.js +2 -0
  105. package/dist/core/session/session-renew.js +125 -69
  106. package/dist/core/session/session-turn-coordinator.js +5 -1
  107. package/dist/eck/kit-renderer.js +12 -1
  108. package/dist/eck/message-renderer.js +79 -1
  109. package/dist/index.js +285 -94
  110. package/dist/ipc.js +114 -8
  111. package/dist/paths.js +3 -0
  112. package/dist/response-system/config-resolver.js +29 -0
  113. package/dist/response-system/coordinator.js +8 -32
  114. package/dist/response-system/engines/v1/proactive-flow.js +1 -1
  115. package/dist/response-system/index.js +1 -0
  116. package/dist/response-system/modes/single-session/index.js +7 -4
  117. package/dist/stats/billing.js +20 -8
  118. package/dist/trigger/manager.js +3 -0
  119. package/dist/trigger/parser.js +126 -11
  120. package/dist/trigger/patch.js +12 -2
  121. package/dist/trigger/scheduler.js +444 -40
  122. package/dist/trigger/validation.js +13 -0
  123. package/dist/utils/aid-bind.js +43 -29
  124. package/dist/utils/error-dict.json +7 -0
  125. package/dist/utils/evolcore-version.js +21 -0
  126. package/dist/utils/instance-registry.js +14 -7
  127. package/dist/utils/log-writer.js +46 -0
  128. package/dist/utils/logger.js +2 -2
  129. package/dist/utils/media-cache.js +4 -1
  130. package/dist/utils/model-prices.jsonl +6 -3
  131. package/dist/utils/restart-safety.js +31 -0
  132. package/dist/utils/stable-semver.js +21 -0
  133. package/dist/utils/stats.js +33 -9
  134. package/dist/utils/system-memory.js +62 -0
  135. package/kits/docs/INDEX.md +2 -1
  136. package/kits/docs/channels/aun.md +4 -13
  137. package/kits/docs/evolcore/INDEX.md +5 -3
  138. package/kits/docs/evolcore/agent.md +9 -1
  139. package/kits/docs/evolcore/aid.md +5 -2
  140. package/kits/docs/evolcore/config.md +47 -2
  141. package/kits/docs/evolcore/contact.md +61 -0
  142. package/kits/docs/evolcore/fs.md +9 -0
  143. package/kits/docs/evolcore/group-rules.md +46 -4
  144. package/kits/docs/evolcore/group.md +15 -6
  145. package/kits/docs/evolcore/model.md +4 -1
  146. package/kits/docs/evolcore/msg.md +12 -6
  147. package/kits/docs/evolcore/response.md +16 -21
  148. package/kits/docs/evolcore/rpc.md +2 -0
  149. package/kits/docs/evolcore/stats.md +15 -2
  150. package/kits/docs/evolcore/storage.md +1 -0
  151. package/kits/docs/evolcore/trigger.md +42 -10
  152. package/kits/docs/path-registry.md +36 -17
  153. package/kits/eck_manifest.json +12 -0
  154. package/kits/eck_message_manifest.json +12 -1
  155. package/kits/migrations/migrate-contact-book-v2.mjs +754 -0
  156. package/kits/rules/01-overview.md +17 -7
  157. package/kits/rules/02-navigation.md +38 -18
  158. package/kits/rules/03-identity.md +28 -24
  159. package/kits/rules/04-relation.md +44 -28
  160. package/kits/rules/05-venue.md +31 -15
  161. package/kits/rules/06-channel.md +11 -7
  162. package/kits/schemas/_meta.json +21 -7
  163. package/kits/schemas/agent-config.schema.6.json +322 -0
  164. package/kits/schemas/agent-config.schema.7.json +303 -0
  165. package/kits/schemas/agent-config.schema.8.json +304 -0
  166. package/kits/schemas/agent-config.schema.9.json +374 -0
  167. package/kits/schemas/contact-book.schema.2.json +43 -0
  168. package/kits/schemas/contact-book.schema.3.json +67 -0
  169. package/kits/schemas/daemon.schema.1.json +3 -2
  170. package/kits/schemas/daemon.schema.2.json +101 -0
  171. package/kits/schemas/daemon.schema.3.json +123 -0
  172. package/kits/schemas/defaults.schema.2.json +85 -0
  173. package/kits/schemas/defaults.schema.3.json +73 -0
  174. package/kits/schemas/relation-config.schema.5.json +47 -0
  175. package/kits/schemas/relation-config.schema.6.json +46 -0
  176. package/kits/schemas/relation-config.schema.7.json +59 -0
  177. package/kits/schemas/role-config.schema.1.json +1 -0
  178. package/kits/schemas/role-registry.schema.1.json +2 -2
  179. package/kits/schemas/single-session.schema.2.json +57 -0
  180. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
  181. package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
  182. package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
  183. package/kits/templates/roles/admin.json +47 -0
  184. package/kits/templates/roles/member.json +5 -0
  185. package/kits/templates/roles/owner.json +1 -0
  186. package/kits/templates/roles/visitor.json +5 -0
  187. package/kits/templates/system-fragments/channel.md +12 -2
  188. package/kits/templates/system-fragments/commands.md +3 -1
  189. package/kits/templates/system-fragments/identity.md +3 -1
  190. package/kits/templates/system-fragments/relation.md +1 -1
  191. package/kits/templates/system-fragments/session.md +6 -2
  192. package/package.json +7 -5
  193. package/MIGRATION-0.5.0.md +0 -378
  194. package/ROLE_ACCESS_CONTROL.md +0 -174
  195. package/assets/brand/evolcore/README.md +0 -19
  196. package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
  197. package/assets/brand/evolcore/evolcore-app-icon.svg +0 -13
  198. package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
  199. package/assets/brand/evolcore/evolcore-brand-board.svg +0 -126
  200. package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
  201. package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
  202. package/assets/brand/evolcore/evolcore-logo-reverse.svg +0 -14
  203. package/assets/brand/evolcore/evolcore-logo.png +0 -0
  204. package/assets/brand/evolcore/evolcore-logo.svg +0 -14
  205. package/assets/brand/evolcore/evolcore-mark.png +0 -0
  206. package/assets/brand/evolcore/evolcore-mark.svg +0 -10
package/README.md CHANGED
@@ -43,7 +43,7 @@ EvolCore 是一个轻量级 AI Agent 网关系统。它为 Claude Code / Codex
43
43
  1. **消息渠道层** (`src/channels/`) - Feishu + WeChat + DingTalk + QQBot + WeCom + AUN 网络
44
44
  2. **消息队列层** (`src/core/message/message-queue.ts`) - 会话级串行处理 + 中断支持
45
45
  3. **命令处理层** (`src/core/command/`) - 斜杠命令处理(slash-handler / menu-handler / command-handler)
46
- 4. **消息处理层** (`src/core/message/message-processor.ts`) - 统一事件处理引擎
46
+ 4. **消息处理层** (`src/core/message/response-engine.ts`) - 统一事件处理引擎
47
47
  5. **会话管理层** (`src/core/session/session-manager.ts`) - 多项目会话管理
48
48
  6. **交互路由层** (`src/core/interaction-router.ts`) - 卡片交互回调注册与路由
49
49
  7. **会话存储层** - JSONL 文件(CLI 共用)+ 文件系统(每 chat 一个目录,含 active.json / meta_*.jsonl / messages.jsonl / health.jsonl)
@@ -63,7 +63,7 @@ MessageQueue.enqueue(streamKey, message)
63
63
  ↓ 否 ↓
64
64
  MessageQueue.processNext() ←──────────────┘
65
65
 
66
- MessageProcessor.processMessage()
66
+ ResponseEngine.processMessage()
67
67
 
68
68
  ├─ 解析会话和项目路径
69
69
  ├─ 创建 StreamFlusher(3 秒批量发送)
@@ -78,12 +78,26 @@ MessageProcessor.processMessage()
78
78
  ### 环境要求
79
79
 
80
80
  - **操作系统**:macOS / Linux / Windows
81
- - **Node.js** >= 18
81
+ - **Node.js** >= 22(依赖 `node:sqlite`)
82
82
  - **Claude Code** >= 2.1.32(`npm install -g @anthropic-ai/claude-code`)
83
83
 
84
84
  ### 1. 安装
85
85
 
86
- **npm 全局安装**(推荐):
86
+ **一行安装(macOS / Linux)**:
87
+
88
+ ```bash
89
+ curl -fsSL https://raw.githubusercontent.com/eLeanwang/evolcore/main/scripts/install.sh | bash
90
+ ```
91
+
92
+ **Windows PowerShell**:
93
+
94
+ ```powershell
95
+ irm https://raw.githubusercontent.com/eLeanwang/evolcore/main/scripts/install.ps1 | iex
96
+ ```
97
+
98
+ 安装器会在新实例上自动进入 `ec init` 交互向导,完成后运行 `ec start` 和 `ec status`。已有配置的升级场景会跳过自动初始化,不会停止或重启服务。详细流程见 [一键安装脚本说明](docs/evolcore-install-script.md)。
99
+
100
+ **npm 全局安装**:
87
101
 
88
102
  ```bash
89
103
  npm install -g evolcore
@@ -128,7 +142,7 @@ ec init aun
128
142
 
129
143
  交互式引导完成以下配置:
130
144
  - 环境检查(Node.js >= 22、claude CLI、SDK 版本)
131
- - 渠道选择(飞书/微信)并扫码登录
145
+ - 渠道选择(飞书 / 微信 / 钉钉 / QQ 频道 / 企业微信 / AUN)并扫码登录
132
146
  - 默认项目路径
133
147
  - 模型选择(sonnet/opus/haiku)
134
148
  - owner 配置:默认走 **Evol App 扫码绑定**,若无 Evol App 可手工录入 AID 兜底
@@ -207,7 +221,8 @@ evolcore/
207
221
  │ │ │ └── slash-gate.ts # 权限前置拦截
208
222
  │ │ ├── message/
209
223
  │ │ │ ├── message-bridge.ts # 渠道 ↔ 核心消息桥
210
- │ │ │ ├── message-processor.ts # 统一消息处理引擎
224
+ │ │ │ ├── response-engine.ts # 统一响应处理引擎
225
+ │ │ │ ├── inbound-admission.ts # 私聊入站准入
211
226
  │ │ │ ├── message-queue.ts # 消息队列(串行+中断)
212
227
  │ │ │ ├── message-cache.ts # 消息缓存
213
228
  │ │ │ ├── message-log.ts # 每 chat 的 messages.jsonl
@@ -216,11 +231,11 @@ evolcore/
216
231
  │ │ │ ├── adapters/ # 各后端会话文件适配器
217
232
  │ │ │ ├── session-fs-store.ts # 文件系统存储原语
218
233
  │ │ │ └── session-manager.ts # 会话管理(多项目支持)
219
- │ │ ├── trigger/ # 触发器引擎
234
+ │ │ ├── auth/ # 授权审计与 Trigger 鉴权
220
235
  │ │ ├── evolagent.ts # EvolAgent 实体
221
236
  │ │ ├── evolagent-registry.ts # Agent 注册表(扫描/路由/热重载)
222
237
  │ │ ├── interaction-router.ts # 卡片交互回调路由
223
- │ │ └── permission.ts # 权限网关
238
+ │ │ └── protected-paths.ts # 受保护路径分级
224
239
  │ ├── channels/
225
240
  │ │ ├── feishu.ts # 飞书 WebSocket 渠道
226
241
  │ │ ├── wechat.ts # 微信 ClawBot 渠道
@@ -228,13 +243,18 @@ evolcore/
228
243
  │ │ ├── qqbot.ts # QQ 频道渠道
229
244
  │ │ ├── wecom.ts # 企业微信 AI Bot 渠道
230
245
  │ │ └── aun.ts # AUN Mesh 网络渠道
246
+ │ ├── config/ # 配置分层、访问策略、联系人绑定
247
+ │ ├── eck/ # ECK 上下文组装(manifest / kit 渲染)
248
+ │ ├── response-system/ # 响应模式决策与参数解析
249
+ │ ├── trigger/ # 触发器引擎
250
+ │ ├── stats/ # 用量统计与计费
231
251
  │ ├── cli/ # CLI 命令
232
252
  │ ├── utils/ # 工具函数
233
253
  │ ├── types.ts # 类型定义
234
254
  │ ├── config-store.ts # 配置加载
235
255
  │ ├── paths.ts # 路径解析
236
256
  │ └── index.ts # 主入口
237
- └── kits/ # 共享上下文模板
257
+ └── kits/ # 共享上下文模板(rules / docs / schemas)
238
258
  ```
239
259
 
240
260
  ## 斜杠命令
@@ -252,6 +272,11 @@ evolcore/
252
272
  - `/check` - 系统健康检查(摘要)
253
273
  - `/help` - 显示所有命令
254
274
 
275
+ **身份绑定**:
276
+ - `/bind <6 位绑定码>` - 将当前 IM 账号绑定到已登记的联系人身份(仅私聊)。
277
+ 支持飞书、微信、钉钉、QQ 频道、企业微信。绑定码有效期 10 分钟,可由
278
+ `ec init <channel>` 生成,或在目标 Agent 的 AUN 私聊发送 `/bind <channel>` 自助申请。
279
+
255
280
  ### 管理员级命令(Admin+ 可用)
256
281
 
257
282
  **项目**:
@@ -280,6 +305,16 @@ evolcore/
280
305
  - `/restart` - 重启服务(自愈机制)
281
306
  - `/repair` - 检查并修复会话
282
307
 
308
+ ### 联系人与关系管理(menu 协议)
309
+
310
+ `/connect` 与 `/group` 是 menu 协议操作,通过 Evol App 或 EC Web 调用,不是 IM 里的斜杠命令。
311
+
312
+ - **`/connect`** - 联系人与关系管理:联系人列表、群组列表、角色分配、管理员授权、拉黑状态。
313
+ 准入策略(`connect.access.write`)为 owner 专属,角色分配与 admin 授权委派给 `role` menu 执行,
314
+ 复用其 agent 级互斥锁、CAS revision 与审计链。
315
+ - **`/group`** - 群组管理:群列表、群级 @ 提及策略、群规则加载策略的查询与更新。
316
+ 群操作纳入权限目录与角色能力判定,通过版本比对和原子写入避免并发覆盖。
317
+
283
318
  ### ⚠️ 进程级 menu 操作鉴权(v3.2 Breaking)
284
319
 
285
320
  进程级 menu 操作(`/system restart/upgrade`、`/agent` agent 生命周期管理)的鉴权已迁移到
@@ -309,7 +344,7 @@ v3.2 新增进程级身份标识。启动时自动生成 `ec+5位数字.agentid.
309
344
  ## 技术栈
310
345
 
311
346
  - **运行时**:Node.js >= 22 + TypeScript(ES modules)
312
- - **AI 后端**:@anthropic-ai/claude-agent-sdk >= 0.3.211、Codex CLI app-server、Gemini CLI
347
+ - **AI 后端**:@anthropic-ai/claude-agent-sdk 0.3.220(固定版本)、Codex CLI app-server、Gemini CLI
313
348
  - **消息渠道**:飞书(@larksuiteoapi/node-sdk)、微信(ClawBot ilink API)、钉钉(dingtalk-stream)、QQ频道(pure-qqbot)、企业微信(AI Bot API)、AUN 网络
314
349
  - **数据存储**:文件系统(per-chat 目录) + JSONL(CLI 共用)
315
350
  - **测试框架**:Vitest
@@ -6,6 +6,13 @@ import { spawn } from 'child_process';
6
6
  const MAX_OUTPUT_BYTES = 1024 * 1024;
7
7
  const EC_EXECUTABLES = new Set(['ec']);
8
8
  const HOST_PROCESS_EXECUTABLES = new Set(['ps', 'pgrep', 'pidof', 'kill', 'pkill', 'killall']);
9
+ const HEAD_EXECUTABLE = ['/usr/bin/head', '/bin/head'].find(candidate => {
10
+ try {
11
+ return fs.statSync(candidate).isFile();
12
+ } catch {
13
+ return false;
14
+ }
15
+ });
9
16
 
10
17
  function fail(message) {
11
18
  process.stderr.write(`ec-safe-output: ${message}\n`);
@@ -34,6 +41,33 @@ function validateCommand(payload) {
34
41
  return !payload.argv.some(arg => /\bnode(?:js|\.exe)?\b/i.test(arg));
35
42
  }
36
43
 
44
+ function validateStdinOnlyHeadArgs(args) {
45
+ if (args === undefined) return true;
46
+ if (!Array.isArray(args) || args.some(arg => typeof arg !== 'string')) return false;
47
+ let optionsEnded = false;
48
+ let expectsOptionValue = false;
49
+ for (const arg of args) {
50
+ if (expectsOptionValue) {
51
+ expectsOptionValue = false;
52
+ continue;
53
+ }
54
+ if (optionsEnded) {
55
+ if (arg !== '-') return false;
56
+ continue;
57
+ }
58
+ if (arg === '--') {
59
+ optionsEnded = true;
60
+ continue;
61
+ }
62
+ if (arg === '-') continue;
63
+ if (!arg.startsWith('-')) return false;
64
+ if (arg === '-n' || arg === '-c' || arg === '--lines' || arg === '--bytes') {
65
+ expectsOptionValue = true;
66
+ }
67
+ }
68
+ return true;
69
+ }
70
+
37
71
  function resolveExecutionPaths(payload) {
38
72
  if (typeof payload.workspace !== 'string' || typeof payload.cwd !== 'string') fail('invalid workspace');
39
73
  let workspace;
@@ -87,36 +121,19 @@ function openOutputFile(payload, paths) {
87
121
  const payload = decodePayload(process.argv[2]);
88
122
  if (!payload) fail('invalid payload');
89
123
  if (!validateCommand(payload)) fail('command is not permitted');
90
- if (payload.headLines !== undefined
91
- && (!Number.isSafeInteger(payload.headLines) || payload.headLines < 1 || payload.headLines > 10_000)) {
92
- fail('invalid head line limit');
93
- }
124
+ if (!validateStdinOnlyHeadArgs(payload.headArgs)) fail('head may only consume stdin');
125
+ if (payload.headArgs !== undefined && !HEAD_EXECUTABLE) fail('trusted head executable is unavailable');
94
126
 
95
127
  const paths = resolveExecutionPaths(payload);
96
128
  const output = openOutputFile(payload, paths);
97
129
  let bytesWritten = 0;
98
- let linesWritten = 0;
99
130
  let limitReached = false;
100
131
  let writeFailed = false;
101
132
 
102
133
  function writeLimited(chunk) {
103
134
  if (limitReached || writeFailed) return;
104
135
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
105
- let allowed = Math.min(buffer.length, MAX_OUTPUT_BYTES - bytesWritten);
106
- if (payload.headLines !== undefined) {
107
- let cursor = 0;
108
- while (cursor < allowed) {
109
- const newline = buffer.indexOf(0x0a, cursor);
110
- if (newline < 0 || newline >= allowed) break;
111
- linesWritten++;
112
- cursor = newline + 1;
113
- if (linesWritten >= payload.headLines) {
114
- allowed = cursor;
115
- limitReached = true;
116
- break;
117
- }
118
- }
119
- }
136
+ const allowed = Math.min(buffer.length, MAX_OUTPUT_BYTES - bytesWritten);
120
137
  if (allowed <= 0) {
121
138
  limitReached = true;
122
139
  return;
@@ -132,13 +149,35 @@ function writeLimited(chunk) {
132
149
  }
133
150
  }
134
151
 
152
+ const head = payload.headArgs === undefined
153
+ ? undefined
154
+ : spawn(HEAD_EXECUTABLE, payload.headArgs, {
155
+ cwd: paths.cwd,
156
+ env: process.env,
157
+ stdio: ['pipe', 'pipe', 'inherit'],
158
+ });
159
+ if (head) {
160
+ head.stdout.on('data', writeLimited);
161
+ // Early-exit options such as --help or a small -n/-c close stdin before the
162
+ // EC process finishes. Ignore EPIPE and keep draining EC output below.
163
+ head.stdin.on('error', () => {});
164
+ }
165
+
135
166
  const child = spawn(payload.argv[0], payload.argv.slice(1), {
136
167
  cwd: paths.cwd,
137
168
  env: process.env,
138
169
  stdio: ['ignore', 'pipe', payload.mergeStderr ? 'pipe' : 'inherit'],
139
170
  });
140
- child.stdout.on('data', writeLimited);
141
- if (payload.mergeStderr) child.stderr.on('data', writeLimited);
171
+ const sourceStreams = [child.stdout, ...(payload.mergeStderr ? [child.stderr] : [])];
172
+ if (head) {
173
+ for (const stream of sourceStreams) {
174
+ // The no-op data listener keeps draining the source if head exits early.
175
+ stream.on('data', () => {});
176
+ stream.pipe(head.stdin, { end: false });
177
+ }
178
+ } else {
179
+ for (const stream of sourceStreams) stream.on('data', writeLimited);
180
+ }
142
181
 
143
182
  child.once('error', error => {
144
183
  if (output) {
@@ -148,7 +187,13 @@ child.once('error', error => {
148
187
  fail(`unable to start command: ${error.message}`);
149
188
  });
150
189
 
151
- child.once('close', code => {
190
+ let sourceClosed = false;
191
+ let sourceCode = 1;
192
+ let headClosed = !head;
193
+ let headCode = 1;
194
+
195
+ function finish() {
196
+ if (!sourceClosed || !headClosed) return;
152
197
  if (output) {
153
198
  try { fs.closeSync(output.fd); } catch { writeFailed = true; }
154
199
  }
@@ -157,5 +202,25 @@ child.once('close', code => {
157
202
  fail('failed to write command output');
158
203
  }
159
204
  if (output) process.stderr.write(`Saved output to ${output.target}\n`);
160
- process.exitCode = code ?? 1;
205
+ process.exitCode = head ? headCode : sourceCode;
206
+ }
207
+
208
+ if (head) {
209
+ head.once('error', error => {
210
+ writeFailed = true;
211
+ process.stderr.write(`ec-safe-output: unable to start head: ${error.message}\n`);
212
+ });
213
+ head.once('close', code => {
214
+ headCode = code ?? 1;
215
+ headClosed = true;
216
+ for (const stream of sourceStreams) stream.unpipe(head.stdin);
217
+ finish();
218
+ });
219
+ }
220
+
221
+ child.once('close', code => {
222
+ sourceCode = code ?? 1;
223
+ sourceClosed = true;
224
+ if (head && !head.stdin.destroyed) head.stdin.end();
225
+ finish();
161
226
  });
@@ -32,6 +32,22 @@ export function normalizeBaseagent(input) {
32
32
  const key = String(input || '').trim().toLowerCase().replace(/_/g, '-');
33
33
  return BASEAGENT_ALIASES[key] || { canonical: 'unknown', displayName: input ? String(input) : 'Unknown' };
34
34
  }
35
+ function directInferencePair(apiKey, baseUrl) {
36
+ const normalizedApiKey = directInferenceValue(apiKey);
37
+ const normalizedBaseUrl = directInferenceValue(baseUrl);
38
+ if (!normalizedApiKey || !normalizedBaseUrl)
39
+ return undefined;
40
+ return { apiKey: normalizedApiKey, baseUrl: normalizedBaseUrl };
41
+ }
42
+ function directInferenceValue(...values) {
43
+ for (const value of values) {
44
+ const normalized = value?.trim();
45
+ const lower = normalized?.toLowerCase();
46
+ if (normalized && !lower?.includes('your-') && !lower?.includes('placeholder'))
47
+ return normalized;
48
+ }
49
+ return undefined;
50
+ }
35
51
  function loadClaudeSettings() {
36
52
  try {
37
53
  const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
@@ -42,6 +58,21 @@ function loadClaudeSettings() {
42
58
  catch { }
43
59
  return {};
44
60
  }
61
+ /**
62
+ * Resolve a complete credential/endpoint pair for direct Anthropic HTTP calls.
63
+ * A pair must come from one source so partial configuration cannot be mixed
64
+ * across Agent config, the process environment, and Claude Code settings.
65
+ */
66
+ export function resolveAnthropicDirectConfig(override) {
67
+ const explicit = directInferencePair(override?.apiKey, override?.baseUrl);
68
+ if (explicit)
69
+ return explicit;
70
+ const environment = directInferencePair(directInferenceValue(process.env.ANTHROPIC_AUTH_TOKEN, process.env.ANTHROPIC_API_KEY), process.env.ANTHROPIC_BASE_URL);
71
+ if (environment)
72
+ return environment;
73
+ const settings = loadClaudeSettings();
74
+ return directInferencePair(directInferenceValue(settings.env?.ANTHROPIC_AUTH_TOKEN, settings.env?.ANTHROPIC_API_KEY), settings.env?.ANTHROPIC_BASE_URL);
75
+ }
45
76
  export function resolveAnthropicConfig(config, override) {
46
77
  const settings = loadClaudeSettings();
47
78
  const isPlaceholder = (v) => !v || v.includes('your-') || v.includes('placeholder');
@@ -103,6 +134,21 @@ function loadCodexSettings() {
103
134
  catch { }
104
135
  return {};
105
136
  }
137
+ /**
138
+ * Resolve a complete credential/endpoint pair for direct OpenAI HTTP calls.
139
+ * auth.json and config.toml together form the Codex SDK source; fields are
140
+ * never combined with a partial Agent or process-environment configuration.
141
+ */
142
+ export function resolveOpenaiDirectConfig(override) {
143
+ const explicit = directInferencePair(override?.apiKey, override?.baseUrl);
144
+ if (explicit)
145
+ return explicit;
146
+ const environment = directInferencePair(process.env.OPENAI_API_KEY, process.env.OPENAI_BASE_URL);
147
+ if (environment)
148
+ return environment;
149
+ const settings = loadCodexSettings();
150
+ return directInferencePair(settings.apiKey, settings.baseUrl);
151
+ }
106
152
  export function resolveOpenaiConfig(config, override) {
107
153
  const codexSettings = loadCodexSettings();
108
154
  const isPlaceholder = (v) => !v || v.includes('your-') || v.includes('placeholder');