@biaoo/tiangong-wiki 0.2.0 → 0.2.2

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 (37) hide show
  1. package/README.md +39 -50
  2. package/README.zh-CN.md +39 -50
  3. package/SKILL.md +75 -107
  4. package/assets/templates/achievement.md +8 -8
  5. package/assets/templates/bridge.md +8 -8
  6. package/assets/templates/concept.md +14 -18
  7. package/assets/templates/faq.md +8 -10
  8. package/assets/templates/lesson.md +8 -8
  9. package/assets/templates/method.md +16 -8
  10. package/assets/templates/misconception.md +10 -10
  11. package/assets/templates/person.md +8 -8
  12. package/assets/templates/research-note.md +10 -10
  13. package/assets/templates/resume.md +11 -10
  14. package/assets/templates/source-summary.md +8 -12
  15. package/assets/tiangong-wiki-framework.png +0 -0
  16. package/assets/wiki.config.default.json +6 -3
  17. package/dist/commands/asset.js +21 -0
  18. package/dist/commands/skill.js +78 -0
  19. package/dist/commands/template.js +30 -0
  20. package/dist/core/cli-env.js +34 -5
  21. package/dist/core/global-config.js +61 -0
  22. package/dist/core/onboarding.js +252 -102
  23. package/dist/core/workflow-context.js +58 -21
  24. package/dist/core/workspace-skills.js +496 -60
  25. package/dist/daemon/server.js +8 -0
  26. package/dist/index.js +36 -1
  27. package/dist/operations/asset.js +81 -0
  28. package/dist/operations/query.js +25 -1
  29. package/dist/operations/template-lint.js +160 -0
  30. package/dist/utils/asset.js +75 -0
  31. package/dist/utils/errors.js +6 -0
  32. package/package.json +2 -1
  33. package/references/cli-interface.md +32 -1
  34. package/references/template-design-guide.md +125 -113
  35. package/references/{env.md → troubleshooting.md} +64 -33
  36. package/references/vault-to-wiki-instruction.md +109 -51
  37. package/references/wiki-maintenance-instruction.md +15 -15
@@ -1,10 +1,11 @@
1
1
  import { accessSync, constants, readFileSync } from "node:fs";
2
2
  import path from "node:path";
3
- import { createInterface } from "node:readline/promises";
4
3
  import { fileURLToPath } from "node:url";
4
+ import { confirm, input, password, select } from "@inquirer/prompts";
5
5
  import { DEFAULT_WIKI_ENV_FILE, getCliEnvironmentInfo, parseEnvFile, serializeEnvEntries } from "./cli-env.js";
6
6
  import { resolveTemplateFilePath, loadConfig } from "./config.js";
7
7
  import { EmbeddingClient } from "./embedding.js";
8
+ import { writeGlobalConfig } from "./global-config.js";
8
9
  import { parseVaultHashMode, resolveAgentSettings } from "./paths.js";
9
10
  import { loadSynologyConfigFromEnv, normalizeSynologyRemotePath, withSynologyClient } from "./synology.js";
10
11
  import { ensureWikiSkillInstall, formatParserSkills, inspectSkillInstall, installParserSkill, OPTIONAL_PARSER_SKILLS, parseParserSkillSelection, parseParserSkills, resolveWorkspaceRootFromWikiPath, resolveWorkspaceSkillPath, resolveWorkspaceSkillPaths, } from "./workspace-skills.js";
@@ -106,16 +107,62 @@ function validateWikiPath(rawValue) {
106
107
  }
107
108
  return null;
108
109
  }
