@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,91 @@
1
+ import matter from "gray-matter";
2
+ import path from "node:path";
3
+ import { getTemplate, resolveTemplateFilePath } from "./config.js";
4
+ import { AppError } from "../utils/errors.js";
5
+ import { ensureDirSync, pathExistsSync, readTextFileSync, writeTextFileSync } from "../utils/fs.js";
6
+ import { slugify } from "../utils/slug.js";
7
+ import { toDateOnly } from "../utils/time.js";
8
+ export const PAGE_TYPE_DIRS = {
9
+ concept: "concepts",
10
+ misconception: "misconceptions",
11
+ bridge: "bridges",
12
+ "source-summary": "source-summaries",
13
+ lesson: "lessons",
14
+ method: "methods",
15
+ person: "people",
16
+ achievement: "achievements",
17
+ resume: "resumes",
18
+ "research-note": "research-notes",
19
+ faq: "faqs",
20
+ };
21
+ function makeUniquePagePath(baseDir, slug) {
22
+ let candidate = path.join(baseDir, `${slug}.md`);
23
+ let counter = 2;
24
+ while (pathExistsSync(candidate)) {
25
+ candidate = path.join(baseDir, `${slug}-${counter}.md`);
26
+ counter += 1;
27
+ }
28
+ return candidate;
29
+ }
30
+ function normalizeFrontmatterArrays(patch, templateData) {
31
+ return {
32
+ sourceRefs: patch.sourceRefs ?? templateData.sourceRefs ?? [],
33
+ relatedPages: patch.relatedPages ?? templateData.relatedPages ?? [],
34
+ tags: patch.tags ?? templateData.tags ?? [],
35
+ };
36
+ }
37
+ export function createPageFromTemplate(paths, config, options) {
38
+ const { pageType, title } = options;
39
+ getTemplate(config, pageType);
40
+ const templatePath = resolveTemplateFilePath(config, paths.wikiRoot, pageType);
41
+ if (!pathExistsSync(templatePath)) {
42
+ throw new AppError(`Template file not found: ${templatePath}`, "not_found");
43
+ }
44
+ const template = matter(readTextFileSync(templatePath));
45
+ const slug = slugify(title);
46
+ const targetDir = path.join(paths.wikiPath, PAGE_TYPE_DIRS[pageType] ?? `${pageType}s`);
47
+ ensureDirSync(targetDir);
48
+ const filePath = makeUniquePagePath(targetDir, slug);
49
+ const pageId = path.relative(paths.wikiPath, filePath).split(path.sep).join("/");
50
+ const today = toDateOnly();
51
+ const patch = options.frontmatterPatch ?? {};
52
+ const templateData = (template.data ?? {});
53
+ const data = {
54
+ ...templateData,
55
+ ...patch,
56
+ pageType,
57
+ title,
58
+ nodeId: options.nodeId !== undefined
59
+ ? options.nodeId
60
+ : patch.nodeId !== undefined
61
+ ? patch.nodeId
62
+ : slug,
63
+ status: patch.status ?? templateData.status ?? "draft",
64
+ visibility: patch.visibility ?? templateData.visibility ?? "private",
65
+ ...normalizeFrontmatterArrays(patch, templateData),
66
+ createdAt: patch.createdAt ?? templateData.createdAt ?? today,
67
+ updatedAt: patch.updatedAt ?? templateData.updatedAt ?? today,
68
+ };
69
+ const output = matter.stringify(options.bodyMarkdown ?? template.content, data);
70
+ writeTextFileSync(filePath, output);
71
+ return { pageId, filePath };
72
+ }
73
+ export function updatePageById(paths, pageId, options) {
74
+ const filePath = path.join(paths.wikiPath, ...pageId.split("/"));
75
+ if (!pathExistsSync(filePath)) {
76
+ throw new AppError(`Page file not found: ${pageId}`, "not_found");
77
+ }
78
+ const existing = matter(readTextFileSync(filePath));
79
+ const patch = options.frontmatterPatch ?? {};
80
+ const currentData = (existing.data ?? {});
81
+ const today = toDateOnly();
82
+ const nextData = {
83
+ ...currentData,
84
+ ...patch,
85
+ ...normalizeFrontmatterArrays(patch, currentData),
86
+ updatedAt: patch.updatedAt ?? today,
87
+ };
88
+ const output = matter.stringify(options.bodyMarkdown ?? existing.content, nextData);
89
+ writeTextFileSync(filePath, output);
90
+ return { pageId, filePath };
91
+ }
@@ -0,0 +1,161 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import path from "node:path";
3
+ import { AppError } from "../utils/errors.js";
4
+ const TRUE_VALUES = new Set(["1", "true", "yes", "on"]);
5
+ const FALSE_VALUES = new Set(["0", "false", "no", "off"]);
6
+ function parseBooleanFlag(label, rawValue, defaultValue) {
7
+ if (rawValue === undefined) {
8
+ return defaultValue;
9
+ }
10
+ const normalized = rawValue.trim().toLowerCase();
11
+ if (TRUE_VALUES.has(normalized)) {
12
+ return true;
13
+ }
14
+ if (FALSE_VALUES.has(normalized)) {
15
+ return false;
16
+ }
17
+ throw new AppError(`${label} must be a boolean value, got ${rawValue}`, "config");
18
+ }
19
+ function parseSyncInterval(raw) {
20
+ if (!raw) {
21
+ return 86_400;
22
+ }
23
+ const value = Number.parseInt(raw, 10);
24
+ if (!Number.isFinite(value) || value < 0) {
25
+ throw new AppError(`WIKI_SYNC_INTERVAL must be a non-negative integer, got ${raw}`, "config");
26
+ }
27
+ return value;
28
+ }
29
+ function parseNonNegativeInteger(raw, defaultValue, label) {
30
+ if (!raw) {
31
+ return defaultValue;
32
+ }
33
+ const value = Number.parseInt(raw, 10);
34
+ if (!Number.isFinite(value) || value < 0) {
35
+ throw new AppError(`${label} must be a non-negative integer, got ${raw}`, "config");
36
+ }
37
+ return value;
38
+ }
39
+ function parsePositiveInteger(raw, defaultValue, label) {
40
+ if (!raw) {
41
+ return defaultValue;
42
+ }
43
+ const value = Number.parseInt(raw, 10);
44
+ if (!Number.isFinite(value) || value <= 0) {
45
+ throw new AppError(`${label} must be a positive integer, got ${raw}`, "config");
46
+ }
47
+ return value;
48
+ }
49
+ function parseOptionalPort(raw, label) {
50
+ if (!raw || !raw.trim()) {
51
+ return null;
52
+ }
53
+ const value = Number.parseInt(raw, 10);
54
+ if (!Number.isFinite(value) || value < 1 || value > 65_535) {
55
+ throw new AppError(`${label} must be an integer between 1 and 65535, got ${raw}`, "config");
56
+ }
57
+ return value;
58
+ }
59
+ function normalizeOptionalUrl(rawValue) {
60
+ const value = rawValue?.trim();
61
+ if (!value) {
62
+ return null;
63
+ }
64
+ if (!/^https?:\/\//i.test(value)) {
65
+ throw new AppError(`WIKI_AGENT_BASE_URL must start with http:// or https://: ${rawValue}`, "config");
66
+ }
67
+ return value.replace(/\/+$/g, "");
68
+ }
69
+ function requireAbsolutePath(label, rawValue) {
70
+ if (!rawValue) {
71
+ throw new AppError(`${label} is required`, "config");
72
+ }
73
+ if (!path.isAbsolute(rawValue)) {
74
+ throw new AppError(`${label} must be an absolute path: ${rawValue}`, "config");
75
+ }
76
+ return path.resolve(rawValue);
77
+ }
78
+ export function parseVaultHashMode(raw) {
79
+ const value = (raw ?? "content").trim().toLowerCase();
80
+ if (value === "content" || value === "mtime") {
81
+ return value;
82
+ }
83
+ throw new AppError(`VAULT_HASH_MODE must be "content" or "mtime", got ${raw}`, "config");
84
+ }
85
+ export function parseWikiAgentBackend(raw) {
86
+ const value = (raw ?? "codex-workflow").trim().toLowerCase();
87
+ if (value === "codex-workflow") {
88
+ return value;
89
+ }
90
+ throw new AppError(`WIKI_AGENT_BACKEND must be "codex-workflow", got ${raw}`, "config");
91
+ }
92
+ export function resolveAgentSettings(env = process.env, options = {}) {
93
+ const enabled = parseBooleanFlag("WIKI_AGENT_ENABLED", env.WIKI_AGENT_ENABLED, false);
94
+ const baseUrl = normalizeOptionalUrl(env.WIKI_AGENT_BASE_URL);
95
+ const apiKey = env.WIKI_AGENT_API_KEY?.trim() || null;
96
+ const model = env.WIKI_AGENT_MODEL?.trim() || null;
97
+ const batchSize = parseNonNegativeInteger(env.WIKI_AGENT_BATCH_SIZE, 5, "WIKI_AGENT_BATCH_SIZE");
98
+ const workflowTimeoutSeconds = parsePositiveInteger(env.WIKI_WORKFLOW_TIMEOUT, 600, "WIKI_WORKFLOW_TIMEOUT");
99
+ const missing = [];
100
+ if (enabled) {
101
+ if (!apiKey) {
102
+ missing.push("WIKI_AGENT_API_KEY");
103
+ }
104
+ if (!model) {
105
+ missing.push("WIKI_AGENT_MODEL");
106
+ }
107
+ }
108
+ if (options.strict && enabled && missing.length > 0) {
109
+ throw new AppError(`WIKI_AGENT_ENABLED=true but missing required settings: ${missing.join(", ")}`, "config");
110
+ }
111
+ return {
112
+ enabled,
113
+ baseUrl,
114
+ apiKey,
115
+ model,
116
+ batchSize,
117
+ workflowTimeoutSeconds,
118
+ configured: enabled && missing.length === 0,
119
+ missing,
120
+ };
121
+ }
122
+ export function getPackageRoot() {
123
+ return path.resolve(fileURLToPath(new URL("../../", import.meta.url)));
124
+ }
125
+ export function resolveRuntimePaths(env = process.env) {
126
+ const wikiPath = requireAbsolutePath("WIKI_PATH", env.WIKI_PATH);
127
+ const wikiRoot = path.resolve(wikiPath, "..");
128
+ const vaultPath = path.resolve(env.VAULT_PATH ?? path.join(wikiRoot, "..", "vault"));
129
+ const dbPath = path.resolve(env.WIKI_DB_PATH ?? path.join(wikiRoot, "index.db"));
130
+ const configPath = path.resolve(env.WIKI_CONFIG_PATH ?? path.join(wikiRoot, "wiki.config.json"));
131
+ const templatesPath = path.resolve(env.WIKI_TEMPLATES_PATH ?? path.join(wikiRoot, "templates"));
132
+ return {
133
+ wikiPath,
134
+ wikiRoot,
135
+ vaultPath,
136
+ vaultHashMode: parseVaultHashMode(env.VAULT_HASH_MODE),
137
+ agentBackend: parseWikiAgentBackend(env.WIKI_AGENT_BACKEND),
138
+ dbPath,
139
+ configPath,
140
+ templatesPath,
141
+ queueArtifactsPath: path.join(wikiRoot, ".queue-artifacts"),
142
+ packageRoot: getPackageRoot(),
143
+ syncIntervalSeconds: parseSyncInterval(env.WIKI_SYNC_INTERVAL),
144
+ daemonHost: "127.0.0.1",
145
+ daemonPort: parseOptionalPort(env.WIKI_DAEMON_PORT, "WIKI_DAEMON_PORT"),
146
+ daemonPidPath: path.join(wikiRoot, ".wiki-daemon.pid"),
147
+ daemonLogPath: path.join(wikiRoot, ".wiki-daemon.log"),
148
+ daemonStatePath: path.join(wikiRoot, ".wiki-daemon.state.json"),
149
+ };
150
+ }
151
+ export function normalizePageId(inputPath, wikiPath) {
152
+ const resolved = path.isAbsolute(inputPath) ? inputPath : path.resolve(wikiPath, inputPath);
153
+ const relative = path.relative(wikiPath, resolved);
154
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
155
+ throw new AppError(`Path is outside wiki/pages: ${inputPath}`, "config");
156
+ }
157
+ return relative.split(path.sep).join("/");
158
+ }
159
+ export function resolvePagePath(pageId, wikiPath) {
160
+ return path.join(wikiPath, ...pageId.split("/"));
161
+ }
@@ -0,0 +1,23 @@
1
+ export function compactPageSummary(page, config) {
2
+ const summary = {
3
+ id: page.id,
4
+ title: page.title,
5
+ pageType: page.pageType,
6
+ status: page.status,
7
+ visibility: page.visibility,
8
+ filePath: page.filePath,
9
+ tags: page.tags,
10
+ updatedAt: page.updatedAt,
11
+ };
12
+ if (page.nodeId) {
13
+ summary.nodeId = page.nodeId;
14
+ }
15
+ for (const columnName of config.allColumnNames) {
16
+ const camelName = columnName.replace(/_([a-z0-9])/g, (_, char) => char.toUpperCase());
17
+ const value = page[camelName];
18
+ if (value !== null && value !== undefined && value !== "") {
19
+ summary[camelName] = value;
20
+ }
21
+ }
22
+ return summary;
23
+ }
@@ -0,0 +1,58 @@
1
+ import { snakeToCamel } from "../utils/case.js";
2
+ function parseJsonField(value, fallback) {
3
+ if (typeof value !== "string" || value.trim() === "") {
4
+ return fallback;
5
+ }
6
+ return JSON.parse(value);
7
+ }
8
+ export function mapPageRow(row, config) {
9
+ const result = {
10
+ id: row.id,
11
+ nodeId: row.node_id,
12
+ title: row.title,
13
+ pageType: row.page_type,
14
+ status: row.status,
15
+ visibility: row.visibility,
16
+ tags: parseJsonField(row.tags, []),
17
+ extra: parseJsonField(row.extra, {}),
18
+ filePath: row.file_path,
19
+ contentHash: row.content_hash,
20
+ summaryText: row.summary_text,
21
+ embeddingStatus: row.embedding_status,
22
+ fileMtime: row.file_mtime,
23
+ createdAt: row.created_at,
24
+ updatedAt: row.updated_at,
25
+ indexedAt: row.indexed_at,
26
+ };
27
+ for (const columnName of config.allColumnNames) {
28
+ result[snakeToCamel(columnName)] = row[columnName];
29
+ }
30
+ return result;
31
+ }
32
+ export function listPageColumns(config) {
33
+ return [
34
+ "id",
35
+ "node_id",
36
+ "title",
37
+ "page_type",
38
+ "status",
39
+ "visibility",
40
+ "tags",
41
+ ...config.allColumnNames,
42
+ "extra",
43
+ "file_path",
44
+ "content_hash",
45
+ "summary_text",
46
+ "embedding_status",
47
+ "file_mtime",
48
+ "created_at",
49
+ "updated_at",
50
+ "indexed_at",
51
+ ];
52
+ }
53
+ export function selectPageById(db, config, pageId) {
54
+ const row = db
55
+ .prepare(`SELECT ${listPageColumns(config).join(", ")} FROM pages WHERE id = ?`)
56
+ .get(pageId);
57
+ return row ? mapPageRow(row, config) : null;
58
+ }
@@ -0,0 +1,20 @@
1
+ import { loadConfig } from "./config.js";
2
+ import { openDb } from "./db.js";
3
+ import { EmbeddingClient } from "./embedding.js";
4
+ import { resolveRuntimePaths } from "./paths.js";
5
+ export function getEmbeddingDimensionFromEnv(env = process.env) {
6
+ const raw = env.EMBEDDING_DIMENSIONS ?? "384";
7
+ const value = Number.parseInt(raw, 10);
8
+ return Number.isFinite(value) && value > 0 ? value : 384;
9
+ }
10
+ export function loadRuntimeConfig(env = process.env) {
11
+ const paths = resolveRuntimePaths(env);
12
+ const config = loadConfig(paths.configPath);
13
+ return { paths, config };
14
+ }
15
+ export function openRuntimeDb(env = process.env) {
16
+ const { paths, config } = loadRuntimeConfig(env);
17
+ const embeddingClient = EmbeddingClient.fromEnv(env);
18
+ const { db } = openDb(paths.dbPath, config, embeddingClient?.settings.dimensions ?? getEmbeddingDimensionFromEnv(env));
19
+ return { db, paths, config, embeddingClient };
20
+ }
@@ -0,0 +1,235 @@
1
+ import { loadConfig } from "./config.js";
2
+ import { clearAllIndexedData, getMeta, openDb, resetVectorTable, setMetaValues, } from "./db.js";
3
+ import { EmbeddingClient } from "./embedding.js";
4
+ import { applyChanges, scanPages, scanSpecificPages } from "./indexer.js";
5
+ import { resolveRuntimePaths } from "./paths.js";
6
+ import { collectVaultFiles, syncVaultIndex } from "./vault.js";
7
+ import { AppError } from "../utils/errors.js";
8
+ import { pathExistsSync } from "../utils/fs.js";
9
+ import { makeSyncId, toOffsetIso } from "../utils/time.js";
10
+ function getEmbeddingDimension(env) {
11
+ const raw = env.EMBEDDING_DIMENSIONS ?? "384";
12
+ const value = Number.parseInt(raw, 10);
13
+ return Number.isFinite(value) && value > 0 ? value : 384;
14
+ }
15
+ function getEmbeddingTargets(db, embedAll, insertedIds, summaryChangedIds) {
16
+ const rows = db.prepare("SELECT rowid, id, summary_text AS summaryText, embedding_status AS embeddingStatus FROM pages").all();
17
+ if (embedAll) {
18
+ return rows.map(({ rowid, id, summaryText }) => ({ rowid, id, summaryText }));
19
+ }
20
+ const changedIds = new Set([...insertedIds, ...summaryChangedIds]);
21
+ return rows
22
+ .filter((row) => changedIds.has(row.id) || row.embeddingStatus === "pending" || row.embeddingStatus === "error")
23
+ .map(({ rowid, id, summaryText }) => ({ rowid, id, summaryText }));
24
+ }
25
+ async function embedPages(db, embeddingClient, targets) {
26
+ if (targets.length === 0) {
27
+ return { attempted: 0, succeeded: 0, failed: 0 };
28
+ }
29
+ const upsertVector = db.prepare("INSERT INTO vec_pages(page_rowid, page_id, embedding) VALUES (?, ?, ?)");
30
+ const deleteVector = db.prepare("DELETE FROM vec_pages WHERE page_rowid = ?");
31
+ const updateStatus = db.prepare("UPDATE pages SET embedding_status = ? WHERE id = ?");
32
+ let succeeded = 0;
33
+ let failed = 0;
34
+ for (let index = 0; index < targets.length; index += 50) {
35
+ const batch = targets.slice(index, index + 50);
36
+ try {
37
+ const embeddings = await embeddingClient.embedBatch(batch.map((item) => item.summaryText));
38
+ const transaction = db.transaction(() => {
39
+ for (let offset = 0; offset < batch.length; offset += 1) {
40
+ deleteVector.run(BigInt(batch[offset].rowid));
41
+ upsertVector.run(BigInt(batch[offset].rowid), batch[offset].id, new Float32Array(embeddings[offset]));
42
+ updateStatus.run("done", batch[offset].id);
43
+ }
44
+ });
45
+ transaction();
46
+ succeeded += batch.length;
47
+ }
48
+ catch (error) {
49
+ const transaction = db.transaction(() => {
50
+ for (const item of batch) {
51
+ updateStatus.run("error", item.id);
52
+ }
53
+ });
54
+ transaction();
55
+ failed += batch.length;
56
+ if (targets.length === batch.length) {
57
+ throw error;
58
+ }
59
+ }
60
+ }
61
+ return {
62
+ attempted: targets.length,
63
+ succeeded,
64
+ failed,
65
+ };
66
+ }
67
+ function countPendingEmbeddings(db) {
68
+ const row = db
69
+ .prepare("SELECT COUNT(*) AS count FROM pages WHERE embedding_status IN ('pending', 'error')")
70
+ .get();
71
+ return row.count;
72
+ }
73
+ export async function syncWorkspace(options = {}) {
74
+ const env = options.env ?? process.env;
75
+ const runtimePaths = resolveRuntimePaths(env);
76
+ if (!pathExistsSync(runtimePaths.wikiPath)) {
77
+ throw new AppError(`WIKI_PATH does not exist: ${runtimePaths.wikiPath}`, "config");
78
+ }
79
+ const config = loadConfig(runtimePaths.configPath);
80
+ const embeddingClient = EmbeddingClient.fromEnv(env);
81
+ const { db, configChanged } = openDb(runtimePaths.dbPath, config, embeddingClient?.settings.dimensions ?? getEmbeddingDimension(env));
82
+ try {
83
+ let mode = options.targetPaths && options.targetPaths.length > 0 && !options.force ? "path" : "full";
84
+ let upgradedToFullSync = false;
85
+ const storedEmbeddingProfile = embeddingClient ? getMeta(db, "embedding_profile") : null;
86
+ const profileChanged = Boolean(embeddingClient && storedEmbeddingProfile && storedEmbeddingProfile !== embeddingClient.profileHash);
87
+ if (mode === "path" && (configChanged || profileChanged)) {
88
+ mode = "full";
89
+ upgradedToFullSync = true;
90
+ }
91
+ if (profileChanged && options.skipEmbedding) {
92
+ throw new AppError("Embedding profile changed, cannot skip embedding.", "config");
93
+ }
94
+ if (options.force) {
95
+ clearAllIndexedData(db);
96
+ mode = "full";
97
+ }
98
+ const changes = mode === "path"
99
+ ? scanSpecificPages(db, runtimePaths.wikiPath, options.targetPaths ?? [], configChanged, false)
100
+ : scanPages(db, runtimePaths.wikiPath, configChanged, false);
101
+ if (mode === "path" &&
102
+ changes.added.length === 0 &&
103
+ changes.modified.length === 0 &&
104
+ changes.deleted.length === 0 &&
105
+ changes.unchanged.length === 0) {
106
+ throw new AppError(`No page matched the requested --path value(s).`, "not_found");
107
+ }
108
+ const applyResult = applyChanges(db, changes, runtimePaths.wikiPath, config);
109
+ if (applyResult.parseErrors.length > 0) {
110
+ throw new AppError("Failed to parse one or more wiki pages during sync.", "runtime", {
111
+ parseErrors: applyResult.parseErrors,
112
+ });
113
+ }
114
+ let embedAll = false;
115
+ if (embeddingClient && profileChanged) {
116
+ resetVectorTable(db, embeddingClient.settings.dimensions);
117
+ db.prepare("UPDATE pages SET embedding_status = 'pending'").run();
118
+ embedAll = true;
119
+ }
120
+ let embeddingAttempted = 0;
121
+ let embeddingSucceeded = 0;
122
+ let embeddingFailed = 0;
123
+ const skipEmbedding = options.skipEmbedding === true || embeddingClient === null;
124
+ if (embeddingClient && !skipEmbedding) {
125
+ const targets = getEmbeddingTargets(db, embedAll, applyResult.inserted, applyResult.summaryChangedIds);
126
+ const embeddingResult = await embedPages(db, embeddingClient, targets);
127
+ embeddingAttempted = embeddingResult.attempted;
128
+ embeddingSucceeded = embeddingResult.succeeded;
129
+ embeddingFailed = embeddingResult.failed;
130
+ const hasPending = countPendingEmbeddings(db) > 0;
131
+ if (!hasPending) {
132
+ setMetaValues(db, {
133
+ embedding_profile: embeddingClient.profileHash,
134
+ });
135
+ }
136
+ }
137
+ let vaultFiles = 0;
138
+ let vaultChanges = 0;
139
+ let syncId = null;
140
+ let vaultQueue = {
141
+ pendingAdded: 0,
142
+ pendingReset: 0,
143
+ removed: 0,
144
+ };
145
+ if (mode === "full") {
146
+ syncId = makeSyncId();
147
+ const currentVaultFiles = await collectVaultFiles(runtimePaths.vaultPath, config.vaultFileTypes, env);
148
+ const vaultResult = syncVaultIndex(db, currentVaultFiles, syncId);
149
+ vaultFiles = vaultResult.files;
150
+ vaultChanges = vaultResult.changes.length;
151
+ vaultQueue = vaultResult.queue;
152
+ }
153
+ setMetaValues(db, {
154
+ config_version: config.configVersion,
155
+ last_sync_at: toOffsetIso(),
156
+ ...(mode === "full" ? { last_sync_id: syncId } : {}),
157
+ ...(options.force ? { last_full_rebuild_at: toOffsetIso() } : {}),
158
+ });
159
+ return {
160
+ mode,
161
+ upgradedToFullSync,
162
+ configChanged,
163
+ profileChanged,
164
+ inserted: applyResult.inserted.length,
165
+ updated: applyResult.updated.length,
166
+ deleted: applyResult.deleted.length,
167
+ unchanged: changes.unchanged.length,
168
+ summaryChanged: applyResult.summaryChangedIds.length,
169
+ embedding: {
170
+ enabled: embeddingClient !== null,
171
+ skipped: skipEmbedding,
172
+ attempted: embeddingAttempted,
173
+ succeeded: embeddingSucceeded,
174
+ failed: embeddingFailed,
175
+ embedAll,
176
+ },
177
+ vault: {
178
+ scanned: mode === "full",
179
+ files: vaultFiles,
180
+ changes: vaultChanges,
181
+ syncId,
182
+ queue: vaultQueue,
183
+ },
184
+ };
185
+ }
186
+ finally {
187
+ db.close();
188
+ }
189
+ }
190
+ export async function embedPendingPages(env = process.env) {
191
+ const runtimePaths = resolveRuntimePaths(env);
192
+ const config = loadConfig(runtimePaths.configPath);
193
+ const embeddingClient = EmbeddingClient.fromEnv(env);
194
+ if (!embeddingClient) {
195
+ return;
196
+ }
197
+ const { db } = openDb(runtimePaths.dbPath, config, embeddingClient.settings.dimensions);
198
+ try {
199
+ const targets = getEmbeddingTargets(db, false, [], []);
200
+ const result = await embedPages(db, embeddingClient, targets);
201
+ if (result.failed === 0 && countPendingEmbeddings(db) === 0) {
202
+ setMetaValues(db, { embedding_profile: embeddingClient.profileHash });
203
+ }
204
+ }
205
+ finally {
206
+ db.close();
207
+ }
208
+ }
209
+ export function readAllPages(db) {
210
+ const rows = db.prepare(`
211
+ SELECT
212
+ id,
213
+ node_id AS nodeId,
214
+ title,
215
+ page_type AS pageType,
216
+ status,
217
+ visibility,
218
+ tags,
219
+ extra,
220
+ file_path AS filePath,
221
+ content_hash AS contentHash,
222
+ summary_text AS summaryText,
223
+ embedding_status AS embeddingStatus,
224
+ file_mtime AS fileMtime,
225
+ created_at AS createdAt,
226
+ updated_at AS updatedAt,
227
+ indexed_at AS indexedAt
228
+ FROM pages
229
+ `).all();
230
+ return rows.map((row) => ({
231
+ ...row,
232
+ tags: row.tags ? JSON.parse(row.tags) : [],
233
+ extra: row.extra ? JSON.parse(row.extra) : {},
234
+ }));
235
+ }