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/dist/index.d.ts +19 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +39 -9
- package/dist/lib/utils.d.ts +25 -10
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +283 -28
- package/dist/react/ConversationGraph.d.ts +7 -4
- package/dist/react/ConversationGraph.d.ts.map +1 -1
- package/dist/react/ConversationGraph.js +451 -45
- package/dist/types.d.ts +33 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/native/index.node +0 -0
- package/native/package.json +2 -2
- package/native/src/lib.rs +222 -18
- package/package.json +8 -8
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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:
|
|
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>> =
|
|
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
|
|
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.
|
|
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": {
|