@subshell-ai/plugin-oh-my-pi 0.2.3 → 0.3.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.d.ts CHANGED
@@ -1,4 +1,31 @@
1
- //#region ../../plugin-api/dist/types-DPriUYLa.d.ts
1
+ //#region ../../plugin-api/dist/types-DPLl6DHG.d.ts
2
+ //#region src/previews.d.ts
3
+ /**
4
+ * How a harness reports loopback ports its own tooling started, for the
5
+ * preview picker.
6
+ *
7
+ * Port discovery attributes sockets by walking the pane's process tree. A
8
+ * harness that brokers its tool shells through a shared daemon (oh-my-pi
9
+ * does) leaves that tree, so a server its tools start is reachable but
10
+ * invisible. A plugin that declares the `previews` capability ships a
11
+ * reporter the control plane can list those ports from.
12
+ *
13
+ * The report carries ports only, never a host: the node dials loopback, so a
14
+ * report cannot point the proxy at another machine.
15
+ */
16
+ type HarnessPreviewReporting = "extension" | "hooks";
17
+ /**
18
+ * What a harness declares about its preview reporting. The rules mirror
19
+ * {@link HarnessSubagentSupport}: `reporting` is checked against the union at
20
+ * load, and `setup` is the copy the preset editor shows when a human act is
21
+ * needed before reports start.
22
+ */
23
+ interface HarnessPreviews {
24
+ /** How reports reach the plane: a per-launch extension, or harness lifecycle hooks. */
25
+ reporting: HarnessPreviewReporting;
26
+ /** One-time human action required for reporting to work, if any. */
27
+ setup?: string;
28
+ }
2
29
  /** How the plugin learns about children: harness lifecycle hooks, or a loaded extension. */
3
30
  type HarnessSubagentDiscovery = "hooks" | "extension";
4
31
  /** Whether the sidebar can move the parent's own view to a child, or the harness keeps that in-app. */
@@ -43,7 +70,7 @@ type PluginType = "agent-harness" | "terminal" | "network";
43
70
  * and a network plugin claiming `resume` are both refused at load, for the
44
71
  * same reason a claimed-but-unimplemented capability is.
45
72
  */
46
- type PluginCapability = "mcp" | "resume" | "attention" | "subagents" | "publish" | "supervise" | "guard" | "settings";
73
+ type PluginCapability = "mcp" | "resume" | "attention" | "subagents" | "previews" | "publish" | "supervise" | "guard" | "settings";
47
74
  /** A harness preset as defined by the user (decoded JSON blobs). */
