@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,967 @@
1
+ import { accessSync, constants, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { createInterface } from "node:readline/promises";
4
+ import { fileURLToPath } from "node:url";
5
+ import { DEFAULT_WIKI_ENV_FILE, getCliEnvironmentInfo, parseEnvFile, serializeEnvEntries } from "./cli-env.js";
6
+ import { resolveTemplateFilePath, loadConfig } from "./config.js";
7
+ import { EmbeddingClient } from "./embedding.js";
8
+ import { parseVaultHashMode, resolveAgentSettings } from "./paths.js";
9
+ import { loadSynologyConfigFromEnv, normalizeSynologyRemotePath, withSynologyClient } from "./synology.js";
10
+ import { ensureWikiSkillInstall, formatParserSkills, inspectSkillInstall, installParserSkill, OPTIONAL_PARSER_SKILLS, parseParserSkillSelection, parseParserSkills, resolveWorkspaceRootFromWikiPath, resolveWorkspaceSkillPath, resolveWorkspaceSkillPaths, } from "./workspace-skills.js";
11
+ import { scaffoldWorkspaceAssets } from "./workspace-bootstrap.js";
12
+ import { AppError } from "../utils/errors.js";
13
+ import { pathExistsSync, writeTextFileSync } from "../utils/fs.js";
14
+ const MANAGED_ENV_KEYS = new Set([
15
+ "WIKI_PATH",
16
+ "VAULT_PATH",
17
+ "VAULT_SOURCE",
18
+ "VAULT_HASH_MODE",
19
+ "VAULT_SYNOLOGY_REMOTE_PATH",
20
+ "WIKI_DB_PATH",
21
+ "WIKI_CONFIG_PATH",
22
+ "WIKI_TEMPLATES_PATH",
23
+ "WIKI_SYNC_INTERVAL",
24
+ "SYNOLOGY_BASE_URL",
25
+ "SYNOLOGY_USERNAME",
26
+ "SYNOLOGY_PASSWORD",
27
+ "SYNOLOGY_VERIFY_SSL",
28
+ "SYNOLOGY_READONLY",
29
+ "EMBEDDING_BASE_URL",
30
+ "EMBEDDING_API_KEY",
31
+ "EMBEDDING_MODEL",
32
+ "EMBEDDING_DIMENSIONS",
33
+ "WIKI_AGENT_ENABLED",
34
+ "WIKI_AGENT_BASE_URL",
35
+ "WIKI_AGENT_API_KEY",
36
+ "WIKI_AGENT_MODEL",
37
+ "WIKI_AGENT_BATCH_SIZE",
38
+ "WIKI_PARSER_SKILLS",
39
+ ]);
40
+ function writeSection(output, title) {
41
+ output.write(`\n${title}\n`);
42
+ }
43
+ function resolvePackageRoot(packageRoot) {
44
+ return packageRoot ?? path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
45
+ }
46
+ function resolveInputPath(value, cwd) {
47
+ return path.resolve(cwd, value.trim());
48
+ }
49
+ function normalizeVaultSource(rawValue) {
50
+ const normalized = (rawValue ?? "local").trim().toLowerCase();
51
+ if (!normalized || normalized === "local") {
52
+ return "local";
53
+ }
54
+ if (normalized === "synology") {
55
+ return "synology";
56
+ }
57
+ throw new AppError(`VAULT_SOURCE must be "local" or "synology", got ${rawValue}`, "config");
58
+ }
59
+ function safeVaultSource(rawValue) {
60
+ try {
61
+ return normalizeVaultSource(rawValue);
62
+ }
63
+ catch {
64
+ return "local";
65
+ }
66
+ }
67
+ function safeVaultHashMode(rawValue, defaultValue) {
68
+ try {
69
+ return parseVaultHashMode(rawValue);
70
+ }
71
+ catch {
72
+ return defaultValue;
73
+ }
74
+ }
75
+ function safeBooleanFlag(rawValue, defaultValue) {
76
+ if (rawValue === undefined || rawValue.trim().length === 0) {
77
+ return defaultValue;
78
+ }
79
+ const normalized = rawValue.trim().toLowerCase();
80
+ if (["1", "true", "yes", "on", "y"].includes(normalized)) {
81
+ return true;
82
+ }
83
+ if (["0", "false", "no", "off", "n"].includes(normalized)) {
84
+ return false;
85
+ }
86
+ return defaultValue;
87
+ }
88
+ function validateNonNegativeInteger(rawValue, label) {
89
+ const parsed = Number.parseInt(rawValue, 10);
90
+ if (!Number.isFinite(parsed) || parsed < 0) {
91
+ return `${label} must be a non-negative integer.`;
92
+ }
93
+ return null;
94
+ }
95
+ function validateUrl(rawValue, label) {
96
+ const value = rawValue.trim();
97
+ if (!/^https?:\/\//i.test(value)) {
98
+ return `${label} must start with http:// or https://.`;
99
+ }
100
+ return null;
101
+ }
102
+ function validateWikiPath(rawValue) {
103
+ const normalized = rawValue.replace(/[\\/]+$/g, "");
104
+ if (!normalized.endsWith("/pages") && !normalized.endsWith("\\pages")) {
105
+ return "WIKI_PATH must point to the wiki/pages directory.";
106
+ }
107
+ return null;
108
+ }
109
+ class ReadlinePromptDriver {
110
+ rl;
111
+ constructor(rl) {
112
+ this.rl = rl;
113
+ }
114
+ ask(prompt) {
115
+ return this.rl.question(prompt);
116
+ }
117
+ close() {
118
+ this.rl.close();
119
+ }
120
+ }
121
+ class BufferedPromptDriver {
122
+ answers;
123
+ output;
124
+ index = 0;
125
+ constructor(answers, output) {
126
+ this.answers = answers;
127
+ this.output = output;
128
+ }
129
+ async ask(prompt) {
130
+ const answer = this.answers[this.index] ?? "";
131
+ this.index += 1;
132
+ this.output.write(`${prompt}${answer}\n`);
133
+ return answer;
134
+ }
135
+ close() { }
136
+ }
137
+ async function readBufferedAnswers(input) {
138
+ const chunks = [];
139
+ for await (const chunk of input) {
140
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
141
+ }
142
+ const raw = Buffer.concat(chunks).toString("utf8");
143
+ if (raw.length === 0) {
144
+ return [];
145
+ }
146
+ return raw.split(/\r?\n/);
147
+ }
148
+ async function createPromptDriver(input, output) {
149
+ if ("isTTY" in input && input.isTTY) {
150
+ return new ReadlinePromptDriver(createInterface({
151
+ input,
152
+ output,
153
+ }));
154
+ }
155
+ return new BufferedPromptDriver(await readBufferedAnswers(input), output);
156
+ }
157
+ async function promptText(driver, ctx, label, defaultValue, options = {}) {
158
+ while (true) {
159
+ const answer = await driver.ask(`${label} [${defaultValue}]: `);
160
+ const candidate = (answer.trim() || defaultValue).trim();
161
+ if (options.required !== false && candidate.length === 0) {
162
+ ctx.output.write(`${label} is required.\n`);
163
+ continue;
164
+ }
165
+ const error = options.validator?.(candidate);
166
+ if (error) {
167
+ ctx.output.write(`${error}\n`);
168
+ continue;
169
+ }
170
+ return options.normalize ? options.normalize(candidate) : candidate;
171
+ }
172
+ }
173
+ async function promptYesNo(driver, ctx, label, defaultValue) {
174
+ const suffix = defaultValue ? "Y/n" : "y/N";
175
+ while (true) {
176
+ const answer = (await driver.ask(`${label} [${suffix}]: `)).trim().toLowerCase();
177
+ if (!answer) {
178
+ return defaultValue;
179
+ }
180
+ if (["y", "yes"].includes(answer)) {
181
+ return true;
182
+ }
183
+ if (["n", "no"].includes(answer)) {
184
+ return false;
185
+ }
186
+ ctx.output.write("Please answer yes or no.\n");
187
+ }
188
+ }
189
+ function formatStep(index, total, title) {
190
+ return `Step ${index}/${total}: ${title}`;
191
+ }
192
+ async function promptVaultSource(driver, ctx, defaultValue) {
193
+ const value = await promptText(driver, ctx, "VAULT_SOURCE (local/synology)", defaultValue, {
194
+ validator: (candidate) => {
195
+ try {
196
+ normalizeVaultSource(candidate);
197
+ return null;
198
+ }
199
+ catch (error) {
200
+ return error instanceof Error ? error.message : String(error);
201
+ }
202
+ },
203
+ normalize: (candidate) => normalizeVaultSource(candidate),
204
+ });
205
+ return value;
206
+ }
207
+ async function promptVaultHashMode(driver, ctx, defaultValue) {
208
+ const value = await promptText(driver, ctx, "VAULT_HASH_MODE", defaultValue, {
209
+ validator: (candidate) => {
210
+ try {
211
+ parseVaultHashMode(candidate);
212
+ return null;
213
+ }
214
+ catch (error) {
215
+ return error instanceof Error ? error.message : String(error);
216
+ }
217
+ },
218
+ normalize: (candidate) => parseVaultHashMode(candidate),
219
+ });
220
+ return value;
221
+ }
222
+ function canReadWrite(targetPath) {
223
+ accessSync(targetPath, constants.R_OK | constants.W_OK);
224
+ return true;
225
+ }
226
+ function canWrite(targetPath) {
227
+ accessSync(targetPath, constants.W_OK);
228
+ return true;
229
+ }
230
+ function collectDoctorCheck(checks, severity, id, summary, recommendation) {
231
+ checks.push({ id, severity, summary, ...(recommendation ? { recommendation } : {}) });
232
+ }
233
+ function getPathDefaults(env, cwd) {
234
+ const vaultSource = safeVaultSource(env.VAULT_SOURCE);
235
+ const wikiRoot = env.WIKI_PATH ? path.resolve(env.WIKI_PATH, "..") : path.join(cwd, "tiangong-wiki");
236
+ const wikiPath = env.WIKI_PATH ? path.resolve(env.WIKI_PATH) : path.join(wikiRoot, "pages");
237
+ const vaultPath = env.VAULT_PATH ? path.resolve(env.VAULT_PATH) : path.join(cwd, "vault");
238
+ const dbPath = env.WIKI_DB_PATH ? path.resolve(env.WIKI_DB_PATH) : path.join(wikiRoot, "index.db");
239
+ const configPath = env.WIKI_CONFIG_PATH ? path.resolve(env.WIKI_CONFIG_PATH) : path.join(wikiRoot, "wiki.config.json");
240
+ const templatesPath = env.WIKI_TEMPLATES_PATH ? path.resolve(env.WIKI_TEMPLATES_PATH) : path.join(wikiRoot, "templates");
241
+ const defaultHashMode = vaultSource === "synology" ? "mtime" : "content";
242
+ return {
243
+ envFilePath: env.WIKI_ENV_FILE ? path.resolve(cwd, env.WIKI_ENV_FILE) : path.join(cwd, DEFAULT_WIKI_ENV_FILE),
244
+ vaultSource,
245
+ wikiPath,
246
+ vaultPath,
247
+ vaultHashMode: safeVaultHashMode(env.VAULT_HASH_MODE, defaultHashMode),
248
+ synologyBaseUrl: env.SYNOLOGY_BASE_URL ?? env.SYNOLOGY_URL ?? null,
249
+ synologyUsername: env.SYNOLOGY_USERNAME ?? env.SYNOLOGY_USER ?? null,
250
+ synologyPassword: env.SYNOLOGY_PASSWORD ?? env.SYNOLOGY_PASS ?? null,
251
+ synologyRemotePath: env.VAULT_SYNOLOGY_REMOTE_PATH ?? null,
252
+ synologyVerifySsl: safeBooleanFlag(env.SYNOLOGY_VERIFY_SSL, true),
253
+ synologyReadonly: safeBooleanFlag(env.SYNOLOGY_READONLY, true),
254
+ dbPath,
255
+ configPath,
256
+ templatesPath,
257
+ syncInterval: env.WIKI_SYNC_INTERVAL ?? "86400",
258
+ embeddingEnabled: Boolean((env.EMBEDDING_BASE_URL ?? env.OPENROUTER_BASE_URL) && (env.EMBEDDING_API_KEY ?? env.OPENROUTER_API_KEY) && (env.EMBEDDING_MODEL ?? env.OPENROUTER_EMBEDDING_MODEL)),
259
+ embeddingBaseUrl: env.EMBEDDING_BASE_URL ?? env.OPENROUTER_BASE_URL ?? "https://api.openai.com/v1",
260
+ embeddingApiKey: env.EMBEDDING_API_KEY ?? env.OPENROUTER_API_KEY ?? null,
261
+ embeddingModel: env.EMBEDDING_MODEL ?? env.OPENROUTER_EMBEDDING_MODEL ?? "text-embedding-3-small",
262
+ embeddingDimensions: env.EMBEDDING_DIMENSIONS ?? "384",
263
+ agentEnabled: (env.WIKI_AGENT_ENABLED ?? "").trim().toLowerCase() === "true",
264
+ agentBaseUrl: env.WIKI_AGENT_BASE_URL ?? "https://api.openai.com/v1",
265
+ agentApiKey: env.WIKI_AGENT_API_KEY ?? null,
266
+ agentModel: env.WIKI_AGENT_MODEL ?? null,
267
+ agentBatchSize: env.WIKI_AGENT_BATCH_SIZE ?? "5",
268
+ parserSkills: parseParserSkills(env.WIKI_PARSER_SKILLS, { strict: false }),
269
+ };
270
+ }
271
+ async function collectEmbeddingSettings(driver, ctx, defaults, env) {
272
+ const enabled = await promptYesNo(driver, ctx, "Enable semantic search with embeddings?", defaults.embeddingEnabled);
273
+ if (!enabled) {
274
+ return {
275
+ embeddingEnabled: false,
276
+ embeddingBaseUrl: null,
277
+ embeddingApiKey: null,
278
+ embeddingModel: null,
279
+ embeddingDimensions: null,
280
+ };
281
+ }
282
+ while (true) {
283
+ const embeddingBaseUrl = await promptText(driver, ctx, "EMBEDDING_BASE_URL", defaults.embeddingBaseUrl ?? "https://api.openai.com/v1", { validator: (value) => validateUrl(value, "EMBEDDING_BASE_URL") });
284
+ const embeddingApiKey = await promptText(driver, ctx, "EMBEDDING_API_KEY", defaults.embeddingApiKey ?? "", { required: true });
285
+ const embeddingModel = await promptText(driver, ctx, "EMBEDDING_MODEL", defaults.embeddingModel ?? "text-embedding-3-small", { required: true });
286
+ const embeddingDimensions = await promptText(driver, ctx, "EMBEDDING_DIMENSIONS", defaults.embeddingDimensions ?? "384", { validator: (value) => validateNonNegativeInteger(value, "EMBEDDING_DIMENSIONS") });
287
+ const shouldProbe = await promptYesNo(driver, ctx, "Probe the embedding endpoint now?", false);
288
+ if (shouldProbe) {
289
+ try {
290
+ const probeEnv = {
291
+ ...env,
292
+ EMBEDDING_BASE_URL: embeddingBaseUrl,
293
+ EMBEDDING_API_KEY: embeddingApiKey,
294
+ EMBEDDING_MODEL: embeddingModel,
295
+ EMBEDDING_DIMENSIONS: embeddingDimensions,
296
+ };
297
+ const client = EmbeddingClient.fromEnv(probeEnv);
298
+ if (!client) {
299
+ throw new AppError("Embedding configuration is incomplete.", "config");
300
+ }
301
+ await client.probe();
302
+ ctx.output.write("Embedding probe succeeded.\n");
303
+ }
304
+ catch (error) {
305
+ const message = error instanceof Error ? error.message : String(error);
306
+ ctx.output.write(`Embedding probe failed: ${message}\n`);
307
+ if (await promptYesNo(driver, ctx, "Re-enter embedding settings?", true)) {
308
+ continue;
309
+ }
310
+ }
311
+ }
312
+ return {
313
+ embeddingEnabled: true,
314
+ embeddingBaseUrl,
315
+ embeddingApiKey,
316
+ embeddingModel,
317
+ embeddingDimensions,
318
+ };
319
+ }
320
+ }
321
+ async function collectAgentSettings(driver, ctx, defaults) {
322
+ const enabled = await promptYesNo(driver, ctx, "Enable automatic vault-to-wiki processing?", defaults.agentEnabled);
323
+ if (!enabled) {
324
+ return {
325
+ agentEnabled: false,
326
+ agentBaseUrl: null,
327
+ agentApiKey: null,
328
+ agentModel: null,
329
+ agentBatchSize: null,
330
+ };
331
+ }
332
+ return {
333
+ agentEnabled: true,
334
+ agentBaseUrl: await promptText(driver, ctx, "WIKI_AGENT_BASE_URL", defaults.agentBaseUrl ?? "https://api.openai.com/v1", { validator: (value) => validateUrl(value, "WIKI_AGENT_BASE_URL") }),
335
+ agentApiKey: await promptText(driver, ctx, "WIKI_AGENT_API_KEY", defaults.agentApiKey ?? "", { required: true }),
336
+ agentModel: await promptText(driver, ctx, "WIKI_AGENT_MODEL", defaults.agentModel ?? "", { required: true }),
337
+ agentBatchSize: await promptText(driver, ctx, "WIKI_AGENT_BATCH_SIZE", defaults.agentBatchSize ?? "5", { validator: (value) => validateNonNegativeInteger(value, "WIKI_AGENT_BATCH_SIZE") }),
338
+ };
339
+ }
340
+ async function collectSynologySettings(driver, ctx, defaults) {
341
+ const synologyBaseUrl = await promptText(driver, ctx, "SYNOLOGY_BASE_URL", defaults.synologyBaseUrl ?? "https://nas.example.com:5001", { validator: (value) => validateUrl(value, "SYNOLOGY_BASE_URL") });
342
+ const synologyUsername = await promptText(driver, ctx, "SYNOLOGY_USERNAME", defaults.synologyUsername ?? "", { required: true });
343
+ const synologyPassword = await promptText(driver, ctx, "SYNOLOGY_PASSWORD", defaults.synologyPassword ?? "", { required: true });
344
+ const synologyRemotePath = await promptText(driver, ctx, "VAULT_SYNOLOGY_REMOTE_PATH", defaults.synologyRemotePath ?? "/homes/user/wiki-vault", {
345
+ validator: (value) => {
346
+ try {
347
+ normalizeSynologyRemotePath(value);
348
+ return null;
349
+ }
350
+ catch (error) {
351
+ return error instanceof Error ? error.message : String(error);
352
+ }
353
+ },
354
+ normalize: (value) => normalizeSynologyRemotePath(value),
355
+ });
356
+ const vaultHashMode = await promptVaultHashMode(driver, ctx, "mtime");
357
+ const synologyVerifySsl = await promptYesNo(driver, ctx, "SYNOLOGY_VERIFY_SSL", defaults.synologyVerifySsl);
358
+ const synologyReadonly = await promptYesNo(driver, ctx, "SYNOLOGY_READONLY", defaults.synologyReadonly);
359
+ return {
360
+ vaultHashMode,
361
+ synologyBaseUrl,
362
+ synologyUsername,
363
+ synologyPassword,
364
+ synologyRemotePath,
365
+ synologyVerifySsl,
366
+ synologyReadonly,
367
+ };
368
+ }
369
+ async function collectParserSkillSettings(driver, ctx, defaults, wikiPath) {
370
+ const { skillsRoot } = resolveWorkspaceSkillPaths(wikiPath);
371
+ ctx.output.write(`tiangong-wiki-skill is required and will be installed at ${path.join(skillsRoot, "tiangong-wiki-skill")}.\n`);
372
+ const selected = new Set(defaults.parserSkills);
373
+ for (const skill of OPTIONAL_PARSER_SKILLS) {
374
+ const enabled = await promptYesNo(driver, ctx, `Install parser skill ${skill.name} (${skill.summary})?`, selected.has(skill.name));
375
+ if (enabled) {
376
+ selected.add(skill.name);
377
+ }
378
+ else {
379
+ selected.delete(skill.name);
380
+ }
381
+ }
382
+ return OPTIONAL_PARSER_SKILLS.map((skill) => skill.name).filter((skill) => selected.has(skill));
383
+ }
384
+ function buildSetupSummary(values) {
385
+ const { workspaceRoot, skillsRoot } = resolveWorkspaceSkillPaths(values.wikiPath);
386
+ const lines = [
387
+ "Configuration summary",
388
+ ` WIKI_ENV_FILE: ${values.envFilePath}`,
389
+ ` WORKSPACE_ROOT: ${workspaceRoot}`,
390
+ ` VAULT_SOURCE: ${values.vaultSource}`,
391
+ ` WIKI_PATH: ${values.wikiPath}`,
392
+ ` VAULT_PATH: ${values.vaultPath}${values.vaultSource === "synology" ? " (local cache)" : ""}`,
393
+ ` VAULT_HASH_MODE: ${values.vaultHashMode}`,
394
+ ` WIKI_DB_PATH: ${values.dbPath}`,
395
+ ` WIKI_CONFIG_PATH: ${values.configPath}`,
396
+ ` WIKI_TEMPLATES_PATH: ${values.templatesPath}`,
397
+ ` WIKI_SYNC_INTERVAL: ${values.syncInterval}`,
398
+ ` Embeddings: ${values.embeddingEnabled ? "enabled" : "disabled"}`,
399
+ ` Vault processing: ${values.agentEnabled ? "enabled" : "disabled"}`,
400
+ ` Skills root: ${skillsRoot}`,
401
+ ` Required skill: tiangong-wiki-skill`,
402
+ ` Parser skills: ${values.parserSkills.length > 0 ? values.parserSkills.join(", ") : "(none)"}`,
403
+ ];
404
+ if (values.embeddingEnabled) {
405
+ lines.push(` EMBEDDING_BASE_URL: ${values.embeddingBaseUrl}`);
406
+ lines.push(` EMBEDDING_MODEL: ${values.embeddingModel}`);
407
+ lines.push(` EMBEDDING_DIMENSIONS: ${values.embeddingDimensions}`);
408
+ }
409
+ if (values.agentEnabled) {
410
+ lines.push(` WIKI_AGENT_BASE_URL: ${values.agentBaseUrl}`);
411
+ lines.push(` WIKI_AGENT_MODEL: ${values.agentModel}`);
412
+ lines.push(` WIKI_AGENT_BATCH_SIZE: ${values.agentBatchSize}`);
413
+ }
414
+ if (values.vaultSource === "synology") {
415
+ lines.push(` SYNOLOGY_BASE_URL: ${values.synologyBaseUrl}`);
416
+ lines.push(` SYNOLOGY_USERNAME: ${values.synologyUsername}`);
417
+ lines.push(` VAULT_SYNOLOGY_REMOTE_PATH: ${values.synologyRemotePath}`);
418
+ lines.push(` SYNOLOGY_VERIFY_SSL: ${values.synologyVerifySsl ? "true" : "false"}`);
419
+ lines.push(` SYNOLOGY_READONLY: ${values.synologyReadonly ? "true" : "false"}`);
420
+ }
421
+ return lines.join("\n");
422
+ }
423
+ function writeSetupEnvFile(values) {
424
+ const existingEntries = pathExistsSync(values.envFilePath) ? parseEnvFile(readFileSync(values.envFilePath, "utf8")) : {};
425
+ const preservedEntries = Object.entries(existingEntries).filter(([key]) => !MANAGED_ENV_KEYS.has(key));
426
+ const managedEntries = [
427
+ ["WIKI_PATH", values.wikiPath],
428
+ ["VAULT_PATH", values.vaultPath],
429
+ ["VAULT_SOURCE", values.vaultSource],
430
+ ["VAULT_HASH_MODE", values.vaultHashMode],
431
+ ["VAULT_SYNOLOGY_REMOTE_PATH", values.vaultSource === "synology" ? values.synologyRemotePath : null],
432
+ ["WIKI_DB_PATH", values.dbPath],
433
+ ["WIKI_CONFIG_PATH", values.configPath],
434
+ ["WIKI_TEMPLATES_PATH", values.templatesPath],
435
+ ["WIKI_SYNC_INTERVAL", values.syncInterval],
436
+ ["SYNOLOGY_BASE_URL", values.vaultSource === "synology" ? values.synologyBaseUrl : null],
437
+ ["SYNOLOGY_USERNAME", values.vaultSource === "synology" ? values.synologyUsername : null],
438
+ ["SYNOLOGY_PASSWORD", values.vaultSource === "synology" ? values.synologyPassword : null],
439
+ ["SYNOLOGY_VERIFY_SSL", values.vaultSource === "synology" ? String(values.synologyVerifySsl) : null],
440
+ ["SYNOLOGY_READONLY", values.vaultSource === "synology" ? String(values.synologyReadonly) : null],
441
+ ["EMBEDDING_BASE_URL", values.embeddingEnabled ? values.embeddingBaseUrl : null],
442
+ ["EMBEDDING_API_KEY", values.embeddingEnabled ? values.embeddingApiKey : null],
443
+ ["EMBEDDING_MODEL", values.embeddingEnabled ? values.embeddingModel : null],
444
+ ["EMBEDDING_DIMENSIONS", values.embeddingEnabled ? values.embeddingDimensions : null],
445
+ ["WIKI_AGENT_ENABLED", values.agentEnabled ? "true" : "false"],
446
+ ["WIKI_AGENT_BASE_URL", values.agentEnabled ? values.agentBaseUrl : null],
447
+ ["WIKI_AGENT_API_KEY", values.agentEnabled ? values.agentApiKey : null],
448
+ ["WIKI_AGENT_MODEL", values.agentEnabled ? values.agentModel : null],
449
+ ["WIKI_AGENT_BATCH_SIZE", values.agentEnabled ? values.agentBatchSize : null],
450
+ ["WIKI_PARSER_SKILLS", formatParserSkills(values.parserSkills)],
451
+ ];
452
+ const body = [
453
+ "# Generated by `tiangong-wiki setup`.",
454
+ "# You can edit this file manually and rerun `tiangong-wiki doctor` to validate changes.",
455
+ "",
456
+ serializeEnvEntries([...managedEntries, ...preservedEntries]),
457
+ ].join("\n");
458
+ writeTextFileSync(values.envFilePath, body);
459
+ }
460
+ export async function runSetupWizard(env = process.env, options = {}) {
461
+ const cwd = options.cwd ?? process.cwd();
462
+ const output = options.output ?? process.stdout;
463
+ const defaults = getPathDefaults(env, cwd);
464
+ const driver = await createPromptDriver(options.input ?? process.stdin, output);
465
+ const ctx = { cwd, output };
466
+ try {
467
+ writeSection(output, "Step 1: Configuration file");
468
+ const envFilePath = await promptText(driver, ctx, "Path for the generated .wiki.env file", defaults.envFilePath, {
469
+ normalize: (value) => resolveInputPath(value, cwd),
470
+ });
471
+ writeSection(output, "Step 2: Vault source");
472
+ const vaultSource = await promptVaultSource(driver, ctx, defaults.vaultSource);
473
+ const totalSteps = vaultSource === "synology" ? 9 : 8;
474
+ writeSection(output, formatStep(3, totalSteps, "Core paths"));
475
+ const wikiPath = await promptText(driver, ctx, "WIKI_PATH", defaults.wikiPath, {
476
+ normalize: (value) => resolveInputPath(value, cwd),
477
+ validator: (value) => validateWikiPath(resolveInputPath(value, cwd)),
478
+ });
479
+ const vaultPath = await promptText(driver, ctx, vaultSource === "synology" ? "VAULT_PATH (local cache directory)" : "VAULT_PATH", defaults.vaultPath, {
480
+ normalize: (value) => resolveInputPath(value, cwd),
481
+ });
482
+ const dbPath = await promptText(driver, ctx, "WIKI_DB_PATH", defaults.dbPath, {
483
+ normalize: (value) => resolveInputPath(value, cwd),
484
+ });
485
+ const configPath = await promptText(driver, ctx, "WIKI_CONFIG_PATH", defaults.configPath, {
486
+ normalize: (value) => resolveInputPath(value, cwd),
487
+ });
488
+ const templatesPath = await promptText(driver, ctx, "WIKI_TEMPLATES_PATH", defaults.templatesPath, {
489
+ normalize: (value) => resolveInputPath(value, cwd),
490
+ });
491
+ let synologyValues;
492
+ if (vaultSource === "synology") {
493
+ writeSection(output, formatStep(4, totalSteps, "Synology NAS"));
494
+ output.write("Synology mode uses VAULT_PATH as the local cache directory for downloaded vault files.\n");
495
+ synologyValues = await collectSynologySettings(driver, ctx, defaults);
496
+ }
497
+ else {
498
+ synologyValues = {
499
+ vaultHashMode: defaults.vaultSource === "local" ? defaults.vaultHashMode : "content",
500
+ synologyBaseUrl: null,
501
+ synologyUsername: null,
502
+ synologyPassword: null,
503
+ synologyRemotePath: null,
504
+ synologyVerifySsl: defaults.synologyVerifySsl,
505
+ synologyReadonly: defaults.synologyReadonly,
506
+ };
507
+ }
508
+ writeSection(output, formatStep(vaultSource === "synology" ? 5 : 4, totalSteps, "Sync schedule"));
509
+ const syncInterval = await promptText(driver, ctx, "WIKI_SYNC_INTERVAL (seconds)", defaults.syncInterval, {
510
+ validator: (value) => validateNonNegativeInteger(value, "WIKI_SYNC_INTERVAL"),
511
+ });
512
+ writeSection(output, formatStep(vaultSource === "synology" ? 6 : 5, totalSteps, "Embedding configuration"));
513
+ const embedding = await collectEmbeddingSettings(driver, ctx, defaults, env);
514
+ writeSection(output, formatStep(vaultSource === "synology" ? 7 : 6, totalSteps, "Automatic vault processing"));
515
+ const agent = await collectAgentSettings(driver, ctx, defaults);
516
+ writeSection(output, formatStep(vaultSource === "synology" ? 8 : 7, totalSteps, "Codex skills"));
517
+ const parserSkills = await collectParserSkillSettings(driver, ctx, defaults, wikiPath);
518
+ const values = {
519
+ envFilePath,
520
+ vaultSource,
521
+ wikiPath,
522
+ vaultPath,
523
+ ...synologyValues,
524
+ dbPath,
525
+ configPath,
526
+ templatesPath,
527
+ syncInterval,
528
+ ...embedding,
529
+ ...agent,
530
+ parserSkills,
531
+ };
532
+ writeSection(output, formatStep(totalSteps, totalSteps, "Confirm"));
533
+ output.write(`${buildSetupSummary(values)}\n`);
534
+ const confirmed = await promptYesNo(driver, ctx, "Write configuration and scaffold workspace assets?", true);
535
+ if (!confirmed) {
536
+ throw new AppError("Setup aborted before writing any files.", "runtime");
537
+ }
538
+ const packageRoot = resolvePackageRoot(options.packageRoot);
539
+ const bootstrap = scaffoldWorkspaceAssets({
540
+ packageRoot,
541
+ wikiRoot: path.resolve(wikiPath, ".."),
542
+ wikiPath,
543
+ vaultPath,
544
+ templatesPath,
545
+ configPath,
546
+ });
547
+ const { workspaceRoot, skillsRoot } = resolveWorkspaceSkillPaths(values.wikiPath);
548
+ const wikiSkillInstall = ensureWikiSkillInstall(values.wikiPath, packageRoot);
549
+ const parserSkillInstalls = values.parserSkills.map((skillName) => installParserSkill(skillName, workspaceRoot, {
550
+ env,
551
+ output,
552
+ }));
553
+ writeSetupEnvFile(values);
554
+ output.write([
555
+ "\ntiangong-wiki setup complete",
556
+ `configuration file: ${values.envFilePath}`,
557
+ `skills root: ${skillsRoot}`,
558
+ `tiangong-wiki-skill: ${wikiSkillInstall.status}`,
559
+ `parser skills: ${values.parserSkills.length > 0 ? values.parserSkills.join(", ") : "(none)"}`,
560
+ `created directories: ${bootstrap.createdDirectories.length}`,
561
+ `copied config: ${bootstrap.copiedConfig}`,
562
+ `copied templates: ${bootstrap.copiedTemplates}`,
563
+ ...(parserSkillInstalls.length > 0
564
+ ? [`installed parser skills: ${parserSkillInstalls.map((item) => `${item.name} (${item.status})`).join(", ")}`]
565
+ : []),
566
+ "",
567
+ "Next steps:",
568
+ "- Run `tiangong-wiki doctor` to validate the generated configuration.",
569
+ "- Run `tiangong-wiki init` to create index.db and perform the first sync.",
570
+ ...(values.vaultSource === "synology"
571
+ ? ["- Protect `.wiki.env` carefully because it now stores Synology credentials."]
572
+ : []),
573
+ ...(values.agentEnabled
574
+ ? ["- Start the background service with `tiangong-wiki daemon start` or `tiangong-wiki daemon run` once init succeeds."]
575
+ : []),
576
+ ].join("\n"));
577
+ return {
578
+ envFilePath: values.envFilePath,
579
+ createdDirectories: bootstrap.createdDirectories,
580
+ copiedConfig: bootstrap.copiedConfig,
581
+ copiedTemplates: bootstrap.copiedTemplates,
582
+ embeddingEnabled: values.embeddingEnabled,
583
+ agentEnabled: values.agentEnabled,
584
+ parserSkills: values.parserSkills,
585
+ skillsRoot,
586
+ };
587
+ }
588
+ finally {
589
+ driver.close();
590
+ }
591
+ }
592
+ function inspectDirectory(checks, id, label, dirPath, options = {}) {
593
+ if (!dirPath) {
594
+ if (options.required !== false) {
595
+ collectDoctorCheck(checks, "error", id, `${label} is not configured.`, options.recommendation ?? "Run `tiangong-wiki setup` to generate a complete workspace configuration.");
596
+ }
597
+ return;
598
+ }
599
+ if (!pathExistsSync(dirPath)) {
600
+ collectDoctorCheck(checks, "error", id, `${label} does not exist: ${dirPath}`, options.recommendation ?? `Create the directory or rerun \`tiangong-wiki setup\` to scaffold ${label}.`);
601
+ return;
602
+ }
603
+ try {
604
+ canReadWrite(dirPath);
605
+ collectDoctorCheck(checks, "ok", id, `${label} is readable and writable: ${dirPath}`);
606
+ }
607
+ catch {
608
+ collectDoctorCheck(checks, "error", id, `${label} is not readable and writable: ${dirPath}`, `Fix filesystem permissions for ${dirPath}.`);
609
+ }
610
+ }
611
+ function inspectDbPath(checks, dbPath) {
612
+ if (!dbPath) {
613
+ collectDoctorCheck(checks, "error", "db-path", "WIKI_DB_PATH is not configured.", "Run `tiangong-wiki setup` to record the database path.");
614
+ return;
615
+ }
616
+ if (pathExistsSync(dbPath)) {
617
+ try {
618
+ canReadWrite(dbPath);
619
+ collectDoctorCheck(checks, "ok", "db-path", `index.db is readable and writable: ${dbPath}`);
620
+ }
621
+ catch {
622
+ collectDoctorCheck(checks, "error", "db-path", `index.db exists but is not readable and writable: ${dbPath}`, `Fix filesystem permissions for ${dbPath}.`);
623
+ }
624
+ return;
625
+ }
626
+ const parentDir = path.dirname(dbPath);
627
+ if (pathExistsSync(parentDir)) {
628
+ try {
629
+ canWrite(parentDir);
630
+ collectDoctorCheck(checks, "warn", "db-path", `index.db does not exist yet and will be created during \`tiangong-wiki init\`: ${dbPath}`, "Run `tiangong-wiki init` to create the database and perform the first sync.");
631
+ return;
632
+ }
633
+ catch {
634
+ // handled below
635
+ }
636
+ }
637
+ collectDoctorCheck(checks, "error", "db-path", `index.db cannot be created at ${dbPath}`, `Ensure ${parentDir} exists and is writable, or rerun \`tiangong-wiki setup\`.`);
638
+ }
639
+ async function inspectVaultSource(checks, env, probe) {
640
+ let source;
641
+ try {
642
+ source = normalizeVaultSource(env.VAULT_SOURCE);
643
+ }
644
+ catch (error) {
645
+ const message = error instanceof Error ? error.message : String(error);
646
+ collectDoctorCheck(checks, "error", "vault-source", message, "Set VAULT_SOURCE to local or synology, or rerun `tiangong-wiki setup`.");
647
+ return;
648
+ }
649
+ let hashMode;
650
+ let hashModeValid = true;
651
+ try {
652
+ hashMode = parseVaultHashMode(env.VAULT_HASH_MODE);
653
+ }
654
+ catch (error) {
655
+ hashModeValid = false;
656
+ const message = error instanceof Error ? error.message : String(error);
657
+ collectDoctorCheck(checks, "error", "vault-hash-mode", message, "Set VAULT_HASH_MODE to content or mtime, or rerun `tiangong-wiki setup`.");
658
+ hashMode = source === "synology" ? "mtime" : "content";
659
+ }
660
+ if (hashModeValid) {
661
+ collectDoctorCheck(checks, "ok", "vault-source", `Vault source is ${source} with ${hashMode} hash mode.`);
662
+ }
663
+ if (source !== "synology") {
664
+ return;
665
+ }
666
+ let remotePath;
667
+ try {
668
+ remotePath = normalizeSynologyRemotePath(env.VAULT_SYNOLOGY_REMOTE_PATH);
669
+ collectDoctorCheck(checks, "ok", "synology-remote-path", `Synology vault remote path is configured: ${remotePath}`);
670
+ }
671
+ catch (error) {
672
+ const message = error instanceof Error ? error.message : String(error);
673
+ collectDoctorCheck(checks, "error", "synology-remote-path", message, "Set VAULT_SYNOLOGY_REMOTE_PATH to the remote vault directory, or rerun `tiangong-wiki setup`.");
674
+ return;
675
+ }
676
+ let config;
677
+ try {
678
+ config = loadSynologyConfigFromEnv(env);
679
+ }
680
+ catch (error) {
681
+ const message = error instanceof Error ? error.message : String(error);
682
+ collectDoctorCheck(checks, "error", "synology-config", message, "Set the required SYNOLOGY_* variables in `.wiki.env` or rerun `tiangong-wiki setup`.");
683
+ return;
684
+ }
685
+ if (!probe) {
686
+ collectDoctorCheck(checks, "ok", "synology-config", `Synology connection settings are configured for ${config.baseUrl} (verify SSL: ${config.verifySsl ? "true" : "false"}, readonly: ${config.readonly ? "true" : "false"}).`);
687
+ return;
688
+ }
689
+ collectDoctorCheck(checks, "ok", "synology-config", `Synology connection settings are configured for ${config.baseUrl} (verify SSL: ${config.verifySsl ? "true" : "false"}, readonly: ${config.readonly ? "true" : "false"}).`);
690
+ try {
691
+ await withSynologyClient(env, async (client) => {
692
+ await client.probeFolder(remotePath);
693
+ });
694
+ collectDoctorCheck(checks, "ok", "synology-probe", `Synology probe succeeded for ${remotePath} via ${config.baseUrl}.`);
695
+ }
696
+ catch (error) {
697
+ const message = error instanceof Error ? error.message : String(error);
698
+ collectDoctorCheck(checks, "error", "synology-probe", `Synology probe failed: ${message}`, "Verify SYNOLOGY_BASE_URL, credentials, remote path, and NAS network reachability.");
699
+ }
700
+ }
701
+ function inspectEmbedding(checks, env, probe) {
702
+ const baseUrl = env.EMBEDDING_BASE_URL ?? env.OPENROUTER_BASE_URL;
703
+ const apiKey = env.EMBEDDING_API_KEY ?? env.OPENROUTER_API_KEY;
704
+ const model = env.EMBEDDING_MODEL ?? env.OPENROUTER_EMBEDDING_MODEL;
705
+ const provided = [baseUrl, apiKey, model].filter(Boolean).length;
706
+ if (provided === 0) {
707
+ collectDoctorCheck(checks, "warn", "embedding", "Semantic search is disabled because EMBEDDING_* is not configured.", "Rerun `tiangong-wiki setup` or update `.wiki.env` to configure EMBEDDING_* if you want `tiangong-wiki search`.");
708
+ return;
709
+ }
710
+ try {
711
+ const client = EmbeddingClient.fromEnv(env);
712
+ if (!client) {
713
+ collectDoctorCheck(checks, "error", "embedding", "Embedding configuration is incomplete.", "Set EMBEDDING_BASE_URL, EMBEDDING_API_KEY, and EMBEDDING_MODEL together.");
714
+ return;
715
+ }
716
+ if (!probe) {
717
+ collectDoctorCheck(checks, "ok", "embedding", `Embedding configuration is complete: ${client.settings.model} @ ${client.settings.baseUrl}`);
718
+ return;
719
+ }
720
+ return client
721
+ .probe()
722
+ .then(() => {
723
+ collectDoctorCheck(checks, "ok", "embedding", `Embedding probe succeeded for ${client.settings.model}.`);
724
+ })
725
+ .catch((error) => {
726
+ const message = error instanceof Error ? error.message : String(error);
727
+ collectDoctorCheck(checks, "error", "embedding", `Embedding probe failed: ${message}`, "Verify EMBEDDING_BASE_URL, EMBEDDING_API_KEY, EMBEDDING_MODEL, and network reachability.");
728
+ });
729
+ }
730
+ catch (error) {
731
+ const message = error instanceof Error ? error.message : String(error);
732
+ collectDoctorCheck(checks, "error", "embedding", `Embedding configuration is invalid: ${message}`, "Fix EMBEDDING_* in `.wiki.env` or rerun `tiangong-wiki setup`.");
733
+ }
734
+ }
735
+ function inspectAgent(checks, env) {
736
+ try {
737
+ const settings = resolveAgentSettings(env);
738
+ if (!settings.enabled) {
739
+ collectDoctorCheck(checks, "ok", "agent", "Automatic vault processing is disabled.");
740
+ return;
741
+ }
742
+ if (settings.missing.length > 0) {
743
+ collectDoctorCheck(checks, "error", "agent", `Automatic vault processing is enabled but missing: ${settings.missing.join(", ")}`, "Set the missing WIKI_AGENT_* values in `.wiki.env` or rerun `tiangong-wiki setup`.");
744
+ return;
745
+ }
746
+ collectDoctorCheck(checks, "ok", "agent", `Automatic vault processing is enabled with model ${settings.model}.`);
747
+ }
748
+ catch (error) {
749
+ const message = error instanceof Error ? error.message : String(error);
750
+ collectDoctorCheck(checks, "error", "agent", `Agent configuration is invalid: ${message}`, "Fix WIKI_AGENT_* in `.wiki.env` or rerun `tiangong-wiki setup`.");
751
+ }
752
+ }
753
+ function inspectWorkspaceSkills(checks, wikiPath, rawParserSkills) {
754
+ if (!wikiPath) {
755
+ collectDoctorCheck(checks, "error", "skills-root", "Workspace skill root cannot be derived until WIKI_PATH is configured.", "Run `tiangong-wiki setup` to configure WIKI_PATH and install workspace-local skills.");
756
+ collectDoctorCheck(checks, "error", "tiangong-wiki-skill", "tiangong-wiki-skill cannot be checked until WIKI_PATH is configured.", "Run `tiangong-wiki setup` to configure WIKI_PATH and install workspace-local skills.");
757
+ return {
758
+ workspaceRoot: null,
759
+ skillsRoot: null,
760
+ requestedParserSkills: [],
761
+ invalidParserSkills: [],
762
+ missingSkills: ["tiangong-wiki-skill"],
763
+ };
764
+ }
765
+ const { workspaceRoot, skillsRoot, wikiSkillPath } = resolveWorkspaceSkillPaths(wikiPath);
766
+ inspectDirectory(checks, "skills-root", "WORKSPACE_SKILLS_ROOT", skillsRoot, {
767
+ recommendation: "Run `tiangong-wiki setup` to create workspace-local skills under .agents/skills.",
768
+ });
769
+ const missingSkills = [];
770
+ const wikiSkill = inspectSkillInstall(wikiSkillPath, "tiangong-wiki-skill");
771
+ if (wikiSkill.readable) {
772
+ collectDoctorCheck(checks, "ok", "tiangong-wiki-skill", `tiangong-wiki-skill is installed: ${wikiSkill.skillMdPath}`);
773
+ }
774
+ else {
775
+ collectDoctorCheck(checks, "error", "tiangong-wiki-skill", `tiangong-wiki-skill is missing or unreadable: ${wikiSkill.skillMdPath}`, "Run `tiangong-wiki setup` to install workspace-local tiangong-wiki-skill.");
776
+ missingSkills.push("tiangong-wiki-skill");
777
+ }
778
+ const { skills: requestedParserSkills, invalid: invalidParserSkills } = parseParserSkillSelection(rawParserSkills);
779
+ if (invalidParserSkills.length > 0) {
780
+ collectDoctorCheck(checks, "error", "parser-skills", `Parser skill configuration is invalid: ${invalidParserSkills.join(", ")}`, "Fix WIKI_PARSER_SKILLS in `.wiki.env` or rerun `tiangong-wiki setup`.");
781
+ return {
782
+ workspaceRoot,
783
+ skillsRoot,
784
+ requestedParserSkills,
785
+ invalidParserSkills,
786
+ missingSkills,
787
+ };
788
+ }
789
+ if (requestedParserSkills.length === 0) {
790
+ collectDoctorCheck(checks, "ok", "parser-skills", "No optional parser skills are declared in WIKI_PARSER_SKILLS.");
791
+ return {
792
+ workspaceRoot,
793
+ skillsRoot,
794
+ requestedParserSkills,
795
+ invalidParserSkills,
796
+ missingSkills,
797
+ };
798
+ }
799
+ const missingParserSkills = requestedParserSkills.filter((skillName) => {
800
+ const result = inspectSkillInstall(resolveWorkspaceSkillPath(workspaceRoot, skillName), skillName);
801
+ return !result.readable;
802
+ });
803
+ if (missingParserSkills.length > 0) {
804
+ collectDoctorCheck(checks, "error", "parser-skills", `Declared parser skills are missing or unreadable: ${missingParserSkills.join(", ")}`, "Rerun `tiangong-wiki setup` or reinstall the missing parser skills into workspace-local .agents/skills.");
805
+ missingSkills.push(...missingParserSkills);
806
+ }
807
+ else {
808
+ collectDoctorCheck(checks, "ok", "parser-skills", `Declared parser skills are installed: ${requestedParserSkills.join(", ")}`);
809
+ }
810
+ return {
811
+ workspaceRoot,
812
+ skillsRoot,
813
+ requestedParserSkills,
814
+ invalidParserSkills,
815
+ missingSkills,
816
+ };
817
+ }
818
+ function inspectConfigAndTemplates(checks, configPath, wikiRoot) {
819
+ if (!configPath) {
820
+ collectDoctorCheck(checks, "error", "config", "WIKI_CONFIG_PATH is not configured.", "Run `tiangong-wiki setup` to record the config path.");
821
+ return;
822
+ }
823
+ if (!pathExistsSync(configPath)) {
824
+ collectDoctorCheck(checks, "error", "config", `wiki.config.json does not exist: ${configPath}`, "Run `tiangong-wiki setup` or `tiangong-wiki init` to scaffold wiki.config.json.");
825
+ return;
826
+ }
827
+ try {
828
+ const config = loadConfig(configPath);
829
+ if (!wikiRoot) {
830
+ collectDoctorCheck(checks, "ok", "config", `Config loaded: ${configPath}`);
831
+ return;
832
+ }
833
+ const missingTemplates = Object.keys(config.templates)
834
+ .map((pageType) => ({
835
+ pageType,
836
+ templatePath: resolveTemplateFilePath(config, wikiRoot, pageType),
837
+ }))
838
+ .filter((entry) => !pathExistsSync(entry.templatePath));
839
+ if (missingTemplates.length > 0) {
840
+ collectDoctorCheck(checks, "error", "templates", `Missing template files: ${missingTemplates.map((entry) => entry.pageType).join(", ")}`, "Run `tiangong-wiki setup` or restore the missing template files under WIKI_TEMPLATES_PATH.");
841
+ return;
842
+ }
843
+ collectDoctorCheck(checks, "ok", "config", `Config loaded successfully with ${Object.keys(config.templates).length} registered templates.`);
844
+ }
845
+ catch (error) {
846
+ const message = error instanceof Error ? error.message : String(error);
847
+ collectDoctorCheck(checks, "error", "config", `Failed to load config: ${message}`, "Fix wiki.config.json or rerun `tiangong-wiki setup` to scaffold a clean copy.");
848
+ }
849
+ }
850
+ function inspectDaemon(checks, wikiRoot) {
851
+ if (!wikiRoot) {
852
+ return;
853
+ }
854
+ const pidPath = path.join(wikiRoot, ".wiki-daemon.pid");
855
+ const statePath = path.join(wikiRoot, ".wiki-daemon.state.json");
856
+ if (!pathExistsSync(pidPath)) {
857
+ collectDoctorCheck(checks, "warn", "daemon", "The wiki daemon is not running.", "Run `tiangong-wiki daemon start` after `tiangong-wiki init` if you want automatic sync cycles.");
858
+ return;
859
+ }
860
+ try {
861
+ const rawPid = readFileSync(pidPath, "utf8").trim();
862
+ const pid = Number.parseInt(rawPid, 10);
863
+ if (!Number.isFinite(pid)) {
864
+ collectDoctorCheck(checks, "error", "daemon", `Daemon PID file is invalid: ${pidPath}`, "Remove the stale PID file or restart the daemon.");
865
+ return;
866
+ }
867
+ try {
868
+ process.kill(pid, 0);
869
+ collectDoctorCheck(checks, "ok", "daemon", `The wiki daemon is running with PID ${pid}${pathExistsSync(statePath) ? " and has a state file." : "."}`);
870
+ }
871
+ catch {
872
+ collectDoctorCheck(checks, "error", "daemon", `Daemon PID file exists but process ${pid} is not running.`, "Run `tiangong-wiki daemon stop` to clear stale state, then restart the daemon if needed.");
873
+ }
874
+ }
875
+ catch (error) {
876
+ const message = error instanceof Error ? error.message : String(error);
877
+ collectDoctorCheck(checks, "error", "daemon", `Failed to inspect daemon state: ${message}`, "Check the daemon state files under the wiki workspace root.");
878
+ }
879
+ }
880
+ function summarizeChecks(checks) {
881
+ return checks.reduce((summary, check) => {
882
+ summary[check.severity] += 1;
883
+ return summary;
884
+ }, { ok: 0, warn: 0, error: 0 });
885
+ }
886
+ function uniqueRecommendations(checks) {
887
+ return Array.from(new Set(checks.map((check) => check.recommendation).filter(Boolean)));
888
+ }
889
+ export async function buildDoctorReport(env = process.env, options = {}) {
890
+ const checks = [];
891
+ const envFile = getCliEnvironmentInfo();
892
+ if (envFile.missingRequestedPath && envFile.requestedPath) {
893
+ collectDoctorCheck(checks, "error", "env-file", `Requested env file does not exist: ${envFile.requestedPath}`, "Create the env file or rerun `tiangong-wiki setup`.");
894
+ }
895
+ else if (envFile.loadedPath) {
896
+ collectDoctorCheck(checks, "ok", "env-file", `Loaded configuration from ${envFile.loadedPath}${envFile.autoDiscovered ? " (auto-discovered)." : "."}`);
897
+ }
898
+ else {
899
+ collectDoctorCheck(checks, "warn", "env-file", "No .wiki.env file was loaded; using process.env only.", "Run `tiangong-wiki setup` to generate a portable `.wiki.env` file.");
900
+ }
901
+ const wikiPath = env.WIKI_PATH ? path.resolve(env.WIKI_PATH) : null;
902
+ const wikiRoot = wikiPath ? path.resolve(wikiPath, "..") : null;
903
+ const workspaceRoot = wikiPath ? resolveWorkspaceRootFromWikiPath(wikiPath) : null;
904
+ const vaultPath = wikiRoot ? path.resolve(env.VAULT_PATH ?? path.join(wikiRoot, "..", "vault")) : null;
905
+ const dbPath = wikiRoot ? path.resolve(env.WIKI_DB_PATH ?? path.join(wikiRoot, "index.db")) : null;
906
+ const configPath = wikiRoot ? path.resolve(env.WIKI_CONFIG_PATH ?? path.join(wikiRoot, "wiki.config.json")) : null;
907
+ const templatesPath = wikiRoot ? path.resolve(env.WIKI_TEMPLATES_PATH ?? path.join(wikiRoot, "templates")) : null;
908
+ const skillStatus = inspectWorkspaceSkills(checks, wikiPath, env.WIKI_PARSER_SKILLS);
909
+ inspectDirectory(checks, "wiki-path", "WIKI_PATH", wikiPath, {
910
+ recommendation: "Run `tiangong-wiki setup` to generate WIKI_PATH and scaffold wiki/pages.",
911
+ });
912
+ inspectDirectory(checks, "vault-path", "VAULT_PATH", vaultPath, {
913
+ recommendation: "Run `tiangong-wiki setup` to generate VAULT_PATH and scaffold the vault directory.",
914
+ });
915
+ inspectDirectory(checks, "templates-path", "WIKI_TEMPLATES_PATH", templatesPath, {
916
+ recommendation: "Run `tiangong-wiki setup` or restore template files under WIKI_TEMPLATES_PATH.",
917
+ });
918
+ await inspectVaultSource(checks, env, options.probe === true);
919
+ inspectDbPath(checks, dbPath);
920
+ inspectConfigAndTemplates(checks, configPath, wikiRoot);
921
+ await inspectEmbedding(checks, env, options.probe === true);
922
+ inspectAgent(checks, env);
923
+ inspectDaemon(checks, wikiRoot);
924
+ const summary = summarizeChecks(checks);
925
+ return {
926
+ ok: summary.error === 0,
927
+ summary,
928
+ envFile: {
929
+ requestedPath: envFile.requestedPath,
930
+ loadedPath: envFile.loadedPath,
931
+ autoDiscovered: envFile.autoDiscovered,
932
+ missingRequestedPath: envFile.missingRequestedPath,
933
+ },
934
+ effectivePaths: {
935
+ wikiPath,
936
+ workspaceRoot,
937
+ vaultPath,
938
+ dbPath,
939
+ configPath,
940
+ templatesPath,
941
+ skillsRoot: skillStatus.skillsRoot,
942
+ },
943
+ skills: {
944
+ requestedParserSkills: skillStatus.requestedParserSkills,
945
+ invalidParserSkills: skillStatus.invalidParserSkills,
946
+ missingSkills: skillStatus.missingSkills,
947
+ },
948
+ checks,
949
+ recommendations: uniqueRecommendations(checks),
950
+ };
951
+ }
952
+ export function formatDoctorReport(report) {
953
+ const lines = ["tiangong-wiki doctor", ""];
954
+ for (const check of report.checks) {
955
+ lines.push(`${check.severity.toUpperCase().padEnd(5)} ${check.id.padEnd(14)} ${check.summary}`);
956
+ }
957
+ lines.push("");
958
+ lines.push(`Summary: ${report.summary.ok} ok, ${report.summary.warn} warn, ${report.summary.error} error`);
959
+ if (report.recommendations.length > 0) {
960
+ lines.push("");
961
+ lines.push("Recommended actions:");
962
+ for (const recommendation of report.recommendations) {
963
+ lines.push(`- ${recommendation}`);
964
+ }
965
+ }
966
+ return lines.join("\n");
967
+ }