@fusionkit/cli 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +32 -8
  2. package/dist/cli.d.ts +1 -0
  3. package/dist/cli.js +1 -0
  4. package/dist/commands/doctor.js +7 -3
  5. package/dist/commands/ensemble-gateway.js +0 -2
  6. package/dist/commands/ensemble-records.d.ts +2 -1
  7. package/dist/commands/ensemble-records.js +3 -1
  8. package/dist/commands/ensemble.js +3 -4
  9. package/dist/commands/fusion.js +16 -13
  10. package/dist/commands/local.js +3 -3
  11. package/dist/cursor-acp.d.ts +3 -5
  12. package/dist/cursor-acp.js +12 -11
  13. package/dist/dashboard.d.ts +65 -0
  14. package/dist/dashboard.js +587 -0
  15. package/dist/fusion/env.d.ts +111 -0
  16. package/dist/fusion/env.js +98 -0
  17. package/dist/fusion/observability.d.ts +39 -0
  18. package/dist/fusion/observability.js +227 -0
  19. package/dist/fusion/preflight.d.ts +12 -0
  20. package/dist/fusion/preflight.js +42 -0
  21. package/dist/fusion/stack.d.ts +66 -0
  22. package/dist/fusion/stack.js +315 -0
  23. package/dist/fusion-config.d.ts +58 -7
  24. package/dist/fusion-config.js +152 -28
  25. package/dist/fusion-init.d.ts +1 -0
  26. package/dist/fusion-init.js +50 -15
  27. package/dist/fusion-quickstart.d.ts +11 -222
  28. package/dist/fusion-quickstart.js +58 -759
  29. package/dist/gateway.d.ts +0 -2
  30. package/dist/gateway.js +0 -2
  31. package/dist/local.d.ts +10 -17
  32. package/dist/local.js +50 -116
  33. package/dist/shared/options.d.ts +2 -1
  34. package/dist/shared/options.js +13 -19
  35. package/dist/shared/proc.d.ts +4 -70
  36. package/dist/shared/proc.js +3 -228
  37. package/dist/test/cli.test.js +11 -6
  38. package/dist/test/dashboard.test.d.ts +1 -0
  39. package/dist/test/dashboard.test.js +214 -0
  40. package/dist/test/fusion-config.test.js +64 -4
  41. package/dist/test/gateway-e2e.test.js +13 -10
  42. package/dist/test/local.test.js +4 -4
  43. package/dist/tools.d.ts +2 -0
  44. package/dist/tools.js +25 -0
  45. package/package.json +14 -9
  46. package/scope/.next/BUILD_ID +1 -1
  47. package/scope/.next/app-build-manifest.json +10 -10
  48. package/scope/.next/app-path-routes-manifest.json +2 -2
  49. package/scope/.next/build-manifest.json +2 -2
  50. package/scope/.next/prerender-manifest.json +13 -13
  51. package/scope/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  52. package/scope/.next/server/app/_not-found.html +1 -1
  53. package/scope/.next/server/app/_not-found.rsc +1 -1
  54. package/scope/.next/server/app/api/environments/route_client-reference-manifest.js +1 -1
  55. package/scope/.next/server/app/api/ingest/route_client-reference-manifest.js +1 -1
  56. package/scope/.next/server/app/api/models/route_client-reference-manifest.js +1 -1
  57. package/scope/.next/server/app/api/replay/route_client-reference-manifest.js +1 -1
  58. package/scope/.next/server/app/api/sessions/[traceId]/route_client-reference-manifest.js +1 -1
  59. package/scope/.next/server/app/api/sessions/route_client-reference-manifest.js +1 -1
  60. package/scope/.next/server/app/api/stream/route_client-reference-manifest.js +1 -1
  61. package/scope/.next/server/app/environments/page_client-reference-manifest.js +1 -1
  62. package/scope/.next/server/app/environments.html +1 -1
  63. package/scope/.next/server/app/environments.rsc +1 -1
  64. package/scope/.next/server/app/index.html +1 -1
  65. package/scope/.next/server/app/index.rsc +1 -1
  66. package/scope/.next/server/app/models/page_client-reference-manifest.js +1 -1
  67. package/scope/.next/server/app/models.html +1 -1
  68. package/scope/.next/server/app/models.rsc +1 -1
  69. package/scope/.next/server/app/page_client-reference-manifest.js +1 -1
  70. package/scope/.next/server/app/sessions/[traceId]/page_client-reference-manifest.js +1 -1
  71. package/scope/.next/server/app-paths-manifest.json +2 -2
  72. package/scope/.next/server/functions-config-manifest.json +2 -2
  73. package/scope/.next/server/pages/404.html +1 -1
  74. package/scope/.next/server/pages/500.html +1 -1
  75. package/scope/.next/server/server-reference-manifest.json +1 -1
  76. /package/scope/.next/static/{vxqImMqlOwssVTua5Facf → BrrtQvnEIgv-OVkaeanKI}/_buildManifest.js +0 -0
  77. /package/scope/.next/static/{vxqImMqlOwssVTua5Facf → BrrtQvnEIgv-OVkaeanKI}/_ssgManifest.js +0 -0
