@rigstate/cli 0.7.37 → 0.7.39
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/.rigstate/memories/2585b51e-f576-4c0c-b69d-2daaaa427bf4.json +18 -0
- package/.rigstate/memories/739c72cd-d1a7-49a7-901c-8f2a9db9e4ca.json +17 -0
- package/.rigstate/memories/bd30d910-dd03-42d7-8eee-8306a5536ca1.json +18 -0
- package/dist/index.cjs +376 -53
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +376 -53
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/ask.ts +100 -0
- package/src/commands/genesis.ts +41 -5
- package/src/commands/remember.ts +67 -0
- package/src/index.ts +4 -0
- package/src/utils/memory-store.ts +183 -0
package/package.json
CHANGED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { searchMemories, getMemoryStats } from '../utils/memory-store.js';
|
|
5
|
+
|
|
6
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
// rigstate ask "<query>"
|
|
8
|
+
// Searches local memories in .rigstate/memories/ using keyword matching.
|
|
9
|
+
// Response time: <10ms (no API calls, no LLM, pure local search).
|
|
10
|
+
//
|
|
11
|
+
// Examples:
|
|
12
|
+
// rigstate ask "Why Supabase?"
|
|
13
|
+
// rigstate ask "typescript patterns"
|
|
14
|
+
// rigstate ask "security" --limit 10
|
|
15
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
export function createAskCommand(): Command {
|
|
18
|
+
return new Command('ask')
|
|
19
|
+
.description('Search local memories instantly (<10ms, offline)')
|
|
20
|
+
.argument('<query>', 'Natural language query to search memories')
|
|
21
|
+
.option('-n, --limit <n>', 'Maximum results to return', '5')
|
|
22
|
+
.action(async (query: string, options) => {
|
|
23
|
+
const limit = parseInt(options.limit, 10) || 5;
|
|
24
|
+
const startTime = performance.now();
|
|
25
|
+
const results = searchMemories(query, limit);
|
|
26
|
+
const elapsed = (performance.now() - startTime).toFixed(1);
|
|
27
|
+
|
|
28
|
+
console.log('');
|
|
29
|
+
|
|
30
|
+
if (results.length === 0) {
|
|
31
|
+
const stats = getMemoryStats();
|
|
32
|
+
console.log(chalk.yellow(`🔍 No memories found for: "${query}"`));
|
|
33
|
+
console.log(chalk.dim(` Searched ${stats.total} memories in ${elapsed}ms.`));
|
|
34
|
+
|
|
35
|
+
if (stats.total === 0) {
|
|
36
|
+
console.log('');
|
|
37
|
+
console.log(chalk.dim(' No memories yet. Start with:'));
|
|
38
|
+
console.log(chalk.white(' rigstate remember "We chose Supabase for real-time and RLS"'));
|
|
39
|
+
}
|
|
40
|
+
console.log('');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.log(chalk.bold(`🧠 ${results.length} memor${results.length === 1 ? 'y' : 'ies'} found`) + chalk.dim(` (${elapsed}ms)`));
|
|
45
|
+
console.log(chalk.dim('────────────────────────────────────────'));
|
|
46
|
+
|
|
47
|
+
for (const { memory, score, matchedFields } of results) {
|
|
48
|
+
const categoryColor = getCategoryColor(memory.category);
|
|
49
|
+
const stars = '⭐'.repeat(Math.min(5, Math.ceil((memory.importance || 5) / 2)));
|
|
50
|
+
|
|
51
|
+
console.log('');
|
|
52
|
+
console.log(` ${categoryColor(memory.category)} ${chalk.bold(memory.title)} ${chalk.dim(`[${stars}]`)}`);
|
|
53
|
+
|
|
54
|
+
// Show a truncated preview of content
|
|
55
|
+
const preview = memory.content.length > 120
|
|
56
|
+
? memory.content.substring(0, 120) + '...'
|
|
57
|
+
: memory.content;
|
|
58
|
+
console.log(` ${chalk.white(preview)}`);
|
|
59
|
+
|
|
60
|
+
// Tags
|
|
61
|
+
if (memory.tags && memory.tags.length > 0) {
|
|
62
|
+
console.log(` ${memory.tags.map(t => chalk.yellow(`#${t}`)).join(' ')}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Metadata line
|
|
66
|
+
const meta = [
|
|
67
|
+
chalk.dim(`score:${score.toFixed(1)}`),
|
|
68
|
+
chalk.dim(`matched:${matchedFields.join(',')}`),
|
|
69
|
+
chalk.dim(`source:${memory.source}`),
|
|
70
|
+
];
|
|
71
|
+
if (memory.created_at) {
|
|
72
|
+
const date = new Date(memory.created_at).toLocaleDateString('nb-NO');
|
|
73
|
+
meta.push(chalk.dim(`date:${date}`));
|
|
74
|
+
}
|
|
75
|
+
console.log(` ${meta.join(' ')}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log('');
|
|
79
|
+
console.log(chalk.dim('────────────────────────────────────────'));
|
|
80
|
+
|
|
81
|
+
const stats = getMemoryStats();
|
|
82
|
+
console.log(chalk.dim(`Searched ${stats.total} memories in ${elapsed}ms`));
|
|
83
|
+
console.log('');
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
function getCategoryColor(category: string): (text: string) => string {
|
|
90
|
+
const colors: Record<string, (text: string) => string> = {
|
|
91
|
+
'ADR': chalk.bgBlue.white,
|
|
92
|
+
'DECISION': chalk.bgCyan.black,
|
|
93
|
+
'LESSON': chalk.bgYellow.black,
|
|
94
|
+
'CONTEXT': chalk.bgGreen.black,
|
|
95
|
+
'INSTRUCTION': chalk.bgMagenta.white,
|
|
96
|
+
'PATTERN': chalk.bgWhite.black,
|
|
97
|
+
'GOTCHA': chalk.bgRed.white,
|
|
98
|
+
};
|
|
99
|
+
return colors[category] || chalk.dim;
|
|
100
|
+
}
|
package/src/commands/genesis.ts
CHANGED
|
@@ -32,6 +32,7 @@ interface GenesisStatusResponse {
|
|
|
32
32
|
|
|
33
33
|
interface GenesisTriggerResponse {
|
|
34
34
|
success: boolean;
|
|
35
|
+
simulation?: boolean;
|
|
35
36
|
data: {
|
|
36
37
|
project_name: string;
|
|
37
38
|
template: string;
|
|
@@ -51,6 +52,7 @@ export function createGenesisCommand(): Command {
|
|
|
51
52
|
return new Command('genesis')
|
|
52
53
|
.description('Initialize project foundation (Phase 0). Detects stack and injects foundation steps.')
|
|
53
54
|
.option('--force', 'Re-run genesis even if already initialized (use with caution)')
|
|
55
|
+
.option('--simulate', 'Dry-run: Calculate plan without modifying database')
|
|
54
56
|
.option('--status', 'Check genesis status without triggering')
|
|
55
57
|
.option('--project-id <id>', 'Override project ID (defaults to linked project)')
|
|
56
58
|
.action(async (options) => {
|
|
@@ -71,7 +73,7 @@ export function createGenesisCommand(): Command {
|
|
|
71
73
|
if (options.status) {
|
|
72
74
|
await checkGenesisStatus(projectId, apiKey, apiUrl);
|
|
73
75
|
} else {
|
|
74
|
-
await triggerGenesis(projectId, apiKey, apiUrl, options.force ?? false);
|
|
76
|
+
await triggerGenesis(projectId, apiKey, apiUrl, options.force ?? false, options.simulate ?? false);
|
|
75
77
|
}
|
|
76
78
|
});
|
|
77
79
|
}
|
|
@@ -161,11 +163,17 @@ export async function triggerGenesis(
|
|
|
161
163
|
projectId: string,
|
|
162
164
|
apiKey: string,
|
|
163
165
|
apiUrl: string,
|
|
164
|
-
force = false
|
|
166
|
+
force = false,
|
|
167
|
+
simulate = false
|
|
165
168
|
): Promise<boolean> {
|
|
166
169
|
console.log('');
|
|
167
|
-
|
|
168
|
-
|
|
170
|
+
if (simulate) {
|
|
171
|
+
console.log(chalk.bold.magenta('🔮 GENESIS SIMULATION'));
|
|
172
|
+
console.log(chalk.dim('Dry-run: Calculating plan without executing changes...'));
|
|
173
|
+
} else {
|
|
174
|
+
console.log(chalk.bold.blue('🏗️ GENESIS PROTOCOL'));
|
|
175
|
+
console.log(chalk.dim('Initializing project foundation...'));
|
|
176
|
+
}
|
|
169
177
|
console.log('');
|
|
170
178
|
|
|
171
179
|
const spinner = ora('Detecting tech stack...').start();
|
|
@@ -201,7 +209,7 @@ export async function triggerGenesis(
|
|
|
201
209
|
// Step 2: Trigger Genesis
|
|
202
210
|
const response = await axios.post<GenesisTriggerResponse>(
|
|
203
211
|
`${apiUrl}/api/v1/genesis`,
|
|
204
|
-
{ project_id: projectId, force },
|
|
212
|
+
{ project_id: projectId, force, simulate },
|
|
205
213
|
{
|
|
206
214
|
headers: { Authorization: `Bearer ${apiKey}` },
|
|
207
215
|
timeout: 60000 // AI enrichment can take time
|
|
@@ -230,6 +238,34 @@ export async function triggerGenesis(
|
|
|
230
238
|
|
|
231
239
|
const { data } = response.data;
|
|
232
240
|
|
|
241
|
+
// ── Simulation Output ─────────────────────────────────────────────────
|
|
242
|
+
if (response.data.simulation) {
|
|
243
|
+
console.log(chalk.bold.magenta('🔮 SIMULATION RESULTS'));
|
|
244
|
+
console.log(chalk.dim('────────────────────────────────────────'));
|
|
245
|
+
console.log(`${chalk.bold('Project:')} ${chalk.cyan(data.project_name)}`);
|
|
246
|
+
console.log(`${chalk.bold('Stack:')} ${chalk.magenta(data.template)}`);
|
|
247
|
+
console.log(`${chalk.bold('Will Create:')} ${chalk.white(data.steps_created)} foundation steps`);
|
|
248
|
+
|
|
249
|
+
if (data.existing_steps_shifted > 0) {
|
|
250
|
+
console.log(`${chalk.bold('Will Shift:')} ${chalk.yellow(`${data.existing_steps_shifted} existing steps down`)}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
console.log('');
|
|
254
|
+
console.log(chalk.bold('📋 Planner Preview:'));
|
|
255
|
+
data.steps.forEach(step => {
|
|
256
|
+
const stepNum = step.step_number;
|
|
257
|
+
// In simulation, step numbers returned might be 1-based index relative to insert,
|
|
258
|
+
// but usually the AI/Builder assigns them relative to start.
|
|
259
|
+
console.log(` ${step.icon || '🔹'} ${chalk.bold(`T-${stepNum}`)}: ${step.title}`);
|
|
260
|
+
if (step.verification_path) {
|
|
261
|
+
console.log(` ${chalk.dim(`Verify: ${step.verification_path}`)}`);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
console.log('');
|
|
265
|
+
console.log(chalk.dim('To execute this plan, run without --simulate.'));
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
|
|
233
269
|
// ── Success Output ────────────────────────────────────────────────────
|
|
234
270
|
console.log(chalk.bold.green('✅ GENESIS COMPLETE'));
|
|
235
271
|
console.log(chalk.dim('────────────────────────────────────────'));
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { saveMemory, getMemoryStats } from '../utils/memory-store.js';
|
|
5
|
+
import type { MemoryCategory } from '@rigstate/shared';
|
|
6
|
+
import { getProjectId } from '../utils/config.js';
|
|
7
|
+
|
|
8
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
9
|
+
// rigstate remember "<text>"
|
|
10
|
+
// Saves a memory to the local .rigstate/memories/ directory.
|
|
11
|
+
//
|
|
12
|
+
// Examples:
|
|
13
|
+
// rigstate remember "We chose Supabase for real-time and RLS"
|
|
14
|
+
// rigstate remember "Never use 'any' in shared types" --category LESSON --tags "typescript,shared"
|
|
15
|
+
// rigstate remember "ADR-001: Monorepo over polyrepo" --category ADR --importance 9
|
|
16
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
export function createRememberCommand(): Command {
|
|
19
|
+
return new Command('remember')
|
|
20
|
+
.description('Save a memory to the local knowledge base (.rigstate/memories/)')
|
|
21
|
+
.argument('<text>', 'The memory content to save')
|
|
22
|
+
.option('-t, --title <title>', 'Title for the memory (defaults to first 60 chars of text)')
|
|
23
|
+
.option('-c, --category <category>', 'Category: ADR, DECISION, LESSON, CONTEXT, INSTRUCTION, PATTERN, GOTCHA', 'CONTEXT')
|
|
24
|
+
.option('--tags <tags>', 'Comma-separated tags (e.g. "supabase,architecture")')
|
|
25
|
+
.option('-i, --importance <n>', 'Importance 1-10 (default: 5)', '5')
|
|
26
|
+
.option('--source <source>', 'Source: USER, AGENT, COUNCIL, GOVERNANCE, HARVEST, IMPORT', 'USER')
|
|
27
|
+
.action(async (text: string, options) => {
|
|
28
|
+
try {
|
|
29
|
+
const projectId = getProjectId();
|
|
30
|
+
|
|
31
|
+
const title = options.title || text.substring(0, 60) + (text.length > 60 ? '...' : '');
|
|
32
|
+
const tags = options.tags ? options.tags.split(',').map((t: string) => t.trim()) : [];
|
|
33
|
+
const importance = Math.min(10, Math.max(1, parseInt(options.importance, 10) || 5));
|
|
34
|
+
|
|
35
|
+
const memory = saveMemory({
|
|
36
|
+
title,
|
|
37
|
+
content: text,
|
|
38
|
+
category: (options.category as MemoryCategory) || 'CONTEXT',
|
|
39
|
+
source: options.source || 'USER',
|
|
40
|
+
tags,
|
|
41
|
+
importance,
|
|
42
|
+
project_id: projectId || undefined,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
console.log('');
|
|
46
|
+
console.log(chalk.bold.green('🧠 Memory Saved'));
|
|
47
|
+
console.log(chalk.dim('────────────────────────────────────────'));
|
|
48
|
+
console.log(`${chalk.bold('ID:')} ${chalk.dim(memory.id)}`);
|
|
49
|
+
console.log(`${chalk.bold('Title:')} ${chalk.cyan(memory.title)}`);
|
|
50
|
+
console.log(`${chalk.bold('Category:')} ${chalk.magenta(memory.category)}`);
|
|
51
|
+
console.log(`${chalk.bold('Importance:')} ${'⭐'.repeat(Math.min(5, Math.ceil(importance / 2)))} (${importance}/10)`);
|
|
52
|
+
if (tags.length > 0) {
|
|
53
|
+
console.log(`${chalk.bold('Tags:')} ${tags.map((t: string) => chalk.yellow(`#${t}`)).join(' ')}`);
|
|
54
|
+
}
|
|
55
|
+
console.log(chalk.dim('────────────────────────────────────────'));
|
|
56
|
+
|
|
57
|
+
// Show stats
|
|
58
|
+
const stats = getMemoryStats();
|
|
59
|
+
console.log(chalk.dim(`Total memories: ${stats.total}`));
|
|
60
|
+
console.log('');
|
|
61
|
+
|
|
62
|
+
} catch (err: any) {
|
|
63
|
+
console.error(chalk.red(`❌ Failed to save memory: ${err.message}`));
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -24,6 +24,8 @@ import { createRoadmapCommand } from './commands/roadmap.js';
|
|
|
24
24
|
import { createCouncilCommand } from './commands/council.js';
|
|
25
25
|
import { createPlanCommand } from './commands/plan.js';
|
|
26
26
|
import { createGenesisCommand } from './commands/genesis.js';
|
|
27
|
+
import { createRememberCommand } from './commands/remember.js';
|
|
28
|
+
import { createAskCommand } from './commands/ask.js';
|
|
27
29
|
import { checkVersion } from './utils/version.js';
|
|
28
30
|
import dotenv from 'dotenv';
|
|
29
31
|
|
|
@@ -65,6 +67,8 @@ program.addCommand(createRoadmapCommand());
|
|
|
65
67
|
program.addCommand(createCouncilCommand());
|
|
66
68
|
program.addCommand(createPlanCommand());
|
|
67
69
|
program.addCommand(createGenesisCommand());
|
|
70
|
+
program.addCommand(createRememberCommand());
|
|
71
|
+
program.addCommand(createAskCommand());
|
|
68
72
|
|
|
69
73
|
program.hook('preAction', async () => {
|
|
70
74
|
await checkVersion();
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { randomUUID } from 'crypto';
|
|
5
|
+
import type { Memory, MemoryInput } from '@rigstate/shared';
|
|
6
|
+
|
|
7
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
8
|
+
// Memory Store: Local file-based storage for Decentralized Intelligence.
|
|
9
|
+
// Memories are stored as individual JSON files in .rigstate/memories/
|
|
10
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
const MEMORIES_DIR = '.rigstate/memories';
|
|
13
|
+
|
|
14
|
+
function getMemoriesDir(): string {
|
|
15
|
+
const dir = path.resolve(process.cwd(), MEMORIES_DIR);
|
|
16
|
+
if (!fs.existsSync(dir)) {
|
|
17
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
return dir;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ── Save ─────────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
export function saveMemory(input: MemoryInput): Memory {
|
|
25
|
+
const dir = getMemoriesDir();
|
|
26
|
+
const now = new Date().toISOString();
|
|
27
|
+
|
|
28
|
+
const memory: Memory = {
|
|
29
|
+
id: randomUUID(),
|
|
30
|
+
...input,
|
|
31
|
+
title: input.title,
|
|
32
|
+
content: input.content,
|
|
33
|
+
category: input.category || 'CONTEXT',
|
|
34
|
+
source: input.source || 'USER',
|
|
35
|
+
tags: input.tags || [],
|
|
36
|
+
importance: input.importance || 5,
|
|
37
|
+
confidence: input.confidence || 1.0,
|
|
38
|
+
created_at: now,
|
|
39
|
+
updated_at: now,
|
|
40
|
+
expires_at: input.expires_at || null,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const filename = `${memory.id}.json`;
|
|
44
|
+
const filepath = path.join(dir, filename);
|
|
45
|
+
fs.writeFileSync(filepath, JSON.stringify(memory, null, 2), 'utf-8');
|
|
46
|
+
|
|
47
|
+
return memory;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── Load All ─────────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
export function loadAllMemories(): Memory[] {
|
|
53
|
+
const dir = getMemoriesDir();
|
|
54
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.json'));
|
|
55
|
+
|
|
56
|
+
const memories: Memory[] = [];
|
|
57
|
+
for (const file of files) {
|
|
58
|
+
try {
|
|
59
|
+
const raw = fs.readFileSync(path.join(dir, file), 'utf-8');
|
|
60
|
+
memories.push(JSON.parse(raw) as Memory);
|
|
61
|
+
} catch {
|
|
62
|
+
// Skip corrupted files silently
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return memories;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Search (Keyword-based, <10ms) ────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
export interface SearchResult {
|
|
72
|
+
memory: Memory;
|
|
73
|
+
score: number;
|
|
74
|
+
matchedFields: string[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function searchMemories(query: string, limit = 5): SearchResult[] {
|
|
78
|
+
const memories = loadAllMemories();
|
|
79
|
+
const tokens = tokenize(query);
|
|
80
|
+
|
|
81
|
+
if (tokens.length === 0) return [];
|
|
82
|
+
|
|
83
|
+
const results: SearchResult[] = [];
|
|
84
|
+
|
|
85
|
+
for (const memory of memories) {
|
|
86
|
+
// Check expiry
|
|
87
|
+
if (memory.expires_at && new Date(memory.expires_at) < new Date()) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let score = 0;
|
|
92
|
+
const matchedFields: string[] = [];
|
|
93
|
+
|
|
94
|
+
// Title match (high weight)
|
|
95
|
+
const titleTokens = tokenize(memory.title);
|
|
96
|
+
const titleMatches = tokens.filter(t => titleTokens.includes(t)).length;
|
|
97
|
+
if (titleMatches > 0) {
|
|
98
|
+
score += titleMatches * 3;
|
|
99
|
+
matchedFields.push('title');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Content match (medium weight)
|
|
103
|
+
const contentLower = memory.content.toLowerCase();
|
|
104
|
+
const contentMatches = tokens.filter(t => contentLower.includes(t)).length;
|
|
105
|
+
if (contentMatches > 0) {
|
|
106
|
+
score += contentMatches * 1;
|
|
107
|
+
matchedFields.push('content');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Tag match (high weight)
|
|
111
|
+
const tagLower = memory.tags.map(t => t.toLowerCase());
|
|
112
|
+
const tagMatches = tokens.filter(t => tagLower.includes(t)).length;
|
|
113
|
+
if (tagMatches > 0) {
|
|
114
|
+
score += tagMatches * 4;
|
|
115
|
+
matchedFields.push('tags');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Category match (bonus)
|
|
119
|
+
if (tokens.includes(memory.category.toLowerCase())) {
|
|
120
|
+
score += 2;
|
|
121
|
+
matchedFields.push('category');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Importance boost
|
|
125
|
+
score *= (memory.importance / 5); // importance 10 = 2x multiplier
|
|
126
|
+
|
|
127
|
+
if (score > 0) {
|
|
128
|
+
results.push({ memory, score, matchedFields });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Sort by score descending, return top N
|
|
133
|
+
return results
|
|
134
|
+
.sort((a, b) => b.score - a.score)
|
|
135
|
+
.slice(0, limit);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── Stats ────────────────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
export function getMemoryStats(): { total: number; byCategory: Record<string, number> } {
|
|
141
|
+
const memories = loadAllMemories();
|
|
142
|
+
const byCategory: Record<string, number> = {};
|
|
143
|
+
|
|
144
|
+
for (const m of memories) {
|
|
145
|
+
byCategory[m.category] = (byCategory[m.category] || 0) + 1;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return { total: memories.length, byCategory };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Delete ───────────────────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
export function deleteMemory(id: string): boolean {
|
|
154
|
+
const dir = getMemoriesDir();
|
|
155
|
+
const filepath = path.join(dir, `${id}.json`);
|
|
156
|
+
if (fs.existsSync(filepath)) {
|
|
157
|
+
fs.unlinkSync(filepath);
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
const STOP_WORDS = new Set([
|
|
166
|
+
'the', 'a', 'an', 'is', 'are', 'was', 'were', 'in', 'on', 'at', 'to',
|
|
167
|
+
'for', 'of', 'with', 'by', 'from', 'and', 'or', 'not', 'this', 'that',
|
|
168
|
+
'it', 'we', 'you', 'they', 'my', 'our', 'your', 'its', 'his', 'her',
|
|
169
|
+
'how', 'what', 'why', 'when', 'where', 'which', 'who', 'do', 'does',
|
|
170
|
+
'did', 'has', 'have', 'had', 'be', 'been', 'being', 'will', 'would',
|
|
171
|
+
'can', 'could', 'should', 'shall', 'may', 'might', 'must',
|
|
172
|
+
'vi', 'er', 'var', 'har', 'den', 'det', 'en', 'et', 'og', 'i', 'på',
|
|
173
|
+
'til', 'fra', 'med', 'som', 'om', 'for', 'av', 'ikke',
|
|
174
|
+
'hvorfor', 'hvordan', 'hva', 'når', 'hvor',
|
|
175
|
+
]);
|
|
176
|
+
|
|
177
|
+
function tokenize(text: string): string[] {
|
|
178
|
+
return text
|
|
179
|
+
.toLowerCase()
|
|
180
|
+
.replace(/[^a-zA-Z0-9æøåÆØÅ\s]/g, ' ')
|
|
181
|
+
.split(/\s+/)
|
|
182
|
+
.filter(t => t.length > 1 && !STOP_WORDS.has(t));
|
|
183
|
+
}
|