@awareness-sdk/local 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/awareness-local.mjs +489 -0
- package/package.json +31 -0
- package/src/api.mjs +122 -0
- package/src/core/cloud-sync.mjs +970 -0
- package/src/core/config.mjs +303 -0
- package/src/core/embedder.mjs +239 -0
- package/src/core/index.mjs +34 -0
- package/src/core/indexer.mjs +726 -0
- package/src/core/knowledge-extractor.mjs +629 -0
- package/src/core/memory-store.mjs +665 -0
- package/src/core/search.mjs +633 -0
- package/src/daemon.mjs +1720 -0
- package/src/mcp-server.mjs +335 -0
- package/src/spec/awareness-spec.json +393 -0
- package/src/web/index.html +1015 -0
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowledge Extractor for Awareness Local
|
|
3
|
+
*
|
|
4
|
+
* 2-layer extraction strategy:
|
|
5
|
+
* Layer 1: Agent pre-extracted insights (primary path, highest quality)
|
|
6
|
+
* The AI agent itself is the best LLM — it already understands context
|
|
7
|
+
* Layer 2: Rule engine fallback (zero LLM dependency)
|
|
8
|
+
* Multilingual regex patterns for when agents write without insights
|
|
9
|
+
*
|
|
10
|
+
* Supports 13 knowledge card categories:
|
|
11
|
+
* Engineering: problem_solution, decision, workflow, key_point, pitfall, insight
|
|
12
|
+
* Personal: personal_preference, important_detail, plan_intention,
|
|
13
|
+
* activity_preference, health_info, career_info, custom_misc
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import crypto from 'node:crypto';
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Multilingual regex patterns (NOT hardcoded to any single language)
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
/** Decision patterns: Chinese, English, Japanese */
|
|
25
|
+
const DECISION_PATTERNS = [
|
|
26
|
+
/\b(decided?|decision|chose|选择了|决定|決定|決めた)\b/i,
|
|
27
|
+
/\b(migrat(ed?|ion)|switched to|replaced|迁移|替换|切换)\b/i,
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/** Problem/solution patterns */
|
|
31
|
+
const SOLUTION_PATTERNS = [
|
|
32
|
+
/\b(fix(ed)?|resolved?|solved?|修复|解决|修正|直した)\b/i,
|
|
33
|
+
/\b(bug|issue|error|problem|问题|バグ|エラー)\b/i,
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
/** Workflow/process patterns */
|
|
37
|
+
const WORKFLOW_PATTERNS = [
|
|
38
|
+
/\b(step\s*\d|workflow|process|流程|步骤|手順)\b/i,
|
|
39
|
+
/\b(first|then|finally|首先|然后|最后|まず|次に)\b/i,
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/** Risk/warning patterns */
|
|
43
|
+
const RISK_PATTERNS = [
|
|
44
|
+
/\b(risk|warning|caution|danger|注意|风险|リスク|危険)\b/i,
|
|
45
|
+
/\b(careful|watch out|be aware|小心|警告)\b/i,
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
/** Task/TODO patterns */
|
|
49
|
+
const TASK_PATTERNS = [
|
|
50
|
+
/^[\s]*[-*]\s*\[\s*\]/m, // - [ ] unchecked checkbox
|
|
51
|
+
/\b(TODO|FIXME|HACK|待办|要做)\b/i,
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// KnowledgeExtractor
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
export class KnowledgeExtractor {
|
|
59
|
+
/**
|
|
60
|
+
* @param {object} memoryStore - MemoryStore instance for file writes
|
|
61
|
+
* @param {object} indexer - Indexer instance for index updates
|
|
62
|
+
*/
|
|
63
|
+
constructor(memoryStore, indexer) {
|
|
64
|
+
this.store = memoryStore;
|
|
65
|
+
this.indexer = indexer;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// -------------------------------------------------------------------------
|
|
69
|
+
// Main entry
|
|
70
|
+
// -------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Extract knowledge cards, tasks, and risks from memory content.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} content - Raw memory content
|
|
76
|
+
* @param {object} metadata - Memory metadata (id, tags, agent_role, etc.)
|
|
77
|
+
* @param {object|null} preExtractedInsights - Agent-provided insights (Layer 1)
|
|
78
|
+
* @returns {Promise<{ cards: object[], tasks: object[], risks: object[] }>}
|
|
79
|
+
*/
|
|
80
|
+
async extract(content, metadata, preExtractedInsights = null) {
|
|
81
|
+
let result;
|
|
82
|
+
|
|
83
|
+
// Layer 1: Agent pre-extracted insights (95% of cases)
|
|
84
|
+
if (preExtractedInsights && this._hasInsights(preExtractedInsights)) {
|
|
85
|
+
result = this.processPreExtracted(preExtractedInsights, metadata);
|
|
86
|
+
} else {
|
|
87
|
+
// Layer 2: Rule engine fallback (SDK/API writes without agent)
|
|
88
|
+
result = this.extractByRules(content, metadata);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Persist extracted artifacts to disk + index
|
|
92
|
+
await this._persistAll(result, metadata);
|
|
93
|
+
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// -------------------------------------------------------------------------
|
|
98
|
+
// Layer 1: Process agent pre-extracted insights
|
|
99
|
+
// -------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Transform agent-provided insights into internal card/task/risk format.
|
|
103
|
+
* The agent (Claude/GPT/Gemini/etc.) already did the hard extraction work
|
|
104
|
+
* during the conversation — we just normalize the structure.
|
|
105
|
+
*
|
|
106
|
+
* @param {object} insights - { knowledge_cards?, action_items?, risks? }
|
|
107
|
+
* @param {object} metadata - Parent memory metadata
|
|
108
|
+
* @returns {{ cards: object[], tasks: object[], risks: object[] }}
|
|
109
|
+
*/
|
|
110
|
+
processPreExtracted(insights, metadata) {
|
|
111
|
+
const cards = [];
|
|
112
|
+
const tasks = [];
|
|
113
|
+
const risks = [];
|
|
114
|
+
|
|
115
|
+
if (insights.knowledge_cards) {
|
|
116
|
+
for (const kc of insights.knowledge_cards) {
|
|
117
|
+
cards.push({
|
|
118
|
+
id: this._generateId('kc'),
|
|
119
|
+
category: kc.category || 'key_point',
|
|
120
|
+
title: kc.title || '',
|
|
121
|
+
summary: kc.content || kc.summary || '',
|
|
122
|
+
confidence: kc.confidence ?? 0.85,
|
|
123
|
+
tags: kc.tags || metadata.tags || [],
|
|
124
|
+
source_memory_id: metadata.id,
|
|
125
|
+
created_at: new Date().toISOString(),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (insights.action_items) {
|
|
131
|
+
for (const item of insights.action_items) {
|
|
132
|
+
tasks.push({
|
|
133
|
+
id: this._generateId('task'),
|
|
134
|
+
title: item.title || '',
|
|
135
|
+
description: item.description || '',
|
|
136
|
+
priority: item.priority || 'medium',
|
|
137
|
+
status: 'open',
|
|
138
|
+
source_memory_id: metadata.id,
|
|
139
|
+
created_at: new Date().toISOString(),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (insights.risks) {
|
|
145
|
+
for (const risk of insights.risks) {
|
|
146
|
+
risks.push({
|
|
147
|
+
id: this._generateId('risk'),
|
|
148
|
+
title: risk.title || '',
|
|
149
|
+
description: risk.description || '',
|
|
150
|
+
severity: risk.severity || 'medium',
|
|
151
|
+
source_memory_id: metadata.id,
|
|
152
|
+
created_at: new Date().toISOString(),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { cards, tasks, risks };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// -------------------------------------------------------------------------
|
|
161
|
+
// Layer 2: Rule-based extraction (multilingual regex)
|
|
162
|
+
// -------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Extract knowledge using multilingual pattern matching.
|
|
166
|
+
* This is the fallback when agents don't provide pre-extracted insights.
|
|
167
|
+
*
|
|
168
|
+
* @param {string} content - Raw content text
|
|
169
|
+
* @param {object} metadata - Parent memory metadata
|
|
170
|
+
* @returns {{ cards: object[], tasks: object[], risks: object[] }}
|
|
171
|
+
*/
|
|
172
|
+
extractByRules(content, metadata) {
|
|
173
|
+
const cards = [];
|
|
174
|
+
const tasks = [];
|
|
175
|
+
const risks = [];
|
|
176
|
+
|
|
177
|
+
if (!content || typeof content !== 'string') {
|
|
178
|
+
return { cards, tasks, risks };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Decision detection
|
|
182
|
+
if (this.matchesPattern(content, DECISION_PATTERNS)) {
|
|
183
|
+
cards.push(this.buildCard('decision', content, metadata));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Problem/solution detection
|
|
187
|
+
if (this.matchesPattern(content, SOLUTION_PATTERNS)) {
|
|
188
|
+
cards.push(this.buildCard('problem_solution', content, metadata));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Workflow detection (only if not already a decision — avoid double-tagging)
|
|
192
|
+
if (this.matchesPattern(content, WORKFLOW_PATTERNS) && !this.matchesPattern(content, DECISION_PATTERNS)) {
|
|
193
|
+
cards.push(this.buildCard('workflow', content, metadata));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Task extraction
|
|
197
|
+
tasks.push(...this.extractTasks(content, TASK_PATTERNS));
|
|
198
|
+
|
|
199
|
+
// Risk extraction
|
|
200
|
+
risks.push(...this.extractRisks(content, RISK_PATTERNS));
|
|
201
|
+
|
|
202
|
+
return { cards, tasks, risks };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// -------------------------------------------------------------------------
|
|
206
|
+
// Pattern matching
|
|
207
|
+
// -------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Test if content matches ANY pattern in the array.
|
|
211
|
+
*
|
|
212
|
+
* @param {string} content - Text to test
|
|
213
|
+
* @param {RegExp[]} patterns - Array of regex patterns
|
|
214
|
+
* @returns {boolean}
|
|
215
|
+
*/
|
|
216
|
+
matchesPattern(content, patterns) {
|
|
217
|
+
return patterns.some((p) => p.test(content));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// -------------------------------------------------------------------------
|
|
221
|
+
// Card / Task / Risk builders
|
|
222
|
+
// -------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Build a knowledge card from matched content.
|
|
226
|
+
*
|
|
227
|
+
* @param {string} category - Card category (decision, problem_solution, workflow, etc.)
|
|
228
|
+
* @param {string} content - Full content text
|
|
229
|
+
* @param {object} metadata - Parent memory metadata
|
|
230
|
+
* @returns {object}
|
|
231
|
+
*/
|
|
232
|
+
buildCard(category, content, metadata) {
|
|
233
|
+
// Extract a meaningful title from the first non-empty line
|
|
234
|
+
const firstLine = content.split('\n').find((l) => l.trim().length > 0) || '';
|
|
235
|
+
const title = firstLine
|
|
236
|
+
.replace(/^#+\s*/, '') // strip markdown headers
|
|
237
|
+
.replace(/^[-*]\s*/, '') // strip list markers
|
|
238
|
+
.slice(0, 100)
|
|
239
|
+
.trim() || category;
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
id: this._generateId('kc'),
|
|
243
|
+
category,
|
|
244
|
+
title,
|
|
245
|
+
summary: this._extractSummary(content),
|
|
246
|
+
confidence: 0.7, // rule-based extraction has lower confidence than agent
|
|
247
|
+
tags: metadata.tags || [],
|
|
248
|
+
source_memory_id: metadata.id,
|
|
249
|
+
created_at: new Date().toISOString(),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Extract TODO/checkbox tasks from content.
|
|
255
|
+
*
|
|
256
|
+
* @param {string} content - Text to scan
|
|
257
|
+
* @param {RegExp[]} patterns - Task detection patterns
|
|
258
|
+
* @returns {object[]}
|
|
259
|
+
*/
|
|
260
|
+
extractTasks(content, patterns) {
|
|
261
|
+
const tasks = [];
|
|
262
|
+
const lines = content.split('\n');
|
|
263
|
+
|
|
264
|
+
for (const line of lines) {
|
|
265
|
+
const trimmed = line.trim();
|
|
266
|
+
|
|
267
|
+
// Unchecked Markdown checkbox: - [ ] task text
|
|
268
|
+
const checkboxMatch = trimmed.match(/^[-*]\s*\[\s*\]\s*(.+)/);
|
|
269
|
+
if (checkboxMatch) {
|
|
270
|
+
tasks.push({
|
|
271
|
+
id: this._generateId('task'),
|
|
272
|
+
title: checkboxMatch[1].trim(),
|
|
273
|
+
description: '',
|
|
274
|
+
priority: 'medium',
|
|
275
|
+
status: 'open',
|
|
276
|
+
created_at: new Date().toISOString(),
|
|
277
|
+
});
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// TODO/FIXME/HACK inline comments
|
|
282
|
+
const todoMatch = trimmed.match(/\b(?:TODO|FIXME|HACK|待办|要做)[:\s]*(.+)/i);
|
|
283
|
+
if (todoMatch) {
|
|
284
|
+
tasks.push({
|
|
285
|
+
id: this._generateId('task'),
|
|
286
|
+
title: todoMatch[1].trim().slice(0, 200),
|
|
287
|
+
description: '',
|
|
288
|
+
priority: trimmed.match(/FIXME/i) ? 'high' : 'medium',
|
|
289
|
+
status: 'open',
|
|
290
|
+
created_at: new Date().toISOString(),
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return tasks;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Extract risk/warning items from content.
|
|
300
|
+
*
|
|
301
|
+
* @param {string} content - Text to scan
|
|
302
|
+
* @param {RegExp[]} patterns - Risk detection patterns
|
|
303
|
+
* @returns {object[]}
|
|
304
|
+
*/
|
|
305
|
+
extractRisks(content, patterns) {
|
|
306
|
+
const risks = [];
|
|
307
|
+
|
|
308
|
+
if (!this.matchesPattern(content, patterns)) {
|
|
309
|
+
return risks;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Find lines containing risk keywords
|
|
313
|
+
const lines = content.split('\n');
|
|
314
|
+
for (const line of lines) {
|
|
315
|
+
const trimmed = line.trim();
|
|
316
|
+
if (!trimmed) continue;
|
|
317
|
+
|
|
318
|
+
const isRiskLine = patterns.some((p) => p.test(trimmed));
|
|
319
|
+
if (isRiskLine) {
|
|
320
|
+
risks.push({
|
|
321
|
+
id: this._generateId('risk'),
|
|
322
|
+
title: trimmed
|
|
323
|
+
.replace(/^[-*]\s*/, '')
|
|
324
|
+
.replace(/^#+\s*/, '')
|
|
325
|
+
.slice(0, 200)
|
|
326
|
+
.trim(),
|
|
327
|
+
description: '',
|
|
328
|
+
severity: this._inferSeverity(trimmed),
|
|
329
|
+
created_at: new Date().toISOString(),
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return risks;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// -------------------------------------------------------------------------
|
|
338
|
+
// Persistence: write extracted artifacts to disk + index
|
|
339
|
+
// -------------------------------------------------------------------------
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Save a knowledge card as a Markdown file and update the index.
|
|
343
|
+
*
|
|
344
|
+
* @param {object} card - Knowledge card object
|
|
345
|
+
* @returns {Promise<string>} Filepath of the written card
|
|
346
|
+
*/
|
|
347
|
+
async saveCard(card) {
|
|
348
|
+
const categoryDir = this._categoryToDir(card.category);
|
|
349
|
+
const filename = this._buildFilename(card.title, card.id);
|
|
350
|
+
const filepath = path.join(categoryDir, filename);
|
|
351
|
+
|
|
352
|
+
const content = this._cardToMarkdown(card);
|
|
353
|
+
|
|
354
|
+
// Ensure directory exists
|
|
355
|
+
const dir = path.dirname(filepath);
|
|
356
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
357
|
+
|
|
358
|
+
// Atomic write: tmp file + rename
|
|
359
|
+
const tmpPath = filepath + '.tmp';
|
|
360
|
+
fs.writeFileSync(tmpPath, content, 'utf-8');
|
|
361
|
+
fs.renameSync(tmpPath, filepath);
|
|
362
|
+
|
|
363
|
+
// Update index
|
|
364
|
+
if (this.indexer?.indexKnowledgeCard) {
|
|
365
|
+
this.indexer.indexKnowledgeCard({
|
|
366
|
+
id: card.id,
|
|
367
|
+
category: card.category,
|
|
368
|
+
title: card.title,
|
|
369
|
+
summary: card.summary || '',
|
|
370
|
+
source_memories: card.source_memory_id ? [card.source_memory_id] : [],
|
|
371
|
+
confidence: card.confidence ?? 0.8,
|
|
372
|
+
status: card.status || 'active',
|
|
373
|
+
tags: card.tags || [],
|
|
374
|
+
created_at: card.created_at || new Date().toISOString(),
|
|
375
|
+
filepath,
|
|
376
|
+
content: card.summary || card.title || '',
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return filepath;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Save a task as a Markdown file and update the index.
|
|
385
|
+
*
|
|
386
|
+
* @param {object} task - Task object
|
|
387
|
+
* @returns {Promise<string>} Filepath of the written task
|
|
388
|
+
*/
|
|
389
|
+
async saveTask(task) {
|
|
390
|
+
const statusDir = task.status === 'done' ? 'tasks/done' : 'tasks/open';
|
|
391
|
+
const awarenessDir = this._getAwarenessDir();
|
|
392
|
+
const filename = this._buildFilename(task.title, task.id);
|
|
393
|
+
const filepath = path.join(awarenessDir, statusDir, filename);
|
|
394
|
+
|
|
395
|
+
const content = this._taskToMarkdown(task);
|
|
396
|
+
|
|
397
|
+
// Ensure directory exists
|
|
398
|
+
const dir = path.dirname(filepath);
|
|
399
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
400
|
+
|
|
401
|
+
// Atomic write
|
|
402
|
+
const tmpPath = filepath + '.tmp';
|
|
403
|
+
fs.writeFileSync(tmpPath, content, 'utf-8');
|
|
404
|
+
fs.renameSync(tmpPath, filepath);
|
|
405
|
+
|
|
406
|
+
// Update index
|
|
407
|
+
if (this.indexer?.indexTask) {
|
|
408
|
+
this.indexer.indexTask({
|
|
409
|
+
id: task.id,
|
|
410
|
+
title: task.title,
|
|
411
|
+
description: task.description || '',
|
|
412
|
+
status: task.status || 'open',
|
|
413
|
+
priority: task.priority || 'medium',
|
|
414
|
+
agent_role: task.agent_role || null,
|
|
415
|
+
created_at: task.created_at || new Date().toISOString(),
|
|
416
|
+
updated_at: new Date().toISOString(),
|
|
417
|
+
filepath,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return filepath;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// -------------------------------------------------------------------------
|
|
425
|
+
// Internal helpers
|
|
426
|
+
// -------------------------------------------------------------------------
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Check if insights object has any meaningful data.
|
|
430
|
+
* @param {object} insights
|
|
431
|
+
* @returns {boolean}
|
|
432
|
+
*/
|
|
433
|
+
_hasInsights(insights) {
|
|
434
|
+
return (
|
|
435
|
+
(insights.knowledge_cards?.length > 0) ||
|
|
436
|
+
(insights.action_items?.length > 0) ||
|
|
437
|
+
(insights.risks?.length > 0)
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Persist all extracted cards, tasks, and risks to disk.
|
|
443
|
+
* @param {{ cards: object[], tasks: object[], risks: object[] }} result
|
|
444
|
+
* @param {object} metadata
|
|
445
|
+
*/
|
|
446
|
+
async _persistAll(result, metadata) {
|
|
447
|
+
const promises = [];
|
|
448
|
+
|
|
449
|
+
for (const card of result.cards) {
|
|
450
|
+
card.source_memory_id = card.source_memory_id || metadata.id;
|
|
451
|
+
promises.push(this.saveCard(card).catch((err) => {
|
|
452
|
+
// Log but don't fail the whole extraction
|
|
453
|
+
console.warn(`[KnowledgeExtractor] Failed to save card ${card.id}:`, err.message);
|
|
454
|
+
}));
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
for (const task of result.tasks) {
|
|
458
|
+
task.source_memory_id = task.source_memory_id || metadata.id;
|
|
459
|
+
promises.push(this.saveTask(task).catch((err) => {
|
|
460
|
+
console.warn(`[KnowledgeExtractor] Failed to save task ${task.id}:`, err.message);
|
|
461
|
+
}));
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Risks are stored as knowledge cards with category 'risk'
|
|
465
|
+
for (const risk of result.risks) {
|
|
466
|
+
const riskCard = {
|
|
467
|
+
id: risk.id,
|
|
468
|
+
category: 'risk',
|
|
469
|
+
title: risk.title,
|
|
470
|
+
summary: risk.description || risk.title,
|
|
471
|
+
confidence: 0.6,
|
|
472
|
+
tags: [],
|
|
473
|
+
severity: risk.severity,
|
|
474
|
+
source_memory_id: risk.source_memory_id || metadata.id,
|
|
475
|
+
created_at: risk.created_at,
|
|
476
|
+
};
|
|
477
|
+
promises.push(this.saveCard(riskCard).catch((err) => {
|
|
478
|
+
console.warn(`[KnowledgeExtractor] Failed to save risk ${risk.id}:`, err.message);
|
|
479
|
+
}));
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
await Promise.all(promises);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Generate a unique ID with a type prefix.
|
|
487
|
+
* @param {string} prefix - 'kc', 'task', or 'risk'
|
|
488
|
+
* @returns {string}
|
|
489
|
+
*/
|
|
490
|
+
_generateId(prefix) {
|
|
491
|
+
const ts = Date.now().toString(36);
|
|
492
|
+
const rand = crypto.randomBytes(4).toString('hex');
|
|
493
|
+
return `${prefix}_${ts}_${rand}`;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Extract a summary from content (first meaningful paragraph, max 200 chars).
|
|
498
|
+
* @param {string} content
|
|
499
|
+
* @returns {string}
|
|
500
|
+
*/
|
|
501
|
+
_extractSummary(content) {
|
|
502
|
+
if (!content) return '';
|
|
503
|
+
|
|
504
|
+
// Skip headers and blank lines, find first content paragraph
|
|
505
|
+
const lines = content.split('\n');
|
|
506
|
+
const paragraphs = [];
|
|
507
|
+
let current = '';
|
|
508
|
+
|
|
509
|
+
for (const line of lines) {
|
|
510
|
+
const trimmed = line.trim();
|
|
511
|
+
if (!trimmed) {
|
|
512
|
+
if (current) {
|
|
513
|
+
paragraphs.push(current);
|
|
514
|
+
current = '';
|
|
515
|
+
}
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
// Skip markdown headers for summary
|
|
519
|
+
if (trimmed.startsWith('#')) continue;
|
|
520
|
+
current += (current ? ' ' : '') + trimmed;
|
|
521
|
+
}
|
|
522
|
+
if (current) paragraphs.push(current);
|
|
523
|
+
|
|
524
|
+
const summary = paragraphs[0] || '';
|
|
525
|
+
if (summary.length <= 200) return summary;
|
|
526
|
+
|
|
527
|
+
const cut = summary.slice(0, 200);
|
|
528
|
+
const lastSpace = cut.lastIndexOf(' ');
|
|
529
|
+
return (lastSpace > 100 ? cut.slice(0, lastSpace) : cut) + '...';
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Map card category to .awareness/ subdirectory.
|
|
534
|
+
* @param {string} category
|
|
535
|
+
* @returns {string}
|
|
536
|
+
*/
|
|
537
|
+
_categoryToDir(category) {
|
|
538
|
+
const awarenessDir = this._getAwarenessDir();
|
|
539
|
+
const dirMap = {
|
|
540
|
+
decision: 'knowledge/decisions',
|
|
541
|
+
problem_solution: 'knowledge/solutions',
|
|
542
|
+
workflow: 'knowledge/workflows',
|
|
543
|
+
risk: 'knowledge/insights',
|
|
544
|
+
};
|
|
545
|
+
const subdir = dirMap[category] || 'knowledge/insights';
|
|
546
|
+
return path.join(awarenessDir, subdir);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Get the .awareness directory path from the memory store.
|
|
551
|
+
* @returns {string}
|
|
552
|
+
*/
|
|
553
|
+
_getAwarenessDir() {
|
|
554
|
+
// MemoryStore exposes the awareness directory path
|
|
555
|
+
if (this.store?.awarenessDir) return this.store.awarenessDir;
|
|
556
|
+
if (this.store?.projectDir) return path.join(this.store.projectDir, '.awareness');
|
|
557
|
+
return path.join(process.cwd(), '.awareness');
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Build a safe filename from title and ID.
|
|
562
|
+
* @param {string} title
|
|
563
|
+
* @param {string} id
|
|
564
|
+
* @returns {string}
|
|
565
|
+
*/
|
|
566
|
+
_buildFilename(title, id) {
|
|
567
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
568
|
+
const slug = (title || id)
|
|
569
|
+
.toLowerCase()
|
|
570
|
+
.replace(/[^a-z0-9\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\s-]/g, '')
|
|
571
|
+
.replace(/\s+/g, '-')
|
|
572
|
+
.replace(/-+/g, '-')
|
|
573
|
+
.replace(/^-|-$/g, '')
|
|
574
|
+
.slice(0, 50) || id;
|
|
575
|
+
return `${date}_${slug}.md`;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Convert a knowledge card to Markdown with YAML front matter.
|
|
580
|
+
* @param {object} card
|
|
581
|
+
* @returns {string}
|
|
582
|
+
*/
|
|
583
|
+
_cardToMarkdown(card) {
|
|
584
|
+
const tags = Array.isArray(card.tags) ? card.tags : [];
|
|
585
|
+
const frontMatter = [
|
|
586
|
+
'---',
|
|
587
|
+
`id: ${card.id}`,
|
|
588
|
+
`category: ${card.category}`,
|
|
589
|
+
`confidence: ${card.confidence ?? 0.7}`,
|
|
590
|
+
`tags: [${tags.join(', ')}]`,
|
|
591
|
+
card.source_memory_id ? `source_memory_id: ${card.source_memory_id}` : null,
|
|
592
|
+
card.severity ? `severity: ${card.severity}` : null,
|
|
593
|
+
`created_at: ${card.created_at || new Date().toISOString()}`,
|
|
594
|
+
'---',
|
|
595
|
+
].filter(Boolean);
|
|
596
|
+
|
|
597
|
+
return `${frontMatter.join('\n')}\n\n# ${card.title}\n\n${card.summary || ''}\n`;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Convert a task to Markdown with YAML front matter.
|
|
602
|
+
* @param {object} task
|
|
603
|
+
* @returns {string}
|
|
604
|
+
*/
|
|
605
|
+
_taskToMarkdown(task) {
|
|
606
|
+
const frontMatter = [
|
|
607
|
+
'---',
|
|
608
|
+
`id: ${task.id}`,
|
|
609
|
+
`priority: ${task.priority || 'medium'}`,
|
|
610
|
+
`status: ${task.status || 'open'}`,
|
|
611
|
+
task.source_memory_id ? `source_memory_id: ${task.source_memory_id}` : null,
|
|
612
|
+
`created_at: ${task.created_at || new Date().toISOString()}`,
|
|
613
|
+
'---',
|
|
614
|
+
].filter(Boolean);
|
|
615
|
+
|
|
616
|
+
return `${frontMatter.join('\n')}\n\n# ${task.title}\n\n${task.description || ''}\n`;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Infer risk severity from keyword intensity.
|
|
621
|
+
* @param {string} text
|
|
622
|
+
* @returns {string}
|
|
623
|
+
*/
|
|
624
|
+
_inferSeverity(text) {
|
|
625
|
+
if (/\b(danger|critical|危険|严重)\b/i.test(text)) return 'high';
|
|
626
|
+
if (/\b(warning|caution|警告|注意)\b/i.test(text)) return 'medium';
|
|
627
|
+
return 'low';
|
|
628
|
+
}
|
|
629
|
+
}
|