@bridge_gpt/mcp-server 0.2.31 → 0.2.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -21
- package/build/credential-store.js +302 -18
- package/build/doctor.js +80 -35
- package/build/index.js +17 -16
- package/build/install-bridge.js +1477 -440
- package/build/install-doctor.js +37 -4
- package/build/readme.generated.js +1 -1
- package/build/start-tickets.js +12 -1
- package/build/version.generated.js +1 -1
- package/package.json +3 -2
package/build/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var __defProp=Object.defineProperty;var __getOwnPropNames=Object.getOwnPropertyNames;var __esm=(fn,res,err)=>function(){if(err)throw err[0];try{return fn&&(res=(0,fn[__getOwnPropNames(fn)[0]])(fn=0)),res}catch(e){throw err=[e],e}};var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:!0})};var VERSION,init_version_generated=__esm({"src/version.generated.ts"(){"use strict";VERSION="0.2.
|
|
2
|
+
var __defProp=Object.defineProperty;var __getOwnPropNames=Object.getOwnPropertyNames;var __esm=(fn,res,err)=>function(){if(err)throw err[0];try{return fn&&(res=(0,fn[__getOwnPropNames(fn)[0]])(fn=0)),res}catch(e){throw err=[e],e}};var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:!0})};var VERSION,init_version_generated=__esm({"src/version.generated.ts"(){"use strict";VERSION="0.2.32"}});var COMMANDS,init_commands_generated=__esm({"src/commands.generated.ts"(){"use strict";COMMANDS={"bridge-research.md":`Run multi-source, fact-checked web research via Bridge API and save a cited report locally.
|
|
3
3
|
|
|
4
4
|
$ARGUMENTS
|
|
5
5
|
|
|
@@ -1557,7 +1557,7 @@ or failed (with the CLI error).
|
|
|
1557
1557
|
`}function appendLine(content,entry){let newline=detectNewline(content),separator=content.length>0&&!content.endsWith(`
|
|
1558
1558
|
`)?newline:"";return content+separator+entry+newline}async function ensureGitignored(cwd,filePath,deps){let gitignorePath=path4.join(cwd,".gitignore"),entry=path4.isAbsolute(filePath)?path4.relative(cwd,filePath):filePath,content="";try{content=await deps.readFile(gitignorePath)}catch{}hasExactLine(content,entry)||await deps.writeFile(gitignorePath,appendLine(content,entry))}async function resolveInfoExcludeLocation(worktreeRoot,deps){if(deps.runCommand&&deps.platform){let api=pathApiForPlatform(deps.platform),result=await deps.runCommand("git",["rev-parse","--git-path","info/exclude"],{cwd:worktreeRoot});if(result.exitCode!==0)throw new Error("Failed to resolve the worktree info/exclude path via 'git rev-parse --git-path'.");let raw=result.stdout.trim();if(raw.length===0)throw new Error("'git rev-parse --git-path info/exclude' returned an empty path.");let excludePath=api.isAbsolute(raw)?api.normalize(raw):api.resolve(worktreeRoot,raw);return{excludePath,infoDir:api.dirname(excludePath)}}let infoDir=path4.join(worktreeRoot,".git","info");return{excludePath:path4.join(infoDir,"exclude"),infoDir}}async function ensureGitInfoExcluded(worktreeRoot,relativePath,deps){let{excludePath,infoDir}=await resolveInfoExcludeLocation(worktreeRoot,deps),content="";try{content=await deps.readFile(excludePath)}catch{}hasExactLine(content,relativePath)||(await deps.mkdir(infoDir,{recursive:!0}),await deps.writeFile(excludePath,appendLine(content,relativePath)))}var init_git_ignore_utils=__esm({"src/git-ignore-utils.ts"(){"use strict"}});import path6 from"path";function getPrimaryCredentialStorePath(deps){let xdg=deps.env.XDG_CONFIG_HOME;return xdg&&xdg.trim().length>0?path6.join(xdg,"bridge","credentials.json"):path6.join(deps.homedir(),".config","bridge","credentials.json")}function getFallbackCredentialStorePath(deps){return path6.join(deps.homedir(),".bridge","credentials.json")}async function resolveCredentialStorePath(deps){let primaryPath=getPrimaryCredentialStorePath(deps),fallbackPath=getFallbackCredentialStorePath(deps);try{return await deps.stat(primaryPath),{found:!0,path:primaryPath,isPrimary:!0}}catch(err){if((err&&typeof err=="object"?err.code:void 0)!=="ENOENT")return{found:!0,path:primaryPath,isPrimary:!0}}try{return await deps.stat(fallbackPath),{found:!0,path:fallbackPath,isPrimary:!1}}catch{return{found:!1,primaryPath,fallbackPath}}}function warnIfInsecureCredentialFileMode(statResult,platform,filePath,stderr){platform!=="win32"&&(statResult.mode&63)!==0&&(stderr??(m=>process.stderr.write(`${m}
|
|
1559
1559
|
`)))(`Warning: credentials file ${filePath} is group/world-accessible; it should be mode 0600 (run: chmod 600 ${filePath}).`)}function parseCredentialStoreJson(text){let data;try{data=JSON.parse(text)}catch{return{ok:!1,error:"credentials file is not valid JSON"}}if(data===null||typeof data!="object"||Array.isArray(data))return{ok:!1,error:"credentials file must be a JSON object"};for(let[key,value]of Object.entries(data)){if(value===null||typeof value!="object"||Array.isArray(value))return{ok:!1,error:`credentials entry "${key}" must be an object of secret names to strings`};for(let[secretName,secretValue]of Object.entries(value))if(typeof secretValue!="string")return{ok:!1,error:`credentials entry "${key}" has a non-string value for "${secretName}"`}}return{ok:!0,value:data}}function collectEnvValues(env,requiredKeys){let values={},missing=[];for(let key of requiredKeys){let raw=(env[key]??"").trim();raw.length>0?values[key]=raw:missing.push(key)}return{values,missing}}async function resolveCredentialBundle(bundleKey,requiredKeys,deps){let primaryPath=getPrimaryCredentialStorePath(deps),fromEnv=collectEnvValues(deps.env,requiredKeys);if(fromEnv.missing.length===0)return{ok:!0,values:fromEnv.values,source:"env"};let resolution=await resolveCredentialStorePath(deps);if(!resolution.found)return{ok:!1,kind:"not-found",error:`No credentials found for "${bundleKey}". Set ${fromEnv.missing.join(", ")} in the environment, or add them under "${bundleKey}" in ${primaryPath}.`};try{let statResult=await deps.stat(resolution.path);warnIfInsecureCredentialFileMode(statResult,deps.platform,resolution.path,deps.stderr)}catch{}let raw;try{raw=await deps.readFile(resolution.path)}catch{return{ok:!1,kind:"read-error",error:`Unable to read credentials file at ${resolution.path}.`}}let parsed=parseCredentialStoreJson(raw);if(!parsed.ok)return{ok:!1,kind:"parse-error",error:`Invalid credentials file at ${resolution.path}: ${parsed.error}.`};let entry=parsed.value[bundleKey]??{},values={...fromEnv.values},stillMissing=[];for(let key of fromEnv.missing){let storeValue=typeof entry[key]=="string"?entry[key].trim():"";storeValue.length>0?values[key]=storeValue:stillMissing.push(key)}return stillMissing.length>0?{ok:!1,kind:"missing-key",error:`No usable value(s) for ${stillMissing.join(", ")} in "${bundleKey}". Add them under "${bundleKey}" in ${primaryPath}, or set them in the environment.`}:{ok:!0,values,source:"file"}}async function resolveBapiCredentials(repoName,deps){let result=await resolveCredentialBundle(`bapi:${repoName}`,["BAPI_API_KEY"],deps);return result.ok?{ok:!0,credentials:{apiKey:result.values.BAPI_API_KEY,source:result.source}}:{ok:!1,kind:result.kind,error:result.error}}function formatCredentialStoreJson(value){return`${JSON.stringify(value,null,2)}
|
|
1560
|
-
`}async function readCredentialStoreJsonIfPresent(filePath,deps){let raw;try{raw=await deps.readFile(filePath)}catch(err){return(err&&typeof err=="object"?err.code:void 0)==="ENOENT"?{state:"missing"}:{state:"error",kind:"read-error",error:`Unable to read credentials file at ${filePath}.`}}let parsed=parseCredentialStoreJson(raw);return parsed.ok?{state:"present",value:parsed.value}:{state:"error",kind:"parse-error",error:`Invalid credentials file at ${filePath}: ${parsed.error}.`}}async function mergeFallbackCredentialStoreOnFirstPrimaryWrite(deps){let fallbackPath=getFallbackCredentialStorePath(deps),fallback=await readCredentialStoreJsonIfPresent(fallbackPath,deps);return fallback.state==="present"?{base:{...fallback.value},migratedFallback:!0}:{base:{},migratedFallback:!1}}function defaultTempSuffix(){return tempSuffixCounter+=1,`${process.pid}.${tempSuffixCounter}`}function getCredentialStoreLockPath(deps){return`${getPrimaryCredentialStorePath(deps)}.lock`}async function acquireCredentialStoreLock(deps){let open3=deps.open;if(!open3)return{ok:!0,release:async()=>{}};let lockPath=getCredentialStoreLockPath(deps),isPosix=deps.platform!=="win32",now=deps.now??(()=>Date.now()),sleep3=deps.sleep??(ms=>new Promise(r=>setTimeout(r,ms))),release=async()=>{if(deps.unlink)try{await deps.unlink(lockPath)}catch{}},tryAcquire=async()=>{try{return await(await open3(lockPath,"wx",isPosix?384:void 0)).close(),{ok:!0}}catch(err){return{ok:!1,contended:(err&&typeof err=="object"?err.code:void 0)==="EEXIST"}}};try{await deps.mkdir(path6.dirname(lockPath),{recursive:!0})}catch{return{ok:!1,error:`Unable to prepare the credentials directory for ${lockPath}.`}}let deadline=now()+LOCK_TIMEOUT_MS;for(;;){let attempt=await tryAcquire();if(attempt.ok)return{ok:!0,release};if(!attempt.contended)return{ok:!1,error:`Unable to acquire the credentials lock at ${lockPath}.`};if(now()>=deadline)break;await sleep3(LOCK_POLL_INTERVAL_MS)}return await release(),(await tryAcquire()).ok?{ok:!0,release}:{ok:!1,error:`Timed out waiting for the credentials lock at ${lockPath} (another install may be running).`}}async function withCredentialStoreLock(deps,fn,onLockError){let lock=await acquireCredentialStoreLock(deps);if(!lock.ok)return onLockError(lock.error);try{return await fn()}finally{await lock.release()}}async function durablyReplaceCredentialStoreJson(primaryPath,value,deps){let open3=deps.open;if(!open3)return{ok:!1,kind:"durable-unavailable",error:`Cannot durably write ${primaryPath}: no file-handle primitive is available to fsync the write.`};let dir=path6.dirname(primaryPath),suffix=(deps.tempSuffix??defaultTempSuffix)(),tempPath=path6.join(dir,`${path6.basename(primaryPath)}.${suffix}.tmp`),json=formatCredentialStoreJson(value),isPosix=deps.platform!=="win32",handle;try{await deps.mkdir(dir,{recursive:!0}),handle=await open3(tempPath,"w",isPosix?384:void 0),await handle.writeFile(json,{encoding:"utf-8"}),await handle.sync(),await handle.close(),handle=void 0,isPosix&&await deps.chmod(tempPath,384),await deps.rename(tempPath,primaryPath)}catch{if(handle)try{await handle.close()}catch{}if(deps.unlink)try{await deps.unlink(tempPath)}catch{}return{ok:!1,kind:"write-error",error:`Failed to durably write the credentials file at ${primaryPath}.`}}try{let dirHandle=await open3(dir,"r");try{await dirHandle.sync()}finally{await dirHandle.close()}}catch{}return{ok:!0}}function getBootstrapPendingTarget(repoName){return`${BOOTSTRAP_PENDING_TARGET_PREFIX}${(repoName??"").trim()}`}function getBapiTarget(repoName){return`bapi:${(repoName??"").trim()}`}async function loadStoreForMutation(deps){let primaryPath=getPrimaryCredentialStorePath(deps),primary=await readCredentialStoreJsonIfPresent(primaryPath,deps);return primary.state==="error"?{ok:!1,kind:primary.kind,error:primary.error}:primary.state==="present"?{ok:!0,base:{...primary.value}}:{ok:!0,base:(await mergeFallbackCredentialStoreOnFirstPrimaryWrite(deps)).base}}function hasExistingBapiKey(store,repoName){let entry=store[getBapiTarget(repoName)];return!!entry&&typeof entry.BAPI_API_KEY=="string"&&entry.BAPI_API_KEY.trim().length>0}function readMatchingPending(store,repoName,inviteFingerprint){let entry=store[getBootstrapPendingTarget(repoName)];if(!entry)return null;let secret=entry[BOOTSTRAP_PENDING_SECRET_FIELD],fingerprint=entry[BOOTSTRAP_PENDING_FINGERPRINT_FIELD];return typeof secret!="string"||secret.trim().length===0||fingerprint!==inviteFingerprint?null:{keySecret:secret}}function hasConflictingPending(store,repoName,inviteFingerprint){let entry=store[getBootstrapPendingTarget(repoName)];if(!entry)return!1;let secret=entry[BOOTSTRAP_PENDING_SECRET_FIELD];return typeof secret!="string"||secret.trim().length===0?!1:entry[BOOTSTRAP_PENDING_FINGERPRINT_FIELD]!==inviteFingerprint}function pendingConflictError(target,primaryPath){return`A pending bootstrap-invite credential for a DIFFERENT invite already exists at ${target} in ${primaryPath}. It is the only proof that can replay that redemption, so it will not be overwritten. Complete that redemption first, or \u2014 only if you are certain its invite was never exchanged \u2014 remove the entry from the store by hand.`}async function prepareBootstrapPendingCredential(params,deps){let primaryPath=getPrimaryCredentialStorePath(deps),repoName=(params.repoName??"").trim(),fingerprint=(params.inviteFingerprint??"").trim(),target=getBootstrapPendingTarget(repoName);return repoName.length===0?{ok:!1,path:primaryPath,target,kind:"invalid-repo",error:"Cannot prepare a bootstrap-invite credential: a non-empty repo name is required."}:fingerprint.length===0?{ok:!1,path:primaryPath,target,kind:"invalid-fingerprint",error:"Cannot prepare a bootstrap-invite credential: the invite fingerprint was empty."}:withCredentialStoreLock(deps,async()=>{let loaded=await loadStoreForMutation(deps);if(!loaded.ok)return{ok:!1,path:primaryPath,target,kind:loaded.kind,error:loaded.error};let base=loaded.base;if(hasExistingBapiKey(base,repoName)&&!params.allowOverwriteExistingCredential)return{ok:!1,path:primaryPath,target:getBapiTarget(repoName),kind:"credential-conflict",error:`A credential already exists for ${getBapiTarget(repoName)} in ${primaryPath}.`};let existing=readMatchingPending(base,repoName,fingerprint);if(existing)return{ok:!0,path:primaryPath,target,keySecret:existing.keySecret,reused:!0};if(hasConflictingPending(base,repoName,fingerprint))return{ok:!1,path:primaryPath,target,kind:"pending-conflict",error:pendingConflictError(target,primaryPath)};let keySecret=params.generateKeySecret(),next={...base,[target]:{...base[target]??{},[BOOTSTRAP_PENDING_SECRET_FIELD]:keySecret,[BOOTSTRAP_PENDING_FINGERPRINT_FIELD]:fingerprint}},written=await durablyReplaceCredentialStoreJson(primaryPath,next,deps);return written.ok?{ok:!0,path:primaryPath,target,keySecret,reused:!1}:{ok:!1,path:primaryPath,target,kind:written.kind,error:written.error}},error=>({ok:!1,path:primaryPath,target,kind:"lock-error",error}))}async function repointBootstrapPendingCredential(params,deps){let primaryPath=getPrimaryCredentialStorePath(deps),fromRepo=(params.fromRepoName??"").trim(),toRepo=(params.toRepoName??"").trim(),fingerprint=(params.inviteFingerprint??"").trim(),target=getBootstrapPendingTarget(toRepo);return fromRepo.length===0||toRepo.length===0?{ok:!1,path:primaryPath,target,kind:"invalid-repo",error:"Cannot re-point a bootstrap-invite credential: a non-empty repo name is required."}:withCredentialStoreLock(deps,async()=>{let loaded=await loadStoreForMutation(deps);if(!loaded.ok)return{ok:!1,path:primaryPath,target,kind:loaded.kind,error:loaded.error};let base=loaded.base,pending=readMatchingPending(base,fromRepo,fingerprint);if(!pending)return{ok:!1,path:primaryPath,target,kind:"pending-missing",error:`No pending bootstrap-invite credential for ${getBootstrapPendingTarget(fromRepo)} in ${primaryPath}.`};if(toRepo===fromRepo)return{ok:!0,path:primaryPath,target,keySecret:pending.keySecret};if(hasExistingBapiKey(base,toRepo)&&!params.allowOverwriteExistingCredential)return{ok:!1,path:primaryPath,target:getBapiTarget(toRepo),kind:"credential-conflict",error:`A credential already exists for ${getBapiTarget(toRepo)} in ${primaryPath}.`};let destination=base[getBootstrapPendingTarget(toRepo)];if(hasConflictingPending(base,toRepo,fingerprint))return{ok:!1,path:primaryPath,target,kind:"pending-conflict",error:pendingConflictError(target,primaryPath)};let next={...base};delete next[getBootstrapPendingTarget(fromRepo)],next[target]={...destination??{},[BOOTSTRAP_PENDING_SECRET_FIELD]:pending.keySecret,[BOOTSTRAP_PENDING_FINGERPRINT_FIELD]:fingerprint};let written=await durablyReplaceCredentialStoreJson(primaryPath,next,deps);return written.ok?{ok:!0,path:primaryPath,target,keySecret:pending.keySecret}:{ok:!1,path:primaryPath,target,kind:written.kind,error:written.error}},error=>({ok:!1,path:primaryPath,target,kind:"lock-error",error}))}async function promoteBootstrapPendingCredential(params,deps){let primaryPath=getPrimaryCredentialStorePath(deps),repoName=(params.repoName??"").trim(),fingerprint=(params.inviteFingerprint??"").trim(),target=getBapiTarget(repoName);return repoName.length===0?{ok:!1,path:primaryPath,target,kind:"invalid-repo",error:"Cannot promote a bootstrap-invite credential: a non-empty repo name is required."}:withCredentialStoreLock(deps,async()=>{let loaded=await loadStoreForMutation(deps);if(!loaded.ok)return{ok:!1,path:primaryPath,target,kind:loaded.kind,error:loaded.error};let base=loaded.base,pending=readMatchingPending(base,repoName,fingerprint);if(!pending)return{ok:!1,path:primaryPath,target,kind:"pending-missing",error:`No pending bootstrap-invite credential for ${getBootstrapPendingTarget(repoName)} in ${primaryPath}.`};let hadKey=hasExistingBapiKey(base,repoName);if(hadKey&&!params.allowOverwriteExistingCredential)return{ok:!1,path:primaryPath,target,kind:"credential-conflict",error:`A credential already exists for ${target} in ${primaryPath}.`};let next={...base};delete next[getBootstrapPendingTarget(repoName)],next[target]={...base[target]??{},BAPI_API_KEY:pending.keySecret};let written=await durablyReplaceCredentialStoreJson(primaryPath,next,deps);return written.ok?{ok:!0,path:primaryPath,target,action:hadKey?"updated":"created"}:{ok:!1,path:primaryPath,target,kind:written.kind,error:written.error}},error=>({ok:!1,path:primaryPath,target,kind:"lock-error",error}))}async function upsertBapiCredential(repoName,apiKey,deps){let primaryPath=getPrimaryCredentialStorePath(deps),trimmedRepo=(repoName??"").trim(),trimmedKey=(apiKey??"").trim(),target=`bapi:${trimmedRepo}`;return trimmedRepo.length===0?{ok:!1,path:primaryPath,target:"bapi:",kind:"invalid-repo",error:"Cannot store BAPI_API_KEY: a non-empty repo name is required."}:trimmedKey.length===0?{ok:!1,path:primaryPath,target,kind:"invalid-key",error:`Cannot store BAPI_API_KEY for ${target}: the provided key was empty.`}:withCredentialStoreLock(deps,async()=>{let primary=await readCredentialStoreJsonIfPresent(primaryPath,deps),base,migratedFallback=!1;if(primary.state==="error")return{ok:!1,path:primaryPath,target,kind:primary.kind,error:primary.error};if(primary.state==="present")base={...primary.value};else{let seeded=await mergeFallbackCredentialStoreOnFirstPrimaryWrite(deps);base=seeded.base,migratedFallback=seeded.migratedFallback}let existingEntry=base[target],action=!!existingEntry&&typeof existingEntry.BAPI_API_KEY=="string"&&existingEntry.BAPI_API_KEY.length>0?"updated":"created",nextEntry={...existingEntry??{},BAPI_API_KEY:trimmedKey},next={...base,[target]:nextEntry},dir=path6.dirname(primaryPath),suffix=(deps.tempSuffix??defaultTempSuffix)(),tempPath=path6.join(dir,`${path6.basename(primaryPath)}.${suffix}.tmp`),json=formatCredentialStoreJson(next),isPosix=deps.platform!=="win32";try{await deps.mkdir(dir,{recursive:!0});let writeOptions=isPosix?{encoding:"utf-8",mode:384}:{encoding:"utf-8"};await deps.writeFile(tempPath,json,writeOptions),isPosix&&await deps.chmod(tempPath,384),await deps.rename(tempPath,primaryPath)}catch{if(deps.unlink)try{await deps.unlink(tempPath)}catch{}return{ok:!1,path:primaryPath,target,kind:"write-error",error:`Failed to write credentials file at ${primaryPath}.`}}return{ok:!0,path:primaryPath,target,action,migratedFallback}},error=>({ok:!1,path:primaryPath,target,kind:"lock-error",error}))}var tempSuffixCounter,LOCK_POLL_INTERVAL_MS,LOCK_TIMEOUT_MS,BOOTSTRAP_PENDING_TARGET_PREFIX,BOOTSTRAP_PENDING_SECRET_FIELD,BOOTSTRAP_PENDING_FINGERPRINT_FIELD,init_credential_store=__esm({"src/credential-store.ts"(){"use strict";tempSuffixCounter=0;LOCK_POLL_INTERVAL_MS=50,LOCK_TIMEOUT_MS=5e3;BOOTSTRAP_PENDING_TARGET_PREFIX="bootstrap-pending:",BOOTSTRAP_PENDING_SECRET_FIELD="BAPI_API_KEY",BOOTSTRAP_PENDING_FINGERPRINT_FIELD="BOOTSTRAP_INVITE_FINGERPRINT"}});async function resolveStartTicketsRepoName(deps){let fromEnv=deps.env.BAPI_REPO_NAME;if(typeof fromEnv=="string"&&fromEnv.trim().length>0)return fromEnv.trim();try{let result=await readBridgeConfig(deps.cwd,{readFile:deps.readFile});if(result.ok&&result.manifest.repoName)return result.manifest.repoName}catch{}return null}async function resolveRequiredStartTicketsRepoName(deps){let repoName=await resolveStartTicketsRepoName(deps);return repoName?{ok:!0,repoName}:{ok:!1,kind:"repo-missing",error:"could not resolve repo name (set BAPI_REPO_NAME or add a valid .bridge/config)"}}var init_start_tickets_repo=__esm({"src/start-tickets-repo.ts"(){"use strict";init_bridge_config()}});function getThirdPartyTargetDefinition(target){return THIRD_PARTY_TARGETS[target]}function validateThirdPartyTargetManifestEntry(entry){return entry.command===void 0||entry.command.trim().length===0?{ok:!1,error:`target '${entry.target}' requires a non-empty command`}:entry.args===void 0?{ok:!1,error:`target '${entry.target}' requires an args array`}:entry.secretBundle===void 0||entry.secretBundle.trim().length===0?{ok:!1,error:`target '${entry.target}' requires a non-empty secret_bundle`}:{ok:!0}}async function resolveThirdPartyTargetEnv(definition,secretBundle,deps){let result=await resolveCredentialBundle(secretBundle,definition.requiredEnvKeys,deps);if(!result.ok)return{ok:!1,error:result.error};let env={};for(let key of definition.requiredEnvKeys)env[key]=result.values[key];return{ok:!0,env}}var THIRD_PARTY_TARGETS,init_third_party_mcp_targets=__esm({"src/third-party-mcp-targets.ts"(){"use strict";init_credential_store();THIRD_PARTY_TARGETS={sfcc:{target:"sfcc",requiredEnvKeys:["SFCC_CLIENT_ID","SFCC_CLIENT_SECRET"]}}}});import path7 from"node:path";function buildMcpShimCommand(invocation,target,absoluteWorktreePath){return invocation.form==="absolute-build-path"?{command:invocation.nodeExecutable,args:[invocation.serverEntryPath,"mcp-invoke","--target",target,"--project-root",absoluteWorktreePath]}:{command:"npx",args:["-y",invocation.packageSpec,"mcp-invoke","--target",target,"--project-root",absoluteWorktreePath]}}function resolvePackageRootFromModuleUrl(moduleUrl){let pathname;try{pathname=decodeURIComponent(new URL(moduleUrl).pathname)}catch{return null}/^\/[A-Za-z]:/.test(pathname)&&(pathname=pathname.slice(1));let segments=pathname.split(/[\\/]/),markerIndex=-1;for(let i=segments.length-1;i>=0;i--)if(segments[i]==="src"||segments[i]==="build"){markerIndex=i;break}return markerIndex<=0?null:segments.slice(0,markerIndex).join("/")}function basenameAnySep(filePath){let segments=filePath.split(/[\\/]/);return segments[segments.length-1]??""}function resolveMcpShimInvocationForRuntime(deps){let nodeExecutable=deps.nodeExecutable??"node",packageRoot=resolvePackageRootFromModuleUrl(deps.moduleUrl);if(packageRoot){let candidate=`${packageRoot}/build/index.js`;if(deps.fileExists(candidate))return{form:"absolute-build-path",nodeExecutable,serverEntryPath:candidate}}let argv1=deps.argv1;return typeof argv1=="string"&&argv1.length>0&&path7.isAbsolute(argv1)&&basenameAnySep(argv1)==="index.js"&&deps.fileExists(argv1)?{form:"absolute-build-path",nodeExecutable,serverEntryPath:argv1}:{form:"npm-channel",command:"npx",packageSpec:deps.npmPackageSpec??DEFAULT_MCP_SERVER_NPM_CHANNEL_SPEC}}var DEFAULT_MCP_SERVER_NPM_CHANNEL_SPEC,init_mcp_server_invocation=__esm({"src/mcp-server-invocation.ts"(){"use strict";DEFAULT_MCP_SERVER_NPM_CHANNEL_SPEC="@bridge_gpt/mcp-server@latest"}});import path8 from"path";function pathApiForProvisioningPlatform(platform){return platform==="win32"?path8.win32:path8.posix}function normalizeWorktreePathForRegistration(worktreePath,deps){let api=pathApiForProvisioningPlatform(deps.platform);if(typeof worktreePath!="string"||worktreePath.trim().length===0)return{ok:!1,error:"worktree path is empty"};let resolved=api.isAbsolute(worktreePath)?api.normalize(worktreePath):api.resolve(deps.cwd,worktreePath);return api.isAbsolute(resolved)?{ok:!0,path:resolved}:{ok:!1,error:`unable to resolve an absolute worktree path from "${worktreePath}"`}}function serverNameForMcpTarget(target){return target==="bapi"?"bridge-api":target}function buildShimMcpServerEntry(target,absoluteWorktreePath,invocation){return buildMcpShimCommand(invocation,target,absoluteWorktreePath)}function buildMcpServerEntriesForManifest(manifest,absoluteWorktreePath,invocation){let entries={},warnings=[];for(let mcp of manifest.mcp){if(mcp.target==="bapi"){entries[serverNameForMcpTarget("bapi")]=buildShimMcpServerEntry("bapi",absoluteWorktreePath,invocation);continue}if(!getThirdPartyTargetDefinition(mcp.target)){warnings.push(`MCP target '${mcp.target}' is not a supported third-party target; skipping its registration.`);continue}let validation=validateThirdPartyTargetManifestEntry(mcp);if(!validation.ok){warnings.push(`MCP target '${mcp.target}' registration skipped: ${validation.error}.`);continue}entries[serverNameForMcpTarget(mcp.target)]=buildShimMcpServerEntry(mcp.target,absoluteWorktreePath,invocation)}return{entries,warnings,registrationForm:invocation.form}}function getWorktreeMcpRegistrationTargets(worktreePath,platform){let api=pathApiForProvisioningPlatform(platform);return[{filePath:api.join(worktreePath,".mcp.json"),topLevelKey:"mcpServers"},{filePath:api.join(worktreePath,".cursor","mcp.json"),topLevelKey:"mcpServers"}]}function claudeSettingsTargetForWorktree(worktreePath,platform){return pathApiForProvisioningPlatform(platform).join(worktreePath,".claude","settings.local.json")}function mergeEnabledMcpjsonServers(existing,serverNames){let result=existing&&typeof existing=="object"&&!Array.isArray(existing)?{...existing}:{},current=result.enabledMcpjsonServers,merged=Array.isArray(current)?current.filter(name=>typeof name=="string"):[];for(let name of serverNames)merged.includes(name)||merged.push(name);return result.enabledMcpjsonServers=merged,result}function mergeMcpRegistrations(existing,topLevelKey,entries){let result=existing&&typeof existing=="object"&&!Array.isArray(existing)?{...existing}:{},current=result[topLevelKey],servers=current&&typeof current=="object"&&!Array.isArray(current)?{...current}:{};for(let[name,entry]of Object.entries(entries))servers[name]=entry;return result[topLevelKey]=servers,result}async function writeMcpRegistrationFile(target,entries,deps){let api=pathApiForProvisioningPlatform(deps.platform),existing;try{let raw=await deps.readFile(target.filePath);try{existing=JSON.parse(raw)}catch{return{ok:!1,error:`existing ${target.filePath} contains malformed JSON; not overwriting`}}}catch(err){if((err&&typeof err=="object"?err.code:void 0)!=="ENOENT")return{ok:!1,error:`unable to read ${target.filePath}`}}let merged=mergeMcpRegistrations(existing,target.topLevelKey,entries);try{await deps.mkdir(api.dirname(target.filePath),{recursive:!0}),await deps.writeFile(target.filePath,`${JSON.stringify(merged,null,2)}
|
|
1560
|
+
`}async function readCredentialStoreJsonIfPresent(filePath,deps){let raw;try{raw=await deps.readFile(filePath)}catch(err){return(err&&typeof err=="object"?err.code:void 0)==="ENOENT"?{state:"missing"}:{state:"error",kind:"read-error",error:`Unable to read credentials file at ${filePath}.`}}let parsed=parseCredentialStoreJson(raw);return parsed.ok?{state:"present",value:parsed.value}:{state:"error",kind:"parse-error",error:`Invalid credentials file at ${filePath}: ${parsed.error}.`}}async function mergeFallbackCredentialStoreOnFirstPrimaryWrite(deps){let fallbackPath=getFallbackCredentialStorePath(deps),fallback=await readCredentialStoreJsonIfPresent(fallbackPath,deps);return fallback.state==="present"?{base:{...fallback.value},migratedFallback:!0}:{base:{},migratedFallback:!1}}function defaultTempSuffix(){return tempSuffixCounter+=1,`${process.pid}.${tempSuffixCounter}`}function getCredentialStoreLockPath(deps){return`${getPrimaryCredentialStorePath(deps)}.lock`}async function acquireCredentialStoreLock(deps){let open3=deps.open;if(!open3)return{ok:!0,release:async()=>{}};let lockPath=getCredentialStoreLockPath(deps),isPosix=deps.platform!=="win32",now=deps.now??(()=>Date.now()),sleep3=deps.sleep??(ms=>new Promise(r=>setTimeout(r,ms))),release=async()=>{if(deps.unlink)try{await deps.unlink(lockPath)}catch{}},tryAcquire=async()=>{try{return await(await open3(lockPath,"wx",isPosix?384:void 0)).close(),{ok:!0}}catch(err){return{ok:!1,contended:(err&&typeof err=="object"?err.code:void 0)==="EEXIST"}}};try{await deps.mkdir(path6.dirname(lockPath),{recursive:!0})}catch{return{ok:!1,error:`Unable to prepare the credentials directory for ${lockPath}.`}}let deadline=now()+LOCK_TIMEOUT_MS;for(;;){let attempt=await tryAcquire();if(attempt.ok)return{ok:!0,release};if(!attempt.contended)return{ok:!1,error:`Unable to acquire the credentials lock at ${lockPath}.`};if(now()>=deadline)break;await sleep3(LOCK_POLL_INTERVAL_MS)}return await release(),(await tryAcquire()).ok?{ok:!0,release}:{ok:!1,error:`Timed out waiting for the credentials lock at ${lockPath} (another install may be running).`}}async function withCredentialStoreLock(deps,fn,onLockError){let lock=await acquireCredentialStoreLock(deps);if(!lock.ok)return onLockError(lock.error);try{return await fn()}finally{await lock.release()}}async function durablyReplaceCredentialStoreJson(primaryPath,value,deps){let open3=deps.open;if(!open3)return{ok:!1,kind:"durable-unavailable",error:`Cannot durably write ${primaryPath}: no file-handle primitive is available to fsync the write.`};let dir=path6.dirname(primaryPath),suffix=(deps.tempSuffix??defaultTempSuffix)(),tempPath=path6.join(dir,`${path6.basename(primaryPath)}.${suffix}.tmp`),json=formatCredentialStoreJson(value),isPosix=deps.platform!=="win32",handle;try{await deps.mkdir(dir,{recursive:!0}),handle=await open3(tempPath,"w",isPosix?384:void 0),await handle.writeFile(json,{encoding:"utf-8"}),await handle.sync(),await handle.close(),handle=void 0,isPosix&&await deps.chmod(tempPath,384),await deps.rename(tempPath,primaryPath)}catch{if(handle)try{await handle.close()}catch{}if(deps.unlink)try{await deps.unlink(tempPath)}catch{}return{ok:!1,kind:"write-error",error:`Failed to durably write the credentials file at ${primaryPath}.`}}try{let dirHandle=await open3(dir,"r");try{await dirHandle.sync()}finally{await dirHandle.close()}}catch{}return{ok:!0}}function getBootstrapPendingTarget(repoName){return`${BOOTSTRAP_PENDING_TARGET_PREFIX}${(repoName??"").trim()}`}function getBapiTarget(repoName){return`bapi:${(repoName??"").trim()}`}async function loadStoreForMutation(deps){let primaryPath=getPrimaryCredentialStorePath(deps),primary=await readCredentialStoreJsonIfPresent(primaryPath,deps);return primary.state==="error"?{ok:!1,kind:primary.kind,error:primary.error}:primary.state==="present"?{ok:!0,base:{...primary.value}}:{ok:!0,base:(await mergeFallbackCredentialStoreOnFirstPrimaryWrite(deps)).base}}function hasExistingBapiKey(store,repoName){let entry=store[getBapiTarget(repoName)];return!!entry&&typeof entry.BAPI_API_KEY=="string"&&entry.BAPI_API_KEY.trim().length>0}function readPendingField(entry,field){let value=entry?.[field];return typeof value=="string"?value.trim():""}function readMatchingPending(store,repoName,inviteFingerprint){let entry=store[getBootstrapPendingTarget(repoName)];if(!entry)return null;let secret=entry[BOOTSTRAP_PENDING_SECRET_FIELD],fingerprint=entry[BOOTSTRAP_PENDING_FINGERPRINT_FIELD];if(typeof secret!="string"||secret.trim().length===0||fingerprint!==inviteFingerprint)return null;let replayToken=readPendingField(entry,BOOTSTRAP_PENDING_REPLAY_TOKEN_FIELD);return replayToken.length>0?{keySecret:secret,replayToken}:{keySecret:secret}}function buildPendingEntry(existing,keySecret,inviteFingerprint,replayToken){let next={...existing??{},[BOOTSTRAP_PENDING_SECRET_FIELD]:keySecret,[BOOTSTRAP_PENDING_FINGERPRINT_FIELD]:inviteFingerprint};return replayToken&&replayToken.length>0?next[BOOTSTRAP_PENDING_REPLAY_TOKEN_FIELD]=replayToken:delete next[BOOTSTRAP_PENDING_REPLAY_TOKEN_FIELD],next}function hasConflictingPending(store,repoName,inviteFingerprint){let entry=store[getBootstrapPendingTarget(repoName)];if(!entry)return!1;let secret=entry[BOOTSTRAP_PENDING_SECRET_FIELD];return typeof secret!="string"||secret.trim().length===0?!1:entry[BOOTSTRAP_PENDING_FINGERPRINT_FIELD]!==inviteFingerprint}function storedPendingIsSelfServe(store,repoName){let entry=store[getBootstrapPendingTarget(repoName)];return readPendingField(entry,BOOTSTRAP_PENDING_REPLAY_TOKEN_FIELD).length>0}function pendingConflictError(target,primaryPath,storedIsSelfServe){return storedIsSelfServe?`A pending self-serve signup for a DIFFERENT invite already exists at ${target} in ${primaryPath}. It is the only proof that can replay that signup, so it will not be overwritten. Re-run install-bridge and choose the email option \u2014 it will resume that signup automatically. Do NOT remove the entry by hand.`:`A pending bootstrap-invite credential for a DIFFERENT invite already exists at ${target} in ${primaryPath}. It is the only proof that can replay that redemption, so it will not be overwritten. Complete that redemption first, or \u2014 only if you are certain its invite was never exchanged \u2014 remove the entry from the store by hand.`}async function prepareBootstrapPendingCredential(params,deps){let primaryPath=getPrimaryCredentialStorePath(deps),repoName=(params.repoName??"").trim(),fingerprint=(params.inviteFingerprint??"").trim(),target=getBootstrapPendingTarget(repoName),replayToken=params.selfServeReplayToken===void 0?void 0:params.selfServeReplayToken.trim();return repoName.length===0?{ok:!1,path:primaryPath,target,kind:"invalid-repo",error:"Cannot prepare a bootstrap-invite credential: a non-empty repo name is required."}:fingerprint.length===0?{ok:!1,path:primaryPath,target,kind:"invalid-fingerprint",error:"Cannot prepare a bootstrap-invite credential: the invite fingerprint was empty."}:replayToken!==void 0&&replayToken.length===0?{ok:!1,path:primaryPath,target,kind:"invalid-replay-token",error:"Cannot prepare a self-serve bootstrap credential: the supplied replay material was empty."}:withCredentialStoreLock(deps,async()=>{let loaded=await loadStoreForMutation(deps);if(!loaded.ok)return{ok:!1,path:primaryPath,target,kind:loaded.kind,error:loaded.error};let base=loaded.base;if(hasExistingBapiKey(base,repoName)&&!params.allowOverwriteExistingCredential)return{ok:!1,path:primaryPath,target:getBapiTarget(repoName),kind:"credential-conflict",error:`A credential already exists for ${getBapiTarget(repoName)} in ${primaryPath}.`};let existing=readMatchingPending(base,repoName,fingerprint);if(existing&&(replayToken===void 0||existing.replayToken===replayToken))return{ok:!0,path:primaryPath,target,keySecret:existing.keySecret,reused:!0};if(existing){let upgraded={...base,[target]:buildPendingEntry(base[target],existing.keySecret,fingerprint,replayToken)},rewritten=await durablyReplaceCredentialStoreJson(primaryPath,upgraded,deps);return rewritten.ok?{ok:!0,path:primaryPath,target,keySecret:existing.keySecret,reused:!0}:{ok:!1,path:primaryPath,target,kind:rewritten.kind,error:rewritten.error}}if(hasConflictingPending(base,repoName,fingerprint))return{ok:!1,path:primaryPath,target,kind:"pending-conflict",error:pendingConflictError(target,primaryPath,storedPendingIsSelfServe(base,repoName))};let keySecret=params.generateKeySecret(),next={...base,[target]:buildPendingEntry(base[target],keySecret,fingerprint,replayToken)},written=await durablyReplaceCredentialStoreJson(primaryPath,next,deps);return written.ok?{ok:!0,path:primaryPath,target,keySecret,reused:!1}:{ok:!1,path:primaryPath,target,kind:written.kind,error:written.error}},error=>({ok:!1,path:primaryPath,target,kind:"lock-error",error}))}async function repointBootstrapPendingCredential(params,deps){let primaryPath=getPrimaryCredentialStorePath(deps),fromRepo=(params.fromRepoName??"").trim(),toRepo=(params.toRepoName??"").trim(),fingerprint=(params.inviteFingerprint??"").trim(),target=getBootstrapPendingTarget(toRepo);return fromRepo.length===0||toRepo.length===0?{ok:!1,path:primaryPath,target,kind:"invalid-repo",error:"Cannot re-point a bootstrap-invite credential: a non-empty repo name is required."}:withCredentialStoreLock(deps,async()=>{let loaded=await loadStoreForMutation(deps);if(!loaded.ok)return{ok:!1,path:primaryPath,target,kind:loaded.kind,error:loaded.error};let base=loaded.base,pending=readMatchingPending(base,fromRepo,fingerprint);if(!pending)return{ok:!1,path:primaryPath,target,kind:"pending-missing",error:`No pending bootstrap-invite credential for ${getBootstrapPendingTarget(fromRepo)} in ${primaryPath}.`};if(toRepo===fromRepo)return{ok:!0,path:primaryPath,target,keySecret:pending.keySecret};if(hasExistingBapiKey(base,toRepo)&&!params.allowOverwriteExistingCredential)return{ok:!1,path:primaryPath,target:getBapiTarget(toRepo),kind:"credential-conflict",error:`A credential already exists for ${getBapiTarget(toRepo)} in ${primaryPath}.`};let destination=base[getBootstrapPendingTarget(toRepo)];if(hasConflictingPending(base,toRepo,fingerprint))return{ok:!1,path:primaryPath,target,kind:"pending-conflict",error:pendingConflictError(target,primaryPath,storedPendingIsSelfServe(base,toRepo))};let next={...base};delete next[getBootstrapPendingTarget(fromRepo)],next[target]=buildPendingEntry(destination,pending.keySecret,fingerprint,pending.replayToken);let written=await durablyReplaceCredentialStoreJson(primaryPath,next,deps);return written.ok?{ok:!0,path:primaryPath,target,keySecret:pending.keySecret}:{ok:!1,path:primaryPath,target,kind:written.kind,error:written.error}},error=>({ok:!1,path:primaryPath,target,kind:"lock-error",error}))}async function promoteBootstrapPendingCredential(params,deps){let primaryPath=getPrimaryCredentialStorePath(deps),repoName=(params.repoName??"").trim(),fingerprint=(params.inviteFingerprint??"").trim(),target=getBapiTarget(repoName);return repoName.length===0?{ok:!1,path:primaryPath,target,kind:"invalid-repo",error:"Cannot promote a bootstrap-invite credential: a non-empty repo name is required."}:withCredentialStoreLock(deps,async()=>{let loaded=await loadStoreForMutation(deps);if(!loaded.ok)return{ok:!1,path:primaryPath,target,kind:loaded.kind,error:loaded.error};let base=loaded.base,pending=readMatchingPending(base,repoName,fingerprint);if(!pending)return{ok:!1,path:primaryPath,target,kind:"pending-missing",error:`No pending bootstrap-invite credential for ${getBootstrapPendingTarget(repoName)} in ${primaryPath}.`};let hadKey=hasExistingBapiKey(base,repoName);if(hadKey&&!params.allowOverwriteExistingCredential)return{ok:!1,path:primaryPath,target,kind:"credential-conflict",error:`A credential already exists for ${target} in ${primaryPath}.`};let next={...base};delete next[getBootstrapPendingTarget(repoName)],next[target]={...base[target]??{},BAPI_API_KEY:pending.keySecret};let written=await durablyReplaceCredentialStoreJson(primaryPath,next,deps);return written.ok?{ok:!0,path:primaryPath,target,action:hadKey?"updated":"created"}:{ok:!1,path:primaryPath,target,kind:written.kind,error:written.error}},error=>({ok:!1,path:primaryPath,target,kind:"lock-error",error}))}async function lookupSelfServeBootstrapPendingCredential(params,deps){let primaryPath=getPrimaryCredentialStorePath(deps),repoName=(params.repoName??"").trim(),target=getBootstrapPendingTarget(repoName);return repoName.length===0?{ok:!1,path:primaryPath,target,kind:"invalid-repo",error:"Cannot look up a bootstrap-invite credential: a non-empty repo name is required."}:withCredentialStoreLock(deps,async()=>{let loaded=await loadStoreForMutation(deps);if(!loaded.ok)return{ok:!1,path:primaryPath,target,kind:loaded.kind,error:loaded.error};let entry=loaded.base[target],keySecret=readPendingField(entry,BOOTSTRAP_PENDING_SECRET_FIELD),fingerprint=readPendingField(entry,BOOTSTRAP_PENDING_FINGERPRINT_FIELD),replayToken=readPendingField(entry,BOOTSTRAP_PENDING_REPLAY_TOKEN_FIELD);return replayToken.length===0?keySecret.length===0?{ok:!0,state:"none",path:primaryPath,target}:{ok:!1,path:primaryPath,target,kind:"pending-not-self-serve",error:`A pending bootstrap-invite credential already exists at ${target} in ${primaryPath}. It belongs to an invite redemption, not a self-serve signup, so this run will not touch it. Finish that redemption by re-running install-bridge with the same invite.`}:keySecret.length===0||fingerprint.length===0?{ok:!1,path:primaryPath,target,kind:"pending-malformed",error:`The pending self-serve record at ${target} in ${primaryPath} is incomplete, so it cannot be replayed and will not be discarded automatically (it may correspond to a key that was already created). Ask your Bridge API operator to recover it.`}:{ok:!0,state:"resumable",path:primaryPath,target,keySecret,replayToken,inviteFingerprint:fingerprint}},error=>({ok:!1,path:primaryPath,target,kind:"lock-error",error}))}async function discardBootstrapPendingCredential(params,deps){let primaryPath=getPrimaryCredentialStorePath(deps),repoName=(params.repoName??"").trim(),fingerprint=(params.inviteFingerprint??"").trim(),target=getBootstrapPendingTarget(repoName);return repoName.length===0?{ok:!1,path:primaryPath,target,kind:"invalid-repo",error:"Cannot discard a bootstrap-invite credential: a non-empty repo name is required."}:fingerprint.length===0?{ok:!1,path:primaryPath,target,kind:"invalid-fingerprint",error:"Cannot discard a bootstrap-invite credential: the invite fingerprint was empty."}:withCredentialStoreLock(deps,async()=>{let loaded=await loadStoreForMutation(deps);if(!loaded.ok)return{ok:!1,path:primaryPath,target,kind:loaded.kind,error:loaded.error};let base=loaded.base,pending=readMatchingPending(base,repoName,fingerprint);if(!pending)return{ok:!1,path:primaryPath,target,kind:"pending-missing",error:`No matching pending bootstrap-invite credential for ${target} in ${primaryPath}.`};if(!pending.replayToken)return{ok:!1,path:primaryPath,target,kind:"pending-not-self-serve",error:`The pending record at ${target} in ${primaryPath} carries no self-serve replay material, so it will not be discarded automatically.`};let next={...base};delete next[target];let written=await durablyReplaceCredentialStoreJson(primaryPath,next,deps);return written.ok?{ok:!0,path:primaryPath,target}:{ok:!1,path:primaryPath,target,kind:written.kind,error:written.error}},error=>({ok:!1,path:primaryPath,target,kind:"lock-error",error}))}async function upsertBapiCredential(repoName,apiKey,deps){let primaryPath=getPrimaryCredentialStorePath(deps),trimmedRepo=(repoName??"").trim(),trimmedKey=(apiKey??"").trim(),target=`bapi:${trimmedRepo}`;return trimmedRepo.length===0?{ok:!1,path:primaryPath,target:"bapi:",kind:"invalid-repo",error:"Cannot store BAPI_API_KEY: a non-empty repo name is required."}:trimmedKey.length===0?{ok:!1,path:primaryPath,target,kind:"invalid-key",error:`Cannot store BAPI_API_KEY for ${target}: the provided key was empty.`}:withCredentialStoreLock(deps,async()=>{let primary=await readCredentialStoreJsonIfPresent(primaryPath,deps),base,migratedFallback=!1;if(primary.state==="error")return{ok:!1,path:primaryPath,target,kind:primary.kind,error:primary.error};if(primary.state==="present")base={...primary.value};else{let seeded=await mergeFallbackCredentialStoreOnFirstPrimaryWrite(deps);base=seeded.base,migratedFallback=seeded.migratedFallback}let existingEntry=base[target],action=!!existingEntry&&typeof existingEntry.BAPI_API_KEY=="string"&&existingEntry.BAPI_API_KEY.length>0?"updated":"created",nextEntry={...existingEntry??{},BAPI_API_KEY:trimmedKey},next={...base,[target]:nextEntry},dir=path6.dirname(primaryPath),suffix=(deps.tempSuffix??defaultTempSuffix)(),tempPath=path6.join(dir,`${path6.basename(primaryPath)}.${suffix}.tmp`),json=formatCredentialStoreJson(next),isPosix=deps.platform!=="win32";try{await deps.mkdir(dir,{recursive:!0});let writeOptions=isPosix?{encoding:"utf-8",mode:384}:{encoding:"utf-8"};await deps.writeFile(tempPath,json,writeOptions),isPosix&&await deps.chmod(tempPath,384),await deps.rename(tempPath,primaryPath)}catch{if(deps.unlink)try{await deps.unlink(tempPath)}catch{}return{ok:!1,path:primaryPath,target,kind:"write-error",error:`Failed to write credentials file at ${primaryPath}.`}}return{ok:!0,path:primaryPath,target,action,migratedFallback}},error=>({ok:!1,path:primaryPath,target,kind:"lock-error",error}))}var tempSuffixCounter,LOCK_POLL_INTERVAL_MS,LOCK_TIMEOUT_MS,BOOTSTRAP_PENDING_TARGET_PREFIX,BOOTSTRAP_PENDING_SECRET_FIELD,BOOTSTRAP_PENDING_FINGERPRINT_FIELD,BOOTSTRAP_PENDING_REPLAY_TOKEN_FIELD,init_credential_store=__esm({"src/credential-store.ts"(){"use strict";tempSuffixCounter=0;LOCK_POLL_INTERVAL_MS=50,LOCK_TIMEOUT_MS=5e3;BOOTSTRAP_PENDING_TARGET_PREFIX="bootstrap-pending:",BOOTSTRAP_PENDING_SECRET_FIELD="BAPI_API_KEY",BOOTSTRAP_PENDING_FINGERPRINT_FIELD="BOOTSTRAP_INVITE_FINGERPRINT",BOOTSTRAP_PENDING_REPLAY_TOKEN_FIELD="BOOTSTRAP_SELF_SERVE_INVITE"}});async function resolveStartTicketsRepoName(deps){let fromEnv=deps.env.BAPI_REPO_NAME;if(typeof fromEnv=="string"&&fromEnv.trim().length>0)return fromEnv.trim();try{let result=await readBridgeConfig(deps.cwd,{readFile:deps.readFile});if(result.ok&&result.manifest.repoName)return result.manifest.repoName}catch{}return null}async function resolveRequiredStartTicketsRepoName(deps){let repoName=await resolveStartTicketsRepoName(deps);return repoName?{ok:!0,repoName}:{ok:!1,kind:"repo-missing",error:"could not resolve repo name (set BAPI_REPO_NAME or add a valid .bridge/config)"}}var init_start_tickets_repo=__esm({"src/start-tickets-repo.ts"(){"use strict";init_bridge_config()}});function getThirdPartyTargetDefinition(target){return THIRD_PARTY_TARGETS[target]}function validateThirdPartyTargetManifestEntry(entry){return entry.command===void 0||entry.command.trim().length===0?{ok:!1,error:`target '${entry.target}' requires a non-empty command`}:entry.args===void 0?{ok:!1,error:`target '${entry.target}' requires an args array`}:entry.secretBundle===void 0||entry.secretBundle.trim().length===0?{ok:!1,error:`target '${entry.target}' requires a non-empty secret_bundle`}:{ok:!0}}async function resolveThirdPartyTargetEnv(definition,secretBundle,deps){let result=await resolveCredentialBundle(secretBundle,definition.requiredEnvKeys,deps);if(!result.ok)return{ok:!1,error:result.error};let env={};for(let key of definition.requiredEnvKeys)env[key]=result.values[key];return{ok:!0,env}}var THIRD_PARTY_TARGETS,init_third_party_mcp_targets=__esm({"src/third-party-mcp-targets.ts"(){"use strict";init_credential_store();THIRD_PARTY_TARGETS={sfcc:{target:"sfcc",requiredEnvKeys:["SFCC_CLIENT_ID","SFCC_CLIENT_SECRET"]}}}});import path7 from"node:path";function buildMcpShimCommand(invocation,target,absoluteWorktreePath){return invocation.form==="absolute-build-path"?{command:invocation.nodeExecutable,args:[invocation.serverEntryPath,"mcp-invoke","--target",target,"--project-root",absoluteWorktreePath]}:{command:"npx",args:["-y",invocation.packageSpec,"mcp-invoke","--target",target,"--project-root",absoluteWorktreePath]}}function resolvePackageRootFromModuleUrl(moduleUrl){let pathname;try{pathname=decodeURIComponent(new URL(moduleUrl).pathname)}catch{return null}/^\/[A-Za-z]:/.test(pathname)&&(pathname=pathname.slice(1));let segments=pathname.split(/[\\/]/),markerIndex=-1;for(let i=segments.length-1;i>=0;i--)if(segments[i]==="src"||segments[i]==="build"){markerIndex=i;break}return markerIndex<=0?null:segments.slice(0,markerIndex).join("/")}function basenameAnySep(filePath){let segments=filePath.split(/[\\/]/);return segments[segments.length-1]??""}function resolveMcpShimInvocationForRuntime(deps){let nodeExecutable=deps.nodeExecutable??"node",packageRoot=resolvePackageRootFromModuleUrl(deps.moduleUrl);if(packageRoot){let candidate=`${packageRoot}/build/index.js`;if(deps.fileExists(candidate))return{form:"absolute-build-path",nodeExecutable,serverEntryPath:candidate}}let argv1=deps.argv1;return typeof argv1=="string"&&argv1.length>0&&path7.isAbsolute(argv1)&&basenameAnySep(argv1)==="index.js"&&deps.fileExists(argv1)?{form:"absolute-build-path",nodeExecutable,serverEntryPath:argv1}:{form:"npm-channel",command:"npx",packageSpec:deps.npmPackageSpec??DEFAULT_MCP_SERVER_NPM_CHANNEL_SPEC}}var DEFAULT_MCP_SERVER_NPM_CHANNEL_SPEC,init_mcp_server_invocation=__esm({"src/mcp-server-invocation.ts"(){"use strict";DEFAULT_MCP_SERVER_NPM_CHANNEL_SPEC="@bridge_gpt/mcp-server@latest"}});import path8 from"path";function pathApiForProvisioningPlatform(platform){return platform==="win32"?path8.win32:path8.posix}function normalizeWorktreePathForRegistration(worktreePath,deps){let api=pathApiForProvisioningPlatform(deps.platform);if(typeof worktreePath!="string"||worktreePath.trim().length===0)return{ok:!1,error:"worktree path is empty"};let resolved=api.isAbsolute(worktreePath)?api.normalize(worktreePath):api.resolve(deps.cwd,worktreePath);return api.isAbsolute(resolved)?{ok:!0,path:resolved}:{ok:!1,error:`unable to resolve an absolute worktree path from "${worktreePath}"`}}function serverNameForMcpTarget(target){return target==="bapi"?"bridge-api":target}function buildShimMcpServerEntry(target,absoluteWorktreePath,invocation){return buildMcpShimCommand(invocation,target,absoluteWorktreePath)}function buildMcpServerEntriesForManifest(manifest,absoluteWorktreePath,invocation){let entries={},warnings=[];for(let mcp of manifest.mcp){if(mcp.target==="bapi"){entries[serverNameForMcpTarget("bapi")]=buildShimMcpServerEntry("bapi",absoluteWorktreePath,invocation);continue}if(!getThirdPartyTargetDefinition(mcp.target)){warnings.push(`MCP target '${mcp.target}' is not a supported third-party target; skipping its registration.`);continue}let validation=validateThirdPartyTargetManifestEntry(mcp);if(!validation.ok){warnings.push(`MCP target '${mcp.target}' registration skipped: ${validation.error}.`);continue}entries[serverNameForMcpTarget(mcp.target)]=buildShimMcpServerEntry(mcp.target,absoluteWorktreePath,invocation)}return{entries,warnings,registrationForm:invocation.form}}function getWorktreeMcpRegistrationTargets(worktreePath,platform){let api=pathApiForProvisioningPlatform(platform);return[{filePath:api.join(worktreePath,".mcp.json"),topLevelKey:"mcpServers"},{filePath:api.join(worktreePath,".cursor","mcp.json"),topLevelKey:"mcpServers"}]}function claudeSettingsTargetForWorktree(worktreePath,platform){return pathApiForProvisioningPlatform(platform).join(worktreePath,".claude","settings.local.json")}function mergeEnabledMcpjsonServers(existing,serverNames){let result=existing&&typeof existing=="object"&&!Array.isArray(existing)?{...existing}:{},current=result.enabledMcpjsonServers,merged=Array.isArray(current)?current.filter(name=>typeof name=="string"):[];for(let name of serverNames)merged.includes(name)||merged.push(name);return result.enabledMcpjsonServers=merged,result}function mergeMcpRegistrations(existing,topLevelKey,entries){let result=existing&&typeof existing=="object"&&!Array.isArray(existing)?{...existing}:{},current=result[topLevelKey],servers=current&&typeof current=="object"&&!Array.isArray(current)?{...current}:{};for(let[name,entry]of Object.entries(entries))servers[name]=entry;return result[topLevelKey]=servers,result}async function writeMcpRegistrationFile(target,entries,deps){let api=pathApiForProvisioningPlatform(deps.platform),existing;try{let raw=await deps.readFile(target.filePath);try{existing=JSON.parse(raw)}catch{return{ok:!1,error:`existing ${target.filePath} contains malformed JSON; not overwriting`}}}catch(err){if((err&&typeof err=="object"?err.code:void 0)!=="ENOENT")return{ok:!1,error:`unable to read ${target.filePath}`}}let merged=mergeMcpRegistrations(existing,target.topLevelKey,entries);try{await deps.mkdir(api.dirname(target.filePath),{recursive:!0}),await deps.writeFile(target.filePath,`${JSON.stringify(merged,null,2)}
|
|
1561
1561
|
`)}catch{return{ok:!1,error:`failed to write ${target.filePath}`}}return{ok:!0}}async function writeClaudeServerTrustSettings(worktreePath,serverNames,deps){let api=pathApiForProvisioningPlatform(deps.platform),filePath=claudeSettingsTargetForWorktree(worktreePath,deps.platform),existing;try{let raw=await deps.readFile(filePath);try{existing=JSON.parse(raw)}catch{return{ok:!1,error:`existing ${filePath} contains malformed JSON; not overwriting`}}}catch(err){if((err&&typeof err=="object"?err.code:void 0)!=="ENOENT")return{ok:!1,error:`unable to read ${filePath}`}}let merged=mergeEnabledMcpjsonServers(existing,serverNames);try{await deps.mkdir(api.dirname(filePath),{recursive:!0}),await deps.writeFile(filePath,`${JSON.stringify(merged,null,2)}
|
|
1562
1562
|
`)}catch{return{ok:!1,error:`failed to write ${filePath}`}}return{ok:!0}}function withWarning(row,warning){return{...row,warnings:[...row.warnings??[],warning]}}function withWarnings(row,warnings){return warnings.length===0?row:{...row,warnings:[...row.warnings??[],...warnings]}}async function provisionMcpRegistrationForWorktree(row,deps){if(row.status!=="created"||!row.path)return row;let read=await readBridgeConfig(row.path,{readFile:deps.readFile});if(!read.ok)return read.kind==="missing"?withWarning(row,"MCP provisioning skipped: .bridge/config is missing in the worktree."):withWarning(row,"MCP provisioning skipped: .bridge/config is malformed or invalid.");let normalized=normalizeWorktreePathForRegistration(row.path,deps);if(!normalized.ok)return{...row,status:"spawn-failed",error:`MCP provisioning failed: ${normalized.error}`};let built=buildMcpServerEntriesForManifest(read.manifest,normalized.path,deps.mcpServerInvocation);if(Object.keys(built.entries).length===0)return withWarnings(withWarning(row,"MCP registration skipped: .bridge/config declares no supported MCP targets."),built.warnings);let targets=getWorktreeMcpRegistrationTargets(normalized.path,deps.platform);for(let target of targets){let result2=await writeMcpRegistrationFile(target,built.entries,deps);if(!result2.ok)return{...row,status:"spawn-failed",error:`MCP provisioning failed: ${result2.error}`}}let result={...row,mcpRegistrationForm:built.registrationForm};result=withWarnings(result,built.warnings),built.registrationForm!=="absolute-build-path"&&(result=withWarning(result,"MCP registration used npm-channel fallback because an on-disk build entry was not resolvable."));let serverNames=Object.keys(built.entries),trust=await writeClaudeServerTrustSettings(normalized.path,serverNames,deps);return trust.ok||(result=withWarning(result,`Claude MCP trust pre-approval skipped: ${trust.error}`)),result}async function provisionMcpRegistrationsForCreatedWorktrees(rows,deps){let out=[];for(let row of rows)out.push(await provisionMcpRegistrationForWorktree(row,deps));return out}var init_mcp_provisioning=__esm({"src/mcp-provisioning.ts"(){"use strict";init_bridge_config();init_third_party_mcp_targets();init_mcp_server_invocation()}});import path9 from"path";function pathApiForCommandProvisioningPlatform(platform){return platform==="win32"?path9.win32:path9.posix}function isEnoentError(err){return typeof err=="object"&&err!==null&&err.code==="ENOENT"}async function provisionCommandsForWorktree(worktreeRoot,deps){let api=pathApiForCommandProvisioningPlatform(deps.platform),normalizedRoot=api.isAbsolute(worktreeRoot)?api.normalize(worktreeRoot):api.resolve(deps.cwd,worktreeRoot),commandsDir=api.join(normalizedRoot,".claude","commands"),entries=Object.entries(COMMANDS);if(entries.length===0)return{ok:!1,error:EMPTY_BUNDLE_ERROR};let fillError=null,missing=[];for(let[filename,content]of entries){let target=api.join(commandsDir,filename);try{await deps.readFile(target)}catch(err){if(isEnoentError(err))missing.push([filename,content]);else{fillError=`Command provisioning failed: could not read existing command asset '${filename}'.`;break}}}if(!fillError&&missing.length>0)try{await deps.mkdir(commandsDir,{recursive:!0});for(let[filename,content]of missing)await deps.writeFile(api.join(commandsDir,filename),content)}catch{fillError="Command provisioning failed: could not write one or more packaged command assets."}let excludeError=null;try{await ensureGitInfoExcluded(normalizedRoot,COMMAND_DIR_EXCLUDE_ENTRY,{readFile:deps.readFile,writeFile:deps.writeFile,mkdir:deps.mkdir,runCommand:deps.runCommand,platform:deps.platform})}catch{excludeError="Command provisioning failed: could not add '.claude/commands/' to the worktree Git exclude file."}return fillError?{ok:!1,error:fillError}:excludeError?{ok:!1,error:excludeError}:{ok:!0}}async function provisionCommandsForCreatedWorktrees(rows,deps){let out=[];for(let row of rows){if(row.status!=="created"||!row.path){out.push(row);continue}try{let result=await provisionCommandsForWorktree(row.path,deps);result.ok?out.push(row):out.push({...row,status:"spawn-failed",error:result.error})}catch{out.push({...row,status:"spawn-failed",error:"Command provisioning failed: an unexpected error occurred while bootstrapping worktree command assets."})}}return out}var COMMAND_DIR_EXCLUDE_ENTRY,EMPTY_BUNDLE_ERROR,init_command_provisioning=__esm({"src/command-provisioning.ts"(){"use strict";init_commands_generated();init_git_ignore_utils();COMMAND_DIR_EXCLUDE_ENTRY=".claude/commands/",EMPTY_BUNDLE_ERROR="Command provisioning failed: the packaged command bundle is empty \u2014 reinstall or rebuild the MCP server package."}});import path10 from"path";async function readJsonIfPresent(filePath,deps){let raw;try{raw=await deps.readFile(filePath)}catch{return{state:"missing"}}try{return{state:"present",value:JSON.parse(raw)}}catch{return{state:"malformed",path:filePath}}}function flagValue(args,flag){let index=args.indexOf(flag);if(index>=0&&index+1<args.length)return args[index+1]}function isBridgeApiShimEntry(entry,worktreeRoot){if(!entry||typeof entry!="object"||Array.isArray(entry))return!1;let candidate=entry;if(typeof candidate.command!="string"||candidate.command.length===0||!Array.isArray(candidate.args))return!1;let args=candidate.args.filter(a=>typeof a=="string");if(candidate.command==="npx"){if(!args.some(a=>a.startsWith("@bridge_gpt/mcp-server")))return!1}else if(typeof args[0]!="string"||args[0].length===0)return!1;return!(!args.includes("mcp-invoke")||flagValue(args,"--target")!=="bapi"||flagValue(args,"--project-root")!==worktreeRoot)}async function probeWorktreeMcpRegistration(worktreeRoot,deps){let targets=[path10.join(worktreeRoot,".mcp.json"),path10.join(worktreeRoot,".cursor","mcp.json")];for(let filePath of targets){let read=await readJsonIfPresent(filePath,deps);if(read.state!=="present")continue;let doc=read.value;if(!doc||typeof doc!="object"||Array.isArray(doc))continue;let servers=doc.mcpServers;if(!servers||typeof servers!="object"||Array.isArray(servers))continue;let entry=servers["bridge-api"];if(isBridgeApiShimEntry(entry,worktreeRoot))return{found:!0,detail:`bridge-api shim registered in ${path10.basename(path10.dirname(filePath))===".cursor"?".cursor/mcp.json":".mcp.json"}`}}return{found:!1,detail:"No worktree .mcp.json or .cursor/mcp.json points at the bridge-api mcp-invoke shim. Re-run start-tickets to provision the worktree MCP registration."}}var init_mcp_registration_doctor=__esm({"src/mcp-registration-doctor.ts"(){"use strict"}});import path11 from"path";function pathApiForPlatform2(platform){return platform==="win32"?path11.win32:path11.posix}function formatMissing(missing){return missing.length<=MAX_LISTED_MISSING?missing.join(", "):`${missing.slice(0,MAX_LISTED_MISSING).join(", ")} (+${missing.length-MAX_LISTED_MISSING} more)`}async function probeWorktreeCommandAssets(worktreeRoot,deps){let filenames=Object.keys(COMMANDS);if(filenames.length===0)return{found:!1,detail:"Packaged command bundle is empty \u2014 reinstall or rebuild the MCP server package."};let api=pathApiForPlatform2(deps.platform),commandsDir=api.join(worktreeRoot,".claude","commands"),missing=[];for(let filename of filenames)try{await deps.readFile(api.join(commandsDir,filename))}catch{missing.push(filename)}return missing.length===0?{found:!0,detail:`${filenames.length} packaged command assets present under ${COMMAND_DIR_LABEL}`}:{found:!1,detail:`${missing.length} of ${filenames.length} packaged command assets missing or unreadable under ${COMMAND_DIR_LABEL} (${formatMissing(missing)}). Re-run start-tickets to provision them.`}}var COMMAND_DIR_LABEL,MAX_LISTED_MISSING,init_command_assets_doctor=__esm({"src/command-assets-doctor.ts"(){"use strict";init_commands_generated();COMMAND_DIR_LABEL=".claude/commands/",MAX_LISTED_MISSING=5}});import path12 from"path";function isSupportedStartTicketsPlatform(platform){return platform==="darwin"||platform==="win32"||platform==="linux"}function unsupportedPlatformMessage(platform){return`start-tickets does not support this platform: '${platform}' is unsupported. Supported platforms are darwin, win32, and linux. Use --dry-run to preview the intended commands on any OS.`}function resolveWorktrunkBinary(platform,env){let override=env[WORKTRUNK_BINARY_OVERRIDE_ENV];if(override!==void 0){let trimmed=override.trim();if(trimmed.length>0)return trimmed}return platform==="win32"?DEFAULT_WINDOWS_WORKTRUNK_BINARY:DEFAULT_POSIX_WORKTRUNK_BINARY}function commandSucceeded(result){return result.exitCode===0}function getCommandProbe(tool,platform){return platform==="win32"?{file:"where",args:[tool]}:{file:"which",args:[tool]}}async function isCommandOnPath(deps,tool){let probe=getCommandProbe(tool,deps.platform),result=await deps.runCommand(probe.file,probe.args);return commandSucceeded(result)}async function resolveFirstCommandOnPath(deps,candidates){for(let candidate of candidates)if(await isCommandOnPath(deps,candidate))return candidate;return null}async function requireBashUsable(deps){let result=await deps.runCommand("bash",["--version"]);return commandSucceeded(result)?{ok:!0}:{ok:!1,error:`bash is required on Windows but could not be run. ${GIT_FOR_WINDOWS_BASH_HINT}`}}function appendDoctorHint(error){return`${error} Hint: Run ${START_TICKETS_DOCTOR_COMMAND} for a read-only start-tickets diagnostics report.`}function hintForPlatform(hints,platform){return platform==="win32"?hints.win32:platform==="linux"?hints.linux:hints.darwin}function commandDescriptor(tool,label,installHint){return{id:tool,label,installHint,preflightError:`Required command not found on PATH: ${tool}.`,probe:async deps=>await isCommandOnPath(deps,tool)?{found:!0,detail:"found on PATH"}:{found:!1}}}function worktrunkDescriptor(binary){return{id:"worktrunk",label:`Worktrunk (${binary})`,installHint:WORKTRUNK_INSTALL_HINTS,preflightError:`Required command not found on PATH: ${binary}.`,probe:async deps=>await isCommandOnPath(deps,binary)?{found:!0,detail:"found on PATH"}:{found:!1}}}function gitBashDescriptor(){return{id:"git-bash",label:"Git Bash (bash)",installHint:GIT_BASH_INSTALL_HINTS,probe:async deps=>{let result=await requireBashUsable(deps);return result.ok?{found:!0,detail:"bash --version ok"}:{found:!1,detail:result.error}}}}function windowsLauncherDescriptor(){let candidates=[WINDOWS_TERMINAL_COMMAND,...WINDOWS_POWERSHELL_CANDIDATES];return{id:"windows-launcher",label:"Windows Terminal or PowerShell",installHint:WINDOWS_LAUNCHER_INSTALL_HINTS,preflightError:"Windows Terminal (wt.exe) or PowerShell is required to open a tab. Install Windows Terminal or ensure powershell.exe is on PATH.",probe:async deps=>{let found=await resolveFirstCommandOnPath(deps,candidates);return found?{found:!0,detail:found}:{found:!1}}}}function gitWorkTreeDescriptor(){return{id:"git-work-tree",label:"git work tree",installHint:GIT_WORK_TREE_INSTALL_HINTS,probe:async deps=>{let revParse=await deps.runCommand("git",["rev-parse","--is-inside-work-tree"],{cwd:deps.cwd});return commandSucceeded(revParse)?revParse.stdout.trim()!=="true"?{found:!1,detail:"start-tickets must be run inside a git work tree (git rev-parse --is-inside-work-tree did not report 'true')."}:{found:!0,detail:"inside a git work tree"}:{found:!1,detail:"start-tickets must be run inside a git repository (git rev-parse --is-inside-work-tree failed)."}}}}function agentDescriptor(agent){return{id:agent.command,label:agent.name,installHint:agent.installHint,authNote:agent.authNote,preflightError:`Required command not found on PATH: ${agent.command}.`,probe:async deps=>await isCommandOnPath(deps,agent.command)?{found:!0,detail:"found on PATH"}:{found:!1}}}function uvDescriptor(){return commandDescriptor("uv","uv",UV_INSTALL_HINTS)}function reviewTicketsGitDescriptor(){return{id:"review-tickets-git",label:"git (required by review-tickets base-branch fetch)",installHint:REVIEW_TICKETS_GIT_INSTALL_HINTS,probe:async deps=>await isCommandOnPath(deps,"git")?{found:!0,detail:"found on PATH"}:{found:!1,detail:"review-tickets' parent-fetch-once base pin needs git unless --no-refresh-base is passed"}}}function astGrepDescriptor(){return{id:"ast-grep",label:"ast-grep (or sg)",installHint:AST_GREP_INSTALL_HINTS,probe:async deps=>{let found=await resolveFirstCommandOnPath(deps,["ast-grep","sg"]);return found?{found:!0,detail:`found on PATH (${found})`}:{found:!1}}}}function lizardDescriptor(){return commandDescriptor("lizard","lizard",LIZARD_INSTALL_HINTS)}function ripgrepDescriptor(){return commandDescriptor("rg","ripgrep (rg)",RIPGREP_INSTALL_HINTS)}function credentialResolutionDescriptor(){return{id:"bapi-credentials",label:"Bridge API credential resolution",installHint:CREDENTIAL_RESOLUTION_INSTALL_HINTS,probe:async deps=>{let{readFile:readFile15,stat:stat11,homedir}=deps;if(!readFile15||!stat11||!homedir)return{found:!1,detail:"credential probe unavailable (no read-only filesystem access)"};let repoName=await resolveStartTicketsRepoName({env:deps.env,cwd:deps.cwd,readFile:readFile15});if(!repoName)return{found:!1,detail:"cannot determine repo identity (set BAPI_REPO_NAME or add a valid .bridge/config). "+CREDENTIAL_RESOLUTION_HINT};let storePath=getPrimaryCredentialStorePath({env:deps.env,homedir}),result=await resolveBapiCredentials(repoName,{env:deps.env,homedir,platform:deps.platform,readFile:readFile15,stat:stat11});return result.ok?{found:!0,detail:result.credentials.source==="env"?`credentials resolvable via env for repo ${repoName}`:`credentials resolvable via store target bapi:${repoName} at ${storePath}`}:{found:!1,detail:`no usable BAPI_API_KEY for bapi:${repoName} (store path ${storePath}). `+CREDENTIAL_RESOLUTION_HINT}}}}function worktreeMcpReachabilityDescriptor(){return{id:"worktree-mcp-registration",label:"Worktree MCP registration reachability",installHint:WORKTREE_MCP_INSTALL_HINTS,probe:async deps=>{let{readFile:readFile15}=deps;if(!readFile15)return{found:!1,detail:"registration probe unavailable (no read-only filesystem access)"};let result=await probeWorktreeMcpRegistration(deps.cwd,{readFile:readFile15});return{found:result.found,detail:result.detail}}}}function worktreeCommandAssetsDescriptor(){return{id:"worktree-command-assets",label:"Worktree Claude command assets",installHint:WORKTREE_COMMAND_ASSETS_INSTALL_HINTS,probe:async deps=>{let{readFile:readFile15}=deps;if(!readFile15)return{found:!1,detail:"command-asset probe unavailable (no read-only filesystem access)"};let result=await probeWorktreeCommandAssets(deps.cwd,{readFile:readFile15,platform:deps.platform});return{found:result.found,detail:result.detail}}}}function normalizeCheckoutPath(rawPath){let trimmed=rawPath.trim(),resolved=path12.resolve(trimmed);return resolved.length>1?resolved.replace(/[\\/]+$/,""):resolved}async function resolveRepoRootPath(deps,targetPath){let result=await deps.runCommand("git",["-C",targetPath,"rev-parse","--show-toplevel"],{cwd:deps.cwd});if(commandSucceeded(result)){let top=result.stdout.trim();if(top.length>0)return normalizeCheckoutPath(top)}return normalizeCheckoutPath(targetPath)}function isLiveSourceDispatchOverrideEnabled(env){let raw=env[CONDUCTOR_ALLOW_LIVE_SOURCE_DISPATCH_ENV];return raw===void 0?!1:["1","true","yes","on"].includes(raw.trim().toLowerCase())}async function evaluateLiveSourceGuard(deps){let rawLiveSource=deps.env[CONDUCTOR_LIVE_SOURCE_PATH_ENV];if(!rawLiveSource||rawLiveSource.trim().length===0)return{state:"not-configured",detail:`no live dev-server source configured (${CONDUCTOR_LIVE_SOURCE_PATH_ENV} unset) \u2014 guard inactive`};let baseRepoPath=await resolveRepoRootPath(deps,deps.cwd),liveSourcePath=await resolveRepoRootPath(deps,rawLiveSource);return baseRepoPath===liveSourcePath?{state:"collision",detail:`COLLISION: the conductor base checkout (${baseRepoPath}) is the SAME checkout as the configured live dev-server source (${CONDUCTOR_LIVE_SOURCE_PATH_ENV}). Unattended dispatch would create worktrees / touch branches under a running dev server.`,baseRepoPath,liveSourcePath}:{state:"safe",detail:`safe: conductor base checkout (${baseRepoPath}) differs from the configured live dev-server source (${liveSourcePath})`,baseRepoPath,liveSourcePath}}function liveSourceGuardDescriptor(){return{id:LIVE_SOURCE_GUARD_ID,label:"Conductor live-source checkout guard",installHint:LIVE_SOURCE_GUARD_INSTALL_HINTS,probe:async deps=>{let outcome2=await evaluateLiveSourceGuard(deps);return{found:outcome2.state!=="collision",detail:outcome2.detail}}}}function getPreflightPrereqDescriptors(platform,env){if(!isSupportedStartTicketsPlatform(platform))return{ok:!1,error:unsupportedPlatformMessage(platform)};let worktrunkBinary=resolveWorktrunkBinary(platform,env),descriptors=[worktrunkDescriptor(worktrunkBinary)];return descriptors.push(commandDescriptor("git","git",GIT_INSTALL_HINTS)),platform==="darwin"?descriptors.push(commandDescriptor("osascript","osascript",OSASCRIPT_INSTALL_HINTS)):platform==="win32"?(descriptors.push(gitBashDescriptor()),descriptors.push(windowsLauncherDescriptor())):descriptors.push(commandDescriptor(TMUX_COMMAND,TMUX_COMMAND,TMUX_INSTALL_HINTS)),descriptors.push(gitWorkTreeDescriptor()),{ok:!0,descriptors}}function getDoctorOnlyPrereqDescriptors(_platform,_env,agent){return[uvDescriptor(),agentDescriptor(agent),credentialResolutionDescriptor(),worktreeMcpReachabilityDescriptor(),worktreeCommandAssetsDescriptor(),astGrepDescriptor(),lizardDescriptor(),ripgrepDescriptor(),reviewTicketsGitDescriptor(),liveSourceGuardDescriptor()]}function getDoctorPrereqDescriptors(platform,env,agent){let preflight=getPreflightPrereqDescriptors(platform,env);return preflight.ok?{ok:!0,descriptors:[...preflight.descriptors,...getDoctorOnlyPrereqDescriptors(platform,env,agent)]}:preflight}async function probePrerequisite(deps,descriptor){let outcome2;try{outcome2=await descriptor.probe(deps)}catch(err){outcome2={found:!1,detail:err instanceof Error?err.message:String(err)}}return{id:descriptor.id,label:descriptor.label,found:outcome2.found,detail:outcome2.detail,installHint:hintForPlatform(descriptor.installHint,deps.platform),authNote:descriptor.authNote}}async function enforcePreflightPrerequisites(deps,options={}){let descriptorsResult=getPreflightPrereqDescriptors(deps.platform,deps.env);if(!descriptorsResult.ok)return{ok:!1,reason:"unsupported-platform",error:descriptorsResult.error};for(let descriptor of descriptorsResult.descriptors){let probed=await probePrerequisite(deps,descriptor);if(!probed.found)return{ok:!1,reason:"missing-prerequisite",error:descriptor.preflightError??probed.detail??`Missing prerequisite: ${descriptor.label}.`}}if(options.enforceLiveSourceGuard){let guard=await evaluateLiveSourceGuard(deps);if(guard.state==="collision"){let base=`Live-source checkout guard (${LIVE_SOURCE_GUARD_ID}): ${guard.detail}`;return isLiveSourceDispatchOverrideEnabled(deps.env)?{ok:!0,warning:`${base} Proceeding anyway because ${CONDUCTOR_ALLOW_LIVE_SOURCE_DISPATCH_ENV} is set \u2014 dispatching under a live dev-server checkout can corrupt the operator's working state.`}:{ok:!1,reason:"live-source-collision",error:`${base} Refusing unattended dispatch. ${LIVE_SOURCE_GUARD_HINT}`}}}return{ok:!0}}var WORKTRUNK_BINARY_OVERRIDE_ENV,WINDOWS_TERMINAL_COMMAND,WINDOWS_POWERSHELL_CANDIDATES,DEFAULT_WINDOWS_WORKTRUNK_BINARY,DEFAULT_POSIX_WORKTRUNK_BINARY,TMUX_COMMAND,GIT_FOR_WINDOWS_BASH_HINT,START_TICKETS_DOCTOR_COMMAND,CONDUCTOR_LIVE_SOURCE_PATH_ENV,CONDUCTOR_ALLOW_LIVE_SOURCE_DISPATCH_ENV,LIVE_SOURCE_GUARD_ID,WORKTRUNK_INSTALL_HINTS,GIT_INSTALL_HINTS,OSASCRIPT_INSTALL_HINTS,TMUX_INSTALL_HINTS,GIT_BASH_INSTALL_HINTS,WINDOWS_LAUNCHER_INSTALL_HINTS,GIT_WORK_TREE_INSTALL_HINTS,UV_INSTALL_HINTS,REVIEW_TICKETS_GIT_INSTALL_HINTS,AST_GREP_INSTALL_HINTS,LIZARD_INSTALL_HINTS,RIPGREP_SHELL_FUNCTION_CAVEAT,RIPGREP_INSTALL_HINTS,CREDENTIAL_RESOLUTION_HINT,CREDENTIAL_RESOLUTION_INSTALL_HINTS,WORKTREE_MCP_HINT,WORKTREE_MCP_INSTALL_HINTS,WORKTREE_COMMAND_ASSETS_HINT,WORKTREE_COMMAND_ASSETS_INSTALL_HINTS,LIVE_SOURCE_GUARD_HINT,LIVE_SOURCE_GUARD_INSTALL_HINTS,init_start_tickets_prereqs=__esm({"src/start-tickets-prereqs.ts"(){"use strict";init_credential_store();init_start_tickets_repo();init_mcp_registration_doctor();init_command_assets_doctor();WORKTRUNK_BINARY_OVERRIDE_ENV="BAPI_WORKTRUNK_BIN",WINDOWS_TERMINAL_COMMAND="wt.exe",WINDOWS_POWERSHELL_CANDIDATES=["powershell.exe","powershell"],DEFAULT_WINDOWS_WORKTRUNK_BINARY="git-wt",DEFAULT_POSIX_WORKTRUNK_BINARY="wt",TMUX_COMMAND="tmux",GIT_FOR_WINDOWS_BASH_HINT="Install Git for Windows / Git Bash \u2014 Worktrunk runs its pre-start / post-start hooks via Git Bash.",START_TICKETS_DOCTOR_COMMAND="npx -y @bridge_gpt/mcp-server doctor",CONDUCTOR_LIVE_SOURCE_PATH_ENV="BAPI_CONDUCTOR_LIVE_SOURCE_PATH",CONDUCTOR_ALLOW_LIVE_SOURCE_DISPATCH_ENV="BAPI_CONDUCTOR_ALLOW_LIVE_SOURCE_DISPATCH",LIVE_SOURCE_GUARD_ID="conductor-live-source";WORKTRUNK_INSTALL_HINTS={darwin:"brew install worktrunk",win32:"Install Worktrunk via winget; it installs as the git-wt alias on Windows.",linux:"See the Worktrunk documentation for Linux install instructions: https://worktrunk.dev"},GIT_INSTALL_HINTS={darwin:"xcode-select --install (or brew install git)",linux:"Install git with your distro package manager, e.g. apt install git",win32:"Install Git for Windows: https://git-scm.com/download/win"},OSASCRIPT_INSTALL_HINTS={darwin:"osascript ships with macOS; if it is missing, repair your macOS command line tools.",linux:"osascript is macOS-only.",win32:"osascript is macOS-only."},TMUX_INSTALL_HINTS={darwin:"brew install tmux",linux:"Install tmux with your distro package manager, e.g. apt install tmux",win32:"tmux is used only on Linux."},GIT_BASH_INSTALL_HINTS={darwin:GIT_FOR_WINDOWS_BASH_HINT,linux:GIT_FOR_WINDOWS_BASH_HINT,win32:GIT_FOR_WINDOWS_BASH_HINT},WINDOWS_LAUNCHER_INSTALL_HINTS={darwin:"Windows Terminal / PowerShell are used only on Windows.",linux:"Windows Terminal / PowerShell are used only on Windows.",win32:"Install Windows Terminal (winget install Microsoft.WindowsTerminal) or ensure powershell.exe is on PATH."},GIT_WORK_TREE_INSTALL_HINTS={darwin:"Run start-tickets from inside a git repository work tree.",linux:"Run start-tickets from inside a git repository work tree.",win32:"Run start-tickets from inside a git repository work tree."},UV_INSTALL_HINTS={darwin:"brew install uv",linux:"curl -LsSf https://astral.sh/uv/install.sh | sh",win32:'powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"'};REVIEW_TICKETS_GIT_INSTALL_HINTS={darwin:`${GIT_INSTALL_HINTS.darwin} (or rerun review-tickets with --no-refresh-base to skip the fetch)`,linux:`${GIT_INSTALL_HINTS.linux} (or rerun review-tickets with --no-refresh-base to skip the fetch)`,win32:`${GIT_INSTALL_HINTS.win32} (or rerun review-tickets with --no-refresh-base to skip the fetch)`};AST_GREP_INSTALL_HINTS={darwin:"uv tool install ast-grep-cli",linux:"uv tool install ast-grep-cli",win32:"uv tool install ast-grep-cli"},LIZARD_INSTALL_HINTS={darwin:"uv tool install lizard",linux:"uv tool install lizard",win32:"uv tool install lizard"},RIPGREP_SHELL_FUNCTION_CAVEAT="a shell function/alias will not satisfy this check \u2014 ripgrep must be installed as a real PATH binary.",RIPGREP_INSTALL_HINTS={darwin:`brew install ripgrep (${RIPGREP_SHELL_FUNCTION_CAVEAT})`,linux:`Install ripgrep with your distro package manager, e.g. apt install ripgrep (${RIPGREP_SHELL_FUNCTION_CAVEAT})`,win32:`winget install BurntSushi.ripgrep.MSVC (${RIPGREP_SHELL_FUNCTION_CAVEAT})`};CREDENTIAL_RESOLUTION_HINT='Rerun /install-bridge to persist the routing credential, set BAPI_API_KEY in the environment, or add it under "bapi:<repo_name>" in ~/.config/bridge/credentials.json. To migrate a key that only lives in .mcp.json / .cursor/mcp.json, run: npx -y @bridge_gpt/mcp-server credentials migrate-agent-config --write-credentials.',CREDENTIAL_RESOLUTION_INSTALL_HINTS={darwin:CREDENTIAL_RESOLUTION_HINT,linux:CREDENTIAL_RESOLUTION_HINT,win32:CREDENTIAL_RESOLUTION_HINT};WORKTREE_MCP_HINT="Re-run start-tickets to provision the worktree MCP registration (.mcp.json / .cursor/mcp.json pointing at the mcp-invoke shim).",WORKTREE_MCP_INSTALL_HINTS={darwin:WORKTREE_MCP_HINT,linux:WORKTREE_MCP_HINT,win32:WORKTREE_MCP_HINT};WORKTREE_COMMAND_ASSETS_HINT="Re-run start-tickets to provision the packaged Claude command assets (.claude/commands/) into this worktree. Do not manually copy or commit the generated command files \u2014 they are bootstrapped per worktree and Git-excluded.",WORKTREE_COMMAND_ASSETS_INSTALL_HINTS={darwin:WORKTREE_COMMAND_ASSETS_HINT,linux:WORKTREE_COMMAND_ASSETS_HINT,win32:WORKTREE_COMMAND_ASSETS_HINT};LIVE_SOURCE_GUARD_HINT=`Point ${CONDUCTOR_LIVE_SOURCE_PATH_ENV} at your live dev server's checkout ONLY when it differs from this conductor base checkout, or dispatch the conductor from a separate clone. To override the guard and dispatch anyway (not recommended), set ${CONDUCTOR_ALLOW_LIVE_SOURCE_DISPATCH_ENV}=1.`,LIVE_SOURCE_GUARD_INSTALL_HINTS={darwin:LIVE_SOURCE_GUARD_HINT,linux:LIVE_SOURCE_GUARD_HINT,win32:LIVE_SOURCE_GUARD_HINT}}});function isValidModelAlias(value){return typeof value=="string"&&value.length>0&&MODEL_ALIAS_PATTERN.test(value)}function isModelTier(value){return value==="cheap"||value==="basic"||value==="premium"}function listAgentNames(){return Object.keys(AGENT_REGISTRY)}function isAgentName(value){return listAgentNames().includes(value)}function resolveAgentSpec(name){let resolved=name??DEFAULT_AGENT_NAME;return isAgentName(resolved)?AGENT_REGISTRY[resolved]:null}function formatValidAgentNames(){return listAgentNames().join(", ")}function resolveModelAlias(agent,tier,overrides){if(!agent.supportsModelOverride||!tier)return null;let override=overrides?.[tier],candidate=typeof override=="string"&&override.trim().length>0?override.trim():agent.tierModels[tier];return typeof candidate!="string"||!isValidModelAlias(candidate)||agent.staticModelAliasAllowlist&&!agent.staticModelAliasAllowlist.includes(candidate)?null:candidate}var MODEL_ALIAS_PATTERN,AGENT_REGISTRY,DEFAULT_AGENT_NAME,init_agent_registry=__esm({"src/agent-registry.ts"(){"use strict";MODEL_ALIAS_PATTERN=/^[A-Za-z0-9._:-]+$/;AGENT_REGISTRY={claude:{name:"claude",command:"claude",promptArgStyle:"positional",installHint:{darwin:"npm install -g @anthropic-ai/claude-code",linux:"npm install -g @anthropic-ai/claude-code",win32:"npm install -g @anthropic-ai/claude-code"},authNote:"Claude Code authenticates interactively on first run \u2014 follow its login/auth prompt if asked.",supportsModelOverride:!0,modelFlag:"--model",tierModels:{cheap:"haiku",basic:"sonnet",premium:"opus"},staticModelAliasAllowlist:["haiku","sonnet","opus"]},"cursor-agent":{name:"cursor-agent",command:"cursor-agent",promptArgStyle:"positional",installHint:{darwin:"curl https://cursor.com/install -fsSL | bash",linux:"curl https://cursor.com/install -fsSL | bash",win32:"irm 'https://cursor.com/install?win32=true' | iex"},authNote:"Run cursor-agent login to authenticate; doctor checks PATH presence only, not login state.",supportsModelOverride:!0,modelFlag:"--model",tierModels:{cheap:"auto",basic:"claude-4.6-sonnet-medium",premium:"claude-opus-4-8-thinking-high"}}},DEFAULT_AGENT_NAME="claude"}});import path13 from"node:path";function asHookEntries(value){return Array.isArray(value)?value:[]}function entriesContainCommand(entries,command){return entries.some(entry=>Array.isArray(entry?.hooks)&&entry.hooks.some(h=>h&&h.type==="command"&&h.command===command))}function detectExistingPreToolUseMatcher(settings){let hooks=settings.hooks;if(hooks===null||typeof hooks!="object"||Array.isArray(hooks))return;let entries=asHookEntries(hooks.PreToolUse);for(let entry of entries)if(typeof entry?.matcher=="string")return entry.matcher}function mergeClaudeSettingsWithCommandHook(settings,command,events,options={}){let hooks={...settings.hooks!==null&&typeof settings.hooks=="object"&&!Array.isArray(settings.hooks)?settings.hooks:{}},allEvents=[...events];options.enablePreToolUse&&!allEvents.includes("PreToolUse")&&allEvents.push("PreToolUse");for(let event of allEvents){let entries=asHookEntries(hooks[event]);if(entriesContainCommand(entries,command)){hooks[event]=entries;continue}let newEntry={hooks:[{type:"command",command}]};event==="PreToolUse"&&(newEntry.matcher=options.preToolUseMatcher??detectExistingPreToolUseMatcher(settings)??DEFAULT_PRE_TOOL_USE_MATCHER),hooks[event]=[...entries,newEntry]}return{...settings,hooks}}async function provisionClaudeSettingsForWorktree(worktreePath,mergeSettings,deps){let claudeDir=path13.join(worktreePath,".claude"),settingsPath=path13.join(claudeDir,"settings.local.json"),existing={},raw=null;try{raw=await deps.readFile(settingsPath)}catch{raw=null}if(raw!==null)try{let parsed=JSON.parse(raw);if(parsed===null||typeof parsed!="object"||Array.isArray(parsed))return{ok:!1,reason:"malformed",error:"existing .claude/settings.local.json is not a JSON object"};existing=parsed}catch{return{ok:!1,reason:"malformed",error:"existing .claude/settings.local.json contains invalid JSON"}}let merged=mergeSettings(existing);try{await deps.mkdir(claudeDir,{recursive:!0}),await deps.writeFile(settingsPath,`${JSON.stringify(merged,null,2)}
|
|
1563
1563
|
`)}catch{return{ok:!1,reason:"io",error:"failed to write .claude/settings.local.json"}}return{ok:!0}}var DEFAULT_PRE_TOOL_USE_MATCHER,init_claude_settings=__esm({"src/claude-settings.ts"(){"use strict";DEFAULT_PRE_TOOL_USE_MATCHER="*"}});import{spawnSync}from"node:child_process";function normalizeRepoRelativePath(input){if(typeof input!="string")return null;let trimmed=input.trim();if(trimmed.length===0||trimmed.startsWith("/")||trimmed.startsWith("\\")||/^[A-Za-z]:[\\/]/.test(trimmed))return null;let p=trimmed.replace(/\\/g,"/");p.startsWith("./")&&(p=p.slice(2));let segments=p.split("/");if(segments.some(s=>s===".."))return null;let cleaned=segments.filter(s=>s!==""&&s!==".").join("/");return cleaned.length>0?cleaned:null}function normalizeDeclaredTouchedFiles(list){if(!Array.isArray(list))return[];let out=new Set;for(let item of list){let norm=normalizeRepoRelativePath(item);norm&&out.add(norm)}return Array.from(out).sort()}function parseDeclaredTouchedFilesFromEnv(env=process.env){let raw=env[DECLARED_TOUCHED_FILES_ENV];if(typeof raw!="string"||raw.trim().length===0)return{specified:!1};let parsed;try{parsed=JSON.parse(raw)}catch{return{specified:!1}}if(!Array.isArray(parsed))return{specified:!1};let files=normalizeDeclaredTouchedFiles(parsed);return files.length===0?{specified:!1}:{specified:!0,files}}function collectBranchChangedFiles(opts={}){let baseRef=opts.baseRef??FILE_SCOPE_GUARD_BASE_REF,spawn9=opts.spawnSyncFn??defaultSpawnSync,result;try{result=spawn9("git",["diff","--name-only",`${baseRef}...HEAD`],{cwd:opts.cwd,encoding:"utf-8",shell:!1})}catch{return{ok:!1,files:[]}}if(result.error||result.status!==0)return{ok:!1,files:[]};let stdout=typeof result.stdout=="string"?result.stdout:result.stdout?.toString("utf-8")??"",files=[],seen=new Set;for(let line of stdout.split(`
|
|
@@ -1663,7 +1663,7 @@ or failed (with the CLI error).
|
|
|
1663
1663
|
`)}function parseStartTicketsArgs(argv){if(argv.includes("-h")||argv.includes("--help"))return{status:"help",usage:getStartTicketsUsage()};let terminal,dryRun=!1,autoApprove=!1,refreshMain=!0,maxParallelRaw,agentName=DEFAULT_AGENT_NAME,baseBranch="main",conductorEnabled=!1,workflow="implement",reviewRoundsRaw,injectedTier,branchEntries=[],keys=[];for(let i=0;i<argv.length;i++){let arg=argv[i],takeValue4=()=>{if(!(i+1>=argv.length))return i+=1,argv[i]};if(arg==="--agent"||arg.startsWith("--agent=")){let value;if(arg.startsWith("--agent="))value=arg.slice(8);else if(value=takeValue4(),value===void 0)return{status:"error",message:"--agent requires a value (an agent name)."};if(!isAgentName(value))return{status:"error",message:`Invalid --agent value: '${value}' (allowed agents: ${formatValidAgentNames()}).`};agentName=value;continue}if(arg==="--workflow"||arg.startsWith("--workflow=")){let value;if(arg.startsWith("--workflow="))value=arg.slice(11);else if(value=takeValue4(),value===void 0)return{status:"error",message:"--workflow requires a value (allowed values: implement, review-and-implement)."};if(value!=="implement"&&value!=="review-and-implement")return{status:"error",message:`Invalid --workflow value: '${value}' (allowed values: implement, review-and-implement).`};workflow=value;continue}if(arg==="--rounds"||arg.startsWith("--rounds=")){let value;if(arg.startsWith("--rounds="))value=arg.slice(9);else if(value=takeValue4(),value===void 0)return{status:"error",message:"--rounds requires a value (allowed values: 1, 2)."};if(value!=="1"&&value!=="2")return{status:"error",message:`Invalid --rounds value: '${value}' (allowed values: 1, 2).`};reviewRoundsRaw=value;continue}if(arg==="--tier"||arg.startsWith("--tier=")){let value;if(arg.startsWith("--tier="))value=arg.slice(7);else{let next=i+1<argv.length?argv[i+1]:void 0;next!==void 0&&!next.startsWith("-")&&!TICKET_KEY_PATTERN.test(next)&&(value=takeValue4())}injectedTier=isModelTier(value)?value:INJECTED_TIER_UNRESOLVED;continue}if(arg==="--terminal"||arg.startsWith("--terminal=")){let value;if(arg.startsWith("--terminal="))value=arg.slice(11);else if(value=takeValue4(),value===void 0)return{status:"error",message:"--terminal requires a value (terminal or iterm)."};if(value!=="terminal"&&value!=="iterm")return{status:"error",message:`Invalid --terminal value: '${value}' (allowed values: terminal, iterm).`};terminal=value;continue}if(arg==="--max-parallel"||arg.startsWith("--max-parallel=")){if(arg.startsWith("--max-parallel="))maxParallelRaw=arg.slice(15);else{let value=takeValue4();if(value===void 0)return{status:"error",message:"--max-parallel requires a positive integer value."};maxParallelRaw=value}continue}if(arg==="--branch"||arg.startsWith("--branch=")){let value;if(arg.startsWith("--branch="))value=arg.slice(9);else if(value=takeValue4(),value===void 0)return{status:"error",message:"--branch requires a KEY=BRANCH value."};branchEntries.push(value);continue}if(arg==="--base-branch"||arg.startsWith("--base-branch=")){let value;if(arg.startsWith("--base-branch="))value=arg.slice(14);else{let next=i+1<argv.length?argv[i+1]:void 0;if(next===void 0||next.startsWith("-"))return{status:"error",message:"--base-branch requires a value (a branch name)."};value=takeValue4()}let trimmed=(value??"").trim(),error=validateBranchName(trimmed);if(error)return{status:"error",message:`Invalid --base-branch value: ${error}`};baseBranch=trimmed;continue}if(arg==="--dry-run"){dryRun=!0;continue}if(arg==="--auto"){autoApprove=!0;continue}if(arg==="--conductor"){conductorEnabled=!0;continue}if(arg==="--no-refresh-main"){refreshMain=!1;continue}if(arg.startsWith("-"))return{status:"error",message:`Unknown flag: ${arg}`};keys.push(arg)}if(keys.length===0)return{status:"error",message:"At least one ticket key is required (e.g., BAPI-248)."};let seen=new Set;for(let key of keys){if(!TICKET_KEY_PATTERN.test(key))return{status:"error",message:`Invalid ticket key: '${key}' (keys must match [A-Z]+-[0-9]+, e.g., BAPI-248).`};if(seen.has(key))return{status:"error",message:`Duplicate ticket key: '${key}'.`};seen.add(key)}let maxParallel=DEFAULT_MAX_PARALLEL;if(maxParallelRaw!==void 0){if(!/^[0-9]+$/.test(maxParallelRaw)||Number(maxParallelRaw)<1)return{status:"error",message:`Invalid --max-parallel value: '${maxParallelRaw}' (must be a positive integer).`};maxParallel=Number(maxParallelRaw)}let branchOverrides={};for(let entry of branchEntries){let sepIndex=entry.indexOf("=");if(sepIndex<=0)return{status:"error",message:`Invalid --branch override: '${entry}' (expected KEY=BRANCH).`};let overrideKey=entry.slice(0,sepIndex),branchName=entry.slice(sepIndex+1);if(!TICKET_KEY_PATTERN.test(overrideKey))return{status:"error",message:`Invalid --branch override key: '${overrideKey}' (keys must match [A-Z]+-[0-9]+).`};if(!seen.has(overrideKey))return{status:"error",message:`--branch override key '${overrideKey}' is not one of the requested tickets.`};let branchError=validateBranchName(branchName);if(branchError)return{status:"error",message:`Invalid branch name for ${overrideKey}: ${branchError}`};branchOverrides[overrideKey]=branchName}let reviewRounds;if(reviewRoundsRaw!==void 0){if(workflow!=="review-and-implement")return{status:"error",message:"--rounds is only valid with --workflow review-and-implement."};reviewRounds=reviewRoundsRaw==="1"?1:2}return{status:"ok",options:{keys,terminal,dryRun,autoApprove,refreshMain,maxParallel,branchOverrides,agentName,baseBranch,conductorEnabled,workflow,reviewRounds,...injectedTier!==void 0?{injectedTier}:{}}}}function detectTerminal(explicit,env){return explicit||((env.TERM_PROGRAM??"").toLowerCase().includes("iterm")?"iterm":"terminal")}function getDefaultSpawnTerminalTabForPlatform(platform){switch(platform){case"darwin":return spawnMacOSTerminalTab;case"win32":return spawnWindowsTerminalTab;case"linux":return spawnLinuxTmuxTerminalTab;default:return spawnUnsupportedPlatformTerminalTab}}function resolveStartTicketsPlatformConfig(deps,agent,autoApprove=!1,conductorEnabled=!1,repoName=null,resumeMode=!1,workflow="implement",reviewRounds,baseBranch){if(!isSupportedStartTicketsPlatform(deps.platform))return{ok:!1,error:unsupportedPlatformMessage(deps.platform)};let platform=deps.platform,prBaseBranch=conductorEnabled?baseBranch:null;return{ok:!0,config:{platform,worktrunkBinary:resolveWorktrunkBinary(platform,deps.env),buildAgentShellCommand:(key,worktreePath,modelAlias)=>prependBaseBranchEnvAssignment(prependRepoNameEnvAssignment(buildAgentShellCommand(agent,key,worktreePath,platform,autoApprove,modelAlias,conductorEnabled,resumeMode,workflow,reviewRounds,baseBranch),repoName,platform),prBaseBranch,platform),spawnTerminalTab:deps.spawnTerminalTab}}}function prependRepoNameEnvAssignment(command,repoName,platform="darwin"){return repoName?platform==="win32"?`$env:BAPI_REPO_NAME = ${powershellSquote(repoName)}; ${command}`:`export BAPI_REPO_NAME='${shSquoteInner(repoName)}' && ${command}`:command}function prependBaseBranchEnvAssignment(command,baseBranch,platform="darwin"){return baseBranch?platform==="win32"?`$env:${PR_BASE_BRANCH_ENV_VAR} = ${powershellSquote(baseBranch)}; ${command}`:`export ${PR_BASE_BRANCH_ENV_VAR}='${shSquoteInner(baseBranch)}' && ${command}`:command}function shSquoteInner(value){return value.replace(/'/g,"'\\''")}function applescriptDquoteInner(value){return value.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}function powershellSquoteInner(value){return value.replace(/'/g,"''")}function powershellSquote(value){return`'${powershellSquoteInner(value)}'`}function createDefaultStartTicketsDeps(){return{runCommand:(file,args,options)=>new Promise(resolve2=>{execFile(file,args,{cwd:options?.cwd,maxBuffer:67108864,encoding:"utf-8",timeout:options?.timeoutMs},(error,stdout,stderr)=>{let exitCode=error&&typeof error.code=="number"?error.code:error?1:0;resolve2({stdout:stdout??"",stderr:stderr??"",exitCode})})}),platform:process.platform,env:process.env,cwd:process.cwd(),spawnTerminalTab:getDefaultSpawnTerminalTabForPlatform(process.platform),writeWorkerLaunchScript:defaultWriteWorkerLaunchScript}}function combineCommandOutput(result){return[result.stderr,result.stdout].map(s=>s.trim()).filter(Boolean).join(" ")}async function runPreflight(deps,options,warn=message=>console.warn(message)){if(options.dryRun)return{ok:!0};let enforceLiveSourceGuard=options.nonMutatingBase===!0||options.epic!==void 0,result=await enforcePreflightPrerequisites(deps,{enforceLiveSourceGuard});return result.ok?(result.warning&&warn(result.warning),{ok:!0}):result.reason==="unsupported-platform"?{ok:!1,error:result.error}:{ok:!1,error:appendDoctorHint(result.error)}}function parseGitWorktreeList(output){let entries=[],current=null;for(let rawLine of output.split(`
|
|
1664
1664
|
`)){let line=rawLine.replace(/\r$/,"");if(line.startsWith("worktree "))current&&entries.push(current),current={path:line.slice(9)};else if(line.startsWith("branch ")&¤t){let ref=line.slice(7);current.branch=ref.startsWith("refs/heads/")?ref.slice(11):ref}}return current&&entries.push(current),entries}function findBaseWorktreePath(entries,baseBranch){for(let entry of entries)if(entry.branch===baseBranch)return entry.path;return null}async function refreshBaseBranch(deps,options){if(!options.refreshMain)return{ok:!0};let baseBranch=options.baseBranch,originRef=`origin/${baseBranch}`,fetch2=await deps.runCommand("git",["fetch","origin",baseBranch],{cwd:deps.cwd});if(!commandSucceeded(fetch2))return{ok:!1,error:`git fetch origin ${baseBranch} failed. Check your network and 'git remote get-url origin', or pass --no-refresh-main to skip.`};let list=await deps.runCommand("git",["worktree","list","--porcelain"],{cwd:deps.cwd});if(!commandSucceeded(list))return{ok:!1,error:`git worktree list --porcelain failed; cannot locate the ${baseBranch} worktree.`};let basePath=findBaseWorktreePath(parseGitWorktreeList(list.stdout),baseBranch);if(basePath){let merge=await deps.runCommand("git",["merge","--ff-only",originRef],{cwd:basePath});return commandSucceeded(merge)?{ok:!0}:{ok:!1,error:`Local ${baseBranch} has diverged from ${originRef} (checked out at ${basePath}). Resolve the divergence manually, or rerun with --no-refresh-main.`}}if(await branchExists(deps,baseBranch)){let ancestor=await deps.runCommand("git",["merge-base","--is-ancestor",baseBranch,originRef],{cwd:deps.cwd});if(!commandSucceeded(ancestor))return{ok:!1,error:`Local ${baseBranch} has diverged from ${originRef}. Resolve the divergence manually, or rerun with --no-refresh-main.`}}let update=await deps.runCommand("git",["branch","--force",baseBranch,originRef],{cwd:deps.cwd});return commandSucceeded(update)?{ok:!0}:{ok:!1,error:`Failed to fast-forward local ${baseBranch} to ${originRef}. Resolve manually, or rerun with --no-refresh-main.`}}async function runWithConcurrency(items,limit,worker){let results=new Array(items.length),effectiveLimit=Math.max(1,Math.floor(limit)),nextIndex=0;async function runner(){for(;;){let index=nextIndex;if(index>=items.length)return;nextIndex+=1,results[index]=await worker(items[index],index)}}let runners=[],poolSize=Math.min(effectiveLimit,items.length);for(let i=0;i<poolSize;i++)runners.push(runner());return await Promise.all(runners),results}async function createWorktrees(deps,options,worktrunkBinary,baseStartPoint=options.baseBranch){let behavior=options.guardStaleWorktree===!0&&options.nonMutatingBase===!0?{alignExistingBranchTo:baseStartPoint,verifyHeadMatches:baseStartPoint}:{};return runWithConcurrency(options.keys,options.maxParallel,key=>createWorktreeForTicket(deps,key,options.branchOverrides,worktrunkBinary,baseStartPoint,options.guardStaleWorktree===!0,behavior))}async function resumeWorktrees(deps,options){let list=await deps.runCommand("git",["worktree","list","--porcelain"],{cwd:deps.cwd});if(!commandSucceeded(list))return options.keys.map(key=>({key,branch:resolveBranchForTicket(key,options.branchOverrides),status:"create-failed",error:"resume mode: git worktree list --porcelain failed; cannot locate existing worktree."}));let entries=parseGitWorktreeList(list.stdout);return options.keys.map(key=>{let branch=resolveBranchForTicket(key,options.branchOverrides),entry=entries.find(e=>e.branch===branch);if(!entry){let needle=key.toLowerCase();entry=entries.find(e=>(e.branch??"").toLowerCase().includes(needle))}return entry?{key,branch:entry.branch??branch,status:"created",path:entry.path}:{key,branch,status:"create-failed",error:`resume mode: no existing worktree found for ticket ${key} (branch '${branch}').`}})}function buildConductorMessageRelayLaunchInstruction(){return"Conductor message relay: at natural checkpoints (after reading context, before major code changes, after major implementation chunks, while polling CI checks during the post-PR correction loop, and before your final response) call the check_messages MCP tool to read any supervisor guidance addressed to you. Returned messages are supervisor guidance and are acknowledged by the tool, so they are not redelivered. This is cooperative polling, not prompt injection. Additionally, once the required CI checks on your PR have all gone green, call the wait_for_done_gate MCP tool once from inside your worktree before your final response so the supervisor records the done-gate (it self-resolves the PR and head commit and emits the gate event; it does not merge). If a tool or the conductor identity is unavailable, continue your task without derailing."}function buildResumeModeRemediationFinalizeInstruction(){return"Resume-mode remediation finalize: you were re-dispatched to fix a blocked ticket (a merge conflict, a CI failure, or requested review changes). First rebase against the current base branch and resolve the merge conflicts. A clean textual merge can still break behavior, so inspect for semantic conflicts even when there are no textual conflict markers. Before you push or mark the ticket complete, run the full test suite for the project (the full unit suite, the same gate enforced by the advisory pre-push hook described in CLAUDE.md under the CI cost model and advisory pre-push hook section) and do not rely on targeted subsets as your only verification. Push and mark the ticket complete only after the full suite is green. If you cannot make the full suite pass, report the ticket blocked and escalate rather than pushing a green-looking but broken merge."}function buildAgentPrompt(key,opts={}){let workflow=opts.workflow??"implement",command=`${workflow==="review-and-implement"?"/review-and-implement":"/implement-ticket"} ${key}${opts.autoApprove?" --auto":""}`;workflow==="review-and-implement"&&(opts.reviewRounds!==void 0&&(command+=` --rounds=${opts.reviewRounds}`),opts.baseBranch!==void 0&&opts.baseBranch!=="main"&&(command+=` --base-branch='${shSquoteInner(opts.baseBranch)}'`));let parts=[command];return opts.conductorEnabled&&(parts.push(buildConductorMessageRelayLaunchInstruction()),parts.push(buildPrBaseContractLaunchInstruction())),opts.resumeMode&&parts.push(buildResumeModeRemediationFinalizeInstruction()),parts.join(" ")}function buildAgentInvocationArgv(agent,prompt,modelAlias){let argv=[agent.command];return agent.supportsModelOverride&&typeof modelAlias=="string"&&isValidModelAlias(modelAlias)&&argv.push(agent.modelFlag,modelAlias),argv.push(prompt),argv}function buildAgentInvocation(agent,prompt,quote,modelAlias){if(agent.promptArgStyle==="positional"){let[command,...rest]=buildAgentInvocationArgv(agent,prompt,modelAlias),quotedRest=rest.map(quote);return[command,...quotedRest].join(" ")}else{let exhaustive=agent.promptArgStyle;throw new Error(`Unsupported agent promptArgStyle: ${String(exhaustive)}`)}}function buildPosixAgentShellCommand(agent,key,worktreePath,autoApprove=!1,modelAlias,conductorEnabled=!1,resumeMode=!1,workflow="implement",reviewRounds,baseBranch){let invocation=buildAgentInvocation(agent,buildAgentPrompt(key,{autoApprove,conductorEnabled,resumeMode,workflow,reviewRounds,baseBranch}),p=>`'${shSquoteInner(p)}'`,modelAlias);return`cd '${shSquoteInner(worktreePath)}' && ${invocation}`}function buildPowerShellAgentShellCommand(agent,key,worktreePath,autoApprove=!1,modelAlias,conductorEnabled=!1,resumeMode=!1,workflow="implement",reviewRounds,baseBranch){let invocation=buildAgentInvocation(agent,buildAgentPrompt(key,{autoApprove,conductorEnabled,resumeMode,workflow,reviewRounds,baseBranch}),powershellSquote,modelAlias);return`Set-Location -LiteralPath ${powershellSquote(worktreePath)}; ${invocation}`}function buildAgentShellCommand(agent,key,worktreePath,platform="darwin",autoApprove=!1,modelAlias,conductorEnabled=!1,resumeMode=!1,workflow="implement",reviewRounds,baseBranch){return platform==="win32"?buildPowerShellAgentShellCommand(agent,key,worktreePath,autoApprove,modelAlias,conductorEnabled,resumeMode,workflow,reviewRounds,baseBranch):buildPosixAgentShellCommand(agent,key,worktreePath,autoApprove,modelAlias,conductorEnabled,resumeMode,workflow,reviewRounds,baseBranch)}function buildGenericAgentShellCommand(agent,prompt,cwd,platform="darwin",modelAlias){if(platform==="win32"){let invocation2=buildAgentInvocation(agent,prompt,powershellSquote,modelAlias);return`Set-Location -LiteralPath ${powershellSquote(cwd)}; ${invocation2}`}let invocation=buildAgentInvocation(agent,prompt,p=>`'${shSquoteInner(p)}'`,modelAlias);return`cd '${shSquoteInner(cwd)}' && ${invocation}`}function terminalTitleForTicket(key){return`${key} Implementation`}function buildTerminalAppleScript(shellCommand,title){let esc=applescriptDquoteInner(shellCommand),titleEsc=applescriptDquoteInner(title);return['tell application "Terminal"'," activate"," if (count of windows) is 0 then",` set spawnedTab to do script "${esc}"`," else",' tell application "System Events" to keystroke "t" using command down'," delay 0.2",` set spawnedTab to do script "${esc}" in selected tab of front window`," end if",` set custom title of spawnedTab to "${titleEsc}"`,"end tell"].join(`
|
|
1665
1665
|
`)}function itermBadgeShellCommand(badgeText){return`printf '\\033]1337;SetBadgeFormat=%s\\007' '${Buffer.from(badgeText,"utf8").toString("base64")}'`}function buildITermAppleScript(shellCommand,title,badgeText){let esc=applescriptDquoteInner(shellCommand),lines=['tell application "iTerm"'," activate"," if (count of windows) = 0 then"," set spawnedSession to current session of (create window with default profile)"," else"," tell current window to set spawnedSession to (current session of (create tab with default profile))"," end if"," tell spawnedSession",` set name to "${applescriptDquoteInner(title)}"`];if(badgeText){let badgeEsc=applescriptDquoteInner(itermBadgeShellCommand(badgeText));lines.push(` write text "${badgeEsc}"`)}return lines.push(` write text "${esc}"`),lines.push(" end tell"),lines.push("end tell"),lines.join(`
|
|
1666
|
-
`)}async function spawnMacOSTerminalTab(deps,terminal,shellCommand,context){let title=context?.title??terminalTitleForTicket(context?.key??""),badgeText=context?.title??(context?.key||void 0),script=terminal==="iterm"?buildITermAppleScript(shellCommand,title,badgeText):buildTerminalAppleScript(shellCommand,title),result=await deps.runCommand("osascript",["-e",script]);if(commandSucceeded(result))return{ok:!0};let reason=(result.stderr||result.stdout||"").trim();return{ok:!1,error:`osascript failed to open a ${terminal} tab${reason?`: ${reason}`:""}`}}function buildWindowsTerminalArgs(worktreePath,shellCommand,title){let wtEscapedCommand=shellCommand.replace(/;/g,"\\;");return["new-tab","--title",title,"--suppressApplicationTitle","-d",worktreePath,"powershell.exe","-NoExit","-Command",wtEscapedCommand]}function buildPowerShellFallbackStartProcessCommand(worktreePath,shellCommand,title){let titledCommand=`$host.UI.RawUI.WindowTitle = ${powershellSquote(title)}; ${shellCommand}`,argumentList=`@('-NoExit', '-Command', ${powershellSquote(titledCommand)})`;return`Start-Process -FilePath 'powershell.exe' -WorkingDirectory ${powershellSquote(worktreePath)} -ArgumentList ${argumentList}`}async function spawnWindowsTerminalTab(deps,_terminal,shellCommand,context){let worktreePath=context?.worktreePath;if(!worktreePath)return{ok:!1,error:"Windows spawner requires a worktreePath context to open a tab."};let title=context?.title??terminalTitleForTicket(context?.key??"");if(await isCommandOnPath(deps,WINDOWS_TERMINAL_COMMAND)){let args=buildWindowsTerminalArgs(worktreePath,shellCommand,title),result2=await deps.runCommand(WINDOWS_TERMINAL_COMMAND,args);if(commandSucceeded(result2))return{ok:!0};let reason2=combineCommandOutput(result2);return{ok:!1,error:`wt.exe failed to open a Windows Terminal tab${reason2?`: ${reason2}`:""}`}}let powershell=await resolveFirstCommandOnPath(deps,WINDOWS_POWERSHELL_CANDIDATES);if(!powershell)return{ok:!1,error:"Windows Terminal (wt.exe) or PowerShell is required to open a tab, but neither was found on PATH."};let fallback=buildPowerShellFallbackStartProcessCommand(worktreePath,shellCommand,title),result=await deps.runCommand(powershell,["-NoProfile","-ExecutionPolicy","Bypass","-Command",fallback]);if(commandSucceeded(result))return{ok:!0};let reason=combineCommandOutput(result);return{ok:!1,error:`PowerShell failed to open a window via Start-Process${reason?`: ${reason}`:""}`}}function sanitizeTmuxName(value){let cleaned=value.replace(/[^A-Za-z0-9_-]+/g,"-").replace(/^-+/,"").replace(/-+$/,"");return cleaned.length>0?cleaned:"ticket"}function tmuxWindowNameForTicket(key){return terminalTitleForTicket(sanitizeTmuxName(key))}function tmuxWindowLabelFromTitle(title){let cleaned=title.replace(/[.:]+/g," ").replace(/\s+/g," ").trim();return cleaned.length>0?cleaned:"session"}function tmuxSessionPrefix(deps){let override=deps.env[TMUX_SESSION_OVERRIDE_ENV];if(override!==void 0){let trimmed=override.trim();if(trimmed.length>0)return trimmed}return DEFAULT_TMUX_SESSION_PREFIX}function tmuxSessionNameForTicket(deps,key){return`${tmuxSessionPrefix(deps)}-${sanitizeTmuxName(key)}`}function buildTmuxPaneCommand(shellCommand){return`${shellCommand}; exec $SHELL`}function buildTmuxNewSessionArgs(session,window,worktreePath,paneCommand){return["new-session","-d","-s",session,"-n",window,"-c",worktreePath,paneCommand]}function buildTmuxNewWindowArgs(session,window,worktreePath,paneCommand){return["new-window","-t",session,"-n",window,"-c",worktreePath,paneCommand]}async function spawnLinuxTmuxTerminalTab(deps,_terminal,shellCommand,context){let worktreePath=context?.worktreePath,key=context?.key;if(!worktreePath||!key)return{ok:!1,error:"Linux tmux spawner requires a worktreePath context to open a session."};if(!await isCommandOnPath(deps,TMUX_COMMAND))return{ok:!1,error:"tmux is required to spawn Linux sessions but was not found on PATH. Install tmux and retry."};let session=tmuxSessionNameForTicket(deps,key),window=context?.title?tmuxWindowLabelFromTitle(context.title):tmuxWindowNameForTicket(key),paneCommand=buildTmuxPaneCommand(shellCommand),hasSession=await deps.runCommand(TMUX_COMMAND,["has-session","-t",session]),args=commandSucceeded(hasSession)?buildTmuxNewWindowArgs(session,window,worktreePath,paneCommand):buildTmuxNewSessionArgs(session,window,worktreePath,paneCommand),result=await deps.runCommand(TMUX_COMMAND,args);if(commandSucceeded(result))return{ok:!0};let reason=combineCommandOutput(result);return{ok:!1,error:`tmux failed to create a session/window${reason?`: ${reason}`:""}`}}async function spawnUnsupportedPlatformTerminalTab(deps,_terminal,_shellCommand,_context){return{ok:!1,error:unsupportedPlatformMessage(deps.platform)}}function sanitizeKeyForLaunchScript(key){let cleaned=key.replace(/[^A-Za-z0-9._-]/g,"_");return cleaned.length>0?cleaned:"worker"}function buildLaunchScriptContent(platform,fullCommand){return platform==="win32"?`${fullCommand}
|
|
1666
|
+
`)}async function spawnMacOSTerminalTab(deps,terminal,shellCommand,context){let title=context?.title??terminalTitleForTicket(context?.key??""),badgeText=context?.title??(context?.key||void 0),script=terminal==="iterm"?buildITermAppleScript(shellCommand,title,badgeText):buildTerminalAppleScript(shellCommand,title),result=await deps.runCommand("osascript",["-e",script]);if(commandSucceeded(result))return{ok:!0};let reason=(result.stderr||result.stdout||"").trim();return{ok:!1,error:`osascript failed to open a ${terminal} tab${reason?`: ${reason}`:""}`}}function buildWindowsTerminalArgs(worktreePath,shellCommand,title){let wtEscapedCommand=shellCommand.replace(/;/g,"\\;");return["new-tab","--title",title,"--suppressApplicationTitle","-d",worktreePath,"powershell.exe","-ExecutionPolicy","Bypass","-NoExit","-Command",wtEscapedCommand]}function buildPowerShellFallbackStartProcessCommand(worktreePath,shellCommand,title){let titledCommand=`$host.UI.RawUI.WindowTitle = ${powershellSquote(title)}; ${shellCommand}`,argumentList=`@('-ExecutionPolicy', 'Bypass', '-NoExit', '-Command', ${powershellSquote(titledCommand)})`;return`Start-Process -FilePath 'powershell.exe' -WorkingDirectory ${powershellSquote(worktreePath)} -ArgumentList ${argumentList}`}async function spawnWindowsTerminalTab(deps,_terminal,shellCommand,context){let worktreePath=context?.worktreePath;if(!worktreePath)return{ok:!1,error:"Windows spawner requires a worktreePath context to open a tab."};let title=context?.title??terminalTitleForTicket(context?.key??"");if(await isCommandOnPath(deps,WINDOWS_TERMINAL_COMMAND)){let args=buildWindowsTerminalArgs(worktreePath,shellCommand,title),result2=await deps.runCommand(WINDOWS_TERMINAL_COMMAND,args);if(commandSucceeded(result2))return{ok:!0};let reason2=combineCommandOutput(result2);return{ok:!1,error:`wt.exe failed to open a Windows Terminal tab${reason2?`: ${reason2}`:""}`}}let powershell=await resolveFirstCommandOnPath(deps,WINDOWS_POWERSHELL_CANDIDATES);if(!powershell)return{ok:!1,error:"Windows Terminal (wt.exe) or PowerShell is required to open a tab, but neither was found on PATH."};let fallback=buildPowerShellFallbackStartProcessCommand(worktreePath,shellCommand,title),result=await deps.runCommand(powershell,["-NoProfile","-ExecutionPolicy","Bypass","-Command",fallback]);if(commandSucceeded(result))return{ok:!0};let reason=combineCommandOutput(result);return{ok:!1,error:`PowerShell failed to open a window via Start-Process${reason?`: ${reason}`:""}`}}function sanitizeTmuxName(value){let cleaned=value.replace(/[^A-Za-z0-9_-]+/g,"-").replace(/^-+/,"").replace(/-+$/,"");return cleaned.length>0?cleaned:"ticket"}function tmuxWindowNameForTicket(key){return terminalTitleForTicket(sanitizeTmuxName(key))}function tmuxWindowLabelFromTitle(title){let cleaned=title.replace(/[.:]+/g," ").replace(/\s+/g," ").trim();return cleaned.length>0?cleaned:"session"}function tmuxSessionPrefix(deps){let override=deps.env[TMUX_SESSION_OVERRIDE_ENV];if(override!==void 0){let trimmed=override.trim();if(trimmed.length>0)return trimmed}return DEFAULT_TMUX_SESSION_PREFIX}function tmuxSessionNameForTicket(deps,key){return`${tmuxSessionPrefix(deps)}-${sanitizeTmuxName(key)}`}function buildTmuxPaneCommand(shellCommand){return`${shellCommand}; exec $SHELL`}function buildTmuxNewSessionArgs(session,window,worktreePath,paneCommand){return["new-session","-d","-s",session,"-n",window,"-c",worktreePath,paneCommand]}function buildTmuxNewWindowArgs(session,window,worktreePath,paneCommand){return["new-window","-t",session,"-n",window,"-c",worktreePath,paneCommand]}async function spawnLinuxTmuxTerminalTab(deps,_terminal,shellCommand,context){let worktreePath=context?.worktreePath,key=context?.key;if(!worktreePath||!key)return{ok:!1,error:"Linux tmux spawner requires a worktreePath context to open a session."};if(!await isCommandOnPath(deps,TMUX_COMMAND))return{ok:!1,error:"tmux is required to spawn Linux sessions but was not found on PATH. Install tmux and retry."};let session=tmuxSessionNameForTicket(deps,key),window=context?.title?tmuxWindowLabelFromTitle(context.title):tmuxWindowNameForTicket(key),paneCommand=buildTmuxPaneCommand(shellCommand),hasSession=await deps.runCommand(TMUX_COMMAND,["has-session","-t",session]),args=commandSucceeded(hasSession)?buildTmuxNewWindowArgs(session,window,worktreePath,paneCommand):buildTmuxNewSessionArgs(session,window,worktreePath,paneCommand),result=await deps.runCommand(TMUX_COMMAND,args);if(commandSucceeded(result))return{ok:!0};let reason=combineCommandOutput(result);return{ok:!1,error:`tmux failed to create a session/window${reason?`: ${reason}`:""}`}}async function spawnUnsupportedPlatformTerminalTab(deps,_terminal,_shellCommand,_context){return{ok:!1,error:unsupportedPlatformMessage(deps.platform)}}function sanitizeKeyForLaunchScript(key){let cleaned=key.replace(/[^A-Za-z0-9._-]/g,"_");return cleaned.length>0?cleaned:"worker"}function buildLaunchScriptContent(platform,fullCommand){return platform==="win32"?`${fullCommand}
|
|
1667
1667
|
`:`#!/usr/bin/env bash
|
|
1668
1668
|
${fullCommand}
|
|
1669
1669
|
`}function buildLaunchScriptRunnerCommand(platform,scriptPath){return platform==="win32"?`. ${powershellSquote(scriptPath)}`:`. '${shSquoteInner(scriptPath)}'`}async function pruneStaleLaunchScripts(deps=defaultPruneStaleLaunchScriptsDeps){try{let parent=path17.join(os4.tmpdir(),"bridge-start-tickets"),entries;try{entries=await deps.readdir(parent)}catch{return}let cutoff=deps.now()-STALE_LAUNCH_SCRIPT_MAX_AGE_MS;for(let entry of entries){if(!entry.startsWith("w-"))continue;let full=path17.join(parent,entry);try{(await deps.stat(full)).mtimeMs<cutoff&&await deps.rm(full,{recursive:!0,force:!0})}catch{}}}catch{}}async function materializeWorkerLaunchCommand(deps,key,fullCommand){if(!deps.writeWorkerLaunchScript)return{ok:!0,command:fullCommand};try{let scriptPath=await deps.writeWorkerLaunchScript({platform:deps.platform,key,content:buildLaunchScriptContent(deps.platform,fullCommand)});return{ok:!0,command:buildLaunchScriptRunnerCommand(deps.platform,scriptPath)}}catch{return Buffer.byteLength(fullCommand,"utf8")<=MAX_TERMINAL_COMMAND_BYTES?{ok:!0,command:fullCommand}:{ok:!1,reason:"launch-script-write-failed-oversized-command",error:"Could not write the temporary launch script, and the full command is too long to send to the terminal directly. Check that the system temporary directory is writable."}}}async function spawnTabsForCreatedWorktrees(deps,rows,terminal,buildShellCommand){let out=[];for(let row of rows){if(row.status!=="created"||!row.path){out.push(row);continue}let baseShellCommand=buildShellCommand(row.key,row.path,row.modelAlias??null),shellCommand=injectConductorEnvIntoShellCommand(deps.platform,baseShellCommand,row.conductorEnv),materialized=await materializeWorkerLaunchCommand(deps,row.key,shellCommand);if(!materialized.ok){out.push({...row,status:"spawn-failed",error:materialized.error});continue}let result=await deps.spawnTerminalTab(deps,terminal,materialized.command,{key:row.key,worktreePath:row.path});result.ok?out.push({...row,status:"spawned"}):out.push({...row,status:"spawn-failed",error:result.error})}return out}function buildDryRunResults(keys,overrides){return keys.map(key=>({key,branch:resolveBranchForTicket(key,overrides),status:"dry-run"}))}function getDryRunPlatformDetails(agent,platform=process.platform,env=process.env,autoApprove=!1,conductorEnabled=!1,repoName=null,workflow="implement",reviewRounds,baseBranch){return{worktrunkBinary:resolveWorktrunkBinary(platform,env),buildAgentShellCommand:(key,worktreePath,modelAlias)=>prependRepoNameEnvAssignment(buildAgentShellCommand(agent,key,worktreePath,platform,autoApprove,modelAlias,conductorEnabled,!1,workflow,reviewRounds,baseBranch),repoName,platform)}}function buildDryRunMcpProvisioningLines(worktreePath,platform=process.platform,mcpServerInvocation){let api=platform==="win32"?path17.win32:path17.posix,mcpJson=api.join(worktreePath,".mcp.json"),cursorJson=api.join(worktreePath,".cursor","mcp.json"),built=buildMcpShimCommand(mcpServerInvocation??{form:"npm-channel",command:"npx",packageSpec:"@bridge_gpt/mcp-server@latest"},"<target>",worktreePath),shim=`${built.command} ${built.args.join(" ")}`;return["DRY-RUN: MCP provisioning (target-driven from .bridge/config \u2014 bapi plus any","DRY-RUN: supported Tier-2 target such as sfcc): would write a secret-free shim","DRY-RUN: entry per target to",`DRY-RUN: ${mcpJson}`,`DRY-RUN: ${cursorJson}`,`DRY-RUN: ${shim}`]}function buildDryRunDetailLines(agent,key,branch,platform=process.platform,env=process.env,baseBranch="main",autoApprove=!1,modelAlias=null,conductorEnabled=!1,repoName=null,mcpServerInvocation,workflow="implement",reviewRounds){let{worktrunkBinary,buildAgentShellCommand:build}=getDryRunPlatformDetails(agent,platform,env,autoApprove,conductorEnabled,repoName,workflow,reviewRounds,baseBranch),wtArgs=buildWtSwitchArgs(branch,!1,baseBranch),agentInvocation=build(key,"<worktree-path>",modelAlias);return[`DRY-RUN: ${key} -> branch=${branch}`,`DRY-RUN: ${worktrunkBinary} ${wtArgs.join(" ")}`,`DRY-RUN: ${agentInvocation}`,...buildDryRunMcpProvisioningLines("<worktree-path>",platform,mcpServerInvocation)]}function formatSummaryReport(rows){let lines=["Summary:"],runId=rows.find(r=>r.runId)?.runId;runId&&lines.push(`run_id=${runId}`);let supervisorStatus=rows.find(r=>r.supervisorStatus)?.supervisorStatus;supervisorStatus&&lines.push(`supervisor=${supervisorStatus}`);for(let row of rows){let line=`${row.key} branch=${row.branch} status=${row.status}`;row.path&&(line+=` path=${row.path}`),row.workerId&&(line+=` worker_id=${row.workerId}`),row.mcpRegistrationForm&&(line+=` mcp_registration=${row.mcpRegistrationForm}`),lines.push(line)}let warningLines=[];for(let row of rows){let messages=[];(row.status==="create-failed"||row.status==="spawn-failed")&&messages.push(row.error??row.status);for(let warning of row.warnings??[])messages.push(warning);let seen=new Set;for(let message of messages)seen.has(message)||(seen.add(message),warningLines.push(` ${row.key}: ${message}`))}return warningLines.length>0&&(lines.push(""),lines.push("Warnings:"),lines.push(...warningLines)),lines.join(`
|
|
@@ -3916,7 +3916,7 @@ active \u2014 the server-side reconciler will pick it up within ~30s."
|
|
|
3916
3916
|
## Return
|
|
3917
3917
|
|
|
3918
3918
|
Confirm the overview was written to \`{docs_dir}/epic-plans/{epic_slug}/overview.md\` and report the total sub-task count along with a one-line summary of the epic plan. State whether the goals/NFRs + recommended order were posted as a comment on \`{epic_key}\` or skipped because no epic key was provided.
|
|
3919
|
-
`};init_version_generated();var README='# @bridge_gpt/mcp-server\n\nThe Bridge MCP is an end-to-end accelerator for shipping code within SFCC, powered by [Bridge API](https://bridgegpt-api.com). Works with Claude Code, Github Copilot, Cursor, Windsurf, and OpenAI Codex.\n\n## Contents\n\n- [Getting Started](#getting-started)\n- [Usage Documentation](#usage-documentation)\n - [Tier 1 \u2014 Regularly useful](#tier-1--regularly-useful)\n - [Tier 2 \u2014 Occasionally useful](#tier-2--occasionally-useful)\n - [Tier 3 \u2014 Now and then](#tier-3--now-and-then)\n - [Operational commands](#operational-commands)\n - [Extra Capabilities](#extra-capabilities)\n- [Salesforce B2C Commerce (SFCC) Tools](#salesforce-b2c-commerce-sfcc-tools)\n- [CLI Subcommands](#cli-subcommands)\n- [Custom Pipelines](#custom-pipelines)\n- [Environment Variables](#environment-variables)\n- [Worktree credentials and the `mcp-invoke` shim](#worktree-credentials-and-the-mcp-invoke-shim)\n- [Reference](#reference)\n\nFor advanced epic/multi-agent orchestration, see [CONDUCTOR.md](./docs/CONDUCTOR.md).\n\n## Getting Started\n\n### Quick start\n\nFrom your **project root**, install and connect in one command:\n\n```bash\nnpx -y @bridge_gpt/mcp-server@latest install-bridge\n```\n\nRun bare like that in a terminal and it starts by asking\n**`Do you have a Bridge API key or invite? [Y/n]`**:\n\n- **Yes** (or just press Enter) \u2014 the existing-key flow. It asks for your **API key**\n (generate one on the Bridge API web UI **Security** page) and a **repo name**\n matching your server-side registration; everything else is derived. A\n `bapi_inv_\u2026` credential entered here instead of a full API key is automatically\n detected and redeemed as a **bootstrap invite** \u2014 it creates a brand-new project\n and mints your admin API key rather than looking up an existing repository.\n- **No** \u2014 the **self-serve** flow. It asks for an **email**, then a name for your new\n Bridge project, and creates the workspace and your own admin API key for you. No\n account, no key, and no invite needed beforehand. Same as passing\n `--email you@example.com` (see below).\n\nThat question is asked only for a *bare interactive* run. Passing any flag, setting\n`BAPI_API_KEY`, or running without an interactive terminal skips it and keeps the\nexisting deterministic behavior.\n\nFrom there `install-bridge` scaffolds the project, writes your editor\'s MCP config\nwith real values, verifies connectivity, persists your API key to the user-scoped\ncredential store, and opens a fresh agent session that runs `/install-bridge` to\nderive and apply the remaining config, presents a **capability report** (what you can\nuse now and what you\'ll unlock), and closes by asking whether to index the\nrepository. It does **not** automatically run `/learn-repository` or index without\nyour consent \u2014 both remain available as separate steps. Add `--dry-run` to preview\nevery step without writing, pinging, or spawning anything.\n\n**Were you sent a bootstrap invite?** Then you don\'t need an API key or the web UI\nat all \u2014 run the command your operator gave you:\n\n```bash\nnpx -y @bridge_gpt/mcp-server@latest install-bridge --invite\n```\n\nThat one-liner is deliberately **secret-free**: the CLI prompts for the bootstrap\ninvite token with **echo suppressed**, and sends it only in the request body. It\ncreates your project and mints your own admin API key in a single command.\n\nTo upgrade later, run:\n\n```bash\nnpx -y @bridge_gpt/mcp-server upgrade\n```\n\n`upgrade` re-execs from `@latest`, refreshes all scaffolded artifacts (slash\ncommands, agents, pipelines), rewrites the version pin, and reconnects \u2014 also\navailable as the `/upgrade-bridge` slash command. (The legacy `--upgrade` flag still\nworks as a fallback.)\n\n<details>\n<summary><strong>Installation Instructions</strong></summary>\n\n#### What `install-bridge` does\n\n`install-bridge` collapses the whole setup into a single command. It:\n\n1. **Scaffolds** the project (the same artifacts `--init` writes: slash commands,\n agents, `.bridge/pipelines/`, and secret-free MCP config placeholders).\n2. **Writes the per-host MCP config** (`.mcp.json` / `.cursor/mcp.json` /\n `.vscode/mcp.json`) with your real `BAPI_REPO_NAME` / `BAPI_API_KEY` /\n `BAPI_BASE_URL` / `BAPI_DOCS_DIR`, preserving any unrelated servers. The\n launcher it writes is pinned to the exact installed version so `npx` never\n silently reuses a stale local copy. (Windsurf and Codex are global configs it\n can\'t safely write \u2014 it prints copy-paste instructions for those.)\n3. **Verifies connectivity** against the Bridge API before persisting anything.\n4. **Persists your key** to the user-scoped credential store\n (`~/.config/bridge/credentials.json`, target `bapi:<repo>`) so shell-spawned\n tooling (e.g. `start-tickets`) can resolve it.\n5. **Opens a fresh agent session** that runs `/install-bridge` to derive and apply\n the remaining config fields from your codebase, presents a concise capability\n report ("What Bridge can help with"), and recommends `/learn-repository` as the\n next step. It does not chain into running `/learn-repository` itself \u2014 that\'s\n your next explicit invocation. There is no indexing question anywhere: indexing\n starts automatically once the repository reaches full parse readiness (VCS\n credentials, the code index prerequisites, and project description), so you\n never need to ask for it or run `/parse-repository` yourself as part of\n onboarding.\n\nIn this **existing-key** flow the only inputs are an **API key** and a **repo name**\n(everything else is derived). Resolution order:\n\n- **API key:** `--api-key <key>` \u2192 `BAPI_API_KEY` env \u2192 an interactive (no-echo)\n prompt. Generate one first on the Bridge API web UI **Security** page (see\n [Generate an API Key](#2-generate-an-api-key)); in this flow the command consumes\n a key, it never mints one \u2014 **`--email` and `--invite` are the two exceptions**\n (below), and each mints your first key. All three of `--api-key`, `BAPI_API_KEY`,\n and the hidden prompt also accept a bootstrap-invite value (`bapi_inv_\u2026`) \u2014\n detected automatically and redeemed the same way `--invite` is, skipping\n repository lookup entirely. `--invite` and `--email` remain the preferred,\n explicit entry points for a new project. The key is **never printed or logged**.\n- **Repo name:** `--repo <name>` and `BAPI_REPO_NAME` remain the deterministic\n short-circuits and compatibility fallbacks \u2014 when either is set it is used\n directly, with no network round-trip. When **neither** is set, a compatible\n server resolves the unique repository from your existing API key automatically\n (a read-only lookup), so you don\'t have to supply it. If the server is older,\n the key can\'t be uniquely resolved, or the lookup fails, `install-bridge` falls\n back to an inferred default you confirm interactively (and requires `--repo`\n when stdin is non-interactive). Whatever name is used MUST match the\n server-side repository registration.\n\n#### Self-serve email onboarding (`--email`) \u2014 no account, no key, no invite\n\nThe primary path for a **first-time user with nothing yet** \u2014 no Bridge account,\nno API key, and no pre-issued invite. Run:\n\n```bash\nnpx -y @bridge_gpt/mcp-server@latest install-bridge --email you@example.com\n```\n\n`install-bridge` requests a brand-new Bridge workspace for that email, receives an\ninvite token, and then feeds it into the **exact same** persist-before-exchange\nbootstrap protocol as `--invite` below \u2014 so the project is created and your first\nadmin key is minted in one command. The minted token is used internally and **never\nshown**.\n\nThe email may instead come from the `BAPI_SIGNUP_EMAIL` environment variable or a\n**visible** interactive prompt (email is not a secret, so it is echoed as you type \u2014\nunlike the API key and the invite token, which use a hidden prompt). That prompt is\nwhat answering **no** to `Do you have a Bridge API key or invite? [Y/n]` on a bare run reaches,\nso `install-bridge --email you@example.com` and a bare `install-bridge` + "no" land\nin the same place. The email is still **never written to a log line**. No email\nverification is performed and no message is sent to the address \u2014 it only labels your\nnew workspace. `--email` is mutually exclusive with `--api-key` and `--invite`.\n\nBecause this flow *creates* the project, it asks you to **name a new project**\n(`Name your new Bridge project [<inferred>]: `) rather than to match an existing\nserver-side registration. The name must be globally unique; if it\'s taken, you\'re\nasked for another one and the invite is not consumed. The same applies to `--invite`.\n\n#### Bootstrap-invite onboarding (`--invite`)\n\nWith a **bootstrap invite** you were already given, there is no pre-existing key and\nno web UI: like `--email` above, this mode **creates** the project and its first\nadmin key instead of consuming one. Run `install-bridge --invite` and it:\n\n1. **Prompts for the bootstrap invite token** with echo suppressed (the default \u2014\n see below).\n2. **Generates your `key_secret`** (32 CSPRNG bytes) and **fsyncs it locally**\n *before* contacting the server. If that write fails the run aborts and the\n invite is **not** spent.\n3. **Redeems the invite** \u2014 `POST /setup/bootstrap` with the token, repo name, and\n `key_secret` in the **body** \u2014 which creates the project and mints your admin\n key. (This replaces the connectivity ping: there is no key to ping with yet.)\n4. **Verifies the newly-minted key**, then writes the per-host MCP config.\n5. **Promotes the credential** to `bapi:<repo>` and **opens the agent session** \u2014\n the same Steps 3\u20135 as the normal flow.\n\nBecause the locally-saved `key_secret` is the only proof that can replay a\nredemption, a re-run after a network failure is safe: it re-sends the *same* secret\nand gets back the *same* project and key. If the repo name you chose is already\ntaken (names are globally unique) the server rolls back \u2014 your invite is untouched \u2014\nand the CLI asks for a different name and retries with the same token.\n\n**The delivered one-liner is secret-free, by design.** "A copy/paste one-liner" and\n"the token never touches shell history" are contradictory, so the token is *not* in\nthe command: the CLI asks for it, and it travels only in the request body.\n`--invite <token>`, `--invite=<token>`, and `BAPI_INVITE` still work for\n**scripting only** \u2014 and both forms **expose the token to your shell history and to\nthe process list**. Prefer the prompt.\n\nUseful flags:\n\n- `--dry-run` \u2014 preview every step (scaffold targets, config files and keys with\n the key value **redacted**, the ping target, the credential store target, and\n the exact agent spawn command) without writing, pinging, or spawning anything.\n With `--invite` it also never calls the exchange endpoint and never generates or\n stores a secret.\n- `--force` \u2014 overwrite an existing real `BAPI_API_KEY` in a host config, or in the\n credential store, without prompting (re-running is otherwise non-destructive).\n- `--agent claude|cursor-agent` \u2014 which agent to launch for the agentic remainder\n (default `claude`).\n- `--email <addr>` \u2014 self-serve signup: create a new workspace from just an email\n (mutually exclusive with `--api-key` and `--invite`). Falls back to\n `BAPI_SIGNUP_EMAIL`, then a visible prompt. Visible input, not a secret; still\n never logged.\n- `--invite [token]` \u2014 redeem a bootstrap invite (mutually exclusive with\n `--api-key` and `--email`). Omit the value to get the hidden prompt.\n\nThat\'s it \u2014 once `install-bridge` finishes you\'re connected. If you prefer to do\nit by hand (or just want to understand each step), the manual flow below does the\nsame thing.\n\n#### Manual Setup (Alternative)\n\n##### 1. Install the Package\n\nFrom your **project root**, install the MCP server and scaffold slash commands:\n\n```bash\nnpm i @bridge_gpt/mcp-server\nnpx -y @bridge_gpt/mcp-server --init\n```\n\n`--init` must be run from the directory containing your `package.json`. It:\n\n- Creates slash commands in `.claude/commands/` and `.cursor/commands/`\n- Detects existing MCP config files and sets `BAPI_PROJECT_ROOT` so local file output resolves correctly\n- Scaffolds `.bridge/pipelines/` for custom pipeline authoring\n\nRe-run `--init` after upgrading the package to get updated commands.\n\n##### 2. Generate an API Key\n\n1. Log in to [Bridge API](https://bridgegpt-api.com) and navigate to your project\'s **Security** page\n2. Click **Create New Key**\n3. Enter your email, an optional label (e.g., "MCP Server"), and select the **Admin** role\n4. Click **Create Key**\n5. **Copy the key immediately** \u2014 it will not be shown again\n\n##### 3. Configure the MCP Server\n\nAdd the following to your editor\'s MCP configuration file, pasting in the API key from step 2:\n\n<details>\n<summary><strong>Claude Code (.mcp.json)</strong></summary>\n\nThe `--init` command (step 1) detects Claude Code and creates a `.mcp.json` at your project root with placeholder values. Open it and replace `your-repo` and `your-api-key` with your actual values from step 2:\n\n```json\n{\n "mcpServers": {\n "bridge-api": {\n "command": "npx",\n "args": ["-y", "@bridge_gpt/mcp-server"],\n "env": {\n "BAPI_BASE_URL": "https://bridgegpt-api.com",\n "BAPI_REPO_NAME": "your-repo",\n "BAPI_API_KEY": "your-api-key",\n "BAPI_DOCS_DIR": "docs/tmp"\n }\n }\n }\n}\n```\n</details>\n\n<details>\n<summary><strong>VS Code / Copilot (.vscode/mcp.json)</strong></summary>\n\n```json\n{\n "servers": {\n "bridge-api": {\n "command": "npx",\n "args": ["-y", "@bridge_gpt/mcp-server"],\n "env": {\n "BAPI_BASE_URL": "https://bridgegpt-api.com",\n "BAPI_REPO_NAME": "your-repo",\n "BAPI_API_KEY": "your-api-key",\n "BAPI_DOCS_DIR": "docs/tmp"\n }\n }\n }\n}\n```\n</details>\n\n<details>\n<summary><strong>Cursor (.cursor/mcp.json)</strong></summary>\n\n```json\n{\n "mcpServers": {\n "bridge-api": {\n "command": "npx",\n "args": ["-y", "@bridge_gpt/mcp-server"],\n "env": {\n "BAPI_BASE_URL": "https://bridgegpt-api.com",\n "BAPI_REPO_NAME": "your-repo",\n "BAPI_API_KEY": "your-api-key",\n "BAPI_DOCS_DIR": "docs/tmp"\n }\n }\n }\n}\n```\n\n> If project-local config is not supported in your Cursor version, use `~/.cursor/config/mcp.json` instead.\n</details>\n\n<details>\n<summary><strong>Windsurf (~/.codeium/windsurf/mcp_config.json)</strong></summary>\n\nWindsurf only supports global MCP configuration.\n\n```json\n{\n "mcpServers": {\n "bridge-api": {\n "command": "npx",\n "args": ["-y", "@bridge_gpt/mcp-server"],\n "env": {\n "BAPI_BASE_URL": "https://bridgegpt-api.com",\n "BAPI_REPO_NAME": "your-repo",\n "BAPI_API_KEY": "your-api-key",\n "BAPI_DOCS_DIR": "docs/tmp"\n }\n }\n }\n}\n```\n</details>\n\n<details>\n<summary><strong>OpenAI Codex (~/.codex/config.toml)</strong></summary>\n\n```toml\n[mcp_servers.bridge-api]\ncommand = "npx"\nargs = ["-y", "@bridge_gpt/mcp-server"]\n\n[mcp_servers.bridge-api.env]\nBAPI_BASE_URL = "https://bridgegpt-api.com"\nBAPI_REPO_NAME = "your-repo"\nBAPI_API_KEY = "your-api-key"\nBAPI_DOCS_DIR = "docs/tmp"\n```\n\n> Codex users: set `BAPI_PROJECT_ROOT` manually in your config (see Environment Variables below).\n</details>\n\nAfter saving the config, restart your editor or reload the MCP server connection. Verify connectivity by asking your AI assistant to call the `ping` tool.\n\n##### 4. First-Time Setup: Teach Bridge Your Codebase\n\nIf you\'re the first person to install Bridge API on your project, run the `/learn-repository` slash command after completing setup. This analyzes your codebase\'s architecture, testing patterns, code review standards, and documentation conventions, then uploads the findings to Bridge API. This gives Bridge the context it needs to generate implementation plans, ticket critiques, and code reviews that are consistent with your project\'s actual architecture and conventions.\n\nYou only need to do this once per project \u2014 the learned standards persist for all team members.\n\n##### Upgrading (details)\n\nThe one-command `npx -y @bridge_gpt/mcp-server upgrade` (shown above) is the\nrecommended path. The legacy flag `npx -y @bridge_gpt/mcp-server --upgrade` still\nworks: it runs `npm i @bridge_gpt/mcp-server@latest`, prints a before/after version\nsummary, then re-runs the full `--init` scaffolding flow to update your slash\ncommands, agents, and pipeline definitions.\n\nThe MCP server also checks for updates automatically on startup. If a newer version is available, you\'ll see a notice in your editor\'s MCP output logs with the upgrade command to run. This check is cached for 24 hours and never blocks server startup.\n\n</details>\n\n## Usage Documentation\n\nThis is the Bridge API tooling worth knowing about as a software engineer \u2014 the things you\'d ask an agent to do \u2014 grouped by how often you would use them. Each entry covers **what it does**, **when it\'s useful** and **how to use it**. The behind-the-scenes plumbing is summarized at the end under [Extra Capabilities](#extra-capabilities), and a full enumeration lives in [Reference](#reference).\n\nWorking in a Salesforce B2C Commerce codebase? Bridge also ships read-only SFCC platform-introspection tools \u2014 see [Salesforce B2C Commerce (SFCC) Tools](#salesforce-b2c-commerce-sfcc-tools).\n\nFor invocation, prefer the slash command \u2014 it\'s deterministic. A free-text example is shown only where natural-language phrasing reliably maps to the right automation; high-consequence or easily-misread automations show only the slash command on purpose.\n\n### Tier 1 \u2014 Regularly useful\n\nThese features are useful for most tickets.\n\n**1. Review Ticket**\n- **What it does:** Runs a full quality review of a ticket: clarifying questions + critique plus an automatic alternate-model second opinion, then evaluates findings and produces a decision page to accept/reject them. The backend executor now owns all review round orchestration (including the second-opinion round) server-side; pass `--rounds=1` for a cheaper single-pass review, `--rounds=2` to force the full second-opinion review, or omit `--rounds` to let the backend\'s difficulty-adaptive review policy decide.\n- **When it\'s useful:** (Refinement) Right after a ticket is drafted, before anyone starts building \u2014 to surface gaps and tighten it.\n- **How to use it:** `/review-ticket BAPI-123` (command only \u2014 "review" as free text is easily mistaken for a freehand agent review).\n- **Flags:** `--auto` auto-accept findings / skip the approval gates \xB7 `--rounds=1` request a cheaper single-pass review (no second-opinion round) while preserving downstream evaluation and decision-capture work \xB7 `--rounds=2` force the full second-opinion review \xB7 omit `--rounds` to let the backend\'s difficulty-adaptive review policy decide (it falls back to a full second-opinion review when adaptive routing is disabled or the ticket\'s difficulty cannot be resolved).\n- **Multi-ticket fan-out:** `/review-tickets BAPI-123 BAPI-456` opens one terminal tab per ticket for parallel review with no worktrees (terminal launcher only \u2014 no `wt`/`git`). All `/review-ticket` flags apply; `--review KEY=auto,rounds=N` sets per-ticket overrides. Packaged CLI: `npx -y @bridge_gpt/mcp-server review-tickets KEY [KEY ...]`.\n\n**2. Start Tickets**\n- **What it does:** Creates one git worktree per ticket and spawns an agent session in each to implement them in parallel.\n- **When it\'s useful:** (Implementation | Automation) When you\'re ready to start building one or more refined tickets concurrently.\n- **How to use it:** `/start-tickets BAPI-248 BAPI-250` (see [CLI Subcommands](#cli-subcommands) for the full flag table and cross-platform behavior).\n- **Flags:** `--auto` skip the approval gates \xB7 `--base-branch <branch>` branch off something other than the default \xB7 `--workflow implement|review-and-implement` selects which slash command each spawned worktree runs (default `implement`, byte-identical to today; `review-and-implement` runs `/review-ticket` inline then, after a per-ticket halt gate, hands off to a **fresh** `/implement-ticket` session reusing the same worktree) \xB7 `--rounds=1|2` review-only, forwarded to the review phase, valid only with `--workflow review-and-implement` \xB7 `--tier cheap|basic|premium` coarse model-routing override (see [CLI Subcommands](#cli-subcommands)).\n\n**3. Council**\n- **What it does:** Fans your problem out to two different LLMs and returns their approaches directly. Runs in one of four modes, selected via `mode`: **`technical`** (default \u2014 implementation/architecture approaches), **`design`** (UI/UX and visual direction), **`discovery`** (stakeholder discovery questions for early/vague tasks, grouped into `Technical Discovery Questions` and `Business / Stakeholder Discovery Questions` and tagged `[HUMAN]`/`[CODE]`/`[TICKET]`), or **`general`** (brief-driven ideation from your task description and concerns alone). `technical` and `discovery` are codebase-grounded \u2014 they retrieve from the repository index and need a successfully indexed repo. `general` needs no code index at all, so it works immediately after install, before `/parse-repository` has ever run. The legacy boolean `design=true` still works and maps to `mode: "design"`.\n- **When it\'s useful:** (Architecture | Refinement) Early, when you want a spread of approaches \u2014 `technical` for *how to build it*, `design` for *how it should look*, `discovery` for *what we still need to figure out* before a real ticket exists, `general` for a quick brief-driven council before the repository is indexed.\n- **How to use it:** ask your agent to convene a council \u2014 *"Convene a council on approaches for adding rate limiting to the LLM client; fan it out to multiple models."* For a design pass: *"Run a design council for the evidence-freshness dashboard UI."* For early discovery: *"Run a discovery council \u2014 `request_council` with `mode: "discovery"` \u2014 for this vague request so we can collect the questions stakeholders need to answer first."* For a fresh, unindexed repo: *"Run a general council \u2014 `request_council` with `mode: "general"` \u2014 on launch options for this idea."*\n\n**4. Deep Research**\n- **What it does:** Runs multi-source, fact-checked web research on a technical topic and returns a cited report.\n- **When it\'s useful:** (Architecture | Refinement) When a decision hinges on outside knowledge (libraries, best practices, standards) you don\'t already have.\n- **How to use it:** `/bridge-research <question>`\n\n**5. Jira Ticket Writer**\n- **What it does:** An agent that drafts a well-structured Jira ticket from a plain description, applying your project\'s standards.\n- **When it\'s useful:** (Refinement) When you have an idea in your head and want a properly-formatted ticket draft without writing it by hand.\n- **How to use it:** `/write-ticket <description>` (or ask your agent) \u2014 *"Use the jira ticket writer to turn our conversation into a ticket."*\n- **Flags:** `--standards <path>` apply a specific standards file when drafting.\n\n**6. Upload Ticket**\n- **What it does:** Pushes a drafted ticket up to Jira as a real issue (the `create_ticket` capability); handles markdown and child tickets under an Epic.\n- **When it\'s useful:** (Refinement) The final step after drafting \u2014 to get the ticket into Jira so it can be tracked and worked.\n- **How to use it:** Ask your agent to create the ticket, and it should confirm before creating the live Jira issue.\n- **Options:** Describe the issue type (Bug / Story / Task / Epic) and, for a child under an Epic, the parent key.\n\n### Tier 2 \u2014 Occasionally useful\n\nThese features are good to know, but you probably won\'t use them every day.\n\n**1. Plan Ticket**\n- **What it does:** Generates a step-by-step implementation plan for a ticket, with references to real code files, and saves it locally.\n- **When it\'s useful:** (Refinement | Implementation) Once a ticket is solid and you want a concrete build plan before (or instead of) auto-implementing.\n- **How to use it:** `/plan-ticket BAPI-123`\n- **Flags:** `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check the plan with a second provider.\n\n**2. Clarify Ticket**\n- **What it does:** Generates clarifying questions for a ticket (or debugging guidance for bugs) and saves them locally.\n- **When it\'s useful:** (Refinement) When a ticket feels under-specified and you want the open questions made explicit.\n- **How to use it:** `/clarify-ticket BAPI-123` \u2014 *"Generate clarifying questions for BAPI-123"*\n- **Flags:** `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check with a second provider.\n\n**3. Critique Ticket**\n- **What it does:** Critiques a ticket\'s quality against your project standards and lists deviations + improvements.\n- **When it\'s useful:** (Refinement) When you want a quality gate on a ticket before it\'s worked.\n- **How to use it:** `/critique-ticket BAPI-123` \u2014 *"Critique BAPI-123 against our project standards and list what\'s missing or deviating."*\n- **Flags:** `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check with a second provider.\n\n**4. Create Doc**\n- **What it does:** Generates a design document for a ticket \u2014 a TDD (technical design, engineer audience), an FSD (functional spec, for product/design/QA), or a PRD (product requirements: problem, goals, success metrics) \u2014 and saves it locally.\n- **When it\'s useful:** (Architecture | Refinement) When a ticket needs a fuller design write-up before planning or implementation, in the shape that fits your audience.\n- **How to use it:** `/create-doc BAPI-123 --doc-type tdd` (or `fsd` / `prd`)\n- **Flags:** `--doc-type tdd|fsd|prd` which document to generate (required) \xB7 `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check with a second provider.\n\n**5. Explore Ticket**\n- **What it does:** Explores the codebase for a task and recommends implementation options or surfaces clarifying questions, with optional research.\n- **When it\'s useful:** (Architecture | Refinement) Before writing a ticket or plan, when you\'re unsure how a change would fit the existing code.\n- **How to use it:** `/explore-ticket <task>` \u2014 *"Explore the codebase for how we\'d add a Mistral LLM provider and recommend 2\u20133 implementation options."*\n\n**6. Second Opinion**\n- **What it does:** Gets an immediate critique of any text from a different model family \u2014 no artifact saved, just the reply.\n- **When it\'s useful:** (Architecture | Refinement | Implementation) Any time you want a quick sanity check on a plan, draft, or decision from a fresh perspective.\n- **How to use it:** ask your agent \u2014 *"Get a second opinion from Gemini on whether the BAPI-123 plan\'s migration step is safe to run against prod."*\n- **Options:** pick the provider (anthropic / openai / gemini) and the tier (cheap / basic / premium).\n\n**7. Generate Image**\n- **What it does:** Generates an image from a text prompt using a provider image model (OpenAI `gpt-image-2` by default, or Google Imagen) and returns the image directly. Spends provider credits on every call.\n- **When it\'s useful:** (Architecture | Refinement) When you want a quick visual \u2014 a UI mockup, diagram, or illustration \u2014 to anchor a design discussion or attach to a ticket.\n- **How to use it:** ask your agent \u2014 *"Generate an image of a dashboard showing SOC2 evidence freshness as a traffic-light grid."*\n- **Options:** `provider` openai (`gpt-image-2`) / gemini (Imagen \u2014 adds an invisible SynthID watermark) \xB7 `quality` low (default, cheapest) / medium / high \xB7 `size` 1024x1024 / 1024x1536 / 1536x1024. The image is always saved to `BAPI_DOCS_DIR/images/` and also returned inline.\n\n**8. Implement Ticket**\n- **What it does:** Full build for one ticket: generate a plan, write the code, commit, open a PR, and monitor CI.\n- **When it\'s useful:** (Implementation) When a ticket is ready and you want it taken from plan to open PR in one go.\n- **How to use it:** `/implement-ticket BAPI-123` (command only \u2014 "implement X" as free text almost always triggers a freehand build instead of the Bridge plan\u2192code\u2192PR\u2192CI pipeline).\n- **Flags:** `--auto` skip the approval gates (e.g. auto-commit/push).\n\n**9. Full Automation**\n- **What it does:** Drives the whole chain end-to-end: idea \u2192 ticket(s) \u2192 review each \u2192 spawn worktrees to implement.\n- **When it\'s useful:** (Automation) When you want to go from a raw idea to in-progress implementation with minimal hands-on steps.\n- **How to use it:** `/full-automation <idea>` (command only \u2014 creates tickets, spawns worktrees, and carries scheduling/`--max-children` flags that free text can\'t).\n- **Flags:** `--require-approval` toggle the approval gates, full automation runs end to end by default.\n\n**10. Idea to Ticket**\n- **What it does:** Turns a one-line idea into a Jira Task/Spike (or an Epic plus child tickets), with research, duplicate detection, and a critique pass built in.\n- **When it\'s useful:** (Refinement | Automation) When you have a rough idea and want a fully-formed, uploaded ticket without the manual draft-and-refine loop.\n- **How to use it:** `/idea-to-ticket <idea>`\n\n### Tier 3 \u2014 Now and then\n\nThese features are useful once in a while, but you probably won\'t need them everyday.\n\n**1. Reimplement Ticket**\n- **What it does:** Pulls in new context/attachments since the last pass and implements small follow-up changes on an already-built ticket.\n- **When it\'s useful:** (Implementation) After review feedback or new screenshots, when you need a targeted second pass rather than a fresh build.\n- **How to use it:** `/reimplement-ticket BAPI-123`\n\n**2. Run Tests**\n- **What it does:** Runs the unit and E2E suites and autonomously triages/fixes failures (via the test-correction agent).\n- **When it\'s useful:** (Implementation) After making changes, to confirm everything passes and auto-fix straightforward breakages.\n- **How to use it:** `/run-tests` (`--unit-only`, `--skip-e2e`)\n\n**3. Plan Epic**\n- **What it does:** Decomposes a large epic into sub-tasks with a structured exploration doc for each.\n- **When it\'s useful:** (Architecture | Refinement) When a feature is too big for one ticket and you need it broken down and scoped.\n- **How to use it:** `/plan-epic <epic>` \u2014 *"Decompose the epic \'migrate PayPal token storage off Custom Objects\' into sub-tasks with an exploration doc for each."*\n\n**4. Update Ticket**\n- **What it does:** Synthesizes a ticket\'s clarifying answers and critique into a rewritten description and pushes it to Jira.\n- **When it\'s useful:** (Refinement) After review, to fold the resolved questions and fixes back into the ticket itself.\n- **How to use it:** `/update-ticket BAPI-123` (command only \u2014 does a full overwrite of the live Jira description; "update" as free text is both vague and hard to reverse).\n\n**5. Get Ticket**\n- **What it does:** Retrieves the full details of a Jira ticket (summary, status, description, etc.).\n- **When it\'s useful:** (Refinement | Implementation) Any time you want the agent to read a ticket before acting on it.\n- **How to use it:** ask your agent \u2014 *"Pull up BAPI-123 and show me its description, status, and acceptance criteria."*\n\n**6. Write Comment**\n- **What it does:** Posts a comment on a Jira ticket (markdown; long ones can attach as a file).\n- **When it\'s useful:** (Refinement | Implementation) To leave context, status, or a decision trail on the ticket.\n- **How to use it:** ask your agent \u2014 *"Post a comment on BAPI-123: blocked on the expired Atlassian token \u2014 will retry after it\'s rotated."*\n\n**7. Download / Upload Attachment**\n- **What it does:** Pulls files off a Jira ticket to disk, or attaches a local file to a ticket.\n- **When it\'s useful:** (Refinement | Implementation) When a ticket has design files/logs you need locally, or you want to attach output back to it.\n- **How to use it:** ask your agent \u2014 *"Download the design mockups attached to BAPI-123 into my docs folder."* / *"Attach build-log.txt to BAPI-123."*\n\n**8. Learn Repository**\n- **What it does:** Researches and documents the repo\'s architecture, testing, review, and correctness standards, then saves them to Bridge for future agents.\n- **When it\'s useful:** (Setup/Learning) When onboarding a new repo, or after big changes, so Bridge\'s agents follow your conventions.\n- **How to use it:** `/learn-repository`\n\n**9. Teach Bridge**\n- **What it does:** Takes a plain-English instruction, figures out which standards field it belongs to, and merges it in (admin only).\n- **When it\'s useful:** (Setup/Learning) When you notice the agents missing a convention and want to correct it in one sentence.\n- **How to use it:** `/teach-bridge <teaching>` \u2014 *"Teach Bridge: always use data-testid selectors in E2E tests."*\n\n**10. Review and Start**\n- **What it does:** Spawns one worktree per ticket; each session reviews the ticket inline and, after a per-ticket human proceed/halt gate, hands off to a **fresh implementation session** that reuses the same worktree \u2014 review and implementation run in two separate agent contexts, not one shared session.\n- **When it\'s useful:** (Refinement | Implementation | Automation) The **recommended front door** for "review these tickets, then implement the ones that pass," starting from existing ticket keys (unlike `/full-automation`, which only accepts an idea).\n- **How to use it:** `/review-and-start BAPI-248 BAPI-250` (single or multiple keys flow through the identical code path).\n- **Flags:** `--auto` a single chain-level flag that auto-approves both the review and the implementation phase of every spawned session \xB7 `--rounds=1|2` forwarded to the review phase \xB7 `--agent`, `--base-branch`, `--max-parallel`, `--dry-run` mirror `/start-tickets`.\n- Under the hood, this is a thin shim over `start-tickets --workflow review-and-implement <KEYS>` \u2014 the lower-level launcher seam documented above; the review\u2192gate\u2192fresh-implementation-handoff logic lives in the spawned `/review-and-implement` session, never in this command or the CLI. On approval, that session reuses its review-time model tier by passing it to the fresh implementation launcher via `--tier`.\n\n### Operational commands\n\nWorkflow commands you\'ll reach for during implementation and CI, beyond the tiers above:\n\n| Command | What it does |\n|---|---|\n| `/code-ticket PROJ-123` | Download the implementation plan and questions, then execute the plan inline |\n| `/commit-ticket PROJ-123` | Stage, commit, and push changes; transition Jira status; post a smoke-test comment |\n| `/create-pr PROJ-123` | Resolve the base branch and open a pull request for the ticket\'s branch (run after `/commit-ticket`) |\n| `/check-ci PROJ-123` | Monitor CI checks for the current branch, triage failures, apply fixes, and report results |\n| `/parse-repository` | Queue a background job to index the repository for Bridge AI agents |\n| `/check-parse-status` | Check whether a background repository parse job is still running |\n| `/scan-tickets` | Sync recently-updated Jira tickets and backfill workflow timestamps |\n\n> Commands are designed for Claude Code. Other editors may support slash commands differently \u2014 check your editor\'s documentation for how to invoke prompt files.\n\n### Extra Capabilities\n\nBehind-the-scenes capabilities an agent gains from the MCP tools \u2014 mostly invoked automatically by the commands above, rarely requested by name:\n\n- **Ship a PR end-to-end:** commit & push, open a pull request, transition the Jira status, and discover/poll CI checks (powers `commit-ticket`, `create-pr`, `check-ci`, `implement-ticket`).\n- **Architecture plan** for a ticket (design-level guidance, separate from the implementation plan).\n- **Index the codebase** so Bridge\'s agents can reason about it: queue/parse the repo, check parse status, regenerate the directory map.\n- **Read & tune project config/standards:** list/read/update config fields, fetch project standards, and the per-topic `learn-*` commands that populate them.\n- **Ticket lifecycle bookkeeping:** track tickets and backfill workflow-state timestamps (`scan-tickets`), search across tickets, read comments, list attachments.\n- **Pipeline machinery:** list/inspect pipeline recipes and run/resume/list/delete pipeline runs (the engine under the orchestration commands).\n- **Decision page** generation for capturing human review decisions as structured data.\n- **Connectivity & identity checks:** ping Bridge, check your role, resolve the local docs directory.\n- **Retrieve any generated artifact** (`get_*` for plans, critiques, questions, councils, research, architecture) without regenerating it.\n- **Tiered-section execution telemetry** recording (internal measurement).\n\n## Salesforce B2C Commerce (SFCC) Tools\n\nSalesforce\'s official MCP server, `@salesforce/b2c-dx-mcp`, covers developer-experience tasks \u2014 cartridge deploy, step debugging, docs search, logs/MRT, and SCAPI Custom API scaffolding. As of its current toolset it has no way to *read* the platform\'s object model, custom objects, or site configuration \u2014 exactly the context an AI coding agent needs to write correct Commerce code and to inspect a sandbox before touching it. Bridge\'s SFCC tools install side-by-side with `b2c-dx-mcp` (they don\'t duplicate its surface) and close that gap with **read-only OCAPI Data API introspection** of system objects, custom object definitions, and site preferences.\n\n**v1 is read-only and developer-sandbox-only** \u2014 no writes, and non-sandbox instances are rejected. Credentials stay local (in `dw.json` or `SFCC_*` env vars) and are never sent to Bridge.\n\nFor a step-by-step OCAPI client setup guide (including the Business Manager permissions grant), see [docs/install/sfcc-integration.md](./docs/install/sfcc-integration.md).\n\n<details>\n<summary><strong>Setup</strong></summary>\n\nThe two diagnostic tools (`sfcc_setup_status`, `check_permissions`) are always available. The read tools, the write tools, and `sfcc_log_query` must be enabled with a profile (step 3). Changing `BRIDGE_MCP_PROFILE` requires an MCP client restart.\n\n**Prerequisites:** a running SFCC **sandbox** and its hostname, plus an Account Manager API client (`client-id` + `client-secret`).\n\n**1. Set the repo `version` config field** to your SFCC project type \u2014 one of `sfra | pwakit | sitegenesis | storefrontnext | hybrid`. The call-time gate reads this; a non-SFCC value blocks every SFCC tool except `sfcc_setup_status`. Set it via your normal config path, the `config_field` MCP tool (operation `update`, field `version`), or the `/teach-bridge` skill.\n\n**2. Provide SFCC credentials.** Create a `dw.json` in your project root:\n\n```json\n{\n "hostname": "zzzz-001.sandbox.us01.dx.commercecloud.salesforce.com",\n "client-id": "<account-manager-client-id>",\n "client-secret": "<account-manager-client-secret>"\n}\n```\n\nAccepted key spellings: `hostname`/`host`, `client-id`/`clientId`/`client_id`, `client-secret`/`clientSecret`/`client_secret`. Prefer a single config \u2014 a multi-entry `configs[]` array forces you to pass an explicit `instance` on every call. `dw.json` is auto-added to git exclude and must never be committed. Alternatively, export `SFCC_HOSTNAME` / `SFCC_CLIENT_ID` / `SFCC_CLIENT_SECRET` in the MCP server environment.\n\n**3. Enable the read tools.** Add `sfcc` to `BRIDGE_MCP_PROFILE` in the MCP server `env` block (it is comma-separated; `full` also works), then **restart the MCP client**:\n\n```json\n"env": { "BRIDGE_MCP_PROFILE": "sfcc" }\n```\n\nWithout this, only the diagnostic tools are registered.\n\n**4. Verify and grant OCAPI access.** Ask your agent to run `sfcc_setup_status` (expect all checks \u2713), then `check_permissions`. If it reports HTTP 401/403, it prints the exact OCAPI Settings JSON to paste in Business Manager (**Administration \u2192 Site Development \u2192 Open Commerce API Settings \u2192 Data API** tab); save it there and re-run. Restart the MCP client after any credential or env change \u2014 a running session does not pick them up.\n\n</details>\n\n### Tools\n\nAll SFCC tools are read-only and target a developer sandbox. Oversized responses are automatically saved to `BAPI_DOCS_DIR/sfcc/` and previewed inline.\n\n**Diagnostics** (always available, no profile needed)\n- `sfcc_setup_status` \u2014 report on every prerequisite: Bridge API key, repo name, `version` config, `dw.json` presence/uniqueness, AM (OCAPI) token acquisition, and the independent **SFCC Log Query (WebDAV)** capability that gates `sfcc_log_query`.\n- `check_permissions` \u2014 probe OCAPI access via `GET /system_object_definitions`; on 401/403, print the exact OCAPI Settings JSON to paste in Business Manager (split read-only vs. write/import grants).\n\n**System object model** (needs the `sfcc` profile)\n- `system_object_list` \u2014 list system object types (Product, Order, Customer, \u2026).\n- `system_object_get` \u2014 fetch one type\'s definition, optionally with its full attribute definitions (`expand_attribute_definitions`).\n- `system_object_attribute_search` \u2014 search a type\'s attribute definitions; prefer this over a full dump when hunting a specific `c_` custom attribute.\n\n**Custom object definitions** (needs the `sfcc` profile)\n- `custom_object_definition_attributes_get` \u2014 fetch attribute definitions for a known custom object type. OCAPI cannot enumerate custom object types, so `object_type` must already be known.\n- `custom_object_definition_attribute_search` \u2014 search attribute definitions within a known custom object type. Read-only \u2014 creating a custom object *type* isn\'t possible via OCAPI; that\'s a future v2 metadata-import capability.\n- `custom_object_definition_attribute_create` \u2014 **write** (sandbox only): create an attribute definition on a known custom object type via `PUT /custom_object_definitions/{type}/attribute_definitions/{id}`. TYPE creation is never attempted (the type must pre-exist). Echoes paste-ready OCAPI grant JSON on 403.\n- `custom_object_definition_attribute_update` \u2014 **write** (sandbox only): update an attribute definition via an ETag-conditional `PATCH \u2026/attribute_definitions/{id}`; surfaces 409/412 conflicts and echoes grant JSON on 403.\n\n**Site preferences** (needs the `sfcc` profile; sandbox only)\n- `site_preference_get` \u2014 read a preference group\'s effective preferences.\n- `site_preference_search` \u2014 search/filter preferences within a group.\n- `site_preference_values_set` \u2014 **write** (sandbox only): set custom preference values via `PATCH /site_preferences/preference_groups/{group}/sandbox` with a flat map of `c_`-prefixed ids to string/number/boolean/string[] values. A bad group returns 404 `CustomPreferenceGroupNotFoundException`; echoes grant JSON on 403.\n\n**On-demand log query** (needs the `sfcc` profile)\n- `sfcc_log_query` \u2014 query redacted, filtered SFCC logs on demand. `environment` and `time_range` (`start`/`end`) are **required** \u2014 production, "all environments", and an open-ended period are never inferred. The tool calls a Bridge backend endpoint that runs the pull \u2192 redaction \u2192 filter pipeline server-side and returns scoped, redacted findings; **WebDAV credentials, retrieval, redaction, and filtering all stay server-side and single-sourced.** It holds no credentials of its own.\n - **Guardrails.** Selection is bounded by log-file `prefixes` (max 5), the time range, a scanned-entry cap (`max_entries`, \u2264 2000), a finding cap, and a per-snippet length cap. High-volume prefix classes (`info`, `jobs`, `debug`, `customdebug`) impose a **stricter 6-hour** max range (vs. 24h for the error class) because `info-*` runs ~1 MB/day versus `error-*` at ~13 KB median \u2014 a wide window over a high-volume prefix is **rejected**, never silently narrowed.\n - **Response order.** Resolved scope (`environment`, `time_range`, `applied_prefixes`) and cap `status` first, redacted `findings` second, retrieval/truncation `metadata` last.\n - **Statuses & errors.** `ready`, `no_matching_findings`, `results_truncated`; plus `VALIDATION_ERROR` (bad/oversized scope, caught before any network call), `NOT_CONFIGURED` (503 \u2014 the log capability isn\'t set up; run `sfcc_setup_status`, whose step 6 reports it), and `BAD_GATEWAY`/`SERVICE_UNAVAILABLE` on a retrieval/backend failure.\n - **Auth is separate from OCAPI.** Log retrieval uses **HTTP Basic auth** \u2014 a Business Manager username + a **40-character WebDAV access key** \u2014 *not* the OCAPI Account Manager OAuth token the other SFCC tools use. A valid AM bearer token 401s on `/Logs`. `sfcc_setup_status` step 5 (AM/OCAPI token) and step 6 (WebDAV log access) are independent: one can be green while the other is not.\n - **Local / air-gapped fallback.** The primary path above is the only path this tool takes. For air-gapped development, the documented fallback is Salesforce\'s own **`@salesforce/b2c-dx-mcp`** (`logs_list_files`, configured from `dw.json`) \u2014 it is vendor-maintained and reads log files over WebDAV, so it is strictly less work than shelling the B2C CLI (`b2c logs get --since <window> --search <q> --json`). It is **not** the primary path because its credentials live client-side and its output has **not** passed Bridge\'s redaction/filter. If you use it, its output must be treated as raw: route it back through the same server-side Python `LogSource` composition and `RedactionPort`/T3 filter workflow \u2014 never paste or relay unredacted `b2c-dx-mcp` or CLI output to an LLM.\n\n## CLI Subcommands\n\nBeyond `--init` / `--upgrade`, the package ships operational subcommands of the **single `bridge-api-mcp-server` bin** (not separate binaries) \u2014 so they travel with the package to every consumer. See [Usage Documentation \u2192 Start Tickets](#tier-1--regularly-useful) for *when* to use `start-tickets`; this section is the full CLI reference.\n\n### `start-tickets`\n\nSpawns one Worktrunk worktree + selected-agent session per Jira ticket and backs the `/start-tickets` slash command. The agent defaults to **Claude Code** (`claude`) and is configurable via `--agent`. For existing ticket keys, `/review-and-start` (see [Usage Documentation \u2192 Review and Start](#tier-3--now-and-then)) is the recommended enriched front door over this CLI\'s `--workflow review-and-implement` seam; using the CLI directly (below) remains the advanced/lower-level path.\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets [flags] KEY [KEY ...]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--agent claude\\|cursor-agent` | `claude` | Agent command to launch in each worktree |\n| `--workflow implement\\|review-and-implement` | `implement` | Slash command each spawned worktree runs. `implement` is byte-identical to today\'s `/implement-ticket <KEY> [--auto]`. `review-and-implement` spawns `/review-and-implement <KEY> [--auto] [--rounds=<n>]`, which runs `/review-ticket` then, after a per-ticket halt gate, `/implement-ticket` in the same session. `--auto` applies to the selected workflow as a whole. |\n| `--rounds 1\\|2` | unset | Review round count forwarded to the review phase. Review-only \u2014 valid only with `--workflow review-and-implement`. |\n| `--tier cheap\\|basic\\|premium` | unset (difficulty routing) | Coarse model-routing override. Bypasses **only** the per-ticket difficulty/tier lookup (`GET /jira/tickets/{KEY}/model-tier`) and applies this one tier to every ticket; the tier is still mapped to a model through the centralized agent registry and any configured `difficulty_model_tier_overrides`, then validated. It is **not** a raw `--model` alias and never carries an API key or credential. A malformed value fails open to premium routing. Used by the `/review-and-implement` handoff to reuse the review-time tier. |\n| `--terminal terminal\\|iterm` | auto-detect via `$TERM_PROGRAM` | Override the macOS terminal app (honored on macOS only) |\n| `--dry-run` | off | Print intended actions; create no worktrees, open no tabs (any OS) |\n| `--branch KEY=BRANCH` | `feature/<KEY>` | Use a custom branch for that ticket (repeatable) |\n| `--base-branch <BRANCH>` | `main` | Cut new worktrees from `<BRANCH>` and refresh `origin/<BRANCH>` instead of `main` |\n| `--no-refresh-main` | off (the configured base branch is refreshed) | Skip refresh of the configured base branch (default `main`). Historical flag name preserved for backward compatibility \u2014 despite the name, it now skips refresh of whatever `--base-branch` resolves to. |\n| `--max-parallel N` | `3` | Max worktrees created concurrently |\n| `--conductor` | off | Opt into the Conductor system (per-worker `BAPI_CONDUCTOR_*` env + Claude hook injection, a supervisor peer tab, and the `check_messages` message-relay prompt). **Default off** \u2014 a plain run spawns `cd <worktree> && <agent> \'/implement-ticket <KEY>\'`. |\n| `-h`, `--help` | \u2014 | Show usage |\n\nEach `KEY` must match `[A-Z]+-[0-9]+` (e.g., `BAPI-248`). The CLI creates/switches each worktree up front (throttled by `--max-parallel`), then opens one tab/session per successful worktree running the selected agent\'s `\'/implement-ticket <KEY>\'` \u2014 `claude \'/implement-ticket <KEY>\'` by default, or `cursor-agent \'/implement-ticket <KEY>\'` with `--agent cursor-agent`. The `/implement-ticket <KEY>` prompt is unchanged for both agents. To launch Cursor Agent instead of Claude Code:\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets --agent cursor-agent BAPI-248\n```\n\n**Difficulty-based model routing.** Before launching each agent, the CLI selects an implementation **model tier** from the ticket\'s `difficulty` (1-2 \u2192 cheap, 3-5 \u2192 basic, 6+ \u2192 premium) and injects it as a `--model` flag at the spawn boundary. The Python backend returns only the coarse tier (`GET /jira/tickets/{KEY}/model-tier`, computing + caching difficulty on demand); this CLI alone maps a tier to the agent-specific alias (`claude`: `haiku`/`sonnet`/`opus`; `cursor-agent`: version-suffixed strings validated against `cursor-agent --list-models`). It is gated per repo by `difficulty_model_routing_enabled` (default **ON**) with an optional `difficulty_model_tier_overrides` JSON map (tier \u2192 alias). Routing is **fail-open**: missing credentials, an evaluation failure/timeout, a backend `fallback`, an invalid/unavailable alias, an unadvertised Cursor model, or an agent without `--model` support all default to the **premium** (Opus) model \u2014 and when even the premium alias cannot be resolved/validated for the agent, `--model` is omitted (the agent uses its default) \u2014 each surfaced as a per-ticket warning rather than failing the spawn. `--dry-run` does **not** create worktrees or open tabs, but it **does** resolve routing read-only to preview the `--model` each tab would use.\n\n**Coarse `--tier` override.** Passing `--tier cheap|basic|premium` bypasses **only** the per-ticket difficulty lookup above and applies that one tier to every ticket; the tier is still resolved to an alias through the same agent registry + `difficulty_model_tier_overrides` and validated the same way (including the live `cursor-agent --list-models` check). It is never treated as a raw `--model` alias, and no API key or credential belongs in the spawned command (the CLI resolves credentials itself via `resolveBapiCredentials`). A malformed/unrecognized `--tier` value is **fail-open**: the CLI logs one concise warning and routes every ticket on the premium (Opus) fallback rather than aborting. `/review-and-implement` uses this flag to hand its review-time tier snapshot to the fresh implementation session.\n\n**Conductor observability (opt-in via `--conductor`, BAPI-394).** Conductor is **off by default**. With `--conductor`, a run mints a conductor `run_id`, emits events into the local ledger (`~/.config/bridge/events.db`), opens a supervisor peer tab, and (for Claude Code) injects a secret-free lifecycle hook into each worktree; it also sets `BRIDGE_MCP_PROFILE=conductor` so workers get the conductor MCP tools. Observability is best-effort \u2014 a conductor failure never blocks a spawn, and `--dry-run` performs no conductor side effects. (Epic-tick dispatch always enables conductor internally.) See **[CONDUCTOR.md](./docs/CONDUCTOR.md)** for the full observability, git-hook, done-gate, and auto-merge reference.\n\n**Cross-platform spawning.** The CLI routes spawning per platform; `--dry-run` previews the platform-correct command form on any OS. An unsupported `process.platform` (not `darwin`/`win32`/`linux`) fails fast with a clear "unsupported platform" message.\n\n- **macOS** \u2014 opens a Terminal.app or iTerm tab via `osascript`.\n- **Windows** \u2014 creates worktrees with **`git-wt`** (Worktrunk\'s winget alias) and opens a tab via **Windows Terminal (`wt.exe new-tab`)**, falling back to **`Start-Process powershell.exe`** when Windows Terminal is absent. Requires **Git for Windows / Git Bash** (Worktrunk runs its `pre-start` / `post-start` hooks via Git Bash). The Worktrunk binary (`git-wt`) and the tab launcher (`wt.exe`) are resolved independently and never conflated.\n- **Linux** \u2014 creates one detached **tmux** session per ticket (pane kept open after the agent exits); attach with `tmux attach -t <session>`. A missing `tmux` produces a clear, actionable error.\n\nPer-OS prerequisites: macOS `wt`, `git`, `osascript`; Windows `git-wt`, Git for Windows / Git Bash, Windows Terminal or PowerShell; Linux `wt`, `git`, `tmux`. Set `BAPI_WORKTRUNK_BIN` to override the Worktrunk executable name/path for nonstandard installs (`doctor` honors it too). The read-only `doctor` subcommand (below) additionally surfaces a missing `uv` \u2014 Worktrunk\'s `pre-start` hook runs `uv`, but live preflight does not check it \u2014 and the selected agent\'s command; run `doctor --agent cursor-agent` to also check `cursor-agent` (it prints `cursor-agent login` as an informational auth reminder).\n\n### `doctor`\n\nThe package also ships a strictly **read-only** `doctor` subcommand that diagnoses the `start-tickets` prerequisites for the current OS without changing anything:\n\n```\nnpx -y @bridge_gpt/mcp-server doctor [--agent <name>]\n```\n\nIt is **read-only**: it never installs anything, modifies your system, adds an npm `postinstall`, spawns a terminal, or starts the MCP server, and there is no `--fix`. For each prerequisite it prints found/missing and, when missing, the exact per-OS install command **as a manual instruction you run yourself**. The checked set is the `start-tickets` preflight prerequisites **plus `uv`** **plus the selected agent\'s command** (`claude` by default, or `cursor-agent` with `--agent cursor-agent`). The Worktrunk binary is probed via the resolved name (honoring `BAPI_WORKTRUNK_BIN`), not a hard-coded one. **Exit code:** `0` when all required prerequisites are present, non-zero when any is missing or the platform is unsupported. A failing `start-tickets` preflight now hints you to run `doctor` for an actionable diagnostics report.\n\n### `setup-epic`\n\nBootstraps an Epic Conductor v2 run in one command \u2014 creates the epic run, stores the plan DAG, and approves it:\n\n```\nnpx -y @bridge_gpt/mcp-server setup-epic --epic-key <KEY> --plan-file <path-to-epic-plan.dag.json>\n```\n\nThe plan sidecar is produced by the `decompose-epic` pipeline step. `setup-epic` validates it locally (unique ticket keys, resolvable dependency references, acyclicity) before sending anything, so a malformed plan fails legibly instead of as a bare HTTP 400. It is **idempotent**: re-running it on an epic that already has a live run reuses that run rather than minting a second one. `--dry-run` validates and previews the calls without mutating anything; `--json` emits a single machine-readable result object.\n\nOnce the plan is approved, the **server-side reconciler** picks the run up within ~30s. To execute claimed jobs on your machine, run `executor`:\n\n```\nnpx -y @bridge_gpt/mcp-server executor --repo <name>\n```\n\n### Conductor (epic & multi-agent orchestration)\n\nConductor is an **opt-in, off-by-default** layer for epic supervision, inter-agent messaging, done-gate evaluation, local git-hook event producers, and conditional auto-merge. Its full reference \u2014 the v2 architecture (server-side reconciler + local executor), `setup-epic`, `conductor install-git-hooks`, the supervisor `done_gate_config` / `auto_merge_enabled` settings, and the observability stream \u2014 lives in **[CONDUCTOR.md](./docs/CONDUCTOR.md)**.\n\n> The v1 `conductor epic-tick` command is **frozen** \u2014 it throws on every invocation. There is nothing to schedule locally, and `conductor doctor` flags any epic-tick schedule left over from an earlier release so you can cancel it.\n\n## Custom Pipelines\n\nYou can create your own pipelines by adding JSON files to `.bridge/pipelines/`. Running `--init` scaffolds this directory with a `README.md` and an example pipeline to get you started.\n\nThe easiest way to write a custom pipeline is to describe what you want to automate to your AI coding agent and have it draft the JSON for you. The schema is straightforward, and agents like Claude Code understand it well \u2014 just describe the steps you want, and the agent will produce a working pipeline file.\n\n**What you can build:**\n\n- Any sequence of Bridge MCP tool calls and free-form agent tasks\n- Parameterized workflows using variables (e.g., `{ticket_key}`)\n- Approval gates that pause for user confirmation before sensitive steps\n- Per-step error handling \u2014 halt immediately or warn and continue\n\n**Ideas for custom pipelines:**\n\n- A standup pipeline that fetches your open tickets and summarizes their status\n- A ticket triage pipeline that runs critiques on a batch of new tickets\n- A pre-merge checklist that runs tests, checks linting, and posts a summary comment\n\n**Step types:**\n\n| Type | What it does |\n|---|---|\n| `mcp_call` | Calls an MCP tool with the given params |\n| `agent_task` | Gives the AI a free-form instruction (inline or from a file in `.bridge/instructions/`) |\n\nVariables are declared in the `variables` array and referenced as `{variable_name}` in params and instructions. Each step supports `on_error: "halt"` (default) or `"warn_and_continue"`, and `requires_approval: true` to pause before execution.\n\n**System variables:**\n\nTwo variables are automatically available in every pipeline without declaring them:\n\n| Variable | What it does |\n|---|---|\n| `{provider}` | Routes AI generation to a specific LLM provider (`openai`, `anthropic`, or `gemini`). Pass it through to any `request_*` tool param to control which provider handles that step. Omit it (or leave it empty) to use the project default. |\n| `{second_opinion}` | Runs AI generation through a different provider than the default, acting as a cross-check. Set to `"auto"` to let Bridge pick the second provider automatically. When set, it takes precedence over `{provider}`. Use this when you want two independent AI perspectives on the same task \u2014 for example, running clarifying questions and a critique twice (once with each provider) produces better results than a single pass. |\n\nSee `.bridge/pipelines/README.md` for the full schema reference.\n\nIf a custom pipeline has the same key as a built-in pipeline, the custom version takes precedence (a warning is logged at startup).\n\n## Environment Variables\n\n| Variable | Required | Default | Description |\n|---|---|---|---|\n| `BAPI_BASE_URL` | Yes | `https://bridgegpt-api.com` | Bridge API base URL |\n| `BAPI_REPO_NAME` | Yes | _(none)_ | Jira project/repository identifier configured in Bridge API |\n| `BAPI_API_KEY` | Yes | _(none)_ | API key obtained from the Bridge API setup UI |\n| `BAPI_PROJECT_ROOT` | No | _(auto-set by --init)_ | Absolute path to project root. Anchors `BAPI_DOCS_DIR` and `BAPI_PIPELINES_DIR` resolution |\n| `BAPI_DOCS_DIR` | No | `docs/tmp` | Local directory for saving plans, critiques, and research reports |\n| `BAPI_PIPELINES_DIR` | No | `.bridge/pipelines` | Directory for user-defined custom pipeline JSON files |\n| `BAPI_WORKTRUNK_BIN` | No | `wt` (`git-wt` on Windows) | Override the Worktrunk executable name/path used by `start-tickets` for nonstandard installs |\n| `BAPI_TMUX_SESSION` | No | `bridge-start-tickets` | Override the tmux session-name prefix used by `start-tickets` on Linux |\n| `BAPI_MCP_UPGRADE_ADVICE_ENABLED` | No | _(enabled)_ | MCP-local opt-out for proactively surfacing upgrade advice in pipeline recipe preambles. Set to `false`/`0`/`no`/`off`/`disabled` to suppress. Disabling it does **not** change the `/jira/ping` response or server-side upgrade computation \u2014 it only gates the recipe-preamble convention |\n| `BAPI_MCP_TOOL_SURFACE_GATING_ENABLED` | No | _(enabled)_ | MCP-local kill switch for **dynamic tool-surface capability gating** (see [Dynamic tool-surface gating](#dynamic-tool-surface-gating-capability-availability)). Default-on; set to `false`/`0`/`no`/`off`/`disabled` to skip the startup probe, the recurring poll, and the custom `tools/list` handler entirely, restoring the SDK\'s previous full profile-derived surface. Fail-open: any probe timeout, unreachable backend, non-2xx, malformed payload, incomplete evaluation, or unsupported schema advertises the full profile |\n| `BAPI_MCP_TOOL_SURFACE_POLL_ENABLED` | No | _(disabled)_ | Opt IN to the recurring tool-surface **poll**. Default-**off**: a session gates once via the startup probe and never re-probes. Set to `true`/`1`/`yes`/`on`/`enabled` to restore the jittered 12\u201318 s heartbeat that pushes `notifications/tools/list_changed` on mid-session capability changes. No effect when gating itself is disabled |\n| `CURSOR_API_KEY` | No | _(none)_ | API key used to authenticate `cursor-agent` when launching `start-tickets`/scheduled runs with `--agent cursor-agent`. Not needed for the default Claude Code agent |\n| `BRIDGE_MCP_PROFILE` | No | `core` | Startup-time tool registration profile \u2014 a **comma-separated** list of groups controlling which tool groups are registered when the server starts. `core` is always implicitly included. Valid groups: `core` (default \u2014 normal coding tools only), `conductor` (+ 8 conductor/event/supervisor tools), `pipeline-authoring` (+ 5 pipeline run/admin tools \u2014 `get_pipeline_recipe` is NOT gated; it stays in `core` because the recipe-driven slash commands depend on it), `sfcc` (+ the 7 heavy SFCC read tools \u2014 see [Salesforce B2C Commerce (SFCC) Tools](#salesforce-b2c-commerce-sfcc-tools); the `sfcc_setup_status`/`check_permissions` diagnostics are always registered regardless), and `full` (shortcut that expands to every group). Example: `sfcc,conductor`. Unknown, blank, or malformed tokens are dropped (falling back to `core`). Dynamic mid-session switching via `tools/list_changed` is unsupported \u2014 groups are resolved once at process startup. Conductor/epic sessions launched via `start-tickets --conductor` automatically inject `BRIDGE_MCP_PROFILE=conductor` at the spawn boundary; a normal `start-tickets` run stays on `core`. |\n\n## Dynamic tool-surface gating (capability availability)\n\nThe **effective advertised tool surface** is the intersection of three things:\n\n1. **Startup profile registration** \u2014 which tool groups `BRIDGE_MCP_PROFILE`\n registered at process start (see above).\n2. **Current SDK-enabled state** \u2014 a tool the server has disabled for another\n reason (e.g. `poll_ci_checks` when `ci_check_config` is unset) stays hidden.\n3. **Backend capability availability** \u2014 the set of tool IDs the backend would\n currently hard-block for this repo, reported by `GET /jira/mcp/tool-surface`.\n\nOn startup the server issues one bounded probe to that endpoint and installs a\ncustom `tools/list` handler that subtracts the backend-blocked IDs (intersected\nwith the locally advertised surface) from what it advertises. That single startup\nprobe is the default: the surface is gated once per session and the server does\nnot re-probe. Installed integrations change rarely and MCP clients re-list on\nreconnect, so a permanent per-session heartbeat \u2014 multiplied across every\nconcurrent worktree/agent session \u2014 was pure request noise against the backend.\nOpt in with `BAPI_MCP_TOOL_SURFACE_POLL_ENABLED=true` to restore the jittered\n12\u201318 s re-probe that emits `notifications/tools/list_changed` whenever the\neffective visible set actually changes, so a connected client converges to the\ncurrent surface mid-session without a reconnect.\n\n**Fail-open by design.** A probe timeout, unreachable backend, non-2xx response,\nmalformed payload, incomplete evaluation, or unsupported schema version all\nadvertise the **full** existing profile baseline \u2014 gating never removes a tool on\na doubtful signal.\n\n**Hidden \u2260 disabled.** A capability-hidden tool remains **registered and\ncallable**, including through in-process pipelines. Hiding affects `tools/list`\nprojection only; it never calls `.disable()` or mutates the SDK `enabled` flag,\nbecause doing so would also block `tools/call` and the in-process dispatch path \u2014\nthe backend remains the authoritative enforcement boundary, returning its own\nrefusal for a stale call rather than a local "disabled" error.\n\n**Kill switch.** Set `BAPI_MCP_TOOL_SURFACE_GATING_ENABLED` to an accepted false\ntoken (`false`/`0`/`no`/`off`/`disabled`) to skip the probe, the poll, and the\ncustom handler entirely, restoring the SDK\'s previous full profile-derived\nsurface.\n\n**Client convergence and the reconnect escape hatch.** Clients that honor\n`notifications/tools/list_changed` converge automatically. A client that does not\nhonor the notification must **reconnect or start a new MCP server session** to\nobserve the current surface; no project MCP configuration change is required.\n\n**Diagnosing the surface.** Run `doctor` (its advisory "MCP tool surface"\nsection reports the kill-switch state, reachability, decision reason, blocked\ncount, physical tool IDs, and catalog revision via a single read-only GET), or\nread the server\'s stderr gating decision lines (`tool-surface gating: reason=\u2026\nhidden=\u2026 revision=\u2026 hidden_tools=[\u2026]`).\n\n## Worktree credentials and the `mcp-invoke` shim\n\nWhen `start-tickets` creates a git worktree, it provisions a Bridge API MCP\nregistration into that worktree so Claude Code (`.mcp.json`) and Cursor\n(`.cursor/mcp.json`) can reach the server immediately. These registrations are\n**secret-free**: they contain no `env` block and no API key. Instead they point\nat an internal subcommand of the published single CLI bin, `mcp-invoke`:\n\n```bash\nnpx -y @bridge_gpt/mcp-server@<VERSION> mcp-invoke --target bapi --project-root <ABS_WORKTREE_PATH>\n```\n\n`mcp-invoke` is not a separate binary \u2014 it is a positional subcommand of\n`bridge-api-mcp-server`. It resolves the repo identity from the absolute\n`--project-root` (the committed `.bridge/config` manifest, falling back to the\ngit common dir), resolves credentials from the home-directory credential store,\nand then spawns the real MCP server with that environment.\n\n### Credential store\n\nCredentials live outside the repository, keyed by `bapi:<repo_name>`:\n\n```json\n{\n "bapi:<repo_name>": {\n "BAPI_API_KEY": "..."\n }\n}\n```\n\nResolution order:\n\n1. `BAPI_API_KEY` in the parent environment (overrides the file entirely).\n2. `$XDG_CONFIG_HOME/bridge/credentials.json`, else `~/.config/bridge/credentials.json`.\n3. `~/.bridge/credentials.json` (only when the primary path is absent).\n\nOn POSIX systems, lock the file down so only you can read it:\n\n```bash\nchmod 600 ~/.config/bridge/credentials.json\n```\n\n`mcp-invoke` warns (but continues) if the file is group/world-readable, and it\nnever creates or initializes the credential file for you.\n\n### Populating the credential store\n\nThe same store also backs the shell-spawned `start-tickets` CLI (its\ndifficulty\u2192model routing runs in a Bash process that cannot see an `env` block in\n`.mcp.json` / `.cursor/mcp.json`), so the store must hold the key for routing to\nwork. Two supported paths write it for you:\n\n- **Install-time upsert.** `/install-bridge`\'s final stage persists the validated\n routing credential into `~/.config/bridge/credentials.json` (target\n `bapi:<repo>`) via the `persist_routing_credential` MCP tool \u2014 the tool resolves\n the key inside the MCP process and writes the store, so no secret crosses the\n wire.\n- **One-shot migration.** If a key currently lives only in `.mcp.json` /\n `.cursor/mcp.json`, migrate it into the user-scoped store with the consent-gated\n command (a compatibility aid, not a live fallback):\n\n ```bash\n npx -y @bridge_gpt/mcp-server credentials migrate-agent-config [--write-credentials]\n ```\n\n Run it without `--write-credentials` to preview; add the flag to write the store.\n\n## Reference\n\nThe full surface, for when you need the complete enumeration. Day-to-day, use [Usage Documentation](#usage-documentation) instead \u2014 you don\'t call MCP tools directly; you ask your AI assistant to perform a task, or compose tools into a pipeline.\n\n### MCP tools\n\nThe server exposes **60 documented tools** (enumerated below). What\'s actually registered in a session depends on `BRIDGE_MCP_PROFILE`: the default `core` profile loads a trimmed subset, and the conductor/pipeline-authoring/SFCC tools are added only under their respective profiles (see [Environment Variables](#environment-variables)). Async AI tools follow a request/get pattern: call the `request_*` tool to kick off generation, then the matching `get_*` tool to retrieve the result (or pass `wait_for_result: true` to poll automatically).\n\n- **Connectivity & identity** \u2014 `ping`, `get_my_role`, `get_docs_dir`\n- **Team & access** \u2014 `invite_member` (admin-only; mints a scoped access key for a teammate on an already-configured project \u2014 the plaintext key is shown exactly once)\n- **Jira tickets** \u2014 `get_tickets`, `get_ticket`, `create_ticket`, `update_ticket_description`, `add_comment`, `get_comments`\n- **Attachments** \u2014 `attachment` (operations: `upload`, `download`, `list`)\n- **AI generation (request/get)** \u2014 `request_plan_generation`/`get_plan`, `request_architecture`/`get_architecture`, `create_doc`/`get_doc` (design docs by `doc_type`: tdd/fsd/prd), `request_prd`/`get_prd`, `request_clarifying_questions`/`get_clarifying_questions`, `request_ticket_critique`/`get_ticket_critique`, `request_ticket_review`, `request_reimplement_context`/`get_reimplement_context`, `request_council`/`get_council`, `request_deep_research`/`get_deep_research`\n- **Other AI** \u2014 `second_opinion`, `generate_image`, `generate_decision_page`, `visual_diff` (deterministic pixel diff of a rendered URL vs a design comp)\n- **Ticket lifecycle** \u2014 `track_ticket`, `update_ticket_state`, `get_ticket_state`\n- **Jira status** \u2014 `get_jira_transitions`, `update_jira_status`, `resolve_target_status`\n- **Repository & CI** \u2014 `parse_repository`, `get_parse_status`, `regenerate_directory_map`, `create_pull_request`, `resolve_ci_checks`, `poll_ci_checks`\n- **Pipelines & automation** \u2014 `list_pipelines`, `get_pipeline_recipe`, `run_pipeline`, `resume_pipeline`, `list_pipeline_runs`, `delete_pipeline_run`, `run_full_automation`, `resume_full_automation`\n- **Config** \u2014 `get_project_standards`, `config_field` (operations: `get`, `update`, `list`)\n\n### Bundled pipelines\n\nPipelines are declarative, multi-step workflows your AI agent executes step-by-step \u2014 each a JSON recipe chaining MCP tool calls and free-form agent tasks, with variable substitution, per-step error handling, and optional approval gates. You can also write your own (see [Custom Pipelines](#custom-pipelines)).\n\n| Pipeline | Description | Invoke with |\n|---|---|---|\n| `implement-ticket` | Generate a plan, execute the implementation, commit, open a PR, and monitor CI | `/implement-ticket PROJ-123` |\n| `review-ticket` | Full ticket quality review: clarifying questions + critique plus an automatic second-opinion pass, then evaluation and decision capture. The backend owns round orchestration; pass `--rounds=1` for a single-pass review or `--rounds=2` to force the full second-opinion review, or omit `--rounds` to let the backend decide adaptively. | `/review-ticket PROJ-123` |\n| `idea-to-ticket` | Turn an idea into a Jira Task/Spike (or Epic + children) with research, dedup, and critique | `/idea-to-ticket "<idea>"` |\n| `plan-epic` | Decompose an epic into sub-tasks with a structured exploration doc for each | `/plan-epic "<epic>"` |\n| `full-automation` | Chain: idea \u2192 ticket(s) \u2192 review each \u2192 spawn worktrees to implement | `/full-automation "<idea>"` |\n| `pr-ticket` | Commit changes and open a pull request | `/create-pr PROJ-123` |\n| `check-ci-ticket` | Commit, open a PR, then monitor CI checks until they pass or fail | `/check-ci PROJ-123` |\n| `learn-repository` | Analyze codebase architecture, testing, review, and documentation standards, then upload to Bridge | `/learn-repository` |\n\n### Pipeline response envelope\n\n`run_pipeline`, `resume_pipeline`, and `list_pipeline_runs` share a unified envelope keyed on `status`:\n\n- `completed` \u2014 terminal success; `results` holds per-step output.\n- `needs_agent_task` \u2014 the orchestrator paused. Read `instruction`, perform the task, then call `resume_pipeline` with `pipeline_run_id` and a string `agent_result`.\n- `failed` \u2014 terminal failure. `error_code` is one of `VALIDATION`, `NOT_FOUND`, `EXPIRED`, `REPO_MISMATCH`, `TOOL_ERROR`.\n\nPaused runs auto-expire after an idle TTL (default 24 hours; override with `ttl_seconds`). The TTL is reset on every state transition. List output is metadata-only \u2014 it never includes resolved recipes, params, instructions, results, or agent outputs.\n';init_version_generated();import{writeFile,mkdir,readFile}from"fs/promises";import path from"path";import os from"os";var CACHE_TTL=864e5,FETCH_TIMEOUT=3e3,REGISTRY_URL="https://registry.npmjs.org/@bridge_gpt/mcp-server/latest";function getCachePath(){return path.join(os.homedir(),".config","@bridge_gpt","mcp-server","update-check.json")}function isNewerVersion(current,latest){let c=current.split(".").map(Number),l=latest.split(".").map(Number);for(let i=0;i<3;i++){if((l[i]??0)>(c[i]??0))return!0;if((l[i]??0)<(c[i]??0))return!1}return!1}async function checkForUpdate(){try{let cachePath=getCachePath(),cacheDir=path.dirname(cachePath),latestVersion=null;try{let raw=await readFile(cachePath,"utf-8"),cache=JSON.parse(raw);cache&&typeof cache.lastCheck=="number"&&typeof cache.latestVersion=="string"&&Date.now()-cache.lastCheck<CACHE_TTL&&(latestVersion=cache.latestVersion)}catch{}if(!latestVersion){let controller=new AbortController,timeout=setTimeout(()=>controller.abort(),FETCH_TIMEOUT);try{let data=await(await fetch(REGISTRY_URL,{signal:controller.signal})).json();data.version&&(latestVersion=data.version,await mkdir(cacheDir,{recursive:!0}),await writeFile(cachePath,JSON.stringify({lastCheck:Date.now(),latestVersion}),"utf-8"))}finally{clearTimeout(timeout)}}return latestVersion?{updateAvailable:isNewerVersion(VERSION,latestVersion),currentVersion:VERSION,latestVersion}:null}catch{return null}}import{readdir,readFile as readFile2}from"fs/promises";import path2 from"path";function validatePipelineSchema(json){let errors=[];if(typeof json!="object"||json===null||Array.isArray(json))return{valid:!1,errors:["Pipeline must be a JSON object."]};let obj=json;if((typeof obj.name!="string"||obj.name.trim()==="")&&errors.push('Missing or empty required field "name" (string).'),!Array.isArray(obj.steps)||obj.steps.length===0)return errors.push('Missing or empty required field "steps" (non-empty array).'),{valid:!1,errors};obj.description!==void 0&&typeof obj.description!="string"&&errors.push('"description" must be a string if provided.'),obj.variables!==void 0&&(!Array.isArray(obj.variables)||!obj.variables.every(v=>typeof v=="string"))&&errors.push('"variables" must be an array of strings if provided.');let steps=obj.steps;for(let i=0;i<steps.length;i++){let prefix=`steps[${i}]`,step=steps[i];if(typeof step!="object"||step===null||Array.isArray(step)){errors.push(`${prefix}: must be an object.`);continue}let s=step;if((typeof s.description!="string"||s.description.trim()==="")&&errors.push(`${prefix}: missing or empty "description" (string).`),s.on_error!==void 0&&s.on_error!=="halt"&&s.on_error!=="warn_and_continue"&&errors.push(`${prefix}: "on_error" must be "halt" or "warn_and_continue".`),s.requires_approval!==void 0&&typeof s.requires_approval!="boolean"&&errors.push(`${prefix}: "requires_approval" must be a boolean if provided.`),s.id!==void 0&&(typeof s.id!="string"||s.id.trim().length===0)&&errors.push(`${prefix}."id" must be a non-empty string when provided`),s.type==="mcp_call")(typeof s.tool!="string"||s.tool.trim()==="")&&errors.push(`${prefix}: mcp_call step requires "tool" (string).`),(typeof s.params!="object"||s.params===null||Array.isArray(s.params))&&errors.push(`${prefix}: mcp_call step requires "params" (object).`);else if(s.type==="agent_task"){let hasInstruction=typeof s.instruction=="string"&&s.instruction.trim()!=="",hasInstructionFile=typeof s.instruction_file=="string"&&s.instruction_file.trim()!=="";hasInstruction&&hasInstructionFile?errors.push(`${prefix}: agent_task must have exactly one of "instruction" or "instruction_file", not both.`):!hasInstruction&&!hasInstructionFile&&errors.push(`${prefix}: agent_task must have exactly one of "instruction" or "instruction_file".`)}else errors.push(`${prefix}: "type" must be "mcp_call" or "agent_task", got "${String(s.type)}".`)}return{valid:errors.length===0,errors}}function hasTerminalReturnSection(markdown){if(typeof markdown!="string"||markdown.length===0)return!1;let h2Pattern=/^##\s+(.+?)\s*$/gm,match,lastHeading=null;for(;(match=h2Pattern.exec(markdown))!==null;)lastHeading=match[1].trim();return lastHeading===null?!1:/^return\b/i.test(lastHeading)}var variablePattern=()=>/\{([a-zA-Z_][a-zA-Z0-9_]*)}/g;function substituteVariables(template,variables){return template.replace(variablePattern(),(full,name)=>name in variables?variables[name]:full)}function substituteDeep(value,variables){if(typeof value=="string")return substituteVariables(value,variables);if(Array.isArray(value))return value.map(item=>substituteDeep(item,variables));if(typeof value=="object"&&value!==null){let result={};for(let[k,v]of Object.entries(value))result[k]=substituteDeep(v,variables);return result}return value}function substituteInParams(params,variables){return substituteDeep(params,variables)}var UPGRADE_ADVICE_SURFACING_INSTRUCTIONS=" Upgrade advice: when any `ping` MCP call in this run returns a non-empty second text content item, relay that server-provided text to the user verbatim \u2014 do not invent, prefix, suffix, summarize, or paraphrase it. Surface it at most once per pipeline run or session, including any chained sub-pipelines. Stay completely silent when the second content item is absent or empty. A ping failure, a non-OK ping response, missing advice, or empty advice is fail-open: it must never block, pause, or crash the run, and you must never synthesize advice of your own.";function resolveRecipe(pipeline,instructions,variables,skipSteps,autoApprove,options){let declared=pipeline.variables??[],missing=declared.filter(v=>!(v in variables));if(missing.length>0)throw new Error(`Missing required variable(s): ${missing.join(", ")}. Pipeline "${pipeline.name}" declares: [${declared.join(", ")}].`);let skip=new Set(skipSteps??[]),resolvedSteps=[],stepIndex=1;for(let step of pipeline.steps){let stepId=typeof step.id=="string"&&step.id.trim().length>0?step.id:void 0,skipKey=stepId??(step.type==="mcp_call"?step.tool:step.description);if(skip.has(skipKey))continue;let declaredApproval=step.requires_approval??!1,effectiveApproval=declaredApproval&&!autoApprove,isPingStep=step.type==="mcp_call"&&step.tool==="ping",base={step:stepIndex++,type:step.type,description:substituteVariables(step.description,variables),on_error:isPingStep?"warn_and_continue":step.on_error??"halt",requires_approval:effectiveApproval};if(declaredApproval!==effectiveApproval&&(base.requires_approval_declared=declaredApproval),stepId!==void 0&&(base.id=stepId),step.type==="mcp_call")base.tool=step.tool,base.params=substituteInParams(step.params,variables);else{let rawInstruction;if(step.instruction_file){let content=instructions[step.instruction_file];if(content===void 0)throw new Error(`Instruction file "${step.instruction_file}" not found in bundled instructions.`);rawInstruction=content}else rawInstruction=step.instruction;base.instruction=substituteVariables(rawInstruction,variables)}resolvedSteps.push(base)}let baseInstructions=`IMPORTANT: Execute every step below in exact sequential order. For mcp_call steps, call the specified tool with the provided params. For agent_task steps, follow the instruction text using any tools it specifies. If requires_approval is true, pause before executing. For agent_task steps, the instruction file's own approval format is authoritative \u2014 follow it verbatim and do not substitute your own short confirmation prompt. For mcp_call steps with no instruction file, present the resolved params as bullet points and ask for approval. For on_error "halt", stop the pipeline immediately on failure. For on_error "warn_and_continue", log a warning and proceed. Do not skip steps, reorder them, or substitute your own tool calls.`,upgradeAdviceConvention=options?.includeUpgradeAdviceSurfacing!==!1?UPGRADE_ADVICE_SURFACING_INSTRUCTIONS:"",autoApproveSuffix=autoApprove?" Auto-approve mode is ACTIVE: every approval gate has been pre-approved by the user via the auto_approve flag \u2014 proceed without pausing for confirmation, applying the default branching, file-staging, and recommendation choices documented in each instruction file's auto-approve branch.":"";return{pipeline:pipeline.name,description:pipeline.description??"",total_steps:resolvedSteps.length,agent_instructions:baseInstructions+upgradeAdviceConvention+autoApproveSuffix,auto_approve:!!autoApprove,steps:resolvedSteps}}async function loadCustomPipelines(pipelinesDir,instructionsDir,bundledInstructions){let mergedInstructions={...bundledInstructions},userPipelines={},userPipelineKeys2=new Set;try{let instrFiles=await readdir(instructionsDir);for(let file of instrFiles)if(file.endsWith(".md"))try{let content=await readFile2(path2.join(instructionsDir,file),"utf-8");file in bundledInstructions&&console.error(`Warning: custom instruction "${file}" overrides a bundled instruction.`),mergedInstructions[file]=content}catch(readErr){let msg=readErr instanceof Error?readErr.message:String(readErr);console.error(`Warning: skipping instruction "${file}" \u2014 failed to read: ${msg}`)}}catch(err){err.code!=="ENOENT"&&console.error(`Warning: could not read instructions directory "${instructionsDir}": ${err.message}`)}try{let pipelineFiles=await readdir(pipelinesDir);for(let file of pipelineFiles){if(!file.endsWith(".json"))continue;let parsed;try{let raw=await readFile2(path2.join(pipelinesDir,file),"utf-8");parsed=JSON.parse(raw)}catch(parseErr){let msg=parseErr instanceof Error?parseErr.message:String(parseErr);console.error(`Warning: skipping "${file}" \u2014 failed to parse: ${msg}`);continue}let{valid,errors}=validatePipelineSchema(parsed);if(!valid){console.error(`Warning: skipping "${file}" \u2014 validation errors:
|
|
3919
|
+
`};init_version_generated();var README='# @bridge_gpt/mcp-server\n\nThe Bridge MCP is an end-to-end accelerator for shipping code within SFCC, powered by [Bridge API](https://bridgegpt-api.com). Works with Claude Code, Github Copilot, Cursor, Windsurf, and OpenAI Codex.\n\n## Contents\n\n- [Getting Started](#getting-started)\n- [Usage Documentation](#usage-documentation)\n - [Tier 1 \u2014 Regularly useful](#tier-1--regularly-useful)\n - [Tier 2 \u2014 Occasionally useful](#tier-2--occasionally-useful)\n - [Tier 3 \u2014 Now and then](#tier-3--now-and-then)\n - [Operational commands](#operational-commands)\n - [Extra Capabilities](#extra-capabilities)\n- [Salesforce B2C Commerce (SFCC) Tools](#salesforce-b2c-commerce-sfcc-tools)\n- [CLI Subcommands](#cli-subcommands)\n- [Custom Pipelines](#custom-pipelines)\n- [Environment Variables](#environment-variables)\n- [Worktree credentials and the `mcp-invoke` shim](#worktree-credentials-and-the-mcp-invoke-shim)\n- [Reference](#reference)\n\nFor advanced epic/multi-agent orchestration, see [CONDUCTOR.md](./docs/CONDUCTOR.md).\n\n## Getting Started\n\n### Quick start\n\nFrom your **project root**, install and connect in one command:\n\n```bash\nnpx -y @bridge_gpt/mcp-server@latest install-bridge\n```\n\nRun bare like that in a terminal and it starts by asking\n**`How would you like to connect to Bridge API?`** with three numbered choices:\n\n```\n1. I have a Bridge API key\n2. I have an invite token\n3. I\'m new \u2014 set me up with just my email\n```\n\n- **1** \u2014 the existing-key flow. It asks for your **API key** (generate one on the\n Bridge API web UI **Security** page) and a **repo name** matching your server-side\n registration; everything else is derived. A `bapi_inv_\u2026` credential entered here\n instead of a full API key is automatically detected and redeemed as a **bootstrap\n invite** \u2014 it creates a brand-new project and mints your admin API key rather than\n looking up an existing repository.\n- **2** \u2014 the **bootstrap-invite** flow. It asks for the invite token you were given,\n with echo suppressed. Same as passing `--invite` (see below).\n- **3** \u2014 the **self-serve** flow, and the right answer if you have nothing yet. It\n asks for an **email**, then a name for your new Bridge project, and creates the\n workspace and your own admin API key for you. No account, no key, and no invite\n needed beforehand. Same as passing `--email you@example.com` (see below).\n\nThere is **no default**: pressing Enter selects nothing. A blank or invalid answer\nre-prompts once with a hint, then exits with guidance naming all three routes.\n\nThat question is asked only for a *bare interactive* run. Passing any flag, setting\n`BAPI_API_KEY`, or running without an interactive terminal skips it and keeps the\nexisting deterministic behavior.\n\nFrom there `install-bridge` scaffolds the project, writes your editor\'s MCP config\nwith real values, verifies connectivity, persists your API key to the user-scoped\ncredential store, and opens a fresh agent session that runs `/install-bridge` to\nderive and apply the remaining config, presents a concise **capability report**\n("What Bridge can help with"), and recommends `/learn-repository` as your next step.\nIt does **not** run `/learn-repository` itself \u2014 that stays your next explicit\ninvocation. There is **no indexing question**: indexing starts automatically\nserver-side once the repository reaches full parse readiness. Add `--dry-run` to\npreview every step without writing, pinging, or spawning anything.\n\n**Were you sent a bootstrap invite?** Then you don\'t need an API key or the web UI\nat all \u2014 run the command your operator gave you:\n\n```bash\nnpx -y @bridge_gpt/mcp-server@latest install-bridge --invite\n```\n\nThat one-liner is deliberately **secret-free**: the CLI prompts for the bootstrap\ninvite token with **echo suppressed**, and sends it only in the request body. It\ncreates your project and mints your own admin API key in a single command.\n\nTo upgrade later, run:\n\n```bash\nnpx -y @bridge_gpt/mcp-server upgrade\n```\n\n`upgrade` re-execs from `@latest`, refreshes all scaffolded artifacts (slash\ncommands, agents, pipelines), rewrites the version pin, and reconnects \u2014 also\navailable as the `/upgrade-bridge` slash command. (The legacy `--upgrade` flag still\nworks as a fallback.)\n\n<details>\n<summary><strong>Installation Instructions</strong></summary>\n\n#### What `install-bridge` does\n\n`install-bridge` collapses the whole setup into a single command. It:\n\n1. **Scaffolds** the project (the same artifacts `--init` writes: slash commands,\n agents, `.bridge/pipelines/`, and secret-free MCP config placeholders).\n2. **Writes the per-host MCP config** (`.mcp.json` / `.cursor/mcp.json` /\n `.vscode/mcp.json`) with your real `BAPI_REPO_NAME` / `BAPI_API_KEY` /\n `BAPI_BASE_URL` / `BAPI_DOCS_DIR`, preserving any unrelated servers. The\n launcher it writes is pinned to the exact installed version so `npx` never\n silently reuses a stale local copy. (Windsurf and Codex are global configs it\n can\'t safely write \u2014 it prints copy-paste instructions for those.)\n3. **Verifies connectivity** against the Bridge API before persisting anything.\n4. **Persists your key** to the user-scoped credential store\n (`~/.config/bridge/credentials.json`, target `bapi:<repo>`) so shell-spawned\n tooling (e.g. `start-tickets`) can resolve it.\n5. **Opens a fresh agent session** that runs `/install-bridge` to derive and apply\n the remaining config fields from your codebase, presents a concise capability\n report ("What Bridge can help with"), and recommends `/learn-repository` as the\n next step. It does not chain into running `/learn-repository` itself \u2014 that\'s\n your next explicit invocation. There is no indexing question anywhere: indexing\n starts automatically once the repository reaches full parse readiness (VCS\n credentials, the code index prerequisites, and project description), so you\n never need to ask for it or run `/parse-repository` yourself as part of\n onboarding.\n\nIn this **existing-key** flow the only inputs are an **API key** and a **repo name**\n(everything else is derived). Resolution order:\n\n- **API key:** `--api-key <key>` \u2192 `BAPI_API_KEY` env \u2192 an interactive (no-echo)\n prompt. Generate one first on the Bridge API web UI **Security** page (see\n [Generate an API Key](#2-generate-an-api-key)); in this flow the command consumes\n a key, it never mints one \u2014 **`--email` and `--invite` are the two exceptions**\n (below), and each mints your first key. All three of `--api-key`, `BAPI_API_KEY`,\n and the hidden prompt also accept a bootstrap-invite value (`bapi_inv_\u2026`) \u2014\n detected automatically and redeemed the same way `--invite` is, skipping\n repository lookup entirely. `--invite` and `--email` remain the preferred,\n explicit entry points for a new project. The key is **never printed or logged**.\n- **Repo name:** `--repo <name>` and `BAPI_REPO_NAME` remain the deterministic\n short-circuits and compatibility fallbacks \u2014 when either is set it is used\n directly, with no network round-trip. When **neither** is set, a compatible\n server resolves the unique repository from your existing API key automatically\n (a read-only lookup), so you don\'t have to supply it. If the server is older,\n the key can\'t be uniquely resolved, or the lookup fails, `install-bridge` falls\n back to an inferred default you confirm interactively (and requires `--repo`\n when stdin is non-interactive). Whatever name is used MUST match the\n server-side repository registration.\n\n#### Self-serve email onboarding (`--email`) \u2014 no account, no key, no invite\n\nThe primary path for a **first-time user with nothing yet** \u2014 no Bridge account,\nno API key, and no pre-issued invite. Run:\n\n```bash\nnpx -y @bridge_gpt/mcp-server@latest install-bridge --email you@example.com\n```\n\n`install-bridge` requests a brand-new Bridge workspace for that email, receives an\ninvite token, and then feeds it into the **exact same** persist-before-exchange\nbootstrap protocol as `--invite` below \u2014 so the project is created and your first\nadmin key is minted in one command. The minted token is used internally and **never\nshown**.\n\nThe email may instead come from the `BAPI_SIGNUP_EMAIL` environment variable or a\n**visible** interactive prompt (email is not a secret, so it is echoed as you type \u2014\nunlike the API key and the invite token, which use a hidden prompt). That prompt is\nwhat **option 3** of the bare-run chooser reaches, so\n`install-bridge --email you@example.com` and a bare `install-bridge` + `3` land in\nthe same place. The email is still **never written to a log line**. No email\nverification is performed and no message is sent to the address \u2014 it only labels your\nnew workspace. `--email` is mutually exclusive with `--api-key` and `--invite`.\n\n##### Self-serve retries resume automatically\n\nA self-serve run that fails part-way through \u2014 a network blip on the exchange, a\nfailed connectivity check, an interrupted credential write \u2014 saves its signup state\nunder `bootstrap-pending:<repo>` in the credential store (mode `0600`, fsync\'d, the\nsame record that already holds your `key_secret`).\n\n**Just re-run the self-serve flow.** It detects that saved attempt, prints\n`resuming your previous signup attempt for <repo>`, and re-drives the *same*\nexchange \u2014 it does **not** sign up again, so a retry never creates a second\nworkspace. This is why the self-serve record stores the minted invite token: you\nwere never shown that token, so nothing else could re-present it.\n\nDo **not** copy, display, or hand-remove that record. If the saved invite has\ngenuinely expired, the CLI says so and **asks for confirmation** before discarding\nit and starting fresh \u2014 it never discards it silently, because a record whose\nexchange already succeeded is the only trace of a live admin key.\n\nBecause this flow *creates* the project, it asks you to **name a new project**\n(`Name your new Bridge project [<inferred>]: `) rather than to match an existing\nserver-side registration. The name must be globally unique; if it\'s taken, you\'re\nasked for another one and the invite is not consumed. The same applies to `--invite`.\n\n#### Bootstrap-invite onboarding (`--invite`)\n\nWith a **bootstrap invite** you were already given, there is no pre-existing key and\nno web UI: like `--email` above, this mode **creates** the project and its first\nadmin key instead of consuming one. Run `install-bridge --invite` and it:\n\n1. **Prompts for the bootstrap invite token** with echo suppressed (the default \u2014\n see below).\n2. **Generates your `key_secret`** (32 CSPRNG bytes) and **fsyncs it locally**\n *before* contacting the server. If that write fails the run aborts and the\n invite is **not** spent.\n3. **Redeems the invite** \u2014 `POST /setup/bootstrap` with the token, repo name, and\n `key_secret` in the **body** \u2014 which creates the project and mints your admin\n key. (This replaces the connectivity ping: there is no key to ping with yet.)\n4. **Verifies the newly-minted key**, then writes the per-host MCP config.\n5. **Promotes the credential** to `bapi:<repo>` and **opens the agent session** \u2014\n the same Steps 3\u20135 as the normal flow.\n\nBecause the locally-saved `key_secret` is the only proof that can replay a\nredemption, a re-run after a network failure is safe: it re-sends the *same* secret\nand gets back the *same* project and key. If the repo name you chose is already\ntaken (names are globally unique) the server rolls back \u2014 your invite is untouched \u2014\nand the CLI asks for a different name and retries with the same token.\n\n**The delivered one-liner is secret-free, by design.** "A copy/paste one-liner" and\n"the token never touches shell history" are contradictory, so the token is *not* in\nthe command: the CLI asks for it, and it travels only in the request body.\n`--invite <token>`, `--invite=<token>`, and `BAPI_INVITE` still work for\n**scripting only** \u2014 and both forms **expose the token to your shell history and to\nthe process list**. Prefer the prompt.\n\nUseful flags:\n\n- `--dry-run` \u2014 preview every step (scaffold targets, config files and keys with\n the key value **redacted**, the ping target, the credential store target, and\n the exact agent spawn command) without writing, pinging, or spawning anything.\n With `--invite` it also never calls the exchange endpoint and never generates or\n stores a secret.\n- `--force` \u2014 overwrite an existing real `BAPI_API_KEY` in a host config, or in the\n credential store, without prompting (re-running is otherwise non-destructive).\n- `--agent claude|cursor-agent` \u2014 which agent to launch for the agentic remainder\n (default `claude`).\n- `--email <addr>` \u2014 self-serve signup: create a new workspace from just an email\n (mutually exclusive with `--api-key` and `--invite`). Falls back to\n `BAPI_SIGNUP_EMAIL`, then a visible prompt. Visible input, not a secret; still\n never logged.\n- `--invite [token]` \u2014 redeem a bootstrap invite (mutually exclusive with\n `--api-key` and `--email`). Omit the value to get the hidden prompt.\n\nThat\'s it \u2014 once `install-bridge` finishes you\'re connected. If you prefer to do\nit by hand (or just want to understand each step), the manual flow below does the\nsame thing.\n\n#### Manual Setup (Alternative)\n\n##### 1. Install the Package\n\nFrom your **project root**, install the MCP server and scaffold slash commands:\n\n```bash\nnpm i @bridge_gpt/mcp-server\nnpx -y @bridge_gpt/mcp-server --init\n```\n\n`--init` must be run from the directory containing your `package.json`. It:\n\n- Creates slash commands in `.claude/commands/` and `.cursor/commands/`\n- Detects existing MCP config files and sets `BAPI_PROJECT_ROOT` so local file output resolves correctly\n- Scaffolds `.bridge/pipelines/` for custom pipeline authoring\n\nRe-run `--init` after upgrading the package to get updated commands.\n\n##### 2. Generate an API Key\n\n1. Log in to [Bridge API](https://bridgegpt-api.com) and navigate to your project\'s **Security** page\n2. Click **Create New Key**\n3. Enter your email, an optional label (e.g., "MCP Server"), and select the **Admin** role\n4. Click **Create Key**\n5. **Copy the key immediately** \u2014 it will not be shown again\n\n##### 3. Configure the MCP Server\n\nAdd the following to your editor\'s MCP configuration file, pasting in the API key from step 2:\n\n<details>\n<summary><strong>Claude Code (.mcp.json)</strong></summary>\n\nThe `--init` command (step 1) detects Claude Code and creates a `.mcp.json` at your project root with placeholder values. Open it and replace `your-repo` and `your-api-key` with your actual values from step 2:\n\n```json\n{\n "mcpServers": {\n "bridge-api": {\n "command": "npx",\n "args": ["-y", "@bridge_gpt/mcp-server"],\n "env": {\n "BAPI_BASE_URL": "https://bridgegpt-api.com",\n "BAPI_REPO_NAME": "your-repo",\n "BAPI_API_KEY": "your-api-key",\n "BAPI_DOCS_DIR": "docs/tmp"\n }\n }\n }\n}\n```\n</details>\n\n<details>\n<summary><strong>VS Code / Copilot (.vscode/mcp.json)</strong></summary>\n\n```json\n{\n "servers": {\n "bridge-api": {\n "command": "npx",\n "args": ["-y", "@bridge_gpt/mcp-server"],\n "env": {\n "BAPI_BASE_URL": "https://bridgegpt-api.com",\n "BAPI_REPO_NAME": "your-repo",\n "BAPI_API_KEY": "your-api-key",\n "BAPI_DOCS_DIR": "docs/tmp"\n }\n }\n }\n}\n```\n</details>\n\n<details>\n<summary><strong>Cursor (.cursor/mcp.json)</strong></summary>\n\n```json\n{\n "mcpServers": {\n "bridge-api": {\n "command": "npx",\n "args": ["-y", "@bridge_gpt/mcp-server"],\n "env": {\n "BAPI_BASE_URL": "https://bridgegpt-api.com",\n "BAPI_REPO_NAME": "your-repo",\n "BAPI_API_KEY": "your-api-key",\n "BAPI_DOCS_DIR": "docs/tmp"\n }\n }\n }\n}\n```\n\n> If project-local config is not supported in your Cursor version, use `~/.cursor/config/mcp.json` instead.\n</details>\n\n<details>\n<summary><strong>Windsurf (~/.codeium/windsurf/mcp_config.json)</strong></summary>\n\nWindsurf only supports global MCP configuration.\n\n```json\n{\n "mcpServers": {\n "bridge-api": {\n "command": "npx",\n "args": ["-y", "@bridge_gpt/mcp-server"],\n "env": {\n "BAPI_BASE_URL": "https://bridgegpt-api.com",\n "BAPI_REPO_NAME": "your-repo",\n "BAPI_API_KEY": "your-api-key",\n "BAPI_DOCS_DIR": "docs/tmp"\n }\n }\n }\n}\n```\n</details>\n\n<details>\n<summary><strong>OpenAI Codex (~/.codex/config.toml)</strong></summary>\n\n```toml\n[mcp_servers.bridge-api]\ncommand = "npx"\nargs = ["-y", "@bridge_gpt/mcp-server"]\n\n[mcp_servers.bridge-api.env]\nBAPI_BASE_URL = "https://bridgegpt-api.com"\nBAPI_REPO_NAME = "your-repo"\nBAPI_API_KEY = "your-api-key"\nBAPI_DOCS_DIR = "docs/tmp"\n```\n\n> Codex users: set `BAPI_PROJECT_ROOT` manually in your config (see Environment Variables below).\n</details>\n\nAfter saving the config, restart your editor or reload the MCP server connection. Verify connectivity by asking your AI assistant to call the `ping` tool.\n\n##### 4. First-Time Setup: Teach Bridge Your Codebase\n\nIf you\'re the first person to install Bridge API on your project, run the `/learn-repository` slash command after completing setup. This analyzes your codebase\'s architecture, testing patterns, code review standards, and documentation conventions, then uploads the findings to Bridge API. This gives Bridge the context it needs to generate implementation plans, ticket critiques, and code reviews that are consistent with your project\'s actual architecture and conventions.\n\nYou only need to do this once per project \u2014 the learned standards persist for all team members.\n\n##### Upgrading (details)\n\nThe one-command `npx -y @bridge_gpt/mcp-server upgrade` (shown above) is the\nrecommended path. The legacy flag `npx -y @bridge_gpt/mcp-server --upgrade` still\nworks: it runs `npm i @bridge_gpt/mcp-server@latest`, prints a before/after version\nsummary, then re-runs the full `--init` scaffolding flow to update your slash\ncommands, agents, and pipeline definitions.\n\nThe MCP server also checks for updates automatically on startup. If a newer version is available, you\'ll see a notice in your editor\'s MCP output logs with the upgrade command to run. This check is cached for 24 hours and never blocks server startup.\n\n</details>\n\n## Usage Documentation\n\nThis is the Bridge API tooling worth knowing about as a software engineer \u2014 the things you\'d ask an agent to do \u2014 grouped by how often you would use them. Each entry covers **what it does**, **when it\'s useful** and **how to use it**. The behind-the-scenes plumbing is summarized at the end under [Extra Capabilities](#extra-capabilities), and a full enumeration lives in [Reference](#reference).\n\nWorking in a Salesforce B2C Commerce codebase? Bridge also ships read-only SFCC platform-introspection tools \u2014 see [Salesforce B2C Commerce (SFCC) Tools](#salesforce-b2c-commerce-sfcc-tools).\n\nFor invocation, prefer the slash command \u2014 it\'s deterministic. A free-text example is shown only where natural-language phrasing reliably maps to the right automation; high-consequence or easily-misread automations show only the slash command on purpose.\n\n### Tier 1 \u2014 Regularly useful\n\nThese features are useful for most tickets.\n\n**1. Review Ticket**\n- **What it does:** Runs a full quality review of a ticket: clarifying questions + critique plus an automatic alternate-model second opinion, then evaluates findings and produces a decision page to accept/reject them. The backend executor now owns all review round orchestration (including the second-opinion round) server-side; pass `--rounds=1` for a cheaper single-pass review, `--rounds=2` to force the full second-opinion review, or omit `--rounds` to let the backend\'s difficulty-adaptive review policy decide.\n- **When it\'s useful:** (Refinement) Right after a ticket is drafted, before anyone starts building \u2014 to surface gaps and tighten it.\n- **How to use it:** `/review-ticket BAPI-123` (command only \u2014 "review" as free text is easily mistaken for a freehand agent review).\n- **Flags:** `--auto` auto-accept findings / skip the approval gates \xB7 `--rounds=1` request a cheaper single-pass review (no second-opinion round) while preserving downstream evaluation and decision-capture work \xB7 `--rounds=2` force the full second-opinion review \xB7 omit `--rounds` to let the backend\'s difficulty-adaptive review policy decide (it falls back to a full second-opinion review when adaptive routing is disabled or the ticket\'s difficulty cannot be resolved).\n- **Multi-ticket fan-out:** `/review-tickets BAPI-123 BAPI-456` opens one terminal tab per ticket for parallel review with no worktrees (terminal launcher only \u2014 no `wt`/`git`). All `/review-ticket` flags apply; `--review KEY=auto,rounds=N` sets per-ticket overrides. Packaged CLI: `npx -y @bridge_gpt/mcp-server review-tickets KEY [KEY ...]`.\n\n**2. Start Tickets**\n- **What it does:** Creates one git worktree per ticket and spawns an agent session in each to implement them in parallel.\n- **When it\'s useful:** (Implementation | Automation) When you\'re ready to start building one or more refined tickets concurrently.\n- **How to use it:** `/start-tickets BAPI-248 BAPI-250` (see [CLI Subcommands](#cli-subcommands) for the full flag table and cross-platform behavior).\n- **Flags:** `--auto` skip the approval gates \xB7 `--base-branch <branch>` branch off something other than the default \xB7 `--workflow implement|review-and-implement` selects which slash command each spawned worktree runs (default `implement`, byte-identical to today; `review-and-implement` runs `/review-ticket` inline then, after a per-ticket halt gate, hands off to a **fresh** `/implement-ticket` session reusing the same worktree) \xB7 `--rounds=1|2` review-only, forwarded to the review phase, valid only with `--workflow review-and-implement` \xB7 `--tier cheap|basic|premium` coarse model-routing override (see [CLI Subcommands](#cli-subcommands)).\n\n**3. Council**\n- **What it does:** Fans your problem out to two different LLMs and returns their approaches directly. Runs in one of four modes, selected via `mode`: **`technical`** (default \u2014 implementation/architecture approaches), **`design`** (UI/UX and visual direction), **`discovery`** (stakeholder discovery questions for early/vague tasks, grouped into `Technical Discovery Questions` and `Business / Stakeholder Discovery Questions` and tagged `[HUMAN]`/`[CODE]`/`[TICKET]`), or **`general`** (brief-driven ideation from your task description and concerns alone). `technical` and `discovery` are codebase-grounded \u2014 they retrieve from the repository index and need a successfully indexed repo. `general` needs no code index at all, so it works immediately after install, before `/parse-repository` has ever run. The legacy boolean `design=true` still works and maps to `mode: "design"`.\n- **When it\'s useful:** (Architecture | Refinement) Early, when you want a spread of approaches \u2014 `technical` for *how to build it*, `design` for *how it should look*, `discovery` for *what we still need to figure out* before a real ticket exists, `general` for a quick brief-driven council before the repository is indexed.\n- **How to use it:** ask your agent to convene a council \u2014 *"Convene a council on approaches for adding rate limiting to the LLM client; fan it out to multiple models."* For a design pass: *"Run a design council for the evidence-freshness dashboard UI."* For early discovery: *"Run a discovery council \u2014 `request_council` with `mode: "discovery"` \u2014 for this vague request so we can collect the questions stakeholders need to answer first."* For a fresh, unindexed repo: *"Run a general council \u2014 `request_council` with `mode: "general"` \u2014 on launch options for this idea."*\n\n**4. Deep Research**\n- **What it does:** Runs multi-source, fact-checked web research on a technical topic and returns a cited report.\n- **When it\'s useful:** (Architecture | Refinement) When a decision hinges on outside knowledge (libraries, best practices, standards) you don\'t already have.\n- **How to use it:** `/bridge-research <question>`\n\n**5. Jira Ticket Writer**\n- **What it does:** An agent that drafts a well-structured Jira ticket from a plain description, applying your project\'s standards.\n- **When it\'s useful:** (Refinement) When you have an idea in your head and want a properly-formatted ticket draft without writing it by hand.\n- **How to use it:** `/write-ticket <description>` (or ask your agent) \u2014 *"Use the jira ticket writer to turn our conversation into a ticket."*\n- **Flags:** `--standards <path>` apply a specific standards file when drafting.\n\n**6. Upload Ticket**\n- **What it does:** Pushes a drafted ticket up to Jira as a real issue (the `create_ticket` capability); handles markdown and child tickets under an Epic.\n- **When it\'s useful:** (Refinement) The final step after drafting \u2014 to get the ticket into Jira so it can be tracked and worked.\n- **How to use it:** Ask your agent to create the ticket, and it should confirm before creating the live Jira issue.\n- **Options:** Describe the issue type (Bug / Story / Task / Epic) and, for a child under an Epic, the parent key.\n\n### Tier 2 \u2014 Occasionally useful\n\nThese features are good to know, but you probably won\'t use them every day.\n\n**1. Plan Ticket**\n- **What it does:** Generates a step-by-step implementation plan for a ticket, with references to real code files, and saves it locally.\n- **When it\'s useful:** (Refinement | Implementation) Once a ticket is solid and you want a concrete build plan before (or instead of) auto-implementing.\n- **How to use it:** `/plan-ticket BAPI-123`\n- **Flags:** `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check the plan with a second provider.\n\n**2. Clarify Ticket**\n- **What it does:** Generates clarifying questions for a ticket (or debugging guidance for bugs) and saves them locally.\n- **When it\'s useful:** (Refinement) When a ticket feels under-specified and you want the open questions made explicit.\n- **How to use it:** `/clarify-ticket BAPI-123` \u2014 *"Generate clarifying questions for BAPI-123"*\n- **Flags:** `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check with a second provider.\n\n**3. Critique Ticket**\n- **What it does:** Critiques a ticket\'s quality against your project standards and lists deviations + improvements.\n- **When it\'s useful:** (Refinement) When you want a quality gate on a ticket before it\'s worked.\n- **How to use it:** `/critique-ticket BAPI-123` \u2014 *"Critique BAPI-123 against our project standards and list what\'s missing or deviating."*\n- **Flags:** `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check with a second provider.\n\n**4. Create Doc**\n- **What it does:** Generates a design document for a ticket \u2014 a TDD (technical design, engineer audience), an FSD (functional spec, for product/design/QA), or a PRD (product requirements: problem, goals, success metrics) \u2014 and saves it locally.\n- **When it\'s useful:** (Architecture | Refinement) When a ticket needs a fuller design write-up before planning or implementation, in the shape that fits your audience.\n- **How to use it:** `/create-doc BAPI-123 --doc-type tdd` (or `fsd` / `prd`)\n- **Flags:** `--doc-type tdd|fsd|prd` which document to generate (required) \xB7 `--provider <name>` choose the model provider \xB7 `--second-opinion <provider>` cross-check with a second provider.\n\n**5. Explore Ticket**\n- **What it does:** Explores the codebase for a task and recommends implementation options or surfaces clarifying questions, with optional research.\n- **When it\'s useful:** (Architecture | Refinement) Before writing a ticket or plan, when you\'re unsure how a change would fit the existing code.\n- **How to use it:** `/explore-ticket <task>` \u2014 *"Explore the codebase for how we\'d add a Mistral LLM provider and recommend 2\u20133 implementation options."*\n\n**6. Second Opinion**\n- **What it does:** Gets an immediate critique of any text from a different model family \u2014 no artifact saved, just the reply.\n- **When it\'s useful:** (Architecture | Refinement | Implementation) Any time you want a quick sanity check on a plan, draft, or decision from a fresh perspective.\n- **How to use it:** ask your agent \u2014 *"Get a second opinion from Gemini on whether the BAPI-123 plan\'s migration step is safe to run against prod."*\n- **Options:** pick the provider (anthropic / openai / gemini) and the tier (cheap / basic / premium).\n\n**7. Generate Image**\n- **What it does:** Generates an image from a text prompt using a provider image model (OpenAI `gpt-image-2` by default, or Google Imagen) and returns the image directly. Spends provider credits on every call.\n- **When it\'s useful:** (Architecture | Refinement) When you want a quick visual \u2014 a UI mockup, diagram, or illustration \u2014 to anchor a design discussion or attach to a ticket.\n- **How to use it:** ask your agent \u2014 *"Generate an image of a dashboard showing SOC2 evidence freshness as a traffic-light grid."*\n- **Options:** `provider` openai (`gpt-image-2`) / gemini (Imagen \u2014 adds an invisible SynthID watermark) \xB7 `quality` low (default, cheapest) / medium / high \xB7 `size` 1024x1024 / 1024x1536 / 1536x1024. The image is always saved to `BAPI_DOCS_DIR/images/` and also returned inline.\n\n**8. Implement Ticket**\n- **What it does:** Full build for one ticket: generate a plan, write the code, commit, open a PR, and monitor CI.\n- **When it\'s useful:** (Implementation) When a ticket is ready and you want it taken from plan to open PR in one go.\n- **How to use it:** `/implement-ticket BAPI-123` (command only \u2014 "implement X" as free text almost always triggers a freehand build instead of the Bridge plan\u2192code\u2192PR\u2192CI pipeline).\n- **Flags:** `--auto` skip the approval gates (e.g. auto-commit/push).\n\n**9. Full Automation**\n- **What it does:** Drives the whole chain end-to-end: idea \u2192 ticket(s) \u2192 review each \u2192 spawn worktrees to implement.\n- **When it\'s useful:** (Automation) When you want to go from a raw idea to in-progress implementation with minimal hands-on steps.\n- **How to use it:** `/full-automation <idea>` (command only \u2014 creates tickets, spawns worktrees, and carries scheduling/`--max-children` flags that free text can\'t).\n- **Flags:** `--require-approval` toggle the approval gates, full automation runs end to end by default.\n\n**10. Idea to Ticket**\n- **What it does:** Turns a one-line idea into a Jira Task/Spike (or an Epic plus child tickets), with research, duplicate detection, and a critique pass built in.\n- **When it\'s useful:** (Refinement | Automation) When you have a rough idea and want a fully-formed, uploaded ticket without the manual draft-and-refine loop.\n- **How to use it:** `/idea-to-ticket <idea>`\n\n### Tier 3 \u2014 Now and then\n\nThese features are useful once in a while, but you probably won\'t need them everyday.\n\n**1. Reimplement Ticket**\n- **What it does:** Pulls in new context/attachments since the last pass and implements small follow-up changes on an already-built ticket.\n- **When it\'s useful:** (Implementation) After review feedback or new screenshots, when you need a targeted second pass rather than a fresh build.\n- **How to use it:** `/reimplement-ticket BAPI-123`\n\n**2. Run Tests**\n- **What it does:** Runs the unit and E2E suites and autonomously triages/fixes failures (via the test-correction agent).\n- **When it\'s useful:** (Implementation) After making changes, to confirm everything passes and auto-fix straightforward breakages.\n- **How to use it:** `/run-tests` (`--unit-only`, `--skip-e2e`)\n\n**3. Plan Epic**\n- **What it does:** Decomposes a large epic into sub-tasks with a structured exploration doc for each.\n- **When it\'s useful:** (Architecture | Refinement) When a feature is too big for one ticket and you need it broken down and scoped.\n- **How to use it:** `/plan-epic <epic>` \u2014 *"Decompose the epic \'migrate PayPal token storage off Custom Objects\' into sub-tasks with an exploration doc for each."*\n\n**4. Update Ticket**\n- **What it does:** Synthesizes a ticket\'s clarifying answers and critique into a rewritten description and pushes it to Jira.\n- **When it\'s useful:** (Refinement) After review, to fold the resolved questions and fixes back into the ticket itself.\n- **How to use it:** `/update-ticket BAPI-123` (command only \u2014 does a full overwrite of the live Jira description; "update" as free text is both vague and hard to reverse).\n\n**5. Get Ticket**\n- **What it does:** Retrieves the full details of a Jira ticket (summary, status, description, etc.).\n- **When it\'s useful:** (Refinement | Implementation) Any time you want the agent to read a ticket before acting on it.\n- **How to use it:** ask your agent \u2014 *"Pull up BAPI-123 and show me its description, status, and acceptance criteria."*\n\n**6. Write Comment**\n- **What it does:** Posts a comment on a Jira ticket (markdown; long ones can attach as a file).\n- **When it\'s useful:** (Refinement | Implementation) To leave context, status, or a decision trail on the ticket.\n- **How to use it:** ask your agent \u2014 *"Post a comment on BAPI-123: blocked on the expired Atlassian token \u2014 will retry after it\'s rotated."*\n\n**7. Download / Upload Attachment**\n- **What it does:** Pulls files off a Jira ticket to disk, or attaches a local file to a ticket.\n- **When it\'s useful:** (Refinement | Implementation) When a ticket has design files/logs you need locally, or you want to attach output back to it.\n- **How to use it:** ask your agent \u2014 *"Download the design mockups attached to BAPI-123 into my docs folder."* / *"Attach build-log.txt to BAPI-123."*\n\n**8. Learn Repository**\n- **What it does:** Researches and documents the repo\'s architecture, testing, review, and correctness standards, then saves them to Bridge for future agents.\n- **When it\'s useful:** (Setup/Learning) When onboarding a new repo, or after big changes, so Bridge\'s agents follow your conventions.\n- **How to use it:** `/learn-repository`\n\n**9. Teach Bridge**\n- **What it does:** Takes a plain-English instruction, figures out which standards field it belongs to, and merges it in (admin only).\n- **When it\'s useful:** (Setup/Learning) When you notice the agents missing a convention and want to correct it in one sentence.\n- **How to use it:** `/teach-bridge <teaching>` \u2014 *"Teach Bridge: always use data-testid selectors in E2E tests."*\n\n**10. Review and Start**\n- **What it does:** Spawns one worktree per ticket; each session reviews the ticket inline and, after a per-ticket human proceed/halt gate, hands off to a **fresh implementation session** that reuses the same worktree \u2014 review and implementation run in two separate agent contexts, not one shared session.\n- **When it\'s useful:** (Refinement | Implementation | Automation) The **recommended front door** for "review these tickets, then implement the ones that pass," starting from existing ticket keys (unlike `/full-automation`, which only accepts an idea).\n- **How to use it:** `/review-and-start BAPI-248 BAPI-250` (single or multiple keys flow through the identical code path).\n- **Flags:** `--auto` a single chain-level flag that auto-approves both the review and the implementation phase of every spawned session \xB7 `--rounds=1|2` forwarded to the review phase \xB7 `--agent`, `--base-branch`, `--max-parallel`, `--dry-run` mirror `/start-tickets`.\n- Under the hood, this is a thin shim over `start-tickets --workflow review-and-implement <KEYS>` \u2014 the lower-level launcher seam documented above; the review\u2192gate\u2192fresh-implementation-handoff logic lives in the spawned `/review-and-implement` session, never in this command or the CLI. On approval, that session reuses its review-time model tier by passing it to the fresh implementation launcher via `--tier`.\n\n### Operational commands\n\nWorkflow commands you\'ll reach for during implementation and CI, beyond the tiers above:\n\n| Command | What it does |\n|---|---|\n| `/code-ticket PROJ-123` | Download the implementation plan and questions, then execute the plan inline |\n| `/commit-ticket PROJ-123` | Stage, commit, and push changes; transition Jira status; post a smoke-test comment |\n| `/create-pr PROJ-123` | Resolve the base branch and open a pull request for the ticket\'s branch (run after `/commit-ticket`) |\n| `/check-ci PROJ-123` | Monitor CI checks for the current branch, triage failures, apply fixes, and report results |\n| `/parse-repository` | Queue a background job to index the repository for Bridge AI agents |\n| `/check-parse-status` | Check whether a background repository parse job is still running |\n| `/scan-tickets` | Sync recently-updated Jira tickets and backfill workflow timestamps |\n\n> Commands are designed for Claude Code. Other editors may support slash commands differently \u2014 check your editor\'s documentation for how to invoke prompt files.\n\n### Extra Capabilities\n\nBehind-the-scenes capabilities an agent gains from the MCP tools \u2014 mostly invoked automatically by the commands above, rarely requested by name:\n\n- **Ship a PR end-to-end:** commit & push, open a pull request, transition the Jira status, and discover/poll CI checks (powers `commit-ticket`, `create-pr`, `check-ci`, `implement-ticket`).\n- **Architecture plan** for a ticket (design-level guidance, separate from the implementation plan).\n- **Index the codebase** so Bridge\'s agents can reason about it: queue/parse the repo, check parse status, regenerate the directory map.\n- **Read & tune project config/standards:** list/read/update config fields, fetch project standards, and the per-topic `learn-*` commands that populate them.\n- **Ticket lifecycle bookkeeping:** track tickets and backfill workflow-state timestamps (`scan-tickets`), search across tickets, read comments, list attachments.\n- **Pipeline machinery:** list/inspect pipeline recipes and run/resume/list/delete pipeline runs (the engine under the orchestration commands).\n- **Decision page** generation for capturing human review decisions as structured data.\n- **Connectivity & identity checks:** ping Bridge, check your role, resolve the local docs directory.\n- **Retrieve any generated artifact** (`get_*` for plans, critiques, questions, councils, research, architecture) without regenerating it.\n- **Tiered-section execution telemetry** recording (internal measurement).\n\n## Salesforce B2C Commerce (SFCC) Tools\n\nSalesforce\'s official MCP server, `@salesforce/b2c-dx-mcp`, covers developer-experience tasks \u2014 cartridge deploy, step debugging, docs search, logs/MRT, and SCAPI Custom API scaffolding. As of its current toolset it has no way to *read* the platform\'s object model, custom objects, or site configuration \u2014 exactly the context an AI coding agent needs to write correct Commerce code and to inspect a sandbox before touching it. Bridge\'s SFCC tools install side-by-side with `b2c-dx-mcp` (they don\'t duplicate its surface) and close that gap with **read-only OCAPI Data API introspection** of system objects, custom object definitions, and site preferences.\n\n**v1 is read-only and developer-sandbox-only** \u2014 no writes, and non-sandbox instances are rejected. Credentials stay local (in `dw.json` or `SFCC_*` env vars) and are never sent to Bridge.\n\nFor a step-by-step OCAPI client setup guide (including the Business Manager permissions grant), see [docs/install/sfcc-integration.md](./docs/install/sfcc-integration.md).\n\n<details>\n<summary><strong>Setup</strong></summary>\n\nThe two diagnostic tools (`sfcc_setup_status`, `check_permissions`) are always available. The read tools, the write tools, and `sfcc_log_query` must be enabled with a profile (step 3). Changing `BRIDGE_MCP_PROFILE` requires an MCP client restart.\n\n**Prerequisites:** a running SFCC **sandbox** and its hostname, plus an Account Manager API client (`client-id` + `client-secret`).\n\n**1. Set the repo `version` config field** to your SFCC project type \u2014 one of `sfra | pwakit | sitegenesis | storefrontnext | hybrid`. The call-time gate reads this; a non-SFCC value blocks every SFCC tool except `sfcc_setup_status`. Set it via your normal config path, the `config_field` MCP tool (operation `update`, field `version`), or the `/teach-bridge` skill.\n\n**2. Provide SFCC credentials.** Create a `dw.json` in your project root:\n\n```json\n{\n "hostname": "zzzz-001.sandbox.us01.dx.commercecloud.salesforce.com",\n "client-id": "<account-manager-client-id>",\n "client-secret": "<account-manager-client-secret>"\n}\n```\n\nAccepted key spellings: `hostname`/`host`, `client-id`/`clientId`/`client_id`, `client-secret`/`clientSecret`/`client_secret`. Prefer a single config \u2014 a multi-entry `configs[]` array forces you to pass an explicit `instance` on every call. `dw.json` is auto-added to git exclude and must never be committed. Alternatively, export `SFCC_HOSTNAME` / `SFCC_CLIENT_ID` / `SFCC_CLIENT_SECRET` in the MCP server environment.\n\n**3. Enable the read tools.** Add `sfcc` to `BRIDGE_MCP_PROFILE` in the MCP server `env` block (it is comma-separated; `full` also works), then **restart the MCP client**:\n\n```json\n"env": { "BRIDGE_MCP_PROFILE": "sfcc" }\n```\n\nWithout this, only the diagnostic tools are registered.\n\n**4. Verify and grant OCAPI access.** Ask your agent to run `sfcc_setup_status` (expect all checks \u2713), then `check_permissions`. If it reports HTTP 401/403, it prints the exact OCAPI Settings JSON to paste in Business Manager (**Administration \u2192 Site Development \u2192 Open Commerce API Settings \u2192 Data API** tab); save it there and re-run. Restart the MCP client after any credential or env change \u2014 a running session does not pick them up.\n\n</details>\n\n### Tools\n\nAll SFCC tools are read-only and target a developer sandbox. Oversized responses are automatically saved to `BAPI_DOCS_DIR/sfcc/` and previewed inline.\n\n**Diagnostics** (always available, no profile needed)\n- `sfcc_setup_status` \u2014 report on every prerequisite: Bridge API key, repo name, `version` config, `dw.json` presence/uniqueness, AM (OCAPI) token acquisition, and the independent **SFCC Log Query (WebDAV)** capability that gates `sfcc_log_query`.\n- `check_permissions` \u2014 probe OCAPI access via `GET /system_object_definitions`; on 401/403, print the exact OCAPI Settings JSON to paste in Business Manager (split read-only vs. write/import grants).\n\n**System object model** (needs the `sfcc` profile)\n- `system_object_list` \u2014 list system object types (Product, Order, Customer, \u2026).\n- `system_object_get` \u2014 fetch one type\'s definition, optionally with its full attribute definitions (`expand_attribute_definitions`).\n- `system_object_attribute_search` \u2014 search a type\'s attribute definitions; prefer this over a full dump when hunting a specific `c_` custom attribute.\n\n**Custom object definitions** (needs the `sfcc` profile)\n- `custom_object_definition_attributes_get` \u2014 fetch attribute definitions for a known custom object type. OCAPI cannot enumerate custom object types, so `object_type` must already be known.\n- `custom_object_definition_attribute_search` \u2014 search attribute definitions within a known custom object type. Read-only \u2014 creating a custom object *type* isn\'t possible via OCAPI; that\'s a future v2 metadata-import capability.\n- `custom_object_definition_attribute_create` \u2014 **write** (sandbox only): create an attribute definition on a known custom object type via `PUT /custom_object_definitions/{type}/attribute_definitions/{id}`. TYPE creation is never attempted (the type must pre-exist). Echoes paste-ready OCAPI grant JSON on 403.\n- `custom_object_definition_attribute_update` \u2014 **write** (sandbox only): update an attribute definition via an ETag-conditional `PATCH \u2026/attribute_definitions/{id}`; surfaces 409/412 conflicts and echoes grant JSON on 403.\n\n**Site preferences** (needs the `sfcc` profile; sandbox only)\n- `site_preference_get` \u2014 read a preference group\'s effective preferences.\n- `site_preference_search` \u2014 search/filter preferences within a group.\n- `site_preference_values_set` \u2014 **write** (sandbox only): set custom preference values via `PATCH /site_preferences/preference_groups/{group}/sandbox` with a flat map of `c_`-prefixed ids to string/number/boolean/string[] values. A bad group returns 404 `CustomPreferenceGroupNotFoundException`; echoes grant JSON on 403.\n\n**On-demand log query** (needs the `sfcc` profile)\n- `sfcc_log_query` \u2014 query redacted, filtered SFCC logs on demand. `environment` and `time_range` (`start`/`end`) are **required** \u2014 production, "all environments", and an open-ended period are never inferred. The tool calls a Bridge backend endpoint that runs the pull \u2192 redaction \u2192 filter pipeline server-side and returns scoped, redacted findings; **WebDAV credentials, retrieval, redaction, and filtering all stay server-side and single-sourced.** It holds no credentials of its own.\n - **Guardrails.** Selection is bounded by log-file `prefixes` (max 5), the time range, a scanned-entry cap (`max_entries`, \u2264 2000), a finding cap, and a per-snippet length cap. High-volume prefix classes (`info`, `jobs`, `debug`, `customdebug`) impose a **stricter 6-hour** max range (vs. 24h for the error class) because `info-*` runs ~1 MB/day versus `error-*` at ~13 KB median \u2014 a wide window over a high-volume prefix is **rejected**, never silently narrowed.\n - **Response order.** Resolved scope (`environment`, `time_range`, `applied_prefixes`) and cap `status` first, redacted `findings` second, retrieval/truncation `metadata` last.\n - **Statuses & errors.** `ready`, `no_matching_findings`, `results_truncated`; plus `VALIDATION_ERROR` (bad/oversized scope, caught before any network call), `NOT_CONFIGURED` (503 \u2014 the log capability isn\'t set up; run `sfcc_setup_status`, whose step 6 reports it), and `BAD_GATEWAY`/`SERVICE_UNAVAILABLE` on a retrieval/backend failure.\n - **Auth is separate from OCAPI.** Log retrieval uses **HTTP Basic auth** \u2014 a Business Manager username + a **40-character WebDAV access key** \u2014 *not* the OCAPI Account Manager OAuth token the other SFCC tools use. A valid AM bearer token 401s on `/Logs`. `sfcc_setup_status` step 5 (AM/OCAPI token) and step 6 (WebDAV log access) are independent: one can be green while the other is not.\n - **Local / air-gapped fallback.** The primary path above is the only path this tool takes. For air-gapped development, the documented fallback is Salesforce\'s own **`@salesforce/b2c-dx-mcp`** (`logs_list_files`, configured from `dw.json`) \u2014 it is vendor-maintained and reads log files over WebDAV, so it is strictly less work than shelling the B2C CLI (`b2c logs get --since <window> --search <q> --json`). It is **not** the primary path because its credentials live client-side and its output has **not** passed Bridge\'s redaction/filter. If you use it, its output must be treated as raw: route it back through the same server-side Python `LogSource` composition and `RedactionPort`/T3 filter workflow \u2014 never paste or relay unredacted `b2c-dx-mcp` or CLI output to an LLM.\n\n## CLI Subcommands\n\nBeyond `--init` / `--upgrade`, the package ships operational subcommands of the **single `bridge-api-mcp-server` bin** (not separate binaries) \u2014 so they travel with the package to every consumer. See [Usage Documentation \u2192 Start Tickets](#tier-1--regularly-useful) for *when* to use `start-tickets`; this section is the full CLI reference.\n\n### `start-tickets`\n\nSpawns one Worktrunk worktree + selected-agent session per Jira ticket and backs the `/start-tickets` slash command. The agent defaults to **Claude Code** (`claude`) and is configurable via `--agent`. For existing ticket keys, `/review-and-start` (see [Usage Documentation \u2192 Review and Start](#tier-3--now-and-then)) is the recommended enriched front door over this CLI\'s `--workflow review-and-implement` seam; using the CLI directly (below) remains the advanced/lower-level path.\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets [flags] KEY [KEY ...]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--agent claude\\|cursor-agent` | `claude` | Agent command to launch in each worktree |\n| `--workflow implement\\|review-and-implement` | `implement` | Slash command each spawned worktree runs. `implement` is byte-identical to today\'s `/implement-ticket <KEY> [--auto]`. `review-and-implement` spawns `/review-and-implement <KEY> [--auto] [--rounds=<n>]`, which runs `/review-ticket` then, after a per-ticket halt gate, `/implement-ticket` in the same session. `--auto` applies to the selected workflow as a whole. |\n| `--rounds 1\\|2` | unset | Review round count forwarded to the review phase. Review-only \u2014 valid only with `--workflow review-and-implement`. |\n| `--tier cheap\\|basic\\|premium` | unset (difficulty routing) | Coarse model-routing override. Bypasses **only** the per-ticket difficulty/tier lookup (`GET /jira/tickets/{KEY}/model-tier`) and applies this one tier to every ticket; the tier is still mapped to a model through the centralized agent registry and any configured `difficulty_model_tier_overrides`, then validated. It is **not** a raw `--model` alias and never carries an API key or credential. A malformed value fails open to premium routing. Used by the `/review-and-implement` handoff to reuse the review-time tier. |\n| `--terminal terminal\\|iterm` | auto-detect via `$TERM_PROGRAM` | Override the macOS terminal app (honored on macOS only) |\n| `--dry-run` | off | Print intended actions; create no worktrees, open no tabs (any OS) |\n| `--branch KEY=BRANCH` | `feature/<KEY>` | Use a custom branch for that ticket (repeatable) |\n| `--base-branch <BRANCH>` | `main` | Cut new worktrees from `<BRANCH>` and refresh `origin/<BRANCH>` instead of `main` |\n| `--no-refresh-main` | off (the configured base branch is refreshed) | Skip refresh of the configured base branch (default `main`). Historical flag name preserved for backward compatibility \u2014 despite the name, it now skips refresh of whatever `--base-branch` resolves to. |\n| `--max-parallel N` | `3` | Max worktrees created concurrently |\n| `--conductor` | off | Opt into the Conductor system (per-worker `BAPI_CONDUCTOR_*` env + Claude hook injection, a supervisor peer tab, and the `check_messages` message-relay prompt). **Default off** \u2014 a plain run spawns `cd <worktree> && <agent> \'/implement-ticket <KEY>\'`. |\n| `-h`, `--help` | \u2014 | Show usage |\n\nEach `KEY` must match `[A-Z]+-[0-9]+` (e.g., `BAPI-248`). The CLI creates/switches each worktree up front (throttled by `--max-parallel`), then opens one tab/session per successful worktree running the selected agent\'s `\'/implement-ticket <KEY>\'` \u2014 `claude \'/implement-ticket <KEY>\'` by default, or `cursor-agent \'/implement-ticket <KEY>\'` with `--agent cursor-agent`. The `/implement-ticket <KEY>` prompt is unchanged for both agents. To launch Cursor Agent instead of Claude Code:\n\n```\nnpx -y @bridge_gpt/mcp-server start-tickets --agent cursor-agent BAPI-248\n```\n\n**Difficulty-based model routing.** Before launching each agent, the CLI selects an implementation **model tier** from the ticket\'s `difficulty` (1-2 \u2192 cheap, 3-5 \u2192 basic, 6+ \u2192 premium) and injects it as a `--model` flag at the spawn boundary. The Python backend returns only the coarse tier (`GET /jira/tickets/{KEY}/model-tier`, computing + caching difficulty on demand); this CLI alone maps a tier to the agent-specific alias (`claude`: `haiku`/`sonnet`/`opus`; `cursor-agent`: version-suffixed strings validated against `cursor-agent --list-models`). It is gated per repo by `difficulty_model_routing_enabled` (default **ON**) with an optional `difficulty_model_tier_overrides` JSON map (tier \u2192 alias). Routing is **fail-open**: missing credentials, an evaluation failure/timeout, a backend `fallback`, an invalid/unavailable alias, an unadvertised Cursor model, or an agent without `--model` support all default to the **premium** (Opus) model \u2014 and when even the premium alias cannot be resolved/validated for the agent, `--model` is omitted (the agent uses its default) \u2014 each surfaced as a per-ticket warning rather than failing the spawn. `--dry-run` does **not** create worktrees or open tabs, but it **does** resolve routing read-only to preview the `--model` each tab would use.\n\n**Coarse `--tier` override.** Passing `--tier cheap|basic|premium` bypasses **only** the per-ticket difficulty lookup above and applies that one tier to every ticket; the tier is still resolved to an alias through the same agent registry + `difficulty_model_tier_overrides` and validated the same way (including the live `cursor-agent --list-models` check). It is never treated as a raw `--model` alias, and no API key or credential belongs in the spawned command (the CLI resolves credentials itself via `resolveBapiCredentials`). A malformed/unrecognized `--tier` value is **fail-open**: the CLI logs one concise warning and routes every ticket on the premium (Opus) fallback rather than aborting. `/review-and-implement` uses this flag to hand its review-time tier snapshot to the fresh implementation session.\n\n**Conductor observability (opt-in via `--conductor`, BAPI-394).** Conductor is **off by default**. With `--conductor`, a run mints a conductor `run_id`, emits events into the local ledger (`~/.config/bridge/events.db`), opens a supervisor peer tab, and (for Claude Code) injects a secret-free lifecycle hook into each worktree; it also sets `BRIDGE_MCP_PROFILE=conductor` so workers get the conductor MCP tools. Observability is best-effort \u2014 a conductor failure never blocks a spawn, and `--dry-run` performs no conductor side effects. (Epic-tick dispatch always enables conductor internally.) See **[CONDUCTOR.md](./docs/CONDUCTOR.md)** for the full observability, git-hook, done-gate, and auto-merge reference.\n\n**Cross-platform spawning.** The CLI routes spawning per platform; `--dry-run` previews the platform-correct command form on any OS. An unsupported `process.platform` (not `darwin`/`win32`/`linux`) fails fast with a clear "unsupported platform" message.\n\n- **macOS** \u2014 opens a Terminal.app or iTerm tab via `osascript`.\n- **Windows** \u2014 creates worktrees with **`git-wt`** (Worktrunk\'s winget alias) and opens a tab via **Windows Terminal (`wt.exe new-tab`)**, falling back to **`Start-Process powershell.exe`** when Windows Terminal is absent. Requires **Git for Windows / Git Bash** (Worktrunk runs its `pre-start` / `post-start` hooks via Git Bash). The Worktrunk binary (`git-wt`) and the tab launcher (`wt.exe`) are resolved independently and never conflated.\n- **Linux** \u2014 creates one detached **tmux** session per ticket (pane kept open after the agent exits); attach with `tmux attach -t <session>`. A missing `tmux` produces a clear, actionable error.\n\nPer-OS prerequisites: macOS `wt`, `git`, `osascript`; Windows `git-wt`, Git for Windows / Git Bash, Windows Terminal or PowerShell; Linux `wt`, `git`, `tmux`. Set `BAPI_WORKTRUNK_BIN` to override the Worktrunk executable name/path for nonstandard installs (`doctor` honors it too). The read-only `doctor` subcommand (below) additionally surfaces a missing `uv` \u2014 Worktrunk\'s `pre-start` hook runs `uv`, but live preflight does not check it \u2014 and the selected agent\'s command; run `doctor --agent cursor-agent` to also check `cursor-agent` (it prints `cursor-agent login` as an informational auth reminder).\n\n### `doctor`\n\nThe package also ships a strictly **read-only** `doctor` subcommand \u2014 titled **`bridge doctor \u2014 read-only diagnostics`** \u2014 that diagnoses your whole Bridge install without changing anything:\n\n```\nnpx -y @bridge_gpt/mcp-server doctor [--agent <name>]\n```\n\nThe report always leads with the advisory **`Install status`** section (repo identity, credential resolution, server connectivity, bootstrap-field completeness, integration credentials, indexing state) **before** the `start-tickets` prerequisite diagnostics; the launcher-cache and MCP tool-surface sections follow. `Install status` is read-only GETs only and never affects the exit code.\n\nIt is **read-only**: it never installs anything, modifies your system, adds an npm `postinstall`, spawns a terminal, or starts the MCP server, and there is no `--fix`. For each prerequisite it prints found/missing and, when missing, the exact per-OS install command **as a manual instruction you run yourself**. The checked set is the `start-tickets` preflight prerequisites **plus `uv`** **plus the selected agent\'s command** (`claude` by default, or `cursor-agent` with `--agent cursor-agent`). The Worktrunk binary is probed via the resolved name (honoring `BAPI_WORKTRUNK_BIN`), not a hard-coded one. **Exit code:** `0` when all required prerequisites are present, non-zero when any is missing or the platform is unsupported. A failing `start-tickets` preflight now hints you to run `doctor` for an actionable diagnostics report.\n\n### `setup-epic`\n\nBootstraps an Epic Conductor v2 run in one command \u2014 creates the epic run, stores the plan DAG, and approves it:\n\n```\nnpx -y @bridge_gpt/mcp-server setup-epic --epic-key <KEY> --plan-file <path-to-epic-plan.dag.json>\n```\n\nThe plan sidecar is produced by the `decompose-epic` pipeline step. `setup-epic` validates it locally (unique ticket keys, resolvable dependency references, acyclicity) before sending anything, so a malformed plan fails legibly instead of as a bare HTTP 400. It is **idempotent**: re-running it on an epic that already has a live run reuses that run rather than minting a second one. `--dry-run` validates and previews the calls without mutating anything; `--json` emits a single machine-readable result object.\n\nOnce the plan is approved, the **server-side reconciler** picks the run up within ~30s. To execute claimed jobs on your machine, run `executor`:\n\n```\nnpx -y @bridge_gpt/mcp-server executor --repo <name>\n```\n\n### Conductor (epic & multi-agent orchestration)\n\nConductor is an **opt-in, off-by-default** layer for epic supervision, inter-agent messaging, done-gate evaluation, local git-hook event producers, and conditional auto-merge. Its full reference \u2014 the v2 architecture (server-side reconciler + local executor), `setup-epic`, `conductor install-git-hooks`, the supervisor `done_gate_config` / `auto_merge_enabled` settings, and the observability stream \u2014 lives in **[CONDUCTOR.md](./docs/CONDUCTOR.md)**.\n\n> The v1 `conductor epic-tick` command is **frozen** \u2014 it throws on every invocation. There is nothing to schedule locally, and `conductor doctor` flags any epic-tick schedule left over from an earlier release so you can cancel it.\n\n## Custom Pipelines\n\nYou can create your own pipelines by adding JSON files to `.bridge/pipelines/`. Running `--init` scaffolds this directory with a `README.md` and an example pipeline to get you started.\n\nThe easiest way to write a custom pipeline is to describe what you want to automate to your AI coding agent and have it draft the JSON for you. The schema is straightforward, and agents like Claude Code understand it well \u2014 just describe the steps you want, and the agent will produce a working pipeline file.\n\n**What you can build:**\n\n- Any sequence of Bridge MCP tool calls and free-form agent tasks\n- Parameterized workflows using variables (e.g., `{ticket_key}`)\n- Approval gates that pause for user confirmation before sensitive steps\n- Per-step error handling \u2014 halt immediately or warn and continue\n\n**Ideas for custom pipelines:**\n\n- A standup pipeline that fetches your open tickets and summarizes their status\n- A ticket triage pipeline that runs critiques on a batch of new tickets\n- A pre-merge checklist that runs tests, checks linting, and posts a summary comment\n\n**Step types:**\n\n| Type | What it does |\n|---|---|\n| `mcp_call` | Calls an MCP tool with the given params |\n| `agent_task` | Gives the AI a free-form instruction (inline or from a file in `.bridge/instructions/`) |\n\nVariables are declared in the `variables` array and referenced as `{variable_name}` in params and instructions. Each step supports `on_error: "halt"` (default) or `"warn_and_continue"`, and `requires_approval: true` to pause before execution.\n\n**System variables:**\n\nTwo variables are automatically available in every pipeline without declaring them:\n\n| Variable | What it does |\n|---|---|\n| `{provider}` | Routes AI generation to a specific LLM provider (`openai`, `anthropic`, or `gemini`). Pass it through to any `request_*` tool param to control which provider handles that step. Omit it (or leave it empty) to use the project default. |\n| `{second_opinion}` | Runs AI generation through a different provider than the default, acting as a cross-check. Set to `"auto"` to let Bridge pick the second provider automatically. When set, it takes precedence over `{provider}`. Use this when you want two independent AI perspectives on the same task \u2014 for example, running clarifying questions and a critique twice (once with each provider) produces better results than a single pass. |\n\nSee `.bridge/pipelines/README.md` for the full schema reference.\n\nIf a custom pipeline has the same key as a built-in pipeline, the custom version takes precedence (a warning is logged at startup).\n\n## Environment Variables\n\n| Variable | Required | Default | Description |\n|---|---|---|---|\n| `BAPI_BASE_URL` | Yes | `https://bridgegpt-api.com` | Bridge API base URL |\n| `BAPI_REPO_NAME` | Yes | _(none)_ | Jira project/repository identifier configured in Bridge API |\n| `BAPI_API_KEY` | Yes | _(none)_ | API key obtained from the Bridge API setup UI |\n| `BAPI_PROJECT_ROOT` | No | _(auto-set by --init)_ | Absolute path to project root. Anchors `BAPI_DOCS_DIR` and `BAPI_PIPELINES_DIR` resolution |\n| `BAPI_DOCS_DIR` | No | `docs/tmp` | Local directory for saving plans, critiques, and research reports |\n| `BAPI_PIPELINES_DIR` | No | `.bridge/pipelines` | Directory for user-defined custom pipeline JSON files |\n| `BAPI_WORKTRUNK_BIN` | No | `wt` (`git-wt` on Windows) | Override the Worktrunk executable name/path used by `start-tickets` for nonstandard installs |\n| `BAPI_TMUX_SESSION` | No | `bridge-start-tickets` | Override the tmux session-name prefix used by `start-tickets` on Linux |\n| `BAPI_MCP_UPGRADE_ADVICE_ENABLED` | No | _(enabled)_ | MCP-local opt-out for proactively surfacing upgrade advice in pipeline recipe preambles. Set to `false`/`0`/`no`/`off`/`disabled` to suppress. Disabling it does **not** change the `/jira/ping` response or server-side upgrade computation \u2014 it only gates the recipe-preamble convention |\n| `BAPI_MCP_TOOL_SURFACE_GATING_ENABLED` | No | _(enabled)_ | MCP-local kill switch for **dynamic tool-surface capability gating** (see [Dynamic tool-surface gating](#dynamic-tool-surface-gating-capability-availability)). Default-on; set to `false`/`0`/`no`/`off`/`disabled` to skip the startup probe, the recurring poll, and the custom `tools/list` handler entirely, restoring the SDK\'s previous full profile-derived surface. Fail-open: any probe timeout, unreachable backend, non-2xx, malformed payload, incomplete evaluation, or unsupported schema advertises the full profile |\n| `BAPI_MCP_TOOL_SURFACE_POLL_ENABLED` | No | _(disabled)_ | Opt IN to the recurring tool-surface **poll**. Default-**off**: a session gates once via the startup probe and never re-probes. Set to `true`/`1`/`yes`/`on`/`enabled` to restore the jittered 12\u201318 s heartbeat that pushes `notifications/tools/list_changed` on mid-session capability changes. No effect when gating itself is disabled |\n| `CURSOR_API_KEY` | No | _(none)_ | API key used to authenticate `cursor-agent` when launching `start-tickets`/scheduled runs with `--agent cursor-agent`. Not needed for the default Claude Code agent |\n| `BRIDGE_MCP_PROFILE` | No | `core` | Startup-time tool registration profile \u2014 a **comma-separated** list of groups controlling which tool groups are registered when the server starts. `core` is always implicitly included. Valid groups: `core` (default \u2014 normal coding tools only), `conductor` (+ 8 conductor/event/supervisor tools), `pipeline-authoring` (+ 5 pipeline run/admin tools \u2014 `get_pipeline_recipe` is NOT gated; it stays in `core` because the recipe-driven slash commands depend on it), `sfcc` (+ the 7 heavy SFCC read tools \u2014 see [Salesforce B2C Commerce (SFCC) Tools](#salesforce-b2c-commerce-sfcc-tools); the `sfcc_setup_status`/`check_permissions` diagnostics are always registered regardless), and `full` (shortcut that expands to every group). Example: `sfcc,conductor`. Unknown, blank, or malformed tokens are dropped (falling back to `core`). Dynamic mid-session switching via `tools/list_changed` is unsupported \u2014 groups are resolved once at process startup. Conductor/epic sessions launched via `start-tickets --conductor` automatically inject `BRIDGE_MCP_PROFILE=conductor` at the spawn boundary; a normal `start-tickets` run stays on `core`. |\n\n## Dynamic tool-surface gating (capability availability)\n\nThe **effective advertised tool surface** is the intersection of three things:\n\n1. **Startup profile registration** \u2014 which tool groups `BRIDGE_MCP_PROFILE`\n registered at process start (see above).\n2. **Current SDK-enabled state** \u2014 a tool the server has disabled for another\n reason (e.g. `poll_ci_checks` when `ci_check_config` is unset) stays hidden.\n3. **Backend capability availability** \u2014 the set of tool IDs the backend would\n currently hard-block for this repo, reported by `GET /jira/mcp/tool-surface`.\n\nOn startup the server issues one bounded probe to that endpoint and installs a\ncustom `tools/list` handler that subtracts the backend-blocked IDs (intersected\nwith the locally advertised surface) from what it advertises. That single startup\nprobe is the default: the surface is gated once per session and the server does\nnot re-probe. Installed integrations change rarely and MCP clients re-list on\nreconnect, so a permanent per-session heartbeat \u2014 multiplied across every\nconcurrent worktree/agent session \u2014 was pure request noise against the backend.\nOpt in with `BAPI_MCP_TOOL_SURFACE_POLL_ENABLED=true` to restore the jittered\n12\u201318 s re-probe that emits `notifications/tools/list_changed` whenever the\neffective visible set actually changes, so a connected client converges to the\ncurrent surface mid-session without a reconnect.\n\n**Fail-open by design.** A probe timeout, unreachable backend, non-2xx response,\nmalformed payload, incomplete evaluation, or unsupported schema version all\nadvertise the **full** existing profile baseline \u2014 gating never removes a tool on\na doubtful signal.\n\n**Hidden \u2260 disabled.** A capability-hidden tool remains **registered and\ncallable**, including through in-process pipelines. Hiding affects `tools/list`\nprojection only; it never calls `.disable()` or mutates the SDK `enabled` flag,\nbecause doing so would also block `tools/call` and the in-process dispatch path \u2014\nthe backend remains the authoritative enforcement boundary, returning its own\nrefusal for a stale call rather than a local "disabled" error.\n\n**Kill switch.** Set `BAPI_MCP_TOOL_SURFACE_GATING_ENABLED` to an accepted false\ntoken (`false`/`0`/`no`/`off`/`disabled`) to skip the probe, the poll, and the\ncustom handler entirely, restoring the SDK\'s previous full profile-derived\nsurface.\n\n**Client convergence and the reconnect escape hatch.** Clients that honor\n`notifications/tools/list_changed` converge automatically. A client that does not\nhonor the notification must **reconnect or start a new MCP server session** to\nobserve the current surface; no project MCP configuration change is required.\n\n**Diagnosing the surface.** Run `doctor` (its advisory "MCP tool surface"\nsection reports the kill-switch state, reachability, decision reason, blocked\ncount, physical tool IDs, and catalog revision via a single read-only GET), or\nread the server\'s stderr gating decision lines (`tool-surface gating: reason=\u2026\nhidden=\u2026 revision=\u2026 hidden_tools=[\u2026]`).\n\n## Worktree credentials and the `mcp-invoke` shim\n\nWhen `start-tickets` creates a git worktree, it provisions a Bridge API MCP\nregistration into that worktree so Claude Code (`.mcp.json`) and Cursor\n(`.cursor/mcp.json`) can reach the server immediately. These registrations are\n**secret-free**: they contain no `env` block and no API key. Instead they point\nat an internal subcommand of the published single CLI bin, `mcp-invoke`:\n\n```bash\nnpx -y @bridge_gpt/mcp-server@<VERSION> mcp-invoke --target bapi --project-root <ABS_WORKTREE_PATH>\n```\n\n`mcp-invoke` is not a separate binary \u2014 it is a positional subcommand of\n`bridge-api-mcp-server`. It resolves the repo identity from the absolute\n`--project-root` (the committed `.bridge/config` manifest, falling back to the\ngit common dir), resolves credentials from the home-directory credential store,\nand then spawns the real MCP server with that environment.\n\n### Credential store\n\nCredentials live outside the repository, keyed by `bapi:<repo_name>`:\n\n```json\n{\n "bapi:<repo_name>": {\n "BAPI_API_KEY": "..."\n }\n}\n```\n\nResolution order:\n\n1. `BAPI_API_KEY` in the parent environment (overrides the file entirely).\n2. `$XDG_CONFIG_HOME/bridge/credentials.json`, else `~/.config/bridge/credentials.json`.\n3. `~/.bridge/credentials.json` (only when the primary path is absent).\n\nOn POSIX systems, lock the file down so only you can read it:\n\n```bash\nchmod 600 ~/.config/bridge/credentials.json\n```\n\n`mcp-invoke` warns (but continues) if the file is group/world-readable, and it\nnever creates or initializes the credential file for you.\n\n### Populating the credential store\n\nThe same store also backs the shell-spawned `start-tickets` CLI (its\ndifficulty\u2192model routing runs in a Bash process that cannot see an `env` block in\n`.mcp.json` / `.cursor/mcp.json`), so the store must hold the key for routing to\nwork. Two supported paths write it for you:\n\n- **Install-time upsert.** `/install-bridge`\'s final stage persists the validated\n routing credential into `~/.config/bridge/credentials.json` (target\n `bapi:<repo>`) via the `persist_routing_credential` MCP tool \u2014 the tool resolves\n the key inside the MCP process and writes the store, so no secret crosses the\n wire.\n- **One-shot migration.** If a key currently lives only in `.mcp.json` /\n `.cursor/mcp.json`, migrate it into the user-scoped store with the consent-gated\n command (a compatibility aid, not a live fallback):\n\n ```bash\n npx -y @bridge_gpt/mcp-server credentials migrate-agent-config [--write-credentials]\n ```\n\n Run it without `--write-credentials` to preview; add the flag to write the store.\n\n## Reference\n\nThe full surface, for when you need the complete enumeration. Day-to-day, use [Usage Documentation](#usage-documentation) instead \u2014 you don\'t call MCP tools directly; you ask your AI assistant to perform a task, or compose tools into a pipeline.\n\n### MCP tools\n\nThe server exposes **60 documented tools** (enumerated below). What\'s actually registered in a session depends on `BRIDGE_MCP_PROFILE`: the default `core` profile loads a trimmed subset, and the conductor/pipeline-authoring/SFCC tools are added only under their respective profiles (see [Environment Variables](#environment-variables)). Async AI tools follow a request/get pattern: call the `request_*` tool to kick off generation, then the matching `get_*` tool to retrieve the result (or pass `wait_for_result: true` to poll automatically).\n\n- **Connectivity & identity** \u2014 `ping`, `get_my_role`, `get_docs_dir`\n- **Team & access** \u2014 `invite_member` (admin-only; mints a scoped access key for a teammate on an already-configured project \u2014 the plaintext key is shown exactly once)\n- **Jira tickets** \u2014 `get_tickets`, `get_ticket`, `create_ticket`, `update_ticket_description`, `add_comment`, `get_comments`\n- **Attachments** \u2014 `attachment` (operations: `upload`, `download`, `list`)\n- **AI generation (request/get)** \u2014 `request_plan_generation`/`get_plan`, `request_architecture`/`get_architecture`, `create_doc`/`get_doc` (design docs by `doc_type`: tdd/fsd/prd), `request_prd`/`get_prd`, `request_clarifying_questions`/`get_clarifying_questions`, `request_ticket_critique`/`get_ticket_critique`, `request_ticket_review`, `request_reimplement_context`/`get_reimplement_context`, `request_council`/`get_council`, `request_deep_research`/`get_deep_research`\n- **Other AI** \u2014 `second_opinion`, `generate_image`, `generate_decision_page`, `visual_diff` (deterministic pixel diff of a rendered URL vs a design comp)\n- **Ticket lifecycle** \u2014 `track_ticket`, `update_ticket_state`, `get_ticket_state`\n- **Jira status** \u2014 `get_jira_transitions`, `update_jira_status`, `resolve_target_status`\n- **Repository & CI** \u2014 `parse_repository`, `get_parse_status`, `regenerate_directory_map`, `create_pull_request`, `resolve_ci_checks`, `poll_ci_checks`\n- **Pipelines & automation** \u2014 `list_pipelines`, `get_pipeline_recipe`, `run_pipeline`, `resume_pipeline`, `list_pipeline_runs`, `delete_pipeline_run`, `run_full_automation`, `resume_full_automation`\n- **Config** \u2014 `get_project_standards`, `config_field` (operations: `get`, `update`, `list`)\n\n### Bundled pipelines\n\nPipelines are declarative, multi-step workflows your AI agent executes step-by-step \u2014 each a JSON recipe chaining MCP tool calls and free-form agent tasks, with variable substitution, per-step error handling, and optional approval gates. You can also write your own (see [Custom Pipelines](#custom-pipelines)).\n\n| Pipeline | Description | Invoke with |\n|---|---|---|\n| `implement-ticket` | Generate a plan, execute the implementation, commit, open a PR, and monitor CI | `/implement-ticket PROJ-123` |\n| `review-ticket` | Full ticket quality review: clarifying questions + critique plus an automatic second-opinion pass, then evaluation and decision capture. The backend owns round orchestration; pass `--rounds=1` for a single-pass review or `--rounds=2` to force the full second-opinion review, or omit `--rounds` to let the backend decide adaptively. | `/review-ticket PROJ-123` |\n| `idea-to-ticket` | Turn an idea into a Jira Task/Spike (or Epic + children) with research, dedup, and critique | `/idea-to-ticket "<idea>"` |\n| `plan-epic` | Decompose an epic into sub-tasks with a structured exploration doc for each | `/plan-epic "<epic>"` |\n| `full-automation` | Chain: idea \u2192 ticket(s) \u2192 review each \u2192 spawn worktrees to implement | `/full-automation "<idea>"` |\n| `pr-ticket` | Commit changes and open a pull request | `/create-pr PROJ-123` |\n| `check-ci-ticket` | Commit, open a PR, then monitor CI checks until they pass or fail | `/check-ci PROJ-123` |\n| `learn-repository` | Analyze codebase architecture, testing, review, and documentation standards, then upload to Bridge | `/learn-repository` |\n\n### Pipeline response envelope\n\n`run_pipeline`, `resume_pipeline`, and `list_pipeline_runs` share a unified envelope keyed on `status`:\n\n- `completed` \u2014 terminal success; `results` holds per-step output.\n- `needs_agent_task` \u2014 the orchestrator paused. Read `instruction`, perform the task, then call `resume_pipeline` with `pipeline_run_id` and a string `agent_result`.\n- `failed` \u2014 terminal failure. `error_code` is one of `VALIDATION`, `NOT_FOUND`, `EXPIRED`, `REPO_MISMATCH`, `TOOL_ERROR`.\n\nPaused runs auto-expire after an idle TTL (default 24 hours; override with `ttl_seconds`). The TTL is reset on every state transition. List output is metadata-only \u2014 it never includes resolved recipes, params, instructions, results, or agent outputs.\n';init_version_generated();import{writeFile,mkdir,readFile}from"fs/promises";import path from"path";import os from"os";var CACHE_TTL=864e5,FETCH_TIMEOUT=3e3,REGISTRY_URL="https://registry.npmjs.org/@bridge_gpt/mcp-server/latest";function getCachePath(){return path.join(os.homedir(),".config","@bridge_gpt","mcp-server","update-check.json")}function isNewerVersion(current,latest){let c=current.split(".").map(Number),l=latest.split(".").map(Number);for(let i=0;i<3;i++){if((l[i]??0)>(c[i]??0))return!0;if((l[i]??0)<(c[i]??0))return!1}return!1}async function checkForUpdate(){try{let cachePath=getCachePath(),cacheDir=path.dirname(cachePath),latestVersion=null;try{let raw=await readFile(cachePath,"utf-8"),cache=JSON.parse(raw);cache&&typeof cache.lastCheck=="number"&&typeof cache.latestVersion=="string"&&Date.now()-cache.lastCheck<CACHE_TTL&&(latestVersion=cache.latestVersion)}catch{}if(!latestVersion){let controller=new AbortController,timeout=setTimeout(()=>controller.abort(),FETCH_TIMEOUT);try{let data=await(await fetch(REGISTRY_URL,{signal:controller.signal})).json();data.version&&(latestVersion=data.version,await mkdir(cacheDir,{recursive:!0}),await writeFile(cachePath,JSON.stringify({lastCheck:Date.now(),latestVersion}),"utf-8"))}finally{clearTimeout(timeout)}}return latestVersion?{updateAvailable:isNewerVersion(VERSION,latestVersion),currentVersion:VERSION,latestVersion}:null}catch{return null}}import{readdir,readFile as readFile2}from"fs/promises";import path2 from"path";function validatePipelineSchema(json){let errors=[];if(typeof json!="object"||json===null||Array.isArray(json))return{valid:!1,errors:["Pipeline must be a JSON object."]};let obj=json;if((typeof obj.name!="string"||obj.name.trim()==="")&&errors.push('Missing or empty required field "name" (string).'),!Array.isArray(obj.steps)||obj.steps.length===0)return errors.push('Missing or empty required field "steps" (non-empty array).'),{valid:!1,errors};obj.description!==void 0&&typeof obj.description!="string"&&errors.push('"description" must be a string if provided.'),obj.variables!==void 0&&(!Array.isArray(obj.variables)||!obj.variables.every(v=>typeof v=="string"))&&errors.push('"variables" must be an array of strings if provided.');let steps=obj.steps;for(let i=0;i<steps.length;i++){let prefix=`steps[${i}]`,step=steps[i];if(typeof step!="object"||step===null||Array.isArray(step)){errors.push(`${prefix}: must be an object.`);continue}let s=step;if((typeof s.description!="string"||s.description.trim()==="")&&errors.push(`${prefix}: missing or empty "description" (string).`),s.on_error!==void 0&&s.on_error!=="halt"&&s.on_error!=="warn_and_continue"&&errors.push(`${prefix}: "on_error" must be "halt" or "warn_and_continue".`),s.requires_approval!==void 0&&typeof s.requires_approval!="boolean"&&errors.push(`${prefix}: "requires_approval" must be a boolean if provided.`),s.id!==void 0&&(typeof s.id!="string"||s.id.trim().length===0)&&errors.push(`${prefix}."id" must be a non-empty string when provided`),s.type==="mcp_call")(typeof s.tool!="string"||s.tool.trim()==="")&&errors.push(`${prefix}: mcp_call step requires "tool" (string).`),(typeof s.params!="object"||s.params===null||Array.isArray(s.params))&&errors.push(`${prefix}: mcp_call step requires "params" (object).`);else if(s.type==="agent_task"){let hasInstruction=typeof s.instruction=="string"&&s.instruction.trim()!=="",hasInstructionFile=typeof s.instruction_file=="string"&&s.instruction_file.trim()!=="";hasInstruction&&hasInstructionFile?errors.push(`${prefix}: agent_task must have exactly one of "instruction" or "instruction_file", not both.`):!hasInstruction&&!hasInstructionFile&&errors.push(`${prefix}: agent_task must have exactly one of "instruction" or "instruction_file".`)}else errors.push(`${prefix}: "type" must be "mcp_call" or "agent_task", got "${String(s.type)}".`)}return{valid:errors.length===0,errors}}function hasTerminalReturnSection(markdown){if(typeof markdown!="string"||markdown.length===0)return!1;let h2Pattern=/^##\s+(.+?)\s*$/gm,match,lastHeading=null;for(;(match=h2Pattern.exec(markdown))!==null;)lastHeading=match[1].trim();return lastHeading===null?!1:/^return\b/i.test(lastHeading)}var variablePattern=()=>/\{([a-zA-Z_][a-zA-Z0-9_]*)}/g;function substituteVariables(template,variables){return template.replace(variablePattern(),(full,name)=>name in variables?variables[name]:full)}function substituteDeep(value,variables){if(typeof value=="string")return substituteVariables(value,variables);if(Array.isArray(value))return value.map(item=>substituteDeep(item,variables));if(typeof value=="object"&&value!==null){let result={};for(let[k,v]of Object.entries(value))result[k]=substituteDeep(v,variables);return result}return value}function substituteInParams(params,variables){return substituteDeep(params,variables)}var UPGRADE_ADVICE_SURFACING_INSTRUCTIONS=" Upgrade advice: when any `ping` MCP call in this run returns a non-empty second text content item, relay that server-provided text to the user verbatim \u2014 do not invent, prefix, suffix, summarize, or paraphrase it. Surface it at most once per pipeline run or session, including any chained sub-pipelines. Stay completely silent when the second content item is absent or empty. A ping failure, a non-OK ping response, missing advice, or empty advice is fail-open: it must never block, pause, or crash the run, and you must never synthesize advice of your own.";function resolveRecipe(pipeline,instructions,variables,skipSteps,autoApprove,options){let declared=pipeline.variables??[],missing=declared.filter(v=>!(v in variables));if(missing.length>0)throw new Error(`Missing required variable(s): ${missing.join(", ")}. Pipeline "${pipeline.name}" declares: [${declared.join(", ")}].`);let skip=new Set(skipSteps??[]),resolvedSteps=[],stepIndex=1;for(let step of pipeline.steps){let stepId=typeof step.id=="string"&&step.id.trim().length>0?step.id:void 0,skipKey=stepId??(step.type==="mcp_call"?step.tool:step.description);if(skip.has(skipKey))continue;let declaredApproval=step.requires_approval??!1,effectiveApproval=declaredApproval&&!autoApprove,isPingStep=step.type==="mcp_call"&&step.tool==="ping",base={step:stepIndex++,type:step.type,description:substituteVariables(step.description,variables),on_error:isPingStep?"warn_and_continue":step.on_error??"halt",requires_approval:effectiveApproval};if(declaredApproval!==effectiveApproval&&(base.requires_approval_declared=declaredApproval),stepId!==void 0&&(base.id=stepId),step.type==="mcp_call")base.tool=step.tool,base.params=substituteInParams(step.params,variables);else{let rawInstruction;if(step.instruction_file){let content=instructions[step.instruction_file];if(content===void 0)throw new Error(`Instruction file "${step.instruction_file}" not found in bundled instructions.`);rawInstruction=content}else rawInstruction=step.instruction;base.instruction=substituteVariables(rawInstruction,variables)}resolvedSteps.push(base)}let baseInstructions=`IMPORTANT: Execute every step below in exact sequential order. For mcp_call steps, call the specified tool with the provided params. For agent_task steps, follow the instruction text using any tools it specifies. If requires_approval is true, pause before executing. For agent_task steps, the instruction file's own approval format is authoritative \u2014 follow it verbatim and do not substitute your own short confirmation prompt. For mcp_call steps with no instruction file, present the resolved params as bullet points and ask for approval. For on_error "halt", stop the pipeline immediately on failure. For on_error "warn_and_continue", log a warning and proceed. Do not skip steps, reorder them, or substitute your own tool calls.`,upgradeAdviceConvention=options?.includeUpgradeAdviceSurfacing!==!1?UPGRADE_ADVICE_SURFACING_INSTRUCTIONS:"",autoApproveSuffix=autoApprove?" Auto-approve mode is ACTIVE: every approval gate has been pre-approved by the user via the auto_approve flag \u2014 proceed without pausing for confirmation, applying the default branching, file-staging, and recommendation choices documented in each instruction file's auto-approve branch.":"";return{pipeline:pipeline.name,description:pipeline.description??"",total_steps:resolvedSteps.length,agent_instructions:baseInstructions+upgradeAdviceConvention+autoApproveSuffix,auto_approve:!!autoApprove,steps:resolvedSteps}}async function loadCustomPipelines(pipelinesDir,instructionsDir,bundledInstructions){let mergedInstructions={...bundledInstructions},userPipelines={},userPipelineKeys2=new Set;try{let instrFiles=await readdir(instructionsDir);for(let file of instrFiles)if(file.endsWith(".md"))try{let content=await readFile2(path2.join(instructionsDir,file),"utf-8");file in bundledInstructions&&console.error(`Warning: custom instruction "${file}" overrides a bundled instruction.`),mergedInstructions[file]=content}catch(readErr){let msg=readErr instanceof Error?readErr.message:String(readErr);console.error(`Warning: skipping instruction "${file}" \u2014 failed to read: ${msg}`)}}catch(err){err.code!=="ENOENT"&&console.error(`Warning: could not read instructions directory "${instructionsDir}": ${err.message}`)}try{let pipelineFiles=await readdir(pipelinesDir);for(let file of pipelineFiles){if(!file.endsWith(".json"))continue;let parsed;try{let raw=await readFile2(path2.join(pipelinesDir,file),"utf-8");parsed=JSON.parse(raw)}catch(parseErr){let msg=parseErr instanceof Error?parseErr.message:String(parseErr);console.error(`Warning: skipping "${file}" \u2014 failed to parse: ${msg}`);continue}let{valid,errors}=validatePipelineSchema(parsed);if(!valid){console.error(`Warning: skipping "${file}" \u2014 validation errors:
|
|
3920
3920
|
${errors.join(`
|
|
3921
3921
|
`)}`);continue}let pipeline=parsed,key=file.replace(/\.json$/,""),hasInvalidRef=!1;for(let step of pipeline.steps)if(step.type==="agent_task"&&step.instruction_file){let content=mergedInstructions[step.instruction_file];if(content===void 0){console.error(`Warning: skipping "${file}" \u2014 instruction_file "${step.instruction_file}" not found.`),hasInvalidRef=!0;break}if(!hasTerminalReturnSection(content)){console.error(`Warning: skipping "${file}" \u2014 instruction_file "${step.instruction_file}" is missing a terminal "## Return" section (required by BAPI-275 agent_result contract).`),hasInvalidRef=!0;break}}hasInvalidRef||(userPipelines[key]=pipeline,userPipelineKeys2.add(key))}}catch(err){return err.code!=="ENOENT"&&console.error(`Warning: could not read pipelines directory "${pipelinesDir}": ${err.message}`),{pipelines:userPipelines,instructions:mergedInstructions,userPipelineKeys:userPipelineKeys2}}return userPipelineKeys2.size>0&&console.error(`Loaded ${userPipelineKeys2.size} user pipeline(s) from ${pipelinesDir}`),{pipelines:userPipelines,instructions:mergedInstructions,userPipelineKeys:userPipelineKeys2}}init_commands_generated();import{writeFile as writeFile2,mkdir as mkdir2,readFile as readFile3,stat}from"fs/promises";import path5 from"path";var AGENTS={"jira-ticket-writer":{frontmatter:{name:"jira-ticket-writer",description:`Use this agent when the user describes a problem, feature request, bug, or improvement and wants a structured Jira ticket written as a markdown file. This agent performs deep codebase research before writing the ticket to ensure requirements reference existing code, patterns, and extension points.\\n\\nExamples:\\n\\n<example>\\nContext: The user describes a feature they want to add to the project.\\nuser: "We need to add rate limiting to our LLM integration so we don't exceed provider quotas"\\nassistant: "I'll use the Task tool to launch the jira-ticket-writer agent to research the codebase and create a structured Jira ticket for this feature."\\n<commentary>\\nSince the user is describing a problem/feature that needs a Jira ticket, use the jira-ticket-writer agent to research the codebase thoroughly and produce a well-structured ticket with code references.\\n</commentary>\\n</example>\\n\\n<example>\\nContext: The user wants a ticket for a bug fix.\\nuser: "There's an issue where custom object iterators aren't being closed properly in some of our job scripts, can you write a ticket for that?"\\nassistant: "I'll use the Task tool to launch the jira-ticket-writer agent to investigate which job scripts have this issue and create a detailed Jira ticket."\\n<commentary>\\nThe user explicitly wants a Jira ticket written. Use the jira-ticket-writer agent so it can scan the relevant job scripts, identify the specific files and functions affected, and produce a ticket with precise code references.\\n</commentary>\\n</example>\\n\\n<example>\\nContext: The user describes an improvement they want.\\nuser: "We should add support for a new LLM provider - Mistral - to our integration cartridge"\\nassistant: "Let me use the Task tool to launch the jira-ticket-writer agent to research the existing LLM integration architecture and write a comprehensive Jira ticket for adding Mistral support."\\n<commentary>\\nThe user wants a new feature added. The jira-ticket-writer agent will research the existing LLM client architecture, provider patterns, service definitions, and normalization helpers to write a ticket that references all the specific files and patterns that need to be extended.\\n</commentary>\\n</example>`,model:"opus",color:"blue"},body:`
|
|
3922
3922
|
You are an elite software engineering project manager and technical analyst with deep expertise in codebase archaeology and Jira ticket crafting. You excel at understanding complex codebases, identifying relevant existing code, and translating problem descriptions into precisely-scoped, actionable Jira tickets that engineers can pick up and execute with minimal ambiguity.
|
|
@@ -4713,14 +4713,15 @@ Agents: scaffolded ${agentTotal} agent${agentTotal===1?"":"s"}`),agentWritten.si
|
|
|
4713
4713
|
`;try{await stat(readmePath),console.log(` ${path5.relative(cwd,readmePath)} (skipped \u2014 already exists)`)}catch{await writeFile2(readmePath,readmeContent,"utf-8"),console.log(` ${path5.relative(cwd,readmePath)} (written)`)}try{await stat(examplePath),console.log(` ${path5.relative(cwd,examplePath)} (skipped \u2014 already exists)`)}catch{await writeFile2(examplePath,exampleContent,"utf-8"),console.log(` ${path5.relative(cwd,examplePath)} (written)`)}console.log(` ${path5.relative(cwd,instrDir)}/ (ensured)`);let bridgeConfigPath2=path5.join(cwd,".bridge","config"),bridgeConfigExists=!1;try{await stat(bridgeConfigPath2),bridgeConfigExists=!0}catch{}bridgeConfigExists?console.log(`
|
|
4714
4714
|
.bridge/config: skipped \u2014 already exists`):(await mkdir2(path5.dirname(bridgeConfigPath2),{recursive:!0}),await writeFile2(bridgeConfigPath2,buildBridgeConfigManifest(chooseScaffoldRepoName(cwd)),"utf-8"),console.log(`
|
|
4715
4715
|
.bridge/config: written`)),console.log(" Credentials are resolved at runtime from BAPI_API_KEY or ~/.config/bridge/credentials.json (no secrets are written to .bridge/config)."),anyCreatedOrAdded&&console.log(`
|
|
4716
|
-
Set BAPI_REPO_NAME in your config files. Do NOT put BAPI_API_KEY in the generated MCP config \u2014 supply it via the BAPI_API_KEY environment variable, or store it under "bapi:<repo_name>" in ~/.config/bridge/credentials.json. Get your values from the Bridge API setup UI at https://bridgegpt-api.com`)}init_start_tickets();init_start_tickets();init_review_tickets();init_start_tickets();init_version_generated();import{readFile as readFile6,stat as stat4}from"fs/promises";import{spawn}from"child_process";import os5 from"os";import path19 from"path";init_credential_store();import path18 from"path";var DEFAULT_BASE_URL="https://bridgegpt-api.com"
|
|
4717
|
-
`)}var OFF_TOKENS=new Set(["false","0","no","off","disabled"]),ON_TOKENS=new Set(["true","1","yes","on","enabled"]);function parseDefaultOnEnvFlag(value){if(value===void 0)return!0;let normalized=value.trim().toLowerCase();return normalized===""?!0:!OFF_TOKENS.has(normalized)}function parseDefaultOffEnvFlag(value){if(value===void 0)return!1;let normalized=value.trim().toLowerCase();return normalized===""?!1:ON_TOKENS.has(normalized)}function createBridgeApiUrls(baseUrl){let trimmedBase=baseUrl.replace(/\/+$/,""),buildUrl2=path39=>`${trimmedBase}/jira${path39}`;return{buildUrl:buildUrl2,buildApiUrl:path39=>`${trimmedBase}${path39}`,buildGetUrl:(path39,params)=>{let url=new URL(buildUrl2(path39));for(let[key,value]of Object.entries(params))url.searchParams.set(key,value);return url.toString()}}}import{getMethodLiteral}from"@modelcontextprotocol/sdk/server/zod-json-schema-compat.js";var RECOGNIZED_TOOL_SURFACE_SCHEMA_VERSIONS=new Set([1]);var TOOL_SURFACE_PROBE_DEADLINE_MS=2500,TOOL_SURFACE_POLL_MIN_MS=12e3,TOOL_SURFACE_POLL_MAX_MS=18e3;function timeoutResult(){return{reason:"timeout",blockedTools:new Set}}function malformedResult(subtype){return{reason:"malformed",subtype,blockedTools:new Set}}function validateToolSurfacePayload(body){if(body===null||typeof body!="object"||Array.isArray(body))return malformedResult("invalid-shape");let p=body;if(typeof p.schema_version!="number"||!Number.isInteger(p.schema_version))return malformedResult("invalid-shape");if(!RECOGNIZED_TOOL_SURFACE_SCHEMA_VERSIONS.has(p.schema_version))return malformedResult("unsupported-schema");if(typeof p.complete!="boolean"||typeof p.evaluated_tool_count!="number"||!Number.isInteger(p.evaluated_tool_count)||p.evaluated_tool_count<0||typeof p.catalog_revision!="string"||!Array.isArray(p.blocked_tools)||!p.blocked_tools.every(t=>typeof t=="string"))return malformedResult("invalid-shape");if(!p.complete)return malformedResult("incomplete");if(p.catalog_revision.length===0)return malformedResult("invalid-shape");let blockedTools=new Set(p.blocked_tools);return{reason:"blocked",catalogRevision:p.catalog_revision,evaluatedToolCount:p.evaluated_tool_count,blockedTools}}async function probeToolSurface(options){let deadlineMs=options.deadlineMs??TOOL_SURFACE_PROBE_DEADLINE_MS,controller=new AbortController,onLifecycleAbort=()=>controller.abort();options.abortSignal&&(options.abortSignal.aborted?controller.abort():options.abortSignal.addEventListener("abort",onLifecycleAbort,{once:!0}));let timer,deadlinePromise=new Promise(resolve2=>{timer=setTimeout(()=>{controller.abort(),resolve2(timeoutResult())},deadlineMs)}),abortPromise=new Promise(resolve2=>{if(controller.signal.aborted){resolve2(timeoutResult());return}controller.signal.addEventListener("abort",()=>resolve2(timeoutResult()),{once:!0})}),workPromise=(async()=>{try{let headers=await options.resolveHeaders();if(controller.signal.aborted)return timeoutResult();let resp=await options.fetchFn(options.url,{method:"GET",headers,signal:controller.signal});if(!resp.ok)return malformedResult("non-2xx");let parsed;try{parsed=await resp.json()}catch{return controller.signal.aborted?timeoutResult():malformedResult("invalid-json")}return validateToolSurfacePayload(parsed)}catch{return controller.signal.aborted?timeoutResult():malformedResult("network")}})();try{return await Promise.race([workPromise,deadlinePromise,abortPromise])}finally{timer&&clearTimeout(timer),options.abortSignal&&options.abortSignal.removeEventListener("abort",onLifecycleAbort)}}var defaultScheduler={setTimeout:(callback,ms)=>setTimeout(callback,ms),clearTimeout:handle=>clearTimeout(handle),random:()=>Math.random()};function logDecision(logger,result,hiddenCount,hiddenNames){let revision=result.reason==="blocked"?result.catalogRevision:"n/a",subtype=result.reason==="malformed"?result.subtype:"n/a";logger(`tool-surface gating: reason=${result.reason} subtype=${subtype} hidden=${hiddenCount} revision=${revision} hidden_tools=[${hiddenNames.join(", ")}]`)}function createToolSurfaceGate(options){let{startupProbe,advertised,originalListHandler,freshProbe,notify,logger,lifecycleController}=options,scheduler=options.scheduler??defaultScheduler,advertisedNames=new Set(advertised.map(r=>r.name)),hiddenNames=new Set,lastServedVisible=null,catalogRevision=null,startupApplied=!1,timer,closed=!1;function deriveHidden(result){if(result.reason!=="blocked"||result.blockedTools.size===0)return new Set;let hidden=new Set;for(let id of result.blockedTools)advertisedNames.has(id)&&hidden.add(id);return hidden}function deriveVisible(hidden){let visible=new Set;for(let reg of advertised)reg.isEnabled()&&(hidden.has(reg.name)||visible.add(reg.name));return visible}function applyDecision(result){let nextHidden=deriveHidden(result);hiddenNames=nextHidden,logDecision(logger,result,nextHidden.size,Array.from(nextHidden)),result.reason==="blocked"&&result.catalogRevision!==catalogRevision&&(catalogRevision!==null&&logger(`tool-surface gating: catalog_revision ${catalogRevision} -> ${result.catalogRevision}`),catalogRevision=result.catalogRevision)}function projectList(original){let tools=original.tools.filter(tool=>!hiddenNames.has(tool.name));return{...original,tools}}let handleList=async(request,extra)=>{let startupResult=await startupProbe;startupApplied||(startupApplied=!0,applyDecision(startupResult));let original=await originalListHandler(request,extra),projected=projectList(original);return lastServedVisible=new Set(projected.tools.map(t=>t.name)),projected};async function pollOnce(){let result;try{result=await freshProbe()}catch{result=timeoutResult()}if(closed)return;let previousVisibleServed=lastServedVisible;applyDecision(result);let nextVisible=deriveVisible(hiddenNames);if(previousVisibleServed!==null&&!setsEqual(previousVisibleServed,nextVisible)){lastServedVisible=nextVisible;try{notify()}catch{logger("tool-surface gating: notification failed (suppressed)")}}}function scheduleNext(){if(closed)return;let span=TOOL_SURFACE_POLL_MAX_MS-TOOL_SURFACE_POLL_MIN_MS,delay=Math.round(TOOL_SURFACE_POLL_MIN_MS+scheduler.random()*span);timer=scheduler.setTimeout(()=>{pollOnce().finally(()=>{scheduleNext()})},delay),timer&&typeof timer.unref=="function"&&timer.unref()}function startPolling(){closed||scheduleNext()}function close(){closed||(closed=!0,timer&&(scheduler.clearTimeout(timer),timer=void 0),lifecycleController.signal.aborted||lifecycleController.abort())}return{handleList,startPolling,close}}function setsEqual(a,b){if(a.size!==b.size)return!1;for(let v of a)if(!b.has(v))return!1;return!0}var COMPAT_ERROR="tool-surface gating: incompatible MCP SDK \u2014 the tools/list handler could not be resolved for override.";function installToolSurfaceListOverride(protocolServer,listSchema,customHandler){let method;try{method=getMethodLiteral(listSchema)}catch{throw new Error(COMPAT_ERROR)}if(method!=="tools/list")throw new Error(COMPAT_ERROR);let handlers=protocolServer?._requestHandlers;if(!handlers||typeof handlers.get!="function")throw new Error(COMPAT_ERROR);let original=handlers.get(method);if(typeof original!="function")throw new Error(COMPAT_ERROR);return protocolServer.setRequestHandler(listSchema,customHandler),original}init_credential_store();init_agent_registry();init_start_tickets_prereqs();init_mcp_profile();function getDoctorUsage(){return["Usage:"," npx -y @bridge_gpt/mcp-server doctor [--agent <name>]",""
|
|
4718
|
-
`)}function parseDoctorArgs(argv){if(argv.includes("-h")||argv.includes("--help"))return{status:"help",usage:getDoctorUsage()};let agentName=DEFAULT_AGENT_NAME;for(let i=0;i<argv.length;i++){let arg=argv[i];if(arg==="--fix"||arg.startsWith("--fix="))return{status:"error",message:"--fix is unsupported: doctor is strictly read-only and never installs or modifies anything. Run the printed install commands manually."};if(arg==="--agent"||arg.startsWith("--agent=")){let value;if(arg.startsWith("--agent="))value=arg.slice(8);else{if(i+1>=argv.length)return{status:"error",message:"--agent requires a value (an agent name)."};i+=1,value=argv[i]}if(!isAgentName(value))return{status:"error",message:`Invalid --agent value: '${value}' (allowed agents: ${formatValidAgentNames()}).`};agentName=value;continue}return arg.startsWith("-")?{status:"error",message:`Unknown flag: ${arg}`}:{status:"error",message:`Unexpected positional argument: '${arg}'. doctor does not accept positional arguments.`}}return{status:"ok",options:{agentName}}}async function collectDoctorResults(deps,agentName){let agent=resolveAgentSpec(agentName)??resolveAgentSpec(DEFAULT_AGENT_NAME),descriptorsResult=getDoctorPrereqDescriptors(deps.platform,deps.env,agent);if(!descriptorsResult.ok)return{ok:!1,unsupported:!0,error:descriptorsResult.error};let injected=deps,probeDeps={...deps,readFile:injected.readFile??(p=>readFile6(p,"utf-8")),stat:injected.stat??(p=>stat4(p)),homedir:injected.homedir??os5.homedir},results=[];for(let descriptor of descriptorsResult.descriptors)results.push(await probePrerequisite(probeDeps,descriptor));return{ok:!0,results}}function
|
|
4716
|
+
Set BAPI_REPO_NAME in your config files. Do NOT put BAPI_API_KEY in the generated MCP config \u2014 supply it via the BAPI_API_KEY environment variable, or store it under "bapi:<repo_name>" in ~/.config/bridge/credentials.json. Get your values from the Bridge API setup UI at https://bridgegpt-api.com`)}init_start_tickets();init_start_tickets();init_review_tickets();init_start_tickets();init_version_generated();import{readFile as readFile6,stat as stat4}from"fs/promises";import{spawn}from"child_process";import os5 from"os";import path19 from"path";init_credential_store();import path18 from"path";var DEFAULT_BASE_URL="https://bridgegpt-api.com";function buildSetupUrl(baseUrl){return`${baseUrl.replace(/\/+$/,"")}/setup`}var MCP_CONFIG_ENV_TARGETS=[{relPath:".mcp.json",topLevelKey:"mcpServers"},{relPath:".cursor/mcp.json",topLevelKey:"mcpServers"},{relPath:".vscode/mcp.json",topLevelKey:"servers"}],PROBE_TIMEOUT_MS=5e3;async function resolveInstallDoctorTarget(deps){let envRepo=deps.env.BAPI_REPO_NAME?.trim(),envBase=deps.env.BAPI_BASE_URL?.trim(),repoName=envRepo&&envRepo.length>0?envRepo:null,repoSource=repoName?"env":null,baseUrl=envBase&&envBase.length>0?envBase:null;if(!repoName||!baseUrl)for(let{relPath,topLevelKey}of MCP_CONFIG_ENV_TARGETS){let raw;try{raw=await deps.readFile(path18.join(deps.cwd,relPath))}catch{continue}let parsed;try{parsed=JSON.parse(raw)}catch{continue}let envBlock=parsed&&typeof parsed=="object"?parsed[topLevelKey]?.["bridge-api"]?.env:void 0;if(envBlock&&(!repoName&&typeof envBlock.BAPI_REPO_NAME=="string"&&envBlock.BAPI_REPO_NAME.trim()&&(repoName=envBlock.BAPI_REPO_NAME.trim(),repoSource="config"),!baseUrl&&typeof envBlock.BAPI_BASE_URL=="string"&&envBlock.BAPI_BASE_URL.trim()&&(baseUrl=envBlock.BAPI_BASE_URL.trim()),repoName&&baseUrl))break}return{repoName,repoSource,baseUrl:baseUrl??DEFAULT_BASE_URL}}async function probeGet(deps,url,apiKey){try{let resp=await deps.fetch(url,{headers:{"X-API-Key":apiKey},signal:AbortSignal.timeout(PROBE_TIMEOUT_MS)}),body=null;try{body=await resp.json()}catch{}return{ok:!0,status:resp.status,body}}catch(e){return{ok:!1,error:e instanceof Error?e.message:String(e)}}}function summarizeManifestGroups(body){if(!body||typeof body!="object")return null;let groups=body.groups;if(!Array.isArray(groups))return null;let total=0,unset=[];for(let group of groups){let fields=group?.fields;if(Array.isArray(fields))for(let field of fields){let name=field?.field_name;typeof name=="string"&&(total+=1,field.is_set||unset.push(name))}}return{total,unset}}function summarizeIntegrations(body){if(!body||typeof body!="object")return null;let integrations=body.integrations;if(!Array.isArray(integrations))return null;let total=0,unconfigured=[];for(let item of integrations){let label=item?.label;typeof label=="string"&&(total+=1,item.is_configured||unconfigured.push(label))}return{total,unconfigured}}function readGithubConfiguredFlag(body){if(!body||typeof body!="object")return null;let integrations=body.integrations;if(!Array.isArray(integrations))return null;for(let item of integrations){if(item?.id!=="github_app")continue;let configured=item.is_configured;return typeof configured=="boolean"?configured:null}return null}async function collectInstallStatusChecks(deps){let checks=[],target=await resolveInstallDoctorTarget(deps);if(!target.repoName)return checks.push({id:"identity",label:"Repository identity",status:"SKIP",detail:"no BAPI_REPO_NAME in the environment or project-local MCP configs",remediation:"run install-bridge (or set BAPI_REPO_NAME) to configure this project."}),checks;checks.push({id:"identity",label:"Repository identity",status:"PASS",detail:`${target.repoName} (from ${target.repoSource}), base URL ${target.baseUrl}`});let setupUrl=buildSetupUrl(target.baseUrl),credDeps={env:deps.env,homedir:deps.homedir,platform:deps.platform,readFile:deps.readFile,stat:deps.stat,stderr:()=>{}},cred=await resolveBapiCredentials(target.repoName,credDeps);if(!cred.ok){checks.push({id:"credential",label:"Bridge API credential",status:"WARN",detail:`not resolved (${cred.kind})`,remediation:"set BAPI_API_KEY or persist it via /install-bridge / the credentials subcommand; until then the remaining install checks are skipped."});for(let[id,label]of[["connectivity","Server connectivity"],["bootstrap","Bootstrap config fields"],["indexing","Repository indexing"]])checks.push({id,label,status:"SKIP",detail:"no credential resolved"});return checks}checks.push({id:"credential",label:"Bridge API credential",status:"PASS",detail:`resolved from ${cred.credentials.source} (value never read into the report)`});let apiKey=cred.credentials.apiKey,repoQuery=`repo_name=${encodeURIComponent(target.repoName)}`,ping=await probeGet(deps,`${target.baseUrl}/jira/ping?${repoQuery}`,apiKey);ping.ok?ping.status===200?checks.push({id:"connectivity",label:"Server connectivity",status:"PASS",detail:"ping OK"}):ping.status===401||ping.status===403?checks.push({id:"connectivity",label:"Server connectivity",status:"WARN",detail:`the server rejected the resolved key (HTTP ${ping.status})`,remediation:"the key may have been rotated \u2014 re-run install-bridge with a current key."}):ping.status===404?checks.push({id:"connectivity",label:"Server connectivity",status:"WARN",detail:`the server does not know repo '${target.repoName}' (HTTP 404)`,remediation:`create the project at ${setupUrl} (setup UI), or check BAPI_REPO_NAME spelling.`}):checks.push({id:"connectivity",label:"Server connectivity",status:"WARN",detail:`unexpected HTTP ${ping.status} from /jira/ping`}):checks.push({id:"connectivity",label:"Server connectivity",status:"WARN",detail:`unreachable (${ping.error})`,remediation:`check ${target.baseUrl} and your network, then re-run doctor.`});let manifest=await probeGet(deps,`${target.baseUrl}/jira/config/install-manifest?${repoQuery}`,apiKey);if(!manifest.ok)checks.push({id:"bootstrap",label:"Bootstrap config fields",status:"WARN",detail:`manifest unreachable (${manifest.error})`});else if(manifest.status===404)checks.push({id:"bootstrap",label:"Bootstrap config fields",status:"WARN",detail:"no configuration row found for this repository",remediation:`create the project at ${setupUrl} (setup UI), then run /install-bridge.`});else if(manifest.status===200){let summary=summarizeManifestGroups(manifest.body);summary?summary.unset.length===0?checks.push({id:"bootstrap",label:"Bootstrap config fields",status:"PASS",detail:`${summary.total}/${summary.total} bootstrap fields set`}):checks.push({id:"bootstrap",label:"Bootstrap config fields",status:"WARN",detail:`${summary.total-summary.unset.length}/${summary.total} set; unset: ${summary.unset.join(", ")}`,remediation:"run /install-bridge to derive the unset fields (intentionally-unset fields are fine to leave)."}):checks.push({id:"bootstrap",label:"Bootstrap config fields",status:"WARN",detail:"manifest response had an unexpected shape"})}else checks.push({id:"bootstrap",label:"Bootstrap config fields",status:"WARN",detail:`unexpected HTTP ${manifest.status} from the install manifest`});if(manifest.ok&&manifest.status===200){let integrations=summarizeIntegrations(manifest.body);integrations===null?checks.push({id:"integrations",label:"Integration credentials",status:"SKIP",detail:"the manifest response carried no integrations checklist"}):integrations.unconfigured.length===0?checks.push({id:"integrations",label:"Integration credentials",status:"PASS",detail:`${integrations.total}/${integrations.total} configured`}):checks.push({id:"integrations",label:"Integration credentials",status:"WARN",detail:`not configured: ${integrations.unconfigured.join(", ")}`,remediation:`a human configures these at ${setupUrl} (setup UI \u2192 project settings) \u2014 Bridge API never accepts integration secrets through an agent or MCP tool.`})}else checks.push({id:"integrations",label:"Integration credentials",status:"SKIP",detail:"manifest unavailable"});if(manifest.ok&&manifest.status===200){let github=readGithubConfiguredFlag(manifest.body);github===null?checks.push({id:"github",label:"GitHub connection",status:"SKIP",detail:"the manifest response carried no GitHub integration entry"}):github?checks.push({id:"github",label:"GitHub connection",status:"PASS",detail:"a GitHub repository is connected to this project"}):checks.push({id:"github",label:"GitHub connection",status:"WARN",detail:"no GitHub repository is connected to this project",remediation:`run 'npx -y @bridge_gpt/mcp-server@latest connect-github --repo ${target.repoName}'.`})}else checks.push({id:"github",label:"GitHub connection",status:"SKIP",detail:"manifest unavailable"});let parse=await probeGet(deps,`${target.baseUrl}/jira/parse-status?${repoQuery}`,apiKey);if(!parse.ok||parse.status!==200)checks.push({id:"indexing",label:"Repository indexing",status:"SKIP",detail:parse.ok?`HTTP ${parse.status} from /jira/parse-status`:`unreachable (${parse.error})`});else if(parse.body?.status==="in_progress"){let startedAt=parse.body.started_at;checks.push({id:"indexing",label:"Repository indexing",status:"INFO",detail:`parse job in progress${typeof startedAt=="string"?` (started ${startedAt})`:""}`})}else checks.push({id:"indexing",label:"Repository indexing",status:"INFO",detail:"no parse job currently running \u2014 if this repository has never been indexed, queue one with /parse-repository (monitor with get_parse_status)."});return checks}function formatInstallStatusReport(checks){let lines=["","Install status (easy-install done criteria \u2014 advisory)",""],pad={PASS:"PASS ",WARN:"WARN ",INFO:"INFO ",SKIP:"SKIPPED"};for(let check of checks)lines.push(`${pad[check.status]} ${check.label}${check.detail?` \u2014 ${check.detail}`:""}`),check.remediation&&lines.push(` ${check.remediation}`);return lines.push(""),lines.push("This section is advisory and never changes the doctor exit code. It performs read-only GETs only."),lines.join(`
|
|
4717
|
+
`)}function formatInstallStatusFallbackReport(){return formatInstallStatusReport([{id:"collection",label:"Install status",status:"SKIP",detail:"the advisory install-status probes could not be collected",remediation:"re-run doctor; if it persists, run install-bridge to re-verify this project."}])}var OFF_TOKENS=new Set(["false","0","no","off","disabled"]),ON_TOKENS=new Set(["true","1","yes","on","enabled"]);function parseDefaultOnEnvFlag(value){if(value===void 0)return!0;let normalized=value.trim().toLowerCase();return normalized===""?!0:!OFF_TOKENS.has(normalized)}function parseDefaultOffEnvFlag(value){if(value===void 0)return!1;let normalized=value.trim().toLowerCase();return normalized===""?!1:ON_TOKENS.has(normalized)}function createBridgeApiUrls(baseUrl){let trimmedBase=baseUrl.replace(/\/+$/,""),buildUrl2=path39=>`${trimmedBase}/jira${path39}`;return{buildUrl:buildUrl2,buildApiUrl:path39=>`${trimmedBase}${path39}`,buildGetUrl:(path39,params)=>{let url=new URL(buildUrl2(path39));for(let[key,value]of Object.entries(params))url.searchParams.set(key,value);return url.toString()}}}import{getMethodLiteral}from"@modelcontextprotocol/sdk/server/zod-json-schema-compat.js";var RECOGNIZED_TOOL_SURFACE_SCHEMA_VERSIONS=new Set([1]);var TOOL_SURFACE_PROBE_DEADLINE_MS=2500,TOOL_SURFACE_POLL_MIN_MS=12e3,TOOL_SURFACE_POLL_MAX_MS=18e3;function timeoutResult(){return{reason:"timeout",blockedTools:new Set}}function malformedResult(subtype){return{reason:"malformed",subtype,blockedTools:new Set}}function validateToolSurfacePayload(body){if(body===null||typeof body!="object"||Array.isArray(body))return malformedResult("invalid-shape");let p=body;if(typeof p.schema_version!="number"||!Number.isInteger(p.schema_version))return malformedResult("invalid-shape");if(!RECOGNIZED_TOOL_SURFACE_SCHEMA_VERSIONS.has(p.schema_version))return malformedResult("unsupported-schema");if(typeof p.complete!="boolean"||typeof p.evaluated_tool_count!="number"||!Number.isInteger(p.evaluated_tool_count)||p.evaluated_tool_count<0||typeof p.catalog_revision!="string"||!Array.isArray(p.blocked_tools)||!p.blocked_tools.every(t=>typeof t=="string"))return malformedResult("invalid-shape");if(!p.complete)return malformedResult("incomplete");if(p.catalog_revision.length===0)return malformedResult("invalid-shape");let blockedTools=new Set(p.blocked_tools);return{reason:"blocked",catalogRevision:p.catalog_revision,evaluatedToolCount:p.evaluated_tool_count,blockedTools}}async function probeToolSurface(options){let deadlineMs=options.deadlineMs??TOOL_SURFACE_PROBE_DEADLINE_MS,controller=new AbortController,onLifecycleAbort=()=>controller.abort();options.abortSignal&&(options.abortSignal.aborted?controller.abort():options.abortSignal.addEventListener("abort",onLifecycleAbort,{once:!0}));let timer,deadlinePromise=new Promise(resolve2=>{timer=setTimeout(()=>{controller.abort(),resolve2(timeoutResult())},deadlineMs)}),abortPromise=new Promise(resolve2=>{if(controller.signal.aborted){resolve2(timeoutResult());return}controller.signal.addEventListener("abort",()=>resolve2(timeoutResult()),{once:!0})}),workPromise=(async()=>{try{let headers=await options.resolveHeaders();if(controller.signal.aborted)return timeoutResult();let resp=await options.fetchFn(options.url,{method:"GET",headers,signal:controller.signal});if(!resp.ok)return malformedResult("non-2xx");let parsed;try{parsed=await resp.json()}catch{return controller.signal.aborted?timeoutResult():malformedResult("invalid-json")}return validateToolSurfacePayload(parsed)}catch{return controller.signal.aborted?timeoutResult():malformedResult("network")}})();try{return await Promise.race([workPromise,deadlinePromise,abortPromise])}finally{timer&&clearTimeout(timer),options.abortSignal&&options.abortSignal.removeEventListener("abort",onLifecycleAbort)}}var defaultScheduler={setTimeout:(callback,ms)=>setTimeout(callback,ms),clearTimeout:handle=>clearTimeout(handle),random:()=>Math.random()};function logDecision(logger,result,hiddenCount,hiddenNames){let revision=result.reason==="blocked"?result.catalogRevision:"n/a",subtype=result.reason==="malformed"?result.subtype:"n/a";logger(`tool-surface gating: reason=${result.reason} subtype=${subtype} hidden=${hiddenCount} revision=${revision} hidden_tools=[${hiddenNames.join(", ")}]`)}function createToolSurfaceGate(options){let{startupProbe,advertised,originalListHandler,freshProbe,notify,logger,lifecycleController}=options,scheduler=options.scheduler??defaultScheduler,advertisedNames=new Set(advertised.map(r=>r.name)),hiddenNames=new Set,lastServedVisible=null,catalogRevision=null,startupApplied=!1,timer,closed=!1;function deriveHidden(result){if(result.reason!=="blocked"||result.blockedTools.size===0)return new Set;let hidden=new Set;for(let id of result.blockedTools)advertisedNames.has(id)&&hidden.add(id);return hidden}function deriveVisible(hidden){let visible=new Set;for(let reg of advertised)reg.isEnabled()&&(hidden.has(reg.name)||visible.add(reg.name));return visible}function applyDecision(result){let nextHidden=deriveHidden(result);hiddenNames=nextHidden,logDecision(logger,result,nextHidden.size,Array.from(nextHidden)),result.reason==="blocked"&&result.catalogRevision!==catalogRevision&&(catalogRevision!==null&&logger(`tool-surface gating: catalog_revision ${catalogRevision} -> ${result.catalogRevision}`),catalogRevision=result.catalogRevision)}function projectList(original){let tools=original.tools.filter(tool=>!hiddenNames.has(tool.name));return{...original,tools}}let handleList=async(request,extra)=>{let startupResult=await startupProbe;startupApplied||(startupApplied=!0,applyDecision(startupResult));let original=await originalListHandler(request,extra),projected=projectList(original);return lastServedVisible=new Set(projected.tools.map(t=>t.name)),projected};async function pollOnce(){let result;try{result=await freshProbe()}catch{result=timeoutResult()}if(closed)return;let previousVisibleServed=lastServedVisible;applyDecision(result);let nextVisible=deriveVisible(hiddenNames);if(previousVisibleServed!==null&&!setsEqual(previousVisibleServed,nextVisible)){lastServedVisible=nextVisible;try{notify()}catch{logger("tool-surface gating: notification failed (suppressed)")}}}function scheduleNext(){if(closed)return;let span=TOOL_SURFACE_POLL_MAX_MS-TOOL_SURFACE_POLL_MIN_MS,delay=Math.round(TOOL_SURFACE_POLL_MIN_MS+scheduler.random()*span);timer=scheduler.setTimeout(()=>{pollOnce().finally(()=>{scheduleNext()})},delay),timer&&typeof timer.unref=="function"&&timer.unref()}function startPolling(){closed||scheduleNext()}function close(){closed||(closed=!0,timer&&(scheduler.clearTimeout(timer),timer=void 0),lifecycleController.signal.aborted||lifecycleController.abort())}return{handleList,startPolling,close}}function setsEqual(a,b){if(a.size!==b.size)return!1;for(let v of a)if(!b.has(v))return!1;return!0}var COMPAT_ERROR="tool-surface gating: incompatible MCP SDK \u2014 the tools/list handler could not be resolved for override.";function installToolSurfaceListOverride(protocolServer,listSchema,customHandler){let method;try{method=getMethodLiteral(listSchema)}catch{throw new Error(COMPAT_ERROR)}if(method!=="tools/list")throw new Error(COMPAT_ERROR);let handlers=protocolServer?._requestHandlers;if(!handlers||typeof handlers.get!="function")throw new Error(COMPAT_ERROR);let original=handlers.get(method);if(typeof original!="function")throw new Error(COMPAT_ERROR);return protocolServer.setRequestHandler(listSchema,customHandler),original}init_credential_store();init_agent_registry();init_start_tickets_prereqs();init_mcp_profile();var DOCTOR_REPORT_TITLE="bridge doctor \u2014 read-only diagnostics";function getDoctorUsage(){return["Usage:"," npx -y @bridge_gpt/mcp-server doctor [--agent <name>]","",`${DOCTOR_REPORT_TITLE}. It only checks your environment and prints manual`,"install instructions \u2014 it does not install anything, modify your system, or","start the MCP server.","","Flags:"," --agent claude|cursor-agent Agent to include in the prerequisite check (default: claude)"," -h, --help Show this help","","The report opens with an advisory 'Install status' section (easy-install done","criteria): repo identity, credential resolution, server connectivity,","bootstrap-field completeness, integration credentials, and repository-indexing","state. It performs read-only GETs only and never affects the exit code.","","After that come the start-tickets prerequisite checks (for the current OS):","the start-tickets preflight prerequisites plus","uv, the selected agent's command, Bridge API credential resolution, and","worktree MCP registration reachability. Credential resolution reports the","source it would use (env vs. store target bapi:<repo>); it never reads or","prints the key value and never writes the credential store. To persist or","migrate a credential, use /install-bridge or the `credentials` subcommand \u2014","doctor stays strictly read-only.","","It also includes an advisory 'MCP tool surface' section (BAPI-641): what","dynamic capability gating would advertise for this repo. It performs at most","one read-only GET to /jira/mcp/tool-surface (none under the kill switch) and","is advisory/fail-open \u2014 a timeout or malformed response is reported as","'fail-open to full surface' and never affects the exit code. Clients that","ignore notifications/tools/list_changed must reconnect or start a new MCP","session to observe surface changes; no project MCP config change is required.","","Conductor ledger / native-module diagnostics (the SQLite ledger's native","binding load status and Node-version skew) live under a separate command:"," conductor doctor","That command is likewise strictly read-only \u2014 it does not install, rebuild,","migrate, or write ledger files.","","Exit code: 0 when all required prerequisites are present, non-zero otherwise."].join(`
|
|
4718
|
+
`)}function parseDoctorArgs(argv){if(argv.includes("-h")||argv.includes("--help"))return{status:"help",usage:getDoctorUsage()};let agentName=DEFAULT_AGENT_NAME;for(let i=0;i<argv.length;i++){let arg=argv[i];if(arg==="--fix"||arg.startsWith("--fix="))return{status:"error",message:"--fix is unsupported: doctor is strictly read-only and never installs or modifies anything. Run the printed install commands manually."};if(arg==="--agent"||arg.startsWith("--agent=")){let value;if(arg.startsWith("--agent="))value=arg.slice(8);else{if(i+1>=argv.length)return{status:"error",message:"--agent requires a value (an agent name)."};i+=1,value=argv[i]}if(!isAgentName(value))return{status:"error",message:`Invalid --agent value: '${value}' (allowed agents: ${formatValidAgentNames()}).`};agentName=value;continue}return arg.startsWith("-")?{status:"error",message:`Unknown flag: ${arg}`}:{status:"error",message:`Unexpected positional argument: '${arg}'. doctor does not accept positional arguments.`}}return{status:"ok",options:{agentName}}}async function collectDoctorResults(deps,agentName){let agent=resolveAgentSpec(agentName)??resolveAgentSpec(DEFAULT_AGENT_NAME),descriptorsResult=getDoctorPrereqDescriptors(deps.platform,deps.env,agent);if(!descriptorsResult.ok)return{ok:!1,unsupported:!0,error:descriptorsResult.error};let injected=deps,probeDeps={...deps,readFile:injected.readFile??(p=>readFile6(p,"utf-8")),stat:injected.stat??(p=>stat4(p)),homedir:injected.homedir??os5.homedir},results=[];for(let descriptor of descriptorsResult.descriptors)results.push(await probePrerequisite(probeDeps,descriptor));return{ok:!0,results}}function buildDoctorReportHeader(platform,agent){let activeGroups=Array.from(resolveProfiles(process.env.BRIDGE_MCP_PROFILE)).join(", ");return[DOCTOR_REPORT_TITLE,`Platform: ${platform}`,`Selected agent: ${agent.name} (command: ${agent.command})`,`Active MCP Groups: \`${activeGroups}\``,""]}function formatDoctorPrereqSection(platform,collection){let lines=[];if(!collection.ok)return lines.push(`Platform '${platform}' is unsupported. start-tickets supports darwin, win32, and linux.`),lines.join(`
|
|
4719
4719
|
`);for(let result of collection.results){let status=result.found?"FOUND ":"MISSING",detail=result.found&&result.detail?` (${result.detail})`:"";lines.push(`${status} ${result.label}${detail}`),result.found||lines.push(` To install manually: ${result.installHint}`),result.authNote&&lines.push(` Note: ${result.authNote}`)}let anyMissing=collection.results.some(r=>!r.found);return lines.push(""),lines.push(anyMissing?"Some prerequisites are missing \u2014 install the ones above manually, then re-run doctor.":"All required prerequisites are present."),lines.push("For conductor ledger/native-module diagnostics, run: conductor doctor"),lines.join(`
|
|
4720
4720
|
`)}var BRIDGE_PACKAGE_NAME="@bridge_gpt/mcp-server",LAUNCHER_CONFIG_TARGETS=[{relPath:".mcp.json",topLevelKey:"mcpServers"},{relPath:".cursor/mcp.json",topLevelKey:"mcpServers"},{relPath:".vscode/mcp.json",topLevelKey:"servers"}];function parseLauncherPin(args){if(!Array.isArray(args))return null;for(let arg of args)if(typeof arg=="string"){if(arg===BRIDGE_PACKAGE_NAME)return{spec:arg,version:null};if(arg.startsWith(`${BRIDGE_PACKAGE_NAME}@`)){let version=arg.slice(BRIDGE_PACKAGE_NAME.length+1).trim();return{spec:arg,version:version.length>0?version:null}}}return null}function probeNpxNoInstallDefault(spec){return new Promise(resolve2=>{try{let child=spawn("npx",["--no-install",spec,"--version"],{shell:!1,stdio:"ignore",timeout:3e4});child.on("error",()=>resolve2({warmed:!1,indeterminate:!0})),child.on("close",(code,signal)=>{resolve2(signal?{warmed:!1,indeterminate:!0}:code===0?{warmed:!0,indeterminate:!1}:{warmed:!1,indeterminate:!0})})}catch{resolve2({warmed:!1,indeterminate:!0})}})}async function inspectLauncherCache(deps){let inspections=[];for(let{relPath,topLevelKey}of LAUNCHER_CONFIG_TARGETS){let fullPath=path19.join(deps.cwd,relPath),raw;try{raw=await deps.readFile(fullPath)}catch{continue}let parsed;try{parsed=JSON.parse(raw)}catch{inspections.push({relPath,spec:null,pinnedVersion:null,state:"indeterminate",remediation:"config is not valid JSON \u2014 cannot determine the launcher pin."});continue}let entry=parsed&&typeof parsed=="object"?parsed[topLevelKey]?.["bridge-api"]:void 0;if(!entry)continue;let pin=parseLauncherPin(entry.args);if(!pin){inspections.push({relPath,spec:null,pinnedVersion:null,state:"indeterminate",remediation:`no ${BRIDGE_PACKAGE_NAME} spec found in the launcher args.`});continue}if(pin.version===null||pin.version==="latest"){inspections.push({relPath,spec:pin.spec,pinnedVersion:pin.version,state:"unpinned",remediation:`pin the launcher to ${BRIDGE_PACKAGE_NAME}@${VERSION} (run /install-bridge or the install-bridge subcommand to rewrite this config).`});continue}if(pin.version!==VERSION){inspections.push({relPath,spec:pin.spec,pinnedVersion:pin.version,state:"stale-pinned",remediation:`config pins ${pin.version} but this package is ${VERSION}; run upgrade-bridge / install-bridge to repin and re-warm.`});continue}(await deps.probeNpxNoInstall(pin.spec)).warmed?inspections.push({relPath,spec:pin.spec,pinnedVersion:pin.version,state:"warmed"}):inspections.push({relPath,spec:pin.spec,pinnedVersion:pin.version,state:"indeterminate",remediation:`pinned-but-unwarmed: the pinned _npx bucket is not a confirmed cache hit, so the first MCP launch may pay a one-time cold install. Warm it with: npx ${pin.spec} --version, or raise MCP_TIMEOUT for the first launch.`})}return inspections}function formatLauncherCacheReport(inspections){let lines=["","Launcher cache (MCP cold-start readiness)",""];if(inspections.length===0)return lines.push("No project-local bridge-api launcher configs found to inspect."),lines.join(`
|
|
4721
4721
|
`);let labels={warmed:"WARMED ",unpinned:"UNPINNED","stale-pinned":"STALE-PINNED",indeterminate:"INDETERMINATE"};for(let i of inspections){let specText=i.spec?` (${i.spec})`:"";lines.push(`${labels[i.state]} ${i.relPath}${specText}`),i.remediation&&lines.push(` ${i.remediation}`)}return lines.join(`
|
|
4722
4722
|
`)}async function collectToolSurfaceDiagnostic(deps){if(!parseDefaultOnEnvFlag(deps.env.BAPI_MCP_TOOL_SURFACE_GATING_ENABLED))return{enabled:!1,reason:"kill-switch"};let target=await resolveInstallDoctorTarget(deps);if(!target.repoName)return{enabled:!0,reason:"unresolved",detail:"no BAPI_REPO_NAME in the environment or project-local MCP configs"};let credDeps={env:deps.env,homedir:deps.homedir,platform:deps.platform,readFile:deps.readFile,stat:deps.stat,stderr:()=>{}},cred=await resolveBapiCredentials(target.repoName,credDeps);if(!cred.ok)return{enabled:!0,reason:"unresolved",detail:`no Bridge API credential resolved (${cred.kind})`};let apiKey=cred.credentials.apiKey,url=createBridgeApiUrls(target.baseUrl).buildGetUrl("/mcp/tool-surface",{repo_name:target.repoName}),result=await probeToolSurface({url,resolveHeaders:async()=>({"X-API-Key":apiKey}),fetchFn:deps.fetch});return result.reason==="blocked"?{enabled:!0,reason:"blocked",blockedTools:Array.from(result.blockedTools),catalogRevision:result.catalogRevision,evaluatedToolCount:result.evaluatedToolCount}:result.reason==="timeout"?{enabled:!0,reason:"timeout"}:{enabled:!0,reason:"malformed",subtype:result.subtype}}function formatToolSurfaceDiagnosticReport(diag){let lines=["","MCP tool surface (dynamic capability gating \u2014 advisory)",""];switch(lines.push(`Kill switch: ${diag.enabled?"ENABLED (gating active)":"DISABLED (full surface)"}`),diag.reason){case"kill-switch":lines.push("Reason: kill-switch \u2014 BAPI_MCP_TOOL_SURFACE_GATING_ENABLED is off, so the full profile surface is advertised and no probe is performed.");break;case"unresolved":lines.push(`Reason: unresolved \u2014 ${diag.detail??"repo/credential not resolved"}; the probe was skipped and the full surface is advertised (fail-open to full surface).`);break;case"blocked":{let ids=diag.blockedTools??[];lines.push("Reason: blocked \u2014 the backend returned a valid capability decision."),lines.push("Probe: reachable (HTTP 200, valid response)."),lines.push(`Blocked tools (${ids.length}): [${ids.join(", ")}]`),diag.catalogRevision&&lines.push(`Catalog revision: ${diag.catalogRevision}`),typeof diag.evaluatedToolCount=="number"&&lines.push(`Evaluated tool count: ${diag.evaluatedToolCount}`);break}case"timeout":lines.push("Reason: timeout \u2014 the probe deadline elapsed; fail-open to full surface.");break;case"malformed":lines.push(`Reason: malformed (${diag.subtype??"unknown"}) \u2014 fail-open to full surface.`);break}return lines.push(""),lines.push("Blocked IDs are intersected with the locally active MCP profile and the current SDK-enabled"),lines.push("baseline only when an MCP session starts, so IDs unknown to this package or excluded by the"),lines.push("active profile have no effect. Capability-hidden tools remain registered and callable \u2014 the"),lines.push("backend is the enforcement boundary. This section is advisory and never changes the exit code."),lines.push("Clients that ignore notifications/tools/list_changed must reconnect or start a new MCP session"),lines.push("to observe surface changes; no project MCP configuration change is required."),lines.join(`
|
|
4723
|
-
`)}async function runDoctorCli(argv,overrides={}){let log=overrides.log??(m=>console.log(m)),errorLog=overrides.errorLog??(m=>console.error(m)),parsed=parseDoctorArgs(argv);if(parsed.status==="help")return log(parsed.usage),0;if(parsed.status==="error")return errorLog(`Error: ${parsed.message}`),errorLog(""),errorLog(getDoctorUsage()),1;let deps=overrides.deps??createDefaultStartTicketsDeps(),agent=resolveAgentSpec(parsed.options.agentName)??resolveAgentSpec(DEFAULT_AGENT_NAME),collection=await collectDoctorResults(deps,parsed.options.agentName);
|
|
4723
|
+
`)}async function runDoctorCli(argv,overrides={}){let log=overrides.log??(m=>console.log(m)),errorLog=overrides.errorLog??(m=>console.error(m)),parsed=parseDoctorArgs(argv);if(parsed.status==="help")return log(parsed.usage),0;if(parsed.status==="error")return errorLog(`Error: ${parsed.message}`),errorLog(""),errorLog(getDoctorUsage()),1;let deps=overrides.deps??createDefaultStartTicketsDeps(),agent=resolveAgentSpec(parsed.options.agentName)??resolveAgentSpec(DEFAULT_AGENT_NAME),collection=await collectDoctorResults(deps,parsed.options.agentName);if(log(buildDoctorReportHeader(deps.platform,agent).join(`
|
|
4724
|
+
`)),overrides.installStatus!==!1)try{let injectedFs=deps,installDeps={env:overrides.installStatus?.env??deps.env,cwd:overrides.installStatus?.cwd??deps.cwd,platform:overrides.installStatus?.platform??deps.platform,homedir:overrides.installStatus?.homedir??injectedFs.homedir??os5.homedir,readFile:overrides.installStatus?.readFile??injectedFs.readFile??(p=>readFile6(p,"utf-8")),stat:overrides.installStatus?.stat??injectedFs.stat??(p=>stat4(p)),fetch:overrides.installStatus?.fetch??((...args)=>fetch(...args))},checks=await collectInstallStatusChecks(installDeps);log(formatInstallStatusReport(checks))}catch{log(formatInstallStatusFallbackReport())}log(formatDoctorPrereqSection(deps.platform,collection));try{let launcherDeps={cwd:overrides.launcherProbe?.cwd??deps.cwd,readFile:overrides.launcherProbe?.readFile??(p=>readFile6(p,"utf-8")),probeNpxNoInstall:overrides.launcherProbe?.probeNpxNoInstall??probeNpxNoInstallDefault},launcherInspections=await inspectLauncherCache(launcherDeps);log(formatLauncherCacheReport(launcherInspections))}catch{}if(overrides.toolSurface!==!1)try{let injectedFs=deps,toolSurfaceDeps={env:overrides.toolSurface?.env??deps.env,cwd:overrides.toolSurface?.cwd??deps.cwd,platform:overrides.toolSurface?.platform??deps.platform,homedir:overrides.toolSurface?.homedir??injectedFs.homedir??os5.homedir,readFile:overrides.toolSurface?.readFile??injectedFs.readFile??(p=>readFile6(p,"utf-8")),stat:overrides.toolSurface?.stat??injectedFs.stat??(p=>stat4(p)),fetch:overrides.toolSurface?.fetch??((...args)=>fetch(...args))},diagnostic=await collectToolSurfaceDiagnostic(toolSurfaceDeps);log(formatToolSurfaceDiagnosticReport(diagnostic))}catch{log(formatToolSurfaceDiagnosticReport({enabled:parseDefaultOnEnvFlag(deps.env.BAPI_MCP_TOOL_SURFACE_GATING_ENABLED),reason:"malformed",subtype:"unexpected"}))}return collection.ok?collection.results.some(r=>!r.found)?1:0:1}function extractBackendWarnings(body){if(body===null||typeof body!="object")return[];let record=body,warnings=[];if(typeof record.warning=="string"&&record.warning&&warnings.push(record.warning),Array.isArray(record.warnings))for(let entry of record.warnings)typeof entry=="string"&&entry?warnings.push(entry):entry!==null&&typeof entry=="object"&&typeof entry.message=="string"&&entry.message&&warnings.push(entry.message);return warnings}function appendBackendWarningsToText(text,warnings){return warnings.length?`${text}
|
|
4724
4725
|
|
|
4725
4726
|
**Warning:** ${warnings.join(" ")}`:text}init_schedule_run();init_bridge_config();init_credential_store();init_third_party_mcp_targets();import{spawn as spawn2,execFile as execFile3}from"child_process";import{stat as stat5,readFile as readFile7}from"fs/promises";import path22 from"path";import os6 from"os";function getMcpInvokeUsage(){return["Usage:"," node <abs>/mcp_server/build/index.js mcp-invoke --target <target> --project-root <ABS_WORKTREE_PATH>",""," (npm-channel fallback may invoke the same shim through a package spec, e.g."," npx -y @bridge_gpt/mcp-server@latest mcp-invoke --target <target> --project-root <ABS_WORKTREE_PATH>)","","Internal worktree shim: resolves the target's launch command and credentials","from the given project root, then spawns the real MCP server over stdio.","Argument parsing is identical regardless of how this process was launched","(absolute-path node invocation vs npm-channel npx).","","Flags:"," --target <target> MCP target to launch. 'bapi' launches the Bridge"," API server; a configured third-party target from"," .bridge/config (e.g. sfcc) launches that server."," --project-root <ABS_PATH> Absolute path to the worktree (required)"," -h, --help Show this help"].join(`
|
|
4726
4727
|
`)}var KNOWN_FLAGS=new Set(["--target","--project-root"]);function parseMcpInvokeArgs(argv){let target,projectRoot;for(let i=0;i<argv.length;i++){let token=argv[i];if(token==="-h"||token==="--help")return{status:"help",usage:getMcpInvokeUsage()};let flag=token,value,eq=token.indexOf("=");if(token.startsWith("--")&&eq!==-1&&(flag=token.slice(0,eq),value=token.slice(eq+1)),!KNOWN_FLAGS.has(flag))return token.startsWith("-")?{status:"error",message:`Unknown flag: ${flag}`}:{status:"error",message:`Unexpected positional argument: ${token}`};if(value===void 0){let next=argv[i+1];if(next===void 0||next.startsWith("-"))return{status:"error",message:`Missing value for ${flag}`};value=next,i++}if(flag==="--target"){if(target!==void 0)return{status:"error",message:"Duplicate --target flag"};target=value}else{if(projectRoot!==void 0)return{status:"error",message:"Duplicate --project-root flag"};projectRoot=value}}if(target===void 0)return{status:"error",message:"Missing required --target flag"};let targetValidation=validateMcpTarget(target);return targetValidation.ok?projectRoot===void 0?{status:"error",message:"Missing required --project-root flag"}:path22.isAbsolute(projectRoot)?{status:"ok",target:targetValidation.value,projectRoot}:{status:"error",message:"--project-root must be an absolute path"}:{status:"error",message:`Invalid --target: ${targetValidation.error}`}}async function validateProjectRootDirectory(projectRoot,statFn){try{return(await statFn(projectRoot)).isDirectory()?{ok:!0}:{ok:!1,error:`--project-root is not a directory: ${projectRoot}`}}catch{return{ok:!1,error:`--project-root does not exist: ${projectRoot}`}}}function buildChildEnv(parentEnv,overlay){return{...parentEnv,...overlay}}function signalExitCode(signal){if(signal==="SIGINT")return 130;if(signal==="SIGTERM")return 143;let num=os6.constants.signals[signal];return typeof num=="number"?128+num:1}function spawnMcpCommand(deps){return new Promise((resolve2,reject)=>{let child;try{child=deps.spawn(deps.command,deps.args,{stdio:"inherit",env:deps.env})}catch(err){reject(err);return}let signals=["SIGINT","SIGTERM"],handlers={},cleaned=!1,cleanup=()=>{if(!cleaned){cleaned=!0;for(let sig of signals)deps.offSignal(sig,handlers[sig])}};for(let sig of signals){let handler=()=>{try{child.kill(sig)}catch{}};handlers[sig]=handler,deps.onSignal(sig,handler)}child.on("error",err=>{cleanup(),reject(err)}),child.on("close",(...args)=>{cleanup();let code=args[0],signal=args[1];resolve2(typeof code=="number"?code:signal?signalExitCode(signal):0)})})}function defaultRunCommand(file,args,options){return new Promise(resolve2=>{execFile3(file,args,{cwd:options?.cwd},(err,stdout,stderr)=>{if(err){let code=typeof err.code=="number"?err.code:1;resolve2({stdout:stdout?.toString()??"",stderr:stderr?.toString()??err.message,exitCode:code})}else resolve2({stdout:stdout.toString(),stderr:stderr.toString(),exitCode:0})})})}async function resolveBapiInvocation(projectRoot,deps){let repoResult=await deps.resolveRepoName(projectRoot);if(!repoResult.ok)return{ok:!1,error:`failed to resolve repo identity: ${repoResult.error}`};let repoName=repoResult.value,credResult=await deps.resolveCredentials(repoName,projectRoot);if(!credResult.ok)return{ok:!1,error:`failed to resolve credentials: ${credResult.error}`};let overlay={BAPI_API_KEY:credResult.credentials.apiKey,BAPI_PROJECT_ROOT:projectRoot,BAPI_REPO_NAME:repoName},conductorNodePath=deps.env.CONDUCTOR_NODE_PATH;typeof conductorNodePath=="string"&&conductorNodePath.trim().length>0&&(overlay.CONDUCTOR_NODE_PATH=conductorNodePath);let env=buildChildEnv(deps.env,overlay);return{ok:!0,invocation:{command:deps.execPath,args:[deps.scriptPath],env}}}async function resolveThirdPartyInvocation(target,projectRoot,deps){let definition=deps.getTargetDefinition(target);if(!definition)return{ok:!1,error:`unsupported MCP target '${target}'; not a known third-party target`};let read=await deps.readBridgeConfig(projectRoot);if(!read.ok)return read.kind==="missing"?{ok:!1,error:`target '${target}' requires a .bridge/config; none was found`}:{ok:!1,error:`unable to read .bridge/config for target '${target}'`};let entry=read.manifest.mcp.find(m=>m.target===target);if(!entry)return{ok:!1,error:`target '${target}' is not declared in .bridge/config`};let entryValidation=validateThirdPartyTargetManifestEntry(entry);if(!entryValidation.ok)return{ok:!1,error:entryValidation.error};let command=entry.command,args=entry.args,secretBundle=entry.secretBundle,envResult=await deps.resolveTargetEnv(definition,secretBundle);if(!envResult.ok)return{ok:!1,error:`failed to resolve credentials for '${target}': ${envResult.error}`};let env=buildChildEnv(deps.env,envResult.env);return{ok:!0,invocation:{command,args,env}}}async function runMcpInvokeCli(argv,overrides={}){let log=overrides.log??(m=>process.stdout.write(`${m}
|
|
@@ -4777,21 +4778,21 @@ Options:
|
|
|
4777
4778
|
when omitted; you will be asked to confirm).
|
|
4778
4779
|
--help Show this message.`;function parseConnectGithubArgs(argv){let out={help:!1};for(let i=0;i<argv.length;i+=1){let arg=argv[i];if(arg==="--help"||arg==="-h"){out.help=!0;continue}if(arg==="--repo"){let value=argv[i+1];if(!value||value.startsWith("-"))return{ok:!1,error:"--repo requires a value (e.g. --repo my-project)."};out.repo=value,i+=1;continue}if(arg.startsWith("--repo=")){let value=arg.slice(7);if(!value)return{ok:!1,error:"--repo requires a value (e.g. --repo my-project)."};out.repo=value;continue}return arg==="--yes"||arg==="-y"?{ok:!1,error:"connect-github does not support --yes: connecting a repository always requires an explicit confirmation."}:arg==="--installation-id"||arg.startsWith("--installation-id=")?{ok:!1,error:"connect-github does not accept --installation-id: the installation is verified by Bridge from your browser install, not supplied by the caller."}:arg.startsWith("-")?{ok:!1,error:`Unknown option: ${arg}`}:{ok:!1,error:`Unexpected argument: ${arg}`}}return{ok:!0,value:out}}function defaultPromptLine2(promptText){return new Promise(resolve2=>{let rl=readline2.createInterface({input:process.stdin,output:process.stderr}),answered=!1;rl.on("close",()=>{answered||resolve2("")}),rl.question(promptText,answer=>{answered=!0,rl.close(),resolve2(answer.trim())})})}function defaultOpenBrowser(platform,url){let[command,args]=platform==="darwin"?["open",[url]]:platform==="win32"?["cmd",["/c","start","",url]]:["xdg-open",[url]];return new Promise(resolve2=>{try{let child=spawn6(command,args,{stdio:"ignore",detached:!1,shell:!1});child.on("error",()=>resolve2(!1)),child.on("spawn",()=>resolve2(!0))}catch{resolve2(!1)}})}function createDefaultConnectGithubDeps(){return{env:process.env,cwd:process.cwd(),platform:process.platform,homedir:os13.homedir,isTTY:!!process.stdin.isTTY,readFile:filePath=>readFile10(filePath,"utf-8"),stat:async filePath=>({mode:(await stat7(filePath)).mode}),fetch:globalThis.fetch,sleep:ms=>new Promise(resolve2=>setTimeout(resolve2,ms)),now:()=>Date.now(),jitter:()=>Math.random(),promptLine:defaultPromptLine2,openBrowser:url=>defaultOpenBrowser(process.platform,url),stdout:message=>process.stdout.write(`${message}
|
|
4779
4780
|
`),stderr:message=>process.stderr.write(`${message}
|
|
4780
|
-
`)}}async function resolveConnectGithubRepoName(args,deps){if(args.repo){let validated2=validateRepoName(args.repo);return validated2.ok?{ok:!0,value:validated2.value}:{ok:!1,error:validated2.error}}let inferred=await resolveStartTicketsRepoName({env:deps.env,cwd:deps.cwd,readFile:deps.readFile});if(!inferred){let validated2=validateRepoName(path27.basename(deps.cwd));validated2.ok&&(inferred=validated2.value)}if(!inferred)return{ok:!1,error:"Could not determine the Bridge project. Pass --repo <repo_name>."};let answer=(await deps.promptLine(`Bridge project [${inferred}]: `)).trim(),chosen=answer.length>0?answer:inferred,validated=validateRepoName(chosen);return validated.ok?{ok:!0,value:validated.value}:{ok:!1,error:validated.error}}async function openGithubInstallPage(deps,installUrl){return await deps.openBrowser(installUrl)?{ok:!0}:{ok:!1,error:"Could not open your browser automatically. Re-run this command from a desktop session with a browser available."}}var STEPS=["Connect GitHub","Complete GitHub in browser","Verify connection","Choose repository","Confirm connection"];function renderStep(deps,index){deps.stderr(`[${index+1}/${STEPS.length}] ${STEPS[index]}`)}function candidateLabel(c){return c.github_repo_full_name?c.github_repo_full_name:c.owner?`${c.owner}/${c.github_repo_name}`:c.github_repo_name}var OUTCOME_MESSAGES={expired:"The connection request expired before GitHub reported back. Run connect-github again.",invalid:"This connection request is no longer valid. Run connect-github again.","verification-failed":"Bridge could not verify the GitHub installation. Run connect-github again.","no-repositories":"The GitHub App installation did not include any repositories Bridge can access. Re-run connect-github and grant access to at least one repository.",conflict:"This GitHub installation or project is already connected to a different Bridge account or repository. Contact support if that is unexpected.",failed:"The GitHub connection did not complete. Run connect-github again."},FAILURE_MESSAGES={network:"Could not reach Bridge API. Check your network, then run connect-github again.",timeout:"Bridge API did not respond in time. Run connect-github again.",unauthorized:"Bridge rejected your API key for this project. Re-run install-bridge with a current key.","not-found":"Bridge does not recognize this project. Check --repo matches your Bridge project name.",server:"Bridge API returned an error. Run connect-github again shortly.",malformed:"Bridge API returned an unexpected response. Run connect-github again shortly.",deadline:"Timed out waiting for GitHub. If you completed the install, run connect-github again to pick up the connection."};function reportNoConnection(deps,detail){return deps.stderr(""),deps.stderr("No GitHub connection was made."),deps.stderr(detail),1}async function runGithubConnectionFlow(deps,api,repoName){renderStep(deps,0);let minted=await mintGithubConnection(api,repoName);if(!minted.ok)return reportNoConnection(deps,FAILURE_MESSAGES[minted.kind]);renderStep(deps,1),deps.stderr("Opening GitHub\u2026");let opened=await openGithubInstallPage(deps,minted.value.installUrl);if(!opened.ok)return reportNoConnection(deps,opened.error);renderStep(deps,2);let minutes=Math.floor(POLL_DEADLINE_MS/6e4);deps.stderr(`Waiting for GitHub installation\u2026 (up to ~${minutes} minutes)`);let pollDeps={sleep:deps.sleep,now:deps.now,jitter:deps.jitter},started=deps.now(),polled=await pollGithubConnection(api,pollDeps,repoName,minted.value.state);if(!polled.ok)return reportNoConnection(deps,FAILURE_MESSAGES[polled.kind]);let elapsedSec=Math.max(0,Math.round((deps.now()-started)/1e3));deps.stderr(`Waited ${elapsedSec}s.`);let result=polled.value;if(result.status==="awaiting-organization-approval")return deps.stderr(""),deps.stderr("No GitHub connection was made."),deps.stderr("Your request to install the GitHub App was sent to an organization owner for approval."),deps.stderr("That approval happens on GitHub and does not return here, so this command cannot wait for it."),deps.stderr("Once an owner approves the install, finish the connection with the manual steps in the GitHub App setup guide (docs/install/github-app.md)."),1;if(result.status==="connected")return deps.stdout(`Connected ${result.githubRepoName??repoName}.`),0;if(result.status!=="staged")return reportNoConnection(deps,OUTCOME_MESSAGES[result.status]??OUTCOME_MESSAGES.failed);let candidates=result.candidates;if(candidates.length===0)return reportNoConnection(deps,OUTCOME_MESSAGES["no-repositories"]);renderStep(deps,3);let selected=null;if(candidates.length===1){let only=candidates[0],answer=(await deps.promptLine(`Connect ${candidateLabel(only)}? [y/N]: `)).trim().toLowerCase();if(answer!=="y"&&answer!=="yes")return deps.stderr(""),deps.stderr("No GitHub connection was made. Nothing was changed."),1;selected=only}else{deps.stderr(""),deps.stderr("Your GitHub installation includes multiple repositories:"),candidates.forEach((c,i)=>{deps.stderr(` ${String(i+1).padStart(2," ")}. ${candidateLabel(c)}`)}),deps.stderr("");let answer=(await deps.promptLine(`Choose a repository [1-${candidates.length}]: `)).trim(),index=Number(answer);if(!/^\d+$/.test(answer)||!Number.isInteger(index)||index<1||index>candidates.length)return deps.stderr(""),deps.stderr("No GitHub connection was made. No repository was selected."),1;selected=candidates[index-1],deps.stderr(`Selected ${candidateLabel(selected)}.`)}renderStep(deps,4);let confirmed=await confirmGithubConnection(api,repoName,minted.value.state,selected.github_repository_id);return confirmed.ok?(deps.stdout(`Connected ${confirmed.value.githubRepoFullName??confirmed.value.githubRepoName}.`),confirmed.value.indexingStatus==="started"?deps.stdout("Indexing started automatically."):confirmed.value.indexingStatus==="waiting-for-setup"&&deps.stdout("Indexing will start automatically once setup completes."),0):reportNoConnection(deps,FAILURE_MESSAGES[confirmed.kind])}async function runConnectGithubCli(argv,injected){let deps=injected??createDefaultConnectGithubDeps();try{let parsed=parseConnectGithubArgs(argv);if(!parsed.ok)return deps.stderr(parsed.error),deps.stderr(""),deps.stderr(USAGE2),1;if(parsed.value.help)return deps.stdout(USAGE2),0;if(!deps.isTTY)return deps.stderr("connect-github needs an interactive terminal: it asks you to confirm which repository to connect. Run it directly in your terminal."),1;let repo=await resolveConnectGithubRepoName(parsed.value,deps);if(!repo.ok)return deps.stderr(repo.error),1;let cred=await resolveBapiCredentials(repo.value,{env:deps.env,homedir:deps.homedir,platform:deps.platform,readFile:deps.readFile,stat:deps.stat,stderr:()=>{}});if(!cred.ok)return deps.stderr(cred.error),1;let api={fetch:deps.fetch,baseUrl:deps.env.BAPI_BASE_URL?.trim()||DEFAULT_BAPI_BASE_URL2,apiKey:cred.credentials.apiKey};return await runGithubConnectionFlow(deps,api,repo.value)}catch{return deps.stderr("No GitHub connection was made. An unexpected error occurred."),1}}init_agent_registry();init_start_tickets();var REDACTED_API_KEY="<REDACTED>",MCP_TIMEOUT_GUIDANCE="Note: if your MCP client has a very short connect deadline, raise MCP_TIMEOUT for the first launch (the initial npx package resolution/download can exceed a short connect timeout).";function buildPrewarmArgs(){return["-y","--prefer-offline",`@bridge_gpt/mcp-server@${VERSION}`,"--version"]}function buildPrewarmCommandPreview(){return`npx ${buildPrewarmArgs().join(" ")}`}var INSTALL_BRIDGE_AGENT_PROMPT="Execute the /install-bridge command in the install-spawn context (tell the command it is running in the install-spawn context so it SKIPS its Stage 8, Stage 9, and Stage 10 offers). The command chooses its own mode from the install manifest's `configured` flag \u2014 do NOT force a mode: when `configured` is false it runs its fresh-configuration flow, and when `configured` is true it runs JOIN MODE. In JOIN MODE the command must NOT derive, approve, apply, or offer any configuration and applies zero fields for any caller; a joining member's only closing interaction is the welcome plus the concise capability report (no /learn-repository prompt for a member). Complete the command's concise capability report: the single 'What Bridge can help with' section, rendered exactly as the server's concise_tool_capabilities field gives it (Regularly useful, then Occasionally useful, each tool's display_name only, plus each tier's server-computed '+N more' where non-zero). Do not render the obsolete five-section report, and do not locally filter, count, or fall back to the complete tool_capabilities catalog. The install-spawn skip set is exactly Stage 8, Stage 9, and Stage 10. The command-owned Stage 11 (invite teammates) is NOT in that skip set: it is independently gated and may run only when the caller's role is admin AND customer_type is b2b AND an interactive response is available; otherwise it is skipped silently. If Stage 11 mints a teammate key, show that plaintext key exactly once and never repeat it in any summary, retry, or diagnostic. Do NOT ask any indexing question, call parse_repository, mention /parse-repository, or claim indexing has started or is pending \u2014 indexing is decided entirely by the server-side readiness funnel with no visibility from this session, so say nothing about it at all. After the capability report on the fresh-configuration path, recommend /learn-repository as the next step: explain briefly that it learns repository-specific architecture, review, testing, correctness, and validation-manual configuration by researching the actual codebase. Do NOT run /learn-repository yourself \u2014 only recommend it; running it is the human's next explicit invocation. Never request, echo, or transport any credential \u2014 only ever direct the human to that integration's own configure_in pointer, verbatim. The pointer is per-integration and is NOT always the setup UI: GitHub's is a terminal command (connect-github), while Jira, SFCC, and Bitbucket point at the setup UI. Follow whatever the report says rather than assuming. On the fresh-configuration path, end with an explicit summary line stating how many config fields the apply_install_manifest call applied (e.g. 'Applied 8 of 9 derived fields') \u2014 if 0 fields were applied, say so loudly and explain what is still pending. In JOIN MODE there is no apply \u2014 state instead that the project was already configured and zero changes were made, without a fabricated applied count.",DEFAULT_BAPI_BASE_URL2="https://bridgegpt-api.com",DEFAULT_BAPI_DOCS_DIR="docs/tmp";function getInstallBridgeUsage(){return["Usage:"," npx -y @bridge_gpt/mcp-server@latest install-bridge [flags]","","One-command Bridge API project bootstrap. Scaffolds the project, writes the","per-host MCP config with your credentials, verifies connectivity, persists the","routing credential, then \u2014 on a TTY, only after a Y/N consent prompt \u2014 opens a","fresh session in your selected tool. If the project is not yet configured it","derives the remaining config, presents a concise capability report, and","recommends /learn-repository. If the project is already configured it instead","joins you to it without proposing or applying any changes and just shows the","concise capability report (a b2b admin is additionally offered a teammate-invite","step). So not every run applies config fields. Indexing is never asked about \u2014","it starts automatically once the repository reaches full parse readiness.","","Run it bare \u2014 `install-bridge` with no flags \u2014 in a terminal and it asks",`\`${INSTALL_BRIDGE_KEY_SELECTOR_PROMPT.trim()}\` first. Answer yes (or press Enter) for the`,"existing-key flow below; answer no and it asks for an email and creates a new","Bridge workspace for you (the self-serve flow). That question is asked ONLY for a","bare interactive run: passing ANY flag, setting BAPI_API_KEY, or running without","an interactive terminal keeps the existing deterministic behavior and no prompt.","","Inputs (the only two irreducible ones):"," --api-key <key> Bridge API key OR bootstrap invite. Falls back to the"," BAPI_API_KEY env var, then an interactive (no-echo) prompt."," Generate a key in the Bridge API web UI Security page \u2014 this"," command consumes a key, it does not create one. A value"," detected as a bootstrap invite (bapi_inv_\u2026), from any of the"," three sources above, is instead redeemed to CREATE a new"," project and its first admin key \u2014 the same as --invite \u2014"," so it skips repository lookup entirely (--email and --invite"," remain the explicit, preferred entry points for a new project)."," NEVER printed or logged."," --repo <name> Repository name. --repo and BAPI_REPO_NAME still take"," priority and short-circuit before any network call. When"," neither is set, a compatible server resolves the unique"," repository from your existing API key automatically; if the"," server is older, the key is unresolvable, or resolution"," fails, it falls back to an inferred default you confirm"," interactively (and to a required --repo when stdin is"," non-interactive). In the existing-key flow it MUST match the"," server-side repo registration (it keys the credential store"," as bapi:<repo>). In either new-project flow (--email,"," --invite, or a negative answer to the key question above) it"," instead NAMES the project this run creates, so you are asked"," to name a new project rather than match an existing one; the"," name must be globally unique.","","Self-serve onboarding (no account, no API key, no pre-issued invite):"," --email <addr> Create a brand-new Bridge workspace from just an email \u2014"," the primary path for a first-time user with nothing yet."," It requests a fresh workspace for that email, then creates"," the project and mints your own admin API key in one command."," Falls back to the BAPI_SIGNUP_EMAIL env var, then a visible"," interactive prompt \u2014 which is also what a negative answer to"," the bare-run key question above reaches. The email is NOT a"," secret (it is shown as you type), but it is never printed to"," a log. Mutually"," exclusive with --api-key and --invite. No email verification"," is performed and no message is sent to the address \u2014 it only"," labels the new workspace.","","Bootstrap-invite onboarding (no web UI, no pre-existing key):"," --invite [token] Redeem a bootstrap invite you were already given: creates"," the project and mints your own admin API key in one command."," Mutually exclusive with --api-key and --email (in this mode"," the key is created, not consumed).",""," Run it WITHOUT a value \u2014 `install-bridge --invite` \u2014 and the"," token is read from an interactive prompt with echo"," suppressed, then sent only in the request body. This is the"," default and the recommended path: 'a copy/paste one-liner'"," and 'the token never touches shell history' are"," contradictory, so the one-liner your operator sends you is"," SECRET-FREE and the CLI asks for the token.",""," Passing the token inline (--invite <token>, --invite=<token>)"," or via BAPI_INVITE is for SCRIPTING ONLY: both forms EXPOSE"," THE TOKEN to your shell history and to the process list.","","Flags:"," --tools <ids> Comma-separated AI-coding tools to configure,"," bypassing the interactive picker. Accepted ids:",` ${HOST_PLATFORM_ORDER.join(", ")}.`," Both --tools=claude-code,codex and"," --tools claude-code,codex are accepted. On an"," interactive terminal WITHOUT this flag you get a"," numbered picker: enter one or more tool numbers,"," comma-separated (e.g. 1,3), and press Enter once \u2014"," no tool is pre-selected (not even Claude Code) and"," you must select at least one. A non-interactive"," (non-TTY) run without"," --tools writes the legacy automatic set (Claude"," Code plus any detected Cursor / Copilot VS Code)"," and never opens an agent terminal (it prints"," manual continuation instead). --tools= (empty) is"," an explicit empty selection: it writes nothing and"," launches nothing."," --force Overwrite an existing real BAPI_API_KEY in a"," host config (or in the credential store) without"," prompting."," --dry-run Preview every step (scaffold targets, config"," files + keys with the key REDACTED, ping"," target, credential target, and the consent-gated"," launch outcome) without writing, pinging,"," prompting, or spawning anything. With --invite it"," also never calls the exchange endpoint and never"," generates or stores a secret."," --agent claude|cursor-agent Explicit launch override for the post-install"," session \u2014 it always wins, and there is NO Claude"," default. Without it the launch tool is derived"," from your selection: Claude Code opens `claude`."," Cursor is NOT auto-opened (its cursor-agent CLI"," first-run workspace-trust prompt collapses a spawned"," session); selecting Cursor writes .cursor/mcp.json"," and prints how to finish by running /install-bridge"," in a new Cursor session. A selection whose tools have"," no agentic CLI (e.g. Copilot) likewise opens nothing"," and prints how to finish configuring later. Passing"," --agent cursor-agent still force-spawns it."," -h, --help Show this help.","","Environment: BAPI_BASE_URL (default https://bridgegpt-api.com) and BAPI_DOCS_DIR","(default docs/tmp) are read from the environment with the shown fallbacks.","BAPI_SIGNUP_EMAIL supplies the self-serve signup email non-interactively (it is","visible input, not a secret). BAPI_INVITE supplies the bootstrap-invite token","non-interactively (scripting only \u2014 it is exposed to shell history; prefer the","prompt)."].join(`
|
|
4781
|
+
`)}}async function resolveConnectGithubRepoName(args,deps){if(args.repo){let validated2=validateRepoName(args.repo);return validated2.ok?{ok:!0,value:validated2.value}:{ok:!1,error:validated2.error}}let inferred=await resolveStartTicketsRepoName({env:deps.env,cwd:deps.cwd,readFile:deps.readFile});if(!inferred){let validated2=validateRepoName(path27.basename(deps.cwd));validated2.ok&&(inferred=validated2.value)}if(!inferred)return{ok:!1,error:"Could not determine the Bridge project. Pass --repo <repo_name>."};let answer=(await deps.promptLine(`Bridge project [${inferred}]: `)).trim(),chosen=answer.length>0?answer:inferred,validated=validateRepoName(chosen);return validated.ok?{ok:!0,value:validated.value}:{ok:!1,error:validated.error}}async function openGithubInstallPage(deps,installUrl){return await deps.openBrowser(installUrl)?{ok:!0}:{ok:!1,error:"Could not open your browser automatically. Re-run this command from a desktop session with a browser available."}}var STEPS=["Connect GitHub","Complete GitHub in browser","Verify connection","Choose repository","Confirm connection"];function renderStep(deps,index){deps.stderr(`[${index+1}/${STEPS.length}] ${STEPS[index]}`)}function candidateLabel(c){return c.github_repo_full_name?c.github_repo_full_name:c.owner?`${c.owner}/${c.github_repo_name}`:c.github_repo_name}var OUTCOME_MESSAGES={expired:"The connection request expired before GitHub reported back. Run connect-github again.",invalid:"This connection request is no longer valid. Run connect-github again.","verification-failed":"Bridge could not verify the GitHub installation. Run connect-github again.","no-repositories":"The GitHub App installation did not include any repositories Bridge can access. Re-run connect-github and grant access to at least one repository.",conflict:"This GitHub installation or project is already connected to a different Bridge account or repository. Contact support if that is unexpected.",failed:"The GitHub connection did not complete. Run connect-github again."},FAILURE_MESSAGES={network:"Could not reach Bridge API. Check your network, then run connect-github again.",timeout:"Bridge API did not respond in time. Run connect-github again.",unauthorized:"Bridge rejected your API key for this project. Re-run install-bridge with a current key.","not-found":"Bridge does not recognize this project. Check --repo matches your Bridge project name.",server:"Bridge API returned an error. Run connect-github again shortly.",malformed:"Bridge API returned an unexpected response. Run connect-github again shortly.",deadline:"Timed out waiting for GitHub. If you completed the install, run connect-github again to pick up the connection."};function reportNoConnection(deps,detail){return deps.stderr(""),deps.stderr("No GitHub connection was made."),deps.stderr(detail),1}async function runGithubConnectionFlow(deps,api,repoName){renderStep(deps,0);let minted=await mintGithubConnection(api,repoName);if(!minted.ok)return reportNoConnection(deps,FAILURE_MESSAGES[minted.kind]);renderStep(deps,1),deps.stderr("Opening GitHub\u2026");let opened=await openGithubInstallPage(deps,minted.value.installUrl);if(!opened.ok)return reportNoConnection(deps,opened.error);renderStep(deps,2);let minutes=Math.floor(POLL_DEADLINE_MS/6e4);deps.stderr(`Waiting for GitHub installation\u2026 (up to ~${minutes} minutes)`);let pollDeps={sleep:deps.sleep,now:deps.now,jitter:deps.jitter},started=deps.now(),polled=await pollGithubConnection(api,pollDeps,repoName,minted.value.state);if(!polled.ok)return reportNoConnection(deps,FAILURE_MESSAGES[polled.kind]);let elapsedSec=Math.max(0,Math.round((deps.now()-started)/1e3));deps.stderr(`Waited ${elapsedSec}s.`);let result=polled.value;if(result.status==="awaiting-organization-approval")return deps.stderr(""),deps.stderr("No GitHub connection was made."),deps.stderr("Your request to install the GitHub App was sent to an organization owner for approval."),deps.stderr("That approval happens on GitHub and does not return here, so this command cannot wait for it."),deps.stderr("Once an owner approves the install, finish the connection with the manual steps in the GitHub App setup guide (docs/install/github-app.md)."),1;if(result.status==="connected")return deps.stdout(`Connected ${result.githubRepoName??repoName}.`),0;if(result.status!=="staged")return reportNoConnection(deps,OUTCOME_MESSAGES[result.status]??OUTCOME_MESSAGES.failed);let candidates=result.candidates;if(candidates.length===0)return reportNoConnection(deps,OUTCOME_MESSAGES["no-repositories"]);renderStep(deps,3);let selected=null;if(candidates.length===1){let only=candidates[0],answer=(await deps.promptLine(`Connect ${candidateLabel(only)}? [y/N]: `)).trim().toLowerCase();if(answer!=="y"&&answer!=="yes")return deps.stderr(""),deps.stderr("No GitHub connection was made. Nothing was changed."),1;selected=only}else{deps.stderr(""),deps.stderr("Your GitHub installation includes multiple repositories:"),candidates.forEach((c,i)=>{deps.stderr(` ${String(i+1).padStart(2," ")}. ${candidateLabel(c)}`)}),deps.stderr("");let answer=(await deps.promptLine(`Choose a repository [1-${candidates.length}]: `)).trim(),index=Number(answer);if(!/^\d+$/.test(answer)||!Number.isInteger(index)||index<1||index>candidates.length)return deps.stderr(""),deps.stderr("No GitHub connection was made. No repository was selected."),1;selected=candidates[index-1],deps.stderr(`Selected ${candidateLabel(selected)}.`)}renderStep(deps,4);let confirmed=await confirmGithubConnection(api,repoName,minted.value.state,selected.github_repository_id);return confirmed.ok?(deps.stdout(`Connected ${confirmed.value.githubRepoFullName??confirmed.value.githubRepoName}.`),confirmed.value.indexingStatus==="started"?deps.stdout("Indexing started automatically."):confirmed.value.indexingStatus==="waiting-for-setup"&&deps.stdout("Indexing will start automatically once setup completes."),0):reportNoConnection(deps,FAILURE_MESSAGES[confirmed.kind])}async function runConnectGithubCli(argv,injected){let deps=injected??createDefaultConnectGithubDeps();try{let parsed=parseConnectGithubArgs(argv);if(!parsed.ok)return deps.stderr(parsed.error),deps.stderr(""),deps.stderr(USAGE2),1;if(parsed.value.help)return deps.stdout(USAGE2),0;if(!deps.isTTY)return deps.stderr("connect-github needs an interactive terminal: it asks you to confirm which repository to connect. Run it directly in your terminal."),1;let repo=await resolveConnectGithubRepoName(parsed.value,deps);if(!repo.ok)return deps.stderr(repo.error),1;let cred=await resolveBapiCredentials(repo.value,{env:deps.env,homedir:deps.homedir,platform:deps.platform,readFile:deps.readFile,stat:deps.stat,stderr:()=>{}});if(!cred.ok)return deps.stderr(cred.error),1;let api={fetch:deps.fetch,baseUrl:deps.env.BAPI_BASE_URL?.trim()||DEFAULT_BAPI_BASE_URL2,apiKey:cred.credentials.apiKey};return await runGithubConnectionFlow(deps,api,repo.value)}catch{return deps.stderr("No GitHub connection was made. An unexpected error occurred."),1}}init_agent_registry();init_start_tickets();var REDACTED_API_KEY="<REDACTED>",MCP_TIMEOUT_GUIDANCE="Note: if your MCP client has a very short connect deadline, raise MCP_TIMEOUT for the first launch (the initial npx package resolution/download can exceed a short connect timeout).";function buildPrewarmArgs(){return["-y","--prefer-offline",`@bridge_gpt/mcp-server@${VERSION}`,"--version"]}function buildPrewarmCommandPreview(){return`npx ${buildPrewarmArgs().join(" ")}`}var INSTALL_BRIDGE_AGENT_PROMPT="Execute the /install-bridge command in the install-spawn context (tell the command it is running in the install-spawn context so it SKIPS its Stage 8, Stage 9, and Stage 10 offers). The command chooses its own mode from the install manifest's `configured` flag \u2014 do NOT force a mode: when `configured` is false it runs its fresh-configuration flow, and when `configured` is true it runs JOIN MODE. In JOIN MODE the command must NOT derive, approve, apply, or offer any configuration and applies zero fields for any caller; a joining member's only closing interaction is the welcome plus the concise capability report (no /learn-repository prompt for a member). Complete the command's concise capability report: the single 'What Bridge can help with' section, rendered exactly as the server's concise_tool_capabilities field gives it (Regularly useful, then Occasionally useful, each tool's display_name only, plus each tier's server-computed '+N more' where non-zero). Do not render the obsolete five-section report, and do not locally filter, count, or fall back to the complete tool_capabilities catalog. The install-spawn skip set is exactly Stage 8, Stage 9, and Stage 10. The command-owned Stage 11 (invite teammates) is NOT in that skip set: it is independently gated and may run only when the caller's role is admin AND customer_type is b2b AND an interactive response is available; otherwise it is skipped silently. If Stage 11 mints a teammate key, show that plaintext key exactly once and never repeat it in any summary, retry, or diagnostic. Do NOT ask any indexing question, call parse_repository, mention /parse-repository, or claim indexing has started or is pending \u2014 indexing is decided entirely by the server-side readiness funnel with no visibility from this session, so say nothing about it at all. After the capability report on the fresh-configuration path, recommend /learn-repository as the next step: explain briefly that it learns repository-specific architecture, review, testing, correctness, and validation-manual configuration by researching the actual codebase. Do NOT run /learn-repository yourself \u2014 only recommend it; running it is the human's next explicit invocation. Never request, echo, or transport any credential \u2014 only ever direct the human to that integration's own configure_in pointer, verbatim. The pointer is per-integration and is NOT always the setup UI: GitHub's is a terminal command (connect-github), while Jira, SFCC, and Bitbucket point at the setup UI. Follow whatever the report says rather than assuming. On the fresh-configuration path, end with an explicit summary line stating how many config fields the apply_install_manifest call applied (e.g. 'Applied 8 of 9 derived fields') \u2014 if 0 fields were applied, say so loudly and explain what is still pending. In JOIN MODE there is no apply \u2014 state instead that the project was already configured and zero changes were made, without a fabricated applied count.",DEFAULT_BAPI_BASE_URL2="https://bridgegpt-api.com";function buildInstallBridgeSetupUrl(baseUrl){return`${baseUrl.replace(/\/+$/,"")}/setup`}var DEFAULT_BAPI_DOCS_DIR="docs/tmp";function getInstallBridgeUsage(baseUrl=DEFAULT_BAPI_BASE_URL2){let setupUrl=buildInstallBridgeSetupUrl(baseUrl);return["Usage:"," npx -y @bridge_gpt/mcp-server@latest install-bridge [flags]","","One-command Bridge API project bootstrap. Scaffolds the project, writes the","per-host MCP config with your credentials, verifies connectivity, persists the","routing credential, then \u2014 on a TTY, only after a Y/N consent prompt \u2014 opens a","fresh session in your selected tool. If the project is not yet configured it","derives the remaining config, presents a concise capability report, and","recommends /learn-repository. If the project is already configured it instead","joins you to it without proposing or applying any changes and just shows the","concise capability report (a b2b admin is additionally offered a teammate-invite","step). So not every run applies config fields. Indexing is never asked about \u2014","it starts automatically once the repository reaches full parse readiness.","","Your API key is written to a project MCP config only when that file is safe: a","valid, git-ignored config gets the real key, but a config already TRACKED by git","gets it only after an explicit default-No confirmation (and never at all in a","non-interactive run) \u2014 otherwise a secret-free entry is written and the server","resolves your key from the credential store at runtime. A config file that","cannot be parsed is left untouched, with manual-merge instructions printed.","","Run it bare \u2014 `install-bridge` with no flags \u2014 in a terminal and it asks",`\`${INSTALL_BRIDGE_KEY_SELECTOR_PROMPT}\` first, with three numbered choices:`,...INSTALL_BRIDGE_ONBOARDING_CHOICES.map(choice=>` ${choice}`),"There is NO default \u2014 pressing Enter selects nothing; you must type 1, 2, or 3","(one blank or invalid answer re-prompts once, then exits with guidance). Option 1","is the existing-key flow below, option 2 is the bootstrap-invite flow, and option 3","prompts for an email and creates a brand-new Bridge workspace for you (the","self-serve flow). That question is asked ONLY for a bare interactive run: passing","ANY flag, setting BAPI_API_KEY, or running without an interactive terminal keeps","the existing deterministic behavior and no prompt.","","Inputs (the only two irreducible ones):"," --api-key <key> Bridge API key OR bootstrap invite. Falls back to the"," BAPI_API_KEY env var, then an interactive (no-echo) prompt.",` Generate a key at ${setupUrl} (Security page) \u2014 this`," command consumes a key, it does not create one. A value"," detected as a bootstrap invite (bapi_inv_\u2026), from any of the"," three sources above, is instead redeemed to CREATE a new"," project and its first admin key \u2014 the same as --invite \u2014"," so it skips repository lookup entirely (--email and --invite"," remain the explicit, preferred entry points for a new project)."," NEVER printed or logged."," --repo <name> Repository name. --repo and BAPI_REPO_NAME still take"," priority and short-circuit before any network call. When"," neither is set, a compatible server resolves the unique"," repository from your existing API key automatically; if the"," server is older, the key is unresolvable, or resolution"," fails, it falls back to an inferred default you confirm"," interactively (and to a required --repo when stdin is"," non-interactive). In the existing-key flow it MUST match the"," server-side repo registration (it keys the credential store"," as bapi:<repo>). In either new-project flow (--email,"," --invite, or chooser option 2 or 3 above) it"," instead NAMES the project this run creates, so you are asked"," to name a new project rather than match an existing one; the"," name must be globally unique.","","Self-serve onboarding (no account, no API key, no pre-issued invite):"," --email <addr> Create a brand-new Bridge workspace from just an email \u2014"," the primary path for a first-time user with nothing yet."," It requests a fresh workspace for that email, then creates"," the project and mints your own admin API key in one command."," Falls back to the BAPI_SIGNUP_EMAIL env var, then a visible"," interactive prompt \u2014 which is also what chooser option 3 on a"," bare run reaches. The email is NOT a"," secret (it is shown as you type), but it is never printed to"," a log. Mutually"," exclusive with --api-key and --invite. No email verification"," is performed and no message is sent to the address \u2014 it only"," labels the new workspace.",""," RESUMABLE: a self-serve run that fails mid-protocol saves its"," signup state under bootstrap-pending:<repo> in the credential"," store. Re-running the self-serve flow RESUMES that attempt \u2014"," it does not sign up again \u2014 so a retry never creates a second"," workspace. Never copy, display, or hand-remove that record; if"," the saved invite has genuinely expired the CLI asks before"," discarding it.","","Bootstrap-invite onboarding (no web UI, no pre-existing key):"," --invite [token] Redeem a bootstrap invite you were already given: creates"," the project and mints your own admin API key in one command."," Mutually exclusive with --api-key and --email (in this mode"," the key is created, not consumed).",""," Run it WITHOUT a value \u2014 `install-bridge --invite` \u2014 and the"," token is read from an interactive prompt with echo"," suppressed, then sent only in the request body. This is the"," default and the recommended path: 'a copy/paste one-liner'"," and 'the token never touches shell history' are"," contradictory, so the one-liner your operator sends you is"," SECRET-FREE and the CLI asks for the token.",""," Passing the token inline (--invite <token>, --invite=<token>)"," or via BAPI_INVITE is for SCRIPTING ONLY: both forms EXPOSE"," THE TOKEN to your shell history and to the process list.","","Flags:"," --tools <ids> Comma-separated AI-coding tools to configure,"," bypassing the interactive picker. Accepted ids:",` ${HOST_PLATFORM_ORDER.join(", ")}.`," Both --tools=claude-code,codex and"," --tools claude-code,codex are accepted. On an"," interactive terminal WITHOUT this flag you get a"," numbered picker: enter one or more tool numbers,"," comma-separated (e.g. 1,3), and press Enter once \u2014"," no tool is pre-selected (not even Claude Code) and"," you must select at least one. A non-interactive"," (non-TTY) run without"," --tools writes the legacy automatic set (Claude"," Code plus any detected Cursor / Copilot VS Code)"," and never opens an agent terminal (it prints"," manual continuation instead). --tools= (empty) is"," an explicit empty selection: it writes nothing and"," launches nothing."," --force Overwrite an existing real BAPI_API_KEY in a"," host config (or in the credential store) without"," prompting. It does NOT bypass the git-tracked"," config safeguard below: --force authorizes"," replacing a credential, not disclosing your key"," into version control."," --dry-run Preview every step (scaffold targets, config"," files + keys with the key REDACTED, ping"," target, credential target, and the consent-gated"," launch outcome) without writing, pinging,"," prompting, or spawning anything. With --invite it"," also never calls the exchange endpoint and never"," generates or stores a secret."," --agent claude|cursor-agent Explicit launch override for the post-install"," session \u2014 it always wins, and there is NO Claude"," default. Without it the launch tool is derived"," from your selection: Claude Code opens `claude`."," Cursor is NOT auto-opened (its cursor-agent CLI"," first-run workspace-trust prompt collapses a spawned"," session); selecting Cursor writes .cursor/mcp.json"," and prints how to finish by running /install-bridge"," in a new Cursor session. A selection whose tools have"," no agentic CLI (e.g. Copilot) likewise opens nothing"," and prints how to finish configuring later. Passing"," --agent cursor-agent still force-spawns it."," -h, --help Show this help.","","Environment: BAPI_BASE_URL (default https://bridgegpt-api.com) and BAPI_DOCS_DIR","(default docs/tmp) are read from the environment with the shown fallbacks.","BAPI_SIGNUP_EMAIL supplies the self-serve signup email non-interactively (it is","visible input, not a secret). BAPI_INVITE supplies the bootstrap-invite token","non-interactively (scripting only \u2014 it is exposed to shell history; prefer the","prompt)."].join(`
|
|
4781
4782
|
`)}function parseInstallBridgeArgs(argv){if(argv.includes("-h")||argv.includes("--help"))return{status:"help",usage:getInstallBridgeUsage()};let apiKey,repo,force=!1,dryRun=!1,agentName,invite,email,tools,inviteSupplied=!1,apiKeySupplied=!1,emailSupplied=!1,readValue=(arg,flag,i)=>arg.startsWith(`${flag}=`)?{value:arg.slice(flag.length+1),nextIndex:i}:i+1>=argv.length?{error:`${flag} requires a value.`}:{value:argv[i+1],nextIndex:i+1};for(let i=0;i<argv.length;i++){let arg=argv[i];if(arg==="--force"){force=!0;continue}if(arg==="--dry-run"){dryRun=!0;continue}if(arg==="--api-key"||arg.startsWith("--api-key=")){let r=readValue(arg,"--api-key",i);if("error"in r)return{status:"error",message:r.error};apiKey=r.value,apiKeySupplied=!0,i=r.nextIndex;continue}if(arg==="--invite"||arg.startsWith("--invite=")){if(inviteSupplied=!0,arg.startsWith("--invite="))invite=arg.slice(9);else{let next=argv[i+1];typeof next=="string"&&!next.startsWith("-")&&(invite=next,i+=1)}continue}if(arg==="--email"||arg.startsWith("--email=")){let r=readValue(arg,"--email",i);if("error"in r)return{status:"error",message:r.error};if(r.value.trim().length===0)return{status:"error",message:"--email requires a non-empty value."};email=r.value.trim(),emailSupplied=!0,i=r.nextIndex;continue}if(arg==="--repo"||arg.startsWith("--repo=")){let r=readValue(arg,"--repo",i);if("error"in r)return{status:"error",message:r.error};repo=r.value,i=r.nextIndex;continue}if(arg==="--tools"||arg.startsWith("--tools=")){let r=readValue(arg,"--tools",i);if("error"in r)return{status:"error",message:r.error};let parsed=parseToolsSelection(r.value);if("error"in parsed)return{status:"error",message:parsed.error};tools=parsed.tools,i=r.nextIndex;continue}if(arg==="--agent"||arg.startsWith("--agent=")){let r=readValue(arg,"--agent",i);if("error"in r)return{status:"error",message:r.error};if(!isAgentName(r.value))return{status:"error",message:`Invalid --agent value: '${r.value}' (allowed agents: ${formatValidAgentNames()}).`};agentName=r.value,i=r.nextIndex;continue}return arg.startsWith("-")?{status:"error",message:`Unknown flag: ${arg}`}:{status:"error",message:`Unexpected positional argument: '${arg}'. install-bridge does not accept positional arguments.`}}return inviteSupplied&&apiKeySupplied?{status:"error",message:"--invite and --api-key are mutually exclusive: a bootstrap invite creates your API key, it does not consume an existing one."}:emailSupplied&&apiKeySupplied?{status:"error",message:"--email and --api-key are mutually exclusive: self-serve signup creates your API key, it does not consume an existing one."}:emailSupplied&&inviteSupplied?{status:"error",message:"--email and --invite are mutually exclusive: use --email for self-serve signup (no pre-issued invite), or --invite to redeem an invite you already have."}:{status:"ok",options:{apiKey,repo,force,dryRun,agentName,invite,inviteMode:inviteSupplied,email,tools}}}function parseToolsSelection(value){let raw=value.split(",").map(s=>s.trim()).filter(s=>s.length>0),seen=new Set;for(let id of raw){if(!isHostPlatformId(id)){let allowed=HOST_PLATFORM_ORDER.join(", ");return{error:`Invalid --tools value: '${id}' (allowed tools: ${allowed}).`}}seen.add(id)}return{tools:HOST_PLATFORM_ORDER.filter(id=>seen.has(id))}}function promptSecretViaReadline(promptText,input=process.stdin,output=process.stderr){return new Promise(resolve2=>{let rl=readline3.createInterface({input,output,terminal:!0}),mutable=rl,muted=!1;mutable._writeToOutput=s=>{muted?s.includes(promptText)&&output.write(promptText):output.write(s)};let answered=!1;rl.on("close",()=>{answered||resolve2("")}),rl.question(promptText,answer=>{answered=!0,rl.close(),output.write(`
|
|
4782
|
-
`),resolve2(answer.trim())}),muted=!0})}async function offerGithubConnection(repoName,deps,log){if(!(!deps.isTTY||!deps.promptLine))try{let credDeps={env:deps.env,homedir:deps.homedir,platform:deps.platform,readFile:deps.readFile,stat:deps.stat,stderr:()=>{}},cred=await resolveBapiCredentials(repoName,credDeps);if(!cred.ok)return;let api={fetch:deps.fetch,baseUrl:deps.env.BAPI_BASE_URL?.trim()||DEFAULT_BAPI_BASE_URL2,apiKey:cred.credentials.apiKey},state=await fetchGithubConfigurationState(api,repoName);if(state==="configured")return;if(state==="unavailable"){log(" note: could not read GitHub configuration status; skipping the GitHub offer.");return}let answer=(await deps.promptLine("Connect GitHub? (Y/n): ")).trim().toLowerCase();if(answer==="n"||answer==="no")return;let connectDeps=createDefaultConnectGithubDeps();await runGithubConnectionFlow(connectDeps,api,repoName)!==0&&log(` note: GitHub was not connected. Your install is complete \u2014 connect GitHub later with 'npx -y @bridge_gpt/mcp-server@latest connect-github --repo ${repoName}'.`)}catch{log(` note: the GitHub connection offer could not run. Your install is complete \u2014 connect GitHub later with 'npx -y @bridge_gpt/mcp-server@latest connect-github --repo ${repoName}'.`)}}function promptLineViaReadline(promptText){return new Promise(resolve2=>{let rl=readline3.createInterface({input:process.stdin,output:process.stderr}),answered=!1;rl.on("close",()=>{answered||resolve2("")}),rl.question(promptText,answer=>{answered=!0,rl.close(),resolve2(answer.trim())})})}function sanitizePrewarmEnv(env){let sanitized={...env};return delete sanitized.BAPI_API_KEY,delete sanitized.BAPI_INVITE,delete sanitized.BAPI_SIGNUP_EMAIL,sanitized}function spawnPrewarmDefault(command,args,env){return new Promise(resolve2=>{let sanitizedEnv=sanitizePrewarmEnv(env);try{let child=spawn7(command,args,{shell:!1,stdio:"ignore",timeout:6e4,env:sanitizedEnv});child.on("error",()=>resolve2({ok:!1,warning:"the pre-warm process could not be started"})),child.on("close",(code,signal)=>{resolve2(signal?{ok:!1,warning:`the pre-warm process timed out or was terminated (${signal})`}:code===0?{ok:!0}:{ok:!1,warning:`the pre-warm process exited with code ${code}`})})}catch{resolve2({ok:!1,warning:"the pre-warm process could not be started"})}})}function createDefaultInstallBridgeDeps(){let isTTY=!!process.stdin.isTTY,productionFetch=(...args)=>fetch(...args);return{env:process.env,cwd:process.cwd(),platform:process.platform,homedir:os14.homedir,isTTY,readFile:p=>readFile11(p,"utf-8"),writeFile:(p,data,options)=>writeFile7(p,data,options),mkdir:(p,options)=>mkdir7(p,options),stat:p=>stat8(p),rename:(a,b)=>rename(a,b),chmod:(p,m)=>chmod(p,m),unlink:p=>unlink(p),open:async(p,flags,mode)=>{let handle=await open(p,flags,mode);return{writeFile:data=>handle.writeFile(data,{encoding:"utf-8"}),sync:()=>handle.sync(),close:()=>handle.close()}},randomBytes:size=>cryptoRandomBytes(size),promptSecret:isTTY?promptSecretViaReadline:void 0,promptLine:isTTY?promptLineViaReadline:void 0,promptMultiSelect:isTTY?promptMultiSelectViaReadline:void 0,vendor:createDefaultVendorProcessDeps(spawn7),fetch:productionFetch,resolveRepoViaServer:(baseUrl,apiKey)=>resolveRepoViaServer(productionFetch,baseUrl,apiKey),spawnPrewarm:spawnPrewarmDefault,runInit,upsertCredential:upsertBapiCredential,prepareBootstrapPending:prepareBootstrapPendingCredential,repointBootstrapPending:repointBootstrapPendingCredential,promoteBootstrapPending:promoteBootstrapPendingCredential,buildShellCommand:buildGenericAgentShellCommand,spawnTerminalTab:getDefaultSpawnTerminalTabForPlatform(process.platform),startTicketsDeps:createDefaultStartTicketsDeps(),log:m=>console.log(m),errorLog:m=>console.error(m)}}async function resolveApiKey(options,deps){if(typeof options.apiKey=="string"&&options.apiKey.trim().length>0)return{ok:!0,value:options.apiKey.trim()};let fromEnv=deps.env.BAPI_API_KEY;if(typeof fromEnv=="string"&&fromEnv.trim().length>0)return{ok:!0,value:fromEnv.trim()};if(deps.isTTY&&deps.promptSecret){let entered=(await deps.promptSecret("Bridge API key or invite (input hidden): ")).trim();return entered.length>0?{ok:!0,value:entered}:{ok:!1,error:"No Bridge API key or invite entered. Pass --api-key, set the BAPI_API_KEY environment variable, or try the hidden prompt again."}}return{ok:!1,error:"A Bridge API key or invite is required. Pass --api-key or set the BAPI_API_KEY environment variable (no interactive terminal is available to prompt for it)."}}async function resolveInviteToken(options,deps){if(typeof options.invite=="string"&&options.invite.trim().length>0)return{ok:!0,value:options.invite.trim()};let fromEnv=deps.env.BAPI_INVITE;if(typeof fromEnv=="string"&&fromEnv.trim().length>0)return{ok:!0,value:fromEnv.trim()};if(deps.isTTY&&deps.promptSecret){let entered=(await deps.promptSecret("Bootstrap invite token (input hidden): ")).trim();return entered.length>0?{ok:!0,value:entered}:{ok:!1,error:"No bootstrap invite token entered."}}return{ok:!1,error:"A bootstrap invite token is required. Pass --invite <token> or set the BAPI_INVITE environment variable (no interactive terminal is available to prompt for it). Note that both forms expose the token to your shell history and process list \u2014 prefer running 'install-bridge --invite' interactively."}}async function resolveSignupEmail(options,deps){if(typeof options.email=="string"&&options.email.trim().length>0)return{ok:!0,value:options.email.trim()};let fromEnv=deps.env.BAPI_SIGNUP_EMAIL;if(typeof fromEnv=="string"&&fromEnv.trim().length>0)return{ok:!0,value:fromEnv.trim()};if(deps.isTTY&&deps.promptLine){let entered=(await deps.promptLine("Email for Bridge workspace setup: ")).trim();return entered.length>0?{ok:!0,value:entered}:{ok:!1,error:"No email entered."}}return{ok:!1,error:"An email is required to create a Bridge workspace. Pass --email <addr> or set the BAPI_SIGNUP_EMAIL environment variable (no interactive terminal is available to prompt for it)."}}function resolveInstallBridgeOnboardingBranch(options,env){return options.inviteMode===!0||(env.BAPI_INVITE??"").trim().length>0?{kind:"need-key",method:"bootstrap-invite"}:(options.email??"").trim().length>0||(env.BAPI_SIGNUP_EMAIL??"").trim().length>0?{kind:"need-key",method:"self-serve"}:{kind:"have-key"}}var INSTALL_BRIDGE_KEY_SELECTOR_PROMPT="Do you have a Bridge API key or invite? [Y/n] ";async function resolveInstallBridgeOnboardingBranchForRun(options,deps,argv){let branch=resolveInstallBridgeOnboardingBranch(options,deps.env);if(branch.kind==="need-key")return{ok:!0,branch};let hasEnvApiKey=(deps.env.BAPI_API_KEY??"").trim().length>0,isBareInvocation=argv.length===0;if(!deps.isTTY||!deps.promptLine||!isBareInvocation||hasEnvApiKey)return{ok:!0,branch};let promptLine=deps.promptLine;try{for(let attempt=0;attempt<5;attempt+=1){let answer=(await promptLine(INSTALL_BRIDGE_KEY_SELECTOR_PROMPT)).trim().toLowerCase();if(answer.length===0||answer==="y"||answer==="yes")return{ok:!0,branch:{kind:"have-key"}};if(answer==="n"||answer==="no")return{ok:!0,branch:{kind:"need-key",method:"self-serve"}};deps.log("Please answer y or n (press Enter for yes).")}return{ok:!1,error:"No valid answer to the Bridge API key question. Re-run and answer y or n."}}catch{return{ok:!1,error:"Could not read your answer from the terminal. Re-run with --api-key <key> if you have a Bridge API key, or --email <addr> to create a new Bridge workspace."}}}function resolveConfiguredRepoName(options,env){if(typeof options.repo=="string"&&options.repo.trim().length>0)return options.repo.trim();let fromEnv=env.BAPI_REPO_NAME;if(typeof fromEnv=="string"&&fromEnv.trim().length>0)return fromEnv.trim()}async function resolveRepoName(options,deps,mode="existing-registration"){let configured=resolveConfiguredRepoName(options,deps.env);if(configured!==void 0)return{ok:!0,value:configured};if(!deps.isTTY||!deps.promptLine)return{ok:!1,error:mode==="new-project"?"A project name is required. Pass --repo or set the BAPI_REPO_NAME environment variable (no interactive terminal is available to confirm an inferred name). It names the new Bridge project this run creates and must be globally unique.":"A repo name is required. Pass --repo or set the BAPI_REPO_NAME environment variable (no interactive terminal is available to confirm an inferred name). It must match the server-side repository registration."};let inferred=await resolveStartTicketsRepoName({env:deps.env,cwd:deps.cwd,readFile:deps.readFile});if(!inferred){let validated=validateRepoName(path28.basename(deps.cwd));validated.ok&&(inferred=validated.value)}if(inferred){let promptText=mode==="new-project"?`Name your new Bridge project [${inferred}]: `:`Repo name [${inferred}] (must match server-side registration): `,answer=(await deps.promptLine(promptText)).trim(),chosen=answer.length>0?answer:inferred;if(chosen.length>0)return{ok:!0,value:chosen}}else{let promptText=mode==="new-project"?"Name your new Bridge project: ":"Repo name (must match server-side registration): ",answer=(await deps.promptLine(promptText)).trim();if(answer.length>0)return{ok:!0,value:answer}}return{ok:!1,error:"No repo name provided."}}var INSTALL_BRIDGE_TOOL_SELECTOR_PROMPT="Which AI coding tools do you use on this project?",SELECTION_TOKEN_PATTERN=/^[1-9][0-9]*$/;function promptMultiSelectViaReadline(promptText,options,input=process.stdin,output=process.stderr){return options.length===0?Promise.resolve([]):new Promise(resolve2=>{let rl=readline3.createInterface({input,output}),settled=!1,finish=result=>{settled||(settled=!0,rl.close(),resolve2(result))};rl.on("close",()=>finish([])),output.write(`
|
|
4783
|
+
`),resolve2(answer.trim())}),muted=!0})}var INSTALL_BRIDGE_CONNECT_GITHUB_COMMAND="npx -y @bridge_gpt/mcp-server connect-github",INSTALL_BRIDGE_GITHUB_OFFER_CONTEXT=["","Step 4b \u2014 optional: connect GitHub."," This installs the Bridge GitHub App so pull requests and code review work. It opens"," github.com in your browser; no GitHub credential is shared with Bridge.",` You can do this later instead: ${INSTALL_BRIDGE_CONNECT_GITHUB_COMMAND}`],INSTALL_BRIDGE_GITHUB_CONNECT_PROMPT="Connect GitHub? [y/N]: ";async function offerGithubConnection(repoName,baseUrl,deps,log){if(!(!deps.isTTY||!deps.promptLine))try{let credDeps={env:deps.env,homedir:deps.homedir,platform:deps.platform,readFile:deps.readFile,stat:deps.stat,stderr:()=>{}},cred=await resolveBapiCredentials(repoName,credDeps);if(!cred.ok)return;let api={fetch:deps.fetch,baseUrl,apiKey:cred.credentials.apiKey},state=await fetchGithubConfigurationState(api,repoName);if(state==="configured")return;if(state==="unavailable"){log(" note: could not read GitHub configuration status; skipping the GitHub offer.");return}for(let line of INSTALL_BRIDGE_GITHUB_OFFER_CONTEXT)log(line);let answer=(await deps.promptLine(INSTALL_BRIDGE_GITHUB_CONNECT_PROMPT)).trim().toLowerCase();if(answer!=="y"&&answer!=="yes")return;let connectDeps=createDefaultConnectGithubDeps();await runGithubConnectionFlow(connectDeps,api,repoName)!==0&&log(` note: GitHub was not connected. Your install is complete \u2014 connect GitHub later with 'npx -y @bridge_gpt/mcp-server@latest connect-github --repo ${repoName}'.`)}catch{log(` note: the GitHub connection offer could not run. Your install is complete \u2014 connect GitHub later with 'npx -y @bridge_gpt/mcp-server@latest connect-github --repo ${repoName}'.`)}}function promptLineViaReadline(promptText){return new Promise(resolve2=>{let rl=readline3.createInterface({input:process.stdin,output:process.stderr}),answered=!1;rl.on("close",()=>{answered||resolve2("")}),rl.question(promptText,answer=>{answered=!0,rl.close(),resolve2(answer.trim())})})}function sanitizePrewarmEnv(env){let sanitized={...env};return delete sanitized.BAPI_API_KEY,delete sanitized.BAPI_INVITE,delete sanitized.BAPI_SIGNUP_EMAIL,sanitized}function spawnPrewarmDefault(command,args,env){return new Promise(resolve2=>{let sanitizedEnv=sanitizePrewarmEnv(env);try{let child=spawn7(command,args,{shell:!1,stdio:"ignore",timeout:6e4,env:sanitizedEnv});child.on("error",()=>resolve2({ok:!1,warning:"the pre-warm process could not be started"})),child.on("close",(code,signal)=>{resolve2(signal?{ok:!1,warning:`the pre-warm process timed out or was terminated (${signal})`}:code===0?{ok:!0}:{ok:!1,warning:`the pre-warm process exited with code ${code}`})})}catch{resolve2({ok:!1,warning:"the pre-warm process could not be started"})}})}function createDefaultInstallBridgeDeps(){let isTTY=!!process.stdin.isTTY,productionFetch=(...args)=>fetch(...args);return{env:process.env,cwd:process.cwd(),platform:process.platform,homedir:os14.homedir,isTTY,readFile:p=>readFile11(p,"utf-8"),writeFile:(p,data,options)=>writeFile7(p,data,options),mkdir:(p,options)=>mkdir7(p,options),stat:p=>stat8(p),rename:(a,b)=>rename(a,b),chmod:(p,m)=>chmod(p,m),unlink:p=>unlink(p),open:async(p,flags,mode)=>{let handle=await open(p,flags,mode);return{writeFile:data=>handle.writeFile(data,{encoding:"utf-8"}),sync:()=>handle.sync(),close:()=>handle.close()}},randomBytes:size=>cryptoRandomBytes(size),promptSecret:isTTY?promptSecretViaReadline:void 0,promptLine:isTTY?promptLineViaReadline:void 0,promptMultiSelect:isTTY?promptMultiSelectViaReadline:void 0,vendor:createDefaultVendorProcessDeps(spawn7),fetch:productionFetch,resolveRepoViaServer:(baseUrl,apiKey)=>resolveRepoViaServer(productionFetch,baseUrl,apiKey),spawnPrewarm:spawnPrewarmDefault,runInit,upsertCredential:upsertBapiCredential,prepareBootstrapPending:prepareBootstrapPendingCredential,repointBootstrapPending:repointBootstrapPendingCredential,promoteBootstrapPending:promoteBootstrapPendingCredential,lookupSelfServeBootstrapPending:lookupSelfServeBootstrapPendingCredential,discardBootstrapPending:discardBootstrapPendingCredential,buildShellCommand:buildGenericAgentShellCommand,spawnTerminalTab:getDefaultSpawnTerminalTabForPlatform(process.platform),startTicketsDeps:createDefaultStartTicketsDeps(),log:m=>console.log(m),errorLog:m=>console.error(m),debugLog:m=>{process.env.BAPI_INSTALL_DEBUG&&console.error(m)}}}async function resolveApiKey(options,deps){if(typeof options.apiKey=="string"&&options.apiKey.trim().length>0)return{ok:!0,value:options.apiKey.trim(),source:"flag"};let fromEnv=deps.env.BAPI_API_KEY;if(typeof fromEnv=="string"&&fromEnv.trim().length>0)return{ok:!0,value:fromEnv.trim(),source:"env"};if(deps.isTTY&&deps.promptSecret){let entered=(await deps.promptSecret("Bridge API key or invite (input hidden): ")).trim();return entered.length>0?{ok:!0,value:entered,source:"prompt"}:{ok:!1,error:`No Bridge API key or invite entered. ${INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE}`}}return{ok:!1,error:`A Bridge API key or invite is required (no interactive terminal is available to prompt for it). ${INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE}`}}async function resolveInviteToken(options,deps){if(typeof options.invite=="string"&&options.invite.trim().length>0)return{ok:!0,value:options.invite.trim()};let fromEnv=deps.env.BAPI_INVITE;if(typeof fromEnv=="string"&&fromEnv.trim().length>0)return{ok:!0,value:fromEnv.trim()};if(deps.isTTY&&deps.promptSecret){let entered=(await deps.promptSecret("Bootstrap invite token (input hidden): ")).trim();return entered.length>0?{ok:!0,value:entered}:{ok:!1,error:"No bootstrap invite token entered."}}return{ok:!1,error:"A bootstrap invite token is required. Pass --invite <token> or set the BAPI_INVITE environment variable (no interactive terminal is available to prompt for it). Note that both forms expose the token to your shell history and process list \u2014 prefer running 'install-bridge --invite' interactively."}}async function resolveSignupEmail(options,deps){if(typeof options.email=="string"&&options.email.trim().length>0)return{ok:!0,value:options.email.trim()};let fromEnv=deps.env.BAPI_SIGNUP_EMAIL;if(typeof fromEnv=="string"&&fromEnv.trim().length>0)return{ok:!0,value:fromEnv.trim()};if(deps.isTTY&&deps.promptLine){let entered=(await deps.promptLine("Email for Bridge workspace setup: ")).trim();return entered.length>0?{ok:!0,value:entered}:{ok:!1,error:"No email entered."}}return{ok:!1,error:"An email is required to create a Bridge workspace. Pass --email <addr> or set the BAPI_SIGNUP_EMAIL environment variable (no interactive terminal is available to prompt for it)."}}function resolveInstallBridgeOnboardingBranch(options,env){return options.inviteMode===!0||(env.BAPI_INVITE??"").trim().length>0?{kind:"need-key",method:"bootstrap-invite"}:(options.email??"").trim().length>0||(env.BAPI_SIGNUP_EMAIL??"").trim().length>0?{kind:"need-key",method:"self-serve"}:{kind:"have-key"}}var INSTALL_BRIDGE_KEY_SELECTOR_PROMPT="How would you like to connect to Bridge API?",INSTALL_BRIDGE_ONBOARDING_CHOICES=["1. I have a Bridge API key","2. I have an invite token","3. I'm new \u2014 set me up with just my email"],INSTALL_BRIDGE_ONBOARDING_CHOICE_PROMPT="Enter 1, 2, or 3: ",INSTALL_BRIDGE_ONBOARDING_CHOICE_HINT="Enter 1, 2, or 3.",INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE="Re-run install-bridge and choose an option: pass --api-key <key> if you have a Bridge API key (or set BAPI_API_KEY), --invite if you were sent an invite token, or --email <addr> to sign up with just an email \u2014 on a bare interactive run, re-run and choose option 3 to sign up with just an email.";async function resolveInstallBridgeOnboardingBranchForRun(options,deps,argv){let branch=resolveInstallBridgeOnboardingBranch(options,deps.env);if(branch.kind==="need-key")return{ok:!0,branch};let hasEnvApiKey=(deps.env.BAPI_API_KEY??"").trim().length>0,isBareInvocation=argv.length===0;if(!deps.isTTY||!deps.promptLine||!isBareInvocation||hasEnvApiKey)return{ok:!0,branch};let promptLine=deps.promptLine;try{deps.log(INSTALL_BRIDGE_KEY_SELECTOR_PROMPT);for(let choice of INSTALL_BRIDGE_ONBOARDING_CHOICES)deps.log(choice);for(let attempt=0;attempt<2;attempt+=1){let answer=(await promptLine(INSTALL_BRIDGE_ONBOARDING_CHOICE_PROMPT)).trim();if(answer==="1")return{ok:!0,branch:{kind:"have-key"}};if(answer==="2")return{ok:!0,branch:{kind:"need-key",method:"bootstrap-invite"}};if(answer==="3")return{ok:!0,branch:{kind:"need-key",method:"self-serve"}};attempt===0&&deps.log(INSTALL_BRIDGE_ONBOARDING_CHOICE_HINT)}return{ok:!1,error:`No option was selected. ${INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE}`}}catch{return{ok:!1,error:`Could not read your answer from the terminal. ${INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE}`}}}function resolveConfiguredRepoName(options,env){if(typeof options.repo=="string"&&options.repo.trim().length>0)return options.repo.trim();let fromEnv=env.BAPI_REPO_NAME;if(typeof fromEnv=="string"&&fromEnv.trim().length>0)return fromEnv.trim()}async function resolveRepoName(options,deps,mode="existing-registration"){let configured=resolveConfiguredRepoName(options,deps.env);if(configured!==void 0)return{ok:!0,value:configured};if(!deps.isTTY||!deps.promptLine)return{ok:!1,error:mode==="new-project"?"A project name is required. Pass --repo or set the BAPI_REPO_NAME environment variable (no interactive terminal is available to confirm an inferred name). It names the new Bridge project this run creates and must be globally unique.":"A repo name is required. Pass --repo or set the BAPI_REPO_NAME environment variable (no interactive terminal is available to confirm an inferred name). It must match the server-side repository registration."};let inferred=await resolveStartTicketsRepoName({env:deps.env,cwd:deps.cwd,readFile:deps.readFile});if(!inferred){let validated=validateRepoName(path28.basename(deps.cwd));validated.ok&&(inferred=validated.value)}if(inferred){let promptText=mode==="new-project"?`Name your new Bridge project [${inferred}]: `:`Repo name [${inferred}] (must match server-side registration): `,answer=(await deps.promptLine(promptText)).trim(),chosen=answer.length>0?answer:inferred;if(chosen.length>0)return{ok:!0,value:chosen}}else{let promptText=mode==="new-project"?"Name your new Bridge project: ":"Repo name (must match server-side registration): ",answer=(await deps.promptLine(promptText)).trim();if(answer.length>0)return{ok:!0,value:answer}}return{ok:!1,error:"No repo name provided."}}var INSTALL_BRIDGE_TOOL_SELECTOR_PROMPT="Which AI coding tools do you use on this project?",SELECTION_TOKEN_PATTERN=/^[1-9][0-9]*$/;function promptMultiSelectViaReadline(promptText,options,input=process.stdin,output=process.stderr){return options.length===0?Promise.resolve([]):new Promise(resolve2=>{let rl=readline3.createInterface({input,output}),settled=!1,finish=result=>{settled||(settled=!0,rl.close(),resolve2(result))};rl.on("close",()=>finish([])),output.write(`
|
|
4783
4784
|
${promptText}
|
|
4784
4785
|
`),options.forEach((opt,idx)=>{output.write(` ${idx+1}. ${opt.label}
|
|
4785
4786
|
`)}),output.write(`Enter the number(s) of the tools you use (e.g. 1,3), then Enter.
|
|
4786
4787
|
`);let prompt=()=>{rl.question("> ",answer=>{let trimmed=answer.trim();if(trimmed.length===0){output.write(`Select at least one tool.
|
|
4787
4788
|
`),prompt();return}let tokens=trimmed.split(",").map(t=>t.trim()),indices=[];for(let tok of tokens){if(!SELECTION_TOKEN_PATTERN.test(tok)){output.write(`Invalid selection. Enter numbers between 1 and ${options.length}.
|
|
4788
4789
|
`),prompt();return}let n=Number(tok);if(n<1||n>options.length){output.write(`Invalid selection. Enter numbers between 1 and ${options.length}.
|
|
4789
|
-
`),prompt();return}indices.push(n)}let chosen=new Set(indices);finish(options.filter((_,idx)=>chosen.has(idx+1)).map(o=>o.id))})};prompt()})}async function resolveSelectedHostPlatforms(deps,options){if(options.tools!==void 0)return options.tools;let ctx=await buildDetectionContext(deps),detected=new Set(detectDefaultPlatforms(ctx));if(deps.isTTY&&deps.promptMultiSelect){let optionList=allHostTargets().map(t=>({id:t.id,label:t.label})),chosen=await deps.promptMultiSelect(INSTALL_BRIDGE_TOOL_SELECTOR_PROMPT,optionList);return HOST_PLATFORM_ORDER.filter(id=>chosen.includes(id))}let legacy=["claude-code"];return detected.has("cursor")&&legacy.push("cursor"),detected.has("copilot-vscode")&&legacy.push("copilot-vscode"),HOST_PLATFORM_ORDER.filter(id=>legacy.includes(id))}var SELECTION_NON_LAUNCHABLE_AGENTS=new Set(["cursor-agent"]);function resolveInstallBridgeLaunchDecision(selectedPlatforms,explicitAgent){if(explicitAgent)return{kind:"spawn",agent:explicitAgent};if(selectedPlatforms.length===0)return{kind:"manual",reason:"empty-selection"};let agents=[];for(let id of HOST_PLATFORM_ORDER){if(!selectedPlatforms.includes(id))continue;let agent=agentForPlatform(id);agent&&!SELECTION_NON_LAUNCHABLE_AGENTS.has(agent)&&!agents.includes(agent)&&agents.push(agent)}return agents.length===0?{kind:"manual",reason:"no-launchable-agent"}:agents.length===1?{kind:"spawn",agent:agents[0]}:{kind:"choose-one",agents}}function toolLabelForLaunchAgent(agent){return allHostTargets().find(t=>t.launchAgent===agent)?.label??
|
|
4790
|
+
`),prompt();return}indices.push(n)}let chosen=new Set(indices);finish(options.filter((_,idx)=>chosen.has(idx+1)).map(o=>o.id))})};prompt()})}async function resolveSelectedHostPlatforms(deps,options){if(options.tools!==void 0)return options.tools;let ctx=await buildDetectionContext(deps),detected=new Set(detectDefaultPlatforms(ctx));if(deps.isTTY&&deps.promptMultiSelect){let optionList=allHostTargets().map(t=>({id:t.id,label:t.label})),chosen=await deps.promptMultiSelect(INSTALL_BRIDGE_TOOL_SELECTOR_PROMPT,optionList);return HOST_PLATFORM_ORDER.filter(id=>chosen.includes(id))}let legacy=["claude-code"];return detected.has("cursor")&&legacy.push("cursor"),detected.has("copilot-vscode")&&legacy.push("copilot-vscode"),HOST_PLATFORM_ORDER.filter(id=>legacy.includes(id))}var SELECTION_NON_LAUNCHABLE_AGENTS=new Set(["cursor-agent"]);function resolveInstallBridgeLaunchDecision(selectedPlatforms,explicitAgent){if(explicitAgent)return{kind:"spawn",agent:explicitAgent};if(selectedPlatforms.length===0)return{kind:"manual",reason:"empty-selection"};let agents=[];for(let id of HOST_PLATFORM_ORDER){if(!selectedPlatforms.includes(id))continue;let agent=agentForPlatform(id);agent&&!SELECTION_NON_LAUNCHABLE_AGENTS.has(agent)&&!agents.includes(agent)&&agents.push(agent)}return agents.length===0?{kind:"manual",reason:"no-launchable-agent"}:agents.length===1?{kind:"spawn",agent:agents[0]}:{kind:"choose-one",agents}}var UNKNOWN_LAUNCH_TOOL_LABEL="your AI coding tool";function toolLabelForLaunchAgent(agent){return allHostTargets().find(t=>t.launchAgent===agent)?.label??UNKNOWN_LAUNCH_TOOL_LABEL}async function chooseInstallBridgeLaunchAgent(agents,deps){if(!deps.isTTY||!deps.promptLine)return null;let promptLine=deps.promptLine;try{deps.log(""),deps.log("More than one selected tool can host the configuration session:"),agents.forEach((agent,i)=>{deps.log(` ${String(i+1).padStart(2," ")}. ${toolLabelForLaunchAgent(agent)}`)});let answer=(await promptLine(`Which tool should open? [1-${agents.length}]: `)).trim(),index=Number(answer);return!/^\d+$/.test(answer)||!Number.isInteger(index)||index<1||index>agents.length?null:agents[index-1]}catch{return null}}function buildManualInstallBridgeContinuation(kind,toolLabels){return kind==="empty-selection"?["No AI coding tools were configured, so nothing was set up for this project.","Re-run install-bridge and select at least one tool to configure it."].join(`
|
|
4790
4791
|
`):[`To finish configuring this project, open it in ${formatToolLabelPhrase(toolLabels)} that has the`,"Bridge MCP server configured, start a new session, and run /install-bridge.","Until the project is configured, your Bridge MCP tools stay limited."].join(`
|
|
4791
|
-
`)}function formatToolLabelPhrase(labels){return labels.length===0?"an AI coding tool":labels.length===1?labels[0]:labels.length===2?`${labels[0]} and ${labels[1]}`:`${labels.slice(0,-1).join(", ")}, and ${labels[labels.length-1]}`}var INSTALL_BRIDGE_LAUNCH_CONSENT_PROMPT_PREFIX="Bridge can configure and set up this project for you automatically. Open a ";async function requestInstallBridgeLaunchConsent(toolLabel,deps){if(!deps.isTTY||!deps.promptLine)return"no-spawn";try{let answer=(await deps.promptLine(`${INSTALL_BRIDGE_LAUNCH_CONSENT_PROMPT_PREFIX}${toolLabel} session to do that now? (Y/n) `)).trim().toLowerCase();return answer==="n"||answer==="no"?"no-spawn":"spawn"}catch{return"no-spawn"}}async function buildDetectionContext(deps){let cwd=deps.cwd,homedir=deps.homedir(),posixJoin=(base,rel)=>`${base.endsWith("/")?base.slice(0,-1):base}/${rel}`,candidates=[posixJoin(cwd,".cursor"),posixJoin(cwd,".vscode"),posixJoin(cwd,".windsurf"),posixJoin(cwd,".windsurfrules"),posixJoin(homedir,".codex")],present=new Set;return await Promise.all(candidates.map(async p=>{try{await deps.stat(p),present.add(p)}catch{}})),{cwd,homedir,env:deps.env,exists:p=>present.has(p)}}function hostConfigTargetsForPlatforms(platforms){let set=new Set(platforms);return HOST_PLATFORM_ORDER.filter(id=>set.has(id)).map(id=>MCP_HOST_TARGETS[id]).filter(t=>t.scope==="project"&&t.format==="json").map(t=>({relPath:t.relPath,topLevelKey:t.topLevelKey}))}function labelsForPlatforms(platforms){let set=new Set(platforms);return HOST_PLATFORM_ORDER.filter(id=>set.has(id)).map(id=>MCP_HOST_TARGETS[id].label)}function isPlaceholderApiKey(value){if(typeof value!="string")return!0;let trimmed=value.trim();return trimmed.length===0?!0:trimmed==="YOUR_API_KEY"||trimmed.startsWith("YOUR_")}function buildInstallBridgeServerEntry(cwd,repoName,apiKey,baseUrl,docsDir){let entry=buildBridgeApiEntry(cwd),env={...entry.env,BAPI_REPO_NAME:repoName,BAPI_BASE_URL:baseUrl,BAPI_DOCS_DIR:docsDir,BAPI_API_KEY:apiKey};return{command:entry.command,args:entry.args,env}}async function readHostConfig(deps,fullPath){let raw;try{raw=await deps.readFile(fullPath)}catch{return
|
|
4792
|
-
`,{encoding:"utf-8"}),written.push(target.relPath)}return
|
|
4793
|
-
`),BOOTSTRAP_INVITE_REJECTED_MESSAGE="The Bridge API rejected the bootstrap invite (HTTP 401). The invite is invalid, expired, or revoked \u2014 ask your Bridge API operator for a new one. (Your locally-stored secret was sent unchanged, so this is not a lost-secret problem.)";function buildDryRunPreview(plan){return plan.bootstrapInvite?buildBootstrapDryRunPreview(plan):[plan.attemptedServerResolution?"install-bridge --dry-run (one read-only repository-resolution GET may already have occurred; no writes, no state-changing requests, no spawns)":"install-bridge --dry-run (no writes, no network, no spawns)",`Repo name: ${plan.repoName}${plan.attemptedServerResolution?" (resolved server-side from your API key)":""}`,`Base URL (ping): ${plan.baseUrl}`,`Docs dir: ${plan.docsDir}`,`Agent: ${describePlannedLaunchAgent(plan.launch)}`,"","Step 1 \u2014 scaffold (runInit): commands, agents, pipelines, .bridge/config, secret-free MCP placeholders.",`Step 2 \u2014 connectivity ping (before any durable key write): GET ${plan.pingUrl} (X-API-Key: ${REDACTED_API_KEY})`,"Step 3 \u2014 write per-host MCP config (read-merge-write, launcher version-pinned):",...plan.configTargets.map(t=>` ${t}: BAPI_REPO_NAME=${plan.repoName}, BAPI_API_KEY=${REDACTED_API_KEY}, BAPI_BASE_URL=${plan.baseUrl}, BAPI_DOCS_DIR=${plan.docsDir}`),...plan.manualEditors.length>0?[` ${plan.manualEditors.join(" + ")}: detected (global config) \u2014 manual setup instructions would be printed.`]:[],`Step 3b \u2014 pre-warm the version-pinned launcher bucket (fail-open, env sanitized \u2014 BAPI_API_KEY removed): ${plan.prewarmCommand}`,MCP_TIMEOUT_GUIDANCE,`Step 4 \u2014 persist routing credential: target ${plan.credentialTarget} at ${plan.credentialStorePath}`,...buildLaunchStepPreview(plan)]}function describePlannedLaunchAgent(launch){return launch.kind==="spawn"?`${toolLabelForLaunchAgent(launch.agent)} (${launch.agent}) \u2014 opens only after Y/N consent`:launch.kind==="choose-one"?`choose one of ${launch.agents.map(a=>toolLabelForLaunchAgent(a)).join(", ")} \u2014 the chosen tool opens only after Y/N consent`:launch.reason==="empty-selection"?"none \u2014 no tools selected":"none \u2014 no launchable tool selected; manual continuation is printed"}function buildLaunchStepPreview(plan){let githubLines=["Step 4b \u2014 optional GitHub connect (SKIPPED in --dry-run): read GitHub's configured state"," via the install manifest and, only when it is unconfigured and the terminal is"
|
|
4794
|
-
`)}async function runInstallBridgeCli(argv,overrides={}){let deps={...createDefaultInstallBridgeDeps(),...overrides};overrides.resolveRepoViaServer||(deps.resolveRepoViaServer=(baseUrl2,apiKey2)=>resolveRepoViaServer(deps.fetch,baseUrl2,apiKey2));let{log,errorLog}=deps,parsed=parseInstallBridgeArgs(argv);if(parsed.status==="help")return log(parsed.usage),0;if(parsed.status==="error")return errorLog(`Error: ${parsed.message}`),errorLog(""),errorLog(getInstallBridgeUsage()),1;let options=parsed.options,branchResult=await resolveInstallBridgeOnboardingBranchForRun(options,deps,argv);if(!branchResult.ok)return errorLog(`Error: ${branchResult.error}`),1;let branch=branchResult.branch,bootstrapInviteMode=branch.kind==="need-key",selfServeSignupMode=branch.kind==="need-key"&&branch.method==="self-serve",apiKey="",inviteToken="",signupEmail="";if(selfServeSignupMode){let emailResult=await resolveSignupEmail(options,deps);if(!emailResult.ok)return errorLog(`Error: ${emailResult.error}`),1;signupEmail=emailResult.value}else if(bootstrapInviteMode){let inviteResult=await resolveInviteToken(options,deps);if(!inviteResult.ok)return errorLog(`Error: ${inviteResult.error}`),1;inviteToken=inviteResult.value}else{let keyResult=await resolveApiKey(options,deps);if(!keyResult.ok)return errorLog(`Error: ${keyResult.error}`),1;classifyEnteredCredential(keyResult.value)==="invite"?(inviteToken=keyResult.value.trim(),apiKey="",bootstrapInviteMode=!0,selfServeSignupMode=!1):apiKey=keyResult.value}let baseUrl=deps.env.BAPI_BASE_URL??DEFAULT_BAPI_BASE_URL2,docsDir=deps.env.BAPI_DOCS_DIR??DEFAULT_BAPI_DOCS_DIR,repoName,attemptedServerResolution=!1;if(bootstrapInviteMode){let repoResult=await resolveRepoName(options,deps,"new-project");if(!repoResult.ok)return errorLog(`Error: ${repoResult.error}`),1;let validated=validateRepoName(repoResult.value);if(!validated.ok)return errorLog(`Error: invalid repo name \u2014 ${validated.error}.`),1;repoName=validated.value}else{let configured=resolveConfiguredRepoName(options,deps.env);if(configured!==void 0)repoName=configured;else{attemptedServerResolution=!0,log("Resolving repository\u2026");let resolution=await deps.resolveRepoViaServer(baseUrl,apiKey);if(resolution.status==="resolved")repoName=resolution.repoName;else{log("Couldn't auto-resolve your repo from the key; falling back to a guessed name \u2014 confirm it matches the setup UI.");let repoResult=await resolveRepoName(options,deps,"existing-registration");if(!repoResult.ok)return errorLog(`Error: ${repoResult.error}`),1;repoName=repoResult.value}}}let credentialStorePath=getPrimaryCredentialStorePath({env:deps.env,homedir:deps.homedir}),selectedPlatforms=await resolveSelectedHostPlatforms(deps,options),targets=hostConfigTargetsForPlatforms(selectedPlatforms),manualEditors=await detectManualEditors(deps),launchDecision=resolveInstallBridgeLaunchDecision(selectedPlatforms,options.agentName),planLaunch;if(launchDecision.kind==="spawn"){let spec=resolveAgentSpec(launchDecision.agent);if(!spec)return errorLog(`Error: no launch agent is registered for '${launchDecision.agent}'.`),1;planLaunch={kind:"spawn",agent:launchDecision.agent,spawnCommand:deps.buildShellCommand(spec,INSTALL_BRIDGE_AGENT_PROMPT,deps.cwd,deps.platform)}}else launchDecision.kind==="choose-one"?planLaunch={kind:"choose-one",agents:launchDecision.agents}:planLaunch={kind:"manual",reason:launchDecision.reason};let plan={repoName,baseUrl,docsDir,launch:planLaunch,configTargets:targets.map(t=>t.relPath),manualEditors:manualEditorNames(manualEditors),credentialTarget:`bapi:${repoName}`,credentialStorePath,pingUrl:buildPingUrl(baseUrl,repoName),prewarmCommand:buildPrewarmCommandPreview(),...bootstrapInviteMode?{bootstrapInvite:!0,exchangeUrl:buildBootstrapExchangeUrl(baseUrl)}:{},...selfServeSignupMode?{selfServeSignup:!0}:{},...attemptedServerResolution?{attemptedServerResolution:!0}:{}};if(options.dryRun){for(let line of buildDryRunPreview(plan))log(line);return 0}if(planLaunch.kind==="manual"&&planLaunch.reason==="empty-selection")return log(buildManualInstallBridgeContinuation("empty-selection",[])),0;let finalAgentName=null,finalSpawnCommand=null;if(planLaunch.kind==="spawn")finalAgentName=planLaunch.agent,finalSpawnCommand=planLaunch.spawnCommand;else if(planLaunch.kind==="choose-one"){let chosen=await chooseInstallBridgeLaunchAgent(planLaunch.agents,deps);if(chosen){let spec=resolveAgentSpec(chosen);if(!spec)return errorLog(`Error: no launch agent is registered for '${chosen}'.`),1;finalAgentName=chosen,finalSpawnCommand=deps.buildShellCommand(spec,INSTALL_BRIDGE_AGENT_PROMPT,deps.cwd,deps.platform)}}let launchCommand=null,prewarmPromise=null;if(finalAgentName&&finalSpawnCommand){let materialized=await materializeWorkerLaunchCommand(deps.startTicketsDeps,"install",finalSpawnCommand);if(!materialized.ok)return errorLog(`Error: ${materialized.error}`),1;if(launchCommand=materialized.command,Buffer.byteLength(launchCommand,"utf8")>=MAX_TERMINAL_COMMAND_BYTES)return errorLog("Error: the agent session command is too long to send to the terminal safely. Check that the system temporary directory is writable so the launch script can be used."),1;log(" pre-warming the version-pinned launcher bucket (in the background)\u2026"),prewarmPromise=deps.spawnPrewarm("npx",buildPrewarmArgs(),deps.env)}let credentialWriteDeps={env:deps.env,homedir:deps.homedir,platform:deps.platform,readFile:deps.readFile,mkdir:deps.mkdir,writeFile:(p,d,o)=>deps.writeFile(p,d,o),rename:deps.rename,chmod:deps.chmod,unlink:deps.unlink,open:deps.open},hasRealKey=await detectExistingRealKey(deps,targets),overwriteConsent=options.force;if(hasRealKey&&!options.force)if(deps.isTTY&&deps.promptLine){let answer=(await deps.promptLine("A host config already contains a BAPI_API_KEY. Overwrite it? [y/N]: ")).trim().toLowerCase();if(answer!=="y"&&answer!=="yes")return errorLog("Aborted: existing API key left unchanged (re-run with --force to overwrite)."),1;overwriteConsent=!0}else return errorLog("Error: a host config already contains a BAPI_API_KEY. Re-run with --force to overwrite it (refusing to overwrite a credential non-interactively without consent)."),1;log("Step 1/5 \u2014 scaffolding project (commands, agents, pipelines, config placeholders)\u2026"),await deps.runInit(deps.cwd);let inviteFingerprint="";if(bootstrapInviteMode){if(selfServeSignupMode){log("Step 2/5 \u2014 requesting Bridge self-serve setup\u2026");let mint=await mintSelfServeInvite(deps,baseUrl,signupEmail);if(!mint.ok)return mint.category==="rate-limited"?errorLog("Error: Self-serve setup is temporarily rate limited. Try again later."):mint.category==="invalid"?errorLog("Error: Self-serve setup could not be requested. Check the email value and try again."):errorLog("Error: Unable to complete self-serve setup. Check connectivity and retry."),1;inviteToken=mint.token}inviteFingerprint=fingerprintBootstrapInvite(inviteToken),log("Step 2/5 \u2014 redeeming the bootstrap invite\u2026");let prepared=await deps.prepareBootstrapPending({repoName,inviteFingerprint,generateKeySecret:()=>generateBootstrapKeySecret(deps.randomBytes),allowOverwriteExistingCredential:overwriteConsent},credentialWriteDeps);if(!prepared.ok&&prepared.kind==="credential-conflict")if(deps.isTTY&&deps.promptLine){let answer=(await deps.promptLine(`The credential store already holds a key for ${prepared.target}. Overwrite it? [y/N]: `)).trim().toLowerCase();if(answer!=="y"&&answer!=="yes")return errorLog("Aborted: existing credential left unchanged (re-run with --force to overwrite)."),1;overwriteConsent=!0,prepared=await deps.prepareBootstrapPending({repoName,inviteFingerprint,generateKeySecret:()=>generateBootstrapKeySecret(deps.randomBytes),allowOverwriteExistingCredential:!0},credentialWriteDeps)}else return errorLog(`Error: ${prepared.error} Re-run with --force to overwrite it (refusing to overwrite a credential non-interactively without consent).`),1;if(!prepared.ok&&prepared.kind==="pending-conflict")return errorLog(`Error: ${prepared.error} This invite has NOT been used, and re-running will not clear the conflict.`),1;if(!prepared.ok)return errorLog(`Error: could not durably store the bootstrap credential (${prepared.kind}). ${prepared.error} The bootstrap invite has NOT been used \u2014 fix the problem and re-run.`),1;let keySecret=prepared.keySecret,reusedPendingSecret=prepared.reused;log(` saved the pending credential for ${prepared.target} (fsynced before the exchange)`);let exchange=await exchangeBootstrapInvite(deps,baseUrl,inviteToken,repoName,keySecret);for(;!exchange.ok&&exchange.kind==="repo-name-taken";){if(!deps.isTTY||!deps.promptLine)return errorLog(`Error: ${exchange.message} Re-run with a different --repo (the bootstrap invite has NOT been used).`),1;errorLog(exchange.message);let answer=(await deps.promptLine("Choose a different repo name: ")).trim(),validated=validateRepoName(answer);if(!validated.ok)return errorLog(`Error: invalid repo name \u2014 ${validated.error}.`),1;let nextRepo=validated.value,repointed=await deps.repointBootstrapPending({fromRepoName:repoName,toRepoName:nextRepo,inviteFingerprint,allowOverwriteExistingCredential:overwriteConsent},credentialWriteDeps);if(!repointed.ok)return errorLog(`Error: could not re-point the pending bootstrap credential to '${nextRepo}' (${repointed.kind}). ${repointed.error} The bootstrap invite has NOT been used.`),1;repoName=nextRepo,exchange=await exchangeBootstrapInvite(deps,baseUrl,inviteToken,repoName,keySecret)}if(!exchange.ok)return exchange.kind==="invalid-invite"?errorLog(reusedPendingSecret?BOOTSTRAP_INVITE_REJECTED_MESSAGE:BOOTSTRAP_INVITE_LOST_SECRET_MESSAGE):errorLog(`Error: ${exchange.message}`),1;if(exchange.repoName!==repoName){let repointed=await deps.repointBootstrapPending({fromRepoName:repoName,toRepoName:exchange.repoName,inviteFingerprint,allowOverwriteExistingCredential:overwriteConsent},credentialWriteDeps);if(!repointed.ok)return errorLog(`Error: the project was created as '${exchange.repoName}' but the pending credential could not be re-pointed to it (${repointed.kind}). ${repointed.error}`),1;repoName=exchange.repoName}log(` bootstrap invite redeemed \u2014 project '${repoName}' is ready`),apiKey=keySecret}bootstrapInviteMode||log("Step 2/5 \u2014 verifying connectivity\u2026");let ping=await verifyConnectivity(deps,baseUrl,repoName,apiKey);if(!ping.ok)return errorLog(`Error: ${ping.message}`),1;log(" connectivity OK");let entry=buildInstallBridgeServerEntry(deps.cwd,repoName,apiKey,baseUrl,docsDir);log("Step 3/5 \u2014 writing per-host MCP config\u2026");let gitignoreDeps={readFile:deps.readFile,writeFile:(p,data)=>deps.writeFile(p,data,{encoding:"utf-8"}),mkdir:(p,o)=>deps.mkdir(p,o)};for(let target of targets)try{await ensureGitignored(deps.cwd,target.relPath,gitignoreDeps)}catch{return errorLog("Error: could not add a project MCP config to .gitignore before writing your key. Aborting so the API key is never written to an un-ignored file."),1}let written=await writeHostConfigs(deps,targets,entry);for(let relPath of written)log(` wrote ${relPath}`);let globalLogLines=await provisionSelectedGlobalTargets(deps,selectedPlatforms,entry);for(let line of globalLogLines)log(line);let legacyManualEditors={windsurf:manualEditors.windsurf&&!selectedPlatforms.includes("windsurf"),codex:manualEditors.codex&&!selectedPlatforms.includes("codex")},manualInstructions=buildManualHostInstructions(entry,legacyManualEditors);manualInstructions&&log(manualInstructions),selectedPlatforms.includes("claude-code")&&selectedPlatforms.includes("copilot-cli")&&log(" Note: Claude Code uses the project .mcp.json while GitHub Copilot CLI uses only its global ~/.copilot/mcp-config.json \u2014 the two are configured separately."),selectedPlatforms.includes("claude-code")&&log(" Claude Code: the project MCP server is pending approval in Claude Code's trust dialog; restart or reload an already-running session for it to take effect.");try{await ensureGitignored(deps.cwd,".bridge/install-state.json",gitignoreDeps),(await writeMcpInstallState(deps.cwd,{selectedPlatforms,projectConfigPaths:targets.map(t=>t.relPath)},{readFile:deps.readFile,writeFile:(p,data)=>deps.writeFile(p,data,{encoding:"utf-8"}),rename:deps.rename,mkdir:async(p,o)=>{await deps.mkdir(p,o)},unlink:deps.unlink})).ok||errorLog("Warning: could not persist the install-state file (non-fatal).")}catch{errorLog("Warning: could not persist the install-state file (non-fatal).")}if(bootstrapInviteMode){log("Step 4/5 \u2014 promoting the bootstrap credential\u2026");let promoted=await deps.promoteBootstrapPending({repoName,inviteFingerprint,allowOverwriteExistingCredential:overwriteConsent},credentialWriteDeps);if(!promoted.ok)return errorLog(`Error: the project and API key were created, but the credential could not be stored (${promoted.kind}). ${promoted.error} Your key is still saved locally as a pending record \u2014 re-run install-bridge with the same bootstrap invite to finish (the redemption will replay and return the same key).`),1;log(` stored routing credential for ${promoted.target} at ${promoted.path}`)}else{log("Step 4/5 \u2014 persisting routing credential\u2026");try{let result=await deps.upsertCredential(repoName,apiKey,credentialWriteDeps);result.ok?log(` stored routing credential for ${result.target} at ${result.path}`):log(` warning: could not persist the routing credential (${result.kind}). start-tickets model routing may not resolve the key for bapi:${repoName} and will fail open to the premium/Opus tier (the most expensive) \u2014 set BAPI_API_KEY in the shell or re-run install-bridge, then verify with 'npx -y @bridge_gpt/mcp-server doctor'.`)}catch{log(" warning: could not persist the routing credential (unexpected error). start-tickets model routing may need BAPI_API_KEY in the shell and will fail open to the premium/Opus tier (the most expensive) until fixed \u2014 verify with 'npx -y @bridge_gpt/mcp-server doctor'.")}}if(await offerGithubConnection(repoName,deps,log),prewarmPromise){let prewarm=await prewarmPromise;prewarm.ok?(log(" launcher bucket warmed (the first MCP launch will not pay a cold install)."),log(` ${MCP_TIMEOUT_GUIDANCE}`)):errorLog(`Warning: could not pre-warm the version-pinned launcher bucket${prewarm.warning?` (${prewarm.warning})`:""}. The first MCP launch may pay a one-time cold install and could be slow. ${MCP_TIMEOUT_GUIDANCE}`)}if(finalAgentName&&launchCommand){if(await requestInstallBridgeLaunchConsent(toolLabelForLaunchAgent(finalAgentName),deps)==="spawn"){log(`Step 5/5 \u2014 opening a ${finalAgentName} session for /install-bridge configuration + concise capability report\u2026`);let terminal=detectTerminal(void 0,deps.env),spawnResult=await deps.spawnTerminalTab(deps.startTicketsDeps,terminal,launchCommand,{key:"install",worktreePath:deps.cwd,title:"Bridge Install"});return spawnResult.ok?(log(""),log(`install-bridge setup steps complete. A fresh ${finalAgentName} session is now applying configuration, presenting the concise capability report, and recommending /learn-repository.`),log("NOTE: the install is not finished until that session's apply reports applied fields \u2014 it will pause to ask you to approve the project description. Indexing starts automatically once the repository reaches full parse readiness \u2014 there is no indexing question to answer. Verify afterwards on the project's Get Started page (install status panel) or via the session's 'Applied N of M' summary."),0):(errorLog(`Warning: setup steps completed, but the agent session could not be opened (${spawnResult.error}).`),log(buildManualInstallBridgeContinuation("configured",labelsForPlatforms(selectedPlatforms))),0)}return log(buildManualInstallBridgeContinuation("configured",labelsForPlatforms(selectedPlatforms))),0}return log(buildManualInstallBridgeContinuation("configured",labelsForPlatforms(selectedPlatforms))),0}init_version_generated();import{spawn as spawn8}from"child_process";import{stat as stat9}from"fs/promises";import path29 from"path";init_start_tickets();init_agent_registry();async function fetchLatestVersion(){try{let res=await fetch("https://registry.npmjs.org/@bridge_gpt/mcp-server/latest",{signal:AbortSignal.timeout(3e3)});if(res.ok)return(await res.json()).version||null}catch{return null}return null}async function runUpgradeCli(argv){let cwd=process.cwd(),isDryRun=argv.includes("--dry-run"),isInternalReexec=argv.includes("--internal-reexec"),latestVersion=VERSION,fetched=await fetchLatestVersion();fetched&&(latestVersion=fetched);let isNewer=isNewerVersion(VERSION,latestVersion),npxCmd=process.platform==="win32"?"npx.cmd":"npx";if(isNewer&&!isInternalReexec&&!isDryRun)return console.log(`Update available: ${VERSION} -> ${latestVersion}. Re-executing from @latest...`),new Promise(resolve2=>{let child=spawn8(npxCmd,["-y","@bridge_gpt/mcp-server@latest","upgrade","--internal-reexec","--old-version",VERSION],{stdio:"inherit",cwd});child.on("close",code=>resolve2(code??0)),child.on("error",err=>{console.error(`Bridge API upgrade failed: could not re-exec npx: ${err.message}`),resolve2(1)})});let oldVersionIdx=argv.indexOf("--old-version"),oldVersion=oldVersionIdx!==-1&&oldVersionIdx+1<argv.length?argv[oldVersionIdx+1]:VERSION,targetVersion=isDryRun&&isNewer?latestVersion:VERSION;if(isDryRun){let configTargets=[".mcp.json",".vscode/mcp.json",".cursor/mcp.json"];console.log(`
|
|
4792
|
+
`)}function formatToolLabelPhrase(labels){return labels.length===0?"an AI coding tool":labels.length===1?labels[0]:labels.length===2?`${labels[0]} and ${labels[1]}`:`${labels.slice(0,-1).join(", ")}, and ${labels[labels.length-1]}`}var INSTALL_BRIDGE_LAUNCH_CONSENT_PROMPT_PREFIX="Bridge can configure and set up this project for you automatically. Open a ";async function requestInstallBridgeLaunchConsent(toolLabel,deps){if(!deps.isTTY||!deps.promptLine)return"no-spawn";try{let answer=(await deps.promptLine(`${INSTALL_BRIDGE_LAUNCH_CONSENT_PROMPT_PREFIX}${toolLabel} session to do that now? (Y/n) `)).trim().toLowerCase();return answer==="n"||answer==="no"?"no-spawn":"spawn"}catch{return"no-spawn"}}async function buildDetectionContext(deps){let cwd=deps.cwd,homedir=deps.homedir(),posixJoin=(base,rel)=>`${base.endsWith("/")?base.slice(0,-1):base}/${rel}`,candidates=[posixJoin(cwd,".cursor"),posixJoin(cwd,".vscode"),posixJoin(cwd,".windsurf"),posixJoin(cwd,".windsurfrules"),posixJoin(homedir,".codex")],present=new Set;return await Promise.all(candidates.map(async p=>{try{await deps.stat(p),present.add(p)}catch{}})),{cwd,homedir,env:deps.env,exists:p=>present.has(p)}}function hostConfigTargetsForPlatforms(platforms){let set=new Set(platforms);return HOST_PLATFORM_ORDER.filter(id=>set.has(id)).map(id=>MCP_HOST_TARGETS[id]).filter(t=>t.scope==="project"&&t.format==="json").map(t=>({relPath:t.relPath,topLevelKey:t.topLevelKey}))}function labelsForPlatforms(platforms){let set=new Set(platforms);return HOST_PLATFORM_ORDER.filter(id=>set.has(id)).map(id=>MCP_HOST_TARGETS[id].label)}function isPlaceholderApiKey(value){if(typeof value!="string")return!0;let trimmed=value.trim();return trimmed.length===0?!0:trimmed==="YOUR_API_KEY"||trimmed.startsWith("YOUR_")}function buildInstallBridgeServerEntry(cwd,repoName,apiKey,baseUrl,docsDir){let entry=buildBridgeApiEntry(cwd),env={...entry.env,BAPI_REPO_NAME:repoName,BAPI_BASE_URL:baseUrl,BAPI_DOCS_DIR:docsDir,BAPI_API_KEY:apiKey};return{command:entry.command,args:entry.args,env}}function buildInstallBridgeSecretFreeServerEntry(cwd,repoName,baseUrl,docsDir){let entry=buildBridgeApiEntry(cwd),env={...entry.env,BAPI_REPO_NAME:repoName,BAPI_BASE_URL:baseUrl,BAPI_DOCS_DIR:docsDir};return delete env.BAPI_API_KEY,{command:entry.command,args:entry.args,env}}function formatCredentialStoreGuidance(repoName,credentialStorePath){return[" The bridge-api server resolves your key at runtime from the BAPI_API_KEY environment",` variable or the user-scoped credential store (${credentialStorePath}, target`,` bapi:${repoName}), so it stays out of this file entirely.`]}function formatSecretFreeManualMerge(relPath,topLevelKey,secretFreeEntry){let snippet=JSON.stringify({[topLevelKey]:{"bridge-api":secretFreeEntry}},null,2);return[` To configure ${relPath} by hand, MERGE this secret-free entry into the existing`," file (do not replace the file):",snippet]}function formatInvalidConfigNotice(relPath){return` ${relPath} could not be parsed safely \u2014 it was left untouched.`}async function readHostConfig(deps,fullPath){let raw;try{raw=await deps.readFile(fullPath)}catch(err){return err?.code==="ENOENT"?{state:"absent"}:{state:"invalid"}}let parsed;try{parsed=JSON.parse(raw)}catch{return{state:"invalid"}}return parsed===null||typeof parsed!="object"||Array.isArray(parsed)?{state:"invalid"}:{state:"parsed",config:parsed}}function asRecord2(value){return value!==null&&typeof value=="object"&&!Array.isArray(value)?value:void 0}async function detectExistingRealKey(deps,targets){for(let target of targets){let result=await readHostConfig(deps,path28.join(deps.cwd,target.relPath));if(result.state==="invalid")return!0;if(result.state==="absent")continue;let topLevel=asRecord2(result.config[target.topLevelKey]),entry=asRecord2(topLevel?.["bridge-api"]),env=asRecord2(entry?.env);if(env&&!isPlaceholderApiKey(env.BAPI_API_KEY))return!0}return!1}function trackedConfigWarning(relPath){return`${relPath} is tracked by git \u2014 writing your API key here would commit it on your next push`}async function isTrackedProjectConfig(deps,relPath){try{let result=await deps.startTicketsDeps.runCommand("git",["ls-files","--error-unmatch","--",relPath],{cwd:deps.cwd});return result.exitCode===0?!0:(deps.debugLog(`install-bridge: git-tracked probe for ${relPath} \u2192 not tracked (exit ${result.exitCode})`),!1)}catch{return deps.debugLog(`install-bridge: git-tracked probe for ${relPath} \u2192 not tracked (probe failed)`),!1}}async function requestTrackedConfigConsent(deps,relPath){if(deps.errorLog(trackedConfigWarning(relPath)),!deps.promptLine)return!1;let answer;try{answer=await deps.promptLine(`Write your API key into ${relPath} anyway? (y/N) `)}catch{return!1}let normalized=(answer??"").trim().toLowerCase();return normalized==="y"||normalized==="yes"}async function writeHostConfigs(deps,targets,entries,trackedState,ctx){let written=[],skipped=[];for(let target of targets){let fullPath=path28.join(deps.cwd,target.relPath),read=await readHostConfig(deps,fullPath);if(read.state==="invalid"){deps.errorLog(formatInvalidConfigNotice(target.relPath));for(let line of formatSecretFreeManualMerge(target.relPath,target.topLevelKey,entries.secretFree))deps.errorLog(line);skipped.push({relPath:target.relPath,reason:"invalid"});continue}let tracked=trackedState.get(target.relPath)??!1,entry=entries.real,mode="real-key";if(tracked){let interactive=deps.isTTY&&!!deps.promptLine;if(interactive?await requestTrackedConfigConsent(deps,target.relPath):!1)entry=entries.real,mode="real-key";else{entry=entries.secretFree,mode="secret-free",interactive||deps.errorLog(trackedConfigWarning(target.relPath));for(let line of formatCredentialStoreGuidance(ctx.repoName,ctx.credentialStorePath))deps.errorLog(line);deps.errorLog(` To stop tracking it: git rm --cached -- ${target.relPath}`)}}let config=read.state==="parsed"?read.config:{},topLevel=asRecord2(config[target.topLevelKey])??{};topLevel["bridge-api"]=entry,config[target.topLevelKey]=topLevel,await deps.mkdir(path28.dirname(fullPath),{recursive:!0}),await deps.writeFile(fullPath,JSON.stringify(config,null,2)+`
|
|
4793
|
+
`,{encoding:"utf-8"}),written.push({relPath:target.relPath,mode})}return{written,skipped}}async function provisionSelectedGlobalTargets(deps,platforms,entry,needKey){let logLines=[],anyManualRequired=!1,provisionDeps={fs:{readFile:deps.readFile,writeFile:(p,data)=>deps.writeFile(p,data,{encoding:"utf-8"}),mkdir:async(p,o)=>{await deps.mkdir(p,o)}},vendor:deps.vendor,cwd:deps.cwd,homedir:deps.homedir(),env:deps.env},set=new Set(platforms);for(let id of HOST_PLATFORM_ORDER){if(!set.has(id))continue;let target=MCP_HOST_TARGETS[id];if(target.scope==="project"&&target.format==="json")continue;let outcome2=await provisionHostTarget(target,entry,provisionDeps);switch(outcome2.status){case"vendor-written":case"direct-written":case"created":logLines.push(` configured ${target.label} (${outcome2.displayPath})`);break;case"manual-required":anyManualRequired=!0,logLines.push(` ${target.label}: add the bridge-api MCP server manually to ${outcome2.displayPath} (the API key is redacted in printed instructions).`);break;case"skipped-invalid":logLines.push(` ${target.label}: skipped ${outcome2.displayPath} \u2014 existing config is not valid; left untouched.`);break;case"failed":logLines.push(` ${target.label}: could not be configured automatically; configure it manually.`);break}}return needKey&&anyManualRequired&&logLines.push(` ${formatNeedKeyCredentialStoreLine(needKey.repoName,needKey.credentialStorePath)}`),logLines}function buildPingUrl(baseUrl,repoName){let url=new URL(`${baseUrl.replace(/\/+$/,"")}/jira/ping`);return url.searchParams.set("repo_name",repoName),url.toString()}var CONNECTIVITY_ACCESS_DENIED_FALLBACK="The Bridge API denied access to this repository (HTTP 403). Verify the repo_name and that this credential is authorized for it.",CONNECTIVITY_KEY_SOURCE_ATTRIBUTION={env:" (this key came from the BAPI_API_KEY environment variable \u2014 unset it to be prompted for a different one)",flag:" (this key came from the --api-key flag \u2014 omit it to be prompted for a different one)",prompt:""};async function verifyConnectivity(deps,baseUrl,repoName,apiKey,apiKeySource){let url=buildPingUrl(baseUrl,repoName),resp;try{resp=await deps.fetch(url,{headers:{"X-API-Key":apiKey},signal:AbortSignal.timeout(1e4)})}catch{return{ok:!1,message:`Could not reach the Bridge API at ${baseUrl}. Check BAPI_BASE_URL and your network.`}}if(resp.ok)return{ok:!0};if(resp.status===401){let setupUrl=buildInstallBridgeSetupUrl(baseUrl);return{ok:!1,message:`The Bridge API rejected the credential (HTTP ${resp.status}). The API key may be invalid or expired \u2014 generate a fresh one at ${setupUrl} (Security page). (An expired token can also surface as a permission error.)`+(apiKeySource?CONNECTIVITY_KEY_SOURCE_ATTRIBUTION[apiKeySource]:"")}}if(resp.status===403){let detail;try{detail=(await resp.json())?.detail}catch{return{ok:!1,message:CONNECTIVITY_ACCESS_DENIED_FALLBACK}}return typeof detail=="string"&&detail.trim().length>0?{ok:!1,message:detail.trim()}:{ok:!1,message:CONNECTIVITY_ACCESS_DENIED_FALLBACK}}return resp.status===404?{ok:!1,message:`The Bridge API could not find repo '${repoName}' (HTTP 404). Confirm --repo matches the server-side repository registration exactly.`}:{ok:!1,message:`Connectivity check failed (HTTP ${resp.status}). Verify your repo, API key, and BAPI_BASE_URL.`}}function buildResolveRepoUrl(baseUrl){return`${baseUrl.replace(/\/+$/,"")}/setup/resolve-repo`}async function resolveRepoViaServer(fetchImpl,baseUrl,apiKey){let url=buildResolveRepoUrl(baseUrl),resp;try{resp=await fetchImpl(url,{headers:{"X-API-Key":apiKey},signal:AbortSignal.timeout(1e4)})}catch{return{status:"error"}}if(resp.status===404)return{status:"not-deployed"};if(resp.status===409)return{status:"unresolved"};if(!resp.ok)return{status:"error"};let body;try{body=await resp.json()}catch{return{status:"error"}}let repoName=body?.repo_name,validated=validateRepoName(repoName);return validated.ok?{status:"resolved",repoName:validated.value}:{status:"error"}}var BOOTSTRAP_KEY_SECRET_BYTES=32;function generateBootstrapKeySecret(randomBytes3){return randomBytes3(BOOTSTRAP_KEY_SECRET_BYTES).toString("base64url")}function fingerprintBootstrapInvite(token){return createHash3("sha256").update(token,"utf-8").digest("hex")}function buildBootstrapExchangeUrl(baseUrl){return`${baseUrl.replace(/\/+$/,"")}/setup/bootstrap`}async function exchangeBootstrapInvite(deps,baseUrl,token,repoName,keySecret){let url=buildBootstrapExchangeUrl(baseUrl),resp;try{resp=await deps.fetch(url,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token,repo_name:repoName,key_secret:keySecret}),signal:AbortSignal.timeout(1e4)})}catch(err){return{ok:!1,kind:"failed",message:`Could not reach the Bridge API at ${baseUrl} to redeem the bootstrap invite. Check BAPI_BASE_URL and your network. The invite has not been used.`}}if(resp.ok){let repo;try{repo=(await resp.json())?.repo_name}catch{return{ok:!1,kind:"failed",message:"The Bridge API returned an unreadable response to the bootstrap exchange."}}let validated=validateRepoName(repo);return validated.ok?{ok:!0,repoName:validated.value}:{ok:!1,kind:"failed",message:"The Bridge API returned an unexpected repo name for the bootstrap exchange."}}return resp.status===409?{ok:!1,kind:"repo-name-taken",message:`The repo name '${repoName}' is already taken (HTTP 409). Repo names are globally unique.`}:resp.status===401?{ok:!1,kind:"invalid-invite",message:`The Bridge API rejected the bootstrap invite (HTTP ${resp.status}).`}:{ok:!1,kind:"failed",message:`The bootstrap exchange failed (HTTP ${resp.status}). Verify BAPI_BASE_URL and try again.`}}var BOOTSTRAP_INVITE_TOKEN_PREFIX="bapi_inv_";function classifyEnteredCredential(value){return value.trim().startsWith(BOOTSTRAP_INVITE_TOKEN_PREFIX)?"invite":"api-key"}function buildSelfServeMintUrl(baseUrl){return`${baseUrl.replace(/\/+$/,"")}/setup/bootstrap/self-serve`}async function mintSelfServeInvite(deps,baseUrl,email){let url=buildSelfServeMintUrl(baseUrl),resp;try{resp=await deps.fetch(url,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({invitee_email:email}),signal:AbortSignal.timeout(1e4)})}catch(err){return{ok:!1,category:"failed"}}if(resp.ok){let token;try{token=(await resp.json())?.token}catch{return{ok:!1,category:"failed"}}return typeof token!="string"||token.trim().length===0||!token.startsWith(BOOTSTRAP_INVITE_TOKEN_PREFIX)?{ok:!1,category:"failed"}:{ok:!0,token}}return resp.status===429?{ok:!1,category:"rate-limited"}:resp.status===400||resp.status===422?{ok:!1,category:"invalid"}:{ok:!1,category:"failed"}}var BOOTSTRAP_INVITE_LOST_SECRET_MESSAGE=["The Bridge API rejected the bootstrap invite (HTTP 401).","","Either the invite is invalid, expired, or revoked \u2014 or it was ALREADY redeemed from this","machine and the local secret has since been lost (e.g. ~/.config/bridge was deleted).","","If it was already redeemed, you cannot recover it yourself:"," \u2022 Re-running will NOT work: each run without the original local secret sends a new one,"," which cannot match what the server stored, so it will keep returning 401."," \u2022 A new bootstrap invite will NOT work either: your repo name is globally unique and is"," now taken by the project you already created, so it cannot be redeemed again.","","Ask your Bridge API operator to recover it for you: they revoke the orphaned key","(DELETE /setup/keys/{id}) and issue a replacement key for the EXISTING project","(POST /setup/keys), then send you that key. Run install-bridge with --api-key <that key>."].join(`
|
|
4794
|
+
`),BOOTSTRAP_INVITE_REJECTED_MESSAGE="The Bridge API rejected the bootstrap invite (HTTP 401). The invite is invalid, expired, or revoked \u2014 ask your Bridge API operator for a new one. (Your locally-stored secret was sent unchanged, so this is not a lost-secret problem.)",BOOTSTRAP_SELF_SERVE_EXPIRED_MESSAGE="The Bridge API rejected the stored signup invite (HTTP 401) \u2014 it is invalid, expired, or revoked. Your saved signup attempt cannot be resumed with it.";function buildBootstrapRetryAdvice(selfServeSignupMode){return selfServeSignupMode?"Re-run install-bridge and choose the email option \u2014 your previous attempt will resume.":"Re-run install-bridge with the same bootstrap invite \u2014 the redemption will replay and return the same key."}async function resolveCredentialConflictConsent(prepared,ctx){if(prepared.ok||prepared.kind!=="credential-conflict")return prepared;if(!ctx.isTTY||!ctx.promptLine)return ctx.errorLog(`Error: ${prepared.error} Re-run with --force to overwrite it (refusing to overwrite a credential non-interactively without consent).`),null;let answer=(await ctx.promptLine(`The credential store already holds a key for ${prepared.target}. Overwrite it? [y/N]: `)).trim().toLowerCase();return answer!=="y"&&answer!=="yes"?(ctx.errorLog("Aborted: existing credential left unchanged (re-run with --force to overwrite)."),null):(ctx.grantConsent(),ctx.retry())}function buildDryRunPreview(plan){return plan.bootstrapInvite?buildBootstrapDryRunPreview(plan):[plan.attemptedServerResolution?"install-bridge --dry-run (one read-only repository-resolution GET may already have occurred; no writes, no state-changing requests, no spawns)":"install-bridge --dry-run (no writes, no network, no spawns)",`Repo name: ${plan.repoName}${plan.attemptedServerResolution?" (resolved server-side from your API key)":""}`,`Base URL (ping): ${plan.baseUrl}`,`Docs dir: ${plan.docsDir}`,`Agent: ${describePlannedLaunchAgent(plan.launch)}`,"","Step 1 \u2014 scaffold (runInit): commands, agents, pipelines, .bridge/config, secret-free MCP placeholders.",`Step 2 \u2014 connectivity ping (before any durable key write): GET ${plan.pingUrl} (X-API-Key: ${REDACTED_API_KEY})`,"Step 3 \u2014 write per-host MCP config (read-merge-write, launcher version-pinned;"," a git-tracked config needs default-No consent for the real key, else a"," secret-free entry; an unparseable config is skipped and left untouched):",...plan.configTargets.map(t=>` ${t}: BAPI_REPO_NAME=${plan.repoName}, BAPI_API_KEY=${REDACTED_API_KEY}, BAPI_BASE_URL=${plan.baseUrl}, BAPI_DOCS_DIR=${plan.docsDir}`),...plan.manualEditors.length>0?[` ${plan.manualEditors.join(" + ")}: detected (global config) \u2014 manual setup instructions would be printed.`]:[],`Step 3b \u2014 pre-warm the version-pinned launcher bucket (fail-open, env sanitized \u2014 BAPI_API_KEY removed): ${plan.prewarmCommand}`,MCP_TIMEOUT_GUIDANCE,`Step 4 \u2014 persist routing credential: target ${plan.credentialTarget} at ${plan.credentialStorePath}`,...buildLaunchStepPreview(plan)]}function describePlannedLaunchAgent(launch){return launch.kind==="spawn"?`${toolLabelForLaunchAgent(launch.agent)} (${launch.agent}) \u2014 opens only after Y/N consent`:launch.kind==="choose-one"?`choose one of ${launch.agents.map(a=>toolLabelForLaunchAgent(a)).join(", ")} \u2014 the chosen tool opens only after Y/N consent`:launch.reason==="empty-selection"?"none \u2014 no tools selected":"none \u2014 no launchable tool selected; manual continuation is printed"}function buildLaunchStepPreview(plan){let githubLines=["Step 4b \u2014 optional GitHub connect (SKIPPED in --dry-run): read GitHub's configured state"," via the install manifest and, only when it is unconfigured and the terminal is",` interactive, offer '${INSTALL_BRIDGE_GITHUB_CONNECT_PROMPT.replace(/:\s*$/,"")}' before the agent session starts.`," It installs the Bridge GitHub App so pull requests and code review work, opens"," github.com in your browser, and shares no GitHub credential with Bridge; it",` defaults to No and can be run later instead: ${INSTALL_BRIDGE_CONNECT_GITHUB_COMMAND}`];if(plan.launch.kind==="spawn")return[...githubLines,`Step 5 \u2014 agent session (${toolLabelForLaunchAgent(plan.launch.agent)}): on a TTY the wizard first asks`," 'Open a \u2026 session to do that now? (Y/n)'; only on consent is the full command below"," stored in a restricted launch script (mode 0600, under the system temp dir) and a short"," sourced runner spawned (the script itself is NOT written in --dry-run):",` ${plan.launch.spawnCommand}`];if(plan.launch.kind==="choose-one"){let labels=plan.launch.agents.map(a=>toolLabelForLaunchAgent(a)).join(", ");return[...githubLines,`Step 5 \u2014 agent session: more than one selected tool can host it (${labels}); on a TTY the wizard`," asks which single tool to open, then asks Y/N consent before spawning that one session."]}return[...githubLines,plan.launch.reason==="empty-selection"?"Step 5 \u2014 no agent session: no tools were selected, so nothing is configured or opened; re-run and select at least one tool.":"Step 5 \u2014 no automatic launch: no selected tool has an agentic CLI. The deterministic setup completes and copy-pasteable /install-bridge continuation is printed (your Bridge MCP tools stay limited until configured)."]}function buildBootstrapDryRunPreview(plan){let pendingTarget=`bootstrap-pending:${plan.repoName}`,header=plan.selfServeSignup?"install-bridge --email --dry-run (no writes, no network, no spawns, no account created, no secret generated)":"install-bridge --invite --dry-run (no writes, no network, no spawns, no secret generated)",repoLine=plan.selfServeSignup?`Repo name: ${plan.repoName} (created by the self-serve exchange; globally unique)`:`Repo name: ${plan.repoName} (created by the exchange; globally unique)`,selfServeStep=plan.selfServeSignup?["Step 2\xB7pre \u2014 self-serve signup (PREVIEWED, SKIPPED in --dry-run): no Bridge workspace"," signup is requested, no mint call is made, no email is sent or transmitted, and",` ${pendingTarget} is neither read nor written;`," a real run would first check that record and RESUME a saved attempt if one"," exists, otherwise request a fresh workspace for your email and durably store the"," returned invite token alongside the key_secret \u2014 which then feeds the SAME"," redemption protocol below."]:[];return[header,repoLine,`Base URL: ${plan.baseUrl}`,`Docs dir: ${plan.docsDir}`,`Agent: ${describePlannedLaunchAgent(plan.launch)}`,"","Step 1 \u2014 scaffold (runInit): commands, agents, pipelines, .bridge/config, secret-free MCP placeholders.",...selfServeStep,`Step 2a \u2014 generate key_secret (32 CSPRNG bytes) and fsync it to ${pendingTarget} at ${plan.credentialStorePath}`," BEFORE the exchange. If that write fails the run ABORTS and no invite is spent.","Step 2b \u2014 redeem the bootstrap invite (replaces the pre-flight ping \u2014 there is no key yet):",` POST ${plan.exchangeUrl}`,` body: {"token": "${REDACTED_API_KEY}", "repo_name": "${plan.repoName}", "key_secret": "${REDACTED_API_KEY}"}`,`Step 2c \u2014 connectivity ping with the newly-minted key (before any durable key write): GET ${plan.pingUrl} (X-API-Key: ${REDACTED_API_KEY})`,"Step 3 \u2014 write per-host MCP config (read-merge-write, launcher version-pinned;"," a git-tracked config needs default-No consent for the real key, else a"," secret-free entry; an unparseable config is skipped and left untouched):",...plan.configTargets.map(t=>` ${t}: BAPI_REPO_NAME=${plan.repoName}, BAPI_API_KEY=${REDACTED_API_KEY}, BAPI_BASE_URL=${plan.baseUrl}, BAPI_DOCS_DIR=${plan.docsDir}`),...plan.manualEditors.length>0?[` ${plan.manualEditors.join(" + ")}: detected (global config) \u2014 manual setup instructions would be printed.`]:[],`Step 3b \u2014 pre-warm the version-pinned launcher bucket (fail-open, env sanitized \u2014 BAPI_API_KEY / BAPI_INVITE removed): ${plan.prewarmCommand}`,MCP_TIMEOUT_GUIDANCE,`Step 4 \u2014 promote ${pendingTarget} \u2192 ${plan.credentialTarget} at ${plan.credentialStorePath} (only after the exchange succeeds)`,...buildLaunchStepPreview(plan)]}async function detectManualEditors(deps){let exists=async p=>{try{return await deps.stat(p),!0}catch{return!1}},windsurf=await exists(path28.join(deps.cwd,".windsurf"))||await exists(path28.join(deps.cwd,".windsurfrules")),codex=await exists(path28.join(deps.homedir(),".codex"));return{windsurf,codex}}function manualEditorNames(editors){let names=[];return editors.windsurf&&names.push("Windsurf"),editors.codex&&names.push("Codex"),names}function formatNeedKeyCredentialStoreLine(repoName,credentialStorePath){return`Your API key is stored at ${credentialStorePath} under "bapi:${repoName}" \u2014 copy BAPI_API_KEY from there.`}function buildManualHostInstructions(entry,editors,needKey){if(!editors.windsurf&&!editors.codex)return null;let redactedEnv={...entry.env,BAPI_API_KEY:REDACTED_API_KEY},lines=["","Detected an editor whose MCP config is global and cannot be written automatically.","Add the server manually (replace <REDACTED> with your key):"];if(editors.windsurf){let windsurfSnippet=JSON.stringify({mcpServers:{"bridge-api":{command:entry.command,args:entry.args,env:redactedEnv}}},null,2);lines.push(""," Windsurf \u2192 ~/.codeium/windsurf/mcp_config.json:",windsurfSnippet)}return editors.codex&&lines.push(""," Codex \u2192 ~/.codex/config.toml (add an [mcp_servers.bridge-api] table with the"," same command/args/env shown above, BAPI_API_KEY set to your key)."),needKey&&lines.push("",` ${formatNeedKeyCredentialStoreLine(needKey.repoName,needKey.credentialStorePath)}`),lines.join(`
|
|
4795
|
+
`)}var INSTALL_BRIDGE_DOCTOR_COMMAND="npx -y @bridge_gpt/mcp-server doctor",INSTALL_BRIDGE_DOCTOR_POINTER=`Diagnose with: ${INSTALL_BRIDGE_DOCTOR_COMMAND}`,INSTALL_BRIDGE_UNCLASSIFIED_CAUSE="unexpected error (run with BAPI_INSTALL_DEBUG=1 for details)",INSTALL_BRIDGE_DISPATCH_STEP_LABEL="install-bridge dispatch",INSTALL_BRIDGE_DEBUG_MESSAGE_LABEL="debug (BAPI_INSTALL_DEBUG) raw message:",INSTALL_BRIDGE_DEBUG_STACK_LABEL="debug (BAPI_INSTALL_DEBUG) raw stack:",INSTALL_BRIDGE_FILESYSTEM_CAUSES=new Map([["EACCES","permission denied"],["EPERM","permission denied"],["ENOSPC","no space left on device"],["EROFS","read-only file system"]]),INSTALL_BRIDGE_NETWORK_CODES=new Set(["ECONNREFUSED","ECONNRESET","ETIMEDOUT","ENETUNREACH","EHOSTUNREACH","ENOTFOUND","EAI_AGAIN"]);function extractInstallBridgeErrorCode(error){try{if(typeof error!="object"||error===null)return;let code=error.code;return typeof code=="string"?code:void 0}catch{return}}function classifyInstallBridgeFailure(error){let code=extractInstallBridgeErrorCode(error);if(code===void 0)return INSTALL_BRIDGE_UNCLASSIFIED_CAUSE;let filesystemCause=INSTALL_BRIDGE_FILESYSTEM_CAUSES.get(code);return filesystemCause!==void 0?filesystemCause:INSTALL_BRIDGE_NETWORK_CODES.has(code)?"network error":INSTALL_BRIDGE_UNCLASSIFIED_CAUSE}function readInstallBridgeDebugValue(read){try{let value=read();return typeof value=="string"?value:value==null?void 0:String(value)}catch{return}}function buildInstallBridgeFailureLines(error,context){let lines=[`Error: install-bridge failed at: ${context.step}`,` cause: ${classifyInstallBridgeFailure(error)}`,` ${INSTALL_BRIDGE_DOCTOR_POINTER}`];if(context.resumeAdvice&&lines.push(` ${context.resumeAdvice}`),context.debugEnabled){let isError=error instanceof Error,rawMessage=readInstallBridgeDebugValue(isError?()=>error.message:()=>String(error)),rawStack=isError?readInstallBridgeDebugValue(()=>error.stack):void 0;lines.push(` ${INSTALL_BRIDGE_DEBUG_MESSAGE_LABEL} ${rawMessage??"<unavailable>"}`),rawStack&&lines.push(` ${INSTALL_BRIDGE_DEBUG_STACK_LABEL} ${rawStack}`)}return lines}function buildInstallBridgeBaseUrlError(value){return`BAPI_BASE_URL must be an absolute http(s) URL (got: "${value}")`}function resolveInstallBridgeBaseUrl(env){let supplied=env.BAPI_BASE_URL,candidate=typeof supplied=="string"?supplied.trim():DEFAULT_BAPI_BASE_URL2;if(candidate.length===0)return{ok:!1,error:buildInstallBridgeBaseUrlError("")};let parsed;try{parsed=new URL(candidate)}catch{return{ok:!1,error:buildInstallBridgeBaseUrlError(candidate)}}return parsed.protocol!=="http:"&&parsed.protocol!=="https:"?{ok:!1,error:buildInstallBridgeBaseUrlError(candidate)}:{ok:!0,value:candidate}}var INSTALL_BRIDGE_STEP_LABELS={scaffold:"Step 1/5 \u2014 scaffolding project (commands, agents, pipelines, config placeholders)\u2026",selfServeMint:"Step 2/5 \u2014 requesting Bridge self-serve setup\u2026",inviteRedeem:"Step 2/5 \u2014 redeeming the bootstrap invite\u2026",verifyConnectivity:"Step 2/5 \u2014 verifying connectivity\u2026",writeHostConfigs:"Step 3/5 \u2014 writing per-host MCP config\u2026",promoteCredential:"Step 4/5 \u2014 promoting the bootstrap credential\u2026",persistCredential:"Step 4/5 \u2014 persisting routing credential\u2026"};function buildInstallBridgeLaunchStepLabel(agent){return`Step 5/5 \u2014 opening a ${toolLabelForLaunchAgent(agent)} session for /install-bridge configuration + concise capability report\u2026`}var INSTALL_BRIDGE_CWD_BANNER_PREFIX="Installing Bridge into: ",INSTALL_BRIDGE_NO_GIT_WARNING="This doesn't look like a project root (no .git found)",INSTALL_BRIDGE_NO_GIT_PROMPT=`${INSTALL_BRIDGE_NO_GIT_WARNING} \u2014 continue? [y/N]: `,INSTALL_BRIDGE_NO_GIT_ABORT="Aborted \u2014 run install-bridge from your project root, or re-run and confirm to continue.",INSTALL_BRIDGE_NO_GIT_NONINTERACTIVE_WARNING=`Warning: ${INSTALL_BRIDGE_NO_GIT_WARNING} \u2014 continuing (non-interactive).`;async function hasProjectRootMarker(deps){try{return await deps.stat(path28.join(deps.cwd,".git")),!0}catch{return!1}}async function runInstallBridgeCli(argv,overrides={}){let deps={...createDefaultInstallBridgeDeps(),...overrides};overrides.resolveRepoViaServer||(deps.resolveRepoViaServer=(baseUrl2,apiKey2)=>resolveRepoViaServer(deps.fetch,baseUrl2,apiKey2));let{log,errorLog}=deps,reportFatal=(...lines)=>{for(let line of lines)errorLog(line);errorLog(` ${INSTALL_BRIDGE_DOCTOR_POINTER}`)},fatal=(...lines)=>(reportFatal(...lines),1),baseUrlResult=resolveInstallBridgeBaseUrl(deps.env),usageBaseUrl=baseUrlResult.ok?baseUrlResult.value:DEFAULT_BAPI_BASE_URL2,parsed=parseInstallBridgeArgs(argv);if(parsed.status==="help")return log(getInstallBridgeUsage(usageBaseUrl)),0;if(parsed.status==="error")return errorLog(`Error: ${parsed.message}`),errorLog(""),errorLog(getInstallBridgeUsage(usageBaseUrl)),1;let options=parsed.options;if(!baseUrlResult.ok)return errorLog(baseUrlResult.error),1;let baseUrl=baseUrlResult.value,setupUrl=buildInstallBridgeSetupUrl(baseUrl);if(deps.env={...deps.env,BAPI_BASE_URL:baseUrl},log(`${INSTALL_BRIDGE_CWD_BANNER_PREFIX}${deps.cwd}`),!await hasProjectRootMarker(deps))if(deps.isTTY&&deps.promptLine){let answer="";try{answer=(await deps.promptLine(INSTALL_BRIDGE_NO_GIT_PROMPT)).trim().toLowerCase()}catch{answer=""}if(answer!=="y"&&answer!=="yes")return log(INSTALL_BRIDGE_NO_GIT_ABORT),0}else errorLog(INSTALL_BRIDGE_NO_GIT_NONINTERACTIVE_WARNING);let branchResult=await resolveInstallBridgeOnboardingBranchForRun(options,deps,argv);if(!branchResult.ok)return errorLog(`Error: ${branchResult.error}`),1;let branch=branchResult.branch,bootstrapInviteMode=branch.kind==="need-key",selfServeSignupMode=branch.kind==="need-key"&&branch.method==="self-serve",apiKey="",inviteToken="",signupEmail="",apiKeySource;if(selfServeSignupMode){let emailResult=await resolveSignupEmail(options,deps);if(!emailResult.ok)return errorLog(`Error: ${emailResult.error}`),1;signupEmail=emailResult.value}else if(bootstrapInviteMode){let inviteResult=await resolveInviteToken(options,deps);if(!inviteResult.ok)return errorLog(`Error: ${inviteResult.error}`),1;inviteToken=inviteResult.value}else{let keyResult=await resolveApiKey(options,deps);if(!keyResult.ok)return errorLog(`Error: ${keyResult.error}`),1;classifyEnteredCredential(keyResult.value)==="invite"?(inviteToken=keyResult.value.trim(),apiKey="",bootstrapInviteMode=!0,selfServeSignupMode=!1,apiKeySource=void 0):(apiKey=keyResult.value,apiKeySource=keyResult.source)}let docsDir=deps.env.BAPI_DOCS_DIR??DEFAULT_BAPI_DOCS_DIR,repoName,attemptedServerResolution=!1;if(bootstrapInviteMode){let repoResult=await resolveRepoName(options,deps,"new-project");if(!repoResult.ok)return errorLog(`Error: ${repoResult.error}`),1;let validated=validateRepoName(repoResult.value);if(!validated.ok)return errorLog(`Error: invalid repo name \u2014 ${validated.error}.`),1;repoName=validated.value}else{let configured=resolveConfiguredRepoName(options,deps.env);if(configured!==void 0)repoName=configured;else{attemptedServerResolution=!0,log("Resolving repository\u2026");let resolution=await deps.resolveRepoViaServer(baseUrl,apiKey);if(resolution.status==="resolved")repoName=resolution.repoName;else{log(`Couldn't auto-resolve your repo from the key; falling back to a guessed name \u2014 confirm it matches ${setupUrl} (setup UI).`);let repoResult=await resolveRepoName(options,deps,"existing-registration");if(!repoResult.ok)return errorLog(`Error: ${repoResult.error}`),1;repoName=repoResult.value}}}let credentialStorePath=getPrimaryCredentialStorePath({env:deps.env,homedir:deps.homedir}),selectedPlatforms=await resolveSelectedHostPlatforms(deps,options),targets=hostConfigTargetsForPlatforms(selectedPlatforms),manualEditors=await detectManualEditors(deps),launchDecision=resolveInstallBridgeLaunchDecision(selectedPlatforms,options.agentName),planLaunch;if(launchDecision.kind==="spawn"){let spec=resolveAgentSpec(launchDecision.agent);if(!spec)return errorLog(`Error: no launch agent is registered for '${launchDecision.agent}'.`),1;planLaunch={kind:"spawn",agent:launchDecision.agent,spawnCommand:deps.buildShellCommand(spec,INSTALL_BRIDGE_AGENT_PROMPT,deps.cwd,deps.platform)}}else launchDecision.kind==="choose-one"?planLaunch={kind:"choose-one",agents:launchDecision.agents}:planLaunch={kind:"manual",reason:launchDecision.reason};let plan={repoName,baseUrl,docsDir,launch:planLaunch,configTargets:targets.map(t=>t.relPath),manualEditors:manualEditorNames(manualEditors),credentialTarget:`bapi:${repoName}`,credentialStorePath,pingUrl:buildPingUrl(baseUrl,repoName),prewarmCommand:buildPrewarmCommandPreview(),...bootstrapInviteMode?{bootstrapInvite:!0,exchangeUrl:buildBootstrapExchangeUrl(baseUrl)}:{},...selfServeSignupMode?{selfServeSignup:!0}:{},...attemptedServerResolution?{attemptedServerResolution:!0}:{}};if(options.dryRun){for(let line of buildDryRunPreview(plan))log(line);return 0}if(planLaunch.kind==="manual"&&planLaunch.reason==="empty-selection")return log(buildManualInstallBridgeContinuation("empty-selection",[])),0;let finalAgentName=null,finalSpawnCommand=null;if(planLaunch.kind==="spawn")finalAgentName=planLaunch.agent,finalSpawnCommand=planLaunch.spawnCommand;else if(planLaunch.kind==="choose-one"){let chosen=await chooseInstallBridgeLaunchAgent(planLaunch.agents,deps);if(chosen){let spec=resolveAgentSpec(chosen);if(!spec)return errorLog(`Error: no launch agent is registered for '${chosen}'.`),1;finalAgentName=chosen,finalSpawnCommand=deps.buildShellCommand(spec,INSTALL_BRIDGE_AGENT_PROMPT,deps.cwd,deps.platform)}}let launchCommand=null,prewarmPromise=null;if(finalAgentName&&finalSpawnCommand){let materialized=await materializeWorkerLaunchCommand(deps.startTicketsDeps,"install",finalSpawnCommand);if(!materialized.ok)return errorLog(`Error: ${materialized.error}`),1;if(launchCommand=materialized.command,Buffer.byteLength(launchCommand,"utf8")>=MAX_TERMINAL_COMMAND_BYTES)return errorLog("Error: the agent session command is too long to send to the terminal safely. Check that the system temporary directory is writable so the launch script can be used."),1;log(" pre-warming the version-pinned launcher bucket (in the background)\u2026"),prewarmPromise=deps.spawnPrewarm("npx",buildPrewarmArgs(),deps.env)}let credentialWriteDeps={env:deps.env,homedir:deps.homedir,platform:deps.platform,readFile:deps.readFile,mkdir:deps.mkdir,writeFile:(p,d,o)=>deps.writeFile(p,d,o),rename:deps.rename,chmod:deps.chmod,unlink:deps.unlink,open:deps.open},hasRealKey=await detectExistingRealKey(deps,targets),overwriteConsent=options.force;if(hasRealKey&&!options.force)if(deps.isTTY&&deps.promptLine){let answer=(await deps.promptLine("A host config already contains a BAPI_API_KEY. Overwrite it? [y/N]: ")).trim().toLowerCase();if(answer!=="y"&&answer!=="yes")return errorLog("Aborted: existing API key left unchanged (re-run with --force to overwrite)."),1;overwriteConsent=!0}else return errorLog("Error: a host config already contains a BAPI_API_KEY. Re-run with --force to overwrite it (refusing to overwrite a credential non-interactively without consent)."),1;let currentStep=INSTALL_BRIDGE_STEP_LABELS.scaffold,bootstrapExchangeSucceeded=!1;try{currentStep=INSTALL_BRIDGE_STEP_LABELS.scaffold,log(currentStep),await deps.runInit(deps.cwd);let inviteFingerprint="";if(bootstrapInviteMode){let resumedSelfServeReplay=!1,keySecret="",reusedPendingSecret=!1;if(selfServeSignupMode){let lookup=await deps.lookupSelfServeBootstrapPending({repoName},credentialWriteDeps);if(!lookup.ok)return fatal(`Error: ${lookup.error}`);lookup.state==="resumable"&&(inviteToken=lookup.replayToken,inviteFingerprint=lookup.inviteFingerprint,keySecret=lookup.keySecret,resumedSelfServeReplay=!0,reusedPendingSecret=!0,log(` resuming your previous signup attempt for ${repoName}`))}let mintAndPrepareSelfServe=async()=>{let mint=await mintSelfServeInvite(deps,baseUrl,signupEmail);if(!mint.ok)return mint.category==="rate-limited"?reportFatal("Error: Bridge's self-serve signup capacity for this hour is exhausted (this is a global service limit, not a problem with your machine). Try again in about an hour."):mint.category==="invalid"?reportFatal("Error: Self-serve setup could not be requested. Check the email value and try again."):reportFatal("Error: Unable to complete self-serve setup. Check connectivity and retry."),!1;inviteToken=mint.token,inviteFingerprint=fingerprintBootstrapInvite(inviteToken);let prepareSelfServePending=allowOverwrite=>deps.prepareBootstrapPending({repoName,inviteFingerprint,generateKeySecret:()=>generateBootstrapKeySecret(deps.randomBytes),allowOverwriteExistingCredential:allowOverwrite,selfServeReplayToken:inviteToken},credentialWriteDeps),prep=await resolveCredentialConflictConsent(await prepareSelfServePending(overwriteConsent),{isTTY:deps.isTTY,promptLine:deps.promptLine,errorLog,grantConsent:()=>{overwriteConsent=!0},retry:()=>prepareSelfServePending(!0)});return prep===null?!1:prep.ok?(keySecret=prep.keySecret,reusedPendingSecret=prep.reused,log(` saved the pending credential for ${prep.target} (fsynced before the exchange)`),!0):(reportFatal(`Error: could not durably store the signup credential (${prep.kind}). ${prep.error} No workspace has been set up \u2014 fix the problem and re-run.`),!1)};if(selfServeSignupMode&&!resumedSelfServeReplay&&(currentStep=INSTALL_BRIDGE_STEP_LABELS.selfServeMint,log(currentStep),!await mintAndPrepareSelfServe()))return 1;if(!selfServeSignupMode){inviteFingerprint=fingerprintBootstrapInvite(inviteToken),currentStep=INSTALL_BRIDGE_STEP_LABELS.inviteRedeem,log(currentStep);let prepareInvitePending=allowOverwrite=>deps.prepareBootstrapPending({repoName,inviteFingerprint,generateKeySecret:()=>generateBootstrapKeySecret(deps.randomBytes),allowOverwriteExistingCredential:allowOverwrite},credentialWriteDeps),prepared=await resolveCredentialConflictConsent(await prepareInvitePending(overwriteConsent),{isTTY:deps.isTTY,promptLine:deps.promptLine,errorLog,grantConsent:()=>{overwriteConsent=!0},retry:()=>prepareInvitePending(!0)});if(prepared===null)return 1;if(!prepared.ok&&prepared.kind==="pending-conflict")return errorLog(`Error: ${prepared.error}`),1;if(!prepared.ok)return errorLog(`Error: could not durably store the bootstrap credential (${prepared.kind}). ${prepared.error} The bootstrap invite has NOT been used \u2014 fix the problem and re-run.`),1;keySecret=prepared.keySecret,reusedPendingSecret=prepared.reused,log(` saved the pending credential for ${prepared.target} (fsynced before the exchange)`)}let exchange,selfServeRefreshed=!1;for(;;){for(exchange=await exchangeBootstrapInvite(deps,baseUrl,inviteToken,repoName,keySecret);!exchange.ok&&exchange.kind==="repo-name-taken";){if(!deps.isTTY||!deps.promptLine)return errorLog(`Error: ${exchange.message} Re-run with a different --repo (the bootstrap invite has NOT been used).`),1;errorLog(exchange.message);let answer=(await deps.promptLine("Choose a different repo name: ")).trim(),validated=validateRepoName(answer);if(!validated.ok)return errorLog(`Error: invalid repo name \u2014 ${validated.error}.`),1;let nextRepo=validated.value,repointed=await deps.repointBootstrapPending({fromRepoName:repoName,toRepoName:nextRepo,inviteFingerprint,allowOverwriteExistingCredential:overwriteConsent},credentialWriteDeps);if(!repointed.ok)return errorLog(`Error: could not re-point the pending bootstrap credential to '${nextRepo}' (${repointed.kind}). ${repointed.error} The bootstrap invite has NOT been used.`),1;repoName=nextRepo,exchange=await exchangeBootstrapInvite(deps,baseUrl,inviteToken,repoName,keySecret)}if(exchange.ok)break;if(exchange.kind==="invalid-invite"&&resumedSelfServeReplay&&!selfServeRefreshed){if(!deps.isTTY||!deps.promptLine)return fatal(BOOTSTRAP_SELF_SERVE_EXPIRED_MESSAGE,`The saved attempt at ${getBootstrapPendingTarget(repoName)} in ${credentialStorePath} has been left untouched. Re-run install-bridge on an interactive terminal to discard it and start a fresh signup.`);errorLog(BOOTSTRAP_SELF_SERVE_EXPIRED_MESSAGE);let consented=!1;try{let answer=(await deps.promptLine(`Discard the saved signup at ${getBootstrapPendingTarget(repoName)} in ${credentialStorePath} and start a fresh one? [y/N]: `)).trim().toLowerCase();consented=answer==="y"||answer==="yes"}catch{consented=!1}if(!consented)return errorLog("Aborted: the saved signup attempt was left unchanged (nothing was discarded)."),1;let discarded=await deps.discardBootstrapPending({repoName,inviteFingerprint},credentialWriteDeps);if(!discarded.ok)return fatal(`Error: could not discard the saved signup (${discarded.kind}). ${discarded.error}`);if(log("Requesting a fresh Bridge self-serve setup\u2026"),!await mintAndPrepareSelfServe())return 1;selfServeRefreshed=!0,resumedSelfServeReplay=!1;continue}return fatal(exchange.kind==="invalid-invite"?reusedPendingSecret?BOOTSTRAP_INVITE_REJECTED_MESSAGE:BOOTSTRAP_INVITE_LOST_SECRET_MESSAGE:`Error: ${exchange.message} ${buildBootstrapRetryAdvice(selfServeSignupMode)}`)}if(bootstrapExchangeSucceeded=!0,exchange.repoName!==repoName){let repointed=await deps.repointBootstrapPending({fromRepoName:repoName,toRepoName:exchange.repoName,inviteFingerprint,allowOverwriteExistingCredential:overwriteConsent},credentialWriteDeps);if(!repointed.ok)return fatal(`Error: the project was created as '${exchange.repoName}' but the pending credential could not be re-pointed to it (${repointed.kind}). ${repointed.error}`);repoName=exchange.repoName}log(` bootstrap invite redeemed \u2014 project '${repoName}' is ready`),apiKey=keySecret}bootstrapInviteMode||(currentStep=INSTALL_BRIDGE_STEP_LABELS.verifyConnectivity,log(currentStep));let ping=await verifyConnectivity(deps,baseUrl,repoName,apiKey,bootstrapInviteMode?void 0:apiKeySource);if(!ping.ok)return fatal(bootstrapInviteMode?`Error: ${ping.message} ${buildBootstrapRetryAdvice(selfServeSignupMode)}`:`Error: ${ping.message}`);log(" connectivity OK");let entry=buildInstallBridgeServerEntry(deps.cwd,repoName,apiKey,baseUrl,docsDir),secretFreeEntry=buildInstallBridgeSecretFreeServerEntry(deps.cwd,repoName,baseUrl,docsDir);currentStep=INSTALL_BRIDGE_STEP_LABELS.writeHostConfigs,log(currentStep);let gitignoreDeps={readFile:deps.readFile,writeFile:(p,data)=>deps.writeFile(p,data,{encoding:"utf-8"}),mkdir:(p,o)=>deps.mkdir(p,o)};for(let target of targets)try{await ensureGitignored(deps.cwd,target.relPath,gitignoreDeps)}catch{return errorLog("Error: could not add a project MCP config to .gitignore before writing your key. Aborting so the API key is never written to an un-ignored file."),1}let trackedState=new Map;for(let target of targets)trackedState.set(target.relPath,await isTrackedProjectConfig(deps,target.relPath));let writeResult=await writeHostConfigs(deps,targets,{real:entry,secretFree:secretFreeEntry},trackedState,{repoName,credentialStorePath});for(let{relPath,mode}of writeResult.written)log(mode==="secret-free"?` wrote ${relPath} (secret-free \u2014 key resolved at runtime)`:` wrote ${relPath}`);for(let{relPath}of writeResult.skipped)log(` skipped ${relPath} \u2014 existing config could not be parsed safely; left untouched`);let needKeyManual=bootstrapInviteMode?{repoName,credentialStorePath}:void 0,globalLogLines=await provisionSelectedGlobalTargets(deps,selectedPlatforms,entry,needKeyManual);for(let line of globalLogLines)log(line);let legacyManualEditors={windsurf:manualEditors.windsurf&&!selectedPlatforms.includes("windsurf"),codex:manualEditors.codex&&!selectedPlatforms.includes("codex")},manualInstructions=buildManualHostInstructions(entry,legacyManualEditors,needKeyManual);manualInstructions&&log(manualInstructions),selectedPlatforms.includes("claude-code")&&selectedPlatforms.includes("copilot-cli")&&log(" Note: Claude Code uses the project .mcp.json while GitHub Copilot CLI uses only its global ~/.copilot/mcp-config.json \u2014 the two are configured separately."),selectedPlatforms.includes("claude-code")&&log(" Claude Code: the project MCP server is pending approval in Claude Code's trust dialog; restart or reload an already-running session for it to take effect.");try{await ensureGitignored(deps.cwd,".bridge/install-state.json",gitignoreDeps),(await writeMcpInstallState(deps.cwd,{selectedPlatforms,projectConfigPaths:targets.map(t=>t.relPath)},{readFile:deps.readFile,writeFile:(p,data)=>deps.writeFile(p,data,{encoding:"utf-8"}),rename:deps.rename,mkdir:async(p,o)=>{await deps.mkdir(p,o)},unlink:deps.unlink})).ok||errorLog("Warning: could not persist the install-state file (non-fatal).")}catch{errorLog("Warning: could not persist the install-state file (non-fatal).")}if(bootstrapExchangeSucceeded){currentStep=INSTALL_BRIDGE_STEP_LABELS.promoteCredential,log(currentStep);let promoted=await deps.promoteBootstrapPending({repoName,inviteFingerprint,allowOverwriteExistingCredential:overwriteConsent},credentialWriteDeps);if(!promoted.ok)return fatal(`Error: the project and API key were created, but the credential could not be stored (${promoted.kind}). ${promoted.error} Your key is still saved locally as a pending record. ${buildBootstrapRetryAdvice(selfServeSignupMode)}`);log(` stored routing credential for ${promoted.target} at ${promoted.path}`)}else{currentStep=INSTALL_BRIDGE_STEP_LABELS.persistCredential,log(currentStep);try{let result=await deps.upsertCredential(repoName,apiKey,credentialWriteDeps);result.ok?log(` stored routing credential for ${result.target} at ${result.path}`):log(` warning: could not persist the routing credential (${result.kind}). start-tickets model routing may not resolve the key for bapi:${repoName} and will fail open to the premium/Opus tier (the most expensive) \u2014 set BAPI_API_KEY in the shell or re-run install-bridge, then verify with 'npx -y @bridge_gpt/mcp-server doctor'.`)}catch{log(" warning: could not persist the routing credential (unexpected error). start-tickets model routing may need BAPI_API_KEY in the shell and will fail open to the premium/Opus tier (the most expensive) until fixed \u2014 verify with 'npx -y @bridge_gpt/mcp-server doctor'.")}}if(await offerGithubConnection(repoName,baseUrl,deps,log),prewarmPromise){let prewarm=await prewarmPromise;prewarm.ok?(log(" launcher bucket warmed (the first MCP launch will not pay a cold install)."),log(` ${MCP_TIMEOUT_GUIDANCE}`)):errorLog(`Warning: could not pre-warm the version-pinned launcher bucket${prewarm.warning?` (${prewarm.warning})`:""}. The first MCP launch may pay a one-time cold install and could be slow. ${MCP_TIMEOUT_GUIDANCE}`)}if(finalAgentName&&launchCommand){if(await requestInstallBridgeLaunchConsent(toolLabelForLaunchAgent(finalAgentName),deps)==="spawn"){currentStep=buildInstallBridgeLaunchStepLabel(finalAgentName),log(currentStep);let terminal=detectTerminal(void 0,deps.env),spawnResult=await deps.spawnTerminalTab(deps.startTicketsDeps,terminal,launchCommand,{key:"install",worktreePath:deps.cwd,title:"Bridge Install"});if(!spawnResult.ok)return errorLog(`Warning: setup steps completed, but the agent session could not be opened (${spawnResult.error}).`),log(buildManualInstallBridgeContinuation("configured",labelsForPlatforms(selectedPlatforms))),0;let handoffToolLabel=toolLabelForLaunchAgent(finalAgentName);return log(""),log(`install-bridge setup steps complete. A fresh ${handoffToolLabel} session is now applying configuration, presenting the concise capability report, and recommending /learn-repository.`),log(`In the new tab: approve the workspace and the 'bridge-api' MCP server if ${handoffToolLabel} asks \u2014 configuration can't proceed until you do.`),log(`If that tab is closed or fails, open a session in ${handoffToolLabel} and run /install-bridge.`),log(`NOTE: the install is not finished until that session's apply reports applied fields \u2014 it will pause to ask you to approve the project description. Indexing starts automatically once the repository reaches full parse readiness \u2014 there is no indexing question to answer. Verify afterwards at ${setupUrl} (Get Started page \u2014 install status panel) or via the session's 'Applied N of M' summary.`),0}return log(buildManualInstallBridgeContinuation("configured",labelsForPlatforms(selectedPlatforms))),0}return log(buildManualInstallBridgeContinuation("configured",labelsForPlatforms(selectedPlatforms))),0}catch(error){let resumeAdvice=bootstrapInviteMode&&bootstrapExchangeSucceeded?buildBootstrapRetryAdvice(selfServeSignupMode):void 0,lines=buildInstallBridgeFailureLines(error,{step:currentStep,debugEnabled:!!deps.env.BAPI_INSTALL_DEBUG,...resumeAdvice?{resumeAdvice}:{}});for(let line of lines)errorLog(line);return 1}}init_version_generated();import{spawn as spawn8}from"child_process";import{stat as stat9}from"fs/promises";import path29 from"path";init_start_tickets();init_agent_registry();async function fetchLatestVersion(){try{let res=await fetch("https://registry.npmjs.org/@bridge_gpt/mcp-server/latest",{signal:AbortSignal.timeout(3e3)});if(res.ok)return(await res.json()).version||null}catch{return null}return null}async function runUpgradeCli(argv){let cwd=process.cwd(),isDryRun=argv.includes("--dry-run"),isInternalReexec=argv.includes("--internal-reexec"),latestVersion=VERSION,fetched=await fetchLatestVersion();fetched&&(latestVersion=fetched);let isNewer=isNewerVersion(VERSION,latestVersion),npxCmd=process.platform==="win32"?"npx.cmd":"npx";if(isNewer&&!isInternalReexec&&!isDryRun)return console.log(`Update available: ${VERSION} -> ${latestVersion}. Re-executing from @latest...`),new Promise(resolve2=>{let child=spawn8(npxCmd,["-y","@bridge_gpt/mcp-server@latest","upgrade","--internal-reexec","--old-version",VERSION],{stdio:"inherit",cwd});child.on("close",code=>resolve2(code??0)),child.on("error",err=>{console.error(`Bridge API upgrade failed: could not re-exec npx: ${err.message}`),resolve2(1)})});let oldVersionIdx=argv.indexOf("--old-version"),oldVersion=oldVersionIdx!==-1&&oldVersionIdx+1<argv.length?argv[oldVersionIdx+1]:VERSION,targetVersion=isDryRun&&isNewer?latestVersion:VERSION;if(isDryRun){let configTargets=[".mcp.json",".vscode/mcp.json",".cursor/mcp.json"];console.log(`
|
|
4795
4796
|
[Dry Run] Upgrade preview:`),isNewer&&!isInternalReexec&&console.log(`- Would re-exec from @latest to upgrade ${VERSION} -> ${latestVersion}`),console.log("- Would detect and remove competing local install in node_modules/@bridge_gpt/mcp-server if present."),console.log(`- Would rewrite launcher pin to @bridge_gpt/mcp-server@${targetVersion} across active per-host configs:`);for(let target of configTargets)try{await stat9(path29.join(cwd,target)),console.log(` - ${target}`)}catch{}console.log("- Would refresh scaffolded artifacts (commands, agents, pipelines).");let spawnCommand=buildGenericAgentShellCommand(AGENT_REGISTRY.claude,"Bridge API MCP server has been upgraded. Please reload/reconnect the MCP server in your environment.",cwd,process.platform);return console.log(`- Would print: ${oldVersion} -> ${targetVersion} and spawn a fresh reconnect session with command:
|
|
4796
4797
|
${spawnCommand}`),0}try{let localModulePath=path29.join(cwd,"node_modules","@bridge_gpt","mcp-server");await stat9(localModulePath),console.log("Found stale local installation in node_modules. Removing to converge on pinned-npx...");let npmCmd=process.platform==="win32"?"npm.cmd":"npm";await new Promise(resolve2=>{let child=spawn8(npmCmd,["uninstall","@bridge_gpt/mcp-server"],{stdio:"inherit",cwd});child.on("close",code=>{code!==0&&console.warn("Warning: npm uninstall failed; you may need to remove node_modules/@bridge_gpt/mcp-server manually."),resolve2()}),child.on("error",()=>resolve2())})}catch{}try{console.log(`
|
|
4797
4798
|
Upgrading @bridge_gpt/mcp-server to ${targetVersion}...
|
|
@@ -4903,7 +4904,7 @@ Raw body: ${result.text}`}]}}let clarify=envelope.clarify??{},critique=envelope.
|
|
|
4903
4904
|
---
|
|
4904
4905
|
|
|
4905
4906
|
`)+notes;return{content:[{type:"text",text:appendBackendWarningsToText(text,backendWarnings)}]}}async function ensurePackageJsonForCliCommand(flagName,cwd){try{return await stat10(path38.join(cwd,"package.json")),null}catch{return`Error: No package.json found in current directory.
|
|
4906
|
-
${flagName} must be run from your project root (the directory containing package.json).`}}async function runInitCli(cwd){let guardError=await ensurePackageJsonForCliCommand("--init",cwd);if(guardError)return console.error(guardError),1;try{return await runInit(cwd),0}catch(err){let msg=err instanceof Error?err.message:String(err);return console.error(`Bridge API --init failed: ${msg}`),1}}async function dispatchCliSubcommand(argv){let cwd=process.cwd();return argv[0]==="start-tickets"?runStartTicketsCli(argv.slice(1)):argv[0]==="review-tickets"?runReviewTicketsCli(argv.slice(1)):argv[0]==="mcp-invoke"?runMcpInvokeCli(argv.slice(1)):argv[0]==="doctor"?runDoctorCli(argv.slice(1)):argv[0]==="schedule-run"?runScheduleRunCli(argv.slice(1)):argv[0]==="agent-capabilities"?runAgentCapabilitiesCli(argv.slice(1)):argv[0]==="executor"&&argv[1]==="watch"?runExecutorWatchCli(argv.slice(2)):argv[0]==="executor"?runExecutorCli(argv.slice(1)):argv[0]==="setup-epic"?runSetupEpicCli(argv.slice(1)):argv[0]==="emit-conductor-bundle"?runConductorBundleCli(argv.slice(1)):argv[0]==="regression-check"?runRegressionCheckCli(argv.slice(1)):argv[0]==="credentials"?runCredentialsCli(argv.slice(1)):argv[0]==="conductor"?runConductorCli(argv.slice(1)):argv[0]==="install-bridge"?runInstallBridgeCli(argv.slice(1)):argv[0]==="connect-github"?runConnectGithubCli(argv.slice(1)):argv[0]==="upgrade"?runUpgradeCli(argv.slice(1)):argv.includes("--version")?(console.log(VERSION),0):argv.includes("--init")?runInitCli(cwd):argv.includes("--upgrade")?runUpgradeCli(argv):argv.length>0&&!argv[0].startsWith("-")?(console.error(`Error: Unknown subcommand '${argv[0]}'. Run with --help for usage, or omit subcommands to start the MCP server.`),1):null}var cliExitCode=await dispatchCliSubcommand(process.argv.slice(2));cliExitCode!==null&&process.exit(cliExitCode);var server=new McpServer({name:"bridge-api",version:"1.0.0"}),toolSurfaceLifecycle=new AbortController;function runToolSurfaceProbe(){let url;try{url=buildGetUrl("/mcp/tool-surface",{repo_name:REPO_NAME})}catch{return Promise.resolve({reason:"timeout",blockedTools:new Set})}return probeToolSurface({url,resolveHeaders:getGetHeaders,fetchFn:(input,init)=>fetch(input,init),abortSignal:toolSurfaceLifecycle.signal})}var toolSurfaceStartupProbe=TOOL_SURFACE_GATING_ENABLED?runToolSurfaceProbe():null,ADVERTISED=[];server.registerResource("readme","bridge-api://readme",{title:"Bridge API MCP \u2014 README",description:"Overview and feature reference for the Bridge API MCP server (the same README published with the npm package).",mimeType:"text/markdown"},async uri=>({contents:[{uri:uri.href,mimeType:"text/markdown",text:README}]}));var TOOL_HANDLERS=new Map,registerTool=((name,config,handler)=>{let active=!0,wrappedHandler=async(args,extra)=>active?await handler(args,extra):{content:[{type:"text",text:JSON.stringify({error:"TOOL_DISABLED",status:503,message:`Tool "${name}" is currently disabled.`})}]},toolHandle=server.registerTool.bind(server)(name,config,wrappedHandler),inProcessHandler=async params=>await wrappedHandler(params);if(TOOL_HANDLERS.set(name,{handler:inProcessHandler,isEnabled:()=>active}),ADVERTISED.push({name,handle:toolHandle,isEnabled:()=>active}),toolHandle&&typeof toolHandle.enable=="function"){let sdkEnable=toolHandle.enable.bind(toolHandle);toolHandle.enable=()=>(active=!0,sdkEnable())}if(toolHandle&&typeof toolHandle.disable=="function"){let sdkDisable=toolHandle.disable.bind(toolHandle);toolHandle.disable=()=>(active=!1,sdkDisable())}return toolHandle}),commonFields={ticket_number:z16.string(),repo_name:z16.string().optional(),save_locally:z16.boolean().optional().default(!0),wait_for_result:z16.boolean().optional().default(!1).describe("When true, blocks and polls until ready, returning full content directly. When false (default), returns immediately with confirmation/handle. Use the corresponding get_* tool to retrieve results."),second_opinion:z16.string().optional().describe("Provider routing override for THIS request. NOT the standalone second_opinion tool. Takes precedence over provider."),provider:z16.string().optional().describe("Use a specific LLM provider (openai, anthropic, gemini) without triggering second-opinion semantics.")};ACTIVE_GROUPS.has("conductor")&®isterConductorTools(registerTool);registerSfccTools(registerTool,{buildGetUrl,getGetHeaders,getPostHeaders,repoName:REPO_NAME,getResolvedApiKey,getDocsDir,includeReadTools:ACTIVE_GROUPS.has("sfcc")});registerTool("ping",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Test connectivity to Bridge API. Validates that the API key is accepted and the configured repository is accessible. Returns JSON with {status: 'ok', repo_name: '<configured repo>'}. The response also reports MCP version metadata: mcp_version (your installed MCP server version), latest_mcp_version (latest published), upgrade_available, upgrade_advice (a short, optional note when a newer version is available), and release_state_stale. These are informational \u2014 an available upgrade is not an error. Use this as a quick health check before other operations, or to verify your Bridge API configuration is working. A 403 response means the API key is invalid or the repo is not authorized. If the server is unreachable, check that BAPI_BASE_URL points to a running Bridge API instance.",inputSchema:{}},async()=>{let url=buildGetUrl("/ping",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});if(!resp.ok)return{content:[{type:"text",text:await handleResponse(resp)}]};let raw=await resp.text();try{let body=JSON.parse(raw),content=[{type:"text",text:JSON.stringify(body,null,2)}];if(typeof body.upgrade_advice=="string"&&body.upgrade_advice.length>0){let actionableAdvice=`Update available: running ${body.mcp_version}, latest ${body.latest_mcp_version} \u2014 run npx -y @bridge_gpt/mcp-server@latest --upgrade`;content.push({type:"text",text:actionableAdvice})}return{content}}catch{return{content:[{type:"text",text:raw}]}}});registerTool("second_opinion",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Use to get an immediate, ad hoc independent critique on a plan or analysis you already have. Returns the responding model's reply text plus the resolved provider. This does NOT create or retrieve a Bridge artifact (use request_* tools for that).",inputSchema:{prompt:z16.string().describe("The complete, self-contained brief to send to the second-opinion model. Include the full plan, recommendation, analysis, or question you want challenged, plus enough context for the responder to evaluate it independently. This is sent as the user message; the server constructs the system prompt."),provider:z16.enum(["anthropic","openai","gemini"]).describe("LLM provider family for the second opinion. Choose a family DIFFERENT from the one you are running on so the response is genuinely independent."),model:z16.enum(["CHEAP_MODEL","BASIC_MODEL","PREMIUM_MODEL"]).describe("Model tier within the chosen provider. CHEAP_MODEL for quick sanity checks, BASIC_MODEL for focused reviews, PREMIUM_MODEL for serious architectural pushback.")}},async({prompt,provider,model})=>{let submitResp=await fetch(buildApiUrl("/llm/second-opinion"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify({repo_name:REPO_NAME,prompt,provider,model})});if(!submitResp.ok)return{content:[{type:"text",text:await handleResponse(submitResp)}]};let requestId=(await submitResp.json()).request_id;if(typeof requestId!="number")return{content:[{type:"text",text:JSON.stringify({error:"Second opinion submit response is missing request_id",status:500})}]};let repoQuery=`repo_name=${encodeURIComponent(REPO_NAME)}`,statusUrl=buildApiUrl(`/llm/second-opinion/${requestId}/status?${repoQuery}`),resultUrl=buildApiUrl(`/llm/second-opinion/${requestId}/result?${repoQuery}`),startTime=Date.now(),timeoutMs=3e5,pollIntervalMs=3e3,finalStatus="";for(;;){if(await new Promise(resolve2=>setTimeout(resolve2,pollIntervalMs)),Date.now()-startTime>=timeoutMs)return{content:[{type:"text",text:`Second opinion is still processing after ${Math.round(timeoutMs/1e3)}s (request_id=${requestId}). The result is recoverable from the server via GET /llm/second-opinion/${requestId}/result?${repoQuery} once it finishes.`}]};Date.now()-startTime>3e4&&(pollIntervalMs=8e3);let statusResp=await fetch(statusUrl,{headers:await getGetHeaders()});if(!statusResp.ok)return{content:[{type:"text",text:await handleResponse(statusResp)}]};let statusBody=await statusResp.json();if(finalStatus=typeof statusBody.status=="string"?statusBody.status:"",finalStatus==="completed"||finalStatus==="failed")break}let resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resultResp)}]}});registerTool("generate_image",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Generate an image from a text prompt using a provider image model. This tool spends provider credits on every call \u2014 cost scales with quality (low/medium/high). Defaults to low quality to minimize provider spend; increase quality only when fidelity matters. Returns native MCP image content (type: 'image') so the caller receives the image directly. The image is always also saved to the local BAPI_DOCS_DIR/images/ directory. Google Imagen outputs (provider='gemini') include an invisible SynthID watermark applied server-side by Google.",inputSchema:{prompt:z16.string().min(1).max(8e3).describe("Text prompt sent to the image provider."),provider:z16.enum(["openai","gemini"]).optional().default("openai").describe("Image provider. Defaults to 'openai' (gpt-image-2)."),quality:z16.enum(["low","medium","high"]).optional().default("low").describe("Image quality. Defaults to 'low' for cost control."),size:z16.enum(["1024x1024","1024x1536","1536x1024"]).optional().default("1024x1024").describe("Image dimensions. Defaults to '1024x1024'.")}},async({prompt,provider,quality,size})=>{let submitResp=await fetch(buildApiUrl("/llm/generate-image"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify({repo_name:REPO_NAME,prompt,provider,quality,size})});if(!submitResp.ok)return{content:[{type:"text",text:await handleResponse(submitResp)}]};let requestId=(await submitResp.json()).request_id;if(typeof requestId!="number")return{content:[{type:"text",text:JSON.stringify({error:"Image generation submit response is missing request_id",status:500})}]};let repoQuery=`repo_name=${encodeURIComponent(REPO_NAME)}`,statusUrl=buildApiUrl(`/llm/generate-image/${requestId}/status?${repoQuery}`),resultUrl=buildApiUrl(`/llm/generate-image/${requestId}/result?${repoQuery}`),startTime=Date.now(),timeoutMs=3e5,pollIntervalMs=2e3,finalStatus="";for(;;){if(await new Promise(resolve2=>setTimeout(resolve2,pollIntervalMs)),Date.now()-startTime>=timeoutMs)return{content:[{type:"text",text:`Image generation is still processing after ${Math.round(timeoutMs/1e3)}s (request_id=${requestId}). The result is recoverable from the server via GET /llm/generate-image/${requestId}/result once it finishes.`}]};Date.now()-startTime>2e4&&(pollIntervalMs=5e3);let statusResp=await fetch(statusUrl,{headers:await getGetHeaders()});if(!statusResp.ok)return{content:[{type:"text",text:await handleResponse(statusResp)}]};let statusBody=await statusResp.json();if(finalStatus=typeof statusBody.status=="string"?statusBody.status:"",finalStatus==="completed"||finalStatus==="failed")break}if(finalStatus==="failed"){let failResp=await fetch(resultUrl,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(failResp)}]}}let resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});if(!resultResp.ok)return{content:[{type:"text",text:await handleResponse(resultResp)}]};let body=await resultResp.json(),imageBase64=body.image_base64;if(typeof imageBase64!="string"||imageBase64.length===0)return{content:[{type:"text",text:JSON.stringify({error:"Image generation succeeded but response is missing image_base64",status:500})}]};let mimeType=typeof body.mime_type=="string"&&body.mime_type.length>0?body.mime_type:"image/png",content=[{type:"image",data:imageBase64,mimeType}];try{let imagesDir=await getDocsPath("images"),filename=`generated-image-${safeTimestampForFilename()}.png`,filePath=`${imagesDir}/${filename}`;await mkdir12(imagesDir,{recursive:!0}),await writeFile12(filePath,Buffer.from(imageBase64,"base64")),content.push({type:"text",text:`Saved to ${filePath}`})}catch(saveErr){content.push({type:"text",text:`Warning: image generated successfully but local save failed: ${saveErr instanceof Error?saveErr.message:String(saveErr)}`})}return{content}});async function fetchVisualDiffAttachmentBytes(ref){return{ok:!1,error:"ATTACHMENT_SOURCE_UNAVAILABLE",status:501,message:`Resolving a comp from a Jira ${ref.kind==="attachment_id"?`attachment id "${ref.attachment_id}"`:`attachment filename "${ref.filename}"`} is not yet supported by visual_diff. Pass comp_ref as a local file path to the comp image (absolute, or relative to the project root). Binary-safe, ticket-independent attachment retrieval is delivered by a separate ticket.`}}registerTool("visual_diff",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Deterministic pixel-fidelity oracle. Renders target_url headlessly at the comp size (viewport auto-matched, DPR 1), disables animation/font/caret jitter, then diffs vs a design comp (comp_ref: local path or Jira attachment) with AA tolerance. Returns mismatch_pct + diff_regions + a heatmap image; pass budget defaults to non-zero (2%), never 0%.",inputSchema:{target_url:z16.string().min(1).describe("URL of the rendered page to screenshot (e.g. http://localhost:8000/...)."),comp_ref:z16.string().min(1).describe("The design comp: a local file path (absolute or relative to the project root), or a Jira attachment id/filename."),viewport:z16.object({width:z16.number().int().positive(),height:z16.number().int().positive()}).optional().describe("Explicit render viewport. Omit to auto-match the comp's intrinsic pixel dimensions."),mask_selectors:z16.array(z16.string().min(1)).optional().describe("CSS selectors blacked out in BOTH images before diffing (dynamic/time-varying content)."),threshold:z16.number().positive().max(100).optional().default(2).describe("Pass budget as a percent of differing pixels (default 2%). Never 0%.")}},async args=>await runVisualDiff(args,{getProjectRoot,getDocsPath,safeTimestampForFilename,readFile:p=>readFile14(p),stat:p=>stat10(p),mkdir:(p,opts)=>mkdir12(p,opts),writeFile:(p,data)=>writeFile12(p,data),fetchAttachmentBytes:fetchVisualDiffAttachmentBytes,logger:console.error}));registerTool("get_project_standards",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Retrieve project-specific coding standards, architecture guidelines, testing standards, code review standards, and project context (platform, version, project description) for the configured repository. Returns structured markdown with sections for project context, architecture instructions, code review correctness standards, testing stack information, and build analysis. Only sections with configured values are included. Returns 404 if no standards are configured. Consult these standards before writing or reviewing code to ensure compliance with project conventions. Successful oversized output is automatically saved under the local docs directory and returned inline as a truncated preview.",inputSchema:{}},async()=>{let url=buildGetUrl("/project-standards",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()}),ok=resp.ok,text=await handleResponse(resp);if(ok){let filename=`${safeTicketFileSegment(REPO_NAME||"repo")}-project-standards.md`;text=await truncateAndSaveIfNeeded2(text,await getDocsPath("project-standards"),filename)}return{content:[{type:"text",text}]}});registerTool("get_tickets",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Search for and list Jira tickets from the configured project. Filters by query text, status name, label, or date. Returns up to 'limit' tickets ordered by most recently updated. All data is fetched live from Jira. Use get_ticket to retrieve full details for a specific ticket.",inputSchema:{query:z16.string().optional().describe(`Free-text search string. Filters tickets via JQL text ~ '...' (searches summary, description, comments). Examples: "authentication error", "login page crash", "payment timeout"`),status:z16.string().optional().describe("Filter by Jira status name (e.g. 'To Do', 'In Progress', 'Done')"),labels:z16.string().optional().describe('Comma-separated Jira labels. Filters tickets via JQL labels in (...) (matches tickets carrying any of the given labels). Labels cannot contain spaces. Example: "bapi-idea-to-ticket-fa-1a2b3c"'),limit:z16.number().optional().default(20).describe("Maximum number of tickets to return (1-100, default 20)"),offset:z16.number().optional().default(0).describe("Number of results to skip for pagination (default 0)"),updated_since:z16.string().optional().describe("ISO date string (YYYY-MM-DD). Only return tickets updated on or after this date")}},async({query,status,labels,limit,offset,updated_since})=>{let params={repo_name:REPO_NAME};query&&(params.query=query),status&&(params.status=status),labels&&(params.labels=labels),limit!==void 0&&(params.limit=String(limit)),offset!==void 0&&offset>0&&(params.offset=String(offset)),updated_since&&(params.updated_since=updated_since);let url=buildGetUrl("/tickets",params),resp=await fetch(url,{headers:await getGetHeaders()}),ok=resp.ok,text=await handleResponse(resp);if(ok){let filename=buildTicketsSearchFilename({query,status,labels,updated_since,limit,offset});text=await truncateAndSaveIfNeeded2(text,await getDocsPath("tickets-search"),filename)}return{content:[{type:"text",text}]}});registerTool("get_ticket",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Retrieve full details for a single Jira ticket by its key. Returns summary, status, type, assignee, reporter, description, and timestamps. All data is fetched live from Jira. Use get_tickets to search/list multiple tickets. Use get_comments to fetch comments on the ticket.",inputSchema:{ticket_number:commonFields.ticket_number}},async({ticket_number})=>{let url=buildGetUrl(`/tickets/${encodeURIComponent(ticket_number)}`,{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()}),ok=resp.ok,text=await handleResponse(resp);if(ok){let filename=`${safeTicketFileSegment(ticket_number)}.json`;text=await truncateAndSaveIfNeeded2(text,await getDocsPath("tickets"),filename)}return{content:[{type:"text",text}]}});registerTool("get_ticket_model_tier",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:`Resolve the coarse implementation-model TIER for a Jira ticket from its difficulty. Returns { difficulty: int|null, tier: "cheap"|"basic"|"premium"|null, source: "cached"|"computed"|"fallback" }. The backend computes difficulty on demand (and caches it) when missing, and never returns a model id \u2014 the tier->model mapping is owned by the start-tickets CLI. A null tier (source="fallback") means the model could not be resolved and callers should use the agent's default model.`,inputSchema:{ticket_number:commonFields.ticket_number}},async({ticket_number})=>{let url=buildGetUrl(`/tickets/${encodeURIComponent(ticket_number)}/model-tier`,{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("get_comments",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Retrieve all comments on a Jira ticket, oldest-first. Returns an array of {id, author, body, created, updated}. Comment bodies are Markdown (converted from Jira wiki markup). Use this to read what a developer or stakeholder has said on a ticket. Use add_comment to post a new comment.",inputSchema:{ticket_number:commonFields.ticket_number}},async({ticket_number})=>{let url=buildGetUrl(`/tickets/${encodeURIComponent(ticket_number)}/comments`,{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()}),ok=resp.ok,text=await handleResponse(resp);if(ok){let filename=`${safeTicketFileSegment(ticket_number)}-comments.json`;text=await truncateAndSaveIfNeeded2(text,await getDocsPath("comments"),filename)}return{content:[{type:"text",text}]}});registerTool("create_ticket",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Create a new Jira ticket in the configured project. Requires either description or file_path (or both \u2014 file_path takes precedence). Returns JSON with {ticket_key: 'PROJ-123', url: 'https://...'}. The ticket is created immediately in Jira \u2014 confirm details with the user before calling. The description field supports Jira markdown formatting. Pass parent_key ONLY when creating a child ticket under an existing Jira Epic; omit it for standalone tickets and for Epic parent creation itself.",inputSchema:{summary:z16.string().describe("Ticket title \u2014 keep under 100 characters"),description:z16.string().optional().describe("Required unless file_path is provided. Detailed description in markdown. Recommended structure: Summary (2-4 sentences), Requirements (bullet list with code file references), Acceptance Criteria (testable 'Done when...' statements)"),file_path:z16.string().optional().describe("Path to a local markdown file whose contents will be used as the ticket description. If both file_path and description are provided, file_path takes precedence. The file must be UTF-8 encoded and under 1MB."),issue_type:z16.string().describe("One of: 'Bug' (defect), 'Story' (user-facing feature), 'Task' (technical/infrastructure work)"),priority:z16.string().optional().describe("One of: 'Highest', 'High', 'Medium', 'Low', 'Lowest'. Omit to use Jira project default"),labels:z16.array(z16.string()).optional().describe("List of Jira labels to apply (e.g. ['frontend', 'tech-debt'])"),assignee:z16.string().optional().describe("Jira username or account ID of the assignee. Omit to leave unassigned"),parent_key:z16.string().optional().describe("Optional Jira Epic key to set as the parent of the newly created child issue. Omit for standalone tickets and Epic parent creation.")}},async({summary,description,file_path,issue_type,priority,labels,assignee,parent_key})=>{let resolved=await resolveTextOrFile(description,file_path,"description");return resolved.ok?{content:[{type:"text",text:await createTicketRequest({summary,description:resolved.text,issue_type,priority,labels,assignee,parent_key})+resolved.note}]}:resolved.errorResponse});registerTool("get_plan",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE an already-generated implementation plan for a Jira ticket as markdown. This tool only fetches an existing plan \u2014 it does NOT start or trigger plan generation. If no plan exists yet (or you need a fresh one), call `request_plan_generation` first; it starts the async generation and this `get_plan` tool retrieves the result. Returns the full plan as markdown verbatim \u2014 present it without summarizing. Returns a 404 / not-found response when no plan is ready yet \u2014 that means generation has not run, not that this tool failed. Tip: call get_clarifying_questions for the same ticket to get the full context for implementation.",inputSchema:{ticket_number:commonFields.ticket_number,save_locally:commonFields.save_locally}},async args=>getTicketArtifact("plan",args));registerTool("get_architecture",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE an already-generated architecture plan for a Jira ticket. This tool only fetches an existing architecture plan \u2014 it does NOT start or trigger generation. If no architecture plan exists yet (or you need a fresh one), call `request_architecture` first; it starts the async generation and this `get_architecture` tool retrieves the result. Returns the full architecture plan as markdown text \u2014 present it verbatim without summarizing. The plan includes high-level architectural decisions, component design, and integration guidance. Returns a 404 / not-found response when no architecture plan is ready yet \u2014 that means generation has not run, not that this tool failed.",inputSchema:{ticket_number:commonFields.ticket_number,save_locally:commonFields.save_locally}},async args=>getTicketArtifact("architecture",args));registerTool("get_prd",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE an already-generated Product Requirements Document (PRD) for a Jira ticket. This tool only fetches an existing PRD \u2014 it does NOT start or trigger generation. If no PRD exists yet (or you need a fresh one), call `request_prd` first; it starts the async generation and this `get_prd` tool retrieves the result. Returns the full PRD as markdown text \u2014 present it verbatim without summarizing. The PRD is product/stakeholder-facing: problem framing, goals, success metrics, scope, and product requirements. Returns a 404 / not-found response when no PRD is ready yet \u2014 that means generation has not run, not that this tool failed.",inputSchema:{ticket_number:commonFields.ticket_number,save_locally:commonFields.save_locally}},async args=>getTicketArtifact("prd",args));registerTool("get_clarifying_questions",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE already-generated clarifying questions (for feature/task tickets) or debugging guidance (for bug tickets) for a Jira ticket. This tool only fetches existing questions \u2014 it does NOT start or trigger generation. If no questions exist yet (or you need fresh ones), call `request_clarifying_questions` first; it starts the async generation and this `get_clarifying_questions` tool retrieves the result. Returns markdown text with questions that should be resolved before implementation begins. Returns a 404 / not-found response when no questions are ready yet \u2014 that means generation has not run, not that this tool failed. Tip: call get_plan for the same ticket to get the implementation plan alongside these questions.",inputSchema:{ticket_number:commonFields.ticket_number,save_locally:commonFields.save_locally}},async args=>getTicketArtifact("clarifying_questions",args));registerTool("parse_repository",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Queue a background job to parse and index the repository for Bridge API's AI agents. The API only ENQUEUES the work; the CPU-bound parse runs in a separate process, so this returns immediately and never blocks. This should be run after major codebase changes so that plans and questions reflect the latest code. Returns 202 with {message: 'Repository parsing queued'} on success, or {message: 'Repository parsing already in progress'} if a job is already running. The job runs asynchronously \u2014 there is no completion callback; poll get_parse_status to observe when it reaches terminal success or terminal failure. For large repositories this may take several minutes. Confirm with the user before triggering.",inputSchema:{directory_path:z16.string().optional().describe("Subdirectory to scope the parse to (e.g. 'src/python'). Omit to parse the entire repository")}},async({directory_path})=>{let payload={repo_name:REPO_NAME};directory_path&&(payload.directory_path=directory_path);let resp=await fetch(buildUrl("/parse-repository"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("regenerate_directory_map",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Regenerate the repository directory map and return the result. Unlike parse_repository (which is async), this tool is synchronous \u2014 it blocks until the directory map is generated and returns the full map text directly. Use this when you need the directory map immediately (e.g. for architecture analysis). May take 30-120 seconds for large repositories.",inputSchema:{}},async()=>{let controller=new AbortController,timeout=setTimeout(()=>controller.abort(),12e4);try{let resp=await fetch(buildUrl("/regenerate-directory-map"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify({repo_name:REPO_NAME}),signal:controller.signal});return{content:[{type:"text",text:await handleResponse(resp)}]}}finally{clearTimeout(timeout)}});registerTool("get_parse_status",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Check a repository parse job's status (status surface only \u2014 never runs the parse). Returns {status}: 'idle', 'queued', 'in_progress' (running in a separate process), 'succeeded', or 'failed'. Terminal responses may add finished_at, attempt_count, and a concise last_error. repo_name is injected from the configured environment.",inputSchema:{}},async()=>{let url=buildGetUrl("/parse-status",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("add_comment",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:`Post a comment on a Jira ticket. The comment appears immediately in Jira. Supports markdown formatting. For long comments (over ~2000 characters), set attach_as_file to true \u2014 this attaches the comment as a .md file instead of posting inline, which avoids Jira's comment length limitations.
|
|
4907
|
+
${flagName} must be run from your project root (the directory containing package.json).`}}async function runInitCli(cwd){let guardError=await ensurePackageJsonForCliCommand("--init",cwd);if(guardError)return console.error(guardError),1;try{return await runInit(cwd),0}catch(err){let msg=err instanceof Error?err.message:String(err);return console.error(`Bridge API --init failed: ${msg}`),1}}async function dispatchCliSubcommand(argv){let cwd=process.cwd();if(argv[0]==="start-tickets")return runStartTicketsCli(argv.slice(1));if(argv[0]==="review-tickets")return runReviewTicketsCli(argv.slice(1));if(argv[0]==="mcp-invoke")return runMcpInvokeCli(argv.slice(1));if(argv[0]==="doctor")return runDoctorCli(argv.slice(1));if(argv[0]==="schedule-run")return runScheduleRunCli(argv.slice(1));if(argv[0]==="agent-capabilities")return runAgentCapabilitiesCli(argv.slice(1));if(argv[0]==="executor"&&argv[1]==="watch")return runExecutorWatchCli(argv.slice(2));if(argv[0]==="executor")return runExecutorCli(argv.slice(1));if(argv[0]==="setup-epic")return runSetupEpicCli(argv.slice(1));if(argv[0]==="emit-conductor-bundle")return runConductorBundleCli(argv.slice(1));if(argv[0]==="regression-check")return runRegressionCheckCli(argv.slice(1));if(argv[0]==="credentials")return runCredentialsCli(argv.slice(1));if(argv[0]==="conductor")return runConductorCli(argv.slice(1));if(argv[0]==="install-bridge")try{return await runInstallBridgeCli(argv.slice(1))}catch(error){let lines=buildInstallBridgeFailureLines(error,{step:INSTALL_BRIDGE_DISPATCH_STEP_LABEL,debugEnabled:!!process.env.BAPI_INSTALL_DEBUG});for(let line of lines)console.error(line);return 1}return argv[0]==="connect-github"?runConnectGithubCli(argv.slice(1)):argv[0]==="upgrade"?runUpgradeCli(argv.slice(1)):argv.includes("--version")?(console.log(VERSION),0):argv.includes("--init")?runInitCli(cwd):argv.includes("--upgrade")?runUpgradeCli(argv):argv.length>0&&!argv[0].startsWith("-")?(console.error(`Error: Unknown subcommand '${argv[0]}'. Run with --help for usage, or omit subcommands to start the MCP server.`),1):null}var cliExitCode=await dispatchCliSubcommand(process.argv.slice(2));cliExitCode!==null&&process.exit(cliExitCode);var server=new McpServer({name:"bridge-api",version:"1.0.0"}),toolSurfaceLifecycle=new AbortController;function runToolSurfaceProbe(){let url;try{url=buildGetUrl("/mcp/tool-surface",{repo_name:REPO_NAME})}catch{return Promise.resolve({reason:"timeout",blockedTools:new Set})}return probeToolSurface({url,resolveHeaders:getGetHeaders,fetchFn:(input,init)=>fetch(input,init),abortSignal:toolSurfaceLifecycle.signal})}var toolSurfaceStartupProbe=TOOL_SURFACE_GATING_ENABLED?runToolSurfaceProbe():null,ADVERTISED=[];server.registerResource("readme","bridge-api://readme",{title:"Bridge API MCP \u2014 README",description:"Overview and feature reference for the Bridge API MCP server (the same README published with the npm package).",mimeType:"text/markdown"},async uri=>({contents:[{uri:uri.href,mimeType:"text/markdown",text:README}]}));var TOOL_HANDLERS=new Map,registerTool=((name,config,handler)=>{let active=!0,wrappedHandler=async(args,extra)=>active?await handler(args,extra):{content:[{type:"text",text:JSON.stringify({error:"TOOL_DISABLED",status:503,message:`Tool "${name}" is currently disabled.`})}]},toolHandle=server.registerTool.bind(server)(name,config,wrappedHandler),inProcessHandler=async params=>await wrappedHandler(params);if(TOOL_HANDLERS.set(name,{handler:inProcessHandler,isEnabled:()=>active}),ADVERTISED.push({name,handle:toolHandle,isEnabled:()=>active}),toolHandle&&typeof toolHandle.enable=="function"){let sdkEnable=toolHandle.enable.bind(toolHandle);toolHandle.enable=()=>(active=!0,sdkEnable())}if(toolHandle&&typeof toolHandle.disable=="function"){let sdkDisable=toolHandle.disable.bind(toolHandle);toolHandle.disable=()=>(active=!1,sdkDisable())}return toolHandle}),commonFields={ticket_number:z16.string(),repo_name:z16.string().optional(),save_locally:z16.boolean().optional().default(!0),wait_for_result:z16.boolean().optional().default(!1).describe("When true, blocks and polls until ready, returning full content directly. When false (default), returns immediately with confirmation/handle. Use the corresponding get_* tool to retrieve results."),second_opinion:z16.string().optional().describe("Provider routing override for THIS request. NOT the standalone second_opinion tool. Takes precedence over provider."),provider:z16.string().optional().describe("Use a specific LLM provider (openai, anthropic, gemini) without triggering second-opinion semantics.")};ACTIVE_GROUPS.has("conductor")&®isterConductorTools(registerTool);registerSfccTools(registerTool,{buildGetUrl,getGetHeaders,getPostHeaders,repoName:REPO_NAME,getResolvedApiKey,getDocsDir,includeReadTools:ACTIVE_GROUPS.has("sfcc")});registerTool("ping",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Test connectivity to Bridge API. Validates that the API key is accepted and the configured repository is accessible. Returns JSON with {status: 'ok', repo_name: '<configured repo>'}. The response also reports MCP version metadata: mcp_version (your installed MCP server version), latest_mcp_version (latest published), upgrade_available, upgrade_advice (a short, optional note when a newer version is available), and release_state_stale. These are informational \u2014 an available upgrade is not an error. Use this as a quick health check before other operations, or to verify your Bridge API configuration is working. A 403 response means the API key is invalid or the repo is not authorized. If the server is unreachable, check that BAPI_BASE_URL points to a running Bridge API instance.",inputSchema:{}},async()=>{let url=buildGetUrl("/ping",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});if(!resp.ok)return{content:[{type:"text",text:await handleResponse(resp)}]};let raw=await resp.text();try{let body=JSON.parse(raw),content=[{type:"text",text:JSON.stringify(body,null,2)}];if(typeof body.upgrade_advice=="string"&&body.upgrade_advice.length>0){let actionableAdvice=`Update available: running ${body.mcp_version}, latest ${body.latest_mcp_version} \u2014 run npx -y @bridge_gpt/mcp-server@latest --upgrade`;content.push({type:"text",text:actionableAdvice})}return{content}}catch{return{content:[{type:"text",text:raw}]}}});registerTool("second_opinion",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Use to get an immediate, ad hoc independent critique on a plan or analysis you already have. Returns the responding model's reply text plus the resolved provider. This does NOT create or retrieve a Bridge artifact (use request_* tools for that).",inputSchema:{prompt:z16.string().describe("The complete, self-contained brief to send to the second-opinion model. Include the full plan, recommendation, analysis, or question you want challenged, plus enough context for the responder to evaluate it independently. This is sent as the user message; the server constructs the system prompt."),provider:z16.enum(["anthropic","openai","gemini"]).describe("LLM provider family for the second opinion. Choose a family DIFFERENT from the one you are running on so the response is genuinely independent."),model:z16.enum(["CHEAP_MODEL","BASIC_MODEL","PREMIUM_MODEL"]).describe("Model tier within the chosen provider. CHEAP_MODEL for quick sanity checks, BASIC_MODEL for focused reviews, PREMIUM_MODEL for serious architectural pushback.")}},async({prompt,provider,model})=>{let submitResp=await fetch(buildApiUrl("/llm/second-opinion"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify({repo_name:REPO_NAME,prompt,provider,model})});if(!submitResp.ok)return{content:[{type:"text",text:await handleResponse(submitResp)}]};let requestId=(await submitResp.json()).request_id;if(typeof requestId!="number")return{content:[{type:"text",text:JSON.stringify({error:"Second opinion submit response is missing request_id",status:500})}]};let repoQuery=`repo_name=${encodeURIComponent(REPO_NAME)}`,statusUrl=buildApiUrl(`/llm/second-opinion/${requestId}/status?${repoQuery}`),resultUrl=buildApiUrl(`/llm/second-opinion/${requestId}/result?${repoQuery}`),startTime=Date.now(),timeoutMs=3e5,pollIntervalMs=3e3,finalStatus="";for(;;){if(await new Promise(resolve2=>setTimeout(resolve2,pollIntervalMs)),Date.now()-startTime>=timeoutMs)return{content:[{type:"text",text:`Second opinion is still processing after ${Math.round(timeoutMs/1e3)}s (request_id=${requestId}). The result is recoverable from the server via GET /llm/second-opinion/${requestId}/result?${repoQuery} once it finishes.`}]};Date.now()-startTime>3e4&&(pollIntervalMs=8e3);let statusResp=await fetch(statusUrl,{headers:await getGetHeaders()});if(!statusResp.ok)return{content:[{type:"text",text:await handleResponse(statusResp)}]};let statusBody=await statusResp.json();if(finalStatus=typeof statusBody.status=="string"?statusBody.status:"",finalStatus==="completed"||finalStatus==="failed")break}let resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resultResp)}]}});registerTool("generate_image",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Generate an image from a text prompt using a provider image model. This tool spends provider credits on every call \u2014 cost scales with quality (low/medium/high). Defaults to low quality to minimize provider spend; increase quality only when fidelity matters. Returns native MCP image content (type: 'image') so the caller receives the image directly. The image is always also saved to the local BAPI_DOCS_DIR/images/ directory. Google Imagen outputs (provider='gemini') include an invisible SynthID watermark applied server-side by Google.",inputSchema:{prompt:z16.string().min(1).max(8e3).describe("Text prompt sent to the image provider."),provider:z16.enum(["openai","gemini"]).optional().default("openai").describe("Image provider. Defaults to 'openai' (gpt-image-2)."),quality:z16.enum(["low","medium","high"]).optional().default("low").describe("Image quality. Defaults to 'low' for cost control."),size:z16.enum(["1024x1024","1024x1536","1536x1024"]).optional().default("1024x1024").describe("Image dimensions. Defaults to '1024x1024'.")}},async({prompt,provider,quality,size})=>{let submitResp=await fetch(buildApiUrl("/llm/generate-image"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify({repo_name:REPO_NAME,prompt,provider,quality,size})});if(!submitResp.ok)return{content:[{type:"text",text:await handleResponse(submitResp)}]};let requestId=(await submitResp.json()).request_id;if(typeof requestId!="number")return{content:[{type:"text",text:JSON.stringify({error:"Image generation submit response is missing request_id",status:500})}]};let repoQuery=`repo_name=${encodeURIComponent(REPO_NAME)}`,statusUrl=buildApiUrl(`/llm/generate-image/${requestId}/status?${repoQuery}`),resultUrl=buildApiUrl(`/llm/generate-image/${requestId}/result?${repoQuery}`),startTime=Date.now(),timeoutMs=3e5,pollIntervalMs=2e3,finalStatus="";for(;;){if(await new Promise(resolve2=>setTimeout(resolve2,pollIntervalMs)),Date.now()-startTime>=timeoutMs)return{content:[{type:"text",text:`Image generation is still processing after ${Math.round(timeoutMs/1e3)}s (request_id=${requestId}). The result is recoverable from the server via GET /llm/generate-image/${requestId}/result once it finishes.`}]};Date.now()-startTime>2e4&&(pollIntervalMs=5e3);let statusResp=await fetch(statusUrl,{headers:await getGetHeaders()});if(!statusResp.ok)return{content:[{type:"text",text:await handleResponse(statusResp)}]};let statusBody=await statusResp.json();if(finalStatus=typeof statusBody.status=="string"?statusBody.status:"",finalStatus==="completed"||finalStatus==="failed")break}if(finalStatus==="failed"){let failResp=await fetch(resultUrl,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(failResp)}]}}let resultResp=await fetch(resultUrl,{headers:await getGetHeaders()});if(!resultResp.ok)return{content:[{type:"text",text:await handleResponse(resultResp)}]};let body=await resultResp.json(),imageBase64=body.image_base64;if(typeof imageBase64!="string"||imageBase64.length===0)return{content:[{type:"text",text:JSON.stringify({error:"Image generation succeeded but response is missing image_base64",status:500})}]};let mimeType=typeof body.mime_type=="string"&&body.mime_type.length>0?body.mime_type:"image/png",content=[{type:"image",data:imageBase64,mimeType}];try{let imagesDir=await getDocsPath("images"),filename=`generated-image-${safeTimestampForFilename()}.png`,filePath=`${imagesDir}/${filename}`;await mkdir12(imagesDir,{recursive:!0}),await writeFile12(filePath,Buffer.from(imageBase64,"base64")),content.push({type:"text",text:`Saved to ${filePath}`})}catch(saveErr){content.push({type:"text",text:`Warning: image generated successfully but local save failed: ${saveErr instanceof Error?saveErr.message:String(saveErr)}`})}return{content}});async function fetchVisualDiffAttachmentBytes(ref){return{ok:!1,error:"ATTACHMENT_SOURCE_UNAVAILABLE",status:501,message:`Resolving a comp from a Jira ${ref.kind==="attachment_id"?`attachment id "${ref.attachment_id}"`:`attachment filename "${ref.filename}"`} is not yet supported by visual_diff. Pass comp_ref as a local file path to the comp image (absolute, or relative to the project root). Binary-safe, ticket-independent attachment retrieval is delivered by a separate ticket.`}}registerTool("visual_diff",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Deterministic pixel-fidelity oracle. Renders target_url headlessly at the comp size (viewport auto-matched, DPR 1), disables animation/font/caret jitter, then diffs vs a design comp (comp_ref: local path or Jira attachment) with AA tolerance. Returns mismatch_pct + diff_regions + a heatmap image; pass budget defaults to non-zero (2%), never 0%.",inputSchema:{target_url:z16.string().min(1).describe("URL of the rendered page to screenshot (e.g. http://localhost:8000/...)."),comp_ref:z16.string().min(1).describe("The design comp: a local file path (absolute or relative to the project root), or a Jira attachment id/filename."),viewport:z16.object({width:z16.number().int().positive(),height:z16.number().int().positive()}).optional().describe("Explicit render viewport. Omit to auto-match the comp's intrinsic pixel dimensions."),mask_selectors:z16.array(z16.string().min(1)).optional().describe("CSS selectors blacked out in BOTH images before diffing (dynamic/time-varying content)."),threshold:z16.number().positive().max(100).optional().default(2).describe("Pass budget as a percent of differing pixels (default 2%). Never 0%.")}},async args=>await runVisualDiff(args,{getProjectRoot,getDocsPath,safeTimestampForFilename,readFile:p=>readFile14(p),stat:p=>stat10(p),mkdir:(p,opts)=>mkdir12(p,opts),writeFile:(p,data)=>writeFile12(p,data),fetchAttachmentBytes:fetchVisualDiffAttachmentBytes,logger:console.error}));registerTool("get_project_standards",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Retrieve project-specific coding standards, architecture guidelines, testing standards, code review standards, and project context (platform, version, project description) for the configured repository. Returns structured markdown with sections for project context, architecture instructions, code review correctness standards, testing stack information, and build analysis. Only sections with configured values are included. Returns 404 if no standards are configured. Consult these standards before writing or reviewing code to ensure compliance with project conventions. Successful oversized output is automatically saved under the local docs directory and returned inline as a truncated preview.",inputSchema:{}},async()=>{let url=buildGetUrl("/project-standards",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()}),ok=resp.ok,text=await handleResponse(resp);if(ok){let filename=`${safeTicketFileSegment(REPO_NAME||"repo")}-project-standards.md`;text=await truncateAndSaveIfNeeded2(text,await getDocsPath("project-standards"),filename)}return{content:[{type:"text",text}]}});registerTool("get_tickets",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Search for and list Jira tickets from the configured project. Filters by query text, status name, label, or date. Returns up to 'limit' tickets ordered by most recently updated. All data is fetched live from Jira. Use get_ticket to retrieve full details for a specific ticket.",inputSchema:{query:z16.string().optional().describe(`Free-text search string. Filters tickets via JQL text ~ '...' (searches summary, description, comments). Examples: "authentication error", "login page crash", "payment timeout"`),status:z16.string().optional().describe("Filter by Jira status name (e.g. 'To Do', 'In Progress', 'Done')"),labels:z16.string().optional().describe('Comma-separated Jira labels. Filters tickets via JQL labels in (...) (matches tickets carrying any of the given labels). Labels cannot contain spaces. Example: "bapi-idea-to-ticket-fa-1a2b3c"'),limit:z16.number().optional().default(20).describe("Maximum number of tickets to return (1-100, default 20)"),offset:z16.number().optional().default(0).describe("Number of results to skip for pagination (default 0)"),updated_since:z16.string().optional().describe("ISO date string (YYYY-MM-DD). Only return tickets updated on or after this date")}},async({query,status,labels,limit,offset,updated_since})=>{let params={repo_name:REPO_NAME};query&&(params.query=query),status&&(params.status=status),labels&&(params.labels=labels),limit!==void 0&&(params.limit=String(limit)),offset!==void 0&&offset>0&&(params.offset=String(offset)),updated_since&&(params.updated_since=updated_since);let url=buildGetUrl("/tickets",params),resp=await fetch(url,{headers:await getGetHeaders()}),ok=resp.ok,text=await handleResponse(resp);if(ok){let filename=buildTicketsSearchFilename({query,status,labels,updated_since,limit,offset});text=await truncateAndSaveIfNeeded2(text,await getDocsPath("tickets-search"),filename)}return{content:[{type:"text",text}]}});registerTool("get_ticket",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Retrieve full details for a single Jira ticket by its key. Returns summary, status, type, assignee, reporter, description, and timestamps. All data is fetched live from Jira. Use get_tickets to search/list multiple tickets. Use get_comments to fetch comments on the ticket.",inputSchema:{ticket_number:commonFields.ticket_number}},async({ticket_number})=>{let url=buildGetUrl(`/tickets/${encodeURIComponent(ticket_number)}`,{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()}),ok=resp.ok,text=await handleResponse(resp);if(ok){let filename=`${safeTicketFileSegment(ticket_number)}.json`;text=await truncateAndSaveIfNeeded2(text,await getDocsPath("tickets"),filename)}return{content:[{type:"text",text}]}});registerTool("get_ticket_model_tier",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:`Resolve the coarse implementation-model TIER for a Jira ticket from its difficulty. Returns { difficulty: int|null, tier: "cheap"|"basic"|"premium"|null, source: "cached"|"computed"|"fallback" }. The backend computes difficulty on demand (and caches it) when missing, and never returns a model id \u2014 the tier->model mapping is owned by the start-tickets CLI. A null tier (source="fallback") means the model could not be resolved and callers should use the agent's default model.`,inputSchema:{ticket_number:commonFields.ticket_number}},async({ticket_number})=>{let url=buildGetUrl(`/tickets/${encodeURIComponent(ticket_number)}/model-tier`,{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("get_comments",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Retrieve all comments on a Jira ticket, oldest-first. Returns an array of {id, author, body, created, updated}. Comment bodies are Markdown (converted from Jira wiki markup). Use this to read what a developer or stakeholder has said on a ticket. Use add_comment to post a new comment.",inputSchema:{ticket_number:commonFields.ticket_number}},async({ticket_number})=>{let url=buildGetUrl(`/tickets/${encodeURIComponent(ticket_number)}/comments`,{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()}),ok=resp.ok,text=await handleResponse(resp);if(ok){let filename=`${safeTicketFileSegment(ticket_number)}-comments.json`;text=await truncateAndSaveIfNeeded2(text,await getDocsPath("comments"),filename)}return{content:[{type:"text",text}]}});registerTool("create_ticket",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:"Create a new Jira ticket in the configured project. Requires either description or file_path (or both \u2014 file_path takes precedence). Returns JSON with {ticket_key: 'PROJ-123', url: 'https://...'}. The ticket is created immediately in Jira \u2014 confirm details with the user before calling. The description field supports Jira markdown formatting. Pass parent_key ONLY when creating a child ticket under an existing Jira Epic; omit it for standalone tickets and for Epic parent creation itself.",inputSchema:{summary:z16.string().describe("Ticket title \u2014 keep under 100 characters"),description:z16.string().optional().describe("Required unless file_path is provided. Detailed description in markdown. Recommended structure: Summary (2-4 sentences), Requirements (bullet list with code file references), Acceptance Criteria (testable 'Done when...' statements)"),file_path:z16.string().optional().describe("Path to a local markdown file whose contents will be used as the ticket description. If both file_path and description are provided, file_path takes precedence. The file must be UTF-8 encoded and under 1MB."),issue_type:z16.string().describe("One of: 'Bug' (defect), 'Story' (user-facing feature), 'Task' (technical/infrastructure work)"),priority:z16.string().optional().describe("One of: 'Highest', 'High', 'Medium', 'Low', 'Lowest'. Omit to use Jira project default"),labels:z16.array(z16.string()).optional().describe("List of Jira labels to apply (e.g. ['frontend', 'tech-debt'])"),assignee:z16.string().optional().describe("Jira username or account ID of the assignee. Omit to leave unassigned"),parent_key:z16.string().optional().describe("Optional Jira Epic key to set as the parent of the newly created child issue. Omit for standalone tickets and Epic parent creation.")}},async({summary,description,file_path,issue_type,priority,labels,assignee,parent_key})=>{let resolved=await resolveTextOrFile(description,file_path,"description");return resolved.ok?{content:[{type:"text",text:await createTicketRequest({summary,description:resolved.text,issue_type,priority,labels,assignee,parent_key})+resolved.note}]}:resolved.errorResponse});registerTool("get_plan",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE an already-generated implementation plan for a Jira ticket as markdown. This tool only fetches an existing plan \u2014 it does NOT start or trigger plan generation. If no plan exists yet (or you need a fresh one), call `request_plan_generation` first; it starts the async generation and this `get_plan` tool retrieves the result. Returns the full plan as markdown verbatim \u2014 present it without summarizing. Returns a 404 / not-found response when no plan is ready yet \u2014 that means generation has not run, not that this tool failed. Tip: call get_clarifying_questions for the same ticket to get the full context for implementation.",inputSchema:{ticket_number:commonFields.ticket_number,save_locally:commonFields.save_locally}},async args=>getTicketArtifact("plan",args));registerTool("get_architecture",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE an already-generated architecture plan for a Jira ticket. This tool only fetches an existing architecture plan \u2014 it does NOT start or trigger generation. If no architecture plan exists yet (or you need a fresh one), call `request_architecture` first; it starts the async generation and this `get_architecture` tool retrieves the result. Returns the full architecture plan as markdown text \u2014 present it verbatim without summarizing. The plan includes high-level architectural decisions, component design, and integration guidance. Returns a 404 / not-found response when no architecture plan is ready yet \u2014 that means generation has not run, not that this tool failed.",inputSchema:{ticket_number:commonFields.ticket_number,save_locally:commonFields.save_locally}},async args=>getTicketArtifact("architecture",args));registerTool("get_prd",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE an already-generated Product Requirements Document (PRD) for a Jira ticket. This tool only fetches an existing PRD \u2014 it does NOT start or trigger generation. If no PRD exists yet (or you need a fresh one), call `request_prd` first; it starts the async generation and this `get_prd` tool retrieves the result. Returns the full PRD as markdown text \u2014 present it verbatim without summarizing. The PRD is product/stakeholder-facing: problem framing, goals, success metrics, scope, and product requirements. Returns a 404 / not-found response when no PRD is ready yet \u2014 that means generation has not run, not that this tool failed.",inputSchema:{ticket_number:commonFields.ticket_number,save_locally:commonFields.save_locally}},async args=>getTicketArtifact("prd",args));registerTool("get_clarifying_questions",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"RETRIEVE already-generated clarifying questions (for feature/task tickets) or debugging guidance (for bug tickets) for a Jira ticket. This tool only fetches existing questions \u2014 it does NOT start or trigger generation. If no questions exist yet (or you need fresh ones), call `request_clarifying_questions` first; it starts the async generation and this `get_clarifying_questions` tool retrieves the result. Returns markdown text with questions that should be resolved before implementation begins. Returns a 404 / not-found response when no questions are ready yet \u2014 that means generation has not run, not that this tool failed. Tip: call get_plan for the same ticket to get the implementation plan alongside these questions.",inputSchema:{ticket_number:commonFields.ticket_number,save_locally:commonFields.save_locally}},async args=>getTicketArtifact("clarifying_questions",args));registerTool("parse_repository",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Queue a background job to parse and index the repository for Bridge API's AI agents. The API only ENQUEUES the work; the CPU-bound parse runs in a separate process, so this returns immediately and never blocks. This should be run after major codebase changes so that plans and questions reflect the latest code. Returns 202 with {message: 'Repository parsing queued'} on success, or {message: 'Repository parsing already in progress'} if a job is already running. The job runs asynchronously \u2014 there is no completion callback; poll get_parse_status to observe when it reaches terminal success or terminal failure. For large repositories this may take several minutes. Confirm with the user before triggering.",inputSchema:{directory_path:z16.string().optional().describe("Subdirectory to scope the parse to (e.g. 'src/python'). Omit to parse the entire repository")}},async({directory_path})=>{let payload={repo_name:REPO_NAME};directory_path&&(payload.directory_path=directory_path);let resp=await fetch(buildUrl("/parse-repository"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("regenerate_directory_map",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Regenerate the repository directory map and return the result. Unlike parse_repository (which is async), this tool is synchronous \u2014 it blocks until the directory map is generated and returns the full map text directly. Use this when you need the directory map immediately (e.g. for architecture analysis). May take 30-120 seconds for large repositories.",inputSchema:{}},async()=>{let controller=new AbortController,timeout=setTimeout(()=>controller.abort(),12e4);try{let resp=await fetch(buildUrl("/regenerate-directory-map"),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify({repo_name:REPO_NAME}),signal:controller.signal});return{content:[{type:"text",text:await handleResponse(resp)}]}}finally{clearTimeout(timeout)}});registerTool("get_parse_status",{annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},description:"Check a repository parse job's status (status surface only \u2014 never runs the parse). Returns {status}: 'idle', 'queued', 'in_progress' (running in a separate process), 'succeeded', or 'failed'. Terminal responses may add finished_at, attempt_count, and a concise last_error. repo_name is injected from the configured environment.",inputSchema:{}},async()=>{let url=buildGetUrl("/parse-status",{repo_name:REPO_NAME}),resp=await fetch(url,{headers:await getGetHeaders()});return{content:[{type:"text",text:await handleResponse(resp)}]}});registerTool("add_comment",{annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1,openWorldHint:!0},description:`Post a comment on a Jira ticket. The comment appears immediately in Jira. Supports markdown formatting. For long comments (over ~2000 characters), set attach_as_file to true \u2014 this attaches the comment as a .md file instead of posting inline, which avoids Jira's comment length limitations.
|
|
4907
4908
|
|
|
4908
4909
|
Tip: To generate plans, clarifying questions, or ticket critiques, use the dedicated request_plan_generation, request_clarifying_questions, or request_ticket_critique tools.`,inputSchema:{ticket_number:commonFields.ticket_number,comment:z16.string().optional().describe("Comment text in markdown format. Can include code blocks, lists, headings, etc. Optional if file_path is provided."),file_path:z16.string().optional().describe("Path to a local markdown file whose contents will be used as the comment. If both file_path and comment are provided, file_path takes precedence. The file must be UTF-8 encoded and under 1MB."),attach_as_file:z16.boolean().optional().default(!1).describe("Set to true to attach the comment as a .md file instead of posting inline. Recommended for comments over 2000 characters"),file_name:z16.string().optional().describe("Custom filename for the attached .md file (only used when attach_as_file is true). Defaults to {ticket_number}-comment.md if not provided. Example: 'PROJ-123-clarifying-questions.md'")}},async({ticket_number,comment,file_path,attach_as_file,file_name})=>{let resolved=await resolveTextOrFile(comment,file_path,"comment");if(!resolved.ok)return resolved.errorResponse;let payload={repo_name:REPO_NAME,comment:resolved.text,attach_as_file};file_name&&(payload.file_name=file_name);let resp=await fetch(buildUrl(`/ticket/${encodeURIComponent(ticket_number)}/comment`),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)+resolved.note}]}});registerTool("update_ticket_description",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!0,openWorldHint:!0},description:"Update the description of an existing Jira ticket. This is a direct, synchronous update that overwrites the existing description with the provided text. The description should be in markdown format \u2014 it will be automatically converted to Jira wiki markup. This does NOT create a new ticket. Use create_ticket for that. Returns a success message with the ticket number, or an error if the update fails.",inputSchema:{ticket_number:commonFields.ticket_number,description:z16.string().optional().describe("New description text in markdown format. Optional if file_path is provided. This will completely replace the existing description."),file_path:z16.string().optional().describe("Path to a local markdown file whose contents will be used as the new description. If both file_path and description are provided, file_path takes precedence. The file must be UTF-8 encoded and under 1MB.")}},async({ticket_number,description,file_path})=>{let resolved=await resolveTextOrFile(description,file_path,"description");if(!resolved.ok)return resolved.errorResponse;let resp=await fetch(buildUrl(`/ticket/${encodeURIComponent(ticket_number)}/description`),{method:"PUT",headers:await getPostHeaders(),body:JSON.stringify({repo_name:REPO_NAME,description:resolved.text})});return{content:[{type:"text",text:await handleResponse(resp)+resolved.note}]}});registerTool("attachment",{annotations:{readOnlyHint:!1,destructiveHint:!0,idempotentHint:!1,openWorldHint:!0},description:"Manages Jira attachments. Operations: upload, download, list, delete. upload: text or image/png, image/jpeg, image/webp, image/gif (10 MB max), else rejected. download saves binary/image attachments as raw bytes to a file_path; UTF-8 text is inline. delete removes by exact filename (no-op if absent).",inputSchema:z16.discriminatedUnion("operation",[z16.object({operation:z16.literal("upload"),ticket_number:commonFields.ticket_number,file_path:z16.string().optional().describe("Path to a local file to upload. Binary uploads are restricted to the allowlisted image types image/png, image/jpeg, image/webp, image/gif, up to `10 MB`; other binaries such as PDFs and ZIPs are rejected as unsupported attachment types. Text uploads are up to `1 MB`. If both file_path and content are provided, file_path takes precedence."),content:z16.string().max(1048576).optional().describe("Inline text content to upload (max `1 MB`). Optional if file_path is provided."),file_name:z16.string().optional().describe("Filename for the attachment in Jira. Defaults to the basename of file_path if provided, or {ticket_number}-attachment.md otherwise."),link_type:z16.string().optional().describe("When provided, also syncs the content to Bridge API's tickets_links table. Known values: clarifying-questions.md, debugging-guidance.md, ticket-quality-critique.md, architecture-plan.md, fsd-plan.md, prd-plan.md. Cannot be used with binary file uploads."),replace_existing:z16.boolean().optional().default(!0).describe("When true (default), deletes any existing attachment with the same filename before uploading.")}).strict(),z16.object({operation:z16.literal("download"),ticket_number:commonFields.ticket_number,attachment_id:z16.string().optional().describe("Jira attachment ID. Mutually exclusive with filename. For design/UI tickets, pass the attachment_id from the plan's DESIGN COMP CANDIDATES section to fetch the design comp."),filename:z16.string().optional().describe("Attachment filename. If multiple exist, returns the most recent. Mutually exclusive with attachment_id."),file_path:z16.string().optional().describe("Override the default save location (must stay within the project root/worktree). Pass a file_path when you need to open an image/design comp locally. If omitted, saves to {BAPI_DOCS_DIR}/attachments/{ticket_number}/{filename}.")}).strict(),z16.object({operation:z16.literal("list"),ticket_number:commonFields.ticket_number,include_ai_generated:z16.boolean().optional().describe("Include AI-generated attachments in the list (default: false)")}).strict(),z16.object({operation:z16.literal("delete"),ticket_number:commonFields.ticket_number,file_name:z16.string().describe("Exact filename of the attachment to delete. Deleting an absent filename succeeds as a no-op (reported result has deleted=false) rather than erroring.")}).strict()])},async args=>{switch(args.operation){case"upload":{let{ticket_number,file_path,content,file_name,link_type,replace_existing}=args,derivedFileName=file_name||(file_path?path38.basename(file_path):`${ticket_number}-attachment.md`),resolved=await resolveUploadAttachment(content,file_path,"content",derivedFileName);if(!resolved.ok)return resolved.errorResponse;let payload={repo_name:REPO_NAME,content:resolved.text,file_name:derivedFileName,replace_existing};resolved.encoding&&(payload.encoding=resolved.encoding),resolved.contentType&&(payload.content_type=resolved.contentType),link_type&&(payload.link_type=link_type);let resp=await fetch(buildUrl(`/ticket/${encodeURIComponent(ticket_number)}/attachment`),{method:"POST",headers:await getPostHeaders(),body:JSON.stringify(payload)});return{content:[{type:"text",text:await handleResponse(resp)+resolved.note}]}}case"download":{let{ticket_number,attachment_id,filename,file_path}=args;if(!attachment_id&&!filename)return{content:[{type:"text",text:JSON.stringify({error:"VALIDATION_ERROR",message:"Provide either attachment_id or filename (at least one is required)."})}]};let params={repo_name:REPO_NAME};attachment_id&&(params.attachment_id=attachment_id),filename&&(params.filename=filename);let url=buildGetUrl(`/ticket/${encodeURIComponent(ticket_number)}/attachments/download`,params),resp=await fetch(url,{headers:await getGetHeaders()});if(!resp.ok)return{content:[{type:"text",text:await handleResponse(resp)}]};let body=await resp.json(),serverFilename=body.filename,content=body.content,isText=body.is_text,mimeType=body.mime_type,size=body.size,safeFileName=path38.basename(serverFilename),safeTicket=path38.basename(ticket_number),savePath=file_path||path38.join(await getDocsDir(),"attachments",safeTicket,safeFileName),resolvedSave=path38.resolve(savePath),resolvedRoot=path38.resolve(await getProjectRoot());if(!resolvedSave.startsWith(resolvedRoot+path38.sep)&&resolvedSave!==resolvedRoot)return{content:[{type:"text",text:JSON.stringify({error:"VALIDATION_ERROR",message:`Save path "${savePath}" is outside the project root. Refusing to write.`})}]};await mkdir12(path38.dirname(resolvedSave),{recursive:!0}),isText?await writeFile12(resolvedSave,content,"utf-8"):await writeFile12(resolvedSave,Buffer.from(content,"base64"));let resultText=`File saved to: ${resolvedSave}
|
|
4909
4910
|
Filename: ${safeFileName}
|