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.
Files changed (73) 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 +65 -4
  7. package/scripts/.tmp-npm-webauth.mjs +49 -0
  8. package/scripts/build-index.mjs +66 -0
  9. package/scripts/check-syntax.mjs +28 -0
  10. package/scripts/login-worker.mjs +50 -0
  11. package/scripts/publish-pack.mjs +277 -0
  12. package/scripts/verify-bootstrap.mjs +238 -0
  13. package/scripts/verify-console.mjs +60 -0
  14. package/scripts/verify-index.mjs +59 -0
  15. package/scripts/verify-mcp.mjs +215 -0
  16. package/skills/larkit/SKILL.md +120 -0
  17. package/skills/larkit/references/console.md +86 -0
  18. package/skills/larkit/references/playbooks.md +177 -0
  19. package/skills/larkit/references/tools.md +115 -0
  20. package/skills/larkit/references/truth-model.md +117 -0
  21. package/src/auth/login.js +171 -0
  22. package/src/auth/service.js +213 -0
  23. package/src/auth/store.js +213 -0
  24. package/src/bootstrap/channel.js +201 -0
  25. package/src/bootstrap/cli-install.js +184 -0
  26. package/src/bootstrap/download.js +182 -0
  27. package/src/bootstrap/init.js +672 -0
  28. package/src/bootstrap/pack-cli.js +97 -0
  29. package/src/bootstrap/pack.js +541 -0
  30. package/src/bootstrap/report.js +62 -0
  31. package/src/bootstrap/skills.js +223 -0
  32. package/src/bootstrap/state.js +66 -0
  33. package/src/bootstrap/tar.js +417 -0
  34. package/src/console/browser.js +338 -0
  35. package/src/console/client.js +827 -0
  36. package/src/console/dispatch.js +101 -0
  37. package/src/docs/client.js +284 -0
  38. package/src/indexer/anchors.js +366 -0
  39. package/src/indexer/db.js +449 -0
  40. package/src/indexer/extract-assets.js +285 -0
  41. package/src/indexer/extract-deb.js +56 -0
  42. package/src/indexer/format-pool.js +64 -0
  43. package/src/indexer/format-worker.js +132 -0
  44. package/src/indexer/format.js +239 -0
  45. package/src/indexer/protocol.js +123 -0
  46. package/src/mcp/server.js +433 -0
  47. package/src/probe/client.js +225 -0
  48. package/src/probe/openapi.js +194 -0
  49. package/src/protocol/catalog.js +146 -0
  50. package/src/protocol/data/README.md +15 -0
  51. package/src/protocol/data/bundle.json +1 -0
  52. package/src/protocol/data/commands.json +12896 -0
  53. package/src/protocol/data/constants.json +8 -0
  54. package/src/protocol/data/reference/PROTOCOL.md +66 -0
  55. package/src/protocol/data/reference/endpoints.json +46 -0
  56. package/src/protocol/data/schema.json.gz +0 -0
  57. package/src/protocol/descriptor.js +358 -0
  58. package/src/protocol/schema.js +235 -0
  59. package/src/protocol/transport.js +387 -0
  60. package/src/shared/browser.js +70 -0
  61. package/src/shared/config.js +90 -0
  62. package/src/shared/paths.js +151 -0
  63. package/src/shared/quiet.js +12 -0
  64. package/src/shared/util.js +124 -0
  65. package/src/truth/fts.js +24 -0
  66. package/src/truth/search.js +554 -0
  67. package/src/truth/verified.js +107 -0
  68. package/verified_truths/README.md +28 -0
  69. package/verified_truths/card-json-2.0-official.md +55 -0
  70. package/verified_truths/client-ws-gateway-boundaries.md +33 -0
  71. package/verified_truths/feishu-developer-console-apis.md +104 -0
  72. package/workers/truth-api/src/index.js +180 -0
  73. package/workers/truth-api/wrangler.toml +31 -0
