@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,215 @@
|
|
|
1
|
+
// Layer 2: Local Search Analyzer
|
|
2
|
+
// Uses ripgrep to search repository and config files for relevant patterns
|
|
3
|
+
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import { existsSync, readFileSync } from 'fs';
|
|
6
|
+
import { join, dirname } from 'path';
|
|
7
|
+
import type { Analyzer, AnalysisContext, AnalysisSuggestion } from '../analysis-pipeline';
|
|
8
|
+
import type { EventStore } from '../../core/types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Local search analyzer using ripgrep
|
|
12
|
+
*/
|
|
13
|
+
export class LocalSearchAnalyzer implements Analyzer {
|
|
14
|
+
name = 'local-search';
|
|
15
|
+
layer = 2;
|
|
16
|
+
|
|
17
|
+
async analyze(context: AnalysisContext, _store: EventStore): Promise<AnalysisSuggestion[]> {
|
|
18
|
+
const suggestions: AnalysisSuggestion[] = [];
|
|
19
|
+
|
|
20
|
+
// Find repository root (look for .git, flake.nix, etc.)
|
|
21
|
+
const repoRoot = this.findRepoRoot(context.cwd);
|
|
22
|
+
if (!repoRoot) {
|
|
23
|
+
return suggestions; // Not in a repository
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Search for relevant patterns based on error
|
|
27
|
+
const stderr = context.stderr.toLowerCase();
|
|
28
|
+
|
|
29
|
+
// Search for missing attributes in Nix files
|
|
30
|
+
if (stderr.includes('attribute') && (stderr.includes('missing') || stderr.includes('undefined'))) {
|
|
31
|
+
const attrMatch = context.stderr.match(/attribute ['"]([^'"]+)['"]/i);
|
|
32
|
+
if (attrMatch) {
|
|
33
|
+
const attrName = attrMatch[1];
|
|
34
|
+
const results = this.searchInRepo(repoRoot, attrName, ['*.nix', 'flake.nix']);
|
|
35
|
+
|
|
36
|
+
if (results.length > 0) {
|
|
37
|
+
suggestions.push({
|
|
38
|
+
id: `suggestion_${Date.now()}_local_search_attr`,
|
|
39
|
+
type: 'tip',
|
|
40
|
+
priority: 'medium',
|
|
41
|
+
title: 'Found Attribute References',
|
|
42
|
+
description: `Found references to "${attrName}" in your repository. Check these files for context.`,
|
|
43
|
+
confidence: 0.7,
|
|
44
|
+
actionableSnippet: `# Found in files:\n${results.slice(0, 5).map(r => `# - ${r}`).join('\n')}`,
|
|
45
|
+
provenance: {
|
|
46
|
+
analyzer: this.name,
|
|
47
|
+
layer: this.layer,
|
|
48
|
+
timestamp: Date.now(),
|
|
49
|
+
},
|
|
50
|
+
timestamp: Date.now(),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Search for template paths in flake-parts
|
|
57
|
+
if (stderr.includes('template') && (stderr.includes('path') || stderr.includes('not found'))) {
|
|
58
|
+
const results = this.searchInRepo(repoRoot, 'template', ['*.nix', 'flake.nix']);
|
|
59
|
+
|
|
60
|
+
if (results.length > 0) {
|
|
61
|
+
suggestions.push({
|
|
62
|
+
id: `suggestion_${Date.now()}_local_search_template`,
|
|
63
|
+
type: 'tip',
|
|
64
|
+
priority: 'medium',
|
|
65
|
+
title: 'Found Template References',
|
|
66
|
+
description: 'Found template references in your Nix files. Check these for path configuration.',
|
|
67
|
+
confidence: 0.65,
|
|
68
|
+
actionableSnippet: `# Template references found in:\n${results.slice(0, 5).map(r => `# - ${r}`).join('\n')}`,
|
|
69
|
+
provenance: {
|
|
70
|
+
analyzer: this.name,
|
|
71
|
+
layer: this.layer,
|
|
72
|
+
timestamp: Date.now(),
|
|
73
|
+
},
|
|
74
|
+
timestamp: Date.now(),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Search for environment variable references
|
|
80
|
+
if (context.stderr.includes('TOKEN') || context.stderr.includes('token')) {
|
|
81
|
+
const tokenMatch = context.stderr.match(/([A-Z_]+TOKEN)/);
|
|
82
|
+
if (tokenMatch) {
|
|
83
|
+
const tokenName = tokenMatch[1];
|
|
84
|
+
const results = this.searchInRepo(repoRoot, tokenName, ['*.sh', '*.env', '.env*', '*.nix']);
|
|
85
|
+
|
|
86
|
+
if (results.length > 0) {
|
|
87
|
+
suggestions.push({
|
|
88
|
+
id: `suggestion_${Date.now()}_local_search_token`,
|
|
89
|
+
type: 'tip',
|
|
90
|
+
priority: 'medium',
|
|
91
|
+
title: 'Found Token References',
|
|
92
|
+
description: `Found references to ${tokenName} in your repository. Check these files for configuration.`,
|
|
93
|
+
confidence: 0.7,
|
|
94
|
+
actionableSnippet: `# ${tokenName} references found in:\n${results.slice(0, 5).map(r => `# - ${r}`).join('\n')}\n\n# Check if ${tokenName} is set:\necho $${tokenName}`,
|
|
95
|
+
provenance: {
|
|
96
|
+
analyzer: this.name,
|
|
97
|
+
layer: this.layer,
|
|
98
|
+
timestamp: Date.now(),
|
|
99
|
+
},
|
|
100
|
+
timestamp: Date.now(),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Check for flake.nix and suggest checking it
|
|
107
|
+
const flakePath = join(repoRoot, 'flake.nix');
|
|
108
|
+
if (existsSync(flakePath) && stderr.includes('nix')) {
|
|
109
|
+
const flakeContent = readFileSync(flakePath, 'utf-8');
|
|
110
|
+
|
|
111
|
+
// Check for common issues
|
|
112
|
+
if (stderr.includes('missing') && !flakeContent.includes('inputs')) {
|
|
113
|
+
suggestions.push({
|
|
114
|
+
id: `suggestion_${Date.now()}_local_search_flake`,
|
|
115
|
+
type: 'tip',
|
|
116
|
+
priority: 'medium',
|
|
117
|
+
title: 'Check flake.nix Configuration',
|
|
118
|
+
description: 'Your flake.nix may be missing inputs or outputs. Review the file structure.',
|
|
119
|
+
confidence: 0.6,
|
|
120
|
+
actionableSnippet: `# Review your flake.nix:
|
|
121
|
+
cat ${flakePath}
|
|
122
|
+
|
|
123
|
+
# Common structure:
|
|
124
|
+
# {
|
|
125
|
+
# inputs = { ... };
|
|
126
|
+
# outputs = { ... };
|
|
127
|
+
# }`,
|
|
128
|
+
provenance: {
|
|
129
|
+
analyzer: this.name,
|
|
130
|
+
layer: this.layer,
|
|
131
|
+
timestamp: Date.now(),
|
|
132
|
+
},
|
|
133
|
+
timestamp: Date.now(),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return suggestions;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Find repository root by looking for markers
|
|
143
|
+
*/
|
|
144
|
+
private findRepoRoot(cwd: string): string | null {
|
|
145
|
+
let current = cwd;
|
|
146
|
+
const maxDepth = 10;
|
|
147
|
+
let depth = 0;
|
|
148
|
+
|
|
149
|
+
while (depth < maxDepth) {
|
|
150
|
+
// Check for common repository markers
|
|
151
|
+
if (existsSync(join(current, '.git')) ||
|
|
152
|
+
existsSync(join(current, 'flake.nix')) ||
|
|
153
|
+
existsSync(join(current, '.gitignore'))) {
|
|
154
|
+
return current;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const parent = dirname(current);
|
|
158
|
+
if (parent === current) {
|
|
159
|
+
break; // Reached filesystem root
|
|
160
|
+
}
|
|
161
|
+
current = parent;
|
|
162
|
+
depth++;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Search repository using ripgrep (or fallback to grep)
|
|
170
|
+
*/
|
|
171
|
+
private searchInRepo(repoRoot: string, pattern: string, filePatterns: string[]): string[] {
|
|
172
|
+
try {
|
|
173
|
+
// Try ripgrep first
|
|
174
|
+
const rgPattern = filePatterns.map(p => `-g "${p}"`).join(' ');
|
|
175
|
+
const command = `rg -l "${pattern}" ${rgPattern} "${repoRoot}" 2>/dev/null || true`;
|
|
176
|
+
const output = execSync(command, {
|
|
177
|
+
cwd: repoRoot,
|
|
178
|
+
encoding: 'utf-8',
|
|
179
|
+
maxBuffer: 1024 * 1024, // 1MB
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
return output
|
|
183
|
+
.split('\n')
|
|
184
|
+
.filter(line => line.trim().length > 0)
|
|
185
|
+
.map(line => line.replace(repoRoot + '/', ''));
|
|
186
|
+
} catch (error) {
|
|
187
|
+
// Fallback to grep if ripgrep not available
|
|
188
|
+
try {
|
|
189
|
+
const grepPattern = filePatterns.map(p => `--include="${p}"`).join(' ');
|
|
190
|
+
const command = `grep -r -l "${pattern}" ${grepPattern} "${repoRoot}" 2>/dev/null || true`;
|
|
191
|
+
const output = execSync(command, {
|
|
192
|
+
cwd: repoRoot,
|
|
193
|
+
encoding: 'utf-8',
|
|
194
|
+
maxBuffer: 1024 * 1024,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
return output
|
|
198
|
+
.split('\n')
|
|
199
|
+
.filter(line => line.trim().length > 0)
|
|
200
|
+
.map(line => line.replace(repoRoot + '/', ''));
|
|
201
|
+
} catch (grepError) {
|
|
202
|
+
// Both failed, return empty
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Create local search analyzer
|
|
211
|
+
*/
|
|
212
|
+
export function createLocalSearchAnalyzer(): Analyzer {
|
|
213
|
+
return new LocalSearchAnalyzer();
|
|
214
|
+
}
|
|
215
|
+
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Event capture system for Ambient Agent Mode
|
|
2
|
+
// Captures terminal commands, outputs, and context
|
|
3
|
+
|
|
4
|
+
import type { TerminalEvent, EventContext } from '../types/agent';
|
|
5
|
+
|
|
6
|
+
let sessionId: string | null = null;
|
|
7
|
+
let currentContext: EventContext | null = null;
|
|
8
|
+
let captureEnabled = false;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Initialize event capture with a session ID
|
|
12
|
+
*/
|
|
13
|
+
export function initCapture(sessionIdParam: string, context?: Partial<EventContext>): void {
|
|
14
|
+
sessionId = sessionIdParam;
|
|
15
|
+
currentContext = {
|
|
16
|
+
sessionId: sessionIdParam,
|
|
17
|
+
workingDirectory: context?.workingDirectory || '',
|
|
18
|
+
environment: context?.environment || {},
|
|
19
|
+
...context,
|
|
20
|
+
};
|
|
21
|
+
captureEnabled = true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Stop event capture
|
|
26
|
+
*/
|
|
27
|
+
export function stopCapture(): void {
|
|
28
|
+
captureEnabled = false;
|
|
29
|
+
sessionId = null;
|
|
30
|
+
currentContext = null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check if capture is enabled
|
|
35
|
+
*/
|
|
36
|
+
export function isCaptureEnabled(): boolean {
|
|
37
|
+
return captureEnabled && sessionId !== null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Update the current context
|
|
42
|
+
*/
|
|
43
|
+
export function updateContext(updates: Partial<EventContext>): void {
|
|
44
|
+
if (currentContext) {
|
|
45
|
+
currentContext = { ...currentContext, ...updates };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Capture a terminal command execution
|
|
51
|
+
*/
|
|
52
|
+
export function captureCommand(
|
|
53
|
+
command: string,
|
|
54
|
+
args: string[],
|
|
55
|
+
env: Record<string, string>,
|
|
56
|
+
cwd: string,
|
|
57
|
+
startTime: number
|
|
58
|
+
): TerminalEvent {
|
|
59
|
+
if (!isCaptureEnabled()) {
|
|
60
|
+
throw new Error('Capture not initialized. Call initCapture() first.');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
id: generateEventId(),
|
|
65
|
+
timestamp: startTime,
|
|
66
|
+
command,
|
|
67
|
+
args,
|
|
68
|
+
env,
|
|
69
|
+
cwd,
|
|
70
|
+
success: false, // Will be updated when result is captured
|
|
71
|
+
context: currentContext ? { ...currentContext } : undefined,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Capture command result
|
|
77
|
+
*/
|
|
78
|
+
export function captureResult(
|
|
79
|
+
event: TerminalEvent,
|
|
80
|
+
stdout: string,
|
|
81
|
+
stderr: string,
|
|
82
|
+
exitCode: number,
|
|
83
|
+
endTime: number
|
|
84
|
+
): TerminalEvent {
|
|
85
|
+
const duration = endTime - event.timestamp;
|
|
86
|
+
const success = exitCode === 0;
|
|
87
|
+
|
|
88
|
+
// Update context with previous command
|
|
89
|
+
if (currentContext) {
|
|
90
|
+
currentContext.previousCommand = event.command;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
...event,
|
|
95
|
+
stdout,
|
|
96
|
+
stderr,
|
|
97
|
+
exitCode,
|
|
98
|
+
duration,
|
|
99
|
+
success,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Generate a unique event ID
|
|
105
|
+
*/
|
|
106
|
+
function generateEventId(): string {
|
|
107
|
+
return `evt_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get current session ID
|
|
112
|
+
*/
|
|
113
|
+
export function getSessionId(): string | null {
|
|
114
|
+
return sessionId;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get current context
|
|
119
|
+
*/
|
|
120
|
+
export function getContext(): EventContext | null {
|
|
121
|
+
return currentContext;
|
|
122
|
+
}
|
|
123
|
+
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// Main Ambient Agent Mode coordinator
|
|
2
|
+
// Orchestrates event capture, storage, analysis, and suggestions
|
|
3
|
+
|
|
4
|
+
import { initCapture, stopCapture, captureCommand, captureResult, updateContext } from './capture';
|
|
5
|
+
import { createStorage, type EventStorage } from './memory';
|
|
6
|
+
import { createAnalyzer, type Analyzer } from './analysis';
|
|
7
|
+
import { MemorySuggestionStore, formatSuggestionsForCLI, type SuggestionStore } from './suggestions';
|
|
8
|
+
import { updateAgentStatus, getAgentStatus } from './status';
|
|
9
|
+
import type { TerminalEvent, AgentConfig, Suggestion } from '../types/agent';
|
|
10
|
+
|
|
11
|
+
export class AmbientAgent {
|
|
12
|
+
private storage: EventStorage;
|
|
13
|
+
private analyzer: Analyzer;
|
|
14
|
+
private suggestionStore: SuggestionStore;
|
|
15
|
+
private config: AgentConfig;
|
|
16
|
+
private sessionId: string;
|
|
17
|
+
|
|
18
|
+
constructor(config: AgentConfig) {
|
|
19
|
+
this.config = config;
|
|
20
|
+
this.sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
21
|
+
this.storage = createStorage(config);
|
|
22
|
+
this.analyzer = createAnalyzer();
|
|
23
|
+
|
|
24
|
+
// Initialize with MemorySuggestionStore (browser-safe)
|
|
25
|
+
this.suggestionStore = new MemorySuggestionStore();
|
|
26
|
+
|
|
27
|
+
// In Node.js environment, upgrade to file-based store
|
|
28
|
+
if (typeof process !== 'undefined' && process.versions?.node) {
|
|
29
|
+
import('./node-suggestions').then(({ FileSuggestionStore }) => {
|
|
30
|
+
this.suggestionStore = new FileSuggestionStore();
|
|
31
|
+
this.suggestionStore.load().catch(err =>
|
|
32
|
+
console.error('Failed to load suggestions:', err)
|
|
33
|
+
);
|
|
34
|
+
}).catch(() => {
|
|
35
|
+
// Fallback already initialized with MemorySuggestionStore
|
|
36
|
+
console.warn('FileSuggestionStore not available, using MemorySuggestionStore');
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (config.enabled && config.captureEvents) {
|
|
41
|
+
initCapture(this.sessionId, {
|
|
42
|
+
workingDirectory: process.cwd?.() || '',
|
|
43
|
+
environment: process.env as Record<string, string>,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Initialize status
|
|
48
|
+
updateAgentStatus({ status: 'idle' });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Capture and analyze a terminal command execution
|
|
53
|
+
*/
|
|
54
|
+
async captureAndAnalyze(
|
|
55
|
+
command: string,
|
|
56
|
+
args: string[],
|
|
57
|
+
env: Record<string, string>,
|
|
58
|
+
cwd: string,
|
|
59
|
+
startTime: number
|
|
60
|
+
): Promise<TerminalEvent> {
|
|
61
|
+
if (!this.config.enabled || !this.config.captureEvents) {
|
|
62
|
+
throw new Error('Agent not enabled or capture disabled');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const event = captureCommand(command, args, env, cwd, startTime);
|
|
66
|
+
return event;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Record command result and generate suggestions
|
|
71
|
+
*/
|
|
72
|
+
async recordResult(
|
|
73
|
+
event: TerminalEvent,
|
|
74
|
+
stdout: string,
|
|
75
|
+
stderr: string,
|
|
76
|
+
exitCode: number,
|
|
77
|
+
endTime: number
|
|
78
|
+
): Promise<Suggestion[]> {
|
|
79
|
+
if (!this.config.enabled || !this.config.captureEvents) {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Update status to analyzing
|
|
84
|
+
updateAgentStatus({
|
|
85
|
+
status: 'analyzing',
|
|
86
|
+
lastCommand: event.command,
|
|
87
|
+
lastCommandTimestamp: event.timestamp,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const completedEvent = captureResult(event, stdout, stderr, exitCode, endTime);
|
|
91
|
+
|
|
92
|
+
// Save to storage
|
|
93
|
+
await this.storage.saveEvent(completedEvent);
|
|
94
|
+
|
|
95
|
+
// Update context
|
|
96
|
+
updateContext({
|
|
97
|
+
previousCommand: completedEvent.command,
|
|
98
|
+
workingDirectory: completedEvent.cwd,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Analyze and generate suggestions
|
|
102
|
+
let suggestions: Suggestion[] = [];
|
|
103
|
+
if (this.config.analyzePatterns && this.config.suggestImprovements) {
|
|
104
|
+
suggestions = await this.analyzer.analyzeEvent(completedEvent, this.storage);
|
|
105
|
+
|
|
106
|
+
for (const suggestion of suggestions) {
|
|
107
|
+
this.suggestionStore.add(suggestion);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Update status based on suggestions
|
|
112
|
+
const allSuggestions = this.suggestionStore.suggestions;
|
|
113
|
+
const highPriorityCount = allSuggestions.filter(s => s.priority === 'high').length;
|
|
114
|
+
const status = highPriorityCount > 0 ? 'issues_found' : 'idle';
|
|
115
|
+
|
|
116
|
+
updateAgentStatus({
|
|
117
|
+
status,
|
|
118
|
+
suggestionCount: allSuggestions.length,
|
|
119
|
+
highPriorityCount,
|
|
120
|
+
lastCommand: event.command,
|
|
121
|
+
lastCommandTimestamp: event.timestamp,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return suggestions;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get current suggestions
|
|
129
|
+
*/
|
|
130
|
+
getSuggestions(priority?: 'low' | 'medium' | 'high'): Suggestion[] {
|
|
131
|
+
if (priority) {
|
|
132
|
+
return this.suggestionStore.getByPriority(priority);
|
|
133
|
+
}
|
|
134
|
+
return this.suggestionStore.suggestions;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get suggestions formatted for CLI
|
|
139
|
+
*/
|
|
140
|
+
getSuggestionsCLI(priority?: 'low' | 'medium' | 'high'): string {
|
|
141
|
+
const suggestions = this.getSuggestions(priority);
|
|
142
|
+
return formatSuggestionsForCLI(suggestions);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get suggestions for the last command
|
|
147
|
+
*/
|
|
148
|
+
getSuggestionsForLastCommand(): Suggestion[] {
|
|
149
|
+
const status = getAgentStatus();
|
|
150
|
+
if (!status.lastCommand) {
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
return this.suggestionStore.getForCommand(status.lastCommand);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get top suggestion
|
|
158
|
+
*/
|
|
159
|
+
getTopSuggestion(limit: number = 1): Suggestion[] {
|
|
160
|
+
return this.suggestionStore.getTop(limit);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Analyze all patterns and generate suggestions
|
|
165
|
+
*/
|
|
166
|
+
async analyzeAllPatterns(): Promise<Suggestion[]> {
|
|
167
|
+
if (!this.config.enabled || !this.config.analyzePatterns) {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const suggestions = await this.analyzer.analyzePatterns(this.storage);
|
|
172
|
+
|
|
173
|
+
for (const suggestion of suggestions) {
|
|
174
|
+
this.suggestionStore.add(suggestion);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return suggestions;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Get storage statistics
|
|
182
|
+
*/
|
|
183
|
+
async getStats() {
|
|
184
|
+
return await this.storage.getStats();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Get recent events
|
|
189
|
+
*/
|
|
190
|
+
async getRecentEvents(limit: number = 10): Promise<TerminalEvent[]> {
|
|
191
|
+
return await this.storage.getEvents(limit);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Clear old events
|
|
196
|
+
*/
|
|
197
|
+
async clearOldEvents(days: number = 30): Promise<void> {
|
|
198
|
+
const cutoff = Date.now() - (days * 24 * 60 * 60 * 1000);
|
|
199
|
+
await this.storage.clearEvents(cutoff);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Stop the agent
|
|
204
|
+
*/
|
|
205
|
+
stop(): void {
|
|
206
|
+
stopCapture();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Update configuration
|
|
211
|
+
*/
|
|
212
|
+
updateConfig(updates: Partial<AgentConfig>): void {
|
|
213
|
+
this.config = { ...this.config, ...updates };
|
|
214
|
+
|
|
215
|
+
if (!this.config.enabled) {
|
|
216
|
+
stopCapture();
|
|
217
|
+
} else if (this.config.captureEvents) {
|
|
218
|
+
initCapture(this.sessionId, {
|
|
219
|
+
workingDirectory: process.cwd?.() || '',
|
|
220
|
+
environment: process.env as Record<string, string>,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Create an Ambient Agent instance
|
|
228
|
+
*/
|
|
229
|
+
export function createAgent(config: AgentConfig): AmbientAgent {
|
|
230
|
+
return new AmbientAgent(config);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Default agent configuration
|
|
235
|
+
*/
|
|
236
|
+
export const defaultAgentConfig: AgentConfig = {
|
|
237
|
+
enabled: false, // Opt-in by default
|
|
238
|
+
captureEvents: true,
|
|
239
|
+
analyzePatterns: true,
|
|
240
|
+
suggestImprovements: true,
|
|
241
|
+
maxEvents: 10000,
|
|
242
|
+
retentionDays: 30,
|
|
243
|
+
};
|
|
244
|
+
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Integration layer for Ambient Agent Mode
|
|
2
|
+
// Connects agent to terminal execution
|
|
3
|
+
|
|
4
|
+
import { createAgent, defaultAgentConfig, type AmbientAgent } from './index';
|
|
5
|
+
import type { AgentConfig, TerminalEvent } from '../types/agent';
|
|
6
|
+
|
|
7
|
+
let agentInstance: AmbientAgent | null = null;
|
|
8
|
+
let agentConfig: AgentConfig = { ...defaultAgentConfig };
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Initialize the agent (opt-in)
|
|
12
|
+
*/
|
|
13
|
+
export function initAgent(config?: Partial<AgentConfig>): AmbientAgent {
|
|
14
|
+
agentConfig = { ...defaultAgentConfig, ...config };
|
|
15
|
+
agentInstance = createAgent(agentConfig);
|
|
16
|
+
return agentInstance;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get the current agent instance
|
|
21
|
+
*/
|
|
22
|
+
export function getAgent(): AmbientAgent | null {
|
|
23
|
+
return agentInstance;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Check if agent is enabled
|
|
28
|
+
*/
|
|
29
|
+
export function isAgentEnabled(): boolean {
|
|
30
|
+
return agentInstance !== null && agentConfig.enabled;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Capture terminal command before execution
|
|
35
|
+
*/
|
|
36
|
+
export async function captureCommandStart(
|
|
37
|
+
command: string,
|
|
38
|
+
args: string[],
|
|
39
|
+
env: Record<string, string>,
|
|
40
|
+
cwd: string
|
|
41
|
+
): Promise<TerminalEvent | null> {
|
|
42
|
+
if (!isAgentEnabled()) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const startTime = Date.now();
|
|
47
|
+
return await agentInstance!.captureAndAnalyze(command, args, env, cwd, startTime);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Capture terminal command result
|
|
52
|
+
*/
|
|
53
|
+
export async function captureCommandResult(
|
|
54
|
+
event: TerminalEvent | null,
|
|
55
|
+
stdout: string,
|
|
56
|
+
stderr: string,
|
|
57
|
+
exitCode: number
|
|
58
|
+
): Promise<void> {
|
|
59
|
+
if (!isAgentEnabled() || !event) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const endTime = Date.now();
|
|
64
|
+
const suggestions = await agentInstance!.recordResult(event, stdout, stderr, exitCode, endTime);
|
|
65
|
+
|
|
66
|
+
// Log suggestions if any (can be enhanced to show in UI)
|
|
67
|
+
if (suggestions.length > 0 && agentConfig.suggestImprovements) {
|
|
68
|
+
console.log('Agent suggestions:', suggestions);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Stop the agent
|
|
74
|
+
*/
|
|
75
|
+
export function stopAgent(): void {
|
|
76
|
+
if (agentInstance) {
|
|
77
|
+
agentInstance.stop();
|
|
78
|
+
agentInstance = null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|