lark-codex-bridge 0.0.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.
@@ -0,0 +1,85 @@
1
+ type AgentEvent = {
2
+ type: 'system';
3
+ sessionId?: string;
4
+ cwd?: string;
5
+ model?: string;
6
+ } | {
7
+ type: 'text';
8
+ delta: string;
9
+ } | {
10
+ type: 'thinking';
11
+ delta: string;
12
+ } | {
13
+ type: 'tool_use';
14
+ id: string;
15
+ name: string;
16
+ input: unknown;
17
+ } | {
18
+ type: 'tool_result';
19
+ id: string;
20
+ output: string;
21
+ isError: boolean;
22
+ } | {
23
+ type: 'usage';
24
+ inputTokens?: number;
25
+ outputTokens?: number;
26
+ costUsd?: number;
27
+ } | {
28
+ type: 'done';
29
+ sessionId?: string;
30
+ } | {
31
+ type: 'error';
32
+ message: string;
33
+ };
34
+
35
+ type ToolStatus = 'running' | 'done' | 'error';
36
+ interface ToolEntry {
37
+ id: string;
38
+ name: string;
39
+ input: unknown;
40
+ status: ToolStatus;
41
+ output?: string;
42
+ }
43
+ type Block = {
44
+ kind: 'text';
45
+ content: string;
46
+ streaming: boolean;
47
+ } | {
48
+ kind: 'tool';
49
+ tool: ToolEntry;
50
+ };
51
+ type FooterStatus = 'thinking' | 'tool_running' | 'streaming' | null;
52
+ type Terminal = 'running' | 'done' | 'interrupted' | 'error' | 'idle_timeout';
53
+ interface RunState {
54
+ blocks: Block[];
55
+ reasoning: {
56
+ content: string;
57
+ active: boolean;
58
+ };
59
+ footer: FooterStatus;
60
+ terminal: Terminal;
61
+ errorMsg?: string;
62
+ /** Set when terminal === 'idle_timeout' — how long the agent was idle before
63
+ * the watchdog gave up (so the message can say "N 分钟无响应"). */
64
+ idleTimeoutMinutes?: number;
65
+ }
66
+ declare const initialState: RunState;
67
+ declare function reduce(state: RunState, evt: AgentEvent): RunState;
68
+ declare function markInterrupted(state: RunState): RunState;
69
+ declare function finalizeIfRunning(state: RunState): RunState;
70
+
71
+ declare function renderCard(state: RunState): object;
72
+
73
+ /**
74
+ * Render `RunState` as plain markdown text — used in `messageReply: 'text'`
75
+ * mode where we stream a markdown message instead of a card.
76
+ *
77
+ * Differences vs `renderCard`:
78
+ * - No collapsible panels, no buttons (markdown messages have neither)
79
+ * - Tool calls collapse to a single short line each (no body)
80
+ * - No reasoning / thinking output (no place to fold it; would be noise)
81
+ * - Footer is appended inline at the bottom while running
82
+ */
83
+ declare function renderText(state: RunState): string;
84
+
85
+ export { type Block, type FooterStatus, type RunState, type Terminal, type ToolEntry, type ToolStatus, finalizeIfRunning, initialState, markInterrupted, reduce, renderCard, renderText };
package/dist/index.js ADDED
@@ -0,0 +1,419 @@
1
+ // src/card/tool-render.ts
2
+ var HEADER_SUMMARY_MAX = 80;
3
+ var BODY_FIELD_MAX = 600;
4
+ var OUTPUT_MAX = 1200;
5
+ var BODY_TOTAL_MAX = 2500;
6
+ function toolHeaderText(tool) {
7
+ const icon = tool.status === "done" ? "\u2705" : tool.status === "error" ? "\u274C" : "\u23F3";
8
+ const summary = summarizeInput(tool.name, tool.input);
9
+ return summary ? `${icon} **${tool.name}** \u2014 ${summary}` : `${icon} **${tool.name}**`;
10
+ }
11
+ function toolBodyMd(tool) {
12
+ const parts = [];
13
+ const inputMd = renderInput(tool);
14
+ if (inputMd) parts.push(inputMd);
15
+ if (tool.output) {
16
+ const truncated = truncate(tool.output, OUTPUT_MAX);
17
+ if (tool.status === "error") {
18
+ parts.push(`**Error**
19
+ \`\`\`
20
+ ${truncated}
21
+ \`\`\``);
22
+ } else if (tool.name === "Bash") {
23
+ parts.push(renderBashOutput(truncated));
24
+ } else {
25
+ parts.push(`**Output**
26
+ \`\`\`
27
+ ${truncated}
28
+ \`\`\``);
29
+ }
30
+ } else if (tool.status === "running") {
31
+ parts.push("_\u8FD0\u884C\u4E2D\u2026_");
32
+ }
33
+ const body = parts.join("\n\n");
34
+ if (body.length <= BODY_TOTAL_MAX) return body;
35
+ return `${body.slice(0, BODY_TOTAL_MAX)}\u2026
36
+
37
+ _\uFF08body \u5DF2\u622A\u65AD,\u5B8C\u6574\u5185\u5BB9\u67E5 \`/doctor\` \u6216\u65E5\u5FD7\uFF09_`;
38
+ }
39
+ function summarizeInput(name, input) {
40
+ if (!input || typeof input !== "object") return "";
41
+ const rec = input;
42
+ const pick = (key, max = HEADER_SUMMARY_MAX) => {
43
+ const v = rec[key];
44
+ if (typeof v !== "string") return "";
45
+ const oneLine = v.replace(/\s+/g, " ").trim();
46
+ return oneLine.length > max ? `${oneLine.slice(0, max)}\u2026` : oneLine;
47
+ };
48
+ switch (name) {
49
+ case "Bash":
50
+ return pick("command");
51
+ case "Read":
52
+ case "Edit":
53
+ case "Write":
54
+ case "NotebookEdit":
55
+ return shortenPath(pick("file_path"));
56
+ case "Grep": {
57
+ const pat = pick("pattern", 40);
58
+ const path = pick("path", 30);
59
+ return path ? `${pat} in ${shortenPath(path)}` : pat;
60
+ }
61
+ case "Glob":
62
+ return pick("pattern");
63
+ case "WebFetch":
64
+ return pick("url");
65
+ case "WebSearch":
66
+ return pick("query", 60);
67
+ case "Agent":
68
+ case "Task":
69
+ return pick("description") || pick("subagent_type");
70
+ default:
71
+ return pick("command") || pick("file_path") || pick("path") || pick("query");
72
+ }
73
+ }
74
+ function renderInput(tool) {
75
+ const input = tool.input;
76
+ if (!input || typeof input !== "object") return "";
77
+ const rec = input;
78
+ const str = (k) => typeof rec[k] === "string" ? rec[k] : "";
79
+ switch (tool.name) {
80
+ case "Bash": {
81
+ const cmd = str("command");
82
+ return cmd ? `**Command**
83
+ \`\`\`bash
84
+ ${truncate(cmd, BODY_FIELD_MAX)}
85
+ \`\`\`` : "";
86
+ }
87
+ case "Read":
88
+ case "Edit":
89
+ case "Write":
90
+ case "NotebookEdit": {
91
+ const fp = str("file_path");
92
+ return fp ? `**File** \`${fp}\`` : "";
93
+ }
94
+ case "Grep": {
95
+ const lines = [];
96
+ if (str("pattern")) lines.push(`**Pattern** \`${str("pattern")}\``);
97
+ if (str("path")) lines.push(`**Path** \`${str("path")}\``);
98
+ return lines.join("\n");
99
+ }
100
+ case "WebFetch":
101
+ return str("url") ? `**URL** ${str("url")}` : "";
102
+ case "WebSearch":
103
+ return str("query") ? `**Query** \`${truncate(str("query"), BODY_FIELD_MAX)}\`` : "";
104
+ default:
105
+ return "";
106
+ }
107
+ }
108
+ function renderBashOutput(out) {
109
+ return `**Output**
110
+ \`\`\`
111
+ ${out}
112
+ \`\`\``;
113
+ }
114
+ function shortenPath(p) {
115
+ if (!p) return p;
116
+ const home = process.env.HOME || "";
117
+ if (home && p.startsWith(home)) return `~${p.slice(home.length)}`;
118
+ return p;
119
+ }
120
+ function truncate(s, max) {
121
+ return s.length > max ? `${s.slice(0, max)}\u2026` : s;
122
+ }
123
+
124
+ // src/card/run-renderer.ts
125
+ var REASONING_MAX = 1500;
126
+ var COLLAPSE_TOOL_THRESHOLD = 3;
127
+ function renderCard(state) {
128
+ const elements = [];
129
+ if (state.reasoning.content) {
130
+ elements.push(reasoningPanel(state.reasoning.content, state.reasoning.active));
131
+ }
132
+ for (const group of groupBlocks(state.blocks)) {
133
+ if (group.kind === "text") {
134
+ if (group.content.trim()) {
135
+ elements.push(markdown(group.content));
136
+ }
137
+ } else {
138
+ elements.push(...renderToolGroup(group.tools, state.terminal !== "running"));
139
+ }
140
+ }
141
+ if (state.terminal === "interrupted") {
142
+ elements.push(noteMd("_\u23F9 \u5DF2\u88AB\u4E2D\u65AD_"));
143
+ } else if (state.terminal === "idle_timeout") {
144
+ const mins = state.idleTimeoutMinutes ?? 0;
145
+ elements.push(noteMd(`_\u23F1 ${mins} \u5206\u949F\u65E0\u54CD\u5E94,\u5DF2\u81EA\u52A8\u7EC8\u6B62_`));
146
+ } else if (state.terminal === "error" && state.errorMsg) {
147
+ elements.push(noteMd(`\u26A0\uFE0F agent \u5931\u8D25\uFF1A${state.errorMsg}`));
148
+ } else if (state.terminal === "done" && elements.length === 0) {
149
+ elements.push(noteMd("_\uFF08\u672A\u8FD4\u56DE\u5185\u5BB9\uFF09_"));
150
+ }
151
+ if (state.terminal === "running") {
152
+ if (state.footer) elements.push(footerStatus(state.footer));
153
+ elements.push(stopButton());
154
+ }
155
+ return {
156
+ schema: "2.0",
157
+ config: {
158
+ streaming_mode: state.terminal === "running",
159
+ summary: { content: summaryText(state) }
160
+ },
161
+ body: { elements }
162
+ };
163
+ }
164
+ function* groupBlocks(blocks) {
165
+ let toolBuf = [];
166
+ for (const b of blocks) {
167
+ if (b.kind === "tool") {
168
+ toolBuf.push(b.tool);
169
+ } else {
170
+ if (toolBuf.length > 0) {
171
+ yield { kind: "tools", tools: toolBuf };
172
+ toolBuf = [];
173
+ }
174
+ yield { kind: "text", content: b.content };
175
+ }
176
+ }
177
+ if (toolBuf.length > 0) yield { kind: "tools", tools: toolBuf };
178
+ }
179
+ function renderToolGroup(tools, finalized) {
180
+ if (tools.length === 0) return [];
181
+ if (tools.length < COLLAPSE_TOOL_THRESHOLD) {
182
+ return tools.map((t) => toolPanel(t, false));
183
+ }
184
+ if (finalized) {
185
+ return [collapsedToolSummary(tools, true)];
186
+ }
187
+ const prior = tools.slice(0, -1);
188
+ const latest = tools[tools.length - 1];
189
+ const out = [];
190
+ if (prior.length > 0) out.push(collapsedToolSummary(prior, false));
191
+ if (latest) out.push(toolPanel(latest, true));
192
+ return out;
193
+ }
194
+ function reasoningPanel(content, active) {
195
+ const title = active ? "\u{1F9E0} **\u601D\u8003\u4E2D**" : "\u{1F9E0} **\u601D\u8003\u5B8C\u6210\uFF0C\u70B9\u51FB\u67E5\u770B**";
196
+ return collapsiblePanel({
197
+ title,
198
+ expanded: active,
199
+ border: "grey",
200
+ body: truncate2(content, REASONING_MAX)
201
+ });
202
+ }
203
+ function toolPanel(tool, expanded) {
204
+ return collapsiblePanel({
205
+ title: toolHeaderText(tool),
206
+ expanded,
207
+ border: tool.status === "error" ? "red" : "grey",
208
+ body: toolBodyMd(tool) || "_\u65E0\u8F93\u51FA_"
209
+ });
210
+ }
211
+ function collapsedToolSummary(tools, finalized) {
212
+ const suffix = finalized ? "\uFF08\u5DF2\u7ED3\u675F\uFF09" : "";
213
+ const title = `\u2615 **${tools.length} \u4E2A\u5DE5\u5177\u8C03\u7528${suffix}**`;
214
+ const headerList = tools.map((t) => `- ${toolHeaderText(t)}`).join("\n");
215
+ return {
216
+ tag: "collapsible_panel",
217
+ expanded: false,
218
+ header: panelHeader(title),
219
+ border: { color: "blue", corner_radius: "5px" },
220
+ vertical_spacing: "8px",
221
+ padding: "8px 8px 8px 8px",
222
+ elements: [{ tag: "markdown", content: headerList, text_size: "notation" }]
223
+ };
224
+ }
225
+ function collapsiblePanel(opts) {
226
+ return {
227
+ tag: "collapsible_panel",
228
+ expanded: opts.expanded,
229
+ header: panelHeader(opts.title),
230
+ border: { color: opts.border, corner_radius: "5px" },
231
+ vertical_spacing: "8px",
232
+ padding: "8px 8px 8px 8px",
233
+ elements: [{ tag: "markdown", content: opts.body, text_size: "notation" }]
234
+ };
235
+ }
236
+ function panelHeader(titleMd) {
237
+ return {
238
+ title: { tag: "markdown", content: titleMd },
239
+ vertical_align: "center",
240
+ icon: { tag: "standard_icon", token: "down-small-ccm_outlined", size: "16px 16px" },
241
+ icon_position: "follow_text",
242
+ icon_expanded_angle: -180
243
+ };
244
+ }
245
+ function markdown(content) {
246
+ return { tag: "markdown", content };
247
+ }
248
+ function noteMd(content) {
249
+ return { tag: "markdown", content, text_size: "notation" };
250
+ }
251
+ function stopButton() {
252
+ return {
253
+ tag: "button",
254
+ text: { tag: "plain_text", content: "\u23F9 \u7EC8\u6B62" },
255
+ type: "danger",
256
+ behaviors: [{ type: "callback", value: { cmd: "stop" } }]
257
+ };
258
+ }
259
+ function footerStatus(status) {
260
+ const text = status === "thinking" ? "\u{1F9E0} \u6B63\u5728\u601D\u8003" : status === "tool_running" ? "\u{1F9F0} \u6B63\u5728\u8C03\u7528\u5DE5\u5177" : "\u270D\uFE0F \u6B63\u5728\u8F93\u51FA";
261
+ return noteMd(text);
262
+ }
263
+ function summaryText(state) {
264
+ if (state.terminal === "interrupted") return "\u5DF2\u4E2D\u65AD";
265
+ if (state.terminal === "idle_timeout") return "\u5DF2\u8D85\u65F6";
266
+ if (state.terminal === "error") return "\u51FA\u9519";
267
+ if (state.terminal === "done") return "\u5DF2\u5B8C\u6210";
268
+ if (state.footer === "tool_running") return "\u6B63\u5728\u8C03\u7528\u5DE5\u5177";
269
+ if (state.footer === "streaming") return "\u6B63\u5728\u8F93\u51FA";
270
+ return "\u601D\u8003\u4E2D";
271
+ }
272
+ function truncate2(s, max) {
273
+ return s.length > max ? `${s.slice(0, max)}\u2026` : s;
274
+ }
275
+
276
+ // src/card/text-renderer.ts
277
+ function renderText(state) {
278
+ const parts = [];
279
+ for (const block of state.blocks) {
280
+ const piece = renderBlock(block);
281
+ if (piece) parts.push(piece);
282
+ }
283
+ if (state.terminal === "interrupted") {
284
+ parts.push("_\u23F9 \u5DF2\u88AB\u4E2D\u65AD_");
285
+ } else if (state.terminal === "idle_timeout") {
286
+ const mins = state.idleTimeoutMinutes ?? 0;
287
+ parts.push(`_\u23F1 ${mins} \u5206\u949F\u65E0\u54CD\u5E94,\u5DF2\u81EA\u52A8\u7EC8\u6B62_`);
288
+ } else if (state.terminal === "error" && state.errorMsg) {
289
+ parts.push(`\u26A0\uFE0F agent \u5931\u8D25:${state.errorMsg}`);
290
+ } else if (state.terminal === "running" && state.footer) {
291
+ parts.push(footerLine(state.footer));
292
+ }
293
+ return parts.join("\n\n");
294
+ }
295
+ function renderBlock(block) {
296
+ if (block.kind === "text") {
297
+ return block.content.trim();
298
+ }
299
+ return toolLine(block.tool);
300
+ }
301
+ function toolLine(tool) {
302
+ return `> ${toolHeaderText(tool)}`;
303
+ }
304
+ function footerLine(status) {
305
+ if (status === "thinking") return "_\u{1F9E0} \u6B63\u5728\u601D\u8003\u2026_";
306
+ if (status === "tool_running") return "_\u{1F9F0} \u6B63\u5728\u8C03\u7528\u5DE5\u5177\u2026_";
307
+ return "_\u270D\uFE0F \u6B63\u5728\u8F93\u51FA\u2026_";
308
+ }
309
+
310
+ // src/card/run-state.ts
311
+ var initialState = {
312
+ blocks: [],
313
+ reasoning: { content: "", active: false },
314
+ footer: "thinking",
315
+ terminal: "running"
316
+ };
317
+ function closeStreamingText(blocks) {
318
+ return blocks.map(
319
+ (b) => b.kind === "text" && b.streaming ? { ...b, streaming: false } : b
320
+ );
321
+ }
322
+ function reduce(state, evt) {
323
+ switch (evt.type) {
324
+ case "text": {
325
+ const last = state.blocks[state.blocks.length - 1];
326
+ if (last && last.kind === "text" && last.streaming) {
327
+ const next = { ...last, content: last.content + evt.delta };
328
+ return {
329
+ ...state,
330
+ blocks: [...state.blocks.slice(0, -1), next],
331
+ reasoning: { ...state.reasoning, active: false },
332
+ footer: "streaming"
333
+ };
334
+ }
335
+ return {
336
+ ...state,
337
+ blocks: [...state.blocks, { kind: "text", content: evt.delta, streaming: true }],
338
+ reasoning: { ...state.reasoning, active: false },
339
+ footer: "streaming"
340
+ };
341
+ }
342
+ case "thinking": {
343
+ return {
344
+ ...state,
345
+ reasoning: { content: state.reasoning.content + evt.delta, active: true },
346
+ footer: "thinking"
347
+ };
348
+ }
349
+ case "tool_use": {
350
+ const tool = {
351
+ id: evt.id,
352
+ name: evt.name,
353
+ input: evt.input,
354
+ status: "running"
355
+ };
356
+ return {
357
+ ...state,
358
+ blocks: [...closeStreamingText(state.blocks), { kind: "tool", tool }],
359
+ reasoning: { ...state.reasoning, active: false },
360
+ footer: "tool_running"
361
+ };
362
+ }
363
+ case "tool_result": {
364
+ const blocks = state.blocks.map((b) => {
365
+ if (b.kind !== "tool" || b.tool.id !== evt.id) return b;
366
+ return {
367
+ ...b,
368
+ tool: {
369
+ ...b.tool,
370
+ status: evt.isError ? "error" : "done",
371
+ output: evt.output
372
+ }
373
+ };
374
+ });
375
+ return { ...state, blocks };
376
+ }
377
+ case "error": {
378
+ return { ...state, terminal: "error", errorMsg: evt.message, footer: null };
379
+ }
380
+ case "done": {
381
+ return {
382
+ ...state,
383
+ blocks: closeStreamingText(state.blocks),
384
+ reasoning: { ...state.reasoning, active: false },
385
+ terminal: "done",
386
+ footer: null
387
+ };
388
+ }
389
+ default:
390
+ return state;
391
+ }
392
+ }
393
+ function markInterrupted(state) {
394
+ return {
395
+ ...state,
396
+ blocks: closeStreamingText(state.blocks),
397
+ reasoning: { ...state.reasoning, active: false },
398
+ terminal: "interrupted",
399
+ footer: null
400
+ };
401
+ }
402
+ function finalizeIfRunning(state) {
403
+ if (state.terminal !== "running") return state;
404
+ return {
405
+ ...state,
406
+ blocks: closeStreamingText(state.blocks),
407
+ reasoning: { ...state.reasoning, active: false },
408
+ terminal: "done",
409
+ footer: null
410
+ };
411
+ }
412
+ export {
413
+ finalizeIfRunning,
414
+ initialState,
415
+ markInterrupted,
416
+ reduce,
417
+ renderCard,
418
+ renderText
419
+ };
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "lark-codex-bridge",
3
+ "version": "0.0.1",
4
+ "description": "Bridge Feishu/Lark messenger with local Codex CLI coding agents",
5
+ "type": "module",
6
+ "packageManager": "pnpm@10.14.0",
7
+ "bin": {
8
+ "lark-codex-bridge": "./bin/lark-codex-bridge.mjs"
9
+ },
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "bin",
19
+ "README.md",
20
+ "README.zh.md",
21
+ "CHANGELOG.md",
22
+ "LICENSE"
23
+ ],
24
+ "scripts": {
25
+ "dev": "tsup --watch",
26
+ "build": "tsup",
27
+ "typecheck": "tsc --noEmit",
28
+ "test": "vitest run",
29
+ "verify": "pnpm typecheck && pnpm build && pnpm test && node dist/cli.js --help && npm --cache .npm-cache pack --dry-run",
30
+ "prepublishOnly": "pnpm verify"
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/xile611/lark-codex-bridge.git"
35
+ },
36
+ "bugs": {
37
+ "url": "https://github.com/xile611/lark-codex-bridge/issues"
38
+ },
39
+ "homepage": "https://github.com/xile611/lark-codex-bridge#readme",
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "dependencies": {
44
+ "@larksuiteoapi/node-sdk": "^1.64.0",
45
+ "commander": "^12.1.0",
46
+ "https-proxy-agent": "^9.0.0",
47
+ "qrcode-terminal": "^0.12.0"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^22.10.0",
51
+ "@types/qrcode-terminal": "^0.12.2",
52
+ "tsup": "^8.3.5",
53
+ "typescript": "^5.6.3",
54
+ "vitest": "^2.1.8"
55
+ },
56
+ "engines": {
57
+ "node": ">=20.0.0"
58
+ },
59
+ "pnpm": {
60
+ "onlyBuiltDependencies": ["esbuild", "protobufjs"]
61
+ },
62
+ "keywords": [
63
+ "feishu",
64
+ "lark",
65
+ "codex",
66
+ "codex-cli",
67
+ "cli",
68
+ "channel",
69
+ "bridge"
70
+ ],
71
+ "license": "MIT"
72
+ }