larkin 0.3.0 → 0.3.2

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.md CHANGED
@@ -5,7 +5,7 @@ Larkin connects Codex, Claude Code, and Pi agent runtimes to Feishu (Lark). It p
5
5
  ## Requirements
6
6
 
7
7
  - A supported macOS, Linux, or Windows (x64) system
8
- - `lark-cli`
8
+ - Official `@larksuite/cli >= 1.0.80` (`lark-cli`) (Larkin product policy)
9
9
  - At least one supported agent runtime and its authentication
10
10
  - Bun 1.3.14 when running the npm package or building from source (standalone binaries bundle their own runtime)
11
11
 
@@ -36,6 +36,10 @@ The rest of this document uses the short `larkin` form; it works as-is after `np
36
36
 
37
37
  Run `larkin --help` or `larkin config --help` for the available commands and configuration options. Local configuration is stored under `~/.larkin` by default; set `LARKIN_CONFIG_DIR` to use another directory.
38
38
 
39
+ ### Feishu message links
40
+
41
+ Feishu clients do not reliably render Markdown links such as `[label](URL)` as clickable in text or Markdown messages. When a recipient must be able to open a link, keep the complete bare HTTPS URL visible, for example: Issue 115 — https://github.com/eddiearc/larkin/issues/115. A label may accompany it, but must not replace the bare URL. Larkin does not rewrite exact, verbatim, or user-authored message bodies to enforce this guidance.
42
+
39
43
  During setup, a new Agent is offered Pi first, followed by Codex and Claude Code. Pi can use an existing
40
44
  official `pi` installation or the official Pi runtime bundled in Larkin. The bundled option supports
41
45
  DeepSeek, Kimi/Moonshot, MiniMax, Zhipu/BigModel, and a custom OpenAI-compatible Base URL. Provider keys
@@ -16,7 +16,7 @@ const agent_cli_capabilities_js_1 = require("./agent-cli-capabilities.cjs");
16
16
  * 5. 版本化:每次实质改动 bump LARKIN_STANDING_PROMPT_VERSION,并同步 eval 数据集/graders/断言。
17
17
  * 6. 用 eval 验证:行为变化必须配套固定场景 + rubric(evals/*、test/support/*-grader.mjs、live 测试)。
18
18
  */
19
- exports.LARKIN_STANDING_PROMPT_VERSION = "larkin-standing-v19";
19
+ exports.LARKIN_STANDING_PROMPT_VERSION = "larkin-standing-v20";
20
20
  /**
21
21
  * Agent 间协作唤醒引导(issue #75):纯文本 @ 不会产生飞书 mention 事件,
22
22
  * 必须由模型直接构造真实的 mention 元素;`--mention` 扩展参数已移除。
@@ -132,7 +132,9 @@ class ContextPromptBuilder {
132
132
  "Only a real Feishu `message_id` beginning with `om_` may be passed to `+messages-reply`; `rem_`, `redeliver_`, and every other synthetic ID must never be replied to. Send with a confirmed `chat_id` and never guess a chat id from a display name.",
133
133
  "Before every send/reply/card write, Larkin probes the exact chat or thread history with the current Bot identity. A nonzero `freshness_conflict` includes bounded unseen context and direct-acks that cursor; reconsider it, then retry the ordinary command. Larkin never saves the blocked message body as a draft.",
134
134
  "For regular textual message bodies, default to `--markdown`, including brief single-line replies, so Feishu renders Markdown structure instead of showing its markers literally.",
135
+ "When a URL must be visible, clickable, or openable by the recipient, include the complete bare `https://...` URL as visible text. Do not rely solely on `[label](URL)`, because Feishu client rendering is unreliable. A label may also be included, but the bare URL must remain present.",
135
136
  "Use native `--text` only when plain text or verbatim preservation is explicitly needed, such as logs, code, or exact whitespace. Both `--markdown` and `--text` remain supported in the Larkin Runtime.",
