@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.
- 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 +11 -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/daemon.d.ts +2 -2
- package/dist/local-recall/daemon.js +7 -9
- 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 +176 -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/memory-service.d.ts +2 -1
- package/dist/local-recall/memory-service.js +3 -3
- package/dist/local-recall/processed-log.d.ts +1 -1
- package/dist/local-recall/processed-log.js +2 -2
- package/dist/local-recall/thinking-extractor.d.ts +2 -2
- package/dist/local-recall/thinking-extractor.js +4 -4
- package/dist/local-recall/types.d.ts +1 -1
- 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 +58 -0
- package/dist/mcp-config.js +108 -0
- package/package.json +5 -2
- package/skills/ff-learning/SKILL.md +2 -2
|
@@ -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[]>;
|
|
@@ -1,71 +1,194 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* the extraction trigger for on-demand use.
|
|
7
|
-
*/
|
|
8
|
-
import { runExtraction } from './daemon.js';
|
|
9
|
-
// ────────────────────────────────────────────────────────────
|
|
10
|
-
// State
|
|
11
|
-
// ────────────────────────────────────────────────────────────
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { LocalRecallDaemonController } from './daemon-controller.js';
|
|
3
|
+
import { getMemory, searchMemories, storeMemory } from './memory-service.js';
|
|
4
|
+
import { createEmbeddingProvider } from './vector/embedding-provider.js';
|
|
5
|
+
import { OramaMemoryIndex } from './vector/orama-index.js';
|
|
12
6
|
let _initialized = false;
|
|
13
7
|
let _directory = null;
|
|
14
|
-
let
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
8
|
+
let _runtime = null;
|
|
9
|
+
let _initPromise = null;
|
|
10
|
+
function parseBoolean(value, fallback) {
|
|
11
|
+
if (value === undefined) {
|
|
12
|
+
return fallback;
|
|
13
|
+
}
|
|
14
|
+
const normalized = value.trim().toLowerCase();
|
|
15
|
+
return !['0', 'false', 'off', 'no'].includes(normalized);
|
|
16
|
+
}
|
|
17
|
+
function parseInterval(value, fallback) {
|
|
18
|
+
if (!value) {
|
|
19
|
+
return fallback;
|
|
20
|
+
}
|
|
21
|
+
const parsed = Number(value);
|
|
22
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
23
|
+
return fallback;
|
|
24
|
+
}
|
|
25
|
+
return Math.round(parsed);
|
|
26
|
+
}
|
|
27
|
+
function createRuntime(directory) {
|
|
28
|
+
const provider = (() => {
|
|
29
|
+
try {
|
|
30
|
+
return createEmbeddingProvider();
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return createEmbeddingProvider({
|
|
34
|
+
...process.env,
|
|
35
|
+
FF_LOCAL_RECALL_EMBEDDING_PROVIDER: 'ollama',
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
})();
|
|
39
|
+
const index = new OramaMemoryIndex(directory, provider);
|
|
40
|
+
const daemon = new LocalRecallDaemonController({
|
|
41
|
+
directory,
|
|
42
|
+
index,
|
|
43
|
+
intervalMs: parseInterval(process.env.FF_LOCAL_RECALL_INDEX_INTERVAL_MS, 15_000),
|
|
44
|
+
extractionEnabled: parseBoolean(process.env.FF_LOCAL_RECALL_EXTRACTION_ENABLED, true),
|
|
45
|
+
});
|
|
46
|
+
return index.initialize().then(() => {
|
|
47
|
+
const autoStart = parseBoolean(process.env.FF_LOCAL_RECALL_DAEMON_AUTOSTART, true);
|
|
48
|
+
if (autoStart) {
|
|
49
|
+
daemon.start();
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
daemon.requestRun('startup');
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
directory,
|
|
56
|
+
index,
|
|
57
|
+
daemon,
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
async function ensureRuntime(directory) {
|
|
62
|
+
if (directory && directory !== _directory) {
|
|
63
|
+
initLocalRecall(directory);
|
|
64
|
+
}
|
|
65
|
+
if (!_initialized || !_directory) {
|
|
66
|
+
throw new Error('local-recall is not initialized');
|
|
67
|
+
}
|
|
68
|
+
if (!_initPromise) {
|
|
69
|
+
_initPromise = createRuntime(_directory).then((runtime) => {
|
|
70
|
+
_runtime = runtime;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
await _initPromise;
|
|
74
|
+
if (!_runtime) {
|
|
75
|
+
throw new Error('local-recall runtime failed to initialize');
|
|
76
|
+
}
|
|
77
|
+
return _runtime;
|
|
78
|
+
}
|
|
22
79
|
export function initLocalRecall(directory) {
|
|
23
|
-
_directory
|
|
80
|
+
if (_directory === directory && _initialized) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (_runtime) {
|
|
84
|
+
_runtime.daemon.stop();
|
|
85
|
+
}
|
|
24
86
|
_initialized = true;
|
|
87
|
+
_directory = directory;
|
|
88
|
+
_runtime = null;
|
|
89
|
+
_initPromise = createRuntime(directory).then((runtime) => {
|
|
90
|
+
_runtime = runtime;
|
|
91
|
+
});
|
|
25
92
|
}
|
|
26
|
-
/**
|
|
27
|
-
* Check if local-recall has been initialized.
|
|
28
|
-
*/
|
|
29
93
|
export function isInitialized() {
|
|
30
94
|
return _initialized;
|
|
31
95
|
}
|
|
32
|
-
/**
|
|
33
|
-
* Get the project directory local-recall is bound to.
|
|
34
|
-
*/
|
|
35
96
|
export function getDirectory() {
|
|
36
97
|
return _directory;
|
|
37
98
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
* Safe to call multiple times — the processed-log prevents
|
|
43
|
-
* duplicate extraction.
|
|
44
|
-
*/
|
|
45
|
-
export async function triggerExtraction() {
|
|
46
|
-
if (!_initialized || !_directory) {
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
try {
|
|
50
|
-
_lastExtraction = await runExtraction(_directory);
|
|
51
|
-
return _lastExtraction;
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
99
|
+
export async function triggerExtraction(directory) {
|
|
100
|
+
const runtime = await ensureRuntime(directory);
|
|
101
|
+
const status = await runtime.daemon.runNow('trigger-extraction');
|
|
102
|
+
return status.lastRun?.extraction ?? null;
|
|
56
103
|
}
|
|
57
|
-
/**
|
|
58
|
-
* Get the results of the last extraction pass.
|
|
59
|
-
*/
|
|
60
104
|
export function getLastExtractionStats() {
|
|
61
|
-
return
|
|
105
|
+
return _runtime?.daemon.getStatus().lastRun?.extraction ?? null;
|
|
62
106
|
}
|
|
63
|
-
/**
|
|
64
|
-
* Shutdown the local-recall system. Currently a no-op but
|
|
65
|
-
* provided for lifecycle symmetry and future cleanup needs.
|
|
66
|
-
*/
|
|
67
107
|
export function shutdownLocalRecall() {
|
|
108
|
+
_runtime?.daemon.stop();
|
|
68
109
|
_initialized = false;
|
|
69
110
|
_directory = null;
|
|
70
|
-
|
|
111
|
+
_runtime = null;
|
|
112
|
+
_initPromise = null;
|
|
113
|
+
}
|
|
114
|
+
function toMemory(args) {
|
|
115
|
+
return {
|
|
116
|
+
id: crypto.randomUUID(),
|
|
117
|
+
sessionID: 'agent-explicit',
|
|
118
|
+
messageID: 'agent-explicit',
|
|
119
|
+
category: args.category,
|
|
120
|
+
title: args.title,
|
|
121
|
+
body: args.content ?? args.description,
|
|
122
|
+
tags: args.tags,
|
|
123
|
+
importance: args.importance ?? 0.7,
|
|
124
|
+
createdAt: Date.now(),
|
|
125
|
+
extractedBy: 'agent-explicit',
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
export async function storeLearningMemory(directory, args) {
|
|
129
|
+
const runtime = await ensureRuntime(directory);
|
|
130
|
+
const memory = toMemory(args);
|
|
131
|
+
await storeMemory(runtime.directory, memory);
|
|
132
|
+
runtime.daemon.requestRun('store');
|
|
133
|
+
return {
|
|
134
|
+
memoryId: memory.id,
|
|
135
|
+
daemon: runtime.daemon.getStatus(),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
export async function searchLearningMemories(directory, criteria) {
|
|
139
|
+
const runtime = await ensureRuntime(directory);
|
|
140
|
+
try {
|
|
141
|
+
const vectorResults = await runtime.index.search(criteria);
|
|
142
|
+
return {
|
|
143
|
+
backend: 'vector',
|
|
144
|
+
results: vectorResults,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
const lexicalResults = await searchMemories(runtime.directory, criteria);
|
|
149
|
+
return {
|
|
150
|
+
backend: 'lexical',
|
|
151
|
+
results: lexicalResults,
|
|
152
|
+
fallbackReason: error instanceof Error ? error.message : String(error),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
export async function getLearningMemory(directory, memoryID) {
|
|
157
|
+
await ensureRuntime(directory);
|
|
158
|
+
return getMemory(directory, memoryID);
|
|
159
|
+
}
|
|
160
|
+
export async function startIndexingDaemon(directory, intervalMs) {
|
|
161
|
+
const runtime = await ensureRuntime(directory);
|
|
162
|
+
const daemon = runtime.daemon.start(intervalMs);
|
|
163
|
+
return {
|
|
164
|
+
daemon,
|
|
165
|
+
index: runtime.index.getStatus(),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
export async function stopIndexingDaemon(directory) {
|
|
169
|
+
const runtime = await ensureRuntime(directory);
|
|
170
|
+
const daemon = runtime.daemon.stop();
|
|
171
|
+
return {
|
|
172
|
+
daemon,
|
|
173
|
+
index: runtime.index.getStatus(),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
export async function getIndexingStatus(directory) {
|
|
177
|
+
const runtime = await ensureRuntime(directory);
|
|
178
|
+
return {
|
|
179
|
+
daemon: runtime.daemon.getStatus(),
|
|
180
|
+
index: runtime.index.getStatus(),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
export async function rebuildIndex(directory) {
|
|
184
|
+
const runtime = await ensureRuntime(directory);
|
|
185
|
+
const daemon = await runtime.daemon.rebuild();
|
|
186
|
+
return {
|
|
187
|
+
daemon,
|
|
188
|
+
index: runtime.index.getStatus(),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
export async function listLearningMemories(directory, criteria) {
|
|
192
|
+
const searchResults = await searchLearningMemories(directory, criteria);
|
|
193
|
+
return searchResults.results;
|
|
71
194
|
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import { getIndexingStatus, getLearningMemory, rebuildIndex, searchLearningMemories, shutdownLocalRecall, startIndexingDaemon, stopIndexingDaemon, storeLearningMemory, initLocalRecall, } from './mcp-server.js';
|
|
5
|
+
const CATEGORIES = [
|
|
6
|
+
'pattern',
|
|
7
|
+
'decision',
|
|
8
|
+
'debugging',
|
|
9
|
+
'preference',
|
|
10
|
+
'context',
|
|
11
|
+
'procedure',
|
|
12
|
+
];
|
|
13
|
+
function toCallResult(payload, isError = false) {
|
|
14
|
+
return {
|
|
15
|
+
isError,
|
|
16
|
+
content: [
|
|
17
|
+
{
|
|
18
|
+
type: 'text',
|
|
19
|
+
text: JSON.stringify(payload),
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function asObject(value) {
|
|
25
|
+
if (!value || typeof value !== 'object') {
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
function asString(value) {
|
|
31
|
+
return typeof value === 'string' ? value : undefined;
|
|
32
|
+
}
|
|
33
|
+
function asNumber(value) {
|
|
34
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
35
|
+
}
|
|
36
|
+
function asStringArray(value) {
|
|
37
|
+
if (!Array.isArray(value) || !value.every((item) => typeof item === 'string')) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
export async function runLocalRecallMCPServer(options = {}) {
|
|
43
|
+
const directory = options.directory ?? process.env.FF_LOCAL_RECALL_DIRECTORY ?? process.cwd();
|
|
44
|
+
initLocalRecall(directory);
|
|
45
|
+
const server = new Server({
|
|
46
|
+
name: 'ff-local-recall',
|
|
47
|
+
version: '1.0.0',
|
|
48
|
+
}, {
|
|
49
|
+
capabilities: {
|
|
50
|
+
tools: {},
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
54
|
+
tools: [
|
|
55
|
+
{
|
|
56
|
+
name: 'local_recall.search',
|
|
57
|
+
description: 'Search local-recall memories with vector retrieval and lexical fallback.',
|
|
58
|
+
inputSchema: {
|
|
59
|
+
type: 'object',
|
|
60
|
+
properties: {
|
|
61
|
+
query: { type: 'string' },
|
|
62
|
+
category: { type: 'string', enum: CATEGORIES },
|
|
63
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
64
|
+
minImportance: { type: 'number' },
|
|
65
|
+
limit: { type: 'number' },
|
|
66
|
+
},
|
|
67
|
+
required: ['query'],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'local_recall.get',
|
|
72
|
+
description: 'Get a memory by unique ID.',
|
|
73
|
+
inputSchema: {
|
|
74
|
+
type: 'object',
|
|
75
|
+
properties: {
|
|
76
|
+
memoryId: { type: 'string' },
|
|
77
|
+
},
|
|
78
|
+
required: ['memoryId'],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'local_recall.store',
|
|
83
|
+
description: 'Store a memory and enqueue async indexing.',
|
|
84
|
+
inputSchema: {
|
|
85
|
+
type: 'object',
|
|
86
|
+
properties: {
|
|
87
|
+
title: { type: 'string' },
|
|
88
|
+
description: { type: 'string' },
|
|
89
|
+
category: { type: 'string', enum: CATEGORIES },
|
|
90
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
91
|
+
importance: { type: 'number' },
|
|
92
|
+
content: { type: 'string' },
|
|
93
|
+
},
|
|
94
|
+
required: ['title', 'description', 'category', 'tags'],
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: 'local_recall.index.start',
|
|
99
|
+
description: 'Start the local-recall daemon.',
|
|
100
|
+
inputSchema: {
|
|
101
|
+
type: 'object',
|
|
102
|
+
properties: {
|
|
103
|
+
intervalMs: { type: 'number' },
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: 'local_recall.index.status',
|
|
109
|
+
description: 'Get local-recall daemon status.',
|
|
110
|
+
inputSchema: {
|
|
111
|
+
type: 'object',
|
|
112
|
+
properties: {},
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: 'local_recall.index.stop',
|
|
117
|
+
description: 'Stop local-recall daemon.',
|
|
118
|
+
inputSchema: {
|
|
119
|
+
type: 'object',
|
|
120
|
+
properties: {},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: 'local_recall.index.rebuild',
|
|
125
|
+
description: 'Perform a full rebuild of the local-recall index.',
|
|
126
|
+
inputSchema: {
|
|
127
|
+
type: 'object',
|
|
128
|
+
properties: {},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
}));
|
|
133
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
134
|
+
const toolName = request.params.name;
|
|
135
|
+
const args = asObject(request.params.arguments);
|
|
136
|
+
try {
|
|
137
|
+
switch (toolName) {
|
|
138
|
+
case 'local_recall.search': {
|
|
139
|
+
const query = asString(args.query);
|
|
140
|
+
if (!query) {
|
|
141
|
+
return toCallResult({ error: 'query is required' }, true);
|
|
142
|
+
}
|
|
143
|
+
const result = await searchLearningMemories(directory, {
|
|
144
|
+
query,
|
|
145
|
+
category: asString(args.category),
|
|
146
|
+
tags: asStringArray(args.tags),
|
|
147
|
+
minImportance: asNumber(args.minImportance),
|
|
148
|
+
limit: asNumber(args.limit),
|
|
149
|
+
});
|
|
150
|
+
return toCallResult(result);
|
|
151
|
+
}
|
|
152
|
+
case 'local_recall.get': {
|
|
153
|
+
const memoryId = asString(args.memoryId);
|
|
154
|
+
if (!memoryId) {
|
|
155
|
+
return toCallResult({ error: 'memoryId is required' }, true);
|
|
156
|
+
}
|
|
157
|
+
const memory = await getLearningMemory(directory, memoryId);
|
|
158
|
+
if (!memory) {
|
|
159
|
+
return toCallResult({ error: `Memory not found: ${memoryId}` }, true);
|
|
160
|
+
}
|
|
161
|
+
return toCallResult({ success: true, memory });
|
|
162
|
+
}
|
|
163
|
+
case 'local_recall.store': {
|
|
164
|
+
const title = asString(args.title);
|
|
165
|
+
const description = asString(args.description);
|
|
166
|
+
const category = asString(args.category);
|
|
167
|
+
const tags = asStringArray(args.tags);
|
|
168
|
+
if (!title || !description || !category || !tags) {
|
|
169
|
+
return toCallResult({
|
|
170
|
+
error: 'title, description, category, and tags are required',
|
|
171
|
+
}, true);
|
|
172
|
+
}
|
|
173
|
+
if (!CATEGORIES.includes(category)) {
|
|
174
|
+
return toCallResult({
|
|
175
|
+
error: `Invalid category: ${category}`,
|
|
176
|
+
}, true);
|
|
177
|
+
}
|
|
178
|
+
const result = await storeLearningMemory(directory, {
|
|
179
|
+
title,
|
|
180
|
+
description,
|
|
181
|
+
category: category,
|
|
182
|
+
tags,
|
|
183
|
+
importance: asNumber(args.importance),
|
|
184
|
+
content: asString(args.content),
|
|
185
|
+
});
|
|
186
|
+
return toCallResult({
|
|
187
|
+
success: true,
|
|
188
|
+
memoryId: result.memoryId,
|
|
189
|
+
daemon: result.daemon,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
case 'local_recall.index.start': {
|
|
193
|
+
const result = await startIndexingDaemon(directory, asNumber(args.intervalMs));
|
|
194
|
+
return toCallResult({ success: true, ...result });
|
|
195
|
+
}
|
|
196
|
+
case 'local_recall.index.status': {
|
|
197
|
+
const result = await getIndexingStatus(directory);
|
|
198
|
+
return toCallResult({ success: true, ...result });
|
|
199
|
+
}
|
|
200
|
+
case 'local_recall.index.stop': {
|
|
201
|
+
const result = await stopIndexingDaemon(directory);
|
|
202
|
+
return toCallResult({ success: true, ...result });
|
|
203
|
+
}
|
|
204
|
+
case 'local_recall.index.rebuild': {
|
|
205
|
+
const result = await rebuildIndex(directory);
|
|
206
|
+
return toCallResult({ success: true, ...result });
|
|
207
|
+
}
|
|
208
|
+
default:
|
|
209
|
+
return toCallResult({ error: `Unknown tool: ${toolName}` }, true);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
return toCallResult({
|
|
214
|
+
error: error instanceof Error ? error.message : String(error),
|
|
215
|
+
}, true);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
const transport = new StdioServerTransport();
|
|
219
|
+
await server.connect(transport);
|
|
220
|
+
const shutdown = () => {
|
|
221
|
+
shutdownLocalRecall();
|
|
222
|
+
};
|
|
223
|
+
process.on('SIGINT', shutdown);
|
|
224
|
+
process.on('SIGTERM', shutdown);
|
|
225
|
+
}
|
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* mcp-tools.ts — OpenCode plugin tool definitions for local-recall.
|
|
3
|
-
*
|
|
4
|
-
* Replaces the legacy ff-learning-{store,search,get} plugins with
|
|
5
|
-
* MCP-backed tools that read from OpenCode's session storage and
|
|
6
|
-
* expose the local-recall memory system.
|
|
7
|
-
*
|
|
8
|
-
* Public contract uses the native MemoryCategory taxonomy
|
|
9
|
-
* (pattern, decision, debugging, preference, context, procedure).
|
|
10
|
-
* The public contract exposes only category-based MCP tools.
|
|
11
|
-
*/
|
|
12
1
|
import type { ToolContext } from '@opencode-ai/plugin/tool';
|
|
13
2
|
export declare const createLearningStoreTool: () => {
|
|
14
3
|
description: string;
|
|
@@ -88,3 +77,27 @@ export declare const createLearningGetTool: () => {
|
|
|
88
77
|
memoryId: string;
|
|
89
78
|
}, context: ToolContext): Promise<string>;
|
|
90
79
|
};
|
|
80
|
+
export declare const createLearningIndexStartTool: () => {
|
|
81
|
+
description: string;
|
|
82
|
+
args: {
|
|
83
|
+
intervalMs: import("zod").ZodOptional<import("zod").ZodNumber>;
|
|
84
|
+
};
|
|
85
|
+
execute(args: {
|
|
86
|
+
intervalMs?: number | undefined;
|
|
87
|
+
}, context: ToolContext): Promise<string>;
|
|
88
|
+
};
|
|
89
|
+
export declare const createLearningIndexStatusTool: () => {
|
|
90
|
+
description: string;
|
|
91
|
+
args: {};
|
|
92
|
+
execute(args: Record<string, never>, context: ToolContext): Promise<string>;
|
|
93
|
+
};
|
|
94
|
+
export declare const createLearningIndexStopTool: () => {
|
|
95
|
+
description: string;
|
|
96
|
+
args: {};
|
|
97
|
+
execute(args: Record<string, never>, context: ToolContext): Promise<string>;
|
|
98
|
+
};
|
|
99
|
+
export declare const createLearningIndexRebuildTool: () => {
|
|
100
|
+
description: string;
|
|
101
|
+
args: {};
|
|
102
|
+
execute(args: Record<string, never>, context: ToolContext): Promise<string>;
|
|
103
|
+
};
|