@subshell-ai/plugin-claude-code 3.1.1 → 3.1.3

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,8 +1,104 @@
1
- //#region ../../plugin-api/dist/types-CW2dMAMW.d.ts
1
+ //#region ../../plugin-api/dist/types-D2qRfuN_.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
+ }
29
+ //#endregion
30
+ //#region src/subagents.d.ts
31
+ /** What a reported agent is doing, as the harness itself says. */
32
+ type HarnessSubagentStatus = "running" | "waiting" | "idle";
33
+ /** A harness agent belongs to an existing terminal, never to a new subshell. */
34
+ interface HarnessSubagent {
35
+ /** Harness-side identifier; unique among this parent's current children */
36
+ id: string;
37
+ /** Parent agent within the harness, when the harness nests */
38
+ parentId?: string;
39
+ /** Display name as the harness reports it; generic ones render numbered */
40
+ name: string;
41
+ /** What the harness says this agent is doing */
42
+ status: HarnessSubagentStatus;
43
+ /**
44
+ * The harness's own one-line label for what this child was asked to do,
45
+ * when the harness has one to give. The host bounds it and normalizes it
46
+ * like a name; it is the harness's summary of the task, never the prompt
47
+ * text or anything read from the child's transcript.
48
+ */
49
+ activity?: string;
50
+ }
51
+ /**
52
+ * A reported child as the control plane SERVES it: what the harness said, plus
53
+ * the server's own observation of when the child appeared. Viewers derive
54
+ * elapsed time from that stamp against their own clock, so a roster that
55
+ * stopped being polled ages honestly instead of freezing at its last read.
56
+ */
57
+ interface HarnessSubagentView extends HarnessSubagent {
58
+ /** Epoch milliseconds of when the server first saw this child; never sent by a harness */
59
+ startedAt: number;
60
+ }
2
61
  /** How the plugin learns about children: harness lifecycle hooks, or a loaded extension. */
3
62
  type HarnessSubagentDiscovery = "hooks" | "extension";
4
63
  /** Whether the sidebar can move the parent's own view to a child, or the harness keeps that in-app. */
5
64
  type HarnessSubagentNavigation = "native" | "unavailable";
65
+ /**
66
+ * Pane operations the host hands an integration that drives its harness's
67
+ * own agent picker. The plugin runs in the control plane, so it cannot open a
68
+ * terminal itself; these are the only two acts it needs, and neither submits
69
+ * a prompt on its own.
70
+ */
71
+ interface HarnessSubagentPane {
72
+ /** The parent pane's current screen with ANSI stripped, exactly as painted. */
73
+ read(): Promise<string>;
74
+ /**
75
+ * Type raw bytes into the pane's own input. Nothing is appended or
76
+ * translated: send `\r` for Enter, `\u001b[1;3D` for Alt+Left, and so on.
77
+ */
78
+ type(text: string): Promise<void>;
79
+ }
80
+ /** What the host hands an integration's {@link HarnessSubagentSupport.select}. */
81
+ interface HarnessSubagentSelectInput {
82
+ /** The child to show, or null to return the parent itself */
83
+ agentId: string | null;
84
+ /** The roster the child came from, server-stamped and bounded */
85
+ agents: readonly HarnessSubagentView[];
86
+ /** The parent terminal this selection must happen in */
87
+ pane: HarnessSubagentPane;
88
+ }
89
+ /** What the integration must report back after the harness actually switched. */
90
+ interface HarnessSubagentSelectResult {
91
+ /** The view the harness showed when the act finished; null for the parent */
92
+ selectedId: string | null;
93
+ }
94
+ /**
95
+ * An integration's own way to move its terminal's view. This is for harnesses
96
+ * whose picker or inspector exists only inside their TUI (Codex's `/subagents`,
97
+ * Claude's agent panel, pi's fleet inspector): the integration reads the pane,
98
+ * sends the harness's own keys, and reports the view it verified. Extensions
99
+ * without a pane driver keep using the report-loop command.
100
+ */
101
+ type HarnessSubagentSelect = (input: HarnessSubagentSelectInput) => Promise<HarnessSubagentSelectResult>;
6
102
  /**
7
103
  * What a plugin declares about its harness's child agents. The host uses
8
104
  * `discovery`/`navigation` to decide what the sidebar may offer; `setup`, when
@@ -16,6 +112,13 @@ interface HarnessSubagentSupport {
16
112
  navigation: HarnessSubagentNavigation;
17
113
  /** One-time user action required for reporting to work, if any */
