@subshell-ai/plugin-claude-code 3.1.0 → 3.1.2
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 +69 -19
- package/dist/index.js +76 -19
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
|
-
//#region ../../plugin-api/dist/types-
|
|
2
|
-
|
|
1
|
+
//#region ../../plugin-api/dist/types-DPriUYLa.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";
|
|
3
6
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* from a plugin file fails to resolve, because there is no `node_modules`
|
|
9
|
-
* beside it. Everything a plugin needs therefore arrives through
|
|
10
|
-
* {@link PluginHost}, handed to the factory it default-exports.
|
|
11
|
-
*
|
|
12
|
-
* This package is types plus PURE helpers only. A plugin bundles it in at
|
|
13
|
-
* build time, so anything with runtime behaviour would freeze at the version
|
|
14
|
-
* the plugin was built against; a filesystem probe that did that would keep
|
|
15
|
-
* searching last year's install locations forever. Those belong on the host.
|
|
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.
|
|
16
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
|
|
17
22
|
/**
|
|
18
23
|
* What kind of thing a plugin provides.
|
|
19
24
|
*
|
|
@@ -38,7 +43,7 @@ type PluginType = "agent-harness" | "terminal" | "network";
|
|
|
38
43
|
* and a network plugin claiming `resume` are both refused at load, for the
|
|
39
44
|
* same reason a claimed-but-unimplemented capability is.
|
|
40
45
|
*/
|
|
41
|
-
type PluginCapability = "mcp" | "resume" | "attention" | "publish" | "supervise" | "guard" | "settings";
|
|
46
|
+
type PluginCapability = "mcp" | "resume" | "attention" | "subagents" | "publish" | "supervise" | "guard" | "settings";
|
|
42
47
|
/** A harness preset as defined by the user (decoded JSON blobs). */
|
|
43
48
|
interface PresetDefinition {
|
|
44
49
|
/** Human-friendly preset name */
|
|
@@ -175,14 +180,25 @@ interface McpLaunchSpec {
|
|
|
175
180
|
}
|
|
176
181
|
/** Per-subshell MCP registration rendered in the plugin's own config dialect. */
|
|
177
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>;
|
|
178
192
|
/** File content the harness reads, written to the subshell's config path */
|
|
179
193
|
fileContent: string;
|
|
180
194
|
/** argv the harness needs to load the file (e.g. claude's ["--mcp-config", path]) */
|
|
181
195
|
args?: string[];
|
|
182
196
|
/**
|
|
183
197
|
* Extra pane env the harness needs to discover the file (e.g. OPENCODE_CONFIG).
|
|
184
|
-
* Consumed by the HOST, which
|
|
185
|
-
*
|
|
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.
|
|
186
202
|
*/
|
|
187
203
|
env?: Record<string, string>;
|
|
188
204
|
}
|
|
@@ -362,6 +378,8 @@ interface PluginSecrets {
|
|
|
362
378
|
* plugin is a binary and an argv rather than eight stubs.
|
|
363
379
|
*/
|
|
364
380
|
interface SubshellPlugin {
|
|
381
|
+
/** Native child-agent reporting and optional in-place navigation. */
|
|
382
|
+
subagents?: HarnessSubagentSupport;
|
|
365
383
|
/** Builds the argv (no shell) used to launch a subshell. */
|
|
366
384
|
buildCommand(input: BuildCommandInput): string[];
|
|
367
385
|
/** Validates a preset definition before saving. */
|
|
@@ -379,6 +397,19 @@ interface SubshellPlugin {
|
|
|
379
397
|
* used as-is, which is right for every harness that prints just a version.
|
|
380
398
|
*/
|
|
381
399
|
parseVersion?(raw: string): string | null;
|
|
400
|
+
/**
|
|
401
|
+
* Turns the harness's own terminal title into the subshell name the rail
|
|
402
|
+
* shows.
|
|
403
|
+
*
|
|
404
|
+
* The host removes escape sequences and control bytes and trims the result
|
|
405
|
+
* before calling this, so `raw` is safe text. Interpret what the title adds
|
|
406
|
+
* around the session name — a brand, a run-state separator, a cycling
|
|
407
|
+
* status glyph — and return the name behind it. Return "" when the title
|
|
408
|
+
* names nothing, and the caller keeps the name the pane already had. Omit
|
|
409
|
+
* the member when the harness titles its pane with the name the host
|
|
410
|
+
* supplied, which needs no interpretation.
|
|
411
|
+
*/
|
|
412
|
+
parsePaneTitle?(raw: string): string;
|
|
382
413
|
/** Overrides manifest-driven detection. Almost no plugin needs this. */
|
|
383
414
|
detect?(): Promise<DetectionResult>;
|
|
384
415
|
/** Restart-resume support. Declare the `resume` capability with it. */
|
|
@@ -395,8 +426,15 @@ interface SubshellPlugin {
|
|
|
395
426
|
* then surfaces one-time manual setup via {@link mcpSetup} instead.
|
|
396
427
|
*/
|
|
397
428
|
mcpRegistration?(launch: McpLaunchSpec, configPath: string): McpRegistration;
|
|
398
|
-
/**
|
|
399
|
-
|
|
429
|
+
/**
|
|
430
|
+
* How users obtain the subshell MCP tools here. Declare the `mcp`
|
|
431
|
+
* capability with it. `reporter` is the host-resolved command prefix that
|
|
432
|
+
* re-enters the subshell binary on a pane's machine (same shape as
|
|
433
|
+
* {@link BuildCommandInput.reporter}), present only when the host could
|
|
434
|
+
* resolve one: use it to render hook-configuration steps, and omit those
|
|
435
|
+
* steps when it is absent rather than guessing a binary name.
|
|
436
|
+
*/
|
|
437
|
+
mcpSetup?(launch: McpLaunchSpec, reporter?: ReporterSpec): McpSetupInfo;
|
|
400
438
|
/** Settings rendered in the PRESET editor and stored on the preset. */
|
|
401
439
|
presetSettings?(): SettingsField[];
|
|
402
440
|
/** Known extra env var suggestions for the preset editor. */
|
|
@@ -617,6 +655,18 @@ interface SubshellManifest {
|
|
|
617
655
|
hostEnv?: string[];
|
|
618
656
|
/** Network facts. Present exactly when `type` is `network`. */
|
|
619
657
|
network?: NetworkManifest;
|
|
658
|
+
/**
|
|
659
|
+
* False means the launch pickers must not offer this plugin, even though
|
|
660
|
+
* it drives a pane. Absent means true.
|
|
661
|
+
*
|
|
662
|
+
* The declaration belongs to the plugin because only it knows whether a
|
|
663
|
+
* bare launch means something: a harness whose useful launch needs inputs
|
|
664
|
+
* no picker carries (SSH needs a destination, which the connect form
|
|
665
|
+
* supplies through its own door) would otherwise be a row that starts a
|
|
666
|
+
* dying pane. This hides the entry, not the route: the plugin stays
|
|
667
|
+
* launchable by id for whatever flow does supply the inputs.
|
|
668
|
+
*/
|
|
669
|
+
pickable?: boolean;
|
|
620
670
|
}
|
|
621
671
|
//#endregion
|
|
622
672
|
//#region src/manifest.d.ts
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { join, resolve } from "node:path";
|
|
2
|
-
//#region ../../plugin-api/dist/manifest-
|
|
2
|
+
//#region ../../plugin-api/dist/manifest-XGvw0ugx.js
|
|
3
3
|
/** Every {@link PluginType}, for anything that has to iterate or validate them. */
|
|
4
4
|
const PLUGIN_TYPES = [
|
|
5
5
|
"agent-harness",
|
|
@@ -135,6 +135,7 @@ function parseManifest(pkgJson) {
|
|
|
135
135
|
const icon = block.icon;
|
|
136
136
|
if (!ICON_EXTENSIONS.some((ext) => icon.endsWith(ext))) return { error: `\`subshell.icon\` must end in one of: ${ICON_EXTENSIONS.join(", ")}` };
|
|
137
137
|
}
|
|
138
|
+
if (block.pickable !== void 0 && typeof block.pickable !== "boolean") return { error: "`subshell.pickable` must be a boolean when present" };
|
|
138
139
|
if (typeof block.entry !== "string" || block.entry.trim() === "") return { error: "`subshell.entry` must be a non-empty relative path" };
|
|
139
140
|
if (block.entry.startsWith("/") || block.entry.split("/").includes("..")) return { error: "`subshell.entry` must stay inside the package (no leading `/` and no `..` segment)" };
|
|
140
141
|
let detect;
|
|
@@ -177,7 +178,8 @@ function parseManifest(pkgJson) {
|
|
|
177
178
|
...detect ? { detect } : {},
|
|
178
179
|
...install ? { install } : {},
|
|
179
180
|
...hostEnv ? { hostEnv } : {},
|
|
180
|
-
...parsedNetwork ? { network: parsedNetwork } : {}
|
|
181
|
+
...parsedNetwork ? { network: parsedNetwork } : {},
|
|
182
|
+
...block.pickable === false ? { pickable: false } : {}
|
|
181
183
|
};
|
|
182
184
|
}
|
|
183
185
|
/**
|
|
@@ -284,7 +286,7 @@ function validateGenericPreset(preset) {
|
|
|
284
286
|
const parsed = parseManifest({
|
|
285
287
|
name: "@subshell-ai/plugin-claude-code",
|
|
286
288
|
description: "Claude Code harness plugin for Subshell",
|
|
287
|
-
version: "3.1.
|
|
289
|
+
version: "3.1.2",
|
|
288
290
|
license: "Apache-2.0",
|
|
289
291
|
type: "module",
|
|
290
292
|
main: "dist/index.js",
|
|
@@ -307,14 +309,14 @@ const parsed = parseManifest({
|
|
|
307
309
|
"lint": "biome check --write --unsafe src && biome format src --write && biome lint src --fix",
|
|
308
310
|
"lint:check": "biome check --no-errors-on-unmatched src",
|
|
309
311
|
"lint:staged": "biome check --no-errors-on-unmatched --write --unsafe --staged src",
|
|
310
|
-
"test": "bun test",
|
|
312
|
+
"test": "bash \"$(git rev-parse --show-toplevel)/scripts/par-test.sh\" 4 bun test",
|
|
311
313
|
"verify-types": "tsc --project tsconfig.json --noEmit"
|
|
312
314
|
},
|
|
313
315
|
devDependencies: {
|
|
314
316
|
"@internal/tsconfig": "workspace:*",
|
|
315
317
|
"@subshell-ai/plugin-api": "workspace:*",
|
|
316
318
|
"@types/bun": "1.3.14",
|
|
317
|
-
"hash-runner": "
|
|
319
|
+
"hash-runner": "5.0.0",
|
|
318
320
|
"tsdown": "0.22.14",
|
|
319
321
|
"typescript": "7.0.2"
|
|
320
322
|
},
|
|
@@ -554,16 +556,27 @@ const reporterHook = (host, reporter, ...verb) => [
|
|
|
554
556
|
* answered (spec 2026-09-23). `Stop` reports `turn_complete` blind here:
|
|
555
557
|
* the reporter reads the hook payload and stays silent for a session
|
|
556
558
|
* parked on background work (`packages/mcp-core/src/report.ts`).
|
|
557
|
-
*
|
|
558
|
-
*
|
|
559
|
-
*
|
|
560
|
-
*
|
|
561
|
-
*
|
|
562
|
-
*
|
|
563
|
-
*
|
|
564
|
-
*
|
|
565
|
-
*
|
|
566
|
-
*
|
|
559
|
+
* `Stop` carries a second command, `report subagents`: the turn-end payload
|
|
560
|
+
* enumerates the harness's live background agents, and the server replaces
|
|
561
|
+
* its roster with that set. Edge events cannot reconcile it alone — a
|
|
562
|
+
* background child killed with `TaskStop` fires no lifecycle hook at all,
|
|
563
|
+
* so its row would pin the rail "running" forever (measured on Claude Code
|
|
564
|
+
* 2.1.296; the rail then only self-heals at the next turn boundary).
|
|
565
|
+
* - `UserPromptSubmit` / `PreToolUse` (the matcher-less entry) — the waiting
|
|
566
|
+
* CLEAR (`resumed`). The plane's idle-watcher can only clear what it can
|
|
567
|
+
* stat, so an agent-node pane — log on the node's disk — stayed "waiting
|
|
568
|
+
* for you" for its whole next turn (2026-09-24). The pane knows better:
|
|
569
|
+
* work has resumed when the human's prompt is submitted, AND when a tool
|
|
570
|
+
* starts after an approval answered in the dialog (approving is not a
|
|
571
|
+
* prompt, so UserPromptSubmit alone misses that path). PreToolUse fires on
|
|
572
|
+
* every tool call; the endpoint's clear is conditional, so the common
|
|
573
|
+
* stamp-less case is a silent no-op. Neither of these two reads its stdin:
|
|
574
|
+
* their payloads are the prompt text and the tool input, and the report is
|
|
575
|
+
* only ever the fact.
|
|
576
|
+
* - `PreToolUse` with `Agent|Task` — subagent spawn labels. Claude's
|
|
577
|
+
* SubagentStart payload has no description field, but the spawn's tool
|
|
578
|
+
* input does, so this second entry runs the same `report subagents` verb,
|
|
579
|
+
* which forwards names and descriptions, never prompts or commands.
|
|
567
580
|
* - `SessionStart` — conversation identity. The restart-resume pin
|
|
568
581
|
* (`--session-id` at launch) only survives while the pane keeps that ONE
|
|
569
582
|
* conversation, but /clear, /resume <other> and /fork start a DIFFERENT
|
|
@@ -573,10 +586,21 @@ const reporterHook = (host, reporter, ...verb) => [
|
|
|
573
586
|
* and reads stdin like `Stop` now does — the reporter forwards `session_id`
|
|
574
587
|
* from that payload and nothing else.
|
|
575
588
|
*/
|
|
576
|
-
const
|
|
589
|
+
const paneHooks = (host, reporter) => ({
|
|
590
|
+
SubagentStart: [{ hooks: [{
|
|
591
|
+
type: "command",
|
|
592
|
+
command: reporterHook(host, reporter, "subagents")
|
|
593
|
+
}] }],
|
|
594
|
+
SubagentStop: [{ hooks: [{
|
|
595
|
+
type: "command",
|
|
596
|
+
command: reporterHook(host, reporter, "subagents")
|
|
597
|
+
}] }],
|
|
577
598
|
Stop: [{ hooks: [{
|
|
578
599
|
type: "command",
|
|
579
600
|
command: reporterHook(host, reporter, "attention", "turn_complete")
|
|
601
|
+
}, {
|
|
602
|
+
type: "command",
|
|
603
|
+
command: reporterHook(host, reporter, "subagents")
|
|
580
604
|
}] }],
|
|
581
605
|
Notification: [{
|
|
582
606
|
matcher: "permission_prompt|agent_needs_input|elicitation_dialog|elicitation_url_dialog",
|
|
@@ -592,10 +616,26 @@ const attentionHooks = (host, reporter) => ({
|
|
|
592
616
|
PreToolUse: [{ hooks: [{
|
|
593
617
|
type: "command",
|
|
594
618
|
command: reporterHook(host, reporter, "attention", "resumed")
|
|
595
|
-
}] }
|
|
619
|
+
}] }, {
|
|
620
|
+
matcher: "Agent|Task",
|
|
621
|
+
hooks: [{
|
|
622
|
+
type: "command",
|
|
623
|
+
command: reporterHook(host, reporter, "subagents")
|
|
624
|
+
}]
|
|
625
|
+
}],
|
|
626
|
+
PostToolUse: [{
|
|
627
|
+
matcher: "Agent|Task",
|
|
628
|
+
hooks: [{
|
|
629
|
+
type: "command",
|
|
630
|
+
command: reporterHook(host, reporter, "subagents")
|
|
631
|
+
}]
|
|
632
|
+
}],
|
|
596
633
|
SessionStart: [{ hooks: [{
|
|
597
634
|
type: "command",
|
|
598
635
|
command: reporterHook(host, reporter, "session")
|
|
636
|
+
}, {
|
|
637
|
+
type: "command",
|
|
638
|
+
command: reporterHook(host, reporter, "subagents")
|
|
599
639
|
}] }]
|
|
600
640
|
});
|
|
601
641
|
/** Claude's transcript folder name for a project dir: every non-alphanumeric → `-`. */
|
|
@@ -603,6 +643,16 @@ function projectSlug(cwd) {
|
|
|
603
643
|
return cwd.replace(/[^a-zA-Z0-9]/g, "-");
|
|
604
644
|
}
|
|
605
645
|
/**
|
|
646
|
+
* Claude Code puts a cycling status glyph at the front of its pane title
|
|
647
|
+
* (`✳ Ship the rename feature`, then `✻ …` on the next sweep). The glyph is
|
|
648
|
+
* Claude's run state, not part of the session name, and leaving it in made the
|
|
649
|
+
* mirrored name churn between sweeps. Strip the leading decoration — glyphs,
|
|
650
|
+
* punctuation, and the space after them — so only the task text remains. A
|
|
651
|
+
* title that is nothing but the glyph parses to nothing, and the caller keeps
|
|
652
|
+
* the name it already had.
|
|
653
|
+
*/
|
|
654
|
+
const STATUS_GLYPH = /^[^\p{L}\p{N}]+/u;
|
|
655
|
+
/**
|
|
606
656
|
* Built-in harness: Claude Code.
|
|
607
657
|
*
|
|
608
658
|
* Launch shape:
|
|
@@ -620,12 +670,19 @@ function projectSlug(cwd) {
|
|
|
620
670
|
* `@subshell-ai/plugin-api`.
|
|
621
671
|
*/
|
|
622
672
|
const createPlugin = (host) => ({
|
|
673
|
+
subagents: {
|
|
674
|
+
discovery: "hooks",
|
|
675
|
+
navigation: "unavailable"
|
|
676
|
+
},
|
|
623
677
|
capabilities: () => [
|
|
678
|
+
"subagents",
|
|
624
679
|
"mcp",
|
|
625
680
|
"resume",
|
|
626
681
|
"attention",
|
|
627
682
|
"settings"
|
|
628
683
|
],
|
|
684
|
+
/** Claude Code prefixes its pane title with a cycling status glyph. */
|
|
685
|
+
parsePaneTitle: (raw) => raw.replace(STATUS_GLYPH, "").trim(),
|
|
629
686
|
supportsAttentionHooks: true,
|
|
630
687
|
/**
|
|
631
688
|
* Restart-resume: conversations are PINNED at start (`--session-id <uuid>`)
|
|
@@ -657,8 +714,8 @@ const createPlugin = (host) => ({
|
|
|
657
714
|
if (mcp?.args) args.push(...mcp.args);
|
|
658
715
|
if (harnessSession) args.push(harnessSession.mode === "resume" ? "--resume" : "--session-id", harnessSession.id);
|
|
659
716
|
const settings = { ...preset.settings ?? {} };
|
|
660
|
-
if (reporter) settings.hooks =
|
|
661
|
-
else host.log.warn("no reporter resolved for this launch: attention
|
|
717
|
+
if (reporter) settings.hooks = paneHooks(host, reporter);
|
|
718
|
+
else host.log.warn("no reporter resolved for this launch: attention, conversation-identity, and subagent-roster hooks are omitted");
|
|
662
719
|
if (Object.keys(settings).length > 0) args.push("--settings", JSON.stringify(settings));
|
|
663
720
|
if (subshellName) args.push("--name", subshellName);
|
|
664
721
|
for (const flag of preset.flags) args.push(flag);
|
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.
|
|
4
|
+
"version": "3.1.2",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|
|
@@ -28,14 +28,14 @@
|
|
|
28
28
|
"lint": "biome check --write --unsafe src && biome format src --write && biome lint src --fix",
|
|
29
29
|
"lint:check": "biome check --no-errors-on-unmatched src",
|
|
30
30
|
"lint:staged": "biome check --no-errors-on-unmatched --write --unsafe --staged src",
|
|
31
|
-
"test": "bun test",
|
|
31
|
+
"test": "bash \"$(git rev-parse --show-toplevel)/scripts/par-test.sh\" 4 bun test",
|
|
32
32
|
"verify-types": "tsc --project tsconfig.json --noEmit"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@internal/tsconfig": "workspace:*",
|
|
36
36
|
"@subshell-ai/plugin-api": "workspace:*",
|
|
37
37
|
"@types/bun": "1.3.14",
|
|
38
|
-
"hash-runner": "
|
|
38
|
+
"hash-runner": "5.0.0",
|
|
39
39
|
"tsdown": "0.22.14",
|
|
40
40
|
"typescript": "7.0.2"
|
|
41
41
|
},
|