@rigstate/cli 0.7.38 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigstate/cli",
3
- "version": "0.7.38",
3
+ "version": "0.7.39",
4
4
  "description": "Rigstate CLI - Code audit, sync and supervision tool",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -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
+ }
@@ -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
+ }