@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,347 @@
1
+ // Analysis Pipeline - Background job system for failure analysis
2
+ // Runs analyzers in layers: heuristic → local search → optional LLM/MCP
3
+
4
+ import type { TerminalObserverEvent, EventStore } from '../core/types';
5
+ import type { Suggestion } from '../types/agent';
6
+
7
+ /**
8
+ * Extended suggestion with confidence, actionable snippet, and provenance
9
+ */
10
+ export interface AnalysisSuggestion extends Suggestion {
11
+ confidence: number; // 0.0 to 1.0
12
+ actionableSnippet?: string; // Code snippet or command to fix the issue
13
+ provenance: {
14
+ analyzer: string; // Which analyzer produced this
15
+ layer: number; // Which layer (1, 2, or 3)
16
+ timestamp: number;
17
+ };
18
+ }
19
+
20
+ /**
21
+ * Analysis job context
22
+ */
23
+ export interface AnalysisJob {
24
+ id: string;
25
+ commandId: string;
26
+ command: string;
27
+ args: string[];
28
+ cwd: string;
29
+ env: Record<string, string>;
30
+ exitCode: number;
31
+ stdout: string;
32
+ stderr: string;
33
+ events: TerminalObserverEvent[]; // Full event context
34
+ timestamp: number;
35
+ status: 'pending' | 'running' | 'completed' | 'cancelled' | 'failed';
36
+ suggestions: AnalysisSuggestion[];
37
+ error?: string;
38
+ }
39
+
40
+ /**
41
+ * Context window for analysis
42
+ */
43
+ export interface AnalysisContext {
44
+ command: string;
45
+ args: string[];
46
+ cwd: string;
47
+ env: Record<string, string>;
48
+ exitCode: number;
49
+ stdout: string;
50
+ stderr: string;
51
+ previousCommands: Array<{
52
+ command: string;
53
+ args: string[];
54
+ exitCode: number;
55
+ timestamp: number;
56
+ }>;
57
+ repoFiles?: string[]; // Relevant files in the repo
58
+ }
59
+
60
+ /**
61
+ * Pluggable analyzer interface
62
+ */
63
+ export interface Analyzer {
64
+ name: string;
65
+ layer: number; // 1, 2, or 3
66
+ analyze(context: AnalysisContext, store: EventStore): Promise<AnalysisSuggestion[]>;
67
+ }
68
+
69
+ /**
70
+ * Job queue for background analysis
71
+ */
72
+ export class AnalysisJobQueue {
73
+ private jobs: Map<string, AnalysisJob> = new Map();
74
+ private running: Set<string> = new Set();
75
+ private analyzers: Analyzer[] = [];
76
+ private store: EventStore | null = null;
77
+ private maxConcurrentJobs = 1;
78
+ private enableLLM = false; // Gate for LLM/MCP calls
79
+
80
+ constructor(store: EventStore | null = null) {
81
+ this.store = store;
82
+ }
83
+
84
+ /**
85
+ * Register an analyzer
86
+ */
87
+ registerAnalyzer(analyzer: Analyzer): void {
88
+ this.analyzers.push(analyzer);
89
+ // Sort by layer
90
+ this.analyzers.sort((a, b) => a.layer - b.layer);
91
+ }
92
+
93
+ /**
94
+ * Enable or disable LLM/MCP layer
95
+ */
96
+ setLLMEnabled(enabled: boolean): void {
97
+ this.enableLLM = enabled;
98
+ }
99
+
100
+ /**
101
+ * Detect failure and enqueue analysis job
102
+ */
103
+ async enqueueFailure(
104
+ commandId: string,
105
+ events: TerminalObserverEvent[],
106
+ store: EventStore
107
+ ): Promise<string | null> {
108
+ // Find command_start, exit_status, and stderr events
109
+ const commandStart = events.find(e => e.type === 'command_start' && e.id === commandId);
110
+ const exitStatus = events.find(e => e.type === 'exit_status' && e.commandId === commandId);
111
+ const stderrChunks = events
112
+ .filter(e => e.type === 'stderr_chunk' && 'commandId' in e && e.commandId === commandId)
113
+ .sort((a, b) => {
114
+ const aIdx = 'chunkIndex' in a ? a.chunkIndex : 0;
115
+ const bIdx = 'chunkIndex' in b ? b.chunkIndex : 0;
116
+ return aIdx - bIdx;
117
+ });
118
+ const stdoutChunks = events
119
+ .filter(e => e.type === 'stdout_chunk' && 'commandId' in e && e.commandId === commandId)
120
+ .sort((a, b) => {
121
+ const aIdx = 'chunkIndex' in a ? a.chunkIndex : 0;
122
+ const bIdx = 'chunkIndex' in b ? b.chunkIndex : 0;
123
+ return aIdx - bIdx;
124
+ });
125
+
126
+ if (!commandStart || !exitStatus || commandStart.type !== 'command_start' || exitStatus.type !== 'exit_status') {
127
+ return null;
128
+ }
129
+
130
+ // Check if it's a failure
131
+ if (exitStatus.success) {
132
+ return null; // Not a failure
133
+ }
134
+
135
+ // Build context
136
+ const stderr = stderrChunks
137
+ .map(e => ('chunk' in e ? e.chunk : ''))
138
+ .join('');
139
+ const stdout = stdoutChunks
140
+ .map(e => ('chunk' in e ? e.chunk : ''))
141
+ .join('');
142
+
143
+ // Get previous commands for context
144
+ const allEvents = await store.getEvents(undefined, undefined, 50);
145
+ const previousCommands = allEvents
146
+ .filter(e => e.type === 'command_start' && e.timestamp < commandStart.timestamp)
147
+ .slice(-5)
148
+ .map(e => {
149
+ if (e.type === 'command_start') {
150
+ const exit = allEvents.find(
151
+ ev => ev.type === 'exit_status' && 'commandId' in ev && ev.commandId === e.id
152
+ );
153
+ return {
154
+ command: e.command,
155
+ args: e.args,
156
+ exitCode: exit && exit.type === 'exit_status' ? exit.exitCode : 0,
157
+ timestamp: e.timestamp,
158
+ };
159
+ }
160
+ return null;
161
+ })
162
+ .filter((c): c is NonNullable<typeof c> => c !== null);
163
+
164
+ const job: AnalysisJob = {
165
+ id: `job_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
166
+ commandId,
167
+ command: commandStart.command,
168
+ args: commandStart.args,
169
+ cwd: commandStart.cwd,
170
+ env: commandStart.envSummary || {},
171
+ exitCode: exitStatus.exitCode,
172
+ stdout,
173
+ stderr,
174
+ events,
175
+ timestamp: Date.now(),
176
+ status: 'pending',
177
+ suggestions: [],
178
+ };
179
+
180
+ this.jobs.set(job.id, job);
181
+ this.processQueue(store);
182
+
183
+ return job.id;
184
+ }
185
+
186
+ /**
187
+ * Process the job queue (non-blocking)
188
+ */
189
+ private async processQueue(store: EventStore): Promise<void> {
190
+ if (this.running.size >= this.maxConcurrentJobs) {
191
+ return;
192
+ }
193
+
194
+ const pendingJob = Array.from(this.jobs.values()).find(j => j.status === 'pending');
195
+ if (!pendingJob) {
196
+ return;
197
+ }
198
+
199
+ this.running.add(pendingJob.id);
200
+ pendingJob.status = 'running';
201
+
202
+ // Process in background (non-blocking)
203
+ this.runAnalysis(pendingJob, store).catch(error => {
204
+ pendingJob.status = 'failed';
205
+ pendingJob.error = String(error);
206
+ this.running.delete(pendingJob.id);
207
+ });
208
+ }
209
+
210
+ /**
211
+ * Run analysis on a job (runs analyzers in layers)
212
+ */
213
+ private async runAnalysis(job: AnalysisJob, store: EventStore): Promise<void> {
214
+ try {
215
+ // Build analysis context
216
+ const context: AnalysisContext = {
217
+ command: job.command,
218
+ args: job.args,
219
+ cwd: job.cwd,
220
+ env: job.env,
221
+ exitCode: job.exitCode,
222
+ stdout: job.stdout,
223
+ stderr: job.stderr,
224
+ previousCommands: await this.getPreviousCommands(job, store),
225
+ };
226
+
227
+ // Run analyzers by layer
228
+ const suggestions: AnalysisSuggestion[] = [];
229
+
230
+ // Layer 1: Heuristic classifiers
231
+ const layer1Analyzers = this.analyzers.filter(a => a.layer === 1);
232
+ for (const analyzer of layer1Analyzers) {
233
+ try {
234
+ const analyzerSuggestions = await analyzer.analyze(context, store);
235
+ suggestions.push(...analyzerSuggestions);
236
+ } catch (error) {
237
+ console.error(`Analyzer ${analyzer.name} failed:`, error);
238
+ }
239
+ }
240
+
241
+ // If we have high-confidence suggestions from layer 1, we might skip layer 2
242
+ const highConfidence = suggestions.filter(s => s.confidence >= 0.8);
243
+ if (highConfidence.length === 0) {
244
+ // Layer 2: Local search
245
+ const layer2Analyzers = this.analyzers.filter(a => a.layer === 2);
246
+ for (const analyzer of layer2Analyzers) {
247
+ try {
248
+ const analyzerSuggestions = await analyzer.analyze(context, store);
249
+ suggestions.push(...analyzerSuggestions);
250
+ } catch (error) {
251
+ console.error(`Analyzer ${analyzer.name} failed:`, error);
252
+ }
253
+ }
254
+ }
255
+
256
+ // Layer 3: Optional LLM/MCP (gated)
257
+ if (this.enableLLM) {
258
+ const layer3Analyzers = this.analyzers.filter(a => a.layer === 3);
259
+ for (const analyzer of layer3Analyzers) {
260
+ try {
261
+ const analyzerSuggestions = await analyzer.analyze(context, store);
262
+ suggestions.push(...analyzerSuggestions);
263
+ } catch (error) {
264
+ console.error(`Analyzer ${analyzer.name} failed:`, error);
265
+ }
266
+ }
267
+ }
268
+
269
+ job.suggestions = suggestions;
270
+ job.status = 'completed';
271
+ } catch (error) {
272
+ job.status = 'failed';
273
+ job.error = String(error);
274
+ } finally {
275
+ this.running.delete(job.id);
276
+ // Process next job
277
+ this.processQueue(store);
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Get previous commands for context
283
+ */
284
+ private async getPreviousCommands(job: AnalysisJob, store: EventStore): Promise<AnalysisContext['previousCommands']> {
285
+ const events = await store.getEvents(undefined, undefined, 50);
286
+ const commandStart = job.events.find(e => e.type === 'command_start' && e.id === job.commandId);
287
+ if (!commandStart) {
288
+ return [];
289
+ }
290
+
291
+ return events
292
+ .filter(e => e.type === 'command_start' && e.timestamp < commandStart.timestamp)
293
+ .slice(-5)
294
+ .map(e => {
295
+ if (e.type === 'command_start') {
296
+ const exit = events.find(
297
+ ev => ev.type === 'exit_status' && 'commandId' in ev && ev.commandId === e.id
298
+ );
299
+ return {
300
+ command: e.command,
301
+ args: e.args,
302
+ exitCode: exit && exit.type === 'exit_status' ? exit.exitCode : 0,
303
+ timestamp: e.timestamp,
304
+ };
305
+ }
306
+ return null;
307
+ })
308
+ .filter((c): c is NonNullable<typeof c> => c !== null);
309
+ }
310
+
311
+ /**
312
+ * Get a job by ID
313
+ */
314
+ getJob(jobId: string): AnalysisJob | undefined {
315
+ return this.jobs.get(jobId);
316
+ }
317
+
318
+ /**
319
+ * Get the last completed job
320
+ */
321
+ getLastJob(): AnalysisJob | undefined {
322
+ const completed = Array.from(this.jobs.values())
323
+ .filter(j => j.status === 'completed')
324
+ .sort((a, b) => b.timestamp - a.timestamp);
325
+ return completed[0];
326
+ }
327
+
328
+ /**
329
+ * Cancel a job
330
+ */
331
+ cancelJob(jobId: string): boolean {
332
+ const job = this.jobs.get(jobId);
333
+ if (job && job.status === 'pending') {
334
+ job.status = 'cancelled';
335
+ return true;
336
+ }
337
+ return false;
338
+ }
339
+
340
+ /**
341
+ * Get all jobs
342
+ */
343
+ getAllJobs(): AnalysisJob[] {
344
+ return Array.from(this.jobs.values());
345
+ }
346
+ }
347
+
@@ -0,0 +1,171 @@
1
+ // Analysis Service - Integrates analysis pipeline with observer
2
+ // Monitors observer events and triggers analysis jobs
3
+
4
+ import { AnalysisJobQueue } from './analysis-pipeline';
5
+ import { createHeuristicAnalyzers, createLocalSearchAnalyzer, createLLMAnalyzer } from './analyzers';
6
+ import type { TerminalObserverEvent, EventStore } from '../core/types';
7
+ import type { ObserverConfig } from '../core/types';
8
+ import type { LLMProviderConfig } from './llm/types';
9
+
10
+ // Extended config that includes optional LLM support
11
+ interface AnalysisServiceConfig extends ObserverConfig {
12
+ llm?: LLMProviderConfig;
13
+ }
14
+
15
+ /**
16
+ * Analysis service that monitors observer events and triggers analysis
17
+ */
18
+ export class AnalysisService {
19
+ private queue: AnalysisJobQueue;
20
+ private store: EventStore | null = null;
21
+ private config: AnalysisServiceConfig | null = null;
22
+ private enabled = false;
23
+
24
+ constructor() {
25
+ this.queue = new AnalysisJobQueue();
26
+ this.setupAnalyzers();
27
+ }
28
+
29
+ /**
30
+ * Initialize the service with observer store
31
+ */
32
+ initialize(store: EventStore, config: ObserverConfig): void {
33
+ this.store = store;
34
+ this.config = config as AnalysisServiceConfig;
35
+ this.queue = new AnalysisJobQueue(store);
36
+ this.setupAnalyzers();
37
+
38
+ // Enable LLM if configured
39
+ const llmConfig = this.config.llm;
40
+ if (llmConfig && llmConfig.enabled) {
41
+ this.queue.setLLMEnabled(true);
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Enable or disable the service
47
+ */
48
+ setEnabled(enabled: boolean): void {
49
+ this.enabled = enabled;
50
+ }
51
+
52
+ /**
53
+ * Check if service is enabled
54
+ */
55
+ isEnabled(): boolean {
56
+ return this.enabled && this.store !== null;
57
+ }
58
+
59
+ /**
60
+ * Setup all analyzers
61
+ */
62
+ private setupAnalyzers(): void {
63
+ // Clear existing
64
+ this.queue = new AnalysisJobQueue(this.store);
65
+
66
+ // Register Layer 1: Heuristic analyzers
67
+ const heuristicAnalyzers = createHeuristicAnalyzers();
68
+ for (const analyzer of heuristicAnalyzers) {
69
+ this.queue.registerAnalyzer(analyzer);
70
+ }
71
+
72
+ // Register Layer 2: Local search
73
+ this.queue.registerAnalyzer(createLocalSearchAnalyzer());
74
+
75
+ // Register Layer 3: LLM (gated)
76
+ const llmConfig = this.config?.llm;
77
+ const llmEnabled = llmConfig?.enabled || false;
78
+ this.queue.registerAnalyzer(createLLMAnalyzer(llmEnabled, llmConfig));
79
+
80
+ // Set LLM enabled state
81
+ this.queue.setLLMEnabled(llmEnabled);
82
+ }
83
+
84
+ /**
85
+ * Process exit status event and trigger analysis if failure
86
+ */
87
+ async processExitStatus(event: TerminalObserverEvent): Promise<string | null> {
88
+ if (!this.isEnabled() || !this.store) {
89
+ return null;
90
+ }
91
+
92
+ if (event.type !== 'exit_status') {
93
+ return null;
94
+ }
95
+
96
+ // Check if it's a failure
97
+ if (event.success) {
98
+ return null; // Not a failure
99
+ }
100
+
101
+ // Get all events for this command
102
+ const commandEvents = await this.store.getEventsByCommand(event.commandId);
103
+
104
+ // Enqueue analysis job
105
+ return await this.queue.enqueueFailure(event.commandId, commandEvents, this.store);
106
+ }
107
+
108
+ /**
109
+ * Get the last analysis job
110
+ */
111
+ getLastJob() {
112
+ return this.queue.getLastJob();
113
+ }
114
+
115
+ /**
116
+ * Get a job by ID
117
+ */
118
+ getJob(jobId: string) {
119
+ return this.queue.getJob(jobId);
120
+ }
121
+
122
+ /**
123
+ * Get all jobs
124
+ */
125
+ getAllJobs() {
126
+ return this.queue.getAllJobs();
127
+ }
128
+
129
+ /**
130
+ * Cancel a job
131
+ */
132
+ cancelJob(jobId: string): boolean {
133
+ return this.queue.cancelJob(jobId);
134
+ }
135
+
136
+ /**
137
+ * Enable LLM analysis
138
+ */
139
+ setLLMEnabled(enabled: boolean): void {
140
+ this.queue.setLLMEnabled(enabled);
141
+ // Re-register LLM analyzer with new setting
142
+ const llmConfig = this.config?.llm;
143
+ this.setupAnalyzers();
144
+ }
145
+
146
+ /**
147
+ * Update LLM configuration
148
+ */
149
+ setLLMConfig(config: LLMProviderConfig | undefined): void {
150
+ if (this.config) {
151
+ this.config.llm = config;
152
+ this.setupAnalyzers();
153
+ }
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Global analysis service instance
159
+ */
160
+ let globalAnalysisService: AnalysisService | null = null;
161
+
162
+ /**
163
+ * Get or create the global analysis service
164
+ */
165
+ export function getAnalysisService(): AnalysisService {
166
+ if (!globalAnalysisService) {
167
+ globalAnalysisService = new AnalysisService();
168
+ }
169
+ return globalAnalysisService;
170
+ }
171
+
@@ -0,0 +1,159 @@
1
+ // Analysis engine for Ambient Agent Mode
2
+ // Analyzes patterns and generates suggestions
3
+
4
+ import type { TerminalEvent, CommandPattern, Suggestion, EventStorage } from '../types/agent';
5
+
6
+ export interface Analyzer {
7
+ analyzeEvent(event: TerminalEvent, storage: EventStorage): Promise<Suggestion[]>;
8
+ analyzePatterns(storage: EventStorage): Promise<Suggestion[]>;
9
+ }
10
+
11
+ /**
12
+ * Default analyzer implementation
13
+ */
14
+ export class DefaultAnalyzer implements Analyzer {
15
+ async analyzeEvent(event: TerminalEvent, storage: EventStorage): Promise<Suggestion[]> {
16
+ const suggestions: Suggestion[] = [];
17
+
18
+ // Check for repeated failures
19
+ if (!event.success) {
20
+ const recentFailures = await storage.getEventsByCommand(event.command, 5);
21
+ const failureCount = recentFailures.filter((e: TerminalEvent) => !e.success).length;
22
+
23
+ if (failureCount >= 3) {
24
+ suggestions.push({
25
+ id: `suggestion_${Date.now()}_repeated_failure`,
26
+ type: 'warning',
27
+ priority: 'high',
28
+ title: 'Repeated Command Failures',
29
+ description: `The command "${event.command}" has failed ${failureCount} times recently. Consider checking the command syntax or environment.`,
30
+ timestamp: Date.now(),
31
+ });
32
+ }
33
+ }
34
+
35
+ // Check for slow commands
36
+ if (event.duration && event.duration > 5000) {
37
+ suggestions.push({
38
+ id: `suggestion_${Date.now()}_slow_command`,
39
+ type: 'optimization',
40
+ priority: 'medium',
41
+ title: 'Slow Command Execution',
42
+ description: `Command "${event.command}" took ${(event.duration / 1000).toFixed(1)}s to execute. Consider optimizing or using a faster alternative.`,
43
+ timestamp: Date.now(),
44
+ });
45
+ }
46
+
47
+ // Check for common patterns
48
+ const patterns = await storage.getPatterns();
49
+ const pattern = patterns.find((p: CommandPattern) => p.command === event.command);
50
+
51
+ if (pattern && pattern.frequency > 5) {
52
+ // Suggest shortcuts for frequently used commands
53
+ if (pattern.commonArgs.length > 0 && event.args.length === 0) {
54
+ suggestions.push({
55
+ id: `suggestion_${Date.now()}_common_args`,
56
+ type: 'tip',
57
+ priority: 'low',
58
+ title: 'Common Arguments',
59
+ description: `You often use "${event.command}" with arguments. Consider creating a shortcut or alias.`,
60
+ command: event.command,
61
+ args: pattern.commonArgs[0].split(' '),
62
+ timestamp: Date.now(),
63
+ });
64
+ }
65
+ }
66
+
67
+ // Check for similar successful commands
68
+ if (!event.success) {
69
+ const similarEvents = await storage.getEvents(20);
70
+ const similarSuccessful = similarEvents.filter(
71
+ (e: TerminalEvent) => e.command === event.command && e.success && e.args.length === event.args.length
72
+ );
73
+
74
+ if (similarSuccessful.length > 0) {
75
+ const lastSuccessful = similarSuccessful[0];
76
+ suggestions.push({
77
+ id: `suggestion_${Date.now()}_similar_success`,
78
+ type: 'command',
79
+ priority: 'medium',
80
+ title: 'Similar Successful Command',
81
+ description: `A similar command succeeded recently. Compare the differences.`,
82
+ command: lastSuccessful.command,
83
+ args: lastSuccessful.args,
84
+ context: {
85
+ previousTimestamp: lastSuccessful.timestamp,
86
+ previousCwd: lastSuccessful.cwd,
87
+ },
88
+ timestamp: Date.now(),
89
+ });
90
+ }
91
+ }
92
+
93
+ return suggestions;
94
+ }
95
+
96
+ async analyzePatterns(storage: EventStorage): Promise<Suggestion[]> {
97
+ const suggestions: Suggestion[] = [];
98
+ const patterns = await storage.getPatterns();
99
+ const stats = await storage.getStats();
100
+
101
+ // Suggest frequently used commands as shortcuts
102
+ const frequentPatterns = patterns
103
+ .filter((p: CommandPattern) => p.frequency >= 5)
104
+ .sort((a: CommandPattern, b: CommandPattern) => b.frequency - a.frequency)
105
+ .slice(0, 5);
106
+
107
+ for (const pattern of frequentPatterns) {
108
+ suggestions.push({
109
+ id: `suggestion_${Date.now()}_frequent_${pattern.id}`,
110
+ type: 'shortcut',
111
+ priority: 'low',
112
+ title: 'Frequently Used Command',
113
+ description: `"${pattern.command}" has been used ${pattern.frequency} times. Consider creating an alias or script.`,
114
+ command: pattern.command,
115
+ timestamp: Date.now(),
116
+ });
117
+ }
118
+
119
+ // Suggest optimization for slow commands
120
+ const slowPatterns = patterns
121
+ .filter((p: CommandPattern) => p.avgDuration > 3000)
122
+ .sort((a: CommandPattern, b: CommandPattern) => b.avgDuration - a.avgDuration)
123
+ .slice(0, 3);
124
+
125
+ for (const pattern of slowPatterns) {
126
+ suggestions.push({
127
+ id: `suggestion_${Date.now()}_slow_${pattern.id}`,
128
+ type: 'optimization',
129
+ priority: 'medium',
130
+ title: 'Slow Command Pattern',
131
+ description: `"${pattern.command}" averages ${(pattern.avgDuration / 1000).toFixed(1)}s execution time. Consider optimization.`,
132
+ command: pattern.command,
133
+ timestamp: Date.now(),
134
+ });
135
+ }
136
+
137
+ // Overall stats suggestions
138
+ if (stats.avgSuccessRate < 0.7) {
139
+ suggestions.push({
140
+ id: `suggestion_${Date.now()}_low_success_rate`,
141
+ type: 'tip',
142
+ priority: 'medium',
143
+ title: 'Low Success Rate',
144
+ description: `Overall command success rate is ${(stats.avgSuccessRate * 100).toFixed(1)}%. Review failed commands for patterns.`,
145
+ timestamp: Date.now(),
146
+ });
147
+ }
148
+
149
+ return suggestions;
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Create analyzer instance
155
+ */
156
+ export function createAnalyzer(): Analyzer {
157
+ return new DefaultAnalyzer();
158
+ }
159
+