@clauderecallhq/cli 0.68.3 → 0.72.0
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/README.md +7 -0
- package/dist/cli.js +337 -306
- package/dist/daemon/embedder-worker.js +3 -0
- package/dist/daemon/entrypoint.js +268 -245
- package/dist/mcp-server.js +106 -91
- package/dist/web/assets/{dist-CPal3OGL.js → dist-CM2Q2okx.js} +1 -1
- package/dist/web/assets/index-BdyfKGwx.css +1 -0
- package/dist/web/assets/index-D8oScLt4.js +657 -0
- package/dist/web/index.html +2 -2
- package/package.json +6 -6
- package/dist/web/assets/index-B-HrjaDy.css +0 -1
- package/dist/web/assets/index-BxxoX8lK.js +0 -645
package/dist/mcp-server.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/* Claude Recall (proprietary). See LICENSE for terms. */
|
|
3
|
-
var
|
|
3
|
+
var Ln=Object.defineProperty;var $=(e,t)=>()=>(e&&(t=e(e=0)),t);var De=(e,t)=>{for(var s in t)Ln(e,s,{get:t[s],enumerable:!0})};import{createRequire as An}from"node:module";var On,In,xn,Me,Fe,mt,gt=$(()=>{"use strict";{let e=process.emit.bind(process);process.emit=function(t,...s){let n=s[0];return t==="warning"&&n instanceof Error&&n.name==="ExperimentalWarning"&&/SQLite/i.test(n.message)?!1:e(t,...s)}}On=An(import.meta.url),In=["node","sqlite"].join(":"),xn=On(In),Me=class{inner;constructor(t){this.inner=t}get(...t){return t.length===0?this.inner.get():this.inner.get(...t)}all(...t){return t.length===0?this.inner.all():this.inner.all(...t)}run(...t){let s=t.length===0?this.inner.run():this.inner.run(...t);return{changes:s.changes,lastInsertRowid:s.lastInsertRowid}}iterate(...t){return t.length===0?this.inner.iterate():this.inner.iterate(...t)}},Fe=class{inner;extensionLoadingEnabled=!1;txDepth=0;constructor(t,s={}){this.inner=new xn.DatabaseSync(t,{readOnly:s.readonly??!1,allowExtension:!0})}prepare(t){return new Me(this.inner.prepare(t))}exec(t){this.inner.exec(t)}close(){this.inner.close()}pragma(t,s={}){if(t.includes("=")){this.inner.exec(`PRAGMA ${t}`);return}if(s.simple){let n=this.inner.prepare(`PRAGMA ${t}`).get();return n&&typeof n=="object"?Object.values(n)[0]:void 0}return this.inner.prepare(`PRAGMA ${t}`).all()}transaction(t){return((...n)=>{this.txDepth===0?this.inner.exec("BEGIN"):this.inner.exec(`SAVEPOINT sp_${this.txDepth}`),this.txDepth+=1;try{let r=t(...n);return this.txDepth-=1,this.txDepth===0?this.inner.exec("COMMIT"):this.inner.exec(`RELEASE sp_${this.txDepth}`),r}catch(r){this.txDepth-=1;try{this.txDepth===0?this.inner.exec("ROLLBACK"):(this.inner.exec(`ROLLBACK TO sp_${this.txDepth}`),this.inner.exec(`RELEASE sp_${this.txDepth}`))}catch{}throw r}})}loadExtension(t,s){this.extensionLoadingEnabled||(this.inner.enableLoadExtension(!0),this.extensionLoadingEnabled=!0),s===void 0?this.inner.loadExtension(t):this.inner.loadExtension(t,s)}},mt=Fe});import{homedir as _t}from"node:os";import{join as Ue,basename as aa}from"node:path";import{existsSync as vn,mkdirSync as Cn,chmodSync as kn,readdirSync as ca,statSync as da}from"node:fs";function x(){vn(b)||Cn(b,{recursive:!0,mode:448}),process.platform!=="win32"&&kn(b,448)}var ua,b,oe,F=$(()=>{"use strict";ua=process.env.CLAUDE_PROJECTS_DIR?process.env.CLAUDE_PROJECTS_DIR:Ue(_t(),".claude","projects"),b=process.env.RECALL_HOME?process.env.RECALL_HOME:Ue(_t(),".recall"),oe=Ue(b,"db.sqlite")});function ht(e){let t=e.prepare("PRAGMA table_info(sessions)").all(),s=new Set(t.map(g=>g.name)),n=[["total_input_tokens","INTEGER"],["total_output_tokens","INTEGER"],["total_cache_create_tokens","INTEGER"],["total_cache_read_tokens","INTEGER"],["primary_model","TEXT"],["auto_title","TEXT"],["auto_title_source","TEXT"],["auto_title_generated_at","INTEGER"],["auto_title_history","TEXT"],["verification_status","TEXT"],["verification_computed_at","INTEGER"],["title_quality","TEXT"],["title_quality_computed_at","INTEGER"],["archive_status","TEXT NOT NULL DEFAULT 'live'"],["archived_at","TEXT"]];for(let[g,_]of n)s.has(g)||e.exec(`ALTER TABLE sessions ADD COLUMN ${g} ${_}`);let r=e.prepare("PRAGMA table_info(collection_sessions)").all(),i=new Set(r.map(g=>g.name)),o=[["source","TEXT NOT NULL DEFAULT 'manual'"],["rule_id","TEXT"]];for(let[g,_]of o)i.has(g)||e.exec(`ALTER TABLE collection_sessions ADD COLUMN ${g} ${_}`);let a=e.prepare("PRAGMA table_info(session_notes)").all(),l=new Set(a.map(g=>g.name)),c=[["auto_synopsis","TEXT"],["auto_synopsis_generated_at","INTEGER"],["auto_synopsis_history","TEXT"]];for(let[g,_]of c)l.has(g)||e.exec(`ALTER TABLE session_notes ADD COLUMN ${g} ${_}`);let d=e.prepare("PRAGMA table_info(threads)").all();new Set(d.map(g=>g.name)).has("folder_id")||(e.exec("ALTER TABLE threads ADD COLUMN folder_id TEXT REFERENCES thread_folders(id) ON DELETE SET NULL"),e.exec("CREATE INDEX IF NOT EXISTS idx_threads_folder ON threads(folder_id)"));let u=e.prepare("PRAGMA table_info(thread_folders)").all(),p=new Set(u.map(g=>g.name));p.has("project_scope")||(e.exec("ALTER TABLE thread_folders ADD COLUMN project_scope TEXT"),e.exec("CREATE INDEX IF NOT EXISTS idx_thread_folders_project ON thread_folders(project_scope)")),p.has("sort_order")||e.exec("ALTER TABLE thread_folders ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0"),e.exec(`
|
|
4
4
|
CREATE TABLE IF NOT EXISTS message_embeddings (
|
|
5
5
|
message_uuid TEXT PRIMARY KEY REFERENCES messages(uuid) ON DELETE CASCADE,
|
|
6
6
|
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
@@ -12,7 +12,7 @@ var En=Object.defineProperty;var $=(e,t)=>()=>(e&&(t=e(e=0)),t);var Ie=(e,t)=>{f
|
|
|
12
12
|
);
|
|
13
13
|
CREATE INDEX IF NOT EXISTS idx_message_embeddings_session ON message_embeddings(session_id);
|
|
14
14
|
CREATE INDEX IF NOT EXISTS idx_message_embeddings_generated ON message_embeddings(generated_at DESC);
|
|
15
|
-
`)}var
|
|
15
|
+
`)}var ft,Et=$(()=>{"use strict";ft=`
|
|
16
16
|
PRAGMA journal_mode = WAL;
|
|
17
17
|
PRAGMA synchronous = NORMAL;
|
|
18
18
|
PRAGMA foreign_keys = ON;
|
|
@@ -668,76 +668,90 @@ CREATE INDEX IF NOT EXISTS idx_synth_results_target
|
|
|
668
668
|
ON bug_synthesis_results(scope, target_id, created_at DESC);
|
|
669
669
|
CREATE INDEX IF NOT EXISTS idx_synth_results_created
|
|
670
670
|
ON bug_synthesis_results(created_at DESC);
|
|
671
|
-
`});import*as
|
|
672
|
-
GROUP BY tag ORDER BY count DESC, tag ASC`).all()}function
|
|
671
|
+
`});import*as Tt from"sqlite-vec";function m(){if(O)return O;x(),O=new mt(oe),Tt.load(O),O.pragma("cache_size = -64000"),O.pragma("mmap_size = 268435456"),O.pragma("temp_store = MEMORY"),O.pragma("busy_timeout = 5000"),O.pragma("journal_size_limit = 67108864"),O.pragma("wal_autocheckpoint = 1000"),O.exec(ft),ht(O);try{O.exec("PRAGMA optimize")}catch{}return O}function St(){if(O){try{O.exec("PRAGMA optimize")}catch{}try{O.exec("INSERT INTO messages_fts(messages_fts, rank) VALUES('merge', 4);")}catch{}try{O.exec("INSERT INTO sessions_fts(sessions_fts, rank) VALUES('merge', 4);")}catch{}try{O.pragma("wal_checkpoint(TRUNCATE)")}catch{}O.close(),O=null}}var O,L=$(()=>{"use strict";gt();F();Et();O=null});import{writeFileSync as jn}from"node:fs";import{join as Xn}from"node:path";function V(e){return e.trim().toLowerCase().replace(/^#+/,"").replace(/[\s/\\]+/g,"-").replace(/[^a-z0-9\-._]/g,"").slice(0,40)}function fe(e,t){let s=V(t);if(!s)throw new Error("tag must contain at least one alphanumeric character");let n=m(),r=new Date().toISOString();return n.prepare("SELECT 1 FROM session_tags WHERE session_id = ? AND tag = ?").get(e,s)?{tag:s,added:!1}:(n.transaction(()=>{n.prepare("INSERT INTO session_tags (session_id, tag, created_at) VALUES (?, ?, ?)").run(e,s,r),n.prepare("INSERT INTO tag_events (session_id, tag, action, at) VALUES (?, ?, 'add', ?)").run(e,s,r)})(),Nt(),{tag:s,added:!0})}function Rt(e,t){let s=V(t);if(!s)return{tag:"",removed:!1};let n=m(),r=new Date().toISOString();return n.prepare("SELECT 1 FROM session_tags WHERE session_id = ? AND tag = ?").get(e,s)?(n.transaction(()=>{n.prepare("DELETE FROM session_tags WHERE session_id = ? AND tag = ?").run(e,s),n.prepare("INSERT INTO tag_events (session_id, tag, action, at) VALUES (?, ?, 'remove', ?)").run(e,s,r)})(),Nt(),{tag:s,removed:!0}):{tag:s,removed:!1}}function he(e){return m().prepare("SELECT tag FROM session_tags WHERE session_id = ? ORDER BY tag").all(e).map(t=>t.tag)}function yt(){return m().prepare(`SELECT tag, COUNT(*) AS count FROM session_tags
|
|
672
|
+
GROUP BY tag ORDER BY count DESC, tag ASC`).all()}function Nt(){try{x();let e=m(),t=e.prepare("SELECT session_id, tag, created_at FROM session_tags ORDER BY session_id, tag").all(),s=e.prepare("SELECT id, session_id, tag, action, at FROM tag_events ORDER BY at ASC, id ASC").all(),n={schema:"claude-recall.tags.v1",backed_up_at:new Date().toISOString(),current:t,events:s};jn(Hn,JSON.stringify(n,null,2))}catch(e){console.error("[tags] backup failed:",e)}}var Hn,Ee=$(()=>{"use strict";L();F();Hn=Xn(b,"tags.json")});function Bn(e,t){let s=e.filter(i=>i.content_text&&i.content_text.trim().length>0);if(s.length<=t)return s;let n=new Set;n.add(0),n.add(s.length-1);let r=(s.length-2)/Math.max(1,t-2);for(let i=1;i<t-1;i++)n.add(Math.floor(i*r));return Array.from(n).sort((i,o)=>i-o).slice(0,t).map(i=>s[i])}function Te(e){let t=m(),s={limit:e.limit??500},n=e.sessionIds&&e.sessionIds.length>0,r=n?"1=1":"s.message_count > 2";if(n){let o=e.sessionIds.map((a,l)=>`@sid_${l}`).join(", ");r+=` AND s.id IN (${o})`,e.sessionIds.forEach((a,l)=>{s[`sid_${l}`]=a})}return e.untaggedOnly&&(r+=" AND NOT EXISTS (SELECT 1 FROM session_tags st WHERE st.session_id = s.id)"),e.project&&(r+=" AND p.name = @project",s.project=e.project),e.collectionId&&(r+=" AND s.id IN (SELECT session_id FROM collection_sessions WHERE collection_id = @col)",s.col=e.collectionId),t.prepare(`SELECT s.id, p.name AS project, s.git_branch,
|
|
673
673
|
NULLIF(sa.alias, '') AS alias,
|
|
674
674
|
COALESCE(s.first_user_message, '') AS first_user_message
|
|
675
675
|
FROM sessions s
|
|
676
676
|
JOIN projects p ON p.id = s.project_id
|
|
677
677
|
LEFT JOIN session_aliases sa ON sa.session_id = s.id
|
|
678
|
-
WHERE ${
|
|
678
|
+
WHERE ${r}
|
|
679
679
|
ORDER BY COALESCE(s.started_at, '') DESC
|
|
680
680
|
LIMIT @limit`).all(s).map(o=>{let a=t.prepare(`SELECT role, COALESCE(content_text, '') AS content_text
|
|
681
681
|
FROM messages WHERE session_id = ?
|
|
682
|
-
ORDER BY COALESCE(timestamp, ''), rowid`).all(o.id),
|
|
682
|
+
ORDER BY COALESCE(timestamp, ''), rowid`).all(o.id),c=Bn(a,5).map(d=>`${d.role}: ${d.content_text.slice(0,400)}`).join(`
|
|
683
683
|
---
|
|
684
|
-
`);return{id:o.id,project:o.project,git_branch:o.git_branch,alias:o.alias,first_user_message:o.first_user_message,message_sample:
|
|
685
|
-
`)}function
|
|
686
|
-
`)}function
|
|
687
|
-
`)}function
|
|
688
|
-
`)}var
|
|
689
|
-
`);for(;n!==-1;){let
|
|
690
|
-
`)}}}async function
|
|
684
|
+
`);return{id:o.id,project:o.project,git_branch:o.git_branch,alias:o.alias,first_user_message:o.first_user_message,message_sample:c,current_tags:he(o.id)}})}var Pe=$(()=>{"use strict";L();Ee()});import{z as j}from"zod";function Xe(e){let t=e.minTags??2,s=e.maxTags??4,n=e.untaggedOnly??!e.sessionId,r=["Auto-tag my Claude Recall sessions using the MCP tools available to you.","","1. Call `list_tags` first to see which tags I already use (prefer those for consistency over inventing new ones).","2. Call `list_sessions_to_tag` with these filters:"],i=[];return e.sessionId?(i.push("limit: 1"),r.push(` ${i.join(", ")}`),r.push(` Then match the session id ${e.sessionId} from the returned list.`)):(n&&i.push("untaggedOnly: true"),e.project&&i.push(`project: "${e.project}"`),e.collectionId&&i.push(`collectionId: "${e.collectionId}"`),i.push(`limit: ${e.limit??100}`),r.push(` ${i.join(", ")}`)),r.push(""),r.push(`3. For each session returned, look at the alias, first user message, git branch, and sampled messages. Pick ${t}-${s} concise, lowercase, hyphen-separated tags describing:`),r.push(" - domain/subsystem (auth, db, frontend, billing, etc.)"),r.push(" - kind of work (bugfix, feature, refactor, research)"),r.push(" - prominent tools or libraries if relevant"),r.push(""),r.push("4. Call `apply_tags` once per session to write the tags."),r.push(""),r.push("Work through EVERY session returned \u2014 do not stop partway. When done, reply with a short summary of how many sessions were tagged. Do not ask clarifying questions; just do the work."),r.join(`
|
|
685
|
+
`)}function Zn(e){let t=e.mode==="detailed";return[`Summarize Claude Recall session ${e.sessionId} using the MCP tools available to you.`,"",`1. Call \`context_for_session\` with id "${e.sessionId}" and mode "condensed" to get the transcript as markdown.`,t?"2. Write a 1-paragraph overview (\u22643 sentences) of what this session was for, then 5-8 bullet points covering:":"2. Write 3-5 bullet points covering:"," - What was accomplished (shipped, decided, learned)"," - What was tried and abandoned"," - Any explicit open questions or follow-ups","","Rules:",'- Be concrete. Name files, functions, and decisions. Avoid vague "discussed X".',"- If nothing was actually shipped or decided, say so plainly.",'- Reply with just the summary \u2014 no preamble, no "Here is the summary:".'].join(`
|
|
686
|
+
`)}function tr(e){return[`Extract every architectural or product decision made in Claude Recall session ${e.sessionId}.`,"",`1. Call \`context_for_session\` with id "${e.sessionId}" and mode "full" (so tool I/O is included \u2014 sometimes the decision lives in a commit message or diff).`,"2. For each distinct decision, emit one block:",""," - **Decision:** one sentence."," - **Why:** the stated rationale (quote if possible).",' - **Alternatives considered:** bullet list, or "none mentioned".',' - **Where it landed:** file path / function name / commit SHA if identifiable, otherwise "TBD".',"","Rules:","- Include only decisions that were actually made, not ideas merely discussed.","- If an alternative was rejected, list it with a one-line reason.","- Group related decisions under a short heading when useful.",'- If the session made zero real decisions, reply exactly with: "No decisions made in this session."',"- No preamble, no closing, just the decision blocks."].join(`
|
|
687
|
+
`)}function nr(e){let t=e.limit??5;return[`Find ${t} Recall sessions most similar to session ${e.sessionId}.`,"",`1. Call \`get_session\` with id "${e.sessionId}" \u2014 note its alias, first user message, tags, and git branch.`,'2. Derive 2-3 short search queries from that content (topic words, library names, error strings \u2014 NOT generic words like "fix" or "add").',`3. Call \`search\` once per query (limit: ${Math.max(5,t*2)}). Dedupe hits by session_id.`,"4. Also call `list_sessions` with the same tag(s) if the target session has any (pick the most specific tag).","5. Union the results. Exclude the target session itself. Rank by a mix of:"," - Shared tags (strongest signal)"," - Matching search hits across multiple queries"," - Recency as a tiebreaker","",`6. Return the top ${t} as a numbered list. For each:`," - **<short_id> \xB7 <project>** \u2014 <alias or first_user_message truncated>",' - One sentence on WHY it is similar (not a generic "same topic" \u2014 be specific).',"","If fewer than 2 genuinely-similar sessions exist, say so rather than padding with weak matches.","No preamble. Just the ranked list."].join(`
|
|
688
|
+
`)}var Vn,Qn,er,sr,rr,ir,or,ar,Lt,He=$(()=>{"use strict";Vn={project:j.string().optional().describe("Exact project name match (optional)."),collectionId:j.string().optional().describe("Restrict to sessions in this collection (optional)."),sessionId:j.string().optional().describe("Full session UUID to tag just one session (optional)."),untaggedOnly:j.boolean().optional().describe("Skip sessions that already have any tag (default: true)."),limit:j.number().int().min(1).max(500).optional().describe("Max sessions to process (default: 100)."),minTags:j.number().int().min(1).max(10).optional().describe("Minimum tags per session (default: 2)."),maxTags:j.number().int().min(1).max(10).optional().describe("Maximum tags per session (default: 4).")};Qn={sessionId:j.string().describe("Session UUID (or 8+-char prefix) to summarize."),mode:j.enum(["brief","detailed"]).optional().describe("brief = 3-5 bullets; detailed = paragraph + bullets. Default: brief.")};er={sessionId:j.string().describe("Session UUID (or 8+-char prefix) to extract decisions from.")};sr={sessionId:j.string().describe("Session UUID (or 8+-char prefix) to find similar sessions to."),limit:j.number().int().min(1).max(20).optional().describe("How many similar sessions to surface (default: 5).")};rr={name:"auto_tag_sessions",title:"Auto-tag Recall sessions",description:"Have the agent auto-tag Recall sessions using the Recall MCP tools. Scope can be restricted to a project, collection, or single session.",argsSchema:Vn,build:Xe,allowedTools:["mcp__recall__list_tags","mcp__recall__list_sessions_to_tag","mcp__recall__apply_tags"]},ir={name:"summarize_session",title:"Summarize a session",description:"Produce a concise, concrete summary of one session \u2014 what shipped, what was tried, what's still open.",argsSchema:Qn,build:Zn,allowedTools:["mcp__recall__get_session","mcp__recall__context_for_session"]},or={name:"extract_decisions",title:"Extract architectural decisions",description:"Scan a session and emit one structured block per architectural / product decision: what, why, alternatives, where it landed.",argsSchema:er,build:tr,allowedTools:["mcp__recall__get_session","mcp__recall__context_for_session"]},ar={name:"find_similar_sessions",title:"Find similar sessions",description:"Given a session, find other sessions that touched the same topic / library / error \u2014 ranked with reasons.",argsSchema:sr,build:nr,allowedTools:["mcp__recall__get_session","mcp__recall__search","mcp__recall__list_sessions","mcp__recall__list_tags"]},Lt=[rr,ir,or,ar]});function as(e,t){let s=Gr.get(e);if(!(!s||s.size===0))for(let n of s)try{n(t)}catch{}}var Gr,ls=$(()=>{"use strict";Gr=new Map});var us={};De(us,{buildScanPrompt:()=>cs,isClaudeCliAvailable:()=>Vr,runClaudeCliScan:()=>si,spawnClaudePrompt:()=>ni});import{execFileSync as Yr,execSync as zr,spawn as Kr}from"node:child_process";function qr(){if(ce)return ce;try{ce=zr("which claude",{encoding:"utf8",stdio:["ignore","pipe","ignore"]}).trim()}catch{ce="claude"}return ce}function Vr(){try{return Yr("command",["-v","claude"],{stdio:"ignore"}),!0}catch{return!1}}function cs(e){return Xe({project:e.project,collectionId:e.collectionId,sessionId:e.sessionIds&&e.sessionIds.length===1?e.sessionIds[0]:void 0,untaggedOnly:e.untaggedOnly,limit:e.limit})}function Qr(e,t){let s=t.get(e);return s||e.slice(0,8)}function Zr(e){try{return Te(e).map(s=>({id:s.id,label:s.alias&&s.alias.trim().length>0?s.alias:s.first_user_message&&s.first_user_message.trim().length>0?s.first_user_message.slice(0,60):s.id.slice(0,8)}))}catch{return[]}}function ei(e){let{scanId:t,total:s,labelTable:n}=e,r=new Set;return i=>{let o=i.trim();if(!o.startsWith("{"))return;let a;try{a=JSON.parse(o)}catch{return}if(!a||typeof a!="object")return;let l=a;if(!(l.type!=="assistant"||!l.message?.content))for(let c of l.message.content){if(c?.type!=="tool_use"||c.name!=="mcp__recall__apply_tags")continue;let d=c.input,u=typeof d?.sessionId=="string"?d.sessionId:null;!u||r.has(u)||(r.add(u),as(t,{type:"progress",current:r.size,total:s,sessionId:u,sessionLabel:Qr(u,n)}))}}}function ti(e){let t="";return s=>{t+=s.toString("utf8");let n=t.indexOf(`
|
|
689
|
+
`);for(;n!==-1;){let r=t.slice(0,n);t=t.slice(n+1),r.length>0&&e(r),n=t.indexOf(`
|
|
690
|
+
`)}}}async function si(e,t={},s){let n=!!t.scanId,r=n?Zr(e):[],i=new Map(r.map(l=>[l.id,l.label])),o=r.length,a;return n&&t.scanId&&(a=ei({scanId:t.scanId,total:o,labelTable:i})),ds({prompt:cs(e),allowedTools:Jr.split(","),opts:t,onProgress:s,onStdoutLine:a,outputFormat:n?"stream-json":"json"})}async function ni(e,t,s={},n){return ds({prompt:e,allowedTools:t,opts:s,onProgress:n,outputFormat:"json"})}function ds(e){let{prompt:t,allowedTools:s,opts:n,onProgress:r,onStdoutLine:i,outputFormat:o}=e,a=["-p",t,"--output-format",o,"--allowedTools",s.join(","),"--permission-mode","bypassPermissions","--no-session-persistence"];return o==="stream-json"&&a.push("--verbose"),n.model&&a.push("--model",n.model),new Promise(l=>{let c=Kr(qr(),a,{stdio:["ignore","pipe","pipe"]}),d=[],u=[],p=i?ti(i):void 0;c.stdout.on("data",_=>{d.push(_),p&&p(_)}),c.stderr.on("data",_=>{if(u.push(_),r){let f=_.toString("utf8").trim();f&&r(f)}});let g=setTimeout(()=>{c.kill("SIGKILL")},1800*1e3);c.on("close",_=>{clearTimeout(g),l({success:_===0,stdout:Buffer.concat(d).toString("utf8"),stderr:Buffer.concat(u).toString("utf8"),exitCode:_})}),c.on("error",_=>{clearTimeout(g),l({success:!1,stdout:"",stderr:String(_),exitCode:null})})})}var Jr,ce,ps=$(()=>{"use strict";Pe();He();ls();Jr=["mcp__recall__list_tags","mcp__recall__list_sessions_to_tag","mcp__recall__apply_tags"].join(",")});import G from"chalk";import{formatDistanceToNowStrict as rd,parseISO as id}from"date-fns";var h,dt=$(()=>{"use strict";h={dim:G.gray,bold:G.bold,project:G.cyan,user:G.blue,assistant:G.green,tool:G.magenta,warn:G.yellow,err:G.red,ok:G.green,accent:G.hex("#f97316")}});var Rn={};De(Rn,{buildHealthReport:()=>bn,buildPipelineDiagnostic:()=>Tn,detectLabelCollisions:()=>Sn,getFreeDiskBytes:()=>Po,runDoctor:()=>Uo});import{existsSync as pn,readFileSync as Oo,statSync as mn,statfsSync as gn}from"node:fs";import{join as _n}from"node:path";import*as fn from"node:http";function vo(e){let t=[];for(let s of e)if(s.alias){if(xo.test(s.alias)){t.push({session_id:s.session_id,alias:s.alias,violation:"fabricated-origin-label",cwd:s.cwd});continue}if(s.cwd){let n=s.cwd.replace(/\/+$/,"").split("/").pop();n&&s.alias.startsWith(`${n} \xB7 `)&&t.push({session_id:s.session_id,alias:s.alias,violation:"fabricated-cwd-branch",cwd:s.cwd})}}return t}function Co(){let e=_n(b,"daemon.port");if(!pn(e))return null;try{let t=Oo(e,"utf8").trim();if(t.startsWith("{")){let n=JSON.parse(t);return typeof n.port=="number"?n.port:null}let s=Number.parseInt(t,10);return Number.isFinite(s)&&s>0&&s<65536?s:null}catch{return null}}function ko(e,t,s=1500){return new Promise(n=>{let r=fn.request({host:"127.0.0.1",port:e,path:t,method:"GET",timeout:s,headers:{host:"127.0.0.1","user-agent":"recall-doctor"}},i=>{let o=[];i.on("data",a=>o.push(Buffer.isBuffer(a)?a:Buffer.from(a))),i.on("end",()=>{if(!i.statusCode||i.statusCode<200||i.statusCode>=300){n(null);return}try{n(JSON.parse(Buffer.concat(o).toString("utf8")))}catch{n(null)}})});r.on("error",()=>n(null)),r.on("timeout",()=>{r.destroy(),n(null)}),r.end()})}function Do(){let e=_n(b,"terminals.json");if(!pn(e))return{exists:!1,mtimeMs:null,ageSeconds:null};try{let t=mn(e),s=Math.floor((Date.now()-t.mtimeMs)/1e3);return{exists:!0,mtimeMs:t.mtimeMs,ageSeconds:s}}catch{return{exists:!1,mtimeMs:null,ageSeconds:null}}}function Mo(){let e=new Date(Date.now()-ut*36e5).toISOString();try{let t=m().prepare(`SELECT
|
|
691
691
|
COUNT(*) AS total,
|
|
692
692
|
SUM(CASE WHEN sa.alias IS NULL OR sa.alias = '' THEN 1 ELSE 0 END) AS without_alias,
|
|
693
693
|
SUM(CASE WHEN (sa.alias IS NULL OR sa.alias = '')
|
|
694
694
|
AND s.auto_title_source = 'heuristic' THEN 1 ELSE 0 END) AS heuristic_only
|
|
695
695
|
FROM sessions s
|
|
696
696
|
LEFT JOIN session_aliases sa ON sa.session_id = s.id
|
|
697
|
-
WHERE s.started_at >= ?`).get(e),s=t.total??0,n=t.without_alias??0,
|
|
697
|
+
WHERE s.started_at >= ?`).get(e),s=t.total??0,n=t.without_alias??0,r=t.heuristic_only??0,i=s>0?r/s:0;return{total:s,withoutAlias:n,heuristicOnly:r,fractionHeuristic:i}}catch{return{total:0,withoutAlias:0,heuristicOnly:0,fractionHeuristic:0}}}async function Tn(){let e=Co(),t=Do(),s=Mo(),n=!1,r=null,i=null,o=null,a=null,l=null;if(e){let u=await ko(e,"/api/health");if(u){n=!0,r=typeof u.uptimeSeconds=="number"?u.uptimeSeconds:null,i=typeof u.version=="string"?u.version:null,o=typeof u.pipeline?.silentTerminalRejections=="number"?u.pipeline.silentTerminalRejections:null,a=u.pipeline?.lastTerminalSyncAt??null;let p=u.pipeline?.autoExtract;p&&(l={circuitBroken:p.circuitBroken===!0,reason:p.reason??null,brokenAt:typeof p.brokenAt=="number"?p.brokenAt:null,consecutiveZeroTokenRuns:typeof p.consecutiveZeroTokenRuns=="number"?p.consecutiveZeroTokenRuns:0})}}let c=[];if(n||c.push("Daemon not reachable on 127.0.0.1 \u2014 start it with `recall start` before further diagnosis."),o!==null&&o>0&&c.push(`Daemon rejected ${o.toLocaleString()} /api/terminal/* request(s) without a valid X-Recall-Token. The editor extension is outdated relative to this daemon \u2014 tab names are not flowing through. Reinstall: \`code --install-extension extensions/vscode/clauderecall-vscode-*.vsix\`, then reload the extension host (Cmd/Ctrl+Shift+P \u2192 "Developer: Restart Extension Host").`),n&&r!==null&&r>30)if(!a)c.push(`Daemon has been running ${Math.round(r/60)} min but has not yet seen a single successful /api/terminal/sync. Either no editor extension is installed/active, or every attempt is being rejected (see counter above).`);else{let u=Date.now()-Date.parse(a);Number.isFinite(u)&&u>hn&&c.push(`Last successful /api/terminal/sync was ${Math.round(u/6e4)} min ago \u2014 extension may have crashed, been disabled, or is failing auth. Run \`recall doctor\` again after restarting the extension host.`)}return!n&&t.exists&&t.ageSeconds!==null&&t.ageSeconds>24*3600&&c.push(`~/.recall/terminals.json is ${Math.round(t.ageSeconds/3600)}h old \u2014 pipeline has not produced fresh data recently. Likely root cause is the same as the rejection counter would show; start the daemon and re-run.`),l?.circuitBroken&&c.push(`Auto-extract circuit breaker tripped \u2014 ${l.reason??"reason unknown"}. Run \`recall semantic auto-extract off\` then \`... on\` to reset, or restart the daemon.`),s.total>=3&&s.fractionHeuristic>=En&&c.push(`${s.heuristicOnly}/${s.total} sessions in the last ${ut}h (${Math.round(s.fractionHeuristic*100)}%) fell back to the heuristic first-message title. A healthy pipeline rate is < 20%. Either the extension is not syncing tab names, or the correlator cannot disambiguate. Reinstall the extension and verify the rejection counter drops to 0.`),{state:n?c.length>0?"degraded":"ok":"down",flags:c,daemon:{running:n,port:e,uptimeSeconds:r,version:i},runtime:{silentTerminalRejections:o,lastTerminalSyncAt:a,autoExtract:l},terminalsJson:t,recentSessions:s}}function Z(e){return e<1024?`${e} B`:e<1024**2?`${(e/1024).toFixed(1)} KB`:e<1024**3?`${(e/1024**2).toFixed(1)} MB`:`${(e/1024**3).toFixed(2)} GB`}function dn(e){try{return mn(e).size}catch{return 0}}function un(e){try{return m().prepare(`SELECT COUNT(*) AS n FROM ${e}_data WHERE block = 1`).get().n}catch{return 0}}function Sn(){try{return m().prepare(`SELECT p.name AS project,
|
|
698
|
+
COALESCE(NULLIF(sa.alias, ''), s.auto_title, substr(s.first_user_message, 1, 60)) AS label,
|
|
699
|
+
COUNT(*) AS count,
|
|
700
|
+
MAX(CASE WHEN sa.alias IS NOT NULL AND sa.alias != '' THEN 1 ELSE 0 END) AS any_aliased
|
|
701
|
+
FROM sessions s
|
|
702
|
+
JOIN projects p ON p.id = s.project_id
|
|
703
|
+
LEFT JOIN session_aliases sa ON sa.session_id = s.id
|
|
704
|
+
WHERE s.indexed_at > datetime('now', '-7 days')
|
|
705
|
+
AND COALESCE(NULLIF(sa.alias, ''), s.auto_title, s.first_user_message) IS NOT NULL
|
|
706
|
+
GROUP BY p.name, label
|
|
707
|
+
HAVING count >= 2
|
|
708
|
+
ORDER BY count DESC, label ASC
|
|
709
|
+
LIMIT 10`).all().map(e=>{let t=e;return{project:t.project,label:t.label,count:t.count,anyAliased:t.any_aliased===1}})}catch{return[]}}function bn(e){let t=m(),s=t.pragma("page_size",{simple:!0})||4096,n=t.pragma("page_count",{simple:!0})||0,r=t.pragma("freelist_count",{simple:!0})||0;e?.("Checking database integrity");let i="ok";try{let y=t.pragma("quick_check").map(M=>M.quick_check);i=y.length===1&&y[0]==="ok"?"ok":y.join("; ")}catch(S){i=`check failed: ${S.message}`}let o=dn(oe),a=dn(`${oe}-wal`),l=0,c=0;try{let S=gn(b);l=Number(S.bavail)*Number(S.bsize),c=Number(S.blocks)*Number(S.bsize)}catch{}e?.("Counting rows");let d=t.prepare(`SELECT
|
|
698
710
|
(SELECT COUNT(*) FROM projects) AS projects,
|
|
699
711
|
(SELECT COUNT(*) FROM sessions) AS sessions,
|
|
700
712
|
(SELECT COUNT(*) FROM messages) AS messages,
|
|
701
|
-
(SELECT COUNT(*) FROM message_usage) AS message_usage`).get(),u=0;try{u=t.prepare("SELECT COUNT(*) AS n FROM vec_chunks").get().n}catch{}e?.("Measuring FTS fragmentation");let p=[];
|
|
713
|
+
(SELECT COUNT(*) FROM message_usage) AS message_usage`).get(),u=0;try{u=t.prepare("SELECT COUNT(*) AS n FROM vec_chunks").get().n}catch{}e?.("Measuring FTS fragmentation");let p=[];l>0&&l<1*1024**3&&p.push(`Disk free is ${Z(l)} \u2014 heavy operations (synthesis, extract-outputs, vector ingest) may fail.`),a>50*1024**2&&p.push(`WAL is ${Z(a)} \u2014 run \`recall optimize\` to truncate it.`),r>n*.2&&n>1e3&&p.push(`${r.toLocaleString()} free pages (${(r/n*100).toFixed(0)}% of file) \u2014 \`recall optimize --vacuum\` will reclaim them.`);let g=un("messages_fts"),_=un("sessions_fts");g>16&&p.push(`messages_fts has ${g} segments \u2014 \`recall optimize\` will merge them.`);let f=0;try{f=t.prepare("SELECT COUNT(*) AS n FROM chunk_queue").get().n}catch{}let E=!1;try{E=t.prepare("SELECT value FROM app_settings WHERE key = 'semantic_enabled'").get()?.value==="1"}catch{}return!E&&f>0?p.push(`${f.toLocaleString()} rows in chunk_queue with semantic disabled \u2014 schema gate is stale; restart daemon (v0.67+) to migrate.`):f>1e5&&p.push(`chunk_queue has ${f.toLocaleString()} pending rows \u2014 embedder is behind. \`recall semantic backfill\` to drain manually.`),{db:{sizeBytes:o,walSizeBytes:a,pageCount:n,pageSize:s,freelistCount:r,freelistBytes:r*s,integrity:i},disk:{freeBytes:l,totalBytes:c},fts:{messages:{fragments:g},sessions:{fragments:_}},vectors:{rows:u},rows:{projects:d.projects,sessions:d.sessions,messages:d.messages,messageUsage:d.message_usage},chunkQueue:{size:f,semanticEnabled:E},warnings:p}}function Fo(e){if(!e)return{stage:()=>{},done:()=>{}};let t=!!process.stderr.isTTY,s="",n=0,r=()=>{if(!s)return;let i=Date.now()-n,o=i<1e3?`${i}ms`:`${(i/1e3).toFixed(1)}s`;t?process.stderr.write(`\r\x1B[2K ${h.ok("\u2713")} ${s} ${h.dim(`(${o})`)}
|
|
702
714
|
`):process.stderr.write(` \u2713 ${s} (${o})
|
|
703
|
-
`),s=""};return{stage(
|
|
704
|
-
`)},done:
|
|
715
|
+
`),s=""};return{stage(i){r(),s=i,n=Date.now(),t?process.stderr.write(` ${h.dim("\u2026")} ${s}`):process.stderr.write(` \u2026 ${s}
|
|
716
|
+
`)},done:r}}async function Uo(e={}){let t=Fo(!e.json);t.stage("Scanning session aliases");let s=m().prepare(`SELECT sa.session_id AS session_id, sa.alias AS alias, s.cwd AS cwd
|
|
705
717
|
FROM session_aliases sa
|
|
706
718
|
LEFT JOIN sessions s ON s.id = sa.session_id
|
|
707
|
-
WHERE sa.alias IS NOT NULL AND sa.alias != ''`).all(),n=
|
|
708
|
-
`);let c=
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
`),
|
|
713
|
-
|
|
719
|
+
WHERE sa.alias IS NOT NULL AND sa.alias != ''`).all(),n=vo(s),r=bn(t.stage);t.stage("Probing daemon");let i=await Tn();t.stage("Detecting label collisions");let o=Sn();if(t.done(),e.json){process.stdout.write(JSON.stringify({scanned:s.length,violations:n.length,items:n,health:r,pipeline:i,labelCollisions:o},null,2)),process.stdout.write(`
|
|
720
|
+
`);let c=i.state==="degraded";return n.length===0&&r.db.integrity==="ok"&&!c?0:1}console.log(h.dim("\u2014 System health \u2014")),console.log(` Database ${Z(r.db.sizeBytes)} (${r.rows.messages.toLocaleString()} messages across ${r.rows.sessions.toLocaleString()} sessions, ${r.rows.projects.toLocaleString()} projects)`),console.log(` WAL ${Z(r.db.walSizeBytes)} (capped at 64 MB; truncated on clean shutdown)`),console.log(` Free pages ${r.db.freelistCount.toLocaleString()} (${Z(r.db.freelistBytes)} reclaimable via VACUUM)`);{let c=r.fts.messages.fragments,d=r.fts.sessions.fragments,u=c===0&&d===0;console.log(` FTS segments messages=${c}, sessions=${d}`+(u?" (merged \u2014 search uses the index)":" (lower is faster \u2014 `recall optimize` merges them)"))}if(r.chunkQueue.size>0){let d=r.chunkQueue.size>1e5?h.warn(r.chunkQueue.size.toLocaleString()):r.chunkQueue.size.toLocaleString();console.log(` Embed queue ${d}`+(r.chunkQueue.semanticEnabled?" (worker is enabled; should drain over time)":" (semantic disabled \u2014 should be 0; if not, schema migration may be stale)"))}if(console.log(` Vector rows ${r.vectors.rows.toLocaleString()}`),r.disk.totalBytes>0){let c=r.disk.freeBytes/r.disk.totalBytes*100;console.log(` Disk free ${Z(r.disk.freeBytes)} of ${Z(r.disk.totalBytes)} (${c.toFixed(1)}%)`)}if(console.log(` Integrity ${r.db.integrity==="ok"?h.ok("ok"):h.err(r.db.integrity)}`),r.warnings.length>0){console.log("");for(let c of r.warnings)console.log(` ${h.warn("!")} ${c}`)}if(console.log(""),console.log(h.dim("\u2014 Pipeline health (tab-name \u2192 session alias) \u2014")),i.daemon.running?console.log(` Daemon ${h.ok("running")} (port ${i.daemon.port}, version ${i.daemon.version??"?"}, up ${i.daemon.uptimeSeconds!==null?Math.round(i.daemon.uptimeSeconds/60)+" min":"?"})`):console.log(` Daemon ${h.warn("not reachable")}`),i.runtime.silentTerminalRejections!==null){let c=i.runtime.silentTerminalRejections;console.log(` Auth rejections ${c===0?h.ok("0"):h.err(c.toLocaleString())} (extension /api/terminal/* requests denied without a valid X-Recall-Token)`)}if(i.runtime.autoExtract){let c=i.runtime.autoExtract;c.circuitBroken?console.log(` Auto-extract ${h.err("circuit broken")} (${c.reason??"unknown"} \u2014 toggle off/on to reset)`):c.consecutiveZeroTokenRuns>0&&console.log(` Auto-extract ${h.warn(`${c.consecutiveZeroTokenRuns} zero-token run(s)`)} (breaker trips at 3)`)}if(i.runtime.lastTerminalSyncAt!==null){let c=Date.now()-Date.parse(i.runtime.lastTerminalSyncAt),d=Number.isFinite(c)?Math.round(c/6e4):null,u=d!==null&&c>hn;console.log(` Last ext sync ${u?h.warn(`${d} min ago`):h.ok(d===0?"just now":`${d} min ago`)} (most recent successful POST /api/terminal/sync)`)}else i.daemon.running&&console.log(` Last ext sync ${h.warn("never")} (no extension has called /api/terminal/sync since the daemon started)`);if(i.terminalsJson.exists&&i.terminalsJson.ageSeconds!==null){let c=Math.round(i.terminalsJson.ageSeconds/3600),d=i.terminalsJson.ageSeconds>24*3600;console.log(` terminals.json ${d?h.warn(`${c}h old`):h.ok(`${c}h old`)} (persisted registry mtime \u2014 fresh means extensions are connecting)`)}let a=i.recentSessions;if(a.total>0){let c=Math.round(a.fractionHeuristic*100),d=a.fractionHeuristic>=En&&a.total>=3;console.log(` Recent titles ${d?h.err(`${c}% heuristic`):h.ok(`${c}% heuristic`)} (${a.heuristicOnly}/${a.total} sessions in last ${ut}h fell back to first-message title)`)}if(i.flags.length>0){console.log("");for(let c of i.flags)console.log(` ${h.warn("!")} ${c}`)}if(console.log(""),console.log(h.dim("\u2014 Label collisions (last 7d) \u2014")),o.length===0)console.log(h.ok(" \u2713 no session-list label collisions in the last week"));else{let c=o.filter(d=>!d.anyAliased);console.log(` ${h.warn(`${o.length} group(s)`)} of 2+ sessions in the same project share an identical display label \u2014 N parallel runs ended up with the same heuristic title.`);for(let d of o.slice(0,5)){let u=d.anyAliased?h.dim("partial alias"):h.warn("NO alias"),p=d.label.length>60?`${d.label.slice(0,57)}\u2026`:d.label;console.log(` ${h.dim(`${d.count}\xD7`)} ${p} ${u} ${h.dim(`(${d.project})`)}`)}o.length>5&&console.log(h.dim(` \u2026 and ${o.length-5} more (--json for full list)`)),console.log(""),console.log(h.dim(" The display layer auto-disambiguates these (HH:MM / msgs / UUID suffix), so customers\n never see two identical rows. To fix at the source \u2014 when spawning `claude -p` from\n a script or Captain-Code subordinate, prepend each prompt with:")),console.log(h.dim(" <!-- claude-recall-alias: T2.1 Run-Prospects -->")),console.log(h.dim(` The watcher will read the header, alias the session, and strip the marker from the
|
|
721
|
+
displayed first user message. See docs/HANDOFF.md \u2192 "Alias header convention".`)),c.length>0&&(console.log(""),console.log(h.warn(` ${c.length} of these groups have NO alias on any row \u2014 strongest candidates
|
|
722
|
+
for the header-alias fix above.`)))}if(console.log(""),console.log(h.dim("\u2014 Tab-name invariant \u2014")),n.length===0)return console.log(h.ok(` \u2713 holds across ${s.length.toLocaleString()} aliased session${s.length===1?"":"s"}`)),console.log(h.dim(" No fabricated origin labels (`VS Code \xB7 cwd \xB7 branch`) found. No deprecated cwd-branch synthesis found.")),r.db.integrity==="ok"?0:1;console.log(h.err(`\u2717 ${n.length} invariant violation${n.length===1?"":"s"} found across ${s.length.toLocaleString()} aliased sessions`)),console.log("");let l=new Map;for(let c of n){let d=l.get(c.violation)??[];d.push(c),l.set(c.violation,d)}for(let[c,d]of l){console.log(h.warn(` ${c} (${d.length})`));for(let u of d.slice(0,10))console.log(` ${u.session_id.slice(0,8)} ${h.dim("\u2192")} ${JSON.stringify(u.alias)}`);d.length>10&&console.log(h.dim(` \u2026 and ${d.length-10} more (rerun with --json for the full list)`)),console.log("")}return console.log(h.dim('Remediation: `recall name <id-prefix> ""` clears a bad alias so the heuristic title takes over,\nor `recall name <id-prefix> "<actual tab name>"` sets it to the real value.')),1}function Po(){try{let e=gn(b);return Number(e.bavail)*Number(e.bsize)}catch{return 0}}var Io,xo,hn,ut,En,yn=$(()=>{"use strict";dt();L();F();Io=["VS Code","VS Code Insiders","Cursor","Windsurf","Warp","iTerm","Terminal","WezTerm","Windows Terminal","Kitty","Alacritty"],xo=new RegExp(`^(${Io.map(e=>e.replace(/ /g,"\\s")).join("|")})\\s\xB7\\s`);hn=5*6e4,ut=24,En=.5});var Nn={};De(Nn,{runOptimize:()=>Bo});import{existsSync as $o,readFileSync as jo}from"node:fs";import{join as Xo}from"node:path";function Ho(){let e=Xo(b,"daemon.pid");if(!$o(e))return!1;try{let t=parseInt(jo(e,"utf-8").trim(),10);return!Number.isFinite(t)||t<=0?!1:(process.kill(t,0),!0)}catch{return!1}}async function me(e,t){let s=Date.now();try{return t(),{step:e,ok:!0,durationMs:Date.now()-s}}catch(n){return{step:e,ok:!1,durationMs:Date.now()-s,error:n.message}}}async function Bo(e={}){let t=m(),s=[];if(e.vacuum&&Ho())return e.json?(process.stdout.write(JSON.stringify({ok:!1,error:"daemon-running",message:"VACUUM requires the daemon to be stopped. Run `recall stop`, then re-run with --vacuum."},null,2)+`
|
|
723
|
+
`),2):(console.error(h.err("\u2717 VACUUM requires the daemon to be stopped. Run `recall stop` first, then re-run with --vacuum.")),2);s.push(await me("wal_checkpoint(TRUNCATE)",()=>{t.pragma("wal_checkpoint(TRUNCATE)")})),s.push(await me("messages_fts optimize",()=>{t.exec("INSERT INTO messages_fts(messages_fts) VALUES('optimize');")})),s.push(await me("sessions_fts optimize",()=>{t.exec("INSERT INTO sessions_fts(sessions_fts) VALUES('optimize');")})),s.push(await me("PRAGMA optimize",()=>{t.exec("PRAGMA optimize")})),e.vacuum&&s.push(await me("VACUUM",()=>{t.exec("VACUUM")}));let n=s.filter(r=>!r.ok);if(e.json)return process.stdout.write(JSON.stringify({ok:n.length===0,steps:s,vacuum:!!e.vacuum},null,2)+`
|
|
724
|
+
`),n.length===0?0:1;for(let r of s){let i=r.ok?h.ok("\u2713"):h.err("\u2717"),o=`${r.durationMs} ms`;console.log(` ${i} ${r.step.padEnd(28)} ${h.dim(o)}`),r.error&&console.log(` ${h.err(r.error)}`)}return n.length===0?(console.log(""),console.log(h.ok("All maintenance passes completed.")),e.vacuum||console.log(h.dim(" Run `recall optimize --vacuum` (with the daemon stopped) to reclaim disk pages from deleted rows.")),0):(console.log(""),console.log(h.warn(`${n.length} step(s) failed \u2014 review the errors above.`)),1)}var wn=$(()=>{"use strict";dt();L();F()});L();import{McpServer as Wo}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as Go}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as T}from"zod";import{fileURLToPath as Yo}from"node:url";var Dn=/<(local-command-caveat|local-command-stdout|command-name|command-message|command-args|system-reminder|user-prompt-submit-hook|task-notification)[\s\S]*?<\/\1>/gi,Mn=/⚡ \*\*Tool call · `[^`]+`\*\*\s*\n+```json\n[\s\S]*?\n```/g,Fn=/\*\*Tool result\*\*\s*\n+```\n[\s\S]*?\n```/g;function Un(e){return e.replace(Dn,"").trim()}function Pn(e){let t=e.replace(Mn,"[tool call]");return t=t.replace(Fn,"[tool result]"),t=t.replace(/_\(unknown block: thinking\)_/g,""),t=t.replace(/(?:\[tool call\]|\[tool result\])(?:\s*(?:\[tool call\]|\[tool result\]))+/g,"[tool activity]"),t=t.replace(/\n{3,}/g,`
|
|
725
|
+
|
|
726
|
+
`),t.trim()}function $n(e){return e.role??e.type??"message"}function bt(e,t,s={}){let n=s.mode??"condensed",r=s.includeSidechain===!0,i=s.since?Date.parse(s.since):0,o=t.filter(d=>!(!r&&d.is_sidechain===1||i&&d.timestamp&&Date.parse(d.timestamp)<i)),a=[];s.prelude&&(a.push(s.prelude.trim()),a.push("")),a.push(`# Claude Recall, past session context (${n})`),a.push(""),a.push(`- **Project**: ${e.project_name}`),e.decoded_path&&a.push(`- **Path**: \`${e.decoded_path}\``),a.push(`- **Session ID**: \`${e.id}\``),e.started_at&&a.push(`- **Started**: ${e.started_at}`),e.ended_at&&a.push(`- **Ended**: ${e.ended_at}`),e.git_branch&&a.push(`- **Branch**: \`${e.git_branch}\``),a.push(`- **Messages**: ${o.length}`),a.push(""),a.push("> This is a transcript of a previous Claude Code session, included for context. Refer back to it when the user asks about past decisions, code written, or problems debugged in this work."),a.push(""),a.push("---"),a.push("");let l=0,c=0;for(let d of o){let u=d.content_text??"",p=Un(u);n==="condensed"&&(p=Pn(p));let g=p.length>0,_=!!d.tool_names&&d.tool_names.length>0;if(!g&&!_){c+=1;continue}let f=$n(d),E=d.timestamp?` \`${d.timestamp}\``:"";a.push(`## ${f}${E}`),a.push(""),_&&n==="condensed"&&(a.push(`_tools used: ${d.tool_names}_`),a.push("")),g&&(a.push(p),a.push("")),l+=1}return a.push("---"),a.push(""),a.push(`_${l} messages included_`+(c?`, ${c} empty skipped`:"")+(r?"":", subagent/sidechain hidden")+"."),a.join(`
|
|
727
|
+
`)}Ee();Pe();import{existsSync as Wn,mkdirSync as Oa,readFileSync as Gn,writeFileSync as Ia,chmodSync as xa}from"node:fs";import{homedir as Yn}from"node:os";import{join as wt}from"node:path";import{z}from"zod";function zn(){return process.env.RECALL_HOME??wt(Yn(),".recall")}function Kn(){return wt(zn(),"config.json")}var Jn=z.object({enabled:z.boolean().default(!1),backend:z.enum(["api","mcp"]).default("api"),apiKey:z.string().optional(),model:z.string().default("claude-opus-4-7"),maxTagsPerSession:z.number().int().min(1).max(10).default(4),minTagsPerSession:z.number().int().min(1).max(10).default(2),autopilot:z.boolean().default(!1)}),$e={enabled:!1,backend:"api",model:"claude-opus-4-7",maxTagsPerSession:4,minTagsPerSession:2,autopilot:!1};function qn(){let e=Kn();if(!Wn(e))return{};try{return JSON.parse(Gn(e,"utf8"))}catch(t){return console.error("[auto-tag-config] failed to parse config.json, using defaults:",t),{}}}function je(){let e=qn().autoTag;if(!e)return{...$e};let t=Jn.safeParse({...$e,...e});return t.success?t.data:{...$e}}He();L();Ee();import{z as I}from"zod";L();F();import{writeFileSync as lr}from"node:fs";import{join as cr}from"node:path";var dr=cr(b,"aliases.json");function At(e){try{let t=JSON.parse(e);if(Array.isArray(t))return t}catch{}return[]}function ur(){return m().prepare("SELECT session_id, alias, updated_at, previous_aliases FROM session_aliases").all().map(t=>({session_id:t.session_id,alias:t.alias,updated_at:t.updated_at,previous_aliases:At(t.previous_aliases)}))}function Ot(e,t){let s=t.trim();if(!s)throw new Error("alias must be non-empty");let n=m(),r=new Date().toISOString(),i=n.prepare("SELECT alias, previous_aliases FROM session_aliases WHERE session_id = ?").get(e),o=[];return i&&(o=At(i.previous_aliases),i.alias!==s&&o.push({alias:i.alias,replaced_at:r})),n.prepare(`INSERT INTO session_aliases (session_id, alias, updated_at, previous_aliases)
|
|
714
728
|
VALUES (?, ?, ?, ?)
|
|
715
729
|
ON CONFLICT(session_id) DO UPDATE SET
|
|
716
730
|
alias = excluded.alias,
|
|
717
731
|
updated_at = excluded.updated_at,
|
|
718
|
-
previous_aliases = excluded.previous_aliases`).run(e,s,
|
|
732
|
+
previous_aliases = excluded.previous_aliases`).run(e,s,r,JSON.stringify(o)),pr(),{session_id:e,alias:s,updated_at:r,previous_aliases:o}}function pr(){try{x();let e=ur(),t={schema:"claude-recall.aliases.v1",backed_up_at:new Date().toISOString(),aliases:e};lr(dr,JSON.stringify(t,null,2))}catch(e){console.error("[aliases] backup failed:",e)}}L();F();import{writeFileSync as mr,mkdirSync as gr,existsSync as _r}from"node:fs";import{join as It}from"node:path";var Be=It(b,"notes");function xt(e){try{let t=JSON.parse(e);if(Array.isArray(t))return t}catch{}return[]}function fr(e){if(!e)return[];try{let t=JSON.parse(e);return Array.isArray(t)?t.filter(s=>!!s&&typeof s=="object"&&typeof s.synopsis=="string"&&typeof s.replaced_at=="string"):[]}catch{return[]}}function hr(){x(),_r(Be)||gr(Be,{recursive:!0})}function Er(e){return{session_id:e.session_id,content:e.content,updated_at:e.updated_at,previous_versions:xt(e.previous_versions),auto_synopsis:e.auto_synopsis??null,auto_synopsis_generated_at:e.auto_synopsis_generated_at??null,auto_synopsis_history:fr(e.auto_synopsis_history)}}var Tr="session_id, content, updated_at, previous_versions, auto_synopsis, auto_synopsis_generated_at, auto_synopsis_history";function We(e){let t=m().prepare(`SELECT ${Tr} FROM session_notes WHERE session_id = ?`).get(e);return t?Er(t):null}function vt(e,t){let s=m(),n=new Date().toISOString(),r=s.prepare("SELECT content, previous_versions FROM session_notes WHERE session_id = ?").get(e),i=[];return r&&(i=xt(r.previous_versions),r.content!==t&&r.content.length>0&&i.push({content:r.content,replaced_at:n})),s.prepare(`INSERT INTO session_notes (session_id, content, updated_at, previous_versions)
|
|
719
733
|
VALUES (?, ?, ?, ?)
|
|
720
734
|
ON CONFLICT(session_id) DO UPDATE SET
|
|
721
735
|
content = excluded.content,
|
|
722
736
|
updated_at = excluded.updated_at,
|
|
723
|
-
previous_versions = excluded.previous_versions`).run(e,t,n,JSON.stringify(
|
|
724
|
-
`;
|
|
725
|
-
VALUES (?, ?, ?, ?, ?)`).run(e,n,t,s?JSON.stringify(s):null,
|
|
737
|
+
previous_versions = excluded.previous_versions`).run(e,t,n,JSON.stringify(i)),Sr(e,t,n),We(e)??{session_id:e,content:t,updated_at:n,previous_versions:i,auto_synopsis:null,auto_synopsis_generated_at:null,auto_synopsis_history:[]}}function Sr(e,t,s){try{hr();let n=It(Be,`${e}.md`),r=`<!-- Claude Recall note \xB7 session ${e} \xB7 updated ${s} -->
|
|
738
|
+
`;mr(n,r+t)}catch(n){console.error("[notes] mirror write failed:",n)}}L();F();import{randomUUID as br}from"node:crypto";import{writeFileSync as Rr,readFileSync as Ja,existsSync as qa}from"node:fs";import{join as yr}from"node:path";var Nr=yr(b,"collections.json"),Ct=8;function wr(e){return{...e}}function Ge(e,t,s,n=null,r=new Date().toISOString()){m().prepare(`INSERT INTO collection_events (collection_id, session_id, action, payload, at)
|
|
739
|
+
VALUES (?, ?, ?, ?, ?)`).run(e,n,t,s?JSON.stringify(s):null,r)}function Lr(e){let t=m().prepare("SELECT * FROM collections WHERE id = ?").get(e);if(!t)throw new Error(`collection not found: ${e}`);return t}function Ar(e){if(!e)return 0;let t=0,s=e,n=new Set,r=m();for(;s;){if(n.has(s))throw new Error("collection cycle detected");n.add(s);let i=r.prepare("SELECT parent_id FROM collections WHERE id = ?").get(s);if(!i)break;t+=1,s=i.parent_id}return t}function kt(e){let t=m().prepare("SELECT * FROM collections WHERE id = ?").get(e);return t?wr(t):null}function Dt(e){let t=(e.name??"").trim();if(!t)throw new Error("name required");if(t.length>120)throw new Error("name too long (max 120 chars)");let s=m(),n=new Date().toISOString(),r=br();if(e.parent_id){if(!kt(e.parent_id))throw new Error("parent collection not found");if(Ar(e.parent_id)>=Ct-1)throw new Error(`max collection depth is ${Ct}`)}return s.transaction(()=>{s.prepare(`INSERT INTO collections
|
|
726
740
|
(id, name, description, icon, color, parent_id, sort_key, created_at, updated_at, archived_at)
|
|
727
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)`).run(
|
|
728
|
-
VALUES (?, ?, ?, ?, ?, ?)`).run(e,t,
|
|
741
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)`).run(r,t,e.description??null,e.icon??null,e.color??null,e.parent_id??null,e.sort_key??"",n,n),Ge(r,"create",{name:t,parent_id:e.parent_id??null,icon:e.icon??null,color:e.color??null},null,n)})(),Ye(),kt(r)}function Mt(e,t,s=null,n={}){let r=m();if(Lr(e),!r.prepare("SELECT 1 FROM sessions WHERE id = ?").get(t))throw new Error(`session not found: ${t}`);if(r.prepare("SELECT 1 FROM collection_sessions WHERE collection_id = ? AND session_id = ?").get(e,t))return{added:!1};let a=n.source??"manual",l=n.rule_id??null;if(a==="auto"&&!l)throw new Error("auto membership requires a rule_id");let c=new Date().toISOString();return r.transaction(()=>{r.prepare(`INSERT INTO collection_sessions (collection_id, session_id, added_at, note, source, rule_id)
|
|
742
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(e,t,c,s,a,l),Ge(e,"add",{note:s,source:a,rule_id:l},t,c)})(),Ye(),{added:!0}}function Ft(e,t){let s=m();if(!s.prepare("SELECT 1 FROM collection_sessions WHERE collection_id = ? AND session_id = ?").get(e,t))return{removed:!1};let r=new Date().toISOString();return s.transaction(()=>{s.prepare("DELETE FROM collection_sessions WHERE collection_id = ? AND session_id = ?").run(e,t),Ge(e,"remove",null,t,r)})(),Ye(),{removed:!0}}function Or(){return m().prepare(`SELECT id, collection_id, session_id, action, payload, at
|
|
729
743
|
FROM collection_events
|
|
730
|
-
ORDER BY at ASC, id ASC`).all()}function
|
|
744
|
+
ORDER BY at ASC, id ASC`).all()}function Ye(){try{x();let e=m(),t=e.prepare(`SELECT id, name, description, icon, color, parent_id, sort_key,
|
|
731
745
|
created_at, updated_at, archived_at
|
|
732
746
|
FROM collections
|
|
733
747
|
ORDER BY COALESCE(parent_id, ''), sort_key, LOWER(name)`).all(),s=e.prepare(`SELECT collection_id, session_id, added_at, note, source, rule_id
|
|
734
748
|
FROM collection_sessions
|
|
735
|
-
ORDER BY collection_id, added_at`).all(),n=
|
|
736
|
-
VALUES (?, ?, ?, ?, ?, ?)`).run(e.tool,s,e.result,e.errorMessage??null,e.caller??null,t)}var K=class{capacity;windowMs;hits=[];constructor(t=
|
|
749
|
+
ORDER BY collection_id, added_at`).all(),n=Or(),r={schema:"claude-recall.collections.v1",backed_up_at:new Date().toISOString(),collections:t,memberships:s,events:n};Rr(Nr,JSON.stringify(r,null,2))}catch(e){console.error("[collections] backup failed:",e)}}L();var Ke=60,Ir=6e4,se=class extends Error{retryAfterMs;constructor(t){super(`MCP write rate limit exceeded \u2014 try again in ${Math.ceil(t/1e3)}s`),this.name="RateLimitError",this.retryAfterMs=t}};function ze(e){let t=new Date().toISOString(),s;try{s=JSON.stringify(e.args??null)}catch{s='"<unserializable>"'}m().prepare(`INSERT INTO mcp_audit_events (tool, args_json, result, error_message, caller, at)
|
|
750
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(e.tool,s,e.result,e.errorMessage??null,e.caller??null,t)}var K=class{capacity;windowMs;hits=[];constructor(t=Ke,s=Ir){this.capacity=t,this.windowMs=s}consume(t=Date.now()){if(this.evict(t),this.hits.length>=this.capacity){let s=this.hits[0],n=Math.max(1,s+this.windowMs-t);throw new se(n)}this.hits.push(t)}remaining(t=Date.now()){return this.evict(t),Math.max(0,this.capacity-this.hits.length)}reset(){this.hits.length=0}evict(t){let s=t-this.windowMs;for(;this.hits.length>0&&this.hits[0]<s;)this.hits.shift()}};async function w(e){try{e.limiter.consume()}catch(t){throw t instanceof se&&ze({tool:e.tool,args:e.args,result:"rate_limited",errorMessage:t.message,caller:e.caller}),t}try{let t=await e.run();return ze({tool:e.tool,args:e.args,result:"ok",caller:e.caller}),t}catch(t){let s=t instanceof Error?t.message:String(t);throw ze({tool:e.tool,args:e.args,result:"error",errorMessage:s,caller:e.caller}),t}}L();import{z as xr}from"zod";function X(e){let t=e.trim();if(t.length<4)return null;let s=m();if(t.length>=32)return s.prepare("SELECT id FROM sessions WHERE id = ?").get(t)?.id??null;let n=s.prepare("SELECT id FROM sessions WHERE id LIKE ? LIMIT 2").all(`${t}%`);return n.length===1?n[0].id:null}function Se(e){return{content:[{type:"text",text:e}],isError:!0}}function N(e){return e instanceof se?Se(e.message):e instanceof xr.ZodError?Se(`invalid input: ${e.message}`):e instanceof Error&&e.message.startsWith("SQLITE_")?Se("database constraint error"):Se(e instanceof Error?e.message:String(e))}function Q(e){return{content:[{type:"text",text:JSON.stringify(e,null,2)}]}}function J(e){return{content:[{type:"text",text:e}],isError:!0}}var vr=`
|
|
737
751
|
|
|
738
752
|
---
|
|
739
753
|
|
|
740
|
-
`,
|
|
754
|
+
`,Cr=5e5;function Ut(e,t={}){let s=t.limiter??new K;e.registerTool("add_tag",{title:"Add tag to a session",description:"Apply a single tag to a session. Tags are normalized server-side (lowercase, hyphens, strip #). Idempotent.",inputSchema:{sessionId:I.string().describe("Full session UUID or 8+-character prefix."),tag:I.string().min(1).describe("Tag to add. Normalized: lowercase, dashes, max 40 chars.")}},async n=>{try{let r=X(n.sessionId);if(!r)return J(`session not found or prefix ambiguous: ${n.sessionId}`);let i=await w({tool:"add_tag",args:{sessionId:r,tag:n.tag},limiter:s,run:()=>fe(r,n.tag)});return Q({sessionId:r,...i})}catch(r){return N(r)}}),e.registerTool("remove_tag",{title:"Remove tag from a session",description:"Remove a single tag from a session. The removal is recorded in tag_events (append-only) so history is preserved.",inputSchema:{sessionId:I.string().describe("Full session UUID or 8+-character prefix."),tag:I.string().min(1).describe("Tag to remove (normalized server-side).")}},async n=>{try{let r=X(n.sessionId);if(!r)return J(`session not found or prefix ambiguous: ${n.sessionId}`);let i=V(n.tag);if(!i)return J("tag must contain at least one alphanumeric character");let o=await w({tool:"remove_tag",args:{sessionId:r,tag:i},limiter:s,run:()=>Rt(r,i)});return Q({sessionId:r,...o})}catch(r){return N(r)}}),e.registerTool("set_alias",{title:"Set session alias",description:"Set a human-friendly alias for a session. Previous alias is archived to previous_aliases (never destroyed). Session UUID remains the immutable primary key.",inputSchema:{sessionId:I.string().describe("Full session UUID or 8+-character prefix."),alias:I.string().min(1).max(120).describe("New alias (non-empty, max 120 chars).")}},async n=>{try{let r=X(n.sessionId);if(!r)return J(`session not found or prefix ambiguous: ${n.sessionId}`);let i=await w({tool:"set_alias",args:{sessionId:r,alias:n.alias},limiter:s,run:()=>Ot(r,n.alias)});return Q(i)}catch(r){return N(r)}}),e.registerTool("append_note",{title:"Append to session note",description:"Append markdown to a session note. If a note already exists, the new content is added below a `---` separator. Previous version is archived in previous_versions.",inputSchema:{sessionId:I.string().describe("Full session UUID or 8+-character prefix."),markdown:I.string().min(1).max(5e4).describe("Markdown to append.")}},async n=>{try{let r=X(n.sessionId);if(!r)return J(`session not found or prefix ambiguous: ${n.sessionId}`);let i=await w({tool:"append_note",args:{sessionId:r,length:n.markdown.length},limiter:s,run:()=>{let o=We(r),a=o&&o.content.length>0?`${o.content}${vr}${n.markdown}`:n.markdown;if(a.length>Cr)throw new Error(`note would exceed the 500 KB cumulative limit (current: ${o?.content.length??0} bytes, adding: ${n.markdown.length} bytes)`);return vt(r,a)}});return Q(i)}catch(r){return N(r)}}),e.registerTool("create_collection",{title:"Create a collection",description:"Create a new collection for grouping sessions. Optionally nest under a parent. Soft-deletion only.",inputSchema:{name:I.string().min(1).max(120),parent_id:I.string().optional().describe("Parent collection id (omit for root)."),icon:I.string().max(32).optional(),color:I.string().max(32).optional(),description:I.string().max(2e3).optional()}},async n=>{try{let r=await w({tool:"create_collection",args:n,limiter:s,run:()=>Dt({name:n.name,parent_id:n.parent_id??null,icon:n.icon??null,color:n.color??null,description:n.description??null})});return Q(r)}catch(r){return N(r)}}),e.registerTool("add_session_to_collection",{title:"Add session to collection",description:"Add a session to a collection. Idempotent: re-adding returns added=false.",inputSchema:{collectionId:I.string().min(1),sessionId:I.string().describe("Full session UUID or 8+-character prefix."),note:I.string().max(2e3).optional()}},async n=>{try{let r=X(n.sessionId);if(!r)return J(`session not found or prefix ambiguous: ${n.sessionId}`);if(!m().prepare("SELECT 1 FROM collections WHERE id = ?").get(n.collectionId))return J("collection not found");let a=await w({tool:"add_session_to_collection",args:{collectionId:n.collectionId,sessionId:r,note:n.note??null},limiter:s,run:()=>Mt(n.collectionId,r,n.note??null)});return Q({collectionId:n.collectionId,sessionId:r,...a})}catch(r){return N(r)}}),e.registerTool("remove_session_from_collection",{title:"Remove session from collection",description:"Remove a session from a collection. The removal is recorded in collection_events (append-only); the underlying session is untouched.",inputSchema:{collectionId:I.string().min(1),sessionId:I.string().describe("Full session UUID or 8+-character prefix.")}},async n=>{try{let r=X(n.sessionId);if(!r)return J(`session not found or prefix ambiguous: ${n.sessionId}`);let i=await w({tool:"remove_session_from_collection",args:{collectionId:n.collectionId,sessionId:r},limiter:s,run:()=>Ft(n.collectionId,r)});return Q({collectionId:n.collectionId,sessionId:r,...i})}catch(r){return N(r)}})}import{z as U}from"zod";L();F();import{randomUUID as Pt}from"node:crypto";import{writeFileSync as $t,readFileSync as El,existsSync as kr,mkdirSync as Dr}from"node:fs";import{join as Je}from"node:path";var be=Je(b,"threads"),Mr=Je(be,"index.json");function jt(){x(),kr(be)||Dr(be,{recursive:!0})}function qe(e,t,s){return{id:e.id,name:e.name,summary:e.summary,created_at:e.created_at,closed_at:e.closed_at,archived:e.archived===1,session_count:t.session_count,origin_count:t.origin_count,project:s?.project??null,project_count:s?.project_count??0,folder_id:e.folder_id??null}}function Xt(e){let t=new Map;if(e.length===0)return t;let s=m(),n=e.map(()=>"?").join(","),r=s.prepare(`SELECT te.thread_id AS thread_id,
|
|
741
755
|
p.name AS project,
|
|
742
756
|
COUNT(*) AS n,
|
|
743
757
|
SUM(CASE WHEN te.role = 'origin' THEN 1 ELSE 0 END) AS origin_n
|
|
@@ -745,7 +759,7 @@ CREATE INDEX IF NOT EXISTS idx_synth_results_created
|
|
|
745
759
|
LEFT JOIN sessions s ON s.id = te.session_id
|
|
746
760
|
LEFT JOIN projects p ON p.id = s.project_id
|
|
747
761
|
WHERE te.thread_id IN (${n})
|
|
748
|
-
GROUP BY te.thread_id, p.name`).all(...e),
|
|
762
|
+
GROUP BY te.thread_id, p.name`).all(...e),i=new Map;for(let o of r){let a=i.get(o.thread_id);a||(a=[],i.set(o.thread_id,a)),a.push(o)}for(let[o,a]of i){let l=a.filter(u=>u.project!==null),c=l.length,d=null;l.length>0&&(d=[...l].sort((p,g)=>g.n-p.n||g.origin_n-p.origin_n||(p.project??"").localeCompare(g.project??""))[0].project),t.set(o,{project:d,project_count:c})}return t}function Ht(e){let t=e.auto_title_source;return{thread_id:e.thread_id,session_id:e.session_id,parent_session_id:e.parent_session_id,role:e.role,confidence:e.confidence,source:e.source,added_at:e.added_at,alias:e.alias,auto_title:e.auto_title,auto_title_source:t==="agent"||t==="heuristic"?t:null,alias_source:e.alias?"manual":null,first_user_message:e.first_user_message,project:e.project}}function Bt(e){let s=m().prepare(`SELECT NULLIF(sa.alias, '') AS alias,
|
|
749
763
|
s.auto_title AS auto_title,
|
|
750
764
|
s.auto_title_source AS auto_title_source,
|
|
751
765
|
s.first_user_message AS first_user_message,
|
|
@@ -753,11 +767,11 @@ CREATE INDEX IF NOT EXISTS idx_synth_results_created
|
|
|
753
767
|
FROM (SELECT ? AS sid) q
|
|
754
768
|
LEFT JOIN sessions s ON s.id = q.sid
|
|
755
769
|
LEFT JOIN session_aliases sa ON sa.session_id = q.sid
|
|
756
|
-
LEFT JOIN projects p ON p.id = s.project_id`).get(e),n=s?.auto_title_source??null;return{alias:s?.alias??null,auto_title:s?.auto_title??null,auto_title_source:n==="agent"||n==="heuristic"?n:null,first_user_message:s?.first_user_message??null,project:s?.project??null}}function
|
|
770
|
+
LEFT JOIN projects p ON p.id = s.project_id`).get(e),n=s?.auto_title_source??null;return{alias:s?.alias??null,auto_title:s?.auto_title??null,auto_title_source:n==="agent"||n==="heuristic"?n:null,first_user_message:s?.first_user_message??null,project:s?.project??null}}function Ve(e){let s=m().prepare(`SELECT
|
|
757
771
|
COUNT(*) AS session_count,
|
|
758
772
|
SUM(CASE WHEN role = 'origin' THEN 1 ELSE 0 END) AS origin_count
|
|
759
|
-
FROM thread_edges WHERE thread_id = ?`).get(e);return{session_count:s?.session_count??0,origin_count:s?.origin_count??0}}function B(e){let t=k(e);t&&(
|
|
760
|
-
VALUES (?, ?, NULL, 'origin', 1.0, 'manual', ?)`).run(n,e.originSessionId,
|
|
773
|
+
FROM thread_edges WHERE thread_id = ?`).get(e);return{session_count:s?.session_count??0,origin_count:s?.origin_count??0}}function B(e){let t=k(e);t&&(jt(),$t(Je(be,`${e}.json`),JSON.stringify(t,null,2)),Wt())}function Wt(){jt();let e=Qe({includeArchived:!0});$t(Mr,JSON.stringify({threads:e},null,2))}function Re(e){let t=e.name.trim();if(!t)throw new Error("thread name cannot be empty");let s=m(),n=Pt(),r=new Date().toISOString();s.prepare("INSERT INTO threads (id, name, summary, created_at) VALUES (?, ?, ?, ?)").run(n,t,e.summary?.trim()||null,r),e.originSessionId&&s.prepare(`INSERT INTO thread_edges (thread_id, session_id, parent_session_id, role, confidence, source, added_at)
|
|
774
|
+
VALUES (?, ?, NULL, 'origin', 1.0, 'manual', ?)`).run(n,e.originSessionId,r),B(n);let i=k(n);if(!i)throw new Error("thread creation succeeded but read-back failed");return i}function Qe(e={}){let t=m(),s=e.includeArchived?"":"WHERE archived = 0",n=t.prepare(`SELECT * FROM threads ${s} ORDER BY created_at DESC`).all(),r=Xt(n.map(i=>i.id));return n.map(i=>qe(i,Ve(i.id),r.get(i.id)))}function k(e){let t=m(),s=t.prepare("SELECT * FROM threads WHERE id = ?").get(e);if(!s)return null;let n=t.prepare(`SELECT e.*,
|
|
761
775
|
NULLIF(sa.alias, '') AS alias,
|
|
762
776
|
s.auto_title AS auto_title,
|
|
763
777
|
s.auto_title_source AS auto_title_source,
|
|
@@ -768,10 +782,10 @@ CREATE INDEX IF NOT EXISTS idx_synth_results_created
|
|
|
768
782
|
LEFT JOIN session_aliases sa ON sa.session_id = e.session_id
|
|
769
783
|
LEFT JOIN projects p ON p.id = s.project_id
|
|
770
784
|
WHERE e.thread_id = ?
|
|
771
|
-
ORDER BY e.added_at ASC`).all(e).map(
|
|
785
|
+
ORDER BY e.added_at ASC`).all(e).map(Ht),r=Xt([e]).get(e);return{...qe(s,Ve(s.id),r),edges:n}}function Gt(e){return m().prepare(`SELECT t.* FROM threads t
|
|
772
786
|
JOIN thread_edges e ON e.thread_id = t.id
|
|
773
787
|
WHERE e.session_id = ? AND t.archived = 0
|
|
774
|
-
ORDER BY t.created_at DESC`).all(e).map(n=>
|
|
788
|
+
ORDER BY t.created_at DESC`).all(e).map(n=>qe(n,Ve(n.id)))}function ye(e){let t=m();if(!t.prepare("SELECT * FROM threads WHERE id = ?").get(e.threadId))throw new Error(`thread ${e.threadId} not found`);let n=new Date().toISOString(),r=e.parentSessionId??null,i=e.role??(r?"child":"origin"),o=e.confidence??1,a=e.source??"manual";if(o<0||o>1)throw new Error("confidence must be 0..1");t.prepare(`INSERT INTO thread_edges
|
|
775
789
|
(thread_id, session_id, parent_session_id, role, confidence, source, added_at)
|
|
776
790
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
777
791
|
ON CONFLICT(thread_id, session_id) DO UPDATE SET
|
|
@@ -779,25 +793,25 @@ CREATE INDEX IF NOT EXISTS idx_synth_results_created
|
|
|
779
793
|
role = excluded.role,
|
|
780
794
|
confidence = excluded.confidence,
|
|
781
795
|
source = excluded.source,
|
|
782
|
-
added_at = excluded.added_at`).run(e.threadId,e.sessionId,i,
|
|
796
|
+
added_at = excluded.added_at`).run(e.threadId,e.sessionId,r,i,o,a,n),B(e.threadId);let l=Bt(e.sessionId);return{thread_id:e.threadId,session_id:e.sessionId,parent_session_id:r,role:i,confidence:o,source:a,added_at:n,alias:l.alias,auto_title:l.auto_title,auto_title_source:l.auto_title_source,alias_source:l.alias?"manual":null,first_user_message:l.first_user_message,project:l.project}}function Yt(e,t){let n=m().prepare("DELETE FROM thread_edges WHERE thread_id = ? AND session_id = ?").run(e,t);return n.changes>0&&B(e),{removed:n.changes}}function ae(e,t,s){let n=m(),r=n.prepare("SELECT * FROM thread_edges WHERE thread_id = ? AND session_id = ?").get(e,t);if(!r)throw new Error("edge not found; add the session first");if(s!==null){if(s===t)throw new Error("cycle detected: session cannot be its own parent");let a=n.prepare("SELECT parent_session_id FROM thread_edges WHERE thread_id = ? AND session_id = ?"),l=s,c=new Set;for(;l!==null;){if(l===t)throw new Error(`cycle detected: setting parent of ${t} to ${s} would create a loop`);if(c.has(l))break;c.add(l),l=a.get(e,l)?.parent_session_id??null}}let i=s?"child":"origin";n.prepare(`UPDATE thread_edges
|
|
783
797
|
SET parent_session_id = ?, role = ?, added_at = ?
|
|
784
|
-
WHERE thread_id = ? AND session_id = ?`).run(s,
|
|
798
|
+
WHERE thread_id = ? AND session_id = ?`).run(s,i,new Date().toISOString(),e,t),B(e);let o=Bt(t);return Ht({...r,parent_session_id:s,role:i,added_at:new Date().toISOString(),alias:o.alias,auto_title:o.auto_title,auto_title_source:o.auto_title_source,first_user_message:o.first_user_message,project:o.project})}function zt(e,t){let s=t.trim();if(!s)throw new Error("name cannot be empty");m().prepare("UPDATE threads SET name = ? WHERE id = ?").run(s,e),B(e);let r=k(e);if(!r)throw new Error(`thread ${e} not found`);return r}function Kt(e){m().prepare("UPDATE threads SET closed_at = ? WHERE id = ?").run(new Date().toISOString(),e),B(e);let s=k(e);if(!s)throw new Error(`thread ${e} not found`);return s}function Jt(e){m().prepare("UPDATE threads SET closed_at = NULL WHERE id = ?").run(e),B(e);let s=k(e);if(!s)throw new Error(`thread ${e} not found`);return s}function qt(e){m().prepare("UPDATE threads SET archived = 1 WHERE id = ?").run(e),B(e);let s=k(e);if(!s)throw new Error(`thread ${e} not found`);return s}function Vt(e,t){if(e===t)throw new Error("cannot merge a thread into itself");let s=m(),n=new Date().toISOString();s.transaction(()=>{let i=s.prepare("SELECT * FROM thread_edges WHERE thread_id = ?").all(e);for(let o of i)s.prepare(`INSERT INTO thread_edges
|
|
785
799
|
(thread_id, session_id, parent_session_id, role, confidence, source, added_at)
|
|
786
800
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
787
801
|
ON CONFLICT(thread_id, session_id) DO UPDATE SET
|
|
788
802
|
parent_session_id = COALESCE(thread_edges.parent_session_id, excluded.parent_session_id),
|
|
789
803
|
role = CASE WHEN thread_edges.role = 'origin' OR excluded.role = 'origin' THEN 'origin' ELSE 'child' END,
|
|
790
804
|
confidence = MAX(thread_edges.confidence, excluded.confidence),
|
|
791
|
-
source = thread_edges.source`).run(t,o.session_id,o.parent_session_id,o.role,o.confidence,o.source,n);s.prepare("DELETE FROM threads WHERE id = ?").run(e)})(),B(t),
|
|
805
|
+
source = thread_edges.source`).run(t,o.session_id,o.parent_session_id,o.role,o.confidence,o.source,n);s.prepare("DELETE FROM threads WHERE id = ?").run(e)})(),B(t),Wt();let r=k(t);if(!r)throw new Error("merge destination disappeared");return r}function Qt(e){if(e.sessionIds.length===0)throw new Error("no sessions to split off");let t=m(),s=new Date().toISOString(),n=Pt();t.transaction(()=>{t.prepare("INSERT INTO threads (id, name, summary, created_at) VALUES (?, ?, NULL, ?)").run(n,e.newThreadName.trim(),s);for(let i of e.sessionIds){let o=t.prepare("SELECT * FROM thread_edges WHERE thread_id = ? AND session_id = ?").get(e.threadId,i);o&&(t.prepare(`INSERT INTO thread_edges
|
|
792
806
|
(thread_id, session_id, parent_session_id, role, confidence, source, added_at)
|
|
793
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)`).run(n,
|
|
794
|
-
`)[0].trim();return s.length>
|
|
807
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`).run(n,i,o.parent_session_id,o.role,o.confidence,o.source,s),t.prepare("DELETE FROM thread_edges WHERE thread_id = ? AND session_id = ?").run(e.threadId,i))}})(),B(e.threadId),B(n);let r=k(n);if(!r)throw new Error("split destination disappeared");return r}L();F();import{writeFileSync as Pr,mkdirSync as $r,existsSync as jr}from"node:fs";import{join as ss}from"node:path";L();var Zt=80;function es(e){if(e.alias&&e.alias.trim())return e.alias.trim();if(e.auto_title&&e.auto_title.trim())return e.auto_title.trim();let t=(e.first_user_message??"").trim();if(!t)return e.id.slice(0,8);let s=t.split(`
|
|
808
|
+
`)[0].trim();return s.length>Zt?s.slice(0,Zt)+"\u2026":s}function Fr(e){return m().prepare(`SELECT s.id AS id,
|
|
795
809
|
sa.alias AS alias,
|
|
796
810
|
s.auto_title AS auto_title,
|
|
797
811
|
s.first_user_message AS first_user_message
|
|
798
812
|
FROM sessions s
|
|
799
813
|
LEFT JOIN session_aliases sa ON sa.session_id = s.id
|
|
800
|
-
WHERE s.id = ?`).get(e)??null}function
|
|
814
|
+
WHERE s.id = ?`).get(e)??null}function Ur(e){let t=Fr(e);return t?es(t):e.slice(0,8)}function ts(e){if(!e)return null;let t=m(),s=t.prepare(`SELECT e.thread_id AS thread_id,
|
|
801
815
|
t.name AS thread_name,
|
|
802
816
|
e.parent_session_id AS parent_session_id,
|
|
803
817
|
e.added_at AS added_at
|
|
@@ -806,7 +820,7 @@ CREATE INDEX IF NOT EXISTS idx_synth_results_created
|
|
|
806
820
|
WHERE e.session_id = ?
|
|
807
821
|
AND t.archived = 0
|
|
808
822
|
ORDER BY e.added_at DESC
|
|
809
|
-
LIMIT 1`).get(e);if(!s)return null;let n=s.parent_session_id?{id:s.parent_session_id,title:
|
|
823
|
+
LIMIT 1`).get(e);if(!s)return null;let n=s.parent_session_id?{id:s.parent_session_id,title:Ur(s.parent_session_id)}:null,r=s.parent_session_id?[e,s.parent_session_id]:[e],i=r.map(()=>"?").join(", "),a=t.prepare(`SELECT e.session_id AS session_id,
|
|
810
824
|
s.id AS id,
|
|
811
825
|
sa.alias AS alias,
|
|
812
826
|
s.auto_title AS auto_title,
|
|
@@ -815,48 +829,48 @@ CREATE INDEX IF NOT EXISTS idx_synth_results_created
|
|
|
815
829
|
LEFT JOIN sessions s ON s.id = e.session_id
|
|
816
830
|
LEFT JOIN session_aliases sa ON sa.session_id = e.session_id
|
|
817
831
|
WHERE e.thread_id = ?
|
|
818
|
-
AND e.session_id NOT IN (${
|
|
819
|
-
ORDER BY e.added_at ASC`).all(s.thread_id,...
|
|
832
|
+
AND e.session_id NOT IN (${i})
|
|
833
|
+
ORDER BY e.added_at ASC`).all(s.thread_id,...r).map(l=>({id:l.session_id,title:l.id?es(l):l.session_id.slice(0,8)}));return{thread_id:s.thread_id,thread_name:s.thread_name,parent_session:n,siblings:a}}var et=ss(b,"titles");var le=5,Ne=15,Xr=500;function ns(e){if(!e)return[];try{let t=JSON.parse(e);if(Array.isArray(t))return t.filter(s=>!!s&&typeof s=="object"&&typeof s.title=="string"&&typeof s.replaced_at=="string")}catch{}return[]}function rs(e){let t=m(),s=t.prepare(`SELECT rowid AS rid, content_text
|
|
820
834
|
FROM messages
|
|
821
835
|
WHERE session_id = ? AND role = 'user' AND is_sidechain = 0
|
|
822
836
|
AND content_text IS NOT NULL AND content_text != ''
|
|
823
837
|
ORDER BY COALESCE(timestamp, ''), rowid ASC
|
|
824
|
-
LIMIT ?`).all(e,
|
|
838
|
+
LIMIT ?`).all(e,le),n=t.prepare(`SELECT rowid AS rid, content_text
|
|
825
839
|
FROM messages
|
|
826
840
|
WHERE session_id = ? AND role = 'user' AND is_sidechain = 0
|
|
827
841
|
AND content_text IS NOT NULL AND content_text != ''
|
|
828
842
|
ORDER BY COALESCE(timestamp, '') DESC, rowid DESC
|
|
829
|
-
LIMIT ?`).all(e,
|
|
843
|
+
LIMIT ?`).all(e,Ne),r=new Map;for(let d of s)r.set(d.rid,d.content_text);for(let d of n)r.set(d.rid,d.content_text);if(r.size===0)throw new Error("no user messages available to summarise");let i=Array.from(r.entries()).sort((d,u)=>d[0]-u[0]).map(([,d])=>({content_text:d})),o=s.length===le&&n.length===Ne&&r.size===le+Ne,a=i.map((d,u)=>{let p=(d.content_text??"").slice(0,Xr);return o&&u===le?`--- (middle of session omitted) ---
|
|
830
844
|
${u+1}. ${p}`:`${u+1}. ${p}`}).join(`
|
|
831
|
-
`),
|
|
832
|
-
`)}var
|
|
833
|
-
`)}function
|
|
834
|
-
FROM sessions WHERE id = ?`).get(e);if(!
|
|
845
|
+
`),l=null;try{l=ts(e)}catch(d){console.error("[autoTitle] thread context resolution failed:",d),l=null}let c=[];return l&&(c.push(Hr(l)),c.push("")),c.push(`You will receive a sample of user messages from a Claude Code session: the first ${le}`,`messages (initial intent) and the last ${Ne} messages (current direction).`,"Write a single descriptive title, max 50 characters, focused on what the user is","currently trying to accomplish. If initial intent and current direction differ, prefer","the current direction. Output ONLY the title, with no quotes and no trailing punctuation.","","Messages:",a),c.join(`
|
|
846
|
+
`)}var Ze=5;function Hr(e){let t=[];t.push("Thread context:"),t.push(`- This session is part of thread "${e.thread_name}".`),e.parent_session&&t.push(`- Parent session: "${e.parent_session.title}"`);let s=e.siblings.length;if(s>0){let r=e.siblings.slice(0,Ze).map(o=>`"${o.title}"`).join(", "),i=s>Ze?`, and ${s-Ze} more`:"";t.push(`- Sibling sessions (${s}): ${r}${i}`)}return t.push(""),t.push("Generate a title that reflects this session's role in the thread."),t.push('If siblings use a pattern like "Phase A / Phase B" or "Wave 1 of 4",'),t.push("follow the same pattern."),t.join(`
|
|
847
|
+
`)}function is(e,t,s){let n=t.trim();if(!n)return;let r=m(),i=r.prepare(`SELECT auto_title, auto_title_source, auto_title_generated_at, auto_title_history
|
|
848
|
+
FROM sessions WHERE id = ?`).get(e);if(!i||s==="heuristic"&&i.auto_title_source==="agent"&&i.auto_title||i.auto_title===n&&i.auto_title_source===s)return;let o=ns(i.auto_title_history),a=new Date().toISOString();i.auto_title&&i.auto_title_source&&o.push({title:i.auto_title,source:i.auto_title_source,replaced_at:a}),r.prepare(`UPDATE sessions
|
|
835
849
|
SET auto_title = ?,
|
|
836
850
|
auto_title_source = ?,
|
|
837
851
|
auto_title_generated_at = ?,
|
|
838
852
|
auto_title_history = ?
|
|
839
|
-
WHERE id = ?`).run(n,s,Date.now(),JSON.stringify(o),e),
|
|
840
|
-
FROM sessions WHERE id = ?`).get(e);return t?{auto_title:t.auto_title,auto_title_source:t.auto_title_source??null,auto_title_generated_at:t.auto_title_generated_at,auto_title_history:
|
|
841
|
-
`;
|
|
842
|
-
`)}catch(
|
|
853
|
+
WHERE id = ?`).run(n,s,Date.now(),JSON.stringify(o),e),Wr(e,n,s,a)}function os(e){let t=m().prepare(`SELECT auto_title, auto_title_source, auto_title_generated_at, auto_title_history
|
|
854
|
+
FROM sessions WHERE id = ?`).get(e);return t?{auto_title:t.auto_title,auto_title_source:t.auto_title_source??null,auto_title_generated_at:t.auto_title_generated_at,auto_title_history:ns(t.auto_title_history)}:null}function Br(){x(),jr(et)||$r(et,{recursive:!0})}function Wr(e,t,s,n){try{Br();let r=ss(et,`${e}.txt`),i=`# Claude Recall auto-title \xB7 session ${e} \xB7 source ${s} \xB7 updated ${n}
|
|
855
|
+
`;Pr(r,i+t+`
|
|
856
|
+
`)}catch(r){console.error("[autoTitle] mirror write failed:",r)}}var ri=50;function ii(e){if(e.length===0)return[];let t=[...e].sort((l,c)=>l.added_at<c.added_at?-1:l.added_at>c.added_at?1:0),s=new Map,n=new Set,r=[];for(let l of t)if(l.parent_session_id){let c=s.get(l.parent_session_id)??[];c.push(l),s.set(l.parent_session_id,c)}let i=t.filter(l=>l.role==="origin"),a=[...i.length>0?i:t.filter(l=>!l.parent_session_id)];for(;a.length>0;){let l=a.shift();if(n.has(l.session_id))continue;n.add(l.session_id),r.push(l.session_id);let c=s.get(l.session_id)??[];for(let d of c)n.has(d.session_id)||a.push(d)}for(let l of t)n.has(l.session_id)||(n.add(l.session_id),r.push(l.session_id));return r}function oi(e){let t=rs(e.sessionId),s=["",`BULK CONTEXT: You are titling session ${e.current} of ${e.total} in this thread.`,"The parent and earlier siblings already have titles (shown above). YOUR JOB:","",'1. Identify the naming pattern. If parent is "Build Feature X" and earlier',' siblings are "Phase A: API design" and "Phase B: client wiring", the pattern',' is "Phase {LETTER}: {topic}".',"2. If no pattern is yet established (this is the first child), INVENT a pattern",' that will scale to N children. Good patterns: "Phase A/B/C", "Wave 1 of M",',' "Step N", or a domain-specific structure if the thread name suggests one.',"3. Output a title that follows the pattern AND describes what THIS session"," does in 50 characters max.","","Output ONLY the title, no quotes, no trailing punctuation."].join(`
|
|
843
857
|
`);return`${t}
|
|
844
|
-
${s}`}function
|
|
845
|
-
`).toLowerCase();for(let o of e.authored_paths){let a=o.toLowerCase();if(t.touched_files.has(o)||
|
|
846
|
-
`,
|
|
847
|
-
`)){let r
|
|
848
|
-
`))if(
|
|
858
|
+
${s}`}function ai(e){return os(e)?.auto_title_source??null}async function li(e){let{spawnClaudePrompt:t,isClaudeCliAvailable:s}=await Promise.resolve().then(()=>(ps(),us));if(!s())throw new Error("claude CLI not found on PATH");let n=await t(e.prompt,[],{model:e.model});if(!n.success)throw new Error(`claude CLI exited ${n.exitCode}: ${n.stderr.slice(-500)}`);let r=ci(n.stdout);if(!r)throw new Error("claude CLI returned an empty title");return r.slice(0,ri)}function ci(e){let t=e.trim();if(!t)return"";try{let s=JSON.parse(t);if(s&&typeof s=="object"){let n=s.result;if(typeof n=="string")return ms(n)}}catch{}return ms(t)}function ms(e){return e.trim().replace(/^["'`]+|["'`]+$/g,"").replace(/\s+/g," ").replace(/[.!?]+$/g,"").trim()}async function _s(e,t={}){let s=k(e);if(!s)throw new Error(`thread not found: ${e}`);let n=ii(s.edges),r=n.length,i=t.force??!1,o=t.signal,a=[],l=[],c=[];for(let d=0;d<n.length;d++){let u=n[d],p=d+1;if(o?.aborted){let _={sessionId:u,reason:"cancelled"};l.push(_),t.onSkipped?.(_);continue}if(!i&&ai(u)==="agent"){let _={sessionId:u,reason:"already-titled"};l.push(_),t.onSkipped?.(_);continue}let g;try{if(gs)g=await gs({sessionId:u,current:p,total:r});else{let _=oi({sessionId:u,current:p,total:r});g=await li({prompt:_,model:t.model})}}catch(_){let f=_ instanceof Error?_.message:String(_??"unknown error"),E={sessionId:u,error:f};c.push(E),t.onFailed?.(E);continue}try{is(u,g,"agent")}catch(_){let f=_ instanceof Error?_.message:String(_??"unknown error"),E={sessionId:u,error:`setAutoTitle failed: ${f}`};c.push(E),t.onFailed?.(E);continue}a.push(u),t.onProgress?.({current:p,total:r,sessionId:u,title:g})}return{generated:a,skipped:l,failed:c}}var gs=null;L();import{execFile as Ni}from"node:child_process";import{promisify as wi}from"node:util";import{readlink as Li,readFile as bs}from"node:fs/promises";import{platform as Le}from"node:os";import{readFileSync as di,statSync as ui}from"node:fs";var pi=200*1024*1024;var st=.5,Es=st,mi=[{maxGapMs:3600*1e3,weight:.7,label:"<1h gap"},{maxGapMs:14400*1e3,weight:.4,label:"<4h gap"},{maxGapMs:1440*60*1e3,weight:.2,label:"<24h gap"}],gi=["let's commit","lets commit","now commit","inspect the diff","inspect this diff","review the diff","review this diff","diff of all changes","diff of changes","based on what we just did","based on the things done","based on all the things","continue from","continuing from","pick up where","next step","now fix","now lets","now let's","from the previous session","from our last session","from the last session"];function fs(e){if(!e)return null;if(e.startsWith("/")){let s=e.split(" \xB7 ");if(s.length>1)return s[1].trim().toLowerCase()}let t=e.split(" \xB7 ");return t.length>1?t[t.length-1].trim().toLowerCase():null}function _i(e,t){let s=t-e;if(s<0)return{weight:0,label:null};for(let n of mi)if(s<=n.maxGapMs)return{weight:n.weight,label:n.label};return{weight:0,label:null}}function fi(e){if(e.length===0)return{weight:0,matched:null,matchedIndex:-1};let t=[.4,.35,.3,.25,.2,.15,.1],s=Math.min(e.length,t.length);for(let n=0;n<s;n++){let r=e[n];if(!r)continue;let i=r.toLowerCase();for(let o of gi)if(i.includes(o))return{weight:t[n],matched:o,matchedIndex:n}}return{weight:0,matched:null,matchedIndex:-1}}function tt(e,t){if(!e||!t||e.length!==t.length)return 0;let s=0;for(let n=0;n<e.length;n++)s+=e[n]*t[n];return s<-1?-1:s>1?1:s}function hi(e,t){if(e.length===0||t.length===0)return 0;let s=0;for(let n of e)for(let r of t){let i=tt(n,r);i>s&&(s=i)}return s}function Ei(e,t){let s=tt(e.mean_embedding,t.mean_embedding),n=tt(e.tail_pool,t.head_pool),r=hi(e.sample_chunks,t.sample_chunks),i=0,o=null;if(s>i&&(i=s,o="mean"),n>i&&(i=n,o="asymmetric"),r>i&&(i=r,o="max_pool"),i<.65)return{weight:0,cosine:i,mode:null};if(i>=.85)return{weight:.3,cosine:i,mode:o};let a=(i-.65)/.2*.3;return{weight:Math.round(a*100)/100,cosine:i,mode:o}}function Ti(e,t){return e.cluster_id===null||t.cluster_id===null?{weight:0,same:!1}:e.cluster_id!==t.cluster_id?{weight:0,same:!1}:{weight:.05,same:!0}}function Si(e,t){if(e.size===0||t.size===0)return{weight:0,count:0};let s=0;for(let r of t)e.has(r)&&s++;return s===0?{weight:0,count:0}:{weight:Math.min(.4,s*.1),count:s}}function bi(e,t){let s=fs(e),n=fs(t);return s&&n&&s===n?{weight:.1,brand:s}:{weight:0,brand:null}}function hs(e){return e.replace(/\s+/g," ").trim().toLowerCase()}function Ri(e,t){let s=0,n=null,r=!1;if(e.authored_paths.size>0){let i=t.recent_user_messages.slice(0,3).join(`
|
|
859
|
+
`).toLowerCase();for(let o of e.authored_paths){let a=o.toLowerCase();if(t.touched_files.has(o)||i.includes(a)){s+=.5,n=o;break}}}if(e.authored_content.length>0&&t.recent_user_messages[0]){let i=hs(t.recent_user_messages[0]);if(i.length>=200)for(let o of e.authored_content){let a=hs(o);if(a.length<200)continue;let l=Math.min(a.length,240),c=a.slice(0,l);if(i.includes(c)){s+=.4,r=!0;break}}}return{weight:Math.min(.6,s),pathMatch:n,contentMatch:r}}function yi(e,t,s=Es){if(t.started_at_ms<=e.started_at_ms)return null;let n=e.ended_at_ms??e.started_at_ms;if(t.started_at_ms<n)return null;let r=_i(n,t.started_at_ms),i=t.recent_user_messages.length>0?t.recent_user_messages:t.first_user_message?[t.first_user_message]:[],o=fi(i),a=Si(e.touched_files,t.touched_files),l=bi(e.auto_title,t.auto_title),c=Ei(e,t),d=Ti(e,t),u=Ri(e,t),p=r.weight+o.weight+a.weight+l.weight+c.weight+d.weight+u.weight;if(p<s)return null;let g=[];if(r.label&&g.push(`temporal ${r.label} (+${r.weight})`),o.matched){let _=o.matchedIndex===0?"opening message":`message #${o.matchedIndex+1}`;g.push(`continuation phrase "${o.matched}" in ${_} (+${o.weight})`)}if(a.count>0&&g.push(`${a.count} file${a.count===1?"":"s"} overlap (+${a.weight.toFixed(1)})`),l.brand&&g.push(`shared brand "${l.brand}" (+${l.weight})`),c.weight>0&&c.mode&&g.push(`semantic ${c.mode==="asymmetric"?"tail\u2192head":c.mode==="max_pool"?"best-chunk":"mean"} ${c.cosine.toFixed(2)} (+${c.weight.toFixed(2)})`),d.same&&g.push(`same cluster (+${d.weight})`),u.weight>0){let _=[];u.pathMatch&&_.push(`opened authored path "${u.pathMatch.split("/").pop()}"`),u.contentMatch&&_.push("verbatim-paste of authored content"),g.push(`doc-authorship: ${_.join(" + ")} (+${u.weight.toFixed(2)})`)}return{parent_id:e.id,child_id:t.id,confidence:Math.min(1,p),signals:{temporal:r.weight,continuation:o.weight,file_overlap:a.weight,same_brand:l.weight,semantic:c.weight,cluster:d.weight,doc_authorship:u.weight},reasons:g}}function Ts(e,t=Es){let s=[];for(let n=0;n<e.length;n++){let r=e[n],i=null;for(let o=0;o<n;o++){let a=e[o],l=yi(a,r,t);l&&(!i||l.confidence>i.confidence)&&(i=l)}i&&s.push(i)}return s}function Ss(e,t={}){let s=t.maxUserMessages??5,n=t.userMessageMaxLen??2e3,r=new Set,i=[],o=new Set,a=[],l;try{if(ui(e).size>pi)return{touched_files:r,recent_user_messages:i,authored_paths:o,authored_content:a};l=di(e,"utf8")}catch{return{touched_files:r,recent_user_messages:i,authored_paths:o,authored_content:a}}let c=0;for(;c<l.length;){let d=l.indexOf(`
|
|
860
|
+
`,c),u=d===-1?l.length:d,p=l.slice(c,u);if(c=d===-1?l.length:d+1,!p.trim())continue;let g;try{g=JSON.parse(p)}catch{continue}let _=g;if(_.type==="user"&&_.message?.role==="user"&&typeof _.message.content=="string"&&i.length<s){let E=_.message.content.trim();E&&i.push(E.length>n?E.slice(0,n):E)}let f=_.message?.content;if(Array.isArray(f))for(let E of f){if(!E||typeof E!="object")continue;let S=E;if(S.type!=="tool_use")continue;let y=S.input??{},M=typeof y.file_path=="string"?y.file_path:null;if(M){let R=we(M);R&&r.add(R)}if((S.name==="Write"||S.name==="Edit"||S.name==="MultiEdit")&&M){let R=we(M);R&&o.add(R);let Y=typeof y.content=="string"?y.content:typeof y.new_string=="string"?y.new_string:null;Y&&Y.length>=200&&a.push(Y.length>4096?Y.slice(0,4096):Y)}if(S.name==="Bash"&&typeof y.command=="string")for(let R of y.command.matchAll(/(?:^|[\s'"`(=])((?:\.\.?\/|\/|src\/|test\/|docs\/|site\/)[A-Za-z0-9_./-]+)/g)){let Y=we(R[1]);Y&&r.add(Y)}if((S.name==="Glob"||S.name==="Grep")&&typeof y.pattern=="string"){let R=we(y.pattern);R&&!R.includes("*")&&r.add(R)}}}return{touched_files:r,recent_user_messages:i,authored_paths:o,authored_content:a}}function we(e){let t=e.trim().replace(/^['"]|['"]$/g,"");return!t||t.length<4||/[<>|;&\$`]/.test(t)?null:t}var rt=wi(Ni),Ai=6,Rs="Active ",ys=" sessions \u2014 ",Oi=6e4;async function Ii(){if(Le()==="win32")return[];for(let t of["/bin/ps","/usr/bin/ps"])try{let{stdout:s}=await rt(t,["-eo","pid=,comm="],{timeout:2e3}),n=[];for(let r of s.split(`
|
|
861
|
+
`)){let i=r.trim().match(/^(\d+)\s+(.+)$/);if(!i)continue;let o=Number(i[1]),a=i[2].trim();(a==="claude"||a.endsWith("/claude")||a.endsWith("/bin/claude"))&&Number.isFinite(o)&&n.push(o)}return n}catch{continue}return[]}async function xi(e){let t=Le();if(t==="linux")try{return(await Li(`/proc/${e}/cwd`)).replace(/\/+$/,"")}catch{return null}if(t==="darwin"||t==="freebsd"||t==="openbsd")for(let s of["/usr/sbin/lsof","/usr/bin/lsof"])try{let{stdout:n}=await rt(s,["-a","-p",String(e),"-d","cwd","-Fn"],{timeout:2e3});for(let r of n.split(`
|
|
862
|
+
`))if(r.startsWith("n"))return r.slice(1).replace(/\/+$/,"");return null}catch{continue}return null}async function vi(e){let t=Le();if(t==="linux")try{let s=await bs(`/proc/${e}/stat`,"utf8"),n=s.lastIndexOf(")");if(n===-1)return null;let r=s.slice(n+1).trim().split(/\s+/),i=Number(r[19]);if(!Number.isFinite(i))return null;let o=await bs("/proc/uptime","utf8"),a=Number(o.split(/\s+/)[0]);return Number.isFinite(a)?Date.now()-a*1e3+i/100*1e3:null}catch{return null}if(t==="darwin"||t==="freebsd"||t==="openbsd")for(let s of["/bin/ps","/usr/bin/ps"])try{let{stdout:n}=await rt(s,["-o","lstart=","-p",String(e)],{timeout:2e3}),r=Date.parse(n.trim());return Number.isFinite(r)?r:null}catch{continue}return null}async function Ci(e,t){let s=await Ii();if(s.length===0)return null;let n=e.replace(/\/+$/,""),r=[];for(let o of s){let a=await xi(o);if(a&&(a===n||a.startsWith(n+"/"))){let l=await vi(o);r.push({pid:o,startMs:l})}}if(r.length===0)return new Set;let i=new Set;for(let{startMs:o}of r){if(o==null)continue;let a=null,l=Oi;for(let c of t){if(i.has(c.session_id)||!c.started_at)continue;let d=Date.parse(c.started_at);if(!Number.isFinite(d))continue;let u=Math.abs(d-o);u<l&&(l=u,a=c)}a&&i.add(a.session_id)}return i}function ki(e){let s=m().prepare("SELECT id, name, decoded_path FROM projects WHERE id = ? LIMIT 1").get(e);if(!s)throw new Error(`project ${e} not found`);return s}function Di(e,t){let s=m(),n=`${Rs}${t}${ys}%`,r=s.prepare(`SELECT t.id
|
|
849
863
|
FROM threads t
|
|
850
864
|
WHERE t.archived = 0
|
|
851
865
|
AND t.name LIKE ?
|
|
852
|
-
ORDER BY t.created_at DESC`).all(n);for(let o of
|
|
866
|
+
ORDER BY t.created_at DESC`).all(n);for(let o of r){let a=k(o.id);if(a&&a.project===t)return a}let i=s.prepare(`SELECT DISTINCT te.thread_id AS id
|
|
853
867
|
FROM thread_edges te
|
|
854
868
|
JOIN sessions s ON s.id = te.session_id
|
|
855
869
|
JOIN threads t ON t.id = te.thread_id
|
|
856
870
|
WHERE s.project_id = ?
|
|
857
871
|
AND t.archived = 0
|
|
858
872
|
AND t.name LIKE ?
|
|
859
|
-
LIMIT 1`).get(e,n);return
|
|
873
|
+
LIMIT 1`).get(e,n);return i?k(i.id):null}function Ns(e){let t=e?new Date(e):new Date,s=t.getFullYear(),n=String(t.getMonth()+1).padStart(2,"0"),r=String(t.getDate()).padStart(2,"0");return`${s}-${n}-${r}`}function nt(e,t){let s=m(),n=t>0,r=n?Date.now()-t*60*60*1e3:0;return n?s.prepare(`SELECT s.id AS session_id,
|
|
860
874
|
sa.alias AS alias,
|
|
861
875
|
s.auto_title AS auto_title,
|
|
862
876
|
s.first_user_message AS first_user_message,
|
|
@@ -867,7 +881,7 @@ ${s}`}function Zi(e){return es(e)?.auto_title_source??null}async function er(e){
|
|
|
867
881
|
WHERE s.project_id = ?
|
|
868
882
|
AND s.file_mtime IS NOT NULL
|
|
869
883
|
AND s.file_mtime > ?
|
|
870
|
-
ORDER BY s.started_at ASC`).all(e,
|
|
884
|
+
ORDER BY s.started_at ASC`).all(e,r):s.prepare(`SELECT s.id AS session_id,
|
|
871
885
|
sa.alias AS alias,
|
|
872
886
|
s.auto_title AS auto_title,
|
|
873
887
|
s.first_user_message AS first_user_message,
|
|
@@ -876,24 +890,25 @@ ${s}`}function Zi(e){return es(e)?.auto_title_source??null}async function er(e){
|
|
|
876
890
|
FROM sessions s
|
|
877
891
|
LEFT JOIN session_aliases sa ON sa.session_id = s.id
|
|
878
892
|
WHERE s.project_id = ?
|
|
879
|
-
ORDER BY s.started_at ASC`).all(e)}function
|
|
893
|
+
ORDER BY s.started_at ASC`).all(e)}function Mi(e){let t=m(),s=[];for(let n of e){if(!n.started_at)continue;let r=Date.parse(n.started_at);if(!Number.isFinite(r))continue;let i=t.prepare("SELECT file_path, ended_at FROM sessions WHERE id = ?").get(n.session_id);if(!i?.file_path)continue;let o=i.ended_at?Date.parse(i.ended_at):null,a=Ss(i.file_path,{maxUserMessages:7});s.push({id:n.session_id,started_at_ms:r,ended_at_ms:Number.isFinite(o)?o:null,first_user_message:n.first_user_message,recent_user_messages:a.recent_user_messages,auto_title:n.auto_title,touched_files:a.touched_files,mean_embedding:null,head_pool:null,tail_pool:null,sample_chunks:[],cluster_id:null,authored_paths:a.authored_paths,authored_content:a.authored_content})}return s}function Fi(e){let s=m().prepare(`SELECT session_id, parent_session_id, source
|
|
880
894
|
FROM thread_edges
|
|
881
|
-
WHERE thread_id = ?`).all(e),n=new Map;for(let
|
|
895
|
+
WHERE thread_id = ?`).all(e),n=new Map;for(let r of s)n.set(r.session_id,{parent_session_id:r.parent_session_id,source:r.source});return n}async function it(e,t={}){let s=ki(e),n=t.windowHours??Ai,r=t.scoreThreshold??st,i=t.useLivePids??!0,o=[],a=[];if(i&&s.decoded_path){let f=nt(e,0),E=await Ci(s.decoded_path,f);if(E===null){let y=Le()==="win32"?"Windows live-PID detection is not yet supported \u2014 falling back to the rolling mtime window.":"No live `claude` processes detected \u2014 falling back to the rolling mtime window. Output may include sessions that are no longer open.";o.push(y),a=nt(e,n)}else E.size===0?(o.push(`No active terminals open in ${s.name} (cwd=${s.decoded_path}). Open a Claude terminal in this repo and re-run.`),a=[]):a=f.filter(S=>E.has(S.session_id))}else a=nt(e,n);a.length===0&&!o.length&&o.push(`No active sessions in ${s.name} within the last ${n}h.`);let l=Di(e,s.name),c=new Set(l?l.edges.map(f=>f.session_id):[]),d=a.filter(f=>!c.has(f.session_id)),u=Mi(a);u.sort((f,E)=>f.started_at_ms-E.started_at_ms);let p=Ts(u,r),g=l?l.edges.filter(f=>f.source!=="auto-active"&&(f.parent_session_id||f.role==="origin")).map(f=>({session_id:f.session_id,parent_session_id:f.parent_session_id})):[],_=l?l.name:`${Rs}${s.name}${ys}${Ns(t.todayIso)}`;return{project:s,thread:{id:l?.id??null,name:_,exists:!!l,existing_session_count:l?.edges.length??0},candidates:a,proposed_additions:d,proposed_edges:p,preserved_manual_edges:g,warnings:o}}function ws(e){let t={thread_id:"",added:0,edges_set:0,preserved_manual:e.preserved_manual_edges.length},s;e.thread.exists&&e.thread.id?s=e.thread.id:s=Re({name:e.thread.name,summary:`Auto-captured by sync-active on ${Ns()}. Members are sessions in ${e.project.name} that were active within the rolling window. Re-runnable: subsequent runs append new active sessions and never overwrite manual edges.`}).id,t.thread_id=s;let n=Fi(s);for(let o of e.candidates)n.has(o.session_id)||(ye({threadId:s,sessionId:o.session_id,source:"auto-active",confidence:.5}),t.added++,n.set(o.session_id,{parent_session_id:null,source:"auto-active"}));for(let o of e.proposed_edges){let a=n.get(o.child_id);if(a&&a.source==="auto-active"&&a.parent_session_id!==o.parent_id&&n.has(o.parent_id))try{ae(s,o.child_id,o.parent_id),t.edges_set++,n.set(o.child_id,{parent_session_id:o.parent_id,source:a.source})}catch{}}let i=m().prepare(`SELECT session_id FROM thread_edges
|
|
882
896
|
WHERE thread_id = ?
|
|
883
897
|
AND source = 'auto-active'
|
|
884
898
|
AND parent_session_id IS NULL
|
|
885
|
-
AND role = 'child'`).all(s);for(let o of
|
|
899
|
+
AND role = 'child'`).all(s);for(let o of i)try{ae(s,o.session_id,null)}catch{}return t}function C(e){return{content:[{type:"text",text:JSON.stringify(e,null,2)}]}}function Ui(e){return{content:[{type:"text",text:e}],isError:!0}}function D(e,t){return U.object(e).strict().parse(t)}var Ps=U.string().uuid(),W=Ps.describe("Thread id (UUID)."),q=Ps.describe("Session UUID (exact, not a prefix)."),Ls={include_archived:U.boolean().optional().describe("Include archived threads (default false).")},As={id:W},Os={session_id:q},Is={name:U.string().min(1).max(200).describe("Human-readable thread name."),summary:U.string().max(4e3).optional().describe("Optional short description."),origin_session_id:q.optional().describe("Seed the thread with this session as its origin.")},xs={thread_id:W,session_id:q,parent_session_id:q.optional().describe("If present, the edge is role=child with this parent; otherwise role=origin."),role:U.enum(["origin","child"]).optional()},vs={thread_id:W,session_id:q,parent_session_id:q.nullable().describe("New parent (null to clear and promote the edge back to role=origin).")},Cs={thread_id:W,session_id:q},ks={thread_id:W,name:U.string().min(1).max(200)},ne={thread_id:W},Ds={source_id:W.describe("Thread to dissolve \u2014 its edges move into dest_id."),dest_id:W.describe("Thread that absorbs source_id.")},Ms={thread_id:W,session_ids:U.array(q).min(1).max(500),new_thread_name:U.string().min(1).max(200)},Fs={thread_id:W,force:U.boolean().optional().describe("When true, regenerate titles for sessions that already have an agent-sourced title. Default false skips them.")},Us={project_id:U.number().int().positive().describe("Numeric project id from list_projects. The sync is repo-scoped and never crosses projects."),mode:U.enum(["preflight","apply"]).describe("preflight returns the proposed plan without writing; apply writes the thread + edges and is idempotent."),window_hours:U.number().min(.5).max(168).optional().describe("Rolling activity window in hours. Default 6.")};function $s(e){e.registerTool("thread_list",{title:"List threads",description:"All threads (v0.15a intent groups), newest first. Excludes archived threads unless include_archived is true.",inputSchema:Ls},async t=>{try{let{include_archived:s}=D(Ls,t);return C(Qe({includeArchived:s??!1}))}catch(s){return N(s)}}),e.registerTool("thread_get",{title:"Get thread with edges",description:"Full thread detail including every session edge (origin + children).",inputSchema:As},async t=>{try{let{id:s}=D(As,t),n=k(s);return n?C(n):Ui(`thread not found: ${s}`)}catch(s){return N(s)}}),e.registerTool("thread_for_session",{title:"List threads containing a session",description:"Return non-archived threads that reference this session (as origin or child).",inputSchema:Os},async t=>{try{let{session_id:s}=D(Os,t);return C(Gt(s))}catch(s){return N(s)}})}function js(e,t={}){let s=t.limiter??new K;e.registerTool("thread_create",{title:"Create a thread",description:"Create a new thread. Optionally seed it with an origin session. Name is required; summary is optional.",inputSchema:Is},async n=>{try{let r=D(Is,n),i=await w({tool:"thread_create",args:r,limiter:s,run:()=>Re({name:r.name,summary:r.summary??null,originSessionId:r.origin_session_id})});return C(i)}catch(r){return N(r)}}),e.registerTool("thread_add_session",{title:"Add session to thread",description:"Attach a session to a thread. If parent_session_id is provided the edge is role=child; otherwise role=origin. Upsert: re-adding updates the edge.",inputSchema:xs},async n=>{try{let r=D(xs,n),i=await w({tool:"thread_add_session",args:r,limiter:s,run:()=>ye({threadId:r.thread_id,sessionId:r.session_id,parentSessionId:r.parent_session_id??null,role:r.role,source:"manual",confidence:1})});return C(i)}catch(r){return N(r)}}),e.registerTool("thread_set_parent",{title:"Set edge parent within thread",description:"Change the parent session of an existing thread edge. Pass null to clear the parent and promote the edge to role=origin.",inputSchema:vs},async n=>{try{let r=D(vs,n),i=await w({tool:"thread_set_parent",args:r,limiter:s,run:()=>ae(r.thread_id,r.session_id,r.parent_session_id)});return C(i)}catch(r){return N(r)}}),e.registerTool("thread_remove_session",{title:"Remove session from thread",description:"Detach a session from a thread. The session itself is untouched; only the edge is removed. The updated thread is re-mirrored to disk.",inputSchema:Cs},async n=>{try{let r=D(Cs,n),i=await w({tool:"thread_remove_session",args:r,limiter:s,run:()=>Yt(r.thread_id,r.session_id)});return C({thread_id:r.thread_id,session_id:r.session_id,...i})}catch(r){return N(r)}}),e.registerTool("thread_rename",{title:"Rename thread",description:"Change the display name of a thread.",inputSchema:ks},async n=>{try{let r=D(ks,n),i=await w({tool:"thread_rename",args:r,limiter:s,run:()=>zt(r.thread_id,r.name)});return C(i)}catch(r){return N(r)}}),e.registerTool("thread_close",{title:"Close thread",description:"Mark the thread as closed (sets closed_at). Thread remains listed; reopen to clear.",inputSchema:ne},async n=>{try{let r=D(ne,n),i=await w({tool:"thread_close",args:r,limiter:s,run:()=>Kt(r.thread_id)});return C(i)}catch(r){return N(r)}}),e.registerTool("thread_reopen",{title:"Reopen thread",description:"Clear closed_at on a closed thread.",inputSchema:ne},async n=>{try{let r=D(ne,n),i=await w({tool:"thread_reopen",args:r,limiter:s,run:()=>Jt(r.thread_id)});return C(i)}catch(r){return N(r)}}),e.registerTool("thread_archive",{title:"Archive thread",description:"Soft-delete a thread by setting archived=1. Archived threads are hidden from thread_list by default but never hard-deleted; data is preserved in SQLite and the JSON mirror.",inputSchema:ne},async n=>{try{let r=D(ne,n),i=await w({tool:"thread_archive",args:r,limiter:s,run:()=>qt(r.thread_id)});return C(i)}catch(r){return N(r)}}),e.registerTool("thread_merge",{title:"Merge two threads",description:"Move every edge from source_id into dest_id, then delete source_id. Origin roles are preserved when present on either side.",inputSchema:Ds},async n=>{try{let r=D(Ds,n),i=await w({tool:"thread_merge",args:r,limiter:s,run:()=>Vt(r.source_id,r.dest_id)});return C(i)}catch(r){return N(r)}}),e.registerTool("thread_split",{title:"Split sessions into a new thread",description:"Peel the specified session_ids out of thread_id into a brand-new thread named new_thread_name. Non-member session_ids are silently skipped.",inputSchema:Ms},async n=>{try{let r=D(Ms,n),i=await w({tool:"thread_split",args:r,limiter:s,run:()=>Qt({threadId:r.thread_id,sessionIds:r.session_ids,newThreadName:r.new_thread_name})});return C(i)}catch(r){return N(r)}}),e.registerTool("sync_active_sessions",{title:"Sync currently active sessions in a repo into a thread",description:'Captures the Captain Code Pattern: scans every Claude session whose JSONL was touched within the rolling window in the given project, infers parent/child edges via the 7-signal scorer (temporal, continuation, file overlap, brand, semantic, cluster, doc-authorship), and stitches them into one thread. Idempotent: re-running reuses the existing "Active <project> sessions \u2014 *" thread, only appends new sessions, and never overwrites edges with source=manual. Use mode=preflight first to show the user the proposed plan, then mode=apply to write.',inputSchema:Us},async n=>{try{let r=D(Us,n);if(r.mode==="preflight"){let o=await it(r.project_id,{windowHours:r.window_hours});return C({plan:o})}let i=await w({tool:"sync_active_sessions",args:r,limiter:s,run:async()=>{let o=await it(r.project_id,{windowHours:r.window_hours}),a=ws(o);return{plan:o,result:a}}});return C(i)}catch(r){return N(r)}}),e.registerTool("generate_thread_titles",{title:"Generate titles for every session in a thread",description:"Walk a thread DAG in topology order (origins first by added_at, then children breadth-first) and generate a coherent agent title for each session. Each successive title sees already-titled ancestors and earlier siblings as strong context so a naming pattern emerges. Set force=true to regenerate already-titled sessions; default false skips them. Returns the final {generated, skipped, failed} summary after the whole walk completes.",inputSchema:Fs},async n=>{try{let r=D(Fs,n),i=await w({tool:"generate_thread_titles",args:r,limiter:s,run:async()=>{let o=await _s(r.thread_id,{force:r.force??!1});if(o.failed.length>0&&o.generated.length===0&&o.skipped.length===0)throw new Error(`all ${o.failed.length} session(s) failed title generation`);return o}});return C(i)}catch(r){return N(r)}})}import{Worker as Xi}from"node:worker_threads";import{join as Hi}from"node:path";import{existsSync as Bi}from"node:fs";import{existsSync as Pi}from"node:fs";import{dirname as Xs}from"node:path";import{fileURLToPath as $i}from"node:url";var Ae=null;function Hs(){if(Ae)return Ae;let e=Xs($i(import.meta.url));for(;!Pi(`${e}/package.json`);){let t=Xs(e);if(t===e)throw new Error(`package.json not found walking up from ${import.meta.url}`);e=t}return Ae=e,Ae}var Bs="BAAI/bge-base-en-v1.5";function Wi(){return Hi(Hs(),"dist","daemon","embedder-worker.js")}var de=null,ue=new Map,ot=0,Oe=!1,Ie=class extends Error{cause;constructor(t){let s=t instanceof Error?t.message:String(t);super(["Semantic search is unavailable on this platform.","",`Reason: ${s}`,"",`Platform: ${process.platform}/${process.arch}, Node ${process.version}`,"","Claude Recall supports macOS (arm64/x64), Linux (x64/arm64), and Windows (x64).","Core CLI features (search, list, context, daemon) work everywhere.","Only `recall semantic *` requires the on-device embedder.","","See: https://clauderecall.com/docs (Supported platforms) \u2014 or file an issue at","https://gitlab.com/clauderecallhq/clauderecallhq/-/issues with the platform line above."].join(`
|
|
900
|
+
`)),this.name="EmbedderUnavailableError",this.cause=t}};function Ws(e){for(let t of ue.values())t.reject(e);ue.clear()}function Gi(){if(de)return de;let e=Wi();if(!Bi(e))throw new Ie(new Error(`embedder-worker bundle not found at ${e}. Run \`npm run build:cli\` to emit it.`));let t=new Xi(e);return t.on("message",s=>{let n=ue.get(s.id);n&&(ue.delete(s.id),n.resolve(s))}),t.on("error",s=>{console.error("[embedder-worker] thread error:",s);let n=s instanceof Error?s:new Error(String(s));Ws(n),de=null,Oe=!1}),t.on("exit",s=>{s!==0&&(console.error(`[embedder-worker] exited with code ${s}`),Ws(new Error(`embedder worker exited with code ${s}`))),de=null,Oe=!1}),de=t,t}function Yi(e){return new Promise((t,s)=>{let n;try{n=Gi()}catch(r){s(r instanceof Error?r:new Error(String(r)));return}ue.set(e.id,{resolve:t,reject:s}),n.postMessage(e)})}function zi(){return ot=ot+1>>>0,String(ot)}function Ki(e){if(!e.ok)throw e.unavailable?new Ie(new Error(e.error)):new Error(e.error);return e}function pe(){return{loaded:Oe,modelId:Bs,dim:768}}async function Gs(e){if(!Oe)throw new Error("[embedder] Model not loaded. Call loadEmbedder() before embedding.");let t=Ki(await Yi({id:zi(),type:"embedQuery",text:e}));return new Float32Array(t.embedding)}L();F();import{writeFileSync as Ji}from"node:fs";import{join as qi}from"node:path";var Vi=qi(b,"recall-events.json");function Ys(e,t,s,n="cli"){m().prepare(`
|
|
886
901
|
INSERT INTO recall_events (session_id, recalled_at, token_count, mode, caller)
|
|
887
902
|
VALUES (?, datetime('now'), ?, ?, ?)
|
|
888
|
-
`).run(e,t,s,n),
|
|
889
|
-
`,"utf-8")}L();async function
|
|
903
|
+
`).run(e,t,s,n),Qi()}function Qi(){x();let t=m().prepare("SELECT id, session_id, recalled_at, token_count, mode, caller FROM recall_events ORDER BY recalled_at DESC").all();Ji(Vi,JSON.stringify(t,null,2)+`
|
|
904
|
+
`,"utf-8")}L();async function zs(e,t=50){let s=await Gs(e),n=m(),r=Buffer.from(s.buffer,s.byteOffset,s.byteLength);return n.prepare(`SELECT v.rowid, v.distance, cm.session_id, cm.text, cm.message_uuids
|
|
890
905
|
FROM vec_chunks v JOIN chunk_meta cm ON cm.rowid = v.rowid
|
|
891
|
-
WHERE v.embedding MATCH ? AND k = ? ORDER BY v.distance`).all(
|
|
892
|
-
WHERE v.embedding MATCH ? AND k = ? ORDER BY v.distance`).all(
|
|
906
|
+
WHERE v.embedding MATCH ? AND k = ? ORDER BY v.distance`).all(r,t).map(o=>({sessionId:o.session_id,chunkRowid:o.rowid,distance:o.distance,text:o.text,messageUuids:JSON.parse(o.message_uuids)}))}async function Ks(e,t=10,s=.65){let n=m(),r=n.prepare("SELECT rowid FROM chunk_meta WHERE session_id = ? ORDER BY rowid LIMIT 1").get(e);if(!r)return[];let i=n.prepare("SELECT embedding FROM vec_chunks WHERE rowid = ?").get(r.rowid);if(!i)return[];let o=n.prepare(`SELECT v.rowid, v.distance, cm.session_id FROM vec_chunks v JOIN chunk_meta cm ON cm.rowid = v.rowid
|
|
907
|
+
WHERE v.embedding MATCH ? AND k = ? ORDER BY v.distance`).all(i.embedding,t*5),a=new Map;for(let c of o){if(c.session_id===e)continue;let d=a.get(c.session_id);(d===void 0||c.distance<d)&&a.set(c.session_id,c.distance)}let l=[];for(let[c,d]of a){let u=1-d;u>=s&&l.push({sessionId:c,similarity:u})}return l.sort((c,d)=>d.similarity-c.similarity),l.slice(0,t)}function Zi(){let e=process.env.RECALL_RRF_K;if(e){let t=parseInt(e,10);if(!isNaN(t)&&t>=1&&t<=1e3)return t}return 60}function Js(e){let t=Zi(),s=new Map;for(let r of e)for(let i=0;i<r.length;i++){let o=r[i],a=1/(t+i+1),l=s.get(o.id);l?(l.score+=a,l.lanes.push(o.lane),i+1<l.bestRank&&(l.bestRank=i+1,l.bestData=o.data)):s.set(o.id,{score:a,lanes:[o.lane],bestRank:i+1,bestData:o.data})}let n=[];for(let[r,i]of s)n.push({id:r,score:i.score,lanes:i.lanes,data:i.bestData});return n.sort((r,i)=>i.score-r.score),n}L();L();var eo=!1,to=null;var so=new Set;function no(){return m().prepare("SELECT COUNT(*) AS n FROM chunk_queue").get().n}function qs(){return{running:eo,queueDepth:no(),lastProcessedAt:to,blacklistedCount:so.size}}import{existsSync as ro,mkdirSync as jc,rmSync as Xc,createWriteStream as Hc,statSync as Bc}from"node:fs";import{join as Vs}from"node:path";F();var io=[{path:"config.json",sha256:"bc00af31a4a31b74040d73370aa83b62da34c90b75eb77bfa7db039d90abd591"},{path:"tokenizer.json",sha256:"d241a60d5e8f04cc1b2b3e9ef7a4921b27bf526d9f6050ab90f9267a1f9e5c66"},{path:"tokenizer_config.json",sha256:"9261e7d79b44c8195c1cada2b453e55b00aeb81e907a6664974b4d7776172ab3"},{path:"onnx/model.onnx",sha256:"9bc579acdba21c253c62a9bf866891355a63ffa3442b52c8a37d75b2ccb91848"}];function oo(){return Vs(b,"models","BAAI","bge-base-en-v1.5")}function Qs(){let e=oo();return io.every(t=>ro(Vs(e,t.path)))}L();L();F();import{join as at}from"node:path";var ao=new Set(["pending","approved","rejected"]),lo=new Set(["L1","L2","L3","L4","user"]),qc=at(b,"links"),co=at(b,"suggestions"),Vc=at(co,"index.json");function Zs(e){try{return JSON.parse(e)}catch{return e}}function uo(e){return{id:e.id,source_session_id:e.source_session_id,target_session_id:e.target_session_id,link_type:e.link_type,confidence:e.confidence,source:e.source,evidence:Zs(e.evidence),approved:e.approved===1,created_at:e.created_at,updated_at:e.updated_at}}function po(e){return{id:e.id,source_session_id:e.source_session_id,target_session_id:e.target_session_id,link_type:e.link_type,confidence:e.confidence,evidence:Zs(e.evidence),status:e.status,inferred_by:e.inferred_by,created_at:e.created_at,decided_at:e.decided_at}}function mo(e){if(!lo.has(e))throw new Error(`invalid inferred_by: ${e}`)}function lt(e){return m().prepare(`SELECT * FROM session_links
|
|
893
908
|
WHERE source_session_id = ? OR target_session_id = ?
|
|
894
|
-
ORDER BY confidence DESC, updated_at DESC`).all(e,e).map(
|
|
909
|
+
ORDER BY confidence DESC, updated_at DESC`).all(e,e).map(uo)}function ct(e={}){let t=m(),s=[],n=[];if(e.status){if(!ao.has(e.status))throw new Error(`invalid status: ${e.status}`);s.push("status = ?"),n.push(e.status)}e.sourceSessionId&&(s.push("source_session_id = ?"),n.push(e.sourceSessionId)),e.targetSessionId&&(s.push("target_session_id = ?"),n.push(e.targetSessionId)),e.inferredBy&&(mo(e.inferredBy),s.push("inferred_by = ?"),n.push(e.inferredBy));let r=s.length?`WHERE ${s.join(" AND ")}`:"",i=Math.max(1,Math.min(5e3,e.limit??1e3));return t.prepare(`SELECT * FROM session_link_suggestions ${r}
|
|
895
910
|
ORDER BY confidence DESC, created_at DESC
|
|
896
|
-
LIMIT ?`).all(...n,
|
|
911
|
+
LIMIT ?`).all(...n,i).map(po)}var go=4e3,_o=2,fo=30,ho=.2,Eo={citation:1,wiki_link:.9,similar:.7,skill_track:.5,bug_pattern:.4,temporal_proximity:.3};function xe(e){return e?Math.ceil(e.length/4):0}function en(e){if(!Number.isFinite(e)||e<0)return 1;let t=Math.exp(-e/fo);return Math.max(ho,t)}function tn(e,t){if(!e||!t)return 0;let s=Date.parse(e),n=Date.parse(t);return!Number.isFinite(s)||!Number.isFinite(n)?0:Math.abs(n-s)/(1e3*60*60*24)}function sn(e){return m().prepare(`SELECT s.id,
|
|
897
912
|
NULLIF(sa.alias, '') AS alias,
|
|
898
913
|
s.auto_title,
|
|
899
914
|
s.auto_title_source,
|
|
@@ -904,25 +919,25 @@ ${s}`}function Zi(e){return es(e)?.auto_title_source??null}async function er(e){
|
|
|
904
919
|
FROM sessions s
|
|
905
920
|
LEFT JOIN session_aliases sa ON sa.session_id = s.id
|
|
906
921
|
LEFT JOIN projects p ON p.id = s.project_id
|
|
907
|
-
WHERE s.id = ?`).get(e)??null}function
|
|
922
|
+
WHERE s.id = ?`).get(e)??null}function nn(e){let s=m().prepare("SELECT summary FROM session_semantic WHERE session_id = ?").get(e);if(!s||!s.summary)return null;let n=s.summary.trim();return n.length>0?n:null}function rn(e){let t=e.alias?.trim(),s=e.auto_title?.trim(),n=e.first_user_message?.trim();return s&&e.auto_title_source==="agent"?s:t||s||(n?n.slice(0,80):e.id.slice(0,8))}function To(e){let s=m().prepare(`SELECT id, auto_title, started_at
|
|
908
923
|
FROM sessions
|
|
909
924
|
WHERE project_id = ?
|
|
910
|
-
ORDER BY COALESCE(started_at, ''), id`).all(e),n=new Set,
|
|
925
|
+
ORDER BY COALESCE(started_at, ''), id`).all(e),n=new Set,r=new Set,i=[];for(let p of s){if(!p.auto_title||!p.auto_title.startsWith("/")){i.push({id:p.id,brand:null,skill:null});continue}let g=p.auto_title.split(" \xB7 "),_=g[0].trim(),f=g.length>1?g.slice(1).join(" \xB7 ").trim():null;i.push({id:p.id,brand:f||null,skill:_||null}),f&&n.add(f),_&&r.add(_)}let o=[...n].sort(),a=new Map;o.forEach((p,g)=>a.set(p,g));let l=[...r].sort(),c=new Map;l.forEach((p,g)=>c.set(p,g));let d=new Map,u=new Map;for(let p of i){if(!p.brand||!p.skill)continue;let g=a.get(p.brand),_=c.get(p.skill);if(g===void 0||_===void 0)continue;let f=`${g}.${_}`,E=(d.get(f)??0)+1;d.set(f,E),u.set(p.id,`${g}.${_}.${E}`)}return{byId:u}}function So(e){return{table:e!==null?To(e):null,originProjectId:e,cache:new Map}}function ve(e,t){let s=e.cache.get(t);if(s)return s;let n=sn(t);if(!n)return null;let r=e.table&&n.project_id===e.originProjectId?e.table.byId.get(t)??null:null,i={session_id:n.id,title:rn(n),decimal:r,summary:nn(n.id),project:n.project,started_at:n.started_at};return e.cache.set(t,i),i}function bo(e,t){let n=m().prepare(`SELECT DISTINCT te.parent_session_id AS pid
|
|
911
926
|
FROM thread_edges te
|
|
912
927
|
WHERE te.session_id = ?
|
|
913
|
-
AND te.parent_session_id IS NOT NULL`).all(t),
|
|
928
|
+
AND te.parent_session_id IS NOT NULL`).all(t),r=[];for(let i of n){if(!i.pid)continue;let o=ve(e,i.pid);o&&r.push(o)}return r}function Ro(e,t){let n=m().prepare(`SELECT DISTINCT te.session_id AS sid
|
|
914
929
|
FROM thread_edges te
|
|
915
|
-
WHERE te.parent_session_id = ?`).all(t),
|
|
916
|
-
${
|
|
930
|
+
WHERE te.parent_session_id = ?`).all(t),r=[];for(let i of n){if(!i.sid)continue;let o=ve(e,i.sid);o&&r.push(o)}return r}function on(e){let t=Eo[e.linkType]??.5,s=re(e.confidence),n=t*s,r=en(e.daysApart),i=e.embeddingCosine??.5,o=re(e.pagerank);if(e.scoring==="pagerank")return re(o);if(e.scoring==="embedding-rerank")return e.embeddingCosine===null?re(n):re(i);let a=.35*n+.2*r+.2*i+.25*o;return re(a)}function re(e){return!Number.isFinite(e)||e<0?0:e>1?1:e}function yo(e,t,s,n,r){let i=new Map;function o(a,l){if(a===l)return;let c=i.get(a);c||(c=new Set,i.set(a,c)),c.add(l)}for(let a of t)o(a.source_session_id,a.target_session_id),o(a.target_session_id,a.source_session_id);for(let a of s)o(e,a.session_id);for(let a of s)o(a.session_id,e);for(let a of n)o(e,a.session_id);for(let a of n)o(a.session_id,e);if(r>1){let a=new Set([e]),l=new Set([e]);for(let c=1;c<r;c++){let d=new Set;for(let u of a){let p=i.get(u);if(p)for(let g of p){if(l.has(g))continue;let _=lt(g).filter(f=>f.approved);for(let f of _)o(f.source_session_id,f.target_session_id),o(f.target_session_id,f.source_session_id);l.add(g),d.add(g)}}if(d.size===0)break;for(let u of d)a.add(u)}}return{edges:i}}function No(e,t={}){let s=t.iterations??12,n=t.damping??.85,r=Array.from(e.edges.keys());if(r.length===0)return new Map;let i=1/r.length,o=new Map(r.map(c=>[c,i]));for(let c=0;c<s;c++){let d=new Map(r.map(u=>[u,(1-n)/r.length]));for(let u of r){let p=e.edges.get(u);if(!p||p.size===0)continue;let g=(o.get(u)??0)/p.size;for(let _ of p)d.set(_,(d.get(_)??0)+n*g)}o=d}let a=0;for(let c of o.values())c>a&&(a=c);if(a<=0)return o;let l=new Map;for(let[c,d]of o)l.set(c,d/a);return l}var an=240;function ln(e,t){let s=e.replace(/\s+/g," ").trim();return s.length<=t?s:`${s.slice(0,t-1).trimEnd()}\u2026`}function wo(e){let t=e.decimal?`${e.decimal} `:"",s=e.session_id.slice(0,8),n="evidence"in e&&e.evidence?` \u2014 ${e.evidence}`:"",r=`- ${t}${e.title} (${s})${n}`;if(e.summary){let i=ln(e.summary,an);return`${r}
|
|
931
|
+
${i}`}return r}function Lo(e,t,s){let n=[],r=[],i=0,o=e.decimal?`${e.decimal}: `:"",a=`# Neighborhood for ${e.session_id} (${o}${e.title})`;if(n.push(a),i+=xe(a),e.summary){let l=ln(e.summary,an*4);n.push(l),i+=xe(l)}n.push("");for(let l of t){if(l.refs.length===0)continue;let c=`## ${l.heading}`,d=xe(c),u=[],p=0;for(let g of l.refs){let _=wo(g),f=xe(_);if(i+d+p+f>s){r.push({session_id:g.session_id,title:g.title,decimal:g.decimal,summary:g.summary,project:g.project,started_at:g.started_at});continue}u.push(_),p+=f}if(u.length>0){n.push(c);for(let g of u)n.push(g);n.push(""),i+=d+p}}for(;n.length>0&&n[n.length-1]==="";)n.pop();return{bundle:n.join(`
|
|
917
932
|
`)+`
|
|
918
|
-
`,budgetUsed:
|
|
933
|
+
`,budgetUsed:i,truncated:r}}function Ao(e,t,s,n,r,i){let o=[];for(let a of s){if(n&&!n.has(a.link_type))continue;let l=null;if(a.source_session_id===t.session_id?l=a.target_session_id:a.target_session_id===t.session_id&&(l=a.source_session_id),!l)continue;let c=ve(e,l);if(!c)continue;let d=tn(t.started_at,c.started_at),u=on({confidence:a.confidence,linkType:a.link_type,daysApart:d,embeddingCosine:null,pagerank:i.get(l)??0,scoring:r});o.push({...c,score:u,evidence:`(suggestion, ${a.inferred_by}) confidence=${a.confidence.toFixed(2)} ${Math.round(d)}d apart`,link_type:a.link_type})}return o}function cn(e,t={}){let s=Math.max(100,Math.floor(t.budget??go)),n=t.scoring??"hybrid",r=Math.max(1,Math.min(5,t.maxDepth??_o)),i=t.includeWikiLinks??!0,o=t.includeSuggestions??!1,a=t.edgeTypes?new Set(t.edgeTypes):null,l=sn(e);if(!l)throw new Error(`session not found: ${e}`);let c=So(l.project_id),d={session_id:l.id,title:rn(l),decimal:c.table?.byId.get(l.id)??null,summary:nn(l.id),project:l.project,started_at:l.started_at};c.cache.set(l.id,d);let u=bo(c,e),p=Ro(c,e),g=lt(e).filter(A=>A.approved).filter(A=>!a||a.has(A.link_type)).filter(A=>i||A.link_type!=="wiki_link"),_=yo(e,g,u,p,r),f=No(_),E=[],S=[],y=[],M=[];for(let A of g){let ee=A.source_session_id===e?A.target_session_id:A.source_session_id,ie=ve(c,ee);if(!ie)continue;let te=tn(d.started_at,ie.started_at),Ce=on({confidence:A.confidence,linkType:A.link_type,daysApart:te,embeddingCosine:null,pagerank:f.get(ee)??0,scoring:n}),ke=en(te),P=`${A.link_type} confidence=${A.confidence.toFixed(2)} recency=${ke.toFixed(2)} (${Math.round(te)}d apart)`,_e={...ie,score:Ce,evidence:P,link_type:A.link_type};A.link_type==="citation"?E.push(_e):A.link_type==="similar"?S.push(_e):A.link_type==="wiki_link"?M.push(_e):y.push(_e)}if(o){let A=ct({sourceSessionId:e,status:"pending",limit:100}),ee=ct({targetSessionId:e,status:"pending",limit:100}),ie=[...A,...ee],te=new Set,Ce=ie.filter(P=>te.has(P.id)?!1:(te.add(P.id),!0)),ke=Ao(c,d,Ce,a,n,f);for(let P of ke)P.link_type==="citation"?E.push(P):P.link_type==="similar"?S.push(P):P.link_type==="wiki_link"?M.push(P):y.push(P)}let R=(A,ee)=>ee.score-A.score;E.sort(R),S.sort(R),y.sort(R),M.sort(R);let ge=Lo(d,[{heading:"Parents",refs:u},{heading:"Children",refs:p},{heading:"Citations (approved)",refs:E},{heading:"Similar sessions",refs:S},{heading:"Cousins (skill track + temporal)",refs:y},{heading:"Wiki links (manual)",refs:M}],s);return{origin:d,parents:u,children:p,citations:E,similar:S,cousins:y,wikiLinks:M,bundle:ge.bundle,budgetUsed:ge.budgetUsed,budgetRemaining:Math.max(0,s-ge.budgetUsed),truncated:ge.truncated}}import{readFileSync as zo}from"node:fs";import{dirname as Ko,join as Jo}from"node:path";var qo=(()=>{try{let e=Ko(Yo(import.meta.url));return Jo(e,"..","..","package.json")}catch{return"package.json"}})(),Vo=(()=>{try{return JSON.parse(zo(qo,"utf8")).version??"0.0.0"}catch{return"0.0.0"}})();function Qo(){let e=process.env.RECALL_MCP_ALLOW_WRITES;return e==="1"||e==="true"}function v(e){return{content:[{type:"text",text:JSON.stringify(e,null,2)}]}}function pt(e){return{content:[{type:"text",text:e}]}}function H(e){return{content:[{type:"text",text:e}],isError:!0}}function Zo(){let e=new Wo({name:"claude-recall",version:Vo}),t=Qo();if(e.registerTool("list_projects",{title:"List projects",description:"Every Claude Code project currently indexed by Recall, with session and message counts and the most recent activity timestamp.",inputSchema:{}},async()=>{let n=m().prepare(`SELECT p.name,
|
|
919
934
|
COUNT(s.id) AS session_count,
|
|
920
935
|
COALESCE(SUM(s.message_count), 0) AS message_count,
|
|
921
936
|
MAX(s.started_at) AS latest
|
|
922
937
|
FROM projects p
|
|
923
938
|
LEFT JOIN sessions s ON s.project_id = p.id
|
|
924
939
|
GROUP BY p.id
|
|
925
|
-
ORDER BY MAX(COALESCE(s.started_at, '')) DESC`).all();return v(n)}),e.registerTool("list_sessions",{title:"List sessions",description:"Recent sessions with alias, tags, and message counts. Optional filters for project, tag, and date range.",inputSchema:{project:T.string().optional().describe("Substring match against project name or decoded filesystem path."),tag:T.string().optional().describe("Only sessions carrying this tag (leading # optional)."),since:T.string().optional().describe("Only sessions started at or after this ISO timestamp or YYYY-MM-DD date."),until:T.string().optional().describe("Only sessions started at or before this ISO timestamp or YYYY-MM-DD date."),limit:T.number().int().min(1).max(500).optional()}},async({project:s,tag:n,since:
|
|
940
|
+
ORDER BY MAX(COALESCE(s.started_at, '')) DESC`).all();return v(n)}),e.registerTool("list_sessions",{title:"List sessions",description:"Recent sessions with alias, tags, and message counts. Optional filters for project, tag, and date range.",inputSchema:{project:T.string().optional().describe("Substring match against project name or decoded filesystem path."),tag:T.string().optional().describe("Only sessions carrying this tag (leading # optional)."),since:T.string().optional().describe("Only sessions started at or after this ISO timestamp or YYYY-MM-DD date."),until:T.string().optional().describe("Only sessions started at or before this ISO timestamp or YYYY-MM-DD date."),limit:T.number().int().min(1).max(500).optional()}},async({project:s,tag:n,since:r,until:i,limit:o})=>{let a=m(),l={limit:o??100},c="s.message_count > 2";if(s&&(c+=" AND (p.name LIKE @proj OR p.decoded_path LIKE @proj)",l.proj=`%${s}%`),r&&(c+=" AND s.started_at >= @since",l.since=r),i&&(c+=" AND s.started_at <= @until",l.until=/^\d{4}-\d{2}-\d{2}$/.test(i)?`${i}T23:59:59.999Z`:i),n){let p=V(n);p&&(c+=" AND s.id IN (SELECT session_id FROM session_tags WHERE tag = @tag)",l.tag=p)}let u=a.prepare(`SELECT s.id, p.name AS project, s.started_at, s.ended_at,
|
|
926
941
|
s.message_count, s.first_user_message, s.git_branch,
|
|
927
942
|
NULLIF(sa.alias, '') AS alias,
|
|
928
943
|
CASE WHEN sn.content IS NOT NULL AND sn.content != '' THEN 1 ELSE 0 END AS has_notes,
|
|
@@ -935,9 +950,9 @@ ${s}`}function Zi(e){return es(e)?.auto_title_source??null}async function er(e){
|
|
|
935
950
|
JOIN projects p ON p.id = s.project_id
|
|
936
951
|
LEFT JOIN session_aliases sa ON sa.session_id = s.id
|
|
937
952
|
LEFT JOIN session_notes sn ON sn.session_id = s.id
|
|
938
|
-
WHERE ${
|
|
953
|
+
WHERE ${c}
|
|
939
954
|
ORDER BY COALESCE(s.started_at, '') DESC
|
|
940
|
-
LIMIT @limit`).all(
|
|
955
|
+
LIMIT @limit`).all(l).map(({tags_csv:p,...g})=>({...g,tags:p?p.split(","):[]}));return v(u)}),e.registerTool("list_tags",{title:"List tags",description:"Every tag currently applied to a session, with its count, most popular first.",inputSchema:{}},async()=>v(yt())),e.registerTool("search",{title:"Search messages",description:"Full-text search over every message in every indexed session. Use `#tag-name` tokens inside the query string to also filter by tag; plain terms are ANDed together.",inputSchema:{query:T.string().describe("Text to find. Supports inline `#tag-name` tokens to narrow to sessions with the tag."),project:T.string().optional().describe("Substring match against project name or path."),limit:T.number().int().min(1).max(200).optional()}},async({query:s,project:n,limit:r})=>{let i=m(),o=s.trim();if(!o)return v({query:"",hits:[],tags:[]});let a=o.split(/\s+/).filter(Boolean),l=a.filter(f=>f.startsWith("#")).map(V).filter(f=>!!f),d=a.filter(f=>!f.startsWith("#")).map(f=>`"${f.replace(/"/g,"")}"`).join(" "),u=Math.max(1,Math.min(200,r??30));if(!d&&l.length>0){let f=`
|
|
941
956
|
SELECT s.id AS session_id,
|
|
942
957
|
s.id AS message_uuid,
|
|
943
958
|
p.name AS project,
|
|
@@ -950,7 +965,7 @@ ${s}`}function Zi(e){return es(e)?.auto_title_source??null}async function er(e){
|
|
|
950
965
|
JOIN projects p ON p.id = s.project_id
|
|
951
966
|
LEFT JOIN session_aliases sa ON sa.session_id = s.id
|
|
952
967
|
WHERE 1=1
|
|
953
|
-
`,
|
|
968
|
+
`,E={limit:u};return n&&(f+=" AND (p.name LIKE @proj OR p.decoded_path LIKE @proj)",E.proj=`%${n}%`),l.forEach((S,y)=>{f+=` AND s.id IN (SELECT session_id FROM session_tags WHERE tag = @tag_${y})`,E[`tag_${y}`]=S}),f+=" ORDER BY COALESCE(s.started_at, '') DESC LIMIT @limit",v({query:o,hits:i.prepare(f).all(E),tags:l})}if(!d)return v({query:o,hits:[],tags:l});let p=`
|
|
954
969
|
SELECT m.session_id AS session_id,
|
|
955
970
|
m.uuid AS message_uuid,
|
|
956
971
|
p.name AS project,
|
|
@@ -965,7 +980,7 @@ ${s}`}function Zi(e){return es(e)?.auto_title_source??null}async function er(e){
|
|
|
965
980
|
JOIN projects p ON p.id = s.project_id
|
|
966
981
|
LEFT JOIN session_aliases sa ON sa.session_id = s.id
|
|
967
982
|
WHERE messages_fts MATCH @fts
|
|
968
|
-
`,g={fts:d,limit:u};n&&(p+=" AND (p.name LIKE @proj OR p.decoded_path LIKE @proj)",g.proj=`%${n}%`),
|
|
983
|
+
`,g={fts:d,limit:u};n&&(p+=" AND (p.name LIKE @proj OR p.decoded_path LIKE @proj)",g.proj=`%${n}%`),l.forEach((f,E)=>{p+=` AND s.id IN (SELECT session_id FROM session_tags WHERE tag = @tag_${E})`,g[`tag_${E}`]=f}),p+=" ORDER BY bm25(messages_fts) LIMIT @limit";let _=i.prepare(p).all(g);if(pe().loaded)try{let f=await zs(o,u),E=_.map(R=>({id:String(R.session_id),data:R,lane:"bm25"})),S=f.map(R=>({id:R.sessionId,data:{session_id:R.sessionId,snippet:R.text,matched_via:"vector"},lane:"vector"})),M=Js([E,S]).slice(0,u).map(R=>({...R.data,session_id:R.id,rrf_score:R.score,lanes:R.lanes}));return v({query:o,hits:M,tags:l,fusion:"rrf"})}catch{}return v({query:o,hits:_,tags:l})}),e.registerTool("find_similar_sessions",{title:"Find similar sessions",description:"Find sessions semantically similar to a given session using vector embeddings (Pro-only). Returns related sessions ranked by cosine similarity.",inputSchema:{session_id:T.string().uuid().describe("Session UUID to find similar sessions for."),limit:T.number().int().min(1).max(50).optional().describe("Max results (default 10)."),min_cosine:T.number().min(0).max(1).optional().describe("Minimum cosine similarity threshold (default 0.65).")}},async({session_id:s,limit:n,min_cosine:r})=>{if(!pe().loaded)return v({upgrade_required:!0,reason:"Semantic vector search requires Pro with the embedding model installed.",buy_url:"https://clauderecall.com/pro"});try{let i=await Ks(s,n??10,r??.65);return v({session_id:s,similar:i})}catch(i){return H(i instanceof Error?i.message:"vector search failed")}}),e.registerTool("semantic_status",{title:"Semantic search status",description:"Health snapshot of the semantic vector search tier: model status, worker status, queue depth.",inputSchema:{}},async()=>{let s=pe(),n=qs(),r=Qs();return v({embedder:s,worker:n,modelInstalled:r})}),e.registerTool("get_session",{title:"Get session transcript",description:"Return the full metadata and ordered messages for a session. Accepts a full UUID or an 8+-character id prefix.",inputSchema:{id:T.string().describe("Session id (full UUID or 8+-character prefix).")}},async({id:s})=>{let n=X(s);if(!n)return H(`session not found or prefix ambiguous: ${s}`);let r=m(),i=r.prepare(`SELECT s.id, s.project_id, s.started_at, s.ended_at,
|
|
969
984
|
s.message_count, s.user_message_count, s.assistant_message_count,
|
|
970
985
|
s.first_user_message, s.git_branch, s.version, s.indexed_at,
|
|
971
986
|
p.name AS project_name,
|
|
@@ -973,11 +988,11 @@ ${s}`}function Zi(e){return es(e)?.auto_title_source??null}async function er(e){
|
|
|
973
988
|
FROM sessions s
|
|
974
989
|
JOIN projects p ON p.id = s.project_id
|
|
975
990
|
LEFT JOIN session_aliases sa ON sa.session_id = s.id
|
|
976
|
-
WHERE s.id = ?`).get(n);if(!
|
|
991
|
+
WHERE s.id = ?`).get(n);if(!i)return H(`session metadata missing for ${n}`);let o=he(n),a=r.prepare(`SELECT uuid, type, role, timestamp, is_sidechain, content_text, tool_names
|
|
977
992
|
FROM messages WHERE session_id = ?
|
|
978
|
-
ORDER BY COALESCE(timestamp, ''), rowid`).all(n);return v({session:{...
|
|
993
|
+
ORDER BY COALESCE(timestamp, ''), rowid`).all(n);return v({session:{...i,tags:o},messages:a})}),e.registerTool("context_for_session",{title:"Export session as context",description:"Render a past session as markdown ready to paste into a fresh Claude conversation. This is the flagship Recall operation: pipe any previous session back into a new chat as memory.",inputSchema:{id:T.string().describe("Session id (full UUID or 8+-character prefix)."),mode:T.enum(["condensed","full"]).optional().describe("`condensed` (default) strips tool-call JSON; `full` keeps everything."),includeSidechain:T.boolean().optional().describe("Include subagent / sidechain messages (default false)."),prelude:T.string().max(1e4).optional().describe("Optional header prepended above the transcript (max 10 000 chars)."),since:T.string().optional().describe("Only messages at or after this ISO timestamp.")}},async({id:s,mode:n,includeSidechain:r,prelude:i,since:o})=>{let a=X(s);if(!a)return H(`session not found or prefix ambiguous: ${s}`);let l=m(),c=l.prepare(`SELECT s.id, p.name AS project_name,
|
|
979
994
|
s.started_at, s.ended_at, s.message_count, s.git_branch
|
|
980
995
|
FROM sessions s JOIN projects p ON p.id = s.project_id
|
|
981
|
-
WHERE s.id = ?`).get(a);if(!
|
|
996
|
+
WHERE s.id = ?`).get(a);if(!c)return H(`session metadata missing for ${a}`);let d=l.prepare(`SELECT uuid, type, role, timestamp, is_sidechain, content_text, tool_names
|
|
982
997
|
FROM messages WHERE session_id = ?
|
|
983
|
-
ORDER BY COALESCE(timestamp, ''), rowid`).all(a),u=
|
|
998
|
+
ORDER BY COALESCE(timestamp, ''), rowid`).all(a),u=bt(c,d,{mode:n??"condensed",includeSidechain:r??!1,prelude:i??null,since:o??null}),p=o?"since":n??"condensed";return Ys(a,Math.ceil(u.length/4),p,"mcp"),pt(u)}),e.registerTool("recall_neighborhood",{title:"Recall: neighborhood context bundle",description:"Build a ranked, budget-bounded markdown bundle for a session: parents + children from thread_edges, plus approved citations / similar / cousins / wiki_links from the cognitive graph. Pipe-friendly output ready to seed a fresh conversation. Reads only approved edges by default \u2014 pending suggestions are opt-in.",inputSchema:{session_id:T.string().describe("Session UUID or 8+-character prefix."),budget:T.number().int().min(100).max(5e4).optional().describe("Token budget for the assembled bundle. Default 4000."),scoring:T.enum(["pagerank","embedding-rerank","hybrid"]).optional().describe("Scoring mode (default hybrid)."),edge_types:T.array(T.enum(["citation","similar","skill_track","bug_pattern","wiki_link","temporal_proximity"])).optional().describe("Restrict to certain link types. Default: all approved types."),max_depth:T.number().int().min(1).max(5).optional().describe("Pagerank traversal depth on the local subgraph (default 2)."),include_wiki_links:T.boolean().optional().describe("Include manual wiki_link rows. Default true."),include_suggestions:T.boolean().optional().describe("Surface pending session_link_suggestions. Debug only; default false."),format:T.enum(["markdown","json"]).optional().describe("markdown (default) returns the bundle as text; json returns the full NeighborhoodResult.")}},async({session_id:s,budget:n,scoring:r,edge_types:i,max_depth:o,include_wiki_links:a,include_suggestions:l,format:c})=>{let d=X(s);if(!d)return H(`session not found or prefix ambiguous: ${s}`);try{let u=cn(d,{budget:n,scoring:r,edgeTypes:i,maxDepth:o,includeWikiLinks:a,includeSuggestions:l});return c==="json"?v(u):pt(u.bundle)}catch(u){return H(u instanceof Error?u.message:String(u))}}),e.registerTool("doctor",{title:"Health check",description:"Read-only diagnostic snapshot of the local Claude Recall database: total size, WAL size, free pages, FTS5 fragmentation for messages and sessions, vector index row count, free disk space, integrity check, and row counts per surface table. Returns structured JSON. Equivalent to running `recall doctor --json` from the shell. Safe to call as often as needed; no side effects.",inputSchema:{}},async()=>{let{buildHealthReport:s}=await Promise.resolve().then(()=>(yn(),Rn));return v(s())}),$s(e),t){let s=Number(process.env.RECALL_MCP_RATE_LIMIT),n=Number.isFinite(s)&&s>0?s:Ke,r=m(),i=new Date(Date.now()-6e4).toISOString(),o=r.prepare("SELECT at FROM mcp_audit_events WHERE at >= ? ORDER BY at ASC").all(i),a=new K(n);for(let l of o){let c=new Date(l.at).getTime();if(Number.isFinite(c))try{a.consume(c)}catch{break}}e.registerTool("list_sessions_to_tag",{title:"List sessions to tag",description:"Return session summaries and sampled messages formatted for an agent to propose tags. Respects the same scope filters used by the Recall UI scan. Only returns data if auto-tagging is enabled in config.",inputSchema:{untaggedOnly:T.boolean().optional().describe("Only sessions with zero tags (default false)."),project:T.string().optional().describe("Exact project name match."),collectionId:T.string().optional().describe("Only sessions in this collection."),limit:T.number().int().min(1).max(200).optional()}},async l=>{let c=je();if(!c.enabled)return H("auto-tagging is disabled; enable it in Recall settings before scanning");let d=Te({untaggedOnly:l.untaggedOnly,project:l.project,collectionId:l.collectionId,limit:l.limit??50});return v({count:d.length,sessions:d,guidance:`Produce ${c.minTagsPerSession}-${c.maxTagsPerSession} tags per session. Prefer existing tags from list_tags for consistency. Use apply_tags to write results.`})}),e.registerTool("apply_tags",{title:"Apply tags to a session",description:"Add one or more tags to a session (merge-mode, never removes existing). Only works when auto-tagging is enabled. Accepts full UUID or 8+-character id prefix.",inputSchema:{sessionId:T.string().describe("Full session UUID or 8+-character prefix."),tags:T.array(T.string()).min(1).max(10).describe("Tags to add. Normalized server-side (lowercase, hyphens, strip #).")}},async({sessionId:l,tags:c})=>{if(!je().enabled)return H("auto-tagging is disabled; enable it in Recall settings before writing tags");let u=X(l);if(!u)return H(`session not found or prefix ambiguous: ${l}`);try{let p=await w({tool:"apply_tags",args:{sessionId:u,tags:c},limiter:a,run:()=>{let g=[],_=[];for(let f of c)try{let{tag:E,added:S}=fe(u,f);S?g.push(E):_.push({tag:E,reason:"already present"})}catch(E){_.push({tag:f,reason:E instanceof Error?E.message:String(E)})}return{sessionId:u,applied:g,skipped:_}}});return v(p)}catch(p){return p instanceof Error&&p.message.startsWith("SQLITE_")?H("database constraint error"):H(p instanceof Error?p.message:String(p))}}),e.registerTool("optimize",{title:"Optimize the database",description:"Run the local maintenance pass: WAL checkpoint (TRUNCATE), FTS5 segment merge for messages and sessions, refresh planner stats. Equivalent to `recall optimize` from the shell. Safe while the daemon is running. Set vacuum=true to also reclaim free pages \u2014 but VACUUM rewrites the entire DB and requires the daemon to be stopped, so it errors out if the daemon is up. Write-mode only.",inputSchema:{vacuum:T.boolean().optional().describe("Also run VACUUM. Requires the daemon to be stopped.")}},async({vacuum:l})=>{let{runOptimize:c}=await Promise.resolve().then(()=>(wn(),Nn)),d=process.stdout.write.bind(process.stdout),u="";process.stdout.write=p=>(u+=typeof p=="string"?p:Buffer.from(p).toString("utf-8"),!0);try{await c({vacuum:!!l,json:!0})}finally{process.stdout.write=d}try{return v(JSON.parse(u.trim()))}catch{return pt(u.trim())}}),Ut(e,{limiter:a}),js(e,{limiter:a}),console.error(`[claude-recall-mcp] MCP writes ENABLED \u2014 write tools registered (rate limit ${n}/min)`),console.error("[claude-recall-mcp] MCP thread writes ENABLED")}else console.error("[claude-recall-mcp] MCP writes DISABLED (read-only) \u2014 pass --allow-writes to enable"),console.error("[claude-recall-mcp] MCP thread writes DISABLED (read-only)");for(let s of Lt)e.registerPrompt(s.name,{title:s.title,description:s.description,argsSchema:s.argsSchema},async n=>({messages:[{role:"user",content:{type:"text",text:s.build(n)}}]}));return e}async function ea(){let e=Zo(),t=new Go;await e.connect(t);let s=async()=>{try{await e.close()}catch{}St(),process.exit(0)};process.on("SIGINT",s),process.on("SIGTERM",s)}var ta=(()=>{try{let e=process.argv[1];return e?import.meta.url===new URL(`file://${e}`).href:!1}catch{return!1}})();ta&&ea().catch(e=>{console.error("[claude-recall-mcp] fatal:",e),process.exit(1)});export{Zo as buildServer};
|