@syntesseraai/opencode-feature-factory 0.3.0 → 0.3.2

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.
Files changed (44) hide show
  1. package/README.md +27 -0
  2. package/agents/building.md +0 -1
  3. package/agents/ff-acceptance.md +0 -2
  4. package/agents/ff-research.md +0 -1
  5. package/agents/ff-review.md +0 -2
  6. package/agents/ff-security.md +0 -2
  7. package/agents/ff-validate.md +0 -2
  8. package/agents/ff-well-architected.md +0 -2
  9. package/agents/planning.md +0 -1
  10. package/agents/reviewing.md +0 -1
  11. package/bin/ff-deploy.js +11 -0
  12. package/bin/ff-local-recall-mcp.js +9 -0
  13. package/dist/index.js +16 -1
  14. package/dist/local-recall/daemon-controller.d.ts +51 -0
  15. package/dist/local-recall/daemon-controller.js +166 -0
  16. package/dist/local-recall/daemon.d.ts +2 -2
  17. package/dist/local-recall/daemon.js +7 -9
  18. package/dist/local-recall/index-state.d.ts +14 -0
  19. package/dist/local-recall/index-state.js +76 -0
  20. package/dist/local-recall/index.d.ts +8 -2
  21. package/dist/local-recall/index.js +9 -2
  22. package/dist/local-recall/mcp-server.d.ts +29 -33
  23. package/dist/local-recall/mcp-server.js +176 -53
  24. package/dist/local-recall/mcp-stdio-server.d.ts +4 -0
  25. package/dist/local-recall/mcp-stdio-server.js +225 -0
  26. package/dist/local-recall/mcp-tools.d.ts +24 -11
  27. package/dist/local-recall/mcp-tools.js +112 -87
  28. package/dist/local-recall/memory-service.d.ts +2 -1
  29. package/dist/local-recall/memory-service.js +3 -3
  30. package/dist/local-recall/processed-log.d.ts +1 -1
  31. package/dist/local-recall/processed-log.js +2 -2
  32. package/dist/local-recall/thinking-extractor.d.ts +2 -2
  33. package/dist/local-recall/thinking-extractor.js +4 -4
  34. package/dist/local-recall/types.d.ts +1 -1
  35. package/dist/local-recall/vector/embedding-provider.d.ts +37 -0
  36. package/dist/local-recall/vector/embedding-provider.js +184 -0
  37. package/dist/local-recall/vector/orama-index.d.ts +37 -0
  38. package/dist/local-recall/vector/orama-index.js +379 -0
  39. package/dist/local-recall/vector/types.d.ts +33 -0
  40. package/dist/local-recall/vector/types.js +1 -0
  41. package/dist/mcp-config.d.ts +58 -0
  42. package/dist/mcp-config.js +108 -0
  43. package/package.json +5 -2
  44. package/skills/ff-learning/SKILL.md +2 -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 `ff-memories/` (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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,17 @@ 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: [
35
+ 'npx',
36
+ '-y',
37
+ '--package=@syntesseraai/opencode-feature-factory@latest',
38
+ '--',
39
+ 'ff-local-recall-mcp',
40
+ ],
41
+ enabled: true,
42
+ },
32
43
  'jina-ai': {
33
44
  type: 'remote',
34
45
  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 global OpenCode config
42
+ // This ensures Feature Factory MCP servers are available across projects
43
+ try {
44
+ await updateMCPConfig($);
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
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * daemon.ts — Background extraction daemon for local-recall.
3
3
  *
4
- * Scans OpenCode session storage for unprocessed assistant messages,
4
+ * Scans OpenCode session storage for unprocessed assistant and thinking messages,
5
5
  * runs the extraction pipeline (session + thinking extractors),
6
6
  * and stores resulting memories with logical IDs and content-hash
7
7
  * idempotency.
@@ -26,7 +26,7 @@ export interface ExtractionStats {
26
26
  *
27
27
  * 1. Find the OpenCode project matching `directory`
28
28
  * 2. Load existing processed log for fast membership checks
29
- * 3. Iterate sessions → messages (assistant only)
29
+ * 3. Iterate sessions → messages (assistant + thinking)
30
30
  * 4. Skip by message-ID *and* content-hash (dual idempotency)
31
31
  * 5. Run session + thinking extractors
32
32
  * 6. Assign logical IDs (session-<sid>-<msgid>-N / thinking-<sid>-<msgid>-N)
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * daemon.ts — Background extraction daemon for local-recall.
3
3
  *
4
- * Scans OpenCode session storage for unprocessed assistant messages,
4
+ * Scans OpenCode session storage for unprocessed assistant and thinking messages,
5
5
  * runs the extraction pipeline (session + thinking extractors),
6
6
  * and stores resulting memories with logical IDs and content-hash
7
7
  * idempotency.
@@ -14,13 +14,12 @@
14
14
  * 1. Message-ID check (fast skip for already-processed messages)
15
15
  * 2. Content-hash check (skips duplicate content across edits/replays)
16
16
  */
17
- import * as path from 'node:path';
18
17
  import * as fs from 'node:fs/promises';
19
18
  import { findProject, listSessions, listMessages, listParts } from './storage-reader.js';
20
19
  import { extractFromMessage } from './session-extractor.js';
21
20
  import { extractThinkingFromMessage } from './thinking-extractor.js';
22
21
  import { readProcessedLog, getProcessedMessageIDs, getProcessedHashes, markProcessed, contentHash, } from './processed-log.js';
23
- import { storeMemories } from './memory-service.js';
22
+ import { getMemoriesDir, storeMemories } from './memory-service.js';
24
23
  // ────────────────────────────────────────────────────────────
25
24
  // Helpers
26
25
  // ────────────────────────────────────────────────────────────
@@ -33,7 +32,7 @@ async function buildMessageContentHash(messageID) {
33
32
  try {
34
33
  const parts = await listParts(messageID);
35
34
  const textParts = parts
36
- .filter((p) => p.type === 'text' || p.type === 'reasoning')
35
+ .filter((p) => p.type === 'text' || p.type === 'reasoning' || p.type === 'thinking')
37
36
  .map((p) => p.text ?? '')
38
37
  .join('\n');
39
38
  // Hash content only (no messageID) so dedupe works across ID changes
@@ -52,7 +51,7 @@ async function buildMessageContentHash(messageID) {
52
51
  *
53
52
  * 1. Find the OpenCode project matching `directory`
54
53
  * 2. Load existing processed log for fast membership checks
55
- * 3. Iterate sessions → messages (assistant only)
54
+ * 3. Iterate sessions → messages (assistant + thinking)
56
55
  * 4. Skip by message-ID *and* content-hash (dual idempotency)
57
56
  * 5. Run session + thinking extractors
58
57
  * 6. Assign logical IDs (session-<sid>-<msgid>-N / thinking-<sid>-<msgid>-N)
@@ -74,8 +73,7 @@ export async function runExtraction(directory) {
74
73
  return stats;
75
74
  }
76
75
  // Ensure local-recall directories exist
77
- const recallDir = path.join(directory, '.feature-factory', 'local-recall');
78
- await fs.mkdir(path.join(recallDir, 'memories'), { recursive: true });
76
+ await fs.mkdir(getMemoriesDir(directory), { recursive: true });
79
77
  // Pre-load processed log for fast lookups
80
78
  const existingLog = await readProcessedLog(directory);
81
79
  const processedMsgIDs = getProcessedMessageIDs(existingLog);
@@ -90,8 +88,8 @@ export async function runExtraction(directory) {
90
88
  const messages = await listMessages(session.id);
91
89
  for (const message of messages) {
92
90
  stats.messagesScanned++;
93
- // Only process assistant messages
94
- if (message.role !== 'assistant') {
91
+ // Only process assistant and thinking messages
92
+ if (message.role !== 'assistant' && message.role !== 'thinking') {
95
93
  stats.messagesSkipped++;
96
94
  continue;
97
95
  }
@@ -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, 'ff-memories', '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 { initLocalRecall, isInitialized, getDirectory, triggerExtraction, getLastExtractionStats, shutdownLocalRecall, } from './mcp-server.js';
14
- export { createLearningStoreTool, createLearningSearchTool, createLearningGetTool, } from './mcp-tools.js';
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';