@syntesseraai/opencode-feature-factory 0.4.2 → 0.4.4

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 (48) hide show
  1. package/agents/building.md +9 -6
  2. package/package.json +2 -2
  3. package/dist/discovery.test.d.ts +0 -10
  4. package/dist/discovery.test.js +0 -97
  5. package/dist/local-recall/daemon-controller.d.ts +0 -51
  6. package/dist/local-recall/daemon-controller.js +0 -166
  7. package/dist/local-recall/daemon.d.ts +0 -35
  8. package/dist/local-recall/daemon.js +0 -262
  9. package/dist/local-recall/index-state.d.ts +0 -14
  10. package/dist/local-recall/index-state.js +0 -76
  11. package/dist/local-recall/index.d.ts +0 -20
  12. package/dist/local-recall/index.js +0 -27
  13. package/dist/local-recall/mcp-server.d.ts +0 -34
  14. package/dist/local-recall/mcp-server.js +0 -194
  15. package/dist/local-recall/mcp-stdio-server.d.ts +0 -4
  16. package/dist/local-recall/mcp-stdio-server.js +0 -225
  17. package/dist/local-recall/mcp-tools.d.ts +0 -103
  18. package/dist/local-recall/mcp-tools.js +0 -187
  19. package/dist/local-recall/memory-service.d.ts +0 -32
  20. package/dist/local-recall/memory-service.js +0 -156
  21. package/dist/local-recall/model-router.d.ts +0 -23
  22. package/dist/local-recall/model-router.js +0 -41
  23. package/dist/local-recall/processed-log.d.ts +0 -41
  24. package/dist/local-recall/processed-log.js +0 -85
  25. package/dist/local-recall/prompt-injection.d.ts +0 -2
  26. package/dist/local-recall/prompt-injection.js +0 -194
  27. package/dist/local-recall/session-extractor.d.ts +0 -19
  28. package/dist/local-recall/session-extractor.js +0 -172
  29. package/dist/local-recall/storage-reader.d.ts +0 -40
  30. package/dist/local-recall/storage-reader.js +0 -157
  31. package/dist/local-recall/thinking-extractor.d.ts +0 -16
  32. package/dist/local-recall/thinking-extractor.js +0 -132
  33. package/dist/local-recall/types.d.ts +0 -152
  34. package/dist/local-recall/types.js +0 -7
  35. package/dist/local-recall/vector/embedding-provider.d.ts +0 -37
  36. package/dist/local-recall/vector/embedding-provider.js +0 -184
  37. package/dist/local-recall/vector/orama-index.d.ts +0 -39
  38. package/dist/local-recall/vector/orama-index.js +0 -408
  39. package/dist/local-recall/vector/types.d.ts +0 -33
  40. package/dist/local-recall/vector/types.js +0 -1
  41. package/dist/output.test.d.ts +0 -8
  42. package/dist/output.test.js +0 -205
  43. package/dist/plugins/ff-reviews-delete-plugin.d.ts +0 -2
  44. package/dist/plugins/ff-reviews-delete-plugin.js +0 -32
  45. package/dist/quality-gate-config.test.d.ts +0 -9
  46. package/dist/quality-gate-config.test.js +0 -164
  47. package/dist/stop-quality-gate.test.d.ts +0 -8
  48. package/dist/stop-quality-gate.test.js +0 -549
