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,115 @@
1
+ # Tool & CLI reference
2
+
3
+ All MCP tools are also available as CLI commands returning the same JSON. Prefer MCP when it is
4
+ mounted; use the CLI for scripts, CI, or when MCP is not configured.
5
+
6
+ ## MCP tools
7
+
8
+ ### `lark_doc` — official documentation (zero config)
9
+
10
+ | Param | Notes |
11
+ | --- | --- |
12
+ | `action` | `search` \| `read` \| `categories` |
13
+ | `query` | title/path keyword for `search` |
14
+ | `category` | taxonomy key, e.g. `服务端 API_即时通讯` |
15
+ | `path` | document path returned by `search` |
16
+ | `offset` / `limit` | line window for large docs (`limit <= 400`) |
17
+
18
+ CLI: `larkit doc search 卡片`, `larkit doc read "<path>" --limit 80`, `larkit doc categories`.
19
+
20
+ ### `lark_truth_search` — local truth engine
21
+
22
+ | Param | Notes |
23
+ | --- | --- |
24
+ | `query` | keyword, API path, card field, error code, IPC channel, command name |
25
+ | `scope` | `all` \| `verified` \| `protocol` \| `web` \| `electron` |
26
+ | `limit` | 1–50, default 10 |
27
+
28
+ Returns compact hits with `kind`, coordinates (`file_path`, `line_number`, `line_start`), a
29
+ 320-char `context`/`snippet`, and protocol schema summaries. Always start with `scope=verified`.
30
+
31
+ CLI: `larkit search card_link --scope electron --limit 5`.
32
+
33
+ ### `lark_truth_inspect` — read code / persist knowledge
34
+
35
+ | Param | Notes |
36
+ | --- | --- |
37
+ | `action` | `read_slice` \| `save_verified` |
38
+ | `file_path` | path from search results (`formatted`-relative or `verified_truths/...`) |
39
+ | `line_number` | target line |
40
+ | `mode` | `context` \| `function` (function prefers the enclosing body) |
41
+ | `max_lines` | default 120, hard cap 200 |
42
+ | `topic`, `content`, `evidence`, `source`, `confidence`, `tags` | for `save_verified` |
43
+
44
+ CLI: `larkit inspect web/network/foo.js 120 --mode=function --max-lines=80`,
45
+ `larkit save "topic" --content "..." --evidence "..." --source probe`.
46
+
47
+ ### `lark_auth` — local Feishu identity
48
+
49
+ Actions: `whoami`, `check`, `list`, `use`, `cookie`, `login`, `login_status`, `import`, `logout`.
50
+
51
+ - `login` is detached: it returns a status file; `login_status` returns `qr_path` to show the user.
52
+ - `import` accepts a cookie string or a JSON file.
53
+ - Profiles live in `~/.larkit/credentials.json`; the active profile in `~/.larkit/config.json`.
54
+
55
+ CLI: `larkit auth check --profile geren`, `larkit auth login geren`, `larkit auth login_status geren`.
56
+
57
+ ### `lark_console` — developer console control
58
+
59
+ | Action | Purpose |
60
+ | --- | --- |
61
+ | `list_apps`, `get_app` | inventory |
62
+ | `create_app`, `update_app_info` | app lifecycle (`dry_run` supported) |
63
+ | `get_credentials` | app_secret (never persist in knowledge files) |
64
+ | `list_scopes`, `applied_scopes`, `grant_scopes` | permissions |
65
+ | `enable_bot_ws` | robot + event WS + card callback WS |
66
+ | `list_versions`, `publish` | versioning/release |
67
+ | `browser_fallback` | Chrome + CDP fallback, captures internal APIs |
68
+
69
+ Always `dry_run:true` first for writes. Full endpoint table: `console.md`.
70
+
71
+ CLI: `larkit console enable_bot_ws --app cli_xxx --profile geren --dry-run`.
72
+
73
+ ### `lark_probe` — real traffic
74
+
75
+ `mode="openapi"`:
76
+ `app_id`, `app_secret` (or env), `method`, `path`, `query`, `body`, `timeout`.
77
+ Automatically obtains/caches `tenant_access_token` (60s early expiry).
78
+
79
+ `mode="client"`:
80
+ `command` (e.g. `PUT_MESSAGE`), `payload`, `transport` (`ws` | `gateway`), `profile`,
81
+ `wait_push`, `timeout`. Uses the bundled protobuf schema so it works without the deb.
82
+
83
+ CLI: `larkit probe openapi --path=/im/v1/chats --app-id=... --app-secret=...`,
84
+ `larkit probe client --command=PUSH_MESSAGES --wait-push --timeout=8000`.
85
+
86
+ ## CLI bootstrap commands
87
+
88
+ ```bash
89
+ npx larkit # init on first run; status afterwards
90
+ larkit init # idempotent bootstrap
91
+ larkit status --json
92
+ larkit update # refresh outdated truth/skill/CLI
93
+ larkit skills list|install|remove [--target=agents,claude,project] [--project]
94
+ larkit pack build|verify|embed|path|stats
95
+ larkit doctor
96
+ larkit mcp # stdio MCP server
97
+ larkit mcp config # print the MCP client JSON
98
+ ```
99
+
100
+ ## Environment
101
+
102
+ | Variable | Purpose |
103
+ | --- | --- |
104
+ | `LARKIT_HOME` | override `~/.larkit` (state, CLI, skill records, downloads) |
105
+ | `LARKIT_DATA_DIR` | truth.db + formatted corpus location |
106
+ | `LARKIT_VERIFIED_DIR` | writable `verified_truths` directory |
107
+ | `LARKIT_PACK_MANIFEST` | default truth pack manifest URL/path |
108
+ | `LARKIT_DEB` / `LARKIT_WEB_DUMP` | inputs for `init --from-assets` |
109
+ | `LARKIT_BIN_DIR` | where the `larkit` shim is written (default `~/.local/bin`) |
110
+ | `FEISHU_APP_ID` / `FEISHU_APP_SECRET` | default probe credentials |
111
+ | `CHROME_PATH` | Chrome for `browser_fallback` |
112
+ | `LARKIT_PROFILE` | default auth profile |
113
+ | `LARKIT_GATEWAY_HEADERS` | extra client gateway headers (JSON) |
114
+
115
+ Config layers: `<data>/config.json` < `~/.larkit/config.json` < `./larkit.config.json`.
@@ -0,0 +1,117 @@
1
+ # Truth model
2
+
3
+ ## What the index contains
4
+
5
+ | Layer | Scope | Content |
6
+ | --- | --- | --- |
7
+ | `verified` | `verified_truths` | conclusions you wrote with `save_verified` (section-level FTS) |
8
+ | `protocol` | `protocol_catalog`, `protocol_docs` | 2020 client commands (cmd/name/request/response/kind) + `PROTOCOL.md`/endpoints notes |
9
+ | `web` | `code_anchors` | 531 deduplicated Web client resources (formatted, line-accurate) |
10
+ | `electron` | `code_anchors` | 8500+ desktop renderer/preload/JSSDK/worker files from `webcontent/*.asar` |
11
+
12
+ Index scale depends on the client version used to build the pack (current reference:
13
+ 2020 commands, 237,729 anchors, 12 verified sections, 17 protocol doc sections).
14
+
15
+ `code_anchors` cover: OpenAPI paths, URLs, IPC channels, `tt.*`/`h5sdk.*`/`LarkAPI.*`, card schema
16
+ keys, error codes, i18n keys, protocol constants, command names.
17
+
18
+ ## Coordinates and reading
19
+
20
+ - Search returns `file_path` **relative to the formatted corpus** (e.g.
21
+ `electron/asar/messenger/35983/f7adb73684.js`) plus `line_number`.
22
+ - Pass that path verbatim to `read_slice`; do not prepend anything.
23
+ - `verified_truths/<slug>.md` paths are also readable; sections carry `line_start`/`line_end`.
24
+ - `read_slice` is bounded (default 12 lines of context, `max_lines <= 120`). Reading a whole
25
+ formatted file is forbidden — the corpus is ~14M lines.
26
+
27
+ ## Writing knowledge (`save_verified`)
28
+
29
+ File format (one topic per file):
30
+
31
+ ```md
32
+ ---
33
+ topic: ...
34
+ verified_at: ...
35
+ source: probe | reverse | doc | console
36
+ confidence: high | medium | low
37
+ tags: [...]
38
+ ---
39
+
40
+ ## 结论
41
+ ...
42
+
43
+ ## 证据
44
+ - probe log_id / URL / file:line
45
+ ```
46
+
47
+ Rules:
48
+
49
+ - Only verified facts. Guesses stay in the conversation.
50
+ - Include a minimal reproduction (request/response/JSON snippet) so the conclusion is actionable.
51
+ - Never write credentials, tokens, or full auth headers.
52
+ - After writing, `lark_truth_search(scope="verified")` must hit immediately (SQLite incremental
53
+ sync). If sync fails, the Markdown is still saved and will be indexed on the next rebuild.
54
+
55
+ ## Storage layout
56
+
57
+ ```
58
+ ~/.larkit/
59
+ ├── data/
60
+ │ ├── truth.db # SQLite FTS5 index
61
+ │ ├── truth.meta.json # build stats
62
+ │ ├── formatted/ # optional code corpus (search works without it)
63
+ │ ├── cache/ # docs, tokens, screenshots, login logs
64
+ │ └── console-api-recipes.json
65
+ ├── verified_truths/ # user-owned Markdown knowledge
66
+ ├── cli/ # durable CLI install (npm prefix)
67
+ ├── downloads/ # cached truth/code pack archives
68
+ ├── install.json # bootstrap state + seed hashes
69
+ ├── mcp.json # MCP client config
70
+ ├── config.json # user-level config (profiles/apps)
71
+ └── credentials.json # auth profiles
72
+ ```
73
+
74
+ Engineers can override `LARKIT_HOME`, `LARKIT_DATA_DIR`, `LARKIT_VERIFIED_DIR`. A git checkout
75
+ continues to use `<repo>/.larkit-data` and `<repo>/verified_truths` for `pnpm index` / `pnpm verify`.
76
+
77
+ ## Installing / updating
78
+
79
+ ```bash
80
+ npx larkit # truth pack (truth.db + code corpus) + skill + CLI + MCP config
81
+ npx larkit update # refresh only outdated pieces (truth check runs on every command)
82
+ npx larkit status --json
83
+ ```
84
+
85
+ Truth pack sources, in priority order:
86
+
87
+ 1. `--pack=<manifest.json|dir|.tar.gz|https://...>`
88
+ 2. `--from-assets --deb=... --web-dump=...` (local build; Linux + `dpkg-deb`)
89
+ 3. `--channel[=<url>]` — the official channel, also the default
90
+ 4. `LARKIT_PACK_MANIFEST`
91
+
92
+ There is exactly one archive (`tiers.full`, ~161 MB): `truth.db` + verified seeds + `formatted/`.
93
+ Nothing is optional — installing always brings the code corpus with it.
94
+
95
+ Seed `verified_truths` files are content-hashed. On update they are replaced only if unmodified;
96
+ user edits are preserved.
97
+
98
+ ## Rebuilding (maintainers)
99
+
100
+ ```bash
101
+ pnpm index # full pipeline into .larkit-data
102
+ node scripts/build-index.mjs --only=protocol,db
103
+ node scripts/build-index.mjs --only=anchors,protocol,db
104
+ npx larkit pack build --out=dist/truth-pack --tag=2026.11.15 --base-url=https://host/path
105
+ node scripts/publish-pack.mjs --pack-dir=dist/truth-pack --base-url=https://host/path
106
+ npx larkit pack verify dist/truth-pack/manifest.json
107
+ ```
108
+
109
+ ## Boundaries
110
+
111
+ - Desktop main process is native (`feishu`/`libiron`); there is no `app.asar`. The corpus covers
112
+ renderer/preload/JSSDK/worker/protocol assets only.
113
+ - i18n bundles are excluded from formatting; query specific keys if needed.
114
+ - Official docs endpoints and console APIs are public but may rate-limit or change; caches and
115
+ `browser_fallback` recordings are the fallback.
116
+ - Private protocol routes and console internals drift with client versions. Record the version you
117
+ verified against.
@@ -0,0 +1,171 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import QRCode from "qrcode";
4
+ import { CACHE_DIR } from "../shared/paths.js";
5
+ import { ensureDir } from "../shared/util.js";
6
+ import { launchBrowser } from "../shared/browser.js";
7
+ import { saveProfile } from "./store.js";
8
+
9
+ const LOGIN_URL =
10
+ "https://accounts.feishu.cn/accounts/page/login?app_id=11&force_login=1&no_trap=1&redirect_uri=https%3A%2F%2Fwww.feishu.cn%2F";
11
+ const NAV_TIMEOUT = 45_000;
12
+ const TOKEN_TIMEOUT = 30_000;
13
+
14
+ function safeName(name) {
15
+ return String(name || "default").replace(/[^\p{L}\p{N}_-]+/gu, "_");
16
+ }
17
+
18
+ function writeStatus(file, data) {
19
+ if (!file) return;
20
+ ensureDir(path.dirname(file));
21
+ fs.writeFileSync(file, `${JSON.stringify(data, null, 2)}\n`);
22
+ }
23
+
24
+ export async function loginWithQr({
25
+ name = "default",
26
+ timeoutMs = 120_000,
27
+ headless = true,
28
+ statusFile,
29
+ } = {}) {
30
+ ensureDir(path.join(CACHE_DIR, "login"));
31
+ const base = `${safeName(name)}-${Date.now()}`;
32
+ const qrPath = path.join(CACHE_DIR, "login", `${base}.png`);
33
+ const screenshotPath = path.join(CACHE_DIR, "login", `${base}-page.png`);
34
+ const status = (phase, extra = {}) =>
35
+ writeStatus(statusFile, {
36
+ phase,
37
+ profile: name,
38
+ updated_at: new Date().toISOString(),
39
+ ...extra,
40
+ });
41
+
42
+ status("starting");
43
+ let browser;
44
+ let page;
45
+ try {
46
+ browser = await launchBrowser({ headless });
47
+ page = await browser.newPage();
48
+ status("loading", { login_url: LOGIN_URL });
49
+
50
+ const tokenPromise = new Promise((resolve, reject) => {
51
+ const timer = setTimeout(
52
+ () => reject(new Error("waiting for qrlogin/init timed out")),
53
+ TOKEN_TIMEOUT,
54
+ );
55
+ page.on("response", async (response) => {
56
+ if (!response.url().includes("/accounts/qrlogin/init")) return;
57
+ try {
58
+ const body = await response.json();
59
+ const token = body?.data?.step_info?.token;
60
+ if (token) {
61
+ clearTimeout(timer);
62
+ resolve(token);
63
+ }
64
+ } catch {
65
+ // ignore non-JSON responses
66
+ }
67
+ });
68
+ });
69
+
70
+ try {
71
+ await page.goto(LOGIN_URL, {
72
+ waitUntil: "networkidle2",
73
+ timeout: NAV_TIMEOUT,
74
+ });
75
+ } catch (error) {
76
+ if (!page.url().includes("accounts.feishu.cn")) throw error;
77
+ }
78
+
79
+ const token = await tokenPromise;
80
+ const qrData = JSON.stringify({ qrlogin: { token } });
81
+ await QRCode.toFile(qrPath, qrData, {
82
+ width: 640,
83
+ margin: 2,
84
+ errorCorrectionLevel: "M",
85
+ });
86
+ status("qr_ready", { qr_path: qrPath, final_url: page.url() });
87
+
88
+ const deadline = Date.now() + Math.max(5_000, Number(timeoutMs) || 120_000);
89
+ let lastCookies = [];
90
+ while (Date.now() < deadline) {
91
+ await new Promise((resolve) => setTimeout(resolve, 1500));
92
+ try {
93
+ lastCookies = await browser.cookies();
94
+ } catch {
95
+ break;
96
+ }
97
+ const session = lastCookies.find((cookie) => cookie.name === "session");
98
+ const device = lastCookies.find(
99
+ (cookie) => cookie.name === "passport_web_did",
100
+ );
101
+ if (session?.value && device?.value) {
102
+ const cookie = lastCookies
103
+ .filter((item) => item.domain.includes("feishu.cn"))
104
+ .map((item) => `${item.name}=${item.value}`)
105
+ .join("; ");
106
+ const saved = saveProfile({ name, cookie, active: true });
107
+ status("success", {
108
+ qr_path: qrPath,
109
+ final_url: page.url(),
110
+ cookies: saved.cookies,
111
+ });
112
+ return {
113
+ ok: true,
114
+ profile: name,
115
+ cookies: saved.cookies,
116
+ qr_path: qrPath,
117
+ final_url: page.url(),
118
+ };
119
+ }
120
+ }
121
+
122
+ try {
123
+ await page.screenshot({ path: screenshotPath, fullPage: false });
124
+ } catch {
125
+ // ignore
126
+ }
127
+ status("timeout", {
128
+ qr_path: qrPath,
129
+ screenshot_path: screenshotPath,
130
+ final_url: page.url(),
131
+ cookie_names: lastCookies.map((cookie) => cookie.name),
132
+ });
133
+ return {
134
+ ok: false,
135
+ profile: name,
136
+ error: "QR login timed out",
137
+ qr_path: qrPath,
138
+ screenshot_path: screenshotPath,
139
+ final_url: page.url(),
140
+ hint: "Open qr_path and scan with Feishu, then retry. Or use lark_auth action=import with a cookie string/file.",
141
+ };
142
+ } catch (error) {
143
+ try {
144
+ if (page) {
145
+ await page.screenshot({ path: screenshotPath, fullPage: false });
146
+ }
147
+ } catch {
148
+ // ignore
149
+ }
150
+ status("error", {
151
+ error: error instanceof Error ? error.message : String(error),
152
+ qr_path: fs.existsSync(qrPath) ? qrPath : null,
153
+ screenshot_path: fs.existsSync(screenshotPath) ? screenshotPath : null,
154
+ final_url: page?.url?.(),
155
+ });
156
+ return {
157
+ ok: false,
158
+ profile: name,
159
+ error: error instanceof Error ? error.message : String(error),
160
+ qr_path: fs.existsSync(qrPath) ? qrPath : null,
161
+ screenshot_path: fs.existsSync(screenshotPath) ? screenshotPath : null,
162
+ final_url: page?.url?.(),
163
+ };
164
+ } finally {
165
+ try {
166
+ await browser?.close();
167
+ } catch {
168
+ // ignore
169
+ }
170
+ }
171
+ }
@@ -0,0 +1,213 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { spawn } from "node:child_process";
4
+ import {
5
+ CACHE_DIR,
6
+ LOCAL_CONFIG_FILE,
7
+ ROOT,
8
+ } from "../shared/paths.js";
9
+ import { ensureDir, readJson, writeJson } from "../shared/util.js";
10
+ import { resolveConsoleSession } from "../console/client.js";
11
+ import {
12
+ authPaths,
13
+ getProfileCookies,
14
+ importProfiles,
15
+ listProfiles,
16
+ removeProfile,
17
+ resolveProfileName,
18
+ setActiveProfile,
19
+ } from "./store.js";
20
+ import { loginWithQr } from "./login.js";
21
+
22
+ function statusFileFor(name) {
23
+ const safe = String(name || "default").replace(/[^\p{L}\p{N}_-]+/gu, "_");
24
+ return path.join(CACHE_DIR, "login", `${safe}.status.json`);
25
+ }
26
+
27
+ export function authList() {
28
+ return {
29
+ ...listProfiles(),
30
+ home: authPaths().home,
31
+ credentials_file: authPaths().credentials,
32
+ };
33
+ }
34
+
35
+ export async function whoami({ profile } = {}) {
36
+ const name = resolveProfileName(profile);
37
+ if (!name) {
38
+ return { ok: false, error: "no larkit profiles; run lark_auth action=login or import" };
39
+ }
40
+ getProfileCookies(name);
41
+ try {
42
+ const session = await resolveConsoleSession(name, { force: true });
43
+ return {
44
+ ok: session.valid,
45
+ profile: name,
46
+ user: session.user,
47
+ console_logged_in: session.valid,
48
+ hint: session.valid
49
+ ? undefined
50
+ : "cookie expired; run lark_auth action=login or import a fresh cookie",
51
+ };
52
+ } catch (error) {
53
+ return {
54
+ ok: false,
55
+ profile: name,
56
+ error: error instanceof Error ? error.message : String(error),
57
+ };
58
+ }
59
+ }
60
+
61
+ export async function checkAuth({ profile } = {}) {
62
+ const name = resolveProfileName(profile);
63
+ if (!name) {
64
+ return { ok: false, error: "no larkit profiles; run lark_auth action=login or import" };
65
+ }
66
+ try {
67
+ const session = await resolveConsoleSession(name, { force: true });
68
+ return {
69
+ ok: session.valid,
70
+ profile: name,
71
+ user: session.user,
72
+ console_logged_in: session.valid,
73
+ page_status: session.page_status,
74
+ csrf_present: Boolean(session.csrf),
75
+ hint: session.valid
76
+ ? undefined
77
+ : "cookie expired; run lark_auth action=login or import a fresh cookie",
78
+ };
79
+ } catch (error) {
80
+ return {
81
+ ok: false,
82
+ profile: name,
83
+ error: error instanceof Error ? error.message : String(error),
84
+ };
85
+ }
86
+ }
87
+
88
+ export function useProfile(name) {
89
+ if (!name) throw new Error("profile name is required");
90
+ const result = setActiveProfile(name);
91
+ const local = readJson(LOCAL_CONFIG_FILE, {});
92
+ writeJson(LOCAL_CONFIG_FILE, { ...local, defaultProfile: name });
93
+ return { ...result, default_profile: name };
94
+ }
95
+
96
+ export function cookieFor(profile) {
97
+ const { name, cookies } = getProfileCookies(profile);
98
+ return { ok: true, profile: name, cookie: cookies, length: cookies.length };
99
+ }
100
+
101
+ export function startLogin(
102
+ name = "default",
103
+ { timeoutMs = 120_000, headless = true, wait = false } = {},
104
+ ) {
105
+ ensureDir(path.join(CACHE_DIR, "login"));
106
+ const statusFile = statusFileFor(name);
107
+ if (wait) {
108
+ const result = loginWithQr({ name, timeoutMs, headless, statusFile });
109
+ return result;
110
+ }
111
+ const logPath = path.join(CACHE_DIR, "login", `${name.replace(/[^\w.-]+/g, "_")}.log`);
112
+ const fd = fs.openSync(logPath, "w");
113
+ const child = spawn(
114
+ process.execPath,
115
+ [
116
+ path.join(ROOT, "scripts", "login-worker.mjs"),
117
+ "--name",
118
+ name,
119
+ "--timeout",
120
+ String(timeoutMs),
121
+ "--status",
122
+ statusFile,
123
+ headless ? "--headless" : "--headful",
124
+ ],
125
+ {
126
+ detached: true,
127
+ stdio: ["ignore", fd, fd],
128
+ cwd: ROOT,
129
+ env: { ...process.env },
130
+ },
131
+ );
132
+ child.unref();
133
+ fs.closeSync(fd);
134
+ return {
135
+ ok: true,
136
+ started: true,
137
+ profile: name,
138
+ pid: child.pid,
139
+ status_file: statusFile,
140
+ log_path: logPath,
141
+ qr_path: path.join(CACHE_DIR, "login", "*.png"),
142
+ hint:
143
+ "Login worker started. Wait a few seconds, read status_file for qr_path, scan the PNG with Feishu, then call lark_auth action=check.",
144
+ };
145
+ }
146
+
147
+ export function loginStatus(name = "default") {
148
+ const statusFile = statusFileFor(name);
149
+ if (!fs.existsSync(statusFile)) {
150
+ return { ok: false, profile: name, status_file: statusFile, error: "no login status yet" };
151
+ }
152
+ return { ok: true, profile: name, status_file: statusFile, ...readJson(statusFile, {}) };
153
+ }
154
+
155
+ export function logout(name) {
156
+ if (!name) throw new Error("profile name is required");
157
+ return removeProfile(name);
158
+ }
159
+
160
+ export function importAuth({ file, cookie, name = "default", active = true } = {}) {
161
+ let input = cookie;
162
+ if (file) {
163
+ const text = fs.readFileSync(path.resolve(file), "utf8");
164
+ try {
165
+ input = JSON.parse(text);
166
+ } catch {
167
+ input = text;
168
+ }
169
+ }
170
+ if (!input) throw new Error("cookie or file is required");
171
+ return importProfiles(input, { active });
172
+ }
173
+
174
+ export function authDispatch({
175
+ action,
176
+ profile,
177
+ name,
178
+ browser,
179
+ file,
180
+ cookie,
181
+ wait,
182
+ timeout_ms: timeoutMs,
183
+ headless,
184
+ } = {}) {
185
+ switch (action) {
186
+ case "list":
187
+ return authList();
188
+ case "whoami":
189
+ return whoami({ profile });
190
+ case "check":
191
+ return checkAuth({ profile });
192
+ case "use":
193
+ return useProfile(name || profile);
194
+ case "cookie":
195
+ return cookieFor(profile);
196
+ case "login":
197
+ return startLogin(name || profile || "default", {
198
+ timeoutMs,
199
+ wait,
200
+ headless: headless !== false && browser !== "headful",
201
+ });
202
+ case "login_status":
203
+ return loginStatus(name || profile || "default");
204
+ case "import":
205
+ return importAuth({ file, cookie, name: name || profile || "default" });
206
+ case "logout":
207
+ return logout(name || profile);
208
+ default:
209
+ throw new Error(
210
+ `unknown auth action: ${action}; expected list|whoami|check|use|cookie|login|login_status|import|logout`,
211
+ );
212
+ }
213
+ }