@geraldmaron/construct 1.3.1 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -53,11 +53,16 @@ export function validateAllGoldenArtifactGates({ rootDir } = {}) {
53
53
  return { pass: errors.length === 0, results, errors, matrix: artifactGateMatrix({ rootDir: root }) };
54
54
  }
55
55
 
56
+ // The committed gate matrix is a deterministic projection of the rootDir; no
57
+ // wall-clock field, so regenerating it (the artifact-gates test writes it on every
58
+ // run) yields a byte-identical file instead of a timestamp-only diff on a tracked
59
+ // artifact. The matrix content is the signal; when it changes, the diff is real.
60
+
56
61
  export function writeArtifactGateMatrixDoc({ rootDir } = {}) {
57
62
  const root = findConstructRoot(rootDir);
58
63
  const out = path.join(root, 'tests', 'certification', 'artifacts', 'gate-matrix.json');
59
64
  fs.mkdirSync(path.dirname(out), { recursive: true });
60
- const payload = { generatedAt: new Date().toISOString(), matrix: artifactGateMatrix({ rootDir: root }) };
65
+ const payload = { matrix: artifactGateMatrix({ rootDir: root }) };
61
66
  fs.writeFileSync(out, `${JSON.stringify(payload, null, 2)}\n`);
62
67
  return out;
63
68
  }
@@ -339,35 +339,33 @@ export async function cmdMcpAdd(id) {
339
339
  }
340
340
  }
341
341
 
