@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,118 @@
|
|
|
1
|
+
// LLM/MCP Integration Types
|
|
2
|
+
// Defines contracts for model-backed reasoning
|
|
3
|
+
|
|
4
|
+
import type { AnalysisContext, AnalysisSuggestion } from '../analysis-pipeline';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Repository metadata for context
|
|
8
|
+
*/
|
|
9
|
+
export interface RepoMetadata {
|
|
10
|
+
root?: string;
|
|
11
|
+
type?: 'git' | 'hg' | 'svn' | 'none';
|
|
12
|
+
files?: string[]; // Relevant files (e.g., *.nix, flake.nix)
|
|
13
|
+
language?: string;
|
|
14
|
+
framework?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Error summary for LLM context
|
|
19
|
+
*/
|
|
20
|
+
export interface ErrorSummary {
|
|
21
|
+
command: string;
|
|
22
|
+
args: string[];
|
|
23
|
+
exitCode: number;
|
|
24
|
+
stderr: string;
|
|
25
|
+
stdout: string;
|
|
26
|
+
cwd: string;
|
|
27
|
+
timestamp: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* MCP Tool Contract Input
|
|
32
|
+
* What we send to the LLM/MCP provider
|
|
33
|
+
*/
|
|
34
|
+
export interface MCPToolInput {
|
|
35
|
+
contextWindow: AnalysisContext;
|
|
36
|
+
errorSummary: ErrorSummary;
|
|
37
|
+
repoMetadata: RepoMetadata;
|
|
38
|
+
previousSuggestions?: AnalysisSuggestion[]; // From heuristic/local search layers
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* MCP Tool Contract Output
|
|
43
|
+
* What we expect back from the LLM/MCP provider
|
|
44
|
+
*/
|
|
45
|
+
export interface MCPToolOutput {
|
|
46
|
+
suggestions: Array<{
|
|
47
|
+
title: string;
|
|
48
|
+
description: string;
|
|
49
|
+
actionableSnippet?: string;
|
|
50
|
+
confidence: number; // 0.0 to 1.0
|
|
51
|
+
type: 'command' | 'optimization' | 'shortcut' | 'warning' | 'tip';
|
|
52
|
+
priority: 'low' | 'medium' | 'high';
|
|
53
|
+
}>;
|
|
54
|
+
provenance: {
|
|
55
|
+
provider: string; // 'ollama', 'openai', 'mcp', etc.
|
|
56
|
+
model?: string;
|
|
57
|
+
timestamp: number;
|
|
58
|
+
tokensUsed?: number;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Sanitized context (after redaction)
|
|
64
|
+
*/
|
|
65
|
+
export interface SanitizedContext {
|
|
66
|
+
original: AnalysisContext;
|
|
67
|
+
sanitized: AnalysisContext;
|
|
68
|
+
redactions: Array<{
|
|
69
|
+
type: 'env' | 'stdout' | 'stderr' | 'command';
|
|
70
|
+
pattern: string;
|
|
71
|
+
replaced: string;
|
|
72
|
+
}>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Re-export for convenience
|
|
76
|
+
export type { AnalysisContext, AnalysisSuggestion } from '../analysis-pipeline';
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* LLM Provider Configuration
|
|
80
|
+
*/
|
|
81
|
+
export interface LLMProviderConfig {
|
|
82
|
+
type: 'ollama' | 'openai' | 'mcp' | 'mock';
|
|
83
|
+
enabled: boolean;
|
|
84
|
+
// Ollama config
|
|
85
|
+
ollama?: {
|
|
86
|
+
baseUrl?: string; // Default: http://localhost:11434
|
|
87
|
+
model?: string; // Default: llama3.2
|
|
88
|
+
};
|
|
89
|
+
// OpenAI config
|
|
90
|
+
openai?: {
|
|
91
|
+
apiKey?: string; // From env var OPENAI_API_KEY
|
|
92
|
+
model?: string; // Default: gpt-4o-mini
|
|
93
|
+
baseUrl?: string; // Default: https://api.openai.com/v1
|
|
94
|
+
};
|
|
95
|
+
// MCP config
|
|
96
|
+
mcp?: {
|
|
97
|
+
serverUrl?: string;
|
|
98
|
+
toolName?: string;
|
|
99
|
+
};
|
|
100
|
+
// Safety settings
|
|
101
|
+
safety?: {
|
|
102
|
+
requireUserReview?: boolean; // Show context before sending (default: true)
|
|
103
|
+
maxContextLength?: number; // Truncate if too long (default: 8000 tokens)
|
|
104
|
+
cacheEnabled?: boolean; // Cache responses (default: false)
|
|
105
|
+
cacheTtl?: number; // Cache TTL in seconds (default: 3600)
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* LLM Provider Interface
|
|
111
|
+
*/
|
|
112
|
+
export interface LLMProvider {
|
|
113
|
+
name: string;
|
|
114
|
+
isAvailable(): Promise<boolean>;
|
|
115
|
+
analyze(input: MCPToolInput): Promise<MCPToolOutput>;
|
|
116
|
+
sanitizeContext(context: AnalysisContext): Promise<SanitizedContext>;
|
|
117
|
+
}
|
|
118
|
+
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
// Memory/storage layer for Ambient Agent Mode
|
|
2
|
+
// Stores terminal events and patterns for analysis
|
|
3
|
+
|
|
4
|
+
import type { TerminalEvent, CommandPattern, Suggestion, AgentConfig } from '../types/agent';
|
|
5
|
+
|
|
6
|
+
export interface EventStorage {
|
|
7
|
+
saveEvent(event: TerminalEvent): Promise<void>;
|
|
8
|
+
getEvents(limit?: number, since?: number): Promise<TerminalEvent[]>;
|
|
9
|
+
getEventsByCommand(command: string, limit?: number): Promise<TerminalEvent[]>;
|
|
10
|
+
getPatterns(): Promise<CommandPattern[]>;
|
|
11
|
+
savePattern(pattern: CommandPattern): Promise<void>;
|
|
12
|
+
saveSuggestion(suggestion: Suggestion): Promise<void>;
|
|
13
|
+
getSuggestions(limit: number): Promise<Suggestion[]>;
|
|
14
|
+
clearEvents(olderThan?: number): Promise<void>;
|
|
15
|
+
getStats(): Promise<{
|
|
16
|
+
totalEvents: number;
|
|
17
|
+
uniqueCommands: number;
|
|
18
|
+
avgSuccessRate: number;
|
|
19
|
+
totalDuration: number;
|
|
20
|
+
}>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* In-memory storage adapter (for testing and headless mode)
|
|
25
|
+
*/
|
|
26
|
+
export class MemoryStorage implements EventStorage {
|
|
27
|
+
private events: TerminalEvent[] = [];
|
|
28
|
+
private patterns: Map<string, CommandPattern> = new Map();
|
|
29
|
+
private suggestions: Suggestion[] = [];
|
|
30
|
+
private config: AgentConfig;
|
|
31
|
+
|
|
32
|
+
constructor(config: AgentConfig) {
|
|
33
|
+
this.config = config;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async saveEvent(event: TerminalEvent): Promise<void> {
|
|
37
|
+
this.events.push(event);
|
|
38
|
+
|
|
39
|
+
// Enforce max events limit
|
|
40
|
+
if (this.config.maxEvents && this.events.length > this.config.maxEvents) {
|
|
41
|
+
this.events = this.events.slice(-this.config.maxEvents);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Update patterns
|
|
45
|
+
await this.updatePattern(event);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async getEvents(limit?: number, since?: number): Promise<TerminalEvent[]> {
|
|
49
|
+
let filtered = this.events;
|
|
50
|
+
|
|
51
|
+
if (since) {
|
|
52
|
+
filtered = filtered.filter(e => e.timestamp >= since);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (limit) {
|
|
56
|
+
filtered = filtered.slice(-limit);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return filtered.sort((a, b) => b.timestamp - a.timestamp);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async getEventsByCommand(command: string, limit?: number): Promise<TerminalEvent[]> {
|
|
63
|
+
let filtered = this.events.filter(e => e.command === command);
|
|
64
|
+
|
|
65
|
+
if (limit) {
|
|
66
|
+
filtered = filtered.slice(-limit);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return filtered.sort((a, b) => b.timestamp - a.timestamp);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async getPatterns(): Promise<CommandPattern[]> {
|
|
73
|
+
return Array.from(this.patterns.values());
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async savePattern(pattern: CommandPattern): Promise<void> {
|
|
77
|
+
this.patterns.set(pattern.id, pattern);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async saveSuggestion(suggestion: Suggestion): Promise<void> {
|
|
81
|
+
this.suggestions.push(suggestion);
|
|
82
|
+
// Keep only recent suggestions (last 100)
|
|
83
|
+
if (this.suggestions.length > 100) {
|
|
84
|
+
this.suggestions = this.suggestions.slice(-100);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async getSuggestions(limit: number): Promise<Suggestion[]> {
|
|
89
|
+
return this.suggestions
|
|
90
|
+
.sort((a, b) => b.timestamp - a.timestamp)
|
|
91
|
+
.slice(0, limit);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async clearEvents(olderThan?: number): Promise<void> {
|
|
95
|
+
if (olderThan) {
|
|
96
|
+
this.events = this.events.filter(e => e.timestamp >= olderThan);
|
|
97
|
+
} else {
|
|
98
|
+
this.events = [];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async getStats(): Promise<{
|
|
103
|
+
totalEvents: number;
|
|
104
|
+
uniqueCommands: number;
|
|
105
|
+
avgSuccessRate: number;
|
|
106
|
+
totalDuration: number;
|
|
107
|
+
}> {
|
|
108
|
+
const uniqueCommands = new Set(this.events.map(e => e.command)).size;
|
|
109
|
+
const successful = this.events.filter(e => e.success).length;
|
|
110
|
+
const avgSuccessRate = this.events.length > 0 ? successful / this.events.length : 0;
|
|
111
|
+
const totalDuration = this.events.reduce((sum, e) => sum + (e.duration || 0), 0);
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
totalEvents: this.events.length,
|
|
115
|
+
uniqueCommands,
|
|
116
|
+
avgSuccessRate,
|
|
117
|
+
totalDuration,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private async updatePattern(event: TerminalEvent): Promise<void> {
|
|
122
|
+
const patternId = `pattern_${event.command}`;
|
|
123
|
+
let pattern = this.patterns.get(patternId);
|
|
124
|
+
|
|
125
|
+
if (!pattern) {
|
|
126
|
+
pattern = {
|
|
127
|
+
id: patternId,
|
|
128
|
+
command: event.command,
|
|
129
|
+
frequency: 0,
|
|
130
|
+
lastUsed: event.timestamp,
|
|
131
|
+
successRate: 0,
|
|
132
|
+
avgDuration: 0,
|
|
133
|
+
commonArgs: [],
|
|
134
|
+
commonEnv: {},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
pattern.frequency += 1;
|
|
139
|
+
pattern.lastUsed = Math.max(pattern.lastUsed, event.timestamp);
|
|
140
|
+
|
|
141
|
+
// Update success rate
|
|
142
|
+
const commandEvents = await this.getEventsByCommand(event.command);
|
|
143
|
+
const successful = commandEvents.filter(e => e.success).length;
|
|
144
|
+
pattern.successRate = commandEvents.length > 0 ? successful / commandEvents.length : 0;
|
|
145
|
+
|
|
146
|
+
// Update average duration
|
|
147
|
+
const durations = commandEvents.filter(e => e.duration !== undefined).map(e => e.duration!);
|
|
148
|
+
pattern.avgDuration = durations.length > 0
|
|
149
|
+
? durations.reduce((sum, d) => sum + d, 0) / durations.length
|
|
150
|
+
: 0;
|
|
151
|
+
|
|
152
|
+
// Track common args
|
|
153
|
+
if (event.args.length > 0) {
|
|
154
|
+
const argKey = event.args.join(' ');
|
|
155
|
+
const existing = pattern.commonArgs.find(a => a === argKey);
|
|
156
|
+
if (!existing) {
|
|
157
|
+
pattern.commonArgs.push(argKey);
|
|
158
|
+
if (pattern.commonArgs.length > 10) {
|
|
159
|
+
pattern.commonArgs = pattern.commonArgs.slice(-10);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
this.patterns.set(patternId, pattern);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* PluresDB storage adapter (for persistent storage)
|
|
170
|
+
*/
|
|
171
|
+
export class PluresDBStorage implements EventStorage {
|
|
172
|
+
private db: any = null;
|
|
173
|
+
private readonly eventPrefix = 'agent:event:';
|
|
174
|
+
private readonly patternPrefix = 'agent:pattern:';
|
|
175
|
+
private readonly suggestionPrefix = 'agent:suggestion:';
|
|
176
|
+
private initialized = false;
|
|
177
|
+
private config: AgentConfig;
|
|
178
|
+
|
|
179
|
+
constructor(config: AgentConfig) {
|
|
180
|
+
this.config = config;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private async ensureInitialized(): Promise<void> {
|
|
184
|
+
if (this.initialized && this.db) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
const { SQLiteCompatibleAPI } = await import('pluresdb');
|
|
190
|
+
|
|
191
|
+
this.db = new SQLiteCompatibleAPI({
|
|
192
|
+
config: {
|
|
193
|
+
port: 34567,
|
|
194
|
+
host: 'localhost',
|
|
195
|
+
dataDir: this.config.storagePath || './pluresdb-data',
|
|
196
|
+
},
|
|
197
|
+
autoStart: true,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
await this.db.start();
|
|
201
|
+
this.initialized = true;
|
|
202
|
+
} catch (error) {
|
|
203
|
+
console.error('Failed to initialize PluresDB for agent storage:', error);
|
|
204
|
+
throw new Error('PluresDB initialization failed for agent storage');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async saveEvent(event: TerminalEvent): Promise<void> {
|
|
209
|
+
await this.ensureInitialized();
|
|
210
|
+
const key = `${this.eventPrefix}${event.id}`;
|
|
211
|
+
await this.db.put(key, event);
|
|
212
|
+
await this.updatePattern(event);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async getEvents(limit?: number, since?: number): Promise<TerminalEvent[]> {
|
|
216
|
+
await this.ensureInitialized();
|
|
217
|
+
const keys = await this.db.list(this.eventPrefix);
|
|
218
|
+
const events: TerminalEvent[] = [];
|
|
219
|
+
|
|
220
|
+
for (const key of keys) {
|
|
221
|
+
try {
|
|
222
|
+
const event = await this.db.getValue(key);
|
|
223
|
+
if (event && (!since || event.timestamp >= since)) {
|
|
224
|
+
events.push(event as TerminalEvent);
|
|
225
|
+
}
|
|
226
|
+
} catch (error) {
|
|
227
|
+
console.error('Failed to load event:', error);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
events.sort((a, b) => b.timestamp - a.timestamp);
|
|
232
|
+
return limit ? events.slice(0, limit) : events;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async getEventsByCommand(command: string, limit?: number): Promise<TerminalEvent[]> {
|
|
236
|
+
const allEvents = await this.getEvents();
|
|
237
|
+
const filtered = allEvents.filter(e => e.command === command);
|
|
238
|
+
return limit ? filtered.slice(0, limit) : filtered;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async getPatterns(): Promise<CommandPattern[]> {
|
|
242
|
+
await this.ensureInitialized();
|
|
243
|
+
const keys = await this.db.list(this.patternPrefix);
|
|
244
|
+
const patterns: CommandPattern[] = [];
|
|
245
|
+
|
|
246
|
+
for (const key of keys) {
|
|
247
|
+
try {
|
|
248
|
+
const pattern = await this.db.getValue(key);
|
|
249
|
+
if (pattern) {
|
|
250
|
+
patterns.push(pattern as CommandPattern);
|
|
251
|
+
}
|
|
252
|
+
} catch (error) {
|
|
253
|
+
console.error('Failed to load pattern:', error);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return patterns;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async savePattern(pattern: CommandPattern): Promise<void> {
|
|
261
|
+
await this.ensureInitialized();
|
|
262
|
+
const key = `${this.patternPrefix}${pattern.id}`;
|
|
263
|
+
await this.db.put(key, pattern);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async saveSuggestion(suggestion: Suggestion): Promise<void> {
|
|
267
|
+
await this.ensureInitialized();
|
|
268
|
+
const key = `${this.suggestionPrefix}${suggestion.id}`;
|
|
269
|
+
await this.db.put(key, suggestion);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async getSuggestions(limit: number): Promise<Suggestion[]> {
|
|
273
|
+
await this.ensureInitialized();
|
|
274
|
+
const keys = await this.db.list(this.suggestionPrefix);
|
|
275
|
+
const suggestions: Suggestion[] = [];
|
|
276
|
+
|
|
277
|
+
for (const key of keys) {
|
|
278
|
+
try {
|
|
279
|
+
const suggestion = await this.db.getValue(key);
|
|
280
|
+
if (suggestion) {
|
|
281
|
+
suggestions.push(suggestion as Suggestion);
|
|
282
|
+
}
|
|
283
|
+
} catch (error) {
|
|
284
|
+
console.error('Failed to load suggestion:', error);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
suggestions.sort((a, b) => b.timestamp - a.timestamp);
|
|
289
|
+
return suggestions.slice(0, limit);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async clearEvents(olderThan?: number): Promise<void> {
|
|
293
|
+
await this.ensureInitialized();
|
|
294
|
+
const keys = await this.db.list(this.eventPrefix);
|
|
295
|
+
|
|
296
|
+
for (const key of keys) {
|
|
297
|
+
try {
|
|
298
|
+
const event = await this.db.getValue(key);
|
|
299
|
+
if (event && (!olderThan || event.timestamp < olderThan)) {
|
|
300
|
+
await this.db.delete(key);
|
|
301
|
+
}
|
|
302
|
+
} catch (error) {
|
|
303
|
+
console.error('Failed to delete event:', error);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async getStats(): Promise<{
|
|
309
|
+
totalEvents: number;
|
|
310
|
+
uniqueCommands: number;
|
|
311
|
+
avgSuccessRate: number;
|
|
312
|
+
totalDuration: number;
|
|
313
|
+
}> {
|
|
314
|
+
const events = await this.getEvents();
|
|
315
|
+
const uniqueCommands = new Set(events.map(e => e.command)).size;
|
|
316
|
+
const successful = events.filter(e => e.success).length;
|
|
317
|
+
const avgSuccessRate = events.length > 0 ? successful / events.length : 0;
|
|
318
|
+
const totalDuration = events.reduce((sum, e) => sum + (e.duration || 0), 0);
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
totalEvents: events.length,
|
|
322
|
+
uniqueCommands,
|
|
323
|
+
avgSuccessRate,
|
|
324
|
+
totalDuration,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private async updatePattern(event: TerminalEvent): Promise<void> {
|
|
329
|
+
const patternId = `pattern_${event.command}`;
|
|
330
|
+
const existingEvents = await this.getEventsByCommand(event.command);
|
|
331
|
+
|
|
332
|
+
const successful = existingEvents.filter(e => e.success).length;
|
|
333
|
+
const successRate = existingEvents.length > 0 ? successful / existingEvents.length : 0;
|
|
334
|
+
const durations = existingEvents.filter(e => e.duration !== undefined).map(e => e.duration!);
|
|
335
|
+
const avgDuration = durations.length > 0
|
|
336
|
+
? durations.reduce((sum, d) => sum + d, 0) / durations.length
|
|
337
|
+
: 0;
|
|
338
|
+
|
|
339
|
+
const pattern: CommandPattern = {
|
|
340
|
+
id: patternId,
|
|
341
|
+
command: event.command,
|
|
342
|
+
frequency: existingEvents.length,
|
|
343
|
+
lastUsed: Math.max(...existingEvents.map(e => e.timestamp)),
|
|
344
|
+
successRate,
|
|
345
|
+
avgDuration,
|
|
346
|
+
commonArgs: [...new Set(existingEvents.flatMap(e => e.args.join(' ')))].slice(0, 10),
|
|
347
|
+
commonEnv: {},
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
await this.savePattern(pattern);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Create storage instance based on config
|
|
356
|
+
*/
|
|
357
|
+
export function createStorage(config: AgentConfig): EventStorage {
|
|
358
|
+
if (config.storagePath) {
|
|
359
|
+
return new PluresDBStorage(config);
|
|
360
|
+
}
|
|
361
|
+
return new MemoryStorage(config);
|
|
362
|
+
}
|
|
363
|
+
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Node.js-only agent status tracking with file persistence
|
|
2
|
+
// This file should only be imported in Node.js environments
|
|
3
|
+
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { homedir } from 'os';
|
|
7
|
+
import type { AgentStatusData } from './status';
|
|
8
|
+
|
|
9
|
+
const STATUS_FILE = join(homedir(), '.runebook', 'agent-status.json');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get current agent status from file (Node.js only)
|
|
13
|
+
*/
|
|
14
|
+
export function getAgentStatusFromFile(): AgentStatusData {
|
|
15
|
+
if (existsSync(STATUS_FILE)) {
|
|
16
|
+
try {
|
|
17
|
+
const content = readFileSync(STATUS_FILE, 'utf-8');
|
|
18
|
+
const data = JSON.parse(content);
|
|
19
|
+
return {
|
|
20
|
+
status: data.status || 'idle',
|
|
21
|
+
lastCommand: data.lastCommand,
|
|
22
|
+
lastCommandTimestamp: data.lastCommandTimestamp,
|
|
23
|
+
suggestionCount: data.suggestionCount || 0,
|
|
24
|
+
highPriorityCount: data.highPriorityCount || 0,
|
|
25
|
+
lastUpdated: data.lastUpdated || Date.now(),
|
|
26
|
+
};
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error('Failed to load agent status:', error);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
status: 'idle',
|
|
34
|
+
suggestionCount: 0,
|
|
35
|
+
highPriorityCount: 0,
|
|
36
|
+
lastUpdated: Date.now(),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Update agent status to file (Node.js only)
|
|
42
|
+
*/
|
|
43
|
+
export function updateAgentStatusToFile(current: AgentStatusData, updates: Partial<AgentStatusData>): void {
|
|
44
|
+
const configDir = join(homedir(), '.runebook');
|
|
45
|
+
if (!existsSync(configDir)) {
|
|
46
|
+
mkdirSync(configDir, { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const updated: AgentStatusData = {
|
|
50
|
+
...current,
|
|
51
|
+
...updates,
|
|
52
|
+
lastUpdated: Date.now(),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
writeFileSync(STATUS_FILE, JSON.stringify(updated, null, 2), 'utf-8');
|
|
56
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Node.js-only suggestion store with file persistence
|
|
2
|
+
// This file should only be imported in Node.js environments (CLI, server)
|
|
3
|
+
|
|
4
|
+
import type { Suggestion } from '../types/agent';
|
|
5
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { homedir } from 'os';
|
|
8
|
+
import { MemorySuggestionStore } from './suggestions';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* File-based persistent suggestion store (Node.js only)
|
|
12
|
+
*/
|
|
13
|
+
export class FileSuggestionStore extends MemorySuggestionStore {
|
|
14
|
+
private storePath: string;
|
|
15
|
+
|
|
16
|
+
constructor(storePath?: string) {
|
|
17
|
+
super();
|
|
18
|
+
this.storePath = storePath || join(homedir(), '.runebook', 'suggestions.json');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async save(): Promise<void> {
|
|
22
|
+
const configDir = join(homedir(), '.runebook');
|
|
23
|
+
if (!existsSync(configDir)) {
|
|
24
|
+
mkdirSync(configDir, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const data = {
|
|
28
|
+
suggestions: this.suggestions,
|
|
29
|
+
lastUpdated: Date.now(),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
writeFileSync(this.storePath, JSON.stringify(data, null, 2), 'utf-8');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async load(): Promise<void> {
|
|
36
|
+
if (existsSync(this.storePath)) {
|
|
37
|
+
try {
|
|
38
|
+
const content = readFileSync(this.storePath, 'utf-8');
|
|
39
|
+
const data = JSON.parse(content);
|
|
40
|
+
if (data.suggestions && Array.isArray(data.suggestions)) {
|
|
41
|
+
this.suggestions = data.suggestions;
|
|
42
|
+
}
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error('Failed to load suggestions from file:', error);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
add(suggestion: Suggestion): void {
|
|
50
|
+
super.add(suggestion);
|
|
51
|
+
// Auto-save on add (async, don't wait)
|
|
52
|
+
this.save().catch(err => console.error('Failed to save suggestions:', err));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
remove(id: string): void {
|
|
56
|
+
super.remove(id);
|
|
57
|
+
this.save().catch(err => console.error('Failed to save suggestions:', err));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
clear(): void {
|
|
61
|
+
super.clear();
|
|
62
|
+
this.save().catch(err => console.error('Failed to save suggestions:', err));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Agent status tracking for UX surfaces
|
|
2
|
+
|
|
3
|
+
export type AgentStatus = 'idle' | 'analyzing' | 'issues_found';
|
|
4
|
+
|
|
5
|
+
export interface AgentStatusData {
|
|
6
|
+
status: AgentStatus;
|
|
7
|
+
lastCommand?: string;
|
|
8
|
+
lastCommandTimestamp?: number;
|
|
9
|
+
suggestionCount: number;
|
|
10
|
+
highPriorityCount: number;
|
|
11
|
+
lastUpdated: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// In-memory status for browser environment
|
|
15
|
+
let inMemoryStatus: AgentStatusData = {
|
|
16
|
+
status: 'idle',
|
|
17
|
+
suggestionCount: 0,
|
|
18
|
+
highPriorityCount: 0,
|
|
19
|
+
lastUpdated: Date.now(),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Check if we're in Node.js environment
|
|
23
|
+
const isNode = typeof process !== 'undefined' && process.versions?.node;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get current agent status
|
|
27
|
+
*/
|
|
28
|
+
export function getAgentStatus(): AgentStatusData {
|
|
29
|
+
if (isNode) {
|
|
30
|
+
// Dynamically load from file in Node.js
|
|
31
|
+
try {
|
|
32
|
+
// Use dynamic import to avoid bundling Node.js modules
|
|
33
|
+
return inMemoryStatus; // Return in-memory for now, will be updated async
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error('Failed to load agent status:', error);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return inMemoryStatus;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Update agent status
|
|
44
|
+
*/
|
|
45
|
+
export function updateAgentStatus(updates: Partial<AgentStatusData>): void {
|
|
46
|
+
inMemoryStatus = {
|
|
47
|
+
...inMemoryStatus,
|
|
48
|
+
...updates,
|
|
49
|
+
lastUpdated: Date.now(),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// In Node.js, also persist to file
|
|
53
|
+
if (isNode) {
|
|
54
|
+
import('./node-status').then(({ updateAgentStatusToFile }) => {
|
|
55
|
+
updateAgentStatusToFile(inMemoryStatus, updates);
|
|
56
|
+
}).catch(err => {
|
|
57
|
+
console.error('Failed to persist status to file:', err);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Format status for display
|
|
64
|
+
*/
|
|
65
|
+
export function formatStatus(status: AgentStatusData): string {
|
|
66
|
+
const statusSymbol = {
|
|
67
|
+
idle: '●',
|
|
68
|
+
analyzing: '⟳',
|
|
69
|
+
issues_found: '⚠',
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const statusText = {
|
|
73
|
+
idle: 'idle',
|
|
74
|
+
analyzing: 'analyzing',
|
|
75
|
+
issues_found: `${status.highPriorityCount} issue${status.highPriorityCount !== 1 ? 's' : ''}`,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return `${statusSymbol[status.status]} ${statusText[status.status]}`;
|
|
79
|
+
}
|
|
80
|
+
|