lark-remote 0.5.4 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/dist/bridge/approval-coordinator.d.ts +18 -1
- package/dist/bridge/approval-coordinator.js +57 -10
- package/dist/bridge/index.js +90 -17
- package/dist/card/approval-render.js +18 -0
- package/dist/card/run-state.js +10 -5
- package/dist/config/index.d.ts +16 -0
- package/dist/config/index.js +46 -6
- package/dist/config/opencode-config.d.ts +3 -3
- package/dist/config/opencode-config.js +38 -22
- package/dist/inbound/authorize.d.ts +34 -0
- package/dist/inbound/authorize.js +31 -0
- package/dist/index.js +116 -63
- package/dist/instance-lock.d.ts +9 -0
- package/dist/instance-lock.js +113 -42
- package/dist/router/config/dsh.d.ts +51 -21
- package/dist/router/config/dsh.js +255 -66
- package/dist/router/config/opencode.js +16 -6
- package/dist/router/config/types.d.ts +6 -3
- package/dist/router/index.d.ts +1 -0
- package/dist/router/index.js +13 -5
- package/dist/runner/common/acp/base-acp-runner.d.ts +12 -0
- package/dist/runner/common/acp/base-acp-runner.js +83 -8
- package/dist/runner/common/acp/protocol-types.d.ts +10 -2
- package/dist/runner/common/acp/protocol-types.js +3 -1
- package/dist/runner/common/connection-based-runner.d.ts +15 -0
- package/dist/runner/common/connection-based-runner.js +40 -5
- package/dist/runner/common/jsonrpc/connection-manager.d.ts +15 -0
- package/dist/runner/common/jsonrpc/connection-manager.js +83 -50
- package/dist/runner/dsh/approval.d.ts +53 -0
- package/dist/runner/dsh/approval.js +161 -0
- package/dist/runner/dsh/auth.d.ts +36 -0
- package/dist/runner/dsh/auth.js +118 -0
- package/dist/runner/dsh/client.d.ts +161 -42
- package/dist/runner/dsh/client.js +551 -244
- package/dist/runner/dsh/daemon.d.ts +85 -0
- package/dist/runner/dsh/daemon.js +272 -0
- package/dist/runner/dsh/merge.d.ts +9 -0
- package/dist/runner/dsh/merge.js +51 -0
- package/dist/runner/dsh/runner.d.ts +94 -11
- package/dist/runner/dsh/runner.js +553 -67
- package/dist/runner/dsh/translator.d.ts +43 -3
- package/dist/runner/dsh/translator.js +115 -38
- package/dist/runner/dsh/types.d.ts +189 -27
- package/dist/runner/dsh/types.js +11 -5
- package/dist/runner/kimi/acp/runner.js +15 -3
- package/dist/runner/opencode/acp/error-monitor.d.ts +2 -2
- package/dist/runner/opencode/acp/error-monitor.js +2 -2
- package/dist/runner/opencode/acp/runner.d.ts +9 -0
- package/dist/runner/opencode/acp/runner.js +40 -1
- package/dist/runner/opencode/acp/translator.d.ts +4 -2
- package/dist/runner/opencode/acp/translator.js +12 -3
- package/dist/runner/pi/rpc/translator.d.ts +2 -0
- package/dist/runner/pi/rpc/translator.js +27 -1
- package/dist/runner/types.d.ts +34 -0
- package/dist/runner/types.js +24 -0
- package/dist/session/dsh/sessions.d.ts +45 -6
- package/dist/session/dsh/sessions.js +171 -27
- package/dist/session/opencode/sessions.d.ts +18 -7
- package/dist/session/opencode/sessions.js +294 -15
- package/dist/session/pi/sessions.js +6 -2
- package/package.json +3 -1
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OpencodeSessionReader: CLI-based session reader using `opencode session list`
|
|
2
|
+
* OpencodeSessionReader: CLI-based session reader using `opencode session list`
|
|
3
|
+
* and `opencode session export` (v2) / `opencode export` (v1).
|
|
3
4
|
*
|
|
4
5
|
* Design:
|
|
5
6
|
* - Uses official CLI commands instead of HTTP API.
|
|
6
7
|
* - `opencode session list --format json` returns sessions with `directory` field (first-class citizen).
|
|
7
|
-
* - `opencode export <sessionID>`
|
|
8
|
+
* - `opencode session export <sessionID>` (v2) or `opencode export <sessionID>`
|
|
9
|
+
* (v1) returns full session content.
|
|
8
10
|
* - Independent of runner (no setRunner circular dependency).
|
|
9
11
|
*
|
|
10
12
|
* Key features:
|
|
@@ -25,6 +27,60 @@ import { paginate, capEvents } from '../common/pagination.js';
|
|
|
25
27
|
import { sortByRecencyDesc } from '../common/recency.js';
|
|
26
28
|
import { TtlCache } from '../../common/ttl-cache.js';
|
|
27
29
|
import { getLogger } from '../../logger/index.js';
|
|
30
|
+
/** v2 CLI defaults `session list` to 100 entries; keep the reader's historical
|
|
31
|
+
* "all sessions in this cwd" contract by asking for a high explicit limit. */
|
|
32
|
+
const MAX_SESSION_LIST_COUNT = 10_000;
|
|
33
|
+
function isV2Export(data) {
|
|
34
|
+
if (data.info && 'location' in data.info)
|
|
35
|
+
return true;
|
|
36
|
+
const messages = data.messages;
|
|
37
|
+
if (!Array.isArray(messages))
|
|
38
|
+
return false;
|
|
39
|
+
return messages.some((m) => m !== null &&
|
|
40
|
+
typeof m === 'object' &&
|
|
41
|
+
typeof m.type === 'string' &&
|
|
42
|
+
!('info' in m));
|
|
43
|
+
}
|
|
44
|
+
/** v1 stores the cwd at `info.directory`; v2 moved it under `info.location`. */
|
|
45
|
+
function exportDirectory(data) {
|
|
46
|
+
const info = data.info;
|
|
47
|
+
return info?.directory ?? info?.location?.directory;
|
|
48
|
+
}
|
|
49
|
+
function parseToolInput(value) {
|
|
50
|
+
if (typeof value !== 'string')
|
|
51
|
+
return value ?? {};
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(value);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** v2 export tool state carries results in `content[]` (text parts) or
|
|
60
|
+
* `error.message`; `result` is model/tool-specific and only used as a
|
|
61
|
+
* last-resort string. */
|
|
62
|
+
function v2ToolOutput(state) {
|
|
63
|
+
if (!state)
|
|
64
|
+
return undefined;
|
|
65
|
+
const texts = [];
|
|
66
|
+
for (const item of Array.isArray(state.content) ? state.content : []) {
|
|
67
|
+
if (typeof item?.text === 'string') {
|
|
68
|
+
texts.push(item.text);
|
|
69
|
+
}
|
|
70
|
+
else if (item?.content?.type === 'text' && typeof item.content.text === 'string') {
|
|
71
|
+
texts.push(item.content.text);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (texts.length > 0)
|
|
75
|
+
return texts.join('\n');
|
|
76
|
+
if (typeof state.error === 'string')
|
|
77
|
+
return state.error;
|
|
78
|
+
if (state.error && typeof state.error.message === 'string')
|
|
79
|
+
return state.error.message;
|
|
80
|
+
if (typeof state.result === 'string')
|
|
81
|
+
return state.result;
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
28
84
|
export class OpencodeSessionReader {
|
|
29
85
|
binary;
|
|
30
86
|
cacheTtlMs;
|
|
@@ -33,7 +89,7 @@ export class OpencodeSessionReader {
|
|
|
33
89
|
/** cwd → 列表缓存(TTL 由 cacheTtlMs 决定)。 */
|
|
34
90
|
listCache;
|
|
35
91
|
constructor(opts = {}) {
|
|
36
|
-
this.binary = 'opencode';
|
|
92
|
+
this.binary = opts.binary ?? 'opencode';
|
|
37
93
|
this.cacheTtlMs = opts.cacheTtlMs ?? 10_000;
|
|
38
94
|
this.listCache = new TtlCache(this.cacheTtlMs);
|
|
39
95
|
this.captureExportOverride = opts.captureExport;
|
|
@@ -102,9 +158,13 @@ export class OpencodeSessionReader {
|
|
|
102
158
|
try {
|
|
103
159
|
// 与 listSessions 的过滤同口径(PathKit.samePath):export 里的 directory
|
|
104
160
|
// 形态由 opencode 自行记录,裸 !== 会让会话列得出却读不出(win32 常态)
|
|
105
|
-
|
|
161
|
+
const directory = exportDirectory(data);
|
|
162
|
+
if (!directory || !samePath(directory, realCwd)) {
|
|
106
163
|
return { events: [] };
|
|
107
164
|
}
|
|
165
|
+
if (isV2Export(data)) {
|
|
166
|
+
return this.readV2SessionContent(data, opts);
|
|
167
|
+
}
|
|
108
168
|
const events = [];
|
|
109
169
|
let usage;
|
|
110
170
|
let aiTitle;
|
|
@@ -276,6 +336,163 @@ export class OpencodeSessionReader {
|
|
|
276
336
|
return { events: [] };
|
|
277
337
|
}
|
|
278
338
|
}
|
|
339
|
+
/**
|
|
340
|
+
* v2 export: messages are flat (`type` + `text`/`content`) and assistant
|
|
341
|
+
* usage lives on the message instead of a `step-finish` part. Keep this
|
|
342
|
+
* mapping isolated from the v1 parser so v1 behaviour stays unchanged.
|
|
343
|
+
*/
|
|
344
|
+
readV2SessionContent(data, opts) {
|
|
345
|
+
const messages = Array.isArray(data.messages) ? data.messages : [];
|
|
346
|
+
const events = [];
|
|
347
|
+
const aiTitle = typeof data.info.title === 'string' && data.info.title.length > 0
|
|
348
|
+
? data.info.title
|
|
349
|
+
: undefined;
|
|
350
|
+
let displayTitle;
|
|
351
|
+
let lastUserIndex = -1;
|
|
352
|
+
for (let i = 0; i < messages.length; i++) {
|
|
353
|
+
if (messages[i]?.type === 'user')
|
|
354
|
+
lastUserIndex = i;
|
|
355
|
+
}
|
|
356
|
+
const startIndex = lastUserIndex >= 0 ? lastUserIndex : 0;
|
|
357
|
+
for (let i = startIndex; i < messages.length; i++) {
|
|
358
|
+
const msg = messages[i];
|
|
359
|
+
if (!msg || typeof msg.type !== 'string')
|
|
360
|
+
continue;
|
|
361
|
+
const timestamp = msg.time?.created ? new Date(msg.time.created).toISOString() : undefined;
|
|
362
|
+
if (msg.type === 'user') {
|
|
363
|
+
if (typeof msg.text === 'string' && msg.text.length > 0) {
|
|
364
|
+
if (!displayTitle)
|
|
365
|
+
displayTitle = msg.text;
|
|
366
|
+
events.push({ type: 'text', content: msg.text, timestamp });
|
|
367
|
+
}
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (msg.type === 'assistant') {
|
|
371
|
+
for (const part of Array.isArray(msg.content) ? msg.content : []) {
|
|
372
|
+
if (part?.type === 'text' && typeof part.text === 'string') {
|
|
373
|
+
events.push({ type: 'text', content: part.text, timestamp });
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
if (part?.type === 'reasoning' && typeof part.text === 'string') {
|
|
377
|
+
events.push({ type: 'thinking', content: part.text, timestamp });
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if (part?.type === 'tool') {
|
|
381
|
+
const toolId = part.id;
|
|
382
|
+
const toolName = part.name ?? '';
|
|
383
|
+
const input = parseToolInput(part.state?.input);
|
|
384
|
+
if (toolId) {
|
|
385
|
+
events.push({
|
|
386
|
+
type: 'tool_use',
|
|
387
|
+
content: JSON.stringify({ id: toolId, name: toolName, input }),
|
|
388
|
+
timestamp,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
const output = v2ToolOutput(part.state);
|
|
392
|
+
if (toolId && output !== undefined) {
|
|
393
|
+
events.push({
|
|
394
|
+
type: 'tool_result',
|
|
395
|
+
content: truncateUtf8(output, TOOL_RESULT_MAX_BYTES),
|
|
396
|
+
timestamp,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
// User-invoked shell messages are a first-class message type in v2.
|
|
404
|
+
if (msg.type === 'shell' && typeof msg.command === 'string') {
|
|
405
|
+
events.push({
|
|
406
|
+
type: 'tool_use',
|
|
407
|
+
content: JSON.stringify({
|
|
408
|
+
id: msg.callID ?? msg.id ?? 'shell',
|
|
409
|
+
name: 'bash',
|
|
410
|
+
input: { command: msg.command },
|
|
411
|
+
}),
|
|
412
|
+
timestamp,
|
|
413
|
+
});
|
|
414
|
+
if (typeof msg.output === 'string') {
|
|
415
|
+
events.push({
|
|
416
|
+
type: 'tool_result',
|
|
417
|
+
content: truncateUtf8(msg.output, TOOL_RESULT_MAX_BYTES),
|
|
418
|
+
timestamp,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
// Keep the compact summary visible in the catch-up tail; other control
|
|
424
|
+
// messages (system/synthetic/agent-switched/model-switched) are noise.
|
|
425
|
+
if (msg.type === 'compaction' && typeof msg.summary === 'string' && msg.summary) {
|
|
426
|
+
events.push({ type: 'text', content: msg.summary, timestamp });
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
// If the last user message had no text (e.g. attachment-only), fall back to
|
|
430
|
+
// the first human text anywhere in the session for the card title.
|
|
431
|
+
if (!displayTitle) {
|
|
432
|
+
for (const msg of messages) {
|
|
433
|
+
if (msg?.type === 'user' && typeof msg.text === 'string' && msg.text.length > 0) {
|
|
434
|
+
displayTitle = msg.text;
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
let latest;
|
|
440
|
+
let cumInput = 0;
|
|
441
|
+
let cumOutput = 0;
|
|
442
|
+
let cumTotal = 0;
|
|
443
|
+
let cumCacheRead = 0;
|
|
444
|
+
let cumCacheWrite = 0;
|
|
445
|
+
for (const msg of messages) {
|
|
446
|
+
if (msg?.type !== 'assistant' || !msg.tokens)
|
|
447
|
+
continue;
|
|
448
|
+
const tokens = msg.tokens;
|
|
449
|
+
const cacheRead = tokens.cache?.read ?? 0;
|
|
450
|
+
const cacheWrite = tokens.cache?.write ?? 0;
|
|
451
|
+
const total = tokens.input + tokens.output + tokens.reasoning + cacheRead + cacheWrite;
|
|
452
|
+
cumInput += tokens.input;
|
|
453
|
+
cumOutput += tokens.output;
|
|
454
|
+
cumTotal += total;
|
|
455
|
+
cumCacheRead += cacheRead;
|
|
456
|
+
cumCacheWrite += cacheWrite;
|
|
457
|
+
// A long autonomous run can end on a degenerate assistant message with
|
|
458
|
+
// zero tokens; skip it so the card reflects the last real step.
|
|
459
|
+
if (total !== 0) {
|
|
460
|
+
latest = {
|
|
461
|
+
inputTokens: tokens.input,
|
|
462
|
+
outputTokens: tokens.output,
|
|
463
|
+
contextLength: tokens.input + cacheRead + cacheWrite,
|
|
464
|
+
cacheReadTokens: cacheRead,
|
|
465
|
+
cacheCreationTokens: cacheWrite,
|
|
466
|
+
totalTokens: total,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (latest) {
|
|
471
|
+
latest.cumulativeTotalTokens = cumTotal;
|
|
472
|
+
latest.cumulativeInputTokens = cumInput;
|
|
473
|
+
latest.cumulativeOutputTokens = cumOutput;
|
|
474
|
+
latest.cumulativeCacheReadTokens = cumCacheRead;
|
|
475
|
+
latest.cumulativeCacheCreationTokens = cumCacheWrite;
|
|
476
|
+
}
|
|
477
|
+
else if (cumInput || cumOutput || cumTotal || cumCacheRead || cumCacheWrite) {
|
|
478
|
+
latest = {
|
|
479
|
+
inputTokens: 0,
|
|
480
|
+
outputTokens: 0,
|
|
481
|
+
contextLength: 0,
|
|
482
|
+
cumulativeTotalTokens: cumTotal,
|
|
483
|
+
cumulativeInputTokens: cumInput,
|
|
484
|
+
cumulativeOutputTokens: cumOutput,
|
|
485
|
+
cumulativeCacheReadTokens: cumCacheRead,
|
|
486
|
+
cumulativeCacheCreationTokens: cumCacheWrite,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
return {
|
|
490
|
+
events: capEvents(events, opts?.maxEvents),
|
|
491
|
+
usage: latest,
|
|
492
|
+
aiTitle,
|
|
493
|
+
displayTitle,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
279
496
|
/**
|
|
280
497
|
* Check if a session is active (updated within 1 hour).
|
|
281
498
|
* Passes cwd to fetchSessionList so it returns sessions for the correct directory.
|
|
@@ -315,8 +532,14 @@ export class OpencodeSessionReader {
|
|
|
315
532
|
// 的 .cmd 垫片,win32 上 execFileSync 不做 PATHEXT 解析会直接 ENOENT,
|
|
316
533
|
// 会话列表永远为空;cross-spawn 走与 runner spawn 相同的垫片执行路径
|
|
317
534
|
// (posix 为 child_process.spawnSync 直通,行为不变)。
|
|
318
|
-
// Pass cwd to
|
|
319
|
-
|
|
535
|
+
// Pass cwd to the CLI so opencode returns sessions for the correct directory.
|
|
536
|
+
//
|
|
537
|
+
// v2 runs CLI commands through a shared daemon and defaults the list to
|
|
538
|
+
// 100 entries. `--standalone` keeps this read-only path independent from
|
|
539
|
+
// the user's daemon state; `--max-count` preserves the reader's "all
|
|
540
|
+
// sessions in this cwd" contract. v1 accepts the same list/flags but not
|
|
541
|
+
// `--standalone`, so a failed first attempt falls back to the bare form.
|
|
542
|
+
const listOpts = {
|
|
320
543
|
encoding: 'utf-8',
|
|
321
544
|
timeout: 10000,
|
|
322
545
|
// default maxBuffer is 1MiB — a large session list (>1MiB JSON)
|
|
@@ -325,7 +548,31 @@ export class OpencodeSessionReader {
|
|
|
325
548
|
maxBuffer: 64 * 1024 * 1024,
|
|
326
549
|
cwd: realCwd, // Key fix: pass user cwd to CLI
|
|
327
550
|
windowsHide: true, // opencode 是 npm .cmd 垫片:不隐藏会闪 cmd.exe 控制台
|
|
328
|
-
}
|
|
551
|
+
};
|
|
552
|
+
const v2Args = [
|
|
553
|
+
'session',
|
|
554
|
+
'list',
|
|
555
|
+
'--format',
|
|
556
|
+
'json',
|
|
557
|
+
'--max-count',
|
|
558
|
+
String(MAX_SESSION_LIST_COUNT),
|
|
559
|
+
'--standalone',
|
|
560
|
+
];
|
|
561
|
+
const v1Args = [
|
|
562
|
+
'session',
|
|
563
|
+
'list',
|
|
564
|
+
'--format',
|
|
565
|
+
'json',
|
|
566
|
+
'--max-count',
|
|
567
|
+
String(MAX_SESSION_LIST_COUNT),
|
|
568
|
+
];
|
|
569
|
+
let res = spawnProcessSync(this.binary, v2Args, listOpts);
|
|
570
|
+
// `--standalone` is v2-only and can also fail if the private server is
|
|
571
|
+
// temporarily unavailable. Retry once without it: this is the exact v1
|
|
572
|
+
// command shape and also uses the shared daemon on v2.
|
|
573
|
+
if (res.error || res.status !== 0) {
|
|
574
|
+
res = spawnProcessSync(this.binary, v1Args, listOpts);
|
|
575
|
+
}
|
|
329
576
|
if (res.error) {
|
|
330
577
|
const enoent = res.error.code === 'ENOENT';
|
|
331
578
|
if (enoent)
|
|
@@ -362,7 +609,7 @@ export class OpencodeSessionReader {
|
|
|
362
609
|
}
|
|
363
610
|
}
|
|
364
611
|
/**
|
|
365
|
-
* Capture raw
|
|
612
|
+
* Capture raw export output (JSON string). Injectable
|
|
366
613
|
* via the constructor; the default routes stdout to a temp file fd to bypass
|
|
367
614
|
* opencode's ~128KB pipe truncation on large sessions.
|
|
368
615
|
*/
|
|
@@ -373,27 +620,59 @@ export class OpencodeSessionReader {
|
|
|
373
620
|
return this.captureExportViaFileFd(sessionId);
|
|
374
621
|
}
|
|
375
622
|
/**
|
|
376
|
-
* default transport: run
|
|
623
|
+
* default transport: run the version-appropriate export command with stdout
|
|
624
|
+
* pointed at a
|
|
377
625
|
* regular-file fd (NOT a pipe). opencode truncates piped stdout at ~128KB for
|
|
378
626
|
* large sessions (huge tool_results), breaking JSON.parse; a file fd receives
|
|
379
|
-
* the full output (verified: 1.3MB parses OK).
|
|
380
|
-
* stdout is
|
|
627
|
+
* the full output (verified: 1.3MB parses OK). spawnProcessSync returns null
|
|
628
|
+
* stdout when it is redirected to a file fd, so we read the file instead.
|
|
381
629
|
*/
|
|
382
630
|
captureExportViaFileFd(sessionId) {
|
|
383
631
|
const tmp = path.join(os.tmpdir(), `opencode-export-${sessionId}-${process.pid}-${Date.now()}.json`);
|
|
384
632
|
let fd;
|
|
385
633
|
try {
|
|
386
|
-
|
|
387
|
-
spawnProcessSync(this.binary, ['export', sessionId], {
|
|
634
|
+
const exportOpts = {
|
|
388
635
|
encoding: 'utf-8',
|
|
389
636
|
timeout: 30000,
|
|
390
|
-
stdio: ['ignore', fd, 'pipe'],
|
|
391
637
|
// stderr is piped here; without maxBuffer a chatty CLI (>1MiB
|
|
392
638
|
// stderr) would throw ENOBUFS and lose the export. Keep the default
|
|
393
639
|
// stdout file-fd transport unaffected by raising the pipe cap.
|
|
394
640
|
maxBuffer: 64 * 1024 * 1024,
|
|
395
641
|
windowsHide: true,
|
|
396
|
-
}
|
|
642
|
+
};
|
|
643
|
+
const run = (args) => {
|
|
644
|
+
if (fd !== undefined) {
|
|
645
|
+
try {
|
|
646
|
+
fs.closeSync(fd);
|
|
647
|
+
}
|
|
648
|
+
catch { }
|
|
649
|
+
}
|
|
650
|
+
fd = fs.openSync(tmp, 'w');
|
|
651
|
+
return spawnProcessSync(this.binary, args, {
|
|
652
|
+
...exportOpts,
|
|
653
|
+
stdio: ['ignore', fd, 'pipe'],
|
|
654
|
+
});
|
|
655
|
+
};
|
|
656
|
+
// v2 moved export under `session`; v1 only has the top-level command.
|
|
657
|
+
let res = run(['session', 'export', '--standalone', sessionId]);
|
|
658
|
+
if (res.error || res.status !== 0) {
|
|
659
|
+
// v2 without a private server may still work through the shared
|
|
660
|
+
// daemon; on v1 this is another unknown-subcommand response.
|
|
661
|
+
res = run(['session', 'export', sessionId]);
|
|
662
|
+
}
|
|
663
|
+
// Last resort: v1's top-level command. Skip only when the server has
|
|
664
|
+
// already told us the session does not exist (a syntax fallback cannot
|
|
665
|
+
// help and would only add another spawn).
|
|
666
|
+
if ((res.error || res.status !== 0) && !/Session not found/i.test(String(res.stderr ?? ''))) {
|
|
667
|
+
res = run(['export', sessionId]);
|
|
668
|
+
}
|
|
669
|
+
if (res.error) {
|
|
670
|
+
throw res.error;
|
|
671
|
+
}
|
|
672
|
+
if (res.status !== 0) {
|
|
673
|
+
const stderr = String(res.stderr ?? '').trim();
|
|
674
|
+
getLogger().warn(`[opencode-session-reader] session export exited ${res.status}${stderr ? `: ${stderr}` : ''}`);
|
|
675
|
+
}
|
|
397
676
|
return fs.readFileSync(tmp, 'utf-8');
|
|
398
677
|
}
|
|
399
678
|
finally {
|
|
@@ -124,8 +124,12 @@ function findSessionFile(sessionId, cwd, sessionsDir) {
|
|
|
124
124
|
try {
|
|
125
125
|
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.jsonl'));
|
|
126
126
|
for (const f of files) {
|
|
127
|
-
// pi filenames: <timestamp>_<uuid>.jsonl —
|
|
128
|
-
|
|
127
|
+
// pi filenames: <timestamp>_<uuid>.jsonl — the session id is the exact
|
|
128
|
+
// uuid suffix. `includes()` would also match an id that is a prefix /
|
|
129
|
+
// substring of another session's uuid, so
|
|
130
|
+
// require the trailing `_<id>.jsonl` form (files without the timestamp
|
|
131
|
+
// prefix keep the plain `<id>.jsonl` form).
|
|
132
|
+
if (f === `${sessionId}.jsonl` || f.endsWith(`_${sessionId}.jsonl`)) {
|
|
129
133
|
return path.join(dir, f);
|
|
130
134
|
}
|
|
131
135
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lark-remote",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
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",
|
|
@@ -99,6 +99,7 @@
|
|
|
99
99
|
"cross-spawn": "^7.0.6",
|
|
100
100
|
"form-data": "^4.0.6",
|
|
101
101
|
"qrcode-generator": "^2.0.4",
|
|
102
|
+
"ws": "^8.21.2",
|
|
102
103
|
"yaml": "^2.9.0",
|
|
103
104
|
"zod": "^4.4.3"
|
|
104
105
|
},
|
|
@@ -106,6 +107,7 @@
|
|
|
106
107
|
"@eslint/js": "^10.0.1",
|
|
107
108
|
"@types/cross-spawn": "^6.0.6",
|
|
108
109
|
"@types/node": "^26.1.2",
|
|
110
|
+
"@types/ws": "^8.18.1",
|
|
109
111
|
"@vitest/coverage-v8": "4.1.10",
|
|
110
112
|
"eslint": "^10.7.0",
|
|
111
113
|
"eslint-config-prettier": "^10.1.8",
|