@fusionkit/cli 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/dist/cli.d.ts +8 -0
- package/dist/cli.js +34 -0
- package/dist/commands/ensemble-gateway.d.ts +2 -0
- package/dist/commands/ensemble-gateway.js +114 -0
- package/dist/commands/ensemble-records.d.ts +33 -0
- package/dist/commands/ensemble-records.js +207 -0
- package/dist/commands/ensemble.d.ts +2 -0
- package/dist/commands/ensemble.js +254 -0
- package/dist/commands/fusion.d.ts +2 -0
- package/dist/commands/fusion.js +112 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +24 -0
- package/dist/commands/lifecycle.d.ts +2 -0
- package/dist/commands/lifecycle.js +124 -0
- package/dist/commands/local.d.ts +2 -0
- package/dist/commands/local.js +25 -0
- package/dist/commands/plane.d.ts +2 -0
- package/dist/commands/plane.js +30 -0
- package/dist/commands/run.d.ts +2 -0
- package/dist/commands/run.js +149 -0
- package/dist/commands/runner.d.ts +2 -0
- package/dist/commands/runner.js +33 -0
- package/dist/commands/secrets.d.ts +2 -0
- package/dist/commands/secrets.js +21 -0
- package/dist/config.d.ts +30 -0
- package/dist/config.js +69 -0
- package/dist/fusion-quickstart.d.ts +182 -0
- package/dist/fusion-quickstart.js +673 -0
- package/dist/gateway.d.ts +63 -0
- package/dist/gateway.js +304 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +28 -0
- package/dist/local.d.ts +40 -0
- package/dist/local.js +144 -0
- package/dist/render.d.ts +7 -0
- package/dist/render.js +131 -0
- package/dist/shared/errors.d.ts +6 -0
- package/dist/shared/errors.js +9 -0
- package/dist/shared/options.d.ts +24 -0
- package/dist/shared/options.js +106 -0
- package/dist/shared/plane.d.ts +13 -0
- package/dist/shared/plane.js +46 -0
- package/dist/shared/preflight.d.ts +15 -0
- package/dist/shared/preflight.js +48 -0
- package/dist/shared/proc.d.ts +41 -0
- package/dist/shared/proc.js +122 -0
- package/dist/test/cli.test.d.ts +1 -0
- package/dist/test/cli.test.js +867 -0
- package/dist/test/e2e.test.d.ts +1 -0
- package/dist/test/e2e.test.js +250 -0
- package/dist/test/fusion-quickstart.test.d.ts +1 -0
- package/dist/test/fusion-quickstart.test.js +189 -0
- package/dist/test/gateway-e2e.test.d.ts +1 -0
- package/dist/test/gateway-e2e.test.js +606 -0
- package/dist/test/handoff.test.d.ts +1 -0
- package/dist/test/handoff.test.js +212 -0
- package/dist/test/local.test.d.ts +1 -0
- package/dist/test/local.test.js +39 -0
- package/dist/test/proc.test.d.ts +1 -0
- package/dist/test/proc.test.js +22 -0
- package/package.json +48 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fusion Harness Gateway CLI wiring. Builds the front-door runner that turns a
|
|
3
|
+
* single tool prompt into a unified HandoffKit/FusionKit harness ensemble run,
|
|
4
|
+
* and exposes it over the provider wire protocols (Codex Responses, Claude
|
|
5
|
+
* Messages, Cursorkit chat), the generic ACP local agent, and the unified
|
|
6
|
+
* front-door acceptance suite.
|
|
7
|
+
*/
|
|
8
|
+
import type { EnsembleModel, UnifiedHarnessKind } from "@fusionkit/ensemble";
|
|
9
|
+
import type { AcpRunner, FrontDoorRunner, FusionGateway, Gateway } from "@fusionkit/model-gateway";
|
|
10
|
+
export type GatewayRunnerConfig = {
|
|
11
|
+
fusionBackendUrl: string;
|
|
12
|
+
repo: string;
|
|
13
|
+
outputRoot: string;
|
|
14
|
+
harnesses: UnifiedHarnessKind[];
|
|
15
|
+
models: EnsembleModel[];
|
|
16
|
+
command?: string;
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
judgeModel?: string;
|
|
19
|
+
cursorKitDir?: string;
|
|
20
|
+
fusionApiKey?: string;
|
|
21
|
+
modelEndpoints?: Record<string, string>;
|
|
22
|
+
};
|
|
23
|
+
export declare function buildFrontDoorRunner(config: GatewayRunnerConfig): FrontDoorRunner;
|
|
24
|
+
export declare function buildAcpRunner(config: GatewayRunnerConfig): AcpRunner;
|
|
25
|
+
export declare function codexConfigSnippet(gatewayUrl: string): string;
|
|
26
|
+
export declare function gatewaySetupSnippets(gatewayUrl: string, cursorKitNote: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* The judge-streamed-trajectory front door: the panel runs once per session to
|
|
29
|
+
* produce candidate trajectories, then the judge acts as a streaming tool-calling
|
|
30
|
+
* agent (FusionKit `trajectory:step`) whose trajectory the user's harness executes.
|
|
31
|
+
* Built on the dialect-aware `startGateway` + a {@link FusionBackend}; iteration is
|
|
32
|
+
* the harness's job (no verify/repair here).
|
|
33
|
+
*/
|
|
34
|
+
export declare function startFusionStepGateway(input: {
|
|
35
|
+
config: GatewayRunnerConfig;
|
|
36
|
+
host: string;
|
|
37
|
+
port: number;
|
|
38
|
+
authToken?: string;
|
|
39
|
+
defaultModel?: string;
|
|
40
|
+
}): Promise<Gateway>;
|
|
41
|
+
export declare function startConfiguredGateway(input: {
|
|
42
|
+
config: GatewayRunnerConfig;
|
|
43
|
+
host: string;
|
|
44
|
+
port: number;
|
|
45
|
+
authToken?: string;
|
|
46
|
+
defaultModel?: string;
|
|
47
|
+
}): Promise<FusionGateway>;
|
|
48
|
+
export declare function runGatewayAcp(config: GatewayRunnerConfig): Promise<void>;
|
|
49
|
+
export type GatewayAcceptanceInput = {
|
|
50
|
+
config: GatewayRunnerConfig;
|
|
51
|
+
sentinel: string;
|
|
52
|
+
host: string;
|
|
53
|
+
outPath: string;
|
|
54
|
+
cursorKitUrl?: string;
|
|
55
|
+
};
|
|
56
|
+
export declare function runGatewayAcceptance(input: GatewayAcceptanceInput): Promise<{
|
|
57
|
+
reportPath: string;
|
|
58
|
+
failed: boolean;
|
|
59
|
+
}>;
|
|
60
|
+
export declare function installRegistryAdapters(input: {
|
|
61
|
+
agentIds: string[];
|
|
62
|
+
installDir: string;
|
|
63
|
+
}): Promise<string[]>;
|
package/dist/gateway.js
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fusion Harness Gateway CLI wiring. Builds the front-door runner that turns a
|
|
3
|
+
* single tool prompt into a unified HandoffKit/FusionKit harness ensemble run,
|
|
4
|
+
* and exposes it over the provider wire protocols (Codex Responses, Claude
|
|
5
|
+
* Messages, Cursorkit chat), the generic ACP local agent, and the unified
|
|
6
|
+
* front-door acceptance suite.
|
|
7
|
+
*/
|
|
8
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { join, resolve } from "node:path";
|
|
10
|
+
import { runFusionPanels, runUnifiedHarnessE2E } from "@fusionkit/ensemble";
|
|
11
|
+
import { emitTrace, newSpanId, newTraceId } from "@fusionkit/protocol";
|
|
12
|
+
import { FusionBackend, installAcpAdapters, runAcpAgent, runFrontDoorAcceptance, startFusionGateway, startGateway } from "@fusionkit/model-gateway";
|
|
13
|
+
function mapStatus(status) {
|
|
14
|
+
if (status === "succeeded")
|
|
15
|
+
return "succeeded";
|
|
16
|
+
if (status === "skipped")
|
|
17
|
+
return "skipped";
|
|
18
|
+
return "failed";
|
|
19
|
+
}
|
|
20
|
+
function summarize(report, primary) {
|
|
21
|
+
const row = report.results.find((entry) => entry.harness === primary) ?? report.results[0];
|
|
22
|
+
const ensemble = row?.ensemble;
|
|
23
|
+
const finalOutput = ensemble?.judgeSynthesisRecord?.final_output ??
|
|
24
|
+
ensemble?.harnessRunResult.output_summary ??
|
|
25
|
+
row?.message ??
|
|
26
|
+
"";
|
|
27
|
+
const evidence = [];
|
|
28
|
+
if (ensemble !== undefined) {
|
|
29
|
+
if (ensemble.artifacts.some((artifact) => artifact.kind === "patch")) {
|
|
30
|
+
evidence.push("patch_artifact");
|
|
31
|
+
}
|
|
32
|
+
if (ensemble.toolRecords.length > 0)
|
|
33
|
+
evidence.push("tool_execution");
|
|
34
|
+
if (ensemble.judgeSynthesisRecord !== undefined)
|
|
35
|
+
evidence.push("judge_synthesis");
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
finalOutput,
|
|
39
|
+
runId: report.id,
|
|
40
|
+
status: mapStatus(row?.status ?? "failed"),
|
|
41
|
+
evidence,
|
|
42
|
+
...(report.reportPath !== undefined ? { reportPath: report.reportPath } : {})
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export function buildFrontDoorRunner(config) {
|
|
46
|
+
return async (input) => {
|
|
47
|
+
const traceId = input.traceId;
|
|
48
|
+
const sessionSpan = newSpanId();
|
|
49
|
+
emitTrace({
|
|
50
|
+
component: "gateway",
|
|
51
|
+
event_type: "session.started",
|
|
52
|
+
traceId,
|
|
53
|
+
spanId: sessionSpan,
|
|
54
|
+
payload: {
|
|
55
|
+
request_id: input.requestId,
|
|
56
|
+
dialect: input.dialect,
|
|
57
|
+
prompt_preview: input.prompt.slice(0, 600),
|
|
58
|
+
environment: {
|
|
59
|
+
repo: config.repo,
|
|
60
|
+
fusion_backend_url: config.fusionBackendUrl,
|
|
61
|
+
harnesses: config.harnesses,
|
|
62
|
+
judge_model: config.judgeModel ?? null,
|
|
63
|
+
models: config.models.map((model) => ({
|
|
64
|
+
id: model.id,
|
|
65
|
+
model: model.model,
|
|
66
|
+
...(model.endpointId !== undefined ? { endpoint_id: model.endpointId } : {})
|
|
67
|
+
})),
|
|
68
|
+
...(config.modelEndpoints !== undefined ? { model_endpoints: config.modelEndpoints } : {})
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
try {
|
|
73
|
+
const report = await runUnifiedHarnessE2E({
|
|
74
|
+
id: `gateway_${input.requestId}`,
|
|
75
|
+
fusionBackendUrl: config.fusionBackendUrl,
|
|
76
|
+
repo: config.repo,
|
|
77
|
+
outputRoot: join(config.outputRoot, input.requestId),
|
|
78
|
+
prompt: input.prompt,
|
|
79
|
+
harnesses: config.harnesses,
|
|
80
|
+
models: config.models,
|
|
81
|
+
traceId,
|
|
82
|
+
...(config.command !== undefined ? { command: config.command } : {}),
|
|
83
|
+
...(config.timeoutMs !== undefined ? { timeoutMs: config.timeoutMs } : {}),
|
|
84
|
+
...(config.judgeModel !== undefined ? { judgeModel: config.judgeModel } : {}),
|
|
85
|
+
...(config.cursorKitDir !== undefined ? { cursorKitDir: config.cursorKitDir } : {}),
|
|
86
|
+
...(config.fusionApiKey !== undefined ? { fusionApiKey: config.fusionApiKey } : {}),
|
|
87
|
+
...(config.modelEndpoints !== undefined ? { modelEndpoints: config.modelEndpoints } : {})
|
|
88
|
+
});
|
|
89
|
+
const summary = summarize(report, config.harnesses[0] ?? "command");
|
|
90
|
+
emitTrace({
|
|
91
|
+
component: "gateway",
|
|
92
|
+
event_type: "session.finished",
|
|
93
|
+
traceId,
|
|
94
|
+
spanId: sessionSpan,
|
|
95
|
+
payload: {
|
|
96
|
+
status: summary.status,
|
|
97
|
+
run_id: summary.runId,
|
|
98
|
+
evidence: summary.evidence,
|
|
99
|
+
final_output_preview: summary.finalOutput.slice(0, 600),
|
|
100
|
+
...(summary.reportPath !== undefined ? { report_path: summary.reportPath } : {})
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
return summary;
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
emitTrace({
|
|
107
|
+
component: "gateway",
|
|
108
|
+
event_type: "session.finished",
|
|
109
|
+
traceId,
|
|
110
|
+
spanId: sessionSpan,
|
|
111
|
+
payload: { status: "failed", error: error instanceof Error ? error.message : String(error) }
|
|
112
|
+
});
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
export function buildAcpRunner(config) {
|
|
118
|
+
const front = buildFrontDoorRunner(config);
|
|
119
|
+
return async (input) => {
|
|
120
|
+
const result = await front({
|
|
121
|
+
dialect: "openai-chat",
|
|
122
|
+
prompt: input.prompt,
|
|
123
|
+
requestedModel: undefined,
|
|
124
|
+
requestId: input.requestId,
|
|
125
|
+
traceId: newTraceId()
|
|
126
|
+
});
|
|
127
|
+
return {
|
|
128
|
+
finalOutput: result.finalOutput,
|
|
129
|
+
runId: result.runId,
|
|
130
|
+
status: result.status,
|
|
131
|
+
evidence: result.evidence
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
export function codexConfigSnippet(gatewayUrl) {
|
|
136
|
+
const base = gatewayUrl.replace(/\/+$/, "");
|
|
137
|
+
return [
|
|
138
|
+
"# ~/.codex/config.toml (or a temporary CODEX_HOME)",
|
|
139
|
+
`model = "fusion-panel"`,
|
|
140
|
+
`model_provider = "fusion-gateway"`,
|
|
141
|
+
"",
|
|
142
|
+
"[model_providers.fusion-gateway]",
|
|
143
|
+
`name = "Fusion Harness Gateway"`,
|
|
144
|
+
`base_url = "${base}/v1"`,
|
|
145
|
+
`wire_api = "responses"`,
|
|
146
|
+
`requires_openai_auth = false`
|
|
147
|
+
].join("\n");
|
|
148
|
+
}
|
|
149
|
+
export function gatewaySetupSnippets(gatewayUrl, cursorKitNote) {
|
|
150
|
+
const base = gatewayUrl.replace(/\/+$/, "");
|
|
151
|
+
return [
|
|
152
|
+
"Front-door setup:",
|
|
153
|
+
"",
|
|
154
|
+
"Codex (OpenAI Responses):",
|
|
155
|
+
codexConfigSnippet(gatewayUrl),
|
|
156
|
+
"",
|
|
157
|
+
"Claude Code (Anthropic Messages); Claude appends /v1/messages, so use the gateway root:",
|
|
158
|
+
` ANTHROPIC_BASE_URL=${base}`,
|
|
159
|
+
` ANTHROPIC_AUTH_TOKEN=local`,
|
|
160
|
+
"",
|
|
161
|
+
"Cursor (via Cursorkit backend):",
|
|
162
|
+
` cursor-agent --endpoint ${cursorKitNote} --model fusion-panel`,
|
|
163
|
+
` Cursorkit model backend: ${base}/v1/chat/completions`,
|
|
164
|
+
"",
|
|
165
|
+
"Generic ACP local agent:",
|
|
166
|
+
" fusionkit ensemble gateway acp --fusion-backend <fusion-backend>",
|
|
167
|
+
"",
|
|
168
|
+
"ACP registry adapters:",
|
|
169
|
+
" fusionkit ensemble gateway acp-registry install codex-cli claude-agent"
|
|
170
|
+
].join("\n");
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Validate the loosely-typed panel output before it crosses the wire to the
|
|
174
|
+
* synthesizer: keep only entries with the required string fields and drop the
|
|
175
|
+
* rest (with a warning) rather than forwarding malformed trajectories.
|
|
176
|
+
*/
|
|
177
|
+
function normalizeWireTrajectories(raw) {
|
|
178
|
+
const out = [];
|
|
179
|
+
for (const entry of raw) {
|
|
180
|
+
if (typeof entry.trajectory_id === "string" &&
|
|
181
|
+
typeof entry.model_id === "string" &&
|
|
182
|
+
typeof entry.status === "string" &&
|
|
183
|
+
typeof entry.final_output === "string") {
|
|
184
|
+
out.push(entry);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
console.error(`fusion: dropping malformed panel trajectory: ${JSON.stringify(entry).slice(0, 200)}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* The judge-streamed-trajectory front door: the panel runs once per session to
|
|
194
|
+
* produce candidate trajectories, then the judge acts as a streaming tool-calling
|
|
195
|
+
* agent (FusionKit `trajectory:step`) whose trajectory the user's harness executes.
|
|
196
|
+
* Built on the dialect-aware `startGateway` + a {@link FusionBackend}; iteration is
|
|
197
|
+
* the harness's job (no verify/repair here).
|
|
198
|
+
*/
|
|
199
|
+
export async function startFusionStepGateway(input) {
|
|
200
|
+
const { config } = input;
|
|
201
|
+
const base = config.fusionBackendUrl.replace(/\/+$/, "");
|
|
202
|
+
const stepUrl = `${base}/v1/fusion/trajectory:step`;
|
|
203
|
+
const defaultModel = input.defaultModel ?? "fusion-panel";
|
|
204
|
+
const runPanels = async ({ task, traceId, sessionSpanId, sessionKey, turn }) => {
|
|
205
|
+
emitTrace({
|
|
206
|
+
component: "gateway",
|
|
207
|
+
event_type: "session.started",
|
|
208
|
+
traceId,
|
|
209
|
+
spanId: sessionSpanId,
|
|
210
|
+
payload: {
|
|
211
|
+
dialect: "fusion-step",
|
|
212
|
+
prompt_preview: task.slice(0, 600),
|
|
213
|
+
environment: {
|
|
214
|
+
repo: config.repo,
|
|
215
|
+
fusion_backend_url: config.fusionBackendUrl,
|
|
216
|
+
harnesses: ["agent"],
|
|
217
|
+
judge_model: config.judgeModel ?? null,
|
|
218
|
+
models: config.models.map((model) => ({
|
|
219
|
+
id: model.id,
|
|
220
|
+
model: model.model,
|
|
221
|
+
...(model.endpointId !== undefined ? { endpoint_id: model.endpointId } : {})
|
|
222
|
+
})),
|
|
223
|
+
...(config.modelEndpoints !== undefined ? { model_endpoints: config.modelEndpoints } : {})
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
console.error(`fusion: running panel (${config.models.map((m) => m.id).join(", ")}) for session ${sessionKey}...`);
|
|
228
|
+
try {
|
|
229
|
+
const wire = await runFusionPanels({
|
|
230
|
+
id: `panels_${sessionKey}_t${turn}`,
|
|
231
|
+
repo: config.repo,
|
|
232
|
+
outputRoot: join(config.outputRoot, sessionKey, `t${turn}`),
|
|
233
|
+
prompt: task,
|
|
234
|
+
models: config.models,
|
|
235
|
+
fusionBackendUrl: config.fusionBackendUrl,
|
|
236
|
+
traceId,
|
|
237
|
+
parentSpanId: sessionSpanId,
|
|
238
|
+
turn,
|
|
239
|
+
...(config.modelEndpoints !== undefined ? { modelEndpoints: config.modelEndpoints } : {}),
|
|
240
|
+
...(config.fusionApiKey !== undefined ? { fusionApiKey: config.fusionApiKey } : {}),
|
|
241
|
+
...(config.timeoutMs !== undefined ? { timeoutMs: config.timeoutMs } : {})
|
|
242
|
+
});
|
|
243
|
+
const trajectories = normalizeWireTrajectories(wire);
|
|
244
|
+
console.error(`fusion: panel produced ${trajectories.length} candidate trajectories ` +
|
|
245
|
+
`(${trajectories.map((t) => `${t.model_id}:${t.status}`).join(", ")})`);
|
|
246
|
+
return trajectories;
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
console.error(`fusion: panel run failed: ${error instanceof Error ? error.stack : String(error)}`);
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
const backend = new FusionBackend({
|
|
254
|
+
stepUrl,
|
|
255
|
+
runPanels,
|
|
256
|
+
defaultModel
|
|
257
|
+
});
|
|
258
|
+
return await startGateway({
|
|
259
|
+
backend,
|
|
260
|
+
host: input.host,
|
|
261
|
+
port: input.port,
|
|
262
|
+
...(input.authToken !== undefined ? { authToken: input.authToken } : {})
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
export async function startConfiguredGateway(input) {
|
|
266
|
+
return await startFusionGateway({
|
|
267
|
+
runner: buildFrontDoorRunner(input.config),
|
|
268
|
+
host: input.host,
|
|
269
|
+
port: input.port,
|
|
270
|
+
...(input.authToken !== undefined ? { authToken: input.authToken } : {}),
|
|
271
|
+
...(input.defaultModel !== undefined ? { defaultModel: input.defaultModel } : {})
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
export async function runGatewayAcp(config) {
|
|
275
|
+
await runAcpAgent({ runner: buildAcpRunner(config) });
|
|
276
|
+
}
|
|
277
|
+
export async function runGatewayAcceptance(input) {
|
|
278
|
+
const gateway = await startConfiguredGateway({
|
|
279
|
+
config: input.config,
|
|
280
|
+
host: input.host,
|
|
281
|
+
port: 0
|
|
282
|
+
});
|
|
283
|
+
try {
|
|
284
|
+
const report = await runFrontDoorAcceptance({
|
|
285
|
+
gatewayUrl: gateway.url(),
|
|
286
|
+
sentinel: input.sentinel,
|
|
287
|
+
acpRunner: buildAcpRunner(input.config)
|
|
288
|
+
});
|
|
289
|
+
mkdirSync(resolve(input.outPath, ".."), { recursive: true });
|
|
290
|
+
writeFileSync(input.outPath, JSON.stringify(report, null, 2) + "\n");
|
|
291
|
+
const failed = report.front_doors.some((door) => door.status === "failed");
|
|
292
|
+
return { reportPath: input.outPath, failed };
|
|
293
|
+
}
|
|
294
|
+
finally {
|
|
295
|
+
await gateway.close();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
export async function installRegistryAdapters(input) {
|
|
299
|
+
const installed = await installAcpAdapters({
|
|
300
|
+
agentIds: input.agentIds,
|
|
301
|
+
installDir: input.installDir
|
|
302
|
+
});
|
|
303
|
+
return installed.map((adapter) => `${adapter.id}@${adapter.version} -> ${adapter.metadataPath}`);
|
|
304
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { PolicyDeniedError } from "@fusionkit/protocol";
|
|
3
|
+
import { buildProgram } from "./cli.js";
|
|
4
|
+
import { PreflightError } from "./shared/preflight.js";
|
|
5
|
+
async function main() {
|
|
6
|
+
const program = buildProgram();
|
|
7
|
+
// Bare invocation prints help on stdout and exits 0 (commander would
|
|
8
|
+
// otherwise print to stderr and exit non-zero).
|
|
9
|
+
if (process.argv.slice(2).length === 0) {
|
|
10
|
+
program.outputHelp();
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
await program.parseAsync(process.argv);
|
|
14
|
+
}
|
|
15
|
+
main().catch((error) => {
|
|
16
|
+
if (error instanceof PolicyDeniedError) {
|
|
17
|
+
console.error(`POLICY DENIED (fail closed):`);
|
|
18
|
+
for (const reason of error.reasons)
|
|
19
|
+
console.error(` - ${reason}`);
|
|
20
|
+
process.exit(2);
|
|
21
|
+
}
|
|
22
|
+
if (error instanceof PreflightError) {
|
|
23
|
+
console.error(error.message);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
console.error(`error: ${error instanceof Error ? error.message : String(error)}`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
});
|
package/dist/local.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { BackendConfig } from "@fusionkit/model-gateway";
|
|
2
|
+
/**
|
|
3
|
+
* `warrant local <tool>` — back a vendor agent harness with a locally running
|
|
4
|
+
* model, with no change to how the tool is invoked. Each launcher ensures the
|
|
5
|
+
* model gateway is up, applies the tool's native configuration shim
|
|
6
|
+
* (environment, config file, or — for Cursor — IDE settings + a public
|
|
7
|
+
* tunnel), then execs the real binary with the user's own arguments.
|
|
8
|
+
*
|
|
9
|
+
* The shim builders below are pure so they can be unit-tested; the dispatcher
|
|
10
|
+
* (`runLocal`) wires them to a started gateway and the real child process.
|
|
11
|
+
*/
|
|
12
|
+
export type LocalTool = "claude" | "codex" | "opencode" | "cursor" | "serve";
|
|
13
|
+
export declare const LOCAL_TOOLS: readonly LocalTool[];
|
|
14
|
+
/** Environment for Claude Code: point it at the gateway's Anthropic surface. */
|
|
15
|
+
export declare function claudeEnv(gatewayUrl: string, authToken?: string): Record<string, string>;
|
|
16
|
+
/**
|
|
17
|
+
* Codex config.toml fragment defining the gateway as a Responses provider.
|
|
18
|
+
* Written into an ephemeral CODEX_HOME so the user's own config is untouched.
|
|
19
|
+
*/
|
|
20
|
+
export declare function codexConfigToml(gatewayUrl: string, model: string): string;
|
|
21
|
+
/** opencode config registering the gateway as an OpenAI-compatible provider. */
|
|
22
|
+
export declare function opencodeConfig(gatewayUrl: string, model: string): Record<string, unknown>;
|
|
23
|
+
/** The opencode `--model provider/model` argument for the gateway provider. */
|
|
24
|
+
export declare function opencodeModelArg(model: string): string;
|
|
25
|
+
/** Human-facing setup for Cursor (IDE plan/chat panel only; needs a public URL). */
|
|
26
|
+
export declare function cursorInstructions(publicUrl: string, model: string): string;
|
|
27
|
+
export type RunLocalOptions = {
|
|
28
|
+
/** Public URL for Cursor's tunnel (or WARRANT_PUBLIC_URL). */
|
|
29
|
+
publicUrl?: string;
|
|
30
|
+
/** Bearer token to require on the gateway. */
|
|
31
|
+
authToken?: string;
|
|
32
|
+
/** Override the resolved backend (tests). */
|
|
33
|
+
config?: BackendConfig;
|
|
34
|
+
log?: (line: string) => void;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Start the gateway, apply the tool's shim, and exec the real binary. Returns
|
|
38
|
+
* the child's exit code. `serve` runs the gateway in the foreground.
|
|
39
|
+
*/
|
|
40
|
+
export declare function runLocal(tool: LocalTool, toolArgs: string[], options?: RunLocalOptions): Promise<number>;
|
package/dist/local.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { createBackend, resolveBackendConfig, startGateway } from "@fusionkit/model-gateway";
|
|
5
|
+
import { spawnTool } from "./shared/proc.js";
|
|
6
|
+
export const LOCAL_TOOLS = ["claude", "codex", "opencode", "cursor", "serve"];
|
|
7
|
+
/** The label a tool uses for the local model in its own UI. */
|
|
8
|
+
const LOCAL_MODEL_LABEL = "warrant-local";
|
|
9
|
+
function backendModel(config) {
|
|
10
|
+
return config.kind === "mlx" ? config.model : config.defaultModel ?? LOCAL_MODEL_LABEL;
|
|
11
|
+
}
|
|
12
|
+
// ---- pure shim builders (unit-tested) ----
|
|
13
|
+
/** Environment for Claude Code: point it at the gateway's Anthropic surface. */
|
|
14
|
+
export function claudeEnv(gatewayUrl, authToken) {
|
|
15
|
+
return {
|
|
16
|
+
ANTHROPIC_BASE_URL: gatewayUrl,
|
|
17
|
+
ANTHROPIC_AUTH_TOKEN: authToken ?? "warrant-local",
|
|
18
|
+
// Surface the local model in the /model picker (Anthropic discovery).
|
|
19
|
+
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: "1"
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Codex config.toml fragment defining the gateway as a Responses provider.
|
|
24
|
+
* Written into an ephemeral CODEX_HOME so the user's own config is untouched.
|
|
25
|
+
*/
|
|
26
|
+
export function codexConfigToml(gatewayUrl, model) {
|
|
27
|
+
return [
|
|
28
|
+
`model = "${model}"`,
|
|
29
|
+
`model_provider = "${LOCAL_MODEL_LABEL}"`,
|
|
30
|
+
"",
|
|
31
|
+
`[model_providers.${LOCAL_MODEL_LABEL}]`,
|
|
32
|
+
`name = "Warrant local"`,
|
|
33
|
+
`base_url = "${gatewayUrl}/v1"`,
|
|
34
|
+
`wire_api = "responses"`,
|
|
35
|
+
`requires_openai_auth = false`,
|
|
36
|
+
""
|
|
37
|
+
].join("\n");
|
|
38
|
+
}
|
|
39
|
+
/** opencode config registering the gateway as an OpenAI-compatible provider. */
|
|
40
|
+
export function opencodeConfig(gatewayUrl, model) {
|
|
41
|
+
return {
|
|
42
|
+
$schema: "https://opencode.ai/config.json",
|
|
43
|
+
provider: {
|
|
44
|
+
[LOCAL_MODEL_LABEL]: {
|
|
45
|
+
npm: "@ai-sdk/openai-compatible",
|
|
46
|
+
name: "Warrant local",
|
|
47
|
+
options: { baseURL: `${gatewayUrl}/v1` },
|
|
48
|
+
models: { [model]: { name: model } }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** The opencode `--model provider/model` argument for the gateway provider. */
|
|
54
|
+
export function opencodeModelArg(model) {
|
|
55
|
+
return `${LOCAL_MODEL_LABEL}/${model}`;
|
|
56
|
+
}
|
|
57
|
+
/** Human-facing setup for Cursor (IDE plan/chat panel only; needs a public URL). */
|
|
58
|
+
export function cursorInstructions(publicUrl, model) {
|
|
59
|
+
return [
|
|
60
|
+
"Cursor backs only its plan/chat panel with a custom model, and cannot reach",
|
|
61
|
+
"localhost — so this uses a public tunnel. In Cursor: Settings -> Models ->",
|
|
62
|
+
"enable 'Override OpenAI Base URL', then set:",
|
|
63
|
+
"",
|
|
64
|
+
` Override OpenAI Base URL : ${publicUrl}/v1`,
|
|
65
|
+
` Model name : ${model}`,
|
|
66
|
+
` OpenAI API Key : warrant-local (any non-empty value)`,
|
|
67
|
+
"",
|
|
68
|
+
"Use the chat/plan panel (Cmd/Ctrl+L). Composer, inline edit, apply, and",
|
|
69
|
+
"autocomplete remain on Cursor's own backend and are not affected."
|
|
70
|
+
].join("\n");
|
|
71
|
+
}
|
|
72
|
+
async function startLocalGateway(config, authToken) {
|
|
73
|
+
const backend = createBackend(config);
|
|
74
|
+
const gateway = await startGateway({
|
|
75
|
+
backend,
|
|
76
|
+
...(authToken !== undefined ? { authToken } : {})
|
|
77
|
+
});
|
|
78
|
+
return { url: gateway.url(), close: () => gateway.close() };
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Start the gateway, apply the tool's shim, and exec the real binary. Returns
|
|
82
|
+
* the child's exit code. `serve` runs the gateway in the foreground.
|
|
83
|
+
*/
|
|
84
|
+
export async function runLocal(tool, toolArgs, options = {}) {
|
|
85
|
+
const log = options.log ?? ((line) => console.error(line));
|
|
86
|
+
const config = options.config ?? resolveBackendConfig();
|
|
87
|
+
const model = backendModel(config);
|
|
88
|
+
const gateway = await startLocalGateway(config, options.authToken);
|
|
89
|
+
log(`warrant local: gateway on ${gateway.url} (model: ${model})`);
|
|
90
|
+
try {
|
|
91
|
+
switch (tool) {
|
|
92
|
+
case "serve": {
|
|
93
|
+
log(`OpenAI: ${gateway.url}/v1`);
|
|
94
|
+
log(`Anthropic: ${gateway.url}/v1/messages`);
|
|
95
|
+
log(`Responses: ${gateway.url}/v1/responses`);
|
|
96
|
+
log("Press Ctrl+C to stop.");
|
|
97
|
+
await new Promise(() => {
|
|
98
|
+
/* run until interrupted */
|
|
99
|
+
});
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
case "claude":
|
|
103
|
+
return await spawnTool("claude", toolArgs, claudeEnv(gateway.url, options.authToken));
|
|
104
|
+
case "codex": {
|
|
105
|
+
const home = mkdtempSync(join(tmpdir(), "warrant-codex-"));
|
|
106
|
+
writeFileSync(join(home, "config.toml"), codexConfigToml(gateway.url, model));
|
|
107
|
+
return await spawnTool("codex", toolArgs, { CODEX_HOME: home });
|
|
108
|
+
}
|
|
109
|
+
case "opencode": {
|
|
110
|
+
const dir = mkdtempSync(join(tmpdir(), "warrant-opencode-"));
|
|
111
|
+
const configPath = join(dir, "opencode.json");
|
|
112
|
+
writeFileSync(configPath, JSON.stringify(opencodeConfig(gateway.url, model), null, 2));
|
|
113
|
+
const args = toolArgs.includes("--model") ? toolArgs : ["--model", opencodeModelArg(model), ...toolArgs];
|
|
114
|
+
return await spawnTool("opencode", args, { OPENCODE_CONFIG: configPath });
|
|
115
|
+
}
|
|
116
|
+
case "cursor": {
|
|
117
|
+
const publicUrl = options.publicUrl ?? process.env.WARRANT_PUBLIC_URL;
|
|
118
|
+
if (publicUrl === undefined || publicUrl.length === 0) {
|
|
119
|
+
log("");
|
|
120
|
+
log("Cursor needs a public URL (it cannot reach localhost). Start a tunnel to");
|
|
121
|
+
log(`${gateway.url} (e.g. 'cloudflared tunnel --url ${gateway.url}' or 'ngrok http`);
|
|
122
|
+
log(`${gateway.url.replace(/^https?:\/\//, "")}'), then re-run with --public-url <url>`);
|
|
123
|
+
log("or set WARRANT_PUBLIC_URL.");
|
|
124
|
+
return 1;
|
|
125
|
+
}
|
|
126
|
+
log("");
|
|
127
|
+
log(cursorInstructions(publicUrl, model));
|
|
128
|
+
log("");
|
|
129
|
+
log("Gateway is running; leave this process up while you use Cursor. Ctrl+C to stop.");
|
|
130
|
+
await new Promise(() => {
|
|
131
|
+
/* keep the gateway (and tunnel target) alive */
|
|
132
|
+
});
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
default: {
|
|
136
|
+
const unreachable = tool;
|
|
137
|
+
throw new Error(`unknown local tool: ${String(unreachable)}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
await gateway.close();
|
|
143
|
+
}
|
|
144
|
+
}
|
package/dist/render.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DisclosureReport, ReceiptBundle, RunSummary } from "@fusionkit/protocol";
|
|
2
|
+
import type { HandoffTraceEvent } from "@fusionkit/handoff";
|
|
3
|
+
/** One screen, five questions. This is the product. */
|
|
4
|
+
export declare function renderReceipt(bundle: ReceiptBundle): string;
|
|
5
|
+
export declare function renderDisclosure(report: DisclosureReport): string;
|
|
6
|
+
export declare function renderRunList(runs: RunSummary[]): string;
|
|
7
|
+
export declare function renderTrace(events: HandoffTraceEvent[]): string;
|