@syntesseraai/opencode-feature-factory 0.3.0 → 0.3.1
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/README.md +27 -0
- package/agents/building.md +0 -1
- package/agents/ff-acceptance.md +0 -2
- package/agents/ff-research.md +0 -1
- package/agents/ff-review.md +0 -2
- package/agents/ff-security.md +0 -2
- package/agents/ff-validate.md +0 -2
- package/agents/ff-well-architected.md +0 -2
- package/agents/planning.md +0 -1
- package/agents/reviewing.md +0 -1
- package/bin/ff-deploy.js +5 -0
- package/bin/ff-local-recall-mcp.js +9 -0
- package/dist/index.js +16 -1
- package/dist/local-recall/daemon-controller.d.ts +51 -0
- package/dist/local-recall/daemon-controller.js +166 -0
- package/dist/local-recall/index-state.d.ts +14 -0
- package/dist/local-recall/index-state.js +76 -0
- package/dist/local-recall/index.d.ts +8 -2
- package/dist/local-recall/index.js +9 -2
- package/dist/local-recall/mcp-server.d.ts +29 -33
- package/dist/local-recall/mcp-server.js +172 -53
- package/dist/local-recall/mcp-stdio-server.d.ts +4 -0
- package/dist/local-recall/mcp-stdio-server.js +225 -0
- package/dist/local-recall/mcp-tools.d.ts +24 -11
- package/dist/local-recall/mcp-tools.js +112 -87
- package/dist/local-recall/vector/embedding-provider.d.ts +37 -0
- package/dist/local-recall/vector/embedding-provider.js +184 -0
- package/dist/local-recall/vector/orama-index.d.ts +37 -0
- package/dist/local-recall/vector/orama-index.js +379 -0
- package/dist/local-recall/vector/types.d.ts +33 -0
- package/dist/local-recall/vector/types.js +1 -0
- package/dist/mcp-config.d.ts +63 -0
- package/dist/mcp-config.js +121 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -29,6 +29,33 @@ This will:
|
|
|
29
29
|
- Copy skills to `~/.config/opencode/skills/`
|
|
30
30
|
- Configure MCP servers in `~/.config/opencode/opencode.json`
|
|
31
31
|
|
|
32
|
+
## Local Recall MCP Daemon
|
|
33
|
+
|
|
34
|
+
The plugin now includes a local MCP daemon binary: `ff-local-recall-mcp`.
|
|
35
|
+
|
|
36
|
+
- `ff-deploy` adds a local MCP server entry named `ff-local-recall`
|
|
37
|
+
- The server exposes memory tools:
|
|
38
|
+
- `local_recall.search`
|
|
39
|
+
- `local_recall.get`
|
|
40
|
+
- `local_recall.store`
|
|
41
|
+
- `local_recall.index.start`
|
|
42
|
+
- `local_recall.index.status`
|
|
43
|
+
- `local_recall.index.stop`
|
|
44
|
+
- `local_recall.index.rebuild`
|
|
45
|
+
|
|
46
|
+
### Environment Variables
|
|
47
|
+
|
|
48
|
+
- `FF_LOCAL_RECALL_DIRECTORY` - Directory that contains `.feature-factory/` (default: current working directory)
|
|
49
|
+
- `FF_LOCAL_RECALL_DAEMON_AUTOSTART` - Start index daemon automatically (`true` by default)
|
|
50
|
+
- `FF_LOCAL_RECALL_INDEX_INTERVAL_MS` - Background daemon interval in milliseconds (default: `15000`)
|
|
51
|
+
- `FF_LOCAL_RECALL_EXTRACTION_ENABLED` - Run extraction during daemon cycles (`true` by default)
|
|
52
|
+
- `FF_LOCAL_RECALL_EMBEDDING_PROVIDER` - Embedding provider (`ollama` default, `openai` optional)
|
|
53
|
+
- `FF_LOCAL_RECALL_OLLAMA_URL` - Ollama base URL (default: `http://127.0.0.1:11434`)
|
|
54
|
+
- `FF_LOCAL_RECALL_OLLAMA_MODEL` - Ollama embedding model (default: `nomic-embed-text`)
|
|
55
|
+
- `FF_LOCAL_RECALL_OPENAI_URL` - OpenAI embeddings endpoint base (default: `https://api.openai.com/v1`)
|
|
56
|
+
- `FF_LOCAL_RECALL_OPENAI_MODEL` - OpenAI embedding model (default: `text-embedding-3-small`)
|
|
57
|
+
- `OPENAI_API_KEY` - Required when `FF_LOCAL_RECALL_EMBEDDING_PROVIDER=openai`
|
|
58
|
+
|
|
32
59
|
## Agents Provided
|
|
33
60
|
|
|
34
61
|
### Primary Agents
|
package/agents/building.md
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Implements features and makes code changes based on implementation plans. Use this agent to execute plans, write code, and build features. Prefer delegation for validation, testing, and documentation.
|
|
3
3
|
mode: primary
|
|
4
|
-
temperature: 0.2
|
|
5
4
|
color: '#10b981'
|
|
6
5
|
tools:
|
|
7
6
|
read: true
|
package/agents/ff-acceptance.md
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Validates implementation against acceptance criteria. Use this to verify that code meets all stated requirements and acceptance criteria with strict binary pass/fail validation. This agent cannot invoke sub-agents - it performs validation directly.
|
|
3
3
|
mode: subagent
|
|
4
|
-
model: anthropic/claude-sonnet-4-20250514
|
|
5
|
-
temperature: 0.1
|
|
6
4
|
tools:
|
|
7
5
|
read: true
|
|
8
6
|
write: false
|
package/agents/ff-research.md
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Research specialist that investigates external topics using MCP tools. Use this agent to research libraries, APIs, best practices, and implementation patterns. This agent cannot invoke sub-agents - it performs research directly.
|
|
3
3
|
mode: subagent
|
|
4
|
-
temperature: 0.2
|
|
5
4
|
tools:
|
|
6
5
|
read: true
|
|
7
6
|
write: false
|
package/agents/ff-review.md
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Reviews code changes for correctness, quality, and test coverage. Use this for detailed code review focusing on correctness, quality, testing, and documentation. This agent cannot invoke sub-agents - it performs review directly.
|
|
3
3
|
mode: subagent
|
|
4
|
-
model: anthropic/claude-sonnet-4-20250514
|
|
5
|
-
temperature: 0.1
|
|
6
4
|
tools:
|
|
7
5
|
read: true
|
|
8
6
|
write: false
|
package/agents/ff-security.md
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Performs deep security audits on code changes. Use this to identify security vulnerabilities, check authentication/authorization, and ensure security best practices. This agent cannot invoke sub-agents - it performs audit directly.
|
|
3
3
|
mode: subagent
|
|
4
|
-
model: anthropic/claude-sonnet-4-20250514
|
|
5
|
-
temperature: 0.1
|
|
6
4
|
tools:
|
|
7
5
|
read: true
|
|
8
6
|
write: false
|
package/agents/ff-validate.md
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Performs comprehensive validation covering acceptance criteria, security, code quality, and architecture. Use this for complete validation across all dimensions. This agent cannot invoke sub-agents - it performs all validation directly.
|
|
3
3
|
mode: subagent
|
|
4
|
-
model: anthropic/claude-sonnet-4-20250514
|
|
5
|
-
temperature: 0.1
|
|
6
4
|
tools:
|
|
7
5
|
read: true
|
|
8
6
|
write: false
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Reviews code against AWS Well-Architected Framework pillars. Use this for architecture reviews covering Operational Excellence, Security, Reliability, Performance, Cost, and Sustainability. This agent cannot invoke sub-agents - it performs review directly.
|
|
3
3
|
mode: subagent
|
|
4
|
-
model: anthropic/claude-sonnet-4-20250514
|
|
5
|
-
temperature: 0.1
|
|
6
4
|
tools:
|
|
7
5
|
read: true
|
|
8
6
|
write: false
|
package/agents/planning.md
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Creates comprehensive implementation plans before making any code changes. Use this agent to analyze requirements, break down tasks, and create detailed implementation plans. Can delegate to read-only sub-agents for parallel research and validation.
|
|
3
3
|
mode: primary
|
|
4
|
-
temperature: 0.1
|
|
5
4
|
color: '#3b82f6'
|
|
6
5
|
tools:
|
|
7
6
|
read: true
|
package/agents/reviewing.md
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Comprehensive validation agent that reviews implementation quality and feeds results back to the building agent. Use this to validate code changes across all dimensions. Can delegate to read-only sub-agents for parallel validation.
|
|
3
3
|
mode: primary
|
|
4
|
-
temperature: 0.1
|
|
5
4
|
color: '#f59e0b'
|
|
6
5
|
tools:
|
|
7
6
|
read: true
|
package/bin/ff-deploy.js
CHANGED
|
@@ -29,6 +29,11 @@ const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
|
29
29
|
|
|
30
30
|
// Default MCP configuration
|
|
31
31
|
const DEFAULT_MCP_SERVERS = {
|
|
32
|
+
'ff-local-recall': {
|
|
33
|
+
type: 'local',
|
|
34
|
+
command: 'ff-local-recall-mcp',
|
|
35
|
+
enabled: true,
|
|
36
|
+
},
|
|
32
37
|
'jina-ai': {
|
|
33
38
|
type: 'remote',
|
|
34
39
|
url: 'https://mcp.jina.ai/v1',
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { runLocalRecallMCPServer } from '../dist/local-recall/mcp-stdio-server.js';
|
|
4
|
+
|
|
5
|
+
runLocalRecallMCPServer().catch((error) => {
|
|
6
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7
|
+
console.error(`Failed to start ff-local-recall MCP server: ${message}`);
|
|
8
|
+
process.exit(1);
|
|
9
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { StopQualityGateHooksPlugin } from './stop-quality-gate.js';
|
|
2
|
+
import { updateMCPConfig } from './mcp-config.js';
|
|
3
|
+
import { $ } from 'bun';
|
|
2
4
|
// Import tool creator functions
|
|
3
5
|
import { createFFAgentsCurrentTool } from './plugins/ff-agents-current-plugin.js';
|
|
4
6
|
import { createFFAgentsShowTool } from './plugins/ff-agents-show-plugin.js';
|
|
5
7
|
import { createFFAgentsClearTool } from './plugins/ff-agents-clear-plugin.js';
|
|
6
|
-
import { createLearningStoreTool, createLearningSearchTool, createLearningGetTool, initLocalRecall, } from './local-recall/index.js';
|
|
8
|
+
import { createLearningStoreTool, createLearningSearchTool, createLearningGetTool, createLearningIndexStartTool, createLearningIndexStatusTool, createLearningIndexStopTool, createLearningIndexRebuildTool, initLocalRecall, } from './local-recall/index.js';
|
|
7
9
|
import { createFFPlanCreateTool } from './plugins/ff-plan-create-plugin.js';
|
|
8
10
|
import { createFFPlanUpdateTool } from './plugins/ff-plan-update-plugin.js';
|
|
9
11
|
import { createFFAgentContextCreateTool } from './plugins/ff-agent-context-create-plugin.js';
|
|
@@ -36,6 +38,15 @@ export const FeatureFactoryPlugin = async (input) => {
|
|
|
36
38
|
}
|
|
37
39
|
// Initialize local-recall memory system
|
|
38
40
|
initLocalRecall(directory);
|
|
41
|
+
// Update MCP server configuration in project opencode.json files
|
|
42
|
+
// This ensures Feature Factory MCP servers are available in the project
|
|
43
|
+
try {
|
|
44
|
+
await updateMCPConfig($, directory);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
// Silently fail - don't block plugin initialization if MCP config update fails
|
|
48
|
+
// This is a convenience feature, not critical for plugin functionality
|
|
49
|
+
}
|
|
39
50
|
// Load hooks from the quality gate plugin
|
|
40
51
|
const qualityGateHooks = await StopQualityGateHooksPlugin(input).catch(() => ({}));
|
|
41
52
|
// Create all tools
|
|
@@ -48,6 +59,10 @@ export const FeatureFactoryPlugin = async (input) => {
|
|
|
48
59
|
'ff-learning-store': createLearningStoreTool(),
|
|
49
60
|
'ff-learning-search': createLearningSearchTool(),
|
|
50
61
|
'ff-learning-get': createLearningGetTool(),
|
|
62
|
+
'ff-learning-index-start': createLearningIndexStartTool(),
|
|
63
|
+
'ff-learning-index-status': createLearningIndexStatusTool(),
|
|
64
|
+
'ff-learning-index-stop': createLearningIndexStopTool(),
|
|
65
|
+
'ff-learning-index-rebuild': createLearningIndexRebuildTool(),
|
|
51
66
|
// Plan tools
|
|
52
67
|
'ff-plan-create': createFFPlanCreateTool(),
|
|
53
68
|
'ff-plan-update': createFFPlanUpdateTool(),
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type ExtractionStats } from './daemon.js';
|
|
2
|
+
import { OramaMemoryIndex } from './vector/orama-index.js';
|
|
3
|
+
export interface DaemonRunReport {
|
|
4
|
+
fullRebuild: boolean;
|
|
5
|
+
extraction: ExtractionStats | null;
|
|
6
|
+
upserted: number;
|
|
7
|
+
removed: number;
|
|
8
|
+
documents: number;
|
|
9
|
+
durationMs: number;
|
|
10
|
+
completedAt: string;
|
|
11
|
+
}
|
|
12
|
+
export interface DaemonStatus {
|
|
13
|
+
running: boolean;
|
|
14
|
+
processing: boolean;
|
|
15
|
+
intervalMs: number;
|
|
16
|
+
pending: boolean;
|
|
17
|
+
pendingReason: string | null;
|
|
18
|
+
lastRun: DaemonRunReport | null;
|
|
19
|
+
lastError: string | null;
|
|
20
|
+
}
|
|
21
|
+
interface ControllerOptions {
|
|
22
|
+
directory: string;
|
|
23
|
+
index: OramaMemoryIndex;
|
|
24
|
+
intervalMs?: number;
|
|
25
|
+
extractionEnabled?: boolean;
|
|
26
|
+
}
|
|
27
|
+
export declare class LocalRecallDaemonController {
|
|
28
|
+
private readonly directory;
|
|
29
|
+
private readonly index;
|
|
30
|
+
private readonly extractionEnabled;
|
|
31
|
+
private intervalMs;
|
|
32
|
+
private timer;
|
|
33
|
+
private running;
|
|
34
|
+
private processing;
|
|
35
|
+
private pending;
|
|
36
|
+
private pendingFullRebuild;
|
|
37
|
+
private pendingReason;
|
|
38
|
+
private lastRun;
|
|
39
|
+
private lastError;
|
|
40
|
+
constructor(options: ControllerOptions);
|
|
41
|
+
start(intervalMs?: number): DaemonStatus;
|
|
42
|
+
stop(): DaemonStatus;
|
|
43
|
+
requestRun(reason?: string, fullRebuild?: boolean): void;
|
|
44
|
+
runNow(reason?: string, fullRebuild?: boolean): Promise<DaemonStatus>;
|
|
45
|
+
rebuild(): Promise<DaemonStatus>;
|
|
46
|
+
getStatus(): DaemonStatus;
|
|
47
|
+
private runLoop;
|
|
48
|
+
private runCycle;
|
|
49
|
+
private waitForIdle;
|
|
50
|
+
}
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { runExtraction } from './daemon.js';
|
|
2
|
+
import { computeMemoryHash, readIndexState, writeIndexState } from './index-state.js';
|
|
3
|
+
import { listAllMemories } from './memory-service.js';
|
|
4
|
+
const DEFAULT_INTERVAL_MS = 15_000;
|
|
5
|
+
export class LocalRecallDaemonController {
|
|
6
|
+
directory;
|
|
7
|
+
index;
|
|
8
|
+
extractionEnabled;
|
|
9
|
+
intervalMs;
|
|
10
|
+
timer = null;
|
|
11
|
+
running = false;
|
|
12
|
+
processing = false;
|
|
13
|
+
pending = false;
|
|
14
|
+
pendingFullRebuild = false;
|
|
15
|
+
pendingReason = null;
|
|
16
|
+
lastRun = null;
|
|
17
|
+
lastError = null;
|
|
18
|
+
constructor(options) {
|
|
19
|
+
this.directory = options.directory;
|
|
20
|
+
this.index = options.index;
|
|
21
|
+
this.intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
22
|
+
this.extractionEnabled = options.extractionEnabled ?? true;
|
|
23
|
+
}
|
|
24
|
+
start(intervalMs) {
|
|
25
|
+
if (intervalMs && intervalMs > 0) {
|
|
26
|
+
this.intervalMs = intervalMs;
|
|
27
|
+
}
|
|
28
|
+
if (this.running) {
|
|
29
|
+
return this.getStatus();
|
|
30
|
+
}
|
|
31
|
+
this.running = true;
|
|
32
|
+
this.timer = setInterval(() => {
|
|
33
|
+
this.requestRun('interval');
|
|
34
|
+
}, this.intervalMs);
|
|
35
|
+
this.requestRun('startup');
|
|
36
|
+
return this.getStatus();
|
|
37
|
+
}
|
|
38
|
+
stop() {
|
|
39
|
+
this.running = false;
|
|
40
|
+
if (this.timer) {
|
|
41
|
+
clearInterval(this.timer);
|
|
42
|
+
this.timer = null;
|
|
43
|
+
}
|
|
44
|
+
return this.getStatus();
|
|
45
|
+
}
|
|
46
|
+
requestRun(reason = 'manual', fullRebuild = false) {
|
|
47
|
+
this.pending = true;
|
|
48
|
+
this.pendingReason = reason;
|
|
49
|
+
if (fullRebuild) {
|
|
50
|
+
this.pendingFullRebuild = true;
|
|
51
|
+
}
|
|
52
|
+
void this.runLoop();
|
|
53
|
+
}
|
|
54
|
+
async runNow(reason = 'manual', fullRebuild = false) {
|
|
55
|
+
this.requestRun(reason, fullRebuild);
|
|
56
|
+
await this.waitForIdle();
|
|
57
|
+
return this.getStatus();
|
|
58
|
+
}
|
|
59
|
+
async rebuild() {
|
|
60
|
+
return this.runNow('rebuild', true);
|
|
61
|
+
}
|
|
62
|
+
getStatus() {
|
|
63
|
+
return {
|
|
64
|
+
running: this.running,
|
|
65
|
+
processing: this.processing,
|
|
66
|
+
intervalMs: this.intervalMs,
|
|
67
|
+
pending: this.pending,
|
|
68
|
+
pendingReason: this.pendingReason,
|
|
69
|
+
lastRun: this.lastRun,
|
|
70
|
+
lastError: this.lastError,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async runLoop() {
|
|
74
|
+
if (this.processing) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
this.processing = true;
|
|
78
|
+
try {
|
|
79
|
+
while (this.pending) {
|
|
80
|
+
const fullRebuild = this.pendingFullRebuild;
|
|
81
|
+
this.pending = false;
|
|
82
|
+
this.pendingFullRebuild = false;
|
|
83
|
+
this.pendingReason = null;
|
|
84
|
+
await this.runCycle(fullRebuild);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
this.processing = false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async runCycle(fullRebuild) {
|
|
92
|
+
const startedAt = Date.now();
|
|
93
|
+
this.lastError = null;
|
|
94
|
+
try {
|
|
95
|
+
const extraction = this.extractionEnabled ? await runExtraction(this.directory) : null;
|
|
96
|
+
const allMemories = await listAllMemories(this.directory);
|
|
97
|
+
const now = Date.now();
|
|
98
|
+
let upserted = 0;
|
|
99
|
+
let removed = 0;
|
|
100
|
+
if (fullRebuild) {
|
|
101
|
+
upserted = await this.index.rebuild(allMemories);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const previousState = await readIndexState(this.directory);
|
|
105
|
+
const nextEntries = {};
|
|
106
|
+
const changed = [];
|
|
107
|
+
for (const memory of allMemories) {
|
|
108
|
+
const hash = computeMemoryHash(memory);
|
|
109
|
+
nextEntries[memory.id] = {
|
|
110
|
+
hash,
|
|
111
|
+
updatedAt: now,
|
|
112
|
+
};
|
|
113
|
+
const previous = previousState.entries[memory.id];
|
|
114
|
+
if (!previous || previous.hash !== hash) {
|
|
115
|
+
changed.push(memory);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const removedIDs = Object.keys(previousState.entries).filter((id) => !nextEntries[id]);
|
|
119
|
+
if (changed.length > 0) {
|
|
120
|
+
upserted = await this.index.upsertMemories(changed);
|
|
121
|
+
}
|
|
122
|
+
if (removedIDs.length > 0) {
|
|
123
|
+
removed = await this.index.removeMemories(removedIDs);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
await writeIndexState(this.directory, {
|
|
127
|
+
version: 1,
|
|
128
|
+
entries: Object.fromEntries(allMemories.map((memory) => [
|
|
129
|
+
memory.id,
|
|
130
|
+
{
|
|
131
|
+
hash: computeMemoryHash(memory),
|
|
132
|
+
updatedAt: now,
|
|
133
|
+
},
|
|
134
|
+
])),
|
|
135
|
+
updatedAt: now,
|
|
136
|
+
});
|
|
137
|
+
const documents = this.index.getStatus().documents;
|
|
138
|
+
this.lastRun = {
|
|
139
|
+
fullRebuild,
|
|
140
|
+
extraction,
|
|
141
|
+
upserted,
|
|
142
|
+
removed,
|
|
143
|
+
documents,
|
|
144
|
+
durationMs: Date.now() - startedAt,
|
|
145
|
+
completedAt: new Date().toISOString(),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
this.lastError = error instanceof Error ? error.message : String(error);
|
|
150
|
+
this.lastRun = {
|
|
151
|
+
fullRebuild,
|
|
152
|
+
extraction: null,
|
|
153
|
+
upserted: 0,
|
|
154
|
+
removed: 0,
|
|
155
|
+
documents: this.index.getStatus().documents,
|
|
156
|
+
durationMs: Date.now() - startedAt,
|
|
157
|
+
completedAt: new Date().toISOString(),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async waitForIdle() {
|
|
162
|
+
while (this.processing || this.pending) {
|
|
163
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Memory } from './types.js';
|
|
2
|
+
export interface IndexStateEntry {
|
|
3
|
+
hash: string;
|
|
4
|
+
updatedAt: number;
|
|
5
|
+
}
|
|
6
|
+
export interface IndexState {
|
|
7
|
+
version: 1;
|
|
8
|
+
entries: Record<string, IndexStateEntry>;
|
|
9
|
+
updatedAt: number;
|
|
10
|
+
}
|
|
11
|
+
export declare function getIndexStatePath(directory: string): string;
|
|
12
|
+
export declare function readIndexState(directory: string): Promise<IndexState>;
|
|
13
|
+
export declare function writeIndexState(directory: string, state: IndexState): Promise<void>;
|
|
14
|
+
export declare function computeMemoryHash(memory: Memory): string;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
const EMPTY_STATE = {
|
|
5
|
+
version: 1,
|
|
6
|
+
entries: {},
|
|
7
|
+
updatedAt: 0,
|
|
8
|
+
};
|
|
9
|
+
function getIndexDir(directory) {
|
|
10
|
+
return path.join(directory, '.feature-factory', 'local-recall', 'index');
|
|
11
|
+
}
|
|
12
|
+
export function getIndexStatePath(directory) {
|
|
13
|
+
return path.join(getIndexDir(directory), 'state.json');
|
|
14
|
+
}
|
|
15
|
+
function normalizeState(raw) {
|
|
16
|
+
if (!raw || typeof raw !== 'object') {
|
|
17
|
+
return { ...EMPTY_STATE };
|
|
18
|
+
}
|
|
19
|
+
const value = raw;
|
|
20
|
+
const entries = {};
|
|
21
|
+
if (value.entries && typeof value.entries === 'object') {
|
|
22
|
+
for (const [id, entry] of Object.entries(value.entries)) {
|
|
23
|
+
if (!entry || typeof entry !== 'object') {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const candidate = entry;
|
|
27
|
+
if (typeof candidate.hash !== 'string') {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
entries[id] = {
|
|
31
|
+
hash: candidate.hash,
|
|
32
|
+
updatedAt: typeof candidate.updatedAt === 'number' ? candidate.updatedAt : 0,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
version: value.version === 1 ? 1 : 1,
|
|
38
|
+
entries,
|
|
39
|
+
updatedAt: typeof value.updatedAt === 'number' ? value.updatedAt : 0,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
async function writeAtomic(filePath, value) {
|
|
43
|
+
const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
44
|
+
await writeFile(tmpPath, JSON.stringify(value, null, 2), 'utf-8');
|
|
45
|
+
await rename(tmpPath, filePath);
|
|
46
|
+
}
|
|
47
|
+
export async function readIndexState(directory) {
|
|
48
|
+
const filePath = getIndexStatePath(directory);
|
|
49
|
+
try {
|
|
50
|
+
const raw = await readFile(filePath, 'utf-8');
|
|
51
|
+
return normalizeState(JSON.parse(raw));
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return { ...EMPTY_STATE };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export async function writeIndexState(directory, state) {
|
|
58
|
+
const filePath = getIndexStatePath(directory);
|
|
59
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
60
|
+
await writeAtomic(filePath, state);
|
|
61
|
+
}
|
|
62
|
+
export function computeMemoryHash(memory) {
|
|
63
|
+
const stable = JSON.stringify({
|
|
64
|
+
id: memory.id,
|
|
65
|
+
sessionID: memory.sessionID,
|
|
66
|
+
messageID: memory.messageID,
|
|
67
|
+
category: memory.category,
|
|
68
|
+
title: memory.title,
|
|
69
|
+
body: memory.body,
|
|
70
|
+
tags: [...memory.tags].sort(),
|
|
71
|
+
importance: memory.importance,
|
|
72
|
+
createdAt: memory.createdAt,
|
|
73
|
+
extractedBy: memory.extractedBy,
|
|
74
|
+
});
|
|
75
|
+
return createHash('sha256').update(stable).digest('hex');
|
|
76
|
+
}
|
|
@@ -10,5 +10,11 @@ export { extractFromMessage, extractFromParts } from './session-extractor.js';
|
|
|
10
10
|
export { extractThinkingFromMessage, extractFromThinkingParts } from './thinking-extractor.js';
|
|
11
11
|
export { isProcessed, isContentProcessed, markProcessed, readProcessedLog, contentHash, getProcessedMessageIDs, getProcessedHashes, } from './processed-log.js';
|
|
12
12
|
export { runExtraction, type ExtractionStats } from './daemon.js';
|
|
13
|
-
export {
|
|
14
|
-
export {
|
|
13
|
+
export { LocalRecallDaemonController, type DaemonStatus, type DaemonRunReport, } from './daemon-controller.js';
|
|
14
|
+
export { readIndexState, writeIndexState, computeMemoryHash, getIndexStatePath, type IndexState, type IndexStateEntry, } from './index-state.js';
|
|
15
|
+
export { createEmbeddingProvider, OllamaEmbeddingProvider, OpenAIEmbeddingProvider, } from './vector/embedding-provider.js';
|
|
16
|
+
export { OramaMemoryIndex } from './vector/orama-index.js';
|
|
17
|
+
export type { EmbeddingProvider, EmbeddingProviderName, VectorIndexManifest, VectorIndexDocument, VectorSearchResponse, } from './vector/types.js';
|
|
18
|
+
export { initLocalRecall, isInitialized, getDirectory, triggerExtraction, getLastExtractionStats, shutdownLocalRecall, storeLearningMemory, searchLearningMemories, getLearningMemory, startIndexingDaemon, stopIndexingDaemon, getIndexingStatus, rebuildIndex, listLearningMemories, type LearningStoreInput, type IndexingStatus, } from './mcp-server.js';
|
|
19
|
+
export { runLocalRecallMCPServer, type LocalRecallMCPServerOptions } from './mcp-stdio-server.js';
|
|
20
|
+
export { createLearningStoreTool, createLearningSearchTool, createLearningGetTool, createLearningIndexStartTool, createLearningIndexStatusTool, createLearningIndexStopTool, createLearningIndexRebuildTool, } from './mcp-tools.js';
|
|
@@ -14,7 +14,14 @@ export { extractThinkingFromMessage, extractFromThinkingParts } from './thinking
|
|
|
14
14
|
export { isProcessed, isContentProcessed, markProcessed, readProcessedLog, contentHash, getProcessedMessageIDs, getProcessedHashes, } from './processed-log.js';
|
|
15
15
|
// Daemon
|
|
16
16
|
export { runExtraction } from './daemon.js';
|
|
17
|
+
export { LocalRecallDaemonController, } from './daemon-controller.js';
|
|
18
|
+
// Index state
|
|
19
|
+
export { readIndexState, writeIndexState, computeMemoryHash, getIndexStatePath, } from './index-state.js';
|
|
20
|
+
// Vector index
|
|
21
|
+
export { createEmbeddingProvider, OllamaEmbeddingProvider, OpenAIEmbeddingProvider, } from './vector/embedding-provider.js';
|
|
22
|
+
export { OramaMemoryIndex } from './vector/orama-index.js';
|
|
17
23
|
// MCP server lifecycle
|
|
18
|
-
export { initLocalRecall, isInitialized, getDirectory, triggerExtraction, getLastExtractionStats, shutdownLocalRecall, } from './mcp-server.js';
|
|
24
|
+
export { initLocalRecall, isInitialized, getDirectory, triggerExtraction, getLastExtractionStats, shutdownLocalRecall, storeLearningMemory, searchLearningMemories, getLearningMemory, startIndexingDaemon, stopIndexingDaemon, getIndexingStatus, rebuildIndex, listLearningMemories, } from './mcp-server.js';
|
|
25
|
+
export { runLocalRecallMCPServer } from './mcp-stdio-server.js';
|
|
19
26
|
// MCP tools (plugin tool creators)
|
|
20
|
-
export { createLearningStoreTool, createLearningSearchTool, createLearningGetTool, } from './mcp-tools.js';
|
|
27
|
+
export { createLearningStoreTool, createLearningSearchTool, createLearningGetTool, createLearningIndexStartTool, createLearningIndexStatusTool, createLearningIndexStopTool, createLearningIndexRebuildTool, } from './mcp-tools.js';
|
|
@@ -1,38 +1,34 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
import type { ExtractionStats } from './daemon.js';
|
|
2
|
+
import { type DaemonStatus } from './daemon-controller.js';
|
|
3
|
+
import type { Memory, MemoryCategory, MemorySearchResult, SearchCriteria } from './types.js';
|
|
4
|
+
import { OramaMemoryIndex } from './vector/orama-index.js';
|
|
5
|
+
import type { VectorSearchResponse } from './vector/types.js';
|
|
6
|
+
export interface LearningStoreInput {
|
|
7
|
+
title: string;
|
|
8
|
+
description: string;
|
|
9
|
+
category: MemoryCategory;
|
|
10
|
+
tags: string[];
|
|
11
|
+
importance?: number;
|
|
12
|
+
content?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface IndexingStatus {
|
|
15
|
+
daemon: DaemonStatus;
|
|
16
|
+
index: ReturnType<OramaMemoryIndex['getStatus']>;
|
|
17
|
+
}
|
|
13
18
|
export declare function initLocalRecall(directory: string): void;
|
|
14
|
-
/**
|
|
15
|
-
* Check if local-recall has been initialized.
|
|
16
|
-
*/
|
|
17
19
|
export declare function isInitialized(): boolean;
|
|
18
|
-
/**
|
|
19
|
-
* Get the project directory local-recall is bound to.
|
|
20
|
-
*/
|
|
21
20
|
export declare function getDirectory(): string | null;
|
|
22
|
-
|
|
23
|
-
* Trigger an extraction pass. Can be called on-demand (e.g. from
|
|
24
|
-
* the ff-learning-store tool) or at startup.
|
|
25
|
-
*
|
|
26
|
-
* Safe to call multiple times — the processed-log prevents
|
|
27
|
-
* duplicate extraction.
|
|
28
|
-
*/
|
|
29
|
-
export declare function triggerExtraction(): Promise<ExtractionStats | null>;
|
|
30
|
-
/**
|
|
31
|
-
* Get the results of the last extraction pass.
|
|
32
|
-
*/
|
|
21
|
+
export declare function triggerExtraction(directory?: string): Promise<ExtractionStats | null>;
|
|
33
22
|
export declare function getLastExtractionStats(): ExtractionStats | null;
|
|
34
|
-
/**
|
|
35
|
-
* Shutdown the local-recall system. Currently a no-op but
|
|
36
|
-
* provided for lifecycle symmetry and future cleanup needs.
|
|
37
|
-
*/
|
|
38
23
|
export declare function shutdownLocalRecall(): void;
|
|
24
|
+
export declare function storeLearningMemory(directory: string, args: LearningStoreInput): Promise<{
|
|
25
|
+
memoryId: string;
|
|
26
|
+
daemon: DaemonStatus;
|
|
27
|
+
}>;
|
|
28
|
+
export declare function searchLearningMemories(directory: string, criteria: SearchCriteria): Promise<VectorSearchResponse>;
|
|
29
|
+
export declare function getLearningMemory(directory: string, memoryID: string): Promise<Memory | null>;
|
|
30
|
+
export declare function startIndexingDaemon(directory: string, intervalMs?: number): Promise<IndexingStatus>;
|
|
31
|
+
export declare function stopIndexingDaemon(directory: string): Promise<IndexingStatus>;
|
|
32
|
+
export declare function getIndexingStatus(directory: string): Promise<IndexingStatus>;
|
|
33
|
+
export declare function rebuildIndex(directory: string): Promise<IndexingStatus>;
|
|
34
|
+
export declare function listLearningMemories(directory: string, criteria: SearchCriteria): Promise<MemorySearchResult[]>;
|