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,28 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { spawnSync } from "node:child_process";
5
+
6
+ const roots = ["bin", "src", "scripts"];
7
+ const files = [];
8
+ function walk(dir) {
9
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
10
+ const full = path.join(dir, entry.name);
11
+ if (entry.isDirectory()) walk(full);
12
+ else if (/\.(?:js|mjs)$/.test(entry.name)) files.push(full);
13
+ }
14
+ }
15
+ for (const root of roots) if (fs.existsSync(root)) walk(root);
16
+
17
+ let failed = 0;
18
+ for (const file of files.sort()) {
19
+ const result = spawnSync(process.execPath, ["--check", file], {
20
+ encoding: "utf8",
21
+ });
22
+ if (result.status !== 0) {
23
+ failed++;
24
+ console.error(`FAIL ${file}\n${result.stderr}`);
25
+ }
26
+ }
27
+ console.log(`${files.length - failed}/${files.length} files passed syntax check`);
28
+ process.exit(failed ? 1 : 0);
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ import "../src/shared/quiet.js";
3
+ import fs from "node:fs";
4
+ import { loginWithQr } from "../src/auth/login.js";
5
+
6
+ function arg(name, fallback) {
7
+ const index = process.argv.indexOf(`--${name}`);
8
+ return index >= 0 ? process.argv[index + 1] : fallback;
9
+ }
10
+
11
+ const name = arg("name", "default");
12
+ const timeoutMs = Number(arg("timeout", "120000")) || 120000;
13
+ const statusFile = arg("status", undefined);
14
+ const headless = !process.argv.includes("--headful");
15
+
16
+ try {
17
+ const result = await loginWithQr({ name, timeoutMs, headless, statusFile });
18
+ if (statusFile) {
19
+ fs.writeFileSync(
20
+ statusFile,
21
+ `${JSON.stringify(
22
+ {
23
+ phase: result.ok ? "success" : "failed",
24
+ updated_at: new Date().toISOString(),
25
+ ...result,
26
+ },
27
+ null,
28
+ 2,
29
+ )}\n`,
30
+ );
31
+ }
32
+ process.exit(result.ok ? 0 : 2);
33
+ } catch (error) {
34
+ if (statusFile) {
35
+ fs.writeFileSync(
36
+ statusFile,
37
+ `${JSON.stringify(
38
+ {
39
+ phase: "error",
40
+ updated_at: new Date().toISOString(),
41
+ error: error instanceof Error ? error.message : String(error),
42
+ },
43
+ null,
44
+ 2,
45
+ )}\n`,
46
+ );
47
+ }
48
+ console.error(error);
49
+ process.exit(1);
50
+ }
@@ -0,0 +1,277 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Publish a built truth pack to Cloudflare R2.
4
+ *
5
+ * # maintainer machine — truth source release (no npm involved)
6
+ * node bin/larkit.js pack build --out=dist/truth-pack \
7
+ * --tag=2026.11.15 --base-url=https://larkit.tapeless.eu.org
8
+ * node scripts/publish-pack.mjs --pack-dir=dist/truth-pack \
9
+ * --bucket=larkit-truth --base-url=https://larkit.tapeless.eu.org
10
+ *
11
+ * Objects are uploaded with immutable cache headers (archive names carry the
12
+ * pack tag), then `manifest.json` last — clients never observe a manifest that
13
+ * points at objects which are not there yet. A pinned copy (`<tag>.json`) is
14
+ * published alongside for reproducible installs, plus `index.json` that lists
15
+ * every published tag so a tag can never be silently reused for new bytes.
16
+ *
17
+ * Uploads go through `wrangler r2 object put`, so either run `wrangler login`
18
+ * once or export CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID.
19
+ */
20
+ import fs from "node:fs";
21
+ import path from "node:path";
22
+ import { spawnSync } from "node:child_process";
23
+ import { createHash } from "node:crypto";
24
+
25
+ const IMMUTABLE = "public, max-age=31536000, immutable";
26
+ const MANIFEST_CACHE = "public, max-age=60, must-revalidate";
27
+
28
+ function parseArgs(argv) {
29
+ const flags = {};
30
+ for (const arg of argv) {
31
+ if (!arg.startsWith("--")) continue;
32
+ const [key, ...rest] = arg.slice(2).split("=");
33
+ const value = rest.join("=");
34
+ flags[key] = value === "" ? true : value;
35
+ }
36
+ return flags;
37
+ }
38
+
39
+ function fail(message) {
40
+ console.error(`[publish] ${message}`);
41
+ process.exit(1);
42
+ }
43
+
44
+ function mib(bytes) {
45
+ return `${(bytes / 1024 / 1024).toFixed(1)} MiB`;
46
+ }
47
+
48
+ const flags = parseArgs(process.argv.slice(2));
49
+ const packDir = path.resolve(flags["pack-dir"] || "dist/truth-pack");
50
+ const bucket = flags.bucket || process.env.LARKIT_R2_BUCKET || "larkit-truth";
51
+ const prefix = String(flags.prefix || "").replace(/^\/+|\/+$/g, "");
52
+ const dryRun = Boolean(flags["dry-run"]);
53
+ const manifestOnly = Boolean(flags["manifest-only"]);
54
+ const force = Boolean(flags.force);
55
+ const wrangler = flags.wrangler || "npx --yes wrangler@latest";
56
+
57
+ const manifestFile = path.join(packDir, "manifest.json");
58
+ if (!fs.existsSync(manifestFile)) {
59
+ fail(
60
+ `missing ${manifestFile}; run "larkit pack build --out=${path.relative(process.cwd(), packDir)}" first`,
61
+ );
62
+ }
63
+
64
+ const manifest = JSON.parse(fs.readFileSync(manifestFile, "utf8"));
65
+ if (manifest.kind !== "larkit-truth-pack") fail(`${manifestFile} is not a truth pack manifest`);
66
+ const tiers = Object.entries(manifest.tiers || {});
67
+ if (!tiers.length) fail(`${manifestFile} has no tiers`);
68
+
69
+ const tag = String(manifest.packVersion || manifest.larkitVersion || "0.0.0");
70
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(tag)) fail(`unsafe pack tag "${tag}"`);
71
+
72
+ function baseUrlFromManifest() {
73
+ for (const [, tier] of tiers) {
74
+ if (typeof tier.url === "string" && /^https?:\/\//.test(tier.url)) {
75
+ return tier.url.replace(/\/[^/]*$/, "");
76
+ }
77
+ }
78
+ return "";
79
+ }
80
+
81
+ const baseUrl = String(
82
+ flags["base-url"] || process.env.LARKIT_PACK_BASE_URL || baseUrlFromManifest(),
83
+ ).replace(/\/+$/, "");
84
+ if (!baseUrl) {
85
+ fail("no public base URL: pass --base-url=https://host/prefix (or build with --base-url)");
86
+ }
87
+
88
+ function keyFor(name) {
89
+ return prefix ? `${prefix}/${name}` : name;
90
+ }
91
+
92
+ function urlFor(name) {
93
+ return `${baseUrl}/${keyFor(name)}`;
94
+ }
95
+
96
+ function sha256(file) {
97
+ return createHash("sha256").update(fs.readFileSync(file)).digest("hex");
98
+ }
99
+
100
+ function upload(localFile, name, { contentType, cacheControl }) {
101
+ const key = keyFor(name);
102
+ const args = [
103
+ "r2",
104
+ "object",
105
+ "put",
106
+ `${bucket}/${key}`,
107
+ `--file=${localFile}`,
108
+ `--content-type=${contentType}`,
109
+ `--cache-control=${cacheControl}`,
110
+ "--remote",
111
+ "-y",
112
+ ];
113
+ const label = `${bucket}/${key}`;
114
+ if (dryRun) {
115
+ console.log(`[publish] dry-run ${label} <- ${path.relative(process.cwd(), localFile)}`);
116
+ return;
117
+ }
118
+ console.log(`[publish] upload ${label} (${mib(fs.statSync(localFile).size)})`);
119
+ const [command, ...prefixArgs] = wrangler.split(/\s+/);
120
+ const result = spawnSync(command, [...prefixArgs, ...args], { stdio: "inherit" });
121
+ if (result.status !== 0) fail(`wrangler failed for ${label}`);
122
+ }
123
+
124
+ async function fetchIndex() {
125
+ try {
126
+ const response = await fetch(urlFor("index.json"), {
127
+ headers: { "cache-control": "no-cache" },
128
+ });
129
+ if (response.status === 404) return { schemaVersion: 1, packs: [] };
130
+ if (!response.ok) fail(`GET ${urlFor("index.json")} -> HTTP ${response.status}`);
131
+ const index = await response.json();
132
+ if (!Array.isArray(index.packs)) return { schemaVersion: 1, packs: [] };
133
+ return index;
134
+ } catch (error) {
135
+ if (dryRun) return { schemaVersion: 1, packs: [] };
136
+ fail(`cannot read ${urlFor("index.json")}: ${error.message}`);
137
+ }
138
+ return { schemaVersion: 1, packs: [] };
139
+ }
140
+
141
+ async function verifyRemote(name, entry) {
142
+ if (dryRun) return;
143
+ const url = urlFor(name);
144
+ const head = await fetch(url, { method: "HEAD", redirect: "follow" });
145
+ if (!head.ok) fail(`HEAD ${url} -> HTTP ${head.status}`);
146
+ const length = Number(head.headers.get("content-length")) || 0;
147
+ if (length !== entry.bytes) fail(`content-length mismatch for ${url}: ${length} != ${entry.bytes}`);
148
+ const range = await fetch(url, { headers: { range: "bytes=0-15" } });
149
+ if (range.status !== 206) {
150
+ console.warn(`[publish] warning: ${url} answered ${range.status} to a range request (no resume)`);
151
+ }
152
+ const response = await fetch(url, { redirect: "follow" });
153
+ const buffer = Buffer.from(await response.arrayBuffer());
154
+ const actual = createHash("sha256").update(buffer).digest("hex");
155
+ if (entry.sha256 && actual !== entry.sha256) {
156
+ fail(`sha256 mismatch for ${url}: ${actual} != ${entry.sha256}`);
157
+ }
158
+ console.log(`[publish] verified ${url}`);
159
+ }
160
+
161
+ // Pre-flight: never publish a manifest that does not match the bytes on disk.
162
+ const artifacts = [];
163
+ for (const [tier] of tiers) {
164
+ if (tier !== "full") {
165
+ fail(
166
+ `legacy "${tier}" tier in ${manifestFile}; larkit now publishes a single "full" archive ` +
167
+ "(rebuild with `larkit pack build`)",
168
+ );
169
+ }
170
+ }
171
+ for (const [tier, entry] of tiers) {
172
+ const file = path.join(packDir, entry.file);
173
+ if (!fs.existsSync(file)) fail(`missing artifact for ${tier} tier: ${file}`);
174
+ const stat = fs.statSync(file);
175
+ if (entry.bytes && stat.size !== entry.bytes) {
176
+ fail(`${entry.file}: size ${stat.size} != manifest ${entry.bytes}`);
177
+ }
178
+ const actual = sha256(file);
179
+ if (entry.sha256 && actual !== entry.sha256) {
180
+ fail(`${entry.file}: sha256 ${actual} != manifest ${entry.sha256}`);
181
+ }
182
+ artifacts.push({ tier, file, name: entry.file, bytes: stat.size, sha256: actual });
183
+ }
184
+
185
+ // Immutability guard: archive names carry the tag, so a tag is a permanent URL.
186
+ const index = await fetchIndex();
187
+ const existing = index.packs.find((entry) => entry.tag === tag);
188
+ const fingerprint = Object.fromEntries(artifacts.map((a) => [a.tier, a.sha256]));
189
+ if (existing && !force) {
190
+ const drift = Object.entries(fingerprint).filter(
191
+ ([tier, sha]) => existing.tiers?.[tier]?.sha256 && existing.tiers[tier].sha256 !== sha,
192
+ );
193
+ if (drift.length) {
194
+ fail(
195
+ `tag "${tag}" is already published with different bytes (${drift
196
+ .map(([tier]) => tier)
197
+ .join(", ")}); rebuild with a new --tag=<date> (tags are immutable), or pass --force`,
198
+ );
199
+ }
200
+ console.log(`[publish] tag "${tag}" already published with identical content`);
201
+ if (!(manifestOnly || force)) {
202
+ console.log("[publish] nothing to upload (use --manifest-only to refresh manifest.json)");
203
+ }
204
+ }
205
+
206
+ console.log(
207
+ `[publish] pack ${tag} (larkit ${manifest.larkitVersion}) -> r2://${bucket}/${prefix || ""}, base ${baseUrl}`,
208
+ );
209
+
210
+ const uploaded = [];
211
+ if (!manifestOnly) {
212
+ for (const artifact of artifacts) {
213
+ if (existing && !force && existing.tiers?.[artifact.tier]?.sha256 === artifact.sha256) {
214
+ console.log(`[publish] re-use ${keyFor(artifact.name)} (same sha256)`);
215
+ continue;
216
+ }
217
+ upload(artifact.file, artifact.name, {
218
+ contentType: "application/gzip",
219
+ cacheControl: IMMUTABLE,
220
+ });
221
+ uploaded.push(artifact);
222
+ }
223
+ }
224
+
225
+ upload(manifestFile, `${tag}.json`, { contentType: "application/json", cacheControl: IMMUTABLE });
226
+ upload(manifestFile, "manifest.json", {
227
+ contentType: "application/json",
228
+ cacheControl: MANIFEST_CACHE,
229
+ });
230
+
231
+ const nextIndex = {
232
+ schemaVersion: 1,
233
+ updatedAt: new Date().toISOString(),
234
+ latest: tag,
235
+ packs: [
236
+ ...index.packs.filter((entry) => entry.tag !== tag),
237
+ {
238
+ tag,
239
+ builtAt: manifest.builtAt || null,
240
+ larkitVersion: manifest.larkitVersion || null,
241
+ clientVersion: manifest.clientVersion || null,
242
+ stats: manifest.stats || null,
243
+ manifest: urlFor(`${tag}.json`),
244
+ tiers: Object.fromEntries(
245
+ artifacts.map((a) => [a.tier, { url: urlFor(a.name), bytes: a.bytes, sha256: a.sha256 }]),
246
+ ),
247
+ },
248
+ ].sort((a, b) => String(a.tag).localeCompare(String(b.tag))),
249
+ };
250
+ const indexPath = path.join(packDir, "index.json");
251
+ fs.writeFileSync(indexPath, `${JSON.stringify(nextIndex, null, 2)}\n`);
252
+ upload(indexPath, "index.json", { contentType: "application/json", cacheControl: MANIFEST_CACHE });
253
+
254
+ if (!manifestOnly) {
255
+ for (const artifact of uploaded) {
256
+ await verifyRemote(artifact.name, artifact);
257
+ }
258
+ }
259
+
260
+ const manifestUrl = urlFor("manifest.json");
261
+ if (!dryRun) {
262
+ const check = await fetch(manifestUrl, { redirect: "follow", headers: { "cache-control": "no-cache" } });
263
+ if (!check.ok) fail(`GET ${manifestUrl} -> HTTP ${check.status}`);
264
+ const served = await check.json();
265
+ if ((served.packVersion || served.larkitVersion) !== tag) {
266
+ fail(`served manifest tag ${served.packVersion || served.larkitVersion} != ${tag}`);
267
+ }
268
+ }
269
+
270
+ console.log("");
271
+ console.log("[publish] done. truth source is live:");
272
+ console.log(` manifest ${manifestUrl}`);
273
+ console.log(` pinned ${urlFor(`${tag}.json`)}`);
274
+ console.log(` index ${urlFor("index.json")}`);
275
+ console.log("");
276
+ console.log("[publish] keep old tags on R2: published npm versions and pinned");
277
+ console.log("[publish] manifests point at those archive names forever.");
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * End-to-end bootstrap verification with a synthetic pack:
4
+ * tar round-trip → pack build → `larkit init` in a temp home → search/status.
5
+ *
6
+ * Uses child processes for init/search because paths are resolved from the
7
+ * environment at module load time.
8
+ */
9
+ import "../src/shared/quiet.js";
10
+ import fs from "node:fs";
11
+ import os from "node:os";
12
+ import path from "node:path";
13
+ import process from "node:process";
14
+ import { spawnSync } from "node:child_process";
15
+ import { createTarGz, extractTarGz } from "../src/bootstrap/tar.js";
16
+
17
+ const root = path.resolve(import.meta.dirname, "..");
18
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "larkit-verify-"));
19
+ let failed = 0;
20
+
21
+ function check(name, condition, detail = "") {
22
+ if (condition) {
23
+ console.log(`PASS ${name}`);
24
+ } else {
25
+ failed++;
26
+ console.error(`FAIL ${name}${detail ? `: ${detail}` : ""}`);
27
+ }
28
+ }
29
+
30
+ async function makeFixture() {
31
+ const { DatabaseSync } = await import("node:sqlite");
32
+ const dataDir = path.join(tmp, "source-data");
33
+ const verifiedDir = path.join(tmp, "source-verified");
34
+ fs.mkdirSync(verifiedDir, { recursive: true });
35
+ const formatted = path.join(dataDir, "formatted", "demo");
36
+ fs.mkdirSync(formatted, { recursive: true });
37
+ fs.writeFileSync(
38
+ path.join(formatted, "sample.js"),
39
+ "function cardLink() {\n return 'card_link';\n}\n",
40
+ );
41
+ fs.writeFileSync(
42
+ path.join(verifiedDir, "demo-truth.md"),
43
+ "---\ntopic: demo card truth\nsource: doc\nconfidence: high\n---\n\n## 结论\n\nchart_spec is documented here.\n",
44
+ );
45
+ const db = new DatabaseSync(path.join(dataDir, "truth.db"));
46
+ db.exec(`
47
+ CREATE TABLE verified_truths (
48
+ id INTEGER PRIMARY KEY, topic TEXT, section TEXT, path TEXT,
49
+ line_start INTEGER, line_end INTEGER, verified_at TEXT, source TEXT,
50
+ confidence TEXT, body TEXT
51
+ );
52
+ CREATE VIRTUAL TABLE verified_fts USING fts5(
53
+ topic, section, body, path, search_text
54
+ );
55
+ CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT);
56
+ `);
57
+ db.prepare(
58
+ `INSERT INTO verified_truths (topic, section, path, line_start, line_end, verified_at, source, confidence, body)
59
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
60
+ ).run(
61
+ "demo card truth",
62
+ "结论",
63
+ "verified_truths/demo-truth.md",
64
+ 1,
65
+ 3,
66
+ "2026-01-01T00:00:00.000Z",
67
+ "doc",
68
+ "high",
69
+ "chart_spec is documented here.",
70
+ );
71
+ db.prepare(
72
+ `INSERT INTO verified_fts (rowid, topic, section, body, path, search_text)
73
+ VALUES (1, ?, ?, ?, ?, ?)`,
74
+ ).run(
75
+ "demo card truth",
76
+ "结论",
77
+ "chart_spec is documented here.",
78
+ "verified_truths/demo-truth.md",
79
+ "chart_spec demo card truth",
80
+ );
81
+ db.close();
82
+ fs.writeFileSync(
83
+ path.join(dataDir, "truth.meta.json"),
84
+ `${JSON.stringify({ generatedAt: new Date().toISOString(), protocolCatalog: 1, codeAnchors: 1, verifiedSections: 1, protocolDocs: 0 }, null, 2)}\n`,
85
+ );
86
+ return { dataDir, verifiedDir };
87
+ }
88
+
89
+ function run(args, env) {
90
+ const result = spawnSync(process.execPath, [path.join(root, "bin", "larkit.js"), ...args], {
91
+ cwd: root,
92
+ env: { ...process.env, ...env },
93
+ encoding: "utf8",
94
+ });
95
+ return {
96
+ status: result.status,
97
+ stdout: result.stdout,
98
+ stderr: result.stderr,
99
+ json: (() => {
100
+ try {
101
+ return JSON.parse(result.stdout);
102
+ } catch {
103
+ return null;
104
+ }
105
+ })(),
106
+ };
107
+ }
108
+
109
+ try {
110
+ // 1. tar round-trip, including long and UTF-8 paths.
111
+ const longRel =
112
+ "formatted/very/deep/directory/that/exceeds/the/classic/ustar/one-hundred-byte/name/limit/for/sure/file.js";
113
+ const tarSource = path.join(tmp, "tar-source");
114
+ fs.mkdirSync(path.join(tarSource, path.dirname(longRel)), { recursive: true });
115
+ fs.writeFileSync(path.join(tarSource, longRel), "console.log('卡片 chart_spec');\n");
116
+ fs.writeFileSync(path.join(tarSource, "short.txt"), "hello\n");
117
+ const archive = path.join(tmp, "roundtrip.tar.gz");
118
+ await createTarGz(archive, {
119
+ entries: [
120
+ { file: path.join(tarSource, "short.txt"), tarPath: "short.txt" },
121
+ { file: path.join(tarSource, longRel), tarPath: longRel },
122
+ ],
123
+ });
124
+ const tarOut = path.join(tmp, "tar-out");
125
+ await extractTarGz(archive, tarOut);
126
+ check("tar: short file", fs.readFileSync(path.join(tarOut, "short.txt"), "utf8") === "hello\n");
127
+ check(
128
+ "tar: long path + utf8",
129
+ fs.readFileSync(path.join(tarOut, longRel), "utf8").includes("卡片 chart_spec"),
130
+ );
131
+ const traversal = path.join(tmp, "traversal.tar.gz");
132
+ await createTarGz(traversal, {
133
+ entries: [{ tarPath: "../escape.txt", content: "nope" }],
134
+ });
135
+ let traversalRejected = false;
136
+ try {
137
+ await extractTarGz(traversal, path.join(tmp, "traversal-out"));
138
+ } catch {
139
+ traversalRejected = true;
140
+ }
141
+ check("tar: path traversal rejected", traversalRejected);
142
+ const symlinkDir = path.join(tmp, "symlink-out");
143
+ fs.mkdirSync(symlinkDir, { recursive: true });
144
+ fs.symlinkSync(path.join(tmp, "outside.txt"), path.join(symlinkDir, "x.txt"));
145
+ const symlinkArchive = path.join(tmp, "symlink.tar.gz");
146
+ await createTarGz(symlinkArchive, {
147
+ entries: [{ tarPath: "x.txt", content: "oops" }],
148
+ });
149
+ let symlinkRejected = false;
150
+ try {
151
+ await extractTarGz(symlinkArchive, symlinkDir);
152
+ } catch {
153
+ symlinkRejected = true;
154
+ }
155
+ check("tar: symlink overwrite rejected", symlinkRejected);
156
+
157
+ // 2. Build a synthetic truth pack.
158
+ const fixture = await makeFixture();
159
+ const packDir = path.join(tmp, "pack");
160
+ const build = spawnSync(
161
+ process.execPath,
162
+ [
163
+ path.join(root, "bin", "larkit.js"),
164
+ "pack",
165
+ "build",
166
+ `--out=${packDir}`,
167
+ ],
168
+ {
169
+ cwd: root,
170
+ env: {
171
+ ...process.env,
172
+ LARKIT_DATA_DIR: fixture.dataDir,
173
+ LARKIT_VERIFIED_DIR: fixture.verifiedDir,
174
+ LARKIT_HOME: path.join(tmp, "builder-home"),
175
+ },
176
+ encoding: "utf8",
177
+ },
178
+ );
179
+ check("pack build exits 0", build.status === 0, build.stderr?.slice(-600));
180
+ check("pack manifest exists", fs.existsSync(path.join(packDir, "manifest.json")));
181
+
182
+ // 3. Init into a temp home.
183
+ const home = path.join(tmp, "home");
184
+ const userHome = path.join(tmp, "userhome");
185
+ fs.mkdirSync(userHome, { recursive: true });
186
+ const skillDir = path.join(userHome, ".agents", "skills", "larkit");
187
+ const env = {
188
+ HOME: userHome,
189
+ USERPROFILE: userHome,
190
+ LARKIT_HOME: home,
191
+ LARKIT_DATA_DIR: path.join(home, "data"),
192
+ LARKIT_VERIFIED_DIR: path.join(home, "verified_truths"),
193
+ LARKIT_BIN_DIR: path.join(home, "bin"),
194
+ };
195
+ const init = run(
196
+ ["init", `--pack=${packDir}`, "--no-cli", "--json"],
197
+ env,
198
+ );
199
+ check("init exits 0", init.status === 0, init.stderr?.slice(-1200));
200
+ check("init reports truth ready", init.json?.steps?.truth?.status === "ready", init.stdout);
201
+ check("truth.db installed", fs.existsSync(path.join(home, "data", "truth.db")));
202
+ check(
203
+ "verified truth seeded",
204
+ fs.existsSync(path.join(home, "verified_truths", "demo-truth.md")),
205
+ );
206
+ check("skill installed", fs.existsSync(path.join(skillDir, "SKILL.md")));
207
+ check("skill references installed", fs.existsSync(path.join(skillDir, "references", "playbooks.md")));
208
+ check("mcp config written", fs.existsSync(path.join(home, "mcp.json")));
209
+
210
+ // 4. Query through the installed home.
211
+ const search = run(["search", "chart_spec", "--scope=verified"], env);
212
+ check("search exits 0", search.status === 0, search.stderr?.slice(-600));
213
+ check("search hits seeded truth", (search.json?.results?.length || 0) > 0, search.stdout);
214
+
215
+ const status = run(["status", "--json"], env);
216
+ check("status exits 0", status.status === 0, status.stderr?.slice(-600));
217
+ check("status sees installed truth", status.json?.truth?.installed === true);
218
+ check("status sees skill", (status.json?.skills?.length || 0) === 1, status.stdout);
219
+
220
+ const slice = run(
221
+ ["inspect", "verified_truths/demo-truth.md", "5", "--max-lines=10"],
222
+ env,
223
+ );
224
+ check("inspect verified path", slice.status === 0 && slice.json?.lines?.length > 0, slice.stderr);
225
+
226
+ fs.rmSync(tmp, { recursive: true, force: true });
227
+ } catch (error) {
228
+ failed++;
229
+ console.error(error);
230
+ try {
231
+ fs.rmSync(tmp, { recursive: true, force: true });
232
+ } catch {
233
+ // ignore
234
+ }
235
+ }
236
+
237
+ console.log(failed ? `\n${failed} bootstrap check(s) failed` : "\nall bootstrap checks passed");
238
+ process.exit(failed ? 1 : 0);
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+ import "../src/shared/quiet.js";
3
+ import process from "node:process";
4
+ import { checkAuth } from "../src/auth/service.js";
5
+ import {
6
+ enableBotWs,
7
+ getCredentials,
8
+ grantScopes,
9
+ listApps,
10
+ publishVersion,
11
+ } from "../src/console/client.js";
12
+ import { probeOpenApi } from "../src/probe/openapi.js";
13
+
14
+ const profileArg = process.argv.find((arg) => arg.startsWith("--profile="));
15
+ const profile = profileArg ? profileArg.slice(10) : "geren";
16
+
17
+ const auth = await checkAuth({ profile });
18
+ console.log("auth:", auth.ok, auth.profile, auth.user?.name || auth.hint || auth.error);
19
+ if (!auth.ok) process.exit(1);
20
+
21
+ const apps = await listApps({ profile, limit: 30 });
22
+ console.log("apps:", apps.ok, apps.total);
23
+ if (!apps.ok || !apps.apps.length) process.exit(1);
24
+
25
+ const app =
26
+ apps.apps.find((item) => item.status === 2 && item.abilities?.includes("bot")) ||
27
+ apps.apps[0];
28
+ console.log("selected app:", app.app_id, app.name, "status=" + app.status);
29
+
30
+ const cred = await getCredentials({ appId: app.app_id, profile });
31
+ console.log("credentials:", cred.ok, "secret_len=" + (cred.app_secret?.length || 0));
32
+ if (!cred.ok) process.exit(1);
33
+
34
+ const probe = await probeOpenApi({
35
+ appId: cred.app_id,
36
+ appSecret: cred.app_secret,
37
+ method: "GET",
38
+ path: "/im/v1/chats",
39
+ query: { page_size: 1 },
40
+ });
41
+ console.log("openapi probe:", probe.status, probe.code, probe.msg, probe.log_id || "");
42
+
43
+ const wsPlan = await enableBotWs({ appId: app.app_id, profile, dryRun: true });
44
+ console.log("bot ws dry-run:", wsPlan.ok, wsPlan.calls?.length || 0, "calls");
45
+
46
+ const scopePlan = await grantScopes({
47
+ appId: app.app_id,
48
+ scopes: ["im:message"],
49
+ profile,
50
+ dryRun: true,
51
+ });
52
+ console.log(
53
+ "scope dry-run:",
54
+ scopePlan.ok,
55
+ "resolved=" + (scopePlan.plan?.resolved?.map((s) => s.name).join(",") || ""),
56
+ "missing=" + (scopePlan.plan?.missing?.join(",") || ""),
57
+ );
58
+
59
+ const publishPlan = await publishVersion({ appId: app.app_id, profile, dryRun: true });
60
+ console.log("publish dry-run:", publishPlan.ok, publishPlan.plan?.steps?.length || 0, "steps");