@nodeint/migent 0.0.0-stage → 0.1.2
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/LICENSE +21 -0
- package/README.md +206 -2
- package/dist/commands/agent.d.ts +14 -0
- package/dist/commands/agent.js +97 -0
- package/dist/commands/config.d.ts +35 -0
- package/dist/commands/config.js +613 -0
- package/dist/commands/doctor.d.ts +5 -0
- package/dist/commands/doctor.js +26 -0
- package/dist/commands/execute.d.ts +12 -0
- package/dist/commands/execute.js +39 -0
- package/dist/commands/init.d.ts +26 -0
- package/dist/commands/init.js +311 -0
- package/dist/commands/service.d.ts +11 -0
- package/dist/commands/service.js +60 -0
- package/dist/commands/sessions.d.ts +3 -0
- package/dist/commands/sessions.js +20 -0
- package/dist/commands/skill.d.ts +4 -0
- package/dist/commands/skill.js +98 -0
- package/dist/commands/watch.d.ts +7 -0
- package/dist/commands/watch.js +169 -0
- package/dist/commands/workspace-registry.d.ts +8 -0
- package/dist/commands/workspace-registry.js +106 -0
- package/dist/display.d.ts +12 -0
- package/dist/display.js +123 -0
- package/dist/hooks.d.ts +6 -0
- package/dist/hooks.js +5 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +57 -0
- package/dist/migrate.d.ts +8 -0
- package/dist/migrate.js +191 -0
- package/dist/program.d.ts +6 -0
- package/dist/program.js +414 -0
- package/dist/provider.d.ts +23 -0
- package/dist/provider.js +136 -0
- package/dist/templates/migent/SKILL.md +62 -0
- package/dist/workspace.d.ts +8 -0
- package/dist/workspace.js +50 -0
- package/package.json +59 -3
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { confirm as clackConfirm, isCancel, select as clackSelect, text as clackText } from "@clack/prompts";
|
|
4
|
+
import { ConfigEditError, ConfigurationError, assignRoles, describeChange, diagnose, inspectConfig, loadModelTarget, loadWorkflowCatalog, loadYamlMapping, matchingModelName, patchConfigText, placeRoleModel, resolveStageSpec, setRoleOptions, upsertModel, whichSync, } from "@nodeint/migent-kernel";
|
|
5
|
+
import { printError } from "../display.js";
|
|
6
|
+
import { cliHooks } from "../hooks.js";
|
|
7
|
+
import { requireCurrentSchema } from "../migrate.js";
|
|
8
|
+
import { acceptedOptions, adapterMap, asCatalog, detected, knownModel, loadCatalog, loadOptionValues, parseOptions, requireAdapter, runProviderCommand, } from "../provider.js";
|
|
9
|
+
import { runDoctor } from "./doctor.js";
|
|
10
|
+
const NEW = "__new__";
|
|
11
|
+
const UNSET = Symbol("unset");
|
|
12
|
+
export function runConfigShow(workspace) {
|
|
13
|
+
const text = readFileSync(path.join(workspace, ".migent", "config.yaml"), "utf8");
|
|
14
|
+
process.stdout.write(text);
|
|
15
|
+
return 0;
|
|
16
|
+
}
|
|
17
|
+
export async function runConfigModel(workspace, options = {}) {
|
|
18
|
+
try {
|
|
19
|
+
return await runModel(workspace, {
|
|
20
|
+
...options,
|
|
21
|
+
adapters: options.adapters ?? adapterMap(),
|
|
22
|
+
which: options.which ?? cliHooks.which ?? whichSync,
|
|
23
|
+
prompter: options.prompter ?? new ClackConfigPrompter(),
|
|
24
|
+
runCommand: options.runCommand ?? runProviderCommand,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (error instanceof ConfigurationError || error instanceof ConfigEditError || error instanceof Error) {
|
|
29
|
+
printError(error.message);
|
|
30
|
+
return 2;
|
|
31
|
+
}
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export async function runConfigRole(workspace, options = {}) {
|
|
36
|
+
try {
|
|
37
|
+
return await runRole(workspace, {
|
|
38
|
+
...options,
|
|
39
|
+
adapters: options.adapters ?? adapterMap(),
|
|
40
|
+
which: options.which ?? cliHooks.which ?? whichSync,
|
|
41
|
+
prompter: options.prompter ?? new ClackConfigPrompter(),
|
|
42
|
+
runCommand: options.runCommand ?? runProviderCommand,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (error instanceof ConfigurationError || error instanceof ConfigEditError || error instanceof Error) {
|
|
47
|
+
printError(error.message);
|
|
48
|
+
return 2;
|
|
49
|
+
}
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function runModel(workspace, args) {
|
|
54
|
+
const key = args.key ?? null;
|
|
55
|
+
let provider = args.provider ?? null;
|
|
56
|
+
let model = args.model ?? null;
|
|
57
|
+
const asJson = args.asJson ?? false;
|
|
58
|
+
const interactive = args.interactive ?? false;
|
|
59
|
+
let optionUpdates = optionUpdatesOf(args.options ?? [], args.clearOptions ?? []);
|
|
60
|
+
if ((provider === null) !== (model === null)) {
|
|
61
|
+
throw new ConfigEditError("Pass both --provider and --model, or omit both.");
|
|
62
|
+
}
|
|
63
|
+
const [config, workflows, view] = loaded(workspace);
|
|
64
|
+
const mutating = provider !== null || optionUpdates !== UNSET;
|
|
65
|
+
if (key === null && !mutating) {
|
|
66
|
+
if (interactive) {
|
|
67
|
+
return interactiveModels(workspace, config, workflows, view, args.adapters, args.which, args.prompter, args.runCommand, asJson);
|
|
68
|
+
}
|
|
69
|
+
emitView(view, asJson, true, false);
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
if (key === null)
|
|
73
|
+
throw new ConfigEditError("Pass a name.");
|
|
74
|
+
if (!mutating) {
|
|
75
|
+
if (interactive) {
|
|
76
|
+
return editModel(workspace, config, workflows, view, key, args.adapters, args.which, args.prompter, args.runCommand, asJson);
|
|
77
|
+
}
|
|
78
|
+
emitModels(selectedModels(view, [key]), asJson);
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
let catalog = null;
|
|
82
|
+
if (provider !== null && model !== null) {
|
|
83
|
+
[provider, model, catalog] = await catalogModel(args.adapters, args.which, args.runCommand, provider, model);
|
|
84
|
+
}
|
|
85
|
+
if (optionUpdates !== UNSET) {
|
|
86
|
+
const currentProvider = provider ?? modelProvider(config, key);
|
|
87
|
+
const currentModel = model ?? loadModelTarget(config, key).model;
|
|
88
|
+
optionUpdates = await checkedOptions(args.adapters, args.which, args.runCommand, currentProvider, currentModel, optionUpdates, provider !== null ? catalog : null);
|
|
89
|
+
}
|
|
90
|
+
const change = upsertModel(config, key, {
|
|
91
|
+
provider,
|
|
92
|
+
model,
|
|
93
|
+
...(optionUpdates === UNSET ? {} : { options: optionUpdates }),
|
|
94
|
+
workflows,
|
|
95
|
+
});
|
|
96
|
+
validateChange(config, change.config, workflows, args.adapters);
|
|
97
|
+
return await commit(workspace, change, { asJson, which: args.which });
|
|
98
|
+
}
|
|
99
|
+
async function runRole(workspace, args) {
|
|
100
|
+
const roles = args.roles ?? [];
|
|
101
|
+
const modelName = args.modelName ?? null;
|
|
102
|
+
let provider = args.provider ?? null;
|
|
103
|
+
let model = args.model ?? null;
|
|
104
|
+
const asJson = args.asJson ?? false;
|
|
105
|
+
const interactive = args.interactive ?? false;
|
|
106
|
+
let optionUpdates = optionUpdatesOf(args.options ?? [], args.clearOptions ?? []);
|
|
107
|
+
if (modelName !== null && (provider !== null || model !== null)) {
|
|
108
|
+
throw new ConfigEditError("Pass --model-name or --provider and --model.");
|
|
109
|
+
}
|
|
110
|
+
if ((provider === null) !== (model === null)) {
|
|
111
|
+
throw new ConfigEditError("Pass both --provider and --model, or omit both.");
|
|
112
|
+
}
|
|
113
|
+
const [config, workflows, view] = loaded(workspace);
|
|
114
|
+
const mutating = modelName !== null || provider !== null || optionUpdates !== UNSET;
|
|
115
|
+
if (roles.length === 0 && !mutating) {
|
|
116
|
+
if (interactive) {
|
|
117
|
+
return interactiveRoles(workspace, config, workflows, view, args.adapters, args.which, args.prompter, args.runCommand, asJson);
|
|
118
|
+
}
|
|
119
|
+
emitView(view, asJson, false, true);
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
if (roles.length === 0)
|
|
123
|
+
throw new ConfigEditError("Pass a role.");
|
|
124
|
+
if (roles.length > 1 && (provider !== null || optionUpdates !== UNSET)) {
|
|
125
|
+
throw new ConfigEditError("Pass one role to set options or a provider model.");
|
|
126
|
+
}
|
|
127
|
+
if (!mutating) {
|
|
128
|
+
if (interactive && roles.length === 1) {
|
|
129
|
+
knownRole(view, roles[0] ?? "");
|
|
130
|
+
return pointRole(workspace, config, workflows, view, roles[0] ?? "", args.adapters, args.which, args.prompter, args.runCommand, asJson);
|
|
131
|
+
}
|
|
132
|
+
emitRoles(selectedRoles(view, roles), asJson);
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
let change;
|
|
136
|
+
if (provider !== null && model !== null) {
|
|
137
|
+
let catalog;
|
|
138
|
+
[provider, model, catalog] = await catalogModel(args.adapters, args.which, args.runCommand, provider, model);
|
|
139
|
+
if (optionUpdates !== UNSET) {
|
|
140
|
+
optionUpdates = await checkedOptions(args.adapters, args.which, args.runCommand, provider, model, optionUpdates, catalog);
|
|
141
|
+
}
|
|
142
|
+
change = placeRoleModel(config, roles[0] ?? "", {
|
|
143
|
+
provider,
|
|
144
|
+
model,
|
|
145
|
+
...(optionUpdates === UNSET ? {} : { options: optionUpdates }),
|
|
146
|
+
workflows,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
else if (modelName !== null) {
|
|
150
|
+
change = assignRoles(config, roles, modelName, { workflows });
|
|
151
|
+
if (optionUpdates !== UNSET) {
|
|
152
|
+
const target = loadModelTarget(change.config, modelName);
|
|
153
|
+
optionUpdates = await checkedOptions(args.adapters, args.which, args.runCommand, target.provider, target.model, optionUpdates);
|
|
154
|
+
const follow = setRoleOptions(change.config, roles[0] ?? "", optionUpdates, { workflows });
|
|
155
|
+
change = describeChange(config, follow.config, workflows);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
const target = roleModel(config, roles[0] ?? "");
|
|
160
|
+
if (optionUpdates === UNSET)
|
|
161
|
+
throw new ConfigEditError("Pass a role.");
|
|
162
|
+
optionUpdates = await checkedOptions(args.adapters, args.which, args.runCommand, target.provider, target.model, optionUpdates);
|
|
163
|
+
change = setRoleOptions(config, roles[0] ?? "", optionUpdates, { workflows });
|
|
164
|
+
}
|
|
165
|
+
validateChange(config, change.config, workflows, args.adapters);
|
|
166
|
+
return await commit(workspace, change, { asJson, which: args.which });
|
|
167
|
+
}
|
|
168
|
+
async function interactiveModels(workspace, config, workflows, view, adapters, which, prompter, runCommand, asJson) {
|
|
169
|
+
const choices = view.models.map((model) => [modelLabel(model), model.key]);
|
|
170
|
+
choices.push(["New model", NEW]);
|
|
171
|
+
const selected = await prompter.select("Model", choices, view.models[0]?.key ?? NEW);
|
|
172
|
+
if (selected === NEW)
|
|
173
|
+
return addModel(workspace, config, workflows, null, adapters, which, prompter, runCommand, asJson);
|
|
174
|
+
return editModel(workspace, config, workflows, view, selected, adapters, which, prompter, runCommand, asJson);
|
|
175
|
+
}
|
|
176
|
+
async function editModel(workspace, config, workflows, view, key, adapters, which, prompter, runCommand, asJson) {
|
|
177
|
+
const current = selectedModels(view, [key])[0];
|
|
178
|
+
if (current === undefined)
|
|
179
|
+
throw new ConfigEditError(`Unknown name: ${key}.`);
|
|
180
|
+
const provider = await selectProvider(prompter, adapters, which, "Provider", current.provider);
|
|
181
|
+
const adapter = requireAdapter(adapters, provider);
|
|
182
|
+
const catalog = await loadCatalog(adapter, which, runCommand);
|
|
183
|
+
const initial = catalog.ids.includes(current.model) ? current.model : (catalog.defaultId ?? undefined);
|
|
184
|
+
let modelId = knownModel(await prompter.select("Model", catalog.ids.map((item) => [item, item]), initial), catalog);
|
|
185
|
+
const stored = await promptedOptions(prompter, adapter, modelId, catalog, which, runCommand, current.options);
|
|
186
|
+
const change = upsertModel(config, key, { provider, model: modelId, options: stored, workflows });
|
|
187
|
+
validateChange(config, change.config, workflows, adapters);
|
|
188
|
+
return confirmAndCommit(workspace, change, prompter, asJson, which);
|
|
189
|
+
}
|
|
190
|
+
async function addModel(workspace, config, workflows, role, adapters, which, prompter, runCommand, asJson) {
|
|
191
|
+
const provider = await selectProvider(prompter, adapters, which, "Provider", null);
|
|
192
|
+
const adapter = requireAdapter(adapters, provider);
|
|
193
|
+
const catalog = await loadCatalog(adapter, which, runCommand);
|
|
194
|
+
const modelId = knownModel(await prompter.select("Model", catalog.ids.map((item) => [item, item]), catalog.defaultId), catalog);
|
|
195
|
+
const options = await promptedOptions(prompter, adapter, modelId, catalog, which, runCommand, {});
|
|
196
|
+
const present = presentOptions(options);
|
|
197
|
+
const existing = matchingModelName(config, { provider, model: modelId, options: present });
|
|
198
|
+
let change;
|
|
199
|
+
if (existing !== null) {
|
|
200
|
+
if (role === null) {
|
|
201
|
+
process.stdout.write(`Model ${existing} already has this provider and model.\n`);
|
|
202
|
+
return 0;
|
|
203
|
+
}
|
|
204
|
+
const assigned = assignRoles(config, [role], existing, { workflows });
|
|
205
|
+
change = describeChange(config, assigned.config, workflows, { reusedModel: existing });
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
const key = (await prompter.text("Name")).trim();
|
|
209
|
+
if (key === "")
|
|
210
|
+
throw new ConfigEditError("Name is required.");
|
|
211
|
+
const models = config.models;
|
|
212
|
+
if (isRecord(models) && Object.hasOwn(models, key))
|
|
213
|
+
throw new ConfigEditError(`Name ${key} already exists.`);
|
|
214
|
+
const created = upsertModel(config, key, { provider, model: modelId, options, workflows });
|
|
215
|
+
if (role === null)
|
|
216
|
+
change = created;
|
|
217
|
+
else {
|
|
218
|
+
const assigned = assignRoles(created.config, [role], key, { workflows });
|
|
219
|
+
change = describeChange(config, assigned.config, workflows);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
validateChange(config, change.config, workflows, adapters);
|
|
223
|
+
return confirmAndCommit(workspace, change, prompter, asJson, which);
|
|
224
|
+
}
|
|
225
|
+
async function interactiveRoles(workspace, config, workflows, view, adapters, which, prompter, runCommand, asJson) {
|
|
226
|
+
const choices = view.roles.map((role) => [roleLabel(role), role.role]);
|
|
227
|
+
choices.push(["New role", NEW]);
|
|
228
|
+
const selected = await prompter.select("Role", choices, view.roles[0]?.role ?? NEW);
|
|
229
|
+
if (selected === NEW) {
|
|
230
|
+
const name = (await prompter.text("Role name")).trim();
|
|
231
|
+
if (name === "")
|
|
232
|
+
throw new ConfigEditError("Role name is required.");
|
|
233
|
+
if (view.roles.some((role) => role.role === name))
|
|
234
|
+
throw new ConfigEditError(`Role ${name} already exists.`);
|
|
235
|
+
return pointRole(workspace, config, workflows, view, name, adapters, which, prompter, runCommand, asJson);
|
|
236
|
+
}
|
|
237
|
+
return pointRole(workspace, config, workflows, view, selected, adapters, which, prompter, runCommand, asJson);
|
|
238
|
+
}
|
|
239
|
+
async function pointRole(workspace, config, workflows, view, role, adapters, which, prompter, runCommand, asJson) {
|
|
240
|
+
const current = view.roles.find((item) => item.role === role) ?? null;
|
|
241
|
+
const choices = view.models.map((model) => [modelLabel(model), model.key]);
|
|
242
|
+
choices.push(["New model", NEW]);
|
|
243
|
+
const selected = await prompter.select("Model", choices, current?.modelName ?? NEW);
|
|
244
|
+
if (selected === NEW) {
|
|
245
|
+
return addModel(workspace, config, workflows, role, adapters, which, prompter, runCommand, asJson);
|
|
246
|
+
}
|
|
247
|
+
const change = assignRoles(config, [role], selected, { workflows });
|
|
248
|
+
validateChange(config, change.config, workflows, adapters);
|
|
249
|
+
return confirmAndCommit(workspace, change, prompter, asJson, which);
|
|
250
|
+
}
|
|
251
|
+
async function confirmAndCommit(workspace, change, prompter, asJson, which) {
|
|
252
|
+
printChange(change);
|
|
253
|
+
if (!(await prompter.confirm("Save this change?"))) {
|
|
254
|
+
process.stdout.write("Left unchanged.\n");
|
|
255
|
+
return 0;
|
|
256
|
+
}
|
|
257
|
+
return commit(workspace, change, { asJson, which, echo: false });
|
|
258
|
+
}
|
|
259
|
+
async function commit(workspace, change, args) {
|
|
260
|
+
const filePath = path.join(workspace, ".migent", "config.yaml");
|
|
261
|
+
const original = readFileSync(filePath, "utf8");
|
|
262
|
+
const updated = patchConfigText(original, change.config);
|
|
263
|
+
if (updated === original) {
|
|
264
|
+
if (args.asJson)
|
|
265
|
+
process.stdout.write(`${JSON.stringify(changePayload(change, false, null))}\n`);
|
|
266
|
+
else
|
|
267
|
+
process.stdout.write("Left unchanged.\n");
|
|
268
|
+
return 0;
|
|
269
|
+
}
|
|
270
|
+
writeFileSync(filePath, updated, "utf8");
|
|
271
|
+
if (args.asJson) {
|
|
272
|
+
const checks = diagnose(workspace, { adapters: adapterMap(), which: args.which });
|
|
273
|
+
const doctor = doctorPayload(checks);
|
|
274
|
+
process.stdout.write(`${JSON.stringify(changePayload(change, true, doctor))}\n`);
|
|
275
|
+
return doctor.ok ? 0 : 1;
|
|
276
|
+
}
|
|
277
|
+
if (args.echo !== false)
|
|
278
|
+
printChange(change);
|
|
279
|
+
return runDoctor(workspace, { which: args.which });
|
|
280
|
+
}
|
|
281
|
+
function validateChange(before, after, workflows, adapters) {
|
|
282
|
+
const beforeModels = isRecord(before.models) ? before.models : {};
|
|
283
|
+
const afterModels = isRecord(after.models) ? after.models : {};
|
|
284
|
+
const changed = new Set();
|
|
285
|
+
for (const key of Object.keys(afterModels)) {
|
|
286
|
+
if (!Object.hasOwn(beforeModels, key) || !sameTarget(safeTarget(before, key), safeTarget(after, key))) {
|
|
287
|
+
changed.add(key);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
for (const key of changed) {
|
|
291
|
+
const target = loadModelTarget(after, key);
|
|
292
|
+
requireAdapter(adapters, target.provider).validateOptions(target.options);
|
|
293
|
+
}
|
|
294
|
+
const beforeRoles = isRecord(before.roles) ? before.roles : {};
|
|
295
|
+
const afterRoles = isRecord(after.roles) ? after.roles : {};
|
|
296
|
+
for (const [name, body] of Object.entries(afterRoles)) {
|
|
297
|
+
const usesChanged = isRecord(body) && typeof body.default_model === "string" && changed.has(body.default_model);
|
|
298
|
+
if (deepEqual(beforeRoles[name], body) && !usesChanged)
|
|
299
|
+
continue;
|
|
300
|
+
if (!isRecord(body) || typeof body.default_model !== "string")
|
|
301
|
+
continue;
|
|
302
|
+
const target = loadModelTarget(after, body.default_model);
|
|
303
|
+
const options = { ...target.options };
|
|
304
|
+
if (isRecord(body.options)) {
|
|
305
|
+
for (const [optionName, value] of Object.entries(body.options)) {
|
|
306
|
+
if (typeof value === "string" && value.trim() !== "")
|
|
307
|
+
options[optionName] = value.trim();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
requireAdapter(adapters, target.provider).validateOptions(options);
|
|
311
|
+
}
|
|
312
|
+
for (const document of workflows) {
|
|
313
|
+
for (const stage of Object.values(document.stages)) {
|
|
314
|
+
let beforeTarget;
|
|
315
|
+
let afterTarget;
|
|
316
|
+
try {
|
|
317
|
+
beforeTarget = resolveStageSpec(before, stage);
|
|
318
|
+
afterTarget = resolveStageSpec(after, stage);
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
if (error instanceof ConfigurationError)
|
|
322
|
+
continue;
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
if (sameTarget(beforeTarget, afterTarget))
|
|
326
|
+
continue;
|
|
327
|
+
requireAdapter(adapters, afterTarget.provider).validateOptions(afterTarget.options);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
async function catalogModel(adapters, which, runCommand, provider, model) {
|
|
332
|
+
const adapter = requireAdapter(adapters, provider);
|
|
333
|
+
const catalog = await loadCatalog(adapter, which, runCommand);
|
|
334
|
+
return [provider, knownModel(model, catalog), catalog];
|
|
335
|
+
}
|
|
336
|
+
async function checkedOptions(adapters, which, runCommand, provider, model, updates, catalog = null) {
|
|
337
|
+
const adapter = requireAdapter(adapters, provider);
|
|
338
|
+
const resolved = catalog ?? (await loadCatalog(adapter, which, runCommand));
|
|
339
|
+
const present = {};
|
|
340
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
341
|
+
if (value !== null)
|
|
342
|
+
present[key] = value;
|
|
343
|
+
}
|
|
344
|
+
if (Object.keys(present).length > 0) {
|
|
345
|
+
await acceptedOptions(adapter, model, resolved, which, runCommand, present);
|
|
346
|
+
}
|
|
347
|
+
return { ...updates };
|
|
348
|
+
}
|
|
349
|
+
async function promptedOptions(prompter, adapter, model, catalog, which, runCommand, current) {
|
|
350
|
+
const stored = {};
|
|
351
|
+
const catalogAdapter = asCatalog(adapter);
|
|
352
|
+
for (const spec of catalogAdapter.providerOptions()) {
|
|
353
|
+
if (!spec.prompt)
|
|
354
|
+
continue;
|
|
355
|
+
const values = await loadOptionValues(adapter, spec.name, model, catalog, which, runCommand);
|
|
356
|
+
if (values === null || values.length === 0)
|
|
357
|
+
continue;
|
|
358
|
+
const choices = values.map((value) => [value, value]);
|
|
359
|
+
if (spec.allowDefault)
|
|
360
|
+
choices.unshift(["provider default", ""]);
|
|
361
|
+
const currentValue = current[spec.name] ?? "";
|
|
362
|
+
const picked = await prompter.select(spec.name, choices, values.includes(currentValue) ? currentValue : "");
|
|
363
|
+
stored[spec.name] = picked || null;
|
|
364
|
+
}
|
|
365
|
+
return stored;
|
|
366
|
+
}
|
|
367
|
+
function optionUpdatesOf(options, clearOptions) {
|
|
368
|
+
if (options.length === 0 && clearOptions.length === 0)
|
|
369
|
+
return UNSET;
|
|
370
|
+
const parsed = parseOptions(options);
|
|
371
|
+
const overlap = Object.keys(parsed).filter((name) => clearOptions.includes(name)).sort();
|
|
372
|
+
if (overlap.length > 0)
|
|
373
|
+
throw new ConfigEditError(`Pass either --option or --clear-option for ${overlap.join(", ")}.`);
|
|
374
|
+
const updates = { ...parsed };
|
|
375
|
+
for (const name of clearOptions) {
|
|
376
|
+
if (name.trim() === "")
|
|
377
|
+
throw new ConfigEditError("Option name is required.");
|
|
378
|
+
updates[name.trim()] = null;
|
|
379
|
+
}
|
|
380
|
+
return updates;
|
|
381
|
+
}
|
|
382
|
+
async function selectProvider(prompter, adapters, which, message, defaultValue) {
|
|
383
|
+
const rows = detected(adapters, which);
|
|
384
|
+
if (rows.length === 0)
|
|
385
|
+
throw new Error("No providers are available.");
|
|
386
|
+
const names = new Set(rows.map(([name]) => name));
|
|
387
|
+
return prompter.select(message, rows.map(([name, found]) => [`${name} ${found ?? "not found"}`, name]), defaultValue !== null && names.has(defaultValue) ? defaultValue : rows[0]?.[0]);
|
|
388
|
+
}
|
|
389
|
+
function loaded(workspace) {
|
|
390
|
+
requireCurrentSchema(workspace);
|
|
391
|
+
const config = loadYamlMapping(path.join(workspace, ".migent", "config.yaml"));
|
|
392
|
+
const workflows = Object.values(loadWorkflowCatalog(workspace).documents);
|
|
393
|
+
return [config, workflows, inspectConfig(config, workflows)];
|
|
394
|
+
}
|
|
395
|
+
function modelProvider(config, key) {
|
|
396
|
+
return loadModelTarget(config, key).provider;
|
|
397
|
+
}
|
|
398
|
+
function roleModel(config, role) {
|
|
399
|
+
const roles = config.roles;
|
|
400
|
+
if (!isRecord(roles) || !isRecord(roles[role]))
|
|
401
|
+
throw new ConfigEditError(`Unknown role: ${role}.`);
|
|
402
|
+
const modelName = roles[role].default_model;
|
|
403
|
+
if (typeof modelName !== "string")
|
|
404
|
+
throw new ConfigurationError(`Missing or invalid roles.${role}.default_model.`);
|
|
405
|
+
return loadModelTarget(config, modelName);
|
|
406
|
+
}
|
|
407
|
+
function selectedModels(view, keys) {
|
|
408
|
+
const byKey = new Map(view.models.map((model) => [model.key, model]));
|
|
409
|
+
return keys.map((key) => {
|
|
410
|
+
const found = byKey.get(key);
|
|
411
|
+
if (found === undefined)
|
|
412
|
+
throw new ConfigEditError(`Unknown name: ${key}.`);
|
|
413
|
+
return found;
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
function selectedRoles(view, names) {
|
|
417
|
+
return names.map((name) => knownRole(view, name));
|
|
418
|
+
}
|
|
419
|
+
function knownRole(view, name) {
|
|
420
|
+
const found = view.roles.find((role) => role.role === name);
|
|
421
|
+
if (found === undefined)
|
|
422
|
+
throw new ConfigEditError(`Unknown role: ${name}.`);
|
|
423
|
+
return found;
|
|
424
|
+
}
|
|
425
|
+
function emitView(view, asJson, models, roles) {
|
|
426
|
+
if (asJson) {
|
|
427
|
+
const payload = {};
|
|
428
|
+
if (models)
|
|
429
|
+
payload.models = view.models.map(modelJson);
|
|
430
|
+
if (roles)
|
|
431
|
+
payload.roles = view.roles.map(roleJson);
|
|
432
|
+
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (models)
|
|
436
|
+
emitModels(view.models, false);
|
|
437
|
+
if (roles)
|
|
438
|
+
emitRoles(view.roles, false);
|
|
439
|
+
}
|
|
440
|
+
function emitModels(models, asJson) {
|
|
441
|
+
if (asJson) {
|
|
442
|
+
process.stdout.write(`${JSON.stringify({ models: models.map(modelJson) })}\n`);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
process.stdout.write("Models\n");
|
|
446
|
+
if (models.length === 0) {
|
|
447
|
+
process.stdout.write(" none\n");
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
for (const model of models)
|
|
451
|
+
process.stdout.write(` ${modelLabel(model)}\n`);
|
|
452
|
+
}
|
|
453
|
+
function emitRoles(roles, asJson) {
|
|
454
|
+
if (asJson) {
|
|
455
|
+
process.stdout.write(`${JSON.stringify({ roles: roles.map(roleJson) })}\n`);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
process.stdout.write("Roles\n");
|
|
459
|
+
if (roles.length === 0) {
|
|
460
|
+
process.stdout.write(" none\n");
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
for (const role of roles)
|
|
464
|
+
process.stdout.write(` ${roleLabel(role)}\n`);
|
|
465
|
+
}
|
|
466
|
+
function modelLabel(model) {
|
|
467
|
+
return `${model.key} - ${model.model}`;
|
|
468
|
+
}
|
|
469
|
+
function roleLabel(role) {
|
|
470
|
+
return `${role.role} - ${role.model}`;
|
|
471
|
+
}
|
|
472
|
+
function modelJson(model) {
|
|
473
|
+
return {
|
|
474
|
+
key: model.key,
|
|
475
|
+
provider: model.provider,
|
|
476
|
+
model: model.model,
|
|
477
|
+
options: { ...model.options },
|
|
478
|
+
roles: [...model.roles],
|
|
479
|
+
stages: model.stages.map((stage) => ({ workflow: stage.workflow, stage: stage.stage })),
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
function roleJson(role) {
|
|
483
|
+
return {
|
|
484
|
+
role: role.role,
|
|
485
|
+
model_name: role.modelName,
|
|
486
|
+
provider: role.provider,
|
|
487
|
+
model: role.model,
|
|
488
|
+
options: { ...role.options },
|
|
489
|
+
shared_with: [...role.sharedWith],
|
|
490
|
+
stages: role.stages.map((stage) => ({
|
|
491
|
+
workflow: stage.workflow,
|
|
492
|
+
stage: stage.stage,
|
|
493
|
+
model_name: stage.modelName,
|
|
494
|
+
options: { ...stage.options },
|
|
495
|
+
})),
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
function printChange(change) {
|
|
499
|
+
if (change.createdModel)
|
|
500
|
+
process.stdout.write(`Created model ${change.createdModel}.\n`);
|
|
501
|
+
if (change.reusedModel)
|
|
502
|
+
process.stdout.write(`Reused model ${change.reusedModel}.\n`);
|
|
503
|
+
if (change.updatedModel)
|
|
504
|
+
process.stdout.write(`Updated model ${change.updatedModel}.\n`);
|
|
505
|
+
if (change.roles.length > 0)
|
|
506
|
+
process.stdout.write(`Roles: ${change.roles.join(", ")}\n`);
|
|
507
|
+
if (change.stagesUpdated.length > 0) {
|
|
508
|
+
process.stdout.write(`Stages: ${change.stagesUpdated.map(([workflow, stage]) => `${workflow}.${stage}`).join(", ")}\n`);
|
|
509
|
+
}
|
|
510
|
+
for (const stage of change.stagesUnchanged) {
|
|
511
|
+
process.stdout.write(`Unchanged: ${stage.workflow}.${stage.stage} (${stage.ignores.join(", ")})\n`);
|
|
512
|
+
}
|
|
513
|
+
if (change.orphanedModels.length > 0)
|
|
514
|
+
process.stdout.write(`Orphaned models: ${change.orphanedModels.join(", ")}\n`);
|
|
515
|
+
}
|
|
516
|
+
function changePayload(change, wrote, doctor) {
|
|
517
|
+
return {
|
|
518
|
+
wrote,
|
|
519
|
+
created_model: change.createdModel,
|
|
520
|
+
reused_model: change.reusedModel,
|
|
521
|
+
updated_model: change.updatedModel,
|
|
522
|
+
roles: [...change.roles],
|
|
523
|
+
stages_updated: change.stagesUpdated.map(([workflow, stage]) => ({ workflow, stage })),
|
|
524
|
+
stages_unchanged: change.stagesUnchanged.map((stage) => ({
|
|
525
|
+
workflow: stage.workflow,
|
|
526
|
+
stage: stage.stage,
|
|
527
|
+
ignores: [...stage.ignores],
|
|
528
|
+
})),
|
|
529
|
+
orphaned_models: [...change.orphanedModels],
|
|
530
|
+
doctor,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
function doctorPayload(checks) {
|
|
534
|
+
return {
|
|
535
|
+
ok: checks.every((check) => check.ok),
|
|
536
|
+
checks: checks.map((check) => ({
|
|
537
|
+
name: check.name,
|
|
538
|
+
ok: check.ok,
|
|
539
|
+
items: [...check.items],
|
|
540
|
+
problems: [...check.problems],
|
|
541
|
+
})),
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
function presentOptions(options) {
|
|
545
|
+
const present = {};
|
|
546
|
+
for (const [key, value] of Object.entries(options)) {
|
|
547
|
+
if (value !== null)
|
|
548
|
+
present[key] = value;
|
|
549
|
+
}
|
|
550
|
+
return present;
|
|
551
|
+
}
|
|
552
|
+
function safeTarget(config, key) {
|
|
553
|
+
try {
|
|
554
|
+
return loadModelTarget(config, key);
|
|
555
|
+
}
|
|
556
|
+
catch (error) {
|
|
557
|
+
if (error instanceof ConfigurationError)
|
|
558
|
+
return null;
|
|
559
|
+
throw error;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
function sameTarget(left, right) {
|
|
563
|
+
if (left === null || right === null)
|
|
564
|
+
return left === right;
|
|
565
|
+
return left.provider === right.provider && left.model === right.model && deepEqual(left.options, right.options);
|
|
566
|
+
}
|
|
567
|
+
function deepEqual(left, right) {
|
|
568
|
+
if (left === right)
|
|
569
|
+
return true;
|
|
570
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
571
|
+
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length)
|
|
572
|
+
return false;
|
|
573
|
+
return left.every((item, index) => deepEqual(item, right[index]));
|
|
574
|
+
}
|
|
575
|
+
if (!isRecord(left) || !isRecord(right))
|
|
576
|
+
return false;
|
|
577
|
+
const leftKeys = Object.keys(left);
|
|
578
|
+
const rightKeys = Object.keys(right);
|
|
579
|
+
if (leftKeys.length !== rightKeys.length)
|
|
580
|
+
return false;
|
|
581
|
+
return leftKeys.every((key) => deepEqual(left[key], right[key]));
|
|
582
|
+
}
|
|
583
|
+
function isRecord(value) {
|
|
584
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
585
|
+
}
|
|
586
|
+
class ClackConfigPrompter {
|
|
587
|
+
async select(message, choices, defaultValue) {
|
|
588
|
+
if (choices.length === 0)
|
|
589
|
+
throw new Error("Choose a value.");
|
|
590
|
+
const values = new Set(choices.map(([, value]) => value));
|
|
591
|
+
const initial = defaultValue != null && values.has(defaultValue) ? defaultValue : choices[0]?.[1];
|
|
592
|
+
const result = await clackSelect({
|
|
593
|
+
message,
|
|
594
|
+
options: choices.map(([label, value]) => ({ label, value })),
|
|
595
|
+
initialValue: initial,
|
|
596
|
+
});
|
|
597
|
+
if (isCancel(result) || typeof result !== "string" || !values.has(result))
|
|
598
|
+
throw new Error("Choose a value.");
|
|
599
|
+
return result;
|
|
600
|
+
}
|
|
601
|
+
async confirm(message) {
|
|
602
|
+
const result = await clackConfirm({ message, initialValue: true });
|
|
603
|
+
if (isCancel(result) || typeof result !== "boolean")
|
|
604
|
+
throw new Error("Answer yes or no.");
|
|
605
|
+
return result;
|
|
606
|
+
}
|
|
607
|
+
async text(message, defaultValue = "") {
|
|
608
|
+
const result = await clackText({ message, initialValue: defaultValue });
|
|
609
|
+
if (isCancel(result) || typeof result !== "string")
|
|
610
|
+
throw new Error("Enter a value.");
|
|
611
|
+
return result;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { diagnose, whichSync } from "@nodeint/migent-kernel";
|
|
2
|
+
import { printDoctor } from "../display.js";
|
|
3
|
+
import { cliHooks } from "../hooks.js";
|
|
4
|
+
import { requireCurrentSchema } from "../migrate.js";
|
|
5
|
+
import { adapterMap } from "../provider.js";
|
|
6
|
+
export async function runDoctor(workspace, options = {}) {
|
|
7
|
+
requireCurrentSchema(workspace);
|
|
8
|
+
const which = options.which ?? cliHooks.which ?? whichSync;
|
|
9
|
+
const checks = diagnose(workspace, { adapters: adapterMap(), which });
|
|
10
|
+
if (options.asJson)
|
|
11
|
+
process.stdout.write(`${JSON.stringify(payload(checks))}\n`);
|
|
12
|
+
else
|
|
13
|
+
printDoctor(checks);
|
|
14
|
+
return checks.every((check) => check.ok) ? 0 : 1;
|
|
15
|
+
}
|
|
16
|
+
function payload(checks) {
|
|
17
|
+
return {
|
|
18
|
+
ok: checks.every((check) => check.ok),
|
|
19
|
+
checks: checks.map((check) => ({
|
|
20
|
+
name: check.name,
|
|
21
|
+
ok: check.ok,
|
|
22
|
+
items: [...check.items],
|
|
23
|
+
problems: [...check.problems],
|
|
24
|
+
})),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { AgentToolRunner } from "@nodeint/migent-kernel";
|
|
2
|
+
export interface ExecuteWorkflowOptions {
|
|
3
|
+
action: string;
|
|
4
|
+
workflowId?: string | null;
|
|
5
|
+
task?: string | null;
|
|
6
|
+
sessionId?: string | null;
|
|
7
|
+
priorSessionId?: string | null;
|
|
8
|
+
maxAttempts?: number | null;
|
|
9
|
+
maxDispatches?: number | null;
|
|
10
|
+
runner?: AgentToolRunner | null;
|
|
11
|
+
}
|
|
12
|
+
export declare function executeWorkflow(workspace: string, options: ExecuteWorkflowOptions): Promise<number>;
|