@rebon/cli-darwin-arm64 1.8.7 → 1.9.2

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.
Files changed (43) hide show
  1. package/bin/compose-runtime/payload/compose/shims/dsh-environment.js +34 -0
  2. package/bin/compose-runtime/src/commands-runtime.mjs +72 -0
  3. package/bin/compose-runtime/src/loader.mjs +7 -0
  4. package/bin/compose-runtime/src/probe.mjs +77 -0
  5. package/bin/compose-runtime/src/provided-modules.json +38 -0
  6. package/bin/compose-runtime/src/realm.mjs +5 -3
  7. package/bin/compose-runtime/src/registry.mjs +15 -2
  8. package/bin/compose-runtime/src/resolve.mjs +3 -33
  9. package/bin/marketplace/.claude-plugin/marketplace.json +258 -0
  10. package/bin/marketplace/plugins/dsh-llm-deepseek/LICENSE +21 -0
  11. package/bin/marketplace/plugins/dsh-llm-deepseek/llm-deepseek.js +672 -0
  12. package/bin/marketplace/plugins/dsh-llm-deepseek/rebon-plugin.json +27 -0
  13. package/bin/marketplace/plugins/dsh-tool-todo/LICENSE +21 -0
  14. package/bin/marketplace/plugins/dsh-tool-todo/rebon-plugin.json +27 -0
  15. package/bin/marketplace/plugins/dsh-tool-todo/tool-todo.js +143 -0
  16. package/bin/marketplace/plugins/dsh-tool-web/LICENSE +21 -0
  17. package/bin/marketplace/plugins/dsh-tool-web/rebon-plugin.json +27 -0
  18. package/bin/marketplace/plugins/dsh-tool-web/tool-web.js +1527 -0
  19. package/bin/marketplace/plugins/dsh-web-search-exa/LICENSE +21 -0
  20. package/bin/marketplace/plugins/dsh-web-search-exa/rebon-plugin.json +27 -0
  21. package/bin/marketplace/plugins/dsh-web-search-exa/web-search-exa.js +122 -0
  22. package/bin/mods-runtime/package.json +13 -0
  23. package/bin/mods-runtime/src/chain.mjs +188 -0
  24. package/bin/mods-runtime/src/classic.mjs +217 -0
  25. package/bin/mods-runtime/src/claude-code.mjs +101 -0
  26. package/bin/mods-runtime/src/client.mjs +316 -0
  27. package/bin/mods-runtime/src/engine.mjs +196 -0
  28. package/bin/mods-runtime/src/index.mjs +58 -0
  29. package/bin/mods-runtime/src/jsx-transform.mjs +324 -0
  30. package/bin/mods-runtime/src/mod.mjs +370 -0
  31. package/bin/mods-runtime/src/render.mjs +122 -0
  32. package/bin/mods-runtime/src/transpile.mjs +167 -0
  33. package/bin/plugin-host/src/cli.mjs +13 -0
  34. package/bin/plugin-host/src/host.mjs +11 -1
  35. package/bin/plugin-host/src/methods.mjs +4 -1
  36. package/bin/plugin-host/src/protocol.mjs +38 -4
  37. package/bin/rebon +2 -2
  38. package/bin/rebon-boa-helper +0 -0
  39. package/bin/rebon-browser-mcp +0 -0
  40. package/bin/rebon-computer-use +0 -0
  41. package/bin/rebon-lsp-mcp +0 -0
  42. package/bin/sandbox-win +0 -0
  43. package/package.json +1 -1
