@cubis/foundry 0.3.20 → 0.3.22

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.
Files changed (3) hide show
  1. package/README.md +40 -14
  2. package/bin/cubis.js +759 -108
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -26,8 +26,9 @@ Compatibility binaries are still shipped for migration:
26
26
  # 1) Install CLI
27
27
  npm install -g @cubis/foundry
28
28
 
29
- # 2) Set Postman key once (recommended: env mode)
29
+ # 2) Set API keys once (recommended: env mode)
30
30
  export POSTMAN_API_KEY="<your-postman-api-key>"
31
+ export STITCH_API_KEY="<your-stitch-api-key>" # Antigravity StitchMCP only
31
32
 
32
33
  # 3) Install workflow bundle for your platform
33
34
  cbx workflows install --platform codex --bundle agent-environment-setup --postman --yes
@@ -54,22 +55,50 @@ cbx workflows install --platform antigravity --terminal-integration --terminal-v
54
55
  cbx workflows install --platform codex --postman
55
56
  cbx workflows install --platform codex --postman --postman-workspace-id null
56
57
  cbx workflows install --platform codex --postman --postman-api-key "<key>"
58
+ cbx workflows install --platform codex --postman --mcp-scope global
59
+ cbx workflows install --platform copilot --postman --mcp-scope project
57
60
  cbx workflows install --platform antigravity --postman
61
+ cbx workflows install --platform antigravity --postman --stitch-api-key "<key>"
58
62
  cbx workflows install --platform copilot --postman
