@open330/kiwimu 0.4.0 β 0.7.1
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/bin/kiwimu +1 -1
- package/package.json +4 -1
- package/personas/namuwiki.json +6 -0
- package/src/build/renderer.ts +49 -2
- package/src/build/static/search.js +33 -2
- package/src/build/static/style.css +181 -44
- package/src/build/templates.ts +297 -167
- package/src/config.ts +35 -29
- package/src/demo/sample-data.ts +70 -0
- package/src/demo/setup.ts +31 -0
- package/src/expand/llm.ts +1 -1
- package/src/index.ts +208 -458
- package/src/ingest/docx.ts +0 -8
- package/src/ingest/legacy.ts +4 -4
- package/src/ingest/pdf.ts +1 -1
- package/src/ingest/pptx.ts +0 -1
- package/src/ingest/web.test.ts +41 -0
- package/src/ingest/web.ts +61 -62
- package/src/llm-client.ts +203 -126
- package/src/pipeline/chunker.test.ts +42 -0
- package/src/pipeline/chunker.ts +1 -48
- package/src/pipeline/llm-chunker.ts +133 -55
- package/src/server.ts +327 -0
- package/src/services/ingest.ts +100 -0
- package/src/store.test.ts +132 -0
- package/src/store.ts +102 -2
- package/src/pipeline/llm-linker.ts +0 -84
package/src/index.ts
CHANGED
|
@@ -8,14 +8,46 @@ import { Store } from "./store";
|
|
|
8
8
|
const program = new Command()
|
|
9
9
|
.name("kiwimu")
|
|
10
10
|
.description("π₯ Kiwi Mu β λλ§μ νμ΅ μν€λ₯Ό λ§λμΈμ")
|
|
11
|
-
.version("0.
|
|
11
|
+
.version("0.7.1");
|
|
12
12
|
|
|
13
13
|
// --- init ---
|
|
14
14
|
program
|
|
15
15
|
.command("init [name]")
|
|
16
16
|
.description("μ Kiwi Mu νλ‘μ νΈλ₯Ό μμ±ν©λλ€")
|
|
17
|
-
.
|
|
17
|
+
.option("--demo", "μν λ°μ΄ν°λ‘ μ¦μ 체ν")
|
|
18
|
+
.action(async (name: string | undefined, opts: { demo?: boolean }) => {
|
|
18
19
|
const root = process.cwd();
|
|
20
|
+
|
|
21
|
+
if (opts.demo) {
|
|
22
|
+
// Demo mode: skip API key prompt, use sample data
|
|
23
|
+
const demoName = name || "Quantum Wiki Demo";
|
|
24
|
+
console.log(`\x1b[34mπ₯ λ°λͺ¨ λͺ¨λλ‘ '${demoName}' μν€λ₯Ό μμ±ν©λλ€...\x1b[0m`);
|
|
25
|
+
|
|
26
|
+
const config = defaultConfig(demoName);
|
|
27
|
+
config.llm.provider = "demo";
|
|
28
|
+
config.llm.model = "";
|
|
29
|
+
config.llm.api_key = "";
|
|
30
|
+
config.llm.endpoint = "";
|
|
31
|
+
saveConfig(root, config);
|
|
32
|
+
|
|
33
|
+
const store = new Store(join(root, DB_FILE));
|
|
34
|
+
store.initSchema();
|
|
35
|
+
|
|
36
|
+
const { setupDemo } = await import("./demo/setup");
|
|
37
|
+
await setupDemo(store);
|
|
38
|
+
|
|
39
|
+
const { buildSite } = await import("./build/renderer");
|
|
40
|
+
const count = await buildSite(store, config, root);
|
|
41
|
+
store.close();
|
|
42
|
+
|
|
43
|
+
console.log(`\x1b[32mβ
${count}κ° νμ΄μ§κ° λΉλλμμ΅λλ€!\x1b[0m`);
|
|
44
|
+
|
|
45
|
+
const { startServer } = await import("./server");
|
|
46
|
+
console.log("π λ°λͺ¨ μν€κ° μ€λΉλμμ΅λλ€! http://localhost:8000 μμ νμΈνμΈμ");
|
|
47
|
+
startServer(root, 8000, "localhost");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
19
51
|
if (Bun.file(join(root, CONFIG_FILE)).size > 0) {
|
|
20
52
|
try {
|
|
21
53
|
require("fs").accessSync(join(root, CONFIG_FILE));
|
|
@@ -25,7 +57,6 @@ program
|
|
|
25
57
|
}
|
|
26
58
|
|
|
27
59
|
const p = await import("@clack/prompts");
|
|
28
|
-
|
|
29
60
|
p.intro("π₯ Kiwi Mu β μ νμ΅ μν€ λ§λ€κΈ°");
|
|
30
61
|
|
|
31
62
|
const values = await p.group({
|
|
@@ -91,94 +122,42 @@ program
|
|
|
91
122
|
// --- add ---
|
|
92
123
|
program
|
|
93
124
|
.command("add <source>")
|
|
94
|
-
.description("URL λλ PDF
|
|
125
|
+
.description("URL λλ νμΌμ μΆκ°ν©λλ€ (PDF, DOCX, PPTX, DOC, PPT, KEY, RTF)")
|
|
95
126
|
.action(async (source: string) => {
|
|
96
127
|
const root = findProjectRoot();
|
|
128
|
+
const config = loadConfig(root);
|
|
129
|
+
const persona = getActivePersona(config);
|
|
97
130
|
const store = new Store(join(root, DB_FILE));
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
131
|
+
try {
|
|
132
|
+
const isUrl = source.startsWith("http://") || source.startsWith("https://");
|
|
133
|
+
|
|
134
|
+
if (isUrl) {
|
|
135
|
+
const { validateUrl } = await import("./ingest/web");
|
|
136
|
+
validateUrl(source);
|
|
137
|
+
console.log(`\x1b[34mπ₯ URL κ°μ Έμ€λ μ€: ${source}\x1b[0m`);
|
|
138
|
+
const { ingestUrl } = await import("./services/ingest");
|
|
139
|
+
const result = await ingestUrl(root, store, source, config.llm, persona, (s) => console.log(` ${s}`));
|
|
140
|
+
console.log(`\x1b[32mβ
π ${result.sourceCount}κ° μλ³Έ + π ${result.conceptCount}κ° κ°λ
λ¬Έμ μμ±\x1b[0m`);
|
|
141
|
+
console.log(`\x1b[34mπ LLM: ${result.usage.totalCalls}ν νΈμΆ, ~$${result.usage.estimatedCostUsd.toFixed(4)}\x1b[0m`);
|
|
142
|
+
} else {
|
|
143
|
+
const { resolve } = await import("path");
|
|
144
|
+
const absPath = resolve(source);
|
|
145
|
+
const file = Bun.file(absPath);
|
|
146
|
+
if (!(await file.exists())) {
|
|
147
|
+
console.log(`\x1b[31mνμΌμ μ°Ύμ μ μμ΅λλ€: ${source}\x1b[0m`);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
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
|
+
}
|
|
156
|
+
} finally {
|
|
109
157
|
store.close();
|
|
110
|
-
return;
|
|
111
158
|
}
|
|
112
|
-
|
|
113
|
-
store.close();
|
|
114
159
|
});
|
|
115
160
|
|
|
116
|
-
async function initLLM(root: string) {
|
|
117
|
-
const config = loadConfig(root);
|
|
118
|
-
const { setLLMConfig } = await import("./llm-client");
|
|
119
|
-
setLLMConfig(config.llm);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
async function addUrl(store: Store, url: string) {
|
|
123
|
-
const { fetchPage } = await import("./ingest/web");
|
|
124
|
-
const { llmChunkDocument, htmlToRawText } = await import("./pipeline/llm-chunker");
|
|
125
|
-
|
|
126
|
-
const root = findProjectRoot();
|
|
127
|
-
await initLLM(root);
|
|
128
|
-
const config = loadConfig(root);
|
|
129
|
-
const persona = getActivePersona(config);
|
|
130
|
-
|
|
131
|
-
console.log(`\x1b[34mπ₯ URL κ°μ Έμ€λ μ€: ${url}\x1b[0m`);
|
|
132
|
-
const { title, html } = await fetchPage(url);
|
|
133
|
-
console.log(` μ λͺ©: ${title}`);
|
|
134
|
-
|
|
135
|
-
const source = store.addSource(url, "web", title, html);
|
|
136
|
-
const rawText = htmlToRawText(html);
|
|
137
|
-
|
|
138
|
-
console.log("\x1b[34mπ LLM κΈ°λ° λ¬Έμ λΆμ μ€...\x1b[0m");
|
|
139
|
-
const { sourceCount, conceptCount } = await llmChunkDocument(rawText, title, source.id, store, 0, persona);
|
|
140
|
-
console.log(`\x1b[32mβ
π ${sourceCount}κ° μλ³Έ + π ${conceptCount}κ° κ°λ
λ¬Έμ μμ±\x1b[0m`);
|
|
141
|
-
|
|
142
|
-
const { getUsageStats, getEstimatedCost, printUsageSummary } = await import("./llm-client");
|
|
143
|
-
printUsageSummary();
|
|
144
|
-
const u = getUsageStats();
|
|
145
|
-
store.addUsageLog(source.id, u.totalCalls, u.promptTokens, u.completionTokens, u.totalTokens, getEstimatedCost());
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
async function addPdf(store: Store, pdfPath: string) {
|
|
149
|
-
const { extractTextFromPdf } = await import("./ingest/pdf");
|
|
150
|
-
const { llmChunkDocument } = await import("./pipeline/llm-chunker");
|
|
151
|
-
const { resolve } = await import("path");
|
|
152
|
-
|
|
153
|
-
const absPath = resolve(pdfPath);
|
|
154
|
-
const file = Bun.file(absPath);
|
|
155
|
-
if (!(await file.exists())) {
|
|
156
|
-
console.log(`\x1b[31mνμΌμ μ°Ύμ μ μμ΅λλ€: ${pdfPath}\x1b[0m`);
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const root = findProjectRoot();
|
|
161
|
-
await initLLM(root);
|
|
162
|
-
const config = loadConfig(root);
|
|
163
|
-
const persona = getActivePersona(config);
|
|
164
|
-
|
|
165
|
-
console.log(`\x1b[34mπ₯ PDF μ²λ¦¬ μ€: ${pdfPath}\x1b[0m`);
|
|
166
|
-
const { title, text } = await extractTextFromPdf(absPath);
|
|
167
|
-
console.log(` μ λͺ©: ${title}`);
|
|
168
|
-
console.log(` ν
μ€νΈ κΈΈμ΄: ${text.length.toLocaleString()} μ`);
|
|
169
|
-
|
|
170
|
-
const source = store.addSource(absPath, "pdf", title, "(PDF)");
|
|
171
|
-
|
|
172
|
-
console.log("\x1b[34mπ LLM κΈ°λ° λ¬Έμ λΆμ μ€...\x1b[0m");
|
|
173
|
-
const { sourceCount, conceptCount } = await llmChunkDocument(text, title, source.id, store, 0, persona);
|
|
174
|
-
console.log(`\x1b[32mβ
π ${sourceCount}κ° μλ³Έ + π ${conceptCount}κ° κ°λ
λ¬Έμ μμ±\x1b[0m`);
|
|
175
|
-
|
|
176
|
-
const { getUsageStats, getEstimatedCost, printUsageSummary } = await import("./llm-client");
|
|
177
|
-
printUsageSummary();
|
|
178
|
-
const u = getUsageStats();
|
|
179
|
-
store.addUsageLog(source.id, u.totalCalls, u.promptTokens, u.completionTokens, u.totalTokens, getEstimatedCost());
|
|
180
|
-
}
|
|
181
|
-
|
|
182
161
|
// --- expand ---
|
|
183
162
|
program
|
|
184
163
|
.command("expand")
|
|
@@ -190,43 +169,45 @@ program
|
|
|
190
169
|
const root = findProjectRoot();
|
|
191
170
|
const config = loadConfig(root);
|
|
192
171
|
const store = new Store(join(root, DB_FILE));
|
|
172
|
+
try {
|
|
173
|
+
const provider: string = opts.provider || (config as Record<string, unknown>).expand?.provider;
|
|
174
|
+
if (!provider) {
|
|
175
|
+
console.log("\x1b[33mνμ₯ νλ‘λ°μ΄λκ° μ€μ λμ§ μμμ΅λλ€.\x1b[0m");
|
|
176
|
+
console.log("μ¬μ©λ²: kiwimu expand --provider anthropic");
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
193
179
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const allPages = store.listPages();
|
|
203
|
-
let pages = allPages;
|
|
204
|
-
if (opts.pages) {
|
|
205
|
-
pages = allPages.filter((p) => (opts.pages as string[]).includes(p.slug));
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
console.log(`\x1b[34mπ§ ${pages.length}κ° λ¬Έμλ₯Ό νμ₯ν©λλ€...\x1b[0m`);
|
|
209
|
-
|
|
210
|
-
const isCli = provider === "claude-cli" || provider === "codex-cli";
|
|
211
|
-
const { expandWithApi, expandWithCli } = await import("./expand/llm");
|
|
180
|
+
const allPages = store.listPages();
|
|
181
|
+
let pages = allPages;
|
|
182
|
+
if (opts.pages) {
|
|
183
|
+
pages = allPages.filter((p) => (opts.pages as string[]).includes(p.slug));
|
|
184
|
+
}
|
|
212
185
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
186
|
+
console.log(`\x1b[34mπ§ ${pages.length}κ° λ¬Έμλ₯Ό νμ₯ν©λλ€...\x1b[0m`);
|
|
187
|
+
|
|
188
|
+
const isCli = provider === "claude-cli" || provider === "codex-cli";
|
|
189
|
+
const { expandWithApi, expandWithCli } = await import("./expand/llm");
|
|
190
|
+
|
|
191
|
+
for (let i = 0; i < pages.length; i++) {
|
|
192
|
+
const page = pages[i];
|
|
193
|
+
console.log(` [${i + 1}/${pages.length}] ${page.title}`);
|
|
194
|
+
try {
|
|
195
|
+
const newContent = isCli
|
|
196
|
+
? await expandWithCli(page, allPages, provider.replace("-cli", ""))
|
|
197
|
+
: await expandWithApi(page, allPages, provider, opts.model);
|
|
198
|
+
store.updatePageContent(page.id, newContent);
|
|
199
|
+
} catch (e: unknown) {
|
|
200
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
201
|
+
console.log(` \x1b[31mμ€ν¨: ${message}\x1b[0m`);
|
|
202
|
+
}
|
|
223
203
|
}
|
|
224
|
-
}
|
|
225
204
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
205
|
+
const { autoLinkPages } = await import("./pipeline/linker");
|
|
206
|
+
const linkCount = autoLinkPages(store);
|
|
207
|
+
console.log(`\x1b[32mβ
νμ₯ μλ£! (${linkCount}κ° λ§ν¬ κ°±μ )\x1b[0m`);
|
|
208
|
+
} finally {
|
|
209
|
+
store.close();
|
|
210
|
+
}
|
|
230
211
|
});
|
|
231
212
|
|
|
232
213
|
// --- build ---
|
|
@@ -237,14 +218,15 @@ program
|
|
|
237
218
|
const root = findProjectRoot();
|
|
238
219
|
const config = loadConfig(root);
|
|
239
220
|
const store = new Store(join(root, DB_FILE));
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
221
|
+
try {
|
|
222
|
+
const { buildSite } = await import("./build/renderer");
|
|
223
|
+
console.log("\x1b[34mπ¨ μν€ λΉλ μ€...\x1b[0m");
|
|
224
|
+
const count = await buildSite(store, config, root);
|
|
225
|
+
console.log(`\x1b[32mβ
${count}κ° νμ΄μ§κ° λΉλλμμ΅λλ€!\x1b[0m`);
|
|
226
|
+
console.log(` μΆλ ₯: ${join(root, config.build.output_dir)}/`);
|
|
227
|
+
} finally {
|
|
228
|
+
store.close();
|
|
229
|
+
}
|
|
248
230
|
});
|
|
249
231
|
|
|
250
232
|
// --- deploy ---
|
|
@@ -258,13 +240,15 @@ program
|
|
|
258
240
|
const config = loadConfig(root);
|
|
259
241
|
const siteDir = join(root, config.build.output_dir);
|
|
260
242
|
|
|
261
|
-
// Auto-build before deploy
|
|
262
243
|
const store = new Store(join(root, DB_FILE));
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
244
|
+
try {
|
|
245
|
+
const { buildSite } = await import("./build/renderer");
|
|
246
|
+
console.log("\x1b[34mπ¨ λΉλ μ€...\x1b[0m");
|
|
247
|
+
const count = await buildSite(store, config, root);
|
|
248
|
+
console.log(`\x1b[32m ${count}κ° νμ΄μ§ λΉλ μλ£\x1b[0m`);
|
|
249
|
+
} finally {
|
|
250
|
+
store.close();
|
|
251
|
+
}
|
|
268
252
|
|
|
269
253
|
console.log(`\x1b[34mπ ${opts.target}μ λ°°ν¬ μ€...\x1b[0m`);
|
|
270
254
|
|
|
@@ -272,7 +256,6 @@ program
|
|
|
272
256
|
const { deployGhPages } = await import("./deploy");
|
|
273
257
|
await deployGhPages(siteDir, opts.message);
|
|
274
258
|
console.log("\x1b[32mβ
GitHub Pagesμ λ°°ν¬λμμ΅λλ€!\x1b[0m");
|
|
275
|
-
// Try to get the pages URL
|
|
276
259
|
try {
|
|
277
260
|
const proc = Bun.spawn(["gh", "repo", "view", "--json", "url", "-q", ".url"], { stdout: "pipe" });
|
|
278
261
|
const repoUrl = (await new Response(proc.stdout).text()).trim();
|
|
@@ -303,8 +286,6 @@ program
|
|
|
303
286
|
const siteDir = join(root, config.build.output_dir);
|
|
304
287
|
|
|
305
288
|
const { existsSync } = await import("fs");
|
|
306
|
-
|
|
307
|
-
// Auto-build if needed
|
|
308
289
|
if (!existsSync(siteDir)) {
|
|
309
290
|
const store = new Store(join(root, DB_FILE));
|
|
310
291
|
const { buildSite } = await import("./build/renderer");
|
|
@@ -312,318 +293,86 @@ program
|
|
|
312
293
|
store.close();
|
|
313
294
|
}
|
|
314
295
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const port = parseInt(opts.port);
|
|
319
|
-
const hostname = opts.host;
|
|
320
|
-
console.log(`\x1b[32mπ₯ Kiwi Mu μλ² μμ!\x1b[0m`);
|
|
321
|
-
console.log(` http://${hostname === "0.0.0.0" ? "localhost" : hostname}:${port}`);
|
|
322
|
-
if (hostname === "0.0.0.0") console.log(" λ€νΈμν¬μ 곡κ°λ¨ (0.0.0.0)");
|
|
323
|
-
console.log(" μΉμμ λ¬Έμ μΆκ° κ°λ₯ν©λλ€.\n");
|
|
324
|
-
|
|
325
|
-
Bun.serve({
|
|
326
|
-
port,
|
|
327
|
-
hostname,
|
|
328
|
-
async fetch(req) {
|
|
329
|
-
const url = new URL(req.url);
|
|
330
|
-
|
|
331
|
-
// ββ API endpoints ββ
|
|
332
|
-
|
|
333
|
-
// File upload endpoint
|
|
334
|
-
if (url.pathname === "/api/upload" && req.method === "POST") {
|
|
335
|
-
if (isProcessing) {
|
|
336
|
-
return Response.json({ error: "μ΄λ―Έ μ²λ¦¬ μ€μ
λλ€", status: processingStatus }, { status: 409 });
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
const formData = await req.formData();
|
|
340
|
-
const file = formData.get("file") as File | null;
|
|
341
|
-
if (!file) {
|
|
342
|
-
return Response.json({ error: "νμΌμ΄ νμν©λλ€" }, { status: 400 });
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const ext = file.name.split(".").pop()?.toLowerCase() || "";
|
|
346
|
-
const supported = ["pdf", "docx", "doc", "pptx", "ppt", "key", "rtf"];
|
|
347
|
-
if (!supported.includes(ext)) {
|
|
348
|
-
return Response.json({ error: `μ§μνμ§ μλ νμ: .${ext}. μ§μ: ${supported.join(", ")}` }, { status: 400 });
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// Save uploaded file
|
|
352
|
-
const uploadDir = join(root, "uploads");
|
|
353
|
-
const { mkdirSync } = await import("fs");
|
|
354
|
-
mkdirSync(uploadDir, { recursive: true });
|
|
355
|
-
const filePath = join(uploadDir, file.name);
|
|
356
|
-
await Bun.write(filePath, await file.arrayBuffer());
|
|
357
|
-
|
|
358
|
-
isProcessing = true;
|
|
359
|
-
processingStatus = "νμΌ μ²λ¦¬ μμ...";
|
|
360
|
-
|
|
361
|
-
(async () => {
|
|
362
|
-
try {
|
|
363
|
-
const store = new Store(join(root, DB_FILE));
|
|
364
|
-
const { setLLMConfig, resetUsageStats, getUsageStats, getEstimatedCost } = await import("./llm-client");
|
|
365
|
-
setLLMConfig(loadConfig(root).llm);
|
|
366
|
-
const { llmChunkDocument } = await import("./pipeline/llm-chunker");
|
|
367
|
-
resetUsageStats();
|
|
368
|
-
|
|
369
|
-
let title: string;
|
|
370
|
-
let text: string;
|
|
371
|
-
|
|
372
|
-
if (ext === "pdf") {
|
|
373
|
-
const { extractTextFromPdf } = await import("./ingest/pdf");
|
|
374
|
-
processingStatus = "PDF ν
μ€νΈ μΆμΆ μ€...";
|
|
375
|
-
({ title, text } = await extractTextFromPdf(filePath));
|
|
376
|
-
} else if (ext === "docx") {
|
|
377
|
-
const { extractTextFromDocx } = await import("./ingest/docx");
|
|
378
|
-
processingStatus = "DOCX ν
μ€νΈ μΆμΆ μ€...";
|
|
379
|
-
({ title, text } = await extractTextFromDocx(filePath));
|
|
380
|
-
} else if (ext === "pptx") {
|
|
381
|
-
const { extractTextFromPptx } = await import("./ingest/pptx");
|
|
382
|
-
processingStatus = "PPTX ν
μ€νΈ μΆμΆ μ€...";
|
|
383
|
-
({ title, text } = await extractTextFromPptx(filePath));
|
|
384
|
-
} else {
|
|
385
|
-
const { extractWithTextutil } = await import("./ingest/legacy");
|
|
386
|
-
processingStatus = `${ext.toUpperCase()} ν
μ€νΈ μΆμΆ μ€...`;
|
|
387
|
-
({ title, text } = await extractWithTextutil(filePath));
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
const src = store.addSource(filePath, ext, title, "(file)");
|
|
391
|
-
// Clean up old pages from previous processing of same source
|
|
392
|
-
store.deletePagesBySource(src.id);
|
|
393
|
-
|
|
394
|
-
processingStatus = "LLM λΆμ μ€...";
|
|
395
|
-
const currentPersona = getActivePersona(loadConfig(root));
|
|
396
|
-
await llmChunkDocument(text, title, src.id, store, 0, currentPersona);
|
|
397
|
-
|
|
398
|
-
const u = getUsageStats();
|
|
399
|
-
store.addUsageLog(src.id, u.totalCalls, u.promptTokens, u.completionTokens, u.totalTokens, getEstimatedCost());
|
|
400
|
-
|
|
401
|
-
processingStatus = "λΉλ μ€...";
|
|
402
|
-
const { buildSite } = await import("./build/renderer");
|
|
403
|
-
await buildSite(store, config, root);
|
|
404
|
-
store.close();
|
|
405
|
-
|
|
406
|
-
processingStatus = "μλ£!";
|
|
407
|
-
} catch (e: any) {
|
|
408
|
-
processingStatus = `μ€λ₯: ${e.message}`;
|
|
409
|
-
} finally {
|
|
410
|
-
setTimeout(() => { isProcessing = false; }, 2000);
|
|
411
|
-
}
|
|
412
|
-
})();
|
|
413
|
-
|
|
414
|
-
return Response.json({ ok: true, message: "νμΌ μ²λ¦¬ μμ" });
|
|
415
|
-
}
|
|
296
|
+
const { startServer } = await import("./server");
|
|
297
|
+
startServer(root, parseInt(opts.port), opts.host);
|
|
298
|
+
});
|
|
416
299
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
const store = new Store(join(root, DB_FILE));
|
|
434
|
-
const { setLLMConfig, resetUsageStats, getUsageStats, getEstimatedCost } = await import("./llm-client");
|
|
435
|
-
setLLMConfig(loadConfig(root).llm);
|
|
436
|
-
resetUsageStats();
|
|
437
|
-
|
|
438
|
-
const source = body.source;
|
|
439
|
-
const { fetchPage } = await import("./ingest/web");
|
|
440
|
-
const { llmChunkDocument, htmlToRawText } = await import("./pipeline/llm-chunker");
|
|
441
|
-
|
|
442
|
-
processingStatus = "URL κ°μ Έμ€λ μ€...";
|
|
443
|
-
const { title, html } = await fetchPage(source);
|
|
444
|
-
const src = store.addSource(source, "web", title, html);
|
|
445
|
-
const rawText = htmlToRawText(html);
|
|
446
|
-
|
|
447
|
-
processingStatus = "LLM λΆμ μ€...";
|
|
448
|
-
const currentPersona = getActivePersona(loadConfig(root));
|
|
449
|
-
await llmChunkDocument(rawText, title, src.id, store, 0, currentPersona);
|
|
450
|
-
|
|
451
|
-
const u = getUsageStats();
|
|
452
|
-
store.addUsageLog(src.id, u.totalCalls, u.promptTokens, u.completionTokens, u.totalTokens, getEstimatedCost());
|
|
453
|
-
|
|
454
|
-
processingStatus = "λΉλ μ€...";
|
|
455
|
-
const { buildSite } = await import("./build/renderer");
|
|
456
|
-
await buildSite(store, config, root);
|
|
457
|
-
store.close();
|
|
458
|
-
|
|
459
|
-
processingStatus = "μλ£!";
|
|
460
|
-
} catch (e: any) {
|
|
461
|
-
processingStatus = `μ€λ₯: ${e.message}`;
|
|
462
|
-
} finally {
|
|
463
|
-
setTimeout(() => { isProcessing = false; }, 2000);
|
|
464
|
-
}
|
|
465
|
-
})();
|
|
466
|
-
|
|
467
|
-
return Response.json({ ok: true, message: "μ²λ¦¬ μμ" });
|
|
468
|
-
}
|
|
300
|
+
// --- quiz ---
|
|
301
|
+
program
|
|
302
|
+
.command("quiz")
|
|
303
|
+
.description("νμ΅ ν΄μ¦λ₯Ό νμ΄λ΄
λλ€")
|
|
304
|
+
.option("-n, --count <count>", "λ¬Έμ μ", "5")
|
|
305
|
+
.action(async (opts) => {
|
|
306
|
+
const root = findProjectRoot();
|
|
307
|
+
const store = new Store(join(root, DB_FILE));
|
|
308
|
+
try {
|
|
309
|
+
store.initSchema(); // ensure quizzes table exists
|
|
310
|
+
const count = parseInt(opts.count) || 5;
|
|
311
|
+
const quizzes = store.getRandomQuizzes(count);
|
|
312
|
+
if (quizzes.length === 0) {
|
|
313
|
+
console.log("\x1b[33mν΄μ¦κ° μμ΅λλ€. λ¨Όμ λ¬Έμλ₯Ό μΆκ°νμΈμ.\x1b[0m");
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
469
316
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const currentConfig = loadConfig(root);
|
|
474
|
-
if (body.wiki_name) currentConfig.project.name = body.wiki_name;
|
|
475
|
-
if (body.provider) currentConfig.llm.provider = body.provider;
|
|
476
|
-
if (body.model) currentConfig.llm.model = body.model;
|
|
477
|
-
if (body.api_key !== undefined) currentConfig.llm.api_key = body.api_key;
|
|
478
|
-
if (body.endpoint !== undefined) currentConfig.llm.endpoint = body.endpoint;
|
|
479
|
-
saveConfig(root, currentConfig);
|
|
480
|
-
// Reload config for serve
|
|
481
|
-
Object.assign(config, currentConfig);
|
|
482
|
-
|
|
483
|
-
// Auto-rebuild site with new settings
|
|
484
|
-
(async () => {
|
|
485
|
-
try {
|
|
486
|
-
const store = new Store(join(root, DB_FILE));
|
|
487
|
-
const { buildSite } = await import("./build/renderer");
|
|
488
|
-
await buildSite(store, currentConfig, root);
|
|
489
|
-
store.close();
|
|
490
|
-
console.log("\x1b[32mβ
μ€μ λ³κ²½ ν μ¬μ΄νΈ 리λΉλ μλ£\x1b[0m");
|
|
491
|
-
} catch (e: any) {
|
|
492
|
-
console.log(`\x1b[31m리λΉλ μ€ν¨: ${e.message}\x1b[0m`);
|
|
493
|
-
}
|
|
494
|
-
})();
|
|
495
|
-
|
|
496
|
-
return Response.json({ ok: true });
|
|
497
|
-
}
|
|
317
|
+
const p = await import("@clack/prompts");
|
|
318
|
+
p.intro("π νμ΅ ν΄μ¦");
|
|
319
|
+
console.log(` ${quizzes.length}κ° λ¬Έμ λ₯Ό νμ΄λ΄
λλ€.\n`);
|
|
498
320
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
return Response.json(masked);
|
|
504
|
-
}
|
|
321
|
+
let score = 0;
|
|
322
|
+
for (let i = 0; i < quizzes.length; i++) {
|
|
323
|
+
const q = quizzes[i];
|
|
324
|
+
const typeLabel = q.quiz_type === "fill_blank" ? "λΉμΉΈ μ±μ°κΈ°" : q.quiz_type === "ox" ? "OX ν΄μ¦" : "λ¨λ΅ν";
|
|
505
325
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
personas: currentConfig.personas || [],
|
|
511
|
-
active: currentConfig.active_persona || "",
|
|
512
|
-
});
|
|
326
|
+
console.log(`\x1b[1m[${i + 1}/${quizzes.length}] ${typeLabel}\x1b[0m`);
|
|
327
|
+
console.log(` ${q.question}`);
|
|
328
|
+
if (q.page_title) {
|
|
329
|
+
console.log(` \x1b[2mμΆμ²: ${q.page_title}\x1b[0m`);
|
|
513
330
|
}
|
|
514
331
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
if (idx === -1) return Response.json({ error: "νλ₯΄μλλ₯Ό μ°Ύμ μ μμ΅λλ€" }, { status: 404 });
|
|
530
|
-
currentConfig.personas[idx] = body.persona;
|
|
531
|
-
if (currentConfig.active_persona === body.original_name && body.persona.name !== body.original_name) {
|
|
532
|
-
currentConfig.active_persona = body.persona.name;
|
|
533
|
-
}
|
|
534
|
-
} else if (body.action === "delete") {
|
|
535
|
-
currentConfig.personas = currentConfig.personas.filter(p => p.name !== body.name);
|
|
536
|
-
if (currentConfig.active_persona === body.name) {
|
|
537
|
-
currentConfig.active_persona = currentConfig.personas[0]?.name || "";
|
|
538
|
-
}
|
|
539
|
-
} else if (body.action === "activate") {
|
|
540
|
-
if (!currentConfig.personas.find(p => p.name === body.name)) {
|
|
541
|
-
return Response.json({ error: "νλ₯΄μλλ₯Ό μ°Ύμ μ μμ΅λλ€" }, { status: 404 });
|
|
542
|
-
}
|
|
543
|
-
currentConfig.active_persona = body.name;
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
saveConfig(root, currentConfig);
|
|
547
|
-
Object.assign(config, currentConfig);
|
|
548
|
-
return Response.json({ ok: true, personas: currentConfig.personas, active: currentConfig.active_persona });
|
|
332
|
+
let userAnswer: string | symbol;
|
|
333
|
+
if (q.quiz_type === "ox") {
|
|
334
|
+
userAnswer = await p.select({
|
|
335
|
+
message: "μ λ΅μ?",
|
|
336
|
+
options: [
|
|
337
|
+
{ value: "O", label: "β O" },
|
|
338
|
+
{ value: "X", label: "β X" },
|
|
339
|
+
],
|
|
340
|
+
});
|
|
341
|
+
} else {
|
|
342
|
+
userAnswer = await p.text({
|
|
343
|
+
message: "μ λ΅μ μ
λ ₯νμΈμ",
|
|
344
|
+
placeholder: "...",
|
|
345
|
+
});
|
|
549
346
|
}
|
|
550
347
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
return Response.json({ error: "μ΄λ―Έ μ²λ¦¬ μ€μ
λλ€" }, { status: 409 });
|
|
555
|
-
}
|
|
556
|
-
isProcessing = true;
|
|
557
|
-
processingStatus = "λΉλ μ€...";
|
|
558
|
-
(async () => {
|
|
559
|
-
try {
|
|
560
|
-
const store = new Store(join(root, DB_FILE));
|
|
561
|
-
const { buildSite } = await import("./build/renderer");
|
|
562
|
-
await buildSite(store, loadConfig(root), root);
|
|
563
|
-
store.close();
|
|
564
|
-
processingStatus = "λΉλ μλ£!";
|
|
565
|
-
console.log("\x1b[32mβ
μλ λΉλ μλ£\x1b[0m");
|
|
566
|
-
} catch (e: any) {
|
|
567
|
-
processingStatus = `λΉλ μ€λ₯: ${e.message}`;
|
|
568
|
-
} finally {
|
|
569
|
-
setTimeout(() => { isProcessing = false; }, 2000);
|
|
570
|
-
}
|
|
571
|
-
})();
|
|
572
|
-
return Response.json({ ok: true, message: "λΉλ μμ" });
|
|
348
|
+
if (p.isCancel(userAnswer)) {
|
|
349
|
+
p.cancel("ν΄μ¦λ₯Ό μ’
λ£ν©λλ€.");
|
|
350
|
+
return;
|
|
573
351
|
}
|
|
574
352
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
const sources = store.listSources();
|
|
579
|
-
const usage = store.getUsageSummary();
|
|
580
|
-
const configData = loadConfig(root);
|
|
581
|
-
store.close();
|
|
582
|
-
|
|
583
|
-
const { renderAdmin } = await import("./build/templates");
|
|
584
|
-
return new Response(renderAdmin({
|
|
585
|
-
wikiName: configData.project.name,
|
|
586
|
-
sources,
|
|
587
|
-
usage,
|
|
588
|
-
llmConfig: configData.llm,
|
|
589
|
-
personas: configData.personas || [],
|
|
590
|
-
activePersona: configData.active_persona || "",
|
|
591
|
-
}), { headers: { "Content-Type": "text/html" } });
|
|
592
|
-
}
|
|
353
|
+
const correct = q.answer.trim().toLowerCase();
|
|
354
|
+
const user = (userAnswer as string).trim().toLowerCase();
|
|
355
|
+
const isCorrect = user === correct || (correct.includes(user) && user.length > 0);
|
|
593
356
|
|
|
594
|
-
if (
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
const links = store.getAllLinks();
|
|
600
|
-
const usage = store.getUsageSummary();
|
|
601
|
-
store.close();
|
|
602
|
-
|
|
603
|
-
return Response.json({
|
|
604
|
-
processing: isProcessing,
|
|
605
|
-
processingStatus,
|
|
606
|
-
sources: sources.length,
|
|
607
|
-
sourcePages: sourcePages.length,
|
|
608
|
-
conceptPages: conceptPages.length,
|
|
609
|
-
links: links.length,
|
|
610
|
-
usage,
|
|
611
|
-
});
|
|
357
|
+
if (isCorrect) {
|
|
358
|
+
score++;
|
|
359
|
+
console.log(` \x1b[32mβ
μ λ΅!\x1b[0m\n`);
|
|
360
|
+
} else {
|
|
361
|
+
console.log(` \x1b[31mβ μ€λ΅! μ λ΅: ${q.answer}\x1b[0m\n`);
|
|
612
362
|
}
|
|
363
|
+
}
|
|
613
364
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
365
|
+
const pct = Math.round((score / quizzes.length) * 100);
|
|
366
|
+
console.log(`\x1b[1mπ κ²°κ³Ό: ${score}/${quizzes.length} (${pct}%)\x1b[0m`);
|
|
367
|
+
if (pct >= 90) console.log(" π μλ²½μ κ°κΉμ΅λλ€!");
|
|
368
|
+
else if (pct >= 70) console.log(" π μ νμ
¨μ΅λλ€!");
|
|
369
|
+
else if (pct >= 50) console.log(" π μ‘°κΈ λ 볡μ΅ν΄λ³΄μΈμ!");
|
|
370
|
+
else console.log(" πͺ λ€μ λμ ν΄λ³΄μΈμ!");
|
|
620
371
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
},
|
|
626
|
-
});
|
|
372
|
+
p.outro("νμ΅μ κ³μνμΈμ! π₯");
|
|
373
|
+
} finally {
|
|
374
|
+
store.close();
|
|
375
|
+
}
|
|
627
376
|
});
|
|
628
377
|
|
|
629
378
|
// --- status ---
|
|
@@ -634,35 +383,36 @@ program
|
|
|
634
383
|
const root = findProjectRoot();
|
|
635
384
|
const config = loadConfig(root);
|
|
636
385
|
const store = new Store(join(root, DB_FILE));
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
386
|
+
try {
|
|
387
|
+
const sources = store.listSources();
|
|
388
|
+
const sourcePages = store.listSourcePages();
|
|
389
|
+
const conceptPages = store.listConceptPages();
|
|
390
|
+
const links = store.getAllLinks();
|
|
391
|
+
|
|
392
|
+
console.log(`\n\x1b[1mπ₯ ${config.project.name}\x1b[0m\n`);
|
|
393
|
+
console.log(` μμ€ ${sources.length}`);
|
|
394
|
+
console.log(` π μλ³Έ ${sourcePages.length}`);
|
|
395
|
+
console.log(` π κ°λ
${conceptPages.length}`);
|
|
396
|
+
console.log(` π λ§ν¬ ${links.length}`);
|
|
397
|
+
console.log(` λΉλ ${config.build.output_dir}`);
|
|
398
|
+
console.log(` λ°°ν¬ ${config.deploy.target}`);
|
|
399
|
+
|
|
400
|
+
if (sourcePages.length) {
|
|
401
|
+
console.log("\n\x1b[1mπ μλ³Έ λ¬Έμ:\x1b[0m");
|
|
402
|
+
for (const p of sourcePages) {
|
|
403
|
+
console.log(` β’ ${p.title} \x1b[2m(${p.slug})\x1b[0m`);
|
|
404
|
+
}
|
|
655
405
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
406
|
+
if (conceptPages.length) {
|
|
407
|
+
console.log("\n\x1b[1mπ κ°λ
λ¬Έμ:\x1b[0m");
|
|
408
|
+
for (const p of conceptPages) {
|
|
409
|
+
console.log(` β’ ${p.title} \x1b[2m(${p.slug})\x1b[0m`);
|
|
410
|
+
}
|
|
661
411
|
}
|
|
412
|
+
console.log();
|
|
413
|
+
} finally {
|
|
414
|
+
store.close();
|
|
662
415
|
}
|
|
663
|
-
|
|
664
|
-
console.log();
|
|
665
|
-
store.close();
|
|
666
416
|
});
|
|
667
417
|
|
|
668
418
|
program.parse();
|