opencode-feishu-plugin 0.2.6 → 0.2.7
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/README.en.md +12 -0
- package/README.md +14 -0
- package/dist/index.js +146 -5
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -402,9 +402,21 @@ Without this relay, any clarifying question would stall the Feishu session forev
|
|
|
402
402
|
| `topicStatusInTitle` | boolean | `false` | Add a status emoji prefix to the root card title (e.g. `🟡 session name`). Off by default: the topic name shows in the sidebar, and flipping it would be noisy |
|
|
403
403
|
| `topicStatusThrottleMs` | number | `1000` | Min root-card status refresh interval (clamped 500–10000); patched only when the kind changes |
|
|
404
404
|
| `cardMaxTables` | number | `4` | Max markdown tables kept per card (clamped 1–5); tables beyond it are degraded **cumulatively per card** into fenced code blocks (no content lost) to avoid Feishu 400 `code=230099` |
|
|
405
|
+
| `runnerCardMaxTools` | number | `12` | Max tool blocks kept on the run card (1–50); older ones collapse into "…omitted N tool calls" |
|
|
406
|
+
| `runnerCardTextMax` | number | `2048` | Per-text-block character cap on the run card (512–8192) |
|
|
407
|
+
| `finalAnswerMinChars` | number | `600` | Final answers at least this long are sent **as their own card/file** (`0` disables splitting) |
|
|
408
|
+
| `finalAnswerFileMinBytes` | number | `20480` | Final answers at least this many bytes are delivered as a `.md` file (8192–102400) |
|
|
405
409
|
| `keepalive` | boolean | `true` | **Location keep-alive**: periodically emits activity so OpenCode does not evict the idle Location after 60 minutes (which unloads the plugin and closes the Feishu long connection) |
|
|
406
410
|
| `keepaliveIntervalMs` | number | `1200000` | Keep-alive interval (default 20 min, clamped 5–45); must stay well below OpenCode's hardcoded 60-minute TTL |
|
|
407
411
|
|
|
412
|
+
### Long answers (run-card slimming + separate final answer)
|
|
413
|
+
|
|
414
|
+
A long turn (dozens of tool calls) can push a single run card to its limits (28KB / 200 elements), triggering degradation and **dropping the oldest blocks** — the card looks "full" and earlier content disappears. Default strategy:
|
|
415
|
+
|
|
416
|
+
1. **The run card is progress-only**: keep the last `runnerCardMaxTools` (default 12) tool blocks, collapsing older ones into "…omitted N tool calls"; each text block is capped at `runnerCardTextMax` (default 2KB).
|
|
417
|
+
2. **The final answer is sent separately**: when a turn ends and the trailing text is at least `finalAnswerMinChars` (default 600 chars), it is sent as its own "✅ 完整回答" card; the run card keeps only a short notice.
|
|
418
|
+
3. **Very long answers become files**: at least `finalAnswerFileMinBytes` (default 20KB) → delivered as a `.md` file (preview/download), never truncated.
|
|
419
|
+
|
|
408
420
|
### Location keep-alive (on by default)
|
|
409
421
|
|
|
410
422
|
OpenCode **evicts idle Locations**, which unloads plugins and closes the Feishu long connection:
|
package/README.md
CHANGED
|
@@ -263,6 +263,10 @@ agent 调 `question` 等 form 类交互时,插件把它转成飞书卡片:
|
|
|
263
263
|
| `topicStatusInTitle` | boolean | `false` | 是否在根卡标题加状态 emoji 前缀 |
|
|
264
264
|
| `topicStatusThrottleMs` | number | `1000` | 根卡状态刷新最小间隔(500–10000) |
|
|
265
265
|
| `cardMaxTables` | number | `4` | 单卡最多保留的 markdown 表格数(1–5);超出按整卡累计降级为围栏代码块,避免飞书 400 `code=230099` |
|
|
266
|
+
| `runnerCardMaxTools` | number | `12` | 运行卡最多保留的工具块数(1–50);更早的合并为「已省略前 N 次工具调用」 |
|
|
267
|
+
| `runnerCardTextMax` | number | `2048` | 运行卡单个文本块字符上限(512–8192) |
|
|
268
|
+
| `finalAnswerMinChars` | number | `600` | 最终回答 ≥ 该长度即**单独成卡/成文件**(0 = 关闭拆分) |
|
|
269
|
+
| `finalAnswerFileMinBytes` | number | `20480` | 最终回答 ≥ 该字节数转为 `.md` 文件发送(8192–102400) |
|
|
266
270
|
| `keepalive` | boolean | `true` | **位置保活**:周期性向 opencode 发一次活动,阻止 60 分钟空闲回收 location(会关掉飞书长连接、机器人失联) |
|
|
267
271
|
| `keepaliveIntervalMs` | number | `1200000` | 保活间隔(默认 20 分钟,夹取 5–45);必须显著小于 opencode 硬编码的 60 分钟 TTL |
|
|
268
272
|
|
|
@@ -284,6 +288,16 @@ permission.evaluate (插件 hook) permission.asked (事件流)
|
|
|
284
288
|
- **三重单人边界**:可用范围「仅本人」+ 不申请群权限 + 代码层 open_id 白名单。
|
|
285
289
|
- **`always` 语义**:仅当请求带 `save[]` 时才持久化,否则等价于「允许一次」。
|
|
286
290
|
|
|
291
|
+
### 长回答处理(运行卡瘦身 + 最终答案独立)
|
|
292
|
+
|
|
293
|
+
长任务(几十次工具调用)会把单张运行卡撑到上限(28KB / 200 元素),触发降级与**丢弃最旧块**——用户会看到"卡被撑满、前面内容消失"。默认策略:
|
|
294
|
+
|
|
295
|
+
1. **运行卡只做进度**:最多保留最近 `runnerCardMaxTools`(默认 12)个工具块,更早的合并为「…已省略前 N 次工具调用」;单个文本块上限 `runnerCardTextMax`(默认 2KB)。
|
|
296
|
+
2. **最终答案独立发送**:一轮结束时末尾文本 ≥ `finalAnswerMinChars`(默认 600 字符)→ 单独发一张「✅ 完整回答」卡(不与被工具噪声塞满的运行卡抢空间);运行卡内只留「完整回答已单独发送」提示。
|
|
297
|
+
3. **超长转文件**:最终回答 ≥ `finalAnswerFileMinBytes`(默认 20KB)→ 作为 `.md` 文件发送(可预览/下载),内容不截断、不丢失。
|
|
298
|
+
|
|
299
|
+
> 想要"完整轨迹保留、不省略工具调用":把 `runnerCardMaxTools` 调到足够大并接受卡片消息变多(或提高 `finalAnswerMinChars` 降低拆分频率)。
|
|
300
|
+
|
|
287
301
|
### 位置保活(防空闲失联,默认开启)
|
|
288
302
|
|
|
289
303
|
opencode 会**回收空闲的 location**,这会连带卸载插件、关闭飞书长连接:
|
package/dist/index.js
CHANGED
|
@@ -144025,6 +144025,10 @@ function resolveConfig2(raw, env = process.env, deps = {}) {
|
|
|
144025
144025
|
const topicStatusThrottleMs = clamp4(asNumber(merged.topicStatusThrottleMs, 1e3), 500, 1e4);
|
|
144026
144026
|
const cardMaxTables = clamp4(asNumber(merged.cardMaxTables, 4), 1, 5);
|
|
144027
144027
|
const keepalive = asBoolean(merged.keepalive, true);
|
|
144028
|
+
const runnerCardMaxTools = clamp4(asNumber(merged.runnerCardMaxTools, 12), 1, 50);
|
|
144029
|
+
const runnerCardTextMax = clamp4(asNumber(merged.runnerCardTextMax, 2048), 512, 8192);
|
|
144030
|
+
const finalAnswerMinChars = clamp4(asNumber(merged.finalAnswerMinChars, 600), 0, 2e4);
|
|
144031
|
+
const finalAnswerFileMinBytes = clamp4(asNumber(merged.finalAnswerFileMinBytes, 20 * 1024), 8192, 102400);
|
|
144028
144032
|
const keepaliveIntervalMs = clamp4(
|
|
144029
144033
|
asNumber(merged.keepaliveIntervalMs, 20 * 60 * 1e3),
|
|
144030
144034
|
5 * 60 * 1e3,
|
|
@@ -144081,7 +144085,11 @@ function resolveConfig2(raw, env = process.env, deps = {}) {
|
|
|
144081
144085
|
topicStatusThrottleMs,
|
|
144082
144086
|
cardMaxTables,
|
|
144083
144087
|
keepalive,
|
|
144084
|
-
keepaliveIntervalMs
|
|
144088
|
+
keepaliveIntervalMs,
|
|
144089
|
+
runnerCardMaxTools,
|
|
144090
|
+
runnerCardTextMax,
|
|
144091
|
+
finalAnswerMinChars,
|
|
144092
|
+
finalAnswerFileMinBytes
|
|
144085
144093
|
};
|
|
144086
144094
|
}
|
|
144087
144095
|
function loadConfigFile(env, deps, warnings) {
|
|
@@ -145173,6 +145181,29 @@ function createFeishuSender(client, log2, options2 = {}) {
|
|
|
145173
145181
|
return void 0;
|
|
145174
145182
|
}
|
|
145175
145183
|
},
|
|
145184
|
+
async sendFile(chatId, fileName, content, replyToMessageId) {
|
|
145185
|
+
try {
|
|
145186
|
+
const uploaded = await client.im.file.create({
|
|
145187
|
+
data: { file_type: "stream", file_name: fileName, file: content }
|
|
145188
|
+
});
|
|
145189
|
+
const fileKey = uploaded?.file_key ?? "";
|
|
145190
|
+
if (!fileKey) return { ok: false, error: "missing file_key" };
|
|
145191
|
+
const payload = { msg_type: "file", content: JSON.stringify({ file_key: fileKey }) };
|
|
145192
|
+
const res = replyToMessageId ? await client.im.message.reply({ path: { message_id: replyToMessageId }, data: payload }) : await client.im.message.create({
|
|
145193
|
+
params: { receive_id_type: "chat_id" },
|
|
145194
|
+
data: { receive_id: chatId, ...payload }
|
|
145195
|
+
});
|
|
145196
|
+
if (res?.code && res.code !== 0) {
|
|
145197
|
+
log2.warn("\u53D1\u9001\u6587\u4EF6\u5931\u8D25", { chatId, fileName, code: res.code, msg: res.msg });
|
|
145198
|
+
return { ok: false, error: `code=${res.code} msg=${res.msg ?? ""}` };
|
|
145199
|
+
}
|
|
145200
|
+
const messageId = res?.data?.message_id ?? "";
|
|
145201
|
+
return messageId ? { ok: true, messageId } : { ok: true };
|
|
145202
|
+
} catch (err2) {
|
|
145203
|
+
log2.warn("\u53D1\u9001\u6587\u4EF6\u5F02\u5E38", { chatId, fileName, error: describeLarkError(err2) });
|
|
145204
|
+
return { ok: false, error: describeLarkError(err2) };
|
|
145205
|
+
}
|
|
145206
|
+
},
|
|
145176
145207
|
async deleteMessage(messageId) {
|
|
145177
145208
|
if (!messageId) return { ok: false, error: "missing messageId" };
|
|
145178
145209
|
try {
|
|
@@ -145885,6 +145916,8 @@ function reduce6(state, event) {
|
|
|
145885
145916
|
status: "error",
|
|
145886
145917
|
output: event.output
|
|
145887
145918
|
}));
|
|
145919
|
+
case "final.separated":
|
|
145920
|
+
return { ...state, finalSeparated: true };
|
|
145888
145921
|
default:
|
|
145889
145922
|
return state;
|
|
145890
145923
|
}
|
|
@@ -146061,6 +146094,14 @@ function closeCodeFences(text) {
|
|
|
146061
146094
|
if (fences && fences.length % 2 !== 0) return `${text}${CODE_FENCE}`;
|
|
146062
146095
|
return text;
|
|
146063
146096
|
}
|
|
146097
|
+
function buildFinalAnswerCard(text) {
|
|
146098
|
+
return {
|
|
146099
|
+
schema: "2.0",
|
|
146100
|
+
config: { update_multi: true },
|
|
146101
|
+
header: { title: { tag: "plain_text", content: "\u2705 \u5B8C\u6574\u56DE\u7B54" }, template: "green" },
|
|
146102
|
+
body: { elements: [{ tag: "markdown", content: truncateCardContent(text, 24 * 1024) }] }
|
|
146103
|
+
};
|
|
146104
|
+
}
|
|
146064
146105
|
|
|
146065
146106
|
// src/feishu/run-renderer.ts
|
|
146066
146107
|
var COLLAPSE_TOOL_THRESHOLD = 3;
|
|
@@ -146068,11 +146109,30 @@ var HEADER_SUMMARY_MAX = 80;
|
|
|
146068
146109
|
var TOOL_INPUT_MAX = 600;
|
|
146069
146110
|
var OUTPUT_FIRST_LINE_MAX = 200;
|
|
146070
146111
|
var TEXT_ELEMENT_MAX = 8 * 1024;
|
|
146112
|
+
var DEFAULT_MAX_TOOLS = 12;
|
|
146113
|
+
var DEFAULT_TEXT_MAX = 2 * 1024;
|
|
146071
146114
|
function renderRunCard(state, stopValue, options2 = {}) {
|
|
146072
146115
|
const elements = [];
|
|
146073
|
-
|
|
146116
|
+
const maxTools = options2.maxTools ?? DEFAULT_MAX_TOOLS;
|
|
146117
|
+
const textMax = options2.textMax ?? DEFAULT_TEXT_MAX;
|
|
146118
|
+
const capped = capToolBlocks(state.blocks, maxTools);
|
|
146119
|
+
if (capped.omitted > 0) {
|
|
146120
|
+
elements.push(note(`\u2026\u5DF2\u7701\u7565\u524D ${capped.omitted} \u6B21\u5DE5\u5177\u8C03\u7528\uFF08\u5B8C\u6574\u8BB0\u5F55\u89C1 opencode \u4F1A\u8BDD\uFF09`));
|
|
146121
|
+
}
|
|
146122
|
+
const groups = groupBlocks(capped.blocks);
|
|
146123
|
+
const lastTextIndex = groups.reduce(
|
|
146124
|
+
(acc, g, i) => g.kind === "text" && g.content.trim() ? i : acc,
|
|
146125
|
+
-1
|
|
146126
|
+
);
|
|
146127
|
+
for (let i = 0; i < groups.length; i += 1) {
|
|
146128
|
+
const group = groups[i];
|
|
146074
146129
|
if (group.kind === "text") {
|
|
146075
|
-
if (group.content.trim())
|
|
146130
|
+
if (!group.content.trim()) continue;
|
|
146131
|
+
if (state.finalSeparated && i === lastTextIndex) {
|
|
146132
|
+
elements.push(note("\u2705 \u5B8C\u6574\u56DE\u7B54\u5DF2\u5355\u72EC\u53D1\u9001\uFF08\u89C1\u4E0B\u65B9\u5361\u7247 / \u9644\u4EF6\uFF09"));
|
|
146133
|
+
continue;
|
|
146134
|
+
}
|
|
146135
|
+
elements.push(markdown(truncateCardContent(group.content, Math.min(textMax, TEXT_ELEMENT_MAX))));
|
|
146076
146136
|
} else {
|
|
146077
146137
|
elements.push(...renderToolGroup(group.tools, state.terminal !== "running"));
|
|
146078
146138
|
}
|
|
@@ -146109,6 +146169,20 @@ function renderRunCard(state, stopValue, options2 = {}) {
|
|
|
146109
146169
|
}
|
|
146110
146170
|
return enforceSize(guarded);
|
|
146111
146171
|
}
|
|
146172
|
+
function capToolBlocks(blocks, maxTools) {
|
|
146173
|
+
const total2 = blocks.reduce((n, b) => n + (b.kind === "tool" ? 1 : 0), 0);
|
|
146174
|
+
if (maxTools <= 0 || total2 <= maxTools) return { blocks: [...blocks], omitted: 0 };
|
|
146175
|
+
let toDrop = total2 - maxTools;
|
|
146176
|
+
const out = [];
|
|
146177
|
+
for (const block of blocks) {
|
|
146178
|
+
if (block.kind === "tool" && toDrop > 0) {
|
|
146179
|
+
toDrop -= 1;
|
|
146180
|
+
continue;
|
|
146181
|
+
}
|
|
146182
|
+
out.push(block);
|
|
146183
|
+
}
|
|
146184
|
+
return { blocks: out, omitted: total2 - maxTools };
|
|
146185
|
+
}
|
|
146112
146186
|
function groupBlocks(blocks) {
|
|
146113
146187
|
const groups = [];
|
|
146114
146188
|
let toolBuf = [];
|
|
@@ -146295,6 +146369,19 @@ function byteLength(value3) {
|
|
|
146295
146369
|
}
|
|
146296
146370
|
|
|
146297
146371
|
// src/feishu/run-controller.ts
|
|
146372
|
+
function trailingText(blocks) {
|
|
146373
|
+
const parts = [];
|
|
146374
|
+
for (let i = blocks.length - 1; i >= 0; i -= 1) {
|
|
146375
|
+
const block = blocks[i];
|
|
146376
|
+
if (block.kind === "tool") {
|
|
146377
|
+
if (parts.length > 0) break;
|
|
146378
|
+
continue;
|
|
146379
|
+
}
|
|
146380
|
+
if (block.content.trim()) parts.unshift(block.content);
|
|
146381
|
+
else if (parts.length > 0) break;
|
|
146382
|
+
}
|
|
146383
|
+
return parts.join("\n\n").trim();
|
|
146384
|
+
}
|
|
146298
146385
|
function createRunController(deps) {
|
|
146299
146386
|
const sessions = /* @__PURE__ */ new Map();
|
|
146300
146387
|
const throttlers = /* @__PURE__ */ new Map();
|
|
@@ -146312,6 +146399,8 @@ function createRunController(deps) {
|
|
|
146312
146399
|
};
|
|
146313
146400
|
const renderCard = (sessionID, state) => renderRunCard(state, deps.buildStopValue?.(sessionID), {
|
|
146314
146401
|
maxTables: deps.cardMaxTables,
|
|
146402
|
+
maxTools: deps.runnerCardMaxTools,
|
|
146403
|
+
textMax: deps.runnerCardTextMax,
|
|
146315
146404
|
onLimit: (report) => {
|
|
146316
146405
|
deps.log.warn("\u8FD0\u884C\u5361\u5185\u5BB9\u8D85\u9650\uFF0C\u5DF2\u964D\u7EA7", {
|
|
146317
146406
|
sessionID,
|
|
@@ -146389,7 +146478,8 @@ function createRunController(deps) {
|
|
|
146389
146478
|
state,
|
|
146390
146479
|
chain: Promise.resolve(),
|
|
146391
146480
|
finalized: false,
|
|
146392
|
-
queuedAt: Date.now()
|
|
146481
|
+
queuedAt: Date.now(),
|
|
146482
|
+
...input.replyToMessageId ? { replyToMessageId: input.replyToMessageId } : {}
|
|
146393
146483
|
};
|
|
146394
146484
|
if (input.delivery === "queue") {
|
|
146395
146485
|
runs.queued.push(card);
|
|
@@ -146419,7 +146509,29 @@ function createRunController(deps) {
|
|
|
146419
146509
|
}
|
|
146420
146510
|
const card = runs.active;
|
|
146421
146511
|
if (!card) return;
|
|
146422
|
-
if (event.type === "execution.succeeded"
|
|
146512
|
+
if (event.type === "execution.succeeded") {
|
|
146513
|
+
const text = trailingText(card.state.blocks);
|
|
146514
|
+
const long = deps.sendFinalAnswer !== void 0 && deps.finalAnswer !== void 0 && text.length >= deps.finalAnswer.minChars;
|
|
146515
|
+
if (long) {
|
|
146516
|
+
card.state = reduce6(reduce6(card.state, { type: "final.separated" }), event);
|
|
146517
|
+
finalize2(card);
|
|
146518
|
+
runs.active = void 0;
|
|
146519
|
+
void deps.sendFinalAnswer({
|
|
146520
|
+
sessionID,
|
|
146521
|
+
chatId: card.chatId,
|
|
146522
|
+
...card.replyToMessageId ? { replyToMessageId: card.replyToMessageId } : {},
|
|
146523
|
+
text
|
|
146524
|
+
}).catch((err2) => {
|
|
146525
|
+
deps.log.warn("\u6700\u7EC8\u7B54\u6848\u53D1\u9001\u5931\u8D25", { sessionID, error: errorMessage(err2) });
|
|
146526
|
+
});
|
|
146527
|
+
return;
|
|
146528
|
+
}
|
|
146529
|
+
update2(card, event, true);
|
|
146530
|
+
finalize2(card);
|
|
146531
|
+
runs.active = void 0;
|
|
146532
|
+
return;
|
|
146533
|
+
}
|
|
146534
|
+
if (event.type === "execution.failed") {
|
|
146423
146535
|
update2(card, event, true);
|
|
146424
146536
|
finalize2(card);
|
|
146425
146537
|
runs.active = void 0;
|
|
@@ -151194,9 +151306,38 @@ async function start(ctx, config, log2, logSink) {
|
|
|
151194
151306
|
enabled: config.stream,
|
|
151195
151307
|
throttleMs: config.streamThrottleMs,
|
|
151196
151308
|
cardMaxTables: config.cardMaxTables,
|
|
151309
|
+
runnerCardMaxTools: config.runnerCardMaxTools,
|
|
151310
|
+
runnerCardTextMax: config.runnerCardTextMax,
|
|
151311
|
+
finalAnswer: { minChars: config.finalAnswerMinChars },
|
|
151312
|
+
// 长回答单独成卡/成文件:运行卡只留「完整回答已单独发送」提示。
|
|
151313
|
+
sendFinalAnswer: (input) => sendFinalAnswer(input),
|
|
151197
151314
|
// 每次 patch 重签强停 token(`stop` 在下方定义,闭包运行时才求值)。
|
|
151198
151315
|
buildStopValue: (sessionID) => stop.buildStopValue(sessionID)
|
|
151199
151316
|
});
|
|
151317
|
+
async function sendFinalAnswer(input) {
|
|
151318
|
+
const bytes = Buffer.byteLength(input.text, "utf8");
|
|
151319
|
+
if (bytes > config.finalAnswerFileMinBytes) {
|
|
151320
|
+
const fileName = `opencode-${input.sessionID.slice(-6)}.md`;
|
|
151321
|
+
const res2 = await sender.sendFile(
|
|
151322
|
+
input.chatId,
|
|
151323
|
+
fileName,
|
|
151324
|
+
Buffer.from(input.text, "utf8"),
|
|
151325
|
+
input.replyToMessageId
|
|
151326
|
+
);
|
|
151327
|
+
if (res2.ok) {
|
|
151328
|
+
log2.info("\u6700\u7EC8\u7B54\u6848\u5DF2\u8F6C\u4E3A\u6587\u4EF6\u53D1\u9001", { sessionID: input.sessionID, bytes, fileName });
|
|
151329
|
+
return;
|
|
151330
|
+
}
|
|
151331
|
+
log2.warn("\u6700\u7EC8\u7B54\u6848\u8F6C\u6587\u4EF6\u5931\u8D25\uFF0C\u964D\u7EA7\u4E3A\u5361\u7247", { sessionID: input.sessionID, error: res2.error ?? "unknown" });
|
|
151332
|
+
}
|
|
151333
|
+
const card = buildFinalAnswerCard(input.text);
|
|
151334
|
+
const res = input.replyToMessageId ? await sender.replyCard(input.replyToMessageId, card) : await sender.sendCard(input.chatId, card);
|
|
151335
|
+
if (!res.ok) {
|
|
151336
|
+
log2.warn("\u6700\u7EC8\u7B54\u6848\u5361\u7247\u53D1\u9001\u5931\u8D25", { sessionID: input.sessionID, error: res.error ?? "unknown" });
|
|
151337
|
+
return;
|
|
151338
|
+
}
|
|
151339
|
+
log2.info("\u6700\u7EC8\u7B54\u6848\u5DF2\u5355\u72EC\u6210\u5361", { sessionID: input.sessionID, bytes });
|
|
151340
|
+
}
|
|
151200
151341
|
const recovery = createSessionRecovery({
|
|
151201
151342
|
log: log2,
|
|
151202
151343
|
resolveLink: (sessionID) => sessionMap.resolveBySession(sessionID),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-feishu-plugin",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "Feishu/Lark plugin for OpenCode V2 — single-user topics as sessions, in-card permission approvals, streaming cards. Minimal permissions, no public endpoint.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|