@hawon/nexus 0.2.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.
- package/dist/cli/index.js +44 -71
- package/dist/index.js +15 -24
- package/dist/mcp/server.js +7 -6
- package/package.json +1 -1
- package/scripts/benchmark.ts +13 -35
- package/src/cli/index.ts +45 -84
- package/src/index.ts +17 -27
- package/src/mcp/server.ts +8 -7
- package/src/memory-engine/index.ts +4 -6
- package/src/memory-engine/semantic.ts +38 -11
- package/src/promptguard/advanced-rules.ts +122 -5
- package/src/promptguard/entropy.ts +21 -2
- package/src/promptguard/rules.ts +87 -2
- package/src/promptguard/scanner.test.ts +1 -4
- package/src/promptguard/scanner.ts +1 -1
- package/src/promptguard/semantic.ts +19 -4
- package/src/promptguard/token-analysis.ts +17 -5
- package/src/review/analyzer.ts +106 -23
- package/src/skills/index.ts +11 -27
- package/src/testing/health-check.ts +19 -2
- package/src/memory-engine/compressor.ts +0 -97
- package/src/memory-engine/context-window.ts +0 -113
- package/src/memory-engine/store.ts +0 -371
- package/src/memory-engine/types.ts +0 -32
- package/src/skills/context-engine.ts +0 -863
- package/src/skills/extractor.ts +0 -224
- package/src/skills/global-context.ts +0 -731
- package/src/skills/library.ts +0 -189
- package/src/skills/pattern-engine.ts +0 -715
- package/src/skills/render-evolved.ts +0 -160
- package/src/skills/skill-reconciler.ts +0 -699
- package/src/skills/smart-extractor.ts +0 -849
- package/src/skills/types.ts +0 -18
- package/src/skills/wisdom-extractor.ts +0 -737
- package/src/superdev-evolution/index.ts +0 -3
- package/src/superdev-evolution/skill-manager.ts +0 -266
- 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
|
-
}
|