@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,158 @@
|
|
|
1
|
+
// Secret redaction utilities for terminal observer
|
|
2
|
+
// Redacts sensitive information from environment variables and output
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Default patterns for secret detection
|
|
6
|
+
*/
|
|
7
|
+
const DEFAULT_SECRET_PATTERNS = [
|
|
8
|
+
/token/i,
|
|
9
|
+
/secret/i,
|
|
10
|
+
/password/i,
|
|
11
|
+
/api[_-]?key/i,
|
|
12
|
+
/auth[_-]?token/i,
|
|
13
|
+
/access[_-]?token/i,
|
|
14
|
+
/private[_-]?key/i,
|
|
15
|
+
/credential/i,
|
|
16
|
+
/bearer/i,
|
|
17
|
+
/session[_-]?id/i,
|
|
18
|
+
/cookie/i,
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Redact a value, replacing it with a placeholder
|
|
23
|
+
* @param value - Value to redact
|
|
24
|
+
* @param fullRedaction - If true, always use [REDACTED]; if false, show first/last 4 chars for long values
|
|
25
|
+
*/
|
|
26
|
+
export function redactValue(value: string, fullRedaction: boolean = false): string {
|
|
27
|
+
if (!value || value.length === 0) {
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// If full redaction requested or value is short, use [REDACTED]
|
|
32
|
+
if (fullRedaction || value.length <= 8) {
|
|
33
|
+
return '[REDACTED]';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// For longer values in partial redaction mode, show first 4 and last 4 chars
|
|
37
|
+
return `${value.substring(0, 4)}...${value.substring(value.length - 4)}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check if a key matches secret patterns
|
|
42
|
+
*/
|
|
43
|
+
export function isSecretKey(key: string, customPatterns: string[] = []): boolean {
|
|
44
|
+
const allPatterns = [...DEFAULT_SECRET_PATTERNS];
|
|
45
|
+
|
|
46
|
+
// Add custom patterns as regex
|
|
47
|
+
for (const pattern of customPatterns) {
|
|
48
|
+
try {
|
|
49
|
+
allPatterns.push(new RegExp(pattern, 'i'));
|
|
50
|
+
} catch (e) {
|
|
51
|
+
// Invalid regex pattern, skip
|
|
52
|
+
console.warn(`Invalid secret pattern: ${pattern}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return allPatterns.some(pattern => pattern.test(key));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Sanitize environment variables by redacting secrets
|
|
61
|
+
*/
|
|
62
|
+
export function sanitizeEnv(
|
|
63
|
+
env: Record<string, string>,
|
|
64
|
+
customPatterns: string[] = []
|
|
65
|
+
): Record<string, string> {
|
|
66
|
+
const sanitized: Record<string, string> = {};
|
|
67
|
+
|
|
68
|
+
for (const [key, value] of Object.entries(env)) {
|
|
69
|
+
if (isSecretKey(key, customPatterns)) {
|
|
70
|
+
// Use full redaction for environment variables
|
|
71
|
+
sanitized[key] = '[REDACTED]';
|
|
72
|
+
} else {
|
|
73
|
+
sanitized[key] = value;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return sanitized;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Redact secrets from a string (for stdout/stderr chunks)
|
|
82
|
+
*/
|
|
83
|
+
export function redactSecretsFromText(
|
|
84
|
+
text: string,
|
|
85
|
+
customPatterns: string[] = []
|
|
86
|
+
): string {
|
|
87
|
+
if (!text) {
|
|
88
|
+
return text;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Common patterns for secrets in output
|
|
92
|
+
const outputPatterns = [
|
|
93
|
+
/(token|secret|password|api[_-]?key|auth[_-]?token|access[_-]?token)\s*[:=]\s*([^\s\n]{8,})/gi,
|
|
94
|
+
/(Bearer|bearer)\s+([A-Za-z0-9\-._~+/]+=*)/g,
|
|
95
|
+
/(-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----[\s\S]*?-----END\s+(?:RSA\s+)?PRIVATE\s+KEY-----)/gi,
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
let redacted = text;
|
|
99
|
+
|
|
100
|
+
// Apply default patterns
|
|
101
|
+
for (const pattern of outputPatterns) {
|
|
102
|
+
redacted = redacted.replace(pattern, (match, key, value) => {
|
|
103
|
+
if (value) {
|
|
104
|
+
return `${key}=[REDACTED]`;
|
|
105
|
+
}
|
|
106
|
+
return '[REDACTED]';
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Apply custom patterns
|
|
111
|
+
for (const pattern of customPatterns) {
|
|
112
|
+
try {
|
|
113
|
+
const regex = new RegExp(pattern, 'gi');
|
|
114
|
+
redacted = redacted.replace(regex, '[REDACTED]');
|
|
115
|
+
} catch (e) {
|
|
116
|
+
// Invalid regex, skip
|
|
117
|
+
console.warn(`Invalid secret pattern: ${pattern}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return redacted;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Validate that redaction is working correctly
|
|
126
|
+
*/
|
|
127
|
+
export function validateRedaction(): boolean {
|
|
128
|
+
const testEnv = {
|
|
129
|
+
PATH: '/usr/bin:/usr/local/bin',
|
|
130
|
+
HOME: '/home/user',
|
|
131
|
+
API_KEY: 'sk-1234567890abcdef',
|
|
132
|
+
TOKEN: 'secret-token-value',
|
|
133
|
+
NORMAL_VAR: 'normal-value',
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const sanitized = sanitizeEnv(testEnv);
|
|
137
|
+
|
|
138
|
+
// Check that secrets are redacted
|
|
139
|
+
if (sanitized.API_KEY === testEnv.API_KEY) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (sanitized.TOKEN === testEnv.TOKEN) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Check that non-secrets are preserved
|
|
148
|
+
if (sanitized.PATH !== testEnv.PATH) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (sanitized.HOME !== testEnv.HOME) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
// Base shell adapter interface
|
|
2
|
+
// All shell adapters must implement this interface
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
TerminalObserverEvent,
|
|
6
|
+
ObserverConfig,
|
|
7
|
+
ShellType,
|
|
8
|
+
CommandStartEvent,
|
|
9
|
+
} from '../types';
|
|
10
|
+
import type { EventStore } from '../storage';
|
|
11
|
+
|
|
12
|
+
export interface ShellAdapter {
|
|
13
|
+
/**
|
|
14
|
+
* Get the shell type this adapter supports
|
|
15
|
+
*/
|
|
16
|
+
getShellType(): ShellType;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Initialize the adapter with config and event store
|
|
20
|
+
*/
|
|
21
|
+
initialize(config: ObserverConfig, store: EventStore): Promise<void>;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Start capturing events
|
|
25
|
+
*/
|
|
26
|
+
start(): Promise<void>;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Stop capturing events
|
|
30
|
+
*/
|
|
31
|
+
stop(): Promise<void>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check if the adapter is active
|
|
35
|
+
*/
|
|
36
|
+
isActive(): boolean;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get shell hook script (for integration into shell init files)
|
|
40
|
+
*/
|
|
41
|
+
getHookScript(): string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Base implementation with common functionality
|
|
46
|
+
*/
|
|
47
|
+
export abstract class BaseShellAdapter implements ShellAdapter {
|
|
48
|
+
protected config: ObserverConfig | null = null;
|
|
49
|
+
protected store: EventStore | null = null;
|
|
50
|
+
protected active = false;
|
|
51
|
+
protected currentCommandId: string | null = null;
|
|
52
|
+
protected commandStartTime: number = 0;
|
|
53
|
+
protected stdoutChunks: string[] = [];
|
|
54
|
+
protected stderrChunks: string[] = [];
|
|
55
|
+
protected stdoutChunkIndex = 0;
|
|
56
|
+
protected stderrChunkIndex = 0;
|
|
57
|
+
|
|
58
|
+
abstract getShellType(): ShellType;
|
|
59
|
+
abstract getHookScript(): string;
|
|
60
|
+
|
|
61
|
+
async initialize(config: ObserverConfig, store: EventStore): Promise<void> {
|
|
62
|
+
this.config = config;
|
|
63
|
+
this.store = store;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async start(): Promise<void> {
|
|
67
|
+
if (!this.config || !this.store) {
|
|
68
|
+
throw new Error('Adapter not initialized. Call initialize() first.');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!this.config.enabled) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this.active = true;
|
|
76
|
+
|
|
77
|
+
// Emit session start event
|
|
78
|
+
await this.emitSessionStart();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async stop(): Promise<void> {
|
|
82
|
+
if (this.active) {
|
|
83
|
+
// Emit session end event
|
|
84
|
+
await this.emitSessionEnd();
|
|
85
|
+
}
|
|
86
|
+
this.active = false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
isActive(): boolean {
|
|
90
|
+
return this.active;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Generate a unique event ID
|
|
95
|
+
*/
|
|
96
|
+
protected generateEventId(): string {
|
|
97
|
+
return `evt_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Generate a unique command ID
|
|
102
|
+
*/
|
|
103
|
+
protected generateCommandId(): string {
|
|
104
|
+
return `cmd_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Emit an event to the store
|
|
109
|
+
*/
|
|
110
|
+
protected async emitEvent(event: TerminalObserverEvent): Promise<void> {
|
|
111
|
+
if (!this.store) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
await this.store.saveEvent(event);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Capture command start
|
|
120
|
+
*/
|
|
121
|
+
protected async captureCommandStart(
|
|
122
|
+
command: string,
|
|
123
|
+
args: string[],
|
|
124
|
+
cwd: string,
|
|
125
|
+
env: Record<string, string>
|
|
126
|
+
): Promise<string> {
|
|
127
|
+
if (!this.config || !this.store) {
|
|
128
|
+
throw new Error('Adapter not initialized');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const commandId = this.generateCommandId();
|
|
132
|
+
this.currentCommandId = commandId;
|
|
133
|
+
this.commandStartTime = Date.now();
|
|
134
|
+
this.stdoutChunks = [];
|
|
135
|
+
this.stderrChunks = [];
|
|
136
|
+
this.stdoutChunkIndex = 0;
|
|
137
|
+
this.stderrChunkIndex = 0;
|
|
138
|
+
|
|
139
|
+
const { sanitizeEnv } = await import('../redaction.js');
|
|
140
|
+
const envSummary = this.config.redactSecrets
|
|
141
|
+
? sanitizeEnv(env, this.config.secretPatterns || [])
|
|
142
|
+
: env;
|
|
143
|
+
|
|
144
|
+
const event: CommandStartEvent = {
|
|
145
|
+
id: commandId,
|
|
146
|
+
type: 'command_start',
|
|
147
|
+
timestamp: this.commandStartTime,
|
|
148
|
+
sessionId: this.config.sessionId || 'unknown',
|
|
149
|
+
shellType: this.getShellType(),
|
|
150
|
+
paneId: this.config.paneId,
|
|
151
|
+
tabId: this.config.tabId,
|
|
152
|
+
command,
|
|
153
|
+
args,
|
|
154
|
+
cwd,
|
|
155
|
+
envSummary,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
await this.emitEvent(event);
|
|
159
|
+
return commandId;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Capture command end
|
|
164
|
+
*/
|
|
165
|
+
protected async captureCommandEnd(commandId: string): Promise<void> {
|
|
166
|
+
if (!this.config || !this.store) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const duration = Date.now() - this.commandStartTime;
|
|
171
|
+
|
|
172
|
+
await this.emitEvent({
|
|
173
|
+
id: this.generateEventId(),
|
|
174
|
+
type: 'command_end',
|
|
175
|
+
timestamp: Date.now(),
|
|
176
|
+
sessionId: this.config.sessionId || 'unknown',
|
|
177
|
+
shellType: this.getShellType(),
|
|
178
|
+
paneId: this.config.paneId,
|
|
179
|
+
tabId: this.config.tabId,
|
|
180
|
+
commandId,
|
|
181
|
+
duration,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Capture stdout chunk
|
|
187
|
+
*/
|
|
188
|
+
protected async captureStdoutChunk(
|
|
189
|
+
commandId: string,
|
|
190
|
+
chunk: string
|
|
191
|
+
): Promise<void> {
|
|
192
|
+
if (!this.config || !this.store) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const { redactSecretsFromText } = await import('../redaction.js');
|
|
197
|
+
const processedChunk = this.config.redactSecrets
|
|
198
|
+
? redactSecretsFromText(chunk, this.config.secretPatterns || [])
|
|
199
|
+
: chunk;
|
|
200
|
+
|
|
201
|
+
await this.emitEvent({
|
|
202
|
+
id: this.generateEventId(),
|
|
203
|
+
type: 'stdout_chunk',
|
|
204
|
+
timestamp: Date.now(),
|
|
205
|
+
sessionId: this.config.sessionId || 'unknown',
|
|
206
|
+
shellType: this.getShellType(),
|
|
207
|
+
paneId: this.config.paneId,
|
|
208
|
+
tabId: this.config.tabId,
|
|
209
|
+
commandId,
|
|
210
|
+
chunk: processedChunk,
|
|
211
|
+
chunkIndex: this.stdoutChunkIndex++,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Capture stderr chunk
|
|
217
|
+
*/
|
|
218
|
+
protected async captureStderrChunk(
|
|
219
|
+
commandId: string,
|
|
220
|
+
chunk: string
|
|
221
|
+
): Promise<void> {
|
|
222
|
+
if (!this.config || !this.store) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const { redactSecretsFromText } = await import('../redaction.js');
|
|
227
|
+
const processedChunk = this.config.redactSecrets
|
|
228
|
+
? redactSecretsFromText(chunk, this.config.secretPatterns || [])
|
|
229
|
+
: chunk;
|
|
230
|
+
|
|
231
|
+
await this.emitEvent({
|
|
232
|
+
id: this.generateEventId(),
|
|
233
|
+
type: 'stderr_chunk',
|
|
234
|
+
timestamp: Date.now(),
|
|
235
|
+
sessionId: this.config.sessionId || 'unknown',
|
|
236
|
+
shellType: this.getShellType(),
|
|
237
|
+
paneId: this.config.paneId,
|
|
238
|
+
tabId: this.config.tabId,
|
|
239
|
+
commandId,
|
|
240
|
+
chunk: processedChunk,
|
|
241
|
+
chunkIndex: this.stderrChunkIndex++,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Capture exit status
|
|
247
|
+
*/
|
|
248
|
+
protected async captureExitStatus(
|
|
249
|
+
commandId: string,
|
|
250
|
+
exitCode: number
|
|
251
|
+
): Promise<void> {
|
|
252
|
+
if (!this.config || !this.store) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
await this.emitEvent({
|
|
257
|
+
id: this.generateEventId(),
|
|
258
|
+
type: 'exit_status',
|
|
259
|
+
timestamp: Date.now(),
|
|
260
|
+
sessionId: this.config.sessionId || 'unknown',
|
|
261
|
+
shellType: this.getShellType(),
|
|
262
|
+
paneId: this.config.paneId,
|
|
263
|
+
tabId: this.config.tabId,
|
|
264
|
+
commandId,
|
|
265
|
+
exitCode,
|
|
266
|
+
success: exitCode === 0,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Emit session start event
|
|
272
|
+
*/
|
|
273
|
+
protected async emitSessionStart(): Promise<void> {
|
|
274
|
+
if (!this.config || !this.store) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const { sanitizeEnv } = await import('../redaction.js');
|
|
279
|
+
const envSummary = this.config.redactSecrets
|
|
280
|
+
? sanitizeEnv(process.env as Record<string, string>, this.config.secretPatterns || [])
|
|
281
|
+
: (process.env as Record<string, string>);
|
|
282
|
+
|
|
283
|
+
await this.emitEvent({
|
|
284
|
+
id: this.generateEventId(),
|
|
285
|
+
type: 'session_start',
|
|
286
|
+
timestamp: Date.now(),
|
|
287
|
+
sessionId: this.config.sessionId || 'unknown',
|
|
288
|
+
shellType: this.getShellType(),
|
|
289
|
+
paneId: this.config.paneId,
|
|
290
|
+
tabId: this.config.tabId,
|
|
291
|
+
cwd: process.cwd(),
|
|
292
|
+
envSummary,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Emit session end event
|
|
298
|
+
*/
|
|
299
|
+
protected async emitSessionEnd(): Promise<void> {
|
|
300
|
+
if (!this.config || !this.store) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const sessionStart = this.config.sessionId
|
|
305
|
+
? await this.store.getEventsBySession(this.config.sessionId, 1)
|
|
306
|
+
: [];
|
|
307
|
+
|
|
308
|
+
const startEvent = sessionStart.find((e: TerminalObserverEvent) => e.type === 'session_start');
|
|
309
|
+
const duration = startEvent
|
|
310
|
+
? Date.now() - startEvent.timestamp
|
|
311
|
+
: 0;
|
|
312
|
+
|
|
313
|
+
await this.emitEvent({
|
|
314
|
+
id: this.generateEventId(),
|
|
315
|
+
type: 'session_end',
|
|
316
|
+
timestamp: Date.now(),
|
|
317
|
+
sessionId: this.config.sessionId || 'unknown',
|
|
318
|
+
shellType: this.getShellType(),
|
|
319
|
+
paneId: this.config.paneId,
|
|
320
|
+
tabId: this.config.tabId,
|
|
321
|
+
duration,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
// Bash shell adapter for terminal observer
|
|
3
|
+
// Provides hooks for capturing bash 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 BashAdapter extends BaseShellAdapter {
|
|
12
|
+
private hookScriptPath: string;
|
|
13
|
+
|
|
14
|
+
constructor() {
|
|
15
|
+
super();
|
|
16
|
+
this.hookScriptPath = join(homedir(), '.runebook', 'bash-hook.sh');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getShellType(): ShellType {
|
|
20
|
+
return 'bash';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
getHookScript(): string {
|
|
24
|
+
// @ts-ignore - Shell script content, not TypeScript
|
|
25
|
+
return (
|
|
26
|
+
'# RuneBook Terminal Observer Hook for Bash\n' +
|
|
27
|
+
'# Add this to your ~/.bashrc or ~/.bash_profile\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\n' +
|
|
48
|
+
' trap \'__runebook_capture_start "$BASH_COMMAND"\' DEBUG\n' +
|
|
49
|
+
' trap \'__runebook_capture_end\' ERR\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
|
+
* This can be called from Node.js to capture command execution
|
|
69
|
+
*/
|
|
70
|
+
async captureCommand(
|
|
71
|
+
command: string,
|
|
72
|
+
args: string[],
|
|
73
|
+
cwd: string,
|
|
74
|
+
env: Record<string, string>
|
|
75
|
+
): Promise<string> {
|
|
76
|
+
return await this.captureCommandStart(command, args, cwd, env);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Programmatic capture of command result
|
|
81
|
+
*/
|
|
82
|
+
async captureCommandResult(
|
|
83
|
+
commandId: string,
|
|
84
|
+
stdout: string,
|
|
85
|
+
stderr: string,
|
|
86
|
+
exitCode: number
|
|
87
|
+
): Promise<void> {
|
|
88
|
+
// Split stdout/stderr into chunks if configured
|
|
89
|
+
const chunkSize = this.config?.chunkSize || 4096;
|
|
90
|
+
|
|
91
|
+
// Capture stdout chunks
|
|
92
|
+
for (let i = 0; i < stdout.length; i += chunkSize) {
|
|
93
|
+
const chunk = stdout.substring(i, i + chunkSize);
|
|
94
|
+
await this.captureStdoutChunk(commandId, chunk);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Capture stderr chunks
|
|
98
|
+
for (let i = 0; i < stderr.length; i += chunkSize) {
|
|
99
|
+
const chunk = stderr.substring(i, i + chunkSize);
|
|
100
|
+
await this.captureStderrChunk(commandId, chunk);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Capture exit status
|
|
104
|
+
await this.captureExitStatus(commandId, exitCode);
|
|
105
|
+
|
|
106
|
+
// Capture command end
|
|
107
|
+
await this.captureCommandEnd(commandId);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Shell adapter factory and utilities
|
|
2
|
+
|
|
3
|
+
import { BashAdapter } from './bash';
|
|
4
|
+
import { ZshAdapter } from './zsh';
|
|
5
|
+
import type { ShellAdapter } from './base';
|
|
6
|
+
import type { ShellType, ObserverConfig } from '../types';
|
|
7
|
+
import type { EventStore } from '../storage';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Detect the current shell type
|
|
11
|
+
*/
|
|
12
|
+
export function detectShellType(): ShellType {
|
|
13
|
+
const shell = process.env.SHELL || '';
|
|
14
|
+
|
|
15
|
+
if (shell.includes('bash')) {
|
|
16
|
+
return 'bash';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (shell.includes('zsh')) {
|
|
20
|
+
return 'zsh';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (shell.includes('nu')) {
|
|
24
|
+
return 'nushell';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return 'unknown';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Create a shell adapter for the specified shell type
|
|
32
|
+
*/
|
|
33
|
+
export function createShellAdapter(shellType?: ShellType): ShellAdapter {
|
|
34
|
+
const type = shellType || detectShellType();
|
|
35
|
+
|
|
36
|
+
switch (type) {
|
|
37
|
+
case 'bash':
|
|
38
|
+
return new BashAdapter();
|
|
39
|
+
case 'zsh':
|
|
40
|
+
return new ZshAdapter();
|
|
41
|
+
case 'nushell':
|
|
42
|
+
// TODO: Implement nushell adapter
|
|
43
|
+
throw new Error('Nushell adapter not yet implemented');
|
|
44
|
+
default:
|
|
45
|
+
throw new Error(`Unsupported shell type: ${type}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get all available shell adapters
|
|
51
|
+
*/
|
|
52
|
+
export function getAvailableAdapters(): ShellAdapter[] {
|
|
53
|
+
return [
|
|
54
|
+
new BashAdapter(),
|
|
55
|
+
new ZshAdapter(),
|
|
56
|
+
// Nushell adapter will be added later
|
|
57
|
+
];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { BashAdapter, ZshAdapter };
|
|
61
|
+
export type { ShellAdapter } from './base';
|
|
62
|
+
|