@brimveyn/aimux-config 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 BrimVeyn
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,155 @@
1
+ # @brimveyn/aimux-config
2
+
3
+ TypeScript configuration API for [aimux](https://github.com/BrimVeyn/aimux) — the terminal multiplexer for AI CLIs.
4
+
5
+ Write your keymaps, theme, and backends in a typed TypeScript file. Method-chaining builder inspired by nvim, with a prefix-trie resolver that supports leader keys and multi-key sequences.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ mkdir -p ~/.config/aimux && cd ~/.config/aimux
11
+ bun init -y
12
+ bun add -d @brimveyn/aimux-config
13
+ ```
14
+
15
+ Then create `~/.config/aimux/aimux.config.ts`:
16
+
17
+ ```ts
18
+ import { defineConfig, actions, themes } from '@brimveyn/aimux-config'
19
+
20
+ export default defineConfig({
21
+ theme: themes.extend('tokyo-night', { accent: '#ff9e64' }),
22
+
23
+ keymaps: (k) =>
24
+ k
25
+ .leader('<Space>')
26
+ .timeout(300)
27
+ .mode('navigation', (m) =>
28
+ m
29
+ .map('j', actions.nextTab)
30
+ .map('k', actions.prevTab)
31
+ .map('<leader>g', actions.sessionPicker)
32
+ .group('<leader>t', 'tabs', (g) =>
33
+ g.map('n', actions.newTab).map('r', actions.renameTab).map('x', actions.closeTab)
34
+ )
35
+ ),
36
+ })
37
+ ```
38
+
39
+ ## Key notation
40
+
41
+ | Notation | Matches |
42
+ | ----------------- | ------------------------------ |
43
+ | `j` | Bare character `j` |
44
+ | `J` | Shift+J (uppercase letter) |
45
+ | `<C-n>` | Ctrl+N |
46
+ | `<M-x>` / `<A-x>` | Meta/Alt+X |
47
+ | `<C-M-a>` | Ctrl+Alt+A |
48
+ | `<CR>` | Return/Enter |
49
+ | `<Esc>` | Escape |
50
+ | `<Space>` | Spacebar |
51
+ | `<Tab>` | Tab |
52
+ | `<BS>` | Backspace |
53
+ | `<Up>` `<Down>` | Arrow keys |
54
+ | `<leader>` | Configured leader chord |
55
+ | `dd` | Multi-key sequence (d, then d) |
56
+ | `<leader>tn` | Leader, then t, then n |
57
+
58
+ Ambiguous prefixes (e.g., `d` is bound AND `dd` is bound) are resolved after a configurable timeout (default 300ms).
59
+
60
+ ## Builder API
61
+
62
+ ### Top-level config
63
+
64
+ ```ts
65
+ defineConfig({
66
+ theme?: ThemeId | ThemeDefinition
67
+ keymaps?: (k: KeymapBuilder) => KeymapBuilder
68
+ backends?: Record<string, BackendConfig> // stub for future use
69
+ sidebar?: SidebarConfig // stub
70
+ hooks?: HooksConfig // stub
71
+ snippets?: SnippetDef[] // stub
72
+ })
73
+ ```
74
+
75
+ ### Keymap builder
76
+
77
+ ```ts
78
+ k.leader(keys) // default: '<Space>'
79
+ .timeout(ms) // default: 300
80
+ .mode(id, configure) // define bindings for a mode
81
+ ```
82
+
83
+ ### Mode builder
84
+
85
+ ```ts
86
+ m.map(keys, action) // bind a key/sequence to an action
87
+ .unmap(keys) // remove a default binding
88
+ .group(prefix, name, g) // sugar for leader-prefixed sub-trees
89
+ .passthrough() // for text-input modes: unmatched keys route to text input
90
+ ```
91
+
92
+ ### Groups
93
+
94
+ Groups organize leader-key sub-trees. `.group('<leader>t', 'tabs', g => g.map('n', ...))` is sugar for `.map('<leader>tn', ...)` with a `name` label used by the help modal.
95
+
96
+ ```ts
97
+ .group('<leader>t', 'tabs', (g) => g
98
+ .map('n', actions.newTab)
99
+ .map('r', actions.renameTab)
100
+ .map('x', actions.closeTab))
101
+ ```
102
+
103
+ ## Actions
104
+
105
+ Pre-built actions cover every built-in aimux operation:
106
+
107
+ **Tab control** — `nextTab`, `prevTab`, `newTab`, `renameTab`, `closeTab`, `restartTab`, `moveTab(n)`, `reorderTab(n)`
108
+
109
+ **Modals** — `sessionPicker`, `snippetPicker`, `themePicker`, `helpModal`, `closeModal`
110
+
111
+ **Sidebar / panels** — `toggleSidebar`, `resizeSidebar(n)`, `toggleGitPanel`, `resizeGitPanel(n)`
112
+
113
+ **Layout / splits** — `splitVertical`, `splitHorizontal`, `focusPane('left'|'right'|'up'|'down')`, `resizePane(n, 'horizontal'|'vertical')`, `closePane`
114
+
115
+ **Mode transitions** — `enterInsert`, `enterLayoutMode`, `leaveTerminalInput`, `quit`
116
+
117
+ **Custom actions** — write an `ActionFn` for dynamic logic:
118
+
119
+ ```ts
120
+ .mode('navigation', (m) => m
121
+ .map('gT', (ctx) => {
122
+ const tabId = ctx.state.activeTabId
123
+ if (!tabId) return null
124
+ return {
125
+ actions: [{ type: 'close-active-tab' }],
126
+ effects: [{ type: 'close-tab', tabId }],
127
+ }
128
+ }))
129
+ ```
130
+
131
+ ## Themes
132
+
133
+ ```ts
134
+ import { themes } from '@brimveyn/aimux-config'
135
+
136
+ // Extend a built-in theme
137
+ themes.extend('tokyo-night', {
138
+ accent: '#ff9e64',
139
+ background: '#1a1b26',
140
+ })
141
+
142
+ // Create a custom theme
143
+ themes.create({
144
+ accent: '#...',
145
+ accentAlt: '#...',
146
+ background: '#...',
147
+ // ... all ThemeColors keys required
148
+ })
149
+ ```
150
+
151
+ Built-in themes: `aimux`, `tokyo-night`, `dracula`, `dracula-at-night`, `everforest`, `gruvbox-dark`, `catppuccin-mocha`, `nord`, `solarized-dark`, `one-dark`, `kanagawa`.
152
+
153
+ ## License
154
+
155
+ MIT
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@brimveyn/aimux-config",
3
+ "version": "0.1.0",
4
+ "description": "TypeScript configuration API for aimux — keymaps, themes, and backends with a fluent builder.",
5
+ "keywords": [
6
+ "aimux",
7
+ "config",
8
+ "keybindings",
9
+ "keymap",
10
+ "leader-key",
11
+ "multiplexer",
12
+ "terminal",
13
+ "trie",
14
+ "tui"
15
+ ],
16
+ "homepage": "https://github.com/BrimVeyn/aimux#readme",
17
+ "bugs": {
18
+ "url": "https://github.com/BrimVeyn/aimux/issues"
19
+ },
20
+ "license": "MIT",
21
+ "author": "BrimVeyn",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/BrimVeyn/aimux.git",
25
+ "directory": "packages/aimux-config"
26
+ },
27
+ "files": [
28
+ "src",
29
+ "README.md",
30
+ "LICENSE"
31
+ ],
32
+ "type": "module",
33
+ "exports": {
34
+ ".": "./src/index.ts",
35
+ "./backends": "./src/backends.ts"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public"
39
+ }
40
+ }
package/src/actions.ts ADDED
@@ -0,0 +1,368 @@
1
+ import type { ActionFn, KeyResult, ModeContext } from './types'
2
+
3
+ function r(
4
+ actions: KeyResult['actions'] = [],
5
+ effects: KeyResult['effects'] = [],
6
+ transition?: KeyResult['transition']
7
+ ): KeyResult {
8
+ return transition ? { actions, effects, transition } : { actions, effects }
9
+ }
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Static actions (KeyResult values — no ctx needed)
13
+ // ---------------------------------------------------------------------------
14
+
15
+ export const nextTab: KeyResult = r([{ delta: 1, type: 'move-active-tab' }])
16
+ export const prevTab: KeyResult = r([{ delta: -1, type: 'move-active-tab' }])
17
+
18
+ export const newTab: KeyResult = r([{ type: 'open-new-tab-modal' }], [], 'modal.new-tab')
19
+ export const renameTab: KeyResult = r([{ type: 'open-rename-tab-modal' }], [], 'modal.rename-tab')
20
+ export const sessionPicker: KeyResult = r(
21
+ [{ type: 'open-session-picker' }],
22
+ [],
23
+ 'modal.session-picker'
24
+ )
25
+ export const snippetPicker: KeyResult = r(
26
+ [{ type: 'open-snippet-picker' }],
27
+ [],
28
+ 'modal.snippet-picker'
29
+ )
30
+ export const themePicker: KeyResult = r(
31
+ [{ type: 'open-theme-picker' }],
32
+ [{ action: 'open', type: 'apply-theme' }],
33
+ 'modal.theme-picker'
34
+ )
35
+ export const helpModal: KeyResult = r([{ type: 'open-help-modal' }], [], 'modal.help')
36
+
37
+ export const toggleSidebar: KeyResult = r([{ type: 'toggle-sidebar' }])
38
+ export const toggleGitPanel: KeyResult = r([{ type: 'toggle-git-panel' }])
39
+
40
+ export const splitVertical: KeyResult = r(
41
+ [{ direction: 'vertical', type: 'open-split-picker' }],
42
+ [],
43
+ 'modal.split-picker'
44
+ )
45
+ export const splitHorizontal: KeyResult = r(
46
+ [{ direction: 'horizontal', type: 'open-split-picker' }],
47
+ [],
48
+ 'modal.split-picker'
49
+ )
50
+
51
+ export const enterInsert: ActionFn = (ctx: ModeContext) => {
52
+ if (!ctx.state.activeTabId) return null
53
+ return r([{ focusMode: 'terminal-input', type: 'set-focus-mode' }], [], 'terminal-input')
54
+ }
55
+
56
+ export const closeModal: KeyResult = r([{ type: 'close-modal' }], [], 'navigation')
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Dynamic actions (need ctx at runtime — ActionFn)
60
+ // ---------------------------------------------------------------------------
61
+
62
+ export const closeTab: ActionFn = (ctx: ModeContext) => {
63
+ const tabId = ctx.state.activeTabId
64
+ if (!tabId) return null
65
+ return r([{ type: 'close-active-tab' }], [{ tabId, type: 'close-tab' }])
66
+ }
67
+
68
+ export const restartTab: ActionFn = (ctx: ModeContext) => {
69
+ const tab = ctx.state.tabs.find((t) => t.id === ctx.state.activeTabId)
70
+ if (!tab) return null
71
+ return r([], [{ tab, type: 'restart-tab' }])
72
+ }
73
+
74
+ export const quit: ActionFn = (ctx: ModeContext) => {
75
+ return r([], [{ state: ctx.state, type: 'quit' }])
76
+ }
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // Parameterized factories
80
+ // ---------------------------------------------------------------------------
81
+
82
+ export function moveTab(delta: number): KeyResult {
83
+ return r([{ delta, type: 'move-active-tab' }])
84
+ }
85
+
86
+ export function reorderTab(delta: number): KeyResult {
87
+ return r([{ delta, type: 'reorder-active-tab' }])
88
+ }
89
+
90
+ export function resizeSidebar(delta: number): KeyResult {
91
+ return r([{ delta, type: 'resize-sidebar' }])
92
+ }
93
+
94
+ export function resizeGitPanel(delta: number): KeyResult {
95
+ return r([{ delta, type: 'resize-git-panel' }])
96
+ }
97
+
98
+ export function focusPane(direction: 'left' | 'right' | 'up' | 'down'): KeyResult {
99
+ return r(
100
+ [
101
+ { direction, type: 'focus-pane-direction' },
102
+ { focusMode: 'terminal-input', type: 'set-focus-mode' },
103
+ ],
104
+ [],
105
+ 'terminal-input'
106
+ )
107
+ }
108
+
109
+ export function resizePane(delta: number, axis: 'horizontal' | 'vertical'): ActionFn {
110
+ return (ctx: ModeContext) => {
111
+ const tabId = ctx.state.activeTabId
112
+ if (!tabId) return null
113
+ return r([{ axis, delta, tabId, type: 'resize-pane' }])
114
+ }
115
+ }
116
+
117
+ export function moveModalSelection(delta: number): KeyResult {
118
+ return r([{ delta, type: 'move-modal-selection' }])
119
+ }
120
+
121
+ export function moveModalSelectionWithPreview(
122
+ delta: number,
123
+ effects: KeyResult['effects']
124
+ ): KeyResult {
125
+ return r([{ delta, type: 'move-modal-selection' }], effects)
126
+ }
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // Modal-specific actions
130
+ // ---------------------------------------------------------------------------
131
+
132
+ export const launchSelectedAssistant: KeyResult = r([], [{ type: 'launch-selected-assistant' }])
133
+
134
+ export const beginCommandEdit: KeyResult = r(
135
+ [{ type: 'begin-command-edit' }],
136
+ [],
137
+ 'modal.new-tab.command-edit'
138
+ )
139
+
140
+ export const cancelCommandEdit = (returnTo: KeyResult['transition']): KeyResult =>
141
+ r([{ type: 'cancel-command-edit' }], [], returnTo)
142
+
143
+ export const commitCommandEdit: KeyResult = r(
144
+ [{ type: 'commit-command-edit' }],
145
+ [{ type: 'save-custom-command' }],
146
+ 'modal.new-tab'
147
+ )
148
+
149
+ export const confirmSelectedSession: KeyResult = r([], [{ type: 'confirm-selected-session' }])
150
+
151
+ export const openCreateSessionModal: KeyResult = r(
152
+ [{ type: 'open-create-session-modal' }],
153
+ [],
154
+ 'modal.create-session'
155
+ )
156
+
157
+ export const openRenameSelectedSession: KeyResult = r(
158
+ [],
159
+ [{ type: 'open-rename-selected-session' }]
160
+ )
161
+
162
+ export const deleteSelectedSession: KeyResult = r([], [{ type: 'delete-selected-session' }])
163
+
164
+ export const beginSessionFilter: KeyResult = r(
165
+ [{ type: 'begin-session-filter' }],
166
+ [],
167
+ 'modal.session-picker.filtering'
168
+ )
169
+
170
+ export const openSnippetEditor: KeyResult = r(
171
+ [{ type: 'open-snippet-editor' }],
172
+ [],
173
+ 'modal.snippet-editor'
174
+ )
175
+
176
+ export const editSelectedSnippet: KeyResult = r([], [{ type: 'edit-selected-snippet' }])
177
+ export const deleteSelectedSnippet: KeyResult = r([], [{ type: 'delete-selected-snippet' }])
178
+
179
+ export const pasteSelectedSnippet: KeyResult = r(
180
+ [{ type: 'close-modal' }],
181
+ [{ type: 'paste-selected-snippet' }],
182
+ 'navigation'
183
+ )
184
+
185
+ export const pasteSnippetToGroup: KeyResult = r(
186
+ [{ type: 'close-modal' }],
187
+ [{ type: 'paste-snippet-to-group' }],
188
+ 'navigation'
189
+ )
190
+
191
+ export const beginSnippetFilter: KeyResult = r(
192
+ [{ type: 'begin-snippet-filter' }],
193
+ [],
194
+ 'modal.snippet-picker.filtering'
195
+ )
196
+
197
+ export const confirmSplit: KeyResult = r([], [{ type: 'confirm-split' }])
198
+
199
+ export const restoreTheme: KeyResult = r(
200
+ [{ type: 'close-modal' }],
201
+ [{ action: 'restore', type: 'apply-theme' }],
202
+ 'navigation'
203
+ )
204
+
205
+ export const confirmTheme: KeyResult = r(
206
+ [{ type: 'close-modal' }],
207
+ [{ action: 'confirm', type: 'apply-theme' }],
208
+ 'navigation'
209
+ )
210
+
211
+ export function previewTheme(delta: 1 | -1): KeyResult {
212
+ return r(
213
+ [{ delta, type: 'move-modal-selection' }],
214
+ [{ action: 'preview', delta, type: 'apply-theme' }]
215
+ )
216
+ }
217
+
218
+ export const switchField: KeyResult = r([{ type: 'switch-create-session-field' }])
219
+ export const selectDirectory: KeyResult = r([{ type: 'select-directory' }])
220
+
221
+ export const backToSessionPicker: KeyResult = r(
222
+ [{ type: 'open-session-picker' }],
223
+ [],
224
+ 'modal.session-picker'
225
+ )
226
+
227
+ export const backToSnippetPicker: KeyResult = r(
228
+ [{ type: 'open-snippet-picker' }],
229
+ [],
230
+ 'modal.snippet-picker'
231
+ )
232
+
233
+ export const saveSnippetEditor: KeyResult = r(
234
+ [{ type: 'close-modal' }],
235
+ [{ type: 'save-snippet-editor' }],
236
+ 'navigation'
237
+ )
238
+
239
+ // Layout-specific
240
+ export const exitLayoutToInput: KeyResult = r(
241
+ [{ focusMode: 'terminal-input', type: 'set-focus-mode' }],
242
+ [],
243
+ 'terminal-input'
244
+ )
245
+
246
+ export const exitLayoutToNavigation: ActionFn = (ctx: ModeContext) => {
247
+ const actions: KeyResult['actions'] = [{ focusMode: 'navigation', type: 'set-focus-mode' }]
248
+ if (!ctx.state.sidebar.visible) {
249
+ actions.push({ type: 'toggle-sidebar' })
250
+ }
251
+ return r(actions, [], 'navigation')
252
+ }
253
+
254
+ export const closePane: ActionFn = (ctx: ModeContext) => {
255
+ const tabId = ctx.state.activeTabId
256
+ if (!tabId) return null
257
+ return r(
258
+ [
259
+ { tabId, type: 'close-pane' },
260
+ { focusMode: 'terminal-input', type: 'set-focus-mode' },
261
+ ],
262
+ [{ tabId, type: 'close-tab' }],
263
+ 'terminal-input'
264
+ )
265
+ }
266
+
267
+ // Navigation-specific dynamic actions
268
+ export const ctrlZSidebar: ActionFn = (ctx: ModeContext) => {
269
+ if (!ctx.state.sidebar.visible) {
270
+ return r([{ type: 'toggle-sidebar' }])
271
+ }
272
+ return r([])
273
+ }
274
+
275
+ // ---------------------------------------------------------------------------
276
+ // Terminal-input escape shortcuts (used by raw-input-handler)
277
+ // These fire while the user is actively in terminal-input mode.
278
+ // ---------------------------------------------------------------------------
279
+
280
+ export const leaveTerminalInput: KeyResult = r(
281
+ [{ focusMode: 'navigation', type: 'set-focus-mode' }],
282
+ [],
283
+ 'navigation'
284
+ )
285
+
286
+ export const enterLayoutMode: KeyResult = r(
287
+ [{ focusMode: 'layout', type: 'set-focus-mode' }],
288
+ [],
289
+ 'layout'
290
+ )
291
+
292
+ export const toggleSidebarFromInput: KeyResult = r([{ type: 'toggle-sidebar' }])
293
+
294
+ // Session name modal
295
+ export const confirmSessionRename: ActionFn = (ctx: ModeContext) => {
296
+ const trimmed = (ctx.state.modal.editBuffer ?? '').trim()
297
+ const sessionId = ctx.state.modal.sessionTargetId
298
+ if (trimmed && sessionId) {
299
+ return r(
300
+ [{ type: 'open-session-picker' }],
301
+ [{ name: trimmed, sessionId, type: 'rename-session' }],
302
+ 'modal.session-picker'
303
+ )
304
+ }
305
+ return r([{ type: 'open-session-picker' }], [], 'modal.session-picker')
306
+ }
307
+
308
+ // Rename tab modal
309
+ export const confirmRenameTab: ActionFn = (ctx: ModeContext) => {
310
+ const trimmed = (ctx.state.modal.editBuffer ?? '').trim()
311
+ const tabId = ctx.state.modal.sessionTargetId
312
+ const actions: KeyResult['actions'] = []
313
+ if (trimmed && tabId) {
314
+ actions.push({ tabId, title: trimmed, type: 'rename-tab' })
315
+ }
316
+ actions.push({ type: 'close-modal' })
317
+ return r(actions, [], 'navigation')
318
+ }
319
+
320
+ // Create session modal
321
+ export const confirmCreateSession: ActionFn = (ctx: ModeContext) => {
322
+ const modal = ctx.state.modal as {
323
+ activeField?: string
324
+ editBuffer?: string
325
+ pendingProjectPath?: string
326
+ }
327
+
328
+ if (modal.activeField === 'directory') {
329
+ return r([{ type: 'select-directory' }])
330
+ }
331
+
332
+ const trimmed = (modal.editBuffer ?? '').trim()
333
+ const projectPath = modal.pendingProjectPath ?? undefined
334
+ const sessionName = trimmed || getDefaultSessionName(projectPath)
335
+ if (sessionName) {
336
+ return r(
337
+ [{ type: 'close-modal' }],
338
+ [{ name: sessionName, projectPath, type: 'create-session' }],
339
+ 'navigation'
340
+ )
341
+ }
342
+ return r([{ type: 'close-modal' }], [], 'navigation')
343
+ }
344
+
345
+ function getDefaultSessionName(projectPath?: string): string {
346
+ if (!projectPath) return ''
347
+ const segments = projectPath.split('/').filter(Boolean)
348
+ return segments.at(-1) ?? ''
349
+ }
350
+
351
+ // Session picker escape (conditional)
352
+ export const sessionPickerEscape: ActionFn = (ctx: ModeContext) => {
353
+ if (!ctx.state.currentSessionId) return null
354
+ return r([{ type: 'close-modal' }], [], 'navigation')
355
+ }
356
+
357
+ // Snippet filter: paste from filter + close
358
+ export const snippetFilterPaste: KeyResult = r(
359
+ [{ type: 'close-modal' }],
360
+ [{ type: 'paste-selected-snippet' }],
361
+ 'navigation'
362
+ )
363
+
364
+ export const snippetFilterPasteToGroup: KeyResult = r(
365
+ [{ type: 'close-modal' }],
366
+ [{ type: 'paste-snippet-to-group' }],
367
+ 'navigation'
368
+ )
@@ -0,0 +1,23 @@
1
+ import type { BackendConfig } from './types'
2
+
3
+ /**
4
+ * Create a Claude backend configuration.
5
+ * Stub — runtime wiring deferred to follow-up work.
6
+ */
7
+ export function claudeBackend(opts?: { model?: string }): BackendConfig {
8
+ return {
9
+ args: opts?.model ? ['--model', opts.model] : [],
10
+ command: 'claude',
11
+ }
12
+ }
13
+
14
+ /**
15
+ * Create a Codex backend configuration.
16
+ * Stub — runtime wiring deferred to follow-up work.
17
+ */
18
+ export function codexBackend(opts?: { args?: string[] }): BackendConfig {
19
+ return {
20
+ args: opts?.args ?? [],
21
+ command: 'codex',
22
+ }
23
+ }