@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,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
+ // }