@fusionkit/cli 0.1.3 → 0.1.4

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 (34) hide show
  1. package/dist/commands/fusion.js +17 -2
  2. package/dist/commands/plane.js +10 -2
  3. package/dist/fusion-config.d.ts +1 -0
  4. package/dist/fusion-config.js +5 -0
  5. package/dist/fusion-quickstart.d.ts +33 -34
  6. package/dist/fusion-quickstart.js +324 -278
  7. package/dist/shared/portless.d.ts +97 -0
  8. package/dist/shared/portless.js +253 -0
  9. package/dist/test/portless.test.d.ts +1 -0
  10. package/dist/test/portless.test.js +65 -0
  11. package/package.json +12 -9
  12. package/scope/.next/BUILD_ID +1 -1
  13. package/scope/.next/app-build-manifest.json +8 -8
  14. package/scope/.next/app-path-routes-manifest.json +3 -3
  15. package/scope/.next/build-manifest.json +2 -2
  16. package/scope/.next/prerender-manifest.json +9 -9
  17. package/scope/.next/required-server-files.json +4 -0
  18. package/scope/.next/server/app/_not-found.html +1 -1
  19. package/scope/.next/server/app/_not-found.rsc +1 -1
  20. package/scope/.next/server/app/environments.html +1 -1
  21. package/scope/.next/server/app/environments.rsc +1 -1
  22. package/scope/.next/server/app/index.html +1 -1
  23. package/scope/.next/server/app/index.rsc +1 -1
  24. package/scope/.next/server/app/models.html +1 -1
  25. package/scope/.next/server/app/models.rsc +1 -1
  26. package/scope/.next/server/app-paths-manifest.json +3 -3
  27. package/scope/.next/server/functions-config-manifest.json +3 -3
  28. package/scope/.next/server/pages/404.html +1 -1
  29. package/scope/.next/server/pages/500.html +1 -1
  30. package/scope/.next/server/server-reference-manifest.json +1 -1
  31. package/scope/package.json +3 -1
  32. package/scope/server.js +1 -1
  33. /package/scope/.next/static/{8b1kwXDxrKvteRVkOC_Z2 → 5tnFLuvnSbNZNtqRgoot8}/_buildManifest.js +0 -0
  34. /package/scope/.next/static/{8b1kwXDxrKvteRVkOC_Z2 → 5tnFLuvnSbNZNtqRgoot8}/_ssgManifest.js +0 -0
@@ -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)
@@ -126,12 +133,13 @@ export function registerFusion(program) {
126
133
  applyFusionOptions(program
127
134
  .command("fusion")
128
135
  .description("one command: real model fusion backs a coding agent")
129
- .argument("[tool]", `${FUSION_TOOLS.join(" | ")} | init (omit on a TTY to pick interactively)`)
136
+ .argument("[tool]", `${FUSION_TOOLS.join(" | ")} | init | stop (omit on a TTY to pick interactively)`)
130
137
  .argument("[args...]", "arguments forwarded to the tool")
131
138
  .option("--tool <tool>", `coding agent to launch (${FUSION_TOOLS.join(" | ")})`)
132
139
  .option("--force", "overwrite an existing fusionkit.json (with `fusion init`)"))
133
140
  .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.")
141
+ "\nRun `fusionkit fusion init` to scaffold a committed fusionkit.json for this repo." +
142
+ "\nRun `fusionkit fusion stop` to reap portless singleton services (router, dashboard, ...).")
135
143
  .action(async (positionalTool, args, opts) => {
136
144
  // `fusion init` scaffolds the per-repo config instead of launching a tool.
137
145
  if (positionalTool === "init") {
@@ -139,6 +147,13 @@ export function registerFusion(program) {
139
147
  const code = await runFusionInit({ repoRoot, force: opts.force === true });
140
148
  process.exit(code);
141
149
  }
150
+ // `fusion stop` reaps persistent portless singletons left running by prior
151
+ // runs (the router, dashboard, ...).
152
+ if (positionalTool === "stop") {
153
+ const stopped = await reapFusionServices((line) => console.error(line));
154
+ console.error(`fusion: stopped ${stopped} portless service(s)`);
155
+ process.exit(0);
156
+ }
142
157
  const { options, configTool } = resolveContext(opts);
143
158
  let tool = opts.tool ? parseFusionTool(opts.tool) : undefined;
144
159
  let toolArgs = [...args];
@@ -2,6 +2,7 @@ import { join } from "node:path";
2
2
  import { Plane, startPlaneServer } from "@fusionkit/plane";
3
3
  import { loadHome, secretStoreFor } from "../config.js";
4
4
  import { resolveDir } from "../shared/plane.js";
5
+ import { createPortlessSession } from "../shared/portless.js";
5
6
  export function registerPlane(program) {
6
7
  const plane = program.command("plane").description("control plane + control panel");
7
8
  plane
@@ -24,7 +25,14 @@ export function registerPlane(program) {
24
25
  const port = opts.port ? Number(opts.port) : home.config.port;
25
26
  const host = opts.host ?? home.config.host;
26
27
  const started = await startPlaneServer(planeInstance, { port, host });
27
- console.log(`warrant plane listening on http://${started.host}:${started.port}`);
28
- console.log(`control panel: http://${started.host}:${started.port}/ui/`);
28
+ // Register a stable portless name for the control panel (enabled unless
29
+ // PORTLESS=0 or no proxy is detected; falls back to the loopback URL).
30
+ const portless = await createPortlessSession({
31
+ enabled: process.env.PORTLESS !== "0",
32
+ log: (line) => console.error(line)
33
+ });
34
+ const baseUrl = portless.register("plane", started.port);
35
+ console.log(`warrant plane listening on ${baseUrl}`);
36
+ console.log(`control panel: ${baseUrl}/ui/`);
29
37
  });
30
38
  }
@@ -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>;