larkin 0.2.75 → 0.2.77

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.
@@ -50,6 +50,7 @@ exports.verifyCallbackProbe = verifyCallbackProbe;
50
50
  const node_crypto_1 = require("node:crypto");
51
51
  const fs = __importStar(require("node:fs"));
52
52
  const path = __importStar(require("node:path"));
53
+ const secure_metadata_js_1 = require("./secure-metadata.cjs");
53
54
  const APP_ID = /^cli_[A-Za-z0-9]+$/;
54
55
  const NONCE = /^[a-f0-9]{32}$/;
55
56
  function credentialFile(configDir, appId) {
@@ -59,7 +60,7 @@ function credentialFile(configDir, appId) {
59
60
  }
60
61
  function readCredential(file) {
61
62
  const stat = fs.lstatSync(file);
62
- if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o777) !== 0o600
63
+ if (!stat.isFile() || stat.isSymbolicLink() || !(0, secure_metadata_js_1.exactMode)(stat, 0o600)
63
64
  || (typeof process.getuid === "function" && stat.uid !== process.getuid())) {
64
65
  throw new Error("callback capability credential must be an owned 0600 regular file");
65
66
  }
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
+ import { exactMode } from "./secure-metadata.mjs";
4
5
  const APP_ID = /^cli_[A-Za-z0-9]+$/;
5
6
  const NONCE = /^[a-f0-9]{32}$/;
6
7
  function credentialFile(configDir, appId) {
@@ -10,7 +11,7 @@ function credentialFile(configDir, appId) {
10
11
  }
11
12
  function readCredential(file) {
12
13
  const stat = fs.lstatSync(file);
13
- if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o777) !== 0o600
14
+ if (!stat.isFile() || stat.isSymbolicLink() || !exactMode(stat, 0o600)
14
15
  || (typeof process.getuid === "function" && stat.uid !== process.getuid())) {
15
16
  throw new Error("callback capability credential must be an owned 0600 regular file");
16
17
  }
@@ -60,6 +60,7 @@ const fs = __importStar(require("node:fs"));
60
60
  const os = __importStar(require("node:os"));
61
61
  const path = __importStar(require("node:path"));
62
62
  const root_layout_js_1 = require("./root-layout.cjs");
63
+ const secure_metadata_js_1 = require("./secure-metadata.cjs");
63
64
  const runtime_model_catalog_js_1 = require("../runtime/runtime-model-catalog.cjs");
64
65
  const process_inspect_cjs_1 = __importDefault(require("./process-inspect.cjs"));
65
66
  const TOP_FIELDS_V3 = new Set(["version", "serverId", "activeAgent", "agents"]);
@@ -264,7 +265,7 @@ function assertPrivateConfigMetadata(metadata, label = "配置文件") {
264
265
  throw new Error(`${label} 必须是普通文件`);
265
266
  if (typeof process.getuid === "function" && metadata.uid !== process.getuid())
266
267
  throw new Error(`${label} owner 不是当前用户`);
267
- if ((metadata.mode & 0o777) !== 0o600)
268
+ if (!(0, secure_metadata_js_1.exactMode)(metadata, 0o600))
268
269
  throw new Error(`${label} 权限必须是 0600`);
269
270
  }
270
271
  function readPrivateFile(file, root, limit, label) {
@@ -663,13 +664,7 @@ function atomicWriteConfig(file, value) {
663
664
  try {
664
665
  fs.renameSync(temporary, file);
665
666
  fs.chmodSync(file, 0o600);
666
- const dirFd = fs.openSync(path.dirname(file), "r");
667
- try {
668
- fs.fsyncSync(dirFd);
669
- }
670
- finally {
671
- fs.closeSync(dirFd);
672
- }
667
+ (0, secure_metadata_js_1.fsyncDirectoryOf)(file);
673
668
  }
674
669
  catch (error) {
675
670
  try {
@@ -3,6 +3,7 @@ import * as fs from "node:fs";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
5
  import { TargetRootLayout, resolveConfigDir as resolveRootConfigDir } from "./root-layout.mjs";
6
+ import { exactMode, fsyncDirectoryOf } from "./secure-metadata.mjs";
6
7
  import { CURRENT_RUNTIME_MODELS } from "../runtime/runtime-model-catalog.mjs";
7
8
  import processInspect from "./process-inspect.cjs";
8
9
  const TOP_FIELDS_V3 = new Set(["version", "serverId", "activeAgent", "agents"]);
@@ -207,7 +208,7 @@ export function assertPrivateConfigMetadata(metadata, label = "配置文件") {
207
208
  throw new Error(`${label} 必须是普通文件`);
208
209
  if (typeof process.getuid === "function" && metadata.uid !== process.getuid())
209
210
  throw new Error(`${label} owner 不是当前用户`);
210
- if ((metadata.mode & 0o777) !== 0o600)
211
+ if (!exactMode(metadata, 0o600))
211
212
  throw new Error(`${label} 权限必须是 0600`);
212
213
  }
213
214
  function readPrivateFile(file, root, limit, label) {
@@ -606,13 +607,7 @@ function atomicWriteConfig(file, value) {
606
607
  try {
607
608
  fs.renameSync(temporary, file);
608
609
  fs.chmodSync(file, 0o600);
609
- const dirFd = fs.openSync(path.dirname(file), "r");
610
- try {
611
- fs.fsyncSync(dirFd);
612
- }
613
- finally {
614
- fs.closeSync(dirFd);
615
- }
610
+ fsyncDirectoryOf(file);
616
611
  }
617
612
  catch (error) {
618
613
  try {
@@ -6,6 +6,7 @@ export const RELEASE_TARGETS = Object.freeze([
6
6
  { platform: "darwin", arch: "x64", bunTarget: "bun-darwin-x64-baseline" },
7
7
  { platform: "linux", arch: "arm64", bunTarget: "bun-linux-arm64" },
8
8
  { platform: "linux", arch: "x64", bunTarget: "bun-linux-x64-baseline" },
9
+ { platform: "windows", arch: "x64", bunTarget: "bun-windows-x64-baseline" },
9
10
  ]);
10
11
  export function artifactFilename(version, platform, arch) {
11
12
  if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version))
@@ -13,14 +14,20 @@ export function artifactFilename(version, platform, arch) {
13
14
  if (!RELEASE_TARGETS.some((target) => target.platform === platform && target.arch === arch)) {
14
15
  throw new Error(`unsupported release target: ${platform}-${arch}`);
15
16
  }
16
- return `larkin-v${version}-${platform}-${arch}`;
17
+ const suffix = platform === "windows" ? ".exe" : "";
18
+ return `larkin-v${version}-${platform}-${arch}${suffix}`;
19
+ }
20
+ /** Node's `os.platform()` reports Windows as "win32"; canonical release manifests use "windows". */
21
+ export function normalizeReleasePlatform(platform) {
22
+ return platform === "win32" ? "windows" : platform;
17
23
  }
18
24
  export function sha256File(file) {
19
25
  return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
20
26
  }
21
27
  export function selectReleaseArtifact(manifest, platform, arch) {
22
- const record = manifest.artifacts.find((candidate) => candidate.platform === platform && candidate.arch === arch);
23
- if (!record || !RELEASE_TARGETS.some((target) => target.platform === platform && target.arch === arch)) {
28
+ const canonicalPlatform = normalizeReleasePlatform(platform);
29
+ const record = manifest.artifacts.find((candidate) => candidate.platform === canonicalPlatform && candidate.arch === arch);
30
+ if (!record || !RELEASE_TARGETS.some((target) => target.platform === canonicalPlatform && target.arch === arch)) {
24
31
  throw new Error(`unsupported platform: ${platform}-${arch}`);
25
32
  }
26
33
  if (record.file !== artifactFilename(manifest.version, record.platform, record.arch) || !/^[a-f0-9]{64}$/.test(record.sha256)) {
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.isWindows = void 0;
7
+ exports.exactMode = exactMode;
8
+ exports.notGroupOrWorldAccessible = notGroupOrWorldAccessible;
9
+ exports.fsyncDirectory = fsyncDirectory;
10
+ exports.fsyncDirectoryOf = fsyncDirectoryOf;
11
+ const node_fs_1 = __importDefault(require("node:fs"));
12
+ const node_path_1 = __importDefault(require("node:path"));
13
+ /**
14
+ * Cross-platform secure-metadata helpers. POSIX platforms enforce ownership and
15
+ * mode bits (fail-closed). Windows has no POSIX mode bits, so mode-bit assertions
16
+ * are skipped there; ownership on Windows is enforced by the filesystem/ACL and the
17
+ * parent-directory containment checks elsewhere. Windows runtime behavior is marked
18
+ * unverified (Owner decision B: no Windows validation in this delivery).
19
+ */
20
+ exports.isWindows = process.platform === "win32";
21
+ /** POSIX: `mode & 0o777` must equal `expected`. Windows: always passes. */
22
+ function exactMode(stat, expected) {
23
+ if (exports.isWindows)
24
+ return true;
25
+ return (stat.mode & 0o777) === expected;
26
+ }
27
+ /** POSIX: group/other must have no access bits. Windows: always passes. */
28
+ function notGroupOrWorldAccessible(stat) {
29
+ if (exports.isWindows)
30
+ return true;
31
+ return (stat.mode & 0o077) === 0;
32
+ }
33
+ /** Fsync a directory after rename for durability. Windows cannot open a directory for fsync, so this is a no-op. */
34
+ function fsyncDirectory(directory) {
35
+ if (exports.isWindows)
36
+ return;
37
+ const fd = node_fs_1.default.openSync(directory, node_fs_1.default.constants.O_RDONLY);
38
+ try {
39
+ node_fs_1.default.fsyncSync(fd);
40
+ }
41
+ finally {
42
+ node_fs_1.default.closeSync(fd);
43
+ }
44
+ }
45
+ /** Fsync the parent directory of `file` after rename for durability. Windows: no-op. */
46
+ function fsyncDirectoryOf(file) {
47
+ fsyncDirectory(node_path_1.default.dirname(file));
48
+ }
@@ -0,0 +1,38 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ /**
4
+ * Cross-platform secure-metadata helpers. POSIX platforms enforce ownership and
5
+ * mode bits (fail-closed). Windows has no POSIX mode bits, so mode-bit assertions
6
+ * are skipped there; ownership on Windows is enforced by the filesystem/ACL and the
7
+ * parent-directory containment checks elsewhere. Windows runtime behavior is marked
8
+ * unverified (Owner decision B: no Windows validation in this delivery).
9
+ */
10
+ export const isWindows = process.platform === "win32";
11
+ /** POSIX: `mode & 0o777` must equal `expected`. Windows: always passes. */
12
+ export function exactMode(stat, expected) {
13
+ if (isWindows)
14
+ return true;
15
+ return (stat.mode & 0o777) === expected;
16
+ }
17
+ /** POSIX: group/other must have no access bits. Windows: always passes. */
18
+ export function notGroupOrWorldAccessible(stat) {
19
+ if (isWindows)
20
+ return true;
21
+ return (stat.mode & 0o077) === 0;
22
+ }
23
+ /** Fsync a directory after rename for durability. Windows cannot open a directory for fsync, so this is a no-op. */
24
+ export function fsyncDirectory(directory) {
25
+ if (isWindows)
26
+ return;
27
+ const fd = fs.openSync(directory, fs.constants.O_RDONLY);
28
+ try {
29
+ fs.fsyncSync(fd);
30
+ }
31
+ finally {
32
+ fs.closeSync(fd);
33
+ }
34
+ }
35
+ /** Fsync the parent directory of `file` after rename for durability. Windows: no-op. */
36
+ export function fsyncDirectoryOf(file) {
37
+ fsyncDirectory(path.dirname(file));
38
+ }
@@ -10,6 +10,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
10
10
  const node_path_1 = __importDefault(require("node:path"));
11
11
  const node_zlib_1 = __importDefault(require("node:zlib"));
12
12
  const process_state_js_1 = require("./process-state.cjs");
13
+ const secure_metadata_js_1 = require("./secure-metadata.cjs");
13
14
  const READY = /^(?:span-[0-9a-f-]+|import-[0-9a-f]{64})\.json$/;
14
15
  const REMNANT = /^\.(?:corrupt|write|ack|delete|stale-lock|purge)-[0-9A-Za-z-]+(?:\.(?:json|tmp|dir))?$/;
15
16
  const HEX_TRACE = /^[0-9a-f]{32}$/;
@@ -333,13 +334,7 @@ class TelemetrySpool {
333
334
  descriptor = undefined;
334
335
  node_fs_1.default.renameSync(temporary, file);
335
336
  node_fs_1.default.chmodSync(file, 0o600);
336
- const directory = node_fs_1.default.openSync(this.config.spoolDir, node_fs_1.default.constants.O_RDONLY);
337
- try {
338
- node_fs_1.default.fsyncSync(directory);
339
- }
340
- finally {
341
- node_fs_1.default.closeSync(directory);
342
- }
337
+ (0, secure_metadata_js_1.fsyncDirectory)(this.config.spoolDir);
343
338
  }
344
339
  finally {
345
340
  if (descriptor !== undefined)
@@ -479,13 +474,7 @@ class TelemetrySpool {
479
474
  }
480
475
  for (const entry of moved)
481
476
  node_fs_1.default.unlinkSync(entry.quarantine);
482
- const directory = node_fs_1.default.openSync(this.config.spoolDir, node_fs_1.default.constants.O_RDONLY);
483
- try {
484
- node_fs_1.default.fsyncSync(directory);
485
- }
486
- finally {
487
- node_fs_1.default.closeSync(directory);
488
- }
477
+ (0, secure_metadata_js_1.fsyncDirectory)(this.config.spoolDir);
489
478
  }
490
479
  catch {
491
480
  for (const entry of moved.reverse())
@@ -3,6 +3,7 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import zlib from "node:zlib";
5
5
  import { inspectProcess } from "./process-state.mjs";
6
+ import { fsyncDirectory } from "./secure-metadata.mjs";
6
7
  const READY = /^(?:span-[0-9a-f-]+|import-[0-9a-f]{64})\.json$/;
7
8
  const REMNANT = /^\.(?:corrupt|write|ack|delete|stale-lock|purge)-[0-9A-Za-z-]+(?:\.(?:json|tmp|dir))?$/;
8
9
  const HEX_TRACE = /^[0-9a-f]{32}$/;
@@ -326,13 +327,7 @@ export class TelemetrySpool {
326
327
  descriptor = undefined;
327
328
  fs.renameSync(temporary, file);
328
329
  fs.chmodSync(file, 0o600);
329
- const directory = fs.openSync(this.config.spoolDir, fs.constants.O_RDONLY);
330
- try {
331
- fs.fsyncSync(directory);
332
- }
333
- finally {
334
- fs.closeSync(directory);
335
- }
330
+ fsyncDirectory(this.config.spoolDir);
336
331
  }
337
332
  finally {
338
333
  if (descriptor !== undefined)
@@ -472,13 +467,7 @@ export class TelemetrySpool {
472
467
  }
473
468
  for (const entry of moved)
474
469
  fs.unlinkSync(entry.quarantine);
475
- const directory = fs.openSync(this.config.spoolDir, fs.constants.O_RDONLY);
476
- try {
477
- fs.fsyncSync(directory);
478
- }
479
- finally {
480
- fs.closeSync(directory);
481
- }
470
+ fsyncDirectory(this.config.spoolDir);
482
471
  }
483
472
  catch {
484
473
  for (const entry of moved.reverse())
@@ -15,6 +15,7 @@ const sdk_trace_node_1 = require("@opentelemetry/sdk-trace-node");
15
15
  const telemetry_spool_js_1 = require("./telemetry-spool.cjs");
16
16
  const telemetry_uploader_js_1 = require("./telemetry-uploader.cjs");
17
17
  const process_state_js_1 = require("./process-state.cjs");
18
+ const secure_metadata_js_1 = require("./secure-metadata.cjs");
18
19
  const nanos = ([seconds, nanoseconds]) => (BigInt(seconds) * 1000000000n + BigInt(nanoseconds)).toString();
19
20
  // OTel JS uses zero-based SpanKind values while OTLP's enum starts at 1.
20
21
  const kind = (value) => value + 1;
@@ -82,13 +83,7 @@ function atomicStateWrite(file, value) {
82
83
  node_fs_1.default.writeFileSync(temporary, JSON.stringify(value), { mode: 0o600, flag: "wx" });
83
84
  node_fs_1.default.renameSync(temporary, file);
84
85
  node_fs_1.default.chmodSync(file, 0o600);
85
- const directory = node_fs_1.default.openSync(node_path_1.default.dirname(file), node_fs_1.default.constants.O_RDONLY);
86
- try {
87
- node_fs_1.default.fsyncSync(directory);
88
- }
89
- finally {
90
- node_fs_1.default.closeSync(directory);
91
- }
86
+ (0, secure_metadata_js_1.fsyncDirectoryOf)(file);
92
87
  }
93
88
  finally {
94
89
  try {
@@ -8,6 +8,7 @@ import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
8
8
  import { TelemetrySpool } from "./telemetry-spool.mjs";
9
9
  import { startTelemetryUploader } from "./telemetry-uploader.mjs";
10
10
  import { inspectProcess } from "./process-state.mjs";
11
+ import { fsyncDirectoryOf } from "./secure-metadata.mjs";
11
12
  const nanos = ([seconds, nanoseconds]) => (BigInt(seconds) * 1000000000n + BigInt(nanoseconds)).toString();
12
13
  // OTel JS uses zero-based SpanKind values while OTLP's enum starts at 1.
13
14
  const kind = (value) => value + 1;
@@ -75,13 +76,7 @@ function atomicStateWrite(file, value) {
75
76
  fs.writeFileSync(temporary, JSON.stringify(value), { mode: 0o600, flag: "wx" });
76
77
  fs.renameSync(temporary, file);
77
78
  fs.chmodSync(file, 0o600);
78
- const directory = fs.openSync(path.dirname(file), fs.constants.O_RDONLY);
79
- try {
80
- fs.fsyncSync(directory);
81
- }
82
- finally {
83
- fs.closeSync(directory);
84
- }
79
+ fsyncDirectoryOf(file);
85
80
  }
86
81
  finally {
87
82
  try {
@@ -435,7 +435,7 @@ export async function main() {
435
435
  die("--result-file 必须是配置根目录内的 .setup-result-<pid>.json");
436
436
  }
437
437
  if (argv.some((arg) => arg === "--app-id" || arg.startsWith("--app-id="))) {
438
- die("不支持 --app-id;机器人必须在飞书网页中选择");
438
+ die("不支持 --app-id;机器人必须在飞书(Lark)网页中选择");
439
439
  }
440
440
  if (!autoSelect)
441
441
  die("setup 注册阶段必须由交互式选择流程启动");
@@ -482,16 +482,16 @@ export async function main() {
482
482
  },
483
483
  onQRCodeReady: ({ url, expireIn }) => {
484
484
  qrcode.generate(url, { small: true }, (code) => say(code));
485
- say(`\n打开以下链接(或扫码),${Math.round(expireIn / 60)} 分钟内有效:\n\n ${url}\n\n[setup 2/5] 等待飞书网页完成授权并回传凭证…`);
485
+ say(`\n打开以下链接(或扫码),${Math.round(expireIn / 60)} 分钟内有效:\n\n ${url}\n\n[setup 2/5] 等待飞书(Lark)网页完成授权并回传凭证…`);
486
486
  say(openBrowser(url) ? "[setup] 已在默认浏览器打开授权页" : "[setup] 未能自动打开浏览器,请手动打开上面的链接");
487
487
  },
488
488
  onStatusChange: ({ status, interval }) => {
489
489
  if (status === "domain_switched")
490
490
  say("[setup] 已切换到 Lark 域名");
491
491
  if (status === "slow_down")
492
- say(`[setup] 飞书要求降低轮询频率,${interval || "稍后"}秒后继续`);
492
+ say(`[setup] 飞书(Lark)要求降低轮询频率,${interval || "稍后"}秒后继续`);
493
493
  if (status === "polling" && ++pollingCount % 12 === 0)
494
- say(`[setup] 仍在等待飞书回传凭证(约 ${pollingCount / 12} 分钟)…`);
494
+ say(`[setup] 仍在等待飞书(Lark)回传凭证(约 ${pollingCount / 12} 分钟)…`);
495
495
  },
496
496
  }).catch(() => die("网页授权失败;未执行凭证同步、文件写入或 Agent 绑定,请重试 setup"));
497
497
  const { client_id: rawId, client_secret: rawSecret, user_info: userInfo } = result || {};
@@ -559,7 +559,7 @@ export async function main() {
559
559
  throw new Error(`官方 Pi ${providerId} 登录失败或已取消;credential/config 未修改`);
560
560
  }
561
561
  if (process.env.LARKIN_TEST_SKIP_BUILTIN_PI_PROVIDER_TURN !== "1") {
562
- say("正在验证内置 Pi provider(受控单轮,不发送飞书消息)…");
562
+ say("正在验证内置 Pi provider(受控单轮,不发送飞书(Lark)消息)…");
563
563
  try {
564
564
  await verifyOfficialPiProviderTurn(piRuntime, choice.model);
565
565
  }
@@ -39,7 +39,7 @@ const APP_ID = selected.agentId;
39
39
  if (explicitAppId && explicitAppId !== selected.feishuAppId)
40
40
  throw new Error("--app-id 必须等于所选 Agent 的 App ID");
41
41
  if (!/^cli_[A-Za-z0-9]+$/.test(APP_ID))
42
- throw new Error(`无效飞书 App ID:${APP_ID}`);
42
+ throw new Error(`无效飞书(Lark) App ID:${APP_ID}`);
43
43
  const SEND_TO = flag("--send-to", "");
44
44
  const TENANT = flag("--tenant", "feishu");
45
45
  const WAIT_MIN = Number(flag("--wait-min", "9"));
@@ -76,7 +76,7 @@ function sendUrlToChat(url, minutes) {
76
76
  const result = spawnSync(managed.command.command, [...managed.command.argsPrefix, "im", "+messages-send", "--chat-id", SEND_TO, "--text", text, "--json"], {
77
77
  encoding: "utf8", env: managed.env,
78
78
  });
79
- log(result.status === 0 ? "[grant] 更新链接已发到飞书群" : `[grant] 发链接失败: ${(result.stderr || "").trim().split("\n")[0]}`);
79
+ log(result.status === 0 ? "[grant] 更新链接已发到飞书(Lark)群" : `[grant] 发链接失败: ${(result.stderr || "").trim().split("\n")[0]}`);
80
80
  }
81
81
  export async function main() {
82
82
  log(`=== 给 bot ${APP_ID} 增补读取权限(registerApp 更新流) ===`);
@@ -94,7 +94,7 @@ export async function main() {
94
94
  },
95
95
  onQRCodeReady: (info) => {
96
96
  const minutes = Math.max(1, Math.round(info.expireIn / 60));
97
- log("\n请用飞书扫码或打开链接,确认给应用增补权限:\n");
97
+ log("\n请用飞书(Lark)扫码或打开链接,确认给应用增补权限:\n");
98
98
  qrcode.generate(info.url, { small: true });
99
99
  log(`\n链接: ${info.url}\n有效期约 ${minutes} 分钟(⚠️ 需本进程保持轮询,否则 user_code 立即失效)\n`);
100
100
  if (URL_FILE) {
@@ -87,7 +87,7 @@ async function pickProfile() {
87
87
  const profiles = listProfiles();
88
88
  if (profiles.length === 0)
89
89
  die("lark-cli 里还没有 bot profile。请运行 larkin setup 创建或连接机器人");
90
- say("\n可复用的飞书机器人(lark-cli profiles):");
90
+ say("\n可复用的飞书(Lark)机器人(lark-cli profiles):");
91
91
  profiles.forEach((profile, index) => say(` [${index + 1}] ${profile.name} appId=${profile.appId}${profile.active ? " (当前活跃,默认)" : ""}`));
92
92
  say(" (创建新机器人请退出后运行 larkin setup)");
93
93
  const activeIndex = Math.max(0, profiles.findIndex((profile) => profile.active));
@@ -298,7 +298,7 @@ export async function main() {
298
298
  }
299
299
  if (!readinessAlreadyCompleted) {
300
300
  piRuntime ??= await createOfficialPiModelRuntime(CFG_DIR, profile.appId);
301
- say("正在验证内置 Pi provider(受控单轮,不发送飞书消息)…");
301
+ say("正在验证内置 Pi provider(受控单轮,不发送飞书(Lark)消息)…");
302
302
  try {
303
303
  if (!(process.env.LARKIN_TEST_SKIP_BUILTIN_PI_PROVIDER_TURN === "1" && process.env.LARKIN_TEST_BOT_REGISTER_MODULE)) {
304
304
  await verifyOfficialPiProviderTurn(piRuntime, selection.model, authAbort.signal);
@@ -329,7 +329,7 @@ export async function main() {
329
329
  say("\n启动这个 Agent:");
330
330
  say(" larkin start # 单服务启动全部已配置 Agent");
331
331
  say(` larkin start --agent ${profile.appId} # 仅调试这个 Agent`);
332
- say("创建独立飞书机器人 + Agent:");
332
+ say("创建独立飞书(Lark)机器人 + Agent:");
333
333
  say(" larkin setup");
334
334
  }
335
335
  if (path.resolve(process.argv[1] || "") === path.resolve(fileURLToPath(import.meta.url))) {
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "larkin",
3
- "version": "0.2.75",
3
+ "version": "0.2.77",
4
4
  "private": false,
5
5
  "larkinPackageRole": "npm-published",
6
6
  "files": [
7
7
  "dist/",
8
8
  "assets/",
9
+ "scripts/npm/",
9
10
  "README.md",
10
11
  "LICENSE",
11
12
  "SECURITY.md",
@@ -15,9 +16,9 @@
15
16
  "packageManager": "bun@1.3.14",
16
17
  "license": "Apache-2.0",
17
18
  "type": "module",
18
- "description": "把 Codex、Claude Code 和 Pi Agent Runtime 接到飞书的本地 Runtime Host",
19
+ "description": "把 Codex、Claude Code 和 Pi Agent Runtime 接到飞书(Lark)的本地 Runtime Host",
19
20
  "bin": {
20
- "larkin": "dist/app/cli.mjs"
21
+ "larkin": "scripts/npm/larkin-bin-shim.mjs"
21
22
  },
22
23
  "repository": {
23
24
  "type": "git",
@@ -28,6 +29,7 @@
28
29
  "typecheck": "bun run scripts/typecheck.mjs",
29
30
  "build": "bun run scripts/build.mjs",
30
31
  "prepare": "bun run build",
32
+ "postinstall": "node scripts/npm/install-binary.mjs",
31
33
  "version:bump": "bun run scripts/bump-version.mjs",
32
34
  "release:check-version": "bun run scripts/check-release-version.mjs",
33
35
  "release:smoke": "bun run scripts/release/smoke.ts",
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env node
2
+ // npm postinstall: download the matching platform standalone binary from the
3
+ // GitHub Release for this package version, verify its SHA-256, and install it
4
+ // atomically under <pkg>/larkin-bin/<platform>-<arch>/. The npm tarball itself
5
+ // never ships the binary; this runs only for registry/git installs (never in the
6
+ // source repo, which ships src/ and scripts/build.mjs).
7
+ //
8
+ // Failures are non-fatal on purpose: `bin` (scripts/npm/larkin-bin-shim.mjs)
9
+ // falls back to the Bun-compiled JS entry when the binary is unavailable.
10
+
11
+ import fs from "node:fs";
12
+ import os from "node:os";
13
+ import path from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+
16
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
17
+ const PKG_ROOT = path.resolve(HERE, "..", "..");
18
+
19
+ function warn(message) {
20
+ process.stderr.write(`[larkin install] ${message}\n`);
21
+ }
22
+
23
+ function inSourceRepo() {
24
+ return fs.existsSync(path.join(PKG_ROOT, "src")) && fs.existsSync(path.join(PKG_ROOT, "scripts", "build.mjs"));
25
+ }
26
+
27
+ async function fetchWithTimeout(url, timeoutMs) {
28
+ const controller = new AbortController();
29
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
30
+ try {
31
+ const response = await fetch(url, { redirect: "follow", signal: controller.signal });
32
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
33
+ return Buffer.from(await response.arrayBuffer());
34
+ } finally {
35
+ clearTimeout(timer);
36
+ }
37
+ }
38
+
39
+ function expectedSha256(checksumsText, filename) {
40
+ for (const line of String(checksumsText).split(/\r?\n/)) {
41
+ const [sha, name] = line.trim().split(/\s+/);
42
+ if (name === filename && /^[a-f0-9]{64}$/.test(sha)) return sha;
43
+ }
44
+ return null;
45
+ }
46
+
47
+ async function main() {
48
+ if (process.env.LARKIN_NPM_BINARY_DISABLE === "1") return;
49
+ if (inSourceRepo()) return;
50
+
51
+ // 延迟导入编译产物:源码仓库/CI 中 `dist/` 可能尚未构建,静态 import 会在
52
+ // 守卫之前失败(bun install 会运行根包 postinstall)。
53
+ const { RELEASE_TARGETS, artifactFilename, normalizeReleasePlatform, sha256File } = await import(
54
+ "../../dist/platform/release-artifacts.mjs"
55
+ );
56
+
57
+ const platform = normalizeReleasePlatform(os.platform());
58
+ const arch = os.arch();
59
+ const target = RELEASE_TARGETS.find((candidate) => candidate.platform === platform && candidate.arch === arch);
60
+ if (!target) return; // no standalone binary for this platform → JS fallback
61
+
62
+ let version;
63
+ try {
64
+ version = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, "package.json"), "utf8")).version;
65
+ } catch (error) {
66
+ warn(`无法读取包版本:${error instanceof Error ? error.message : String(error)}`);
67
+ return;
68
+ }
69
+
70
+ let filename;
71
+ try {
72
+ filename = artifactFilename(version, target.platform, target.arch);
73
+ } catch (error) {
74
+ warn(`跳过下载:${error instanceof Error ? error.message : String(error)}`);
75
+ return;
76
+ }
77
+
78
+ const baseUrl = String(process.env.LARKIN_RELEASE_BASE_URL || `https://github.com/eddiearc/larkin/releases/download/v${version}`).replace(/\/+$/, "");
79
+ const destinationDir = path.join(PKG_ROOT, "larkin-bin", `${target.platform}-${target.arch}`);
80
+ const destination = path.join(destinationDir, target.platform === "windows" ? "larkin.exe" : "larkin");
81
+
82
+ let expected;
83
+ try {
84
+ const checksums = await fetchWithTimeout(`${baseUrl}/SHA256SUMS`, 30_000);
85
+ expected = expectedSha256(checksums.toString("utf8"), filename);
86
+ } catch (error) {
87
+ if (fs.existsSync(destination)) {
88
+ warn(`SHA256SUMS 下载失败,保留已安装二进制:${error instanceof Error ? error.message : String(error)}`);
89
+ return;
90
+ }
91
+ warn(`SHA256SUMS 下载失败,将回退到 Bun JS 入口:${error instanceof Error ? error.message : String(error)}`);
92
+ return;
93
+ }
94
+ if (!expected) {
95
+ warn(`SHA256SUMS 中缺少 ${filename} 的校验和,将回退到 Bun JS 入口`);
96
+ return;
97
+ }
98
+
99
+ if (fs.existsSync(destination) && sha256File(destination) === expected) return; // already current
100
+
101
+ let bytes;
102
+ try {
103
+ bytes = await fetchWithTimeout(`${baseUrl}/${filename}`, 5 * 60_000);
104
+ } catch (error) {
105
+ warn(`二进制下载失败,将回退到 Bun JS 入口:${error instanceof Error ? error.message : String(error)}`);
106
+ return;
107
+ }
108
+
109
+ fs.mkdirSync(destinationDir, { recursive: true });
110
+ const temporary = path.join(destinationDir, `.larkin.${process.pid}.${Date.now()}.tmp`);
111
+ try {
112
+ fs.writeFileSync(temporary, bytes);
113
+ if (sha256File(temporary) !== expected) throw new Error("checksum mismatch");
114
+ fs.chmodSync(temporary, 0o755);
115
+ fs.renameSync(temporary, destination);
116
+ process.stderr.write(`[larkin install] installed ${filename}\n`);
117
+ } catch (error) {
118
+ try { fs.unlinkSync(temporary); } catch { /* best effort */ }
119
+ warn(`安装二进制失败,将回退到 Bun JS 入口:${error instanceof Error ? error.message : String(error)}`);
120
+ }
121
+ }
122
+
123
+ main().catch((error) => {
124
+ warn(`安装二进制异常,将回退到 Bun JS 入口:${error instanceof Error ? error.message : String(error)}`);
125
+ });
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+ // npm `bin` entry. Prefers the postinstall-downloaded standalone binary; falls
3
+ // back to the Bun-compiled JS entry when the binary is absent (unsupported
4
+ // platform or a failed/interrupted install) and Bun is on PATH.
5
+
6
+ import fs from "node:fs";
7
+ import os from "node:os";
8
+ import path from "node:path";
9
+ import { spawnSync } from "node:child_process";
10
+ import { fileURLToPath } from "node:url";
11
+ import { RELEASE_TARGETS, normalizeReleasePlatform } from "../../dist/platform/release-artifacts.mjs";
12
+
13
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
14
+ const PKG_ROOT = path.resolve(HERE, "..", "..");
15
+
16
+ const platform = normalizeReleasePlatform(os.platform());
17
+ const target = RELEASE_TARGETS.find((candidate) => candidate.platform === platform && candidate.arch === os.arch());
18
+ const binary = target
19
+ ? path.join(PKG_ROOT, "larkin-bin", `${target.platform}-${target.arch}`, target.platform === "windows" ? "larkin.exe" : "larkin")
20
+ : null;
21
+ const argv = process.argv.slice(2);
22
+
23
+ function run(command, args) {
24
+ const result = spawnSync(command, args, { stdio: "inherit" });
25
+ if (result.error) return null;
26
+ return result.status ?? 0;
27
+ }
28
+
29
+ if (binary && fs.existsSync(binary)) {
30
+ const status = run(binary, argv);
31
+ if (status !== null) process.exit(status);
32
+ process.stderr.write(`[larkin] 平台二进制启动失败,尝试 Bun fallback…\n`);
33
+ }
34
+
35
+ const bunEntry = path.join(PKG_ROOT, "dist", "app", "cli.mjs");
36
+ if (fs.existsSync(bunEntry)) {
37
+ const bun = process.env.LARKIN_BUN || "bun";
38
+ const status = run(bun, [bunEntry, ...argv]);
39
+ if (status !== null) process.exit(status);
40
+ }
41
+
42
+ process.stderr.write(
43
+ "[larkin] 未找到可用的平台二进制,也未找到 Bun。\n" +
44
+ " - 联网重新安装会自动下载对应平台二进制:npm install -g larkin\n" +
45
+ " - 或安装 Bun 1.3.14 后重试(会走 JS 入口 fallback)\n" +
46
+ " - 或从 GitHub Release 下载 standalone 二进制:https://github.com/eddiearc/larkin/releases\n",
47
+ );
48
+ process.exit(1);