halfcycle 0.3.35 → 0.3.37

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.
@@ -14548,7 +14548,10 @@ var matcherSchema = external_exports.discriminatedUnion("kind", [
14548
14548
  var HALFCYCLE_DIR_NAME = ".halfcycle";
14549
14549
  var ENGAGEMENTS_DIR_NAME = "engagements";
14550
14550
  var ENGAGEMENT_ENV_FILENAME = "env";
14551
+ var NOT_THIS_ACCOUNT_MARKER_FILENAME = "not-this-account";
14551
14552
  var ACCOUNT_STORE_FILENAME = "account.json";
14553
+ var BUNDLE_PIN_REL = ".halfcycle/bundle.json";
14554
+ var PIN_ENGAGEMENT_ID_FIELD = "engagementId";
14552
14555
  var ENGAGEMENT_ID_PATTERN = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$";
14553
14556
  var ENGAGEMENT_ID_SHAPE = new RegExp(ENGAGEMENT_ID_PATTERN);
14554
14557
  function isEngagementId(value) {
@@ -14746,6 +14749,10 @@ function looksLikeSecret(text) {
14746
14749
  return false;
14747
14750
  }
14748
14751
 
14752
+ // ../core/dist/mcp-registration.js
14753
+ var MCP_REGISTRATION_REL = ".mcp.json";
14754
+ var MCP_SERVER_KEY = "halfcycle";
14755
+
14749
14756
  // dist/control-origin.js
14750
14757
  var DEFAULT_CONTROL_ORIGIN = "https://control.halfcycle.ai";
14751
14758
  function resolveControlOrigin(env) {
@@ -14761,10 +14768,10 @@ var EXCERPT_DEFAULTS = {
14761
14768
  maxExcerptsPerFile: 5,
14762
14769
  maxExcerptsPerChangeSet: 20
14763
14770
  };
14764
- function readConfig() {
14765
- const guardServiceUrl = readGuardOrigin().origin;
14766
- const guardServiceToken = process.env["GUARD_SERVICE_TOKEN"];
14767
- const guardEngagementId = process.env["GUARD_ENGAGEMENT_ID"];
14771
+ function readConfig(env = process.env) {
14772
+ const guardServiceUrl = readGuardOrigin(env).origin;
14773
+ const guardServiceToken = env["GUARD_SERVICE_TOKEN"];
14774
+ const guardEngagementId = env["GUARD_ENGAGEMENT_ID"];
14768
14775
  const missing = [];
14769
14776
  if (!guardServiceToken)
14770
14777
  missing.push("GUARD_SERVICE_TOKEN");
@@ -14786,8 +14793,8 @@ function readNeverConfiguredReason() {
14786
14793
  function isHalfcycleOwnCi() {
14787
14794
  return process.env["HALFCYCLE_INTERNAL_CI"] === "1";
14788
14795
  }
14789
- function readGuardOrigin() {
14790
- return resolveGuardOrigin(process.env);
14796
+ function readGuardOrigin(env = process.env) {
14797
+ return resolveGuardOrigin(env);
14791
14798
  }
14792
14799
  function readActionsOidcEnv() {
14793
14800
  const requestUrl = process.env["ACTIONS_ID_TOKEN_REQUEST_URL"]?.trim();
@@ -14796,11 +14803,19 @@ function readActionsOidcEnv() {
14796
14803
  return void 0;
14797
14804
  return { requestUrl, requestToken };
14798
14805
  }
14799
- function readControlOrigin() {
14800
- return resolveControlOrigin(process.env);
14806
+ function readControlOrigin(env = process.env) {
14807
+ return resolveControlOrigin(env);
14808
+ }
14809
+ function readEnvFilePath(env = process.env) {
14810
+ const raw = env["HALFCYCLE_ENV_FILE"]?.trim();
14811
+ return raw ? raw : void 0;
14812
+ }
14813
+ function readEnvironmentToken() {
14814
+ const raw = process.env["HALFCYCLE_TOKEN"];
14815
+ return raw?.trim() ? raw : void 0;
14801
14816
  }
14802
- function readEnvFilePath() {
14803
- const raw = process.env["HALFCYCLE_ENV_FILE"]?.trim();
14817
+ function readMcpServerUrl() {
14818
+ const raw = process.env["CLAUDE_CODE_MCP_SERVER_URL"];
14804
14819
  return raw ? raw : void 0;
14805
14820
  }
14806
14821
  function readExcerptConfig() {
@@ -15281,6 +15296,25 @@ var ciTokenExchangeResponseSchema = external_exports.object({
15281
15296
  expiresAt: utcIso8601,
15282
15297
  engagementId: external_exports.string()
15283
15298
  }).strict();
15299
+ var NO_CREDENTIAL_PLACEHOLDER = "halfcycle:no-credential";
15300
+ var NOT_THIS_ACCOUNT_PLACEHOLDER = "halfcycle:not-this-account";
15301
+ var UPDATE_CLAUDE_CODE_PLACEHOLDER = "halfcycle:update-claude-code";
15302
+ var CREDENTIAL_PLACEHOLDERS = [
15303
+ NO_CREDENTIAL_PLACEHOLDER,
15304
+ NOT_THIS_ACCOUNT_PLACEHOLDER,
15305
+ UPDATE_CLAUDE_CODE_PLACEHOLDER
15306
+ ];
15307
+ var SWITCH_ACCOUNT_ANSWER = 'answer "s" when asked';
15308
+ var NO_CREDENTIAL_SENTENCE = 'Halfcycle is not connected: this machine has no working Halfcycle credential for this project. Run "npx halfcycle" in the project folder to sign in again.';
15309
+ var NOT_THIS_ACCOUNT_SENTENCE = `Halfcycle is not connected: this project belongs to another Halfcycle account. To switch, run "npx halfcycle" in this project and ${SWITCH_ACCOUNT_ANSWER}.`;
15310
+ var UPDATE_CLAUDE_CODE_SENTENCE = "Halfcycle is not connected: this Claude Code version does not name the server it connects to, so no credential was sent. Update Claude Code, then reopen.";
15311
+ var CANNOT_REACH_SENTENCE = "Halfcycle is not connected: its server could not be reached, so its tools are unavailable in this session. Check your network, then reopen the project.";
15312
+ var SERVICE_PROBLEM_SENTENCE = "Halfcycle is not connected: its service had a problem just now, so nothing in this session is being checked. Reopen the project to try again.";
15313
+ var PLACEHOLDER_SENTENCES = Object.freeze({
15314
+ [NO_CREDENTIAL_PLACEHOLDER]: NO_CREDENTIAL_SENTENCE,
15315
+ [NOT_THIS_ACCOUNT_PLACEHOLDER]: NOT_THIS_ACCOUNT_SENTENCE,
15316
+ [UPDATE_CLAUDE_CODE_PLACEHOLDER]: UPDATE_CLAUDE_CODE_SENTENCE
15317
+ });
15284
15318
 
15285
15319
  // dist/hook-protocol.js
15286
15320
  import { writeSync } from "node:fs";
@@ -15289,7 +15323,7 @@ function emitBlock(reason) {
15289
15323
  process.exit(0);
15290
15324
  }
15291
15325
  function nonBlockingReply(event, text) {
15292
- if (event === "PostToolUse") {
15326
+ if (event === "PostToolUse" || event === "SessionStart") {
15293
15327
  return JSON.stringify({
15294
15328
  systemMessage: text,
15295
15329
  hookSpecificOutput: { hookEventName: event, additionalContext: text }
@@ -15334,11 +15368,15 @@ This is not a service outage: the guard service answered. The suspect is this re
15334
15368
  }
15335
15369
  var UNCONFIGURED_EXIT_TOOL_USE = 2;
15336
15370
  var UNCONFIGURED_EXIT_STOP_FAMILY = 1;
15337
- function emitNeverConfigured(missing, exitCode, reason) {
15338
- writeSync(2, neverConfiguredReport(missing, reason));
15371
+ function emitNeverConfigured(exitCode, reason) {
15372
+ writeSync(2, neverConfiguredReport(reason));
15339
15373
  process.exitCode = exitCode;
15340
15374
  }
15341
- var HOOK_REMEDY = `Fix it by running "npx halfcycle" in this repository: it may open your browser to sign in first, then writes this engagement's credentials to ~/.halfcycle \u2014 outside the repository \u2014 and the hook loads them itself.`;
15375
+ var SIGN_IN_COMMAND = '"npx halfcycle"';
15376
+ var HOOK_REMEDY = `Run ${SIGN_IN_COMMAND} in the project folder to sign in.`;
15377
+ function namesTheSignInCommand(text) {
15378
+ return text.includes(SIGN_IN_COMMAND);
15379
+ }
15342
15380
  var PERMISSIONS_BLOCK = "\n permissions:\n contents: read\n id-token: write\n\n";
15343
15381
  var BOTH_LINES_SENTENCE = "BOTH LINES. Declaring any permission replaces the defaults rather than adding to them, so a block naming only the identity token takes read access away from the checkout step and a private repository stops checking out before this check is reached.";
15344
15382
  var CI_CANNOT_PROVE_PROJECT = "has no way to prove which project it belongs to";
@@ -15346,17 +15384,17 @@ var CI_REMEDY_LEAD = "Add this to the workflow \u2014 at the top of the file, or
15346
15384
  var CI_OLDER_SETUP = "The older setup, GUARD_SERVICE_TOKEN and GUARD_ENGAGEMENT_ID supplied as repository secrets, still works unchanged, and a job that has both needs neither line.";
15347
15385
  var CI_REMEDY = `This job cannot vouch for anything, because it ${CI_CANNOT_PROVE_PROJECT}. ${CI_REMEDY_LEAD} ${CI_OLDER_SETUP}`;
15348
15386
  var CI_NOT_SUPPORTED_MESSAGE = "[Halfcycle CI] This is Halfcycle-internal and is not supported in your CI.\n";
15349
- var NEVER_CONFIGURED_HEADLINE = "GUARD NEVER CONFIGURED";
15387
+ var NEVER_CONFIGURED_HEADLINE = "Halfcycle guidance and support won't be available";
15350
15388
  var NOT_A_SERVICE_OUTAGE = "This is not a service outage.";
15351
- function neverConfiguredMessage(missing, remedy, label = "[Halfcycle]") {
15352
- return `${label} ${NEVER_CONFIGURED_HEADLINE} \u2014 this repository has no guard coverage.
15353
- No evaluation was attempted, because there is nothing to attempt one with: ${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} absent.
15354
- ${NOT_A_SERVICE_OUTAGE} It means the guard has never run here \u2014 not on this change, and not on any change before it.
15389
+ var NO_SIGN_IN_HERE = `${NEVER_CONFIGURED_HEADLINE} in this project on this machine: there is no Halfcycle sign-in for this project here`;
15390
+ function neverConfiguredMessage(remedy, label = "[Halfcycle]") {
15391
+ return `${label} ${NO_SIGN_IN_HERE}. Edits made in this session won't be checked.
15392
+ ${NOT_A_SERVICE_OUTAGE}
15355
15393
  ${remedy}
15356
15394
  `;
15357
15395
  }
15358
15396
  function ciNeverConfiguredMessage() {
15359
- return `[Halfcycle CI] ${NEVER_CONFIGURED_HEADLINE} \u2014 this job ${CI_CANNOT_PROVE_PROJECT}, so it checked nothing. ${CI_REMEDY_LEAD}
15397
+ return `[Halfcycle CI] ${NEVER_CONFIGURED_HEADLINE} in this job: it ${CI_CANNOT_PROVE_PROJECT}, so it checked nothing. ${CI_REMEDY_LEAD}
15360
15398
  ${NOT_A_SERVICE_OUTAGE} Nothing was sent: this job holds no credential and was not permitted to ask GitHub for a signed identity token. It speaks for this job on this change only \u2014 other jobs and earlier changes it did not see.
15361
15399
  ${CI_OLDER_SETUP}
15362
15400
  `;
@@ -15365,10 +15403,10 @@ function notThisAccountReport(label) {
15365
15403
  return `${label} This project belongs to another Halfcycle account. Halfcycle is Solo today \u2014 one person works on a project at a time \u2014 so no credential exists for this machine and none will be issued. Nothing here needs fixing; this copy simply is not checked. Ask the project's owner for view access, or \u2014 if this account is wrong \u2014 sign in as the one that owns the project and run "npx halfcycle" again.
15366
15404
  `;
15367
15405
  }
15368
- function neverConfiguredReport(missing, reason, label = "[Halfcycle]") {
15406
+ function neverConfiguredReport(reason, label = "[Halfcycle]") {
15369
15407
  switch (reason) {
15370
15408
  case "no-credential-yet":
15371
- return neverConfiguredMessage(missing, HOOK_REMEDY, label);
15409
+ return neverConfiguredMessage(HOOK_REMEDY, label);
15372
15410
  case "not-this-account":
15373
15411
  return notThisAccountReport(label);
15374
15412
  }
@@ -15424,6 +15462,21 @@ function credentialRenewalFailedMessage(remedy, label = "[Halfcycle]") {
15424
15462
  ${remedy}
15425
15463
  `;
15426
15464
  }
15465
+ function noCoverageStatement(reason, remedySaid = false) {
15466
+ if (reason === "not-this-account") {
15467
+ return [
15468
+ "[halfcycle] This project belongs to another Halfcycle account, so nothing here is checked.",
15469
+ "[halfcycle] Your work carries on exactly as it would without Halfcycle \u2014 no edit is",
15470
+ "[halfcycle] evaluated, and nothing you do is reported as having passed.",
15471
+ "[halfcycle] Halfcycle is Solo today \u2014 one developer per project \u2014 with Team plans coming.",
15472
+ "[halfcycle] Ask the project's owner for view access.",
15473
+ "[halfcycle] If this account is wrong, sign in as the one that owns the project and run",
15474
+ '[halfcycle] "npx halfcycle" again.'
15475
+ ].join("\n");
15476
+ }
15477
+ const statement = `${NO_SIGN_IN_HERE}, so edits made in this session won't be checked.`;
15478
+ return remedySaid ? statement : `${statement} ${HOOK_REMEDY}`;
15479
+ }
15427
15480
  function notRunReport(notRun, label = "[Halfcycle]") {
15428
15481
  if (!notRun || notRun.length === 0)
15429
15482
  return "";
@@ -17186,7 +17239,7 @@ function cleanRunSummary(verdict, scope) {
17186
17239
  }
17187
17240
 
17188
17241
  // dist/credential-store.js
17189
- import { chmodSync, readFileSync as readFileSync5, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2, realpathSync } from "node:fs";
17242
+ import { chmodSync, existsSync as existsSync2, readFileSync as readFileSync5, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2, realpathSync } from "node:fs";
17190
17243
  import { homedir as homedir2, platform } from "node:os";
17191
17244
  import { dirname as dirname3, join as join5 } from "node:path";
17192
17245
  var REFRESHED_TOKEN_KEYS = ["HALFCYCLE_TOKEN", "GUARD_SERVICE_TOKEN"];
@@ -17194,6 +17247,9 @@ var TOKEN_READ_KEY = "GUARD_SERVICE_TOKEN";
17194
17247
  function composeEngagementEnvPath(engagementId, home) {
17195
17248
  return join5(home ?? homedir2(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId, ENGAGEMENT_ENV_FILENAME);
17196
17249
  }
17250
+ function hasNotThisAccountMarker(engagementId, home) {
17251
+ return existsSync2(join5(home ?? homedir2(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId, NOT_THIS_ACCOUNT_MARKER_FILENAME));
17252
+ }
17197
17253
  function readAccountCredential(controlOrigin, home) {
17198
17254
  let raw;
17199
17255
  try {
@@ -17208,13 +17264,20 @@ function readAccountCredential(controlOrigin, home) {
17208
17264
  return entry.credential;
17209
17265
  }
17210
17266
  function readStoredEngagementToken(envFilePath) {
17267
+ const values = readEngagementStore(envFilePath);
17268
+ return values === void 0 ? void 0 : storedEngagementToken(values);
17269
+ }
17270
+ function readEngagementStore(envFilePath) {
17211
17271
  let raw;
17212
17272
  try {
17213
17273
  raw = readFileSync5(envFilePath, "utf-8");
17214
17274
  } catch {
17215
17275
  return void 0;
17216
17276
  }
17217
- const value = parseEnvText(raw)[TOKEN_READ_KEY];
17277
+ return parseEnvText(raw);
17278
+ }
17279
+ function storedEngagementToken(values) {
17280
+ const value = values[TOKEN_READ_KEY];
17218
17281
  return value && value.trim() !== "" ? value : void 0;
17219
17282
  }
17220
17283
  function writeRefreshedToken(opts) {
@@ -17263,6 +17326,7 @@ function writeRefreshedToken(opts) {
17263
17326
 
17264
17327
  // dist/credential-refresh.js
17265
17328
  var CONTROL_TIMEOUT_MS = 5e3;
17329
+ var CONNECTION_CHECK_DEADLINE_MS = 3e3;
17266
17330
  var UNAUTHORIZED = 401;
17267
17331
  async function probeAccount(controlOrigin, credential, fetchFn = globalThis.fetch) {
17268
17332
  const controller = new AbortController();
@@ -17325,14 +17389,14 @@ var UNREACHABLE_REMEDY = "The Halfcycle service could not be reached to renew th
17325
17389
  var UNREADABLE_REFUSAL_REMEDY = "The Halfcycle service declined to renew this credential and gave no reason this version could read.";
17326
17390
  var UNREADABLE_MINT_REMEDY = 'The Halfcycle service answered with a credential this version could not read. Update the installed Halfcycle runner: run "npx halfcycle" in this repository.';
17327
17391
  async function refreshEngagementCredential(opts) {
17328
- const envFilePath = readEnvFilePath();
17392
+ const envFilePath = readEnvFilePath(opts.source);
17329
17393
  if (!envFilePath)
17330
17394
  return { ok: false, remedy: HOOK_REMEDY };
17331
17395
  const stored = readStoredEngagementToken(envFilePath);
17332
17396
  if (stored !== void 0 && stored !== opts.refusedToken) {
17333
17397
  return { ok: true, token: stored, source: "disk" };
17334
17398
  }
17335
- const controlOrigin = readControlOrigin().origin;
17399
+ const controlOrigin = readControlOrigin(opts.source).origin;
17336
17400
  const accountCredential = readAccountCredential(controlOrigin, opts.home);
17337
17401
  if (!accountCredential)
17338
17402
  return { ok: false, remedy: HOOK_REMEDY };
@@ -17375,7 +17439,7 @@ async function evaluateWithRefresh(opts) {
17375
17439
  }
17376
17440
 
17377
17441
  // dist/seat-configuration.js
17378
- import { existsSync as existsSync2, readFileSync as readFileSync6, realpathSync as realpathSync2 } from "node:fs";
17442
+ import { existsSync as existsSync3, readFileSync as readFileSync6, realpathSync as realpathSync2 } from "node:fs";
17379
17443
  import { homedir as homedir3 } from "node:os";
17380
17444
  import { join as join6, relative as relative3, resolve } from "node:path";
17381
17445
  function gatherSeatConfiguration(repoRoot, read = readSeatConfiguration) {
@@ -17484,7 +17548,7 @@ function readPathStates(projectDir, repoRoot) {
17484
17548
  const status = getWiringStatus(HALFCYCLE_HOOK_WIRING_PATHS, projectDir);
17485
17549
  if (!status.ok)
17486
17550
  return unknown2();
17487
- const mapped = mapWiringStatus(status.value, prefix, HALFCYCLE_HOOK_WIRING_PATHS, (rel) => existsSync2(join6(projectDir, rel)));
17551
+ const mapped = mapWiringStatus(status.value, prefix, HALFCYCLE_HOOK_WIRING_PATHS, (rel) => existsSync3(join6(projectDir, rel)));
17488
17552
  return mapped ?? unknown2();
17489
17553
  }
17490
17554
  function mapWiringStatus(output, prefix, paths, onDisk) {
@@ -17566,7 +17630,7 @@ async function evaluateEdit(reply, readSeat) {
17566
17630
  const configResult = readConfig();
17567
17631
  if (!configResult.config) {
17568
17632
  await emitRunRecord({ runType: "hook", outcome: "unconfigured", changeSet });
17569
- emitNeverConfigured(configResult.missing, UNCONFIGURED_EXIT_TOOL_USE, readNeverConfiguredReason());
17633
+ emitNeverConfigured(UNCONFIGURED_EXIT_TOOL_USE, readNeverConfiguredReason());
17570
17634
  return;
17571
17635
  }
17572
17636
  const { guardServiceUrl, guardServiceToken, guardEngagementId } = configResult.config;
@@ -17656,17 +17720,17 @@ ${guardLines}${notRunBlock}`);
17656
17720
  }
17657
17721
  }
17658
17722
  function readStdin() {
17659
- return new Promise((resolve2, reject) => {
17723
+ return new Promise((resolve4, reject) => {
17660
17724
  const chunks = [];
17661
17725
  process.stdin.on("data", (chunk) => chunks.push(chunk));
17662
- process.stdin.on("end", () => resolve2(Buffer.concat(chunks).toString("utf-8")));
17726
+ process.stdin.on("end", () => resolve4(Buffer.concat(chunks).toString("utf-8")));
17663
17727
  process.stdin.on("error", reject);
17664
17728
  });
17665
17729
  }
17666
17730
 
17667
17731
  // dist/hooks/marker.js
17668
17732
  import { createHash } from "node:crypto";
17669
- import { existsSync as existsSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3, readdirSync as readdirSync3, statSync } from "node:fs";
17733
+ import { existsSync as existsSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3, readdirSync as readdirSync3, statSync } from "node:fs";
17670
17734
  import { tmpdir } from "node:os";
17671
17735
  import { join as join7 } from "node:path";
17672
17736
  var MARKER_TTL_MS = 24 * 60 * 60 * 1e3;
@@ -17695,7 +17759,7 @@ function dedupePath(repoRoot, sessionId) {
17695
17759
  }
17696
17760
  function readMarker(repoRoot, sessionId) {
17697
17761
  const path = markerPath(repoRoot, sessionId);
17698
- if (!existsSync3(path))
17762
+ if (!existsSync4(path))
17699
17763
  return null;
17700
17764
  try {
17701
17765
  return readFileSync7(path, "utf-8").trim() || null;
@@ -17706,7 +17770,7 @@ function readMarker(repoRoot, sessionId) {
17706
17770
  function deleteMarker(repoRoot, sessionId) {
17707
17771
  for (const path of [markerPath(repoRoot, sessionId), dedupePath(repoRoot, sessionId)]) {
17708
17772
  try {
17709
- if (existsSync3(path))
17773
+ if (existsSync4(path))
17710
17774
  unlinkSync3(path);
17711
17775
  } catch {
17712
17776
  }
@@ -17714,7 +17778,7 @@ function deleteMarker(repoRoot, sessionId) {
17714
17778
  }
17715
17779
  function readDedupe(repoRoot, sessionId) {
17716
17780
  const path = dedupePath(repoRoot, sessionId);
17717
- if (!existsSync3(path))
17781
+ if (!existsSync4(path))
17718
17782
  return null;
17719
17783
  try {
17720
17784
  const parsed = JSON.parse(readFileSync7(path, "utf-8"));
@@ -17861,7 +17925,7 @@ async function evaluateSession(input, kind, reply, readSeat) {
17861
17925
  const configResult = readConfig();
17862
17926
  if (!configResult.config) {
17863
17927
  await emitRunRecord({ runType: "hook", outcome: "unconfigured", changeSet, diffBase });
17864
- emitNeverConfigured(configResult.missing, UNCONFIGURED_EXIT_STOP_FAMILY, readNeverConfiguredReason());
17928
+ emitNeverConfigured(UNCONFIGURED_EXIT_STOP_FAMILY, readNeverConfiguredReason());
17865
17929
  return;
17866
17930
  }
17867
17931
  const { guardServiceUrl, guardServiceToken, guardEngagementId } = configResult.config;
@@ -17954,6 +18018,7 @@ ${guardLines}${notRunBlock}`);
17954
18018
  }
17955
18019
 
17956
18020
  // dist/hooks/stdin.js
18021
+ var HOOK_STDIN_GRACE_MS = 2e3;
17957
18022
  async function readHookStdin() {
17958
18023
  try {
17959
18024
  const raw = await readStdin2();
@@ -17968,9 +18033,9 @@ async function readHookStdin() {
17968
18033
  }
17969
18034
  }
17970
18035
  function readStdin2() {
17971
- return new Promise((resolve2) => {
18036
+ return new Promise((resolve4) => {
17972
18037
  if (process.stdin.isTTY) {
17973
- resolve2("");
18038
+ resolve4("");
17974
18039
  return;
17975
18040
  }
17976
18041
  const chunks = [];
@@ -17980,9 +18045,9 @@ function readStdin2() {
17980
18045
  return;
17981
18046
  settled = true;
17982
18047
  clearTimeout(timer);
17983
- resolve2(Buffer.concat(chunks).toString("utf-8"));
18048
+ resolve4(Buffer.concat(chunks).toString("utf-8"));
17984
18049
  };
17985
- const timer = setTimeout(done, 2e3);
18050
+ const timer = setTimeout(done, HOOK_STDIN_GRACE_MS);
17986
18051
  process.stdin.on("data", (chunk) => chunks.push(chunk));
17987
18052
  process.stdin.on("end", done);
17988
18053
  process.stdin.on("error", done);
@@ -18002,14 +18067,14 @@ async function runStop() {
18002
18067
  }
18003
18068
 
18004
18069
  // dist/resolve-engagement.js
18005
- import { existsSync as existsSync4, readFileSync as readFileSync8, realpathSync as realpathSync3 } from "node:fs";
18070
+ import { existsSync as existsSync5, readFileSync as readFileSync8, realpathSync as realpathSync3 } from "node:fs";
18006
18071
  import { join as join8, sep as sep2 } from "node:path";
18007
18072
  import { homedir as homedir4 } from "node:os";
18008
18073
  function defaultRegistryPath() {
18009
18074
  return join8(homedir4(), ".halfcycle", "registry.json");
18010
18075
  }
18011
18076
  function readRegistry(registryPath = defaultRegistryPath()) {
18012
- if (!existsSync4(registryPath))
18077
+ if (!existsSync5(registryPath))
18013
18078
  return {};
18014
18079
  try {
18015
18080
  const parsed = JSON.parse(readFileSync8(registryPath, "utf-8"));
@@ -18059,41 +18124,380 @@ function runResolveEngagement(cwd, registryPath) {
18059
18124
  return 0;
18060
18125
  }
18061
18126
 
18127
+ // dist/refresh-credential.js
18128
+ import { readFileSync as readFileSync10 } from "node:fs";
18129
+ import { resolve as resolve3 } from "node:path";
18130
+
18131
+ // dist/mcp-headers.js
18132
+ import { readFileSync as readFileSync9 } from "node:fs";
18133
+ import { homedir as homedir5 } from "node:os";
18134
+ import { dirname as dirname4, resolve as resolve2 } from "node:path";
18135
+ function defaultWriteErr(text) {
18136
+ process.stderr.write(text);
18137
+ }
18138
+ function headerLine(bearer) {
18139
+ return `${JSON.stringify({ Authorization: `Bearer ${bearer}` })}
18140
+ `;
18141
+ }
18142
+ function projectRootFromBinary(binaryPath) {
18143
+ const depth = VENDORED_BIN_REL.split("/").length - 1;
18144
+ return resolve2(dirname4(binaryPath), ...Array(depth).fill(".."));
18145
+ }
18146
+ function readPinnedId(root) {
18147
+ let raw;
18148
+ try {
18149
+ raw = readFileSync9(resolve2(root, BUNDLE_PIN_REL), "utf-8");
18150
+ } catch {
18151
+ return { problem: "no-pin" };
18152
+ }
18153
+ let pin;
18154
+ try {
18155
+ pin = JSON.parse(raw);
18156
+ } catch {
18157
+ return { problem: "no-id" };
18158
+ }
18159
+ const id = typeof pin === "object" && pin !== null ? pin[PIN_ENGAGEMENT_ID_FIELD] : void 0;
18160
+ return typeof id === "string" && id !== "" ? { id } : { problem: "no-id" };
18161
+ }
18162
+ function resolveStore(root, home) {
18163
+ const pinned = readPinnedId(root);
18164
+ if ("problem" in pinned)
18165
+ return { ok: false, problem: pinned.problem };
18166
+ if (!isEngagementId(pinned.id))
18167
+ return { ok: false, problem: "bad-id" };
18168
+ const notThisAccount = hasNotThisAccountMarker(pinned.id, home);
18169
+ const path = composeEngagementEnvPath(pinned.id, home);
18170
+ const values = readEngagementStore(path);
18171
+ if (values === void 0) {
18172
+ return { ok: false, problem: notThisAccount ? "not-this-account" : "no-credential", engagementId: pinned.id };
18173
+ }
18174
+ return { ok: true, path, values };
18175
+ }
18176
+ function urlOrigin(url2) {
18177
+ if (/[\s\u0000-\u001f\u007f-\u009f]/u.test(url2))
18178
+ return void 0;
18179
+ const match = /^([A-Za-z][A-Za-z0-9+.-]*:\/\/[^/?#\\]*)/.exec(url2);
18180
+ return match ? match[1].toLowerCase() : void 0;
18181
+ }
18182
+ function shownUrl(url2) {
18183
+ return url2.replace(/[^\x21-\x7e]/g, "").slice(0, 200);
18184
+ }
18185
+ function sentences(lines) {
18186
+ return lines.map((line) => `halfcycle: ${line}
18187
+ `).join("");
18188
+ }
18189
+ function badIdSentence(root) {
18190
+ return sentences([
18191
+ `${resolve2(root, BUNDLE_PIN_REL)} names an engagement id that is not a Halfcycle project id,`,
18192
+ 'so no credential was read. Delete that file and run "npx halfcycle" again: this repository',
18193
+ "is then set up as a new Halfcycle project."
18194
+ ]);
18195
+ }
18196
+ function storeProblemSentence(problem, root, home, engagementId) {
18197
+ switch (problem) {
18198
+ case "no-pin":
18199
+ return sentences([
18200
+ `no ${BUNDLE_PIN_REL} in ${root}, so this repository has no Halfcycle engagement.`,
18201
+ 'run "npx halfcycle" here to install one.'
18202
+ ]);
18203
+ case "no-id":
18204
+ return sentences([
18205
+ `${resolve2(root, BUNDLE_PIN_REL)} names no engagement id.`,
18206
+ 'run "npx halfcycle" here to rewrite it.'
18207
+ ]);
18208
+ case "bad-id":
18209
+ return badIdSentence(root);
18210
+ case "not-this-account":
18211
+ return sentences([
18212
+ "this project belongs to another Halfcycle account, so this machine has no",
18213
+ "credential for it and the Halfcycle tools are not available here. Nothing is",
18214
+ "broken \u2014 ask the project's owner for view access, or sign in as the account",
18215
+ 'that owns it and run "npx halfcycle" again.'
18216
+ ]);
18217
+ case "no-credential":
18218
+ return sentences([
18219
+ `this machine holds no credential for engagement ${engagementId ?? ""}.`,
18220
+ `run "npx halfcycle" in this repository \u2014 it will sign you in through your browser if needed, and write the credential to ${resolve2(home, HALFCYCLE_DIR_NAME)}, outside this repository.`
18221
+ ]);
18222
+ }
18223
+ }
18224
+ async function refreshWithinDeadline(source, opts, writeErr) {
18225
+ let timer;
18226
+ const deadline = new Promise((settle) => {
18227
+ timer = setTimeout(() => settle(void 0), opts.deadlineMs ?? CONTROL_TIMEOUT_MS);
18228
+ });
18229
+ const refresh = refreshCredentialToken({
18230
+ fetchFn: opts.fetchFn,
18231
+ home: opts.home,
18232
+ write: writeErr,
18233
+ source
18234
+ }).then((result) => result.token, () => void 0);
18235
+ try {
18236
+ return await Promise.race([refresh, deadline]);
18237
+ } finally {
18238
+ clearTimeout(timer);
18239
+ }
18240
+ }
18241
+ async function chooseBearer(opts) {
18242
+ const writeErr = opts.writeErr ?? defaultWriteErr;
18243
+ const home = opts.home ?? homedir5();
18244
+ const root = projectRootFromBinary(opts.binaryPath);
18245
+ const environmentToken = opts.environmentTokenFirst ? readEnvironmentToken() : void 0;
18246
+ const store = resolveStore(root, home);
18247
+ let storedToken;
18248
+ if (environmentToken === void 0) {
18249
+ if (!store.ok) {
18250
+ writeErr(storeProblemSentence(store.problem, root, home, store.engagementId));
18251
+ return store.problem === "not-this-account" ? NOT_THIS_ACCOUNT_PLACEHOLDER : NO_CREDENTIAL_PLACEHOLDER;
18252
+ }
18253
+ storedToken = storedEngagementToken(store.values);
18254
+ if (storedToken === void 0) {
18255
+ writeErr(sentences([
18256
+ `GUARD_SERVICE_TOKEN is absent or empty in ${store.path}.`,
18257
+ `run "npx halfcycle" in this repository to rewrite this machine's credential.`
18258
+ ]));
18259
+ return NO_CREDENTIAL_PLACEHOLDER;
18260
+ }
18261
+ }
18262
+ const requested = opts.requestedUrl;
18263
+ const recorded = store.ok ? store.values["HALFCYCLE_MCP_URL"] : void 0;
18264
+ if (requested === void 0 && store.ok) {
18265
+ writeErr(sentences([
18266
+ "Claude Code did not say which server it is connecting to, so the Halfcycle credential was not sent.",
18267
+ "update Claude Code, then reconnect."
18268
+ ]));
18269
+ return UPDATE_CLAUDE_CODE_PLACEHOLDER;
18270
+ }
18271
+ if (!recorded) {
18272
+ if (!store.ok && store.problem === "bad-id") {
18273
+ writeErr(badIdSentence(root));
18274
+ } else {
18275
+ writeErr(sentences([
18276
+ "this machine has no Halfcycle server address recorded for this repository, so the credential was not sent.",
18277
+ 'run "npx halfcycle" in this repository on this machine to record it.'
18278
+ ]));
18279
+ }
18280
+ return NO_CREDENTIAL_PLACEHOLDER;
18281
+ }
18282
+ const want = urlOrigin(recorded);
18283
+ const got = requested === void 0 ? void 0 : urlOrigin(requested);
18284
+ if (want === void 0 || got !== want) {
18285
+ writeErr(sentences([
18286
+ `.mcp.json asks for this repository's Halfcycle credential to be sent to ${shownUrl(requested ?? "")},`,
18287
+ `which is not this machine's Halfcycle server (${want ?? "none recorded"}). The credential was NOT sent.`,
18288
+ 'if nobody meant to change .mcp.json, treat that change as suspect; "npx halfcycle" restores the entry.'
18289
+ ]));
18290
+ return NO_CREDENTIAL_PLACEHOLDER;
18291
+ }
18292
+ if (environmentToken !== void 0)
18293
+ return environmentToken;
18294
+ if (!store.ok || storedToken === void 0)
18295
+ return NO_CREDENTIAL_PLACEHOLDER;
18296
+ const source = {
18297
+ HALFCYCLE_ENV_FILE: store.path,
18298
+ GUARD_SERVICE_TOKEN: storedToken,
18299
+ GUARD_ENGAGEMENT_ID: store.values["GUARD_ENGAGEMENT_ID"],
18300
+ HALFCYCLE_SERVICE_URL: store.values["HALFCYCLE_SERVICE_URL"]
18301
+ };
18302
+ const renewed = opts.renewal === "renew-here" ? await refreshWithinDeadline(source, opts, writeErr) : opts.renewal.token;
18303
+ return renewed ?? storedToken;
18304
+ }
18305
+ async function mcpHeaders(opts) {
18306
+ const writeErr = opts.writeErr ?? defaultWriteErr;
18307
+ try {
18308
+ return headerLine(await chooseBearer({
18309
+ ...opts,
18310
+ writeErr,
18311
+ requestedUrl: readMcpServerUrl(),
18312
+ renewal: "renew-here",
18313
+ environmentTokenFirst: true
18314
+ }));
18315
+ } catch (err) {
18316
+ try {
18317
+ writeErr(sentences([
18318
+ `the credential could not be read: ${err instanceof Error ? err.message : String(err)}`,
18319
+ `run "npx halfcycle" in this repository to rewrite this machine's credential.`
18320
+ ]));
18321
+ } catch {
18322
+ return headerLine(NO_CREDENTIAL_PLACEHOLDER);
18323
+ }
18324
+ return headerLine(NO_CREDENTIAL_PLACEHOLDER);
18325
+ }
18326
+ }
18327
+
18062
18328
  // dist/refresh-credential.js
18063
18329
  var UNAUTHORIZED2 = 401;
18064
- async function runRefreshCredential(opts) {
18330
+ var LIVE_PROBE_STATUSES = /* @__PURE__ */ new Set([200, 403]);
18331
+ async function refreshCredentialToken(opts) {
18332
+ const source = opts?.source;
18065
18333
  if (readNeverConfiguredReason() === "not-this-account")
18066
- return 0;
18067
- if (!readEnvFilePath())
18068
- return 0;
18069
- const configResult = readConfig();
18334
+ return { token: void 0 };
18335
+ if (!readEnvFilePath(source))
18336
+ return { token: void 0 };
18337
+ const configResult = readConfig(source);
18070
18338
  if (!configResult.config)
18071
- return 0;
18339
+ return { token: void 0 };
18072
18340
  const { guardServiceToken, guardEngagementId } = configResult.config;
18073
- const controlOrigin = readControlOrigin().origin;
18341
+ const controlOrigin = readControlOrigin(source).origin;
18074
18342
  const fetchFn = opts?.fetchFn ?? globalThis.fetch;
18075
18343
  const accountCredential = readAccountCredential(controlOrigin, opts?.home);
18076
18344
  if (accountCredential)
18077
18345
  await probeAccount(controlOrigin, accountCredential, fetchFn);
18078
18346
  const probe = await probeAccount(controlOrigin, guardServiceToken, fetchFn);
18079
- if (!probe.reached || probe.status !== UNAUTHORIZED2)
18080
- return 0;
18347
+ if (!probe.reached)
18348
+ return { token: void 0 };
18349
+ if (probe.status !== UNAUTHORIZED2) {
18350
+ return { token: LIVE_PROBE_STATUSES.has(probe.status) ? guardServiceToken : void 0 };
18351
+ }
18081
18352
  const refreshed = await refreshEngagementCredential({
18082
18353
  engagementId: guardEngagementId,
18083
18354
  refusedToken: guardServiceToken,
18084
18355
  fetchFn,
18085
- home: opts?.home
18356
+ home: opts?.home,
18357
+ source
18086
18358
  });
18087
18359
  if (!refreshed.ok) {
18088
18360
  const write = opts?.write ?? ((text) => process.stdout.write(text));
18089
18361
  write(credentialRenewalFailedMessage(refreshed.remedy));
18362
+ return { token: void 0 };
18090
18363
  }
18091
- return 0;
18364
+ return { token: refreshed.token };
18092
18365
  }
18366
+ var CHECKED_SOURCES = /* @__PURE__ */ new Set(["startup", "resume"]);
18367
+ var INITIALIZE_BODY = JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: {} });
18368
+ function registeredMcpUrl(projectRoot) {
18369
+ let parsed;
18370
+ try {
18371
+ parsed = JSON.parse(readFileSync10(resolve3(projectRoot, MCP_REGISTRATION_REL), "utf-8"));
18372
+ } catch {
18373
+ return void 0;
18374
+ }
18375
+ if (typeof parsed !== "object" || parsed === null)
18376
+ return void 0;
18377
+ const servers = parsed["mcpServers"];
18378
+ if (typeof servers !== "object" || servers === null || !Object.hasOwn(servers, MCP_SERVER_KEY))
18379
+ return void 0;
18380
+ const entry = servers[MCP_SERVER_KEY];
18381
+ if (typeof entry !== "object" || entry === null)
18382
+ return void 0;
18383
+ const url2 = entry["url"];
18384
+ return typeof url2 === "string" ? url2 : void 0;
18385
+ }
18386
+ function namesItsServer(body) {
18387
+ if (typeof body !== "object" || body === null)
18388
+ return false;
18389
+ const result = body["result"];
18390
+ if (typeof result !== "object" || result === null)
18391
+ return false;
18392
+ const info = result["serverInfo"];
18393
+ if (typeof info !== "object" || info === null)
18394
+ return false;
18395
+ const name = info["name"];
18396
+ return typeof name === "string" && name !== "";
18397
+ }
18398
+ function discardBody(res) {
18399
+ res.body?.cancel().catch(() => void 0);
18400
+ }
18401
+ async function probeDelivery(url2, bearer, fetchFn, deadlineMs) {
18402
+ const controller = new AbortController();
18403
+ const timer = setTimeout(() => controller.abort(), deadlineMs);
18404
+ try {
18405
+ let res;
18406
+ try {
18407
+ res = await fetchFn(url2, {
18408
+ method: "POST",
18409
+ headers: { "content-type": "application/json", authorization: `Bearer ${bearer}` },
18410
+ body: INITIALIZE_BODY,
18411
+ redirect: "manual",
18412
+ signal: controller.signal
18413
+ });
18414
+ } catch {
18415
+ return CANNOT_REACH_SENTENCE;
18416
+ }
18417
+ if (res.status === 401) {
18418
+ discardBody(res);
18419
+ return NO_CREDENTIAL_SENTENCE;
18420
+ }
18421
+ if (res.status < 200 || res.status > 299) {
18422
+ discardBody(res);
18423
+ return SERVICE_PROBLEM_SENTENCE;
18424
+ }
18425
+ let body;
18426
+ try {
18427
+ body = await res.json();
18428
+ } catch {
18429
+ return controller.signal.aborted ? CANNOT_REACH_SENTENCE : SERVICE_PROBLEM_SENTENCE;
18430
+ }
18431
+ return namesItsServer(body) ? void 0 : SERVICE_PROBLEM_SENTENCE;
18432
+ } finally {
18433
+ clearTimeout(timer);
18434
+ }
18435
+ }
18436
+ async function checkConnection(renewedToken, opts) {
18437
+ const url2 = registeredMcpUrl(projectRootFromBinary(opts.binaryPath));
18438
+ if (url2 === void 0)
18439
+ return void 0;
18440
+ const bearer = await chooseBearer({
18441
+ binaryPath: opts.binaryPath,
18442
+ home: opts.home,
18443
+ fetchFn: opts.fetchFn,
18444
+ writeErr: opts.writeErr,
18445
+ requestedUrl: url2,
18446
+ renewal: { token: renewedToken },
18447
+ environmentTokenFirst: false
18448
+ });
18449
+ if (CREDENTIAL_PLACEHOLDERS.includes(bearer)) {
18450
+ return PLACEHOLDER_SENTENCES[bearer];
18451
+ }
18452
+ return probeDelivery(url2, bearer, opts.fetchFn ?? globalThis.fetch, opts.checkDeadlineMs ?? CONNECTION_CHECK_DEADLINE_MS);
18453
+ }
18454
+ function defaultWriteErr2(text) {
18455
+ process.stderr.write(text);
18456
+ }
18457
+ async function runRefreshCredential(opts) {
18458
+ const writeErr = opts.writeErr ?? defaultWriteErr2;
18459
+ try {
18460
+ const renewalSaid = [];
18461
+ let renewalOpen = true;
18462
+ const [input, renewedToken] = await Promise.all([
18463
+ (opts.readInput ?? readHookStdin)(),
18464
+ refreshWithinDeadline(void 0, { fetchFn: opts.fetchFn, home: opts.home, deadlineMs: opts.renewalDeadlineMs }, (text) => {
18465
+ if (renewalOpen)
18466
+ renewalSaid.push(text);
18467
+ })
18468
+ ]);
18469
+ renewalOpen = false;
18470
+ const parts = [];
18471
+ if (CHECKED_SOURCES.has(input["source"])) {
18472
+ const outcome = await checkConnection(renewedToken, { ...opts, writeErr });
18473
+ if (outcome !== void 0)
18474
+ parts.push(outcome);
18475
+ }
18476
+ if (readConfig().config === null) {
18477
+ const remedySaid = parts.some(namesTheSignInCommand);
18478
+ parts.push(noCoverageStatement(readNeverConfiguredReason(), remedySaid));
18479
+ }
18480
+ for (const said of renewalSaid)
18481
+ parts.push(said.trimEnd());
18482
+ return parts.length === 0 ? "" : `${nonBlockingReply("SessionStart", parts.join("\n"))}
18483
+ `;
18484
+ } catch (err) {
18485
+ try {
18486
+ writeErr(`halfcycle: the session-start check could not finish: ${(err instanceof Error ? err.message : String(err)).replace(/\s+/g, " ")}
18487
+ `);
18488
+ } catch {
18489
+ return "";
18490
+ }
18491
+ return "";
18492
+ }
18493
+ }
18494
+
18495
+ // dist/bin.js
18496
+ import { fileURLToPath } from "node:url";
18093
18497
 
18094
18498
  // dist/capture.js
18095
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, existsSync as existsSync5 } from "node:fs";
18096
- import { dirname as dirname4 } from "node:path";
18499
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, existsSync as existsSync6 } from "node:fs";
18500
+ import { dirname as dirname5 } from "node:path";
18097
18501
  function runCapture(provider, endpoint, opts = {}) {
18098
18502
  const repoRoot = opts.repoRoot ?? process.cwd();
18099
18503
  const raw = opts.input ?? readInputFile(opts.fromFile);
@@ -18128,7 +18532,7 @@ function runCapture(provider, endpoint, opts = {}) {
18128
18532
  const fixture = fixturePath(repoRoot, provider, slug);
18129
18533
  const manifestFile = manifestPath(repoRoot);
18130
18534
  try {
18131
- mkdirSync3(dirname4(fixture), { recursive: true });
18535
+ mkdirSync3(dirname5(fixture), { recursive: true });
18132
18536
  writeFileSync4(fixture, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
18133
18537
  } catch (err) {
18134
18538
  const msg = err instanceof Error ? err.message : String(err);
@@ -18160,7 +18564,7 @@ function readInputFile(fromFile) {
18160
18564
  if (!fromFile)
18161
18565
  return void 0;
18162
18566
  try {
18163
- return readFileSync9(fromFile, "utf-8");
18567
+ return readFileSync11(fromFile, "utf-8");
18164
18568
  } catch {
18165
18569
  return void 0;
18166
18570
  }
@@ -18180,8 +18584,13 @@ async function main() {
18180
18584
  return;
18181
18585
  }
18182
18586
  if (cmd === "refresh-credential") {
18183
- const code = await runRefreshCredential();
18184
- process.exit(code);
18587
+ const reply = await runRefreshCredential({ binaryPath: fileURLToPath(import.meta.url) });
18588
+ process.stdout.write(reply, () => process.exit(0));
18589
+ return;
18590
+ }
18591
+ if (cmd === "mcp-headers") {
18592
+ const line = await mcpHeaders({ binaryPath: fileURLToPath(import.meta.url) });
18593
+ process.stdout.write(line, () => process.exit(0));
18185
18594
  return;
18186
18595
  }
18187
18596
  if (cmd === "resolve-engagement") {
@@ -18249,16 +18658,17 @@ Commit the fixture; the next guard evaluation of this endpoint will pass.
18249
18658
  halfcycle-runner hook stop
18250
18659
  halfcycle-runner hook subagent-stop
18251
18660
  halfcycle-runner refresh-credential
18661
+ halfcycle-runner mcp-headers
18252
18662
  halfcycle-runner resolve-engagement <cwd>
18253
18663
  halfcycle-runner capture <provider> <endpoint> [--from <file>]
18254
18664
  `);
18255
18665
  process.exit(1);
18256
18666
  }
18257
18667
  function readStdin3() {
18258
- return new Promise((resolve2, reject) => {
18668
+ return new Promise((resolve4, reject) => {
18259
18669
  const chunks = [];
18260
18670
  process.stdin.on("data", (chunk) => chunks.push(chunk));
18261
- process.stdin.on("end", () => resolve2(Buffer.concat(chunks).toString("utf-8")));
18671
+ process.stdin.on("end", () => resolve4(Buffer.concat(chunks).toString("utf-8")));
18262
18672
  process.stdin.on("error", reject);
18263
18673
  });
18264
18674
  }