lark-remote 0.1.1 → 0.1.3

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/README.en.md +2 -5
  2. package/README.md +1 -4
  3. package/dist/bridge/index.d.ts +1 -3
  4. package/dist/bridge/index.js +11 -12
  5. package/dist/card/card-budget.js +1 -2
  6. package/dist/card/card-shared.d.ts +8 -0
  7. package/dist/card/card-shared.js +14 -0
  8. package/dist/card/run-renderer.js +1 -0
  9. package/dist/card/run-state.d.ts +4 -0
  10. package/dist/config/codex-config.d.ts +3 -5
  11. package/dist/config/codex-config.js +7 -7
  12. package/dist/config/dir.d.ts +1 -0
  13. package/dist/config/dir.js +4 -0
  14. package/dist/config/index.d.ts +0 -10
  15. package/dist/config/index.js +0 -11
  16. package/dist/connector/index.js +28 -0
  17. package/dist/index.js +89 -41
  18. package/dist/router/config/codex.js +8 -12
  19. package/dist/router/index.d.ts +15 -2
  20. package/dist/router/index.js +314 -44
  21. package/dist/router/utils.d.ts +3 -0
  22. package/dist/router/utils.js +15 -3
  23. package/dist/runner/claude/runner.d.ts +0 -1
  24. package/dist/runner/claude/runner.js +1 -1
  25. package/dist/runner/codex/runner.d.ts +0 -2
  26. package/dist/runner/codex/runner.js +1 -1
  27. package/dist/runner/common/spawning-runner.d.ts +0 -1
  28. package/dist/runner/common/spawning-runner.js +2 -4
  29. package/dist/runner/kimi/runner.d.ts +0 -1
  30. package/dist/runner/kimi/runner.js +1 -1
  31. package/dist/runner/opencode/runner.d.ts +0 -2
  32. package/dist/runner/opencode/runner.js +1 -1
  33. package/dist/runner/pi/runner.d.ts +0 -1
  34. package/dist/runner/pi/runner.js +1 -1
  35. package/dist/runner/probe.d.ts +45 -0
  36. package/dist/runner/probe.js +87 -0
  37. package/dist/runner/types.d.ts +3 -0
  38. package/dist/session/claude/sessions.d.ts +8 -6
  39. package/dist/session/claude/sessions.js +178 -26
  40. package/dist/session/codex/rollout-reader.d.ts +1 -0
  41. package/dist/session/codex/rollout-reader.js +15 -0
  42. package/dist/session/codex/sessions.d.ts +1 -1
  43. package/dist/session/codex/sessions.js +2 -1
  44. package/dist/session/kimi/sessions.d.ts +3 -1
  45. package/dist/session/kimi/sessions.js +42 -6
  46. package/dist/session/opencode/sessions.d.ts +0 -2
  47. package/dist/session/opencode/sessions.js +1 -1
  48. package/dist/startup-contact.d.ts +4 -2
  49. package/dist/startup-contact.js +7 -4
  50. package/package.json +39 -4
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import { parseCliArgs, resolveConfigDir, setConfigDir, printHelp, printVersion,
5
5
  import { FeishuConnector } from './connector/index.js';
6
6
  import { ClaudeRunner, CodexExecRunner, OpencodeExecRunner, PiRunner, KimiRunner, } from './runner/index.js';
7
7
  import { AgentRegistry } from './runner/registry.js';
8
+ import { probeAllAgents } from './runner/probe.js';
8
9
  import { SessionReaderRegistry, SessionStore } from './session/index.js';
9
10
  import { ClaudeSessionReader, CodexSessionReader, OpencodeSessionReader, PiSessionReader, KimiSessionReader, } from './session/index.js';
10
11
  import { CommandRouter, isImmediateAction, formatUsageStats, } from './router/index.js';
@@ -21,6 +22,7 @@ import { markdownDiv } from './card/collapsible.js';
21
22
  import { sessionEventPanel } from './router/card-helpers.js';
22
23
  import { newSessionButton, agentDisplayName } from './card/card-shared.js';
23
24
  import path from 'node:path';
25
+ import fs from 'node:fs';
24
26
  /** Send auto-resume card for a restored session */
25
27
  async function sendAutoResumeCard(connector, contact, cwd, session, state) {
26
28
  if (!contact.userId)
@@ -143,27 +145,23 @@ function setupInstanceLockAndHandlers(instanceLock, logger, configDir) {
143
145
  }
144
146
  /** Initialize per-workspace runner factory + agent registry. */
145
147
  function initializeRunner(config, configDir, cliArgs) {
146
- const createRunner = (workspace) => {
147
- const runner = new ClaudeRunner({
148
- binary: config.claude.binary,
149
- model: config.claude.model,
150
- effort: config.claude.effort,
151
- stopGraceMs: config.claude.stopGraceMs,
148
+ const agentRegistry = new AgentRegistry();
149
+ // P1-15: Claude factory reads from configContainer (not closure) so runtime
150
+ // config changes (model, effort, stopGraceMs) take effect after
151
+ // bridge.setConfig() + clearRunners(). Same pattern as codex/pi/kimi.
152
+ agentRegistry.register('claude', (ws) => {
153
+ const container = agentRegistry.getConfigContainer();
154
+ const latestConfig = container?.current ?? config;
155
+ const claudeConfig = latestConfig.claude;
156
+ return new ClaudeRunner({
157
+ model: claudeConfig.model,
158
+ effort: claudeConfig.effort,
159
+ stopGraceMs: claudeConfig.stopGraceMs,
152
160
  settings: cliArgs.settings,
153
161
  pidDir: configDir,
154
- workspace,
162
+ workspace: ws,
155
163
  });
156
- return runner;
157
- };
158
- const runner = createRunner('default');
159
- runner.killOrphan();
160
- runner.registerExitHandlers();
161
- // Wire multi-agent registries. All five agents (claude/codex/opencode/pi/kimi)
162
- // are already registered. Bridge and Router depend on these registries instead of
163
- // importing session readers directly, so adding a new agent is "register factory + reader"
164
- // with zero upstream changes.
165
- const agentRegistry = new AgentRegistry();
166
- agentRegistry.register('claude', (ws) => createRunner(ws));
164
+ });
167
165
  // Register CodexExecRunner (codex exec --json, approval_policy=never).
168
166
  // Factory reads latest config from container.current so runtime config changes
169
167
  // (reasoningEffort, model, etc.) take effect after bridge.setConfig() + clearRunners().
@@ -173,7 +171,6 @@ function initializeRunner(config, configDir, cliArgs) {
173
171
  const latestConfig = container?.current ?? config;
174
172
  const codexConfig = getAgentConfig(latestConfig, 'codex');
175
173
  return new CodexExecRunner({
176
- binary: codexConfig?.binary ?? 'codex',
177
174
  model: codexConfig?.model,
178
175
  modelProvider: codexConfig?.modelProvider,
179
176
  reasoningEffort: codexConfig?.reasoningEffort,
@@ -185,20 +182,17 @@ function initializeRunner(config, configDir, cliArgs) {
185
182
  });
186
183
  // Register OpencodeExecRunner (run mode)
187
184
  // Using `opencode run --format json --auto` instead of HTTP+SSE server mode
188
- const opencodeConfig = getAgentConfig(config, 'opencode');
189
185
  // Register session reader (same instance as used by runner)
190
186
  const sessionReaderRegistry = new SessionReaderRegistry();
191
187
  sessionReaderRegistry.register('claude', new ClaudeSessionReader());
192
188
  sessionReaderRegistry.register('codex', codexSessionReader);
193
189
  // Register OpencodeSessionReader (CLI version)
194
190
  // Using `opencode session list` and `opencode export` instead of HTTP API
195
- const opencodeSessionReader = new OpencodeSessionReader({
196
- binary: opencodeConfig?.binary ?? 'opencode',
197
- });
191
+ const opencodeSessionReader = new OpencodeSessionReader();
198
192
  agentRegistry.register('opencode', (ws) => {
199
193
  // Get latest config from container (set below for pi)
200
194
  const container = agentRegistry.getConfigContainer();
201
- const latestConfig = container?.current;
195
+ const latestConfig = container?.current ?? config;
202
196
  const ocConfig = getAgentConfig(latestConfig, 'opencode');
203
197
  // Build model string with validation: provider/model format
204
198
  let model;
@@ -213,9 +207,9 @@ function initializeRunner(config, configDir, cliArgs) {
213
207
  }
214
208
  }
215
209
  return new OpencodeExecRunner({
216
- binary: ocConfig?.binary ?? 'opencode',
217
210
  model,
218
- stopGraceMs: config.claude?.stopGraceMs ?? DEFAULT_STOP_GRACE_MS,
211
+ // P1-15: read stopGraceMs from latestConfig (not startup closure)
212
+ stopGraceMs: latestConfig.claude?.stopGraceMs ?? DEFAULT_STOP_GRACE_MS,
219
213
  pidDir: configDir,
220
214
  workspace: ws,
221
215
  sessionReader: opencodeSessionReader,
@@ -232,10 +226,9 @@ function initializeRunner(config, configDir, cliArgs) {
232
226
  agentRegistry.register('pi', (ws) => {
233
227
  // 每次 factory 调用时从 registry 获取最新 config
234
228
  const container = agentRegistry.getConfigContainer();
235
- const latestConfig = container?.current;
229
+ const latestConfig = container?.current ?? config;
236
230
  const piConf = getAgentConfig(latestConfig, 'pi');
237
231
  return new PiRunner({
238
- binary: piConf?.binary ?? 'pi',
239
232
  provider: piConf?.provider ?? 'Volcano',
240
233
  model: piConf?.model ?? 'glm-5.2',
241
234
  thinking: piConf?.thinking ?? 'medium',
@@ -254,13 +247,13 @@ function initializeRunner(config, configDir, cliArgs) {
254
247
  agentRegistry.register('kimi', (ws) => {
255
248
  // 每次 factory 调用时从 registry 获取最新 config
256
249
  const container = agentRegistry.getConfigContainer();
257
- const latestConfig = container?.current;
250
+ const latestConfig = container?.current ?? config;
258
251
  const kimiConf = getAgentConfig(latestConfig, 'kimi');
259
252
  return new KimiRunner({
260
- binary: kimiConf?.binary ?? 'kimi',
261
253
  model: kimiConf?.model ?? 'kimi-code/k3',
262
254
  thinkingEffort: kimiConf?.thinkingEffort ?? 'max',
263
- stopGraceMs: config.claude?.stopGraceMs ?? DEFAULT_STOP_GRACE_MS,
255
+ // P1-15: read stopGraceMs from latestConfig (not startup closure)
256
+ stopGraceMs: latestConfig.claude?.stopGraceMs ?? DEFAULT_STOP_GRACE_MS,
264
257
  pidDir: configDir,
265
258
  workspace: ws,
266
259
  sessionReader: kimiSessionReader,
@@ -275,10 +268,25 @@ function initializeRunner(config, configDir, cliArgs) {
275
268
  agentRegistry.registerDisplayName('kimi', 'Kimi');
276
269
  // 设置全局 registry,供 agentDisplayName 等全局函数使用
277
270
  AgentRegistry.setGlobalInstance(agentRegistry);
278
- return { createRunner, runner, agentRegistry, sessionReaderRegistry };
271
+ // Probe agent CLI availability (fire-and-forget, populates cache for /config card).
272
+ // Don't block startup — log unavailable agents as warnings.
273
+ probeAllAgents()
274
+ .then((availability) => {
275
+ const unavailable = [...availability.entries()].filter(([, ok]) => !ok).map(([kind]) => kind);
276
+ if (unavailable.length > 0) {
277
+ getLogger().warn(`[probe] agent CLI not found or not functional: ${unavailable.join(', ')}`);
278
+ }
279
+ else {
280
+ getLogger().info('[probe] all agent CLIs available');
281
+ }
282
+ })
283
+ .catch(() => {
284
+ // Probe failure is non-fatal; /config card will retry on open.
285
+ });
286
+ return { agentRegistry, sessionReaderRegistry };
279
287
  }
280
288
  /** Setup message and card action handlers. */
281
- function setupMessageHandlers(connector, router, bridge, sessionStore, workspaceStore, binder, logger) {
289
+ function setupMessageHandlers(connector, router, bridge, sessionStore, workspaceStore, binder, logger, config) {
282
290
  connector.setMessageHandler((msg) => {
283
291
  // 绑定/授权闸门:仅 owner 放行;非 owner 静默丢弃;未绑定时要求 PIN 认领
284
292
  const decision = binder.classify(msg.userId, msg.content, msg.chatId);
@@ -288,9 +296,50 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
288
296
  if (decision.kind === 'pin_wrong')
289
297
  return;
290
298
  if (decision.kind === 'bind_success') {
291
- void connector
292
- .sendWithRetry(msg.chatId, { text: '✅ 已绑定到本账号,此后仅你可使用本应用' })
293
- .catch((err) => logger.error('[binder] bind-success reply failed:', err));
299
+ // First-run onboarding: set default cwd + send welcome + Help card.
300
+ // setCwd is synchronous (outside the async closure) so that the user's
301
+ // next message — which will hit the `owner` branch now that
302
+ // startup-contact.json has been written — finds cwd already set.
303
+ // process.cwd() is the only sensible default: the user started
304
+ // lark-remote in the directory they want to work in.
305
+ // setCwd only when no cwd is set yet (e.g. fresh start after bind).
306
+ // Don't overwrite an existing cwd — the user may have already set one
307
+ // via /cd or a previous bind. Use realpath for canonical form (matching
308
+ // Claude JSONL cwd, e.g. /tmp → /private/tmp on macOS).
309
+ let resolvedCwd;
310
+ try {
311
+ resolvedCwd = fs.realpathSync(process.cwd());
312
+ }
313
+ catch {
314
+ resolvedCwd = process.cwd();
315
+ }
316
+ if (!sessionStore.getCwd(msg.userId)) {
317
+ sessionStore.setCwd(msg.userId, resolvedCwd);
318
+ }
319
+ void (async () => {
320
+ try {
321
+ // Message 1: bind confirmation + status (combined to avoid
322
+ // Feishu out-of-order delivery across three separate messages).
323
+ await connector.sendWithRetry(msg.chatId, {
324
+ text: `✅ 已绑定到本账号,此后仅你可使用本应用\n` +
325
+ `📂 当前工作目录: \`${resolvedCwd}\`\n` +
326
+ `🤖 默认 Agent: ${agentDisplayName(config.defaultAgent)}\n` +
327
+ `💡 直接输入消息即可开始对话,或 /cd 切换目录`,
328
+ });
329
+ // Message 2: Help card.
330
+ // Use router.cmdHelp() + manual send (without replyTo) so the Help
331
+ // card appears as an independent message, not a reply to the PIN
332
+ // message. router.handle('/help') would set replyTo=msg.messageId
333
+ // (the PIN), causing the card to nest under the PIN in Feishu UI.
334
+ const helpResult = router.cmdHelp();
335
+ if (helpResult.card) {
336
+ await connector.sendWithRetry(msg.chatId, { card: helpResult.card });
337
+ }
338
+ }
339
+ catch (err) {
340
+ logger.error('[binder] onboarding send failed:', err);
341
+ }
342
+ })();
294
343
  return;
295
344
  }
296
345
  // decision.kind === 'owner':正常处理(不再每条覆盖 startup-contact)
@@ -488,11 +537,11 @@ async function main() {
488
537
  logger.info('config loaded');
489
538
  logger.info(`configDir = ${configDir}`);
490
539
  logger.info(`feishu.appId = ${config.feishu.appId}`);
491
- logger.info(`claude.binary = ${config.claude.binary}`);
540
+ logger.info(`claude.model = ${config.claude.model}`);
492
541
  if (cliArgs.settings) {
493
542
  logger.info(`claude.settings = ${cliArgs.settings}`);
494
543
  }
495
- const { runner, agentRegistry, sessionReaderRegistry } = initializeRunner(config, configDir, cliArgs);
544
+ const { agentRegistry, sessionReaderRegistry } = initializeRunner(config, configDir, cliArgs);
496
545
  const connector = new FeishuConnector(config);
497
546
  const startupContactStore = new StartupContactStore(path.join(configDir, 'startup-contact.json'));
498
547
  const binder = new OwnerBinder(startupContactStore);
@@ -507,7 +556,6 @@ async function main() {
507
556
  const sessionStore = new SessionStore(path.join(configDir, 'last-session.json'), config.defaultAgent);
508
557
  const workspaceStore = new WorkspaceStore(path.join(configDir, 'workspace.json'));
509
558
  const bridge = new Bridge({
510
- runner,
511
559
  connector,
512
560
  sessionStore,
513
561
  config,
@@ -525,7 +573,7 @@ async function main() {
525
573
  restartSpawner: () => spawnReplacementBridge(path.join(configDir, 'logs')),
526
574
  sessionReaderRegistry,
527
575
  });
528
- setupMessageHandlers(connector, router, bridge, sessionStore, workspaceStore, binder, logger);
576
+ setupMessageHandlers(connector, router, bridge, sessionStore, workspaceStore, binder, logger, config);
529
577
  try {
530
578
  await connector.connect();
531
579
  }
@@ -535,7 +583,7 @@ async function main() {
535
583
  }
536
584
  if (binder.isBound()) {
537
585
  // 仅已绑定时发送启动通知;未绑定时不打扰(PIN 引导已在控制台输出)
538
- await sendStartupHello(connector, startupContactStore);
586
+ await sendStartupHello(connector, startupContactStore, { dev: cliArgs.dev });
539
587
  }
540
588
  // Auto-restore: resume the persisted sessionId if available
541
589
  const restoredContact = startupContactStore.getContact();
@@ -6,14 +6,12 @@ export class CodexConfigBuilder {
6
6
  buildFields(displayConfig) {
7
7
  const fields = [];
8
8
  // 从 codex config.toml 动态读取 provider 和 model
9
- const codexBinary = displayConfig.agents?.codex?.binary ?? 'codex';
10
- const codexCfg = loadCodexConfig({ binary: codexBinary });
9
+ const codexCfg = loadCodexConfig();
11
10
  const codexProviderNames = codexCfg.providerNames;
12
11
  const currentModelProvider = displayConfig.agents?.codex?.modelProvider ?? codexCfg.currentProvider;
13
12
  const currentCodexModel = displayConfig.agents?.codex?.model ?? codexCfg.currentModel;
14
- const currentReasoningEffort = displayConfig.agents?.codex?.reasoningEffort ??
15
- getDefaultReasoningEffort(currentCodexModel, codexBinary);
16
- // 传入当前 provider 过滤模型列���
13
+ const currentReasoningEffort = displayConfig.agents?.codex?.reasoningEffort ?? getDefaultReasoningEffort(currentCodexModel);
14
+ // 传入当前 provider 过滤模型列表
17
15
  const codexModelOptions = codexCfg.modelOptions(currentModelProvider);
18
16
  fields.push({
19
17
  key: 'agents.codex.modelProvider',
@@ -42,7 +40,7 @@ export class CodexConfigBuilder {
42
40
  label: '推理强度',
43
41
  type: 'select',
44
42
  // 档位按目录实际声明透传(P2-5):codex 支持 none/Custom,不再按标准枚举过滤
45
- options: getReasoningEffortOptions(currentCodexModel, codexBinary),
43
+ options: getReasoningEffortOptions(currentCodexModel),
46
44
  currentValue: currentReasoningEffort,
47
45
  });
48
46
  return fields;
@@ -50,10 +48,9 @@ export class CodexConfigBuilder {
50
48
  handleFieldChange(key, value, config) {
51
49
  const patches = [];
52
50
  patches.push({ key, value });
53
- // provider 变更时,重置 model 为�� provider 的首个模型
51
+ // provider 变更时,重置 model 为新 provider 的首个模型
54
52
  if (key === 'agents.codex.modelProvider' && typeof value === 'string') {
55
- const codexBinary = config.agents?.codex?.binary ?? 'codex';
56
- const codexCfg = loadCodexConfig({ binary: codexBinary });
53
+ const codexCfg = loadCodexConfig();
57
54
  const newModelOptions = codexCfg.modelOptions(value);
58
55
  const currentModel = config.agents?.codex?.model;
59
56
  const currentModelIsValid = newModelOptions.some((m) => m === currentModel);
@@ -86,14 +83,13 @@ export class CodexConfigBuilder {
86
83
  * 对 undefined 做键删除,P1)。
87
84
  */
88
85
  effortPatchForModel(config, newModel) {
89
- const codexBinary = config.agents?.codex?.binary ?? 'codex';
90
86
  const currentReasoningEffort = config.agents?.codex?.reasoningEffort;
91
- const newModelSupportedEfforts = getReasoningEffortOptions(newModel, codexBinary);
87
+ const newModelSupportedEfforts = getReasoningEffortOptions(newModel);
92
88
  const isCurrentEffortValid = currentReasoningEffort && newModelSupportedEfforts.includes(currentReasoningEffort);
93
89
  if (isCurrentEffortValid)
94
90
  return null;
95
91
  const middle = newModelSupportedEfforts[Math.floor((newModelSupportedEfforts.length - 1) / 2)];
96
- const newEffort = middle ?? getDefaultReasoningEffort(newModel, codexBinary);
92
+ const newEffort = middle ?? getDefaultReasoningEffort(newModel);
97
93
  return { key: 'agents.codex.reasoningEffort', value: newEffort };
98
94
  }
99
95
  }
@@ -26,7 +26,7 @@ interface CommandResult {
26
26
  * stop — interrupt current run
27
27
  * ls.file — send file only
28
28
  * ws.remove — delete workspace alias only
29
- * resume.use — set sessionId only
29
+ * resume.use — set sessionId + agent for correct reader routing
30
30
  * help.* — read-only help commands
31
31
  * order.delete — delete order only
32
32
  * order.exec is intercepted by index.ts at the enqueue boundary
@@ -73,6 +73,8 @@ interface CardActionResponse {
73
73
  [key: string]: unknown;
74
74
  }
75
75
  export declare class CommandRouter {
76
+ /** Valid agent kinds — single source of truth for resume.use / resume.page / cmdResume. */
77
+ static readonly VALID_AGENTS: readonly ["claude", "codex", "opencode", "pi", "kimi"];
76
78
  private sessionStore;
77
79
  private bridge;
78
80
  private config;
@@ -187,6 +189,11 @@ export declare class CommandRouter {
187
189
  * Mirrors handleLsPage — updates the same card, never sends a new one.
188
190
  */
189
191
  private handleResumePage;
192
+ /**
193
+ * Handle active.page: paginate the /active run list in place.
194
+ * Mirrors handleResumePage — updates the same card, never sends a new one.
195
+ */
196
+ private handleActivePage;
190
197
  /**
191
198
  * Handle ls.browse: browse directory without switching cwd.
192
199
  */
@@ -223,6 +230,11 @@ export declare class CommandRouter {
223
230
  * Handle order.delete: remove the order and update card in place.
224
231
  */
225
232
  private handleOrderDelete;
233
+ /**
234
+ * Handle order.page: paginate the /order list in place.
235
+ * Mirrors handleLsPage — updates the same card, never sends a new one.
236
+ */
237
+ private handleOrderPage;
226
238
  /**
227
239
  * Handle ws.remove: remove the workspace alias and refresh the /ws list card
228
240
  * in place (mirrors handleOrderDelete). Without this the stale alias would
@@ -273,7 +285,7 @@ export declare class CommandRouter {
273
285
  */
274
286
  private propagateConfigSave;
275
287
  private executeCommand;
276
- private cmdHelp;
288
+ cmdHelp(): CommandResult;
277
289
  private cmdStatus;
278
290
  private cmdPs;
279
291
  private cmdStop;
@@ -307,6 +319,7 @@ export declare class CommandRouter {
307
319
  /**
308
320
  * Build CardKit 2.0 /active card from bridge memory (not file system).
309
321
  * Shows only runs that were started by THIS bridge process.
322
+ * Paginates with ACTIVE_PAGE_SIZE (default 20) items per page.
310
323
  */
311
324
  private buildActiveCardFromMemory;
312
325
  private cmdReconnect;