109
- class ReadlinePromptDriver {
110
- rl;
111
- constructor(rl) {
112
- this.rl = rl;
113
- }
114
- ask(prompt) {
115
- return this.rl.question(prompt);
110
+ class InquirerPromptDriver {
111
+ inputStream;
112
+ outputStream;
113
+ constructor(inputStream, outputStream) {
114
+ this.inputStream = inputStream;
115
+ this.outputStream = outputStream;
116
+ }
117
+ input(options) {
118
+ return input({
119
+ message: options.message,
120
+ default: options.defaultValue,
121
+ required: options.required !== false,
122
+ validate: (value) => options.validator?.(value) ?? true,
123
+ }, this.getContext());
124
+ }
125
+ password(options) {
126
+ const defaultValue = options.defaultValue ?? "";
127
+ const hasDefault = defaultValue.length > 0;
128
+ return password({
129
+ message: hasDefault ? `${options.message} (press enter to keep current value)` : options.message,
130
+ mask: "*",
131
+ validate: (value) => {
132
+ const candidate = value.length === 0 && hasDefault ? defaultValue : value;
133
+ if (options.required !== false && candidate.length === 0) {
134
+ return `${options.message} is required.`;
135
+ }
136
+ return options.validator?.(candidate) ?? true;
137
+ },
138
+ }, this.getContext()).then((value) => (value.length === 0 && hasDefault ? defaultValue : value));
139
+ }
140
+ confirm(options) {
141
+ return confirm({
142
+ message: options.message,
143
+ default: options.defaultValue,
144
+ }, this.getContext());
145
+ }
146
+ select(options) {
147
+ return select({
148
+ message: options.message,
149
+ default: options.defaultValue,
150
+ choices: options.choices.map((choice) => ({
151
+ value: choice.value,
152
+ name: choice.label,
153
+ description: choice.description,
154
+ })),
155
+ }, this.getContext());
156
+ }
157
+ getContext() {
158
+ return {
159
+ input: this.inputStream,
160
+ output: this.outputStream,
161
+ clearPromptOnDone: false,
162
+ };
116
163
  }
117
164
  close() {
118
- this.rl.close();
165
+ // Inquirer manages prompt lifecycle; no explicit teardown needed here.
119
166
  }
120
167
  }
121
168
  class BufferedPromptDriver {
@@ -126,13 +173,87 @@ class BufferedPromptDriver {
126
173
  this.answers = answers;
127
174
  this.output = output;
128
175
  }
129
- async ask(prompt) {
176
+ async input(options) {
177
+ const defaultValue = options.defaultValue ?? "";
178
+ const label = formatBufferedPromptLabel(options.message, defaultValue);
179
+ while (true) {
180
+ const answer = this.readAnswer(label);
181
+ const candidate = (answer.trim() || defaultValue).trim();
182
+ if (options.required !== false && candidate.length === 0) {
183
+ this.output.write(`${options.message} is required.\n`);
184
+ continue;
185
+ }
186
+ const error = options.validator?.(candidate);
187
+ if (error) {
188
+ this.output.write(`${error}\n`);
189
+ continue;
190
+ }
191
+ return candidate;
192
+ }
193
+ }
194
+ async password(options) {
195
+ const defaultValue = options.defaultValue ?? "";
196
+ const label = formatBufferedPromptLabel(options.message, defaultValue, {
197
+ defaultDisplay: defaultValue.length > 0 ? "(saved)" : "",
198
+ });
199
+ while (true) {
200
+ const answer = this.readAnswer(label);
201
+ const candidate = answer.length === 0 ? defaultValue : answer;
202
+ if (options.required !== false && candidate.length === 0) {
203
+ this.output.write(`${options.message} is required.\n`);
204
+ continue;
205
+ }
206
+ const error = options.validator?.(candidate);
207
+ if (error) {
208
+ this.output.write(`${error}\n`);
209
+ continue;
210
+ }
211
+ return candidate;
212
+ }
213
+ }
214
+ async confirm(options) {
215
+ const suffix = options.defaultValue ? "Y/n" : "y/N";
216
+ while (true) {
217
+ const answer = this.readAnswer(`${options.message} [${suffix}]: `).trim().toLowerCase();
218
+ if (!answer) {
219
+ return options.defaultValue;
220
+ }
221
+ if (["y", "yes"].includes(answer)) {
222
+ return true;
223
+ }
224
+ if (["n", "no"].includes(answer)) {
225
+ return false;
226
+ }
227
+ this.output.write("Please answer yes or no.\n");
228
+ }
229
+ }
230
+ async select(options) {
231
+ const choiceList = options.choices.map((choice) => choice.value).join("/");
232
+ while (true) {
233
+ const answer = this.readAnswer(`${options.message} [${choiceList}] (${options.defaultValue}): `).trim().toLowerCase();
234
+ if (!answer) {
235
+ return options.defaultValue;
236
+ }
237
+ const match = options.choices.find((choice) => {
238
+ return choice.value.toLowerCase() === answer || choice.label.trim().toLowerCase() === answer;
239
+ });
240
+ if (match) {
241
+ return match.value;
242
+ }
243
+ this.output.write(`Please choose one of: ${choiceList}.\n`);
244
+ }
245
+ }
246
+ close() { }
247
+ readAnswer(prompt) {
130
248
  const answer = this.answers[this.index] ?? "";
131
249
  this.index += 1;
132
250
  this.output.write(`${prompt}${answer}\n`);
133
251
  return answer;
134
252
  }
135
- close() { }
253
+ }
254
+ function formatBufferedPromptLabel(message, defaultValue, options = {}) {
255
+ const display = options.defaultDisplay ?? defaultValue;
256
+ return display ? `${message} [${display}]: ` : `${message}: `;
136
257
  }
137
258
  async function readBufferedAnswers(input) {
138
259
  const chunks = [];
@@ -147,77 +268,72 @@ async function readBufferedAnswers(input) {
147
268
  }
148
269
  async function createPromptDriver(input, output) {
149
270
  if ("isTTY" in input && input.isTTY) {
150
- return new ReadlinePromptDriver(createInterface({
151
- input,
152
- output,
153
- }));
271
+ return new InquirerPromptDriver(input, output);
154
272
  }
155
273
  return new BufferedPromptDriver(await readBufferedAnswers(input), output);
156
274
  }
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
- }
275
+ async function promptText(driver, label, defaultValue, options = {}) {
276
+ const candidate = await driver.input({
277
+ message: label,
278
+ defaultValue,
279
+ required: options.required,
280
+ validator: options.validator,
281
+ });
282
+ return options.normalize ? options.normalize(candidate) : candidate;
172
283
  }
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
- }
284
+ async function promptPassword(driver, label, defaultValue, options = {}) {
285
+ const candidate = await driver.password({
286
+ message: label,
287
+ defaultValue,
288
+ required: options.required,
289
+ validator: options.validator,
290
+ });
291
+ return options.normalize ? options.normalize(candidate) : candidate;
292
+ }
293
+ async function promptYesNo(driver, label, defaultValue) {
294
+ return driver.confirm({
295
+ message: label,
296
+ defaultValue,
297
+ });
188
298
  }
189
299
  function formatStep(index, total, title) {
190
300
  return `Step ${index}/${total}: ${title}`;
191
301
  }
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),
302
+ async function promptVaultSource(driver, defaultValue) {
303
+ return driver.select({
304
+ message: "VAULT_SOURCE",
305
+ defaultValue,
306
+ choices: [
307
+ {
308
+ value: "local",
309
+ label: "local",
310
+ description: "Read the vault directly from the local filesystem.",
311
+ },
312
+ {
313
+ value: "synology",
314
+ label: "synology",
315
+ description: "Download the vault from Synology NAS into a local cache.",
316
+ },
317
+ ],
204
318
  });
205
- return value;
206
319
  }
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),
320
+ async function promptVaultHashMode(driver, defaultValue) {
321
+ return driver.select({
322
+ message: "VAULT_HASH_MODE",
323
+ defaultValue,
324
+ choices: [
325
+ {
326
+ value: "content",
327
+ label: "content",
328
+ description: "Hash file content to detect changes.",
329
+ },
330
+ {
331
+ value: "mtime",
332
+ label: "mtime",
333
+ description: "Use modification time, recommended for remote Synology sync.",
334
+ },
335
+ ],
219
336
  });
220
- return value;
221
337
  }
