wispy-cli 0.7.0 → 0.9.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/wispy.mjs +172 -2
- package/core/cron.mjs +346 -0
- package/core/engine.mjs +272 -4
- package/core/index.mjs +3 -0
- package/core/memory.mjs +275 -0
- package/core/subagents.mjs +343 -0
- package/core/tools.mjs +60 -0
- package/lib/wispy-repl.mjs +131 -0
- package/package.json +6 -4
package/core/engine.mjs
CHANGED
|
@@ -19,6 +19,8 @@ import { ProviderRegistry } from "./providers.mjs";
|
|
|
19
19
|
import { ToolRegistry } from "./tools.mjs";
|
|
20
20
|
import { SessionManager } from "./session.mjs";
|
|
21
21
|
import { MCPManager, ensureDefaultMcpConfig } from "./mcp.mjs";
|
|
22
|
+
import { MemoryManager } from "./memory.mjs";
|
|
23
|
+
import { SubAgentManager } from "./subagents.mjs";
|
|
22
24
|
|
|
23
25
|
const MAX_TOOL_ROUNDS = 10;
|
|
24
26
|
const MAX_CONTEXT_CHARS = 40_000;
|
|
@@ -30,6 +32,8 @@ export class WispyEngine {
|
|
|
30
32
|
this.tools = new ToolRegistry();
|
|
31
33
|
this.sessions = new SessionManager();
|
|
32
34
|
this.mcpManager = new MCPManager(config.mcpConfigPath ?? MCP_CONFIG_PATH);
|
|
35
|
+
this.memory = new MemoryManager(WISPY_DIR);
|
|
36
|
+
this.subagents = new SubAgentManager(this, this.sessions);
|
|
33
37
|
this._initialized = false;
|
|
34
38
|
this._activeWorkstream = config.workstream
|
|
35
39
|
?? process.env.WISPY_WORKSTREAM
|
|
@@ -56,6 +60,9 @@ export class WispyEngine {
|
|
|
56
60
|
// Register built-in tools
|
|
57
61
|
this.tools.registerBuiltin();
|
|
58
62
|
|
|
63
|
+
// Register memory tools
|
|
64
|
+
this._registerMemoryTools();
|
|
65
|
+
|
|
59
66
|
// Initialize MCP
|
|
60
67
|
if (!opts.skipMcp) {
|
|
61
68
|
await ensureDefaultMcpConfig(this.mcpManager.configPath);
|
|
@@ -209,6 +216,29 @@ export class WispyEngine {
|
|
|
209
216
|
return this._toolSpawnAsyncAgent(args, messages, session);
|
|
210
217
|
case "ralph_loop":
|
|
211
218
|
return this._toolRalphLoop(args, messages, session);
|
|
219
|
+
case "memory_save":
|
|
220
|
+
return this._toolMemorySave(args);
|
|
221
|
+
case "memory_search":
|
|
222
|
+
return this._toolMemorySearch(args);
|
|
223
|
+
case "memory_list":
|
|
224
|
+
return this._toolMemoryList();
|
|
225
|
+
case "memory_get":
|
|
226
|
+
return this._toolMemoryGet(args);
|
|
227
|
+
case "memory_append":
|
|
228
|
+
return this._toolMemoryAppend(args);
|
|
229
|
+
case "memory_delete":
|
|
230
|
+
return this._toolMemoryDelete(args);
|
|
231
|
+
// Sub-agent tools (v0.9)
|
|
232
|
+
case "spawn_subagent":
|
|
233
|
+
return this._toolSpawnSubagent(args, opts);
|
|
234
|
+
case "list_subagents":
|
|
235
|
+
return this._toolListSubagents();
|
|
236
|
+
case "get_subagent_result":
|
|
237
|
+
return this._toolGetSubagentResult(args);
|
|
238
|
+
case "kill_subagent":
|
|
239
|
+
return this._toolKillSubagent(args);
|
|
240
|
+
case "steer_subagent":
|
|
241
|
+
return this._toolSteerSubagent(args);
|
|
212
242
|
default:
|
|
213
243
|
return this.tools.execute(name, args);
|
|
214
244
|
}
|
|
@@ -421,6 +451,88 @@ export class WispyEngine {
|
|
|
421
451
|
return { success: true, iterations: MAX_ITERATIONS, result: lastResult, verified: false, message: "Max iterations reached" };
|
|
422
452
|
}
|
|
423
453
|
|
|
454
|
+
// ── Sub-agent tools (v0.9) ──────────────────────────────────────────────────
|
|
455
|
+
|
|
456
|
+
async _toolSpawnSubagent(args, parentOpts = {}) {
|
|
457
|
+
const agent = await this.subagents.spawn({
|
|
458
|
+
task: args.task,
|
|
459
|
+
label: args.label,
|
|
460
|
+
model: args.model ?? null,
|
|
461
|
+
timeout: args.timeout_seconds ?? 300,
|
|
462
|
+
workstream: this._activeWorkstream,
|
|
463
|
+
onNotify: parentOpts?.onSubagentNotify ?? null,
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
success: true,
|
|
468
|
+
id: agent.id,
|
|
469
|
+
label: agent.label,
|
|
470
|
+
status: agent.status,
|
|
471
|
+
message: `Sub-agent '${agent.label}' spawned (id: ${agent.id}). Use get_subagent_result to check when done.`,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async _toolListSubagents() {
|
|
476
|
+
const inMemory = this.subagents.list();
|
|
477
|
+
const history = await this.subagents.listHistory(20);
|
|
478
|
+
|
|
479
|
+
// Merge: in-memory takes precedence
|
|
480
|
+
const inMemoryIds = new Set(inMemory.map(a => a.id));
|
|
481
|
+
const combined = [
|
|
482
|
+
...inMemory.map(a => a.toJSON()),
|
|
483
|
+
...history.filter(h => !inMemoryIds.has(h.id)),
|
|
484
|
+
].slice(0, 30);
|
|
485
|
+
|
|
486
|
+
return {
|
|
487
|
+
success: true,
|
|
488
|
+
agents: combined.map(a => ({
|
|
489
|
+
id: a.id,
|
|
490
|
+
label: a.label,
|
|
491
|
+
status: a.status,
|
|
492
|
+
task: a.task?.slice(0, 80),
|
|
493
|
+
model: a.model,
|
|
494
|
+
createdAt: a.createdAt,
|
|
495
|
+
completedAt: a.completedAt,
|
|
496
|
+
})),
|
|
497
|
+
total: combined.length,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async _toolGetSubagentResult(args) {
|
|
502
|
+
const inMemory = this.subagents.get(args.id);
|
|
503
|
+
if (inMemory) {
|
|
504
|
+
return {
|
|
505
|
+
success: true,
|
|
506
|
+
...inMemory.toJSON(),
|
|
507
|
+
result_preview: inMemory.result?.slice(0, 500),
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Try disk
|
|
512
|
+
const disk = await this.subagents.loadFromDisk(args.id);
|
|
513
|
+
if (disk) {
|
|
514
|
+
return { success: true, ...disk, result_preview: disk.result?.slice(0, 500) };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return { success: false, error: `Sub-agent not found: ${args.id}` };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
_toolKillSubagent(args) {
|
|
521
|
+
const agent = this.subagents.get(args.id);
|
|
522
|
+
if (!agent) return { success: false, error: `Sub-agent not found: ${args.id}` };
|
|
523
|
+
this.subagents.kill(args.id);
|
|
524
|
+
return { success: true, id: args.id, message: `Sub-agent '${agent.label}' killed.` };
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
_toolSteerSubagent(args) {
|
|
528
|
+
try {
|
|
529
|
+
this.subagents.steer(args.id, args.message);
|
|
530
|
+
return { success: true, id: args.id, message: `Guidance sent to sub-agent.` };
|
|
531
|
+
} catch (err) {
|
|
532
|
+
return { success: false, error: err.message };
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
424
536
|
// ── Context optimization ─────────────────────────────────────────────────────
|
|
425
537
|
|
|
426
538
|
_optimizeContext(messages, maxTokens = 30_000) {
|
|
@@ -476,10 +588,18 @@ export class WispyEngine {
|
|
|
476
588
|
parts.push("## Project Context (WISPY.md)", wispyMd, "");
|
|
477
589
|
}
|
|
478
590
|
|
|
479
|
-
// Load memories
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
591
|
+
// Load memories via MemoryManager
|
|
592
|
+
try {
|
|
593
|
+
const memories = await this.memory.getContextForPrompt(lastUserMessage);
|
|
594
|
+
if (memories) {
|
|
595
|
+
parts.push("## Persistent Memory", memories, "");
|
|
596
|
+
}
|
|
597
|
+
} catch {
|
|
598
|
+
// Fallback to old method
|
|
599
|
+
const memories = await this._loadMemories();
|
|
600
|
+
if (memories) {
|
|
601
|
+
parts.push("## Persistent Memory", memories, "");
|
|
602
|
+
}
|
|
483
603
|
}
|
|
484
604
|
|
|
485
605
|
return parts.join("\n");
|
|
@@ -512,6 +632,154 @@ export class WispyEngine {
|
|
|
512
632
|
return sections.length ? sections.join("\n\n") : null;
|
|
513
633
|
}
|
|
514
634
|
|
|
635
|
+
// ── Memory tools ─────────────────────────────────────────────────────────────
|
|
636
|
+
|
|
637
|
+
_registerMemoryTools() {
|
|
638
|
+
const memoryTools = [
|
|
639
|
+
{
|
|
640
|
+
name: "memory_save",
|
|
641
|
+
description: "Save important information to persistent memory. Use this to remember facts, preferences, or information for future conversations.",
|
|
642
|
+
parameters: {
|
|
643
|
+
type: "object",
|
|
644
|
+
properties: {
|
|
645
|
+
key: { type: "string", description: "Memory key/filename (e.g., 'user', 'MEMORY', 'projects/myapp', 'daily/2025-01-01')" },
|
|
646
|
+
content: { type: "string", description: "Content to save" },
|
|
647
|
+
title: { type: "string", description: "Optional title for the memory file" },
|
|
648
|
+
},
|
|
649
|
+
required: ["key", "content"],
|
|
650
|
+
},
|
|
651
|
+
},
|
|
652
|
+
{
|
|
653
|
+
name: "memory_append",
|
|
654
|
+
description: "Append a new entry to an existing memory file without overwriting it.",
|
|
655
|
+
parameters: {
|
|
656
|
+
type: "object",
|
|
657
|
+
properties: {
|
|
658
|
+
key: { type: "string", description: "Memory key/filename" },
|
|
659
|
+
content: { type: "string", description: "Content to append" },
|
|
660
|
+
},
|
|
661
|
+
required: ["key", "content"],
|
|
662
|
+
},
|
|
663
|
+
},
|
|
664
|
+
{
|
|
665
|
+
name: "memory_search",
|
|
666
|
+
description: "Search across all memory files for information. Returns matching snippets.",
|
|
667
|
+
parameters: {
|
|
668
|
+
type: "object",
|
|
669
|
+
properties: {
|
|
670
|
+
query: { type: "string", description: "Search query" },
|
|
671
|
+
},
|
|
672
|
+
required: ["query"],
|
|
673
|
+
},
|
|
674
|
+
},
|
|
675
|
+
{
|
|
676
|
+
name: "memory_list",
|
|
677
|
+
description: "List all memory files with their keys and previews.",
|
|
678
|
+
parameters: { type: "object", properties: {}, required: [] },
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
name: "memory_get",
|
|
682
|
+
description: "Get the full content of a specific memory file.",
|
|
683
|
+
parameters: {
|
|
684
|
+
type: "object",
|
|
685
|
+
properties: {
|
|
686
|
+
key: { type: "string", description: "Memory key to retrieve" },
|
|
687
|
+
},
|
|
688
|
+
required: ["key"],
|
|
689
|
+
},
|
|
690
|
+
},
|
|
691
|
+
{
|
|
692
|
+
name: "memory_delete",
|
|
693
|
+
description: "Delete a memory file.",
|
|
694
|
+
parameters: {
|
|
695
|
+
type: "object",
|
|
696
|
+
properties: {
|
|
697
|
+
key: { type: "string", description: "Memory key to delete" },
|
|
698
|
+
},
|
|
699
|
+
required: ["key"],
|
|
700
|
+
},
|
|
701
|
+
},
|
|
702
|
+
];
|
|
703
|
+
|
|
704
|
+
for (const tool of memoryTools) {
|
|
705
|
+
this.tools._definitions.set(tool.name, tool);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
async _toolMemorySave(args) {
|
|
710
|
+
try {
|
|
711
|
+
const result = await this.memory.save(args.key, args.content, { title: args.title });
|
|
712
|
+
return { success: true, key: args.key, message: `Saved to memory: ${args.key}` };
|
|
713
|
+
} catch (err) {
|
|
714
|
+
return { success: false, error: err.message };
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
async _toolMemoryAppend(args) {
|
|
719
|
+
try {
|
|
720
|
+
const result = await this.memory.append(args.key, args.content);
|
|
721
|
+
return { success: true, key: args.key, message: `Appended to memory: ${args.key}` };
|
|
722
|
+
} catch (err) {
|
|
723
|
+
return { success: false, error: err.message };
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
async _toolMemorySearch(args) {
|
|
728
|
+
try {
|
|
729
|
+
const results = await this.memory.search(args.query, { limit: 10 });
|
|
730
|
+
if (results.length === 0) {
|
|
731
|
+
return { success: true, results: [], message: "No memories found matching your query." };
|
|
732
|
+
}
|
|
733
|
+
return {
|
|
734
|
+
success: true,
|
|
735
|
+
results: results.map(r => ({
|
|
736
|
+
key: r.key,
|
|
737
|
+
matchCount: r.matchCount,
|
|
738
|
+
snippets: r.snippets.map(s => `[line ${s.lineNumber}] ${s.text}`),
|
|
739
|
+
})),
|
|
740
|
+
};
|
|
741
|
+
} catch (err) {
|
|
742
|
+
return { success: false, error: err.message };
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
async _toolMemoryList() {
|
|
747
|
+
try {
|
|
748
|
+
const keys = await this.memory.list();
|
|
749
|
+
return {
|
|
750
|
+
success: true,
|
|
751
|
+
memories: keys.map(k => ({
|
|
752
|
+
key: k.key,
|
|
753
|
+
preview: k.preview,
|
|
754
|
+
size: k.size,
|
|
755
|
+
updatedAt: k.updatedAt,
|
|
756
|
+
})),
|
|
757
|
+
total: keys.length,
|
|
758
|
+
};
|
|
759
|
+
} catch (err) {
|
|
760
|
+
return { success: false, error: err.message };
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
async _toolMemoryGet(args) {
|
|
765
|
+
try {
|
|
766
|
+
const mem = await this.memory.get(args.key);
|
|
767
|
+
if (!mem) return { success: false, error: `Memory "${args.key}" not found` };
|
|
768
|
+
return { success: true, key: args.key, content: mem.content };
|
|
769
|
+
} catch (err) {
|
|
770
|
+
return { success: false, error: err.message };
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
async _toolMemoryDelete(args) {
|
|
775
|
+
try {
|
|
776
|
+
const result = await this.memory.delete(args.key);
|
|
777
|
+
return result;
|
|
778
|
+
} catch (err) {
|
|
779
|
+
return { success: false, error: err.message };
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
515
783
|
// ── Cleanup ──────────────────────────────────────────────────────────────────
|
|
516
784
|
|
|
517
785
|
destroy() {
|
package/core/index.mjs
CHANGED
|
@@ -10,3 +10,6 @@ export { ProviderRegistry } from "./providers.mjs";
|
|
|
10
10
|
export { ToolRegistry } from "./tools.mjs";
|
|
11
11
|
export { MCPClient, MCPManager, ensureDefaultMcpConfig } from "./mcp.mjs";
|
|
12
12
|
export { loadConfig, saveConfig, detectProvider, PROVIDERS, WISPY_DIR, MCP_CONFIG_PATH, SESSIONS_DIR, CONVERSATIONS_DIR, MEMORY_DIR } from "./config.mjs";
|
|
13
|
+
export { MemoryManager } from "./memory.mjs";
|
|
14
|
+
export { CronManager } from "./cron.mjs";
|
|
15
|
+
export { SubAgentManager, SubAgent } from "./subagents.mjs";
|
package/core/memory.mjs
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/memory.mjs — Long-term memory system for Wispy
|
|
3
|
+
*
|
|
4
|
+
* File-based memory stored in ~/.wispy/memory/
|
|
5
|
+
* - MEMORY.md — main persistent memory
|
|
6
|
+
* - daily/YYYY-MM-DD.md — daily logs
|
|
7
|
+
* - projects/<name>.md — project-specific memory
|
|
8
|
+
* - user.md — user preferences/info
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { readFile, writeFile, appendFile, mkdir, readdir, unlink, stat } from "node:fs/promises";
|
|
13
|
+
|
|
14
|
+
const MAX_SEARCH_RESULTS = 20;
|
|
15
|
+
const MAX_SNIPPET_CHARS = 200;
|
|
16
|
+
|
|
17
|
+
export class MemoryManager {
|
|
18
|
+
constructor(wispyDir) {
|
|
19
|
+
this.memoryDir = path.join(wispyDir, "memory");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Ensure memory directory exists
|
|
24
|
+
*/
|
|
25
|
+
async _ensureDir(subDir = "") {
|
|
26
|
+
const dir = subDir ? path.join(this.memoryDir, subDir) : this.memoryDir;
|
|
27
|
+
await mkdir(dir, { recursive: true });
|
|
28
|
+
return dir;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolve key to file path
|
|
33
|
+
* Keys can be: "user", "MEMORY", "daily/2025-01-01", "projects/myproject"
|
|
34
|
+
*/
|
|
35
|
+
_keyToPath(key) {
|
|
36
|
+
// Normalize: strip .md extension if present
|
|
37
|
+
const clean = key.replace(/\.md$/, "");
|
|
38
|
+
return path.join(this.memoryDir, `${clean}.md`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Save (overwrite) a memory file
|
|
43
|
+
*/
|
|
44
|
+
async save(key, content, metadata = {}) {
|
|
45
|
+
await this._ensureDir();
|
|
46
|
+
|
|
47
|
+
// Ensure subdirectory exists
|
|
48
|
+
const filePath = this._keyToPath(key);
|
|
49
|
+
const dir = path.dirname(filePath);
|
|
50
|
+
await mkdir(dir, { recursive: true });
|
|
51
|
+
|
|
52
|
+
const ts = new Date().toISOString();
|
|
53
|
+
const header = metadata.title
|
|
54
|
+
? `# ${metadata.title}\n\n_Last updated: ${ts}_\n\n`
|
|
55
|
+
: `_Last updated: ${ts}_\n\n`;
|
|
56
|
+
|
|
57
|
+
await writeFile(filePath, header + content, "utf8");
|
|
58
|
+
return { key, path: filePath };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Append to an existing memory file (creates if doesn't exist)
|
|
63
|
+
*/
|
|
64
|
+
async append(key, content) {
|
|
65
|
+
await this._ensureDir();
|
|
66
|
+
const filePath = this._keyToPath(key);
|
|
67
|
+
const dir = path.dirname(filePath);
|
|
68
|
+
await mkdir(dir, { recursive: true });
|
|
69
|
+
|
|
70
|
+
const ts = new Date().toISOString().slice(0, 16);
|
|
71
|
+
await appendFile(filePath, `\n- [${ts}] ${content}\n`, "utf8");
|
|
72
|
+
return { key, path: filePath };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get a specific memory file
|
|
77
|
+
*/
|
|
78
|
+
async get(key) {
|
|
79
|
+
const filePath = this._keyToPath(key);
|
|
80
|
+
try {
|
|
81
|
+
const content = await readFile(filePath, "utf8");
|
|
82
|
+
return { key, content, path: filePath };
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* List all memory keys (relative paths without .md)
|
|
90
|
+
*/
|
|
91
|
+
async list() {
|
|
92
|
+
await this._ensureDir();
|
|
93
|
+
const keys = [];
|
|
94
|
+
await this._collectKeys(this.memoryDir, this.memoryDir, keys);
|
|
95
|
+
return keys;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async _collectKeys(baseDir, dir, keys) {
|
|
99
|
+
let entries;
|
|
100
|
+
try {
|
|
101
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
102
|
+
} catch {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
for (const entry of entries) {
|
|
106
|
+
const fullPath = path.join(dir, entry.name);
|
|
107
|
+
if (entry.isDirectory()) {
|
|
108
|
+
await this._collectKeys(baseDir, fullPath, keys);
|
|
109
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
110
|
+
const rel = path.relative(baseDir, fullPath).replace(/\.md$/, "");
|
|
111
|
+
const fileStat = await stat(fullPath).catch(() => null);
|
|
112
|
+
const content = await readFile(fullPath, "utf8").catch(() => "");
|
|
113
|
+
keys.push({
|
|
114
|
+
key: rel,
|
|
115
|
+
path: fullPath,
|
|
116
|
+
size: content.trim().length,
|
|
117
|
+
preview: content.trim().slice(0, 80).replace(/\n/g, " "),
|
|
118
|
+
updatedAt: fileStat?.mtime?.toISOString() ?? null,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Full-text search across all memory files
|
|
126
|
+
* Returns matching snippets with file path and line numbers
|
|
127
|
+
*/
|
|
128
|
+
async search(query, opts = {}) {
|
|
129
|
+
if (!query?.trim()) return [];
|
|
130
|
+
await this._ensureDir();
|
|
131
|
+
|
|
132
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
133
|
+
const results = [];
|
|
134
|
+
await this._searchDir(this.memoryDir, this.memoryDir, terms, results);
|
|
135
|
+
|
|
136
|
+
// Sort by relevance (number of term matches)
|
|
137
|
+
results.sort((a, b) => b.matchCount - a.matchCount);
|
|
138
|
+
|
|
139
|
+
return results.slice(0, opts.limit ?? MAX_SEARCH_RESULTS);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async _searchDir(baseDir, dir, terms, results) {
|
|
143
|
+
let entries;
|
|
144
|
+
try {
|
|
145
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
146
|
+
} catch {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
for (const entry of entries) {
|
|
150
|
+
const fullPath = path.join(dir, entry.name);
|
|
151
|
+
if (entry.isDirectory()) {
|
|
152
|
+
await this._searchDir(baseDir, fullPath, terms, results);
|
|
153
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
154
|
+
const content = await readFile(fullPath, "utf8").catch(() => "");
|
|
155
|
+
const lines = content.split("\n");
|
|
156
|
+
const key = path.relative(baseDir, fullPath).replace(/\.md$/, "");
|
|
157
|
+
|
|
158
|
+
let matchCount = 0;
|
|
159
|
+
const matchingLines = [];
|
|
160
|
+
|
|
161
|
+
for (let i = 0; i < lines.length; i++) {
|
|
162
|
+
const line = lines[i];
|
|
163
|
+
const lower = line.toLowerCase();
|
|
164
|
+
const lineMatches = terms.filter(t => lower.includes(t)).length;
|
|
165
|
+
if (lineMatches > 0) {
|
|
166
|
+
matchCount += lineMatches;
|
|
167
|
+
matchingLines.push({
|
|
168
|
+
lineNumber: i + 1,
|
|
169
|
+
text: line.slice(0, MAX_SNIPPET_CHARS),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (matchCount > 0) {
|
|
175
|
+
results.push({
|
|
176
|
+
key,
|
|
177
|
+
path: fullPath,
|
|
178
|
+
matchCount,
|
|
179
|
+
snippets: matchingLines.slice(0, 5),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Delete a memory file
|
|
188
|
+
*/
|
|
189
|
+
async delete(key) {
|
|
190
|
+
const filePath = this._keyToPath(key);
|
|
191
|
+
try {
|
|
192
|
+
await unlink(filePath);
|
|
193
|
+
return { success: true, key };
|
|
194
|
+
} catch {
|
|
195
|
+
return { success: false, key, error: "Not found" };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Auto-capture important info from conversation messages
|
|
201
|
+
* Uses AI to extract key facts (called from engine)
|
|
202
|
+
*/
|
|
203
|
+
async autoCapture(messages, aiCall) {
|
|
204
|
+
// Extract user messages for analysis
|
|
205
|
+
const recentUserMsgs = messages
|
|
206
|
+
.filter(m => m.role === "user")
|
|
207
|
+
.slice(-5)
|
|
208
|
+
.map(m => m.content)
|
|
209
|
+
.join("\n\n");
|
|
210
|
+
|
|
211
|
+
if (!recentUserMsgs.trim()) return null;
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const prompt = `Extract any important facts, preferences, or information from this conversation that should be remembered for future sessions. Be concise. If nothing important, reply "nothing".
|
|
215
|
+
|
|
216
|
+
Conversation:
|
|
217
|
+
${recentUserMsgs.slice(0, 2000)}`;
|
|
218
|
+
|
|
219
|
+
const result = await aiCall(prompt);
|
|
220
|
+
if (result && result.toLowerCase() !== "nothing") {
|
|
221
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
222
|
+
await this.append(`daily/${today}`, result.slice(0, 500));
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
225
|
+
} catch {
|
|
226
|
+
// Silently fail — auto-capture is optional
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Get today's daily log key
|
|
233
|
+
*/
|
|
234
|
+
getDailyKey() {
|
|
235
|
+
return `daily/${new Date().toISOString().slice(0, 10)}`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Get formatted memory context for injection into system prompt
|
|
240
|
+
*/
|
|
241
|
+
async getContextForPrompt(query = "", maxChars = 3000) {
|
|
242
|
+
try {
|
|
243
|
+
const results = [];
|
|
244
|
+
|
|
245
|
+
// Always include MEMORY.md if it exists
|
|
246
|
+
const mainMemory = await this.get("MEMORY");
|
|
247
|
+
if (mainMemory?.content?.trim()) {
|
|
248
|
+
results.push(`### MEMORY.md\n${mainMemory.content.trim()}`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Include user.md if it exists
|
|
252
|
+
const userMemory = await this.get("user");
|
|
253
|
+
if (userMemory?.content?.trim()) {
|
|
254
|
+
results.push(`### user.md\n${userMemory.content.trim()}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Search for relevant memories if query provided
|
|
258
|
+
if (query.trim()) {
|
|
259
|
+
const searchResults = await this.search(query, { limit: 5 });
|
|
260
|
+
for (const r of searchResults) {
|
|
261
|
+
if (r.key === "MEMORY" || r.key === "user") continue; // already included
|
|
262
|
+
const mem = await this.get(r.key);
|
|
263
|
+
if (mem?.content?.trim()) {
|
|
264
|
+
results.push(`### ${r.key}.md\n${mem.content.trim()}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const joined = results.join("\n\n");
|
|
270
|
+
return joined.slice(0, maxChars) || null;
|
|
271
|
+
} catch {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|