@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.
@@ -0,0 +1,191 @@
1
+ import { readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { confirm as clackConfirm, isCancel } from "@clack/prompts";
4
+ import { SCHEMA_VERSION, documentVersion, loadYamlMapping, migrateConfig, migrateWorkflow, patchConfigText, patchWorkflowText, workflowsDirectory, ConfigurationError, } from "@nodeint/migent-kernel";
5
+ import { printError, printNotice } from "./display.js";
6
+ import { adapterMap } from "./provider.js";
7
+ const OUTDATED = "Schema is outdated. Run `migent migrate` to update.";
8
+ export function requireCurrentSchema(workspace) {
9
+ const newer = [];
10
+ let outdated = false;
11
+ for (const [label, filePath] of schemaFiles(workspace)) {
12
+ const version = storedVersion(filePath);
13
+ if (version === null || version === SCHEMA_VERSION)
14
+ continue;
15
+ if (version > SCHEMA_VERSION) {
16
+ newer.push(`${label} schema_version ${version} is newer than this Migent (${SCHEMA_VERSION}).`);
17
+ continue;
18
+ }
19
+ outdated = true;
20
+ }
21
+ if (outdated)
22
+ throw new ConfigurationError(OUTDATED);
23
+ if (newer.length > 0)
24
+ throw new ConfigurationError(newer.join(" "));
25
+ }
26
+ export async function runMigrate(workspace, options = {}) {
27
+ const adapters = options.adapters ?? migrationAdapters();
28
+ const rewrites = pendingRewrites(workspace, adapters);
29
+ if (rewrites.length === 0) {
30
+ process.stdout.write("Schema is current.\n");
31
+ return 0;
32
+ }
33
+ for (const rewrite of rewrites) {
34
+ for (const note of rewrite.notes)
35
+ printNotice(note);
36
+ }
37
+ if (!options.assumeYes && !(await accepted(options.confirm ?? null))) {
38
+ return options.confirm == null && !process.stdin.isTTY ? 2 : 0;
39
+ }
40
+ for (const rewrite of rewrites)
41
+ writeFileSync(rewrite.path, rewrite.text, "utf8");
42
+ process.stdout.write(`Upgraded schema to version ${SCHEMA_VERSION}.\n`);
43
+ return 0;
44
+ }
45
+ export function migrationAdapters(adapters = adapterMap()) {
46
+ const mapped = {};
47
+ for (const [name, adapter] of Object.entries(adapters)) {
48
+ const candidate = adapter;
49
+ mapped[name] = {
50
+ modelMigrations: candidate.modelMigrations ?? {},
51
+ roleMigrations: candidate.roleMigrations ?? {},
52
+ stageMigrations: candidate.stageMigrations ?? {},
53
+ };
54
+ }
55
+ return mapped;
56
+ }
57
+ async function accepted(confirm) {
58
+ const question = `Upgrade schema to version ${SCHEMA_VERSION}?`;
59
+ if (confirm !== null) {
60
+ if (await confirm(question))
61
+ return true;
62
+ process.stdout.write("Left unchanged.\n");
63
+ return false;
64
+ }
65
+ if (!process.stdin.isTTY) {
66
+ printError("Pass --yes to upgrade without confirmation.");
67
+ return false;
68
+ }
69
+ const result = await clackConfirm({ message: question, initialValue: false });
70
+ if (isCancel(result) || result !== true) {
71
+ process.stdout.write("Left unchanged.\n");
72
+ return false;
73
+ }
74
+ return true;
75
+ }
76
+ function pendingRewrites(workspace, adapters) {
77
+ const configPath = path.join(workspace, ".migent", "config.yaml");
78
+ if (!isFile(configPath))
79
+ return [];
80
+ const [config, configRewrite] = configRewriteOf(configPath, adapters);
81
+ if (config === null)
82
+ return [];
83
+ const rewrites = [];
84
+ if (configRewrite !== null)
85
+ rewrites.push(configRewrite);
86
+ rewrites.push(...workflowRewrites(workspace, config, adapters));
87
+ return rewrites;
88
+ }
89
+ function schemaFiles(workspace) {
90
+ const files = [[".migent/config.yaml", path.join(workspace, ".migent", "config.yaml")]];
91
+ const directory = workflowsDirectory(workspace);
92
+ if (isDirectory(directory)) {
93
+ for (const name of readdirSync(directory).filter((item) => item.endsWith(".yaml")).sort()) {
94
+ const filePath = path.join(directory, name);
95
+ if (isFile(filePath))
96
+ files.push([`.migent/workflows/${name}`, filePath]);
97
+ }
98
+ }
99
+ return files;
100
+ }
101
+ function storedVersion(filePath) {
102
+ if (!isFile(filePath))
103
+ return null;
104
+ try {
105
+ return documentVersion(loadYamlMapping(filePath));
106
+ }
107
+ catch {
108
+ return null;
109
+ }
110
+ }
111
+ function configRewriteOf(filePath, adapters) {
112
+ const original = readFileSync(filePath, "utf8");
113
+ let parsed;
114
+ try {
115
+ parsed = loadYamlMapping(filePath);
116
+ }
117
+ catch {
118
+ return [null, null];
119
+ }
120
+ const updated = structuredClone(parsed);
121
+ let notes;
122
+ try {
123
+ notes = migrateConfig(updated, adapters);
124
+ }
125
+ catch (error) {
126
+ if (error instanceof ConfigurationError)
127
+ throw error;
128
+ return [null, null];
129
+ }
130
+ if (notes.length === 0)
131
+ return [updated, null];
132
+ const rewritten = patchConfigText(original, updated);
133
+ if (rewritten === original)
134
+ return [updated, null];
135
+ const labeled = notes.map((note) => note.startsWith("schema_version") ? `Migrated .migent/config.yaml ${note}` : note);
136
+ return [updated, { path: filePath, text: rewritten, notes: labeled }];
137
+ }
138
+ function workflowRewrites(workspace, config, adapters) {
139
+ const directory = workflowsDirectory(workspace);
140
+ if (!isDirectory(directory))
141
+ return [];
142
+ const rewrites = [];
143
+ for (const name of readdirSync(directory).filter((item) => item.endsWith(".yaml")).sort()) {
144
+ const filePath = path.join(directory, name);
145
+ if (!isFile(filePath))
146
+ continue;
147
+ const original = readFileSync(filePath, "utf8");
148
+ let parsed;
149
+ try {
150
+ parsed = loadYamlMapping(filePath);
151
+ }
152
+ catch {
153
+ continue;
154
+ }
155
+ const updated = structuredClone(parsed);
156
+ let changed;
157
+ try {
158
+ changed = migrateWorkflow(updated, config, adapters);
159
+ }
160
+ catch (error) {
161
+ if (error instanceof ConfigurationError)
162
+ throw error;
163
+ continue;
164
+ }
165
+ if (changed.length === 0)
166
+ continue;
167
+ const rewritten = patchWorkflowText(original, updated);
168
+ if (rewritten === original)
169
+ continue;
170
+ const relative = `.migent/workflows/${name}`;
171
+ const notes = changed.map((note) => note.startsWith("schema_version") ? `Migrated ${relative} ${note}` : `${relative}: ${note}`);
172
+ rewrites.push({ path: filePath, text: rewritten, notes });
173
+ }
174
+ return rewrites;
175
+ }
176
+ function isFile(filePath) {
177
+ try {
178
+ return statSync(filePath).isFile();
179
+ }
180
+ catch {
181
+ return false;
182
+ }
183
+ }
184
+ function isDirectory(filePath) {
185
+ try {
186
+ return statSync(filePath).isDirectory();
187
+ }
188
+ catch {
189
+ return false;
190
+ }
191
+ }
@@ -0,0 +1,6 @@
1
+ import { Command } from "commander";
2
+ export interface CliStatus {
3
+ code: number;
4
+ handled: boolean;
5
+ }
6
+ export declare function createProgram(status: CliStatus): Command;
@@ -0,0 +1,414 @@
1
+ import { createRequire } from "node:module";
2
+ import { Command } from "commander";
3
+ import { ConfigurationError, DEFAULT_MAX_DISPATCHES } from "@nodeint/migent-kernel";
4
+ import { dispatchAgent } from "./commands/agent.js";
5
+ import { runConfigModel, runConfigRole, runConfigShow } from "./commands/config.js";
6
+ import { runDoctor } from "./commands/doctor.js";
7
+ import { executeWorkflow } from "./commands/execute.js";
8
+ import { runInit } from "./commands/init.js";
9
+ import { listSessions } from "./commands/sessions.js";
10
+ import { parsePort, runServiceStart, runServiceStatus, runServiceStop } from "./commands/service.js";
11
+ import { installSkill, updateSkill } from "./commands/skill.js";
12
+ import { watchSession } from "./commands/watch.js";
13
+ import { runRegister, runRemove, runWorkspaceList } from "./commands/workspace-registry.js";
14
+ import { printError } from "./display.js";
15
+ import { runMigrate } from "./migrate.js";
16
+ import { findWorkspace, invocationCwdPath } from "./workspace.js";
17
+ const { version } = createRequire(import.meta.url)("../package.json");
18
+ const START_EXITS = [
19
+ "Exit 0 when the session is completed.",
20
+ "Exit 1 when the session is blocked.",
21
+ "Exit 2 on a configuration error.",
22
+ "Exit 3 when --attempts is spent. The session stays active.",
23
+ "Exit 4 when a route would exceed max_revisions. The session stays active.",
24
+ "Exit 5 when --max-dispatches is spent. The session stays active.",
25
+ "Exit 130 when cancelled.",
26
+ ];
27
+ const CONTINUE_EXITS = [
28
+ "Exit 0 when the session is completed.",
29
+ "Exit 1 when the session is blocked.",
30
+ "Exit 2 on a configuration error.",
31
+ "Exit 3 when --attempts is spent. The session stays active.",
32
+ "Exit 4 when a route would exceed max_revisions. The session stays active.",
33
+ "Exit 5 when --max-dispatches is spent. The session stays active.",
34
+ "Exit 130 when cancelled.",
35
+ ];
36
+ const STAGE_EXITS = [
37
+ "Exit 0 when the stage outcome is complete, or the session is already completed.",
38
+ "Exit 1 when the stage is not complete, or the session is blocked.",
39
+ "Exit 2 on a configuration error.",
40
+ "Exit 4 when a route would exceed max_revisions. The session stays active.",
41
+ "Exit 130 when cancelled.",
42
+ ];
43
+ export function createProgram(status) {
44
+ const program = new Command();
45
+ program
46
+ .name("migent")
47
+ .description("Run named workflows and standalone roles from .migent/.")
48
+ .version(version, "-V, --version", "Print the Migent version.")
49
+ .exitOverride();
50
+ const start = program
51
+ .command("start")
52
+ .description("Create a session and continue until it stops.")
53
+ .argument("<workflow>", "Workflow id, or a path to a workflow file.")
54
+ .requiredOption("--task <task>", "Task recorded on the new session.");
55
+ addDispatchOptions(start);
56
+ start.addHelpText("after", exitText(START_EXITS)).action((workflow, options) => settle(status, () => {
57
+ const ceiling = dispatchCeiling(options);
58
+ if (typeof ceiling === "string")
59
+ return 2;
60
+ return executeWorkflow(findWorkspace(invocationCwdPath()), {
61
+ action: "start",
62
+ workflowId: workflow,
63
+ task: String(options.task),
64
+ priorSessionId: optionalText(options.prior),
65
+ maxAttempts: attemptsOf(options),
66
+ maxDispatches: ceiling,
67
+ });
68
+ }));
69
+ const continueCommand = program
70
+ .command("continue")
71
+ .description("Resume a session until it stops.")
72
+ .argument("<session>", "Session to resume.");
73
+ addDispatchOptions(continueCommand);
74
+ continueCommand.addHelpText("after", exitText(CONTINUE_EXITS)).action((session, options) => settle(status, () => {
75
+ const ceiling = dispatchCeiling(options);
76
+ if (typeof ceiling === "string")
77
+ return 2;
78
+ return executeWorkflow(findWorkspace(invocationCwdPath()), {
79
+ action: "continue",
80
+ sessionId: session,
81
+ priorSessionId: optionalText(options.prior),
82
+ maxAttempts: attemptsOf(options),
83
+ maxDispatches: ceiling,
84
+ });
85
+ }));
86
+ program
87
+ .command("stage")
88
+ .description("Advance one stage. Dispatch the next eligible stage once.")
89
+ .argument("[session]", "Session to advance. Omit when passing --workflow.")
90
+ .option("--workflow <workflow>", "Workflow id or path. Creates a session and dispatches its next stage once.")
91
+ .option("--task <task>", "Task recorded on the new session. Required with --workflow.")
92
+ .option("--prior <session>", "Completed session whose workflow and decision match requires.")
93
+ .addHelpText("after", exitText(STAGE_EXITS))
94
+ .action((session, options) => settle(status, () => {
95
+ const sessionId = text(session);
96
+ const workflow = optionalText(options.workflow);
97
+ const task = optionalText(options.task);
98
+ if (sessionId !== null && (workflow !== null || task !== null)) {
99
+ printError("Pass SESSION, or --workflow and --task, not both.");
100
+ return 2;
101
+ }
102
+ if (sessionId === null && (workflow === null || task === null)) {
103
+ printError("Pass SESSION, or both --workflow and --task.");
104
+ return 2;
105
+ }
106
+ return executeWorkflow(findWorkspace(invocationCwdPath()), {
107
+ action: "stage",
108
+ workflowId: workflow,
109
+ task,
110
+ sessionId,
111
+ priorSessionId: optionalText(options.prior),
112
+ maxAttempts: null,
113
+ maxDispatches: null,
114
+ });
115
+ }));
116
+ program
117
+ .command("init")
118
+ .description("Create a minimal config, one workflow, and a sessions gitignore.")
119
+ .option("--provider <provider>", "Provider for the default model. Pass with --model.")
120
+ .option("--model <model>", "Model id for the default model. Pass with --provider.")
121
+ .option("--option <pair>", "KEY=VALUE stored under models.*.options.", collect, [])
122
+ .option("--preset <preset>", "Workflow to create: implement or plan. Default: implement.", "implement")
123
+ .action((options) => settle(status, () => {
124
+ const provider = optionalText(options.provider);
125
+ const model = optionalText(options.model);
126
+ const interactive = provider === null && Boolean(process.stdin.isTTY);
127
+ return runInit(invocationCwdPath(), {
128
+ provider,
129
+ model,
130
+ options: stringList(options.option),
131
+ preset: typeof options.preset === "string" ? options.preset : "implement",
132
+ interactive,
133
+ });
134
+ }));
135
+ const config = program.command("config").description("Update models and roles in .migent/config.yaml.");
136
+ config
137
+ .command("show")
138
+ .description("Print .migent/config.yaml.")
139
+ .action(() => settle(status, () => runConfigShow(findWorkspace(invocationCwdPath()))));
140
+ config
141
+ .command("model")
142
+ .description("List models, or create and update one model name.")
143
+ .argument("[name]", "Name to show or update.")
144
+ .option("--provider <provider>", "Provider for this name. Pass with --model.")
145
+ .option("--model <model>", "Model id for this name. Pass with --provider.")
146
+ .option("--option <pair>", "KEY=VALUE stored under this model's options.", collect, [])
147
+ .option("--clear-option <key>", "Remove one key from this model's options.", collect, [])
148
+ .option("--json", "Print one JSON object.")
149
+ .action((name, options) => settle(status, () => {
150
+ const provider = optionalText(options.provider);
151
+ const model = optionalText(options.model);
152
+ const optionValues = stringList(options.option);
153
+ const clearOptions = stringList(options.clearOption);
154
+ const mutating = provider !== null || model !== null || optionValues.length > 0 || clearOptions.length > 0;
155
+ const interactive = Boolean(process.stdin.isTTY) && options.json !== true && !mutating;
156
+ return runConfigModel(findWorkspace(invocationCwdPath()), {
157
+ key: text(name),
158
+ provider,
159
+ model,
160
+ options: optionValues,
161
+ clearOptions,
162
+ asJson: options.json === true,
163
+ interactive,
164
+ });
165
+ }));
166
+ config
167
+ .command("role")
168
+ .description("List roles, or point roles at a model.")
169
+ .argument("[role...]", "Role to show or update.")
170
+ .option("--model-name <name>", "Point the roles at this model.")
171
+ .option("--provider <provider>", "Provider for this role only. Pass with --model.")
172
+ .option("--model <model>", "Model id for this role only. Pass with --provider.")
173
+ .option("--option <pair>", "KEY=VALUE stored under this role's options.", collect, [])
174
+ .option("--clear-option <key>", "Remove one key from this role's options.", collect, [])
175
+ .option("--json", "Print one JSON object.")
176
+ .action((roles, options) => settle(status, () => {
177
+ const unique = roles
178
+ .map((role) => text(role))
179
+ .filter((role) => role !== null)
180
+ .filter((role, index, all) => all.indexOf(role) === index);
181
+ const provider = optionalText(options.provider);
182
+ const model = optionalText(options.model);
183
+ const modelName = optionalText(options.modelName);
184
+ const optionValues = stringList(options.option);
185
+ const clearOptions = stringList(options.clearOption);
186
+ const mutating = modelName !== null || provider !== null || model !== null || optionValues.length > 0 || clearOptions.length > 0;
187
+ const interactive = Boolean(process.stdin.isTTY) && options.json !== true && !mutating;
188
+ return runConfigRole(findWorkspace(invocationCwdPath()), {
189
+ roles: unique,
190
+ modelName,
191
+ provider,
192
+ model,
193
+ options: optionValues,
194
+ clearOptions,
195
+ asJson: options.json === true,
196
+ interactive,
197
+ });
198
+ }));
199
+ showHelpWhenBare(status, config);
200
+ program
201
+ .command("doctor")
202
+ .description("Check config, workflows, and provider CLIs.")
203
+ .option("--json", "Print the checks as one JSON object.")
204
+ .action((options) => settle(status, () => runDoctor(findWorkspace(invocationCwdPath()), { asJson: options.json === true })));
205
+ program
206
+ .command("migrate")
207
+ .description("Upgrade config and workflows to the current schema version.")
208
+ .option("-y, --yes", "Upgrade without asking. Use this when no terminal is available.")
209
+ .action((options) => settle(status, () => runMigrate(findWorkspace(invocationCwdPath()), { assumeYes: options.yes === true })));
210
+ program
211
+ .command("sessions")
212
+ .description("List sessions and their latest status.")
213
+ .option("--json", "Print the same rows as a JSON array.")
214
+ .action((options) => settle(status, () => listSessions(findWorkspace(invocationCwdPath()), { asJson: options.json === true })));
215
+ program
216
+ .command("watch")
217
+ .description("Follow a session.")
218
+ .argument("<session>", "Session to follow.")
219
+ .option("--execution <execution>", "Limit followed events and --until to this execution.")
220
+ .option("--json", "With --once, print one JSON snapshot. While following, print JSON lines.")
221
+ .option("--cursor <offset>", "Replay events.jsonl from this byte offset. 0 replays the whole log.")
222
+ .option("--once", "Print the snapshot and events cursor, then exit.")
223
+ .option("--until", "Exit when --execution reaches completed, failed, or cancelled.")
224
+ .action((session, options) => settle(status, () => {
225
+ const cursor = options.cursor === undefined ? null : Number(options.cursor);
226
+ if (options.cursor !== undefined && (!Number.isInteger(cursor) || (cursor ?? 0) < 0)) {
227
+ throw new ConfigurationError("--cursor must be >= 0.");
228
+ }
229
+ return watchSession(findWorkspace(invocationCwdPath()), session, {
230
+ once: options.once === true,
231
+ executionId: optionalText(options.execution),
232
+ untilTerminal: options.until === true,
233
+ jsonLines: options.json === true,
234
+ cursor,
235
+ });
236
+ }));
237
+ const agent = program.command("agent").description("Run one role outside a workflow.");
238
+ const agentStart = agent.command("start").description("Create a session and dispatch one turn.");
239
+ addAgentOptions(agentStart);
240
+ agentStart.action((options) => settle(status, () => runAgent(options, null)));
241
+ const agentContinue = agent
242
+ .command("continue")
243
+ .description("Append a turn to a session.")
244
+ .argument("<session>", "Session to continue.");
245
+ addAgentOptions(agentContinue);
246
+ agentContinue.action((session, options) => settle(status, () => runAgent(options, session)));
247
+ showHelpWhenBare(status, agent);
248
+ const skill = program.command("skill").description("Install or update the Migent skill in this project.");
249
+ skill
250
+ .command("install")
251
+ .description("Install the Migent skill without replacing an existing directory.")
252
+ .action(() => settle(status, () => installSkill(findWorkspace(invocationCwdPath()))));
253
+ skill
254
+ .command("update")
255
+ .description("Update a managed Migent skill to the bundled template.")
256
+ .option("--force", "Replace a locally modified managed skill with the bundled template.")
257
+ .action((options) => settle(status, () => updateSkill(findWorkspace(invocationCwdPath()), { force: options.force === true })));
258
+ showHelpWhenBare(status, skill);
259
+ const service = program.command("service").description("Run the local workspace registry service.");
260
+ service
261
+ .command("start")
262
+ .description("Start the registry service, or adopt one that is already running.")
263
+ .option("--port <port>", "Bind 127.0.0.1 on this port. The default is 47321.")
264
+ .option("--foreground", "Serve in this process instead of detaching.")
265
+ .action((options) => settle(status, () => {
266
+ const port = options.port === undefined ? null : parsePort(String(options.port));
267
+ return runServiceStart(port, options.foreground === true);
268
+ }));
269
+ service
270
+ .command("stop")
271
+ .description("Stop the registry service when its process identity matches.")
272
+ .action(() => settle(status, () => runServiceStop()));
273
+ service
274
+ .command("status")
275
+ .description("Report whether the registry service is running.")
276
+ .option("--json", "Print one JSON object.")
277
+ .action((options) => settle(status, () => runServiceStatus(options.json === true)));
278
+ showHelpWhenBare(status, service);
279
+ const workspace = program.command("workspace").description("Register this machine's Migent workspaces.");
280
+ workspace
281
+ .command("register")
282
+ .description("Register the workspace that contains .migent/config.yaml.")
283
+ .option("--path <path>", "Workspace root or a directory inside it.")
284
+ .option("--name <name>", "Display name. Defaults to the directory name.")
285
+ .action((options) => settle(status, () => runRegister(optionalText(options.path), optionalText(options.name))));
286
+ workspace
287
+ .command("list")
288
+ .description("List registered workspaces.")
289
+ .option("--json", "Print one JSON object.")
290
+ .action((options) => settle(status, () => runWorkspaceList(options.json === true)));
291
+ workspace
292
+ .command("remove")
293
+ .description("Remove one registered workspace by id.")
294
+ .argument("<id>", "Workspace id to remove.")
295
+ .action((id) => settle(status, () => runRemove(id)));
296
+ showHelpWhenBare(status, workspace);
297
+ program.allowExcessArguments();
298
+ program.action(() => {
299
+ const unknown = program.args[0];
300
+ if (unknown) {
301
+ program.error(`error: unknown command '${unknown}'`, { code: "commander.unknownCommand" });
302
+ }
303
+ program.outputHelp();
304
+ status.handled = true;
305
+ status.code = 2;
306
+ });
307
+ return program;
308
+ }
309
+ function addDispatchOptions(command) {
310
+ command
311
+ .option("--prior <session>", "Completed session whose workflow and decision match requires.")
312
+ .option("--attempts <count>", "Failed executions allowed for one stage. Default: 3.")
313
+ .option("--max-dispatches <count>", `Stage dispatches allowed in this command. Default: ${DEFAULT_MAX_DISPATCHES}. Pass --unlimited-dispatches to remove the ceiling.`)
314
+ .option("--unlimited-dispatches", "Remove the dispatch ceiling for this command.");
315
+ }
316
+ function addAgentOptions(command) {
317
+ command
318
+ .requiredOption("--role <role>", "Key under roles in .migent/config.yaml.")
319
+ .option("--task <task>", "Label stored on the new session. Defaults to the role key.")
320
+ .option("--provider <provider>", "Provider for this role. Pass with --model, or omit both.")
321
+ .option("--model <model>", "Native model id for this role. Pass with --provider, or omit both.")
322
+ .option("--option <pair>", "KEY=VALUE overriding one provider option for this turn.", collect, [])
323
+ .option("--prompt <prompt>", "Prompt text.")
324
+ .option("--file <file>", "Read the prompt from this file.")
325
+ .option("--stdin", "Read the prompt from stdin.")
326
+ .option("--json", "Print the result as one JSON object.");
327
+ }
328
+ function showHelpWhenBare(status, command) {
329
+ command.action(() => {
330
+ command.outputHelp();
331
+ status.handled = true;
332
+ status.code = 2;
333
+ });
334
+ }
335
+ function settle(status, action) {
336
+ return Promise.resolve(action()).then((code) => {
337
+ status.handled = true;
338
+ status.code = code;
339
+ });
340
+ }
341
+ function exitText(lines) {
342
+ return `\n${lines.join("\n")}\n`;
343
+ }
344
+ function collect(value, previous) {
345
+ previous.push(value);
346
+ return previous;
347
+ }
348
+ function stringList(value) {
349
+ if (!Array.isArray(value))
350
+ return [];
351
+ return value.filter((item) => typeof item === "string");
352
+ }
353
+ async function runAgent(options, sessionId) {
354
+ const provider = optionalText(options.provider);
355
+ const model = optionalText(options.model);
356
+ if ((provider === null) !== (model === null)) {
357
+ printError("Pass both --provider and --model, or omit both.");
358
+ return 2;
359
+ }
360
+ const prompt = optionalText(options.prompt);
361
+ const promptFile = optionalText(options.file);
362
+ const promptStdin = options.stdin === true;
363
+ const selected = Number(prompt !== null) + Number(promptFile !== null) + Number(promptStdin);
364
+ if (selected !== 1) {
365
+ printError("Pass one of --prompt, --file, or --stdin.");
366
+ return 2;
367
+ }
368
+ return dispatchAgent(findWorkspace(invocationCwdPath()), {
369
+ role: String(options.role),
370
+ task: optionalText(options.task),
371
+ sessionId,
372
+ provider,
373
+ model,
374
+ options: stringList(options.option),
375
+ prompt,
376
+ promptFile,
377
+ promptStdin,
378
+ asJson: options.json === true,
379
+ });
380
+ }
381
+ function dispatchCeiling(options) {
382
+ const maxDispatches = optionalText(options.maxDispatches);
383
+ const unlimited = options.unlimitedDispatches === true;
384
+ if (unlimited && maxDispatches !== null) {
385
+ printError("--max-dispatches cannot be combined with --unlimited-dispatches.");
386
+ return "error";
387
+ }
388
+ if (unlimited)
389
+ return null;
390
+ if (maxDispatches === null)
391
+ return DEFAULT_MAX_DISPATCHES;
392
+ return integerFlag(maxDispatches, "--max-dispatches");
393
+ }
394
+ function attemptsOf(options) {
395
+ const raw = optionalText(options.attempts);
396
+ if (raw === null)
397
+ return 3;
398
+ return integerFlag(raw, "--attempts");
399
+ }
400
+ function integerFlag(raw, flag) {
401
+ if (!/^[0-9]+$/.test(raw) || Number(raw) < 1) {
402
+ throw new ConfigurationError(`${flag} must be at least 1.`);
403
+ }
404
+ return Number(raw);
405
+ }
406
+ function optionalText(value) {
407
+ return text(typeof value === "string" ? value : null);
408
+ }
409
+ function text(value) {
410
+ if (typeof value !== "string")
411
+ return null;
412
+ const stripped = value.trim();
413
+ return stripped === "" ? null : stripped;
414
+ }
@@ -0,0 +1,23 @@
1
+ import { ModelCatalog } from "@nodeint/migent-adapters";
2
+ import type { AgentAdapter, ProviderOption } from "@nodeint/migent-kernel";
3
+ export interface CatalogAdapter extends AgentAdapter {
4
+ modelCatalogCommand(): string[];
5
+ parseModelCatalog(stdout: string): ModelCatalog;
6
+ optionValuesCommand(name: string, model: string): string[] | null;
7
+ parseOptionValues(name: string, text: string, model?: string): readonly string[];
8
+ providerOptions(): readonly ProviderOption[];
9
+ }
10
+ export type Which = (command: string) => string | null;
11
+ export type RunCommand = (argv: string[]) => string | Promise<string>;
12
+ export declare class ProviderCommandFailed extends Error {
13
+ }
14
+ export declare function adapterMap(): Record<string, AgentAdapter>;
15
+ export declare function asCatalog(adapter: AgentAdapter): CatalogAdapter;
16
+ export declare function requireAdapter(adapters: Record<string, AgentAdapter>, provider: string): AgentAdapter;
17
+ export declare function detected(adapters: Record<string, AgentAdapter>, which: Which): Array<[string, string | null, AgentAdapter]>;
18
+ export declare function loadCatalog(adapter: AgentAdapter, which: Which, runCommand: RunCommand): Promise<ModelCatalog>;
19
+ export declare function knownModel(model: string, catalog: ModelCatalog): string;
20
+ export declare function parseOptions(pairs: readonly string[]): Record<string, string>;
21
+ export declare function loadOptionValues(adapter: AgentAdapter, name: string, model: string, catalog: ModelCatalog, which: Which, runCommand: RunCommand): Promise<readonly string[] | null>;
22
+ export declare function acceptedOptions(adapter: AgentAdapter, model: string, catalog: ModelCatalog, which: Which, runCommand: RunCommand, options: Readonly<Record<string, string>>): Promise<Record<string, string>>;
23
+ export declare function runProviderCommand(argv: string[]): string;