@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.
package/dist/index.js ADDED
@@ -0,0 +1,458 @@
1
+ //#region ../../plugin-api/dist/manifest-XGvw0ugx.js
2
+ /** Every {@link PluginType}, for anything that has to iterate or validate them. */
3
+ const PLUGIN_TYPES = [
4
+ "agent-harness",
5
+ "terminal",
6
+ "network"
7
+ ];
8
+ /** Every {@link PluginPlatform}, for validation. */
9
+ const PLUGIN_PLATFORMS = ["darwin", "linux"];
10
+ /**
11
+ * The MCP server name every harness registers the built-in `subshell mcp`
12
+ * server under: the config object key, and the `hermes mcp add|remove`
13
+ * argument alike. The server reports the same name in its MCP handshake, which
14
+ * is what harnesses surface in wire tool ids (`mcp__<name>__<tool>`).
15
+ */
16
+ const MCP_SERVER_NAME = "subshell";
17
+ /**
18
+ * Image formats a plugin icon may be. The server maps these to a Content-Type
19
+ * from a fixed table rather than sniffing the file, so this list and that
20
+ * table are one decision: adding a format here without adding it there leaves
21
+ * an icon that parses and then 500s when something asks for it.
22
+ */
23
+ const ICON_EXTENSIONS = [
24
+ ".svg",
25
+ ".png",
26
+ ".webp"
27
+ ];
28
+ /**
29
+ * Ids become directory names under `<dataDir>/plugins/`, so they are path
30
+ * segments and nothing else: no separators, no traversal, no surprises from a
31
+ * case-insensitive filesystem.
32
+ */
33
+ const ID_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
34
+ function isRecord(value) {
35
+ return typeof value === "object" && value !== null && !Array.isArray(value);
36
+ }
37
+ /** The privilege wrappers no install command may ever run, by first-word basename. */
38
+ const PRIVILEGED_COMMANDS = [
39
+ "sudo",
40
+ "doas",
41
+ "pkexec"
42
+ ];
43
+ /** Shell boundaries that introduce a NEW command word: `;` `|` `&` (so `&&` and `||` too) and newline. */
44
+ const COMMAND_BOUNDARY_RE = /[;&|\n]+/;
45
+ /** A leading `NAME=…` env assignment, which `sh` strips before choosing the command. */
46
+ const ENV_ASSIGNMENT_RE = /^[A-Za-z_][A-Za-z0-9_]*=\S*\s*/;
47
+ /**
48
+ * Whether a shell line runs one of {@link PRIVILEGED_COMMANDS} AS A COMMAND
49
+ * anywhere in it — not merely at the start.
50
+ *
51
+ * `install.command` is the one manifest field a host RUNS, and it runs through
52
+ * `sh -c` (`api/network/install-network.route.ts`), which means any boundary
53
+ * in the line can start the privileged command the start of the line hides:
54
+ * `"apt-get update && sudo apt-get install -y x"` is sudo-without-prompt on a
55
+ * NOPASSWD host. A line-start regex was the first gate and it was not one.
56
+ *
57
+ * The split is CONSERVATIVE, not a shell parser: segments are taken at every
58
+ * boundary character quote-blind (so a `;` inside a quoted string still
59
+ * counts, which can only over-refuse), leading env assignments are stripped
60
+ * (`FOO=bar sudo x` runs sudo), and each segment's first word is compared by
61
+ * BASENAME (`/usr/bin/sudo` is sudo, `pkexec-demo` is not). A word merely
62
+ * CONTAINING the token is fine — the comparison is equality on the basename.
63
+ * The accepted limit is a wrapper hidden inside quotes (`sh -c 'sudo …'`),
64
+ * which is the same limit `PluginHost.run`'s first-word check has, and it is
65
+ * accepted for the same reason: a plugin whose code LOADS can already run
66
+ * anything in this process (`docs/security.md` §11.9) — this rule refuses
67
+ * careless manifests deterministically, it is not a sandbox.
68
+ */
69
+ function needsPrivileges(command) {
70
+ for (const segment of command.split(COMMAND_BOUNDARY_RE)) {
71
+ let rest = segment.trim();
72
+ while (ENV_ASSIGNMENT_RE.test(rest)) rest = rest.replace(ENV_ASSIGNMENT_RE, "");
73
+ if (rest === "") continue;
74
+ const ws = rest.search(/\s/);
75
+ const firstWord = ws === -1 ? rest : rest.slice(0, ws);
76
+ const base = firstWord.slice(firstWord.lastIndexOf("/") + 1);
77
+ if (PRIVILEGED_COMMANDS.includes(base)) return true;
78
+ }
79
+ return false;
80
+ }
81
+ /**
82
+ * Whether a string may be rendered as a link in an operator's browser.
83
+ *
84
+ * Every `docsUrl` in this contract ends up as the `href` of an anchor on an
85
+ * admin page, and an `href` is not inert: a `javascript:` URL in one is script
86
+ * running on the control plane's own origin, in the session of the one person
87
+ * who can install plugins. So a URL this contract carries is restricted to the
88
+ * two schemes that navigate, and both the parser below and the host check it.
89
+ *
90
+ * It is checked in two places on purpose. A manifest is static data, so a bad
91
+ * value there is a plugin defect and is refused at load. A URL a plugin
92
+ * REPORTS at runtime is often something it read off a vendor CLI — Tailscale's
93
+ * `AuthURL` comes from whichever control server the operator pointed the
94
+ * daemon at — so the host drops that one instead, because refusing to load a
95
+ * working plugin over a value its control server chose would be the wrong
96
+ * failure.
97
+ *
98
+ * Not a reachability claim: nothing fetches these, and a docs page that 404s
99
+ * is a broken link rather than a hazard.
100
+ * @param value - the candidate URL, as written in a manifest or returned by a plugin
101
+ */
102
+ function isDocsUrl(value) {
103
+ let parsed;
104
+ try {
105
+ parsed = new URL(value);
106
+ } catch {
107
+ return false;
108
+ }
109
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
110
+ }
111
+ /**
112
+ * Reads and validates the `subshell` block of a plugin's package.json.
113
+ *
114
+ * Answers `{ error }` rather than throwing, because every caller is reporting
115
+ * a broken plugin to a person rather than handling an exception: the node
116
+ * marks the plugin broken and renders this message beside it. The strings are
117
+ * therefore specific on purpose.
118
+ * @param pkgJson - the parsed package.json of a plugin directory
119
+ */
120
+ function parseManifest(pkgJson) {
121
+ if (!isRecord(pkgJson)) return { error: "package.json is not an object" };
122
+ const block = pkgJson.subshell;
123
+ if (!isRecord(block)) return { error: "package.json has no `subshell` block, so it is not a Subshell plugin" };
124
+ if (typeof block.apiVersion !== "number" || !Number.isInteger(block.apiVersion)) return { error: "`subshell.apiVersion` must be an integer" };
125
+ if (block.apiVersion < 1) return { error: `this plugin declares plugin-api ${block.apiVersion}, which is not a version; the lowest is 1 and this host implements 2` };
126
+ if (block.apiVersion > 2) return { error: `this plugin needs plugin-api ${block.apiVersion}, but this host implements 2; upgrade Subshell` };
127
+ if (typeof block.id !== "string" || !ID_RE.test(block.id)) return { error: "`subshell.id` must be lowercase letters, digits and hyphens, starting alphanumeric (it becomes a directory name)" };
128
+ if (typeof block.type !== "string" || !PLUGIN_TYPES.includes(block.type)) return { error: `\`subshell.type\` must be one of: ${PLUGIN_TYPES.join(", ")}` };
129
+ if (typeof block.name !== "string" || block.name.trim() === "") return { error: "`subshell.name` must be a non-empty string" };
130
+ if (typeof block.description !== "string") return { error: "`subshell.description` must be a string" };
131
+ if (block.icon !== void 0) {
132
+ if (typeof block.icon !== "string" || block.icon.trim() === "") return { error: "`subshell.icon` must be a non-empty relative path to an image in the package" };
133
+ if (block.icon.startsWith("/") || block.icon.split("/").includes("..")) return { error: "`subshell.icon` must stay inside the package (no leading `/` and no `..` segment)" };
134
+ const icon = block.icon;
135
+ if (!ICON_EXTENSIONS.some((ext) => icon.endsWith(ext))) return { error: `\`subshell.icon\` must end in one of: ${ICON_EXTENSIONS.join(", ")}` };
136
+ }
137
+ if (block.pickable !== void 0 && typeof block.pickable !== "boolean") return { error: "`subshell.pickable` must be a boolean when present" };
138
+ if (typeof block.entry !== "string" || block.entry.trim() === "") return { error: "`subshell.entry` must be a non-empty relative path" };
139
+ if (block.entry.startsWith("/") || block.entry.split("/").includes("..")) return { error: "`subshell.entry` must stay inside the package (no leading `/` and no `..` segment)" };
140
+ let detect;
141
+ if (block.detect !== void 0) {
142
+ const d = block.detect;
143
+ if (!isRecord(d) || typeof d.binaryName !== "string" || typeof d.envOverride !== "string" || !Array.isArray(d.knownPaths) || !d.knownPaths.every((k) => typeof k === "string")) return { error: "`subshell.detect` needs binaryName, envOverride and a knownPaths array of strings" };
144
+ detect = {
145
+ binaryName: d.binaryName,
146
+ envOverride: d.envOverride,
147
+ knownPaths: d.knownPaths
148
+ };
149
+ }
150
+ let install;
151
+ if (block.install !== void 0) {
152
+ const i = block.install;
153
+ if (!isRecord(i) || typeof i.command !== "string" || typeof i.docsUrl !== "string") return { error: "`subshell.install` needs a command and a docsUrl" };
154
+ if (needsPrivileges(i.command)) return { error: "`subshell.install.command` must not need sudo, doas or pkexec — the host runs it through a shell and has no terminal for a password prompt; put privileged steps in `subshell.network.privileged`" };
155
+ if (!isDocsUrl(i.docsUrl)) return { error: "`subshell.install.docsUrl` must be an http(s) URL — it is rendered as a link on an admin page" };
156
+ install = {
157
+ command: i.command,
158
+ docsUrl: i.docsUrl
159
+ };
160
+ }
161
+ let hostEnv;
162
+ if (block.hostEnv !== void 0) {
163
+ if (!Array.isArray(block.hostEnv) || !block.hostEnv.every((k) => typeof k === "string" && k.trim() !== "")) return { error: "`subshell.hostEnv` must be an array of environment variable names" };
164
+ hostEnv = [...block.hostEnv];
165
+ }
166
+ const type = block.type;
167
+ const parsedNetwork = parseNetworkBlock(block.network, type);
168
+ if (parsedNetwork !== void 0 && "error" in parsedNetwork) return parsedNetwork;
169
+ return {
170
+ apiVersion: block.apiVersion,
171
+ id: block.id,
172
+ type,
173
+ name: block.name,
174
+ description: block.description,
175
+ ...typeof block.icon === "string" ? { icon: block.icon } : {},
176
+ entry: block.entry,
177
+ ...detect ? { detect } : {},
178
+ ...install ? { install } : {},
179
+ ...hostEnv ? { hostEnv } : {},
180
+ ...parsedNetwork ? { network: parsedNetwork } : {},
181
+ ...block.pickable === false ? { pickable: false } : {}
182
+ };
183
+ }
184
+ /**
185
+ * Parses `subshell.network`, which is required for and exclusive to `network`.
186
+ *
187
+ * Both directions are refused rather than tolerated. A network plugin without
188
+ * the block would be unrenderable — nothing could say which platforms it runs
189
+ * on or what publishing it exposes, and the safe default for "what does this
190
+ * expose" is not a default anyone should pick silently. A harness WITH one
191
+ * declares facts nothing reads, which is how a manifest starts lying.
192
+ * @returns the parsed block, an error, or undefined when there is none to have
193
+ */
194
+ function parseNetworkBlock(raw, type) {
195
+ if (type !== "network") {
196
+ if (raw !== void 0) return { error: `\`subshell.network\` is only for \`type: "network"\` plugins, and this one is \`${type}\`` };
197
+ return;
198
+ }
199
+ if (!isRecord(raw)) return { error: "`subshell.network` is required for `type: \"network\"` plugins" };
200
+ if (!Array.isArray(raw.platforms) || raw.platforms.length === 0 || !raw.platforms.every((p) => typeof p === "string" && PLUGIN_PLATFORMS.includes(p))) return { error: `\`subshell.network.platforms\` must be a non-empty array of: ${PLUGIN_PLATFORMS.join(", ")}` };
201
+ if (raw.exposure !== "private" && raw.exposure !== "public-with-gate") return { error: "`subshell.network.exposure` must be \"private\" or \"public-with-gate\"" };
202
+ if (raw.interactiveLogin !== void 0 && typeof raw.interactiveLogin !== "boolean") return { error: "`subshell.network.interactiveLogin` must be a boolean" };
203
+ if (raw.publishImplicit !== void 0 && typeof raw.publishImplicit !== "boolean") return { error: "`subshell.network.publishImplicit` must be a boolean" };
204
+ let privileged;
205
+ if (raw.privileged !== void 0) {
206
+ if (!isRecord(raw.privileged)) return { error: "`subshell.network.privileged` must be an object keyed by platform" };
207
+ privileged = {};
208
+ for (const [platform, steps] of Object.entries(raw.privileged)) {
209
+ if (!PLUGIN_PLATFORMS.includes(platform)) return { error: `\`subshell.network.privileged\` has an unknown platform "${platform}"` };
210
+ if (!Array.isArray(steps) || !steps.every((s) => isRecord(s) && typeof s.label === "string" && s.label.trim() !== "" && typeof s.command === "string" && s.command.trim() !== "" && (s.docsUrl === void 0 || typeof s.docsUrl === "string") && (s.group === void 0 || typeof s.group === "string"))) return { error: `\`subshell.network.privileged.${platform}\` must be an array of { label, command, docsUrl?, group? }` };
211
+ const badGroup = steps.find((s) => s.group !== void 0 && s.group.trim() === "");
212
+ if (badGroup) return { error: `\`subshell.network.privileged.${platform}\` has an empty \`group\`, which is a heading with no words: "${badGroup.label}"` };
213
+ const badDocs = steps.find((s) => s.docsUrl !== void 0 && !isDocsUrl(s.docsUrl));
214
+ if (badDocs) return { error: `\`subshell.network.privileged.${platform}\` has a docsUrl that is not an http(s) URL: "${badDocs.label}"` };
215
+ privileged[platform] = steps.map((s) => ({
216
+ label: s.label,
217
+ command: s.command,
218
+ ...s.docsUrl ? { docsUrl: s.docsUrl } : {},
219
+ ...s.group ? { group: s.group } : {}
220
+ }));
221
+ }
222
+ }
223
+ let labels;
224
+ if (raw.labels !== void 0) {
225
+ if (!isRecord(raw.labels)) return { error: "`subshell.network.labels` must be an object" };
226
+ for (const key of ["credential", "publish"]) {
227
+ const value = raw.labels[key];
228
+ if (value !== void 0 && (typeof value !== "string" || value.trim() === "")) return { error: `\`subshell.network.labels.${key}\` must be a non-empty string` };
229
+ }
230
+ const credDocs = raw.labels.credentialDocsUrl;
231
+ if (credDocs !== void 0 && (typeof credDocs !== "string" || !isDocsUrl(credDocs))) return { error: "`subshell.network.labels.credentialDocsUrl` must be an http(s) URL — it is rendered as a link on an admin page" };
232
+ labels = {
233
+ ...typeof raw.labels.credential === "string" ? { credential: raw.labels.credential } : {},
234
+ ...typeof raw.labels.publish === "string" ? { publish: raw.labels.publish } : {},
235
+ ...typeof raw.labels.credentialDocsUrl === "string" ? { credentialDocsUrl: raw.labels.credentialDocsUrl } : {}
236
+ };
237
+ }
238
+ return {
239
+ platforms: [...raw.platforms],
240
+ exposure: raw.exposure,
241
+ ...raw.interactiveLogin === true ? { interactiveLogin: true } : {},
242
+ ...raw.publishImplicit === true ? { publishImplicit: true } : {},
243
+ ...privileged ? { privileged } : {},
244
+ ...labels && Object.keys(labels).length > 0 ? { labels } : {}
245
+ };
246
+ }
247
+ //#endregion
248
+ //#region ../../plugin-api/dist/index.js
249
+ /**
250
+ * The preset checks every built-in harness performs unchanged: a non-empty
251
+ * name, string env values, and flags that look like flags. Harnesses with
252
+ * extra rules run their own checks on top of these.
253
+ * @param preset - The decoded preset definition to check
254
+ * @returns Issues per field; `valid` is true when no issues were found
255
+ */
256
+ function validateGenericPreset(preset) {
257
+ const issues = [];
258
+ if (!preset.name || preset.name.trim().length === 0) issues.push({
259
+ field: "name",
260
+ message: "Preset name is required."
261
+ });
262
+ for (const [key, value] of Object.entries(preset.env)) if (typeof value !== "string") issues.push({
263
+ field: "env",
264
+ message: `Env var "${key}" must be a string.`
265
+ });
266
+ for (const flag of preset.flags) if (!flag.startsWith("-")) issues.push({
267
+ field: "flags",
268
+ message: `Flag "${flag}" must start with "-".`
269
+ });
270
+ return {
271
+ valid: issues.length === 0,
272
+ issues
273
+ };
274
+ }
275
+ //#endregion
276
+ //#region src/manifest.ts
277
+ /**
278
+ * This package's own `subshell` block, read from its package.json so the
279
+ * manifest has ONE source of truth.
280
+ *
281
+ * A host that installs this plugin at runtime reads package.json off disk; a
282
+ * host that ships it built in imports this. Both must describe the same
283
+ * plugin, and the only way to guarantee that is for them to be the same bytes.
284
+ */
285
+ const parsed = parseManifest({
286
+ name: "@subshell-ai/plugin-oh-my-pi",
287
+ description: "Oh My Pi harness plugin for Subshell",
288
+ version: "0.2.0",
289
+ license: "Apache-2.0",
290
+ type: "module",
291
+ main: "dist/index.js",
292
+ exports: { ".": {
293
+ "types": "./dist/index.d.ts",
294
+ "import": "./dist/index.js"
295
+ } },
296
+ types: "dist/index.d.ts",
297
+ homepage: "https://subshell.sh",
298
+ repository: {
299
+ "type": "git",
300
+ "url": "git+https://github.com/subshell-ai/subshell.git",
301
+ "directory": "packages/plugins/oh-my-pi"
302
+ },
303
+ bugs: { "url": "https://github.com/subshell-ai/subshell/issues" },
304
+ scripts: {
305
+ "build": "tsdown",
306
+ "build:dev": "hash-runner",
307
+ "clean": "rm -rf .turbo node_modules dist .hashes.json",
308
+ "lint": "biome check --write --unsafe src && biome format src --write && biome lint src --fix",
309
+ "lint:check": "biome check --no-errors-on-unmatched src",
310
+ "lint:staged": "biome check --no-errors-on-unmatched --write --unsafe --staged src",
311
+ "test": "bash \"$(git rev-parse --show-toplevel)/scripts/par-test.sh\" 4 bun test",
312
+ "verify-types": "tsc --project tsconfig.json --noEmit"
313
+ },
314
+ devDependencies: {
315
+ "@internal/tsconfig": "workspace:*",
316
+ "@subshell-ai/plugin-api": "workspace:*",
317
+ "@types/bun": "1.3.14",
318
+ "hash-runner": "4.0.0",
319
+ "tsdown": "0.22.14",
320
+ "typescript": "7.0.2"
321
+ },
322
+ files: ["dist", "icon.svg"],
323
+ publishConfig: { "access": "public" },
324
+ subshell: {
325
+ "apiVersion": 2,
326
+ "id": "oh-my-pi",
327
+ "type": "agent-harness",
328
+ "name": "Oh My Pi",
329
+ "description": "Extensible coding agent from omp.sh (interactive TUI)",
330
+ "icon": "icon.svg",
331
+ "entry": "dist/index.js",
332
+ "detect": {
333
+ "binaryName": "omp",
334
+ "envOverride": "OMP_PATH",
335
+ "knownPaths": [".bun/bin/omp", ".local/bin/omp"]
336
+ },
337
+ "install": {
338
+ "command": "curl -fsSL https://omp.sh/install | sh",
339
+ "docsUrl": "https://github.com/can1357/oh-my-pi#install"
340
+ }
341
+ }
342
+ });
343
+ if ("error" in parsed) throw new Error(`@subshell-ai/plugin-oh-my-pi has an invalid manifest: ${parsed.error}`);
344
+ const manifest = parsed;
345
+ //#endregion
346
+ //#region src/index.ts
347
+ const SETTINGS = [
348
+ {
349
+ key: "model",
350
+ label: "Model",
351
+ description: "Model ID or pattern passed to omp",
352
+ type: "string"
353
+ },
354
+ {
355
+ key: "provider",
356
+ label: "Provider",
357
+ description: "Provider name, such as anthropic or openai",
358
+ type: "string"
359
+ },
360
+ {
361
+ key: "thinking",
362
+ label: "Thinking level",
363
+ description: "Reasoning effort for the subshell",
364
+ type: "select",
365
+ choices: [
366
+ "off",
367
+ "minimal",
368
+ "low",
369
+ "medium",
370
+ "high",
371
+ "xhigh",
372
+ "max",
373
+ "auto"
374
+ ]
375
+ }
376
+ ];
377
+ /**
378
+ * Oh My Pi uses its own executable and native MCP discovery, independently of
379
+ * pi. An explicit extension directory contributes mcp.json without redirecting
380
+ * the user's config or credentials. Verified against omp 18.12.6.
381
+ */
382
+ const createPlugin = () => ({
383
+ capabilities: () => ["mcp", "settings"],
384
+ buildCommand({ binary, preset, extraFlags, mcp }) {
385
+ const args = [binary, ...mcp?.args ?? []];
386
+ const settings = preset.settings ?? {};
387
+ for (const key of [
388
+ "model",
389
+ "provider",
390
+ "thinking"
391
+ ]) {
392
+ const value = settings[key];
393
+ if (typeof value === "string" && value) args.push(`--${key}`, value);
394
+ }
395
+ args.push(...preset.flags, ...extraFlags ?? []);
396
+ return args;
397
+ },
398
+ mcpRegistration(launch, configPath) {
399
+ const fileContent = `${JSON.stringify({ mcpServers: { [MCP_SERVER_NAME]: {
400
+ type: "stdio",
401
+ command: launch.command,
402
+ args: launch.args
403
+ } } }, null, 2)}\n`;
404
+ return {
405
+ fileContent,
406
+ files: {
407
+ "package.json": JSON.stringify({
408
+ name: "subshell-mcp",
409
+ private: true,
410
+ type: "module",
411
+ omp: { extensions: ["bridge.mjs"] }
412
+ }),
413
+ "bridge.mjs": "export default function () {}\n",
414
+ "mcp.json": fileContent
415
+ },
416
+ args: ["--extension", `${configPath}.d`]
417
+ };
418
+ },
419
+ mcpSetup() {
420
+ return {
421
+ mode: "auto",
422
+ summary: "Subshell registers its MCP server through a per-launch Oh My Pi extension. Your existing configuration stays in place."
423
+ };
424
+ },
425
+ validatePreset: validateGenericPreset,
426
+ presetSettings: () => SETTINGS,
427
+ suggestedEnv: () => [{
428
+ key: "ANTHROPIC_API_KEY",
429
+ description: "Anthropic API key"
430
+ }, {
431
+ key: "OPENAI_API_KEY",
432
+ description: "OpenAI API key"
433
+ }],
434
+ suggestedFlags: () => [
435
+ {
436
+ flag: "--model anthropic/claude-sonnet-4-5",
437
+ description: "Model in provider/model form"
438
+ },
439
+ {
440
+ flag: "--thinking high",
441
+ description: "Reasoning effort for the subshell"
442
+ },
443
+ {
444
+ flag: "--tools read,grep,find",
445
+ description: "Tools to enable"
446
+ },
447
+ {
448
+ flag: "--append-system-prompt <text>",
449
+ description: "Append instructions to the system prompt"
450
+ },
451
+ {
452
+ flag: "--no-session",
453
+ description: "Don't save the conversation"
454
+ }
455
+ ]
456
+ });
457
+ //#endregion
458
+ export { createPlugin as default, manifest };
package/icon.svg ADDED
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><title>Oh My Pi</title><rect width="64" height="64" rx="12" fill="#171717"/><path d="M12 18h14l15 28h11M38 18h14" fill="none" stroke="#fff" stroke-width="5" stroke-linejoin="round"/></svg>
package/package.json CHANGED
@@ -1,6 +1,70 @@
1
1
  {
2
2
  "name": "@subshell-ai/plugin-oh-my-pi",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "description": "Oh My Pi harness plugin for Subshell",
4
+ "version": "0.2.0",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "main": "dist/index.js",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "types": "dist/index.d.ts",
15
+ "homepage": "https://subshell.sh",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/subshell-ai/subshell.git",
19
+ "directory": "packages/plugins/oh-my-pi"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/subshell-ai/subshell/issues"
23
+ },
24
+ "scripts": {
25
+ "build": "tsdown",
26
+ "build:dev": "hash-runner",
27
+ "clean": "rm -rf .turbo node_modules dist .hashes.json",
28
+ "lint": "biome check --write --unsafe src && biome format src --write && biome lint src --fix",
29
+ "lint:check": "biome check --no-errors-on-unmatched src",
30
+ "lint:staged": "biome check --no-errors-on-unmatched --write --unsafe --staged src",
31
+ "test": "bash \"$(git rev-parse --show-toplevel)/scripts/par-test.sh\" 4 bun test",
32
+ "verify-types": "tsc --project tsconfig.json --noEmit"
33
+ },
34
+ "devDependencies": {
35
+ "@internal/tsconfig": "workspace:*",
36
+ "@subshell-ai/plugin-api": "workspace:*",
37
+ "@types/bun": "1.3.14",
38
+ "hash-runner": "4.0.0",
39
+ "tsdown": "0.22.14",
40
+ "typescript": "7.0.2"
41
+ },
42
+ "files": [
43
+ "dist",
44
+ "icon.svg"
45
+ ],
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "subshell": {
50
+ "apiVersion": 2,
51
+ "id": "oh-my-pi",
52
+ "type": "agent-harness",
53
+ "name": "Oh My Pi",
54
+ "description": "Extensible coding agent from omp.sh (interactive TUI)",
55
+ "icon": "icon.svg",
56
+ "entry": "dist/index.js",
57
+ "detect": {
58
+ "binaryName": "omp",
59
+ "envOverride": "OMP_PATH",
60
+ "knownPaths": [
61
+ ".bun/bin/omp",
62
+ ".local/bin/omp"
63
+ ]
64
+ },
65
+ "install": {
66
+ "command": "curl -fsSL https://omp.sh/install | sh",
67
+ "docsUrl": "https://github.com/can1357/oh-my-pi#install"
68
+ }
69
+ }
70
+ }