@syntesseraai/opencode-feature-factory 0.3.1 → 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 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 `.feature-factory/` (default: current working directory)
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: 'ff-local-recall-mcp',
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 project opencode.json files
42
- // This ensures Feature Factory MCP servers are available in the project
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($, directory);
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 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
  }
@@ -7,7 +7,7 @@ const EMPTY_STATE = {
7
7
  updatedAt: 0,
8
8
  };
9
9
  function getIndexDir(directory) {
10
- return path.join(directory, '.feature-factory', 'local-recall', 'index');
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
- if (parseBoolean(process.env.FF_LOCAL_RECALL_DAEMON_AUTOSTART, true)) {
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 .feature-factory/local-recall/memories/
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 .feature-factory/local-recall/memories/
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, '.feature-factory', 'local-recall', 'memories');
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 .feature-factory/local-recall/processed.json
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 .feature-factory/local-recall/processed.json
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, '.feature-factory', 'local-recall', 'processed.json');
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 / thinking parts.
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 / thinking parts.
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 "reasoning" type parts (extended thinking)
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
  };
@@ -213,7 +213,7 @@ export class OramaMemoryIndex {
213
213
  return memories.length;
214
214
  }
215
215
  get indexDir() {
216
- return path.join(this.directory, '.feature-factory', 'local-recall', 'index');
216
+ return path.join(this.directory, 'ff-memories', 'index');
217
217
  }
218
218
  get manifestPath() {
219
219
  return path.join(this.indexDir, 'manifest.json');
@@ -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 project's opencode.json
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 files.
47
+ * Update the MCP servers configuration in global opencode.json.
48
48
  *
49
49
  * This function:
50
- * 1. Reads existing config from both root and .opencode directories
51
- * 2. Merges MCP servers from both configs (preserving all existing servers)
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 .opencode/opencode.json
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, directory: string): Promise<void>;
57
+ export declare function updateMCPConfig($: BunShell): Promise<void>;
63
58
  export {};
@@ -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 project's opencode.json
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 files.
51
+ * Update the MCP servers configuration in global opencode.json.
48
52
  *
49
53
  * This function:
50
- * 1. Reads existing config from both root and .opencode directories
51
- * 2. Merges MCP servers from both configs (preserving all existing servers)
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 .opencode/opencode.json
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($, directory) {
63
- const rootConfigPath = `${directory}/opencode.json`;
64
- const dotOpencodeConfigPath = `${directory}/.opencode/opencode.json`;
65
- const dotOpencodeDir = `${directory}/.opencode`;
66
- // Read existing configs
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 for .opencode/opencode.json
88
- const updatedDotOpencodeConfig = {
89
- ...(dotOpencodeJson ?? {}),
74
+ // Prepare updated global config
75
+ const updatedGlobalConfig = {
76
+ ...(globalJson ?? {}),
90
77
  mcp: updatedMcpServers,
91
78
  };
92
- // Ensure .opencode directory exists
79
+ // Ensure global config directory exists
93
80
  try {
94
- await $ `mkdir -p ${dotOpencodeDir}`.quiet();
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 .opencode/opencode.json if it exists and has content
100
- if (dotOpencodeJson && Object.keys(dotOpencodeJson).length > 0) {
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 = `${dotOpencodeConfigPath}.backup.${timestamp}`;
104
- const backupContent = JSON.stringify(dotOpencodeJson, null, 2);
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 .opencode/opencode.json
113
- const configContent = JSON.stringify(updatedDotOpencodeConfig, null, 2);
99
+ // Write updated config to global opencode.json
100
+ const configContent = JSON.stringify(updatedGlobalConfig, null, 2);
114
101
  try {
115
- await $ `echo ${configContent} > ${dotOpencodeConfigPath}`.quiet();
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.1",
4
+ "version": "0.3.2",
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 `.feature-factory/local-recall/memories/` and automatically extracted from OpenCode session history.
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
- .feature-factory/local-recall/
171
+ ff-memories/
172
172
  ├── memories/ # All memory JSON files
173
173
  │ ├── {uuid}.json # Individual memory files
174
174
  │ └── ...