lark-remote 0.1.12 → 0.2.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 (160) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.en.md +2 -6
  3. package/README.md +3 -7
  4. package/dist/bridge/final-usage.d.ts +24 -0
  5. package/dist/bridge/final-usage.js +55 -0
  6. package/dist/bridge/inbound-media.d.ts +0 -3
  7. package/dist/bridge/inbound-media.js +0 -1
  8. package/dist/bridge/index.d.ts +7 -7
  9. package/dist/bridge/index.js +100 -133
  10. package/dist/bridge/queue-manager.js +4 -3
  11. package/dist/card/approval-render.js +39 -94
  12. package/dist/card/bash-renderer.js +12 -21
  13. package/dist/card/card-shared.d.ts +14 -5
  14. package/dist/card/card-shared.js +23 -34
  15. package/dist/card/run-renderer.d.ts +0 -22
  16. package/dist/card/run-renderer.js +206 -153
  17. package/dist/card/run-state.d.ts +29 -1
  18. package/dist/card/run-state.js +172 -84
  19. package/dist/card/text-truncate.d.ts +0 -9
  20. package/dist/card/text-truncate.js +0 -9
  21. package/dist/card/tool-render.d.ts +4 -1
  22. package/dist/card/tool-render.js +76 -32
  23. package/dist/cli.js +2 -2
  24. package/dist/common/fs.d.ts +14 -0
  25. package/dist/common/fs.js +57 -3
  26. package/dist/common/truncate.d.ts +0 -6
  27. package/dist/common/truncate.js +1 -1
  28. package/dist/common/ttl-cache.d.ts +24 -0
  29. package/dist/common/ttl-cache.js +53 -0
  30. package/dist/config/codex-config.js +14 -11
  31. package/dist/config/index.d.ts +0 -8
  32. package/dist/config/index.js +0 -16
  33. package/dist/config/kimi-config.d.ts +1 -1
  34. package/dist/config/kimi-config.js +2 -1
  35. package/dist/config/opencode-config.d.ts +1 -1
  36. package/dist/config/opencode-config.js +16 -10
  37. package/dist/config/qr.d.ts +36 -0
  38. package/dist/config/qr.js +124 -0
  39. package/dist/config/wizard.js +69 -27
  40. package/dist/connector/index.d.ts +3 -2
  41. package/dist/connector/index.js +5 -3
  42. package/dist/index.js +14 -41
  43. package/dist/instance-lock.js +7 -0
  44. package/dist/persistence/atomic-write.d.ts +2 -0
  45. package/dist/persistence/atomic-write.js +20 -5
  46. package/dist/persistence/load-json-file.d.ts +1 -2
  47. package/dist/persistence/load-json-file.js +2 -3
  48. package/dist/platform/agent-stopper.d.ts +22 -0
  49. package/dist/platform/agent-stopper.js +19 -0
  50. package/dist/platform/command.d.ts +11 -0
  51. package/dist/platform/command.js +107 -0
  52. package/dist/platform/identity.d.ts +33 -0
  53. package/dist/platform/identity.js +187 -0
  54. package/dist/platform/path.d.ts +24 -0
  55. package/dist/platform/path.js +110 -0
  56. package/dist/platform/select.d.ts +9 -0
  57. package/dist/platform/select.js +12 -0
  58. package/dist/platform/shell.d.ts +51 -0
  59. package/dist/platform/shell.js +75 -0
  60. package/dist/platform/spawn.d.ts +41 -0
  61. package/dist/platform/spawn.js +77 -0
  62. package/dist/platform/terminator-posix.d.ts +38 -0
  63. package/dist/platform/terminator-posix.js +103 -0
  64. package/dist/platform/terminator-win32.d.ts +21 -0
  65. package/dist/platform/terminator-win32.js +110 -0
  66. package/dist/platform/terminator.d.ts +44 -0
  67. package/dist/platform/terminator.js +24 -0
  68. package/dist/platform/types.d.ts +23 -0
  69. package/dist/platform/types.js +5 -0
  70. package/dist/restart.js +4 -2
  71. package/dist/router/card-helpers.d.ts +33 -6
  72. package/dist/router/card-helpers.js +67 -11
  73. package/dist/router/config/common/render.d.ts +1 -1
  74. package/dist/router/config/common/render.js +1 -1
  75. package/dist/router/config-diff.d.ts +4 -0
  76. package/dist/router/config-diff.js +70 -0
  77. package/dist/router/index.d.ts +73 -6
  78. package/dist/router/index.js +663 -590
  79. package/dist/router/utils.d.ts +6 -0
  80. package/dist/router/utils.js +12 -0
  81. package/dist/runner/bash/runner.d.ts +9 -2
  82. package/dist/runner/bash/runner.js +45 -20
  83. package/dist/runner/claude/session.d.ts +4 -4
  84. package/dist/runner/claude/session.js +18 -9
  85. package/dist/runner/codex/app-server/protocol-types.d.ts +35 -1
  86. package/dist/runner/codex/app-server/runner.d.ts +1 -16
  87. package/dist/runner/codex/app-server/runner.js +2 -2
  88. package/dist/runner/codex/app-server/translator.d.ts +18 -0
  89. package/dist/runner/codex/app-server/translator.js +146 -0
  90. package/dist/runner/common/acp/base-acp-runner.d.ts +116 -0
  91. package/dist/runner/common/acp/base-acp-runner.js +248 -0
  92. package/dist/runner/common/acp/base-translator.d.ts +141 -0
  93. package/dist/runner/common/acp/base-translator.js +271 -0
  94. package/dist/runner/common/acp/protocol-helpers.d.ts +70 -0
  95. package/dist/runner/common/acp/protocol-helpers.js +82 -0
  96. package/dist/runner/common/connection-based-runner.d.ts +0 -2
  97. package/dist/runner/common/connection-based-runner.js +1 -1
  98. package/dist/runner/common/jsonrpc/transport.d.ts +2 -0
  99. package/dist/runner/common/jsonrpc/transport.js +25 -5
  100. package/dist/runner/common/process-stopper.d.ts +4 -26
  101. package/dist/runner/common/process-stopper.js +4 -76
  102. package/dist/runner/common/runner-utils.d.ts +0 -5
  103. package/dist/runner/common/runner-utils.js +0 -7
  104. package/dist/runner/common/spawning-runner.d.ts +42 -132
  105. package/dist/runner/common/spawning-runner.js +52 -271
  106. package/dist/runner/common/usage.d.ts +2 -0
  107. package/dist/runner/common/usage.js +2 -0
  108. package/dist/runner/dsh/index.d.ts +0 -2
  109. package/dist/runner/dsh/index.js +0 -2
  110. package/dist/runner/dsh/runner.d.ts +0 -2
  111. package/dist/runner/dsh/runner.js +1 -1
  112. package/dist/runner/index.d.ts +1 -1
  113. package/dist/runner/kimi/acp/runner.d.ts +63 -53
  114. package/dist/runner/kimi/acp/runner.js +189 -334
  115. package/dist/runner/kimi/acp/translator.d.ts +18 -101
  116. package/dist/runner/kimi/acp/translator.js +61 -255
  117. package/dist/runner/opencode/acp/runner.d.ts +22 -46
  118. package/dist/runner/opencode/acp/runner.js +74 -297
  119. package/dist/runner/opencode/acp/translator.d.ts +28 -104
  120. package/dist/runner/opencode/acp/translator.js +54 -249
  121. package/dist/runner/pi/rpc/index.d.ts +0 -2
  122. package/dist/runner/pi/rpc/index.js +0 -2
  123. package/dist/runner/pi/rpc/translator.d.ts +3 -0
  124. package/dist/runner/pi/rpc/translator.js +47 -9
  125. package/dist/runner/probe.d.ts +8 -11
  126. package/dist/runner/probe.js +17 -21
  127. package/dist/runner/registry.d.ts +4 -4
  128. package/dist/runner/registry.js +4 -4
  129. package/dist/runner/resolve-agent-choices.js +6 -2
  130. package/dist/runner/sync-agent-choices.js +1 -1
  131. package/dist/runner/types.d.ts +38 -1
  132. package/dist/session/claude/session-index.js +3 -1
  133. package/dist/session/claude/sessions.js +37 -64
  134. package/dist/session/codex/rollout-reader.d.ts +10 -14
  135. package/dist/session/codex/rollout-reader.js +126 -48
  136. package/dist/session/common/constants.d.ts +7 -0
  137. package/dist/session/common/constants.js +9 -0
  138. package/dist/session/common/pagination.d.ts +0 -2
  139. package/dist/session/common/pagination.js +1 -1
  140. package/dist/session/common/recency.d.ts +7 -0
  141. package/dist/session/common/recency.js +10 -0
  142. package/dist/session/common/two-pass.d.ts +22 -0
  143. package/dist/session/common/two-pass.js +70 -0
  144. package/dist/session/common/usage-accumulator.d.ts +14 -0
  145. package/dist/session/common/usage-accumulator.js +15 -0
  146. package/dist/session/dsh/sessions.js +15 -11
  147. package/dist/session/kimi/sessions.d.ts +24 -4
  148. package/dist/session/kimi/sessions.js +99 -103
  149. package/dist/session/opencode/sessions.d.ts +2 -14
  150. package/dist/session/opencode/sessions.js +35 -15
  151. package/dist/session/pi/sessions.js +64 -95
  152. package/dist/update/index.d.ts +2 -3
  153. package/dist/update/index.js +1 -2
  154. package/dist/update/install.d.ts +2 -1
  155. package/dist/update/install.js +58 -9
  156. package/dist/update/version-check.d.ts +5 -0
  157. package/dist/update/version-check.js +9 -0
  158. package/package.json +9 -7
  159. package/dist/update/startup-hint.d.ts +0 -5
  160. package/dist/update/startup-hint.js +0 -11
