@syntesseraai/opencode-feature-factory 0.2.45 → 0.3.0

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 (46) hide show
  1. package/agents/building.md +13 -14
  2. package/agents/ff-acceptance.md +12 -15
  3. package/agents/ff-research.md +12 -16
  4. package/agents/ff-review.md +12 -15
  5. package/agents/ff-security.md +12 -15
  6. package/agents/ff-validate.md +12 -15
  7. package/agents/ff-well-architected.md +12 -15
  8. package/agents/planning.md +12 -24
  9. package/agents/reviewing.md +12 -24
  10. package/dist/index.js +7 -7
  11. package/dist/local-recall/daemon.d.ts +35 -0
  12. package/dist/local-recall/daemon.js +188 -0
  13. package/dist/local-recall/index.d.ts +14 -0
  14. package/dist/local-recall/index.js +20 -0
  15. package/dist/local-recall/mcp-server.d.ts +38 -0
  16. package/dist/local-recall/mcp-server.js +71 -0
  17. package/dist/local-recall/mcp-tools.d.ts +90 -0
  18. package/dist/local-recall/mcp-tools.js +162 -0
  19. package/dist/local-recall/memory-service.d.ts +31 -0
  20. package/dist/local-recall/memory-service.js +156 -0
  21. package/dist/local-recall/model-router.d.ts +23 -0
  22. package/dist/local-recall/model-router.js +41 -0
  23. package/dist/local-recall/processed-log.d.ts +41 -0
  24. package/dist/local-recall/processed-log.js +82 -0
  25. package/dist/local-recall/session-extractor.d.ts +19 -0
  26. package/dist/local-recall/session-extractor.js +172 -0
  27. package/dist/local-recall/storage-reader.d.ts +40 -0
  28. package/dist/local-recall/storage-reader.js +147 -0
  29. package/dist/local-recall/thinking-extractor.d.ts +16 -0
  30. package/dist/local-recall/thinking-extractor.js +132 -0
  31. package/dist/local-recall/types.d.ts +129 -0
  32. package/dist/local-recall/types.js +7 -0
  33. package/package.json +1 -1
  34. package/skills/ff-learning/SKILL.md +166 -689
  35. package/dist/learning/memory-get.d.ts +0 -24
  36. package/dist/learning/memory-get.js +0 -155
  37. package/dist/learning/memory-search.d.ts +0 -20
  38. package/dist/learning/memory-search.js +0 -193
  39. package/dist/learning/memory-store.d.ts +0 -20
  40. package/dist/learning/memory-store.js +0 -85
  41. package/dist/plugins/ff-learning-get-plugin.d.ts +0 -2
  42. package/dist/plugins/ff-learning-get-plugin.js +0 -55
  43. package/dist/plugins/ff-learning-search-plugin.d.ts +0 -2
  44. package/dist/plugins/ff-learning-search-plugin.js +0 -65
  45. package/dist/plugins/ff-learning-store-plugin.d.ts +0 -2
  46. package/dist/plugins/ff-learning-store-plugin.js +0 -70
