@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,105 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
// Zsh shell adapter for terminal observer
|
|
3
|
+
// Provides hooks for capturing zsh shell events
|
|
4
|
+
|
|
5
|
+
import { BaseShellAdapter } from './base';
|
|
6
|
+
import type { ShellType, ObserverConfig } from '../types';
|
|
7
|
+
import type { EventStore } from '../storage';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { homedir } from 'os';
|
|
10
|
+
|
|
11
|
+
export class ZshAdapter extends BaseShellAdapter {
|
|
12
|
+
private hookScriptPath: string;
|
|
13
|
+
|
|
14
|
+
constructor() {
|
|
15
|
+
super();
|
|
16
|
+
this.hookScriptPath = join(homedir(), '.runebook', 'zsh-hook.sh');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getShellType(): ShellType {
|
|
20
|
+
return 'zsh';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
getHookScript(): string {
|
|
24
|
+
// @ts-ignore - Shell script content, not TypeScript
|
|
25
|
+
return (
|
|
26
|
+
'# RuneBook Terminal Observer Hook for Zsh\n' +
|
|
27
|
+
'# Add this to your ~/.zshrc\n\n' +
|
|
28
|
+
'if [ -n "$RUNBOOK_OBSERVER_ENABLED" ]; then\n' +
|
|
29
|
+
' # Function to capture command start\n' +
|
|
30
|
+
' __runebook_capture_start() {\n' +
|
|
31
|
+
' local cmd="$1"\n' +
|
|
32
|
+
' local args="${@:2}"\n' +
|
|
33
|
+
' local cwd="$PWD"\n \n' +
|
|
34
|
+
' # Call runebook observer API (if available)\n' +
|
|
35
|
+
' if command -v runebook >/dev/null 2>&1; then\n' +
|
|
36
|
+
' runebook observer capture-start "$cmd" "$args" "$cwd" &\n' +
|
|
37
|
+
' fi\n' +
|
|
38
|
+
' }\n\n' +
|
|
39
|
+
' # Function to capture command end\n' +
|
|
40
|
+
' __runebook_capture_end() {\n' +
|
|
41
|
+
' local exit_code=$?\n \n' +
|
|
42
|
+
' if command -v runebook >/dev/null 2>&1; then\n' +
|
|
43
|
+
' runebook observer capture-end $exit_code &\n' +
|
|
44
|
+
' fi\n \n' +
|
|
45
|
+
' return $exit_code\n' +
|
|
46
|
+
' }\n\n' +
|
|
47
|
+
' # Hook into command execution using zsh hooks\n' +
|
|
48
|
+
' preexec_functions+=(__runebook_capture_start)\n' +
|
|
49
|
+
' precmd_functions+=(__runebook_capture_end)\n' +
|
|
50
|
+
'fi\n'
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async initialize(config: ObserverConfig, store: EventStore): Promise<void> {
|
|
55
|
+
await super.initialize(config, store);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async start(): Promise<void> {
|
|
59
|
+
await super.start();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async stop(): Promise<void> {
|
|
63
|
+
await super.stop();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Programmatic capture - for use when shell hooks are not available
|
|
68
|
+
*/
|
|
69
|
+
async captureCommand(
|
|
70
|
+
command: string,
|
|
71
|
+
args: string[],
|
|
72
|
+
cwd: string,
|
|
73
|
+
env: Record<string, string>
|
|
74
|
+
): Promise<string> {
|
|
75
|
+
return await this.captureCommandStart(command, args, cwd, env);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Programmatic capture of command result
|
|
80
|
+
*/
|
|
81
|
+
async captureCommandResult(
|
|
82
|
+
commandId: string,
|
|
83
|
+
stdout: string,
|
|
84
|
+
stderr: string,
|
|
85
|
+
exitCode: number
|
|
86
|
+
): Promise<void> {
|
|
87
|
+
const chunkSize = this.config?.chunkSize || 4096;
|
|
88
|
+
|
|
89
|
+
// Capture stdout chunks
|
|
90
|
+
for (let i = 0; i < stdout.length; i += chunkSize) {
|
|
91
|
+
const chunk = stdout.substring(i, i + chunkSize);
|
|
92
|
+
await this.captureStdoutChunk(commandId, chunk);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Capture stderr chunks
|
|
96
|
+
for (let i = 0; i < stderr.length; i += chunkSize) {
|
|
97
|
+
const chunk = stderr.substring(i, i + chunkSize);
|
|
98
|
+
await this.captureStderrChunk(commandId, chunk);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
await this.captureExitStatus(commandId, exitCode);
|
|
102
|
+
await this.captureCommandEnd(commandId);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
// Event storage layer for terminal observer
|
|
2
|
+
// Supports both PluresDB and local file-based storage
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
TerminalObserverEvent,
|
|
6
|
+
EventStore,
|
|
7
|
+
EventType,
|
|
8
|
+
ObserverConfig,
|
|
9
|
+
} from './types';
|
|
10
|
+
import { mkdir, readFile, writeFile } from 'fs/promises';
|
|
11
|
+
import { join } from 'path';
|
|
12
|
+
import { homedir } from 'os';
|
|
13
|
+
import { existsSync } from 'fs';
|
|
14
|
+
|
|
15
|
+
// Re-export EventStore from types for convenience
|
|
16
|
+
export type { EventStore } from './types';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Local file-based storage adapter
|
|
20
|
+
* Stores events as JSON files in a directory
|
|
21
|
+
*/
|
|
22
|
+
export class LocalFileStore implements EventStore {
|
|
23
|
+
private events: TerminalObserverEvent[] = [];
|
|
24
|
+
private config: ObserverConfig;
|
|
25
|
+
private initialized = false;
|
|
26
|
+
private storagePath: string;
|
|
27
|
+
private eventsFile: string;
|
|
28
|
+
private writePromise: Promise<void> | null = null;
|
|
29
|
+
|
|
30
|
+
constructor(config: ObserverConfig) {
|
|
31
|
+
this.config = config;
|
|
32
|
+
this.storagePath = config.storagePath || join(homedir(), '.runebook', 'observer');
|
|
33
|
+
this.eventsFile = join(this.storagePath, 'events.json');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private async ensureInitialized(): Promise<void> {
|
|
37
|
+
if (this.initialized) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Create storage directory (mkdir with recursive handles already exists)
|
|
42
|
+
try {
|
|
43
|
+
await mkdir(this.storagePath, { recursive: true });
|
|
44
|
+
} catch (error) {
|
|
45
|
+
// Directory already exists or can't be created
|
|
46
|
+
console.error('Failed to create storage directory:', error);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Load existing events from file (handle ENOENT directly)
|
|
50
|
+
try {
|
|
51
|
+
const data = await readFile(this.eventsFile, 'utf-8');
|
|
52
|
+
this.events = JSON.parse(data);
|
|
53
|
+
} catch (error: unknown) {
|
|
54
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
|
55
|
+
// File doesn't exist yet, start with empty array
|
|
56
|
+
this.events = [];
|
|
57
|
+
} else {
|
|
58
|
+
console.error('Failed to load events from file:', error);
|
|
59
|
+
this.events = [];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.initialized = true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private async persistEvents(): Promise<void> {
|
|
67
|
+
// Serialize writes to prevent race conditions and data corruption
|
|
68
|
+
const writeOp = (async () => {
|
|
69
|
+
// If there's already a write in progress, wait for it
|
|
70
|
+
while (this.writePromise) {
|
|
71
|
+
await this.writePromise;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Now perform this write
|
|
75
|
+
try {
|
|
76
|
+
await writeFile(this.eventsFile, JSON.stringify(this.events, null, 2), 'utf-8');
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error('Failed to persist events to file:', error);
|
|
79
|
+
}
|
|
80
|
+
})();
|
|
81
|
+
|
|
82
|
+
this.writePromise = writeOp;
|
|
83
|
+
await writeOp;
|
|
84
|
+
|
|
85
|
+
// Clear writePromise only if it's still this operation
|
|
86
|
+
if (this.writePromise === writeOp) {
|
|
87
|
+
this.writePromise = null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async saveEvent(event: TerminalObserverEvent): Promise<void> {
|
|
92
|
+
await this.ensureInitialized();
|
|
93
|
+
|
|
94
|
+
this.events.push(event);
|
|
95
|
+
|
|
96
|
+
// Enforce max events limit
|
|
97
|
+
if (this.config.maxEvents && this.config.maxEvents > 0) {
|
|
98
|
+
if (this.events.length > this.config.maxEvents) {
|
|
99
|
+
// Remove oldest events
|
|
100
|
+
this.events = this.events.slice(-this.config.maxEvents);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Persist to file
|
|
105
|
+
await this.persistEvents();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async getEvents(
|
|
109
|
+
type?: EventType,
|
|
110
|
+
since?: number,
|
|
111
|
+
limit?: number
|
|
112
|
+
): Promise<TerminalObserverEvent[]> {
|
|
113
|
+
await this.ensureInitialized();
|
|
114
|
+
|
|
115
|
+
let filtered = this.events;
|
|
116
|
+
|
|
117
|
+
if (type) {
|
|
118
|
+
filtered = filtered.filter(e => e.type === type);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (since) {
|
|
122
|
+
filtered = filtered.filter(e => e.timestamp >= since);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Sort by timestamp (newest first)
|
|
126
|
+
filtered.sort((a, b) => b.timestamp - a.timestamp);
|
|
127
|
+
|
|
128
|
+
if (limit && limit > 0) {
|
|
129
|
+
filtered = filtered.slice(0, limit);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return filtered;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async getEventsByCommand(
|
|
136
|
+
commandId: string
|
|
137
|
+
): Promise<TerminalObserverEvent[]> {
|
|
138
|
+
await this.ensureInitialized();
|
|
139
|
+
|
|
140
|
+
return this.events.filter(e => {
|
|
141
|
+
// command_start events use their id as the command identifier
|
|
142
|
+
if (e.type === 'command_start' && e.id === commandId) {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
// Other events reference the command via commandId field
|
|
146
|
+
if ('commandId' in e && e.commandId === commandId) {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
}).sort((a, b) => a.timestamp - b.timestamp);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async getEventsBySession(
|
|
154
|
+
sessionId: string,
|
|
155
|
+
limit?: number
|
|
156
|
+
): Promise<TerminalObserverEvent[]> {
|
|
157
|
+
await this.ensureInitialized();
|
|
158
|
+
|
|
159
|
+
let filtered = this.events.filter(e => e.sessionId === sessionId);
|
|
160
|
+
|
|
161
|
+
filtered.sort((a, b) => b.timestamp - a.timestamp);
|
|
162
|
+
|
|
163
|
+
if (limit && limit > 0) {
|
|
164
|
+
filtered = filtered.slice(0, limit);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return filtered;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async clearEvents(olderThan?: number): Promise<void> {
|
|
171
|
+
await this.ensureInitialized();
|
|
172
|
+
|
|
173
|
+
if (olderThan) {
|
|
174
|
+
this.events = this.events.filter(e => e.timestamp >= olderThan);
|
|
175
|
+
} else {
|
|
176
|
+
this.events = [];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Persist changes to file
|
|
180
|
+
await this.persistEvents();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async getStats(): Promise<{
|
|
184
|
+
totalEvents: number;
|
|
185
|
+
eventsByType: Record<EventType, number>;
|
|
186
|
+
sessions: number;
|
|
187
|
+
}> {
|
|
188
|
+
await this.ensureInitialized();
|
|
189
|
+
|
|
190
|
+
const eventsByType: Record<string, number> = {};
|
|
191
|
+
const sessions = new Set<string>();
|
|
192
|
+
|
|
193
|
+
for (const event of this.events) {
|
|
194
|
+
eventsByType[event.type] = (eventsByType[event.type] || 0) + 1;
|
|
195
|
+
sessions.add(event.sessionId);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
totalEvents: this.events.length,
|
|
200
|
+
eventsByType: eventsByType as Record<EventType, number>,
|
|
201
|
+
sessions: sessions.size,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* PluresDB storage adapter
|
|
208
|
+
*/
|
|
209
|
+
export class PluresDBEventStore implements EventStore {
|
|
210
|
+
private db: any = null;
|
|
211
|
+
private readonly eventPrefix = 'observer:event:';
|
|
212
|
+
private initialized = false;
|
|
213
|
+
private config: ObserverConfig;
|
|
214
|
+
|
|
215
|
+
constructor(config: ObserverConfig) {
|
|
216
|
+
this.config = config;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private async ensureInitialized(): Promise<void> {
|
|
220
|
+
if (this.initialized && this.db) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
const { SQLiteCompatibleAPI } = await import('pluresdb');
|
|
226
|
+
|
|
227
|
+
this.db = new SQLiteCompatibleAPI({
|
|
228
|
+
config: {
|
|
229
|
+
port: 34567,
|
|
230
|
+
host: 'localhost',
|
|
231
|
+
dataDir: this.config.storagePath || './pluresdb-data',
|
|
232
|
+
},
|
|
233
|
+
autoStart: true,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
await this.db.start();
|
|
237
|
+
this.initialized = true;
|
|
238
|
+
} catch (error) {
|
|
239
|
+
console.error('Failed to initialize PluresDB for observer storage:', error);
|
|
240
|
+
throw new Error('PluresDB initialization failed for observer storage');
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async saveEvent(event: TerminalObserverEvent): Promise<void> {
|
|
245
|
+
await this.ensureInitialized();
|
|
246
|
+
const key = `${this.eventPrefix}${event.id}`;
|
|
247
|
+
await this.db.put(key, event);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async getEvents(
|
|
251
|
+
type?: EventType,
|
|
252
|
+
since?: number,
|
|
253
|
+
limit?: number
|
|
254
|
+
): Promise<TerminalObserverEvent[]> {
|
|
255
|
+
await this.ensureInitialized();
|
|
256
|
+
const keys = await this.db.list(this.eventPrefix);
|
|
257
|
+
const events: TerminalObserverEvent[] = [];
|
|
258
|
+
|
|
259
|
+
for (const key of keys) {
|
|
260
|
+
try {
|
|
261
|
+
const event = await this.db.getValue(key);
|
|
262
|
+
if (event) {
|
|
263
|
+
if (type && event.type !== type) {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (since && event.timestamp < since) {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
events.push(event as TerminalObserverEvent);
|
|
270
|
+
}
|
|
271
|
+
} catch (error) {
|
|
272
|
+
console.error('Failed to load event:', error);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
events.sort((a, b) => b.timestamp - a.timestamp);
|
|
277
|
+
return limit && limit > 0 ? events.slice(0, limit) : events;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async getEventsByCommand(
|
|
281
|
+
commandId: string
|
|
282
|
+
): Promise<TerminalObserverEvent[]> {
|
|
283
|
+
const allEvents = await this.getEvents();
|
|
284
|
+
return allEvents.filter(e => {
|
|
285
|
+
// command_start events use their id as the command identifier
|
|
286
|
+
if (e.type === 'command_start' && e.id === commandId) {
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
// Other events reference the command via commandId field
|
|
290
|
+
if ('commandId' in e && e.commandId === commandId) {
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
return false;
|
|
294
|
+
}).sort((a, b) => a.timestamp - b.timestamp);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async getEventsBySession(
|
|
298
|
+
sessionId: string,
|
|
299
|
+
limit?: number
|
|
300
|
+
): Promise<TerminalObserverEvent[]> {
|
|
301
|
+
const allEvents = await this.getEvents();
|
|
302
|
+
let filtered = allEvents.filter(e => e.sessionId === sessionId);
|
|
303
|
+
|
|
304
|
+
filtered.sort((a, b) => b.timestamp - a.timestamp);
|
|
305
|
+
|
|
306
|
+
if (limit && limit > 0) {
|
|
307
|
+
filtered = filtered.slice(0, limit);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return filtered;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async clearEvents(olderThan?: number): Promise<void> {
|
|
314
|
+
await this.ensureInitialized();
|
|
315
|
+
const keys = await this.db.list(this.eventPrefix);
|
|
316
|
+
|
|
317
|
+
for (const key of keys) {
|
|
318
|
+
try {
|
|
319
|
+
const event = await this.db.getValue(key);
|
|
320
|
+
if (event && (!olderThan || event.timestamp < olderThan)) {
|
|
321
|
+
await this.db.delete(key);
|
|
322
|
+
}
|
|
323
|
+
} catch (error) {
|
|
324
|
+
console.error('Failed to delete event:', error);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async getStats(): Promise<{
|
|
330
|
+
totalEvents: number;
|
|
331
|
+
eventsByType: Record<EventType, number>;
|
|
332
|
+
sessions: number;
|
|
333
|
+
}> {
|
|
334
|
+
const events = await this.getEvents();
|
|
335
|
+
const eventsByType: Record<string, number> = {};
|
|
336
|
+
const sessions = new Set<string>();
|
|
337
|
+
|
|
338
|
+
for (const event of events) {
|
|
339
|
+
eventsByType[event.type] = (eventsByType[event.type] || 0) + 1;
|
|
340
|
+
sessions.add(event.sessionId);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
totalEvents: events.length,
|
|
345
|
+
eventsByType: eventsByType as Record<EventType, number>,
|
|
346
|
+
sessions: sessions.size,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Create event store based on config
|
|
353
|
+
*/
|
|
354
|
+
export function createEventStore(config: ObserverConfig): EventStore {
|
|
355
|
+
if (config.usePluresDB && config.storagePath) {
|
|
356
|
+
return new PluresDBEventStore(config);
|
|
357
|
+
}
|
|
358
|
+
return new LocalFileStore(config);
|
|
359
|
+
}
|
|
360
|
+
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// Canonical event schema for terminal observer layer
|
|
2
|
+
// Low-level shell event capture
|
|
3
|
+
|
|
4
|
+
export type ShellType = 'bash' | 'zsh' | 'nushell' | 'unknown';
|
|
5
|
+
|
|
6
|
+
export type EventType =
|
|
7
|
+
| 'command_start'
|
|
8
|
+
| 'command_end'
|
|
9
|
+
| 'stdout_chunk'
|
|
10
|
+
| 'stderr_chunk'
|
|
11
|
+
| 'exit_status'
|
|
12
|
+
| 'cwd_change'
|
|
13
|
+
| 'env_change'
|
|
14
|
+
| 'session_start'
|
|
15
|
+
| 'session_end';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Base event structure for all terminal observer events
|
|
19
|
+
*/
|
|
20
|
+
export interface BaseTerminalEvent {
|
|
21
|
+
id: string;
|
|
22
|
+
type: EventType;
|
|
23
|
+
timestamp: number;
|
|
24
|
+
sessionId: string;
|
|
25
|
+
shellType: ShellType;
|
|
26
|
+
paneId?: string; // Terminal pane/tab identifier
|
|
27
|
+
tabId?: string; // Terminal tab identifier (if applicable)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Command start event - fired when a command begins execution
|
|
32
|
+
*/
|
|
33
|
+
export interface CommandStartEvent extends BaseTerminalEvent {
|
|
34
|
+
type: 'command_start';
|
|
35
|
+
command: string;
|
|
36
|
+
args: string[];
|
|
37
|
+
cwd: string;
|
|
38
|
+
envSummary: Record<string, string>; // Sanitized environment variables
|
|
39
|
+
pid?: number; // Process ID if available
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Command end event - fired when a command completes
|
|
44
|
+
*/
|
|
45
|
+
export interface CommandEndEvent extends BaseTerminalEvent {
|
|
46
|
+
type: 'command_end';
|
|
47
|
+
commandId: string; // Reference to command_start event
|
|
48
|
+
duration: number; // Milliseconds
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Stdout chunk event - incremental stdout output
|
|
53
|
+
*/
|
|
54
|
+
export interface StdoutChunkEvent extends BaseTerminalEvent {
|
|
55
|
+
type: 'stdout_chunk';
|
|
56
|
+
commandId: string; // Reference to command_start event
|
|
57
|
+
chunk: string;
|
|
58
|
+
chunkIndex: number; // Sequential chunk number for this command
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Stderr chunk event - incremental stderr output
|
|
63
|
+
*/
|
|
64
|
+
export interface StderrChunkEvent extends BaseTerminalEvent {
|
|
65
|
+
type: 'stderr_chunk';
|
|
66
|
+
commandId: string; // Reference to command_start event
|
|
67
|
+
chunk: string;
|
|
68
|
+
chunkIndex: number; // Sequential chunk number for this command
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Exit status event - command exit code
|
|
73
|
+
*/
|
|
74
|
+
export interface ExitStatusEvent extends BaseTerminalEvent {
|
|
75
|
+
type: 'exit_status';
|
|
76
|
+
commandId: string; // Reference to command_start event
|
|
77
|
+
exitCode: number;
|
|
78
|
+
success: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* CWD change event - working directory changed
|
|
83
|
+
*/
|
|
84
|
+
export interface CwdChangeEvent extends BaseTerminalEvent {
|
|
85
|
+
type: 'cwd_change';
|
|
86
|
+
cwd: string;
|
|
87
|
+
previousCwd?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Environment change event - environment variables changed
|
|
92
|
+
*/
|
|
93
|
+
export interface EnvChangeEvent extends BaseTerminalEvent {
|
|
94
|
+
type: 'env_change';
|
|
95
|
+
envSummary: Record<string, string>; // Sanitized environment variables
|
|
96
|
+
changedKeys: string[]; // Keys that were added/modified
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Session start event
|
|
101
|
+
*/
|
|
102
|
+
export interface SessionStartEvent extends BaseTerminalEvent {
|
|
103
|
+
type: 'session_start';
|
|
104
|
+
shellType: ShellType;
|
|
105
|
+
cwd: string;
|
|
106
|
+
envSummary: Record<string, string>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Session end event
|
|
111
|
+
*/
|
|
112
|
+
export interface SessionEndEvent extends BaseTerminalEvent {
|
|
113
|
+
type: 'session_end';
|
|
114
|
+
duration: number; // Session duration in milliseconds
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Union type for all terminal observer events
|
|
119
|
+
*/
|
|
120
|
+
export type TerminalObserverEvent =
|
|
121
|
+
| CommandStartEvent
|
|
122
|
+
| CommandEndEvent
|
|
123
|
+
| StdoutChunkEvent
|
|
124
|
+
| StderrChunkEvent
|
|
125
|
+
| ExitStatusEvent
|
|
126
|
+
| CwdChangeEvent
|
|
127
|
+
| EnvChangeEvent
|
|
128
|
+
| SessionStartEvent
|
|
129
|
+
| SessionEndEvent;
|
|
130
|
+
|
|
131
|
+
// Re-export LLMProviderConfig from agent/llm/types
|
|
132
|
+
export type { LLMProviderConfig } from '../agent/llm/types';
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Configuration for terminal observer
|
|
136
|
+
*/
|
|
137
|
+
export interface ObserverConfig {
|
|
138
|
+
enabled: boolean; // Opt-in flag
|
|
139
|
+
shellType?: ShellType; // Auto-detect if not specified
|
|
140
|
+
sessionId?: string; // Auto-generate if not specified
|
|
141
|
+
paneId?: string;
|
|
142
|
+
tabId?: string;
|
|
143
|
+
storagePath?: string; // Path for local storage or PluresDB
|
|
144
|
+
usePluresDB?: boolean; // Use PluresDB if available, otherwise local store
|
|
145
|
+
redactSecrets: boolean; // Enable secret redaction
|
|
146
|
+
secretPatterns?: string[]; // Additional patterns to redact
|
|
147
|
+
chunkSize?: number; // Max size for stdout/stderr chunks (bytes)
|
|
148
|
+
maxEvents?: number; // Maximum events to store (0 = unlimited)
|
|
149
|
+
retentionDays?: number; // Days to retain events (0 = unlimited)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Event storage interface
|
|
154
|
+
*/
|
|
155
|
+
export interface EventStore {
|
|
156
|
+
saveEvent(event: TerminalObserverEvent): Promise<void>;
|
|
157
|
+
getEvents(
|
|
158
|
+
type?: EventType,
|
|
159
|
+
since?: number,
|
|
160
|
+
limit?: number
|
|
161
|
+
): Promise<TerminalObserverEvent[]>;
|
|
162
|
+
getEventsByCommand(
|
|
163
|
+
commandId: string
|
|
164
|
+
): Promise<TerminalObserverEvent[]>;
|
|
165
|
+
getEventsBySession(
|
|
166
|
+
sessionId: string,
|
|
167
|
+
limit?: number
|
|
168
|
+
): Promise<TerminalObserverEvent[]>;
|
|
169
|
+
clearEvents(olderThan?: number): Promise<void>;
|
|
170
|
+
getStats(): Promise<{
|
|
171
|
+
totalEvents: number;
|
|
172
|
+
eventsByType: Record<EventType, number>;
|
|
173
|
+
sessions: number;
|
|
174
|
+
}>;
|
|
175
|
+
}
|
|
176
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
tui?: boolean;
|
|
6
|
+
surface?: 1 | 2 | 3 | 4;
|
|
7
|
+
border?: boolean;
|
|
8
|
+
pad?: 1 | 2 | 3 | 4 | 5 | 6;
|
|
9
|
+
radius?: 1 | 2 | 3 | 4;
|
|
10
|
+
shadow?: 1 | 2 | 3;
|
|
11
|
+
class?: string;
|
|
12
|
+
style?: string;
|
|
13
|
+
children?: Snippet;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let {
|
|
17
|
+
tui = false,
|
|
18
|
+
surface,
|
|
19
|
+
border = false,
|
|
20
|
+
pad,
|
|
21
|
+
radius,
|
|
22
|
+
shadow,
|
|
23
|
+
class: cls = '',
|
|
24
|
+
style = '',
|
|
25
|
+
children
|
|
26
|
+
}: Props = $props();
|
|
27
|
+
|
|
28
|
+
const inlineStyle = $derived([
|
|
29
|
+
surface != null ? `background: var(--surface-${surface})` : null,
|
|
30
|
+
border ? `border: 1px solid var(--border-color)` : null,
|
|
31
|
+
pad != null ? `padding: var(--space-${pad})` : null,
|
|
32
|
+
radius != null ? `border-radius: var(--radius-${radius})` : null,
|
|
33
|
+
shadow != null ? `box-shadow: var(--shadow-${shadow})` : null,
|
|
34
|
+
style || null,
|
|
35
|
+
].filter(Boolean).join('; '));
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<div class="dd-box {cls}" style={inlineStyle} data-tui={tui}>
|
|
39
|
+
{@render children?.()}
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<style>
|
|
43
|
+
.dd-box {
|
|
44
|
+
color: var(--text-1);
|
|
45
|
+
box-sizing: border-box;
|
|
46
|
+
}
|
|
47
|
+
</style>
|