@hawon/nexus 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +60 -38
  2. package/dist/cli/index.js +76 -145
  3. package/dist/index.js +15 -26
  4. package/dist/mcp/server.js +61 -32
  5. package/package.json +2 -1
  6. package/scripts/auto-skill.sh +54 -0
  7. package/scripts/auto-sync.sh +11 -0
  8. package/scripts/benchmark.ts +444 -0
  9. package/scripts/scan-tool-result.sh +46 -0
  10. package/src/cli/index.ts +79 -172
  11. package/src/index.ts +17 -29
  12. package/src/mcp/server.ts +67 -41
  13. package/src/memory-engine/index.ts +4 -6
  14. package/src/memory-engine/nexus-memory.test.ts +437 -0
  15. package/src/memory-engine/nexus-memory.ts +631 -0
  16. package/src/memory-engine/semantic.ts +380 -0
  17. package/src/parser/parse.ts +1 -21
  18. package/src/promptguard/advanced-rules.ts +129 -12
  19. package/src/promptguard/entropy.ts +21 -2
  20. package/src/promptguard/evolution/auto-update.ts +16 -6
  21. package/src/promptguard/multilingual-rules.ts +68 -0
  22. package/src/promptguard/rules.ts +87 -2
  23. package/src/promptguard/scanner.test.ts +262 -0
  24. package/src/promptguard/scanner.ts +1 -1
  25. package/src/promptguard/semantic.ts +19 -4
  26. package/src/promptguard/token-analysis.ts +17 -5
  27. package/src/review/analyzer.test.ts +279 -0
  28. package/src/review/analyzer.ts +112 -28
  29. package/src/shared/stop-words.ts +21 -0
  30. package/src/skills/index.ts +11 -27
  31. package/src/skills/memory-skill-engine.ts +1044 -0
  32. package/src/testing/health-check.ts +19 -2
  33. package/src/cost/index.ts +0 -3
  34. package/src/cost/tracker.ts +0 -290
  35. package/src/cost/types.ts +0 -34
  36. package/src/memory-engine/compressor.ts +0 -97
  37. package/src/memory-engine/context-window.ts +0 -113
  38. package/src/memory-engine/store.ts +0 -371
  39. package/src/memory-engine/types.ts +0 -32
  40. package/src/skills/context-engine.ts +0 -863
  41. package/src/skills/extractor.ts +0 -224
  42. package/src/skills/global-context.ts +0 -726
  43. package/src/skills/library.ts +0 -189
  44. package/src/skills/pattern-engine.ts +0 -712
  45. package/src/skills/render-evolved.ts +0 -160
  46. package/src/skills/skill-reconciler.ts +0 -703
  47. package/src/skills/smart-extractor.ts +0 -843
  48. package/src/skills/types.ts +0 -18
  49. package/src/skills/wisdom-extractor.ts +0 -737
  50. package/src/superdev-evolution/index.ts +0 -3
  51. package/src/superdev-evolution/skill-manager.ts +0 -266
  52. package/src/superdev-evolution/types.ts +0 -20
