@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,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
|
+
|