/find-sess <what you remember>: find a past session in this repo with a Haiku call, then resume it

Searches digests of recent local sessions for the current project, using Python 3 and Haiku ranking. A numbered result resumes the chosen session. A later unsuccessful search clears previous matches. Search is bounded and may miss older history.
claude plugin install find-sess --marketplace sruthik27/claude-foundry
Requires Claude Code 2.1.296+. Reload plugins or start a new session after installation.
/find-sess the session about upload retries
/find-sess 2
MIT licensed. Maintained by Sruthik Issac.
hooks/register.ts 49 lines1import type { Register } from 'claude-code'
2import type { Digest } from './pick.js'
3import { parsePicks, pickPrompt } from './pick.js'
4
5/** The last search's matches, so `/find-sess 2` can resume the second. */
6let found: Digest[] = []
7let generation = 0
8
9export const register: Register = on => {
10 on('session.start', async ($, e, next) => {
11 const r = await next(e)
12 found = []; generation++
13 await $.command.register({ name: 'find-sess', description: 'Find a past session in this repo: /find-sess <what you remember> · /find-sess <n> resumes match n', argumentHint: '<what you remember> | <n>' })
14 return r
15 })
16
17 on('classic.SessionStart', async (_$, e, next) => { found = []; generation++; return next(e) })
18
19 on('command.run', { command: 'find-sess' }, async ($, e) => {
20 const q = e.args.trim()
21 if (!q) return { text: 'Usage: /find-sess <what you remember>, then /find-sess <n> to open a match.' }
22
23 if (/^[1-5]$/.test(q)) {
24 const d = found[Number(q) - 1]
25 if (!d) return { text: 'No such match. Search first: /find-sess <what you remember>' }
26 // A command hook can't run another command while it runs; queue it for when it's done.
27 $.clock.after(50, () => { void $.command.run({ command: 'resume', args: d.id }).catch(() => undefined) })
28 return { text: `Opening "${d.title}"… If it doesn't switch: claude --resume ${d.id}` }
29 }
30
31 found = []
32 const search = ++generation
33 const home = (await $.env.get('HOME')) ?? ''
34 const dir = `${home}/.claude/projects/${(await $.session.cwd()).replace(/[^A-Za-z0-9]/g, '-')}`
35 const run = await $.process.run(['python3', `${$.plugin.root}/hooks/digest.py`, dir, await $.session.id(), '100', q], { timeoutMs: 180000 })
36 let ds: Digest[] = []
37 try { ds = JSON.parse(run.stdout) as Digest[] } catch { /* below */ }
38 if (ds.length === 0) return { text: `No past sessions found for this repo (${dir}).` }
39
40 const reply = await $.model.complete({ model: 'haiku', effort: 'low', maxTokens: 400, timeoutMs: 60000, prompt: pickPrompt(q, ds) })
41 const picks = reply.isAnswered ? parsePicks(reply.text, ds.length) : []
42 if (picks.length === 0) return { text: `No match in the last ${ds.length} sessions for "${q}".` }
43 if (search !== generation) return { text: 'Search superseded by a newer search or session switch.' }
44 found = picks.map(p => ds[p.n - 1]!)
45 const lines = picks.map((p, i) => { const d = ds[p.n - 1]!; return ` ${i + 1}. ${d.title} (${d.date}${d.branch ? ` · ${d.branch}` : ''})\n ${p.why}` })
46 return { text: `Matches in the last ${ds.length} sessions:\n${lines.join('\n')}\n\nOpen one: /find-sess <number>` }
47 })
48}
49hooks/pick.ts 29 lines1export type Digest = { id: string; title: string; branch: string | null; date: string; prompts: string[]; edits: string[] }
2export type Pick = { n: number; why: string }
3
4export function pickPrompt(query: string, ds: readonly Digest[]): string {
5 const list = ds.map((d, i) =>
6 `[${i + 1}] ${d.title} | ${d.date}${d.branch ? ` | ${d.branch}` : ''}\n asked: ${d.prompts.join(' / ')}${d.edits.length ? `\n edited: ${d.edits.join(', ')}` : ''}`).join('\n')
7 return `Find which past coding sessions match what the user remembers.
8
9They remember: ${query}
10
11Sessions (newest first):
12${list}
13
14Answer with up to 5 lines, best match first, each exactly:
15<number> | <one short reason it matches>
16Only include real matches. If none match, answer: NONE`
17}
18
19export function parsePicks(reply: string, count: number): Pick[] {
20 const out: Pick[] = []
21 for (const line of reply.split('\n')) {
22 const m = /^\s*\[?(\d+)\]?\s*\|\s*(.+)$/.exec(line)
23 const n = Number(m?.[1])
24 if (!m || n < 1 || n > count || out.some(p => p.n === n)) continue
25 out.push({ n, why: m[2]!.trim().slice(0, 120) })
26 }
27 return out.slice(0, 5)
28}
29