codedeck 0.2.0 → 0.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/README.md +45 -16
- package/dist/cli/charts/bar-chart.js +104 -0
- package/dist/cli/charts/bar-chart.js.map +1 -0
- package/dist/cli/charts/sparkline.js +18 -0
- package/dist/cli/charts/sparkline.js.map +1 -0
- package/dist/cli/charts/stacked-bar.js +59 -0
- package/dist/cli/charts/stacked-bar.js.map +1 -0
- package/dist/cli/cli-name.js +20 -0
- package/dist/cli/cli-name.js.map +1 -0
- package/dist/cli/commands/claims.js +170 -0
- package/dist/cli/commands/claims.js.map +1 -0
- package/dist/cli/commands/doctor.js +22 -1
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/models.js +5 -5
- package/dist/cli/commands/models.js.map +1 -1
- package/dist/cli/commands/open.js +309 -315
- package/dist/cli/commands/open.js.map +1 -1
- package/dist/cli/commands/ps.js +226 -23
- package/dist/cli/commands/ps.js.map +1 -1
- package/dist/cli/commands/rename.js +29 -0
- package/dist/cli/commands/rename.js.map +1 -0
- package/dist/cli/commands/run.js +50 -18
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/cli/commands/setup.js +989 -72
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/usage.js +192 -0
- package/dist/cli/commands/usage.js.map +1 -0
- package/dist/cli/commands/wait.js +3 -2
- package/dist/cli/commands/wait.js.map +1 -1
- package/dist/cli/commands/web.js +224 -0
- package/dist/cli/commands/web.js.map +1 -0
- package/dist/cli/index.js +36 -21
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/picker-state.js +75 -8
- package/dist/cli/picker-state.js.map +1 -1
- package/dist/cli/picker.js +13 -3
- package/dist/cli/picker.js.map +1 -1
- package/dist/cli/ui.js +54 -4
- package/dist/cli/ui.js.map +1 -1
- package/dist/cli/usage/dashboard.js +330 -0
- package/dist/cli/usage/dashboard.js.map +1 -0
- package/dist/cli/usage/snapshot.js +112 -0
- package/dist/cli/usage/snapshot.js.map +1 -0
- package/dist/config/config.js +326 -5
- package/dist/config/config.js.map +1 -1
- package/dist/config/orchestrator-mode.js +72 -0
- package/dist/config/orchestrator-mode.js.map +1 -0
- package/dist/config/paths.js +28 -9
- package/dist/config/paths.js.map +1 -1
- package/dist/core/autocompact.js +77 -0
- package/dist/core/autocompact.js.map +1 -0
- package/dist/core/driver.js.map +1 -1
- package/dist/core/models.js +166 -3
- package/dist/core/models.js.map +1 -1
- package/dist/core/pricing.js +83 -0
- package/dist/core/pricing.js.map +1 -0
- package/dist/core/roles.js +14 -3
- package/dist/core/roles.js.map +1 -1
- package/dist/core/run-usage.js +46 -0
- package/dist/core/run-usage.js.map +1 -0
- package/dist/core/session.js +18 -0
- package/dist/core/session.js.map +1 -1
- package/dist/daemon/daemon.js +286 -8
- package/dist/daemon/daemon.js.map +1 -1
- package/dist/daemon/ipc.js +10 -3
- package/dist/daemon/ipc.js.map +1 -1
- package/dist/drivers/antigravity/driver.js +158 -0
- package/dist/drivers/antigravity/driver.js.map +1 -0
- package/dist/drivers/antigravity/parser.js +168 -0
- package/dist/drivers/antigravity/parser.js.map +1 -0
- package/dist/drivers/claude/driver.js +13 -3
- package/dist/drivers/claude/driver.js.map +1 -1
- package/dist/drivers/opencode/driver.js +1 -114
- package/dist/drivers/opencode/driver.js.map +1 -1
- package/dist/drivers/opencode/parser.js +86 -0
- package/dist/drivers/opencode/parser.js.map +1 -0
- package/dist/drivers/registry.js +2 -0
- package/dist/drivers/registry.js.map +1 -1
- package/dist/drivers/session-driver.js +6 -4
- package/dist/drivers/session-driver.js.map +1 -1
- package/dist/drivers/session-runtime.js +1 -0
- package/dist/drivers/session-runtime.js.map +1 -1
- package/dist/git/worktree.js +1 -1
- package/dist/git/worktree.js.map +1 -1
- package/dist/open/contract.js +116 -0
- package/dist/open/contract.js.map +1 -0
- package/dist/open/injection.js +60 -0
- package/dist/open/injection.js.map +1 -0
- package/dist/open/launchers/claude.js +215 -0
- package/dist/open/launchers/claude.js.map +1 -0
- package/dist/open/launchers/opencode.js +213 -0
- package/dist/open/launchers/opencode.js.map +1 -0
- package/dist/open/orchestrator-prose.js +12 -0
- package/dist/open/orchestrator-prose.js.map +1 -0
- package/dist/open/pty.js +281 -0
- package/dist/open/pty.js.map +1 -0
- package/dist/open/runtime.js +632 -0
- package/dist/open/runtime.js.map +1 -0
- package/dist/plugin/agents/auditor.md +7 -1
- package/dist/plugin/agents/general.md +12 -1
- package/dist/plugin/agents/orchestrator-edit.md +67 -0
- package/dist/plugin/agents/orchestrator-read.md +67 -0
- package/dist/plugin/agents/orchestrator.md +74 -17
- package/dist/plugin/agents/reviewer.md +3 -0
- package/dist/plugin/commands/autonomous.md +58 -0
- package/dist/plugin/hooks/hooks.json +24 -0
- package/dist/plugin/hooks/session-id.sh +23 -0
- package/dist/plugin/hooks/session-name-lib.mjs +40 -0
- package/dist/plugin/hooks/session-name-worker.mjs +124 -0
- package/dist/plugin/hooks/session-name.mjs +66 -0
- package/dist/plugin/hooks/session-name.sh +7 -0
- package/dist/plugin/pty-shim.mjs +114 -0
- package/dist/plugin/statusline.sh +214 -22
- package/dist/plugin/themes/codedeck-rage.json +74 -0
- package/dist/plugin/themes/codedeck-ultra.json +52 -44
- package/dist/store/claims.js +152 -0
- package/dist/store/claims.js.map +1 -0
- package/dist/store/database.js +50 -15
- package/dist/store/database.js.map +1 -1
- package/dist/store/sessions.js +234 -3
- package/dist/store/sessions.js.map +1 -1
- package/dist/web/canvas-page.js +921 -0
- package/dist/web/canvas-page.js.map +1 -0
- package/package.json +3 -2
- package/dist/plugin/settings.json +0 -7
|
@@ -1,8 +1,270 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AUTOCOMPACT_DEFAULT_CAP, AUTOCOMPACT_DEFAULT_PERCENT, } from "../../core/autocompact.js";
|
|
2
|
+
import { getBatchModels, getCachedOrDiscoverModels, } from "../../core/models.js";
|
|
3
|
+
import { isAgentId } from "../../core/session.js";
|
|
4
|
+
import { itemKey } from "../picker-state.js";
|
|
2
5
|
import { runScreens } from "../picker.js";
|
|
3
6
|
import { colors, readDimensions } from "../ui.js";
|
|
7
|
+
import { getCliName } from "../cli-name.js";
|
|
4
8
|
import { getRegistry } from "../../drivers/registry.js";
|
|
5
|
-
import {
|
|
9
|
+
import { ROLES } from "../../core/roles.js";
|
|
10
|
+
import { getPaths } from "../../config/paths.js";
|
|
11
|
+
import { BALANCED_PRESET, DISPATCHER_PRESET, EXPLORER_PRESET, ORCHESTRATOR_PRESETS, createSetupConfigStore, DEFAULT_CONFIG, isOrchestratorMode, loadConfig, orchestratorModeLabel, resolveOrchestratorMode, saveConfig, serializeConfig, } from "../../config/config.js";
|
|
12
|
+
const ORCHESTRATOR_SCREEN_ROLE = "orchestrator-mode";
|
|
13
|
+
const ORCHESTRATOR_PICKER_GROUP = "orchestrator";
|
|
14
|
+
const PARALLELISM_NONE = "none";
|
|
15
|
+
const SANDBOX_SCREEN_ROLE = "sandbox";
|
|
16
|
+
const SANDBOX_PICKER_GROUP = "sandbox";
|
|
17
|
+
const SANDBOX_OFF = "workspace-write";
|
|
18
|
+
const SANDBOX_ON = "danger-full-access";
|
|
19
|
+
const AUTOCOMPACT_SCREEN_ROLE = "autocompact";
|
|
20
|
+
const AUTOCOMPACT_PICKER_GROUP = "autocompact";
|
|
21
|
+
const AUTOCOMPACT_OFF = "off";
|
|
22
|
+
const AUTOCOMPACT_ON = "on";
|
|
23
|
+
const AUTOCOMPACT_DESCRIPTION = `Native autocompact applies to Claude and OpenCode sessions. Claude's default window is min(${AUTOCOMPACT_DEFAULT_CAP / 1000}k tokens, ${AUTOCOMPACT_DEFAULT_PERCENT * 100}% of the context window); OpenCode uses its internal context trigger.`;
|
|
24
|
+
const ORCHESTRATOR_PARALLELISM_NOTE_LINES = [
|
|
25
|
+
"parallelism is advisory. Version 1 puts the requested cap in the",
|
|
26
|
+
"orchestrator prompt for review verification and does not enforce the",
|
|
27
|
+
"cap in code.",
|
|
28
|
+
];
|
|
29
|
+
export const ORCHESTRATOR_PARALLELISM_NOTE = ORCHESTRATOR_PARALLELISM_NOTE_LINES.join(" ");
|
|
30
|
+
const ORCHESTRATOR_PARAMETERS = [
|
|
31
|
+
"investigate",
|
|
32
|
+
"selfWork",
|
|
33
|
+
"tools",
|
|
34
|
+
"parallelism",
|
|
35
|
+
];
|
|
36
|
+
function copyOrchestratorMode(mode) {
|
|
37
|
+
return {
|
|
38
|
+
investigate: mode.investigate,
|
|
39
|
+
selfWork: mode.selfWork,
|
|
40
|
+
tools: mode.tools,
|
|
41
|
+
...(mode.parallelism === undefined ? {} : { parallelism: mode.parallelism }),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function selectionId(selection) {
|
|
45
|
+
return typeof selection === "string" ? selection : selection.id;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Turns a preset or a complete custom answer into the config block. The
|
|
49
|
+
* returned object contains parameters only, so labels and preset names cannot
|
|
50
|
+
* leak into the saved JSON.
|
|
51
|
+
*/
|
|
52
|
+
export function orchestratorConfigFromSelection(selection, parameters) {
|
|
53
|
+
const id = selectionId(selection);
|
|
54
|
+
if (Object.hasOwn(ORCHESTRATOR_PRESETS, id)) {
|
|
55
|
+
return copyOrchestratorMode(ORCHESTRATOR_PRESETS[id]);
|
|
56
|
+
}
|
|
57
|
+
if (id !== "custom" || parameters === undefined)
|
|
58
|
+
return undefined;
|
|
59
|
+
const mode = {
|
|
60
|
+
investigate: parameters.investigate,
|
|
61
|
+
selfWork: parameters.selfWork,
|
|
62
|
+
tools: parameters.tools,
|
|
63
|
+
...(parameters.parallelism === undefined ? {} : { parallelism: parameters.parallelism }),
|
|
64
|
+
};
|
|
65
|
+
return isOrchestratorMode(mode) ? mode : undefined;
|
|
66
|
+
}
|
|
67
|
+
export function orchestratorDisplayLabel(config) {
|
|
68
|
+
return orchestratorModeLabel(resolveOrchestratorMode(config));
|
|
69
|
+
}
|
|
70
|
+
function modeValue(mode, parameter) {
|
|
71
|
+
if (parameter === "parallelism")
|
|
72
|
+
return mode.parallelism === undefined ? PARALLELISM_NONE : String(mode.parallelism);
|
|
73
|
+
return mode[parameter];
|
|
74
|
+
}
|
|
75
|
+
function parameterItems(parameter, mode) {
|
|
76
|
+
const choices = parameter === "investigate"
|
|
77
|
+
? ["none", "read", "free"]
|
|
78
|
+
: parameter === "selfWork"
|
|
79
|
+
? ["none", "trivial", "small"]
|
|
80
|
+
: parameter === "tools"
|
|
81
|
+
? ["dispatch", "read", "edit"]
|
|
82
|
+
: [PARALLELISM_NONE, "1", "2", "3", "4", "5", "6", "8", "12"];
|
|
83
|
+
const current = modeValue(mode, parameter);
|
|
84
|
+
const ordered = [current, ...choices.filter((choice) => choice !== current)];
|
|
85
|
+
return ordered.map((choice) => ({
|
|
86
|
+
id: choice,
|
|
87
|
+
label: parameter === "parallelism" && choice === PARALLELISM_NONE ? "sem limite" : choice,
|
|
88
|
+
group: ORCHESTRATOR_PICKER_GROUP,
|
|
89
|
+
harness: ORCHESTRATOR_PICKER_GROUP,
|
|
90
|
+
...(choice === current ? { note: "atual" } : {}),
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
export function buildOrchestratorScreen(config = {}, index = 0, total = 1) {
|
|
94
|
+
const mode = resolveOrchestratorMode(config);
|
|
95
|
+
const presets = [
|
|
96
|
+
["dispatcher", DISPATCHER_PRESET],
|
|
97
|
+
["balanced", BALANCED_PRESET],
|
|
98
|
+
["explorer", EXPLORER_PRESET],
|
|
99
|
+
];
|
|
100
|
+
const presetItems = presets.map(([name, preset]) => ({
|
|
101
|
+
id: name,
|
|
102
|
+
label: name,
|
|
103
|
+
group: ORCHESTRATOR_PICKER_GROUP,
|
|
104
|
+
harness: ORCHESTRATOR_PICKER_GROUP,
|
|
105
|
+
note: `${preset.investigate} / ${preset.selfWork} / ${preset.tools}`,
|
|
106
|
+
}));
|
|
107
|
+
return {
|
|
108
|
+
role: ORCHESTRATOR_SCREEN_ROLE,
|
|
109
|
+
title: `orchestrator (${orchestratorModeLabel(mode)})`,
|
|
110
|
+
counter: `agente ${index + 1} de ${total}`,
|
|
111
|
+
description: ORCHESTRATOR_PARALLELISM_NOTE_LINES,
|
|
112
|
+
items: [
|
|
113
|
+
...presetItems,
|
|
114
|
+
{
|
|
115
|
+
id: "custom",
|
|
116
|
+
label: "custom",
|
|
117
|
+
group: ORCHESTRATOR_PICKER_GROUP,
|
|
118
|
+
harness: ORCHESTRATOR_PICKER_GROUP,
|
|
119
|
+
note: "ajustar os quatro parametros",
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
pinned: false,
|
|
123
|
+
known: new Set([
|
|
124
|
+
...presetItems.map((item) => itemKey(item.harness, item.id)),
|
|
125
|
+
itemKey(ORCHESTRATOR_PICKER_GROUP, "custom"),
|
|
126
|
+
]),
|
|
127
|
+
harnesses: new Set([ORCHESTRATOR_PICKER_GROUP]),
|
|
128
|
+
next: (result) => result.kind === "picked" && result.id === "custom"
|
|
129
|
+
? [buildOrchestratorParameterScreen(ORCHESTRATOR_PARAMETERS[0], mode, 0)]
|
|
130
|
+
: [],
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
export function buildSandboxScreen(config = {}, index = 0, total = 1) {
|
|
134
|
+
const selected = config.defaultSandbox === SANDBOX_ON ? SANDBOX_ON : SANDBOX_OFF;
|
|
135
|
+
const values = selected === SANDBOX_ON ? [SANDBOX_ON, SANDBOX_OFF] : [SANDBOX_OFF, SANDBOX_ON];
|
|
136
|
+
const items = values.map((value) => ({
|
|
137
|
+
id: value,
|
|
138
|
+
label: value === SANDBOX_ON ? "ON" : "OFF",
|
|
139
|
+
group: SANDBOX_PICKER_GROUP,
|
|
140
|
+
harness: SANDBOX_PICKER_GROUP,
|
|
141
|
+
...(value === selected ? { note: "atual" } : {}),
|
|
142
|
+
}));
|
|
143
|
+
return {
|
|
144
|
+
role: SANDBOX_SCREEN_ROLE,
|
|
145
|
+
title: "Danger full access",
|
|
146
|
+
counter: `configuração ${index + 1} de ${total}`,
|
|
147
|
+
description: ["Danger full access applies to new Codex sessions."],
|
|
148
|
+
items,
|
|
149
|
+
pinned: true,
|
|
150
|
+
known: new Set(items.map((item) => itemKey(item.harness, item.id))),
|
|
151
|
+
harnesses: new Set([SANDBOX_PICKER_GROUP]),
|
|
152
|
+
next: () => [],
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
export function buildAutocompactScreen(config = {}, index = 0, total = 1) {
|
|
156
|
+
// A hand-tuned block without `enabled` still resolves as active in the
|
|
157
|
+
// Claude driver (only `enabled: false` disables), so show it as ON rather
|
|
158
|
+
// than let an accepted OFF silently switch off a working setup.
|
|
159
|
+
const selected = config.autocompact === undefined || config.autocompact.enabled === false
|
|
160
|
+
? AUTOCOMPACT_OFF
|
|
161
|
+
: AUTOCOMPACT_ON;
|
|
162
|
+
const values = selected === AUTOCOMPACT_ON
|
|
163
|
+
? [AUTOCOMPACT_ON, AUTOCOMPACT_OFF]
|
|
164
|
+
: [AUTOCOMPACT_OFF, AUTOCOMPACT_ON];
|
|
165
|
+
const items = values.map((value) => ({
|
|
166
|
+
id: value,
|
|
167
|
+
label: value.toUpperCase(),
|
|
168
|
+
group: AUTOCOMPACT_PICKER_GROUP,
|
|
169
|
+
harness: AUTOCOMPACT_PICKER_GROUP,
|
|
170
|
+
...(value === selected ? { note: "atual" } : {}),
|
|
171
|
+
}));
|
|
172
|
+
return {
|
|
173
|
+
role: AUTOCOMPACT_SCREEN_ROLE,
|
|
174
|
+
title: "Claude native autocompact",
|
|
175
|
+
counter: `configuração ${index + 1} de ${total}`,
|
|
176
|
+
description: [AUTOCOMPACT_DESCRIPTION],
|
|
177
|
+
items,
|
|
178
|
+
pinned: true,
|
|
179
|
+
known: new Set(items.map((item) => itemKey(item.harness, item.id))),
|
|
180
|
+
harnesses: new Set([AUTOCOMPACT_PICKER_GROUP]),
|
|
181
|
+
next: () => [],
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function modeAfterParameter(mode, result) {
|
|
185
|
+
const parameters = collectOrchestratorParameters([result], mode);
|
|
186
|
+
return orchestratorConfigFromSelection("custom", parameters) ?? mode;
|
|
187
|
+
}
|
|
188
|
+
function buildOrchestratorParameterScreen(parameter, mode, index, error) {
|
|
189
|
+
const items = parameterItems(parameter, mode);
|
|
190
|
+
const description = parameter === "parallelism"
|
|
191
|
+
? [...ORCHESTRATOR_PARALLELISM_NOTE_LINES, "Type orchestrator:N for another positive cap."]
|
|
192
|
+
: ORCHESTRATOR_PARALLELISM_NOTE_LINES;
|
|
193
|
+
return {
|
|
194
|
+
role: `${ORCHESTRATOR_SCREEN_ROLE}.${parameter}`,
|
|
195
|
+
title: `orchestrator (${orchestratorModeLabel(mode)}) · ${parameter}`,
|
|
196
|
+
counter: `parametro ${index + 1} de ${ORCHESTRATOR_PARAMETERS.length}`,
|
|
197
|
+
...(error === undefined ? {} : { error }),
|
|
198
|
+
description,
|
|
199
|
+
items,
|
|
200
|
+
pinned: false,
|
|
201
|
+
known: new Set(items.map((item) => itemKey(item.harness, item.id))),
|
|
202
|
+
harnesses: new Set([ORCHESTRATOR_PICKER_GROUP]),
|
|
203
|
+
next: (result) => {
|
|
204
|
+
if (parameter === "parallelism" &&
|
|
205
|
+
result.kind === "picked" &&
|
|
206
|
+
result.id !== PARALLELISM_NONE &&
|
|
207
|
+
numericParallelism(result.id) === undefined) {
|
|
208
|
+
return [
|
|
209
|
+
buildOrchestratorParameterScreen(parameter, mode, index, "parallelism must be a positive finite number"),
|
|
210
|
+
];
|
|
211
|
+
}
|
|
212
|
+
if (index + 1 >= ORCHESTRATOR_PARAMETERS.length)
|
|
213
|
+
return [];
|
|
214
|
+
const nextIndex = index + 1;
|
|
215
|
+
return [
|
|
216
|
+
buildOrchestratorParameterScreen(ORCHESTRATOR_PARAMETERS[nextIndex], modeAfterParameter(mode, result), nextIndex),
|
|
217
|
+
];
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function numericParallelism(value) {
|
|
222
|
+
const parsed = Number(value);
|
|
223
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
224
|
+
}
|
|
225
|
+
/** Keeps skipped parameter screens at the mode shown when customization began. */
|
|
226
|
+
export function collectOrchestratorParameters(results, existing) {
|
|
227
|
+
const parameters = {
|
|
228
|
+
investigate: existing.investigate,
|
|
229
|
+
selfWork: existing.selfWork,
|
|
230
|
+
tools: existing.tools,
|
|
231
|
+
...(existing.parallelism === undefined ? {} : { parallelism: existing.parallelism }),
|
|
232
|
+
};
|
|
233
|
+
for (const result of results) {
|
|
234
|
+
if (result.kind !== "picked")
|
|
235
|
+
continue;
|
|
236
|
+
const parameter = result.role.startsWith(`${ORCHESTRATOR_SCREEN_ROLE}.`)
|
|
237
|
+
? result.role.slice(`${ORCHESTRATOR_SCREEN_ROLE}.`.length)
|
|
238
|
+
: "";
|
|
239
|
+
if (parameter === "investigate" && ["none", "read", "free"].includes(result.id)) {
|
|
240
|
+
parameters.investigate = result.id;
|
|
241
|
+
}
|
|
242
|
+
else if (parameter === "selfWork" && ["none", "trivial", "small"].includes(result.id)) {
|
|
243
|
+
parameters.selfWork = result.id;
|
|
244
|
+
}
|
|
245
|
+
else if (parameter === "tools" && ["dispatch", "read", "edit"].includes(result.id)) {
|
|
246
|
+
parameters.tools = result.id;
|
|
247
|
+
}
|
|
248
|
+
else if (parameter === "parallelism") {
|
|
249
|
+
if (result.id === PARALLELISM_NONE)
|
|
250
|
+
delete parameters.parallelism;
|
|
251
|
+
else {
|
|
252
|
+
const parsed = numericParallelism(result.id);
|
|
253
|
+
if (parsed !== undefined)
|
|
254
|
+
parameters.parallelism = parsed;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return parameters;
|
|
259
|
+
}
|
|
260
|
+
export function collectOrchestratorSelection(selection, parameters, existing = {}) {
|
|
261
|
+
if (selection?.kind !== "picked")
|
|
262
|
+
return undefined;
|
|
263
|
+
if (selection.id !== "custom")
|
|
264
|
+
return orchestratorConfigFromSelection(selection);
|
|
265
|
+
const current = resolveOrchestratorMode(existing);
|
|
266
|
+
return orchestratorConfigFromSelection("custom", collectOrchestratorParameters(parameters, current));
|
|
267
|
+
}
|
|
6
268
|
/**
|
|
7
269
|
* Both streams have to be a terminal, not just stdout. A piped stdin leaves
|
|
8
270
|
* `question` waiting on input that can never arrive, and `tail -f /dev/null |
|
|
@@ -18,67 +280,122 @@ export function isInteractiveTerminal() {
|
|
|
18
280
|
export function needsModelSetup(config, isTTY = isInteractiveTerminal()) {
|
|
19
281
|
if (!isTTY)
|
|
20
282
|
return false;
|
|
21
|
-
|
|
283
|
+
// `agents` is what setup writes now. A config carrying only the older
|
|
284
|
+
// per-harness `models` has never answered the per-role question, so it still
|
|
285
|
+
// counts as unset.
|
|
286
|
+
return config == null || config.agents == null;
|
|
22
287
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
288
|
+
/**
|
|
289
|
+
* The catalog of every installed harness, flattened into one list.
|
|
290
|
+
*
|
|
291
|
+
* The group is the bare agent id rather than a prettier label because it is
|
|
292
|
+
* also what free text has to type as a prefix ("codex:gpt-5.7"). A header
|
|
293
|
+
* reading "Codex" over rows you reach by typing "codex" would hide that.
|
|
294
|
+
*/
|
|
295
|
+
/** One normalization for every id, so the catalog and the pin agree on a key. */
|
|
296
|
+
function modelId(raw) {
|
|
297
|
+
return typeof raw === "string" ? raw.trim() : "";
|
|
298
|
+
}
|
|
299
|
+
function catalogItems(installed) {
|
|
300
|
+
const known = new Set();
|
|
301
|
+
const items = [];
|
|
302
|
+
for (const harness of installed) {
|
|
303
|
+
for (const provider of harness.providers) {
|
|
304
|
+
for (const model of provider.models) {
|
|
305
|
+
// A driver reporting a blank id used to become a selectable blank row
|
|
306
|
+
// that Enter would save as the model. The line wizard guarded this and
|
|
307
|
+
// the port dropped the guard.
|
|
308
|
+
const id = modelId(model.id);
|
|
309
|
+
if (!id)
|
|
310
|
+
continue;
|
|
311
|
+
// Keyed by both halves: two harnesses listing the same id are two real
|
|
312
|
+
// choices, and only a repeat within one harness is a duplicate.
|
|
313
|
+
const key = itemKey(harness.agent, id);
|
|
314
|
+
if (known.has(key))
|
|
315
|
+
continue;
|
|
316
|
+
known.add(key);
|
|
317
|
+
// The name is carried for the filter, not for the row: most of the
|
|
318
|
+
// catalog spells a version readably there ("Claude Opus 5") and only in
|
|
319
|
+
// the id the way it is typed.
|
|
320
|
+
const name = typeof model.name === "string" ? model.name.trim() : "";
|
|
321
|
+
items.push({
|
|
322
|
+
id,
|
|
323
|
+
label: id,
|
|
324
|
+
group: harness.agent,
|
|
325
|
+
harness: harness.agent,
|
|
326
|
+
...(name && name !== id ? { name } : {}),
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return { items, known };
|
|
26
332
|
}
|
|
27
|
-
function pinnedFor(
|
|
333
|
+
function pinnedFor(installed, known, configured, fallbackHarness) {
|
|
28
334
|
if (configured) {
|
|
335
|
+
const inCatalog = known.has(itemKey(configured.harness, configured.model));
|
|
29
336
|
// A saved id the catalog no longer lists still gets shown. Hiding it is how
|
|
30
337
|
// the wizard used to answer `open`'s own "that model is gone, run setup":
|
|
31
338
|
// the user arrived here and could not see which model it meant. Marking it
|
|
32
339
|
// synthetic makes keeping it cost the same second Enter as typing it.
|
|
33
|
-
return
|
|
34
|
-
|
|
35
|
-
|
|
340
|
+
return {
|
|
341
|
+
id: configured.model,
|
|
342
|
+
label: configured.model,
|
|
343
|
+
group: configured.harness,
|
|
344
|
+
harness: configured.harness,
|
|
345
|
+
note: inCatalog ? `atual · ${configured.harness}` : `atual · ${configured.harness}, fora do catalogo`,
|
|
346
|
+
...(inCatalog ? {} : { synthetic: true }),
|
|
347
|
+
};
|
|
36
348
|
}
|
|
349
|
+
// Nothing saved for this role, so the suggestion comes from the harness that
|
|
350
|
+
// runs everything else by default, and only if that harness names a default.
|
|
351
|
+
const harness = installed.find((candidate) => candidate.agent === fallbackHarness);
|
|
352
|
+
if (!harness)
|
|
353
|
+
return undefined;
|
|
37
354
|
for (const provider of harness.providers) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
355
|
+
// Trimmed the same way the catalog trims, or the lookup misses its own row
|
|
356
|
+
// and a harness that pads its ids ends up pinning nothing.
|
|
357
|
+
const real = provider.models
|
|
358
|
+
.map((model) => ({ id: modelId(model.id), isDefault: model.isDefault }))
|
|
359
|
+
.find((model) => model.isDefault && known.has(itemKey(harness.agent, model.id)));
|
|
360
|
+
if (real) {
|
|
361
|
+
return {
|
|
362
|
+
id: real.id,
|
|
363
|
+
label: real.id,
|
|
364
|
+
group: harness.agent,
|
|
365
|
+
harness: harness.agent,
|
|
366
|
+
note: `padrao · ${harness.agent}`,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
41
369
|
}
|
|
42
|
-
// With no config and no isDefault,
|
|
43
|
-
// that and calling it the default would stamp a coin flip as a
|
|
370
|
+
// With no config and no isDefault, the first row is just catalog order.
|
|
371
|
+
// Hoisting that and calling it the default would stamp a coin flip as a
|
|
372
|
+
// recommendation.
|
|
44
373
|
}
|
|
45
|
-
export function
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
// catalog spells a version readably there ("Claude Opus 5") and only in
|
|
59
|
-
// the id the way it is typed.
|
|
60
|
-
const name = typeof model.name === "string" ? model.name.trim() : "";
|
|
61
|
-
grouped.push({
|
|
62
|
-
id,
|
|
63
|
-
label: id,
|
|
64
|
-
group: provider.provider,
|
|
65
|
-
...(name && name !== id ? { name } : {}),
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
const pinned = pinnedFor(harness, [...seen], configured);
|
|
70
|
-
const items = pinned ? [pinned, ...grouped.filter((item) => item.id !== pinned.id)] : grouped;
|
|
374
|
+
export function buildRoleScreen(role, installed, index, total, configured, fallbackHarness = "claude") {
|
|
375
|
+
const { items: catalog, known } = catalogItems(installed);
|
|
376
|
+
const pinned = pinnedFor(installed, known, configured, fallbackHarness);
|
|
377
|
+
const pinnedKey = pinned ? itemKey(pinned.harness, pinned.id) : undefined;
|
|
378
|
+
const items = pinned
|
|
379
|
+
? [pinned, ...catalog.filter((item) => itemKey(item.harness, item.id) !== pinnedKey)]
|
|
380
|
+
: catalog;
|
|
381
|
+
// One screen now spans every harness, so a harness that answered with an
|
|
382
|
+
// error no longer owns the error line by itself. Naming each one keeps a
|
|
383
|
+
// missing catalog readable as "codex is why codex has no rows".
|
|
384
|
+
const errors = installed
|
|
385
|
+
.filter((harness) => harness.error)
|
|
386
|
+
.map((harness) => `${harness.agent}: ${harness.error}`);
|
|
71
387
|
return {
|
|
72
|
-
|
|
73
|
-
title:
|
|
388
|
+
role,
|
|
389
|
+
title: role,
|
|
74
390
|
counter: `agente ${index + 1} de ${total}`,
|
|
75
|
-
error:
|
|
391
|
+
...(errors.length > 0 ? { error: errors.join(" · ") } : {}),
|
|
76
392
|
items,
|
|
77
393
|
pinned: pinned !== undefined,
|
|
78
|
-
known
|
|
394
|
+
known,
|
|
395
|
+
harnesses: new Set(installed.map((harness) => harness.agent)),
|
|
79
396
|
};
|
|
80
397
|
}
|
|
81
|
-
export function buildScreens(harnesses, configured) {
|
|
398
|
+
export function buildScreens(roles, harnesses, configured, fallbackHarness = "claude") {
|
|
82
399
|
const seen = new Set();
|
|
83
400
|
// A merged disk cache can hand back the same agent twice.
|
|
84
401
|
const installed = harnesses.filter((harness) => {
|
|
@@ -87,18 +404,21 @@ export function buildScreens(harnesses, configured) {
|
|
|
87
404
|
seen.add(harness.agent);
|
|
88
405
|
return true;
|
|
89
406
|
});
|
|
90
|
-
|
|
407
|
+
if (installed.length === 0)
|
|
408
|
+
return [];
|
|
409
|
+
return roles.map((role, index) => buildRoleScreen(role, installed, index, roles.length, configured[role], fallbackHarness));
|
|
91
410
|
}
|
|
92
411
|
export function collectSelections(results, existing, shown) {
|
|
93
412
|
// Starts from what was already saved: skipping has to leave it intact, and a
|
|
94
413
|
// brand new map would wipe a choice made on an earlier run.
|
|
95
|
-
const
|
|
414
|
+
const agents = { ...(existing ?? {}) };
|
|
96
415
|
for (const result of results) {
|
|
97
|
-
if (result.kind
|
|
98
|
-
|
|
416
|
+
if (result.kind !== "picked")
|
|
417
|
+
continue;
|
|
418
|
+
agents[result.role] = { harness: result.harness, model: result.id };
|
|
99
419
|
}
|
|
100
420
|
const aborted = results.some((result) => result.kind === "aborted");
|
|
101
|
-
return {
|
|
421
|
+
return { agents, write: shown > 0 && !aborted };
|
|
102
422
|
}
|
|
103
423
|
/** Below this no list survives once the chrome is placed. */
|
|
104
424
|
const MIN_ROWS = 8;
|
|
@@ -131,11 +451,11 @@ export async function runModelSetupWizard(options = {}) {
|
|
|
131
451
|
console.error(`Warning: Could not discover models: ${error instanceof Error ? error.message : String(error)}`);
|
|
132
452
|
harnesses = [];
|
|
133
453
|
}
|
|
134
|
-
const
|
|
135
|
-
// Writing `
|
|
454
|
+
const roleScreens = buildScreens(ROLES, harnesses, config.agents ?? {}, config.defaultAgent ?? "claude");
|
|
455
|
+
// Writing `agents` is what marks first-run setup as done. Doing that after
|
|
136
456
|
// showing nothing would spend the single prompt the user ever gets.
|
|
137
|
-
if (
|
|
138
|
-
console.error("Warning: No installed
|
|
457
|
+
if (roleScreens.length === 0) {
|
|
458
|
+
console.error("Warning: No installed harness reported any model; skipping model setup.");
|
|
139
459
|
return config;
|
|
140
460
|
}
|
|
141
461
|
const dim = options.dimensions ?? readDimensions(output);
|
|
@@ -144,39 +464,636 @@ export async function runModelSetupWizard(options = {}) {
|
|
|
144
464
|
return config;
|
|
145
465
|
}
|
|
146
466
|
const paint = colors(Boolean(output.isTTY) && !process.env.NO_COLOR);
|
|
147
|
-
const
|
|
148
|
-
|
|
467
|
+
const screens = [
|
|
468
|
+
...roleScreens,
|
|
469
|
+
buildOrchestratorScreen(config, roleScreens.length, roleScreens.length + 1),
|
|
470
|
+
];
|
|
471
|
+
const total = screens.length + 2;
|
|
472
|
+
const sandbox = buildSandboxScreen(config, screens.length, total);
|
|
473
|
+
const autocompact = buildAutocompactScreen(config, screens.length + 1, total);
|
|
474
|
+
const results = await runScreens([...screens, sandbox, autocompact], { input, output, onResize: watchResize }, paint);
|
|
475
|
+
if (results.some((result) => result.kind === "aborted")) {
|
|
476
|
+
console.error("Warning: Model setup was interrupted; nothing was saved.");
|
|
477
|
+
return config;
|
|
478
|
+
}
|
|
479
|
+
const answeredResults = results.filter((result) => result.kind !== "aborted");
|
|
480
|
+
const agentResults = answeredResults.filter((result) => roleScreens.some((screen) => screen.role === result.role));
|
|
481
|
+
const { agents, write } = collectSelections(agentResults, config.agents, roleScreens.length);
|
|
149
482
|
if (!write) {
|
|
150
483
|
console.error("Warning: Model setup was interrupted; nothing was saved.");
|
|
151
484
|
return config;
|
|
152
485
|
}
|
|
153
|
-
const
|
|
486
|
+
const orchestratorSelection = answeredResults.find((result) => result.role === ORCHESTRATOR_SCREEN_ROLE);
|
|
487
|
+
if (orchestratorSelection === undefined) {
|
|
488
|
+
console.error("Warning: Orchestrator setup was interrupted; nothing was saved.");
|
|
489
|
+
return config;
|
|
490
|
+
}
|
|
491
|
+
let orchestrator;
|
|
492
|
+
if (orchestratorSelection.kind === "picked" && orchestratorSelection.id === "custom") {
|
|
493
|
+
const parameterResults = answeredResults.filter((result) => result.role.startsWith(`${ORCHESTRATOR_SCREEN_ROLE}.`));
|
|
494
|
+
orchestrator = collectOrchestratorSelection(orchestratorSelection, parameterResults, config);
|
|
495
|
+
if (orchestrator === undefined) {
|
|
496
|
+
console.error("Warning: Orchestrator setup was invalid; nothing was saved.");
|
|
497
|
+
return config;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
else if (orchestratorSelection.kind === "picked") {
|
|
501
|
+
orchestrator = collectOrchestratorSelection(orchestratorSelection, [], config);
|
|
502
|
+
if (orchestrator === undefined) {
|
|
503
|
+
console.error("Warning: Orchestrator setup was invalid; nothing was saved.");
|
|
504
|
+
return config;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
const updatedConfig = { ...config, agents };
|
|
508
|
+
if (orchestrator !== undefined)
|
|
509
|
+
updatedConfig.orchestrator = orchestrator;
|
|
510
|
+
const sandboxSelection = answeredResults.find((result) => result.role === SANDBOX_SCREEN_ROLE);
|
|
511
|
+
if (sandboxSelection?.kind === "picked") {
|
|
512
|
+
if (sandboxSelection.id === SANDBOX_OFF || sandboxSelection.id === SANDBOX_ON) {
|
|
513
|
+
updatedConfig.defaultSandbox = sandboxSelection.id;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const autocompactSelection = answeredResults.find((result) => result.role === AUTOCOMPACT_SCREEN_ROLE);
|
|
517
|
+
if (autocompactSelection?.kind === "picked") {
|
|
518
|
+
if (autocompactSelection.id === AUTOCOMPACT_ON) {
|
|
519
|
+
updatedConfig.autocompact = { ...config.autocompact, enabled: true };
|
|
520
|
+
}
|
|
521
|
+
else if (autocompactSelection.id === AUTOCOMPACT_OFF && config.autocompact !== undefined) {
|
|
522
|
+
updatedConfig.autocompact = { ...config.autocompact, enabled: false };
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
let saved = false;
|
|
154
526
|
try {
|
|
155
527
|
(options.save ?? saveConfig)(updatedConfig);
|
|
528
|
+
saved = true;
|
|
156
529
|
}
|
|
157
530
|
catch (error) {
|
|
158
531
|
console.error(`Warning: Could not save config: ${error instanceof Error ? error.message : String(error)}`);
|
|
159
532
|
}
|
|
160
|
-
|
|
161
|
-
|
|
533
|
+
if (!saved)
|
|
534
|
+
return updatedConfig;
|
|
535
|
+
const summary = roleScreens
|
|
536
|
+
.map((screen) => {
|
|
537
|
+
const binding = agents[screen.role];
|
|
538
|
+
return `${screen.role} ${binding ? `${binding.harness}:${binding.model}` : "unset"}`;
|
|
539
|
+
})
|
|
162
540
|
.join(" · ");
|
|
163
|
-
|
|
541
|
+
const modeSummary = orchestrator === undefined ? "" : ` · orchestrator ${orchestratorModeLabel(orchestrator)}`;
|
|
542
|
+
output.write(`\n saved: ${summary}${modeSummary}\n\n`);
|
|
164
543
|
return updatedConfig;
|
|
165
544
|
}
|
|
166
|
-
export
|
|
545
|
+
export class SetupUsageError extends Error {
|
|
546
|
+
constructor(message) {
|
|
547
|
+
super(message);
|
|
548
|
+
this.name = "SetupUsageError";
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
function invalidBindMessage(value) {
|
|
552
|
+
return `Invalid --bind "${value}": expected role=harness:model (role: general|orchestrator|reviewer|auditor; harness: claude|codex|opencode|omp; model: non-empty and without whitespace, control characters or '=')`;
|
|
553
|
+
}
|
|
554
|
+
export function parseBind(value) {
|
|
555
|
+
const equals = value.indexOf("=");
|
|
556
|
+
const roleValue = equals < 0 ? "" : value.slice(0, equals);
|
|
557
|
+
const right = equals < 0 ? "" : value.slice(equals + 1);
|
|
558
|
+
const colon = right.indexOf(":");
|
|
559
|
+
const harnessValue = colon < 0 ? "" : right.slice(0, colon);
|
|
560
|
+
const model = colon < 0 ? "" : right.slice(colon + 1);
|
|
561
|
+
const role = ROLES.includes(roleValue) ? roleValue : undefined;
|
|
562
|
+
const harness = isAgentId(harnessValue) ? harnessValue : undefined;
|
|
563
|
+
const modelPattern = /^[^\p{White_Space}\p{Cc}\p{Cf}=]+$/u;
|
|
564
|
+
if (!role || !harness || !modelPattern.test(model))
|
|
565
|
+
throw new SetupUsageError(invalidBindMessage(value));
|
|
566
|
+
return { role, binding: { harness, model } };
|
|
567
|
+
}
|
|
568
|
+
export const parseSetupBind = parseBind;
|
|
569
|
+
function parseError(message, json) {
|
|
570
|
+
return { ok: false, message, json };
|
|
571
|
+
}
|
|
572
|
+
function isSetupFlag(arg) {
|
|
573
|
+
return arg.startsWith("-");
|
|
574
|
+
}
|
|
575
|
+
export function parseSetupArgs(args) {
|
|
576
|
+
const json = args.some((arg) => arg === "--json" || arg.startsWith("--json="));
|
|
577
|
+
const binds = [];
|
|
578
|
+
let refresh = false;
|
|
579
|
+
let nonInteractive = false;
|
|
580
|
+
let dryRun = false;
|
|
581
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
582
|
+
const arg = args[index];
|
|
583
|
+
if (arg === "--refresh") {
|
|
584
|
+
refresh = true;
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
if (arg === "--non-interactive") {
|
|
588
|
+
nonInteractive = true;
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
if (arg === "--dry-run") {
|
|
592
|
+
dryRun = true;
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
if (arg === "--json") {
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
if (arg === "--bind") {
|
|
599
|
+
const value = args[index + 1];
|
|
600
|
+
if (value === undefined || isSetupFlag(value)) {
|
|
601
|
+
return parseError('Option "--bind" expects role=harness:model.', json);
|
|
602
|
+
}
|
|
603
|
+
index += 1;
|
|
604
|
+
try {
|
|
605
|
+
binds.push(parseBind(value));
|
|
606
|
+
}
|
|
607
|
+
catch (error) {
|
|
608
|
+
return parseError(error instanceof Error ? error.message : String(error), json);
|
|
609
|
+
}
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
if (arg.startsWith("--bind=")) {
|
|
613
|
+
try {
|
|
614
|
+
binds.push(parseBind(arg.slice("--bind=".length)));
|
|
615
|
+
}
|
|
616
|
+
catch (error) {
|
|
617
|
+
return parseError(error instanceof Error ? error.message : String(error), json);
|
|
618
|
+
}
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
if (arg.startsWith("-"))
|
|
622
|
+
return parseError(`Unknown option "${arg}".`, json);
|
|
623
|
+
return parseError(`Unexpected argument "${arg}".`, json);
|
|
624
|
+
}
|
|
625
|
+
return {
|
|
626
|
+
ok: true,
|
|
627
|
+
options: {
|
|
628
|
+
refresh,
|
|
629
|
+
nonInteractive,
|
|
630
|
+
json,
|
|
631
|
+
dryRun,
|
|
632
|
+
binds,
|
|
633
|
+
batch: nonInteractive || json || dryRun || binds.length > 0,
|
|
634
|
+
},
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
function writeLine(stream, message) {
|
|
638
|
+
if (stream) {
|
|
639
|
+
stream.write(`${message}\n`);
|
|
640
|
+
}
|
|
641
|
+
else {
|
|
642
|
+
console.error(message);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
function configValidation(read) {
|
|
646
|
+
return {
|
|
647
|
+
status: read.status,
|
|
648
|
+
source: read.source,
|
|
649
|
+
path: read.path,
|
|
650
|
+
message: read.status === "invalid" ? read.message : null,
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
function notNeededCatalog() {
|
|
654
|
+
return { status: "not-needed", source: "none", ageMs: null, message: null };
|
|
655
|
+
}
|
|
656
|
+
function emptyEnvelope(message, code, path) {
|
|
657
|
+
return {
|
|
658
|
+
proposta: null,
|
|
659
|
+
validacoes: {
|
|
660
|
+
config: { status: "not-run", source: "none", path, message: "not-run" },
|
|
661
|
+
catalogo: { status: "not-needed", source: "none", ageMs: null, message: "not-run" },
|
|
662
|
+
bindings: [],
|
|
663
|
+
},
|
|
664
|
+
mudancas: [],
|
|
665
|
+
resultado: { status: "error", code, saved: false, message },
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
function jsonObject(value) {
|
|
669
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
670
|
+
}
|
|
671
|
+
function jsonValue(value) {
|
|
672
|
+
if (value === undefined)
|
|
673
|
+
return null;
|
|
674
|
+
if (Array.isArray(value))
|
|
675
|
+
return value.map(jsonValue);
|
|
676
|
+
if (jsonObject(value)) {
|
|
677
|
+
return Object.fromEntries(Object.keys(value).sort((left, right) => left.localeCompare(right)).map((key) => [key, jsonValue(value[key])]));
|
|
678
|
+
}
|
|
679
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null) {
|
|
680
|
+
return value;
|
|
681
|
+
}
|
|
682
|
+
return null;
|
|
683
|
+
}
|
|
684
|
+
function jsonEqual(left, right) {
|
|
685
|
+
return JSON.stringify(jsonValue(left)) === JSON.stringify(jsonValue(right));
|
|
686
|
+
}
|
|
687
|
+
function pointerPart(value) {
|
|
688
|
+
return value.replaceAll("~", "~0").replaceAll("/", "~1");
|
|
689
|
+
}
|
|
690
|
+
function diffAt(output, pathValue, beforePresent, before, afterPresent, after, expandObjectChildren = false) {
|
|
691
|
+
const beforeObject = beforePresent && jsonObject(before) ? before : undefined;
|
|
692
|
+
const afterObject = afterPresent && jsonObject(after) ? after : undefined;
|
|
693
|
+
if (beforeObject !== undefined && afterObject !== undefined) {
|
|
694
|
+
const keys = [...new Set([...Object.keys(beforeObject), ...Object.keys(afterObject)])].sort((left, right) => left.localeCompare(right));
|
|
695
|
+
if (keys.length === 0)
|
|
696
|
+
return;
|
|
697
|
+
for (const key of keys) {
|
|
698
|
+
diffAt(output, `${pathValue}/${pointerPart(key)}`, Object.hasOwn(beforeObject, key), beforeObject[key], Object.hasOwn(afterObject, key), afterObject[key]);
|
|
699
|
+
}
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
if (!beforePresent && afterObject !== undefined) {
|
|
703
|
+
if (expandObjectChildren) {
|
|
704
|
+
const keys = Object.keys(afterObject).sort((left, right) => left.localeCompare(right));
|
|
705
|
+
if (keys.length === 0) {
|
|
706
|
+
output.push({
|
|
707
|
+
path: pathValue,
|
|
708
|
+
beforePresent: false,
|
|
709
|
+
before: null,
|
|
710
|
+
afterPresent: true,
|
|
711
|
+
after: jsonValue(afterObject),
|
|
712
|
+
});
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
for (const key of keys) {
|
|
716
|
+
const value = afterObject[key];
|
|
717
|
+
const childPath = `${pathValue}/${pointerPart(key)}`;
|
|
718
|
+
if (jsonObject(value)) {
|
|
719
|
+
output.push({
|
|
720
|
+
path: childPath,
|
|
721
|
+
beforePresent: false,
|
|
722
|
+
before: null,
|
|
723
|
+
afterPresent: true,
|
|
724
|
+
after: jsonValue(value),
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
else {
|
|
728
|
+
diffAt(output, childPath, false, undefined, true, value);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
output.push({
|
|
734
|
+
path: pathValue,
|
|
735
|
+
beforePresent: false,
|
|
736
|
+
before: null,
|
|
737
|
+
afterPresent: true,
|
|
738
|
+
after: jsonValue(afterObject),
|
|
739
|
+
});
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
if (beforeObject !== undefined && !afterPresent) {
|
|
743
|
+
if (expandObjectChildren) {
|
|
744
|
+
const keys = Object.keys(beforeObject).sort((left, right) => left.localeCompare(right));
|
|
745
|
+
if (keys.length === 0) {
|
|
746
|
+
output.push({
|
|
747
|
+
path: pathValue,
|
|
748
|
+
beforePresent: true,
|
|
749
|
+
before: jsonValue(beforeObject),
|
|
750
|
+
afterPresent: false,
|
|
751
|
+
after: null,
|
|
752
|
+
});
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
for (const key of keys) {
|
|
756
|
+
const value = beforeObject[key];
|
|
757
|
+
const childPath = `${pathValue}/${pointerPart(key)}`;
|
|
758
|
+
if (jsonObject(value)) {
|
|
759
|
+
output.push({
|
|
760
|
+
path: childPath,
|
|
761
|
+
beforePresent: true,
|
|
762
|
+
before: jsonValue(value),
|
|
763
|
+
afterPresent: false,
|
|
764
|
+
after: null,
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
else {
|
|
768
|
+
diffAt(output, childPath, true, value, false, undefined);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
output.push({
|
|
774
|
+
path: pathValue,
|
|
775
|
+
beforePresent: true,
|
|
776
|
+
before: jsonValue(beforeObject),
|
|
777
|
+
afterPresent: false,
|
|
778
|
+
after: null,
|
|
779
|
+
});
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
if (beforePresent === afterPresent && (!beforePresent || jsonEqual(before, after)))
|
|
783
|
+
return;
|
|
784
|
+
output.push({
|
|
785
|
+
path: pathValue,
|
|
786
|
+
beforePresent,
|
|
787
|
+
before: jsonValue(before),
|
|
788
|
+
afterPresent,
|
|
789
|
+
after: jsonValue(after),
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
export function diffConfig(before, after) {
|
|
793
|
+
const output = [];
|
|
794
|
+
const beforeObject = jsonObject(before) ? before : {};
|
|
795
|
+
const afterObject = jsonObject(after) ? after : {};
|
|
796
|
+
const keys = [...new Set([...Object.keys(beforeObject), ...Object.keys(afterObject)])].sort((left, right) => left.localeCompare(right));
|
|
797
|
+
for (const key of keys) {
|
|
798
|
+
diffAt(output, `/${pointerPart(key)}`, Object.hasOwn(beforeObject, key), beforeObject[key], Object.hasOwn(afterObject, key), afterObject[key], true);
|
|
799
|
+
}
|
|
800
|
+
return output.sort((left, right) => left.path.localeCompare(right.path));
|
|
801
|
+
}
|
|
802
|
+
function catalogContains(catalog, model) {
|
|
803
|
+
return catalog.providers.some((provider) => provider.models.some((candidate) => candidate.id === model || (candidate.aliases !== undefined && candidate.aliases.some((alias) => alias === model))));
|
|
804
|
+
}
|
|
805
|
+
function validateBindings(bindings, catalog) {
|
|
806
|
+
const byAgent = new Map(catalog.models.map((item) => [item.agent, item]));
|
|
807
|
+
const entries = bindings.map(({ role, binding }) => {
|
|
808
|
+
const found = byAgent.get(binding.harness);
|
|
809
|
+
if (found?.available === false) {
|
|
810
|
+
return {
|
|
811
|
+
role,
|
|
812
|
+
harness: binding.harness,
|
|
813
|
+
model: binding.model,
|
|
814
|
+
status: "harness-unavailable",
|
|
815
|
+
message: `Cannot apply binding for role "${role}": harness "${binding.harness}" is unavailable.`,
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
if (catalog.status === "unavailable" || found === undefined) {
|
|
819
|
+
return {
|
|
820
|
+
role,
|
|
821
|
+
harness: binding.harness,
|
|
822
|
+
model: binding.model,
|
|
823
|
+
status: "unverified",
|
|
824
|
+
message: `Cannot validate model "${binding.model}" for harness "${binding.harness}": catalog unavailable.`,
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
if (catalog.status === "offline" && !catalogContains(found, binding.model)) {
|
|
828
|
+
return {
|
|
829
|
+
role,
|
|
830
|
+
harness: binding.harness,
|
|
831
|
+
model: binding.model,
|
|
832
|
+
status: "unverified",
|
|
833
|
+
message: `Cannot validate model "${binding.model}" for harness "${binding.harness}": the catalog is stale. Re-run with --refresh.`,
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
if (!catalogContains(found, binding.model)) {
|
|
837
|
+
return {
|
|
838
|
+
role,
|
|
839
|
+
harness: binding.harness,
|
|
840
|
+
model: binding.model,
|
|
841
|
+
status: "unknown-model",
|
|
842
|
+
message: `Model "${binding.model}" is not in the ${binding.harness} catalog for role "${role}".`,
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
return { role, harness: binding.harness, model: binding.model, status: "accepted", message: "" };
|
|
846
|
+
});
|
|
847
|
+
const failed = entries.find((entry) => entry.status !== "accepted");
|
|
848
|
+
const code = failed === undefined
|
|
849
|
+
? undefined
|
|
850
|
+
: failed.status === "harness-unavailable"
|
|
851
|
+
? 11
|
|
852
|
+
: failed.status === "unknown-model"
|
|
853
|
+
? 12
|
|
854
|
+
: 13;
|
|
855
|
+
let message = null;
|
|
856
|
+
if (catalog.status === "offline") {
|
|
857
|
+
message = failed?.status === "unverified" ? failed.message : "Catalog is stale; using offline cache.";
|
|
858
|
+
}
|
|
859
|
+
else if (catalog.status === "unavailable") {
|
|
860
|
+
message = failed?.message ?? "Catalog unavailable.";
|
|
861
|
+
}
|
|
862
|
+
return { entries, code, message };
|
|
863
|
+
}
|
|
864
|
+
function catalogValidation(catalog, message) {
|
|
865
|
+
if (catalog === undefined)
|
|
866
|
+
return notNeededCatalog();
|
|
867
|
+
return {
|
|
868
|
+
status: catalog.status,
|
|
869
|
+
source: catalog.source,
|
|
870
|
+
ageMs: catalog.ageMs,
|
|
871
|
+
message,
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
function configSummary(config) {
|
|
875
|
+
const agents = config.agents ?? {};
|
|
876
|
+
return ROLES.map((role) => {
|
|
877
|
+
const binding = agents[role];
|
|
878
|
+
return `${role} ${binding ? `${binding.harness}:${binding.model}` : "unset"}`;
|
|
879
|
+
}).join(" · ");
|
|
880
|
+
}
|
|
881
|
+
function errorResult(read, proposal, catalog, bindings, changes, code, message) {
|
|
882
|
+
return {
|
|
883
|
+
code,
|
|
884
|
+
envelope: {
|
|
885
|
+
proposta: proposal,
|
|
886
|
+
validacoes: { config: configValidation(read), catalogo: catalog, bindings },
|
|
887
|
+
mudancas: changes,
|
|
888
|
+
resultado: { status: "error", code, saved: false, message },
|
|
889
|
+
},
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
export async function runSetupBatch(options, dependencies = {}) {
|
|
893
|
+
const stderr = dependencies.stderr;
|
|
894
|
+
const store = dependencies.configStore ?? createSetupConfigStore();
|
|
895
|
+
let read;
|
|
896
|
+
try {
|
|
897
|
+
read = store.read();
|
|
898
|
+
}
|
|
899
|
+
catch (error) {
|
|
900
|
+
const path = getPaths().configFile;
|
|
901
|
+
const message = `Cannot save config "${path}": ${error instanceof Error ? error.message : String(error)}.`;
|
|
902
|
+
writeLine(stderr, message);
|
|
903
|
+
return {
|
|
904
|
+
code: 15,
|
|
905
|
+
envelope: emptyEnvelope(message, 15, path),
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
if (read.status === "invalid") {
|
|
909
|
+
const message = read.readError
|
|
910
|
+
? `Cannot save config "${getPaths().configFile}": ${read.message}.`
|
|
911
|
+
: read.message ?? `Config file "${read.path}" contains invalid JSON; no changes were written. Repair or move it and retry.`;
|
|
912
|
+
writeLine(stderr, message);
|
|
913
|
+
return errorResult(read, null, notNeededCatalog(), [], [], read.readError ? 15 : 14, message);
|
|
914
|
+
}
|
|
915
|
+
const current = { ...DEFAULT_CONFIG, ...(read.config ?? {}) };
|
|
916
|
+
const lastByRole = new Map();
|
|
917
|
+
options.binds.forEach((binding, index) => lastByRole.set(binding.role, index));
|
|
918
|
+
const winning = options.binds.filter((binding, index) => lastByRole.get(binding.role) === index);
|
|
919
|
+
const existingAgents = jsonObject(current.agents) ? current.agents : {};
|
|
920
|
+
const proposed = options.binds.length === 0
|
|
921
|
+
? { ...current }
|
|
922
|
+
: {
|
|
923
|
+
...current,
|
|
924
|
+
agents: {
|
|
925
|
+
...existingAgents,
|
|
926
|
+
...Object.fromEntries(winning.map(({ role, binding }) => [role, binding])),
|
|
927
|
+
},
|
|
928
|
+
};
|
|
929
|
+
const changes = diffConfig(current, proposed);
|
|
930
|
+
let catalog;
|
|
931
|
+
let bindingValidations = [];
|
|
932
|
+
let catalogMessage = null;
|
|
933
|
+
if (winning.length > 0 || options.refresh) {
|
|
934
|
+
const agents = winning.length > 0
|
|
935
|
+
? [...new Set(winning.map(({ binding }) => binding.harness))]
|
|
936
|
+
: undefined;
|
|
937
|
+
catalog = await getBatchModels(dependencies.registry ?? getRegistry(), {
|
|
938
|
+
agents,
|
|
939
|
+
refresh: options.refresh,
|
|
940
|
+
allowNetwork: !options.dryRun || options.refresh,
|
|
941
|
+
now: dependencies.now,
|
|
942
|
+
timeoutMs: dependencies.timeoutMs,
|
|
943
|
+
discover: dependencies.discoverModels,
|
|
944
|
+
saveCache: dependencies.saveCache,
|
|
945
|
+
onDiscoveryStart: () => writeLine(stderr, "discovering models..."),
|
|
946
|
+
});
|
|
947
|
+
if (catalog.discoveryError !== undefined) {
|
|
948
|
+
writeLine(stderr, `Warning: ${catalog.discoveryError}.`);
|
|
949
|
+
}
|
|
950
|
+
if (catalog.cacheWriteFailed)
|
|
951
|
+
writeLine(stderr, "Warning: Could not update models cache.");
|
|
952
|
+
if (winning.length > 0) {
|
|
953
|
+
const validation = validateBindings(winning, catalog);
|
|
954
|
+
bindingValidations = validation.entries;
|
|
955
|
+
catalogMessage = validation.message;
|
|
956
|
+
if (validation.code !== undefined) {
|
|
957
|
+
for (const entry of bindingValidations) {
|
|
958
|
+
if (entry.status !== "accepted")
|
|
959
|
+
writeLine(stderr, entry.message);
|
|
960
|
+
}
|
|
961
|
+
return errorResult(read, proposed, catalogValidation(catalog, catalogMessage), bindingValidations, changes, validation.code, bindingValidations.find((entry) => entry.status !== "accepted")?.message ?? "Setup validation failed.");
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
else if (catalog.status === "unavailable") {
|
|
965
|
+
catalogMessage = "Catalog unavailable.";
|
|
966
|
+
const message = "Model catalog unavailable.";
|
|
967
|
+
writeLine(stderr, message);
|
|
968
|
+
return errorResult(read, proposed, catalogValidation(catalog, catalogMessage), [], changes, 13, message);
|
|
969
|
+
}
|
|
970
|
+
else if (catalog.status === "offline") {
|
|
971
|
+
catalogMessage = "Catalog is stale; using offline cache.";
|
|
972
|
+
writeLine(stderr, catalogMessage);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
const validations = {
|
|
976
|
+
config: configValidation(read),
|
|
977
|
+
catalogo: catalogValidation(catalog, catalogMessage),
|
|
978
|
+
bindings: bindingValidations,
|
|
979
|
+
};
|
|
980
|
+
if (winning.length === 0) {
|
|
981
|
+
const message = "Configuration unchanged.";
|
|
982
|
+
if (!options.json)
|
|
983
|
+
writeLine(stderr, message);
|
|
984
|
+
return {
|
|
985
|
+
code: 0,
|
|
986
|
+
envelope: { proposta: proposed, validacoes: validations, mudancas: changes, resultado: { status: "unchanged", code: 0, saved: false, message } },
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
if (options.dryRun) {
|
|
990
|
+
const message = "Dry run; configuration not written.";
|
|
991
|
+
if (!options.json)
|
|
992
|
+
writeLine(stderr, message);
|
|
993
|
+
return {
|
|
994
|
+
code: 0,
|
|
995
|
+
envelope: { proposta: proposed, validacoes: validations, mudancas: changes, resultado: { status: "dry-run", code: 0, saved: false, message } },
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
const serialized = serializeConfig(proposed);
|
|
999
|
+
let saved = false;
|
|
1000
|
+
try {
|
|
1001
|
+
const canonicalRaw = read.source === "canonical" ? read.raw : null;
|
|
1002
|
+
if (canonicalRaw !== null && canonicalRaw === serialized) {
|
|
1003
|
+
saved = false;
|
|
1004
|
+
}
|
|
1005
|
+
else {
|
|
1006
|
+
saved = store.save(proposed) !== false;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
catch (error) {
|
|
1010
|
+
const message = `Cannot save config "${getPaths().configFile}": ${error instanceof Error ? error.message : String(error)}.`;
|
|
1011
|
+
writeLine(stderr, message);
|
|
1012
|
+
return errorResult(read, proposed, catalogValidation(catalog, catalogMessage), bindingValidations, changes, 15, message);
|
|
1013
|
+
}
|
|
1014
|
+
if (!saved) {
|
|
1015
|
+
const message = "Configuration unchanged.";
|
|
1016
|
+
if (!options.json)
|
|
1017
|
+
writeLine(stderr, message);
|
|
1018
|
+
return {
|
|
1019
|
+
code: 0,
|
|
1020
|
+
envelope: { proposta: proposed, validacoes: validations, mudancas: [], resultado: { status: "unchanged", code: 0, saved: false, message } },
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
const message = "Configuration saved.";
|
|
1024
|
+
if (!options.json)
|
|
1025
|
+
writeLine(stderr, `saved: ${configSummary(proposed)}`);
|
|
1026
|
+
return {
|
|
1027
|
+
code: 0,
|
|
1028
|
+
envelope: { proposta: proposed, validacoes: validations, mudancas: changes, resultado: { status: "applied", code: 0, saved: true, message } },
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
function commandTokens(opts, command) {
|
|
1032
|
+
const tokens = [];
|
|
1033
|
+
if (opts.refresh)
|
|
1034
|
+
tokens.push("--refresh");
|
|
1035
|
+
if (opts.nonInteractive)
|
|
1036
|
+
tokens.push("--non-interactive");
|
|
1037
|
+
if (opts.json)
|
|
1038
|
+
tokens.push("--json");
|
|
1039
|
+
if (opts.dryRun)
|
|
1040
|
+
tokens.push("--dry-run");
|
|
1041
|
+
const binds = Array.isArray(opts.bind) ? opts.bind : opts.bind === undefined ? [] : [opts.bind];
|
|
1042
|
+
for (const bind of binds) {
|
|
1043
|
+
tokens.push("--bind");
|
|
1044
|
+
if (bind !== true)
|
|
1045
|
+
tokens.push(String(bind));
|
|
1046
|
+
}
|
|
1047
|
+
return tokens.concat(command.args.map(String));
|
|
1048
|
+
}
|
|
1049
|
+
export async function executeSetupAction(args, dependencies = {}) {
|
|
1050
|
+
const parsed = parseSetupArgs(args);
|
|
1051
|
+
const stdout = dependencies.stdout ?? process.stdout;
|
|
1052
|
+
const input = dependencies.input ?? process.stdin;
|
|
1053
|
+
const writeError = (message) => writeLine(dependencies.stderr, message);
|
|
1054
|
+
if (!parsed.ok) {
|
|
1055
|
+
if (parsed.json)
|
|
1056
|
+
stdout.write(`${JSON.stringify(emptyEnvelope(parsed.message, 2, getPaths().configFile))}\n`);
|
|
1057
|
+
writeError(parsed.message);
|
|
1058
|
+
return { code: 2, envelope: parsed.json ? emptyEnvelope(parsed.message, 2, getPaths().configFile) : undefined };
|
|
1059
|
+
}
|
|
1060
|
+
const tty = dependencies.isTTY ?? Boolean(input.isTTY && stdout.isTTY);
|
|
1061
|
+
if (!parsed.options.batch) {
|
|
1062
|
+
if (!tty) {
|
|
1063
|
+
const message = `${getCliName()} setup needs a terminal on both stdin and stdout.`;
|
|
1064
|
+
writeError(message);
|
|
1065
|
+
return { code: 1 };
|
|
1066
|
+
}
|
|
1067
|
+
await runModelSetupWizard({
|
|
1068
|
+
registry: dependencies.registry,
|
|
1069
|
+
input: dependencies.input,
|
|
1070
|
+
output: dependencies.stdout,
|
|
1071
|
+
refresh: parsed.options.refresh,
|
|
1072
|
+
discoverModels: dependencies.wizardDiscoverModels,
|
|
1073
|
+
save: dependencies.saveConfig,
|
|
1074
|
+
isTTY: tty,
|
|
1075
|
+
});
|
|
1076
|
+
return { code: 0 };
|
|
1077
|
+
}
|
|
1078
|
+
const result = await runSetupBatch(parsed.options, dependencies);
|
|
1079
|
+
if (parsed.options.json)
|
|
1080
|
+
stdout.write(`${JSON.stringify(result.envelope)}\n`);
|
|
1081
|
+
return result;
|
|
1082
|
+
}
|
|
1083
|
+
export function registerSetupCommand(program, dependencies = {}) {
|
|
167
1084
|
program
|
|
168
1085
|
.command("setup")
|
|
169
|
-
.description("Choose the model each
|
|
1086
|
+
.description("Choose the harness and model each agent should run on")
|
|
1087
|
+
.allowUnknownOption(true)
|
|
1088
|
+
.allowExcessArguments(true)
|
|
170
1089
|
.option("--refresh", "ignore the cached catalog and rediscover")
|
|
171
|
-
.
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
await runModelSetupWizard({ refresh: opts.refresh });
|
|
1090
|
+
.option("--non-interactive", "run setup without the picker")
|
|
1091
|
+
.option("--json", "output one machine-readable envelope")
|
|
1092
|
+
.option("--dry-run", "show the proposed config without writing it")
|
|
1093
|
+
.option("--bind [binding]", "bind a role to a harness and model", (value, previous = []) => [...previous, value], [])
|
|
1094
|
+
.action(async (opts, command) => {
|
|
1095
|
+
const result = await executeSetupAction(commandTokens(opts, command), dependencies);
|
|
1096
|
+
process.exitCode = result.code;
|
|
180
1097
|
});
|
|
181
1098
|
}
|
|
182
1099
|
//# sourceMappingURL=setup.js.map
|