heyspex 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +157 -0
- package/bin/hs.mjs +1028 -0
- package/bin/postinstall.mjs +88 -0
- package/dev-factory/lib/fleet-readiness.mjs +274 -0
- package/dev-factory/lib/fleet-state.mjs +250 -0
- package/dev-factory/lib/sufficiency-score.mjs +441 -0
- package/dist/cli/hs-check.mjs +747 -0
- package/dist/cli/hs-claim.mjs +541 -0
- package/dist/cli/hs-connect.mjs +3436 -0
- package/dist/cli/hs-defer.mjs +8455 -0
- package/dist/cli/hs-env.mjs +36845 -0
- package/dist/cli/hs-init.mjs +1145 -0
- package/dist/cli/hs-install.mjs +1390 -0
- package/dist/cli/hs-join.mjs +38553 -0
- package/dist/cli/hs-vault-pull.mjs +7782 -0
- package/dist/cli/hs-vault.mjs +4166 -0
- package/dist/cli/hs-workspace.mjs +2898 -0
- package/dist/cli-templates/customer-vault/specs/entity-model.md +1735 -0
- package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +531 -0
- package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +48 -0
- package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +548 -0
- package/docs/mcp-client-config.md +200 -0
- package/package.json +124 -0
- package/scripts/check-mcp-freshness.mjs +379 -0
- package/scripts/dev-server-status.mjs +722 -0
- package/scripts/gateway-skills-lib.mjs +421 -0
- package/scripts/lib/console-url-resolver.mjs +152 -0
- package/scripts/lib/dev-mode.mjs +369 -0
- package/scripts/lib/hs-link.mjs +154 -0
- package/scripts/lib/hs-source-commit.mjs +207 -0
- package/scripts/lib/hs-source-log.mjs +366 -0
- package/scripts/lib/hs-source-preflight.mjs +402 -0
- package/scripts/lib/hs-source-push.mjs +307 -0
- package/scripts/lib/hs-source-secret-scan.mjs +263 -0
- package/scripts/lib/standalone-doctor.mjs +89 -0
- package/scripts/vault-id-lib.mjs +173 -0
- package/vendor/heyspex-mcp/alignment-gate.mjs +1347 -0
- package/vendor/heyspex-mcp/blocker-resolver.mjs +891 -0
- package/vendor/heyspex-mcp/breakdown-ladder.mjs +309 -0
- package/vendor/heyspex-mcp/breakdown-source.mjs +236 -0
- package/vendor/heyspex-mcp/breakdown-writer.mjs +524 -0
- package/vendor/heyspex-mcp/brief-observability.mjs +67 -0
- package/vendor/heyspex-mcp/canvas-inbox.mjs +130 -0
- package/vendor/heyspex-mcp/capture-active-rehydrate.mjs +76 -0
- package/vendor/heyspex-mcp/capture-buffer-patches.mjs +147 -0
- package/vendor/heyspex-mcp/capture-channel.mjs +577 -0
- package/vendor/heyspex-mcp/capture-console-client.mjs +290 -0
- package/vendor/heyspex-mcp/capture-focus.mjs +1366 -0
- package/vendor/heyspex-mcp/capture-kind-suggest.mjs +121 -0
- package/vendor/heyspex-mcp/capture-outbox.mjs +239 -0
- package/vendor/heyspex-mcp/capture-stage-gate.mjs +160 -0
- package/vendor/heyspex-mcp/cli.mjs +15754 -0
- package/vendor/heyspex-mcp/config-settings.mjs +419 -0
- package/vendor/heyspex-mcp/console-fetch.mjs +342 -0
- package/vendor/heyspex-mcp/defer-spec.mjs +527 -0
- package/vendor/heyspex-mcp/deferred-reclaim.mjs +284 -0
- package/vendor/heyspex-mcp/design-tools.mjs +449 -0
- package/vendor/heyspex-mcp/dev-factory-bridge.mjs +66 -0
- package/vendor/heyspex-mcp/diagnose-skeleton.mjs +114 -0
- package/vendor/heyspex-mcp/fleet-queue-tools.mjs +241 -0
- package/vendor/heyspex-mcp/hooks-engine.mjs +399 -0
- package/vendor/heyspex-mcp/login-handoff.mjs +438 -0
- package/vendor/heyspex-mcp/mcp-subprocess.mjs +136 -0
- package/vendor/heyspex-mcp/mvp-mode.mjs +101 -0
- package/vendor/heyspex-mcp/next-resolution.mjs +240 -0
- package/vendor/heyspex-mcp/next-review.mjs +548 -0
- package/vendor/heyspex-mcp/peer-presence.mjs +430 -0
- package/vendor/heyspex-mcp/plan-ground-conflicts.mjs +321 -0
- package/vendor/heyspex-mcp/plan-ground.mjs +2001 -0
- package/vendor/heyspex-mcp/plan-ingest.mjs +476 -0
- package/vendor/heyspex-mcp/plan-push.mjs +1826 -0
- package/vendor/heyspex-mcp/plan-review.mjs +1419 -0
- package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
- package/vendor/heyspex-mcp/procedure-tools.mjs +970 -0
- package/vendor/heyspex-mcp/promote-capture.mjs +1280 -0
- package/vendor/heyspex-mcp/promote-diagnose.mjs +187 -0
- package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +338 -0
- package/vendor/heyspex-mcp/promote-research.mjs +1553 -0
- package/vendor/heyspex-mcp/promotion-helpers.mjs +280 -0
- package/vendor/heyspex-mcp/read-path.mjs +138 -0
- package/vendor/heyspex-mcp/recipe-tools.mjs +258 -0
- package/vendor/heyspex-mcp/regen-tasks.mjs +326 -0
- package/vendor/heyspex-mcp/register-spec.mjs +417 -0
- package/vendor/heyspex-mcp/research-focus.mjs +476 -0
- package/vendor/heyspex-mcp/research-tools.mjs +109 -0
- package/vendor/heyspex-mcp/research-type-suggest.mjs +112 -0
- package/vendor/heyspex-mcp/resolution-focus.mjs +45 -0
- package/vendor/heyspex-mcp/review-focus.mjs +55 -0
- package/vendor/heyspex-mcp/review-rollup.mjs +47 -0
- package/vendor/heyspex-mcp/run-front.mjs +60 -0
- package/vendor/heyspex-mcp/scheduler.mjs +813 -0
- package/vendor/heyspex-mcp/seat-identity.mjs +281 -0
- package/vendor/heyspex-mcp/settings-tools.mjs +119 -0
- package/vendor/heyspex-mcp/spec-id-lib.mjs +790 -0
- package/vendor/heyspex-mcp/split-research.mjs +832 -0
- package/vendor/heyspex-mcp/submit-for-review.mjs +190 -0
- package/vendor/heyspex-mcp/summary-contract.mjs +162 -0
- package/vendor/heyspex-mcp/test-helpers.mjs +43 -0
- package/vendor/heyspex-mcp/validate-linkage.mjs +574 -0
- package/vendor/heyspex-mcp/vault-resolve.mjs +297 -0
- package/vendor/heyspex-mcp/vault-settings.mjs +87 -0
- package/vendor/heyspex-mcp/verify-reproduction.mjs +314 -0
- package/vendor/heyspex-mcp/why-now.mjs +64 -0
- package/vendor/heyspex-mcp/yaml-scalar.mjs +34 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console HTTP helpers — extracted from cli.mjs so the timeout / retry /
|
|
3
|
+
* error-kind classification are unit-testable without booting the full MCP
|
|
4
|
+
* stdio server.
|
|
5
|
+
*
|
|
6
|
+
* Transport recovery (recurring false `CONSOLE_UNREACHABLE` when the server is up):
|
|
7
|
+
*
|
|
8
|
+
* 1. Default timeout is 30 s; one retry on `AbortError` → `CONSOLE_TIMEOUT`.
|
|
9
|
+
* 2. On network failure (`status: 0`), try alternate loopback hosts (localhost /
|
|
10
|
+
* 127.0.0.1 / [::1]) — Windows often binds one family only.
|
|
11
|
+
* 2b. On loopback network failure, backoff-retry the same URL (Next dev restart).
|
|
12
|
+
* 3. Optional `recoverFromTransportFailure` (wired in cli.mjs to
|
|
13
|
+
* `getDevServerStatus`) scans candidate ports; when the console is live on a
|
|
14
|
+
* different port than `.heyspex/console-url`, retries once and heals the file.
|
|
15
|
+
* 4. When discovery finds a live server but the retry still fails →
|
|
16
|
+
* `CONSOLE_URL_MISMATCH` (not "server down").
|
|
17
|
+
*
|
|
18
|
+
* Factory pattern matches `capture-console-client.mjs`: cli.mjs binds the real
|
|
19
|
+
* `getConsoleUrl`; tests pass stubs for both URL resolution and the fetch impl.
|
|
20
|
+
*
|
|
21
|
+
* TRANSPORT AUTH ([[HS-GD45-MCPC-TASK-mcp-cloud-auth-shared-console-resolver]]):
|
|
22
|
+
* {@link getAuthHeader} resolves an `Authorization: Bearer <token>` header
|
|
23
|
+
* (precedence: `HEYSPEX_MCP_TOKEN` env → `~/.heyspex/credentials.yaml`
|
|
24
|
+
* `mcp_token`, written by `hs mcp pair <code>`) and `makeConsoleFetch` attaches
|
|
25
|
+
* it to every outbound request — `consoleFetch` AND `postClaimEndpoint` share
|
|
26
|
+
* one transport, so both get it for free. No token resolvable → no header,
|
|
27
|
+
* exactly today's (unauthenticated) behavior against a local dev console.
|
|
28
|
+
*/
|
|
29
|
+
import { readFileSync } from 'node:fs';
|
|
30
|
+
import { homedir } from 'node:os';
|
|
31
|
+
import { join } from 'node:path';
|
|
32
|
+
|
|
33
|
+
export const DEFAULT_CONSOLE_TIMEOUT_MS = 30000;
|
|
34
|
+
|
|
35
|
+
/** Backoff when Next dev restarts (next.config change, etc.) — ECONNREFUSED is transient. */
|
|
36
|
+
export const DEV_RESTART_RETRY_DELAYS_MS = [1500, 2500, 4000];
|
|
37
|
+
|
|
38
|
+
function sleep(ms) {
|
|
39
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Detect AbortError from a thrown fetch error. Covers the standard
|
|
44
|
+
* `AbortError` name and the substring fallback some runtimes use
|
|
45
|
+
* ("This operation was aborted").
|
|
46
|
+
*/
|
|
47
|
+
export function isAbortError(err) {
|
|
48
|
+
if (!err) return false;
|
|
49
|
+
if (err.name === 'AbortError') return true;
|
|
50
|
+
const msg = typeof err.message === 'string' ? err.message : String(err);
|
|
51
|
+
return /aborted/i.test(msg);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Same port/path, alternate loopback hosts. Mirrors the probe rationale in
|
|
56
|
+
* scripts/dev-server-status.mjs (Next may listen on ::1 while fetch tried IPv4).
|
|
57
|
+
*/
|
|
58
|
+
export function loopbackHostVariants(urlString) {
|
|
59
|
+
let base;
|
|
60
|
+
try {
|
|
61
|
+
base = new URL(urlString);
|
|
62
|
+
} catch {
|
|
63
|
+
return [urlString];
|
|
64
|
+
}
|
|
65
|
+
const alts = new Set([base.hostname]);
|
|
66
|
+
if (base.hostname === 'localhost' || base.hostname === '127.0.0.1' || base.hostname === '::1') {
|
|
67
|
+
alts.add('localhost');
|
|
68
|
+
alts.add('127.0.0.1');
|
|
69
|
+
alts.add('::1');
|
|
70
|
+
}
|
|
71
|
+
const out = [];
|
|
72
|
+
for (const host of alts) {
|
|
73
|
+
const u = new URL(base.href);
|
|
74
|
+
u.hostname = host;
|
|
75
|
+
out.push(u.href);
|
|
76
|
+
}
|
|
77
|
+
return [...new Set(out)];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isLoopbackUrl(urlString) {
|
|
81
|
+
try {
|
|
82
|
+
const { hostname } = new URL(urlString);
|
|
83
|
+
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Rewrite the origin of `urlString` to match `originUrl` (port-discovery heal).
|
|
91
|
+
*/
|
|
92
|
+
export function rewriteUrlOrigin(urlString, originUrl) {
|
|
93
|
+
const target = new URL(urlString);
|
|
94
|
+
const origin = new URL(originUrl);
|
|
95
|
+
target.protocol = origin.protocol;
|
|
96
|
+
target.hostname = origin.hostname;
|
|
97
|
+
target.port = origin.port;
|
|
98
|
+
return target.href;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Resolve the outbound `Authorization` header, or `{}` when no token
|
|
103
|
+
* resolves (no header at all — matches today's unauthenticated behavior
|
|
104
|
+
* against a local dev console). Precedence: `HEYSPEX_MCP_TOKEN` env →
|
|
105
|
+
* `~/.heyspex/credentials.yaml` `mcp_token`. Injectable `env`/`homeDir` for
|
|
106
|
+
* tests; never throws (an unreadable credentials file is just "no token").
|
|
107
|
+
*/
|
|
108
|
+
export function getAuthHeader({ env = process.env, homeDir = homedir() } = {}) {
|
|
109
|
+
const envToken = env.HEYSPEX_MCP_TOKEN?.trim();
|
|
110
|
+
if (envToken) return { Authorization: `Bearer ${envToken}` };
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const raw = readFileSync(join(homeDir, '.heyspex', 'credentials.yaml'), 'utf8');
|
|
114
|
+
const match = raw.match(/^mcp_token:\s*["']?([^"'\r\n#]+)/m);
|
|
115
|
+
const token = match?.[1]?.trim();
|
|
116
|
+
if (token) return { Authorization: `Bearer ${token}` };
|
|
117
|
+
} catch {
|
|
118
|
+
// Missing file, missing key, or unreadable — no credentials-file token.
|
|
119
|
+
}
|
|
120
|
+
return {};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Build a console-fetch client bound to the given URL resolver + fetch impl.
|
|
125
|
+
*
|
|
126
|
+
* @param {object} deps
|
|
127
|
+
* @param {() => string} deps.getConsoleUrl
|
|
128
|
+
* @param {typeof fetch} [deps.fetchImpl]
|
|
129
|
+
* @param {number} [deps.defaultTimeoutMs]
|
|
130
|
+
* @param {(failedResult: object, ctx: { url: string, init: object, timeoutMs: number }) => Promise<{ retryUrl?: string, reason?: string, healedUrl?: string, discoveryHealed?: boolean, devServerStatus?: object } | null | undefined>} [deps.recoverFromTransportFailure]
|
|
131
|
+
* @param {() => object} [deps.getAuthHeader] Resolves the outbound Authorization header (default: {@link getAuthHeader}).
|
|
132
|
+
*/
|
|
133
|
+
export function makeConsoleFetch({
|
|
134
|
+
getConsoleUrl,
|
|
135
|
+
fetchImpl = fetch,
|
|
136
|
+
defaultTimeoutMs = DEFAULT_CONSOLE_TIMEOUT_MS,
|
|
137
|
+
recoverFromTransportFailure,
|
|
138
|
+
getAuthHeader: getAuthHeaderDep = getAuthHeader,
|
|
139
|
+
}) {
|
|
140
|
+
async function attempt(url, init, timeoutMs) {
|
|
141
|
+
const controller = new AbortController();
|
|
142
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
143
|
+
try {
|
|
144
|
+
const response = await fetchImpl(url, { ...init, signal: controller.signal });
|
|
145
|
+
const text = await response.text();
|
|
146
|
+
let parsed = null;
|
|
147
|
+
try {
|
|
148
|
+
parsed = text ? JSON.parse(text) : null;
|
|
149
|
+
} catch {
|
|
150
|
+
parsed = { raw: text };
|
|
151
|
+
}
|
|
152
|
+
return { ok: response.ok, status: response.status, body: parsed, url };
|
|
153
|
+
} catch (err) {
|
|
154
|
+
const errorKind = isAbortError(err) ? 'timeout' : 'network';
|
|
155
|
+
return {
|
|
156
|
+
ok: false,
|
|
157
|
+
status: 0,
|
|
158
|
+
body: null,
|
|
159
|
+
url,
|
|
160
|
+
error: err?.message || String(err),
|
|
161
|
+
errorKind,
|
|
162
|
+
};
|
|
163
|
+
} finally {
|
|
164
|
+
clearTimeout(timer);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function recoverNetworkFailure(result, ctx) {
|
|
169
|
+
const { url, init, timeoutMs } = ctx;
|
|
170
|
+
|
|
171
|
+
for (const alt of loopbackHostVariants(url)) {
|
|
172
|
+
if (alt === url) continue;
|
|
173
|
+
const retry = await attempt(alt, init, timeoutMs);
|
|
174
|
+
if (retry.ok) {
|
|
175
|
+
return {
|
|
176
|
+
...retry,
|
|
177
|
+
recoveredVia: 'loopback_host',
|
|
178
|
+
attemptedUrl: url,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Next.js dev restart window (e.g. next.config.mjs change) — loopback only, brief ECONNREFUSED.
|
|
184
|
+
if (isLoopbackUrl(url)) {
|
|
185
|
+
let last = result;
|
|
186
|
+
for (const delay of DEV_RESTART_RETRY_DELAYS_MS) {
|
|
187
|
+
await sleep(delay);
|
|
188
|
+
const retry = await attempt(url, init, timeoutMs);
|
|
189
|
+
if (retry.ok) {
|
|
190
|
+
return {
|
|
191
|
+
...retry,
|
|
192
|
+
recoveredVia: 'dev_restart',
|
|
193
|
+
attemptedUrl: url,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
last = retry;
|
|
197
|
+
if (retry.errorKind === 'timeout') break;
|
|
198
|
+
}
|
|
199
|
+
result = last;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (!recoverFromTransportFailure) return result;
|
|
203
|
+
|
|
204
|
+
let recovery;
|
|
205
|
+
try {
|
|
206
|
+
recovery = await recoverFromTransportFailure(result, ctx);
|
|
207
|
+
} catch {
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
if (!recovery) return result;
|
|
211
|
+
|
|
212
|
+
if (recovery.retryUrl && recovery.retryUrl !== url) {
|
|
213
|
+
const retry = await attempt(recovery.retryUrl, init, timeoutMs);
|
|
214
|
+
if (retry.ok) {
|
|
215
|
+
return {
|
|
216
|
+
...retry,
|
|
217
|
+
recoveredVia: recovery.reason || 'port_discovery',
|
|
218
|
+
attemptedUrl: url,
|
|
219
|
+
...(recovery.discoveryHealed
|
|
220
|
+
? { discoveryHealed: true, healedUrl: recovery.healedUrl || recovery.retryUrl }
|
|
221
|
+
: {}),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
...result,
|
|
226
|
+
urlMismatch: true,
|
|
227
|
+
suggestedUrl: recovery.healedUrl || recovery.retryUrl,
|
|
228
|
+
devServerStatus: recovery.devServerStatus,
|
|
229
|
+
discoveryAttempted: recovery.retryUrl,
|
|
230
|
+
discoveryHealed: recovery.discoveryHealed === true,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (recovery.devServerStatus?.running) {
|
|
235
|
+
const live = recovery.devServerStatus.servers?.find((s) => s.probe === 'console');
|
|
236
|
+
return {
|
|
237
|
+
...result,
|
|
238
|
+
urlMismatch: true,
|
|
239
|
+
suggestedUrl: live?.url,
|
|
240
|
+
devServerStatus: recovery.devServerStatus,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function withRetry(url, init, timeoutMs, { retryOnTimeout = true } = {}) {
|
|
248
|
+
let result = await attempt(url, init, timeoutMs);
|
|
249
|
+
if (!result.ok && result.errorKind === 'timeout' && retryOnTimeout) {
|
|
250
|
+
result = await attempt(url, init, timeoutMs);
|
|
251
|
+
}
|
|
252
|
+
if (result.ok) return result;
|
|
253
|
+
if (result.errorKind === 'network') {
|
|
254
|
+
result = await recoverNetworkFailure(result, { url, init, timeoutMs });
|
|
255
|
+
}
|
|
256
|
+
return result;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* POST/DELETE/GET against `/api/specs/:id/claim`.
|
|
261
|
+
*/
|
|
262
|
+
async function postClaimEndpoint(method, specId, bodyJson) {
|
|
263
|
+
const consoleUrl = getConsoleUrl();
|
|
264
|
+
const url = `${consoleUrl}/api/specs/${encodeURIComponent(specId)}/claim`;
|
|
265
|
+
const init = {
|
|
266
|
+
method,
|
|
267
|
+
headers: { 'content-type': 'application/json', ...getAuthHeaderDep() },
|
|
268
|
+
};
|
|
269
|
+
if (bodyJson !== undefined) {
|
|
270
|
+
init.body = JSON.stringify(bodyJson);
|
|
271
|
+
}
|
|
272
|
+
return withRetry(url, init, defaultTimeoutMs);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Generic fetch helper for console API with timeout + JSON parsing + 1 retry
|
|
277
|
+
* on AbortError. Returns `{ ok, status, body, url, error?, errorKind? }`.
|
|
278
|
+
*
|
|
279
|
+
* `options.retryOnTimeout: false` opts a NON-IDEMPOTENT call (a create) out
|
|
280
|
+
* of the blind timeout-retry against a REMOTE console ([[HS-2FQY]]): the
|
|
281
|
+
* server-side Idempotency-Key dedup ([[HS-70TD-DIAG]]) lives in a
|
|
282
|
+
* per-process in-memory registry, so on a serverless deployment
|
|
283
|
+
* (app.heyspex.com) the retry can land on a DIFFERENT instance and mint a
|
|
284
|
+
* second spec id for the same logical create. Against a loopback console
|
|
285
|
+
* the retry is kept — one dev process, the registry replays reliably.
|
|
286
|
+
*/
|
|
287
|
+
async function consoleFetch(path, options = {}) {
|
|
288
|
+
const consoleUrl = getConsoleUrl();
|
|
289
|
+
const url = `${consoleUrl}${path}`;
|
|
290
|
+
const init = {
|
|
291
|
+
method: options.method || 'GET',
|
|
292
|
+
headers: { 'content-type': 'application/json', ...getAuthHeaderDep(), ...options.headers },
|
|
293
|
+
};
|
|
294
|
+
if (options.body !== undefined) {
|
|
295
|
+
init.body = typeof options.body === 'string' ? options.body : JSON.stringify(options.body);
|
|
296
|
+
}
|
|
297
|
+
const timeoutMs = options.timeout || defaultTimeoutMs;
|
|
298
|
+
const retryOnTimeout = options.retryOnTimeout === false ? isLoopbackUrl(url) : true;
|
|
299
|
+
return withRetry(url, init, timeoutMs, { retryOnTimeout });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return { postClaimEndpoint, consoleFetch };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Map a failed transport result to an agent-facing error code. Splits
|
|
307
|
+
* timeout (server up but slow / cold-compiling) from unreachable (server
|
|
308
|
+
* actually down). Caller supplies the HTTP-error fallback used when the
|
|
309
|
+
* transport succeeded but the console returned a 4xx/5xx.
|
|
310
|
+
*/
|
|
311
|
+
export function consoleErrorCode(result, httpFailureCode) {
|
|
312
|
+
if (result.urlMismatch) return 'CONSOLE_URL_MISMATCH';
|
|
313
|
+
if (result.errorKind === 'timeout') return 'CONSOLE_TIMEOUT';
|
|
314
|
+
if (result.status === 0) return 'CONSOLE_UNREACHABLE';
|
|
315
|
+
return httpFailureCode;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Agent-facing message for a failed transport result. Tailored per
|
|
320
|
+
* error kind so the agent can act: retry on timeout vs start the dev
|
|
321
|
+
* server on network error.
|
|
322
|
+
*/
|
|
323
|
+
export function consoleErrorMessage(result, httpFailureMessage) {
|
|
324
|
+
if (result.urlMismatch) {
|
|
325
|
+
const live =
|
|
326
|
+
result.suggestedUrl
|
|
327
|
+
|| result.devServerStatus?.servers?.find((s) => s.probe === 'console')?.url;
|
|
328
|
+
const healNote = result.discoveryHealed
|
|
329
|
+
? ' Updated `.heyspex/console-url` — retry the tool call.'
|
|
330
|
+
: ' Call `heyspex_dev_server_status` and retry — do NOT hand-write vault files.';
|
|
331
|
+
return live
|
|
332
|
+
? `Spex-console dev server is running at ${live} but MCP tried ${result.url}.${healNote}`
|
|
333
|
+
: `Spex-console dev server appears to be running on another port than ${result.url}.${healNote}`;
|
|
334
|
+
}
|
|
335
|
+
if (result.errorKind === 'timeout') {
|
|
336
|
+
return `Console at ${result.url} timed out (retried once). It may be cold-compiling or under load — retry shortly. Do not conclude the dev server is down.`;
|
|
337
|
+
}
|
|
338
|
+
if (result.status === 0) {
|
|
339
|
+
return `Could not reach spex-console at ${result.url}: ${result.error}. Call heyspex_dev_server_status before concluding the server is down.`;
|
|
340
|
+
}
|
|
341
|
+
return httpFailureMessage;
|
|
342
|
+
}
|