lark-remote 0.1.2 → 0.1.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 (50) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.en.md +2 -5
  3. package/README.md +1 -4
  4. package/dist/bridge/index.d.ts +5 -1
  5. package/dist/bridge/index.js +12 -2
  6. package/dist/card/card-budget.js +1 -2
  7. package/dist/card/run-renderer.js +1 -0
  8. package/dist/card/run-state.d.ts +4 -0
  9. package/dist/config/codex-config.d.ts +3 -5
  10. package/dist/config/codex-config.js +7 -7
  11. package/dist/config/dir.d.ts +1 -0
  12. package/dist/config/dir.js +4 -0
  13. package/dist/config/index.d.ts +0 -10
  14. package/dist/config/index.js +0 -11
  15. package/dist/connector/index.js +28 -0
  16. package/dist/index.js +74 -21
  17. package/dist/router/config/codex.js +8 -12
  18. package/dist/router/config/index.d.ts +10 -0
  19. package/dist/router/config/index.js +17 -0
  20. package/dist/router/index.d.ts +40 -1
  21. package/dist/router/index.js +514 -48
  22. package/dist/router/utils.d.ts +3 -0
  23. package/dist/router/utils.js +15 -3
  24. package/dist/runner/claude/runner.d.ts +0 -1
  25. package/dist/runner/claude/runner.js +1 -1
  26. package/dist/runner/codex/runner.d.ts +0 -2
  27. package/dist/runner/codex/runner.js +1 -1
  28. package/dist/runner/common/spawning-runner.d.ts +0 -1
  29. package/dist/runner/common/spawning-runner.js +2 -4
  30. package/dist/runner/kimi/runner.d.ts +0 -1
  31. package/dist/runner/kimi/runner.js +1 -1
  32. package/dist/runner/opencode/runner.d.ts +0 -2
  33. package/dist/runner/opencode/runner.js +1 -1
  34. package/dist/runner/pi/runner.d.ts +0 -1
  35. package/dist/runner/pi/runner.js +1 -1
  36. package/dist/runner/probe.d.ts +45 -0
  37. package/dist/runner/probe.js +87 -0
  38. package/dist/runner/types.d.ts +3 -0
  39. package/dist/session/claude/sessions.d.ts +1 -1
  40. package/dist/session/claude/sessions.js +77 -31
  41. package/dist/session/codex/rollout-reader.js +9 -0
  42. package/dist/session/kimi/sessions.d.ts +3 -1
  43. package/dist/session/kimi/sessions.js +9 -3
  44. package/dist/session/opencode/sessions.d.ts +0 -2
  45. package/dist/session/opencode/sessions.js +1 -1
  46. package/dist/startup-contact.d.ts +4 -2
  47. package/dist/startup-contact.js +7 -4
  48. package/dist/workspace/index.d.ts +19 -1
  49. package/dist/workspace/index.js +51 -5
  50. package/package.json +39 -4
package/CHANGELOG.md CHANGED
@@ -5,6 +5,67 @@ 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.4] - 2026-08-13
9
+
10
+ ### 新增
11
+
12
+ - 工作区列表支持 `lastUsedAt` 排序,最近使用的 workspace 排在前面;排序切换 UI 移至卡片上方,明确显示当前模式与切换目标
13
+ - `/config` 卡片:未安装的 Coding Agent 沉底排序,已安装 agent 保持注册顺序;探测结果过期(`undefined`)时保持原位不误沉底
14
+
15
+ ### 修复
16
+
17
+ - `/ws`、`/order` 卡片分页降为 15 条/页,修复飞书 ErrCode 11310 元素超限
18
+ - workspace 排序相关 P2/P3 review findings 修复
19
+
20
+ ## [0.1.3] - 2026-08-12
21
+
22
+ ### 新增
23
+
24
+ - Agent 可用性探测:启动时自动检测本机已安装/可用的 Agent CLI(`which` 探测),不可用的 Agent 会在 `/config` 卡片中标注;探测失败不阻塞启动,打开 `/config` 时会重试
25
+ - 首次运行引导:PIN 绑定成功后自动设置默认工作目录,并发送欢迎卡片 + 帮助卡片
26
+ - `message.patch` 业务码可观测性探针
27
+ - Context 统计支持显示占比:codex 上报 `context window` 上限时,卡片按百分比展示上下文用量
28
+
29
+ ### 修复
30
+
31
+ - `displayTitle`/`summary` 跳过 task-notification 注入
32
+ - 修复 `scanClaudeSessions` 对迁移到其他目录的会话的遗漏(S2)
33
+ - 修复 probe 生命周期、工厂回退、卡片回调等评审发现的问题(F1-F8)
34
+ - 清理引用已删除文档(lessons-redlines、resume-card-budget-failure-analysis)导致的失效链接
35
+
36
+ ### 变更
37
+
38
+ - 提取 `createAgentRegistries` 工厂,统一各 Agent 注册逻辑
39
+ - 移除 Agent 二进制路径配置,CLI 名称改为硬编码
40
+ - 移除 `pnpm-lock.yaml`,统一使用 `bun.lock` 管理依赖
41
+ - 新增 husky lint-staged pre-commit / pre-push 钩子(提交前 lint + typecheck)
42
+ - 启用 npm provenance(构建产物可溯源)
43
+ - 移除 watchdog 相关用户文档及 `open-source-playbook` 等临时文档
44
+
45
+ ### 测试
46
+
47
+ - 合并 stub 工厂、净化 fixtures、移除冗余测试
48
+ - 新增 P1-15 configContainer 锚点测试(含 codex 负向断言)
49
+
50
+ ## [0.1.2] - 2026-08-10
51
+
52
+ ### 修复
53
+
54
+ - claude / opencode / kimi 的 Agent 工厂改为从 `configContainer` 读取配置,避免使用过期闭包导致配置不生效(P1-15)
55
+ - codex / kimi 的 `readSessionContent` 增加 cwd 校验,防止越界读取会话
56
+ - `listClaudeSessions` / `isClaudeSessionActive` 支持跨目录回退,处理会话迁移后的定位
57
+ - 修复 `handleQueueInput` 竞态:替换任务在 await 之前注册
58
+ - 完成卡片 `resume.use` 携带 agent 字段,确保会话 reader 路由正确
59
+
60
+ ### 维护
61
+
62
+ - 升级 `@vitest/coverage-v8` 至 4.1.10
63
+ - 移除 CLI 子进程 smoke test,清理测试套件
64
+
65
+ ## [0.1.1] - 2026-08-09
66
+
67
+ 版本号 0.1.0 → 0.1.1,无用户可见功能变更。
68
+
8
69
  ## [0.1.0] - 2026-08-06
