@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.
Files changed (148) hide show
  1. package/ANALYSIS_LADDER.md +231 -0
  2. package/CHANGELOG.md +124 -0
  3. package/INTEGRATIONS.md +242 -0
  4. package/LICENSE +21 -0
  5. package/MEMORY.md +253 -0
  6. package/NIXOS.md +357 -0
  7. package/QUICKSTART.md +157 -0
  8. package/README.md +295 -0
  9. package/RELEASE.md +190 -0
  10. package/ValidationChecklist.md +598 -0
  11. package/docs/demo.md +338 -0
  12. package/docs/llm-integration.md +300 -0
  13. package/docs/parallel-execution-plan.md +160 -0
  14. package/flake.nix +228 -0
  15. package/integrations/README.md +242 -0
  16. package/integrations/demo-steps.sh +64 -0
  17. package/integrations/nvim-runebook.lua +140 -0
  18. package/integrations/tmux-status.sh +51 -0
  19. package/integrations/vim-runebook.vim +77 -0
  20. package/integrations/wezterm-status-simple.lua +48 -0
  21. package/integrations/wezterm-status.lua +76 -0
  22. package/nixos-module.nix +156 -0
  23. package/package.json +76 -0
  24. package/packages/design-dojo/index.js +4 -0
  25. package/packages/design-dojo/package.json +20 -0
  26. package/packages/design-dojo/tokens.css +69 -0
  27. package/playwright.config.ts +16 -0
  28. package/scripts/check-versions.cjs +62 -0
  29. package/scripts/demo.sh +220 -0
  30. package/shell.nix +31 -0
  31. package/src/app.html +13 -0
  32. package/src/cli/index.ts +1050 -0
  33. package/src/lib/agent/analysis-pipeline.ts +347 -0
  34. package/src/lib/agent/analysis-service.ts +171 -0
  35. package/src/lib/agent/analysis.ts +159 -0
  36. package/src/lib/agent/analyzers/heuristic.ts +289 -0
  37. package/src/lib/agent/analyzers/index.ts +7 -0
  38. package/src/lib/agent/analyzers/llm.ts +204 -0
  39. package/src/lib/agent/analyzers/local-search.ts +215 -0
  40. package/src/lib/agent/capture.ts +123 -0
  41. package/src/lib/agent/index.ts +244 -0
  42. package/src/lib/agent/integration.ts +81 -0
  43. package/src/lib/agent/llm/providers/base.ts +99 -0
  44. package/src/lib/agent/llm/providers/index.ts +60 -0
  45. package/src/lib/agent/llm/providers/mock.ts +67 -0
  46. package/src/lib/agent/llm/providers/ollama.ts +151 -0
  47. package/src/lib/agent/llm/providers/openai.ts +153 -0
  48. package/src/lib/agent/llm/sanitizer.ts +170 -0
  49. package/src/lib/agent/llm/types.ts +118 -0
  50. package/src/lib/agent/memory.ts +363 -0
  51. package/src/lib/agent/node-status.ts +56 -0
  52. package/src/lib/agent/node-suggestions.ts +64 -0
  53. package/src/lib/agent/status.ts +80 -0
  54. package/src/lib/agent/suggestions.ts +169 -0
  55. package/src/lib/components/Canvas.svelte +124 -0
  56. package/src/lib/components/ConnectionLine.svelte +46 -0
  57. package/src/lib/components/DisplayNode.svelte +167 -0
  58. package/src/lib/components/InputNode.svelte +158 -0
  59. package/src/lib/components/TerminalNode.svelte +237 -0
  60. package/src/lib/components/Toolbar.svelte +359 -0
  61. package/src/lib/components/TransformNode.svelte +327 -0
  62. package/src/lib/core/index.ts +31 -0
  63. package/src/lib/core/observer.ts +278 -0
  64. package/src/lib/core/redaction.ts +158 -0
  65. package/src/lib/core/shell-adapters/base.ts +325 -0
  66. package/src/lib/core/shell-adapters/bash.ts +110 -0
  67. package/src/lib/core/shell-adapters/index.ts +62 -0
  68. package/src/lib/core/shell-adapters/zsh.ts +105 -0
  69. package/src/lib/core/storage.ts +360 -0
  70. package/src/lib/core/types.ts +176 -0
  71. package/src/lib/design-dojo/Box.svelte +47 -0
  72. package/src/lib/design-dojo/Button.svelte +75 -0
  73. package/src/lib/design-dojo/Input.svelte +65 -0
  74. package/src/lib/design-dojo/List.svelte +38 -0
  75. package/src/lib/design-dojo/Select.svelte +48 -0
  76. package/src/lib/design-dojo/SplitPane.svelte +43 -0
  77. package/src/lib/design-dojo/StatusBar.svelte +61 -0
  78. package/src/lib/design-dojo/Table.svelte +47 -0
  79. package/src/lib/design-dojo/Text.svelte +36 -0
  80. package/src/lib/design-dojo/Toggle.svelte +48 -0
  81. package/src/lib/design-dojo/index.ts +10 -0
  82. package/src/lib/stores/canvas-praxis.ts +268 -0
  83. package/src/lib/stores/canvas.ts +58 -0
  84. package/src/lib/types/agent.ts +78 -0
  85. package/src/lib/types/canvas.ts +71 -0
  86. package/src/lib/utils/storage.ts +326 -0
  87. package/src/lib/utils/yaml-loader.ts +52 -0
  88. package/src/routes/+layout.svelte +5 -0
  89. package/src/routes/+layout.ts +5 -0
  90. package/src/routes/+page.svelte +32 -0
  91. package/src-tauri/Cargo.lock +5735 -0
  92. package/src-tauri/Cargo.toml +38 -0
  93. package/src-tauri/build.rs +3 -0
  94. package/src-tauri/capabilities/default.json +10 -0
  95. package/src-tauri/icons/128x128.png +0 -0
  96. package/src-tauri/icons/128x128@2x.png +0 -0
  97. package/src-tauri/icons/32x32.png +0 -0
  98. package/src-tauri/icons/Square107x107Logo.png +0 -0
  99. package/src-tauri/icons/Square142x142Logo.png +0 -0
  100. package/src-tauri/icons/Square150x150Logo.png +0 -0
  101. package/src-tauri/icons/Square284x284Logo.png +0 -0
  102. package/src-tauri/icons/Square30x30Logo.png +0 -0
  103. package/src-tauri/icons/Square310x310Logo.png +0 -0
  104. package/src-tauri/icons/Square44x44Logo.png +0 -0
  105. package/src-tauri/icons/Square71x71Logo.png +0 -0
  106. package/src-tauri/icons/Square89x89Logo.png +0 -0
  107. package/src-tauri/icons/StoreLogo.png +0 -0
  108. package/src-tauri/icons/icon.icns +0 -0
  109. package/src-tauri/icons/icon.ico +0 -0
  110. package/src-tauri/icons/icon.png +0 -0
  111. package/src-tauri/src/agents/agent1.rs +66 -0
  112. package/src-tauri/src/agents/agent2.rs +80 -0
  113. package/src-tauri/src/agents/agent3.rs +73 -0
  114. package/src-tauri/src/agents/agent4.rs +66 -0
  115. package/src-tauri/src/agents/agent5.rs +68 -0
  116. package/src-tauri/src/agents/agent6.rs +75 -0
  117. package/src-tauri/src/agents/base.rs +52 -0
  118. package/src-tauri/src/agents/mod.rs +17 -0
  119. package/src-tauri/src/core/coordination.rs +117 -0
  120. package/src-tauri/src/core/mod.rs +12 -0
  121. package/src-tauri/src/core/ownership.rs +61 -0
  122. package/src-tauri/src/core/types.rs +132 -0
  123. package/src-tauri/src/execution/mod.rs +5 -0
  124. package/src-tauri/src/execution/runner.rs +143 -0
  125. package/src-tauri/src/lib.rs +161 -0
  126. package/src-tauri/src/main.rs +6 -0
  127. package/src-tauri/src/memory/api.rs +422 -0
  128. package/src-tauri/src/memory/client.rs +156 -0
  129. package/src-tauri/src/memory/encryption.rs +79 -0
  130. package/src-tauri/src/memory/migration.rs +110 -0
  131. package/src-tauri/src/memory/mod.rs +28 -0
  132. package/src-tauri/src/memory/schema.rs +275 -0
  133. package/src-tauri/src/memory/tests.rs +192 -0
  134. package/src-tauri/src/orchestrator/coordinator.rs +232 -0
  135. package/src-tauri/src/orchestrator/mod.rs +13 -0
  136. package/src-tauri/src/orchestrator/planner.rs +304 -0
  137. package/src-tauri/tauri.conf.json +35 -0
  138. package/static/examples/date-time-example.yaml +147 -0
  139. package/static/examples/hello-world.yaml +74 -0
  140. package/static/examples/transform-example.yaml +157 -0
  141. package/static/favicon.png +0 -0
  142. package/static/svelte.svg +1 -0
  143. package/static/tauri.svg +6 -0
  144. package/static/vite.svg +1 -0
  145. package/svelte.config.js +18 -0
  146. package/tsconfig.json +19 -0
  147. package/vite.config.js +45 -0
  148. 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,7 @@
1
+ // Analyzer exports
2
+
3
+ export * from './heuristic';
4
+ export * from './local-search';
5
+ export * from './llm';
6
+ export type { Analyzer, AnalysisContext, AnalysisSuggestion, AnalysisJob } from '../analysis-pipeline';
7
+
@@ -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
+