lark-remote 0.1.7 → 0.1.8

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 (67) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/bridge/approval-coordinator.d.ts +15 -1
  3. package/dist/bridge/approval-coordinator.js +35 -2
  4. package/dist/bridge/index.d.ts +19 -6
  5. package/dist/bridge/index.js +25 -8
  6. package/dist/card/approval-render.js +187 -45
  7. package/dist/card/card-shared.d.ts +6 -4
  8. package/dist/card/card-shared.js +6 -4
  9. package/dist/card/run-renderer.d.ts +7 -0
  10. package/dist/card/run-renderer.js +22 -11
  11. package/dist/config/index.d.ts +24 -1
  12. package/dist/config/index.js +34 -3
  13. package/dist/index.js +28 -14
  14. package/dist/router/card-action-payload.d.ts +30 -0
  15. package/dist/router/card-action-payload.js +39 -0
  16. package/dist/router/config/claude.js +8 -3
  17. package/dist/router/config/dsh.d.ts +51 -0
  18. package/dist/router/config/dsh.js +179 -0
  19. package/dist/router/config/index.d.ts +1 -1
  20. package/dist/router/config/index.js +4 -1
  21. package/dist/router/config/types.d.ts +6 -0
  22. package/dist/router/index.d.ts +20 -1
  23. package/dist/router/index.js +113 -7
  24. package/dist/router/utils.js +12 -2
  25. package/dist/runner/agent-choices-common.js +3 -0
  26. package/dist/runner/claude/session.js +24 -5
  27. package/dist/runner/codex/app-server/protocol-types.d.ts +35 -0
  28. package/dist/runner/codex/app-server/protocol-types.js +1 -1
  29. package/dist/runner/codex/app-server/runner.d.ts +10 -0
  30. package/dist/runner/codex/app-server/runner.js +56 -1
  31. package/dist/runner/codex/app-server/translator.d.ts +7 -0
  32. package/dist/runner/codex/app-server/translator.js +45 -0
  33. package/dist/runner/common/acp/protocol-types.d.ts +33 -0
  34. package/dist/runner/common/acp/protocol-types.js +2 -0
  35. package/dist/runner/dsh/client.d.ts +95 -0
  36. package/dist/runner/dsh/client.js +418 -0
  37. package/dist/runner/dsh/fake-dsh-server.d.ts +60 -0
  38. package/dist/runner/dsh/fake-dsh-server.js +156 -0
  39. package/dist/runner/dsh/index.d.ts +3 -0
  40. package/dist/runner/dsh/index.js +4 -0
  41. package/dist/runner/dsh/runner.d.ts +57 -0
  42. package/dist/runner/dsh/runner.js +191 -0
  43. package/dist/runner/dsh/translator.d.ts +28 -0
  44. package/dist/runner/dsh/translator.js +98 -0
  45. package/dist/runner/dsh/types.d.ts +155 -0
  46. package/dist/runner/dsh/types.js +14 -0
  47. package/dist/runner/index.d.ts +1 -0
  48. package/dist/runner/index.js +1 -0
  49. package/dist/runner/kimi/acp/runner.js +69 -1
  50. package/dist/runner/kimi/acp/translator.d.ts +14 -0
  51. package/dist/runner/kimi/acp/translator.js +108 -3
  52. package/dist/runner/pi/rpc/runner.d.ts +1 -7
  53. package/dist/runner/pi/rpc/runner.js +2 -11
  54. package/dist/runner/probe.js +6 -1
  55. package/dist/runner/question-common.d.ts +26 -0
  56. package/dist/runner/question-common.js +42 -0
  57. package/dist/runner/types.d.ts +63 -10
  58. package/dist/session/claude/sessions.js +37 -15
  59. package/dist/session/dsh/index.d.ts +1 -0
  60. package/dist/session/dsh/index.js +2 -0
  61. package/dist/session/dsh/sessions.d.ts +39 -0
  62. package/dist/session/dsh/sessions.js +237 -0
  63. package/dist/session/index.d.ts +1 -0
  64. package/dist/session/index.js +1 -0
  65. package/dist/workspace/index.d.ts +4 -0
  66. package/dist/workspace/index.js +8 -1
  67. package/package.json +2 -1
