@rebon/cli-darwin-arm64 1.8.6 → 1.9.1

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,370 @@
1
+ // One loaded mod: its hooks module, its chain, its `$`, and the service
2
+ // the plane drives it through.
3
+ //
4
+ // A mod is a plane plugin of one shape. It registers exactly one service,
5
+ // `mod`, and one scope handler. The service is how rebon reaches the chain
6
+ // — a classic hook event, a render ask, a press, a command, one of its own
7
+ // tools — and the scope handler is where `$` binds to a session: once the
8
+ // scope opens `session.start` fires, and from then on `$`'s seat calls have
9
+ // somewhere to go.
10
+ //
11
+ // The declarations a mod loads against come from the manifest rebon
12
+ // synthesised by scanning the hooks module (`$claudeMod` in the load
13
+ // request's config): the commands and tools it names there are registered
14
+ // with the host's registrar here, so the plane projects them like any
15
+ // plugin's, and a `$.command.register` / `$.tool.register` at run time
16
+ // refines a declared one rather than adding a name the ceiling lacks.
17
+ import { pathToFileURL } from 'node:url';
18
+ import path from 'node:path';
19
+ import { HookChain } from './chain.mjs';
20
+ import { buildEngine } from './engine.mjs';
21
+ import { dispatchClassic } from './classic.mjs';
22
+ import { HandlerRegistry, isElement } from './render.mjs';
23
+ import { ClientHost } from './client.mjs';
24
+ import { registerModRoot, unregisterModRoot } from './transpile.mjs';
25
+
26
+ /// The one service every mod registers.
27
+ export const MOD_SERVICE = 'mod';
28
+
29
+ class ModError extends Error {
30
+ constructor(code, message) {
31
+ super(message);
32
+ this.code = code;
33
+ this.name = 'ModError';
34
+ }
35
+ }
36
+
37
+ const SURFACES = new Set(['tui', 'desktop', 'acp', 'web', 'mobile']);
38
+
39
+ function diagnostic(level, message) {
40
+ process.stderr.write(`${JSON.stringify({ level, message: String(message).replace(/[\r\n]+/g, ' ').slice(0, 512) })}\n`);
41
+ }
42
+
43
+ export class Mod {
44
+ constructor({ pluginId, root, entry, marker }) {
45
+ this.pluginId = pluginId;
46
+ this.root = path.resolve(root);
47
+ this.entry = entry;
48
+ this.name = marker.name ?? pluginId;
49
+ this.version = marker.version;
50
+ this.options = Object.freeze({ ...(marker.options ?? {}) });
51
+ this.declaredCommands = marker.commands ?? [];
52
+ this.declaredTools = marker.tools ?? [];
53
+ this.chain = new HookChain({ onFailure: (failure) => this.#hookFailed(failure) });
54
+ this.timers = new Set();
55
+ this.pendingCalls = new Map();
56
+ this.handlers = new HandlerRegistry();
57
+ this.clients = new ClientHost(this);
58
+ this.scopeCtx = null;
59
+ this.facts = { cwd: this.root, sessionId: undefined, model: undefined, surface: undefined, turns: 0 };
60
+ this.registeredSpecs = { commands: new Map(), tools: new Map() };
61
+ this.engine = buildEngine(this);
62
+ }
63
+
64
+ /// Imports the hooks module and runs its `register`.
65
+ async load(importModule = (url) => import(url)) {
66
+ registerModRoot(this.root);
67
+ const file = path.resolve(this.root, this.entry);
68
+ let module;
69
+ try {
70
+ module = await importModule(pathToFileURL(file).href);
71
+ } catch (cause) {
72
+ throw new ModError('[ENTRY_FAILED]', `hooks module ${this.entry} failed to load: ${cause?.message ?? cause}`);
73
+ }
74
+ const register = typeof module?.register === 'function' ? module.register : module?.default?.register;
75
+ if (typeof register !== 'function') {
76
+ throw new ModError('[NO_REGISTER]', `hooks module ${this.entry} exports no register function`);
77
+ }
78
+ const on = (pattern, matcherOrHook, maybeHook) => this.chain.on(pattern, matcherOrHook, maybeHook);
79
+ try {
80
+ await register(on, this.options);
81
+ } catch (cause) {
82
+ throw new ModError('[REGISTER_FAILED]', `register() of ${this.name} failed: ${cause?.message ?? cause}`);
83
+ }
84
+ }
85
+
86
+ /// The registrar shape the host keeps for this plugin.
87
+ sealed() {
88
+ const serviceHandlers = new Map([[MOD_SERVICE, (request, ctx) => this.handleService(request, ctx)]]);
89
+ const toolHandlers = new Map();
90
+ const tools = [];
91
+ for (const declared of this.declaredTools) {
92
+ const name = declared.name;
93
+ tools.push({ name, description: declared.description || `Tool ${name} of the mod ${this.name}`, inputSchema: declared.inputSchema ?? { type: 'object' } });
94
+ toolHandlers.set(name, (request, ctx) => this.serveTool(name, request, ctx));
95
+ }
96
+ const commandHandlers = new Map();
97
+ const commands = [];
98
+ for (const declared of this.declaredCommands) {
99
+ const name = declared.name;
100
+ const definition = {
101
+ name,
102
+ description: declared.description || `/${name}, a command of the mod ${this.name}`,
103
+ kind: { type: 'prompt' },
104
+ surfaces: ['tui', 'desktop', 'acp', 'web', 'mobile'],
105
+ };
106
+ if (declared.argumentHint) definition.hint = declared.argumentHint;
107
+ commands.push(Object.freeze(definition));
108
+ commandHandlers.set(name, (request, ctx) => this.serveCommand(name, request, ctx));
109
+ }
110
+ return Object.freeze({
111
+ services: Object.freeze([MOD_SERVICE]),
112
+ eventTopics: Object.freeze([]),
113
+ llmProviders: Object.freeze([]),
114
+ llmAdapterInfo: Object.freeze({}),
115
+ tools: Object.freeze(tools),
116
+ commands: Object.freeze(commands),
117
+ commandHandlers,
118
+ serviceHandlers,
119
+ topicHandlers: new Map(),
120
+ llmAdapters: new Map(),
121
+ toolHandlers,
122
+ scopeHandlers: Object.freeze([(ctx) => this.onScopeOpen(ctx)]),
123
+ });
124
+ }
125
+
126
+ dispose() {
127
+ for (const timer of this.timers) { clearTimeout(timer); clearInterval(timer); }
128
+ this.timers.clear();
129
+ for (const pending of this.pendingCalls.values()) {
130
+ clearTimeout(pending.expiry);
131
+ pending.settle({ result: { error: 'the mod was unloaded' }, isError: true });
132
+ }
133
+ this.pendingCalls.clear();
134
+ this.handlers.clear();
135
+ this.clients.disposeAll();
136
+ this.scopeCtx = null;
137
+ unregisterModRoot(this.root);
138
+ }
139
+
140
+ // ---- the scope ------------------------------------------------------
141
+
142
+ async onScopeOpen(ctx) {
143
+ this.scopeCtx = ctx;
144
+ if (ctx.workspaceRoot) this.facts.cwd = ctx.workspaceRoot;
145
+ try {
146
+ await this.chain.dispatch('session.start', { cwd: this.facts.cwd }, async () => ({ cwd: this.facts.cwd }), this.dispatchOptions());
147
+ } catch (error) {
148
+ this.report('session.start', error);
149
+ }
150
+ return () => { if (this.scopeCtx === ctx) this.scopeCtx = null; };
151
+ }
152
+
153
+ seat(method, params) {
154
+ const ctx = this.scopeCtx;
155
+ if (!ctx) return Promise.reject(new ModError('[NO_SESSION]', `$.${method} was called before the mod ${this.name} was bound to a session`));
156
+ return ctx.seat('mods', method, { ...params });
157
+ }
158
+
159
+ sessionFact(name) {
160
+ return this.facts[name];
161
+ }
162
+
163
+ dispatchOptions(extra = {}) {
164
+ return { engine: this.engine, origin: { kind: 'engine' }, ...extra };
165
+ }
166
+
167
+ report(where, error) {
168
+ const message = `${this.name}: ${where}: ${error?.message ?? error}`;
169
+ diagnostic('warn', message);
170
+ if (this.scopeCtx) void this.seat('ui.log', { text: message, to: 'debug' }).catch(() => {});
171
+ }
172
+
173
+ log(text, to = 'transcript') {
174
+ if (this.scopeCtx) void this.seat('ui.log', { text, to }).catch(() => {});
175
+ else diagnostic('info', `${this.name}: ${text}`);
176
+ }
177
+
178
+ #hookFailed({ event, pattern, error, inCatch }) {
179
+ this.report(`${event} (hook on ${pattern}${inCatch ? ', its .catch' : ''})`, error);
180
+ }
181
+
182
+ // ---- registrations at run time ----------------------------------------
183
+
184
+ ownToolName(name) {
185
+ return name.startsWith('mcp__') ? name : `mcp__${this.name}__${name}`;
186
+ }
187
+
188
+ async registerTool(spec) {
189
+ if (!spec || typeof spec.name !== 'string') throw new ModError('[WRONG_SHAPE]', '$.tool.register needs { name, description, inputSchema }');
190
+ const full = this.ownToolName(spec.name);
191
+ if (!this.declaredTools.some((tool) => tool.name === full)) {
192
+ throw new ModError('[UNDECLARED_TOOL]', `tool ${spec.name} is not one the hooks module names in a $.tool.register({ name: "..." }) literal; spell it as one`);
193
+ }
194
+ this.registeredSpecs.tools.set(full, spec);
195
+ return this.seat('tool.register', { name: full, description: spec.description, inputSchema: spec.inputSchema ?? spec.input_schema ?? { type: 'object' } });
196
+ }
197
+
198
+ async registerCommand(spec) {
199
+ if (!spec || typeof spec.name !== 'string') throw new ModError('[WRONG_SHAPE]', '$.command.register needs { name, description }');
200
+ if (!this.declaredCommands.some((command) => command.name === spec.name)) {
201
+ throw new ModError('[UNDECLARED_COMMAND]', `command ${spec.name} is not one the hooks module names in a $.command.register({ name: "..." }) literal; spell it as one`);
202
+ }
203
+ this.registeredSpecs.commands.set(spec.name, spec);
204
+ return this.seat('command.register', { name: spec.name, description: spec.description, argumentHint: spec.argumentHint ?? null });
205
+ }
206
+
207
+ /// `$.tool.call`: one of this mod's own tools runs through its chain;
208
+ /// any other goes to rebon through the scope's `tool/invoke`.
209
+ async callTool(input) {
210
+ const tool = input?.tool;
211
+ if (typeof tool !== 'string') throw new ModError('[WRONG_SHAPE]', '$.tool.call needs { tool, input }');
212
+ if (this.declaredTools.some((declared) => declared.name === tool)) {
213
+ const { result } = await this.chain.dispatch('tool.call', { tool, input: input.input ?? {}, origin: { kind: 'plugin', name: this.name } }, async () => ({ deny: `no hook of ${this.name} answers ${tool}` }), this.dispatchOptions());
214
+ return result;
215
+ }
216
+ const ctx = this.scopeCtx;
217
+ if (!ctx) throw new ModError('[NO_SESSION]', `$.tool.call(${tool}) was made before the mod was bound to a session`);
218
+ try {
219
+ const result = await ctx.invoke(tool, input.input ?? {});
220
+ return { result, isError: false, ref: { tool } };
221
+ } catch (error) {
222
+ return { deny: error?.message ?? String(error) };
223
+ }
224
+ }
225
+
226
+ // ---- what the plane asks ----------------------------------------------
227
+
228
+ async serveTool(name, request, ctx) {
229
+ this.scopeCtx ??= ctx;
230
+ const e = { tool: name, input: request?.input ?? {}, tool_use_id: request?.toolUseId ?? null, origin: { kind: 'model' } };
231
+ const { result } = await this.chain.dispatch('tool.call', e, async () => ({ deny: `no hook of ${this.name} answers the tool ${name}` }), this.dispatchOptions({ signal: ctx?.signal }));
232
+ if (result !== null && typeof result === 'object' && typeof result.deny === 'string') {
233
+ throw new ModError('[TOOL_DENIED]', result.deny);
234
+ }
235
+ return result?.result ?? result ?? null;
236
+ }
237
+
238
+ async serveCommand(name, request, ctx) {
239
+ this.scopeCtx ??= ctx;
240
+ const answer = await this.runCommand({ command: name, args: request?.rest ?? '', raw: request?.raw ?? `/${name}`, surface: request?.surface }, ctx);
241
+ return typeof answer === 'string' ? answer : answer?.text ?? '';
242
+ }
243
+
244
+ async runCommand({ command, args, raw, surface }, ctx) {
245
+ const e = {
246
+ command,
247
+ args: args ?? '',
248
+ raw: raw ?? `/${command}${args ? ` ${args}` : ''}`,
249
+ origin: { kind: 'user' },
250
+ presentation: { surface: SURFACES.has(surface) ? surface : 'tui', isFullscreen: surface !== 'tui' },
251
+ };
252
+ const { result } = await this.chain.dispatch('command.run', e, async () => ({ text: '' }), this.dispatchOptions({ signal: ctx?.signal }));
253
+ if (typeof result === 'string') return { text: result };
254
+ if (result && typeof result === 'object') return { text: typeof result.text === 'string' ? result.text : '', context: result.context };
255
+ return { text: '' };
256
+ }
257
+
258
+ async render({ component, surface, requestId, props, viewport }) {
259
+ const e = { component, surface: surface ?? 'terminal', requestId, props: props ?? {}, viewport: viewport ?? undefined };
260
+ const engineDraws = Symbol('engine');
261
+ const { result } = await this.chain.dispatch('ui.render', e, async () => engineDraws, this.dispatchOptions());
262
+ if (result === engineDraws || result === undefined || result === null) {
263
+ this.clients.forget(requestId);
264
+ return { engine: true };
265
+ }
266
+ if (isElement(result)) {
267
+ const context = { component, surface: e.surface, viewport: e.viewport, requestId };
268
+ await this.clients.prepare(requestId, result, context);
269
+ const drawn = new Set();
270
+ const tree = this.handlers.serialize(requestId, result, (node) => {
271
+ drawn.add(String(node.props.key ?? ''));
272
+ return this.clients.draw(requestId, node, context);
273
+ });
274
+ this.clients.sweep(requestId, drawn);
275
+ return { tree };
276
+ }
277
+ if (typeof result === 'string') {
278
+ this.clients.forget(requestId);
279
+ return { tree: { type: 'Text', props: {}, children: [result] } };
280
+ }
281
+ return { error: `the ui.render hook of ${this.name} answered ${typeof result}, not a tree` };
282
+ }
283
+
284
+ async act(kind, request) {
285
+ const { component, surface, requestId, element } = request;
286
+ const base = { plugin: this.name, element, component, surface: surface ?? 'terminal', requestId };
287
+ const handlerName = kind === 'press' ? 'onPress' : kind === 'select' ? 'onSelect' : request.inputKind === 'change' ? 'onInput' : 'onSubmit';
288
+ const core = async (final) => {
289
+ const handler = this.handlers.handler(requestId, element, handlerName) ?? (kind === 'press' ? this.handlers.handler(requestId, element, 'onLinkPress') : undefined);
290
+ if (typeof handler === 'function') {
291
+ try {
292
+ await handler(kind === 'press' ? { element, href: final.href } : final.value, final);
293
+ } catch (error) {
294
+ this.report(`ui.${kind} (${handlerName} of ${element})`, error);
295
+ }
296
+ }
297
+ return kind === 'press' ? { element } : { element, value: final.value };
298
+ };
299
+ const e = kind === 'press'
300
+ ? { ...base, href: request.href }
301
+ : kind === 'input'
302
+ ? { ...base, kind: request.inputKind ?? 'submit', value: request.value ?? '' }
303
+ : { ...base, value: request.value };
304
+ const { result } = await this.chain.dispatch(`ui.${kind}`, e, core, this.dispatchOptions());
305
+ return result ?? {};
306
+ }
307
+
308
+ /// The site's focus ring about to move onto `element` (absent: off the
309
+ /// mod's elements), raised as `ui.focus`: `{ deny }` keeps the ring where
310
+ /// it is, else `{ element }` is where it lands, which a hook may rewrite.
311
+ async focus(request) {
312
+ const e = {
313
+ component: request.component,
314
+ requestId: request.requestId,
315
+ origin: request.origin?.kind === 'plugin' ? { kind: 'plugin', name: String(request.origin.name ?? this.name) } : { kind: 'person' },
316
+ };
317
+ if (typeof request.element === 'string') {
318
+ e.plugin = this.name;
319
+ e.element = request.element;
320
+ }
321
+ const { result } = await this.chain.dispatch('ui.focus', e, async (final) => ({ element: final.element ?? null }), this.dispatchOptions());
322
+ if (result && typeof result.deny === 'string') return { deny: result.deny };
323
+ const element = result && 'element' in result ? result.element : e.element ?? null;
324
+ return { element: typeof element === 'string' ? element : null };
325
+ }
326
+
327
+ describe() {
328
+ return {
329
+ name: this.name,
330
+ version: this.version ?? null,
331
+ root: this.root,
332
+ patterns: this.chain.patterns(),
333
+ commands: this.declaredCommands.map((command) => command.name),
334
+ tools: this.declaredTools.map((tool) => tool.name),
335
+ pendingToolCalls: this.pendingCalls.size,
336
+ timers: this.timers.size,
337
+ clients: this.clients.size,
338
+ };
339
+ }
340
+
341
+ async handleService(request, ctx) {
342
+ if (request === null || typeof request !== 'object') throw new ModError('[WRONG_SHAPE]', 'a mod service request is an object with a kind');
343
+ if (ctx && !this.scopeCtx) this.scopeCtx = ctx;
344
+ switch (request.kind) {
345
+ case 'classic': {
346
+ if (request.input?.cwd) this.facts.cwd = request.input.cwd;
347
+ if (request.input?.session_id) this.facts.sessionId = request.input.session_id;
348
+ return dispatchClassic(this, request);
349
+ }
350
+ case 'facts':
351
+ Object.assign(this.facts, request.facts ?? {});
352
+ return {};
353
+ case 'render': return this.render(request);
354
+ case 'press': return this.act('press', request);
355
+ case 'input': return this.act('input', request);
356
+ case 'select': return this.act('select', request);
357
+ case 'command': return this.runCommand(request, ctx);
358
+ case 'describe': return this.describe();
359
+ case 'forget':
360
+ this.handlers.forget(request.requestId);
361
+ this.clients.forget(request.requestId);
362
+ return {};
363
+ case 'focus': return this.focus(request);
364
+ case 'clientKey': return this.clients.key(request.requestId, request.element, request.key ?? {});
365
+ case 'clientPointer': return this.clients.pointer(request.requestId, request.element, request.pointer ?? {});
366
+ default:
367
+ throw new ModError('[UNKNOWN_KIND]', `mod service does not know the kind ${JSON.stringify(request.kind)}`);
368
+ }
369
+ }
370
+ }
@@ -0,0 +1,122 @@
1
+ // What a render hook draws with, and what leaves the process.
2
+ //
3
+ // `$.ui.resolve(e)` answers the element table of the surface `e` is drawn
4
+ // on: a frozen object of constructors, the JSX tags a hook destructures.
5
+ // Each constructor builds plain data. Before a tree crosses to rebon it is
6
+ // serialised here: handler props are taken off and remembered by the
7
+ // element's `key` for the instance (`requestId`) that drew them, so a press
8
+ // reported by the surface finds its closure, and everything else is JSON.
9
+ //
10
+ // The tables mirror `Elements` in the Claude Code declarations: the
11
+ // terminal alone has `Raster` and `Image`, the mobile table has no `Input`,
12
+ // `Select` or `Client`, and `vscode` has no `Client`. A surface that cannot
13
+ // draw an element it is handed says so on its own side; the table here is
14
+ // what lets `e.surface` narrow the constructors a hook may reach for.
15
+ import { ELEMENT, normalizeChildren } from './claude-code.mjs';
16
+
17
+ const HANDLER_PROPS = Object.freeze(['onPress', 'onInput', 'onSubmit', 'onSelect', 'onLinkPress', 'onChange']);
18
+
19
+ const TABLES = Object.freeze({
20
+ terminal: ['Box', 'Text', 'Button', 'Input', 'Select', 'Link', 'Code', 'Markdown', 'Client', 'Raster', 'Image'],
21
+ desktop: ['Box', 'Text', 'Button', 'Input', 'Select', 'Svg', 'Link', 'Code', 'Markdown', 'Client'],
22
+ mobile: ['Box', 'Text', 'Button', 'Svg', 'Link', 'Code', 'Markdown'],
23
+ vscode: ['Box', 'Text', 'Button', 'Input', 'Select', 'Svg', 'Link', 'Code', 'Markdown'],
24
+ });
25
+
26
+ export const SURFACES = Object.freeze(Object.keys(TABLES));
27
+
28
+ function constructor(type) {
29
+ const build = (props = {}) => {
30
+ const { children, ...rest } = props;
31
+ return { [ELEMENT]: true, type, props: rest, children: normalizeChildren(children) };
32
+ };
33
+ Object.defineProperty(build, 'name', { value: type });
34
+ return build;
35
+ }
36
+
37
+ const tables = new Map();
38
+ const clientTables = new Map();
39
+
40
+ /// The frozen element table for `surface`; `terminal` when unknown.
41
+ export function elementTable(surface) {
42
+ const name = TABLES[surface] ? surface : 'terminal';
43
+ if (!tables.has(name)) {
44
+ const table = {};
45
+ for (const type of TABLES[name]) table[type] = constructor(type);
46
+ tables.set(name, Object.freeze(table));
47
+ }
48
+ return tables.get(name);
49
+ }
50
+
51
+ /// The table a `Client`'s surface module draws with: its surface's,
52
+ /// without `Client` (a surface module draws no Client of its own).
53
+ export function clientElementTable(surface) {
54
+ const name = TABLES[surface] ? surface : 'terminal';
55
+ if (!clientTables.has(name)) {
56
+ const { Client: _client, ...rest } = elementTable(name);
57
+ clientTables.set(name, Object.freeze(rest));
58
+ }
59
+ return clientTables.get(name);
60
+ }
61
+
62
+ /// Remembers handler closures per drawn instance.
63
+ export class HandlerRegistry {
64
+ #byInstance = new Map();
65
+
66
+ /// Serialises `tree` for `requestId`, keeping its handlers here. A
67
+ /// `Client` is handed to `drawClient`, and what that returns is drawn as
68
+ /// its one child; without it a Client goes out undrawn.
69
+ serialize(requestId, tree, drawClient) {
70
+ const handlers = new Map();
71
+ let autoKey = 0;
72
+ const walk = (node) => {
73
+ if (typeof node === 'string') return node;
74
+ if (!node || !node[ELEMENT]) return String(node);
75
+ if (node.type === 'Client') {
76
+ const props = { key: String(node.props.key ?? ''), module: String(node.props.module ?? '') };
77
+ for (const name of ['width', 'height', 'flexGrow']) {
78
+ if (node.props[name] !== undefined) props[name] = node.props[name];
79
+ }
80
+ const drawn = typeof drawClient === 'function' ? drawClient(node) : undefined;
81
+ return { type: 'Client', props, children: drawn === undefined ? [] : [walk(drawn)] };
82
+ }
83
+ const props = {};
84
+ let key = node.props.key;
85
+ const own = {};
86
+ for (const [name, value] of Object.entries(node.props)) {
87
+ if (HANDLER_PROPS.includes(name) && typeof value === 'function') { own[name] = value; continue; }
88
+ if (value === undefined) continue;
89
+ if (typeof value === 'function') continue;
90
+ props[name] = value;
91
+ }
92
+ if (Object.keys(own).length > 0) {
93
+ if (key === undefined || key === null || key === '') {
94
+ key = `auto-${++autoKey}`;
95
+ props.key = key;
96
+ }
97
+ handlers.set(String(key), own);
98
+ }
99
+ return { type: node.type, props, children: node.children.map(walk) };
100
+ };
101
+ const serialized = walk(tree);
102
+ this.#byInstance.set(requestId, handlers);
103
+ return serialized;
104
+ }
105
+
106
+ handler(requestId, key, name) {
107
+ return this.#byInstance.get(requestId)?.get(String(key))?.[name];
108
+ }
109
+
110
+ forget(requestId) {
111
+ this.#byInstance.delete(requestId);
112
+ }
113
+
114
+ clear() {
115
+ this.#byInstance.clear();
116
+ }
117
+ }
118
+
119
+ /// Whether `value` is an element a constructor built (a tree's root).
120
+ export function isElement(value) {
121
+ return Boolean(value && typeof value === 'object' && value[ELEMENT]);
122
+ }
@@ -0,0 +1,167 @@
1
+ // Loading a mod's files: the module hooks that make `.tsx` and the bare
2
+ // `claude-code` specifier resolve.
3
+ //
4
+ // A hooks module "is an ES module whatever its suffix", may be TypeScript,
5
+ // may hold JSX, and imports `claude-code` for its state helpers (at run time
6
+ // the type imports are empty, the value imports are not). None of that is
7
+ // something Node does by itself, so this installs synchronous module hooks
8
+ // (`module.registerHooks`) that answer for files under a registered mod root
9
+ // and hand everything else on untouched: a Cordis entry or the host's own
10
+ // files never pass through here.
11
+ //
12
+ // Order of operations on a `.tsx`: JSX to `h()` calls first
13
+ // (`jsx-transform.mjs`), then Node's own `stripTypeScriptTypes` in
14
+ // `transform` mode — the JSX is what the type stripper refuses, so it has to
15
+ // be gone before the stripper sees the file.
16
+ import { readFileSync, realpathSync, statSync } from 'node:fs';
17
+ import { registerHooks, stripTypeScriptTypes } from 'node:module';
18
+ import path from 'node:path';
19
+ import { fileURLToPath, pathToFileURL } from 'node:url';
20
+ import { transformJsx } from './jsx-transform.mjs';
21
+
22
+ /// The one bare specifier a mod imports: the Claude Code runtime module.
23
+ export const CLAUDE_CODE_SPECIFIER = 'claude-code';
24
+
25
+ const SHIM_URL = new URL('./claude-code.mjs', import.meta.url).href;
26
+
27
+ /// The suffixes a mod's files may carry. A file named otherwise is not a
28
+ /// mod file, and an import of one is refused rather than guessed at.
29
+ export const MOD_EXTENSIONS = Object.freeze(['.ts', '.tsx', '.jsx', '.js', '.mjs', '.cjs', '.mts', '.cts']);
30
+
31
+ /// The suffixes tried, in order, for a relative import written without one
32
+ /// (`./card`), as TypeScript and Claude Code resolve it.
33
+ const IMPLIED_EXTENSIONS = Object.freeze(['.ts', '.tsx', '.mts', '.js', '.jsx', '.mjs']);
34
+
35
+ /// For a `.js`-family import with no file behind it, the TypeScript source
36
+ /// it was written against: `./util.js` naming `./util.ts`.
37
+ const SOURCE_FOR = Object.freeze({ '.js': ['.ts', '.tsx'], '.jsx': ['.tsx'], '.mjs': ['.mts'], '.cjs': ['.cts'] });
38
+
39
+ function isFile(filename) {
40
+ try {
41
+ return statSync(filename).isFile();
42
+ } catch {
43
+ return false;
44
+ }
45
+ }
46
+
47
+ /// The mod file a relative import names: the file itself when it is one,
48
+ /// else the TypeScript resolution of an import written without a suffix
49
+ /// (`./card` → `./card.ts`, `./dir` → `./dir/index.ts`) or with the
50
+ /// emitted one (`./util.js` → `./util.ts`). `null` when none exists.
51
+ export function resolveModFile(filename) {
52
+ const extension = path.extname(filename);
53
+ if (MOD_EXTENSIONS.includes(extension) && isFile(filename)) return filename;
54
+ for (const source of SOURCE_FOR[extension] ?? []) {
55
+ const candidate = filename.slice(0, -extension.length) + source;
56
+ if (isFile(candidate)) return candidate;
57
+ }
58
+ for (const implied of IMPLIED_EXTENSIONS) {
59
+ if (isFile(filename + implied)) return filename + implied;
60
+ }
61
+ for (const implied of IMPLIED_EXTENSIONS) {
62
+ const index = path.join(filename, `index${implied}`);
63
+ if (isFile(index)) return index;
64
+ }
65
+ return null;
66
+ }
67
+
68
+ const roots = new Set();
69
+ let installed = false;
70
+
71
+ /// A mod root as the hooks see its files: Node resolves a module to where it
72
+ /// really is, so a root reached through a link is also known by that.
73
+ function rootUrls(root) {
74
+ const given = path.resolve(root);
75
+ let real = given;
76
+ try {
77
+ real = realpathSync(given);
78
+ } catch {
79
+ // Not there (yet): only the path as given can name its files.
80
+ }
81
+ return [...new Set([given, real])].map((dir) => {
82
+ const url = pathToFileURL(dir).href;
83
+ return url.endsWith('/') ? url : `${url}/`;
84
+ });
85
+ }
86
+
87
+ /// Says that files under `root` are a mod's, so the hooks answer for them.
88
+ export function registerModRoot(root) {
89
+ for (const url of rootUrls(root)) roots.add(url);
90
+ install();
91
+ }
92
+
93
+ export function unregisterModRoot(root) {
94
+ for (const url of rootUrls(root)) roots.delete(url);
95
+ }
96
+
97
+ function underModRoot(url) {
98
+ for (const root of roots) if (url.startsWith(root)) return true;
99
+ return false;
100
+ }
101
+
102
+ /// Whether `source` holds TypeScript syntax the stripper has to remove.
103
+ function needsTypeStrip(extension) {
104
+ return extension === '.ts' || extension === '.tsx' || extension === '.mts' || extension === '.cts';
105
+ }
106
+
107
+ /// Turns one mod file into the JavaScript module Node evaluates.
108
+ export function transpileModSource(source, filename) {
109
+ const extension = path.extname(filename);
110
+ let code = source;
111
+ if (extension === '.tsx' || extension === '.jsx') code = transformJsx(code);
112
+ if (needsTypeStrip(extension)) {
113
+ code = stripTypeScriptTypes(code, { mode: 'transform', sourceUrl: pathToFileURL(filename).href });
114
+ }
115
+ return code;
116
+ }
117
+
118
+ function install() {
119
+ if (installed) return;
120
+ installed = true;
121
+ // `stripTypeScriptTypes` announces itself as experimental once per process
122
+ // on stderr, where the host expects one JSON diagnostic per line. The
123
+ // warning is known and the API is pinned by this package's tests; keep it
124
+ // off the host's diagnostics stream.
125
+ const emitWarning = process.emitWarning;
126
+ process.emitWarning = function filtered(warning, ...rest) {
127
+ if (String(warning?.message ?? warning).includes('stripTypeScriptTypes')) return undefined;
128
+ return emitWarning.call(process, warning, ...rest);
129
+ };
130
+ registerHooks({
131
+ resolve(specifier, context, nextResolve) {
132
+ const parent = context.parentURL ?? '';
133
+ if (specifier === CLAUDE_CODE_SPECIFIER && underModRoot(parent)) {
134
+ return { url: SHIM_URL, format: 'module', shortCircuit: true };
135
+ }
136
+ if (underModRoot(parent) && (specifier.startsWith('./') || specifier.startsWith('../'))) {
137
+ const url = new URL(specifier, parent).href;
138
+ if (!underModRoot(url)) {
139
+ throw new Error(`[PATH_ESCAPES] ${specifier} resolves outside the mod's folder`);
140
+ }
141
+ const resolved = resolveModFile(fileURLToPath(url));
142
+ if (resolved === null) {
143
+ const extension = path.extname(fileURLToPath(url));
144
+ if (MOD_EXTENSIONS.includes(extension)) return nextResolve(specifier, context); // not there: Node says so
145
+ throw new Error(`[NOT_A_MOD_FILE] ${specifier} is not named .ts, .tsx, .jsx, .js, .mjs, .cjs, .mts or .cts, and no such file with one exists`);
146
+ }
147
+ const target = pathToFileURL(resolved).href;
148
+ if (!underModRoot(target)) {
149
+ throw new Error(`[PATH_ESCAPES] ${specifier} resolves outside the mod's folder`);
150
+ }
151
+ return { url: target, format: 'module', shortCircuit: true };
152
+ }
153
+ return nextResolve(specifier, context);
154
+ },
155
+ load(url, context, nextLoad) {
156
+ if (!underModRoot(url)) return nextLoad(url, context);
157
+ const filename = fileURLToPath(url);
158
+ const extension = path.extname(filename);
159
+ if (!MOD_EXTENSIONS.includes(extension)) return nextLoad(url, context);
160
+ // Read directly rather than through `nextLoad`: the default loader
161
+ // decides a `.ts` or `.cjs` file's format from its name, and the rule
162
+ // here is that a mod's file is an ES module whatever it is named.
163
+ const source = readFileSync(filename, 'utf8');
164
+ return { format: 'module', source: transpileModSource(source, filename), shortCircuit: true };
165
+ },
166
+ });
167
+ }
@@ -143,5 +143,18 @@ if (import.meta.url === pathToFileURL(process.argv[1]).href) {
143
143
  process.exitCode = await runHost({
144
144
  input: process.stdin, output: process.stdout, error: process.stderr, loader,
145
145
  });
146
+ // Once the host returns there is nobody left to serve: rebon shut it
147
+ // down, or rebon is gone and stdin ended. A plugin's timer or open handle
148
+ // must not keep the process alive past that — it would outlive rebon as
149
+ // an orphan holding whatever the plugin opened. What was already written
150
+ // is flushed first; a stream that cannot flush does not hold the exit.
151
+ const flushed = (stream) => new Promise((resolve) => {
152
+ if (stream.destroyed || !stream.writable) return resolve();
153
+ stream.once('error', () => resolve());
154
+ try { stream.write('', () => resolve()); } catch { resolve(); }
155
+ });
156
+ const grace = new Promise((resolve) => setTimeout(resolve, 1000).unref());
157
+ await Promise.race([Promise.all([flushed(process.stdout), flushed(process.stderr)]), grace]);
158
+ process.exit();
146
159
  }
147
160
  }