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