lark-remote 0.1.13 → 0.2.1
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 +36 -3
- package/README.en.md +7 -11
- package/README.md +7 -11
- package/dist/bridge/final-usage.d.ts +24 -0
- package/dist/bridge/final-usage.js +55 -0
- package/dist/bridge/index.d.ts +0 -7
- package/dist/bridge/index.js +22 -64
- package/dist/bridge/queue-manager.js +4 -3
- package/dist/card/approval-render.js +39 -94
- package/dist/card/bash-renderer.js +12 -21
- package/dist/card/card-shared.d.ts +13 -1
- package/dist/card/card-shared.js +21 -30
- package/dist/card/run-renderer.d.ts +0 -22
- package/dist/card/run-renderer.js +206 -153
- package/dist/card/run-state.d.ts +29 -1
- package/dist/card/run-state.js +172 -84
- package/dist/card/text-truncate.d.ts +0 -9
- package/dist/card/text-truncate.js +0 -9
- package/dist/card/tool-render.d.ts +4 -1
- package/dist/card/tool-render.js +76 -32
- package/dist/cli.js +2 -2
- package/dist/common/fs.d.ts +14 -0
- package/dist/common/fs.js +57 -3
- package/dist/config/codex-config.js +2 -0
- package/dist/config/index.d.ts +0 -5
- package/dist/config/index.js +0 -11
- package/dist/config/kimi-config.js +1 -0
- package/dist/config/opencode-config.js +13 -2
- package/dist/config/qr.d.ts +36 -0
- package/dist/config/qr.js +124 -0
- package/dist/config/wizard.js +69 -27
- package/dist/connector/index.d.ts +3 -2
- package/dist/connector/index.js +5 -4
- package/dist/index.js +15 -42
- package/dist/instance-lock.js +7 -0
- package/dist/persistence/atomic-write.d.ts +2 -0
- package/dist/persistence/atomic-write.js +20 -5
- package/dist/persistence/load-json-file.d.ts +1 -2
- package/dist/persistence/load-json-file.js +2 -3
- package/dist/platform/agent-stopper.d.ts +22 -0
- package/dist/platform/agent-stopper.js +19 -0
- package/dist/platform/command.d.ts +11 -0
- package/dist/platform/command.js +107 -0
- package/dist/platform/identity.d.ts +33 -0
- package/dist/platform/identity.js +187 -0
- package/dist/platform/path.d.ts +24 -0
- package/dist/platform/path.js +110 -0
- package/dist/platform/select.d.ts +9 -0
- package/dist/platform/select.js +12 -0
- package/dist/platform/shell.d.ts +51 -0
- package/dist/platform/shell.js +75 -0
- package/dist/platform/spawn.d.ts +41 -0
- package/dist/platform/spawn.js +77 -0
- package/dist/platform/terminator-posix.d.ts +38 -0
- package/dist/platform/terminator-posix.js +103 -0
- package/dist/platform/terminator-win32.d.ts +21 -0
- package/dist/platform/terminator-win32.js +110 -0
- package/dist/platform/terminator.d.ts +44 -0
- package/dist/platform/terminator.js +24 -0
- package/dist/platform/types.d.ts +23 -0
- package/dist/platform/types.js +5 -0
- package/dist/restart.js +4 -2
- package/dist/router/card-helpers.d.ts +33 -6
- package/dist/router/card-helpers.js +67 -11
- package/dist/router/config/common/render.d.ts +1 -1
- package/dist/router/config/common/render.js +1 -1
- package/dist/router/config-diff.d.ts +4 -0
- package/dist/router/config-diff.js +70 -0
- package/dist/router/index.d.ts +66 -6
- package/dist/router/index.js +451 -316
- package/dist/router/utils.d.ts +6 -0
- package/dist/router/utils.js +12 -0
- package/dist/runner/bash/runner.d.ts +9 -2
- package/dist/runner/bash/runner.js +45 -20
- package/dist/runner/claude/session.d.ts +4 -1
- package/dist/runner/claude/session.js +18 -4
- package/dist/runner/codex/app-server/protocol-types.d.ts +35 -1
- package/dist/runner/codex/app-server/translator.d.ts +18 -0
- package/dist/runner/codex/app-server/translator.js +146 -0
- package/dist/runner/common/acp/base-acp-runner.d.ts +116 -0
- package/dist/runner/common/acp/base-acp-runner.js +248 -0
- package/dist/runner/common/acp/base-translator.d.ts +141 -0
- package/dist/runner/common/acp/base-translator.js +271 -0
- package/dist/runner/common/acp/protocol-helpers.d.ts +39 -0
- package/dist/runner/common/acp/protocol-helpers.js +34 -0
- package/dist/runner/common/jsonrpc/transport.d.ts +2 -0
- package/dist/runner/common/jsonrpc/transport.js +25 -5
- package/dist/runner/common/process-stopper.d.ts +4 -26
- package/dist/runner/common/process-stopper.js +4 -76
- package/dist/runner/common/runner-utils.d.ts +0 -5
- package/dist/runner/common/runner-utils.js +0 -7
- package/dist/runner/common/spawning-runner.d.ts +42 -132
- package/dist/runner/common/spawning-runner.js +52 -271
- package/dist/runner/common/usage.d.ts +2 -0
- package/dist/runner/common/usage.js +2 -0
- package/dist/runner/index.d.ts +1 -1
- package/dist/runner/kimi/acp/runner.d.ts +45 -42
- package/dist/runner/kimi/acp/runner.js +140 -293
- package/dist/runner/kimi/acp/translator.d.ts +18 -100
- package/dist/runner/kimi/acp/translator.js +56 -242
- package/dist/runner/opencode/acp/runner.d.ts +22 -46
- package/dist/runner/opencode/acp/runner.js +73 -256
- package/dist/runner/opencode/acp/translator.d.ts +28 -103
- package/dist/runner/opencode/acp/translator.js +51 -236
- package/dist/runner/pi/rpc/translator.d.ts +3 -0
- package/dist/runner/pi/rpc/translator.js +47 -9
- package/dist/runner/probe.d.ts +8 -11
- package/dist/runner/probe.js +17 -21
- package/dist/runner/registry.d.ts +4 -4
- package/dist/runner/registry.js +4 -4
- package/dist/runner/types.d.ts +38 -1
- package/dist/session/claude/sessions.js +35 -58
- package/dist/session/codex/rollout-reader.d.ts +10 -0
- package/dist/session/codex/rollout-reader.js +123 -41
- package/dist/session/common/constants.d.ts +7 -0
- package/dist/session/common/constants.js +9 -0
- package/dist/session/common/two-pass.d.ts +22 -0
- package/dist/session/common/two-pass.js +70 -0
- package/dist/session/dsh/sessions.js +7 -6
- package/dist/session/kimi/sessions.d.ts +6 -0
- package/dist/session/kimi/sessions.js +50 -73
- package/dist/session/opencode/sessions.js +26 -8
- package/dist/session/pi/sessions.js +54 -73
- package/dist/update/index.d.ts +1 -2
- package/dist/update/index.js +1 -2
- package/dist/update/install.d.ts +2 -1
- package/dist/update/install.js +58 -9
- package/dist/update/version-check.d.ts +5 -0
- package/dist/update/version-check.js +9 -0
- package/package.json +9 -6
- package/dist/update/startup-hint.d.ts +0 -5
- package/dist/update/startup-hint.js +0 -11
|
@@ -12,18 +12,19 @@
|
|
|
12
12
|
* readSessionContent: session.history → map SessionEvents to display events +
|
|
13
13
|
* session-wide usage accumulation (assistant/message.usage).
|
|
14
14
|
*/
|
|
15
|
-
import {
|
|
15
|
+
import { spawnProcessSync } from '../../platform/spawn.js';
|
|
16
16
|
import { cumulativeUsageFields } from '../common/usage-accumulator.js';
|
|
17
17
|
import { sortByRecencyDesc } from '../common/recency.js';
|
|
18
18
|
import { getLogger } from '../../logger/index.js';
|
|
19
19
|
import { DEFAULT_DSH_HOST } from '../../config/index.js';
|
|
20
|
+
import { samePath } from '../../platform/path.js';
|
|
20
21
|
import { capEvents, paginate } from '../common/pagination.js';
|
|
21
22
|
import { truncateUtf8, TOOL_RESULT_MAX_BYTES } from '../../common/truncate.js';
|
|
22
23
|
/**
|
|
23
24
|
* CC-05: history 拉取上限。DSH session.history 的 maxMessages 同时限制返回条数,
|
|
24
25
|
* 若用它同时充当展示截断,>50 事件的会话累计 usage 会少算。这里拉取一个足够大的
|
|
25
26
|
* 上限保证 usage 累加覆盖整个会话;展示仍由 capEvents(opts?.maxEvents ?? 50) 截断。
|
|
26
|
-
* (若 DSH 支持分页 cursor
|
|
27
|
+
* (若 DSH 支持分页 cursor 可改成分页,当前用有界大拉取)
|
|
27
28
|
*/
|
|
28
29
|
const DSH_HISTORY_FETCH_LIMIT = 10_000;
|
|
29
30
|
/** Synchronous unary POST via curl (the reader interface is synchronous). */
|
|
@@ -34,7 +35,7 @@ function defaultSyncRequest(baseUrl, method, payload) {
|
|
|
34
35
|
method,
|
|
35
36
|
payload,
|
|
36
37
|
});
|
|
37
|
-
const res =
|
|
38
|
+
const res = spawnProcessSync('curl', [
|
|
38
39
|
'-s',
|
|
39
40
|
'-X',
|
|
40
41
|
'POST',
|
|
@@ -49,7 +50,7 @@ function defaultSyncRequest(baseUrl, method, payload) {
|
|
|
49
50
|
}
|
|
50
51
|
let parsed;
|
|
51
52
|
try {
|
|
52
|
-
parsed = JSON.parse(res.stdout);
|
|
53
|
+
parsed = JSON.parse(String(res.stdout));
|
|
53
54
|
}
|
|
54
55
|
catch (err) {
|
|
55
56
|
throw new Error(`DSH ${method} returned non-JSON: ${err.message}`, {
|
|
@@ -82,7 +83,7 @@ export class DshSessionReader {
|
|
|
82
83
|
const value = this.unary('session.list', {});
|
|
83
84
|
const items = value.items ?? [];
|
|
84
85
|
const filtered = items
|
|
85
|
-
.filter((i) => i.cwd
|
|
86
|
+
.filter((i) => i.cwd !== undefined && samePath(i.cwd, cwd))
|
|
86
87
|
.map((i) => ({
|
|
87
88
|
sessionId: String(i.sessionId),
|
|
88
89
|
summary: '',
|
|
@@ -173,7 +174,7 @@ export class DshSessionReader {
|
|
|
173
174
|
isSessionActive(sessionId, cwd) {
|
|
174
175
|
try {
|
|
175
176
|
const value = this.unary('session.list', {});
|
|
176
|
-
const found = (value.items ?? []).find((i) => String(i.sessionId) === sessionId && i.cwd
|
|
177
|
+
const found = (value.items ?? []).find((i) => String(i.sessionId) === sessionId && i.cwd !== undefined && samePath(i.cwd, cwd));
|
|
177
178
|
return found?.running === true;
|
|
178
179
|
}
|
|
179
180
|
catch (error) {
|
|
@@ -117,6 +117,12 @@ export declare class KimiSessionReader implements AgentSessionReader {
|
|
|
117
117
|
inFlightSince?: number;
|
|
118
118
|
records: KimiCompactionRecord[];
|
|
119
119
|
};
|
|
120
|
+
/**
|
|
121
|
+
* W2.5 单源:resolveSessionWirePath 与 isSessionActive 共用的 cwd 守卫
|
|
122
|
+
* (realpathSync → state.json workDir/cwd 三态 → index workDir 兜底 →
|
|
123
|
+
* fail-closed)。返回 sessionDir;会话未知或守卫不过返回 null。
|
|
124
|
+
*/
|
|
125
|
+
private verifySessionCwd;
|
|
120
126
|
/**
|
|
121
127
|
* Resolve the main wire.jsonl path for a session after passing the cwd
|
|
122
128
|
* guard (state.json + session index fallback). Returns null when the
|
|
@@ -4,7 +4,8 @@ import os from 'node:os';
|
|
|
4
4
|
import { readJsonlLines as readJsonlLinesShared, readLastNJsonlLines } from '../common/jsonl.js';
|
|
5
5
|
import { UsageAccumulator, cumulativeUsageFields } from '../common/usage-accumulator.js';
|
|
6
6
|
import { getLogger } from '../../logger/index.js';
|
|
7
|
-
import {
|
|
7
|
+
import { isStale } from '../common/constants.js';
|
|
8
|
+
import { samePath } from '../../platform/path.js';
|
|
8
9
|
import { capEvents, paginate } from '../common/pagination.js';
|
|
9
10
|
import { sortByRecencyDesc } from '../common/recency.js';
|
|
10
11
|
import { TtlCache } from '../../common/ttl-cache.js';
|
|
@@ -124,7 +125,7 @@ export function checkCwdGuard(state, realCwd) {
|
|
|
124
125
|
const sessionWorkDir = extractWorkDir(state);
|
|
125
126
|
if (sessionWorkDir === undefined)
|
|
126
127
|
return 'unverifiable';
|
|
127
|
-
if (sessionWorkDir
|
|
128
|
+
if (samePath(sessionWorkDir, realCwd))
|
|
128
129
|
return 'verified';
|
|
129
130
|
return 'failed';
|
|
130
131
|
}
|
|
@@ -209,35 +210,36 @@ export class KimiSessionReader {
|
|
|
209
210
|
const realCwd = fs.realpathSync(cwd);
|
|
210
211
|
const sessions = [];
|
|
211
212
|
for (const entry of this.scanSessionIndex()) {
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
213
|
+
// W2.7:state.json 一次读取共用(cwd 兜底 + summary),I/O 减半。
|
|
214
|
+
// 两次 catch 语义保留:解析失败 = 无法判 cwd → 跳过(continue);
|
|
215
|
+
// 解析成功但字段缺失 = summary 落 wire.jsonl 提取。
|
|
216
|
+
const statePath = path.join(entry.sessionDir, 'state.json');
|
|
217
|
+
let state = null;
|
|
218
|
+
let stateParsed = false;
|
|
219
|
+
if (fs.existsSync(statePath)) {
|
|
217
220
|
try {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
entryWorkDir = extractWorkDir(state);
|
|
221
|
-
}
|
|
221
|
+
state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
222
|
+
stateParsed = true;
|
|
222
223
|
}
|
|
223
224
|
catch {
|
|
224
|
-
//
|
|
225
|
+
// Corrupt state.json — cannot determine cwd → skip below
|
|
225
226
|
}
|
|
226
227
|
}
|
|
228
|
+
// Index workDir is v1-only; v2 sessions may have empty/missing workDir.
|
|
229
|
+
// When index workDir is absent, fall back to state.json's cwd field.
|
|
230
|
+
let entryWorkDir = entry.workDir;
|
|
231
|
+
if (!entryWorkDir) {
|
|
232
|
+
if (stateParsed && state)
|
|
233
|
+
entryWorkDir = extractWorkDir(state);
|
|
234
|
+
// Unreadable state — cannot determine cwd, entryWorkDir stays undefined
|
|
235
|
+
}
|
|
227
236
|
if (entryWorkDir !== realCwd) {
|
|
228
237
|
continue;
|
|
229
238
|
}
|
|
230
|
-
const statePath = path.join(entry.sessionDir, 'state.json');
|
|
231
239
|
let summary = 'New Session';
|
|
232
|
-
if (
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
// v1: state.title / state.lastPrompt; v2: these fields may be absent
|
|
236
|
-
summary = state.title || state.lastPrompt || '';
|
|
237
|
-
}
|
|
238
|
-
catch {
|
|
239
|
-
// Corrupt state.json — fall through to wire.jsonl extraction
|
|
240
|
-
}
|
|
240
|
+
if (stateParsed && state) {
|
|
241
|
+
// v1: state.title / state.lastPrompt; v2: these fields may be absent
|
|
242
|
+
summary = state.title || state.lastPrompt || '';
|
|
241
243
|
}
|
|
242
244
|
// v2 state.json omits title/lastPrompt: extract from wire.jsonl's last
|
|
243
245
|
// turn.prompt instead. Only scan a small tail to find it — the summary
|
|
@@ -549,11 +551,11 @@ export class KimiSessionReader {
|
|
|
549
551
|
};
|
|
550
552
|
}
|
|
551
553
|
/**
|
|
552
|
-
*
|
|
553
|
-
*
|
|
554
|
-
*
|
|
554
|
+
* W2.5 单源:resolveSessionWirePath 与 isSessionActive 共用的 cwd 守卫
|
|
555
|
+
* (realpathSync → state.json workDir/cwd 三态 → index workDir 兜底 →
|
|
556
|
+
* fail-closed)。返回 sessionDir;会话未知或守卫不过返回 null。
|
|
555
557
|
*/
|
|
556
|
-
|
|
558
|
+
verifySessionCwd(sessionId, cwd) {
|
|
557
559
|
// Find session directory from index (also retrieve index entry for
|
|
558
560
|
// fallback cwd guard when state.json is missing or unparseable)
|
|
559
561
|
const indexEntry = this.findSessionIndexEntry(sessionId);
|
|
@@ -561,7 +563,6 @@ export class KimiSessionReader {
|
|
|
561
563
|
if (!sessionDir) {
|
|
562
564
|
return null;
|
|
563
565
|
}
|
|
564
|
-
const statePath = path.join(sessionDir, 'state.json');
|
|
565
566
|
let realCwd;
|
|
566
567
|
try {
|
|
567
568
|
realCwd = fs.realpathSync(cwd);
|
|
@@ -571,6 +572,7 @@ export class KimiSessionReader {
|
|
|
571
572
|
return null;
|
|
572
573
|
}
|
|
573
574
|
let cwdGuardPassed = false;
|
|
575
|
+
const statePath = path.join(sessionDir, 'state.json');
|
|
574
576
|
try {
|
|
575
577
|
if (fs.existsSync(statePath)) {
|
|
576
578
|
const state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
@@ -578,13 +580,12 @@ export class KimiSessionReader {
|
|
|
578
580
|
if (guardResult === 'failed') {
|
|
579
581
|
return null;
|
|
580
582
|
}
|
|
581
|
-
if (guardResult === '
|
|
582
|
-
|
|
583
|
-
// Fall through to index-based guard below
|
|
583
|
+
if (guardResult === 'verified') {
|
|
584
|
+
cwdGuardPassed = true;
|
|
584
585
|
}
|
|
585
586
|
else {
|
|
586
|
-
|
|
587
|
-
|
|
587
|
+
getLogger().warn(`[kimi-session-reader] state.json has no workDir/cwd field for session ${sessionId}, falling back to index workDir`);
|
|
588
|
+
// Fall through to index-based guard below
|
|
588
589
|
}
|
|
589
590
|
}
|
|
590
591
|
}
|
|
@@ -610,7 +611,18 @@ export class KimiSessionReader {
|
|
|
610
611
|
return null;
|
|
611
612
|
}
|
|
612
613
|
}
|
|
613
|
-
|
|
614
|
+
return { sessionDir };
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Resolve the main wire.jsonl path for a session after passing the cwd
|
|
618
|
+
* guard (state.json + session index fallback). Returns null when the
|
|
619
|
+
* session is unknown, fails the guard, or has no wire file.
|
|
620
|
+
*/
|
|
621
|
+
resolveSessionWirePath(sessionId, cwd) {
|
|
622
|
+
const verified = this.verifySessionCwd(sessionId, cwd);
|
|
623
|
+
if (!verified)
|
|
624
|
+
return null;
|
|
625
|
+
const wirePath = path.join(verified.sessionDir, 'agents', 'main', 'wire.jsonl');
|
|
614
626
|
if (!fs.existsSync(wirePath)) {
|
|
615
627
|
return null;
|
|
616
628
|
}
|
|
@@ -620,46 +632,11 @@ export class KimiSessionReader {
|
|
|
620
632
|
* Check if a session is currently active
|
|
621
633
|
*/
|
|
622
634
|
isSessionActive(sessionId, cwd) {
|
|
623
|
-
|
|
624
|
-
const
|
|
625
|
-
if (!
|
|
635
|
+
// W2.5:cwd 守卫与 resolveSessionWirePath 共用 verifySessionCwd(单源)。
|
|
636
|
+
const verified = this.verifySessionCwd(sessionId, cwd);
|
|
637
|
+
if (!verified)
|
|
626
638
|
return false;
|
|
627
|
-
|
|
628
|
-
// Cwd guard: the session's working directory must match the requested
|
|
629
|
-
// cwd (align with claude/pi/opencode). Mirrors readSessionContent's
|
|
630
|
-
// fail-closed cwd check (state.json workDir/cwd, then index workDir).
|
|
631
|
-
let realCwd;
|
|
632
|
-
try {
|
|
633
|
-
realCwd = fs.realpathSync(cwd);
|
|
634
|
-
}
|
|
635
|
-
catch {
|
|
636
|
-
return false;
|
|
637
|
-
}
|
|
638
|
-
let cwdGuardPassed = false;
|
|
639
|
-
const statePath = path.join(sessionDir, 'state.json');
|
|
640
|
-
try {
|
|
641
|
-
if (fs.existsSync(statePath)) {
|
|
642
|
-
const state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
643
|
-
const guardResult = checkCwdGuard(state, realCwd);
|
|
644
|
-
if (guardResult === 'failed')
|
|
645
|
-
return false;
|
|
646
|
-
if (guardResult === 'verified')
|
|
647
|
-
cwdGuardPassed = true;
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
catch {
|
|
651
|
-
// state.json unparseable — fall through to index-based guard
|
|
652
|
-
}
|
|
653
|
-
if (!cwdGuardPassed) {
|
|
654
|
-
const indexWorkDir = indexEntry?.workDir;
|
|
655
|
-
if (indexWorkDir && indexWorkDir !== realCwd)
|
|
656
|
-
return false;
|
|
657
|
-
// v2 sessions have empty index workDir (v1-only field) and may lack
|
|
658
|
-
// state.json cwd — fail-closed to prevent cross-workspace access.
|
|
659
|
-
if (!indexWorkDir)
|
|
660
|
-
return false;
|
|
661
|
-
}
|
|
662
|
-
const wirePath = path.join(sessionDir, 'agents', 'main', 'wire.jsonl');
|
|
639
|
+
const wirePath = path.join(verified.sessionDir, 'agents', 'main', 'wire.jsonl');
|
|
663
640
|
if (!fs.existsSync(wirePath)) {
|
|
664
641
|
return false;
|
|
665
642
|
}
|
|
@@ -667,7 +644,7 @@ export class KimiSessionReader {
|
|
|
667
644
|
const stat = fs.statSync(wirePath);
|
|
668
645
|
const now = Date.now();
|
|
669
646
|
// Check mtime freshness
|
|
670
|
-
if (
|
|
647
|
+
if (isStale(stat.mtimeMs, now)) {
|
|
671
648
|
return false;
|
|
672
649
|
}
|
|
673
650
|
// Read the tail to check for active indicators. kimi appends
|
|
@@ -13,14 +13,15 @@
|
|
|
13
13
|
* - isSessionActive: use `updated` timestamp with 1 hour stale window.
|
|
14
14
|
* - Simple memory cache with TTL.
|
|
15
15
|
*/
|
|
16
|
-
import {
|
|
16
|
+
import { spawnProcessSync } from '../../platform/spawn.js';
|
|
17
17
|
import fs from 'node:fs';
|
|
18
18
|
import { silentlyUnlink } from '../../common/fs.js';
|
|
19
19
|
import os from 'node:os';
|
|
20
20
|
import path from 'node:path';
|
|
21
21
|
import { truncateUtf8, TOOL_RESULT_MAX_BYTES } from '../../common/truncate.js';
|
|
22
22
|
import { getLogger } from '../../logger/index.js';
|
|
23
|
-
import {
|
|
23
|
+
import { isStale } from '../common/constants.js';
|
|
24
|
+
import { samePath } from '../../platform/path.js';
|
|
24
25
|
import { paginate, capEvents } from '../common/pagination.js';
|
|
25
26
|
import { sortByRecencyDesc } from '../common/recency.js';
|
|
26
27
|
import { TtlCache } from '../../common/ttl-cache.js';
|
|
@@ -46,7 +47,7 @@ export class OpencodeSessionReader {
|
|
|
46
47
|
const realCwd = this.realpath(cwd);
|
|
47
48
|
const entries = this.fetchSessionList(realCwd);
|
|
48
49
|
// Filter by directory === realpath(cwd)
|
|
49
|
-
const filtered = entries.filter((e) => e.directory
|
|
50
|
+
const filtered = entries.filter((e) => samePath(e.directory, realCwd));
|
|
50
51
|
// Sort by updated descending; same-updated ties use id as a deterministic
|
|
51
52
|
// secondary key so CLI/list-cache rebuilds never reorder the page.
|
|
52
53
|
sortByRecencyDesc(filtered, (s) => s.updated, (s) => s.id);
|
|
@@ -281,11 +282,11 @@ export class OpencodeSessionReader {
|
|
|
281
282
|
const realCwd = this.realpath(cwd);
|
|
282
283
|
try {
|
|
283
284
|
const entries = this.fetchSessionList(realCwd);
|
|
284
|
-
const entry = entries.find((e) => e.id === sessionId && e.directory
|
|
285
|
+
const entry = entries.find((e) => e.id === sessionId && samePath(e.directory, realCwd));
|
|
285
286
|
if (!entry)
|
|
286
287
|
return false;
|
|
287
288
|
// Check if updated within STALE_MS
|
|
288
|
-
return
|
|
289
|
+
return !isStale(entry.updated);
|
|
289
290
|
}
|
|
290
291
|
catch {
|
|
291
292
|
return false;
|
|
@@ -308,8 +309,12 @@ export class OpencodeSessionReader {
|
|
|
308
309
|
return cached;
|
|
309
310
|
}
|
|
310
311
|
try {
|
|
312
|
+
// spawnProcessSync(cross-spawn)而非 execFileSync:opencode 是 npm 安装
|
|
313
|
+
// 的 .cmd 垫片,win32 上 execFileSync 不做 PATHEXT 解析会直接 ENOENT,
|
|
314
|
+
// 会话列表永远为空;cross-spawn 走与 runner spawn 相同的垫片执行路径
|
|
315
|
+
// (posix 为 child_process.spawnSync 直通,行为不变)。
|
|
311
316
|
// Pass cwd to execFileSync so opencode CLI returns sessions for the correct directory
|
|
312
|
-
const
|
|
317
|
+
const res = spawnProcessSync(this.binary, ['session', 'list', '--format', 'json'], {
|
|
313
318
|
encoding: 'utf-8',
|
|
314
319
|
timeout: 10000,
|
|
315
320
|
// P1-15: default maxBuffer is 1MiB — a large session list (>1MiB JSON)
|
|
@@ -317,7 +322,19 @@ export class OpencodeSessionReader {
|
|
|
317
322
|
// user sees "no sessions" even though the CLI returned healthy data.
|
|
318
323
|
maxBuffer: 64 * 1024 * 1024,
|
|
319
324
|
cwd: realCwd, // Key fix: pass user cwd to CLI
|
|
320
|
-
|
|
325
|
+
windowsHide: true, // opencode 是 npm .cmd 垫片:不隐藏会闪 cmd.exe 控制台
|
|
326
|
+
});
|
|
327
|
+
if (res.error) {
|
|
328
|
+
const enoent = res.error.code === 'ENOENT';
|
|
329
|
+
if (enoent)
|
|
330
|
+
return [];
|
|
331
|
+
throw res.error;
|
|
332
|
+
}
|
|
333
|
+
if (res.status !== 0) {
|
|
334
|
+
const stderr = String(res.stderr ?? '').trim();
|
|
335
|
+
throw new Error(`exit ${res.status}${stderr ? `: ${stderr}` : ''}`);
|
|
336
|
+
}
|
|
337
|
+
const output = String(res.stdout ?? '').trim();
|
|
321
338
|
// CLI returns empty string when no sessions exist - treat as empty array
|
|
322
339
|
if (!output) {
|
|
323
340
|
return [];
|
|
@@ -365,7 +382,7 @@ export class OpencodeSessionReader {
|
|
|
365
382
|
let fd;
|
|
366
383
|
try {
|
|
367
384
|
fd = fs.openSync(tmp, 'w');
|
|
368
|
-
|
|
385
|
+
spawnProcessSync(this.binary, ['export', sessionId], {
|
|
369
386
|
encoding: 'utf-8',
|
|
370
387
|
timeout: 30000,
|
|
371
388
|
stdio: ['ignore', fd, 'pipe'],
|
|
@@ -373,6 +390,7 @@ export class OpencodeSessionReader {
|
|
|
373
390
|
// stderr) would throw ENOBUFS and lose the export. Keep the default
|
|
374
391
|
// stdout file-fd transport unaffected by raising the pipe cap.
|
|
375
392
|
maxBuffer: 64 * 1024 * 1024,
|
|
393
|
+
windowsHide: true,
|
|
376
394
|
});
|
|
377
395
|
return fs.readFileSync(tmp, 'utf-8');
|
|
378
396
|
}
|
|
@@ -1,19 +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
|
|
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
7
|
import { UsageAccumulator, contextWindowOccupancy, cumulativeUsageFields, } from '../common/usage-accumulator.js';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
8
|
+
import { isStale } from '../common/constants.js';
|
|
9
|
+
import { encodeProjectDirName } from '../../platform/path.js';
|
|
10
|
+
import { paginate } from '../common/pagination.js';
|
|
9
11
|
import { sortByRecencyDesc } from '../common/recency.js';
|
|
10
12
|
import { TtlCache } from '../../common/ttl-cache.js';
|
|
11
13
|
function defaultPiDir() {
|
|
12
14
|
return path.join(os.homedir(), '.pi', 'agent');
|
|
13
15
|
}
|
|
14
|
-
/**
|
|
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
|
+
*/
|
|
15
23
|
function projectDirForCwd(cwd, sessionsDir) {
|
|
16
|
-
const encodedCwd = cwd
|
|
24
|
+
const encodedCwd = encodeProjectDirName(cwd).replace(/^-+/, '');
|
|
17
25
|
const encoded = `--${encodedCwd}--`;
|
|
18
26
|
return path.join(sessionsDir, encoded);
|
|
19
27
|
}
|
|
@@ -143,14 +151,8 @@ function piScalarScan(filePath) {
|
|
|
143
151
|
const acc = new UsageAccumulator();
|
|
144
152
|
let tailOffset = -1;
|
|
145
153
|
let lastUserText;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
try {
|
|
149
|
-
obj = JSON.parse(line);
|
|
150
|
-
}
|
|
151
|
-
catch {
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
+
scanJsonlOnce(filePath, (rawObj, line, offset) => {
|
|
155
|
+
const obj = rawObj;
|
|
154
156
|
if (obj.type === 'message') {
|
|
155
157
|
const msg = obj.message;
|
|
156
158
|
if (!msg)
|
|
@@ -168,9 +170,9 @@ function piScalarScan(filePath) {
|
|
|
168
170
|
}
|
|
169
171
|
// last user message offset + displayTitle. Record the byte offset
|
|
170
172
|
// where the tail should start: the beginning of the NEXT line after
|
|
171
|
-
// this user message.
|
|
173
|
+
// this user message (see tailOffsetAfter).
|
|
172
174
|
if (msg.role === 'user') {
|
|
173
|
-
tailOffset =
|
|
175
|
+
tailOffset = tailOffsetAfter(line, offset);
|
|
174
176
|
const text = extractPiText(msg.content);
|
|
175
177
|
// Compress skill-injection bodies so the "最近输入" label shows what
|
|
176
178
|
// the user invoked (skill:x) + any trailing real input, not a
|
|
@@ -219,44 +221,35 @@ function buildPiUsage(acc) {
|
|
|
219
221
|
};
|
|
220
222
|
}
|
|
221
223
|
/**
|
|
222
|
-
* P2-6 + P2-5 second-pass:
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
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.
|
|
227
229
|
*/
|
|
228
|
-
function
|
|
230
|
+
function mapPiTailLine(rawObj) {
|
|
229
231
|
const events = [];
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
events.push({ type: block.type, content: block.content, timestamp: obj.timestamp });
|
|
252
|
-
}
|
|
253
|
-
if (blocks.length === 0 && message.role) {
|
|
254
|
-
events.push({ type: role, content: `(${role} event)`, timestamp: obj.timestamp });
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
catch {
|
|
258
|
-
/* skip */
|
|
259
|
-
}
|
|
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 });
|
|
260
253
|
}
|
|
261
254
|
return events;
|
|
262
255
|
}
|
|
@@ -297,29 +290,17 @@ export class PiSessionReader {
|
|
|
297
290
|
if (fileCwd && fileCwd !== cwd) {
|
|
298
291
|
return { events: [] };
|
|
299
292
|
}
|
|
300
|
-
// P2-5 + P2-6:
|
|
301
|
-
//
|
|
302
|
-
// scalars (usage, tailOffset, displayTitle)
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
// memory for both raw line strings and parsed objects, instead of
|
|
308
|
-
// O(whole file).
|
|
309
|
-
//
|
|
310
|
-
// Parse ratio: ≈1.0–1.5× line count when a user message exists.
|
|
311
|
-
// When the session has NO user message, tailOffset stays -1 and the
|
|
312
|
-
// tail IS the whole file → scan(N) + tail(N) = 2.0× — the known
|
|
313
|
-
// 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).
|
|
314
300
|
const scan = piScalarScan(filePath);
|
|
315
|
-
const
|
|
316
|
-
const events = extractPiEventsFromTail(tailLines);
|
|
317
|
-
// Apply maxEvents cap: keep the LAST N events (most recent), matching
|
|
318
|
-
// CodexSessionReader's slice(-maxEvents). maxEvents limits the catch-up
|
|
319
|
-
// tail so auto-resume cards don't load the entire session.
|
|
320
|
-
const cappedEvents = capEvents(events, opts?.maxEvents);
|
|
301
|
+
const events = scanTailEvents(filePath, scan.tailOffset, mapPiTailLine, opts?.maxEvents);
|
|
321
302
|
return {
|
|
322
|
-
events
|
|
303
|
+
events,
|
|
323
304
|
usage: scan.usage,
|
|
324
305
|
displayTitle: scan.displayTitle,
|
|
325
306
|
};
|
|
@@ -330,7 +311,7 @@ export class PiSessionReader {
|
|
|
330
311
|
return false;
|
|
331
312
|
try {
|
|
332
313
|
const st = fs.statSync(filePath);
|
|
333
|
-
return
|
|
314
|
+
return !isStale(st.mtimeMs);
|
|
334
315
|
}
|
|
335
316
|
catch {
|
|
336
317
|
return false;
|
package/dist/update/index.d.ts
CHANGED
|
@@ -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 } from './version-check.js';
|
|
8
|
+
export { isNewer, checkLatestVersion, formatUpdateHint, type VersionCheckResult, } from './version-check.js';
|
|
9
9
|
export { runInstallLatest, type InstallResult } from './install.js';
|
|
10
|
-
export { formatUpdateHint } from './startup-hint.js';
|
package/dist/update/index.js
CHANGED
|
@@ -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
|
package/dist/update/install.d.ts
CHANGED
|
@@ -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
|
|
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;
|