@syntesseraai/opencode-feature-factory 0.3.1 → 0.3.3
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 +1 -1
- package/bin/ff-deploy.js +7 -1
- package/dist/index.js +3 -3
- package/dist/local-recall/daemon.d.ts +2 -2
- package/dist/local-recall/daemon.js +52 -13
- package/dist/local-recall/index-state.js +1 -1
- package/dist/local-recall/mcp-server.js +5 -1
- 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 +17 -4
- package/dist/local-recall/vector/orama-index.js +1 -1
- package/dist/mcp-config.d.ts +6 -11
- package/dist/mcp-config.js +26 -39
- package/package.json +1 -1
- package/skills/ff-learning/SKILL.md +2 -2
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ The plugin now includes a local MCP daemon binary: `ff-local-recall-mcp`.
|
|
|
45
45
|
|
|
46
46
|
### Environment Variables
|
|
47
47
|
|
|
48
|
-
- `FF_LOCAL_RECALL_DIRECTORY` - Directory that contains
|
|
48
|
+
- `FF_LOCAL_RECALL_DIRECTORY` - Directory that contains `ff-memories/` (default: current working directory)
|
|
49
49
|
- `FF_LOCAL_RECALL_DAEMON_AUTOSTART` - Start index daemon automatically (`true` by default)
|
|
50
50
|
- `FF_LOCAL_RECALL_INDEX_INTERVAL_MS` - Background daemon interval in milliseconds (default: `15000`)
|
|
51
51
|
- `FF_LOCAL_RECALL_EXTRACTION_ENABLED` - Run extraction during daemon cycles (`true` by default)
|
package/bin/ff-deploy.js
CHANGED
|
@@ -31,7 +31,13 @@ const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
|
31
31
|
const DEFAULT_MCP_SERVERS = {
|
|
32
32
|
'ff-local-recall': {
|
|
33
33
|
type: 'local',
|
|
34
|
-
command:
|
|
34
|
+
command: [
|
|
35
|
+
'npx',
|
|
36
|
+
'-y',
|
|
37
|
+
'--package=@syntesseraai/opencode-feature-factory@latest',
|
|
38
|
+
'--',
|
|
39
|
+
'ff-local-recall-mcp',
|
|
40
|
+
],
|
|
35
41
|
enabled: true,
|
|
36
42
|
},
|
|
37
43
|
'jina-ai': {
|
package/dist/index.js
CHANGED
|
@@ -38,10 +38,10 @@ export const FeatureFactoryPlugin = async (input) => {
|
|
|
38
38
|
}
|
|
39
39
|
// Initialize local-recall memory system
|
|
40
40
|
initLocalRecall(directory);
|
|
41
|
-
// Update MCP server configuration in
|
|
42
|
-
// This ensures Feature Factory MCP servers are available
|
|
41
|
+
// Update MCP server configuration in global OpenCode config
|
|
42
|
+
// This ensures Feature Factory MCP servers are available across projects
|
|
43
43
|
try {
|
|
44
|
-
await updateMCPConfig(
|
|
44
|
+
await updateMCPConfig($);
|
|
45
45
|
}
|
|
46
46
|
catch (error) {
|
|
47
47
|
// Silently fail - don't block plugin initialization if MCP config update fails
|
|
@@ -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
|
|
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,42 @@
|
|
|
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';
|
|
23
|
+
function getErrorMessage(error) {
|
|
24
|
+
return error instanceof Error ? error.message : String(error);
|
|
25
|
+
}
|
|
26
|
+
function recordFailure(stats, failure) {
|
|
27
|
+
let message;
|
|
28
|
+
let rawError;
|
|
29
|
+
switch (failure.scope) {
|
|
30
|
+
case 'project':
|
|
31
|
+
message = `No OpenCode project found for directory: ${failure.directory}`;
|
|
32
|
+
break;
|
|
33
|
+
case 'message':
|
|
34
|
+
message = `Error processing message ${failure.messageID}: ${getErrorMessage(failure.error)}`;
|
|
35
|
+
rawError = failure.error;
|
|
36
|
+
break;
|
|
37
|
+
case 'session':
|
|
38
|
+
message = `Error processing session ${failure.sessionID}: ${getErrorMessage(failure.error)}`;
|
|
39
|
+
rawError = failure.error;
|
|
40
|
+
break;
|
|
41
|
+
case 'extraction':
|
|
42
|
+
message = `Extraction failed: ${getErrorMessage(failure.error)}`;
|
|
43
|
+
rawError = failure.error;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
stats.errors.push(message);
|
|
47
|
+
if (rawError !== undefined) {
|
|
48
|
+
console.error('[local-recall-daemon]', message, rawError);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
console.error('[local-recall-daemon]', message);
|
|
52
|
+
}
|
|
24
53
|
// ────────────────────────────────────────────────────────────
|
|
25
54
|
// Helpers
|
|
26
55
|
// ────────────────────────────────────────────────────────────
|
|
@@ -33,7 +62,7 @@ async function buildMessageContentHash(messageID) {
|
|
|
33
62
|
try {
|
|
34
63
|
const parts = await listParts(messageID);
|
|
35
64
|
const textParts = parts
|
|
36
|
-
.filter((p) => p.type === 'text' || p.type === 'reasoning')
|
|
65
|
+
.filter((p) => p.type === 'text' || p.type === 'reasoning' || p.type === 'thinking')
|
|
37
66
|
.map((p) => p.text ?? '')
|
|
38
67
|
.join('\n');
|
|
39
68
|
// Hash content only (no messageID) so dedupe works across ID changes
|
|
@@ -52,7 +81,7 @@ async function buildMessageContentHash(messageID) {
|
|
|
52
81
|
*
|
|
53
82
|
* 1. Find the OpenCode project matching `directory`
|
|
54
83
|
* 2. Load existing processed log for fast membership checks
|
|
55
|
-
* 3. Iterate sessions → messages (assistant
|
|
84
|
+
* 3. Iterate sessions → messages (assistant + thinking)
|
|
56
85
|
* 4. Skip by message-ID *and* content-hash (dual idempotency)
|
|
57
86
|
* 5. Run session + thinking extractors
|
|
58
87
|
* 6. Assign logical IDs (session-<sid>-<msgid>-N / thinking-<sid>-<msgid>-N)
|
|
@@ -70,12 +99,11 @@ export async function runExtraction(directory) {
|
|
|
70
99
|
// Find project for this directory
|
|
71
100
|
const project = await findProject(directory);
|
|
72
101
|
if (!project) {
|
|
73
|
-
stats
|
|
102
|
+
recordFailure(stats, { scope: 'project', directory });
|
|
74
103
|
return stats;
|
|
75
104
|
}
|
|
76
105
|
// Ensure local-recall directories exist
|
|
77
|
-
|
|
78
|
-
await fs.mkdir(path.join(recallDir, 'memories'), { recursive: true });
|
|
106
|
+
await fs.mkdir(getMemoriesDir(directory), { recursive: true });
|
|
79
107
|
// Pre-load processed log for fast lookups
|
|
80
108
|
const existingLog = await readProcessedLog(directory);
|
|
81
109
|
const processedMsgIDs = getProcessedMessageIDs(existingLog);
|
|
@@ -90,8 +118,8 @@ export async function runExtraction(directory) {
|
|
|
90
118
|
const messages = await listMessages(session.id);
|
|
91
119
|
for (const message of messages) {
|
|
92
120
|
stats.messagesScanned++;
|
|
93
|
-
// Only process assistant messages
|
|
94
|
-
if (message.role !== 'assistant') {
|
|
121
|
+
// Only process assistant and thinking messages
|
|
122
|
+
if (message.role !== 'assistant' && message.role !== 'thinking') {
|
|
95
123
|
stats.messagesSkipped++;
|
|
96
124
|
continue;
|
|
97
125
|
}
|
|
@@ -149,6 +177,7 @@ export async function runExtraction(directory) {
|
|
|
149
177
|
processedHashes.add(msgHash);
|
|
150
178
|
// Mark as processed with content hash
|
|
151
179
|
newProcessedEntries.push({
|
|
180
|
+
status: 'success',
|
|
152
181
|
messageID: message.id,
|
|
153
182
|
contentHash: msgHash,
|
|
154
183
|
processedAt: Date.now(),
|
|
@@ -156,19 +185,29 @@ export async function runExtraction(directory) {
|
|
|
156
185
|
});
|
|
157
186
|
}
|
|
158
187
|
catch (err) {
|
|
159
|
-
stats
|
|
188
|
+
recordFailure(stats, {
|
|
189
|
+
scope: 'message',
|
|
190
|
+
messageID: message.id,
|
|
191
|
+
error: err,
|
|
192
|
+
});
|
|
160
193
|
// Still mark as processed to avoid re-trying broken messages
|
|
161
194
|
newProcessedEntries.push({
|
|
195
|
+
status: 'failed',
|
|
162
196
|
messageID: message.id,
|
|
163
197
|
contentHash: msgHash,
|
|
164
198
|
processedAt: Date.now(),
|
|
165
199
|
memoriesCreated: 0,
|
|
200
|
+
failure: getErrorMessage(err),
|
|
166
201
|
});
|
|
167
202
|
}
|
|
168
203
|
}
|
|
169
204
|
}
|
|
170
205
|
catch (err) {
|
|
171
|
-
stats
|
|
206
|
+
recordFailure(stats, {
|
|
207
|
+
scope: 'session',
|
|
208
|
+
sessionID: session.id,
|
|
209
|
+
error: err,
|
|
210
|
+
});
|
|
172
211
|
}
|
|
173
212
|
}
|
|
174
213
|
// Batch store all new memories
|
|
@@ -182,7 +221,7 @@ export async function runExtraction(directory) {
|
|
|
182
221
|
}
|
|
183
222
|
}
|
|
184
223
|
catch (err) {
|
|
185
|
-
stats
|
|
224
|
+
recordFailure(stats, { scope: 'extraction', error: err });
|
|
186
225
|
}
|
|
187
226
|
return stats;
|
|
188
227
|
}
|
|
@@ -7,7 +7,7 @@ const EMPTY_STATE = {
|
|
|
7
7
|
updatedAt: 0,
|
|
8
8
|
};
|
|
9
9
|
function getIndexDir(directory) {
|
|
10
|
-
return path.join(directory, '
|
|
10
|
+
return path.join(directory, 'ff-memories', 'index');
|
|
11
11
|
}
|
|
12
12
|
export function getIndexStatePath(directory) {
|
|
13
13
|
return path.join(getIndexDir(directory), 'state.json');
|
|
@@ -44,9 +44,13 @@ function createRuntime(directory) {
|
|
|
44
44
|
extractionEnabled: parseBoolean(process.env.FF_LOCAL_RECALL_EXTRACTION_ENABLED, true),
|
|
45
45
|
});
|
|
46
46
|
return index.initialize().then(() => {
|
|
47
|
-
|
|
47
|
+
const autoStart = parseBoolean(process.env.FF_LOCAL_RECALL_DAEMON_AUTOSTART, true);
|
|
48
|
+
if (autoStart) {
|
|
48
49
|
daemon.start();
|
|
49
50
|
}
|
|
51
|
+
else {
|
|
52
|
+
daemon.requestRun('startup');
|
|
53
|
+
}
|
|
50
54
|
return {
|
|
51
55
|
directory,
|
|
52
56
|
index,
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* Memory Service
|
|
3
3
|
*
|
|
4
4
|
* MCP-backed memory domain service that stores and retrieves
|
|
5
|
-
* extracted memories as JSON files in
|
|
5
|
+
* extracted memories as JSON files in ff-memories/memories/
|
|
6
6
|
*/
|
|
7
7
|
import type { Memory, SearchCriteria, MemorySearchResult } from './types.js';
|
|
8
|
+
export declare function getMemoriesDir(directory: string): string;
|
|
8
9
|
/**
|
|
9
10
|
* Persist a memory to disk.
|
|
10
11
|
*/
|
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
* Memory Service
|
|
3
3
|
*
|
|
4
4
|
* MCP-backed memory domain service that stores and retrieves
|
|
5
|
-
* extracted memories as JSON files in
|
|
5
|
+
* extracted memories as JSON files in ff-memories/memories/
|
|
6
6
|
*/
|
|
7
7
|
import { readFile, writeFile, readdir, mkdir } from 'fs/promises';
|
|
8
8
|
import path from 'path';
|
|
9
9
|
const { join, resolve } = path;
|
|
10
10
|
/** Only allow IDs that are safe for filenames: alphanumeric, hyphens, underscores. */
|
|
11
11
|
const SAFE_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
12
|
-
function getMemoriesDir(directory) {
|
|
13
|
-
return join(directory, '
|
|
12
|
+
export function getMemoriesDir(directory) {
|
|
13
|
+
return join(directory, 'ff-memories', 'memories');
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
16
|
* Validate a memory ID and resolve its file path with containment check.
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* hash derived from the message content so re-extractions with identical
|
|
7
7
|
* content are skipped even if message IDs change.
|
|
8
8
|
*
|
|
9
|
-
* Stored as a JSON file at
|
|
9
|
+
* Stored as a JSON file at ff-memories/processed.json
|
|
10
10
|
*/
|
|
11
11
|
import type { ProcessedEntry } from './types.js';
|
|
12
12
|
/**
|
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
* hash derived from the message content so re-extractions with identical
|
|
7
7
|
* content are skipped even if message IDs change.
|
|
8
8
|
*
|
|
9
|
-
* Stored as a JSON file at
|
|
9
|
+
* Stored as a JSON file at ff-memories/processed.json
|
|
10
10
|
*/
|
|
11
11
|
import { createHash } from 'node:crypto';
|
|
12
12
|
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
13
13
|
import { join, dirname } from 'node:path';
|
|
14
14
|
function getLogPath(directory) {
|
|
15
|
-
return join(directory, '
|
|
15
|
+
return join(directory, 'ff-memories', 'processed.json');
|
|
16
16
|
}
|
|
17
17
|
// ────────────────────────────────────────────────────────────
|
|
18
18
|
// Content hashing
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* thinking-extractor.ts — Extracts learnings from "thinking" / reasoning blocks.
|
|
3
3
|
*
|
|
4
4
|
* OpenCode stores extended-thinking or chain-of-thought content in parts
|
|
5
|
-
* with type "reasoning". These often contain high-signal insights about
|
|
5
|
+
* with type "reasoning" or "thinking". These often contain high-signal insights about
|
|
6
6
|
* decision making and problem solving that are worth capturing.
|
|
7
7
|
*/
|
|
8
8
|
import type { ExtractionInput, ExtractionResult, OCPart } from './types.js';
|
|
9
9
|
/**
|
|
10
|
-
* Extract learnings specifically from reasoning
|
|
10
|
+
* Extract learnings specifically from reasoning/thinking parts.
|
|
11
11
|
*/
|
|
12
12
|
export declare function extractFromThinkingParts(input: ExtractionInput, parts: OCPart[]): ExtractionResult[];
|
|
13
13
|
/**
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* thinking-extractor.ts — Extracts learnings from "thinking" / reasoning blocks.
|
|
3
3
|
*
|
|
4
4
|
* OpenCode stores extended-thinking or chain-of-thought content in parts
|
|
5
|
-
* with type "reasoning". These often contain high-signal insights about
|
|
5
|
+
* with type "reasoning" or "thinking". These often contain high-signal insights about
|
|
6
6
|
* decision making and problem solving that are worth capturing.
|
|
7
7
|
*/
|
|
8
8
|
import { listParts } from './storage-reader.js';
|
|
@@ -97,13 +97,13 @@ function deriveThinkingTitle(text) {
|
|
|
97
97
|
// Public API
|
|
98
98
|
// ────────────────────────────────────────────────────────────
|
|
99
99
|
/**
|
|
100
|
-
* Extract learnings specifically from reasoning
|
|
100
|
+
* Extract learnings specifically from reasoning/thinking parts.
|
|
101
101
|
*/
|
|
102
102
|
export function extractFromThinkingParts(input, parts) {
|
|
103
103
|
const results = [];
|
|
104
104
|
for (const part of parts) {
|
|
105
|
-
// Accept
|
|
106
|
-
if (part.type !== 'reasoning' || !part.text)
|
|
105
|
+
// Accept reasoning/thinking type parts (extended thinking)
|
|
106
|
+
if ((part.type !== 'reasoning' && part.type !== 'thinking') || !part.text)
|
|
107
107
|
continue;
|
|
108
108
|
if (part.text.length < MIN_THINKING_LENGTH)
|
|
109
109
|
continue;
|
|
@@ -32,7 +32,7 @@ export interface OCSession {
|
|
|
32
32
|
export interface OCMessage {
|
|
33
33
|
id: string;
|
|
34
34
|
sessionID: string;
|
|
35
|
-
role: 'user' | 'assistant';
|
|
35
|
+
role: 'user' | 'assistant' | 'thinking';
|
|
36
36
|
time: {
|
|
37
37
|
created: number;
|
|
38
38
|
};
|
|
@@ -119,11 +119,24 @@ export interface ExtractionResult {
|
|
|
119
119
|
/** Where the extraction came from — used to generate logical IDs */
|
|
120
120
|
source: 'session' | 'thinking';
|
|
121
121
|
}
|
|
122
|
-
|
|
123
|
-
export interface ProcessedEntry {
|
|
122
|
+
interface ProcessedEntryBase {
|
|
124
123
|
messageID: string;
|
|
125
124
|
processedAt: number;
|
|
126
|
-
memoriesCreated: number;
|
|
127
125
|
/** SHA-256 hex hash of the concatenated extracted bodies for content-level idempotency */
|
|
128
126
|
contentHash: string;
|
|
129
127
|
}
|
|
128
|
+
/** Tracks which messages have already been processed */
|
|
129
|
+
export type ProcessedEntry = (ProcessedEntryBase & {
|
|
130
|
+
status: 'success';
|
|
131
|
+
memoriesCreated: number;
|
|
132
|
+
failure?: undefined;
|
|
133
|
+
}) | (ProcessedEntryBase & {
|
|
134
|
+
status: 'failed';
|
|
135
|
+
memoriesCreated: 0;
|
|
136
|
+
failure: string;
|
|
137
|
+
}) | (ProcessedEntryBase & {
|
|
138
|
+
status?: undefined;
|
|
139
|
+
memoriesCreated: number;
|
|
140
|
+
failure?: undefined;
|
|
141
|
+
});
|
|
142
|
+
export {};
|
|
@@ -213,7 +213,7 @@ export class OramaMemoryIndex {
|
|
|
213
213
|
return memories.length;
|
|
214
214
|
}
|
|
215
215
|
get indexDir() {
|
|
216
|
-
return path.join(this.directory, '
|
|
216
|
+
return path.join(this.directory, 'ff-memories', 'index');
|
|
217
217
|
}
|
|
218
218
|
get manifestPath() {
|
|
219
219
|
return path.join(this.indexDir, 'manifest.json');
|
package/dist/mcp-config.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
type BunShell = any;
|
|
2
2
|
/**
|
|
3
3
|
* Default MCP server configuration to be added by the plugin
|
|
4
|
-
* These servers will be merged into the
|
|
4
|
+
* These servers will be merged into the global OpenCode config.
|
|
5
5
|
*/
|
|
6
6
|
export declare const DEFAULT_MCP_SERVERS: {
|
|
7
7
|
readonly 'ff-local-recall': {
|
|
@@ -44,20 +44,15 @@ export interface MCPServers {
|
|
|
44
44
|
*/
|
|
45
45
|
export declare function mergeMCPServers(existing: MCPServers | undefined, defaults: typeof DEFAULT_MCP_SERVERS): MCPServers;
|
|
46
46
|
/**
|
|
47
|
-
* Update the MCP servers configuration in opencode.json
|
|
47
|
+
* Update the MCP servers configuration in global opencode.json.
|
|
48
48
|
*
|
|
49
49
|
* This function:
|
|
50
|
-
* 1. Reads existing config from
|
|
51
|
-
* 2.
|
|
50
|
+
* 1. Reads existing config from ~/.config/opencode/opencode.json
|
|
51
|
+
* 2. Preserves existing MCP servers
|
|
52
52
|
* 3. Adds default Feature Factory MCP servers that don't exist
|
|
53
|
-
* 4. Writes updated config to
|
|
54
|
-
*
|
|
55
|
-
* Note: Writing to .opencode/opencode.json keeps Feature Factory MCP config
|
|
56
|
-
* separate from the user's root opencode.json, following the same pattern
|
|
57
|
-
* as the quality gate configuration.
|
|
53
|
+
* 4. Writes updated config back to ~/.config/opencode/opencode.json
|
|
58
54
|
*
|
|
59
55
|
* @param $ - Bun shell instance
|
|
60
|
-
* @param directory - The project directory
|
|
61
56
|
*/
|
|
62
|
-
export declare function updateMCPConfig($: BunShell
|
|
57
|
+
export declare function updateMCPConfig($: BunShell): Promise<void>;
|
|
63
58
|
export {};
|
package/dist/mcp-config.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { readJsonFile } from './quality-gate-config.js';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
const GLOBAL_OPENCODE_DIR = join(homedir(), '.config', 'opencode');
|
|
5
|
+
const GLOBAL_OPENCODE_CONFIG_PATH = join(GLOBAL_OPENCODE_DIR, 'opencode.json');
|
|
2
6
|
/**
|
|
3
7
|
* Default MCP server configuration to be added by the plugin
|
|
4
|
-
* These servers will be merged into the
|
|
8
|
+
* These servers will be merged into the global OpenCode config.
|
|
5
9
|
*/
|
|
6
10
|
export const DEFAULT_MCP_SERVERS = {
|
|
7
11
|
'ff-local-recall': {
|
|
@@ -44,38 +48,21 @@ export function mergeMCPServers(existing, defaults) {
|
|
|
44
48
|
return result;
|
|
45
49
|
}
|
|
46
50
|
/**
|
|
47
|
-
* Update the MCP servers configuration in opencode.json
|
|
51
|
+
* Update the MCP servers configuration in global opencode.json.
|
|
48
52
|
*
|
|
49
53
|
* This function:
|
|
50
|
-
* 1. Reads existing config from
|
|
51
|
-
* 2.
|
|
54
|
+
* 1. Reads existing config from ~/.config/opencode/opencode.json
|
|
55
|
+
* 2. Preserves existing MCP servers
|
|
52
56
|
* 3. Adds default Feature Factory MCP servers that don't exist
|
|
53
|
-
* 4. Writes updated config to
|
|
54
|
-
*
|
|
55
|
-
* Note: Writing to .opencode/opencode.json keeps Feature Factory MCP config
|
|
56
|
-
* separate from the user's root opencode.json, following the same pattern
|
|
57
|
-
* as the quality gate configuration.
|
|
57
|
+
* 4. Writes updated config back to ~/.config/opencode/opencode.json
|
|
58
58
|
*
|
|
59
59
|
* @param $ - Bun shell instance
|
|
60
|
-
* @param directory - The project directory
|
|
61
60
|
*/
|
|
62
|
-
export async function updateMCPConfig(
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const [rootJson, dotOpencodeJson] = await Promise.all([
|
|
68
|
-
readJsonFile($, rootConfigPath),
|
|
69
|
-
readJsonFile($, dotOpencodeConfigPath),
|
|
70
|
-
]);
|
|
71
|
-
// Get existing MCP servers from both configs
|
|
72
|
-
const rootMcpServers = (rootJson?.mcp ?? {});
|
|
73
|
-
const dotOpencodeMcpServers = (dotOpencodeJson?.mcp ?? {});
|
|
74
|
-
// Merge existing servers (dotOpencode overrides root)
|
|
75
|
-
const existingMcpServers = {
|
|
76
|
-
...rootMcpServers,
|
|
77
|
-
...dotOpencodeMcpServers,
|
|
78
|
-
};
|
|
61
|
+
export async function updateMCPConfig($) {
|
|
62
|
+
// Read existing global config
|
|
63
|
+
const globalJson = await readJsonFile($, GLOBAL_OPENCODE_CONFIG_PATH);
|
|
64
|
+
// Get existing MCP servers from global config
|
|
65
|
+
const existingMcpServers = (globalJson?.mcp ?? {});
|
|
79
66
|
// Merge with default MCP servers
|
|
80
67
|
const updatedMcpServers = mergeMCPServers(existingMcpServers, DEFAULT_MCP_SERVERS);
|
|
81
68
|
// Check if any changes are needed
|
|
@@ -84,24 +71,24 @@ export async function updateMCPConfig($, directory) {
|
|
|
84
71
|
// All default servers already exist, no need to update
|
|
85
72
|
return;
|
|
86
73
|
}
|
|
87
|
-
// Prepare updated config
|
|
88
|
-
const
|
|
89
|
-
...(
|
|
74
|
+
// Prepare updated global config
|
|
75
|
+
const updatedGlobalConfig = {
|
|
76
|
+
...(globalJson ?? {}),
|
|
90
77
|
mcp: updatedMcpServers,
|
|
91
78
|
};
|
|
92
|
-
// Ensure
|
|
79
|
+
// Ensure global config directory exists
|
|
93
80
|
try {
|
|
94
|
-
await $ `mkdir -p ${
|
|
81
|
+
await $ `mkdir -p ${GLOBAL_OPENCODE_DIR}`.quiet();
|
|
95
82
|
}
|
|
96
83
|
catch {
|
|
97
84
|
// Directory might already exist, ignore
|
|
98
85
|
}
|
|
99
|
-
// Backup existing
|
|
100
|
-
if (
|
|
86
|
+
// Backup existing global config if it exists and has content
|
|
87
|
+
if (globalJson && Object.keys(globalJson).length > 0) {
|
|
101
88
|
try {
|
|
102
89
|
const timestamp = new Date().toISOString().split('T')[0].replace(/-/g, '');
|
|
103
|
-
const backupPath = `${
|
|
104
|
-
const backupContent = JSON.stringify(
|
|
90
|
+
const backupPath = `${GLOBAL_OPENCODE_CONFIG_PATH}.backup.${timestamp}`;
|
|
91
|
+
const backupContent = JSON.stringify(globalJson, null, 2);
|
|
105
92
|
await $ `echo ${backupContent} > ${backupPath}`.quiet();
|
|
106
93
|
}
|
|
107
94
|
catch (error) {
|
|
@@ -109,10 +96,10 @@ export async function updateMCPConfig($, directory) {
|
|
|
109
96
|
console.warn('[feature-factory] Could not create backup:', error);
|
|
110
97
|
}
|
|
111
98
|
}
|
|
112
|
-
// Write updated config to
|
|
113
|
-
const configContent = JSON.stringify(
|
|
99
|
+
// Write updated config to global opencode.json
|
|
100
|
+
const configContent = JSON.stringify(updatedGlobalConfig, null, 2);
|
|
114
101
|
try {
|
|
115
|
-
await $ `echo ${configContent} > ${
|
|
102
|
+
await $ `echo ${configContent} > ${GLOBAL_OPENCODE_CONFIG_PATH}`.quiet();
|
|
116
103
|
}
|
|
117
104
|
catch (error) {
|
|
118
105
|
// Silently fail - don't block if we can't write the config
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@syntesseraai/opencode-feature-factory",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "OpenCode plugin for Feature Factory agents - provides sub-agents and skills for validation, review, security, and architecture assessment",
|
|
7
7
|
"license": "MIT",
|
|
@@ -10,7 +10,7 @@ metadata:
|
|
|
10
10
|
|
|
11
11
|
# Learning Skill (Local-Recall)
|
|
12
12
|
|
|
13
|
-
Use this skill to capture insights, store knowledge, and retrieve past learnings to improve future work. Memories are stored as JSON in
|
|
13
|
+
Use this skill to capture insights, store knowledge, and retrieve past learnings to improve future work. Memories are stored as JSON in `ff-memories/memories/` and automatically extracted from OpenCode session history.
|
|
14
14
|
|
|
15
15
|
## How It Works
|
|
16
16
|
|
|
@@ -168,7 +168,7 @@ Search learnings:
|
|
|
168
168
|
### Directory Structure
|
|
169
169
|
|
|
170
170
|
```
|
|
171
|
-
|
|
171
|
+
ff-memories/
|
|
172
172
|
├── memories/ # All memory JSON files
|
|
173
173
|
│ ├── {uuid}.json # Individual memory files
|
|
174
174
|
│ └── ...
|