@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,422 @@
|
|
|
1
|
+
// Rust API layer for cognitive memory storage
|
|
2
|
+
// Provides: append_event, list_sessions, query_recent_errors, get_context, persist_suggestion
|
|
3
|
+
|
|
4
|
+
use crate::memory::client::PluresDBClient;
|
|
5
|
+
use crate::memory::encryption::EncryptionProvider;
|
|
6
|
+
use crate::memory::schema::*;
|
|
7
|
+
use anyhow::{Context, Result};
|
|
8
|
+
use chrono::{DateTime, Duration as ChronoDuration, Utc};
|
|
9
|
+
use serde_json::Value;
|
|
10
|
+
use std::collections::HashMap;
|
|
11
|
+
|
|
12
|
+
/// Main memory store API
|
|
13
|
+
pub struct MemoryStore {
|
|
14
|
+
pub(crate) client: PluresDBClient,
|
|
15
|
+
encryption: Option<Box<dyn EncryptionProvider>>,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
impl MemoryStore {
|
|
19
|
+
pub async fn new(client: PluresDBClient) -> Result<Self> {
|
|
20
|
+
// TODO: Initialize encryption if configured
|
|
21
|
+
let encryption: Option<Box<dyn EncryptionProvider>> = None;
|
|
22
|
+
|
|
23
|
+
Ok(Self { client, encryption })
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// Append an event to memory storage
|
|
27
|
+
pub async fn append_event(&self, event: MemoryEvent) -> Result<()> {
|
|
28
|
+
let key = format!("memory:event:{}", event.id);
|
|
29
|
+
let value = serde_json::to_value(&event)?;
|
|
30
|
+
|
|
31
|
+
// Encrypt if encryption is enabled
|
|
32
|
+
let value = if let Some(enc) = &self.encryption {
|
|
33
|
+
enc.encrypt(&value).await?
|
|
34
|
+
} else {
|
|
35
|
+
value
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
self.client.put(&key, &value).await?;
|
|
39
|
+
|
|
40
|
+
// Also update session if it's a session event
|
|
41
|
+
if event.event_type == "session_start" {
|
|
42
|
+
if let Ok(session) = serde_json::from_value::<Session>(event.data.clone()) {
|
|
43
|
+
let session_key = format!("memory:session:{}", session.id);
|
|
44
|
+
self.client
|
|
45
|
+
.put(&session_key, &serde_json::to_value(&session)?)
|
|
46
|
+
.await?;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Store provenance if provided
|
|
51
|
+
if let Some(prov) = event.provenance {
|
|
52
|
+
let prov_key = format!("memory:provenance:{}", prov.id);
|
|
53
|
+
self.client
|
|
54
|
+
.put(&prov_key, &serde_json::to_value(&prov)?)
|
|
55
|
+
.await?;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
Ok(())
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// List all sessions
|
|
62
|
+
pub async fn list_sessions(&self) -> Result<Vec<Session>> {
|
|
63
|
+
let keys = self.client.list("memory:session:").await?;
|
|
64
|
+
let mut sessions = Vec::new();
|
|
65
|
+
|
|
66
|
+
for key in keys {
|
|
67
|
+
if let Some(value) = self.client.get(&key).await? {
|
|
68
|
+
// Decrypt if encryption is enabled
|
|
69
|
+
let value = if let Some(enc) = &self.encryption {
|
|
70
|
+
enc.decrypt(&value).await?
|
|
71
|
+
} else {
|
|
72
|
+
value
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
if let Ok(session) = serde_json::from_value::<Session>(value) {
|
|
76
|
+
sessions.push(session);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Sort by started_at descending
|
|
82
|
+
sessions.sort_by(|a, b| b.started_at.cmp(&a.started_at));
|
|
83
|
+
|
|
84
|
+
Ok(sessions)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// Query recent errors
|
|
88
|
+
pub async fn query_recent_errors(
|
|
89
|
+
&self,
|
|
90
|
+
limit: Option<usize>,
|
|
91
|
+
since: Option<DateTime<Utc>>,
|
|
92
|
+
severity: Option<&str>,
|
|
93
|
+
) -> Result<Vec<Error>> {
|
|
94
|
+
let keys = self.client.list("memory:error:").await?;
|
|
95
|
+
let mut errors = Vec::new();
|
|
96
|
+
|
|
97
|
+
for key in keys {
|
|
98
|
+
if let Some(value) = self.client.get(&key).await? {
|
|
99
|
+
// Decrypt if encryption is enabled
|
|
100
|
+
let value = if let Some(enc) = &self.encryption {
|
|
101
|
+
enc.decrypt(&value).await?
|
|
102
|
+
} else {
|
|
103
|
+
value
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
if let Ok(error) = serde_json::from_value::<Error>(value) {
|
|
107
|
+
// Filter by timestamp
|
|
108
|
+
if let Some(since_time) = since {
|
|
109
|
+
if error.timestamp < since_time {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Filter by severity
|
|
115
|
+
if let Some(sev) = severity {
|
|
116
|
+
if error.severity != sev {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
errors.push(error);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Sort by timestamp descending
|
|
127
|
+
errors.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
|
|
128
|
+
|
|
129
|
+
// Apply limit
|
|
130
|
+
if let Some(limit) = limit {
|
|
131
|
+
errors.truncate(limit);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
Ok(errors)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// Get context window for analysis
|
|
138
|
+
pub async fn get_context(
|
|
139
|
+
&self,
|
|
140
|
+
session_id: &str,
|
|
141
|
+
window: ChronoDuration,
|
|
142
|
+
) -> Result<ContextWindow> {
|
|
143
|
+
let end_time = Utc::now();
|
|
144
|
+
let start_time = end_time - window;
|
|
145
|
+
|
|
146
|
+
// Get session
|
|
147
|
+
let session_key = format!("memory:session:{}", session_id);
|
|
148
|
+
let session: Session = if let Some(value) = self.client.get(&session_key).await? {
|
|
149
|
+
let value = if let Some(enc) = &self.encryption {
|
|
150
|
+
enc.decrypt(&value).await?
|
|
151
|
+
} else {
|
|
152
|
+
value
|
|
153
|
+
};
|
|
154
|
+
serde_json::from_value(value).context("Failed to deserialize session")?
|
|
155
|
+
} else {
|
|
156
|
+
anyhow::bail!("Session not found: {}", session_id);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// Get commands in time window
|
|
160
|
+
let command_keys = self.client.list("memory:command:").await?;
|
|
161
|
+
let mut commands = Vec::new();
|
|
162
|
+
for key in command_keys {
|
|
163
|
+
if let Some(value) = self.client.get(&key).await? {
|
|
164
|
+
let value = if let Some(enc) = &self.encryption {
|
|
165
|
+
enc.decrypt(&value).await?
|
|
166
|
+
} else {
|
|
167
|
+
value
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
if let Ok(cmd) = serde_json::from_value::<Command>(value) {
|
|
171
|
+
if cmd.session_id == session_id
|
|
172
|
+
&& cmd.started_at >= start_time
|
|
173
|
+
&& cmd.started_at <= end_time
|
|
174
|
+
{
|
|
175
|
+
commands.push(cmd);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
commands.sort_by(|a, b| a.started_at.cmp(&b.started_at));
|
|
181
|
+
|
|
182
|
+
// Get outputs for these commands
|
|
183
|
+
let output_keys = self.client.list("memory:output:").await?;
|
|
184
|
+
let mut outputs = Vec::new();
|
|
185
|
+
let command_ids: std::collections::HashSet<String> =
|
|
186
|
+
commands.iter().map(|c| c.id.clone()).collect();
|
|
187
|
+
for key in output_keys {
|
|
188
|
+
if let Some(value) = self.client.get(&key).await? {
|
|
189
|
+
let value = if let Some(enc) = &self.encryption {
|
|
190
|
+
enc.decrypt(&value).await?
|
|
191
|
+
} else {
|
|
192
|
+
value
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
if let Ok(output) = serde_json::from_value::<Output>(value) {
|
|
196
|
+
if command_ids.contains(&output.command_id) {
|
|
197
|
+
outputs.push(output);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
outputs.sort_by(|a, b| a.chunk_index.cmp(&b.chunk_index));
|
|
203
|
+
|
|
204
|
+
// Get errors in time window
|
|
205
|
+
let error_keys = self.client.list("memory:error:").await?;
|
|
206
|
+
let mut errors = Vec::new();
|
|
207
|
+
for key in error_keys {
|
|
208
|
+
if let Some(value) = self.client.get(&key).await? {
|
|
209
|
+
let value = if let Some(enc) = &self.encryption {
|
|
210
|
+
enc.decrypt(&value).await?
|
|
211
|
+
} else {
|
|
212
|
+
value
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
if let Ok(error) = serde_json::from_value::<Error>(value) {
|
|
216
|
+
if error.session_id == session_id
|
|
217
|
+
&& error.timestamp >= start_time
|
|
218
|
+
&& error.timestamp <= end_time
|
|
219
|
+
{
|
|
220
|
+
errors.push(error);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
errors.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
|
|
226
|
+
|
|
227
|
+
// Get insights
|
|
228
|
+
let insight_keys = self.client.list("memory:insight:").await?;
|
|
229
|
+
let mut insights = Vec::new();
|
|
230
|
+
for key in insight_keys {
|
|
231
|
+
if let Some(value) = self.client.get(&key).await? {
|
|
232
|
+
let value = if let Some(enc) = &self.encryption {
|
|
233
|
+
enc.decrypt(&value).await?
|
|
234
|
+
} else {
|
|
235
|
+
value
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
if let Ok(insight) = serde_json::from_value::<Insight>(value) {
|
|
239
|
+
if insight.session_id.as_ref().map(|s| s.as_str()) == Some(session_id)
|
|
240
|
+
&& insight.generated_at >= start_time
|
|
241
|
+
&& insight.generated_at <= end_time
|
|
242
|
+
{
|
|
243
|
+
insights.push(insight);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
insights.sort_by(|a, b| a.generated_at.cmp(&b.generated_at));
|
|
249
|
+
|
|
250
|
+
Ok(ContextWindow {
|
|
251
|
+
session_id: session_id.to_string(),
|
|
252
|
+
start_time,
|
|
253
|
+
end_time,
|
|
254
|
+
commands,
|
|
255
|
+
outputs,
|
|
256
|
+
errors,
|
|
257
|
+
insights,
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/// Persist a suggestion
|
|
262
|
+
pub async fn persist_suggestion(&self, suggestion: Suggestion) -> Result<()> {
|
|
263
|
+
let key = format!("memory:suggestion:{}", suggestion.id);
|
|
264
|
+
let value = serde_json::to_value(&suggestion)?;
|
|
265
|
+
|
|
266
|
+
// Encrypt if encryption is enabled
|
|
267
|
+
let value = if let Some(enc) = &self.encryption {
|
|
268
|
+
enc.encrypt(&value).await?
|
|
269
|
+
} else {
|
|
270
|
+
value
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
self.client.put(&key, &value).await?;
|
|
274
|
+
Ok(())
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/// Get all suggestions, optionally filtered by priority
|
|
278
|
+
pub async fn get_suggestions(
|
|
279
|
+
&self,
|
|
280
|
+
priority: Option<&str>,
|
|
281
|
+
limit: Option<usize>,
|
|
282
|
+
) -> Result<Vec<Suggestion>> {
|
|
283
|
+
let keys = self.client.list("memory:suggestion:").await?;
|
|
284
|
+
let mut suggestions = Vec::new();
|
|
285
|
+
|
|
286
|
+
for key in keys {
|
|
287
|
+
if let Some(value) = self.client.get(&key).await? {
|
|
288
|
+
let value = if let Some(enc) = &self.encryption {
|
|
289
|
+
enc.decrypt(&value).await?
|
|
290
|
+
} else {
|
|
291
|
+
value
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
if let Ok(suggestion) = serde_json::from_value::<Suggestion>(value) {
|
|
295
|
+
// Filter by priority if specified
|
|
296
|
+
if let Some(pri) = priority {
|
|
297
|
+
if suggestion.priority != pri {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Skip dismissed suggestions
|
|
303
|
+
if suggestion.dismissed {
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
suggestions.push(suggestion);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Sort by rank descending
|
|
313
|
+
suggestions.sort_by(|a, b| {
|
|
314
|
+
b.rank
|
|
315
|
+
.partial_cmp(&a.rank)
|
|
316
|
+
.unwrap_or(std::cmp::Ordering::Equal)
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// Apply limit
|
|
320
|
+
if let Some(limit) = limit {
|
|
321
|
+
suggestions.truncate(limit);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
Ok(suggestions)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/// Store a command
|
|
328
|
+
pub async fn store_command(&self, command: Command) -> Result<()> {
|
|
329
|
+
let key = format!("memory:command:{}", command.id);
|
|
330
|
+
let value = serde_json::to_value(&command)?;
|
|
331
|
+
|
|
332
|
+
let value = if let Some(enc) = &self.encryption {
|
|
333
|
+
enc.encrypt(&value).await?
|
|
334
|
+
} else {
|
|
335
|
+
value
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
self.client.put(&key, &value).await?;
|
|
339
|
+
Ok(())
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/// Store an output chunk (with optional compression)
|
|
343
|
+
pub async fn store_output(&self, output: &mut Output, compress: bool) -> Result<()> {
|
|
344
|
+
if compress && !output.compressed {
|
|
345
|
+
use flate2::write::GzEncoder;
|
|
346
|
+
use flate2::Compression;
|
|
347
|
+
use std::io::Write;
|
|
348
|
+
|
|
349
|
+
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
|
|
350
|
+
encoder.write_all(&output.content)?;
|
|
351
|
+
let compressed = encoder.finish()?;
|
|
352
|
+
|
|
353
|
+
output.content = compressed;
|
|
354
|
+
output.compressed = true;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
let key = format!("memory:output:{}", output.id);
|
|
358
|
+
let value = serde_json::to_value(output)?;
|
|
359
|
+
|
|
360
|
+
let value = if let Some(enc) = &self.encryption {
|
|
361
|
+
enc.encrypt(&value).await?
|
|
362
|
+
} else {
|
|
363
|
+
value
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
self.client.put(&key, &value).await?;
|
|
367
|
+
Ok(())
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/// Store an error
|
|
371
|
+
pub async fn store_error(&self, error: Error) -> Result<()> {
|
|
372
|
+
let key = format!("memory:error:{}", error.id);
|
|
373
|
+
let value = serde_json::to_value(&error)?;
|
|
374
|
+
|
|
375
|
+
let value = if let Some(enc) = &self.encryption {
|
|
376
|
+
enc.encrypt(&value).await?
|
|
377
|
+
} else {
|
|
378
|
+
value
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
self.client.put(&key, &value).await?;
|
|
382
|
+
Ok(())
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/// Store an insight
|
|
386
|
+
pub async fn store_insight(&self, insight: Insight) -> Result<()> {
|
|
387
|
+
let key = format!("memory:insight:{}", insight.id);
|
|
388
|
+
let value = serde_json::to_value(&insight)?;
|
|
389
|
+
|
|
390
|
+
let value = if let Some(enc) = &self.encryption {
|
|
391
|
+
enc.encrypt(&value).await?
|
|
392
|
+
} else {
|
|
393
|
+
value
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
self.client.put(&key, &value).await?;
|
|
397
|
+
Ok(())
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/// Wipe all memory data (for testing/cleanup)
|
|
401
|
+
pub async fn wipe_all(&self) -> Result<()> {
|
|
402
|
+
let prefixes = vec![
|
|
403
|
+
"memory:session:",
|
|
404
|
+
"memory:command:",
|
|
405
|
+
"memory:output:",
|
|
406
|
+
"memory:error:",
|
|
407
|
+
"memory:insight:",
|
|
408
|
+
"memory:suggestion:",
|
|
409
|
+
"memory:provenance:",
|
|
410
|
+
"memory:event:",
|
|
411
|
+
];
|
|
412
|
+
|
|
413
|
+
for prefix in prefixes {
|
|
414
|
+
let keys = self.client.list(prefix).await?;
|
|
415
|
+
for key in keys {
|
|
416
|
+
self.client.delete(&key).await?;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
Ok(())
|
|
421
|
+
}
|
|
422
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// PluresDB HTTP client wrapper
|
|
2
|
+
// Communicates with PluresDB server via HTTP API
|
|
3
|
+
//
|
|
4
|
+
// NOTE: The HTTP API endpoints used here are placeholders.
|
|
5
|
+
// The actual PluresDB HTTP API may differ. Adjust endpoints
|
|
6
|
+
// based on the actual PluresDB server API documentation.
|
|
7
|
+
// Alternatively, consider using a Rust FFI binding to PluresDB
|
|
8
|
+
// if available, or the SQLiteCompatibleAPI via FFI.
|
|
9
|
+
|
|
10
|
+
use anyhow::{Context, Result};
|
|
11
|
+
use reqwest::Client;
|
|
12
|
+
use serde_json::Value;
|
|
13
|
+
use std::time::Duration;
|
|
14
|
+
|
|
15
|
+
pub struct PluresDBClient {
|
|
16
|
+
client: Client,
|
|
17
|
+
base_url: String,
|
|
18
|
+
data_dir: String,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
impl PluresDBClient {
|
|
22
|
+
pub fn new(host: &str, port: u16, data_dir: &str) -> Result<Self> {
|
|
23
|
+
let base_url = format!("http://{}:{}", host, port);
|
|
24
|
+
|
|
25
|
+
let client = Client::builder()
|
|
26
|
+
.timeout(Duration::from_secs(30))
|
|
27
|
+
.build()
|
|
28
|
+
.context("Failed to create HTTP client")?;
|
|
29
|
+
|
|
30
|
+
Ok(Self {
|
|
31
|
+
client,
|
|
32
|
+
base_url,
|
|
33
|
+
data_dir: data_dir.to_string(),
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// Put a value into PluresDB
|
|
38
|
+
pub async fn put(&self, key: &str, value: &Value) -> Result<()> {
|
|
39
|
+
let url = format!("{}/api/v1/put", self.base_url);
|
|
40
|
+
let payload = serde_json::json!({
|
|
41
|
+
"key": key,
|
|
42
|
+
"value": value,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
let response = self
|
|
46
|
+
.client
|
|
47
|
+
.post(&url)
|
|
48
|
+
.json(&payload)
|
|
49
|
+
.send()
|
|
50
|
+
.await
|
|
51
|
+
.context("Failed to send PUT request")?;
|
|
52
|
+
|
|
53
|
+
if !response.status().is_success() {
|
|
54
|
+
let status = response.status();
|
|
55
|
+
let text = response.text().await.unwrap_or_default();
|
|
56
|
+
anyhow::bail!("PluresDB PUT failed with status {}: {}", status, text);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
Ok(())
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Get a value from PluresDB
|
|
63
|
+
pub async fn get(&self, key: &str) -> Result<Option<Value>> {
|
|
64
|
+
let url = format!("{}/api/v1/get", self.base_url);
|
|
65
|
+
let payload = serde_json::json!({
|
|
66
|
+
"key": key,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
let response = self
|
|
70
|
+
.client
|
|
71
|
+
.post(&url)
|
|
72
|
+
.json(&payload)
|
|
73
|
+
.send()
|
|
74
|
+
.await
|
|
75
|
+
.context("Failed to send GET request")?;
|
|
76
|
+
|
|
77
|
+
if response.status() == 404 {
|
|
78
|
+
return Ok(None);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if !response.status().is_success() {
|
|
82
|
+
let status = response.status();
|
|
83
|
+
let text = response.text().await.unwrap_or_default();
|
|
84
|
+
anyhow::bail!("PluresDB GET failed with status {}: {}", status, text);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let result: Value = response.json().await.context("Failed to parse response")?;
|
|
88
|
+
Ok(result.get("value").cloned())
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// List keys with a prefix
|
|
92
|
+
pub async fn list(&self, prefix: &str) -> Result<Vec<String>> {
|
|
93
|
+
let url = format!("{}/api/v1/list", self.base_url);
|
|
94
|
+
let payload = serde_json::json!({
|
|
95
|
+
"prefix": prefix,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
let response = self
|
|
99
|
+
.client
|
|
100
|
+
.post(&url)
|
|
101
|
+
.json(&payload)
|
|
102
|
+
.send()
|
|
103
|
+
.await
|
|
104
|
+
.context("Failed to send LIST request")?;
|
|
105
|
+
|
|
106
|
+
if !response.status().is_success() {
|
|
107
|
+
let status = response.status();
|
|
108
|
+
let text = response.text().await.unwrap_or_default();
|
|
109
|
+
anyhow::bail!("PluresDB LIST failed with status {}: {}", status, text);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let result: Value = response.json().await.context("Failed to parse response")?;
|
|
113
|
+
let keys = result
|
|
114
|
+
.get("keys")
|
|
115
|
+
.and_then(|v| v.as_array())
|
|
116
|
+
.ok_or_else(|| anyhow::anyhow!("Invalid LIST response format"))?;
|
|
117
|
+
|
|
118
|
+
Ok(keys
|
|
119
|
+
.iter()
|
|
120
|
+
.filter_map(|v| v.as_str().map(|s| s.to_string()))
|
|
121
|
+
.collect())
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// Delete a key
|
|
125
|
+
pub async fn delete(&self, key: &str) -> Result<()> {
|
|
126
|
+
let url = format!("{}/api/v1/delete", self.base_url);
|
|
127
|
+
let payload = serde_json::json!({
|
|
128
|
+
"key": key,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
let response = self
|
|
132
|
+
.client
|
|
133
|
+
.post(&url)
|
|
134
|
+
.json(&payload)
|
|
135
|
+
.send()
|
|
136
|
+
.await
|
|
137
|
+
.context("Failed to send DELETE request")?;
|
|
138
|
+
|
|
139
|
+
if !response.status().is_success() && response.status() != 404 {
|
|
140
|
+
let status = response.status();
|
|
141
|
+
let text = response.text().await.unwrap_or_default();
|
|
142
|
+
anyhow::bail!("PluresDB DELETE failed with status {}: {}", status, text);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
Ok(())
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/// Check if PluresDB server is available
|
|
149
|
+
pub async fn health_check(&self) -> Result<bool> {
|
|
150
|
+
let url = format!("{}/health", self.base_url);
|
|
151
|
+
match self.client.get(&url).send().await {
|
|
152
|
+
Ok(response) => Ok(response.status().is_success()),
|
|
153
|
+
Err(_) => Ok(false),
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Encryption hooks interface for cognitive memory
|
|
2
|
+
// Provides abstraction for encrypting/decrypting stored data
|
|
3
|
+
|
|
4
|
+
use anyhow::Result;
|
|
5
|
+
use async_trait::async_trait;
|
|
6
|
+
use serde_json::Value;
|
|
7
|
+
|
|
8
|
+
/// Encryption provider trait
|
|
9
|
+
/// If PluresDB supports encryption natively, use that.
|
|
10
|
+
/// Otherwise, implement this trait for application-level encryption.
|
|
11
|
+
#[async_trait]
|
|
12
|
+
pub trait EncryptionProvider: Send + Sync {
|
|
13
|
+
/// Encrypt a JSON value
|
|
14
|
+
async fn encrypt(&self, value: &Value) -> Result<Value>;
|
|
15
|
+
|
|
16
|
+
/// Decrypt a JSON value
|
|
17
|
+
async fn decrypt(&self, value: &Value) -> Result<Value>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// No-op encryption provider (for when encryption is disabled)
|
|
21
|
+
pub struct NoOpEncryption;
|
|
22
|
+
|
|
23
|
+
#[async_trait]
|
|
24
|
+
impl EncryptionProvider for NoOpEncryption {
|
|
25
|
+
async fn encrypt(&self, value: &Value) -> Result<Value> {
|
|
26
|
+
Ok(value.clone())
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async fn decrypt(&self, value: &Value) -> Result<Value> {
|
|
30
|
+
Ok(value.clone())
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// TODO: Implement AES-256-GCM encryption provider
|
|
35
|
+
// This would use a key derived from user configuration or keychain
|
|
36
|
+
// Example implementation:
|
|
37
|
+
//
|
|
38
|
+
// pub struct Aes256GcmEncryption {
|
|
39
|
+
// key: [u8; 32],
|
|
40
|
+
// }
|
|
41
|
+
//
|
|
42
|
+
// #[async_trait]
|
|
43
|
+
// impl EncryptionProvider for Aes256GcmEncryption {
|
|
44
|
+
// async fn encrypt(&self, value: &Value) -> Result<Value> {
|
|
45
|
+
// // Serialize to JSON string
|
|
46
|
+
// let json_str = serde_json::to_string(value)?;
|
|
47
|
+
// // Encrypt using AES-256-GCM
|
|
48
|
+
// // Return encrypted data as base64-encoded string in JSON
|
|
49
|
+
// // ...
|
|
50
|
+
// }
|
|
51
|
+
//
|
|
52
|
+
// async fn decrypt(&self, value: &Value) -> Result<Value> {
|
|
53
|
+
// // Extract encrypted data from JSON
|
|
54
|
+
// // Decrypt using AES-256-GCM
|
|
55
|
+
// // Deserialize back to JSON Value
|
|
56
|
+
// // ...
|
|
57
|
+
// }
|
|
58
|
+
// }
|
|
59
|
+
|
|
60
|
+
// TODO: Check if PluresDB has native encryption support
|
|
61
|
+
// If yes, create a PluresDBNativeEncryption provider that uses PluresDB's encryption APIs
|
|
62
|
+
// Example:
|
|
63
|
+
//
|
|
64
|
+
// pub struct PluresDBNativeEncryption {
|
|
65
|
+
// // PluresDB handles encryption internally
|
|
66
|
+
// }
|
|
67
|
+
//
|
|
68
|
+
// #[async_trait]
|
|
69
|
+
// impl EncryptionProvider for PluresDBNativeEncryption {
|
|
70
|
+
// async fn encrypt(&self, value: &Value) -> Result<Value> {
|
|
71
|
+
// // PluresDB encrypts automatically, just pass through
|
|
72
|
+
// Ok(value.clone())
|
|
73
|
+
// }
|
|
74
|
+
//
|
|
75
|
+
// async fn decrypt(&self, value: &Value) -> Result<Value> {
|
|
76
|
+
// // PluresDB decrypts automatically, just pass through
|
|
77
|
+
// Ok(value.clone())
|
|
78
|
+
// }
|
|
79
|
+
// }
|