@@ -1,24 +0,0 @@
1
- export interface GetCriteria {
2
- memoryId?: string;
3
- filePath?: string;
4
- }
5
- export interface MemoryData {
6
- id: string;
7
- title: string;
8
- description: string;
9
- date: string;
10
- memoryType: string;
11
- agentId: string;
12
- importance: number;
13
- tags: string[];
14
- source?: string;
15
- relatedMemories?: string[];
16
- context?: {
17
- project?: string;
18
- task?: string;
19
- files?: string[];
20
- };
21
- content: string;
22
- filePath: string;
23
- }
24
- export declare function getMemory(directory: string, criteria: GetCriteria): Promise<MemoryData | null>;
@@ -1,155 +0,0 @@
1
- import { readFile } from 'fs/promises';
2
- export async function getMemory(directory, criteria) {
3
- try {
4
- let filePath = null;
5
- if (criteria.filePath) {
6
- filePath = `${directory}/${criteria.filePath}`;
7
- }
8
- else if (criteria.memoryId) {
9
- // Search for file by ID
10
- filePath = await findFileById(directory, criteria.memoryId);
11
- if (!filePath) {
12
- return null;
13
- }
14
- }
15
- else {
16
- return null;
17
- }
18
- const content = await readFile(filePath, 'utf-8');
19
- const metadata = parseFrontmatter(content);
20
- if (!metadata) {
21
- return null;
22
- }
23
- // Extract content after frontmatter
24
- const contentMatch = content.match(/^---\n[\s\S]*?\n---\n\n([\s\S]*)$/);
25
- const bodyContent = contentMatch ? contentMatch[1] : '';
26
- return {
27
- id: metadata.id,
28
- title: metadata.title,
29
- description: metadata.description,
30
- date: metadata.date,
31
- memoryType: metadata.memory_type,
32
- agentId: metadata.agent_id,
33
- importance: metadata.importance,
34
- tags: metadata.tags || [],
35
- source: metadata.source,
36
- relatedMemories: metadata.related_memories,
37
- context: metadata.context,
38
- content: bodyContent,
39
- filePath: criteria.filePath || (filePath ? filePath.replace(`${directory}/`, '') : ''),
40
- };
41
- }
42
- catch {
43
- return null;
44
- }
45
- }
46
- async function findFileById(cwd, id) {
47
- const { glob } = await import('glob');
48
- try {
49
- const files = await glob('**/*.md', {
50
- cwd: `${cwd}/.feature-factory/memories`,
51
- absolute: true,
52
- });
53
- for (const file of files) {
54
- try {
55
- const content = await readFile(file, 'utf-8');
56
- const metadata = parseFrontmatter(content);
57
- if (metadata && metadata.id === id) {
58
- return file;
59
- }
60
- }
61
- catch {
62
- continue;
63
- }
64
- }
65
- }
66
- catch {
67
- // Directory might not exist
68
- }
69
- return null;
70
- }
71
- function parseFrontmatter(content) {
72
- const match = content.match(/^---\n([\s\S]*?)\n---/);
73
- if (!match)
74
- return null;
75
- const frontmatter = match[1];
76
- const result = {};
77
- // Simple YAML parsing for our specific format
78
- const lines = frontmatter.split('\n');
79
- let currentKey = null;
80
- let currentArray = [];
81
- let currentObject = {};
82
- let objectKey = null;
83
- for (const line of lines) {
84
- const trimmed = line.trim();
85
- // Check for array item
86
- if (trimmed.startsWith('- ')) {
87
- const value = trimmed.substring(2).replace(/^['"]|['"]$/g, '');
88
- if (currentKey) {
89
- currentArray.push(value);
90
- }
91
- continue;
92
- }
93
- // Check for nested object property
94
- if (trimmed.match(/^\w+:\s*/)) {
95
- const colonIndex = trimmed.indexOf(':');
96
- const key = trimmed.substring(0, colonIndex).trim();
97
- const value = trimmed
98
- .substring(colonIndex + 1)
99
- .trim()
100
- .replace(/^['"]|['"]$/g, '');
101
- // Check if this is a top-level key or nested
102
- if (line.startsWith(' ') && objectKey) {
103
- // Nested property
104
- if (key === 'files' && value === '') {
105
- // Start of files array
106
- currentObject[key] = [];
107
- }
108
- else {
109
- currentObject[key] = value;
110
- }
111
- }
112
- else {
113
- // Top-level key
114
- if (currentKey && currentArray.length > 0) {
115
- result[currentKey] = currentArray;
116
- currentArray = [];
117
- }
118
- if (objectKey && Object.keys(currentObject).length > 0) {
119
- result[objectKey] = currentObject;
120
- currentObject = {};
121
- }
122
- if (value === '') {
123
- // Could be start of array or object
124
- currentKey = key;
125
- currentArray = [];
126
- objectKey = key;
127
- currentObject = {};
128
- }
129
- else {
130
- // Simple value
131
- result[key] = value;
132
- currentKey = null;
133
- objectKey = null;
134
- }
135
- }
136
- continue;
137
- }
138
- // Check for array item in nested context
139
- if (trimmed.startsWith('- ') && objectKey) {
140
- const value = trimmed.substring(2).replace(/^['"]|['"]$/g, '');
141
- if (!currentObject.files) {
142
- currentObject.files = [];
143
- }
144
- currentObject.files.push(value);
145
- }
146
- }
147
- // Handle any remaining data
148
- if (currentKey && currentArray.length > 0) {
149
- result[currentKey] = currentArray;
150
- }
151
- if (objectKey && Object.keys(currentObject).length > 0) {
152
- result[objectKey] = currentObject;
153
- }
154
- return result;
155
- }
@@ -1,20 +0,0 @@
1
- export interface SearchCriteria {
2
- query: string;
3
- tags?: string[];
4
- memoryType?: 'episodic' | 'semantic' | 'procedural';
5
- agentId?: string;
6
- limit?: number;
7
- minImportance?: number;
8
- }
9
- export interface MemoryMetadata {
10
- id: string;
11
- title: string;
12
- description: string;
13
- date: string;
14
- memoryType: string;
15
- agentId: string;
16
- importance: number;
17
- tags: string[];
18
- filePath: string;
19
- }
20
- export declare function searchMemories(directory: string, criteria: SearchCriteria): Promise<MemoryMetadata[]>;
@@ -1,193 +0,0 @@
1
- import { readFile, readdir } from 'fs/promises';
2
- import { join, relative } from 'path';
3
- export async function searchMemories(directory, criteria) {
4
- const memoriesDir = `${directory}/.feature-factory/memories`;
5
- const results = [];
6
- try {
7
- // Find all memory files
8
- const files = await findAllMemoryFiles(memoriesDir);
9
- // Parse and filter each file
10
- for (const filePath of files) {
11
- try {
12
- const content = await readFile(filePath, 'utf-8');
13
- const metadata = parseFrontmatter(content);
14
- if (!metadata)
15
- continue;
16
- // Apply filters
17
- if (criteria.memoryType && metadata.memory_type !== criteria.memoryType) {
18
- continue;
19
- }
20
- if (criteria.agentId && metadata.agent_id !== criteria.agentId) {
21
- continue;
22
- }
23
- if (criteria.minImportance !== undefined &&
24
- metadata.importance < criteria.minImportance) {
25
- continue;
26
- }
27
- if (criteria.tags && criteria.tags.length > 0) {
28
- const tags = metadata.tags || [];
29
- const hasMatchingTag = criteria.tags.some((tag) => tags.includes(tag));
30
- if (!hasMatchingTag)
31
- continue;
32
- }
33
- // Check query match
34
- const queryLower = criteria.query.toLowerCase();
35
- const title = metadata.title || '';
36
- const description = metadata.description || '';
37
- const tags = metadata.tags || [];
38
- const titleMatch = title.toLowerCase().includes(queryLower);
39
- const descMatch = description.toLowerCase().includes(queryLower);
40
- const tagMatch = tags.some((tag) => tag.toLowerCase().includes(queryLower));
41
- if (!titleMatch && !descMatch && !tagMatch) {
42
- continue;
43
- }
44
- // Calculate relevance score
45
- let relevance = 0;
46
- if (titleMatch)
47
- relevance += 3;
48
- if (descMatch)
49
- relevance += 2;
50
- if (tagMatch)
51
- relevance += 1;
52
- results.push({
53
- id: metadata.id,
54
- title: metadata.title,
55
- description: metadata.description,
56
- date: metadata.date,
57
- memoryType: metadata.memory_type,
58
- agentId: metadata.agent_id,
59
- importance: metadata.importance,
60
- tags: tags,
61
- filePath: relative(directory, filePath),
62
- relevance,
63
- });
64
- }
65
- catch {
66
- // Skip files that can't be parsed
67
- continue;
68
- }
69
- }
70
- // Sort by relevance and importance
71
- results.sort((a, b) => {
72
- const scoreA = a.relevance * a.importance;
73
- const scoreB = b.relevance * b.importance;
74
- return scoreB - scoreA;
75
- });
76
- // Limit results
77
- const limit = criteria.limit || 10;
78
- return results.slice(0, limit).map((r) => ({
79
- id: r.id,
80
- title: r.title,
81
- description: r.description,
82
- date: r.date,
83
- memoryType: r.memoryType,
84
- agentId: r.agentId,
85
- importance: r.importance,
86
- tags: r.tags,
87
- filePath: r.filePath,
88
- }));
89
- }
90
- catch {
91
- return [];
92
- }
93
- }
94
- async function findAllMemoryFiles(dir) {
95
- const files = [];
96
- try {
97
- const entries = await readdir(dir, { withFileTypes: true, recursive: true });
98
- for (const entry of entries) {
99
- if (entry.isFile() && entry.name.endsWith('.md')) {
100
- files.push(join(dir, entry.parentPath || '', entry.name));
101
- }
102
- }
103
- }
104
- catch {
105
- // Directory might not exist
106
- }
107
- return files;
108
- }
109
- function parseFrontmatter(content) {
110
- const match = content.match(/^---\n([\s\S]*?)\n---/);
111
- if (!match)
112
- return null;
113
- const frontmatter = match[1];
114
- const result = {};
115
- // Simple YAML parsing for our specific format
116
- const lines = frontmatter.split('\n');
117
- let currentKey = null;
118
- let currentArray = [];
119
- let currentObject = {};
120
- let objectKey = null;
121
- for (const line of lines) {
122
- const trimmed = line.trim();
123
- // Check for array item
124
- if (trimmed.startsWith('- ')) {
125
- const value = trimmed.substring(2).replace(/^['"]|['"]$/g, '');
126
- if (currentKey) {
127
- currentArray.push(value);
128
- }
129
- continue;
130
- }
131
- // Check for nested object property
132
- if (trimmed.match(/^\w+:\s*/)) {
133
- const colonIndex = trimmed.indexOf(':');
134
- const key = trimmed.substring(0, colonIndex).trim();
135
- const value = trimmed
136
- .substring(colonIndex + 1)
137
- .trim()
138
- .replace(/^['"]|['"]$/g, '');
139
- // Check if this is a top-level key or nested
140
- if (line.startsWith(' ') && objectKey) {
141
- // Nested property
142
- if (key === 'files' && value === '') {
143
- // Start of files array
144
- currentObject[key] = [];
145
- }
146
- else {
147
- currentObject[key] = value;
148
- }
149
- }
150
- else {
151
- // Top-level key
152
- if (currentKey && currentArray.length > 0) {
153
- result[currentKey] = currentArray;
154
- currentArray = [];
155
- }
156
- if (objectKey && Object.keys(currentObject).length > 0) {
157
- result[objectKey] = currentObject;
158
- currentObject = {};
159
- }
160
- if (value === '') {
161
- // Could be start of array or object
162
- currentKey = key;
163
- currentArray = [];
164
- objectKey = key;
165
- currentObject = {};
166
- }
167
- else {
168
- // Simple value
169
- result[key] = value;
170
- currentKey = null;
171
- objectKey = null;
172
- }
173
- }
174
- continue;
175
- }
176
- // Check for array item in nested context
177
- if (trimmed.startsWith('- ') && objectKey) {
178
- const value = trimmed.substring(2).replace(/^['"]|['"]$/g, '');
179
- if (!currentObject.files) {
180
- currentObject.files = [];
181
- }
182
- currentObject.files.push(value);
183
- }
184
- }
185
- // Handle any remaining data
186
- if (currentKey && currentArray.length > 0) {
187
- result[currentKey] = currentArray;
188
- }
189
- if (objectKey && Object.keys(currentObject).length > 0) {
190
- result[objectKey] = currentObject;
191
- }
192
- return result;
193
- }
@@ -1,20 +0,0 @@
1
- export interface MemoryInput {
2
- title: string;
3
- description: string;
4
- memoryType: 'episodic' | 'semantic' | 'procedural';
5
- tags: string[];
6
- importance: number;
7
- content?: string;
8
- source?: 'conversation' | 'research' | 'implementation' | 'review';
9
- relatedMemories?: string[];
10
- context?: {
11
- project?: string;
12
- task?: string;
13
- files?: string[];
14
- };
15
- }
16
- export interface MemoryResult {
17
- id: string;
18
- filePath: string;
19
- }
20
- export declare function storeMemory(directory: string, memoryInput: MemoryInput): Promise<MemoryResult>;
@@ -1,85 +0,0 @@
1
- import { v4 as uuidv4 } from 'uuid';
2
- import { writeFile, mkdir } from 'fs/promises';
3
- import { dirname } from 'path';
4
- export async function storeMemory(directory, memoryInput) {
5
- const id = uuidv4();
6
- const date = new Date().toISOString();
7
- // Determine file path based on memory type
8
- const filePath = generateMemoryFilePath(memoryInput.memoryType, id, date);
9
- const fullPath = `${directory}/${filePath}`;
10
- // Generate frontmatter
11
- const frontmatter = generateFrontmatter(id, date, memoryInput);
12
- // Generate full content
13
- const fullContent = `${frontmatter}\n\n${memoryInput.content || ''}`;
14
- // Ensure directory exists
15
- await mkdir(dirname(fullPath), { recursive: true });
16
- // Write file
17
- await writeFile(fullPath, fullContent, 'utf-8');
18
- return {
19
- id,
20
- filePath,
21
- };
22
- }
23
- function generateMemoryFilePath(memoryType, id, date) {
24
- const dateObj = new Date(date);
25
- const year = dateObj.getUTCFullYear();
26
- const month = String(dateObj.getUTCMonth() + 1).padStart(2, '0');
27
- const day = String(dateObj.getUTCDate()).padStart(2, '0');
28
- const hours = String(dateObj.getUTCHours()).padStart(2, '0');
29
- const minutes = String(dateObj.getUTCMinutes()).padStart(2, '0');
30
- const seconds = String(dateObj.getUTCSeconds()).padStart(2, '0');
31
- switch (memoryType) {
32
- case 'episodic':
33
- return `.feature-factory/memories/episodic/${year}/${month}/${day}/${year}-${month}-${day}-${hours}-${minutes}-${seconds}-episodic.md`;
34
- case 'semantic':
35
- return `.feature-factory/memories/semantic/${memoryType}-${id}.md`;
36
- case 'procedural':
37
- return `.feature-factory/memories/procedural/procedural-${id}.md`;
38
- default:
39
- return `.feature-factory/memories/semantic/semantic-${id}.md`;
40
- }
41
- }
42
- function generateFrontmatter(id, date, input) {
43
- const frontmatter = {
44
- id,
45
- title: input.title,
46
- description: input.description,
47
- date,
48
- memory_type: input.memoryType,
49
- agent_id: 'agent', // This would be populated from context
50
- importance: input.importance,
51
- tags: input.tags,
52
- };
53
- if (input.source) {
54
- frontmatter.source = input.source;
55
- }
56
- if (input.relatedMemories && input.relatedMemories.length > 0) {
57
- frontmatter.related_memories = input.relatedMemories;
58
- }
59
- if (input.context) {
60
- frontmatter.context = input.context;
61
- }
62
- // Convert to YAML format
63
- const yamlLines = Object.entries(frontmatter).map(([key, value]) => {
64
- if (Array.isArray(value)) {
65
- return `${key}:\n${value.map((v) => ` - '${v}'`).join('\n')}`;
66
- }
67
- else if (typeof value === 'object' && value !== null) {
68
- return `${key}:\n${Object.entries(value)
69
- .map(([k, v]) => {
70
- if (Array.isArray(v)) {
71
- return ` ${k}:\n${v.map((item) => ` - '${item}'`).join('\n')}`;
72
- }
73
- return ` ${k}: '${v}'`;
74
- })
75
- .join('\n')}`;
76
- }
77
- else if (typeof value === 'number') {
78
- return `${key}: ${value}`;
79
- }
80
- else {
81
- return `${key}: '${value}'`;
82
- }
83
- });
84
- return `---\n${yamlLines.join('\n')}\n---`;
85
- }
@@ -1,2 +0,0 @@
1
- import { tool } from '@opencode-ai/plugin/tool';
2
- export declare function createFFLearningGetTool(): ReturnType<typeof tool>;
@@ -1,55 +0,0 @@
1
- import { tool } from '@opencode-ai/plugin/tool';
2
- import { getMemory } from '../learning/memory-get.js';
3
- export function createFFLearningGetTool() {
4
- return tool({
5
- description: 'Retrieve the full content of a specific memory by ID or file path',
6
- args: {
7
- memoryId: tool.schema.string().optional().describe('UUID of the memory to retrieve'),
8
- filePath: tool.schema.string().optional().describe('Direct file path to the memory file'),
9
- },
10
- async execute(args, toolCtx) {
11
- try {
12
- if (!args.memoryId && !args.filePath) {
13
- return JSON.stringify({
14
- success: false,
15
- error: 'Either memoryId or filePath must be provided',
16
- }, null, 2);
17
- }
18
- const result = await getMemory(toolCtx.directory, {
19
- memoryId: args.memoryId,
20
- filePath: args.filePath,
21
- });
22
- if (!result) {
23
- return JSON.stringify({
24
- success: false,
25
- error: `Memory not found: ${args.memoryId || args.filePath}`,
26
- }, null, 2);
27
- }
28
- return JSON.stringify({
29
- success: true,
30
- memory: {
31
- id: result.id,
32
- title: result.title,
33
- description: result.description,
34
- date: result.date,
35
- memoryType: result.memoryType,
36
- agentId: result.agentId,
37
- importance: result.importance,
38
- tags: result.tags,
39
- source: result.source,
40
- relatedMemories: result.relatedMemories,
41
- context: result.context,
42
- content: result.content,
43
- filePath: result.filePath,
44
- },
45
- }, null, 2);
46
- }
47
- catch (error) {
48
- return JSON.stringify({
49
- success: false,
50
- error: `Failed to get memory: ${error}`,
51
- }, null, 2);
52
- }
53
- },
54
- });
55
- }
@@ -1,2 +0,0 @@
1
- import { tool } from '@opencode-ai/plugin/tool';
2
- export declare function createFFLearningSearchTool(): ReturnType<typeof tool>;
@@ -1,65 +0,0 @@
1
- import { tool } from '@opencode-ai/plugin/tool';
2
- import { searchMemories } from '../learning/memory-search.js';
3
- export function createFFLearningSearchTool() {
4
- return tool({
5
- description: 'Search for memories by query, tags, type, or agent. Returns matching memory metadata sorted by relevance and importance',
6
- args: {
7
- query: tool.schema
8
- .string()
9
- .describe('Search query string to match against titles and descriptions'),
10
- tags: tool.schema.array(tool.schema.string()).optional().describe('Filter by specific tags'),
11
- memoryType: tool.schema
12
- .enum(['episodic', 'semantic', 'procedural'])
13
- .optional()
14
- .describe('Filter by memory type'),
15
- agentId: tool.schema.string().optional().describe('Filter by agent that created the memory'),
16
- limit: tool.schema
17
- .number()
18
- .min(1)
19
- .max(50)
20
- .default(10)
21
- .describe('Maximum number of results to return (default: 10)'),
22
- minImportance: tool.schema
23
- .number()
24
- .min(0)
25
- .max(1)
26
- .optional()
27
- .describe('Minimum importance threshold (0.0-1.0)'),
28
- },
29
- async execute(args, toolCtx) {
30
- try {
31
- const criteria = {
32
- query: args.query,
33
- tags: args.tags,
34
- memoryType: args.memoryType,
35
- agentId: args.agentId,
36
- limit: args.limit,
37
- minImportance: args.minImportance,
38
- };
39
- const results = await searchMemories(toolCtx.directory, criteria);
40
- return JSON.stringify({
41
- success: true,
42
- count: results.length,
43
- query: args.query,
44
- memories: results.map((m) => ({
45
- id: m.id,
46
- title: m.title,
47
- description: m.description,
48
- memoryType: m.memoryType,
49
- agentId: m.agentId,
50
- importance: m.importance,
51
- tags: m.tags,
52
- date: m.date,
53
- filePath: m.filePath,
54
- })),
55
- }, null, 2);
56
- }
57
- catch (error) {
58
- return JSON.stringify({
59
- success: false,
60
- error: `Failed to search memories: ${error}`,
61
- }, null, 2);
62
- }
63
- },
64
- });
65
- }
@@ -1,2 +0,0 @@
1
- import { tool } from '@opencode-ai/plugin/tool';
2
- export declare function createFFLearningStoreTool(): ReturnType<typeof tool>;