Lets a session read how full its context window is (tokens, window, percentage, and whether auto-compact is on), to decide at a break between two pieces of…

Lets a session read how full its context window is, so at the break between two pieces of work it can decide whether to compact now, rather than have auto-compact run in the middle of the next piece. Part of claude-code-self-tools.
context_usage (Claude sees mcp__self-context__context_usage), with no arguments. It answers with one line, such as:
Context: 123,456 of 200,000 tokens (62%). Auto-compact is on. To compact at this break, call compact_after_turn.
/config menu's autoCompact row.compact_after_turn is available./plugin marketplace add moefingers/claude-code-self-tools
/plugin install self-context@claude-code-self-toolshooks/register.ts 74 lines1// A session reads how full its context window is, to decide at the break between two pieces of
2// work whether to compact now rather than let auto-compact run in the middle of the next one.
3// Read-only. The figures are the main conversation's, the ones the status line shows: the API has
4// no reading of a subagent's own window, so a subagent is refused rather than handed figures that
5// are not its own.
6import type { EngineInterface, Register } from 'claude-code'
7
8/** The compaction tool of the self-compact plugin, named when it is installed. */
9const COMPACT_TOOL = 'mcp__self-compact__compact_after_turn'
10
11const DESCRIPTION = [
12 "Reports how full this conversation's context window is: tokens used, the window's size, the percentage, and whether auto-compact is on.",
13 'Call it at the break between two pieces of work, to decide whether to compact now, before the next piece starts, rather than have auto-compact run in the middle of it.',
14 'If the compact_after_turn tool is available, that is how to compact at the break.',
15 'Not needed mid-piece or on every turn. Read-only: it changes nothing.',
16].join(' ')
17
18/** A whole number with thousands separators, written out: the module has no locale to ask. */
19const grouped = (n: number): string => String(Math.round(n)).replace(/\B(?=(\d{3})+(?!\d))/g, ',')
20
21/** Whether auto-compact is on, as the /config menu's row says; undefined when there is no such row. */
22async function autoCompact($: EngineInterface): Promise<boolean | undefined> {
23 try {
24 const row = (await $.config.list()).find(r => r.key === 'autoCompact')
25 return typeof row?.value === 'boolean' ? row.value : undefined
26 } catch {
27 return undefined
28 }
29}
30
31/** Whether the self-compact tool is one the model can call now. */
32async function canCompact($: EngineInterface): Promise<boolean> {
33 try {
34 return (await $.tool.list()).some(t => t.name === COMPACT_TOOL)
35 } catch {
36 return false
37 }
38}
39
40async function reading($: EngineInterface): Promise<string> {
41 const [{ context }, auto, compact] = await Promise.all([$.session.usage(), autoCompact($), canCompact($)])
42 const window = `${grouped(context.window)} tokens`
43 const used =
44 context.tokens === undefined
45 ? `Context: no reading yet. The window holds ${window}; it is measured from the model's next response, so a new or just-compacted conversation has none.`
46 : `Context: ${grouped(context.tokens)} of ${window} (${context.percent ?? Math.round((context.tokens * 100) / context.window)}%).`
47 const autoLine =
48 auto === undefined ? 'Auto-compact: not listed in /config.' : `Auto-compact is ${auto ? 'on' : 'off'}.`
49 const how = compact ? ' To compact at this break, call compact_after_turn.' : ''
50 return `${used} ${autoLine}${how}`
51}
52
53export const register: Register = on => {
54 on('session.start', async ($, e, next) => {
55 const started = await next(e)
56 await $.tool.register({
57 name: 'context_usage',
58 description: DESCRIPTION,
59 inputSchema: { type: 'object', properties: {}, additionalProperties: false },
60 isDeferred: false,
61 })
62 return started
63 })
64
65 on('tool.call', { tool: 'mcp__self-context__context_usage' }, async ($, e) => {
66 if (e.agentId !== undefined) {
67 return {
68 deny: "A subagent has no reading of its own window: these figures are the main conversation's. A subagent compacts on its own near its limit; to close a piece cleanly, finish it and return its result.",
69 }
70 }
71 return { result: await reading($) }
72 }).catch(($, e, next) => (next.called ? next(e) : { deny: 'self-context: reading the context window failed.' }))
73}
74