create-keel-and-deck-app 0.1.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 (35) hide show
  1. package/index.js +76 -0
  2. package/package.json +23 -0
  3. package/template/index.html +12 -0
  4. package/template/package.json +41 -0
  5. package/template/src/App.tsx +193 -0
  6. package/template/src/hooks/use-session-events.ts +36 -0
  7. package/template/src/lib/tauri.ts +68 -0
  8. package/template/src/lib/types.ts +111 -0
  9. package/template/src/main.tsx +10 -0
  10. package/template/src/stores/agents.ts +65 -0
  11. package/template/src/stores/events.ts +27 -0
  12. package/template/src/stores/feeds.ts +34 -0
  13. package/template/src/stores/issues.ts +35 -0
  14. package/template/src/stores/memory.ts +30 -0
  15. package/template/src/stores/ui.ts +17 -0
  16. package/template/src/styles/globals.css +24 -0
  17. package/template/src-tauri/Cargo.toml +28 -0
  18. package/template/src-tauri/build.rs +3 -0
  19. package/template/src-tauri/capabilities/default.json +12 -0
  20. package/template/src-tauri/icons/.gitkeep +0 -0
  21. package/template/src-tauri/src/commands/channels.rs +119 -0
  22. package/template/src-tauri/src/commands/events.rs +43 -0
  23. package/template/src-tauri/src/commands/issues.rs +29 -0
  24. package/template/src-tauri/src/commands/memory.rs +52 -0
  25. package/template/src-tauri/src/commands/mod.rs +8 -0
  26. package/template/src-tauri/src/commands/projects.rs +38 -0
  27. package/template/src-tauri/src/commands/scheduler.rs +67 -0
  28. package/template/src-tauri/src/commands/sessions.rs +80 -0
  29. package/template/src-tauri/src/commands/workspace.rs +62 -0
  30. package/template/src-tauri/src/lib.rs +93 -0
  31. package/template/src-tauri/src/main.rs +6 -0
  32. package/template/src-tauri/src/workspace.rs +21 -0
  33. package/template/src-tauri/tauri.conf.json +30 -0
  34. package/template/tsconfig.json +21 -0
  35. package/template/vite.config.ts +17 -0