@@ -0,0 +1,237 @@
1
+ /**
2
+ * DshSessionReader — AgentSessionReader backed by the DSH Web Host over HTTP.
3
+ *
4
+ * The AgentSessionReader contract is fully synchronous (file-style readers),
5
+ * but DSH exposes only async HTTP. The reader therefore bridges with
6
+ * `spawnSync('curl', ...)` for its unary calls (session.list / session.history)
7
+ * — a synchronous HTTP round-trip to the local DSH host. This keeps the
8
+ * router/bridge interface contract intact without touching any upstream logic.
9
+ *
10
+ * listSessions: session.list → filter by cwd → sort by updatedAt desc → local
11
+ * pagination (DSH v1 has no cursor pagination).
12
+ * readSessionContent: session.history → map SessionEvents to display events +
13
+ * session-wide usage accumulation (assistant/message.usage).
14
+ */
15
+ import { spawnSync } from 'node:child_process';
16
+ import { getLogger } from '../../logger/index.js';
17
+ import { DEFAULT_DSH_HOST } from '../../config/index.js';
18
+ import { capEvents, paginate } from '../common/pagination.js';
19
+ import { truncateUtf8, TOOL_RESULT_MAX_BYTES } from '../../common/truncate.js';
20
+ /** Synchronous unary POST via curl (the reader interface is synchronous). */
21
+ function defaultSyncRequest(baseUrl, method, payload) {
22
+ const body = JSON.stringify({
23
+ type: 'client-request',
24
+ rpcId: `rpc-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`,
25
+ method,
26
+ payload,
27
+ });
28
+ const res = spawnSync('curl', [
29
+ '-s',
30
+ '-X',
31
+ 'POST',
32
+ `${baseUrl}/api/${method}`,
33
+ '-H',
34
+ 'content-type: application/json',
35
+ '--data',
36
+ body,
37
+ ], { encoding: 'utf-8', timeout: 15_000, maxBuffer: 64 * 1024 * 1024 });
38
+ if (res.error || res.status !== 0) {
39
+ throw new Error(`DSH ${method} failed: ${(res.error?.message ?? res.stderr) || 'unknown'}`);
40
+ }
41
+ let parsed;
42
+ try {
43
+ parsed = JSON.parse(res.stdout);
44
+ }
45
+ catch (err) {
46
+ throw new Error(`DSH ${method} returned non-JSON: ${err.message}`, {
47
+ cause: err,
48
+ });
49
+ }
50
+ if (parsed.result?.ok !== true) {
51
+ throw new Error(parsed.result?.error?.message ?? `DSH ${method} failed`);
52
+ }
53
+ return parsed.result.value;
54
+ }
55
+ export class DshSessionReader {
56
+ baseUrl;
57
+ syncRequest;
58
+ constructor(opts = {}) {
59
+ this.baseUrl = (opts.host ?? DEFAULT_DSH_HOST).replace(/\/$/, '');
60
+ this.syncRequest = opts.syncRequest ?? defaultSyncRequest;
61
+ }
62
+ unary(method, payload) {
63
+ return this.syncRequest(this.baseUrl, method, payload);
64
+ }
65
+ listSessions(cwd, opts) {
66
+ try {
67
+ const value = this.unary('session.list', {});
68
+ const items = value.items ?? [];
69
+ const filtered = items
70
+ .filter((i) => i.cwd === cwd)
71
+ .sort((a, b) => b.updatedAt - a.updatedAt || String(a.sessionId).localeCompare(String(b.sessionId)))
72
+ .map((i) => ({
73
+ sessionId: String(i.sessionId),
74
+ summary: '',
75
+ mtime: i.updatedAt,
76
+ ...(i.agentPreset ? { agentPreset: i.agentPreset } : {}),
77
+ }));
78
+ const { items: page, total } = paginate(filtered, opts ?? {});
79
+ return { sessions: page, total };
80
+ }
81
+ catch (error) {
82
+ getLogger().error(`[dsh-session-reader] listSessions error: ${error.message}`);
83
+ return { sessions: [], total: 0 };
84
+ }
85
+ }
86
+ getNewestSession(cwd) {
87
+ return this.listSessions(cwd, { limit: 1 }).sessions[0] ?? null;
88
+ }
89
+ readSessionContent(sessionId, cwd, opts) {
90
+ try {
91
+ const value = this.unary('session.history', {
92
+ sessionId,
93
+ maxMessages: opts?.maxEvents ?? 50,
94
+ });
95
+ const events = [];
96
+ let displayTitle;
97
+ let cumInput = 0;
98
+ let cumOutput = 0;
99
+ let cumCacheRead = 0;
100
+ let cumCacheWrite = 0;
101
+ let lastUsage;
102
+ for (const entry of value.events ?? []) {
103
+ const ev = entry.event;
104
+ if (!ev)
105
+ continue;
106
+ const contentEvents = mapEvent(ev);
107
+ events.push(...contentEvents.events);
108
+ if (contentEvents.title)
109
+ displayTitle = contentEvents.title;
110
+ const u = contentEvents.usage;
111
+ if (u) {
112
+ // Cumulative (session-wide): sum across ALL assistant/message usage.
113
+ cumInput += u.inputTokens ?? 0;
114
+ cumOutput += u.outputTokens ?? 0;
115
+ cumCacheRead += u.cacheReadTokens ?? 0;
116
+ cumCacheWrite += u.cacheWriteTokens ?? 0;
117
+ // Per-turn "current window" fields reflect the LAST assistant/message
118
+ // with usage (ccusage/`last_token_usage` 口径, AGENTS.md §9.21) — a
119
+ // multi-turn session's current context water-mark is the last turn's,
120
+ // not the accumulated total.
121
+ lastUsage = u;
122
+ }
123
+ }
124
+ let sessionUsage;
125
+ if (lastUsage) {
126
+ const input = lastUsage.inputTokens ?? 0;
127
+ const output = lastUsage.outputTokens ?? 0;
128
+ const cacheRead = lastUsage.cacheReadTokens ?? 0;
129
+ const cacheWrite = lastUsage.cacheWriteTokens ?? 0;
130
+ sessionUsage = {
131
+ inputTokens: input,
132
+ outputTokens: output,
133
+ contextLength: input + cacheRead + cacheWrite,
134
+ cacheReadTokens: cacheRead,
135
+ cacheCreationTokens: cacheWrite,
136
+ totalTokens: input + output + cacheRead + cacheWrite,
137
+ cumulativeTotalTokens: cumInput + cumOutput + cumCacheRead + cumCacheWrite,
138
+ cumulativeInputTokens: cumInput,
139
+ cumulativeOutputTokens: cumOutput,
140
+ cumulativeCacheReadTokens: cumCacheRead,
141
+ cumulativeCacheCreationTokens: cumCacheWrite,
142
+ };
143
+ }
144
+ return {
145
+ events: capEvents(events, opts?.maxEvents),
146
+ ...(sessionUsage ? { usage: sessionUsage } : {}),
147
+ ...(displayTitle ? { displayTitle } : {}),
148
+ };
149
+ }
150
+ catch (error) {
151
+ getLogger().error(`[dsh-session-reader] readSessionContent error: ${error.message}`);
152
+ return { events: [] };
153
+ }
154
+ }
155
+ isSessionActive(sessionId, cwd) {
156
+ try {
157
+ const value = this.unary('session.list', {});
158
+ const found = (value.items ?? []).find((i) => String(i.sessionId) === sessionId && i.cwd === cwd);
159
+ return found?.running === true;
160
+ }
161
+ catch (error) {
162
+ getLogger().error(`[dsh-session-reader] isSessionActive error: ${error.message}`);
163
+ return false;
164
+ }
165
+ }
166
+ }
167
+ /** Map one DSH SessionEvent to display content events (+ title/usage side effects). */
168
+ function mapEvent(ev) {
169
+ const out = { events: [] };
170
+ switch (ev.type) {
171
+ case 'user/message': {
172
+ const content = ev.data.content ?? [];
173
+ const text = content
174
+ .filter((c) => c.type === 'text')
175
+ .map((c) => c.text ?? '')
176
+ .join('');
177
+ out.events.push({ type: 'user', content: text, timestamp: new Date(ev.time).toISOString() });
178
+ break;
179
+ }
180
+ case 'assistant/message': {
181
+ const content = ev.data.message
182
+ ?.content ?? [];
183
+ const text = content
184
+ .filter((c) => c.type === 'text')
185
+ .map((c) => c.text ?? '')
186
+ .join('');
187
+ if (text)
188
+ out.events.push({
189
+ type: 'assistant',
190
+ content: text,
191
+ timestamp: new Date(ev.time).toISOString(),
192
+ });
193
+ const usage = ev.data.usage;
194
+ if (usage && typeof usage === 'object')
195
+ out.usage = usage;
196
+ break;
197
+ }
198
+ case 'tool/call': {
199
+ const name = String(ev.data.name ?? '');
200
+ const args = typeof ev.data.arguments === 'string' ? ev.data.arguments : '';
201
+ out.events.push({
202
+ type: 'tool_use',
203
+ content: `${name}(${truncateUtf8(args, 200)})`,
204
+ timestamp: new Date(ev.time).toISOString(),
205
+ });
206
+ break;
207
+ }
208
+ case 'tool/result': {
209
+ const msg = ev.data.message;
210
+ const raw = stringifyContent(msg?.content);
211
+ out.events.push({
212
+ type: 'tool_result',
213
+ content: truncateUtf8(raw, TOOL_RESULT_MAX_BYTES),
214
+ timestamp: new Date(ev.time).toISOString(),
215
+ });
216
+ break;
217
+ }
218
+ default:
219
+ break;
220
+ }
221
+ return out;
222
+ }
223
+ function stringifyContent(content) {
224
+ if (content == null)
225
+ return '[tool result]';
226
+ if (typeof content === 'string')
227
+ return content;
228
+ if (Array.isArray(content)) {
229
+ return content
230
+ .map((c) => c && typeof c === 'object' && 'text' in c
231
+ ? String(c.text)
232
+ : JSON.stringify(c))
233
+ .join('');
234
+ }
235
+ return JSON.stringify(content);
236
+ }
237
+ //# sourceMappingURL=sessions.js.map
@@ -5,3 +5,4 @@ export { CodexSessionReader } from './codex/index.js';
5
5
  export { OpencodeSessionReader } from './opencode/index.js';
