@nodeint/migent 0.1.3 → 0.1.5
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 +57 -41
- package/dist/commands/agent.js +111 -59
- package/dist/commands/config.js +20 -8
- package/dist/commands/execute.d.ts +6 -0
- package/dist/commands/execute.js +30 -14
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +91 -87
- package/dist/commands/prepare.d.ts +1 -0
- package/dist/commands/prepare.js +14 -0
- package/dist/commands/repair.d.ts +1 -0
- package/dist/commands/repair.js +22 -0
- package/dist/commands/service.js +5 -4
- package/dist/commands/sessions.d.ts +24 -1
- package/dist/commands/sessions.js +165 -13
- package/dist/commands/skill.js +51 -32
- package/dist/commands/watch.d.ts +2 -1
- package/dist/commands/watch.js +130 -67
- package/dist/commands/workspace-registry.js +10 -1
- package/dist/display.js +21 -9
- package/dist/errors.d.ts +3 -0
- package/dist/errors.js +6 -0
- package/dist/exits.d.ts +10 -0
- package/dist/exits.js +26 -0
- package/dist/follow-log.d.ts +29 -0
- package/dist/follow-log.js +172 -0
- package/dist/fs-utils.d.ts +3 -0
- package/dist/fs-utils.js +26 -0
- package/dist/index.js +5 -1
- package/dist/migrate.js +2 -17
- package/dist/program.js +388 -168
- package/dist/session-row.d.ts +20 -0
- package/dist/session-row.js +21 -0
- package/dist/signals.d.ts +3 -0
- package/dist/signals.js +56 -0
- package/dist/templates/migent/SKILL.md +159 -50
- package/dist/templates/migent/references/schema-config.yaml +35 -0
- package/dist/templates/migent/references/schema-workflow.yaml +61 -0
- package/dist/text.d.ts +4 -0
- package/dist/text.js +13 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/dist/workspace.js +5 -12
- package/package.json +4 -4
package/dist/commands/init.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
import { mkdirSync, readdirSync,
|
|
1
|
+
import { mkdirSync, readdirSync, writeFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { confirm as clackConfirm, isCancel, select as clackSelect } from "@clack/prompts";
|
|
4
|
-
import { diagnose, whichSync, yamlScalar } from "@nodeint/migent-kernel";
|
|
4
|
+
import { ConfigurationError, diagnose, whichSync, yamlScalar, } from "@nodeint/migent-kernel";
|
|
5
|
+
import { PromptCancelled } from "../errors.js";
|
|
6
|
+
import { exists, isDirectory, isFile } from "../fs-utils.js";
|
|
5
7
|
import { printAlreadyInitialized, printCreated, printError, printInitConflicts, } from "../display.js";
|
|
6
8
|
import { cliHooks } from "../hooks.js";
|
|
7
9
|
import { runDoctor } from "./doctor.js";
|
|
10
|
+
import { installSkill } from "./skill.js";
|
|
8
11
|
import { acceptedOptions, adapterMap, asCatalog, detected, knownModel, loadCatalog, loadOptionValues, parseOptions, requireAdapter, runProviderCommand, } from "../provider.js";
|
|
9
12
|
export const PRESETS = ["implement", "plan"];
|
|
10
13
|
export async function runInit(workspace, options = {}) {
|
|
11
14
|
const provider = options.provider ?? null;
|
|
12
15
|
const model = options.model ?? null;
|
|
13
16
|
const preset = options.preset ?? "implement";
|
|
17
|
+
const skill = options.skill === true;
|
|
14
18
|
const interactive = options.interactive ?? false;
|
|
15
19
|
const chosen = options.adapters ?? adapterMap();
|
|
16
20
|
const resolve = options.which ?? cliHooks.which ?? whichSync;
|
|
@@ -20,11 +24,23 @@ export async function runInit(workspace, options = {}) {
|
|
|
20
24
|
printError("preset must be implement or plan.");
|
|
21
25
|
return 2;
|
|
22
26
|
}
|
|
27
|
+
const optionPairs = options.options ?? [];
|
|
28
|
+
const classified = classify(workspace, chosen, resolve);
|
|
29
|
+
if (classified.state === "ready") {
|
|
30
|
+
if (provider !== null || model !== null || optionPairs.length > 0) {
|
|
31
|
+
printError("Already initialized; ignoring --provider/--model/--option.");
|
|
32
|
+
}
|
|
33
|
+
if (options.presetExplicit === true)
|
|
34
|
+
printError("Already initialized; ignoring --preset.");
|
|
35
|
+
printAlreadyInitialized();
|
|
36
|
+
if (skill)
|
|
37
|
+
return installSkill(workspace);
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
23
40
|
if ((provider === null) !== (model === null)) {
|
|
24
41
|
printError("Pass both --provider and --model, or omit both.");
|
|
25
42
|
return 2;
|
|
26
43
|
}
|
|
27
|
-
const optionPairs = options.options ?? [];
|
|
28
44
|
if (optionPairs.length > 0 && provider === null) {
|
|
29
45
|
printError("Pass --provider and --model with --option.");
|
|
30
46
|
return 2;
|
|
@@ -33,34 +49,58 @@ export async function runInit(workspace, options = {}) {
|
|
|
33
49
|
printError("Pass --provider and --model when init is not interactive.");
|
|
34
50
|
return 2;
|
|
35
51
|
}
|
|
36
|
-
const classified = classify(workspace, chosen, resolve);
|
|
37
|
-
if (classified.state === "ready") {
|
|
38
|
-
printAlreadyInitialized();
|
|
39
|
-
return 0;
|
|
40
|
-
}
|
|
41
52
|
if (classified.state === "conflict") {
|
|
42
53
|
printInitConflicts(classified.conflicts);
|
|
43
54
|
return 1;
|
|
44
55
|
}
|
|
45
56
|
let primary;
|
|
46
|
-
let reviewer;
|
|
47
57
|
try {
|
|
48
|
-
|
|
58
|
+
primary = await choices(chosen, resolve, prompt, {
|
|
49
59
|
provider,
|
|
50
60
|
model,
|
|
51
61
|
options: parseOptions(optionPairs),
|
|
52
|
-
preset,
|
|
53
|
-
interactive,
|
|
54
62
|
runCommand: execute,
|
|
55
63
|
});
|
|
56
64
|
}
|
|
57
65
|
catch (error) {
|
|
66
|
+
if (error instanceof PromptCancelled)
|
|
67
|
+
throw error;
|
|
58
68
|
printError(error instanceof Error ? error.message : String(error));
|
|
59
69
|
return 2;
|
|
60
70
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
71
|
+
let install = skill;
|
|
72
|
+
if (!install && interactive) {
|
|
73
|
+
try {
|
|
74
|
+
install = await prompt.confirm("Install the Migent skill in .agents/skills/migent?");
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (error instanceof PromptCancelled)
|
|
78
|
+
throw error;
|
|
79
|
+
printError(error instanceof Error ? error.message : String(error));
|
|
80
|
+
return 2;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (install && installSkill(workspace) !== 0)
|
|
84
|
+
return 1;
|
|
85
|
+
const paths = writeProject(workspace, preset, primary);
|
|
86
|
+
printCreated(paths, `migent session run --workflow ${preset} --task "..."`);
|
|
87
|
+
if (interactive) {
|
|
88
|
+
const doctorCode = await runDoctor(workspace, { which: resolve });
|
|
89
|
+
if (doctorCode !== 0)
|
|
90
|
+
writeDoctorProblems(workspace, chosen, resolve);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
writeDoctorProblems(workspace, chosen, resolve);
|
|
94
|
+
}
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
function writeDoctorProblems(workspace, adapters, which) {
|
|
98
|
+
const problems = diagnose(workspace, { adapters, which }).flatMap((check) => check.problems);
|
|
99
|
+
if (problems.length === 0)
|
|
100
|
+
return;
|
|
101
|
+
process.stderr.write("Doctor reported problems:\n");
|
|
102
|
+
for (const problem of problems)
|
|
103
|
+
process.stderr.write(`${problem}\n`);
|
|
64
104
|
}
|
|
65
105
|
async function choices(adapters, which, prompt, args) {
|
|
66
106
|
let provider = args.provider;
|
|
@@ -71,7 +111,7 @@ async function choices(adapters, which, prompt, args) {
|
|
|
71
111
|
provider = await prompt.selectProvider(rows, "Default provider");
|
|
72
112
|
const adapter = adapters[provider];
|
|
73
113
|
if (adapter === undefined)
|
|
74
|
-
throw new
|
|
114
|
+
throw new ConfigurationError(`Unknown provider: ${provider}.`);
|
|
75
115
|
const catalog = await loadCatalog(adapter, which, args.runCommand);
|
|
76
116
|
model = await prompt.selectModel(provider, catalog, "Model");
|
|
77
117
|
stored = await promptOptions(adapter, model, catalog, which, args.runCommand, prompt);
|
|
@@ -79,27 +119,15 @@ async function choices(adapters, which, prompt, args) {
|
|
|
79
119
|
else {
|
|
80
120
|
const adapter = requireAdapter(adapters, provider);
|
|
81
121
|
if (model === null)
|
|
82
|
-
throw new
|
|
122
|
+
throw new ConfigurationError("Model is required.");
|
|
83
123
|
const catalog = await loadCatalog(adapter, which, args.runCommand);
|
|
84
124
|
model = knownModel(model, catalog);
|
|
85
125
|
stored = await acceptedOptions(adapter, model, catalog, which, args.runCommand, args.options);
|
|
86
126
|
}
|
|
87
127
|
if (model.trim() === "")
|
|
88
|
-
throw new
|
|
128
|
+
throw new ConfigurationError("Model is required.");
|
|
89
129
|
const primary = { provider, model: model.trim(), options: stored };
|
|
90
|
-
|
|
91
|
-
return [primary, null];
|
|
92
|
-
if (await prompt.confirm("Use the default model for both planner and reviewer?"))
|
|
93
|
-
return [primary, null];
|
|
94
|
-
const rows = detected(adapters, which);
|
|
95
|
-
const reviewerProvider = await prompt.selectProvider(rows, "Reviewer provider");
|
|
96
|
-
const reviewerAdapter = adapters[reviewerProvider];
|
|
97
|
-
if (reviewerAdapter === undefined)
|
|
98
|
-
throw new Error(`Unknown provider: ${reviewerProvider}.`);
|
|
99
|
-
const reviewerCatalog = await loadCatalog(reviewerAdapter, which, args.runCommand);
|
|
100
|
-
const reviewerModel = (await prompt.selectModel(reviewerProvider, reviewerCatalog, "Reviewer model")).trim();
|
|
101
|
-
const reviewerOptions = await promptOptions(reviewerAdapter, reviewerModel, reviewerCatalog, which, args.runCommand, prompt);
|
|
102
|
-
return [primary, { provider: reviewerProvider, model: reviewerModel, options: reviewerOptions }];
|
|
130
|
+
return primary;
|
|
103
131
|
}
|
|
104
132
|
function classify(workspace, adapters, which) {
|
|
105
133
|
const root = path.join(workspace, ".migent");
|
|
@@ -133,28 +161,24 @@ function classify(workspace, adapters, which) {
|
|
|
133
161
|
}
|
|
134
162
|
return { state: "conflict", conflicts: owned };
|
|
135
163
|
}
|
|
136
|
-
function writeProject(workspace, preset, primary
|
|
164
|
+
function writeProject(workspace, preset, primary) {
|
|
137
165
|
const root = path.join(workspace, ".migent");
|
|
138
166
|
const workflows = path.join(root, "workflows");
|
|
139
167
|
mkdirSync(workflows, { recursive: true });
|
|
140
|
-
writeFileSync(path.join(root, "config.yaml"), renderConfig(preset, primary
|
|
168
|
+
writeFileSync(path.join(root, "config.yaml"), renderConfig(preset, primary), "utf8");
|
|
141
169
|
const workflowName = `${preset}.yaml`;
|
|
142
170
|
writeFileSync(path.join(workflows, workflowName), renderWorkflow(preset), "utf8");
|
|
143
171
|
writeFileSync(path.join(root, ".gitignore"), "sessions/\n", "utf8");
|
|
144
172
|
return [".migent/config.yaml", `.migent/workflows/${workflowName}`, ".migent/.gitignore"];
|
|
145
173
|
}
|
|
146
|
-
function renderConfig(preset, primary
|
|
174
|
+
function renderConfig(preset, primary) {
|
|
147
175
|
const models = [["default", primary]];
|
|
148
176
|
let roles;
|
|
149
177
|
if (preset === "implement")
|
|
150
178
|
roles = [["developer", "default"]];
|
|
151
|
-
else
|
|
179
|
+
else
|
|
152
180
|
roles = [["planner", "default"], ["reviewer", "default"]];
|
|
153
|
-
|
|
154
|
-
models.push(["review", reviewer]);
|
|
155
|
-
roles = [["planner", "default"], ["reviewer", "review"]];
|
|
156
|
-
}
|
|
157
|
-
const lines = ["schema_version: 1", "models:"];
|
|
181
|
+
const lines = ["schema_version: 2", "models:"];
|
|
158
182
|
for (const [name, choice] of models) {
|
|
159
183
|
lines.push(` ${name}:`, ` provider: ${yamlScalar(choice.provider)}`, ` model: ${yamlScalar(choice.model)}`);
|
|
160
184
|
const entries = Object.entries(choice.options);
|
|
@@ -171,7 +195,7 @@ function renderConfig(preset, primary, reviewer) {
|
|
|
171
195
|
}
|
|
172
196
|
function renderWorkflow(preset) {
|
|
173
197
|
if (preset === "implement") {
|
|
174
|
-
return ("schema_version:
|
|
198
|
+
return ("schema_version: 2\n" +
|
|
175
199
|
"id: implement\n" +
|
|
176
200
|
"stages:\n" +
|
|
177
201
|
" - id: implement\n" +
|
|
@@ -179,11 +203,9 @@ function renderWorkflow(preset) {
|
|
|
179
203
|
" depends_on: []\n" +
|
|
180
204
|
" instructions:\n" +
|
|
181
205
|
" - Implement the requested change.\n" +
|
|
182
|
-
" - Verify the result with relevant checks or tests.\n"
|
|
183
|
-
"completion:\n" +
|
|
184
|
-
" stage: implement\n");
|
|
206
|
+
" - Verify the result with relevant checks or tests.\n");
|
|
185
207
|
}
|
|
186
|
-
return ("schema_version:
|
|
208
|
+
return ("schema_version: 2\n" +
|
|
187
209
|
"id: plan\n" +
|
|
188
210
|
"stages:\n" +
|
|
189
211
|
" - id: plan\n" +
|
|
@@ -192,6 +214,7 @@ function renderWorkflow(preset) {
|
|
|
192
214
|
" max_revisions: 3\n" +
|
|
193
215
|
" instructions:\n" +
|
|
194
216
|
" - Analyze the task and write an implementation plan.\n" +
|
|
217
|
+
" - When revising, edit the existing plan.md in place. Do not replace the whole file.\n" +
|
|
195
218
|
" produces:\n" +
|
|
196
219
|
" artifact: plan.md\n" +
|
|
197
220
|
" - id: review-plan\n" +
|
|
@@ -200,6 +223,7 @@ function renderWorkflow(preset) {
|
|
|
200
223
|
" - plan\n" +
|
|
201
224
|
" instructions:\n" +
|
|
202
225
|
" - Review the plan for correctness, missing risks, and unnecessary scope.\n" +
|
|
226
|
+
" - Report every gap of the same kind in one response.\n" +
|
|
203
227
|
" decision:\n" +
|
|
204
228
|
" values:\n" +
|
|
205
229
|
" - approved\n" +
|
|
@@ -207,13 +231,10 @@ function renderWorkflow(preset) {
|
|
|
207
231
|
" routes:\n" +
|
|
208
232
|
" approved: complete\n" +
|
|
209
233
|
" revise: plan\n" +
|
|
210
|
-
"
|
|
211
|
-
"
|
|
212
|
-
"
|
|
213
|
-
"
|
|
214
|
-
" - name: plan\n" +
|
|
215
|
-
" from_stage: plan\n" +
|
|
216
|
-
" artifact: plan.md\n");
|
|
234
|
+
"outputs:\n" +
|
|
235
|
+
" - name: plan\n" +
|
|
236
|
+
" from_stage: plan\n" +
|
|
237
|
+
" artifact: plan.md\n");
|
|
217
238
|
}
|
|
218
239
|
async function promptOptions(adapter, model, catalog, which, runCommand, prompt) {
|
|
219
240
|
const stored = {};
|
|
@@ -235,28 +256,32 @@ function providerOptions(adapter) {
|
|
|
235
256
|
class ClackPrompter {
|
|
236
257
|
async selectProvider(rows, message) {
|
|
237
258
|
if (rows.length === 0)
|
|
238
|
-
throw new
|
|
259
|
+
throw new ConfigurationError("No providers are available.");
|
|
239
260
|
const names = new Set(rows.map(([name]) => name));
|
|
240
261
|
const result = await clackSelect({
|
|
241
262
|
message,
|
|
242
263
|
options: rows.map(([name, found]) => ({ value: name, label: `${name} ${found ?? "not found"}` })),
|
|
243
264
|
initialValue: rows[0]?.[0],
|
|
244
265
|
});
|
|
245
|
-
if (isCancel(result)
|
|
246
|
-
throw new
|
|
266
|
+
if (isCancel(result))
|
|
267
|
+
throw new PromptCancelled();
|
|
268
|
+
if (typeof result !== "string" || !names.has(result))
|
|
269
|
+
throw new ConfigurationError("Provider is required.");
|
|
247
270
|
return result;
|
|
248
271
|
}
|
|
249
272
|
async selectModel(_provider, catalog, message) {
|
|
250
273
|
if (catalog.ids.length === 0)
|
|
251
|
-
throw new
|
|
274
|
+
throw new ConfigurationError("Model is required.");
|
|
252
275
|
const initial = catalog.defaultId && catalog.ids.includes(catalog.defaultId) ? catalog.defaultId : catalog.ids[0];
|
|
253
276
|
const result = await clackSelect({
|
|
254
277
|
message,
|
|
255
278
|
options: catalog.ids.map((id) => ({ value: id, label: id })),
|
|
256
279
|
initialValue: initial,
|
|
257
280
|
});
|
|
258
|
-
if (isCancel(result)
|
|
259
|
-
throw new
|
|
281
|
+
if (isCancel(result))
|
|
282
|
+
throw new PromptCancelled();
|
|
283
|
+
if (!catalog.ids.includes(String(result)))
|
|
284
|
+
throw new ConfigurationError(`Unknown model: ${String(result)}.`);
|
|
260
285
|
return String(result);
|
|
261
286
|
}
|
|
262
287
|
async selectOption(name, values, allowDefault) {
|
|
@@ -272,40 +297,19 @@ class ClackPrompter {
|
|
|
272
297
|
initialValue: allowDefault ? "" : values[0],
|
|
273
298
|
});
|
|
274
299
|
const allowed = new Set(allowDefault ? ["", ...values] : values);
|
|
275
|
-
if (isCancel(result)
|
|
276
|
-
throw new
|
|
300
|
+
if (isCancel(result))
|
|
301
|
+
throw new PromptCancelled();
|
|
302
|
+
if (!allowed.has(String(result ?? ""))) {
|
|
303
|
+
throw new ConfigurationError(`Unknown ${name} value: ${String(result)}.`);
|
|
277
304
|
}
|
|
278
305
|
return result == null ? "" : String(result);
|
|
279
306
|
}
|
|
280
307
|
async confirm(message) {
|
|
281
308
|
const result = await clackConfirm({ message, initialValue: true });
|
|
282
|
-
if (isCancel(result)
|
|
283
|
-
throw new
|
|
309
|
+
if (isCancel(result))
|
|
310
|
+
throw new PromptCancelled();
|
|
311
|
+
if (typeof result !== "boolean")
|
|
312
|
+
throw new ConfigurationError("Answer yes or no.");
|
|
284
313
|
return result;
|
|
285
314
|
}
|
|
286
315
|
}
|
|
287
|
-
function exists(filePath) {
|
|
288
|
-
try {
|
|
289
|
-
statSync(filePath);
|
|
290
|
-
return true;
|
|
291
|
-
}
|
|
292
|
-
catch {
|
|
293
|
-
return false;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
function isFile(filePath) {
|
|
297
|
-
try {
|
|
298
|
-
return statSync(filePath).isFile();
|
|
299
|
-
}
|
|
300
|
-
catch {
|
|
301
|
-
return false;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
function isDirectory(filePath) {
|
|
305
|
-
try {
|
|
306
|
-
return statSync(filePath).isDirectory();
|
|
307
|
-
}
|
|
308
|
-
catch {
|
|
309
|
-
return false;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function prepareWorkflowSession(workspace: string, sessionId: string, workflowId: string, priorSessionId: string | null): number;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createPreparedSession, log, relativeTaskPath } from "@nodeint/migent-kernel";
|
|
2
|
+
import { requireCurrentSchema } from "../migrate.js";
|
|
3
|
+
export function prepareWorkflowSession(workspace, sessionId, workflowId, priorSessionId) {
|
|
4
|
+
requireCurrentSchema(workspace);
|
|
5
|
+
const id = createPreparedSession(workspace, sessionId, workflowId, priorSessionId);
|
|
6
|
+
const payload = {
|
|
7
|
+
session_id: id,
|
|
8
|
+
status: "prepared",
|
|
9
|
+
task_path: relativeTaskPath(id),
|
|
10
|
+
};
|
|
11
|
+
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
12
|
+
log(`session_id: ${id}`);
|
|
13
|
+
return 0;
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function repairWorkflowSession(workspace: string, sessionId: string, acceptTask: boolean): number;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { isSessionLockLost, repairSession, SessionLockHeldError } from "@nodeint/migent-kernel";
|
|
2
|
+
import { printError } from "../display.js";
|
|
3
|
+
import { requireCurrentSchema } from "../migrate.js";
|
|
4
|
+
export function repairWorkflowSession(workspace, sessionId, acceptTask) {
|
|
5
|
+
requireCurrentSchema(workspace);
|
|
6
|
+
try {
|
|
7
|
+
const status = repairSession(workspace, sessionId, { acceptTask });
|
|
8
|
+
const payload = {
|
|
9
|
+
session_id: sessionId,
|
|
10
|
+
status: typeof status.status === "string" ? status.status : null,
|
|
11
|
+
};
|
|
12
|
+
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
13
|
+
return 0;
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
if (error instanceof SessionLockHeldError || isSessionLockLost(error)) {
|
|
17
|
+
printError(error.message);
|
|
18
|
+
return 2;
|
|
19
|
+
}
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|
package/dist/commands/service.js
CHANGED
|
@@ -34,6 +34,7 @@ export async function runServiceStop() {
|
|
|
34
34
|
printError(result.message || "Registry service did not stop.");
|
|
35
35
|
return 1;
|
|
36
36
|
}
|
|
37
|
+
process.stderr.write(result.outcome === "absent" ? "Not running.\n" : "Stopped.\n");
|
|
37
38
|
return 0;
|
|
38
39
|
}
|
|
39
40
|
export async function runServiceStatus(asJson) {
|
|
@@ -48,13 +49,13 @@ export async function runServiceStatus(asJson) {
|
|
|
48
49
|
else {
|
|
49
50
|
process.stdout.write("stopped\n");
|
|
50
51
|
}
|
|
51
|
-
return 0;
|
|
52
|
+
return report.state === "running" ? 0 : 1;
|
|
52
53
|
}
|
|
53
54
|
export function parsePort(raw) {
|
|
54
55
|
if (!/^[0-9]+$/.test(raw))
|
|
55
|
-
throw new ConfigurationError("--port must be an integer.");
|
|
56
|
+
throw new ConfigurationError("--port must be an integer from 1 to 65535.");
|
|
56
57
|
const port = Number(raw);
|
|
57
|
-
if (port > 65535)
|
|
58
|
-
throw new ConfigurationError("--port must be an integer.");
|
|
58
|
+
if (port < 1 || port > 65535)
|
|
59
|
+
throw new ConfigurationError("--port must be an integer from 1 to 65535.");
|
|
59
60
|
return port;
|
|
60
61
|
}
|
|
@@ -1,3 +1,26 @@
|
|
|
1
|
-
export
|
|
1
|
+
export interface SessionListOptions {
|
|
2
2
|
asJson?: boolean;
|
|
3
|
+
workflow?: string | null;
|
|
4
|
+
status?: string | null;
|
|
5
|
+
limit?: number | null;
|
|
6
|
+
latest?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function listSessions(workspace: string, options?: SessionListOptions): number;
|
|
9
|
+
export declare function runSessionMetrics(workspace: string, session: string | undefined, options: {
|
|
10
|
+
asJson: boolean;
|
|
11
|
+
workflow: string | null;
|
|
12
|
+
status: string | null;
|
|
13
|
+
limit: string | null;
|
|
3
14
|
}): number;
|
|
15
|
+
export declare function runSessions(workspace: string, session: string | undefined, options: {
|
|
16
|
+
asJson: boolean;
|
|
17
|
+
follow: boolean;
|
|
18
|
+
execution: string | null;
|
|
19
|
+
until: boolean;
|
|
20
|
+
cursor: string | undefined;
|
|
21
|
+
exitWhenDone: boolean;
|
|
22
|
+
workflow: string | null;
|
|
23
|
+
status: string | null;
|
|
24
|
+
limit: string | null;
|
|
25
|
+
latest: boolean;
|
|
26
|
+
}): Promise<number>;
|
|
@@ -1,20 +1,172 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { listSessionMetrics, listSessionRows, sessionMetrics, ConfigurationError } from "@nodeint/migent-kernel";
|
|
4
|
+
import { printError, printSessions } from "../display.js";
|
|
5
|
+
import { adapterMap } from "../provider.js";
|
|
6
|
+
import { SESSION_COLUMN, sessionFields } from "../session-row.js";
|
|
7
|
+
import { optionalText } from "../text.js";
|
|
8
|
+
import { followSession } from "./watch.js";
|
|
9
|
+
const SESSION_STATUSES = new Set(["active", "prepared", "completed", "blocked", "cancelled", "corrupt"]);
|
|
3
10
|
export function listSessions(workspace, options = {}) {
|
|
4
|
-
const rows = listSessionRows(workspace);
|
|
11
|
+
const rows = selectSessionRows(listSessionRows(workspace), options);
|
|
12
|
+
if (options.latest) {
|
|
13
|
+
const row = rows[0];
|
|
14
|
+
if (options.asJson) {
|
|
15
|
+
process.stdout.write(`${JSON.stringify(row === undefined ? null : sessionJson(row))}\n`);
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
printSessions(row === undefined ? [] : [row]);
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
5
21
|
if (options.asJson) {
|
|
6
|
-
|
|
7
|
-
session: row[1] ?? "",
|
|
8
|
-
workflow: row[2] ?? "",
|
|
9
|
-
status: row[3] ?? "",
|
|
10
|
-
execution: row[4] ?? "",
|
|
11
|
-
decision: row[5] ?? "",
|
|
12
|
-
outputs: row[6] ?? "",
|
|
13
|
-
task: row[7] ?? "",
|
|
14
|
-
}));
|
|
15
|
-
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
22
|
+
process.stdout.write(`${JSON.stringify(rows.map((row) => sessionJson(row)))}\n`);
|
|
16
23
|
return 0;
|
|
17
24
|
}
|
|
18
25
|
printSessions(rows);
|
|
19
26
|
return 0;
|
|
20
27
|
}
|
|
28
|
+
export function runSessionMetrics(workspace, session, options) {
|
|
29
|
+
if (!options.asJson) {
|
|
30
|
+
printError("session metrics requires --json.");
|
|
31
|
+
return 2;
|
|
32
|
+
}
|
|
33
|
+
const selection = parseListSelection({ ...options, latest: false });
|
|
34
|
+
const sessionId = optionalText(session);
|
|
35
|
+
if (sessionId !== null && hasListFilter(selection)) {
|
|
36
|
+
printError("List filters cannot be combined with a session.");
|
|
37
|
+
return 2;
|
|
38
|
+
}
|
|
39
|
+
const adapters = adapterMap();
|
|
40
|
+
if (sessionId === null) {
|
|
41
|
+
const rows = listSessionMetrics(workspace, adapters, {
|
|
42
|
+
workflow: selection.workflow ?? null,
|
|
43
|
+
status: selection.status ?? null,
|
|
44
|
+
limit: selection.limit ?? null,
|
|
45
|
+
});
|
|
46
|
+
for (const row of rows)
|
|
47
|
+
process.stdout.write(`${JSON.stringify(row)}\n`);
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
const eventsPath = path.join(workspace, ".migent", "sessions", sessionId, "events.jsonl");
|
|
51
|
+
if (!existsSync(eventsPath)) {
|
|
52
|
+
printError(`No events found for session: ${sessionId}`);
|
|
53
|
+
return 2;
|
|
54
|
+
}
|
|
55
|
+
process.stdout.write(`${JSON.stringify(sessionMetrics(workspace, sessionId, adapters))}\n`);
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
export async function runSessions(workspace, session, options) {
|
|
59
|
+
const selection = parseListSelection(options);
|
|
60
|
+
const sessionId = optionalText(session);
|
|
61
|
+
if (sessionId !== null && hasListFilter(selection)) {
|
|
62
|
+
printError("List filters cannot be combined with a session.");
|
|
63
|
+
return 2;
|
|
64
|
+
}
|
|
65
|
+
if (sessionId === null)
|
|
66
|
+
return listOnly(workspace, options, selection);
|
|
67
|
+
if (options.until && !options.follow) {
|
|
68
|
+
printError("--until requires --follow.");
|
|
69
|
+
return 2;
|
|
70
|
+
}
|
|
71
|
+
if (options.cursor !== undefined && !options.follow) {
|
|
72
|
+
printError("--cursor requires --follow.");
|
|
73
|
+
return 2;
|
|
74
|
+
}
|
|
75
|
+
if (options.exitWhenDone && !options.follow) {
|
|
76
|
+
printError("--exit-when-done requires --follow.");
|
|
77
|
+
return 2;
|
|
78
|
+
}
|
|
79
|
+
if (options.until && options.execution === null) {
|
|
80
|
+
printError("--until requires --execution.");
|
|
81
|
+
return 2;
|
|
82
|
+
}
|
|
83
|
+
const cursor = parseCursor(options.cursor);
|
|
84
|
+
return followSession(workspace, sessionId, {
|
|
85
|
+
once: !options.follow,
|
|
86
|
+
executionId: options.execution,
|
|
87
|
+
untilTerminal: options.until,
|
|
88
|
+
jsonLines: options.asJson,
|
|
89
|
+
cursor,
|
|
90
|
+
exitWhenDone: options.exitWhenDone,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
function listOnly(workspace, options, selection) {
|
|
94
|
+
if (options.follow) {
|
|
95
|
+
printError("--follow requires a session.");
|
|
96
|
+
return 2;
|
|
97
|
+
}
|
|
98
|
+
if (options.until) {
|
|
99
|
+
printError("--until requires --follow.");
|
|
100
|
+
return 2;
|
|
101
|
+
}
|
|
102
|
+
if (options.cursor !== undefined) {
|
|
103
|
+
printError("--cursor requires --follow.");
|
|
104
|
+
return 2;
|
|
105
|
+
}
|
|
106
|
+
if (options.exitWhenDone) {
|
|
107
|
+
printError("--exit-when-done requires --follow.");
|
|
108
|
+
return 2;
|
|
109
|
+
}
|
|
110
|
+
if (options.execution !== null) {
|
|
111
|
+
printError("--execution requires a session.");
|
|
112
|
+
return 2;
|
|
113
|
+
}
|
|
114
|
+
return listSessions(workspace, { ...selection, asJson: options.asJson });
|
|
115
|
+
}
|
|
116
|
+
function selectSessionRows(rows, options) {
|
|
117
|
+
let selected = [...rows];
|
|
118
|
+
if (options.workflow != null) {
|
|
119
|
+
selected = selected.filter((row) => row[SESSION_COLUMN.workflow] === options.workflow);
|
|
120
|
+
}
|
|
121
|
+
if (options.status != null) {
|
|
122
|
+
selected = selected.filter((row) => row[SESSION_COLUMN.status] === options.status);
|
|
123
|
+
}
|
|
124
|
+
const limit = options.latest ? 1 : (options.limit ?? null);
|
|
125
|
+
if (limit !== null)
|
|
126
|
+
return selected.slice(0, limit);
|
|
127
|
+
return selected;
|
|
128
|
+
}
|
|
129
|
+
function sessionJson(row) {
|
|
130
|
+
const fields = sessionFields(row);
|
|
131
|
+
return {
|
|
132
|
+
session: fields.session,
|
|
133
|
+
workflow: fields.workflow,
|
|
134
|
+
status: fields.status,
|
|
135
|
+
execution: fields.execution,
|
|
136
|
+
decision: fields.decision,
|
|
137
|
+
outputs: fields.outputs,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function hasListFilter(selection) {
|
|
141
|
+
return selection.latest === true || selection.workflow != null || selection.status != null || selection.limit != null;
|
|
142
|
+
}
|
|
143
|
+
function parseListSelection(options) {
|
|
144
|
+
const workflow = optionalText(options.workflow);
|
|
145
|
+
if (options.workflow !== null && workflow === null)
|
|
146
|
+
throw new ConfigurationError("--workflow is empty.");
|
|
147
|
+
const status = optionalText(options.status);
|
|
148
|
+
if (options.status !== null && status === null)
|
|
149
|
+
throw new ConfigurationError("--status is empty.");
|
|
150
|
+
if (status !== null && !SESSION_STATUSES.has(status)) {
|
|
151
|
+
throw new ConfigurationError("--status must be active, prepared, completed, blocked, cancelled, or corrupt.");
|
|
152
|
+
}
|
|
153
|
+
let limit = null;
|
|
154
|
+
if (options.limit !== null) {
|
|
155
|
+
if (!/^[0-9]+$/.test(options.limit))
|
|
156
|
+
throw new ConfigurationError("--limit must be an integer >= 1.");
|
|
157
|
+
limit = Number(options.limit);
|
|
158
|
+
if (limit < 1)
|
|
159
|
+
throw new ConfigurationError("--limit must be an integer >= 1.");
|
|
160
|
+
}
|
|
161
|
+
if (options.latest && limit !== null)
|
|
162
|
+
throw new ConfigurationError("--latest cannot be combined with --limit.");
|
|
163
|
+
return { workflow, status, limit, latest: options.latest };
|
|
164
|
+
}
|
|
165
|
+
function parseCursor(raw) {
|
|
166
|
+
if (raw === undefined)
|
|
167
|
+
return null;
|
|
168
|
+
const cursor = Number(raw);
|
|
169
|
+
if (!Number.isInteger(cursor) || cursor < 0)
|
|
170
|
+
throw new ConfigurationError("--cursor must be >= 0.");
|
|
171
|
+
return cursor;
|
|
172
|
+
}
|