9
70
 
10
71
  Initial release.
@@ -50,3 +111,7 @@ Initial release.
50
111
  - Live Feishu API tests gated behind `FEISHU_LIVE_TEST=1` (external contributors can run full test suite without credentials)
51
112
 
52
113
  [0.1.0]: https://github.com/bungabungawoda/lark-remote/releases/tag/v0.1.0
114
+ [0.1.1]: https://github.com/bungabungawoda/lark-remote/releases/tag/v0.1.1
115
+ [0.1.2]: https://github.com/bungabungawoda/lark-remote/releases/tag/v0.1.2
116
+ [0.1.3]: https://github.com/bungabungawoda/lark-remote/releases/tag/v0.1.3
117
+ [0.1.4]: https://github.com/bungabungawoda/lark-remote/releases/tag/v0.1.4
package/README.en.md CHANGED
@@ -64,7 +64,6 @@ feishu:
64
64
  defaultAgent: claude
65
65
 
66
66
  claude:
67
- binary: claude
68
67
  model: claude-opus-4-8
69
68
  effort: medium # low | medium | high | xhigh | max
70
69
  # permissionMode is hardcoded to bypassPermissions (inside the runner)
@@ -79,7 +78,7 @@ logging:
79
78
  level: info # debug | info | warn | error
80
79
 
81
80
  idle:
82
- watchdogMinutes: 15 # 0 disables the idle watchdog
81
+ watchdogMinutes: 15 # 0 disables the idle timeout auto-stop
83
82
  ```
84
83
 
85
84
  Override the config directory with `--config-dir <path>`; override the Claude settings path with `--settings <path>` or the `CLAUDE_SETTINGS_PATH` environment variable.
@@ -98,8 +97,6 @@ lark-remote --settings ~/.claude/settings.json # specify Claude settings file
98
97
 
99
98
  The bridge prints nothing to the terminal after startup; logs go to `~/.lark-remote/logs/YYYY-MM-DD/lark-remote-<pid>.log` (rotated daily, one subdirectory per day). Only one `lark-remote` instance is allowed per `configDir` — a duplicate start fails with the existing pid. After connecting to Feishu, a startup notification (with start time and pid) is sent to the most recent private chat.
100
99
 
101
- For automatic process restart, set up your own supervisor such as systemd, launchd, or pm2.
102
-
103
100
  Each agent run creates one CardKit 2.0 card that updates in place with thinking, body text, and tool summaries in real time.
104
101
  Timestamps are read from the JSONL and shown in local time as `YYYY-MM-DD HH:mm` for thinking, body, tool call/result, and session history events.
105
102
  The card clearly shows completion, error, interruption, or idle timeout at the end; the in-card "⏹ Stop" button is equivalent to `/stop`.
@@ -152,7 +149,7 @@ Full docs under [`docs/`](docs/):
152
149
  - Single-card streaming architecture and Feishu acceptance: [`docs/en/architecture/streaming-card.md`](docs/en/architecture/streaming-card.md)
153
150
  - Template for adding a new agent: [`docs/en/guides/add-new-agent.md`](docs/en/guides/add-new-agent.md)
154
151
  - Codex config card guide: [`docs/en/guides/codex-config.md`](docs/en/guides/codex-config.md)
155
- - Feishu CardKit 2.0 component reference: [Feishu Open Platform official docs](https://open.feishu.cn/document/feishu-cards/card-json-v2-components/component-json-v2-overview)
152
+ - Feishu CardKit 2.0 component reference: [Official Feishu docs](https://open.feishu.cn/document/feishu-cards/card-json-v2-components/component-json-v2-overview)
156
153
  - AI collaboration rules and red lines: [`CLAUDE.md`](CLAUDE.md)
157
154
 
158
155
  > This package is a CLI tool only — no programmatic API is exposed.
package/README.md CHANGED
@@ -64,7 +64,6 @@ feishu:
64
64
  defaultAgent: claude
65
65
 
66
66
  claude:
67
- binary: claude
68
67
  model: claude-opus-4-8
69
68
  effort: medium # low | medium | high | xhigh | max
70
69
  # permissionMode 硬编码为 bypassPermissions(runner 内部)
@@ -79,7 +78,7 @@ logging:
79
78
  level: info # debug | info | warn | error
80
79
 
81
80
  idle:
82
- watchdogMinutes: 15 # 0 关闭空闲看门狗
81
+ watchdogMinutes: 15 # 0 关闭空闲超时自动停止
83
82
  ```