6
6
  export { PiSessionReader } from './pi/index.js';
7
7
  export { KimiSessionReader } from './kimi/index.js';
8
+ export { DshSessionReader } from './dsh/index.js';
@@ -8,4 +8,5 @@ export { CodexSessionReader } from './codex/index.js';
8
8
  export { OpencodeSessionReader } from './opencode/index.js';
9
9
  export { PiSessionReader } from './pi/index.js';
10
10
  export { KimiSessionReader } from './kimi/index.js';
11
+ export { DshSessionReader } from './dsh/index.js';
11
12
  //# sourceMappingURL=index.js.map
@@ -11,6 +11,10 @@ export interface WorkspaceEntry {
11
11
  * mid-write cannot leave a truncated JSON file. On load, a corrupt file
12
12
  * is treated as empty (with a warning) so the bridge can still start.
13
13
  *
14
+ * lastUsedAt 语义(2026-08-19 语义对齐):保存(save)与使用(use/touch)
15
+ * 都会把时间戳更新为当前时间。save 内部调用 touch,保证「最近使用」排序
16
+ * 对新保存的 workspace 同样生效(新条目应排在列表最前,而非沉底)。
17
+ *
14
18
  * Data migration: old format `{ alias: "path" }` is normalized to
15
19
  * `{ alias: { path, lastUsedAt: 0 } }` on first load and persisted.
16
20
  */
@@ -10,6 +10,10 @@ import { getLogger } from '../logger/index.js';
10
10
  * mid-write cannot leave a truncated JSON file. On load, a corrupt file
11
11
  * is treated as empty (with a warning) so the bridge can still start.
12
12
  *
13
+ * lastUsedAt 语义(2026-08-19 语义对齐):保存(save)与使用(use/touch)
14
+ * 都会把时间戳更新为当前时间。save 内部调用 touch,保证「最近使用」排序
15
+ * 对新保存的 workspace 同样生效(新条目应排在列表最前,而非沉底)。
16
+ *
13
17
  * Data migration: old format `{ alias: "path" }` is normalized to
14
18
  * `{ alias: { path, lastUsedAt: 0 } }` on first load and persisted.
15
19
  */
@@ -60,8 +64,11 @@ export class WorkspaceStore {
60
64
  atomicWriteJson(this.filePath, obj);
61
65
  }
62
66
  save(name, dirPath) {
67
+ // 保存即视为使用(语义对齐):新建/覆盖的 workspace 都应拿到当前
68
+ // lastUsedAt,而不是 0,否则「最近使用」排序下新条目会沉底。
69
+ // touch() 负责统一写入时间戳并 persist,消除 save 写 0 / use 写 now 的不对称。
63
70
  this.data.set(name, { path: dirPath, lastUsedAt: 0 });
64
- this.persist();
71
+ this.touch(name);
65
72
  }
66
73
  get(name) {
67
74
  return this.data.get(name)?.path;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lark-remote",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
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",
@@ -47,6 +47,7 @@
47
47
  "test:opencode": "vitest run --project agent-opencode",
48
48
  "test:pi": "vitest run --project agent-pi",
49
49
  "test:kimi": "vitest run --project agent-kimi",
50
+ "test:dsh": "vitest run --project agent-dsh",
50
51
  "test:bash": "vitest run --project agent-bash",
51
52
  "test:infra": "vitest run --project 'infra-*'",
52
53
  "test:agents": "vitest run --project 'agent-*'",