@@ -0,0 +1,34 @@
1
+ import { create } from "zustand";
2
+ import { mergeFeedItem } from "@deck-ui/chat";
3
+ import type { FeedItem } from "@deck-ui/chat";
4
+
5
+ interface FeedState {
6
+ items: Record<string, FeedItem[]>;
7
+ pushFeedItem: (sessionKey: string, item: FeedItem) => void;
8
+ setFeed: (sessionKey: string, items: FeedItem[]) => void;
9
+ clearFeed: (sessionKey: string) => void;
10
+ }
11
+
12
+ export const useFeedStore = create<FeedState>((set) => ({
13
+ items: {},
14
+
15
+ pushFeedItem: (sessionKey, item) =>
16
+ set((s) => ({
17
+ items: {
18
+ ...s.items,
19
+ [sessionKey]: mergeFeedItem(s.items[sessionKey] ?? [], item),
20
+ },
21
+ })),
22
+
23
+ setFeed: (sessionKey, items) =>
24
+ set((s) => ({
25
+ items: { ...s.items, [sessionKey]: items },
26
+ })),
27
+
28
+ clearFeed: (sessionKey) =>
29
+ set((s) => {
30
+ const next = { ...s.items };
31
+ delete next[sessionKey];
32
+ return { items: next };
33
+ }),
34
+ }));
@@ -0,0 +1,35 @@
1
+ import { create } from "zustand";
2
+ import { tauriIssues } from "../lib/tauri";
3
+ import type { Issue } from "../lib/types";
4
+
5
+ interface IssueState {
6
+ issues: Issue[];
7
+ loading: boolean;
8
+ loadIssues: (projectId: string) => Promise<void>;
9
+ createIssue: (projectId: string, title: string, description: string) => Promise<void>;
10
+ updateIssueStatus: (issueId: string, status: string) => void;
11
+ }
12
+
13
+ export const useIssueStore = create<IssueState>((set, get) => ({
14
+ issues: [],
15
+ loading: false,
16
+
17
+ loadIssues: async (projectId) => {
18
+ set({ loading: true });
19
+ const issues = await tauriIssues.list(projectId);
20
+ set({ issues, loading: false });
21
+ },
22
+
23
+ createIssue: async (projectId, title, description) => {
24
+ const issue = await tauriIssues.create(projectId, title, description);
25
+ set((s) => ({ issues: [...s.issues, issue] }));
26
+ },
27
+
28
+ updateIssueStatus: (issueId, status) => {
29
+ set((s) => ({
30
+ issues: s.issues.map((i) =>
31
+ i.id === issueId ? { ...i, status } : i,
32
+ ),
33
+ }));
34
+ },
35
+ }));
@@ -0,0 +1,30 @@
1
+ import { create } from "zustand";
2
+ import type { Memory } from "@deck-ui/memory";
3
+ import { tauriMemory } from "../lib/tauri";
4
+
5
+ interface MemoryState {
6
+ memories: Memory[];
7
+ loading: boolean;
8
+ loadMemories: (projectId: string) => Promise<void>;
9
+ deleteMemory: (memoryId: string) => void;
10
+ }
11
+
12
+ export const useMemoryStore = create<MemoryState>((set) => ({
13
+ memories: [],
14
+ loading: false,
15
+
16
+ loadMemories: async (projectId) => {
17
+ set({ loading: true });
18
+ try {
19
+ const memories = await tauriMemory.list(projectId);
20
+ set({ memories, loading: false });
21
+ } catch {
22
+ set({ loading: false });
23
+ }
24
+ },
25
+
26
+ deleteMemory: (memoryId) =>
27
+ set((s) => ({
28
+ memories: s.memories.filter((m) => m.id !== memoryId),
29
+ })),
30
+ }));
@@ -0,0 +1,17 @@
1
+ import { create } from "zustand";
2
+
3
+ export type ViewMode = "files" | "instructions";
4
+
5
+ interface UIState {
6
+ viewMode: ViewMode;
7
+ chatOpen: boolean;
8
+ setViewMode: (mode: ViewMode) => void;
9
+ setChatOpen: (open: boolean) => void;
10
+ }
11
+
12
+ export const useUIStore = create<UIState>((set) => ({
13
+ viewMode: "files",
14
+ chatOpen: false,
15
+ setViewMode: (viewMode) => set({ viewMode }),
16
+ setChatOpen: (chatOpen) => set({ chatOpen }),
17
+ }));
@@ -0,0 +1,24 @@
1
+ @import "tailwindcss";
2
+ @import "@deck-ui/core/src/globals.css";
3
+ @import "@deck-ui/core/src/styles.css";
4
+ @import "@deck-ui/chat/src/styles.css";
5
+
6
+ /* Scan local source + deck-ui packages for Tailwind classes */
7
+ @source "../";
8
+ @source "../../node_modules/@deck-ui/board/src";
9
+ @source "../../node_modules/@deck-ui/chat/src";
10
+ @source "../../node_modules/@deck-ui/layout/src";
11
+ @source "../../node_modules/@deck-ui/core/src";
12
+ @source "../../node_modules/@deck-ui/skills/src";
13
+ @source "../../node_modules/@deck-ui/routines/src";
14
+ @source "../../node_modules/@deck-ui/connections/src";
15
+ @source "../../node_modules/@deck-ui/events/src";
16
+ @source "../../node_modules/@deck-ui/memory/src";
17
+ @source "../../node_modules/@deck-ui/review/src";
18
+
19
+ body {
20
+ margin: 0;
21
+ overflow: hidden;
22
+ font-family: ui-sans-serif, -apple-system, system-ui, "Segoe UI", Helvetica,
23
+ Arial, sans-serif;
24
+ }
@@ -0,0 +1,28 @@
1
+ [package]
2
+ name = "{{APP_NAME_SNAKE}}"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+
6
+ [lib]
7
+ name = "{{APP_NAME_SNAKE}}_lib"
8
+ crate-type = ["staticlib", "cdylib", "rlib"]
9
+
10
+ [build-dependencies]
11
+ tauri-build = { version = "2", features = [] }
12
+
13
+ [dependencies]
14
+ tauri = { version = "2", features = [] }
15
+ serde = { version = "1", features = ["derive"] }
16
+ serde_json = "1"
17
+ tokio = { version = "1", features = ["full"] }
18
+ uuid = { version = "1", features = ["v4"] }
19
+ keel-tauri = { git = "https://github.com/ja-818/keel-and-deck.git" }
20
+ keel-db = { git = "https://github.com/ja-818/keel-and-deck.git" }
21
+ keel-sessions = { git = "https://github.com/ja-818/keel-and-deck.git" }
22
+ keel-events = { git = "https://github.com/ja-818/keel-and-deck.git" }
23
+ keel-scheduler = { git = "https://github.com/ja-818/keel-and-deck.git" }
24
+ keel-channels = { git = "https://github.com/ja-818/keel-and-deck.git" }
25
+ keel-memory = { git = "https://github.com/ja-818/keel-and-deck.git" }
26
+ anyhow = "1"
27
+ chrono = { version = "0.4", features = ["serde"] }
28
+ libsql = "0.6"
@@ -0,0 +1,3 @@
1
+ fn main() {
2
+ tauri_build::build()
3
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/nickelpack/tauri/v2/crates/tauri-utils/schema.json",
3
+ "identifier": "default",
4
+ "description": "Default capability for the main window",
5
+ "windows": ["main"],
6
+ "permissions": [
7
+ "core:default",
8
+ "core:event:default",
9
+ "core:event:allow-listen",
10
+ "core:event:allow-emit"
11
+ ]
12
+ }
File without changes
@@ -0,0 +1,119 @@
1
+ use keel_tauri::state::AppState;
2
+ use tauri::State;
3
+
4
+ /// List all configured channels from the channels table.
5
+ /// Returns an empty list if the table does not exist yet.
6
+ #[tauri::command]
7
+ pub async fn list_channels(
8
+ state: State<'_, AppState>,
9
+ ) -> Result<Vec<serde_json::Value>, String> {
10
+ let rows = state
11
+ .db
12
+ .conn()
13
+ .query(
14
+ "SELECT id, channel_type, name, status, config, created_at \
15
+ FROM channels ORDER BY created_at DESC",
16
+ libsql::params![],
17
+ )
18
+ .await;
19
+
20
+ match rows {
21
+ Ok(mut rows) => {
22
+ let mut results = Vec::new();
23
+ while let Ok(Some(row)) = rows.next().await {
24
+ let entry = serde_json::json!({
25
+ "id": row.get::<String>(0).unwrap_or_default(),
26
+ "channel_type": row.get::<String>(1).unwrap_or_default(),
27
+ "name": row.get::<String>(2).unwrap_or_default(),
28
+ "status": row.get::<String>(3).unwrap_or_default(),
29
+ "config": row.get::<String>(4).unwrap_or_default(),
30
+ "created_at": row.get::<String>(5).unwrap_or_default(),
31
+ });
32
+ results.push(entry);
33
+ }
34
+ Ok(results)
35
+ }
36
+ Err(_) => Ok(Vec::new()),
37
+ }
38
+ }
39
+
40
+ #[tauri::command]
41
+ pub async fn add_channel(
42
+ state: State<'_, AppState>,
43
+ channel_type: String,
44
+ name: String,
45
+ config: serde_json::Value,
46
+ ) -> Result<serde_json::Value, String> {
47
+ let id = uuid::Uuid::new_v4().to_string();
48
+ let now = chrono::Utc::now().to_rfc3339();
49
+ let config_str = serde_json::to_string(&config).map_err(|e| e.to_string())?;
50
+
51
+ state
52
+ .db
53
+ .conn()
54
+ .execute(
55
+ "INSERT INTO channels (id, channel_type, name, status, config, created_at) \
56
+ VALUES (?1, ?2, ?3, 'disconnected', ?4, ?5)",
57
+ [&id, &channel_type, &name, &config_str, &now],
58
+ )
59
+ .await
60
+ .map_err(|e| e.to_string())?;
61
+
62
+ Ok(serde_json::json!({
63
+ "id": id,
64
+ "channel_type": channel_type,
65
+ "name": name,
66
+ "status": "disconnected",
67
+ "config": config_str,
68
+ "created_at": now,
69
+ }))
70
+ }
71
+
72
+ #[tauri::command]
73
+ pub async fn remove_channel(
74
+ state: State<'_, AppState>,
75
+ channel_id: String,
76
+ ) -> Result<(), String> {
77
+ state
78
+ .db
79
+ .conn()
80
+ .execute("DELETE FROM channels WHERE id = ?1", [&channel_id])
81
+ .await
82
+ .map_err(|e| e.to_string())?;
83
+ Ok(())
84
+ }
85
+
86
+ #[tauri::command]
87
+ pub async fn connect_channel(
88
+ state: State<'_, AppState>,
89
+ channel_id: String,
90
+ ) -> Result<(), String> {
91
+ state
92
+ .db
93
+ .conn()
94
+ .execute(
95
+ "UPDATE channels SET status = 'connecting' WHERE id = ?1",
96
+ [&channel_id],
97
+ )
98
+ .await
99
+ .map_err(|e| e.to_string())?;
100
+ // Actual connection logic would be handled by a background task.
101
+ Ok(())
102
+ }
103
+
104
+ #[tauri::command]
105
+ pub async fn disconnect_channel(
106
+ state: State<'_, AppState>,
107
+ channel_id: String,
108
+ ) -> Result<(), String> {
109
+ state
110
+ .db
111
+ .conn()
112
+ .execute(
113
+ "UPDATE channels SET status = 'disconnected' WHERE id = ?1",
114
+ [&channel_id],
115
+ )
116
+ .await
117
+ .map_err(|e| e.to_string())?;
118
+ Ok(())
119
+ }
@@ -0,0 +1,43 @@
1
+ use keel_tauri::state::AppState;
2
+ use tauri::State;
3
+
4
+ /// List recent events from the event log table.
5
+ /// Falls back to an empty list if the table does not yet exist.
6
+ #[tauri::command]
7
+ pub async fn list_events(
8
+ state: State<'_, AppState>,
9
+ project_id: String,
10
+ ) -> Result<Vec<serde_json::Value>, String> {
11
+ let rows = state
12
+ .db
13
+ .conn()
14
+ .query(
15
+ "SELECT id, input_type, source_channel, source_identifier, \
16
+ payload, project_id, created_at \
17
+ FROM event_log WHERE project_id = ?1 \
18
+ ORDER BY created_at DESC LIMIT 100",
19
+ [project_id],
20
+ )
21
+ .await;
22
+
23
+ match rows {
24
+ Ok(mut rows) => {
25
+ let mut results = Vec::new();
26
+ while let Ok(Some(row)) = rows.next().await {
27
+ let entry = serde_json::json!({
28
+ "id": row.get::<String>(0).unwrap_or_default(),
29
+ "input_type": row.get::<String>(1).unwrap_or_default(),
30
+ "source_channel": row.get::<String>(2).unwrap_or_default(),
31
+ "source_identifier": row.get::<String>(3).unwrap_or_default(),
32
+ "payload": row.get::<String>(4).unwrap_or_default(),
33
+ "project_id": row.get::<String>(5).unwrap_or_default(),
34
+ "created_at": row.get::<String>(6).unwrap_or_default(),
35
+ });
36
+ results.push(entry);
37
+ }
38
+ Ok(results)
39
+ }
40
+ // Table may not exist yet in fresh databases.
41
+ Err(_) => Ok(Vec::new()),
42
+ }
43
+ }
@@ -0,0 +1,29 @@
1
+ use keel_tauri::keel_db::Issue;
2
+ use keel_tauri::state::AppState;
3
+ use tauri::State;
4
+
5
+ #[tauri::command]
6
+ pub async fn list_issues(
7
+ state: State<'_, AppState>,
8
+ project_id: String,
9
+ ) -> Result<Vec<Issue>, String> {
10
+ state
11
+ .db
12
+ .list_issues(&project_id)
13
+ .await
14
+ .map_err(|e| e.to_string())
15
+ }
16
+
17
+ #[tauri::command]
18
+ pub async fn create_issue(
19
+ state: State<'_, AppState>,
20
+ project_id: String,
21
+ title: String,
22
+ description: String,
23
+ ) -> Result<Issue, String> {
24
+ state
25
+ .db
26
+ .create_issue(&project_id, &title, &description, None)
27
+ .await
28
+ .map_err(|e| e.to_string())
29
+ }
@@ -0,0 +1,52 @@
1
+ use keel_tauri::keel_memory::{Memory, MemoryCategory, MemoryQuery, MemoryStore};
2
+ use tauri::State;
3
+
4
+ #[tauri::command]
5
+ pub async fn list_memories(
6
+ store: State<'_, MemoryStore>,
7
+ project_id: String,
8
+ ) -> Result<Vec<Memory>, String> {
9
+ store
10
+ .list_by_project(&project_id)
11
+ .await
12
+ .map_err(|e| e.to_string())
13
+ }
14
+
15
+ #[tauri::command]
16
+ pub async fn create_memory(
17
+ store: State<'_, MemoryStore>,
18
+ project_id: String,
19
+ content: String,
20
+ category: String,
21
+ tags: Vec<String>,
22
+ ) -> Result<Memory, String> {
23
+ let cat: MemoryCategory = category.parse().map_err(|e: anyhow::Error| e.to_string())?;
24
+ store
25
+ .create(&project_id, &content, cat, "user", tags)
26
+ .await
27
+ .map_err(|e| e.to_string())
28
+ }
29
+
30
+ #[tauri::command]
31
+ pub async fn delete_memory(
32
+ store: State<'_, MemoryStore>,
33
+ memory_id: String,
34
+ ) -> Result<(), String> {
35
+ store.delete(&memory_id).await.map_err(|e| e.to_string())
36
+ }
37
+
38
+ #[tauri::command]
39
+ pub async fn search_memories(
40
+ store: State<'_, MemoryStore>,
41
+ project_id: String,
42
+ query: String,
43
+ ) -> Result<Vec<Memory>, String> {
44
+ store
45
+ .search(MemoryQuery {
46
+ project_id: Some(project_id),
47
+ search_text: Some(query),
48
+ ..Default::default()
49
+ })
50
+ .await
51
+ .map_err(|e| e.to_string())
52
+ }
@@ -0,0 +1,8 @@
1
+ pub mod channels;
2
+ pub mod events;
3
+ pub mod issues;
4
+ pub mod memory;
5
+ pub mod projects;
6
+ pub mod scheduler;
7
+ pub mod sessions;
8
+ pub mod workspace;
@@ -0,0 +1,38 @@
1
+ use crate::workspace;
2
+ use keel_tauri::keel_db::Project;
3
+ use keel_tauri::state::AppState;
4
+ use tauri::State;
5
+
6
+ #[tauri::command]
7
+ pub async fn list_projects(state: State<'_, AppState>) -> Result<Vec<Project>, String> {
8
+ state.db.list_projects().await.map_err(|e| e.to_string())
9
+ }
10
+
11
+ #[tauri::command]
12
+ pub async fn create_project(
13
+ state: State<'_, AppState>,
14
+ name: String,
15
+ folder_path: String,
16
+ ) -> Result<Project, String> {
17
+ // Seed workspace files (CLAUDE.md, etc.)
18
+ workspace::seed_workspace(&folder_path);
19
+
20
+ state
21
+ .db
22
+ .create_project(&name, &folder_path)
23
+ .await
24
+ .map_err(|e| e.to_string())
25
+ }
26
+
27
+ #[tauri::command]
28
+ pub async fn delete_project(
29
+ state: State<'_, AppState>,
30
+ project_id: String,
31
+ ) -> Result<(), String> {
32
+ // Only removes from DB — does NOT delete the folder on disk.
33
+ state
34
+ .db
35
+ .delete_project(&project_id)
36
+ .await
37
+ .map_err(|e| e.to_string())
38
+ }
@@ -0,0 +1,67 @@
1
+ use keel_tauri::state::AppState;
2
+ use tauri::State;
3
+
4
+ #[tauri::command]
5
+ pub async fn add_heartbeat(
6
+ state: State<'_, AppState>,
7
+ config: serde_json::Value,
8
+ ) -> Result<String, String> {
9
+ let scheduler = state
10
+ .scheduler
11
+ .as_ref()
12
+ .ok_or_else(|| "Scheduler not initialized".to_string())?;
13
+
14
+ let hb_config: keel_scheduler::HeartbeatConfig =
15
+ serde_json::from_value(config).map_err(|e| e.to_string())?;
16
+
17
+ let mut sched = scheduler.lock().await;
18
+ let id = sched.add_heartbeat(hb_config);
19
+ Ok(id)
20
+ }
21
+
22
+ #[tauri::command]
23
+ pub async fn remove_heartbeat(
24
+ state: State<'_, AppState>,
25
+ id: String,
26
+ ) -> Result<(), String> {
27
+ let scheduler = state
28
+ .scheduler
29
+ .as_ref()
30
+ .ok_or_else(|| "Scheduler not initialized".to_string())?;
31
+
32
+ let mut sched = scheduler.lock().await;
33
+ sched.remove_heartbeat(&id);
34
+ Ok(())
35
+ }
36
+
37
+ #[tauri::command]
38
+ pub async fn add_cron(
39
+ state: State<'_, AppState>,
40
+ config: serde_json::Value,
41
+ ) -> Result<String, String> {
42
+ let scheduler = state
43
+ .scheduler
44
+ .as_ref()
45
+ .ok_or_else(|| "Scheduler not initialized".to_string())?;
46
+
47
+ let cron_config: keel_scheduler::CronJobConfig =
48
+ serde_json::from_value(config).map_err(|e| e.to_string())?;
49
+
50
+ let mut sched = scheduler.lock().await;
51
+ sched.add_cron(cron_config).map_err(|e| e.to_string())
52
+ }
53
+
54
+ #[tauri::command]
55
+ pub async fn remove_cron(
56
+ state: State<'_, AppState>,
57
+ id: String,
58
+ ) -> Result<(), String> {
59
+ let scheduler = state
60
+ .scheduler
61
+ .as_ref()
62
+ .ok_or_else(|| "Scheduler not initialized".to_string())?;
63
+
64
+ let mut sched = scheduler.lock().await;
65
+ sched.remove_cron(&id);
66
+ Ok(())
67
+ }
@@ -0,0 +1,80 @@
1
+ use crate::workspace;
2
+ use keel_tauri::chat_session::ChatSessionState;
3
+ use keel_tauri::paths::expand_tilde;
4
+ use keel_tauri::session_runner::{spawn_and_monitor, PersistOptions};
5
+ use keel_tauri::state::AppState;
6
+ use tauri::State;
7
+
8
+ #[tauri::command]
9
+ pub async fn start_session(
10
+ app_handle: tauri::AppHandle,
11
+ state: State<'_, AppState>,
12
+ chat_state: State<'_, ChatSessionState>,
13
+ project_id: String,
14
+ prompt: String,
15
+ ) -> Result<String, String> {
16
+ let project = state
17
+ .db
18
+ .get_project(&project_id)
19
+ .await
20
+ .map_err(|e| e.to_string())?
21
+ .ok_or_else(|| "Project not found".to_string())?;
22
+
23
+ let working_dir = expand_tilde(&project.folder_path);
24
+
25
+ // Seed workspace files on first use.
26
+ workspace::seed_workspace(&project.folder_path);
27
+
28
+ // Build system prompt from CLAUDE.md if it exists.
29
+ let system_prompt = {
30
+ let claude_md = working_dir.join("CLAUDE.md");
31
+ if claude_md.exists() {
32
+ std::fs::read_to_string(&claude_md).ok()
33
+ } else {
34
+ None
35
+ }
36
+ };
37
+
38
+ // Resume previous session if we have one.
39
+ let resume_id = chat_state.get();
40
+
41
+ let session_key = "main".to_string();
42
+
43
+ let _handle = spawn_and_monitor(
44
+ &app_handle,
45
+ &session_key,
46
+ &prompt,
47
+ resume_id.as_deref(),
48
+ Some(working_dir),
49
+ system_prompt.as_deref(),
50
+ Some(chat_state.inner().clone()),
51
+ Some(PersistOptions {
52
+ db: state.db.clone(),
53
+ project_id: project_id.clone(),
54
+ feed_key: session_key.clone(),
55
+ source: "desktop".to_string(),
56
+ }),
57
+ );
58
+
59
+ Ok(session_key)
60
+ }
61
+
62
+ #[tauri::command]
63
+ pub async fn load_chat_feed(
64
+ state: State<'_, AppState>,
65
+ project_id: String,
66
+ feed_key: String,
67
+ ) -> Result<Vec<serde_json::Value>, String> {
68
+ let rows = state
69
+ .db
70
+ .list_chat_feed(&project_id, &feed_key)
71
+ .await
72
+ .map_err(|e| e.to_string())?;
73
+
74
+ let items: Vec<serde_json::Value> = rows
75
+ .into_iter()
76
+ .filter_map(|row| serde_json::from_str(&row.data_json).ok())
77
+ .collect();
78
+
79
+ Ok(items)
80
+ }