@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,327 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { TransformNode } from '../types/canvas';
|
|
3
|
+
import { canvasStore, nodeDataStore, getNodeInputData, updateNodeData } from '../stores/canvas';
|
|
4
|
+
import Box from '../design-dojo/Box.svelte';
|
|
5
|
+
import Select from '../design-dojo/Select.svelte';
|
|
6
|
+
import Text from '../design-dojo/Text.svelte';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
node: TransformNode;
|
|
10
|
+
tui?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let { node, tui = false }: Props = $props();
|
|
14
|
+
|
|
15
|
+
let output = $state<any>('');
|
|
16
|
+
let error = $state<string>('');
|
|
17
|
+
let isProcessing = $state(false);
|
|
18
|
+
|
|
19
|
+
// Subscribe to node data changes and apply transformation
|
|
20
|
+
$effect(() => {
|
|
21
|
+
const canvas = $canvasStore;
|
|
22
|
+
const nodeData = $nodeDataStore;
|
|
23
|
+
|
|
24
|
+
// Get input data from connected nodes
|
|
25
|
+
if (node.inputs && node.inputs.length > 0) {
|
|
26
|
+
const inputData = getNodeInputData(node.id, node.inputs[0].id, canvas.connections, nodeData);
|
|
27
|
+
if (inputData !== undefined) {
|
|
28
|
+
applyTransform(inputData);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
async function applyTransform(inputData: any) {
|
|
34
|
+
if (!node.code.trim()) {
|
|
35
|
+
output = inputData;
|
|
36
|
+
updateNodeData(node.id, 'output', inputData);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
isProcessing = true;
|
|
41
|
+
error = '';
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
let result: any;
|
|
45
|
+
|
|
46
|
+
switch (node.transformType) {
|
|
47
|
+
case 'map':
|
|
48
|
+
// Execute JavaScript map function
|
|
49
|
+
if (Array.isArray(inputData)) {
|
|
50
|
+
// Note: Using Function constructor allows user-defined transformations
|
|
51
|
+
// This is intended for local use only. Do not use with untrusted input.
|
|
52
|
+
const mapFn = new Function('item', 'index', `"use strict"; return (${node.code})`);
|
|
53
|
+
result = inputData.map((item, index) => mapFn(item, index));
|
|
54
|
+
} else {
|
|
55
|
+
error = 'Map transform requires array input';
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
break;
|
|
59
|
+
|
|
60
|
+
case 'filter':
|
|
61
|
+
// Execute JavaScript filter function
|
|
62
|
+
if (Array.isArray(inputData)) {
|
|
63
|
+
const filterFn = new Function('item', 'index', `"use strict"; return (${node.code})`);
|
|
64
|
+
result = inputData.filter((item, index) => filterFn(item, index));
|
|
65
|
+
} else {
|
|
66
|
+
error = 'Filter transform requires array input';
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
break;
|
|
70
|
+
|
|
71
|
+
case 'reduce':
|
|
72
|
+
// Execute JavaScript reduce function
|
|
73
|
+
if (Array.isArray(inputData)) {
|
|
74
|
+
if (inputData.length === 0) {
|
|
75
|
+
error = 'Reduce transform requires non-empty array';
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const reduceFn = new Function('acc', 'item', 'index', `"use strict"; return (${node.code})`);
|
|
79
|
+
// Provide initial value of 0 for safety
|
|
80
|
+
result = inputData.reduce((acc, item, index) => reduceFn(acc, item, index), 0);
|
|
81
|
+
} else {
|
|
82
|
+
error = 'Reduce transform requires array input';
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
break;
|
|
86
|
+
|
|
87
|
+
case 'sudolang':
|
|
88
|
+
// Sudolang is not implemented yet - just pass through
|
|
89
|
+
result = inputData;
|
|
90
|
+
error = 'Sudolang transform not yet implemented - passing through data';
|
|
91
|
+
break;
|
|
92
|
+
|
|
93
|
+
default:
|
|
94
|
+
result = inputData;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
output = result;
|
|
98
|
+
updateNodeData(node.id, 'output', result);
|
|
99
|
+
} catch (e) {
|
|
100
|
+
error = e instanceof Error ? e.message : String(e);
|
|
101
|
+
output = '';
|
|
102
|
+
} finally {
|
|
103
|
+
isProcessing = false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function updateCode(event: Event) {
|
|
108
|
+
const target = event.target as HTMLTextAreaElement;
|
|
109
|
+
canvasStore.updateNode(node.id, { code: target.value });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function updateTransformType(event: Event) {
|
|
113
|
+
const target = event.target as HTMLSelectElement;
|
|
114
|
+
const newType = target.value as TransformNode['transformType'];
|
|
115
|
+
canvasStore.updateNode(node.id, { transformType: newType });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function getPlaceholder(type: TransformNode['transformType']): string {
|
|
119
|
+
switch (type) {
|
|
120
|
+
case 'map':
|
|
121
|
+
return 'item * 2';
|
|
122
|
+
case 'filter':
|
|
123
|
+
return 'item > 10';
|
|
124
|
+
case 'reduce':
|
|
125
|
+
return 'acc + item';
|
|
126
|
+
case 'sudolang':
|
|
127
|
+
return '// Sudolang code';
|
|
128
|
+
default:
|
|
129
|
+
return '';
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
</script>
|
|
133
|
+
|
|
134
|
+
<Box class="transform-node" surface={2} border radius={3} shadow={2} style="border-color: var(--accent)" {tui}>
|
|
135
|
+
<Box class="node-header" surface={3} {tui}>
|
|
136
|
+
<span class="node-icon">🔄</span>
|
|
137
|
+
<Text class="node-title">{node.label || 'Transform'}</Text>
|
|
138
|
+
</Box>
|
|
139
|
+
|
|
140
|
+
<Box class="node-body" pad={3}>
|
|
141
|
+
<div class="control-group">
|
|
142
|
+
<label for="transform-type-{node.id}">Type:</label>
|
|
143
|
+
<Select
|
|
144
|
+
{tui}
|
|
145
|
+
id="transform-type-{node.id}"
|
|
146
|
+
value={node.transformType}
|
|
147
|
+
onchange={updateTransformType}
|
|
148
|
+
>
|
|
149
|
+
<option value="map">Map</option>
|
|
150
|
+
<option value="filter">Filter</option>
|
|
151
|
+
<option value="reduce">Reduce</option>
|
|
152
|
+
<option value="sudolang">Sudolang (planned)</option>
|
|
153
|
+
</Select>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<div class="control-group">
|
|
157
|
+
<label for="code-{node.id}">Code:</label>
|
|
158
|
+
<textarea
|
|
159
|
+
id="code-{node.id}"
|
|
160
|
+
value={node.code}
|
|
161
|
+
oninput={updateCode}
|
|
162
|
+
placeholder={getPlaceholder(node.transformType)}
|
|
163
|
+
rows="4"
|
|
164
|
+
class="code-textarea"
|
|
165
|
+
></textarea>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
{#if error}
|
|
169
|
+
<Box class="error-message" surface={1} pad={2} radius={2}>
|
|
170
|
+
<Text class="error-text">{error}</Text>
|
|
171
|
+
</Box>
|
|
172
|
+
{/if}
|
|
173
|
+
|
|
174
|
+
{#if isProcessing}
|
|
175
|
+
<Box class="status-processing" surface={1} pad={2} radius={2}>
|
|
176
|
+
<Text variant={2}>Processing...</Text>
|
|
177
|
+
</Box>
|
|
178
|
+
{:else if output !== ''}
|
|
179
|
+
<Box class="output-preview" surface={1} pad={2} radius={2}>
|
|
180
|
+
<Text class="output-label">Output:</Text>
|
|
181
|
+
<Text mono variant={1} class="output-pre">{typeof output === 'object' ? JSON.stringify(output, null, 2) : String(output)}</Text>
|
|
182
|
+
</Box>
|
|
183
|
+
{/if}
|
|
184
|
+
</Box>
|
|
185
|
+
|
|
186
|
+
<!-- Input and output ports -->
|
|
187
|
+
<div class="ports">
|
|
188
|
+
{#each node.inputs as port}
|
|
189
|
+
<div class="port input-port" data-port-id={port.id}>
|
|
190
|
+
<span class="port-label">{port.name}</span>
|
|
191
|
+
</div>
|
|
192
|
+
{/each}
|
|
193
|
+
|
|
194
|
+
{#each node.outputs as port}
|
|
195
|
+
<div class="port output-port" data-port-id={port.id}>
|
|
196
|
+
<span class="port-label">{port.name}</span>
|
|
197
|
+
</div>
|
|
198
|
+
{/each}
|
|
199
|
+
</div>
|
|
200
|
+
</Box>
|
|
201
|
+
|
|
202
|
+
<style>
|
|
203
|
+
:global(.transform-node) {
|
|
204
|
+
min-width: 320px;
|
|
205
|
+
max-width: 450px;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
:global(.transform-node .node-header) {
|
|
209
|
+
padding: var(--space-2) var(--space-3);
|
|
210
|
+
border-bottom: 1px solid var(--border-color);
|
|
211
|
+
display: flex;
|
|
212
|
+
align-items: center;
|
|
213
|
+
gap: var(--space-2);
|
|
214
|
+
border-radius: var(--radius-3) var(--radius-3) 0 0;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.node-icon {
|
|
218
|
+
font-size: 18px;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
:global(.transform-node .node-title) {
|
|
222
|
+
font-weight: 600;
|
|
223
|
+
font-size: var(--font-size-1);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
:global(.transform-node .node-body) {
|
|
227
|
+
padding: var(--space-3);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.control-group {
|
|
231
|
+
margin-bottom: var(--space-3);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
label {
|
|
235
|
+
display: block;
|
|
236
|
+
margin-bottom: var(--space-1);
|
|
237
|
+
font-size: var(--font-size-0);
|
|
238
|
+
color: var(--text-2);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.code-textarea {
|
|
242
|
+
width: 100%;
|
|
243
|
+
padding: var(--space-2);
|
|
244
|
+
background: var(--surface-1);
|
|
245
|
+
border: 1px solid var(--border-color);
|
|
246
|
+
border-radius: var(--radius-2);
|
|
247
|
+
color: var(--text-1);
|
|
248
|
+
font-family: var(--font-mono);
|
|
249
|
+
font-size: var(--font-size-0);
|
|
250
|
+
resize: vertical;
|
|
251
|
+
box-sizing: border-box;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.code-textarea::placeholder {
|
|
255
|
+
color: var(--text-3);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.code-textarea:focus {
|
|
259
|
+
outline: none;
|
|
260
|
+
border-color: var(--brand);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
:global(.transform-node .error-message) {
|
|
264
|
+
margin-top: var(--space-2);
|
|
265
|
+
border: 1px solid var(--error);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
:global(.transform-node .error-text) {
|
|
269
|
+
font-size: var(--font-size-0);
|
|
270
|
+
color: var(--error);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
:global(.transform-node .status-processing) {
|
|
274
|
+
margin-top: var(--space-2);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
:global(.transform-node .output-preview) {
|
|
278
|
+
margin-top: var(--space-2);
|
|
279
|
+
max-height: 150px;
|
|
280
|
+
overflow-y: auto;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
:global(.transform-node .output-label) {
|
|
284
|
+
display: block;
|
|
285
|
+
margin-bottom: var(--space-1);
|
|
286
|
+
font-size: var(--font-size-0);
|
|
287
|
+
font-weight: 600;
|
|
288
|
+
color: var(--brand);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
:global(.transform-node .output-pre) {
|
|
292
|
+
display: block;
|
|
293
|
+
font-size: var(--font-size-0);
|
|
294
|
+
white-space: pre-wrap;
|
|
295
|
+
word-break: break-word;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.ports {
|
|
299
|
+
position: relative;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.port {
|
|
303
|
+
position: absolute;
|
|
304
|
+
width: 12px;
|
|
305
|
+
height: 12px;
|
|
306
|
+
background: var(--brand);
|
|
307
|
+
border: 2px solid var(--surface-2);
|
|
308
|
+
border-radius: 50%;
|
|
309
|
+
cursor: crosshair;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.input-port {
|
|
313
|
+
left: -8px;
|
|
314
|
+
top: 50%;
|
|
315
|
+
transform: translateY(-50%);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.output-port {
|
|
319
|
+
right: -8px;
|
|
320
|
+
top: 50%;
|
|
321
|
+
transform: translateY(-50%);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.port-label {
|
|
325
|
+
display: none;
|
|
326
|
+
}
|
|
327
|
+
</style>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// RuneBook Core - Terminal Observer Module
|
|
2
|
+
// Main entry point for event capture and observability
|
|
3
|
+
|
|
4
|
+
export { TerminalObserver, createObserver, defaultObserverConfig } from './observer';
|
|
5
|
+
export type { ObserverConfig } from './types';
|
|
6
|
+
export type { TerminalObserverEvent, EventType, ShellType } from './types';
|
|
7
|
+
export { createEventStore } from './storage';
|
|
8
|
+
export type { EventStore } from './storage';
|
|
9
|
+
export { createShellAdapter, detectShellType } from './shell-adapters';
|
|
10
|
+
export type { ShellAdapter } from './shell-adapters';
|
|
11
|
+
export {
|
|
12
|
+
sanitizeEnv,
|
|
13
|
+
redactSecretsFromText,
|
|
14
|
+
isSecretKey,
|
|
15
|
+
redactValue,
|
|
16
|
+
validateRedaction,
|
|
17
|
+
} from './redaction';
|
|
18
|
+
|
|
19
|
+
// Re-export types for convenience
|
|
20
|
+
export type {
|
|
21
|
+
CommandStartEvent,
|
|
22
|
+
CommandEndEvent,
|
|
23
|
+
StdoutChunkEvent,
|
|
24
|
+
StderrChunkEvent,
|
|
25
|
+
ExitStatusEvent,
|
|
26
|
+
CwdChangeEvent,
|
|
27
|
+
EnvChangeEvent,
|
|
28
|
+
SessionStartEvent,
|
|
29
|
+
SessionEndEvent,
|
|
30
|
+
} from './types';
|
|
31
|
+
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
// Terminal Observer - Main coordinator for event capture
|
|
2
|
+
// Orchestrates shell adapters, event storage, and configuration
|
|
3
|
+
|
|
4
|
+
import { createShellAdapter, detectShellType } from './shell-adapters';
|
|
5
|
+
import { createEventStore } from './storage';
|
|
6
|
+
import type {
|
|
7
|
+
ObserverConfig,
|
|
8
|
+
TerminalObserverEvent,
|
|
9
|
+
EventType,
|
|
10
|
+
ShellType,
|
|
11
|
+
} from './types';
|
|
12
|
+
import type { ShellAdapter } from './shell-adapters';
|
|
13
|
+
import type { EventStore } from './storage';
|
|
14
|
+
|
|
15
|
+
// Re-export ObserverConfig for convenience
|
|
16
|
+
export type { ObserverConfig } from './types';
|
|
17
|
+
|
|
18
|
+
export class TerminalObserver {
|
|
19
|
+
private config: ObserverConfig;
|
|
20
|
+
private adapter: ShellAdapter | null = null;
|
|
21
|
+
private store: EventStore | null = null;
|
|
22
|
+
private sessionId: string;
|
|
23
|
+
|
|
24
|
+
constructor(config: Partial<ObserverConfig> = {}) {
|
|
25
|
+
this.sessionId = config.sessionId || `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
26
|
+
|
|
27
|
+
this.config = {
|
|
28
|
+
enabled: false, // Opt-in by default
|
|
29
|
+
redactSecrets: true,
|
|
30
|
+
usePluresDB: false,
|
|
31
|
+
chunkSize: 4096,
|
|
32
|
+
maxEvents: 10000,
|
|
33
|
+
retentionDays: 30,
|
|
34
|
+
...config,
|
|
35
|
+
sessionId: this.sessionId,
|
|
36
|
+
shellType: config.shellType || detectShellType(),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Initialize the observer with storage and shell adapter
|
|
42
|
+
*/
|
|
43
|
+
async initialize(): Promise<void> {
|
|
44
|
+
if (!this.config.enabled) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Create event store
|
|
49
|
+
this.store = createEventStore(this.config);
|
|
50
|
+
|
|
51
|
+
// Create shell adapter
|
|
52
|
+
this.adapter = createShellAdapter(this.config.shellType);
|
|
53
|
+
|
|
54
|
+
// Initialize adapter
|
|
55
|
+
await this.adapter.initialize(this.config, this.store);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Start capturing events
|
|
60
|
+
*/
|
|
61
|
+
async start(): Promise<void> {
|
|
62
|
+
if (!this.config.enabled) {
|
|
63
|
+
throw new Error('Observer is not enabled. Set enabled: true in config.');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!this.adapter || !this.store) {
|
|
67
|
+
await this.initialize();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (this.adapter) {
|
|
71
|
+
await this.adapter.start();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Stop capturing events
|
|
77
|
+
*/
|
|
78
|
+
async stop(): Promise<void> {
|
|
79
|
+
if (this.adapter) {
|
|
80
|
+
await this.adapter.stop();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Check if observer is active
|
|
86
|
+
*/
|
|
87
|
+
isActive(): boolean {
|
|
88
|
+
return this.adapter?.isActive() || false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get the shell hook script for manual integration
|
|
93
|
+
*/
|
|
94
|
+
getHookScript(): string {
|
|
95
|
+
if (!this.adapter) {
|
|
96
|
+
this.adapter = createShellAdapter(this.config.shellType);
|
|
97
|
+
}
|
|
98
|
+
return this.adapter.getHookScript();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Programmatically capture a command execution
|
|
103
|
+
* Useful when shell hooks are not available
|
|
104
|
+
*/
|
|
105
|
+
async captureCommand(
|
|
106
|
+
command: string,
|
|
107
|
+
args: string[],
|
|
108
|
+
cwd: string,
|
|
109
|
+
env: Record<string, string>
|
|
110
|
+
): Promise<string> {
|
|
111
|
+
if (!this.config.enabled || !this.adapter || !this.store) {
|
|
112
|
+
throw new Error('Observer not initialized or not enabled');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Use adapter's programmatic capture
|
|
116
|
+
const { BashAdapter } = await import('./shell-adapters/bash.js');
|
|
117
|
+
const { ZshAdapter } = await import('./shell-adapters/zsh.js');
|
|
118
|
+
|
|
119
|
+
if (this.adapter instanceof BashAdapter) {
|
|
120
|
+
return await (this.adapter as any).captureCommand(command, args, cwd, env);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (this.adapter instanceof ZshAdapter) {
|
|
124
|
+
return await (this.adapter as any).captureCommand(command, args, cwd, env);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
throw new Error('Programmatic capture not supported for this shell adapter');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Programmatically capture command result
|
|
132
|
+
*/
|
|
133
|
+
async captureCommandResult(
|
|
134
|
+
commandId: string,
|
|
135
|
+
stdout: string,
|
|
136
|
+
stderr: string,
|
|
137
|
+
exitCode: number
|
|
138
|
+
): Promise<void> {
|
|
139
|
+
if (!this.config.enabled || !this.adapter || !this.store) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const { BashAdapter } = await import('./shell-adapters/bash.js');
|
|
144
|
+
const { ZshAdapter } = await import('./shell-adapters/zsh.js');
|
|
145
|
+
|
|
146
|
+
if (this.adapter instanceof BashAdapter) {
|
|
147
|
+
await (this.adapter as any).captureCommandResult(commandId, stdout, stderr, exitCode);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (this.adapter instanceof ZshAdapter) {
|
|
152
|
+
await (this.adapter as any).captureCommandResult(commandId, stdout, stderr, exitCode);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get events from storage
|
|
159
|
+
*/
|
|
160
|
+
async getEvents(
|
|
161
|
+
type?: EventType,
|
|
162
|
+
since?: number,
|
|
163
|
+
limit?: number
|
|
164
|
+
): Promise<TerminalObserverEvent[]> {
|
|
165
|
+
if (!this.store) {
|
|
166
|
+
await this.initialize();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!this.store) {
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return await this.store.getEvents(type, since, limit);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get events for a specific command
|
|
178
|
+
*/
|
|
179
|
+
async getEventsByCommand(commandId: string): Promise<TerminalObserverEvent[]> {
|
|
180
|
+
if (!this.store) {
|
|
181
|
+
await this.initialize();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!this.store) {
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return await this.store.getEventsByCommand(commandId);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get events for current session
|
|
193
|
+
*/
|
|
194
|
+
async getEventsBySession(limit?: number): Promise<TerminalObserverEvent[]> {
|
|
195
|
+
if (!this.store) {
|
|
196
|
+
await this.initialize();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (!this.store) {
|
|
200
|
+
return [];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return await this.store.getEventsBySession(this.sessionId, limit);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Get storage statistics
|
|
208
|
+
*/
|
|
209
|
+
async getStats() {
|
|
210
|
+
if (!this.store) {
|
|
211
|
+
await this.initialize();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!this.store) {
|
|
215
|
+
return {
|
|
216
|
+
totalEvents: 0,
|
|
217
|
+
eventsByType: {},
|
|
218
|
+
sessions: 0,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return await this.store.getStats();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Clear old events
|
|
227
|
+
*/
|
|
228
|
+
async clearEvents(days?: number): Promise<void> {
|
|
229
|
+
if (!this.store) {
|
|
230
|
+
await this.initialize();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!this.store) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const olderThan = days
|
|
238
|
+
? Date.now() - (days * 24 * 60 * 60 * 1000)
|
|
239
|
+
: undefined;
|
|
240
|
+
|
|
241
|
+
await this.store.clearEvents(olderThan);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Update configuration
|
|
246
|
+
*/
|
|
247
|
+
updateConfig(updates: Partial<ObserverConfig>): void {
|
|
248
|
+
this.config = { ...this.config, ...updates };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Get current configuration
|
|
253
|
+
*/
|
|
254
|
+
getConfig(): ObserverConfig {
|
|
255
|
+
return { ...this.config };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Create a terminal observer instance
|
|
261
|
+
*/
|
|
262
|
+
export function createObserver(config: Partial<ObserverConfig> = {}): TerminalObserver {
|
|
263
|
+
return new TerminalObserver(config);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Default observer configuration
|
|
268
|
+
*/
|
|
269
|
+
export const defaultObserverConfig: ObserverConfig = {
|
|
270
|
+
enabled: false,
|
|
271
|
+
redactSecrets: true,
|
|
272
|
+
usePluresDB: false,
|
|
273
|
+
chunkSize: 4096,
|
|
274
|
+
maxEvents: 10000,
|
|
275
|
+
retentionDays: 30,
|
|
276
|
+
shellType: detectShellType(),
|
|
277
|
+
};
|
|
278
|
+
|