SLOPSHOPPER

composio-pulse

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

newguardcommandtoaststatusprocess
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · composio-pulse
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /pulse ⎿ composio-pulse: No composio calls recorded yet. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

claude-composio-mods

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/.

  • ticket-band: Shows the Linear ticket for the current branch above the prompt, with one-press Done once every PR has merged.
  • review-chaser: /reviews lists your open PRs waiting on reviewers, with a Slack nudge button per reviewer.
  • snippet: /snippet drafts your daily Slack update from the last 24h of Linear + GitHub and posts it on one press.
  • composio-daily: /standup pane, looks up failing-test errors in Datadog ("seen before"), and sets your Slack status while the agent is busy.
  • link-prefetch: Paste a Slack thread, Linear issue or GitHub PR link and Claude gets its contents as context before it answers.
  • composio-pulse: Times every composio CLI call into a status-line sparkline (/pulse for details) and blocks re-linking a toolkit that's already connected.
  • review-lint: When an edit adds a cache or a brittle test assertion, hands Claude a cache-review checklist or rules against implementation-coupled tests.

Install

/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>.

Requirements

  • The Composio CLI, installed at ~/.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.

Settings

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.

Source 1 files
hooks/register.tsx 119 lines
1import 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