lark-remote 0.2.2 → 0.4.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 (39) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.en.md +8 -5
  3. package/README.md +10 -6
  4. package/dist/binder.d.ts +9 -16
  5. package/dist/binder.js +14 -40
  6. package/dist/bridge/final-usage.js +2 -2
  7. package/dist/bridge/inbound-media.js +7 -5
  8. package/dist/bridge/index.js +75 -75
  9. package/dist/clone.d.ts +139 -0
  10. package/dist/clone.js +415 -0
  11. package/dist/common/fs.js +1 -1
  12. package/dist/config/codex-config.js +79 -24
  13. package/dist/config/index.js +1 -1
  14. package/dist/config/kimi-config.js +16 -5
  15. package/dist/connector/index.d.ts +25 -0
  16. package/dist/connector/index.js +133 -50
  17. package/dist/error-classification.d.ts +7 -0
  18. package/dist/error-classification.js +37 -1
  19. package/dist/index.js +48 -15
  20. package/dist/platform/command.js +1 -1
  21. package/dist/platform/sleep-blocker.d.ts +17 -0
  22. package/dist/platform/sleep-blocker.js +73 -0
  23. package/dist/platform/terminator-win32.js +1 -1
  24. package/dist/restart.d.ts +20 -3
  25. package/dist/restart.js +42 -23
  26. package/dist/router/config/kimi.js +4 -3
  27. package/dist/router/index.d.ts +10 -0
  28. package/dist/router/index.js +44 -6
  29. package/dist/runner/common/acp/base-acp-runner.js +7 -2
  30. package/dist/runner/common/acp/base-translator.d.ts +19 -0
  31. package/dist/runner/common/acp/base-translator.js +82 -0
  32. package/dist/runner/common/acp/protocol-types.d.ts +16 -2
  33. package/dist/runner/common/acp/protocol-types.js +3 -0
  34. package/dist/runner/kimi/acp/translator.js +60 -4
  35. package/dist/session/session-store.d.ts +1 -1
  36. package/dist/session/session-store.js +1 -1
  37. package/dist/workspace/index.d.ts +1 -1
  38. package/dist/workspace/index.js +1 -1
  39. package/package.json +2 -2
@@ -11,6 +11,36 @@ const KIND_TO_TOOL = {
11
11
  search: 'Grep',
12
12
  fetch: 'WebFetch',
13
13
  };
14
+ /**
15
+ * Join the text payloads of a tool_call(/_update) content array
16
+ * (`[{type:'content', content:{type:'text', text}}]` entries). Returns
17
+ * undefined when no text entry exists (diff/terminal-only content).
18
+ */
19
+ function extractToolCallContentText(content) {
20
+ if (!Array.isArray(content))
21
+ return undefined;
22
+ let text = '';
23
+ let found = false;
24
+ for (const entry of content) {
25
+ const e = entry;
26
+ if (e?.type === 'content' && e.content?.type === 'text' && typeof e.content.text === 'string') {
27
+ text += e.content.text;
28
+ found = true;
29
+ }
30
+ }
31
+ return found ? text : undefined;
32
+ }
33
+ /** Best-effort stringify for tracked rawInput objects (never throws). */
34
+ function stringifyUnknown(value) {
35
+ if (typeof value === 'string')
36
+ return value;
37
+ try {
38
+ return JSON.stringify(value) ?? String(value);
39
+ }
40
+ catch {
41
+ return String(value);
42
+ }
43
+ }
14
44
  // =============================================================================
15
45
  // Translator base class
16
46
  // =============================================================================