18
114
  setup?: string;
115
+ /**
116
+ * The integration's own pane driver, when it has one. Requires
117
+ * `navigation: "native"`; the host uses it instead of queueing a command.
118
+ * Works for hook and extension discovery alike: the host only needs the
119
+ * pane's read/type, and the edit gate is the same.
120
+ */
121
+ select?: HarnessSubagentSelect;
19
122
  }
20
123
  //#endregion
21
124
  //#region src/types.d.ts
@@ -43,7 +146,7 @@ type PluginType = "agent-harness" | "terminal" | "network";
43
146
  * and a network plugin claiming `resume` are both refused at load, for the
44
147
  * same reason a claimed-but-unimplemented capability is.
45
148
  */
46
- type PluginCapability = "mcp" | "resume" | "attention" | "subagents" | "publish" | "supervise" | "guard" | "settings";
149
+ type PluginCapability = "mcp" | "resume" | "attention" | "subagents" | "previews" | "publish" | "supervise" | "guard" | "settings";
47
150
  /** A harness preset as defined by the user (decoded JSON blobs). */
48
151
  interface PresetDefinition {
49
152
  /** Human-friendly preset name */
@@ -380,6 +483,13 @@ interface PluginSecrets {
380
483
  interface SubshellPlugin {
381
484
  /** Native child-agent reporting and optional in-place navigation. */
382
485
  subagents?: HarnessSubagentSupport;
486
+ /**
487
+ * Native preview-port reporting: the harness tells the plane which loopback
488
+ * ports its tooling serves, so the picker lists servers discovery cannot
489
+ * attribute to the pane's process tree. Declare the `previews` capability
490
+ * with it.
491
+ */
492
+ previews?: HarnessPreviews;
383
493
  /** Builds the argv (no shell) used to launch a subshell. */
384
494
  buildCommand(input: BuildCommandInput): string[];
385
495
  /** Validates a preset definition before saving. */
@@ -397,6 +507,19 @@ interface SubshellPlugin {
397
507
  * used as-is, which is right for every harness that prints just a version.
398
508
  */
399
509
  parseVersion?(raw: string): string | null;
510
+ /**
511
+ * Turns the harness's own terminal title into the subshell name the rail
512
+ * shows.
513
+ *
514
+ * The host removes escape sequences and control bytes and trims the result
515
+ * before calling this, so `raw` is safe text. Interpret what the title adds
516
+ * around the session name — a brand, a run-state separator, a cycling
517
+ * status glyph — and return the name behind it. Return "" when the title
518
+ * names nothing, and the caller keeps the name the pane already had. Omit
519
+ * the member when the harness titles its pane with the name the host
520
+ * supplied, which needs no interpretation.
521
+ */
522
+ parsePaneTitle?(raw: string): string;
400
523
  /** Overrides manifest-driven detection. Almost no plugin needs this. */
401
524
  detect?(): Promise<DetectionResult>;
402
525
  /** Restart-resume support. Declare the `resume` capability with it. */
@@ -487,6 +610,24 @@ interface InstallSpec {
487
610
  */
488
611
  docsUrl: string;
489
612
  }
613
+ /**
614
+ * How this plugin's harness CLI updates itself in place.
615
+ *
616
+ * The installer doubles as the updater for every built-in today (`curl … | sh`
617
+ * and `npm i -g` lines are idempotent), so this is OPTIONAL and falls back to
618
+ * {@link InstallSpec}. Declare it where the vendor ships a dedicated verb or a
619
+ * different channel — `claude update`, say — so pressing Update runs the
620
+ * vendor's own update path rather than reinstalling.
621
+ *
622
+ * Like `install.command`, this is a SHELL LINE the host runs through `sh -c`,
623
+ * on the node that holds the harness, and it may not need privileges.
624
+ */
625
+ interface UpdateSpec {
626
+ /** Shell line that updates the CLI in place. */
627
+ command: string;
628
+ /** Where the vendor documents it; http(s) only, when present. */
629
+ docsUrl?: string;
630
+ }
490
631
  /** One step an operator must run themselves, because the host may not. */
491
632
  interface PrivilegedStep {
492
633
  /** What it does, in a few words: "Install the daemon" */
@@ -630,6 +771,11 @@ interface SubshellManifest {
630
771
  detect?: DetectSpec;
631
772
  /** Install guidance shown when the binary is missing */
632
773
  install?: InstallSpec;
774
+ /**
775
+ * How the CLI updates itself, when the installer is not the updater.
776
+ * Absent means `install.command` is the update path too.
777
+ */
778
+ update?: UpdateSpec;
633
779
  /**
634
780
  * Environment variables this plugin computes against (spec 2026-09-10 §5).
635
781
  *
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { join, resolve } from "node:path";
2
- //#region ../../plugin-api/dist/manifest-XGvw0ugx.js
2
+ //#region ../../plugin-api/dist/manifest-CrULrf80.js
3
3
  /** Every {@link PluginType}, for anything that has to iterate or validate them. */
4
4
  const PLUGIN_TYPES = [
5
5
  "agent-harness",
@@ -159,6 +159,19 @@ function parseManifest(pkgJson) {
159
159
  docsUrl: i.docsUrl
160
160
  };
161
161
  }
162
+ let update;
163
+ if (block.update !== void 0) {
164
+ const u = block.update;
165
+ if (!isRecord(u) || typeof u.command !== "string" || u.command.trim() === "") return { error: "`subshell.update` needs a non-empty command" };
166
+ if (needsPrivileges(u.command)) return { error: "`subshell.update.command` must not need sudo, doas or pkexec — the host runs it through a shell and has no terminal for a password prompt" };
167
+ if (u.docsUrl !== void 0) {
168
+ if (typeof u.docsUrl !== "string" || !isDocsUrl(u.docsUrl)) return { error: "`subshell.update.docsUrl` must be an http(s) URL — it is rendered as a link on an admin page" };
169
+ update = {
170
+ command: u.command,
171
+ docsUrl: u.docsUrl
172
+ };
173
+ } else update = { command: u.command };
174
+ }
162
175
  let hostEnv;
163
176
  if (block.hostEnv !== void 0) {
164
177
  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" };
@@ -177,6 +190,7 @@ function parseManifest(pkgJson) {
177
190
  entry: block.entry,
178
191
  ...detect ? { detect } : {},
179
192
  ...install ? { install } : {},
193
+ ...update ? { update } : {},
180
194
  ...hostEnv ? { hostEnv } : {},
181
195
  ...parsedNetwork ? { network: parsedNetwork } : {},
182
196
  ...block.pickable === false ? { pickable: false } : {}
@@ -286,7 +300,7 @@ function validateGenericPreset(preset) {
286
300
  const parsed = parseManifest({
287
301
  name: "@subshell-ai/plugin-claude-code",
288
302
  description: "Claude Code harness plugin for Subshell",
289
- version: "3.1.1",
303
+ version: "3.1.3",
290
304
  license: "Apache-2.0",
291
305
  type: "module",
292
306
  main: "dist/index.js",
@@ -316,7 +330,7 @@ const parsed = parseManifest({
316
330
  "@internal/tsconfig": "workspace:*",
317
331
  "@subshell-ai/plugin-api": "workspace:*",
318
332
  "@types/bun": "1.3.14",
319
- "hash-runner": "4.0.0",
333
+ "hash-runner": "5.0.0",
320
334
  "tsdown": "0.22.14",
321
335
  "typescript": "7.0.2"
322
336
  },
@@ -556,6 +570,12 @@ const reporterHook = (host, reporter, ...verb) => [
556
570
  * answered (spec 2026-09-23). `Stop` reports `turn_complete` blind here:
557
571
  * the reporter reads the hook payload and stays silent for a session
558
572
  * parked on background work (`packages/mcp-core/src/report.ts`).
573
+ * `Stop` carries a second command, `report subagents`: the turn-end payload
574
+ * enumerates the harness's live background agents, and the server replaces
575
+ * its roster with that set. Edge events cannot reconcile it alone — a
576
+ * background child killed with `TaskStop` fires no lifecycle hook at all,
577
+ * so its row would pin the rail "running" forever (measured on Claude Code
578
+ * 2.1.296; the rail then only self-heals at the next turn boundary).
559
579
  * - `UserPromptSubmit` / `PreToolUse` (the matcher-less entry) — the waiting
560
580
  * CLEAR (`resumed`). The plane's idle-watcher can only clear what it can
561
581
  * stat, so an agent-node pane — log on the node's disk — stayed "waiting
@@ -592,6 +612,9 @@ const paneHooks = (host, reporter) => ({
592
612
  Stop: [{ hooks: [{
593
613
  type: "command",
594
614
  command: reporterHook(host, reporter, "attention", "turn_complete")
615
+ }, {
616
+ type: "command",
617
+ command: reporterHook(host, reporter, "subagents")
595
618
  }] }],
596
619
  Notification: [{
597
620
  matcher: "permission_prompt|agent_needs_input|elicitation_dialog|elicitation_url_dialog",
@@ -634,6 +657,78 @@ function projectSlug(cwd) {
634
657
  return cwd.replace(/[^a-zA-Z0-9]/g, "-");
635
658
  }
636
659
  /**
660
+ * Claude Code puts a cycling status glyph at the front of its pane title
661
+ * (`✳ Ship the rename feature`, then `✻ …` on the next sweep). The glyph is
662
+ * Claude's run state, not part of the session name, and leaving it in made the
663
+ * mirrored name churn between sweeps. Strip the leading decoration — glyphs,
664
+ * punctuation, and the space after them — so only the task text remains. A
665
+ * title that is nothing but the glyph parses to nothing, and the caller keeps
666
+ * the name it already had.
667
+ */
668
+ const STATUS_GLYPH = /^[^\p{L}\p{N}]+/u;
669
+ const DOWN = "\x1B[B";
670
+ const UP = "\x1B[A";
671
+ const ENTER = "\r";
672
+ const ESC = "\x1B";
673
+ const CLAUDE_SELECT_MARK = "[>❯]";
674
+ const CLAUDE_STATUS_GLYPH = "[●◉◯○◦•]";
675
+ const CLAUDE_AGENT_ROW = new RegExp(`^\\s*(${CLAUDE_SELECT_MARK})?\\s*(${CLAUDE_STATUS_GLYPH})?\\s*([^\\s·]+)(?:\\s+·\\s+|\\s{2,})`);
676
+ const CLAUDE_MAIN_ROW = new RegExp(`^\\s*(${CLAUDE_SELECT_MARK})?\\s*(${CLAUDE_STATUS_GLYPH})?\\s*main(?:\\s{2,}.*)?\\s*$`);
677
+ function claudePanelRows(screen) {
678
+ const lines = screen.split("\n");
679
+ const prompt = lines.findLastIndex((line) => /^\s*❯\s+(?![●◉◯○◦•])/.test(line));
680
+ return (prompt < 0 ? lines : lines.slice(prompt + 1)).map((line) => {
681
+ const main = line.match(CLAUDE_MAIN_ROW);
682
+ if (main) return {
683
+ name: "main",
684
+ selected: main[1] === ">" || main[1] === "❯"
685
+ };
686
+ const agent = line.match(CLAUDE_AGENT_ROW);
687
+ return agent ? {
688
+ name: (agent[3] ?? "").trim(),
689
+ selected: agent[1] === ">" || agent[1] === "❯"
690
+ } : void 0;
691
+ }).filter((row) => row !== void 0 && (row.name === "main" || /^[\p{L}\p{N}_.-]+$/u.test(row.name)));
692
+ }
693
+ async function selectClaudeSubagent(input) {
694
+ const { agentId, agents, pane } = input;
695
+ let screen = await pane.read();
696
+ let rows = claudePanelRows(screen);
697
+ if (agentId === null) {
698
+ if (rows.some((row) => row.selected && row.name === "main")) return { selectedId: null };
699
+ if (!rows.length) {
700
+ if (!(/main\s*>\s*\S/.test(screen) || agents.some((agent) => screen.includes(agent.name)))) throw new Error("Claude isn't showing an agent view to return from.");
701
+ await pane.type(ESC);
702
+ screen = await pane.read();
703
+ rows = claudePanelRows(screen);
704
+ }
705
+ if (!rows.length) throw new Error("Claude isn't showing its agent panel.");
706
+ if (!rows.some((row) => row.selected)) return { selectedId: null };
707
+ for (let step = 0; step < rows.length + 2; step++) {
708
+ if (claudePanelRows(await pane.read()).findIndex((row) => row.selected) === 0) break;
709
+ await pane.type(UP);
710
+ }
711
+ if (!claudePanelRows(await pane.read()).some((row) => row.selected && row.name === "main")) throw new Error("Claude didn't select the main agent in its agent panel.");
712
+ await pane.type(ENTER);
713
+ return { selectedId: null };
714
+ }
715
+ const target = agents.find((agent) => agent.id === agentId);
716
+ if (!target) throw new Error("Claude didn't report that subagent.");
717
+ if (!rows.length) throw new Error("Claude isn't showing its agent panel.");
718
+ const targetName = target.name.trim();
719
+ const ordinal = agents.filter((agent) => agent.name.trim() === targetName).findIndex((agent) => agent.id === agentId);
720
+ const targetRow = rows.flatMap((row, index) => row.name === targetName ? [index] : [])[ordinal] ?? -1;
721
+ if (targetRow < 0) throw new Error("Claude didn't list that subagent in its agent panel.");
722
+ for (let step = 0; step < rows.length + 2; step++) {
723
+ const current = claudePanelRows(await pane.read()).findIndex((row) => row.selected);
724
+ if (current === targetRow) break;
725
+ await pane.type(current < 0 || current < targetRow ? DOWN : UP);
726
+ }
727
+ if (!claudePanelRows(await pane.read())[targetRow]?.selected) throw new Error("Claude didn't select that subagent in its agent panel.");
728
+ await pane.type(ENTER);
729
+ return { selectedId: agentId };
730
+ }
731
+ /**
637
732
  * Built-in harness: Claude Code.
638
733
  *
639
734
  * Launch shape:
@@ -653,7 +748,8 @@ function projectSlug(cwd) {
653
748
  const createPlugin = (host) => ({
654
749
  subagents: {
655
750
  discovery: "hooks",
656
- navigation: "unavailable"
751
+ navigation: "native",
752
+ select: selectClaudeSubagent
657
753
  },
658
754
  capabilities: () => [
659
755
  "subagents",
@@ -662,6 +758,8 @@ const createPlugin = (host) => ({
662
758
  "attention",
663
759
  "settings"
664
760
  ],
761
+ /** Claude Code prefixes its pane title with a cycling status glyph. */
762
+ parsePaneTitle: (raw) => raw.replace(STATUS_GLYPH, "").trim(),
665
763
  supportsAttentionHooks: true,
666
764
  /**
667
765
  * Restart-resume: conversations are PINNED at start (`--session-id <uuid>`)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@subshell-ai/plugin-claude-code",
3
3
  "description": "Claude Code harness plugin for Subshell",
4
- "version": "3.1.1",
4
+ "version": "3.1.3",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
@@ -35,7 +35,7 @@
35
35
  "@internal/tsconfig": "workspace:*",
36
36
  "@subshell-ai/plugin-api": "workspace:*",
37
37
  "@types/bun": "1.3.14",
38
- "hash-runner": "4.0.0",
38
+ "hash-runner": "5.0.0",
39
39
  "tsdown": "0.22.14",
40
40
  "typescript": "7.0.2"
41
41
  },