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.js CHANGED
@@ -22,7 +22,7 @@ import { Command } from "commander";
22
22
  // package.json
23
23
  var package_default = {
24
24
  name: "cleargate",
25
- version: "0.11.5",
25
+ version: "0.12.0",
26
26
  private: false,
27
27
  type: "module",
28
28
  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.",
@@ -1485,6 +1485,24 @@ var PREFIX_MAP = [
1485
1485
  { prefix: "BUG-", type: "bug", bucket: "bugs" },
1486
1486
  { prefix: "INITIATIVE-", type: "initiative", bucket: "initiatives" }
1487
1487
  ];
1488
+ var SPRINT_REPORT_FILENAMES = ["REPORT.md"];
1489
+ var SPRINT_REPORT_CANONICAL_RE = /^SPRINT-\d{2,}_REPORT\.md$/;
1490
+ function isSprintReportPath(relPath) {
1491
+ const normalised = relPath.replace(/\\/g, "/");
1492
+ const match = /\.cleargate\/sprint-runs\/(SPRINT-\d{2,})\/([^/]+)$/.exec(normalised);
1493
+ if (!match) return false;
1494
+ const filename = match[2];
1495
+ return SPRINT_REPORT_FILENAMES.includes(filename) || SPRINT_REPORT_CANONICAL_RE.test(filename);
1496
+ }
1497
+ function deriveBucketFromReportPath(relPath) {
1498
+ const normalised = relPath.replace(/\\/g, "/");
1499
+ const match = /\.cleargate\/sprint-runs\/(SPRINT-\d{2,})\//.exec(normalised);
1500
+ if (!match) {
1501
+ throw new Error(`deriveBucketFromReportPath: cannot extract SPRINT-NN from: ${relPath}`);
1502
+ }
1503
+ const id = match[1];
1504
+ return { type: "sprint", id, bucket: "sprints" };
1505
+ }
1488
1506
  function deriveBucket(filename) {
1489
1507
  const base = filename.includes("/") ? filename.split("/").pop() : filename;
1490
1508
  const stem = base.endsWith(".md") ? base.slice(0, -3) : base;
@@ -1605,6 +1623,12 @@ function serializePage(page, body) {
1605
1623
  if (page.sprint_cleargate_id !== void 0) {
1606
1624
  lines.push(`sprint_cleargate_id: "${page.sprint_cleargate_id}"`);
1607
1625
  }
1626
+ if (page.report_raw_path !== void 0) {
1627
+ lines.push(`report_raw_path: "${page.report_raw_path}"`);
1628
+ }
1629
+ if (page.last_report_ingest_commit !== void 0) {
1630
+ lines.push(`last_report_ingest_commit: "${page.last_report_ingest_commit}"`);
1631
+ }
1608
1632
  lines.push("---");
1609
1633
  const fm = lines.join("\n");
1610
1634
  return `${fm}
@@ -1627,7 +1651,9 @@ function parsePage(raw) {
1627
1651
  const last_contradict_sha = fm["last_contradict_sha"] !== void 0 ? String(fm["last_contradict_sha"]) : void 0;
1628
1652
  const parent_cleargate_id = fm["parent_cleargate_id"] !== void 0 ? String(fm["parent_cleargate_id"]) : void 0;
1629
1653
  const sprint_cleargate_id = fm["sprint_cleargate_id"] !== void 0 ? String(fm["sprint_cleargate_id"]) : void 0;
1630
- 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 };
1654
+ const report_raw_path = fm["report_raw_path"] !== void 0 ? String(fm["report_raw_path"]) : void 0;
1655
+ const last_report_ingest_commit = fm["last_report_ingest_commit"] !== void 0 ? String(fm["last_report_ingest_commit"]) : void 0;
1656
+ 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 };
1631
1657
  }
1632
1658
  function parseFmRaw(raw) {
1633
1659
  const lines = raw.split("\n");
@@ -3146,28 +3172,33 @@ async function wikiIngestHandler(opts) {
3146
3172
  const rawPath = opts.rawPath;
3147
3173
  const absRawPath = path17.isAbsolute(rawPath) ? rawPath : path17.resolve(cwd, rawPath);
3148
3174
  const relRawPath = path17.relative(cwd, absRawPath).replace(/\\/g, "/");
3149
- const deliveryRoot = path17.join(cwd, ".cleargate", "delivery");
3150
- const deliveryRootNorm = deliveryRoot.replace(/\\/g, "/");
3151
- const absDeliveryRoot = deliveryRoot;
3152
- const relToDelivery = path17.relative(absDeliveryRoot, absRawPath);
3153
- if (relToDelivery.startsWith("..") || path17.isAbsolute(relToDelivery)) {
3154
- stderr(`wiki ingest: ${rawPath} not under .cleargate/delivery/
3175
+ const isSprintReport = isSprintReportPath(relRawPath);
3176
+ if (!isSprintReport) {
3177
+ const deliveryRoot = path17.join(cwd, ".cleargate", "delivery");
3178
+ const absDeliveryRoot = deliveryRoot;
3179
+ const relToDelivery = path17.relative(absDeliveryRoot, absRawPath);
3180
+ if (relToDelivery.startsWith("..") || path17.isAbsolute(relToDelivery)) {
3181
+ stderr(`wiki ingest: ${rawPath} not under .cleargate/delivery/
3155
3182
  `);
3156
- exit(2);
3157
- return;
3158
- }
3159
- void deliveryRootNorm;
3160
- const isExcluded = EXCLUDED_SUFFIXES2.some((excl) => relRawPath.startsWith(excl));
3161
- if (isExcluded) {
3162
- stdout(`wiki ingest: ${rawPath} excluded (skip)
3183
+ exit(2);
3184
+ return;
3185
+ }
3186
+ const isExcluded = EXCLUDED_SUFFIXES2.some((excl) => relRawPath.startsWith(excl));
3187
+ if (isExcluded) {
3188
+ stdout(`wiki ingest: ${rawPath} excluded (skip)
3163
3189
  `);
3164
- exit(0);
3165
- return;
3190
+ exit(0);
3191
+ return;
3192
+ }
3166
3193
  }
3167
3194
  const filename = path17.basename(absRawPath);
3168
3195
  let bucketInfo;
3169
3196
  try {
3170
- bucketInfo = deriveBucket(filename);
3197
+ if (isSprintReport) {
3198
+ bucketInfo = deriveBucketFromReportPath(relRawPath);
3199
+ } else {
3200
+ bucketInfo = deriveBucket(filename);
3201
+ }
3171
3202
  } catch (e) {
3172
3203
  stderr(`wiki ingest: cannot determine bucket for ${rawPath}: ${e.message}
3173
3204
  `);
@@ -3210,16 +3241,26 @@ async function wikiIngestHandler(opts) {
3210
3241
  }
3211
3242
  const currentSha = getGitSha(absRawPath, gitRunner) ?? "";
3212
3243
  const pageExists = fs17.existsSync(pagePath);
3213
- if (pageExists && currentSha !== "") {
3214
- let isNoOp = false;
3244
+ let existingPage;
3245
+ if (pageExists) {
3215
3246
  try {
3216
3247
  const existingPageContent = fs17.readFileSync(pagePath, "utf8");
3217
- const existingPage = parsePage(existingPageContent);
3218
- if (existingPage.last_ingest_commit === currentSha) {
3219
- const contentUnchanged = checkContentUnchanged(absRawPath, currentSha, relRawPath, gitRunner);
3220
- if (contentUnchanged) {
3248
+ existingPage = parsePage(existingPageContent);
3249
+ } catch {
3250
+ }
3251
+ }
3252
+ if (existingPage !== void 0 && currentSha !== "") {
3253
+ let isNoOp = false;
3254
+ try {
3255
+ if (isSprintReport) {
3256
+ if (existingPage.last_report_ingest_commit === currentSha) {
3221
3257
  isNoOp = true;
3222
3258
  }
3259
+ } else {
3260
+ if (existingPage.last_ingest_commit === currentSha) {
3261
+ const contentUnchanged = checkContentUnchanged(absRawPath, currentSha, relRawPath, gitRunner);
3262
+ if (contentUnchanged) isNoOp = true;
3263
+ }
3223
3264
  }
3224
3265
  } catch {
3225
3266
  }
@@ -3231,36 +3272,53 @@ async function wikiIngestHandler(opts) {
3231
3272
  }
3232
3273
  }
3233
3274
  const action = pageExists ? "update" : "create";
3234
- let existingLastContradictSha;
3235
- if (pageExists) {
3236
- try {
3237
- const existingPageContent = fs17.readFileSync(pagePath, "utf8");
3238
- const existingPage = parsePage(existingPageContent);
3239
- existingLastContradictSha = existingPage.last_contradict_sha;
3240
- } catch {
3241
- }
3242
- }
3275
+ const timestamp = now();
3276
+ const existingLastContradictSha = existingPage?.last_contradict_sha;
3277
+ const existingReportRawPath = existingPage?.report_raw_path;
3278
+ const existingLastReportIngestCommit = existingPage?.last_report_ingest_commit;
3243
3279
  const parent = buildParentRef2(fm);
3244
3280
  const children = buildChildrenRefs2(fm);
3245
- const timestamp = now();
3246
3281
  const wikiPage = {
3247
3282
  type,
3248
3283
  id,
3249
- parent,
3250
- children,
3251
- status: String(fm["status"] ?? ""),
3252
- remote_id: String(fm["remote_id"] ?? ""),
3253
- raw_path: relRawPath,
3284
+ parent: isSprintReport ? existingPage?.parent ?? "" : parent,
3285
+ children: isSprintReport ? existingPage?.children ?? [] : children,
3286
+ status: isSprintReport ? existingPage?.status ?? String(fm["status"] ?? "") : String(fm["status"] ?? ""),
3287
+ remote_id: isSprintReport ? existingPage?.remote_id ?? String(fm["remote_id"] ?? "") : String(fm["remote_id"] ?? ""),
3288
+ // raw_path tracks the plan file path; for report-only ingest preserve existing or use relRawPath as fallback
3289
+ raw_path: isSprintReport ? existingPage?.raw_path ?? relRawPath : relRawPath,
3254
3290
  last_ingest: timestamp,
3255
- last_ingest_commit: currentSha,
3291
+ // last_ingest_commit tracks the plan source; preserve when re-ingesting report
3292
+ last_ingest_commit: isSprintReport ? existingPage?.last_ingest_commit ?? "" : currentSha,
3256
3293
  repo,
3257
3294
  // Carry forward last_contradict_sha so Phase 4 idempotency survives re-ingest
3258
3295
  ...existingLastContradictSha !== void 0 ? { last_contradict_sha: existingLastContradictSha } : {},
3259
- // Hierarchy keys (§11.7): read from raw fm stamped at raw-side, not wiki-side
3260
- ...typeof fm["parent_cleargate_id"] === "string" ? { parent_cleargate_id: fm["parent_cleargate_id"] } : {},
3261
- ...typeof fm["sprint_cleargate_id"] === "string" ? { sprint_cleargate_id: fm["sprint_cleargate_id"] } : {}
3296
+ // Hierarchy keys (§11.7): read from raw fm for plan ingest; preserve for report ingest
3297
+ ...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"] } : {},
3298
+ ...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"] } : {},
3299
+ // CR-063 sprint-report fields
3300
+ report_raw_path: isSprintReport ? relRawPath : existingReportRawPath ?? void 0,
3301
+ last_report_ingest_commit: isSprintReport ? currentSha : existingLastReportIngestCommit ?? void 0
3262
3302
  };
3263
- const pageBody = buildPageBody2({ id, fm, body });
3303
+ let existingPageBody = "";
3304
+ if (existingPage !== void 0 && pageExists) {
3305
+ try {
3306
+ const existingPageContent = fs17.readFileSync(pagePath, "utf8");
3307
+ const lines = existingPageContent.split("\n");
3308
+ let closingDash = -1;
3309
+ for (let i = 1; i < lines.length; i++) {
3310
+ if (lines[i] === "---") {
3311
+ closingDash = i;
3312
+ break;
3313
+ }
3314
+ }
3315
+ if (closingDash !== -1) {
3316
+ existingPageBody = lines.slice(closingDash + 1).join("\n").replace(/^\n/, "");
3317
+ }
3318
+ } catch {
3319
+ }
3320
+ }
3321
+ const pageBody = buildPageBody2({ id, fm, body, isSprintReport, existingPageBody });
3264
3322
  const pageContent = serializePage(wikiPage, pageBody);
3265
3323
  fs17.mkdirSync(pageDir, { recursive: true });
3266
3324
  fs17.writeFileSync(pagePath, pageContent, "utf8");
@@ -3338,7 +3396,37 @@ function buildChildrenRefs2(fm) {
3338
3396
  return `[[${s}]]`;
3339
3397
  });
3340
3398
  }
3399
+ function extractReportBlock(pageBody) {
3400
+ const beginMarker = "<!-- BEGIN sprint-report -->";
3401
+ const endMarker = "<!-- END sprint-report -->";
3402
+ const beginIdx = pageBody.indexOf(beginMarker);
3403
+ const endIdx = pageBody.indexOf(endMarker);
3404
+ if (beginIdx === -1 || endIdx === -1 || endIdx <= beginIdx) return void 0;
3405
+ return pageBody.slice(beginIdx, endIdx + endMarker.length);
3406
+ }
3407
+ function extractPlanStub(pageBody) {
3408
+ const beginMarker = "<!-- BEGIN sprint-report -->";
3409
+ const beginIdx = pageBody.indexOf(beginMarker);
3410
+ if (beginIdx === -1) return pageBody;
3411
+ return pageBody.slice(0, beginIdx);
3412
+ }
3341
3413
  function buildPageBody2(item) {
3414
+ const { isSprintReport, existingPageBody } = item;
3415
+ if (isSprintReport) {
3416
+ const planStub = existingPageBody ? extractPlanStub(existingPageBody) : buildPlanStub(item);
3417
+ const reportBlock = buildReportBlock(item.body);
3418
+ return planStub + reportBlock;
3419
+ } else {
3420
+ const planStub = buildPlanStub(item);
3421
+ const existingReportBlock = existingPageBody ? extractReportBlock(existingPageBody) : void 0;
3422
+ if (existingReportBlock !== void 0) {
3423
+ const stub = planStub.trimEnd() + "\n\n";
3424
+ return stub + existingReportBlock + "\n";
3425
+ }
3426
+ return planStub;
3427
+ }
3428
+ }
3429
+ function buildPlanStub(item) {
3342
3430
  const title = String(item.fm["title"] ?? item.id);
3343
3431
  const summary = String(
3344
3432
  item.fm["description"] ?? item.body.split("\n")[0] ?? "No summary available."
@@ -3362,6 +3450,16 @@ function buildPageBody2(item) {
3362
3450
  ""
3363
3451
  ].join("\n");
3364
3452
  }
3453
+ function buildReportBlock(reportBody) {
3454
+ return [
3455
+ "<!-- BEGIN sprint-report -->",
3456
+ "## Sprint Report",
3457
+ "",
3458
+ reportBody.trim(),
3459
+ "<!-- END sprint-report -->",
3460
+ ""
3461
+ ].join("\n");
3462
+ }
3365
3463
  function appendLogEntry(wikiRoot, entry) {
3366
3464
  const logPath = path17.join(wikiRoot, "log.md");
3367
3465
  const logEntry = [
@@ -5823,7 +5921,7 @@ async function writeCachedGate(absPath, result, opts) {
5823
5921
  throw new Error(`writeCachedGate: failed to parse frontmatter in ${absPath}`);
5824
5922
  }
5825
5923
  const existing = coerceCachedGate(fm["cached_gate_result"]);
5826
- if (existing && JSON.stringify(existing) === JSON.stringify(newResult)) {
5924
+ if (existing && existing.pass === newResult.pass && JSON.stringify(existing.failing_criteria) === JSON.stringify(newResult.failing_criteria)) {
5827
5925
  return;
5828
5926
  }
5829
5927
  const newFm = {};
@@ -7037,6 +7135,10 @@ function refreshScopedGateCaches(sprintId, cwd, execFn) {
7037
7135
  if (!absPath) {
7038
7136
  continue;
7039
7137
  }
7138
+ if (absPath.includes(`${path31.sep}archive${path31.sep}`)) {
7139
+ result.skipped.push(id);
7140
+ continue;
7141
+ }
7040
7142
  let status = "";
7041
7143
  try {
7042
7144
  const raw = fs30.readFileSync(absPath, "utf8");
@@ -10320,6 +10422,19 @@ async function pushHandler(fileOrId, opts = {}) {
10320
10422
  async function handlePush(filePath, ctx) {
10321
10423
  const { projectRoot, identity, sprintRoot, nowFn, resolveMcp, stdout, stderr, exit } = ctx;
10322
10424
  const resolvedPath = path49.isAbsolute(filePath) ? filePath : path49.resolve(projectRoot, filePath);
10425
+ if (SPRINT_RUNS_PATH_REGEX.test(resolvedPath)) {
10426
+ if (!SPRINT_REPORT_PATH_REGEX.test(resolvedPath)) {
10427
+ stderr(
10428
+ `Error: path not in allowlist. Only sprint report files are accepted from sprint-runs/.
10429
+ Allowed: .cleargate/sprint-runs/SPRINT-NN/REPORT.md
10430
+ .cleargate/sprint-runs/SPRINT-NN/SPRINT-NN_REPORT.md
10431
+ Got: "${resolvedPath}"
10432
+ `
10433
+ );
10434
+ exit(2);
10435
+ return;
10436
+ }
10437
+ }
10323
10438
  let rawContent;
10324
10439
  try {
10325
10440
  rawContent = await fsPromises11.readFile(resolvedPath, "utf8");
@@ -10349,7 +10464,7 @@ async function handlePush(filePath, ctx) {
10349
10464
  return;
10350
10465
  }
10351
10466
  const itemId = getItemId4(fm);
10352
- const type = getItemType2(fm);
10467
+ const type = getItemTypeWithPathOverride(resolvedPath, fm);
10353
10468
  if (!type) {
10354
10469
  stderr(`Error: cannot determine item type from frontmatter in "${resolvedPath}".
10355
10470
  `);
@@ -10362,6 +10477,9 @@ async function handlePush(filePath, ctx) {
10362
10477
  if (h1) payloadForPush["title"] = h1;
10363
10478
  }
10364
10479
  payloadForPush["body"] = body;
10480
+ if (payloadForPush["origin"] === void 0) {
10481
+ payloadForPush["origin"] = "cleargate-cli";
10482
+ }
10365
10483
  const mcp2 = await resolveMcp();
10366
10484
  let result;
10367
10485
  try {
@@ -10481,17 +10599,20 @@ async function writeAtomic8(filePath, content) {
10481
10599
  await fsPromises11.rename(tmpPath, filePath);
10482
10600
  }
10483
10601
  function getItemId4(fm) {
10484
- for (const key of ["story_id", "epic_id", "proposal_id", "cr_id", "bug_id"]) {
10602
+ for (const key of ["story_id", "epic_id", "proposal_id", "sprint_id", "cr_id", "bug_id"]) {
10485
10603
  const val = fm[key];
10486
10604
  if (typeof val === "string" && val) return val;
10487
10605
  }
10488
10606
  return "unknown";
10489
10607
  }
10608
+ var SPRINT_RUNS_PATH_REGEX = /\.cleargate[\\/]sprint-runs[\\/]/;
10609
+ var SPRINT_REPORT_PATH_REGEX = /\.cleargate[\\/]sprint-runs[\\/]SPRINT-\d{2,}[\\/](REPORT|SPRINT-\d{2,}_REPORT)\.md$/;
10490
10610
  function getItemType2(fm) {
10491
10611
  const typeMap = {
10492
10612
  story_id: "story",
10493
10613
  epic_id: "epic",
10494
10614
  proposal_id: "proposal",
10615
+ sprint_id: "sprint",
10495
10616
  cr_id: "cr",
10496
10617
  bug_id: "bug"
10497
10618
  };
@@ -10500,6 +10621,10 @@ function getItemType2(fm) {
10500
10621
  }
10501
10622
  return null;
10502
10623
  }
10624
+ function getItemTypeWithPathOverride(localPath, fm) {
10625
+ if (SPRINT_REPORT_PATH_REGEX.test(localPath)) return "sprint_report";
10626
+ return getItemType2(fm);
10627
+ }
10503
10628
 
10504
10629
  // src/commands/conflicts.ts
10505
10630
  import * as fsPromises12 from "fs/promises";
@@ -10926,8 +11051,25 @@ var AuthFetcher = class {
10926
11051
  }
10927
11052
  };
10928
11053
 
11054
+ // src/auth/service-token-fetcher.ts
11055
+ var ServiceTokenFetcher = class {
11056
+ constructor(token) {
11057
+ this.token = token;
11058
+ }
11059
+ token;
11060
+ async getAccessToken() {
11061
+ return this.token;
11062
+ }
11063
+ };
11064
+
10929
11065
  // src/commands/mcp-serve.ts
10930
11066
  var DEFAULT_BASE_URL = "https://cleargate-mcp.soula.ge";
11067
+ var ServiceToken401Error = class extends Error {
11068
+ constructor() {
11069
+ super("service-token-401");
11070
+ this.name = "ServiceToken401Error";
11071
+ }
11072
+ };
10931
11073
  async function mcpServeHandler(opts) {
10932
11074
  const fetchFn = opts.fetch ?? globalThis.fetch;
10933
11075
  const stdout = opts.stdout ?? ((s) => process.stdout.write(s));
@@ -10937,32 +11079,44 @@ async function mcpServeHandler(opts) {
10937
11079
  flags: { profile: opts.profile, mcpUrl: opts.mcpUrlFlag }
10938
11080
  });
10939
11081
  const baseUrl = cfg.mcpUrl ?? DEFAULT_BASE_URL;
10940
- const store = await (opts.createStore ?? createTokenStore)({
10941
- ...opts.keychainService !== void 0 ? { keychainService: opts.keychainService } : {},
10942
- ...opts.forceBackend !== void 0 ? { forceBackend: opts.forceBackend } : {}
10943
- });
10944
- const fetcher = new AuthFetcher({
10945
- baseUrl,
10946
- loadRefresh: () => store.load(opts.profile),
10947
- saveRefresh: (t) => store.save(opts.profile, t),
10948
- ...opts.fetch !== void 0 ? { fetch: opts.fetch } : {},
10949
- ...opts.now !== void 0 ? { now: opts.now } : {}
10950
- });
10951
- try {
10952
- await fetcher.getAccessToken();
10953
- } catch (err) {
10954
- if (err instanceof RefreshError) {
10955
- stderr(
10956
- `cleargate mcp serve: refresh failed (${err.status} ${err.code}). Run \`cleargate join <invite-url>\` to re-authenticate.
11082
+ const serviceToken = process.env["CLEARGATE_SERVICE_TOKEN"] ?? "";
11083
+ let fetcher;
11084
+ let isServiceTokenMode;
11085
+ if (serviceToken.length > 0) {
11086
+ isServiceTokenMode = true;
11087
+ fetcher = new ServiceTokenFetcher(serviceToken);
11088
+ stderr("cleargate mcp serve: auth mode = service-token\n");
11089
+ } else {
11090
+ isServiceTokenMode = false;
11091
+ const store = await (opts.createStore ?? createTokenStore)({
11092
+ ...opts.keychainService !== void 0 ? { keychainService: opts.keychainService } : {},
11093
+ ...opts.forceBackend !== void 0 ? { forceBackend: opts.forceBackend } : {}
11094
+ });
11095
+ const authFetcher = new AuthFetcher({
11096
+ baseUrl,
11097
+ loadRefresh: () => store.load(opts.profile),
11098
+ saveRefresh: (t) => store.save(opts.profile, t),
11099
+ ...opts.fetch !== void 0 ? { fetch: opts.fetch } : {},
11100
+ ...opts.now !== void 0 ? { now: opts.now } : {}
11101
+ });
11102
+ fetcher = authFetcher;
11103
+ stderr("cleargate mcp serve: auth mode = keychain-refresh\n");
11104
+ try {
11105
+ await authFetcher.getAccessToken();
11106
+ } catch (err) {
11107
+ if (err instanceof RefreshError) {
11108
+ stderr(
11109
+ `cleargate mcp serve: refresh failed (${err.status} ${err.code}). Run \`cleargate join <invite-url>\` to re-authenticate.
10957
11110
  `
10958
- );
10959
- } else {
10960
- stderr(
10961
- `cleargate mcp serve: ${err instanceof Error ? err.message : String(err)}
11111
+ );
11112
+ } else {
11113
+ stderr(
11114
+ `cleargate mcp serve: ${err instanceof Error ? err.message : String(err)}
10962
11115
  `
10963
- );
11116
+ );
11117
+ }
11118
+ return exit(1);
10964
11119
  }
10965
- return exit(1);
10966
11120
  }
10967
11121
  const inputStream = opts.stdin ?? process.stdin;
10968
11122
  const rl = readline5.createInterface({
@@ -10973,8 +11127,23 @@ async function mcpServeHandler(opts) {
10973
11127
  for await (const line of rl) {
10974
11128
  if (!line.trim()) continue;
10975
11129
  try {
10976
- await proxyOne(line, baseUrl, fetcher, fetchFn, stdout, stderr);
11130
+ await proxyOne(
11131
+ line,
11132
+ baseUrl,
11133
+ fetcher,
11134
+ isServiceTokenMode,
11135
+ fetchFn,
11136
+ stdout,
11137
+ stderr
11138
+ );
10977
11139
  } catch (err) {
11140
+ if (err instanceof ServiceToken401Error) {
11141
+ stderr(
11142
+ `cleargate mcp serve: CLEARGATE_SERVICE_TOKEN rejected by /mcp (401). Issue a new token in the admin console: Tokens \u2192 Issue \u2192 copy snippet.
11143
+ `
11144
+ );
11145
+ return exit(1);
11146
+ }
10978
11147
  const errMsg = err instanceof Error ? err.message : String(err);
10979
11148
  stderr(`cleargate mcp serve: proxy error: ${errMsg}
10980
11149
  `);
@@ -10991,7 +11160,7 @@ async function mcpServeHandler(opts) {
10991
11160
  }
10992
11161
  }
10993
11162
  }
10994
- async function proxyOne(line, baseUrl, fetcher, fetchFn, stdout, stderr) {
11163
+ async function proxyOne(line, baseUrl, fetcher, isServiceTokenMode, fetchFn, stdout, stderr) {
10995
11164
  let parsed;
10996
11165
  try {
10997
11166
  parsed = JSON.parse(line);
@@ -11004,6 +11173,9 @@ async function proxyOne(line, baseUrl, fetcher, fetchFn, stdout, stderr) {
11004
11173
  let access = await fetcher.getAccessToken();
11005
11174
  let res = await postFrame(baseUrl, line, access, fetchFn);
11006
11175
  if (res.status === 401) {
11176
+ if (isServiceTokenMode) {
11177
+ throw new ServiceToken401Error();
11178
+ }
11007
11179
  fetcher.invalidate();
11008
11180
  access = await fetcher.getAccessToken();
11009
11181
  res = await postFrame(baseUrl, line, access, fetchFn);