@subshell-ai/plugin-codex 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.
@@ -0,0 +1,341 @@
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
+ /**
309
+ * Built-in harness: Codex (OpenAI's `@openai/codex` CLI).
310
+ *
311
+ * Launch shape:
312
+ * codex [-m <model>] [-s <sandbox>] [-a <approval>] [mcp -c pairs]
313
+ * [profile flags] [extra flags]
314
+ * A bare launch (no subcommand, no prompt) opens the interactive TUI. Codex
315
+ * has no create-time flag for naming its own session and no way to pin a
316
+ * conversation id, so neither the subshell name nor a resume capability is
317
+ * forwarded — restarts start a fresh conversation (the documented default).
318
+ * The pane's
319
+ * cwd is already the working directory, so `-C/--cd` is never passed, and
320
+ * `--dangerously-bypass-approvals-and-sandbox` is NEVER baked into a launch
321
+ * (a user may put it in their own profile flags — that is their call).
322
+ *
323
+ * subshell MCP is wired automatically WITHOUT touching user state: every
324
+ * launch carries `-c mcp_servers.subshell.command=… -c mcp_servers.subshell.args=…`
325
+ * config overrides (dotted path, value parsed as TOML — verified against the
326
+ * real binary), which codex merges over ~/.codex/config.toml for that run
327
+ * only. No config file the harness must read, no CODEX_HOME redirect (that
328
+ * dir holds the user's auth.json). The spawned `subshell mcp` child inherits
329
+ * the subshell's baked SUBSHELL_* pane env for its credential, like every
330
+ * harness.
331
+ * (Flags/values verified against @openai/codex 2026-09 help output.)
332
+ *
333
+ * Identity, detection and install guidance live in this package's
334
+ * package.json `subshell` block, NOT here: the host reads them without
335
+ * importing or executing a line of this file.
336
+ *
337
+ * `host` carries what this module cannot import. See `@subshell-ai/plugin-api`.
338
+ */
339
+ declare const createPlugin: PluginFactory;
340
+ //#endregion
341
+ export { createPlugin as default, manifest };
package/dist/index.js ADDED
@@ -0,0 +1,321 @@
1
+ //#region ../../plugin-api/dist/manifest-im30YVIq.js
2
+ /** Every {@link PluginType}, for anything that has to iterate or validate them. */
3
+ const PLUGIN_TYPES = ["agent-harness", "terminal"];
4
+ /**
5
+ * The MCP server name every harness registers the built-in `subshell mcp`
6
+ * server under: the config object key, and the `hermes mcp add|remove`
7
+ * argument alike. The server reports the same name in its MCP handshake, which
8
+ * is what harnesses surface in wire tool ids (`mcp__<name>__<tool>`).
9
+ */
10
+ const MCP_SERVER_NAME = "subshell";
11
+ /**
12
+ * Ids become directory names under `<dataDir>/plugins/`, so they are path
13
+ * segments and nothing else: no separators, no traversal, no surprises from a
14
+ * case-insensitive filesystem.
15
+ */
16
+ const ID_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
17
+ function isRecord(value) {
18
+ return typeof value === "object" && value !== null && !Array.isArray(value);
19
+ }
20
+ /**
21
+ * Reads and validates the `subshell` block of a plugin's package.json.
22
+ *
23
+ * Answers `{ error }` rather than throwing, because every caller is reporting
24
+ * a broken plugin to a person rather than handling an exception: the node
25
+ * marks the plugin broken and renders this message beside it. The strings are
26
+ * therefore specific on purpose.
27
+ * @param pkgJson - the parsed package.json of a plugin directory
28
+ */
29
+ function parseManifest(pkgJson) {
30
+ if (!isRecord(pkgJson)) return { error: "package.json is not an object" };
31
+ const block = pkgJson.subshell;
32
+ if (!isRecord(block)) return { error: "package.json has no `subshell` block, so it is not a Subshell plugin" };
33
+ if (typeof block.apiVersion !== "number" || !Number.isInteger(block.apiVersion)) return { error: "`subshell.apiVersion` must be an integer" };
34
+ 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` };
35
+ if (block.apiVersion > 1) return { error: `this plugin needs plugin-api ${block.apiVersion}, but this host implements 1; upgrade the agent` };
36
+ 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)" };
37
+ if (typeof block.type !== "string" || !PLUGIN_TYPES.includes(block.type)) return { error: `\`subshell.type\` must be one of: ${PLUGIN_TYPES.join(", ")}` };
38
+ if (typeof block.name !== "string" || block.name.trim() === "") return { error: "`subshell.name` must be a non-empty string" };
39
+ if (typeof block.description !== "string") return { error: "`subshell.description` must be a string" };
40
+ if (block.icon !== void 0 && typeof block.icon !== "string") return { error: "`subshell.icon` must be a string" };
41
+ if (typeof block.entry !== "string" || block.entry.trim() === "") return { error: "`subshell.entry` must be a non-empty relative path" };
42
+ if (block.entry.startsWith("/") || block.entry.split("/").includes("..")) return { error: "`subshell.entry` must stay inside the package (no leading `/` and no `..` segment)" };
43
+ let detect;
44
+ if (block.detect !== void 0) {
45
+ const d = block.detect;
46
+ 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" };
47
+ detect = {
48
+ binaryName: d.binaryName,
49
+ envOverride: d.envOverride,
50
+ knownPaths: d.knownPaths
51
+ };
52
+ }
53
+ let install;
54
+ if (block.install !== void 0) {
55
+ const i = block.install;
56
+ if (!isRecord(i) || typeof i.command !== "string" || typeof i.docsUrl !== "string") return { error: "`subshell.install` needs a command and a docsUrl" };
57
+ install = {
58
+ command: i.command,
59
+ docsUrl: i.docsUrl
60
+ };
61
+ }
62
+ return {
63
+ apiVersion: block.apiVersion,
64
+ id: block.id,
65
+ type: block.type,
66
+ name: block.name,
67
+ description: block.description,
68
+ ...typeof block.icon === "string" ? { icon: block.icon } : {},
69
+ entry: block.entry,
70
+ ...detect ? { detect } : {},
71
+ ...install ? { install } : {}
72
+ };
73
+ }
74
+ //#endregion
75
+ //#region ../../plugin-api/dist/index.js
76
+ /**
77
+ * The profile checks every built-in harness performs unchanged: a non-empty
78
+ * name, string env values, and flags that look like flags. Harnesses with
79
+ * extra rules run their own checks on top of these.
80
+ * @param profile - The decoded profile definition to check
81
+ * @returns Issues per field; `valid` is true when no issues were found
82
+ */
83
+ function validateGenericProfile(profile) {
84
+ const issues = [];
85
+ if (!profile.name || profile.name.trim().length === 0) issues.push({
86
+ field: "name",
87
+ message: "Profile name is required."
88
+ });
89
+ for (const [key, value] of Object.entries(profile.env)) if (typeof value !== "string") issues.push({
90
+ field: "env",
91
+ message: `Env var "${key}" must be a string.`
92
+ });
93
+ for (const flag of profile.flags) if (!flag.startsWith("-")) issues.push({
94
+ field: "flags",
95
+ message: `Flag "${flag}" must start with "-".`
96
+ });
97
+ return {
98
+ valid: issues.length === 0,
99
+ issues
100
+ };
101
+ }
102
+ //#endregion
103
+ //#region src/manifest.ts
104
+ /**
105
+ * This package's own `subshell` block, read from its package.json so the
106
+ * manifest has ONE source of truth.
107
+ *
108
+ * A host that installs this plugin at runtime reads package.json off disk; a
109
+ * host that ships it built in imports this. Both must describe the same
110
+ * plugin, and the only way to guarantee that is for them to be the same bytes.
111
+ */
112
+ const parsed = parseManifest({
113
+ name: "@subshell-ai/plugin-codex",
114
+ description: "Codex harness plugin for Subshell",
115
+ version: "0.0.1",
116
+ license: "Apache-2.0",
117
+ type: "module",
118
+ main: "dist/index.js",
119
+ exports: { ".": {
120
+ "types": "./dist/index.d.ts",
121
+ "import": "./dist/index.js"
122
+ } },
123
+ types: "dist/index.d.ts",
124
+ scripts: {
125
+ "build": "tsdown",
126
+ "build:dev": "hash-runner",
127
+ "clean": "rm -rf .turbo node_modules dist .hashes.json",
128
+ "lint": "biome check --write --unsafe src && biome format src --write && biome lint src --fix",
129
+ "lint:check": "biome check --no-errors-on-unmatched src",
130
+ "lint:staged": "biome check --no-errors-on-unmatched --write --unsafe --staged src",
131
+ "test": "bun test",
132
+ "verify-types": "tsc --project tsconfig.json --noEmit"
133
+ },
134
+ devDependencies: {
135
+ "@internal/tsconfig": "workspace:*",
136
+ "@subshell-ai/plugin-api": "workspace:*",
137
+ "@types/bun": "1.3.14",
138
+ "hash-runner": "4.0.0",
139
+ "tsdown": "0.22.14",
140
+ "typescript": "7.0.2"
141
+ },
142
+ files: ["dist"],
143
+ publishConfig: { "access": "public" },
144
+ subshell: {
145
+ "apiVersion": 1,
146
+ "id": "codex",
147
+ "type": "agent-harness",
148
+ "name": "Codex",
149
+ "description": "OpenAI's agentic coding CLI (interactive TUI)",
150
+ "icon": "📖",
151
+ "entry": "dist/index.js",
152
+ "detect": {
153
+ "binaryName": "codex",
154
+ "envOverride": "CODEX_PATH",
155
+ "knownPaths": [
156
+ ".local/bin/codex",
157
+ ".npm-global/bin/codex",
158
+ ".bun/bin/codex"
159
+ ]
160
+ },
161
+ "install": {
162
+ "command": "curl -fsSL https://chatgpt.com/codex/install.sh | sh",
163
+ "docsUrl": "https://learn.chatgpt.com/docs/codex/cli"
164
+ }
165
+ }
166
+ });
167
+ if ("error" in parsed) throw new Error(`@subshell-ai/plugin-codex has an invalid manifest: ${parsed.error}`);
168
+ const manifest = parsed;
169
+ //#endregion
170
+ //#region src/index.ts
171
+ /** Known Codex settings, applied as per-invocation CLI flags. */
172
+ const CODEX_SETTINGS_FIELDS = [
173
+ {
174
+ key: "model",
175
+ label: "Model",
176
+ description: "Model name passed as -m/--model (e.g. 'gpt-5-codex')",
177
+ type: "string"
178
+ },
179
+ {
180
+ key: "sandbox",
181
+ label: "Sandbox",
182
+ description: "What the agent may touch without asking (-s/--sandbox)",
183
+ type: "select",
184
+ choices: [
185
+ "read-only",
186
+ "workspace-write",
187
+ "danger-full-access"
188
+ ]
189
+ },
190
+ {
191
+ key: "askForApproval",
192
+ label: "Ask for approval",
193
+ description: "When codex pauses for a human decision (-a/--ask-for-approval)",
194
+ type: "select",
195
+ choices: ["on-request", "never"]
196
+ }
197
+ ];
198
+ const SUGGESTED_ENV = [{
199
+ key: "OPENAI_API_KEY",
200
+ description: "API key for the api-key login mode (else ChatGPT sign-in)"
201
+ }];
202
+ const SUGGESTED_FLAGS = [
203
+ {
204
+ flag: "--search",
205
+ description: "Enable live web search"
206
+ },
207
+ {
208
+ flag: "-p work",
209
+ description: "Apply a profile from ~/.codex/config.toml"
210
+ },
211
+ {
212
+ flag: "-c mcp_servers.foo.command=\"/usr/bin/foo\"",
213
+ description: "One-off config override (dotted path, value parsed as TOML)"
214
+ }
215
+ ];
216
+ /** A TOML basic string from an ASCII-safe JS string. */
217
+ function tomlString(value) {
218
+ return JSON.stringify(value);
219
+ }
220
+ /** A TOML inline array of basic strings — same JSON-compatibility argument. */
221
+ function tomlStringArray(values) {
222
+ return `[${values.map(tomlString).join(", ")}]`;
223
+ }
224
+ /**
225
+ * Built-in harness: Codex (OpenAI's `@openai/codex` CLI).
226
+ *
227
+ * Launch shape:
228
+ * codex [-m <model>] [-s <sandbox>] [-a <approval>] [mcp -c pairs]
229
+ * [profile flags] [extra flags]
230
+ * A bare launch (no subcommand, no prompt) opens the interactive TUI. Codex
231
+ * has no create-time flag for naming its own session and no way to pin a
232
+ * conversation id, so neither the subshell name nor a resume capability is
233
+ * forwarded — restarts start a fresh conversation (the documented default).
234
+ * The pane's
235
+ * cwd is already the working directory, so `-C/--cd` is never passed, and
236
+ * `--dangerously-bypass-approvals-and-sandbox` is NEVER baked into a launch
237
+ * (a user may put it in their own profile flags — that is their call).
238
+ *
239
+ * subshell MCP is wired automatically WITHOUT touching user state: every
240
+ * launch carries `-c mcp_servers.subshell.command=… -c mcp_servers.subshell.args=…`
241
+ * config overrides (dotted path, value parsed as TOML — verified against the
242
+ * real binary), which codex merges over ~/.codex/config.toml for that run
243
+ * only. No config file the harness must read, no CODEX_HOME redirect (that
244
+ * dir holds the user's auth.json). The spawned `subshell mcp` child inherits
245
+ * the subshell's baked SUBSHELL_* pane env for its credential, like every
246
+ * harness.
247
+ * (Flags/values verified against @openai/codex 2026-09 help output.)
248
+ *
249
+ * Identity, detection and install guidance live in this package's
250
+ * package.json `subshell` block, NOT here: the host reads them without
251
+ * importing or executing a line of this file.
252
+ *
253
+ * `host` carries what this module cannot import. See `@subshell-ai/plugin-api`.
254
+ */
255
+ const createPlugin = (_host) => ({
256
+ capabilities: () => ["mcp", "settings"],
257
+ buildCommand(input) {
258
+ const { binary, profile, extraFlags, mcp } = input;
259
+ const args = [binary];
260
+ const s = profile.settings ?? {};
261
+ if (typeof s.model === "string" && s.model) args.push("-m", s.model);
262
+ if (typeof s.sandbox === "string" && s.sandbox) args.push("-s", s.sandbox);
263
+ if (typeof s.askForApproval === "string" && s.askForApproval) args.push("-a", s.askForApproval);
264
+ if (mcp?.args) args.push(...mcp.args);
265
+ for (const flag of profile.flags) args.push(flag);
266
+ if (extraFlags) args.push(...extraFlags);
267
+ return args;
268
+ },
269
+ /**
270
+ * Codex config fragment registering subshell as a stdio MCP server — the
271
+ * exact `[mcp_servers.subshell]` block `~/.codex/config.toml` expects. The
272
+ * block is NOT what activates the server on subshell launches: `buildCommand`
273
+ * splices live `-c` overrides instead (see the module doc), so this file is a
274
+ * manual-setup reference a user may append to their own config.
275
+ */
276
+ mcpRegistration(launch, _configPath) {
277
+ return {
278
+ fileContent: [
279
+ "# Subshell: cross-subshell MCP server, in the shape ~/.codex/config.toml expects.",
280
+ "#",
281
+ "# This file is a MANUAL-SETUP REFERENCE ONLY: subshell's own launches pass the",
282
+ "# live values as `-c mcp_servers.subshell.*=…` per-invocation overrides, so codex",
283
+ "# never reads this file. To register subshell permanently (e.g. terminal codex),",
284
+ "# append the block below to ~/.codex/config.toml.",
285
+ "",
286
+ `[mcp_servers.${MCP_SERVER_NAME}]`,
287
+ `command = ${tomlString(launch.command)}`,
288
+ `args = ${tomlStringArray(launch.args)}`,
289
+ ""
290
+ ].join("\n"),
291
+ args: [
292
+ "-c",
293
+ `mcp_servers.${MCP_SERVER_NAME}.command=${tomlString(launch.command)}`,
294
+ "-c",
295
+ `mcp_servers.${MCP_SERVER_NAME}.args=${tomlStringArray(launch.args)}`
296
+ ],
297
+ env: void 0
298
+ };
299
+ },
300
+ /** Auto: `buildCommand` wires `-c mcp_servers.subshell.*` into every subshell. */
301
+ mcpSetup(_launch) {
302
+ return {
303
+ mode: "auto",
304
+ summary: "Subshell registers itself with every Codex subshell automatically (per-invocation -c mcp_servers.subshell.* overrides; your ~/.codex config is never modified)."
305
+ };
306
+ },
307
+ validateProfile(profile) {
308
+ return validateGenericProfile(profile);
309
+ },
310
+ profileSettings() {
311
+ return CODEX_SETTINGS_FIELDS;
312
+ },
313
+ suggestedEnv() {
314
+ return SUGGESTED_ENV;
315
+ },
316
+ suggestedFlags() {
317
+ return SUGGESTED_FLAGS;
318
+ }
319
+ });
320
+ //#endregion
321
+ export { createPlugin as default, manifest };
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@subshell-ai/plugin-codex",
3
+ "description": "Codex 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": "codex",
42
+ "type": "agent-harness",
43
+ "name": "Codex",
44
+ "description": "OpenAI's agentic coding CLI (interactive TUI)",
45
+ "icon": "\ud83d\udcd6",
46
+ "entry": "dist/index.js",
47
+ "detect": {
48
+ "binaryName": "codex",
49
+ "envOverride": "CODEX_PATH",
50
+ "knownPaths": [
51
+ ".local/bin/codex",
52
+ ".npm-global/bin/codex",
53
+ ".bun/bin/codex"
54
+ ]
55
+ },
56
+ "install": {
57
+ "command": "curl -fsSL https://chatgpt.com/codex/install.sh | sh",
58
+ "docsUrl": "https://learn.chatgpt.com/docs/codex/cli"
59
+ }
60
+ }
61
+ }