cleargate 0.11.5 → 0.12.0

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/dist/cli.cjs CHANGED
@@ -783,7 +783,7 @@ var import_commander = require("commander");
783
783
  // package.json
784
784
  var package_default = {
785
785
  name: "cleargate",
786
- version: "0.11.5",
786
+ version: "0.12.0",
787
787
  private: false,
788
788
  type: "module",
789
789
  description: "Planning framework for Claude Code agents \u2014 sprint/epic/story protocol, five-role agent team (architect/developer/qa/devops/reporter), Karpathy-style awareness wiki.",
@@ -2304,6 +2304,24 @@ var PREFIX_MAP = [
2304
2304
  { prefix: "BUG-", type: "bug", bucket: "bugs" },
2305
2305
  { prefix: "INITIATIVE-", type: "initiative", bucket: "initiatives" }
2306
2306
  ];
2307
+ var SPRINT_REPORT_FILENAMES = ["REPORT.md"];
2308
+ var SPRINT_REPORT_CANONICAL_RE = /^SPRINT-\d{2,}_REPORT\.md$/;
2309
+ function isSprintReportPath(relPath) {
2310
+ const normalised = relPath.replace(/\\/g, "/");
2311
+ const match = /\.cleargate\/sprint-runs\/(SPRINT-\d{2,})\/([^/]+)$/.exec(normalised);
2312
+ if (!match) return false;
2313
+ const filename = match[2];
2314
+ return SPRINT_REPORT_FILENAMES.includes(filename) || SPRINT_REPORT_CANONICAL_RE.test(filename);
2315
+ }
2316
+ function deriveBucketFromReportPath(relPath) {
2317
+ const normalised = relPath.replace(/\\/g, "/");
2318
+ const match = /\.cleargate\/sprint-runs\/(SPRINT-\d{2,})\//.exec(normalised);
2319
+ if (!match) {
2320
+ throw new Error(`deriveBucketFromReportPath: cannot extract SPRINT-NN from: ${relPath}`);
2321
+ }
2322
+ const id = match[1];
2323
+ return { type: "sprint", id, bucket: "sprints" };
2324
+ }
2307
2325
  function deriveBucket(filename) {
2308
2326
  const base = filename.includes("/") ? filename.split("/").pop() : filename;
2309
2327
  const stem = base.endsWith(".md") ? base.slice(0, -3) : base;
@@ -2427,6 +2445,12 @@ function serializePage(page, body) {
2427
2445
  if (page.sprint_cleargate_id !== void 0) {
2428
2446
  lines.push(`sprint_cleargate_id: "${page.sprint_cleargate_id}"`);
2429
2447
  }
2448
+ if (page.report_raw_path !== void 0) {
2449
+ lines.push(`report_raw_path: "${page.report_raw_path}"`);
2450
+ }
2451
+ if (page.last_report_ingest_commit !== void 0) {
2452
+ lines.push(`last_report_ingest_commit: "${page.last_report_ingest_commit}"`);
2453
+ }
2430
2454
  lines.push("---");
2431
2455
  const fm = lines.join("\n");
2432
2456
  return `${fm}
@@ -2449,7 +2473,9 @@ function parsePage(raw) {
2449
2473
  const last_contradict_sha = fm["last_contradict_sha"] !== void 0 ? String(fm["last_contradict_sha"]) : void 0;
2450
2474
  const parent_cleargate_id = fm["parent_cleargate_id"] !== void 0 ? String(fm["parent_cleargate_id"]) : void 0;
2451
2475
  const sprint_cleargate_id = fm["sprint_cleargate_id"] !== void 0 ? String(fm["sprint_cleargate_id"]) : void 0;
2452
- return { type, id, parent, children, status, remote_id, raw_path, last_ingest, last_ingest_commit, repo, last_contradict_sha, parent_cleargate_id, sprint_cleargate_id };
2476
+ const report_raw_path = fm["report_raw_path"] !== void 0 ? String(fm["report_raw_path"]) : void 0;
2477
+ const last_report_ingest_commit = fm["last_report_ingest_commit"] !== void 0 ? String(fm["last_report_ingest_commit"]) : void 0;
2478
+ return { type, id, parent, children, status, remote_id, raw_path, last_ingest, last_ingest_commit, repo, last_contradict_sha, parent_cleargate_id, sprint_cleargate_id, report_raw_path, last_report_ingest_commit };
2453
2479
  }
2454
2480
  function parseFmRaw(raw) {
2455
2481
  const lines = raw.split("\n");
@@ -3981,28 +4007,33 @@ async function wikiIngestHandler(opts) {
3981
4007
  const rawPath = opts.rawPath;
3982
4008
  const absRawPath = path20.isAbsolute(rawPath) ? rawPath : path20.resolve(cwd, rawPath);
3983
4009
  const relRawPath = path20.relative(cwd, absRawPath).replace(/\\/g, "/");
3984
- const deliveryRoot = path20.join(cwd, ".cleargate", "delivery");
3985
- const deliveryRootNorm = deliveryRoot.replace(/\\/g, "/");
3986
- const absDeliveryRoot = deliveryRoot;
3987
- const relToDelivery = path20.relative(absDeliveryRoot, absRawPath);
3988
- if (relToDelivery.startsWith("..") || path20.isAbsolute(relToDelivery)) {
3989
- stderr(`wiki ingest: ${rawPath} not under .cleargate/delivery/
4010
+ const isSprintReport = isSprintReportPath(relRawPath);
4011
+ if (!isSprintReport) {
4012
+ const deliveryRoot = path20.join(cwd, ".cleargate", "delivery");
4013
+ const absDeliveryRoot = deliveryRoot;
4014
+ const relToDelivery = path20.relative(absDeliveryRoot, absRawPath);
4015
+ if (relToDelivery.startsWith("..") || path20.isAbsolute(relToDelivery)) {
4016
+ stderr(`wiki ingest: ${rawPath} not under .cleargate/delivery/
3990
4017
  `);
3991
- exit(2);
3992
- return;
3993
- }
3994
- void deliveryRootNorm;
3995
- const isExcluded = EXCLUDED_SUFFIXES2.some((excl) => relRawPath.startsWith(excl));
3996
- if (isExcluded) {
3997
- stdout(`wiki ingest: ${rawPath} excluded (skip)
4018
+ exit(2);
4019
+ return;
4020
+ }
4021
+ const isExcluded = EXCLUDED_SUFFIXES2.some((excl) => relRawPath.startsWith(excl));
4022
+ if (isExcluded) {
4023
+ stdout(`wiki ingest: ${rawPath} excluded (skip)
3998
4024
  `);
3999
- exit(0);
4000
- return;
4025
+ exit(0);
4026
+ return;
4027
+ }
4001
4028
  }
4002
4029
  const filename = path20.basename(absRawPath);
4003
4030
  let bucketInfo;
4004
4031
  try {
4005
- bucketInfo = deriveBucket(filename);
4032
+ if (isSprintReport) {
4033
+ bucketInfo = deriveBucketFromReportPath(relRawPath);
4034
+ } else {
4035
+ bucketInfo = deriveBucket(filename);
4036
+ }
4006
4037
  } catch (e) {
4007
4038
  stderr(`wiki ingest: cannot determine bucket for ${rawPath}: ${e.message}
4008
4039
  `);
@@ -4045,16 +4076,26 @@ async function wikiIngestHandler(opts) {
4045
4076
  }
4046
4077
  const currentSha = getGitSha(absRawPath, gitRunner) ?? "";
4047
4078
  const pageExists = fs19.existsSync(pagePath);
4048
- if (pageExists && currentSha !== "") {
4049
- let isNoOp = false;
4079
+ let existingPage;
4080
+ if (pageExists) {
4050
4081
  try {
4051
4082
  const existingPageContent = fs19.readFileSync(pagePath, "utf8");
4052
- const existingPage = parsePage(existingPageContent);
4053
- if (existingPage.last_ingest_commit === currentSha) {
4054
- const contentUnchanged = checkContentUnchanged(absRawPath, currentSha, relRawPath, gitRunner);
4055
- if (contentUnchanged) {
4083
+ existingPage = parsePage(existingPageContent);
4084
+ } catch {
4085
+ }
4086
+ }
4087
+ if (existingPage !== void 0 && currentSha !== "") {
4088
+ let isNoOp = false;
4089
+ try {
4090
+ if (isSprintReport) {
4091
+ if (existingPage.last_report_ingest_commit === currentSha) {
4056
4092
  isNoOp = true;
4057
4093
  }
4094
+ } else {
4095
+ if (existingPage.last_ingest_commit === currentSha) {
4096
+ const contentUnchanged = checkContentUnchanged(absRawPath, currentSha, relRawPath, gitRunner);
4097
+ if (contentUnchanged) isNoOp = true;
4098
+ }
4058
4099
  }
4059
4100
  } catch {
4060
4101
  }
@@ -4066,36 +4107,53 @@ async function wikiIngestHandler(opts) {
4066
4107
  }
4067
4108
  }
4068
4109
  const action = pageExists ? "update" : "create";
4069
- let existingLastContradictSha;
4070
- if (pageExists) {
4071
- try {
4072
- const existingPageContent = fs19.readFileSync(pagePath, "utf8");
4073
- const existingPage = parsePage(existingPageContent);
4074
- existingLastContradictSha = existingPage.last_contradict_sha;
4075
- } catch {
4076
- }
4077
- }
4110
+ const timestamp = now();
4111
+ const existingLastContradictSha = existingPage?.last_contradict_sha;
4112
+ const existingReportRawPath = existingPage?.report_raw_path;
4113
+ const existingLastReportIngestCommit = existingPage?.last_report_ingest_commit;
4078
4114
  const parent = buildParentRef2(fm);
4079
4115
  const children = buildChildrenRefs2(fm);
4080
- const timestamp = now();
4081
4116
  const wikiPage = {
4082
4117
  type,
4083
4118
  id,
4084
- parent,
4085
- children,
4086
- status: String(fm["status"] ?? ""),
4087
- remote_id: String(fm["remote_id"] ?? ""),
4088
- raw_path: relRawPath,
4119
+ parent: isSprintReport ? existingPage?.parent ?? "" : parent,
4120
+ children: isSprintReport ? existingPage?.children ?? [] : children,
4121
+ status: isSprintReport ? existingPage?.status ?? String(fm["status"] ?? "") : String(fm["status"] ?? ""),
4122
+ remote_id: isSprintReport ? existingPage?.remote_id ?? String(fm["remote_id"] ?? "") : String(fm["remote_id"] ?? ""),
4123
+ // raw_path tracks the plan file path; for report-only ingest preserve existing or use relRawPath as fallback
4124
+ raw_path: isSprintReport ? existingPage?.raw_path ?? relRawPath : relRawPath,
4089
4125
  last_ingest: timestamp,
4090
- last_ingest_commit: currentSha,
4126
+ // last_ingest_commit tracks the plan source; preserve when re-ingesting report
4127
+ last_ingest_commit: isSprintReport ? existingPage?.last_ingest_commit ?? "" : currentSha,
4091
4128
  repo,
4092
4129
  // Carry forward last_contradict_sha so Phase 4 idempotency survives re-ingest
4093
4130
  ...existingLastContradictSha !== void 0 ? { last_contradict_sha: existingLastContradictSha } : {},
4094
- // Hierarchy keys (§11.7): read from raw fm stamped at raw-side, not wiki-side
4095
- ...typeof fm["parent_cleargate_id"] === "string" ? { parent_cleargate_id: fm["parent_cleargate_id"] } : {},
4096
- ...typeof fm["sprint_cleargate_id"] === "string" ? { sprint_cleargate_id: fm["sprint_cleargate_id"] } : {}
4131
+ // Hierarchy keys (§11.7): read from raw fm for plan ingest; preserve for report ingest
4132
+ ...isSprintReport ? existingPage?.parent_cleargate_id !== void 0 ? { parent_cleargate_id: existingPage.parent_cleargate_id } : {} : typeof fm["parent_cleargate_id"] === "string" ? { parent_cleargate_id: fm["parent_cleargate_id"] } : {},
4133
+ ...isSprintReport ? existingPage?.sprint_cleargate_id !== void 0 ? { sprint_cleargate_id: existingPage.sprint_cleargate_id } : {} : typeof fm["sprint_cleargate_id"] === "string" ? { sprint_cleargate_id: fm["sprint_cleargate_id"] } : {},
4134
+ // CR-063 sprint-report fields
4135
+ report_raw_path: isSprintReport ? relRawPath : existingReportRawPath ?? void 0,
4136
+ last_report_ingest_commit: isSprintReport ? currentSha : existingLastReportIngestCommit ?? void 0
4097
4137
  };
4098
- const pageBody = buildPageBody2({ id, fm, body });
4138
+ let existingPageBody = "";
4139
+ if (existingPage !== void 0 && pageExists) {
4140
+ try {
4141
+ const existingPageContent = fs19.readFileSync(pagePath, "utf8");
4142
+ const lines = existingPageContent.split("\n");
4143
+ let closingDash = -1;
4144
+ for (let i = 1; i < lines.length; i++) {
4145
+ if (lines[i] === "---") {
4146
+ closingDash = i;
4147
+ break;
4148
+ }
4149
+ }
4150
+ if (closingDash !== -1) {
4151
+ existingPageBody = lines.slice(closingDash + 1).join("\n").replace(/^\n/, "");
4152
+ }
4153
+ } catch {
4154
+ }
4155
+ }
4156
+ const pageBody = buildPageBody2({ id, fm, body, isSprintReport, existingPageBody });
4099
4157
  const pageContent = serializePage(wikiPage, pageBody);
4100
4158
  fs19.mkdirSync(pageDir, { recursive: true });
4101
4159
  fs19.writeFileSync(pagePath, pageContent, "utf8");
@@ -4173,7 +4231,37 @@ function buildChildrenRefs2(fm) {
4173
4231
  return `[[${s}]]`;
4174
4232
  });
4175
4233
  }
4234
+ function extractReportBlock(pageBody) {
4235
+ const beginMarker = "<!-- BEGIN sprint-report -->";
4236
+ const endMarker = "<!-- END sprint-report -->";
4237
+ const beginIdx = pageBody.indexOf(beginMarker);
4238
+ const endIdx = pageBody.indexOf(endMarker);
4239
+ if (beginIdx === -1 || endIdx === -1 || endIdx <= beginIdx) return void 0;
4240
+ return pageBody.slice(beginIdx, endIdx + endMarker.length);
4241
+ }
4242
+ function extractPlanStub(pageBody) {
4243
+ const beginMarker = "<!-- BEGIN sprint-report -->";
4244
+ const beginIdx = pageBody.indexOf(beginMarker);
4245
+ if (beginIdx === -1) return pageBody;
4246
+ return pageBody.slice(0, beginIdx);
4247
+ }
4176
4248
  function buildPageBody2(item) {
4249
+ const { isSprintReport, existingPageBody } = item;
4250
+ if (isSprintReport) {
4251
+ const planStub = existingPageBody ? extractPlanStub(existingPageBody) : buildPlanStub(item);
4252
+ const reportBlock = buildReportBlock(item.body);
4253
+ return planStub + reportBlock;
4254
+ } else {
4255
+ const planStub = buildPlanStub(item);
4256
+ const existingReportBlock = existingPageBody ? extractReportBlock(existingPageBody) : void 0;
4257
+ if (existingReportBlock !== void 0) {
4258
+ const stub = planStub.trimEnd() + "\n\n";
4259
+ return stub + existingReportBlock + "\n";
4260
+ }
4261
+ return planStub;
4262
+ }
4263
+ }
4264
+ function buildPlanStub(item) {
4177
4265
  const title = String(item.fm["title"] ?? item.id);
4178
4266
  const summary = String(
4179
4267
  item.fm["description"] ?? item.body.split("\n")[0] ?? "No summary available."
@@ -4197,6 +4285,16 @@ function buildPageBody2(item) {
4197
4285
  ""
4198
4286
  ].join("\n");
4199
4287
  }
4288
+ function buildReportBlock(reportBody) {
4289
+ return [
4290
+ "<!-- BEGIN sprint-report -->",
4291
+ "## Sprint Report",
4292
+ "",
4293
+ reportBody.trim(),
4294
+ "<!-- END sprint-report -->",
4295
+ ""
4296
+ ].join("\n");
4297
+ }
4200
4298
  function appendLogEntry(wikiRoot, entry) {
4201
4299
  const logPath = path20.join(wikiRoot, "log.md");
4202
4300
  const logEntry = [
@@ -6677,7 +6775,7 @@ async function writeCachedGate(absPath, result, opts) {
6677
6775
  throw new Error(`writeCachedGate: failed to parse frontmatter in ${absPath}`);
6678
6776
  }
6679
6777
  const existing = coerceCachedGate(fm["cached_gate_result"]);
6680
- if (existing && JSON.stringify(existing) === JSON.stringify(newResult)) {
6778
+ if (existing && existing.pass === newResult.pass && JSON.stringify(existing.failing_criteria) === JSON.stringify(newResult.failing_criteria)) {
6681
6779
  return;
6682
6780
  }
6683
6781
  const newFm = {};
@@ -8203,6 +8301,10 @@ function refreshScopedGateCaches(sprintId, cwd, execFn) {
8203
8301
  if (!absPath) {
8204
8302
  continue;
8205
8303
  }
8304
+ if (absPath.includes(`${path36.sep}archive${path36.sep}`)) {
8305
+ result.skipped.push(id);
8306
+ continue;
8307
+ }
8206
8308
  let status = "";
8207
8309
  try {
8208
8310
  const raw = fs34.readFileSync(absPath, "utf8");
@@ -11524,6 +11626,19 @@ async function pushHandler(fileOrId, opts = {}) {
11524
11626
  async function handlePush(filePath, ctx) {
11525
11627
  const { projectRoot, identity, sprintRoot, nowFn, resolveMcp, stdout, stderr, exit } = ctx;
11526
11628
  const resolvedPath = path55.isAbsolute(filePath) ? filePath : path55.resolve(projectRoot, filePath);
11629
+ if (SPRINT_RUNS_PATH_REGEX.test(resolvedPath)) {
11630
+ if (!SPRINT_REPORT_PATH_REGEX.test(resolvedPath)) {
11631
+ stderr(
11632
+ `Error: path not in allowlist. Only sprint report files are accepted from sprint-runs/.
11633
+ Allowed: .cleargate/sprint-runs/SPRINT-NN/REPORT.md
11634
+ .cleargate/sprint-runs/SPRINT-NN/SPRINT-NN_REPORT.md
11635
+ Got: "${resolvedPath}"
11636
+ `
11637
+ );
11638
+ exit(2);
11639
+ return;
11640
+ }
11641
+ }
11527
11642
  let rawContent;
11528
11643
  try {
11529
11644
  rawContent = await fsPromises11.readFile(resolvedPath, "utf8");
@@ -11553,7 +11668,7 @@ async function handlePush(filePath, ctx) {
11553
11668
  return;
11554
11669
  }
11555
11670
  const itemId = getItemId4(fm);
11556
- const type = getItemType2(fm);
11671
+ const type = getItemTypeWithPathOverride(resolvedPath, fm);
11557
11672
  if (!type) {
11558
11673
  stderr(`Error: cannot determine item type from frontmatter in "${resolvedPath}".
11559
11674
  `);
@@ -11566,6 +11681,9 @@ async function handlePush(filePath, ctx) {
11566
11681
  if (h1) payloadForPush["title"] = h1;
11567
11682
  }
11568
11683
  payloadForPush["body"] = body;
11684
+ if (payloadForPush["origin"] === void 0) {
11685
+ payloadForPush["origin"] = "cleargate-cli";
11686
+ }
11569
11687
  const mcp2 = await resolveMcp();
11570
11688
  let result;
11571
11689
  try {
@@ -11685,17 +11803,20 @@ async function writeAtomic8(filePath, content) {
11685
11803
  await fsPromises11.rename(tmpPath, filePath);
11686
11804
  }
11687
11805
  function getItemId4(fm) {
11688
- for (const key of ["story_id", "epic_id", "proposal_id", "cr_id", "bug_id"]) {
11806
+ for (const key of ["story_id", "epic_id", "proposal_id", "sprint_id", "cr_id", "bug_id"]) {
11689
11807
  const val = fm[key];
11690
11808
  if (typeof val === "string" && val) return val;
11691
11809
  }
11692
11810
  return "unknown";
11693
11811
  }
11812
+ var SPRINT_RUNS_PATH_REGEX = /\.cleargate[\\/]sprint-runs[\\/]/;
11813
+ var SPRINT_REPORT_PATH_REGEX = /\.cleargate[\\/]sprint-runs[\\/]SPRINT-\d{2,}[\\/](REPORT|SPRINT-\d{2,}_REPORT)\.md$/;
11694
11814
  function getItemType2(fm) {
11695
11815
  const typeMap = {
11696
11816
  story_id: "story",
11697
11817
  epic_id: "epic",
11698
11818
  proposal_id: "proposal",
11819
+ sprint_id: "sprint",
11699
11820
  cr_id: "cr",
11700
11821
  bug_id: "bug"
11701
11822
  };
@@ -11704,6 +11825,10 @@ function getItemType2(fm) {
11704
11825
  }
11705
11826
  return null;
11706
11827
  }
11828
+ function getItemTypeWithPathOverride(localPath, fm) {
11829
+ if (SPRINT_REPORT_PATH_REGEX.test(localPath)) return "sprint_report";
11830
+ return getItemType2(fm);
11831
+ }
11707
11832
 
11708
11833
  // src/commands/conflicts.ts
11709
11834
  init_cjs_shims();
@@ -12140,8 +12265,26 @@ var AuthFetcher = class {
12140
12265
  }
12141
12266
  };
12142
12267
 
12268
+ // src/auth/service-token-fetcher.ts
12269
+ init_cjs_shims();
12270
+ var ServiceTokenFetcher = class {
12271
+ constructor(token) {
12272
+ this.token = token;
12273
+ }
12274
+ token;
12275
+ async getAccessToken() {
12276
+ return this.token;
12277
+ }
12278
+ };
12279
+
12143
12280
  // src/commands/mcp-serve.ts
12144
12281
  var DEFAULT_BASE_URL = "https://cleargate-mcp.soula.ge";
12282
+ var ServiceToken401Error = class extends Error {
12283
+ constructor() {
12284
+ super("service-token-401");
12285
+ this.name = "ServiceToken401Error";
12286
+ }
12287
+ };
12145
12288
  async function mcpServeHandler(opts) {
12146
12289
  const fetchFn = opts.fetch ?? globalThis.fetch;
12147
12290
  const stdout = opts.stdout ?? ((s) => process.stdout.write(s));
@@ -12151,32 +12294,44 @@ async function mcpServeHandler(opts) {
12151
12294
  flags: { profile: opts.profile, mcpUrl: opts.mcpUrlFlag }
12152
12295
  });
12153
12296
  const baseUrl = cfg.mcpUrl ?? DEFAULT_BASE_URL;
12154
- const store = await (opts.createStore ?? createTokenStore)({
12155
- ...opts.keychainService !== void 0 ? { keychainService: opts.keychainService } : {},
12156
- ...opts.forceBackend !== void 0 ? { forceBackend: opts.forceBackend } : {}
12157
- });
12158
- const fetcher = new AuthFetcher({
12159
- baseUrl,
12160
- loadRefresh: () => store.load(opts.profile),
12161
- saveRefresh: (t) => store.save(opts.profile, t),
12162
- ...opts.fetch !== void 0 ? { fetch: opts.fetch } : {},
12163
- ...opts.now !== void 0 ? { now: opts.now } : {}
12164
- });
12165
- try {
12166
- await fetcher.getAccessToken();
12167
- } catch (err) {
12168
- if (err instanceof RefreshError) {
12169
- stderr(
12170
- `cleargate mcp serve: refresh failed (${err.status} ${err.code}). Run \`cleargate join <invite-url>\` to re-authenticate.
12297
+ const serviceToken = process.env["CLEARGATE_SERVICE_TOKEN"] ?? "";
12298
+ let fetcher;
12299
+ let isServiceTokenMode;
12300
+ if (serviceToken.length > 0) {
12301
+ isServiceTokenMode = true;
12302
+ fetcher = new ServiceTokenFetcher(serviceToken);
12303
+ stderr("cleargate mcp serve: auth mode = service-token\n");
12304
+ } else {
12305
+ isServiceTokenMode = false;
12306
+ const store = await (opts.createStore ?? createTokenStore)({
12307
+ ...opts.keychainService !== void 0 ? { keychainService: opts.keychainService } : {},
12308
+ ...opts.forceBackend !== void 0 ? { forceBackend: opts.forceBackend } : {}
12309
+ });
12310
+ const authFetcher = new AuthFetcher({
12311
+ baseUrl,
12312
+ loadRefresh: () => store.load(opts.profile),
12313
+ saveRefresh: (t) => store.save(opts.profile, t),
12314
+ ...opts.fetch !== void 0 ? { fetch: opts.fetch } : {},
12315
+ ...opts.now !== void 0 ? { now: opts.now } : {}
12316
+ });
12317
+ fetcher = authFetcher;
12318
+ stderr("cleargate mcp serve: auth mode = keychain-refresh\n");
12319
+ try {
12320
+ await authFetcher.getAccessToken();
12321
+ } catch (err) {
12322
+ if (err instanceof RefreshError) {
12323
+ stderr(
12324
+ `cleargate mcp serve: refresh failed (${err.status} ${err.code}). Run \`cleargate join <invite-url>\` to re-authenticate.
12171
12325
  `
12172
- );
12173
- } else {
12174
- stderr(
12175
- `cleargate mcp serve: ${err instanceof Error ? err.message : String(err)}
12326
+ );
12327
+ } else {
12328
+ stderr(
12329
+ `cleargate mcp serve: ${err instanceof Error ? err.message : String(err)}
12176
12330
  `
12177
- );
12331
+ );
12332
+ }
12333
+ return exit(1);
12178
12334
  }
12179
- return exit(1);
12180
12335
  }
12181
12336
  const inputStream = opts.stdin ?? process.stdin;
12182
12337
  const rl = readline5.createInterface({
@@ -12187,8 +12342,23 @@ async function mcpServeHandler(opts) {
12187
12342
  for await (const line of rl) {
12188
12343
  if (!line.trim()) continue;
12189
12344
  try {
12190
- await proxyOne(line, baseUrl, fetcher, fetchFn, stdout, stderr);
12345
+ await proxyOne(
12346
+ line,
12347
+ baseUrl,
12348
+ fetcher,
12349
+ isServiceTokenMode,
12350
+ fetchFn,
12351
+ stdout,
12352
+ stderr
12353
+ );
12191
12354
  } catch (err) {
12355
+ if (err instanceof ServiceToken401Error) {
12356
+ stderr(
12357
+ `cleargate mcp serve: CLEARGATE_SERVICE_TOKEN rejected by /mcp (401). Issue a new token in the admin console: Tokens \u2192 Issue \u2192 copy snippet.
12358
+ `
12359
+ );
12360
+ return exit(1);
12361
+ }
12192
12362
  const errMsg = err instanceof Error ? err.message : String(err);
12193
12363
  stderr(`cleargate mcp serve: proxy error: ${errMsg}
12194
12364
  `);
@@ -12205,7 +12375,7 @@ async function mcpServeHandler(opts) {
12205
12375
  }
12206
12376
  }
12207
12377
  }
12208
- async function proxyOne(line, baseUrl, fetcher, fetchFn, stdout, stderr) {
12378
+ async function proxyOne(line, baseUrl, fetcher, isServiceTokenMode, fetchFn, stdout, stderr) {
12209
12379
  let parsed;
12210
12380
  try {
12211
12381
  parsed = JSON.parse(line);
@@ -12218,6 +12388,9 @@ async function proxyOne(line, baseUrl, fetcher, fetchFn, stdout, stderr) {
12218
12388
  let access = await fetcher.getAccessToken();
12219
12389
  let res = await postFrame(baseUrl, line, access, fetchFn);
12220
12390
  if (res.status === 401) {
12391
+ if (isServiceTokenMode) {
12392
+ throw new ServiceToken401Error();
12393
+ }
12221
12394
  fetcher.invalidate();
12222
12395
  access = await fetcher.getAccessToken();
12223
12396
  res = await postFrame(baseUrl, line, access, fetchFn);