@subshell-ai/plugin-oh-my-pi 0.0.0-stage → 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 +203 -0
- package/dist/index.d.ts +670 -0
- package/dist/index.js +458 -0
- package/icon.svg +1 -0
- package/package.json +68 -4
- package/README.md +0 -3
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
//#region ../../plugin-api/dist/types-CW2dMAMW.d.ts
|
|
2
|
+
/** How the plugin learns about children: harness lifecycle hooks, or a loaded extension. */
|
|
3
|
+
type HarnessSubagentDiscovery = "hooks" | "extension";
|
|
4
|
+
/** Whether the sidebar can move the parent's own view to a child, or the harness keeps that in-app. */
|
|
5
|
+
type HarnessSubagentNavigation = "native" | "unavailable";
|
|
6
|
+
/**
|
|
7
|
+
* What a plugin declares about its harness's child agents. The host uses
|
|
8
|
+
* `discovery`/`navigation` to decide what the sidebar may offer; `setup`, when
|
|
9
|
+
* the harness needs a one-time human act (trusting hooks, installing an
|
|
10
|
+
* extension), is the copy the preset editor shows for that act.
|
|
11
|
+
*/
|
|
12
|
+
interface HarnessSubagentSupport {
|
|
13
|
+
/** How subagent lifecycle events reach Subshell */
|
|
14
|
+
discovery: HarnessSubagentDiscovery;
|
|
15
|
+
/** Whether subshell may navigate the parent's own view to a child */
|
|
16
|
+
navigation: HarnessSubagentNavigation;
|
|
17
|
+
/** One-time user action required for reporting to work, if any */
|
|
18
|
+
setup?: string;
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/types.d.ts
|
|
22
|
+
/**
|
|
23
|
+
* What kind of thing a plugin provides.
|
|
24
|
+
*
|
|
25
|
+
* For humans: it groups and labels in the UI and filters the catalog. Code
|
|
26
|
+
* branches on {@link PluginCapability} instead, so adding a type here touches
|
|
27
|
+
* labels while adding a capability touches the launch pipeline.
|
|
28
|
+
*
|
|
29
|
+
* ONE exception, and it is structural rather than a branch on a label: a
|
|
30
|
+
* `network` plugin implements {@link NetworkPlugin} instead of
|
|
31
|
+
* {@link SubshellPlugin}, so the loader picks WHICH members to require by
|
|
32
|
+
* type. A harness drives a thing in a pane; a network plugin connects this
|
|
33
|
+
* host to one network. They share the store, the manifest and the capability
|
|
34
|
+
* vocabulary, and nothing else.
|
|
35
|
+
*/
|
|
36
|
+
type PluginType = "agent-harness" | "terminal" | "network";
|
|
37
|
+
/**
|
|
38
|
+
* What a plugin can DO. The launch pipeline and the UI read this; neither
|
|
39
|
+
* branches on {@link PluginType}.
|
|
40
|
+
*
|
|
41
|
+
* The union is shared across types and the applicable SUBSET is not, so
|
|
42
|
+
* {@link capabilityMismatches} takes the type: a harness claiming `publish`
|
|
43
|
+
* and a network plugin claiming `resume` are both refused at load, for the
|
|
44
|
+
* same reason a claimed-but-unimplemented capability is.
|
|
45
|
+
*/
|
|
46
|
+
type PluginCapability = "mcp" | "resume" | "attention" | "subagents" | "publish" | "supervise" | "guard" | "settings";
|
|
47
|
+
/** A harness preset as defined by the user (decoded JSON blobs). */
|
|
48
|
+
interface PresetDefinition {
|
|
49
|
+
/** Human-friendly preset name */
|
|
50
|
+
name: string;
|
|
51
|
+
/** Optional longer description */
|
|
52
|
+
description?: string | null;
|
|
53
|
+
/** Extra environment variables to set on the subshell */
|
|
54
|
+
env: Record<string, string>;
|
|
55
|
+
/** Extra CLI flags to pass to the harness binary */
|
|
56
|
+
flags: string[];
|
|
57
|
+
/** Settings blob passed to the harness (e.g. claude --settings JSON) */
|
|
58
|
+
settings: Record<string, unknown> | null;
|
|
59
|
+
/** If true, only this preset's config sources apply (isolation) */
|
|
60
|
+
configIsolation: boolean;
|
|
61
|
+
/** If true, new subshells from this preset auto-restart on exit */
|
|
62
|
+
restartOnExit?: boolean;
|
|
63
|
+
}
|
|
64
|
+
/** Snapshot of a single field-level validation error on a preset. */
|
|
65
|
+
interface PresetValidationIssue {
|
|
66
|
+
/** Field name (e.g. "name", "env", "settings") */
|
|
67
|
+
field: string;
|
|
68
|
+
/** Human-readable problem description */
|
|
69
|
+
message: string;
|
|
70
|
+
}
|
|
71
|
+
interface PresetValidationResult {
|
|
72
|
+
valid: boolean;
|
|
73
|
+
issues: PresetValidationIssue[];
|
|
74
|
+
}
|
|
75
|
+
/** Everything a plugin needs to build a launch command. */
|
|
76
|
+
interface BuildCommandInput {
|
|
77
|
+
/** Resolved absolute path to the harness binary */
|
|
78
|
+
binary: string;
|
|
79
|
+
/** Working directory the harness runs in */
|
|
80
|
+
cwd: string;
|
|
81
|
+
/** The validated preset being used */
|
|
82
|
+
preset: PresetDefinition;
|
|
83
|
+
/** Subshell display name ("" = let the harness pick a default) */
|
|
84
|
+
subshellName: string;
|
|
85
|
+
/** Any additional CLI flags from route/request context */
|
|
86
|
+
extraFlags?: string[];
|
|
87
|
+
/**
|
|
88
|
+
* The subshell's MCP registration (channels + subshell orchestration), as
|
|
89
|
+
* produced by this plugin's own `mcpRegistration` and already written to
|
|
90
|
+
* disk. Plugins consume ONLY `mcp.args`: splice them where the dialect needs
|
|
91
|
+
* them (claude right after the binary). `mcp.env` is baked into the pane by
|
|
92
|
+
* the host before the command runs; plugins must not consume it themselves.
|
|
93
|
+
* Undefined = no registration for this launch.
|
|
94
|
+
*/
|
|
95
|
+
mcp?: McpRegistration;
|
|
96
|
+
/**
|
|
97
|
+
* Conversation identity for restart-resume; set only when this plugin
|
|
98
|
+
* declares the `resume` capability. `mode: "start"` means the conversation
|
|
99
|
+
* is NEW and must be created under exactly this id (pin it, because the host
|
|
100
|
+
* stores it and later resumes by it); `mode: "resume"` names an EXISTING
|
|
101
|
+
* conversation to continue (the host only asks after {@link HarnessResume.resumePath}
|
|
102
|
+
* pointed at a file the host found there).
|
|
103
|
+
*/
|
|
104
|
+
harnessSession?: {
|
|
105
|
+
id: string;
|
|
106
|
+
mode: "start" | "resume";
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* How a harness hook re-enters the subshell binary on the PANE's machine, as
|
|
110
|
+
* the host resolved it for that machine: `{ command, args }` with the
|
|
111
|
+
* reporting subcommand already appended, so a plugin adds only its own verb
|
|
112
|
+
* words. Splice it into whatever hook mechanism the harness has.
|
|
113
|
+
*
|
|
114
|
+
* A hook runs where the pane runs, and the only program guaranteed to exist
|
|
115
|
+
* there is the binary that launched the pane — which is why this arrives
|
|
116
|
+
* from the host rather than being a command a plugin writes for itself. The
|
|
117
|
+
* hooks here were once `bun -e '<inlined JS>'`, and every machine without a
|
|
118
|
+
* bun on its PATH (i.e. most of them) opened its harness on
|
|
119
|
+
* `bun: command not found`.
|
|
120
|
+
*
|
|
121
|
+
* Undefined means the host could not resolve one. OMIT the hooks then: a
|
|
122
|
+
* command the pane cannot run costs the same signal and adds an error to
|
|
123
|
+
* every session.
|
|
124
|
+
*/
|
|
125
|
+
reporter?: ReporterSpec;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* A command prefix that re-enters the subshell binary on a pane's machine,
|
|
129
|
+
* ready for a plugin's own verb words to be appended.
|
|
130
|
+
*
|
|
131
|
+
* Shaped like {@link McpLaunchSpec} and resolved from the same host question
|
|
132
|
+
* ("am I a compiled binary, or is `bun` running my entry script?"), but kept
|
|
133
|
+
* separate because they carry different subcommands and the MCP one is
|
|
134
|
+
* operator-overridable while this one is not.
|
|
135
|
+
*/
|
|
136
|
+
interface ReporterSpec {
|
|
137
|
+
/** Executable to run (the binary itself, or the interpreter running it). */
|
|
138
|
+
command: string;
|
|
139
|
+
/** Everything before the plugin's verb words (any entry script, then the subcommand). */
|
|
140
|
+
args: string[];
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Restart-resume, implemented only by plugins that can continue a previous
|
|
144
|
+
* conversation.
|
|
145
|
+
*
|
|
146
|
+
* The ids here are HARNESS conversation ids (a claude transcript uuid, say),
|
|
147
|
+
* never subshell ids: the host pins {@link allocateHarnessSessionId} at launch,
|
|
148
|
+
* stores it on the subshell row, and consults {@link resumePath} before every
|
|
149
|
+
* restart to choose between continuing and starting fresh.
|
|
150
|
+
*
|
|
151
|
+
* Both members are PURE, and that is the load-bearing property (spec
|
|
152
|
+
* 2026-09-10 §5): a resume runs on the CONTROL PLANE, for a pane that lives on
|
|
153
|
+
* a machine the plane cannot see. Computing (rather than checking) is what
|
|
154
|
+
* lets one plugin implementation serve local and remote restarts identically.
|
|
155
|
+
*/
|
|
156
|
+
interface HarnessResume {
|
|
157
|
+
/** Allocates the HARNESS conversation id to pin at launch (a uuid for claude). */
|
|
158
|
+
allocateHarnessSessionId(): string;
|
|
159
|
+
/**
|
|
160
|
+
* Where the resumable transcript would be, given the target machine's
|
|
161
|
+
* environment. PURE: it computes a path and never touches a filesystem, so
|
|
162
|
+
* the control plane can build it for a machine it cannot see. The HOST
|
|
163
|
+
* checks existence.
|
|
164
|
+
*/
|
|
165
|
+
resumePath(harnessSessionId: string, cwd: string, hostEnv: HostEnv): string;
|
|
166
|
+
}
|
|
167
|
+
/** The parts of a target machine's environment a plugin may compute against. */
|
|
168
|
+
interface HostEnv {
|
|
169
|
+
/** The node's home directory */
|
|
170
|
+
homeDir: string;
|
|
171
|
+
/** Values for the variables this plugin's manifest declared it needs */
|
|
172
|
+
env: Record<string, string>;
|
|
173
|
+
}
|
|
174
|
+
/** How to spawn the `subshell mcp` stdio server. */
|
|
175
|
+
interface McpLaunchSpec {
|
|
176
|
+
/** Executable to run (the self command, the interpreter, or the agent binary) */
|
|
177
|
+
command: string;
|
|
178
|
+
/** Arguments for the executable (e.g. the mcp entry script path) */
|
|
179
|
+
args: string[];
|
|
180
|
+
}
|
|
181
|
+
/** Per-subshell MCP registration rendered in the plugin's own config dialect. */
|
|
182
|
+
interface McpRegistration {
|
|
183
|
+
/**
|
|
184
|
+
* Companion integration modules written next to the config file, under
|
|
185
|
+
* `<configPath>.d/`. Keys are basenames only, and the host refuses a name
|
|
186
|
+
* outside `[A-Za-z0-9_-]+\.(mjs|py|json)`, more than eight files, or more
|
|
187
|
+
* than 64 KiB of content — by throwing at launch, so the registration must
|
|
188
|
+
* stay inside those rules. Each file ships self-contained: on a pane's
|
|
189
|
+
* machine nothing of ours is importable (see the package doc).
|
|
190
|
+
*/
|
|
191
|
+
files?: Record<string, string>;
|
|
192
|
+
/** File content the harness reads, written to the subshell's config path */
|
|
193
|
+
fileContent: string;
|
|
194
|
+
/** argv the harness needs to load the file (e.g. claude's ["--mcp-config", path]) */
|
|
195
|
+
args?: string[];
|
|
196
|
+
/**
|
|
197
|
+
* Extra pane env the harness needs to discover the file (e.g. OPENCODE_CONFIG).
|
|
198
|
+
* Consumed by the HOST, which layers it AFTER preset env in the pane's
|
|
199
|
+
* environment: the wiring wins on purpose, so the harness always finds the
|
|
200
|
+
* config this launch wrote even when a preset sets the same name. Plugins
|
|
201
|
+
* never read it back.
|
|
202
|
+
*/
|
|
203
|
+
env?: Record<string, string>;
|
|
204
|
+
}
|
|
205
|
+
/** One copy-paste line shown in the preset editor for manual-setup harnesses. */
|
|
206
|
+
interface McpSetupStep {
|
|
207
|
+
/** What the user should do, and where the text goes */
|
|
208
|
+
label: string;
|
|
209
|
+
/** Copyable command or snippet */
|
|
210
|
+
command: string;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* How a harness obtains the `subshell mcp` tools.
|
|
214
|
+
*
|
|
215
|
+
* Discriminated on purpose: auto harnesses explain themselves in one line and
|
|
216
|
+
* manual harnesses carry steps, and a plugin cannot mix the two.
|
|
217
|
+
*/
|
|
218
|
+
type McpSetupInfo = {
|
|
219
|
+
mode: "auto";
|
|
220
|
+
summary: string;
|
|
221
|
+
} | {
|
|
222
|
+
mode: "manual";
|
|
223
|
+
steps: McpSetupStep[];
|
|
224
|
+
};
|
|
225
|
+
/** A single option in a plugin's settings editor. */
|
|
226
|
+
interface SettingsField {
|
|
227
|
+
/** Key into the settings object (e.g. "permissionMode") */
|
|
228
|
+
key: string;
|
|
229
|
+
/** Property label */
|
|
230
|
+
label: string;
|
|
231
|
+
/** Short description for the editor */
|
|
232
|
+
description?: string;
|
|
233
|
+
/**
|
|
234
|
+
* One of: string, boolean, number, select, secret.
|
|
235
|
+
*
|
|
236
|
+
* `secret` is WRITE-ONLY and network-only: the host stores it through
|
|
237
|
+
* {@link PluginHost.secrets} rather than in the settings object, reads
|
|
238
|
+
* report only whether one is set, and a plugin never receives the value —
|
|
239
|
+
* it names the secret and the host hydrates it into a process it spawns
|
|
240
|
+
* ({@link SupervisedProcessSpec}). The preset editor does not render this
|
|
241
|
+
* type; a harness declaring one is a bug rather than a second secret store.
|
|
242
|
+
*/
|
|
243
|
+
type: "string" | "boolean" | "number" | "select" | "secret";
|
|
244
|
+
/** Choices when type === "select" */
|
|
245
|
+
choices?: string[];
|
|
246
|
+
/** True when the field must be set before the plugin can act */
|
|
247
|
+
required?: boolean;
|
|
248
|
+
/** Placeholder / example, shown in the editor. Never a real credential. */
|
|
249
|
+
placeholder?: string;
|
|
250
|
+
/** Default value when unset */
|
|
251
|
+
default?: string | boolean | number;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Why there is no binary.
|
|
255
|
+
*
|
|
256
|
+
* `no-binary` is not a failure: a plugin (a `terminal` one, say) can declare
|
|
257
|
+
* no binary at all, and rendering that as "not on PATH" tells someone their
|
|
258
|
+
* PATH is wrong about something that never wanted one.
|
|
259
|
+
*/
|
|
260
|
+
type DetectionReason = "not-on-path" | "override-invalid" | "no-binary";
|
|
261
|
+
/** A detection answer: the path, or the reason there is not one. */
|
|
262
|
+
type DetectionResult = {
|
|
263
|
+
path: string;
|
|
264
|
+
reason?: undefined;
|
|
265
|
+
} | {
|
|
266
|
+
path: null;
|
|
267
|
+
reason: DetectionReason;
|
|
268
|
+
};
|
|
269
|
+
/**
|
|
270
|
+
* Everything the host lends a plugin, because a plugin can import none of it.
|
|
271
|
+
*
|
|
272
|
+
* This is also the version boundary: a host at a higher `apiVersion` keeps
|
|
273
|
+
* older plugins working by keeping the fields they were compiled against, so
|
|
274
|
+
* members are ADDED here and never removed or retyped.
|
|
275
|
+
*/
|
|
276
|
+
interface PluginHost {
|
|
277
|
+
/** The API version this host implements. Always >= the plugin's own. */
|
|
278
|
+
readonly apiVersion: number;
|
|
279
|
+
/** Resolve a binary through the host's full lookup ladder. */
|
|
280
|
+
findBinary(name: string, envOverride: string, knownPaths: string[]): Promise<string | null>;
|
|
281
|
+
/** The same lookup, reporting why it failed. */
|
|
282
|
+
detectBinary(name: string, envOverride: string, knownPaths: string[]): Promise<DetectionResult>;
|
|
283
|
+
/** Run a short command with a deadline; trimmed stdout, or null. */
|
|
284
|
+
probeVersion(binary: string, args?: string[]): Promise<string | null>;
|
|
285
|
+
/** POSIX-quote one argument for a shell command line. */
|
|
286
|
+
shellQuote(value: string): string;
|
|
287
|
+
/** Structured logging, namespaced to the plugin. */
|
|
288
|
+
log: {
|
|
289
|
+
debug(message: string): void;
|
|
290
|
+
warn(message: string): void;
|
|
291
|
+
};
|
|
292
|
+
/**
|
|
293
|
+
* Runs a command to completion under the host's bounds and reports it.
|
|
294
|
+
*
|
|
295
|
+
* The one way a plugin executes anything. A plugin does not spawn: the host
|
|
296
|
+
* owns the deadline, the environment allowlist (the process's own
|
|
297
|
+
* environment holds the auth secret and the database path, and is never
|
|
298
|
+
* handed over), the PATH a service-run server would otherwise lack, and the
|
|
299
|
+
* output cap. Two refusals throw rather than returning a result, because
|
|
300
|
+
* both are bugs in the plugin rather than outcomes: `argv[0]` that is not an
|
|
301
|
+
* ABSOLUTE path (resolve it with {@link findBinary} — a bare name would
|
|
302
|
+
* resolve against a PATH the plugin cannot see), and `argv[0]` that is
|
|
303
|
+
* `sudo`. The server has no terminal to answer a password prompt, so a
|
|
304
|
+
* privileged command is a copy-paste instruction for the operator and never
|
|
305
|
+
* something this runs.
|
|
306
|
+
*
|
|
307
|
+
* Long-running processes are NOT this: a plugin that needs one describes it
|
|
308
|
+
* ({@link SupervisedProcessSpec}) and the host supervises it.
|
|
309
|
+
* @since apiVersion 2
|
|
310
|
+
*/
|
|
311
|
+
run(argv: string[], opts?: RunOptions): Promise<RunResult>;
|
|
312
|
+
/**
|
|
313
|
+
* Write-only credential storage, scoped to this plugin.
|
|
314
|
+
*
|
|
315
|
+
* There is deliberately NO `get`. A plugin that could read a credential
|
|
316
|
+
* could put it in an argv (visible in `ps`), a log line or a hint string;
|
|
317
|
+
* every legitimate consumer is a process the HOST spawns, so the host
|
|
318
|
+
* hydrates the value itself from the name the plugin gives it. Values are
|
|
319
|
+
* stored 0600 under the host's data directory, never in the database and
|
|
320
|
+
* never in a settings row.
|
|
321
|
+
* @since apiVersion 2
|
|
322
|
+
*/
|
|
323
|
+
secrets: PluginSecrets;
|
|
324
|
+
/** The operating system this host runs on. */
|
|
325
|
+
readonly platform: PluginPlatform;
|
|
326
|
+
/** The home directory of the user this host runs as. */
|
|
327
|
+
readonly homeDir: string;
|
|
328
|
+
}
|
|
329
|
+
/** Where a plugin can be driven at all. Mirrors the manifest's `platforms`. */
|
|
330
|
+
type PluginPlatform = "darwin" | "linux";
|
|
331
|
+
/** Options for {@link PluginHost.run}. */
|
|
332
|
+
interface RunOptions {
|
|
333
|
+
/** Deadline in milliseconds. Default 30_000; the host caps it at 10 minutes. */
|
|
334
|
+
timeoutMs?: number;
|
|
335
|
+
/** Called per output line, ANSI already stripped, from both streams interleaved. */
|
|
336
|
+
onLine?: (line: string) => void;
|
|
337
|
+
/**
|
|
338
|
+
* Ends the run early without failing it.
|
|
339
|
+
*
|
|
340
|
+
* What an interactive login needs: `tailscale up` with no key blocks until a
|
|
341
|
+
* human finishes in a browser, having already printed the URL — so the
|
|
342
|
+
* plugin reads the URL off {@link onLine} and aborts, leaving the daemon
|
|
343
|
+
* waiting. An aborted run reports `aborted: true` and whatever was captured.
|
|
344
|
+
*/
|
|
345
|
+
signal?: AbortSignal;
|
|
346
|
+
/** Extra environment on top of the host's allowlist. Never overrides PATH. */
|
|
347
|
+
env?: Record<string, string>;
|
|
348
|
+
/** Text to write to stdin. Absent means stdin is /dev/null, so a prompt cannot hang forever. */
|
|
349
|
+
stdin?: string;
|
|
350
|
+
}
|
|
351
|
+
/** What {@link PluginHost.run} reports. Never throws for a non-zero exit. */
|
|
352
|
+
interface RunResult {
|
|
353
|
+
/** Exit code, or null when the process was signalled or never started. */
|
|
354
|
+
code: number | null;
|
|
355
|
+
/** Captured stdout, ANSI stripped, capped. */
|
|
356
|
+
stdout: string;
|
|
357
|
+
/** Captured stderr, ANSI stripped, capped. */
|
|
358
|
+
stderr: string;
|
|
359
|
+
/** True when the deadline ended it. */
|
|
360
|
+
timedOut: boolean;
|
|
361
|
+
/** True when {@link RunOptions.signal} ended it. */
|
|
362
|
+
aborted: boolean;
|
|
363
|
+
}
|
|
364
|
+
/** Write-only, per-plugin credential storage. See {@link PluginHost.secrets}. */
|
|
365
|
+
interface PluginSecrets {
|
|
366
|
+
/** Stores (or replaces) a credential under this name. */
|
|
367
|
+
set(name: string, value: string): Promise<void>;
|
|
368
|
+
/** Whether a credential is stored under this name. The only read there is. */
|
|
369
|
+
has(name: string): Promise<boolean>;
|
|
370
|
+
/** Removes it. Absent is not an error. */
|
|
371
|
+
delete(name: string): Promise<void>;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* The plugin itself.
|
|
375
|
+
*
|
|
376
|
+
* Only the first three members are required. Everything else is a capability
|
|
377
|
+
* the plugin opts into and declares from {@link capabilities}, so a `terminal`
|
|
378
|
+
* plugin is a binary and an argv rather than eight stubs.
|
|
379
|
+
*/
|
|
380
|
+
interface SubshellPlugin {
|
|
381
|
+
/** Native child-agent reporting and optional in-place navigation. */
|
|
382
|
+
subagents?: HarnessSubagentSupport;
|
|
383
|
+
/** Builds the argv (no shell) used to launch a subshell. */
|
|
384
|
+
buildCommand(input: BuildCommandInput): string[];
|
|
385
|
+
/** Validates a preset definition before saving. */
|
|
386
|
+
validatePreset(preset: PresetDefinition): PresetValidationResult;
|
|
387
|
+
/** Which optional members below are meaningful on this plugin. */
|
|
388
|
+
capabilities(): PluginCapability[];
|
|
389
|
+
/** Maps a harness exit code to a human label (null = unknown). */
|
|
390
|
+
exitStatus?(code: number): string | null;
|
|
391
|
+
/**
|
|
392
|
+
* Interprets the raw stdout of the version probe.
|
|
393
|
+
*
|
|
394
|
+
* Probing is the host's job (it owns the deadline); making sense of the
|
|
395
|
+
* output is the plugin's, because only the plugin knows its harness prints
|
|
396
|
+
* a banner rather than a bare semver. Omit it and the trimmed output is
|
|
397
|
+
* used as-is, which is right for every harness that prints just a version.
|
|
398
|
+
*/
|
|
399
|
+
parseVersion?(raw: string): string | null;
|
|
400
|
+
/** Overrides manifest-driven detection. Almost no plugin needs this. */
|
|
401
|
+
detect?(): Promise<DetectionResult>;
|
|
402
|
+
/** Restart-resume support. Declare the `resume` capability with it. */
|
|
403
|
+
resume?: HarnessResume;
|
|
404
|
+
/**
|
|
405
|
+
* True when `buildCommand` wires this harness's native "needs attention"
|
|
406
|
+
* reporting into the launch. The host's quiet-output idle watcher skips
|
|
407
|
+
* these. Declare the `attention` capability with it.
|
|
408
|
+
*/
|
|
409
|
+
supportsAttentionHooks?: boolean;
|
|
410
|
+
/**
|
|
411
|
+
* Renders the per-subshell MCP registration in this harness's config format.
|
|
412
|
+
* Omit when the harness cannot consume a per-subshell config file; the host
|
|
413
|
+
* then surfaces one-time manual setup via {@link mcpSetup} instead.
|
|
414
|
+
*/
|
|
415
|
+
mcpRegistration?(launch: McpLaunchSpec, configPath: string): McpRegistration;
|
|
416
|
+
/**
|
|
417
|
+
* How users obtain the subshell MCP tools here. Declare the `mcp`
|
|
418
|
+
* capability with it. `reporter` is the host-resolved command prefix that
|
|
419
|
+
* re-enters the subshell binary on a pane's machine (same shape as
|
|
420
|
+
* {@link BuildCommandInput.reporter}), present only when the host could
|
|
421
|
+
* resolve one: use it to render hook-configuration steps, and omit those
|
|
422
|
+
* steps when it is absent rather than guessing a binary name.
|
|
423
|
+
*/
|
|
424
|
+
mcpSetup?(launch: McpLaunchSpec, reporter?: ReporterSpec): McpSetupInfo;
|
|
425
|
+
/** Settings rendered in the PRESET editor and stored on the preset. */
|
|
426
|
+
presetSettings?(): SettingsField[];
|
|
427
|
+
/** Known extra env var suggestions for the preset editor. */
|
|
428
|
+
suggestedEnv?(): {
|
|
429
|
+
key: string;
|
|
430
|
+
description: string;
|
|
431
|
+
}[];
|
|
432
|
+
/** Known CLI flag suggestions for the preset editor. */
|
|
433
|
+
suggestedFlags?(): {
|
|
434
|
+
flag: string;
|
|
435
|
+
description: string;
|
|
436
|
+
}[];
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* A factory that returns a harness, for a plugin that knows which it is.
|
|
440
|
+
*
|
|
441
|
+
* {@link PluginFactory} is the LOADER's view — it must accept either shape,
|
|
442
|
+
* because it decides which to expect from the manifest. A plugin itself has no
|
|
443
|
+
* such ambiguity, and annotating its own factory with the union would widen
|
|
444
|
+
* what its tests can call: `createPlugin(host).buildCommand(...)` stops
|
|
445
|
+
* type-checking on a value that might be a network plugin. So a plugin
|
|
446
|
+
* declares the half it implements and the loader keeps the union.
|
|
447
|
+
*/
|
|
448
|
+
type HarnessPluginFactory = (host: PluginHost) => SubshellPlugin;
|
|
449
|
+
//#endregion
|
|
450
|
+
//#region ../../plugin-api/dist/index.d.ts
|
|
451
|
+
/** How a host finds this plugin's binary, as data, so a scan loads no plugin code. */
|
|
452
|
+
interface DetectSpec {
|
|
453
|
+
/** Executable name to look for, e.g. "claude" */
|
|
454
|
+
binaryName: string;
|
|
455
|
+
/** Env var that overrides the lookup outright, e.g. "CLAUDE_PATH" */
|
|
456
|
+
envOverride: string;
|
|
457
|
+
/**
|
|
458
|
+
* Well-known locations, tried after PATH and before the version managers.
|
|
459
|
+
*
|
|
460
|
+
* HOME-relative (`.local/bin/mytool`), or ABSOLUTE when the entry starts
|
|
461
|
+
* with `/` — which is how a vendor's GUI install is named, since
|
|
462
|
+
* `/Applications/Tailscale.app/Contents/MacOS/Tailscale` is nobody's HOME.
|
|
463
|
+
* Each is a candidate to search, so an entry that does not resolve costs
|
|
464
|
+
* nothing and the ladder carries on.
|
|
465
|
+
*/
|
|
466
|
+
knownPaths: string[];
|
|
467
|
+
}
|
|
468
|
+
/** Where to send someone whose binary is missing. */
|
|
469
|
+
interface InstallSpec {
|
|
470
|
+
/**
|
|
471
|
+
* Copy-pasteable install command for the official installer.
|
|
472
|
+
*
|
|
473
|
+
* A command the HOST may run on request, which is why it may not be
|
|
474
|
+
* privileged: `sudo`, `doas` and `pkexec` are refused by the parser at any
|
|
475
|
+
* command boundary (the line is run through a shell — see
|
|
476
|
+
* {@link needsPrivileges}), so an installer that needs root is described
|
|
477
|
+
* under {@link NetworkManifest.privileged} instead and is only ever shown to
|
|
478
|
+
* copy. Without that refusal the two would be one field with two meanings,
|
|
479
|
+
* and the surface offering a button would have to guess.
|
|
480
|
+
*/
|
|
481
|
+
command: string;
|
|
482
|
+
/**
|
|
483
|
+
* URL of the installation documentation.
|
|
484
|
+
*
|
|
485
|
+
* Rendered as a link on an admin page, so it must be `http:` or `https:`
|
|
486
|
+
* and the parser refuses anything else — see {@link isDocsUrl}.
|
|
487
|
+
*/
|
|
488
|
+
docsUrl: string;
|
|
489
|
+
}
|
|
490
|
+
/** One step an operator must run themselves, because the host may not. */
|
|
491
|
+
interface PrivilegedStep {
|
|
492
|
+
/** What it does, in a few words: "Install the daemon" */
|
|
493
|
+
label: string;
|
|
494
|
+
/** The command to copy. Usually `sudo …`. */
|
|
495
|
+
command: string;
|
|
496
|
+
/** Where the vendor documents it; `http:`/`https:` only, as {@link isDocsUrl} requires. */
|
|
497
|
+
docsUrl?: string;
|
|
498
|
+
/**
|
|
499
|
+
* Which ALTERNATIVE this step belongs to, and the heading it renders under.
|
|
500
|
+
*
|
|
501
|
+
* Steps sharing a group are one sequence to run in order; different groups
|
|
502
|
+
* are different ways to arrive at the same place, and a surface renders an
|
|
503
|
+
* `or` between them rather than continuing the numbering. Steps with no
|
|
504
|
+
* group are one plain sequence, which is what every step was before this
|
|
505
|
+
* existed.
|
|
506
|
+
*
|
|
507
|
+
* It exists because a platform can genuinely offer two routes: on macOS
|
|
508
|
+
* Tailscale ships both a GUI app and a command-line daemon, and a page that
|
|
509
|
+
* numbered those 1..3 told a person to install both. Non-empty when present
|
|
510
|
+
* — it is a heading, so `""` is refused rather than coerced.
|
|
511
|
+
*/
|
|
512
|
+
group?: string;
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* The `subshell.network` block. Required when `type` is `network`.
|
|
516
|
+
*
|
|
517
|
+
* Everything here is DATA a host reads without loading plugin code: which
|
|
518
|
+
* machines this plugin can run on at all, whether it can produce a login URL,
|
|
519
|
+
* what publishing it exposes, and which steps only a human with root can
|
|
520
|
+
* perform. A page can therefore say "not available on this platform" or print
|
|
521
|
+
* the two sudo commands before any plugin code has been imported — and before
|
|
522
|
+
* the vendor's CLI is anywhere on the machine.
|
|
523
|
+
*/
|
|
524
|
+
interface NetworkManifest {
|
|
525
|
+
/**
|
|
526
|
+
* Operating systems this plugin can be driven on.
|
|
527
|
+
*
|
|
528
|
+
* Not a hint: a host refuses every act on a platform absent from this list,
|
|
529
|
+
* and the UI renders the row as unavailable rather than offering a button
|
|
530
|
+
* that would 409. Vendors differ here for real reasons — a CLI that talks to
|
|
531
|
+
* a logged-in desktop session cannot be driven from a launchd service.
|
|
532
|
+
*/
|
|
533
|
+
platforms: PluginPlatform[];
|
|
534
|
+
/**
|
|
535
|
+
* True when `join({})` with no credential can yield a URL a human finishes.
|
|
536
|
+
*
|
|
537
|
+
* Data rather than a capability because it describes what a RETURN VALUE may
|
|
538
|
+
* be, and a capability is checked against members. The UI reads it to decide
|
|
539
|
+
* whether to offer "sign in" beside "paste a key".
|
|
540
|
+
*/
|
|
541
|
+
interactiveLogin?: boolean;
|
|
542
|
+
/**
|
|
543
|
+
* True when publishing leaves NOTHING the daemon can later be asked about.
|
|
544
|
+
*
|
|
545
|
+
* NetBird is the case that forced the field: a join is the whole publish,
|
|
546
|
+
* so `status()` can only ever see the join, and a plugin that declared
|
|
547
|
+
* itself `published` would claim a thing its own vendor CLI never told it.
|
|
548
|
+
* The HOST's publish record is the one witness of the transition, and this
|
|
549
|
+
* flag is what lets the host (and only the host) upgrade `joined` to
|
|
550
|
+
* `published` from it. A plugin without the flag is never upgraded: for
|
|
551
|
+
* Tailscale a serve reset outside this app is a real state, and `joined`
|
|
552
|
+
* is the honest reading of it.
|
|
553
|
+
*/
|
|
554
|
+
publishImplicit?: boolean;
|
|
555
|
+
/**
|
|
556
|
+
* What publishing on this network exposes the server to.
|
|
557
|
+
*
|
|
558
|
+
* `private` is a network only invited machines are on. `public-with-gate`
|
|
559
|
+
* reaches the open internet with an identity check in front — a different
|
|
560
|
+
* security posture, stated here so every surface can say so before the act
|
|
561
|
+
* rather than after it.
|
|
562
|
+
*/
|
|
563
|
+
exposure: "private" | "public-with-gate";
|
|
564
|
+
/**
|
|
565
|
+
* Steps the host can never perform, per platform, in the order to run them.
|
|
566
|
+
*
|
|
567
|
+
* Every mesh VPN installs a daemon as root and the server has no terminal to
|
|
568
|
+
* answer a password prompt, so these are printed to copy and never executed.
|
|
569
|
+
* Keeping them out of {@link InstallSpec} is what lets a surface offer a
|
|
570
|
+
* button for one and a copy row for the other without inspecting the string.
|
|
571
|
+
*/
|
|
572
|
+
privileged?: Partial<Record<PluginPlatform, PrivilegedStep[]>>;
|
|
573
|
+
/**
|
|
574
|
+
* What this network calls the two things a person acts on.
|
|
575
|
+
*
|
|
576
|
+
* Vendors name the same act differently and a generic word is wrong rather
|
|
577
|
+
* than merely bland: Tailscale takes an "auth key", NetBird a "setup key",
|
|
578
|
+
* Cloudflare a "tunnel token", and a field labelled "Auth key" on the
|
|
579
|
+
* NetBird row asks for something NetBird does not have. Likewise publishing
|
|
580
|
+
* is "Tailscale Serve" on one network and starting a tunnel on another.
|
|
581
|
+
*
|
|
582
|
+
* MANIFEST data rather than something the plugin returns, for the same
|
|
583
|
+
* reason `platforms` is: a surface renders these before any plugin code has
|
|
584
|
+
* been loaded, and before the vendor's CLI is anywhere on the machine. Both
|
|
585
|
+
* are optional and both have a sensible generic default, so a plugin that
|
|
586
|
+
* says nothing is merely plain rather than broken.
|
|
587
|
+
*/
|
|
588
|
+
labels?: NetworkLabels;
|
|
589
|
+
}
|
|
590
|
+
/** The vendor's own words for the two acts a person takes. See {@link NetworkManifest.labels}. */
|
|
591
|
+
interface NetworkLabels {
|
|
592
|
+
/** What to call the pasted credential: "Auth key", "Setup key", "Tunnel token". */
|
|
593
|
+
credential?: string;
|
|
594
|
+
/** What to call publishing: "Publish with Tailscale Serve", "Start tunnel". */
|
|
595
|
+
publish?: string;
|
|
596
|
+
/**
|
|
597
|
+
* Where the credential named by `credential` comes from — the vendor page
|
|
598
|
+
* that mints one.
|
|
599
|
+
*
|
|
600
|
+
* The credential box asks for an "Auth key" on a card that otherwise says
|
|
601
|
+
* nothing about what an auth key is or where to get it, and every vendor
|
|
602
|
+
* words that differently enough that the SPA cannot write the sentence
|
|
603
|
+
* itself. So the plugin names its page, and the card renders it as a Docs
|
|
604
|
+
* link on the label row. http(s) only, refused at parse like every other
|
|
605
|
+
* URL this contract carries — it lands in an `href` on an admin page.
|
|
606
|
+
*/
|
|
607
|
+
credentialDocsUrl?: string;
|
|
608
|
+
}
|
|
609
|
+
/** The parsed `subshell` block. */
|
|
610
|
+
interface SubshellManifest {
|
|
611
|
+
/** plugin-api version this plugin was built against */
|
|
612
|
+
apiVersion: number;
|
|
613
|
+
/** Stable id, also its directory name */
|
|
614
|
+
id: string;
|
|
615
|
+
/** What kind of thing this provides */
|
|
616
|
+
type: PluginType;
|
|
617
|
+
/** Display name */
|
|
618
|
+
name: string;
|
|
619
|
+
/** One-line description shown in the UI */
|
|
620
|
+
description: string;
|
|
621
|
+
/**
|
|
622
|
+
* Relative path to this plugin's icon file, e.g. `icon.svg`. Served by
|
|
623
|
+
* the control plane at `/api/plugins/<id>/icon`; absent means the UI
|
|
624
|
+
* renders a monogram instead.
|
|
625
|
+
*/
|
|
626
|
+
icon?: string;
|
|
627
|
+
/** Module to import, relative to the package directory */
|
|
628
|
+
entry: string;
|
|
629
|
+
/** Manifest-driven detection; absent for a plugin that needs no binary */
|
|
630
|
+
detect?: DetectSpec;
|
|
631
|
+
/** Install guidance shown when the binary is missing */
|
|
632
|
+
install?: InstallSpec;
|
|
633
|
+
/**
|
|
634
|
+
* Environment variables this plugin computes against (spec 2026-09-10 §5).
|
|
635
|
+
*
|
|
636
|
+
* The node reports the VALUES of exactly these names at `ready`, so the
|
|
637
|
+
* reported set grows by declaration rather than a machine shipping its
|
|
638
|
+
* whole environment to the control plane. Declaring data is deliberate: a
|
|
639
|
+
* WRONG name is silent, yielding an absent key and the plugin's own
|
|
640
|
+
* fallback, i.e. a resume that quietly never offers itself (§11).
|
|
641
|
+
*/
|
|
642
|
+
hostEnv?: string[];
|
|
643
|
+
/** Network facts. Present exactly when `type` is `network`. */
|
|
644
|
+
network?: NetworkManifest;
|
|
645
|
+
/**
|
|
646
|
+
* False means the launch pickers must not offer this plugin, even though
|
|
647
|
+
* it drives a pane. Absent means true.
|
|
648
|
+
*
|
|
649
|
+
* The declaration belongs to the plugin because only it knows whether a
|
|
650
|
+
* bare launch means something: a harness whose useful launch needs inputs
|
|
651
|
+
* no picker carries (SSH needs a destination, which the connect form
|
|
652
|
+
* supplies through its own door) would otherwise be a row that starts a
|
|
653
|
+
* dying pane. This hides the entry, not the route: the plugin stays
|
|
654
|
+
* launchable by id for whatever flow does supply the inputs.
|
|
655
|
+
*/
|
|
656
|
+
pickable?: boolean;
|
|
657
|
+
}
|
|
658
|
+
//#endregion
|
|
659
|
+
//#region src/manifest.d.ts
|
|
660
|
+
declare const manifest: SubshellManifest;
|
|
661
|
+
//#endregion
|
|
662
|
+
//#region src/index.d.ts
|
|
663
|
+
/**
|
|
664
|
+
* Oh My Pi uses its own executable and native MCP discovery, independently of
|
|
665
|
+
* pi. An explicit extension directory contributes mcp.json without redirecting
|
|
666
|
+
* the user's config or credentials. Verified against omp 18.12.6.
|
|
667
|
+
*/
|
|
668
|
+
declare const createPlugin: HarnessPluginFactory;
|
|
669
|
+
//#endregion
|
|
670
|
+
export { createPlugin as default, manifest };
|