@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.
Files changed (63) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.js +3 -17
  3. package/dist/commands/fusion.js +28 -9
  4. package/dist/cursor-acp.d.ts +20 -0
  5. package/dist/cursor-acp.js +205 -0
  6. package/dist/fusion-config.d.ts +1 -0
  7. package/dist/fusion-config.js +5 -0
  8. package/dist/fusion-quickstart.d.ts +33 -34
  9. package/dist/fusion-quickstart.js +324 -278
  10. package/dist/gateway.js +13 -1
  11. package/dist/shared/portless.d.ts +97 -0
  12. package/dist/shared/portless.js +253 -0
  13. package/dist/test/cli.test.js +24 -139
  14. package/dist/test/portless.test.d.ts +1 -0
  15. package/dist/test/portless.test.js +65 -0
  16. package/package.json +12 -9
  17. package/scope/.next/BUILD_ID +1 -1
  18. package/scope/.next/app-build-manifest.json +14 -14
  19. package/scope/.next/app-path-routes-manifest.json +4 -4
  20. package/scope/.next/build-manifest.json +2 -2
  21. package/scope/.next/prerender-manifest.json +10 -10
  22. package/scope/.next/required-server-files.json +4 -0
  23. package/scope/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  24. package/scope/.next/server/app/_not-found.html +1 -1
  25. package/scope/.next/server/app/_not-found.rsc +1 -1
  26. package/scope/.next/server/app/api/environments/route_client-reference-manifest.js +1 -1
  27. package/scope/.next/server/app/api/ingest/route_client-reference-manifest.js +1 -1
  28. package/scope/.next/server/app/api/models/route_client-reference-manifest.js +1 -1
  29. package/scope/.next/server/app/api/replay/route_client-reference-manifest.js +1 -1
  30. package/scope/.next/server/app/api/sessions/[traceId]/route_client-reference-manifest.js +1 -1
  31. package/scope/.next/server/app/api/sessions/route_client-reference-manifest.js +1 -1
  32. package/scope/.next/server/app/api/stream/route_client-reference-manifest.js +1 -1
  33. package/scope/.next/server/app/environments/page_client-reference-manifest.js +1 -1
  34. package/scope/.next/server/app/environments.html +1 -1
  35. package/scope/.next/server/app/environments.rsc +1 -1
  36. package/scope/.next/server/app/index.html +1 -1
  37. package/scope/.next/server/app/index.rsc +1 -1
  38. package/scope/.next/server/app/models/page_client-reference-manifest.js +1 -1
  39. package/scope/.next/server/app/models.html +1 -1
  40. package/scope/.next/server/app/models.rsc +1 -1
  41. package/scope/.next/server/app/page_client-reference-manifest.js +1 -1
  42. package/scope/.next/server/app/sessions/[traceId]/page_client-reference-manifest.js +1 -1
  43. package/scope/.next/server/app-paths-manifest.json +4 -4
  44. package/scope/.next/server/functions-config-manifest.json +3 -3
  45. package/scope/.next/server/pages/404.html +1 -1
  46. package/scope/.next/server/pages/500.html +1 -1
  47. package/scope/.next/server/server-reference-manifest.json +1 -1
  48. package/scope/package.json +3 -1
  49. package/scope/server.js +1 -1
  50. package/dist/commands/init.d.ts +0 -2
  51. package/dist/commands/init.js +0 -24
  52. package/dist/commands/lifecycle.d.ts +0 -2
  53. package/dist/commands/lifecycle.js +0 -124
  54. package/dist/commands/plane.d.ts +0 -2
  55. package/dist/commands/plane.js +0 -30
  56. package/dist/commands/run.d.ts +0 -2
  57. package/dist/commands/run.js +0 -149
  58. package/dist/commands/runner.d.ts +0 -2
  59. package/dist/commands/runner.js +0 -33
  60. package/dist/commands/secrets.d.ts +0 -2
  61. package/dist/commands/secrets.js +0 -21
  62. /package/scope/.next/static/{8b1kwXDxrKvteRVkOC_Z2 → vxqImMqlOwssVTua5Facf}/_buildManifest.js +0 -0
  63. /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 fusion init
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 fusion init` — scaffold `fusionkit.json` for this repo.
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. The global `--dir` option must precede the
17
- * subcommand (`enablePositionalOptions` keeps the launcher commands' passthrough
18
- * unambiguous). Each `register*` helper attaches its command(s) and reads the
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);
@@ -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(" | ")} | init (omit on a TTY to pick interactively)`)
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 fusion init` to scaffold a committed fusionkit.json for this repo.")
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 init` scaffolds the per-repo config instead of launching a tool.
137
- if (positionalTool === "init") {
138
- const repoRoot = configRepoRoot(resolveOptions(opts));
139
- const code = await runFusionInit({ repoRoot, force: opts.force === true });
140
- process.exit(code);
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
+ }
@@ -8,6 +8,7 @@ export type FusionConfig = {
8
8
  judgeModel?: string;
9
9
  local?: boolean;
10
10
  observe?: boolean;
11
+ portless?: boolean;
11
12
  cursorKitDir?: string | null;
12
13
  port?: number | null;
13
14
  };
@@ -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.1.1";
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
- export type ModelServers = {
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
- * Bring up one real model server per panel model and return an id -> base URL
156
- * map. `mlx` specs run locally; cloud specs are fronted by FusionKit. When
157
- * `endpoints` is supplied (pre-running servers or tests), those are used
158
- * verbatim and nothing is spawned.
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 startModelServers(options: {
168
+ export declare function startRouter(options: {
161
169
  specs: PanelModelSpec[];
162
- endpoints?: Record<string, string>;
170
+ judgeModel?: string;
163
171
  fusionkitDir?: string;
164
172
  logsDir?: string;
165
173
  report?: StackReporter;
166
174
  log: (line: string) => void;
167
- }): Promise<ModelServers>;
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>;