@plures/runebook 0.4.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.
- package/ANALYSIS_LADDER.md +231 -0
- package/CHANGELOG.md +124 -0
- package/INTEGRATIONS.md +242 -0
- package/LICENSE +21 -0
- package/MEMORY.md +253 -0
- package/NIXOS.md +357 -0
- package/QUICKSTART.md +157 -0
- package/README.md +295 -0
- package/RELEASE.md +190 -0
- package/ValidationChecklist.md +598 -0
- package/docs/demo.md +338 -0
- package/docs/llm-integration.md +300 -0
- package/docs/parallel-execution-plan.md +160 -0
- package/flake.nix +228 -0
- package/integrations/README.md +242 -0
- package/integrations/demo-steps.sh +64 -0
- package/integrations/nvim-runebook.lua +140 -0
- package/integrations/tmux-status.sh +51 -0
- package/integrations/vim-runebook.vim +77 -0
- package/integrations/wezterm-status-simple.lua +48 -0
- package/integrations/wezterm-status.lua +76 -0
- package/nixos-module.nix +156 -0
- package/package.json +76 -0
- package/packages/design-dojo/index.js +4 -0
- package/packages/design-dojo/package.json +20 -0
- package/packages/design-dojo/tokens.css +69 -0
- package/playwright.config.ts +16 -0
- package/scripts/check-versions.cjs +62 -0
- package/scripts/demo.sh +220 -0
- package/shell.nix +31 -0
- package/src/app.html +13 -0
- package/src/cli/index.ts +1050 -0
- package/src/lib/agent/analysis-pipeline.ts +347 -0
- package/src/lib/agent/analysis-service.ts +171 -0
- package/src/lib/agent/analysis.ts +159 -0
- package/src/lib/agent/analyzers/heuristic.ts +289 -0
- package/src/lib/agent/analyzers/index.ts +7 -0
- package/src/lib/agent/analyzers/llm.ts +204 -0
- package/src/lib/agent/analyzers/local-search.ts +215 -0
- package/src/lib/agent/capture.ts +123 -0
- package/src/lib/agent/index.ts +244 -0
- package/src/lib/agent/integration.ts +81 -0
- package/src/lib/agent/llm/providers/base.ts +99 -0
- package/src/lib/agent/llm/providers/index.ts +60 -0
- package/src/lib/agent/llm/providers/mock.ts +67 -0
- package/src/lib/agent/llm/providers/ollama.ts +151 -0
- package/src/lib/agent/llm/providers/openai.ts +153 -0
- package/src/lib/agent/llm/sanitizer.ts +170 -0
- package/src/lib/agent/llm/types.ts +118 -0
- package/src/lib/agent/memory.ts +363 -0
- package/src/lib/agent/node-status.ts +56 -0
- package/src/lib/agent/node-suggestions.ts +64 -0
- package/src/lib/agent/status.ts +80 -0
- package/src/lib/agent/suggestions.ts +169 -0
- package/src/lib/components/Canvas.svelte +124 -0
- package/src/lib/components/ConnectionLine.svelte +46 -0
- package/src/lib/components/DisplayNode.svelte +167 -0
- package/src/lib/components/InputNode.svelte +158 -0
- package/src/lib/components/TerminalNode.svelte +237 -0
- package/src/lib/components/Toolbar.svelte +359 -0
- package/src/lib/components/TransformNode.svelte +327 -0
- package/src/lib/core/index.ts +31 -0
- package/src/lib/core/observer.ts +278 -0
- package/src/lib/core/redaction.ts +158 -0
- package/src/lib/core/shell-adapters/base.ts +325 -0
- package/src/lib/core/shell-adapters/bash.ts +110 -0
- package/src/lib/core/shell-adapters/index.ts +62 -0
- package/src/lib/core/shell-adapters/zsh.ts +105 -0
- package/src/lib/core/storage.ts +360 -0
- package/src/lib/core/types.ts +176 -0
- package/src/lib/design-dojo/Box.svelte +47 -0
- package/src/lib/design-dojo/Button.svelte +75 -0
- package/src/lib/design-dojo/Input.svelte +65 -0
- package/src/lib/design-dojo/List.svelte +38 -0
- package/src/lib/design-dojo/Select.svelte +48 -0
- package/src/lib/design-dojo/SplitPane.svelte +43 -0
- package/src/lib/design-dojo/StatusBar.svelte +61 -0
- package/src/lib/design-dojo/Table.svelte +47 -0
- package/src/lib/design-dojo/Text.svelte +36 -0
- package/src/lib/design-dojo/Toggle.svelte +48 -0
- package/src/lib/design-dojo/index.ts +10 -0
- package/src/lib/stores/canvas-praxis.ts +268 -0
- package/src/lib/stores/canvas.ts +58 -0
- package/src/lib/types/agent.ts +78 -0
- package/src/lib/types/canvas.ts +71 -0
- package/src/lib/utils/storage.ts +326 -0
- package/src/lib/utils/yaml-loader.ts +52 -0
- package/src/routes/+layout.svelte +5 -0
- package/src/routes/+layout.ts +5 -0
- package/src/routes/+page.svelte +32 -0
- package/src-tauri/Cargo.lock +5735 -0
- package/src-tauri/Cargo.toml +38 -0
- package/src-tauri/build.rs +3 -0
- package/src-tauri/capabilities/default.json +10 -0
- package/src-tauri/icons/128x128.png +0 -0
- package/src-tauri/icons/128x128@2x.png +0 -0
- package/src-tauri/icons/32x32.png +0 -0
- package/src-tauri/icons/Square107x107Logo.png +0 -0
- package/src-tauri/icons/Square142x142Logo.png +0 -0
- package/src-tauri/icons/Square150x150Logo.png +0 -0
- package/src-tauri/icons/Square284x284Logo.png +0 -0
- package/src-tauri/icons/Square30x30Logo.png +0 -0
- package/src-tauri/icons/Square310x310Logo.png +0 -0
- package/src-tauri/icons/Square44x44Logo.png +0 -0
- package/src-tauri/icons/Square71x71Logo.png +0 -0
- package/src-tauri/icons/Square89x89Logo.png +0 -0
- package/src-tauri/icons/StoreLogo.png +0 -0
- package/src-tauri/icons/icon.icns +0 -0
- package/src-tauri/icons/icon.ico +0 -0
- package/src-tauri/icons/icon.png +0 -0
- package/src-tauri/src/agents/agent1.rs +66 -0
- package/src-tauri/src/agents/agent2.rs +80 -0
- package/src-tauri/src/agents/agent3.rs +73 -0
- package/src-tauri/src/agents/agent4.rs +66 -0
- package/src-tauri/src/agents/agent5.rs +68 -0
- package/src-tauri/src/agents/agent6.rs +75 -0
- package/src-tauri/src/agents/base.rs +52 -0
- package/src-tauri/src/agents/mod.rs +17 -0
- package/src-tauri/src/core/coordination.rs +117 -0
- package/src-tauri/src/core/mod.rs +12 -0
- package/src-tauri/src/core/ownership.rs +61 -0
- package/src-tauri/src/core/types.rs +132 -0
- package/src-tauri/src/execution/mod.rs +5 -0
- package/src-tauri/src/execution/runner.rs +143 -0
- package/src-tauri/src/lib.rs +161 -0
- package/src-tauri/src/main.rs +6 -0
- package/src-tauri/src/memory/api.rs +422 -0
- package/src-tauri/src/memory/client.rs +156 -0
- package/src-tauri/src/memory/encryption.rs +79 -0
- package/src-tauri/src/memory/migration.rs +110 -0
- package/src-tauri/src/memory/mod.rs +28 -0
- package/src-tauri/src/memory/schema.rs +275 -0
- package/src-tauri/src/memory/tests.rs +192 -0
- package/src-tauri/src/orchestrator/coordinator.rs +232 -0
- package/src-tauri/src/orchestrator/mod.rs +13 -0
- package/src-tauri/src/orchestrator/planner.rs +304 -0
- package/src-tauri/tauri.conf.json +35 -0
- package/static/examples/date-time-example.yaml +147 -0
- package/static/examples/hello-world.yaml +74 -0
- package/static/examples/transform-example.yaml +157 -0
- package/static/favicon.png +0 -0
- package/static/svelte.svg +1 -0
- package/static/tauri.svg +6 -0
- package/static/vite.svg +1 -0
- package/svelte.config.js +18 -0
- package/tsconfig.json +19 -0
- package/vite.config.js +45 -0
- package/vitest.config.ts +21 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// Layer 1: Heuristic Classifiers
|
|
2
|
+
// Common error patterns: nix errors, git auth, missing attrs, syntax errors
|
|
3
|
+
|
|
4
|
+
import type { Analyzer, AnalysisContext, AnalysisSuggestion } from '../analysis-pipeline';
|
|
5
|
+
import type { EventStore } from '../../core/types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Heuristic analyzer for common Nix errors
|
|
9
|
+
*/
|
|
10
|
+
export class NixErrorAnalyzer implements Analyzer {
|
|
11
|
+
name = 'nix-error';
|
|
12
|
+
layer = 1;
|
|
13
|
+
|
|
14
|
+
async analyze(context: AnalysisContext, _store: EventStore): Promise<AnalysisSuggestion[]> {
|
|
15
|
+
const suggestions: AnalysisSuggestion[] = [];
|
|
16
|
+
const stderr = context.stderr.toLowerCase();
|
|
17
|
+
const stdout = context.stdout.toLowerCase();
|
|
18
|
+
|
|
19
|
+
// Missing attribute errors
|
|
20
|
+
if (stderr.includes('attribute') && (stderr.includes('missing') || stderr.includes('undefined'))) {
|
|
21
|
+
const attrMatch = context.stderr.match(/attribute ['"]([^'"]+)['"]/i);
|
|
22
|
+
const attrName = attrMatch ? attrMatch[1] : 'unknown';
|
|
23
|
+
|
|
24
|
+
suggestions.push({
|
|
25
|
+
id: `suggestion_${Date.now()}_nix_missing_attr`,
|
|
26
|
+
type: 'warning',
|
|
27
|
+
priority: 'high',
|
|
28
|
+
title: 'Missing Nix Attribute',
|
|
29
|
+
description: `The attribute "${attrName}" is not defined. Check your flake.nix or configuration.`,
|
|
30
|
+
confidence: 0.9,
|
|
31
|
+
actionableSnippet: `# Check if "${attrName}" is defined in your flake.nix or imported modules`,
|
|
32
|
+
provenance: {
|
|
33
|
+
analyzer: this.name,
|
|
34
|
+
layer: this.layer,
|
|
35
|
+
timestamp: Date.now(),
|
|
36
|
+
},
|
|
37
|
+
timestamp: Date.now(),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// flake-parts template path errors
|
|
42
|
+
if (stderr.includes('template') && (stderr.includes('path') || stderr.includes('not found'))) {
|
|
43
|
+
suggestions.push({
|
|
44
|
+
id: `suggestion_${Date.now()}_flake_parts_template`,
|
|
45
|
+
type: 'warning',
|
|
46
|
+
priority: 'high',
|
|
47
|
+
title: 'Flake-Parts Template Path Error',
|
|
48
|
+
description: 'Template path not found. Check your flake-parts configuration and template paths.',
|
|
49
|
+
confidence: 0.85,
|
|
50
|
+
actionableSnippet: `# Verify template paths in your flake.nix:
|
|
51
|
+
# - Check imports.flake-parts.inputs
|
|
52
|
+
# - Verify template paths in perSystem or systems`,
|
|
53
|
+
provenance: {
|
|
54
|
+
analyzer: this.name,
|
|
55
|
+
layer: this.layer,
|
|
56
|
+
timestamp: Date.now(),
|
|
57
|
+
},
|
|
58
|
+
timestamp: Date.now(),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// buildEnv font conflicts
|
|
63
|
+
if (stderr.includes('font') && (stderr.includes('conflict') || stderr.includes('duplicate'))) {
|
|
64
|
+
suggestions.push({
|
|
65
|
+
id: `suggestion_${Date.now()}_nix_font_conflict`,
|
|
66
|
+
type: 'warning',
|
|
67
|
+
priority: 'medium',
|
|
68
|
+
title: 'Nix buildEnv Font Conflict',
|
|
69
|
+
description: 'Font conflict detected in buildEnv. Multiple packages may be providing the same font.',
|
|
70
|
+
confidence: 0.8,
|
|
71
|
+
actionableSnippet: `# Resolve font conflicts by:
|
|
72
|
+
# 1. Use buildEnv with ignoreCollisions = true
|
|
73
|
+
# 2. Or use fontconfig to manage font priorities
|
|
74
|
+
# 3. Check for duplicate font packages in your configuration`,
|
|
75
|
+
provenance: {
|
|
76
|
+
analyzer: this.name,
|
|
77
|
+
layer: this.layer,
|
|
78
|
+
timestamp: Date.now(),
|
|
79
|
+
},
|
|
80
|
+
timestamp: Date.now(),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Nix evaluation errors
|
|
85
|
+
if (stderr.includes('error:') && (stderr.includes('evaluation') || stderr.includes('nix'))) {
|
|
86
|
+
const errorMatch = context.stderr.match(/error:\s*(.+?)(?:\n|$)/i);
|
|
87
|
+
const errorMsg = errorMatch ? errorMatch[1].trim() : 'Unknown Nix error';
|
|
88
|
+
|
|
89
|
+
suggestions.push({
|
|
90
|
+
id: `suggestion_${Date.now()}_nix_eval_error`,
|
|
91
|
+
type: 'warning',
|
|
92
|
+
priority: 'high',
|
|
93
|
+
title: 'Nix Evaluation Error',
|
|
94
|
+
description: `Nix evaluation failed: ${errorMsg.substring(0, 100)}`,
|
|
95
|
+
confidence: 0.75,
|
|
96
|
+
actionableSnippet: `# Check your Nix expression for syntax errors
|
|
97
|
+
# Common issues:
|
|
98
|
+
# - Missing commas in attribute sets
|
|
99
|
+
# - Incorrect function calls
|
|
100
|
+
# - Type mismatches`,
|
|
101
|
+
provenance: {
|
|
102
|
+
analyzer: this.name,
|
|
103
|
+
layer: this.layer,
|
|
104
|
+
timestamp: Date.now(),
|
|
105
|
+
},
|
|
106
|
+
timestamp: Date.now(),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return suggestions;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Heuristic analyzer for Git authentication errors
|
|
116
|
+
*/
|
|
117
|
+
export class GitAuthAnalyzer implements Analyzer {
|
|
118
|
+
name = 'git-auth';
|
|
119
|
+
layer = 1;
|
|
120
|
+
|
|
121
|
+
async analyze(context: AnalysisContext, _store: EventStore): Promise<AnalysisSuggestion[]> {
|
|
122
|
+
const suggestions: AnalysisSuggestion[] = [];
|
|
123
|
+
const stderr = context.stderr.toLowerCase();
|
|
124
|
+
const stdout = context.stdout.toLowerCase();
|
|
125
|
+
const combined = stderr + stdout;
|
|
126
|
+
|
|
127
|
+
// GitHub rate limit
|
|
128
|
+
if (combined.includes('rate limit') || combined.includes('api rate limit')) {
|
|
129
|
+
suggestions.push({
|
|
130
|
+
id: `suggestion_${Date.now()}_github_rate_limit`,
|
|
131
|
+
type: 'warning',
|
|
132
|
+
priority: 'high',
|
|
133
|
+
title: 'GitHub Rate Limit Exceeded',
|
|
134
|
+
description: 'GitHub API rate limit exceeded. Wait before retrying or use authentication.',
|
|
135
|
+
confidence: 0.95,
|
|
136
|
+
actionableSnippet: `# Set GITHUB_TOKEN environment variable:
|
|
137
|
+
export GITHUB_TOKEN=your_token_here
|
|
138
|
+
|
|
139
|
+
# Or use gh auth login:
|
|
140
|
+
gh auth login`,
|
|
141
|
+
provenance: {
|
|
142
|
+
analyzer: this.name,
|
|
143
|
+
layer: this.layer,
|
|
144
|
+
timestamp: Date.now(),
|
|
145
|
+
},
|
|
146
|
+
timestamp: Date.now(),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Git authentication errors
|
|
151
|
+
if (combined.includes('authentication failed') ||
|
|
152
|
+
combined.includes('permission denied') ||
|
|
153
|
+
(combined.includes('git') && combined.includes('auth'))) {
|
|
154
|
+
suggestions.push({
|
|
155
|
+
id: `suggestion_${Date.now()}_git_auth`,
|
|
156
|
+
type: 'warning',
|
|
157
|
+
priority: 'high',
|
|
158
|
+
title: 'Git Authentication Error',
|
|
159
|
+
description: 'Git authentication failed. Check your credentials or token.',
|
|
160
|
+
confidence: 0.9,
|
|
161
|
+
actionableSnippet: `# Check git credentials:
|
|
162
|
+
git config --list | grep credential
|
|
163
|
+
|
|
164
|
+
# Set up authentication:
|
|
165
|
+
# For HTTPS: git config --global credential.helper store
|
|
166
|
+
# For SSH: ssh-keygen -t ed25519 -C "your_email@example.com"
|
|
167
|
+
# For GitHub: gh auth login`,
|
|
168
|
+
provenance: {
|
|
169
|
+
analyzer: this.name,
|
|
170
|
+
layer: this.layer,
|
|
171
|
+
timestamp: Date.now(),
|
|
172
|
+
},
|
|
173
|
+
timestamp: Date.now(),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Token environment variable issues
|
|
178
|
+
if (combined.includes('token') && (combined.includes('not set') || combined.includes('missing'))) {
|
|
179
|
+
const tokenMatch = context.stderr.match(/([A-Z_]+TOKEN|GITHUB_TOKEN|GITLAB_TOKEN)/i);
|
|
180
|
+
const tokenName = tokenMatch ? tokenMatch[1] : 'TOKEN';
|
|
181
|
+
|
|
182
|
+
suggestions.push({
|
|
183
|
+
id: `suggestion_${Date.now()}_token_env`,
|
|
184
|
+
type: 'warning',
|
|
185
|
+
priority: 'high',
|
|
186
|
+
title: 'Token Environment Variable Missing',
|
|
187
|
+
description: `The ${tokenName} environment variable is not set or is invalid.`,
|
|
188
|
+
confidence: 0.85,
|
|
189
|
+
actionableSnippet: `# Set the token environment variable:
|
|
190
|
+
export ${tokenName}=your_token_here
|
|
191
|
+
|
|
192
|
+
# Or add to your shell profile:
|
|
193
|
+
echo 'export ${tokenName}=your_token_here' >> ~/.bashrc # or ~/.zshrc`,
|
|
194
|
+
provenance: {
|
|
195
|
+
analyzer: this.name,
|
|
196
|
+
layer: this.layer,
|
|
197
|
+
timestamp: Date.now(),
|
|
198
|
+
},
|
|
199
|
+
timestamp: Date.now(),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return suggestions;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Heuristic analyzer for syntax errors
|
|
209
|
+
*/
|
|
210
|
+
export class SyntaxErrorAnalyzer implements Analyzer {
|
|
211
|
+
name = 'syntax-error';
|
|
212
|
+
layer = 1;
|
|
213
|
+
|
|
214
|
+
async analyze(context: AnalysisContext, _store: EventStore): Promise<AnalysisSuggestion[]> {
|
|
215
|
+
const suggestions: AnalysisSuggestion[] = [];
|
|
216
|
+
const stderr = context.stderr.toLowerCase();
|
|
217
|
+
const combined = context.stderr + context.stdout;
|
|
218
|
+
|
|
219
|
+
// Common syntax error patterns
|
|
220
|
+
if (stderr.includes('syntax error') || stderr.includes('parse error')) {
|
|
221
|
+
// Try to extract file and line number
|
|
222
|
+
const fileMatch = combined.match(/([^\s:]+):(\d+):/);
|
|
223
|
+
const file = fileMatch ? fileMatch[1] : 'unknown';
|
|
224
|
+
const line = fileMatch ? fileMatch[2] : 'unknown';
|
|
225
|
+
|
|
226
|
+
suggestions.push({
|
|
227
|
+
id: `suggestion_${Date.now()}_syntax_error`,
|
|
228
|
+
type: 'warning',
|
|
229
|
+
priority: 'high',
|
|
230
|
+
title: 'Syntax Error Detected',
|
|
231
|
+
description: `Syntax error in ${file} at line ${line}. Check the file for syntax issues.`,
|
|
232
|
+
confidence: 0.8,
|
|
233
|
+
actionableSnippet: `# Check ${file} at line ${line}:
|
|
234
|
+
# - Missing brackets, braces, or parentheses
|
|
235
|
+
# - Incorrect indentation
|
|
236
|
+
# - Missing semicolons or commas
|
|
237
|
+
# - Unclosed strings or comments`,
|
|
238
|
+
provenance: {
|
|
239
|
+
analyzer: this.name,
|
|
240
|
+
layer: this.layer,
|
|
241
|
+
timestamp: Date.now(),
|
|
242
|
+
},
|
|
243
|
+
timestamp: Date.now(),
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Missing command/executable
|
|
248
|
+
if (stderr.includes('command not found') || stderr.includes('not found')) {
|
|
249
|
+
const cmdMatch = context.stderr.match(/['"]?([^\s'"]+)['"]?\s+not found/i);
|
|
250
|
+
const cmd = cmdMatch ? cmdMatch[1] : 'command';
|
|
251
|
+
|
|
252
|
+
suggestions.push({
|
|
253
|
+
id: `suggestion_${Date.now()}_command_not_found`,
|
|
254
|
+
type: 'warning',
|
|
255
|
+
priority: 'medium',
|
|
256
|
+
title: 'Command Not Found',
|
|
257
|
+
description: `The command "${cmd}" is not found in your PATH.`,
|
|
258
|
+
confidence: 0.9,
|
|
259
|
+
actionableSnippet: `# Install the missing command or check your PATH:
|
|
260
|
+
which ${cmd}
|
|
261
|
+
echo $PATH
|
|
262
|
+
|
|
263
|
+
# For Nix users:
|
|
264
|
+
nix-shell -p ${cmd}
|
|
265
|
+
# Or add to your flake.nix`,
|
|
266
|
+
provenance: {
|
|
267
|
+
analyzer: this.name,
|
|
268
|
+
layer: this.layer,
|
|
269
|
+
timestamp: Date.now(),
|
|
270
|
+
},
|
|
271
|
+
timestamp: Date.now(),
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return suggestions;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Create all heuristic analyzers
|
|
281
|
+
*/
|
|
282
|
+
export function createHeuristicAnalyzers(): Analyzer[] {
|
|
283
|
+
return [
|
|
284
|
+
new NixErrorAnalyzer(),
|
|
285
|
+
new GitAuthAnalyzer(),
|
|
286
|
+
new SyntaxErrorAnalyzer(),
|
|
287
|
+
];
|
|
288
|
+
}
|
|
289
|
+
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// Layer 3: Optional LLM/MCP Analyzer (Gated)
|
|
2
|
+
// Uses LLM or MCP to provide intelligent suggestions
|
|
3
|
+
|
|
4
|
+
import type { Analyzer, AnalysisContext, AnalysisSuggestion } from '../analysis-pipeline';
|
|
5
|
+
import type { EventStore } from '../../core/types';
|
|
6
|
+
import type { LLMProvider, LLMProviderConfig, MCPToolInput, RepoMetadata } from '../llm/types';
|
|
7
|
+
import { createLLMProvider } from '../llm/providers';
|
|
8
|
+
import { existsSync, readFileSync } from 'fs';
|
|
9
|
+
import { join, dirname } from 'path';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* LLM/MCP analyzer (gated - only runs if enabled)
|
|
13
|
+
*/
|
|
14
|
+
export class LLMAnalyzer implements Analyzer {
|
|
15
|
+
name = 'llm-analyzer';
|
|
16
|
+
layer = 3;
|
|
17
|
+
private enabled = false;
|
|
18
|
+
private provider: LLMProvider | null = null;
|
|
19
|
+
private config: LLMProviderConfig | undefined = undefined;
|
|
20
|
+
|
|
21
|
+
constructor(enabled: boolean = false, config?: LLMProviderConfig) {
|
|
22
|
+
this.enabled = enabled;
|
|
23
|
+
this.config = config;
|
|
24
|
+
if (enabled && config) {
|
|
25
|
+
this.provider = createLLMProvider(config);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
setEnabled(enabled: boolean): void {
|
|
30
|
+
this.enabled = enabled;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
setConfig(config: LLMProviderConfig): void {
|
|
34
|
+
this.config = config;
|
|
35
|
+
if (this.enabled && config.enabled) {
|
|
36
|
+
this.provider = createLLMProvider(config);
|
|
37
|
+
} else {
|
|
38
|
+
this.provider = null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async analyze(context: AnalysisContext, _store: EventStore): Promise<AnalysisSuggestion[]> {
|
|
43
|
+
if (!this.enabled || !this.provider || !this.config) {
|
|
44
|
+
return []; // Gated - don't run if not enabled
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check if provider is available
|
|
48
|
+
const available = await this.provider.isAvailable();
|
|
49
|
+
if (!available) {
|
|
50
|
+
console.warn(`LLM provider ${this.config.type} is not available`);
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const suggestions: AnalysisSuggestion[] = [];
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
// Build MCP tool input
|
|
58
|
+
const repoMetadata = this.detectRepoMetadata(context.cwd);
|
|
59
|
+
const errorSummary = {
|
|
60
|
+
command: context.command,
|
|
61
|
+
args: context.args,
|
|
62
|
+
exitCode: context.exitCode,
|
|
63
|
+
stderr: context.stderr,
|
|
64
|
+
stdout: context.stdout,
|
|
65
|
+
cwd: context.cwd,
|
|
66
|
+
timestamp: Date.now(),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const mcpInput: MCPToolInput = {
|
|
70
|
+
contextWindow: context,
|
|
71
|
+
errorSummary,
|
|
72
|
+
repoMetadata,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Call LLM provider
|
|
76
|
+
const output = await this.provider.analyze(mcpInput);
|
|
77
|
+
|
|
78
|
+
// Convert MCP output to AnalysisSuggestion format
|
|
79
|
+
for (const suggestion of output.suggestions) {
|
|
80
|
+
suggestions.push({
|
|
81
|
+
id: `llm_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
82
|
+
type: suggestion.type,
|
|
83
|
+
priority: suggestion.priority,
|
|
84
|
+
title: suggestion.title,
|
|
85
|
+
description: suggestion.description,
|
|
86
|
+
confidence: suggestion.confidence,
|
|
87
|
+
actionableSnippet: suggestion.actionableSnippet,
|
|
88
|
+
provenance: {
|
|
89
|
+
analyzer: this.name,
|
|
90
|
+
layer: this.layer,
|
|
91
|
+
timestamp: output.provenance.timestamp,
|
|
92
|
+
},
|
|
93
|
+
timestamp: Date.now(),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error('LLM analyzer failed:', error);
|
|
98
|
+
// Don't throw - return empty suggestions on error
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return suggestions;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Detect repository metadata from working directory
|
|
106
|
+
*/
|
|
107
|
+
private detectRepoMetadata(cwd: string): RepoMetadata {
|
|
108
|
+
const repoRoot = this.findRepoRoot(cwd);
|
|
109
|
+
if (!repoRoot) {
|
|
110
|
+
return {};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Detect repo type
|
|
114
|
+
let repoType: RepoMetadata['type'] = 'none';
|
|
115
|
+
if (existsSync(join(repoRoot, '.git'))) {
|
|
116
|
+
repoType = 'git';
|
|
117
|
+
} else if (existsSync(join(repoRoot, '.hg'))) {
|
|
118
|
+
repoType = 'hg';
|
|
119
|
+
} else if (existsSync(join(repoRoot, '.svn'))) {
|
|
120
|
+
repoType = 'svn';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Find relevant files
|
|
124
|
+
const files: string[] = [];
|
|
125
|
+
const relevantPatterns = ['flake.nix', '*.nix', 'package.json', 'Cargo.toml', '*.sh'];
|
|
126
|
+
|
|
127
|
+
for (const pattern of relevantPatterns) {
|
|
128
|
+
// Simple check - in real implementation, would use glob
|
|
129
|
+
if (pattern.includes('*')) {
|
|
130
|
+
// Skip glob patterns for now
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (existsSync(join(repoRoot, pattern))) {
|
|
134
|
+
files.push(pattern);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Detect language/framework from common files
|
|
139
|
+
let language: string | undefined;
|
|
140
|
+
let framework: string | undefined;
|
|
141
|
+
|
|
142
|
+
if (existsSync(join(repoRoot, 'flake.nix')) || existsSync(join(repoRoot, 'default.nix'))) {
|
|
143
|
+
language = 'nix';
|
|
144
|
+
} else if (existsSync(join(repoRoot, 'package.json'))) {
|
|
145
|
+
language = 'javascript';
|
|
146
|
+
try {
|
|
147
|
+
const pkgContent = readFileSync(join(repoRoot, 'package.json'), 'utf-8');
|
|
148
|
+
const pkg = JSON.parse(pkgContent);
|
|
149
|
+
if (pkg.dependencies?.react || pkg.devDependencies?.react) {
|
|
150
|
+
framework = 'react';
|
|
151
|
+
} else if (pkg.dependencies?.svelte || pkg.devDependencies?.svelte) {
|
|
152
|
+
framework = 'svelte';
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
// Ignore parse errors
|
|
156
|
+
}
|
|
157
|
+
} else if (existsSync(join(repoRoot, 'Cargo.toml'))) {
|
|
158
|
+
language = 'rust';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
root: repoRoot,
|
|
163
|
+
type: repoType,
|
|
164
|
+
files: files.length > 0 ? files : undefined,
|
|
165
|
+
language,
|
|
166
|
+
framework,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Find repository root by looking for markers
|
|
172
|
+
*/
|
|
173
|
+
private findRepoRoot(cwd: string): string | null {
|
|
174
|
+
let current = cwd;
|
|
175
|
+
const maxDepth = 10;
|
|
176
|
+
let depth = 0;
|
|
177
|
+
|
|
178
|
+
while (depth < maxDepth) {
|
|
179
|
+
// Check for common repository markers
|
|
180
|
+
if (existsSync(join(current, '.git')) ||
|
|
181
|
+
existsSync(join(current, 'flake.nix')) ||
|
|
182
|
+
existsSync(join(current, '.gitignore'))) {
|
|
183
|
+
return current;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const parent = dirname(current);
|
|
187
|
+
if (parent === current) {
|
|
188
|
+
break; // Reached filesystem root
|
|
189
|
+
}
|
|
190
|
+
current = parent;
|
|
191
|
+
depth++;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Create LLM analyzer
|
|
200
|
+
*/
|
|
201
|
+
export function createLLMAnalyzer(enabled: boolean = false, config?: LLMProviderConfig): Analyzer {
|
|
202
|
+
return new LLMAnalyzer(enabled, config);
|
|
203
|
+
}
|
|
204
|
+
|