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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +5 -1
- package/bin/bin.bundle.mjs +266 -88
- package/dist/bin.js +382 -221
- package/dist/bin.js.map +3 -3
- package/dist/build-record/obtain.d.ts +7 -3
- package/dist/build-record/obtain.d.ts.map +1 -1
- package/dist/build-record/render-v2.d.ts +4 -3
- package/dist/build-record/render-v2.d.ts.map +1 -1
- package/dist/cli-contract.d.ts +7 -0
- package/dist/cli-contract.d.ts.map +1 -1
- package/dist/control-origin.d.ts +2 -2
- package/dist/create-engagement.d.ts +27 -1
- package/dist/create-engagement.d.ts.map +1 -1
- package/dist/device-signin.d.ts +93 -17
- package/dist/device-signin.d.ts.map +1 -1
- package/dist/engagement-credential.d.ts +8 -4
- package/dist/engagement-credential.d.ts.map +1 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +211 -122
- package/dist/index.js.map +3 -3
- package/dist/install.d.ts +35 -9
- package/dist/install.d.ts.map +1 -1
- package/dist/loopback-signin.d.ts +8 -2
- package/dist/loopback-signin.d.ts.map +1 -1
- package/dist/open-phase.d.ts +12 -3
- package/dist/open-phase.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/mint-board-code.d.ts +0 -41
- package/dist/mint-board-code.d.ts.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "halfcycle",
|
|
3
|
-
"version": "0.3.
|
|
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
|
|
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
|
|
package/bin/bin.bundle.mjs
CHANGED
|
@@ -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}.
|
|
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(
|
|
15352
|
-
writeSync(2, neverConfiguredReport(
|
|
15373
|
+
function emitNeverConfigured(exitCode, reason) {
|
|
15374
|
+
writeSync(2, neverConfiguredReport(reason));
|
|
15353
15375
|
process.exitCode = exitCode;
|
|
15354
15376
|
}
|
|
15355
|
-
var
|
|
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 = "
|
|
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
|
-
|
|
15366
|
-
|
|
15367
|
-
|
|
15368
|
-
${NOT_A_SERVICE_OUTAGE}
|
|
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}
|
|
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(
|
|
15408
|
+
function neverConfiguredReport(reason, label = "[Halfcycle]") {
|
|
15383
15409
|
switch (reason) {
|
|
15384
15410
|
case "no-credential-yet":
|
|
15385
|
-
return neverConfiguredMessage(
|
|
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
|
-
|
|
15414
|
-
|
|
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(
|
|
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((
|
|
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", () =>
|
|
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(
|
|
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((
|
|
18035
|
+
return new Promise((resolve4) => {
|
|
17996
18036
|
if (process.stdin.isTTY) {
|
|
17997
|
-
|
|
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
|
-
|
|
18047
|
+
resolve4(Buffer.concat(chunks).toString("utf-8"));
|
|
18008
18048
|
};
|
|
18009
|
-
const timer = setTimeout(done,
|
|
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
|
-
|
|
18088
|
-
|
|
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
|
|
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
|
|
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
|
|
18258
|
+
return NO_CREDENTIAL_PLACEHOLDER;
|
|
18254
18259
|
}
|
|
18255
18260
|
}
|
|
18256
|
-
const requested =
|
|
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
|
|
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
|
|
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
|
|
18289
|
+
return NO_CREDENTIAL_PLACEHOLDER;
|
|
18285
18290
|
}
|
|
18286
18291
|
if (environmentToken !== void 0)
|
|
18287
|
-
return
|
|
18292
|
+
return environmentToken;
|
|
18288
18293
|
if (!store.ok || storedToken === void 0)
|
|
18289
|
-
return
|
|
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
|
|
18297
|
-
return
|
|
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 ??
|
|
18305
|
+
const writeErr = opts.writeErr ?? defaultWriteErr;
|
|
18301
18306
|
try {
|
|
18302
|
-
return await
|
|
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
|
|
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
|
|
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
|
|
18409
|
-
process.exit(
|
|
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((
|
|
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", () =>
|
|
18670
|
+
process.stdin.on("end", () => resolve4(Buffer.concat(chunks).toString("utf-8")));
|
|
18493
18671
|
process.stdin.on("error", reject);
|
|
18494
18672
|
});
|
|
18495
18673
|
}
|