@syntesseraai/opencode-feature-factory 0.4.3 → 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.
- package/package.json +2 -2
- package/dist/discovery.test.d.ts +0 -10
- package/dist/discovery.test.js +0 -97
- package/dist/local-recall/daemon-controller.d.ts +0 -51
- package/dist/local-recall/daemon-controller.js +0 -166
- package/dist/local-recall/daemon.d.ts +0 -35
- package/dist/local-recall/daemon.js +0 -262
- package/dist/local-recall/index-state.d.ts +0 -14
- package/dist/local-recall/index-state.js +0 -76
- package/dist/local-recall/index.d.ts +0 -20
- package/dist/local-recall/index.js +0 -27
- package/dist/local-recall/mcp-server.d.ts +0 -34
- package/dist/local-recall/mcp-server.js +0 -194
- package/dist/local-recall/mcp-stdio-server.d.ts +0 -4
- package/dist/local-recall/mcp-stdio-server.js +0 -225
- package/dist/local-recall/mcp-tools.d.ts +0 -103
- package/dist/local-recall/mcp-tools.js +0 -187
- package/dist/local-recall/memory-service.d.ts +0 -32
- package/dist/local-recall/memory-service.js +0 -156
- package/dist/local-recall/model-router.d.ts +0 -23
- package/dist/local-recall/model-router.js +0 -41
- package/dist/local-recall/processed-log.d.ts +0 -41
- package/dist/local-recall/processed-log.js +0 -85
- package/dist/local-recall/prompt-injection.d.ts +0 -2
- package/dist/local-recall/prompt-injection.js +0 -194
- package/dist/local-recall/session-extractor.d.ts +0 -19
- package/dist/local-recall/session-extractor.js +0 -172
- package/dist/local-recall/storage-reader.d.ts +0 -40
- package/dist/local-recall/storage-reader.js +0 -157
- package/dist/local-recall/thinking-extractor.d.ts +0 -16
- package/dist/local-recall/thinking-extractor.js +0 -132
- package/dist/local-recall/types.d.ts +0 -152
- package/dist/local-recall/types.js +0 -7
- package/dist/local-recall/vector/embedding-provider.d.ts +0 -37
- package/dist/local-recall/vector/embedding-provider.js +0 -184
- package/dist/local-recall/vector/orama-index.d.ts +0 -39
- package/dist/local-recall/vector/orama-index.js +0 -408
- package/dist/local-recall/vector/types.d.ts +0 -33
- package/dist/local-recall/vector/types.js +0 -1
- package/dist/output.test.d.ts +0 -8
- package/dist/output.test.js +0 -205
- package/dist/plugins/ff-reviews-delete-plugin.d.ts +0 -2
- package/dist/plugins/ff-reviews-delete-plugin.js +0 -32
- package/dist/quality-gate-config.test.d.ts +0 -9
- package/dist/quality-gate-config.test.js +0 -164
- package/dist/stop-quality-gate.test.d.ts +0 -8
- package/dist/stop-quality-gate.test.js +0 -549
|
@@ -1,132 +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 { listParts } from './storage-reader.js';
|
|
9
|
-
const THINKING_SIGNALS = [
|
|
10
|
-
{
|
|
11
|
-
category: 'decision',
|
|
12
|
-
weight: 0.9,
|
|
13
|
-
patterns: [
|
|
14
|
-
/\blet me think about\b/i,
|
|
15
|
-
/\bthe (better|right|correct) approach\b/i,
|
|
16
|
-
/\bI('ll| will) go with\b/i,
|
|
17
|
-
/\bweighing (the )?(options|trade-?offs)\b/i,
|
|
18
|
-
/\boption [A-D1-4] (is|seems|looks)\b/i,
|
|
19
|
-
/\bpros and cons\b/i,
|
|
20
|
-
],
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
category: 'debugging',
|
|
24
|
-
weight: 0.85,
|
|
25
|
-
patterns: [
|
|
26
|
-
/\bthe (issue|problem|bug) (is|was|seems)\b/i,
|
|
27
|
-
/\broot cause\b/i,
|
|
28
|
-
/\bthis (fails|breaks|errors) because\b/i,
|
|
29
|
-
/\bI (notice|see|found) (that |the )?(error|issue|bug)\b/i,
|
|
30
|
-
/\bstack trace (shows|indicates|reveals)\b/i,
|
|
31
|
-
],
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
category: 'pattern',
|
|
35
|
-
weight: 0.8,
|
|
36
|
-
patterns: [
|
|
37
|
-
/\bthe (standard|common|typical) (pattern|approach|way)\b/i,
|
|
38
|
-
/\bbest practice\b/i,
|
|
39
|
-
/\bfollow(ing)? the (same |existing )?(pattern|convention)\b/i,
|
|
40
|
-
/\bthis is (a |the )?(standard|idiomatic)\b/i,
|
|
41
|
-
],
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
category: 'context',
|
|
45
|
-
weight: 0.7,
|
|
46
|
-
patterns: [
|
|
47
|
-
/\blooking at the (code|codebase|architecture|structure)\b/i,
|
|
48
|
-
/\bthe (project|repo|codebase) (uses|follows|has)\b/i,
|
|
49
|
-
/\bbased on (the |my )?understanding\b/i,
|
|
50
|
-
/\bthe (current|existing) (setup|config|architecture)\b/i,
|
|
51
|
-
],
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
category: 'procedure',
|
|
55
|
-
weight: 0.75,
|
|
56
|
-
patterns: [
|
|
57
|
-
/\bfirst[\s\S]+then[\s\S]+finally\b/i,
|
|
58
|
-
/\bstep \d/i,
|
|
59
|
-
/\bthe (process|procedure|workflow) (is|involves)\b/i,
|
|
60
|
-
/\bI need to:?\s*\n/i,
|
|
61
|
-
],
|
|
62
|
-
},
|
|
63
|
-
];
|
|
64
|
-
// ────────────────────────────────────────────────────────────
|
|
65
|
-
// Analysis
|
|
66
|
-
// ────────────────────────────────────────────────────────────
|
|
67
|
-
const MIN_THINKING_LENGTH = 120;
|
|
68
|
-
const MIN_THINKING_CONFIDENCE = 0.2;
|
|
69
|
-
function classifyThinking(text) {
|
|
70
|
-
let best = null;
|
|
71
|
-
for (const signal of THINKING_SIGNALS) {
|
|
72
|
-
let hits = 0;
|
|
73
|
-
for (const pat of signal.patterns) {
|
|
74
|
-
if (pat.test(text))
|
|
75
|
-
hits++;
|
|
76
|
-
}
|
|
77
|
-
if (hits === 0)
|
|
78
|
-
continue;
|
|
79
|
-
const confidence = Math.min(1, (hits / signal.patterns.length) * signal.weight);
|
|
80
|
-
if (!best || confidence > best.confidence) {
|
|
81
|
-
best = { category: signal.category, confidence };
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return best;
|
|
85
|
-
}
|
|
86
|
-
function deriveThinkingTitle(text) {
|
|
87
|
-
// Attempt to find a summary-like sentence
|
|
88
|
-
const sentences = text.split(/[.!?]\s+/).filter((s) => s.trim().length > 15);
|
|
89
|
-
const summary = sentences.find((s) => /\b(so|therefore|conclusion|result|decided|approach)\b/i.test(s)) ??
|
|
90
|
-
sentences[0];
|
|
91
|
-
if (!summary)
|
|
92
|
-
return 'Thinking insight';
|
|
93
|
-
const clean = summary.replace(/^[\s*#-]+/, '').trim();
|
|
94
|
-
return clean.length > 120 ? clean.slice(0, 117) + '...' : clean;
|
|
95
|
-
}
|
|
96
|
-
// ────────────────────────────────────────────────────────────
|
|
97
|
-
// Public API
|
|
98
|
-
// ────────────────────────────────────────────────────────────
|
|
99
|
-
/**
|
|
100
|
-
* Extract learnings specifically from reasoning/thinking parts.
|
|
101
|
-
*/
|
|
102
|
-
export function extractFromThinkingParts(input, parts) {
|
|
103
|
-
const results = [];
|
|
104
|
-
for (const part of parts) {
|
|
105
|
-
// Accept reasoning/thinking type parts (extended thinking)
|
|
106
|
-
if ((part.type !== 'reasoning' && part.type !== 'thinking') || !part.text)
|
|
107
|
-
continue;
|
|
108
|
-
if (part.text.length < MIN_THINKING_LENGTH)
|
|
109
|
-
continue;
|
|
110
|
-
const classification = classifyThinking(part.text);
|
|
111
|
-
if (!classification || classification.confidence < MIN_THINKING_CONFIDENCE)
|
|
112
|
-
continue;
|
|
113
|
-
results.push({
|
|
114
|
-
sessionID: input.sessionID,
|
|
115
|
-
messageID: input.messageID,
|
|
116
|
-
category: classification.category,
|
|
117
|
-
title: deriveThinkingTitle(part.text),
|
|
118
|
-
body: part.text,
|
|
119
|
-
tags: ['thinking', 'reasoning'],
|
|
120
|
-
importance: Math.min(1, classification.confidence * 1.1), // slight boost for thinking
|
|
121
|
-
source: 'thinking',
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
return results;
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* High-level: extract thinking-based learnings from a message.
|
|
128
|
-
*/
|
|
129
|
-
export async function extractThinkingFromMessage(input) {
|
|
130
|
-
const parts = await listParts(input.messageID);
|
|
131
|
-
return extractFromThinkingParts(input, parts);
|
|
132
|
-
}
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Local-Recall MCP-Only Memory Types
|
|
3
|
-
*
|
|
4
|
-
* Defines the domain types for reading OpenCode's native storage
|
|
5
|
-
* and exposing memory retrieval via MCP tools.
|
|
6
|
-
*/
|
|
7
|
-
/** Raw project record from ~/.local/share/opencode/storage/project/*.json */
|
|
8
|
-
export interface OCProject {
|
|
9
|
-
id: string;
|
|
10
|
-
worktree: string;
|
|
11
|
-
vcs: string;
|
|
12
|
-
sandboxes: unknown[];
|
|
13
|
-
time: {
|
|
14
|
-
created: number;
|
|
15
|
-
updated: number;
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
/** Raw session record from ~/.local/share/opencode/storage/session/<projectHash>/ses_*.json */
|
|
19
|
-
export interface OCSession {
|
|
20
|
-
id: string;
|
|
21
|
-
slug: string;
|
|
22
|
-
version: string;
|
|
23
|
-
projectID: string;
|
|
24
|
-
directory: string;
|
|
25
|
-
title: string;
|
|
26
|
-
time: {
|
|
27
|
-
created: number;
|
|
28
|
-
updated: number;
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
/** Raw message record from ~/.local/share/opencode/storage/message/<sessionID>/msg_*.json */
|
|
32
|
-
export interface OCMessage {
|
|
33
|
-
id: string;
|
|
34
|
-
sessionID: string;
|
|
35
|
-
role: 'user' | 'assistant' | 'thinking';
|
|
36
|
-
time: {
|
|
37
|
-
created: number;
|
|
38
|
-
};
|
|
39
|
-
summary: {
|
|
40
|
-
title: string;
|
|
41
|
-
diffs: unknown[];
|
|
42
|
-
};
|
|
43
|
-
agent: string;
|
|
44
|
-
model: {
|
|
45
|
-
providerID: string;
|
|
46
|
-
modelID: string;
|
|
47
|
-
};
|
|
48
|
-
variant: string;
|
|
49
|
-
}
|
|
50
|
-
/** Raw part record from ~/.local/share/opencode/storage/part/<messageID>/prt_*.json */
|
|
51
|
-
export interface OCPart {
|
|
52
|
-
id: string;
|
|
53
|
-
sessionID: string;
|
|
54
|
-
messageID: string;
|
|
55
|
-
type: string;
|
|
56
|
-
text?: string;
|
|
57
|
-
synthetic?: boolean;
|
|
58
|
-
time?: {
|
|
59
|
-
start?: number;
|
|
60
|
-
end?: number;
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
/** A memory extracted from a conversation turn */
|
|
64
|
-
export interface Memory {
|
|
65
|
-
id: string;
|
|
66
|
-
sessionID: string;
|
|
67
|
-
messageID: string;
|
|
68
|
-
/** Describes the kind of insight */
|
|
69
|
-
category: MemoryCategory;
|
|
70
|
-
/** Short human-readable summary */
|
|
71
|
-
title: string;
|
|
72
|
-
/** Full text of the extracted memory */
|
|
73
|
-
body: string;
|
|
74
|
-
/** Searchable tags */
|
|
75
|
-
tags: string[];
|
|
76
|
-
/** 0-1 relevance weight */
|
|
77
|
-
importance: number;
|
|
78
|
-
/** Epoch ms extraction timestamp */
|
|
79
|
-
createdAt: number;
|
|
80
|
-
/** Which model was used for extraction */
|
|
81
|
-
extractedBy: string;
|
|
82
|
-
}
|
|
83
|
-
export type MemoryCategory = 'pattern' | 'decision' | 'debugging' | 'preference' | 'context' | 'procedure';
|
|
84
|
-
/** Criteria for searching memories */
|
|
85
|
-
export interface SearchCriteria {
|
|
86
|
-
query: string;
|
|
87
|
-
category?: MemoryCategory;
|
|
88
|
-
tags?: string[];
|
|
89
|
-
sessionID?: string;
|
|
90
|
-
minImportance?: number;
|
|
91
|
-
limit?: number;
|
|
92
|
-
}
|
|
93
|
-
/** Result metadata returned from search (excludes full body for brevity) */
|
|
94
|
-
export interface MemorySearchResult {
|
|
95
|
-
id: string;
|
|
96
|
-
sessionID: string;
|
|
97
|
-
category: MemoryCategory;
|
|
98
|
-
title: string;
|
|
99
|
-
tags: string[];
|
|
100
|
-
importance: number;
|
|
101
|
-
createdAt: number;
|
|
102
|
-
/** Relevance score from search ranking */
|
|
103
|
-
relevance: number;
|
|
104
|
-
}
|
|
105
|
-
/** Identifies a message to extract from */
|
|
106
|
-
export interface ExtractionInput {
|
|
107
|
-
sessionID: string;
|
|
108
|
-
messageID: string;
|
|
109
|
-
}
|
|
110
|
-
/** A single extracted learning before it becomes a full Memory */
|
|
111
|
-
export interface ExtractionResult {
|
|
112
|
-
sessionID: string;
|
|
113
|
-
messageID: string;
|
|
114
|
-
category: MemoryCategory;
|
|
115
|
-
title: string;
|
|
116
|
-
body: string;
|
|
117
|
-
tags: string[];
|
|
118
|
-
importance: number;
|
|
119
|
-
/** Where the extraction came from — used to generate logical IDs */
|
|
120
|
-
source: 'session' | 'thinking';
|
|
121
|
-
}
|
|
122
|
-
interface ProcessedMessageEntryBase {
|
|
123
|
-
messageID: string;
|
|
124
|
-
processedAt: number;
|
|
125
|
-
/** SHA-256 hex hash of the concatenated extracted bodies for content-level idempotency */
|
|
126
|
-
contentHash: string;
|
|
127
|
-
}
|
|
128
|
-
/** Tracks which messages have already been processed */
|
|
129
|
-
export type ProcessedMessageEntry = (ProcessedMessageEntryBase & {
|
|
130
|
-
status: 'success';
|
|
131
|
-
memoriesCreated: number;
|
|
132
|
-
failure?: undefined;
|
|
133
|
-
}) | (ProcessedMessageEntryBase & {
|
|
134
|
-
status: 'failed';
|
|
135
|
-
memoriesCreated: 0;
|
|
136
|
-
failure: string;
|
|
137
|
-
}) | (ProcessedMessageEntryBase & {
|
|
138
|
-
status?: undefined;
|
|
139
|
-
memoriesCreated: number;
|
|
140
|
-
failure?: undefined;
|
|
141
|
-
});
|
|
142
|
-
/** Persistent daemon-level failures that are not tied to a specific message record. */
|
|
143
|
-
export type ProcessedFailureEntry = {
|
|
144
|
-
kind: 'failure';
|
|
145
|
-
scope: 'project' | 'session' | 'extraction';
|
|
146
|
-
processedAt: number;
|
|
147
|
-
failure: string;
|
|
148
|
-
directory?: string;
|
|
149
|
-
sessionID?: string;
|
|
150
|
-
};
|
|
151
|
-
export type ProcessedEntry = ProcessedMessageEntry | ProcessedFailureEntry;
|
|
152
|
-
export {};
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import type { EmbeddingProvider, EmbeddingProviderName } from './types.js';
|
|
2
|
-
interface EmbeddingProviderEnv extends NodeJS.ProcessEnv {
|
|
3
|
-
FF_LOCAL_RECALL_EMBEDDING_PROVIDER?: string;
|
|
4
|
-
FF_LOCAL_RECALL_OLLAMA_URL?: string;
|
|
5
|
-
FF_LOCAL_RECALL_OLLAMA_MODEL?: string;
|
|
6
|
-
FF_LOCAL_RECALL_OPENAI_URL?: string;
|
|
7
|
-
FF_LOCAL_RECALL_OPENAI_MODEL?: string;
|
|
8
|
-
OPENAI_API_KEY?: string;
|
|
9
|
-
}
|
|
10
|
-
export declare class OllamaEmbeddingProvider implements EmbeddingProvider {
|
|
11
|
-
readonly name: EmbeddingProviderName;
|
|
12
|
-
readonly model: string;
|
|
13
|
-
readonly baseURL: string;
|
|
14
|
-
dimensions?: number;
|
|
15
|
-
constructor(options?: {
|
|
16
|
-
model?: string;
|
|
17
|
-
baseURL?: string;
|
|
18
|
-
});
|
|
19
|
-
embed(input: string[]): Promise<number[][]>;
|
|
20
|
-
private embedBatch;
|
|
21
|
-
private embedLegacy;
|
|
22
|
-
}
|
|
23
|
-
export declare class OpenAIEmbeddingProvider implements EmbeddingProvider {
|
|
24
|
-
readonly name: EmbeddingProviderName;
|
|
25
|
-
readonly model: string;
|
|
26
|
-
readonly baseURL: string;
|
|
27
|
-
readonly apiKey: string;
|
|
28
|
-
dimensions?: number;
|
|
29
|
-
constructor(options: {
|
|
30
|
-
apiKey: string;
|
|
31
|
-
model?: string;
|
|
32
|
-
baseURL?: string;
|
|
33
|
-
});
|
|
34
|
-
embed(input: string[]): Promise<number[][]>;
|
|
35
|
-
}
|
|
36
|
-
export declare function createEmbeddingProvider(env?: EmbeddingProviderEnv): EmbeddingProvider;
|
|
37
|
-
export {};
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
const DEFAULT_OLLAMA_URL = 'http://127.0.0.1:11434';
|
|
2
|
-
const DEFAULT_OLLAMA_MODEL = 'nomic-embed-text';
|
|
3
|
-
const DEFAULT_OPENAI_URL = 'https://api.openai.com/v1';
|
|
4
|
-
const DEFAULT_OPENAI_MODEL = 'text-embedding-3-small';
|
|
5
|
-
function normalizeBaseURL(url) {
|
|
6
|
-
return url.endsWith('/') ? url.slice(0, -1) : url;
|
|
7
|
-
}
|
|
8
|
-
function toErrorMessage(error) {
|
|
9
|
-
return error instanceof Error ? error.message : String(error);
|
|
10
|
-
}
|
|
11
|
-
function assertEmbeddingShape(vectors) {
|
|
12
|
-
if (!Array.isArray(vectors)) {
|
|
13
|
-
throw new Error('Embedding response was not an array');
|
|
14
|
-
}
|
|
15
|
-
for (const vector of vectors) {
|
|
16
|
-
if (!Array.isArray(vector) || !vector.every((value) => typeof value === 'number')) {
|
|
17
|
-
throw new Error('Embedding response contained invalid vectors');
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
return vectors;
|
|
21
|
-
}
|
|
22
|
-
function validateDimensions(vectors, currentDimensions) {
|
|
23
|
-
const first = vectors[0];
|
|
24
|
-
if (!first) {
|
|
25
|
-
throw new Error('Embedding provider returned no vectors');
|
|
26
|
-
}
|
|
27
|
-
const nextDimensions = first.length;
|
|
28
|
-
if (nextDimensions === 0) {
|
|
29
|
-
throw new Error('Embedding provider returned empty vectors');
|
|
30
|
-
}
|
|
31
|
-
for (const vector of vectors) {
|
|
32
|
-
if (vector.length !== nextDimensions) {
|
|
33
|
-
throw new Error('Embedding provider returned mixed vector dimensions');
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
if (currentDimensions && currentDimensions !== nextDimensions) {
|
|
37
|
-
throw new Error(`Embedding dimensions changed from ${currentDimensions} to ${nextDimensions}`);
|
|
38
|
-
}
|
|
39
|
-
return nextDimensions;
|
|
40
|
-
}
|
|
41
|
-
async function parseError(response) {
|
|
42
|
-
try {
|
|
43
|
-
const text = await response.text();
|
|
44
|
-
if (!text) {
|
|
45
|
-
return `${response.status} ${response.statusText}`;
|
|
46
|
-
}
|
|
47
|
-
return `${response.status} ${response.statusText}: ${text}`;
|
|
48
|
-
}
|
|
49
|
-
catch {
|
|
50
|
-
return `${response.status} ${response.statusText}`;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
export class OllamaEmbeddingProvider {
|
|
54
|
-
name = 'ollama';
|
|
55
|
-
model;
|
|
56
|
-
baseURL;
|
|
57
|
-
dimensions;
|
|
58
|
-
constructor(options) {
|
|
59
|
-
this.model = options?.model ?? DEFAULT_OLLAMA_MODEL;
|
|
60
|
-
this.baseURL = normalizeBaseURL(options?.baseURL ?? DEFAULT_OLLAMA_URL);
|
|
61
|
-
}
|
|
62
|
-
async embed(input) {
|
|
63
|
-
if (input.length === 0) {
|
|
64
|
-
return [];
|
|
65
|
-
}
|
|
66
|
-
let vectors = null;
|
|
67
|
-
let primaryError = null;
|
|
68
|
-
try {
|
|
69
|
-
vectors = await this.embedBatch(input);
|
|
70
|
-
}
|
|
71
|
-
catch (error) {
|
|
72
|
-
primaryError = toErrorMessage(error);
|
|
73
|
-
}
|
|
74
|
-
if (!vectors) {
|
|
75
|
-
try {
|
|
76
|
-
const legacyVectors = await Promise.all(input.map((value) => this.embedLegacy(value)));
|
|
77
|
-
vectors = legacyVectors;
|
|
78
|
-
}
|
|
79
|
-
catch (error) {
|
|
80
|
-
const fallbackError = toErrorMessage(error);
|
|
81
|
-
throw new Error(`Failed to fetch embeddings from Ollama. batch=${primaryError ?? 'n/a'} fallback=${fallbackError}`);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
const dimensions = validateDimensions(vectors, this.dimensions);
|
|
85
|
-
this.dimensions = dimensions;
|
|
86
|
-
return vectors;
|
|
87
|
-
}
|
|
88
|
-
async embedBatch(input) {
|
|
89
|
-
const response = await fetch(`${this.baseURL}/api/embed`, {
|
|
90
|
-
method: 'POST',
|
|
91
|
-
headers: {
|
|
92
|
-
'Content-Type': 'application/json',
|
|
93
|
-
},
|
|
94
|
-
body: JSON.stringify({
|
|
95
|
-
model: this.model,
|
|
96
|
-
input,
|
|
97
|
-
}),
|
|
98
|
-
});
|
|
99
|
-
if (!response.ok) {
|
|
100
|
-
throw new Error(await parseError(response));
|
|
101
|
-
}
|
|
102
|
-
const payload = (await response.json());
|
|
103
|
-
return assertEmbeddingShape(payload.embeddings);
|
|
104
|
-
}
|
|
105
|
-
async embedLegacy(input) {
|
|
106
|
-
const response = await fetch(`${this.baseURL}/api/embeddings`, {
|
|
107
|
-
method: 'POST',
|
|
108
|
-
headers: {
|
|
109
|
-
'Content-Type': 'application/json',
|
|
110
|
-
},
|
|
111
|
-
body: JSON.stringify({
|
|
112
|
-
model: this.model,
|
|
113
|
-
prompt: input,
|
|
114
|
-
}),
|
|
115
|
-
});
|
|
116
|
-
if (!response.ok) {
|
|
117
|
-
throw new Error(await parseError(response));
|
|
118
|
-
}
|
|
119
|
-
const payload = (await response.json());
|
|
120
|
-
const [embedding] = assertEmbeddingShape([payload.embedding]);
|
|
121
|
-
return embedding;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
export class OpenAIEmbeddingProvider {
|
|
125
|
-
name = 'openai';
|
|
126
|
-
model;
|
|
127
|
-
baseURL;
|
|
128
|
-
apiKey;
|
|
129
|
-
dimensions;
|
|
130
|
-
constructor(options) {
|
|
131
|
-
this.apiKey = options.apiKey;
|
|
132
|
-
this.model = options.model ?? DEFAULT_OPENAI_MODEL;
|
|
133
|
-
this.baseURL = normalizeBaseURL(options.baseURL ?? DEFAULT_OPENAI_URL);
|
|
134
|
-
}
|
|
135
|
-
async embed(input) {
|
|
136
|
-
if (input.length === 0) {
|
|
137
|
-
return [];
|
|
138
|
-
}
|
|
139
|
-
const response = await fetch(`${this.baseURL}/embeddings`, {
|
|
140
|
-
method: 'POST',
|
|
141
|
-
headers: {
|
|
142
|
-
'Content-Type': 'application/json',
|
|
143
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
144
|
-
},
|
|
145
|
-
body: JSON.stringify({
|
|
146
|
-
model: this.model,
|
|
147
|
-
input,
|
|
148
|
-
}),
|
|
149
|
-
});
|
|
150
|
-
if (!response.ok) {
|
|
151
|
-
throw new Error(await parseError(response));
|
|
152
|
-
}
|
|
153
|
-
const payload = (await response.json());
|
|
154
|
-
const embeddings = assertEmbeddingShape(payload.data?.map((entry) => entry.embedding) ?? []);
|
|
155
|
-
const dimensions = validateDimensions(embeddings, this.dimensions);
|
|
156
|
-
this.dimensions = dimensions;
|
|
157
|
-
return embeddings;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
function getRequestedProvider(env) {
|
|
161
|
-
const value = (env.FF_LOCAL_RECALL_EMBEDDING_PROVIDER ?? 'ollama').trim().toLowerCase();
|
|
162
|
-
if (value === 'openai') {
|
|
163
|
-
return 'openai';
|
|
164
|
-
}
|
|
165
|
-
return 'ollama';
|
|
166
|
-
}
|
|
167
|
-
export function createEmbeddingProvider(env = process.env) {
|
|
168
|
-
const provider = getRequestedProvider(env);
|
|
169
|
-
if (provider === 'openai') {
|
|
170
|
-
const apiKey = env.OPENAI_API_KEY;
|
|
171
|
-
if (!apiKey) {
|
|
172
|
-
throw new Error('OPENAI_API_KEY is required when FF_LOCAL_RECALL_EMBEDDING_PROVIDER=openai');
|
|
173
|
-
}
|
|
174
|
-
return new OpenAIEmbeddingProvider({
|
|
175
|
-
apiKey,
|
|
176
|
-
model: env.FF_LOCAL_RECALL_OPENAI_MODEL,
|
|
177
|
-
baseURL: env.FF_LOCAL_RECALL_OPENAI_URL,
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
return new OllamaEmbeddingProvider({
|
|
181
|
-
model: env.FF_LOCAL_RECALL_OLLAMA_MODEL,
|
|
182
|
-
baseURL: env.FF_LOCAL_RECALL_OLLAMA_URL,
|
|
183
|
-
});
|
|
184
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import type { Memory, MemorySearchResult, SearchCriteria } from '../types.js';
|
|
2
|
-
import type { EmbeddingProvider } from './types.js';
|
|
3
|
-
export declare class OramaMemoryIndex {
|
|
4
|
-
private readonly directory;
|
|
5
|
-
private readonly provider;
|
|
6
|
-
private db;
|
|
7
|
-
private readonly documents;
|
|
8
|
-
private readonly embedMaxChars;
|
|
9
|
-
private dimensions;
|
|
10
|
-
private updatedAt;
|
|
11
|
-
constructor(directory: string, provider: EmbeddingProvider);
|
|
12
|
-
initialize(): Promise<void>;
|
|
13
|
-
getStatus(): {
|
|
14
|
-
documents: number;
|
|
15
|
-
dimensions: number | null;
|
|
16
|
-
provider: string;
|
|
17
|
-
model: string;
|
|
18
|
-
updatedAt: string | null;
|
|
19
|
-
};
|
|
20
|
-
search(criteria: SearchCriteria): Promise<MemorySearchResult[]>;
|
|
21
|
-
upsertMemories(memories: Memory[]): Promise<number>;
|
|
22
|
-
removeMemories(memoryIDs: string[]): Promise<number>;
|
|
23
|
-
rebuild(memories: Memory[]): Promise<number>;
|
|
24
|
-
private get indexDir();
|
|
25
|
-
private get manifestPath();
|
|
26
|
-
private get documentsPath();
|
|
27
|
-
private buildSchema;
|
|
28
|
-
private ensureDB;
|
|
29
|
-
private rebuildDatabaseFromDocuments;
|
|
30
|
-
private hydrateDB;
|
|
31
|
-
private loadSnapshot;
|
|
32
|
-
private persistSnapshot;
|
|
33
|
-
private writeAtomic;
|
|
34
|
-
private memoryToEmbeddingInput;
|
|
35
|
-
private prepareEmbeddingInput;
|
|
36
|
-
private toDocument;
|
|
37
|
-
private assertEmbeddingDimensions;
|
|
38
|
-
private embedBatch;
|
|
39
|
-
}
|