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.
- package/README.md +3 -3
- package/dist/agent/context-prompt.cjs +2 -2
- package/dist/agent/context-prompt.mjs +2 -2
- package/dist/app/agent-cli.mjs +3 -3
- package/dist/app/agent-lark-cli-workspace.cjs +3 -2
- package/dist/app/agent-lark-cli-workspace.mjs +3 -2
- package/dist/app/cli.mjs +1 -1
- package/dist/app/lark-cli.mjs +53 -3
- package/dist/app/local-control.mjs +11 -6
- package/dist/app/official-lark-cli.cjs +66 -7
- package/dist/app/official-lark-cli.mjs +66 -7
- package/dist/app/runtime-agent-config.mjs +3 -2
- package/dist/app/setup.mjs +4 -4
- package/dist/platform/callback-capability.cjs +2 -1
- package/dist/platform/callback-capability.mjs +2 -1
- package/dist/platform/config.cjs +3 -8
- package/dist/platform/config.mjs +3 -8
- package/dist/platform/release-artifacts.mjs +10 -3
- package/dist/platform/secure-metadata.cjs +48 -0
- package/dist/platform/secure-metadata.mjs +38 -0
- package/dist/platform/telemetry-spool.cjs +3 -14
- package/dist/platform/telemetry-spool.mjs +3 -14
- package/dist/platform/telemetry-tracing.cjs +2 -7
- package/dist/platform/telemetry-tracing.mjs +2 -7
- package/dist/setup/bot-register.mjs +5 -5
- package/dist/setup/grant-scopes.mjs +3 -3
- package/dist/setup/setup-bind.mjs +3 -3
- package/package.json +5 -3
- package/scripts/npm/install-binary.mjs +125 -0
- package/scripts/npm/larkin-bin-shim.mjs +48 -0
|
@@ -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() || (
|
|
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
|
|
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
|
}
|
package/dist/platform/config.cjs
CHANGED
|
@@ -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 ((
|
|
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
|
-
|
|
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 {
|
package/dist/platform/config.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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]
|
|
492
|
+
say(`[setup] 飞书(Lark)要求降低轮询频率,${interval || "稍后"}秒后继续`);
|
|
493
493
|
if (status === "polling" && ++pollingCount % 12 === 0)
|
|
494
|
-
say(`[setup]
|
|
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(
|
|
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]
|
|
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
|
|
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
|
|
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("
|
|
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.
|
|
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
|
|
19
|
+
"description": "把 Codex、Claude Code 和 Pi Agent Runtime 接到飞书(Lark)的本地 Runtime Host",
|
|
19
20
|
"bin": {
|
|
20
|
-
"larkin": "
|
|
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);
|