@rigstate/cli 0.7.29 → 0.7.31

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.29",
3
+ "version": "0.7.31",
4
4
  "description": "Rigstate CLI - Code audit, sync and supervision tool",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -22,6 +22,7 @@ import { trackSkillUsage } from './telemetry.js';
22
22
  import { jitProvisionSkill } from '../utils/skills-provisioner.js';
23
23
  import { syncProjectRules } from '../commands/sync-rules.js';
24
24
  import { Logger } from '../utils/logger.js';
25
+ import { KnowledgeHarvester } from './harvester.js';
25
26
 
26
27
  export class GuardianDaemon extends EventEmitter {
27
28
  private config: DaemonConfig;
@@ -31,6 +32,8 @@ export class GuardianDaemon extends EventEmitter {
31
32
  private heuristicEngine: ReturnType<typeof createHeuristicEngine> | null = null;
32
33
  private interventionProtocol: ReturnType<typeof createInterventionProtocol> | null = null;
33
34
  private bridgeListener: ReturnType<typeof createBridgeListener> | null = null;
35
+ private harvester: KnowledgeHarvester | null = null;
36
+ private syncInterval: NodeJS.Timeout | null = null;
34
37
 
35
38
  constructor(config: DaemonConfig) {
36
39
  super();
@@ -60,19 +63,35 @@ export class GuardianDaemon extends EventEmitter {
60
63
  this.interventionProtocol = createInterventionProtocol();
61
64
  this.guardianMonitor = createGuardianMonitor(this.config.projectId, this.config.apiUrl, this.config.apiKey);
62
65
 
66
+ // Initialize Harvester 🌾
67
+ this.harvester = new KnowledgeHarvester({
68
+ projectId: this.config.projectId,
69
+ apiUrl: this.config.apiUrl,
70
+ apiKey: this.config.apiKey,
71
+ watchPath: process.cwd()
72
+ });
73
+
63
74
  // 2. Load and Sync Rules
64
75
  await this.guardianMonitor.loadRules();
65
76
  Logger.info(`Loaded ${this.guardianMonitor.getRuleCount()} rules`);
66
77
 
67
78
  // Auto-Sync Brain to IDE (The "Missing Link")
68
79
  Logger.info('Syncing Brain to IDE (.cursor/rules)...');
69
- await syncProjectRules(this.config.projectId, this.config.apiKey, this.config.apiUrl);
80
+ await this.runRuleSync(); // Initial sync
70
81
 
71
82
  await this.syncHeuristics();
72
83
 
73
84
  // 3. Setup File Watcher
74
85
  if (this.config.checkOnChange) {
75
86
  this.setupFileWatcher();
87
+ // Start Harvester
88
+ await this.harvester.start();
89
+
90
+ // Start Auto-Sync Poller (Every 5 minutes)
91
+ Logger.info('Starting Auto-Sync Poller (5m interval)...');
92
+ this.syncInterval = setInterval(() => {
93
+ this.runRuleSync().catch(e => Logger.error(`Auto-Sync failed: ${e.message}`));
94
+ }, 5 * 60 * 1000);
76
95
  }
77
96
 
78
97
  // 4. Setup Bridge
@@ -90,6 +109,19 @@ export class GuardianDaemon extends EventEmitter {
90
109
  this.emit('started', this.state);
91
110
  }
92
111
 
112
+ private async runRuleSync() {
113
+ try {
114
+ // We use the existing syncProjectRules command logic
115
+ // Ideally we should silent the spinner output for background runs
116
+ // For now, we utilize it "as is" but maybe we capture logs if needed
117
+ // TODO: Refactor syncProjectRules to be less verbose in daemon mode
118
+ await syncProjectRules(this.config.projectId, this.config.apiKey, this.config.apiUrl);
119
+ Logger.debug('Auto-Sync completed successfully');
120
+ } catch (error: any) {
121
+ Logger.warn(`Rule sync hiccup: ${error.message}`);
122
+ }
123
+ }
124
+
93
125
  private printWelcome() {
94
126
  console.log(chalk.bold.blue('\nšŸ›”ļø Guardian Daemon Starting...'));
95
127
  console.log(chalk.dim(`Project: ${this.config.projectId}`));
@@ -251,6 +283,11 @@ export class GuardianDaemon extends EventEmitter {
251
283
  console.log(chalk.dim('\nšŸ›‘ Stopping Guardian Daemon...'));
252
284
  if (this.fileWatcher) await this.fileWatcher.stop();
253
285
  if (this.bridgeListener) await this.bridgeListener.disconnect();
286
+
287
+ // Cleanup new components
288
+ if (this.harvester) await this.harvester.stop();
289
+ if (this.syncInterval) clearInterval(this.syncInterval);
290
+
254
291
  this.state.isRunning = false;
255
292
  console.log(chalk.green('āœ“ Daemon stopped.'));
256
293
  this.emit('stopped', this.state);
@@ -0,0 +1,199 @@
1
+ import { EventEmitter } from 'events';
2
+ import chokidar from 'chokidar';
3
+ import path from 'path';
4
+ import fs from 'fs/promises';
5
+ import crypto from 'crypto';
6
+ import axios from 'axios';
7
+ import { Logger } from '../utils/logger.js';
8
+
9
+ export interface HarvesterConfig {
10
+ projectId: string;
11
+ apiUrl: string;
12
+ apiKey: string;
13
+ watchPath: string; // usually process.cwd()
14
+ }
15
+
16
+ /**
17
+ * The Knowledge Harvester 🌾
18
+ *
19
+ * Watches the local rule repositories (.cursor/rules/*.mdc) for new patterns
20
+ * or corrections that the user teaches their local AI agent.
21
+ *
22
+ * When a new insight is detected, it is harvested (read), validated,
23
+ * and shipped to the Rigstate Curator Protocol as a signal.
24
+ */
25
+ export class KnowledgeHarvester extends EventEmitter {
26
+ private watcher: chokidar.FSWatcher | null = null;
27
+ private config: HarvesterConfig;
28
+ private ruleHashes: Map<string, string> = new Map();
29
+ private isReady: boolean = false;
30
+ private processingQueue: Set<string> = new Set();
31
+ private debounceTimers: Map<string, NodeJS.Timeout> = new Map();
32
+
33
+ // Ignore list to prevent feedback loops with system rules
34
+ private IGNORED_PREFIXES = ['rigstate-identity', 'rigstate-guardian'];
35
+
36
+ constructor(config: HarvesterConfig) {
37
+ super();
38
+ this.config = config;
39
+ }
40
+
41
+ async start() {
42
+ // Load initial state to establish baseline (don't harvest initial load)
43
+ await this.loadHashes();
44
+
45
+ const rulesPath = path.join(this.config.watchPath, '.cursor', 'rules');
46
+ const watchPattern = path.join(rulesPath, '**', '*.mdc');
47
+
48
+ Logger.debug(`🌾 Harvester watching: ${watchPattern}`);
49
+
50
+ this.watcher = chokidar.watch(watchPattern, {
51
+ persistent: true,
52
+ ignoreInitial: true, // Don't harvest what's already there on boot
53
+ awaitWriteFinish: {
54
+ stabilityThreshold: 2000,
55
+ pollInterval: 100
56
+ }
57
+ });
58
+
59
+ this.watcher
60
+ .on('add', (path) => this.handleFileEvent(path, 'add'))
61
+ .on('change', (path) => this.handleFileEvent(path, 'change')); // We treat change same as add (new version of truth)
62
+
63
+ this.isReady = true;
64
+ }
65
+
66
+ async stop() {
67
+ if (this.watcher) {
68
+ await this.watcher.close();
69
+ this.watcher = null;
70
+ }
71
+ // Save state? Maybe not needed as we re-hash on boot.
72
+ }
73
+
74
+ private async handleFileEvent(filePath: string, event: 'add' | 'change') {
75
+ const fileName = path.basename(filePath);
76
+
77
+ // 1. Filter ignored files
78
+ if (this.IGNORED_PREFIXES.some(prefix => fileName.startsWith(prefix))) {
79
+ return;
80
+ }
81
+
82
+ // 2. Debounce (users save frequently)
83
+ if (this.debounceTimers.has(filePath)) {
84
+ clearTimeout(this.debounceTimers.get(filePath));
85
+ }
86
+
87
+ this.debounceTimers.set(filePath, setTimeout(async () => {
88
+ this.processFile(filePath);
89
+ this.debounceTimers.delete(filePath);
90
+ }, 5000)); // 5 second quiet period
91
+ }
92
+
93
+ private async processFile(filePath: string) {
94
+ if (this.processingQueue.has(filePath)) return;
95
+ this.processingQueue.add(filePath);
96
+
97
+ try {
98
+ const content = await fs.readFile(filePath, 'utf-8');
99
+ const currentHash = this.computeHash(content);
100
+
101
+ // 3. Check against memory (did we just harvest this?)
102
+ // We also need to check against the server-sync cache (did we just download this?)
103
+ // For now, simple memory check.
104
+ if (this.ruleHashes.get(filePath) === currentHash) {
105
+ Logger.debug(`Skipping ${path.basename(filePath)} (unchanged hash)`);
106
+ return;
107
+ }
108
+
109
+ // 4. Validate Content
110
+ if (content.length < 20) {
111
+ Logger.debug(`Skipping ${path.basename(filePath)} (too short)`);
112
+ return;
113
+ }
114
+
115
+ // 5. Submit Signal
116
+ await this.submitSignal(filePath, content);
117
+
118
+ // 6. Update Hash
119
+ this.ruleHashes.set(filePath, currentHash);
120
+
121
+ } catch (error: any) {
122
+ Logger.warn(`Harvester failed to process ${path.basename(filePath)}: ${error.message}`);
123
+ } finally {
124
+ this.processingQueue.delete(filePath);
125
+ }
126
+ }
127
+
128
+ private async submitSignal(filePath: string, content: string) {
129
+ const title = path.basename(filePath, '.mdc');
130
+ const relativePath = path.relative(process.cwd(), filePath);
131
+
132
+ Logger.info(`🌾 Harvesting new knowledge: ${title}`);
133
+
134
+ try {
135
+ // NOTE: We're using the 'mcp_submit_curator_signal' endpoint logic here
136
+ // URL: /api/v1/curator/signals (Assumed endpoint based on user request)
137
+
138
+ // Extract frontmatter description if possible
139
+ const descriptionMatch = content.match(/description:\s*(.*)/);
140
+ const description = descriptionMatch ? descriptionMatch[1].trim() : "Auto-harvested from IDE interaction";
141
+
142
+ const payload = {
143
+ project_id: this.config.projectId,
144
+ title: title,
145
+ category: 'ARCHITECTURE', // Default
146
+ severity: 'MEDIUM',
147
+ instruction: content,
148
+ reasoning: `Harvested from local file: ${relativePath}`,
149
+ source_type: 'IDE_HARVESTER'
150
+ };
151
+
152
+ const response = await axios.post(`${this.config.apiUrl}/api/v1/curator/signals`, payload, {
153
+ headers: { Authorization: `Bearer ${this.config.apiKey}` }
154
+ });
155
+
156
+ if (response.data.success) {
157
+ Logger.info(`āœ… Signal submitted for review: ${title}`);
158
+ } else {
159
+ throw new Error(response.data.error || 'Unknown API error');
160
+ }
161
+
162
+ } catch (error: any) {
163
+ if (error.response?.status === 404) {
164
+ // API endpoint might not exist yet during migration
165
+ Logger.debug('Curator API not reachable (404). Signal stored locally (mock).');
166
+ } else {
167
+ Logger.error(`Failed to submit signal: ${error.message}`);
168
+ // Don't update hash so we try again next edit
169
+ this.ruleHashes.delete(filePath);
170
+ throw error; // Re-throw to catch block
171
+ }
172
+ }
173
+ }
174
+
175
+ private async loadHashes() {
176
+ // Initial scan to build baseline so we don't upload everything on start
177
+ const rulesPath = path.join(this.config.watchPath, '.cursor', 'rules');
178
+ try {
179
+ // Ensure dir exists
180
+ await fs.mkdir(rulesPath, { recursive: true });
181
+
182
+ // This is a simplified recursive walk since we know structure is flat usually
183
+ const files = await fs.readdir(rulesPath);
184
+ for (const file of files) {
185
+ if (file.endsWith('.mdc')) {
186
+ const fullPath = path.join(rulesPath, file);
187
+ const content = await fs.readFile(fullPath, 'utf-8');
188
+ this.ruleHashes.set(fullPath, this.computeHash(content));
189
+ }
190
+ }
191
+ } catch (e) {
192
+ // Directory might not exist yet, that's fine
193
+ }
194
+ }
195
+
196
+ private computeHash(content: string): string {
197
+ return crypto.createHash('sha256').update(content).digest('hex');
198
+ }
199
+ }
package/src/index.ts CHANGED
@@ -61,7 +61,6 @@ program.addCommand(createOverrideCommand());
61
61
  program.addCommand(createIdeaCommand());
62
62
  program.addCommand(createReleaseCommand());
63
63
  program.addCommand(createRoadmapCommand());
64
- program.addCommand(createRoadmapCommand());
65
64
  program.addCommand(createCouncilCommand());
66
65
  program.addCommand(createPlanCommand());
67
66