@subshell-ai/plugin-oh-my-pi 0.0.0-stage → 0.2.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.
@@ -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 };