@fusionkit/cli 0.1.3 → 0.1.5
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 +2 -2
- package/dist/cli.js +3 -17
- package/dist/commands/fusion.js +28 -9
- package/dist/cursor-acp.d.ts +20 -0
- package/dist/cursor-acp.js +205 -0
- package/dist/fusion-config.d.ts +1 -0
- package/dist/fusion-config.js +5 -0
- package/dist/fusion-quickstart.d.ts +33 -34
- package/dist/fusion-quickstart.js +324 -278
- package/dist/gateway.js +13 -1
- package/dist/shared/portless.d.ts +97 -0
- package/dist/shared/portless.js +253 -0
- package/dist/test/cli.test.js +24 -139
- package/dist/test/portless.test.d.ts +1 -0
- package/dist/test/portless.test.js +65 -0
- package/package.json +12 -9
- package/scope/.next/BUILD_ID +1 -1
- package/scope/.next/app-build-manifest.json +14 -14
- package/scope/.next/app-path-routes-manifest.json +4 -4
- package/scope/.next/build-manifest.json +2 -2
- package/scope/.next/prerender-manifest.json +10 -10
- package/scope/.next/required-server-files.json +4 -0
- package/scope/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/scope/.next/server/app/_not-found.html +1 -1
- package/scope/.next/server/app/_not-found.rsc +1 -1
- package/scope/.next/server/app/api/environments/route_client-reference-manifest.js +1 -1
- package/scope/.next/server/app/api/ingest/route_client-reference-manifest.js +1 -1
- package/scope/.next/server/app/api/models/route_client-reference-manifest.js +1 -1
- package/scope/.next/server/app/api/replay/route_client-reference-manifest.js +1 -1
- package/scope/.next/server/app/api/sessions/[traceId]/route_client-reference-manifest.js +1 -1
- package/scope/.next/server/app/api/sessions/route_client-reference-manifest.js +1 -1
- package/scope/.next/server/app/api/stream/route_client-reference-manifest.js +1 -1
- package/scope/.next/server/app/environments/page_client-reference-manifest.js +1 -1
- package/scope/.next/server/app/environments.html +1 -1
- package/scope/.next/server/app/environments.rsc +1 -1
- package/scope/.next/server/app/index.html +1 -1
- package/scope/.next/server/app/index.rsc +1 -1
- package/scope/.next/server/app/models/page_client-reference-manifest.js +1 -1
- package/scope/.next/server/app/models.html +1 -1
- package/scope/.next/server/app/models.rsc +1 -1
- package/scope/.next/server/app/page_client-reference-manifest.js +1 -1
- package/scope/.next/server/app/sessions/[traceId]/page_client-reference-manifest.js +1 -1
- package/scope/.next/server/app-paths-manifest.json +4 -4
- package/scope/.next/server/functions-config-manifest.json +3 -3
- package/scope/.next/server/pages/404.html +1 -1
- package/scope/.next/server/pages/500.html +1 -1
- package/scope/.next/server/server-reference-manifest.json +1 -1
- package/scope/package.json +3 -1
- package/scope/server.js +1 -1
- package/dist/commands/init.d.ts +0 -2
- package/dist/commands/init.js +0 -24
- package/dist/commands/lifecycle.d.ts +0 -2
- package/dist/commands/lifecycle.js +0 -124
- package/dist/commands/plane.d.ts +0 -2
- package/dist/commands/plane.js +0 -30
- package/dist/commands/run.d.ts +0 -2
- package/dist/commands/run.js +0 -149
- package/dist/commands/runner.d.ts +0 -2
- package/dist/commands/runner.js +0 -33
- package/dist/commands/secrets.d.ts +0 -2
- package/dist/commands/secrets.js +0 -21
- /package/scope/.next/static/{8b1kwXDxrKvteRVkOC_Z2 → vxqImMqlOwssVTua5Facf}/_buildManifest.js +0 -0
- /package/scope/.next/static/{8b1kwXDxrKvteRVkOC_Z2 → vxqImMqlOwssVTua5Facf}/_ssgManifest.js +0 -0
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ cloud panel (skip with `--yes`). Use `--local` for the on-device MLX panel, or
|
|
|
47
47
|
Tired of long flag lines? Scaffold a committed `fusionkit.json`:
|
|
48
48
|
|
|
49
49
|
```bash
|
|
50
|
-
fusionkit
|
|
50
|
+
fusionkit init
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
It records the panel, judge, default tool, and run defaults so the whole team
|
|
@@ -60,7 +60,7 @@ config and a dry-run preview with `fusionkit status`.
|
|
|
60
60
|
- `fusionkit codex | claude | cursor` — launch that agent backed by the panel.
|
|
61
61
|
- `fusionkit serve` — just run the gateway and print setup snippets for any tool.
|
|
62
62
|
- `fusionkit fusion [tool]` — the generic launcher (interactive picker on a TTY).
|
|
63
|
-
- `fusionkit
|
|
63
|
+
- `fusionkit init` — scaffold `fusionkit.json` for this repo.
|
|
64
64
|
- `fusionkit doctor` — check prerequisites with fix hints.
|
|
65
65
|
- `fusionkit status` — show the effective config and what a run will do.
|
|
66
66
|
|
package/dist/cli.js
CHANGED
|
@@ -5,18 +5,11 @@ import { FUSIONKIT_PYPI_VERSION } from "./fusion-quickstart.js";
|
|
|
5
5
|
import { registerDoctor } from "./commands/doctor.js";
|
|
6
6
|
import { registerEnsemble } from "./commands/ensemble.js";
|
|
7
7
|
import { registerFusion } from "./commands/fusion.js";
|
|
8
|
-
import { registerInit } from "./commands/init.js";
|
|
9
|
-
import { registerLifecycle } from "./commands/lifecycle.js";
|
|
10
8
|
import { registerLocal } from "./commands/local.js";
|
|
11
|
-
import { registerPlane } from "./commands/plane.js";
|
|
12
|
-
import { registerRun } from "./commands/run.js";
|
|
13
|
-
import { registerRunner } from "./commands/runner.js";
|
|
14
|
-
import { registerSecrets } from "./commands/secrets.js";
|
|
15
9
|
/**
|
|
16
|
-
* Build the `fusionkit` command tree.
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* global home directory via `program.opts().dir`.
|
|
10
|
+
* Build the `fusionkit` command tree. `enablePositionalOptions` keeps the
|
|
11
|
+
* launcher commands' passthrough unambiguous (fusionkit's own flags must
|
|
12
|
+
* precede the tool name). Each `register*` helper attaches its command(s).
|
|
20
13
|
*/
|
|
21
14
|
function cliVersion() {
|
|
22
15
|
// dist/cli.js -> ../package.json is the published package manifest.
|
|
@@ -34,14 +27,7 @@ export function buildProgram() {
|
|
|
34
27
|
.name("fusionkit")
|
|
35
28
|
.description("real model fusion behind your coding agent (codex, claude, cursor)")
|
|
36
29
|
.version(`@fusionkit/cli ${cliVersion()} (synthesizer: fusionkit@${FUSIONKIT_PYPI_VERSION} from PyPI)`, "-v, --version", "print the CLI (npm) and pinned synthesizer (PyPI) versions")
|
|
37
|
-
.option("-d, --dir <dir>", "fusionkit home (default: ./.fusionkit)")
|
|
38
30
|
.enablePositionalOptions();
|
|
39
|
-
registerInit(program);
|
|
40
|
-
registerPlane(program);
|
|
41
|
-
registerRunner(program);
|
|
42
|
-
registerSecrets(program);
|
|
43
|
-
registerRun(program);
|
|
44
|
-
registerLifecycle(program);
|
|
45
31
|
registerEnsemble(program);
|
|
46
32
|
registerLocal(program);
|
|
47
33
|
registerFusion(program);
|
package/dist/commands/fusion.js
CHANGED
|
@@ -4,6 +4,7 @@ import { loadFusionConfig } from "../fusion-config.js";
|
|
|
4
4
|
import { runFusionInit } from "../fusion-init.js";
|
|
5
5
|
import { fail } from "../shared/errors.js";
|
|
6
6
|
import { collect, parseFusionTool, parseIdValue, parsePanelModelSpec, parsePort } from "../shared/options.js";
|
|
7
|
+
import { reapFusionServices } from "../shared/portless.js";
|
|
7
8
|
/** Attach the panel/gateway flags shared by `fusion` and the per-tool launchers. */
|
|
8
9
|
function applyFusionOptions(command) {
|
|
9
10
|
return command
|
|
@@ -23,6 +24,8 @@ function applyFusionOptions(command) {
|
|
|
23
24
|
.option("--yes", "skip the interactive cloud-panel cost confirmation")
|
|
24
25
|
.option("--auth-token <token>", "require a bearer token on the gateway")
|
|
25
26
|
.option("--port <n>", "gateway port (default: ephemeral)")
|
|
27
|
+
.option("--portless", "route services through portless stable URLs (default; needs the proxy)")
|
|
28
|
+
.option("--no-portless", "disable portless; use raw loopback ports (same as PORTLESS=0)")
|
|
26
29
|
.allowUnknownOption()
|
|
27
30
|
.passThroughOptions();
|
|
28
31
|
}
|
|
@@ -52,6 +55,8 @@ function resolveOptions(opts) {
|
|
|
52
55
|
options.observe = opts.observe;
|
|
53
56
|
if (opts.yes === true)
|
|
54
57
|
options.yes = true;
|
|
58
|
+
if (opts.portless !== undefined)
|
|
59
|
+
options.portless = opts.portless;
|
|
55
60
|
if (opts.authToken !== undefined)
|
|
56
61
|
options.authToken = opts.authToken;
|
|
57
62
|
if (opts.port !== undefined)
|
|
@@ -92,6 +97,8 @@ function mergeConfig(options, config) {
|
|
|
92
97
|
options.local = config.local;
|
|
93
98
|
if (options.observe === undefined && config.observe !== undefined)
|
|
94
99
|
options.observe = config.observe;
|
|
100
|
+
if (options.portless === undefined && config.portless !== undefined)
|
|
101
|
+
options.portless = config.portless;
|
|
95
102
|
if (options.cursorKitDir === undefined && config.cursorKitDir != null)
|
|
96
103
|
options.cursorKitDir = config.cursorKitDir;
|
|
97
104
|
if (options.port === undefined && config.port != null)
|
|
@@ -122,22 +129,34 @@ function resolveContext(opts) {
|
|
|
122
129
|
return { options, ...(config?.tool !== undefined ? { configTool: config.tool } : {}) };
|
|
123
130
|
}
|
|
124
131
|
export function registerFusion(program) {
|
|
132
|
+
// Top-level `init` — scaffold a committed fusionkit.json for this repo.
|
|
133
|
+
program
|
|
134
|
+
.command("init")
|
|
135
|
+
.description("scaffold a committed fusionkit.json for this repo")
|
|
136
|
+
.option("--repo <dir>", "coding workspace the panel fuses over")
|
|
137
|
+
.option("--force", "overwrite an existing fusionkit.json")
|
|
138
|
+
.action(async (opts) => {
|
|
139
|
+
const repoRoot = configRepoRoot(resolveOptions(opts));
|
|
140
|
+
const code = await runFusionInit({ repoRoot, force: opts.force === true });
|
|
141
|
+
process.exit(code);
|
|
142
|
+
});
|
|
125
143
|
// Generic `fusion [tool]` — keeps the original surface and interactive pick.
|
|
126
144
|
applyFusionOptions(program
|
|
127
145
|
.command("fusion")
|
|
128
146
|
.description("one command: real model fusion backs a coding agent")
|
|
129
|
-
.argument("[tool]", `${FUSION_TOOLS.join(" | ")} |
|
|
147
|
+
.argument("[tool]", `${FUSION_TOOLS.join(" | ")} | stop (omit on a TTY to pick interactively)`)
|
|
130
148
|
.argument("[args...]", "arguments forwarded to the tool")
|
|
131
|
-
.option("--tool <tool>", `coding agent to launch (${FUSION_TOOLS.join(" | ")})`)
|
|
132
|
-
.option("--force", "overwrite an existing fusionkit.json (with `fusion init`)"))
|
|
149
|
+
.option("--tool <tool>", `coding agent to launch (${FUSION_TOOLS.join(" | ")})`))
|
|
133
150
|
.addHelpText("after", "\nfusionkit's own flags must precede the tool name; everything after the tool is forwarded to it." +
|
|
134
|
-
"\nRun `fusionkit
|
|
151
|
+
"\nRun `fusionkit init` to scaffold a committed fusionkit.json for this repo." +
|
|
152
|
+
"\nRun `fusionkit fusion stop` to reap portless singleton services (router, dashboard, ...).")
|
|
135
153
|
.action(async (positionalTool, args, opts) => {
|
|
136
|
-
// `fusion
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
154
|
+
// `fusion stop` reaps persistent portless singletons left running by prior
|
|
155
|
+
// runs (the router, dashboard, ...).
|
|
156
|
+
if (positionalTool === "stop") {
|
|
157
|
+
const stopped = await reapFusionServices((line) => console.error(line));
|
|
158
|
+
console.error(`fusion: stopped ${stopped} portless service(s)`);
|
|
159
|
+
process.exit(0);
|
|
141
160
|
}
|
|
142
161
|
const { options, configTool } = resolveContext(opts);
|
|
143
162
|
let tool = opts.tool ? parseFusionTool(opts.tool) : undefined;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real Cursor ACP front-door producer. Spawns the Cursorkit bridge (its
|
|
3
|
+
* local-model backend pointed at the running Fusion Harness Gateway) and drives
|
|
4
|
+
* the real cursor-agent CLI in ACP mode, asserting the fusion-synthesized
|
|
5
|
+
* sentinel reaches Cursor via session/update. Returns undefined when the
|
|
6
|
+
* Cursorkit checkout or the cursor-agent CLI are unavailable, so the acceptance
|
|
7
|
+
* suite records the explicit `blocked` / `cursorkit_backend_not_running`
|
|
8
|
+
* outcome instead of a silent pass.
|
|
9
|
+
*/
|
|
10
|
+
import type { FrontDoorOutcomeProducer } from "@fusionkit/model-gateway";
|
|
11
|
+
export type CursorAcpProducerInput = {
|
|
12
|
+
cursorKitDir: string | undefined;
|
|
13
|
+
gatewayUrl: string;
|
|
14
|
+
sentinel: string;
|
|
15
|
+
repo: string;
|
|
16
|
+
command?: string;
|
|
17
|
+
modelName?: string;
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
};
|
|
20
|
+
export declare function buildCursorAcpProducer(input: CursorAcpProducerInput): FrontDoorOutcomeProducer | undefined;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real Cursor ACP front-door producer. Spawns the Cursorkit bridge (its
|
|
3
|
+
* local-model backend pointed at the running Fusion Harness Gateway) and drives
|
|
4
|
+
* the real cursor-agent CLI in ACP mode, asserting the fusion-synthesized
|
|
5
|
+
* sentinel reaches Cursor via session/update. Returns undefined when the
|
|
6
|
+
* Cursorkit checkout or the cursor-agent CLI are unavailable, so the acceptance
|
|
7
|
+
* suite records the explicit `blocked` / `cursorkit_backend_not_running`
|
|
8
|
+
* outcome instead of a silent pass.
|
|
9
|
+
*/
|
|
10
|
+
import { spawn } from "node:child_process";
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
import { delimiter, join } from "node:path";
|
|
13
|
+
import { createInterface } from "node:readline";
|
|
14
|
+
function commandOnPath(command) {
|
|
15
|
+
if (command.includes("/"))
|
|
16
|
+
return existsSync(command);
|
|
17
|
+
const pathValue = process.env.PATH ?? "";
|
|
18
|
+
return pathValue
|
|
19
|
+
.split(delimiter)
|
|
20
|
+
.filter((entry) => entry.length > 0)
|
|
21
|
+
.some((dir) => existsSync(join(dir, command)));
|
|
22
|
+
}
|
|
23
|
+
function normalizeModelBaseUrl(gatewayUrl) {
|
|
24
|
+
const trimmed = gatewayUrl.replace(/\/+$/, "");
|
|
25
|
+
return trimmed.endsWith("/v1") ? trimmed : `${trimmed}/v1`;
|
|
26
|
+
}
|
|
27
|
+
export function buildCursorAcpProducer(input) {
|
|
28
|
+
const command = input.command ?? "cursor-agent";
|
|
29
|
+
if (input.cursorKitDir === undefined || input.cursorKitDir.length === 0) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
if (!existsSync(join(input.cursorKitDir, "dist/src/cli.js"))) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
if (!commandOnPath(command)) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
return () => runCursorAcpOutcome({ ...input, command });
|
|
39
|
+
}
|
|
40
|
+
async function runCursorAcpOutcome(input) {
|
|
41
|
+
const cursorKitDir = input.cursorKitDir;
|
|
42
|
+
const modelName = input.modelName ?? "local-fusion";
|
|
43
|
+
const bridgePort = 9700 + Math.floor(Math.random() * 250);
|
|
44
|
+
const bridgeEnv = {};
|
|
45
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
46
|
+
if (value === undefined)
|
|
47
|
+
continue;
|
|
48
|
+
if (key.startsWith("BRIDGE_") || key.startsWith("MODEL_") || key.startsWith("CURSOR_UPSTREAM")) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
bridgeEnv[key] = value;
|
|
52
|
+
}
|
|
53
|
+
Object.assign(bridgeEnv, {
|
|
54
|
+
BRIDGE_PORT: String(bridgePort),
|
|
55
|
+
BRIDGE_ROUTE_INVENTORY: "true",
|
|
56
|
+
CURSOR_UPSTREAM_BASE_URL: "https://api2.cursor.sh",
|
|
57
|
+
MODEL_BASE_URL: normalizeModelBaseUrl(input.gatewayUrl),
|
|
58
|
+
MODEL_API_KEY: "local",
|
|
59
|
+
MODEL_NAME: modelName,
|
|
60
|
+
MODEL_PROVIDER_MODEL: "fusion-panel",
|
|
61
|
+
MODEL_CONTEXT_TOKEN_LIMIT: "128000"
|
|
62
|
+
});
|
|
63
|
+
let bridgeOut = "";
|
|
64
|
+
const bridge = spawn(process.execPath, ["dist/src/cli.js", "serve"], {
|
|
65
|
+
cwd: cursorKitDir,
|
|
66
|
+
env: bridgeEnv,
|
|
67
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
68
|
+
});
|
|
69
|
+
bridge.stdout.on("data", (chunk) => {
|
|
70
|
+
bridgeOut += chunk.toString("utf8");
|
|
71
|
+
});
|
|
72
|
+
bridge.stderr.on("data", (chunk) => {
|
|
73
|
+
bridgeOut += chunk.toString("utf8");
|
|
74
|
+
});
|
|
75
|
+
const evidence = [];
|
|
76
|
+
try {
|
|
77
|
+
const deadline = Date.now() + 20_000;
|
|
78
|
+
while (!/bridge listening/.test(bridgeOut) && Date.now() < deadline) {
|
|
79
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
80
|
+
}
|
|
81
|
+
if (!/bridge listening/.test(bridgeOut)) {
|
|
82
|
+
return {
|
|
83
|
+
id: "cursor-acp",
|
|
84
|
+
status: "failed",
|
|
85
|
+
reason: "cursorkit_bridge_did_not_start",
|
|
86
|
+
evidence
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const acpText = await driveCursorAgentSentinel({
|
|
90
|
+
command: input.command,
|
|
91
|
+
bridgePort,
|
|
92
|
+
modelName,
|
|
93
|
+
cwd: input.repo,
|
|
94
|
+
sentinel: input.sentinel,
|
|
95
|
+
timeoutMs: input.timeoutMs ?? 120_000
|
|
96
|
+
});
|
|
97
|
+
if (acpText.includes(input.sentinel)) {
|
|
98
|
+
evidence.push(input.sentinel);
|
|
99
|
+
return {
|
|
100
|
+
id: "cursor-acp",
|
|
101
|
+
status: "passed",
|
|
102
|
+
request_path: "/agent.v1.AgentService/Run",
|
|
103
|
+
evidence
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
id: "cursor-acp",
|
|
108
|
+
status: "failed",
|
|
109
|
+
reason: "sentinel_not_observed_in_cursor_session_update",
|
|
110
|
+
evidence
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
return {
|
|
115
|
+
id: "cursor-acp",
|
|
116
|
+
status: "failed",
|
|
117
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
118
|
+
evidence
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
bridge.kill("SIGTERM");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async function driveCursorAgentSentinel(input) {
|
|
126
|
+
const acp = spawn(input.command, [
|
|
127
|
+
"--endpoint",
|
|
128
|
+
`http://127.0.0.1:${input.bridgePort}`,
|
|
129
|
+
"--model",
|
|
130
|
+
input.modelName,
|
|
131
|
+
"--mode",
|
|
132
|
+
"ask",
|
|
133
|
+
"acp"
|
|
134
|
+
], { cwd: input.cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
135
|
+
let acpText = "";
|
|
136
|
+
let nextId = 1;
|
|
137
|
+
const pending = new Map();
|
|
138
|
+
const rl = createInterface({ input: acp.stdout });
|
|
139
|
+
const send = (method, params) => {
|
|
140
|
+
const id = nextId++;
|
|
141
|
+
acp.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`);
|
|
142
|
+
return new Promise((resolve, reject) => pending.set(id, { resolve, reject }));
|
|
143
|
+
};
|
|
144
|
+
rl.on("line", (line) => {
|
|
145
|
+
let message;
|
|
146
|
+
try {
|
|
147
|
+
message = JSON.parse(line);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (message.id !== undefined && message.method === undefined) {
|
|
153
|
+
const waiter = pending.get(Number(message.id));
|
|
154
|
+
if (waiter === undefined)
|
|
155
|
+
return;
|
|
156
|
+
pending.delete(Number(message.id));
|
|
157
|
+
if (message.error !== undefined)
|
|
158
|
+
waiter.reject(message.error);
|
|
159
|
+
else
|
|
160
|
+
waiter.resolve(message.result);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (message.method !== undefined) {
|
|
164
|
+
if (message.method === "session/update")
|
|
165
|
+
acpText += JSON.stringify(message.params);
|
|
166
|
+
if (message.id !== undefined) {
|
|
167
|
+
acp.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id: message.id, result: { outcome: { outcome: "skipped", reason: "acceptance" } } })}\n`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
const withTimeout = (promise, ms) => Promise.race([
|
|
172
|
+
promise,
|
|
173
|
+
new Promise((_resolve, reject) => setTimeout(() => reject(new Error("ACP step timed out")), ms))
|
|
174
|
+
]);
|
|
175
|
+
try {
|
|
176
|
+
await withTimeout(send("initialize", {
|
|
177
|
+
protocolVersion: 1,
|
|
178
|
+
clientCapabilities: {
|
|
179
|
+
fs: { readTextFile: false, writeTextFile: false },
|
|
180
|
+
terminal: false
|
|
181
|
+
},
|
|
182
|
+
clientInfo: { name: "fusionkit-acceptance", version: "0.1.0" }
|
|
183
|
+
}), 60_000);
|
|
184
|
+
await withTimeout(send("authenticate", { methodId: "cursor_login" }), 60_000);
|
|
185
|
+
const session = (await withTimeout(send("session/new", { cwd: input.cwd, mcpServers: [] }), 60_000));
|
|
186
|
+
const sessionId = session.sessionId ?? session.session?.id;
|
|
187
|
+
if (sessionId === undefined)
|
|
188
|
+
return acpText;
|
|
189
|
+
await withTimeout(send("session/prompt", {
|
|
190
|
+
sessionId,
|
|
191
|
+
prompt: [
|
|
192
|
+
{
|
|
193
|
+
type: "text",
|
|
194
|
+
text: `Reply with exactly this token and nothing else: ${input.sentinel}`
|
|
195
|
+
}
|
|
196
|
+
]
|
|
197
|
+
}), input.timeoutMs);
|
|
198
|
+
await new Promise((resolve) => setTimeout(resolve, 1_000));
|
|
199
|
+
return acpText;
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
rl.close();
|
|
203
|
+
acp.kill("SIGTERM");
|
|
204
|
+
}
|
|
205
|
+
}
|
package/dist/fusion-config.d.ts
CHANGED
package/dist/fusion-config.js
CHANGED
|
@@ -91,6 +91,11 @@ export function parseFusionConfig(raw, source) {
|
|
|
91
91
|
throw new FusionConfigError(`${source}: observe must be a boolean`);
|
|
92
92
|
config.observe = raw.observe;
|
|
93
93
|
}
|
|
94
|
+
if (raw.portless !== undefined) {
|
|
95
|
+
if (typeof raw.portless !== "boolean")
|
|
96
|
+
throw new FusionConfigError(`${source}: portless must be a boolean`);
|
|
97
|
+
config.portless = raw.portless;
|
|
98
|
+
}
|
|
94
99
|
if (raw.cursorKitDir !== undefined && raw.cursorKitDir !== null) {
|
|
95
100
|
if (typeof raw.cursorKitDir !== "string") {
|
|
96
101
|
throw new FusionConfigError(`${source}: cursorKitDir must be a string or null`);
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import type { ChildProcess } from "node:child_process";
|
|
15
15
|
import type { EnsembleModel } from "@fusionkit/ensemble";
|
|
16
|
+
import type { PortlessSession } from "./shared/portless.js";
|
|
16
17
|
export type FusionTool = "codex" | "claude" | "cursor" | "serve";
|
|
17
18
|
export declare const FUSION_TOOLS: readonly FusionTool[];
|
|
18
19
|
export type PanelProvider = "mlx" | "openai" | "anthropic" | "google" | "openai-compatible";
|
|
@@ -34,7 +35,7 @@ export type PanelModelSpec = {
|
|
|
34
35
|
* synthesizer (`fusionkit serve`) and the single-model OpenAI shim
|
|
35
36
|
* (`fusionkit serve-endpoint`). Pinned so `uvx` resolves a reproducible build.
|
|
36
37
|
*/
|
|
37
|
-
export declare const FUSIONKIT_PYPI_VERSION = "0.
|
|
38
|
+
export declare const FUSIONKIT_PYPI_VERSION = "0.2.0";
|
|
38
39
|
/**
|
|
39
40
|
* Default cloud panel — works cross-platform with only `OPENAI_API_KEY` and
|
|
40
41
|
* `ANTHROPIC_API_KEY` set. The judge defaults to the first entry.
|
|
@@ -94,23 +95,29 @@ export type Observability = {
|
|
|
94
95
|
traceDir: string;
|
|
95
96
|
close: () => Promise<void>;
|
|
96
97
|
};
|
|
97
|
-
/**
|
|
98
|
-
* Start the scope dashboard on the fixed port, backed by a fresh per-run SQLite
|
|
99
|
-
* file and trace dir, and return the URLs the caller injects (as
|
|
100
|
-
* FUSION_TRACE_URL / FUSION_TRACE_DIR) into every spawned process. Prefers the
|
|
101
|
-
* prebuilt bundle shipped inside the npm package; falls back to building the
|
|
102
|
-
* app from source in a monorepo dev checkout.
|
|
103
|
-
*/
|
|
104
98
|
export declare function startObservability(input: {
|
|
105
99
|
log: (line: string) => void;
|
|
106
100
|
logFile?: string;
|
|
107
101
|
report?: StackReporter;
|
|
102
|
+
portless: PortlessSession;
|
|
108
103
|
}): Promise<Observability>;
|
|
109
|
-
|
|
104
|
+
/**
|
|
105
|
+
* The single `fusionkit serve` router: one process that fronts every panel
|
|
106
|
+
* model (passthrough, routed by the endpoint id in the request `model` field)
|
|
107
|
+
* and also performs trajectory synthesis. `endpoints` maps each panel id to the
|
|
108
|
+
* router URL so the harness reaches its model through the one base URL.
|
|
109
|
+
*/
|
|
110
|
+
export type Router = {
|
|
111
|
+
url: string;
|
|
112
|
+
port: number;
|
|
113
|
+
/** The router process pid (owns its portless route across runs). */
|
|
114
|
+
pid?: number;
|
|
110
115
|
endpoints: Record<string, string>;
|
|
111
|
-
judgeUrl: string;
|
|
112
|
-
judgeModel: string;
|
|
113
116
|
models: EnsembleModel[];
|
|
117
|
+
/** The endpoint id used as the judge/synthesizer. */
|
|
118
|
+
judgeModel: string;
|
|
119
|
+
/** Sorted endpoint ids — the router's discover-or-spawn identity token. */
|
|
120
|
+
identity: string;
|
|
114
121
|
close: () => Promise<void>;
|
|
115
122
|
};
|
|
116
123
|
/**
|
|
@@ -152,19 +159,20 @@ export type StackEvent = {
|
|
|
152
159
|
};
|
|
153
160
|
export type StackReporter = (event: StackEvent) => void;
|
|
154
161
|
/**
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
162
|
+
* Spawn the single `fusionkit serve` router fronting every panel model + the
|
|
163
|
+
* synthesizer. MLX specs first get an in-process OpenAI-compatible gateway
|
|
164
|
+
* (loopback) that the router proxies to; cloud specs call their provider
|
|
165
|
+
* directly. Returns the router URL, an id->routerUrl endpoint map, and a close
|
|
166
|
+
* that tears down the router process and any MLX gateways it fronts.
|
|
159
167
|
*/
|
|
160
|
-
export declare function
|
|
168
|
+
export declare function startRouter(options: {
|
|
161
169
|
specs: PanelModelSpec[];
|
|
162
|
-
|
|
170
|
+
judgeModel?: string;
|
|
163
171
|
fusionkitDir?: string;
|
|
164
172
|
logsDir?: string;
|
|
165
173
|
report?: StackReporter;
|
|
166
174
|
log: (line: string) => void;
|
|
167
|
-
}): Promise<
|
|
175
|
+
}): Promise<Router>;
|
|
168
176
|
export type FusionStack = {
|
|
169
177
|
fusionUrl: string;
|
|
170
178
|
endpoints: Record<string, string>;
|
|
@@ -185,24 +193,10 @@ export type StartFusionStackOptions = {
|
|
|
185
193
|
timeoutMs?: number;
|
|
186
194
|
logsDir?: string;
|
|
187
195
|
report?: StackReporter;
|
|
196
|
+
/** Active portless session; defaults to a disabled (loopback) session. */
|
|
197
|
+
portless?: PortlessSession;
|
|
188
198
|
log: (line: string) => void;
|
|
189
199
|
};
|
|
190
|
-
/**
|
|
191
|
-
* Spawn a `fusionkit serve` as the trajectory-synthesis backend, configured
|
|
192
|
-
* with the judge model. FusionKit owns synthesis, so the agent harness fuses
|
|
193
|
-
* its trajectories through this server's `/v1/fusion/trajectories:fuse`.
|
|
194
|
-
*/
|
|
195
|
-
export declare function startSynthesisServer(input: {
|
|
196
|
-
fusionkitDir?: string;
|
|
197
|
-
judgeModel: string;
|
|
198
|
-
judgeBaseUrl: string;
|
|
199
|
-
env: Record<string, string | undefined>;
|
|
200
|
-
logFile?: string;
|
|
201
|
-
log: (line: string) => void;
|
|
202
|
-
}): Promise<{
|
|
203
|
-
url: string;
|
|
204
|
-
child: ChildProcess;
|
|
205
|
-
}>;
|
|
206
200
|
export declare function startFusionStack(options: StartFusionStackOptions): Promise<FusionStack>;
|
|
207
201
|
/**
|
|
208
202
|
* Start the Cursorkit bridge with its local-model backend pointed at the fusion
|
|
@@ -212,6 +206,7 @@ export declare function startCursorBridge(input: {
|
|
|
212
206
|
cursorKitDir: string;
|
|
213
207
|
fusionUrl: string;
|
|
214
208
|
logFile?: string;
|
|
209
|
+
caCertPath?: string;
|
|
215
210
|
log: (line: string) => void;
|
|
216
211
|
}): Promise<{
|
|
217
212
|
child: ChildProcess;
|
|
@@ -234,8 +229,12 @@ export type RunFusionOptions = {
|
|
|
234
229
|
observe?: boolean;
|
|
235
230
|
/** Skip the interactive cost/scope confirmation for the cloud panel. */
|
|
236
231
|
yes?: boolean;
|
|
232
|
+
/** Route services through portless (stable named URLs + singletons). Default on. */
|
|
233
|
+
portless?: boolean;
|
|
237
234
|
log?: (line: string) => void;
|
|
238
235
|
};
|
|
236
|
+
/** Whether portless is enabled: explicit flag/config wins, else on unless PORTLESS=0. */
|
|
237
|
+
export declare function portlessEnabled(options: RunFusionOptions): boolean;
|
|
239
238
|
export declare function runFusion(tool: FusionTool, toolArgs: string[], options?: RunFusionOptions): Promise<number>;
|
|
240
239
|
/** Interactive tool picker for when no `--tool` was provided on a TTY. */
|
|
241
240
|
export declare function pickTool(): Promise<FusionTool>;
|