@@ -1,737 +0,0 @@
1
- /**
2
- * Wisdom Extractor — Extracts principles, not procedures
3
- *
4
- * Instead of: "Run `Edit` on `src/scanner.ts`"
5
- * Produces: "When hardening a regex-based scanner, add dynamic property
6
- * access detection because attackers use bracket notation to
7
- * bypass literal pattern matching."
8
- *
9
- * Three types of wisdom:
10
- *
11
- * 1. PATTERN → PRINCIPLE
12
- * "이럴 때는 이렇게 해라"
13
- * When [situation], do [approach] because [reason].
14
- *
15
- * 2. ANTI-PATTERN → WARNING
16
- * "이렇게 하면 안 된다"
17
- * Don't [approach] when [situation] because [consequence].
18
- *
19
- * 3. DECISION → RATIONALE
20
- * "왜 이 방법을 선택했는가"
21
- * Chose [A] over [B] because [tradeoff].
22
- */
23
-
24
- import type { ParsedMessage, ToolCall } from "../parser/types.js";
25
- import { createHash } from "node:crypto";
26
-
27
- // ─── Types ───────────────────────────────────────────────────────
28
-
29
- export type Wisdom = {
30
- id: string;
31
- type: "principle" | "warning" | "rationale";
32
- /** One-line title. */
33
- title: string;
34
- /** The wisdom itself — situation → approach → reason. */
35
- body: WisdomBody;
36
- /** Abstraction level: how general is this wisdom? */
37
- abstraction: "specific" | "moderate" | "general";
38
- /** Domain tags. */
39
- domains: string[];
40
- /** Confidence. */
41
- confidence: number;
42
- /** Source session. */
43
- sourceSessionId: string;
44
- /** Source message range. */
45
- sourceRange: [number, number];
46
- /** When extracted. */
47
- extractedAt: string;
48
- };
49
-
50
- export type WisdomBody = {
51
- /** When does this apply? */
52
- situation: string;
53
- /** What should you do (principle) or not do (warning)? */
54
- approach: string;
55
- /** Why? The underlying reason. */
56
- reason: string;
57
- /** What's the alternative that doesn't work? (for warnings/rationales) */
58
- alternative?: string;
59
- /** Concrete example from the session (brief). */
60
- example?: string;
61
- };
62
-
63
- // ─── Detection Patterns ──────────────────────────────────────────
64
-
65
- /**
66
- * Detects "wisdom-worthy" moments in conversations.
67
- *
68
- * A moment is wisdom-worthy when:
69
- * 1. User asked → Claude tried approach A → failed → tried approach B → succeeded
70
- * → PRINCIPLE: "When X, do B instead of A because..."
71
- *
72
- * 2. User corrected Claude's approach
73
- * → WARNING: "Don't do A when X because..."
74
- *
75
- * 3. Claude explicitly chose between alternatives
76
- * → RATIONALE: "Chose A over B because..."
77
- *
78
- * 4. User learned something surprising
79
- * → PRINCIPLE: "When X, note that Y (counterintuitive)"
80
- *
81
- * 5. A complex multi-step task succeeded
82
- * → PRINCIPLE: "When doing X, the key steps are..."
83
- */
84
-
85
- type ConversationWindow = {
86
- messages: ParsedMessage[];
87
- startIndex: number;
88
- endIndex: number;
89
- };
90
-
91
- // ─── Main Extraction ─────────────────────────────────────────────
92
-
93
- export function extractWisdom(
94
- messages: ParsedMessage[],
95
- sessionId: string,
96
- ): Wisdom[] {
97
- const wisdoms: Wisdom[] = [];
98
-
99
- // Pass 1: Find failure-recovery patterns (approach A failed → B worked)
100
- wisdoms.push(...findFailureRecoveries(messages, sessionId));
101
-
102
- // Pass 2: Find user corrections (user redirected Claude's approach)
103
- wisdoms.push(...findUserCorrections(messages, sessionId));
104
-
105
- // Pass 3: Find explicit decisions (Claude chose between alternatives)
106
- wisdoms.push(...findExplicitDecisions(messages, sessionId));
107
-
108
- // Pass 4: Find learning moments (user expressed surprise or understanding)
109
- wisdoms.push(...findLearningMoments(messages, sessionId));
110
-
111
- // Pass 5: Find successful complex tasks (multi-tool sequences that worked)
112
- wisdoms.push(...findSuccessfulPatterns(messages, sessionId));
113
-
114
- // Deduplicate by similarity
115
- return deduplicateWisdom(wisdoms);
116
- }
117
-
118
- // ─── Pass 1: Failure → Recovery ──────────────────────────────────
119
-
120
- function findFailureRecoveries(messages: ParsedMessage[], sessionId: string): Wisdom[] {
121
- const wisdoms: Wisdom[] = [];
122
-
123
- for (let i = 0; i < messages.length - 2; i++) {
124
- // Look for: assistant with error → user/assistant retry → success
125
- const msg = messages[i];
126
- if (msg.role !== "assistant") continue;
127
- if (isNoiseMessage(msg)) continue;
128
-
129
- // Check for failure in tool results
130
- const hasFailure = msg.toolCalls?.some((tc) =>
131
- tc.result && /error|fail|denied|not found|exit code [1-9]/i.test(tc.result),
132
- );
133
- if (!hasFailure) continue;
134
-
135
- const failedTools = msg.toolCalls?.filter((tc) =>
136
- tc.result && /error|fail/i.test(tc.result),
137
- ) ?? [];
138
-
139
- // Look ahead for recovery (successful tool use)
140
- let recoveryMsg: ParsedMessage | null = null;
141
- let recoveryIndex = -1;
142
- for (let j = i + 1; j < Math.min(i + 6, messages.length); j++) {
143
- const candidate = messages[j];
144
- if (candidate.role === "assistant" && isNoiseMessage(candidate)) continue;
145
- if (candidate.role === "assistant" && candidate.toolCalls?.length) {
146
- const allSuccess = candidate.toolCalls.every((tc) =>
147
- !tc.result || !/error|fail/i.test(tc.result),
148
- );
149
- if (allSuccess) {
150
- recoveryMsg = candidate;
151
- recoveryIndex = j;
152
- break;
153
- }
154
- }
155
- }
156
-
157
- if (!recoveryMsg || recoveryIndex < 0) continue;
158
-
159
- // Extract the situation, what failed, what worked
160
- const failedApproach = describeToolAction(failedTools[0]);
161
- const successApproach = describeToolAction(recoveryMsg.toolCalls?.[0]);
162
-
163
- // Get user context (what were they trying to do?)
164
- const userContext = findNearestUserMessage(messages, i);
165
- const situation = abstractSituation(userContext?.content ?? "");
166
-
167
- wisdoms.push({
168
- id: hashWisdom(`recovery:${i}:${sessionId}`),
169
- type: "principle",
170
- title: `${situation.slice(0, 40)} — ${successApproach.method}가 더 효과적`,
171
- body: {
172
- situation: situation || "이 상황에서",
173
- approach: successApproach.description,
174
- reason: `처음 시도한 ${failedApproach.method}가 실패했기 때문 (${failedApproach.error})`,
175
- alternative: failedApproach.description,
176
- example: `${failedApproach.method} → 실패 → ${successApproach.method} → 성공`,
177
- },
178
- abstraction: "moderate",
179
- domains: extractDomains(messages.slice(i, recoveryIndex + 1)),
180
- confidence: 0.7,
181
- sourceSessionId: sessionId,
182
- sourceRange: [i, recoveryIndex],
183
- extractedAt: new Date().toISOString(),
184
- });
185
-
186
- i = recoveryIndex; // Skip to after recovery
187
- }
188
-
189
- return wisdoms;
190
- }
191
-
192
- // ─── Pass 2: User Corrections ────────────────────────────────────
193
-
194
- function findUserCorrections(messages: ParsedMessage[], sessionId: string): Wisdom[] {
195
- const wisdoms: Wisdom[] = [];
196
-
197
- const redirectPatterns = [
198
- /아니[야요]?\s/i, /그게\s*아니라/i, /말고/i, /대신/i, /다시/i,
199
- /아닌데/i, /그거\s*말고/i, /다르게/i, /바꿔/i,
200
- /no[,.]?\s*(?:not|don't|instead)/i, /wait/i, /actually/i,
201
- /wrong/i, /scratch that/i, /instead/i,
202
- ];
203
-
204
- for (let i = 1; i < messages.length; i++) {
205
- const msg = messages[i];
206
- if (msg.role !== "user") continue;
207
- if (isNoiseMessage(msg)) continue;
208
-
209
- const isRedirect = redirectPatterns.some((p) => p.test(msg.content));
210
- if (!isRedirect) continue;
211
-
212
- // What was Claude doing before?
213
- const prevAssistant = findPrevAssistant(messages, i);
214
- if (!prevAssistant) continue;
215
-
216
- // What did the user want instead?
217
- const userWant = msg.content;
218
-
219
- // What happened after the correction?
220
- const nextAssistant = findNextAssistant(messages, i);
221
-
222
- const wrongApproach = abstractApproach(prevAssistant);
223
- const rightApproach = abstractApproach(nextAssistant);
224
- const situation = abstractSituation(
225
- findNearestUserMessage(messages, Math.max(0, i - 3))?.content ?? "",
226
- );
227
-
228
- wisdoms.push({
229
- id: hashWisdom(`correction:${i}:${sessionId}`),
230
- type: "warning",
231
- title: `${situation.slice(0, 40)}에서 ${wrongApproach.slice(0, 30)}하지 말 것`,
232
- body: {
233
- situation,
234
- approach: rightApproach || userWant.slice(0, 100),
235
- reason: `사용자가 "${userWant.slice(0, 60)}"로 수정함`,
236
- alternative: wrongApproach,
237
- },
238
- abstraction: "moderate",
239
- domains: extractDomains(messages.slice(Math.max(0, i - 2), Math.min(i + 3, messages.length))),
240
- confidence: 0.8,
241
- sourceSessionId: sessionId,
242
- sourceRange: [Math.max(0, i - 1), Math.min(i + 2, messages.length - 1)],
243
- extractedAt: new Date().toISOString(),
244
- });
245
- }
246
-
247
- return wisdoms;
248
- }
249
-
250
- // ─── Pass 3: Explicit Decisions ──────────────────────────────────
251
-
252
- function findExplicitDecisions(messages: ParsedMessage[], sessionId: string): Wisdom[] {
253
- const wisdoms: Wisdom[] = [];
254
-
255
- const decisionPatterns = [
256
- /(?:chose|선택|picked|decided|결정)\s+(\S+)\s+(?:over|대신|instead of|말고)\s+(\S+)/i,
257
- /(?:better|나은|preferred|적합)\s+(?:to|approach|방법|than)\s/i,
258
- /(?:A|B|option|방법|방안)\s*(?:vs|versus|대|or)\s/i,
259
- /(?:tradeoff|trade-off|트레이드오프|장단점)/i,
260
- ];
261
-
262
- for (let i = 0; i < messages.length; i++) {
263
- const msg = messages[i];
264
- if (msg.role !== "assistant") continue;
265
-
266
- const text = msg.content;
267
- const hasDecision = decisionPatterns.some((p) => p.test(text));
268
- if (!hasDecision) continue;
269
-
270
- // Extract the decision context
271
- const userQ = findNearestUserMessage(messages, i);
272
- const situation = abstractSituation(userQ?.content ?? "");
273
-
274
- // Try to extract the alternatives from the text
275
- const alternatives = extractAlternatives(text);
276
-
277
- if (alternatives) {
278
- wisdoms.push({
279
- id: hashWisdom(`decision:${i}:${sessionId}`),
280
- type: "rationale",
281
- title: `${alternatives.chosen}을 선택한 이유`,
282
- body: {
283
- situation,
284
- approach: alternatives.chosen,
285
- reason: alternatives.reason || "구체적 이유는 문맥 참조",
286
- alternative: alternatives.rejected,
287
- },
288
- abstraction: "moderate",
289
- domains: extractDomains([msg]),
290
- confidence: 0.6,
291
- sourceSessionId: sessionId,
292
- sourceRange: [i, i],
293
- extractedAt: new Date().toISOString(),
294
- });
295
- }
296
- }
297
-
298
- return wisdoms;
299
- }
300
-
301
- // ─── Pass 4: Learning Moments ────────────────────────────────────
302
-
303
- function findLearningMoments(messages: ParsedMessage[], sessionId: string): Wisdom[] {
304
- const wisdoms: Wisdom[] = [];
305
-
306
- const surprisePatterns = [
307
- /아\s*그렇구나/i, /몰랐/i, /처음\s*알았/i, /신기/i,
308
- /oh\s*i\s*see/i, /didn.t\s*know/i, /interesting/i, /til\b/i,
309
- /그런\s*거였/i, /이유가\s*있었/i,
310
- ];
311
-
312
- for (let i = 0; i < messages.length; i++) {
313
- const msg = messages[i];
314
- if (msg.role !== "user") continue;
315
-
316
- const isSurprised = surprisePatterns.some((p) => p.test(msg.content));
317
- if (!isSurprised) continue;
318
-
319
- // What did they just learn? (previous assistant message)
320
- const prevAssistant = findPrevAssistant(messages, i);
321
- if (!prevAssistant || prevAssistant.content.length < 20) continue;
322
-
323
- const learned = abstractLearning(prevAssistant.content);
324
- const context = abstractSituation(
325
- findNearestUserMessage(messages, Math.max(0, i - 3))?.content ?? "",
326
- );
327
-
328
- wisdoms.push({
329
- id: hashWisdom(`learning:${i}:${sessionId}`),
330
- type: "principle",
331
- title: `${context}에서 알게 된 점`,
332
- body: {
333
- situation: context,
334
- approach: learned,
335
- reason: "실제 작업 과정에서 발견",
336
- },
337
- abstraction: "general",
338
- domains: extractDomains(messages.slice(Math.max(0, i - 2), i + 1)),
339
- confidence: 0.65,
340
- sourceSessionId: sessionId,
341
- sourceRange: [Math.max(0, i - 1), i],
342
- extractedAt: new Date().toISOString(),
343
- });
344
- }
345
-
346
- return wisdoms;
347
- }
348
-
349
- // ─── Pass 5: Successful Complex Patterns ─────────────────────────
350
-
351
- function findSuccessfulPatterns(messages: ParsedMessage[], sessionId: string): Wisdom[] {
352
- const wisdoms: Wisdom[] = [];
353
-
354
- // Find sequences of 3+ tool calls followed by positive user feedback
355
- for (let i = 0; i < messages.length; i++) {
356
- const msg = messages[i];
357
- if (msg.role !== "assistant") continue;
358
-
359
- const toolCount = msg.toolCalls?.length ?? 0;
360
- if (toolCount < 3) continue;
361
-
362
- // Check for positive feedback within next 3 messages
363
- const feedback = findPositiveFeedback(messages, i);
364
- if (!feedback) continue;
365
-
366
- // Abstract the pattern
367
- const toolTypes = [...new Set(msg.toolCalls?.map((tc) => tc.name) ?? [])];
368
- const userContext = findNearestUserMessage(messages, i);
369
- const situation = abstractSituation(userContext?.content ?? "");
370
- const approach = abstractMultiStepApproach(msg.toolCalls ?? []);
371
-
372
- wisdoms.push({
373
- id: hashWisdom(`pattern:${i}:${sessionId}`),
374
- type: "principle",
375
- title: `${situation} — 효과적 접근법`,
376
- body: {
377
- situation,
378
- approach,
379
- reason: "이 순서로 했을 때 성공적이었음",
380
- example: toolTypes.join(" → "),
381
- },
382
- abstraction: "moderate",
383
- domains: extractDomains([msg]),
384
- confidence: 0.7,
385
- sourceSessionId: sessionId,
386
- sourceRange: [i, feedback.index],
387
- extractedAt: new Date().toISOString(),
388
- });
389
-
390
- i = feedback.index;
391
- }
392
-
393
- return wisdoms;
394
- }
395
-
396
- // ─── Abstraction Helpers ─────────────────────────────────────────
397
-
398
- /** Convert specific user request into a general situation description. */
399
- function abstractSituation(text: string): string {
400
- if (!text || text.length < 3) return "";
401
-
402
- // Remove specific file paths, variable names, etc.
403
- let abstracted = text
404
- .replace(/\/[\w./\\-]+/g, "")
405
- .replace(/`[^`]+`/g, "")
406
- .replace(/https?:\/\/\S+/g, "")
407
- .replace(/\b[0-9a-f]{8,}\b/g, "")
408
- .replace(/<[^>]+>/g, "")
409
- .replace(/\s+/g, " ")
410
- .trim();
411
-
412
- if (abstracted.length < 5) return "";
413
-
414
- // Take first meaningful sentence
415
- const firstSentence = abstracted.split(/[.!?\n]/)[0]?.trim() ?? abstracted;
416
- return firstSentence.slice(0, 80);
417
- }
418
-
419
- /** Abstract what an assistant was doing into a general approach. */
420
- function abstractApproach(msg: ParsedMessage | null): string {
421
- if (!msg) return "이전 접근법";
422
-
423
- if (msg.toolCalls && msg.toolCalls.length > 0) {
424
- const toolNames = [...new Set(msg.toolCalls.map((tc) => tc.name))];
425
- return `${toolNames.join(", ")} 사용`;
426
- }
427
-
428
- // Extract key action from text
429
- const text = msg.content.slice(0, 200);
430
- const actionMatch = text.match(/(?:먼저|우선|first)\s+(.{10,40})/i) ??
431
- text.match(/(.{10,40})(?:하겠|할게|합니다|했습니다)/);
432
- if (actionMatch) return actionMatch[1].trim();
433
-
434
- return text.slice(0, 60);
435
- }
436
-
437
- /** Abstract a learning into a general principle. */
438
- function abstractLearning(assistantText: string): string {
439
- // Extract the key insight — usually the first substantive sentence
440
- const sentences = assistantText.split(/[.!?\n]/).filter((s) => s.trim().length > 15);
441
- if (sentences.length === 0) return assistantText.slice(0, 100);
442
-
443
- // Prefer sentences with explanation markers
444
- const explanatory = sentences.find((s) =>
445
- /때문|because|이유|reason|즉|essentially|사실|actually|중요한|important/.test(s),
446
- );
447
-
448
- return (explanatory ?? sentences[0]).trim().slice(0, 150);
449
- }
450
-
451
- /** Abstract a multi-step tool sequence into a general approach. */
452
- function abstractMultiStepApproach(toolCalls: ToolCall[]): string {
453
- const steps: string[] = [];
454
- const seen = new Set<string>();
455
-
456
- for (const tc of toolCalls) {
457
- const toolName = tc.name;
458
- if (seen.has(toolName)) continue;
459
- seen.add(toolName);
460
-
461
- switch (toolName) {
462
- case "Bash":
463
- steps.push("명령어로 현재 상태를 확인");
464
- break;
465
- case "Read":
466
- steps.push("관련 파일을 읽어서 구조를 파악");
467
- break;
468
- case "Grep":
469
- steps.push("코드베이스에서 관련 패턴을 검색");
470
- break;
471
- case "Edit":
472
- steps.push("문제가 있는 부분을 수정");
473
- break;
474
- case "Write":
475
- steps.push("새로운 파일을 생성");
476
- break;
477
- case "Agent":
478
- steps.push("복잡한 하위 작업을 에이전트에 위임");
479
- break;
480
- case "WebSearch":
481
- steps.push("최신 정보나 문서를 검색");
482
- break;
483
- default:
484
- steps.push(`${toolName} 사용`);
485
- }
486
- }
487
-
488
- if (steps.length <= 1) return steps[0] ?? "단일 단계 접근";
489
- return steps.map((s, i) => `${i + 1}. ${s}`).join("\n");
490
- }
491
-
492
- /** Describe a tool call at an abstract level. */
493
- function describeToolAction(tc: ToolCall | undefined): { method: string; description: string; error: string } {
494
- if (!tc) return { method: "알 수 없음", description: "알 수 없는 접근", error: "" };
495
-
496
- const method = tc.name;
497
- let description = method;
498
- let error = "";
499
-
500
- if (tc.name === "Bash" && typeof tc.input["command"] === "string") {
501
- const cmd = tc.input["command"] as string;
502
- // Abstract the command
503
- if (cmd.includes("npm") || cmd.includes("pnpm")) description = "패키지 매니저 실행";
504
- else if (cmd.includes("git")) description = "git 작업";
505
- else if (cmd.includes("tsc") || cmd.includes("typescript")) description = "타입 체크";
506
- else if (cmd.includes("test")) description = "테스트 실행";
507
- else description = "쉘 명령 실행";
508
- } else if (tc.name === "Edit") {
509
- description = "코드 수정";
510
- } else if (tc.name === "Read") {
511
- description = "파일 읽기";
512
- }
513
-
514
- if (tc.result && /error|fail/i.test(tc.result)) {
515
- const errorMatch = tc.result.match(/(?:error|Error)[:\s]+(.{10,60})/i);
516
- error = errorMatch ? errorMatch[1].trim() : "실행 실패";
517
- }
518
-
519
- return { method, description, error };
520
- }
521
-
522
- // ─── Utility Helpers ─────────────────────────────────────────────
523
-
524
- function isNoiseMessage(msg: ParsedMessage): boolean {
525
- const t = msg.content.trim();
526
- return t.startsWith("<task-notification>") ||
527
- t.startsWith("<system-reminder>") ||
528
- t.startsWith("<local-command") ||
529
- t.startsWith("┌──") ||
530
- t.startsWith("{") ||
531
- /^<[a-z]/.test(t) ||
532
- t.length < 3;
533
- }
534
-
535
- function findNearestUserMessage(messages: ParsedMessage[], index: number): ParsedMessage | null {
536
- for (let i = index; i >= Math.max(0, index - 8); i--) {
537
- if (messages[i].role === "user" && messages[i].content.trim().length > 5) {
538
- if (isNoiseMessage(messages[i])) continue;
539
- return messages[i];
540
- }
541
- }
542
- return null;
543
- }
544
-
545
- function findPrevAssistant(messages: ParsedMessage[], index: number): ParsedMessage | null {
546
- for (let i = index - 1; i >= Math.max(0, index - 3); i--) {
547
- if (messages[i].role === "assistant" && messages[i].content.trim().length > 10) {
548
- return messages[i];
549
- }
550
- }
551
- return null;
552
- }
553
-
554
- function findNextAssistant(messages: ParsedMessage[], index: number): ParsedMessage | null {
555
- for (let i = index + 1; i < Math.min(index + 4, messages.length); i++) {
556
- if (messages[i].role === "assistant" && messages[i].content.trim().length > 10) {
557
- return messages[i];
558
- }
559
- }
560
- return null;
561
- }
562
-
563
- function findPositiveFeedback(
564
- messages: ParsedMessage[],
565
- index: number,
566
- ): { message: ParsedMessage; index: number } | null {
567
- const positivePatterns = [
568
- /좋아|완벽|됐어|됐다|고마워|감사|잘됐|ㅇㅇ|ㄱㄱ|ㅇㅋ/,
569
- /good|perfect|great|thanks|nice|works|awesome/i,
570
- /다음|이제|next|now/i, // Moving on = implicit success
571
- ];
572
-
573
- for (let i = index + 1; i < Math.min(index + 5, messages.length); i++) {
574
- const msg = messages[i];
575
- if (msg.role !== "user") continue;
576
- if (positivePatterns.some((p) => p.test(msg.content))) {
577
- return { message: msg, index: i };
578
- }
579
- }
580
- return null;
581
- }
582
-
583
- function extractDomains(messages: ParsedMessage[]): string[] {
584
- const domains = new Set<string>();
585
-
586
- const domainPatterns: [string, RegExp][] = [
587
- ["security", /보안|security|취약|vulnerab|exploit|injection|audit/i],
588
- ["testing", /테스트|test|spec|assert|coverage/i],
589
- ["devops", /deploy|배포|docker|ci\/cd|github actions|npm publish/i],
590
- ["frontend", /react|vue|angular|css|html|component|jsx|tsx/i],
591
- ["backend", /server|api|database|sql|rest|graphql/i],
592
- ["git", /git|commit|push|branch|merge|pr|pull request/i],
593
- ["performance", /성능|optimize|performance|cache|speed|slow/i],
594
- ["refactoring", /refactor|리팩토|clean|정리|restructure/i],
595
- ["debugging", /debug|디버그|error|에러|log|stack trace/i],
596
- ["documentation", /문서|docs|readme|comment|주석/i],
597
- ["open-source", /오픈소스|open.?source|contribute|pr|issue/i],
598
- ];
599
-
600
- const allText = messages.map((m) => m.content).join(" ");
601
- for (const [domain, pattern] of domainPatterns) {
602
- if (pattern.test(allText)) domains.add(domain);
603
- }
604
-
605
- return [...domains];
606
- }
607
-
608
- function extractAlternatives(text: string): { chosen: string; rejected: string; reason: string } | null {
609
- // Try to find "A vs B" or "chose A over B" patterns
610
- const vsMatch = text.match(/(\S+)\s+(?:vs|versus|대)\s+(\S+)/i);
611
- if (vsMatch) {
612
- return { chosen: vsMatch[1], rejected: vsMatch[2], reason: "" };
613
- }
614
-
615
- const choseMatch = text.match(/(?:chose|선택|picked)\s+(.+?)\s+(?:over|대신|instead of)\s+(.+?)(?:\.|,|$)/i);
616
- if (choseMatch) {
617
- return { chosen: choseMatch[1].trim(), rejected: choseMatch[2].trim(), reason: "" };
618
- }
619
-
620
- return null;
621
- }
622
-
623
- function deduplicateWisdom(wisdoms: Wisdom[]): Wisdom[] {
624
- const unique: Wisdom[] = [];
625
- const seen = new Set<string>();
626
-
627
- for (const w of wisdoms) {
628
- // Simple dedup key: type + first 30 chars of situation + first 30 chars of approach
629
- const key = `${w.type}:${w.body.situation.slice(0, 30)}:${w.body.approach.slice(0, 30)}`;
630
- if (seen.has(key)) continue;
631
- seen.add(key);
632
- unique.push(w);
633
- }
634
-
635
- return unique.sort((a, b) => b.confidence - a.confidence);
636
- }
637
-
638
- function hashWisdom(input: string): string {
639
- return createHash("sha256").update(input).digest("hex").slice(0, 10);
640
- }
641
-
642
- // ─── Obsidian Renderer ───────────────────────────────────────────
643
-
644
- export function renderWisdomMarkdown(wisdom: Wisdom): string {
645
- const lines: string[] = [];
646
-
647
- const typeEmoji = wisdom.type === "principle" ? "💡"
648
- : wisdom.type === "warning" ? "⚠️" : "🤔";
649
- const typeLabel = wisdom.type === "principle" ? "원칙"
650
- : wisdom.type === "warning" ? "주의" : "판단 근거";
651
-
652
- lines.push("---");
653
- lines.push(`type: wisdom`);
654
- lines.push(`wisdom_type: ${wisdom.type}`);
655
- lines.push(`domains: [${wisdom.domains.map((d) => `"${d}"`).join(", ")}]`);
656
- lines.push(`confidence: ${wisdom.confidence.toFixed(2)}`);
657
- lines.push(`abstraction: ${wisdom.abstraction}`);
658
- lines.push(`tags: [claude/wisdom, claude/${wisdom.type}]`);
659
- lines.push("---");
660
- lines.push("");
661
- lines.push(`# ${typeEmoji} ${wisdom.title}`);
662
- lines.push("");
663
- lines.push(`> **${typeLabel}** | 확신도: ${(wisdom.confidence * 100).toFixed(0)}% | 추상화: ${wisdom.abstraction}`);
664
- lines.push("");
665
-
666
- // Situation
667
- lines.push("## 상황");
668
- lines.push("");
669
- lines.push(wisdom.body.situation);
670
- lines.push("");
671
-
672
- // Approach
673
- if (wisdom.type === "warning") {
674
- lines.push("## 하지 말 것");
675
- lines.push("");
676
- lines.push(`~~${wisdom.body.alternative ?? wisdom.body.approach}~~`);
677
- lines.push("");
678
- lines.push("## 대신 이렇게");
679
- lines.push("");
680
- lines.push(wisdom.body.approach);
681
- } else {
682
- lines.push("## 접근법");
683
- lines.push("");
684
- lines.push(wisdom.body.approach);
685
- }
686
- lines.push("");
687
-
688
- // Reason
689
- lines.push("## 이유");
690
- lines.push("");
691
- lines.push(wisdom.body.reason);
692
- lines.push("");
693
-
694
- // Example
695
- if (wisdom.body.example) {
696
- lines.push("## 실제 사례");
697
- lines.push("");
698
- lines.push(`\`${wisdom.body.example}\``);
699
- lines.push("");
700
- }
701
-
702
- // Alternative (for rationales)
703
- if (wisdom.type === "rationale" && wisdom.body.alternative) {
704
- lines.push("## 대안");
705
- lines.push("");
706
- lines.push(wisdom.body.alternative);
707
- lines.push("");
708
- }
709
-
710
- return lines.join("\n");
711
- }
712
-
713
- /** Export wisdom collection to Obsidian vault. */
714
- export function exportWisdomToObsidian(
715
- wisdoms: Wisdom[],
716
- vaultPath: string,
717
- ): string[] {
718
- const { writeFileSync, mkdirSync, existsSync } = require("node:fs") as typeof import("node:fs");
719
- const { join } = require("node:path") as typeof import("node:path");
720
-
721
- const dir = join(vaultPath, "Wisdom");
722
- if (!existsSync(dir)) {
723
- mkdirSync(dir, { recursive: true });
724
- }
725
-
726
- const files: string[] = [];
727
- for (const w of wisdoms) {
728
- const safeName = w.title
729
- .replace(/[<>:"/\\|?*#^[\]]/g, "-")
730
- .replace(/-+/g, "-")
731
- .slice(0, 60);
732
- const filePath = join(dir, `${safeName}.md`);
733
- writeFileSync(filePath, renderWisdomMarkdown(w), "utf-8");
734
- files.push(filePath);
735
- }
736
- return files;
737
- }