84
83
 
85
84
  配置目录可用 `--config-dir <path>` 覆盖;Claude settings 路径可用 `--settings <path>` 或 `CLAUDE_SETTINGS_PATH` 环境变量覆盖。
@@ -98,8 +97,6 @@ lark-remote --settings ~/.claude/settings.json # 指定 Claude 配置文件
98
97
 
99
98
  bridge 启动后不在终端输出,运行日志写入 `~/.lark-remote/logs/YYYY-MM-DD/lark-remote-<pid>.log`(按日期轮转,每天一个子目录)。同一 `configDir` 只允许一个 `lark-remote` 实例,重复启动会直接报错并提示已有 pid。连接飞书成功后会向最近私聊用户发送启动通知,包含启动时间和进程号。
100
99
 
101
- 如需进程自动重启,可自行配置 systemd / launchd / pm2 等守护方案。
102
-
103
100
  每次 agent 运行会创建一张 CardKit 2.0 卡片,并在原地实时更新 thinking、正文和工具摘要。
104
101
  卡片会读取 JSONL 的 `timestamp`,以本地时间 `YYYY-MM-DD HH:mm` 显示 thinking、正文、tool call/result 和会话历史事件。
105
102
  结束时卡片明确显示完成、出错、中断或空闲超时;运行中的「⏹ 终止」按钮与 `/stop` 等价。
