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.
- package/AGENT_SKILL.md +12 -0
- package/README.md +765 -2
- package/bin/larkit-mcp.js +8 -0
- package/bin/larkit.js +532 -0
- package/larkit.config.example.json +12 -0
- package/package.json +65 -4
- package/scripts/.tmp-npm-webauth.mjs +49 -0
- package/scripts/build-index.mjs +66 -0
- package/scripts/check-syntax.mjs +28 -0
- package/scripts/login-worker.mjs +50 -0
- package/scripts/publish-pack.mjs +277 -0
- package/scripts/verify-bootstrap.mjs +238 -0
- package/scripts/verify-console.mjs +60 -0
- package/scripts/verify-index.mjs +59 -0
- package/scripts/verify-mcp.mjs +215 -0
- package/skills/larkit/SKILL.md +120 -0
- package/skills/larkit/references/console.md +86 -0
- package/skills/larkit/references/playbooks.md +177 -0
- package/skills/larkit/references/tools.md +115 -0
- package/skills/larkit/references/truth-model.md +117 -0
- package/src/auth/login.js +171 -0
- package/src/auth/service.js +213 -0
- package/src/auth/store.js +213 -0
- package/src/bootstrap/channel.js +201 -0
- package/src/bootstrap/cli-install.js +184 -0
- package/src/bootstrap/download.js +182 -0
- package/src/bootstrap/init.js +672 -0
- package/src/bootstrap/pack-cli.js +97 -0
- package/src/bootstrap/pack.js +541 -0
- package/src/bootstrap/report.js +62 -0
- package/src/bootstrap/skills.js +223 -0
- package/src/bootstrap/state.js +66 -0
- package/src/bootstrap/tar.js +417 -0
- package/src/console/browser.js +338 -0
- package/src/console/client.js +827 -0
- package/src/console/dispatch.js +101 -0
- package/src/docs/client.js +284 -0
- package/src/indexer/anchors.js +366 -0
- package/src/indexer/db.js +449 -0
- package/src/indexer/extract-assets.js +285 -0
- package/src/indexer/extract-deb.js +56 -0
- package/src/indexer/format-pool.js +64 -0
- package/src/indexer/format-worker.js +132 -0
- package/src/indexer/format.js +239 -0
- package/src/indexer/protocol.js +123 -0
- package/src/mcp/server.js +433 -0
- package/src/probe/client.js +225 -0
- package/src/probe/openapi.js +194 -0
- package/src/protocol/catalog.js +146 -0
- package/src/protocol/data/README.md +15 -0
- package/src/protocol/data/bundle.json +1 -0
- package/src/protocol/data/commands.json +12896 -0
- package/src/protocol/data/constants.json +8 -0
- package/src/protocol/data/reference/PROTOCOL.md +66 -0
- package/src/protocol/data/reference/endpoints.json +46 -0
- package/src/protocol/data/schema.json.gz +0 -0
- package/src/protocol/descriptor.js +358 -0
- package/src/protocol/schema.js +235 -0
- package/src/protocol/transport.js +387 -0
- package/src/shared/browser.js +70 -0
- package/src/shared/config.js +90 -0
- package/src/shared/paths.js +151 -0
- package/src/shared/quiet.js +12 -0
- package/src/shared/util.js +124 -0
- package/src/truth/fts.js +24 -0
- package/src/truth/search.js +554 -0
- package/src/truth/verified.js +107 -0
- package/verified_truths/README.md +28 -0
- package/verified_truths/card-json-2.0-official.md +55 -0
- package/verified_truths/client-ws-gateway-boundaries.md +33 -0
- package/verified_truths/feishu-developer-console-apis.md +104 -0
- package/workers/truth-api/src/index.js +180 -0
- package/workers/truth-api/wrangler.toml +31 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
DATA_DIR,
|
|
5
|
+
HOME_CONFIG_FILE,
|
|
6
|
+
PROJECT_CONFIG_FILE,
|
|
7
|
+
} from "./paths.js";
|
|
8
|
+
import { readJson } from "./util.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Configuration layers (lowest to highest precedence):
|
|
12
|
+
* 1. <data dir>/config.json (legacy / machine level)
|
|
13
|
+
* 2. ~/.larkit/config.json (user level)
|
|
14
|
+
* 3. ./larkit.config.json (project level, next to the app you build)
|
|
15
|
+
* Environment variables still win inside getAppCredentials().
|
|
16
|
+
*/
|
|
17
|
+
export function loadConfig() {
|
|
18
|
+
const machine = readJson(path.join(DATA_DIR, "config.json"), {});
|
|
19
|
+
const home = readJson(HOME_CONFIG_FILE, {});
|
|
20
|
+
const project = readJson(PROJECT_CONFIG_FILE, {});
|
|
21
|
+
const merged = {
|
|
22
|
+
...machine,
|
|
23
|
+
...home,
|
|
24
|
+
...project,
|
|
25
|
+
apps: { ...(machine.apps || {}), ...(home.apps || {}), ...(project.apps || {}) },
|
|
26
|
+
console: {
|
|
27
|
+
...(machine.console || {}),
|
|
28
|
+
...(home.console || {}),
|
|
29
|
+
...(project.console || {}),
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
return merged;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getAppCredentials(appId, config = loadConfig()) {
|
|
36
|
+
if (
|
|
37
|
+
process.env.FEISHU_APP_ID &&
|
|
38
|
+
process.env.FEISHU_APP_SECRET &&
|
|
39
|
+
(!appId || appId === process.env.FEISHU_APP_ID)
|
|
40
|
+
) {
|
|
41
|
+
return {
|
|
42
|
+
app_id: appId || process.env.FEISHU_APP_ID,
|
|
43
|
+
app_secret: process.env.FEISHU_APP_SECRET,
|
|
44
|
+
source: "env",
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const apps = config.apps || {};
|
|
48
|
+
if (appId) {
|
|
49
|
+
const direct = apps[appId];
|
|
50
|
+
if (direct?.app_secret) {
|
|
51
|
+
return { app_id: appId, app_secret: direct.app_secret, source: "config" };
|
|
52
|
+
}
|
|
53
|
+
for (const [name, value] of Object.entries(apps)) {
|
|
54
|
+
if (value?.app_id === appId && value?.app_secret) {
|
|
55
|
+
return {
|
|
56
|
+
app_id: appId,
|
|
57
|
+
app_secret: value.app_secret,
|
|
58
|
+
source: `config:${name}`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const first = Object.entries(apps).find(
|
|
64
|
+
([, value]) => value?.app_id && value?.app_secret,
|
|
65
|
+
);
|
|
66
|
+
if (first) {
|
|
67
|
+
return {
|
|
68
|
+
app_id: first[1].app_id,
|
|
69
|
+
app_secret: first[1].app_secret,
|
|
70
|
+
source: `config:${first[0]}`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function configForMcp() {
|
|
77
|
+
const config = loadConfig();
|
|
78
|
+
return {
|
|
79
|
+
default_profile: config.defaultProfile || config.console?.profile || null,
|
|
80
|
+
apps: Object.keys(config.apps || {}),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function ensureConfigFile() {
|
|
85
|
+
return fs.existsSync(PROJECT_CONFIG_FILE);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function configPath() {
|
|
89
|
+
return path.resolve(PROJECT_CONFIG_FILE);
|
|
90
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* PKG_ROOT is the immutable package directory (src/, skills/, protocol data).
|
|
8
|
+
* LARKIT_HOME is the mutable per-user directory created by `larkit init`.
|
|
9
|
+
*
|
|
10
|
+
* A git checkout keeps using the historical in-repo `.larkit-data/` layout so
|
|
11
|
+
* `pnpm index` / `pnpm verify` continue to work unchanged. An installed CLI
|
|
12
|
+
* (global install or `npx larkit`) has no `.larkit-data/` next to the package
|
|
13
|
+
* and therefore stores everything under `~/.larkit/`.
|
|
14
|
+
*/
|
|
15
|
+
export const PKG_ROOT = path.resolve(
|
|
16
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
17
|
+
"../..",
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
/** Backwards-compatible alias: code that reads bundled assets uses ROOT. */
|
|
21
|
+
export const ROOT = PKG_ROOT;
|
|
22
|
+
|
|
23
|
+
const REPO_DATA_DIR = path.join(PKG_ROOT, ".larkit-data");
|
|
24
|
+
export const IS_REPO_CHECKOUT =
|
|
25
|
+
fs.existsSync(path.join(PKG_ROOT, ".git")) ||
|
|
26
|
+
fs.existsSync(path.join(PKG_ROOT, "pnpm-lock.yaml"));
|
|
27
|
+
|
|
28
|
+
const HAS_REPO_DATA = fs.existsSync(REPO_DATA_DIR);
|
|
29
|
+
|
|
30
|
+
export const LARKIT_HOME = process.env.LARKIT_HOME
|
|
31
|
+
? path.resolve(process.env.LARKIT_HOME)
|
|
32
|
+
: path.join(os.homedir(), ".larkit");
|
|
33
|
+
|
|
34
|
+
export const DATA_DIR = process.env.LARKIT_DATA_DIR
|
|
35
|
+
? path.resolve(process.env.LARKIT_DATA_DIR)
|
|
36
|
+
: HAS_REPO_DATA
|
|
37
|
+
? REPO_DATA_DIR
|
|
38
|
+
: path.join(LARKIT_HOME, "data");
|
|
39
|
+
|
|
40
|
+
export const VERIFIED_DIR = process.env.LARKIT_VERIFIED_DIR
|
|
41
|
+
? path.resolve(process.env.LARKIT_VERIFIED_DIR)
|
|
42
|
+
: HAS_REPO_DATA
|
|
43
|
+
? path.join(PKG_ROOT, "verified_truths")
|
|
44
|
+
: path.join(LARKIT_HOME, "verified_truths");
|
|
45
|
+
|
|
46
|
+
export const RAW_DIR = path.join(DATA_DIR, "raw");
|
|
47
|
+
export const CACHE_DIR = path.join(DATA_DIR, "cache");
|
|
48
|
+
export const DEB_ROOT = path.join(RAW_DIR, "deb-root");
|
|
49
|
+
export const FEISHU_APP_DIR = path.join(
|
|
50
|
+
DEB_ROOT,
|
|
51
|
+
"opt",
|
|
52
|
+
"bytedance",
|
|
53
|
+
"feishu",
|
|
54
|
+
);
|
|
55
|
+
export const ELECTRON_EXTRACT_DIR = path.join(
|
|
56
|
+
DATA_DIR,
|
|
57
|
+
"extracted",
|
|
58
|
+
"electron",
|
|
59
|
+
);
|
|
60
|
+
export const FORMATTED_DIR = path.join(DATA_DIR, "formatted");
|
|
61
|
+
export const TRUTH_DB = path.join(DATA_DIR, "truth.db");
|
|
62
|
+
export const TRUTH_META = path.join(DATA_DIR, "truth.meta.json");
|
|
63
|
+
export const WEB_INDEX = path.join(RAW_DIR, "web-index.json");
|
|
64
|
+
export const ELECTRON_INDEX = path.join(RAW_DIR, "electron-index.json");
|
|
65
|
+
export const EXTRACT_MANIFEST = path.join(RAW_DIR, "manifest.extract.json");
|
|
66
|
+
export const FORMAT_MANIFEST = path.join(
|
|
67
|
+
FORMATTED_DIR,
|
|
68
|
+
".format-manifest.json",
|
|
69
|
+
);
|
|
70
|
+
export const ANCHORS_JSONL = path.join(RAW_DIR, "anchors.jsonl");
|
|
71
|
+
export const PROTOCOL_CATALOG_JSON = path.join(
|
|
72
|
+
RAW_DIR,
|
|
73
|
+
"protocol-catalog.json",
|
|
74
|
+
);
|
|
75
|
+
export const PROTOCOL_DOCS_JSON = path.join(RAW_DIR, "protocol-docs.json");
|
|
76
|
+
|
|
77
|
+
export const PROTOCOL_REFERENCE_DIR = path.join(
|
|
78
|
+
PKG_ROOT,
|
|
79
|
+
"src",
|
|
80
|
+
"protocol",
|
|
81
|
+
"data",
|
|
82
|
+
"reference",
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
/** Immutable assets shipped inside the npm package. */
|
|
86
|
+
export const SKILL_SOURCE_DIR = path.join(PKG_ROOT, "skills", "larkit");
|
|
87
|
+
|
|
88
|
+
/** Mutable per-user locations. */
|
|
89
|
+
export const INSTALL_STATE_FILE = path.join(LARKIT_HOME, "install.json");
|
|
90
|
+
export const INSTALL_LOCK_FILE = path.join(LARKIT_HOME, ".install.lock");
|
|
91
|
+
export const CLI_INSTALL_DIR = path.join(LARKIT_HOME, "cli");
|
|
92
|
+
export const DOWNLOAD_DIR = path.join(LARKIT_HOME, "downloads");
|
|
93
|
+
export const LOG_FILE = path.join(LARKIT_HOME, "install.log");
|
|
94
|
+
export const BIN_DIR = process.env.LARKIT_BIN_DIR
|
|
95
|
+
? path.resolve(process.env.LARKIT_BIN_DIR)
|
|
96
|
+
: path.join(os.homedir(), ".local", "bin");
|
|
97
|
+
export const MCP_CONFIG_FILE = path.join(LARKIT_HOME, "mcp.json");
|
|
98
|
+
|
|
99
|
+
/** Configuration: project-local overrides user-level defaults. */
|
|
100
|
+
export const PROJECT_CONFIG_FILE = process.env.LARKIT_CONFIG
|
|
101
|
+
? path.resolve(process.env.LARKIT_CONFIG)
|
|
102
|
+
: path.join(process.cwd(), "larkit.config.json");
|
|
103
|
+
export const HOME_CONFIG_FILE = path.join(LARKIT_HOME, "config.json");
|
|
104
|
+
/** Backwards-compatible exports. */
|
|
105
|
+
export const CONFIG_FILE = PROJECT_CONFIG_FILE;
|
|
106
|
+
export const LOCAL_CONFIG_FILE = HOME_CONFIG_FILE;
|
|
107
|
+
|
|
108
|
+
export const AUTH_CREDENTIALS_FILE = path.join(
|
|
109
|
+
LARKIT_HOME,
|
|
110
|
+
"credentials.json",
|
|
111
|
+
);
|
|
112
|
+
export const AUTH_CONFIG_FILE = path.join(LARKIT_HOME, "config.json");
|
|
113
|
+
export const CONSOLE_RECIPES_FILE = path.join(
|
|
114
|
+
DATA_DIR,
|
|
115
|
+
"console-api-recipes.json",
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
/** Build inputs (only needed for `larkit init --from-assets`). */
|
|
119
|
+
export const WEB_DUMP_DIR = process.env.LARKIT_WEB_DUMP
|
|
120
|
+
? path.resolve(process.env.LARKIT_WEB_DUMP)
|
|
121
|
+
: path.join(PKG_ROOT, "feishu-web-dump");
|
|
122
|
+
export const WEB_SUMMARY = path.join(WEB_DUMP_DIR, "summary.json");
|
|
123
|
+
export const DEB_FILE = process.env.LARKIT_DEB
|
|
124
|
+
? path.resolve(process.env.LARKIT_DEB)
|
|
125
|
+
: [
|
|
126
|
+
path.join(PKG_ROOT, "feishu-linux.deb"),
|
|
127
|
+
path.join(LARKIT_HOME, "assets", "feishu-linux.deb"),
|
|
128
|
+
].find((candidate) => fs.existsSync(candidate)) ||
|
|
129
|
+
path.join(PKG_ROOT, "feishu-linux.deb");
|
|
130
|
+
|
|
131
|
+
/** Render a user-facing relative path that does not leak the install layout. */
|
|
132
|
+
export function displayPath(target) {
|
|
133
|
+
const resolved = path.resolve(target);
|
|
134
|
+
const formatted = path.resolve(FORMATTED_DIR);
|
|
135
|
+
const verified = path.resolve(VERIFIED_DIR);
|
|
136
|
+
const root = path.resolve(ROOT);
|
|
137
|
+
if (resolved === formatted || resolved.startsWith(formatted + path.sep)) {
|
|
138
|
+
return `formatted/${path.relative(formatted, resolved).split(path.sep).join("/")}`;
|
|
139
|
+
}
|
|
140
|
+
if (resolved === verified || resolved.startsWith(verified + path.sep)) {
|
|
141
|
+
return `verified_truths/${path.relative(verified, resolved).split(path.sep).join("/")}`;
|
|
142
|
+
}
|
|
143
|
+
if (resolved === root || resolved.startsWith(root + path.sep)) {
|
|
144
|
+
return path.relative(root, resolved).split(path.sep).join("/");
|
|
145
|
+
}
|
|
146
|
+
const data = path.resolve(DATA_DIR);
|
|
147
|
+
if (resolved === data || resolved.startsWith(data + path.sep)) {
|
|
148
|
+
return path.relative(data, resolved).split(path.sep).join("/");
|
|
149
|
+
}
|
|
150
|
+
return resolved;
|
|
151
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* node:sqlite is stable enough for this local index but still emits an
|
|
3
|
+
* ExperimentalWarning on Node 24. Suppress only that warning so CLI JSON
|
|
4
|
+
* output stays clean; all other warnings pass through.
|
|
5
|
+
*/
|
|
6
|
+
const originalEmitWarning = process.emitWarning.bind(process);
|
|
7
|
+
process.emitWarning = (warning, ...args) => {
|
|
8
|
+
const text =
|
|
9
|
+
typeof warning === "string" ? warning : warning?.message || String(warning);
|
|
10
|
+
if (/SQLite is an experimental feature/i.test(text)) return;
|
|
11
|
+
return originalEmitWarning(warning, ...args);
|
|
12
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
|
|
5
|
+
export function ensureDir(dir) {
|
|
6
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
7
|
+
return dir;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function readJson(file, fallback) {
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
13
|
+
} catch {
|
|
14
|
+
return fallback;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function writeJson(file, value) {
|
|
19
|
+
ensureDir(path.dirname(file));
|
|
20
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
21
|
+
fs.writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`);
|
|
22
|
+
fs.renameSync(tmp, file);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function sha1(data) {
|
|
26
|
+
return createHash("sha1").update(data).digest("hex");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function sha256(data) {
|
|
30
|
+
return createHash("sha256").update(data).digest("hex");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function toPosix(p) {
|
|
34
|
+
return p.split(path.sep).join("/");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function relativeTo(base, target) {
|
|
38
|
+
return toPosix(path.relative(base, target));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Resolve a user supplied relative path inside base, rejecting traversal. */
|
|
42
|
+
export function safeJoin(base, rel) {
|
|
43
|
+
const clean = String(rel || "").replace(/^[/\\]+/, "");
|
|
44
|
+
const resolved = path.resolve(base, clean);
|
|
45
|
+
const prefix = path.resolve(base) + path.sep;
|
|
46
|
+
if (resolved !== path.resolve(base) && !resolved.startsWith(prefix)) {
|
|
47
|
+
throw new Error(`path escapes root: ${rel}`);
|
|
48
|
+
}
|
|
49
|
+
return resolved;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function walkFiles(dir, options = {}) {
|
|
53
|
+
const {
|
|
54
|
+
extensions = null,
|
|
55
|
+
skipDirs = new Set([
|
|
56
|
+
"node_modules",
|
|
57
|
+
".git",
|
|
58
|
+
"i18n",
|
|
59
|
+
"locales",
|
|
60
|
+
"__tests__",
|
|
61
|
+
"test",
|
|
62
|
+
]),
|
|
63
|
+
maxFileBytes = Infinity,
|
|
64
|
+
includeHidden = false,
|
|
65
|
+
} = options;
|
|
66
|
+
const out = [];
|
|
67
|
+
if (!fs.existsSync(dir)) return out;
|
|
68
|
+
const stack = [dir];
|
|
69
|
+
while (stack.length) {
|
|
70
|
+
const current = stack.pop();
|
|
71
|
+
let entries;
|
|
72
|
+
try {
|
|
73
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
74
|
+
} catch {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
if (!includeHidden && entry.name.startsWith(".")) continue;
|
|
79
|
+
const full = path.join(current, entry.name);
|
|
80
|
+
if (entry.isDirectory()) {
|
|
81
|
+
if (skipDirs.has(entry.name)) continue;
|
|
82
|
+
stack.push(full);
|
|
83
|
+
} else if (entry.isFile()) {
|
|
84
|
+
if (extensions && !extensions.has(path.extname(entry.name).toLowerCase())) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const st = fs.statSync(full);
|
|
89
|
+
if (st.size > maxFileBytes || st.size === 0) continue;
|
|
90
|
+
out.push({ path: full, size: st.size, mtimeMs: st.mtimeMs });
|
|
91
|
+
} catch {
|
|
92
|
+
// ignore unreadable files
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
out.sort((a, b) => a.path.localeCompare(b.path));
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function truncate(value, max = 300) {
|
|
102
|
+
const s = String(value ?? "");
|
|
103
|
+
return s.length <= max ? s : `${s.slice(0, Math.max(0, max - 1))}…`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function oneLine(value, max = 300) {
|
|
107
|
+
return truncate(String(value ?? "").replace(/\s+/g, " ").trim(), max);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function sleep(ms) {
|
|
111
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function fileSizeMiB(bytes) {
|
|
115
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MiB`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function log(...args) {
|
|
119
|
+
console.error(...args);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function nowIso() {
|
|
123
|
+
return new Date().toISOString();
|
|
124
|
+
}
|
package/src/truth/fts.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** FTS helpers shared by the indexer and the query layer. */
|
|
2
|
+
|
|
3
|
+
export function normalizeSearchText(value) {
|
|
4
|
+
return String(value ?? "")
|
|
5
|
+
.toLowerCase()
|
|
6
|
+
.replace(/[_/.:#-]+/g, " ")
|
|
7
|
+
.replace(/[^\p{L}\p{N}\s]+/gu, " ")
|
|
8
|
+
.replace(/\s+/g, " ")
|
|
9
|
+
.trim();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function buildFtsQuery(query, { conjunction = false } = {}) {
|
|
13
|
+
const normalized = normalizeSearchText(query);
|
|
14
|
+
if (!normalized) return "";
|
|
15
|
+
const terms = [...new Set(normalized.split(" ").filter(Boolean))].slice(0, 12);
|
|
16
|
+
return terms
|
|
17
|
+
.map((term) => `"${term.replace(/"/g, "")}"*`)
|
|
18
|
+
.join(conjunction ? " AND " : " OR ");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function searchTextFor(parts) {
|
|
22
|
+
const raw = parts.filter(Boolean).join(" ");
|
|
23
|
+
return `${raw}\n${normalizeSearchText(raw)}`;
|
|
24
|
+
}
|