Skip to content

Commit ef4738a

Browse files
author
Test User
committed
feat(sessions): restore terraphim_sessions + OpenCode SQLite connector
- Restored from git history (pre-#1910 polyrepo) - rusqlite + SQLite read for opencode connector - opencode-connector feature flag - 60 tests pass
1 parent 5552074 commit ef4738a

16 files changed

Lines changed: 6211 additions & 0 deletions

File tree

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
[package]
2+
name = "terraphim_sessions"
3+
version.workspace = true
4+
edition.workspace = true
5+
description = "Session management for AI coding assistant history - search across Claude Code, Cursor, Aider sessions"
6+
license = "Apache-2.0"
7+
authors = ["Terraphim Contributors"]
8+
documentation = "https://terraphim.ai"
9+
homepage = "https://terraphim.ai"
10+
repository = "https://github.com/terraphim/terraphim-ai"
11+
keywords = ["sessions", "ai", "search", "claude", "cursor"]
12+
readme = "../../README.md"
13+
14+
[features]
15+
default = []
16+
17+
# Enable terraphim-session-analyzer integration for enhanced parsing
18+
terraphim-session-analyzer = ["dep:terraphim-session-analyzer"]
19+
20+
# Enable full TSA features including Cursor support
21+
tsa-full = ["terraphim-session-analyzer", "terraphim-session-analyzer/connectors"]
22+
23+
# Enable Aider session connector
24+
aider-connector = ["dep:regex", "dep:terraphim-markdown-parser"]
25+
26+
# Enable Cline session connector (VS Code extension)
27+
cline-connector = []
28+
29+
# Enable OpenCode session connector
30+
opencode-connector = ["dep:rusqlite"]
31+
32+
# Enable Codex (OpenAI) session connector
33+
codex-connector = []
34+
35+
# Enable all extra connectors
36+
extra-connectors = ["aider-connector", "cline-connector", "opencode-connector", "codex-connector"]
37+
38+
# Enable terraphim knowledge graph enrichment
39+
enrichment = ["terraphim_automata", "terraphim_rolegraph", "terraphim_types"]
40+
41+
# Enable BM25-ranked session search via terraphim_types score module
42+
search-index = ["terraphim_types"]
43+
44+
# All features
45+
full = ["tsa-full", "extra-connectors", "enrichment", "search-index"]
46+
47+
[dependencies]
48+
# Core dependencies
49+
tokio = { workspace = true, features = ["full"] }
50+
async-trait = { workspace = true }
51+
serde = { workspace = true, features = ["derive"] }
52+
serde_json = { workspace = true }
53+
chrono = { workspace = true, features = ["serde"] }
54+
uuid = { workspace = true }
55+
thiserror = { workspace = true }
56+
anyhow = { workspace = true }
57+
tracing = { workspace = true }
58+
59+
60+
# Time handling (same as CLA)
61+
jiff = { version = "0.2", features = ["serde"] }
62+
63+
# File system
64+
walkdir = "2.4"
65+
dirs = "5.0"
66+
67+
# File watching
68+
notify = "8.2"
69+
70+
# Feature-gated: regex for Aider/Cline connectors
71+
regex = { version = "1.10", optional = true }
72+
rusqlite = { version = "0.32", features = ["bundled"], optional = true }
73+
74+
# Feature-gated: Terraphim Session Analyzer
75+
terraphim-session-analyzer = { version = "1.19.2", optional = true, registry = "terraphim" }
76+
77+
# Feature-gated: Markdown parsing for Aider/Cline connectors
78+
terraphim-markdown-parser = { version = "1.20.2", registry = "terraphim", optional = true }
79+
80+
# Feature-gated: Terraphim enrichment (uses published crates.io versions)
81+
terraphim_automata = { version = ">=1.4.10", registry = "terraphim", optional = true }
82+
terraphim_rolegraph = { version = ">=1.4.10", registry = "terraphim", optional = true }
83+
terraphim_types = { version = ">=1.4.10", registry = "terraphim", optional = true }
84+
85+
86+
87+
[dev-dependencies]
88+
tempfile = { workspace = true }
89+
90+
tokio-test = "0.4"
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
//! TSA-based session connectors
2+
//!
3+
//! These connectors wrap terraphim-session-analyzer's connectors
4+
//! to provide enhanced parsing capabilities.
5+
6+
use super::from_normalized_session;
7+
use crate::connector::{ConnectorStatus, ImportOptions, SessionConnector};
8+
use crate::model::Session;
9+
use anyhow::Result;
10+
use async_trait::async_trait;
11+
use std::path::PathBuf;
12+
use terraphim_session_analyzer::connectors::{
13+
ImportOptions as ClaImportOptions, SessionConnector as ClaSessionConnector,
14+
};
15+
16+
/// TSA-powered Claude Code connector
17+
///
18+
/// Uses terraphim-session-analyzer for enhanced parsing with agent attribution,
19+
/// tool tracking, and detailed analytics.
20+
#[derive(Debug, Default)]
21+
pub struct ClaClaudeConnector {
22+
inner: terraphim_session_analyzer::connectors::ClaudeCodeConnector,
23+
}
24+
25+
#[async_trait]
26+
impl SessionConnector for ClaClaudeConnector {
27+
fn source_id(&self) -> &str {
28+
"claude-code"
29+
}
30+
31+
fn display_name(&self) -> &str {
32+
"Claude Code (CLA)"
33+
}
34+
35+
fn detect(&self) -> ConnectorStatus {
36+
match self.inner.detect() {
37+
terraphim_session_analyzer::connectors::ConnectorStatus::Available {
38+
path,
39+
sessions_estimate,
40+
} => ConnectorStatus::Available {
41+
path,
42+
sessions_estimate,
43+
},
44+
terraphim_session_analyzer::connectors::ConnectorStatus::NotFound => {
45+
ConnectorStatus::NotFound
46+
}
47+
terraphim_session_analyzer::connectors::ConnectorStatus::Error(e) => {
48+
ConnectorStatus::Error(e)
49+
}
50+
}
51+
}
52+
53+
fn default_path(&self) -> Option<PathBuf> {
54+
self.inner.default_path()
55+
}
56+
57+
async fn import(&self, options: &ImportOptions) -> Result<Vec<Session>> {
58+
let cla_options = to_cla_options(options);
59+
60+
// CLA import is synchronous, wrap in blocking task
61+
// Create a new connector inside the blocking task since it's stateless
62+
let sessions = tokio::task::spawn_blocking(move || {
63+
let connector = terraphim_session_analyzer::connectors::ClaudeCodeConnector;
64+
connector.import(&cla_options)
65+
})
66+
.await??;
67+
68+
Ok(sessions
69+
.into_iter()
70+
.map(|ns| from_normalized_session(ns, "cla"))
71+
.collect())
72+
}
73+
}
74+
75+
/// CLA-powered Cursor IDE connector
76+
///
77+
/// Uses claude-log-analyzer's Cursor connector for SQLite parsing.
78+
#[cfg(feature = "tsa-full")]
79+
#[derive(Debug, Default)]
80+
pub struct ClaCursorConnector {
81+
inner: terraphim_session_analyzer::connectors::cursor::CursorConnector,
82+
}
83+
84+
#[cfg(feature = "tsa-full")]
85+
#[async_trait]
86+
impl SessionConnector for ClaCursorConnector {
87+
fn source_id(&self) -> &str {
88+
"cursor"
89+
}
90+
91+
fn display_name(&self) -> &str {
92+
"Cursor IDE"
93+
}
94+
95+
fn detect(&self) -> ConnectorStatus {
96+
match self.inner.detect() {
97+
terraphim_session_analyzer::connectors::ConnectorStatus::Available {
98+
path,
99+
sessions_estimate,
100+
} => ConnectorStatus::Available {
101+
path,
102+
sessions_estimate,
103+
},
104+
terraphim_session_analyzer::connectors::ConnectorStatus::NotFound => {
105+
ConnectorStatus::NotFound
106+
}
107+
terraphim_session_analyzer::connectors::ConnectorStatus::Error(e) => {
108+
ConnectorStatus::Error(e)
109+
}
110+
}
111+
}
112+
113+
fn default_path(&self) -> Option<PathBuf> {
114+
self.inner.default_path()
115+
}
116+
117+
async fn import(&self, options: &ImportOptions) -> Result<Vec<Session>> {
118+
let cla_options = to_cla_options(options);
119+
120+
// CLA import is synchronous, wrap in blocking task
121+
// Create a new connector inside the blocking task since it's stateless
122+
let sessions = tokio::task::spawn_blocking(move || {
123+
let connector = terraphim_session_analyzer::connectors::cursor::CursorConnector;
124+
connector.import(&cla_options)
125+
})
126+
.await??;
127+
128+
Ok(sessions
129+
.into_iter()
130+
.map(|ns| from_normalized_session(ns, "cursor"))
131+
.collect())
132+
}
133+
}
134+
135+
/// Convert our ImportOptions to CLA's ImportOptions
136+
fn to_cla_options(options: &ImportOptions) -> ClaImportOptions {
137+
ClaImportOptions {
138+
path: options.path.clone(),
139+
since: options.since,
140+
until: options.until,
141+
limit: options.limit,
142+
incremental: options.incremental,
143+
}
144+
}
145+
146+
// Placeholder for when tsa-full is not enabled
147+
#[cfg(not(feature = "tsa-full"))]
148+
#[derive(Debug, Default)]
149+
pub struct ClaCursorConnector;
150+
151+
#[cfg(not(feature = "tsa-full"))]
152+
#[async_trait]
153+
impl SessionConnector for ClaCursorConnector {
154+
fn source_id(&self) -> &str {
155+
"cursor-stub"
156+
}
157+
158+
fn display_name(&self) -> &str {
159+
"Cursor IDE (requires tsa-full feature)"
160+
}
161+
162+
fn detect(&self) -> ConnectorStatus {
163+
ConnectorStatus::Error("Cursor support requires tsa-full feature".to_string())
164+
}
165+
166+
fn default_path(&self) -> Option<PathBuf> {
167+
None
168+
}
169+
170+
async fn import(&self, _options: &ImportOptions) -> Result<Vec<Session>> {
171+
anyhow::bail!("Cursor support requires tsa-full feature")
172+
}
173+
}
174+
175+
#[cfg(test)]
176+
mod tests {
177+
use super::*;
178+
179+
#[test]
180+
fn test_cla_claude_connector() {
181+
let connector = ClaClaudeConnector::default();
182+
assert_eq!(connector.source_id(), "claude-code");
183+
assert_eq!(connector.display_name(), "Claude Code (CLA)");
184+
}
185+
186+
#[cfg(feature = "tsa-full")]
187+
#[test]
188+
fn test_cla_cursor_connector() {
189+
let connector = ClaCursorConnector::default();
190+
assert_eq!(connector.source_id(), "cursor");
191+
assert_eq!(connector.display_name(), "Cursor IDE");
192+
}
193+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//! Terraphim Session Analyzer Integration
2+
//!
3+
//! This module provides enhanced session import capabilities
4+
//! when the `terraphim-session-analyzer` feature is enabled.
5+
6+
mod connector;
7+
8+
pub use connector::{ClaClaudeConnector, ClaCursorConnector};
9+
10+
use crate::model::{ContentBlock, Message, MessageRole, Session, SessionMetadata};
11+
use terraphim_session_analyzer::connectors::{NormalizedMessage, NormalizedSession};
12+
13+
/// Convert a CLA NormalizedSession to our Session type
14+
pub(crate) fn from_normalized_session(ns: NormalizedSession, prefix: &str) -> Session {
15+
let messages: Vec<Message> = ns
16+
.messages
17+
.into_iter()
18+
.map(from_normalized_message)
19+
.collect();
20+
21+
Session {
22+
id: format!("{}:{}", prefix, ns.external_id),
23+
source: ns.source,
24+
external_id: ns.external_id,
25+
title: ns.title,
26+
source_path: ns.source_path,
27+
started_at: ns.started_at,
28+
ended_at: ns.ended_at,
29+
messages,
30+
metadata: SessionMetadata::new(
31+
ns.metadata
32+
.get("project_path")
33+
.and_then(|v| v.as_str())
34+
.map(|s| s.to_string()),
35+
None,
36+
vec![],
37+
ns.metadata,
38+
),
39+
}
40+
}
41+
42+
/// Convert a CLA NormalizedMessage to our Message type
43+
fn from_normalized_message(nm: NormalizedMessage) -> Message {
44+
let role = MessageRole::from(nm.role.as_str());
45+
46+
Message {
47+
idx: nm.idx,
48+
role,
49+
author: nm.author,
50+
content: nm.content.clone(),
51+
blocks: vec![ContentBlock::Text { text: nm.content }],
52+
created_at: nm.created_at,
53+
extra: nm.extra,
54+
}
55+
}

0 commit comments

Comments
 (0)