@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,412 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import http from "node:http";
3
+ import https from "node:https";
4
+ import path from "node:path";
5
+ import { URL } from "node:url";
6
+ import { AppError } from "../utils/errors.js";
7
+ const TRUE_VALUES = new Set(["1", "true", "yes", "on", "y"]);
8
+ const FALSE_VALUES = new Set(["0", "false", "no", "off", "n"]);
9
+ const COMMON_ERROR_CODES = {
10
+ 100: "Unknown error",
11
+ 101: "No parameter of API, method, or version",
12
+ 102: "Requested API does not exist",
13
+ 103: "Requested method does not exist",
14
+ 104: "Requested version does not support this function",
15
+ 105: "Session has no permission",
16
+ 106: "Session timeout",
17
+ 107: "Session interrupted by duplicate login",
18
+ 119: "SID not found",
19
+ };
20
+ const FILE_ERROR_CODES = {
21
+ 400: "Invalid parameter of file operation",
22
+ 401: "Unknown error of file operation",
23
+ 402: "System is too busy",
24
+ 403: "Invalid user for this file operation",
25
+ 404: "Invalid group for this file operation",
26
+ 405: "Invalid user and group for this file operation",
27
+ 406: "Cannot get user/group info from account server",
28
+ 407: "Operation not permitted",
29
+ 408: "No such file or directory",
30
+ 409: "Unsupported file system",
31
+ 410: "Failed to connect internet-based file system",
32
+ 411: "Read-only file system",
33
+ 412: "Filename too long in non-encrypted file system",
34
+ 413: "Filename too long in encrypted file system",
35
+ 414: "File already exists",
36
+ 415: "Disk quota exceeded",
37
+ 416: "No space left on device",
38
+ 417: "Input/output error",
39
+ 418: "Illegal name or path",
40
+ 419: "Illegal file name",
41
+ 420: "Illegal file name on FAT file system",
42
+ 421: "Device or resource busy",
43
+ 599: "No such task for file operation",
44
+ };
45
+ const API_ALIASES = {
46
+ auth: ["SYNO.API.Auth", "SYNO.APPAuth", "SYNO.API.Authenticator"],
47
+ "filestation.list": ["SYNO.FileStation.List"],
48
+ "filestation.download": ["SYNO.FileStation.Download", "SYNO.FileStation.download"],
49
+ };
50
+ function parseBooleanFlag(rawValue, label, defaultValue) {
51
+ if (rawValue === undefined || rawValue.trim().length === 0) {
52
+ return defaultValue;
53
+ }
54
+ const normalized = rawValue.trim().toLowerCase();
55
+ if (TRUE_VALUES.has(normalized)) {
56
+ return true;
57
+ }
58
+ if (FALSE_VALUES.has(normalized)) {
59
+ return false;
60
+ }
61
+ throw new AppError(`${label} must be a boolean value, got ${rawValue}`, "config");
62
+ }
63
+ function parsePositiveInteger(rawValue, label, defaultValue) {
64
+ if (!rawValue || rawValue.trim().length === 0) {
65
+ return defaultValue;
66
+ }
67
+ const value = Number.parseInt(rawValue, 10);
68
+ if (!Number.isFinite(value) || value < 1) {
69
+ throw new AppError(`${label} must be a positive integer, got ${rawValue}`, "config");
70
+ }
71
+ return value;
72
+ }
73
+ function normalizeApiName(name) {
74
+ return name.replace(/[^a-z0-9]/gi, "").toLowerCase();
75
+ }
76
+ function parseErrorCode(rawValue) {
77
+ if (typeof rawValue === "number" && Number.isFinite(rawValue)) {
78
+ return Math.trunc(rawValue);
79
+ }
80
+ if (typeof rawValue === "string" && /^\d+$/.test(rawValue)) {
81
+ return Number.parseInt(rawValue, 10);
82
+ }
83
+ return null;
84
+ }
85
+ function describeErrorCode(code) {
86
+ if (code === null) {
87
+ return null;
88
+ }
89
+ return COMMON_ERROR_CODES[code] ?? FILE_ERROR_CODES[code] ?? null;
90
+ }
91
+ function encodeWireValue(value) {
92
+ if (typeof value === "boolean") {
93
+ return value ? "true" : "false";
94
+ }
95
+ return String(value);
96
+ }
97
+ function decodeJsonBody(body, context) {
98
+ const text = body.toString("utf8");
99
+ try {
100
+ return JSON.parse(text);
101
+ }
102
+ catch (error) {
103
+ throw new AppError(`Synology returned a non-JSON response for ${context}`, "runtime", {
104
+ cause: error instanceof Error ? error.message : String(error),
105
+ body: text.slice(0, 500),
106
+ });
107
+ }
108
+ }
109
+ function parseApiData(response, context) {
110
+ const payload = decodeJsonBody(response.body, context);
111
+ if (payload.success !== true) {
112
+ const code = parseErrorCode(payload.error?.code);
113
+ const message = describeErrorCode(code) ?? "Synology API request failed";
114
+ throw new AppError(`${context} failed: ${message}`, "runtime", {
115
+ status: response.status,
116
+ errorCode: code,
117
+ details: payload.error?.errors,
118
+ });
119
+ }
120
+ if (payload.data && typeof payload.data === "object" && !Array.isArray(payload.data)) {
121
+ return payload.data;
122
+ }
123
+ if (payload.data === undefined || payload.data === null) {
124
+ return {};
125
+ }
126
+ throw new AppError(`Synology returned an unexpected payload for ${context}`, "runtime", {
127
+ status: response.status,
128
+ payload: payload.data,
129
+ });
130
+ }
131
+ function buildEndpoint(baseUrl, resourcePath) {
132
+ const cleaned = resourcePath
133
+ .replace(/^https?:\/\/[^/]+/i, "")
134
+ .replace(/^\/+/, "")
135
+ .replace(/^webapi\/+/i, "");
136
+ const base = new URL(baseUrl);
137
+ const rootPath = base.pathname.replace(/\/+$/g, "");
138
+ base.pathname = `${rootPath || ""}/webapi/${cleaned}`.replace(/\/{2,}/g, "/");
139
+ base.search = "";
140
+ return base;
141
+ }
142
+ function makeRequest(url, options) {
143
+ const requestUrl = new URL(url.toString());
144
+ for (const [key, value] of Object.entries(options.params ?? {})) {
145
+ if (value === undefined || value === null) {
146
+ continue;
147
+ }
148
+ requestUrl.searchParams.set(key, encodeWireValue(value));
149
+ }
150
+ const transport = requestUrl.protocol === "https:" ? https : http;
151
+ return new Promise((resolve, reject) => {
152
+ const request = transport.request(requestUrl, {
153
+ method: options.method ?? "GET",
154
+ headers: options.headers,
155
+ rejectUnauthorized: requestUrl.protocol === "https:" ? options.verifySsl : undefined,
156
+ }, (response) => {
157
+ const chunks = [];
158
+ response.on("data", (chunk) => {
159
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
160
+ });
161
+ response.on("end", () => {
162
+ resolve({
163
+ status: response.statusCode ?? 0,
164
+ headers: response.headers,
165
+ body: Buffer.concat(chunks),
166
+ });
167
+ });
168
+ });
169
+ request.setTimeout(options.timeoutMs, () => {
170
+ request.destroy(new Error(`Timed out after ${options.timeoutMs}ms`));
171
+ });
172
+ request.on("error", (error) => {
173
+ reject(new AppError(`Failed to reach Synology endpoint ${requestUrl.toString()}`, "runtime", {
174
+ cause: error instanceof Error ? error.message : String(error),
175
+ }));
176
+ });
177
+ request.end();
178
+ });
179
+ }
180
+ export function normalizeSynologyBaseUrl(rawValue) {
181
+ const value = rawValue?.trim();
182
+ if (!value) {
183
+ throw new AppError("SYNOLOGY_BASE_URL is required when VAULT_SOURCE=synology", "config");
184
+ }
185
+ if (!/^https?:\/\//i.test(value)) {
186
+ throw new AppError(`SYNOLOGY_BASE_URL must start with http:// or https://: ${rawValue}`, "config");
187
+ }
188
+ return value.replace(/\/+$/g, "");
189
+ }
190
+ export function normalizeSynologyRemotePath(rawValue, label = "VAULT_SYNOLOGY_REMOTE_PATH") {
191
+ const value = rawValue?.trim();
192
+ if (!value) {
193
+ throw new AppError(`${label} is required when VAULT_SOURCE=synology`, "config");
194
+ }
195
+ if (!value.startsWith("/")) {
196
+ throw new AppError(`${label} must start with '/': ${rawValue}`, "config");
197
+ }
198
+ const normalized = `/${value.split("/").filter(Boolean).join("/")}`;
199
+ return normalized || "/";
200
+ }
201
+ export function loadSynologyConfigFromEnv(env = process.env) {
202
+ const username = (env.SYNOLOGY_USERNAME ?? env.SYNOLOGY_USER ?? "").trim();
203
+ if (!username) {
204
+ throw new AppError("SYNOLOGY_USERNAME is required when VAULT_SOURCE=synology", "config");
205
+ }
206
+ const password = env.SYNOLOGY_PASSWORD ?? env.SYNOLOGY_PASS ?? "";
207
+ if (!password) {
208
+ throw new AppError("SYNOLOGY_PASSWORD is required when VAULT_SOURCE=synology", "config");
209
+ }
210
+ return {
211
+ baseUrl: normalizeSynologyBaseUrl(env.SYNOLOGY_BASE_URL ?? env.SYNOLOGY_URL),
212
+ username,
213
+ password,
214
+ verifySsl: parseBooleanFlag(env.SYNOLOGY_VERIFY_SSL, "SYNOLOGY_VERIFY_SSL", true),
215
+ timeoutMs: parsePositiveInteger(env.SYNOLOGY_TIMEOUT, "SYNOLOGY_TIMEOUT", 30) * 1000,
216
+ session: env.SYNOLOGY_SESSION?.trim() || "FileStation",
217
+ readonly: parseBooleanFlag(env.SYNOLOGY_READONLY, "SYNOLOGY_READONLY", false),
218
+ };
219
+ }
220
+ export class SynologyClient {
221
+ config;
222
+ sid = null;
223
+ apiInfo = {};
224
+ constructor(config) {
225
+ this.config = config;
226
+ }
227
+ async request(endpoint, options) {
228
+ return makeRequest(endpoint, {
229
+ ...options,
230
+ verifySsl: this.config.verifySsl,
231
+ timeoutMs: this.config.timeoutMs,
232
+ });
233
+ }
234
+ async fetchApiInfo() {
235
+ if (Object.keys(this.apiInfo).length > 0) {
236
+ return;
237
+ }
238
+ const response = await this.request(buildEndpoint(this.config.baseUrl, "query.cgi"), {
239
+ params: {
240
+ api: "SYNO.API.Info",
241
+ version: 1,
242
+ method: "query",
243
+ query: "all",
244
+ },
245
+ });
246
+ const data = parseApiData(response, "SYNO.API.Info.query");
247
+ this.apiInfo = data;
248
+ if (Object.keys(this.apiInfo).length === 0) {
249
+ throw new AppError("Synology API discovery returned an empty API map", "runtime");
250
+ }
251
+ }
252
+ resolveApiSpec(apiKey) {
253
+ const candidates = API_ALIASES[apiKey] ?? [apiKey];
254
+ for (const candidate of candidates) {
255
+ const spec = this.apiInfo[candidate];
256
+ if (spec) {
257
+ return this.parseApiSpec(candidate, spec);
258
+ }
259
+ }
260
+ const normalizedCandidates = new Set(candidates.map(normalizeApiName));
261
+ for (const [apiName, spec] of Object.entries(this.apiInfo)) {
262
+ if (normalizedCandidates.has(normalizeApiName(apiName))) {
263
+ return this.parseApiSpec(apiName, spec);
264
+ }
265
+ }
266
+ throw new AppError(`Synology API ${apiKey} is not available on the target DSM`, "runtime", {
267
+ availableCount: Object.keys(this.apiInfo).length,
268
+ });
269
+ }
270
+ parseApiSpec(apiName, rawSpec) {
271
+ const pathValue = typeof rawSpec.path === "string" ? rawSpec.path.trim() : "";
272
+ if (!pathValue) {
273
+ throw new AppError(`Synology API ${apiName} did not expose a request path`, "runtime");
274
+ }
275
+ const versionValue = rawSpec.maxVersion ?? rawSpec.version ?? 1;
276
+ const version = Number.parseInt(String(versionValue), 10);
277
+ if (!Number.isFinite(version) || version < 1) {
278
+ throw new AppError(`Synology API ${apiName} exposed an invalid version: ${String(versionValue)}`, "runtime");
279
+ }
280
+ return {
281
+ apiName,
282
+ path: pathValue,
283
+ version,
284
+ };
285
+ }
286
+ async callJson(apiKey, method, params = {}, options = {}) {
287
+ await this.fetchApiInfo();
288
+ const includeSid = options.includeSid !== false;
289
+ if (includeSid && !this.sid) {
290
+ await this.login();
291
+ }
292
+ const spec = this.resolveApiSpec(apiKey);
293
+ const response = await this.request(buildEndpoint(this.config.baseUrl, spec.path), {
294
+ params: {
295
+ api: spec.apiName,
296
+ version: spec.version,
297
+ method,
298
+ ...params,
299
+ ...(includeSid && this.sid ? { _sid: this.sid } : {}),
300
+ },
301
+ });
302
+ return parseApiData(response, `${spec.apiName}.${method}`);
303
+ }
304
+ async login() {
305
+ if (this.sid) {
306
+ return this.sid;
307
+ }
308
+ const data = await this.callJson("auth", "login", {
309
+ account: this.config.username,
310
+ passwd: this.config.password,
311
+ session: this.config.session,
312
+ format: "sid",
313
+ }, { includeSid: false });
314
+ const sid = data.sid;
315
+ if (typeof sid !== "string" || sid.length === 0) {
316
+ throw new AppError("Synology login succeeded but did not return a sid", "runtime");
317
+ }
318
+ this.sid = sid;
319
+ return sid;
320
+ }
321
+ async logout() {
322
+ if (!this.sid) {
323
+ return;
324
+ }
325
+ try {
326
+ await this.callJson("auth", "logout", { session: this.config.session });
327
+ }
328
+ catch {
329
+ // Ignore logout failures because the session is already being discarded.
330
+ }
331
+ finally {
332
+ this.sid = null;
333
+ }
334
+ }
335
+ async listFolderPage(folderPath, offset = 0, limit = 500) {
336
+ const normalizedPath = normalizeSynologyRemotePath(folderPath, "folder_path");
337
+ const data = await this.callJson("filestation.list", "list", {
338
+ folder_path: normalizedPath,
339
+ additional: JSON.stringify(["size", "time"]),
340
+ offset,
341
+ limit,
342
+ filetype: "all",
343
+ });
344
+ const files = data.files;
345
+ if (Array.isArray(files)) {
346
+ return files;
347
+ }
348
+ const items = data.items;
349
+ if (Array.isArray(items)) {
350
+ return items;
351
+ }
352
+ return [];
353
+ }
354
+ async listFolderAll(folderPath, pageSize = 500) {
355
+ const results = [];
356
+ let offset = 0;
357
+ while (true) {
358
+ const items = await this.listFolderPage(folderPath, offset, pageSize);
359
+ if (items.length === 0) {
360
+ break;
361
+ }
362
+ results.push(...items);
363
+ if (items.length < pageSize) {
364
+ break;
365
+ }
366
+ offset += pageSize;
367
+ }
368
+ return results;
369
+ }
370
+ async probeFolder(folderPath) {
371
+ await this.listFolderPage(folderPath, 0, 1);
372
+ }
373
+ async downloadFile(remotePath, outputPath) {
374
+ const normalizedPath = normalizeSynologyRemotePath(remotePath, "path");
375
+ if (!this.sid) {
376
+ await this.login();
377
+ }
378
+ await this.fetchApiInfo();
379
+ const spec = this.resolveApiSpec("filestation.download");
380
+ const response = await this.request(buildEndpoint(this.config.baseUrl, spec.path), {
381
+ params: {
382
+ api: spec.apiName,
383
+ version: spec.version,
384
+ method: "download",
385
+ path: normalizedPath,
386
+ mode: "download",
387
+ _sid: this.sid,
388
+ },
389
+ });
390
+ const contentType = String(response.headers["content-type"] ?? "").toLowerCase();
391
+ if (contentType.includes("application/json") || response.body.subarray(0, 1).toString("utf8") === "{") {
392
+ parseApiData(response, `${spec.apiName}.download`);
393
+ throw new AppError(`Synology download returned JSON instead of file bytes for ${normalizedPath}`, "runtime");
394
+ }
395
+ if (response.status >= 400) {
396
+ throw new AppError(`Synology download failed with HTTP ${response.status}`, "runtime", {
397
+ path: normalizedPath,
398
+ });
399
+ }
400
+ await mkdir(path.dirname(outputPath), { recursive: true });
401
+ await writeFile(outputPath, response.body);
402
+ }
403
+ }
404
+ export async function withSynologyClient(env, callback) {
405
+ const client = new SynologyClient(loadSynologyConfigFromEnv(env));
406
+ try {
407
+ return await callback(client);
408
+ }
409
+ finally {
410
+ await client.logout();
411
+ }
412
+ }
@@ -0,0 +1,38 @@
1
+ import { AppError } from "../utils/errors.js";
2
+ const TRUE_VALUES = new Set(["1", "true", "yes", "on"]);
3
+ const FALSE_VALUES = new Set(["0", "false", "no", "off"]);
4
+ function parseBooleanFlag(label, rawValue, defaultValue) {
5
+ if (rawValue === undefined) {
6
+ return defaultValue;
7
+ }
8
+ const normalized = rawValue.trim().toLowerCase();
9
+ if (TRUE_VALUES.has(normalized)) {
10
+ return true;
11
+ }
12
+ if (FALSE_VALUES.has(normalized)) {
13
+ return false;
14
+ }
15
+ throw new AppError(`${label} must be a boolean value, got ${rawValue}`, "config");
16
+ }
17
+ function parseMode(rawValue) {
18
+ const normalized = (rawValue ?? "proposal").trim().toLowerCase();
19
+ if (normalized === "proposal" || normalized === "apply") {
20
+ return normalized;
21
+ }
22
+ throw new AppError(`WIKI_AGENT_TEMPLATE_EVOLUTION_MODE must be "proposal" or "apply", got ${rawValue}`, "config");
23
+ }
24
+ export function resolveTemplateEvolutionSettings(env = process.env) {
25
+ const enabled = parseBooleanFlag("WIKI_AGENT_ALLOW_TEMPLATE_EVOLUTION", env.WIKI_AGENT_ALLOW_TEMPLATE_EVOLUTION, false);
26
+ const mode = parseMode(env.WIKI_AGENT_TEMPLATE_EVOLUTION_MODE);
27
+ return {
28
+ enabled,
29
+ mode,
30
+ canApply: enabled && mode === "apply",
31
+ };
32
+ }
33
+ export function assertTemplateEvolutionAllowed(manifest, settings) {
34
+ const requestedTemplateCreation = manifest.actions.some((action) => action.kind === "create_template");
35
+ if (requestedTemplateCreation && !settings.canApply) {
36
+ throw new AppError("Template evolution action is not allowed. Set WIKI_AGENT_ALLOW_TEMPLATE_EVOLUTION=true and WIKI_AGENT_TEMPLATE_EVOLUTION_MODE=apply to permit template creation.", "config");
37
+ }
38
+ }