@plures/runebook 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/ANALYSIS_LADDER.md +231 -0
  2. package/CHANGELOG.md +124 -0
  3. package/INTEGRATIONS.md +242 -0
  4. package/LICENSE +21 -0
  5. package/MEMORY.md +253 -0
  6. package/NIXOS.md +357 -0
  7. package/QUICKSTART.md +157 -0
  8. package/README.md +295 -0
  9. package/RELEASE.md +190 -0
  10. package/ValidationChecklist.md +598 -0
  11. package/docs/demo.md +338 -0
  12. package/docs/llm-integration.md +300 -0
  13. package/docs/parallel-execution-plan.md +160 -0
  14. package/flake.nix +228 -0
  15. package/integrations/README.md +242 -0
  16. package/integrations/demo-steps.sh +64 -0
  17. package/integrations/nvim-runebook.lua +140 -0
  18. package/integrations/tmux-status.sh +51 -0
  19. package/integrations/vim-runebook.vim +77 -0
  20. package/integrations/wezterm-status-simple.lua +48 -0
  21. package/integrations/wezterm-status.lua +76 -0
  22. package/nixos-module.nix +156 -0
  23. package/package.json +76 -0
  24. package/packages/design-dojo/index.js +4 -0
  25. package/packages/design-dojo/package.json +20 -0
  26. package/packages/design-dojo/tokens.css +69 -0
  27. package/playwright.config.ts +16 -0
  28. package/scripts/check-versions.cjs +62 -0
  29. package/scripts/demo.sh +220 -0
  30. package/shell.nix +31 -0
  31. package/src/app.html +13 -0
  32. package/src/cli/index.ts +1050 -0
  33. package/src/lib/agent/analysis-pipeline.ts +347 -0
  34. package/src/lib/agent/analysis-service.ts +171 -0
  35. package/src/lib/agent/analysis.ts +159 -0
  36. package/src/lib/agent/analyzers/heuristic.ts +289 -0
  37. package/src/lib/agent/analyzers/index.ts +7 -0
  38. package/src/lib/agent/analyzers/llm.ts +204 -0
  39. package/src/lib/agent/analyzers/local-search.ts +215 -0
  40. package/src/lib/agent/capture.ts +123 -0
  41. package/src/lib/agent/index.ts +244 -0
  42. package/src/lib/agent/integration.ts +81 -0
  43. package/src/lib/agent/llm/providers/base.ts +99 -0
  44. package/src/lib/agent/llm/providers/index.ts +60 -0
  45. package/src/lib/agent/llm/providers/mock.ts +67 -0
  46. package/src/lib/agent/llm/providers/ollama.ts +151 -0
  47. package/src/lib/agent/llm/providers/openai.ts +153 -0
  48. package/src/lib/agent/llm/sanitizer.ts +170 -0
  49. package/src/lib/agent/llm/types.ts +118 -0
  50. package/src/lib/agent/memory.ts +363 -0
  51. package/src/lib/agent/node-status.ts +56 -0
  52. package/src/lib/agent/node-suggestions.ts +64 -0
  53. package/src/lib/agent/status.ts +80 -0
  54. package/src/lib/agent/suggestions.ts +169 -0
  55. package/src/lib/components/Canvas.svelte +124 -0
  56. package/src/lib/components/ConnectionLine.svelte +46 -0
  57. package/src/lib/components/DisplayNode.svelte +167 -0
  58. package/src/lib/components/InputNode.svelte +158 -0
  59. package/src/lib/components/TerminalNode.svelte +237 -0
  60. package/src/lib/components/Toolbar.svelte +359 -0
  61. package/src/lib/components/TransformNode.svelte +327 -0
  62. package/src/lib/core/index.ts +31 -0
  63. package/src/lib/core/observer.ts +278 -0
  64. package/src/lib/core/redaction.ts +158 -0
  65. package/src/lib/core/shell-adapters/base.ts +325 -0
  66. package/src/lib/core/shell-adapters/bash.ts +110 -0
  67. package/src/lib/core/shell-adapters/index.ts +62 -0
  68. package/src/lib/core/shell-adapters/zsh.ts +105 -0
  69. package/src/lib/core/storage.ts +360 -0
  70. package/src/lib/core/types.ts +176 -0
  71. package/src/lib/design-dojo/Box.svelte +47 -0
  72. package/src/lib/design-dojo/Button.svelte +75 -0
  73. package/src/lib/design-dojo/Input.svelte +65 -0
  74. package/src/lib/design-dojo/List.svelte +38 -0
  75. package/src/lib/design-dojo/Select.svelte +48 -0
  76. package/src/lib/design-dojo/SplitPane.svelte +43 -0
  77. package/src/lib/design-dojo/StatusBar.svelte +61 -0
  78. package/src/lib/design-dojo/Table.svelte +47 -0
  79. package/src/lib/design-dojo/Text.svelte +36 -0
  80. package/src/lib/design-dojo/Toggle.svelte +48 -0
  81. package/src/lib/design-dojo/index.ts +10 -0
  82. package/src/lib/stores/canvas-praxis.ts +268 -0
  83. package/src/lib/stores/canvas.ts +58 -0
  84. package/src/lib/types/agent.ts +78 -0
  85. package/src/lib/types/canvas.ts +71 -0
  86. package/src/lib/utils/storage.ts +326 -0
  87. package/src/lib/utils/yaml-loader.ts +52 -0
  88. package/src/routes/+layout.svelte +5 -0
  89. package/src/routes/+layout.ts +5 -0
  90. package/src/routes/+page.svelte +32 -0
  91. package/src-tauri/Cargo.lock +5735 -0
  92. package/src-tauri/Cargo.toml +38 -0
  93. package/src-tauri/build.rs +3 -0
  94. package/src-tauri/capabilities/default.json +10 -0
  95. package/src-tauri/icons/128x128.png +0 -0
  96. package/src-tauri/icons/128x128@2x.png +0 -0
  97. package/src-tauri/icons/32x32.png +0 -0
  98. package/src-tauri/icons/Square107x107Logo.png +0 -0
  99. package/src-tauri/icons/Square142x142Logo.png +0 -0
  100. package/src-tauri/icons/Square150x150Logo.png +0 -0
  101. package/src-tauri/icons/Square284x284Logo.png +0 -0
  102. package/src-tauri/icons/Square30x30Logo.png +0 -0
  103. package/src-tauri/icons/Square310x310Logo.png +0 -0
  104. package/src-tauri/icons/Square44x44Logo.png +0 -0
  105. package/src-tauri/icons/Square71x71Logo.png +0 -0
  106. package/src-tauri/icons/Square89x89Logo.png +0 -0
  107. package/src-tauri/icons/StoreLogo.png +0 -0
  108. package/src-tauri/icons/icon.icns +0 -0
  109. package/src-tauri/icons/icon.ico +0 -0
  110. package/src-tauri/icons/icon.png +0 -0
  111. package/src-tauri/src/agents/agent1.rs +66 -0
  112. package/src-tauri/src/agents/agent2.rs +80 -0
  113. package/src-tauri/src/agents/agent3.rs +73 -0
  114. package/src-tauri/src/agents/agent4.rs +66 -0
  115. package/src-tauri/src/agents/agent5.rs +68 -0
  116. package/src-tauri/src/agents/agent6.rs +75 -0
  117. package/src-tauri/src/agents/base.rs +52 -0
  118. package/src-tauri/src/agents/mod.rs +17 -0
  119. package/src-tauri/src/core/coordination.rs +117 -0
  120. package/src-tauri/src/core/mod.rs +12 -0
  121. package/src-tauri/src/core/ownership.rs +61 -0
  122. package/src-tauri/src/core/types.rs +132 -0
  123. package/src-tauri/src/execution/mod.rs +5 -0
  124. package/src-tauri/src/execution/runner.rs +143 -0
  125. package/src-tauri/src/lib.rs +161 -0
  126. package/src-tauri/src/main.rs +6 -0
  127. package/src-tauri/src/memory/api.rs +422 -0
  128. package/src-tauri/src/memory/client.rs +156 -0
  129. package/src-tauri/src/memory/encryption.rs +79 -0
  130. package/src-tauri/src/memory/migration.rs +110 -0
  131. package/src-tauri/src/memory/mod.rs +28 -0
  132. package/src-tauri/src/memory/schema.rs +275 -0
  133. package/src-tauri/src/memory/tests.rs +192 -0
  134. package/src-tauri/src/orchestrator/coordinator.rs +232 -0
  135. package/src-tauri/src/orchestrator/mod.rs +13 -0
  136. package/src-tauri/src/orchestrator/planner.rs +304 -0
  137. package/src-tauri/tauri.conf.json +35 -0
  138. package/static/examples/date-time-example.yaml +147 -0
  139. package/static/examples/hello-world.yaml +74 -0
  140. package/static/examples/transform-example.yaml +157 -0
  141. package/static/favicon.png +0 -0
  142. package/static/svelte.svg +1 -0
  143. package/static/tauri.svg +6 -0
  144. package/static/vite.svg +1 -0
  145. package/svelte.config.js +18 -0
  146. package/tsconfig.json +19 -0
  147. package/vite.config.js +45 -0
  148. package/vitest.config.ts +21 -0
@@ -0,0 +1,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
+