@fusionkit/cli 0.1.6 → 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.
- package/README.md +8 -6
- package/dist/commands/doctor.js +7 -3
- package/dist/commands/fusion.js +16 -8
- package/dist/fusion/env.d.ts +4 -1
- package/dist/fusion/env.js +1 -1
- package/dist/fusion/stack.d.ts +4 -0
- package/dist/fusion/stack.js +21 -1
- package/dist/fusion-config.d.ts +58 -6
- package/dist/fusion-config.js +152 -22
- package/dist/fusion-init.d.ts +1 -0
- package/dist/fusion-init.js +48 -4
- package/dist/fusion-quickstart.js +1 -0
- package/dist/test/cli.test.js +3 -3
- package/dist/test/fusion-config.test.js +64 -4
- package/package.json +14 -14
- package/scope/.next/BUILD_ID +1 -1
- package/scope/.next/app-build-manifest.json +9 -9
- package/scope/.next/app-path-routes-manifest.json +2 -2
- package/scope/.next/build-manifest.json +2 -2
- package/scope/.next/prerender-manifest.json +10 -10
- 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/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.html +1 -1
- package/scope/.next/server/app/models.rsc +1 -1
- package/scope/.next/server/app-paths-manifest.json +2 -2
- 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/.next/static/{x7wPUCpgS31-5ZHJkcKsU → BrrtQvnEIgv-OVkaeanKI}/_buildManifest.js +0 -0
- /package/scope/.next/static/{x7wPUCpgS31-5ZHJkcKsU → BrrtQvnEIgv-OVkaeanKI}/_ssgManifest.js +0 -0
package/README.md
CHANGED
|
@@ -44,23 +44,25 @@ cloud panel (skip with `--yes`). Use `--local` for the on-device MLX panel, or
|
|
|
44
44
|
|
|
45
45
|
## Per-repo config
|
|
46
46
|
|
|
47
|
-
Tired of long flag lines? Scaffold a committed
|
|
47
|
+
Tired of long flag lines? Scaffold a committed `.fusionkit/` folder:
|
|
48
48
|
|
|
49
49
|
```bash
|
|
50
50
|
fusionkit init
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
It
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
It writes `.fusionkit/fusion.json` (the panel, judge, default tool, and run
|
|
54
|
+
defaults) plus editable system-prompt overrides in `.fusionkit/prompts/*.md`, so
|
|
55
|
+
the whole team can just run `fusionkit codex`. Only env-var *names* for keys are
|
|
56
|
+
stored, never secrets. Explicit CLI flags always override the folder. A legacy
|
|
57
|
+
`fusionkit.json` is auto-migrated on first run. Inspect the effective config and
|
|
58
|
+
a dry-run preview with `fusionkit status`.
|
|
57
59
|
|
|
58
60
|
## Commands
|
|
59
61
|
|
|
60
62
|
- `fusionkit codex | claude | cursor` — launch that agent backed by the panel.
|
|
61
63
|
- `fusionkit serve` — just run the gateway and print setup snippets for any tool.
|
|
62
64
|
- `fusionkit fusion [tool]` — the generic launcher (interactive picker on a TTY).
|
|
63
|
-
- `fusionkit init` — scaffold
|
|
65
|
+
- `fusionkit init` — scaffold the committed `.fusionkit/` folder for this repo.
|
|
64
66
|
- `fusionkit doctor` — check prerequisites with fix hints.
|
|
65
67
|
- `fusionkit status` — show the effective config and what a run will do.
|
|
66
68
|
|
package/dist/commands/doctor.js
CHANGED
|
@@ -56,13 +56,15 @@ function runDoctor() {
|
|
|
56
56
|
console.log("");
|
|
57
57
|
console.log(bold("repo config"));
|
|
58
58
|
try {
|
|
59
|
-
const config = loadFusionConfig(repoRoot);
|
|
59
|
+
const config = loadFusionConfig(repoRoot, (message) => console.log(` ${gray(glyph.bullet())} ${dim(message)}`));
|
|
60
60
|
if (config === undefined) {
|
|
61
|
-
console.log(` ${gray(glyph.bullet())} no ${cyan("fusionkit
|
|
61
|
+
console.log(` ${gray(glyph.bullet())} no ${cyan(".fusionkit/")} yet — run ${bold("fusionkit fusion init")}`);
|
|
62
62
|
}
|
|
63
63
|
else {
|
|
64
|
+
const overrides = Object.keys(config.prompts ?? {});
|
|
64
65
|
console.log(` ${green(glyph.tick())} ${cyan(fusionConfigPath(repoRoot))}`);
|
|
65
66
|
console.log(` ${dim(`tool: ${config.tool ?? "(unset)"} panel: ${(config.panel ?? []).map((s) => s.id).join(", ") || "(unset)"}`)}`);
|
|
67
|
+
console.log(` ${dim(`prompt overrides: ${overrides.length > 0 ? overrides.join(", ") : "(none — built-in defaults)"}`)}`);
|
|
66
68
|
}
|
|
67
69
|
}
|
|
68
70
|
catch (error) {
|
|
@@ -94,7 +96,7 @@ function runStatus() {
|
|
|
94
96
|
}
|
|
95
97
|
let config;
|
|
96
98
|
try {
|
|
97
|
-
config = loadFusionConfig(repoRoot);
|
|
99
|
+
config = loadFusionConfig(repoRoot, (message) => console.log(dim(message)));
|
|
98
100
|
}
|
|
99
101
|
catch (error) {
|
|
100
102
|
console.log(`${red("config error:")} ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -110,6 +112,8 @@ function runStatus() {
|
|
|
110
112
|
console.log(`${dim("tool:")} ${bold(tool)}`);
|
|
111
113
|
console.log(`${dim("judge:")} ${judge}`);
|
|
112
114
|
console.log(`${dim("observe:")} ${config?.observe === true ? "on" : "off"}`);
|
|
115
|
+
const overrides = Object.keys(config?.prompts ?? {});
|
|
116
|
+
console.log(`${dim("prompts:")} ${overrides.length > 0 ? overrides.join(", ") : dim("(built-in defaults)")}`);
|
|
113
117
|
console.log(bold("\npanel"));
|
|
114
118
|
for (const spec of panel)
|
|
115
119
|
console.log(` ${glyph.bullet()} ${panelLabel(spec)}`);
|
package/dist/commands/fusion.js
CHANGED
|
@@ -17,9 +17,9 @@ function applyFusionOptions(command) {
|
|
|
17
17
|
.option("--fusionkit-dir <dir>", "local FusionKit checkout (dev override for the uvx synthesizer)")
|
|
18
18
|
.option("--repo <dir>", "coding workspace the panel fuses over")
|
|
19
19
|
.option("--local", "use the local MLX panel trio instead of the default cloud panel")
|
|
20
|
-
.option("--no-local", "override a fusionkit
|
|
20
|
+
.option("--no-local", "override a .fusionkit default of local=true")
|
|
21
21
|
.option("--observe", "boot the local scope dashboard and stream live trace events")
|
|
22
|
-
.option("--no-observe", "override a fusionkit
|
|
22
|
+
.option("--no-observe", "override a .fusionkit default of observe=true")
|
|
23
23
|
.option("--yes", "skip the interactive cloud-panel cost confirmation")
|
|
24
24
|
.option("--auth-token <token>", "require a bearer token on the gateway")
|
|
25
25
|
.option("--port <n>", "gateway port (default: ephemeral)")
|
|
@@ -98,6 +98,8 @@ function mergeConfig(options, config) {
|
|
|
98
98
|
options.portless = config.portless;
|
|
99
99
|
if (options.port === undefined && config.port != null)
|
|
100
100
|
options.port = config.port;
|
|
101
|
+
if (options.prompts === undefined && config.prompts !== undefined)
|
|
102
|
+
options.prompts = config.prompts;
|
|
101
103
|
}
|
|
102
104
|
/** The repo root used for config lookup: --repo if given, else the cwd's git root. */
|
|
103
105
|
function configRepoRoot(options) {
|
|
@@ -124,15 +126,21 @@ function resolveContext(opts) {
|
|
|
124
126
|
return { options, ...(config?.tool !== undefined ? { configTool: config.tool } : {}) };
|
|
125
127
|
}
|
|
126
128
|
export function registerFusion(program) {
|
|
127
|
-
// Top-level `init` — scaffold a committed fusionkit
|
|
129
|
+
// Top-level `init` — scaffold a committed .fusionkit/ folder for this repo.
|
|
128
130
|
program
|
|
129
131
|
.command("init")
|
|
130
|
-
.description("scaffold a committed fusionkit
|
|
132
|
+
.description("scaffold a committed .fusionkit/ folder for this repo")
|
|
131
133
|
.option("--repo <dir>", "coding workspace the panel fuses over")
|
|
132
|
-
.option("--
|
|
134
|
+
.option("--fusionkit-dir <dir>", "local FusionKit checkout (dev override for default prompts)")
|
|
135
|
+
.option("--force", "overwrite an existing .fusionkit/ config and prompts")
|
|
133
136
|
.action(async (opts) => {
|
|
134
|
-
const
|
|
135
|
-
const
|
|
137
|
+
const options = resolveOptions(opts);
|
|
138
|
+
const repoRoot = configRepoRoot(options);
|
|
139
|
+
const code = await runFusionInit({
|
|
140
|
+
repoRoot,
|
|
141
|
+
force: opts.force === true,
|
|
142
|
+
...(options.fusionkitDir !== undefined ? { fusionkitDir: options.fusionkitDir } : {})
|
|
143
|
+
});
|
|
136
144
|
process.exit(code);
|
|
137
145
|
});
|
|
138
146
|
// Generic `fusion [tool]` — keeps the original surface and interactive pick.
|
|
@@ -143,7 +151,7 @@ export function registerFusion(program) {
|
|
|
143
151
|
.argument("[args...]", "arguments forwarded to the tool")
|
|
144
152
|
.option("--tool <tool>", `coding agent to launch (${FUSION_TOOLS.join(" | ")})`))
|
|
145
153
|
.addHelpText("after", "\nfusionkit's own flags must precede the tool name; everything after the tool is forwarded to it." +
|
|
146
|
-
"\nRun `fusionkit init` to scaffold a committed fusionkit
|
|
154
|
+
"\nRun `fusionkit init` to scaffold a committed .fusionkit/ folder for this repo." +
|
|
147
155
|
"\nRun `fusionkit fusion stop` to reap portless singleton services (router, dashboard, ...).")
|
|
148
156
|
.action(async (positionalTool, args, opts) => {
|
|
149
157
|
// `fusion stop` reaps persistent portless singletons left running by prior
|
package/dist/fusion/env.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PromptOverrides } from "../fusion-config.js";
|
|
1
2
|
/** A launchable tool id from the registry, or the `serve` pseudo-tool. */
|
|
2
3
|
export type FusionTool = string;
|
|
3
4
|
export type PanelProvider = "mlx" | "openai" | "anthropic" | "google" | "openai-compatible";
|
|
@@ -32,6 +33,8 @@ export type RunFusionOptions = {
|
|
|
32
33
|
yes?: boolean;
|
|
33
34
|
/** Route services through portless (stable named URLs + singletons). Default on. */
|
|
34
35
|
portless?: boolean;
|
|
36
|
+
/** System-prompt overrides forwarded to the synthesizer's router config. */
|
|
37
|
+
prompts?: PromptOverrides;
|
|
35
38
|
log?: (line: string) => void;
|
|
36
39
|
};
|
|
37
40
|
/**
|
|
@@ -77,7 +80,7 @@ export type StackReporter = (event: StackEvent) => void;
|
|
|
77
80
|
* synthesizer (`fusionkit serve`) and the single-model OpenAI shim
|
|
78
81
|
* (`fusionkit serve-endpoint`). Pinned so `uvx` resolves a reproducible build.
|
|
79
82
|
*/
|
|
80
|
-
export declare const FUSIONKIT_PYPI_VERSION = "0.
|
|
83
|
+
export declare const FUSIONKIT_PYPI_VERSION = "0.3.0";
|
|
81
84
|
/**
|
|
82
85
|
* Default cloud panel — works cross-platform with only `OPENAI_API_KEY` and
|
|
83
86
|
* `ANTHROPIC_API_KEY` set. The judge defaults to the first entry.
|
package/dist/fusion/env.js
CHANGED
|
@@ -10,7 +10,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
10
10
|
* synthesizer (`fusionkit serve`) and the single-model OpenAI shim
|
|
11
11
|
* (`fusionkit serve-endpoint`). Pinned so `uvx` resolves a reproducible build.
|
|
12
12
|
*/
|
|
13
|
-
export const FUSIONKIT_PYPI_VERSION = "0.
|
|
13
|
+
export const FUSIONKIT_PYPI_VERSION = "0.3.0";
|
|
14
14
|
/**
|
|
15
15
|
* Default cloud panel — works cross-platform with only `OPENAI_API_KEY` and
|
|
16
16
|
* `ANTHROPIC_API_KEY` set. The judge defaults to the first entry.
|
package/dist/fusion/stack.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { EnsembleModel } from "@fusionkit/ensemble";
|
|
2
2
|
import type { PortlessSession } from "../shared/portless.js";
|
|
3
|
+
import type { PromptOverrides } from "../fusion-config.js";
|
|
3
4
|
import type { PanelModelSpec, StackReporter } from "./env.js";
|
|
4
5
|
/**
|
|
5
6
|
* The single `fusionkit serve` router: one process that fronts every panel
|
|
@@ -31,6 +32,7 @@ export declare function startRouter(options: {
|
|
|
31
32
|
specs: PanelModelSpec[];
|
|
32
33
|
judgeModel?: string;
|
|
33
34
|
fusionkitDir?: string;
|
|
35
|
+
prompts?: PromptOverrides;
|
|
34
36
|
logsDir?: string;
|
|
35
37
|
report?: StackReporter;
|
|
36
38
|
log: (line: string) => void;
|
|
@@ -46,6 +48,8 @@ export type StartFusionStackOptions = {
|
|
|
46
48
|
models: PanelModelSpec[];
|
|
47
49
|
endpoints?: Record<string, string>;
|
|
48
50
|
fusionkitDir?: string;
|
|
51
|
+
/** System-prompt overrides emitted into the router's synthesizer config. */
|
|
52
|
+
prompts?: PromptOverrides;
|
|
49
53
|
judgeModel?: string;
|
|
50
54
|
/** Pre-running fusionkit serve URL for trajectory synthesis (skips spawn). */
|
|
51
55
|
synthesisUrl?: string;
|
package/dist/fusion/stack.js
CHANGED
|
@@ -10,6 +10,7 @@ import { MlxBackend, startGateway } from "@fusionkit/model-gateway";
|
|
|
10
10
|
import { startFusionStepGateway } from "../gateway.js";
|
|
11
11
|
import { createPortlessSession } from "../shared/portless.js";
|
|
12
12
|
import { freePort, spawnLogged, terminate, waitForHttp } from "../shared/proc.js";
|
|
13
|
+
import { PROMPT_CONFIG_KEY, PROMPT_IDS } from "../fusion-config.js";
|
|
13
14
|
import { defaultKeyEnv, fusionkitPyCommand, loadEnvFileInto } from "./env.js";
|
|
14
15
|
/**
|
|
15
16
|
* Heuristic: does the captured output indicate a permanent failure (bad key,
|
|
@@ -85,6 +86,19 @@ function routerConfigYaml(input) {
|
|
|
85
86
|
// Generous budget: reasoning models (gpt-5.x) spend tokens on reasoning before
|
|
86
87
|
// producing content, so a small cap can yield an empty answer.
|
|
87
88
|
lines.push("sampling: {temperature: 0.2, top_p: 0.9, max_tokens: 8192}");
|
|
89
|
+
// Committed `.fusionkit/prompts/*.md` overrides flow into the synthesizer's
|
|
90
|
+
// PromptOverrides here. JSON.stringify yields a valid YAML double-quoted
|
|
91
|
+
// scalar, so multi-line prompts are escaped safely.
|
|
92
|
+
const promptEntries = PROMPT_IDS.flatMap((id) => {
|
|
93
|
+
const value = input.prompts?.[id];
|
|
94
|
+
return value !== undefined ? [[PROMPT_CONFIG_KEY[id], value]] : [];
|
|
95
|
+
});
|
|
96
|
+
if (promptEntries.length > 0) {
|
|
97
|
+
lines.push("prompts:");
|
|
98
|
+
for (const [key, value] of promptEntries) {
|
|
99
|
+
lines.push(` ${key}: ${JSON.stringify(value)}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
88
102
|
lines.push("");
|
|
89
103
|
return lines.join("\n");
|
|
90
104
|
}
|
|
@@ -142,7 +156,12 @@ export async function startRouter(options) {
|
|
|
142
156
|
gateways.push(gateway);
|
|
143
157
|
mlxUrls[spec.id] = gateway.url();
|
|
144
158
|
}
|
|
145
|
-
const config = routerConfigYaml({
|
|
159
|
+
const config = routerConfigYaml({
|
|
160
|
+
specs,
|
|
161
|
+
mlxUrls,
|
|
162
|
+
judgeId: judgeSpec.id,
|
|
163
|
+
...(options.prompts !== undefined ? { prompts: options.prompts } : {})
|
|
164
|
+
});
|
|
146
165
|
const configDir = mkdtempSync(join(tmpdir(), "fusion-router-"));
|
|
147
166
|
const configPath = join(configDir, "router.yaml");
|
|
148
167
|
writeFileSync(configPath, config);
|
|
@@ -235,6 +254,7 @@ export async function startFusionStack(options) {
|
|
|
235
254
|
specs: options.models,
|
|
236
255
|
...(options.judgeModel !== undefined ? { judgeModel: options.judgeModel } : {}),
|
|
237
256
|
...(options.fusionkitDir !== undefined ? { fusionkitDir: options.fusionkitDir } : {}),
|
|
257
|
+
...(options.prompts !== undefined ? { prompts: options.prompts } : {}),
|
|
238
258
|
...(options.logsDir !== undefined ? { logsDir: options.logsDir } : {}),
|
|
239
259
|
...(report !== undefined ? { report } : {}),
|
|
240
260
|
log: options.log
|
package/dist/fusion-config.d.ts
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import type { FusionTool, PanelModelSpec } from "./fusion-quickstart.js";
|
|
2
|
+
export declare const FUSION_CONFIG_DIRNAME = ".fusionkit";
|
|
3
|
+
export declare const FUSION_CONFIG_BASENAME = "fusion.json";
|
|
4
|
+
export declare const FUSION_PROMPTS_DIRNAME = "prompts";
|
|
5
|
+
/** Legacy single-file config at the repo root (pre-`.fusionkit/`). */
|
|
2
6
|
export declare const FUSION_CONFIG_FILENAME = "fusionkit.json";
|
|
3
|
-
export declare const FUSION_CONFIG_VERSION = "fusionkit.fusion.
|
|
7
|
+
export declare const FUSION_CONFIG_VERSION = "fusionkit.fusion.v2";
|
|
8
|
+
/**
|
|
9
|
+
* The committable system-prompt override ids. Each maps to a
|
|
10
|
+
* `.fusionkit/prompts/<id>.md` file and to a `FusionConfig.prompts` key in the
|
|
11
|
+
* Python synthesizer (see {@link PROMPT_CONFIG_KEY}).
|
|
12
|
+
*/
|
|
13
|
+
export declare const PROMPT_IDS: readonly ["judge", "synthesizer", "trajectory-synthesizer", "trajectory-step", "verifier", "panel"];
|
|
14
|
+
export type PromptId = (typeof PROMPT_IDS)[number];
|
|
15
|
+
/** Map each prompt override id to the `prompts:` key fusionkit's config expects. */
|
|
16
|
+
export declare const PROMPT_CONFIG_KEY: Record<PromptId, string>;
|
|
17
|
+
export type PromptOverrides = Partial<Record<PromptId, string>>;
|
|
4
18
|
export type FusionConfig = {
|
|
5
19
|
version: typeof FUSION_CONFIG_VERSION;
|
|
6
20
|
tool?: FusionTool;
|
|
@@ -10,19 +24,57 @@ export type FusionConfig = {
|
|
|
10
24
|
observe?: boolean;
|
|
11
25
|
portless?: boolean;
|
|
12
26
|
port?: number | null;
|
|
27
|
+
/**
|
|
28
|
+
* System-prompt overrides, loaded from `.fusionkit/prompts/*.md`. Not stored
|
|
29
|
+
* inline in `config.json` — it is hydrated from the prompt files on load.
|
|
30
|
+
*/
|
|
31
|
+
prompts?: PromptOverrides;
|
|
13
32
|
};
|
|
14
33
|
export declare class FusionConfigError extends Error {
|
|
15
34
|
constructor(message: string);
|
|
16
35
|
}
|
|
36
|
+
/** The `.fusionkit/` directory at the repo root. */
|
|
37
|
+
export declare function fusionConfigDir(repoRoot: string): string;
|
|
38
|
+
/** The `.fusionkit/fusion.json` settings file. */
|
|
17
39
|
export declare function fusionConfigPath(repoRoot: string): string;
|
|
18
|
-
/**
|
|
40
|
+
/** The legacy `fusionkit.json` at the repo root (pre-`.fusionkit/`). */
|
|
41
|
+
export declare function legacyFusionConfigPath(repoRoot: string): string;
|
|
42
|
+
/** The `.fusionkit/prompts/` directory holding the override files. */
|
|
43
|
+
export declare function fusionPromptsDir(repoRoot: string): string;
|
|
44
|
+
/** The `.fusionkit/prompts/<id>.md` file for a single prompt override. */
|
|
45
|
+
export declare function fusionPromptPath(repoRoot: string, id: PromptId): string;
|
|
46
|
+
/**
|
|
47
|
+
* Validate a parsed settings object as a {@link FusionConfig}, throwing on any
|
|
48
|
+
* problem. Prompt overrides are loaded separately from `.fusionkit/prompts/`,
|
|
49
|
+
* not from this object. A `v1` version is accepted and upgraded to `v2`.
|
|
50
|
+
*/
|
|
19
51
|
export declare function parseFusionConfig(raw: unknown, source: string): FusionConfig;
|
|
20
52
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
53
|
+
* Read the committed prompt overrides from `.fusionkit/prompts/*.md`. Only files
|
|
54
|
+
* that exist and are non-empty (after trimming) become overrides.
|
|
55
|
+
*/
|
|
56
|
+
export declare function readFusionPrompts(repoRoot: string): PromptOverrides;
|
|
57
|
+
/**
|
|
58
|
+
* Load the per-repo config. Prefers `.fusionkit/config.json`; if it is absent
|
|
59
|
+
* but a legacy `fusionkit.json` exists, auto-migrates it into the folder (the
|
|
60
|
+
* original is left intact) and loads from there. Returns `undefined` when no
|
|
61
|
+
* config exists; throws {@link FusionConfigError} on malformed content.
|
|
62
|
+
*
|
|
63
|
+
* `onNotice` receives a one-line message when a migration happens.
|
|
64
|
+
*/
|
|
65
|
+
export declare function loadFusionConfig(repoRoot: string, onNotice?: (message: string) => void): FusionConfig | undefined;
|
|
66
|
+
/**
|
|
67
|
+
* Write `.fusionkit/config.json` (creating the folder), refusing to clobber
|
|
68
|
+
* unless `force`. Prompt overrides are stored as files, not inline, so any
|
|
69
|
+
* `prompts` on the config object is omitted here.
|
|
23
70
|
*/
|
|
24
|
-
export declare function loadFusionConfig(repoRoot: string): FusionConfig | undefined;
|
|
25
|
-
/** Write `fusionkit.json` at the repo root, refusing to clobber unless `force`. */
|
|
26
71
|
export declare function writeFusionConfig(repoRoot: string, config: FusionConfig, options?: {
|
|
27
72
|
force?: boolean;
|
|
28
73
|
}): string;
|
|
74
|
+
/**
|
|
75
|
+
* Write prompt override files into `.fusionkit/prompts/`. Existing files are
|
|
76
|
+
* left untouched unless `force`. Returns the paths actually written.
|
|
77
|
+
*/
|
|
78
|
+
export declare function writeFusionPrompts(repoRoot: string, prompts: PromptOverrides, options?: {
|
|
79
|
+
force?: boolean;
|
|
80
|
+
}): string[];
|
package/dist/fusion-config.js
CHANGED
|
@@ -1,29 +1,85 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Per-repo fusion configuration
|
|
2
|
+
* Per-repo fusion configuration, stored in a committed `.fusionkit/` folder at
|
|
3
|
+
* the repo root:
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* (`keyEnv`), never the secret values.
|
|
5
|
+
* .fusionkit/
|
|
6
|
+
* fusion.json - all settings (panel, judge, default tool, run defaults)
|
|
7
|
+
* prompts/<id>.md - optional system-prompt overrides (one file per prompt)
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* The folder is safe to commit: it stores only the env-var *names* that hold API
|
|
10
|
+
* keys (`keyEnv`), never the secret values. A prompt file that exists and is
|
|
11
|
+
* non-empty overrides the matching built-in synthesizer prompt; absent/empty
|
|
12
|
+
* falls back to the built-in default.
|
|
13
|
+
*
|
|
14
|
+
* Precedence at run time is: explicit CLI flags > .fusionkit > built-in
|
|
15
|
+
* defaults. CLI flags always win, so the folder is a default layer, not a lock.
|
|
16
|
+
*
|
|
17
|
+
* Legacy `fusionkit.json` files at the repo root are auto-migrated into
|
|
18
|
+
* `.fusionkit/fusion.json` on first load (the original is left intact as a
|
|
19
|
+
* back-compat fallback).
|
|
11
20
|
*/
|
|
12
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
22
|
import { join } from "node:path";
|
|
14
23
|
import { FUSION_TOOLS } from "./fusion-quickstart.js";
|
|
15
24
|
import { PANEL_PROVIDERS } from "./shared/options.js";
|
|
25
|
+
export const FUSION_CONFIG_DIRNAME = ".fusionkit";
|
|
26
|
+
// `fusion.json` (not `config.json`) so the fusion settings never collide with
|
|
27
|
+
// the plane home's `.fusionkit/config.json` (`warrant.config.v2`).
|
|
28
|
+
export const FUSION_CONFIG_BASENAME = "fusion.json";
|
|
29
|
+
export const FUSION_PROMPTS_DIRNAME = "prompts";
|
|
30
|
+
/** Legacy single-file config at the repo root (pre-`.fusionkit/`). */
|
|
16
31
|
export const FUSION_CONFIG_FILENAME = "fusionkit.json";
|
|
17
|
-
export const FUSION_CONFIG_VERSION = "fusionkit.fusion.
|
|
32
|
+
export const FUSION_CONFIG_VERSION = "fusionkit.fusion.v2";
|
|
33
|
+
/** Versions `parseFusionConfig` will load; `v1` is upgraded to `v2` in memory. */
|
|
34
|
+
const SUPPORTED_CONFIG_VERSIONS = ["fusionkit.fusion.v1", "fusionkit.fusion.v2"];
|
|
35
|
+
/**
|
|
36
|
+
* The committable system-prompt override ids. Each maps to a
|
|
37
|
+
* `.fusionkit/prompts/<id>.md` file and to a `FusionConfig.prompts` key in the
|
|
38
|
+
* Python synthesizer (see {@link PROMPT_CONFIG_KEY}).
|
|
39
|
+
*/
|
|
40
|
+
export const PROMPT_IDS = [
|
|
41
|
+
"judge",
|
|
42
|
+
"synthesizer",
|
|
43
|
+
"trajectory-synthesizer",
|
|
44
|
+
"trajectory-step",
|
|
45
|
+
"verifier",
|
|
46
|
+
"panel"
|
|
47
|
+
];
|
|
48
|
+
/** Map each prompt override id to the `prompts:` key fusionkit's config expects. */
|
|
49
|
+
export const PROMPT_CONFIG_KEY = {
|
|
50
|
+
judge: "judge_system",
|
|
51
|
+
synthesizer: "synthesizer_system",
|
|
52
|
+
"trajectory-synthesizer": "trajectory_synthesizer_system",
|
|
53
|
+
"trajectory-step": "trajectory_step_system",
|
|
54
|
+
verifier: "verifier_system",
|
|
55
|
+
panel: "panel_system"
|
|
56
|
+
};
|
|
18
57
|
export class FusionConfigError extends Error {
|
|
19
58
|
constructor(message) {
|
|
20
59
|
super(message);
|
|
21
60
|
this.name = "FusionConfigError";
|
|
22
61
|
}
|
|
23
62
|
}
|
|
63
|
+
/** The `.fusionkit/` directory at the repo root. */
|
|
64
|
+
export function fusionConfigDir(repoRoot) {
|
|
65
|
+
return join(repoRoot, FUSION_CONFIG_DIRNAME);
|
|
66
|
+
}
|
|
67
|
+
/** The `.fusionkit/fusion.json` settings file. */
|
|
24
68
|
export function fusionConfigPath(repoRoot) {
|
|
69
|
+
return join(fusionConfigDir(repoRoot), FUSION_CONFIG_BASENAME);
|
|
70
|
+
}
|
|
71
|
+
/** The legacy `fusionkit.json` at the repo root (pre-`.fusionkit/`). */
|
|
72
|
+
export function legacyFusionConfigPath(repoRoot) {
|
|
25
73
|
return join(repoRoot, FUSION_CONFIG_FILENAME);
|
|
26
74
|
}
|
|
75
|
+
/** The `.fusionkit/prompts/` directory holding the override files. */
|
|
76
|
+
export function fusionPromptsDir(repoRoot) {
|
|
77
|
+
return join(fusionConfigDir(repoRoot), FUSION_PROMPTS_DIRNAME);
|
|
78
|
+
}
|
|
79
|
+
/** The `.fusionkit/prompts/<id>.md` file for a single prompt override. */
|
|
80
|
+
export function fusionPromptPath(repoRoot, id) {
|
|
81
|
+
return join(fusionPromptsDir(repoRoot), `${id}.md`);
|
|
82
|
+
}
|
|
27
83
|
function isRecord(value) {
|
|
28
84
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
29
85
|
}
|
|
@@ -57,12 +113,16 @@ function validatePanelEntry(entry, index) {
|
|
|
57
113
|
}
|
|
58
114
|
return spec;
|
|
59
115
|
}
|
|
60
|
-
/**
|
|
116
|
+
/**
|
|
117
|
+
* Validate a parsed settings object as a {@link FusionConfig}, throwing on any
|
|
118
|
+
* problem. Prompt overrides are loaded separately from `.fusionkit/prompts/`,
|
|
119
|
+
* not from this object. A `v1` version is accepted and upgraded to `v2`.
|
|
120
|
+
*/
|
|
61
121
|
export function parseFusionConfig(raw, source) {
|
|
62
122
|
if (!isRecord(raw))
|
|
63
123
|
throw new FusionConfigError(`${source}: must be a JSON object`);
|
|
64
|
-
if (raw.version !==
|
|
65
|
-
throw new FusionConfigError(`${source}: unsupported version ${JSON.stringify(raw.version)} (expected
|
|
124
|
+
if (typeof raw.version !== "string" || !SUPPORTED_CONFIG_VERSIONS.includes(raw.version)) {
|
|
125
|
+
throw new FusionConfigError(`${source}: unsupported version ${JSON.stringify(raw.version)} (expected one of ${SUPPORTED_CONFIG_VERSIONS.join(", ")})`);
|
|
66
126
|
}
|
|
67
127
|
const config = { version: FUSION_CONFIG_VERSION };
|
|
68
128
|
if (raw.tool !== undefined) {
|
|
@@ -104,14 +164,7 @@ export function parseFusionConfig(raw, source) {
|
|
|
104
164
|
}
|
|
105
165
|
return config;
|
|
106
166
|
}
|
|
107
|
-
|
|
108
|
-
* Load `<repoRoot>/fusionkit.json` if present. Returns `undefined` when the file
|
|
109
|
-
* does not exist; throws {@link FusionConfigError} on malformed content.
|
|
110
|
-
*/
|
|
111
|
-
export function loadFusionConfig(repoRoot) {
|
|
112
|
-
const path = fusionConfigPath(repoRoot);
|
|
113
|
-
if (!existsSync(path))
|
|
114
|
-
return undefined;
|
|
167
|
+
function readAndParse(path) {
|
|
115
168
|
let raw;
|
|
116
169
|
try {
|
|
117
170
|
raw = JSON.parse(readFileSync(path, "utf8"));
|
|
@@ -121,12 +174,89 @@ export function loadFusionConfig(repoRoot) {
|
|
|
121
174
|
}
|
|
122
175
|
return parseFusionConfig(raw, path);
|
|
123
176
|
}
|
|
124
|
-
/**
|
|
177
|
+
/**
|
|
178
|
+
* Read the committed prompt overrides from `.fusionkit/prompts/*.md`. Only files
|
|
179
|
+
* that exist and are non-empty (after trimming) become overrides.
|
|
180
|
+
*/
|
|
181
|
+
export function readFusionPrompts(repoRoot) {
|
|
182
|
+
const dir = fusionPromptsDir(repoRoot);
|
|
183
|
+
const prompts = {};
|
|
184
|
+
if (!existsSync(dir))
|
|
185
|
+
return prompts;
|
|
186
|
+
for (const id of PROMPT_IDS) {
|
|
187
|
+
const path = fusionPromptPath(repoRoot, id);
|
|
188
|
+
if (!existsSync(path))
|
|
189
|
+
continue;
|
|
190
|
+
const text = readFileSync(path, "utf8").trim();
|
|
191
|
+
if (text.length > 0)
|
|
192
|
+
prompts[id] = text;
|
|
193
|
+
}
|
|
194
|
+
return prompts;
|
|
195
|
+
}
|
|
196
|
+
function withPrompts(repoRoot, config) {
|
|
197
|
+
const prompts = readFusionPrompts(repoRoot);
|
|
198
|
+
if (Object.keys(prompts).length === 0)
|
|
199
|
+
return config;
|
|
200
|
+
return { ...config, prompts };
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Load the per-repo config. Prefers `.fusionkit/config.json`; if it is absent
|
|
204
|
+
* but a legacy `fusionkit.json` exists, auto-migrates it into the folder (the
|
|
205
|
+
* original is left intact) and loads from there. Returns `undefined` when no
|
|
206
|
+
* config exists; throws {@link FusionConfigError} on malformed content.
|
|
207
|
+
*
|
|
208
|
+
* `onNotice` receives a one-line message when a migration happens.
|
|
209
|
+
*/
|
|
210
|
+
export function loadFusionConfig(repoRoot, onNotice) {
|
|
211
|
+
const newPath = fusionConfigPath(repoRoot);
|
|
212
|
+
if (existsSync(newPath)) {
|
|
213
|
+
return withPrompts(repoRoot, readAndParse(newPath));
|
|
214
|
+
}
|
|
215
|
+
const legacyPath = legacyFusionConfigPath(repoRoot);
|
|
216
|
+
if (!existsSync(legacyPath))
|
|
217
|
+
return undefined;
|
|
218
|
+
const config = readAndParse(legacyPath);
|
|
219
|
+
try {
|
|
220
|
+
writeFusionConfig(repoRoot, config);
|
|
221
|
+
onNotice?.(`migrated ${legacyPath} into ${newPath}`);
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
// Could not write the migrated copy (e.g. read-only FS); use the legacy
|
|
225
|
+
// file in place for this run rather than failing.
|
|
226
|
+
}
|
|
227
|
+
return withPrompts(repoRoot, config);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Write `.fusionkit/config.json` (creating the folder), refusing to clobber
|
|
231
|
+
* unless `force`. Prompt overrides are stored as files, not inline, so any
|
|
232
|
+
* `prompts` on the config object is omitted here.
|
|
233
|
+
*/
|
|
125
234
|
export function writeFusionConfig(repoRoot, config, options = {}) {
|
|
126
235
|
const path = fusionConfigPath(repoRoot);
|
|
127
236
|
if (existsSync(path) && options.force !== true) {
|
|
128
237
|
throw new FusionConfigError(`${path} already exists (pass --force to overwrite)`);
|
|
129
238
|
}
|
|
130
|
-
|
|
239
|
+
mkdirSync(fusionConfigDir(repoRoot), { recursive: true });
|
|
240
|
+
const { prompts: _prompts, ...persisted } = config;
|
|
241
|
+
writeFileSync(path, JSON.stringify(persisted, null, 2) + "\n");
|
|
131
242
|
return path;
|
|
132
243
|
}
|
|
244
|
+
/**
|
|
245
|
+
* Write prompt override files into `.fusionkit/prompts/`. Existing files are
|
|
246
|
+
* left untouched unless `force`. Returns the paths actually written.
|
|
247
|
+
*/
|
|
248
|
+
export function writeFusionPrompts(repoRoot, prompts, options = {}) {
|
|
249
|
+
mkdirSync(fusionPromptsDir(repoRoot), { recursive: true });
|
|
250
|
+
const written = [];
|
|
251
|
+
for (const id of PROMPT_IDS) {
|
|
252
|
+
const text = prompts[id];
|
|
253
|
+
if (text === undefined)
|
|
254
|
+
continue;
|
|
255
|
+
const path = fusionPromptPath(repoRoot, id);
|
|
256
|
+
if (existsSync(path) && options.force !== true)
|
|
257
|
+
continue;
|
|
258
|
+
writeFileSync(path, text.endsWith("\n") ? text : `${text}\n`);
|
|
259
|
+
written.push(path);
|
|
260
|
+
}
|
|
261
|
+
return written;
|
|
262
|
+
}
|
package/dist/fusion-init.d.ts
CHANGED
package/dist/fusion-init.js
CHANGED
|
@@ -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 {
|
|
7
|
-
import {
|
|
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
|
|
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`);
|
|
@@ -103,8 +133,22 @@ export async function runFusionInit(input) {
|
|
|
103
133
|
}
|
|
104
134
|
throw error;
|
|
105
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
|
+
: [];
|
|
106
144
|
out.write("\n");
|
|
107
145
|
done(`wrote ${cyan(fusionConfigPath(input.repoRoot))}`);
|
|
108
|
-
|
|
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}`)}`);
|
|
109
153
|
return 0;
|
|
110
154
|
}
|
|
@@ -201,6 +201,7 @@ export async function runFusion(tool, toolArgs, options = {}) {
|
|
|
201
201
|
...(report !== undefined ? { report } : {}),
|
|
202
202
|
...(options.endpoints !== undefined ? { endpoints: options.endpoints } : {}),
|
|
203
203
|
...(options.fusionkitDir !== undefined ? { fusionkitDir: options.fusionkitDir } : {}),
|
|
204
|
+
...(options.prompts !== undefined ? { prompts: options.prompts } : {}),
|
|
204
205
|
...(options.judgeModel !== undefined ? { judgeModel: options.judgeModel } : {}),
|
|
205
206
|
...(options.synthesisUrl !== undefined ? { synthesisUrl: options.synthesisUrl } : {}),
|
|
206
207
|
...(options.authToken !== undefined ? { authToken: options.authToken } : {}),
|