@open330/kiwimu 0.4.1 β 0.8.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 +98 -49
- package/bin/kiwimu +1 -1
- package/package.json +4 -1
- package/personas/namuwiki.json +6 -0
- package/src/build/renderer.ts +50 -2
- package/src/build/static/search.js +33 -2
- package/src/build/static/style.css +84 -1
- package/src/build/templates.ts +353 -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 +234 -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 +144 -59
- package/src/server.ts +327 -0
- package/src/services/ingest.ts +100 -0
- package/src/store.test.ts +132 -0
- package/src/store.ts +206 -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.8.0");
|
|
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,53 @@ 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.error(`\x1b[31mβ νμΌμ μ°Ύμ μ μμ΅λλ€: ${source}\x1b[0m`);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
const ext = source.split(".").pop()?.toLowerCase() || "";
|
|
151
|
+
const SUPPORTED_EXTENSIONS = ['pdf', 'docx', 'pptx', 'doc', 'ppt', 'key', 'rtf'];
|
|
152
|
+
if (!SUPPORTED_EXTENSIONS.includes(ext)) {
|
|
153
|
+
console.error(`\x1b[31mβ μ§μνμ§ μλ νμΌ νμμ
λλ€: .${ext}\x1b[0m`);
|
|
154
|
+
console.error(` μ§μ νμ: ${SUPPORTED_EXTENSIONS.join(', ')}`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
console.log(`\x1b[34mπ₯ νμΌ μ²λ¦¬ μ€: ${source}\x1b[0m`);
|
|
158
|
+
const { ingestFile } = await import("./services/ingest");
|
|
159
|
+
const result = await ingestFile(root, store, absPath, source, config.llm, persona, (s) => console.log(` ${s}`));
|
|
160
|
+
console.log(`\x1b[32mβ
π ${result.sourceCount}κ° μλ³Έ + π ${result.conceptCount}κ° κ°λ
λ¬Έμ μμ±\x1b[0m`);
|
|
161
|
+
console.log(`\x1b[34mπ LLM: ${result.usage.totalCalls}ν νΈμΆ, ~$${result.usage.estimatedCostUsd.toFixed(4)}\x1b[0m`);
|
|
162
|
+
}
|
|
163
|
+
} catch (e: unknown) {
|
|
164
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
165
|
+
console.error(`\x1b[31mβ ${message}\x1b[0m`);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
} finally {
|
|
109
168
|
store.close();
|
|
110
|
-
return;
|
|
111
169
|
}
|
|
112
|
-
|
|
113
|
-
store.close();
|
|
114
170
|
});
|
|
115
171
|
|
|
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
172
|
// --- expand ---
|
|
183
173
|
program
|
|
184
174
|
.command("expand")
|
|
@@ -190,43 +180,45 @@ program
|
|
|
190
180
|
const root = findProjectRoot();
|
|
191
181
|
const config = loadConfig(root);
|
|
192
182
|
const store = new Store(join(root, DB_FILE));
|
|
183
|
+
try {
|
|
184
|
+
const provider: string = opts.provider || (config as Record<string, unknown>).expand?.provider;
|
|
185
|
+
if (!provider) {
|
|
186
|
+
console.log("\x1b[33mνμ₯ νλ‘λ°μ΄λκ° μ€μ λμ§ μμμ΅λλ€.\x1b[0m");
|
|
187
|
+
console.log("μ¬μ©λ²: kiwimu expand --provider anthropic");
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
193
190
|
|
|
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");
|
|
191
|
+
const allPages = store.listPages();
|
|
192
|
+
let pages = allPages;
|
|
193
|
+
if (opts.pages) {
|
|
194
|
+
pages = allPages.filter((p) => (opts.pages as string[]).includes(p.slug));
|
|
195
|
+
}
|
|
212
196
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
197
|
+
console.log(`\x1b[34mπ§ ${pages.length}κ° λ¬Έμλ₯Ό νμ₯ν©λλ€...\x1b[0m`);
|
|
198
|
+
|
|
199
|
+
const isCli = provider === "claude-cli" || provider === "codex-cli";
|
|
200
|
+
const { expandWithApi, expandWithCli } = await import("./expand/llm");
|
|
201
|
+
|
|
202
|
+
for (let i = 0; i < pages.length; i++) {
|
|
203
|
+
const page = pages[i];
|
|
204
|
+
console.log(` [${i + 1}/${pages.length}] ${page.title}`);
|
|
205
|
+
try {
|
|
206
|
+
const newContent = isCli
|
|
207
|
+
? await expandWithCli(page, allPages, provider.replace("-cli", ""))
|
|
208
|
+
: await expandWithApi(page, allPages, provider, opts.model);
|
|
209
|
+
store.updatePageContent(page.id, newContent);
|
|
210
|
+
} catch (e: unknown) {
|
|
211
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
212
|
+
console.error(` \x1b[31mβ μ€ν¨: ${message}\x1b[0m`);
|
|
213
|
+
}
|
|
223
214
|
}
|
|
224
|
-
}
|
|
225
215
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
216
|
+
const { autoLinkPages } = await import("./pipeline/linker");
|
|
217
|
+
const linkCount = autoLinkPages(store);
|
|
218
|
+
console.log(`\x1b[32mβ
νμ₯ μλ£! (${linkCount}κ° λ§ν¬ κ°±μ )\x1b[0m`);
|
|
219
|
+
} finally {
|
|
220
|
+
store.close();
|
|
221
|
+
}
|
|
230
222
|
});
|
|
231
223
|
|
|
232
224
|
// --- build ---
|
|
@@ -237,14 +229,15 @@ program
|
|
|
237
229
|
const root = findProjectRoot();
|
|
238
230
|
const config = loadConfig(root);
|
|
239
231
|
const store = new Store(join(root, DB_FILE));
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
232
|
+
try {
|
|
233
|
+
const { buildSite } = await import("./build/renderer");
|
|
234
|
+
console.log("\x1b[34mπ¨ μν€ λΉλ μ€...\x1b[0m");
|
|
235
|
+
const count = await buildSite(store, config, root);
|
|
236
|
+
console.log(`\x1b[32mβ
${count}κ° νμ΄μ§κ° λΉλλμμ΅λλ€!\x1b[0m`);
|
|
237
|
+
console.log(` μΆλ ₯: ${join(root, config.build.output_dir)}/`);
|
|
238
|
+
} finally {
|
|
239
|
+
store.close();
|
|
240
|
+
}
|
|
248
241
|
});
|
|
249
242
|
|
|
250
243
|
// --- deploy ---
|
|
@@ -258,13 +251,15 @@ program
|
|
|
258
251
|
const config = loadConfig(root);
|
|
259
252
|
const siteDir = join(root, config.build.output_dir);
|
|
260
253
|
|
|
261
|
-
// Auto-build before deploy
|
|
262
254
|
const store = new Store(join(root, DB_FILE));
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
255
|
+
try {
|
|
256
|
+
const { buildSite } = await import("./build/renderer");
|
|
257
|
+
console.log("\x1b[34mπ¨ λΉλ μ€...\x1b[0m");
|
|
258
|
+
const count = await buildSite(store, config, root);
|
|
259
|
+
console.log(`\x1b[32m ${count}κ° νμ΄μ§ λΉλ μλ£\x1b[0m`);
|
|
260
|
+
} finally {
|
|
261
|
+
store.close();
|
|
262
|
+
}
|
|
268
263
|
|
|
269
264
|
console.log(`\x1b[34mπ ${opts.target}μ λ°°ν¬ μ€...\x1b[0m`);
|
|
270
265
|
|
|
@@ -272,7 +267,6 @@ program
|
|
|
272
267
|
const { deployGhPages } = await import("./deploy");
|
|
273
268
|
await deployGhPages(siteDir, opts.message);
|
|
274
269
|
console.log("\x1b[32mβ
GitHub Pagesμ λ°°ν¬λμμ΅λλ€!\x1b[0m");
|
|
275
|
-
// Try to get the pages URL
|
|
276
270
|
try {
|
|
277
271
|
const proc = Bun.spawn(["gh", "repo", "view", "--json", "url", "-q", ".url"], { stdout: "pipe" });
|
|
278
272
|
const repoUrl = (await new Response(proc.stdout).text()).trim();
|
|
@@ -287,7 +281,8 @@ program
|
|
|
287
281
|
await deployVercel(siteDir);
|
|
288
282
|
console.log("\x1b[32mβ
Vercelμ λ°°ν¬λμμ΅λλ€!\x1b[0m");
|
|
289
283
|
} else {
|
|
290
|
-
console.
|
|
284
|
+
console.error(`\x1b[31mβ μ§μνμ§ μλ λ°°ν¬ λμ: ${opts.target}\x1b[0m`);
|
|
285
|
+
process.exit(1);
|
|
291
286
|
}
|
|
292
287
|
});
|
|
293
288
|
|
|
@@ -303,8 +298,6 @@ program
|
|
|
303
298
|
const siteDir = join(root, config.build.output_dir);
|
|
304
299
|
|
|
305
300
|
const { existsSync } = await import("fs");
|
|
306
|
-
|
|
307
|
-
// Auto-build if needed
|
|
308
301
|
if (!existsSync(siteDir)) {
|
|
309
302
|
const store = new Store(join(root, DB_FILE));
|
|
310
303
|
const { buildSite } = await import("./build/renderer");
|
|
@@ -312,318 +305,100 @@ program
|
|
|
312
305
|
store.close();
|
|
313
306
|
}
|
|
314
307
|
|
|
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
|
-
}
|
|
308
|
+
const { startServer } = await import("./server");
|
|
309
|
+
startServer(root, parseInt(opts.port), opts.host);
|
|
310
|
+
});
|
|
416
311
|
|
|
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
|
-
}
|
|
312
|
+
// --- quiz ---
|
|
313
|
+
program
|
|
314
|
+
.command("quiz")
|
|
315
|
+
.description("νμ΅ ν΄μ¦λ₯Ό νμ΄λ΄
λλ€")
|
|
316
|
+
.option("-n, --count <count>", "λ¬Έμ μ", "5")
|
|
317
|
+
.action(async (opts) => {
|
|
318
|
+
const root = findProjectRoot();
|
|
319
|
+
const store = new Store(join(root, DB_FILE));
|
|
320
|
+
try {
|
|
321
|
+
store.initSchema(); // ensure quizzes table exists
|
|
322
|
+
const count = parseInt(opts.count) || 5;
|
|
323
|
+
const quizzes = store.getSmartQuizzes(count);
|
|
324
|
+
if (quizzes.length === 0) {
|
|
325
|
+
console.log("\x1b[33mν΄μ¦κ° μμ΅λλ€. λ¨Όμ λ¬Έμλ₯Ό μΆκ°νμΈμ.\x1b[0m");
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
469
328
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
-
}
|
|
329
|
+
const p = await import("@clack/prompts");
|
|
330
|
+
p.intro("π νμ΅ ν΄μ¦");
|
|
331
|
+
console.log(` ${quizzes.length}κ° λ¬Έμ λ₯Ό νμ΄λ΄
λλ€.\n`);
|
|
332
|
+
|
|
333
|
+
let score = 0;
|
|
334
|
+
for (let i = 0; i < quizzes.length; i++) {
|
|
335
|
+
const q = quizzes[i];
|
|
336
|
+
const typeLabel = q.quiz_type === "fill_blank" ? "λΉμΉΈ μ±μ°κΈ°" : q.quiz_type === "ox" ? "OX ν΄μ¦" : "λ¨λ΅ν";
|
|
498
337
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
return Response.json(masked);
|
|
338
|
+
console.log(`\x1b[1m[${i + 1}/${quizzes.length}] ${typeLabel}\x1b[0m`);
|
|
339
|
+
console.log(` ${q.question}`);
|
|
340
|
+
if (q.page_title) {
|
|
341
|
+
console.log(` \x1b[2mμΆμ²: ${q.page_title}\x1b[0m`);
|
|
504
342
|
}
|
|
505
343
|
|
|
506
|
-
|
|
507
|
-
if (
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
344
|
+
let userAnswer: string | symbol;
|
|
345
|
+
if (q.quiz_type === "ox") {
|
|
346
|
+
userAnswer = await p.select({
|
|
347
|
+
message: "μ λ΅μ?",
|
|
348
|
+
options: [
|
|
349
|
+
{ value: "O", label: "β O" },
|
|
350
|
+
{ value: "X", label: "β X" },
|
|
351
|
+
],
|
|
352
|
+
});
|
|
353
|
+
} else {
|
|
354
|
+
userAnswer = await p.text({
|
|
355
|
+
message: "μ λ΅μ μ
λ ₯νμΈμ",
|
|
356
|
+
placeholder: "...",
|
|
512
357
|
});
|
|
513
358
|
}
|
|
514
359
|
|
|
515
|
-
if (
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
if (!currentConfig.personas) currentConfig.personas = [];
|
|
519
|
-
|
|
520
|
-
if (body.action === "add") {
|
|
521
|
-
const { name, description, system_prompt, content_style } = body.persona;
|
|
522
|
-
if (!name) return Response.json({ error: "μ΄λ¦μ΄ νμν©λλ€" }, { status: 400 });
|
|
523
|
-
if (currentConfig.personas.find(p => p.name === name)) {
|
|
524
|
-
return Response.json({ error: "μ΄λ―Έ μ‘΄μ¬νλ νλ₯΄μλμ
λλ€" }, { status: 409 });
|
|
525
|
-
}
|
|
526
|
-
currentConfig.personas.push({ name, description: description || "", system_prompt: system_prompt || "", content_style: content_style || "" });
|
|
527
|
-
} else if (body.action === "update") {
|
|
528
|
-
const idx = currentConfig.personas.findIndex(p => p.name === body.original_name);
|
|
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 });
|
|
360
|
+
if (p.isCancel(userAnswer)) {
|
|
361
|
+
p.cancel("ν΄μ¦λ₯Ό μ’
λ£ν©λλ€.");
|
|
362
|
+
return;
|
|
549
363
|
}
|
|
550
364
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
if (isProcessing) {
|
|
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: "λΉλ μμ" });
|
|
573
|
-
}
|
|
365
|
+
const norm = (s: string) => s.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
366
|
+
const isCorrect = norm(userAnswer as string) === norm(q.answer);
|
|
574
367
|
|
|
575
|
-
|
|
576
|
-
if (url.pathname === "/admin") {
|
|
577
|
-
const store = new Store(join(root, DB_FILE));
|
|
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
|
-
}
|
|
368
|
+
store.addQuizAttempt(q.id, isCorrect);
|
|
593
369
|
|
|
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
|
-
});
|
|
370
|
+
if (isCorrect) {
|
|
371
|
+
score++;
|
|
372
|
+
console.log(` \x1b[32mβ
μ λ΅!\x1b[0m`);
|
|
373
|
+
} else {
|
|
374
|
+
console.log(` \x1b[31mβ μ€λ΅! μ λ΅: ${q.answer}\x1b[0m`);
|
|
612
375
|
}
|
|
376
|
+
if (q.explanation) {
|
|
377
|
+
console.log(` \x1b[36mπ‘ ${q.explanation}\x1b[0m`);
|
|
378
|
+
}
|
|
379
|
+
console.log();
|
|
380
|
+
}
|
|
613
381
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
382
|
+
const pct = Math.round((score / quizzes.length) * 100);
|
|
383
|
+
console.log(`\x1b[1mπ κ²°κ³Ό: ${score}/${quizzes.length} (${pct}%)\x1b[0m`);
|
|
384
|
+
if (pct >= 90) console.log(" π μλ²½μ κ°κΉμ΅λλ€!");
|
|
385
|
+
else if (pct >= 70) console.log(" π μ νμ
¨μ΅λλ€!");
|
|
386
|
+
else if (pct >= 50) console.log(" π μ‘°κΈ λ 볡μ΅ν΄λ³΄μΈμ!");
|
|
387
|
+
else console.log(" πͺ λ€μ λμ ν΄λ³΄μΈμ!");
|
|
388
|
+
|
|
389
|
+
const stats = store.getQuizStats();
|
|
390
|
+
if (stats.total > 0) {
|
|
391
|
+
const overallPct = Math.round(stats.correct / stats.total * 100);
|
|
392
|
+
console.log(`\nπ μ 체 ν΅κ³: ${stats.correct}/${stats.total} μ λ΅ (${overallPct}%)`);
|
|
393
|
+
if (stats.unattempted > 0) {
|
|
394
|
+
console.log(` π λ―Έμλ ν΄μ¦: ${stats.unattempted}κ°`);
|
|
623
395
|
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
p.outro("νμ΅μ κ³μνμΈμ! π₯");
|
|
399
|
+
} finally {
|
|
400
|
+
store.close();
|
|
401
|
+
}
|
|
627
402
|
});
|
|
628
403
|
|
|
629
404
|
// --- status ---
|
|
@@ -634,35 +409,36 @@ program
|
|
|
634
409
|
const root = findProjectRoot();
|
|
635
410
|
const config = loadConfig(root);
|
|
636
411
|
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
|
-
|
|
412
|
+
try {
|
|
413
|
+
const sources = store.listSources();
|
|
414
|
+
const sourcePages = store.listSourcePages();
|
|
415
|
+
const conceptPages = store.listConceptPages();
|
|
416
|
+
const links = store.getAllLinks();
|
|
417
|
+
|
|
418
|
+
console.log(`\n\x1b[1mπ₯ ${config.project.name}\x1b[0m\n`);
|
|
419
|
+
console.log(` μμ€ ${sources.length}`);
|
|
420
|
+
console.log(` π μλ³Έ ${sourcePages.length}`);
|
|
421
|
+
console.log(` π κ°λ
${conceptPages.length}`);
|
|
422
|
+
console.log(` π λ§ν¬ ${links.length}`);
|
|
423
|
+
console.log(` λΉλ ${config.build.output_dir}`);
|
|
424
|
+
console.log(` λ°°ν¬ ${config.deploy.target}`);
|
|
425
|
+
|
|
426
|
+
if (sourcePages.length) {
|
|
427
|
+
console.log("\n\x1b[1mπ μλ³Έ λ¬Έμ:\x1b[0m");
|
|
428
|
+
for (const p of sourcePages) {
|
|
429
|
+
console.log(` β’ ${p.title} \x1b[2m(${p.slug})\x1b[0m`);
|
|
430
|
+
}
|
|
655
431
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
432
|
+
if (conceptPages.length) {
|
|
433
|
+
console.log("\n\x1b[1mπ κ°λ
λ¬Έμ:\x1b[0m");
|
|
434
|
+
for (const p of conceptPages) {
|
|
435
|
+
console.log(` β’ ${p.title} \x1b[2m(${p.slug})\x1b[0m`);
|
|
436
|
+
}
|
|
661
437
|
}
|
|
438
|
+
console.log();
|
|
439
|
+
} finally {
|
|
440
|
+
store.close();
|
|
662
441
|
}
|
|
663
|
-
|
|
664
|
-
console.log();
|
|
665
|
-
store.close();
|
|
666
442
|
});
|
|
667
443
|
|
|
668
444
|
program.parse();
|