Compact the conversation once after 40 idle minutes, while the 1h prompt cache is still warm.

Personal Claude Code plugins.
/plugin marketplace add itkrt2y/claude-plugins
Compacts the main conversation once after 40 idle minutes, while the 1h prompt cache is still warm, so the next turn after a long break starts from a small context instead of re-caching the whole one.
Function hooks are early access: set CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1, for example in the env block of ~/.claude/settings.json.
/plugin install idle-compact@itkrt2y-claude-plugins
Inspired by takahirom's idle-compact.
Respond to pull request review threads with the respond-reviews skill. One script, scripts/review-threads.sh, lists unresolved threads as JSON, replies inside a thread, resolves threads and edits posted replies, so Claude does not hand-write GraphQL queries each time. Requires gh and jq.
/plugin install review-threads@itkrt2y-claude-plugins
The rebase skill tries git rebase in the main conversation and hands the rebase to a subagent only when it stops on conflicts, so conflict diffs and git output stay out of the main context. The subagent never pushes.
/plugin install rebase@itkrt2y-claude-plugins
The circleci-triage skill fetches failed CircleCI tests with the circleci CLI in a subagent, then tells flaky failures from regressions by reading the actual values and reproducing locally.
/plugin install circleci-triage@itkrt2y-claude-plugins
~/.claude/projects/<project>/RULES.md as always-on rules for that project. Claude Code's auto memory loads only the first 200 lines or 25KB of memory/MEMORY.md; turn on the inject_memory_index option to inject the whole index as well.memory-index-compact skill shortens MEMORY.md when it nears the limit while keeping every link and every status of unfinished work./plugin install project-memory@itkrt2y-claude-plugins
Shows Claude Code notifications on the GNOME desktop, titled with the session name set by /rename, else the git branch, else the directory name, so you can tell parallel sessions apart.
Supported environment: a local GNOME session on Linux. Tested with GNOME Shell 50 (Wayland) on Fedora 44. Other desktops, macOS, WSL, and SSH or headless sessions without a notification server are not supported.
Requirements (Fedora package names in parentheses):
notify-send (libnotify)jq (jq)tac and timeout from GNU coreutils (coreutils)canberra-gtk-play (libcanberra-gtk3) and the freedesktop sound theme (sound-theme-freedesktop) to play a sound. Without them, notifications are silent./plugin install desktop-notify@itkrt2y-claude-plugins
Lists the pull requests created in the current session in a band above the prompt, each linked as owner/repo#number.
gh pr create succeeds in the Bash tool, or when an MCP tool named create_pull_request (such as the GitHub MCP server's) succeeds. The PR URL is read from the tool's output, so GitHub Enterprise hosts work too./session-prs or the Hide button hides the band; the next PR created shows it again.Every PR it picks up also goes into an index kept in the plugin's store and shared by all sessions, with the session ID, working directory and branch of the session that recorded it first. /session-prs list lists that index from any session, wherever each session's worktree lives:
gh and the claude --resume <session-id> command for the session that created it.claude --from-pr <number>./session-prs prune drops them from the index.The states come from gh pr list --author @me, so /session-prs list needs gh signed in to each host. A session created before the plugin was installed joins the index when it is resumed. Claude Code deletes transcripts after cleanupPeriodDays (30 days by default), so raise it if PRs stay open longer than that.
Function hooks are early access: set CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1, for example in the env block of ~/.claude/settings.json.
/plugin install session-prs@itkrt2y-claude-plugins
claude plugin validate .
for p in plugins/*/; do claude plugin validate "$p"; done
for p in plugins/idle-compact plugins/session-prs; do CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 claude plugin test "$p"; donehooks/idle-compact.ts 112 lines1import type { EngineInterface, Register, Timer } from 'claude-code'
2
3// Compact the main conversation once after it has been idle for 40 minutes,
4// while the 1h prompt cache is still warm.
5export const IDLE_MS = 40 * 60 * 1000
6// A timer that fires this late (for example after the machine slept) may find
7// the cache already cold, so compacting would re-cache the whole context.
8export const LATEST_MS = 55 * 60 * 1000
9
10type Armed = {
11 generation: number
12 timer: Timer
13 sessionId: string
14 armedAt: number
15}
16
17type State = {
18 generation: number
19 armed: Armed | null
20 currentTurnId: string | null
21 isCompacting: boolean
22}
23
24async function debug($: EngineInterface, text: string) {
25 try {
26 await $.ui.log(`idle-compact: ${text}`, { to: 'debug' })
27 } catch {}
28}
29
30async function notice($: EngineInterface, text: string) {
31 try {
32 await $.ui.log(`idle-compact: ${text}`, { to: 'transcript' })
33 } catch {}
34}
35
36function cancel(state: State) {
37 state.generation++
38 state.armed?.timer.cancel()
39 state.armed = null
40}
41
42async function arm($: EngineInterface, state: State) {
43 cancel(state)
44 const generation = state.generation
45 const armedAt = await $.clock.now()
46 const sessionId = await $.session.id()
47 // A turn may have started while awaiting.
48 if (state.generation !== generation) return
49 const timer = $.clock.after(IDLE_MS, () => void fire($, state, generation))
50 state.armed = { generation, timer, sessionId, armedAt }
51 await debug($, `armed at ${new Date(armedAt).toISOString()}`)
52}
53
54async function fire($: EngineInterface, state: State, generation: number) {
55 const armed = state.armed
56 if (armed === null || armed.generation !== generation || state.isCompacting) return
57 state.armed = null
58 try {
59 const elapsed = (await $.clock.now()) - armed.armedAt
60 const minutes = (elapsed / 60000).toFixed(1)
61 if (elapsed < IDLE_MS || elapsed >= LATEST_MS) {
62 await debug($, `fired after ${minutes} min: outside the window, skipped`)
63 return
64 }
65 if ((await $.session.id()) !== armed.sessionId) {
66 await debug($, `fired after ${minutes} min: session changed, skipped`)
67 return
68 }
69 if (state.generation !== generation) return
70 state.isCompacting = true
71 await $.session.compact()
72 await notice($, `compacted after ${minutes} idle minutes`)
73 } catch (error) {
74 // Refused while a turn runs, with DISABLE_COMPACT, or in a headless host: not retried.
75 await debug($, `compaction failed: ${error instanceof Error ? error.message : String(error)}`)
76 } finally {
77 state.isCompacting = false
78 }
79}
80
81export const register: Register = (on) => {
82 const state: State = { generation: 0, armed: null, currentTurnId: null, isCompacting: false }
83
84 on('turn.start', ($, e, next) => {
85 cancel(state)
86 state.currentTurnId = e.turnId
87 return next(e)
88 })
89
90 on('turn.complete', async ($, e, next) => {
91 const result = await next(e)
92 // Only a main-loop turn the person sent, answered normally, arms the timer.
93 if (e.agentId !== undefined || e.reason !== 'answer' || state.isCompacting) return result
94 if (e.turnId !== state.currentTurnId) return result
95 state.currentTurnId = null
96 await arm($, state)
97 return result
98 })
99
100 // A manual or automatic compaction leaves nothing for the pending timer to do.
101 on('session.compact', ($, e, next) => {
102 if (e.agentId === undefined && (e.trigger === 'manual' || e.trigger === 'auto')) cancel(state)
103 return next(e)
104 })
105
106 on('session.end', ($, e, next) => {
107 cancel(state)
108 state.currentTurnId = null
109 return next(e)
110 })
111}
112