### Migration Test Setup Source: https://docs.rs/sessionwiki/latest/src/sessionwiki/index.rs.html Helper function to initialize an in-memory SQLite database with initial metadata and notes for testing. ```rust fn mem() -> Connection { let c = Connection::open_in_memory().unwrap(); c.execute_batch( "CREATE TABLE meta(key TEXT PRIMARY KEY, value TEXT NOT NULL); CREATE TABLE notes(session_id TEXT PRIMARY KEY, note TEXT NOT NULL, updated TEXT NOT NULL); INSERT INTO meta VALUES('durable_version','1'); INSERT INTO notes VALUES('s1','keep me','t');", ) .unwrap(); c } ``` -------------------------------- ### In-Memory Database Setup Source: https://docs.rs/sessionwiki/latest/src/sessionwiki/index.rs.html Helper function to initialize an in-memory SQLite connection with the required schema for session tracking. ```rust fn mem() -> Connection { let c = Connection::open_in_memory().unwrap(); c.execute_batch( "CREATE TABLE files( path TEXT PRIMARY KEY, mtime INTEGER NOT NULL DEFAULT 0, size INTEGER NOT NULL DEFAULT 0, session_id TEXT NOT NULL, tool TEXT NOT NULL, project TEXT NOT NULL DEFAULT '', title TEXT NOT NULL DEFAULT '', started TEXT, ended TEXT, msg_count INTEGER NOT NULL DEFAULT 0, kind TEXT NOT NULL DEFAULT 'main', archived_at TEXT); CREATE TABLE summaries(session_id TEXT PRIMARY KEY, summary TEXT NOT NULL, created TEXT NOT NULL); CREATE TABLE tags(session_id TEXT NOT NULL, tag TEXT NOT NULL, PRIMARY KEY(session_id, tag));", ) .unwrap(); c } ``` -------------------------------- ### session_start() Source: https://docs.rs/sessionwiki/latest/sessionwiki/hook/fn.session_start.html The SessionStart hook entry point. Reads CC's JSON from stdin (bounded), prints a fenced project brief to stdout, and ALWAYS returns cleanly (empty output on any error/garbage/no-history). Never returns Err - a non-zero exit or a Rust error on stdout would pollute the agent context at session start. ```APIDOC ## session_start() ### Description The SessionStart hook entry point. Reads CC's JSON from stdin (bounded), prints a fenced project brief to stdout, and ALWAYS returns cleanly (empty output on any error/garbage/no-history). Never returns Err - a non-zero exit or a Rust error on stdout would pollute the agent context at session start. ### Function Signature ```rust pub fn session_start() ``` ### Parameters None. ### Return Value - **Type**: `()` (unit) - **Description**: Always returns cleanly. On any error, garbage input, or no history, it prints empty output to stdout and returns normally. It never returns an `Err`. ``` -------------------------------- ### account_link.rs: swapdex session attribution module Source: https://docs.rs/sessionwiki/latest/src/sessionwiki/account_link.rs.html Complete implementation of the optional swapdex integration module. It reads swapdex's timeline.jsonl (overridable via SESSIONWIKI_SWAPDEX_TIMELINE), parses only 'use'/'restore' events, and attributes each session to the last switch before its start. Includes defensive parsing (skips malformed lines, caps to last 4000 lines) and a unit test verifying attribution and unattributed sessions. ```rust //! Optional swapdex integration: attribute each session to the ACCOUNT profile //! that was active when it started, by reading swapdex's switch timeline //! (read-only). No swapdex on the machine -> no events -> no badges, silently. //! //! The join mirrors swapdex's own `sessions` attribution: a session belongs to //! the last `use`/`restore` event for its tool with ts <= session start. A //! session that predates every switch stays unattributed (None) - a missing //! badge, never a guess. use serde_json::Value; use std::path::PathBuf; pub struct SwitchEvent { pub ts: i64, pub tool: String, pub account: String, } /// swapdex's timeline location (same `dirs::data_dir` convention swapdex /// uses). `SESSIONWIKI_SWAPDEX_TIMELINE` overrides for tests. fn timeline_path() -> Option { if let Some(p) = std::env::var_os("SESSIONWIKI_SWAPDEX_TIMELINE") { return Some(p.into()); } Some(dirs::data_dir()?.join("swapdex").join("timeline.jsonl")) } /// Parse the timeline defensively: only `use`/`restore` events count (they are /// the moments the active account changed), malformed lines are skipped. pub fn load_events() -> Vec { let Some(path) = timeline_path() else { return Vec::new(); }; let Ok(text) = std::fs::read_to_string(path) else { return Vec::new(); }; let mut out = Vec::new(); // Defense against a producer-contract change or a hand-edited file: // swapdex bounds the timeline to ~1000 events, but cap our read anyway // (newest entries are at the tail, which is the part attribution needs). let lines: Vec<&str> = text.lines().collect(); let tail = lines.len().saturating_sub(4000); for line in &lines[tail..] { let Ok(v) = serde_json::from_str::(line) else { continue; }; if !matches!(v["action"].as_str(), Some("use") | Some("restore")) { continue; } if let (Some(ts), Some(tool), Some(account)) = (v["ts"].as_i64(), v["tool"].as_str(), v["account"].as_str()) { out.push(SwitchEvent { ts, tool: tool.to_string(), // Strip control chars at the source: every consumer (CLI // badge, web, JSON) then gets a terminal-safe name. account: account.chars().filter(|c| !c.is_control()).collect(), }); } } out } /// The profile active when a session of `tool` started, or None (no events for /// that tool before the start - including the no-swapdex case). pub fn account_for( events: &[SwitchEvent], tool: &str, started_rfc3339: Option<&str>, ) -> Option { let started = chrono::DateTime::parse_from_rfc3339(started_rfc3339?) .ok()? .timestamp(); events .iter() .filter(|e| e.tool == tool && e.ts <= started) .max_by_key(|e| e.ts) .map(|e| e.account.clone()) } /// Fill `account` on freshly queried rows. One timeline read per query call; /// zero work when no swapdex timeline exists. pub fn annotate<'a, I: IntoIterator>(rows: I) { let events = load_events(); if events.is_empty() { return; } for r in rows { r.account = account_for(&events, &r.tool, r.started.as_deref()); } } #[cfg(test)] mod tests { use super::*; fn ev(ts: i64, tool: &str, account: &str) -> SwitchEvent { SwitchEvent { ts, tool: tool.into(), account: account.into(), } } #[test] fn attributes_to_the_last_switch_before_start() { let events = vec![ ev(100, "codex", "work"), ev(200, "codex", "personal"), ev(150, "claude-code", "work"), ]; // codex session started at t=250 -> personal (the t=200 switch). let started = chrono::DateTime::from_timestamp(250, 0) .unwrap() .to_rfc3339(); assert_eq!( account_for(&events, "codex", Some(&started)).as_deref(), Some("personal") ); // claude session at t=250 -> work (its own tool's events only). assert_eq!( account_for(&events, "claude-code", Some(&started)).as_deref(), Some("work") ); // A session that predates every switch stays unattributed. let early = chrono::DateTime::from_timestamp(50, 0) .unwrap() .to_rfc3339(); assert_eq!( account_for(&events, "codex", Some(&early)).as_deref(), None ); } } ``` -------------------------------- ### render_window: render bounded session window as Markdown Source: https://docs.rs/sessionwiki/latest/src/sessionwiki/window.rs.html Renders the bounded window for a session as a Markdown string. Builds a header with title, tool, project (or "(no project)"), start time, and message count; then renders each message via render_msg. When budget_chars is set, keeps the most recent turns within the budget by walking backward, and inserts an omission marker if earlier turns were dropped. Appends a footer line pointing to the full session with `sessionwiki show --full`. ```rust pub fn render_window(session: &Session, opts: &WindowOpts) -> String { let started = session .started .map(|d| d.format("%Y-%m-%d %H:%M").to_string()) .unwrap_or_else(|| "?".into()); let header = format!( "## {} [{}] · {} · {} · {} messages", session.title, session.tool, if session.project.is_empty() { "(no project)" } else { &session.project }, started, session.messages.len() ); let blocks: Vec = session .messages .iter() .map(|m| render_msg(m, opts)) .collect(); let (kept, omitted) = match opts.budget_chars { None => (blocks, 0usize), Some(budget) => { // Keep the most recent turns within the budget (walk backward). let mut acc = 0usize; let mut taken: Vec = Vec::new(); for b in blocks.iter().rev() { if acc + b.len() > budget && !taken.is_empty() { break; } acc += b.len(); taken.push(b.clone()); } let omitted = blocks.len() - taken.len(); taken.reverse(); (taken, omitted) } }; let mut out = String::new(); out.push_str(&header); out.push_str("\n\n"); if omitted > 0 { out.push_str(&format!("[… {omitted} earlier turn(s) omitted …]\n\n")); } out.push_str(&kept.join("\n\n")); out.push_str(&format!( "\n\n→ full: sessionwiki show {} --full", session.id )); out } ``` -------------------------------- ### account_for Source: https://docs.rs/sessionwiki/latest/sessionwiki/account_link/fn.account_for.html Returns the active profile when a session of the given tool started, or None if no events exist for that tool before the start time. ```APIDOC ## Function: account_for ### Description Returns the active profile when a session of the specified tool started. Returns None if there are no events for that tool before the start time, including the case where no swapdex exists. ### Signature ```rust pub fn account_for( events: &[SwitchEvent], tool: &str, started_rfc3339: Option<&str>, ) -> Option ``` ### Parameters - **events** (&[SwitchEvent]) - A slice of SwitchEvent representing the event history. - **tool** (&str) - The name of the tool for which to find the active profile. - **started_rfc3339** (Option<&str>) - Optional start time in RFC3339 format. If provided, only events before this time are considered. ### Returns - **Option** - The active profile when the session started, or None if no events exist for that tool before the start time. ``` -------------------------------- ### account_for() implementation with session events and optional start Source: https://docs.rs/sessionwiki/latest/src/sessionwiki/account_link.rs.html Defines an AccountLink enum and an account_for function that takes a slice of events with session and payload, an account name, and an optional start time. It returns an Option. ```rust pub enum AccountLink { Same, Link { destination: String }, } pub struct Event { session: String, payload: Payload, } pub enum Payload { Account { name: String, link: Option, }, } pub fn account_for( events: &[Event], account: &str, start: Option, ) -> Option { events .iter() .filter_map(|event| match event { Event { session: _, payload: Payload::Event { name, link }, } if name == account && start.map_or(true, |s| event.time >= s) => { Some(link.clone()) } _ => None, }) .next() } ``` -------------------------------- ### SQLite Session Parsing Test Source: https://docs.rs/sessionwiki/latest/src/sessionwiki/adapters/opencode.rs.html Unit test setup for parsing a session from a SQLite database. ```rust #[cfg(test)] mod tests { use super::{make_key, OpenCode}; use crate::adapters::Adapter; use chrono::DateTime; use rusqlite::Connection; #[test] fn parses_a_sqlite_session() { let db = std::env::temp_dir().join(format!("sw-oc-{}.db", std::process::id())); let _ = std::fs::remove_file(&db); { let conn = Connection::open(&db).unwrap(); conn.execute_batch( "CREATE TABLE session(id TEXT PRIMARY KEY, parent_id TEXT, project_id TEXT, directory TEXT, title TEXT, time_created INTEGER, time_updated INTEGER); ``` -------------------------------- ### session_start function Source: https://docs.rs/sessionwiki/latest/src/sessionwiki/hook.rs.html Initializes the session by reading input from stdin, canonicalizing the working directory, and retrieving project briefs from the index. ```rust pub fn session_start() { let mut buf = String::new(); // bounded read so a never-closing/huge stdin cannot hang the 10s hook let _ = std::io::stdin().take(64 * 1024).read_to_string(&mut buf); let Some((cwd, session_id)) = validated_input(&buf) else { return; }; // canonicalize the launch dir; only an existing absolute dir is briefed let Ok(canon) = std::fs::canonicalize(&cwd) else { return; }; let canon = canon.to_string_lossy().into_owned(); let Ok(conn) = crate::index::open() else { return; // DB locked/corrupt -> empty, exit 0 }; let Ok(rows) = crate::index::project_brief(&conn, &canon, 5) else { return; }; let nonce = crate::util::short_id(&session_id); let entries: Vec = rows .iter() .filter(|r| r.session_id != session_id) // never brief the current session .map(|r| { let files = crate::index::files_for(&conn, &r.session_id) .unwrap_or_default() .iter() .take(3) .map(|p| relativize(p, &canon)) .collect(); let date = r .started .as_deref() .and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok()) .map(|d| d.format("%Y-%m-%d").to_string()) .unwrap_or_default(); BriefEntry { date, tool: r.tool.clone(), files, title: r.title.clone(), } }) .collect(); print!("{}", render_brief(&entries, &nonce)); } ``` -------------------------------- ### Print Native Resume Instructions Source: https://docs.rs/sessionwiki/latest/src/sessionwiki/commands.rs.html Checks for a native resume command for the specified tool and prints instructions for the user to resume the session. ```rust fn print_native_resume(tool: &str, path: &std::path::Path, target: &std::path::Path) { if let Some(info) = crate::resume::for_session(tool, path, &target.to_string_lossy()) { println!("resume it there:"); println!( " {}", cyan(&format!( "cd {} && {}", target.display(), info.command_line() )) ); } } ``` -------------------------------- ### Split History into Runs Source: https://docs.rs/sessionwiki/latest/src/sessionwiki/adapters/aider.rs.html Splits the content of an aider history file into a vector of Run structs based on the '# aider chat started at' header. ```rust fn split_runs(content: &str) -> Vec { let mut runs: Vec = Vec::new(); let mut cur: Option = None; for line in content.lines() { if let Some(ts) = line.strip_prefix("# aider chat started at ") { if let Some(r) = cur.take() { runs.push(r); } cur = Some(Run { started: parse_aider_ts(ts), body: String::new(), }); } else if let Some(r) = cur.as_mut() { r.body.push_str(line); r.body.push('\n'); } // lines before the first header (cur == None) are dropped } if let Some(r) = cur.take() { runs.push(r); } runs } ``` -------------------------------- ### serve Source: https://docs.rs/sessionwiki/latest/sessionwiki/web/fn.serve.html Documentation for the `serve` function in the `sessionwiki::web` module. ```APIDOC ## serve ### Description Local read-only viewer over the index. By default it does not sync, so it stays snappy and can run while a CLI index pass is in progress (WAL allows concurrent readers) - which means sessions created after the last `list`/`search` won’t show until the index is refreshed. Pass `sync` to bring the index up to date once before serving. ### Function Signature `pub fn serve(port: u16, no_open: bool, sync: bool) -> Result<()>` ### Parameters - **port** (u16) - Required - The port number on which the server will listen. - **no_open** (bool) - Required - If true, the browser will not be automatically opened. - **sync** (bool) - Required - If true, the index is brought up to date once before serving. ### Return Value - **Result<()>** - Returns `Ok(())` on success, or an error if the server fails to start. ``` -------------------------------- ### Test grouping consecutive lines by commit Source: https://docs.rs/sessionwiki/latest/src/sessionwiki/blame.rs.html Test that group_runs groups consecutive lines by commit, producing Run entries with start and end line numbers. ```rust fn groups_consecutive_lines_by_commit() { let lines = vec![ LineBlame { line: 1, commit: "a".into(), author_time: 10, }, LineBlame { line: 2, commit: "a".into(), author_time: 10, }, LineBlame { line: 3, commit: "b".into(), author_time: 20, }, LineBlame { line: 4, commit: "a".into(), author_time: 10, }, ]; let runs = group_runs(&lines); assert_eq!( runs, vec![ Run { start: 1, end: 2, commit: "a".into(), author_time: 10 }, Run { start: 3, end: 3, commit: "b".into(), author_time: 20 }, Run { start: 4, end: 4, commit: "a".into(), author_time: 10 }, ] ); } ``` -------------------------------- ### Resolve by native ID Source: https://docs.rs/sessionwiki/latest/src/sessionwiki/index.rs.html Matches rows where the native ID (derived from the path) starts with the provided prefix. Uses a coarse path filter followed by an exact native ID check. ```rust fn resolve_by_native_id(conn: &Connection, prefix: &str) -> Result> { let mut stmt = conn.prepare(&format!( "SELECT {RESOLVE_COLS}, {SUMMARY_SQL}, {TAGS_SQL}, (archived_at IS NOT NULL) FROM files f WHERE path LIKE ?1 ESCAPE '\\' LIMIT 500", ))?; let pattern = format!("%{}%", escape_like(prefix)); let want = prefix.to_ascii_lowercase(); let rows = stmt.query_map(params![pattern], map_resolve_row)?; let mut out = Vec::new(); for row in rows { let row = row?; if native_id_of(&row.path).is_some_and(|n| n.starts_with(&want)) { out.push(row); if out.len() >= 10 { break; } } } Ok(out) } ``` -------------------------------- ### Collapse per-line blame into contiguous runs with the same commit Source: https://docs.rs/sessionwiki/latest/src/sessionwiki/blame.rs.html Group consecutive `LineBlame` entries that share the same commit and whose line numbers are contiguous. The resulting `Run` records the start and end line and the commit reference. ```rust pub fn group_runs(lines: &[LineBlame]) -> Vec { let mut runs: Vec = Vec::new(); for lb in lines { if let Some(last) = runs.last_mut() { if last.commit == lb.commit && lb.line == last.end + 1 { last.end = lb.line; continue; } } runs.push(Run { start: lb.line, end: lb.line, commit: lb.commit.clone(), author_time: lb.author_time, }); } runs } ``` -------------------------------- ### detect(project: &str) -> Option<&'static str> Source: https://docs.rs/sessionwiki/latest/sessionwiki/adapters/harness/fn.detect.html Returns the harness tag for a session ("oh-my-claudecode" / "oh-my-openagent") or None, based on the project's orchestration directory. Transcript text is intentionally not consulted. ```APIDOC ## Function: detect ### Description Returns the harness tag for a session ("oh-my-claudecode" / "oh-my-openagent") or None, based on the project's orchestration directory. Transcript text is intentionally not consulted. ### Signature pub fn detect(project: &str) -> Option<&'static str> ### Parameters - **project** (&str) - Required - The project's orchestration directory path. ### Returns - **Option<&'static str>** - The harness tag ("oh-my-claudecode" / "oh-my-openagent") or None if not detected. ### Notes - Transcript text is intentionally not consulted. ``` -------------------------------- ### Retrieve ChatGPT thread URL for a task Source: https://docs.rs/sessionwiki/latest/src/sessionwiki/adapters/prodex.rs.html Locates the newest JSON session file in the 'sessions' directory relative to the provided task path and extracts the thread URL. It enforces that the URL must start with 'https://chatgpt.com/'. ```rust pub fn thread_url_for_task(task_path: &Path) -> Option { let sessions = task_path.parent()?.parent()?.join("sessions"); let mut candidates: Vec<(std::time::SystemTime, String)> = Vec::new(); for e in std::fs::read_dir(sessions).ok()?.flatten() { let p = e.path(); if p.extension().is_none_or(|x| x != "json") { continue; } let Ok(v) = serde_json::from_slice::(&std::fs::read(&p).ok()?) else { continue; }; let Some(url) = v["thread"].as_str() else { continue; }; if !url.starts_with("https://chatgpt.com/") { continue; } let mtime = e .metadata() .and_then(|m| m.modified()) .unwrap_or(std::time::SystemTime::UNIX_EPOCH); candidates.push((mtime, url.to_string())); } candidates.sort_by_key(|(t, _)| *t); candidates.pop().map(|(_, u)| u) } ``` -------------------------------- ### serve function for local web server Source: https://docs.rs/sessionwiki/latest/src/sessionwiki/web.rs.html Initializes a local HTTP server to serve session data. It supports an optional sync flag to update the index before serving and includes browser auto-opening logic. ```rust pub fn serve(port: u16, no_open: bool, sync: bool) -> Result<()> { let mut conn = index::open()?; if sync { index::sync(&mut conn, None)?; } let addr = format!("127.0.0.1:{port}"); let server = Server::http(&addr).map_err(|e| anyhow::anyhow!("bind {addr}: {e}"))?; let url = format!("http://{addr}"); println!("sessionwiki web: {url}"); if !sync { println!("(read-only view; run `sessionwiki list` or `web --sync` to refresh the index)"); } let wsl = is_wsl(); if should_open_browser(no_open, wsl) { open_browser(&url); } else if wsl && !no_open { println!("(WSL detected - open the URL above in your browser; auto-open is off here to avoid stray windows)"); } for request in server.incoming_requests() { // Defend against DNS rebinding: a page on a malicious site can rebind its // own hostname to 127.0.0.1 and have the browser fetch this server, then // read your sessions same-origin. Such a request carries the attacker's // hostname in the Host header, so only serve requests addressed to the // loopback name we actually bound. let host = request .headers() .iter() .find(|h| h.field.equiv("Host")) .map(|h| h.value.as_str().to_string()); let origin = request .headers() .iter() .find(|h| h.field.equiv("Origin")) .map(|h| h.value.as_str().to_string()); // Host check defeats DNS rebinding (an attacker hostname rebound to // 127.0.0.1 carries its name in Host). The Origin check is a second layer // against a plain cross-origin fetch from a malicious site, which the // browser stamps with its Origin - we don't rely on the same-origin // policy alone. if !host_matches(host.as_deref(), port) || !origin_ok(origin.as_deref(), port) { let _ = request.respond(Response::from_string("forbidden").with_status_code(403)); continue; } let url = request.url().to_string(); let (path, query) = url.split_once('?').unwrap_or((url.as_str(), "")); let result = match path { "/" => html(INDEX_HTML), "/api/stats" => api_stats(&conn), "/api/sessions" => api_sessions(&conn, query), "/api/search" => api_search(&conn, query), "/api/trace" => api_trace(&conn, query), "/api/file" => api_file(&conn, query), "/api/projects" => api_projects(&conn), p if p.starts_with("/api/related/") => { api_related(&conn, p.trim_start_matches("/api/related/")) } p if p.starts_with("/api/session/") => { api_session(&conn, p.trim_start_matches("/api/session/")) } _ => Ok(Response::from_string("not found") .with_status_code(404) .boxed()), }; let response = match result { Ok(r) => r, Err(e) => Response::from_string(json!({ "error": e.to_string() }).to_string()) .with_status_code(500) .boxed(), }; let _ = request.respond(response); } Ok(()) } ``` -------------------------------- ### title_from_messages: Pick session title from first user message Source: https://docs.rs/sessionwiki/latest/src/sessionwiki/adapters/mod.rs.html Returns the first user message that is not harness boilerplate (does not start with '<' and is not empty), truncated to 80 characters. Falls back to "(no user prompt)" if none found. ```rust pub(crate) fn title_from_messages(messages: &[crate::model::Message]) -> String { messages .iter() .find(|m| { m.role == crate::model::Role::User && !m.text.trim_start().starts_with('<') && !m.text.trim().is_empty() }) .map(|m| crate::util::truncate(&m.text, 80)) .unwrap_or_else(|| "(no user prompt)".into()) } ``` -------------------------------- ### report: Build StoreReport from adapter Source: https://docs.rs/sessionwiki/latest/src/sessionwiki/adapters/mod.rs.html Returns a `StoreReport` for the adapter's store. For shared stores (e.g. SQLite), presence is checked via the store itself before the root-exists gate; size is the backing files, count is the sessions, and time span comes from per-session change-tokens. For filesystem stores, it scans the root directory, counting files and summing their sizes, and returns `None` if the root doesn't exist or contains no session files. ```rust pub fn report(adapter: &dyn Adapter) -> Option { let root = adapter.root()?; // Shared store (e.g. SQLite): presence comes from the store itself (the db // can live outside `root` via OPENCODE_DB), so this is checked before the // root-exists gate. Size is the backing files, count is the sessions, and // the time span comes from the per-session change-tokens. if let Some(store) = adapter.store() { if store.keys.is_empty() { return None; // present but no sessions yet - not worth a scan row } let bytes = store .files .iter() .filter_map(|f| f.metadata().ok()) .map(|m| m.len()) .sum(); // Tokens are last-activity ms; drop the 0 sentinel (a missing timestamp) // so it cannot backdate the span to 1970. let oldest = store .keys .iter() .map(|(_, t)| *t) .filter(|t| *t > 0) .min() .and_then(DateTime::from_timestamp_millis); let newest = store .keys .iter() .map(|(_, t)| *t) .filter(|t| *t > 0) .max() .and_then(DateTime::from_timestamp_millis); return Some(StoreReport { tool: adapter.name(), root, files: store.keys.len(), bytes, oldest, newest, }); } if !root.exists() { return None; } let files = adapter.discover().files; let mut bytes: u64 = 0; let mut oldest: Option> = None; let mut newest: Option> = None; let mut count = 0usize; for f in &files { let Ok(meta) = f.metadata() else { continue }; count += 1; bytes += meta.len(); if let Ok(modified) = meta.modified() { let t: DateTime = modified.into(); if oldest.is_none_or(|o| t < o) { oldest = Some(t); } if newest.is_none_or(|n| t > n) { newest = Some(t); } } } if count == 0 { return None; // root exists but holds no session files } Some(StoreReport { tool: adapter.name(), root, files: count, bytes, oldest, newest, }) } ``` -------------------------------- ### Open URL in System Browser Source: https://docs.rs/sessionwiki/latest/src/sessionwiki/web.rs.html Spawns a system command to open the provided URL, selecting the appropriate tool based on the target operating system. ```rust fn open_browser(url: &str) { #[cfg(target_os = "macos")] let cmd = "open"; #[cfg(target_os = "windows")] let cmd = "explorer"; #[cfg(not(any(target_os = "macos", target_os = "windows")))] let cmd = "xdg-open"; let _ = std::process::Command::new(cmd) .arg(url) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .spawn(); } ``` -------------------------------- ### discovery_rows: Serialize neutralized discovery rows with exclusion and limit Source: https://docs.rs/sessionwiki/latest/src/sessionwiki/mcp.rs.html Converts SessionRow values to neutralized JSON, dropping rows whose session_id starts with the exclude prefix and capping to the given limit. The absolute path is never serialized, only the native_id UUID. ```rust fn discovery_rows(rows: &[crate::index::SessionRow], exclude: Option<&str>, limit: usize) -> Value { let out: Vec = rows .iter() .filter(|r| match exclude { Some(e) if !e.is_empty() => !r.session_id.starts_with(e), _ => true, }) .take(limit) .map(|r| { let mut v = serde_json::to_value(r).unwrap_or_else(|_| json!({})); neutralize_row(&mut v); v }) .collect(); json_result(out) } ``` -------------------------------- ### OpenCode Adapter Implementation Source: https://docs.rs/sessionwiki/latest/src/sessionwiki/adapters/opencode.rs.html Full implementation of the OpenCode adapter for sessionwiki, handling both SQLite and legacy JSON storage. It uses the Adapter trait and provides methods for discovery and session parsing. ```rust 1use super::{dedup_paths, ok_or_flag, title_from_messages, Adapter, Discovered, Store}; 2use crate::model::{Message, Role, Session}; 3use crate::util::{short_id, truncate}; 4use anyhow::{Context, Result}; 5use chrono::{DateTime, Utc}; 6use rusqlite::{params, Connection, OpenFlags}; 7use serde_json::Value; 8use std::collections::HashMap; 9use std::path::{Path, PathBuf}; 10use walkdir::WalkDir; 11 12/// OpenCode (sst/opencode) keeps its data under `$XDG_DATA_HOME/opencode` 13/// (default `~/.local/share/opencode`, the same on macOS - it uses 14/// xdg-basedir, not the platform data dir). Two storage generations exist: 15/// /// - **SQLite (v1.2.0+, the current default):** `opencode.db` (and /// `opencode-.db`) with `session` / `message` / `part` tables. /// This is the source of truth on any recent install, so when a db is /// present we index it as a shared [`Store`] and ignore the JSON below. /// - **Legacy JSON:** `storage/session|message|part/**.json`, one file each. /// Still read on pre-1.2.0 installs that never migrated. pub struct OpenCode; impl Adapter for OpenCode { fn name(&self) -> &'static str { "opencode" } fn root(&self) -> Option { // The opencode data dir - exists for both the SQLite and JSON layouts, // so `scan` presence and deletion reconciliation work either way. data_dir() } fn discover(&self) -> Discovered { // Legacy JSON only. When a db exists, `store()` is Some and the indexer // never calls this (the JSON files are stale post-migration leftovers). let Some(dir) = data_dir() else { return Vec::new().into(); }; let sessions = dir.join("storage").join("session"); if !sessions.exists() { return Vec::new().into(); // no legacy layout - normal } let mut had_error = false; let files = WalkDir::new(sessions) .min_depth(2) .max_depth(2) .into_iter() .filter_map(|e| ok_or_flag(e, &mut had_error)) .filter(|e| e.file_type().is_file()) .filter(|e| e.path().extension().is_some_and(|x| x == "json")) .map(|e| e.into_path()) .collect(); Discovered { files, had_error } } fn store(&self) -> Option { let dbs: Vec = db_paths().into_iter().filter(|p| p.is_file()).collect(); if dbs.is_empty() { return None; } let mut keys: Vec<(String, i64)> = Vec::new(); let mut had_error = false; for db in &dbs { let conn = match open_ro(db) { Ok(c) => c, Err(_) => { had_error = true; continue; } }; let mut stmt = match conn.prepare("SELECT id, time_updated, time_created FROM session") { Ok(s) => s, Err(_) => { had_error = true; continue; } }; let rows = stmt.query_map([], |r| { let id: String = r.get(0)?; let updated: Option = r.get(1)?; let created: Option = r.get(2)?; Ok((id, updated.or(created).unwrap_or(0))) }); match rows { Ok(rows) => { for (id, token) in rows.flatten() { keys.push((make_key(db, &id), token)); } } Err(_) => had_error = true, } } Some(Store { keys, files: dbs, had_error, }) } fn parse_key(&self, key: &str) -> Result { let (db, sid) = key.split_once(KEY_SEP).context("malformed opencode key")?; let conn = open_ro(Path::new(db)).with_context(|| format!("open {db}"))?; let (project, raw_title, created, updated): (String, String, Option, Option) = conn.query_row( "SELECT directory, title, time_created, time_updated FROM session WHERE id = ?1", params![sid], |r| { Ok(( r.get::<_, Option>(0)?.unwrap_or_default(), r.get::<_, Option>(1)?.unwrap_or_default(), r.get(2)?, r.get(3)?, )) }, )?; ``` -------------------------------- ### neutralize_row: Sanitize serialized SessionRow fields Source: https://docs.rs/sessionwiki/latest/src/sessionwiki/mcp.rs.html Neutralizes untrusted free-text fields (title, preview, summary, account, tags) and redacts the project path. Leaves structured fields (id, native_id, tool, kind, started, msgs, archived) unchanged because native_id is a UUID and the absolute path is never serialized. ```rust fn neutralize_row(v: &mut Value) { let Some(obj) = v.as_object_mut() else { return; }; for key in ["title", "preview", "summary", "account"] { if let Some(s) = obj.get(key).and_then(Value::as_str) { let n = crate::commands::neutralize_field(s); obj.insert(key.into(), json!(n)); } } if let Some(p) = obj.get("project").and_then(Value::as_str) { let s = safe_project(p); obj.insert("project".into(), json!(s)); } if let Some(tags) = obj.get("tags").and_then(Value::as_array) { let t: Vec = tags .iter() .filter_map(Value::as_str) .map(|s| json!(crate::commands::neutralize_field(s))) .collect(); obj.insert("tags".into(), json!(t)); } } ``` -------------------------------- ### impl ResumeInfo Source: https://docs.rs/sessionwiki/latest/sessionwiki/resume/struct.ResumeInfo.html The ResumeInfo struct provides a method to generate the command line string for reopening a session. ```APIDOC ## Struct ResumeInfo ### Description Represents how to reopen a session in its original tool. Built from the session file path, since every supported tool encodes its native session id in the filename. ### Fields - **program**: `&'static str` - The program to run. - **args**: `Vec` - Arguments for the program. - **cwd**: `Option` - The working directory to run in. None when the session has no usable cwd. - **verified_cwd**: `bool` - Whether `cwd` is trusted as genuinely belonging to this session. A caller must not launch the tool automatically when false. - **note**: `Option` - An optional note. ### Methods #### `command_line(&self) -> String` Returns the full command line string for reopening the session. #### Description Constructs the command line by combining the program and arguments into a single string, suitable for execution. ``` -------------------------------- ### Attributing a commit to a session and resolving ambiguity Source: https://docs.rs/sessionwiki/latest/src/sessionwiki/blame.rs.html Map a commit's author-time to the session most likely behind it. Prefer sessions whose [started, ended] window contains the commit time; otherwise, pick the most recent session that ended at or before the commit time within LAG_WINDOW_SECS. If this yields multiple sessions, returns Ambiguous. If none, Unattributed. Uses project_ancestor to break ties. ```rust pub fn attribute_commit( author_time: i64, repo_path: &str, candidates: &[TouchingSession], ) -> Attribution { let containing: Vec<&TouchingSession> = candidates .iter() .filter(|s| { matches!((s.started, s.ended), (Some(a), Some(b)) if a <= author_time && author_time <= b) }) .collect(); if containing.len() == 1 { return Attribution::Confident(containing[0].clone()); } if containing.len() > 1 { return disambiguate(containing, repo_path); } // (b) most-recent-before within the lag window let mut before: Vec<&TouchingSession> = candidates .iter() ``` -------------------------------- ### for_session(tool: &str, path: &Path, project: &str) -> Option Source: https://docs.rs/sessionwiki/latest/sessionwiki/resume/fn.for_session.html Retrieves resume information for a session identified by the given tool, path, and project. Returns None if no resume information is available. ```APIDOC ## Function: for_session ### Description Retrieves resume information for a session identified by the given tool, path, and project. Returns None if no resume information is available. ### Signature pub fn for_session(tool: &str, path: &Path, project: &str) -> Option ### Parameters - **tool** (&str) - Required - The name of the tool. - **path** (&Path) - Required - The path to the session. - **project** (&str) - Required - The name of the project. ### Return Value - **Option** - Returns Some(ResumeInfo) if resume information is found, otherwise None. ``` -------------------------------- ### show Source: https://docs.rs/sessionwiki/latest/sessionwiki/commands/fn.show.html The `show` function displays session information based on the provided parameters. It is part of the `sessionwiki::commands` module and is publicly accessible. ```APIDOC ## Function `show` ### Description Displays session information based on the provided parameters. ### Signature ```rust pub fn show( id: &str, full: bool, json: bool, outline: bool, window: bool, budget: Option, live: bool, no_sync: bool, ) -> Result<()> ``` ### Parameters - **id** (`&str`) - The session identifier. - **full** (`bool`) - Whether to show full details. - **json** (`bool`) - Whether to output in JSON format. - **outline** (`bool`) - Whether to show an outline. - **window** (`bool`) - Whether to show in a window. - **budget** (`Option`) - Optional budget limit. - **live** (`bool`) - Whether to use live data. - **no_sync** (`bool`) - Whether to disable synchronization. ### Return Value Returns `Result<()>` indicating success or failure. ``` -------------------------------- ### open() Source: https://docs.rs/sessionwiki/latest/sessionwiki/index/fn.open.html Opens a connection to the session index. Returns a Result containing a Connection on success. ```APIDOC ## Function: open ### Description Opens a connection to the session index. ### Signature ```rust pub fn open() -> Result ``` ### Returns - `Result` - A result containing a `Connection` on success, or an error on failure. ``` -------------------------------- ### pub fn report(adapter: &dyn Adapter) -> Option Source: https://docs.rs/sessionwiki/latest/sessionwiki/adapters/fn.report.html Returns a filesystem-only summary of a store, used by scan. No parsing involved. ```APIDOC ## Function: report ### Description Returns a filesystem-only summary of a store, used by scan. No parsing involved. ### Signature pub fn report(adapter: &dyn Adapter) -> Option ### Parameters - **adapter** (&dyn Adapter) - Required - A reference to a trait object implementing the Adapter trait. ### Return Value - **Option** - An optional StoreReport containing the summary, or None if not available. ``` -------------------------------- ### ResumeInfo struct and command_line method Source: https://docs.rs/sessionwiki/latest/src/sessionwiki/resume.rs.html Defines the structure for session resumption information and provides a helper to generate the command line string for the tool. ```rust 1use std::path::{Path, PathBuf}; 2 3/// How to reopen a session in its original tool. Built from the session 4/// file path, since every supported tool encodes its native session id 5/// in the filename. 6pub struct ResumeInfo { 7 pub program: &'static str, 8 pub args: Vec, 9 /// The tool resolves sessions relative to the project, so the command 10 /// should run there. None when the session has no usable cwd. 11 pub cwd: Option, 12 /// Whether `cwd` is trusted as genuinely belonging to this session. A 13 /// session file is untrusted input and can claim any directory, so a caller 14 /// that *launches* the tool there (loading that dir's CLAUDE.md/.mcp.json/ 15 /// etc.) must not do so automatically when this is false. 16 pub verified_cwd: bool, 17 pub note: Option, 18} 19 20impl ResumeInfo { 21 pub fn command_line(&self) -> String { 22 let mut s = String::from(self.program); 23 for a in &self.args { 24 s.push(' '); 25 s.push_str(a); 26 } 27 s 28 } 29} ```