@sentropic/h2a-runtime 0.98.7-rc.4 → 0.98.7-rc.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agy-claude.d.ts +41 -1
- package/dist/agy-claude.d.ts.map +1 -1
- package/dist/agy-claude.js +147 -2
- package/dist/agy-claude.js.map +1 -1
- package/dist/central-mcp.d.ts +32 -0
- package/dist/central-mcp.d.ts.map +1 -1
- package/dist/central-mcp.js +186 -69
- package/dist/central-mcp.js.map +1 -1
- package/dist/conv-guard.d.ts.map +1 -1
- package/dist/conv-guard.js +6 -2
- package/dist/conv-guard.js.map +1 -1
- package/dist/file-lock.d.ts +4 -2
- package/dist/file-lock.d.ts.map +1 -1
- package/dist/file-lock.js +24 -10
- package/dist/file-lock.js.map +1 -1
- package/dist/gateway-host/config-file.d.ts +2 -0
- package/dist/gateway-host/config-file.d.ts.map +1 -1
- package/dist/gateway-host/config-file.js +5 -1
- package/dist/gateway-host/config-file.js.map +1 -1
- package/dist/gateway-host/daemon.d.ts.map +1 -1
- package/dist/gateway-host/daemon.js +1 -2
- package/dist/gateway-host/daemon.js.map +1 -1
- package/dist/identity-cull/cull.js +1 -1
- package/dist/identity-cull/cull.js.map +1 -1
- package/dist/identity-cull/identity-fence.d.ts +48 -0
- package/dist/identity-cull/identity-fence.d.ts.map +1 -0
- package/dist/identity-cull/identity-fence.js +221 -0
- package/dist/identity-cull/identity-fence.js.map +1 -0
- package/dist/identity-operator-break.d.ts +43 -0
- package/dist/identity-operator-break.d.ts.map +1 -0
- package/dist/identity-operator-break.js +85 -0
- package/dist/identity-operator-break.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +59 -34
- package/dist/index.js.map +1 -1
- package/dist/llm-mesh-accounts.d.ts +2 -2
- package/dist/llm-mesh-accounts.d.ts.map +1 -1
- package/dist/llm-mesh-accounts.js +3 -5
- package/dist/llm-mesh-accounts.js.map +1 -1
- package/dist/native-host.d.ts +21 -0
- package/dist/native-host.d.ts.map +1 -1
- package/dist/native-host.js +163 -5
- package/dist/native-host.js.map +1 -1
- package/dist/native-terminal/fleet.d.ts +11 -0
- package/dist/native-terminal/fleet.d.ts.map +1 -1
- package/dist/native-terminal/fleet.js +58 -1
- package/dist/native-terminal/fleet.js.map +1 -1
- package/dist/native-terminal/host-scope.d.ts +18 -0
- package/dist/native-terminal/host-scope.d.ts.map +1 -0
- package/dist/native-terminal/host-scope.js +65 -0
- package/dist/native-terminal/host-scope.js.map +1 -0
- package/dist/native-terminal/host.d.ts +1 -0
- package/dist/native-terminal/host.d.ts.map +1 -1
- package/dist/native-terminal/host.js +4 -1
- package/dist/native-terminal/host.js.map +1 -1
- package/dist/native-terminal/op.d.ts +36 -1
- package/dist/native-terminal/op.d.ts.map +1 -1
- package/dist/native-terminal/op.js +190 -33
- package/dist/native-terminal/op.js.map +1 -1
- package/dist/native-terminal/process.d.ts.map +1 -1
- package/dist/native-terminal/process.js +10 -1
- package/dist/native-terminal/process.js.map +1 -1
- package/dist/native-terminal/screen.d.ts.map +1 -1
- package/dist/native-terminal/screen.js +1 -1
- package/dist/native-terminal/screen.js.map +1 -1
- package/dist/native-terminal/server.d.ts +12 -2
- package/dist/native-terminal/server.d.ts.map +1 -1
- package/dist/native-terminal/server.js +143 -42
- package/dist/native-terminal/server.js.map +1 -1
- package/dist/native-terminal/session-scope.d.ts +2 -0
- package/dist/native-terminal/session-scope.d.ts.map +1 -0
- package/dist/native-terminal/session-scope.js +32 -0
- package/dist/native-terminal/session-scope.js.map +1 -0
- package/dist/registry.d.ts +5 -12
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +20 -3
- package/dist/registry.js.map +1 -1
- package/dist/restore.d.ts.map +1 -1
- package/dist/restore.js +5 -37
- package/dist/restore.js.map +1 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +3 -3
- package/dist/run.js.map +1 -1
- package/dist/tmux.d.ts +1 -1
- package/dist/tmux.d.ts.map +1 -1
- package/dist/tmux.js +34 -2
- package/dist/tmux.js.map +1 -1
- package/package.json +2 -2
package/dist/agy-claude.d.ts
CHANGED
|
@@ -1,2 +1,42 @@
|
|
|
1
|
-
export
|
|
1
|
+
export declare const AGY_CLAUDE_QUOTA_CODE = "agy-claude-quota-exhausted";
|
|
2
|
+
export type AgyEffort = "low" | "medium" | "high";
|
|
3
|
+
export declare class AgyEffortError extends Error {
|
|
4
|
+
readonly code = "agy-effort-unsupported";
|
|
5
|
+
}
|
|
6
|
+
export declare function normalizeAgyEffort(effort: string | undefined): AgyEffort | undefined;
|
|
7
|
+
export declare function isAgyClaudeModel(model: string | undefined): boolean;
|
|
8
|
+
export declare function resolveAgyModel(model: string | undefined, effort?: string): string | undefined;
|
|
9
|
+
export declare class AgyClaudeBuildOptInError extends Error {
|
|
10
|
+
readonly code = "agy-claude-build-requires-opt-in";
|
|
11
|
+
}
|
|
12
|
+
export declare function assertAgyClaudeBuildAllowed(options: {
|
|
13
|
+
profile: string;
|
|
14
|
+
model?: string | undefined;
|
|
15
|
+
role?: string | undefined;
|
|
16
|
+
allowClaudeBuild?: boolean | undefined;
|
|
17
|
+
}): void;
|
|
18
|
+
export type AgyClaudeQuotaStatus = {
|
|
19
|
+
status: "unknown" | "exhausted";
|
|
20
|
+
observedAt?: string;
|
|
21
|
+
resetsAt?: string;
|
|
22
|
+
signature?: string;
|
|
23
|
+
};
|
|
24
|
+
export declare function detectAgyClaudeQuotaExhaustion(output: string, now?: number): {
|
|
25
|
+
exhausted: boolean;
|
|
26
|
+
signature?: string;
|
|
27
|
+
resetsAt?: string;
|
|
28
|
+
};
|
|
29
|
+
export declare class AgyClaudeQuotaExhaustedError extends Error {
|
|
30
|
+
readonly signature: string;
|
|
31
|
+
readonly code = "agy-claude-quota-exhausted";
|
|
32
|
+
readonly retrySafe = false;
|
|
33
|
+
readonly retryable = false;
|
|
34
|
+
readonly quota: AgyClaudeQuotaStatus;
|
|
35
|
+
constructor(signature: string, output: string, now?: number);
|
|
36
|
+
get resetsAt(): string | undefined;
|
|
37
|
+
}
|
|
38
|
+
export declare function agyClaudeQuotaError(signature: string, output: string, now?: number): AgyClaudeQuotaExhaustedError;
|
|
39
|
+
export declare function agyQuotaStatusPath(): string;
|
|
40
|
+
export declare function readAgyClaudeQuotaStatus(path?: string): AgyClaudeQuotaStatus;
|
|
41
|
+
export declare function writeAgyClaudeQuotaStatus(status: AgyClaudeQuotaStatus, path?: string): void;
|
|
2
42
|
//# sourceMappingURL=agy-claude.d.ts.map
|
package/dist/agy-claude.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agy-claude.d.ts","sourceRoot":"","sources":["../src/agy-claude.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"agy-claude.d.ts","sourceRoot":"","sources":["../src/agy-claude.ts"],"names":[],"mappings":"AAYA,eAAO,MAAM,qBAAqB,+BAA+B,CAAC;AAClE,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;AAClD,qBAAa,cAAe,SAAQ,KAAK;IACvC,QAAQ,CAAC,IAAI,4BAA4B;CAC1C;AACD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAKpF;AACD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAEnE;AACD,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAU9F;AACD,qBAAa,wBAAyB,SAAQ,KAAK;IACjD,QAAQ,CAAC,IAAI,sCAAsC;CACpD;AACD,wBAAgB,2BAA2B,CAAC,OAAO,EAAE;IACnD,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,gBAAgB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAChH,GAAG,IAAI,CAOP;AACD,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,SAAS,GAAG,WAAW,CAAC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AACF,wBAAgB,8BAA8B,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,SAAa,GAAG;IAChF,SAAS,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC3D,CAyCA;AACD,qBAAa,4BAA6B,SAAQ,KAAK;IAKzC,QAAQ,CAAC,SAAS,EAAE,MAAM;IAJtC,QAAQ,CAAC,IAAI,gCAAyB;IACtC,QAAQ,CAAC,SAAS,SAAS;IAC3B,QAAQ,CAAC,SAAS,SAAS;IAC3B,QAAQ,CAAC,KAAK,EAAE,oBAAoB,CAAC;gBAChB,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,SAAa;IAMxE,IAAI,QAAQ,IAAI,MAAM,GAAG,SAAS,CAAgC;CACnE;AACD,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,SAAa,GAAG,4BAA4B,CAErH;AACD,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AACD,wBAAgB,wBAAwB,CAAC,IAAI,SAAuB,GAAG,oBAAoB,CAU1F;AACD,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,oBAAoB,EAAE,IAAI,SAAuB,GAAG,IAAI,CAKzG"}
|
package/dist/agy-claude.js
CHANGED
|
@@ -1,3 +1,148 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
1
|
+
// AGY/Claude shared contract — VENDORED copy of
|
|
2
|
+
// `packages/h2a/src/runtime/agy-claude.ts` (see agy-claude-vendor-sync.test.ts).
|
|
3
|
+
// Why vendored instead of imported: this module is a leaf (node builtins
|
|
4
|
+
// only) that both the runtime and the core need; a cross-package import in
|
|
5
|
+
// either direction creates an undeclared edge that breaks isolated installs
|
|
6
|
+
// of this package (MODULE_NOT_FOUND at host startup) or an unorderable
|
|
7
|
+
// typecheck cycle. Edit the CORE copy first, then sync this file exactly
|
|
8
|
+
// (the sync test fails closed on any drift).
|
|
9
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
export const AGY_CLAUDE_QUOTA_CODE = "agy-claude-quota-exhausted";
|
|
13
|
+
export class AgyEffortError extends Error {
|
|
14
|
+
code = "agy-effort-unsupported";
|
|
15
|
+
}
|
|
16
|
+
export function normalizeAgyEffort(effort) {
|
|
17
|
+
if (effort === undefined)
|
|
18
|
+
return undefined;
|
|
19
|
+
if (effort === "max")
|
|
20
|
+
return "high";
|
|
21
|
+
if (effort === "low" || effort === "medium" || effort === "high")
|
|
22
|
+
return effort;
|
|
23
|
+
throw new AgyEffortError(`invalid AGY effort ${effort}; AGY effort must be low|medium|high (max maps to high)`);
|
|
24
|
+
}
|
|
25
|
+
export function isAgyClaudeModel(model) {
|
|
26
|
+
return model?.startsWith("claude-") ?? false;
|
|
27
|
+
}
|
|
28
|
+
export function resolveAgyModel(model, effort) {
|
|
29
|
+
const level = normalizeAgyEffort(effort);
|
|
30
|
+
const claude = model && /^(claude-(?:opus|sonnet)-5-5)(?:-(.*))?$/.exec(model);
|
|
31
|
+
if (claude) {
|
|
32
|
+
// Validate a supplied suffix even when a separate effort overrides it.
|
|
33
|
+
const suffix = normalizeAgyEffort(claude[2]);
|
|
34
|
+
const resolved = level ?? suffix;
|
|
35
|
+
return resolved ? `${claude[1]}-${resolved}` : model;
|
|
36
|
+
}
|
|
37
|
+
return model;
|
|
38
|
+
}
|
|
39
|
+
export class AgyClaudeBuildOptInError extends Error {
|
|
40
|
+
code = "agy-claude-build-requires-opt-in";
|
|
41
|
+
}
|
|
42
|
+
export function assertAgyClaudeBuildAllowed(options) {
|
|
43
|
+
if (options.role !== undefined && !["review", "challenge", "build"].includes(options.role)) {
|
|
44
|
+
throw new Error("invalid agy run role: expected review|challenge|build");
|
|
45
|
+
}
|
|
46
|
+
if (options.profile === "agy" && isAgyClaudeModel(options.model) && options.role === "build" && options.allowClaudeBuild !== true) {
|
|
47
|
+
throw new AgyClaudeBuildOptInError("AGY Claude credits are reserved for reviews and adversarial challenges; build requires --allow-claude-build");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function detectAgyClaudeQuotaExhaustion(output, now = Date.now()) {
|
|
51
|
+
let preliminary = { exhausted: false };
|
|
52
|
+
const lines = output.split(/\r?\n/);
|
|
53
|
+
for (let index = 0; index < lines.length; index++) {
|
|
54
|
+
let line = lines[index];
|
|
55
|
+
// Provider diagnostics only: prose mentioning 429/quota is not evidence.
|
|
56
|
+
if (!/^\s*(?:AGY_ERROR\s*\{|(?:API )?error\s*:)/i.test(line))
|
|
57
|
+
continue;
|
|
58
|
+
let structured = /AGY_ERROR\s*(\{.*)/.exec(line)?.[1];
|
|
59
|
+
if (structured) {
|
|
60
|
+
// Native/tmux screen captures insert newlines at visual column wraps,
|
|
61
|
+
// including inside JSON strings. Reassemble only an anchored diagnostic,
|
|
62
|
+
// require complete JSON, and never consume the next provider diagnostic.
|
|
63
|
+
let end = index;
|
|
64
|
+
for (;;) {
|
|
65
|
+
try {
|
|
66
|
+
JSON.parse(structured);
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
const next = lines[end + 1];
|
|
71
|
+
if (next === undefined || /^\s*(?:AGY_ERROR\s*\{|(?:API )?error\s*:)/i.test(next))
|
|
72
|
+
break;
|
|
73
|
+
structured += next;
|
|
74
|
+
end++;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
JSON.parse(structured);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
continue;
|
|
82
|
+
} // Partial chunks still prove no complete reset.
|
|
83
|
+
line = `AGY_ERROR ${structured}`;
|
|
84
|
+
index = end;
|
|
85
|
+
}
|
|
86
|
+
let signature;
|
|
87
|
+
if (/\b429\b/.test(line))
|
|
88
|
+
signature = "agy-claude:http-429";
|
|
89
|
+
else if (/RESOURCE_EXHAUSTED/i.test(line))
|
|
90
|
+
signature = "agy-claude:resource-exhausted";
|
|
91
|
+
else if (/quota (?:exceeded|reached)|individual quota/i.test(line))
|
|
92
|
+
signature = "agy-claude:quota-exceeded";
|
|
93
|
+
if (!signature)
|
|
94
|
+
continue;
|
|
95
|
+
const duration = /Resets in\s+((?:\d+(?:\.\d+)?\s*[dhms]\s*)+)/i.exec(line)?.[1];
|
|
96
|
+
let ms = 0;
|
|
97
|
+
if (duration)
|
|
98
|
+
for (const part of duration.matchAll(/(\d+(?:\.\d+)?)\s*([dhms])/gi)) {
|
|
99
|
+
ms += Number(part[1]) * ({ d: 86_400_000, h: 3_600_000, m: 60_000, s: 1_000 }[part[2].toLowerCase()] ?? 0);
|
|
100
|
+
}
|
|
101
|
+
const resetsAt = duration && Number.isFinite(now + ms) && ms < 8.64e15 - now
|
|
102
|
+
? new Date(now + ms).toISOString() : undefined;
|
|
103
|
+
if (resetsAt)
|
|
104
|
+
return { exhausted: true, signature, resetsAt };
|
|
105
|
+
preliminary = { exhausted: true, signature };
|
|
106
|
+
}
|
|
107
|
+
return preliminary;
|
|
108
|
+
}
|
|
109
|
+
export class AgyClaudeQuotaExhaustedError extends Error {
|
|
110
|
+
signature;
|
|
111
|
+
code = AGY_CLAUDE_QUOTA_CODE;
|
|
112
|
+
retrySafe = false;
|
|
113
|
+
retryable = false;
|
|
114
|
+
quota;
|
|
115
|
+
constructor(signature, output, now = Date.now()) {
|
|
116
|
+
super("AGY Claude quota exhausted; no fallback or automatic retry. The caller chooses the next action.");
|
|
117
|
+
this.signature = signature;
|
|
118
|
+
const detected = detectAgyClaudeQuotaExhaustion(output, now);
|
|
119
|
+
this.quota = { status: "exhausted", signature, observedAt: new Date(now).toISOString(),
|
|
120
|
+
...(detected.resetsAt ? { resetsAt: detected.resetsAt } : {}) };
|
|
121
|
+
}
|
|
122
|
+
get resetsAt() { return this.quota.resetsAt; }
|
|
123
|
+
}
|
|
124
|
+
export function agyClaudeQuotaError(signature, output, now = Date.now()) {
|
|
125
|
+
return new AgyClaudeQuotaExhaustedError(signature, output, now);
|
|
126
|
+
}
|
|
127
|
+
export function agyQuotaStatusPath() {
|
|
128
|
+
return join(process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state"), "h2a", "agy-claude-quota.json");
|
|
129
|
+
}
|
|
130
|
+
export function readAgyClaudeQuotaStatus(path = agyQuotaStatusPath()) {
|
|
131
|
+
try {
|
|
132
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
133
|
+
if (value.status === "exhausted" && typeof value.observedAt === "string" && Number.isFinite(Date.parse(value.observedAt))) {
|
|
134
|
+
return { status: value.status, observedAt: value.observedAt,
|
|
135
|
+
...(typeof value.signature === "string" ? { signature: value.signature } : {}),
|
|
136
|
+
...(typeof value.resetsAt === "string" && Number.isFinite(Date.parse(value.resetsAt)) ? { resetsAt: value.resetsAt } : {}) };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch { /* No provider observation: never invent availability. */ }
|
|
140
|
+
return { status: "unknown" };
|
|
141
|
+
}
|
|
142
|
+
export function writeAgyClaudeQuotaStatus(status, path = agyQuotaStatusPath()) {
|
|
143
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
144
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
145
|
+
writeFileSync(temporary, JSON.stringify(status), { mode: 0o600 });
|
|
146
|
+
renameSync(temporary, path);
|
|
147
|
+
}
|
|
3
148
|
//# sourceMappingURL=agy-claude.js.map
|
package/dist/agy-claude.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agy-claude.js","sourceRoot":"","sources":["../src/agy-claude.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,cAAc,2BAA2B,CAAC"}
|
|
1
|
+
{"version":3,"file":"agy-claude.js","sourceRoot":"","sources":["../src/agy-claude.ts"],"names":[],"mappings":"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"}
|
package/dist/central-mcp.d.ts
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
type CentralMarker = Readonly<{
|
|
2
|
+
endpoint: string;
|
|
3
|
+
generation: string;
|
|
4
|
+
pid: number;
|
|
5
|
+
startedAt: string;
|
|
6
|
+
}>;
|
|
7
|
+
type CentralPing = Readonly<{
|
|
8
|
+
kind: "generation";
|
|
9
|
+
generation: string;
|
|
10
|
+
}> | Readonly<{
|
|
11
|
+
kind: "dead";
|
|
12
|
+
}> | Readonly<{
|
|
13
|
+
kind: "ambiguous";
|
|
14
|
+
}>;
|
|
1
15
|
export type CentralMcpPreparation = Readonly<{
|
|
2
16
|
status: "central";
|
|
3
17
|
endpoint: string;
|
|
@@ -6,6 +20,20 @@ export type CentralMcpPreparation = Readonly<{
|
|
|
6
20
|
status: "degraded";
|
|
7
21
|
reason: string;
|
|
8
22
|
}>;
|
|
23
|
+
/** Test seam mirroring the core marker-path options. Production always uses
|
|
24
|
+
* `/run/user/<uid>`; the path is never env-derived. */
|
|
25
|
+
export interface CentralMcpPaths {
|
|
26
|
+
readonly runtimeBase?: string;
|
|
27
|
+
}
|
|
28
|
+
export declare const H2A_MCP_CENTRAL_ENV = "H2A_MCP_CENTRAL";
|
|
29
|
+
export declare const H2A_MCP_CENTRAL_ENDPOINT_ENV = "H2A_MCP_CENTRAL_ENDPOINT";
|
|
30
|
+
export declare function centralMcpPing(endpoint: string, timeoutMs?: number): Promise<CentralPing>;
|
|
31
|
+
/**
|
|
32
|
+
* Read the protected central marker without exposing its token to callers
|
|
33
|
+
* that only need liveness/generation metadata. A missing marker is the sole
|
|
34
|
+
* non-error absence case; malformed or insecure state remains a hard failure.
|
|
35
|
+
*/
|
|
36
|
+
export declare function readCentralMcpMarker(paths?: CentralMcpPaths): CentralMarker | undefined;
|
|
9
37
|
/** Raw (unstructured) central MCP server output. Read by `h2a logs`, rotated at spawn. */
|
|
10
38
|
export declare function resolveCentralMcpLogPath(): string;
|
|
11
39
|
/**
|
|
@@ -15,6 +43,7 @@ export declare function resolveCentralMcpLogPath(): string;
|
|
|
15
43
|
*/
|
|
16
44
|
export declare function ensureCentralMcp(options?: Readonly<{
|
|
17
45
|
root?: string;
|
|
46
|
+
paths?: CentralMcpPaths;
|
|
18
47
|
}>): Promise<Readonly<{
|
|
19
48
|
endpoint: string;
|
|
20
49
|
generation: string;
|
|
@@ -24,9 +53,12 @@ export declare function prepareCentralMcpForLaunch(options: Readonly<{
|
|
|
24
53
|
root?: string;
|
|
25
54
|
profile: string;
|
|
26
55
|
cwd: string;
|
|
56
|
+
paths?: CentralMcpPaths;
|
|
27
57
|
}>): Promise<CentralMcpPreparation | undefined>;
|
|
28
58
|
/** Restore launches inherit these values; each re-entered run rewrites its host config. */
|
|
29
59
|
export declare function prepareCentralMcpForRestore(options?: Readonly<{
|
|
30
60
|
root?: string;
|
|
61
|
+
paths?: CentralMcpPaths;
|
|
31
62
|
}>): Promise<CentralMcpPreparation | undefined>;
|
|
63
|
+
export {};
|
|
32
64
|
//# sourceMappingURL=central-mcp.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"central-mcp.d.ts","sourceRoot":"","sources":["../src/central-mcp.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"central-mcp.d.ts","sourceRoot":"","sources":["../src/central-mcp.ts"],"names":[],"mappings":"AA0CA,KAAK,aAAa,GAAG,QAAQ,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC,CAAC;AAEH,KAAK,WAAW,GACZ,QAAQ,CAAC;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,GACpD,QAAQ,CAAC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,GAC1B,QAAQ,CAAC;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,CAAC,CAAC;AAEpC,MAAM,MAAM,qBAAqB,GAC7B,QAAQ,CAAC;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,GACrE,QAAQ,CAAC;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAMrD;wDACwD;AACxD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAID,eAAO,MAAM,mBAAmB,oBAAoB,CAAC;AACrD,eAAO,MAAM,4BAA4B,6BAA6B,CAAC;AA0DvE,wBAAsB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,SAA8B,GAAG,OAAO,CAAC,WAAW,CAAC,CAyBpH;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,GAAE,eAAoB,GAAG,aAAa,GAAG,SAAS,CAuC3F;AA4BD,0FAA0F;AAC1F,wBAAgB,wBAAwB,IAAI,MAAM,CAEjD;AAuFD;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,GAAE,QAAQ,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,eAAe,CAAA;CAAE,CAAM,GACjE,OAAO,CAAC,QAAQ,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,SAAS,CAAC,CA6DzE;AAoFD,mFAAmF;AACnF,wBAAsB,0BAA0B,CAAC,OAAO,EAAE,QAAQ,CAAC;IACjE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,eAAe,CAAC;CACzB,CAAC,GAAG,OAAO,CAAC,qBAAqB,GAAG,SAAS,CAAC,CAyB9C;AAED,2FAA2F;AAC3F,wBAAsB,2BAA2B,CAC/C,OAAO,GAAE,QAAQ,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,eAAe,CAAA;CAAE,CAAM,GACjE,OAAO,CAAC,qBAAqB,GAAG,SAAS,CAAC,CAU5C"}
|
package/dist/central-mcp.js
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Runtime-side central MCP lifecycle.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Runtime-side central MCP lifecycle. This module owns its end of the
|
|
3
|
+
* central contract: endpoint selection, marker read/validation, HTTP
|
|
4
|
+
* liveness ping, and starting the existing foreground serve command as a
|
|
5
|
+
* detached child when necessary. It deliberately never names the core
|
|
6
|
+
* package: host-config rendering stays a core command invoked over the
|
|
7
|
+
* `h2a` binary (the same spawn pattern used for `mcp-central-serve`
|
|
8
|
+
* below), so isolated installs of this package keep working and degrade
|
|
9
|
+
* cleanly where core is absent.
|
|
10
|
+
*
|
|
11
|
+
* Cross-package constants below (env names, marker directory/file, ping
|
|
12
|
+
* path, file mode) mirror the core contract values. They are pinned by the
|
|
13
|
+
* vitest suite in this directory, which exercises them against real
|
|
14
|
+
* markers and a real HTTP peer.
|
|
6
15
|
*/
|
|
7
|
-
import { spawn } from "node:child_process";
|
|
8
|
-
import { chmodSync, closeSync, constants, existsSync, mkdirSync, openSync, readFileSync, readSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
16
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
17
|
+
import { chmodSync, closeSync, constants, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
9
18
|
import { homedir } from "node:os";
|
|
10
19
|
import { dirname, join } from "node:path";
|
|
11
20
|
import { getH2aConfig, resolveConfigPath, setH2aConfig } from "./config.js";
|
|
@@ -14,24 +23,135 @@ const CENTRAL_PORT_BASE = 47_000;
|
|
|
14
23
|
const CENTRAL_PORT_SPAN = 10_000;
|
|
15
24
|
const CENTRAL_START_TIMEOUT_MS = 10_000;
|
|
16
25
|
const CENTRAL_START_POLL_MS = 100;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
// Stable cross-package contract values (mirrored from core; pinned by the
|
|
27
|
+
// vitest suite in this directory against real markers and a real HTTP peer).
|
|
28
|
+
export const H2A_MCP_CENTRAL_ENV = "H2A_MCP_CENTRAL";
|
|
29
|
+
export const H2A_MCP_CENTRAL_ENDPOINT_ENV = "H2A_MCP_CENTRAL_ENDPOINT";
|
|
30
|
+
const CENTRAL_RUNTIME_DIRECTORY = "h2a-mcp-central";
|
|
31
|
+
const CENTRAL_MARKER_FILE = "marker.json";
|
|
32
|
+
const CENTRAL_PING_PATH = "/_h2a-central/ping";
|
|
33
|
+
const CENTRAL_LIVENESS_TIMEOUT_MS = 1_500;
|
|
34
|
+
const CENTRAL_HOST_SETUP_TIMEOUT_MS = 60_000;
|
|
35
|
+
function currentUid() {
|
|
36
|
+
if (typeof process.getuid !== "function") {
|
|
37
|
+
throw new Error(`${H2A_MCP_CENTRAL_ENDPOINT_ENV} requires a current uid`);
|
|
38
|
+
}
|
|
39
|
+
return process.getuid();
|
|
40
|
+
}
|
|
41
|
+
function centralMcpMarkerPath(paths = {}) {
|
|
42
|
+
const base = paths.runtimeBase ?? join("/run/user", String(currentUid()));
|
|
43
|
+
return join(base, CENTRAL_RUNTIME_DIRECTORY, CENTRAL_MARKER_FILE);
|
|
44
|
+
}
|
|
45
|
+
function isLoopbackHostname(hostname) {
|
|
46
|
+
return hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]" || hostname === "localhost";
|
|
47
|
+
}
|
|
48
|
+
function parseCentralMcpEndpoint(value) {
|
|
49
|
+
if (!value || value.trim().length === 0) {
|
|
50
|
+
throw new Error(`${H2A_MCP_CENTRAL_ENDPOINT_ENV} must be a non-empty absolute http URL`);
|
|
51
|
+
}
|
|
52
|
+
let endpoint;
|
|
53
|
+
try {
|
|
54
|
+
endpoint = new URL(value);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
throw new Error(`${H2A_MCP_CENTRAL_ENDPOINT_ENV} must be a non-empty absolute http URL`);
|
|
58
|
+
}
|
|
59
|
+
if (endpoint.protocol !== "http:" ||
|
|
60
|
+
!isLoopbackHostname(endpoint.hostname) ||
|
|
61
|
+
!endpoint.port ||
|
|
62
|
+
Number(endpoint.port) < 1 ||
|
|
63
|
+
Number(endpoint.port) > 65_535 ||
|
|
64
|
+
endpoint.username ||
|
|
65
|
+
endpoint.password ||
|
|
66
|
+
endpoint.search ||
|
|
67
|
+
endpoint.hash) {
|
|
68
|
+
throw new Error(`${H2A_MCP_CENTRAL_ENDPOINT_ENV} must be an absolute http URL with an explicit port`);
|
|
69
|
+
}
|
|
70
|
+
return endpoint.href;
|
|
71
|
+
}
|
|
72
|
+
function errorHasCode(error, expectedCode) {
|
|
73
|
+
let current = error;
|
|
74
|
+
for (let depth = 0; depth < 4 && current && typeof current === "object"; depth += 1) {
|
|
75
|
+
if (current.code === expectedCode)
|
|
76
|
+
return true;
|
|
77
|
+
current = current.cause;
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
export async function centralMcpPing(endpoint, timeoutMs = CENTRAL_LIVENESS_TIMEOUT_MS) {
|
|
82
|
+
const canonicalEndpoint = parseCentralMcpEndpoint(endpoint);
|
|
83
|
+
const controller = new AbortController();
|
|
84
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
85
|
+
const pingUrl = new URL(CENTRAL_PING_PATH, canonicalEndpoint).href;
|
|
86
|
+
return fetch(pingUrl, { signal: controller.signal }).then(async (response) => {
|
|
87
|
+
clearTimeout(timeout);
|
|
88
|
+
if (!response.ok)
|
|
89
|
+
return { kind: "dead" };
|
|
90
|
+
try {
|
|
91
|
+
const body = (await response.json());
|
|
92
|
+
return typeof body.generation === "string" && body.generation.length > 0
|
|
93
|
+
? { kind: "generation", generation: body.generation }
|
|
94
|
+
: { kind: "dead" };
|
|
31
95
|
}
|
|
32
|
-
|
|
96
|
+
catch {
|
|
97
|
+
return { kind: "dead" };
|
|
98
|
+
}
|
|
99
|
+
}, (error) => {
|
|
100
|
+
clearTimeout(timeout);
|
|
101
|
+
// ECONNREFUSED is the only transport failure that positively proves no
|
|
102
|
+
// listener owns this registration. Timeouts and all other errors are not.
|
|
103
|
+
return (errorHasCode(error, "ECONNREFUSED") ? { kind: "dead" } : { kind: "ambiguous" });
|
|
33
104
|
});
|
|
34
|
-
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Read the protected central marker without exposing its token to callers
|
|
108
|
+
* that only need liveness/generation metadata. A missing marker is the sole
|
|
109
|
+
* non-error absence case; malformed or insecure state remains a hard failure.
|
|
110
|
+
*/
|
|
111
|
+
export function readCentralMcpMarker(paths = {}) {
|
|
112
|
+
const path = centralMcpMarkerPath(paths);
|
|
113
|
+
let info;
|
|
114
|
+
try {
|
|
115
|
+
info = lstatSync(path);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (error.code === "ENOENT")
|
|
119
|
+
return undefined;
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
if (!info.isFile())
|
|
123
|
+
throw new Error(`central MCP marker is not a regular file: ${path}`);
|
|
124
|
+
if (info.uid !== currentUid())
|
|
125
|
+
throw new Error(`central MCP marker is not owned by the current user: ${path}`);
|
|
126
|
+
if ((info.mode & 0o777) !== 0o600)
|
|
127
|
+
throw new Error(`central MCP marker must have mode 0600: ${path}`);
|
|
128
|
+
let value;
|
|
129
|
+
try {
|
|
130
|
+
value = JSON.parse(readFileSync(path, "utf8"));
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
throw new Error(`central MCP marker is malformed: ${path}`);
|
|
134
|
+
}
|
|
135
|
+
if (typeof value.endpoint !== "string" ||
|
|
136
|
+
typeof value.generation !== "string" ||
|
|
137
|
+
value.generation.length === 0 ||
|
|
138
|
+
typeof value.pid !== "number" ||
|
|
139
|
+
typeof value.startedAt !== "string" ||
|
|
140
|
+
typeof value.token !== "string" ||
|
|
141
|
+
value.token.length === 0) {
|
|
142
|
+
throw new Error(`central MCP marker is malformed: ${path}`);
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
return {
|
|
146
|
+
endpoint: parseCentralMcpEndpoint(value.endpoint),
|
|
147
|
+
generation: value.generation,
|
|
148
|
+
pid: value.pid,
|
|
149
|
+
startedAt: value.startedAt,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
throw new Error(`central MCP marker is malformed: ${path}`);
|
|
154
|
+
}
|
|
35
155
|
}
|
|
36
156
|
function delay(milliseconds) {
|
|
37
157
|
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
@@ -127,7 +247,7 @@ function centralLogFile() {
|
|
|
127
247
|
catch { /* rotation is best-effort; the spawn below still proceeds */ }
|
|
128
248
|
return path;
|
|
129
249
|
}
|
|
130
|
-
async function waitForCentralStart(
|
|
250
|
+
async function waitForCentralStart(paths, endpoint, spawnFailure) {
|
|
131
251
|
const deadline = Date.now() + CENTRAL_START_TIMEOUT_MS;
|
|
132
252
|
while (Date.now() < deadline) {
|
|
133
253
|
const failure = spawnFailure();
|
|
@@ -136,9 +256,9 @@ async function waitForCentralStart(core, endpoint, spawnFailure) {
|
|
|
136
256
|
appendCrashLog("mcp-server", { event: "startupError", reason: "spawn-failed", detail: failure.message });
|
|
137
257
|
throw failure;
|
|
138
258
|
}
|
|
139
|
-
const ping = await
|
|
259
|
+
const ping = await centralMcpPing(endpoint, CENTRAL_START_POLL_MS);
|
|
140
260
|
if (ping.kind === "generation") {
|
|
141
|
-
const marker =
|
|
261
|
+
const marker = readCentralMcpMarker(paths);
|
|
142
262
|
if (marker?.endpoint === endpoint && marker.generation === ping.generation) {
|
|
143
263
|
return { endpoint, generation: ping.generation };
|
|
144
264
|
}
|
|
@@ -159,10 +279,10 @@ export async function ensureCentralMcp(options = {}) {
|
|
|
159
279
|
const endpoint = configuredCentralEndpoint();
|
|
160
280
|
if (!endpoint)
|
|
161
281
|
return undefined;
|
|
162
|
-
const
|
|
163
|
-
const marker =
|
|
282
|
+
const paths = options.paths ?? {};
|
|
283
|
+
const marker = readCentralMcpMarker(paths);
|
|
164
284
|
if (marker) {
|
|
165
|
-
const ping = await
|
|
285
|
+
const ping = await centralMcpPing(marker.endpoint);
|
|
166
286
|
if (ping.kind === "ambiguous") {
|
|
167
287
|
appendCrashLog("mcp-server", { event: "refusal", instanceId: marker.generation, reason: "ambiguous-liveness", detail: marker.endpoint });
|
|
168
288
|
throw new Error(`central MCP liveness at ${marker.endpoint} is ambiguous; refusing to start another server`);
|
|
@@ -185,8 +305,8 @@ export async function ensureCentralMcp(options = {}) {
|
|
|
185
305
|
stdio: ["ignore", logFd, logFd],
|
|
186
306
|
env: {
|
|
187
307
|
...process.env,
|
|
188
|
-
[
|
|
189
|
-
[
|
|
308
|
+
[H2A_MCP_CENTRAL_ENV]: "1",
|
|
309
|
+
[H2A_MCP_CENTRAL_ENDPOINT_ENV]: endpoint,
|
|
190
310
|
},
|
|
191
311
|
});
|
|
192
312
|
child.once("error", (error) => {
|
|
@@ -211,7 +331,7 @@ export async function ensureCentralMcp(options = {}) {
|
|
|
211
331
|
finally {
|
|
212
332
|
closeSync(logFd);
|
|
213
333
|
}
|
|
214
|
-
return waitForCentralStart(
|
|
334
|
+
return waitForCentralStart(paths, endpoint, () => spawnError);
|
|
215
335
|
}
|
|
216
336
|
function profileMcpConfig(profile, cwd) {
|
|
217
337
|
switch (profile) {
|
|
@@ -235,33 +355,9 @@ function profileMcpConfig(profile, cwd) {
|
|
|
235
355
|
return undefined;
|
|
236
356
|
}
|
|
237
357
|
}
|
|
238
|
-
function
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
repair: false,
|
|
242
|
-
dryRun: false,
|
|
243
|
-
sessionFreshnessGuarantee: "runtime central MCP writer",
|
|
244
|
-
nativeCommandFailureLimit: "runtime central MCP writer",
|
|
245
|
-
version: "runtime-central-mcp",
|
|
246
|
-
hosts: [{
|
|
247
|
-
host,
|
|
248
|
-
ok: true,
|
|
249
|
-
findings: [],
|
|
250
|
-
diagnostics: [],
|
|
251
|
-
changed: [],
|
|
252
|
-
preserved: [],
|
|
253
|
-
failures: [],
|
|
254
|
-
unverifiable: [],
|
|
255
|
-
unrepaired: [],
|
|
256
|
-
coherencePaths: [],
|
|
257
|
-
plannedActions: [],
|
|
258
|
-
repairMarkerPath: "",
|
|
259
|
-
}],
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
function activateCentralMcpEnvironment(core, endpoint) {
|
|
263
|
-
process.env[core.H2A_MCP_CENTRAL_ENV] = "1";
|
|
264
|
-
process.env[core.H2A_MCP_CENTRAL_ENDPOINT_ENV] = endpoint;
|
|
358
|
+
function activateCentralMcpEnvironment(endpoint) {
|
|
359
|
+
process.env[H2A_MCP_CENTRAL_ENV] = "1";
|
|
360
|
+
process.env[H2A_MCP_CENTRAL_ENDPOINT_ENV] = endpoint;
|
|
265
361
|
}
|
|
266
362
|
function deactivateCentralMcpEnvironment() {
|
|
267
363
|
delete process.env.H2A_MCP_CENTRAL;
|
|
@@ -284,25 +380,46 @@ function restoreHostConfig(path, snapshot) {
|
|
|
284
380
|
function errorMessage(error) {
|
|
285
381
|
return error instanceof Error ? error.message : String(error);
|
|
286
382
|
}
|
|
383
|
+
/**
|
|
384
|
+
* Render the host config through the core CLI over the `h2a` binary (never an
|
|
385
|
+
* in-process core import). Output stays captured (quiet launch path); a
|
|
386
|
+
* nonzero exit degrades to the per-session sidecar exactly like before. The
|
|
387
|
+
* real installation doctor runs here instead of the former always-ok stub, so
|
|
388
|
+
* a genuinely incoherent host setup now surfaces as degraded-with-reason
|
|
389
|
+
* rather than a masked central — still fail-closed, never a crash.
|
|
390
|
+
*/
|
|
391
|
+
function writeHostConfig(target, cwd) {
|
|
392
|
+
let status = null;
|
|
393
|
+
let spawnError;
|
|
394
|
+
try {
|
|
395
|
+
const result = spawnSync("h2a", ["host", "setup", "--host", target.host, "--write", target.path], { cwd, stdio: "pipe", encoding: "utf8", timeout: CENTRAL_HOST_SETUP_TIMEOUT_MS });
|
|
396
|
+
status = result.status;
|
|
397
|
+
spawnError = result.error;
|
|
398
|
+
}
|
|
399
|
+
catch (error) {
|
|
400
|
+
spawnError = error instanceof Error ? error : new Error(String(error));
|
|
401
|
+
}
|
|
402
|
+
if (status !== 0) {
|
|
403
|
+
throw new Error(`central MCP could not write the ${target.host} host config at ${target.path}` +
|
|
404
|
+
(spawnError ? ` (${spawnError.message})` : ""));
|
|
405
|
+
}
|
|
406
|
+
}
|
|
287
407
|
/** Ensure central MCP and rewrite the launched host's existing MCP config path. */
|
|
288
408
|
export async function prepareCentralMcpForLaunch(options) {
|
|
289
409
|
let hostConfig;
|
|
290
410
|
try {
|
|
291
|
-
const central = await ensureCentralMcp({
|
|
411
|
+
const central = await ensureCentralMcp({
|
|
412
|
+
...(options.root ? { root: options.root } : {}),
|
|
413
|
+
...(options.paths ? { paths: options.paths } : {}),
|
|
414
|
+
});
|
|
292
415
|
if (!central)
|
|
293
416
|
return undefined;
|
|
294
|
-
const core = await loadCoreCentralMcp();
|
|
295
417
|
const target = profileMcpConfig(options.profile, options.cwd);
|
|
296
418
|
if (target)
|
|
297
419
|
hostConfig = { path: target.path, snapshot: captureHostConfig(target.path) };
|
|
298
|
-
activateCentralMcpEnvironment(
|
|
299
|
-
if (target)
|
|
300
|
-
|
|
301
|
-
const code = core.runCli(["host", "setup", "--host", target.host, "--write", target.path], quiet, { doctorHostInstallations: () => coherentHostSetupReport(target.host) });
|
|
302
|
-
if (code !== 0) {
|
|
303
|
-
throw new Error(`central MCP could not write the ${target.host} host config at ${target.path}`);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
420
|
+
activateCentralMcpEnvironment(central.endpoint);
|
|
421
|
+
if (target)
|
|
422
|
+
writeHostConfig(target, options.cwd);
|
|
306
423
|
return { status: "central", ...central };
|
|
307
424
|
}
|
|
308
425
|
catch (error) {
|
|
@@ -325,7 +442,7 @@ export async function prepareCentralMcpForRestore(options = {}) {
|
|
|
325
442
|
const central = await ensureCentralMcp(options);
|
|
326
443
|
if (!central)
|
|
327
444
|
return undefined;
|
|
328
|
-
activateCentralMcpEnvironment(
|
|
445
|
+
activateCentralMcpEnvironment(central.endpoint);
|
|
329
446
|
return { status: "central", ...central };
|
|
330
447
|
}
|
|
331
448
|
catch (error) {
|