@@ -0,0 +1,101 @@
1
+ // The `claude-code` module a mod imports at run time.
2
+ //
3
+ // The type imports (`import type { Register } from 'claude-code'`) are
4
+ // erased before this file is reached. What survives are the value imports:
5
+ // `h` and `Fragment`, which JSX compiles against (also installed as globals,
6
+ // since a hooks module may use JSX without importing anything), and the
7
+ // state library — `atom`, `derive`, `read`, `update`, `memberOf` — which the
8
+ // mods reference says comes from the same import.
9
+ //
10
+ // An element is plain data: `{ type, props, children }`, nothing a surface
11
+ // cannot read as JSON. Handler props (`onPress`, `onInput`, ...) are kept by
12
+ // the mod's own render registry and never leave the process; see
13
+ // `render.mjs`.
14
+
15
+ /// The marker the surface table's constructors leave on what they build.
16
+ export const ELEMENT = Symbol.for('rebon.mod.element');
17
+
18
+ export function Fragment(props) {
19
+ return { [ELEMENT]: true, type: 'Fragment', props: {}, children: normalizeChildren(props?.children) };
20
+ }
21
+
22
+ /// JSX factory. `type` is a constructor from `$.ui.resolve(e)` (a function),
23
+ /// `Fragment`, or — refused later by validation — a string tag.
24
+ export function h(type, props, ...children) {
25
+ const flat = normalizeChildren(children);
26
+ const given = props === null || props === undefined ? {} : { ...props };
27
+ if (typeof type === 'function') return type({ ...given, children: flat });
28
+ return { [ELEMENT]: true, type: String(type), props: given, children: flat };
29
+ }
30
+
31
+ /// Children as a flat list of elements and strings; `null`, `undefined` and
32
+ /// booleans draw nothing, numbers draw as text.
33
+ export function normalizeChildren(children) {
34
+ const out = [];
35
+ const push = (child) => {
36
+ if (child === null || child === undefined || typeof child === 'boolean') return;
37
+ if (Array.isArray(child)) { child.forEach(push); return; }
38
+ if (typeof child === 'number' || typeof child === 'bigint') { out.push(String(child)); return; }
39
+ if (typeof child === 'string') { out.push(child); return; }
40
+ if (typeof child === 'object' && child[ELEMENT]) {
41
+ if (child.type === 'Fragment') { child.children.forEach(push); return; }
42
+ out.push(child);
43
+ return;
44
+ }
45
+ out.push(String(child));
46
+ };
47
+ push(children);
48
+ return out;
49
+ }
50
+
51
+ if (typeof globalThis.h !== 'function') globalThis.h = h;
52
+ if (typeof globalThis.Fragment !== 'function') globalThis.Fragment = Fragment;
53
+
54
+ // ---- the state library -----------------------------------------------
55
+
56
+ const ATOM = Symbol.for('rebon.mod.atom');
57
+ const DERIVED = Symbol.for('rebon.mod.derived');
58
+
59
+ /// A named value in `$.state` with an initial value, by typed reference.
60
+ export function atom(ref, initial, options = {}) {
61
+ return Object.freeze({ [ATOM]: true, ref: Object.freeze({ ...ref }), initial, options: Object.freeze({ ...options }) });
62
+ }
63
+
64
+ /// A value computed from other atoms while drawing.
65
+ export function derive(sources, compute) {
66
+ return Object.freeze({ [DERIVED]: true, sources: Object.freeze([...sources]), compute });
67
+ }
68
+
69
+ /// A member of a state family, named by `e`'s instance.
70
+ export function memberOf(family, e) {
71
+ const id = typeof e === 'string' ? e : e?.requestId ?? e?.id;
72
+ return atom({ ...family.ref, id }, family.initial, family.options);
73
+ }
74
+
75
+ /// Reads an atom (or a derived value) through `$`, subscribing the drawing.
76
+ export async function read($, source) {
77
+ if (source?.[DERIVED]) {
78
+ const values = await Promise.all(source.sources.map((inner) => read($, inner)));
79
+ return source.compute(...values);
80
+ }
81
+ if (!source?.[ATOM]) throw new TypeError('read() takes an atom or a derived value');
82
+ const { value } = await $.state.get(source.ref);
83
+ return value === undefined ? source.initial : value;
84
+ }
85
+
86
+ /// Reads, applies and writes with a version check, again on a miss, so two
87
+ /// updates before a redraw both land.
88
+ export async function update($, source, fn) {
89
+ if (!source?.[ATOM]) throw new TypeError('update() takes an atom');
90
+ for (let attempt = 0; attempt < 8; attempt += 1) {
91
+ const current = await $.state.get(source.ref);
92
+ const value = fn(current.value === undefined ? source.initial : current.value);
93
+ const written = await $.state.set(source.ref, value, { ifVersion: current.version });
94
+ if (written.isWritten !== false) return written.value;
95
+ }
96
+ throw new Error(`update(): ${source.ref.plugin}.${source.ref.key} kept changing underneath`);
97
+ }
98
+
99
+ export function isAtom(value) {
100
+ return Boolean(value?.[ATOM]);
101
+ }
@@ -0,0 +1,316 @@
1
+ // The `Client` element: a mod's surface module, run here, drawn there.
2
+ //
3
+ // In Claude Code a `Client` runs its surface module on the terminal's own
4
+ // drawing thread. Rebon's surfaces are not JavaScript, so the instance lives
5
+ // where the mod does, in this process: its function, its local state, its
6
+ // timers and its key listener. When the mod's render hook draws a tree
7
+ // holding a `<Client key module props width height />`, the instance under
8
+ // that key is called with its props and the region it was given, and what it
9
+ // returns travels to the surface as the Client's one child. A `setState`, a
10
+ // tick or a key asks the surface to draw the site again (`ui.invalidate`),
11
+ // which runs the render hook and the instance once more with its state kept.
12
+ //
13
+ // The surface hands back what the person does to the region: keys while the
14
+ // Client holds the site's focus ring (`clientKey`), clicks (`clientPointer`).
15
+ // `surface.post(data)` raises `ui.message` on the mod's chain; a hook that
16
+ // answers `{ props }` hands the instance its next props until the render
17
+ // hook draws different ones.
18
+ import path from 'node:path';
19
+ import { pathToFileURL } from 'node:url';
20
+ import { clientElementTable, isElement } from './render.mjs';
21
+ import { resolveModFile } from './transpile.mjs';
22
+
23
+ /// Renders in a row that each `setState` without a key, pointer, tick or
24
+ /// new props between: a render loop, and the instance unmounts.
25
+ const RENDER_LOOP_STREAK = 3;
26
+ /// The least interval `surface.every` runs at.
27
+ const MIN_TICK_MS = 16;
28
+ /// How much a post may hold, serialised.
29
+ const MAX_POST_CHARS = 100_000;
30
+
31
+ function instanceId(requestId, key) {
32
+ return `${requestId}\u0000${key}`;
33
+ }
34
+
35
+ /// The cells a `width` / `height` prop gives out of `available`: a count, a
36
+ /// percentage of it, or all of it when the prop says neither.
37
+ export function regionCells(value, available) {
38
+ const room = Math.max(0, Math.floor(available ?? 0));
39
+ if (typeof value === 'number' && Number.isFinite(value)) return Math.max(0, Math.min(room, Math.floor(value)));
40
+ if (typeof value === 'string' && value.trim().endsWith('%')) {
41
+ const percent = Number.parseFloat(value);
42
+ if (Number.isFinite(percent)) return Math.max(0, Math.floor((room * Math.min(100, percent)) / 100));
43
+ }
44
+ return room;
45
+ }
46
+
47
+ /// The function a surface module exports: its default, else its one
48
+ /// PascalCase export.
49
+ export function moduleFunction(namespace) {
50
+ if (typeof namespace?.default === 'function') return namespace.default;
51
+ const named = Object.entries(namespace ?? {}).filter(([name, value]) => /^[A-Z]/.test(name) && typeof value === 'function');
52
+ return named.length === 1 ? named[0][1] : undefined;
53
+ }
54
+
55
+ export class ClientHost {
56
+ #instances = new Map();
57
+ #invalidateQueued = false;
58
+
59
+ constructor(mod, { importModule = (url) => import(url) } = {}) {
60
+ this.mod = mod;
61
+ this.importModule = importModule;
62
+ }
63
+
64
+ /// Loads the surface module of every `Client` in `tree` that has no
65
+ /// running instance yet, ahead of the drawing, which is synchronous.
66
+ async prepare(requestId, tree, context) {
67
+ for (const node of clientNodes(tree)) {
68
+ const key = String(node.props.key ?? '');
69
+ const module = String(node.props.module ?? '');
70
+ if (!key || !module) continue;
71
+ const id = instanceId(requestId, key);
72
+ const held = this.#instances.get(id);
73
+ if (held && held.module === module) continue;
74
+ if (held) this.#unmount(held);
75
+ const instance = this.#newInstance(requestId, key, module, context);
76
+ this.#instances.set(id, instance);
77
+ try {
78
+ instance.fn = await this.#load(module);
79
+ } catch (error) {
80
+ instance.failed = error?.message ?? String(error);
81
+ this.mod.report(`Client ${module}`, error);
82
+ }
83
+ }
84
+ }
85
+
86
+ /// What the instance under `node`'s key draws now, as an element.
87
+ draw(requestId, node, context) {
88
+ const key = String(node.props.key ?? '');
89
+ const instance = this.#instances.get(instanceId(requestId, key));
90
+ const { Text } = clientElementTable(context.surface);
91
+ if (!instance) return Text({ dimColor: true, children: `Client ${key} is not loaded` });
92
+ instance.context = context;
93
+ instance.columns = regionCells(node.props.width, context.viewport?.columns);
94
+ instance.rows = regionCells(node.props.height, context.viewport?.rows);
95
+ const treeProps = JSON.stringify(node.props.props ?? null);
96
+ if (treeProps !== instance.lastTreeProps) {
97
+ instance.lastTreeProps = treeProps;
98
+ instance.props = node.props.props;
99
+ instance.propsOverride = undefined;
100
+ instance.loopStreak = 0;
101
+ }
102
+ if (instance.failed !== undefined) {
103
+ return Text({ dimColor: true, children: `${instance.module} stopped: ${instance.failed}` });
104
+ }
105
+ instance.setStateInDraw = false;
106
+ instance.drawing = true;
107
+ let drawn;
108
+ try {
109
+ drawn = instance.fn(instance.propsOverride !== undefined ? instance.propsOverride : instance.props, instance.surface);
110
+ } catch (error) {
111
+ this.#fail(instance, error);
112
+ return Text({ dimColor: true, children: `${instance.module} stopped: ${instance.failed}` });
113
+ } finally {
114
+ instance.drawing = false;
115
+ }
116
+ instance.loopStreak = instance.setStateInDraw ? instance.loopStreak + 1 : 0;
117
+ if (instance.loopStreak >= RENDER_LOOP_STREAK) {
118
+ this.#fail(instance, new Error(`setState on ${RENDER_LOOP_STREAK} renders in a row with nothing between: a render loop`));
119
+ return Text({ dimColor: true, children: `${instance.module} stopped: ${instance.failed}` });
120
+ }
121
+ if (typeof drawn === 'string') return Text({ children: drawn });
122
+ if (!isElement(drawn)) return Text({ dimColor: true, children: '' });
123
+ return drawn;
124
+ }
125
+
126
+ /// Unmounts the instances under `requestId` its latest drawing no longer
127
+ /// holds.
128
+ sweep(requestId, keys) {
129
+ for (const instance of [...this.#instances.values()]) {
130
+ if (instance.requestId === requestId && !keys.has(instance.key)) this.#unmount(instance);
131
+ }
132
+ }
133
+
134
+ /// Unmounts every instance of one site (its pane closed).
135
+ forget(requestId) {
136
+ this.sweep(requestId, new Set());
137
+ }
138
+
139
+ disposeAll() {
140
+ for (const instance of [...this.#instances.values()]) this.#unmount(instance);
141
+ }
142
+
143
+ /// A key the person pressed while the Client held the focus ring.
144
+ key(requestId, element, event) {
145
+ const instance = this.#instances.get(instanceId(requestId, String(element)));
146
+ if (!instance || instance.failed !== undefined || typeof instance.keyFn !== 'function') return { heard: false };
147
+ instance.loopStreak = 0;
148
+ try {
149
+ instance.keyFn(Object.freeze({ ...keyEvent(event) }));
150
+ } catch (error) {
151
+ this.#fail(instance, error);
152
+ }
153
+ return { heard: true };
154
+ }
155
+
156
+ /// A pointer event over the Client's region, region-relative.
157
+ pointer(requestId, element, event) {
158
+ const instance = this.#instances.get(instanceId(requestId, String(element)));
159
+ if (!instance || instance.failed !== undefined || typeof instance.pointerFn !== 'function') return { heard: false };
160
+ instance.loopStreak = 0;
161
+ try {
162
+ instance.pointerFn(Object.freeze({ ...event }));
163
+ } catch (error) {
164
+ this.#fail(instance, error);
165
+ }
166
+ return { heard: true };
167
+ }
168
+
169
+ /// How many instances run; for tests and `describe`.
170
+ get size() {
171
+ return this.#instances.size;
172
+ }
173
+
174
+ // ---- inside ----------------------------------------------------------
175
+
176
+ async #load(module) {
177
+ const base = path.resolve(this.mod.root, path.dirname(this.mod.entry));
178
+ const wanted = path.resolve(base, module);
179
+ const root = this.mod.root.endsWith(path.sep) ? this.mod.root : this.mod.root + path.sep;
180
+ if (!wanted.startsWith(root)) throw new Error(`[PATH_ESCAPES] the Client module ${module} is outside the mod's folder`);
181
+ const file = resolveModFile(wanted);
182
+ if (file === null) throw new Error(`[NO_MODULE] the Client module ${module} names no file`);
183
+ const namespace = await this.importModule(pathToFileURL(file).href);
184
+ const fn = moduleFunction(namespace);
185
+ if (typeof fn !== 'function') throw new Error(`[NO_COMPONENT] ${module} exports no default function and no one PascalCase function`);
186
+ return fn;
187
+ }
188
+
189
+ #newInstance(requestId, key, module, context) {
190
+ const host = this;
191
+ const instance = {
192
+ requestId, key, module, context,
193
+ fn: undefined, failed: undefined,
194
+ state: undefined, props: undefined, propsOverride: undefined, lastTreeProps: undefined,
195
+ columns: 0, rows: 0,
196
+ timers: new Set(), keyFn: undefined, pointerFn: undefined,
197
+ drawing: false, setStateInDraw: false, loopStreak: 0,
198
+ pendingPost: undefined, postQueued: false,
199
+ };
200
+ instance.surface = Object.freeze({
201
+ get elements() { return clientElementTable(instance.context?.surface); },
202
+ get state() { return instance.state; },
203
+ get columns() { return instance.columns; },
204
+ get rows() { return instance.rows; },
205
+ setState(next) {
206
+ instance.state = next;
207
+ if (instance.drawing) instance.setStateInDraw = true;
208
+ host.#invalidate();
209
+ },
210
+ every(ms, fn) {
211
+ const timer = setInterval(() => {
212
+ if (instance.failed !== undefined) return;
213
+ instance.loopStreak = 0;
214
+ try { fn(); } catch (error) { host.#fail(instance, error); }
215
+ }, Math.max(MIN_TICK_MS, Number(ms) || 0));
216
+ instance.timers.add(timer);
217
+ return () => { clearInterval(timer); instance.timers.delete(timer); };
218
+ },
219
+ onKey(fn) {
220
+ instance.keyFn = fn;
221
+ return () => { if (instance.keyFn === fn) instance.keyFn = undefined; };
222
+ },
223
+ onPointer(fn) {
224
+ instance.pointerFn = fn;
225
+ return () => { if (instance.pointerFn === fn) instance.pointerFn = undefined; };
226
+ },
227
+ post(data) {
228
+ let text;
229
+ try { text = JSON.stringify(data); } catch { return; }
230
+ if (text === undefined || text.length > MAX_POST_CHARS) return;
231
+ instance.pendingPost = JSON.parse(text);
232
+ if (instance.postQueued) return;
233
+ instance.postQueued = true;
234
+ setTimeout(() => host.#deliver(instance), 0);
235
+ },
236
+ });
237
+ return instance;
238
+ }
239
+
240
+ async #deliver(instance) {
241
+ instance.postQueued = false;
242
+ if (instance.failed !== undefined || !this.#instances.has(instanceId(instance.requestId, instance.key))) return;
243
+ const data = instance.pendingPost;
244
+ instance.pendingPost = undefined;
245
+ const e = {
246
+ surface: instance.context?.surface ?? 'terminal',
247
+ component: instance.context?.component ?? 'Pane',
248
+ requestId: instance.requestId,
249
+ element: instance.key,
250
+ module: instance.module,
251
+ data,
252
+ };
253
+ try {
254
+ const { result } = await this.mod.chain.dispatch('ui.message', e, async () => ({}), this.mod.dispatchOptions());
255
+ if (result && typeof result === 'object' && result.props !== undefined) {
256
+ instance.propsOverride = result.props;
257
+ instance.loopStreak = 0;
258
+ this.#invalidate();
259
+ }
260
+ } catch (error) {
261
+ this.mod.report(`ui.message from ${instance.module}`, error);
262
+ }
263
+ }
264
+
265
+ #fail(instance, error) {
266
+ instance.failed = error?.message ?? String(error);
267
+ this.#stopTimers(instance);
268
+ this.mod.report(`Client ${instance.module}`, error);
269
+ this.#invalidate();
270
+ }
271
+
272
+ #stopTimers(instance) {
273
+ for (const timer of instance.timers) clearInterval(timer);
274
+ instance.timers.clear();
275
+ }
276
+
277
+ #unmount(instance) {
278
+ this.#stopTimers(instance);
279
+ instance.keyFn = undefined;
280
+ instance.pointerFn = undefined;
281
+ this.#instances.delete(instanceId(instance.requestId, instance.key));
282
+ }
283
+
284
+ /// One redraw of the mod's sites per turn of the event loop, however many
285
+ /// instances asked.
286
+ #invalidate() {
287
+ if (this.#invalidateQueued) return;
288
+ this.#invalidateQueued = true;
289
+ setTimeout(() => {
290
+ this.#invalidateQueued = false;
291
+ if (this.mod.scopeCtx) void this.mod.seat('ui.invalidate', { event: 'ui.render' }).catch(() => {});
292
+ }, 0);
293
+ }
294
+ }
295
+
296
+ /// The `Client` elements in a tree a render hook returned, in order.
297
+ export function clientNodes(tree) {
298
+ const out = [];
299
+ const walk = (node) => {
300
+ if (!isElement(node)) return;
301
+ if (node.type === 'Client') { out.push(node); return; }
302
+ for (const child of node.children ?? []) walk(child);
303
+ };
304
+ walk(tree);
305
+ return out;
306
+ }
307
+
308
+ /// A key as `ClientKeyEvent` spells it: the name, and only the modifiers
309
+ /// held.
310
+ function keyEvent(event) {
311
+ const out = { key: String(event?.key ?? '') };
312
+ if (event?.ctrl) out.ctrl = true;
313
+ if (event?.shift) out.shift = true;
314
+ if (event?.meta) out.meta = true;
315
+ return out;
316
+ }
@@ -0,0 +1,196 @@
1
+ // `$`, the engine interface a mod's hooks are handed.
2
+ //
3
+ // Flat, `<noun>.<method>`, frozen. The nouns here are the core ones a
4
+ // hooks module reaches for in the Claude Code declarations; what each does
5
+ // on Rebon is one of three things:
6
+ //
7
+ // * answered locally — `plugin`, `ui.resolve`, `clock`, the timer half of
8
+ // the chain's own life;
9
+ // * answered by the `mods` seat over `seat/call` — display (`ui.status`,
10
+ // `ui.toast`, `ui.log`, panes), state and store, files, processes, the
11
+ // network, settings, environment, the session's facts;
12
+ // * dispatched through the chain itself — `prompt.submit`, `command.run`,
13
+ // `tool.call` on a tool the mod registered.
14
+ //
15
+ // A seat call made before the mod's scope opened, or after it closed,
16
+ // rejects naming that: a mod that acts on its own schedule starts from
17
+ // `session.start`, which fires once the scope is open.
18
+ import { elementTable } from './render.mjs';
19
+
20
+ /// The seat the engine interface is answered by on the Rust side.
21
+ export const MODS_SEAT = 'mods';
22
+
23
+ export function buildEngine(mod) {
24
+ const seat = (method, params = {}) => mod.seat(method, params);
25
+ const timers = mod.timers;
26
+
27
+ const $ = {
28
+ plugin: Object.freeze({ name: mod.name, root: mod.root }),
29
+
30
+ ui: Object.freeze({
31
+ resolve: (e) => elementTable(e?.surface),
32
+ status: (text) => { void seat('ui.status', { text: text ?? null }); },
33
+ toast: (text, options = {}) => { void seat('ui.toast', { text, ...options }); },
34
+ log: (text, options = {}) => { void seat('ui.log', { text: String(text), to: options.to ?? 'transcript' }); },
35
+ notice: (toolUseId, text) => { void seat('ui.notice', { toolUseId, text: text ?? null }); },
36
+ invalidate: (event = 'ui.render') => { void seat('ui.invalidate', { event }); },
37
+ open: (pane) => seat('ui.open', pane),
38
+ close: (pane) => {
39
+ if (pane?.id !== undefined) mod.clients.forget(String(pane.id));
40
+ return seat('ui.close', pane);
41
+ },
42
+ panes: () => seat('ui.panes', {}),
43
+ focus: (args) => seat('ui.focus', args),
44
+ scroll: (args) => seat('ui.scroll', args),
45
+ copy: (args) => seat('ui.copy', args),
46
+ selection: () => seat('ui.selection', {}),
47
+ ask: (question, options) => seat('ui.ask', { question, options: Array.isArray(options) ? { choices: options } : options ?? {} }),
48
+ blit: async () => ({ deny: 'the terminal raster is not drawn on Rebon' }),
49
+ }),
50
+
51
+ state: Object.freeze({
52
+ get: (ref) => seat('state.get', { ref }),
53
+ set: (ref, value, options = {}) => seat('state.set', { ref, value, ...options }),
54
+ }),
55
+
56
+ store: Object.freeze({
57
+ get: (key) => seat('store.get', { key }),
58
+ set: (key, value) => seat('store.set', { key, value }),
59
+ delete: (key) => seat('store.delete', { key }),
60
+ keys: () => seat('store.keys', {}),
61
+ }),
62
+
63
+ clock: Object.freeze({
64
+ now: () => Date.now(),
65
+ sleep: (ms, options = {}) => new Promise((resolve, reject) => {
66
+ const timer = setTimeout(() => { timers.delete(timer); resolve(); }, ms);
67
+ timers.add(timer);
68
+ options.signal?.addEventListener('abort', () => { clearTimeout(timer); timers.delete(timer); reject(new Error('sleep aborted')); }, { once: true });
69
+ }),
70
+ after: (ms, fn) => {
71
+ const timer = setTimeout(() => { timers.delete(timer); runTimer(mod, fn); }, ms);
72
+ timers.add(timer);
73
+ return Object.freeze({ cancel: () => { clearTimeout(timer); timers.delete(timer); } });
74
+ },
75
+ every: (ms, fn) => {
76
+ const timer = setInterval(() => runTimer(mod, fn), Math.max(10, ms));
77
+ timers.add(timer);
78
+ return Object.freeze({ cancel: () => { clearInterval(timer); timers.delete(timer); } });
79
+ },
80
+ }),
81
+
82
+ session: Object.freeze({
83
+ id: () => mod.sessionFact('sessionId'),
84
+ cwd: () => mod.sessionFact('cwd'),
85
+ root: () => mod.sessionFact('cwd'),
86
+ model: () => mod.sessionFact('model'),
87
+ surface: () => mod.sessionFact('surface'),
88
+ surfaces: () => seat('session.surfaces', {}),
89
+ turns: () => mod.sessionFact('turns'),
90
+ messages: (args = {}) => seat('session.messages', args),
91
+ usage: (args = {}) => seat('session.usage', args),
92
+ version: () => seat('session.version', {}),
93
+ repo: () => seat('session.repo', {}),
94
+ append: (args) => seat('session.append', args),
95
+ send: (args) => seat('session.send', args),
96
+ compact: (args = {}) => seat('session.compact', args),
97
+ authorize: () => seat('session.authorize', {}),
98
+ }),
99
+
100
+ turn: Object.freeze({
101
+ abort: (input = {}) => seat('turn.abort', input),
102
+ }),
103
+
104
+ prompt: Object.freeze({
105
+ submit: (input) => seat('prompt.submit', typeof input === 'string' ? { text: input } : input),
106
+ fill: (input) => seat('prompt.fill', input),
107
+ suggest: (input) => seat('prompt.suggest', typeof input === 'string' ? { text: input } : input),
108
+ read: () => seat('prompt.read', {}),
109
+ compose: (args = {}) => seat('prompt.compose', args),
110
+ }),
111
+
112
+ tool: Object.freeze({
113
+ list: () => seat('tool.list', {}),
114
+ call: (input) => mod.callTool(input),
115
+ check: (input) => seat('tool.check', input),
116
+ register: (spec) => mod.registerTool(spec),
117
+ }),
118
+
119
+ command: Object.freeze({
120
+ list: () => seat('command.list', {}),
121
+ run: (input) => seat('command.run', typeof input === 'string' ? { command: input } : input),
122
+ register: (spec) => mod.registerCommand(spec),
123
+ }),
124
+
125
+ config: Object.freeze({
126
+ list: () => seat('config.list', {}),
127
+ set: (input) => seat('config.set', input),
128
+ }),
129
+
130
+ settings: Object.freeze({
131
+ read: (args = {}) => seat('settings.read', args),
132
+ }),
133
+
134
+ env: Object.freeze({
135
+ get: (name) => seat('env.get', { name }),
136
+ set: (name, value) => seat('env.set', { name, value: value ?? null }),
137
+ }),
138
+
139
+ fs: Object.freeze({
140
+ read: (path, options = {}) => seat('fs.read', { path, ...options }),
141
+ write: (path, text) => seat('fs.write', { path, text }),
142
+ list: (path) => seat('fs.list', { path: path ?? null }),
143
+ exists: (path) => seat('fs.exists', { path }),
144
+ stat: (path, options = {}) => seat('fs.stat', { path, ...options }),
145
+ ancestors: (request) => seat('fs.ancestors', request),
146
+ }),
147
+
148
+ process: Object.freeze({
149
+ run: (argv, init = {}) => seat('process.run', { argv: [...argv], ...init }),
150
+ spawn: async (request) => {
151
+ // No streaming on this plane: the child runs to completion and its
152
+ // output comes back whole, which is what `run` answers.
153
+ const done = await seat('process.run', { argv: [...request.argv], cwd: request.cwd, env: request.env, input: request.input });
154
+ return { ...done, isStreamed: false };
155
+ },
156
+ }),
157
+
158
+ http: Object.freeze({
159
+ fetch: (url, init = {}) => seat('http.fetch', { url: String(url), ...init }),
160
+ }),
161
+
162
+ model: Object.freeze({
163
+ complete: (request, options = {}) => seat('model.complete', { ...request, ...options }),
164
+ fork: (request) => seat('model.fork', request),
165
+ classify: (text, labels, options = {}) => seat('model.classify', { text, labels: [...labels], ...options }),
166
+ }),
167
+
168
+ agent: Object.freeze({
169
+ list: () => seat('agent.list', {}),
170
+ spawn: (input) => seat('agent.spawn', input),
171
+ register: (spec) => seat('agent.register', spec),
172
+ }),
173
+
174
+ mcp: Object.freeze({
175
+ call: (server, tool, args = {}) => mod.callTool({ tool: `mcp__${server}__${tool}`, input: args }),
176
+ connect: (server) => seat('mcp.connect', { server }),
177
+ }),
178
+
179
+ audio: Object.freeze({
180
+ play: async () => ({ isPlayed: false, reason: 'no-audio' }),
181
+ speak: async () => ({ isSpoken: false, reason: 'no-audio' }),
182
+ }),
183
+
184
+ telemetry: Object.freeze({
185
+ log: (entry) => { void seat('ui.log', { text: JSON.stringify(entry), to: 'debug' }); },
186
+ mark: () => {},
187
+ }),
188
+ };
189
+ return Object.freeze($);
190
+ }
191
+
192
+ function runTimer(mod, fn) {
193
+ Promise.resolve()
194
+ .then(() => fn(mod.engine))
195
+ .catch((error) => mod.report('timer', error));
196
+ }
@@ -0,0 +1,58 @@
1
+ // The package's root export: the loader seam for Claude Code mods.
2
+ //
3
+ // A mod — a Claude Code plugin of function hooks, `.claude-plugin/plugin.json`
4
+ // beside a `hooks/hooks.json` naming one hooks module — is a plane plugin of
5
+ // a shape the host's own loader does not know. This decides it is one by
6
+ // reading the load request, not the module: rebon marks the request's
7
+ // `config.$claudeMod` when it synthesised the manifest from the plugin's
8
+ // folder, so what loads here is exactly what rebon said it was loading.
9
+ //
10
+ // `createLoader({ next })` is the host's contract: `next` is the loader a
11
+ // request this one does not recognise is handed to, untouched.
12
+ import { Mod, MOD_SERVICE } from './mod.mjs';
13
+
14
+ export { MOD_SERVICE };
15
+ export { transformJsx } from './jsx-transform.mjs';
16
+ export { transpileModSource } from './transpile.mjs';
17
+ export { HookChain, patternSelects, matcherSelects } from './chain.mjs';
18
+ export { elementTable, SURFACES } from './render.mjs';
19
+
20
+ const mods = new Map();
21
+
22
+ /// The marker rebon puts on a mod's load request.
23
+ export const MARKER = '$claudeMod';
24
+
25
+ /// Whether a load request is a mod's.
26
+ export function isModRequest(request) {
27
+ const marker = request?.config?.[MARKER];
28
+ return marker !== null && typeof marker === 'object' && !Array.isArray(marker);
29
+ }
30
+
31
+ export async function createLoader({ next }) {
32
+ return {
33
+ async load(request) {
34
+ if (!isModRequest(request)) return next(request);
35
+ const mod = new Mod({
36
+ pluginId: request.pluginId,
37
+ root: request.root,
38
+ entry: request.entry,
39
+ marker: request.config[MARKER],
40
+ });
41
+ await mod.load();
42
+ mods.set(request.pluginId, mod);
43
+ return mod.sealed();
44
+ },
45
+ async unload(pluginId) {
46
+ const mod = mods.get(pluginId);
47
+ if (!mod) return false;
48
+ mods.delete(pluginId);
49
+ mod.dispose();
50
+ return true;
51
+ },
52
+ };
53
+ }
54
+
55
+ /// The mods this process has loaded, by plugin id.
56
+ export function loadedMods() {
57
+ return new Map(mods);
58
+ }