@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/dist/index.cjs +321 -165
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +325 -169
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/daemon/core.ts +38 -1
- package/src/daemon/harvester.ts +199 -0
- package/src/index.ts +0 -1
package/package.json
CHANGED
package/src/daemon/core.ts
CHANGED
|
@@ -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
|
|
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
|
|