@subshell-ai/plugin-claude-code 0.0.1
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 +348 -0
- package/dist/index.js +490 -0
- package/package.json +62 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
//#region ../../plugin-api/dist/types-90lJTTx6.d.ts
|
|
2
|
+
//#region src/types.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* The contract a Subshell plugin implements.
|
|
5
|
+
*
|
|
6
|
+
* A plugin is loaded from disk by a compiled binary, which means it CANNOT
|
|
7
|
+
* import anything of ours at runtime. Measured on bun 1.4.2: a bare specifier
|
|
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.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* What kind of thing a plugin provides.
|
|
19
|
+
*
|
|
20
|
+
* For humans: it groups and labels in the UI and filters the catalog. Code
|
|
21
|
+
* branches on {@link PluginCapability} instead, so adding a type here touches
|
|
22
|
+
* labels while adding a capability touches the launch pipeline.
|
|
23
|
+
*/
|
|
24
|
+
type PluginType = "agent-harness" | "terminal";
|
|
25
|
+
/**
|
|
26
|
+
* What a plugin can DO. The launch pipeline and the UI read this; neither
|
|
27
|
+
* branches on {@link PluginType}.
|
|
28
|
+
*/
|
|
29
|
+
type PluginCapability = "mcp" | "resume" | "attention" | "settings";
|
|
30
|
+
/** A harness profile as defined by the user (decoded JSON blobs). */
|
|
31
|
+
interface ProfileDefinition {
|
|
32
|
+
/** Human-friendly profile name */
|
|
33
|
+
name: string;
|
|
34
|
+
/** Optional longer description */
|
|
35
|
+
description?: string | null;
|
|
36
|
+
/** Extra environment variables to set on the subshell */
|
|
37
|
+
env: Record<string, string>;
|
|
38
|
+
/** Extra CLI flags to pass to the harness binary */
|
|
39
|
+
flags: string[];
|
|
40
|
+
/** Settings blob passed to the harness (e.g. claude --settings JSON) */
|
|
41
|
+
settings: Record<string, unknown> | null;
|
|
42
|
+
/** If true, only this profile's config sources apply (isolation) */
|
|
43
|
+
configIsolation: boolean;
|
|
44
|
+
/** If true, new subshells from this profile auto-restart on exit */
|
|
45
|
+
restartOnExit?: boolean;
|
|
46
|
+
}
|
|
47
|
+
/** Snapshot of a single field-level validation error on a profile. */
|
|
48
|
+
interface ProfileValidationIssue {
|
|
49
|
+
/** Field name (e.g. "name", "env", "settings") */
|
|
50
|
+
field: string;
|
|
51
|
+
/** Human-readable problem description */
|
|
52
|
+
message: string;
|
|
53
|
+
}
|
|
54
|
+
interface ProfileValidationResult {
|
|
55
|
+
valid: boolean;
|
|
56
|
+
issues: ProfileValidationIssue[];
|
|
57
|
+
}
|
|
58
|
+
/** Everything a plugin needs to build a launch command. */
|
|
59
|
+
interface BuildCommandInput {
|
|
60
|
+
/** Resolved absolute path to the harness binary */
|
|
61
|
+
binary: string;
|
|
62
|
+
/** Working directory the harness runs in */
|
|
63
|
+
cwd: string;
|
|
64
|
+
/** The validated profile being used */
|
|
65
|
+
profile: ProfileDefinition;
|
|
66
|
+
/** Subshell display name ("" = let the harness pick a default) */
|
|
67
|
+
subshellName: string;
|
|
68
|
+
/** Any additional CLI flags from route/request context */
|
|
69
|
+
extraFlags?: string[];
|
|
70
|
+
/**
|
|
71
|
+
* The subshell's MCP registration (channels + subshell orchestration), as
|
|
72
|
+
* produced by this plugin's own `mcpRegistration` and already written to
|
|
73
|
+
* disk. Plugins consume ONLY `mcp.args`: splice them where the dialect needs
|
|
74
|
+
* them (claude right after the binary). `mcp.env` is baked into the pane by
|
|
75
|
+
* the host before the command runs; plugins must not consume it themselves.
|
|
76
|
+
* Undefined = no registration for this launch.
|
|
77
|
+
*/
|
|
78
|
+
mcp?: McpRegistration;
|
|
79
|
+
/**
|
|
80
|
+
* Conversation identity for restart-resume; set only when this plugin
|
|
81
|
+
* declares the `resume` capability. `mode: "start"` means the conversation
|
|
82
|
+
* is NEW and must be created under exactly this id (pin it, because the host
|
|
83
|
+
* stores it and later resumes by it); `mode: "resume"` names an EXISTING
|
|
84
|
+
* conversation to continue (the host only asks after {@link HarnessResume.canResume}
|
|
85
|
+
* confirmed it survives).
|
|
86
|
+
*/
|
|
87
|
+
harnessSession?: {
|
|
88
|
+
id: string;
|
|
89
|
+
mode: "start" | "resume";
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Restart-resume, implemented only by plugins that can continue a previous
|
|
94
|
+
* conversation.
|
|
95
|
+
*
|
|
96
|
+
* The ids here are HARNESS conversation ids (a claude transcript uuid, say),
|
|
97
|
+
* never subshell ids: the host pins {@link allocateHarnessSessionId} at launch,
|
|
98
|
+
* stores it on the subshell row, and consults {@link canResume} before every
|
|
99
|
+
* restart to choose between continuing and starting fresh.
|
|
100
|
+
*/
|
|
101
|
+
interface HarnessResume {
|
|
102
|
+
/** Allocates the HARNESS conversation id to pin at launch (a uuid for claude). */
|
|
103
|
+
allocateHarnessSessionId(): string;
|
|
104
|
+
/**
|
|
105
|
+
* Whether the harness conversation `harnessSessionId` (last run in `cwd`)
|
|
106
|
+
* still exists and can be resumed. False means the host launches a fresh
|
|
107
|
+
* conversation rather than handing the harness an id it would reject.
|
|
108
|
+
*/
|
|
109
|
+
canResume(harnessSessionId: string, cwd: string): boolean;
|
|
110
|
+
}
|
|
111
|
+
/** How to spawn the `subshell mcp` stdio server. */
|
|
112
|
+
interface McpLaunchSpec {
|
|
113
|
+
/** Executable to run (the self command, the interpreter, or the agent binary) */
|
|
114
|
+
command: string;
|
|
115
|
+
/** Arguments for the executable (e.g. the mcp entry script path) */
|
|
116
|
+
args: string[];
|
|
117
|
+
}
|
|
118
|
+
/** Per-subshell MCP registration rendered in the plugin's own config dialect. */
|
|
119
|
+
interface McpRegistration {
|
|
120
|
+
/** File content the harness reads, written to the subshell's config path */
|
|
121
|
+
fileContent: string;
|
|
122
|
+
/** argv the harness needs to load the file (e.g. claude's ["--mcp-config", path]) */
|
|
123
|
+
args?: string[];
|
|
124
|
+
/**
|
|
125
|
+
* Extra pane env the harness needs to discover the file (e.g. OPENCODE_CONFIG).
|
|
126
|
+
* Consumed by the HOST, which bakes it into the pane env ahead of profile env;
|
|
127
|
+
* plugins never read it back.
|
|
128
|
+
*/
|
|
129
|
+
env?: Record<string, string>;
|
|
130
|
+
}
|
|
131
|
+
/** One copy-paste line shown in the profile editor for manual-setup harnesses. */
|
|
132
|
+
interface McpSetupStep {
|
|
133
|
+
/** What the user should do, and where the text goes */
|
|
134
|
+
label: string;
|
|
135
|
+
/** Copyable command or snippet */
|
|
136
|
+
command: string;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* How a harness obtains the `subshell mcp` tools.
|
|
140
|
+
*
|
|
141
|
+
* Discriminated on purpose: auto harnesses explain themselves in one line and
|
|
142
|
+
* manual harnesses carry steps, and a plugin cannot mix the two.
|
|
143
|
+
*/
|
|
144
|
+
type McpSetupInfo = {
|
|
145
|
+
mode: "auto";
|
|
146
|
+
summary: string;
|
|
147
|
+
} | {
|
|
148
|
+
mode: "manual";
|
|
149
|
+
steps: McpSetupStep[];
|
|
150
|
+
};
|
|
151
|
+
/** A single option in a plugin's settings editor. */
|
|
152
|
+
interface SettingsField {
|
|
153
|
+
/** Key into the settings object (e.g. "permissionMode") */
|
|
154
|
+
key: string;
|
|
155
|
+
/** Property label */
|
|
156
|
+
label: string;
|
|
157
|
+
/** Short description for the editor */
|
|
158
|
+
description?: string;
|
|
159
|
+
/** One of: string, boolean, number, select */
|
|
160
|
+
type: "string" | "boolean" | "number" | "select";
|
|
161
|
+
/** Choices when type === "select" */
|
|
162
|
+
choices?: string[];
|
|
163
|
+
/** Default value when unset */
|
|
164
|
+
default?: string | boolean | number;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Why there is no binary.
|
|
168
|
+
*
|
|
169
|
+
* `no-binary` is not a failure: a plugin (a `terminal` one, say) can declare
|
|
170
|
+
* no binary at all, and rendering that as "not on PATH" tells someone their
|
|
171
|
+
* PATH is wrong about something that never wanted one.
|
|
172
|
+
*/
|
|
173
|
+
type DetectionReason = "not-on-path" | "override-invalid" | "no-binary";
|
|
174
|
+
/** A detection answer: the path, or the reason there is not one. */
|
|
175
|
+
type DetectionResult = {
|
|
176
|
+
path: string;
|
|
177
|
+
reason?: undefined;
|
|
178
|
+
} | {
|
|
179
|
+
path: null;
|
|
180
|
+
reason: DetectionReason;
|
|
181
|
+
};
|
|
182
|
+
/**
|
|
183
|
+
* Everything the host lends a plugin, because a plugin can import none of it.
|
|
184
|
+
*
|
|
185
|
+
* This is also the version boundary: a host at a higher `apiVersion` keeps
|
|
186
|
+
* older plugins working by keeping the fields they were compiled against, so
|
|
187
|
+
* members are ADDED here and never removed or retyped.
|
|
188
|
+
*/
|
|
189
|
+
interface PluginHost {
|
|
190
|
+
/** The API version this host implements. Always >= the plugin's own. */
|
|
191
|
+
readonly apiVersion: number;
|
|
192
|
+
/** Resolve a binary through the host's full lookup ladder. */
|
|
193
|
+
findBinary(name: string, envOverride: string, knownPaths: string[]): Promise<string | null>;
|
|
194
|
+
/** The same lookup, reporting why it failed. */
|
|
195
|
+
detectBinary(name: string, envOverride: string, knownPaths: string[]): Promise<DetectionResult>;
|
|
196
|
+
/** Run a short command with a deadline; trimmed stdout, or null. */
|
|
197
|
+
probeVersion(binary: string, args?: string[]): Promise<string | null>;
|
|
198
|
+
/** POSIX-quote one argument for a shell command line. */
|
|
199
|
+
shellQuote(value: string): string;
|
|
200
|
+
/** Structured logging, namespaced to the plugin. */
|
|
201
|
+
log: {
|
|
202
|
+
debug(message: string): void;
|
|
203
|
+
warn(message: string): void;
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* The plugin itself.
|
|
208
|
+
*
|
|
209
|
+
* Only the first three members are required. Everything else is a capability
|
|
210
|
+
* the plugin opts into and declares from {@link capabilities}, so a `terminal`
|
|
211
|
+
* plugin is a binary and an argv rather than eight stubs.
|
|
212
|
+
*/
|
|
213
|
+
interface SubshellPlugin {
|
|
214
|
+
/** Builds the argv (no shell) used to launch a subshell. */
|
|
215
|
+
buildCommand(input: BuildCommandInput): string[];
|
|
216
|
+
/** Validates a profile definition before saving. */
|
|
217
|
+
validateProfile(profile: ProfileDefinition): ProfileValidationResult;
|
|
218
|
+
/** Which optional members below are meaningful on this plugin. */
|
|
219
|
+
capabilities(): PluginCapability[];
|
|
220
|
+
/** Maps a harness exit code to a human label (null = unknown). */
|
|
221
|
+
exitStatus?(code: number): string | null;
|
|
222
|
+
/**
|
|
223
|
+
* Interprets the raw stdout of the version probe.
|
|
224
|
+
*
|
|
225
|
+
* Probing is the host's job (it owns the deadline); making sense of the
|
|
226
|
+
* output is the plugin's, because only the plugin knows its harness prints
|
|
227
|
+
* a banner rather than a bare semver. Omit it and the trimmed output is
|
|
228
|
+
* used as-is, which is right for every harness that prints just a version.
|
|
229
|
+
*/
|
|
230
|
+
parseVersion?(raw: string): string | null;
|
|
231
|
+
/** Overrides manifest-driven detection. Almost no plugin needs this. */
|
|
232
|
+
detect?(): Promise<DetectionResult>;
|
|
233
|
+
/** Restart-resume support. Declare the `resume` capability with it. */
|
|
234
|
+
resume?: HarnessResume;
|
|
235
|
+
/**
|
|
236
|
+
* True when `buildCommand` wires this harness's native "needs attention"
|
|
237
|
+
* reporting into the launch. The host's quiet-output idle watcher skips
|
|
238
|
+
* these. Declare the `attention` capability with it.
|
|
239
|
+
*/
|
|
240
|
+
supportsAttentionHooks?: boolean;
|
|
241
|
+
/**
|
|
242
|
+
* Renders the per-subshell MCP registration in this harness's config format.
|
|
243
|
+
* Omit when the harness cannot consume a per-subshell config file; the host
|
|
244
|
+
* then surfaces one-time manual setup via {@link mcpSetup} instead.
|
|
245
|
+
*/
|
|
246
|
+
mcpRegistration?(launch: McpLaunchSpec, configPath: string): McpRegistration;
|
|
247
|
+
/** How users obtain the subshell MCP tools here. Declare the `mcp` capability with it. */
|
|
248
|
+
mcpSetup?(launch: McpLaunchSpec): McpSetupInfo;
|
|
249
|
+
/** Settings rendered in the PROFILE editor and stored on the profile. */
|
|
250
|
+
profileSettings?(): SettingsField[];
|
|
251
|
+
/** Known extra env var suggestions for the profile editor. */
|
|
252
|
+
suggestedEnv?(): {
|
|
253
|
+
key: string;
|
|
254
|
+
description: string;
|
|
255
|
+
}[];
|
|
256
|
+
/** Known CLI flag suggestions for the profile editor. */
|
|
257
|
+
suggestedFlags?(): {
|
|
258
|
+
flag: string;
|
|
259
|
+
description: string;
|
|
260
|
+
}[];
|
|
261
|
+
}
|
|
262
|
+
/** What a plugin module default-exports. */
|
|
263
|
+
type PluginFactory = (host: PluginHost) => SubshellPlugin;
|
|
264
|
+
//#endregion
|
|
265
|
+
//#region ../../plugin-api/dist/index.d.ts
|
|
266
|
+
/** How a host finds this plugin's binary, as data, so a scan loads no plugin code. */
|
|
267
|
+
interface DetectSpec {
|
|
268
|
+
/** Executable name to look for, e.g. "claude" */
|
|
269
|
+
binaryName: string;
|
|
270
|
+
/** Env var that overrides the lookup outright, e.g. "CLAUDE_PATH" */
|
|
271
|
+
envOverride: string;
|
|
272
|
+
/** Well-known locations relative to HOME, tried after PATH */
|
|
273
|
+
knownPaths: string[];
|
|
274
|
+
}
|
|
275
|
+
/** Where to send someone whose binary is missing. */
|
|
276
|
+
interface InstallSpec {
|
|
277
|
+
/** Copy-pasteable install command for the official installer */
|
|
278
|
+
command: string;
|
|
279
|
+
/** URL of the installation documentation */
|
|
280
|
+
docsUrl: string;
|
|
281
|
+
}
|
|
282
|
+
/** The parsed `subshell` block. */
|
|
283
|
+
interface SubshellManifest {
|
|
284
|
+
/** plugin-api version this plugin was built against */
|
|
285
|
+
apiVersion: number;
|
|
286
|
+
/** Stable id, also its directory name */
|
|
287
|
+
id: string;
|
|
288
|
+
/** What kind of thing this provides */
|
|
289
|
+
type: PluginType;
|
|
290
|
+
/** Display name */
|
|
291
|
+
name: string;
|
|
292
|
+
/** One-line description shown in the UI */
|
|
293
|
+
description: string;
|
|
294
|
+
/** Optional emoji/glyph */
|
|
295
|
+
icon?: string;
|
|
296
|
+
/** Module to import, relative to the package directory */
|
|
297
|
+
entry: string;
|
|
298
|
+
/** Manifest-driven detection; absent for a plugin that needs no binary */
|
|
299
|
+
detect?: DetectSpec;
|
|
300
|
+
/** Install guidance shown when the binary is missing */
|
|
301
|
+
install?: InstallSpec;
|
|
302
|
+
}
|
|
303
|
+
//#endregion
|
|
304
|
+
//#region src/manifest.d.ts
|
|
305
|
+
declare const manifest: SubshellManifest;
|
|
306
|
+
//#endregion
|
|
307
|
+
//#region src/index.d.ts
|
|
308
|
+
/** The `--settings` hooks object injected into every Claude Code launch. */
|
|
309
|
+
declare const ATTENTION_HOOKS: {
|
|
310
|
+
readonly Stop: readonly [{
|
|
311
|
+
readonly hooks: readonly [{
|
|
312
|
+
readonly type: "command";
|
|
313
|
+
readonly command: string;
|
|
314
|
+
}];
|
|
315
|
+
}];
|
|
316
|
+
readonly Notification: readonly [{
|
|
317
|
+
readonly hooks: readonly [{
|
|
318
|
+
readonly type: "command";
|
|
319
|
+
readonly command: string;
|
|
320
|
+
}];
|
|
321
|
+
}];
|
|
322
|
+
readonly SessionStart: readonly [{
|
|
323
|
+
readonly hooks: readonly [{
|
|
324
|
+
readonly type: "command";
|
|
325
|
+
readonly command: string;
|
|
326
|
+
}];
|
|
327
|
+
}];
|
|
328
|
+
};
|
|
329
|
+
/**
|
|
330
|
+
* Built-in harness: Claude Code.
|
|
331
|
+
*
|
|
332
|
+
* Launch shape:
|
|
333
|
+
* claude --settings <json> --name <subshell> [profile flags] [extra flags]
|
|
334
|
+
* Settings are passed as a JSON string via `--settings` (per-invocation, so
|
|
335
|
+
* profile config never needs to write into the user's real ~/.claude).
|
|
336
|
+
*
|
|
337
|
+
* Identity, detection and install guidance live in this package's
|
|
338
|
+
* package.json `subshell` block, NOT here: the host reads them without
|
|
339
|
+
* importing or executing a line of this file, which is what lets it list a
|
|
340
|
+
* plugin cheaply and detect `claude` before this plugin is even installed.
|
|
341
|
+
*
|
|
342
|
+
* `host` carries what this module cannot import (binary lookup, the bounded
|
|
343
|
+
* version probe, shell quoting, a namespaced logger). See
|
|
344
|
+
* `@subshell-ai/plugin-api`.
|
|
345
|
+
*/
|
|
346
|
+
declare const createPlugin: PluginFactory;
|
|
347
|
+
//#endregion
|
|
348
|
+
export { ATTENTION_HOOKS, createPlugin as default, manifest };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
//#region ../../plugin-api/dist/manifest-im30YVIq.js
|
|
5
|
+
/** Every {@link PluginType}, for anything that has to iterate or validate them. */
|
|
6
|
+
const PLUGIN_TYPES = ["agent-harness", "terminal"];
|
|
7
|
+
/**
|
|
8
|
+
* The MCP server name every harness registers the built-in `subshell mcp`
|
|
9
|
+
* server under: the config object key, and the `hermes mcp add|remove`
|
|
10
|
+
* argument alike. The server reports the same name in its MCP handshake, which
|
|
11
|
+
* is what harnesses surface in wire tool ids (`mcp__<name>__<tool>`).
|
|
12
|
+
*/
|
|
13
|
+
const MCP_SERVER_NAME = "subshell";
|
|
14
|
+
/**
|
|
15
|
+
* Ids become directory names under `<dataDir>/plugins/`, so they are path
|
|
16
|
+
* segments and nothing else: no separators, no traversal, no surprises from a
|
|
17
|
+
* case-insensitive filesystem.
|
|
18
|
+
*/
|
|
19
|
+
const ID_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
20
|
+
function isRecord(value) {
|
|
21
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Reads and validates the `subshell` block of a plugin's package.json.
|
|
25
|
+
*
|
|
26
|
+
* Answers `{ error }` rather than throwing, because every caller is reporting
|
|
27
|
+
* a broken plugin to a person rather than handling an exception: the node
|
|
28
|
+
* marks the plugin broken and renders this message beside it. The strings are
|
|
29
|
+
* therefore specific on purpose.
|
|
30
|
+
* @param pkgJson - the parsed package.json of a plugin directory
|
|
31
|
+
*/
|
|
32
|
+
function parseManifest(pkgJson) {
|
|
33
|
+
if (!isRecord(pkgJson)) return { error: "package.json is not an object" };
|
|
34
|
+
const block = pkgJson.subshell;
|
|
35
|
+
if (!isRecord(block)) return { error: "package.json has no `subshell` block, so it is not a Subshell plugin" };
|
|
36
|
+
if (typeof block.apiVersion !== "number" || !Number.isInteger(block.apiVersion)) return { error: "`subshell.apiVersion` must be an integer" };
|
|
37
|
+
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 1` };
|
|
38
|
+
if (block.apiVersion > 1) return { error: `this plugin needs plugin-api ${block.apiVersion}, but this host implements 1; upgrade the agent` };
|
|
39
|
+
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)" };
|
|
40
|
+
if (typeof block.type !== "string" || !PLUGIN_TYPES.includes(block.type)) return { error: `\`subshell.type\` must be one of: ${PLUGIN_TYPES.join(", ")}` };
|
|
41
|
+
if (typeof block.name !== "string" || block.name.trim() === "") return { error: "`subshell.name` must be a non-empty string" };
|
|
42
|
+
if (typeof block.description !== "string") return { error: "`subshell.description` must be a string" };
|
|
43
|
+
if (block.icon !== void 0 && typeof block.icon !== "string") return { error: "`subshell.icon` must be a string" };
|
|
44
|
+
if (typeof block.entry !== "string" || block.entry.trim() === "") return { error: "`subshell.entry` must be a non-empty relative path" };
|
|
45
|
+
if (block.entry.startsWith("/") || block.entry.split("/").includes("..")) return { error: "`subshell.entry` must stay inside the package (no leading `/` and no `..` segment)" };
|
|
46
|
+
let detect;
|
|
47
|
+
if (block.detect !== void 0) {
|
|
48
|
+
const d = block.detect;
|
|
49
|
+
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" };
|
|
50
|
+
detect = {
|
|
51
|
+
binaryName: d.binaryName,
|
|
52
|
+
envOverride: d.envOverride,
|
|
53
|
+
knownPaths: d.knownPaths
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
let install;
|
|
57
|
+
if (block.install !== void 0) {
|
|
58
|
+
const i = block.install;
|
|
59
|
+
if (!isRecord(i) || typeof i.command !== "string" || typeof i.docsUrl !== "string") return { error: "`subshell.install` needs a command and a docsUrl" };
|
|
60
|
+
install = {
|
|
61
|
+
command: i.command,
|
|
62
|
+
docsUrl: i.docsUrl
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
apiVersion: block.apiVersion,
|
|
67
|
+
id: block.id,
|
|
68
|
+
type: block.type,
|
|
69
|
+
name: block.name,
|
|
70
|
+
description: block.description,
|
|
71
|
+
...typeof block.icon === "string" ? { icon: block.icon } : {},
|
|
72
|
+
entry: block.entry,
|
|
73
|
+
...detect ? { detect } : {},
|
|
74
|
+
...install ? { install } : {}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region ../../plugin-api/dist/index.js
|
|
79
|
+
/**
|
|
80
|
+
* The profile checks every built-in harness performs unchanged: a non-empty
|
|
81
|
+
* name, string env values, and flags that look like flags. Harnesses with
|
|
82
|
+
* extra rules run their own checks on top of these.
|
|
83
|
+
* @param profile - The decoded profile definition to check
|
|
84
|
+
* @returns Issues per field; `valid` is true when no issues were found
|
|
85
|
+
*/
|
|
86
|
+
function validateGenericProfile(profile) {
|
|
87
|
+
const issues = [];
|
|
88
|
+
if (!profile.name || profile.name.trim().length === 0) issues.push({
|
|
89
|
+
field: "name",
|
|
90
|
+
message: "Profile name is required."
|
|
91
|
+
});
|
|
92
|
+
for (const [key, value] of Object.entries(profile.env)) if (typeof value !== "string") issues.push({
|
|
93
|
+
field: "env",
|
|
94
|
+
message: `Env var "${key}" must be a string.`
|
|
95
|
+
});
|
|
96
|
+
for (const flag of profile.flags) if (!flag.startsWith("-")) issues.push({
|
|
97
|
+
field: "flags",
|
|
98
|
+
message: `Flag "${flag}" must start with "-".`
|
|
99
|
+
});
|
|
100
|
+
return {
|
|
101
|
+
valid: issues.length === 0,
|
|
102
|
+
issues
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
//#endregion
|
|
106
|
+
//#region src/manifest.ts
|
|
107
|
+
/**
|
|
108
|
+
* This package's own `subshell` block, read from its package.json so the
|
|
109
|
+
* manifest has ONE source of truth.
|
|
110
|
+
*
|
|
111
|
+
* A host that installs this plugin at runtime reads package.json off disk; a
|
|
112
|
+
* host that ships it built in imports this. Both must describe the same
|
|
113
|
+
* plugin, and the only way to guarantee that is for them to be the same bytes.
|
|
114
|
+
*/
|
|
115
|
+
const parsed = parseManifest({
|
|
116
|
+
name: "@subshell-ai/plugin-claude-code",
|
|
117
|
+
description: "Claude Code harness plugin for Subshell",
|
|
118
|
+
version: "0.0.1",
|
|
119
|
+
license: "Apache-2.0",
|
|
120
|
+
type: "module",
|
|
121
|
+
main: "dist/index.js",
|
|
122
|
+
exports: { ".": {
|
|
123
|
+
"types": "./dist/index.d.ts",
|
|
124
|
+
"import": "./dist/index.js"
|
|
125
|
+
} },
|
|
126
|
+
types: "dist/index.d.ts",
|
|
127
|
+
scripts: {
|
|
128
|
+
"build": "tsdown",
|
|
129
|
+
"build:dev": "hash-runner",
|
|
130
|
+
"clean": "rm -rf .turbo node_modules dist .hashes.json",
|
|
131
|
+
"lint": "biome check --write --unsafe src && biome format src --write && biome lint src --fix",
|
|
132
|
+
"lint:check": "biome check --no-errors-on-unmatched src",
|
|
133
|
+
"lint:staged": "biome check --no-errors-on-unmatched --write --unsafe --staged src",
|
|
134
|
+
"test": "bun test",
|
|
135
|
+
"verify-types": "tsc --project tsconfig.json --noEmit"
|
|
136
|
+
},
|
|
137
|
+
devDependencies: {
|
|
138
|
+
"@internal/tsconfig": "workspace:*",
|
|
139
|
+
"@subshell-ai/plugin-api": "workspace:*",
|
|
140
|
+
"@types/bun": "1.3.14",
|
|
141
|
+
"hash-runner": "4.0.0",
|
|
142
|
+
"tsdown": "0.22.14",
|
|
143
|
+
"typescript": "7.0.2"
|
|
144
|
+
},
|
|
145
|
+
files: ["dist"],
|
|
146
|
+
publishConfig: { "access": "public" },
|
|
147
|
+
subshell: {
|
|
148
|
+
"apiVersion": 1,
|
|
149
|
+
"id": "claude-code",
|
|
150
|
+
"type": "agent-harness",
|
|
151
|
+
"name": "Claude Code",
|
|
152
|
+
"description": "Anthropic's agentic coding assistant (interactive CLI)",
|
|
153
|
+
"icon": "🤖",
|
|
154
|
+
"entry": "dist/index.js",
|
|
155
|
+
"detect": {
|
|
156
|
+
"binaryName": "claude",
|
|
157
|
+
"envOverride": "CLAUDE_PATH",
|
|
158
|
+
"knownPaths": [
|
|
159
|
+
".local/bin/claude",
|
|
160
|
+
".local/share/claude/versions/claude",
|
|
161
|
+
".claude/local/claude",
|
|
162
|
+
".npm-global/bin/claude"
|
|
163
|
+
]
|
|
164
|
+
},
|
|
165
|
+
"install": {
|
|
166
|
+
"command": "curl -fsSL https://claude.ai/install.sh | bash",
|
|
167
|
+
"docsUrl": "https://code.claude.com/docs/en/setup"
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
if ("error" in parsed) throw new Error(`@subshell-ai/plugin-claude-code has an invalid manifest: ${parsed.error}`);
|
|
172
|
+
const manifest = parsed;
|
|
173
|
+
//#endregion
|
|
174
|
+
//#region src/index.ts
|
|
175
|
+
/** Known claude-code settings editor fields (top-level `--settings` keys). */
|
|
176
|
+
const CLAUDE_SETTINGS_FIELDS = [
|
|
177
|
+
{
|
|
178
|
+
key: "permissionMode",
|
|
179
|
+
label: "Permission mode",
|
|
180
|
+
description: "How permissions are handled for tool use",
|
|
181
|
+
type: "select",
|
|
182
|
+
choices: [
|
|
183
|
+
"default",
|
|
184
|
+
"acceptEdits",
|
|
185
|
+
"bypassPermissions",
|
|
186
|
+
"plan",
|
|
187
|
+
"dontAsk"
|
|
188
|
+
]
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
key: "model",
|
|
192
|
+
label: "Model",
|
|
193
|
+
description: "Model alias or full name (e.g. 'sonnet', 'opus', 'fable')",
|
|
194
|
+
type: "string"
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
key: "maxTokens",
|
|
198
|
+
label: "Max output tokens",
|
|
199
|
+
type: "number"
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
key: "outputStyle",
|
|
203
|
+
label: "Output style",
|
|
204
|
+
type: "select",
|
|
205
|
+
choices: [
|
|
206
|
+
"default",
|
|
207
|
+
"compact",
|
|
208
|
+
"expanded"
|
|
209
|
+
]
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
key: "alwaysAllowReadOnly",
|
|
213
|
+
label: "Always allow read-only tools",
|
|
214
|
+
type: "boolean"
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
key: "includeCoAuthoredBy",
|
|
218
|
+
label: "Add Co-Authored-By footer",
|
|
219
|
+
type: "boolean"
|
|
220
|
+
}
|
|
221
|
+
];
|
|
222
|
+
const SUGGESTED_ENV = [
|
|
223
|
+
{
|
|
224
|
+
key: "ANTHROPIC_API_KEY",
|
|
225
|
+
description: "API key (overrides logged-in account)"
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
key: "ANTHROPIC_AUTH_TOKEN",
|
|
229
|
+
description: "OAuth/bearer token instead of an API key"
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
key: "ANTHROPIC_MODEL",
|
|
233
|
+
description: "Model alias (deprecated in favor of settings)"
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
key: "ANTHROPIC_BASE_URL",
|
|
237
|
+
description: "API base URL (e.g. a gateway/proxy)"
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
key: "ANTHROPIC_DEFAULT_SONNET_MODEL",
|
|
241
|
+
description: "Model behind the 'sonnet' alias"
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
key: "ANTHROPIC_DEFAULT_OPUS_MODEL",
|
|
245
|
+
description: "Model behind the 'opus' alias"
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
key: "ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
|
249
|
+
description: "Model behind the 'haiku' alias"
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
key: "CLAUDE_CODE_MAX_OUTPUT_TOKENS",
|
|
253
|
+
description: "Max output tokens"
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
key: "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC",
|
|
257
|
+
description: "Disable telemetry/update traffic (1)"
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
key: "CLAUDE_CODE_SSE_URL",
|
|
261
|
+
description: "Anthropic SSE URL override (e.g. http://localhost:8080/anthropic)"
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
key: "CLAUDE_CODE_USE_BEDROCK",
|
|
265
|
+
description: "Use Amazon Bedrock (1)"
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
key: "CLAUDE_CODE_USE_VERTEX",
|
|
269
|
+
description: "Use Google Vertex AI (1)"
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
key: "BASH_DEFAULT_TIMEOUT_MS",
|
|
273
|
+
description: "Default timeout for Bash tool commands"
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
key: "MAX_THINKING_TOKENS",
|
|
277
|
+
description: "Cap on extended-thinking tokens"
|
|
278
|
+
}
|
|
279
|
+
];
|
|
280
|
+
const SUGGESTED_FLAGS = [
|
|
281
|
+
{
|
|
282
|
+
flag: "--dangerously-skip-permissions",
|
|
283
|
+
description: "Skip all permission prompts"
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
flag: "--permission-mode plan",
|
|
287
|
+
description: "Start in plan mode"
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
flag: "--model sonnet",
|
|
291
|
+
description: "Use a specific model alias"
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
flag: "--effort high",
|
|
295
|
+
description: "Effort level for the subshell"
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
flag: "--fallback-model opus",
|
|
299
|
+
description: "Fall back when the primary model is overloaded"
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
flag: "--agent reviewer",
|
|
303
|
+
description: "Use a custom agent definition"
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
flag: "--append-system-prompt <text>",
|
|
307
|
+
description: "Append to the system prompt"
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
flag: "--allowedTools Bash,Read,Edit",
|
|
311
|
+
description: "Allow only these tools"
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
flag: "--disallowedTools WebSearch",
|
|
315
|
+
description: "Deny these tools"
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
flag: "--mcp-config <path>",
|
|
319
|
+
description: "Load MCP servers from a JSON file"
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
flag: "--max-turns <n>",
|
|
323
|
+
description: "Cap agentic turns (print mode)"
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
flag: "--continue",
|
|
327
|
+
description: "Continue the most recent conversation"
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
flag: "--resume <id>",
|
|
331
|
+
description: "Resume a specific past conversation"
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
flag: "--fork-session",
|
|
335
|
+
description: "Fork instead of resuming in place"
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
flag: "--worktree",
|
|
339
|
+
description: "Run in a fresh git worktree"
|
|
340
|
+
}
|
|
341
|
+
];
|
|
342
|
+
/**
|
|
343
|
+
* Fire-and-forget attention reporting. Each hook runs `bun -e` (bun is on the
|
|
344
|
+
* pane PATH — the image ships it, and no curl exists there) and POSTs the
|
|
345
|
+
* subshell's own bearer to the attention endpoint; the server gates delivery
|
|
346
|
+
* on the subshell's bell and derives the "waiting for you" state. The env
|
|
347
|
+
* vars are baked into the pane by the backend (`subshellMcpEnv`), and every
|
|
348
|
+
* failure path is swallowed: a missing hook event costs one notification,
|
|
349
|
+
* never a broken subshell turn. SUBSHELL_BASE_URL must be reachable from inside
|
|
350
|
+
* the pane's network — in the container that means the published port, which
|
|
351
|
+
* compose already passes via APP_BASE_URL.
|
|
352
|
+
*/
|
|
353
|
+
const attentionPing = (kind) => `bun -e 'fetch(process.env.SUBSHELL_BASE_URL+"/api/subshells/"+process.env.SUBSHELL_ID+"/attention",{method:"POST",headers:{authorization:"Bearer "+process.env.SUBSHELL_API_KEY,"content-type":"application/json"},body:JSON.stringify({kind:${JSON.stringify(kind)}}),signal:AbortSignal.timeout(5000)}).catch(()=>{}).finally(()=>process.exit(0))'`;
|
|
354
|
+
/**
|
|
355
|
+
* Conversation-identity reporting. The restart-resume pin (`--session-id`
|
|
356
|
+
* at launch) only survives while the pane keeps that ONE conversation — but
|
|
357
|
+
* /clear, /resume <other>, and /fork start a DIFFERENT transcript id in-pane
|
|
358
|
+
* and nothing else tells the server, so the next restart would resurrect a
|
|
359
|
+
* stale conversation (observed 2026-09-03). The SessionStart hook fires on
|
|
360
|
+
* every such transition (source: startup|resume|clear|compact|fork).
|
|
361
|
+
*
|
|
362
|
+
* Unlike the attention pings, THIS command reads stdin — the deliberate,
|
|
363
|
+
* documented exception to the no-stdin rule (whose rationale is that
|
|
364
|
+
* Stop/Notification payloads carry conversation text): the SessionStart
|
|
365
|
+
* payload is pure metadata ({session_id, transcript_path, cwd, source,…})
|
|
366
|
+
* and ONLY `session_id` is forwarded; the response is fire-and-forget like
|
|
367
|
+
* the pings — every failure path is swallowed.
|
|
368
|
+
*
|
|
369
|
+
* Both awaits are bounded: a self-kill timer caps the WHOLE hook (4 s — an
|
|
370
|
+
* unclosed stdin must never stall the pane start, and a SessionStart hook
|
|
371
|
+
* blocks the harness until it exits) and a 2 s fetch timeout caps the POST
|
|
372
|
+
* (a lost report is harmless — the next transition re-reports).
|
|
373
|
+
*/
|
|
374
|
+
const sessionReportPing = () => "bun -e 'setTimeout(()=>process.exit(0),4000);try{const j=JSON.parse(await Bun.stdin.text());if(j.session_id)await fetch(process.env.SUBSHELL_BASE_URL+\"/api/subshells/\"+process.env.SUBSHELL_ID+\"/harness-session\",{method:\"POST\",headers:{authorization:\"Bearer \"+process.env.SUBSHELL_API_KEY,\"content-type\":\"application/json\"},body:JSON.stringify({sessionId:j.session_id}),signal:AbortSignal.timeout(2000)})}catch{}process.exit(0)'";
|
|
375
|
+
/** The `--settings` hooks object injected into every Claude Code launch. */
|
|
376
|
+
const ATTENTION_HOOKS = {
|
|
377
|
+
Stop: [{ hooks: [{
|
|
378
|
+
type: "command",
|
|
379
|
+
command: attentionPing("turn_complete")
|
|
380
|
+
}] }],
|
|
381
|
+
Notification: [{ hooks: [{
|
|
382
|
+
type: "command",
|
|
383
|
+
command: attentionPing("needs_attention")
|
|
384
|
+
}] }],
|
|
385
|
+
SessionStart: [{ hooks: [{
|
|
386
|
+
type: "command",
|
|
387
|
+
command: sessionReportPing()
|
|
388
|
+
}] }]
|
|
389
|
+
};
|
|
390
|
+
/** Claude's state dir: the documented override, else `~/.claude`. */
|
|
391
|
+
function claudeConfigDir() {
|
|
392
|
+
const override = process.env.CLAUDE_CONFIG_DIR?.trim();
|
|
393
|
+
return override ? resolve(override) : join(homedir(), ".claude");
|
|
394
|
+
}
|
|
395
|
+
/** Claude's transcript folder name for a project dir: every non-alphanumeric → `-`. */
|
|
396
|
+
function projectSlug(cwd) {
|
|
397
|
+
return cwd.replace(/[^a-zA-Z0-9]/g, "-");
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Built-in harness: Claude Code.
|
|
401
|
+
*
|
|
402
|
+
* Launch shape:
|
|
403
|
+
* claude --settings <json> --name <subshell> [profile flags] [extra flags]
|
|
404
|
+
* Settings are passed as a JSON string via `--settings` (per-invocation, so
|
|
405
|
+
* profile config never needs to write into the user's real ~/.claude).
|
|
406
|
+
*
|
|
407
|
+
* Identity, detection and install guidance live in this package's
|
|
408
|
+
* package.json `subshell` block, NOT here: the host reads them without
|
|
409
|
+
* importing or executing a line of this file, which is what lets it list a
|
|
410
|
+
* plugin cheaply and detect `claude` before this plugin is even installed.
|
|
411
|
+
*
|
|
412
|
+
* `host` carries what this module cannot import (binary lookup, the bounded
|
|
413
|
+
* version probe, shell quoting, a namespaced logger). See
|
|
414
|
+
* `@subshell-ai/plugin-api`.
|
|
415
|
+
*/
|
|
416
|
+
const createPlugin = (_host) => ({
|
|
417
|
+
capabilities: () => [
|
|
418
|
+
"mcp",
|
|
419
|
+
"resume",
|
|
420
|
+
"attention",
|
|
421
|
+
"settings"
|
|
422
|
+
],
|
|
423
|
+
supportsAttentionHooks: true,
|
|
424
|
+
/**
|
|
425
|
+
* Restart-resume: conversations are PINNED at start (`--session-id <uuid>`)
|
|
426
|
+
* so subshell always knows the exact id to resume by, and resume mode adds
|
|
427
|
+
* `--resume <id>` (Claude appends to the same transcript, so one id
|
|
428
|
+
* survives repeated restarts). Pinning is what avoids both `--continue`'s
|
|
429
|
+
* "most recent in this directory" ambiguity, since several subshells can
|
|
430
|
+
* share a cwd, and parsing the exit banner back out of the pane log.
|
|
431
|
+
*/
|
|
432
|
+
resume: {
|
|
433
|
+
allocateHarnessSessionId: () => crypto.randomUUID(),
|
|
434
|
+
canResume: (harnessSessionId, cwd) => existsSync(join(claudeConfigDir(), "projects", projectSlug(cwd), `${harnessSessionId}.jsonl`))
|
|
435
|
+
},
|
|
436
|
+
buildCommand(input) {
|
|
437
|
+
const { binary, profile, subshellName, extraFlags, mcp, harnessSession } = input;
|
|
438
|
+
const args = [binary];
|
|
439
|
+
if (mcp?.args) args.push(...mcp.args);
|
|
440
|
+
if (harnessSession) args.push(harnessSession.mode === "resume" ? "--resume" : "--session-id", harnessSession.id);
|
|
441
|
+
const settings = {
|
|
442
|
+
...profile.settings ?? {},
|
|
443
|
+
hooks: ATTENTION_HOOKS
|
|
444
|
+
};
|
|
445
|
+
args.push("--settings", JSON.stringify(settings));
|
|
446
|
+
if (subshellName) args.push("--name", subshellName);
|
|
447
|
+
for (const flag of profile.flags) args.push(flag);
|
|
448
|
+
if (extraFlags) args.push(...extraFlags);
|
|
449
|
+
return args;
|
|
450
|
+
},
|
|
451
|
+
/** Claude Code's `--mcp-config` document: `{ mcpServers: { subshell: {...} } }`. */
|
|
452
|
+
mcpRegistration(launch, configPath) {
|
|
453
|
+
return {
|
|
454
|
+
fileContent: `${JSON.stringify({ mcpServers: { [MCP_SERVER_NAME]: {
|
|
455
|
+
command: launch.command,
|
|
456
|
+
args: launch.args
|
|
457
|
+
} } }, null, 2)}\n`,
|
|
458
|
+
args: ["--mcp-config", configPath]
|
|
459
|
+
};
|
|
460
|
+
},
|
|
461
|
+
/** Auto: `buildCommand` wires `--mcp-config` into every subshell. */
|
|
462
|
+
mcpSetup(_launch) {
|
|
463
|
+
return {
|
|
464
|
+
mode: "auto",
|
|
465
|
+
summary: "Subshell registers itself with every Claude Code subshell automatically (via --mcp-config)."
|
|
466
|
+
};
|
|
467
|
+
},
|
|
468
|
+
validateProfile(profile) {
|
|
469
|
+
return validateGenericProfile(profile);
|
|
470
|
+
},
|
|
471
|
+
profileSettings() {
|
|
472
|
+
return CLAUDE_SETTINGS_FIELDS;
|
|
473
|
+
},
|
|
474
|
+
suggestedEnv() {
|
|
475
|
+
return SUGGESTED_ENV;
|
|
476
|
+
},
|
|
477
|
+
suggestedFlags() {
|
|
478
|
+
return SUGGESTED_FLAGS;
|
|
479
|
+
},
|
|
480
|
+
exitStatus(code) {
|
|
481
|
+
return {
|
|
482
|
+
1: "error doing work",
|
|
483
|
+
5: "permissions denied",
|
|
484
|
+
10: "closed-loop complete",
|
|
485
|
+
11: "gate closed"
|
|
486
|
+
}[code] ?? null;
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
//#endregion
|
|
490
|
+
export { ATTENTION_HOOKS, createPlugin as default, manifest };
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@subshell-ai/plugin-claude-code",
|
|
3
|
+
"description": "Claude Code harness plugin for Subshell",
|
|
4
|
+
"version": "0.0.1",
|
|
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
|
+
"scripts": {
|
|
16
|
+
"build": "tsdown",
|
|
17
|
+
"build:dev": "hash-runner",
|
|
18
|
+
"clean": "rm -rf .turbo node_modules dist .hashes.json",
|
|
19
|
+
"lint": "biome check --write --unsafe src && biome format src --write && biome lint src --fix",
|
|
20
|
+
"lint:check": "biome check --no-errors-on-unmatched src",
|
|
21
|
+
"lint:staged": "biome check --no-errors-on-unmatched --write --unsafe --staged src",
|
|
22
|
+
"test": "bun test",
|
|
23
|
+
"verify-types": "tsc --project tsconfig.json --noEmit"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@internal/tsconfig": "workspace:*",
|
|
27
|
+
"@subshell-ai/plugin-api": "workspace:*",
|
|
28
|
+
"@types/bun": "1.3.14",
|
|
29
|
+
"hash-runner": "4.0.0",
|
|
30
|
+
"tsdown": "0.22.14",
|
|
31
|
+
"typescript": "7.0.2"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist"
|
|
35
|
+
],
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"subshell": {
|
|
40
|
+
"apiVersion": 1,
|
|
41
|
+
"id": "claude-code",
|
|
42
|
+
"type": "agent-harness",
|
|
43
|
+
"name": "Claude Code",
|
|
44
|
+
"description": "Anthropic's agentic coding assistant (interactive CLI)",
|
|
45
|
+
"icon": "\ud83e\udd16",
|
|
46
|
+
"entry": "dist/index.js",
|
|
47
|
+
"detect": {
|
|
48
|
+
"binaryName": "claude",
|
|
49
|
+
"envOverride": "CLAUDE_PATH",
|
|
50
|
+
"knownPaths": [
|
|
51
|
+
".local/bin/claude",
|
|
52
|
+
".local/share/claude/versions/claude",
|
|
53
|
+
".claude/local/claude",
|
|
54
|
+
".npm-global/bin/claude"
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
"install": {
|
|
58
|
+
"command": "curl -fsSL https://claude.ai/install.sh | bash",
|
|
59
|
+
"docsUrl": "https://code.claude.com/docs/en/setup"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|