@rohirik/openltm-core 2.8.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.
- package/README.md +67 -0
- package/assets/opencode/agents/aegis.md +211 -0
- package/assets/opencode/plugins/aegis.ts +3 -0
- package/assets/opencode/skills/AgentTrustBoundaries/ContextCrushDefense.md +104 -0
- package/assets/opencode/skills/AgentTrustBoundaries/SKILL.md +31 -0
- package/assets/opencode/skills/AgentTrustBoundaries/TrustBoundaryPatterns.md +114 -0
- package/assets/opencode/skills/AgentTrustBoundaries/Workflows/DefendContextCrush.md +27 -0
- package/assets/opencode/skills/AgentTrustBoundaries/Workflows/HandleUntrustedContent.md +27 -0
- package/assets/opencode/skills/CommandPathSafety/CommandInjectionPatterns.md +95 -0
- package/assets/opencode/skills/CommandPathSafety/PathTraversalAndInstallerSafety.md +106 -0
- package/assets/opencode/skills/CommandPathSafety/SKILL.md +31 -0
- package/assets/opencode/skills/CommandPathSafety/Workflows/EnforcePathBoundaries.md +27 -0
- package/assets/opencode/skills/CommandPathSafety/Workflows/HardenCommandExecution.md +27 -0
- package/assets/opencode/skills/SecretSafeHandling/CloudCredentialPatterns.md +106 -0
- package/assets/opencode/skills/SecretSafeHandling/SKILL.md +31 -0
- package/assets/opencode/skills/SecretSafeHandling/SecretHandlingPlaybook.md +102 -0
- package/assets/opencode/skills/SecretSafeHandling/Workflows/DesignSecretSafeFlow.md +27 -0
- package/assets/opencode/skills/SecretSafeHandling/Workflows/RemoveSecretExposure.md +27 -0
- package/package.json +41 -0
- package/src/__tests__/cli/claude.test.ts +122 -0
- package/src/__tests__/cli/detect.test.ts +91 -0
- package/src/__tests__/cli/install.test.ts +161 -0
- package/src/__tests__/cli/opencode.test.ts +169 -0
- package/src/__tests__/cli/pi.test.ts +113 -0
- package/src/__tests__/cli.test.ts +70 -0
- package/src/__tests__/events/crossProcess.test.ts +82 -0
- package/src/__tests__/events/index.test.ts +32 -0
- package/src/__tests__/extensions.test.ts +81 -0
- package/src/__tests__/migrations/retention.test.ts +118 -0
- package/src/__tests__/queue/index.test.ts +61 -0
- package/src/__tests__/scheduler/index.test.ts +39 -0
- package/src/__tests__/vec/index.test.ts +130 -0
- package/src/__tests__/vec/parity.test.ts +70 -0
- package/src/adapterTypes.ts +23 -0
- package/src/cli/_shared.ts +120 -0
- package/src/cli/bin.ts +97 -0
- package/src/cli/claude.ts +124 -0
- package/src/cli/detect.ts +55 -0
- package/src/cli/hook.ts +25 -0
- package/src/cli/index.ts +22 -0
- package/src/cli/install.ts +185 -0
- package/src/cli/opencode.ts +193 -0
- package/src/cli/pi.ts +74 -0
- package/src/cli/types.ts +78 -0
- package/src/config.ts +163 -0
- package/src/context.ts +172 -0
- package/src/dao/conflicts.ts +26 -0
- package/src/dao/contextItems.ts +70 -0
- package/src/dao/embeddings.ts +78 -0
- package/src/dao/index.ts +9 -0
- package/src/dao/provenanceAudit.ts +108 -0
- package/src/dao/types.ts +142 -0
- package/src/db.ts +780 -0
- package/src/dedup.ts +12 -0
- package/src/embeddings.ts +386 -0
- package/src/events/index.ts +130 -0
- package/src/extensions.ts +140 -0
- package/src/graph.ts +268 -0
- package/src/index.ts +95 -0
- package/src/janitor/archive.ts +66 -0
- package/src/janitor/decay.ts +60 -0
- package/src/janitor/dedup.ts +333 -0
- package/src/janitor/embeddings.ts +209 -0
- package/src/janitor/index.ts +215 -0
- package/src/janitor/promote.ts +188 -0
- package/src/janitor/providers/anthropic.ts +91 -0
- package/src/janitor/providers/cohere.ts +135 -0
- package/src/janitor/providers/gemini.ts +156 -0
- package/src/janitor/providers/ollama.ts +177 -0
- package/src/janitor/providers/openai.ts +121 -0
- package/src/janitor/providers/openrouter.ts +182 -0
- package/src/janitor/providers/types.ts +154 -0
- package/src/janitor/providers/utils.ts +35 -0
- package/src/janitor/supersedes.ts +199 -0
- package/src/lib/honker.ts +54 -0
- package/src/lib/honkerTypes.ts +109 -0
- package/src/lib/jsonlLogger.ts +92 -0
- package/src/lib/writeQueue.ts +28 -0
- package/src/migrations.ts +415 -0
- package/src/paths.ts +22 -0
- package/src/proposals.ts +120 -0
- package/src/providers/disabled.ts +19 -0
- package/src/providers/embeddingProvider.ts +49 -0
- package/src/providers/gemini.ts +37 -0
- package/src/providers/index.ts +2 -0
- package/src/providers/ollama.ts +43 -0
- package/src/providers/openai.ts +35 -0
- package/src/queue/index.ts +53 -0
- package/src/queue/worker.ts +77 -0
- package/src/recall/categorise.ts +139 -0
- package/src/recall/explainer.ts +76 -0
- package/src/scheduler/index.ts +97 -0
- package/src/schema.sql +191 -0
- package/src/secretsScrubber.ts +105 -0
- package/src/shared-db.ts +158 -0
- package/src/vec/index.ts +161 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* janitor/index.ts — Janitor orchestrator.
|
|
3
|
+
* Runs inside the server.ts process, sharing the DB instance.
|
|
4
|
+
* Coordinates decay, promote, dedup, and embedding generation.
|
|
5
|
+
*/
|
|
6
|
+
import { getSetting, setSetting, getDb } from "../shared-db.js";
|
|
7
|
+
import { runDecay, type DecayResult } from "./decay.js";
|
|
8
|
+
import { findDuplicates, saveDedupCandidates, type DedupResult } from "./dedup.js";
|
|
9
|
+
import { embedMissingMemories } from "./embeddings.js";
|
|
10
|
+
import { runPromote, type PromoteResult } from "./promote.js";
|
|
11
|
+
import { runArchive, type ArchiveResult } from "./archive.js";
|
|
12
|
+
import { SETTING_KEYS, getDefault } from "./providers/types.js";
|
|
13
|
+
|
|
14
|
+
export interface JanitorStatus {
|
|
15
|
+
/** Whether the janitor is currently running. */
|
|
16
|
+
running: boolean;
|
|
17
|
+
/** Last run timestamp (ISO string) or null if never run. */
|
|
18
|
+
lastRun: string | null;
|
|
19
|
+
/** Result of the last run. */
|
|
20
|
+
lastResult: JanitorRunResult | null;
|
|
21
|
+
/** Auto-run interval in minutes (0 = disabled). */
|
|
22
|
+
intervalMinutes: number;
|
|
23
|
+
/** Next scheduled run (ISO string) or null if auto-run disabled. */
|
|
24
|
+
nextRun: string | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface JanitorRunResult {
|
|
28
|
+
timestamp: string;
|
|
29
|
+
/** Duration in milliseconds. */
|
|
30
|
+
durationMs: number;
|
|
31
|
+
embed: { embedded: number };
|
|
32
|
+
decay: DecayResult;
|
|
33
|
+
archive: ArchiveResult;
|
|
34
|
+
promote: PromoteResult;
|
|
35
|
+
dedup: { pairsCompared: number; candidatesFound: number };
|
|
36
|
+
errors: string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let _running = false;
|
|
40
|
+
let _lastRun: string | null = null;
|
|
41
|
+
let _lastResult: JanitorRunResult | null = null;
|
|
42
|
+
let _interval: ReturnType<typeof setInterval> | null = null;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Run all janitor tasks in sequence:
|
|
46
|
+
* 1. Embed missing memories (required for dedup)
|
|
47
|
+
* 2. Decay stale memories
|
|
48
|
+
* 3. Promote eligible context_items
|
|
49
|
+
* 4. Find duplicates (no auto-merge without LLM verification)
|
|
50
|
+
*/
|
|
51
|
+
export async function runJanitor(): Promise<JanitorRunResult> {
|
|
52
|
+
if (_running) {
|
|
53
|
+
throw new Error("Janitor is already running");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
_running = true;
|
|
57
|
+
const startTime = Date.now();
|
|
58
|
+
const errors: string[] = [];
|
|
59
|
+
|
|
60
|
+
let embedCount = 0;
|
|
61
|
+
let decayResult: DecayResult = { refreshed: 0, deprecated: 0 };
|
|
62
|
+
let archiveResult: ArchiveResult = { archived: 0 };
|
|
63
|
+
let promoteResult: PromoteResult = {
|
|
64
|
+
promoted: 0,
|
|
65
|
+
skipped: 0,
|
|
66
|
+
scanned: 0,
|
|
67
|
+
};
|
|
68
|
+
let dedupResult: DedupResult = {
|
|
69
|
+
pairsCompared: 0,
|
|
70
|
+
candidates: [],
|
|
71
|
+
autoMerged: 0,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
// 1. Embed missing memories
|
|
76
|
+
try {
|
|
77
|
+
embedCount = await embedMissingMemories();
|
|
78
|
+
} catch (e) {
|
|
79
|
+
errors.push(`embed: ${String(e)}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 2. Decay stale memories (batch SQL refresh + deprecation)
|
|
83
|
+
try {
|
|
84
|
+
decayResult = runDecay();
|
|
85
|
+
} catch (e) {
|
|
86
|
+
errors.push(`decay: ${String(e)}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 3. Archive evicted deprecated memories
|
|
90
|
+
try {
|
|
91
|
+
archiveResult = runArchive();
|
|
92
|
+
} catch (e) {
|
|
93
|
+
errors.push(`archive: ${String(e)}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 4. Promote context_items
|
|
97
|
+
try {
|
|
98
|
+
promoteResult = runPromote();
|
|
99
|
+
} catch (e) {
|
|
100
|
+
errors.push(`promote: ${String(e)}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 5. Find duplicates and save as pending for review
|
|
104
|
+
try {
|
|
105
|
+
dedupResult = await findDuplicates(0.85, true);
|
|
106
|
+
if (dedupResult.candidates.length > 0) {
|
|
107
|
+
saveDedupCandidates(dedupResult.candidates);
|
|
108
|
+
}
|
|
109
|
+
} catch (e) {
|
|
110
|
+
errors.push(`dedup: ${String(e)}`);
|
|
111
|
+
}
|
|
112
|
+
} finally {
|
|
113
|
+
_running = false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const result: JanitorRunResult = {
|
|
117
|
+
timestamp: new Date().toISOString(),
|
|
118
|
+
durationMs: Date.now() - startTime,
|
|
119
|
+
embed: { embedded: embedCount },
|
|
120
|
+
decay: decayResult,
|
|
121
|
+
archive: archiveResult,
|
|
122
|
+
promote: promoteResult,
|
|
123
|
+
dedup: {
|
|
124
|
+
pairsCompared: dedupResult.pairsCompared,
|
|
125
|
+
candidatesFound: dedupResult.candidates.length,
|
|
126
|
+
},
|
|
127
|
+
errors,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Persist run stats to settings for /openltm:health
|
|
131
|
+
await Promise.all([
|
|
132
|
+
setSetting(SETTING_KEYS.JANITOR_LAST_RUN_AT, result.timestamp),
|
|
133
|
+
setSetting(SETTING_KEYS.JANITOR_LAST_DECAY_REFRESHED, String(decayResult.refreshed)),
|
|
134
|
+
setSetting(SETTING_KEYS.JANITOR_LAST_DEPRECATED, String(decayResult.deprecated)),
|
|
135
|
+
setSetting(SETTING_KEYS.JANITOR_LAST_ARCHIVED, String(archiveResult.archived)),
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
// WAL hygiene + query planner refresh
|
|
139
|
+
try {
|
|
140
|
+
const db = getDb();
|
|
141
|
+
db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
142
|
+
db.exec("PRAGMA analysis_limit=400");
|
|
143
|
+
db.exec("ANALYZE");
|
|
144
|
+
} catch { /* non-fatal — next run will retry */ }
|
|
145
|
+
|
|
146
|
+
_lastRun = result.timestamp;
|
|
147
|
+
_lastResult = result;
|
|
148
|
+
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Get current janitor status. */
|
|
153
|
+
export function getJanitorStatus(): JanitorStatus {
|
|
154
|
+
const intervalMinutes = Number.parseInt(
|
|
155
|
+
getSetting(SETTING_KEYS.JANITOR_INTERVAL_MINUTES) ||
|
|
156
|
+
getDefault(SETTING_KEYS.JANITOR_INTERVAL_MINUTES),
|
|
157
|
+
10,
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
let nextRun: string | null = null;
|
|
161
|
+
if (intervalMinutes > 0 && _lastRun) {
|
|
162
|
+
const lastRunTime = new Date(_lastRun).getTime();
|
|
163
|
+
const nextRunTime = lastRunTime + intervalMinutes * 60 * 1000;
|
|
164
|
+
nextRun = new Date(nextRunTime).toISOString();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
running: _running,
|
|
169
|
+
lastRun: _lastRun,
|
|
170
|
+
lastResult: _lastResult,
|
|
171
|
+
intervalMinutes,
|
|
172
|
+
nextRun,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Start auto-run interval. Called by server.ts on startup if interval > 0.
|
|
178
|
+
* Safe to call multiple times — clears any existing interval first.
|
|
179
|
+
*/
|
|
180
|
+
export function startAutoRun(): void {
|
|
181
|
+
stopAutoRun();
|
|
182
|
+
|
|
183
|
+
const intervalMinutes = Number.parseInt(
|
|
184
|
+
getSetting(SETTING_KEYS.JANITOR_INTERVAL_MINUTES) ||
|
|
185
|
+
getDefault(SETTING_KEYS.JANITOR_INTERVAL_MINUTES),
|
|
186
|
+
10,
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
if (intervalMinutes <= 0) return;
|
|
190
|
+
|
|
191
|
+
const intervalMs = intervalMinutes * 60 * 1000;
|
|
192
|
+
_interval = setInterval(async () => {
|
|
193
|
+
try {
|
|
194
|
+
await runJanitor();
|
|
195
|
+
} catch {
|
|
196
|
+
// Logged in result.errors — don't crash the interval
|
|
197
|
+
}
|
|
198
|
+
}, intervalMs);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Stop auto-run interval. */
|
|
202
|
+
export function stopAutoRun(): void {
|
|
203
|
+
if (_interval) {
|
|
204
|
+
clearInterval(_interval);
|
|
205
|
+
_interval = null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Re-export sub-modules for direct access from server routes
|
|
210
|
+
export { approveMemory, getPendingMemories, rejectMemory } from "./promote.js";
|
|
211
|
+
export { mergeMemories, parseDedupSource } from "./dedup.js";
|
|
212
|
+
export { supersede } from "./supersedes.js";
|
|
213
|
+
export { touchMemory } from "./decay.js";
|
|
214
|
+
export { getEmbeddingProvider, semanticSearch, findSimilarMemories } from "./embeddings.js";
|
|
215
|
+
export { runArchive } from "./archive.js";
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* promote.ts — Auto-promote context_items (decisions/gotchas) to pending memories.
|
|
3
|
+
* Unlike the existing context.ts promote() which creates active memories immediately,
|
|
4
|
+
* this creates them as 'pending' for review in the approval UI.
|
|
5
|
+
*/
|
|
6
|
+
import { getDb, getSetting } from "../shared-db.js";
|
|
7
|
+
import { normalizeKey } from "../dedup.js";
|
|
8
|
+
import { SETTING_KEYS, getDefault } from "./providers/types.js";
|
|
9
|
+
|
|
10
|
+
export interface PromoteResult {
|
|
11
|
+
/** Number of context_items promoted to pending memories. */
|
|
12
|
+
promoted: number;
|
|
13
|
+
/** Number of context_items skipped (already promoted or duplicate). */
|
|
14
|
+
skipped: number;
|
|
15
|
+
/** Total eligible items scanned. */
|
|
16
|
+
scanned: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Scan context_items for decisions/gotchas that should be promoted to memories.
|
|
21
|
+
*
|
|
22
|
+
* Criteria:
|
|
23
|
+
* 1. Type is 'decision' or 'gotcha' (not goals or progress)
|
|
24
|
+
* 2. Status is 'active' (not already pending_promotion or promoted)
|
|
25
|
+
* 3. Not already linked to a memory (memory_id IS NULL)
|
|
26
|
+
* 4. Content is substantial enough (> 20 chars)
|
|
27
|
+
*
|
|
28
|
+
* Creates memories with status='pending' for review/approval.
|
|
29
|
+
*/
|
|
30
|
+
export function runPromote(): PromoteResult {
|
|
31
|
+
const db = getDb();
|
|
32
|
+
const result: PromoteResult = { promoted: 0, skipped: 0, scanned: 0 };
|
|
33
|
+
|
|
34
|
+
const minImportance = Number.parseInt(
|
|
35
|
+
getSetting(SETTING_KEYS.PROMOTE_MIN_IMPORTANCE) ||
|
|
36
|
+
getDefault(SETTING_KEYS.PROMOTE_MIN_IMPORTANCE),
|
|
37
|
+
10,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// Find eligible context items
|
|
41
|
+
const items = db
|
|
42
|
+
.query<
|
|
43
|
+
{
|
|
44
|
+
id: number;
|
|
45
|
+
project_name: string;
|
|
46
|
+
type: string;
|
|
47
|
+
content: string;
|
|
48
|
+
},
|
|
49
|
+
[]
|
|
50
|
+
>(
|
|
51
|
+
`SELECT id, project_name, type, content
|
|
52
|
+
FROM context_items
|
|
53
|
+
WHERE type IN ('decision', 'gotcha')
|
|
54
|
+
AND status = 'active'
|
|
55
|
+
AND memory_id IS NULL
|
|
56
|
+
AND LENGTH(content) > 20
|
|
57
|
+
ORDER BY id ASC`,
|
|
58
|
+
)
|
|
59
|
+
.all();
|
|
60
|
+
|
|
61
|
+
result.scanned = items.length;
|
|
62
|
+
|
|
63
|
+
for (const item of items) {
|
|
64
|
+
const dedupKey = normalizeKey(item.content);
|
|
65
|
+
|
|
66
|
+
// Check if a memory with this dedup_key already exists
|
|
67
|
+
const existing = db
|
|
68
|
+
.query<{ id: number }, [string]>(
|
|
69
|
+
"SELECT id FROM memories WHERE dedup_key = ?",
|
|
70
|
+
)
|
|
71
|
+
.get(dedupKey);
|
|
72
|
+
|
|
73
|
+
if (existing) {
|
|
74
|
+
// Link the context_item to the existing memory
|
|
75
|
+
db.run(
|
|
76
|
+
"UPDATE context_items SET memory_id = ?, status = 'promoted' WHERE id = ?",
|
|
77
|
+
[existing.id, item.id],
|
|
78
|
+
);
|
|
79
|
+
result.skipped++;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Map context type to memory category
|
|
84
|
+
const category = item.type === "decision" ? "architecture" : "gotcha";
|
|
85
|
+
const importance = item.type === "gotcha" ? 4 : minImportance;
|
|
86
|
+
|
|
87
|
+
// Create a pending memory
|
|
88
|
+
const insertResult = db.run(
|
|
89
|
+
`INSERT INTO memories (content, category, importance, confidence, source, project_scope, dedup_key, status)
|
|
90
|
+
VALUES (?, ?, ?, 0.8, 'auto-promote', ?, ?, 'pending')`,
|
|
91
|
+
[item.content, category, importance, item.project_name, dedupKey],
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const memoryId = Number(insertResult.lastInsertRowid);
|
|
95
|
+
|
|
96
|
+
// Update the context_item to link it and mark as pending_promotion
|
|
97
|
+
db.run(
|
|
98
|
+
"UPDATE context_items SET memory_id = ?, status = 'pending_promotion' WHERE id = ?",
|
|
99
|
+
[memoryId, item.id],
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
result.promoted++;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Approve a pending memory — set status to 'active'.
|
|
110
|
+
* Also updates the linked context_item status to 'promoted'.
|
|
111
|
+
*/
|
|
112
|
+
export function approveMemory(memoryId: number): boolean {
|
|
113
|
+
const db = getDb();
|
|
114
|
+
const mem = db
|
|
115
|
+
.query<{ id: number; status: string }, [number]>(
|
|
116
|
+
"SELECT id, status FROM memories WHERE id = ?",
|
|
117
|
+
)
|
|
118
|
+
.get(memoryId);
|
|
119
|
+
|
|
120
|
+
if (!mem || mem.status !== "pending") return false;
|
|
121
|
+
|
|
122
|
+
db.run("UPDATE memories SET status = 'active' WHERE id = ?", [memoryId]);
|
|
123
|
+
db.run(
|
|
124
|
+
"UPDATE context_items SET status = 'promoted' WHERE memory_id = ?",
|
|
125
|
+
[memoryId],
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Reject a pending memory — delete it and reset the context_item.
|
|
133
|
+
*/
|
|
134
|
+
export function rejectMemory(memoryId: number): boolean {
|
|
135
|
+
const db = getDb();
|
|
136
|
+
const mem = db
|
|
137
|
+
.query<{ id: number; status: string }, [number]>(
|
|
138
|
+
"SELECT id, status FROM memories WHERE id = ?",
|
|
139
|
+
)
|
|
140
|
+
.get(memoryId);
|
|
141
|
+
|
|
142
|
+
if (!mem || mem.status !== "pending") return false;
|
|
143
|
+
|
|
144
|
+
// Reset linked context_items back to active
|
|
145
|
+
db.run(
|
|
146
|
+
"UPDATE context_items SET memory_id = NULL, status = 'active' WHERE memory_id = ?",
|
|
147
|
+
[memoryId],
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
// Delete the pending memory
|
|
151
|
+
db.run("DELETE FROM memories WHERE id = ?", [memoryId]);
|
|
152
|
+
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get all pending memories for the review UI.
|
|
158
|
+
*/
|
|
159
|
+
export function getPendingMemories(): Array<{
|
|
160
|
+
id: number;
|
|
161
|
+
content: string;
|
|
162
|
+
category: string;
|
|
163
|
+
importance: number;
|
|
164
|
+
confidence: number;
|
|
165
|
+
project_scope: string | null;
|
|
166
|
+
source: string | null;
|
|
167
|
+
created_at: string;
|
|
168
|
+
}> {
|
|
169
|
+
const db = getDb();
|
|
170
|
+
return db
|
|
171
|
+
.query<
|
|
172
|
+
{
|
|
173
|
+
id: number;
|
|
174
|
+
content: string;
|
|
175
|
+
category: string;
|
|
176
|
+
importance: number;
|
|
177
|
+
confidence: number;
|
|
178
|
+
project_scope: string | null;
|
|
179
|
+
source: string | null;
|
|
180
|
+
created_at: string;
|
|
181
|
+
},
|
|
182
|
+
[]
|
|
183
|
+
>(
|
|
184
|
+
`SELECT id, content, category, importance, confidence, project_scope, source, created_at
|
|
185
|
+
FROM memories WHERE status = 'pending' ORDER BY created_at DESC`,
|
|
186
|
+
)
|
|
187
|
+
.all();
|
|
188
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* anthropic.ts — Anthropic Claude provider for LLM chat (no embedding API).
|
|
3
|
+
*/
|
|
4
|
+
import {
|
|
5
|
+
SETTING_KEYS,
|
|
6
|
+
type ChatInput,
|
|
7
|
+
type ChatResult,
|
|
8
|
+
type LLMProvider,
|
|
9
|
+
} from "./types.js";
|
|
10
|
+
import { httpErrorResult, makeApiKeyGetter, makeModelGetter } from "./utils.js";
|
|
11
|
+
|
|
12
|
+
const ANTHROPIC_API_BASE = "https://api.anthropic.com/v1";
|
|
13
|
+
const ANTHROPIC_VERSION = "2023-06-01";
|
|
14
|
+
|
|
15
|
+
const getApiKey = makeApiKeyGetter(SETTING_KEYS.ANTHROPIC_API_KEY, "ANTHROPIC_API_KEY", "Anthropic");
|
|
16
|
+
const getLlmModel = makeModelGetter(SETTING_KEYS.ANTHROPIC_LLM_MODEL);
|
|
17
|
+
|
|
18
|
+
function authHeaders(apiKey: string) {
|
|
19
|
+
return {
|
|
20
|
+
"Content-Type": "application/json",
|
|
21
|
+
"x-api-key": apiKey,
|
|
22
|
+
"anthropic-version": ANTHROPIC_VERSION,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const anthropicLLM: LLMProvider = {
|
|
27
|
+
name: "anthropic",
|
|
28
|
+
|
|
29
|
+
async chat(input: ChatInput): Promise<ChatResult> {
|
|
30
|
+
const apiKey = getApiKey();
|
|
31
|
+
const model = getLlmModel();
|
|
32
|
+
|
|
33
|
+
// Anthropic uses a single `system` string; only the first system message is used.
|
|
34
|
+
const systemMsg = input.messages.find((m) => m.role === "system");
|
|
35
|
+
const messages = input.messages
|
|
36
|
+
.filter((m) => m.role !== "system")
|
|
37
|
+
.map((m) => ({ role: m.role as "user" | "assistant", content: m.content }));
|
|
38
|
+
|
|
39
|
+
const body: Record<string, unknown> = {
|
|
40
|
+
model,
|
|
41
|
+
messages,
|
|
42
|
+
max_tokens: input.maxTokens ?? 1024,
|
|
43
|
+
temperature: input.temperature ?? 0.1,
|
|
44
|
+
};
|
|
45
|
+
if (systemMsg) body.system = systemMsg.content;
|
|
46
|
+
|
|
47
|
+
const res = await fetch(`${ANTHROPIC_API_BASE}/messages`, {
|
|
48
|
+
method: "POST",
|
|
49
|
+
headers: authHeaders(apiKey),
|
|
50
|
+
body: JSON.stringify(body),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
const { error } = await httpErrorResult(res);
|
|
55
|
+
throw new Error(`Anthropic chat failed: ${error}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const data = (await res.json()) as {
|
|
59
|
+
content: Array<{ type: string; text: string }>;
|
|
60
|
+
model: string;
|
|
61
|
+
usage: { input_tokens: number; output_tokens: number };
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
content: data.content.find((b) => b.type === "text")?.text ?? "",
|
|
66
|
+
model: data.model,
|
|
67
|
+
promptTokens: data.usage.input_tokens,
|
|
68
|
+
completionTokens: data.usage.output_tokens,
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
async verify(): Promise<{ ok: boolean; error?: string }> {
|
|
73
|
+
try {
|
|
74
|
+
const apiKey = getApiKey();
|
|
75
|
+
const model = getLlmModel();
|
|
76
|
+
const res = await fetch(`${ANTHROPIC_API_BASE}/messages`, {
|
|
77
|
+
method: "POST",
|
|
78
|
+
headers: authHeaders(apiKey),
|
|
79
|
+
body: JSON.stringify({
|
|
80
|
+
model,
|
|
81
|
+
messages: [{ role: "user", content: "Reply with OK" }],
|
|
82
|
+
max_tokens: 5,
|
|
83
|
+
}),
|
|
84
|
+
});
|
|
85
|
+
if (!res.ok) return httpErrorResult(res);
|
|
86
|
+
return { ok: true };
|
|
87
|
+
} catch (e) {
|
|
88
|
+
return { ok: false, error: String(e) };
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cohere.ts — Cohere provider for embeddings and LLM chat (v2 API).
|
|
3
|
+
*/
|
|
4
|
+
import {
|
|
5
|
+
SETTING_KEYS,
|
|
6
|
+
type ChatInput,
|
|
7
|
+
type ChatResult,
|
|
8
|
+
type EmbedInput,
|
|
9
|
+
type EmbedResult,
|
|
10
|
+
type EmbeddingProvider,
|
|
11
|
+
type EmbeddingVector,
|
|
12
|
+
type LLMProvider,
|
|
13
|
+
} from "./types.js";
|
|
14
|
+
import { httpErrorResult, makeApiKeyGetter, makeModelGetter } from "./utils.js";
|
|
15
|
+
|
|
16
|
+
const COHERE_API_BASE = "https://api.cohere.com/v2";
|
|
17
|
+
|
|
18
|
+
const getApiKey = makeApiKeyGetter(SETTING_KEYS.COHERE_API_KEY, "COHERE_API_KEY", "Cohere");
|
|
19
|
+
const getEmbedModel = makeModelGetter(SETTING_KEYS.COHERE_EMBED_MODEL);
|
|
20
|
+
const getLlmModel = makeModelGetter(SETTING_KEYS.COHERE_LLM_MODEL);
|
|
21
|
+
|
|
22
|
+
function authHeaders(apiKey: string) {
|
|
23
|
+
return { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const cohereEmbedding: EmbeddingProvider = {
|
|
27
|
+
name: "cohere",
|
|
28
|
+
|
|
29
|
+
async embed(input: EmbedInput): Promise<EmbedResult> {
|
|
30
|
+
const apiKey = getApiKey();
|
|
31
|
+
const model = getEmbedModel();
|
|
32
|
+
|
|
33
|
+
const res = await fetch(`${COHERE_API_BASE}/embed`, {
|
|
34
|
+
method: "POST",
|
|
35
|
+
headers: authHeaders(apiKey),
|
|
36
|
+
body: JSON.stringify({
|
|
37
|
+
model,
|
|
38
|
+
texts: input.texts,
|
|
39
|
+
input_type: "search_document",
|
|
40
|
+
embedding_types: ["float"],
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (!res.ok) {
|
|
45
|
+
const { error } = await httpErrorResult(res);
|
|
46
|
+
throw new Error(`Cohere embed failed: ${error}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const data = (await res.json()) as {
|
|
50
|
+
embeddings: { float: number[][] };
|
|
51
|
+
meta?: { billed_units?: { input_tokens?: number } };
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const vectors: EmbeddingVector[] = data.embeddings.float.map(
|
|
55
|
+
(e) => new Float32Array(e),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
vectors,
|
|
60
|
+
model,
|
|
61
|
+
dimensions: vectors[0]?.length ?? 0,
|
|
62
|
+
totalTokens: data.meta?.billed_units?.input_tokens ?? 0,
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
async verify(): Promise<{ ok: boolean; error?: string }> {
|
|
67
|
+
try {
|
|
68
|
+
await this.embed({ texts: ["test"] });
|
|
69
|
+
return { ok: true };
|
|
70
|
+
} catch (e) {
|
|
71
|
+
return { ok: false, error: String(e) };
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const cohereLLM: LLMProvider = {
|
|
77
|
+
name: "cohere",
|
|
78
|
+
|
|
79
|
+
async chat(input: ChatInput): Promise<ChatResult> {
|
|
80
|
+
const apiKey = getApiKey();
|
|
81
|
+
const model = getLlmModel();
|
|
82
|
+
|
|
83
|
+
const res = await fetch(`${COHERE_API_BASE}/chat`, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: authHeaders(apiKey),
|
|
86
|
+
body: JSON.stringify({
|
|
87
|
+
model,
|
|
88
|
+
messages: input.messages.map((m) => ({
|
|
89
|
+
role: m.role === "assistant" ? "assistant" : m.role === "system" ? "system" : "user",
|
|
90
|
+
content: m.content,
|
|
91
|
+
})),
|
|
92
|
+
max_tokens: input.maxTokens ?? 1024,
|
|
93
|
+
temperature: input.temperature ?? 0.1,
|
|
94
|
+
...(input.jsonMode ? { response_format: { type: "json_object" } } : {}),
|
|
95
|
+
}),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (!res.ok) {
|
|
99
|
+
const { error } = await httpErrorResult(res);
|
|
100
|
+
throw new Error(`Cohere chat failed: ${error}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const data = (await res.json()) as {
|
|
104
|
+
message: { content: Array<{ type: string; text: string }> };
|
|
105
|
+
usage?: { billed_units?: { input_tokens?: number; output_tokens?: number } };
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
content: data.message.content.find((b) => b.type === "text")?.text ?? "",
|
|
110
|
+
model,
|
|
111
|
+
promptTokens: data.usage?.billed_units?.input_tokens ?? 0,
|
|
112
|
+
completionTokens: data.usage?.billed_units?.output_tokens ?? 0,
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
async verify(): Promise<{ ok: boolean; error?: string }> {
|
|
117
|
+
try {
|
|
118
|
+
const apiKey = getApiKey();
|
|
119
|
+
const model = getLlmModel();
|
|
120
|
+
const res = await fetch(`${COHERE_API_BASE}/chat`, {
|
|
121
|
+
method: "POST",
|
|
122
|
+
headers: authHeaders(apiKey),
|
|
123
|
+
body: JSON.stringify({
|
|
124
|
+
model,
|
|
125
|
+
messages: [{ role: "user", content: "Reply with OK" }],
|
|
126
|
+
max_tokens: 5,
|
|
127
|
+
}),
|
|
128
|
+
});
|
|
129
|
+
if (!res.ok) return httpErrorResult(res);
|
|
130
|
+
return { ok: true };
|
|
131
|
+
} catch (e) {
|
|
132
|
+
return { ok: false, error: String(e) };
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
};
|