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,101 @@
1
+ import {
2
+ appliedScopes,
3
+ createApp,
4
+ enableBotWs,
5
+ getApp,
6
+ getCredentials,
7
+ grantScopes,
8
+ listApps,
9
+ listScopes,
10
+ listVersions,
11
+ publishVersion,
12
+ updateAppInfo,
13
+ } from "./client.js";
14
+ import { browserFallback } from "./browser.js";
15
+
16
+ export async function consoleDispatch(args = {}) {
17
+ const action = args.action;
18
+ const profile = args.profile;
19
+ switch (action) {
20
+ case "list_apps":
21
+ return listApps({
22
+ profile,
23
+ limit: args.limit,
24
+ query: args.query,
25
+ });
26
+ case "create_app":
27
+ return createApp({
28
+ name: args.name,
29
+ desc: args.desc,
30
+ avatar: args.avatar,
31
+ appSceneType: args.app_scene_type,
32
+ primaryLang: args.primary_lang,
33
+ profile,
34
+ dryRun: args.dry_run,
35
+ });
36
+ case "get_app":
37
+ return getApp({ appId: args.app_id, profile });
38
+ case "get_credentials":
39
+ return getCredentials({ appId: args.app_id, profile });
40
+ case "update_app_info":
41
+ return updateAppInfo({
42
+ appId: args.app_id,
43
+ name: args.name,
44
+ desc: args.desc,
45
+ avatar: args.avatar,
46
+ i18n: args.i18n,
47
+ languages: args.languages,
48
+ profile,
49
+ dryRun: args.dry_run,
50
+ });
51
+ case "list_scopes":
52
+ return listScopes({ appId: args.app_id, profile });
53
+ case "applied_scopes":
54
+ return appliedScopes({ appId: args.app_id, profile });
55
+ case "grant_scopes":
56
+ return grantScopes({
57
+ appId: args.app_id,
58
+ scopes: args.scopes,
59
+ scopeType: args.scope_type,
60
+ operation: args.operation,
61
+ profile,
62
+ dryRun: args.dry_run,
63
+ });
64
+ case "list_versions":
65
+ return listVersions({ appId: args.app_id, profile });
66
+ case "enable_bot_ws":
67
+ return enableBotWs({
68
+ appId: args.app_id,
69
+ enableBot: args.enable_bot,
70
+ enableEvents: args.enable_events,
71
+ enableCallbacks: args.enable_callbacks,
72
+ enableCardRequestMode: args.enable_card_request_mode,
73
+ profile,
74
+ dryRun: args.dry_run,
75
+ });
76
+ case "publish":
77
+ return publishVersion({
78
+ appId: args.app_id,
79
+ version: args.version,
80
+ remark: args.remark,
81
+ fullRelease: args.full_release,
82
+ createIfMissing: args.create_if_missing,
83
+ release: args.release,
84
+ profile,
85
+ dryRun: args.dry_run,
86
+ });
87
+ case "browser_fallback":
88
+ return browserFallback({
89
+ profile,
90
+ url: args.url,
91
+ waitMs: args.wait_ms,
92
+ actions: args.actions,
93
+ screenshot: args.screenshot,
94
+ recordAs: args.record_as,
95
+ });
96
+ default:
97
+ throw new Error(
98
+ `unknown console action: ${action}; expected list_apps|create_app|get_app|get_credentials|update_app_info|list_scopes|applied_scopes|grant_scopes|list_versions|enable_bot_ws|publish|browser_fallback`,
99
+ );
100
+ }
101
+ }
@@ -0,0 +1,284 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import crypto from "node:crypto";
4
+ import { CACHE_DIR, displayPath } from "../shared/paths.js";
5
+ import { ensureDir, readJson, writeJson } from "../shared/util.js";
6
+
7
+ const DIRECTORY_LIST_URL =
8
+ "https://open.feishu.cn/api/tools/docment/directory_list";
9
+ const URI_MAPPING_URL =
10
+ "https://open.feishu.cn/document_portal/v1/document_portal/v1/document/uri/mapping";
11
+ const DOC_DETAIL_URL =
12
+ "https://open.feishu.cn/document_portal/v1/document/get_detail";
13
+ const TREE_CACHE = path.join(CACHE_DIR, "docs-tree.json");
14
+ const DOC_CACHE_DIR = path.join(CACHE_DIR, "docs");
15
+
16
+ let state = null;
17
+
18
+ function fetchJson(url, timeout = 45_000) {
19
+ return fetch(url, { signal: AbortSignal.timeout(timeout) }).then(async (response) => {
20
+ if (!response.ok) {
21
+ throw new Error(`HTTP ${response.status} for ${url}`);
22
+ }
23
+ return response.json();
24
+ });
25
+ }
26
+
27
+ function walk(nodes, category, out, uriMap) {
28
+ for (const node of nodes || []) {
29
+ if (node.type === "DocumentType") {
30
+ out.push({
31
+ title: node.name,
32
+ full_path: node.fullPath,
33
+ path: uriMap[node.fullPath] || node.fullPath,
34
+ category,
35
+ });
36
+ continue;
37
+ }
38
+ if (node.type === "DirectoryType") {
39
+ walk(node.items, category, out, uriMap);
40
+ }
41
+ }
42
+ }
43
+
44
+ async function loadState({ force = false } = {}) {
45
+ if (state && !force) return state;
46
+ const cached = readJson(TREE_CACHE, null);
47
+ if (
48
+ !force &&
49
+ cached?.fetched_at &&
50
+ Date.now() - cached.fetched_at < 24 * 60 * 60 * 1000 &&
51
+ cached.tree &&
52
+ cached.uri_map
53
+ ) {
54
+ state = buildState(cached.tree, cached.uri_map, cached.fetched_at);
55
+ return state;
56
+ }
57
+ try {
58
+ const [treeResponse, mappingResponse] = await Promise.all([
59
+ fetchJson(DIRECTORY_LIST_URL),
60
+ fetchJson(URI_MAPPING_URL),
61
+ ]);
62
+ if (treeResponse.code !== 0) {
63
+ throw new Error(`directory_list: ${treeResponse.msg || treeResponse.code}`);
64
+ }
65
+ if (mappingResponse.code !== 0) {
66
+ throw new Error(`uri mapping: ${mappingResponse.msg || mappingResponse.code}`);
67
+ }
68
+ const tree = treeResponse.data.items;
69
+ const uriMap = mappingResponse.data.uriMap || {};
70
+ writeJson(TREE_CACHE, {
71
+ fetched_at: Date.now(),
72
+ tree,
73
+ uri_map: uriMap,
74
+ });
75
+ state = buildState(tree, uriMap, Date.now());
76
+ return state;
77
+ } catch (error) {
78
+ if (cached?.tree && cached?.uri_map) {
79
+ state = buildState(cached.tree, cached.uri_map, cached.fetched_at);
80
+ state.stale = true;
81
+ state.warning = error.message;
82
+ return state;
83
+ }
84
+ throw error;
85
+ }
86
+ }
87
+
88
+ function buildState(tree, uriMap, fetchedAt) {
89
+ const categories = [];
90
+ const docs = [];
91
+ for (const level1 of tree || []) {
92
+ if (level1.type !== "DirectoryType") continue;
93
+ for (const level2 of level1.items || []) {
94
+ if (level2.type !== "DirectoryType") continue;
95
+ const category = `${level1.name}_${level2.name}`;
96
+ const before = docs.length;
97
+ walk([level2], category, docs, uriMap);
98
+ categories.push({
99
+ category,
100
+ level1: level1.name,
101
+ level2: level2.name,
102
+ docs: docs.length - before,
103
+ });
104
+ }
105
+ }
106
+ const byPath = new Map(docs.map((doc) => [doc.path, doc]));
107
+ const byFullPath = new Map(docs.map((doc) => [doc.full_path, doc]));
108
+ return {
109
+ fetchedAt,
110
+ categories,
111
+ docs,
112
+ byPath,
113
+ byFullPath,
114
+ uriMap,
115
+ tree,
116
+ };
117
+ }
118
+
119
+ function cleanMarkdown(content) {
120
+ return String(content || "")
121
+ .replace(/:::html\n?/g, "")
122
+ .replace(/:::\n?/g, "")
123
+ .replace(/<md-[a-z0-9-]+[^>]*>/g, "")
124
+ .replace(/<\/md-[a-z0-9-]+>/g, "")
125
+ .replace(/<\/?div[^>]*>/g, "")
126
+ .replace(/<\/?tr[^>]*>/g, "")
127
+ .replace(/<\/?font[^>]*>/g, "")
128
+ .replace(/\n{3,}/g, "\n\n")
129
+ .trim();
130
+ }
131
+
132
+ function headingsOf(content) {
133
+ const headings = [];
134
+ const lines = content.split("\n");
135
+ for (let i = 0; i < lines.length; i++) {
136
+ const match = lines[i].match(/^(#{1,4})\s+(.*)$/);
137
+ if (match) {
138
+ headings.push({ level: match[1].length, title: match[2].trim(), line: i + 1 });
139
+ }
140
+ }
141
+ return headings.slice(0, 60);
142
+ }
143
+
144
+ export async function searchDocs({ query = "", category = "", limit = 30 } = {}) {
145
+ const data = await loadState();
146
+ const safeLimit = Math.max(1, Math.min(Number(limit) || 30, 200));
147
+ if (!query && !category) {
148
+ return {
149
+ ok: true,
150
+ kind: "categories",
151
+ total_categories: data.categories.length,
152
+ total_docs: data.docs.length,
153
+ warning: data.warning,
154
+ categories: data.categories.slice(0, 120),
155
+ hint: "Pass category (一级目录_二级目录) to list a category, or query to search titles/paths.",
156
+ };
157
+ }
158
+ let candidates = data.docs;
159
+ let resolvedCategory = null;
160
+ if (category) {
161
+ const exact = data.categories.find((item) => item.category === category);
162
+ const lower = category.toLowerCase();
163
+ const fuzzy =
164
+ exact ||
165
+ data.categories.find((item) => item.category.toLowerCase() === lower) ||
166
+ data.categories.find((item) => item.category.toLowerCase().includes(lower)) ||
167
+ data.categories.find((item) =>
168
+ item.category.toLowerCase().replace(/\s+/g, "").includes(lower.replace(/\s+/g, "")),
169
+ );
170
+ if (!fuzzy) {
171
+ return {
172
+ ok: false,
173
+ error: `unknown category: ${category}`,
174
+ suggestions: data.categories
175
+ .filter((item) => item.category.toLowerCase().includes(lower.slice(0, 3)))
176
+ .slice(0, 20),
177
+ hint: "Use a category from suggestions, or pass only query to search all docs.",
178
+ };
179
+ }
180
+ resolvedCategory = fuzzy.category;
181
+ candidates = candidates.filter((doc) => doc.category === resolvedCategory);
182
+ }
183
+ if (query) {
184
+ const q = query.toLowerCase();
185
+ candidates = candidates.filter(
186
+ (doc) =>
187
+ doc.title.toLowerCase().includes(q) ||
188
+ doc.path.toLowerCase().includes(q) ||
189
+ doc.full_path.toLowerCase().includes(q),
190
+ );
191
+ }
192
+ const total = candidates.length;
193
+ return {
194
+ ok: true,
195
+ query: query || undefined,
196
+ category: resolvedCategory || undefined,
197
+ total,
198
+ returned: Math.min(total, safeLimit),
199
+ docs: candidates.slice(0, safeLimit).map((doc) => ({
200
+ title: doc.title,
201
+ path: doc.path,
202
+ category: doc.category,
203
+ })),
204
+ warning: data.warning,
205
+ hint:
206
+ total > safeLimit
207
+ ? "Narrow query/category or raise limit. Use lark_doc action=read with a path."
208
+ : undefined,
209
+ };
210
+ }
211
+
212
+ export async function readDoc({ docPath, offset = 0, limit = 0 } = {}) {
213
+ if (!docPath) throw new Error("path is required");
214
+ const data = await loadState();
215
+ const requested = String(docPath).trim();
216
+ const mapped = data.uriMap[requested] || requested;
217
+ const known = data.byPath.get(mapped) || data.byFullPath.get(requested);
218
+ const requestPath = known?.path || mapped;
219
+ const url = new URL(DOC_DETAIL_URL);
220
+ url.searchParams.set("fullPath", requestPath);
221
+ const response = await fetch(url, {
222
+ signal: AbortSignal.timeout(45_000),
223
+ });
224
+ if (!response.ok) throw new Error(`HTTP ${response.status} reading ${requestPath}`);
225
+ const payload = await response.json();
226
+ if (payload.code !== 0) {
227
+ throw new Error(`document API ${payload.code}: ${payload.msg || "unknown error"}`);
228
+ }
229
+ const detail = payload.data || {};
230
+ const title = detail.title || detail.name || known?.title || "";
231
+ const content = cleanMarkdown(
232
+ detail.content || detail.body || JSON.stringify(detail, null, 2),
233
+ );
234
+ ensureDir(DOC_CACHE_DIR);
235
+ const hash = crypto.createHash("md5").update(requestPath).digest("hex").slice(0, 12);
236
+ const safeName = (known?.title || title || "doc")
237
+ .replace(/[^\p{L}\p{N}_-]+/gu, "_")
238
+ .slice(0, 60);
239
+ const filePath = path.join(DOC_CACHE_DIR, `${safeName}_${hash}.md`);
240
+ const text = `# ${title}\n\n${content}\n`;
241
+ fs.writeFileSync(filePath, text);
242
+ const lines = text.split("\n");
243
+ const start = Math.max(0, Number(offset) || 0);
244
+ const requestedLimit = Number(limit) || 0;
245
+ const shouldSlice = requestedLimit > 0 || text.length > 5000;
246
+ const sliceLimit = requestedLimit > 0 ? Math.min(requestedLimit, 400) : 120;
247
+ const end = shouldSlice ? Math.min(lines.length, start + sliceLimit) : lines.length;
248
+ return {
249
+ ok: true,
250
+ title,
251
+ path: requestPath,
252
+ file_path: displayPath(filePath),
253
+ bytes: Buffer.byteLength(text),
254
+ total_lines: lines.length,
255
+ offset: start,
256
+ limit: shouldSlice ? sliceLimit : lines.length,
257
+ headings: headingsOf(text),
258
+ content: lines.slice(start, end).join("\n"),
259
+ truncated: end < lines.length,
260
+ hint:
261
+ end < lines.length
262
+ ? `Large document: read again with offset=${end} limit=${sliceLimit}, or grep file_path.`
263
+ : undefined,
264
+ };
265
+ }
266
+
267
+ export async function docCategories() {
268
+ const data = await loadState();
269
+ return {
270
+ ok: true,
271
+ total_categories: data.categories.length,
272
+ total_docs: data.docs.length,
273
+ categories: data.categories,
274
+ };
275
+ }
276
+
277
+ export function resetDocCache() {
278
+ state = null;
279
+ try {
280
+ fs.rmSync(TREE_CACHE, { force: true });
281
+ } catch {
282
+ // ignore
283
+ }
284
+ }