Claude Mod Manager: fetches the cmod program for every mod, runs the uninstall step of each mod Claude Code removes, and carries calls from one mod to another.

<img src=".claude-plugin/icon.png" alt="Claude Mod Manager" width="160">
<h1 align="center">Claude Mod Manager</h1>
Claude Mod Manager (cmod) installs and builds Claude Code mods. A mod is a Claude Code plugin built with the @cmodjs/core package. It can add hooks, panes, slash commands, tools, and permission rules, and it can run its own install step. cmod installs mods, runs their install and uninstall steps with your consent, and helps you build and publish your own.
In Claude Code, add the mod's marketplace and install the mod:
/plugin marketplace add <owner>/<repo>
/plugin install <mod>@<marketplace>
Claude Code installs the cmod plugin with the mod. cmod then fetches the cmod program and opens the mod's installer pane, and the mod runs its install step once you accept. The pane names each command the mod runs, each key it binds, and each permission it asks for, such as "connect to api.github.com" or "run gh on your computer". It then asks for any setting the mod needs, and walks you through any step only you can do, such as signing in. The mod can reach nothing else through cmod. A progress bar shows each step. A notice tells you when the mod is ready.
/mods lists every mod with the one-line description from its plugin.json, whether Claude Code has it turned off, its settings, its permissions, its keys, and its own pages. Turn a permission off there and the mod loses it before its next call. Turn off and Turn on run cmod disable and cmod enable, and Remove asks once, then runs cmod remove <mod>.
A mod keeps itself up to date. Its setup turns on Claude Code's automatic updates for the marketplace it came from, unless you already chose, and the install question says so. Turn them off under /plugin Marketplaces. An update that changes what the mod runs asks you again before it runs.
When you remove a mod with /plugin uninstall, cmod runs the mod's uninstall step at the next session start or prompt. If Claude Code quits before the uninstall step finishes, the next session runs it again. If the uninstall step fails, a notice names the saved step to fix and the command that runs it again, cmod teardown <mod>. The mod keeps running in a session that is already open until you run /reload-plugins there.
In a terminal, one command does the same, for a mod or any other Claude Code plugin:
cmod install <owner>/<repo>
A plugin that is not a mod installs through Claude Code alone, with no consent question. cmod update, cmod enable, cmod disable, and cmod remove work on any plugin the same way. The cmod plugin puts cmod in ~/.local/bin the first time it runs. A mod that ships a program, such as trace, runs it by its name in Claude Code even when your system has a command of the same name, and a mod built only for some machines says so before it runs anything. cmod list shows every plugin Claude Code has, and whether cmod set up each mod.
The cmod plugin runs these programs, and sends no data anywhere:
setup/bootstrap.sh, the first time it runs. It runs the cmod launcher from the @cmodjs/cli package Claude Code installs with the plugin. The launcher downloads the cmod program for your machine from the GitHub release of this version at github.com/heyJordanParker/cmod, checks it against the release's SHA256SUMS, keeps it in ~/.local/share/cmod/bin/cmod/<version>/, and links ~/.local/share/cmod/programs/cmod and ~/.local/bin/cmod to it. It leaves a ~/.local/bin/cmod it did not make in place. That download is its only network request.setup/path.sh, at each session start. It writes one line to the file Claude Code reads into every Bash command's environment, export PATH="~/.local/share/cmod/programs:$PATH", so Claude runs each program a mod installs by its name.cmod teardown <mod> --events, when you remove a mod. At each session start and prompt, the plugin reads enabledPlugins from your Claude Code user settings. For each mod that left the list, it runs cmod teardown, which runs the uninstall step you approved when you installed the mod.cmod permission <mod> <name> [value] on|off, when you turn a permission on or off in /mods, cmod option <mod> <key> <value>, when you save a setting there, and cmod enable, cmod disable, and cmod remove with a mod's name, when you turn it on or off or remove it there.A setting you change in /mods goes through cmod option to Claude Code's /config, the same place Claude Code keeps it when you change it there. The cmod plugin never changes Claude Code's settings itself.
It writes files in two places only: the cmod program in ~/.local/share/cmod/ with its links at ~/.local/share/cmod/programs/cmod and ~/.local/bin/cmod, and the list of mods it has seen, which it keeps in Claude Code's own storage for the plugin.
Every mod built with cmod, the cmod plugin included, adds one hook on each Claude Code event a mod can use, and only a mod that decides permissions hooks Claude Code's permission check. So the cmod plugin hooks tool calls, prompts, and what Claude Code draws, and no permission check. On a tool call, the cmod plugin answers nothing of its own: it passes the event on unchanged. It never allows or denies a call, never rewrites a tool's input, and never changes a tool's output. It draws one line above the prompt while it downloads cmod, and it answers a call from one mod to another that names a mod you have not installed.
Your changes to a mod live in its config folders, outside the mod's code. Each mod has two:
~/.claude/cmods/<mod>/ yours, in every project
<project>/.claude/cmods/<mod>/ your team's, committed with the repository
A file in the project folder wins over the same file in yours. CLAUDE_CONFIG_DIR moves ~/.claude, and removing the mod keeps both folders.
A mod's options, such as a token or a branch to watch, are yours to set in /config. To set them for your whole team, commit an options.json in the project folder that lists only the options to change:
{ "branch": "release" }
The project's value wins over yours, and a value your organization set in managed settings wins over both. A secret never goes in the project file. The mod ignores an option it does not have and a value that does not fit, and logs one line naming the file, the option, and the fix. cmod reads the project's file when the mod starts, and again after a /cd.
To replace the text of a Skill a mod ships, put your own file at the same path in a config folder:
~/.claude/cmods/<mod>/skills/<skill>/SKILL.md
Claude reads your text in place of the mod's from the next time the Skill loads, and updates keep it. cmod drops the file's frontmatter, so the mod's own name and description stay. Delete the file to get the mod's text back.
npm i -g @cmodjs/cli
cmod new my-mod
cd my-mod
cmod link
npm i -g @cmodjs/cli puts cmod on PATH, and bun add -g @cmodjs/cli does the same. bunx @cmodjs/cli new my-mod runs it once without installing. cmod new writes the mod and installs its packages. cmod link loads it in every new Claude Code session. The mod lives in src/:
src/
├── mod.tsx defineMod, and the mappings: the pane, the slot renders, the hooks
├── state.ts the starting state, grouped by how long it lasts
├── panes/
│ └── prompts.tsx one definePane per file
└── components/
└── prompt-count.tsx what the pane and the slot render draw with
src/mod.tsx attaches each part to Claude Code with one line:
import { defineMod } from '../node_modules/@cmodjs/core/mod.js'
import { Box } from '../node_modules/@cmodjs/core/ui/elements.js'
import { slots } from '../node_modules/@cmodjs/core/ui/slots.js'
import { PromptCount } from './components/prompt-count.js'
import { promptsPane } from './panes/prompts.js'
import { initialState } from './state.js'
export const myMod = defineMod({
name: 'my-mod',
state: initialState,
setup(mod) {
mod.ui.pane(promptsPane)
mod.ui.render(slots.AbovePrompt, ({ hasSurvey, Default }) =>
hasSurvey ? <Default /> : <Box flexDirection="column"><Default /><PromptCount count={mod.state.session.prompts} /></Box>)
mod.on('UserPromptSubmit', () => {
mod.state.session.prompts += 1
})
},
})
src/state.ts holds the starting state:
export type MyModState = { session: { prompts: number } }
export const initialState: MyModState = { session: { prompts: 0 } }
src/panes/prompts.tsx is the mod's pane:
export const promptsPane = definePane<MyModState>({
id: 'my-mod',
title: 'my-mod',
render: (mod) => <PromptCount count={mod.state.session.prompts} />,
})
src/components/prompt-count.tsx is what the pane and the band above the prompt draw:
export function PromptCount({ count }: { readonly count: number }): RenderElement {
return <Text>{`Prompts this session: ${count}`}</Text>
}
Start claude, and the mod counts every prompt. cmod check checks the layout, the imports, and the lint, validates the mod with Claude Code, type-checks it, and runs its tests. A fresh clone has no .claude-plugin/types/ yet, so cmod check has Claude Code write it before the type check. cmod publish releases the mod on GitHub, and prints the link that lists it in Anthropic's plugin directory.
The mod author docs live in core/docs/ and ship inside @cmodjs/core. So every mod's node_modules/@cmodjs/core/docs/ holds the docs of the version it installed. The .claude/CLAUDE.md that cmod new writes points coding agents there. A project plugin, made with cmod new --project, gets no .claude/CLAUDE.md.
defineMod, setup, and what mod can callmod.on, every event, and what a hook can answertestModcmod command@cmodjs/core on npm, the library every mod imports, with its docs in core/docs/.@cmodjs/cli on npm, the cmod command, which runs the newest cmod program. It also holds the source of the cmod program, which each GitHub release carries for macOS and Linux.MIT. See LICENSE.
hooks/register.ts 19 lines1import type { On, PluginOptions } from 'claude-code'
2import { notInstalled } from '../node_modules/@cmodjs/core/mod.js'
3import { registerMod } from '../node_modules/@cmodjs/core/register.js'
4import { cmodPlugin } from '../src/mod.js'
5import type { Cmod } from '../types/index.js'
6
7export function register(addHook: On, options: PluginOptions): void {
8 addHook('engine.create', async (_$, eventInput, passOn) => {
9 const built = await passOn(eventInput)
10 const cmod: Cmod = {
11 async call(input) {
12 return { deny: notInstalled(input.to) }
13 },
14 }
15 return { ...built, cmod }
16 })
17 registerMod(addHook, cmodPlugin, options)
18}
19src/mod.ts 71 lines1import { slashCommand } from '../node_modules/@cmodjs/core/jobs/slash-command.js'
2import { defineMod, longestMs, messageOf } from '../node_modules/@cmodjs/core/mod.js'
3import { parseEvent } from '../node_modules/@cmodjs/core/records.js'
4import { modsMemory, modsPanel, type ModsState } from './mods.js'
5
6export type CmodPluginState = { readonly global: { installedPlugins: readonly string[] | null } } & ModsState
7
8const panels = new WeakMap<object, ReturnType<typeof modsPanel<CmodPluginState>>>()
9
10export const cmodPlugin = defineMod({
11 name: 'cmod',
12 state: { global: { installedPlugins: null } as CmodPluginState['global'], memory: modsMemory },
13
14 api: {
15 async openSettings(input: { readonly mod: string; readonly page?: string }, mod) {
16 const panel = panels.get(mod)
17 if (panel === undefined) throw new Error('cmod has not started its /mods panel yet.')
18 await panel.open(input.mod, input.page)
19 return null
20 },
21 },
22
23 setup(mod) {
24 const triedRemovals = new Set<string>()
25 const mods = modsPanel(mod)
26 panels.set(mod, mods)
27
28 mod.use(
29 slashCommand({
30 name: 'mods',
31 description: 'See your mods, turn them off or remove them, and change their options, permissions, keys, and pages',
32 argumentHint: '[mod] [page]',
33 immediate: true,
34 async reply({ positionals }) {
35 const [name, page] = positionals
36 await mods.open(name, page)
37 },
38 }),
39 )
40
41 const tearDown = async (name: string) => {
42 const { exitCode, stdout, stderr } = await mod.process.run(['cmod', 'teardown', name, '--events'], { timeoutMs: longestMs })
43 const outcome = parseEvent(stdout.trim().split('\n').at(-1) ?? '')
44 const isAnswered = outcome.kind === 'done' || outcome.kind === 'missing' || outcome.kind === 'failed'
45 if (isAnswered) mod.state.global.installedPlugins = (mod.state.global.installedPlugins ?? []).filter((installed) => installed !== name)
46 if (outcome.kind === 'done') mod.ui.toast(`${name} is uninstalled. A session that still runs it stops after /reload-plugins.`)
47 else if (outcome.kind === 'failed') mod.ui.toast(outcome.message)
48 else if (exitCode !== 0) mod.ui.toast(`cmod teardown ${name} exited ${exitCode}: ${stderr.trim().split('\n').at(-1)}`)
49 }
50
51 const watchRemovals = async () => {
52 const enabledPlugins = (await mod.claude.settings.read({ source: 'user' }))['enabledPlugins'] as Record<string, unknown> | undefined
53 const current = [...new Set(Object.keys(enabledPlugins ?? {}).map((key) => key.replace(/@[^@]*$/, '')))]
54 const previous = mod.state.global.installedPlugins
55 const removed = (previous ?? []).filter((name) => !current.includes(name))
56 const installed = [...current, ...removed]
57 if (previous === null || previous.length !== installed.length || !installed.every((name) => previous.includes(name))) mod.state.global.installedPlugins = installed
58 for (const name of removed) {
59 if (triedRemovals.has(name)) continue
60 triedRemovals.add(name)
61 void tearDown(name).catch((error: unknown) => {
62 mod.ui.toast(`cmod teardown ${name} did not run: ${messageOf(error)}`)
63 })
64 }
65 }
66
67 mod.on('SessionStart', watchRemovals)
68 mod.on('UserPromptSubmit', watchRemovals)
69 },
70})
71src/mods.ts 247 lines1import type { ConfigRow, RenderElement } from 'claude-code'
2import { longestMs, messageOf, type Mod } from '../node_modules/@cmodjs/core/mod.js'
3import { consentPath, finishStepsMethod, isObject, openPageMethod, parseConsent, pendingStepsMethod, permissionWords, readRecord, readSteps, settingsPagesMethod, storeFolder } from '../node_modules/@cmodjs/core/records.js'
4import { Tabs } from '../node_modules/@cmodjs/core/ui/components.js'
5import { definePane } from '../node_modules/@cmodjs/core/ui/define-pane.js'
6import { Box, Button, Input, Text } from '../node_modules/@cmodjs/core/ui/elements.js'
7
8export type ModsState = { readonly memory: { selected: string | null; tab: Tab; notice: string | null; removing: string | null; views: readonly ModView[] } }
9
10export const modsMemory: ModsState['memory'] = { selected: null, tab: 'options', notice: null, removing: null, views: [] }
11
12type Tab = 'options' | 'permissions' | 'keys' | 'pages'
13
14type Permission = { readonly item: string; readonly isOn: boolean }
15
16export type ModView = {
17 readonly name: string
18 readonly version: string
19 readonly description: string | undefined
20 readonly isOn: boolean
21 readonly permissions: readonly Permission[]
22 readonly keys: readonly (readonly [string, string])[]
23 readonly options: readonly ConfigRow[]
24 readonly pages: readonly { readonly id: string; readonly title: string }[]
25 readonly pendingSteps: readonly string[]
26}
27
28const tabs: readonly { readonly key: Tab; readonly label: string }[] = [
29 { key: 'options', label: 'Options' },
30 { key: 'permissions', label: 'Permissions' },
31 { key: 'keys', label: 'Keys' },
32 { key: 'pages', label: 'Pages' },
33]
34
35export function modsPanel<State extends ModsState>(mod: Mod<State>) {
36 const load = async (): Promise<readonly ModView[]> => {
37 const store = storeFolder({ HOME: await mod.claude.env.home(), XDG_DATA_HOME: await mod.claude.env.dataHome() })
38 const consent = (await mod.fs.exists(consentPath(store))) ? parseConsent(JSON.parse(await mod.fs.read(consentPath(store))), consentPath(store)) : {}
39 const rows = await mod.claude.config.list()
40 const enabledPlugins = (await mod.claude.settings.read())['enabledPlugins']
41 const turnedOff = Object.entries(isObject(enabledPlugins) ? enabledPlugins : {})
42 .filter(([, isEnabled]) => isEnabled === false)
43 .map(([id]) => id.replace(/@[^@]*$/, ''))
44 const names = (await mod.fs.exists(`${store}/records`)) ? (await mod.fs.list(`${store}/records`)).filter((entry) => entry.name.endsWith('.json')).map((entry) => entry.name.slice(0, -'.json'.length)) : []
45 const read = (path: string) => mod.fs.read(path).catch(() => undefined)
46 const records = (await Promise.all(names.map((name) => readRecord(read, store, name)))).filter((record) => record !== undefined)
47 const views = await Promise.all(
48 records.map(async (record): Promise<ModView> => {
49 const manifest: unknown = await mod.fs.read(`${record.root}/package.json`).then(JSON.parse, () => undefined)
50 const plugin: unknown = await mod.fs.read(`${record.root}/.claude-plugin/plugin.json`).then(JSON.parse, () => undefined)
51 const declared = (isObject(manifest) ? readSteps(manifest)?.permissions : undefined) ?? []
52 const granted = consent[record.name] ?? []
53 const ask = async <Value>(method: string): Promise<readonly Value[]> => {
54 const answer = await mod.claude.cmod.call({ to: record.name, method, input: null }).catch(() => undefined)
55 return isObject(answer) && Array.isArray(answer['value']) ? (answer['value'] as Value[]) : []
56 }
57 const pages = await ask<ModView['pages'][number]>(settingsPagesMethod)
58 const pendingSteps = await ask<string>(pendingStepsMethod)
59 return {
60 name: record.name,
61 version: record.version,
62 description: isObject(plugin) && typeof plugin['description'] === 'string' ? plugin['description'] : undefined,
63 isOn: !turnedOff.includes(record.name),
64 permissions: declared.map((item) => ({ item, isOn: granted.includes(item) })),
65 keys: Object.entries(record.keys).map(([key, command]) => [key, `/${command}`] as const),
66 options: rows.filter((row) => row.key.startsWith(`${record.name}.`)),
67 pages,
68 pendingSteps,
69 }
70 }),
71 )
72 return views.sort((left, right) => left.name.localeCompare(right.name))
73 }
74
75 const refresh = async () => {
76 try {
77 mod.state.memory.views = await load()
78 } catch (error) {
79 mod.state.memory.notice = `The mods did not load: ${messageOf(error)}`
80 }
81 }
82
83 const act = (task: () => Promise<string | undefined>) =>
84 void task()
85 .then((notice) => {
86 mod.state.memory.notice = notice ?? null
87 return refresh()
88 })
89 .catch((error: unknown) => void (mod.state.memory.notice = messageOf(error)))
90
91 const toggle = (name: string, { item, isOn }: Permission) =>
92 act(async () => {
93 const split = item.indexOf(':')
94 const words = split === -1 ? [item] : [item.slice(0, split), item.slice(split + 1)]
95 const { exitCode, stdout, stderr } = await mod.process.run(['cmod', 'permission', name, ...words, isOn ? 'off' : 'on'])
96 return exitCode === 0 ? stdout.trim() : stderr.trim()
97 })
98
99 const remove = (name: string) =>
100 act(async () => {
101 mod.state.memory.removing = null
102 mod.state.memory.notice = `Removing ${name}…`
103 const { exitCode, stderr } = await mod.process.run(['cmod', 'remove', name], { timeoutMs: longestMs })
104 if (exitCode === 0) return `Removed ${name}. This session stops running it after /reload-plugins.`
105 return `cmod remove ${name} exited ${exitCode}: ${stderr.trim().split('\n').at(-1) ?? ''}`
106 })
107
108 const turn = (name: string, isOn: boolean) =>
109 act(async () => {
110 const action = isOn ? 'enable' : 'disable'
111 const { exitCode, stderr } = await mod.process.run(['cmod', action, name])
112 if (exitCode === 0) return `Turned ${name} ${isOn ? 'on' : 'off'}. This session follows after /reload-plugins.`
113 return `cmod ${action} ${name} exited ${exitCode}: ${stderr.trim().split('\n').at(-1) ?? ''}`
114 })
115
116 const setOption = (name: string, row: ConfigRow, text: string) =>
117 act(async () => {
118 const value = text.split(',').map((item) => item.trim()).join(',')
119 const { exitCode, stderr } = await mod.process.run(['cmod', 'option', name, row.key.slice(name.length + 1), value])
120 if (exitCode === 0) return `Saved ${row.label}. ${name} reads it at its next start.`
121 return `cmod option ${name} exited ${exitCode}: ${stderr.trim().split('\n').at(-1) ?? ''}`
122 })
123
124 const drawTab = (view: ModView, tab: Tab): RenderElement => {
125 if (tab === 'options') {
126 if (view.options.length === 0) return Text({ dimColor: true, children: `${view.name} has no options.` })
127 return Box({
128 flexDirection: 'column',
129 gap: 1,
130 children: view.options.map((row) =>
131 Box({
132 flexDirection: 'column',
133 children: [
134 Text({ children: [Text({ bold: true, children: row.label }), row.isLocked ? Text({ dimColor: true, children: ' set by your organization' }) : ''] }),
135 row.isLocked ? Text({ children: String(row.value) }) : Input({ key: `option:${row.key}`, label: '› ', value: Array.isArray(row.value) ? row.value.join(', ') : String(row.value), submitLabel: 'save', onSubmit: (text: string) => setOption(view.name, row, text) }),
136 ],
137 }),
138 ),
139 })
140 }
141 if (tab === 'permissions') {
142 if (view.permissions.length === 0) return Text({ dimColor: true, children: `${view.name} asks for no permissions.` })
143 return Box({ flexDirection: 'column', children: view.permissions.map((permission) => Button({ plain: true, key: `permission:${permission.item}`, label: `${permission.isOn ? '[x]' : '[ ]'} ${permissionWords(permission.item)}`, onPress: () => toggle(view.name, permission) })) })
144 }
145 if (tab === 'keys') {
146 if (view.keys.length === 0) return Text({ dimColor: true, children: `${view.name} binds no keys.` })
147 return Box({ flexDirection: 'column', children: view.keys.map(([key, command]) => Text({ children: `${key.padEnd(12)}${command}` })) })
148 }
149 if (view.pages.length === 0) return Text({ dimColor: true, children: `${view.name} adds no pages.` })
150 return Box({ gap: 2, children: view.pages.map((page) => Button({ key: `page:${page.id}`, label: page.title, onPress: () => act(async () => (await mod.claude.cmod.call({ to: view.name, method: openPageMethod, input: page.id }), undefined)) })) })
151 }
152
153 const pane = definePane<State>({
154 id: 'mods',
155 title: 'Mods',
156 closeOnEscape: true,
157 render(current) {
158 const { selected, tab, notice, removing, views } = current.state.memory
159 const view = views.find((each) => each.name === selected) ?? views[0]
160 if (view === undefined) return Text({ dimColor: true, children: notice ?? 'No mod is set up yet. Run cmod install <owner/repo> to add one.' })
161 const list = Box({
162 flexDirection: 'column',
163 minWidth: 18,
164 children: views.map((each) => {
165 const label = `${each === view ? '▸' : ' '} ${each.name}${each.isOn ? '' : ' (off)'}`
166 return each === view ? Text({ bold: true, children: label }) : Button({ plain: true, key: `mod:${each.name}`, label, onPress: () => void Object.assign(current.state.memory, { selected: each.name, removing: null }) })
167 }),
168 })
169 const actions =
170 view.name === 'cmod'
171 ? []
172 : removing === view.name
173 ? [
174 Box({
175 flexDirection: 'column',
176 children: [
177 Text({ color: 'warning', children: `Remove ${view.name}? Its uninstall step runs, and Claude Code deletes it.` }),
178 Box({
179 gap: 2,
180 children: [
181 Button({ key: 'keep', label: 'Keep', onPress: () => void (current.state.memory.removing = null) }),
182 Button({ key: 'remove-confirmed', label: 'Remove', onPress: () => remove(view.name) }),
183 ],
184 }),
185 ],
186 }),
187 ]
188 : [
189 Box({
190 gap: 2,
191 children: [
192 Button({ key: 'turn', label: view.isOn ? 'Turn off' : 'Turn on', onPress: () => turn(view.name, !view.isOn) }),
193 Button({ key: 'remove', label: 'Remove', onPress: () => void (current.state.memory.removing = view.name) }),
194 ],
195 }),
196 ]
197 const details = Box({
198 flexDirection: 'column',
199 gap: 1,
200 children: [
201 Box({
202 flexDirection: 'column',
203 children: [
204 Text({ children: [Text({ bold: true, children: view.name }), Text({ dimColor: true, children: ` ${view.version}` })] }),
205 ...(view.description === undefined ? [] : [Text({ dimColor: true, children: view.description })]),
206 ...(view.isOn ? [] : [Text({ color: 'warning', children: 'Off: Claude Code does not load it.' })]),
207 ],
208 }),
209 ...actions,
210 ...(view.pendingSteps.length === 0
211 ? []
212 : [
213 Box({
214 gap: 2,
215 children: [
216 Text({ color: 'warning', children: `Needs setup: ${view.pendingSteps.join(', ')}` }),
217 Button({ key: 'finish', label: 'Finish setup', onPress: () => act(async () => (await mod.claude.cmod.call({ to: view.name, method: finishStepsMethod, input: null }), undefined)) }),
218 ],
219 }),
220 ]),
221 Tabs({ tabs, selected: tab, onSelect: (key) => void (current.state.memory.tab = key as Tab), children: drawTab(view, tab) }),
222 ...(notice === null ? [] : [Text({ dimColor: true, children: notice })]),
223 ],
224 })
225 return Box({ gap: 2, children: [list, details] })
226 },
227 })
228
229 const handle = mod.ui.pane(pane)
230
231 return {
232 async open(name?: string, page?: string) {
233 await refresh()
234 if (name !== undefined) {
235 if (!mod.state.memory.views.some((view) => view.name === name)) throw new Error(`${name} is not a mod cmod set up. cmod list shows the mods.`)
236 mod.state.memory.selected = name
237 mod.state.memory.tab = 'options'
238 }
239 if (name !== undefined && page !== undefined) {
240 await mod.claude.cmod.call({ to: name, method: openPageMethod, input: page })
241 return
242 }
243 await handle.open({ focus: true })
244 },
245 }
246}
247types/index.d.ts 11 lines1export type Cmod = {
2 call(input: { to: string; method: string; input: unknown }): Promise<unknown>
3}
4
5declare module 'claude-code' {
6 interface EngineInterface {
7 cmod: Cmod
8 }
9 interface CmodDependencies {}
10}
11