@@ -57,7 +57,11 @@ interface BridgeDeps {
57
57
  /** Workspace store for fallback when sessionStore has no cwd */
58
58
  workspaceStore?: {
59
59
  get(name: string): string | undefined;
60
- list(): [string, string][];
60
+ list(): Array<{
61
+ name: string;
62
+ path: string;
63
+ lastUsedAt: number;
64
+ }>;
61
65
  };
62
66
  /** Override the idle watchdog timeout (default 15 min). Tests use a small value. */
63
67
  idleTimeoutMs?: number;
@@ -542,7 +542,9 @@ export class Bridge {
542
542
  if (!cwd && this.workspaceStore) {
543
543
  const workspaces = this.workspaceStore.list();
544
544
  if (workspaces.length > 0) {
545
- cwd = workspaces[0][1];
545
+ // NOTE: fallback uses insertion order, not sort preference — by design
546
+ // (workspace-sorting.md §9: cwd fallback stays insertion-order for now)
547
+ cwd = workspaces[0].path;
546
548
  getLogger().info(`[bridge] resolveCwd fallback cwd=${cwd} (first saved workspace)`);
547
549
  }
548
550
  }
@@ -652,6 +654,7 @@ export class Bridge {
652
654
  // Final usage read from jsonl after the run completes. live stream-json does
653
655
  // not emit compact_boundary, so the live contextLength is unreliable.
654
656
  let finalContextLength;
657
+ let finalContextLimit;
655
658
  let finalCompactCount;
656
659
  let finalCacheReadTokens;
657
660
  let finalCacheCreationTokens;
@@ -825,6 +828,7 @@ export class Bridge {
825
828
  ? undefined
826
829
  : this.resolveFinalUsage(finalSessionId, cwd, agentKind);
827
830
  finalContextLength = finalUsage?.contextLength ?? contextLength;
831
+ finalContextLimit = finalUsage?.contextLimit;
828
832
  finalCompactCount = finalUsage?.compactCount;
829
833
  finalCumulativeTotalTokens = finalUsage?.cumulativeTotalTokens;
830
834
  finalCumulativeInputTokens = finalUsage?.cumulativeInputTokens;
@@ -858,7 +862,7 @@ export class Bridge {
858
862
  finalCacheCreationTokens = finalUsage?.cacheCreationTokens ?? finalCacheCreationTokens;
859
863
  finalTotalTokens = finalUsage?.totalTokens ?? finalTotalTokens;
860
864
  }
861
- getLogger().info(`[bridge] final usage runId=${runId} contextLength=${finalContextLength} compactCount=${finalCompactCount ?? 'undefined'} cacheRead=${finalCacheReadTokens ?? 'undefined'} cacheCreate=${finalCacheCreationTokens ?? 'undefined'}`);
865
+ getLogger().info(`[bridge] final usage runId=${runId} contextLength=${finalContextLength} contextLimit=${finalContextLimit ?? 'undefined'} compactCount=${finalCompactCount ?? 'undefined'} cacheRead=${finalCacheReadTokens ?? 'undefined'} cacheCreate=${finalCacheCreationTokens ?? 'undefined'}`);
862
866
  // for-await 自然结束 = stream 关闭 = CLI 进程退出。
863
867
  // 进程退出(for-await end)= 唯一终态触发源。
864
868
  // 1) finalizing(result 已收到)-> 转 done/error + usage meta
@@ -871,6 +875,7 @@ export class Bridge {
871
875
  resultSubtype: finalState.resultSubtype,
872
876
  errorMsg: finalState.errorMsg,
873
877
  contextLength: finalContextLength,
878
+ contextLimit: finalContextLimit,
874
879
  compactCount: finalCompactCount,
875
880
  cacheReadTokens: finalCacheReadTokens,
876
881
  cacheCreationTokens: finalCacheCreationTokens,
@@ -887,6 +892,7 @@ export class Bridge {
887
892
  else if (finalState.terminal === 'running') {
888
893
  await cardSession.finish('error', {
889
894
  contextLength: finalContextLength,
895
+ contextLimit: finalContextLimit,
890
896
  compactCount: finalCompactCount,
891
897
  cacheReadTokens: finalCacheReadTokens,
892
898
  cacheCreationTokens: finalCacheCreationTokens,
@@ -906,6 +912,7 @@ export class Bridge {
906
912
  // 仅补充 usage meta(finalizing 期间被 /stop 或 idle 超时也展示 token 统计)
907
913
  await cardSession.finish(finalState.terminal, {
908
914
  contextLength: finalContextLength,
915
+ contextLimit: finalContextLimit,
909
916
  compactCount: finalCompactCount,
910
917
  cacheReadTokens: finalCacheReadTokens,
911
918
  cacheCreationTokens: finalCacheCreationTokens,
@@ -937,6 +944,7 @@ export class Bridge {
937
944
  if (catchTerminal === 'running' || catchTerminal === 'finalizing') {
938
945
  await cardSession.finish('error', {
939
946
  contextLength: finalContextLength,
947
+ contextLimit: finalContextLimit,
940
948
  compactCount: finalCompactCount,
941
949
  cacheReadTokens: finalCacheReadTokens,
942
950
  cacheCreationTokens: finalCacheCreationTokens,
@@ -954,6 +962,7 @@ export class Bridge {
954
962
  if (sawResult) {
955
963
  await cardSession.finish(catchTerminal, {
956
964
  contextLength: finalContextLength,
965
+ contextLimit: finalContextLimit,
957
966
  compactCount: finalCompactCount,
958
967
  cacheReadTokens: finalCacheReadTokens,
959
968
  cacheCreationTokens: finalCacheCreationTokens,
@@ -1048,6 +1057,7 @@ export class Bridge {
1048
1057
  return content.usage
1049
1058
  ? {
1050
1059
  contextLength: content.usage.contextLength,
1060
+ contextLimit: content.usage.contextLimit,
1051
1061
  compactCount: content.usage.compactCount,
1052
1062
  cacheReadTokens: content.usage.cacheReadTokens,
1053
1063
  cacheCreationTokens: content.usage.cacheCreationTokens,
@@ -46,8 +46,7 @@ export function enforceCardBudget(card, opts = {}) {
46
46
  * when table truncation alone doesn't bring the card under the byte limit.
47
47
  *
48
48
  * 裁剪判定是**结构化**的:所有 collapsible_panel 都是待裁剪对象,不看面板
49
- * 标题文本。历史教训(2026-08-08,详见
50
- * docs/zh/architecture/resume-card-budget-failure-analysis.md):旧实现用
49
+ * 标题文本。历史教训(2026-08-08):旧实现用
51
50
  * emoji 标题模式(🤖👤💭🔧🟢🔴)识别「会话事件面板」,但真实
52
51
  * sessionEventPanel 产出的标题是裸英文 type 名(emoji 在事件正文而非标题),
53
52
  * 导致裁剪阶段对真实 resume 卡零命中,任何 >28KB 的卡片无裁剪直通极端降级,
@@ -776,6 +776,7 @@ function buildSummaryContent(state) {
776
776
  const empty = !hasContent ? '\n\n(未返回内容)' : '';
777
777
  const usageStatsStr = formatUsageStats({
778
778
  contextLength: state.contextLength,
779
+ contextLimit: state.contextLimit,
779
780
  compactCount: state.compactCount,
780
781
  cacheReadTokens: state.cacheReadTokens,
781
782
  cacheCreationTokens: state.cacheCreationTokens,
@@ -53,6 +53,8 @@ export interface RunState {
53
53
  sessionId?: string;
54
54
  resultSubtype?: 'success' | 'error';
55
55
  contextLength?: number;
56
+ /** 当前模型 context window 上限(codex jsonl 提供);卡片据此渲染百分比。 */
57
+ contextLimit?: number;
56
58
  errorMsg?: string;
57
59
  idleTimeoutMinutes?: number;
58
60
  compactCount?: number;
@@ -79,6 +81,8 @@ export interface RunState {
79
81
  export interface FinishMeta {
80
82
  resultSubtype?: 'success' | 'error';
81
83
  contextLength?: number;
84
+ /** 当前模型 context window 上限;透传到 RunState 供 done 卡片显示百分比。 */
85
+ contextLimit?: number;
82
86
  errorMsg?: string;
83
87
  idleTimeoutMinutes?: number;
84
88
  compactCount?: number;
@@ -76,7 +76,7 @@ export declare function parseCodexModelsOutput(stdout: string): BundledModelInfo
76
76
  * (P3-4/P3-15/P3-5);成功结果 TTL 1h,失败/空结果短 TTL 负缓存;stat 异常时
77
77
  * 指纹为空(P1-1/P3-10)。
78
78
  */
79
- export declare function getCodexCatalogModels(binary: string, codexHome?: string): BundledModelInfo[];
79
+ export declare function getCodexCatalogModels(codexHome?: string): BundledModelInfo[];
80
80
  /**
81
81
  * Test utility: clear the catalog cache. Tests that exercise the bundled
82
82
  * command path (`--bundled`) directly use `invalidateCodexBundledTestCache`
@@ -88,18 +88,16 @@ export declare function invalidateCodexBundledCache(): void;
88
88
  * @param codexHome 可选 codex 配置目录覆盖(与 loadCodexConfig 的 codexHome 对齐,
89
89
  * 与 loadCodexConfig 的 codexHome 对齐);省略时走 $CODEX_HOME → ~/.codex。
90
90
  */
91
- export declare function getReasoningEffortOptions(model: string, binary?: string, codexHome?: string): readonly string[];
91
+ export declare function getReasoningEffortOptions(model: string, codexHome?: string): readonly string[];
92
92
  /**
93
93
  * Get default reasoning effort for a model.
94
94
  * 未声明/未知模型 → undefined(codex 语义:ModelPreset.default_reasoning_effort
95
95
  * unwrap_or(ReasoningEffort::None),即不传 effort;P3-12)。
96
96
  */
97
- export declare function getDefaultReasoningEffort(model: string, binary?: string, codexHome?: string): string | undefined;
97
+ export declare function getDefaultReasoningEffort(model: string, codexHome?: string): string | undefined;
98
98
  interface LoadCodexConfigOpts {
99
99
  /** Explicit codex home override. Default: $CODEX_HOME -> ~/.codex */
100
100
  codexHome?: string;
101
- /** codex binary path, for `debug models --bundled`. Default: 'codex' */
102
- binary?: string;
103
101
  }
104
102
  /**
105
103
  * Read codex config.toml, extract provider and model info
@@ -154,7 +154,8 @@ export function parseCodexModelsOutput(stdout) {
154
154
  * (P3-4/P3-15/P3-5);成功结果 TTL 1h,失败/空结果短 TTL 负缓存;stat 异常时
155
155
  * 指纹为空(P1-1/P3-10)。
156
156
  */
157
- export function getCodexCatalogModels(binary, codexHome) {
157
+ export function getCodexCatalogModels(codexHome) {
158
+ const binary = 'codex';
158
159
  const home = resolveCodexHome(codexHome);
159
160
  const catalogPath = readModelCatalogJsonPath(home);
160
161
  const bundled = catalogPath === undefined || catalogPath === TOML_PARSE_FAILED_SENTINEL;
@@ -217,8 +218,8 @@ export function invalidateCodexBundledCache() {
217
218
  * @param codexHome 可选 codex 配置目录覆盖(与 loadCodexConfig 的 codexHome 对齐,
218
219
  * 与 loadCodexConfig 的 codexHome 对齐);省略时走 $CODEX_HOME → ~/.codex。
219
220
  */
220
- export function getReasoningEffortOptions(model, binary = 'codex', codexHome) {
221
- const models = getCodexCatalogModels(binary, codexHome);
221
+ export function getReasoningEffortOptions(model, codexHome) {
222
+ const models = getCodexCatalogModels(codexHome);
222
223
  const found = models.find((m) => m.slug === model);
223
224
  // 未命中(含未知模型,codex fallback 元数据 supported 为空)→ [],不虚构档位(P1)
224
225
  return found?.supportedReasoningLevels ?? [];
@@ -228,8 +229,8 @@ export function getReasoningEffortOptions(model, binary = 'codex', codexHome) {
228
229
  * 未声明/未知模型 → undefined(codex 语义:ModelPreset.default_reasoning_effort
229
230
  * unwrap_or(ReasoningEffort::None),即不传 effort;P3-12)。
230
231
  */
231
- export function getDefaultReasoningEffort(model, binary = 'codex', codexHome) {
232
- const models = getCodexCatalogModels(binary, codexHome);
232
+ export function getDefaultReasoningEffort(model, codexHome) {
233
+ const models = getCodexCatalogModels(codexHome);
233
234
  return models.find((m) => m.slug === model)?.defaultReasoningLevel;
234
235
  }
235
236
  /**
@@ -249,13 +250,12 @@ function extractCustomProvider(providers) {
249
250
  * - anthropic: bundled models with 'claude-*' prefix
250
251
  */
251
252
  export function loadCodexConfig(opts = {}) {
252
- const binary = opts.binary ?? 'codex';
253
253
  const codexHome = resolveCodexHome(opts.codexHome);
254
254
  const configFile = path.join(codexHome, 'config.toml');
255
255
  const catalogMode = isCodexCatalogMode(codexHome);
256
256
  // 统一目录来源:catalog 模式 = 活动目录(model_catalog_json 整体替换内置目录);
257
257
  // 否则 = bundled(与原行为一致)
258
- const catalogSlugs = getCodexCatalogModels(binary, codexHome).map((m) => m.slug);
258
+ const catalogSlugs = getCodexCatalogModels(codexHome).map((m) => m.slug);
259
259
  const allModels = catalogSlugs.length > 0 ? catalogSlugs : FALLBACK_MODELS;
260
260
  const anthropicModels = allModels.filter((m) => m.startsWith('claude-'));
261
261
  if (!fs.existsSync(configFile)) {
@@ -1,6 +1,7 @@
1
1
  interface CliArgs {
2
2
  configDir?: string;
3
3
  settings?: string;
4
+ dev?: boolean;
4
5
  help?: boolean;
5
6
  version?: boolean;
6
7
  }
@@ -49,6 +49,9 @@ export function parseCliArgs(args = process.argv.slice(2)) {
49
49
  result.settings = args[++i];
50
50
  }
51
51
  }
52
+ else if (arg === '--dev') {
53
+ result.dev = true;
54
+ }
52
55
  }
53
56
  return result;
54
57
  }
@@ -73,6 +76,7 @@ export function printHelp() {
73
76
  'Options:',
74
77
  ' --config-dir <path> 自定义配置目录(默认 ~/.lark-remote,可用于同机多实例)',
75
78
  ' --settings <path> 指定 Claude 配置文件路径',
79
+ ' --dev 开发模式:标记从源码 bun src/index.ts 启动(看门狗据此选择拉起方式)',
76
80
  ' -h, --help 显示本帮助信息',
77
81
  ' -v, --version 显示版本号',
78
82
  ];
@@ -15,7 +15,6 @@ export declare const MODEL_ID_TO_ALIAS: Record<string, string>;
15
15
  /** alias → model ID 反向映射。 */
16
16
  export declare const MODEL_ALIAS_TO_ID: Record<string, string>;
17
17
  declare const ClaudeConfigSchema: z.ZodObject<{
18
- binary: z.ZodDefault<z.ZodString>;
19
18
  model: z.ZodDefault<z.ZodString>;
20
19
  effort: z.ZodDefault<z.ZodEnum<{
21
20
  medium: "medium";
@@ -28,7 +27,6 @@ declare const ClaudeConfigSchema: z.ZodObject<{
28
27
  }, z.core.$strip>;
29
28
  /** Codex-specific configuration. */
30
29
  export declare const CodexConfigSchema: z.ZodObject<{
31
- binary: z.ZodDefault<z.ZodString>;
32
30
  model: z.ZodOptional<z.ZodString>;
33
31
  modelProvider: z.ZodOptional<z.ZodString>;
34
32
  reasoningEffort: z.ZodOptional<z.ZodString>;
@@ -36,13 +34,11 @@ export declare const CodexConfigSchema: z.ZodObject<{
36
34
  }, z.core.$strip>;
37
35
  /** OpenCode-specific configuration (run mode: opencode run --format json --auto). */
38
36
  declare const OpencodeConfigSchema: z.ZodObject<{
39
- binary: z.ZodDefault<z.ZodString>;
40
37
  providerID: z.ZodDefault<z.ZodString>;
41
38
  modelID: z.ZodDefault<z.ZodString>;
42
39
  }, z.core.$strip>;
43
40
  /** Pi-specific configuration. pi is a spawn-per-message CLI like Claude. */
44
41
  declare const PiConfigSchema: z.ZodObject<{
45
- binary: z.ZodDefault<z.ZodString>;
46
42
  provider: z.ZodDefault<z.ZodString>;
47
43
  model: z.ZodDefault<z.ZodString>;
48
44
  thinking: z.ZodDefault<z.ZodEnum<{
@@ -57,7 +53,6 @@ declare const PiConfigSchema: z.ZodObject<{
57
53
  }, z.core.$strip>;
58
54
  /** Kimi-specific configuration. kimi is a spawn-per-message CLI. */
59
55
  declare const KimiConfigSchema: z.ZodObject<{
60
- binary: z.ZodDefault<z.ZodString>;
61
56
  model: z.ZodDefault<z.ZodString>;
62
57
  thinkingEffort: z.ZodDefault<z.ZodEnum<{
63
58
  max: "max";
@@ -71,7 +66,6 @@ export declare const AppConfigSchema: z.ZodObject<{
71
66
  appSecret: z.ZodString;
72
67
  }, z.core.$strip>;
73
68
  claude: z.ZodDefault<z.ZodObject<{
74
- binary: z.ZodDefault<z.ZodString>;
75
69
  model: z.ZodDefault<z.ZodString>;
76
70
  effort: z.ZodDefault<z.ZodEnum<{
77
71
  medium: "medium";
@@ -84,19 +78,16 @@ export declare const AppConfigSchema: z.ZodObject<{
84
78
  }, z.core.$strip>>;
85
79
  agents: z.ZodOptional<z.ZodObject<{
86
80
  codex: z.ZodOptional<z.ZodObject<{
87
- binary: z.ZodDefault<z.ZodString>;
88
81
  model: z.ZodOptional<z.ZodString>;
89
82
  modelProvider: z.ZodOptional<z.ZodString>;
90
83
  reasoningEffort: z.ZodOptional<z.ZodString>;
91
84
  stopGraceMs: z.ZodDefault<z.ZodNumber>;
92
85
  }, z.core.$strip>>;
93
86
  opencode: z.ZodOptional<z.ZodObject<{
94
- binary: z.ZodDefault<z.ZodString>;
95
87
  providerID: z.ZodDefault<z.ZodString>;
96
88
  modelID: z.ZodDefault<z.ZodString>;
97
89
  }, z.core.$strip>>;
98
90
  pi: z.ZodOptional<z.ZodObject<{
99
- binary: z.ZodDefault<z.ZodString>;
100
91
  provider: z.ZodDefault<z.ZodString>;
101
92
  model: z.ZodDefault<z.ZodString>;
102
93
  thinking: z.ZodDefault<z.ZodEnum<{
@@ -110,7 +101,6 @@ export declare const AppConfigSchema: z.ZodObject<{
110
101
  tools: z.ZodDefault<z.ZodString>;
111
102
  }, z.core.$strip>>;
112
103
  kimi: z.ZodOptional<z.ZodObject<{
113
- binary: z.ZodDefault<z.ZodString>;
114
104
  model: z.ZodDefault<z.ZodString>;
115
105
  thinkingEffort: z.ZodDefault<z.ZodEnum<{
116
106
  max: "max";
@@ -13,7 +13,6 @@ export const CLAUDE_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max'];
13
13
  export const PI_THINKING_LEVELS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh'];
14
14
  /** 默认值常量:schema 与 template 共享,避免双源不一致(G25 Magic Numbers)。 */
15
15
  const DEFAULTS = {
16
- CLAUDE_BINARY: 'claude',
17
16
  CLAUDE_MODEL: 'claude-opus-4-8',
18
17
  STOP_GRACE_MS: 5000,
19
18
  IDLE_WATCHDOG_MINUTES: 15,
@@ -43,7 +42,6 @@ const FeishuConfigSchema = z.object({
43
42
  appSecret: z.string().min(1),
44
43
  });
45
44
  const ClaudeConfigSchema = z.object({
46
- binary: z.string().default(DEFAULTS.CLAUDE_BINARY),
47
45
  model: z.string().default(DEFAULTS.CLAUDE_MODEL),
48
46
  effort: z.enum(CLAUDE_EFFORTS).default('medium'),
49
47
  // permissionMode 硬编码为 bypassPermissions(runner 内部),不通过 config 配置
@@ -51,8 +49,6 @@ const ClaudeConfigSchema = z.object({
51
49
  });
52
50
  /** Codex-specific configuration. */
53
51
  export const CodexConfigSchema = z.object({
54
- /** Path to codex binary. Default: 'codex' (in PATH). */
55
- binary: z.string().default('codex'),
56
52
  /** model to use. Undefined → codex reads from its config.toml. */
57
53
  model: z.string().optional(),
58
54
  /** model provider, e.g. 'volcengine-coding-plan'. Undefined → codex reads from its config.toml. */
@@ -70,8 +66,6 @@ export const CodexConfigSchema = z.object({
70
66
  });
71
67
  /** OpenCode-specific configuration (run mode: opencode run --format json --auto). */
72
68
  const OpencodeConfigSchema = z.object({
73
- /** Path to opencode binary. Default: 'opencode' (in PATH). */
74
- binary: z.string().default('opencode'),
75
69
  /** Provider ID for the LLM backend (e.g. 'anthropic'). */
76
70
  providerID: z.string().default('anthropic'),
77
71
  /** Model ID for the LLM backend (e.g. 'claude-sonnet-4-20250514'). */
@@ -79,8 +73,6 @@ const OpencodeConfigSchema = z.object({
79
73
  });
80
74
  /** Pi-specific configuration. pi is a spawn-per-message CLI like Claude. */
81
75
  const PiConfigSchema = z.object({
82
- /** Path to pi binary. Default: 'pi' (in PATH). */
83
- binary: z.string().default('pi'),
84
76
  /** LLM provider, e.g. 'Volcano', 'anthropic', 'openai'. */
85
77
  provider: z.string().default('Volcano'),
86
78
  /** Model ID or alias, e.g. 'glm-5.2'. */
@@ -92,8 +84,6 @@ const PiConfigSchema = z.object({
92
84
  });
93
85
  /** Kimi-specific configuration. kimi is a spawn-per-message CLI. */
94
86
  const KimiConfigSchema = z.object({
95
- /** Path to kimi binary. Default: 'kimi' (in PATH). */
96
- binary: z.string().default('kimi'),
97
87
  /** Model ID or alias, e.g. 'kimi-code/k3'. */
98
88
  model: z.string().default('kimi-code/k3'),
99
89
  /** Thinking effort: 'on', 'max'. */
@@ -169,7 +159,6 @@ const TEMPLATE = `feishu:
169
159
  appSecret: ""
170
160
 
171
161
  claude:
172
- binary: ${DEFAULTS.CLAUDE_BINARY}
173
162
  model: ${DEFAULTS.CLAUDE_MODEL}
174
163
  effort: medium
175
164
  stopGraceMs: ${DEFAULTS.STOP_GRACE_MS}
@@ -53,6 +53,14 @@ function shouldRetrySendError(err) {
53
53
  return true;
54
54
  return false;
55
55
  }
56
+ /**
57
+ * Safely extract the im.v1.message.patch service from a LarkChannel.
58
+ * Returns undefined when the channel mock omits rawClient (unit tests).
59
+ */
60
+ function tryGetPatchService(channel) {
61
+ const c = channel.rawClient;
62
+ return c?.im?.v1?.message;
63
+ }
56
64
  export class FeishuConnector {
57
65
  channel;
58
66
  onMessage;
@@ -81,6 +89,26 @@ export class FeishuConnector {
81
89
  },
82
90
  },
83
91
  });
92
+ // 观测探针(2026-08-11 run 卡定格事故):飞书业务码错误以 HTTP 200 + {code!=0}
93
+ // 返回时,lark SDK 正常 resolve、@larksuite/channel 的 patchCard 丢弃返回值,
94
+ // 导致终态卡 patch 被业务层拒绝时全链路无日志无兜底。这里只观测不改行为——
95
+ // 不 throw、不重试,返回值原样透传。
96
+ // 详见 .adversarial-tdd/prompt-patchcard-business-code-observability.md
97
+ // Guard: unit-test mocks may omit rawClient; skip probe installation in that case.
98
+ const messageService = tryGetPatchService(this.channel);
99
+ if (messageService?.patch) {
100
+ const origPatch = messageService.patch.bind(messageService);
101
+ messageService.patch = (async (request, options) => {
102
+ const res = await origPatch(request, options);
103
+ if (typeof res?.code === 'number' && res.code !== 0) {
104
+ const content = request?.data?.content;
105
+ const bytes = typeof content === 'string' ? Buffer.byteLength(content, 'utf8') : undefined;
106
+ getLogger().warn(`[feishu] message.patch business error code=${res.code} msg=${String(res.msg)} ` +
107
+ `messageId=${String(request?.path?.message_id)} bytes=${bytes ?? 'unknown'}`);
108
+ }
109
+ return res;
110
+ });
111
+ }
84
112
  this.channel.on('message', (msg) => {
85
113
  if (msg.chatType !== 'p2p')
86
114
  return;