lark-remote 0.1.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.
- package/CHANGELOG.md +52 -0
- package/LICENSE +21 -0
- package/README.en.md +162 -0
- package/README.md +161 -0
- package/dist/binder.d.ts +57 -0
- package/dist/binder.js +105 -0
- package/dist/bridge/index.d.ts +338 -0
- package/dist/bridge/index.js +1281 -0
- package/dist/bridge/queue-manager.d.ts +277 -0
- package/dist/bridge/queue-manager.js +729 -0
- package/dist/card/bash-card-session.d.ts +78 -0
- package/dist/card/bash-card-session.js +174 -0
- package/dist/card/bash-renderer.d.ts +24 -0
- package/dist/card/bash-renderer.js +187 -0
- package/dist/card/card-budget.d.ts +31 -0
- package/dist/card/card-budget.js +280 -0
- package/dist/card/card-session.d.ts +83 -0
- package/dist/card/card-session.js +185 -0
- package/dist/card/card-shared.d.ts +28 -0
- package/dist/card/card-shared.js +99 -0
- package/dist/card/collapsible.d.ts +26 -0
- package/dist/card/collapsible.js +87 -0
- package/dist/card/run-card-session.d.ts +78 -0
- package/dist/card/run-card-session.js +165 -0
- package/dist/card/run-renderer.d.ts +66 -0
- package/dist/card/run-renderer.js +841 -0
- package/dist/card/run-state.d.ts +103 -0
- package/dist/card/run-state.js +313 -0
- package/dist/card/text-truncate.d.ts +56 -0
- package/dist/card/text-truncate.js +184 -0
- package/dist/card/time.d.ts +1 -0
- package/dist/card/time.js +14 -0
- package/dist/card/tool-render.d.ts +25 -0
- package/dist/card/tool-render.js +176 -0
- package/dist/cli.d.ts +28 -0
- package/dist/cli.js +89 -0
- package/dist/common/guards.d.ts +37 -0
- package/dist/common/guards.js +65 -0
- package/dist/config/codex-config.d.ts +113 -0
- package/dist/config/codex-config.js +428 -0
- package/dist/config/dir.d.ts +17 -0
- package/dist/config/dir.js +81 -0
- package/dist/config/index.d.ts +219 -0
- package/dist/config/index.js +388 -0
- package/dist/config/kimi-config.d.ts +33 -0
- package/dist/config/kimi-config.js +111 -0
- package/dist/config/opencode-config.d.ts +20 -0
- package/dist/config/opencode-config.js +124 -0
- package/dist/config/pi-config.d.ts +49 -0
- package/dist/config/pi-config.js +151 -0
- package/dist/config/wizard.d.ts +12 -0
- package/dist/config/wizard.js +84 -0
- package/dist/connector/file-limits.d.ts +7 -0
- package/dist/connector/file-limits.js +8 -0
- package/dist/connector/index.d.ts +86 -0
- package/dist/connector/index.js +340 -0
- package/dist/error-classification.d.ts +15 -0
- package/dist/error-classification.js +24 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +580 -0
- package/dist/instance-lock.d.ts +30 -0
- package/dist/instance-lock.js +184 -0
- package/dist/logger/index.d.ts +57 -0
- package/dist/logger/index.js +178 -0
- package/dist/order/index.d.ts +27 -0
- package/dist/order/index.js +90 -0
- package/dist/persistence/atomic-write.d.ts +10 -0
- package/dist/persistence/atomic-write.js +65 -0
- package/dist/persistence/load-json-file.d.ts +9 -0
- package/dist/persistence/load-json-file.js +23 -0
- package/dist/restart.d.ts +24 -0
- package/dist/restart.js +73 -0
- package/dist/router/card-helpers.d.ts +28 -0
- package/dist/router/card-helpers.js +65 -0
- package/dist/router/config/claude.d.ts +14 -0
- package/dist/router/config/claude.js +72 -0
- package/dist/router/config/codex.d.ts +22 -0
- package/dist/router/config/codex.js +100 -0
- package/dist/router/config/common/render.d.ts +16 -0
- package/dist/router/config/common/render.js +158 -0
- package/dist/router/config/index.d.ts +18 -0
- package/dist/router/config/index.js +38 -0
- package/dist/router/config/kimi.d.ts +21 -0
- package/dist/router/config/kimi.js +63 -0
- package/dist/router/config/opencode.d.ts +12 -0
- package/dist/router/config/opencode.js +44 -0
- package/dist/router/config/pi.d.ts +12 -0
- package/dist/router/config/pi.js +50 -0
- package/dist/router/config/types.d.ts +43 -0
- package/dist/router/config/types.js +8 -0
- package/dist/router/index.d.ts +318 -0
- package/dist/router/index.js +2715 -0
- package/dist/router/order-exec-dispatch.d.ts +30 -0
- package/dist/router/order-exec-dispatch.js +48 -0
- package/dist/router/utils.d.ts +44 -0
- package/dist/router/utils.js +116 -0
- package/dist/runner/bash/index.d.ts +4 -0
- package/dist/runner/bash/index.js +5 -0
- package/dist/runner/bash/runner.d.ts +55 -0
- package/dist/runner/bash/runner.js +213 -0
- package/dist/runner/claude/index.d.ts +4 -0
- package/dist/runner/claude/index.js +5 -0
- package/dist/runner/claude/runner.d.ts +29 -0
- package/dist/runner/claude/runner.js +83 -0
- package/dist/runner/codex/argv.d.ts +33 -0
- package/dist/runner/codex/argv.js +40 -0
- package/dist/runner/codex/index.d.ts +1 -0
- package/dist/runner/codex/index.js +2 -0
- package/dist/runner/codex/jsonl.d.ts +37 -0
- package/dist/runner/codex/jsonl.js +221 -0
- package/dist/runner/codex/runner.d.ts +65 -0
- package/dist/runner/codex/runner.js +124 -0
- package/dist/runner/common/exec-translator.d.ts +73 -0
- package/dist/runner/common/exec-translator.js +105 -0
- package/dist/runner/common/jsonl-stream.d.ts +39 -0
- package/dist/runner/common/jsonl-stream.js +203 -0
- package/dist/runner/common/process-stopper.d.ts +28 -0
- package/dist/runner/common/process-stopper.js +79 -0
- package/dist/runner/common/runner-utils.d.ts +21 -0
- package/dist/runner/common/runner-utils.js +65 -0
- package/dist/runner/common/spawn-heartbeat.d.ts +33 -0
- package/dist/runner/common/spawn-heartbeat.js +51 -0
- package/dist/runner/common/spawning-runner.d.ts +321 -0
- package/dist/runner/common/spawning-runner.js +634 -0
- package/dist/runner/common/usage.d.ts +31 -0
- package/dist/runner/common/usage.js +29 -0
- package/dist/runner/index.d.ts +19 -0
- package/dist/runner/index.js +24 -0
- package/dist/runner/kimi/index.d.ts +1 -0
- package/dist/runner/kimi/index.js +2 -0
- package/dist/runner/kimi/jsonl.d.ts +28 -0
- package/dist/runner/kimi/jsonl.js +143 -0
- package/dist/runner/kimi/runner.d.ts +50 -0
- package/dist/runner/kimi/runner.js +105 -0
- package/dist/runner/opencode/argv.d.ts +32 -0
- package/dist/runner/opencode/argv.js +35 -0
- package/dist/runner/opencode/index.d.ts +1 -0
- package/dist/runner/opencode/index.js +2 -0
- package/dist/runner/opencode/jsonl.d.ts +68 -0
- package/dist/runner/opencode/jsonl.js +295 -0
- package/dist/runner/opencode/runner.d.ts +63 -0
- package/dist/runner/opencode/runner.js +93 -0
- package/dist/runner/pi/index.d.ts +1 -0
- package/dist/runner/pi/index.js +2 -0
- package/dist/runner/pi/jsonl.d.ts +128 -0
- package/dist/runner/pi/jsonl.js +249 -0
- package/dist/runner/pi/runner.d.ts +37 -0
- package/dist/runner/pi/runner.js +76 -0
- package/dist/runner/registry.d.ts +54 -0
- package/dist/runner/registry.js +75 -0
- package/dist/runner/resolve-agent-choices.d.ts +7 -0
- package/dist/runner/resolve-agent-choices.js +76 -0
- package/dist/runner/sync-agent-choices.d.ts +6 -0
- package/dist/runner/sync-agent-choices.js +73 -0
- package/dist/runner/types.d.ts +250 -0
- package/dist/runner/types.js +8 -0
- package/dist/session/claude/index.d.ts +1 -0
- package/dist/session/claude/index.js +2 -0
- package/dist/session/claude/session-reader.d.ts +34 -0
- package/dist/session/claude/session-reader.js +46 -0
- package/dist/session/claude/sessions.d.ts +53 -0
- package/dist/session/claude/sessions.js +528 -0
- package/dist/session/codex/index.d.ts +1 -0
- package/dist/session/codex/index.js +2 -0
- package/dist/session/codex/rollout-reader.d.ts +88 -0
- package/dist/session/codex/rollout-reader.js +473 -0
- package/dist/session/codex/sessions.d.ts +21 -0
- package/dist/session/codex/sessions.js +40 -0
- package/dist/session/common/catch-up.d.ts +12 -0
- package/dist/session/common/catch-up.js +31 -0
- package/dist/session/common/constants.d.ts +11 -0
- package/dist/session/common/constants.js +12 -0
- package/dist/session/common/content-blocks.d.ts +37 -0
- package/dist/session/common/content-blocks.js +68 -0
- package/dist/session/common/jsonl.d.ts +77 -0
- package/dist/session/common/jsonl.js +351 -0
- package/dist/session/common/usage-accumulator.d.ts +58 -0
- package/dist/session/common/usage-accumulator.js +72 -0
- package/dist/session/index.d.ts +7 -0
- package/dist/session/index.js +11 -0
- package/dist/session/kimi/index.d.ts +1 -0
- package/dist/session/kimi/index.js +2 -0
- package/dist/session/kimi/sessions.d.ts +81 -0
- package/dist/session/kimi/sessions.js +527 -0
- package/dist/session/opencode/index.d.ts +1 -0
- package/dist/session/opencode/index.js +2 -0
- package/dist/session/opencode/sessions.d.ts +81 -0
- package/dist/session/opencode/sessions.js +388 -0
- package/dist/session/pi/index.d.ts +1 -0
- package/dist/session/pi/index.js +2 -0
- package/dist/session/pi/sessions.d.ts +27 -0
- package/dist/session/pi/sessions.js +399 -0
- package/dist/session/registry.d.ts +12 -0
- package/dist/session/registry.js +20 -0
- package/dist/session/session-store.d.ts +86 -0
- package/dist/session/session-store.js +450 -0
- package/dist/startup-contact.d.ts +18 -0
- package/dist/startup-contact.js +51 -0
- package/dist/workspace/index.d.ts +19 -0
- package/dist/workspace/index.js +53 -0
- package/package.json +93 -0
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { getLogger } from '../logger/index.js';
|
|
3
|
+
import { atomicWriteJson } from '../persistence/atomic-write.js';
|
|
4
|
+
/**
|
|
5
|
+
* Per-user session store. Persists cwd + per-agent sessions to disk so that
|
|
6
|
+
* the last workspace AND the last-used sessionId per agent are restored after
|
|
7
|
+
* a bridge restart.
|
|
8
|
+
*
|
|
9
|
+
* Persistence strategy:
|
|
10
|
+
* - Persists `cwd`, `sessions` (per-agent sessionId), `previousSessions`
|
|
11
|
+
* (parked restore slots for `/config` agent switching), and `arrivalSessions`
|
|
12
|
+
* (arrival baseline; '' entries are meaningful and must survive restart).
|
|
13
|
+
* - Atomic write (temp + rename) to avoid truncated files on crash
|
|
14
|
+
* - Corrupt files are treated as empty (with a warning)
|
|
15
|
+
*
|
|
16
|
+
* P3.3: sessions is Map<AgentKind, sessionId> (one session per agent), allowing
|
|
17
|
+
* each agent to maintain its own session context when switching between agents.
|
|
18
|
+
*/
|
|
19
|
+
export class SessionStore {
|
|
20
|
+
sessions = new Map();
|
|
21
|
+
persistPath;
|
|
22
|
+
defaultAgent;
|
|
23
|
+
/**
|
|
24
|
+
* 进程内会话代际(2026-08-09 /new 被在途 run init 写回撤销事故):
|
|
25
|
+
* 每次「用户意图移动 session 指针」+1。system.init 写回通道不 bump。
|
|
26
|
+
* Bridge 在 run 启动时快照 epoch,init 写回前比较——不一致说明 /new、
|
|
27
|
+
* /cd、/resume 等在 run 在途时移动了指针,该 run 的写回是 stale 的,丢弃。
|
|
28
|
+
* 不持久化:重启后无在途 run,stale 写回不可能发生。
|
|
29
|
+
* 独立 Map 而非 SessionEntry 字段:entry 有 ~10 处整体重构点,字段易漏传。
|
|
30
|
+
*/
|
|
31
|
+
agentEpochs = new Map(); // key: userId agent
|
|
32
|
+
userEpochs = new Map(); // key: userId(/cd 跨 agent 清全部)
|
|
33
|
+
static epochKey(userId, agent) {
|
|
34
|
+
return `${userId} ${agent}`;
|
|
35
|
+
}
|
|
36
|
+
/** 当前代际 = user 级 + agent 级之和;任一级 bump 都会改变总和。 */
|
|
37
|
+
getSessionEpoch(userId, agent) {
|
|
38
|
+
return ((this.userEpochs.get(userId) ?? 0) +
|
|
39
|
+
(this.agentEpochs.get(SessionStore.epochKey(userId, agent)) ?? 0));
|
|
40
|
+
}
|
|
41
|
+
/** 用户意图移动了指定 agent 的 session 指针(/new、/resume、/config 切换)。 */
|
|
42
|
+
bumpSessionEpoch(userId, agent) {
|
|
43
|
+
const k = SessionStore.epochKey(userId, agent);
|
|
44
|
+
this.agentEpochs.set(k, (this.agentEpochs.get(k) ?? 0) + 1);
|
|
45
|
+
}
|
|
46
|
+
/** 用户意图移动了全部 agent 的 session 指针(/cd、/ws use 的 setCwd)。 */
|
|
47
|
+
bumpUserEpoch(userId) {
|
|
48
|
+
this.userEpochs.set(userId, (this.userEpochs.get(userId) ?? 0) + 1);
|
|
49
|
+
}
|
|
50
|
+
constructor(persistPath, defaultAgent) {
|
|
51
|
+
this.persistPath = persistPath;
|
|
52
|
+
this.defaultAgent = defaultAgent;
|
|
53
|
+
if (persistPath)
|
|
54
|
+
this.load();
|
|
55
|
+
}
|
|
56
|
+
get(userId) {
|
|
57
|
+
return this.sessions.get(userId);
|
|
58
|
+
}
|
|
59
|
+
set(userId, entry) {
|
|
60
|
+
this.sessions.set(userId, entry);
|
|
61
|
+
this.autoPersist();
|
|
62
|
+
}
|
|
63
|
+
delete(userId) {
|
|
64
|
+
this.sessions.delete(userId);
|
|
65
|
+
this.autoPersist();
|
|
66
|
+
}
|
|
67
|
+
/** Clear sessionId for a specific agent, keeping cwd and other agent sessions */
|
|
68
|
+
clearSessionId(userId, agent = 'claude', opts) {
|
|
69
|
+
// 无条件 bump:空→空也必须可检测(事故场景正是 run 以 (none) 启动,
|
|
70
|
+
// /new 后 store 仍为空,值比较会误判「没变」)。entry 不存在同样 bump。
|
|
71
|
+
this.bumpSessionEpoch(userId, agent);
|
|
72
|
+
const entry = this.sessions.get(userId);
|
|
73
|
+
if (entry) {
|
|
74
|
+
const newSessions = new Map(entry.sessions);
|
|
75
|
+
newSessions.set(agent, '');
|
|
76
|
+
const newSessionCwds = new Map(entry.sessionCwds);
|
|
77
|
+
if (opts?.clearSessionCwd) {
|
|
78
|
+
newSessionCwds.set(agent, '');
|
|
79
|
+
}
|
|
80
|
+
this.sessions.set(userId, {
|
|
81
|
+
sessions: newSessions,
|
|
82
|
+
previousSessions: entry.previousSessions,
|
|
83
|
+
sessionCwds: newSessionCwds,
|
|
84
|
+
arrivalSessions: entry.arrivalSessions,
|
|
85
|
+
cwd: entry.cwd,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
this.autoPersist();
|
|
89
|
+
}
|
|
90
|
+
/** Get previous sessionId for a specific agent (stored when switching away) */
|
|
91
|
+
getPreviousSessionId(userId, agent) {
|
|
92
|
+
const entry = this.sessions.get(userId);
|
|
93
|
+
if (!entry)
|
|
94
|
+
return undefined;
|
|
95
|
+
const sessionId = entry.previousSessions.get(agent);
|
|
96
|
+
if (!sessionId)
|
|
97
|
+
return undefined;
|
|
98
|
+
return sessionId;
|
|
99
|
+
}
|
|
100
|
+
/** Set previous sessionId for a specific agent (for potential restore when switching back) */
|
|
101
|
+
setPreviousSessionId(userId, agent, sessionId) {
|
|
102
|
+
const entry = this.sessions.get(userId);
|
|
103
|
+
if (entry) {
|
|
104
|
+
const newPrevious = new Map(entry.previousSessions);
|
|
105
|
+
newPrevious.set(agent, sessionId);
|
|
106
|
+
this.sessions.set(userId, {
|
|
107
|
+
sessions: entry.sessions,
|
|
108
|
+
previousSessions: newPrevious,
|
|
109
|
+
sessionCwds: entry.sessionCwds,
|
|
110
|
+
arrivalSessions: entry.arrivalSessions,
|
|
111
|
+
cwd: entry.cwd,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
const newPrevious = new Map();
|
|
116
|
+
newPrevious.set(agent, sessionId);
|
|
117
|
+
this.sessions.set(userId, {
|
|
118
|
+
sessions: new Map(),
|
|
119
|
+
previousSessions: newPrevious,
|
|
120
|
+
sessionCwds: new Map(),
|
|
121
|
+
arrivalSessions: new Map(),
|
|
122
|
+
cwd: '',
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
this.autoPersist();
|
|
126
|
+
}
|
|
127
|
+
/** Clear previous sessionId for a specific agent */
|
|
128
|
+
clearPreviousSessionId(userId, agent) {
|
|
129
|
+
const entry = this.sessions.get(userId);
|
|
130
|
+
if (entry) {
|
|
131
|
+
const newPrevious = new Map(entry.previousSessions);
|
|
132
|
+
newPrevious.delete(agent);
|
|
133
|
+
this.sessions.set(userId, {
|
|
134
|
+
sessions: entry.sessions,
|
|
135
|
+
previousSessions: newPrevious,
|
|
136
|
+
sessionCwds: entry.sessionCwds,
|
|
137
|
+
arrivalSessions: entry.arrivalSessions,
|
|
138
|
+
cwd: entry.cwd,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
this.autoPersist();
|
|
142
|
+
}
|
|
143
|
+
/** Get the arrival baseline sessionId for a specific agent ('' and missing → undefined) */
|
|
144
|
+
getArrivalSessionId(userId, agent) {
|
|
145
|
+
const entry = this.sessions.get(userId);
|
|
146
|
+
if (!entry)
|
|
147
|
+
return undefined;
|
|
148
|
+
const sessionId = entry.arrivalSessions.get(agent);
|
|
149
|
+
if (!sessionId)
|
|
150
|
+
return undefined;
|
|
151
|
+
return sessionId;
|
|
152
|
+
}
|
|
153
|
+
/** Set the arrival baseline for a specific agent ('' explicitly clears the arrival baseline) */
|
|
154
|
+
setArrivalSessionId(userId, agent, sessionId) {
|
|
155
|
+
const entry = this.sessions.get(userId);
|
|
156
|
+
if (entry) {
|
|
157
|
+
const newArrival = new Map(entry.arrivalSessions);
|
|
158
|
+
newArrival.set(agent, sessionId);
|
|
159
|
+
this.sessions.set(userId, {
|
|
160
|
+
sessions: entry.sessions,
|
|
161
|
+
previousSessions: entry.previousSessions,
|
|
162
|
+
sessionCwds: entry.sessionCwds,
|
|
163
|
+
arrivalSessions: newArrival,
|
|
164
|
+
cwd: entry.cwd,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
const newArrival = new Map();
|
|
169
|
+
newArrival.set(agent, sessionId);
|
|
170
|
+
this.sessions.set(userId, {
|
|
171
|
+
sessions: new Map(),
|
|
172
|
+
previousSessions: new Map(),
|
|
173
|
+
sessionCwds: new Map(),
|
|
174
|
+
arrivalSessions: newArrival,
|
|
175
|
+
cwd: '',
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
this.autoPersist();
|
|
179
|
+
}
|
|
180
|
+
/** Set cwd for a user, clearing all sessionIds (§9.1) */
|
|
181
|
+
setCwd(userId, cwd) {
|
|
182
|
+
// §9.1 setCwd 清全部 agent 的 sessionId → user 级 bump,覆盖 sessions
|
|
183
|
+
// map 里还没有 key 的 agent(run 刚启动、首个 init 未写回时 /cd 的边界)。
|
|
184
|
+
this.bumpUserEpoch(userId);
|
|
185
|
+
const entry = this.sessions.get(userId);
|
|
186
|
+
if (entry) {
|
|
187
|
+
const newSessions = new Map(entry.sessions);
|
|
188
|
+
for (const key of newSessions.keys()) {
|
|
189
|
+
newSessions.set(key, '');
|
|
190
|
+
}
|
|
191
|
+
this.sessions.set(userId, {
|
|
192
|
+
sessions: newSessions,
|
|
193
|
+
previousSessions: new Map(),
|
|
194
|
+
sessionCwds: new Map(),
|
|
195
|
+
arrivalSessions: new Map(),
|
|
196
|
+
cwd,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
this.sessions.set(userId, {
|
|
201
|
+
sessions: new Map(),
|
|
202
|
+
previousSessions: new Map(),
|
|
203
|
+
sessionCwds: new Map(),
|
|
204
|
+
arrivalSessions: new Map(),
|
|
205
|
+
cwd,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
this.autoPersist();
|
|
209
|
+
}
|
|
210
|
+
/** Get cwd for a user, or undefined if no session */
|
|
211
|
+
getCwd(userId) {
|
|
212
|
+
return this.sessions.get(userId)?.cwd;
|
|
213
|
+
}
|
|
214
|
+
/** Get sessionId for a specific agent, or undefined if no session or sessionId is empty */
|
|
215
|
+
getSessionId(userId, agent = 'claude') {
|
|
216
|
+
const entry = this.sessions.get(userId);
|
|
217
|
+
if (!entry || !entry.sessions)
|
|
218
|
+
return undefined;
|
|
219
|
+
const sessionId = entry.sessions.get(agent);
|
|
220
|
+
if (!sessionId)
|
|
221
|
+
return undefined;
|
|
222
|
+
return sessionId;
|
|
223
|
+
}
|
|
224
|
+
/** Set sessionId for a specific agent, optionally with cwd */
|
|
225
|
+
setSessionId(userId, agent, sessionId, cwd) {
|
|
226
|
+
this.bumpSessionEpoch(userId, agent);
|
|
227
|
+
const entry = this.sessions.get(userId);
|
|
228
|
+
if (entry) {
|
|
229
|
+
const newSessions = new Map(entry.sessions);
|
|
230
|
+
newSessions.set(agent, sessionId);
|
|
231
|
+
this.sessions.set(userId, {
|
|
232
|
+
sessions: newSessions,
|
|
233
|
+
previousSessions: entry.previousSessions,
|
|
234
|
+
sessionCwds: entry.sessionCwds,
|
|
235
|
+
arrivalSessions: entry.arrivalSessions,
|
|
236
|
+
cwd: cwd ?? entry.cwd,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
const newSessions = new Map();
|
|
241
|
+
newSessions.set(agent, sessionId);
|
|
242
|
+
this.sessions.set(userId, {
|
|
243
|
+
sessions: newSessions,
|
|
244
|
+
previousSessions: new Map(),
|
|
245
|
+
sessionCwds: new Map(),
|
|
246
|
+
arrivalSessions: new Map(),
|
|
247
|
+
cwd: cwd ?? '',
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
this.autoPersist();
|
|
251
|
+
}
|
|
252
|
+
/** Set sessionId for a specific agent and cwd together */
|
|
253
|
+
setSessionIdAndCwd(userId, agent, sessionId, cwd, sessionCwd) {
|
|
254
|
+
const entry = this.sessions.get(userId);
|
|
255
|
+
if (entry) {
|
|
256
|
+
const newSessions = new Map(entry.sessions);
|
|
257
|
+
newSessions.set(agent, sessionId);
|
|
258
|
+
const newSessionCwds = new Map(entry.sessionCwds);
|
|
259
|
+
if (sessionCwd !== undefined) {
|
|
260
|
+
newSessionCwds.set(agent, sessionCwd);
|
|
261
|
+
}
|
|
262
|
+
this.sessions.set(userId, {
|
|
263
|
+
sessions: newSessions,
|
|
264
|
+
previousSessions: entry.previousSessions,
|
|
265
|
+
sessionCwds: newSessionCwds,
|
|
266
|
+
arrivalSessions: entry.arrivalSessions,
|
|
267
|
+
cwd,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
const newSessions = new Map();
|
|
272
|
+
newSessions.set(agent, sessionId);
|
|
273
|
+
this.sessions.set(userId, {
|
|
274
|
+
sessions: newSessions,
|
|
275
|
+
previousSessions: new Map(),
|
|
276
|
+
sessionCwds: new Map(),
|
|
277
|
+
arrivalSessions: new Map(),
|
|
278
|
+
cwd,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
this.autoPersist();
|
|
282
|
+
}
|
|
283
|
+
/** Get sessionCwd for a specific agent, or undefined if not set */
|
|
284
|
+
getSessionCwd(userId, agent = 'claude') {
|
|
285
|
+
const entry = this.sessions.get(userId);
|
|
286
|
+
if (!entry)
|
|
287
|
+
return undefined;
|
|
288
|
+
const sessionCwd = entry.sessionCwds.get(agent);
|
|
289
|
+
if (!sessionCwd)
|
|
290
|
+
return undefined;
|
|
291
|
+
return sessionCwd;
|
|
292
|
+
}
|
|
293
|
+
/** Set sessionId and sessionCwd for a specific agent, without touching cwd */
|
|
294
|
+
setSessionIdAndSessionCwd(userId, agent, sessionId, sessionCwd) {
|
|
295
|
+
const entry = this.sessions.get(userId);
|
|
296
|
+
if (entry) {
|
|
297
|
+
const newSessions = new Map(entry.sessions);
|
|
298
|
+
newSessions.set(agent, sessionId);
|
|
299
|
+
const newSessionCwds = new Map(entry.sessionCwds);
|
|
300
|
+
newSessionCwds.set(agent, sessionCwd);
|
|
301
|
+
this.sessions.set(userId, {
|
|
302
|
+
sessions: newSessions,
|
|
303
|
+
previousSessions: entry.previousSessions,
|
|
304
|
+
sessionCwds: newSessionCwds,
|
|
305
|
+
arrivalSessions: entry.arrivalSessions,
|
|
306
|
+
cwd: entry.cwd,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
const newSessions = new Map();
|
|
311
|
+
newSessions.set(agent, sessionId);
|
|
312
|
+
const newSessionCwds = new Map();
|
|
313
|
+
newSessionCwds.set(agent, sessionCwd);
|
|
314
|
+
this.sessions.set(userId, {
|
|
315
|
+
sessions: newSessions,
|
|
316
|
+
previousSessions: new Map(),
|
|
317
|
+
sessionCwds: newSessionCwds,
|
|
318
|
+
arrivalSessions: new Map(),
|
|
319
|
+
cwd: '',
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
this.autoPersist();
|
|
323
|
+
}
|
|
324
|
+
// -- Persistence helpers --
|
|
325
|
+
/** Persisted format: only cwd per user (sessionId is process-scoped) */
|
|
326
|
+
load() {
|
|
327
|
+
if (!this.persistPath)
|
|
328
|
+
return;
|
|
329
|
+
try {
|
|
330
|
+
if (!fs.existsSync(this.persistPath))
|
|
331
|
+
return;
|
|
332
|
+
const raw = fs.readFileSync(this.persistPath, 'utf-8');
|
|
333
|
+
const parsed = JSON.parse(raw);
|
|
334
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
335
|
+
for (const [userId, data] of Object.entries(parsed)) {
|
|
336
|
+
if (typeof data === 'object' && data !== null) {
|
|
337
|
+
const entry = data;
|
|
338
|
+
// All four session fields are required; a missing one indicates a
|
|
339
|
+
// corrupt record — skip it entirely. cwd is allowed to be absent
|
|
340
|
+
// (legitimately-empty cwd state, see round7 tests).
|
|
341
|
+
if (!entry.sessions ||
|
|
342
|
+
!entry.previousSessions ||
|
|
343
|
+
!entry.arrivalSessions ||
|
|
344
|
+
!entry.sessionCwds) {
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
const sessions = new Map();
|
|
348
|
+
for (const [agent, sid] of Object.entries(entry.sessions)) {
|
|
349
|
+
if (sid) {
|
|
350
|
+
sessions.set(agent, sid);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
const previousSessions = new Map();
|
|
354
|
+
for (const [agent, sid] of Object.entries(entry.previousSessions)) {
|
|
355
|
+
if (sid) {
|
|
356
|
+
previousSessions.set(agent, sid);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const arrivalSessions = new Map();
|
|
360
|
+
for (const [agent, sid] of Object.entries(entry.arrivalSessions)) {
|
|
361
|
+
// '' entries are meaningful: explicit "cleared arrival" must
|
|
362
|
+
// survive a restart instead of being re-defaulted.
|
|
363
|
+
arrivalSessions.set(agent, sid);
|
|
364
|
+
}
|
|
365
|
+
const sessionCwds = new Map();
|
|
366
|
+
for (const [agent, scwd] of Object.entries(entry.sessionCwds)) {
|
|
367
|
+
if (scwd) {
|
|
368
|
+
sessionCwds.set(agent, scwd);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
// Persist any user that has state: cwd set, or any meaningful
|
|
372
|
+
// sessions/previousSessions value, or any arrival baseline entry
|
|
373
|
+
// ('' arrivals are meaningful). Fully empty records are skipped.
|
|
374
|
+
const hasCwd = !!entry.cwd && entry.cwd.length > 0;
|
|
375
|
+
if (hasCwd ||
|
|
376
|
+
sessions.size > 0 ||
|
|
377
|
+
previousSessions.size > 0 ||
|
|
378
|
+
arrivalSessions.size > 0) {
|
|
379
|
+
this.sessions.set(userId, {
|
|
380
|
+
sessions,
|
|
381
|
+
previousSessions,
|
|
382
|
+
sessionCwds,
|
|
383
|
+
arrivalSessions,
|
|
384
|
+
cwd: entry.cwd ?? '',
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
getLogger().warn('[session] persisted session store corrupted, ignoring');
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
autoPersist() {
|
|
396
|
+
if (!this.persistPath)
|
|
397
|
+
return;
|
|
398
|
+
try {
|
|
399
|
+
// Persist cwd, sessions, previousSessions (parked restore slots) and
|
|
400
|
+
// arrivalSessions (arrival baseline; '' entries are meaningful).
|
|
401
|
+
const obj = {};
|
|
402
|
+
for (const [userId, entry] of this.sessions) {
|
|
403
|
+
// Persist any user that has state: cwd set, or any meaningful
|
|
404
|
+
// sessions/previousSessions value, or any arrival baseline entry
|
|
405
|
+
// ('' arrivals are meaningful). Empty cwd is written as '' so the
|
|
406
|
+
// other fields survive a rebuild.
|
|
407
|
+
const hasSessions = [...entry.sessions.values()].some((sid) => sid);
|
|
408
|
+
const hasPrevious = [...entry.previousSessions.values()].some((sid) => sid);
|
|
409
|
+
const hasArrival = entry.arrivalSessions.size > 0;
|
|
410
|
+
const hasSessionCwds = entry.sessionCwds.size > 0;
|
|
411
|
+
if (entry.cwd.length > 0 || hasSessions || hasPrevious || hasArrival || hasSessionCwds) {
|
|
412
|
+
const sessionsObj = {};
|
|
413
|
+
for (const [agent, sid] of entry.sessions) {
|
|
414
|
+
if (sid) {
|
|
415
|
+
sessionsObj[agent] = sid;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const previousObj = {};
|
|
419
|
+
for (const [agent, sid] of entry.previousSessions) {
|
|
420
|
+
if (sid) {
|
|
421
|
+
previousObj[agent] = sid;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const arrivalObj = {};
|
|
425
|
+
for (const [agent, sid] of entry.arrivalSessions) {
|
|
426
|
+
arrivalObj[agent] = sid;
|
|
427
|
+
}
|
|
428
|
+
const sessionCwdsObj = {};
|
|
429
|
+
for (const [agent, scwd] of entry.sessionCwds) {
|
|
430
|
+
if (scwd) {
|
|
431
|
+
sessionCwdsObj[agent] = scwd;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
obj[userId] = {
|
|
435
|
+
cwd: entry.cwd,
|
|
436
|
+
sessions: sessionsObj,
|
|
437
|
+
previousSessions: previousObj,
|
|
438
|
+
sessionCwds: sessionCwdsObj,
|
|
439
|
+
arrivalSessions: arrivalObj,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
atomicWriteJson(this.persistPath, obj);
|
|
444
|
+
}
|
|
445
|
+
catch (err) {
|
|
446
|
+
getLogger().warn('[session] failed to persist session store:', err);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
//# sourceMappingURL=session-store.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface StartupContact {
|
|
2
|
+
chatId?: string;
|
|
3
|
+
userId?: string;
|
|
4
|
+
}
|
|
5
|
+
interface StartupHelloChannel {
|
|
6
|
+
sendWithRetry(to: string, input: {
|
|
7
|
+
text: string;
|
|
8
|
+
}): Promise<string>;
|
|
9
|
+
}
|
|
10
|
+
export declare class StartupContactStore {
|
|
11
|
+
private readonly filePath;
|
|
12
|
+
constructor(filePath: string);
|
|
13
|
+
save(contact: StartupContact): void;
|
|
14
|
+
getContact(): StartupContact | undefined;
|
|
15
|
+
}
|
|
16
|
+
export declare function formatStartupHello(now?: Date, pid?: number): string;
|
|
17
|
+
export declare function sendStartupHello(connector: StartupHelloChannel, store: StartupContactStore): Promise<void>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { getLogger } from './logger/index.js';
|
|
2
|
+
import { atomicWriteJson } from './persistence/atomic-write.js';
|
|
3
|
+
import { loadJsonFile } from './persistence/load-json-file.js';
|
|
4
|
+
export class StartupContactStore {
|
|
5
|
+
filePath;
|
|
6
|
+
constructor(filePath) {
|
|
7
|
+
this.filePath = filePath;
|
|
8
|
+
}
|
|
9
|
+
save(contact) {
|
|
10
|
+
atomicWriteJson(this.filePath, contact);
|
|
11
|
+
}
|
|
12
|
+
getContact() {
|
|
13
|
+
const parsed = loadJsonFile(this.filePath, undefined);
|
|
14
|
+
if (!parsed)
|
|
15
|
+
return undefined;
|
|
16
|
+
const contact = {};
|
|
17
|
+
if (typeof parsed.chatId === 'string' && parsed.chatId.length > 0)
|
|
18
|
+
contact.chatId = parsed.chatId;
|
|
19
|
+
if (typeof parsed.userId === 'string' && parsed.userId.length > 0)
|
|
20
|
+
contact.userId = parsed.userId;
|
|
21
|
+
return contact.chatId || contact.userId ? contact : undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function formatStartupHello(now = new Date(), pid = process.pid) {
|
|
25
|
+
const startedAt = new Intl.DateTimeFormat('zh-CN', {
|
|
26
|
+
year: 'numeric',
|
|
27
|
+
month: '2-digit',
|
|
28
|
+
day: '2-digit',
|
|
29
|
+
hour: '2-digit',
|
|
30
|
+
minute: '2-digit',
|
|
31
|
+
second: '2-digit',
|
|
32
|
+
hour12: false,
|
|
33
|
+
}).format(now);
|
|
34
|
+
return `lark-remote 已启动\n启动时间:${startedAt}\n进程号:${pid}`;
|
|
35
|
+
}
|
|
36
|
+
export async function sendStartupHello(connector, store) {
|
|
37
|
+
const contact = store.getContact();
|
|
38
|
+
const recipient = contact?.chatId ?? contact?.userId;
|
|
39
|
+
if (!recipient) {
|
|
40
|
+
getLogger().info('[startup] no known p2p recipient for hello message');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
await connector.sendWithRetry(recipient, { text: formatStartupHello() });
|
|
45
|
+
getLogger().info('[startup] hello message sent');
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
getLogger().warn('[startup] hello message failed:', err);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=startup-contact.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent named-alias store for workspace directories.
|
|
3
|
+
*
|
|
4
|
+
* Atomic write (§9.10): writes a temp file then `fs.rename` so a crash
|
|
5
|
+
* mid-write cannot leave a truncated JSON file. On load, a corrupt file
|
|
6
|
+
* is treated as empty (with a warning) so the bridge can still start.
|
|
7
|
+
*/
|
|
8
|
+
export declare class WorkspaceStore {
|
|
9
|
+
private filePath;
|
|
10
|
+
private data;
|
|
11
|
+
constructor(filePath?: string);
|
|
12
|
+
private load;
|
|
13
|
+
private persist;
|
|
14
|
+
save(name: string, dirPath: string): void;
|
|
15
|
+
get(name: string): string | undefined;
|
|
16
|
+
has(name: string): boolean;
|
|
17
|
+
remove(name: string): void;
|
|
18
|
+
list(): [string, string][];
|
|
19
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import { atomicWriteJson } from '../persistence/atomic-write.js';
|
|
4
|
+
import { loadJsonFile } from '../persistence/load-json-file.js';
|
|
5
|
+
/**
|
|
6
|
+
* Persistent named-alias store for workspace directories.
|
|
7
|
+
*
|
|
8
|
+
* Atomic write (§9.10): writes a temp file then `fs.rename` so a crash
|
|
9
|
+
* mid-write cannot leave a truncated JSON file. On load, a corrupt file
|
|
10
|
+
* is treated as empty (with a warning) so the bridge can still start.
|
|
11
|
+
*/
|
|
12
|
+
export class WorkspaceStore {
|
|
13
|
+
filePath;
|
|
14
|
+
data;
|
|
15
|
+
constructor(filePath) {
|
|
16
|
+
this.filePath = filePath ?? path.join(os.homedir(), '.lark-remote', 'workspace.json');
|
|
17
|
+
this.data = new Map();
|
|
18
|
+
this.load();
|
|
19
|
+
}
|
|
20
|
+
load() {
|
|
21
|
+
const parsed = loadJsonFile(this.filePath, undefined);
|
|
22
|
+
if (!parsed)
|
|
23
|
+
return;
|
|
24
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
25
|
+
if (typeof v === 'string')
|
|
26
|
+
this.data.set(k, v);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
persist() {
|
|
30
|
+
const obj = {};
|
|
31
|
+
for (const [k, v] of this.data)
|
|
32
|
+
obj[k] = v;
|
|
33
|
+
atomicWriteJson(this.filePath, obj);
|
|
34
|
+
}
|
|
35
|
+
save(name, dirPath) {
|
|
36
|
+
this.data.set(name, dirPath);
|
|
37
|
+
this.persist();
|
|
38
|
+
}
|
|
39
|
+
get(name) {
|
|
40
|
+
return this.data.get(name);
|
|
41
|
+
}
|
|
42
|
+
has(name) {
|
|
43
|
+
return this.data.has(name);
|
|
44
|
+
}
|
|
45
|
+
remove(name) {
|
|
46
|
+
this.data.delete(name);
|
|
47
|
+
this.persist();
|
|
48
|
+
}
|
|
49
|
+
list() {
|
|
50
|
+
return [...this.data.entries()];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=index.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lark-remote",
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"bin": {
|
|
14
|
+
"lark-remote": "dist/cli.js"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"!dist/**/*.js.map",
|
|
19
|
+
"!dist/**/*.d.ts.map",
|
|
20
|
+
"!dist/config/codex-bundled-test-helpers.*",
|
|
21
|
+
"!dist/test-helpers.*",
|
|
22
|
+
"README.md",
|
|
23
|
+
"README.en.md",
|
|
24
|
+
"LICENSE",
|
|
25
|
+
"CHANGELOG.md"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"dev": "bun src/index.ts",
|
|
29
|
+
"build": "rm -rf dist && tsc && chmod +x dist/index.js dist/cli.js",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"lint": "eslint src tests",
|
|
32
|
+
"lint:fix": "eslint src tests --fix",
|
|
33
|
+
"format": "prettier --write 'src/**/*.ts' 'tests/**/*.ts'",
|
|
34
|
+
"format:check": "prettier --check 'src/**/*.ts' 'tests/**/*.ts'",
|
|
35
|
+
"test": "vitest run",
|
|
36
|
+
"test:watch": "vitest",
|
|
37
|
+
"prepublishOnly": "tsc --noEmit && vitest run && rm -rf dist && tsc && chmod +x dist/index.js dist/cli.js",
|
|
38
|
+
"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)}\""
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public",
|
|
42
|
+
"provenance": false
|
|
43
|
+
},
|
|
44
|
+
"keywords": [
|
|
45
|
+
"feishu",
|
|
46
|
+
"lark",
|
|
47
|
+
"claude-code",
|
|
48
|
+
"codex",
|
|
49
|
+
"opencode",
|
|
50
|
+
"kimi",
|
|
51
|
+
"coding-agent",
|
|
52
|
+
"bridge",
|
|
53
|
+
"cli",
|
|
54
|
+
"remote-control"
|
|
55
|
+
],
|
|
56
|
+
"author": "hhr-code",
|
|
57
|
+
"license": "MIT",
|
|
58
|
+
"repository": {
|
|
59
|
+
"type": "git",
|
|
60
|
+
"url": "git+https://github.com/hhr-code/lark-remote.git"
|
|
61
|
+
},
|
|
62
|
+
"bugs": {
|
|
63
|
+
"url": "https://github.com/hhr-code/lark-remote/issues"
|
|
64
|
+
},
|
|
65
|
+
"homepage": "https://github.com/hhr-code/lark-remote#readme",
|
|
66
|
+
"engines": {
|
|
67
|
+
"node": ">=20"
|
|
68
|
+
},
|
|
69
|
+
"type": "module",
|
|
70
|
+
"dependencies": {
|
|
71
|
+
"@iarna/toml": "^2.2.5",
|
|
72
|
+
"@larksuite/channel": "^0.3.0",
|
|
73
|
+
"axios": "^1.18.0",
|
|
74
|
+
"form-data": "^4.0.6",
|
|
75
|
+
"qrcode-terminal": "^0.12.0",
|
|
76
|
+
"yaml": "^2.9.0",
|
|
77
|
+
"zod": "^4.4.3"
|
|
78
|
+
},
|
|
79
|
+
"devDependencies": {
|
|
80
|
+
"@eslint/js": "^10.0.1",
|
|
81
|
+
"@types/form-data": "^2.5.2",
|
|
82
|
+
"@types/node": "^26.1.2",
|
|
83
|
+
"@types/qrcode-terminal": "^0.12.2",
|
|
84
|
+
"@vitest/coverage-v8": "4.1.9",
|
|
85
|
+
"eslint": "^10.7.0",
|
|
86
|
+
"eslint-config-prettier": "^10.1.8",
|
|
87
|
+
"eslint-plugin-prettier": "^5.5.6",
|
|
88
|
+
"prettier": "^3.9.5",
|
|
89
|
+
"typescript": "^6.0.0",
|
|
90
|
+
"typescript-eslint": "^8.63.0",
|
|
91
|
+
"vitest": "^4.1.9"
|
|
92
|
+
}
|
|
93
|
+
}
|