@@ -0,0 +1,285 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import * as asar from "@electron/asar";
4
+ import {
5
+ ELECTRON_EXTRACT_DIR,
6
+ ELECTRON_INDEX,
7
+ FEISHU_APP_DIR,
8
+ RAW_DIR,
9
+ WEB_DUMP_DIR,
10
+ WEB_INDEX,
11
+ WEB_SUMMARY,
12
+ } from "../shared/paths.js";
13
+ import {
14
+ ensureDir,
15
+ fileSizeMiB,
16
+ log,
17
+ readJson,
18
+ relativeTo,
19
+ toPosix,
20
+ writeJson,
21
+ } from "../shared/util.js";
22
+ import { ensureDebExtracted, directorySize } from "./extract-deb.js";
23
+
24
+ /**
25
+ * Curated desktop asar set. The Linux client has no single app.asar: its main
26
+ * process is native (feishu/libiron), and every window is a separate asar.
27
+ * We extract all non-i18n JS from the windows that carry messaging, cards,
28
+ * spaces and settings, plus the preload/resource bundles.
29
+ */
30
+ const ASAR_SELECTION = [
31
+ { name: "resource.asar", all: true },
32
+ { name: "lark-api.asar", all: true },
33
+ { name: "jssdk-mf.asar", all: true },
34
+ { name: "js-worker.asar", all: true },
35
+ { name: "shared-libs.asar", all: true },
36
+ { name: "messenger.asar", all: false },
37
+ { name: "main-window.asar", all: false },
38
+ { name: "messenger-modals.asar", all: false },
39
+ { name: "space.asar", all: false },
40
+ { name: "setting.asar", all: false },
41
+ { name: "search.asar", all: false },
42
+ ];
43
+
44
+ const LOOSE_EXTENSIONS = new Set([".js", ".css", ".json"]);
45
+ const ASAR_EXTENSIONS = new Set([".js", ".css", ".json"]);
46
+
47
+ function isInterestingAsarEntry(entry, all) {
48
+ if (entry.endsWith("/")) return false;
49
+ const ext = path.extname(entry).toLowerCase();
50
+ if (!ASAR_EXTENSIONS.has(ext)) return false;
51
+ if (!all && entry.startsWith("/i18n/")) return false;
52
+ // Massive locale bundles are noise for a code/truth index.
53
+ if (entry.includes("/i18n/") && !all) return false;
54
+ return true;
55
+ }
56
+
57
+ function buildWebIndex() {
58
+ const summary = readJson(WEB_SUMMARY, null);
59
+ if (!summary?.captures?.length) {
60
+ throw new Error(`missing or invalid ${WEB_SUMMARY}`);
61
+ }
62
+ const entries = [];
63
+ const seen = new Set();
64
+ let missing = 0;
65
+ let bytes = 0;
66
+ for (const capture of summary.captures) {
67
+ const key = capture.file;
68
+ if (!key || seen.has(key)) continue;
69
+ seen.add(key);
70
+ const sourcePath = path.join(WEB_DUMP_DIR, capture.file);
71
+ if (!fs.existsSync(sourcePath)) {
72
+ missing++;
73
+ continue;
74
+ }
75
+ const stat = fs.statSync(sourcePath);
76
+ bytes += stat.size;
77
+ entries.push({
78
+ scope: "web",
79
+ sourceTag: "web",
80
+ relPath: toPosix(capture.file),
81
+ sourcePath,
82
+ assetType: capture.assetType,
83
+ kind: capture.kind,
84
+ sha1: capture.sha1,
85
+ bytes: stat.size,
86
+ resourceUrl: capture.resourceUrl,
87
+ targetType: capture.targetType,
88
+ });
89
+ }
90
+ writeJson(WEB_INDEX, {
91
+ generatedAt: new Date().toISOString(),
92
+ totalCaptures: summary.captures.length,
93
+ uniqueFiles: entries.length,
94
+ missing,
95
+ bytes,
96
+ entries,
97
+ });
98
+ log(
99
+ `[extract] web index: ${entries.length}/${summary.captures.length} files, ${fileSizeMiB(bytes)}${missing ? `, missing=${missing}` : ""}`,
100
+ );
101
+ return entries;
102
+ }
103
+
104
+ function buildLooseElectronIndex() {
105
+ const root = path.join(FEISHU_APP_DIR, "webcontent");
106
+ const entries = [];
107
+ const seen = new Set();
108
+ const stack = [root];
109
+ while (stack.length) {
110
+ const current = stack.pop();
111
+ let children;
112
+ try {
113
+ children = fs.readdirSync(current, { withFileTypes: true });
114
+ } catch {
115
+ continue;
116
+ }
117
+ for (const child of children) {
118
+ if (child.name.startsWith(".")) continue;
119
+ const full = path.join(current, child.name);
120
+ if (child.isDirectory()) {
121
+ if (child.name === "i18n" || child.name === "locales") continue;
122
+ if (child.name === "node_modules") continue;
123
+ stack.push(full);
124
+ continue;
125
+ }
126
+ if (!child.isFile()) continue;
127
+ const ext = path.extname(child.name).toLowerCase();
128
+ if (!LOOSE_EXTENSIONS.has(ext)) continue;
129
+ let stat;
130
+ try {
131
+ stat = fs.statSync(full);
132
+ } catch {
133
+ continue;
134
+ }
135
+ if (stat.size === 0 || stat.size > 15 * 1024 * 1024) continue;
136
+ const rel = relativeTo(root, full);
137
+ if (seen.has(rel)) continue;
138
+ seen.add(rel);
139
+ entries.push({
140
+ scope: "electron",
141
+ sourceTag: "loose",
142
+ relPath: rel,
143
+ sourcePath: full,
144
+ assetType: ext.slice(1),
145
+ bytes: stat.size,
146
+ });
147
+ }
148
+ }
149
+ return entries;
150
+ }
151
+
152
+ function buildAsarElectronIndex() {
153
+ const webcontent = path.join(FEISHU_APP_DIR, "webcontent");
154
+ const entries = [];
155
+ let totalBytes = 0;
156
+ let extracted = 0;
157
+ let skipped = 0;
158
+ for (const selection of ASAR_SELECTION) {
159
+ const archive = path.join(webcontent, selection.name);
160
+ if (!fs.existsSync(archive)) {
161
+ log(`[extract] asar missing: ${selection.name}`);
162
+ continue;
163
+ }
164
+ const archiveBase = selection.name.replace(/\.asar$/, "");
165
+ const outRoot = path.join(ELECTRON_EXTRACT_DIR, "asar", archiveBase);
166
+ ensureDir(outRoot);
167
+ const list = asar.listPackage(archive);
168
+ let archiveCount = 0;
169
+ for (const entry of list) {
170
+ if (!isInterestingAsarEntry(entry, selection.all)) continue;
171
+ const rel = entry.replace(/^\/+/, "");
172
+ const dest = path.join(outRoot, rel);
173
+ const ext = path.extname(rel).toLowerCase();
174
+ archiveCount++;
175
+ if (fs.existsSync(dest) && fs.statSync(dest).size > 0) {
176
+ skipped++;
177
+ } else {
178
+ try {
179
+ const buf = asar.extractFile(archive, rel);
180
+ ensureDir(path.dirname(dest));
181
+ fs.writeFileSync(dest, buf);
182
+ extracted++;
183
+ } catch (error) {
184
+ log(`[extract] asar read failed ${selection.name}:${entry}: ${error.message}`);
185
+ continue;
186
+ }
187
+ }
188
+ let size = 0;
189
+ try {
190
+ size = fs.statSync(dest).size;
191
+ } catch {
192
+ continue;
193
+ }
194
+ totalBytes += size;
195
+ entries.push({
196
+ scope: "electron",
197
+ sourceTag: `asar/${archiveBase}`,
198
+ relPath: rel,
199
+ sourcePath: dest,
200
+ assetType: ext.slice(1),
201
+ bytes: size,
202
+ archive: selection.name,
203
+ });
204
+ }
205
+ log(
206
+ `[extract] ${selection.name}: ${archiveCount} selected files (${extracted} new, ${skipped} cached)`,
207
+ );
208
+ }
209
+ return { entries, totalBytes, extracted, skipped };
210
+ }
211
+
212
+ function writeElectronReadme() {
213
+ const file = path.join(ELECTRON_EXTRACT_DIR, "README.md");
214
+ const text = `# Electron extraction notes
215
+
216
+ The Linux client **does not ship a single \`app.asar\`**. Its window code lives in
217
+ \`webcontent/*.asar\`, while the Electron main process is the native \`feishu\`
218
+ binary plus \`libiron\` / \`liblark\`. This directory therefore contains:
219
+
220
+ - \`loose/\`: JS/CSS/JSON that the deb already extracts under \`webcontent/\`
221
+ (jssdk worklets, js-worker, openplatform, lark-api, bridge.js, ...).
222
+ - \`asar/<archive>/\`: every non-i18n JS/CSS/JSON entry from the curated asar
223
+ set (messenger, main-window, messenger-modals, space, setting, search,
224
+ resource, lark-api, jssdk-mf, js-worker, shared-libs).
225
+
226
+ The \`resource.asar/js/*\` files are the closest equivalent to preload /
227
+ bootstrap scripts; \`messenger.asar/common/*\` and
228
+ \`main-window.asar/common/*\` are the renderer chunks used by the desktop UI.
229
+
230
+ No package.json was present inside the selected asar archives. Desktop
231
+ protocol/schema truth ultimately comes from \`pb.desc\`, \`pb_server.desc\`,
232
+ the jssdk worklets and these renderer chunks.
233
+ `;
234
+ fs.writeFileSync(file, text);
235
+ }
236
+
237
+ export function extractAssets() {
238
+ ensureDebExtracted();
239
+ ensureDir(RAW_DIR);
240
+ ensureDir(ELECTRON_EXTRACT_DIR);
241
+ writeElectronReadme();
242
+
243
+ const webEntries = buildWebIndex();
244
+ const looseEntries = buildLooseElectronIndex();
245
+ const asarResult = buildAsarElectronIndex();
246
+ const electronEntries = [...looseEntries, ...asarResult.entries];
247
+
248
+ writeJson(ELECTRON_INDEX, {
249
+ generatedAt: new Date().toISOString(),
250
+ loose: looseEntries.length,
251
+ asar: asarResult.entries.length,
252
+ total: electronEntries.length,
253
+ bytes: electronEntries.reduce((sum, e) => sum + e.bytes, 0),
254
+ entries: electronEntries,
255
+ });
256
+
257
+ const manifest = {
258
+ generatedAt: new Date().toISOString(),
259
+ deb: "feishu-linux.deb",
260
+ clientVersion: readClientVersion(),
261
+ web: { entries: webEntries.length, bytes: webEntries.reduce((s, e) => s + e.bytes, 0) },
262
+ electron: {
263
+ loose: looseEntries.length,
264
+ asar: asarResult.entries.length,
265
+ extractedNow: asarResult.extracted,
266
+ cached: asarResult.skipped,
267
+ bytes: electronEntries.reduce((s, e) => s + e.bytes, 0),
268
+ },
269
+ extractedDirBytes: directorySize(ELECTRON_EXTRACT_DIR),
270
+ };
271
+ writeJson(path.join(RAW_DIR, "manifest.extract.json"), manifest);
272
+ log(
273
+ `[extract] electron index: ${electronEntries.length} files (${manifest.electron.bytes ? fileSizeMiB(manifest.electron.bytes) : "0"})`,
274
+ );
275
+ return { webEntries, electronEntries, manifest };
276
+ }
277
+
278
+ function readClientVersion() {
279
+ try {
280
+ const manifest = readJson(path.join(FEISHU_APP_DIR, "manifest.json"), {});
281
+ return manifest.appVersion || "unknown";
282
+ } catch {
283
+ return "unknown";
284
+ }
285
+ }
@@ -0,0 +1,56 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ import { DEB_FILE, DEB_ROOT, FEISHU_APP_DIR } from "../shared/paths.js";
5
+ import { ensureDir, log, fileSizeMiB } from "../shared/util.js";
6
+
7
+ export function ensureDebExtracted({ force = false } = {}) {
8
+ const marker = path.join(FEISHU_APP_DIR, "manifest.json");
9
+ if (!force && fs.existsSync(marker)) {
10
+ log(`[extract] deb already extracted: ${DEB_ROOT}`);
11
+ return FEISHU_APP_DIR;
12
+ }
13
+ if (!fs.existsSync(DEB_FILE)) {
14
+ throw new Error(`missing ${DEB_FILE}`);
15
+ }
16
+ ensureDir(DEB_ROOT);
17
+ log(`[extract] dpkg-deb -x ${path.basename(DEB_FILE)} -> ${DEB_ROOT}`);
18
+ const t0 = Date.now();
19
+ const result = spawnSync("dpkg-deb", ["-x", DEB_FILE, DEB_ROOT], {
20
+ stdio: "inherit",
21
+ });
22
+ if (result.status !== 0) {
23
+ throw new Error(`dpkg-deb failed with exit code ${result.status}`);
24
+ }
25
+ const size = directorySize(DEB_ROOT);
26
+ log(
27
+ `[extract] deb extracted in ${((Date.now() - t0) / 1000).toFixed(1)}s (${fileSizeMiB(size)})`,
28
+ );
29
+ return FEISHU_APP_DIR;
30
+ }
31
+
32
+ export function directorySize(dir) {
33
+ let total = 0;
34
+ const stack = [dir];
35
+ while (stack.length) {
36
+ const current = stack.pop();
37
+ let entries;
38
+ try {
39
+ entries = fs.readdirSync(current, { withFileTypes: true });
40
+ } catch {
41
+ continue;
42
+ }
43
+ for (const entry of entries) {
44
+ const full = path.join(current, entry.name);
45
+ if (entry.isDirectory()) stack.push(full);
46
+ else if (entry.isFile()) {
47
+ try {
48
+ total += fs.statSync(full).size;
49
+ } catch {
50
+ // ignore
51
+ }
52
+ }
53
+ }
54
+ }
55
+ return total;
56
+ }
@@ -0,0 +1,64 @@
1
+ import os from "node:os";
2
+ import { Worker } from "node:worker_threads";
3
+
4
+ export async function runFormatPool(tasks, options = {}) {
5
+ const concurrency = Math.max(
6
+ 1,
7
+ Math.min(
8
+ options.concurrency || Math.min(8, os.availableParallelism?.() || os.cpus().length),
9
+ tasks.length || 1,
10
+ ),
11
+ );
12
+ if (tasks.length === 0) return [];
13
+
14
+ const workerUrl = new URL("./format-worker.js", import.meta.url);
15
+ const workers = [];
16
+ const results = [];
17
+ let next = 0;
18
+ let completed = 0;
19
+ let failed = false;
20
+
21
+ return await new Promise((resolve, reject) => {
22
+ const finish = () => {
23
+ for (const worker of workers) worker.terminate();
24
+ resolve(results);
25
+ };
26
+
27
+ const assign = (worker) => {
28
+ if (next >= tasks.length) {
29
+ if (completed === tasks.length && !failed) finish();
30
+ return;
31
+ }
32
+ const task = tasks[next++];
33
+ worker.postMessage(task);
34
+ };
35
+
36
+ const onMessage = (worker, msg) => {
37
+ completed++;
38
+ results.push({ ...msg, code: undefined });
39
+ if (typeof options.onResult === "function") options.onResult(msg);
40
+ if (options.progress && completed % options.progress === 0) {
41
+ console.error(`[format] ${completed}/${tasks.length}`);
42
+ }
43
+ assign(worker);
44
+ };
45
+
46
+ for (let i = 0; i < concurrency; i++) {
47
+ const worker = new Worker(workerUrl);
48
+ workers.push(worker);
49
+ worker.on("message", (msg) => onMessage(worker, msg));
50
+ worker.on("error", (error) => {
51
+ failed = true;
52
+ for (const w of workers) w.terminate();
53
+ reject(error);
54
+ });
55
+ worker.on("exit", (code) => {
56
+ if (code !== 0 && !failed) {
57
+ failed = true;
58
+ reject(new Error(`format worker exited with code ${code}`));
59
+ }
60
+ });
61
+ assign(worker);
62
+ }
63
+ });
64
+ }
@@ -0,0 +1,132 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { parentPort } from "node:worker_threads";
4
+ import * as prettier from "prettier";
5
+ import beautify from "js-beautify";
6
+
7
+ function parserFor(file) {
8
+ const ext = path.extname(file).toLowerCase();
9
+ if (ext === ".json") return "json";
10
+ if (ext === ".css") return "css";
11
+ if (ext === ".html") return "html";
12
+ return "babel";
13
+ }
14
+
15
+ function basicBreak(code) {
16
+ // Last-resort line breaker for pathological minified input. It keeps strings
17
+ // intact and inserts newlines at structural characters.
18
+ let out = "";
19
+ let quote = "";
20
+ let escape = false;
21
+ for (let i = 0; i < code.length; i++) {
22
+ const ch = code[i];
23
+ out += ch;
24
+ if (quote) {
25
+ if (escape) escape = false;
26
+ else if (ch === "\\") escape = true;
27
+ else if (ch === quote) quote = "";
28
+ continue;
29
+ }
30
+ if (ch === '"' || ch === "'" || ch === "`") {
31
+ quote = ch;
32
+ continue;
33
+ }
34
+ if (ch === ";" || ch === "{" || ch === "}") {
35
+ if (i + 1 < code.length && code[i + 1] !== "\n") out += "\n";
36
+ }
37
+ }
38
+ return out;
39
+ }
40
+
41
+ function beautifyFallback(code, parser) {
42
+ try {
43
+ if (parser === "json") return `${JSON.stringify(JSON.parse(code), null, 2)}\n`;
44
+ if (parser === "css") {
45
+ return `${beautify.css(code, { indent_size: 2, preserve_newlines: false })}\n`;
46
+ }
47
+ if (parser === "html") {
48
+ return `${beautify.html(code, { indent_size: 2, wrap_line_length: 120 })}\n`;
49
+ }
50
+ return `${beautify.js(code, {
51
+ indent_size: 2,
52
+ preserve_newlines: false,
53
+ max_preserve_newlines: 1,
54
+ brace_style: "collapse,preserve-inline",
55
+ end_with_newline: true,
56
+ })}\n`;
57
+ } catch {
58
+ return `${basicBreak(code)}\n`;
59
+ }
60
+ }
61
+
62
+ parentPort.on("message", async (task) => {
63
+ const { id, srcPath, parser: requestedParser } = task;
64
+ const parser = requestedParser || parserFor(srcPath);
65
+ try {
66
+ const buf = fs.readFileSync(srcPath);
67
+ if (buf.includes(0)) {
68
+ throw new Error("binary content");
69
+ }
70
+ const code = buf.toString("utf8");
71
+ if (!code.trim()) {
72
+ throw new Error("empty content");
73
+ }
74
+
75
+ let out = "";
76
+ let used = parser;
77
+ let fallback = false;
78
+
79
+ // js-beautify is substantially faster for huge bundles; Prettier gives the
80
+ // best line-number fidelity for the normal case.
81
+ if (buf.length > 4 * 1024 * 1024) {
82
+ fallback = true;
83
+ out = beautifyFallback(code, parser);
84
+ } else {
85
+ try {
86
+ out = await prettier.format(code, {
87
+ parser,
88
+ printWidth: 120,
89
+ tabWidth: 2,
90
+ useTabs: false,
91
+ semi: true,
92
+ singleQuote: false,
93
+ trailingComma: "all",
94
+ bracketSpacing: true,
95
+ arrowParens: "always",
96
+ endOfLine: "lf",
97
+ });
98
+ used = parser;
99
+ } catch (error) {
100
+ fallback = true;
101
+ out = beautifyFallback(code, parser);
102
+ out = `${out}`;
103
+ if (!out.endsWith("\n")) out += "\n";
104
+ }
105
+ }
106
+
107
+ if (buf.length > 500 && !out.includes("\n")) {
108
+ fallback = true;
109
+ out = beautifyFallback(out, parser);
110
+ }
111
+ if (!out.endsWith("\n")) out += "\n";
112
+
113
+ const lines = out.split("\n").length - 1;
114
+ parentPort.postMessage({
115
+ id,
116
+ ok: true,
117
+ code: out,
118
+ parser: used,
119
+ fallback,
120
+ srcBytes: buf.length,
121
+ outBytes: Buffer.byteLength(out),
122
+ lines,
123
+ });
124
+ } catch (error) {
125
+ parentPort.postMessage({
126
+ id,
127
+ ok: false,
128
+ error: error instanceof Error ? error.message : String(error),
129
+ parser,
130
+ });
131
+ }
132
+ });