@awareness-sdk/local 0.1.3 → 0.1.5
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/package.json +1 -1
- package/src/core/cloud-sync.mjs +6 -0
- package/src/core/config.mjs +10 -3
- package/src/core/indexer.mjs +11 -3
- package/src/core/knowledge-extractor.mjs +32 -1
- package/src/daemon.mjs +136 -43
- package/src/mcp-server.mjs +38 -1
- package/src/spec/awareness-spec.json +3 -2
package/package.json
CHANGED
package/src/core/cloud-sync.mjs
CHANGED
|
@@ -475,6 +475,12 @@ export class CloudSync {
|
|
|
475
475
|
|
|
476
476
|
res.on('data', (chunk) => {
|
|
477
477
|
buffer += chunk;
|
|
478
|
+
// SECURITY C7: Cap SSE buffer to prevent unbounded memory growth
|
|
479
|
+
if (buffer.length > 1024 * 1024) {
|
|
480
|
+
console.warn(`${LOG_PREFIX} SSE buffer overflow (>1MB) — dropping`);
|
|
481
|
+
buffer = '';
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
478
484
|
const { parsed, remainder } = parseSSE(buffer);
|
|
479
485
|
buffer = remainder;
|
|
480
486
|
|
package/src/core/config.mjs
CHANGED
|
@@ -181,7 +181,10 @@ export function initLocalConfig(projectDir) {
|
|
|
181
181
|
config.device.id = deviceId;
|
|
182
182
|
config.device.name = deviceName;
|
|
183
183
|
|
|
184
|
-
|
|
184
|
+
// SECURITY C6: Atomic write (tmp+rename) to prevent corruption on crash
|
|
185
|
+
const tmpPath = configPath + '.tmp';
|
|
186
|
+
fs.writeFileSync(tmpPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
187
|
+
fs.renameSync(tmpPath, configPath);
|
|
185
188
|
return config;
|
|
186
189
|
}
|
|
187
190
|
|
|
@@ -235,9 +238,11 @@ export function saveCloudConfig(projectDir, { apiKey, memoryId, apiBase }) {
|
|
|
235
238
|
}
|
|
236
239
|
|
|
237
240
|
const configPath = getConfigPath(projectDir);
|
|
238
|
-
// Ensure dirs exist before writing
|
|
239
241
|
ensureLocalDirs(projectDir);
|
|
240
|
-
|
|
242
|
+
// SECURITY C6: Atomic write
|
|
243
|
+
const tmpPath = configPath + '.tmp';
|
|
244
|
+
fs.writeFileSync(tmpPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
245
|
+
fs.renameSync(tmpPath, configPath);
|
|
241
246
|
|
|
242
247
|
return config;
|
|
243
248
|
}
|
|
@@ -283,6 +288,8 @@ function deepClone(obj) {
|
|
|
283
288
|
*/
|
|
284
289
|
function deepMerge(target, source) {
|
|
285
290
|
for (const key of Object.keys(source)) {
|
|
291
|
+
// SECURITY H7: Prevent prototype pollution
|
|
292
|
+
if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue;
|
|
286
293
|
const srcVal = source[key];
|
|
287
294
|
const tgtVal = target[key];
|
|
288
295
|
|
package/src/core/indexer.mjs
CHANGED
|
@@ -634,13 +634,21 @@ export class Indexer {
|
|
|
634
634
|
*
|
|
635
635
|
* @param {number} [limit=5]
|
|
636
636
|
*/
|
|
637
|
-
getOpenTasks(limit =
|
|
637
|
+
getOpenTasks(limit = 0) {
|
|
638
|
+
if (limit > 0) {
|
|
639
|
+
return this.db
|
|
640
|
+
.prepare(
|
|
641
|
+
`SELECT * FROM tasks WHERE status = 'open'
|
|
642
|
+
ORDER BY created_at DESC LIMIT ?`
|
|
643
|
+
)
|
|
644
|
+
.all(limit);
|
|
645
|
+
}
|
|
638
646
|
return this.db
|
|
639
647
|
.prepare(
|
|
640
648
|
`SELECT * FROM tasks WHERE status = 'open'
|
|
641
|
-
ORDER BY created_at DESC
|
|
649
|
+
ORDER BY created_at DESC`
|
|
642
650
|
)
|
|
643
|
-
.all(
|
|
651
|
+
.all();
|
|
644
652
|
}
|
|
645
653
|
|
|
646
654
|
/**
|
|
@@ -154,7 +154,18 @@ export class KnowledgeExtractor {
|
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
|
|
157
|
+
// Collect completed_tasks for auto-completion processing
|
|
158
|
+
const completedTasks = [];
|
|
159
|
+
if (insights.completed_tasks) {
|
|
160
|
+
for (const ct of insights.completed_tasks) {
|
|
161
|
+
const taskId = (ct.task_id || '').trim();
|
|
162
|
+
if (taskId) {
|
|
163
|
+
completedTasks.push({ task_id: taskId, reason: ct.reason || '' });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { cards, tasks, risks, completedTasks };
|
|
158
169
|
}
|
|
159
170
|
|
|
160
171
|
// -------------------------------------------------------------------------
|
|
@@ -480,6 +491,26 @@ export class KnowledgeExtractor {
|
|
|
480
491
|
}
|
|
481
492
|
|
|
482
493
|
await Promise.all(promises);
|
|
494
|
+
|
|
495
|
+
// Auto-complete tasks identified by the LLM
|
|
496
|
+
if (result.completedTasks && this.indexer) {
|
|
497
|
+
for (const ct of result.completedTasks) {
|
|
498
|
+
try {
|
|
499
|
+
const existing = this.indexer.db
|
|
500
|
+
.prepare('SELECT * FROM tasks WHERE id = ?')
|
|
501
|
+
.get(ct.task_id);
|
|
502
|
+
if (existing && existing.status !== 'done') {
|
|
503
|
+
this.indexer.indexTask({
|
|
504
|
+
...existing,
|
|
505
|
+
status: 'done',
|
|
506
|
+
updated_at: new Date().toISOString(),
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
} catch (err) {
|
|
510
|
+
console.warn(`[KnowledgeExtractor] Failed to auto-complete task ${ct.task_id}:`, err.message);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
483
514
|
}
|
|
484
515
|
|
|
485
516
|
/**
|
package/src/daemon.mjs
CHANGED
|
@@ -50,23 +50,38 @@ function nowISO() {
|
|
|
50
50
|
*/
|
|
51
51
|
function jsonResponse(res, data, status = 200) {
|
|
52
52
|
const body = JSON.stringify(data);
|
|
53
|
+
// SECURITY: Only allow requests from localhost dashboard (not arbitrary websites)
|
|
54
|
+
const origin = 'http://localhost:37800';
|
|
53
55
|
res.writeHead(status, {
|
|
54
56
|
'Content-Type': 'application/json',
|
|
55
57
|
'Content-Length': Buffer.byteLength(body),
|
|
56
|
-
'Access-Control-Allow-Origin':
|
|
58
|
+
'Access-Control-Allow-Origin': origin,
|
|
57
59
|
});
|
|
58
60
|
res.end(body);
|
|
59
61
|
}
|
|
60
62
|
|
|
63
|
+
/** Max request body size (10 MB) — prevents memory exhaustion DoS. */
|
|
64
|
+
const MAX_BODY_BYTES = 10 * 1024 * 1024;
|
|
65
|
+
|
|
61
66
|
/**
|
|
62
67
|
* Read the full request body as a string.
|
|
68
|
+
* Rejects with 413 if body exceeds MAX_BODY_BYTES.
|
|
63
69
|
* @param {http.IncomingMessage} req
|
|
64
70
|
* @returns {Promise<string>}
|
|
65
71
|
*/
|
|
66
72
|
function readBody(req) {
|
|
67
73
|
return new Promise((resolve, reject) => {
|
|
68
74
|
const chunks = [];
|
|
69
|
-
|
|
75
|
+
let totalBytes = 0;
|
|
76
|
+
req.on('data', (chunk) => {
|
|
77
|
+
totalBytes += chunk.length;
|
|
78
|
+
if (totalBytes > MAX_BODY_BYTES) {
|
|
79
|
+
req.destroy();
|
|
80
|
+
reject(new Error('Payload too large (max 10MB)'));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
chunks.push(chunk);
|
|
84
|
+
});
|
|
70
85
|
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
71
86
|
req.on('error', reject);
|
|
72
87
|
});
|
|
@@ -151,6 +166,11 @@ export class AwarenessLocalDaemon {
|
|
|
151
166
|
* 7. Start fs.watch on memories dir
|
|
152
167
|
*/
|
|
153
168
|
async start() {
|
|
169
|
+
// SECURITY C4: Prevent unhandled rejections from crashing the daemon
|
|
170
|
+
process.on('unhandledRejection', (err) => {
|
|
171
|
+
console.error('[awareness-local] unhandled rejection:', err?.message || err);
|
|
172
|
+
});
|
|
173
|
+
|
|
154
174
|
if (await this.isRunning()) {
|
|
155
175
|
console.log(
|
|
156
176
|
`[awareness-local] daemon already running on port ${this.port}`
|
|
@@ -623,10 +643,31 @@ export class AwarenessLocalDaemon {
|
|
|
623
643
|
const session = this._createSession(args.source);
|
|
624
644
|
const stats = this.indexer.getStats();
|
|
625
645
|
const recentCards = this.indexer.getRecentKnowledge(args.max_cards ?? 5);
|
|
626
|
-
const openTasks = this.indexer.getOpenTasks(args.max_tasks ??
|
|
646
|
+
const openTasks = this.indexer.getOpenTasks(args.max_tasks ?? 0);
|
|
627
647
|
const recentSessions = this.indexer.getRecentSessions(args.days ?? 7);
|
|
628
648
|
const spec = this._loadSpec();
|
|
629
649
|
|
|
650
|
+
// Compute attention_summary for LLM-side triage
|
|
651
|
+
const now = Date.now();
|
|
652
|
+
const staleDays = 3;
|
|
653
|
+
const staleCutoff = now - staleDays * 86400000;
|
|
654
|
+
const staleTasks = openTasks.filter(t => {
|
|
655
|
+
const created = t.created_at ? new Date(t.created_at).getTime() : now;
|
|
656
|
+
return created < staleCutoff;
|
|
657
|
+
}).length;
|
|
658
|
+
const riskCards = this.indexer.db
|
|
659
|
+
.prepare("SELECT COUNT(*) as cnt FROM knowledge_cards WHERE (category = 'risk' OR category = 'pitfall') AND status = 'active'")
|
|
660
|
+
.get();
|
|
661
|
+
const highRisks = riskCards?.cnt || 0;
|
|
662
|
+
|
|
663
|
+
const attentionSummary = {
|
|
664
|
+
stale_tasks: staleTasks,
|
|
665
|
+
high_risks: highRisks,
|
|
666
|
+
total_open_tasks: openTasks.length,
|
|
667
|
+
total_knowledge_cards: recentCards.length,
|
|
668
|
+
needs_attention: staleTasks > 0 || highRisks > 0,
|
|
669
|
+
};
|
|
670
|
+
|
|
630
671
|
return {
|
|
631
672
|
content: [{
|
|
632
673
|
type: 'text',
|
|
@@ -637,6 +678,7 @@ export class AwarenessLocalDaemon {
|
|
|
637
678
|
open_tasks: openTasks,
|
|
638
679
|
recent_sessions: recentSessions,
|
|
639
680
|
stats,
|
|
681
|
+
attention_summary: attentionSummary,
|
|
640
682
|
synthesized_rules: spec.core_lines?.join('\n') || '',
|
|
641
683
|
init_guides: spec.init_guides || {},
|
|
642
684
|
agent_profiles: [],
|
|
@@ -653,31 +695,20 @@ export class AwarenessLocalDaemon {
|
|
|
653
695
|
const items = this.search
|
|
654
696
|
? await this.search.getFullContent(args.ids)
|
|
655
697
|
: [];
|
|
698
|
+
// Return as readable text (no JSON noise for the Agent)
|
|
699
|
+
const sections = items.map(r => {
|
|
700
|
+
const header = r.title ? `## ${r.title}` : '';
|
|
701
|
+
return `${header}\n\n${r.content || '(no content)'}`;
|
|
702
|
+
});
|
|
656
703
|
return {
|
|
657
|
-
content: [{
|
|
658
|
-
type: 'text',
|
|
659
|
-
text: JSON.stringify({
|
|
660
|
-
results: items,
|
|
661
|
-
total: items.length,
|
|
662
|
-
mode: 'local',
|
|
663
|
-
detail: 'full',
|
|
664
|
-
}),
|
|
665
|
-
}],
|
|
704
|
+
content: [{ type: 'text', text: sections.join('\n\n---\n\n') || '(no results)' }],
|
|
666
705
|
};
|
|
667
706
|
}
|
|
668
707
|
|
|
669
708
|
// Phase 1: search + summary
|
|
670
709
|
if (!args.semantic_query && !args.keyword_query) {
|
|
671
710
|
return {
|
|
672
|
-
content: [{
|
|
673
|
-
type: 'text',
|
|
674
|
-
text: JSON.stringify({
|
|
675
|
-
results: [],
|
|
676
|
-
total: 0,
|
|
677
|
-
mode: 'local',
|
|
678
|
-
detail: 'summary',
|
|
679
|
-
}),
|
|
680
|
-
}],
|
|
711
|
+
content: [{ type: 'text', text: 'No query provided. Use semantic_query or keyword_query to search.' }],
|
|
681
712
|
};
|
|
682
713
|
}
|
|
683
714
|
|
|
@@ -685,17 +716,32 @@ export class AwarenessLocalDaemon {
|
|
|
685
716
|
? await this.search.recall(args)
|
|
686
717
|
: [];
|
|
687
718
|
|
|
719
|
+
if (!summaries.length) {
|
|
720
|
+
return {
|
|
721
|
+
content: [{ type: 'text', text: 'No matching memories found.' }],
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Format as readable text for the Agent (not raw JSON)
|
|
726
|
+
const lines = summaries.map((r, i) => {
|
|
727
|
+
const type = r.type ? `[${r.type}]` : '';
|
|
728
|
+
const title = r.title || '(untitled)';
|
|
729
|
+
const summary = r.summary ? `\n ${r.summary}` : '';
|
|
730
|
+
return `${i + 1}. ${type} ${title}${summary}`;
|
|
731
|
+
});
|
|
732
|
+
const readableText = `Found ${summaries.length} memories:\n\n${lines.join('\n\n')}`;
|
|
733
|
+
|
|
734
|
+
// IDs in a separate content block for Phase 2 expansion
|
|
735
|
+
const idsMeta = JSON.stringify({
|
|
736
|
+
_ids: summaries.map(r => r.id),
|
|
737
|
+
_hint: 'To see full content, call awareness_recall(detail="full", ids=[...]) with IDs above.',
|
|
738
|
+
});
|
|
739
|
+
|
|
688
740
|
return {
|
|
689
|
-
content: [
|
|
690
|
-
type: 'text',
|
|
691
|
-
text:
|
|
692
|
-
|
|
693
|
-
total: summaries.length,
|
|
694
|
-
mode: 'local',
|
|
695
|
-
detail: args.detail || 'summary',
|
|
696
|
-
search_method: 'hybrid',
|
|
697
|
-
}),
|
|
698
|
-
}],
|
|
741
|
+
content: [
|
|
742
|
+
{ type: 'text', text: readableText },
|
|
743
|
+
{ type: 'text', text: idsMeta },
|
|
744
|
+
],
|
|
699
745
|
};
|
|
700
746
|
}
|
|
701
747
|
|
|
@@ -920,7 +966,8 @@ export class AwarenessLocalDaemon {
|
|
|
920
966
|
*/
|
|
921
967
|
_apiListTasks(_req, res, url) {
|
|
922
968
|
const status = url.searchParams.get('status') || null;
|
|
923
|
-
const
|
|
969
|
+
const limitParam = url.searchParams.get('limit');
|
|
970
|
+
const limit = limitParam ? parseInt(limitParam, 10) : 0;
|
|
924
971
|
|
|
925
972
|
if (!this.indexer) {
|
|
926
973
|
return jsonResponse(res, { items: [], total: 0 });
|
|
@@ -939,8 +986,11 @@ export class AwarenessLocalDaemon {
|
|
|
939
986
|
sql += ' WHERE ' + conditions.join(' AND ');
|
|
940
987
|
}
|
|
941
988
|
|
|
942
|
-
sql += ` ORDER BY CASE priority WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 ELSE 4 END, created_at DESC
|
|
943
|
-
|
|
989
|
+
sql += ` ORDER BY CASE priority WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 ELSE 4 END, created_at DESC`;
|
|
990
|
+
if (limit > 0) {
|
|
991
|
+
sql += ` LIMIT ?`;
|
|
992
|
+
params.push(limit);
|
|
993
|
+
}
|
|
944
994
|
|
|
945
995
|
const rows = this.indexer.db.prepare(sql).all(...params);
|
|
946
996
|
return jsonResponse(res, { items: rows, total: rows.length });
|
|
@@ -1043,7 +1093,9 @@ export class AwarenessLocalDaemon {
|
|
|
1043
1093
|
}
|
|
1044
1094
|
|
|
1045
1095
|
try {
|
|
1046
|
-
|
|
1096
|
+
const tmpCfg = configPath + '.tmp';
|
|
1097
|
+
fs.writeFileSync(tmpCfg, JSON.stringify(config, null, 2), 'utf-8');
|
|
1098
|
+
fs.renameSync(tmpCfg, configPath);
|
|
1047
1099
|
} catch (err) {
|
|
1048
1100
|
return jsonResponse(res, { error: 'Failed to save config: ' + err.message }, 500);
|
|
1049
1101
|
}
|
|
@@ -1078,9 +1130,13 @@ export class AwarenessLocalDaemon {
|
|
|
1078
1130
|
let params;
|
|
1079
1131
|
try { params = JSON.parse(body); } catch { return jsonResponse(res, { error: 'Invalid JSON' }, 400); }
|
|
1080
1132
|
|
|
1081
|
-
const
|
|
1082
|
-
const
|
|
1083
|
-
|
|
1133
|
+
const config = this._loadConfig();
|
|
1134
|
+
const apiBase = config?.cloud?.api_base || 'https://awareness.market/api/v1';
|
|
1135
|
+
|
|
1136
|
+
// SECURITY C5: Don't hold connection for 5 minutes.
|
|
1137
|
+
// Poll a few times (max 30s), then return pending for client to retry.
|
|
1138
|
+
const interval = Math.max((params.interval || 5) * 1000, 3000);
|
|
1139
|
+
const maxPolls = Math.min(Math.floor(30000 / interval), 6);
|
|
1084
1140
|
|
|
1085
1141
|
for (let i = 0; i < maxPolls; i++) {
|
|
1086
1142
|
try {
|
|
@@ -1100,8 +1156,10 @@ export class AwarenessLocalDaemon {
|
|
|
1100
1156
|
}
|
|
1101
1157
|
|
|
1102
1158
|
async _apiCloudListMemories(req, res, url) {
|
|
1103
|
-
|
|
1104
|
-
|
|
1159
|
+
// SECURITY C3: Use cloud config API key instead of query param (avoid log/referer leaks)
|
|
1160
|
+
const config = this._loadConfig();
|
|
1161
|
+
const apiKey = config?.cloud?.api_key;
|
|
1162
|
+
if (!apiKey) return jsonResponse(res, { error: 'Cloud not configured. Connect via /api/v1/cloud/connect first.' }, 400);
|
|
1105
1163
|
|
|
1106
1164
|
const apiBase = this.config?.cloud?.api_base || 'https://awareness.market/api/v1';
|
|
1107
1165
|
try {
|
|
@@ -1248,11 +1306,18 @@ export class AwarenessLocalDaemon {
|
|
|
1248
1306
|
return this.indexer.createSession(source || 'local');
|
|
1249
1307
|
}
|
|
1250
1308
|
|
|
1309
|
+
/** Max content size per memory (1 MB). */
|
|
1310
|
+
static MAX_CONTENT_BYTES = 1024 * 1024;
|
|
1311
|
+
|
|
1251
1312
|
/** Write a single memory, index it, and trigger knowledge extraction. */
|
|
1252
1313
|
async _remember(params) {
|
|
1253
1314
|
if (!params.content) {
|
|
1254
1315
|
return { error: 'content is required for remember action' };
|
|
1255
1316
|
}
|
|
1317
|
+
// SECURITY H1: Reject oversized content to prevent FTS5/embedding freeze
|
|
1318
|
+
if (typeof params.content === 'string' && params.content.length > AwarenessLocalDaemon.MAX_CONTENT_BYTES) {
|
|
1319
|
+
return { error: `Content too large (${params.content.length} bytes, max ${AwarenessLocalDaemon.MAX_CONTENT_BYTES})` };
|
|
1320
|
+
}
|
|
1256
1321
|
|
|
1257
1322
|
// Auto-generate title from content if not provided
|
|
1258
1323
|
let title = params.title || '';
|
|
@@ -1444,10 +1509,35 @@ ${item.description || item.title || ''}
|
|
|
1444
1509
|
}
|
|
1445
1510
|
}
|
|
1446
1511
|
|
|
1512
|
+
// Auto-complete tasks identified by the LLM
|
|
1513
|
+
let tasksAutoCompleted = 0;
|
|
1514
|
+
if (Array.isArray(insights.completed_tasks)) {
|
|
1515
|
+
for (const completed of insights.completed_tasks) {
|
|
1516
|
+
const taskId = (completed.task_id || '').trim();
|
|
1517
|
+
if (!taskId) continue;
|
|
1518
|
+
try {
|
|
1519
|
+
const existing = this.indexer.db
|
|
1520
|
+
.prepare('SELECT * FROM tasks WHERE id = ?')
|
|
1521
|
+
.get(taskId);
|
|
1522
|
+
if (existing && existing.status !== 'done') {
|
|
1523
|
+
this.indexer.indexTask({
|
|
1524
|
+
...existing,
|
|
1525
|
+
status: 'done',
|
|
1526
|
+
updated_at: nowISO(),
|
|
1527
|
+
});
|
|
1528
|
+
tasksAutoCompleted++;
|
|
1529
|
+
}
|
|
1530
|
+
} catch (err) {
|
|
1531
|
+
console.warn(`[AwarenessDaemon] Failed to auto-complete task '${taskId}':`, err.message);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1447
1536
|
return {
|
|
1448
1537
|
status: 'ok',
|
|
1449
1538
|
cards_created: cardsCreated,
|
|
1450
1539
|
tasks_created: tasksCreated,
|
|
1540
|
+
tasks_auto_completed: tasksAutoCompleted,
|
|
1451
1541
|
mode: 'local',
|
|
1452
1542
|
};
|
|
1453
1543
|
}
|
|
@@ -1461,7 +1551,7 @@ ${item.description || item.title || ''}
|
|
|
1461
1551
|
// Full context dump
|
|
1462
1552
|
const stats = this.indexer.getStats();
|
|
1463
1553
|
const knowledge = this.indexer.getRecentKnowledge(limit);
|
|
1464
|
-
const tasks = this.indexer.getOpenTasks(
|
|
1554
|
+
const tasks = this.indexer.getOpenTasks(0);
|
|
1465
1555
|
const sessions = this.indexer.getRecentSessions(7);
|
|
1466
1556
|
return { stats, knowledge_cards: knowledge, open_tasks: tasks, recent_sessions: sessions, mode: 'local' };
|
|
1467
1557
|
}
|
|
@@ -1487,8 +1577,11 @@ ${item.description || item.title || ''}
|
|
|
1487
1577
|
}
|
|
1488
1578
|
|
|
1489
1579
|
if (conditions.length) sql += ' WHERE ' + conditions.join(' AND ');
|
|
1490
|
-
sql += ' ORDER BY created_at DESC
|
|
1491
|
-
|
|
1580
|
+
sql += ' ORDER BY created_at DESC';
|
|
1581
|
+
if (limit > 0) {
|
|
1582
|
+
sql += ' LIMIT ?';
|
|
1583
|
+
sqlParams.push(limit);
|
|
1584
|
+
}
|
|
1492
1585
|
|
|
1493
1586
|
const tasks = this.indexer.db.prepare(sql).all(...sqlParams);
|
|
1494
1587
|
return { tasks, total: tasks.length, mode: 'local' };
|
package/src/mcp-server.mjs
CHANGED
|
@@ -206,11 +206,48 @@ export class LocalMcpServer {
|
|
|
206
206
|
ids: params.ids,
|
|
207
207
|
});
|
|
208
208
|
|
|
209
|
+
const effectiveDetail = params.detail || 'summary';
|
|
210
|
+
|
|
211
|
+
if (effectiveDetail === 'summary' && summaries.length > 0) {
|
|
212
|
+
// Format summary as readable text for the Agent, with IDs hidden
|
|
213
|
+
// but available for Phase 2 expansion
|
|
214
|
+
const lines = summaries.map((r, i) => {
|
|
215
|
+
const title = r.title || '(untitled)';
|
|
216
|
+
const type = r.type ? `[${r.type}]` : '';
|
|
217
|
+
const summary = r.summary ? `\n ${r.summary}` : '';
|
|
218
|
+
return `${i + 1}. ${type} ${title}${summary}`;
|
|
219
|
+
});
|
|
220
|
+
const readableText = `Found ${summaries.length} memories:\n\n${lines.join('\n\n')}`;
|
|
221
|
+
|
|
222
|
+
// Return readable text + structured data for programmatic use
|
|
223
|
+
return {
|
|
224
|
+
content: [
|
|
225
|
+
{ type: 'text', text: readableText },
|
|
226
|
+
{ type: 'text', text: JSON.stringify({
|
|
227
|
+
_ids: summaries.map(r => r.id),
|
|
228
|
+
_meta: { detail: 'summary', total: summaries.length, mode: 'local' },
|
|
229
|
+
}) },
|
|
230
|
+
],
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (effectiveDetail === 'full' && summaries.length > 0) {
|
|
235
|
+
// Full content — return as readable text
|
|
236
|
+
const sections = summaries.map(r => {
|
|
237
|
+
const header = r.title ? `## ${r.title}` : '';
|
|
238
|
+
return `${header}\n\n${r.content || '(no content)'}`;
|
|
239
|
+
});
|
|
240
|
+
return {
|
|
241
|
+
content: [{ type: 'text', text: sections.join('\n\n---\n\n') }],
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Fallback / empty results
|
|
209
246
|
return mcpResult({
|
|
210
247
|
results: summaries,
|
|
211
248
|
total: summaries.length,
|
|
212
249
|
mode: 'local',
|
|
213
|
-
detail:
|
|
250
|
+
detail: effectiveDetail,
|
|
214
251
|
search_method: 'hybrid',
|
|
215
252
|
});
|
|
216
253
|
} catch (err) {
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
{
|
|
59
59
|
"name": "awareness_init",
|
|
60
60
|
"short_desc": "Load cross-session project memory and context",
|
|
61
|
-
"long_desc": "Load cross-session project memory. Returns session_id, context (narratives, tasks, knowledge, active_skills), and usage rules. Call ONCE at session start."
|
|
61
|
+
"long_desc": "Load cross-session project memory. Returns session_id, context (narratives, tasks, knowledge, active_skills, attention_summary), and usage rules. Call ONCE at session start. ATTENTION PROTOCOL: When context.attention_summary.needs_attention is true, review all attention items (stale tasks, high risks) and automatically: (1) remind the user about stale tasks, (2) warn about high risks before starting work, (3) update resolved items via awareness_record."
|
|
62
62
|
},
|
|
63
63
|
{
|
|
64
64
|
"name": "awareness_get_agent_prompt",
|
|
@@ -385,7 +385,8 @@
|
|
|
385
385
|
"notes_lines": [
|
|
386
386
|
"## OpenClaw Notes",
|
|
387
387
|
"- OpenClaw uses a plugin-based configuration via ~/.openclaw/openclaw.json.",
|
|
388
|
-
"- Install
|
|
388
|
+
"- Install as plugin: openclaw plugins install @awareness-sdk/openclaw-memory",
|
|
389
|
+
"- Or install as skill: npx clawhub@latest install awareness-memory",
|
|
389
390
|
"- MCP tools are exposed through the plugin system, no rules file needed."
|
|
390
391
|
]
|
|
391
392
|
}
|