gcsdk 1.0.7 → 1.0.9

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/native/src/lib.rs CHANGED
@@ -5,9 +5,8 @@ use chrono::{DateTime, Utc};
5
5
  use neon::types::extract::Json;
6
6
  use once_cell::sync::Lazy;
7
7
  use serde::{Deserialize, Serialize};
8
- use sha2::{Digest, Sha256};
9
- use std::collections::HashMap;
10
8
  use std::sync::Mutex;
9
+ use std::time::{SystemTime, UNIX_EPOCH};
11
10
  #[derive(Serialize, Deserialize, Clone)]
12
11
  struct ChatNode {
13
12
  id: String,
@@ -15,15 +14,26 @@ struct ChatNode {
15
14
  role: String,
16
15
  content: String,
17
16
  timestamp: i64,
18
- vfs_snapshot: HashMap<String, String>, // Filepath -> Content
17
+ vfs_snapshot: std::collections::HashMap<String, String>, // Filepath -> Content
18
+ }
19
+
20
+ #[derive(Serialize, Deserialize)]
21
+ struct TreeNode {
22
+ id: String,
23
+ parent_id: Option<String>,
24
+ role: String,
25
+ content: String,
26
+ timestamp: i64,
27
+ branches: Vec<String>,
28
+ children: Vec<TreeNode>,
19
29
  }
20
30
 
21
31
  #[derive(Default)]
22
32
  struct ConversationRepo {
23
33
  // Every commit in the tree indexed by its id
24
- nodes: HashMap<String, ChatNode>,
34
+ nodes: std::collections::HashMap<String, ChatNode>,
25
35
  //ponters to the latest node for a given name
26
- branches: HashMap<String, String>,
36
+ branches: std::collections::HashMap<String, String>,
27
37
  }
28
38
 
29
39
  static REPO: Lazy<Mutex<ConversationRepo>> = Lazy::new(|| Mutex::new(ConversationRepo::default()));
@@ -42,18 +52,37 @@ fn commit(
42
52
  role: String,
43
53
  content: String,
44
54
  branch_name: String,
45
- Json(vfs_snapshot): Json<HashMap<String, String>>,
55
+ Json(vfs_snapshot): Json<std::collections::HashMap<String, String>>,
46
56
  ) -> Result<String, neon::types::extract::Error> {
47
- let mut hasher = Sha256::new();
48
- hasher.update(&content);
49
- let id = format!("{:x}", hasher.finalize());
57
+ // Important: commit IDs must be unique for a git-like DAG.
58
+ // Hashing only `content` causes collisions (e.g. repeated greetings),
59
+ // which can overwrite nodes and even create parent cycles -> infinite loops / hangs.
60
+ let timestamp_ms = Utc::now().timestamp_millis();
61
+ let nonce_ns: u128 = SystemTime::now()
62
+ .duration_since(UNIX_EPOCH)
63
+ .unwrap_or_default()
64
+ .as_nanos();
65
+
66
+ let mut hasher = <sha2::Sha256 as sha2::Digest>::new();
67
+ sha2::Digest::update(&mut hasher, role.as_bytes());
68
+ sha2::Digest::update(&mut hasher, b"\0");
69
+ if let Some(pid) = &parent_id {
70
+ sha2::Digest::update(&mut hasher, pid.as_bytes());
71
+ }
72
+ sha2::Digest::update(&mut hasher, b"\0");
73
+ sha2::Digest::update(&mut hasher, content.as_bytes());
74
+ sha2::Digest::update(&mut hasher, b"\0");
75
+ sha2::Digest::update(&mut hasher, timestamp_ms.to_string().as_bytes());
76
+ sha2::Digest::update(&mut hasher, b"\0");
77
+ sha2::Digest::update(&mut hasher, nonce_ns.to_string().as_bytes());
78
+ let id = format!("{:x}", sha2::Digest::finalize(hasher));
50
79
 
51
80
  let node = ChatNode {
52
81
  id: id.clone(),
53
82
  parent_id: parent_id.clone(),
54
83
  role: role.clone(),
55
84
  content: content.clone(),
56
- timestamp: Utc::now().timestamp_millis(),
85
+ timestamp: timestamp_ms,
57
86
  vfs_snapshot: vfs_snapshot.clone(),
58
87
  };
59
88
 
@@ -85,10 +114,19 @@ fn get_history(branch_name: String) -> Result<Json<Vec<ChatNode>>, neon::types::
85
114
 
86
115
  let mut nodes = Vec::new();
87
116
  let mut current_id = repo.branches.get(&branch_name).cloned();
117
+ let mut visited: std::collections::HashSet<String> = std::collections::HashSet::new();
88
118
 
89
119
  while let Some(id) = current_id {
120
+ // Cycle guard: corrupted graphs (e.g. from past ID collisions) must not hang.
121
+ if !visited.insert(id.clone()) {
122
+ break;
123
+ }
90
124
  if let Some(node) = repo.nodes.get(&id) {
91
125
  nodes.push(node.clone());
126
+ // Additional guard against self-parent links.
127
+ if node.parent_id.as_deref() == Some(id.as_str()) {
128
+ break;
129
+ }
92
130
  current_id = node.parent_id.clone();
93
131
  } else {
94
132
  break;
@@ -115,12 +153,176 @@ fn checkout(branch_name: String) -> Result<Option<String>, neon::types::extract:
115
153
  Ok(repo.branches.get(&branch_name).cloned())
116
154
  }
117
155
 
156
+ #[neon::export]
157
+ fn create_branch_from_commit(
158
+ branch_name: String,
159
+ commit_id: String,
160
+ ) -> Result<bool, neon::types::extract::Error> {
161
+ let mut repo = REPO.lock().unwrap();
162
+
163
+ // Verify the commit exists
164
+ if !repo.nodes.contains_key(&commit_id) {
165
+ return Ok(false);
166
+ }
167
+
168
+ repo.branches.insert(branch_name, commit_id);
169
+ Ok(true)
170
+ }
171
+
172
+ #[neon::export]
173
+ fn get_history_to_commit(commit_id: String) -> Result<Json<Vec<ChatNode>>, neon::types::extract::Error> {
174
+ let repo = REPO.lock().unwrap();
175
+
176
+ let mut nodes = Vec::new();
177
+ let mut current_id = Some(commit_id);
178
+ let mut visited: std::collections::HashSet<String> = std::collections::HashSet::new();
179
+
180
+ while let Some(id) = current_id {
181
+ // Cycle guard
182
+ if !visited.insert(id.clone()) {
183
+ break;
184
+ }
185
+ if let Some(node) = repo.nodes.get(&id) {
186
+ nodes.push(node.clone());
187
+ if node.parent_id.as_deref() == Some(id.as_str()) {
188
+ break;
189
+ }
190
+ current_id = node.parent_id.clone();
191
+ } else {
192
+ break;
193
+ }
194
+ }
195
+
196
+ nodes.reverse(); // Return oldest-first
197
+ Ok(Json(nodes))
198
+ }
199
+
200
+ #[neon::export]
201
+ fn return_tree_as_json() -> Result<String, neon::types::extract::Error> {
202
+ let repo = REPO.lock().unwrap();
203
+
204
+ if repo.nodes.is_empty() {
205
+ return Ok("[]".to_string());
206
+ }
207
+
208
+ // Build children map: parent_id -> [child_ids]
209
+ let mut children_map: std::collections::HashMap<String, Vec<String>> =
210
+ std::collections::HashMap::new();
211
+ for node in repo.nodes.values() {
212
+ if let Some(pid) = &node.parent_id {
213
+ children_map
214
+ .entry(pid.clone())
215
+ .or_default()
216
+ .push(node.id.clone());
217
+ }
218
+ }
219
+
220
+ // Build branch lookup: node_id -> [branch_names]
221
+ let branches: std::collections::HashMap<&String, Vec<&String>> =
222
+ repo.branches
223
+ .iter()
224
+ .fold(std::collections::HashMap::new(), |mut acc, (name, id)| {
225
+ acc.entry(id).or_default().push(name);
226
+ acc
227
+ });
228
+
229
+ // Find root nodes (no parent)
230
+ let mut roots: Vec<_> = repo
231
+ .nodes
232
+ .values()
233
+ .filter(|n| n.parent_id.is_none())
234
+ .collect();
235
+ roots.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
236
+
237
+ // Build JSON tree structure
238
+ let json_trees: Vec<TreeNode> = roots
239
+ .iter()
240
+ .map(|root| {
241
+ let mut visited = std::collections::HashSet::<String>::new();
242
+ build_json_tree(&root.id, &repo.nodes, &children_map, &branches, &mut visited)
243
+ })
244
+ .collect();
245
+
246
+ // Serialize to JSON string without pretty formatting
247
+ let json_string = serde_json::to_string(&json_trees)?;
248
+ Ok(json_string)
249
+ }
250
+
251
+ fn build_json_tree(
252
+ node_id: &str,
253
+ nodes: &std::collections::HashMap<String, ChatNode>,
254
+ children_map: &std::collections::HashMap<String, Vec<String>>,
255
+ branches: &std::collections::HashMap<&String, Vec<&String>>,
256
+ visited: &mut std::collections::HashSet<String>,
257
+ ) -> TreeNode {
258
+ // Cycle guard: if we ever see the same node twice in the recursion, stop.
259
+ if !visited.insert(node_id.to_string()) {
260
+ let node = &nodes[node_id];
261
+ let branch_names: Vec<String> = branches
262
+ .get(&node.id)
263
+ .map(|b| b.iter().map(|s| (*s).clone()).collect())
264
+ .unwrap_or_default();
265
+
266
+ return TreeNode {
267
+ id: node.id.clone(),
268
+ parent_id: node.parent_id.clone(),
269
+ role: node.role.clone(),
270
+ content: node.content.clone(),
271
+ timestamp: node.timestamp,
272
+ branches: branch_names,
273
+ children: Vec::new(),
274
+ };
275
+ }
276
+
277
+ let node = &nodes[node_id];
278
+
279
+ // Get branch names for this node
280
+ let branch_names: Vec<String> = branches
281
+ .get(&node.id)
282
+ .map(|b| b.iter().map(|s| (*s).clone()).collect())
283
+ .unwrap_or_default();
284
+
285
+ // Build children recursively
286
+ let mut children: Vec<TreeNode> = Vec::new();
287
+ if let Some(child_ids) = children_map.get(node_id) {
288
+ let mut sorted_children: Vec<_> = child_ids.iter().collect();
289
+ sorted_children.sort_by(|a, b| {
290
+ let ta = nodes.get(*a).map(|n| n.timestamp).unwrap_or(0);
291
+ let tb = nodes.get(*b).map(|n| n.timestamp).unwrap_or(0);
292
+ ta.cmp(&tb)
293
+ });
294
+
295
+ for child_id in sorted_children {
296
+ children.push(build_json_tree(child_id, nodes, children_map, branches, visited));
297
+ }
298
+ }
299
+
300
+ TreeNode {
301
+ id: node.id.clone(),
302
+ parent_id: node.parent_id.clone(),
303
+ role: node.role.clone(),
304
+ content: node.content.clone(),
305
+ timestamp: node.timestamp,
306
+ branches: branch_names,
307
+ children,
308
+ }
309
+ }
310
+
118
311
  fn build_tree(
119
312
  node_id: &str,
120
- nodes: &HashMap<String, ChatNode>,
121
- children_map: &HashMap<String, Vec<String>>,
122
- branches: &HashMap<&String, Vec<&String>>,
313
+ nodes: &std::collections::HashMap<String, ChatNode>,
314
+ children_map: &std::collections::HashMap<String, Vec<String>>,
315
+ branches: &std::collections::HashMap<&String, Vec<&String>>,
316
+ visited: &mut std::collections::HashSet<String>,
123
317
  ) -> termtree::Tree<String> {
318
+ // Cycle guard (rendering should never hang)
319
+ if !visited.insert(node_id.to_string()) {
320
+ return termtree::Tree::new(format!(
321
+ "\x1b[31m{} (cycle detected)\x1b[0m",
322
+ &node_id[..7.min(node_id.len())]
323
+ ));
324
+ }
325
+
124
326
  let node = &nodes[node_id];
125
327
  let short_id = &node.id[..7.min(node.id.len())];
126
328
 
@@ -163,7 +365,7 @@ fn build_tree(
163
365
  });
164
366
 
165
367
  for child_id in sorted_children {
166
- tree.push(build_tree(child_id, nodes, children_map, branches));
368
+ tree.push(build_tree(child_id, nodes, children_map, branches, visited));
167
369
  }
168
370
  }
169
371
 
@@ -179,7 +381,8 @@ fn render_tree() -> Result<String, neon::types::extract::Error> {
179
381
  }
180
382
 
181
383
  // Build children map: parent_id -> [child_ids]
182
- let mut children_map: HashMap<String, Vec<String>> = HashMap::new();
384
+ let mut children_map: std::collections::HashMap<String, Vec<String>> =
385
+ std::collections::HashMap::new();
183
386
  for node in repo.nodes.values() {
184
387
  if let Some(pid) = &node.parent_id {
185
388
  children_map
@@ -190,10 +393,10 @@ fn render_tree() -> Result<String, neon::types::extract::Error> {
190
393
  }
191
394
 
192
395
  // Build branch lookup: node_id -> [branch_names]
193
- let branches: HashMap<&String, Vec<&String>> =
396
+ let branches: std::collections::HashMap<&String, Vec<&String>> =
194
397
  repo.branches
195
398
  .iter()
196
- .fold(HashMap::new(), |mut acc, (name, id)| {
399
+ .fold(std::collections::HashMap::new(), |mut acc, (name, id)| {
197
400
  acc.entry(id).or_default().push(name);
198
401
  acc
199
402
  });
@@ -210,7 +413,8 @@ fn render_tree() -> Result<String, neon::types::extract::Error> {
210
413
  let output: String = roots
211
414
  .iter()
212
415
  .map(|root| {
213
- let tree = build_tree(&root.id, &repo.nodes, &children_map, &branches);
416
+ let mut visited = std::collections::HashSet::<String>::new();
417
+ let tree = build_tree(&root.id, &repo.nodes, &children_map, &branches, &mut visited);
214
418
  tree.to_string()
215
419
  })
216
420
  .collect::<Vec<_>>()
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "gcsdk",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "Git Chat SDK - A Git-like conversation SDK for managing chat histories with branching support",
5
+ "type": "module",
5
6
  "main": "dist/index.js",
6
7
  "types": "dist/index.d.ts",
7
8
  "exports": {
@@ -10,6 +11,11 @@
10
11
  "require": "./dist/index.js",
11
12
  "import": "./dist/index.js"
12
13
  },
14
+ "./types": {
15
+ "types": "./dist/types.d.ts",
16
+ "require": "./dist/types.js",
17
+ "import": "./dist/types.js"
18
+ },
13
19
  "./react": {
14
20
  "types": "./dist/react/ConversationGraph.d.ts",
15
21
  "require": "./dist/react/ConversationGraph.js",
@@ -58,20 +64,14 @@
58
64
  "@types/bun": "^1.3.5",
59
65
  "@types/node": "^20.0.0",
60
66
  "@types/react": "^18.0.0",
61
- "react": "^18.0.0 || ^19.0.0",
62
- "reactflow": "^11.11.4",
63
67
  "typescript": "^5.0.0"
64
68
  },
65
69
  "peerDependencies": {
66
- "react": "^18.0.0 || ^19.0.0",
67
- "reactflow": "^11.0.0"
70
+ "react": "^18.0.0 || ^19.0.0"
68
71
  },
69
72
  "peerDependenciesMeta": {
70
73
  "react": {
71
74
  "optional": true
72
- },
73
- "reactflow": {
74
- "optional": true
75
75
  }
76
76
  },
77
77
  "engines": {