vibe-coder-kit 6.0.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.
@@ -0,0 +1,199 @@
1
+ // Knowledge Store
2
+ // Semantic search and knowledge management
3
+
4
+ import { readFileSync, writeFileSync, existsSync, readdirSync } from 'fs';
5
+ import { join, resolve, normalize } from 'path';
6
+
7
+ export interface KnowledgeEntry {
8
+ id: string;
9
+ title: string;
10
+ date: string;
11
+ tags: string[];
12
+ context: string;
13
+ solution: string;
14
+ phase?: string;
15
+ relatedEntries?: string[];
16
+ embedding?: number[];
17
+ }
18
+
19
+ export interface SearchResult {
20
+ entry: KnowledgeEntry;
21
+ score: number;
22
+ }
23
+
24
+ export class KnowledgeStore {
25
+ private knowledgeDir: string;
26
+ private indexPath: string;
27
+ private entries: KnowledgeEntry[] = [];
28
+
29
+ constructor(knowledgeDir: string) {
30
+ // Normalize and validate path to prevent path traversal
31
+ this.knowledgeDir = resolve(normalize(knowledgeDir));
32
+ this.indexPath = join(this.knowledgeDir, 'INDEX.md');
33
+ this.loadEntries();
34
+ }
35
+
36
+ private isPathSafe(filePath: string): boolean {
37
+ const resolved = resolve(normalize(filePath));
38
+ return resolved.startsWith(this.knowledgeDir);
39
+ }
40
+
41
+ private loadEntries(): void {
42
+ if (!existsSync(this.knowledgeDir)) {
43
+ console.warn(`[KnowledgeStore] Directory not found: ${this.knowledgeDir}`);
44
+ return;
45
+ }
46
+
47
+ try {
48
+ const files = readdirSync(this.knowledgeDir).filter(f => f.endsWith('.md') && f !== 'INDEX.md');
49
+
50
+ for (const file of files) {
51
+ const filePath = join(this.knowledgeDir, file);
52
+
53
+ // Path traversal check
54
+ if (!this.isPathSafe(filePath)) {
55
+ console.warn(`[KnowledgeStore] Skipping potentially unsafe path: ${file}`);
56
+ continue;
57
+ }
58
+
59
+ try {
60
+ const content = readFileSync(filePath, 'utf-8');
61
+ const entry = this.parseEntry(content, file);
62
+ if (entry) this.entries.push(entry);
63
+ } catch (e) {
64
+ console.error(`[KnowledgeStore] Failed to read ${file}: ${(e as Error).message}`);
65
+ }
66
+ }
67
+ } catch (e) {
68
+ console.error(`[KnowledgeStore] Failed to load entries: ${(e as Error).message}`);
69
+ }
70
+ }
71
+
72
+ private parseEntry(content: string, filename: string): KnowledgeEntry | null {
73
+ const titleMatch = content.match(/^#\s+(.+)/m);
74
+ const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/);
75
+ const tagsMatch = content.match(/\*\*Tags:\*\*\s*(.+)/);
76
+ const contextMatch = content.match(/## Context\n([\s\S]*?)(?=\n##|$)/);
77
+ const solutionMatch = content.match(/## Solution\n([\s\S]*?)(?=\n##|$)/);
78
+ const phaseMatch = content.match(/\*\*Phase:\*\*\s*(.+)/);
79
+
80
+ if (!titleMatch) return null;
81
+
82
+ return {
83
+ id: filename.replace('.md', ''),
84
+ title: titleMatch[1].trim(),
85
+ date: dateMatch?.[1]?.trim() || new Date().toISOString().split('T')[0],
86
+ tags: tagsMatch?.[1]?.split(',').map(t => t.trim()) || [],
87
+ context: contextMatch?.[1]?.trim() || '',
88
+ solution: solutionMatch?.[1]?.trim() || '',
89
+ phase: phaseMatch?.[1]?.trim(),
90
+ };
91
+ }
92
+
93
+ addEntry(entry: Omit<KnowledgeEntry, 'id'>): KnowledgeEntry {
94
+ const id = `knowledge-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
95
+ const newEntry: KnowledgeEntry = { ...entry, id };
96
+ this.entries.push(newEntry);
97
+ this.saveEntry(newEntry);
98
+ this.updateIndex();
99
+ return newEntry;
100
+ }
101
+
102
+ private saveEntry(entry: KnowledgeEntry): void {
103
+ const filePath = join(this.knowledgeDir, `${entry.id}.md`);
104
+
105
+ // Path traversal check
106
+ if (!this.isPathSafe(filePath)) {
107
+ throw new Error(`Invalid file path: ${entry.id}.md`);
108
+ }
109
+
110
+ const content = `# ${entry.title}
111
+
112
+ **Date:** ${entry.date}
113
+ **Tags:** ${entry.tags.join(', ')}
114
+ ${entry.phase ? `**Phase:** ${entry.phase}` : ''}
115
+
116
+ ## Context
117
+
118
+ ${entry.context}
119
+
120
+ ## Solution
121
+
122
+ ${entry.solution}
123
+
124
+ ${entry.relatedEntries?.length ? `## Related\n\n${entry.relatedEntries.map(r => `- ${r}`).join('\n')}` : ''}
125
+ `;
126
+ writeFileSync(filePath, content);
127
+ }
128
+
129
+ private updateIndex(): void {
130
+ let index = '# Knowledge Index\n\n';
131
+ index += '| # | Date | Title | Tags | File |\n';
132
+ index += '|---|------|-------|------|------|\n';
133
+
134
+ this.entries
135
+ .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
136
+ .forEach((entry, i) => {
137
+ index += `| ${i + 1} | ${entry.date} | ${entry.title} | ${entry.tags.join(', ')} | ${entry.id}.md |\n`;
138
+ });
139
+
140
+ writeFileSync(this.indexPath, index);
141
+ }
142
+
143
+ search(query: string): SearchResult[] {
144
+ // Limit query length to prevent ReDoS
145
+ const sanitizedQuery = query.substring(0, 100);
146
+ const queryLower = sanitizedQuery.toLowerCase();
147
+ const queryWords = queryLower.split(/\s+/).filter(w => w.length > 0);
148
+
149
+ return this.entries
150
+ .map(entry => {
151
+ let score = 0;
152
+ const entryText = `${entry.title} ${entry.tags.join(' ')} ${entry.context} ${entry.solution}`.toLowerCase();
153
+
154
+ for (const word of queryWords) {
155
+ // Escape regex special characters
156
+ const escapedWord = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
157
+ const regex = new RegExp(escapedWord, 'gi');
158
+
159
+ if (entryText.includes(word)) score += 1;
160
+ if (entry.title.toLowerCase().includes(word)) score += 2;
161
+ if (entry.tags.some(t => t.toLowerCase().includes(word))) score += 1.5;
162
+ }
163
+
164
+ return { entry, score };
165
+ })
166
+ .filter(r => r.score > 0)
167
+ .sort((a, b) => b.score - a.score);
168
+ }
169
+
170
+ getByTag(tag: string): KnowledgeEntry[] {
171
+ return this.entries.filter(e => e.tags.includes(tag));
172
+ }
173
+
174
+ getByPhase(phase: string): KnowledgeEntry[] {
175
+ return this.entries.filter(e => e.phase === phase);
176
+ }
177
+
178
+ getRecent(count: number = 10): KnowledgeEntry[] {
179
+ return [...this.entries]
180
+ .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
181
+ .slice(0, count);
182
+ }
183
+
184
+ getStats(): { total: number; byTag: Record<string, number>; byPhase: Record<string, number> } {
185
+ const byTag: Record<string, number> = {};
186
+ const byPhase: Record<string, number> = {};
187
+
188
+ for (const entry of this.entries) {
189
+ for (const tag of entry.tags) {
190
+ byTag[tag] = (byTag[tag] || 0) + 1;
191
+ }
192
+ if (entry.phase) {
193
+ byPhase[entry.phase] = (byPhase[entry.phase] || 0) + 1;
194
+ }
195
+ }
196
+
197
+ return { total: this.entries.length, byTag, byPhase };
198
+ }
199
+ }
@@ -0,0 +1,174 @@
1
+ // Plugin Registry
2
+ // Hot-reloadable plugin system for rules and hooks
3
+
4
+ import { readFileSync, readdirSync, existsSync, watchFile } from 'fs';
5
+ import { join, extname } from 'path';
6
+ import { parse as parseYaml } from 'yaml';
7
+
8
+ export interface Rule {
9
+ id: string;
10
+ name: string;
11
+ pattern?: string;
12
+ action: 'block' | 'warn' | 'info';
13
+ message: string;
14
+ severity: 'critical' | 'high' | 'medium' | 'low';
15
+ category: string;
16
+ }
17
+
18
+ export interface Hook {
19
+ phase: string;
20
+ action: string;
21
+ priority: number;
22
+ timing: 'pre' | 'post';
23
+ }
24
+
25
+ export interface Plugin {
26
+ name: string;
27
+ version: string;
28
+ description: string;
29
+ author: string;
30
+ rules: Rule[];
31
+ hooks: Hook[];
32
+ config: Record<string, unknown>;
33
+ enabled: boolean;
34
+ }
35
+
36
+ export class PluginRegistry {
37
+ private plugins: Map<string, Plugin> = new Map();
38
+ private pluginsDir: string;
39
+
40
+ constructor(pluginsDir: string) {
41
+ this.pluginsDir = pluginsDir;
42
+ this.loadPlugins();
43
+ }
44
+
45
+ private loadPlugins(): void {
46
+ const coreDir = join(this.pluginsDir, 'core');
47
+ const customDir = join(this.pluginsDir, 'custom');
48
+
49
+ if (existsSync(coreDir)) this.loadFromDir(coreDir);
50
+ if (existsSync(customDir)) this.loadFromDir(customDir);
51
+ }
52
+
53
+ private loadFromDir(dir: string): void {
54
+ const entries = readdirSync(dir, { withFileTypes: true });
55
+
56
+ for (const entry of entries) {
57
+ if (entry.isDirectory()) {
58
+ const pluginDir = join(dir, entry.name);
59
+ const manifestPath = join(pluginDir, 'plugin.yaml');
60
+
61
+ if (existsSync(manifestPath)) {
62
+ try {
63
+ const content = readFileSync(manifestPath, 'utf-8');
64
+ const manifest = parseYaml(content);
65
+ const rules = this.loadRules(pluginDir);
66
+ const hooks = this.loadHooks(pluginDir);
67
+
68
+ const plugin: Plugin = {
69
+ name: manifest.name || entry.name,
70
+ version: manifest.version || '1.0.0',
71
+ description: manifest.description || '',
72
+ author: manifest.author || 'unknown',
73
+ rules,
74
+ hooks,
75
+ config: manifest.config || {},
76
+ enabled: manifest.enabled !== false,
77
+ };
78
+
79
+ this.plugins.set(plugin.name, plugin);
80
+ console.log(`[PluginRegistry] Loaded: ${plugin.name} v${plugin.version} (${rules.length} rules, ${hooks.length} hooks)`);
81
+ } catch (err) {
82
+ console.error(`[PluginRegistry] Failed to load ${entry.name}:`, err);
83
+ }
84
+ }
85
+ }
86
+ }
87
+ }
88
+
89
+ private loadRules(pluginDir: string): Rule[] {
90
+ const rulesFile = join(pluginDir, 'rules.yaml');
91
+ if (!existsSync(rulesFile)) return [];
92
+
93
+ try {
94
+ const content = readFileSync(rulesFile, 'utf-8');
95
+ const data = parseYaml(content);
96
+ return data.rules || [];
97
+ } catch {
98
+ return [];
99
+ }
100
+ }
101
+
102
+ private loadHooks(pluginDir: string): Hook[] {
103
+ const hooksFile = join(pluginDir, 'hooks.yaml');
104
+ if (!existsSync(hooksFile)) return [];
105
+
106
+ try {
107
+ const content = readFileSync(hooksFile, 'utf-8');
108
+ const data = parseYaml(content);
109
+ return data.hooks || [];
110
+ } catch {
111
+ return [];
112
+ }
113
+ }
114
+
115
+ getPlugin(name: string): Plugin | undefined {
116
+ return this.plugins.get(name);
117
+ }
118
+
119
+ getAllPlugins(): Plugin[] {
120
+ return Array.from(this.plugins.values());
121
+ }
122
+
123
+ getEnabledPlugins(): Plugin[] {
124
+ return this.getAllPlugins().filter(p => p.enabled);
125
+ }
126
+
127
+ getAllRules(): Rule[] {
128
+ return this.getEnabledPlugins().flatMap(p => p.rules);
129
+ }
130
+
131
+ getHooksForPhase(phase: string, timing: 'pre' | 'post'): Hook[] {
132
+ return this.getEnabledPlugins()
133
+ .flatMap(p => p.hooks)
134
+ .filter(h => h.phase === phase && h.timing === timing)
135
+ .sort((a, b) => a.priority - b.priority);
136
+ }
137
+
138
+ validateRule(code: string): Array<{ rule: Rule; matched: boolean; line?: number }> {
139
+ const rules = this.getAllRules();
140
+ const results: Array<{ rule: Rule; matched: boolean; line?: number }> = [];
141
+
142
+ for (const rule of rules) {
143
+ if (rule.pattern) {
144
+ try {
145
+ const regex = new RegExp(rule.pattern, 'gm');
146
+ const match = regex.exec(code);
147
+ results.push({
148
+ rule,
149
+ matched: match !== null,
150
+ line: match ? this.getLineNumber(code, match.index) : undefined,
151
+ });
152
+ } catch {
153
+ results.push({ rule, matched: false });
154
+ }
155
+ }
156
+ }
157
+
158
+ return results;
159
+ }
160
+
161
+ private getLineNumber(code: string, index: number): number {
162
+ return code.substring(0, index).split('\n').length;
163
+ }
164
+
165
+ enablePlugin(name: string): void {
166
+ const plugin = this.plugins.get(name);
167
+ if (plugin) plugin.enabled = true;
168
+ }
169
+
170
+ disablePlugin(name: string): void {
171
+ const plugin = this.plugins.get(name);
172
+ if (plugin) plugin.enabled = false;
173
+ }
174
+ }
@@ -0,0 +1,141 @@
1
+ // Saga Pattern
2
+ // Compensating transactions for workflow rollback
3
+
4
+ export interface SagaStep<T = unknown> {
5
+ name: string;
6
+ execute: () => Promise<T>;
7
+ compensate: (result: T) => Promise<void>;
8
+ }
9
+
10
+ export interface SagaConfig {
11
+ maxRetries: number;
12
+ retryDelayMs: number;
13
+ maxRetryDelayMs: number;
14
+ stepTimeoutMs: number;
15
+ }
16
+
17
+ export interface SagaResult {
18
+ success: boolean;
19
+ completedSteps: string[];
20
+ failedCompensations: string[];
21
+ error?: string;
22
+ }
23
+
24
+ export class Saga {
25
+ private steps: SagaStep[] = [];
26
+ private config: SagaConfig;
27
+
28
+ constructor(config?: Partial<SagaConfig>) {
29
+ this.config = {
30
+ maxRetries: Math.min(config?.maxRetries ?? 3, 10), // Cap at 10
31
+ retryDelayMs: config?.retryDelayMs ?? 1000,
32
+ maxRetryDelayMs: config?.maxRetryDelayMs ?? 30000, // 30s max
33
+ stepTimeoutMs: config?.stepTimeoutMs ?? 60000, // 60s per step
34
+ };
35
+ }
36
+
37
+ addStep<T>(step: SagaStep<T>): void {
38
+ this.steps.push(step as SagaStep);
39
+ }
40
+
41
+ async execute(): Promise<SagaResult> {
42
+ const completedSteps: Array<{ name: string; result: unknown }> = [];
43
+
44
+ for (const step of this.steps) {
45
+ let retries = 0;
46
+ let lastError: Error | undefined;
47
+
48
+ while (retries < this.config.maxRetries) {
49
+ try {
50
+ console.log(`[Saga] Executing: ${step.name} (attempt ${retries + 1})`);
51
+
52
+ // Add timeout to step execution
53
+ const result = await this.executeWithTimeout(
54
+ () => step.execute(),
55
+ this.config.stepTimeoutMs
56
+ );
57
+
58
+ completedSteps.push({ name: step.name, result });
59
+ break;
60
+ } catch (error) {
61
+ lastError = error as Error;
62
+ retries++;
63
+
64
+ if (retries < this.config.maxRetries) {
65
+ const delayMs = Math.min(
66
+ this.config.retryDelayMs * retries,
67
+ this.config.maxRetryDelayMs
68
+ );
69
+ console.log(`[Saga] Retrying ${step.name} (${retries}/${this.config.maxRetries}) after ${delayMs}ms`);
70
+ await this.delay(delayMs);
71
+ }
72
+ }
73
+ }
74
+
75
+ if (lastError && retries >= this.config.maxRetries) {
76
+ console.log(`[Saga] Step ${step.name} failed after ${retries} retries`);
77
+ const failedCompensations = await this.compensate([...completedSteps].reverse());
78
+ return {
79
+ success: false,
80
+ completedSteps: completedSteps.map(s => s.name),
81
+ failedCompensations,
82
+ error: `Step ${step.name} failed: ${lastError.message}`,
83
+ };
84
+ }
85
+ }
86
+
87
+ return {
88
+ success: true,
89
+ completedSteps: completedSteps.map(s => s.name),
90
+ failedCompensations: [],
91
+ };
92
+ }
93
+
94
+ private async executeWithTimeout<T>(
95
+ operation: () => Promise<T>,
96
+ timeoutMs: number
97
+ ): Promise<T> {
98
+ return new Promise<T>((resolve, reject) => {
99
+ const timeoutId = setTimeout(() => {
100
+ reject(new Error(`Operation timed out after ${timeoutMs}ms`));
101
+ }, timeoutMs);
102
+
103
+ operation()
104
+ .then(result => {
105
+ clearTimeout(timeoutId);
106
+ resolve(result);
107
+ })
108
+ .catch(error => {
109
+ clearTimeout(timeoutId);
110
+ reject(error);
111
+ });
112
+ });
113
+ }
114
+
115
+ private async compensate(completedSteps: Array<{ name: string; result: unknown }>): Promise<string[]> {
116
+ console.log('[Saga] Compensating completed steps...');
117
+ const failedCompensations: string[] = [];
118
+
119
+ for (const { name, result } of completedSteps) {
120
+ const step = this.steps.find(s => s.name === name);
121
+ if (step) {
122
+ try {
123
+ console.log(`[Saga] Compensating: ${name}`);
124
+ await this.executeWithTimeout(
125
+ () => step.compensate(result),
126
+ this.config.stepTimeoutMs
127
+ );
128
+ } catch (error) {
129
+ console.error(`[Saga] Compensation failed for ${name}:`, (error as Error).message);
130
+ failedCompensations.push(name);
131
+ }
132
+ }
133
+ }
134
+ return failedCompensations;
135
+ }
136
+
137
+ private delay(ms: number): Promise<void> {
138
+ const delayMs = Math.max(0, Math.min(ms, this.config.maxRetryDelayMs));
139
+ return new Promise(resolve => setTimeout(resolve, delayMs));
140
+ }
141
+ }