lark-remote 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.
Files changed (201) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/LICENSE +21 -0
  3. package/README.en.md +162 -0
  4. package/README.md +161 -0
  5. package/dist/binder.d.ts +57 -0
  6. package/dist/binder.js +105 -0
  7. package/dist/bridge/index.d.ts +338 -0
  8. package/dist/bridge/index.js +1281 -0
  9. package/dist/bridge/queue-manager.d.ts +277 -0
  10. package/dist/bridge/queue-manager.js +729 -0
  11. package/dist/card/bash-card-session.d.ts +78 -0
  12. package/dist/card/bash-card-session.js +174 -0
  13. package/dist/card/bash-renderer.d.ts +24 -0
  14. package/dist/card/bash-renderer.js +187 -0
  15. package/dist/card/card-budget.d.ts +31 -0
  16. package/dist/card/card-budget.js +280 -0
  17. package/dist/card/card-session.d.ts +83 -0
  18. package/dist/card/card-session.js +185 -0
  19. package/dist/card/card-shared.d.ts +28 -0
  20. package/dist/card/card-shared.js +99 -0
  21. package/dist/card/collapsible.d.ts +26 -0
  22. package/dist/card/collapsible.js +87 -0
  23. package/dist/card/run-card-session.d.ts +78 -0
  24. package/dist/card/run-card-session.js +165 -0
  25. package/dist/card/run-renderer.d.ts +66 -0
  26. package/dist/card/run-renderer.js +841 -0
  27. package/dist/card/run-state.d.ts +103 -0
  28. package/dist/card/run-state.js +313 -0
  29. package/dist/card/text-truncate.d.ts +56 -0
  30. package/dist/card/text-truncate.js +184 -0
  31. package/dist/card/time.d.ts +1 -0
  32. package/dist/card/time.js +14 -0
  33. package/dist/card/tool-render.d.ts +25 -0
  34. package/dist/card/tool-render.js +176 -0
  35. package/dist/cli.d.ts +28 -0
  36. package/dist/cli.js +89 -0
  37. package/dist/common/guards.d.ts +37 -0
  38. package/dist/common/guards.js +65 -0
  39. package/dist/config/codex-config.d.ts +113 -0
  40. package/dist/config/codex-config.js +428 -0
  41. package/dist/config/dir.d.ts +17 -0
  42. package/dist/config/dir.js +81 -0
  43. package/dist/config/index.d.ts +219 -0
  44. package/dist/config/index.js +388 -0
  45. package/dist/config/kimi-config.d.ts +33 -0
  46. package/dist/config/kimi-config.js +111 -0
  47. package/dist/config/opencode-config.d.ts +20 -0
  48. package/dist/config/opencode-config.js +124 -0
  49. package/dist/config/pi-config.d.ts +49 -0
  50. package/dist/config/pi-config.js +151 -0
  51. package/dist/config/wizard.d.ts +12 -0
  52. package/dist/config/wizard.js +84 -0
  53. package/dist/connector/file-limits.d.ts +7 -0
  54. package/dist/connector/file-limits.js +8 -0
  55. package/dist/connector/index.d.ts +86 -0
  56. package/dist/connector/index.js +340 -0
  57. package/dist/error-classification.d.ts +15 -0
  58. package/dist/error-classification.js +24 -0
  59. package/dist/index.d.ts +1 -0
  60. package/dist/index.js +580 -0
  61. package/dist/instance-lock.d.ts +30 -0
  62. package/dist/instance-lock.js +184 -0
  63. package/dist/logger/index.d.ts +57 -0
  64. package/dist/logger/index.js +178 -0
  65. package/dist/order/index.d.ts +27 -0
  66. package/dist/order/index.js +90 -0
  67. package/dist/persistence/atomic-write.d.ts +10 -0
  68. package/dist/persistence/atomic-write.js +65 -0
  69. package/dist/persistence/load-json-file.d.ts +9 -0
  70. package/dist/persistence/load-json-file.js +23 -0
  71. package/dist/restart.d.ts +24 -0
  72. package/dist/restart.js +73 -0
  73. package/dist/router/card-helpers.d.ts +28 -0
  74. package/dist/router/card-helpers.js +65 -0
  75. package/dist/router/config/claude.d.ts +14 -0
  76. package/dist/router/config/claude.js +72 -0
  77. package/dist/router/config/codex.d.ts +22 -0
  78. package/dist/router/config/codex.js +100 -0
  79. package/dist/router/config/common/render.d.ts +16 -0
  80. package/dist/router/config/common/render.js +158 -0
  81. package/dist/router/config/index.d.ts +18 -0
  82. package/dist/router/config/index.js +38 -0
  83. package/dist/router/config/kimi.d.ts +21 -0
  84. package/dist/router/config/kimi.js +63 -0
  85. package/dist/router/config/opencode.d.ts +12 -0
  86. package/dist/router/config/opencode.js +44 -0
  87. package/dist/router/config/pi.d.ts +12 -0
  88. package/dist/router/config/pi.js +50 -0
  89. package/dist/router/config/types.d.ts +43 -0
  90. package/dist/router/config/types.js +8 -0
  91. package/dist/router/index.d.ts +318 -0
  92. package/dist/router/index.js +2715 -0
  93. package/dist/router/order-exec-dispatch.d.ts +30 -0
  94. package/dist/router/order-exec-dispatch.js +48 -0
  95. package/dist/router/utils.d.ts +44 -0
  96. package/dist/router/utils.js +116 -0
  97. package/dist/runner/bash/index.d.ts +4 -0
  98. package/dist/runner/bash/index.js +5 -0
  99. package/dist/runner/bash/runner.d.ts +55 -0
  100. package/dist/runner/bash/runner.js +213 -0
  101. package/dist/runner/claude/index.d.ts +4 -0
  102. package/dist/runner/claude/index.js +5 -0
  103. package/dist/runner/claude/runner.d.ts +29 -0
  104. package/dist/runner/claude/runner.js +83 -0
  105. package/dist/runner/codex/argv.d.ts +33 -0
  106. package/dist/runner/codex/argv.js +40 -0
  107. package/dist/runner/codex/index.d.ts +1 -0
  108. package/dist/runner/codex/index.js +2 -0
  109. package/dist/runner/codex/jsonl.d.ts +37 -0
  110. package/dist/runner/codex/jsonl.js +221 -0
  111. package/dist/runner/codex/runner.d.ts +65 -0
  112. package/dist/runner/codex/runner.js +124 -0
  113. package/dist/runner/common/exec-translator.d.ts +73 -0
  114. package/dist/runner/common/exec-translator.js +105 -0
  115. package/dist/runner/common/jsonl-stream.d.ts +39 -0
  116. package/dist/runner/common/jsonl-stream.js +203 -0
  117. package/dist/runner/common/process-stopper.d.ts +28 -0
  118. package/dist/runner/common/process-stopper.js +79 -0
  119. package/dist/runner/common/runner-utils.d.ts +21 -0
  120. package/dist/runner/common/runner-utils.js +65 -0
  121. package/dist/runner/common/spawn-heartbeat.d.ts +33 -0
  122. package/dist/runner/common/spawn-heartbeat.js +51 -0
  123. package/dist/runner/common/spawning-runner.d.ts +321 -0
  124. package/dist/runner/common/spawning-runner.js +634 -0
  125. package/dist/runner/common/usage.d.ts +31 -0
  126. package/dist/runner/common/usage.js +29 -0
  127. package/dist/runner/index.d.ts +19 -0
  128. package/dist/runner/index.js +24 -0
  129. package/dist/runner/kimi/index.d.ts +1 -0
  130. package/dist/runner/kimi/index.js +2 -0
  131. package/dist/runner/kimi/jsonl.d.ts +28 -0
  132. package/dist/runner/kimi/jsonl.js +143 -0
  133. package/dist/runner/kimi/runner.d.ts +50 -0
  134. package/dist/runner/kimi/runner.js +105 -0
  135. package/dist/runner/opencode/argv.d.ts +32 -0
  136. package/dist/runner/opencode/argv.js +35 -0
  137. package/dist/runner/opencode/index.d.ts +1 -0
  138. package/dist/runner/opencode/index.js +2 -0
  139. package/dist/runner/opencode/jsonl.d.ts +68 -0
  140. package/dist/runner/opencode/jsonl.js +295 -0
  141. package/dist/runner/opencode/runner.d.ts +63 -0
  142. package/dist/runner/opencode/runner.js +93 -0
  143. package/dist/runner/pi/index.d.ts +1 -0
  144. package/dist/runner/pi/index.js +2 -0
  145. package/dist/runner/pi/jsonl.d.ts +128 -0
  146. package/dist/runner/pi/jsonl.js +249 -0
  147. package/dist/runner/pi/runner.d.ts +37 -0
  148. package/dist/runner/pi/runner.js +76 -0
  149. package/dist/runner/registry.d.ts +54 -0
  150. package/dist/runner/registry.js +75 -0
  151. package/dist/runner/resolve-agent-choices.d.ts +7 -0
  152. package/dist/runner/resolve-agent-choices.js +76 -0
  153. package/dist/runner/sync-agent-choices.d.ts +6 -0
  154. package/dist/runner/sync-agent-choices.js +73 -0
  155. package/dist/runner/types.d.ts +250 -0
  156. package/dist/runner/types.js +8 -0
  157. package/dist/session/claude/index.d.ts +1 -0
  158. package/dist/session/claude/index.js +2 -0
  159. package/dist/session/claude/session-reader.d.ts +34 -0
  160. package/dist/session/claude/session-reader.js +46 -0
  161. package/dist/session/claude/sessions.d.ts +53 -0
  162. package/dist/session/claude/sessions.js +528 -0
  163. package/dist/session/codex/index.d.ts +1 -0
  164. package/dist/session/codex/index.js +2 -0
  165. package/dist/session/codex/rollout-reader.d.ts +88 -0
  166. package/dist/session/codex/rollout-reader.js +473 -0
  167. package/dist/session/codex/sessions.d.ts +21 -0
  168. package/dist/session/codex/sessions.js +40 -0
  169. package/dist/session/common/catch-up.d.ts +12 -0
  170. package/dist/session/common/catch-up.js +31 -0
  171. package/dist/session/common/constants.d.ts +11 -0
  172. package/dist/session/common/constants.js +12 -0
  173. package/dist/session/common/content-blocks.d.ts +37 -0
  174. package/dist/session/common/content-blocks.js +68 -0
  175. package/dist/session/common/jsonl.d.ts +77 -0
  176. package/dist/session/common/jsonl.js +351 -0
  177. package/dist/session/common/usage-accumulator.d.ts +58 -0
  178. package/dist/session/common/usage-accumulator.js +72 -0
  179. package/dist/session/index.d.ts +7 -0
  180. package/dist/session/index.js +11 -0
  181. package/dist/session/kimi/index.d.ts +1 -0
  182. package/dist/session/kimi/index.js +2 -0
  183. package/dist/session/kimi/sessions.d.ts +81 -0
  184. package/dist/session/kimi/sessions.js +527 -0
  185. package/dist/session/opencode/index.d.ts +1 -0
  186. package/dist/session/opencode/index.js +2 -0
  187. package/dist/session/opencode/sessions.d.ts +81 -0
  188. package/dist/session/opencode/sessions.js +388 -0
  189. package/dist/session/pi/index.d.ts +1 -0
  190. package/dist/session/pi/index.js +2 -0
  191. package/dist/session/pi/sessions.d.ts +27 -0
  192. package/dist/session/pi/sessions.js +399 -0
  193. package/dist/session/registry.d.ts +12 -0
  194. package/dist/session/registry.js +20 -0
  195. package/dist/session/session-store.d.ts +86 -0
  196. package/dist/session/session-store.js +450 -0
  197. package/dist/startup-contact.d.ts +18 -0
  198. package/dist/startup-contact.js +51 -0
  199. package/dist/workspace/index.d.ts +19 -0
  200. package/dist/workspace/index.js +53 -0
  201. package/package.json +93 -0
