halfcycle 0.3.36 → 0.3.38

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "halfcycle",
3
- "version": "0.3.36",
3
+ "version": "0.3.38",
4
4
  "description": "Halfcycle Method bundle — resolution-stub slash commands, remote method-delivery registration, and governance-hook wiring for a Halfcycle engagement repo. It ships NO worker role files: a project's roles are authored from that project's own recorded decisions at orchestration kickoff (FX-2, W3-F-27).",
5
5
  "commands": [
6
6
  {
package/README.md CHANGED
@@ -146,6 +146,8 @@ This matters enough to be specific about.
146
146
 
147
147
  **Halfcycle never holds or proxies your Claude credential.** Inference runs on your own Claude Code, on your own subscription or seats. We are not a hosted runtime reselling you inference.
148
148
 
149
+ **Your own Halfcycle credential lives outside this repository, in a file named `env` under your home directory — never in anything you'd commit.** Closing a phase rewrites it, to clear which phase was open, and it is also renewed there automatically to keep your connection current. Nothing else changes it.
150
+
149
151
  **What the checks receive is the shape of a change, not its contents.** File paths, structural shapes, environment variable names and how they are used, the tables a write touches, route signatures, outbound host names. There is no file body and no diff in the request.
150
152
 
151
153
  **Source excerpts are off, at both ends, for every engagement today.** The wire format does have a field for them, so the honest version of this is a conditional statement rather than a flat one, and it rests on three things rather than on our good intentions: your runner does not attach an excerpt unless you turn a flag on, and that flag is off by default; anything that does arrive for an engagement that has not opted in is stripped on our side before the checks or any model see it; and there is no way to opt in today, because nothing in the product can set that flag. If that ever changes, our privacy policy changes with it, and it is the policy — not this paragraph — that states the guarantee.
@@ -191,7 +193,9 @@ npx halfcycle
191
193
 
192
194
  Then open the folder in Claude Code and run `/halfcycle-setup`, and answer the questions it asks about your project.
193
195
 
194
- **Accept the workspace trust prompt when Claude Code shows it.** The install writes a credential helper and a set of hooks that are shell commands, so Claude Code asks before it runs them. It is expected, and it appears once.
196
+ **Accept the workspace trust prompt when Claude Code shows it.** The install writes a credential helper and a set of hooks that are shell commands, so Claude Code asks before it runs them. It is expected.
197
+
198
+ **A second prompt follows — it is not the trust prompt again.** After you accept the trust prompt, Claude Code asks whether to use a new MCP server. The cursor starts on *Continue without using this MCP server*; choose to use the server instead, or Halfcycle will not connect.
195
199
 
196
200
  You will get a board link on install. That is where the work becomes visible to anyone who is not you.
197
201
 
@@ -14749,6 +14749,10 @@ function looksLikeSecret(text) {
14749
14749
  return false;
14750
14750
  }
14751
14751
 
14752
+ // ../core/dist/mcp-registration.js
14753
+ var MCP_REGISTRATION_REL = ".mcp.json";
14754
+ var MCP_SERVER_KEY = "halfcycle";
14755
+
14752
14756
  // dist/control-origin.js
14753
14757
  var DEFAULT_CONTROL_ORIGIN = "https://control.halfcycle.ai";
14754
14758
  function resolveControlOrigin(env) {
@@ -15285,7 +15289,9 @@ var engagementCredentialResponseSchema = external_exports.object({
15285
15289
  sessionToken: external_exports.string(),
15286
15290
  mcpUrl: external_exports.string(),
15287
15291
  guardUrl: external_exports.string(),
15288
- controlTelemetryUrl: external_exports.string()
15292
+ controlTelemetryUrl: external_exports.string(),
15293
+ methodVersion: external_exports.string().optional(),
15294
+ consoleUrl: external_exports.string().optional()
15289
15295
  }).strict();
15290
15296
  var ciTokenExchangeResponseSchema = external_exports.object({
15291
15297
  token: external_exports.string(),
@@ -15295,6 +15301,22 @@ var ciTokenExchangeResponseSchema = external_exports.object({
15295
15301
  var NO_CREDENTIAL_PLACEHOLDER = "halfcycle:no-credential";
15296
15302
  var NOT_THIS_ACCOUNT_PLACEHOLDER = "halfcycle:not-this-account";
15297
15303
  var UPDATE_CLAUDE_CODE_PLACEHOLDER = "halfcycle:update-claude-code";
15304
+ var CREDENTIAL_PLACEHOLDERS = [
15305
+ NO_CREDENTIAL_PLACEHOLDER,
15306
+ NOT_THIS_ACCOUNT_PLACEHOLDER,
15307
+ UPDATE_CLAUDE_CODE_PLACEHOLDER
15308
+ ];
15309
+ var SWITCH_ACCOUNT_ANSWER = 'answer "s" when asked';
15310
+ 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.';
15311
+ 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}.`;
15312
+ 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.";
15313
+ 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.";
15314
+ 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.";
15315
+ var PLACEHOLDER_SENTENCES = Object.freeze({
15316
+ [NO_CREDENTIAL_PLACEHOLDER]: NO_CREDENTIAL_SENTENCE,
15317
+ [NOT_THIS_ACCOUNT_PLACEHOLDER]: NOT_THIS_ACCOUNT_SENTENCE,
15318
+ [UPDATE_CLAUDE_CODE_PLACEHOLDER]: UPDATE_CLAUDE_CODE_SENTENCE
15319
+ });
15298
15320
 
15299
15321
  // dist/hook-protocol.js
15300
15322
  import { writeSync } from "node:fs";
@@ -15303,7 +15325,7 @@ function emitBlock(reason) {
15303
15325
  process.exit(0);
15304
15326
  }
15305
15327
  function nonBlockingReply(event, text) {
15306
- if (event === "PostToolUse") {
15328
+ if (event === "PostToolUse" || event === "SessionStart") {
15307
15329
  return JSON.stringify({
15308
15330
  systemMessage: text,
15309
15331
  hookSpecificOutput: { hookEventName: event, additionalContext: text }
@@ -15337,7 +15359,7 @@ function hookReply(event) {
15337
15359
  };
15338
15360
  }
15339
15361
  function infraWarningText(message) {
15340
- return `[Halfcycle] WARNING: Guard service unavailable \u2014 ${message}. Session continues (fail-open).`;
15362
+ return `[Halfcycle] WARNING: Guard service unavailable \u2014 ${message}. The session continues without checks.`;
15341
15363
  }
15342
15364
  function localFaultText(message) {
15343
15365
  return `[Halfcycle] WARNING: Local fault \u2014 ${message}. The guard service was not contacted. Session continues (fail-open).`;
@@ -15348,11 +15370,15 @@ This is not a service outage: the guard service answered. The suspect is this re
15348
15370
  }
15349
15371
  var UNCONFIGURED_EXIT_TOOL_USE = 2;
15350
15372
  var UNCONFIGURED_EXIT_STOP_FAMILY = 1;
15351
- function emitNeverConfigured(missing, exitCode, reason) {
15352
- writeSync(2, neverConfiguredReport(missing, reason));
15373
+ function emitNeverConfigured(exitCode, reason) {
15374
+ writeSync(2, neverConfiguredReport(reason));
15353
15375
  process.exitCode = exitCode;
15354
15376
  }
15355
- 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.`;
15377
+ var SIGN_IN_COMMAND = '"npx halfcycle"';
15378
+ var HOOK_REMEDY = `Run ${SIGN_IN_COMMAND} in the project folder to sign in.`;
15379
+ function namesTheSignInCommand(text) {
15380
+ return text.includes(SIGN_IN_COMMAND);
15381
+ }
15356
15382
  var PERMISSIONS_BLOCK = "\n permissions:\n contents: read\n id-token: write\n\n";
15357
15383
  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.";
15358
15384
  var CI_CANNOT_PROVE_PROJECT = "has no way to prove which project it belongs to";
@@ -15360,17 +15386,17 @@ var CI_REMEDY_LEAD = "Add this to the workflow \u2014 at the top of the file, or
15360
15386
  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.";
15361
15387
  var CI_REMEDY = `This job cannot vouch for anything, because it ${CI_CANNOT_PROVE_PROJECT}. ${CI_REMEDY_LEAD} ${CI_OLDER_SETUP}`;
15362
15388
  var CI_NOT_SUPPORTED_MESSAGE = "[Halfcycle CI] This is Halfcycle-internal and is not supported in your CI.\n";
15363
- var NEVER_CONFIGURED_HEADLINE = "GUARD NEVER CONFIGURED";
15389
+ var NEVER_CONFIGURED_HEADLINE = "Halfcycle guidance and support won't be available";
15364
15390
  var NOT_A_SERVICE_OUTAGE = "This is not a service outage.";
15365
- function neverConfiguredMessage(missing, remedy, label = "[Halfcycle]") {
15366
- return `${label} ${NEVER_CONFIGURED_HEADLINE} \u2014 this repository has no guard coverage.
15367
- No evaluation was attempted, because there is nothing to attempt one with: ${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} absent.
15368
- ${NOT_A_SERVICE_OUTAGE} It means the guard has never run here \u2014 not on this change, and not on any change before it.
15391
+ var NO_SIGN_IN_HERE = `${NEVER_CONFIGURED_HEADLINE} in this project on this machine: there is no Halfcycle sign-in for this project here`;
15392
+ function neverConfiguredMessage(remedy, label = "[Halfcycle]") {
15393
+ return `${label} ${NO_SIGN_IN_HERE}. Edits made in this session won't be checked.
15394
+ ${NOT_A_SERVICE_OUTAGE}
15369
15395
  ${remedy}
15370
15396
  `;
15371
15397
  }
15372
15398
  function ciNeverConfiguredMessage() {
15373
- return `[Halfcycle CI] ${NEVER_CONFIGURED_HEADLINE} \u2014 this job ${CI_CANNOT_PROVE_PROJECT}, so it checked nothing. ${CI_REMEDY_LEAD}
15399
+ return `[Halfcycle CI] ${NEVER_CONFIGURED_HEADLINE} in this job: it ${CI_CANNOT_PROVE_PROJECT}, so it checked nothing. ${CI_REMEDY_LEAD}
15374
15400
  ${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.
15375
15401
  ${CI_OLDER_SETUP}
15376
15402
  `;
@@ -15379,10 +15405,10 @@ function notThisAccountReport(label) {
15379
15405
  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.
15380
15406
  `;
15381
15407
  }
15382
- function neverConfiguredReport(missing, reason, label = "[Halfcycle]") {
15408
+ function neverConfiguredReport(reason, label = "[Halfcycle]") {
15383
15409
  switch (reason) {
15384
15410
  case "no-credential-yet":
15385
- return neverConfiguredMessage(missing, HOOK_REMEDY, label);
15411
+ return neverConfiguredMessage(HOOK_REMEDY, label);
15386
15412
  case "not-this-account":
15387
15413
  return notThisAccountReport(label);
15388
15414
  }
@@ -15410,11 +15436,8 @@ function classifyFailure(failure) {
15410
15436
  return {
15411
15437
  kind: "service",
15412
15438
  message: failure.message,
15413
- // Unchanged wording for this class ON PURPOSE — a genuine outage still says
15414
- // exactly what it said before, and still fails open locally. This task moved
15415
- // ONE condition out of this branch; it did not redefine the branch.
15416
- label: "infrastructure failure",
15417
- reason: `infrastructure failure: ${failure.message}`
15439
+ label: "service unavailable",
15440
+ reason: failure.message
15418
15441
  };
15419
15442
  }
15420
15443
  var CREDENTIAL_REJECTED_EXIT_TOOL_USE = UNCONFIGURED_EXIT_TOOL_USE;
@@ -15438,6 +15461,21 @@ function credentialRenewalFailedMessage(remedy, label = "[Halfcycle]") {
15438
15461
  ${remedy}
15439
15462
  `;
15440
15463
  }
15464
+ function noCoverageStatement(reason, remedySaid = false) {
15465
+ if (reason === "not-this-account") {
15466
+ return [
15467
+ "[halfcycle] This project belongs to another Halfcycle account, so nothing here is checked.",
15468
+ "[halfcycle] Your work carries on exactly as it would without Halfcycle \u2014 no edit is",
15469
+ "[halfcycle] evaluated, and nothing you do is reported as having passed.",
15470
+ "[halfcycle] Halfcycle is Solo today \u2014 one developer per project \u2014 with Team plans coming.",
15471
+ "[halfcycle] Ask the project's owner for view access.",
15472
+ "[halfcycle] If this account is wrong, sign in as the one that owns the project and run",
15473
+ '[halfcycle] "npx halfcycle" again.'
15474
+ ].join("\n");
15475
+ }
15476
+ const statement = `${NO_SIGN_IN_HERE}, so edits made in this session won't be checked.`;
15477
+ return remedySaid ? statement : `${statement} ${HOOK_REMEDY}`;
15478
+ }
15441
15479
  function notRunReport(notRun, label = "[Halfcycle]") {
15442
15480
  if (!notRun || notRun.length === 0)
15443
15481
  return "";
@@ -17287,6 +17325,7 @@ function writeRefreshedToken(opts) {
17287
17325
 
17288
17326
  // dist/credential-refresh.js
17289
17327
  var CONTROL_TIMEOUT_MS = 5e3;
17328
+ var CONNECTION_CHECK_DEADLINE_MS = 3e3;
17290
17329
  var UNAUTHORIZED = 401;
17291
17330
  async function probeAccount(controlOrigin, credential, fetchFn = globalThis.fetch) {
17292
17331
  const controller = new AbortController();
@@ -17590,7 +17629,7 @@ async function evaluateEdit(reply, readSeat) {
17590
17629
  const configResult = readConfig();
17591
17630
  if (!configResult.config) {
17592
17631
  await emitRunRecord({ runType: "hook", outcome: "unconfigured", changeSet });
17593
- emitNeverConfigured(configResult.missing, UNCONFIGURED_EXIT_TOOL_USE, readNeverConfiguredReason());
17632
+ emitNeverConfigured(UNCONFIGURED_EXIT_TOOL_USE, readNeverConfiguredReason());
17594
17633
  return;
17595
17634
  }
17596
17635
  const { guardServiceUrl, guardServiceToken, guardEngagementId } = configResult.config;
@@ -17680,10 +17719,10 @@ ${guardLines}${notRunBlock}`);
17680
17719
  }
17681
17720
  }
17682
17721
  function readStdin() {
17683
- return new Promise((resolve3, reject) => {
17722
+ return new Promise((resolve4, reject) => {
17684
17723
  const chunks = [];
17685
17724
  process.stdin.on("data", (chunk) => chunks.push(chunk));
17686
- process.stdin.on("end", () => resolve3(Buffer.concat(chunks).toString("utf-8")));
17725
+ process.stdin.on("end", () => resolve4(Buffer.concat(chunks).toString("utf-8")));
17687
17726
  process.stdin.on("error", reject);
17688
17727
  });
17689
17728
  }
@@ -17885,7 +17924,7 @@ async function evaluateSession(input, kind, reply, readSeat) {
17885
17924
  const configResult = readConfig();
17886
17925
  if (!configResult.config) {
17887
17926
  await emitRunRecord({ runType: "hook", outcome: "unconfigured", changeSet, diffBase });
17888
- emitNeverConfigured(configResult.missing, UNCONFIGURED_EXIT_STOP_FAMILY, readNeverConfiguredReason());
17927
+ emitNeverConfigured(UNCONFIGURED_EXIT_STOP_FAMILY, readNeverConfiguredReason());
17889
17928
  return;
17890
17929
  }
17891
17930
  const { guardServiceUrl, guardServiceToken, guardEngagementId } = configResult.config;
@@ -17978,6 +18017,7 @@ ${guardLines}${notRunBlock}`);
17978
18017
  }
17979
18018
 
17980
18019
  // dist/hooks/stdin.js
18020
+ var HOOK_STDIN_GRACE_MS = 2e3;
17981
18021
  async function readHookStdin() {
17982
18022
  try {
17983
18023
  const raw = await readStdin2();
@@ -17992,9 +18032,9 @@ async function readHookStdin() {
17992
18032
  }
17993
18033
  }
17994
18034
  function readStdin2() {
17995
- return new Promise((resolve3) => {
18035
+ return new Promise((resolve4) => {
17996
18036
  if (process.stdin.isTTY) {
17997
- resolve3("");
18037
+ resolve4("");
17998
18038
  return;
17999
18039
  }
18000
18040
  const chunks = [];
@@ -18004,9 +18044,9 @@ function readStdin2() {
18004
18044
  return;
18005
18045
  settled = true;
18006
18046
  clearTimeout(timer);
18007
- resolve3(Buffer.concat(chunks).toString("utf-8"));
18047
+ resolve4(Buffer.concat(chunks).toString("utf-8"));
18008
18048
  };
18009
- const timer = setTimeout(done, 2e3);
18049
+ const timer = setTimeout(done, HOOK_STDIN_GRACE_MS);
18010
18050
  process.stdin.on("data", (chunk) => chunks.push(chunk));
18011
18051
  process.stdin.on("end", done);
18012
18052
  process.stdin.on("error", done);
@@ -18084,52 +18124,16 @@ function runResolveEngagement(cwd, registryPath) {
18084
18124
  }
18085
18125
 
18086
18126
  // dist/refresh-credential.js
18087
- var UNAUTHORIZED2 = 401;
18088
- var LIVE_PROBE_STATUSES = /* @__PURE__ */ new Set([200, 403]);
18089
- async function refreshCredentialToken(opts) {
18090
- const source = opts?.source;
18091
- if (readNeverConfiguredReason() === "not-this-account")
18092
- return { token: void 0 };
18093
- if (!readEnvFilePath(source))
18094
- return { token: void 0 };
18095
- const configResult = readConfig(source);
18096
- if (!configResult.config)
18097
- return { token: void 0 };
18098
- const { guardServiceToken, guardEngagementId } = configResult.config;
18099
- const controlOrigin = readControlOrigin(source).origin;
18100
- const fetchFn = opts?.fetchFn ?? globalThis.fetch;
18101
- const accountCredential = readAccountCredential(controlOrigin, opts?.home);
18102
- if (accountCredential)
18103
- await probeAccount(controlOrigin, accountCredential, fetchFn);
18104
- const probe = await probeAccount(controlOrigin, guardServiceToken, fetchFn);
18105
- if (!probe.reached)
18106
- return { token: void 0 };
18107
- if (probe.status !== UNAUTHORIZED2) {
18108
- return { token: LIVE_PROBE_STATUSES.has(probe.status) ? guardServiceToken : void 0 };
18109
- }
18110
- const refreshed = await refreshEngagementCredential({
18111
- engagementId: guardEngagementId,
18112
- refusedToken: guardServiceToken,
18113
- fetchFn,
18114
- home: opts?.home,
18115
- source
18116
- });
18117
- if (!refreshed.ok) {
18118
- const write = opts?.write ?? ((text) => process.stdout.write(text));
18119
- write(credentialRenewalFailedMessage(refreshed.remedy));
18120
- return { token: void 0 };
18121
- }
18122
- return { token: refreshed.token };
18123
- }
18124
- async function runRefreshCredential(opts) {
18125
- await refreshCredentialToken(opts);
18126
- return 0;
18127
- }
18127
+ import { readFileSync as readFileSync10 } from "node:fs";
18128
+ import { resolve as resolve3 } from "node:path";
18128
18129
 
18129
18130
  // dist/mcp-headers.js
18130
18131
  import { readFileSync as readFileSync9 } from "node:fs";
18131
18132
  import { homedir as homedir5 } from "node:os";
18132
18133
  import { dirname as dirname4, resolve as resolve2 } from "node:path";
18134
+ function defaultWriteErr(text) {
18135
+ process.stderr.write(text);
18136
+ }
18133
18137
  function headerLine(bearer) {
18134
18138
  return `${JSON.stringify({ Authorization: `Bearer ${bearer}` })}
18135
18139
  `;
@@ -18233,16 +18237,17 @@ async function refreshWithinDeadline(source, opts, writeErr) {
18233
18237
  clearTimeout(timer);
18234
18238
  }
18235
18239
  }
18236
- async function decide(opts, writeErr) {
18240
+ async function chooseBearer(opts) {
18241
+ const writeErr = opts.writeErr ?? defaultWriteErr;
18237
18242
  const home = opts.home ?? homedir5();
18238
18243
  const root = projectRootFromBinary(opts.binaryPath);
18239
- const environmentToken = readEnvironmentToken();
18244
+ const environmentToken = opts.environmentTokenFirst ? readEnvironmentToken() : void 0;
18240
18245
  const store = resolveStore(root, home);
18241
18246
  let storedToken;
18242
18247
  if (environmentToken === void 0) {
18243
18248
  if (!store.ok) {
18244
18249
  writeErr(storeProblemSentence(store.problem, root, home, store.engagementId));
18245
- return headerLine(store.problem === "not-this-account" ? NOT_THIS_ACCOUNT_PLACEHOLDER : NO_CREDENTIAL_PLACEHOLDER);
18250
+ return store.problem === "not-this-account" ? NOT_THIS_ACCOUNT_PLACEHOLDER : NO_CREDENTIAL_PLACEHOLDER;
18246
18251
  }
18247
18252
  storedToken = storedEngagementToken(store.values);
18248
18253
  if (storedToken === void 0) {
@@ -18250,17 +18255,17 @@ async function decide(opts, writeErr) {
18250
18255
  `GUARD_SERVICE_TOKEN is absent or empty in ${store.path}.`,
18251
18256
  `run "npx halfcycle" in this repository to rewrite this machine's credential.`
18252
18257
  ]));
18253
- return headerLine(NO_CREDENTIAL_PLACEHOLDER);
18258
+ return NO_CREDENTIAL_PLACEHOLDER;
18254
18259
  }
18255
18260
  }
18256
- const requested = readMcpServerUrl();
18261
+ const requested = opts.requestedUrl;
18257
18262
  const recorded = store.ok ? store.values["HALFCYCLE_MCP_URL"] : void 0;
18258
18263
  if (requested === void 0 && store.ok) {
18259
18264
  writeErr(sentences([
18260
18265
  "Claude Code did not say which server it is connecting to, so the Halfcycle credential was not sent.",
18261
18266
  "update Claude Code, then reconnect."
18262
18267
  ]));
18263
- return headerLine(UPDATE_CLAUDE_CODE_PLACEHOLDER);
18268
+ return UPDATE_CLAUDE_CODE_PLACEHOLDER;
18264
18269
  }
18265
18270
  if (!recorded) {
18266
18271
  if (!store.ok && store.problem === "bad-id") {
@@ -18271,7 +18276,7 @@ async function decide(opts, writeErr) {
18271
18276
  'run "npx halfcycle" in this repository on this machine to record it.'
18272
18277
  ]));
18273
18278
  }
18274
- return headerLine(NO_CREDENTIAL_PLACEHOLDER);
18279
+ return NO_CREDENTIAL_PLACEHOLDER;
18275
18280
  }
18276
18281
  const want = urlOrigin(recorded);
18277
18282
  const got = requested === void 0 ? void 0 : urlOrigin(requested);
@@ -18281,25 +18286,31 @@ async function decide(opts, writeErr) {
18281
18286
  `which is not this machine's Halfcycle server (${want ?? "none recorded"}). The credential was NOT sent.`,
18282
18287
  'if nobody meant to change .mcp.json, treat that change as suspect; "npx halfcycle" restores the entry.'
18283
18288
  ]));
18284
- return headerLine(NO_CREDENTIAL_PLACEHOLDER);
18289
+ return NO_CREDENTIAL_PLACEHOLDER;
18285
18290
  }
18286
18291
  if (environmentToken !== void 0)
18287
- return headerLine(environmentToken);
18292
+ return environmentToken;
18288
18293
  if (!store.ok || storedToken === void 0)
18289
- return headerLine(NO_CREDENTIAL_PLACEHOLDER);
18294
+ return NO_CREDENTIAL_PLACEHOLDER;
18290
18295
  const source = {
18291
18296
  HALFCYCLE_ENV_FILE: store.path,
18292
18297
  GUARD_SERVICE_TOKEN: storedToken,
18293
18298
  GUARD_ENGAGEMENT_ID: store.values["GUARD_ENGAGEMENT_ID"],
18294
18299
  HALFCYCLE_SERVICE_URL: store.values["HALFCYCLE_SERVICE_URL"]
18295
18300
  };
18296
- const refreshed = await refreshWithinDeadline(source, opts, writeErr);
18297
- return headerLine(refreshed ?? storedToken);
18301
+ const renewed = opts.renewal === "renew-here" ? await refreshWithinDeadline(source, opts, writeErr) : opts.renewal.token;
18302
+ return renewed ?? storedToken;
18298
18303
  }
18299
18304
  async function mcpHeaders(opts) {
18300
- const writeErr = opts.writeErr ?? ((text) => void process.stderr.write(text));
18305
+ const writeErr = opts.writeErr ?? defaultWriteErr;
18301
18306
  try {
18302
- return await decide(opts, writeErr);
18307
+ return headerLine(await chooseBearer({
18308
+ ...opts,
18309
+ writeErr,
18310
+ requestedUrl: readMcpServerUrl(),
18311
+ renewal: "renew-here",
18312
+ environmentTokenFirst: true
18313
+ }));
18303
18314
  } catch (err) {
18304
18315
  try {
18305
18316
  writeErr(sentences([
@@ -18313,11 +18324,178 @@ async function mcpHeaders(opts) {
18313
18324
  }
18314
18325
  }
18315
18326
 
18327
+ // dist/refresh-credential.js
18328
+ var UNAUTHORIZED2 = 401;
18329
+ var LIVE_PROBE_STATUSES = /* @__PURE__ */ new Set([200, 403]);
18330
+ async function refreshCredentialToken(opts) {
18331
+ const source = opts?.source;
18332
+ if (readNeverConfiguredReason() === "not-this-account")
18333
+ return { token: void 0 };
18334
+ if (!readEnvFilePath(source))
18335
+ return { token: void 0 };
18336
+ const configResult = readConfig(source);
18337
+ if (!configResult.config)
18338
+ return { token: void 0 };
18339
+ const { guardServiceToken, guardEngagementId } = configResult.config;
18340
+ const controlOrigin = readControlOrigin(source).origin;
18341
+ const fetchFn = opts?.fetchFn ?? globalThis.fetch;
18342
+ const accountCredential = readAccountCredential(controlOrigin, opts?.home);
18343
+ if (accountCredential)
18344
+ await probeAccount(controlOrigin, accountCredential, fetchFn);
18345
+ const probe = await probeAccount(controlOrigin, guardServiceToken, fetchFn);
18346
+ if (!probe.reached)
18347
+ return { token: void 0 };
18348
+ if (probe.status !== UNAUTHORIZED2) {
18349
+ return { token: LIVE_PROBE_STATUSES.has(probe.status) ? guardServiceToken : void 0 };
18350
+ }
18351
+ const refreshed = await refreshEngagementCredential({
18352
+ engagementId: guardEngagementId,
18353
+ refusedToken: guardServiceToken,
18354
+ fetchFn,
18355
+ home: opts?.home,
18356
+ source
18357
+ });
18358
+ if (!refreshed.ok) {
18359
+ const write = opts?.write ?? ((text) => process.stdout.write(text));
18360
+ write(credentialRenewalFailedMessage(refreshed.remedy));
18361
+ return { token: void 0 };
18362
+ }
18363
+ return { token: refreshed.token };
18364
+ }
18365
+ var CHECKED_SOURCES = /* @__PURE__ */ new Set(["startup", "resume"]);
18366
+ var INITIALIZE_BODY = JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: {} });
18367
+ function registeredMcpUrl(projectRoot) {
18368
+ let parsed;
18369
+ try {
18370
+ parsed = JSON.parse(readFileSync10(resolve3(projectRoot, MCP_REGISTRATION_REL), "utf-8"));
18371
+ } catch {
18372
+ return void 0;
18373
+ }
18374
+ if (typeof parsed !== "object" || parsed === null)
18375
+ return void 0;
18376
+ const servers = parsed["mcpServers"];
18377
+ if (typeof servers !== "object" || servers === null || !Object.hasOwn(servers, MCP_SERVER_KEY))
18378
+ return void 0;
18379
+ const entry = servers[MCP_SERVER_KEY];
18380
+ if (typeof entry !== "object" || entry === null)
18381
+ return void 0;
18382
+ const url2 = entry["url"];
18383
+ return typeof url2 === "string" ? url2 : void 0;
18384
+ }
18385
+ function namesItsServer(body) {
18386
+ if (typeof body !== "object" || body === null)
18387
+ return false;
18388
+ const result = body["result"];
18389
+ if (typeof result !== "object" || result === null)
18390
+ return false;
18391
+ const info = result["serverInfo"];
18392
+ if (typeof info !== "object" || info === null)
18393
+ return false;
18394
+ const name = info["name"];
18395
+ return typeof name === "string" && name !== "";
18396
+ }
18397
+ function discardBody(res) {
18398
+ res.body?.cancel().catch(() => void 0);
18399
+ }
18400
+ async function probeDelivery(url2, bearer, fetchFn, deadlineMs) {
18401
+ const controller = new AbortController();
18402
+ const timer = setTimeout(() => controller.abort(), deadlineMs);
18403
+ try {
18404
+ let res;
18405
+ try {
18406
+ res = await fetchFn(url2, {
18407
+ method: "POST",
18408
+ headers: { "content-type": "application/json", authorization: `Bearer ${bearer}` },
18409
+ body: INITIALIZE_BODY,
18410
+ redirect: "manual",
18411
+ signal: controller.signal
18412
+ });
18413
+ } catch {
18414
+ return CANNOT_REACH_SENTENCE;
18415
+ }
18416
+ if (res.status === 401) {
18417
+ discardBody(res);
18418
+ return NO_CREDENTIAL_SENTENCE;
18419
+ }
18420
+ if (res.status < 200 || res.status > 299) {
18421
+ discardBody(res);
18422
+ return SERVICE_PROBLEM_SENTENCE;
18423
+ }
18424
+ let body;
18425
+ try {
18426
+ body = await res.json();
18427
+ } catch {
18428
+ return controller.signal.aborted ? CANNOT_REACH_SENTENCE : SERVICE_PROBLEM_SENTENCE;
18429
+ }
18430
+ return namesItsServer(body) ? void 0 : SERVICE_PROBLEM_SENTENCE;
18431
+ } finally {
18432
+ clearTimeout(timer);
18433
+ }
18434
+ }
18435
+ async function checkConnection(renewedToken, opts) {
18436
+ const url2 = registeredMcpUrl(projectRootFromBinary(opts.binaryPath));
18437
+ if (url2 === void 0)
18438
+ return void 0;
18439
+ const bearer = await chooseBearer({
18440
+ binaryPath: opts.binaryPath,
18441
+ home: opts.home,
18442
+ fetchFn: opts.fetchFn,
18443
+ writeErr: opts.writeErr,
18444
+ requestedUrl: url2,
18445
+ renewal: { token: renewedToken },
18446
+ environmentTokenFirst: false
18447
+ });
18448
+ if (CREDENTIAL_PLACEHOLDERS.includes(bearer)) {
18449
+ return PLACEHOLDER_SENTENCES[bearer];
18450
+ }
18451
+ return probeDelivery(url2, bearer, opts.fetchFn ?? globalThis.fetch, opts.checkDeadlineMs ?? CONNECTION_CHECK_DEADLINE_MS);
18452
+ }
18453
+ function defaultWriteErr2(text) {
18454
+ process.stderr.write(text);
18455
+ }
18456
+ async function runRefreshCredential(opts) {
18457
+ const writeErr = opts.writeErr ?? defaultWriteErr2;
18458
+ try {
18459
+ const renewalSaid = [];
18460
+ let renewalOpen = true;
18461
+ const [input, renewedToken] = await Promise.all([
18462
+ (opts.readInput ?? readHookStdin)(),
18463
+ refreshWithinDeadline(void 0, { fetchFn: opts.fetchFn, home: opts.home, deadlineMs: opts.renewalDeadlineMs }, (text) => {
18464
+ if (renewalOpen)
18465
+ renewalSaid.push(text);
18466
+ })
18467
+ ]);
18468
+ renewalOpen = false;
18469
+ const parts = [];
18470
+ if (CHECKED_SOURCES.has(input["source"])) {
18471
+ const outcome = await checkConnection(renewedToken, { ...opts, writeErr });
18472
+ if (outcome !== void 0)
18473
+ parts.push(outcome);
18474
+ }
18475
+ if (readConfig().config === null) {
18476
+ const remedySaid = parts.some(namesTheSignInCommand);
18477
+ parts.push(noCoverageStatement(readNeverConfiguredReason(), remedySaid));
18478
+ }
18479
+ for (const said of renewalSaid)
18480
+ parts.push(said.trimEnd());
18481
+ return parts.length === 0 ? "" : `${nonBlockingReply("SessionStart", parts.join("\n"))}
18482
+ `;
18483
+ } catch (err) {
18484
+ try {
18485
+ writeErr(`halfcycle: the session-start check could not finish: ${(err instanceof Error ? err.message : String(err)).replace(/\s+/g, " ")}
18486
+ `);
18487
+ } catch {
18488
+ return "";
18489
+ }
18490
+ return "";
18491
+ }
18492
+ }
18493
+
18316
18494
  // dist/bin.js
18317
18495
  import { fileURLToPath } from "node:url";
18318
18496
 
18319
18497
  // dist/capture.js
18320
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, existsSync as existsSync6 } from "node:fs";
18498
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, existsSync as existsSync6 } from "node:fs";
18321
18499
  import { dirname as dirname5 } from "node:path";
18322
18500
  function runCapture(provider, endpoint, opts = {}) {
18323
18501
  const repoRoot = opts.repoRoot ?? process.cwd();
@@ -18385,7 +18563,7 @@ function readInputFile(fromFile) {
18385
18563
  if (!fromFile)
18386
18564
  return void 0;
18387
18565
  try {
18388
- return readFileSync10(fromFile, "utf-8");
18566
+ return readFileSync11(fromFile, "utf-8");
18389
18567
  } catch {
18390
18568
  return void 0;
18391
18569
  }
@@ -18405,8 +18583,8 @@ async function main() {
18405
18583
  return;
18406
18584
  }
18407
18585
  if (cmd === "refresh-credential") {
18408
- const code = await runRefreshCredential();
18409
- process.exit(code);
18586
+ const reply = await runRefreshCredential({ binaryPath: fileURLToPath(import.meta.url) });
18587
+ process.stdout.write(reply, () => process.exit(0));
18410
18588
  return;
18411
18589
  }
18412
18590
  if (cmd === "mcp-headers") {
@@ -18486,10 +18664,10 @@ Commit the fixture; the next guard evaluation of this endpoint will pass.
18486
18664
  process.exit(1);
18487
18665
  }
18488
18666
  function readStdin3() {
18489
- return new Promise((resolve3, reject) => {
18667
+ return new Promise((resolve4, reject) => {
18490
18668
  const chunks = [];
18491
18669
  process.stdin.on("data", (chunk) => chunks.push(chunk));
18492
- process.stdin.on("end", () => resolve3(Buffer.concat(chunks).toString("utf-8")));
18670
+ process.stdin.on("end", () => resolve4(Buffer.concat(chunks).toString("utf-8")));
18493
18671
  process.stdin.on("error", reject);
18494
18672
  });
18495
18673
  }