@@ -1,17 +1,27 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import os from 'node:os';
4
- import { findJsonlLine, scanJsonlLines, readJsonlLinesFromOffset } from '../common/jsonl.js';
4
+ import { findJsonlLine } from '../common/jsonl.js';
5
+ import { scanJsonlOnce, tailOffsetAfter, scanTailEvents } from '../common/two-pass.js';
5
6
  import { extractContentBlocks } from '../common/content-blocks.js';
6
- import { UsageAccumulator } from '../common/usage-accumulator.js';
7
- import { STALE_MS } from '../common/constants.js';
8
- import { capEvents, paginate } from '../common/pagination.js';
7
+ import { UsageAccumulator, contextWindowOccupancy, cumulativeUsageFields, } from '../common/usage-accumulator.js';
8
+ import { isStale } from '../common/constants.js';
9
+ import { encodeProjectDirName } from '../../platform/path.js';
10
+ import { paginate } from '../common/pagination.js';
11
+ import { sortByRecencyDesc } from '../common/recency.js';
12
+ import { TtlCache } from '../../common/ttl-cache.js';
9
13
  function defaultPiDir() {
10
14
  return path.join(os.homedir(), '.pi', 'agent');
11
15
  }
12
- /** Encode cwd to pi's directory name format: '--<cwd-with-/->- + '-' --' */
16
+ /**
17
+ * Encode cwd to pi's directory name format: '--<cwd with / \ : -> - --'
18
+ *
19
+ * 与 claude 共用 `encodeProjectDirName`(同一套 win32 归一规则),再去掉
20
+ * 前导分隔符产生的首个 `-`:`/Users/x` -> `Users-x`(不是 `-Users-x`)。
21
+ * win32 同理:`C:\Users\x` -> `C--Users-x`。
22
+ */
13
23
  function projectDirForCwd(cwd, sessionsDir) {
14
- const encodedCwd = cwd.replace(/^\//, '').replace(/\//g, '-');
24
+ const encodedCwd = encodeProjectDirName(cwd).replace(/^-+/, '');
15
25
  const encoded = `--${encodedCwd}--`;
16
26
  return path.join(sessionsDir, encoded);
17
27
  }
@@ -19,10 +29,7 @@ function projectDirForCwd(cwd, sessionsDir) {
19
29
  * P1-19: TTL cache for pi directory scans (mirror codex getSessionIndex's
20
30
  * 5s TTL). Repeated /resume pages re-scan the whole sessions dir per call.
21
31
  */
22
- const PI_LIST_TTL_MS = 5000;
23
- /** Upper bound on cached listings; FIFO eviction past this. */
24
- const PI_LIST_MAX_ENTRIES = 32;
25
- const piListCache = new Map();
32
+ const piListCache = new TtlCache(5_000, 32);
26
33
  /** Pi-specific field-name mapping for content block extraction. */
27
34
  export const PI_MAPPING = {
28
35
  toolUseType: 'toolCall',
@@ -144,14 +151,8 @@ function piScalarScan(filePath) {
144
151
  const acc = new UsageAccumulator();
145
152
  let tailOffset = -1;
146
153
  let lastUserText;
147
- scanJsonlLines(filePath, (line, offset) => {
148
- let obj;
149
- try {
150
- obj = JSON.parse(line);
151
- }
152
- catch {
153
- return;
154
- }
154
+ scanJsonlOnce(filePath, (rawObj, line, offset) => {
155
+ const obj = rawObj;
155
156
  if (obj.type === 'message') {
156
157
  const msg = obj.message;
157
158
  if (!msg)
@@ -169,9 +170,9 @@ function piScalarScan(filePath) {
169
170
  }
170
171
  // last user message offset + displayTitle. Record the byte offset
171
172
  // where the tail should start: the beginning of the NEXT line after
172
- // this user message.
173
+ // this user message (see tailOffsetAfter).
173
174
  if (msg.role === 'user') {
174
- tailOffset = offset + Buffer.byteLength(line, 'utf-8') + 1;
175
+ tailOffset = tailOffsetAfter(line, offset);
175
176
  const text = extractPiText(msg.content);
176
177
  // Compress skill-injection bodies so the "最近输入" label shows what
177
178
  // the user invoked (skill:x) + any trailing real input, not a
@@ -207,62 +208,48 @@ function buildPiUsage(acc) {
207
208
  return undefined;
208
209
  }
209
210
  const l = acc.last;
210
- const contextLength = l.input + l.cacheRead + l.cacheCreation;
211
211
  return {
212
212
  inputTokens: l.input,
213
213
  outputTokens: l.output,
214
- contextLength,
214
+ contextLength: contextWindowOccupancy(l),
215
215
  compactCount: acc.compactCount > 0 ? acc.compactCount : undefined,
216
216
  cacheReadTokens: l.cacheRead > 0 ? l.cacheRead : 0,
217
217
  cacheCreationTokens: l.cacheCreation > 0 ? l.cacheCreation : 0,
218
218
  totalTokens: l.total > 0 ? l.total : undefined,
219
219
  // Cumulative (session-wide): sum of ALL runs in the jsonl file.
220
- cumulativeTotalTokens: t.input + t.output + t.cacheRead + t.cacheCreation,
221
- cumulativeInputTokens: t.input,
222
- cumulativeOutputTokens: t.output,
223
- cumulativeCacheReadTokens: t.cacheRead,
224
- cumulativeCacheCreationTokens: t.cacheCreation,
220
+ ...cumulativeUsageFields(t),
225
221
  };
226
222
  }
227
223
  /**
228
- * P2-6 + P2-5 second-pass: extract content-block events from the tail
229
- * (lines after the last user message), re-parsing only those lines.
230
- * `tailLines` is already the tail-only slice (read via
231
- * `readJsonlLinesFromOffset`), so both raw line-string memory and parsed-
232
- * object memory are O(tail), not O(whole file) — P2-6 + P2-5.
224
+ * P2-6 + P2-5 second-pass tail mapper: expand one parsed tail line into
225
+ * content-block events. Re-parses only tail lines (scanTailEvents does the
226
+ * offset re-read + line filtering), so both raw line-string memory and
227
+ * parsed-object memory are O(tail), not O(whole file) — P2-6 + P2-5.
228
+ * `maxEvents` capping is applied by scanTailEvents.
233
229
  */
234
- function extractPiEventsFromTail(tailLines) {
230
+ function mapPiTailLine(rawObj) {
235
231
  const events = [];
236
- for (const line of tailLines) {
237
- if (!line.trim())
238
- continue;
239
- try {
240
- const obj = JSON.parse(line);
241
- if (obj.type !== 'message' || !obj.message)
242
- continue;
243
- const message = obj.message;
244
- // Handle error messages: when stopReason is "error", content is empty
245
- // but errorMessage has details
246
- if (message.stopReason === 'error' && message.errorMessage) {
247
- events.push({
248
- type: 'error',
249
- content: `❌ ${message.errorMessage}`,
250
- timestamp: obj.timestamp,
251
- });
252
- continue;
253
- }
254
- const blocks = extractContentBlocks(message.content, PI_MAPPING);
255
- const role = message.role ?? 'unknown';
256
- for (const block of blocks) {
257
- events.push({ type: block.type, content: block.content, timestamp: obj.timestamp });
258
- }
259
- if (blocks.length === 0 && message.role) {
260
- events.push({ type: role, content: `(${role} event)`, timestamp: obj.timestamp });
261
- }
262
- }
263
- catch {
264
- /* skip */
265
- }
232
+ const obj = rawObj;
233
+ if (obj.type !== 'message' || !obj.message)
234
+ return events;
235
+ const message = obj.message;
236
+ // Handle error messages: when stopReason is "error", content is empty
237
+ // but errorMessage has details
238
+ if (message.stopReason === 'error' && message.errorMessage) {
239
+ events.push({
240
+ type: 'error',
241
+ content: `❌ ${message.errorMessage}`,
242
+ timestamp: obj.timestamp,
243
+ });
244
+ return events;
245
+ }
246
+ const blocks = extractContentBlocks(message.content, PI_MAPPING);
247
+ const role = message.role ?? 'unknown';
248
+ for (const block of blocks) {
249
+ events.push({ type: block.type, content: block.content, timestamp: obj.timestamp });
250
+ }
251
+ if (blocks.length === 0 && message.role) {
252
+ events.push({ type: role, content: `(${role} event)`, timestamp: obj.timestamp });
266
253
  }
267
254
  return events;
268
255
  }
@@ -303,29 +290,17 @@ export class PiSessionReader {
303
290
  if (fileCwd && fileCwd !== cwd) {
304
291
  return { events: [] };
305
292
  }
306
- // P2-5 + P2-6: First pass STREAMS the file once via `scanJsonlLines`
307
- // (no `string[]` materialized), parsing each line once to collect
308
- // scalars (usage, tailOffset, displayTitle) while retaining no parsed
309
- // objects and no whole-file line array. The tailOffset is the byte
310
- // offset where the tail begins (start of the line after the last user
311
- // message). A second pass re-reads ONLY the tail from that offset via
312
- // `readJsonlLinesFromOffset` and re-parses it for events — O(tail)
313
- // memory for both raw line strings and parsed objects, instead of
314
- // O(whole file).
315
- //
316
- // Parse ratio: ≈1.0–1.5× line count when a user message exists.
317
- // When the session has NO user message, tailOffset stays -1 and the
318
- // tail IS the whole file → scan(N) + tail(N) = 2.0× — the known
319
- // asymptotic upper bound of the two-phase design.
293
+ // P2-5 + P2-6: Two-pass scan (see `scanJsonlOnce` / `scanTailEvents` in
294
+ // session/common/two-pass.ts): pass 1 streams the file once collecting
295
+ // scalars (usage, tailOffset, displayTitle) with O(1) parsed-object
296
+ // retention; pass 2 re-reads ONLY the tail and re-parses it for events,
297
+ // capped at maxEvents (keeps the LAST N events, matching
298
+ // CodexSessionReader's slice(-maxEvents), so auto-resume cards don't
299
+ // load the entire session).
320
300
  const scan = piScalarScan(filePath);
321
- const tailLines = readJsonlLinesFromOffset(filePath, scan.tailOffset >= 0 ? scan.tailOffset : 0);
322
- const events = extractPiEventsFromTail(tailLines);
323
- // Apply maxEvents cap: keep the LAST N events (most recent), matching
324
- // CodexSessionReader's slice(-maxEvents). maxEvents limits the catch-up
325
- // tail so auto-resume cards don't load the entire session.
326
- const cappedEvents = capEvents(events, opts?.maxEvents);
301
+ const events = scanTailEvents(filePath, scan.tailOffset, mapPiTailLine, opts?.maxEvents);
327
302
  return {
328
- events: cappedEvents,
303
+ events,
329
304
  usage: scan.usage,
330
305
  displayTitle: scan.displayTitle,
331
306
  };
@@ -336,7 +311,7 @@ export class PiSessionReader {
336
311
  return false;
337
312
  try {
338
313
  const st = fs.statSync(filePath);
339
- return Date.now() - st.mtimeMs < STALE_MS;
314
+ return !isStale(st.mtimeMs);
340
315
  }
341
316
  catch {
342
317
  return false;
@@ -346,8 +321,8 @@ export class PiSessionReader {
346
321
  listSessionsByScan(cwd) {
347
322
  const cacheKey = `${this.sessionsDir}\u0000${cwd}`;
348
323
  const cached = piListCache.get(cacheKey);
349
- if (cached && Date.now() - cached.builtAt < PI_LIST_TTL_MS) {
350
- return cached.sessions;
324
+ if (cached) {
325
+ return cached;
351
326
  }
352
327
  const dir = projectDirForCwd(cwd, this.sessionsDir);
353
328
  let files;
@@ -381,14 +356,8 @@ export class PiSessionReader {
381
356
  }
382
357
  }
383
358
  // Same-mtime ties: secondary key keeps the full order deterministic.
384
- sessions.sort((a, b) => b.mtime - a.mtime || a.sessionId.localeCompare(b.sessionId));
385
- piListCache.set(cacheKey, { builtAt: Date.now(), sessions });
386
- // Bound the cache (map iteration order = insertion order).
387
- if (piListCache.size > PI_LIST_MAX_ENTRIES) {
388
- const oldest = piListCache.keys().next().value;
389
- if (oldest !== undefined)
390
- piListCache.delete(oldest);
391
- }
359
+ sortByRecencyDesc(sessions, (s) => s.mtime, (s) => s.sessionId);
360
+ piListCache.set(cacheKey, sessions);
392
361
  return sessions;
393
362
  }
394
363
  }
@@ -5,6 +5,5 @@
5
5
  * package installation (detect package manager + global install),
6
6
  * and the combined workflow for the /update command and --update CLI flag.
7
7
  */
8
- export { isNewer, checkLatestVersion, type VersionCheckResult, type UpdateCache, } from './version-check.js';
9
- export { runInstallLatest, type PackageManager, type InstallResult } from './install.js';
10
- export { formatUpdateHint } from './startup-hint.js';
8
+ export { isNewer, checkLatestVersion, formatUpdateHint, type VersionCheckResult, } from './version-check.js';
9
+ export { runInstallLatest, type InstallResult } from './install.js';
@@ -5,7 +5,6 @@
5
5
  * package installation (detect package manager + global install),
6
6
  * and the combined workflow for the /update command and --update CLI flag.
7
7
  */
8
- export { isNewer, checkLatestVersion, } from './version-check.js';
8
+ export { isNewer, checkLatestVersion, formatUpdateHint, } from './version-check.js';
9
9
  export { runInstallLatest } from './install.js';
10
- export { formatUpdateHint } from './startup-hint.js';
11
10
  //# sourceMappingURL=index.js.map
@@ -38,7 +38,8 @@ export declare function detectPackageManager(scriptPath?: string): PackageManage
38
38
  * Run the global install command to upgrade lark-remote to the latest version.
39
39
  *
40
40
  * @param opts.packageManager - Override detected package manager
41
- * @param opts.execFn - Override execFile for testing
41
+ * @param opts.execFn - Override exec for testing; default routes through
42
+ * platform/spawn.ts (cross-spawn, win32 .cmd shim safe)
42
43
  */
43
44
  export declare function runInstallLatest(opts?: {
44
45
  packageManager?: PackageManager | null;
@@ -1,6 +1,53 @@
1
- import { execFile, execFileSync } from 'node:child_process';
2
1
  import fs from 'node:fs';
3
2
  import { getLogger } from '../logger/index.js';
3
+ import { resolveExecutable } from '../platform/command.js';
4
+ import { currentPlatform, isWin32 } from '../platform/select.js';
5
+ import { spawnProcess, spawnProcessSync } from '../platform/spawn.js';
6
+ /**
7
+ * execFile 回调风格适配 cross-spawn(platform/spawn.ts)。
8
+ *
9
+ * npm/pnpm 在 Windows 是 .cmd 垫片,execFile 不经 shell 无法执行——Node 自
10
+ * CVE-2024-27980 收紧后对无 shell 启动 .cmd 直接抛错,win32 上 /update 会
11
+ * 永远失败。执行统一走 spawnProcess(cross-spawn 处理 PATHEXT + .cmd 经
12
+ * cmd.exe 受控执行),错误/退出码语义对齐 execFile:
13
+ * - 非 0 退出 → err.message 为 execFile 同款 "Command failed: ..." 形态
14
+ * (含 stderr 首段,EACCES 提示逻辑依赖它);
15
+ * - 'error' 事件(ENOENT 等)→ 原样回调。
16
+ */
17
+ function execViaSpawnProcess(file, args, options, callback) {
18
+ const proc = spawnProcess(file, args, {
19
+ timeout: options.timeout,
20
+ stdio: ['ignore', 'pipe', 'pipe'],
21
+ windowsHide: true,
22
+ });
23
+ let stdout = '';
24
+ let stderr = '';
25
+ let settled = false;
26
+ const finish = (error) => {
27
+ if (settled)
28
+ return;
29
+ settled = true;
30
+ callback(error, stdout, stderr);
31
+ };
32
+ proc.stdout?.setEncoding('utf8');
33
+ proc.stdout?.on('data', (chunk) => {
34
+ stdout += chunk;
35
+ });
36
+ proc.stderr?.setEncoding('utf8');
37
+ proc.stderr?.on('data', (chunk) => {
38
+ stderr += chunk;
39
+ });
40
+ proc.on('error', (err) => finish(err));
41
+ proc.on('close', (code, signal) => {
42
+ if (code === 0) {
43
+ finish(null);
44
+ return;
45
+ }
46
+ const detail = stderr.trim() || stdout.trim();
47
+ const how = signal ? ` terminated with ${signal}` : ` with exit code ${code ?? 'unknown'}`;
48
+ finish(new Error(`Command failed: ${file} ${args.join(' ')}${how}${detail ? `\n${detail}` : ''}`));
49
+ });
50
+ }
4
51
  /**
5
52
  * Infer the installing package manager from the real path of the running
6
53
  * script (e.g. `.../node_modules/lark-remote/dist/index.js`).
@@ -54,15 +101,16 @@ export function detectPackageManager(scriptPath) {
54
101
  if (inferred)
55
102
  return inferred;
56
103
  }
57
- // 3. which availability fallback
104
+ // 3. availability fallback: 纯 Node PATH 解析(win32 PATHEXT),不再依赖外部 which
105
+ // (Windows 无 which);win32 再用 where.exe 兜底罕见的解析失败场景(v2 §4.3)
58
106
  const candidates = ['npm', 'bun', 'pnpm'];
59
107
  for (const cmd of candidates) {
60
- try {
61
- execFileSync('which', [cmd], { stdio: 'pipe', timeout: 3000 });
108
+ if (resolveExecutable(cmd))
62
109
  return cmd;
63
- }
64
- catch {
65
- // Not found, try next
110
+ if (isWin32(currentPlatform)) {
111
+ const res = spawnProcessSync('where.exe', [cmd], { stdio: 'pipe', timeout: 3000 });
112
+ if (!res.error && res.status === 0)
113
+ return cmd;
66
114
  }
67
115
  }
68
116
  return null;
@@ -82,7 +130,8 @@ function getInstallCommand(pm) {
82
130
  * Run the global install command to upgrade lark-remote to the latest version.
83
131
  *
84
132
  * @param opts.packageManager - Override detected package manager
85
- * @param opts.execFn - Override execFile for testing
133
+ * @param opts.execFn - Override exec for testing; default routes through
134
+ * platform/spawn.ts (cross-spawn, win32 .cmd shim safe)
86
135
  */
87
136
  export function runInstallLatest(opts) {
88
137
  // If packageManager is explicitly null, skip detection and fail immediately.
@@ -104,7 +153,7 @@ export function runInstallLatest(opts) {
104
153
  });
105
154
  }
106
155
  const { cmd, args } = getInstallCommand(pm);
107
- const execFn = opts?.execFn ?? ((...args) => execFile(...args));
156
+ const execFn = opts?.execFn ?? execViaSpawnProcess;
108
157
  const logger = getLogger();
109
158
  logger.info(`[update] running: ${cmd} ${args.join(' ')}`);
110
159
  return new Promise((resolve) => {
@@ -29,3 +29,8 @@ export declare function checkLatestVersion(opts?: {
29
29
  bypassCache?: boolean;
30
30
  registryUrl?: string;
31
31
  }): Promise<VersionCheckResult>;
32
+ /**
33
+ * Format an update hint string for the startup notification.
34
+ * Returns null if no newer version is available.
35
+ */
36
+ export declare function formatUpdateHint(current: string, latest: string): string | null;
@@ -146,4 +146,13 @@ export async function checkLatestVersion(opts) {
146
146
  }
147
147
  return { current, latest };
148
148
  }
149
+ /**
150
+ * Format an update hint string for the startup notification.
151
+ * Returns null if no newer version is available.
152
+ */
153
+ export function formatUpdateHint(current, latest) {
154
+ if (!isNewer(current, latest))
155
+ return null;
156
+ return `📦 有新版本 ${latest} 可用(当前 ${current}),发送 /update 升级`;
157
+ }
149
158
  //# sourceMappingURL=version-check.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lark-remote",
3
- "version": "0.1.12",
3
+ "version": "0.2.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",
@@ -25,8 +25,9 @@
25
25
  ],
26
26
  "scripts": {
27
27
  "dev": "bun src/index.ts",
28
- "build": "rm -rf dist && tsc && chmod +x dist/index.js dist/cli.js",
29
- "typecheck": "tsc --noEmit",
28
+ "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc && node -e \"if(process.platform!=='win32'){const fs=require('fs');for(const f of ['dist/index.js','dist/cli.js'])fs.chmodSync(f,0o755)}\"",
29
+ "typecheck": "tsc --noEmit && tsc -p tsconfig.test-infra.json",
30
+ "typecheck:test-infra": "tsc -p tsconfig.test-infra.json",
30
31
  "lint": "eslint src tests",
31
32
  "lint:fix": "eslint src tests --fix",
32
33
  "format": "prettier --write 'src/**/*.ts' 'tests/**/*.ts'",
@@ -42,6 +43,7 @@
42
43
  "test:config": "vitest run --project infra-config",
43
44
  "test:order": "vitest run --project infra-order",
44
45
  "test:misc": "vitest run --project infra-misc",
46
+ "test:platform": "vitest run --project infra-platform",
45
47
  "test:claude": "vitest run --project agent-claude",
46
48
  "test:codex": "vitest run --project agent-codex",
47
49
  "test:opencode": "vitest run --project agent-opencode",
@@ -57,7 +59,7 @@
57
59
  "test:pi-full": "vitest run --project agent-pi --project infra-runner --project infra-session",
58
60
  "test:kimi-full": "vitest run --project agent-kimi --project infra-runner --project infra-session",
59
61
  "test:live": "vitest run --project live",
60
- "prepublishOnly": "tsc --noEmit && vitest run && rm -rf dist && tsc && chmod +x dist/index.js dist/cli.js",
62
+ "prepublishOnly": "npm run typecheck && vitest run && npm run build",
61
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)}\""
62
64
  },
63
65
  "publishConfig": {
@@ -94,16 +96,16 @@
94
96
  "@iarna/toml": "^2.2.5",
95
97
  "@larksuite/channel": "^0.3.0",
96
98
  "axios": "^1.18.0",
99
+ "cross-spawn": "^7.0.6",
97
100
  "form-data": "^4.0.6",
98
- "qrcode-terminal": "^0.12.0",
101
+ "qrcode-generator": "^2.0.4",
99
102
  "yaml": "^2.9.0",
100
103
  "zod": "^4.4.3"
101
104
  },
102
105
  "devDependencies": {
103
106
  "@eslint/js": "^10.0.1",
104
- "@types/form-data": "^2.5.2",
107
+ "@types/cross-spawn": "^6.0.6",
105
108
  "@types/node": "^26.1.2",
106
- "@types/qrcode-terminal": "^0.12.2",
107
109
  "@vitest/coverage-v8": "4.1.10",
108
110
  "eslint": "^10.7.0",
109
111
  "eslint-config-prettier": "^10.1.8",
@@ -1,5 +0,0 @@
1
- /**
2
- * Format an update hint string for the startup notification.
3
- * Returns null if no newer version is available.
4
- */
5
- export declare function formatUpdateHint(current: string, latest: string): string | null;
@@ -1,11 +0,0 @@
1
- import { isNewer } from './version-check.js';
2
- /**
3
- * Format an update hint string for the startup notification.
4
- * Returns null if no newer version is available.
5
- */
6
- export function formatUpdateHint(current, latest) {
7
- if (!isNewer(current, latest))
8
- return null;
9
- return `📦 有新版本 ${latest} 可用(当前 ${current}),发送 /update 升级`;
10
- }
11
- //# sourceMappingURL=startup-hint.js.map