@hullcode/plugin 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,34 @@
1
+ Hull Code Proprietary Notice
2
+
3
+ Hull-authored portions of this distribution are proprietary and are not
4
+ offered for public contribution or relicensed by this notice. All rights
5
+ reserved.
6
+
7
+ This distribution includes software derived from third-party projects. The
8
+ following MIT license and copyright notices apply to those inherited portions
9
+ and must remain with copies or substantial portions of that software.
10
+
11
+ ---
12
+
13
+ MIT License
14
+
15
+ Copyright (c) 2026 Kilo Code
16
+ Copyright (c) 2025 opencode
17
+
18
+ Permission is hereby granted, free of charge, to any person obtaining a copy
19
+ of this software and associated documentation files (the "Software"), to deal
20
+ in the Software without restriction, including without limitation the rights
21
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
22
+ copies of the Software, and to permit persons to whom the Software is
23
+ furnished to do so, subject to the following conditions:
24
+
25
+ The above copyright notice and this permission notice shall be included in all
26
+ copies or substantial portions of the Software.
27
+
28
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
29
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
30
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
31
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
32
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
33
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
34
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ import type { Plugin } from "@hullcode/plugin";
2
+ export declare const FolderWorkspacePlugin: Plugin;
3
+ export default FolderWorkspacePlugin;
@@ -0,0 +1,30 @@
1
+ import { mkdir, rm } from "node:fs/promises";
2
+ export const FolderWorkspacePlugin = async ({ experimental_workspace }) => {
3
+ experimental_workspace.register("folder", {
4
+ name: "Folder",
5
+ description: "Create a blank folder",
6
+ configure(config) {
7
+ const rand = "" + Math.random();
8
+ return {
9
+ ...config,
10
+ directory: `/tmp/folder/folder-${rand}`,
11
+ };
12
+ },
13
+ async create(config) {
14
+ if (!config.directory)
15
+ return;
16
+ await mkdir(config.directory, { recursive: true });
17
+ },
18
+ async remove(config) {
19
+ await rm(config.directory, { recursive: true, force: true });
20
+ },
21
+ target(config) {
22
+ return {
23
+ type: "local",
24
+ directory: config.directory,
25
+ };
26
+ },
27
+ });
28
+ return {};
29
+ };
30
+ export default FolderWorkspacePlugin;
@@ -0,0 +1,2 @@
1
+ import { Plugin } from "./index.js";
2
+ export declare const ExamplePlugin: Plugin;
@@ -0,0 +1,16 @@
1
+ import { tool } from "./tool.js";
2
+ export const ExamplePlugin = async (_ctx) => {
3
+ return {
4
+ tool: {
5
+ mytool: tool({
6
+ description: "This is a custom tool",
7
+ args: {
8
+ foo: tool.schema.string().describe("foo"),
9
+ },
10
+ async execute(args) {
11
+ return `Hello ${args.foo}!`;
12
+ },
13
+ }),
14
+ },
15
+ };
16
+ };
@@ -0,0 +1,322 @@
1
+ import type { Event, createHullClient, Project, Model, Provider, Permission, UserMessage, Message, Part, Config as SDKConfig } from "@hullcode/sdk";
2
+ import type { Provider as ProviderV2, Model as ModelV2, Auth } from "@hullcode/sdk/v2";
3
+ import type { BunShell } from "./shell.js";
4
+ import { type ToolDefinition } from "./tool.js";
5
+ export * from "./tool.js";
6
+ export type ProviderContext = {
7
+ source: "env" | "config" | "custom" | "api";
8
+ info: Provider;
9
+ options: Record<string, any>;
10
+ };
11
+ export type WorkspaceInfo = {
12
+ id: string;
13
+ type: string;
14
+ name: string;
15
+ branch: string | null;
16
+ directory: string | null;
17
+ extra: unknown | null;
18
+ projectID: string;
19
+ };
20
+ export type WorkspaceTarget = {
21
+ type: "local";
22
+ directory: string;
23
+ } | {
24
+ type: "remote";
25
+ url: string | URL;
26
+ headers?: HeadersInit;
27
+ };
28
+ export type WorkspaceAdapter = {
29
+ name: string;
30
+ description: string;
31
+ configure(config: WorkspaceInfo): WorkspaceInfo | Promise<WorkspaceInfo>;
32
+ create(config: WorkspaceInfo, env: Record<string, string | undefined>, from?: WorkspaceInfo): Promise<void>;
33
+ remove(config: WorkspaceInfo): Promise<void>;
34
+ target(config: WorkspaceInfo): WorkspaceTarget | Promise<WorkspaceTarget>;
35
+ };
36
+ export type PluginInput = {
37
+ client: ReturnType<typeof createHullClient>;
38
+ project: Project;
39
+ directory: string;
40
+ worktree: string;
41
+ experimental_workspace: {
42
+ register(type: string, adapter: WorkspaceAdapter): void;
43
+ };
44
+ serverUrl: URL;
45
+ $: BunShell;
46
+ };
47
+ export type PluginOptions = Record<string, unknown>;
48
+ export type Config = Omit<SDKConfig, "plugin"> & {
49
+ plugin?: Array<string | [string, PluginOptions]>;
50
+ };
51
+ export type Plugin = (input: PluginInput, options?: PluginOptions) => Promise<Hooks>;
52
+ export type PluginModule = {
53
+ id?: string;
54
+ server: Plugin;
55
+ tui?: never;
56
+ };
57
+ type Rule = {
58
+ key: string;
59
+ op: "eq" | "neq";
60
+ value: string;
61
+ };
62
+ export type AuthHook = {
63
+ provider: string;
64
+ loader?: (auth: () => Promise<Auth>, provider: Provider) => Promise<Record<string, any>>;
65
+ methods: ({
66
+ type: "oauth";
67
+ label: string;
68
+ prompts?: Array<{
69
+ type: "text";
70
+ key: string;
71
+ message: string;
72
+ placeholder?: string;
73
+ validate?: (value: string) => string | undefined;
74
+ /** @deprecated Use `when` instead */
75
+ condition?: (inputs: Record<string, string>) => boolean;
76
+ when?: Rule;
77
+ } | {
78
+ type: "select";
79
+ key: string;
80
+ message: string;
81
+ options: Array<{
82
+ label: string;
83
+ value: string;
84
+ hint?: string;
85
+ }>;
86
+ /** @deprecated Use `when` instead */
87
+ condition?: (inputs: Record<string, string>) => boolean;
88
+ when?: Rule;
89
+ }>;
90
+ authorize(inputs?: Record<string, string>): Promise<AuthOAuthResult>;
91
+ } | {
92
+ type: "api";
93
+ label: string;
94
+ prompts?: Array<{
95
+ type: "text";
96
+ key: string;
97
+ message: string;
98
+ placeholder?: string;
99
+ validate?: (value: string) => string | undefined;
100
+ /** @deprecated Use `when` instead */
101
+ condition?: (inputs: Record<string, string>) => boolean;
102
+ when?: Rule;
103
+ } | {
104
+ type: "select";
105
+ key: string;
106
+ message: string;
107
+ options: Array<{
108
+ label: string;
109
+ value: string;
110
+ hint?: string;
111
+ }>;
112
+ /** @deprecated Use `when` instead */
113
+ condition?: (inputs: Record<string, string>) => boolean;
114
+ when?: Rule;
115
+ }>;
116
+ authorize?(inputs?: Record<string, string>): Promise<{
117
+ type: "success";
118
+ key: string;
119
+ provider?: string;
120
+ metadata?: Record<string, string>;
121
+ } | {
122
+ type: "failed";
123
+ }>;
124
+ })[];
125
+ };
126
+ export type AuthOAuthResult = {
127
+ url: string;
128
+ instructions: string;
129
+ } & ({
130
+ method: "auto";
131
+ callback(): Promise<({
132
+ type: "success";
133
+ provider?: string;
134
+ } & ({
135
+ refresh: string;
136
+ access: string;
137
+ expires: number;
138
+ accountId?: string;
139
+ enterpriseUrl?: string;
140
+ } | {
141
+ key: string;
142
+ metadata?: Record<string, string>;
143
+ })) | {
144
+ type: "failed";
145
+ }>;
146
+ } | {
147
+ method: "code";
148
+ callback(code: string): Promise<({
149
+ type: "success";
150
+ provider?: string;
151
+ } & ({
152
+ refresh: string;
153
+ access: string;
154
+ expires: number;
155
+ accountId?: string;
156
+ enterpriseUrl?: string;
157
+ } | {
158
+ key: string;
159
+ metadata?: Record<string, string>;
160
+ })) | {
161
+ type: "failed";
162
+ }>;
163
+ });
164
+ export type ProviderHookContext = {
165
+ auth?: Auth;
166
+ };
167
+ export type ProviderHook = {
168
+ id: string;
169
+ models?: (provider: ProviderV2, ctx: ProviderHookContext) => Promise<Record<string, ModelV2>>;
170
+ };
171
+ /** @deprecated Use AuthOAuthResult instead. */
172
+ export type AuthOuathResult = AuthOAuthResult;
173
+ export interface Hooks {
174
+ dispose?: () => Promise<void>;
175
+ event?: (input: {
176
+ event: Event;
177
+ }) => Promise<void>;
178
+ config?: (input: Config) => Promise<void>;
179
+ tool?: {
180
+ [key: string]: ToolDefinition;
181
+ };
182
+ auth?: AuthHook;
183
+ provider?: ProviderHook;
184
+ /**
185
+ * Called when a new message is received
186
+ */
187
+ "chat.message"?: (input: {
188
+ sessionID: string;
189
+ agent?: string;
190
+ model?: {
191
+ providerID: string;
192
+ modelID: string;
193
+ };
194
+ messageID?: string;
195
+ variant?: string;
196
+ }, output: {
197
+ message: UserMessage;
198
+ parts: Part[];
199
+ }) => Promise<void>;
200
+ /**
201
+ * Modify parameters sent to LLM
202
+ */
203
+ "chat.params"?: (input: {
204
+ sessionID: string;
205
+ agent: string;
206
+ model: Model;
207
+ provider: ProviderContext;
208
+ message: UserMessage;
209
+ }, output: {
210
+ temperature: number;
211
+ topP: number;
212
+ topK: number;
213
+ maxOutputTokens: number | undefined;
214
+ options: Record<string, any>;
215
+ }) => Promise<void>;
216
+ "chat.headers"?: (input: {
217
+ sessionID: string;
218
+ agent: string;
219
+ model: Model;
220
+ provider: ProviderContext;
221
+ message: UserMessage;
222
+ }, output: {
223
+ headers: Record<string, string>;
224
+ }) => Promise<void>;
225
+ "permission.ask"?: (input: Permission, output: {
226
+ status: "ask" | "deny" | "allow";
227
+ }) => Promise<void>;
228
+ "command.execute.before"?: (input: {
229
+ command: string;
230
+ sessionID: string;
231
+ arguments: string;
232
+ }, output: {
233
+ parts: Part[];
234
+ }) => Promise<void>;
235
+ "tool.execute.before"?: (input: {
236
+ tool: string;
237
+ sessionID: string;
238
+ callID: string;
239
+ }, output: {
240
+ args: any;
241
+ }) => Promise<void>;
242
+ "shell.env"?: (input: {
243
+ cwd: string;
244
+ sessionID?: string;
245
+ callID?: string;
246
+ }, output: {
247
+ env: Record<string, string>;
248
+ }) => Promise<void>;
249
+ "tool.execute.after"?: (input: {
250
+ tool: string;
251
+ sessionID: string;
252
+ callID: string;
253
+ args: any;
254
+ }, output: {
255
+ title: string;
256
+ output: string;
257
+ metadata: any;
258
+ }) => Promise<void>;
259
+ "experimental.chat.messages.transform"?: (input: {}, output: {
260
+ messages: {
261
+ info: Message;
262
+ parts: Part[];
263
+ }[];
264
+ }) => Promise<void>;
265
+ "experimental.chat.system.transform"?: (input: {
266
+ sessionID?: string;
267
+ model: Model;
268
+ }, output: {
269
+ system: string[];
270
+ }) => Promise<void>;
271
+ "experimental.provider.small_model"?: (input: {
272
+ provider: ProviderV2;
273
+ }, output: {
274
+ model?: ModelV2;
275
+ }) => Promise<void>;
276
+ /**
277
+ * Called before session compaction starts. Allows plugins to customize
278
+ * the compaction prompt.
279
+ *
280
+ * - `context`: Additional context strings appended to the default prompt
281
+ * - `prompt`: If set, replaces the default compaction prompt entirely
282
+ */
283
+ "experimental.session.compacting"?: (input: {
284
+ sessionID: string;
285
+ }, output: {
286
+ context: string[];
287
+ prompt?: string;
288
+ }) => Promise<void>;
289
+ /**
290
+ * Called after compaction succeeds and before a synthetic user
291
+ * auto-continue message is added.
292
+ *
293
+ * - `enabled`: Defaults to `true`. Set to `false` to skip the synthetic
294
+ * user "continue" turn.
295
+ */
296
+ "experimental.compaction.autocontinue"?: (input: {
297
+ sessionID: string;
298
+ agent: string;
299
+ model: Model;
300
+ provider: ProviderContext;
301
+ message: UserMessage;
302
+ overflow: boolean;
303
+ }, output: {
304
+ enabled: boolean;
305
+ }) => Promise<void>;
306
+ "experimental.text.complete"?: (input: {
307
+ sessionID: string;
308
+ messageID: string;
309
+ partID: string;
310
+ }, output: {
311
+ text: string;
312
+ }) => Promise<void>;
313
+ /**
314
+ * Modify tool definitions (description and parameters) sent to LLM
315
+ */
316
+ "tool.definition"?: (input: {
317
+ toolID: string;
318
+ }, output: {
319
+ description: string;
320
+ parameters: any;
321
+ }) => Promise<void>;
322
+ }
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export * from "./tool.js";
@@ -0,0 +1,109 @@
1
+ export type ShellFunction = (input: Uint8Array) => Uint8Array;
2
+ export type ShellExpression = {
3
+ toString(): string;
4
+ } | Array<ShellExpression> | string | {
5
+ raw: string;
6
+ } | ReadableStream;
7
+ export interface BunShell {
8
+ (strings: TemplateStringsArray, ...expressions: ShellExpression[]): BunShellPromise;
9
+ /**
10
+ * Perform bash-like brace expansion on the given pattern.
11
+ * @param pattern - Brace pattern to expand
12
+ */
13
+ braces(pattern: string): string[];
14
+ /**
15
+ * Escape strings for input into shell commands.
16
+ */
17
+ escape(input: string): string;
18
+ /**
19
+ * Change the default environment variables for shells created by this instance.
20
+ */
21
+ env(newEnv?: Record<string, string | undefined>): BunShell;
22
+ /**
23
+ * Default working directory to use for shells created by this instance.
24
+ */
25
+ cwd(newCwd?: string): BunShell;
26
+ /**
27
+ * Configure the shell to not throw an exception on non-zero exit codes.
28
+ */
29
+ nothrow(): BunShell;
30
+ /**
31
+ * Configure whether or not the shell should throw an exception on non-zero exit codes.
32
+ */
33
+ throws(shouldThrow: boolean): BunShell;
34
+ }
35
+ export interface BunShellPromise extends Promise<BunShellOutput> {
36
+ readonly stdin: WritableStream;
37
+ /**
38
+ * Change the current working directory of the shell.
39
+ */
40
+ cwd(newCwd: string): this;
41
+ /**
42
+ * Set environment variables for the shell.
43
+ */
44
+ env(newEnv: Record<string, string> | undefined): this;
45
+ /**
46
+ * By default, the shell will write to the current process's stdout and stderr, as well as buffering that output.
47
+ * This configures the shell to only buffer the output.
48
+ */
49
+ quiet(): this;
50
+ /**
51
+ * Read from stdout as a string, line by line
52
+ * Automatically calls quiet() to disable echoing to stdout.
53
+ */
54
+ lines(): AsyncIterable<string>;
55
+ /**
56
+ * Read from stdout as a string.
57
+ * Automatically calls quiet() to disable echoing to stdout.
58
+ */
59
+ text(encoding?: BufferEncoding): Promise<string>;
60
+ /**
61
+ * Read from stdout as a JSON object
62
+ * Automatically calls quiet()
63
+ */
64
+ json(): Promise<any>;
65
+ /**
66
+ * Read from stdout as an ArrayBuffer
67
+ * Automatically calls quiet()
68
+ */
69
+ arrayBuffer(): Promise<ArrayBuffer>;
70
+ /**
71
+ * Read from stdout as a Blob
72
+ * Automatically calls quiet()
73
+ */
74
+ blob(): Promise<Blob>;
75
+ /**
76
+ * Configure the shell to not throw an exception on non-zero exit codes.
77
+ */
78
+ nothrow(): this;
79
+ /**
80
+ * Configure whether or not the shell should throw an exception on non-zero exit codes.
81
+ */
82
+ throws(shouldThrow: boolean): this;
83
+ }
84
+ export interface BunShellOutput {
85
+ readonly stdout: Buffer;
86
+ readonly stderr: Buffer;
87
+ readonly exitCode: number;
88
+ /**
89
+ * Read from stdout as a string
90
+ */
91
+ text(encoding?: BufferEncoding): string;
92
+ /**
93
+ * Read from stdout as a JSON object
94
+ */
95
+ json(): any;
96
+ /**
97
+ * Read from stdout as an ArrayBuffer
98
+ */
99
+ arrayBuffer(): ArrayBuffer;
100
+ /**
101
+ * Read from stdout as an Uint8Array
102
+ */
103
+ bytes(): Uint8Array;
104
+ /**
105
+ * Read from stdout as a Blob
106
+ */
107
+ blob(): Blob;
108
+ }
109
+ export type BunShellError = Error & BunShellOutput;
package/dist/shell.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/tool.d.ts ADDED
@@ -0,0 +1,60 @@
1
+ import { z } from "zod";
2
+ export type ToolContext = {
3
+ sessionID: string;
4
+ messageID: string;
5
+ agent: string;
6
+ /**
7
+ * Current project directory for this session.
8
+ * Prefer this over process.cwd() when resolving relative paths.
9
+ */
10
+ directory: string;
11
+ /**
12
+ * Project worktree root for this session.
13
+ * Useful for generating stable relative paths (e.g. path.relative(worktree, absPath)).
14
+ */
15
+ worktree: string;
16
+ abort: AbortSignal;
17
+ metadata(input: {
18
+ title?: string;
19
+ metadata?: {
20
+ [key: string]: any;
21
+ };
22
+ }): void;
23
+ ask(input: AskInput): Promise<void>;
24
+ };
25
+ type AskInput = {
26
+ permission: string;
27
+ patterns: string[];
28
+ always: string[];
29
+ metadata: {
30
+ [key: string]: any;
31
+ };
32
+ };
33
+ export type ToolAttachment = {
34
+ type: "file";
35
+ mime: string;
36
+ url: string;
37
+ filename?: string;
38
+ };
39
+ export type ToolResult = string | {
40
+ title?: string;
41
+ output: string;
42
+ metadata?: {
43
+ [key: string]: any;
44
+ };
45
+ attachments?: ToolAttachment[];
46
+ };
47
+ export declare function tool<Args extends z.ZodRawShape>(input: {
48
+ description: string;
49
+ args: Args;
50
+ execute(args: z.infer<z.ZodObject<Args>>, context: ToolContext): Promise<ToolResult>;
51
+ }): {
52
+ description: string;
53
+ args: Args;
54
+ execute(args: z.infer<z.ZodObject<Args>>, context: ToolContext): Promise<ToolResult>;
55
+ };
56
+ export declare namespace tool {
57
+ var schema: typeof z;
58
+ }
59
+ export type ToolDefinition = ReturnType<typeof tool>;
60
+ export {};
package/dist/tool.js ADDED
@@ -0,0 +1,5 @@
1
+ import { z } from "zod";
2
+ export function tool(input) {
3
+ return input;
4
+ }
5
+ tool.schema = z;
package/dist/tui.d.ts ADDED
@@ -0,0 +1,511 @@
1
+ import type { AgentPart, HullClient, Event, FilePart, LspStatus, McpStatus, Todo, Message, Part, Provider, PermissionRequest, QuestionRequest, Session, SessionStatus, BackgroundProcessInfo, TextPart, Config as SdkConfig } from "@hullcode/sdk/v2";
2
+ import type { CliRenderer, KeyEvent, RGBA, Renderable, SlotMode } from "@opentui/core";
3
+ import type { Binding, Keymap } from "@opentui/keymap";
4
+ import { type BindingConfig, type CreateBindingLookupOptions, type KeySequenceFormatPart, type SequenceBindingLike } from "@opentui/keymap/extras";
5
+ import type { JSX, SolidPlugin } from "@opentui/solid";
6
+ import type { Config as PluginConfig, PluginOptions } from "./index.js";
7
+ export type { CliRenderer, KeyEvent, Renderable, SlotMode } from "@opentui/core";
8
+ export { stringifyKeySequence, stringifyKeyStroke } from "@opentui/keymap";
9
+ export type { Binding, KeyLike, KeySequencePart, KeyStringifyInput, StringifyOptions } from "@opentui/keymap";
10
+ export { formatCommandBindings, formatKeySequence } from "@opentui/keymap/extras";
11
+ export type { BindingConfig, BindingLookup, BindingValue, CreateBindingLookupOptions, FormatCommandBindingsOptions, FormatKeySequenceOptions, KeySequenceFormatPart, SequenceBindingLike, } from "@opentui/keymap/extras";
12
+ export declare function createBindingLookup(config: BindingConfig<Renderable, KeyEvent> | undefined, options?: CreateBindingLookupOptions<Renderable, KeyEvent>): import("@opentui/keymap/extras").BindingLookup<Renderable, KeyEvent>;
13
+ export type TuiRouteCurrent = {
14
+ name: "home";
15
+ } | {
16
+ name: "session";
17
+ params: {
18
+ sessionID: string;
19
+ prompt?: unknown;
20
+ };
21
+ } | {
22
+ name: string;
23
+ params?: Record<string, unknown>;
24
+ };
25
+ export type TuiRouteDefinition = {
26
+ name: string;
27
+ render: (input: {
28
+ params?: Record<string, unknown>;
29
+ }) => JSX.Element;
30
+ };
31
+ export type TuiKeys = {
32
+ formatSequence: (parts: readonly KeySequenceFormatPart[] | undefined) => string;
33
+ formatBindings: (bindings: readonly SequenceBindingLike[] | undefined) => string | undefined;
34
+ };
35
+ export type TuiKeymap = Keymap<Renderable, KeyEvent>;
36
+ export type TuiModeApi = {
37
+ current: () => string;
38
+ push: (mode: string) => () => void;
39
+ };
40
+ /**
41
+ * Legacy `api.command` shape kept so v1 plugins can initialize. Remove in v2.
42
+ *
43
+ * @deprecated Use `api.keymap.registerLayer({ commands, bindings })` instead.
44
+ */
45
+ export type TuiCommand = {
46
+ title: string;
47
+ value: string;
48
+ description?: string;
49
+ category?: string;
50
+ keybind?: string;
51
+ suggested?: boolean;
52
+ hidden?: boolean;
53
+ enabled?: boolean;
54
+ slash?: {
55
+ name: string;
56
+ aliases?: string[];
57
+ };
58
+ onSelect?: (dialog?: TuiDialogStack) => void | Promise<void>;
59
+ };
60
+ /**
61
+ * Legacy `api.command` API kept so v1 plugins can initialize. Remove in v2.
62
+ *
63
+ * @deprecated Use `api.keymap.registerLayer`, `api.keymap.dispatchCommand`, and
64
+ * `api.keymap.dispatchCommand("command.palette.show")` instead.
65
+ */
66
+ export type TuiCommandApi = {
67
+ /** @deprecated Use `api.keymap.registerLayer({ commands, bindings })` instead. */
68
+ register: (cb: () => TuiCommand[]) => () => void;
69
+ /** @deprecated Use `api.keymap.dispatchCommand(name)` instead. */
70
+ trigger: (value: string) => void;
71
+ /** @deprecated Use `api.keymap.dispatchCommand("command.palette.show")` instead. */
72
+ show: () => void;
73
+ };
74
+ export type TuiDialogProps = {
75
+ size?: "medium" | "large" | "xlarge";
76
+ onClose: () => void;
77
+ children?: JSX.Element;
78
+ };
79
+ export type TuiDialogStack = {
80
+ replace: (render: () => JSX.Element, onClose?: () => void) => void;
81
+ clear: () => void;
82
+ setSize: (size: "medium" | "large" | "xlarge") => void;
83
+ readonly size: "medium" | "large" | "xlarge";
84
+ readonly depth: number;
85
+ readonly open: boolean;
86
+ };
87
+ export type TuiDialogAlertProps = {
88
+ title: string;
89
+ message: string;
90
+ onConfirm?: () => void;
91
+ };
92
+ export type TuiDialogConfirmProps = {
93
+ title: string;
94
+ message: string;
95
+ onConfirm?: () => void;
96
+ onCancel?: () => void;
97
+ };
98
+ export type TuiDialogPromptProps = {
99
+ title: string;
100
+ description?: () => JSX.Element;
101
+ placeholder?: string;
102
+ value?: string;
103
+ busy?: boolean;
104
+ busyText?: string;
105
+ onConfirm?: (value: string) => void;
106
+ onCancel?: () => void;
107
+ };
108
+ export type TuiDialogSelectOption<Value = unknown> = {
109
+ title: string;
110
+ value: Value;
111
+ description?: string;
112
+ footer?: JSX.Element | string;
113
+ category?: string;
114
+ disabled?: boolean;
115
+ onSelect?: () => void;
116
+ };
117
+ export type TuiDialogSelectProps<Value = unknown> = {
118
+ title: string;
119
+ placeholder?: string;
120
+ options: TuiDialogSelectOption<Value>[];
121
+ flat?: boolean;
122
+ onMove?: (option: TuiDialogSelectOption<Value>) => void;
123
+ onFilter?: (query: string) => void;
124
+ onSelect?: (option: TuiDialogSelectOption<Value>) => void;
125
+ skipFilter?: boolean;
126
+ current?: Value;
127
+ };
128
+ export type TuiPromptInfo = {
129
+ input: string;
130
+ mode?: "normal" | "shell";
131
+ parts: (Omit<FilePart, "id" | "messageID" | "sessionID"> | Omit<AgentPart, "id" | "messageID" | "sessionID"> | (Omit<TextPart, "id" | "messageID" | "sessionID"> & {
132
+ source?: {
133
+ text: {
134
+ start: number;
135
+ end: number;
136
+ value: string;
137
+ };
138
+ };
139
+ }))[];
140
+ };
141
+ export type TuiPromptRef = {
142
+ focused: boolean;
143
+ current: TuiPromptInfo;
144
+ set(prompt: TuiPromptInfo): void;
145
+ reset(): void;
146
+ blur(): void;
147
+ focus(): void;
148
+ submit(): void;
149
+ };
150
+ export type TuiPromptProps = {
151
+ sessionID?: string;
152
+ visible?: boolean;
153
+ disabled?: boolean;
154
+ onSubmit?: () => void;
155
+ ref?: (ref: TuiPromptRef | undefined) => void;
156
+ hint?: JSX.Element;
157
+ right?: JSX.Element;
158
+ showPlaceholder?: boolean;
159
+ placeholders?: {
160
+ normal?: string[];
161
+ shell?: string[];
162
+ };
163
+ };
164
+ export type TuiToast = {
165
+ variant?: "info" | "success" | "warning" | "error";
166
+ title?: string;
167
+ message: string;
168
+ duration?: number;
169
+ };
170
+ export type TuiAttentionWhen = "always" | "focused" | "blurred";
171
+ export declare const TuiAttentionSoundNames: readonly ["default", "question", "permission", "error", "done", "subagent_done"];
172
+ export type TuiAttentionSoundName = (typeof TuiAttentionSoundNames)[number];
173
+ export type TuiAttentionSound = boolean | {
174
+ name?: TuiAttentionSoundName;
175
+ volume?: number;
176
+ when?: TuiAttentionWhen;
177
+ };
178
+ export type TuiAttentionNotification = boolean | {
179
+ when?: TuiAttentionWhen;
180
+ };
181
+ export type TuiAttentionSoundPack = {
182
+ id: string;
183
+ name?: string;
184
+ sounds: Partial<Record<TuiAttentionSoundName, string>>;
185
+ };
186
+ export type TuiAttentionSoundPackInfo = {
187
+ id: string;
188
+ name?: string;
189
+ active: boolean;
190
+ builtin: boolean;
191
+ };
192
+ export type TuiAttentionSoundboardActivateOptions = {
193
+ persist?: boolean;
194
+ };
195
+ export type TuiAttentionSoundboard = {
196
+ registerPack(pack: TuiAttentionSoundPack): () => void;
197
+ activate(id: string, options?: TuiAttentionSoundboardActivateOptions): boolean;
198
+ current(): string;
199
+ list(): ReadonlyArray<TuiAttentionSoundPackInfo>;
200
+ };
201
+ export type TuiAttentionNotifyInput = {
202
+ title?: string;
203
+ message: string;
204
+ notification?: TuiAttentionNotification;
205
+ sound?: TuiAttentionSound;
206
+ };
207
+ export type TuiAttentionNotifySkipReason = "attention_disabled" | "empty_message" | "blurred" | "focused" | "focus_unknown" | "renderer_destroyed";
208
+ export type TuiAttentionNotifyResult = {
209
+ ok: boolean;
210
+ notification: boolean;
211
+ sound: boolean;
212
+ skipped?: TuiAttentionNotifySkipReason;
213
+ };
214
+ export type TuiAttention = {
215
+ notify(input: TuiAttentionNotifyInput): Promise<TuiAttentionNotifyResult>;
216
+ soundboard: TuiAttentionSoundboard;
217
+ };
218
+ export type TuiThemeCurrent = {
219
+ readonly primary: RGBA;
220
+ readonly secondary: RGBA;
221
+ readonly accent: RGBA;
222
+ readonly error: RGBA;
223
+ readonly warning: RGBA;
224
+ readonly success: RGBA;
225
+ readonly info: RGBA;
226
+ readonly text: RGBA;
227
+ readonly textMuted: RGBA;
228
+ readonly selectedListItemText: RGBA;
229
+ readonly background: RGBA;
230
+ readonly backgroundPanel: RGBA;
231
+ readonly backgroundElement: RGBA;
232
+ readonly backgroundMenu: RGBA;
233
+ readonly border: RGBA;
234
+ readonly borderActive: RGBA;
235
+ readonly borderSubtle: RGBA;
236
+ readonly diffAdded: RGBA;
237
+ readonly diffRemoved: RGBA;
238
+ readonly diffContext: RGBA;
239
+ readonly diffHunkHeader: RGBA;
240
+ readonly diffHighlightAdded: RGBA;
241
+ readonly diffHighlightRemoved: RGBA;
242
+ readonly diffAddedBg: RGBA;
243
+ readonly diffRemovedBg: RGBA;
244
+ readonly diffContextBg: RGBA;
245
+ readonly diffLineNumber: RGBA;
246
+ readonly diffAddedLineNumberBg: RGBA;
247
+ readonly diffRemovedLineNumberBg: RGBA;
248
+ readonly markdownText: RGBA;
249
+ readonly markdownHeading: RGBA;
250
+ readonly markdownLink: RGBA;
251
+ readonly markdownLinkText: RGBA;
252
+ readonly markdownCode: RGBA;
253
+ readonly markdownBlockQuote: RGBA;
254
+ readonly markdownEmph: RGBA;
255
+ readonly markdownStrong: RGBA;
256
+ readonly markdownHorizontalRule: RGBA;
257
+ readonly markdownListItem: RGBA;
258
+ readonly markdownListEnumeration: RGBA;
259
+ readonly markdownImage: RGBA;
260
+ readonly markdownImageText: RGBA;
261
+ readonly markdownCodeBlock: RGBA;
262
+ readonly syntaxComment: RGBA;
263
+ readonly syntaxKeyword: RGBA;
264
+ readonly syntaxFunction: RGBA;
265
+ readonly syntaxVariable: RGBA;
266
+ readonly syntaxString: RGBA;
267
+ readonly syntaxNumber: RGBA;
268
+ readonly syntaxType: RGBA;
269
+ readonly syntaxOperator: RGBA;
270
+ readonly syntaxPunctuation: RGBA;
271
+ readonly thinkingOpacity: number;
272
+ };
273
+ export type TuiTheme = {
274
+ readonly current: TuiThemeCurrent;
275
+ readonly selected: string;
276
+ has: (name: string) => boolean;
277
+ set: (name: string) => boolean;
278
+ install: (jsonPath: string) => Promise<void>;
279
+ mode: () => "dark" | "light";
280
+ readonly ready: boolean;
281
+ };
282
+ export type TuiKV = {
283
+ get: <Value = unknown>(key: string, fallback?: Value) => Value;
284
+ set: (key: string, value: unknown) => void;
285
+ readonly ready: boolean;
286
+ };
287
+ export type TuiState = {
288
+ readonly ready: boolean;
289
+ readonly config: SdkConfig;
290
+ readonly provider: ReadonlyArray<Provider>;
291
+ readonly path: {
292
+ state: string;
293
+ config: string;
294
+ worktree: string;
295
+ directory: string;
296
+ };
297
+ readonly vcs: {
298
+ branch?: string;
299
+ } | undefined;
300
+ session: {
301
+ count: () => number;
302
+ get: (sessionID: string) => Session | undefined;
303
+ diff: (sessionID: string) => ReadonlyArray<TuiSidebarFileItem>;
304
+ todo: (sessionID: string) => ReadonlyArray<TuiSidebarTodoItem>;
305
+ processes: (sessionID: string) => ReadonlyArray<TuiSidebarBackgroundProcessItem>;
306
+ messages: (sessionID: string) => ReadonlyArray<Message>;
307
+ status: (sessionID: string) => SessionStatus | undefined;
308
+ permission: (sessionID: string) => ReadonlyArray<PermissionRequest>;
309
+ question: (sessionID: string) => ReadonlyArray<QuestionRequest>;
310
+ };
311
+ part: (messageID: string) => ReadonlyArray<Part>;
312
+ lsp: () => ReadonlyArray<TuiSidebarLspItem>;
313
+ mcp: () => ReadonlyArray<TuiSidebarMcpItem>;
314
+ };
315
+ type TuiBindingLookupView = {
316
+ readonly bindings: ReadonlyArray<Binding<Renderable, KeyEvent>>;
317
+ get: (command: string) => ReadonlyArray<Binding<Renderable, KeyEvent>>;
318
+ has: (command: string) => boolean;
319
+ gather: (name: string, commands: readonly string[]) => ReadonlyArray<Binding<Renderable, KeyEvent>>;
320
+ pick: (name: string, commands: readonly string[]) => Binding<Renderable, KeyEvent>[];
321
+ omit: (name: string, commands: readonly string[]) => Binding<Renderable, KeyEvent>[];
322
+ };
323
+ type TuiAttentionConfigView = {
324
+ enabled: boolean;
325
+ notifications: boolean;
326
+ sound: boolean;
327
+ volume: number;
328
+ sound_pack: string;
329
+ sounds: Partial<Record<TuiAttentionSoundName, string>>;
330
+ };
331
+ type TuiConfigView = Pick<PluginConfig, "$schema" | "theme" | "plugin"> & NonNullable<PluginConfig["tui"]> & {
332
+ leader_timeout: number;
333
+ attention: TuiAttentionConfigView;
334
+ plugin_enabled?: Record<string, boolean>;
335
+ keybinds: TuiBindingLookupView;
336
+ };
337
+ export type TuiApp = {
338
+ readonly version: string;
339
+ };
340
+ type Frozen<Value> = Value extends (...args: never[]) => unknown ? Value : Value extends ReadonlyArray<infer Item> ? ReadonlyArray<Frozen<Item>> : Value extends object ? {
341
+ readonly [Key in keyof Value]: Frozen<Value[Key]>;
342
+ } : Value;
343
+ export type TuiSidebarMcpItem = {
344
+ name: string;
345
+ status: McpStatus["status"];
346
+ error?: string;
347
+ };
348
+ export type TuiSidebarLspItem = Pick<LspStatus, "id" | "root" | "status">;
349
+ export type TuiSidebarTodoItem = Pick<Todo, "content" | "status">;
350
+ export type TuiSidebarBackgroundProcessItem = Pick<BackgroundProcessInfo, "id" | "pid" | "command" | "cwd" | "description" | "ports" | "status" | "output">;
351
+ export type TuiSidebarFileItem = {
352
+ file: string;
353
+ additions: number;
354
+ deletions: number;
355
+ };
356
+ export type TuiHostSlotMap = {
357
+ app: {};
358
+ app_bottom: {};
359
+ home_logo: {};
360
+ home_prompt: {
361
+ ref?: (ref: TuiPromptRef | undefined) => void;
362
+ };
363
+ home_prompt_right: {};
364
+ session_prompt: {
365
+ session_id: string;
366
+ visible?: boolean;
367
+ disabled?: boolean;
368
+ on_submit?: () => void;
369
+ ref?: (ref: TuiPromptRef | undefined) => void;
370
+ };
371
+ session_prompt_right: {
372
+ session_id: string;
373
+ };
374
+ home_bottom: {};
375
+ home_footer: {};
376
+ sidebar_title: {
377
+ session_id: string;
378
+ title: string;
379
+ share_url?: string;
380
+ };
381
+ sidebar_content: {
382
+ session_id: string;
383
+ };
384
+ sidebar_footer: {
385
+ session_id: string;
386
+ };
387
+ };
388
+ export type TuiSlotMap<Slots extends Record<string, object> = {}> = TuiHostSlotMap & Slots;
389
+ type TuiSlotShape<Name extends string, Slots extends Record<string, object>> = Name extends keyof TuiHostSlotMap ? TuiHostSlotMap[Name] : Name extends keyof Slots ? Slots[Name] : Record<string, unknown>;
390
+ export type TuiSlotProps<Name extends string = string, Slots extends Record<string, object> = {}> = {
391
+ name: Name;
392
+ mode?: SlotMode;
393
+ children?: JSX.Element;
394
+ } & TuiSlotShape<Name, Slots>;
395
+ export type TuiSlotContext = {
396
+ theme: TuiTheme;
397
+ };
398
+ type SlotCore<Slots extends Record<string, object> = {}> = SolidPlugin<TuiSlotMap<Slots>, TuiSlotContext>;
399
+ export type TuiSlotPlugin<Slots extends Record<string, object> = {}> = Omit<SlotCore<Slots>, "id"> & {
400
+ id?: never;
401
+ };
402
+ export type TuiSlots = {
403
+ register: {
404
+ (plugin: TuiSlotPlugin): string;
405
+ <Slots extends Record<string, object>>(plugin: TuiSlotPlugin<Slots>): string;
406
+ };
407
+ };
408
+ export type TuiEventBus = {
409
+ on: <Type extends Event["type"]>(type: Type, handler: (event: Extract<Event, {
410
+ type: Type;
411
+ }>) => void) => () => void;
412
+ };
413
+ export type TuiDispose = () => void | Promise<void>;
414
+ export type TuiLifecycle = {
415
+ readonly signal: AbortSignal;
416
+ onDispose: (fn: TuiDispose) => () => void;
417
+ };
418
+ export type TuiPluginState = "first" | "updated" | "same";
419
+ export type TuiPluginEntry = {
420
+ id: string;
421
+ source: "file" | "npm" | "internal";
422
+ spec: string;
423
+ target: string;
424
+ requested?: string;
425
+ version?: string;
426
+ modified?: number;
427
+ first_time: number;
428
+ last_time: number;
429
+ time_changed: number;
430
+ load_count: number;
431
+ fingerprint: string;
432
+ };
433
+ export type TuiPluginMeta = TuiPluginEntry & {
434
+ state: TuiPluginState;
435
+ };
436
+ export type TuiPluginStatus = {
437
+ id: string;
438
+ source: TuiPluginEntry["source"];
439
+ spec: string;
440
+ target: string;
441
+ enabled: boolean;
442
+ active: boolean;
443
+ };
444
+ export type TuiPluginInstallOptions = {
445
+ global?: boolean;
446
+ };
447
+ export type TuiPluginInstallResult = {
448
+ ok: true;
449
+ dir: string;
450
+ tui: boolean;
451
+ } | {
452
+ ok: false;
453
+ message: string;
454
+ missing?: boolean;
455
+ };
456
+ export type TuiWorkspace = {
457
+ current: () => string | undefined;
458
+ set: (workspaceID?: string) => void;
459
+ };
460
+ export type TuiPluginApi = {
461
+ app: TuiApp;
462
+ attention: TuiAttention;
463
+ /**
464
+ * Legacy `api.command` API kept so v1 plugins can initialize. Remove in v2.
465
+ *
466
+ * @deprecated Use `api.keymap.registerLayer`, `api.keymap.dispatchCommand`, and
467
+ * `api.keymap.dispatchCommand("command.palette.show")` instead.
468
+ */
469
+ command?: TuiCommandApi;
470
+ keys: TuiKeys;
471
+ keymap: TuiKeymap;
472
+ mode: TuiModeApi;
473
+ route: {
474
+ register: (routes: TuiRouteDefinition[]) => () => void;
475
+ navigate: (name: string, params?: Record<string, unknown>) => void;
476
+ readonly current: TuiRouteCurrent;
477
+ };
478
+ ui: {
479
+ Dialog: (props: TuiDialogProps) => JSX.Element;
480
+ DialogAlert: (props: TuiDialogAlertProps) => JSX.Element;
481
+ DialogConfirm: (props: TuiDialogConfirmProps) => JSX.Element;
482
+ DialogPrompt: (props: TuiDialogPromptProps) => JSX.Element;
483
+ DialogSelect: <Value = unknown>(props: TuiDialogSelectProps<Value>) => JSX.Element;
484
+ Slot: <Name extends string>(props: TuiSlotProps<Name>) => JSX.Element | null;
485
+ Prompt: (props: TuiPromptProps) => JSX.Element;
486
+ toast: (input: TuiToast) => void;
487
+ dialog: TuiDialogStack;
488
+ };
489
+ readonly tuiConfig: Frozen<TuiConfigView>;
490
+ kv: TuiKV;
491
+ state: TuiState;
492
+ theme: TuiTheme;
493
+ client: HullClient;
494
+ event: TuiEventBus;
495
+ renderer: CliRenderer;
496
+ slots: TuiSlots;
497
+ plugins: {
498
+ list: () => ReadonlyArray<TuiPluginStatus>;
499
+ activate: (id: string) => Promise<boolean>;
500
+ deactivate: (id: string) => Promise<boolean>;
501
+ add: (spec: string) => Promise<boolean>;
502
+ install: (spec: string, options?: TuiPluginInstallOptions) => Promise<TuiPluginInstallResult>;
503
+ };
504
+ lifecycle: TuiLifecycle;
505
+ };
506
+ export type TuiPlugin = (api: TuiPluginApi, options: PluginOptions | undefined, meta: TuiPluginMeta) => Promise<void>;
507
+ export type TuiPluginModule = {
508
+ id?: string;
509
+ tui: TuiPlugin;
510
+ server?: never;
511
+ };
package/dist/tui.js ADDED
@@ -0,0 +1,7 @@
1
+ import { createBindingLookup as createKeymapBindingLookup, } from "@opentui/keymap/extras";
2
+ export { stringifyKeySequence, stringifyKeyStroke } from "@opentui/keymap";
3
+ export { formatCommandBindings, formatKeySequence } from "@opentui/keymap/extras";
4
+ export function createBindingLookup(config, options) {
5
+ return createKeymapBindingLookup(config ?? {}, options);
6
+ }
7
+ export const TuiAttentionSoundNames = ["default", "question", "permission", "error", "done", "subagent_done"];
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/package.json",
3
+ "name": "@hullcode/plugin",
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "license": "UNLICENSED",
7
+ "scripts": {
8
+ "typecheck": "tsgo --noEmit",
9
+ "build": "tsc"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "import": "./dist/index.js",
14
+ "types": "./dist/index.d.ts"
15
+ },
16
+ "./tool": {
17
+ "import": "./dist/tool.js",
18
+ "types": "./dist/tool.d.ts"
19
+ },
20
+ "./tui": {
21
+ "import": "./dist/tui.js",
22
+ "types": "./dist/tui.d.ts"
23
+ }
24
+ },
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "dependencies": {
29
+ "@hullcode/sdk": "0.1.0",
30
+ "effect": "4.0.0-beta.74",
31
+ "zod": "4.1.8"
32
+ },
33
+ "peerDependencies": {
34
+ "@opentui/core": ">=0.3.2",
35
+ "@opentui/solid": ">=0.3.2",
36
+ "@opentui/keymap": ">=0.3.2"
37
+ },
38
+ "peerDependenciesMeta": {
39
+ "@opentui/core": {
40
+ "optional": true
41
+ },
42
+ "@opentui/keymap": {
43
+ "optional": true
44
+ },
45
+ "@opentui/solid": {
46
+ "optional": true
47
+ }
48
+ },
49
+ "devDependencies": {
50
+ "@opentui/core": "0.3.2",
51
+ "@opentui/solid": "0.3.2",
52
+ "@tsconfig/node22": "22.0.2",
53
+ "@types/node": "24.12.4",
54
+ "typescript": "5.8.2",
55
+ "@typescript/native-preview": "7.0.0-dev.20260316.1",
56
+ "@opentui/keymap": "0.3.2"
57
+ },
58
+ "repository": {
59
+ "type": "git",
60
+ "url": "https://github.com/INFINYSYNC-LABS/hullcode",
61
+ "directory": "packages/plugin"
62
+ }
63
+ }