@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,186 @@
1
+ import { getMeta } from "../core/db.js";
2
+ import { resolveRuntimePaths } from "../core/paths.js";
3
+ import { openRuntimeDb } from "../core/runtime.js";
4
+ import { inspectDaemonAvailability, requestDaemonJson } from "../daemon/client.js";
5
+ import { runDaemonServer } from "../daemon/server.js";
6
+ import { clearDaemonArtifacts, isDaemonProcessRunning, readDaemonPid, readDaemonState, writeDaemonPid } from "../daemon/state.js";
7
+ import { AppError } from "../utils/errors.js";
8
+ import { pathExistsSync } from "../utils/fs.js";
9
+ import { ensureTextOrJson, writeJson, writeText } from "../utils/output.js";
10
+ import { spawnDetachedCurrentProcess } from "../utils/process.js";
11
+ const DAEMON_STARTUP_TIMEOUT_MS = 5_000;
12
+ const DAEMON_STARTUP_POLL_MS = 100;
13
+ async function sleep(ms) {
14
+ await new Promise((resolve) => setTimeout(resolve, ms));
15
+ }
16
+ function resolveDaemonRunLaunchMode(env = process.env) {
17
+ return env.WIKI_DAEMON_LAUNCH_MODE === "start" ? "start" : "run";
18
+ }
19
+ function readLastSyncAt(env = process.env) {
20
+ try {
21
+ const { db } = openRuntimeDb(env);
22
+ try {
23
+ return getMeta(db, "last_sync_at");
24
+ }
25
+ finally {
26
+ db.close();
27
+ }
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ }
33
+ function buildFallbackStatus(env = process.env) {
34
+ const paths = resolveRuntimePaths(env);
35
+ const pidFromPidFile = readDaemonPid(paths.daemonPidPath);
36
+ const state = readDaemonState(paths.daemonStatePath);
37
+ const pid = state?.pid ?? pidFromPidFile;
38
+ const running = isDaemonProcessRunning(pid);
39
+ return {
40
+ running,
41
+ pid,
42
+ host: state?.host ?? null,
43
+ port: state?.port ?? null,
44
+ lastSyncAt: readLastSyncAt(env),
45
+ nextSyncAt: state?.nextRunAt ?? null,
46
+ lastResult: state?.lastResult ?? null,
47
+ syncIntervalSeconds: state?.syncIntervalSeconds ?? null,
48
+ launchMode: state?.launchMode ?? null,
49
+ currentTask: state?.currentTask ?? null,
50
+ state,
51
+ };
52
+ }
53
+ async function stopViaSignal(pid, env = process.env) {
54
+ const paths = resolveRuntimePaths(env);
55
+ try {
56
+ process.kill(pid, "SIGTERM");
57
+ }
58
+ catch (error) {
59
+ if (error.code === "ESRCH") {
60
+ clearDaemonArtifacts(paths);
61
+ return { status: "stopped", pid: null };
62
+ }
63
+ throw error;
64
+ }
65
+ for (let index = 0; index < 10; index += 1) {
66
+ if (!isDaemonProcessRunning(pid)) {
67
+ clearDaemonArtifacts(paths);
68
+ return { status: "stopped", pid: null };
69
+ }
70
+ await sleep(100);
71
+ }
72
+ return { status: "stopping", pid };
73
+ }
74
+ async function waitForHealthyDaemon(expectedPid, env = process.env) {
75
+ const paths = resolveRuntimePaths(env);
76
+ const deadline = Date.now() + DAEMON_STARTUP_TIMEOUT_MS;
77
+ while (Date.now() < deadline) {
78
+ const availability = await inspectDaemonAvailability(env);
79
+ if (availability.status === "healthy" && availability.pid === expectedPid) {
80
+ return;
81
+ }
82
+ if (!isDaemonProcessRunning(expectedPid)) {
83
+ clearDaemonArtifacts(paths);
84
+ throw new AppError(`Failed to start daemon. Check ${paths.daemonLogPath} for details.`, "runtime");
85
+ }
86
+ await sleep(DAEMON_STARTUP_POLL_MS);
87
+ }
88
+ await stopViaSignal(expectedPid, env).catch(() => undefined);
89
+ clearDaemonArtifacts(paths);
90
+ throw new AppError(`Timed out waiting for daemon to become healthy. Check ${paths.daemonLogPath} for details.`, "runtime");
91
+ }
92
+ function renderStatusText(payload) {
93
+ return [
94
+ "tiangong-wiki daemon status",
95
+ `running: ${payload.running}`,
96
+ `pid: ${payload.pid ?? ""}`,
97
+ `host: ${payload.host ?? ""}`,
98
+ `port: ${payload.port ?? ""}`,
99
+ `lastSyncAt: ${payload.lastSyncAt ?? ""}`,
100
+ `nextSyncAt: ${payload.nextSyncAt ?? ""}`,
101
+ `lastResult: ${payload.lastResult ?? ""}`,
102
+ `syncIntervalSeconds: ${payload.syncIntervalSeconds ?? ""}`,
103
+ `launchMode: ${payload.launchMode ?? ""}`,
104
+ `currentTask: ${payload.currentTask ?? ""}`,
105
+ ].join("\n");
106
+ }
107
+ export function registerDaemonCommand(program) {
108
+ const daemon = program.command("daemon").description("Manage the background sync daemon");
109
+ daemon
110
+ .command("start")
111
+ .description("Start the wiki daemon as a detached local background service")
112
+ .action(async () => {
113
+ const paths = resolveRuntimePaths(process.env);
114
+ const availability = await inspectDaemonAvailability(process.env);
115
+ if (availability.status === "healthy" || availability.status === "degraded") {
116
+ throw new AppError(`Daemon is already running with PID ${availability.pid}`, "runtime", availability);
117
+ }
118
+ if (pathExistsSync(paths.daemonPidPath) || pathExistsSync(paths.daemonStatePath)) {
119
+ clearDaemonArtifacts(paths);
120
+ }
121
+ const pid = spawnDetachedCurrentProcess(["daemon", "run"], {
122
+ env: {
123
+ ...process.env,
124
+ WIKI_DAEMON_LAUNCH_MODE: "start",
125
+ },
126
+ logFile: paths.daemonLogPath,
127
+ });
128
+ if (!pid) {
129
+ throw new AppError("Failed to start daemon", "runtime");
130
+ }
131
+ writeDaemonPid(paths.daemonPidPath, pid);
132
+ await waitForHealthyDaemon(pid, process.env);
133
+ writeJson({ status: "started", pid });
134
+ });
135
+ daemon
136
+ .command("stop")
137
+ .description("Stop the wiki daemon")
138
+ .action(async () => {
139
+ const availability = await inspectDaemonAvailability(process.env);
140
+ if (availability.status === "healthy" && availability.endpoint) {
141
+ writeJson(await requestDaemonJson({
142
+ endpoint: availability.endpoint,
143
+ method: "POST",
144
+ path: "/shutdown",
145
+ }));
146
+ return;
147
+ }
148
+ const paths = resolveRuntimePaths(process.env);
149
+ const pid = availability.pid ?? readDaemonPid(paths.daemonPidPath);
150
+ if (!isDaemonProcessRunning(pid)) {
151
+ clearDaemonArtifacts(paths);
152
+ writeJson({ status: "stopped", pid: null });
153
+ return;
154
+ }
155
+ writeJson(await stopViaSignal(pid, process.env));
156
+ });
157
+ daemon
158
+ .command("status")
159
+ .description("Show daemon state and scheduling information")
160
+ .option("--format <format>", "text or json", "text")
161
+ .action(async (options) => {
162
+ const format = ensureTextOrJson(options.format);
163
+ const availability = await inspectDaemonAvailability(process.env);
164
+ const payload = availability.status === "healthy" && availability.endpoint
165
+ ? await requestDaemonJson({
166
+ endpoint: availability.endpoint,
167
+ method: "GET",
168
+ path: "/status",
169
+ })
170
+ : buildFallbackStatus(process.env);
171
+ if (format === "json") {
172
+ writeJson(payload);
173
+ return;
174
+ }
175
+ writeText(renderStatusText(payload));
176
+ });
177
+ daemon
178
+ .command("run")
179
+ .description("Run the wiki daemon in the foreground")
180
+ .action(async () => {
181
+ await runDaemonServer({
182
+ env: process.env,
183
+ launchMode: resolveDaemonRunLaunchMode(process.env),
184
+ });
185
+ });
186
+ }
@@ -0,0 +1,112 @@
1
+ import path from "node:path";
2
+ import { inspectDaemonAvailability } from "../daemon/client.js";
3
+ import { clearDaemonArtifacts, isDaemonProcessRunning, writeDaemonPid } from "../daemon/state.js";
4
+ import { resolveRuntimePaths } from "../core/paths.js";
5
+ import { AppError } from "../utils/errors.js";
6
+ import { pathExistsSync } from "../utils/fs.js";
7
+ import { ensureTextOrJson, writeJson, writeText } from "../utils/output.js";
8
+ import { openTarget, spawnDetachedCurrentProcess } from "../utils/process.js";
9
+ const DAEMON_STARTUP_TIMEOUT_MS = 5_000;
10
+ const DAEMON_STARTUP_POLL_MS = 100;
11
+ async function sleep(ms) {
12
+ await new Promise((resolve) => setTimeout(resolve, ms));
13
+ }
14
+ function getDashboardDistEntry(env = process.env) {
15
+ const paths = resolveRuntimePaths(env);
16
+ return path.join(paths.packageRoot, "dist", "dashboard", "index.html");
17
+ }
18
+ async function waitForHealthyDaemon(expectedPid, env = process.env) {
19
+ const paths = resolveRuntimePaths(env);
20
+ const deadline = Date.now() + DAEMON_STARTUP_TIMEOUT_MS;
21
+ while (Date.now() < deadline) {
22
+ const availability = await inspectDaemonAvailability(env);
23
+ if (availability.status === "healthy" && availability.endpoint && availability.pid === expectedPid) {
24
+ return availability.endpoint;
25
+ }
26
+ if (!isDaemonProcessRunning(expectedPid)) {
27
+ clearDaemonArtifacts(paths);
28
+ throw new AppError(`Failed to start daemon. Check ${paths.daemonLogPath} for details.`, "runtime");
29
+ }
30
+ await sleep(DAEMON_STARTUP_POLL_MS);
31
+ }
32
+ clearDaemonArtifacts(paths);
33
+ throw new AppError(`Timed out waiting for daemon to become healthy. Check ${paths.daemonLogPath} for details.`, "runtime");
34
+ }
35
+ async function stopDegradedDaemon(env = process.env) {
36
+ const paths = resolveRuntimePaths(env);
37
+ const availability = await inspectDaemonAvailability(env);
38
+ if (availability.status !== "degraded" || !availability.pid) {
39
+ return;
40
+ }
41
+ try {
42
+ process.kill(availability.pid, "SIGTERM");
43
+ }
44
+ catch (error) {
45
+ if (error.code !== "ESRCH") {
46
+ throw error;
47
+ }
48
+ }
49
+ const deadline = Date.now() + 2_000;
50
+ while (Date.now() < deadline) {
51
+ if (!isDaemonProcessRunning(availability.pid)) {
52
+ clearDaemonArtifacts(paths);
53
+ return;
54
+ }
55
+ await sleep(100);
56
+ }
57
+ clearDaemonArtifacts(paths);
58
+ }
59
+ async function ensureDashboardDaemon(env = process.env) {
60
+ const availability = await inspectDaemonAvailability(env);
61
+ if (availability.status === "healthy" && availability.endpoint) {
62
+ return availability.endpoint;
63
+ }
64
+ if (availability.status === "degraded") {
65
+ await stopDegradedDaemon(env);
66
+ }
67
+ const paths = resolveRuntimePaths(env);
68
+ const pid = spawnDetachedCurrentProcess(["daemon", "run"], {
69
+ env: {
70
+ ...env,
71
+ WIKI_DAEMON_LAUNCH_MODE: "start",
72
+ },
73
+ logFile: paths.daemonLogPath,
74
+ });
75
+ if (!pid) {
76
+ throw new AppError("Failed to start daemon", "runtime");
77
+ }
78
+ writeDaemonPid(paths.daemonPidPath, pid);
79
+ return waitForHealthyDaemon(pid, env);
80
+ }
81
+ export function registerDashboardCommand(program) {
82
+ program
83
+ .command("dashboard")
84
+ .description("Open the local dashboard in a browser, starting the daemon if needed")
85
+ .option("--no-open", "Do not open the dashboard URL in a browser")
86
+ .option("--format <format>", "text or json", "text")
87
+ .action(async (options) => {
88
+ const format = ensureTextOrJson(options.format);
89
+ const dashboardEntry = getDashboardDistEntry(process.env);
90
+ if (!pathExistsSync(dashboardEntry)) {
91
+ throw new AppError(`Dashboard assets are missing at ${dashboardEntry}. Run \`npm run build\` before opening the dashboard.`, "not_found");
92
+ }
93
+ const endpoint = await ensureDashboardDaemon(process.env);
94
+ const url = `http://${endpoint.host}:${endpoint.port}/dashboard`;
95
+ const opened = options.open !== false;
96
+ if (opened) {
97
+ openTarget(url);
98
+ }
99
+ const payload = {
100
+ url,
101
+ opened,
102
+ pid: endpoint.pid,
103
+ host: endpoint.host,
104
+ port: endpoint.port,
105
+ };
106
+ if (format === "json") {
107
+ writeJson(payload);
108
+ return;
109
+ }
110
+ writeText(url);
111
+ });
112
+ }
@@ -0,0 +1,22 @@
1
+ import { buildDoctorReport, formatDoctorReport } from "../core/onboarding.js";
2
+ import { ensureTextOrJson, writeJson, writeText } from "../utils/output.js";
3
+ export function registerDoctorCommand(program) {
4
+ program
5
+ .command("doctor")
6
+ .description("Diagnose the current wiki configuration and runtime health")
7
+ .option("--probe", "Probe configured remote services such as embeddings and Synology NAS")
8
+ .option("--format <format>", "Output format: text or json", "text")
9
+ .action(async (options) => {
10
+ const format = ensureTextOrJson(options.format);
11
+ const report = await buildDoctorReport(process.env, { probe: options.probe === true });
12
+ if (format === "json") {
13
+ writeJson(report);
14
+ }
15
+ else {
16
+ writeText(formatDoctorReport(report));
17
+ }
18
+ if (!report.ok) {
19
+ process.exitCode = 2;
20
+ }
21
+ });
22
+ }
@@ -0,0 +1,28 @@
1
+ import { executeServerBackedOperation, requestDaemonJson } from "../daemon/client.js";
2
+ import { exportGraphContent } from "../operations/export.js";
3
+ import { writeJson, writeText } from "../utils/output.js";
4
+ import { writeTextFileSync } from "../utils/fs.js";
5
+ export function registerExportGraphCommand(program) {
6
+ program
7
+ .command("export-graph")
8
+ .description("Export graph nodes and edges as JSON")
9
+ .option("--output <filePath>", "Write JSON to a file")
10
+ .action(async (options) => {
11
+ const payload = await executeServerBackedOperation({
12
+ kind: "read",
13
+ local: () => exportGraphContent(process.env),
14
+ remote: (endpoint) => requestDaemonJson({
15
+ endpoint,
16
+ method: "POST",
17
+ path: "/export/graph",
18
+ }),
19
+ });
20
+ const content = `${JSON.stringify(payload, null, 2)}\n`;
21
+ if (options.output) {
22
+ writeTextFileSync(options.output, content);
23
+ writeJson({ output: options.output, nodes: payload.nodes.length, edges: payload.edges.length });
24
+ return;
25
+ }
26
+ writeText(content);
27
+ });
28
+ }
@@ -0,0 +1,31 @@
1
+ import { executeServerBackedOperation, requestDaemonJson } from "../daemon/client.js";
2
+ import { exportIndexContent } from "../operations/export.js";
3
+ import { writeText } from "../utils/output.js";
4
+ import { writeTextFileSync } from "../utils/fs.js";
5
+ export function registerExportIndexCommand(program) {
6
+ program
7
+ .command("export-index")
8
+ .description("Export a human-readable Markdown index of pages")
9
+ .option("--output <filePath>", "Write Markdown output to a file")
10
+ .option("--group-by <mode>", "Group by pageType or tags", "pageType")
11
+ .action(async (options) => {
12
+ const payload = await executeServerBackedOperation({
13
+ kind: "read",
14
+ local: () => exportIndexContent(process.env, {
15
+ groupBy: options.groupBy ?? undefined,
16
+ }),
17
+ remote: (endpoint) => requestDaemonJson({
18
+ endpoint,
19
+ method: "POST",
20
+ path: "/export/index",
21
+ body: {
22
+ groupBy: options.groupBy ?? undefined,
23
+ },
24
+ }),
25
+ });
26
+ if (options.output) {
27
+ writeTextFileSync(options.output, `${payload.content}\n`);
28
+ }
29
+ writeText(String(payload.content ?? ""));
30
+ });
31
+ }
@@ -0,0 +1,36 @@
1
+ import { executeServerBackedOperation, requestDaemonJson } from "../daemon/client.js";
2
+ import { camelToSnake } from "../utils/case.js";
3
+ import { findPages } from "../operations/query.js";
4
+ import { writeJson } from "../utils/output.js";
5
+ export function registerFindCommand(program, config) {
6
+ const command = program
7
+ .command("find")
8
+ .description("Find wiki pages by structured metadata filters")
9
+ .option("--type <pageType>", "Filter by pageType")
10
+ .option("--status <status>", "Filter by status")
11
+ .option("--visibility <visibility>", "Filter by visibility")
12
+ .option("--tag <tag>", "Filter by tag")
13
+ .option("--node-id <nodeId>", "Filter by nodeId")
14
+ .option("--updated-after <date>", "Filter by updatedAt >= date")
15
+ .option("--sort <column>", "Sort column")
16
+ .option("--limit <number>", "Max rows to return", "50");
17
+ const dynamicFields = config
18
+ ? [...new Set([...Object.keys(config.customColumns), ...Object.values(config.templates).flatMap((template) => Object.keys(template.columns))])]
19
+ : [];
20
+ for (const field of dynamicFields) {
21
+ command.option(`--${camelToSnake(field).replace(/_/g, "-")} <value>`, `Filter by ${field}`);
22
+ }
23
+ command.action(async (options) => {
24
+ const payload = await executeServerBackedOperation({
25
+ kind: "read",
26
+ local: () => findPages(process.env, options),
27
+ remote: (endpoint) => requestDaemonJson({
28
+ endpoint,
29
+ method: "GET",
30
+ path: "/find",
31
+ query: options,
32
+ }),
33
+ });
34
+ writeJson(payload);
35
+ });
36
+ }
@@ -0,0 +1,32 @@
1
+ import { executeServerBackedOperation, requestDaemonJson } from "../daemon/client.js";
2
+ import { ftsSearchPages } from "../operations/query.js";
3
+ import { writeJson } from "../utils/output.js";
4
+ export function registerFtsCommand(program) {
5
+ program
6
+ .command("fts")
7
+ .description("Run full-text search over title, tags, and summary text")
8
+ .argument("<query>", "FTS query")
9
+ .option("--type <pageType>", "Optional pageType filter")
10
+ .option("--limit <number>", "Max rows to return", "20")
11
+ .action(async (query, options) => {
12
+ const payload = await executeServerBackedOperation({
13
+ kind: "read",
14
+ local: () => ftsSearchPages(process.env, {
15
+ query,
16
+ type: options.type ?? undefined,
17
+ limit: options.limit ?? undefined,
18
+ }),
19
+ remote: (endpoint) => requestDaemonJson({
20
+ endpoint,
21
+ method: "GET",
22
+ path: "/fts",
23
+ query: {
24
+ query,
25
+ type: options.type ?? undefined,
26
+ limit: options.limit ?? undefined,
27
+ },
28
+ }),
29
+ });
30
+ writeJson(payload);
31
+ });
32
+ }
@@ -0,0 +1,35 @@
1
+ import { executeServerBackedOperation, requestDaemonJson } from "../daemon/client.js";
2
+ import { traverseGraph } from "../operations/query.js";
3
+ import { writeJson } from "../utils/output.js";
4
+ export function registerGraphCommand(program) {
5
+ program
6
+ .command("graph")
7
+ .description("Traverse the tiangong-wiki graph with recursive CTEs")
8
+ .argument("<root>", "Root nodeId or page id")
9
+ .option("--depth <number>", "Traversal depth", "1")
10
+ .option("--edge-type <edgeType>", "Optional edge type filter")
11
+ .option("--direction <direction>", "outgoing, incoming, or both", "both")
12
+ .action(async (root, options) => {
13
+ const payload = await executeServerBackedOperation({
14
+ kind: "read",
15
+ local: () => traverseGraph(process.env, {
16
+ root,
17
+ depth: options.depth ?? undefined,
18
+ edgeType: options.edgeType ?? undefined,
19
+ direction: options.direction ?? undefined,
20
+ }),
21
+ remote: (endpoint) => requestDaemonJson({
22
+ endpoint,
23
+ method: "GET",
24
+ path: "/graph",
25
+ query: {
26
+ root,
27
+ depth: options.depth ?? undefined,
28
+ edgeType: options.edgeType ?? undefined,
29
+ direction: options.direction ?? undefined,
30
+ },
31
+ }),
32
+ });
33
+ writeJson(payload);
34
+ });
35
+ }
@@ -0,0 +1,48 @@
1
+ import { EmbeddingClient } from "../core/embedding.js";
2
+ import { resolveRuntimePaths } from "../core/paths.js";
3
+ import { syncWorkspace } from "../core/sync.js";
4
+ import { getWikiAgentStatus } from "../core/vault-processing.js";
5
+ import { scaffoldWorkspaceAssets } from "../core/workspace-bootstrap.js";
6
+ import { AppError } from "../utils/errors.js";
7
+ import { spawnDetachedCurrentProcess } from "../utils/process.js";
8
+ import { writeJson } from "../utils/output.js";
9
+ export function registerInitCommand(program) {
10
+ program
11
+ .command("init")
12
+ .description("Initialize wiki workspace assets and run the first structured sync")
13
+ .option("--force", "Force a full rebuild of the index")
14
+ .action(async (options) => {
15
+ const paths = resolveRuntimePaths(process.env);
16
+ const bootstrap = scaffoldWorkspaceAssets({
17
+ packageRoot: paths.packageRoot,
18
+ wikiRoot: paths.wikiRoot,
19
+ wikiPath: paths.wikiPath,
20
+ vaultPath: paths.vaultPath,
21
+ templatesPath: paths.templatesPath,
22
+ configPath: paths.configPath,
23
+ });
24
+ const structuredSync = await syncWorkspace({
25
+ force: options.force === true,
26
+ skipEmbedding: true,
27
+ });
28
+ let backgroundEmbeddingStarted = false;
29
+ let backgroundPid;
30
+ if (EmbeddingClient.fromEnv(process.env)) {
31
+ backgroundPid = spawnDetachedCurrentProcess(["embed-pending"], { env: process.env });
32
+ backgroundEmbeddingStarted = typeof backgroundPid === "number";
33
+ }
34
+ const wikiAgent = getWikiAgentStatus(process.env);
35
+ if (wikiAgent.enabled && wikiAgent.missing.length > 0) {
36
+ throw new AppError(`WIKI_AGENT_ENABLED=true but missing required settings: ${wikiAgent.missing.join(", ")}`, "config");
37
+ }
38
+ writeJson({
39
+ initialized: true,
40
+ copiedConfig: bootstrap.copiedConfig,
41
+ copiedTemplates: bootstrap.copiedTemplates,
42
+ sync: structuredSync,
43
+ backgroundEmbeddingStarted,
44
+ ...(backgroundEmbeddingStarted ? { backgroundPid } : {}),
45
+ backgroundQueueProcessingStarted: false,
46
+ });
47
+ });
48
+ }
@@ -0,0 +1,35 @@
1
+ import { executeServerBackedOperation, requestDaemonJson } from "../daemon/client.js";
2
+ import { renderLintResult, runLint } from "../operations/query.js";
3
+ import { ensureTextOrJson, writeJson, writeText } from "../utils/output.js";
4
+ export function registerLintCommand(program) {
5
+ program
6
+ .command("lint")
7
+ .description("Validate wiki pages, references, and graph integrity")
8
+ .option("--path <pagePath>", "Lint only one page")
9
+ .option("--level <level>", "error, warning, or info", "info")
10
+ .option("--format <format>", "text or json", "text")
11
+ .action(async (options) => {
12
+ const format = ensureTextOrJson(options.format);
13
+ const payload = await executeServerBackedOperation({
14
+ kind: "read",
15
+ local: () => runLint(process.env, {
16
+ path: options.path ?? undefined,
17
+ level: options.level ?? undefined,
18
+ }),
19
+ remote: (endpoint) => requestDaemonJson({
20
+ endpoint,
21
+ method: "GET",
22
+ path: "/lint",
23
+ query: {
24
+ path: options.path ?? undefined,
25
+ level: options.level ?? undefined,
26
+ },
27
+ }),
28
+ });
29
+ if (format === "json") {
30
+ writeJson(payload);
31
+ return;
32
+ }
33
+ writeText(renderLintResult(payload));
34
+ });
35
+ }
@@ -0,0 +1,28 @@
1
+ import { executeServerBackedOperation, requestDaemonJson } from "../daemon/client.js";
2
+ import { listPages } from "../operations/query.js";
3
+ import { writeJson } from "../utils/output.js";
4
+ export function registerListCommand(program) {
5
+ program
6
+ .command("list")
7
+ .description("List wiki pages")
8
+ .option("--type <pageType>", "Optional pageType filter")
9
+ .option("--sort <column>", "Sort column", "updatedAt")
10
+ .option("--limit <number>", "Max rows to return", "50")
11
+ .action(async (options) => {
12
+ const payload = await executeServerBackedOperation({
13
+ kind: "read",
14
+ local: () => listPages(process.env, options),
15
+ remote: (endpoint) => requestDaemonJson({
16
+ endpoint,
17
+ method: "GET",
18
+ path: "/list",
19
+ query: {
20
+ type: options.type ?? undefined,
21
+ sort: options.sort ?? undefined,
22
+ limit: options.limit ?? undefined,
23
+ },
24
+ }),
25
+ });
26
+ writeJson(payload);
27
+ });
28
+ }
@@ -0,0 +1,24 @@
1
+ import { executeServerBackedOperation, requestDaemonJson } from "../daemon/client.js";
2
+ import { getPageInfo } from "../operations/query.js";
3
+ import { writeJson } from "../utils/output.js";
4
+ export function registerPageInfoCommand(program) {
5
+ program
6
+ .command("page-info")
7
+ .description("Show full metadata and edge details for one page")
8
+ .argument("<pageId>", "Page id relative to wiki/pages")
9
+ .action(async (inputPageId) => {
10
+ const payload = await executeServerBackedOperation({
11
+ kind: "read",
12
+ local: () => getPageInfo(process.env, inputPageId),
13
+ remote: (endpoint) => requestDaemonJson({
14
+ endpoint,
15
+ method: "GET",
16
+ path: "/page-info",
17
+ query: {
18
+ pageId: inputPageId,
19
+ },
20
+ }),
21
+ });
22
+ writeJson(payload);
23
+ });
24
+ }
@@ -0,0 +1,32 @@
1
+ import { executeServerBackedOperation, requestDaemonJson } from "../daemon/client.js";
2
+ import { searchPages } from "../operations/query.js";
3
+ import { writeJson } from "../utils/output.js";
4
+ export function registerSearchCommand(program) {
5
+ program
6
+ .command("search")
7
+ .description("Run semantic search over page summary embeddings")
8
+ .argument("<query>", "Natural language query")
9
+ .option("--type <pageType>", "Optional pageType filter")
10
+ .option("--limit <number>", "Max rows to return", "10")
11
+ .action(async (query, options) => {
12
+ const payload = await executeServerBackedOperation({
13
+ kind: "read",
14
+ local: () => searchPages(process.env, {
15
+ query,
16
+ type: options.type ?? undefined,
17
+ limit: options.limit ?? undefined,
18
+ }),
19
+ remote: (endpoint) => requestDaemonJson({
20
+ endpoint,
21
+ method: "GET",
22
+ path: "/search",
23
+ query: {
24
+ query,
25
+ type: options.type ?? undefined,
26
+ limit: options.limit ?? undefined,
27
+ },
28
+ }),
29
+ });
30
+ writeJson(payload);
31
+ });
32
+ }