@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,1050 @@
1
+ #!/usr/bin/env node
2
+ // Headless CLI for RuneBook
3
+ // SSH-friendly interface without GUI
4
+
5
+ import { createAgent, defaultAgentConfig } from '../lib/agent/index';
6
+ import { formatSuggestionsForCLI, formatSuggestionCompact } from '../lib/agent/suggestions';
7
+ import { getAgentStatus, formatStatus } from '../lib/agent/status';
8
+ import type { AgentConfig } from '../lib/types/agent';
9
+ import { createObserver, type ObserverConfig } from '../lib/core';
10
+ import { getAnalysisService } from '../lib/agent/analysis-service';
11
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
12
+ import { join } from 'path';
13
+ import { homedir } from 'os';
14
+
15
+ const CONFIG_FILE = join(homedir(), '.runebook', 'agent-config.json');
16
+ const OBSERVER_CONFIG_FILE = join(homedir(), '.runebook', 'observer-config.json');
17
+
18
+ interface CLIOptions {
19
+ enabled?: boolean;
20
+ storagePath?: string;
21
+ maxEvents?: number;
22
+ retentionDays?: number;
23
+ }
24
+
25
+ /**
26
+ * Load agent configuration from file or use defaults
27
+ */
28
+ function loadConfig(): AgentConfig {
29
+ if (existsSync(CONFIG_FILE)) {
30
+ try {
31
+ const content = readFileSync(CONFIG_FILE, 'utf-8');
32
+ return { ...defaultAgentConfig, ...JSON.parse(content) };
33
+ } catch (error) {
34
+ console.error('Failed to load config, using defaults:', error);
35
+ }
36
+ }
37
+ return { ...defaultAgentConfig };
38
+ }
39
+
40
+ /**
41
+ * Save agent configuration to file
42
+ */
43
+ function saveConfig(config: AgentConfig): void {
44
+ const configDir = join(homedir(), '.runebook');
45
+ if (!existsSync(configDir)) {
46
+ mkdirSync(configDir, { recursive: true });
47
+ }
48
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
49
+ }
50
+
51
+ /**
52
+ * Initialize agent from CLI
53
+ */
54
+ function initAgentFromCLI(options: CLIOptions = {}): void {
55
+ const config = loadConfig();
56
+ const updatedConfig: AgentConfig = {
57
+ ...config,
58
+ enabled: options.enabled !== undefined ? options.enabled : config.enabled,
59
+ storagePath: options.storagePath || config.storagePath,
60
+ maxEvents: options.maxEvents || config.maxEvents,
61
+ retentionDays: options.retentionDays || config.retentionDays,
62
+ };
63
+
64
+ saveConfig(updatedConfig);
65
+ console.log('Agent configuration updated.');
66
+ console.log('Config:', JSON.stringify(updatedConfig, null, 2));
67
+ }
68
+
69
+ /**
70
+ * Show agent status
71
+ */
72
+ async function showStatus(): Promise<void> {
73
+ const config = loadConfig();
74
+ const agent = createAgent(config);
75
+
76
+ console.log('\n=== Ambient Agent Status ===\n');
77
+ console.log(`Enabled: ${config.enabled ? 'Yes' : 'No'}`);
78
+ console.log(`Capture Events: ${config.captureEvents ? 'Yes' : 'No'}`);
79
+ console.log(`Analyze Patterns: ${config.analyzePatterns ? 'Yes' : 'No'}`);
80
+ console.log(`Suggest Improvements: ${config.suggestImprovements ? 'Yes' : 'No'}`);
81
+ console.log(`Storage Path: ${config.storagePath || 'Memory (in-memory)'}`);
82
+ console.log(`Max Events: ${config.maxEvents || 'Unlimited'}`);
83
+ console.log(`Retention Days: ${config.retentionDays || 'Unlimited'}`);
84
+
85
+ if (config.enabled) {
86
+ const stats = await agent.getStats();
87
+ console.log('\n=== Statistics ===');
88
+ console.log(`Total Events: ${stats.totalEvents}`);
89
+ console.log(`Unique Commands: ${stats.uniqueCommands}`);
90
+ console.log(`Average Success Rate: ${(stats.avgSuccessRate * 100).toFixed(1)}%`);
91
+ console.log(`Total Duration: ${(stats.totalDuration / 1000).toFixed(1)}s`);
92
+ }
93
+
94
+ agent.stop();
95
+ }
96
+
97
+ /**
98
+ * Show suggestions
99
+ */
100
+ async function showSuggestions(priority?: 'low' | 'medium' | 'high'): Promise<void> {
101
+ const config = loadConfig();
102
+
103
+ if (!config.enabled) {
104
+ console.log('Agent is not enabled. Run: runebook agent enable');
105
+ return;
106
+ }
107
+
108
+ const agent = createAgent(config);
109
+ const suggestions = agent.getSuggestionsCLI(priority);
110
+ console.log(suggestions);
111
+
112
+ // Also analyze patterns for new suggestions
113
+ await agent.analyzeAllPatterns();
114
+ const newSuggestions = agent.getSuggestionsCLI(priority);
115
+ if (newSuggestions !== 'No suggestions available.\n') {
116
+ console.log('\n=== New Pattern-Based Suggestions ===');
117
+ console.log(newSuggestions);
118
+ }
119
+
120
+ agent.stop();
121
+ }
122
+
123
+ /**
124
+ * Show agent status
125
+ */
126
+ function showSuggestStatus(): void {
127
+ const status = getAgentStatus();
128
+ const statusText = formatStatus(status);
129
+
130
+ console.log(`\n=== Agent Status ===\n`);
131
+ console.log(`Status: ${statusText}`);
132
+ console.log(`Suggestions: ${status.suggestionCount}`);
133
+ console.log(`High Priority: ${status.highPriorityCount}`);
134
+
135
+ if (status.lastCommand) {
136
+ const date = new Date(status.lastCommandTimestamp || 0).toLocaleString();
137
+ console.log(`Last Command: ${status.lastCommand} (${date})`);
138
+ }
139
+
140
+ console.log();
141
+ }
142
+
143
+ /**
144
+ * Show top suggestion
145
+ */
146
+ async function showTopSuggestion(): Promise<void> {
147
+ const config = loadConfig();
148
+
149
+ if (!config.enabled) {
150
+ console.log('Agent is not enabled. Run: runebook agent enable');
151
+ return;
152
+ }
153
+
154
+ const agent = createAgent(config);
155
+ const topSuggestions = agent.getTopSuggestion(1);
156
+
157
+ if (topSuggestions.length === 0) {
158
+ console.log('No suggestions available.');
159
+ return;
160
+ }
161
+
162
+ const suggestion = topSuggestions[0];
163
+ console.log(formatSuggestionCompact(suggestion));
164
+ console.log(`\n${suggestion.description}`);
165
+ if (suggestion.command) {
166
+ const args = suggestion.args ? suggestion.args.join(' ') : '';
167
+ console.log(`\nCommand: ${suggestion.command} ${args}`);
168
+ }
169
+
170
+ agent.stop();
171
+ }
172
+
173
+ /**
174
+ * Show suggestions for last command
175
+ */
176
+ async function showLastCommandSuggestions(): Promise<void> {
177
+ const config = loadConfig();
178
+
179
+ if (!config.enabled) {
180
+ console.log('Agent is not enabled. Run: runebook agent enable');
181
+ return;
182
+ }
183
+
184
+ const agent = createAgent(config);
185
+ const suggestions = agent.getSuggestionsForLastCommand();
186
+ const status = getAgentStatus();
187
+
188
+ if (!status.lastCommand) {
189
+ console.log('No command has been executed yet.');
190
+ return;
191
+ }
192
+
193
+ console.log(`\n=== Suggestions for "${status.lastCommand}" ===\n`);
194
+
195
+ if (suggestions.length === 0) {
196
+ console.log('No suggestions for this command.');
197
+ } else {
198
+ console.log(formatSuggestionsForCLI(suggestions));
199
+ }
200
+
201
+ agent.stop();
202
+ }
203
+
204
+ /**
205
+ * Show recent events
206
+ */
207
+ async function showEvents(limit: number = 10): Promise<void> {
208
+ const config = loadConfig();
209
+
210
+ if (!config.enabled) {
211
+ console.log('Agent is not enabled. Run: runebook agent enable');
212
+ return;
213
+ }
214
+
215
+ const agent = createAgent(config);
216
+ const events = await agent.getRecentEvents(limit);
217
+
218
+ console.log(`\n=== Recent Events (${events.length}) ===\n`);
219
+
220
+ for (const event of events) {
221
+ const date = new Date(event.timestamp).toLocaleString();
222
+ const status = event.success ? '✓' : '✗';
223
+ const duration = event.duration ? `${(event.duration / 1000).toFixed(2)}s` : 'N/A';
224
+ const args = event.args.length > 0 ? ` ${event.args.join(' ')}` : '';
225
+
226
+ console.log(`${status} [${date}] ${event.command}${args}`);
227
+ console.log(` Duration: ${duration}, Exit Code: ${event.exitCode || 'N/A'}`);
228
+ if (event.cwd) {
229
+ console.log(` CWD: ${event.cwd}`);
230
+ }
231
+ console.log();
232
+ }
233
+
234
+ agent.stop();
235
+ }
236
+
237
+ /**
238
+ * Clear old events
239
+ */
240
+ async function clearEvents(days: number = 30): Promise<void> {
241
+ const config = loadConfig();
242
+
243
+ if (!config.enabled) {
244
+ console.log('Agent is not enabled.');
245
+ return;
246
+ }
247
+
248
+ const agent = createAgent(config);
249
+ await agent.clearOldEvents(days);
250
+ console.log(`Cleared events older than ${days} days.`);
251
+
252
+ agent.stop();
253
+ }
254
+
255
+ /**
256
+ * Load observer configuration
257
+ */
258
+ function loadObserverConfig(): ObserverConfig {
259
+ if (existsSync(OBSERVER_CONFIG_FILE)) {
260
+ try {
261
+ const content = readFileSync(OBSERVER_CONFIG_FILE, 'utf-8');
262
+ return JSON.parse(content);
263
+ } catch (error) {
264
+ console.error('Failed to load observer config, using defaults:', error);
265
+ }
266
+ }
267
+ return {
268
+ enabled: false,
269
+ redactSecrets: true,
270
+ usePluresDB: false,
271
+ chunkSize: 4096,
272
+ maxEvents: 10000,
273
+ retentionDays: 30,
274
+ };
275
+ }
276
+
277
+ /**
278
+ * Save observer configuration
279
+ */
280
+ function saveObserverConfig(config: ObserverConfig): void {
281
+ const configDir = join(homedir(), '.runebook');
282
+ if (!existsSync(configDir)) {
283
+ mkdirSync(configDir, { recursive: true });
284
+ }
285
+ writeFileSync(OBSERVER_CONFIG_FILE, JSON.stringify(config, null, 2));
286
+ }
287
+
288
+ /**
289
+ * Tail events (stream events as they come in)
290
+ */
291
+ async function tailEvents(follow: boolean = true): Promise<void> {
292
+ const config = loadObserverConfig();
293
+
294
+ if (!config.enabled) {
295
+ console.log('Observer is not enabled. Run: runebook observer enable');
296
+ return;
297
+ }
298
+
299
+ const observer = createObserver(config);
300
+ await observer.initialize();
301
+
302
+ console.log('Tailing events (press Ctrl+C to stop)...\n');
303
+
304
+ let lastTimestamp = Date.now();
305
+
306
+ const pollInterval = setInterval(async () => {
307
+ try {
308
+ const events = await observer.getEvents(undefined, lastTimestamp);
309
+
310
+ for (const event of events) {
311
+ const date = new Date(event.timestamp).toISOString();
312
+ const type = event.type.padEnd(15);
313
+
314
+ switch (event.type) {
315
+ case 'command_start':
316
+ console.log(`[${date}] ${type} ${event.command} ${event.args.join(' ')}`);
317
+ console.log(` CWD: ${event.cwd}`);
318
+ break;
319
+ case 'command_end':
320
+ console.log(`[${date}] ${type} commandId: ${event.commandId}, duration: ${event.duration}ms`);
321
+ break;
322
+ case 'stdout_chunk':
323
+ console.log(`[${date}] ${type} [${event.chunkIndex}] ${event.chunk.substring(0, 100)}${event.chunk.length > 100 ? '...' : ''}`);
324
+ break;
325
+ case 'stderr_chunk':
326
+ console.log(`[${date}] ${type} [${event.chunkIndex}] ${event.chunk.substring(0, 100)}${event.chunk.length > 100 ? '...' : ''}`);
327
+ break;
328
+ case 'exit_status':
329
+ console.log(`[${date}] ${type} exitCode: ${event.exitCode}, success: ${event.success}`);
330
+ break;
331
+ default:
332
+ console.log(`[${date}] ${type} ${JSON.stringify(event).substring(0, 100)}`);
333
+ }
334
+
335
+ lastTimestamp = Math.max(lastTimestamp, event.timestamp);
336
+ }
337
+ } catch (error) {
338
+ console.error('Error tailing events:', error);
339
+ }
340
+ }, 1000); // Poll every second
341
+
342
+ // Handle Ctrl+C
343
+ process.on('SIGINT', () => {
344
+ clearInterval(pollInterval);
345
+ observer.stop();
346
+ console.log('\nStopped tailing events.');
347
+ process.exit(0);
348
+ });
349
+
350
+ if (!follow) {
351
+ // One-time fetch
352
+ setTimeout(() => {
353
+ clearInterval(pollInterval);
354
+ observer.stop();
355
+ }, 5000);
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Show LLM provider status
361
+ */
362
+ async function showLLMStatus(): Promise<void> {
363
+ const obsConfig = loadObserverConfig();
364
+
365
+ console.log('\n=== LLM/MCP Integration Status ===\n');
366
+
367
+ // Check if llm config exists (extended observer config)
368
+ const llmConfig = (obsConfig as any).llm;
369
+ if (!llmConfig) {
370
+ console.log('Status: Disabled (no configuration)');
371
+ console.log('\nTo enable LLM analysis, add LLM configuration to observer config.');
372
+ console.log('Example:');
373
+ console.log(' {');
374
+ console.log(' "llm": {');
375
+ console.log(' "type": "ollama",');
376
+ console.log(' "enabled": true,');
377
+ console.log(' "ollama": { "model": "llama3.2" }');
378
+ console.log(' }');
379
+ console.log(' }');
380
+ return;
381
+ }
382
+
383
+ console.log(`Provider Type: ${llmConfig.type}`);
384
+ console.log(`Enabled: ${llmConfig.enabled ? 'Yes' : 'No'}`);
385
+
386
+ if (!llmConfig.enabled) {
387
+ console.log('\nLLM analysis is configured but disabled.');
388
+ return;
389
+ }
390
+
391
+ // Check provider availability
392
+ try {
393
+ const { createLLMProvider, isProviderAvailable } = await import('../lib/agent/llm/providers');
394
+ const available = await isProviderAvailable(llmConfig);
395
+ console.log(`Available: ${available ? 'Yes' : 'No'}`);
396
+
397
+ if (available) {
398
+ const provider = createLLMProvider(llmConfig);
399
+ if (provider) {
400
+ console.log(`Provider: ${provider.name}`);
401
+ }
402
+ } else {
403
+ console.log('\n⚠️ Provider is not available.');
404
+ if (llmConfig.type === 'ollama') {
405
+ console.log('Make sure Ollama is running: ollama serve');
406
+ } else if (llmConfig.type === 'openai') {
407
+ console.log('Make sure OPENAI_API_KEY environment variable is set.');
408
+ }
409
+ }
410
+
411
+ // Show safety settings
412
+ if (llmConfig.safety) {
413
+ console.log('\n=== Safety Settings ===');
414
+ console.log(`Require User Review: ${llmConfig.safety.requireUserReview !== false ? 'Yes' : 'No'}`);
415
+ console.log(`Cache Enabled: ${llmConfig.safety.cacheEnabled ? 'Yes' : 'No'}`);
416
+ if (llmConfig.safety.cacheEnabled) {
417
+ console.log(`Cache TTL: ${llmConfig.safety.cacheTtl || 3600} seconds`);
418
+ }
419
+ if (llmConfig.safety.maxContextLength) {
420
+ console.log(`Max Context Length: ${llmConfig.safety.maxContextLength} tokens`);
421
+ }
422
+ }
423
+
424
+ // Show provider-specific config
425
+ console.log('\n=== Provider Configuration ===');
426
+ if (llmConfig.type === 'ollama' && llmConfig.ollama) {
427
+ console.log(`Base URL: ${llmConfig.ollama.baseUrl || 'http://localhost:11434'}`);
428
+ console.log(`Model: ${llmConfig.ollama.model || 'llama3.2'}`);
429
+ } else if (llmConfig.type === 'openai' && llmConfig.openai) {
430
+ const apiKey = llmConfig.openai.apiKey || process.env.OPENAI_API_KEY;
431
+ console.log(`API Key: ${apiKey ? '***' + apiKey.slice(-4) : 'Not set'}`);
432
+ console.log(`Model: ${llmConfig.openai.model || 'gpt-4o-mini'}`);
433
+ console.log(`Base URL: ${llmConfig.openai.baseUrl || 'https://api.openai.com/v1'}`);
434
+ } else if (llmConfig.type === 'mock') {
435
+ console.log('Mock provider (for testing)');
436
+ } else if (llmConfig.type === 'mcp') {
437
+ console.log('MCP provider (not yet implemented)');
438
+ }
439
+
440
+ } catch (error: any) {
441
+ console.error('Error checking LLM status:', error.message);
442
+ }
443
+
444
+ console.log();
445
+ }
446
+
447
+ /**
448
+ * Inspect cognitive memory storage
449
+ */
450
+ async function inspectMemory(): Promise<void> {
451
+ try {
452
+ const { SQLiteCompatibleAPI } = await import('pluresdb');
453
+
454
+ const db = new SQLiteCompatibleAPI({
455
+ config: {
456
+ port: 34567,
457
+ host: 'localhost',
458
+ dataDir: './pluresdb-data',
459
+ },
460
+ autoStart: true,
461
+ });
462
+
463
+ await db.start();
464
+
465
+ // List sessions
466
+ const sessionKeys = await db.list('memory:session:');
467
+ const sessions = [];
468
+ for (const key of sessionKeys) {
469
+ try {
470
+ const session = await db.getValue(key);
471
+ if (session) {
472
+ sessions.push(session);
473
+ }
474
+ } catch (error) {
475
+ // Skip errors
476
+ }
477
+ }
478
+
479
+ // List errors
480
+ const errorKeys = await db.list('memory:error:');
481
+ const errors = [];
482
+ for (const key of errorKeys.slice(0, 10)) {
483
+ try {
484
+ const error = await db.getValue(key);
485
+ if (error) {
486
+ errors.push(error);
487
+ }
488
+ } catch (error) {
489
+ // Skip errors
490
+ }
491
+ }
492
+
493
+ // List suggestions
494
+ const suggestionKeys = await db.list('memory:suggestion:');
495
+ const suggestions = [];
496
+ for (const key of suggestionKeys.slice(0, 10)) {
497
+ try {
498
+ const suggestion = await db.getValue(key);
499
+ if (suggestion && !suggestion.dismissed) {
500
+ suggestions.push(suggestion);
501
+ }
502
+ } catch (error) {
503
+ // Skip errors
504
+ }
505
+ }
506
+
507
+ // Sort suggestions by rank
508
+ suggestions.sort((a, b) => (b.rank || 0) - (a.rank || 0));
509
+
510
+ console.log('\n=== RuneBook Cognitive Memory ===\n');
511
+ console.log(`Sessions: ${sessions.length}`);
512
+ console.log(`Recent Errors: ${errors.length}`);
513
+ console.log(`Active Suggestions: ${suggestions.length}\n`);
514
+
515
+ if (sessions.length > 0) {
516
+ console.log('=== Recent Sessions ===');
517
+ for (const session of sessions.slice(0, 5)) {
518
+ const started = new Date(session.started_at).toLocaleString();
519
+ console.log(` ${session.id.substring(0, 8)}... - ${session.shell_type} (started: ${started})`);
520
+ }
521
+ console.log();
522
+ }
523
+
524
+ if (errors.length > 0) {
525
+ console.log('=== Recent Errors ===');
526
+ for (const error of errors.slice(0, 5)) {
527
+ console.log(` [${error.severity}] ${error.error_type} - ${error.message.substring(0, 60)}`);
528
+ }
529
+ console.log();
530
+ }
531
+
532
+ if (suggestions.length > 0) {
533
+ console.log('=== Top Suggestions ===');
534
+ for (const suggestion of suggestions.slice(0, 5)) {
535
+ console.log(` [${suggestion.priority}] ${suggestion.title} - ${suggestion.description.substring(0, 60)}`);
536
+ }
537
+ }
538
+
539
+ } catch (error: any) {
540
+ if (error.message && error.message.includes('PluresDB')) {
541
+ console.error('Error: PluresDB server is not available.');
542
+ console.error('Make sure PluresDB is running on localhost:34567');
543
+ console.error('Or use local storage by setting usePluresDB: false in config');
544
+ } else {
545
+ console.error('Error inspecting memory:', error);
546
+ }
547
+ }
548
+ }
549
+
550
+ /**
551
+ * Wipe all cognitive memory storage
552
+ */
553
+ async function wipeMemory(): Promise<void> {
554
+ try {
555
+ const { SQLiteCompatibleAPI } = await import('pluresdb');
556
+
557
+ const db = new SQLiteCompatibleAPI({
558
+ config: {
559
+ port: 34567,
560
+ host: 'localhost',
561
+ dataDir: './pluresdb-data',
562
+ },
563
+ autoStart: true,
564
+ });
565
+
566
+ await db.start();
567
+
568
+ // Wipe all memory prefixes (matching Rust API implementation)
569
+ const prefixes = [
570
+ 'memory:session:',
571
+ 'memory:command:',
572
+ 'memory:output:',
573
+ 'memory:error:',
574
+ 'memory:insight:',
575
+ 'memory:suggestion:',
576
+ 'memory:provenance:',
577
+ 'memory:event:',
578
+ ];
579
+
580
+ let totalDeleted = 0;
581
+
582
+ for (const prefix of prefixes) {
583
+ const keys = await db.list(prefix);
584
+ for (const key of keys) {
585
+ try {
586
+ await db.delete(key);
587
+ totalDeleted++;
588
+ } catch (error) {
589
+ console.error(`Failed to delete ${key}:`, error);
590
+ }
591
+ }
592
+ }
593
+
594
+ console.log(`\n✅ Wiped all memory data (${totalDeleted} items deleted)\n`);
595
+ console.log('Warning: This permanently deleted all stored data.');
596
+
597
+ } catch (error: any) {
598
+ if (error.message && error.message.includes('PluresDB')) {
599
+ console.error('Error: PluresDB server is not available.');
600
+ console.error('Make sure PluresDB is running on localhost:34567');
601
+ } else {
602
+ console.error('Error wiping memory:', error);
603
+ }
604
+ process.exit(1);
605
+ }
606
+ }
607
+
608
+ /**
609
+ * Show observer events
610
+ */
611
+ async function showObserverEvents(limit: number = 10): Promise<void> {
612
+ const config = loadObserverConfig();
613
+
614
+ if (!config.enabled) {
615
+ console.log('Observer is not enabled. Run: runebook observer enable');
616
+ return;
617
+ }
618
+
619
+ const observer = createObserver(config);
620
+ await observer.initialize();
621
+ const events = await observer.getEvents(undefined, undefined, limit);
622
+
623
+ console.log(`\n=== Observer Events (${events.length}) ===\n`);
624
+
625
+ for (const event of events) {
626
+ const date = new Date(event.timestamp).toISOString();
627
+ console.log(`[${date}] ${event.type}`);
628
+ console.log(` Session: ${event.sessionId}`);
629
+ console.log(` Shell: ${event.shellType}`);
630
+ if (event.type === 'command_start') {
631
+ console.log(` Command: ${event.command} ${event.args.join(' ')}`);
632
+ console.log(` CWD: ${event.cwd}`);
633
+ }
634
+ console.log();
635
+ }
636
+
637
+ observer.stop();
638
+ }
639
+
640
+ /**
641
+ * Show analysis results for last failure
642
+ */
643
+ async function showLastAnalysis(): Promise<void> {
644
+ const obsConfig = loadObserverConfig();
645
+
646
+ if (!obsConfig.enabled) {
647
+ console.log('Observer is not enabled. Run: runebook observer enable');
648
+ return;
649
+ }
650
+
651
+ const observer = createObserver(obsConfig);
652
+ await observer.initialize();
653
+
654
+ // Get the store from observer (it's created during initialize)
655
+ // We need to access it through the observer's internal state
656
+ // For now, we'll create a new store with the same config
657
+ const { createEventStore } = await import('../lib/core/storage');
658
+ const store = createEventStore(obsConfig);
659
+
660
+ const analysisService = getAnalysisService();
661
+ analysisService.initialize(store, obsConfig);
662
+ analysisService.setEnabled(true);
663
+
664
+ // Get last exit_status event that failed
665
+ const events = await observer.getEvents('exit_status', undefined, 50);
666
+ const failures = events.filter(e => e.type === 'exit_status' && !e.success);
667
+
668
+ if (failures.length === 0) {
669
+ console.log('No recent failures found.');
670
+ observer.stop();
671
+ return;
672
+ }
673
+
674
+ // Process the most recent failure
675
+ const lastFailure = failures[0];
676
+ const jobId = await analysisService.processExitStatus(lastFailure);
677
+
678
+ if (!jobId) {
679
+ console.log('Failed to create analysis job.');
680
+ observer.stop();
681
+ return;
682
+ }
683
+
684
+ // Wait a bit for analysis to complete (non-blocking, but we'll poll)
685
+ let job = analysisService.getJob(jobId);
686
+ let attempts = 0;
687
+ while (job && job.status === 'running' && attempts < 30) {
688
+ await new Promise(resolve => setTimeout(resolve, 500));
689
+ job = analysisService.getJob(jobId);
690
+ attempts++;
691
+ }
692
+
693
+ job = analysisService.getJob(jobId);
694
+
695
+ if (!job) {
696
+ console.log('Analysis job not found.');
697
+ observer.stop();
698
+ return;
699
+ }
700
+
701
+ console.log('\n=== Analysis Results ===\n');
702
+ console.log(`Command: ${job.command} ${job.args.join(' ')}`);
703
+ console.log(`Exit Code: ${job.exitCode}`);
704
+ console.log(`Status: ${job.status}`);
705
+ console.log(`CWD: ${job.cwd}`);
706
+
707
+ if (job.stderr) {
708
+ console.log(`\nStderr:\n${job.stderr.substring(0, 500)}${job.stderr.length > 500 ? '...' : ''}`);
709
+ }
710
+
711
+ if (job.suggestions.length === 0) {
712
+ console.log('\nNo suggestions generated.');
713
+ } else {
714
+ console.log(`\n=== Suggestions (${job.suggestions.length}) ===\n`);
715
+
716
+ for (const suggestion of job.suggestions) {
717
+ console.log(`[${suggestion.provenance.analyzer}] ${suggestion.title} (confidence: ${(suggestion.confidence * 100).toFixed(0)}%)`);
718
+ console.log(` ${suggestion.description}`);
719
+ if (suggestion.actionableSnippet) {
720
+ console.log(`\n ${suggestion.actionableSnippet.split('\n').join('\n ')}`);
721
+ }
722
+ console.log();
723
+ }
724
+ }
725
+
726
+ observer.stop();
727
+ }
728
+
729
+ /**
730
+ * Main CLI handler
731
+ */
732
+ function main() {
733
+ const args = process.argv.slice(2);
734
+ const command = args[0];
735
+ const subcommand = args[1];
736
+
737
+ if (!command) {
738
+ console.log(`
739
+ RuneBook CLI
740
+
741
+ Usage:
742
+ runebook <command> [subcommand] [options]
743
+
744
+ Commands:
745
+ agent <command> Agent commands (enable, disable, status, suggestions, events, clear, config)
746
+ observer <command> Observer commands (enable, disable, status, events, tail)
747
+ analyze <command> Analysis commands (last)
748
+ memory <command> Memory commands (inspect)
749
+ llm <command> LLM/MCP commands (status)
750
+
751
+ Examples:
752
+ runebook agent enable
753
+ runebook agent status
754
+ runebook suggest status
755
+ runebook suggest top
756
+ runebook suggest last
757
+ runebook observer enable
758
+ runebook observer events tail
759
+ runebook observer events 20
760
+ runebook memory inspect
761
+ `);
762
+ process.exit(0);
763
+ }
764
+
765
+ (async () => {
766
+ try {
767
+ if (command === 'agent') {
768
+ // Agent commands
769
+ if (!subcommand) {
770
+ console.log(`
771
+ RuneBook Agent CLI
772
+
773
+ Usage:
774
+ runebook agent <command> [options]
775
+
776
+ Commands:
777
+ enable Enable the agent
778
+ disable Disable the agent
779
+ status Show agent status and statistics
780
+ suggestions Show current suggestions
781
+ events [limit] Show recent events (default: 10)
782
+ clear [days] Clear events older than N days (default: 30)
783
+ config <key> <val> Set configuration option
784
+ `);
785
+ process.exit(0);
786
+ }
787
+
788
+ switch (subcommand) {
789
+ case 'enable':
790
+ initAgentFromCLI({ enabled: true });
791
+ console.log('Agent enabled.');
792
+ break;
793
+
794
+ case 'disable':
795
+ initAgentFromCLI({ enabled: false });
796
+ console.log('Agent disabled.');
797
+ break;
798
+
799
+ case 'status':
800
+ await showStatus();
801
+ break;
802
+
803
+ case 'suggestions':
804
+ const priority = args[2] as 'low' | 'medium' | 'high' | undefined;
805
+ await showSuggestions(priority);
806
+ break;
807
+
808
+ case 'events':
809
+ const limit = args[2] ? parseInt(args[2], 10) : 10;
810
+ await showEvents(limit);
811
+ break;
812
+
813
+ case 'clear':
814
+ const days = args[2] ? parseInt(args[2], 10) : 30;
815
+ await clearEvents(days);
816
+ break;
817
+
818
+ case 'config':
819
+ const key = args[2];
820
+ const value = args[3];
821
+ if (!key || !value) {
822
+ console.error('Usage: runebook agent config <key> <value>');
823
+ process.exit(1);
824
+ }
825
+ const config = loadConfig();
826
+ (config as any)[key] = isNaN(Number(value)) ? value : Number(value);
827
+ saveConfig(config);
828
+ console.log(`Set ${key} = ${value}`);
829
+ break;
830
+
831
+ default:
832
+ console.error(`Unknown agent command: ${subcommand}`);
833
+ process.exit(1);
834
+ }
835
+ } else if (command === 'observer') {
836
+ // Observer commands
837
+ if (!subcommand) {
838
+ console.log(`
839
+ RuneBook Observer CLI
840
+
841
+ Usage:
842
+ runebook observer <command> [options]
843
+
844
+ Commands:
845
+ enable Enable the observer
846
+ disable Disable the observer
847
+ status Show observer status
848
+ events [limit] Show recent events (default: 10)
849
+ events tail Tail events in real-time
850
+ `);
851
+ process.exit(0);
852
+ }
853
+
854
+ switch (subcommand) {
855
+ case 'enable':
856
+ const obsConfig = loadObserverConfig();
857
+ obsConfig.enabled = true;
858
+ saveObserverConfig(obsConfig);
859
+ console.log('Observer enabled.');
860
+ break;
861
+
862
+ case 'disable':
863
+ const obsConfig2 = loadObserverConfig();
864
+ obsConfig2.enabled = false;
865
+ saveObserverConfig(obsConfig2);
866
+ console.log('Observer disabled.');
867
+ break;
868
+
869
+ case 'status':
870
+ const obsConfig3 = loadObserverConfig();
871
+ const observer = createObserver(obsConfig3);
872
+ await observer.initialize();
873
+ const stats = await observer.getStats();
874
+
875
+ console.log('\n=== Terminal Observer Status ===\n');
876
+ console.log(`Enabled: ${obsConfig3.enabled ? 'Yes' : 'No'}`);
877
+ console.log(`Shell Type: ${obsConfig3.shellType || 'auto-detect'}`);
878
+ console.log(`Redact Secrets: ${obsConfig3.redactSecrets ? 'Yes' : 'No'}`);
879
+ console.log(`Storage: ${obsConfig3.usePluresDB ? 'PluresDB' : 'Local'}`);
880
+ console.log(`Storage Path: ${obsConfig3.storagePath || 'In-memory'}`);
881
+ console.log(`Max Events: ${obsConfig3.maxEvents || 'Unlimited'}`);
882
+ console.log(`Retention Days: ${obsConfig3.retentionDays || 'Unlimited'}`);
883
+ console.log('\n=== Statistics ===');
884
+ console.log(`Total Events: ${stats.totalEvents}`);
885
+ console.log(`Sessions: ${stats.sessions}`);
886
+ console.log(`Events by Type:`);
887
+ for (const [type, count] of Object.entries(stats.eventsByType)) {
888
+ console.log(` ${type}: ${count}`);
889
+ }
890
+
891
+ observer.stop();
892
+ break;
893
+
894
+ case 'events':
895
+ if (args[2] === 'tail') {
896
+ await tailEvents(true);
897
+ } else {
898
+ const limit = args[2] ? parseInt(args[2], 10) : 10;
899
+ await showObserverEvents(limit);
900
+ }
901
+ break;
902
+
903
+ default:
904
+ console.error(`Unknown observer command: ${subcommand}`);
905
+ process.exit(1);
906
+ }
907
+ } else if (command === 'suggest') {
908
+ // Suggestion commands
909
+ if (!subcommand) {
910
+ console.log(`
911
+ RuneBook Suggest CLI
912
+
913
+ Usage:
914
+ runebook suggest <command>
915
+
916
+ Commands:
917
+ status Show current agent status (idle/analyzing/issues found)
918
+ top Show top suggestion on demand
919
+ last Show suggestions for last command
920
+
921
+ Examples:
922
+ runebook suggest status
923
+ runebook suggest top
924
+ runebook suggest last
925
+ `);
926
+ process.exit(0);
927
+ }
928
+
929
+ switch (subcommand) {
930
+ case 'status':
931
+ showSuggestStatus();
932
+ break;
933
+
934
+ case 'top':
935
+ await showTopSuggestion();
936
+ break;
937
+
938
+ case 'last':
939
+ await showLastCommandSuggestions();
940
+ break;
941
+
942
+ default:
943
+ console.error(`Unknown suggest command: ${subcommand}`);
944
+ process.exit(1);
945
+ }
946
+ } else if (command === 'analyze') {
947
+ // Analysis commands
948
+ if (!subcommand) {
949
+ console.log(`
950
+ RuneBook Analysis CLI
951
+
952
+ Usage:
953
+ runebook analyze <command> [options]
954
+
955
+ Commands:
956
+ last Analyze the last command failure
957
+ `);
958
+ process.exit(0);
959
+ }
960
+
961
+ switch (subcommand) {
962
+ case 'last':
963
+ await showLastAnalysis();
964
+ break;
965
+
966
+ default:
967
+ console.error(`Unknown analyze command: ${subcommand}`);
968
+ process.exit(1);
969
+ }
970
+ } else if (command === 'memory') {
971
+ // Memory commands
972
+ if (!subcommand) {
973
+ console.log(`
974
+ RuneBook Memory CLI
975
+
976
+ Usage:
977
+ runebook memory <command> [options]
978
+
979
+ Commands:
980
+ inspect Inspect cognitive memory storage
981
+ wipe Wipe all cognitive memory data (permanent deletion)
982
+ `);
983
+ process.exit(0);
984
+ }
985
+
986
+ switch (subcommand) {
987
+ case 'inspect':
988
+ await inspectMemory();
989
+ break;
990
+
991
+ case 'wipe':
992
+ // Confirm before wiping
993
+ console.log('⚠️ WARNING: This will permanently delete all memory data.');
994
+ console.log('This includes sessions, commands, outputs, errors, insights, suggestions, and provenance.');
995
+ console.log('');
996
+ console.log('To proceed, run: runebook memory wipe --confirm');
997
+ if (args[2] === '--confirm') {
998
+ await wipeMemory();
999
+ } else {
1000
+ console.log('\nUse --confirm flag to proceed with deletion.');
1001
+ }
1002
+ break;
1003
+
1004
+ default:
1005
+ console.error(`Unknown memory command: ${subcommand}`);
1006
+ process.exit(1);
1007
+ }
1008
+ } else if (command === 'llm') {
1009
+ // LLM commands
1010
+ if (!subcommand) {
1011
+ console.log(`
1012
+ RuneBook LLM CLI
1013
+
1014
+ Usage:
1015
+ runebook llm <command> [options]
1016
+
1017
+ Commands:
1018
+ status Show LLM provider status and configuration
1019
+ `);
1020
+ process.exit(0);
1021
+ }
1022
+
1023
+ switch (subcommand) {
1024
+ case 'status':
1025
+ await showLLMStatus();
1026
+ break;
1027
+
1028
+ default:
1029
+ console.error(`Unknown llm command: ${subcommand}`);
1030
+ process.exit(1);
1031
+ }
1032
+ } else {
1033
+ console.error(`Unknown command: ${command}`);
1034
+ console.log('Use "runebook agent", "runebook observer", "runebook suggest", "runebook analyze", "runebook memory", or "runebook llm"');
1035
+ process.exit(1);
1036
+ }
1037
+ } catch (error) {
1038
+ console.error('Error:', error);
1039
+ process.exit(1);
1040
+ }
1041
+ })();
1042
+ }
1043
+
1044
+ // Run if called directly
1045
+ if (import.meta.url === `file://${process.argv[1]}`) {
1046
+ main();
1047
+ }
1048
+
1049
+ export { main, loadConfig, saveConfig };
1050
+