cleargate 0.11.4 → 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
@@ -9,8 +9,9 @@ import {
9
9
  AcquireError,
10
10
  acquireAccessToken,
11
11
  getMembershipState,
12
- loadConfig
13
- } from "./chunk-Q3BTSXCK.js";
12
+ loadConfig,
13
+ saveConfig
14
+ } from "./chunk-WFNLCTY5.js";
14
15
  import {
15
16
  createTokenStore
16
17
  } from "./chunk-4V4QABOJ.js";
@@ -21,7 +22,7 @@ import { Command } from "commander";
21
22
  // package.json
22
23
  var package_default = {
23
24
  name: "cleargate",
24
- version: "0.11.4",
25
+ version: "0.12.0",
25
26
  private: false,
26
27
  type: "module",
27
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.",
@@ -633,7 +634,8 @@ async function joinHandler(opts) {
633
634
  if (UUID_V4_RE.test(opts.inviteUrl)) {
634
635
  token = opts.inviteUrl;
635
636
  const cfg = loadConfig({
636
- flags: { profile: opts.profile, mcpUrl: opts.mcpUrlFlag }
637
+ flags: { profile: opts.profile, mcpUrl: opts.mcpUrlFlag },
638
+ ...opts.configPath !== void 0 ? { configPath: opts.configPath } : {}
637
639
  });
638
640
  if (!cfg.mcpUrl) {
639
641
  stderr(
@@ -970,9 +972,15 @@ async function joinHandler(opts) {
970
972
  try {
971
973
  const store = await (opts.createStore ?? createTokenStore)();
972
974
  await store.save(opts.profile, refreshToken);
975
+ saveConfig(
976
+ { mcpUrl: baseUrl },
977
+ opts.configPath !== void 0 ? { configPath: opts.configPath } : {}
978
+ );
973
979
  stdout(`joined project '${projectName}' as '${hostname3()}'
974
980
  `);
975
981
  stdout(`refresh token saved to ${store.backend}.
982
+ `);
983
+ stdout(`mcp_url ${baseUrl} saved to ~/.cleargate/config.json.
976
984
  `);
977
985
  } catch (err) {
978
986
  stderr(
@@ -1477,6 +1485,24 @@ var PREFIX_MAP = [
1477
1485
  { prefix: "BUG-", type: "bug", bucket: "bugs" },
1478
1486
  { prefix: "INITIATIVE-", type: "initiative", bucket: "initiatives" }
1479
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
+ }
1480
1506
  function deriveBucket(filename) {
1481
1507
  const base = filename.includes("/") ? filename.split("/").pop() : filename;
1482
1508
  const stem = base.endsWith(".md") ? base.slice(0, -3) : base;
@@ -1597,6 +1623,12 @@ function serializePage(page, body) {
1597
1623
  if (page.sprint_cleargate_id !== void 0) {
1598
1624
  lines.push(`sprint_cleargate_id: "${page.sprint_cleargate_id}"`);
1599
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
+ }
1600
1632
  lines.push("---");
1601
1633
  const fm = lines.join("\n");
1602
1634
  return `${fm}
@@ -1619,7 +1651,9 @@ function parsePage(raw) {
1619
1651
  const last_contradict_sha = fm["last_contradict_sha"] !== void 0 ? String(fm["last_contradict_sha"]) : void 0;
1620
1652
  const parent_cleargate_id = fm["parent_cleargate_id"] !== void 0 ? String(fm["parent_cleargate_id"]) : void 0;
1621
1653
  const sprint_cleargate_id = fm["sprint_cleargate_id"] !== void 0 ? String(fm["sprint_cleargate_id"]) : void 0;
1622
- 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 };
1623
1657
  }
1624
1658
  function parseFmRaw(raw) {
1625
1659
  const lines = raw.split("\n");
@@ -3138,28 +3172,33 @@ async function wikiIngestHandler(opts) {
3138
3172
  const rawPath = opts.rawPath;
3139
3173
  const absRawPath = path17.isAbsolute(rawPath) ? rawPath : path17.resolve(cwd, rawPath);
3140
3174
  const relRawPath = path17.relative(cwd, absRawPath).replace(/\\/g, "/");
3141
- const deliveryRoot = path17.join(cwd, ".cleargate", "delivery");
3142
- const deliveryRootNorm = deliveryRoot.replace(/\\/g, "/");
3143
- const absDeliveryRoot = deliveryRoot;
3144
- const relToDelivery = path17.relative(absDeliveryRoot, absRawPath);
3145
- if (relToDelivery.startsWith("..") || path17.isAbsolute(relToDelivery)) {
3146
- 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/
3147
3182
  `);
3148
- exit(2);
3149
- return;
3150
- }
3151
- void deliveryRootNorm;
3152
- const isExcluded = EXCLUDED_SUFFIXES2.some((excl) => relRawPath.startsWith(excl));
3153
- if (isExcluded) {
3154
- 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)
3155
3189
  `);
3156
- exit(0);
3157
- return;
3190
+ exit(0);
3191
+ return;
3192
+ }
3158
3193
  }
3159
3194
  const filename = path17.basename(absRawPath);
3160
3195
  let bucketInfo;
3161
3196
  try {
3162
- bucketInfo = deriveBucket(filename);
3197
+ if (isSprintReport) {
3198
+ bucketInfo = deriveBucketFromReportPath(relRawPath);
3199
+ } else {
3200
+ bucketInfo = deriveBucket(filename);
3201
+ }
3163
3202
  } catch (e) {
3164
3203
  stderr(`wiki ingest: cannot determine bucket for ${rawPath}: ${e.message}
3165
3204
  `);
@@ -3202,16 +3241,26 @@ async function wikiIngestHandler(opts) {
3202
3241
  }
3203
3242
  const currentSha = getGitSha(absRawPath, gitRunner) ?? "";
3204
3243
  const pageExists = fs17.existsSync(pagePath);
3205
- if (pageExists && currentSha !== "") {
3206
- let isNoOp = false;
3244
+ let existingPage;
3245
+ if (pageExists) {
3207
3246
  try {
3208
3247
  const existingPageContent = fs17.readFileSync(pagePath, "utf8");
3209
- const existingPage = parsePage(existingPageContent);
3210
- if (existingPage.last_ingest_commit === currentSha) {
3211
- const contentUnchanged = checkContentUnchanged(absRawPath, currentSha, relRawPath, gitRunner);
3212
- 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) {
3213
3257
  isNoOp = true;
3214
3258
  }
3259
+ } else {
3260
+ if (existingPage.last_ingest_commit === currentSha) {
3261
+ const contentUnchanged = checkContentUnchanged(absRawPath, currentSha, relRawPath, gitRunner);
3262
+ if (contentUnchanged) isNoOp = true;
3263
+ }
3215
3264
  }
3216
3265
  } catch {
3217
3266
  }
@@ -3223,36 +3272,53 @@ async function wikiIngestHandler(opts) {
3223
3272
  }
3224
3273
  }
3225
3274
  const action = pageExists ? "update" : "create";
3226
- let existingLastContradictSha;
3227
- if (pageExists) {
3228
- try {
3229
- const existingPageContent = fs17.readFileSync(pagePath, "utf8");
3230
- const existingPage = parsePage(existingPageContent);
3231
- existingLastContradictSha = existingPage.last_contradict_sha;
3232
- } catch {
3233
- }
3234
- }
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;
3235
3279
  const parent = buildParentRef2(fm);
3236
3280
  const children = buildChildrenRefs2(fm);
3237
- const timestamp = now();
3238
3281
  const wikiPage = {
3239
3282
  type,
3240
3283
  id,
3241
- parent,
3242
- children,
3243
- status: String(fm["status"] ?? ""),
3244
- remote_id: String(fm["remote_id"] ?? ""),
3245
- 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,
3246
3290
  last_ingest: timestamp,
3247
- 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,
3248
3293
  repo,
3249
3294
  // Carry forward last_contradict_sha so Phase 4 idempotency survives re-ingest
3250
3295
  ...existingLastContradictSha !== void 0 ? { last_contradict_sha: existingLastContradictSha } : {},
3251
- // Hierarchy keys (§11.7): read from raw fm stamped at raw-side, not wiki-side
3252
- ...typeof fm["parent_cleargate_id"] === "string" ? { parent_cleargate_id: fm["parent_cleargate_id"] } : {},
3253
- ...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
3254
3302
  };
3255
- 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 });
3256
3322
  const pageContent = serializePage(wikiPage, pageBody);
3257
3323
  fs17.mkdirSync(pageDir, { recursive: true });
3258
3324
  fs17.writeFileSync(pagePath, pageContent, "utf8");
@@ -3330,7 +3396,37 @@ function buildChildrenRefs2(fm) {
3330
3396
  return `[[${s}]]`;
3331
3397
  });
3332
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
+ }
3333
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) {
3334
3430
  const title = String(item.fm["title"] ?? item.id);
3335
3431
  const summary = String(
3336
3432
  item.fm["description"] ?? item.body.split("\n")[0] ?? "No summary available."
@@ -3354,6 +3450,16 @@ function buildPageBody2(item) {
3354
3450
  ""
3355
3451
  ].join("\n");
3356
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
+ }
3357
3463
  function appendLogEntry(wikiRoot, entry) {
3358
3464
  const logPath = path17.join(wikiRoot, "log.md");
3359
3465
  const logEntry = [
@@ -5815,7 +5921,7 @@ async function writeCachedGate(absPath, result, opts) {
5815
5921
  throw new Error(`writeCachedGate: failed to parse frontmatter in ${absPath}`);
5816
5922
  }
5817
5923
  const existing = coerceCachedGate(fm["cached_gate_result"]);
5818
- 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)) {
5819
5925
  return;
5820
5926
  }
5821
5927
  const newFm = {};
@@ -7029,6 +7135,10 @@ function refreshScopedGateCaches(sprintId, cwd, execFn) {
7029
7135
  if (!absPath) {
7030
7136
  continue;
7031
7137
  }
7138
+ if (absPath.includes(`${path31.sep}archive${path31.sep}`)) {
7139
+ result.skipped.push(id);
7140
+ continue;
7141
+ }
7032
7142
  let status = "";
7033
7143
  try {
7034
7144
  const raw = fs30.readFileSync(absPath, "utf8");
@@ -10312,6 +10422,19 @@ async function pushHandler(fileOrId, opts = {}) {
10312
10422
  async function handlePush(filePath, ctx) {
10313
10423
  const { projectRoot, identity, sprintRoot, nowFn, resolveMcp, stdout, stderr, exit } = ctx;
10314
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
+ }
10315
10438
  let rawContent;
10316
10439
  try {
10317
10440
  rawContent = await fsPromises11.readFile(resolvedPath, "utf8");
@@ -10341,7 +10464,7 @@ async function handlePush(filePath, ctx) {
10341
10464
  return;
10342
10465
  }
10343
10466
  const itemId = getItemId4(fm);
10344
- const type = getItemType2(fm);
10467
+ const type = getItemTypeWithPathOverride(resolvedPath, fm);
10345
10468
  if (!type) {
10346
10469
  stderr(`Error: cannot determine item type from frontmatter in "${resolvedPath}".
10347
10470
  `);
@@ -10354,6 +10477,9 @@ async function handlePush(filePath, ctx) {
10354
10477
  if (h1) payloadForPush["title"] = h1;
10355
10478
  }
10356
10479
  payloadForPush["body"] = body;
10480
+ if (payloadForPush["origin"] === void 0) {
10481
+ payloadForPush["origin"] = "cleargate-cli";
10482
+ }
10357
10483
  const mcp2 = await resolveMcp();
10358
10484
  let result;
10359
10485
  try {
@@ -10473,17 +10599,20 @@ async function writeAtomic8(filePath, content) {
10473
10599
  await fsPromises11.rename(tmpPath, filePath);
10474
10600
  }
10475
10601
  function getItemId4(fm) {
10476
- 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"]) {
10477
10603
  const val = fm[key];
10478
10604
  if (typeof val === "string" && val) return val;
10479
10605
  }
10480
10606
  return "unknown";
10481
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$/;
10482
10610
  function getItemType2(fm) {
10483
10611
  const typeMap = {
10484
10612
  story_id: "story",
10485
10613
  epic_id: "epic",
10486
10614
  proposal_id: "proposal",
10615
+ sprint_id: "sprint",
10487
10616
  cr_id: "cr",
10488
10617
  bug_id: "bug"
10489
10618
  };
@@ -10492,6 +10621,10 @@ function getItemType2(fm) {
10492
10621
  }
10493
10622
  return null;
10494
10623
  }
10624
+ function getItemTypeWithPathOverride(localPath, fm) {
10625
+ if (SPRINT_REPORT_PATH_REGEX.test(localPath)) return "sprint_report";
10626
+ return getItemType2(fm);
10627
+ }
10495
10628
 
10496
10629
  // src/commands/conflicts.ts
10497
10630
  import * as fsPromises12 from "fs/promises";
@@ -10918,8 +11051,25 @@ var AuthFetcher = class {
10918
11051
  }
10919
11052
  };
10920
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
+
10921
11065
  // src/commands/mcp-serve.ts
10922
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
+ };
10923
11073
  async function mcpServeHandler(opts) {
10924
11074
  const fetchFn = opts.fetch ?? globalThis.fetch;
10925
11075
  const stdout = opts.stdout ?? ((s) => process.stdout.write(s));
@@ -10929,32 +11079,44 @@ async function mcpServeHandler(opts) {
10929
11079
  flags: { profile: opts.profile, mcpUrl: opts.mcpUrlFlag }
10930
11080
  });
10931
11081
  const baseUrl = cfg.mcpUrl ?? DEFAULT_BASE_URL;
10932
- const store = await (opts.createStore ?? createTokenStore)({
10933
- ...opts.keychainService !== void 0 ? { keychainService: opts.keychainService } : {},
10934
- ...opts.forceBackend !== void 0 ? { forceBackend: opts.forceBackend } : {}
10935
- });
10936
- const fetcher = new AuthFetcher({
10937
- baseUrl,
10938
- loadRefresh: () => store.load(opts.profile),
10939
- saveRefresh: (t) => store.save(opts.profile, t),
10940
- ...opts.fetch !== void 0 ? { fetch: opts.fetch } : {},
10941
- ...opts.now !== void 0 ? { now: opts.now } : {}
10942
- });
10943
- try {
10944
- await fetcher.getAccessToken();
10945
- } catch (err) {
10946
- if (err instanceof RefreshError) {
10947
- stderr(
10948
- `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.
10949
11110
  `
10950
- );
10951
- } else {
10952
- stderr(
10953
- `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)}
10954
11115
  `
10955
- );
11116
+ );
11117
+ }
11118
+ return exit(1);
10956
11119
  }
10957
- return exit(1);
10958
11120
  }
10959
11121
  const inputStream = opts.stdin ?? process.stdin;
10960
11122
  const rl = readline5.createInterface({
@@ -10965,8 +11127,23 @@ async function mcpServeHandler(opts) {
10965
11127
  for await (const line of rl) {
10966
11128
  if (!line.trim()) continue;
10967
11129
  try {
10968
- 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
+ );
10969
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
+ }
10970
11147
  const errMsg = err instanceof Error ? err.message : String(err);
10971
11148
  stderr(`cleargate mcp serve: proxy error: ${errMsg}
10972
11149
  `);
@@ -10983,7 +11160,7 @@ async function mcpServeHandler(opts) {
10983
11160
  }
10984
11161
  }
10985
11162
  }
10986
- async function proxyOne(line, baseUrl, fetcher, fetchFn, stdout, stderr) {
11163
+ async function proxyOne(line, baseUrl, fetcher, isServiceTokenMode, fetchFn, stdout, stderr) {
10987
11164
  let parsed;
10988
11165
  try {
10989
11166
  parsed = JSON.parse(line);
@@ -10996,6 +11173,9 @@ async function proxyOne(line, baseUrl, fetcher, fetchFn, stdout, stderr) {
10996
11173
  let access = await fetcher.getAccessToken();
10997
11174
  let res = await postFrame(baseUrl, line, access, fetchFn);
10998
11175
  if (res.status === 401) {
11176
+ if (isServiceTokenMode) {
11177
+ throw new ServiceToken401Error();
11178
+ }
10999
11179
  fetcher.invalidate();
11000
11180
  access = await fetcher.getAccessToken();
11001
11181
  res = await postFrame(baseUrl, line, access, fetchFn);
@@ -11119,7 +11299,7 @@ program.command("init").description("initialise a repo with ClearGate scaffold (
11119
11299
  await initHandler({ force: opts.force ?? false, yes: opts.yes ?? false, pin: opts.pin, fromSource: opts.fromSource });
11120
11300
  });
11121
11301
  program.command("whoami").description("print the currently authenticated agent identity").option("--json", "CR-011: emit membership state as JSON (no network call)").action(async (opts) => {
11122
- const { whoamiHandler } = await import("./whoami-W4U6DPVG.js");
11302
+ const { whoamiHandler } = await import("./whoami-GQTFZHFQ.js");
11123
11303
  const parentOpts = program.opts();
11124
11304
  await whoamiHandler({
11125
11305
  profile: parentOpts.profile,