@biaoo/tiangong-wiki 0.2.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 (136) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +167 -0
  3. package/README.zh-CN.md +167 -0
  4. package/SKILL.md +116 -0
  5. package/agents/openai.yaml +4 -0
  6. package/assets/config.example.env +18 -0
  7. package/assets/templates/achievement.md +32 -0
  8. package/assets/templates/bridge.md +33 -0
  9. package/assets/templates/concept.md +47 -0
  10. package/assets/templates/faq.md +31 -0
  11. package/assets/templates/lesson.md +31 -0
  12. package/assets/templates/method.md +31 -0
  13. package/assets/templates/misconception.md +35 -0
  14. package/assets/templates/person.md +31 -0
  15. package/assets/templates/research-note.md +34 -0
  16. package/assets/templates/resume.md +34 -0
  17. package/assets/templates/source-summary.md +35 -0
  18. package/assets/vllm/qwen3_5_openai_developer.jinja +182 -0
  19. package/assets/wiki.config.default.json +193 -0
  20. package/dist/commands/check-config.js +77 -0
  21. package/dist/commands/create.js +32 -0
  22. package/dist/commands/daemon.js +186 -0
  23. package/dist/commands/dashboard.js +112 -0
  24. package/dist/commands/doctor.js +22 -0
  25. package/dist/commands/export-graph.js +28 -0
  26. package/dist/commands/export-index.js +31 -0
  27. package/dist/commands/find.js +36 -0
  28. package/dist/commands/fts.js +32 -0
  29. package/dist/commands/graph.js +35 -0
  30. package/dist/commands/init.js +48 -0
  31. package/dist/commands/lint.js +35 -0
  32. package/dist/commands/list.js +28 -0
  33. package/dist/commands/page-info.js +24 -0
  34. package/dist/commands/search.js +32 -0
  35. package/dist/commands/setup.js +15 -0
  36. package/dist/commands/stat.js +20 -0
  37. package/dist/commands/sync.js +38 -0
  38. package/dist/commands/template.js +71 -0
  39. package/dist/commands/type.js +88 -0
  40. package/dist/commands/vault.js +64 -0
  41. package/dist/core/agent.js +201 -0
  42. package/dist/core/cli-env.js +129 -0
  43. package/dist/core/codex-workflow.js +233 -0
  44. package/dist/core/config.js +126 -0
  45. package/dist/core/db.js +292 -0
  46. package/dist/core/embedding.js +104 -0
  47. package/dist/core/frontmatter.js +287 -0
  48. package/dist/core/indexer.js +241 -0
  49. package/dist/core/onboarding.js +967 -0
  50. package/dist/core/page-files.js +91 -0
  51. package/dist/core/paths.js +161 -0
  52. package/dist/core/presenters.js +23 -0
  53. package/dist/core/query.js +58 -0
  54. package/dist/core/runtime.js +20 -0
  55. package/dist/core/sync.js +235 -0
  56. package/dist/core/synology.js +412 -0
  57. package/dist/core/template-evolution.js +38 -0
  58. package/dist/core/vault-processing.js +742 -0
  59. package/dist/core/vault.js +594 -0
  60. package/dist/core/workflow-context.js +188 -0
  61. package/dist/core/workflow-result.js +162 -0
  62. package/dist/core/workspace-bootstrap.js +30 -0
  63. package/dist/core/workspace-skills.js +220 -0
  64. package/dist/daemon/client.js +147 -0
  65. package/dist/daemon/server.js +807 -0
  66. package/dist/daemon/state.js +53 -0
  67. package/dist/dashboard/assets/index-1FgAUZ28.css +1 -0
  68. package/dist/dashboard/assets/index-6A0PWT4X.js +154 -0
  69. package/dist/dashboard/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  70. package/dist/dashboard/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  71. package/dist/dashboard/assets/jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff +0 -0
  72. package/dist/dashboard/assets/jetbrains-mono-cyrillic-500-normal-DmUKJPL_.woff2 +0 -0
  73. package/dist/dashboard/assets/jetbrains-mono-cyrillic-700-normal-BWTpRfYl.woff2 +0 -0
  74. package/dist/dashboard/assets/jetbrains-mono-cyrillic-700-normal-CEoEElIJ.woff +0 -0
  75. package/dist/dashboard/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  76. package/dist/dashboard/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  77. package/dist/dashboard/assets/jetbrains-mono-greek-500-normal-D7SFKleX.woff +0 -0
  78. package/dist/dashboard/assets/jetbrains-mono-greek-500-normal-JpySY46c.woff2 +0 -0
  79. package/dist/dashboard/assets/jetbrains-mono-greek-700-normal-C6CZE3T8.woff2 +0 -0
  80. package/dist/dashboard/assets/jetbrains-mono-greek-700-normal-DEigVDxa.woff +0 -0
  81. package/dist/dashboard/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  82. package/dist/dashboard/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  83. package/dist/dashboard/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
  84. package/dist/dashboard/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
  85. package/dist/dashboard/assets/jetbrains-mono-latin-700-normal-BYuf6tUa.woff2 +0 -0
  86. package/dist/dashboard/assets/jetbrains-mono-latin-700-normal-D3wTyLJW.woff +0 -0
  87. package/dist/dashboard/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  88. package/dist/dashboard/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  89. package/dist/dashboard/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
  90. package/dist/dashboard/assets/jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff +0 -0
  91. package/dist/dashboard/assets/jetbrains-mono-latin-ext-700-normal-CZipNAKV.woff2 +0 -0
  92. package/dist/dashboard/assets/jetbrains-mono-latin-ext-700-normal-CxPITLHs.woff +0 -0
  93. package/dist/dashboard/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  94. package/dist/dashboard/assets/jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff +0 -0
  95. package/dist/dashboard/assets/jetbrains-mono-vietnamese-700-normal-BDLVIk2r.woff +0 -0
  96. package/dist/dashboard/assets/space-grotesk-latin-400-normal-BnQMeOim.woff +0 -0
  97. package/dist/dashboard/assets/space-grotesk-latin-400-normal-CJ-V5oYT.woff2 +0 -0
  98. package/dist/dashboard/assets/space-grotesk-latin-500-normal-CNSSEhBt.woff +0 -0
  99. package/dist/dashboard/assets/space-grotesk-latin-500-normal-lFbtlQH6.woff2 +0 -0
  100. package/dist/dashboard/assets/space-grotesk-latin-700-normal-CwsQ-cCU.woff +0 -0
  101. package/dist/dashboard/assets/space-grotesk-latin-700-normal-RjhwGPKo.woff2 +0 -0
  102. package/dist/dashboard/assets/space-grotesk-latin-ext-400-normal-CfP_5XZW.woff2 +0 -0
  103. package/dist/dashboard/assets/space-grotesk-latin-ext-400-normal-DRPE3kg4.woff +0 -0
  104. package/dist/dashboard/assets/space-grotesk-latin-ext-500-normal-3dgZTiw9.woff +0 -0
  105. package/dist/dashboard/assets/space-grotesk-latin-ext-500-normal-DUe3BAxM.woff2 +0 -0
  106. package/dist/dashboard/assets/space-grotesk-latin-ext-700-normal-BQnZhY3m.woff2 +0 -0
  107. package/dist/dashboard/assets/space-grotesk-latin-ext-700-normal-HVCqSBdx.woff +0 -0
  108. package/dist/dashboard/assets/space-grotesk-vietnamese-400-normal-B7xT_GF5.woff2 +0 -0
  109. package/dist/dashboard/assets/space-grotesk-vietnamese-400-normal-BIWiOVfw.woff +0 -0
  110. package/dist/dashboard/assets/space-grotesk-vietnamese-500-normal-BTqKIpxg.woff +0 -0
  111. package/dist/dashboard/assets/space-grotesk-vietnamese-500-normal-BmEvtly_.woff2 +0 -0
  112. package/dist/dashboard/assets/space-grotesk-vietnamese-700-normal-DMty7AZE.woff2 +0 -0
  113. package/dist/dashboard/assets/space-grotesk-vietnamese-700-normal-Duxec5Rn.woff +0 -0
  114. package/dist/dashboard/index.html +18 -0
  115. package/dist/index.js +86 -0
  116. package/dist/operations/dashboard.js +1231 -0
  117. package/dist/operations/export.js +110 -0
  118. package/dist/operations/query.js +649 -0
  119. package/dist/operations/type-template.js +210 -0
  120. package/dist/operations/write.js +143 -0
  121. package/dist/types/config.js +1 -0
  122. package/dist/types/page.js +1 -0
  123. package/dist/utils/case.js +22 -0
  124. package/dist/utils/errors.js +26 -0
  125. package/dist/utils/fs.js +77 -0
  126. package/dist/utils/output.js +33 -0
  127. package/dist/utils/process.js +60 -0
  128. package/dist/utils/segmenter.js +24 -0
  129. package/dist/utils/slug.js +10 -0
  130. package/dist/utils/time.js +24 -0
  131. package/package.json +64 -0
  132. package/references/cli-interface.md +312 -0
  133. package/references/env.md +122 -0
  134. package/references/template-design-guide.md +271 -0
  135. package/references/vault-to-wiki-instruction.md +110 -0
  136. package/references/wiki-maintenance-instruction.md +190 -0
