larkit 0.0.0-stage → 0.1.0
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/AGENT_SKILL.md +12 -0
- package/README.md +765 -2
- package/bin/larkit-mcp.js +8 -0
- package/bin/larkit.js +532 -0
- package/larkit.config.example.json +12 -0
- package/package.json +65 -4
- package/scripts/.tmp-npm-webauth.mjs +49 -0
- package/scripts/build-index.mjs +66 -0
- package/scripts/check-syntax.mjs +28 -0
- package/scripts/login-worker.mjs +50 -0
- package/scripts/publish-pack.mjs +277 -0
- package/scripts/verify-bootstrap.mjs +238 -0
- package/scripts/verify-console.mjs +60 -0
- package/scripts/verify-index.mjs +59 -0
- package/scripts/verify-mcp.mjs +215 -0
- package/skills/larkit/SKILL.md +120 -0
- package/skills/larkit/references/console.md +86 -0
- package/skills/larkit/references/playbooks.md +177 -0
- package/skills/larkit/references/tools.md +115 -0
- package/skills/larkit/references/truth-model.md +117 -0
- package/src/auth/login.js +171 -0
- package/src/auth/service.js +213 -0
- package/src/auth/store.js +213 -0
- package/src/bootstrap/channel.js +201 -0
- package/src/bootstrap/cli-install.js +184 -0
- package/src/bootstrap/download.js +182 -0
- package/src/bootstrap/init.js +672 -0
- package/src/bootstrap/pack-cli.js +97 -0
- package/src/bootstrap/pack.js +541 -0
- package/src/bootstrap/report.js +62 -0
- package/src/bootstrap/skills.js +223 -0
- package/src/bootstrap/state.js +66 -0
- package/src/bootstrap/tar.js +417 -0
- package/src/console/browser.js +338 -0
- package/src/console/client.js +827 -0
- package/src/console/dispatch.js +101 -0
- package/src/docs/client.js +284 -0
- package/src/indexer/anchors.js +366 -0
- package/src/indexer/db.js +449 -0
- package/src/indexer/extract-assets.js +285 -0
- package/src/indexer/extract-deb.js +56 -0
- package/src/indexer/format-pool.js +64 -0
- package/src/indexer/format-worker.js +132 -0
- package/src/indexer/format.js +239 -0
- package/src/indexer/protocol.js +123 -0
- package/src/mcp/server.js +433 -0
- package/src/probe/client.js +225 -0
- package/src/probe/openapi.js +194 -0
- package/src/protocol/catalog.js +146 -0
- package/src/protocol/data/README.md +15 -0
- package/src/protocol/data/bundle.json +1 -0
- package/src/protocol/data/commands.json +12896 -0
- package/src/protocol/data/constants.json +8 -0
- package/src/protocol/data/reference/PROTOCOL.md +66 -0
- package/src/protocol/data/reference/endpoints.json +46 -0
- package/src/protocol/data/schema.json.gz +0 -0
- package/src/protocol/descriptor.js +358 -0
- package/src/protocol/schema.js +235 -0
- package/src/protocol/transport.js +387 -0
- package/src/shared/browser.js +70 -0
- package/src/shared/config.js +90 -0
- package/src/shared/paths.js +151 -0
- package/src/shared/quiet.js +12 -0
- package/src/shared/util.js +124 -0
- package/src/truth/fts.js +24 -0
- package/src/truth/search.js +554 -0
- package/src/truth/verified.js +107 -0
- package/verified_truths/README.md +28 -0
- package/verified_truths/card-json-2.0-official.md +55 -0
- package/verified_truths/client-ws-gateway-boundaries.md +33 -0
- package/verified_truths/feishu-developer-console-apis.md +104 -0
- package/workers/truth-api/src/index.js +180 -0
- package/workers/truth-api/wrangler.toml +31 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import {
|
|
6
|
+
BIN_DIR,
|
|
7
|
+
CLI_INSTALL_DIR,
|
|
8
|
+
IS_REPO_CHECKOUT,
|
|
9
|
+
PKG_ROOT,
|
|
10
|
+
displayPath,
|
|
11
|
+
} from "../shared/paths.js";
|
|
12
|
+
import { ensureDir, readJson, writeJson } from "../shared/util.js";
|
|
13
|
+
|
|
14
|
+
function runNpm(args) {
|
|
15
|
+
const command = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
16
|
+
const result = spawnSync(command, args, {
|
|
17
|
+
encoding: "utf8",
|
|
18
|
+
shell: process.platform === "win32",
|
|
19
|
+
env: {
|
|
20
|
+
...process.env,
|
|
21
|
+
npm_config_update_notifier: "false",
|
|
22
|
+
npm_config_fund: "false",
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
if (result.error) throw result.error;
|
|
26
|
+
if (result.status !== 0) {
|
|
27
|
+
const detail = (result.stderr || result.stdout || "").trim().split("\n").slice(-8).join("\n");
|
|
28
|
+
throw new Error(`npm ${args[0]} failed (exit ${result.status})${detail ? `:\n${detail}` : ""}`);
|
|
29
|
+
}
|
|
30
|
+
return result.stdout;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function shimBody(target) {
|
|
34
|
+
return `#!/usr/bin/env sh\nexec node "${target}" "$@"\n`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function cmdShimBody(target) {
|
|
38
|
+
return `@echo off\r\nnode "${target}" %*\r\n`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function writeShims(entry, binDir) {
|
|
42
|
+
const binDirOfPackage = path.dirname(entry);
|
|
43
|
+
const targets = {
|
|
44
|
+
larkit: path.join(binDirOfPackage, "larkit.js"),
|
|
45
|
+
"larkit-mcp": path.join(binDirOfPackage, "larkit-mcp.js"),
|
|
46
|
+
};
|
|
47
|
+
ensureDir(binDir);
|
|
48
|
+
const written = [];
|
|
49
|
+
for (const [name, target] of Object.entries(targets)) {
|
|
50
|
+
if (!fs.existsSync(target)) continue;
|
|
51
|
+
const file = path.join(binDir, name);
|
|
52
|
+
fs.writeFileSync(file, shimBody(target));
|
|
53
|
+
fs.chmodSync(file, 0o755);
|
|
54
|
+
written.push(file);
|
|
55
|
+
if (process.platform === "win32") {
|
|
56
|
+
const cmd = `${file}.cmd`;
|
|
57
|
+
fs.writeFileSync(cmd, cmdShimBody(target));
|
|
58
|
+
written.push(cmd);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return written;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function binDirOnPath(binDir = BIN_DIR) {
|
|
65
|
+
const parts = String(process.env.PATH || "")
|
|
66
|
+
.split(path.delimiter)
|
|
67
|
+
.filter(Boolean)
|
|
68
|
+
.map((part) => path.resolve(part));
|
|
69
|
+
return parts.includes(path.resolve(binDir));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function detectCliInstall({ binDir = BIN_DIR } = {}) {
|
|
73
|
+
const shim = path.join(binDir, "larkit");
|
|
74
|
+
const installedPackage = path.join(CLI_INSTALL_DIR, "node_modules", "larkit", "package.json");
|
|
75
|
+
const repoVersion = IS_REPO_CHECKOUT
|
|
76
|
+
? readJson(path.join(PKG_ROOT, "package.json"), {}).version || null
|
|
77
|
+
: null;
|
|
78
|
+
return {
|
|
79
|
+
shim,
|
|
80
|
+
shim_installed: fs.existsSync(shim),
|
|
81
|
+
bin_dir: binDir,
|
|
82
|
+
on_path: binDirOnPath(binDir),
|
|
83
|
+
mode: fs.existsSync(installedPackage)
|
|
84
|
+
? "local-prefix"
|
|
85
|
+
: IS_REPO_CHECKOUT
|
|
86
|
+
? "repo"
|
|
87
|
+
: null,
|
|
88
|
+
version: readJson(installedPackage, {}).version || repoVersion,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Install a durable `larkit` command.
|
|
94
|
+
*
|
|
95
|
+
* Default strategy: install the package into ~/.larkit/cli and drop a tiny
|
|
96
|
+
* shell shim into ~/.local/bin, so no sudo/global prefix is required and the
|
|
97
|
+
* command survives npx cache cleanup. In a git checkout the shim points at
|
|
98
|
+
* the checkout itself. `--global` delegates to `npm install -g`.
|
|
99
|
+
*/
|
|
100
|
+
export function installCli({
|
|
101
|
+
version,
|
|
102
|
+
force = false,
|
|
103
|
+
global = false,
|
|
104
|
+
binDir = BIN_DIR,
|
|
105
|
+
source = null,
|
|
106
|
+
} = {}) {
|
|
107
|
+
if (global) {
|
|
108
|
+
const target = source || `larkit@${version}`;
|
|
109
|
+
runNpm(["install", "-g", target, "--no-audit", "--no-fund", "--loglevel=error"]);
|
|
110
|
+
return {
|
|
111
|
+
mode: "global",
|
|
112
|
+
entry: null,
|
|
113
|
+
shim: null,
|
|
114
|
+
bin_dir: binDir,
|
|
115
|
+
on_path: binDirOnPath(binDir),
|
|
116
|
+
status: "installed",
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const devCheckout =
|
|
121
|
+
IS_REPO_CHECKOUT && fs.existsSync(path.join(PKG_ROOT, "bin", "larkit.js"));
|
|
122
|
+
|
|
123
|
+
if (devCheckout && !source) {
|
|
124
|
+
const entry = path.join(PKG_ROOT, "bin", "larkit.js");
|
|
125
|
+
const written = writeShims(entry, binDir);
|
|
126
|
+
return {
|
|
127
|
+
mode: "repo",
|
|
128
|
+
entry,
|
|
129
|
+
shim: path.join(binDir, "larkit"),
|
|
130
|
+
shims: written,
|
|
131
|
+
bin_dir: binDir,
|
|
132
|
+
on_path: binDirOnPath(binDir),
|
|
133
|
+
status: "linked",
|
|
134
|
+
version,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const packageRoot = path.join(CLI_INSTALL_DIR, "node_modules", "larkit");
|
|
139
|
+
const entry = path.join(packageRoot, "bin", "larkit.js");
|
|
140
|
+
const installed = readJson(path.join(packageRoot, "package.json"), {});
|
|
141
|
+
let status = "present";
|
|
142
|
+
if (force || !fs.existsSync(entry) || (version && installed.version !== version)) {
|
|
143
|
+
ensureDir(CLI_INSTALL_DIR);
|
|
144
|
+
const manifest = path.join(CLI_INSTALL_DIR, "package.json");
|
|
145
|
+
if (!fs.existsSync(manifest)) {
|
|
146
|
+
writeJson(manifest, { name: "larkit-cli-install", private: true });
|
|
147
|
+
}
|
|
148
|
+
const target = source || `larkit@${version}`;
|
|
149
|
+
runNpm([
|
|
150
|
+
"install",
|
|
151
|
+
"--prefix",
|
|
152
|
+
CLI_INSTALL_DIR,
|
|
153
|
+
"--no-save",
|
|
154
|
+
"--no-package-lock",
|
|
155
|
+
"--no-audit",
|
|
156
|
+
"--no-fund",
|
|
157
|
+
"--loglevel=error",
|
|
158
|
+
target,
|
|
159
|
+
]);
|
|
160
|
+
if (!fs.existsSync(entry)) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
`npm installed ${target} but ${displayPath(entry)} is missing; check that the package bin entry is correct`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
status = "installed";
|
|
166
|
+
}
|
|
167
|
+
const written = writeShims(entry, binDir);
|
|
168
|
+
return {
|
|
169
|
+
mode: "local-prefix",
|
|
170
|
+
entry,
|
|
171
|
+
shim: path.join(binDir, "larkit"),
|
|
172
|
+
shims: written,
|
|
173
|
+
bin_dir: binDir,
|
|
174
|
+
on_path: binDirOnPath(binDir),
|
|
175
|
+
status,
|
|
176
|
+
version: readJson(path.join(packageRoot, "package.json"), {}).version || version,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function mcpServerCommand() {
|
|
181
|
+
const shim = path.join(BIN_DIR, "larkit-mcp");
|
|
182
|
+
if (fs.existsSync(shim)) return shim;
|
|
183
|
+
return "larkit-mcp";
|
|
184
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { Readable, Transform } from "node:stream";
|
|
4
|
+
import { pipeline } from "node:stream/promises";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import {
|
|
8
|
+
ensureDir,
|
|
9
|
+
fileSizeMiB,
|
|
10
|
+
log,
|
|
11
|
+
readJson,
|
|
12
|
+
} from "../shared/util.js";
|
|
13
|
+
import { PKG_ROOT } from "../shared/paths.js";
|
|
14
|
+
|
|
15
|
+
const VERSION = readJson(path.join(PKG_ROOT, "package.json"), {}).version || "0.0.0";
|
|
16
|
+
|
|
17
|
+
export function isHttpUrl(value) {
|
|
18
|
+
return /^https?:\/\//i.test(String(value));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isFileUrl(value) {
|
|
22
|
+
return /^file:\/\//i.test(String(value));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function sha256File(file, onProgress) {
|
|
26
|
+
const hash = createHash("sha256");
|
|
27
|
+
let bytes = 0;
|
|
28
|
+
for await (const chunk of fs.createReadStream(file)) {
|
|
29
|
+
hash.update(chunk);
|
|
30
|
+
bytes += chunk.length;
|
|
31
|
+
onProgress?.(bytes);
|
|
32
|
+
}
|
|
33
|
+
return { sha256: hash.digest("hex"), bytes };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function sha256FileSync(file) {
|
|
37
|
+
return createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function finalize(partFile, destFile, { expectedSha256, expectedBytes } = {}) {
|
|
41
|
+
const stat = fs.statSync(partFile);
|
|
42
|
+
if (expectedBytes && stat.size !== expectedBytes) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`download size mismatch: got ${stat.size} bytes, expected ${expectedBytes}`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
if (expectedSha256) {
|
|
48
|
+
const { sha256 } = await sha256File(partFile);
|
|
49
|
+
if (sha256 !== expectedSha256) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`download checksum mismatch: got ${sha256}, expected ${expectedSha256}`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
fs.rmSync(destFile, { force: true });
|
|
56
|
+
fs.renameSync(partFile, destFile);
|
|
57
|
+
return { file: destFile, bytes: stat.size };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Copy a local file with the same atomic/verified semantics as a download. */
|
|
61
|
+
export async function copyLocalFile(source, dest, options = {}) {
|
|
62
|
+
const from = isFileUrl(source) ? fileURLToPath(source) : path.resolve(source);
|
|
63
|
+
if (!fs.existsSync(from)) throw new Error(`file not found: ${source}`);
|
|
64
|
+
ensureDir(path.dirname(dest));
|
|
65
|
+
const part = `${dest}.part`;
|
|
66
|
+
await pipeline(fs.createReadStream(from), fs.createWriteStream(part));
|
|
67
|
+
return finalize(part, dest, options);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Download (or copy) with resume + sha256 verification. A `.part` file is
|
|
72
|
+
* reused when the server supports Range requests, so interrupted installs can
|
|
73
|
+
* continue instead of restarting a multi-hundred-MB download.
|
|
74
|
+
*/
|
|
75
|
+
export async function downloadFile(source, dest, options = {}) {
|
|
76
|
+
const {
|
|
77
|
+
expectedSha256,
|
|
78
|
+
expectedBytes,
|
|
79
|
+
onProgress,
|
|
80
|
+
quiet = false,
|
|
81
|
+
label = path.basename(dest),
|
|
82
|
+
} = options;
|
|
83
|
+
|
|
84
|
+
if (!isHttpUrl(source)) {
|
|
85
|
+
if (!quiet) log(`[download] copy ${path.basename(source)} -> ${dest}`);
|
|
86
|
+
return copyLocalFile(source, dest, { expectedSha256, expectedBytes });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
ensureDir(path.dirname(dest));
|
|
90
|
+
const part = `${dest}.part`;
|
|
91
|
+
let start = fs.existsSync(part) ? fs.statSync(part).size : 0;
|
|
92
|
+
let response;
|
|
93
|
+
let lastError;
|
|
94
|
+
|
|
95
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
96
|
+
const headers = {
|
|
97
|
+
"user-agent": `larkit/${VERSION}`,
|
|
98
|
+
"accept-encoding": "identity",
|
|
99
|
+
};
|
|
100
|
+
if (start > 0) headers.range = `bytes=${start}-`;
|
|
101
|
+
try {
|
|
102
|
+
response = await fetch(source, { headers, redirect: "follow" });
|
|
103
|
+
} catch (error) {
|
|
104
|
+
lastError = error;
|
|
105
|
+
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (response.status === 416 && start > 0) {
|
|
109
|
+
// The .part file is already complete (or larger). Hash it.
|
|
110
|
+
const result = await finalize(part, dest, { expectedSha256, expectedBytes });
|
|
111
|
+
onProgress?.(result.bytes, result.bytes);
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
if (!response.ok && response.status !== 206) {
|
|
115
|
+
throw new Error(`HTTP ${response.status} for ${source}`);
|
|
116
|
+
}
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
if (!response) {
|
|
120
|
+
throw new Error(`download failed: ${lastError?.message || source}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const resuming = start > 0 && response.status === 206;
|
|
124
|
+
if (start > 0 && !resuming) {
|
|
125
|
+
start = 0;
|
|
126
|
+
fs.rmSync(part, { force: true });
|
|
127
|
+
}
|
|
128
|
+
const length = Number(response.headers.get("content-length")) || 0;
|
|
129
|
+
const total = expectedBytes || (resuming ? start + length : length);
|
|
130
|
+
if (!quiet) {
|
|
131
|
+
log(
|
|
132
|
+
`[download] ${label} ${resuming ? "resume " : ""}${total ? fileSizeMiB(total) : ""}`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let received = 0;
|
|
137
|
+
let lastTick = 0;
|
|
138
|
+
const progress = new Transform({
|
|
139
|
+
transform(chunk, _encoding, callback) {
|
|
140
|
+
received += chunk.length;
|
|
141
|
+
const now = Date.now();
|
|
142
|
+
if (now - lastTick > 500) {
|
|
143
|
+
lastTick = now;
|
|
144
|
+
onProgress?.(start + received, total);
|
|
145
|
+
}
|
|
146
|
+
callback(null, chunk);
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
await pipeline(
|
|
152
|
+
Readable.fromWeb(response.body),
|
|
153
|
+
progress,
|
|
154
|
+
fs.createWriteStream(part, { flags: resuming ? "a" : "w" }),
|
|
155
|
+
);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`download interrupted at ${fileSizeMiB(start + received)}: ${error.message}; rerun to resume`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
const result = await finalize(part, dest, { expectedSha256, expectedBytes });
|
|
162
|
+
onProgress?.(result.bytes, result.bytes);
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function fetchJson(url) {
|
|
167
|
+
if (!isHttpUrl(url)) {
|
|
168
|
+
const file = isFileUrl(url) ? fileURLToPath(url) : path.resolve(url);
|
|
169
|
+
return readJson(file, null);
|
|
170
|
+
}
|
|
171
|
+
const response = await fetch(url, {
|
|
172
|
+
headers: { "user-agent": `larkit/${VERSION}` },
|
|
173
|
+
redirect: "follow",
|
|
174
|
+
});
|
|
175
|
+
if (!response.ok) throw new Error(`HTTP ${response.status} for ${url}`);
|
|
176
|
+
return response.json();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function formatPercent(received, total) {
|
|
180
|
+
if (!total) return `${fileSizeMiB(received)}`;
|
|
181
|
+
return `${Math.min(100, Math.round((received / total) * 100))}% (${fileSizeMiB(received)}/${fileSizeMiB(total)})`;
|
|
182
|
+
}
|