Paste a Slack thread, Linear issue or GitHub PR link and Claude gets its contents as context before it answers

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 105 lines1import type { EngineInterface, Register } from 'claude-code'
2
3type $ = EngineInterface
4type Json = any // composio responses are per-tool; each caller picks the fields it needs
5
6const CONTEXT_CAP = 12_000
7
8async function composio($: $, slug: string, data: unknown): Promise<Json | null> {
9 const bin = `${await $.env.get('HOME')}/.composio/composio`
10 const run = await $.process
11 .run([bin, 'execute', slug, '-d', JSON.stringify(data)], { timeoutMs: 20_000 })
12 .catch(() => null)
13 if (!run || run.exitCode !== 0) return null
14 const out = await Promise.resolve()
15 .then(() => JSON.parse(run.stdout))
16 .catch(() => null)
17 return out?.successful ? out.data : null
18}
19
20const linear = async ($: $, query: string, variables: object): Promise<Json | null> =>
21 (await composio($, 'LINEAR_RUN_QUERY_OR_MUTATION', { query_or_mutation: query, variables }))?.data ?? null
22
23type Link =
24 | { kind: 'slack'; url: string; channel: string; ts: string }
25 | { kind: 'linear'; url: string; id: string }
26 | { kind: 'github'; url: string; owner: string; repo: string; number: number }
27
28function findLinks(text: string): Link[] {
29 const links: Link[] = []
30 for (const m of text.matchAll(/https:\/\/[\w-]+\.slack\.com\/archives\/(\w+)\/p(\d{10})(\d{6})\S*/g)) {
31 const root = m[0].match(/thread_ts=(\d+\.\d+)/)?.[1]
32 links.push({ kind: 'slack', url: m[0], channel: m[1]!, ts: root ?? `${m[2]}.${m[3]}` })
33 }
34 for (const m of text.matchAll(/https:\/\/linear\.app\/[\w-]+\/issue\/([A-Z]+-\d+)\S*/g)) {
35 links.push({ kind: 'linear', url: m[0], id: m[1]! })
36 }
37 for (const m of text.matchAll(/https:\/\/github\.com\/([\w.-]+)\/([\w.-]+)\/pull\/(\d+)\S*/g)) {
38 links.push({ kind: 'github', url: m[0], owner: m[1]!, repo: m[2]!, number: Number(m[3]) })
39 }
40 // ponytail: first 5 links only, so a pasted wall of URLs can't stall the prompt
41 return links.filter((l, i) => links.findIndex(o => o.url === l.url) === i).slice(0, 5)
42}
43
44async function fetchLink($: $, link: Link): Promise<unknown> {
45 switch (link.kind) {
46 case 'slack': {
47 const data = await composio($, 'SLACK_FETCH_MESSAGE_THREAD_FROM_A_CONVERSATION', {
48 channel: link.channel,
49 ts: link.ts,
50 limit: 100,
51 })
52 return data?.messages?.map((m: Json) => ({ user: m.user ?? m.username, ts: m.ts, text: m.text }))
53 }
54 case 'linear':
55 return (
56 await linear(
57 $,
58 'query($id:String!){ issue(id:$id){ identifier title url state{name} assignee{name} priorityLabel description comments(first:30){nodes{user{name} body createdAt}} } }',
59 { id: link.id },
60 )
61 )?.issue
62 case 'github': {
63 const pr = await composio($, 'GITHUB_GET_A_PULL_REQUEST', {
64 owner: link.owner,
65 repo: link.repo,
66 pull_number: link.number,
67 })
68 return (
69 pr && {
70 title: pr.title,
71 state: pr.merged ? 'merged' : pr.state,
72 draft: pr.draft,
73 author: pr.user?.login,
74 base: pr.base?.ref,
75 head: pr.head?.ref,
76 size: `+${pr.additions} -${pr.deletions} in ${pr.changed_files} files`,
77 reviewers: pr.requested_reviewers?.map((r: Json) => r.login),
78 body: pr.body,
79 }
80 )
81 }
82 }
83}
84
85export const register: Register = on => {
86 on('prompt.submit', async ($, e, next) => {
87 const links = findLinks(e.text)
88 if (links.length === 0) return next(e)
89
90 $.ui.status(`prefetching ${links.length} link${links.length > 1 ? 's' : ''}…`)
91 const fetched = await Promise.all(links.map(l => fetchLink($, l).catch(() => null)))
92 $.ui.status(undefined)
93
94 const blocks = links.flatMap((l, i) =>
95 fetched[i] == null
96 ? []
97 : [`Prefetched via Composio (${l.kind}) ${l.url}:\n${JSON.stringify(fetched[i], null, 1).slice(0, CONTEXT_CAP)}`],
98 )
99 const missed = links.length - blocks.length
100 $.ui.toast(`Prefetched ${blocks.length}/${links.length} link${links.length > 1 ? 's' : ''}${missed ? `, ${missed} failed` : ''}`)
101
102 return next({ ...e, context: [...(e.context ?? []), ...blocks] })
103 })
104}
105