@@ -1,194 +0,0 @@
1
- import { searchLearningMemories } from './mcp-server.js';
2
- const MEMORY_CONTEXT_HEADER = '## Local Recall: Relevant Memories';
3
- const MEMORY_CONTEXT_GUIDANCE = 'Use these prior project learnings only when they directly improve the current response.';
4
- const DEFAULT_INTERNAL_MARKERS = ['[LOCAL_RECALL_INTERNAL]'];
5
- const APPROX_CHARS_PER_TOKEN = 4;
6
- const pendingContextBySession = new Map();
7
- function parseBooleanEnv(name, fallback) {
8
- const value = process.env[name];
9
- if (!value) {
10
- return fallback;
11
- }
12
- const normalized = value.trim().toLowerCase();
13
- if (['1', 'true', 'yes', 'on'].includes(normalized)) {
14
- return true;
15
- }
16
- if (['0', 'false', 'no', 'off'].includes(normalized)) {
17
- return false;
18
- }
19
- return fallback;
20
- }
21
- function parseNumberEnv(name, fallback, options) {
22
- const value = process.env[name];
23
- if (!value) {
24
- return fallback;
25
- }
26
- const parsed = Number(value);
27
- if (!Number.isFinite(parsed)) {
28
- return fallback;
29
- }
30
- const withInteger = options?.integer ? Math.trunc(parsed) : parsed;
31
- const min = options?.min ?? Number.NEGATIVE_INFINITY;
32
- const max = options?.max ?? Number.POSITIVE_INFINITY;
33
- return Math.min(max, Math.max(min, withInteger));
34
- }
35
- function parseInternalMarkers() {
36
- const configured = process.env.FF_LOCAL_RECALL_PROMPT_INTERNAL_MARKERS;
37
- if (!configured) {
38
- return DEFAULT_INTERNAL_MARKERS;
39
- }
40
- const markers = configured
41
- .split(',')
42
- .map((entry) => entry.trim())
43
- .filter((entry) => entry.length > 0);
44
- return markers.length > 0 ? markers : DEFAULT_INTERNAL_MARKERS;
45
- }
46
- function getPromptInjectionConfig() {
47
- return {
48
- enabled: parseBooleanEnv('FF_LOCAL_RECALL_PROMPT_INJECTION_ENABLED', true),
49
- minPromptChars: parseNumberEnv('FF_LOCAL_RECALL_PROMPT_INJECTION_MIN_PROMPT_CHARS', 20, {
50
- min: 1,
51
- max: 2000,
52
- integer: true,
53
- }),
54
- maxQueryChars: parseNumberEnv('FF_LOCAL_RECALL_PROMPT_INJECTION_MAX_QUERY_CHARS', 2000, {
55
- min: 64,
56
- max: 20000,
57
- integer: true,
58
- }),
59
- searchLimit: parseNumberEnv('FF_LOCAL_RECALL_PROMPT_INJECTION_SEARCH_LIMIT', 8, {
60
- min: 1,
61
- max: 50,
62
- integer: true,
63
- }),
64
- maxResults: parseNumberEnv('FF_LOCAL_RECALL_PROMPT_INJECTION_MAX_RESULTS', 5, {
65
- min: 1,
66
- max: 20,
67
- integer: true,
68
- }),
69
- minRelevance: parseNumberEnv('FF_LOCAL_RECALL_PROMPT_INJECTION_MIN_RELEVANCE', 0.2, {
70
- min: 0,
71
- max: 1,
72
- }),
73
- minImportance: parseNumberEnv('FF_LOCAL_RECALL_PROMPT_INJECTION_MIN_IMPORTANCE', 0.2, {
74
- min: 0,
75
- max: 1,
76
- }),
77
- maxTokens: parseNumberEnv('FF_LOCAL_RECALL_PROMPT_INJECTION_MAX_TOKENS', 400, {
78
- min: 50,
79
- max: 4000,
80
- integer: true,
81
- }),
82
- sessionOnly: parseBooleanEnv('FF_LOCAL_RECALL_PROMPT_INJECTION_SESSION_ONLY', false),
83
- internalMarkers: parseInternalMarkers(),
84
- };
85
- }
86
- function estimateTokens(text) {
87
- return Math.ceil(text.length / APPROX_CHARS_PER_TOKEN);
88
- }
89
- function normalizeInlineText(value) {
90
- return value.replace(/\s+/g, ' ').trim();
91
- }
92
- function extractUserPromptText(parts) {
93
- const text = parts
94
- .filter((part) => part.type === 'text')
95
- .filter((part) => !part.synthetic)
96
- .map((part) => part.text)
97
- .join('\n')
98
- .trim();
99
- return text;
100
- }
101
- function isInternalPrompt(prompt, markers) {
102
- return markers.some((marker) => marker.length > 0 && prompt.includes(marker));
103
- }
104
- function scoreMemory(memory) {
105
- return memory.relevance * 0.75 + memory.importance * 0.25;
106
- }
107
- function rankMemories(memories, config) {
108
- return memories
109
- .filter((memory) => memory.relevance >= config.minRelevance)
110
- .sort((a, b) => {
111
- const scoreDifference = scoreMemory(b) - scoreMemory(a);
112
- if (scoreDifference !== 0) {
113
- return scoreDifference;
114
- }
115
- return b.createdAt - a.createdAt;
116
- });
117
- }
118
- function formatMemoryLine(index, memory) {
119
- const title = normalizeInlineText(memory.title);
120
- const tags = memory.tags.length > 0 ? ` | tags: ${memory.tags.slice(0, 5).join(', ')}` : '';
121
- return `${index}. ${title} (category: ${memory.category}, relevance: ${memory.relevance.toFixed(2)}, importance: ${memory.importance.toFixed(2)}${tags})`;
122
- }
123
- function buildMemoryContext(memories, config) {
124
- const lines = [];
125
- let usedTokens = estimateTokens(MEMORY_CONTEXT_HEADER) + estimateTokens(MEMORY_CONTEXT_GUIDANCE);
126
- for (const memory of memories) {
127
- if (lines.length >= config.maxResults) {
128
- break;
129
- }
130
- const line = formatMemoryLine(lines.length + 1, memory);
131
- const lineTokens = estimateTokens(line);
132
- if (lines.length > 0 && usedTokens + lineTokens > config.maxTokens) {
133
- break;
134
- }
135
- lines.push(line);
136
- usedTokens += lineTokens;
137
- }
138
- if (lines.length === 0) {
139
- return null;
140
- }
141
- return [MEMORY_CONTEXT_HEADER, MEMORY_CONTEXT_GUIDANCE, ...lines.map((line) => `- ${line}`)].join('\n');
142
- }
143
- export function createLocalRecallPromptHooks(directory) {
144
- const config = getPromptInjectionConfig();
145
- if (!config.enabled) {
146
- return {};
147
- }
148
- return {
149
- 'chat.message': async (input, output) => {
150
- const promptText = extractUserPromptText(output.parts);
151
- if (promptText.length < config.minPromptChars) {
152
- pendingContextBySession.delete(input.sessionID);
153
- return;
154
- }
155
- if (isInternalPrompt(promptText, config.internalMarkers)) {
156
- pendingContextBySession.delete(input.sessionID);
157
- return;
158
- }
159
- const criteria = {
160
- query: promptText.slice(0, config.maxQueryChars),
161
- limit: config.searchLimit,
162
- minImportance: config.minImportance,
163
- };
164
- if (config.sessionOnly) {
165
- criteria.sessionID = input.sessionID;
166
- }
167
- try {
168
- const searchOutput = await searchLearningMemories(directory, criteria);
169
- const ranked = rankMemories(searchOutput.results, config);
170
- const context = buildMemoryContext(ranked, config);
171
- if (!context) {
172
- pendingContextBySession.delete(input.sessionID);
173
- return;
174
- }
175
- pendingContextBySession.set(input.sessionID, context);
176
- }
177
- catch {
178
- pendingContextBySession.delete(input.sessionID);
179
- }
180
- },
181
- 'experimental.chat.system.transform': async (input, output) => {
182
- const sessionID = input.sessionID;
183
- if (!sessionID) {
184
- return;
185
- }
186
- const pendingContext = pendingContextBySession.get(sessionID);
187
- if (!pendingContext) {
188
- return;
189
- }
190
- output.system.push(pendingContext);
191
- pendingContextBySession.delete(sessionID);
192
- },
193
- };
194
- }
@@ -1,19 +0,0 @@
1
- /**
2
- * session-extractor.ts — Extracts learnings from OpenCode session messages.
3
- *
4
- * Scans assistant text parts for reusable knowledge (patterns, decisions,
5
- * debugging insights, preferences, context, procedures) and returns
6
- * ExtractionResult[] ready for storage.
7
- */
8
- import type { ExtractionInput, ExtractionResult, OCPart } from './types.js';
9
- /**
10
- * Extract learnings from a set of text parts for a single message.
11
- *
12
- * Each qualifying text part that passes heuristic classification
13
- * becomes one ExtractionResult.
14
- */
15
- export declare function extractFromParts(input: ExtractionInput, parts: OCPart[]): ExtractionResult[];
16
- /**
17
- * High-level: extract from a message by reading its parts from storage.
18
- */
19
- export declare function extractFromMessage(input: ExtractionInput): Promise<ExtractionResult[]>;
@@ -1,172 +0,0 @@
1
- /**
2
- * session-extractor.ts — Extracts learnings from OpenCode session messages.
3
- *
4
- * Scans assistant text parts for reusable knowledge (patterns, decisions,
5
- * debugging insights, preferences, context, procedures) and returns
6
- * ExtractionResult[] ready for storage.
7
- */
8
- import { listParts } from './storage-reader.js';
9
- const CATEGORY_RULES = [
10
- {
11
- category: 'pattern',
12
- weight: 0.9,
13
- patterns: [
14
- /\bpattern\b/i,
15
- /\bbest practice\b/i,
16
- /\bidiom(atic)?\b/i,
17
- /\bconvention\b/i,
18
- /\balways use\b/i,
19
- /\bprefer\s+\w+\s+over\b/i,
20
- ],
21
- },
22
- {
23
- category: 'decision',
24
- weight: 0.85,
25
- patterns: [
26
- /\bdecid(e|ed|ing)\b/i,
27
- /\bchose\b/i,
28
- /\btrade-?off\b/i,
29
- /\bwent with\b/i,
30
- /\binstead of\b/i,
31
- /\bwe (should|chose|picked)\b/i,
32
- ],
33
- },
34
- {
35
- category: 'debugging',
36
- weight: 0.8,
37
- patterns: [
38
- /\bbug\b/i,
39
- /\bfix(ed|ing)?\b/i,
40
- /\berror\b/i,
41
- /\broot cause\b/i,
42
- /\bwork-?around\b/i,
43
- /\bregression\b/i,
44
- /\bstack trace\b/i,
45
- ],
46
- },
47
- {
48
- category: 'preference',
49
- weight: 0.7,
50
- patterns: [
51
- /\bprefer(ence|red|s)?\b/i,
52
- /\bstyle\b/i,
53
- /\bdon't like\b/i,
54
- /\bfavor\b/i,
55
- /\brather\b/i,
56
- ],
57
- },
58
- {
59
- category: 'procedure',
60
- weight: 0.75,
61
- patterns: [
62
- /\bstep[- ]?by[- ]?step\b/i,
63
- /\bworkflow\b/i,
64
- /\bprocedure\b/i,
65
- /\bhow to\b/i,
66
- /\brecipe\b/i,
67
- /\brun(ning)?\s+(the|this)?\s*command\b/i,
68
- ],
69
- },
70
- {
71
- category: 'context',
72
- weight: 0.6,
73
- patterns: [
74
- /\barchitecture\b/i,
75
- /\bdesign\b/i,
76
- /\bconstraint\b/i,
77
- /\brequirement\b/i,
78
- /\binfrastructure\b/i,
79
- /\bstack\b/i,
80
- ],
81
- },
82
- ];
83
- // ────────────────────────────────────────────────────────────
84
- // Helpers
85
- // ────────────────────────────────────────────────────────────
86
- /** Score text against category rules and return best match (if any). */
87
- function classifyText(text) {
88
- let best = null;
89
- for (const rule of CATEGORY_RULES) {
90
- let hits = 0;
91
- for (const pat of rule.patterns) {
92
- if (pat.test(text))
93
- hits++;
94
- }
95
- if (hits === 0)
96
- continue;
97
- const confidence = Math.min(1, (hits / rule.patterns.length) * rule.weight);
98
- if (!best || confidence > best.confidence) {
99
- best = { category: rule.category, confidence };
100
- }
101
- }
102
- return best;
103
- }
104
- /** Derive a short title from the first meaningful sentence. */
105
- function deriveTitle(text) {
106
- const firstLine = text.split(/\n/).find((l) => l.trim().length > 10) ?? text;
107
- const trimmed = firstLine.replace(/^#+\s*/, '').trim();
108
- return trimmed.length > 120 ? trimmed.slice(0, 117) + '...' : trimmed;
109
- }
110
- /** Extract simple tags from text. */
111
- function deriveTags(text) {
112
- const tags = new Set();
113
- // Language mentions
114
- const langs = text.match(/\b(TypeScript|JavaScript|Python|Rust|Go|Java|C\+\+|Ruby|Swift|Kotlin)\b/gi);
115
- if (langs)
116
- langs.forEach((l) => tags.add(l.toLowerCase()));
117
- // Framework mentions
118
- const frameworks = text.match(/\b(React|Next\.?js|Vue|Angular|Express|Django|Flask|Spring|Rails)\b/gi);
119
- if (frameworks)
120
- frameworks.forEach((f) => tags.add(f.toLowerCase()));
121
- // Tool mentions
122
- const tools = text.match(/\b(Docker|Kubernetes|Terraform|AWS|GCP|Azure|GitHub|GitLab)\b/gi);
123
- if (tools)
124
- tools.forEach((t) => tags.add(t.toLowerCase()));
125
- return [...tags].slice(0, 10);
126
- }
127
- // ────────────────────────────────────────────────────────────
128
- // Minimum thresholds
129
- // ────────────────────────────────────────────────────────────
130
- /** Minimum text length to consider for extraction. */
131
- const MIN_TEXT_LENGTH = 80;
132
- /** Minimum confidence to accept a classification. */
133
- const MIN_CONFIDENCE = 0.15;
134
- // ────────────────────────────────────────────────────────────
135
- // Public API
136
- // ────────────────────────────────────────────────────────────
137
- /**
138
- * Extract learnings from a set of text parts for a single message.
139
- *
140
- * Each qualifying text part that passes heuristic classification
141
- * becomes one ExtractionResult.
142
- */
143
- export function extractFromParts(input, parts) {
144
- const results = [];
145
- for (const part of parts) {
146
- if (part.type !== 'text' || !part.text)
147
- continue;
148
- if (part.text.length < MIN_TEXT_LENGTH)
149
- continue;
150
- const classification = classifyText(part.text);
151
- if (!classification || classification.confidence < MIN_CONFIDENCE)
152
- continue;
153
- results.push({
154
- sessionID: input.sessionID,
155
- messageID: input.messageID,
156
- category: classification.category,
157
- title: deriveTitle(part.text),
158
- body: part.text,
159
- tags: deriveTags(part.text),
160
- importance: classification.confidence,
161
- source: 'session',
162
- });
163
- }
164
- return results;
165
- }
166
- /**
167
- * High-level: extract from a message by reading its parts from storage.
168
- */
169
- export async function extractFromMessage(input) {
170
- const parts = await listParts(input.messageID);
171
- return extractFromParts(input, parts);
172
- }
@@ -1,40 +0,0 @@
1
- /**
2
- * OpenCode Storage Readers
3
- *
4
- * Reads OpenCode's native JSON storage from ~/.local/share/opencode/storage/
5
- * with adapters for project, session, message, and part entities.
6
- */
7
- import type { OCProject, OCSession, OCMessage, OCPart } from './types.js';
8
- /**
9
- * Find the project record whose worktree matches `directory`.
10
- * The project hash is a SHA-1 of the worktree path.
11
- */
12
- export declare function findProject(directory: string): Promise<OCProject | null>;
13
- /**
14
- * List all known projects.
15
- */
16
- export declare function listProjects(): Promise<OCProject[]>;
17
- /**
18
- * List sessions for a given project hash.
19
- */
20
- export declare function listSessions(projectID: string): Promise<OCSession[]>;
21
- /**
22
- * Get a single session by its ID within a project.
23
- */
24
- export declare function getSession(projectID: string, sessionID: string): Promise<OCSession | null>;
25
- /**
26
- * List all messages in a session, sorted by creation time.
27
- */
28
- export declare function listMessages(sessionID: string): Promise<OCMessage[]>;
29
- /**
30
- * Get a single message by ID.
31
- */
32
- export declare function getMessage(sessionID: string, messageID: string): Promise<OCMessage | null>;
33
- /**
34
- * List all parts for a given message, sorted by start time.
35
- */
36
- export declare function listParts(messageID: string): Promise<OCPart[]>;
37
- /**
38
- * Get a single part by ID.
39
- */
40
- export declare function getPart(messageID: string, partID: string): Promise<OCPart | null>;
@@ -1,157 +0,0 @@
1
- /**
2
- * OpenCode Storage Readers
3
- *
4
- * Reads OpenCode's native JSON storage from ~/.local/share/opencode/storage/
5
- * with adapters for project, session, message, and part entities.
6
- */
7
- import { readFile, readdir, stat, realpath } from 'fs/promises';
8
- import { join } from 'path';
9
- import { homedir } from 'os';
10
- // ── Storage base path ───────────────────────────────────────────
11
- function getStorageBase() {
12
- return join(homedir(), '.local', 'share', 'opencode', 'storage');
13
- }
14
- // ── Helpers ─────────────────────────────────────────────────────
15
- async function readJsonFile(filePath) {
16
- const raw = await readFile(filePath, 'utf-8');
17
- return JSON.parse(raw);
18
- }
19
- async function readAllJsonInDir(dirPath) {
20
- try {
21
- const entries = await readdir(dirPath);
22
- const jsonFiles = entries.filter((e) => e.endsWith('.json'));
23
- const results = [];
24
- for (const file of jsonFiles) {
25
- try {
26
- const data = await readJsonFile(join(dirPath, file));
27
- results.push(data);
28
- }
29
- catch {
30
- // Skip malformed files
31
- }
32
- }
33
- return results;
34
- }
35
- catch {
36
- return [];
37
- }
38
- }
39
- async function dirExists(dirPath) {
40
- try {
41
- const s = await stat(dirPath);
42
- return s.isDirectory();
43
- }
44
- catch {
45
- return false;
46
- }
47
- }
48
- function getPartStartTime(part) {
49
- const start = part.time?.start;
50
- return typeof start === 'number' ? start : Number.POSITIVE_INFINITY;
51
- }
52
- // ── Project Reader ──────────────────────────────────────────────
53
- /**
54
- * Find the project record whose worktree matches `directory`.
55
- * The project hash is a SHA-1 of the worktree path.
56
- */
57
- export async function findProject(directory) {
58
- const projectDir = join(getStorageBase(), 'project');
59
- const projects = await readAllJsonInDir(projectDir);
60
- // Normalise both sides so symlinks / trailing slashes don't cause mismatches
61
- let normalisedInput;
62
- try {
63
- normalisedInput = await realpath(directory);
64
- }
65
- catch {
66
- normalisedInput = directory; // fallback when directory doesn't exist yet
67
- }
68
- for (const p of projects) {
69
- let normalisedWorktree;
70
- try {
71
- normalisedWorktree = await realpath(p.worktree);
72
- }
73
- catch {
74
- normalisedWorktree = p.worktree;
75
- }
76
- if (normalisedWorktree === normalisedInput || p.worktree === directory) {
77
- return p;
78
- }
79
- }
80
- return null;
81
- }
82
- /**
83
- * List all known projects.
84
- */
85
- export async function listProjects() {
86
- return readAllJsonInDir(join(getStorageBase(), 'project'));
87
- }
88
- // ── Session Reader ──────────────────────────────────────────────
89
- /**
90
- * List sessions for a given project hash.
91
- */
92
- export async function listSessions(projectID) {
93
- const sessionDir = join(getStorageBase(), 'session', projectID);
94
- return readAllJsonInDir(sessionDir);
95
- }
96
- /**
97
- * Get a single session by its ID within a project.
98
- */
99
- export async function getSession(projectID, sessionID) {
100
- const sessionDir = join(getStorageBase(), 'session', projectID);
101
- try {
102
- return await readJsonFile(join(sessionDir, `${sessionID}.json`));
103
- }
104
- catch {
105
- return null;
106
- }
107
- }
108
- // ── Message Reader ──────────────────────────────────────────────
109
- /**
110
- * List all messages in a session, sorted by creation time.
111
- */
112
- export async function listMessages(sessionID) {
113
- const msgDir = join(getStorageBase(), 'message', sessionID);
114
- const messages = await readAllJsonInDir(msgDir);
115
- return messages.sort((a, b) => a.time.created - b.time.created);
116
- }
117
- /**
118
- * Get a single message by ID.
119
- */
120
- export async function getMessage(sessionID, messageID) {
121
- const msgDir = join(getStorageBase(), 'message', sessionID);
122
- try {
123
- return await readJsonFile(join(msgDir, `${messageID}.json`));
124
- }
125
- catch {
126
- return null;
127
- }
128
- }
129
- // ── Part Reader ─────────────────────────────────────────────────
130
- /**
131
- * List all parts for a given message, sorted by start time.
132
- */
133
- export async function listParts(messageID) {
134
- const partDir = join(getStorageBase(), 'part', messageID);
135
- if (!(await dirExists(partDir)))
136
- return [];
137
- const parts = await readAllJsonInDir(partDir);
138
- return parts.sort((a, b) => {
139
- const timeDelta = getPartStartTime(a) - getPartStartTime(b);
140
- if (timeDelta !== 0) {
141
- return timeDelta;
142
- }
143
- return a.id.localeCompare(b.id);
144
- });
145
- }
146
- /**
147
- * Get a single part by ID.
148
- */
149
- export async function getPart(messageID, partID) {
150
- const partDir = join(getStorageBase(), 'part', messageID);
151
- try {
152
- return await readJsonFile(join(partDir, `${partID}.json`));
153
- }
154
- catch {
155
- return null;
156
- }
157
- }
@@ -1,16 +0,0 @@
1
- /**
2
- * thinking-extractor.ts — Extracts learnings from "thinking" / reasoning blocks.
3
- *
4
- * OpenCode stores extended-thinking or chain-of-thought content in parts
5
- * with type "reasoning" or "thinking". These often contain high-signal insights about
6
- * decision making and problem solving that are worth capturing.
7
- */
8
- import type { ExtractionInput, ExtractionResult, OCPart } from './types.js';
9
- /**
10
- * Extract learnings specifically from reasoning/thinking parts.
11
- */
12
- export declare function extractFromThinkingParts(input: ExtractionInput, parts: OCPart[]): ExtractionResult[];
13
- /**
14
- * High-level: extract thinking-based learnings from a message.
15
- */
16
- export declare function extractThinkingFromMessage(input: ExtractionInput): Promise<ExtractionResult[]>;