48
75
  interface PresetDefinition {
49
76
  /** Human-friendly preset name */
@@ -380,6 +407,13 @@ interface PluginSecrets {
380
407
  interface SubshellPlugin {
381
408
  /** Native child-agent reporting and optional in-place navigation. */
382
409
  subagents?: HarnessSubagentSupport;
410
+ /**
411
+ * Native preview-port reporting: the harness tells the plane which loopback
412
+ * ports its tooling serves, so the picker lists servers discovery cannot
413
+ * attribute to the pane's process tree. Declare the `previews` capability
414
+ * with it.
415
+ */
416
+ previews?: HarnessPreviews;
383
417
  /** Builds the argv (no shell) used to launch a subshell. */
384
418
  buildCommand(input: BuildCommandInput): string[];
385
419
  /** Validates a preset definition before saving. */
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- //#region ../../plugin-api/dist/manifest-Cdy9tSLl.js
1
+ //#region ../../plugin-api/dist/manifest-DgWWfiH-.js
2
2
  /** Every {@link PluginType}, for anything that has to iterate or validate them. */
3
3
  const PLUGIN_TYPES = [
4
4
  "agent-harness",
@@ -287,6 +287,158 @@ function validateGenericPreset(preset) {
287
287
  };
288
288
  }
289
289
  //#endregion
290
+ //#region src/previews.ts
291
+ /**
292
+ * omp brokers tool shells through its own daemon, so servers those tools
293
+ * start live outside the pane's process tree and port discovery cannot see
294
+ * them. This extension watches tool results for loopback URLs and reports
295
+ * the ports to the control plane, where the preview picker lists them.
296
+ *
297
+ * It deliberately reports what the output says, not a scan: a tool result
298
+ * from something that executes (bash or a custom tool) is the harness's own
299
+ * evidence that it started something, so file-reading tools are skipped. The
300
+ * report is port-only (the node dials loopback), bounded, and coalesced. A
301
+ * port the harness has not mentioned for ten minutes stops being refreshed,
302
+ * and the server's TTL retires it. A pane without the identity environment
303
+ * has no endpoint and stays quiet.
304
+ */
305
+ const PREVIEW_BRIDGE = `
306
+ export default function (pi) {
307
+ const REPORT_DELAY_MS = 750;
308
+ const HEARTBEAT_MS = 60_000;
309
+ const FRESH_MS = 10 * 60_000;
310
+ const MAX_PORTS = 32;
311
+ const MAX_TITLE = 120;
312
+ const MAX_SCAN = 64 * 1024;
313
+ const SKIP_TOOLS = { read: true, edit: true, write: true, grep: true, glob: true };
314
+ const URL_RE = /(?:https?:\\/\\/)?(?:localhost|127\\.0\\.0\\.1|0\\.0\\.0\\.0|\\[::1\\]):(\\d{1,5})(?!\\d)/gi;
315
+
316
+ const ports = new Map();
317
+ let timer = null;
318
+ let heartbeat = null;
319
+ let reporting = false;
320
+ let queued = false;
321
+ let warned = false;
322
+ let disabled = false;
323
+ const prune = now => {
324
+ for (const [port, entry] of ports) {
325
+ if (now - entry.seenAt > FRESH_MS) ports.delete(port);
326
+ }
327
+ if (ports.size === 0 && heartbeat !== null) {
328
+ clearInterval(heartbeat);
329
+ heartbeat = null;
330
+ }
331
+ };
332
+ const remember = (port, title) => {
333
+ if (!Number.isInteger(port) || port < 1 || port > 65535) return;
334
+ const now = Date.now();
335
+ prune(now);
336
+ const previous = ports.get(port);
337
+ if (previous) {
338
+ previous.seenAt = now;
339
+ if (title && !previous.title) previous.title = title;
340
+ } else {
341
+ if (ports.size >= MAX_PORTS) return;
342
+ ports.set(port, { title, seenAt: now });
343
+ }
344
+ if (heartbeat === null) heartbeat = setInterval(beat, HEARTBEAT_MS);
345
+ schedule();
346
+ };
347
+
348
+ const scan = text => {
349
+ const bounded = text.slice(0, MAX_SCAN);
350
+ URL_RE.lastIndex = 0;
351
+ let match;
352
+ while ((match = URL_RE.exec(bounded))) {
353
+ const port = Number(match[1]);
354
+ const start = bounded.lastIndexOf('\\n', match.index) + 1;
355
+ const end = bounded.indexOf('\\n', match.index);
356
+ const line = bounded.slice(start, end === -1 ? bounded.length : end).trim();
357
+ remember(port, line ? line.slice(0, MAX_TITLE) : undefined);
358
+ }
359
+ };
360
+
361
+ const report = async () => {
362
+ if (disabled || ports.size === 0) return;
363
+ if (reporting) {
364
+ queued = true;
365
+ return;
366
+ }
367
+ const base = process.env.SUBSHELL_BASE_URL;
368
+ const token = process.env.SUBSHELL_API_KEY;
369
+ const id = process.env.SUBSHELL_ID;
370
+ if (!base || !token || !id) {
371
+ if (!warned) {
372
+ warned = true;
373
+ pi.logger?.debug?.('subshell preview report skipped: the pane identity environment is missing');
374
+ }
375
+ return;
376
+ }
377
+ prune(Date.now());
378
+ if (ports.size === 0) return;
379
+ reporting = true;
380
+ try {
381
+ const response = await fetch(base + '/api/subshells/' + encodeURIComponent(id) + '/views/report', {
382
+ method: 'POST',
383
+ headers: { authorization: 'Bearer ' + token, 'content-type': 'application/json' },
384
+ body: JSON.stringify({
385
+ ports: [...ports.entries()].map(([port, entry]) => ({
386
+ port,
387
+ ...(entry.title ? { title: entry.title } : {}),
388
+ })),
389
+ }),
390
+ signal: AbortSignal.timeout(2000),
391
+ });
392
+ if (response.status === 404 || response.status === 401) {
393
+ // The server either predates the route or no longer accepts this
394
+ // pane's key. Retrying every heartbeat cannot fix either, so stop
395
+ // until a restart hands the extension a new process.
396
+ disabled = true;
397
+ if (heartbeat !== null) {
398
+ clearInterval(heartbeat);
399
+ heartbeat = null;
400
+ }
401
+ }
402
+ if (!response.ok && !warned) {
403
+ warned = true;
404
+ pi.logger?.debug?.('subshell preview report failed', { status: response.status });
405
+ }
406
+ } catch {
407
+ // A failed report is not fatal: the next tool result reports again.
408
+ } finally {
409
+ reporting = false;
410
+ if (queued) {
411
+ queued = false;
412
+ void report();
413
+ }
414
+ }
415
+ };
416
+
417
+ const schedule = () => {
418
+ if (timer !== null) return;
419
+ timer = setTimeout(() => {
420
+ timer = null;
421
+ void report();
422
+ }, REPORT_DELAY_MS);
423
+ };
424
+
425
+ const beat = () => {
426
+ prune(Date.now());
427
+ if (ports.size > 0) void report();
428
+ };
429
+
430
+ pi.on('tool_result', event => {
431
+ if (!event || event.isError) return;
432
+ const name = typeof event.toolName === 'string' ? event.toolName : '';
433
+ if (SKIP_TOOLS[name] === true) return;
434
+ const content = Array.isArray(event.content) ? event.content : [];
435
+ for (const block of content) {
436
+ if (block && block.type === 'text' && typeof block.text === 'string') scan(block.text);
437
+ }
438
+ });
439
+ }
440
+ `;
441
+ //#endregion
290
442
  //#region src/subagents.ts
291
443
  /**
292
444
  * omp's session event bus already carries the roster the task tool is running:
@@ -486,7 +638,7 @@ export default function (pi) {
486
638
  const parsed = parseManifest({
487
639
  name: "@subshell-ai/plugin-oh-my-pi",
488
640
  description: "Oh My Pi harness plugin for Subshell",
489
- version: "0.2.3",
641
+ version: "0.3.0",
490
642
  license: "Apache-2.0",
491
643
  type: "module",
492
644
  main: "dist/index.js",
@@ -769,8 +921,10 @@ const createPlugin = () => ({
769
921
  discovery: "extension",
770
922
  navigation: "unavailable"
771
923
  },
924
+ previews: { reporting: "extension" },
772
925
  capabilities: () => [
773
926
  "subagents",
927
+ "previews",
774
928
  "mcp",
775
929
  "settings"
776
930
  ],
@@ -806,11 +960,13 @@ const createPlugin = () => ({
806
960
  omp: { extensions: [
807
961
  "bridge.mjs",
808
962
  "subagents.mjs",
963
+ "previews.mjs",
809
964
  "provider.mjs"
810
965
  ] }
811
966
  }),
812
967
  "bridge.mjs": "export default function () {}\n",
813
968
  "subagents.mjs": SUBAGENT_BRIDGE,
969
+ "previews.mjs": PREVIEW_BRIDGE,
814
970
  "provider.mjs": MODEL_CLI_PROVIDER_SHIM,
815
971
  "mcp.json": fileContent
816
972
  },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@subshell-ai/plugin-oh-my-pi",
3
3
  "description": "Oh My Pi harness plugin for Subshell",
4
- "version": "0.2.3",
4
+ "version": "0.3.0",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",