342
- // GitHub: source token from gh CLI, env, or prompt (--token flag)
343
- if (id === 'github') {
342
+ // GitHub's hosted remote server uses one-click OAuth by default: the host runs
343
+ // the browser flow and stores the token in its own secure store, so we wire the
344
+ // URL only and keep no credential. `--token`/`--pat` opts into the PAT fallback
345
+ // (headless/CI), sourced from gh CLI or env and emitted as an env reference.
346
+ let authMode = id === 'github' ? 'oauth' : 'pat';
347
+ if (id === 'github' && (flags.token || flags.pat)) {
348
+ authMode = 'pat';
344
349
  const envToken = process.env.GITHUB_TOKEN || process.env.GITHUB_PERSONAL_ACCESS_TOKEN;
345
- const forceToken = flags.token;
346
-
347
350
  let ghCliToken = '';
348
351
  try {
349
352
  execSync('gh auth status -h github.com', { stdio: 'ignore' });
350
353
  ghCliToken = execSync('gh auth token -h github.com 2>/dev/null', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
351
354
  } catch { /* gh not installed or not authed */ }
352
355
 
353
- if (forceToken) {
356
+ const best = envToken || ghCliToken;
357
+ if (process.stdin.isTTY && typeof flags.token !== 'string') {
354
358
  const rl = createInterface({ input: process.stdin, output: process.stdout });
355
- const best = envToken || ghCliToken;
356
359
  const hint = best ? ' [press Enter to use detected token]' : '';
357
360
  const value = await prompt(rl, `\n > GITHUB_TOKEN${hint}: `);
358
361
  rl.close();
359
362
  envValues.GITHUB_TOKEN = value.trim() || best || '';
360
- } else if (ghCliToken) {
361
- console.log('\n ✓ GitHub token sourced from gh CLI (gh auth login).');
362
- envValues.GITHUB_TOKEN = ghCliToken;
363
- } else if (envToken) {
364
- console.log('\n ✓ GitHub token detected in environment.');
365
- envValues.GITHUB_TOKEN = envToken;
366
363
  } else {
367
- console.log('\n ✗ No GitHub token found.');
368
- console.log(' Run: gh auth login -h github.com (opens browser, stores token automatically)');
369
- console.log(' Then re-run: construct mcp add github');
370
- console.log(' Or provide a PAT: construct mcp add github --token');
364
+ envValues.GITHUB_TOKEN = (typeof flags.token === 'string' ? flags.token : '') || best || '';
365
+ }
366
+ if (!envValues.GITHUB_TOKEN) {
367
+ console.log('\n --token requested but no PAT found (gh CLI / env / prompt).');
368
+ console.log(' Run `gh auth login -h github.com`, or omit --token to use OAuth.');
371
369
  process.exit(1);
372
370
  }
373
371
  }
@@ -418,7 +416,7 @@ export async function cmdMcpAdd(id) {
418
416
  const settings = loadSettings();
419
417
  if (!settings.mcpServers) settings.mcpServers = {};
420
418
  if (isManagedOnHost(mcp, 'claude')) {
421
- settings.mcpServers[id] = buildClaudeMcpEntry(id, mcp, resolvedValues);
419
+ settings.mcpServers[id] = buildClaudeMcpEntry(id, mcp, resolvedValues, { auth: authMode });
422
420
  } else {
423
421
  delete settings.mcpServers[id];
424
422
  }
@@ -431,7 +429,7 @@ export async function cmdMcpAdd(id) {
431
429
  delete oc.mcp[id];
432
430
  if (id === 'memory') delete oc.mcp.cass;
433
431
  if (isManagedOnHost(mcp, 'opencode')) {
434
- oc.mcp[openCodeId] = buildOpenCodeMcpEntry(id, mcp, resolvedValues).entry;
432
+ oc.mcp[openCodeId] = buildOpenCodeMcpEntry(id, mcp, resolvedValues, { auth: authMode }).entry;
435
433
  } else {
436
434
  delete oc.mcp[openCodeId];
437
435
  }
@@ -451,8 +449,15 @@ export async function cmdMcpAdd(id) {
451
449
 
452
450
  console.log(`\n✓ ${mcp.name} successfully wired into Claude Code, OpenCode, and Codex.`);
453
451
  console.log(`✓ Setup mode: ${setupMode}`);
452
+ console.log(`✓ Auth: ${id === 'github' ? authMode : 'managed'}`);
454
453
  console.log(`✓ Services synchronized.`);
455
454
  console.log(`\nNext: Restart OpenCode, Claude Code, or Codex to activate the new tools.`);
455
+ if (id === 'github' && authMode === 'oauth') {
456
+ console.log(`\nAuthenticate (one-click OAuth, no token stored on disk):`);
457
+ console.log(` Claude Code: \`claude mcp login github\` · OpenCode: \`opencode mcp auth github\``);
458
+ console.log(` VS Code / Cursor: approve the sign-in prompt on first use.`);
459
+ console.log(` PAT fallback (headless/CI): \`construct mcp add github --token\``);
460
+ }
456
461
  for (const host of ['claude', 'opencode', 'codex']) {
457
462
  const support = getHostSupport(mcp, host);
458
463
  const hostLabel = host === 'claude' ? 'Claude Code' : host === 'opencode' ? 'OpenCode' : 'Codex';
@@ -40,14 +40,24 @@ function resolveArgs(args, resolvedValues) {
40
40
  );
41
41
  }
42
42
 
43
+ // Remote MCP header values carry secrets (e.g. `Authorization: Bearer __GITHUB_TOKEN__`).
44
+ // Emit a host-resolved env reference rather than the literal token value, so a live
45
+ // credential never lands in a config file on disk. Each host expands a different
46
+ // reference syntax; the secret stays in one place (config.env / the shell env) and the
47
+ // host resolves it at launch.
48
+
49
+ const SECRET_REF = {
50
+ claude: (name) => `\${${name}}`,
51
+ vscode: (name) => `\${env:${name}}`,
52
+ opencode: (name) => `{env:${name}}`,
53
+ };
54
+
43
55
  function buildLocalEnvironment(mcpDef, resolvedValues) {
44
56
  return stripUnresolvedValues(resolveTemplateObject(mcpDef.env ?? {}, resolvedValues));
45
57
  }
46
58
 
47
- function buildRemoteHeaders(mcpDef, resolvedValues) {
48
- return stripUnresolvedValues(
49
- resolveTemplateObject(mcpDef.headers ?? {}, resolvedValues, (name) => `{env:${name}}`),
50
- );
59
+ function buildRemoteHeaders(mcpDef, secretRef) {
60
+ return resolveTemplateObject(mcpDef.headers ?? {}, {}, secretRef);
51
61
  }
52
62
 
53
63
  function withMemoryDefaults(id, values) {
@@ -56,11 +66,18 @@ function withMemoryDefaults(id, values) {
56
66
  return { ...values, MEMORY_PORT: "8765" };
57
67
  }
58
68
 
59
- export function buildClaudeMcpEntry(id, mcpDef, resolvedValues = {}) {
69
+ // auth defaults to "oauth": remote MCP servers (e.g. GitHub's hosted server) are
70
+ // configured by URL only, and the host runs the browser OAuth flow and stores the
71
+ // token in its own secure store — nothing secret touches the config file. "pat"
72
+ // is the opt-in fallback for non-OAuth contexts (headless/CI), emitting an env
73
+ // reference for the credential, never the literal value.
74
+
75
+ export function buildClaudeMcpEntry(id, mcpDef, resolvedValues = {}, { host = "claude", auth = "oauth" } = {}) {
60
76
  const values = withMemoryDefaults(id, { CX_TOOLKIT_DIR: ROOT_DIR, ...resolvedValues });
77
+ const secretRef = SECRET_REF[host] ?? SECRET_REF.claude;
61
78
 
62
79
  if (mcpDef.type === "url") {
63
- const headers = buildRemoteHeaders(mcpDef, values);
80
+ const headers = auth === "pat" ? buildRemoteHeaders(mcpDef, secretRef) : {};
64
81
  const url =
65
82
  id === "memory" && values.MEMORY_PORT
66
83
  ? `http://127.0.0.1:${values.MEMORY_PORT}/`
@@ -80,7 +97,7 @@ export function buildClaudeMcpEntry(id, mcpDef, resolvedValues = {}) {
80
97
  };
81
98
  }
82
99
 
83
- export function buildOpenCodeMcpEntry(id, mcpDef, resolvedValues = {}) {
100
+ export function buildOpenCodeMcpEntry(id, mcpDef, resolvedValues = {}, { auth = "oauth" } = {}) {
84
101
  const runtimeValues = withMemoryDefaults(id, {
85
102
  CX_TOOLKIT_DIR: ROOT_DIR,
86
103
  ...resolvedValues,
@@ -92,12 +109,13 @@ export function buildOpenCodeMcpEntry(id, mcpDef, resolvedValues = {}) {
92
109
  id === "memory" && runtimeValues.MEMORY_PORT
93
110
  ? `http://127.0.0.1:${runtimeValues.MEMORY_PORT}/`
94
111
  : resolveTemplateString(mcpDef.url, runtimeValues);
112
+ const headers = auth === "pat" ? buildRemoteHeaders(mcpDef, SECRET_REF.opencode) : {};
95
113
  return {
96
114
  id: openCodeId,
97
115
  entry: {
98
116
  type: "remote",
99
117
  url,
100
- ...(Object.keys(buildRemoteHeaders(mcpDef, runtimeValues)).length > 0 ? { headers: buildRemoteHeaders(mcpDef, runtimeValues) } : {}),
118
+ ...(Object.keys(headers).length > 0 ? { headers } : {}),
101
119
  },
102
120
  };
103
121
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geraldmaron/construct",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
4
4
  "type": "module",
5
5
  "packageManager": "npm@11.5.1",
6
6
  "description": "Construct — agent orchestration layer for OpenCode, Claude Code, and other coding surfaces",
@@ -1481,7 +1481,7 @@ function syncVSCode(targetDir = null, wants = true) {
1481
1481
  const existingIsRemote = existingEntry && (existingEntry.type === 'http' || existingEntry.type === 'remote');
1482
1482
  const transportMismatch = registryWantsCommand && existingIsRemote;
1483
1483
  if (existingEntry && !hasPlaceholder && !transportMismatch) continue;
1484
- config.servers[id] = buildClaudeMcpEntry(id, mcpDef, process.env);
1484
+ config.servers[id] = buildClaudeMcpEntry(id, mcpDef, process.env, { host: "vscode" });
1485
1485
  }
1486
1486
  if (!DRY_RUN) {
1487
1487
  mkdirp(path.dirname(mcpPath));
@@ -1505,7 +1505,7 @@ function syncVSCode(targetDir = null, wants = true) {
1505
1505
  const existingIsRemote = existingEntry && (existingEntry.type === 'http' || existingEntry.type === 'remote');
1506
1506
  const transportMismatch = registryWantsCommand && existingIsRemote;
1507
1507
  if (existingEntry && !hasPlaceholder && !transportMismatch) continue;
1508
- config.servers[id] = buildClaudeMcpEntry(id, mcpDef, process.env);
1508
+ config.servers[id] = buildClaudeMcpEntry(id, mcpDef, process.env, { host: "vscode" });
1509
1509
  }
1510
1510
  if (!DRY_RUN) fs.writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n");
1511
1511
  synced = true;
@@ -1575,7 +1575,7 @@ function syncCursor(targetDir = null, wants = true) {
1575
1575
  const existingIsRemote = existingEntry && (existingEntry.type === 'http' || existingEntry.type === 'remote');
1576
1576
  const transportMismatch = registryWantsCommand && existingIsRemote;
1577
1577
  if (existingEntry && !hasPlaceholder && !transportMismatch) continue;
1578
- config.mcpServers[id] = buildClaudeMcpEntry(id, mcpDef, process.env);
1578
+ config.mcpServers[id] = buildClaudeMcpEntry(id, mcpDef, process.env, { host: "vscode" });
1579
1579
  }
1580
1580
  if (!DRY_RUN) {
1581
1581
  mkdirp(path.dirname(cursorMcpPath));