larkit 0.0.0-stage → 0.1.1

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.
Files changed (72) hide show
  1. package/AGENT_SKILL.md +12 -0
  2. package/README.md +765 -2
  3. package/bin/larkit-mcp.js +8 -0
  4. package/bin/larkit.js +532 -0
  5. package/larkit.config.example.json +12 -0
  6. package/package.json +73 -4
  7. package/scripts/build-index.mjs +66 -0
  8. package/scripts/check-syntax.mjs +28 -0
  9. package/scripts/login-worker.mjs +50 -0
  10. package/scripts/publish-pack.mjs +277 -0
  11. package/scripts/verify-bootstrap.mjs +238 -0
  12. package/scripts/verify-console.mjs +60 -0
  13. package/scripts/verify-index.mjs +59 -0
  14. package/scripts/verify-mcp.mjs +215 -0
  15. package/skills/larkit/SKILL.md +120 -0
  16. package/skills/larkit/references/console.md +86 -0
  17. package/skills/larkit/references/playbooks.md +177 -0
  18. package/skills/larkit/references/tools.md +115 -0
  19. package/skills/larkit/references/truth-model.md +117 -0
  20. package/src/auth/login.js +171 -0
  21. package/src/auth/service.js +213 -0
  22. package/src/auth/store.js +213 -0
  23. package/src/bootstrap/channel.js +201 -0
  24. package/src/bootstrap/cli-install.js +184 -0
  25. package/src/bootstrap/download.js +182 -0
  26. package/src/bootstrap/init.js +672 -0
  27. package/src/bootstrap/pack-cli.js +97 -0
  28. package/src/bootstrap/pack.js +541 -0
  29. package/src/bootstrap/report.js +62 -0
  30. package/src/bootstrap/skills.js +223 -0
  31. package/src/bootstrap/state.js +66 -0
  32. package/src/bootstrap/tar.js +417 -0
  33. package/src/console/browser.js +338 -0
  34. package/src/console/client.js +827 -0
  35. package/src/console/dispatch.js +101 -0
  36. package/src/docs/client.js +284 -0
  37. package/src/indexer/anchors.js +366 -0
  38. package/src/indexer/db.js +449 -0
  39. package/src/indexer/extract-assets.js +285 -0
  40. package/src/indexer/extract-deb.js +56 -0
  41. package/src/indexer/format-pool.js +64 -0
  42. package/src/indexer/format-worker.js +132 -0
  43. package/src/indexer/format.js +239 -0
  44. package/src/indexer/protocol.js +123 -0
  45. package/src/mcp/server.js +433 -0
  46. package/src/probe/client.js +225 -0
  47. package/src/probe/openapi.js +194 -0
  48. package/src/protocol/catalog.js +146 -0
  49. package/src/protocol/data/README.md +15 -0
  50. package/src/protocol/data/bundle.json +1 -0
  51. package/src/protocol/data/commands.json +12896 -0
  52. package/src/protocol/data/constants.json +8 -0
  53. package/src/protocol/data/reference/PROTOCOL.md +66 -0
  54. package/src/protocol/data/reference/endpoints.json +46 -0
  55. package/src/protocol/data/schema.json.gz +0 -0
  56. package/src/protocol/descriptor.js +358 -0
  57. package/src/protocol/schema.js +235 -0
  58. package/src/protocol/transport.js +387 -0
  59. package/src/shared/browser.js +70 -0
  60. package/src/shared/config.js +90 -0
  61. package/src/shared/paths.js +151 -0
  62. package/src/shared/quiet.js +12 -0
  63. package/src/shared/util.js +124 -0
  64. package/src/truth/fts.js +24 -0
  65. package/src/truth/search.js +554 -0
  66. package/src/truth/verified.js +107 -0
  67. package/verified_truths/README.md +28 -0
  68. package/verified_truths/card-json-2.0-official.md +55 -0
  69. package/verified_truths/client-ws-gateway-boundaries.md +33 -0
  70. package/verified_truths/feishu-developer-console-apis.md +104 -0
  71. package/workers/truth-api/src/index.js +180 -0
  72. package/workers/truth-api/wrangler.toml +31 -0
@@ -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
+ }