@@ -25,6 +55,16 @@ export class BaseAcpTranslator {
25
55
  liveCostUsd;
26
56
  /** Current turn id, set by produceTurnStarted. */
27
57
  currentTurnId = '';
58
+ /**
59
+ * Latest streaming-args text per tool call (keyed by wire toolCallId).
60
+ * kimi ACP sends the permission request BEFORE tool.call.started (verified
61
+ * live against kimi 0.42.0, 2026-09-13): the request_permission toolCall
62
+ * carries only title + a truncated summary in content, NO rawInput — the
63
+ * full args only ever reach the client as accumulated text in the
64
+ * tool_call/tool_call_update streaming deltas. Tracking them here lets the
65
+ * approval handler correlate by toolCallId and recover the real command.
66
+ */
67
+ toolCallArgsByCallId = new Map();
28
68
  /**
29
69
  * Handle a notification from the ACP server and return translated events.
30
70
  * Wire envelope: {sessionId, update: {sessionUpdate: '<kind>', ...}} — the
@@ -191,6 +231,13 @@ export class BaseAcpTranslator {
191
231
  // Keep raw string as-is
192
232
  }
193
233
  }
234
+ const argsText = extractToolCallContentText(event.content);
235
+ if (argsText !== undefined) {
236
+ this.trackToolCallArgs(event.toolCallId, argsText);
237
+ }
238
+ else if (event.rawInput !== undefined && event.rawInput !== null) {
239
+ this.trackToolCallArgs(event.toolCallId, stringifyUnknown(event.rawInput));
240
+ }
194
241
  const mapped = event.kind ? KIND_TO_TOOL[event.kind] : undefined;
195
242
  const name = mapped ?? event.title;
196
243
  const summary = mapped && event.title && event.title !== name ? event.title : undefined;
@@ -218,6 +265,22 @@ export class BaseAcpTranslator {
218
265
  handleToolCallUpdate(event, _sessionId) {
219
266
  const isError = event.status === 'failed';
220
267
  const content = this.extractToolResultContent(event);
268
+ // Terminal updates end the args-streaming phase — drop the tracked entry.
269
+ // In-progress delta text is cumulative (events-map.ts
270
+ // toolCallDeltaToSessionUpdate: content.text = accumulator.args), so the
271
+ // latest text replaces the previous one rather than appending.
272
+ if (event.status === 'completed' || event.status === 'failed') {
273
+ this.toolCallArgsByCallId.delete(event.toolCallId);
274
+ }
275
+ else {
276
+ const argsText = extractToolCallContentText(event.content);
277
+ if (argsText !== undefined) {
278
+ this.trackToolCallArgs(event.toolCallId, argsText);
279
+ }
280
+ else if (event.rawInput !== undefined && event.rawInput !== null) {
281
+ this.trackToolCallArgs(event.toolCallId, stringifyUnknown(event.rawInput));
282
+ }
283
+ }
221
284
  return [
222
285
  {
223
286
  type: 'user',
@@ -235,6 +298,25 @@ export class BaseAcpTranslator {
235
298
  },
236
299
  ];
237
300
  }
301
+ /**
302
+ * Latest tracked streaming-args text for a tool call (undefined when never
303
+ * seen or already terminated). Approval handlers correlate
304
+ * request_permission's toolCall.toolCallId against this to recover the real
305
+ * command (see toolCallArgsByCallId).
306
+ */
307
+ getTrackedToolCallArgs(toolCallId) {
308
+ return this.toolCallArgsByCallId.get(toolCallId);
309
+ }
310
+ /** Record latest args text, bounded so a long session cannot grow it forever. */
311
+ trackToolCallArgs(toolCallId, argsText) {
312
+ this.toolCallArgsByCallId.delete(toolCallId);
313
+ this.toolCallArgsByCallId.set(toolCallId, argsText);
314
+ if (this.toolCallArgsByCallId.size > 100) {
315
+ const oldest = this.toolCallArgsByCallId.keys().next().value;
316
+ if (oldest !== undefined)
317
+ this.toolCallArgsByCallId.delete(oldest);
318
+ }
319
+ }
238
320
  /**
239
321
  * tool_result content extraction. Default: rawOutput passthrough (kimi
240
322
  * semantics). opencode overrides — its rawOutput is an OBJECT
@@ -22,6 +22,9 @@
22
22
  * 以 `turn_diff` 快照发出——禁止把 delta 直接当快照或把全文当增量重复追加。
23
23
  * 4. `tool_call` 事件可能**没有 rawInput**(工具 lazy-create,参数在后续
24
24
  * tool_call_update 才补齐);translator 必须归一化为 `{}`,不得因缺失崩溃。
25
+ * 5. `session/request_permission` 的 toolCall **不一定带 rawInput**(kimi 实测:
26
+ * 审批门先于 tool.call.started,只有 title + content 截断摘要);完整命令
27
+ * 需按 toolCallId 关联审批前的流式 args 文本恢复(BaseAcpTranslator 追踪)。
25
28
  */
26
29
  export type { JsonRpcRequest, JsonRpcNotification, JsonRpcSuccessResponse, JsonRpcErrorResponse, } from '../jsonrpc/types.js';
27
30
  /** Standard JSON-RPC error codes. */
@@ -226,9 +229,20 @@ export interface PermissionOption {
226
229
  }
227
230
  export interface RequestPermissionParams {
228
231
  sessionId: string;
232
+ /**
233
+ * Approval target. Divergence by server (2026-09-13 live probes):
234
+ * - kimi 0.42: {toolCallId, title: toolName, content: [truncated summary
235
+ * text]} — NO rawInput; the approval gate fires BEFORE tool.call.started,
236
+ * so the full args only exist in the pre-approval tool_call/_update
237
+ * streaming deltas (correlate by toolCallId, see BaseAcpTranslator).
238
+ * - opencode: {toolCallId, title: full command for bash/shell, rawInput:
239
+ * full input object} — title/rawInput are directly usable.
240
+ */
229
241
  toolCall?: {
230
- title: string;
231
- rawInput: string;
242
+ toolCallId?: string;
243
+ title?: string;
244
+ rawInput?: unknown;
245
+ content?: unknown[];
232
246
  [key: string]: unknown;
233
247
  };
234
248
  options: PermissionOption[];
@@ -22,6 +22,9 @@
22
22
  * 以 `turn_diff` 快照发出——禁止把 delta 直接当快照或把全文当增量重复追加。
23
23
  * 4. `tool_call` 事件可能**没有 rawInput**(工具 lazy-create,参数在后续
24
24
  * tool_call_update 才补齐);translator 必须归一化为 `{}`,不得因缺失崩溃。
25
+ * 5. `session/request_permission` 的 toolCall **不一定带 rawInput**(kimi 实测:
26
+ * 审批门先于 tool.call.started,只有 title + content 截断摘要);完整命令
27
+ * 需按 toolCallId 关联审批前的流式 args 文本恢复(BaseAcpTranslator 追踪)。
25
28
  */
26
29
  /** Standard JSON-RPC error codes. */
27
30
  export const RpcErrorCode = {
@@ -120,13 +120,31 @@ export class KimiAcpTranslator extends BaseAcpTranslator {
120
120
  getLogger().info(`[kimi-acp-translator] permission request without toolCall, auto-responding cancelled (requestId=${requestId})`);
121
121
  return [];
122
122
  }
123
+ // 命令恢复(2026-09-13 对 kimi 0.42.0 实测):审批门在 tool.call.started
124
+ // 之前触发,request_permission 的 toolCall 只有 title("Bash")+ content
125
+ // 里一句截断摘要("Requesting approval to Running: <cmd>…"),无 rawInput;
126
+ // 完整命令只从审批前的 tool_call/tool_call_update 流式 args 累积文本拿到
127
+ // (基类按 toolCallId 追踪)。解析顺序:流式 args JSON 的 command 字段 →
128
+ // title;流式缺失时退回 content 摘要放进 reason(旧版 kimi 不推 delta)。
129
+ const tracked = typeof toolCall.toolCallId === 'string'
130
+ ? this.getTrackedToolCallArgs(toolCall.toolCallId)
131
+ : undefined;
132
+ const argsCommand = tracked ? extractCommandFromArgsText(tracked) : undefined;
133
+ const rawInput = typeof toolCall.rawInput === 'string' && toolCall.rawInput.length > 0
134
+ ? toolCall.rawInput
135
+ : undefined;
136
+ const summary = extractPermissionSummaryText(toolCall.content);
123
137
  const view = {
124
138
  requestId,
125
139
  kind: 'command',
126
- command: params.toolCall?.title ?? undefined,
127
- reason: params.toolCall?.rawInput
128
- ? truncateWithEllipsis(params.toolCall.rawInput, 200)
129
- : undefined,
140
+ command: argsCommand ?? toolCall.title ?? undefined,
141
+ reason: argsCommand
142
+ ? undefined
143
+ : rawInput
144
+ ? truncateWithEllipsis(rawInput, 200)
145
+ : summary
146
+ ? truncateWithEllipsis(summary, 200)
147
+ : undefined,
130
148
  // §P4: 从服务端 options kind 派生——带 approve_always(或 allow_always)
131
149
  // 才提供「本会话总是允许」(acceptForSession);否则与旧行为一致。
132
150
  availableDecisions: deriveAcpAvailableDecisions(params.options ?? []),
@@ -244,4 +262,42 @@ function extractQuestionText(toolCall) {
244
262
  }
245
263
  return toolCall?.title?.trim() || '请选择';
246
264
  }
265
+ /**
266
+ * 从流式累积的 args 文本恢复命令:完整 JSON 且含 string command 字段时取该
267
+ * 字段;否则(非 Bash 工具 / 防御性半截 JSON)返回原文。
268
+ */
269
+ function extractCommandFromArgsText(argsText) {
270
+ try {
271
+ const parsed = JSON.parse(argsText);
272
+ if (parsed && typeof parsed === 'object') {
273
+ const command = parsed.command;
274
+ if (typeof command === 'string' && command.length > 0)
275
+ return command;
276
+ }
277
+ }
278
+ catch {
279
+ // 半截 JSON(审批正常在 args 流式完成后才到,此处纯防御)——用原文。
280
+ }
281
+ return argsText;
282
+ }
283
+ /**
284
+ * request_permission toolCall.content 里的摘要文本(kimi 0.42:
285
+ * "Requesting approval to Running: <cmd>…",约 64 字符截断)。无文本返回
286
+ * undefined。
287
+ */
288
+ function extractPermissionSummaryText(content) {
289
+ if (!Array.isArray(content))
290
+ return undefined;
291
+ let text = '';
292
+ let found = false;
293
+ for (const block of content) {
294
+ const t = block?.content
295
+ ?.text;
296
+ if (typeof t === 'string' && t.length > 0) {
297
+ text += t;
298
+ found = true;
299
+ }
300
+ }
301
+ return found ? text : undefined;
302
+ }
247
303
  //# sourceMappingURL=translator.js.map
@@ -14,7 +14,7 @@ interface SessionEntry {
14
14
  /**
15
15
  * Per-user session store. Persists cwd + per-agent sessions to disk so that
16
16
  * the last workspace AND the last-used sessionId per agent are restored after
17
- * a bridge restart.
17
+ * a lark-remote restart.
18
18
  *
19
19
  * Persistence strategy:
20
20
  * - Persists `cwd`, `sessions` (per-agent sessionId), `previousSessions`
@@ -4,7 +4,7 @@ import { atomicWriteJson } from '../persistence/atomic-write.js';
4
4
  /**
5
5
  * Per-user session store. Persists cwd + per-agent sessions to disk so that
6
6
  * the last workspace AND the last-used sessionId per agent are restored after
7
- * a bridge restart.
7
+ * a lark-remote restart.
8
8
  *
9
9
  * Persistence strategy:
10
10
  * - Persists `cwd`, `sessions` (per-agent sessionId), `previousSessions`
@@ -9,7 +9,7 @@ export interface WorkspaceEntry {
9
9
  *
10
10
  * Atomic write (§9.10): writes a temp file then `fs.rename` so a crash
11
11
  * mid-write cannot leave a truncated JSON file. On load, a corrupt file
12
- * is treated as empty (with a warning) so the bridge can still start.
12
+ * is treated as empty (with a warning) so the lark-remote can still start.
13
13
  *
14
14
  * lastUsedAt 语义(2026-08-19 语义对齐):保存(save)与使用(use/touch)
15
15
  * 都会把时间戳更新为当前时间。save 内部调用 touch,保证「最近使用」排序
@@ -8,7 +8,7 @@ import { getLogger } from '../logger/index.js';
8
8
  *
9
9
  * Atomic write (§9.10): writes a temp file then `fs.rename` so a crash
10
10
  * mid-write cannot leave a truncated JSON file. On load, a corrupt file
11
- * is treated as empty (with a warning) so the bridge can still start.
11
+ * is treated as empty (with a warning) so the lark-remote can still start.
12
12
  *
13
13
  * lastUsedAt 语义(2026-08-19 语义对齐):保存(save)与使用(use/touch)
14
14
  * 都会把时间戳更新为当前时间。save 内部调用 touch,保证「最近使用」排序
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lark-remote",
3
- "version": "0.2.2",
3
+ "version": "0.4.0",
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",
@@ -58,7 +58,7 @@
58
58
  "test:opencode-full": "vitest run --project agent-opencode --project infra-runner --project infra-session",
59
59
  "test:pi-full": "vitest run --project agent-pi --project infra-runner --project infra-session",
60
60
  "test:kimi-full": "vitest run --project agent-kimi --project infra-runner --project infra-session",
61
- "test:live": "vitest run --project live",
61
+ "test:live": "node node_modules/vitest/vitest.mjs run --project live",
62
62
  "prepublishOnly": "npm run typecheck && vitest run && npm run build",
63
63
  "prepack": "node -e \"const fs=require('fs');if(!fs.existsSync('dist/cli.js')){console.error('dist/ missing — run bun run build first');process.exit(1)}\""
64
64
  },