lark-remote 0.1.10 → 0.1.11

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/CHANGELOG.md CHANGED
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.1.11] - 2026-08-24
9
+
10
+ ### 新增
11
+
12
+ - kimi ACP 支持 Bash 命令自主执行:`terminal/*` reverse RPC 将 Bash 工具下放到本地子进程(一次性 `bash -c <script>`),yolo/auto 模式自主跑命令,manual 模式仍走审批;run 卡片显示带真实命令与本地输出的 Bash 面板,并过滤 kimi 自产的双份 Bash 通知
13
+
14
+ ### 变更
15
+
16
+ - kimi ACP 能力声明:`fs` 保持关闭(走本地磁盘兜底,避免未实现的 `fs/*` RPC 报 `METHOD_NOT_FOUND`),`terminal` 打开
17
+ - 文档清理:移除设计文档中对内部文档名(`lessons-redlines`)与本地仓库路径(`/Users/.../lark-remote`)的引用
18
+
8
19
  ## [0.1.10] - 2026-08-22
9
20
 
10
21
  ### 新增
@@ -39,7 +39,7 @@ export declare const RpcErrorCode: {
39
39
  export interface InitializeParams {
40
40
  /** Protocol version — must be 1 (number, not string). */
41
41
  protocolVersion: number;
42
- /** Client capabilities: explicitly declare what we support (all off). */
42
+ /** Client capabilities: declare only reverse-RPC surfaces we implement. */
43
43
  clientCapabilities: {
44
44
  fs: {
45
45
  readTextFile: boolean;
@@ -209,6 +209,12 @@ export declare const ServerRequestMethod: {
209
209
  readonly REQUEST_PERMISSION: "session/request_permission";
210
210
  /** Kimi AskUserQuestion elicitation form(客户端广告 elicitation.form 时启用)。 */
211
211
  readonly ELICITATION_CREATE: "elicitation/create";
212
+ /** kimi terminal 工具 reverse RPC:Bash 执行下放客户端(acp-terminal)。 */
213
+ readonly TERMINAL_CREATE: "terminal/create";
214
+ readonly TERMINAL_OUTPUT: "terminal/output";
215
+ readonly TERMINAL_WAIT_FOR_EXIT: "terminal/wait_for_exit";
216
+ readonly TERMINAL_KILL: "terminal/kill";
217
+ readonly TERMINAL_RELEASE: "terminal/release";
212
218
  };
213
219
  export interface PermissionOption {
214
220
  /** Opaque option identifier — echo back in response outcome.optionId. */
@@ -269,3 +275,34 @@ export interface ElicitationCreateResponse {
269
275
  /** accept 时的答案,key 为 q0..qn(按问题顺序)。 */
270
276
  content?: Record<string, string | string[]>;
271
277
  }
278
+ export interface TerminalCreateParams {
279
+ sessionId: string;
280
+ /** 恒为 'bash'(服务端 spawn 只接受 bash -c)。 */
281
+ command: string;
282
+ /** ['-c', <script>]:-c 在 args[0],脚本在 args[1]。 */
283
+ args: string[];
284
+ env?: Array<{
285
+ name: string;
286
+ value: string;
287
+ }>;
288
+ /** 可能为 null → 回退 session cwd。 */
289
+ cwd?: string | null;
290
+ /** 输出缓冲上限(字节),默认 4MB。 */
291
+ outputByteLimit?: number;
292
+ }
293
+ export interface TerminalOutputParams {
294
+ sessionId: string;
295
+ terminalId: string;
296
+ }
297
+ export interface TerminalWaitForExitParams {
298
+ sessionId: string;
299
+ terminalId: string;
300
+ }
301
+ export interface TerminalKillParams {
302
+ sessionId: string;
303
+ terminalId: string;
304
+ }
305
+ export interface TerminalReleaseParams {
306
+ sessionId: string;
307
+ terminalId: string;
308
+ }
@@ -73,5 +73,11 @@ export const ServerRequestMethod = {
73
73
  REQUEST_PERMISSION: 'session/request_permission',
74
74
  /** Kimi AskUserQuestion elicitation form(客户端广告 elicitation.form 时启用)。 */
75
75
  ELICITATION_CREATE: 'elicitation/create',
76
+ /** kimi terminal 工具 reverse RPC:Bash 执行下放客户端(acp-terminal)。 */
77
+ TERMINAL_CREATE: 'terminal/create',
78
+ TERMINAL_OUTPUT: 'terminal/output',
79
+ TERMINAL_WAIT_FOR_EXIT: 'terminal/wait_for_exit',
80
+ TERMINAL_KILL: 'terminal/kill',
81
+ TERMINAL_RELEASE: 'terminal/release',
76
82
  };
77
83
  //# sourceMappingURL=protocol-types.js.map
@@ -46,6 +46,21 @@ export declare class KimiAcpRunner extends ConnectionBasedRunner<KimiAcpClient,
46
46
  private readonly compactPollTimeoutMs;
47
47
  /** Pending approval requests: requestId → kind + view + options. */
48
48
  private pendingApprovals;
49
+ /** 存活 terminal(kimi 把 Bash 执行下放客户端):terminalId → 本地句柄。 */
50
+ private terminals;
51
+ /** terminalId 自增序号(term-1, term-2, …)。 */
52
+ private terminalSeq;
53
+ /**
54
+ * kimi 服务端对 Bash 工具的 tool_call/tool_call_update 通知(title='Bash')。
55
+ * Bash 的真实执行走 terminal/* reverse RPC 下放客户端,tool 事件由本 runner
56
+ * 自产(tool_use 带命令、tool_result 带本地输出);这些通知必须过滤,否则
57
+ * 卡片出现双份 Bash 面板(通知版无 rawInput、terminal embed 无 rawOutput,
58
+ * 只能渲染「_无输出_」)。收集到的 toolCallId 用于过滤对应的 update。
59
+ */
60
+ private bashToolCallIds;
61
+ /** 当前 run 的 session cwd(terminal/create 未给 cwd 时回退用)。 */
62
+ private activeCwd;
63
+ private readonly processStopper;
49
64
  /** Tracker for the in-flight prompt request — needed for cancellation. */
50
65
  private promptSettled;
51
66
  constructor(opts: KimiAcpRunnerOptions);
@@ -102,5 +117,41 @@ export declare class KimiAcpRunner extends ConnectionBasedRunner<KimiAcpClient,
102
117
  */
103
118
  private waitForCompactionRecord;
104
119
  private handleNotification;
120
+ /**
121
+ * Filter kimi's own Bash tool notifications so the run card doesn't get a
122
+ * duplicate, output-less Bash panel alongside the runner-produced one.
123
+ *
124
+ * - tool_call title='Bash' → remember the toolCallId, drop the notification
125
+ * (the tool_use comes from terminal/create with the real command).
126
+ * - tool_call_update for a remembered id → drop (streaming args deltas and
127
+ * the terminal embed final update; the tool_result is emitted locally).
128
+ * Returns true when the notification should be swallowed.
129
+ */
130
+ private filterBashNotifications;
105
131
  private handleServerRequest;
132
+ /**
133
+ * Handle a kimi terminal/* reverse RPC by spawning a one-shot local process.
134
+ *
135
+ * 契约(kimi-code acp-terminal/acpTerminalRunner.ts):
136
+ * create → 立即回 {terminalId},进程异步跑,不阻塞 RPC;
137
+ * output → 回累计 stdout(非增量,服务端按 emitted 偏移切片);
138
+ * wait_for_exit → 未退出则等 exit 再回 {exitCode, signal};
139
+ * kill/release → 杀进程并回 {}。
140
+ */
141
+ private handleTerminalRequest;
142
+ /**
143
+ * terminal/create: spawn 一次性进程(bash -c <script>),流式累积
144
+ * stdout(+stderr) 到缓冲,立即回 {terminalId}。
145
+ */
146
+ private handleTerminalCreate;
147
+ /** terminal/output: 回累计全量输出 + truncated + 当前退出状态(可未退出)。 */
148
+ private handleTerminalOutput;
149
+ /** terminal/wait_for_exit: 未退出则等 exit promise 再回 {exitCode, signal}。 */
150
+ private handleTerminalWaitForExit;
151
+ /** terminal/kill: 立即杀进程组,回 {}。 */
152
+ private handleTerminalKill;
153
+ /** terminal/release: 从 map 删除;进程仍在则一并杀,回 {}。 */
154
+ private handleTerminalRelease;
155
+ /** Kill every live terminal (dispose/connection-close 清理钩子)。 */
156
+ private cleanupTerminals;
106
157
  }
@@ -11,19 +11,31 @@
11
11
  * pattern, same forceFinish/stopRequested semantics, same turn idle timeout,
12
12
  * same ConnectionLostError retry.
13
13
  */
14
+ import { spawn } from 'node:child_process';
15
+ import { StringDecoder } from 'node:string_decoder';
14
16
  import { ConnectionManager as KimiAcpConnectionManager, } from '../../common/jsonrpc/connection-manager.js';
15
17
  import { KimiAcpTranslator } from './translator.js';
16
- import { RpcErrorCode, ServerRequestMethod, } from '../../common/acp/protocol-types.js';
18
+ import { NotificationMethod, RpcErrorCode, ServerRequestMethod, } from '../../common/acp/protocol-types.js';
17
19
  import { findOptionIdByKind } from '../../common/acp/protocol-helpers.js';
18
20
  import { mapAnswersByIndex } from '../../question-common.js';
19
21
  import { getLogger } from '../../../logger/index.js';
20
22
  import { ConnectionBasedRunner } from '../../common/connection-based-runner.js';
23
+ import { ProcessStopper } from '../../common/process-stopper.js';
21
24
  /** R2: how long to wait for the background compaction record after the
22
25
  * /compact prompt settles (design doc §6.1). Real kimi 0.36.0 lands
23
26
  * context.apply_compaction ~8s after end_turn. */
24
27
  const COMPACT_POLL_TIMEOUT_MS = 30_000;
25
28
  /** Poll interval for the wire.jsonl compaction record. */
26
29
  const COMPACT_POLL_INTERVAL_MS = 1_000;
30
+ // =============================================================================
31
+ // kimi terminal 工具(Bash 执行下放客户端)
32
+ // 契约:kimi-code/packages/acp-server/src/acp-terminal/acpTerminalRunner.ts
33
+ // 一次性 bash -c <script>,非交互 PTY、无 stdin;服务端每 250ms 轮询
34
+ // terminal/output,客户端只回累计 stdout。创建/轮询/等待/杀/释放五个
35
+ // reverse RPC 全部由本 runner 本地 spawn 子进程实现。
36
+ // =============================================================================
37
+ /** terminal/create 未显式给 outputByteLimit 时的缓冲上限(对齐服务端默认 4MB)。 */
38
+ const TERMINAL_DEFAULT_OUTPUT_LIMIT = 4 * 1024 * 1024;
27
39
  /**
28
40
  * Map user-facing permission mode to ACP mode id.
29
41
  *
@@ -162,6 +174,21 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
162
174
  compactPollTimeoutMs;
163
175
  /** Pending approval requests: requestId → kind + view + options. */
164
176
  pendingApprovals = new Map();
177
+ /** 存活 terminal(kimi 把 Bash 执行下放客户端):terminalId → 本地句柄。 */
178
+ terminals = new Map();
179
+ /** terminalId 自增序号(term-1, term-2, …)。 */
180
+ terminalSeq = 0;
181
+ /**
182
+ * kimi 服务端对 Bash 工具的 tool_call/tool_call_update 通知(title='Bash')。
183
+ * Bash 的真实执行走 terminal/* reverse RPC 下放客户端,tool 事件由本 runner
184
+ * 自产(tool_use 带命令、tool_result 带本地输出);这些通知必须过滤,否则
185
+ * 卡片出现双份 Bash 面板(通知版无 rawInput、terminal embed 无 rawOutput,
186
+ * 只能渲染「_无输出_」)。收集到的 toolCallId 用于过滤对应的 update。
187
+ */
188
+ bashToolCallIds = new Set();
189
+ /** 当前 run 的 session cwd(terminal/create 未给 cwd 时回退用)。 */
190
+ activeCwd = null;
191
+ processStopper = new ProcessStopper({ graceMs: 2_000 });
165
192
  /** Tracker for the in-flight prompt request — needed for cancellation. */
166
193
  promptSettled = false;
167
194
  constructor(opts) {
@@ -178,8 +205,13 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
178
205
  initializeParams: {
179
206
  protocolVersion: 1,
180
207
  clientCapabilities: {
208
+ // fs 保持关闭:kimi 服务端在 fs 能力为 false 时走本地磁盘兜底
209
+ // (acpFsService.ts AcpHostFileSystem.inner),kimi 是本地子进程,
210
+ // 本地盘即工作区盘,读写正常;声明 true 则服务端改发 fs/* reverse
211
+ // RPC,lark-remote 未实现该面,文件读写会 METHOD_NOT_FOUND 失败。
181
212
  fs: { readTextFile: false, writeTextFile: false },
182
- terminal: false,
213
+ // terminal 常开:yolo/auto 自主跑命令;manual 走 session/request_permission 审批。
214
+ terminal: true,
183
215
  // AskUserQuestion 走 elicitation form(原生多题+多选;form 失败时
184
216
  // kimi 服务端自动回退 request_permission 桥,客户端两者都处理)。
185
217
  // 注意:ACP SDK 对 elicitation.form 的 zod schema 是对象
@@ -223,6 +255,7 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
223
255
  this.currentTranslator = null;
224
256
  this.promptSettled = false;
225
257
  this.activeSessionId = null;
258
+ this.activeCwd = null;
226
259
  }
227
260
  async releaseConnection(cwd) {
228
261
  await this.connectionManager.release(cwd);
@@ -231,6 +264,8 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
231
264
  this.connectionManager.notifyIdle(cwd);
232
265
  }
233
266
  async disposeConnections() {
267
+ // 关闭前 kill/release 所有存活 terminal,避免孤儿 bash 进程。
268
+ await this.cleanupTerminals();
234
269
  await this.connectionManager.disposeAll();
235
270
  }
236
271
  /**
@@ -245,10 +280,14 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
245
280
  const client = await this.connectionManager.acquire(opts.cwd);
246
281
  this.connectionManager.notifyActivity(opts.cwd);
247
282
  this.currentClient = client;
283
+ this.activeCwd = opts.cwd;
248
284
  client.setHooks({
249
285
  onNotification: (method, params) => this.handleNotification(method, params),
250
286
  onServerRequest: (id, method, params) => this.handleServerRequest(id, method, params),
251
- onClose: () => this.failTurn('Kimi ACP connection closed'),
287
+ onClose: () => {
288
+ void this.cleanupTerminals();
289
+ this.failTurn('Kimi ACP connection closed');
290
+ },
252
291
  });
253
292
  const sessionId = opts.sessionId;
254
293
  if (!sessionId) {
@@ -382,11 +421,13 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
382
421
  const client = await this.connectionManager.acquire(opts.cwd);
383
422
  this.connectionManager.notifyActivity(opts.cwd);
384
423
  this.currentClient = client;
424
+ this.activeCwd = opts.cwd;
385
425
  client.setHooks({
386
426
  onNotification: (method, params) => this.handleNotification(method, params),
387
427
  onServerRequest: (id, method, params) => this.handleServerRequest(id, method, params),
388
428
  onClose: () => {
389
429
  getLogger().warn(`[${this.logTag}] client connection closed`);
430
+ void this.cleanupTerminals();
390
431
  this.failTurn('Kimi ACP connection closed');
391
432
  },
392
433
  });
@@ -494,10 +535,61 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
494
535
  getLogger().warn(`[${this.logTag}] compaction record not observed within ${this.compactPollTimeoutMs}ms (baseline=${baselineCount}); proceeding without confirmation`);
495
536
  }
496
537
  handleNotification(method, params) {
538
+ // Terminal-backed Bash 的通知走 runner 自产事件(见 bashToolCallIds),
539
+ // 不交给 translator;其余通知(Read/Edit/text/thinking)正常翻译。
540
+ if (method === NotificationMethod.SESSION_UPDATE && this.filterBashNotifications(params)) {
541
+ return;
542
+ }
497
543
  const events = this.currentTranslator?.handleNotification(method, params) ?? [];
498
544
  this.pushEvents(events);
499
545
  }
546
+ /**
547
+ * Filter kimi's own Bash tool notifications so the run card doesn't get a
548
+ * duplicate, output-less Bash panel alongside the runner-produced one.
549
+ *
550
+ * - tool_call title='Bash' → remember the toolCallId, drop the notification
551
+ * (the tool_use comes from terminal/create with the real command).
552
+ * - tool_call_update for a remembered id → drop (streaming args deltas and
553
+ * the terminal embed final update; the tool_result is emitted locally).
554
+ * Returns true when the notification should be swallowed.
555
+ */
556
+ filterBashNotifications(params) {
557
+ if (typeof params !== 'object' || params === null)
558
+ return false;
559
+ const update = params.update;
560
+ if (!update || typeof update !== 'object')
561
+ return false;
562
+ if (update.sessionUpdate === 'tool_call') {
563
+ if (update.title === 'Bash') {
564
+ if (typeof update.toolCallId === 'string') {
565
+ this.bashToolCallIds.add(update.toolCallId);
566
+ }
567
+ return true;
568
+ }
569
+ return false;
570
+ }
571
+ if (update.sessionUpdate === 'tool_call_update') {
572
+ const toolCallId = update.toolCallId;
573
+ if (typeof toolCallId === 'string' && this.bashToolCallIds.has(toolCallId)) {
574
+ // 终态后清掉 id,避免集合随会话无限增长(漏清无碍:id 全局唯一)。
575
+ if (update.status === 'completed' || update.status === 'failed') {
576
+ this.bashToolCallIds.delete(toolCallId);
577
+ }
578
+ return true;
579
+ }
580
+ return false;
581
+ }
582
+ return false;
583
+ }
500
584
  handleServerRequest(id, method, params) {
585
+ // kimi terminal 工具:Bash 执行下放客户端(acp-terminal reverse RPC)。
586
+ // 请求本身是纯 request/response I/O,不交给 translator(避免落入下方
587
+ // 「空事件 → 拒绝」兜底);对应的 tool_use/tool_result 事件由
588
+ // handleTerminalCreate 自产(带本地命令与输出,见 bashToolCallIds)。
589
+ if (method.startsWith('terminal/')) {
590
+ void this.handleTerminalRequest(id, method, params);
591
+ return;
592
+ }
501
593
  const events = this.currentTranslator?.handleServerRequest(id, method, params) ?? [];
502
594
  if (events.length === 0) {
503
595
  // Unhandled/unsupported server request must be explicitly responded to
@@ -527,5 +619,221 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
527
619
  }
528
620
  this.pushEvents(events);
529
621
  }
622
+ /**
623
+ * Handle a kimi terminal/* reverse RPC by spawning a one-shot local process.
624
+ *
625
+ * 契约(kimi-code acp-terminal/acpTerminalRunner.ts):
626
+ * create → 立即回 {terminalId},进程异步跑,不阻塞 RPC;
627
+ * output → 回累计 stdout(非增量,服务端按 emitted 偏移切片);
628
+ * wait_for_exit → 未退出则等 exit 再回 {exitCode, signal};
629
+ * kill/release → 杀进程并回 {}。
630
+ */
631
+ async handleTerminalRequest(id, method, params) {
632
+ try {
633
+ switch (method) {
634
+ case ServerRequestMethod.TERMINAL_CREATE:
635
+ this.handleTerminalCreate(id, params);
636
+ return;
637
+ case ServerRequestMethod.TERMINAL_OUTPUT:
638
+ this.handleTerminalOutput(id, params);
639
+ return;
640
+ case ServerRequestMethod.TERMINAL_WAIT_FOR_EXIT:
641
+ await this.handleTerminalWaitForExit(id, params);
642
+ return;
643
+ case ServerRequestMethod.TERMINAL_KILL:
644
+ await this.handleTerminalKill(id, params);
645
+ return;
646
+ case ServerRequestMethod.TERMINAL_RELEASE:
647
+ await this.handleTerminalRelease(id, params);
648
+ return;
649
+ default:
650
+ this.currentClient?.respondError(id, RpcErrorCode.METHOD_NOT_FOUND, `Unsupported server request: ${method}`);
651
+ }
652
+ }
653
+ catch (err) {
654
+ getLogger().warn(`[${this.logTag}] terminal request failed method=${method}: ${err.message}`);
655
+ this.currentClient?.respondError(id, RpcErrorCode.INTERNAL_ERROR, `Terminal request failed: ${err.message}`);
656
+ }
657
+ }
658
+ /**
659
+ * terminal/create: spawn 一次性进程(bash -c <script>),流式累积
660
+ * stdout(+stderr) 到缓冲,立即回 {terminalId}。
661
+ */
662
+ handleTerminalCreate(id, params) {
663
+ const env = params.env
664
+ ? Object.fromEntries(params.env.map((e) => [e.name, e.value]))
665
+ : undefined;
666
+ const cwd = params.cwd ?? this.activeCwd ?? undefined;
667
+ const outputLimit = params.outputByteLimit ?? TERMINAL_DEFAULT_OUTPUT_LIMIT;
668
+ const proc = spawn(params.command, params.args ?? [], {
669
+ cwd,
670
+ env: env ? { ...process.env, ...env } : undefined,
671
+ stdio: ['ignore', 'pipe', 'pipe'],
672
+ // ProcessStopper 用负 PID 杀进程组(kill(-pgid));子进程必须是组长
673
+ // 才能命中,否则 kill(-pid) 抛 ESRCH 被吞 → kill/release/清理全失效。
674
+ // 与 JsonlRpcTransport / spawning-runner 的 detached:true 同模式。
675
+ detached: true,
676
+ });
677
+ // 每个流独立 decoder:多字节字符跨 chunk 拆分时由 decoder 保留不完整
678
+ // 序列,避免按 chunk 单独 toString 产生 U+FFFD 替换符。
679
+ const stdoutDecoder = new StringDecoder('utf8');
680
+ const stderrDecoder = new StringDecoder('utf8');
681
+ const append = (chunk, decoder) => {
682
+ if (handle.truncated)
683
+ return;
684
+ const remaining = handle.outputLimit - handle.outputBytes;
685
+ if (remaining > 0 && chunk.length <= remaining) {
686
+ handle.output += decoder.write(chunk);
687
+ handle.outputBytes += chunk.length;
688
+ }
689
+ else if (remaining > 0) {
690
+ // 只喂 remaining 字节;decoder 内部保留末尾不完整序列,不会在字节
691
+ // 边界产生替换符。outputBytes 按「喂入字节」计,保证不超过上限。
692
+ handle.output += decoder.write(chunk.subarray(0, remaining));
693
+ handle.outputBytes += remaining;
694
+ }
695
+ // 契约(手册):truncated = 缓冲 >= limit——恰好填满也标 true。
696
+ if (handle.outputBytes >= handle.outputLimit)
697
+ handle.truncated = true;
698
+ };
699
+ proc.stdout?.on('data', (chunk) => append(chunk, stdoutDecoder));
700
+ proc.stderr?.on('data', (chunk) => append(chunk, stderrDecoder));
701
+ // spawn 失败(如 ENOENT)只发 'error' 不发 'exit'——一并 resolve,防止
702
+ // wait_for_exit 永久挂起(runner 层 ENOENT 兜底红线)。
703
+ // 用 resolveExit 承接,避免回调在 handle 初始化前引用它(TDZ)。
704
+ let resolveExit;
705
+ const exitPromise = new Promise((resolve) => {
706
+ resolveExit = resolve;
707
+ });
708
+ // 与 resolveExit 同款 TDZ 规避:tool_result 发射闭包在 terminalId 初始化
709
+ // 后赋值,exit/error 回调执行时经 emitResult 间接调用。
710
+ let emitTerminalResult = undefined;
711
+ const emitResult = () => {
712
+ emitTerminalResult?.();
713
+ };
714
+ const handle = {
715
+ proc,
716
+ output: '',
717
+ outputBytes: 0,
718
+ outputLimit,
719
+ truncated: false,
720
+ exitCode: null,
721
+ signal: null,
722
+ exitPromise,
723
+ };
724
+ proc.once('exit', (code, signal) => {
725
+ handle.exitCode = code;
726
+ handle.signal = signal;
727
+ resolveExit?.();
728
+ emitResult();
729
+ });
730
+ proc.once('error', (err) => {
731
+ getLogger().warn(`[${this.logTag}] terminal process error: ${err.message}`);
732
+ if (handle.exitCode === null && handle.signal === null) {
733
+ handle.exitCode = 1;
734
+ handle.signal = null;
735
+ }
736
+ resolveExit?.();
737
+ emitResult();
738
+ });
739
+ const terminalId = `term-${++this.terminalSeq}`;
740
+ this.terminals.set(terminalId, handle);
741
+ // Bash 工具事件由本 runner 自产(kimi 通知已过滤):create 即推 tool_use
742
+ // 带真实命令,让 run 卡立刻出现可识别的 Bash 面板。
743
+ const script = Array.isArray(params.args) && params.args.length > 1 ? params.args[1] : params.command;
744
+ let resultEmitted = false;
745
+ emitTerminalResult = () => {
746
+ if (resultEmitted)
747
+ return;
748
+ resultEmitted = true;
749
+ this.pushEvents([
750
+ {
751
+ type: 'user',
752
+ message: {
753
+ content: [
754
+ {
755
+ type: 'tool_result',
756
+ tool_use_id: terminalId,
757
+ content: handle.output,
758
+ is_error: handle.exitCode !== 0,
759
+ },
760
+ ],
761
+ },
762
+ timestamp: new Date().toISOString(),
763
+ },
764
+ ]);
765
+ };
766
+ this.pushEvents([
767
+ {
768
+ type: 'assistant',
769
+ message: {
770
+ content: [
771
+ {
772
+ type: 'tool_use',
773
+ id: terminalId,
774
+ name: 'Bash',
775
+ input: { command: script, cwd },
776
+ },
777
+ ],
778
+ },
779
+ timestamp: new Date().toISOString(),
780
+ },
781
+ ]);
782
+ getLogger().info(`[${this.logTag}] terminal created id=${terminalId} command=${params.command} args=${JSON.stringify(params.args ?? [])}`);
783
+ this.currentClient?.respond(id, { terminalId });
784
+ }
785
+ /** terminal/output: 回累计全量输出 + truncated + 当前退出状态(可未退出)。 */
786
+ handleTerminalOutput(id, params) {
787
+ const handle = this.terminals.get(params.terminalId);
788
+ if (!handle) {
789
+ this.currentClient?.respondError(id, RpcErrorCode.INVALID_PARAMS, `Unknown terminal: ${params.terminalId}`);
790
+ return;
791
+ }
792
+ this.currentClient?.respond(id, {
793
+ output: handle.output,
794
+ truncated: handle.truncated,
795
+ exitStatus: { exitCode: handle.exitCode, signal: handle.signal },
796
+ });
797
+ }
798
+ /** terminal/wait_for_exit: 未退出则等 exit promise 再回 {exitCode, signal}。 */
799
+ async handleTerminalWaitForExit(id, params) {
800
+ const handle = this.terminals.get(params.terminalId);
801
+ if (!handle) {
802
+ this.currentClient?.respondError(id, RpcErrorCode.INVALID_PARAMS, `Unknown terminal: ${params.terminalId}`);
803
+ return;
804
+ }
805
+ await handle.exitPromise;
806
+ this.currentClient?.respond(id, { exitCode: handle.exitCode, signal: handle.signal });
807
+ }
808
+ /** terminal/kill: 立即杀进程组,回 {}。 */
809
+ async handleTerminalKill(id, params) {
810
+ const handle = this.terminals.get(params.terminalId);
811
+ if (!handle) {
812
+ this.currentClient?.respondError(id, RpcErrorCode.INVALID_PARAMS, `Unknown terminal: ${params.terminalId}`);
813
+ return;
814
+ }
815
+ await this.processStopper.stop(handle.proc, { immediate: true });
816
+ this.currentClient?.respond(id, {});
817
+ }
818
+ /** terminal/release: 从 map 删除;进程仍在则一并杀,回 {}。 */
819
+ async handleTerminalRelease(id, params) {
820
+ const handle = this.terminals.get(params.terminalId);
821
+ if (!handle) {
822
+ this.currentClient?.respondError(id, RpcErrorCode.INVALID_PARAMS, `Unknown terminal: ${params.terminalId}`);
823
+ return;
824
+ }
825
+ this.terminals.delete(params.terminalId);
826
+ // stop 对已退出进程是 no-op;仍在跑则立即 SIGTERM+SIGKILL。
827
+ await this.processStopper.stop(handle.proc, { immediate: true });
828
+ this.currentClient?.respond(id, {});
829
+ }
830
+ /** Kill every live terminal (dispose/connection-close 清理钩子)。 */
831
+ async cleanupTerminals() {
832
+ const handles = [...this.terminals.values()];
833
+ this.terminals.clear();
834
+ for (const handle of handles) {
835
+ await this.processStopper.stop(handle.proc, { immediate: true });
836
+ }
837
+ }
530
838
  }
531
839
  //# sourceMappingURL=runner.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lark-remote",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "Feishu (Lark) private-chat bridge to local coding agents — Claude Code / Codex / opencode / pi / kimi, with live-streaming CardKit 2.0 cards",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",