@@ -0,0 +1,30 @@
1
+ import type { CommandRouter } from './index.js';
2
+ import type { Bridge } from '../bridge/index.js';
3
+ /** Context shared by all card-action dispatch paths (file-local; callers pass it structurally). */
4
+ interface DispatchContext {
5
+ userId: string;
6
+ chatId: string;
7
+ messageId: string;
8
+ }
9
+ /**
10
+ * Dispatch an `order.exec` card action as an equivalent queued user message
11
+ * (Plan A): resolve the order text at the enqueue boundary, then enqueue it
12
+ * through `router.handle(orderText)` — the same path a hand-typed message
13
+ * takes. The real text flows as the queue `messagePreview` (→ queue card
14
+ * display + edit default_value) and as the forwarded content; a unique
15
+ * internal key is used as the queue dedup key instead of the Feishu card
16
+ * messageId (one order card is clickable N times, so the card messageId is
17
+ * 1:N with enqueue actions and must not be reused).
18
+ *
19
+ * Extracted from `src/index.ts` so the enqueue-time contract is unit-testable
20
+ * without wiring up the full `main()` graph. Returns a short status string
21
+ * for the caller to log; performs its own error feedback via `bridge.sendResult`.
22
+ */
23
+ export declare function dispatchOrderExecForQueue(args: {
24
+ router: CommandRouter;
25
+ bridge: Bridge;
26
+ workspace: string;
27
+ orderId: string | undefined;
28
+ ctx: DispatchContext;
29
+ }): Promise<'enqueued' | 'missing-id' | 'not-found'>;
30
+ export {};
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Dispatch an `order.exec` card action as an equivalent queued user message
3
+ * (Plan A): resolve the order text at the enqueue boundary, then enqueue it
4
+ * through `router.handle(orderText)` — the same path a hand-typed message
5
+ * takes. The real text flows as the queue `messagePreview` (→ queue card
6
+ * display + edit default_value) and as the forwarded content; a unique
7
+ * internal key is used as the queue dedup key instead of the Feishu card
8
+ * messageId (one order card is clickable N times, so the card messageId is
9
+ * 1:N with enqueue actions and must not be reused).
10
+ *
11
+ * Extracted from `src/index.ts` so the enqueue-time contract is unit-testable
12
+ * without wiring up the full `main()` graph. Returns a short status string
13
+ * for the caller to log; performs its own error feedback via `bridge.sendResult`.
14
+ */
15
+ export async function dispatchOrderExecForQueue(args) {
16
+ const { router, bridge, workspace, orderId, ctx } = args;
17
+ if (!orderId) {
18
+ await bridge.sendResult({ text: '卡片 payload 缺少必要信息' }, ctx);
19
+ return 'missing-id';
20
+ }
21
+ const resolved = router.resolveOrderExecForQueue(orderId);
22
+ if (!resolved) {
23
+ await bridge.sendResult({ text: '指令不存在或已被删除' }, ctx);
24
+ return 'not-found';
25
+ }
26
+ // Step4/D4: 入队时刻(T0)快照 agent+session,随任务闭包带到 T1 执行时刻,
27
+ // 避免排队期间 /new、/config 改写 live 状态导致语义漂移。唯一捕获点
28
+ // Bridge.currentBinding。
29
+ const binding = bridge.currentBinding(ctx.userId);
30
+ bridge.enqueue(workspace, async () => {
31
+ await router.handle(resolved.orderText, ctx, { cwdOverride: workspace, binding });
32
+ }, {
33
+ taskMeta: {
34
+ userId: ctx.userId,
35
+ chatId: ctx.chatId,
36
+ messageId: resolved.internalKey,
37
+ messagePreview: resolved.orderText.slice(0, 3000),
38
+ // The internal key is NOT a valid Feishu message id, so the queue
39
+ // status card must reply to the real Feishu card message id instead.
40
+ feishuReplyTo: ctx.messageId,
41
+ // D3/Step4:binding 随 taskMeta 存进 QueuedTask,供替换闭包
42
+ // (queue.edit/queue.immediate)复用,不重新快照。
43
+ binding,
44
+ },
45
+ });
46
+ return 'enqueued';
47
+ }
48
+ //# sourceMappingURL=order-exec-dispatch.js.map
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Pure utility functions extracted from router/index.ts.
3
+ * Path-safety helpers, display formatting, and validation — no Router state.
4
+ */
5
+ import type { ActiveRunSnapshot } from '../bridge/index.js';
6
+ export interface SessionDisplayUsage {
7
+ contextLength?: number;
8
+ compactCount?: number;
9
+ cacheReadTokens?: number;
10
+ cacheCreationTokens?: number;
11
+ }
12
+ export declare function activeRunUsage(activeRun: ActiveRunSnapshot | undefined): SessionDisplayUsage | undefined;
13
+ /**
14
+ * Format usage stats (context length, compact count, cache tokens) into a display string.
15
+ * Each stat on its own line with blank lines between sections.
16
+ * @param usage - Usage stats object
17
+ * @param options - Optional display options (showResult adds result line)
18
+ */
19
+ export declare function formatUsageStats(usage?: {
20
+ contextLength?: number;
21
+ compactCount?: number;
22
+ cacheReadTokens?: number;
23
+ cacheCreationTokens?: number;
24
+ /** Agent-declared total; when present the display uses max(total, sum of parts). */
25
+ totalTokens?: number;
26
+ /** Real input tokens from the agent's result event. When present, used
27
+ * directly for the "Input token" line instead of contextLength+cache. */
28
+ inputTokens?: number;
29
+ /** Real output tokens from the agent's result event. When present, used
30
+ * directly instead of the 10% estimate. */
31
+ outputTokens?: number;
32
+ /** Session-cumulative total/input/output (all runs), from session jsonl. When
33
+ * present, appended as "· 累计 X" on the Total/Input/Output lines. */
34
+ cumulativeTotalTokens?: number;
35
+ cumulativeInputTokens?: number;
36
+ cumulativeOutputTokens?: number;
37
+ /** Session-cumulative cache read tokens (all runs). */
38
+ cumulativeCacheReadTokens?: number;
39
+ /** Session-cumulative cache creation tokens (all runs). */
40
+ cumulativeCacheCreationTokens?: number;
41
+ }, options?: {
42
+ showResult?: boolean;
43
+ result?: string;
44
+ }): string;
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Pure utility functions extracted from router/index.ts.
3
+ * Path-safety helpers, display formatting, and validation — no Router state.
4
+ */
5
+ export function activeRunUsage(activeRun) {
6
+ if (!activeRun || activeRun.contextLength === undefined)
7
+ return undefined;
8
+ return {
9
+ contextLength: activeRun.contextLength,
10
+ compactCount: activeRun.compactCount,
11
+ cacheReadTokens: activeRun.cacheReadTokens,
12
+ cacheCreationTokens: activeRun.cacheCreationTokens,
13
+ };
14
+ }
15
+ // --- Extracted helpers for refactoring ---
16
+ /**
17
+ * Format number to K unit string, rounding to the nearest integer K
18
+ * (e.g., 120000 → 120K, 1500 → 2K, 1950 → 2K). Values < 1000 are returned as-is.
19
+ */
20
+ function formatTokenK(n) {
21
+ if (n >= 1000) {
22
+ const k = Math.round(n / 1000);
23
+ return `${k}K`;
24
+ }
25
+ return n.toString();
26
+ }
27
+ /**
28
+ * Format usage stats (context length, compact count, cache tokens) into a display string.
29
+ * Each stat on its own line with blank lines between sections.
30
+ * @param usage - Usage stats object
31
+ * @param options - Optional display options (showResult adds result line)
32
+ */
33
+ export function formatUsageStats(usage, options) {
34
+ const lines = [];
35
+ // 1. Success 状态(通用格式,无具体结果值)
36
+ lines.push('✅ 已完成');
37
+ // 1.5. 可选的结果行(showResult 为 true 时显示)
38
+ if (options?.showResult && options?.result) {
39
+ lines.push(`结果 - ${options.result}`);
40
+ }
41
+ // 2. Context 长度
42
+ if (usage?.contextLength !== undefined) {
43
+ lines.push(`Context - ${formatTokenK(usage.contextLength)}`);
44
+ }
45
+ // 3. Compact 次数
46
+ if (usage?.compactCount) {
47
+ lines.push(`Compact - ${usage.compactCount}次`);
48
+ }
49
+ // 4-7. Token 统计
50
+ const cacheRead = usage?.cacheReadTokens ?? 0;
51
+ const cacheCreation = usage?.cacheCreationTokens ?? 0;
52
+ const realInput = usage?.inputTokens;
53
+ const realOutput = usage?.outputTokens;
54
+ // Cumulative suffix (session-wide totals), appended to Input/Output lines.
55
+ const cumInputSuffix = usage?.cumulativeInputTokens !== undefined
56
+ ? ` · 累计 ${formatTokenK(usage.cumulativeInputTokens)}`
57
+ : '';
58
+ const cumOutputSuffix = usage?.cumulativeOutputTokens !== undefined
59
+ ? ` · 累计 ${formatTokenK(usage.cumulativeOutputTokens)}`
60
+ : '';
61
+ const cumTotalSuffix = usage?.cumulativeTotalTokens !== undefined
62
+ ? ` · 累计 ${formatTokenK(usage.cumulativeTotalTokens)}`
63
+ : '';
64
+ // 计算累计 cache 命中率
65
+ const cumInput = usage?.cumulativeInputTokens;
66
+ const cumCacheRead = usage?.cumulativeCacheReadTokens;
67
+ const cumCachePercent = cumInput !== undefined && cumCacheRead !== undefined && cumInput + cumCacheRead > 0
68
+ ? Math.round((cumCacheRead / (cumInput + cumCacheRead)) * 100)
69
+ : null;
70
+ // 构建累计 suffix
71
+ const cumCacheReadSuffix = cumCacheRead !== undefined
72
+ ? ` · 累计 ${formatTokenK(cumCacheRead)}` +
73
+ (cumCachePercent !== null ? ` (${cumCachePercent}%)` : '')
74
+ : '';
75
+ const cumCacheCreateSuffix = usage?.cumulativeCacheCreationTokens !== undefined
76
+ ? ` · 累计 ${formatTokenK(usage.cumulativeCacheCreationTokens)}`
77
+ : '';
78
+ if (realInput !== undefined && realOutput !== undefined) {
79
+ // Unified ccusage-aligned formula:
80
+ // total = max(total_tokens, input + output + cacheRead + cacheCreation)
81
+ // cache% = cacheRead / (input + cacheRead)
82
+ // cacheRead/cacheCreation are never double-counted; total_tokens folds in any
83
+ // extra (e.g. opencode reasoning, which is separate from output) via max().
84
+ const promptTokens = realInput + cacheRead;
85
+ const cachePercent = promptTokens > 0 ? Math.round((cacheRead / promptTokens) * 100) : 0;
86
+ const sumParts = realInput + realOutput + cacheRead + cacheCreation;
87
+ const totalTokens = usage?.totalTokens != null ? Math.max(usage.totalTokens, sumParts) : sumParts;
88
+ lines.push(`Input token - ${formatTokenK(realInput)}${cumInputSuffix}`);
89
+ lines.push(`Output token - ${formatTokenK(realOutput)}${cumOutputSuffix}`);
90
+ if (cacheCreation > 0) {
91
+ lines.push(`Cache create - ${formatTokenK(cacheCreation)}${cumCacheCreateSuffix}`);
92
+ }
93
+ lines.push(`Cached token - ${formatTokenK(cacheRead)} (${cachePercent}%)${cumCacheReadSuffix}`);
94
+ lines.push(`Total token - ${formatTokenK(totalTokens)}${cumTotalSuffix}`);
95
+ }
96
+ else {
97
+ // Estimate path: when real input/output are absent,
98
+ // fall back to the 10% estimate based on contextLength.
99
+ const inputRef = usage?.contextLength ?? cacheRead;
100
+ if (inputRef > 0 || cacheRead > 0 || cacheCreation > 0) {
101
+ const totalInput = inputRef + cacheRead;
102
+ const outputEst = inputRef > 0 ? Math.round(inputRef * 0.1) : 0;
103
+ const cachePercent = totalInput > 0 ? Math.round((cacheRead / totalInput) * 100) : 0;
104
+ const totalTokens = totalInput + outputEst + cacheCreation;
105
+ lines.push(`Input token - ${formatTokenK(totalInput)}${cumInputSuffix}`);
106
+ lines.push(`Output token - ${formatTokenK(outputEst)}${cumOutputSuffix}`);
107
+ if (cacheCreation > 0) {
108
+ lines.push(`Cache create - ${formatTokenK(cacheCreation)}${cumCacheCreateSuffix}`);
109
+ }
110
+ lines.push(`Cached token - ${formatTokenK(cacheRead)} (${cachePercent}%)${cumCacheReadSuffix}`);
111
+ lines.push(`Total token - ${formatTokenK(totalTokens)}${cumTotalSuffix}`);
112
+ }
113
+ }
114
+ return lines.join('\n');
115
+ }
116
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Bash Runner 模块入口
3
+ */
4
+ export { BashProcessRunner, type BashRunner } from './runner.js';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Bash Runner 模块入口
3
+ */
4
+ export { BashProcessRunner } from './runner.js';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,55 @@
1
+ interface BashOutputEvent {
2
+ type: 'stdout' | 'stderr' | 'exit';
3
+ content: string;
4
+ exitCode?: number;
5
+ }
6
+ export interface BashRunner {
7
+ run(command: string, opts: {
8
+ cwd: string;
9
+ }): AsyncGenerator<BashOutputEvent>;
10
+ stop(opts?: {
11
+ immediate?: boolean;
12
+ }): Promise<void>;
13
+ get isRunning(): boolean;
14
+ }
15
+ /**
16
+ * Lightweight bash executor for `!` commands.
17
+ * Spawns `bash -c "<command>"` and yields stdout/stderr/exit as events.
18
+ *
19
+ * Output streaming is event-driven: the run loop races the next stdout/stderr
20
+ * `data` event against process `exit`/`error`, so it wakes instantly on either
21
+ * (no `setTimeout` busy-wait polling). This drops exit latency from ~100ms
22
+ * (the previous poll granularity) to ~0 and avoids periodic CPU wakeups while
23
+ * a long command produces no output. Each stream has a single `data` handler
24
+ * that logs, enqueues, and wakes the loop.
25
+ */
26
+ export declare class BashProcessRunner implements BashRunner {
27
+ private currentProcess;
28
+ /**
29
+ * 背压高水位/低水位(P1-4 层④,参照 jsonl-stream.ts 的 pauseThreshold/
30
+ * resumeThreshold)。stdout/stderr 队列超过高水位时 pause 流(内存有界),
31
+ * 消费者 drain 到低水位后 resume。构造可注入以便测试用小水位。
32
+ */
33
+ private readonly queueHighWater;
34
+ private readonly queueLowWater;
35
+ private stopper;
36
+ constructor(opts?: {
37
+ stopGraceMs?: number;
38
+ queueHighWater?: number;
39
+ queueLowWater?: number;
40
+ });
41
+ get isRunning(): boolean;
42
+ run(command: string, opts: {
43
+ cwd: string;
44
+ }): AsyncGenerator<BashOutputEvent>;
45
+ /**
46
+ * P1-22: called by the process-level exit dispatcher on SIGINT/SIGTERM/exit.
47
+ * Kills the whole process group (leader + background children) via
48
+ * ProcessStopper, matching agent-runner cleanupOnExit semantics.
49
+ */
50
+ cleanupOnExit(): void;
51
+ stop(opts?: {
52
+ immediate?: boolean;
53
+ }): Promise<void>;
54
+ }
55
+ export {};
@@ -0,0 +1,213 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { getLogger } from '../../logger/index.js';
3
+ import { ProcessStopper } from '../common/process-stopper.js';
4
+ import { registerExitCleanup, unregisterExitCleanup } from '../common/spawning-runner.js';
5
+ /**
6
+ * Lightweight bash executor for `!` commands.
7
+ * Spawns `bash -c "<command>"` and yields stdout/stderr/exit as events.
8
+ *
9
+ * Output streaming is event-driven: the run loop races the next stdout/stderr
10
+ * `data` event against process `exit`/`error`, so it wakes instantly on either
11
+ * (no `setTimeout` busy-wait polling). This drops exit latency from ~100ms
12
+ * (the previous poll granularity) to ~0 and avoids periodic CPU wakeups while
13
+ * a long command produces no output. Each stream has a single `data` handler
14
+ * that logs, enqueues, and wakes the loop.
15
+ */
16
+ export class BashProcessRunner {
17
+ currentProcess = null;
18
+ /**
19
+ * 背压高水位/低水位(P1-4 层④,参照 jsonl-stream.ts 的 pauseThreshold/
20
+ * resumeThreshold)。stdout/stderr 队列超过高水位时 pause 流(内存有界),
21
+ * 消费者 drain 到低水位后 resume。构造可注入以便测试用小水位。
22
+ */
23
+ queueHighWater;
24
+ queueLowWater;
25
+ stopper;
26
+ constructor(opts) {
27
+ const stopGraceMs = opts?.stopGraceMs ?? 1000;
28
+ this.queueHighWater = opts?.queueHighWater ?? 32;
29
+ this.queueLowWater = opts?.queueLowWater ?? 16;
30
+ if (this.queueLowWater > this.queueHighWater) {
31
+ this.queueLowWater = this.queueHighWater;
32
+ }
33
+ this.stopper = new ProcessStopper({ graceMs: stopGraceMs });
34
+ }
35
+ get isRunning() {
36
+ return (this.currentProcess !== null &&
37
+ this.currentProcess.exitCode === null &&
38
+ this.currentProcess.signalCode === null);
39
+ }
40
+ async *run(command, opts) {
41
+ if (this.isRunning) {
42
+ getLogger().warn('[bash-runner] run() called while already running');
43
+ throw new Error('bash process already running');
44
+ }
45
+ getLogger().debug(`[bash-runner] spawning command="${command}" cwd=${opts.cwd}`);
46
+ const proc = spawn('bash', ['-c', command], {
47
+ cwd: opts.cwd,
48
+ stdio: ['ignore', 'pipe', 'pipe'],
49
+ // Spawn in new process group so we can kill the entire group
50
+ detached: true,
51
+ });
52
+ this.currentProcess = proc;
53
+ getLogger().info(`[bash-runner] spawn pid=${proc.pid} command="${command.slice(0, 50)}..." cwd=${opts.cwd}`);
54
+ // P1-22: register with the process-level exit dispatcher while the process
55
+ // is alive — bridge exit/restart (SIGINT/SIGTERM/exit) then kills the whole
56
+ // group via cleanupOnExit instead of orphaning `!` processes.
57
+ registerExitCleanup(this);
58
+ // Single handler per stream: log + enqueue + wake the run loop. The
59
+ // previous implementation registered two `data` listeners per stream (one
60
+ // for logging, one for queueing) -- both fired on the same chunk.
61
+ const stdoutQueue = [];
62
+ const stderrQueue = [];
63
+ // 背压状态(P1-4 层④):队列超高水位 → pause 对应流;drain 到低水位 → resume。
64
+ let stdoutPaused = false;
65
+ let stderrPaused = false;
66
+ const maybePauseStdout = () => {
67
+ if (!stdoutPaused && stdoutQueue.length > this.queueHighWater) {
68
+ stdoutPaused = true;
69
+ proc.stdout.pause();
70
+ }
71
+ };
72
+ const maybeResumeStdout = () => {
73
+ if (stdoutPaused && stdoutQueue.length <= this.queueLowWater) {
74
+ stdoutPaused = false;
75
+ proc.stdout.resume();
76
+ }
77
+ };
78
+ const maybePauseStderr = () => {
79
+ if (!stderrPaused && stderrQueue.length > this.queueHighWater) {
80
+ stderrPaused = true;
81
+ proc.stderr.pause();
82
+ }
83
+ };
84
+ const maybeResumeStderr = () => {
85
+ if (stderrPaused && stderrQueue.length <= this.queueLowWater) {
86
+ stderrPaused = false;
87
+ proc.stderr.resume();
88
+ }
89
+ };
90
+ // Resolved by a `data` event to signal "more output may be available,
91
+ // re-check the queues". Held as a field so either stream's handler can
92
+ // resolve the single in-flight waiter. Cleared on resolve and on loop exit.
93
+ let dataWaiter = null;
94
+ const notifyData = () => {
95
+ if (dataWaiter) {
96
+ const resolve = dataWaiter;
97
+ dataWaiter = null;
98
+ resolve();
99
+ }
100
+ };
101
+ proc.stdout?.on('data', (chunk) => {
102
+ const text = chunk.toString('utf-8');
103
+ getLogger().debug(`[bash-runner] stdout: ${text.slice(0, 100)}`);
104
+ stdoutQueue.push(text);
105
+ maybePauseStdout();
106
+ notifyData();
107
+ });
108
+ proc.stderr?.on('data', (chunk) => {
109
+ const text = chunk.toString('utf-8');
110
+ getLogger().debug(`[bash-runner] stderr: ${text.slice(0, 100)}`);
111
+ stderrQueue.push(text);
112
+ maybePauseStderr();
113
+ notifyData();
114
+ });
115
+ // Use the `exit` event (not `close`): `close` waits for ALL child processes
116
+ // including background daemons (`nohup &`), which can take forever. `exit`
117
+ // fires as soon as bash itself exits. A SIGKILL from /stop sets `signal`
118
+ // while `code` stays null, so callers distinguish signal-kill from exit.
119
+ // A spawn failure (e.g. bash missing) emits `error` and never `exit`;
120
+ // resolving completion with code=1 here lets the loop terminate instead of
121
+ // spinning forever on a permanently-null exitCode check (the old poll loop
122
+ // had the same latent hang on a missing bash binary).
123
+ let completionResult = null;
124
+ const completion = new Promise((resolve) => {
125
+ proc.once('exit', (code, signal) => {
126
+ completionResult = { code, signal };
127
+ resolve();
128
+ });
129
+ proc.once('error', (err) => {
130
+ getLogger().error('[bash-runner] process error:', err.message);
131
+ completionResult = { code: 1, signal: null };
132
+ resolve();
133
+ });
134
+ });
135
+ try {
136
+ while (completionResult === null) {
137
+ // Drain queued chunks before waiting, so output is streamed
138
+ // incrementally rather than batched at exit.
139
+ while (stdoutQueue.length > 0) {
140
+ yield { type: 'stdout', content: stdoutQueue.shift() };
141
+ maybeResumeStdout();
142
+ }
143
+ while (stderrQueue.length > 0) {
144
+ yield { type: 'stderr', content: stderrQueue.shift() };
145
+ maybeResumeStderr();
146
+ }
147
+ if (completionResult === null) {
148
+ // Wait for the next chunk OR process exit/error -- no fixed-interval
149
+ // polling. Racing `completion` guarantees the loop wakes on exit even
150
+ // when the command produces no output (e.g. `true`, `nohup &`), which
151
+ // the previous setTimeout(100) poll only caught after a full tick.
152
+ const nextData = new Promise((resolve) => {
153
+ dataWaiter = resolve;
154
+ });
155
+ await Promise.race([nextData, completion]);
156
+ dataWaiter = null;
157
+ }
158
+ }
159
+ // Drain any chunks that arrived between the last drain and exit. (Data
160
+ // emitted after the exit yield is not delivered -- matching the prior
161
+ // contract; the bridge accumulates output/stderr from pre-exit events.)
162
+ while (stdoutQueue.length > 0) {
163
+ yield { type: 'stdout', content: stdoutQueue.shift() };
164
+ maybeResumeStdout();
165
+ }
166
+ while (stderrQueue.length > 0) {
167
+ yield { type: 'stderr', content: stderrQueue.shift() };
168
+ maybeResumeStderr();
169
+ }
170
+ const { code, signal } = completionResult;
171
+ getLogger().info(`[bash-runner] process exited with code=${code} signal=${signal}`);
172
+ // A SIGKILL'd process (e.g. from /stop) has exitCode=null and a signal
173
+ // set. Reporting `code ?? 0` would falsely claim success; use -1 to
174
+ // signal abnormal termination by signal.
175
+ yield { type: 'exit', content: '', exitCode: code ?? (signal ? -1 : 0) };
176
+ }
177
+ finally {
178
+ dataWaiter = null;
179
+ this.currentProcess = null;
180
+ // Run finished — remove from the exit dispatcher so dead runners are not
181
+ // retained (mirror agent-runner slot eviction; keeps the Set bounded to
182
+ // active bash runs only).
183
+ unregisterExitCleanup(this);
184
+ }
185
+ }
186
+ /**
187
+ * P1-22: called by the process-level exit dispatcher on SIGINT/SIGTERM/exit.
188
+ * Kills the whole process group (leader + background children) via
189
+ * ProcessStopper, matching agent-runner cleanupOnExit semantics.
190
+ */
191
+ cleanupOnExit() {
192
+ const proc = this.currentProcess;
193
+ if (!proc || proc.exitCode !== null || proc.signalCode !== null)
194
+ return;
195
+ try {
196
+ void this.stopper.stop(proc, { immediate: true });
197
+ }
198
+ catch {
199
+ // fire-and-forget: nothing else to do at process exit
200
+ }
201
+ }
202
+ async stop(opts) {
203
+ const proc = this.currentProcess;
204
+ if (!proc || proc.exitCode !== null || proc.signalCode !== null) {
205
+ this.currentProcess = null;
206
+ return;
207
+ }
208
+ // Delegate to ProcessStopper for unified stop semantics
209
+ await this.stopper.stop(proc, { immediate: opts?.immediate });
210
+ this.currentProcess = null;
211
+ }
212
+ }
213
+ //# sourceMappingURL=runner.js.map
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Claude Runner 模块入口
3
+ */
4
+ export { ClaudeRunner } from './runner.js';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Claude Runner 模块入口
3
+ */
4
+ export { ClaudeRunner } from './runner.js';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,29 @@
1
+ import { type AgentEvent, type SpawnOptions, type AgentSessionReader, type AgentRunner as IAgentRunner, type AgentStatusInfo } from '../types.js';
2
+ import { SpawningRunner } from '../common/spawning-runner.js';
3
+ /**
4
+ * Claude Agent Runner
5
+ *
6
+ * 使用 Claude Code CLI (claude) 作为后端 AI 代理
7
+ */
8
+ export declare class ClaudeRunner extends SpawningRunner implements IAgentRunner {
9
+ readonly kind: "claude";
10
+ readonly sessionReader: AgentSessionReader;
11
+ private defaultModel;
12
+ private defaultEffort;
13
+ private defaultSettings?;
14
+ constructor(opts: {
15
+ binary?: string;
16
+ model?: string;
17
+ effort?: string;
18
+ settings?: string;
19
+ stopGraceMs?: number;
20
+ pidDir?: string;
21
+ workspace: string;
22
+ spawnHeartbeatMs?: number;
23
+ sessionReader?: AgentSessionReader;
24
+ });
25
+ get pid(): number | undefined;
26
+ getStatusInfo(): AgentStatusInfo;
27
+ protected buildArgv(opts: SpawnOptions): string[];
28
+ protected translate(rawEvent: unknown, _ctx: unknown): AgentEvent | AgentEvent[] | null;
29
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Claude Runner 实现
3
+ *
4
+ * 从 runner/index.ts 提取的 ClaudeRunner 类
5
+ */
6
+ import { ClaudeSessionReader } from '../../session/claude/index.js';
7
+ import { SpawningRunner } from '../common/spawning-runner.js';
8
+ import { MODEL_ID_TO_ALIAS } from '../../config/index.js';
9
+ /**
10
+ * Claude Agent Runner
11
+ *
12
+ * 使用 Claude Code CLI (claude) 作为后端 AI 代理
13
+ */
14
+ export class ClaudeRunner extends SpawningRunner {
15
+ kind = 'claude';
16
+ sessionReader;
17
+ defaultModel;
18
+ defaultEffort;
19
+ defaultSettings;
20
+ constructor(opts) {
21
+ super({
22
+ binary: opts.binary ?? 'claude',
23
+ pidDir: opts.pidDir,
24
+ workspace: opts.workspace,
25
+ stopGraceMs: opts.stopGraceMs,
26
+ spawnHeartbeatMs: opts.spawnHeartbeatMs,
27
+ pidFilePrefix: 'claude',
28
+ logTag: 'claude-runner',
29
+ });
30
+ this.defaultModel = opts.model ?? 'claude-opus-4-8';
31
+ this.defaultEffort = opts.effort ?? 'medium';
32
+ this.defaultSettings = opts.settings;
33
+ this.sessionReader = opts.sessionReader ?? new ClaudeSessionReader();
34
+ }
35
+ get pid() {
36
+ return this.currentProcess?.pid;
37
+ }
38
+ getStatusInfo() {
39
+ const alias = MODEL_ID_TO_ALIAS[this.defaultModel] ?? this.defaultModel;
40
+ const isHaiku = this.defaultModel.includes('haiku');
41
+ return {
42
+ kind: 'claude',
43
+ model: alias,
44
+ reasoning: isHaiku ? 'off' : this.defaultEffort,
45
+ };
46
+ }
47
+ buildArgv(opts) {
48
+ const args = [
49
+ '-p',
50
+ this.currentMessage,
51
+ '--output-format',
52
+ 'stream-json',
53
+ '--verbose',
54
+ // Hardcoded: Feishu chat doesn't support interactive approval.
55
+ '--permission-mode',
56
+ 'bypassPermissions',
57
+ ];
58
+ if (opts.sessionId) {
59
+ args.push('--resume', opts.sessionId);
60
+ }
61
+ if (opts.model) {
62
+ args.push('--model', opts.model);
63
+ }
64
+ const effort = opts.effort ?? this.defaultEffort;
65
+ if (effort) {
66
+ args.push('--effort', effort);
67
+ }
68
+ const settings = opts.settings ?? this.defaultSettings;
69
+ if (settings) {
70
+ args.push('--settings', settings);
71
+ }
72
+ return args;
73
+ }
74
+ translate(rawEvent, _ctx) {
75
+ const event = rawEvent;
76
+ // 统一方案:所有 runner 自己生成 timestamp
77
+ if (!event.timestamp) {
78
+ event.timestamp = new Date().toISOString();
79
+ }
80
+ return event;
81
+ }
82
+ }
83
+ //# sourceMappingURL=runner.js.map