@@ -3,8 +3,9 @@
3
3
  * per-repo `fusionkit.json`. On a non-interactive stdin the prompts fall back to
4
4
  * their defaults, so `fusion init` still produces a sensible config in CI.
5
5
  */
6
- import { DEFAULT_CLOUD_PANEL, DEFAULT_TRIO, defaultKeyEnv } from "./fusion-quickstart.js";
7
- import { FUSION_CONFIG_VERSION, FusionConfigError, fusionConfigPath, writeFusionConfig } from "./fusion-config.js";
6
+ import { execFileSync } from "node:child_process";
7
+ import { DEFAULT_CLOUD_PANEL, DEFAULT_TRIO, defaultKeyEnv, fusionkitPyCommand } from "./fusion-quickstart.js";
8
+ import { FUSION_CONFIG_VERSION, FusionConfigError, fusionConfigPath, fusionPromptsDir, PROMPT_IDS, writeFusionConfig, writeFusionPrompts } from "./fusion-config.js";
8
9
  import { parsePanelModelSpec } from "./shared/options.js";
9
10
  import { confirm, done, note, select, text } from "./ui/prompt.js";
10
11
  import { uiStream } from "./ui/runtime.js";
@@ -18,6 +19,35 @@ function withKeyEnv(spec) {
18
19
  const keyEnv = defaultKeyEnv(provider);
19
20
  return keyEnv !== undefined ? { ...spec, keyEnv } : { ...spec };
20
21
  }
