2026-07-12 00:28:17 +08:00
// Query and attune sandbox helpers for the Core package.
2026-07-12 00:37:32 +08:00
import { readLines , fs , path } from './db.ts' ;
2026-05-31 03:29:41 +08:00
2026-07-12 00:37:32 +08:00
type SqliteDb = any ;
type DbRow = Record < string , any >;
interface QueryOptions extends Record < string , any > {
limit? : number ;
sessionId? : string ;
sessions? : string [];
project? : string ;
after? : string ;
before? : string ;
cwd? : string ;
branch? : string ;
source? : string ;
includeMeta? : boolean ;
query? : string ;
projectLimit? : number ;
memoryLimit? : number ;
}
interface ColumnAliases {
sessionId : string ;
project : string ;
timestamp : string ;
branch : string ;
source? : string ;
}
interface RememberInput {
path : string ;
session_id? : string ;
message_start? : string ;
message_end? : string ;
summary : string ;
project? : string ;
anchors? : unknown ;
}
interface ForgetInput {
id : string ;
reason : string ;
}
function normalizeOpts ( optsOrScalar : QueryOptions | string | number | null | undefined , scalarKey = 'sessionId' ) : QueryOptions {
2026-06-02 01:36:47 +08:00
if ( optsOrScalar == null ) return {};
if ( typeof optsOrScalar === 'string' ) return { [ scalarKey ] : optsOrScalar };
if ( typeof optsOrScalar === 'number' ) return { limit : optsOrScalar };
return optsOrScalar ;
}
2026-07-12 00:37:32 +08:00
function buildWhere ( opts : QueryOptions , aliases : ColumnAliases ) {
const clauses : string [] = [];
const params : any [] = [];
2026-06-02 01:36:47 +08:00
if ( opts . sessionId ) { clauses . push ( ` ${ aliases . sessionId } = ?` ); params . push ( opts . sessionId ); }
if ( opts . sessions ? . length ) {
clauses . push ( ` ${ aliases . sessionId } IN ( ${ opts . sessions . map (() => '?' ). join ( ',' ) } )` );
params . push (... opts . sessions );
}
if ( opts . project ) { clauses . push ( ` ${ aliases . project } LIKE ?` ); params . push ( opts . project ); }
if ( opts . after ) { clauses . push ( ` ${ aliases . timestamp } > ?` ); params . push ( opts . after ); }
if ( opts . before ) { clauses . push ( ` ${ aliases . timestamp } < ?` ); params . push ( opts . before ); }
if ( opts . branch ) { clauses . push ( ` ${ aliases . branch } = ?` ); params . push ( opts . branch ); }
2026-06-17 23:40:36 +08:00
if ( opts . source && opts . source !== 'all' && aliases . source ) {
clauses . push ( `COALESCE( ${ aliases . source } , 'claude') = ?` );
params . push ( opts . source );
}
2026-06-02 01:36:47 +08:00
return { where : clauses.length ? clauses . join ( ' AND ' ) : '1=1' , params };
}
2026-06-04 15:49:57 +08:00
const BASH_EXIT_PAT = 'Exit code %' ;
2026-06-02 09:19:46 +08:00
2026-07-12 00:37:32 +08:00
function assertReadOnlySql ( sql : unknown ) : void {
2026-06-10 02:05:27 +08:00
const text = String ( sql || '' ). trim ();
if ( ! /^(SELECT|WITH)\b/i . test ( text )) {
throw new Error ( 'sql() only supports read-only SELECT/WITH queries' );
}
if ( /\b(INSERT|UPDATE|DELETE|REPLACE|CREATE|DROP|ALTER|PRAGMA|VACUUM|ATTACH|DETACH)\b/i . test ( text )) {
throw new Error ( 'sql() only supports read-only SELECT/WITH queries' );
}
}
2026-06-10 03:50:38 +08:00
const CJK_TEXT_RE = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
2026-07-12 00:37:32 +08:00
function assertEnglishMemoryText ( value : unknown , label : string ) : void {
2026-06-10 03:50:38 +08:00
const text = String ( value || '' );
if ( ! text . trim ()) return ;
if ( CJK_TEXT_RE . test ( text )) {
const requirement = label . includes ( 'query' ) ? 'must use English terms' : 'must be written in English' ;
throw new Error ( ` ${ label } ${ requirement } ; translate user-language terms before using the memory layer` );
}
}
2026-07-12 00:37:32 +08:00
function buildSafeFtsQuery ( text : unknown ) : string {
2026-06-12 22:20:29 +08:00
const tokens = String ( text || '' ). match ( /[\p{Letter}\p{Number}]+/gu) || [];
return tokens
. slice ( 0 , 12 )
. map ( token => `" ${ token } "` )
. join ( ' ' );
}
2026-07-12 00:37:32 +08:00
function createQueryApi ( db : SqliteDb ) {
const q = ( sql : string , ... p : any []) => {
2026-06-10 02:05:27 +08:00
assertReadOnlySql ( sql );
return db . prepare ( sql ). all (... p );
};
2026-05-31 03:29:41 +08:00
2026-07-12 00:37:32 +08:00
const normalizeOverviewOpts = ( optsOrScalar : QueryOptions | string | number | null | undefined ) : QueryOptions => {
2026-06-10 03:04:11 +08:00
if ( optsOrScalar == null ) return {};
if ( typeof optsOrScalar === 'string' ) return { project : optsOrScalar };
if ( typeof optsOrScalar === 'number' ) return { limit : optsOrScalar };
return optsOrScalar ;
};
2026-07-12 00:37:32 +08:00
const search = ( text : string , opts : QueryOptions = {}) => {
2026-06-17 23:40:36 +08:00
const { limit = 20 , sessionId , project , after , before , cwd , source , includeMeta = false } = opts ;
2026-05-31 03:29:41 +08:00
let where = 'WHERE mf.text MATCH ?' ;
2026-07-12 00:37:32 +08:00
const filterParams : any [] = [];
2026-07-08 16:35:10 +08:00
if ( sessionId ) { where += ' AND mf.session_id=?' ; filterParams . push ( sessionId ); }
if ( project ) { where += ' AND s.project LIKE ?' ; filterParams . push ( project ); }
if ( after ) { where += ' AND m.timestamp>?' ; filterParams . push ( after ); }
if ( before ) { where += ' AND m.timestamp<?' ; filterParams . push ( before ); }
if ( cwd ) { where += ' AND m.cwd LIKE ?' ; filterParams . push ( cwd ); }
if ( source && source !== 'all' ) { where += " AND COALESCE(m.source, s.source, 'claude')=?" ; filterParams . push ( source ); }
2026-06-12 22:20:29 +08:00
if ( ! includeMeta ) where += ' AND COALESCE(m.is_meta,0)=0' ;
2026-07-08 16:35:10 +08:00
const stmt = db . prepare ( `
2026-06-17 23:40:36 +08:00
SELECT m.uuid,m.session_id,m.text,m.content_type,m.is_meta,m.role,m.timestamp,m.model,m.cwd,m.source as m_source,
2026-06-03 19:18:22 +08:00
s.id as s_id,s.title as s_title,s.project as s_project,s.started_at as s_started,
2026-06-17 23:40:36 +08:00
s.source as s_source,
2026-06-03 19:18:22 +08:00
rank
2026-05-31 03:29:41 +08:00
FROM messages_fts mf JOIN messages m ON m.uuid=mf.uuid LEFT JOIN sessions s ON s.id=m.session_id
2026-07-08 16:35:10 +08:00
${ where } ORDER BY rank LIMIT ?` );
2026-07-12 00:37:32 +08:00
const runMatch = ( matchText : string ) : DbRow [] => stmt . all ( matchText , ... filterParams , limit );
2026-07-08 16:35:10 +08:00
// Honor raw FTS5 syntax when the query is valid, but never crash on ordinary
// input (hyphens, punctuation) that FTS5 would parse as operators: fall back
// to safe per-token quoting, the same tokenization memories() uses.
let rows ;
try {
rows = runMatch ( text );
} catch {
const safe = buildSafeFtsQuery ( text );
rows = safe ? runMatch ( safe ) : [];
}
2026-07-12 00:37:32 +08:00
return rows . map (( r : DbRow ) => {
2026-06-12 22:20:29 +08:00
const metaClause = includeMeta ? '' : 'AND COALESCE(is_meta,0)=0' ;
2026-05-31 03:29:41 +08:00
const ctx = db . prepare (
2026-06-17 23:40:36 +08:00
`SELECT uuid,text,content_type,is_meta,role,timestamp,model,COALESCE(source, 'claude') as source FROM messages WHERE session_id=? AND uuid!=? ${ metaClause } ORDER BY ABS(JULIANDAY(timestamp)-JULIANDAY(?)) LIMIT 6`
2026-07-12 00:37:32 +08:00
). all ( r . session_id , r . uuid , r . timestamp ). sort (( a : DbRow , b : DbRow ) => a . timestamp < b . timestamp ? - 1 : 1 );
2026-06-17 23:40:36 +08:00
const sourceValue = r . m_source || r . s_source || 'claude' ;
2026-05-31 03:29:41 +08:00
return {
2026-06-17 23:40:36 +08:00
message : { uuid : r.uuid , text : r.text , content_type : r.content_type , is_meta : r.is_meta || 0 , role : r.role , timestamp : r.timestamp , model : r.model , cwd : r.cwd , source : sourceValue },
session : { id : r.s_id , title : r.s_title , project : r.s_project , started_at : r.s_started , source : r.s_source || sourceValue },
2026-06-03 19:18:22 +08:00
rank : r.rank ,
2026-05-31 03:29:41 +08:00
context : ctx ,
};
});
};
2026-07-12 00:37:32 +08:00
const context = ( uuid : string ) => {
2026-05-31 03:29:41 +08:00
const msg = db . prepare ( 'SELECT * FROM messages WHERE uuid=?' ). get ( uuid );
if ( ! msg ) return null ;
const session = db . prepare ( 'SELECT * FROM sessions WHERE id=?' ). get ( msg . session_id );
2026-07-12 00:37:32 +08:00
const chain : DbRow [] = [];
2026-05-31 03:29:41 +08:00
let cur = msg ;
while ( cur ? . parent_uuid ) { cur = db . prepare ( 'SELECT * FROM messages WHERE uuid=?' ). get ( cur . parent_uuid ); if ( cur ) chain . unshift ( cur ); }
2026-07-12 00:37:32 +08:00
const subagent = msg . agent_id ? db . prepare ( 'SELECT * FROM subagents WHERE agent_id=?' ). get ( msg . agent_id ) : null ;
2026-05-31 03:29:41 +08:00
let workflow = null ;
if ( msg . agent_id ) {
const wa = db . prepare ( 'SELECT * FROM workflow_agents WHERE agent_id=?' ). get ( msg . agent_id );
if ( wa ) workflow = db . prepare ( 'SELECT * FROM workflows WHERE run_id=?' ). get ( wa . run_id );
}
return { message : msg , parentChain : chain , session , subagent , workflow };
};
2026-07-12 00:37:32 +08:00
const trace = ( uuid : string ) => {
const chain : DbRow [] = [];
2026-05-31 03:29:41 +08:00
let cur = db . prepare ( 'SELECT * FROM messages WHERE uuid=?' ). get ( uuid );
while ( cur ) { chain . unshift ( cur ); cur = cur . parent_uuid ? db . prepare ( 'SELECT * FROM messages WHERE uuid=?' ). get ( cur . parent_uuid ) : null ; }
return chain ;
};
2026-07-12 00:37:32 +08:00
const thread = ( sid : string , opts : QueryOptions = {}) => {
2026-06-12 22:20:29 +08:00
const includeMeta = opts ? . includeMeta === true ;
const metaClause = includeMeta ? '' : 'AND COALESCE(is_meta,0)=0' ;
return db . prepare ( `SELECT * FROM messages WHERE session_id=? ${ metaClause } ORDER BY timestamp` ). all ( sid );
};
2026-05-31 03:29:41 +08:00
2026-07-12 00:37:32 +08:00
const subagents = ( optsOrSid? : QueryOptions | string ) => {
2026-06-02 01:36:47 +08:00
const opts = normalizeOpts ( optsOrSid );
const { limit = 100 } = opts ;
2026-06-17 23:40:36 +08:00
const needsJoin = opts . project || opts . branch || opts . source ;
const { where , params } = buildWhere ( opts , { sessionId : 'sa.session_id' , project : 's.project' , timestamp : 'sa.session_id' , branch : 's.git_branch' , source : 's.source' });
2026-06-02 01:36:47 +08:00
params . push ( limit );
const join = needsJoin ? 'LEFT JOIN sessions s ON s.id=sa.session_id' : '' ;
2026-07-12 00:37:32 +08:00
return db . prepare ( `SELECT sa.* FROM subagents sa ${ join } WHERE ${ where } LIMIT ?` ). all (... params ). map (( r : DbRow ) => {
2026-05-31 03:29:41 +08:00
const c = db . prepare ( 'SELECT COUNT(*) as c FROM messages WHERE agent_id=?' ). get ( r . agent_id );
return { ... r , messageCount : c?.c || 0 };
});
};
2026-07-12 00:37:32 +08:00
const workflows = ( optsOrSid? : QueryOptions | string ) => {
2026-06-02 01:36:47 +08:00
const opts = normalizeOpts ( optsOrSid );
const { limit = 100 } = opts ;
2026-06-17 23:40:36 +08:00
const needsJoin = opts . project || opts . branch || opts . source ;
const { where , params } = buildWhere ( opts , { sessionId : 'w.session_id' , project : 's.project' , timestamp : 'w.timestamp' , branch : 's.git_branch' , source : 's.source' });
2026-06-02 01:36:47 +08:00
params . push ( limit );
const join = needsJoin ? 'LEFT JOIN sessions s ON s.id=w.session_id' : '' ;
return db . prepare ( `SELECT w.* FROM workflows w ${ join } WHERE ${ where } ORDER BY w.timestamp DESC LIMIT ?` ). all (... params );
};
2026-05-31 03:29:41 +08:00
2026-07-12 00:37:32 +08:00
const workflowTree = ( runId : string ) => {
2026-05-31 03:29:41 +08:00
const wf = db . prepare ( 'SELECT * FROM workflows WHERE run_id=?' ). get ( runId );
if ( ! wf ) return null ;
2026-06-04 15:49:57 +08:00
let result = null ;
2026-07-12 00:37:32 +08:00
try { result = JSON . parse ( wf . result_json ); } catch { /* keep the raw result nullable */ }
const agents = db . prepare ( 'SELECT * FROM workflow_agents WHERE run_id=?' ). all ( runId ). map (( a : DbRow ) => {
2026-06-04 15:49:57 +08:00
const mc = db . prepare ( 'SELECT COUNT(*) as c FROM messages WHERE agent_id=?' ). get ( a . agent_id );
return { ... a , messageCount : mc?.c || 0 };
});
return { ... wf , result , agents };
2026-05-31 03:29:41 +08:00
};
2026-07-12 00:37:32 +08:00
const fileHistory = ( fp : string , opts : QueryOptions = {}) => {
2026-06-17 23:40:36 +08:00
const { limit = 200 , after , before , source } = opts ;
2026-06-02 01:36:47 +08:00
let where = 'tc.file_path=?' ;
2026-07-12 00:37:32 +08:00
const params : any [] = [ fp ];
2026-06-02 01:36:47 +08:00
if ( after ) { where += ' AND m.timestamp > ?' ; params . push ( after ); }
if ( before ) { where += ' AND m.timestamp < ?' ; params . push ( before ); }
2026-06-17 23:40:36 +08:00
if ( source && source !== 'all' ) { where += " AND COALESCE(s.source, 'claude') = ?" ; params . push ( source ); }
2026-06-02 01:36:47 +08:00
params . push ( limit );
2026-05-31 03:29:41 +08:00
return db . prepare (
2026-06-02 01:36:47 +08:00
`SELECT tc.*,s.title as s_title,s.project as s_project,m.timestamp as ts FROM tool_calls tc LEFT JOIN sessions s ON s.id=tc.session_id LEFT JOIN messages m ON m.uuid=tc.message_uuid WHERE ${ where } ORDER BY m.timestamp LIMIT ?`
2026-07-12 00:37:32 +08:00
). all (... params ). map (( r : DbRow ) => ({
2026-05-31 03:29:41 +08:00
toolCall : { id : r.id , message_uuid : r.message_uuid , name : r.name , input_json : r.input_json },
session : { id : r.session_id , title : r.s_title , project : r.s_project },
2026-06-02 01:36:47 +08:00
timestamp : r.ts ,
2026-05-31 03:29:41 +08:00
}));
};
2026-07-12 00:37:32 +08:00
const failures = ( optsOrSid? : QueryOptions | string ) => {
2026-06-02 01:36:47 +08:00
const opts = normalizeOpts ( optsOrSid );
const { limit = 50 } = opts ;
2026-06-17 23:40:36 +08:00
const needsJoin = opts . project || opts . branch || opts . source ;
const { where , params : filterParams } = buildWhere ( opts , { sessionId : 'tr.session_id' , project : 's.project' , timestamp : 'rm.timestamp' , branch : 's.git_branch' , source : 's.source' });
2026-06-02 01:36:47 +08:00
const join = needsJoin ? 'LEFT JOIN sessions s ON s.id=tr.session_id' : '' ;
2026-06-02 09:19:46 +08:00
const errorCond = `(tr.is_error = 1 OR tr.content LIKE ' ${ BASH_EXIT_PAT } ')` ;
const allParams = [... filterParams , limit ];
2026-06-07 16:42:41 +08:00
const rows = db . prepare ( `SELECT tr.* FROM tool_results tr ${ join } LEFT JOIN messages rm ON rm.uuid=tr.message_uuid WHERE ${ errorCond } AND ${ where } ORDER BY rm.timestamp DESC LIMIT ?` ). all (... allParams );
2026-07-12 00:37:32 +08:00
return rows . map (( r : DbRow ) => {
2026-05-31 03:29:41 +08:00
const tc = db . prepare ( 'SELECT * FROM tool_calls WHERE id=?' ). get ( r . tool_use_id );
const session = db . prepare ( 'SELECT * FROM sessions WHERE id=?' ). get ( r . session_id );
const rm = db . prepare ( 'SELECT * FROM messages WHERE uuid=?' ). get ( r . message_uuid );
const next = rm ? . timestamp ? db . prepare ( 'SELECT * FROM messages WHERE session_id=? AND timestamp>? ORDER BY timestamp LIMIT 3' ). all ( r . session_id , rm . timestamp ) : [];
return { toolCall : tc , result : r , session , nextMessages : next };
});
};
2026-07-12 00:37:32 +08:00
const sessions = ( optsOrN? : QueryOptions | number | string ) => {
2026-06-02 01:36:47 +08:00
const opts = normalizeOpts ( optsOrN , 'sessionId' );
const { limit = 50 } = opts ;
2026-06-17 23:40:36 +08:00
const { where , params } = buildWhere ( opts , { sessionId : 's.id' , project : 's.project' , timestamp : 's.started_at' , branch : 's.git_branch' , source : 's.source' });
2026-06-02 01:36:47 +08:00
params . push ( limit );
return db . prepare ( `SELECT * FROM sessions s WHERE ${ where } ORDER BY ended_at DESC LIMIT ?` ). all (... params );
};
2026-05-31 03:29:41 +08:00
2026-06-02 01:36:47 +08:00
const recent = ( n = 10 ) => sessions ({ limit : n });
2026-07-12 00:37:32 +08:00
const summaries = ( optsOrSid? : QueryOptions | string ) => {
2026-06-02 01:36:47 +08:00
const opts = normalizeOpts ( optsOrSid );
const { limit = 100 } = opts ;
2026-06-17 23:40:36 +08:00
const { where , params } = buildWhere ( opts , { sessionId : 'su.session_id' , project : 's.project' , timestamp : 'su.timestamp' , branch : 's.git_branch' , source : 's.source' });
2026-06-02 01:36:47 +08:00
params . push ( limit );
return db . prepare ( `SELECT su.*, s.title as session_title, s.project FROM summaries su LEFT JOIN sessions s ON s.id=su.session_id WHERE ${ where } ORDER BY su.timestamp DESC LIMIT ?` ). all (... params );
};
2026-06-01 16:52:58 +08:00
2026-07-12 00:37:32 +08:00
const overview = ( optsOrScalar? : QueryOptions | string | number ) => {
2026-06-10 03:04:11 +08:00
const opts = normalizeOverviewOpts ( optsOrScalar );
const cwd = process . cwd ();
const sessionLimit = opts . limit ?? 8 ;
const projectLimit = opts . projectLimit ?? 20 ;
const memoryLimit = opts . memoryLimit ?? 100 ;
2026-07-12 00:37:32 +08:00
const projectDescriptor = ( row : DbRow | null , source : string , confidence : string ) => row ? ({
2026-06-10 03:04:11 +08:00
project : row.project ,
project_path : row.project_path || null ,
source ,
confidence ,
}) : null ;
2026-07-12 00:37:32 +08:00
const latestProjectByPattern = ( pattern : string ) : DbRow | undefined => {
2026-06-10 03:04:11 +08:00
const fromSessions = db . prepare ( `
SELECT project, project_path
FROM sessions
WHERE project LIKE ?
ORDER BY COALESCE(ended_at, started_at) DESC
LIMIT 1
` ). get ( pattern );
if ( fromSessions ) return fromSessions ;
return db . prepare ( `
SELECT project, NULL AS project_path
FROM memories
WHERE project LIKE ?
ORDER BY created_at DESC
LIMIT 1
` ). get ( pattern );
};
const resolveCurrentProject = () => {
if ( opts . project ) {
const row = latestProjectByPattern ( opts . project );
const confidence = row ? ( /[%_]/ . test ( opts . project ) ? 'inferred' : 'exact' ) : 'unknown' ;
return projectDescriptor ( row || { project : opts.project , project_path : null }, 'opts' , confidence );
}
const paths = db . prepare ( `
SELECT project, project_path, MAX(COALESCE(ended_at, started_at)) AS last_seen
FROM sessions
WHERE project IS NOT NULL AND project_path IS NOT NULL AND project_path != ''
GROUP BY project, project_path
` ). all ();
const byProjectPath = paths
2026-07-12 00:37:32 +08:00
. filter (( r : DbRow ) => cwd === r . project_path || cwd . startsWith ( r . project_path + path . sep ))
. sort (( a : DbRow , b : DbRow ) => b . project_path . length - a . project_path . length || String ( b . last_seen || '' ). localeCompare ( String ( a . last_seen || '' )))[ 0 ];
2026-06-10 03:04:11 +08:00
if ( byProjectPath ) return projectDescriptor ( byProjectPath , 'cwd_project_path' , 'exact' );
const byMessageCwd = db . prepare ( `
SELECT s.project, s.project_path, MAX(m.timestamp) AS last_seen
FROM messages m
LEFT JOIN sessions s ON s.id=m.session_id
WHERE m.cwd = ? AND s.project IS NOT NULL
GROUP BY s.project, s.project_path
ORDER BY last_seen DESC
LIMIT 1
` ). get ( cwd );
if ( byMessageCwd ) return projectDescriptor ( byMessageCwd , 'cwd_messages' , 'inferred' );
return null ;
};
const projects = db . prepare ( `
WITH names AS (
SELECT project FROM sessions WHERE project IS NOT NULL GROUP BY project
UNION
2026-06-12 22:20:29 +08:00
SELECT project FROM memories WHERE project IS NOT NULL AND deleted_at IS NULL GROUP BY project
2026-06-10 03:04:11 +08:00
),
session_stats AS (
SELECT project, COUNT(*) AS session_count, MAX(COALESCE(ended_at, started_at)) AS last_session_at
FROM sessions
WHERE project IS NOT NULL
GROUP BY project
),
memory_stats AS (
SELECT project, COUNT(*) AS memory_count, MAX(created_at) AS last_memory_at
FROM memories
2026-06-12 22:20:29 +08:00
WHERE project IS NOT NULL AND deleted_at IS NULL
2026-06-10 03:04:11 +08:00
GROUP BY project
)
SELECT
n.project,
(
SELECT s2.project_path
FROM sessions s2
WHERE s2.project = n.project AND s2.project_path IS NOT NULL
ORDER BY COALESCE(s2.ended_at, s2.started_at) DESC
LIMIT 1
) AS project_path,
COALESCE(ss.session_count, 0) AS session_count,
COALESCE(ms.memory_count, 0) AS memory_count,
ss.last_session_at,
ms.last_memory_at
FROM names n
LEFT JOIN session_stats ss ON ss.project = n.project
LEFT JOIN memory_stats ms ON ms.project = n.project
ORDER BY COALESCE(ss.last_session_at, ms.last_memory_at) DESC
LIMIT ?
2026-07-12 00:37:32 +08:00
` ). all ( projectLimit ). map (( row : DbRow ) => {
2026-06-10 03:04:11 +08:00
const branches = db . prepare ( `
SELECT git_branch
FROM sessions
WHERE project = ? AND git_branch IS NOT NULL AND git_branch != ''
GROUP BY git_branch
ORDER BY MAX(COALESCE(ended_at, started_at)) DESC
LIMIT 5
2026-07-12 00:37:32 +08:00
` ). all ( row . project ). map (( r : DbRow ) => r . git_branch );
2026-06-10 03:04:11 +08:00
return { ... row , recent_branches : branches };
});
const currentProject = resolveCurrentProject ();
let current_project = null ;
if ( currentProject ? . project ) {
const sessionTotal = db . prepare ( 'SELECT COUNT(*) AS c FROM sessions WHERE project = ?' ). get ( currentProject . project ) ? . c || 0 ;
const sessionsForProject = db . prepare ( `
2026-06-17 23:40:36 +08:00
SELECT id, title, project, project_path, started_at, ended_at, git_branch, message_count, COALESCE(source, 'claude') AS source
2026-06-10 03:04:11 +08:00
FROM sessions
WHERE project = ?
ORDER BY COALESCE(ended_at, started_at) DESC
LIMIT ?
` ). all ( currentProject . project , sessionLimit );
2026-06-12 22:20:29 +08:00
const memoryTotal = db . prepare ( 'SELECT COUNT(*) AS c FROM memories WHERE project = ? AND deleted_at IS NULL' ). get ( currentProject . project ) ? . c || 0 ;
2026-06-10 03:04:11 +08:00
const memoriesForProject = db . prepare ( `
2026-06-12 22:20:29 +08:00
SELECT id, path, anchors, summary, session_id, project, created_at
2026-06-10 03:04:11 +08:00
FROM memories
2026-06-12 22:20:29 +08:00
WHERE project = ? AND deleted_at IS NULL
2026-06-10 03:04:11 +08:00
ORDER BY created_at DESC
LIMIT ?
` ). all ( currentProject . project , memoryLimit );
current_project = {
project : currentProject.project ,
project_path : currentProject.project_path ,
session_total : sessionTotal ,
sessions : sessionsForProject ,
memory_total : memoryTotal ,
memories : memoriesForProject ,
};
}
const totalProjects = db . prepare ( `
SELECT COUNT(*) AS c
FROM (
SELECT project FROM sessions WHERE project IS NOT NULL GROUP BY project
UNION
2026-06-12 22:20:29 +08:00
SELECT project FROM memories WHERE project IS NOT NULL AND deleted_at IS NULL GROUP BY project
2026-06-10 03:04:11 +08:00
)
` ). get () ? . c || 0 ;
const totalSessions = db . prepare ( 'SELECT COUNT(*) AS c FROM sessions' ). get () ? . c || 0 ;
2026-06-12 22:20:29 +08:00
const totalMemories = db . prepare ( 'SELECT COUNT(*) AS c FROM memories WHERE deleted_at IS NULL' ). get () ? . c || 0 ;
2026-06-17 23:40:36 +08:00
const sources = db . prepare ( `
SELECT COALESCE(source, 'claude') AS source,
COUNT(*) AS session_count,
MAX(COALESCE(ended_at, started_at)) AS last_session_at
FROM sessions
GROUP BY COALESCE(source, 'claude')
ORDER BY last_session_at DESC
` ). all ();
2026-06-10 03:04:11 +08:00
return {
current : {
cwd ,
project : currentProject ,
},
current_project ,
projects ,
totals : {
projects : totalProjects ,
sessions : totalSessions ,
memories : totalMemories ,
2026-06-17 23:40:36 +08:00
sources ,
2026-06-10 03:04:11 +08:00
},
};
};
2026-07-12 00:37:32 +08:00
const resolveJsonlPath = ( messageUuid : string ) : string | null => {
2026-06-17 23:40:36 +08:00
const msg = db . prepare ( 'SELECT session_id, agent_id, source FROM messages WHERE uuid=?' ). get ( messageUuid );
2026-05-31 03:29:41 +08:00
if ( ! msg ) return null ;
2026-06-17 23:40:36 +08:00
if ( msg . source === 'codex' || String ( messageUuid ). startsWith ( 'codex:' )) {
const match = /^codex:([^:]+):(\d+)$/ . exec ( String ( messageUuid ));
if ( ! match ) return null ;
const rawThreadId = match [ 1 ];
if ( ! msg . agent_id ) {
const ses = db . prepare ( 'SELECT jsonl_path FROM sessions WHERE id=?' ). get ( msg . session_id );
if ( ses ? . jsonl_path ) return ses . jsonl_path ;
}
return db . prepare ( `
SELECT jsonl_path FROM index_state
WHERE jsonl_path LIKE ? AND jsonl_path LIKE '%.jsonl'
ORDER BY length(jsonl_path) ASC
LIMIT 1
` ). get ( `% ${ rawThreadId } .jsonl` ) ? . jsonl_path || null ;
}
2026-05-31 03:29:41 +08:00
if ( msg . agent_id ) {
const wa = db . prepare ( 'SELECT agent_id, run_id, session_id FROM workflow_agents WHERE agent_id=?' ). get ( msg . agent_id );
if ( wa ) {
const ses = db . prepare ( 'SELECT jsonl_path FROM sessions WHERE id=?' ). get ( wa . session_id );
if ( ses ) return path . join ( path . dirname ( ses . jsonl_path ), wa . session_id , 'subagents' , 'workflows' , wa . run_id , wa . agent_id + '.jsonl' );
}
const sa = db . prepare ( 'SELECT agent_id, session_id FROM subagents WHERE agent_id=?' ). get ( msg . agent_id );
if ( sa ) {
const ses = db . prepare ( 'SELECT jsonl_path FROM sessions WHERE id=?' ). get ( sa . session_id );
if ( ses ) return path . join ( path . dirname ( ses . jsonl_path ), sa . session_id , 'subagents' , sa . agent_id + '.jsonl' );
}
} else {
const ses = db . prepare ( 'SELECT jsonl_path FROM sessions WHERE id=?' ). get ( msg . session_id );
if ( ses ) return ses . jsonl_path ;
}
return null ;
};
2026-07-12 00:37:32 +08:00
const findCodexRawLine = ( jsonlPath : string | null , uuid : string ) : string | null => {
2026-06-17 23:40:36 +08:00
const match = /^codex:[^:]+:(\d+)$/ . exec ( String ( uuid ));
if ( ! match || ! jsonlPath || ! fs . existsSync ( jsonlPath )) return null ;
const targetLine = Number ( match [ 1 ]);
let lineNum = 0 ;
let found = null ;
readLines ( jsonlPath , ( line ) => {
lineNum ++ ;
if ( lineNum !== targetLine ) return ;
found = line ;
return false ;
});
return found ;
};
2026-07-12 00:37:32 +08:00
const findRawLine = ( jsonlPath : string | null , uuid : string ) : string | null => {
2026-05-31 03:29:41 +08:00
if ( ! jsonlPath || ! fs . existsSync ( jsonlPath )) return null ;
2026-06-17 23:40:36 +08:00
if ( String ( uuid ). startsWith ( 'codex:' )) return findCodexRawLine ( jsonlPath , uuid );
2026-05-31 03:29:41 +08:00
let found = null ;
readLines ( jsonlPath , ( line ) => {
if ( ! line . includes ( uuid )) return ;
2026-07-12 00:37:32 +08:00
try { const obj = JSON . parse ( line ); if ( obj . uuid === uuid ) { found = line ; return false ; } } catch { /* skip malformed JSONL lines */ }
2026-05-31 03:29:41 +08:00
});
return found ;
};
2026-07-12 00:37:32 +08:00
const raw = ( messageUuid : string , opts : { offset? : number ; limit? : number } = {}) => {
2026-05-31 03:29:41 +08:00
const { offset = 0 , limit = 10000 } = opts ;
const jsonlPath = resolveJsonlPath ( messageUuid );
const line = findRawLine ( jsonlPath , messageUuid );
if ( ! line ) return null ;
return {
text : line.slice ( offset , offset + limit ),
totalLength : line.length ,
offset ,
limit ,
hasMore : offset + limit < line . length ,
};
};
2026-07-12 00:37:32 +08:00
const memories = ( optsOrSid? : QueryOptions | string ) => {
2026-06-10 02:05:27 +08:00
const opts = normalizeOpts ( optsOrSid );
const { limit = 50 , query } = opts ;
2026-06-10 03:50:38 +08:00
assertEnglishMemoryText ( query , 'memories() query' );
2026-06-17 23:40:36 +08:00
const needsJoin = opts . branch || opts . source ;
2026-06-10 02:05:27 +08:00
const { where : baseWhere , params } = buildWhere ( opts , {
sessionId : 'mem.session_id' ,
project : 'mem.project' ,
timestamp : 'mem.created_at' ,
branch : 's.git_branch' ,
2026-06-17 23:40:36 +08:00
source : 's.source' ,
2026-06-10 02:05:27 +08:00
});
2026-07-12 00:37:32 +08:00
const where = baseWhere + ' AND mem.deleted_at IS NULL' ;
2026-06-10 02:05:27 +08:00
const join = needsJoin ? 'LEFT JOIN sessions s ON s.id=mem.session_id' : '' ;
2026-06-12 22:20:29 +08:00
const hasQuery = String ( query || '' ). trim (). length > 0 ;
const ftsQuery = buildSafeFtsQuery ( query );
if ( ! hasQuery ) {
params . push ( limit );
return db . prepare ( `SELECT mem.* FROM memories mem ${ join } WHERE ${ where } ORDER BY mem.created_at DESC LIMIT ?` ). all (... params );
}
if ( ! ftsQuery ) return [];
params . unshift ( ftsQuery );
params . push ( limit );
return db . prepare ( `
SELECT mem.*, mf.rank AS rank
FROM memories_fts mf
JOIN memories mem ON mem.rowid = mf.rowid
${ join }
WHERE memories_fts MATCH ? AND ${ where }
ORDER BY mf.rank, mem.created_at DESC
LIMIT ?
` ). all (... params );
2026-06-10 02:05:27 +08:00
};
2026-06-10 03:04:11 +08:00
return { sql : q , search , context , trace , thread , subagents , workflows , workflowTree , fileHistory , failures , sessions , recent , summaries , raw , memories , overview };
2026-05-31 03:29:41 +08:00
}
2026-07-12 00:37:32 +08:00
function createAttuneApi ( db : SqliteDb ) {
const resolveMemoryPath = ( memoryPath : string , sessionId? : string ) : string => {
2026-06-10 02:05:27 +08:00
let base = null ;
if ( sessionId ) {
base = db . prepare ( 'SELECT project_path FROM sessions WHERE id=?' ). get ( sessionId ) ? . project_path || null ;
}
const resolved = path . isAbsolute ( memoryPath )
? path . normalize ( memoryPath )
: path . resolve ( base || process . cwd (), memoryPath );
let stat ;
try {
stat = fs . statSync ( resolved );
} catch {
throw new Error ( `remember() memory file does not exist: ${ resolved } ` );
}
if ( ! stat . isFile ()) throw new Error ( `remember() memory path is not a file: ${ resolved } ` );
return resolved ;
};
2026-07-12 00:37:32 +08:00
const normalizeAnchors = ( anchors : unknown ) : string | null => {
2026-06-12 22:20:29 +08:00
if ( anchors == null ) return null ;
let parsed = anchors ;
if ( typeof anchors === 'string' ) {
const trimmed = anchors . trim ();
if ( ! trimmed ) return null ;
try {
parsed = JSON . parse ( trimmed );
} catch {
throw new Error ( 'remember() anchors must be a JSON array' );
}
}
if ( ! Array . isArray ( parsed )) throw new Error ( 'remember() anchors must be an array' );
for ( const anchor of parsed ) {
if ( ! anchor || typeof anchor !== 'object' || Array . isArray ( anchor )) {
throw new Error ( 'remember() anchors entries must be objects' );
}
}
return parsed . length ? JSON . stringify ( parsed ) : null ;
};
2026-07-12 00:37:32 +08:00
const remember = ({ path : memoryPath , session_id , message_start , message_end , summary , project , anchors } : RememberInput ) => {
2026-06-10 02:05:27 +08:00
if ( ! memoryPath || ! summary ) throw new Error ( 'remember() requires path and summary' );
2026-06-10 03:50:38 +08:00
assertEnglishMemoryText ( summary , 'remember() summary' );
2026-06-10 02:05:27 +08:00
const normalizedPath = resolveMemoryPath ( memoryPath , session_id );
2026-06-12 22:20:29 +08:00
const normalizedAnchors = normalizeAnchors ( anchors );
2026-06-10 02:05:27 +08:00
const id = `mem- ${ Date . now () } - ${ Math . random (). toString ( 36 ). slice ( 2 , 8 ) } ` ;
const proj = project || db . prepare ( 'SELECT project FROM sessions WHERE id=?' ). get ( session_id ) ? . project || null ;
const created_at = new Date (). toISOString ();
2026-06-12 22:20:29 +08:00
db . prepare ( 'INSERT OR REPLACE INTO memories (id, session_id, project, message_start, message_end, path, anchors, summary, created_at) VALUES (?,?,?,?,?,?,?,?,?)' ). run (
id , session_id || null , proj , message_start || null , message_end || null , normalizedPath , normalizedAnchors , summary , created_at );
return { id , path : normalizedPath , project : proj , anchors : normalizedAnchors , created_at };
2026-06-10 02:05:27 +08:00
};
2026-07-12 00:37:32 +08:00
const forget = ({ id , reason } : ForgetInput ) => {
2026-06-12 22:20:29 +08:00
const deletionReason = String ( reason || '' ). trim ();
if ( ! id || ! deletionReason ) throw new Error ( 'forget() requires id and reason' );
const row = db . prepare ( 'SELECT id, deleted_at, deleted_reason FROM memories WHERE id=?' ). get ( id );
if ( ! row ) throw new Error ( `forget() memory not found: ${ id } ` );
if ( row . deleted_at ) {
return { id , deleted_at : row.deleted_at , deleted_reason : row.deleted_reason , already_deleted : true };
}
const deleted_at = new Date (). toISOString ();
db . prepare ( 'UPDATE memories SET deleted_at=?, deleted_reason=? WHERE id=?' ). run ( deleted_at , deletionReason , id );
return { id , deleted_at , deleted_reason : deletionReason };
};
return { remember , forget };
2026-06-10 02:05:27 +08:00
}
2026-06-12 22:20:29 +08:00
export { createQueryApi , createAttuneApi };