@awareness-sdk/local 0.1.2 → 0.1.4
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/cli.cjs +7 -0
- package/package.json +2 -2
- 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 +114 -43
- package/src/mcp-server.mjs +38 -1
- package/bin/cli.js +0 -2
package/bin/cli.cjs
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Wrapper to load the ESM entry point from CommonJS-compatible bin
|
|
4
|
+
const { pathToFileURL } = require('node:url');
|
|
5
|
+
const { join } = require('node:path');
|
|
6
|
+
const entry = join(__dirname, 'awareness-local.mjs');
|
|
7
|
+
import(pathToFileURL(entry).href);
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awareness-sdk/local",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Local-first AI agent memory system. No account needed.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
|
-
"awareness-local": "bin/cli.
|
|
8
|
+
"awareness-local": "bin/cli.cjs"
|
|
9
9
|
},
|
|
10
10
|
"main": "./src/api.mjs",
|
|
11
11
|
"exports": {
|
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,7 +643,7 @@ 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
|
|
|
@@ -653,31 +673,20 @@ export class AwarenessLocalDaemon {
|
|
|
653
673
|
const items = this.search
|
|
654
674
|
? await this.search.getFullContent(args.ids)
|
|
655
675
|
: [];
|
|
676
|
+
// Return as readable text (no JSON noise for the Agent)
|
|
677
|
+
const sections = items.map(r => {
|
|
678
|
+
const header = r.title ? `## ${r.title}` : '';
|
|
679
|
+
return `${header}\n\n${r.content || '(no content)'}`;
|
|
680
|
+
});
|
|
656
681
|
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
|
-
}],
|
|
682
|
+
content: [{ type: 'text', text: sections.join('\n\n---\n\n') || '(no results)' }],
|
|
666
683
|
};
|
|
667
684
|
}
|
|
668
685
|
|
|
669
686
|
// Phase 1: search + summary
|
|
670
687
|
if (!args.semantic_query && !args.keyword_query) {
|
|
671
688
|
return {
|
|
672
|
-
content: [{
|
|
673
|
-
type: 'text',
|
|
674
|
-
text: JSON.stringify({
|
|
675
|
-
results: [],
|
|
676
|
-
total: 0,
|
|
677
|
-
mode: 'local',
|
|
678
|
-
detail: 'summary',
|
|
679
|
-
}),
|
|
680
|
-
}],
|
|
689
|
+
content: [{ type: 'text', text: 'No query provided. Use semantic_query or keyword_query to search.' }],
|
|
681
690
|
};
|
|
682
691
|
}
|
|
683
692
|
|
|
@@ -685,17 +694,32 @@ export class AwarenessLocalDaemon {
|
|
|
685
694
|
? await this.search.recall(args)
|
|
686
695
|
: [];
|
|
687
696
|
|
|
697
|
+
if (!summaries.length) {
|
|
698
|
+
return {
|
|
699
|
+
content: [{ type: 'text', text: 'No matching memories found.' }],
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Format as readable text for the Agent (not raw JSON)
|
|
704
|
+
const lines = summaries.map((r, i) => {
|
|
705
|
+
const type = r.type ? `[${r.type}]` : '';
|
|
706
|
+
const title = r.title || '(untitled)';
|
|
707
|
+
const summary = r.summary ? `\n ${r.summary}` : '';
|
|
708
|
+
return `${i + 1}. ${type} ${title}${summary}`;
|
|
709
|
+
});
|
|
710
|
+
const readableText = `Found ${summaries.length} memories:\n\n${lines.join('\n\n')}`;
|
|
711
|
+
|
|
712
|
+
// IDs in a separate content block for Phase 2 expansion
|
|
713
|
+
const idsMeta = JSON.stringify({
|
|
714
|
+
_ids: summaries.map(r => r.id),
|
|
715
|
+
_hint: 'To see full content, call awareness_recall(detail="full", ids=[...]) with IDs above.',
|
|
716
|
+
});
|
|
717
|
+
|
|
688
718
|
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
|
-
}],
|
|
719
|
+
content: [
|
|
720
|
+
{ type: 'text', text: readableText },
|
|
721
|
+
{ type: 'text', text: idsMeta },
|
|
722
|
+
],
|
|
699
723
|
};
|
|
700
724
|
}
|
|
701
725
|
|
|
@@ -920,7 +944,8 @@ export class AwarenessLocalDaemon {
|
|
|
920
944
|
*/
|
|
921
945
|
_apiListTasks(_req, res, url) {
|
|
922
946
|
const status = url.searchParams.get('status') || null;
|
|
923
|
-
const
|
|
947
|
+
const limitParam = url.searchParams.get('limit');
|
|
948
|
+
const limit = limitParam ? parseInt(limitParam, 10) : 0;
|
|
924
949
|
|
|
925
950
|
if (!this.indexer) {
|
|
926
951
|
return jsonResponse(res, { items: [], total: 0 });
|
|
@@ -939,8 +964,11 @@ export class AwarenessLocalDaemon {
|
|
|
939
964
|
sql += ' WHERE ' + conditions.join(' AND ');
|
|
940
965
|
}
|
|
941
966
|
|
|
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
|
-
|
|
967
|
+
sql += ` ORDER BY CASE priority WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 ELSE 4 END, created_at DESC`;
|
|
968
|
+
if (limit > 0) {
|
|
969
|
+
sql += ` LIMIT ?`;
|
|
970
|
+
params.push(limit);
|
|
971
|
+
}
|
|
944
972
|
|
|
945
973
|
const rows = this.indexer.db.prepare(sql).all(...params);
|
|
946
974
|
return jsonResponse(res, { items: rows, total: rows.length });
|
|
@@ -1043,7 +1071,9 @@ export class AwarenessLocalDaemon {
|
|
|
1043
1071
|
}
|
|
1044
1072
|
|
|
1045
1073
|
try {
|
|
1046
|
-
|
|
1074
|
+
const tmpCfg = configPath + '.tmp';
|
|
1075
|
+
fs.writeFileSync(tmpCfg, JSON.stringify(config, null, 2), 'utf-8');
|
|
1076
|
+
fs.renameSync(tmpCfg, configPath);
|
|
1047
1077
|
} catch (err) {
|
|
1048
1078
|
return jsonResponse(res, { error: 'Failed to save config: ' + err.message }, 500);
|
|
1049
1079
|
}
|
|
@@ -1078,9 +1108,13 @@ export class AwarenessLocalDaemon {
|
|
|
1078
1108
|
let params;
|
|
1079
1109
|
try { params = JSON.parse(body); } catch { return jsonResponse(res, { error: 'Invalid JSON' }, 400); }
|
|
1080
1110
|
|
|
1081
|
-
const
|
|
1082
|
-
const
|
|
1083
|
-
|
|
1111
|
+
const config = this._loadConfig();
|
|
1112
|
+
const apiBase = config?.cloud?.api_base || 'https://awareness.market/api/v1';
|
|
1113
|
+
|
|
1114
|
+
// SECURITY C5: Don't hold connection for 5 minutes.
|
|
1115
|
+
// Poll a few times (max 30s), then return pending for client to retry.
|
|
1116
|
+
const interval = Math.max((params.interval || 5) * 1000, 3000);
|
|
1117
|
+
const maxPolls = Math.min(Math.floor(30000 / interval), 6);
|
|
1084
1118
|
|
|
1085
1119
|
for (let i = 0; i < maxPolls; i++) {
|
|
1086
1120
|
try {
|
|
@@ -1100,8 +1134,10 @@ export class AwarenessLocalDaemon {
|
|
|
1100
1134
|
}
|
|
1101
1135
|
|
|
1102
1136
|
async _apiCloudListMemories(req, res, url) {
|
|
1103
|
-
|
|
1104
|
-
|
|
1137
|
+
// SECURITY C3: Use cloud config API key instead of query param (avoid log/referer leaks)
|
|
1138
|
+
const config = this._loadConfig();
|
|
1139
|
+
const apiKey = config?.cloud?.api_key;
|
|
1140
|
+
if (!apiKey) return jsonResponse(res, { error: 'Cloud not configured. Connect via /api/v1/cloud/connect first.' }, 400);
|
|
1105
1141
|
|
|
1106
1142
|
const apiBase = this.config?.cloud?.api_base || 'https://awareness.market/api/v1';
|
|
1107
1143
|
try {
|
|
@@ -1248,11 +1284,18 @@ export class AwarenessLocalDaemon {
|
|
|
1248
1284
|
return this.indexer.createSession(source || 'local');
|
|
1249
1285
|
}
|
|
1250
1286
|
|
|
1287
|
+
/** Max content size per memory (1 MB). */
|
|
1288
|
+
static MAX_CONTENT_BYTES = 1024 * 1024;
|
|
1289
|
+
|
|
1251
1290
|
/** Write a single memory, index it, and trigger knowledge extraction. */
|
|
1252
1291
|
async _remember(params) {
|
|
1253
1292
|
if (!params.content) {
|
|
1254
1293
|
return { error: 'content is required for remember action' };
|
|
1255
1294
|
}
|
|
1295
|
+
// SECURITY H1: Reject oversized content to prevent FTS5/embedding freeze
|
|
1296
|
+
if (typeof params.content === 'string' && params.content.length > AwarenessLocalDaemon.MAX_CONTENT_BYTES) {
|
|
1297
|
+
return { error: `Content too large (${params.content.length} bytes, max ${AwarenessLocalDaemon.MAX_CONTENT_BYTES})` };
|
|
1298
|
+
}
|
|
1256
1299
|
|
|
1257
1300
|
// Auto-generate title from content if not provided
|
|
1258
1301
|
let title = params.title || '';
|
|
@@ -1444,10 +1487,35 @@ ${item.description || item.title || ''}
|
|
|
1444
1487
|
}
|
|
1445
1488
|
}
|
|
1446
1489
|
|
|
1490
|
+
// Auto-complete tasks identified by the LLM
|
|
1491
|
+
let tasksAutoCompleted = 0;
|
|
1492
|
+
if (Array.isArray(insights.completed_tasks)) {
|
|
1493
|
+
for (const completed of insights.completed_tasks) {
|
|
1494
|
+
const taskId = (completed.task_id || '').trim();
|
|
1495
|
+
if (!taskId) continue;
|
|
1496
|
+
try {
|
|
1497
|
+
const existing = this.indexer.db
|
|
1498
|
+
.prepare('SELECT * FROM tasks WHERE id = ?')
|
|
1499
|
+
.get(taskId);
|
|
1500
|
+
if (existing && existing.status !== 'done') {
|
|
1501
|
+
this.indexer.indexTask({
|
|
1502
|
+
...existing,
|
|
1503
|
+
status: 'done',
|
|
1504
|
+
updated_at: nowISO(),
|
|
1505
|
+
});
|
|
1506
|
+
tasksAutoCompleted++;
|
|
1507
|
+
}
|
|
1508
|
+
} catch (err) {
|
|
1509
|
+
console.warn(`[AwarenessDaemon] Failed to auto-complete task '${taskId}':`, err.message);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1447
1514
|
return {
|
|
1448
1515
|
status: 'ok',
|
|
1449
1516
|
cards_created: cardsCreated,
|
|
1450
1517
|
tasks_created: tasksCreated,
|
|
1518
|
+
tasks_auto_completed: tasksAutoCompleted,
|
|
1451
1519
|
mode: 'local',
|
|
1452
1520
|
};
|
|
1453
1521
|
}
|
|
@@ -1461,7 +1529,7 @@ ${item.description || item.title || ''}
|
|
|
1461
1529
|
// Full context dump
|
|
1462
1530
|
const stats = this.indexer.getStats();
|
|
1463
1531
|
const knowledge = this.indexer.getRecentKnowledge(limit);
|
|
1464
|
-
const tasks = this.indexer.getOpenTasks(
|
|
1532
|
+
const tasks = this.indexer.getOpenTasks(0);
|
|
1465
1533
|
const sessions = this.indexer.getRecentSessions(7);
|
|
1466
1534
|
return { stats, knowledge_cards: knowledge, open_tasks: tasks, recent_sessions: sessions, mode: 'local' };
|
|
1467
1535
|
}
|
|
@@ -1487,8 +1555,11 @@ ${item.description || item.title || ''}
|
|
|
1487
1555
|
}
|
|
1488
1556
|
|
|
1489
1557
|
if (conditions.length) sql += ' WHERE ' + conditions.join(' AND ');
|
|
1490
|
-
sql += ' ORDER BY created_at DESC
|
|
1491
|
-
|
|
1558
|
+
sql += ' ORDER BY created_at DESC';
|
|
1559
|
+
if (limit > 0) {
|
|
1560
|
+
sql += ' LIMIT ?';
|
|
1561
|
+
sqlParams.push(limit);
|
|
1562
|
+
}
|
|
1492
1563
|
|
|
1493
1564
|
const tasks = this.indexer.db.prepare(sql).all(...sqlParams);
|
|
1494
1565
|
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) {
|
package/bin/cli.js
DELETED