Times every composio CLI call into a status-line sparkline against a latency budget, and blocks re-linking a connected toolkit

Seven Claude Code mods (function-hook plugins) that wire your terminal into Slack, Linear, GitHub and Datadog through the Composio CLI.
Demo: video/out.mp4. The video is built with fframes; its source is in video/.
/reviews lists your open PRs waiting on reviewers, with a Slack nudge button per reviewer./snippet drafts your daily Slack update from the last 24h of Linear + GitHub and posts it on one press./standup pane, looks up failing-test errors in Datadog ("seen before"), and sets your Slack status while the agent is busy.composio CLI call into a status-line sparkline (/pulse for details) and blocks re-linking a toolkit that's already connected./plugin marketplace add tridha643/claude-composio-mods
/plugin install ticket-band@claude-composio-mods
Install whichever mods you want the same way. To hack on them instead, clone the repo and run claude --plugin-dir plugins/<mod>.
~/.composio/composio and logged in, with the toolkits each mod uses connected (Slack, Linear, GitHub, Datadog).gh, logged in, for review-chaser and snippet.review-chaser and snippet search PRs in the ComposioHQ org; change the query in hooks/register.tsx for your own org.Set these from the /plugin config menu:
composio-daily → standup_channel: the Slack channel the standup Post button sends to.snippet → channel: the Slack channel /snippet posts to.ticket-band → team_keys: comma-separated Linear team keys to look for in branch names. Empty matches any KEY-123.composio-pulse → search_budget_ms: marks the median search time ✓ or ✗ against this budget. 0 hides the mark.An empty channel hides that Post button.
hooks/register.tsx 119 lines1import type { EngineInterface, Register } from 'claude-code'
2
3let budget = 0 // userConfig.search_budget_ms; 0 hides the budget mark
4const KEEP = 60
5// composio in command position (line start, after ; & | ( ` or a `--` wrapper), not as an argument.
6const CALL = /(?:^|[;&|(`]|\s--|\b(?:do|then|else))\s*(?:\S*\/)?composio\s+(search|execute|run|proxy)(?=\s|$)([^;&|\n]*)/m
7const LINK = /(?:^|[;&|(`]|\s--|\b(?:do|then|else))\s*(?:\S*\/)?composio\s+link\s+([a-z0-9_]+)([^;&|\n]*)/m
8const BARS = '▁▂▃▄▅▆▇█'
9
10type Call = { kind: string; label: string; ms: number; ok: boolean; at: number }
11
12const fmt = (ms: number) => (ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`)
13
14const pct = (values: number[], p: number) => {
15 const sorted = [...values].sort((a, b) => a - b)
16 return sorted[Math.min(sorted.length - 1, Math.floor((p / 100) * sorted.length))] ?? 0
17}
18
19const spark = (values: number[]) => {
20 const max = Math.max(...values, 1)
21 return values.map(v => BARS[Math.min(BARS.length - 1, Math.floor((v / max) * BARS.length))]).join('')
22}
23
24const labelOf = (kind: string, rest: string) =>
25 kind === 'search'
26 ? (/["']([^"']+)["']/.exec(rest)?.[1] ?? rest.trim().split(/\s+/)[0] ?? '').slice(0, 40)
27 : (rest.split(/\s+/).find(token => /^[A-Z][A-Z0-9_]{2,}$/.test(token)) ?? kind)
28
29async function history($: EngineInterface) {
30 return ((await $.store.get('calls')) as Call[] | undefined) ?? []
31}
32
33function showStatus($: EngineInterface, calls: Call[]) {
34 if (calls.length === 0) return $.ui.status(undefined)
35 const searches = calls.filter(c => c.kind === 'search' && c.ok).map(c => c.ms)
36 const execs = calls.filter(c => c.kind !== 'search' && c.ok).map(c => c.ms)
37 const last = calls.at(-1)!
38 const parts = [`composio ${spark(calls.slice(-24).map(c => c.ms))}`]
39 if (searches.length) {
40 const p50 = pct(searches, 50)
41 parts.push(`search p50 ${fmt(p50)}${budget ? ` ${p50 <= budget ? '✓' : '✗'}${budget}` : ''}`)
42 }
43 if (execs.length) parts.push(`exec p50 ${fmt(pct(execs, 50))}`)
44 parts.push(`last ${last.label} ${last.ok ? '✓' : '✗'} ${fmt(last.ms)}`)
45 $.ui.status(parts.join(' │ '))
46}
47
48export const register: Register = (on, options) => {
49 budget = Number(options.search_budget_ms) || 0
50
51 on('session.start', async ($, e, next) => {
52 await $.command.register({ name: 'pulse', description: 'Latency of the composio CLI calls Claude has made, per kind and per call' })
53 showStatus($, await history($))
54 return next(e)
55 })
56
57 on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
58 const link = LINK.exec(e.command)
59 if (link && !/--(alias|list)\b/.test(link[2] ?? '')) {
60 const toolkit = link[1]!
61 const listed = await $.process.run([`${await $.env.get('HOME')}/.composio/composio`, 'connections', 'list'], { timeoutMs: 20_000 }).catch(() => null)
62 const accounts = listed?.exitCode === 0 ? (JSON.parse(listed.stdout)[toolkit] ?? []) : []
63 const active = accounts.find((a: any) => a.status === 'ACTIVE')
64 if (active) {
65 $.ui.toast(`blocked: ${toolkit} is already connected`)
66 return { deny: `composio-pulse: ${toolkit} already has an ACTIVE connection (${active.word_id}); EXPIRED rows beside it are stale. Skip linking and run \`composio execute\` directly. Pass --alias to add a second account on purpose.` }
67 }
68 }
69
70 const call = CALL.exec(e.command)
71 if (!call) return next(e)
72
73 const started = await $.clock.now()
74 const ran = await next(e)
75 const ms = (await $.clock.now()) - started
76 const kind = call[1]!
77 const ok = ran.deny === undefined && !ran.isError && !/"successful":\s*false/.test(ran.text ?? '')
78 const entry: Call = { kind, label: labelOf(kind, call[2] ?? ''), ms, ok, at: Date.now() }
79
80 const calls = [...(await history($)), entry].slice(-KEEP)
81 await $.store.set('calls', calls)
82 showStatus($, calls)
83
84 if (kind === 'search' && ok) {
85 const best = (await $.store.get('best-search')) as number | undefined
86 if (best === undefined || ms < best) {
87 await $.store.set('best-search', ms)
88 if (best !== undefined) $.ui.toast(`🏁 new composio search PB: ${fmt(ms)} (was ${fmt(best)})`)
89 }
90 }
91 return ran
92 })
93
94 on('command.run', { command: 'pulse' }, async $ => {
95 const calls = await history($)
96 if (calls.length === 0) return { text: 'No composio calls recorded yet.' }
97 const kinds = [...new Set(calls.map(c => c.kind))]
98 const summary = kinds.map(kind => {
99 const ms = calls.filter(c => c.kind === kind && c.ok).map(c => c.ms)
100 const failed = calls.filter(c => c.kind === kind && !c.ok).length
101 return `${kind.padEnd(8)} n=${String(ms.length).padEnd(3)} p50 ${fmt(pct(ms, 50)).padEnd(6)} p95 ${fmt(pct(ms, 95)).padEnd(6)}${failed ? ` failed ${failed}` : ''}`
102 })
103 const best = (await $.store.get('best-search')) as number | undefined
104 const recent = calls.slice(-15).reverse().map(c =>
105 `${new Date(c.at).toLocaleTimeString('en-GB', { hour12: false })} ${c.ok ? '✓' : '✗'} ${fmt(c.ms).padStart(6)} ${c.kind.padEnd(8)} ${c.label}`,
106 )
107 return {
108 text: [
109 `composio CLI latency, last ${calls.length} calls (end to end, CLI startup included${budget ? `; search budget ${budget}ms` : ''}${best ? `, PB ${fmt(best)}` : ''})`,
110 spark(calls.map(c => c.ms)),
111 '',
112 ...summary,
113 '',
114 ...recent,
115 ].join('\n'),
116 }
117 })
118}
119