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,213 @@
1
+ import fs from "node:fs";
2
+ import {
3
+ AUTH_CONFIG_FILE,
4
+ AUTH_CREDENTIALS_FILE,
5
+ LARKIT_HOME,
6
+ } from "../shared/paths.js";
7
+ import { loadConfig } from "../shared/config.js";
8
+ import { ensureDir, readJson, writeJson } from "../shared/util.js";
9
+
10
+ function ensureHome() {
11
+ ensureDir(LARKIT_HOME);
12
+ try {
13
+ fs.chmodSync(LARKIT_HOME, 0o700);
14
+ } catch {
15
+ // best effort
16
+ }
17
+ }
18
+
19
+ function readCredentials() {
20
+ ensureHome();
21
+ return readJson(AUTH_CREDENTIALS_FILE, { profiles: {} });
22
+ }
23
+
24
+ function writeCredentials(credentials) {
25
+ ensureHome();
26
+ writeJson(AUTH_CREDENTIALS_FILE, credentials);
27
+ try {
28
+ fs.chmodSync(AUTH_CREDENTIALS_FILE, 0o600);
29
+ } catch {
30
+ // best effort
31
+ }
32
+ }
33
+
34
+ function readAuthConfig() {
35
+ ensureHome();
36
+ return readJson(AUTH_CONFIG_FILE, { activeProfile: null });
37
+ }
38
+
39
+ function writeAuthConfig(config) {
40
+ ensureHome();
41
+ // Preserve user-level keys (defaultProfile, apps, console) that share this
42
+ // file; auth only owns `activeProfile`.
43
+ const current = readJson(AUTH_CONFIG_FILE, {});
44
+ writeJson(AUTH_CONFIG_FILE, { ...current, ...config });
45
+ try {
46
+ fs.chmodSync(AUTH_CONFIG_FILE, 0o600);
47
+ } catch {
48
+ // best effort
49
+ }
50
+ }
51
+
52
+ export function listProfiles() {
53
+ const credentials = readCredentials();
54
+ const authConfig = readAuthConfig();
55
+ const project = loadConfig();
56
+ return {
57
+ active_profile: authConfig.activeProfile || null,
58
+ larkit_default_profile:
59
+ project.defaultProfile || project.console?.profile || null,
60
+ profiles: Object.entries(credentials.profiles || {}).map(([name, profile]) => ({
61
+ name,
62
+ active: name === authConfig.activeProfile,
63
+ cookies: String(profile.cookies || "").split(";").filter(Boolean).length,
64
+ created_at: profile.createdAt || null,
65
+ updated_at: profile.updatedAt || null,
66
+ })),
67
+ };
68
+ }
69
+
70
+ export function resolveProfileName(profile) {
71
+ const credentials = readCredentials();
72
+ const authConfig = readAuthConfig();
73
+ const project = loadConfig();
74
+ const preferred =
75
+ profile ||
76
+ process.env.LARKIT_PROFILE ||
77
+ project.defaultProfile ||
78
+ project.console?.profile;
79
+ if (preferred && credentials.profiles?.[preferred]) return preferred;
80
+ if (preferred && profile) {
81
+ throw new Error(
82
+ `unknown larkit profile "${preferred}"; run "larkit auth list"`,
83
+ );
84
+ }
85
+ return (
86
+ authConfig.activeProfile ||
87
+ Object.keys(credentials.profiles || {})[0] ||
88
+ null
89
+ );
90
+ }
91
+
92
+ export function getProfileCookies(profile) {
93
+ const credentials = readCredentials();
94
+ const name = resolveProfileName(profile);
95
+ const entry = name ? credentials.profiles?.[name] : null;
96
+ if (!entry?.cookies) {
97
+ throw new Error(
98
+ `no larkit profile "${name || "<none>"}"; run "larkit auth login <name>" or "larkit auth import --file <cookie.json>"`,
99
+ );
100
+ }
101
+ return { name, cookies: entry.cookies, profile: entry };
102
+ }
103
+
104
+ export function saveProfile({ name, cookie, active = false } = {}) {
105
+ if (!name) throw new Error("profile name is required");
106
+ if (!cookie) throw new Error("cookie is required");
107
+ const credentials = readCredentials();
108
+ const now = new Date().toISOString();
109
+ const existing = credentials.profiles?.[name];
110
+ credentials.profiles = credentials.profiles || {};
111
+ credentials.profiles[name] = {
112
+ name,
113
+ cookies: String(cookie).trim(),
114
+ createdAt: existing?.createdAt || now,
115
+ updatedAt: now,
116
+ };
117
+ writeCredentials(credentials);
118
+ const authConfig = readAuthConfig();
119
+ if (active || !authConfig.activeProfile) {
120
+ authConfig.activeProfile = name;
121
+ writeAuthConfig(authConfig);
122
+ }
123
+ return {
124
+ ok: true,
125
+ profile: name,
126
+ active_profile: authConfig.activeProfile,
127
+ cookies: String(cookie).split(";").filter(Boolean).length,
128
+ };
129
+ }
130
+
131
+ export function setActiveProfile(name) {
132
+ const credentials = readCredentials();
133
+ if (!credentials.profiles?.[name]) throw new Error(`unknown profile: ${name}`);
134
+ const authConfig = readAuthConfig();
135
+ authConfig.activeProfile = name;
136
+ writeAuthConfig(authConfig);
137
+ return { ok: true, active_profile: name };
138
+ }
139
+
140
+ export function removeProfile(name) {
141
+ const credentials = readCredentials();
142
+ if (!credentials.profiles?.[name]) return { ok: false, error: "profile not found" };
143
+ delete credentials.profiles[name];
144
+ writeCredentials(credentials);
145
+ const authConfig = readAuthConfig();
146
+ if (authConfig.activeProfile === name) {
147
+ authConfig.activeProfile = Object.keys(credentials.profiles)[0] || null;
148
+ writeAuthConfig(authConfig);
149
+ }
150
+ return { ok: true, profile: name, active_profile: authConfig.activeProfile };
151
+ }
152
+
153
+ export function importProfiles(input, { active = false } = {}) {
154
+ const credentials = readCredentials();
155
+ const now = new Date().toISOString();
156
+ let imported = 0;
157
+ if (input && typeof input === "object" && input.profiles) {
158
+ for (const [name, value] of Object.entries(input.profiles)) {
159
+ const cookie = value?.cookies || value?.cookie;
160
+ if (!cookie) continue;
161
+ credentials.profiles[name] = {
162
+ name,
163
+ cookies: String(cookie).trim(),
164
+ createdAt: value.createdAt || now,
165
+ updatedAt: now,
166
+ };
167
+ imported++;
168
+ }
169
+ } else if (input && typeof input === "object" && input.cookie) {
170
+ const name = input.name || "default";
171
+ credentials.profiles[name] = {
172
+ name,
173
+ cookies: String(input.cookie).trim(),
174
+ createdAt: now,
175
+ updatedAt: now,
176
+ };
177
+ imported++;
178
+ } else if (typeof input === "string" && input.includes("=")) {
179
+ const name = "default";
180
+ credentials.profiles[name] = {
181
+ name,
182
+ cookies: input.trim(),
183
+ createdAt: now,
184
+ updatedAt: now,
185
+ };
186
+ imported++;
187
+ }
188
+ if (!imported) throw new Error("no profiles/cookies found to import");
189
+ writeCredentials(credentials);
190
+ const authConfig = readAuthConfig();
191
+ if (active || !authConfig.activeProfile) {
192
+ authConfig.activeProfile = Object.keys(credentials.profiles)[0];
193
+ writeAuthConfig(authConfig);
194
+ }
195
+ return {
196
+ ok: true,
197
+ imported,
198
+ active_profile: authConfig.activeProfile,
199
+ profiles: listProfiles().profiles,
200
+ };
201
+ }
202
+
203
+ export function readRawCredentials() {
204
+ return readCredentials();
205
+ }
206
+
207
+ export function authPaths() {
208
+ return {
209
+ home: LARKIT_HOME,
210
+ credentials: AUTH_CREDENTIALS_FILE,
211
+ config: AUTH_CONFIG_FILE,
212
+ };
213
+ }
@@ -0,0 +1,201 @@
1
+ import fs from "node:fs";
2
+ import { FORMATTED_DIR, TRUTH_DB } from "../shared/paths.js";
3
+ import {
4
+ DEFAULT_TRUTH_CHANNEL,
5
+ TIER_NAME,
6
+ installTruthPack,
7
+ isChannelSource,
8
+ loadPackManifest,
9
+ packageVersion,
10
+ } from "./pack.js";
11
+ import { initialState, readState, writeState } from "./state.js";
12
+
13
+ export { DEFAULT_TRUTH_CHANNEL };
14
+
15
+ /** How often a normal command is allowed to look for a newer truth pack. */
16
+ export const AUTO_UPDATE_TTL_MS = 6 * 60 * 60 * 1000;
17
+
18
+ /** sha256 of the single unified archive that carries truth + code corpus. */
19
+ export function packShaOf(manifest) {
20
+ return manifest?.tiers?.[TIER_NAME]?.sha256 || null;
21
+ }
22
+
23
+ export function truthTagOf(manifest) {
24
+ return manifest?.packVersion || manifest?.larkitVersion || null;
25
+ }
26
+
27
+ /**
28
+ * Resolve where the truth source comes from.
29
+ *
30
+ * --pack=<dir|url> explicit source (remembered when it is a URL)
31
+ * LARKIT_PACK_MANIFEST same, via env
32
+ * --channel[=<url>] opt in to the official/custom truth channel
33
+ * remembered channel state.steps.truth.channel (set by a previous run)
34
+ * otherwise the official channel
35
+ *
36
+ * There is no bundled pack and no core/code split: every install downloads the
37
+ * same unified archive.
38
+ */
39
+ export function resolveTruthChannel(state, options = {}) {
40
+ const explicit = options.pack || process.env.LARKIT_PACK_MANIFEST || null;
41
+ const remembered = state?.steps?.truth?.channel || null;
42
+ const flag = options.channel;
43
+ const requested = flag
44
+ ? flag === true || flag === "default" || flag === "latest"
45
+ ? DEFAULT_TRUTH_CHANNEL
46
+ : String(flag)
47
+ : null;
48
+ const channel = isChannelSource(explicit) ? explicit : requested || remembered;
49
+ return { explicit, remembered, channel, requested };
50
+ }
51
+
52
+ /** The channel a plain command should consult. */
53
+ export function activeChannel(state) {
54
+ return state?.steps?.truth?.channel || DEFAULT_TRUTH_CHANNEL;
55
+ }
56
+
57
+ async function fetchManifest(source, { timeoutMs } = {}) {
58
+ if (!timeoutMs) return (await loadPackManifest(source)).manifest;
59
+ const controller = new AbortController();
60
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
61
+ try {
62
+ const response = await fetch(source, {
63
+ headers: { "user-agent": `larkit/${packageVersion()}` },
64
+ redirect: "follow",
65
+ signal: controller.signal,
66
+ });
67
+ if (!response.ok) throw new Error(`HTTP ${response.status} for ${source}`);
68
+ return await response.json();
69
+ } finally {
70
+ clearTimeout(timer);
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Compare the installed truth pack with the channel without changing anything.
76
+ * Backs `larkit status --check`.
77
+ */
78
+ export async function checkTruthChannel({ channel } = {}) {
79
+ const state = readState() || initialState();
80
+ const target = channel
81
+ ? channel === "default"
82
+ ? DEFAULT_TRUTH_CHANNEL
83
+ : String(channel)
84
+ : activeChannel(state);
85
+ const manifest = await fetchManifest(target);
86
+ const latestSha = packShaOf(manifest);
87
+ const currentSha = state.steps?.truth?.packSha || null;
88
+ const tier = manifest.tiers?.[TIER_NAME] || null;
89
+ return {
90
+ channel: target,
91
+ checked: true,
92
+ current: state.steps?.truth?.packVersion || null,
93
+ currentSha,
94
+ latest: truthTagOf(manifest),
95
+ latestSha,
96
+ clientVersion: manifest.clientVersion || null,
97
+ archive: tier ? { bytes: tier.bytes, sha256: tier.sha256 } : null,
98
+ upToDate: Boolean(latestSha && currentSha && latestSha === currentSha),
99
+ };
100
+ }
101
+
102
+ export function autoUpdateDisabled(env = process.env) {
103
+ if (env.CI) return true;
104
+ const flag = env.LARKIT_NO_AUTO_UPDATE;
105
+ return Boolean(flag && flag !== "0" && flag !== "false");
106
+ }
107
+
108
+ /** Persist a check result without clobbering the rest of the install state. */
109
+ export function recordTruth(state, patch) {
110
+ const next = { ...(readState() || state) };
111
+ next.steps = {
112
+ ...(next.steps || {}),
113
+ truth: { ...(next.steps?.truth || {}), ...patch },
114
+ };
115
+ writeState(next);
116
+ return next;
117
+ }
118
+
119
+ /**
120
+ * Startup hook: read the channel, and if the published pack is newer than the
121
+ * installed one, install it before the command runs.
122
+ *
123
+ * Guardrails: no-op in CI, with LARKIT_NO_AUTO_UPDATE=1, or within the TTL
124
+ * window; the lookup itself is time-boxed, and any failure only produces one
125
+ * warning — the command always runs with whatever truth exists.
126
+ */
127
+ export async function maybeAutoUpdate({ reporter, force = false, timeoutMs = 2500 } = {}) {
128
+ if (autoUpdateDisabled()) return { status: "disabled" };
129
+ const state = readState() || initialState();
130
+ const truth = state.steps?.truth || {};
131
+ const channel = activeChannel(state);
132
+
133
+ const ttl = Number(process.env.LARKIT_TRUTH_TTL_MS || AUTO_UPDATE_TTL_MS);
134
+ const last = Number(truth.lastCheckAt || 0);
135
+ if (!force && Date.now() - last < ttl) {
136
+ return { status: "fresh", channel, checkedAt: last ? new Date(last).toISOString() : null };
137
+ }
138
+
139
+ let manifest;
140
+ try {
141
+ manifest = await fetchManifest(channel, { timeoutMs });
142
+ } catch (error) {
143
+ if (fs.existsSync(TRUTH_DB)) {
144
+ recordTruth(state, { lastCheckAt: Date.now() - ttl + 15 * 60 * 1000 });
145
+ reporter?.warn?.(`truth channel unreachable (${error.message}); keeping the local truth`);
146
+ return { status: "unreachable", channel, error: error.message };
147
+ }
148
+ throw new Error(
149
+ `cannot reach the truth channel ${channel} (${error.message}).\n` +
150
+ "retry when the network is back, or install from a pack file:\n" +
151
+ " larkit init --pack=<manifest.json|pack-dir|.tar.gz>",
152
+ );
153
+ }
154
+
155
+ const latestSha = packShaOf(manifest);
156
+ const latest = truthTagOf(manifest);
157
+ if (!latestSha || latestSha === truth.packSha) {
158
+ recordTruth(state, { lastCheckAt: Date.now(), latestSeen: latest, channel });
159
+ return { status: "up-to-date", channel, latest };
160
+ }
161
+
162
+ const tier = manifest.tiers?.[TIER_NAME];
163
+ reporter?.info?.(
164
+ `truth update ${truth.packVersion || "?"} → ${latest} (${tier ? `${(tier.bytes / 1024 / 1024).toFixed(1)} MiB` : "pack"})`,
165
+ );
166
+ try {
167
+ const result = await installTruthPack({
168
+ source: channel,
169
+ quiet: true,
170
+ reporter,
171
+ state: { truth },
172
+ });
173
+ recordTruth(state, {
174
+ status: "ready",
175
+ source: result.source,
176
+ channel,
177
+ packVersion: result.packVersion,
178
+ packSha: result.packSha,
179
+ stats: result.stats,
180
+ seeds: result.seeds || truth.seeds || {},
181
+ skipped: false,
182
+ lastCheckAt: Date.now(),
183
+ latestSeen: latest,
184
+ codeFiles: result.files || truth.codeFiles || null,
185
+ });
186
+ reporter?.ok?.(`truth ${result.packVersion} installed (${fmt(result.stats)})`);
187
+ return { status: "updated", channel, latest, result };
188
+ } catch (error) {
189
+ recordTruth(state, { lastCheckAt: Date.now() - ttl + 15 * 60 * 1000 });
190
+ reporter?.warn?.(`truth update failed (${error.message}); keeping the local truth`);
191
+ return { status: "failed", channel, error: error.message };
192
+ }
193
+ }
194
+
195
+ function fmt(stats = {}) {
196
+ const parts = [];
197
+ if (stats.protocolCatalog) parts.push(`${stats.protocolCatalog} commands`);
198
+ if (stats.codeAnchors) parts.push(`${stats.codeAnchors} anchors`);
199
+ if (fs.existsSync(FORMATTED_DIR)) parts.push("code corpus");
200
+ return parts.join(" · ") || "up to date";
201
+ }
@@ -0,0 +1,184 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import process from "node:process";
4
+ import { spawnSync } from "node:child_process";
5
+ import {
6
+ BIN_DIR,
7
+ CLI_INSTALL_DIR,
8
+ IS_REPO_CHECKOUT,
9
+ PKG_ROOT,
10
+ displayPath,
11
+ } from "../shared/paths.js";
12
+ import { ensureDir, readJson, writeJson } from "../shared/util.js";
13
+
14
+ function runNpm(args) {
15
+ const command = process.platform === "win32" ? "npm.cmd" : "npm";
16
+ const result = spawnSync(command, args, {
17
+ encoding: "utf8",
18
+ shell: process.platform === "win32",
19
+ env: {
20
+ ...process.env,
21
+ npm_config_update_notifier: "false",
22
+ npm_config_fund: "false",
23
+ },
24
+ });
25
+ if (result.error) throw result.error;
26
+ if (result.status !== 0) {
27
+ const detail = (result.stderr || result.stdout || "").trim().split("\n").slice(-8).join("\n");
28
+ throw new Error(`npm ${args[0]} failed (exit ${result.status})${detail ? `:\n${detail}` : ""}`);
29
+ }
30
+ return result.stdout;
31
+ }
32
+
33
+ function shimBody(target) {
34
+ return `#!/usr/bin/env sh\nexec node "${target}" "$@"\n`;
35
+ }
36
+
37
+ function cmdShimBody(target) {
38
+ return `@echo off\r\nnode "${target}" %*\r\n`;
39
+ }
40
+
41
+ function writeShims(entry, binDir) {
42
+ const binDirOfPackage = path.dirname(entry);
43
+ const targets = {
44
+ larkit: path.join(binDirOfPackage, "larkit.js"),
45
+ "larkit-mcp": path.join(binDirOfPackage, "larkit-mcp.js"),
46
+ };
47
+ ensureDir(binDir);
48
+ const written = [];
49
+ for (const [name, target] of Object.entries(targets)) {
50
+ if (!fs.existsSync(target)) continue;
51
+ const file = path.join(binDir, name);
52
+ fs.writeFileSync(file, shimBody(target));
53
+ fs.chmodSync(file, 0o755);
54
+ written.push(file);
55
+ if (process.platform === "win32") {
56
+ const cmd = `${file}.cmd`;
57
+ fs.writeFileSync(cmd, cmdShimBody(target));
58
+ written.push(cmd);
59
+ }
60
+ }
61
+ return written;
62
+ }
63
+
64
+ export function binDirOnPath(binDir = BIN_DIR) {
65
+ const parts = String(process.env.PATH || "")
66
+ .split(path.delimiter)
67
+ .filter(Boolean)
68
+ .map((part) => path.resolve(part));
69
+ return parts.includes(path.resolve(binDir));
70
+ }
71
+
72
+ export function detectCliInstall({ binDir = BIN_DIR } = {}) {
73
+ const shim = path.join(binDir, "larkit");
74
+ const installedPackage = path.join(CLI_INSTALL_DIR, "node_modules", "larkit", "package.json");
75
+ const repoVersion = IS_REPO_CHECKOUT
76
+ ? readJson(path.join(PKG_ROOT, "package.json"), {}).version || null
77
+ : null;
78
+ return {
79
+ shim,
80
+ shim_installed: fs.existsSync(shim),
81
+ bin_dir: binDir,
82
+ on_path: binDirOnPath(binDir),
83
+ mode: fs.existsSync(installedPackage)
84
+ ? "local-prefix"
85
+ : IS_REPO_CHECKOUT
86
+ ? "repo"
87
+ : null,
88
+ version: readJson(installedPackage, {}).version || repoVersion,
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Install a durable `larkit` command.
94
+ *
95
+ * Default strategy: install the package into ~/.larkit/cli and drop a tiny
96
+ * shell shim into ~/.local/bin, so no sudo/global prefix is required and the
97
+ * command survives npx cache cleanup. In a git checkout the shim points at
98
+ * the checkout itself. `--global` delegates to `npm install -g`.
99
+ */
100
+ export function installCli({
101
+ version,
102
+ force = false,
103
+ global = false,
104
+ binDir = BIN_DIR,
105
+ source = null,
106
+ } = {}) {
107
+ if (global) {
108
+ const target = source || `larkit@${version}`;
109
+ runNpm(["install", "-g", target, "--no-audit", "--no-fund", "--loglevel=error"]);
110
+ return {
111
+ mode: "global",
112
+ entry: null,
113
+ shim: null,
114
+ bin_dir: binDir,
115
+ on_path: binDirOnPath(binDir),
116
+ status: "installed",
117
+ };
118
+ }
119
+
120
+ const devCheckout =
121
+ IS_REPO_CHECKOUT && fs.existsSync(path.join(PKG_ROOT, "bin", "larkit.js"));
122
+
123
+ if (devCheckout && !source) {
124
+ const entry = path.join(PKG_ROOT, "bin", "larkit.js");
125
+ const written = writeShims(entry, binDir);
126
+ return {
127
+ mode: "repo",
128
+ entry,
129
+ shim: path.join(binDir, "larkit"),
130
+ shims: written,
131
+ bin_dir: binDir,
132
+ on_path: binDirOnPath(binDir),
133
+ status: "linked",
134
+ version,
135
+ };
136
+ }
137
+
138
+ const packageRoot = path.join(CLI_INSTALL_DIR, "node_modules", "larkit");
139
+ const entry = path.join(packageRoot, "bin", "larkit.js");
140
+ const installed = readJson(path.join(packageRoot, "package.json"), {});
141
+ let status = "present";
142
+ if (force || !fs.existsSync(entry) || (version && installed.version !== version)) {
143
+ ensureDir(CLI_INSTALL_DIR);
144
+ const manifest = path.join(CLI_INSTALL_DIR, "package.json");
145
+ if (!fs.existsSync(manifest)) {
146
+ writeJson(manifest, { name: "larkit-cli-install", private: true });
147
+ }
148
+ const target = source || `larkit@${version}`;
149
+ runNpm([
150
+ "install",
151
+ "--prefix",
152
+ CLI_INSTALL_DIR,
153
+ "--no-save",
154
+ "--no-package-lock",
155
+ "--no-audit",
156
+ "--no-fund",
157
+ "--loglevel=error",
158
+ target,
159
+ ]);
160
+ if (!fs.existsSync(entry)) {
161
+ throw new Error(
162
+ `npm installed ${target} but ${displayPath(entry)} is missing; check that the package bin entry is correct`,
163
+ );
164
+ }
165
+ status = "installed";
166
+ }
167
+ const written = writeShims(entry, binDir);
168
+ return {
169
+ mode: "local-prefix",
170
+ entry,
171
+ shim: path.join(binDir, "larkit"),
172
+ shims: written,
173
+ bin_dir: binDir,
174
+ on_path: binDirOnPath(binDir),
175
+ status,
176
+ version: readJson(path.join(packageRoot, "package.json"), {}).version || version,
177
+ };
178
+ }
179
+
180
+ export function mcpServerCommand() {
181
+ const shim = path.join(BIN_DIR, "larkit-mcp");
182
+ if (fs.existsSync(shim)) return shim;
183
+ return "larkit-mcp";
184
+ }