@oisincoveney/pipeline 3.11.7 → 3.11.9

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.
@@ -24,11 +24,11 @@ const RUNNER_OPENCODE_ENV = [{
24
24
  const DEFAULT_RUNNER_RESOURCES = {
25
25
  limits: {
26
26
  cpu: "4",
27
- memory: "8Gi"
27
+ memory: "12Gi"
28
28
  },
29
29
  requests: {
30
30
  cpu: "1",
31
- memory: "4Gi"
31
+ memory: "8Gi"
32
32
  }
33
33
  };
34
34
  const kubernetesNameSchema = z.string().min(1);
@@ -0,0 +1,28 @@
1
+ import { applyJsonEdit, ensureTrailingNewline, formatJson, parseJsonRecord, setIfMissing } from "./json-config-merge.js";
2
+ //#region src/claude-user-config.ts
3
+ function mergeClaudeUserConfig(currentText, projection) {
4
+ if (currentText === void 0) return {
5
+ content: formatJson(projection),
6
+ ok: true
7
+ };
8
+ const parsed = parseJsonRecord(currentText);
9
+ if (!parsed.ok) return parsed;
10
+ return {
11
+ content: ensureTrailingNewline(Object.entries(projection.mcpServers ?? {}).reduce((nextContent, [name, server]) => setIfMissing(nextContent, parsed.value, ["mcpServers", name], server), currentText)),
12
+ ok: true
13
+ };
14
+ }
15
+ function replaceClaudeUserMcpServers(currentText, projection) {
16
+ if (currentText === void 0) return {
17
+ content: formatJson(projection),
18
+ ok: true
19
+ };
20
+ const parsed = parseJsonRecord(currentText);
21
+ if (!parsed.ok) return parsed;
22
+ return {
23
+ content: ensureTrailingNewline(applyJsonEdit(currentText, ["mcpServers"], projection.mcpServers ?? {})),
24
+ ok: true
25
+ };
26
+ }
27
+ //#endregion
28
+ export { mergeClaudeUserConfig, replaceClaudeUserMcpServers };
@@ -304,7 +304,12 @@ function createCliProgram(options = {}) {
304
304
  const config = loadPipelineConfig(process.env.PIPELINE_TARGET_PATH ?? process.cwd(), { allowMissingLintFileReferences: true });
305
305
  console.log(renderGatewayConfig(config));
306
306
  });
307
- gatewayCommand.command("configure-host").description("Rewrite host MCP config to the singleton pipeline gateway").addOption(new Option$1("--host <host>", "host config to update").choices(["all", "opencode"]).default("all").argParser(parseGatewayHost)).addOption(new Option$1("--scope <scope>", "config scope to update").choices(["project", "global"]).default("project").argParser(parseGatewayHostScope)).action((flags) => {
307
+ gatewayCommand.command("configure-host").description("Rewrite host MCP config to the singleton pipeline gateway").addOption(new Option$1("--host <host>", "host config to update").choices([
308
+ "all",
309
+ "opencode",
310
+ "claude-code",
311
+ "codex"
312
+ ]).default("all").argParser(parseGatewayHost)).addOption(new Option$1("--scope <scope>", "config scope to update").choices(["project", "global"]).default("project").argParser(parseGatewayHostScope)).action((flags) => {
308
313
  const cwd = process.env.PIPELINE_TARGET_PATH ?? process.cwd();
309
314
  const result = configureGatewayHosts(loadPipelineConfig(cwd, { allowMissingLintFileReferences: true }), {
310
315
  cwd,
@@ -343,7 +348,8 @@ function createCliProgram(options = {}) {
343
348
  program.command("install-commands").description("Install generated slash-command adapters into per-machine host dirs (~/.claude, ~/.config/opencode, ~/.codex)").addOption(new Option$1("--host <host>", "host command set to install").choices([
344
349
  "all",
345
350
  "opencode",
346
- "claude-code"
351
+ "claude-code",
352
+ "codex"
347
353
  ]).default("all").argParser(parseCommandHost)).option("--dry-run", "show planned changes without writing files").option("--check", "fail if generated command files are missing or stale").option("--force", "overwrite manually edited command files").action(async (flags) => {
348
354
  const result = await installCommands({
349
355
  ...flags,
@@ -0,0 +1,67 @@
1
+ import { ensureTrailingNewline } from "./json-config-merge.js";
2
+ //#region src/codex-config.ts
3
+ const PIPELINE_GATEWAY_SECTION_HEADERS = ["[mcp_servers.pipeline-gateway]", "[mcp_servers.pipeline-gateway.env_http_headers]"];
4
+ const CODEX_FEATURES_SECTION_HEADER = "[features]";
5
+ const CODEX_HOOKS_FEATURE = "hooks";
6
+ function mergeCodexConfig(currentText, projection) {
7
+ return ensureTrailingNewline([enableCodexHooksFeature(removePipelineGatewaySections(currentText ?? "").trimEnd()).trimEnd(), projection.trimEnd()].filter(Boolean).join("\n\n"));
8
+ }
9
+ function removePipelineGatewaySections(content) {
10
+ return PIPELINE_GATEWAY_SECTION_HEADERS.reduce((nextContent, header) => removeTomlSection(nextContent, header), content);
11
+ }
12
+ function removeTomlSection(content, header) {
13
+ const lines = content.split("\n");
14
+ const kept = [];
15
+ let removing = false;
16
+ for (const line of lines) {
17
+ if (line.trim() === header) {
18
+ removing = true;
19
+ continue;
20
+ }
21
+ if (removing && line.startsWith("[") && line.trimEnd().endsWith("]")) removing = false;
22
+ if (!removing) kept.push(line);
23
+ }
24
+ return kept.join("\n");
25
+ }
26
+ function enableCodexHooksFeature(content) {
27
+ return setTomlFeature(content, CODEX_HOOKS_FEATURE, "true");
28
+ }
29
+ function setTomlFeature(content, key, value) {
30
+ const lines = content.split("\n");
31
+ const sectionStart = lines.findIndex((line) => line.trim() === CODEX_FEATURES_SECTION_HEADER);
32
+ if (sectionStart === -1) return [
33
+ content.trimEnd(),
34
+ CODEX_FEATURES_SECTION_HEADER,
35
+ `${key} = ${value}`
36
+ ].filter(Boolean).join("\n");
37
+ const sectionEnd = nextTomlSectionIndex(lines, sectionStart + 1);
38
+ const beforeSection = lines.slice(0, sectionStart + 1);
39
+ const featureLines = lines.slice(sectionStart + 1, sectionEnd);
40
+ const afterSection = lines.slice(sectionEnd);
41
+ let replaced = false;
42
+ const mergedFeatureLines = featureLines.flatMap((line) => {
43
+ if (!tomlKeyPattern(key).test(line)) return [line];
44
+ if (replaced) return [];
45
+ replaced = true;
46
+ return [`${key} = ${value}`];
47
+ });
48
+ if (!replaced) mergedFeatureLines.push(`${key} = ${value}`);
49
+ return [
50
+ ...beforeSection,
51
+ ...mergedFeatureLines,
52
+ ...afterSection
53
+ ].join("\n");
54
+ }
55
+ function nextTomlSectionIndex(lines, startIndex) {
56
+ const nextIndex = lines.findIndex((line, index) => index >= startIndex && isTomlSectionHeader(line));
57
+ return nextIndex === -1 ? lines.length : nextIndex;
58
+ }
59
+ function isTomlSectionHeader(line) {
60
+ const trimmed = line.trim();
61
+ return trimmed.startsWith("[") && trimmed.endsWith("]");
62
+ }
63
+ function tomlKeyPattern(key) {
64
+ return new RegExp(`^\\s*${key}\\s*=`);
65
+ }
66
+ //#endregion
67
+ export { mergeCodexConfig };
@@ -1,4 +1,3 @@
1
- import { renderClaudeGatewayMcpServers } from "../mcp/gateway.js";
2
1
  import { opencodeAgentName } from "../runtime/opencode-agent-name.js";
3
2
  import { mergeClaudeSettings } from "../claude-settings-config.js";
4
3
  import { CLAUDE_PROJECT_CONFIG_PATH, commandIdForHost, compactLines, entrypointDescription, entrypointEntries, instructionsPointer, invocationForHost } from "./shared.js";
@@ -83,11 +82,9 @@ function agentDefinitions(config) {
83
82
  };
84
83
  });
85
84
  }
86
- function settingsDefinition(config) {
87
- const settings = { permissions: { allow: ["Bash(moka run *)"] } };
88
- if (config.mcp_gateway) settings.mcpServers = renderClaudeGatewayMcpServers(config);
85
+ function settingsDefinition() {
89
86
  return [{
90
- content: `${JSON.stringify(settings, null, 2)}\n`,
87
+ content: `${JSON.stringify({ permissions: { allow: ["Bash(moka run *)"] } }, null, 2)}\n`,
91
88
  host: CLAUDE_CODE_HOST,
92
89
  invocation: invocationForHost(CLAUDE_CODE_HOST),
93
90
  path: CLAUDE_PROJECT_CONFIG_PATH
@@ -97,7 +94,7 @@ function claudeCodeDefinitions(config, cwd) {
97
94
  return [
98
95
  ...commandDefinitions(config),
99
96
  ...agentDefinitions(config),
100
- ...settingsDefinition(config),
97
+ ...settingsDefinition(),
101
98
  projectAgentsMdDefinition(cwd, CLAUDE_CODE_HOST)
102
99
  ];
103
100
  }
@@ -1,4 +1,4 @@
1
- import { join } from "node:path";
1
+ import { dirname, join } from "node:path";
2
2
  import { homedir } from "node:os";
3
3
  //#region src/install-commands/shared.ts
4
4
  const GENERATED_MARKER = "<!-- Generated by @oisincoveney/pipeline. -->";
@@ -12,12 +12,19 @@ const AGENTS_MD_END = "<!-- @oisincoveney/pipeline:agents:end -->";
12
12
  const SINGLE_OPENCODE_PLUGIN_ARRAY_RE = /\n {2}"plugin": \[\n {4}("[^"]+")\n {2}\]/;
13
13
  const OPENCODE_PROJECT_CONFIG_PATH = ".opencode/opencode.json";
14
14
  const CLAUDE_PROJECT_CONFIG_PATH = ".claude/settings.json";
15
+ const CLAUDE_USER_CONFIG_PATH = ".claude.json";
16
+ const CODEX_CONFIG_PATH = ".codex/config.toml";
15
17
  const OPENCODE_COMMAND_PREFIX = "moka-";
16
18
  const ENTRYPOINT_PATH_PATTERNS = {
17
19
  opencode: [/^\.opencode\/commands\/(?:moka-)?([^/]+)\.md$/],
18
20
  "claude-code": [/^\.claude\/commands\/(?:moka-)?([^/]+)\.md$/]
19
21
  };
20
22
  const COMMAND_HOSTS = ["opencode", "claude-code"];
23
+ const INSTALL_HOSTS = [
24
+ "opencode",
25
+ "claude-code",
26
+ "codex"
27
+ ];
21
28
  /**
22
29
  * The per-machine host config dirs. Each honors the same env var the host tool
23
30
  * itself uses to relocate its config (so containers/CI can redirect, and so
@@ -51,6 +58,7 @@ function stripPrefix(value, prefix) {
51
58
  */
52
59
  function resolveHarnessTarget(relPath) {
53
60
  const normalized = relPath.replaceAll("\\", "/");
61
+ if (normalized === ".claude.json") return join(dirname(claudeGlobalConfigDir()), CLAUDE_USER_CONFIG_PATH);
54
62
  if (normalized === ".claude" || normalized.startsWith(".claude/")) return join(claudeGlobalConfigDir(), stripPrefix(normalized, ".claude"));
55
63
  if (normalized === ".codex" || normalized.startsWith(".codex/")) return join(codexGlobalConfigDir(), stripPrefix(normalized, ".codex"));
56
64
  if (normalized === ".gemini" || normalized.startsWith(".gemini/")) return join(geminiGlobalConfigDir(), stripPrefix(normalized, ".gemini"));
@@ -87,4 +95,4 @@ function commandIdForHost(host, entrypointId) {
87
95
  return entrypointId;
88
96
  }
89
97
  //#endregion
90
- export { AGENTS_MD_END, AGENTS_MD_START, CLAUDE_PROJECT_CONFIG_PATH, COMMAND_HOSTS, ENTRYPOINT_PATH_PATTERNS, GENERATED_MARKER, GENERATED_TS_MARKER, GENERATED_YAML_MARKER, OPENCODE_PROJECT_CONFIG_PATH, OWNER_MARKER_PREFIX, OWNER_TS_MARKER_PREFIX, OWNER_YAML_MARKER_PREFIX, SINGLE_OPENCODE_PLUGIN_ARRAY_RE, commandIdForHost, compactLines, entrypointDescription, entrypointEntries, instructionsPointer, invocationForHost, profileEntries, resolveHarnessTarget };
98
+ export { AGENTS_MD_END, AGENTS_MD_START, CLAUDE_PROJECT_CONFIG_PATH, CLAUDE_USER_CONFIG_PATH, CODEX_CONFIG_PATH, COMMAND_HOSTS, ENTRYPOINT_PATH_PATTERNS, GENERATED_MARKER, GENERATED_TS_MARKER, GENERATED_YAML_MARKER, INSTALL_HOSTS, OPENCODE_PROJECT_CONFIG_PATH, OWNER_MARKER_PREFIX, OWNER_TS_MARKER_PREFIX, OWNER_YAML_MARKER_PREFIX, SINGLE_OPENCODE_PLUGIN_ARRAY_RE, commandIdForHost, compactLines, entrypointDescription, entrypointEntries, instructionsPointer, invocationForHost, profileEntries, resolveHarnessTarget };
@@ -1,6 +1,10 @@
1
1
  import { loadPipelineConfig } from "./config/load.js";
2
2
  import "./config.js";
3
- import { COMMAND_HOSTS, ENTRYPOINT_PATH_PATTERNS, invocationForHost, resolveHarnessTarget } from "./install-commands/shared.js";
3
+ import { isRecord } from "./json-config-merge.js";
4
+ import { mergeClaudeUserConfig } from "./claude-user-config.js";
5
+ import { mergeCodexConfig } from "./codex-config.js";
6
+ import { renderClaudeGatewayUserConfig, renderCodexGatewayConfig } from "./mcp/gateway.js";
7
+ import { CLAUDE_USER_CONFIG_PATH, CODEX_CONFIG_PATH, COMMAND_HOSTS, ENTRYPOINT_PATH_PATTERNS, INSTALL_HOSTS, invocationForHost, resolveHarnessTarget } from "./install-commands/shared.js";
4
8
  import { opencodeAdapter } from "./install-commands/opencode.js";
5
9
  import { claudeCodeAdapter } from "./install-commands/claude-code.js";
6
10
  import { existsSync, readFileSync, statSync } from "node:fs";
@@ -12,7 +16,7 @@ const ADAPTERS = {
12
16
  "claude-code": claudeCodeAdapter
13
17
  };
14
18
  function definitionsFor(host, config, cwd) {
15
- return dedupeDefinitionsByPath((host === "all" ? COMMAND_HOSTS : [host]).flatMap((name) => ADAPTERS[name].definitions(config, cwd)));
19
+ return dedupeDefinitionsByPath([...selectedCommandHosts(host).flatMap((name) => ADAPTERS[name].definitions(config, cwd)), ...gatewayHostConfigDefinitions(host, config)]);
16
20
  }
17
21
  function dedupeDefinitionsByPath(definitions) {
18
22
  const lastIndexes = /* @__PURE__ */ new Map();
@@ -21,8 +25,17 @@ function dedupeDefinitionsByPath(definitions) {
21
25
  });
22
26
  return definitions.filter((definition, index) => lastIndexes.get(definition.path) === index);
23
27
  }
24
- function selectedHosts(host) {
25
- return host === "all" ? [...COMMAND_HOSTS] : [host];
28
+ function selectedInstallHosts(host) {
29
+ return host === "all" ? [...INSTALL_HOSTS] : [host];
30
+ }
31
+ function isActiveCommandHost(host) {
32
+ return host === "opencode" || host === "claude-code";
33
+ }
34
+ function isInstallHost(host) {
35
+ return INSTALL_HOSTS.some((candidate) => candidate === host);
36
+ }
37
+ function selectedCommandHosts(host) {
38
+ return selectedInstallHosts(host).filter(isActiveCommandHost);
26
39
  }
27
40
  function resourceRootsFor(host) {
28
41
  return ADAPTERS[host].resourceRoots;
@@ -41,8 +54,8 @@ function generatedHostFor(content) {
41
54
  return COMMAND_HOSTS.find((host) => content.includes(`<!-- @oisincoveney/pipeline:host=${host} -->`) || content.includes(`// @oisincoveney/pipeline:host=${host}`) || content.includes(`# @oisincoveney/pipeline:host=${host}`));
42
55
  }
43
56
  async function obsoleteGeneratedItems(host, wantedPaths) {
44
- const hosts = new Set(selectedHosts(host));
45
- const roots = selectedHosts(host).flatMap((selectedHost) => resourceRootsFor(selectedHost));
57
+ const hosts = new Set(selectedCommandHosts(host));
58
+ const roots = selectedCommandHosts(host).flatMap((selectedHost) => resourceRootsFor(selectedHost));
46
59
  return (await Promise.all(roots.map(async (root) => {
47
60
  const absRoot = resolveHarnessTarget(root);
48
61
  return (await listFiles(absRoot)).map((absolutePath) => ({
@@ -73,14 +86,43 @@ function entrypointIdFromGeneratedPath(host, path) {
73
86
  if (match) return match[1];
74
87
  }
75
88
  }
89
+ const CONFIG_MERGES = {
90
+ [CLAUDE_USER_CONFIG_PATH]: (existingContent, projectionContent) => {
91
+ const projection = JSON.parse(projectionContent);
92
+ if (!isRecord(projection)) return {
93
+ conflict: true,
94
+ content: projectionContent
95
+ };
96
+ const merged = mergeClaudeUserConfig(existingContent, projection);
97
+ return merged.ok ? {
98
+ conflict: false,
99
+ content: merged.content
100
+ } : {
101
+ conflict: true,
102
+ content: projectionContent
103
+ };
104
+ },
105
+ [CODEX_CONFIG_PATH]: (existingContent, projectionContent) => ({
106
+ conflict: false,
107
+ content: mergeCodexConfig(existingContent, projectionContent)
108
+ })
109
+ };
76
110
  function resolveDefinitionContent(definition, target) {
77
- const adapter = ADAPTERS[definition.host];
111
+ const configMerge = CONFIG_MERGES[definition.path];
112
+ if (configMerge) return configMerge(existsSync(target) ? readFileSync(target, "utf8") : void 0, definition.content);
113
+ const adapter = adapterForDefinition(definition);
78
114
  if (!(adapter?.mergeDefinition && existsSync(target))) return {
79
115
  conflict: false,
80
116
  content: definition.content
81
117
  };
82
118
  return applyMergeDefinition(adapter.mergeDefinition.bind(adapter), definition, target);
83
119
  }
120
+ function adapterForDefinition(definition) {
121
+ return isActiveDefinitionHost(definition.host) ? ADAPTERS[definition.host] : void 0;
122
+ }
123
+ function isActiveDefinitionHost(host) {
124
+ return host === "opencode" || host === "claude-code";
125
+ }
84
126
  function applyMergeDefinition(merge, definition, target) {
85
127
  const merged = merge(definition, readFileSync(target, "utf8"));
86
128
  if (!merged) return {
@@ -96,6 +138,27 @@ function applyMergeDefinition(merge, definition, target) {
96
138
  content: merged.content
97
139
  };
98
140
  }
141
+ function gatewayHostConfigDefinitions(host, config) {
142
+ if (!config.mcp_gateway) return [];
143
+ return selectedInstallHosts(host).flatMap(gatewayHostConfigDefinition(config));
144
+ }
145
+ function gatewayHostConfigDefinition(config) {
146
+ return (host) => {
147
+ if (host === "claude-code") return [{
148
+ content: renderClaudeGatewayUserConfig(config),
149
+ host,
150
+ invocation: invocationForHost(host),
151
+ path: CLAUDE_USER_CONFIG_PATH
152
+ }];
153
+ if (host === "codex") return [{
154
+ content: renderCodexGatewayConfig(config),
155
+ host,
156
+ invocation: "codex",
157
+ path: CODEX_CONFIG_PATH
158
+ }];
159
+ return [];
160
+ };
161
+ }
99
162
  function actionFor(path, content, force, block) {
100
163
  if (!existsSync(path)) return "create";
101
164
  const current = readFileSync(path, "utf8");
@@ -120,7 +183,8 @@ function upsertGeneratedBlock(current, content, block) {
120
183
  return `${current.trimEnd()}${separator}${content}`;
121
184
  }
122
185
  function adapterForcesDefinition(definition) {
123
- const fn = ADAPTERS[definition.host]?.isAlwaysForced;
186
+ if (definition.path in CONFIG_MERGES) return true;
187
+ const fn = adapterForDefinition(definition)?.isAlwaysForced;
124
188
  return fn ? fn(definition) : false;
125
189
  }
126
190
  function installActionForDefinition(definition, target, resolved, force) {
@@ -212,8 +276,9 @@ async function installCommands(options = {}) {
212
276
  }
213
277
  function parseCommandHost(value) {
214
278
  const host = value ?? "all";
215
- if (host === "all" || COMMAND_HOSTS.includes(host)) return host;
216
- throw new Error(`Unsupported host "${host}". Supported values: all, ${COMMAND_HOSTS.join(", ")}.`);
279
+ if (host === "all") return host;
280
+ if (isInstallHost(host)) return host;
281
+ throw new Error(`Unsupported host "${host}". Supported values: all, ${INSTALL_HOSTS.join(", ")}.`);
217
282
  }
218
283
  function formatInstallCommandsResult(result) {
219
284
  return result.items.map((item) => `${item.action} ${item.host}: ${item.path} (${item.invocation})`).join("\n");
@@ -125,11 +125,7 @@ function isRecord(value) {
125
125
  function normalizeManifest(value) {
126
126
  const files = {};
127
127
  const manifestFiles = isRecord(value) ? value.files : void 0;
128
- if (!isRecord(manifestFiles)) return {
129
- files,
130
- repository: DEFAULT_HOOK_INSTALL_SOURCE,
131
- version: 1
132
- };
128
+ if (!isRecord(manifestFiles)) return emptyManifest();
133
129
  for (const [path, entry] of Object.entries(manifestFiles)) if (isRecord(entry) && typeof entry.hash === "string") files[path] = { hash: entry.hash };
134
130
  return {
135
131
  files,
@@ -1,20 +1,16 @@
1
1
  import { execa } from "execa";
2
- import { dirname, join } from "node:path";
2
+ import { join } from "node:path";
3
3
  import { homedir, tmpdir } from "node:os";
4
- import { fileURLToPath } from "node:url";
5
4
  import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
6
5
  //#region src/install-rules.ts
7
6
  const DEFAULT_RULES_INSTALL_SOURCE = "oisin-ee/rules";
7
+ const RULESYNC_PACKAGE = "rulesync@8.30.1";
8
8
  const RULESYNC_TARGETS = [
9
9
  "claudecode",
10
10
  "codexcli",
11
11
  "geminicli",
12
12
  "opencode"
13
13
  ];
14
- function packageRoot() {
15
- return join(dirname(fileURLToPath(import.meta.url)), "..");
16
- }
17
- const PACKAGE_ROOT = packageRoot();
18
14
  async function cloneRulesRepository(targetDir) {
19
15
  await execa("gh", [
20
16
  "repo",
@@ -41,11 +37,13 @@ async function withRulesSource(sourceOverride, useSource) {
41
37
  }
42
38
  async function defaultRulesyncRunner(args, opts) {
43
39
  try {
44
- await execa("rulesync", args, {
40
+ await execa("npx", [
41
+ "--yes",
42
+ RULESYNC_PACKAGE,
43
+ ...args
44
+ ], {
45
45
  cwd: opts.cwd,
46
46
  env: opts.env,
47
- localDir: PACKAGE_ROOT,
48
- preferLocal: true,
49
47
  stdio: "inherit"
50
48
  });
51
49
  } catch (error) {
@@ -1,3 +1,5 @@
1
+ import { replaceClaudeUserMcpServers } from "../claude-user-config.js";
2
+ import { mergeCodexConfig } from "../codex-config.js";
1
3
  import { PipelineMcpGatewayError } from "./gateway-error.js";
2
4
  import { McpGatewayService, McpGatewayServiceLive } from "../runtime/services/mcp-gateway-service.js";
3
5
  import { resolveRepoLocalBackendSpecs } from "./repo-local-backends.js";
@@ -68,15 +70,31 @@ function renderOpenCodeGatewayConfig(config) {
68
70
  function renderClaudeGatewayMcpServers(config) {
69
71
  const gateway = configuredGateway(config);
70
72
  return { [PIPELINE_GATEWAY_SERVER_ID]: {
71
- headers: gatewayOpenCodeHeaders(gateway),
73
+ headers: gatewayClaudeHeaders(gateway),
72
74
  type: "http",
73
75
  url: gatewayUrl(gateway)
74
76
  } };
75
77
  }
78
+ function renderClaudeGatewayUserConfig(config) {
79
+ return `${JSON.stringify({ mcpServers: renderClaudeGatewayMcpServers(config) }, null, 2)}\n`;
80
+ }
81
+ function renderCodexGatewayConfig(config) {
82
+ const gateway = configuredGateway(config);
83
+ return [
84
+ `[mcp_servers.${PIPELINE_GATEWAY_SERVER_ID}]`,
85
+ `url = ${tomlString(gatewayUrl(gateway))}`,
86
+ "",
87
+ `[mcp_servers.${PIPELINE_GATEWAY_SERVER_ID}.env_http_headers]`,
88
+ `Authorization = ${tomlString(gateway.authorization_env)}`,
89
+ ""
90
+ ].join("\n");
91
+ }
76
92
  function configureGatewayHosts(config, options) {
77
93
  return selectedGatewayHosts(options.host).map((host) => {
78
- const path = gatewayHostConfigPath(options.scope, options.cwd);
79
- const content = renderOpenCodeGatewayConfig(config);
94
+ const adapter = GATEWAY_HOST_CONFIGS[host];
95
+ const path = adapter.path(options.scope, options.cwd);
96
+ const current = existsSync(path) ? readFileSync(path, "utf8") : void 0;
97
+ const content = adapter.configureContent(config, current);
80
98
  const backupPath = backupIfExists(path);
81
99
  mkdirSync(dirname(path), { recursive: true });
82
100
  writeFileSync(path, content);
@@ -211,12 +229,42 @@ function callGatewayRpc(gateway, url, body) {
211
229
  });
212
230
  }
213
231
  function selectedGatewayHosts(host) {
214
- return host === "all" ? ["opencode"] : [host];
215
- }
216
- function gatewayHostConfigPath(scope, cwd) {
232
+ return host === "all" ? [
233
+ "opencode",
234
+ "claude-code",
235
+ "codex"
236
+ ] : [host];
237
+ }
238
+ const GATEWAY_HOST_CONFIGS = {
239
+ "claude-code": {
240
+ configureContent: (config, current) => {
241
+ const merged = replaceClaudeUserMcpServers(current, { mcpServers: renderClaudeGatewayMcpServers(config) });
242
+ if (!merged.ok) throw new PipelineMcpGatewayError("Cannot parse Claude Code user config.");
243
+ return merged.content;
244
+ },
245
+ path: claudeGatewayConfigPath
246
+ },
247
+ codex: {
248
+ configureContent: (config, current) => mergeCodexConfig(current, renderCodexGatewayConfig(config)),
249
+ path: codexGatewayConfigPath
250
+ },
251
+ opencode: {
252
+ configureContent: (config) => renderOpenCodeGatewayConfig(config),
253
+ path: opencodeGatewayConfigPath
254
+ }
255
+ };
256
+ function opencodeGatewayConfigPath(scope, cwd) {
217
257
  if (scope === "project") return join(cwd, ".opencode", "opencode.json");
218
258
  return join(process.env.OPENCODE_CONFIG_DIR ?? join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "opencode"), "opencode.json");
219
259
  }
260
+ function claudeGatewayConfigPath(scope, cwd) {
261
+ if (scope === "project") return join(cwd, ".mcp.json");
262
+ return join(dirname(process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude")), ".claude.json");
263
+ }
264
+ function codexGatewayConfigPath(scope, cwd) {
265
+ if (scope === "project") return join(cwd, ".codex", "config.toml");
266
+ return join(process.env.CODEX_HOME ?? join(homedir(), ".codex"), "config.toml");
267
+ }
220
268
  function backupIfExists(path) {
221
269
  if (!existsSync(path)) return;
222
270
  const backupPath = `${path}.bak-${Date.now()}`;
@@ -255,6 +303,12 @@ function gatewayAuthorizationHeader(gateway) {
255
303
  function gatewayOpenCodeHeaders(gateway) {
256
304
  return { Authorization: gatewayAuthorizationHeader(gateway) };
257
305
  }
306
+ function gatewayClaudeHeaders(gateway) {
307
+ return { Authorization: `\${${gateway.authorization_env}}` };
308
+ }
309
+ function tomlString(value) {
310
+ return JSON.stringify(value);
311
+ }
258
312
  function checkThv(cwd) {
259
313
  return Effect.gen(function* () {
260
314
  yield* (yield* McpGatewayService).runToolHiveVersion(cwd);
@@ -329,4 +383,4 @@ function legacyContentHit(cwd, path, pattern) {
329
383
  return pattern.test(readFileSync(fullPath, "utf8")) ? path : void 0;
330
384
  }
331
385
  //#endregion
332
- export { configureGatewayHosts, gatewayServerForProfile, localGatewayStatus, reconcileGateway, renderClaudeGatewayMcpServers, renderGatewayConfig, renderOpenCodeGatewayConfig, runGatewayDoctor, startLocalGateway };
386
+ export { configureGatewayHosts, gatewayServerForProfile, localGatewayStatus, reconcileGateway, renderClaudeGatewayUserConfig, renderCodexGatewayConfig, renderGatewayConfig, renderOpenCodeGatewayConfig, runGatewayDoctor, startLocalGateway };
package/package.json CHANGED
@@ -127,7 +127,7 @@
127
127
  "prepack": "bun run build:cli"
128
128
  },
129
129
  "type": "module",
130
- "version": "3.11.7",
130
+ "version": "3.11.9",
131
131
  "description": "Config-driven multi-agent pipeline runner for repository work",
132
132
  "main": "./dist/index.js",
133
133
  "types": "./dist/index.d.ts",