@maestrofrontier/frontier 1.4.0
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/AGENTS.md +214 -0
- package/CLAUDE.md +29 -0
- package/LICENSE +21 -0
- package/README.md +521 -0
- package/bin/maestro.cjs +75 -0
- package/commands/compress.md +36 -0
- package/commands/context-bar.md +30 -0
- package/commands/frontier.md +124 -0
- package/commands/settings.md +101 -0
- package/commands/terse.md +23 -0
- package/commands/update.md +59 -0
- package/docs/orchestration.md +168 -0
- package/frontier/cli.cjs +248 -0
- package/frontier/config.cjs +441 -0
- package/frontier/dispatch.cjs +255 -0
- package/frontier/judge.cjs +92 -0
- package/frontier/run.cjs +148 -0
- package/frontier/schema.cjs +112 -0
- package/frontier/semaphore.cjs +49 -0
- package/frontier/synthesize.cjs +79 -0
- package/hooks/frontier-autorun.cjs +124 -0
- package/hooks/hooks.json +103 -0
- package/hooks/maestro-doctrine-guard.cjs +81 -0
- package/hooks/maestro-gate-reminder.cjs +58 -0
- package/hooks/maestro-gate-telemetry.cjs +77 -0
- package/hooks/maestro-loop-guard.cjs +76 -0
- package/hooks/maestro-phase-scope.cjs +118 -0
- package/hooks/maestro-statusline-sync.cjs +152 -0
- package/hooks/maestro-subagent-guard.cjs +148 -0
- package/hooks/maestro-terse-mode.cjs +189 -0
- package/hooks/maestro-toolbudget-advisory.cjs +127 -0
- package/integrations/README.md +87 -0
- package/integrations/cline/skills/frontier/SKILL.md +75 -0
- package/integrations/codex/prompts/frontier.md +66 -0
- package/integrations/codex/prompts/update.md +36 -0
- package/integrations/cursor/commands/frontier.md +63 -0
- package/integrations/cursor/commands/update.md +34 -0
- package/integrations/gemini/commands/frontier.toml +76 -0
- package/integrations/windsurf/workflows/frontier.md +70 -0
- package/package.json +52 -0
- package/scripts/install.cjs +490 -0
- package/settings/cli.cjs +140 -0
- package/settings/config.cjs +309 -0
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@maestrofrontier/frontier",
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "Achieve Frontier AI performance in your CLI by fusing the model CLIs you already run. Maestro Frontier is an opt-in, zero-dependency local multi-CLI fusion engine for AI coding agents: fan a prompt across a panel of any 1 to 8 local model CLIs you pick, have a judge model and a synthesizer you choose read the answers into a structured analysis and write one grounded synthesis (default Opus 4.8, override either with --judge/--synth). On a 100-task benchmark every fusion panel outscored its individual member models. Three adapters ship today: Opus 4.8, GPT-5.5, Gemini 3.1 Pro, with Kimi, DeepSeek, GLM, and Qwen to follow. Off, single, and fusion modes switch via /maestro:frontier. Built on Maestro orchestration discipline: decision-gated routing, verified done-claims, surgical scope, and structural enforcement hooks.",
|
|
5
|
+
"keywords": ["multi-cli-fusion", "fusion-engine", "frontier", "multi-agent", "orchestration", "claude-code", "gemini", "codex", "agents", "hooks", "doctrine"],
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/mbanderas/maestro.git"
|
|
10
|
+
},
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=18"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"maestro": "bin/maestro.cjs"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"bin/maestro.cjs",
|
|
19
|
+
"frontier/cli.cjs",
|
|
20
|
+
"frontier/config.cjs",
|
|
21
|
+
"frontier/dispatch.cjs",
|
|
22
|
+
"frontier/judge.cjs",
|
|
23
|
+
"frontier/run.cjs",
|
|
24
|
+
"frontier/schema.cjs",
|
|
25
|
+
"frontier/semaphore.cjs",
|
|
26
|
+
"frontier/synthesize.cjs",
|
|
27
|
+
"scripts/install.cjs",
|
|
28
|
+
"AGENTS.md",
|
|
29
|
+
"CLAUDE.md",
|
|
30
|
+
"docs/orchestration.md",
|
|
31
|
+
"commands/",
|
|
32
|
+
"integrations/",
|
|
33
|
+
"hooks/frontier-autorun.cjs",
|
|
34
|
+
"hooks/hooks.json",
|
|
35
|
+
"hooks/maestro-doctrine-guard.cjs",
|
|
36
|
+
"hooks/maestro-gate-reminder.cjs",
|
|
37
|
+
"hooks/maestro-gate-telemetry.cjs",
|
|
38
|
+
"hooks/maestro-loop-guard.cjs",
|
|
39
|
+
"hooks/maestro-phase-scope.cjs",
|
|
40
|
+
"hooks/maestro-statusline-sync.cjs",
|
|
41
|
+
"hooks/maestro-subagent-guard.cjs",
|
|
42
|
+
"hooks/maestro-terse-mode.cjs",
|
|
43
|
+
"hooks/maestro-toolbudget-advisory.cjs",
|
|
44
|
+
"settings/cli.cjs",
|
|
45
|
+
"settings/config.cjs"
|
|
46
|
+
],
|
|
47
|
+
"scripts": {
|
|
48
|
+
"test": "node scripts/run-hook-tests.cjs",
|
|
49
|
+
"lint": "npx --yes markdownlint-cli2",
|
|
50
|
+
"bench-verify": "node scripts/bench-verify.cjs"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Maestro installer — writes doctrine + engine + tool wrapper into a target
|
|
3
|
+
// project. Append-only for AGENTS.md, no-clobber for wrapper files, safe to
|
|
4
|
+
// re-run. Zero dependencies (Node stdlib only). CommonJS (.cjs).
|
|
5
|
+
//
|
|
6
|
+
// Usage (as module): const { run } = require('./install.cjs'); run(argv);
|
|
7
|
+
// Usage (as script): node scripts/install.cjs [flags]
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
// ---- constants ----
|
|
16
|
+
|
|
17
|
+
const PKG_ROOT = path.join(__dirname, '..');
|
|
18
|
+
const SENTINEL = '<!-- maestro:begin -->';
|
|
19
|
+
const SENTINEL_END = '<!-- maestro:end -->';
|
|
20
|
+
|
|
21
|
+
// Map target -> { templateSrc, projectDest, userDest }
|
|
22
|
+
// templateSrc is relative to PKG_ROOT.
|
|
23
|
+
// userDest null means no global install path for this target.
|
|
24
|
+
const WRAPPER_MAP = {
|
|
25
|
+
codex: {
|
|
26
|
+
src: 'integrations/codex/prompts/frontier.md',
|
|
27
|
+
proj: '.codex/prompts/frontier.md',
|
|
28
|
+
user: path.join(os.homedir(), '.codex', 'prompts', 'frontier.md'),
|
|
29
|
+
},
|
|
30
|
+
cursor: {
|
|
31
|
+
src: 'integrations/cursor/commands/frontier.md',
|
|
32
|
+
proj: '.cursor/commands/frontier.md',
|
|
33
|
+
user: null, // no global path for cursor
|
|
34
|
+
},
|
|
35
|
+
gemini: {
|
|
36
|
+
src: 'integrations/gemini/commands/frontier.toml',
|
|
37
|
+
proj: '.gemini/commands/frontier.toml',
|
|
38
|
+
user: path.join(os.homedir(), '.gemini', 'commands', 'frontier.toml'),
|
|
39
|
+
},
|
|
40
|
+
cline: {
|
|
41
|
+
src: 'integrations/cline/skills/frontier/SKILL.md',
|
|
42
|
+
proj: '.cline/skills/frontier/SKILL.md',
|
|
43
|
+
user: path.join(os.homedir(), '.cline', 'skills', 'frontier', 'SKILL.md'),
|
|
44
|
+
},
|
|
45
|
+
windsurf: {
|
|
46
|
+
src: 'integrations/windsurf/workflows/frontier.md',
|
|
47
|
+
proj: '.windsurf/workflows/frontier.md',
|
|
48
|
+
user: path.join(os.homedir(), '.codeium', 'windsurf', 'global_workflows', 'frontier.md'),
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Marker dirs used for auto-detection (scanned inside project root)
|
|
53
|
+
const AUTO_MARKERS = [
|
|
54
|
+
{ dir: '.cursor', target: 'cursor' },
|
|
55
|
+
{ dir: '.gemini', target: 'gemini' },
|
|
56
|
+
{ dir: '.codex', target: 'codex' },
|
|
57
|
+
{ dir: '.cline', target: 'cline' },
|
|
58
|
+
{ dir: '.windsurf',target: 'windsurf' },
|
|
59
|
+
{ dir: '.claude', target: 'claude' },
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
// ---- safety helpers ----
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Returns true if p is a symlink (lstat-based). Never throws.
|
|
66
|
+
* @param {string} p
|
|
67
|
+
* @returns {boolean}
|
|
68
|
+
*/
|
|
69
|
+
function isSymlink(p) {
|
|
70
|
+
try {
|
|
71
|
+
return fs.lstatSync(p).isSymbolicLink();
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Create directories for destPath. Refuses to create through a symlinked
|
|
79
|
+
* ancestor directory. Returns true on success, false on refusal.
|
|
80
|
+
* @param {string} destPath
|
|
81
|
+
* @returns {boolean}
|
|
82
|
+
*/
|
|
83
|
+
function safeMkdirp(destPath) {
|
|
84
|
+
const dir = path.dirname(destPath);
|
|
85
|
+
// Walk ancestors from PKG_ROOT outward — only check the leaf dir because
|
|
86
|
+
// we cannot reliably validate every ancestor on all OSes; the write will
|
|
87
|
+
// fail safely if anything is wrong.
|
|
88
|
+
try {
|
|
89
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
90
|
+
return true;
|
|
91
|
+
} catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Write buf to dest. Refuses if dest (or its parent dir) is a symlink.
|
|
98
|
+
* Returns { ok: true } or { ok: false, reason: string }.
|
|
99
|
+
* @param {string} dest
|
|
100
|
+
* @param {string|Buffer} content
|
|
101
|
+
* @returns {{ ok: boolean, reason?: string }}
|
|
102
|
+
*/
|
|
103
|
+
function safeWrite(dest, content) {
|
|
104
|
+
// Check parent dir
|
|
105
|
+
const dir = path.dirname(dest);
|
|
106
|
+
if (isSymlink(dir)) {
|
|
107
|
+
return { ok: false, reason: `parent dir is a symlink: ${dir}` };
|
|
108
|
+
}
|
|
109
|
+
// Check destination itself
|
|
110
|
+
if (isSymlink(dest)) {
|
|
111
|
+
return { ok: false, reason: `destination is a symlink: ${dest}` };
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
fs.writeFileSync(dest, content, 'utf8');
|
|
115
|
+
return { ok: true };
|
|
116
|
+
} catch (err) {
|
|
117
|
+
return { ok: false, reason: String(err.message || err) };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---- parse argv ----
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @param {string[]} argv
|
|
125
|
+
* @returns {{ target: string, project: string, user: boolean, dryRun: boolean, noHooks: boolean }}
|
|
126
|
+
*/
|
|
127
|
+
function parseArgs(argv) {
|
|
128
|
+
const opts = {
|
|
129
|
+
target: 'auto',
|
|
130
|
+
project: process.cwd(),
|
|
131
|
+
user: false,
|
|
132
|
+
dryRun: false,
|
|
133
|
+
noHooks: false,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
let i = 0;
|
|
137
|
+
while (i < argv.length) {
|
|
138
|
+
const a = argv[i];
|
|
139
|
+
if (a === '--target' && i + 1 < argv.length) {
|
|
140
|
+
opts.target = argv[++i];
|
|
141
|
+
} else if (a === '--project' && i + 1 < argv.length) {
|
|
142
|
+
opts.project = argv[++i];
|
|
143
|
+
} else if (a === '--user') {
|
|
144
|
+
opts.user = true;
|
|
145
|
+
} else if (a === '--dry-run') {
|
|
146
|
+
opts.dryRun = true;
|
|
147
|
+
} else if (a === '--no-hooks') {
|
|
148
|
+
opts.noHooks = true;
|
|
149
|
+
}
|
|
150
|
+
i++;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
opts.project = path.resolve(opts.project);
|
|
154
|
+
return opts;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ---- auto-detect ----
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Detect which tool is in use by looking for marker dirs.
|
|
161
|
+
* @param {string} projectRoot
|
|
162
|
+
* @returns {string} detected target or 'none'
|
|
163
|
+
*/
|
|
164
|
+
function detectTarget(projectRoot) {
|
|
165
|
+
for (const { dir, target } of AUTO_MARKERS) {
|
|
166
|
+
try {
|
|
167
|
+
const p = path.join(projectRoot, dir);
|
|
168
|
+
const st = fs.lstatSync(p);
|
|
169
|
+
if (st.isDirectory()) return target;
|
|
170
|
+
} catch {
|
|
171
|
+
// not found
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return 'none';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ---- install actions ----
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Install doctrine (AGENTS.md) into project root. Append-only, idempotent.
|
|
181
|
+
* @param {string} projectRoot
|
|
182
|
+
* @param {boolean} dryRun
|
|
183
|
+
* @param {(msg: string) => void} log
|
|
184
|
+
* @returns {boolean} true = success (or no-op), false = error
|
|
185
|
+
*/
|
|
186
|
+
function installDoctrine(projectRoot, dryRun, log) {
|
|
187
|
+
const dest = path.join(projectRoot, 'AGENTS.md');
|
|
188
|
+
const srcPath = path.join(PKG_ROOT, 'AGENTS.md');
|
|
189
|
+
|
|
190
|
+
let srcContent;
|
|
191
|
+
try {
|
|
192
|
+
srcContent = fs.readFileSync(srcPath, 'utf8');
|
|
193
|
+
} catch (err) {
|
|
194
|
+
log(`ERROR: cannot read package AGENTS.md: ${err.message}`);
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const block = `\n${SENTINEL}\n${srcContent}\n${SENTINEL_END}\n`;
|
|
199
|
+
|
|
200
|
+
// Check if dest exists
|
|
201
|
+
let existsStat;
|
|
202
|
+
try { existsStat = fs.lstatSync(dest); } catch { existsStat = null; }
|
|
203
|
+
|
|
204
|
+
if (existsStat) {
|
|
205
|
+
if (existsStat.isSymbolicLink()) {
|
|
206
|
+
log(`ERROR: AGENTS.md is a symlink — refusing to write through it: ${dest}`);
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let existing;
|
|
211
|
+
try { existing = fs.readFileSync(dest, 'utf8'); } catch (err) {
|
|
212
|
+
log(`ERROR: cannot read existing AGENTS.md: ${err.message}`);
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (existing.includes(SENTINEL)) {
|
|
217
|
+
log(`[doctrine] AGENTS.md already contains sentinel — skipping`);
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Append block
|
|
222
|
+
if (dryRun) {
|
|
223
|
+
log(`[dry-run] would append maestro doctrine to existing ${dest}`);
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const res = safeWrite(dest, existing + block);
|
|
228
|
+
if (!res.ok) {
|
|
229
|
+
log(`ERROR: failed to append to AGENTS.md: ${res.reason}`);
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
log(`[doctrine] appended maestro block to existing AGENTS.md`);
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Absent — write fresh. Wrap in sentinel block so subsequent runs detect it.
|
|
237
|
+
if (dryRun) {
|
|
238
|
+
log(`[dry-run] would create ${dest}`);
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!safeMkdirp(dest)) {
|
|
243
|
+
log(`ERROR: could not create parent dir for ${dest}`);
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const freshContent = SENTINEL + '\n' + srcContent + '\n' + SENTINEL_END + '\n';
|
|
248
|
+
const res = safeWrite(dest, freshContent);
|
|
249
|
+
if (!res.ok) {
|
|
250
|
+
log(`ERROR: failed to write AGENTS.md: ${res.reason}`);
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
log(`[doctrine] wrote AGENTS.md`);
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Recursively copy srcDir -> destDir, skipping *.test.cjs files.
|
|
259
|
+
* @param {string} srcDir
|
|
260
|
+
* @param {string} destDir
|
|
261
|
+
* @param {boolean} dryRun
|
|
262
|
+
* @param {(msg: string) => void} log
|
|
263
|
+
* @returns {boolean}
|
|
264
|
+
*/
|
|
265
|
+
function copyDirRecursive(srcDir, destDir, dryRun, log) {
|
|
266
|
+
let entries;
|
|
267
|
+
try {
|
|
268
|
+
entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
269
|
+
} catch (err) {
|
|
270
|
+
log(`ERROR: cannot read dir ${srcDir}: ${err.message}`);
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
let ok = true;
|
|
275
|
+
for (const entry of entries) {
|
|
276
|
+
if (entry.isFile() && entry.name.endsWith('.test.cjs')) continue;
|
|
277
|
+
|
|
278
|
+
const src = path.join(srcDir, entry.name);
|
|
279
|
+
const dest = path.join(destDir, entry.name);
|
|
280
|
+
|
|
281
|
+
if (entry.isDirectory()) {
|
|
282
|
+
if (!copyDirRecursive(src, dest, dryRun, log)) ok = false;
|
|
283
|
+
} else if (entry.isFile()) {
|
|
284
|
+
if (dryRun) {
|
|
285
|
+
log(`[dry-run] would write ${dest}`);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (isSymlink(dest)) {
|
|
290
|
+
log(`ERROR: destination is a symlink — refusing: ${dest}`);
|
|
291
|
+
ok = false;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (isSymlink(path.dirname(dest))) {
|
|
295
|
+
log(`ERROR: destination parent is a symlink — refusing: ${dest}`);
|
|
296
|
+
ok = false;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
302
|
+
const content = fs.readFileSync(src);
|
|
303
|
+
fs.writeFileSync(dest, content);
|
|
304
|
+
log(`[engine] copied ${dest}`);
|
|
305
|
+
} catch (err) {
|
|
306
|
+
log(`ERROR: failed to copy ${src} -> ${dest}: ${err.message}`);
|
|
307
|
+
ok = false;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return ok;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Install engine files (frontier/ dir + bin/maestro.cjs).
|
|
316
|
+
* @param {string} projectRoot
|
|
317
|
+
* @param {boolean} dryRun
|
|
318
|
+
* @param {(msg: string) => void} log
|
|
319
|
+
* @returns {boolean}
|
|
320
|
+
*/
|
|
321
|
+
function installEngine(projectRoot, dryRun, log) {
|
|
322
|
+
const srcFrontier = path.join(PKG_ROOT, 'frontier');
|
|
323
|
+
const destFrontier = path.join(projectRoot, 'frontier');
|
|
324
|
+
const srcBin = path.join(PKG_ROOT, 'bin', 'maestro.cjs');
|
|
325
|
+
const destBin = path.join(projectRoot, 'bin', 'maestro.cjs');
|
|
326
|
+
|
|
327
|
+
let ok = copyDirRecursive(srcFrontier, destFrontier, dryRun, log);
|
|
328
|
+
|
|
329
|
+
// bin/maestro.cjs
|
|
330
|
+
if (dryRun) {
|
|
331
|
+
log(`[dry-run] would write ${destBin}`);
|
|
332
|
+
} else {
|
|
333
|
+
if (isSymlink(destBin)) {
|
|
334
|
+
log(`ERROR: bin/maestro.cjs is a symlink — refusing: ${destBin}`);
|
|
335
|
+
ok = false;
|
|
336
|
+
} else {
|
|
337
|
+
try {
|
|
338
|
+
fs.mkdirSync(path.dirname(destBin), { recursive: true });
|
|
339
|
+
fs.writeFileSync(destBin, fs.readFileSync(srcBin));
|
|
340
|
+
log(`[engine] copied ${destBin}`);
|
|
341
|
+
} catch (err) {
|
|
342
|
+
log(`ERROR: failed to copy bin/maestro.cjs: ${err.message}`);
|
|
343
|
+
ok = false;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return ok;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Install wrapper file (no-clobber).
|
|
353
|
+
* @param {string} target
|
|
354
|
+
* @param {string} projectRoot
|
|
355
|
+
* @param {boolean} userGlobal
|
|
356
|
+
* @param {boolean} dryRun
|
|
357
|
+
* @param {(msg: string) => void} log
|
|
358
|
+
* @returns {boolean}
|
|
359
|
+
*/
|
|
360
|
+
function installWrapper(target, projectRoot, userGlobal, dryRun, log) {
|
|
361
|
+
if (target === 'claude') {
|
|
362
|
+
log('[claude] No wrapper file — plugin delivers the command.');
|
|
363
|
+
log('[claude] To install the plugin: /plugin marketplace add mbanderas/maestro');
|
|
364
|
+
log('[claude] Then: /plugin install maestro@maestro');
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const mapping = WRAPPER_MAP[target];
|
|
369
|
+
if (!mapping) {
|
|
370
|
+
log(`ERROR: unknown target: ${target}`);
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const src = path.join(PKG_ROOT, mapping.src);
|
|
375
|
+
|
|
376
|
+
let dest;
|
|
377
|
+
if (userGlobal) {
|
|
378
|
+
if (!mapping.user) {
|
|
379
|
+
log(`[wrapper] --user not supported for target ${target} — writing to project instead`);
|
|
380
|
+
dest = path.join(projectRoot, mapping.proj);
|
|
381
|
+
} else {
|
|
382
|
+
dest = mapping.user;
|
|
383
|
+
}
|
|
384
|
+
} else {
|
|
385
|
+
dest = path.join(projectRoot, mapping.proj);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Check if dest exists already (no-clobber)
|
|
389
|
+
let destStat;
|
|
390
|
+
try { destStat = fs.lstatSync(dest); } catch { destStat = null; }
|
|
391
|
+
|
|
392
|
+
if (destStat) {
|
|
393
|
+
if (destStat.isSymbolicLink()) {
|
|
394
|
+
log(`ERROR: wrapper dest is a symlink — refusing: ${dest}`);
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
log(`[wrapper] skipped (exists, not clobbered): ${dest}`);
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
let srcContent;
|
|
402
|
+
try {
|
|
403
|
+
srcContent = fs.readFileSync(src, 'utf8');
|
|
404
|
+
} catch (err) {
|
|
405
|
+
log(`ERROR: cannot read template ${src}: ${err.message}`);
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (dryRun) {
|
|
410
|
+
log(`[dry-run] would create ${dest}`);
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (!safeMkdirp(dest)) {
|
|
415
|
+
log(`ERROR: could not create parent dir for ${dest}`);
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const res = safeWrite(dest, srcContent);
|
|
420
|
+
if (!res.ok) {
|
|
421
|
+
log(`ERROR: failed to write wrapper ${dest}: ${res.reason}`);
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
log(`[wrapper] wrote ${dest}`);
|
|
425
|
+
return true;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// ---- main entry ----
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Run the installer. Returns a numeric exit code (0 = success).
|
|
432
|
+
* @param {string[]} argv
|
|
433
|
+
* @returns {number}
|
|
434
|
+
*/
|
|
435
|
+
function run(argv) {
|
|
436
|
+
const opts = parseArgs(argv || []);
|
|
437
|
+
const { target: rawTarget, project, user: userGlobal, dryRun } = opts;
|
|
438
|
+
|
|
439
|
+
const lines = [];
|
|
440
|
+
const log = (msg) => { lines.push(msg); process.stdout.write(msg + '\n'); };
|
|
441
|
+
|
|
442
|
+
if (dryRun) log('[dry-run] planning only — no files will be written');
|
|
443
|
+
|
|
444
|
+
// Resolve target
|
|
445
|
+
let target = rawTarget;
|
|
446
|
+
if (target === 'auto') {
|
|
447
|
+
target = detectTarget(project);
|
|
448
|
+
if (target === 'none') {
|
|
449
|
+
log('[auto] no tool marker dir found — installing doctrine + engine only');
|
|
450
|
+
log('[auto] pass --target <tool> to install a command wrapper');
|
|
451
|
+
} else {
|
|
452
|
+
log(`[auto] detected target: ${target}`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const VALID_TARGETS = ['auto', 'claude', 'codex', 'cursor', 'gemini', 'cline', 'windsurf'];
|
|
457
|
+
if (!VALID_TARGETS.includes(rawTarget)) {
|
|
458
|
+
log(`ERROR: unknown --target value: ${rawTarget}`);
|
|
459
|
+
return 1;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
let anyError = false;
|
|
463
|
+
|
|
464
|
+
// 1. Doctrine
|
|
465
|
+
if (!installDoctrine(project, dryRun, log)) anyError = true;
|
|
466
|
+
|
|
467
|
+
// 2. Engine
|
|
468
|
+
if (!installEngine(project, dryRun, log)) anyError = true;
|
|
469
|
+
|
|
470
|
+
// 3. Wrapper (skip if no specific target detected)
|
|
471
|
+
if (target !== 'none') {
|
|
472
|
+
if (!installWrapper(target, project, userGlobal, dryRun, log)) anyError = true;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (anyError) {
|
|
476
|
+
log('install completed with errors (see above)');
|
|
477
|
+
return 1;
|
|
478
|
+
}
|
|
479
|
+
log('install complete');
|
|
480
|
+
return 0;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// ---- CLI entry ----
|
|
484
|
+
|
|
485
|
+
if (require.main === module) {
|
|
486
|
+
const code = run(process.argv.slice(2));
|
|
487
|
+
process.exit(code);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
module.exports = { run };
|
package/settings/cli.cjs
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Maestro Settings — portable CLI. Usable from Codex and any other agent,
|
|
3
|
+
// and the write path the /maestro:settings command calls. Subcommands:
|
|
4
|
+
// settings status [--json]
|
|
5
|
+
// settings set <terse|frontier|context-bar> <value> [--judge M] [--synth M] [--models a,b,c]
|
|
6
|
+
// All state I/O goes through settings/config.cjs, which is the one writer
|
|
7
|
+
// over the three existing stores. Zero deps, CJS.
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const settings = require('./config.cjs');
|
|
12
|
+
|
|
13
|
+
function getFlag(argv, flag) {
|
|
14
|
+
const i = argv.indexOf(flag);
|
|
15
|
+
return i !== -1 && i + 1 < argv.length ? argv[i + 1] : null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function fmtFrontier(f) {
|
|
19
|
+
if (!f || !f.mode || f.mode === 'off') return 'off';
|
|
20
|
+
if (f.mode === 'single') return 'single ' + (f.model || '?');
|
|
21
|
+
if (f.mode === 'fusion') {
|
|
22
|
+
let s = 'fusion ' + (f.preset || '?');
|
|
23
|
+
if (f.preset === 'custom' && Array.isArray(f.models)) s += ' [' + f.models.join(',') + ']';
|
|
24
|
+
const extra = [];
|
|
25
|
+
if (f.judgeModel) extra.push('judge=' + f.judgeModel);
|
|
26
|
+
if (f.synthModel) extra.push('synth=' + f.synthModel);
|
|
27
|
+
if (extra.length) s += ' ' + extra.join(' ');
|
|
28
|
+
return s;
|
|
29
|
+
}
|
|
30
|
+
return f.mode;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function cmdStatus(argv) {
|
|
34
|
+
const scope = getFlag(argv, '--scope') || undefined;
|
|
35
|
+
const all = settings.readAll(scope);
|
|
36
|
+
if (argv.includes('--json')) {
|
|
37
|
+
process.stdout.write(JSON.stringify(all, null, 2) + '\n');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const t = all.terse;
|
|
41
|
+
const cb = all.contextBar;
|
|
42
|
+
const lines = ['Maestro settings'];
|
|
43
|
+
lines.push(' terse ' + t.level + ' (source: ' + t.source + ')' +
|
|
44
|
+
(t.envOverride ? ' [MAESTRO_TERSE_LEVEL override active]' : ''));
|
|
45
|
+
lines.push(' frontier ' + fmtFrontier(all.frontier));
|
|
46
|
+
lines.push(' context-bar ' + (cb.enabled ? 'on' : 'off') +
|
|
47
|
+
(cb.scriptConfirmed ? '' : ' [status-line script unconfirmed: ' + cb.dir + ']'));
|
|
48
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function cmdList(argv) {
|
|
52
|
+
const c = settings.catalog();
|
|
53
|
+
if (argv.includes('--json')) {
|
|
54
|
+
process.stdout.write(JSON.stringify(c, null, 2) + '\n');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const labelOf = {};
|
|
58
|
+
c.frontier.models.forEach(m => { labelOf[m.id] = m.label; });
|
|
59
|
+
const lines = ['Maestro settings — available values'];
|
|
60
|
+
lines.push(' terse ' + c.terse.values.join(' | '));
|
|
61
|
+
lines.push(' context-bar ' + c.contextBar.values.join(' | '));
|
|
62
|
+
lines.push(' frontier off | single:<model> | fusion:<preset>');
|
|
63
|
+
lines.push(' models ' + c.frontier.models.map(m => m.id + ' (' + m.label + ')').join(', '));
|
|
64
|
+
lines.push(' presets');
|
|
65
|
+
c.frontier.presets.forEach(p => {
|
|
66
|
+
const desc = p.models
|
|
67
|
+
? p.models.map(id => labelOf[id] || id).join(' + ')
|
|
68
|
+
: 'choose your own models (--models a,b,c)';
|
|
69
|
+
lines.push(' ' + p.id.padEnd(14) + desc);
|
|
70
|
+
});
|
|
71
|
+
lines.push(' judge/synth ' + c.frontier.stageModels.join(', ') +
|
|
72
|
+
' (default judge=' + c.frontier.defaults.judge + ', synth=' + c.frontier.defaults.synth + ')');
|
|
73
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function cmdSet(argv) {
|
|
77
|
+
const key = argv[0];
|
|
78
|
+
const value = argv[1];
|
|
79
|
+
if (!key || value === undefined) {
|
|
80
|
+
process.stderr.write('Usage: settings set <terse|frontier|context-bar> <value>\n');
|
|
81
|
+
process.exit(2);
|
|
82
|
+
}
|
|
83
|
+
const opts = {
|
|
84
|
+
judge: getFlag(argv, '--judge'),
|
|
85
|
+
synth: getFlag(argv, '--synth'),
|
|
86
|
+
models: getFlag(argv, '--models'),
|
|
87
|
+
model: getFlag(argv, '--model'),
|
|
88
|
+
preset: getFlag(argv, '--preset'),
|
|
89
|
+
scope: getFlag(argv, '--scope'),
|
|
90
|
+
};
|
|
91
|
+
Object.keys(opts).forEach(k => { if (opts[k] == null) delete opts[k]; });
|
|
92
|
+
|
|
93
|
+
const r = settings.setKey(key, value, opts);
|
|
94
|
+
if (!r.ok) {
|
|
95
|
+
process.stderr.write('ERROR: ' + r.error + '\n');
|
|
96
|
+
process.exit(2);
|
|
97
|
+
}
|
|
98
|
+
process.stdout.write('set ' + key + ' = ' + value + '\n');
|
|
99
|
+
if (r.warning) process.stdout.write('WARNING: ' + r.warning + '\n');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function usageText() {
|
|
103
|
+
return (
|
|
104
|
+
'Usage:\n' +
|
|
105
|
+
' settings status [--json] [--scope <name>]\n' +
|
|
106
|
+
' settings list [--json]\n' +
|
|
107
|
+
' settings help\n' +
|
|
108
|
+
' settings set <key> <value> [--judge M] [--synth M] [--models a,b,c] [--scope <name>]\n' +
|
|
109
|
+
' terse <off|lite|full|ultra>\n' +
|
|
110
|
+
' frontier <off | single:<model> | fusion:<preset>>\n' +
|
|
111
|
+
' context-bar <on|off>\n' +
|
|
112
|
+
' --scope targets a named frontier state (e.g. codex, cursor); omit to autodetect (Claude Code => per-workspace cc-<hash>)\n'
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function usage() {
|
|
117
|
+
process.stderr.write(usageText());
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// `help` prints the usage grammar plus the available-values catalog to
|
|
121
|
+
// stdout, so a non-interactive user (Codex, scripts) gets the same matrix the
|
|
122
|
+
// keyboard picker offers.
|
|
123
|
+
function cmdHelp() {
|
|
124
|
+
process.stdout.write(usageText() + '\n');
|
|
125
|
+
cmdList([]);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function main() {
|
|
129
|
+
const argv = process.argv.slice(2);
|
|
130
|
+
const cmd = argv[0];
|
|
131
|
+
if (cmd === 'status') cmdStatus(argv.slice(1));
|
|
132
|
+
else if (cmd === 'list') cmdList(argv.slice(1));
|
|
133
|
+
else if (cmd === 'help' || cmd === '--help' || cmd === '-h') cmdHelp();
|
|
134
|
+
else if (cmd === 'set') cmdSet(argv.slice(1));
|
|
135
|
+
else { usage(); process.exit(2); }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (require.main === module) main();
|
|
139
|
+
|
|
140
|
+
module.exports = { main, fmtFrontier };
|