@open330/kiwimu 0.7.1 β 1.1.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.
- package/README.md +189 -62
- package/package.json +1 -1
- package/src/build/renderer.ts +273 -32
- package/src/build/static/dynamic-qa.js +423 -0
- package/src/build/static/edit-page.js +58 -0
- package/src/build/static/peek-panel.css +201 -0
- package/src/build/static/peek-panel.js +470 -0
- package/src/build/static/search.js +30 -15
- package/src/build/static/style.css +821 -6
- package/src/build/templates.ts +757 -49
- package/src/config.ts +41 -3
- package/src/demo/sample-data.ts +75 -8
- package/src/demo/setup.ts +26 -7
- package/src/expand/llm.ts +2 -2
- package/src/index.ts +497 -64
- package/src/ingest/docx.ts +1 -1
- package/src/ingest/markdown.ts +21 -0
- package/src/ingest/pdf.ts +4 -2
- package/src/llm-client.ts +63 -69
- package/src/pipeline/citations.ts +107 -0
- package/src/pipeline/llm-chunker.ts +281 -128
- package/src/pipeline/standardizer.ts +41 -0
- package/src/server.ts +466 -33
- package/src/services/dynamic-qa.ts +190 -0
- package/src/services/embedding.ts +122 -0
- package/src/services/index-generator.ts +185 -0
- package/src/services/ingest.ts +84 -26
- package/src/services/lint.ts +249 -0
- package/src/services/promote.ts +150 -0
- package/src/store.test.ts +11 -0
- package/src/store.ts +652 -15
- package/src/utils.ts +30 -0
package/src/index.ts
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
import { join } from "path";
|
|
5
|
-
import { CONFIG_FILE, DB_FILE, defaultConfig, findProjectRoot, getActivePersona, loadConfig, saveConfig } from "./config";
|
|
5
|
+
import { CONFIG_FILE, DB_FILE, SUPPORTED_EXTENSIONS, defaultConfig, findProjectRoot, getActivePersona, loadConfig, saveConfig } from "./config";
|
|
6
6
|
import { Store } from "./store";
|
|
7
7
|
|
|
8
8
|
const program = new Command()
|
|
9
9
|
.name("kiwimu")
|
|
10
10
|
.description("π₯ Kiwi Mu β λλ§μ νμ΅ μν€λ₯Ό λ§λμΈμ")
|
|
11
|
-
.version("0.
|
|
11
|
+
.version("0.8.0");
|
|
12
12
|
|
|
13
13
|
// --- init ---
|
|
14
14
|
program
|
|
@@ -43,14 +43,16 @@ program
|
|
|
43
43
|
console.log(`\x1b[32mβ
${count}κ° νμ΄μ§κ° λΉλλμμ΅λλ€!\x1b[0m`);
|
|
44
44
|
|
|
45
45
|
const { startServer } = await import("./server");
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
const demoPort = parseInt(process.env.KIWI_PORT || '8000', 10);
|
|
47
|
+
console.log(`π λ°λͺ¨ μν€κ° μ€λΉλμμ΅λλ€! http://localhost:${demoPort} μμ νμΈνμΈμ`);
|
|
48
|
+
startServer(root, demoPort, "localhost");
|
|
48
49
|
return;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
if (Bun.file(join(root, CONFIG_FILE)).size > 0) {
|
|
52
53
|
try {
|
|
53
|
-
|
|
54
|
+
const { accessSync } = await import("fs");
|
|
55
|
+
accessSync(join(root, CONFIG_FILE));
|
|
54
56
|
console.log("\x1b[33mμ΄λ―Έ μ΄κΈ°νλ νλ‘μ νΈμ
λλ€.\x1b[0m");
|
|
55
57
|
return;
|
|
56
58
|
} catch {}
|
|
@@ -81,13 +83,13 @@ program
|
|
|
81
83
|
p.text({
|
|
82
84
|
message: "λͺ¨λΈλͺ
",
|
|
83
85
|
placeholder:
|
|
84
|
-
results.provider === "gemini" ? "gemini-
|
|
85
|
-
results.provider === "azure-openai" ? "gpt-5-nano" :
|
|
86
|
-
results.provider === "openai" ? "gpt-
|
|
86
|
+
results.provider === "gemini" ? "gemini-3.1-flash-lite-preview" :
|
|
87
|
+
results.provider === "azure-openai" ? "gpt-5.4-nano" :
|
|
88
|
+
results.provider === "openai" ? "gpt-5.4-nano" : "claude-sonnet-4-6",
|
|
87
89
|
initialValue:
|
|
88
|
-
results.provider === "gemini" ? "gemini-
|
|
89
|
-
results.provider === "azure-openai" ? "gpt-5-nano" :
|
|
90
|
-
results.provider === "openai" ? "gpt-
|
|
90
|
+
results.provider === "gemini" ? "gemini-3.1-flash-lite-preview" :
|
|
91
|
+
results.provider === "azure-openai" ? "gpt-5.4-nano" :
|
|
92
|
+
results.provider === "openai" ? "gpt-5.4-nano" : "claude-sonnet-4-6",
|
|
91
93
|
}),
|
|
92
94
|
apiKey: () =>
|
|
93
95
|
p.password({
|
|
@@ -122,13 +124,14 @@ program
|
|
|
122
124
|
// --- add ---
|
|
123
125
|
program
|
|
124
126
|
.command("add <source>")
|
|
125
|
-
.description("URL λλ
|
|
127
|
+
.description("URL, νμΌ, λλ λλ ν 리λ₯Ό μΆκ°ν©λλ€ (PDF, DOCX, PPTX, DOC, PPT, KEY, RTF, MD)")
|
|
126
128
|
.action(async (source: string) => {
|
|
127
129
|
const root = findProjectRoot();
|
|
128
130
|
const config = loadConfig(root);
|
|
129
131
|
const persona = getActivePersona(config);
|
|
130
132
|
const store = new Store(join(root, DB_FILE));
|
|
131
133
|
try {
|
|
134
|
+
const schema = config.schema;
|
|
132
135
|
const isUrl = source.startsWith("http://") || source.startsWith("https://");
|
|
133
136
|
|
|
134
137
|
if (isUrl) {
|
|
@@ -136,23 +139,63 @@ program
|
|
|
136
139
|
validateUrl(source);
|
|
137
140
|
console.log(`\x1b[34mπ₯ URL κ°μ Έμ€λ μ€: ${source}\x1b[0m`);
|
|
138
141
|
const { ingestUrl } = await import("./services/ingest");
|
|
139
|
-
const result = await ingestUrl(root, store, source, config.llm, persona, (s) => console.log(` ${s}`));
|
|
142
|
+
const result = await ingestUrl(root, store, source, config.llm, persona, (s) => console.log(` ${s}`), schema);
|
|
140
143
|
console.log(`\x1b[32mβ
π ${result.sourceCount}κ° μλ³Έ + π ${result.conceptCount}κ° κ°λ
λ¬Έμ μμ±\x1b[0m`);
|
|
141
144
|
console.log(`\x1b[34mπ LLM: ${result.usage.totalCalls}ν νΈμΆ, ~$${result.usage.estimatedCostUsd.toFixed(4)}\x1b[0m`);
|
|
142
145
|
} else {
|
|
143
|
-
const { resolve } = await import("path");
|
|
146
|
+
const { resolve, basename } = await import("path");
|
|
144
147
|
const absPath = resolve(source);
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
148
|
+
const { statSync, readdirSync } = await import("fs");
|
|
149
|
+
|
|
150
|
+
let stat;
|
|
151
|
+
try {
|
|
152
|
+
stat = statSync(absPath);
|
|
153
|
+
} catch {
|
|
154
|
+
console.error(`\x1b[31mβ νμΌμ μ°Ύμ μ μμ΅λλ€: ${source}\x1b[0m`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (stat.isDirectory()) {
|
|
159
|
+
// Find all .md files in directory
|
|
160
|
+
const mdFiles = readdirSync(absPath)
|
|
161
|
+
.filter(f => f.endsWith('.md'))
|
|
162
|
+
.map(f => join(absPath, f));
|
|
163
|
+
|
|
164
|
+
if (mdFiles.length === 0) {
|
|
165
|
+
console.error("λλ ν 리μ .md νμΌμ΄ μμ΅λλ€");
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
console.log(`π ${mdFiles.length}κ° λ§ν¬λ€μ΄ νμΌ λ°κ²¬`);
|
|
170
|
+
const { ingestFile } = await import("./services/ingest");
|
|
171
|
+
for (const mdFile of mdFiles) {
|
|
172
|
+
console.log(`\x1b[34mπ₯ νμΌ μ²λ¦¬ μ€: ${basename(mdFile)}\x1b[0m`);
|
|
173
|
+
const result = await ingestFile(root, store, mdFile, basename(mdFile), config.llm, persona, (s) => console.log(` ${s}`), schema);
|
|
174
|
+
console.log(`\x1b[32mβ
π ${result.sourceCount}κ° μλ³Έ + π ${result.conceptCount}κ° κ°λ
\x1b[0m`);
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
const file = Bun.file(absPath);
|
|
178
|
+
if (!(await file.exists())) {
|
|
179
|
+
console.error(`\x1b[31mβ νμΌμ μ°Ύμ μ μμ΅λλ€: ${source}\x1b[0m`);
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
const ext = source.split(".").pop()?.toLowerCase() || "";
|
|
183
|
+
if (!SUPPORTED_EXTENSIONS.includes(ext)) {
|
|
184
|
+
console.error(`\x1b[31mβ μ§μνμ§ μλ νμΌ νμμ
λλ€: .${ext}\x1b[0m`);
|
|
185
|
+
console.error(` μ§μ νμ: ${SUPPORTED_EXTENSIONS.join(', ')}`);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
console.log(`\x1b[34mπ₯ νμΌ μ²λ¦¬ μ€: ${source}\x1b[0m`);
|
|
189
|
+
const { ingestFile } = await import("./services/ingest");
|
|
190
|
+
const result = await ingestFile(root, store, absPath, source, config.llm, persona, (s) => console.log(` ${s}`), schema);
|
|
191
|
+
console.log(`\x1b[32mβ
π ${result.sourceCount}κ° μλ³Έ + π ${result.conceptCount}κ° κ°λ
λ¬Έμ μμ±\x1b[0m`);
|
|
192
|
+
console.log(`\x1b[34mπ LLM: ${result.usage.totalCalls}ν νΈμΆ, ~$${result.usage.estimatedCostUsd.toFixed(4)}\x1b[0m`);
|
|
149
193
|
}
|
|
150
|
-
console.log(`\x1b[34mπ₯ νμΌ μ²λ¦¬ μ€: ${source}\x1b[0m`);
|
|
151
|
-
const { ingestFile } = await import("./services/ingest");
|
|
152
|
-
const result = await ingestFile(root, store, absPath, source, config.llm, persona, (s) => console.log(` ${s}`));
|
|
153
|
-
console.log(`\x1b[32mβ
π ${result.sourceCount}κ° μλ³Έ + π ${result.conceptCount}κ° κ°λ
λ¬Έμ μμ±\x1b[0m`);
|
|
154
|
-
console.log(`\x1b[34mπ LLM: ${result.usage.totalCalls}ν νΈμΆ, ~$${result.usage.estimatedCostUsd.toFixed(4)}\x1b[0m`);
|
|
155
194
|
}
|
|
195
|
+
} catch (e: unknown) {
|
|
196
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
197
|
+
console.error(`\x1b[31mβ ${message}\x1b[0m`);
|
|
198
|
+
process.exit(1);
|
|
156
199
|
} finally {
|
|
157
200
|
store.close();
|
|
158
201
|
}
|
|
@@ -198,13 +241,17 @@ program
|
|
|
198
241
|
store.updatePageContent(page.id, newContent);
|
|
199
242
|
} catch (e: unknown) {
|
|
200
243
|
const message = e instanceof Error ? e.message : String(e);
|
|
201
|
-
console.
|
|
244
|
+
console.error(` \x1b[31mβ μ€ν¨: ${message}\x1b[0m`);
|
|
202
245
|
}
|
|
203
246
|
}
|
|
204
247
|
|
|
205
248
|
const { autoLinkPages } = await import("./pipeline/linker");
|
|
206
249
|
const linkCount = autoLinkPages(store);
|
|
207
250
|
console.log(`\x1b[32mβ
νμ₯ μλ£! (${linkCount}κ° λ§ν¬ κ°±μ )\x1b[0m`);
|
|
251
|
+
} catch (e: unknown) {
|
|
252
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
253
|
+
console.error(`\x1b[31mβ ${message}\x1b[0m`);
|
|
254
|
+
process.exit(1);
|
|
208
255
|
} finally {
|
|
209
256
|
store.close();
|
|
210
257
|
}
|
|
@@ -224,6 +271,23 @@ program
|
|
|
224
271
|
const count = await buildSite(store, config, root);
|
|
225
272
|
console.log(`\x1b[32mβ
${count}κ° νμ΄μ§κ° λΉλλμμ΅λλ€!\x1b[0m`);
|
|
226
273
|
console.log(` μΆλ ₯: ${join(root, config.build.output_dir)}/`);
|
|
274
|
+
|
|
275
|
+
// Generate embeddings (optional β uses [embedding] config or falls back to [llm])
|
|
276
|
+
try {
|
|
277
|
+
const embConfig = config.embedding
|
|
278
|
+
? { ...config.llm, provider: config.embedding.provider, api_key: config.embedding.api_key }
|
|
279
|
+
: config.llm;
|
|
280
|
+
if (embConfig.api_key && embConfig.provider !== "demo") {
|
|
281
|
+
const { generateMissingEmbeddings } = await import("./services/embedding");
|
|
282
|
+
await generateMissingEmbeddings(store, embConfig, (msg) => console.log(msg));
|
|
283
|
+
}
|
|
284
|
+
} catch (e: unknown) {
|
|
285
|
+
console.log(` β μλ² λ© μμ± κ±΄λλ: ${e instanceof Error ? e.message : String(e)}`);
|
|
286
|
+
}
|
|
287
|
+
} catch (e: unknown) {
|
|
288
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
289
|
+
console.error(`\x1b[31mβ ${message}\x1b[0m`);
|
|
290
|
+
process.exit(1);
|
|
227
291
|
} finally {
|
|
228
292
|
store.close();
|
|
229
293
|
}
|
|
@@ -246,31 +310,42 @@ program
|
|
|
246
310
|
console.log("\x1b[34mπ¨ λΉλ μ€...\x1b[0m");
|
|
247
311
|
const count = await buildSite(store, config, root);
|
|
248
312
|
console.log(`\x1b[32m ${count}κ° νμ΄μ§ λΉλ μλ£\x1b[0m`);
|
|
313
|
+
} catch (e: unknown) {
|
|
314
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
315
|
+
console.error(`\x1b[31mβ λΉλ μ€ν¨: ${message}\x1b[0m`);
|
|
316
|
+
process.exit(1);
|
|
249
317
|
} finally {
|
|
250
318
|
store.close();
|
|
251
319
|
}
|
|
252
320
|
|
|
253
|
-
|
|
321
|
+
try {
|
|
322
|
+
console.log(`\x1b[34mπ ${opts.target}μ λ°°ν¬ μ€...\x1b[0m`);
|
|
254
323
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
324
|
+
if (opts.target === "gh-pages") {
|
|
325
|
+
const { deployGhPages } = await import("./deploy");
|
|
326
|
+
await deployGhPages(siteDir, opts.message);
|
|
327
|
+
console.log("\x1b[32mβ
GitHub Pagesμ λ°°ν¬λμμ΅λλ€!\x1b[0m");
|
|
328
|
+
try {
|
|
329
|
+
const proc = Bun.spawn(["gh", "repo", "view", "--json", "url", "-q", ".url"], { stdout: "pipe" });
|
|
330
|
+
const repoUrl = (await new Response(proc.stdout).text()).trim();
|
|
331
|
+
if (repoUrl) {
|
|
332
|
+
const owner = repoUrl.split("/").slice(-2).join("/").replace("https://github.com/", "");
|
|
333
|
+
const [user, repo] = owner.split("/");
|
|
334
|
+
console.log(` https://${user}.github.io/${repo}/`);
|
|
335
|
+
}
|
|
336
|
+
} catch {}
|
|
337
|
+
} else if (opts.target === "vercel") {
|
|
338
|
+
const { deployVercel } = await import("./deploy");
|
|
339
|
+
await deployVercel(siteDir);
|
|
340
|
+
console.log("\x1b[32mβ
Vercelμ λ°°ν¬λμμ΅λλ€!\x1b[0m");
|
|
341
|
+
} else {
|
|
342
|
+
console.error(`\x1b[31mβ μ§μνμ§ μλ λ°°ν¬ λμ: ${opts.target}\x1b[0m`);
|
|
343
|
+
process.exit(1);
|
|
344
|
+
}
|
|
345
|
+
} catch (e: unknown) {
|
|
346
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
347
|
+
console.error(`\x1b[31mβ λ°°ν¬ μ€ν¨: ${message}\x1b[0m`);
|
|
348
|
+
process.exit(1);
|
|
274
349
|
}
|
|
275
350
|
});
|
|
276
351
|
|
|
@@ -281,20 +356,29 @@ program
|
|
|
281
356
|
.option("-p, --port <port>", "ν¬νΈ λ²νΈ", "8000")
|
|
282
357
|
.option("-H, --host <host>", "λ°μΈλ μ£Όμ", "localhost")
|
|
283
358
|
.action(async (opts) => {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
359
|
+
try {
|
|
360
|
+
const root = findProjectRoot();
|
|
361
|
+
const config = loadConfig(root);
|
|
362
|
+
const siteDir = join(root, config.build.output_dir);
|
|
287
363
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
364
|
+
const { existsSync } = await import("fs");
|
|
365
|
+
if (!existsSync(siteDir)) {
|
|
366
|
+
const store = new Store(join(root, DB_FILE));
|
|
367
|
+
try {
|
|
368
|
+
const { buildSite } = await import("./build/renderer");
|
|
369
|
+
await buildSite(store, config, root);
|
|
370
|
+
} finally {
|
|
371
|
+
store.close();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
295
374
|
|
|
296
|
-
|
|
297
|
-
|
|
375
|
+
const { startServer } = await import("./server");
|
|
376
|
+
startServer(root, parseInt(opts.port), opts.host);
|
|
377
|
+
} catch (e: unknown) {
|
|
378
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
379
|
+
console.error(`\x1b[31mβ ${message}\x1b[0m`);
|
|
380
|
+
process.exit(1);
|
|
381
|
+
}
|
|
298
382
|
});
|
|
299
383
|
|
|
300
384
|
// --- quiz ---
|
|
@@ -308,7 +392,7 @@ program
|
|
|
308
392
|
try {
|
|
309
393
|
store.initSchema(); // ensure quizzes table exists
|
|
310
394
|
const count = parseInt(opts.count) || 5;
|
|
311
|
-
const quizzes = store.
|
|
395
|
+
const quizzes = store.getSmartQuizzes(count);
|
|
312
396
|
if (quizzes.length === 0) {
|
|
313
397
|
console.log("\x1b[33mν΄μ¦κ° μμ΅λλ€. λ¨Όμ λ¬Έμλ₯Ό μΆκ°νμΈμ.\x1b[0m");
|
|
314
398
|
return;
|
|
@@ -350,16 +434,25 @@ program
|
|
|
350
434
|
return;
|
|
351
435
|
}
|
|
352
436
|
|
|
353
|
-
const
|
|
354
|
-
const
|
|
355
|
-
|
|
437
|
+
const norm = (s: string) => s.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
438
|
+
const isCorrect = norm(userAnswer as string) === norm(q.answer);
|
|
439
|
+
|
|
440
|
+
store.addQuizAttempt(q.id, isCorrect);
|
|
441
|
+
|
|
442
|
+
// SM-2 spaced repetition update
|
|
443
|
+
const quality = isCorrect ? 4 : 1; // 4=correct with hesitation, 1=wrong
|
|
444
|
+
store.updateQuizSRS(q.id, quality);
|
|
356
445
|
|
|
357
446
|
if (isCorrect) {
|
|
358
447
|
score++;
|
|
359
|
-
console.log(` \x1b[32mβ
μ λ΅!\x1b[0m
|
|
448
|
+
console.log(` \x1b[32mβ
μ λ΅!\x1b[0m`);
|
|
360
449
|
} else {
|
|
361
|
-
console.log(` \x1b[31mβ μ€λ΅! μ λ΅: ${q.answer}\x1b[0m
|
|
450
|
+
console.log(` \x1b[31mβ μ€λ΅! μ λ΅: ${q.answer}\x1b[0m`);
|
|
451
|
+
}
|
|
452
|
+
if (q.explanation) {
|
|
453
|
+
console.log(` \x1b[36mπ‘ ${q.explanation}\x1b[0m`);
|
|
362
454
|
}
|
|
455
|
+
console.log();
|
|
363
456
|
}
|
|
364
457
|
|
|
365
458
|
const pct = Math.round((score / quizzes.length) * 100);
|
|
@@ -369,7 +462,176 @@ program
|
|
|
369
462
|
else if (pct >= 50) console.log(" π μ‘°κΈ λ 볡μ΅ν΄λ³΄μΈμ!");
|
|
370
463
|
else console.log(" πͺ λ€μ λμ ν΄λ³΄μΈμ!");
|
|
371
464
|
|
|
465
|
+
const stats = store.getQuizStats();
|
|
466
|
+
if (stats.total > 0) {
|
|
467
|
+
const overallPct = Math.round(stats.correct / stats.total * 100);
|
|
468
|
+
console.log(`\nπ μ 체 ν΅κ³: ${stats.correct}/${stats.total} μ λ΅ (${overallPct}%)`);
|
|
469
|
+
if (stats.unattempted > 0) {
|
|
470
|
+
console.log(` π λ―Έμλ ν΄μ¦: ${stats.unattempted}κ°`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
372
474
|
p.outro("νμ΅μ κ³μνμΈμ! π₯");
|
|
475
|
+
} catch (e: unknown) {
|
|
476
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
477
|
+
console.error(`\x1b[31mβ ${message}\x1b[0m`);
|
|
478
|
+
process.exit(1);
|
|
479
|
+
} finally {
|
|
480
|
+
store.close();
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// --- lint ---
|
|
485
|
+
program
|
|
486
|
+
.command("lint")
|
|
487
|
+
.description("μν€ κ±΄κ° μνλ₯Ό κ²μ¬ν©λλ€ (orphan pages, dead links, etc.)")
|
|
488
|
+
.action(async () => {
|
|
489
|
+
const root = findProjectRoot();
|
|
490
|
+
const store = new Store(join(root, DB_FILE));
|
|
491
|
+
try {
|
|
492
|
+
const { lintWiki } = await import("./services/lint");
|
|
493
|
+
const report = lintWiki(store);
|
|
494
|
+
|
|
495
|
+
const { summary, issues } = report;
|
|
496
|
+
|
|
497
|
+
console.log(`\n\x1b[1mπ Wiki Lint Report\x1b[0m\n`);
|
|
498
|
+
console.log(` Pages: ${summary.total_pages} Links: ${summary.total_links}\n`);
|
|
499
|
+
|
|
500
|
+
if (issues.length === 0) {
|
|
501
|
+
console.log("\x1b[32m β
No issues found!\x1b[0m\n");
|
|
502
|
+
} else {
|
|
503
|
+
const errors = issues.filter(i => i.severity === 'error');
|
|
504
|
+
const warnings = issues.filter(i => i.severity === 'warning');
|
|
505
|
+
const infos = issues.filter(i => i.severity === 'info');
|
|
506
|
+
|
|
507
|
+
if (errors.length > 0) {
|
|
508
|
+
console.log(`\x1b[31m β Errors (${errors.length})\x1b[0m`);
|
|
509
|
+
for (const issue of errors) {
|
|
510
|
+
console.log(` \x1b[31mβ’ [${issue.type}] ${issue.message}\x1b[0m`);
|
|
511
|
+
if (issue.suggestion) console.log(` \x1b[2mβ ${issue.suggestion}\x1b[0m`);
|
|
512
|
+
}
|
|
513
|
+
console.log();
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (warnings.length > 0) {
|
|
517
|
+
console.log(`\x1b[33m β Warnings (${warnings.length})\x1b[0m`);
|
|
518
|
+
for (const issue of warnings) {
|
|
519
|
+
console.log(` \x1b[33mβ’ [${issue.type}] ${issue.message}\x1b[0m`);
|
|
520
|
+
if (issue.suggestion) console.log(` \x1b[2mβ ${issue.suggestion}\x1b[0m`);
|
|
521
|
+
}
|
|
522
|
+
console.log();
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (infos.length > 0) {
|
|
526
|
+
console.log(`\x1b[36m βΉ Info (${infos.length})\x1b[0m`);
|
|
527
|
+
for (const issue of infos) {
|
|
528
|
+
console.log(` \x1b[36mβ’ [${issue.type}] ${issue.message}\x1b[0m`);
|
|
529
|
+
if (issue.suggestion) console.log(` \x1b[2mβ ${issue.suggestion}\x1b[0m`);
|
|
530
|
+
}
|
|
531
|
+
console.log();
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
console.log(`\x1b[1m Summary: \x1b[31m${summary.errors} errors\x1b[0m, \x1b[33m${summary.warnings} warnings\x1b[0m, \x1b[36m${summary.info} info\x1b[0m\n`);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (summary.errors > 0) process.exit(1);
|
|
538
|
+
} catch (e: unknown) {
|
|
539
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
540
|
+
console.error(`\x1b[31mβ ${message}\x1b[0m`);
|
|
541
|
+
process.exit(1);
|
|
542
|
+
} finally {
|
|
543
|
+
store.close();
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
// --- cite (backfill citations) ---
|
|
548
|
+
program
|
|
549
|
+
.command("cite")
|
|
550
|
+
.description("κΈ°μ‘΄ κ°λ
νμ΄μ§μ λν΄ μΈμ© μ 보λ₯Ό μμΆμ ν©λλ€ (LLM νΈμΆ νμ)")
|
|
551
|
+
.option("--dry-run", "μ€μ DBμ μ μ₯νμ§ μκ³ κ²°κ³Όλ§ νμ")
|
|
552
|
+
.action(async (opts: { dryRun?: boolean }) => {
|
|
553
|
+
const root = findProjectRoot();
|
|
554
|
+
const config = loadConfig(root);
|
|
555
|
+
const store = new Store(join(root, DB_FILE));
|
|
556
|
+
try {
|
|
557
|
+
const conceptPages = store.listConceptPages();
|
|
558
|
+
const sourcePages = store.listSourcePages();
|
|
559
|
+
|
|
560
|
+
if (conceptPages.length === 0) {
|
|
561
|
+
console.log("\x1b[33mκ°λ
νμ΄μ§κ° μμ΅λλ€.\x1b[0m");
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
if (sourcePages.length === 0) {
|
|
565
|
+
console.log("\x1b[33mμλ³Έ νμ΄μ§κ° μμ΅λλ€.\x1b[0m");
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
if (!config.llm.api_key || config.llm.provider === "demo") {
|
|
569
|
+
console.error("\x1b[31mβ LLM API ν€κ° νμν©λλ€.\x1b[0m");
|
|
570
|
+
process.exit(1);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const { LLMClient } = await import("./llm-client");
|
|
574
|
+
const llmClient = new LLMClient(config.llm);
|
|
575
|
+
|
|
576
|
+
const sourcePageList = sourcePages.map(p => `- ${p.title} [slug: ${p.slug}]`).join("\n");
|
|
577
|
+
|
|
578
|
+
console.log(`\x1b[34mπ ${conceptPages.length}κ° κ°λ
νμ΄μ§μ λν΄ μΈμ© μμΆμ μμ...\x1b[0m`);
|
|
579
|
+
console.log(` μλ³Έ νμ΄μ§: ${sourcePages.length}κ°\n`);
|
|
580
|
+
|
|
581
|
+
let totalCitations = 0;
|
|
582
|
+
|
|
583
|
+
for (let i = 0; i < conceptPages.length; i++) {
|
|
584
|
+
const page = conceptPages[i];
|
|
585
|
+
console.log(` [${i + 1}/${conceptPages.length}] ${page.title}...`);
|
|
586
|
+
|
|
587
|
+
const system = `You analyze wiki content and identify which source pages each claim comes from.
|
|
588
|
+
Return valid JSON only. No markdown fences.`;
|
|
589
|
+
|
|
590
|
+
const prompt = `Given this concept page content and a list of source pages, identify which source pages each major claim or fact comes from.
|
|
591
|
+
|
|
592
|
+
Concept page: "${page.title}"
|
|
593
|
+
Content:
|
|
594
|
+
${page.content.slice(0, 3000)}
|
|
595
|
+
|
|
596
|
+
Available source pages:
|
|
597
|
+
${sourcePageList}
|
|
598
|
+
|
|
599
|
+
Return a JSON array of citation matches:
|
|
600
|
+
[{"source_page_slug": "the-slug", "excerpt": "brief relevant quote or claim from the concept page (max 150 chars)"}]
|
|
601
|
+
|
|
602
|
+
Only include matches where you are confident the content derives from that source. Return an empty array [] if no clear matches.`;
|
|
603
|
+
|
|
604
|
+
try {
|
|
605
|
+
const raw = await llmClient.chatComplete(system, prompt, 2048);
|
|
606
|
+
let cleaned = raw.replace(/^```json?\n?/m, "").replace(/\n?```\s*$/m, "").trim();
|
|
607
|
+
const matches = JSON.parse(cleaned) as Array<{ source_page_slug: string; excerpt?: string }>;
|
|
608
|
+
|
|
609
|
+
for (const match of matches) {
|
|
610
|
+
const sourcePage = store.getPage(match.source_page_slug);
|
|
611
|
+
if (!sourcePage || !sourcePage.source_id) continue;
|
|
612
|
+
|
|
613
|
+
if (!opts.dryRun) {
|
|
614
|
+
store.addCitation(page.id, sourcePage.source_id, sourcePage.id, match.excerpt || null, null);
|
|
615
|
+
}
|
|
616
|
+
totalCitations++;
|
|
617
|
+
console.log(` β ${sourcePage.title}${match.excerpt ? ': "' + match.excerpt.slice(0, 60) + '..."' : ''}`);
|
|
618
|
+
}
|
|
619
|
+
} catch (e: unknown) {
|
|
620
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
621
|
+
console.log(` \x1b[33mβ μ€ν¨: ${message}\x1b[0m`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (opts.dryRun) {
|
|
626
|
+
console.log(`\n\x1b[33mπ DRY RUN: ${totalCitations}κ° μΈμ© λ°κ²¬ (μ μ₯νμ§ μμ)\x1b[0m`);
|
|
627
|
+
} else {
|
|
628
|
+
console.log(`\n\x1b[32mβ
${totalCitations}κ° μΈμ© μ λ³΄κ° μμ±λμμ΅λλ€.\x1b[0m`);
|
|
629
|
+
console.log(` μΈμ© νν©: kiwimu serve ν /provenance νμ΄μ§μμ νμΈ`);
|
|
630
|
+
}
|
|
631
|
+
} catch (e: unknown) {
|
|
632
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
633
|
+
console.error(`\x1b[31mβ ${message}\x1b[0m`);
|
|
634
|
+
process.exit(1);
|
|
373
635
|
} finally {
|
|
374
636
|
store.close();
|
|
375
637
|
}
|
|
@@ -387,13 +649,13 @@ program
|
|
|
387
649
|
const sources = store.listSources();
|
|
388
650
|
const sourcePages = store.listSourcePages();
|
|
389
651
|
const conceptPages = store.listConceptPages();
|
|
390
|
-
const
|
|
652
|
+
const linkCount = store.countLinks();
|
|
391
653
|
|
|
392
654
|
console.log(`\n\x1b[1mπ₯ ${config.project.name}\x1b[0m\n`);
|
|
393
655
|
console.log(` μμ€ ${sources.length}`);
|
|
394
656
|
console.log(` π μλ³Έ ${sourcePages.length}`);
|
|
395
657
|
console.log(` π κ°λ
${conceptPages.length}`);
|
|
396
|
-
console.log(` π λ§ν¬ ${
|
|
658
|
+
console.log(` π λ§ν¬ ${linkCount}`);
|
|
397
659
|
console.log(` λΉλ ${config.build.output_dir}`);
|
|
398
660
|
console.log(` λ°°ν¬ ${config.deploy.target}`);
|
|
399
661
|
|
|
@@ -410,9 +672,180 @@ program
|
|
|
410
672
|
}
|
|
411
673
|
}
|
|
412
674
|
console.log();
|
|
675
|
+
} catch (e: unknown) {
|
|
676
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
677
|
+
console.error(`\x1b[31mβ ${message}\x1b[0m`);
|
|
678
|
+
process.exit(1);
|
|
679
|
+
} finally {
|
|
680
|
+
store.close();
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
// --- log ---
|
|
685
|
+
program
|
|
686
|
+
.command("log")
|
|
687
|
+
.description("νλ λ‘κ·Έλ₯Ό νμν©λλ€")
|
|
688
|
+
.option("-n, --count <count>", "νμν νλͺ© μ", "20")
|
|
689
|
+
.option("--action <action>", "μ‘μ
μΌλ‘ νν°λ§ (ingest, page_created, quiz_attempted, query λ±)")
|
|
690
|
+
.action((opts) => {
|
|
691
|
+
const root = findProjectRoot();
|
|
692
|
+
const store = new Store(join(root, DB_FILE));
|
|
693
|
+
try {
|
|
694
|
+
const limit = parseInt(opts.count) || 20;
|
|
695
|
+
const entries = store.getActivityLog(limit, 0, opts.action || undefined);
|
|
696
|
+
if (entries.length === 0) {
|
|
697
|
+
console.log("\x1b[33mνλ λ‘κ·Έκ° μμ΅λλ€.\x1b[0m");
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
for (const e of entries) {
|
|
701
|
+
const action = e.action.toUpperCase().padEnd(15);
|
|
702
|
+
console.log(`\x1b[2m[${e.created_at}]\x1b[0m \x1b[36m[${action}]\x1b[0m ${e.title}`);
|
|
703
|
+
}
|
|
704
|
+
const stats = store.getActivityStats();
|
|
705
|
+
console.log(`\n\x1b[2mμ΄ ${stats.total}건\x1b[0m`);
|
|
413
706
|
} finally {
|
|
414
707
|
store.close();
|
|
415
708
|
}
|
|
416
709
|
});
|
|
417
710
|
|
|
711
|
+
// --- schema ---
|
|
712
|
+
program
|
|
713
|
+
.command("schema")
|
|
714
|
+
.description("μ€ν€λ§ μ€μ μ κ΄λ¦¬ν©λλ€")
|
|
715
|
+
.option("--init", "κΈ°λ³Έ [schema] μΉμ
μ kiwi.tomlμ μΆκ°ν©λλ€")
|
|
716
|
+
.option("--validate", "κΈ°μ‘΄ νμ΄μ§κ° μ€ν€λ§ κ·μΉμ λΆν©νλμ§ νμΈν©λλ€")
|
|
717
|
+
.action(async (opts: { init?: boolean; validate?: boolean }) => {
|
|
718
|
+
if (opts.init) {
|
|
719
|
+
// Generate default schema section and append to kiwi.toml
|
|
720
|
+
const root = findProjectRoot();
|
|
721
|
+
const { readFileSync, writeFileSync } = await import("fs");
|
|
722
|
+
const configPath = join(root, CONFIG_FILE);
|
|
723
|
+
const existing = readFileSync(configPath, "utf-8");
|
|
724
|
+
|
|
725
|
+
if (existing.includes("[schema]")) {
|
|
726
|
+
console.log("\x1b[33m[schema] μΉμ
μ΄ μ΄λ―Έ μ‘΄μ¬ν©λλ€.\x1b[0m");
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const defaultSchema = `
|
|
731
|
+
[schema]
|
|
732
|
+
# Wiki structure rules
|
|
733
|
+
categories = ["Fundamentals", "Advanced Topics", "Applications", "History", "People"]
|
|
734
|
+
# Naming conventions: 'noun_phrase', 'question', 'topic'
|
|
735
|
+
naming_convention = "noun_phrase"
|
|
736
|
+
# Content length rules (characters)
|
|
737
|
+
min_page_length = 200
|
|
738
|
+
max_page_length = 3000
|
|
739
|
+
|
|
740
|
+
[schema.terms]
|
|
741
|
+
# Term standardization: abbreviation = "Standard Form"
|
|
742
|
+
# "ML" = "Machine Learning"
|
|
743
|
+
# "DL" = "Deep Learning"
|
|
744
|
+
|
|
745
|
+
[schema.page_template]
|
|
746
|
+
sections = ["Definition", "Explanation", "Examples", "Related Concepts"]
|
|
747
|
+
`;
|
|
748
|
+
writeFileSync(configPath, existing.trimEnd() + "\n" + defaultSchema);
|
|
749
|
+
console.log("\x1b[32m[schema] μΉμ
μ΄ kiwi.tomlμ μΆκ°λμμ΅λλ€.\x1b[0m");
|
|
750
|
+
console.log(" νμμ λ§κ² μμ ν΄μ£ΌμΈμ.");
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
if (opts.validate) {
|
|
755
|
+
const root = findProjectRoot();
|
|
756
|
+
const config = loadConfig(root);
|
|
757
|
+
const schema = config.schema;
|
|
758
|
+
|
|
759
|
+
if (!schema) {
|
|
760
|
+
console.log("\x1b[33mμ€ν€λ§κ° μ μλμ§ μμμ΅λλ€. 'kiwimu schema --init'μΌλ‘ μμ±νμΈμ.\x1b[0m");
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const store = new Store(join(root, DB_FILE));
|
|
765
|
+
try {
|
|
766
|
+
const pages = store.listPages();
|
|
767
|
+
let issueCount = 0;
|
|
768
|
+
|
|
769
|
+
for (const page of pages) {
|
|
770
|
+
const issues: string[] = [];
|
|
771
|
+
|
|
772
|
+
// Check min length
|
|
773
|
+
if (schema.min_page_length && page.content.length < schema.min_page_length) {
|
|
774
|
+
issues.push(`κΈΈμ΄ ${page.content.length}μ < μ΅μ ${schema.min_page_length}μ`);
|
|
775
|
+
}
|
|
776
|
+
// Check max length
|
|
777
|
+
if (schema.max_page_length && page.content.length > schema.max_page_length) {
|
|
778
|
+
issues.push(`κΈΈμ΄ ${page.content.length}μ > μ΅λ ${schema.max_page_length}μ`);
|
|
779
|
+
}
|
|
780
|
+
// Check required sections
|
|
781
|
+
if (schema.page_template?.sections?.length && page.page_type === "concept") {
|
|
782
|
+
for (const section of schema.page_template.sections) {
|
|
783
|
+
const sectionPattern = new RegExp(`^##\\s+${section.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "mi");
|
|
784
|
+
if (!sectionPattern.test(page.content)) {
|
|
785
|
+
issues.push(`λλ½λ μΉμ
: "${section}"`);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
// Check category assignment
|
|
790
|
+
if (schema.categories?.length && page.page_type === "concept" && !page.category) {
|
|
791
|
+
issues.push("μΉ΄ν
κ³ λ¦¬ λ―Έμ§μ ");
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
if (issues.length > 0) {
|
|
795
|
+
issueCount += issues.length;
|
|
796
|
+
console.log(`\x1b[33m ${page.title}\x1b[0m (${page.slug})`);
|
|
797
|
+
for (const issue of issues) {
|
|
798
|
+
console.log(` - ${issue}`);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
if (issueCount === 0) {
|
|
804
|
+
console.log("\x1b[32mλͺ¨λ νμ΄μ§κ° μ€ν€λ§ κ·μΉμ λΆν©ν©λλ€.\x1b[0m");
|
|
805
|
+
} else {
|
|
806
|
+
console.log(`\n\x1b[33mμ΄ ${issueCount}κ° μ΄μ λ°κ²¬ (${pages.length}κ° νμ΄μ§ κ²μ¬)\x1b[0m`);
|
|
807
|
+
}
|
|
808
|
+
} finally {
|
|
809
|
+
store.close();
|
|
810
|
+
}
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Default: display current schema settings
|
|
815
|
+
const root = findProjectRoot();
|
|
816
|
+
const config = loadConfig(root);
|
|
817
|
+
const schema = config.schema;
|
|
818
|
+
|
|
819
|
+
if (!schema) {
|
|
820
|
+
console.log("\x1b[33mμ€ν€λ§κ° μ μλμ§ μμμ΅λλ€.\x1b[0m");
|
|
821
|
+
console.log(" 'kiwimu schema --init'μΌλ‘ κΈ°λ³Έ μ€ν€λ§λ₯Ό μμ±νμΈμ.");
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
console.log("\n\x1b[1m[schema] μ€μ :\x1b[0m\n");
|
|
826
|
+
|
|
827
|
+
if (schema.categories?.length) {
|
|
828
|
+
console.log(` μΉ΄ν
κ³ λ¦¬: ${schema.categories.join(", ")}`);
|
|
829
|
+
}
|
|
830
|
+
if (schema.naming_convention) {
|
|
831
|
+
console.log(` λͺ
λͺ
κ·μΉ: ${schema.naming_convention}`);
|
|
832
|
+
}
|
|
833
|
+
if (schema.min_page_length != null) {
|
|
834
|
+
console.log(` μ΅μ νμ΄μ§ κΈΈμ΄: ${schema.min_page_length}μ`);
|
|
835
|
+
}
|
|
836
|
+
if (schema.max_page_length != null) {
|
|
837
|
+
console.log(` μ΅λ νμ΄μ§ κΈΈμ΄: ${schema.max_page_length}μ`);
|
|
838
|
+
}
|
|
839
|
+
if (schema.terms && Object.keys(schema.terms).length > 0) {
|
|
840
|
+
console.log(` μ©μ΄ νμ€ν:`);
|
|
841
|
+
for (const [abbrev, standard] of Object.entries(schema.terms)) {
|
|
842
|
+
console.log(` ${abbrev} -> ${standard}`);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
if (schema.page_template?.sections?.length) {
|
|
846
|
+
console.log(` νμ΄μ§ ν
νλ¦Ώ μΉμ
: ${schema.page_template.sections.join(", ")}`);
|
|
847
|
+
}
|
|
848
|
+
console.log();
|
|
849
|
+
});
|
|
850
|
+
|
|
418
851
|
program.parse();
|