22
+ /**
23
+ * Pull the built-in default prompts from the Python `fusionkit` CLI
24
+ * (`fusionkit prompts dump`) so the scaffolded `.fusionkit/prompts/*.md` files
25
+ * match the synthesizer's source of truth. Returns `undefined` if the CLI is
26
+ * unreachable (e.g. offline) — callers fall back to leaving prompts unset, in
27
+ * which case the built-in defaults are used at run time.
28
+ */
29
+ function fetchDefaultPrompts(fusionkitDir) {
30
+ const runner = fusionkitPyCommand(fusionkitDir);
31
+ try {
32
+ const stdout = execFileSync(runner.command, [...runner.prefix, "prompts", "dump"], {
33
+ encoding: "utf8",
34
+ stdio: ["ignore", "pipe", "ignore"],
35
+ timeout: 120_000,
36
+ ...(runner.cwd !== undefined ? { cwd: runner.cwd } : {})
37
+ });
38
+ const parsed = JSON.parse(stdout);
39
+ const prompts = {};
40
+ for (const id of PROMPT_IDS) {
41
+ const value = parsed[id];
42
+ if (typeof value === "string" && value.length > 0)
43
+ prompts[id] = value;
44
+ }
45
+ return Object.keys(prompts).length > 0 ? prompts : undefined;
46
+ }
47
+ catch {
48
+ return undefined;
49
+ }
50
+ }
21
51
  async function buildCustomPanel() {
22
52
  out.write(dim("Add panel models as ID=PROVIDER:MODEL (e.g. gpt=openai:gpt-5.5). Blank line to finish.\n"));
23
53
  const specs = [];
@@ -42,7 +72,7 @@ async function buildCustomPanel() {
42
72
  export async function runFusionInit(input) {
43
73
  if (input.repoRoot === undefined) {
44
74
  out.write(`${red("error:")} not inside a git repository.\n` +
45
- " cd into your project (or run from a repo) so fusionkit.json lands at the repo root.\n");
75
+ " cd into your project (or run from a repo) so .fusionkit/ lands at the repo root.\n");
46
76
  return 1;
47
77
  }
48
78
  out.write(`\n${brandHeader("let's set up model fusion for this repo")}\n\n`);
@@ -51,7 +81,7 @@ export async function runFusionInit(input) {
51
81
  options: [
52
82
  { value: "codex", label: "codex", hint: "OpenAI Codex CLI" },
53
83
  { value: "claude", label: "claude", hint: "Claude Code" },
54
- { value: "cursor", label: "cursor", hint: "cursor-agent (needs a Cursorkit checkout)" },
84
+ { value: "cursor", label: "cursor", hint: "cursor-agent (logged-in CLI)" },
55
85
  { value: "serve", label: "serve", hint: "just run the gateway and print setup" }
56
86
  ],
57
87
  defaultIndex: 0
@@ -83,14 +113,6 @@ export async function runFusionInit(input) {
83
113
  }
84
114
  const judgeDefault = panel[0]?.model ?? "";
85
115
  const judgeModel = await text({ message: "Judge model (for synthesis)", defaultValue: judgeDefault });
86
- let cursorKitDir;
87
- if (tool === "cursor") {
88
- const answer = await text({
89
- message: "Cursorkit checkout dir (optional, can set FUSIONKIT_CURSORKIT_DIR later)"
90
- });
91
- if (answer.length > 0)
92
- cursorKitDir = answer;
93
- }
94
116
  const observe = await confirm({ message: "Enable the observability dashboard by default?", defaultValue: false });
95
117
  const config = {
96
118
  version: FUSION_CONFIG_VERSION,
@@ -98,8 +120,7 @@ export async function runFusionInit(input) {
98
120
  panel,
99
121
  ...(judgeModel.length > 0 ? { judgeModel } : {}),
100
122
  local: preset === "local",
101
- observe,
102
- ...(cursorKitDir !== undefined ? { cursorKitDir } : {})
123
+ observe
103
124
  };
104
125
  let path;
105
126
  try {
@@ -112,8 +133,22 @@ export async function runFusionInit(input) {
112
133
  }
113
134
  throw error;
114
135
  }
136
+ // Scaffold editable prompt overrides from the synthesizer's built-in defaults.
137
+ // If the Python CLI is unreachable, skip silently — unset prompts use the
138
+ // built-in defaults at run time, and the user can eject them later with
139
+ // `fusionkit prompts dump --dir .fusionkit/prompts`.
140
+ const defaultPrompts = fetchDefaultPrompts(input.fusionkitDir);
141
+ const wrotePrompts = defaultPrompts !== undefined
142
+ ? writeFusionPrompts(input.repoRoot, defaultPrompts, { force: input.force === true })
143
+ : [];
115
144
  out.write("\n");
116
145
  done(`wrote ${cyan(fusionConfigPath(input.repoRoot))}`);
117
- note(`commit it, then just run: ${bold(`fusionkit ${tool === "serve" ? "serve" : tool}`)}`);
146
+ if (wrotePrompts.length > 0) {
147
+ note(`editable prompts in ${cyan(fusionPromptsDir(input.repoRoot))} (empty file = built-in default)`);
148
+ }
149
+ else if (defaultPrompts === undefined) {
150
+ note(`prompts use built-in defaults; run ${bold("fusionkit prompts dump --dir .fusionkit/prompts")} to customize`);
151
+ }
152
+ note(`commit ${cyan(".fusionkit/")}, then just run: ${bold(`fusionkit ${tool === "serve" ? "serve" : tool}`)}`);
118
153
  return 0;
119
154
  }
@@ -10,229 +10,18 @@
10
10
  *
11
11
  * No mocks: the panel is real models, candidates are real patches verified by
12
12
  * really running the repo's tests, and the judge is a real model.
13
- */
14
- import type { ChildProcess } from "node:child_process";
15
- import type { EnsembleModel } from "@fusionkit/ensemble";
16
- import type { PortlessSession } from "./shared/portless.js";
17
- export type FusionTool = "codex" | "claude" | "cursor" | "serve";
13
+ *
14
+ * This module is the run orchestrator; the supporting pieces live in `./fusion/`
15
+ * (env + defaults, the observability dashboard, the model stack, and preflight)
16
+ * and are re-exported here so existing import paths keep working.
17
+ */
18
+ import type { FusionTool, RunFusionOptions } from "./fusion/env.js";
19
+ export * from "./fusion/env.js";
20
+ export * from "./fusion/observability.js";
21
+ export * from "./fusion/stack.js";
22
+ export * from "./fusion/preflight.js";
23
+ /** Launchable fusion tools (registry-derived) plus the `serve` pseudo-tool. */
18
24
  export declare const FUSION_TOOLS: readonly FusionTool[];
19
- export type PanelProvider = "mlx" | "openai" | "anthropic" | "google" | "openai-compatible";
20
- /**
21
- * One panel model. `mlx` models run locally via the in-repo provisioner; cloud
22
- * providers (openai/anthropic/google/openai-compatible) are fronted as
23
- * OpenAI-compatible endpoints by FusionKit's `serve-endpoint` command, run via
24
- * `uvx fusionkit` (no checkout required).
25
- */
26
- export type PanelModelSpec = {
27
- id: string;
28
- model: string;
29
- provider?: PanelProvider;
30
- baseUrl?: string;
31
- keyEnv?: string;
32
- };
33
- /**
34
- * The PyPI version of the `fusionkit` Python distribution that provides the
35
- * synthesizer (`fusionkit serve`) and the single-model OpenAI shim
36
- * (`fusionkit serve-endpoint`). Pinned so `uvx` resolves a reproducible build.
37
- */
38
- export declare const FUSIONKIT_PYPI_VERSION = "0.2.0";
39
- /**
40
- * Default cloud panel — works cross-platform with only `OPENAI_API_KEY` and
41
- * `ANTHROPIC_API_KEY` set. The judge defaults to the first entry.
42
- */
43
- export declare const DEFAULT_CLOUD_PANEL: readonly PanelModelSpec[];
44
- /** The locally cached MLX trio (Apple Silicon only) used behind `--local`. */
45
- export declare const DEFAULT_TRIO: readonly PanelModelSpec[];
46
- /**
47
- * How to invoke the `fusionkit` Python CLI: from PyPI via `uvx` by default
48
- * (no checkout), or from a local checkout via `uv run` when `fusionkitDir` is
49
- * given (a dev override). Returns the command plus the argv prefix that
50
- * precedes the subcommand (`serve`, `serve-endpoint`, ...).
51
- */
52
- export declare function fusionkitPyCommand(fusionkitDir?: string): {
53
- command: string;
54
- prefix: string[];
55
- cwd?: string;
56
- };
57
- /**
58
- * Parse a `.env` file (KEY=VALUE lines, `#` comments, optional `export`,
59
- * single/double quotes) and fill any keys not already present in `env`.
60
- * Existing env values win, so an explicitly exported key is never overridden.
61
- */
62
- export declare function loadEnvFileInto(path: string, env: Record<string, string | undefined>): void;
63
- /** Default env var holding the API key for each cloud provider. */
64
- export declare function defaultKeyEnv(provider: PanelProvider): string | undefined;
65
- /** The git repository root containing `dir`, or undefined if it is not in a repo. */
66
- export declare function gitToplevel(dir: string): string | undefined;
67
- /**
68
- * Compute the binaries and API keys the run requires given the tool, panel, and
69
- * options. Pre-running endpoints (`--model-endpoint`) and a pre-running
70
- * `--synthesis-url` drop the corresponding requirements.
71
- */
72
- export declare function preflightRequirements(tool: FusionTool, models: PanelModelSpec[], options: RunFusionOptions): {
73
- requiredBins: string[];
74
- requiredEnv: string[];
75
- };
76
- /** Fixed port for the local observability dashboard (the scope app). */
77
- export declare const SCOPE_DASHBOARD_PORT = 4317;
78
- /**
79
- * Locate the isolated scope dashboard app (handoffkit/apps/scope) by walking up
80
- * from this module. Works from both the compiled dist and src layouts. Only the
81
- * monorepo dev fallback uses this — published installs ship a prebuilt bundle.
82
- */
83
- export declare function findScopeAppDir(): string;
84
- /**
85
- * Path to the prebuilt, self-contained dashboard server staged into the CLI
86
- * package (`scope/server.js`, a sibling of `dist/`), or undefined when it is
87
- * absent — i.e. a monorepo dev checkout where the bundle was never staged. Both
88
- * the compiled `dist/fusion-quickstart.js` and the `src/` layout resolve to the
89
- * same `<cli-package>/scope/server.js`.
90
- */
91
- export declare function bundledScopeServer(): string | undefined;
92
- export type Observability = {
93
- url: string;
94
- ingestUrl: string;
95
- traceDir: string;
96
- close: () => Promise<void>;
97
- };
98
- export declare function startObservability(input: {
99
- log: (line: string) => void;
100
- logFile?: string;
101
- report?: StackReporter;
102
- portless: PortlessSession;
103
- }): Promise<Observability>;
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;
115
- endpoints: Record<string, string>;
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;
121
- close: () => Promise<void>;
122
- };
123
- /**
124
- * Structured boot progress. When a reporter is supplied the stack emits these
125
- * events instead of the plain `fusion: ...` log lines, so a live TUI (or any
126
- * other consumer) can render per-stage status. Without one, callers keep getting
127
- * the existing line logs.
128
- */
129
- export type StackEvent = {
130
- kind: "server.start";
131
- id: string;
132
- label: string;
133
- } | {
134
- kind: "server.ready";
135
- id: string;
136
- detail: string;
137
- } | {
138
- kind: "server.fail";
139
- id: string;
140
- detail: string;
141
- } | {
142
- kind: "synth.start";
143
- } | {
144
- kind: "synth.ready";
145
- detail: string;
146
- } | {
147
- kind: "gateway.start";
148
- } | {
149
- kind: "gateway.ready";
150
- detail: string;
151
- } | {
152
- kind: "dashboard.start";
153
- } | {
154
- kind: "dashboard.ready";
155
- detail: string;
156
- } | {
157
- kind: "dashboard.fail";
158
- detail: string;
159
- };
160
- export type StackReporter = (event: StackEvent) => void;
161
- /**
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.
167
- */
168
- export declare function startRouter(options: {
169
- specs: PanelModelSpec[];
170
- judgeModel?: string;
171
- fusionkitDir?: string;
172
- logsDir?: string;
173
- report?: StackReporter;
174
- log: (line: string) => void;
175
- }): Promise<Router>;
176
- export type FusionStack = {
177
- fusionUrl: string;
178
- endpoints: Record<string, string>;
179
- close: () => Promise<void>;
180
- };
181
- export type StartFusionStackOptions = {
182
- repo: string;
183
- outputRoot: string;
184
- models: PanelModelSpec[];
185
- endpoints?: Record<string, string>;
186
- fusionkitDir?: string;
187
- judgeModel?: string;
188
- /** Pre-running fusionkit serve URL for trajectory synthesis (skips spawn). */
189
- synthesisUrl?: string;
190
- host?: string;
191
- port?: number;
192
- authToken?: string;
193
- timeoutMs?: number;
194
- logsDir?: string;
195
- report?: StackReporter;
196
- /** Active portless session; defaults to a disabled (loopback) session. */
197
- portless?: PortlessSession;
198
- log: (line: string) => void;
199
- };
200
- export declare function startFusionStack(options: StartFusionStackOptions): Promise<FusionStack>;
201
- /**
202
- * Start the Cursorkit bridge with its local-model backend pointed at the fusion
203
- * gateway, and resolve once it is listening. Returns the child and its port.
204
- */
205
- export declare function startCursorBridge(input: {
206
- cursorKitDir: string;
207
- fusionUrl: string;
208
- logFile?: string;
209
- caCertPath?: string;
210
- log: (line: string) => void;
211
- }): Promise<{
212
- child: ChildProcess;
213
- port: number;
214
- }>;
215
- export type RunFusionOptions = {
216
- models?: PanelModelSpec[];
217
- endpoints?: Record<string, string>;
218
- fusionkitDir?: string;
219
- repo?: string;
220
- judgeModel?: string;
221
- synthesisUrl?: string;
222
- cursorKitDir?: string;
223
- authToken?: string;
224
- port?: number;
225
- timeoutMs?: number;
226
- /** Use the local MLX panel trio (Apple Silicon) instead of the cloud panel. */
227
- local?: boolean;
228
- /** Boot the local scope dashboard and stream trace events into it. */
229
- observe?: boolean;
230
- /** Skip the interactive cost/scope confirmation for the cloud panel. */
231
- yes?: boolean;
232
- /** Route services through portless (stable named URLs + singletons). Default on. */
233
- portless?: boolean;
234
- log?: (line: string) => void;
235
- };
236
25
  /** Whether portless is enabled: explicit flag/config wins, else on unless PORTLESS=0. */
237
26
  export declare function portlessEnabled(options: RunFusionOptions): boolean;
238
27
  export declare function runFusion(tool: FusionTool, toolArgs: string[], options?: RunFusionOptions): Promise<number>;