137
+ "Never rewrite or normalize an exact or verbatim user-supplied body to expose a URL; the existing exact-content paths remain authoritative and preserve the supplied body unchanged.",
136
138
  "For exact text supplied directly in the current instruction or Inbox event, pass the body unchanged as one literal `--text` argument. A direct literal must not use command substitution, backticks, `eval`, `echo`, or an unquoted variable; if it cannot be represented safely, stop and report the limitation instead of normalizing it.",
137
139
  "An explicit exact or verbatim direct literal uses `--text` and overrides the regular markdown default.",
138
140
  `For a common exact send with a confirmed chat id, use the complete schematic recipe \`${executable} im +messages-send --chat-id <confirmed_chat_id> --text '<exact_body_as_one_literal_argument>' --json\`; replace each placeholder with the corresponding confirmed or exact literal value.`,
@@ -13,7 +13,7 @@ import { agentCliPromptCapabilities } from "./agent-cli-capabilities.mjs";
13
13
  * 5. 版本化:每次实质改动 bump LARKIN_STANDING_PROMPT_VERSION,并同步 eval 数据集/graders/断言。
14
14
  * 6. 用 eval 验证:行为变化必须配套固定场景 + rubric(evals/*、test/support/*-grader.mjs、live 测试)。
15
15
  */
16
- export const LARKIN_STANDING_PROMPT_VERSION = "larkin-standing-v19";
16
+ export const LARKIN_STANDING_PROMPT_VERSION = "larkin-standing-v20";
17
17
  /**
18
18
  * Agent 间协作唤醒引导(issue #75):纯文本 @ 不会产生飞书 mention 事件,
19
19
  * 必须由模型直接构造真实的 mention 元素;`--mention` 扩展参数已移除。
@@ -129,7 +129,9 @@ export class ContextPromptBuilder {
129
129
  "Only a real Feishu `message_id` beginning with `om_` may be passed to `+messages-reply`; `rem_`, `redeliver_`, and every other synthetic ID must never be replied to. Send with a confirmed `chat_id` and never guess a chat id from a display name.",
130
130
  "Before every send/reply/card write, Larkin probes the exact chat or thread history with the current Bot identity. A nonzero `freshness_conflict` includes bounded unseen context and direct-acks that cursor; reconsider it, then retry the ordinary command. Larkin never saves the blocked message body as a draft.",
131
131
  "For regular textual message bodies, default to `--markdown`, including brief single-line replies, so Feishu renders Markdown structure instead of showing its markers literally.",
132
+ "When a URL must be visible, clickable, or openable by the recipient, include the complete bare `https://...` URL as visible text. Do not rely solely on `[label](URL)`, because Feishu client rendering is unreliable. A label may also be included, but the bare URL must remain present.",
132
133
  "Use native `--text` only when plain text or verbatim preservation is explicitly needed, such as logs, code, or exact whitespace. Both `--markdown` and `--text` remain supported in the Larkin Runtime.",
134
+ "Never rewrite or normalize an exact or verbatim user-supplied body to expose a URL; the existing exact-content paths remain authoritative and preserve the supplied body unchanged.",
133
135
  "For exact text supplied directly in the current instruction or Inbox event, pass the body unchanged as one literal `--text` argument. A direct literal must not use command substitution, backticks, `eval`, `echo`, or an unquoted variable; if it cannot be represented safely, stop and report the limitation instead of normalizing it.",
134
136
  "An explicit exact or verbatim direct literal uses `--text` and overrides the regular markdown default.",
135
137
  `For a common exact send with a confirmed chat id, use the complete schematic recipe \`${executable} im +messages-send --chat-id <confirmed_chat_id> --text '<exact_body_as_one_literal_argument>' --json\`; replace each placeholder with the corresponding confirmed or exact literal value.`,
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.OFFICIAL_LARK_CLI_INSTALL = exports.OFFICIAL_LARK_CLI_VERSION = void 0;
7
+ exports.formatOfficialLarkCliConsent = formatOfficialLarkCliConsent;
7
8
  exports.invalidateOfficialLarkCliProbeCache = invalidateOfficialLarkCliProbeCache;
8
9
  exports.probeOfficialLarkCli = probeOfficialLarkCli;
9
10
  exports.resolveOfficialLarkCli = resolveOfficialLarkCli;
@@ -20,8 +21,25 @@ function safeRealpath(target) {
20
21
  return null;
21
22
  }
22
23
  }
23
- exports.OFFICIAL_LARK_CLI_VERSION = "1.0.79";
24
+ exports.OFFICIAL_LARK_CLI_VERSION = "1.0.80";
24
25
  exports.OFFICIAL_LARK_CLI_INSTALL = `npm install --global @larksuite/cli@${exports.OFFICIAL_LARK_CLI_VERSION}`;
26
+ function formatOfficialLarkCliConsent(request) {
27
+ if (request.action === "upgrade")
28
+ return {
29
+ lines: [
30
+ `[setup 0/5] 检测到官方 lark-cli 需要升级:${request.reason}`,
31
+ `将执行:${request.command}`,
32
+ ],
33
+ question: "是否升级?[y/N] ",
34
+ };
35
+ return {
36
+ lines: [
37
+ `[setup 0/5] ${request.reason}。Larkin 需要安装未修改的官方 lark-cli 作为 Feishu (Lark) 命令下游。`,
38
+ `将执行:${request.command}`,
39
+ ],
40
+ question: "是否安装?[y/N] ",
41
+ };
42
+ }
25
43
  function loginShellPath(dependencies) {
26
44
  const env = dependencies.env ?? process.env;
27
45
  if ((dependencies.platform ?? process.platform) === "win32")
@@ -121,7 +139,7 @@ function validateOfficialCommand(command, env, spawn) {
121
139
  if (!compatibleVersion(command.version))
122
140
  return {
123
141
  state: "outdated",
124
- reason: `官方 lark-cli ${command.version} 低于最低兼容版本 ${exports.OFFICIAL_LARK_CLI_VERSION}: ${command.command}`,
142
+ reason: `官方 lark-cli ${command.version} 低于产品策略最低版本 ${exports.OFFICIAL_LARK_CLI_VERSION}: ${command.command}`,
125
143
  nextAction: `升级:${exports.OFFICIAL_LARK_CLI_INSTALL}`,
126
144
  };
127
145
  const version = spawn(command.command, ["--version"], {
@@ -215,7 +233,7 @@ function probeOfficialLarkCliUncached(dependencies) {
215
233
  if (!compatibleVersion(command.version))
216
234
  return {
217
235
  state: "outdated",
218
- reason: `真实 login shell 的官方 @larksuite/cli ${command.version} 低于最低兼容版本 ${exports.OFFICIAL_LARK_CLI_VERSION}`,
236
+ reason: `真实 login shell 的官方 @larksuite/cli ${command.version} 低于产品策略最低版本 ${exports.OFFICIAL_LARK_CLI_VERSION}`,
219
237
  nextAction: `升级:${exports.OFFICIAL_LARK_CLI_INSTALL}`,
220
238
  };
221
239
  return validateOfficialCommand(command, env, dependencies.spawn ?? node_child_process_1.spawnSync);
@@ -239,13 +257,14 @@ function resolveOfficialLarkCli(dependencies = {}) {
239
257
  throw new Error(`${result.reason};${result.nextAction}`);
240
258
  return result.command;
241
259
  }
242
- function installOfficialLarkCli(dependencies = {}) {
260
+ function installOfficialLarkCli(dependencies = {}, action = "install") {
243
261
  const run = dependencies.spawn ?? node_child_process_1.spawnSync;
244
262
  const result = run("npm", ["install", "--global", `@larksuite/cli@${exports.OFFICIAL_LARK_CLI_VERSION}`], {
245
263
  encoding: "utf8", env: dependencies.env ?? process.env, stdio: "inherit",
246
264
  });
247
- if (result.status !== 0 || result.error)
248
- throw new Error(`官方 lark-cli 安装失败(exit=${result.status ?? "none"})`);
265
+ if (result.status !== 0 || result.error) {
266
+ throw new Error(`官方 lark-cli ${action === "upgrade" ? "升级" : "安装"}失败(exit=${result.status ?? "none"})`);
267
+ }
249
268
  invalidateOfficialLarkCliProbeCache();
250
269
  return resolveOfficialLarkCli(dependencies);
251
270
  }
@@ -258,8 +277,13 @@ async function ensureOfficialLarkCliForSetup(input) {
258
277
  if (!input.interactive) {
259
278
  throw new Error(`${probe.reason};非交互 setup 不会安装或升级依赖。请在终端运行 larkin setup,并确认:${exports.OFFICIAL_LARK_CLI_INSTALL}`);
260
279
  }
261
- if (!await input.confirmInstall(exports.OFFICIAL_LARK_CLI_INSTALL)) {
262
- throw new Error("未获得明确同意;没有安装官方 lark-cli,也没有写入 Agent 配置");
280
+ const request = probe.state === "outdated"
281
+ ? { action: "upgrade", command: exports.OFFICIAL_LARK_CLI_INSTALL, state: probe.state,
282
+ reason: probe.reason, nextAction: probe.nextAction }
283
+ : { action: "install", command: exports.OFFICIAL_LARK_CLI_INSTALL, state: probe.state,
284
+ reason: probe.reason, nextAction: probe.nextAction };
285
+ if (!await input.confirmInstall(request)) {
286
+ throw new Error(`未获得明确同意;没有${request.action === "upgrade" ? "升级" : "安装"}官方 lark-cli,也没有写入 Agent 配置`);
263
287
  }
264
- return { command: installOfficialLarkCli(input), installed: true };
288
+ return { command: installOfficialLarkCli(input, request.action), installed: true, setupAction: request.action };
265
289
  }
@@ -9,8 +9,25 @@ function safeRealpath(target) {
9
9
  return null;
10
10
  }
11
11
  }
12
- export const OFFICIAL_LARK_CLI_VERSION = "1.0.79";
12
+ export const OFFICIAL_LARK_CLI_VERSION = "1.0.80";
13
13
  export const OFFICIAL_LARK_CLI_INSTALL = `npm install --global @larksuite/cli@${OFFICIAL_LARK_CLI_VERSION}`;
14
+ export function formatOfficialLarkCliConsent(request) {
15
+ if (request.action === "upgrade")
16
+ return {
17
+ lines: [
18
+ `[setup 0/5] 检测到官方 lark-cli 需要升级:${request.reason}`,
19
+ `将执行:${request.command}`,
20
+ ],
21
+ question: "是否升级?[y/N] ",
22
+ };
23
+ return {
24
+ lines: [
25
+ `[setup 0/5] ${request.reason}。Larkin 需要安装未修改的官方 lark-cli 作为 Feishu (Lark) 命令下游。`,
26
+ `将执行:${request.command}`,
27
+ ],
28
+ question: "是否安装?[y/N] ",
29
+ };
30
+ }
14
31
  function loginShellPath(dependencies) {
15
32
  const env = dependencies.env ?? process.env;
16
33
  if ((dependencies.platform ?? process.platform) === "win32")
@@ -110,7 +127,7 @@ function validateOfficialCommand(command, env, spawn) {
110
127
  if (!compatibleVersion(command.version))
111
128
  return {
112
129
  state: "outdated",
113
- reason: `官方 lark-cli ${command.version} 低于最低兼容版本 ${OFFICIAL_LARK_CLI_VERSION}: ${command.command}`,
130
+ reason: `官方 lark-cli ${command.version} 低于产品策略最低版本 ${OFFICIAL_LARK_CLI_VERSION}: ${command.command}`,
114
131
  nextAction: `升级:${OFFICIAL_LARK_CLI_INSTALL}`,
115
132
  };
116
133
  const version = spawn(command.command, ["--version"], {
@@ -204,7 +221,7 @@ function probeOfficialLarkCliUncached(dependencies) {
204
221
  if (!compatibleVersion(command.version))
205
222
  return {
206
223
  state: "outdated",
207
- reason: `真实 login shell 的官方 @larksuite/cli ${command.version} 低于最低兼容版本 ${OFFICIAL_LARK_CLI_VERSION}`,
224
+ reason: `真实 login shell 的官方 @larksuite/cli ${command.version} 低于产品策略最低版本 ${OFFICIAL_LARK_CLI_VERSION}`,
208
225
  nextAction: `升级:${OFFICIAL_LARK_CLI_INSTALL}`,
209
226
  };
210
227
  return validateOfficialCommand(command, env, dependencies.spawn ?? spawnSync);
@@ -228,13 +245,14 @@ export function resolveOfficialLarkCli(dependencies = {}) {
228
245
  throw new Error(`${result.reason};${result.nextAction}`);
229
246
  return result.command;
230
247
  }
231
- export function installOfficialLarkCli(dependencies = {}) {
248
+ export function installOfficialLarkCli(dependencies = {}, action = "install") {
232
249
  const run = dependencies.spawn ?? spawnSync;
233
250
  const result = run("npm", ["install", "--global", `@larksuite/cli@${OFFICIAL_LARK_CLI_VERSION}`], {
234
251
  encoding: "utf8", env: dependencies.env ?? process.env, stdio: "inherit",
235
252
  });
236
- if (result.status !== 0 || result.error)
237
- throw new Error(`官方 lark-cli 安装失败(exit=${result.status ?? "none"})`);
253
+ if (result.status !== 0 || result.error) {
254
+ throw new Error(`官方 lark-cli ${action === "upgrade" ? "升级" : "安装"}失败(exit=${result.status ?? "none"})`);
255
+ }
238
256
  invalidateOfficialLarkCliProbeCache();
239
257
  return resolveOfficialLarkCli(dependencies);
240
258
  }
@@ -247,8 +265,13 @@ export async function ensureOfficialLarkCliForSetup(input) {
247
265
  if (!input.interactive) {
248
266
  throw new Error(`${probe.reason};非交互 setup 不会安装或升级依赖。请在终端运行 larkin setup,并确认:${OFFICIAL_LARK_CLI_INSTALL}`);
249
267
  }
250
- if (!await input.confirmInstall(OFFICIAL_LARK_CLI_INSTALL)) {
251
- throw new Error("未获得明确同意;没有安装官方 lark-cli,也没有写入 Agent 配置");
268
+ const request = probe.state === "outdated"
269
+ ? { action: "upgrade", command: OFFICIAL_LARK_CLI_INSTALL, state: probe.state,
270
+ reason: probe.reason, nextAction: probe.nextAction }
271
+ : { action: "install", command: OFFICIAL_LARK_CLI_INSTALL, state: probe.state,
272
+ reason: probe.reason, nextAction: probe.nextAction };
273
+ if (!await input.confirmInstall(request)) {
274
+ throw new Error(`未获得明确同意;没有${request.action === "upgrade" ? "升级" : "安装"}官方 lark-cli,也没有写入 Agent 配置`);
252
275
  }
253
- return { command: installOfficialLarkCli(input), installed: true };
276
+ return { command: installOfficialLarkCli(input, request.action), installed: true, setupAction: request.action };
254
277
  }
@@ -14,7 +14,7 @@ import { requestAgentUpsert } from "./local-control.mjs";
14
14
  import { openOwnedDashboardWhenReady } from "./setup-dashboard.mjs";
15
15
  import { probeNativeRuntimeReadiness } from "../runtime/runtime-readiness.mjs";
16
16
  import { internalCommandSpec, processCommandToken } from "./internal-command.mjs";
17
- import { ensureOfficialLarkCliForSetup, } from "./official-lark-cli.mjs";
17
+ import { ensureOfficialLarkCliForSetup, formatOfficialLarkCliConsent, } from "./official-lark-cli.mjs";
18
18
  const CFG_DIR = process.env.LARKIN_CONFIG_DIR || path.join(os.homedir(), ".larkin");
19
19
  const argv = process.argv.slice(2);
20
20
  const has = (name) => argv.includes(name);
@@ -127,16 +127,19 @@ export async function main() {
127
127
  const official = await ensureOfficialLarkCliForSetup({
128
128
  env: process.env,
129
129
  interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
130
- async confirmInstall(command) {
131
- say("[setup 0/5] Larkin 需要未修改的官方 lark-cli 作为 Feishu (Lark) 命令下游。");
132
- say(`将执行:${command}`);
130
+ async confirmInstall(request) {
131
+ const copy = formatOfficialLarkCliConsent(request);
132
+ for (const line of copy.lines)
133
+ say(line);
133
134
  const input = readline.createInterface({ input: process.stdin, output: process.stdout });
134
- const answer = (await input.question("是否安装?[y/N] ")).trim().toLowerCase();
135
+ const answer = (await input.question(copy.question)).trim().toLowerCase();
135
136
  input.close();
136
137
  return answer === "y" || answer === "yes";
137
138
  },
138
139
  }).catch((error) => die(error instanceof Error ? error.message : String(error)));
139
- say(`[setup 0/5] ✓ 官方 lark-cli ${official.command.version}: ${official.command.command}${official.installed ? "(刚刚安装)" : ""}`);
140
+ const completedAction = official.setupAction === "upgrade" ? "(刚刚升级)"
141
+ : official.setupAction === "install" ? "(刚刚安装)" : "";
142
+ say(`[setup 0/5] ✓ 官方 lark-cli ${official.command.version}: ${official.command.command}${completedAction}`);
140
143
  say("\nAgent 与飞书(Lark)机器人按 App ID 一一对应:");
141
144
  say(" • 网页选择同一个机器人 → 热更新该 Agent,不重启其他 Agent");
142
145
  say(" • 网页创建新机器人 → 热挂载新 Agent,状态彼此独立\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "larkin",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "private": false,
5
5
  "larkinPackageRole": "npm-published",
6
6
  "files": [
@@ -74,6 +74,8 @@
74
74
  "test:eval:authoritative-freshness-gate": "bun test --max-concurrency 1 test/unit/evals/authoritative-freshness-gate.test.mjs",
75
75
  "test:eval:agent-experience-v6": "bun test --max-concurrency 1 test/unit/evals/agent-experience-v6.test.mjs",
76
76
  "test:eval:mention-construction": "bun run build && LARKIN_RUN_MENTION_CONSTRUCTION_EVAL=1 bun test --max-concurrency 1 test/live/mention-construction-agent-eval.test.mjs",
77
+ "test:eval:clickable-link-delivery": "bun run build && bun test --max-concurrency 1 test/unit/evals/clickable-link-delivery.test.mjs",
78
+ "test:eval:clickable-link-delivery:native": "bun run build && LARKIN_RUN_CLICKABLE_LINK_DELIVERY_EVAL=1 bun test --max-concurrency 1 test/live/clickable-link-delivery-agent-eval.test.mjs",
77
79
  "test:eval:public-contribution-governance": "bun test --max-concurrency 1 test/unit/evals/public-contribution-governance.test.mjs",
78
80
  "test:live:public-contribution-governance": "bun test --max-concurrency 1 test/live/public-contribution-governance-live.test.mjs",
79
81
  "test:eval:authoritative-freshness-gate:native": "bun run build && LARKIN_RUN_AUTHORITATIVE_FRESHNESS_EVAL=1 bun test --max-concurrency 1 test/live/authoritative-freshness-gate-agent-eval.test.mjs",