59
63
  ```
60
64
 
61
65
  Install bootstrap behavior:
62
66
  - `cbx workflows install` now also bootstraps `ENGINEERING_RULES.md` and `TECH.md` (creates when missing; keeps existing files unless explicitly regenerated).
63
- - When install scope is `global`, workflow/skill/agent artifacts install to global paths, but rule sync + engineering artifacts are maintained in workspace (`project`) scope.
64
- - Optional `--postman` bootstrap creates `postman_setting.json` and installs/configures the Postman skill/MCP for Codex, Antigravity, and Copilot.
67
+ - When install scope is `global` (default), skills/powers install to global paths, while workflows + agents stay in workspace (`project`) paths.
68
+ - Rule sync + engineering artifacts (`AGENTS.md`/`GEMINI.md`/Copilot instructions, `ENGINEERING_RULES.md`, `TECH.md`) are maintained in workspace (`project`) scope.
69
+ - Codex workflow templates are maintained in workspace `.agents/workflows` so workflow-wrapper routing remains discoverable in project rules.
70
+ - Optional `--postman` bootstrap creates `cbx_config.json`, stores managed MCP definitions in `.cbx/mcp/`, and installs/configures Postman MCP for Codex, Antigravity, and Copilot.
71
+ - For Antigravity only, `--postman` also installs a default `StitchMCP` entry (`stitch.googleapis.com/mcp`) using key from `--stitch-api-key` or `STITCH_API_KEY`.
72
+ - Use `--mcp-scope <project|workspace|global|user>` to choose where MCP runtime config is installed (interactive installs prompt for this when not provided).
65
73
  - Use `cbx rules init --platform <platform> --overwrite` to force-regenerate both files.
66
74
 
67
- Postman setup behavior:
68
- - `postman_setting.json` is generated in project root (or `~/.cbx/postman_setting.json` with `--scope global`).
75
+ Postman + Antigravity Stitch setup behavior:
76
+ - `cbx_config.json` is generated in workspace root (project MCP scope) or `~/.cbx/cbx_config.json` (global MCP scope).
77
+ - Managed MCP definition files are generated under `.cbx/mcp/<platform>/postman.json` (workspace scope) or `~/.cbx/mcp/<platform>/postman.json` (global scope).
69
78
  - Env-first auth is supported: when `POSTMAN_API_KEY` is set, generated settings keep `apiKey: null` and MCP config uses `Bearer ${POSTMAN_API_KEY}`.
70
79
  - Inline auth is supported with `--postman-api-key <key>`.
71
80
  - `--postman-workspace-id null` writes JSON `null` for `defaultWorkspaceId`.
72
- - In project scope, `postman_setting.json` is auto-added to `.gitignore` (no duplicate entries).
81
+ - Antigravity gets an additional managed file at `.cbx/mcp/antigravity/stitch.json` (or `~/.cbx/mcp/antigravity/stitch.json` in global MCP scope).
82
+ - Stitch key source priority for Antigravity: `--stitch-api-key` then `STITCH_API_KEY`; when unset, generated config keeps placeholder `X-Goog-Api-Key: ur stitch key`.
83
+ - In project MCP scope, `cbx_config.json` and `.cbx/mcp/` are auto-added to `.gitignore` (no duplicate entries).
84
+
85
+ Platform runtime MCP placement:
86
+ - Codex:
87
+ - Global MCP scope: `~/.codex/config.toml` via `codex mcp add`.
88
+ - Workspace MCP scope: `.vscode/mcp.json`.
89
+ - Antigravity (Gemini CLI):
90
+ - Global MCP scope: `~/.gemini/settings.json` (`mcpServers`, includes `postman` + default `StitchMCP`).
91
+ - Workspace MCP scope: `.gemini/settings.json` (`mcpServers`, includes `postman` + default `StitchMCP`).
92
+ - Copilot:
93
+ - Workspace MCP scope: `.vscode/mcp.json`.
94
+ - Global MCP scope: `~/.copilot/mcp-config.json`.
95
+
96
+ API key docs:
97
+ - Google Stitch MCP setup docs: [stitch.withgoogle.com/docs/mcp/setup](https://stitch.withgoogle.com/docs/mcp/setup)
98
+ - Google Stitch settings (create API key): [stitch.withgoogle.com/settings](https://stitch.withgoogle.com/settings)
99
+ - Google Cloud API key guidance: [Authenticate to Google and Google Cloud MCP servers](https://docs.cloud.google.com/mcp/authenticate-mcp)
100
+ - Postman API key creation: [Generate and use Postman API keys](https://learning.postman.com/docs/developer/postman-api/authentication/)
101
+ - Postman MCP setup: [Set up the Postman MCP Server](https://learning.postman.com/docs/developer/postman-api/postman-mcp-server/set-up-postman-mcp-server)
73
102
 
74
103
  `rules` manages strict engineering policy and a generated codebase tech map:
75
104
 
@@ -177,11 +206,9 @@ Project scope:
177
206
  - Terminal integration (optional): `.agent/terminal-integration`
178
207
 
179
208
  Global scope:
180
- - Workflows: `~/.gemini/antigravity/workflows`
181
- - Agents: `~/.gemini/antigravity/agents`
182
209
  - Skills: `~/.gemini/antigravity/skills`
183
210
  - Rules: `~/.gemini/GEMINI.md`
184
- - Terminal integration (optional): `~/.gemini/antigravity/terminal-integration`
211
+ - Workflows/agents/terminal-integration: default install keeps these in workspace (`.agent/...`) paths.
185
212
 
186
213
  ### Antigravity Terminal Integration (Optional)
187
214
 
@@ -191,7 +218,7 @@ Install-time options:
191
218
 
192
219
  Behavior:
193
220
  - Interactive installs prompt whether to enable terminal verification integration.
194
- - If enabled, cbx writes managed scripts/config under `.agent/terminal-integration` (or global equivalent).
221
+ - If enabled, cbx writes managed scripts/config under `.agent/terminal-integration`.
195
222
  - cbx also writes a managed terminal verification block into Antigravity rule files so post-task verification commands are explicit.
196
223
  - Removing the bundle cleans the managed terminal integration directory and block.
197
224
 
@@ -208,9 +235,9 @@ Project scope:
208
235
  - Example usage: `$workflow-plan`, `$agent-backend-specialist`
209
236
 
210
237
  Global scope:
211
- - Workflow templates (reference docs): `~/.agents/workflows`
212
238
  - Skills: `~/.agents/skills`
213
239
  - Rules: `~/.codex/AGENTS.md`
240
+ - Workflow templates (reference docs): default install keeps these in workspace `.agents/workflows`
214
241
  - Agents: not installed for Codex runtime
215
242
 
216
243
  Legacy compatibility note:
@@ -226,10 +253,9 @@ Project scope:
226
253
  - Skill schema note: `cbx` normalizes Copilot skill frontmatter by removing unsupported top-level keys like `displayName` and `keywords` during install.
227
254
 
228
255
  Global scope:
229
- - Workflows: `~/.copilot/workflows`
230
- - Agents: `~/.copilot/agents`
231
256
  - Skills: `~/.copilot/skills`
232
257
  - Rules: `~/.copilot/copilot-instructions.md`
258
+ - Workflows/agents: default install keeps these in workspace (`.github/...`) paths.
233
259
 
234
260
  ## Rule Auto-Sync
235
261
 
@@ -261,7 +287,7 @@ Default scope:
261
287
  - `cbx install` (legacy alias)
262
288
  - `cbx init` (legacy alias)
263
289
  - Default scope for these commands is `global`.
264
- - Rule files (`AGENTS.md`/`GEMINI.md`/Copilot instructions) and engineering files (`ENGINEERING_RULES.md`, `TECH.md`) are still updated in workspace (`project`) scope during install.
290
+ - In this default global mode, only skills/powers install globally. Workflows/agents and rule/engineering files remain workspace-scoped.
265
291
  - Other workflow/rules commands default to `project`.
266
292
 
267
293
  Optional:
package/bin/cubis.js CHANGED
@@ -13,13 +13,16 @@ import {
13
13
  writeFile
14
14
  } from "node:fs/promises";
15
15
  import { createRequire } from "node:module";
16
+ import { execFile as execFileCallback } from "node:child_process";
16
17
  import os from "node:os";
17
18
  import path from "node:path";
18
19
  import process from "node:process";
20
+ import { promisify } from "node:util";
19
21
  import { fileURLToPath } from "node:url";
20
22
 
21
23
  const require = createRequire(import.meta.url);
22
24
  const { version: CLI_VERSION } = require("../package.json");
25
+ const execFile = promisify(execFileCallback);
23
26
 
24
27
  const MANAGED_BLOCK_START_RE = /<!--\s*cbx:workflows:auto:start[^>]*-->/g;
25
28
  const MANAGED_BLOCK_END_RE = /<!--\s*cbx:workflows:auto:end\s*-->/g;
@@ -125,9 +128,16 @@ const DEFAULT_TERMINAL_VERIFIER = "codex";
125
128
  const POSTMAN_API_KEY_ENV_VAR = "POSTMAN_API_KEY";
126
129
  const POSTMAN_MCP_URL = "https://mcp.postman.com/minimal";
127
130
  const POSTMAN_SKILL_ID = "postman";
131
+ const STITCH_MCP_SERVER_ID = "StitchMCP";
132
+ const STITCH_API_KEY_ENV_VAR = "STITCH_API_KEY";
133
+ const STITCH_MCP_URL = "https://stitch.googleapis.com/mcp";
134
+ const STITCH_API_KEY_PLACEHOLDER = "ur stitch key";
135
+ const CBX_CONFIG_FILENAME = "cbx_config.json";
128
136
  const POSTMAN_SETTINGS_FILENAME = "postman_setting.json";
129
137
  const POSTMAN_API_KEY_MISSING_WARNING =
130
- `Postman API key is not configured. Set ${POSTMAN_API_KEY_ENV_VAR} or update ${POSTMAN_SETTINGS_FILENAME}.`;
138
+ `Postman API key is not configured. Set ${POSTMAN_API_KEY_ENV_VAR} or update ${CBX_CONFIG_FILENAME}.`;
139
+ const STITCH_API_KEY_MISSING_WARNING =
140
+ `Google Stitch API key is not configured. Set ${STITCH_API_KEY_ENV_VAR} or update ${CBX_CONFIG_FILENAME}.`;
131
141
  const TECH_SCAN_MAX_FILES = 5000;
132
142
  const TECH_SCAN_IGNORED_DIRS = new Set([
133
143
  ".git",
@@ -279,6 +289,14 @@ function normalizeScope(value) {
279
289
  throw new Error(`Unknown scope '${value}'. Use --scope project or --scope global.`);
280
290
  }
281
291
 
292
+ function normalizeMcpScope(value, fallback = "project") {
293
+ if (!value) return fallback;
294
+ const normalized = value.trim().toLowerCase();
295
+ if (normalized === "project" || normalized === "workspace") return "project";
296
+ if (normalized === "global" || normalized === "user") return "global";
297
+ throw new Error(`Unknown MCP scope '${value}'. Use project/workspace or global/user.`);
298
+ }
299
+
282
300
  function normalizeTerminalVerifier(value) {
283
301
  if (!value) return null;
284
302
  const normalized = value.trim().toLowerCase();
@@ -1295,6 +1313,19 @@ async function resolveProfilePaths(profileId, scope, cwd = process.cwd()) {
1295
1313
  };
1296
1314
  }
1297
1315
 
1316
+ async function resolveArtifactProfilePaths(profileId, scope, cwd = process.cwd()) {
1317
+ const scopedPaths = await resolveProfilePaths(profileId, scope, cwd);
1318
+ if (scope !== "global") return scopedPaths;
1319
+
1320
+ // Global install mode is skills-only. Keep workflows/agents in workspace scope.
1321
+ const workspacePaths = await resolveProfilePaths(profileId, "project", cwd);
1322
+ return {
1323
+ ...scopedPaths,
1324
+ workflowsDir: workspacePaths.workflowsDir,
1325
+ agentsDir: workspacePaths.agentsDir
1326
+ };
1327
+ }
1328
+
1298
1329
  async function listBundleIds() {
1299
1330
  const root = path.join(agentAssetsRoot(), "workflows");
1300
1331
  if (!(await pathExists(root))) return [];
@@ -2241,7 +2272,7 @@ async function writeGeneratedArtifact({ destination, content, dryRun = false })
2241
2272
  return { action: exists ? "replaced" : "installed", path: destination };
2242
2273
  }
2243
2274
 
2244
- function resolvePostmanSettingsPath({ scope, cwd = process.cwd() }) {
2275
+ function resolveLegacyPostmanSettingsPath({ scope, cwd = process.cwd() }) {
2245
2276
  if (scope === "global") {
2246
2277
  return path.join(os.homedir(), ".cbx", POSTMAN_SETTINGS_FILENAME);
2247
2278
  }
@@ -2249,20 +2280,116 @@ function resolvePostmanSettingsPath({ scope, cwd = process.cwd() }) {
2249
2280
  return path.join(workspaceRoot, POSTMAN_SETTINGS_FILENAME);
2250
2281
  }
2251
2282
 
2252
- function buildPostmanMcpConfig({ apiKey = null, mcpUrl = POSTMAN_MCP_URL }) {
2253
- const authHeader = apiKey
2254
- ? `Bearer ${apiKey}`
2255
- : `Bearer \${${POSTMAN_API_KEY_ENV_VAR}}`;
2283
+ function resolveCbxConfigPath({ scope, cwd = process.cwd() }) {
2284
+ if (scope === "global") {
2285
+ return path.join(os.homedir(), ".cbx", CBX_CONFIG_FILENAME);
2286
+ }
2287
+ const workspaceRoot = findWorkspaceRoot(cwd);
2288
+ return path.join(workspaceRoot, CBX_CONFIG_FILENAME);
2289
+ }
2290
+
2291
+ function resolveMcpRootPath({ scope, cwd = process.cwd() }) {
2292
+ if (scope === "global") {
2293
+ return path.join(os.homedir(), ".cbx", "mcp");
2294
+ }
2295
+ const workspaceRoot = findWorkspaceRoot(cwd);
2296
+ return path.join(workspaceRoot, ".cbx", "mcp");
2297
+ }
2298
+
2299
+ function resolvePostmanMcpDefinitionPath({ platform, scope, cwd = process.cwd() }) {
2300
+ return path.join(resolveMcpRootPath({ scope, cwd }), platform, `${POSTMAN_SKILL_ID}.json`);
2301
+ }
2302
+
2303
+ function resolveStitchMcpDefinitionPath({ scope, cwd = process.cwd() }) {
2304
+ return path.join(resolveMcpRootPath({ scope, cwd }), "antigravity", "stitch.json");
2305
+ }
2306
+
2307
+ function buildPostmanAuthHeader({ apiKey = null, apiKeyEnvVar = POSTMAN_API_KEY_ENV_VAR }) {
2308
+ return apiKey ? `Bearer ${apiKey}` : `Bearer \${${apiKeyEnvVar}}`;
2309
+ }
2310
+
2311
+ function buildStitchApiHeader({ apiKey = null }) {
2312
+ return `X-Goog-Api-Key: ${apiKey || STITCH_API_KEY_PLACEHOLDER}`;
2313
+ }
2314
+
2315
+ function buildPostmanMcpDefinition({
2316
+ apiKey = null,
2317
+ apiKeyEnvVar = POSTMAN_API_KEY_ENV_VAR,
2318
+ mcpUrl = POSTMAN_MCP_URL
2319
+ }) {
2256
2320
  return {
2257
- mcpServers: {
2258
- postman: {
2259
- url: mcpUrl,
2260
- headers: {
2261
- Authorization: authHeader
2262
- }
2263
- }
2264
- },
2265
- disabled: false
2321
+ schemaVersion: 1,
2322
+ server: POSTMAN_SKILL_ID,
2323
+ transport: "http",
2324
+ url: mcpUrl,
2325
+ headers: {
2326
+ Authorization: buildPostmanAuthHeader({ apiKey, apiKeyEnvVar })
2327
+ }
2328
+ };
2329
+ }
2330
+
2331
+ function buildStitchMcpDefinition({
2332
+ apiKey = null,
2333
+ mcpUrl = STITCH_MCP_URL
2334
+ }) {
2335
+ return {
2336
+ schemaVersion: 1,
2337
+ server: STITCH_MCP_SERVER_ID,
2338
+ transport: "command",
2339
+ command: "npx",
2340
+ args: [
2341
+ "-y",
2342
+ "mcp-remote",
2343
+ mcpUrl,
2344
+ "--header",
2345
+ buildStitchApiHeader({ apiKey })
2346
+ ],
2347
+ env: {}
2348
+ };
2349
+ }
2350
+
2351
+ function buildVsCodePostmanServer({
2352
+ apiKey = null,
2353
+ apiKeyEnvVar = POSTMAN_API_KEY_ENV_VAR,
2354
+ mcpUrl = POSTMAN_MCP_URL
2355
+ }) {
2356
+ return {
2357
+ type: "sse",
2358
+ url: mcpUrl,
2359
+ headers: {
2360
+ Authorization: buildPostmanAuthHeader({ apiKey, apiKeyEnvVar })
2361
+ }
2362
+ };
2363
+ }
2364
+
2365
+ function buildGeminiPostmanServer({
2366
+ apiKey = null,
2367
+ apiKeyEnvVar = POSTMAN_API_KEY_ENV_VAR,
2368
+ mcpUrl = POSTMAN_MCP_URL
2369
+ }) {
2370
+ return {
2371
+ httpUrl: mcpUrl,
2372
+ headers: {
2373
+ Authorization: buildPostmanAuthHeader({ apiKey, apiKeyEnvVar })
2374
+ }
2375
+ };
2376
+ }
2377
+
2378
+ function buildGeminiStitchServer({
2379
+ apiKey = null,
2380
+ mcpUrl = STITCH_MCP_URL
2381
+ }) {
2382
+ return {
2383
+ $typeName: "exa.cascade_plugins_pb.CascadePluginCommandTemplate",
2384
+ command: "npx",
2385
+ args: [
2386
+ "-y",
2387
+ "mcp-remote",
2388
+ mcpUrl,
2389
+ "--header",
2390
+ buildStitchApiHeader({ apiKey })
2391
+ ],
2392
+ env: {}
2266
2393
  };
2267
2394
  }
2268
2395
 
@@ -2272,12 +2399,297 @@ function getPostmanApiKeySource({ apiKey, envApiKey }) {
2272
2399
  return "unset";
2273
2400
  }
2274
2401
 
2402
+ function getStitchApiKeySource({ apiKey, envApiKey }) {
2403
+ if (apiKey) return "inline";
2404
+ if (envApiKey) return "env";
2405
+ return "unset";
2406
+ }
2407
+
2275
2408
  function normalizePostmanApiKey(value) {
2276
2409
  if (value === undefined || value === null) return null;
2277
2410
  const normalized = String(value).trim();
2278
2411
  return normalized || null;
2279
2412
  }
2280
2413
 
2414
+ function parseStoredPostmanConfig(raw) {
2415
+ if (!raw || typeof raw !== "object") return null;
2416
+ const source = raw.postman && typeof raw.postman === "object" ? raw.postman : raw;
2417
+
2418
+ const apiKey = normalizePostmanApiKey(source.apiKey);
2419
+ const apiKeyEnvVar = String(source.apiKeyEnvVar || POSTMAN_API_KEY_ENV_VAR).trim() || POSTMAN_API_KEY_ENV_VAR;
2420
+ const mcpUrl = String(source.mcpUrl || POSTMAN_MCP_URL).trim() || POSTMAN_MCP_URL;
2421
+ const defaultWorkspaceId = normalizePostmanWorkspaceId(source.defaultWorkspaceId);
2422
+
2423
+ return {
2424
+ apiKey,
2425
+ apiKeyEnvVar,
2426
+ mcpUrl,
2427
+ defaultWorkspaceId
2428
+ };
2429
+ }
2430
+
2431
+ function parseStoredStitchConfig(raw) {
2432
+ if (!raw || typeof raw !== "object") return null;
2433
+ const source = raw.stitch && typeof raw.stitch === "object" ? raw.stitch : null;
2434
+ if (!source) return null;
2435
+
2436
+ const apiKey = normalizePostmanApiKey(source.apiKey);
2437
+ const apiKeyEnvVar = String(source.apiKeyEnvVar || STITCH_API_KEY_ENV_VAR).trim() || STITCH_API_KEY_ENV_VAR;
2438
+ const mcpUrl = String(source.mcpUrl || STITCH_MCP_URL).trim() || STITCH_MCP_URL;
2439
+
2440
+ return {
2441
+ apiKey,
2442
+ apiKeyEnvVar,
2443
+ mcpUrl
2444
+ };
2445
+ }
2446
+
2447
+ async function readJsonFileIfExists(filePath) {
2448
+ if (!(await pathExists(filePath))) return { exists: false, value: null };
2449
+ try {
2450
+ const raw = await readFile(filePath, "utf8");
2451
+ return { exists: true, value: JSON.parse(raw) };
2452
+ } catch (error) {
2453
+ return { exists: true, value: null, error };
2454
+ }
2455
+ }
2456
+
2457
+ async function upsertJsonObjectFile({ targetPath, updater, dryRun = false }) {
2458
+ const exists = await pathExists(targetPath);
2459
+ const warnings = [];
2460
+ let parsed = {};
2461
+
2462
+ if (exists) {
2463
+ try {
2464
+ const raw = await readFile(targetPath, "utf8");
2465
+ const decoded = JSON.parse(raw);
2466
+ if (decoded && typeof decoded === "object" && !Array.isArray(decoded)) {
2467
+ parsed = decoded;
2468
+ } else {
2469
+ warnings.push(`Existing JSON at ${targetPath} was not an object. Resetting structure.`);
2470
+ }
2471
+ } catch {
2472
+ warnings.push(`Existing JSON at ${targetPath} could not be parsed. Resetting structure.`);
2473
+ }
2474
+ }
2475
+
2476
+ const nextValue = updater(parsed);
2477
+ const content = `${JSON.stringify(nextValue, null, 2)}\n`;
2478
+ const writeResult = await writeGeneratedArtifact({
2479
+ destination: targetPath,
2480
+ content,
2481
+ dryRun
2482
+ });
2483
+
2484
+ return {
2485
+ action: writeResult.action,
2486
+ filePath: targetPath,
2487
+ warnings
2488
+ };
2489
+ }
2490
+
2491
+ async function removeGeneratedArtifactIfExists({ targetPath, dryRun = false }) {
2492
+ const exists = await pathExists(targetPath);
2493
+ if (!exists) {
2494
+ return {
2495
+ action: "missing",
2496
+ path: targetPath
2497
+ };
2498
+ }
2499
+
2500
+ if (!dryRun) {
2501
+ await rm(targetPath, { recursive: true, force: true });
2502
+ }
2503
+
2504
+ return {
2505
+ action: dryRun ? "would-remove" : "removed",
2506
+ path: targetPath
2507
+ };
2508
+ }
2509
+
2510
+ async function applyPostmanMcpForPlatform({
2511
+ platform,
2512
+ mcpScope,
2513
+ apiKey,
2514
+ apiKeyEnvVar,
2515
+ mcpUrl,
2516
+ stitchApiKey,
2517
+ stitchMcpUrl,
2518
+ includeStitchMcp = false,
2519
+ dryRun = false,
2520
+ cwd = process.cwd()
2521
+ }) {
2522
+ const workspaceRoot = findWorkspaceRoot(cwd);
2523
+ const warnings = [];
2524
+
2525
+ if (platform === "antigravity") {
2526
+ const settingsPath =
2527
+ mcpScope === "global"
2528
+ ? path.join(os.homedir(), ".gemini", "settings.json")
2529
+ : path.join(workspaceRoot, ".gemini", "settings.json");
2530
+ const result = await upsertJsonObjectFile({
2531
+ targetPath: settingsPath,
2532
+ updater: (existing) => {
2533
+ const next = { ...existing };
2534
+ const mcpServers =
2535
+ next.mcpServers && typeof next.mcpServers === "object" && !Array.isArray(next.mcpServers)
2536
+ ? { ...next.mcpServers }
2537
+ : {};
2538
+ mcpServers[POSTMAN_SKILL_ID] = buildGeminiPostmanServer({
2539
+ apiKey,
2540
+ apiKeyEnvVar,
2541
+ mcpUrl
2542
+ });
2543
+ if (includeStitchMcp) {
2544
+ mcpServers[STITCH_MCP_SERVER_ID] = buildGeminiStitchServer({
2545
+ apiKey: stitchApiKey,
2546
+ mcpUrl: stitchMcpUrl
2547
+ });
2548
+ }
2549
+ next.mcpServers = mcpServers;
2550
+ return next;
2551
+ },
2552
+ dryRun
2553
+ });
2554
+ return {
2555
+ kind: "gemini-settings",
2556
+ scope: mcpScope,
2557
+ path: settingsPath,
2558
+ action: result.action,
2559
+ warnings: [...warnings, ...result.warnings]
2560
+ };
2561
+ }
2562
+
2563
+ if (platform === "copilot") {
2564
+ const configPath =
2565
+ mcpScope === "global"
2566
+ ? path.join(os.homedir(), ".copilot", "mcp-config.json")
2567
+ : path.join(workspaceRoot, ".vscode", "mcp.json");
2568
+ const result = await upsertJsonObjectFile({
2569
+ targetPath: configPath,
2570
+ updater: (existing) => {
2571
+ const next = { ...existing };
2572
+ const servers =
2573
+ next.servers && typeof next.servers === "object" && !Array.isArray(next.servers)
2574
+ ? { ...next.servers }
2575
+ : {};
2576
+ servers[POSTMAN_SKILL_ID] = buildVsCodePostmanServer({
2577
+ apiKey,
2578
+ apiKeyEnvVar,
2579
+ mcpUrl
2580
+ });
2581
+ next.servers = servers;
2582
+ return next;
2583
+ },
2584
+ dryRun
2585
+ });
2586
+ return {
2587
+ kind: mcpScope === "global" ? "copilot-cli-mcp" : "vscode-mcp",
2588
+ scope: mcpScope,
2589
+ path: configPath,
2590
+ action: result.action,
2591
+ warnings: [...warnings, ...result.warnings]
2592
+ };
2593
+ }
2594
+
2595
+ if (platform === "codex") {
2596
+ if (mcpScope === "project") {
2597
+ const vscodePath = path.join(workspaceRoot, ".vscode", "mcp.json");
2598
+ const result = await upsertJsonObjectFile({
2599
+ targetPath: vscodePath,
2600
+ updater: (existing) => {
2601
+ const next = { ...existing };
2602
+ const servers =
2603
+ next.servers && typeof next.servers === "object" && !Array.isArray(next.servers)
2604
+ ? { ...next.servers }
2605
+ : {};
2606
+ servers[POSTMAN_SKILL_ID] = buildVsCodePostmanServer({
2607
+ apiKey,
2608
+ apiKeyEnvVar,
2609
+ mcpUrl
2610
+ });
2611
+ next.servers = servers;
2612
+ return next;
2613
+ },
2614
+ dryRun
2615
+ });
2616
+ return {
2617
+ kind: "vscode-mcp",
2618
+ scope: mcpScope,
2619
+ path: vscodePath,
2620
+ action: result.action,
2621
+ warnings: [...warnings, ...result.warnings]
2622
+ };
2623
+ }
2624
+
2625
+ const codexConfigPath = path.join(os.homedir(), ".codex", "config.toml");
2626
+ if (dryRun) {
2627
+ return {
2628
+ kind: "codex-cli",
2629
+ scope: mcpScope,
2630
+ path: codexConfigPath,
2631
+ action: "would-patch",
2632
+ warnings
2633
+ };
2634
+ }
2635
+
2636
+ try {
2637
+ await execFile("codex", ["mcp", "remove", POSTMAN_SKILL_ID], { cwd });
2638
+ } catch {
2639
+ // Best effort. Add will still run and becomes source of truth.
2640
+ }
2641
+
2642
+ try {
2643
+ await execFile(
2644
+ "codex",
2645
+ [
2646
+ "mcp",
2647
+ "add",
2648
+ POSTMAN_SKILL_ID,
2649
+ "--url",
2650
+ mcpUrl,
2651
+ "--bearer-token-env-var",
2652
+ apiKeyEnvVar || POSTMAN_API_KEY_ENV_VAR
2653
+ ],
2654
+ { cwd }
2655
+ );
2656
+ } catch (error) {
2657
+ warnings.push(
2658
+ `Failed to register Postman MCP via Codex CLI. Ensure 'codex' is installed and rerun. (${error.message})`
2659
+ );
2660
+ return {
2661
+ kind: "codex-cli",
2662
+ scope: mcpScope,
2663
+ path: codexConfigPath,
2664
+ action: "failed",
2665
+ warnings
2666
+ };
2667
+ }
2668
+
2669
+ if (apiKey) {
2670
+ warnings.push(
2671
+ "Codex global MCP uses bearer token env vars. Inline apiKey in cbx_config.json is not directly used by Codex."
2672
+ );
2673
+ }
2674
+
2675
+ return {
2676
+ kind: "codex-cli",
2677
+ scope: mcpScope,
2678
+ path: codexConfigPath,
2679
+ action: "patched",
2680
+ warnings
2681
+ };
2682
+ }
2683
+
2684
+ return {
2685
+ kind: "unknown",
2686
+ scope: mcpScope,
2687
+ path: null,
2688
+ action: "skipped",
2689
+ warnings: [`Unsupported platform '${platform}' for Postman MCP installation.`]
2690
+ };
2691
+ }
2692
+
2281
2693
  async function ensureGitIgnoreEntry({
2282
2694
  filePath,
2283
2695
  entry,
@@ -2310,13 +2722,15 @@ async function ensureGitIgnoreEntry({
2310
2722
  }
2311
2723
 
2312
2724
  async function resolvePostmanInstallSelection({
2725
+ platform,
2313
2726
  scope,
2314
2727
  options,
2315
2728
  cwd = process.cwd()
2316
2729
  }) {
2317
2730
  const hasApiKeyOption = options.postmanApiKey !== undefined;
2318
2731
  const hasWorkspaceOption = options.postmanWorkspaceId !== undefined;
2319
- const enabled = Boolean(options.postman) || hasApiKeyOption || hasWorkspaceOption;
2732
+ const hasStitchApiKeyOption = options.stitchApiKey !== undefined;
2733
+ const enabled = Boolean(options.postman) || hasApiKeyOption || hasWorkspaceOption || hasStitchApiKeyOption;
2320
2734
  if (!enabled) return { enabled: false };
2321
2735
 
2322
2736
  const explicitApiKey = hasApiKeyOption ? String(options.postmanApiKey || "").trim() : "";
@@ -2325,7 +2739,14 @@ async function resolvePostmanInstallSelection({
2325
2739
  let defaultWorkspaceId = hasWorkspaceOption
2326
2740
  ? normalizePostmanWorkspaceId(options.postmanWorkspaceId)
2327
2741
  : null;
2742
+ const requestedMcpScope = options.mcpScope
2743
+ ? normalizeMcpScope(options.mcpScope, normalizeMcpScope(scope, "project"))
2744
+ : null;
2745
+ let mcpScope = requestedMcpScope || normalizeMcpScope(scope, "project");
2328
2746
  const warnings = [];
2747
+ const stitchEnabled = platform === "antigravity";
2748
+ let stitchApiKey = hasStitchApiKeyOption ? normalizePostmanApiKey(options.stitchApiKey) : null;
2749
+ const envStitchApiKey = normalizePostmanApiKey(process.env[STITCH_API_KEY_ENV_VAR]);
2329
2750
 
2330
2751
  const canPrompt = !options.yes && process.stdin.isTTY;
2331
2752
  if (canPrompt && !hasApiKeyOption && !apiKey && !envApiKey) {
@@ -2348,34 +2769,91 @@ async function resolvePostmanInstallSelection({
2348
2769
  defaultWorkspaceId = normalizePostmanWorkspaceId(promptedWorkspaceId);
2349
2770
  }
2350
2771
 
2772
+ if (canPrompt && stitchEnabled && !hasStitchApiKeyOption && !stitchApiKey && !envStitchApiKey) {
2773
+ const promptedStitchApiKey = String(
2774
+ await input({
2775
+ message: `Google Stitch API key (optional, leave blank to keep ${STITCH_API_KEY_ENV_VAR} env mode):`,
2776
+ default: ""
2777
+ })
2778
+ ).trim();
2779
+ if (promptedStitchApiKey) {
2780
+ stitchApiKey = promptedStitchApiKey;
2781
+ }
2782
+ }
2783
+
2784
+ if (canPrompt && !requestedMcpScope) {
2785
+ mcpScope = await select({
2786
+ message: "Install MCP config in workspace or global scope?",
2787
+ choices: [
2788
+ { name: scope === "global" ? "Global (recommended)" : "Global", value: "global" },
2789
+ { name: scope === "project" ? "Workspace (recommended)" : "Workspace", value: "project" }
2790
+ ],
2791
+ default: mcpScope
2792
+ });
2793
+ }
2794
+
2351
2795
  const apiKeySource = getPostmanApiKeySource({ apiKey, envApiKey });
2352
2796
  if (apiKeySource === "unset") {
2353
2797
  warnings.push(POSTMAN_API_KEY_MISSING_WARNING);
2354
2798
  }
2355
2799
 
2356
- const settingsPath = resolvePostmanSettingsPath({ scope, cwd });
2357
- const settings = {
2358
- apiKey: apiKey || null,
2359
- apiKeyEnvVar: POSTMAN_API_KEY_ENV_VAR,
2360
- apiKeySource,
2361
- defaultWorkspaceId: defaultWorkspaceId ?? null,
2362
- mcpUrl: POSTMAN_MCP_URL,
2800
+ const stitchApiKeySource = stitchEnabled
2801
+ ? getStitchApiKeySource({ apiKey: stitchApiKey, envApiKey: envStitchApiKey })
2802
+ : null;
2803
+ if (stitchEnabled && stitchApiKeySource === "unset") {
2804
+ warnings.push(STITCH_API_KEY_MISSING_WARNING);
2805
+ }
2806
+ if (!stitchEnabled && hasStitchApiKeyOption) {
2807
+ warnings.push("--stitch-api-key is only used for --platform antigravity.");
2808
+ }
2809
+
2810
+ const cbxConfigPath = resolveCbxConfigPath({ scope: mcpScope, cwd });
2811
+ const legacySettingsPath = resolveLegacyPostmanSettingsPath({ scope: mcpScope, cwd });
2812
+ const cbxConfig = {
2813
+ schemaVersion: 1,
2363
2814
  generatedBy: "cbx workflows install --postman",
2364
- generatedAt: new Date().toISOString()
2815
+ generatedAt: new Date().toISOString(),
2816
+ mcp: {
2817
+ scope: mcpScope,
2818
+ server: POSTMAN_SKILL_ID,
2819
+ platform
2820
+ },
2821
+ postman: {
2822
+ apiKey: apiKey || null,
2823
+ apiKeyEnvVar: POSTMAN_API_KEY_ENV_VAR,
2824
+ apiKeySource,
2825
+ defaultWorkspaceId: defaultWorkspaceId ?? null,
2826
+ mcpUrl: POSTMAN_MCP_URL
2827
+ }
2365
2828
  };
2829
+ if (stitchEnabled) {
2830
+ cbxConfig.stitch = {
2831
+ server: STITCH_MCP_SERVER_ID,
2832
+ apiKey: stitchApiKey || null,
2833
+ apiKeyEnvVar: STITCH_API_KEY_ENV_VAR,
2834
+ apiKeySource: stitchApiKeySource,
2835
+ mcpUrl: STITCH_MCP_URL
2836
+ };
2837
+ }
2366
2838
 
2367
2839
  return {
2368
2840
  enabled: true,
2369
2841
  apiKey,
2370
2842
  apiKeySource,
2843
+ stitchEnabled,
2844
+ stitchApiKey,
2845
+ stitchApiKeySource,
2371
2846
  defaultWorkspaceId: defaultWorkspaceId ?? null,
2847
+ mcpScope,
2372
2848
  warnings,
2373
- settings,
2374
- settingsPath
2849
+ cbxConfig,
2850
+ cbxConfigPath,
2851
+ legacySettingsPath
2375
2852
  };
2376
2853
  }
2377
2854
 
2378
2855
  async function configurePostmanInstallArtifacts({
2856
+ platform,
2379
2857
  scope,
2380
2858
  profilePaths,
2381
2859
  postmanSelection,
@@ -2385,34 +2863,55 @@ async function configurePostmanInstallArtifacts({
2385
2863
  }) {
2386
2864
  if (!postmanSelection?.enabled) return null;
2387
2865
 
2388
- let warnings = postmanSelection.warnings.filter((warning) => warning !== POSTMAN_API_KEY_MISSING_WARNING);
2389
- const settingsContent = `${JSON.stringify(postmanSelection.settings, null, 2)}\n`;
2390
- const settingsResult = await writeTextFile({
2391
- targetPath: postmanSelection.settingsPath,
2392
- content: settingsContent,
2866
+ let warnings = postmanSelection.warnings.filter(
2867
+ (warning) => warning !== POSTMAN_API_KEY_MISSING_WARNING && warning !== STITCH_API_KEY_MISSING_WARNING
2868
+ );
2869
+ const cbxConfigContent = `${JSON.stringify(postmanSelection.cbxConfig, null, 2)}\n`;
2870
+ const cbxConfigResult = await writeTextFile({
2871
+ targetPath: postmanSelection.cbxConfigPath,
2872
+ content: cbxConfigContent,
2393
2873
  overwrite,
2394
2874
  dryRun
2395
2875
  });
2396
2876
 
2397
- let effectiveApiKey = normalizePostmanApiKey(postmanSelection.settings.apiKey);
2877
+ let effectiveApiKey = normalizePostmanApiKey(postmanSelection.cbxConfig?.postman?.apiKey);
2878
+ let effectiveApiKeyEnvVar = String(
2879
+ postmanSelection.cbxConfig?.postman?.apiKeyEnvVar || POSTMAN_API_KEY_ENV_VAR
2880
+ ).trim();
2398
2881
  let effectiveDefaultWorkspaceId = postmanSelection.defaultWorkspaceId ?? null;
2399
- let effectiveMcpUrl = postmanSelection.settings.mcpUrl || POSTMAN_MCP_URL;
2400
-
2401
- if (settingsResult.action === "skipped" || settingsResult.action === "would-skip") {
2402
- try {
2403
- const existingSettingsRaw = await readFile(postmanSelection.settingsPath, "utf8");
2404
- const existingSettings = JSON.parse(existingSettingsRaw);
2405
- effectiveApiKey = normalizePostmanApiKey(existingSettings?.apiKey);
2406
- effectiveDefaultWorkspaceId = normalizePostmanWorkspaceId(existingSettings?.defaultWorkspaceId);
2407
- const existingMcpUrl = String(existingSettings?.mcpUrl || "").trim();
2408
- if (existingMcpUrl) {
2409
- effectiveMcpUrl = existingMcpUrl;
2410
- }
2411
- } catch {
2882
+ let effectiveMcpUrl = postmanSelection.cbxConfig?.postman?.mcpUrl || POSTMAN_MCP_URL;
2883
+ const shouldInstallStitch = Boolean(postmanSelection.stitchEnabled);
2884
+ let effectiveStitchApiKey = shouldInstallStitch
2885
+ ? normalizePostmanApiKey(postmanSelection.cbxConfig?.stitch?.apiKey)
2886
+ : null;
2887
+ let effectiveStitchMcpUrl = shouldInstallStitch
2888
+ ? postmanSelection.cbxConfig?.stitch?.mcpUrl || STITCH_MCP_URL
2889
+ : STITCH_MCP_URL;
2890
+
2891
+ if (cbxConfigResult.action === "skipped" || cbxConfigResult.action === "would-skip") {
2892
+ const existingCbxConfig = await readJsonFileIfExists(postmanSelection.cbxConfigPath);
2893
+ const existingLegacySettings = await readJsonFileIfExists(postmanSelection.legacySettingsPath);
2894
+ const storedPostmanConfig =
2895
+ parseStoredPostmanConfig(existingCbxConfig.value) || parseStoredPostmanConfig(existingLegacySettings.value);
2896
+ const storedStitchConfig =
2897
+ shouldInstallStitch &&
2898
+ (parseStoredStitchConfig(existingCbxConfig.value) || parseStoredStitchConfig(existingLegacySettings.value));
2899
+
2900
+ if (storedPostmanConfig) {
2901
+ effectiveApiKey = storedPostmanConfig.apiKey;
2902
+ effectiveApiKeyEnvVar = storedPostmanConfig.apiKeyEnvVar || POSTMAN_API_KEY_ENV_VAR;
2903
+ effectiveDefaultWorkspaceId = storedPostmanConfig.defaultWorkspaceId;
2904
+ effectiveMcpUrl = storedPostmanConfig.mcpUrl || POSTMAN_MCP_URL;
2905
+ } else {
2412
2906
  warnings.push(
2413
- `Existing ${POSTMAN_SETTINGS_FILENAME} could not be parsed. Using install-time Postman values for MCP config.`
2907
+ `Existing ${CBX_CONFIG_FILENAME} (or legacy ${POSTMAN_SETTINGS_FILENAME}) could not be parsed. Using install-time Postman values for MCP config.`
2414
2908
  );
2415
2909
  }
2910
+
2911
+ if (storedStitchConfig) {
2912
+ effectiveStitchApiKey = storedStitchConfig.apiKey;
2913
+ effectiveStitchMcpUrl = storedStitchConfig.mcpUrl || STITCH_MCP_URL;
2914
+ }
2416
2915
  }
2417
2916
 
2418
2917
  const envApiKey = normalizePostmanApiKey(process.env[POSTMAN_API_KEY_ENV_VAR]);
@@ -2423,52 +2922,112 @@ async function configurePostmanInstallArtifacts({
2423
2922
  if (effectiveApiKeySource === "unset") {
2424
2923
  warnings.push(POSTMAN_API_KEY_MISSING_WARNING);
2425
2924
  }
2925
+ const envStitchApiKey = normalizePostmanApiKey(process.env[STITCH_API_KEY_ENV_VAR]);
2926
+ const effectiveStitchApiKeySource = shouldInstallStitch
2927
+ ? getStitchApiKeySource({
2928
+ apiKey: effectiveStitchApiKey,
2929
+ envApiKey: envStitchApiKey
2930
+ })
2931
+ : null;
2932
+ if (shouldInstallStitch && effectiveStitchApiKeySource === "unset") {
2933
+ warnings.push(STITCH_API_KEY_MISSING_WARNING);
2934
+ }
2426
2935
 
2427
- let gitIgnoreResult = null;
2428
- if (scope === "project") {
2936
+ const gitIgnoreResults = [];
2937
+ if (postmanSelection.mcpScope === "project") {
2429
2938
  const workspaceRoot = findWorkspaceRoot(cwd);
2430
2939
  const gitIgnorePath = path.join(workspaceRoot, ".gitignore");
2431
- gitIgnoreResult = await ensureGitIgnoreEntry({
2940
+ const configIgnore = await ensureGitIgnoreEntry({
2941
+ filePath: gitIgnorePath,
2942
+ entry: CBX_CONFIG_FILENAME,
2943
+ dryRun
2944
+ });
2945
+ gitIgnoreResults.push(configIgnore);
2946
+
2947
+ const mcpIgnore = await ensureGitIgnoreEntry({
2432
2948
  filePath: gitIgnorePath,
2433
- entry: POSTMAN_SETTINGS_FILENAME,
2949
+ entry: ".cbx/mcp/",
2434
2950
  dryRun
2435
2951
  });
2952
+ gitIgnoreResults.push(mcpIgnore);
2436
2953
  }
2437
2954
 
2438
- const postmanSkillDir = path.join(profilePaths.skillsDir, POSTMAN_SKILL_ID);
2439
- const postmanMcpPath = path.join(postmanSkillDir, "mcp.json");
2440
- let mcpResult = null;
2441
- const postmanSkillExists = await pathExists(postmanSkillDir);
2442
- if (!dryRun && !postmanSkillExists) {
2443
- mcpResult = {
2444
- action: "missing-postman-skill",
2445
- path: postmanMcpPath
2446
- };
2447
- } else {
2448
- const mcpConfigContent = `${JSON.stringify(
2449
- buildPostmanMcpConfig({
2450
- apiKey: effectiveApiKey,
2451
- mcpUrl: effectiveMcpUrl
2955
+ const mcpDefinitionPath = resolvePostmanMcpDefinitionPath({
2956
+ platform,
2957
+ scope: postmanSelection.mcpScope,
2958
+ cwd
2959
+ });
2960
+ const mcpDefinitionContent = `${JSON.stringify(
2961
+ buildPostmanMcpDefinition({
2962
+ apiKey: effectiveApiKey,
2963
+ apiKeyEnvVar: effectiveApiKeyEnvVar,
2964
+ mcpUrl: effectiveMcpUrl
2965
+ }),
2966
+ null,
2967
+ 2
2968
+ )}\n`;
2969
+ const mcpDefinitionResult = await writeGeneratedArtifact({
2970
+ destination: mcpDefinitionPath,
2971
+ content: mcpDefinitionContent,
2972
+ dryRun
2973
+ });
2974
+ let stitchMcpDefinitionPath = null;
2975
+ let stitchMcpDefinitionResult = null;
2976
+ if (shouldInstallStitch) {
2977
+ stitchMcpDefinitionPath = resolveStitchMcpDefinitionPath({
2978
+ scope: postmanSelection.mcpScope,
2979
+ cwd
2980
+ });
2981
+ const stitchMcpDefinitionContent = `${JSON.stringify(
2982
+ buildStitchMcpDefinition({
2983
+ apiKey: effectiveStitchApiKey,
2984
+ mcpUrl: effectiveStitchMcpUrl
2452
2985
  }),
2453
2986
  null,
2454
2987
  2
2455
2988
  )}\n`;
2456
- mcpResult = await writeGeneratedArtifact({
2457
- destination: postmanMcpPath,
2458
- content: mcpConfigContent,
2989
+ stitchMcpDefinitionResult = await writeGeneratedArtifact({
2990
+ destination: stitchMcpDefinitionPath,
2991
+ content: stitchMcpDefinitionContent,
2459
2992
  dryRun
2460
2993
  });
2461
2994
  }
2462
2995
 
2996
+ const mcpRuntimeResult = await applyPostmanMcpForPlatform({
2997
+ platform,
2998
+ mcpScope: postmanSelection.mcpScope,
2999
+ apiKey: effectiveApiKey,
3000
+ apiKeyEnvVar: effectiveApiKeyEnvVar,
3001
+ mcpUrl: effectiveMcpUrl,
3002
+ stitchApiKey: effectiveStitchApiKey,
3003
+ stitchMcpUrl: effectiveStitchMcpUrl,
3004
+ includeStitchMcp: shouldInstallStitch,
3005
+ dryRun,
3006
+ cwd
3007
+ });
3008
+ warnings.push(...(mcpRuntimeResult.warnings || []));
3009
+
3010
+ const legacySkillMcpCleanup = await removeGeneratedArtifactIfExists({
3011
+ targetPath: path.join(profilePaths.skillsDir, POSTMAN_SKILL_ID, "mcp.json"),
3012
+ dryRun
3013
+ });
3014
+
2463
3015
  return {
2464
3016
  enabled: true,
3017
+ mcpScope: postmanSelection.mcpScope,
2465
3018
  apiKeySource: effectiveApiKeySource,
3019
+ stitchApiKeySource: effectiveStitchApiKeySource,
2466
3020
  defaultWorkspaceId: effectiveDefaultWorkspaceId,
2467
3021
  warnings,
2468
- settingsPath: postmanSelection.settingsPath,
2469
- settingsResult,
2470
- gitIgnoreResult,
2471
- mcpResult
3022
+ cbxConfigPath: postmanSelection.cbxConfigPath,
3023
+ cbxConfigResult,
3024
+ gitIgnoreResults,
3025
+ mcpDefinitionPath,
3026
+ mcpDefinitionResult,
3027
+ stitchMcpDefinitionPath,
3028
+ stitchMcpDefinitionResult,
3029
+ mcpRuntimeResult,
3030
+ legacySkillMcpCleanup
2472
3031
  };
2473
3032
  }
2474
3033
 
@@ -2632,12 +3191,13 @@ async function installBundleArtifacts({
2632
3191
  platform,
2633
3192
  scope,
2634
3193
  overwrite,
3194
+ profilePathsOverride = null,
2635
3195
  extraSkillIds = [],
2636
3196
  terminalVerifierSelection = null,
2637
3197
  dryRun = false,
2638
3198
  cwd = process.cwd()
2639
3199
  }) {
2640
- const profilePaths = await resolveProfilePaths(platform, scope, cwd);
3200
+ const profilePaths = profilePathsOverride || (await resolveArtifactProfilePaths(platform, scope, cwd));
2641
3201
  const platformSpec = manifest.platforms?.[platform];
2642
3202
 
2643
3203
  if (!platformSpec) {
@@ -2766,6 +3326,46 @@ async function installBundleArtifacts({
2766
3326
  };
2767
3327
  }
2768
3328
 
3329
+ async function installCodexProjectWorkflowTemplates({
3330
+ bundleId,
3331
+ manifest,
3332
+ overwrite,
3333
+ dryRun = false,
3334
+ cwd = process.cwd()
3335
+ }) {
3336
+ const platform = "codex";
3337
+ const platformSpec = manifest.platforms?.[platform];
3338
+ if (!platformSpec) return { installed: [], skipped: [] };
3339
+
3340
+ const workflowFiles = Array.isArray(platformSpec.workflows) ? platformSpec.workflows : [];
3341
+ if (workflowFiles.length === 0) return { installed: [], skipped: [] };
3342
+
3343
+ const profilePaths = await resolveProfilePaths(platform, "project", cwd);
3344
+ if (!dryRun) {
3345
+ await mkdir(profilePaths.workflowsDir, { recursive: true });
3346
+ }
3347
+
3348
+ const bundleRoot = path.join(agentAssetsRoot(), "workflows", bundleId);
3349
+ const platformRoot = path.join(bundleRoot, "platforms", platform);
3350
+ const installed = [];
3351
+ const skipped = [];
3352
+
3353
+ for (const workflowFile of workflowFiles) {
3354
+ const source = path.join(platformRoot, "workflows", workflowFile);
3355
+ const destination = path.join(profilePaths.workflowsDir, path.basename(workflowFile));
3356
+
3357
+ if (!(await pathExists(source))) {
3358
+ throw new Error(`Missing workflow source file: ${source}`);
3359
+ }
3360
+
3361
+ const result = await copyArtifact({ source, destination, overwrite, dryRun });
3362
+ if (result.action === "skipped" || result.action === "would-skip") skipped.push(destination);
3363
+ else installed.push(destination);
3364
+ }
3365
+
3366
+ return { installed, skipped };
3367
+ }
3368
+
2769
3369
  async function seedRuleFileFromTemplateIfMissing({
2770
3370
  bundleId,
2771
3371
  manifest,
@@ -2833,10 +3433,11 @@ async function removeBundleArtifacts({
2833
3433
  manifest,
2834
3434
  platform,
2835
3435
  scope,
3436
+ profilePathsOverride = null,
2836
3437
  dryRun = false,
2837
3438
  cwd = process.cwd()
2838
3439
  }) {
2839
- const profilePaths = await resolveProfilePaths(platform, scope, cwd);
3440
+ const profilePaths = profilePathsOverride || (await resolveArtifactProfilePaths(platform, scope, cwd));
2840
3441
  const platformSpec = manifest.platforms?.[platform];
2841
3442
  if (!platformSpec) throw new Error(`Bundle '${bundleId}' does not define platform '${platform}'.`);
2842
3443
 
@@ -2886,6 +3487,7 @@ function printPlatforms() {
2886
3487
  );
2887
3488
  console.log(` global skills: ${profile.global.skillDirs[0]}`);
2888
3489
  console.log(` global rules: ${profile.global.ruleFilesByPriority.join(" | ")}`);
3490
+ console.log(" default install: workflows/agents -> project, skills -> global");
2889
3491
  }
2890
3492
  }
2891
3493
 
@@ -2995,18 +3597,35 @@ function printPostmanSetupSummary({ postmanSetup }) {
2995
3597
  if (!postmanSetup?.enabled) return;
2996
3598
 
2997
3599
  console.log("\nPostman setup:");
2998
- console.log(`- Settings file: ${postmanSetup.settingsResult.action} (${postmanSetup.settingsPath})`);
2999
- console.log(`- API key source: ${postmanSetup.apiKeySource}`);
3600
+ console.log(`- MCP scope: ${postmanSetup.mcpScope}`);
3601
+ console.log(`- Config file: ${postmanSetup.cbxConfigResult.action} (${postmanSetup.cbxConfigPath})`);
3602
+ console.log(`- Postman API key source: ${postmanSetup.apiKeySource}`);
3603
+ if (postmanSetup.stitchApiKeySource) {
3604
+ console.log(`- Stitch API key source: ${postmanSetup.stitchApiKeySource}`);
3605
+ }
3000
3606
  console.log(
3001
3607
  `- Default workspace ID: ${postmanSetup.defaultWorkspaceId === null ? "null" : postmanSetup.defaultWorkspaceId}`
3002
3608
  );
3003
- if (postmanSetup.gitIgnoreResult) {
3609
+ for (const ignoreResult of postmanSetup.gitIgnoreResults || []) {
3610
+ console.log(`- .gitignore (${ignoreResult.filePath}): ${ignoreResult.action}`);
3611
+ }
3612
+ console.log(
3613
+ `- Managed MCP definition (${postmanSetup.mcpDefinitionPath}): ${postmanSetup.mcpDefinitionResult.action}`
3614
+ );
3615
+ if (postmanSetup.stitchMcpDefinitionPath && postmanSetup.stitchMcpDefinitionResult) {
3004
3616
  console.log(
3005
- `- .gitignore (${postmanSetup.gitIgnoreResult.filePath}): ${postmanSetup.gitIgnoreResult.action}`
3617
+ `- Managed Stitch MCP definition (${postmanSetup.stitchMcpDefinitionPath}): ${postmanSetup.stitchMcpDefinitionResult.action}`
3006
3618
  );
3007
3619
  }
3008
- if (postmanSetup.mcpResult) {
3009
- console.log(`- Postman MCP config (${postmanSetup.mcpResult.path}): ${postmanSetup.mcpResult.action}`);
3620
+ if (postmanSetup.mcpRuntimeResult) {
3621
+ console.log(
3622
+ `- Platform MCP target (${postmanSetup.mcpRuntimeResult.path || "n/a"}): ${postmanSetup.mcpRuntimeResult.action}`
3623
+ );
3624
+ }
3625
+ if (postmanSetup.legacySkillMcpCleanup) {
3626
+ console.log(
3627
+ `- Legacy skill mcp.json cleanup (${postmanSetup.legacySkillMcpCleanup.path}): ${postmanSetup.legacySkillMcpCleanup.action}`
3628
+ );
3010
3629
  }
3011
3630
 
3012
3631
  if (postmanSetup.warnings.length > 0) {
@@ -3060,21 +3679,22 @@ function printRemoveSummary({
3060
3679
  async function createDoctorReport({ platform, scope, cwd = process.cwd() }) {
3061
3680
  const profile = WORKFLOW_PROFILES[platform];
3062
3681
  const profilePaths = await resolveProfilePaths(platform, scope, cwd);
3682
+ const artifactPaths = await resolveArtifactProfilePaths(platform, scope, cwd);
3063
3683
  const agentsEnabled = platformInstallsCustomAgents(platform);
3064
3684
 
3065
3685
  const pathStatus = {
3066
3686
  workflows: {
3067
- path: profilePaths.workflowsDir,
3068
- exists: await pathExists(profilePaths.workflowsDir)
3687
+ path: artifactPaths.workflowsDir,
3688
+ exists: await pathExists(artifactPaths.workflowsDir)
3069
3689
  },
3070
3690
  agents: {
3071
- path: profilePaths.agentsDir,
3691
+ path: artifactPaths.agentsDir,
3072
3692
  enabled: agentsEnabled,
3073
- exists: agentsEnabled ? await pathExists(profilePaths.agentsDir) : null
3693
+ exists: agentsEnabled ? await pathExists(artifactPaths.agentsDir) : null
3074
3694
  },
3075
3695
  skills: {
3076
- path: profilePaths.skillsDir,
3077
- exists: await pathExists(profilePaths.skillsDir)
3696
+ path: artifactPaths.skillsDir,
3697
+ exists: await pathExists(artifactPaths.skillsDir)
3078
3698
  }
3079
3699
  };
3080
3700
 
@@ -3100,7 +3720,7 @@ async function createDoctorReport({ platform, scope, cwd = process.cwd() }) {
3100
3720
 
3101
3721
  let terminalIntegration = null;
3102
3722
  if (platform === "antigravity") {
3103
- const integrationDir = getAntigravityTerminalIntegrationDir(profilePaths);
3723
+ const integrationDir = getAntigravityTerminalIntegrationDir(artifactPaths);
3104
3724
  const configPath = path.join(integrationDir, "config.json");
3105
3725
  const exists = await pathExists(integrationDir);
3106
3726
  const configExists = await pathExists(configPath);
@@ -3220,7 +3840,7 @@ async function createDoctorReport({ platform, scope, cwd = process.cwd() }) {
3220
3840
  }
3221
3841
 
3222
3842
  if (platform === "copilot" && pathStatus.skills.exists) {
3223
- const findings = await validateCopilotSkillsSchema(profilePaths.skillsDir);
3843
+ const findings = await validateCopilotSkillsSchema(artifactPaths.skillsDir);
3224
3844
  if (findings.length > 0) {
3225
3845
  const preview = findings
3226
3846
  .slice(0, 5)
@@ -3236,7 +3856,7 @@ async function createDoctorReport({ platform, scope, cwd = process.cwd() }) {
3236
3856
  }
3237
3857
 
3238
3858
  if (platform === "copilot" && pathStatus.agents.exists) {
3239
- const findings = await validateCopilotAgentsSchema(profilePaths.agentsDir);
3859
+ const findings = await validateCopilotAgentsSchema(artifactPaths.agentsDir);
3240
3860
  if (findings.length > 0) {
3241
3861
  const preview = findings
3242
3862
  .slice(0, 5)
@@ -3326,16 +3946,24 @@ function withInstallOptions(command) {
3326
3946
  .option("--overwrite", "overwrite existing files")
3327
3947
  .option(
3328
3948
  "--postman",
3329
- "optional: install Postman skill and generate postman_setting.json"
3949
+ "optional: install Postman skill and generate cbx_config.json"
3330
3950
  )
3331
3951
  .option(
3332
3952
  "--postman-api-key <key>",
3333
- "optional: set Postman API key inline for generated postman_setting.json and installed Postman MCP config"
3953
+ "optional: set Postman API key inline for generated cbx_config.json and installed Postman MCP config"
3334
3954
  )
3335
3955
  .option(
3336
3956
  "--postman-workspace-id <id|null>",
3337
3957
  "optional: set default Postman workspace ID (use 'null' for no default)"
3338
3958
  )
3959
+ .option(
3960
+ "--stitch-api-key <key>",
3961
+ "optional: Antigravity only. Set Google Stitch API key inline for StitchMCP config"
3962
+ )
3963
+ .option(
3964
+ "--mcp-scope <scope>",
3965
+ "optional: MCP config scope for --postman (project|workspace|global|user)"
3966
+ )
3339
3967
  .option(
3340
3968
  "--terminal-integration",
3341
3969
  "Antigravity only: enable terminal verification integration (prompts for verifier when interactive)"
@@ -3447,7 +4075,7 @@ async function cleanupAntigravityTerminalIntegration({
3447
4075
  cwd,
3448
4076
  dryRun = false
3449
4077
  }) {
3450
- const profilePaths = await resolveProfilePaths("antigravity", scope, cwd);
4078
+ const profilePaths = await resolveArtifactProfilePaths("antigravity", scope, cwd);
3451
4079
  const integrationDir = getAntigravityTerminalIntegrationDir(profilePaths);
3452
4080
  const dirRemoved = await safeRemove(integrationDir, dryRun);
3453
4081
 
@@ -3475,10 +4103,12 @@ async function cleanupAntigravityTerminalIntegration({
3475
4103
 
3476
4104
  async function runWorkflowInstall(options) {
3477
4105
  try {
4106
+ const cwd = process.cwd();
3478
4107
  const scope = normalizeScope(options.scope);
3479
4108
  const ruleScope = scope === "global" ? "project" : scope;
3480
4109
  const dryRun = Boolean(options.dryRun);
3481
- const platform = await resolvePlatform(options.platform, scope, process.cwd());
4110
+ const platform = await resolvePlatform(options.platform, scope, cwd);
4111
+ const artifactProfilePaths = await resolveArtifactProfilePaths(platform, scope, cwd);
3482
4112
  const bundleId = await chooseBundle(options.bundle);
3483
4113
  const manifest = await readBundleManifest(bundleId);
3484
4114
 
@@ -3498,9 +4128,10 @@ async function runWorkflowInstall(options) {
3498
4128
  options
3499
4129
  });
3500
4130
  const postmanSelection = await resolvePostmanInstallSelection({
4131
+ platform,
3501
4132
  scope,
3502
4133
  options,
3503
- cwd: process.cwd()
4134
+ cwd
3504
4135
  });
3505
4136
 
3506
4137
  const installResult = await installBundleArtifacts({
@@ -3509,12 +4140,28 @@ async function runWorkflowInstall(options) {
3509
4140
  platform,
3510
4141
  scope,
3511
4142
  overwrite: Boolean(options.overwrite),
4143
+ profilePathsOverride: artifactProfilePaths,
3512
4144
  extraSkillIds: postmanSelection.enabled ? [POSTMAN_SKILL_ID] : [],
3513
4145
  terminalVerifierSelection,
3514
4146
  dryRun,
3515
- cwd: process.cwd()
4147
+ cwd
3516
4148
  });
3517
4149
 
4150
+ if (platform === "codex" && scope === "global") {
4151
+ const codexProjectPaths = await resolveProfilePaths("codex", "project", cwd);
4152
+ if (path.resolve(artifactProfilePaths.workflowsDir) !== path.resolve(codexProjectPaths.workflowsDir)) {
4153
+ const codexProjectWorkflows = await installCodexProjectWorkflowTemplates({
4154
+ bundleId,
4155
+ manifest,
4156
+ overwrite: Boolean(options.overwrite),
4157
+ dryRun,
4158
+ cwd
4159
+ });
4160
+ installResult.installed.push(...codexProjectWorkflows.installed);
4161
+ installResult.skipped.push(...codexProjectWorkflows.skipped);
4162
+ }
4163
+ }
4164
+
3518
4165
  await seedRuleFileFromTemplateIfMissing({
3519
4166
  bundleId,
3520
4167
  manifest,
@@ -3522,14 +4169,14 @@ async function runWorkflowInstall(options) {
3522
4169
  scope: ruleScope,
3523
4170
  overwrite: Boolean(options.overwrite),
3524
4171
  dryRun,
3525
- cwd: process.cwd()
4172
+ cwd
3526
4173
  });
3527
4174
 
3528
4175
  const syncResult = await syncRulesForPlatform({
3529
4176
  platform,
3530
4177
  scope: ruleScope,
3531
4178
  dryRun,
3532
- cwd: process.cwd()
4179
+ cwd
3533
4180
  });
3534
4181
  const engineeringArtifactsResult = await upsertEngineeringArtifacts({
3535
4182
  platform,
@@ -3537,22 +4184,23 @@ async function runWorkflowInstall(options) {
3537
4184
  overwrite: false,
3538
4185
  dryRun,
3539
4186
  skipTech: false,
3540
- cwd: process.cwd()
4187
+ cwd
3541
4188
  });
3542
4189
  const postmanSetupResult = await configurePostmanInstallArtifacts({
4190
+ platform,
3543
4191
  scope,
3544
4192
  profilePaths: installResult.profilePaths,
3545
4193
  postmanSelection,
3546
4194
  overwrite: Boolean(options.overwrite),
3547
4195
  dryRun,
3548
- cwd: process.cwd()
4196
+ cwd
3549
4197
  });
3550
4198
 
3551
4199
  const terminalVerificationRuleResult =
3552
4200
  platform === "antigravity" && installResult.terminalIntegration
3553
4201
  ? await upsertTerminalVerificationForInstall({
3554
4202
  scope: ruleScope,
3555
- cwd: process.cwd(),
4203
+ cwd,
3556
4204
  terminalIntegration: installResult.terminalIntegration,
3557
4205
  dryRun
3558
4206
  })
@@ -3565,7 +4213,7 @@ async function runWorkflowInstall(options) {
3565
4213
  bundleId,
3566
4214
  artifacts: installResult.artifacts,
3567
4215
  ruleFilePath: syncResult.filePath,
3568
- cwd: process.cwd()
4216
+ cwd
3569
4217
  });
3570
4218
  }
3571
4219
 
@@ -3611,9 +4259,12 @@ async function runWorkflowRemove(target, options) {
3611
4259
  throw new Error("Missing <bundle-or-workflow>. Usage: cbx workflows remove <bundle-or-workflow>");
3612
4260
  }
3613
4261
 
4262
+ const cwd = process.cwd();
3614
4263
  const scope = normalizeScope(options.scope);
4264
+ const ruleScope = scope === "global" ? "project" : scope;
3615
4265
  const dryRun = Boolean(options.dryRun);
3616
- const platform = await resolvePlatform(options.platform, scope, process.cwd());
4266
+ const platform = await resolvePlatform(options.platform, scope, cwd);
4267
+ const artifactProfilePaths = await resolveArtifactProfilePaths(platform, scope, cwd);
3617
4268
  const bundleIds = await listBundleIds();
3618
4269
 
3619
4270
  let removed = [];
@@ -3640,8 +4291,9 @@ async function runWorkflowRemove(target, options) {
3640
4291
  manifest,
3641
4292
  platform,
3642
4293
  scope,
4294
+ profilePathsOverride: artifactProfilePaths,
3643
4295
  dryRun,
3644
- cwd: process.cwd()
4296
+ cwd
3645
4297
  });
3646
4298
 
3647
4299
  removed = removeResult.removed;
@@ -3649,7 +4301,7 @@ async function runWorkflowRemove(target, options) {
3649
4301
  if (platform === "antigravity") {
3650
4302
  terminalIntegrationCleanup = await cleanupAntigravityTerminalIntegration({
3651
4303
  scope,
3652
- cwd: process.cwd(),
4304
+ cwd,
3653
4305
  dryRun
3654
4306
  });
3655
4307
  if (terminalIntegrationCleanup.dirRemoved) {
@@ -3657,8 +4309,7 @@ async function runWorkflowRemove(target, options) {
3657
4309
  }
3658
4310
  }
3659
4311
  } else {
3660
- const profilePaths = await resolveProfilePaths(platform, scope, process.cwd());
3661
- const workflowFile = await findWorkflowFileByTarget(profilePaths.workflowsDir, target);
4312
+ const workflowFile = await findWorkflowFileByTarget(artifactProfilePaths.workflowsDir, target);
3662
4313
 
3663
4314
  if (!workflowFile) {
3664
4315
  throw new Error(`Could not find workflow or bundle '${target}' in platform '${platform}'.`);
@@ -3682,9 +4333,9 @@ async function runWorkflowRemove(target, options) {
3682
4333
 
3683
4334
  const syncResult = await syncRulesForPlatform({
3684
4335
  platform,
3685
- scope,
4336
+ scope: ruleScope,
3686
4337
  dryRun,
3687
- cwd: process.cwd()
4338
+ cwd
3688
4339
  });
3689
4340
 
3690
4341
  if (!dryRun && removedType === "bundle") {
@@ -3693,7 +4344,7 @@ async function runWorkflowRemove(target, options) {
3693
4344
  platform,
3694
4345
  bundleId: target,
3695
4346
  ruleFilePath: syncResult.filePath,
3696
- cwd: process.cwd()
4347
+ cwd
3697
4348
  });
3698
4349
  }
3699
4350
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cubis/foundry",
3
- "version": "0.3.20",
3
+ "version": "0.3.22",
4
4
  "description": "Cubis Foundry CLI for workflow-first AI agent environments",
5
5
  "type": "module",
6
6
  "bin": {