222
338
  function canReadWrite(targetPath) {
223
339
  accessSync(targetPath, constants.R_OK | constants.W_OK);
@@ -269,7 +385,7 @@ function getPathDefaults(env, cwd) {
269
385
  };
270
386
  }
271
387
  async function collectEmbeddingSettings(driver, ctx, defaults, env) {
272
- const enabled = await promptYesNo(driver, ctx, "Enable semantic search with embeddings?", defaults.embeddingEnabled);
388
+ const enabled = await promptYesNo(driver, "Enable semantic search with embeddings?", defaults.embeddingEnabled);
273
389
  if (!enabled) {
274
390
  return {
275
391
  embeddingEnabled: false,
@@ -280,11 +396,15 @@ async function collectEmbeddingSettings(driver, ctx, defaults, env) {
280
396
  };
281
397
  }
282
398
  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);
399
+ const embeddingBaseUrl = await promptText(driver, "EMBEDDING_BASE_URL", defaults.embeddingBaseUrl ?? "https://api.openai.com/v1", {
400
+ validator: (value) => validateUrl(value, "EMBEDDING_BASE_URL"),
401
+ });
402
+ const embeddingApiKey = await promptPassword(driver, "EMBEDDING_API_KEY", defaults.embeddingApiKey ?? "", {
403
+ required: true,
404
+ });
405
+ const embeddingModel = await promptText(driver, "EMBEDDING_MODEL", defaults.embeddingModel ?? "text-embedding-3-small", { required: true });
406
+ const embeddingDimensions = await promptText(driver, "EMBEDDING_DIMENSIONS", defaults.embeddingDimensions ?? "384", { validator: (value) => validateNonNegativeInteger(value, "EMBEDDING_DIMENSIONS") });
407
+ const shouldProbe = await promptYesNo(driver, "Probe the embedding endpoint now?", false);
288
408
  if (shouldProbe) {
289
409
  try {
290
410
  const probeEnv = {
@@ -304,7 +424,7 @@ async function collectEmbeddingSettings(driver, ctx, defaults, env) {
304
424
  catch (error) {
305
425
  const message = error instanceof Error ? error.message : String(error);
306
426
  ctx.output.write(`Embedding probe failed: ${message}\n`);
307
- if (await promptYesNo(driver, ctx, "Re-enter embedding settings?", true)) {
427
+ if (await promptYesNo(driver, "Re-enter embedding settings?", true)) {
308
428
  continue;
309
429
  }
310
430
  }
@@ -319,7 +439,7 @@ async function collectEmbeddingSettings(driver, ctx, defaults, env) {
319
439
  }
320
440
  }
321
441
  async function collectAgentSettings(driver, ctx, defaults) {
322
- const enabled = await promptYesNo(driver, ctx, "Enable automatic vault-to-wiki processing?", defaults.agentEnabled);
442
+ const enabled = await promptYesNo(driver, "Enable automatic vault-to-wiki processing?", defaults.agentEnabled);
323
443
  if (!enabled) {
324
444
  return {
325
445
  agentEnabled: false,
@@ -331,17 +451,19 @@ async function collectAgentSettings(driver, ctx, defaults) {
331
451
  }
332
452
  return {
333
453
  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") }),
454
+ agentBaseUrl: await promptText(driver, "WIKI_AGENT_BASE_URL", defaults.agentBaseUrl ?? "https://api.openai.com/v1", { validator: (value) => validateUrl(value, "WIKI_AGENT_BASE_URL") }),
455
+ agentApiKey: await promptPassword(driver, "WIKI_AGENT_API_KEY", defaults.agentApiKey ?? "", { required: true }),
456
+ agentModel: await promptText(driver, "WIKI_AGENT_MODEL", defaults.agentModel ?? "", { required: true }),
457
+ agentBatchSize: await promptText(driver, "WIKI_AGENT_BATCH_SIZE", defaults.agentBatchSize ?? "5", { validator: (value) => validateNonNegativeInteger(value, "WIKI_AGENT_BATCH_SIZE") }),
338
458
  };
339
459
  }
340
460
  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", {
461
+ const synologyBaseUrl = await promptText(driver, "SYNOLOGY_BASE_URL", defaults.synologyBaseUrl ?? "https://nas.example.com:5001", { validator: (value) => validateUrl(value, "SYNOLOGY_BASE_URL") });
462
+ const synologyUsername = await promptText(driver, "SYNOLOGY_USERNAME", defaults.synologyUsername ?? "", { required: true });
463
+ const synologyPassword = await promptPassword(driver, "SYNOLOGY_PASSWORD", defaults.synologyPassword ?? "", {
464
+ required: true,
465
+ });
466
+ const synologyRemotePath = await promptText(driver, "VAULT_SYNOLOGY_REMOTE_PATH", defaults.synologyRemotePath ?? "/homes/user/wiki-vault", {
345
467
  validator: (value) => {
346
468
  try {
347
469
  normalizeSynologyRemotePath(value);
@@ -353,9 +475,9 @@ async function collectSynologySettings(driver, ctx, defaults) {
353
475
  },
354
476
  normalize: (value) => normalizeSynologyRemotePath(value),
355
477
  });
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);
478
+ const vaultHashMode = await promptVaultHashMode(driver, "mtime");
479
+ const synologyVerifySsl = await promptYesNo(driver, "SYNOLOGY_VERIFY_SSL", defaults.synologyVerifySsl);
480
+ const synologyReadonly = await promptYesNo(driver, "SYNOLOGY_READONLY", defaults.synologyReadonly);
359
481
  return {
360
482
  vaultHashMode,
361
483
  synologyBaseUrl,
@@ -371,7 +493,7 @@ async function collectParserSkillSettings(driver, ctx, defaults, wikiPath) {
371
493
  ctx.output.write(`tiangong-wiki-skill is required and will be installed at ${path.join(skillsRoot, "tiangong-wiki-skill")}.\n`);
372
494
  const selected = new Set(defaults.parserSkills);
373
495
  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));
496
+ const enabled = await promptYesNo(driver, `Install parser skill ${skill.name} (${skill.summary})?`, selected.has(skill.name));
375
497
  if (enabled) {
376
498
  selected.add(skill.name);
377
499
  }
@@ -465,27 +587,27 @@ export async function runSetupWizard(env = process.env, options = {}) {
465
587
  const ctx = { cwd, output };
466
588
  try {
467
589
  writeSection(output, "Step 1: Configuration file");
468
- const envFilePath = await promptText(driver, ctx, "Path for the generated .wiki.env file", defaults.envFilePath, {
590
+ const envFilePath = await promptText(driver, "Path for the generated .wiki.env file", defaults.envFilePath, {
469
591
  normalize: (value) => resolveInputPath(value, cwd),
470
592
  });
471
593
  writeSection(output, "Step 2: Vault source");
472
- const vaultSource = await promptVaultSource(driver, ctx, defaults.vaultSource);
594
+ const vaultSource = await promptVaultSource(driver, defaults.vaultSource);
473
595
  const totalSteps = vaultSource === "synology" ? 9 : 8;
474
596
  writeSection(output, formatStep(3, totalSteps, "Core paths"));
475
- const wikiPath = await promptText(driver, ctx, "WIKI_PATH", defaults.wikiPath, {
597
+ const wikiPath = await promptText(driver, "WIKI_PATH", defaults.wikiPath, {
476
598
  normalize: (value) => resolveInputPath(value, cwd),
477
599
  validator: (value) => validateWikiPath(resolveInputPath(value, cwd)),
478
600
  });
479
- const vaultPath = await promptText(driver, ctx, vaultSource === "synology" ? "VAULT_PATH (local cache directory)" : "VAULT_PATH", defaults.vaultPath, {
601
+ const vaultPath = await promptText(driver, vaultSource === "synology" ? "VAULT_PATH (local cache directory)" : "VAULT_PATH", defaults.vaultPath, {
480
602
  normalize: (value) => resolveInputPath(value, cwd),
481
603
  });
482
- const dbPath = await promptText(driver, ctx, "WIKI_DB_PATH", defaults.dbPath, {
604
+ const dbPath = await promptText(driver, "WIKI_DB_PATH", defaults.dbPath, {
483
605
  normalize: (value) => resolveInputPath(value, cwd),
484
606
  });
485
- const configPath = await promptText(driver, ctx, "WIKI_CONFIG_PATH", defaults.configPath, {
607
+ const configPath = await promptText(driver, "WIKI_CONFIG_PATH", defaults.configPath, {
486
608
  normalize: (value) => resolveInputPath(value, cwd),
487
609
  });
488
- const templatesPath = await promptText(driver, ctx, "WIKI_TEMPLATES_PATH", defaults.templatesPath, {
610
+ const templatesPath = await promptText(driver, "WIKI_TEMPLATES_PATH", defaults.templatesPath, {
489
611
  normalize: (value) => resolveInputPath(value, cwd),
490
612
  });
491
613
  let synologyValues;
@@ -506,7 +628,7 @@ export async function runSetupWizard(env = process.env, options = {}) {
506
628
  };
507
629
  }
508
630
  writeSection(output, formatStep(vaultSource === "synology" ? 5 : 4, totalSteps, "Sync schedule"));
509
- const syncInterval = await promptText(driver, ctx, "WIKI_SYNC_INTERVAL (seconds)", defaults.syncInterval, {
631
+ const syncInterval = await promptText(driver, "WIKI_SYNC_INTERVAL (seconds)", defaults.syncInterval, {
510
632
  validator: (value) => validateNonNegativeInteger(value, "WIKI_SYNC_INTERVAL"),
511
633
  });
512
634
  writeSection(output, formatStep(vaultSource === "synology" ? 6 : 5, totalSteps, "Embedding configuration"));
@@ -531,7 +653,7 @@ export async function runSetupWizard(env = process.env, options = {}) {
531
653
  };
532
654
  writeSection(output, formatStep(totalSteps, totalSteps, "Confirm"));
533
655
  output.write(`${buildSetupSummary(values)}\n`);
534
- const confirmed = await promptYesNo(driver, ctx, "Write configuration and scaffold workspace assets?", true);
656
+ const confirmed = await promptYesNo(driver, "Write configuration and scaffold workspace assets?", true);
535
657
  if (!confirmed) {
536
658
  throw new AppError("Setup aborted before writing any files.", "runtime");
537
659
  }
@@ -551,9 +673,12 @@ export async function runSetupWizard(env = process.env, options = {}) {
551
673
  output,
552
674
  }));
553
675
  writeSetupEnvFile(values);
676
+ const globalConfig = writeGlobalConfig(values.envFilePath, env);
554
677
  output.write([
555
678
  "\ntiangong-wiki setup complete",
556
679
  `configuration file: ${values.envFilePath}`,
680
+ `default workspace config: ${globalConfig.configPath}`,
681
+ `workspace root: ${workspaceRoot}`,
557
682
  `skills root: ${skillsRoot}`,
558
683
  `tiangong-wiki-skill: ${wikiSkillInstall.status}`,
559
684
  `parser skills: ${values.parserSkills.length > 0 ? values.parserSkills.join(", ") : "(none)"}`,
@@ -565,6 +690,11 @@ export async function runSetupWizard(env = process.env, options = {}) {
565
690
  : []),
566
691
  "",
567
692
  "Next steps:",
693
+ `- Commands inside ${JSON.stringify(workspaceRoot)} will auto-discover the local .wiki.env first.`,
694
+ `- Commands outside the workspace will fall back to the default workspace config at ${globalConfig.configPath}.`,
695
+ `- Example: cd ${JSON.stringify(workspaceRoot)} && tiangong-wiki doctor`,
696
+ `- Example: tiangong-wiki --env-file ${JSON.stringify(values.envFilePath)} doctor`,
697
+ `- Example: cd ${JSON.stringify(workspaceRoot)} && tiangong-wiki init`,
568
698
  "- Run `tiangong-wiki doctor` to validate the generated configuration.",
569
699
  "- Run `tiangong-wiki init` to create index.db and perform the first sync.",
570
700
  ...(values.vaultSource === "synology"
@@ -576,6 +706,7 @@ export async function runSetupWizard(env = process.env, options = {}) {
576
706
  ].join("\n"));
577
707
  return {
578
708
  envFilePath: values.envFilePath,
709
+ globalConfigPath: globalConfig.configPath,
579
710
  createdDirectories: bootstrap.createdDirectories,
580
711
  copiedConfig: bootstrap.copiedConfig,
581
712
  copiedTemplates: bootstrap.copiedTemplates,
@@ -892,11 +1023,26 @@ export async function buildDoctorReport(env = process.env, options = {}) {
892
1023
  if (envFile.missingRequestedPath && envFile.requestedPath) {
893
1024
  collectDoctorCheck(checks, "error", "env-file", `Requested env file does not exist: ${envFile.requestedPath}`, "Create the env file or rerun `tiangong-wiki setup`.");
894
1025
  }
1026
+ else if (envFile.missingDefaultPath && envFile.defaultPath) {
1027
+ collectDoctorCheck(checks, "error", "env-file", `The default workspace config points to a missing env file: ${envFile.defaultPath}`, envFile.globalConfigPath
1028
+ ? `Fix or remove ${envFile.globalConfigPath}, rerun \`tiangong-wiki setup\`, or pass \`--env-file\` explicitly.`
1029
+ : "Fix the default workspace config, rerun `tiangong-wiki setup`, or pass `--env-file` explicitly.");
1030
+ }
895
1031
  else if (envFile.loadedPath) {
896
- collectDoctorCheck(checks, "ok", "env-file", `Loaded configuration from ${envFile.loadedPath}${envFile.autoDiscovered ? " (auto-discovered)." : "."}`);
1032
+ const sourceLabel = envFile.source === "explicit-env-file"
1033
+ ? "from --env-file or WIKI_ENV_FILE."
1034
+ : envFile.source === "nearest-env-file"
1035
+ ? "from the current workspace (auto-discovered)."
1036
+ : envFile.source === "global-default-env-file"
1037
+ ? "from the global default workspace config."
1038
+ : ".";
1039
+ collectDoctorCheck(checks, "ok", "env-file", `Loaded configuration from ${envFile.loadedPath} ${sourceLabel}`);
1040
+ }
1041
+ else if (envFile.source === "process-env") {
1042
+ collectDoctorCheck(checks, "ok", "env-file", "Using runtime paths provided directly via process.env; no .wiki.env file was loaded.");
897
1043
  }
898
1044
  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.");
1045
+ collectDoctorCheck(checks, "warn", "env-file", "No workspace configuration was found from --env-file, WIKI_ENV_FILE, the current directory, or the global default workspace config.", "Run `tiangong-wiki setup`, set `WIKI_ENV_FILE`, or pass `--env-file` to point at a workspace explicitly.");
900
1046
  }
901
1047
  const wikiPath = env.WIKI_PATH ? path.resolve(env.WIKI_PATH) : null;
902
1048
  const wikiRoot = wikiPath ? path.resolve(wikiPath, "..") : null;
@@ -930,6 +1076,10 @@ export async function buildDoctorReport(env = process.env, options = {}) {
930
1076
  loadedPath: envFile.loadedPath,
931
1077
  autoDiscovered: envFile.autoDiscovered,
932
1078
  missingRequestedPath: envFile.missingRequestedPath,
1079
+ missingDefaultPath: envFile.missingDefaultPath,
1080
+ source: envFile.source,
1081
+ globalConfigPath: envFile.globalConfigPath,
1082
+ defaultPath: envFile.defaultPath,
933
1083
  },
934
1084
  effectivePaths: {
935
1085
  wikiPath,