@@ -0,0 +1,104 @@
1
+ import { sha256Text } from "../utils/fs.js";
2
+ import { AppError } from "../utils/errors.js";
3
+ export class EmbeddingClient {
4
+ settings;
5
+ constructor(settings) {
6
+ this.settings = settings;
7
+ }
8
+ static fromEnv(env = process.env) {
9
+ const baseUrl = env.EMBEDDING_BASE_URL ?? env.OPENROUTER_BASE_URL;
10
+ const apiKey = env.EMBEDDING_API_KEY ?? env.OPENROUTER_API_KEY;
11
+ const model = env.EMBEDDING_MODEL ?? env.OPENROUTER_EMBEDDING_MODEL;
12
+ const rawDimensions = env.EMBEDDING_DIMENSIONS ?? "384";
13
+ if (!baseUrl || !apiKey || !model) {
14
+ return null;
15
+ }
16
+ const dimensions = Number.parseInt(rawDimensions, 10);
17
+ if (!Number.isFinite(dimensions) || dimensions <= 0) {
18
+ throw new AppError(`Invalid EMBEDDING_DIMENSIONS: ${rawDimensions}`, "config");
19
+ }
20
+ return new EmbeddingClient({
21
+ baseUrl: baseUrl.replace(/\/+$/g, ""),
22
+ apiKey,
23
+ model,
24
+ dimensions,
25
+ });
26
+ }
27
+ get profileHash() {
28
+ return sha256Text(`${this.settings.baseUrl}:${this.settings.model}:${this.settings.dimensions}`);
29
+ }
30
+ async probe() {
31
+ await this.embedBatch(["tiangong-wiki-skill probe"]);
32
+ }
33
+ async embedBatch(inputs) {
34
+ if (inputs.length === 0) {
35
+ return [];
36
+ }
37
+ const response = await this.requestWithRetry({
38
+ model: this.settings.model,
39
+ input: inputs,
40
+ dimensions: this.settings.dimensions,
41
+ });
42
+ if (!response?.data || !Array.isArray(response.data)) {
43
+ throw new AppError("Embedding API returned an invalid response", "runtime");
44
+ }
45
+ const embeddings = response.data
46
+ .sort((left, right) => left.index - right.index)
47
+ .map((item) => item.embedding);
48
+ if (embeddings.length !== inputs.length) {
49
+ throw new AppError(`Embedding API returned ${embeddings.length} vectors for ${inputs.length} inputs`, "runtime");
50
+ }
51
+ const normalizedEmbeddings = embeddings.map((embedding) => {
52
+ if (embedding.length === this.settings.dimensions) {
53
+ return embedding;
54
+ }
55
+ if (embedding.length > this.settings.dimensions) {
56
+ return embedding.slice(0, this.settings.dimensions);
57
+ }
58
+ throw new AppError(`Embedding dimensions mismatch: expected ${this.settings.dimensions}, got ${embedding.length}`, "runtime");
59
+ });
60
+ for (const embedding of normalizedEmbeddings) {
61
+ if (embedding.length !== this.settings.dimensions) {
62
+ throw new AppError(`Embedding dimensions mismatch: expected ${this.settings.dimensions}, got ${embedding.length}`, "runtime");
63
+ }
64
+ }
65
+ return normalizedEmbeddings;
66
+ }
67
+ async embedAll(inputs, batchSize = 50) {
68
+ const results = [];
69
+ for (let index = 0; index < inputs.length; index += batchSize) {
70
+ const batch = inputs.slice(index, index + batchSize);
71
+ const embeddings = await this.embedBatch(batch);
72
+ results.push(...embeddings);
73
+ }
74
+ return results;
75
+ }
76
+ async requestWithRetry(payload) {
77
+ let lastError;
78
+ for (let attempt = 1; attempt <= 3; attempt += 1) {
79
+ try {
80
+ const response = await fetch(`${this.settings.baseUrl}/embeddings`, {
81
+ method: "POST",
82
+ headers: {
83
+ "content-type": "application/json",
84
+ authorization: `Bearer ${this.settings.apiKey}`,
85
+ },
86
+ body: JSON.stringify(payload),
87
+ signal: AbortSignal.timeout(30_000),
88
+ });
89
+ if (!response.ok) {
90
+ const body = await response.text();
91
+ throw new AppError(`Embedding API request failed with status ${response.status}`, "runtime", { body });
92
+ }
93
+ return (await response.json());
94
+ }
95
+ catch (error) {
96
+ lastError = error;
97
+ if (attempt < 3) {
98
+ await new Promise((resolve) => setTimeout(resolve, attempt * 250));
99
+ }
100
+ }
101
+ }
102
+ throw lastError instanceof AppError ? lastError : new AppError(String(lastError), "runtime");
103
+ }
104
+ }
@@ -0,0 +1,287 @@
1
+ import matter from "gray-matter";
2
+ import path from "node:path";
3
+ import { getTemplate } from "./config.js";
4
+ import { normalizePageId } from "./paths.js";
5
+ import { camelToSnake, humanizeFieldName } from "../utils/case.js";
6
+ import { toDateOnly } from "../utils/time.js";
7
+ const FIXED_FIELDS = new Set([
8
+ "pageType",
9
+ "title",
10
+ "nodeId",
11
+ "status",
12
+ "visibility",
13
+ "tags",
14
+ "createdAt",
15
+ "updatedAt",
16
+ ]);
17
+ function isPlainObject(value) {
18
+ return Object.prototype.toString.call(value) === "[object Object]";
19
+ }
20
+ function normalizeStringArray(value) {
21
+ if (value === undefined || value === null || value === "") {
22
+ return [];
23
+ }
24
+ if (Array.isArray(value)) {
25
+ return value
26
+ .map((item) => (item === null || item === undefined ? "" : String(item).trim()))
27
+ .filter(Boolean);
28
+ }
29
+ return [String(value).trim()].filter(Boolean);
30
+ }
31
+ function normalizeScalar(value) {
32
+ if (value instanceof Date) {
33
+ return value.toISOString().slice(0, 10);
34
+ }
35
+ if (Array.isArray(value)) {
36
+ return value.map((item) => normalizeScalar(item));
37
+ }
38
+ if (isPlainObject(value)) {
39
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, normalizeScalar(item)]));
40
+ }
41
+ if (typeof value === "string") {
42
+ return value.trim();
43
+ }
44
+ return value;
45
+ }
46
+ function normalizeColumnValue(value) {
47
+ const normalized = normalizeScalar(value);
48
+ if (typeof normalized === "boolean") {
49
+ return normalized ? 1 : 0;
50
+ }
51
+ if (Array.isArray(normalized) || isPlainObject(normalized)) {
52
+ return JSON.stringify(normalized);
53
+ }
54
+ return normalized;
55
+ }
56
+ export function normalizeDateField(value) {
57
+ if (value instanceof Date) {
58
+ return value.toISOString().slice(0, 10);
59
+ }
60
+ if (typeof value === "string" && value.trim()) {
61
+ const dateOnly = value.trim().slice(0, 10);
62
+ if (/^\d{4}-\d{2}-\d{2}$/.test(dateOnly)) {
63
+ return dateOnly;
64
+ }
65
+ }
66
+ return null;
67
+ }
68
+ function normalizeReference(value, resolve) {
69
+ const trimmed = value.trim();
70
+ if (resolve === "nodeId") {
71
+ return trimmed;
72
+ }
73
+ return trimmed
74
+ .replace(/^\.?\//, "")
75
+ .replace(/^pages\//, "")
76
+ .split(path.sep)
77
+ .join("/");
78
+ }
79
+ function extractFirstParagraph(body) {
80
+ const cleaned = body
81
+ .replace(/```[\s\S]*?```/g, "")
82
+ .split(/\r?\n\r?\n/)
83
+ .map((chunk) => chunk.trim())
84
+ .find((chunk) => chunk && !chunk.startsWith("#"));
85
+ if (!cleaned) {
86
+ return "";
87
+ }
88
+ return cleaned.replace(/\s+/g, " ").slice(0, 200);
89
+ }
90
+ function valueToSummaryLine(fieldName, value) {
91
+ if (value === undefined || value === null || value === "") {
92
+ return null;
93
+ }
94
+ if (Array.isArray(value) && value.length === 0) {
95
+ return null;
96
+ }
97
+ const label = humanizeFieldName(fieldName);
98
+ const content = Array.isArray(value)
99
+ ? value.map((item) => String(item)).join(", ")
100
+ : typeof value === "object"
101
+ ? JSON.stringify(value)
102
+ : String(value);
103
+ return `${label}: ${content}`;
104
+ }
105
+ function buildSummaryText(pageType, title, tags, data, config, templateFields, body) {
106
+ const lines = [`[${pageType}] ${title}`];
107
+ if (tags.length > 0) {
108
+ lines.push(`标签: ${tags.join(", ")}`);
109
+ }
110
+ for (const field of Object.keys(config.customColumns)) {
111
+ const line = valueToSummaryLine(field, data[field]);
112
+ if (line) {
113
+ lines.push(line);
114
+ }
115
+ }
116
+ const seen = new Set(["title", "tags"]);
117
+ for (const field of [...config.defaultSummaryFields, ...templateFields]) {
118
+ if (seen.has(field)) {
119
+ continue;
120
+ }
121
+ seen.add(field);
122
+ const line = valueToSummaryLine(field, data[field]);
123
+ if (line) {
124
+ lines.push(line);
125
+ }
126
+ }
127
+ const bodyPreview = extractFirstParagraph(body);
128
+ if (bodyPreview) {
129
+ lines.push("---", bodyPreview);
130
+ }
131
+ return lines.join("\n");
132
+ }
133
+ function buildEdges(page, data, config, pageType) {
134
+ const template = getTemplate(config, pageType);
135
+ const source = page.nodeId ?? page.id;
136
+ const rules = [
137
+ ...Object.entries(config.commonEdges),
138
+ ...Object.entries(template.edges),
139
+ ];
140
+ const edges = [];
141
+ for (const [field, rule] of rules) {
142
+ const rawValues = normalizeStringArray(data[field]);
143
+ const matcher = rule.match ? new RegExp(rule.match) : null;
144
+ for (const value of rawValues) {
145
+ if (matcher && !matcher.test(value)) {
146
+ continue;
147
+ }
148
+ const target = normalizeReference(value, rule.resolve);
149
+ if (!target) {
150
+ continue;
151
+ }
152
+ if (rule.resolve === "path" && target.startsWith("vault/")) {
153
+ continue;
154
+ }
155
+ edges.push({
156
+ source,
157
+ target,
158
+ edgeType: rule.edgeType,
159
+ sourcePage: page.id,
160
+ metadata: { field },
161
+ });
162
+ }
163
+ }
164
+ return edges;
165
+ }
166
+ function buildExtraAndColumns(data, config, pageType) {
167
+ const template = getTemplate(config, pageType);
168
+ const columnValues = {};
169
+ const extra = {};
170
+ const registeredFields = new Set([
171
+ ...FIXED_FIELDS,
172
+ ...Object.keys(config.customColumns),
173
+ ...Object.keys(template.columns),
174
+ ...Object.keys(config.commonEdges),
175
+ ...Object.keys(template.edges),
176
+ ]);
177
+ for (const [field, value] of Object.entries(data)) {
178
+ if (config.customColumns[field] || template.columns[field]) {
179
+ columnValues[camelToSnake(field)] = normalizeColumnValue(value);
180
+ continue;
181
+ }
182
+ if (FIXED_FIELDS.has(field)) {
183
+ continue;
184
+ }
185
+ extra[field] = normalizeScalar(value);
186
+ }
187
+ const unregisteredFields = Object.keys(data)
188
+ .filter((field) => !registeredFields.has(field))
189
+ .sort();
190
+ return { columnValues, extra, unregisteredFields };
191
+ }
192
+ export function parsePage(filePath, wikiPath, config) {
193
+ const pageId = normalizePageId(filePath, wikiPath);
194
+ const today = toDateOnly();
195
+ let parsedMatter;
196
+ try {
197
+ parsedMatter = matter.read(filePath);
198
+ }
199
+ catch (error) {
200
+ return {
201
+ ok: false,
202
+ error: {
203
+ filePath,
204
+ code: "yaml_parse_error",
205
+ message: `Failed to parse frontmatter for ${pageId}`,
206
+ details: error instanceof Error ? error.message : String(error),
207
+ },
208
+ };
209
+ }
210
+ if (!isPlainObject(parsedMatter.data)) {
211
+ return {
212
+ ok: false,
213
+ error: {
214
+ filePath,
215
+ code: "invalid_frontmatter",
216
+ message: `Frontmatter must be a YAML object for ${pageId}`,
217
+ },
218
+ };
219
+ }
220
+ const data = parsedMatter.data;
221
+ const pageType = typeof data.pageType === "string" ? data.pageType.trim() : "";
222
+ const title = typeof data.title === "string" ? data.title.trim() : "";
223
+ if (!pageType) {
224
+ return {
225
+ ok: false,
226
+ error: {
227
+ filePath,
228
+ code: "missing_page_type",
229
+ message: `Missing pageType in ${pageId}`,
230
+ },
231
+ };
232
+ }
233
+ if (!config.templates[pageType]) {
234
+ return {
235
+ ok: false,
236
+ error: {
237
+ filePath,
238
+ code: "unknown_page_type",
239
+ message: `Unknown pageType "${pageType}" in ${pageId}`,
240
+ },
241
+ };
242
+ }
243
+ if (!title) {
244
+ return {
245
+ ok: false,
246
+ error: {
247
+ filePath,
248
+ code: "missing_title",
249
+ message: `Missing title in ${pageId}`,
250
+ },
251
+ };
252
+ }
253
+ const tags = normalizeStringArray(data.tags);
254
+ const { columnValues, extra, unregisteredFields } = buildExtraAndColumns(data, config, pageType);
255
+ const page = {
256
+ id: pageId,
257
+ nodeId: typeof data.nodeId === "string" && data.nodeId.trim() ? data.nodeId.trim() : null,
258
+ title,
259
+ pageType,
260
+ status: typeof data.status === "string" && data.status.trim() ? data.status.trim() : "draft",
261
+ visibility: typeof data.visibility === "string" && data.visibility.trim() ? data.visibility.trim() : "private",
262
+ tags,
263
+ extra,
264
+ filePath: path.resolve(filePath),
265
+ contentHash: null,
266
+ summaryText: "",
267
+ embeddingStatus: "pending",
268
+ fileMtime: null,
269
+ createdAt: normalizeDateField(data.createdAt) ?? today,
270
+ updatedAt: normalizeDateField(data.updatedAt) ?? today,
271
+ indexedAt: null,
272
+ };
273
+ const template = getTemplate(config, pageType);
274
+ const summaryText = buildSummaryText(pageType, title, tags, data, config, template.summaryFields, parsedMatter.content);
275
+ page.summaryText = summaryText;
276
+ const edges = buildEdges(page, data, config, pageType);
277
+ const parsed = {
278
+ page,
279
+ columnValues,
280
+ edges,
281
+ summaryText,
282
+ body: parsedMatter.content,
283
+ rawData: data,
284
+ unregisteredFields,
285
+ };
286
+ return { ok: true, parsed };
287
+ }
@@ -0,0 +1,241 @@
1
+ import { normalizeDateField, parsePage } from "./frontmatter.js";
2
+ import { rebuildFts } from "./db.js";
3
+ import { normalizePageId, resolvePagePath } from "./paths.js";
4
+ import { fileStatSync, listFilesRecursiveSync, pathExistsSync, sha256FileSync } from "../utils/fs.js";
5
+ import { toDateOnly, toOffsetIso } from "../utils/time.js";
6
+ function getExistingPages(db) {
7
+ const rows = db
8
+ .prepare("SELECT id, file_path AS filePath, content_hash AS contentHash FROM pages")
9
+ .all();
10
+ return new Map(rows.map((row) => [row.id, { filePath: row.filePath, contentHash: row.contentHash }]));
11
+ }
12
+ function makeScanEntry(filePath, wikiPath) {
13
+ return {
14
+ id: normalizePageId(filePath, wikiPath),
15
+ filePath,
16
+ contentHash: sha256FileSync(filePath),
17
+ fileMtime: Number(new Date().getTime()),
18
+ };
19
+ }
20
+ function createScanEntry(filePath, wikiPath) {
21
+ const entry = makeScanEntry(filePath, wikiPath);
22
+ const stats = fileStatSync(filePath);
23
+ return { ...entry, fileMtime: stats.mtimeMs };
24
+ }
25
+ export function scanPages(db, wikiPath, configChanged, force = false) {
26
+ const existing = getExistingPages(db);
27
+ const currentEntries = listFilesRecursiveSync(wikiPath, ".md").map((filePath) => createScanEntry(filePath, wikiPath));
28
+ const seenIds = new Set();
29
+ const added = [];
30
+ const modified = [];
31
+ const unchanged = [];
32
+ for (const entry of currentEntries) {
33
+ seenIds.add(entry.id);
34
+ const existingEntry = existing.get(entry.id);
35
+ if (!existingEntry) {
36
+ added.push(entry);
37
+ continue;
38
+ }
39
+ if (force || configChanged || existingEntry.contentHash !== entry.contentHash) {
40
+ modified.push(entry);
41
+ continue;
42
+ }
43
+ unchanged.push(entry);
44
+ }
45
+ const deleted = [...existing.entries()]
46
+ .filter(([pageId]) => !seenIds.has(pageId))
47
+ .map(([id, value]) => ({ id, filePath: value.filePath }))
48
+ .sort((left, right) => left.id.localeCompare(right.id));
49
+ return { added, modified, deleted, unchanged };
50
+ }
51
+ export function scanSpecificPages(db, wikiPath, pageIdsOrPaths, configChanged, force = false) {
52
+ const existing = getExistingPages(db);
53
+ const requestedIds = [...new Set(pageIdsOrPaths.map((value) => normalizePageId(value, wikiPath)))];
54
+ const added = [];
55
+ const modified = [];
56
+ const unchanged = [];
57
+ const deleted = [];
58
+ for (const pageId of requestedIds) {
59
+ const filePath = resolvePagePath(pageId, wikiPath);
60
+ const existingEntry = existing.get(pageId);
61
+ if (!pathExistsSync(filePath)) {
62
+ if (existingEntry) {
63
+ deleted.push({ id: pageId, filePath: existingEntry.filePath });
64
+ }
65
+ continue;
66
+ }
67
+ const entry = createScanEntry(filePath, wikiPath);
68
+ if (!existingEntry) {
69
+ added.push(entry);
70
+ continue;
71
+ }
72
+ if (force || configChanged || existingEntry.contentHash !== entry.contentHash) {
73
+ modified.push(entry);
74
+ continue;
75
+ }
76
+ unchanged.push(entry);
77
+ }
78
+ return { added, modified, deleted, unchanged };
79
+ }
80
+ function serializeRow(parsed, entry, config, previousEmbeddingStatus, summaryChanged) {
81
+ const row = {
82
+ id: parsed.page.id,
83
+ node_id: parsed.page.nodeId,
84
+ title: parsed.page.title,
85
+ page_type: parsed.page.pageType,
86
+ status: parsed.page.status,
87
+ visibility: parsed.page.visibility,
88
+ tags: JSON.stringify(parsed.page.tags),
89
+ extra: JSON.stringify(parsed.page.extra),
90
+ file_path: parsed.page.filePath,
91
+ content_hash: entry.contentHash,
92
+ summary_text: parsed.summaryText,
93
+ embedding_status: summaryChanged || !previousEmbeddingStatus ? "pending" : previousEmbeddingStatus,
94
+ file_mtime: entry.fileMtime,
95
+ created_at: parsed.page.createdAt,
96
+ updated_at: parsed.page.updatedAt,
97
+ indexed_at: toOffsetIso(),
98
+ };
99
+ for (const columnName of config.allColumnNames) {
100
+ row[columnName] = parsed.columnValues[columnName] ?? null;
101
+ }
102
+ return row;
103
+ }
104
+ function buildInsertStatement(config) {
105
+ const columns = [
106
+ "id",
107
+ "node_id",
108
+ "title",
109
+ "page_type",
110
+ "status",
111
+ "visibility",
112
+ "tags",
113
+ ...config.allColumnNames,
114
+ "extra",
115
+ "file_path",
116
+ "content_hash",
117
+ "summary_text",
118
+ "embedding_status",
119
+ "file_mtime",
120
+ "created_at",
121
+ "updated_at",
122
+ "indexed_at",
123
+ ];
124
+ const placeholders = columns.map((column) => `@${column}`);
125
+ return `INSERT INTO pages (${columns.join(", ")}) VALUES (${placeholders.join(", ")})`;
126
+ }
127
+ function buildUpdateStatement(config) {
128
+ const columns = [
129
+ "node_id",
130
+ "title",
131
+ "page_type",
132
+ "status",
133
+ "visibility",
134
+ "tags",
135
+ ...config.allColumnNames,
136
+ "extra",
137
+ "file_path",
138
+ "content_hash",
139
+ "summary_text",
140
+ "embedding_status",
141
+ "file_mtime",
142
+ "created_at",
143
+ "updated_at",
144
+ "indexed_at",
145
+ ];
146
+ return `UPDATE pages SET ${columns.map((column) => `${column} = @${column}`).join(", ")} WHERE id = @id`;
147
+ }
148
+ export function applyChanges(db, changes, wikiPath, config) {
149
+ const parseResults = [...changes.added, ...changes.modified].map((entry) => ({
150
+ entry,
151
+ result: parsePage(entry.filePath, wikiPath, config),
152
+ }));
153
+ const parseErrors = parseResults
154
+ .filter((item) => !item.result.ok)
155
+ .map((item) => (item.result.ok ? null : item.result.error))
156
+ .filter((item) => item !== null);
157
+ if (parseErrors.length > 0) {
158
+ return {
159
+ inserted: [],
160
+ updated: [],
161
+ deleted: [],
162
+ summaryChangedIds: [],
163
+ parseErrors,
164
+ };
165
+ }
166
+ const parsedEntries = parseResults.map((item) => ({
167
+ entry: item.entry,
168
+ parsed: item.result.parsed,
169
+ }));
170
+ const insertStatement = db.prepare(buildInsertStatement(config));
171
+ const updateStatement = db.prepare(buildUpdateStatement(config));
172
+ const selectExistingPage = db.prepare("SELECT rowid, summary_text AS summaryText, embedding_status AS embeddingStatus, created_at AS createdAt FROM pages WHERE id = ?");
173
+ const deleteEdgesBySourcePage = db.prepare("DELETE FROM edges WHERE source_page = ?");
174
+ const insertEdge = db.prepare(`
175
+ INSERT OR REPLACE INTO edges(source, target, edge_type, source_page, metadata)
176
+ VALUES(@source, @target, @edge_type, @source_page, @metadata)
177
+ `);
178
+ const deletePage = db.prepare("DELETE FROM pages WHERE id = ?");
179
+ const deleteVecRow = db.prepare("DELETE FROM vec_pages WHERE page_rowid = ?");
180
+ const selectPageRowid = db.prepare("SELECT rowid FROM pages WHERE id = ?");
181
+ const inserted = [];
182
+ const updated = [];
183
+ const deleted = [];
184
+ const summaryChangedIds = [];
185
+ const hasContentChanges = parsedEntries.length > 0 || changes.deleted.length > 0;
186
+ const transaction = db.transaction(() => {
187
+ for (const { entry, parsed } of parsedEntries) {
188
+ const existing = selectExistingPage.get(entry.id);
189
+ const isInsert = !existing;
190
+ const summaryChanged = isInsert || existing.summaryText !== parsed.summaryText;
191
+ const today = toDateOnly();
192
+ parsed.page.createdAt =
193
+ normalizeDateField(parsed.rawData.createdAt) ?? existing?.createdAt ?? today;
194
+ parsed.page.updatedAt = isInsert
195
+ ? normalizeDateField(parsed.rawData.updatedAt) ?? today
196
+ : today;
197
+ const row = serializeRow(parsed, entry, config, existing?.embeddingStatus ?? null, summaryChanged);
198
+ if (isInsert) {
199
+ insertStatement.run(row);
200
+ inserted.push(entry.id);
201
+ }
202
+ else {
203
+ updateStatement.run(row);
204
+ updated.push(entry.id);
205
+ }
206
+ if (summaryChanged) {
207
+ summaryChangedIds.push(entry.id);
208
+ }
209
+ deleteEdgesBySourcePage.run(entry.id);
210
+ for (const edge of parsed.edges) {
211
+ insertEdge.run({
212
+ source: edge.source,
213
+ target: edge.target,
214
+ edge_type: edge.edgeType,
215
+ source_page: edge.sourcePage,
216
+ metadata: JSON.stringify(edge.metadata),
217
+ });
218
+ }
219
+ }
220
+ for (const page of changes.deleted) {
221
+ const existing = selectPageRowid.get(page.id);
222
+ deleteEdgesBySourcePage.run(page.id);
223
+ if (existing) {
224
+ deleteVecRow.run(BigInt(existing.rowid));
225
+ }
226
+ deletePage.run(page.id);
227
+ deleted.push(page.id);
228
+ }
229
+ if (hasContentChanges) {
230
+ rebuildFts(db);
231
+ }
232
+ });
233
+ transaction();
234
+ return {
235
+ inserted,
236
+ updated,
237
+ deleted,
238
+ summaryChangedIds,
239
+ parseErrors: [],
240
+ };
241
+ }