@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,39 @@
|
|
|
1
|
+
import { AgentToolRunner, DEFAULT_MAX_DISPATCHES, executeNamedWorkflow, } from "@nodeint/migent-kernel";
|
|
2
|
+
import { cliHooks } from "../hooks.js";
|
|
3
|
+
import { requireCurrentSchema } from "../migrate.js";
|
|
4
|
+
import { adapterMap } from "../provider.js";
|
|
5
|
+
export async function executeWorkflow(workspace, options) {
|
|
6
|
+
requireCurrentSchema(workspace);
|
|
7
|
+
const once = options.action === "stage";
|
|
8
|
+
const creating = options.action === "start" || (once && text(options.sessionId) === null);
|
|
9
|
+
const maxAttempts = options.maxAttempts === undefined ? 3 : options.maxAttempts;
|
|
10
|
+
const maxDispatches = options.maxDispatches === undefined ? DEFAULT_MAX_DISPATCHES : options.maxDispatches;
|
|
11
|
+
const runner = options.runner ?? cliHooks.runner ?? new AgentToolRunner(adapterMap());
|
|
12
|
+
const result = await executeNamedWorkflow(workspace, {
|
|
13
|
+
workflowId: creating ? (options.workflowId ?? null) : null,
|
|
14
|
+
task: creating ? (options.task ?? null) : null,
|
|
15
|
+
sessionId: creating ? null : (options.sessionId ?? null),
|
|
16
|
+
priorSessionId: options.priorSessionId ?? null,
|
|
17
|
+
maxAttempts: once ? null : maxAttempts,
|
|
18
|
+
maxDispatches: once ? null : maxDispatches,
|
|
19
|
+
once,
|
|
20
|
+
resetRevisions: options.action === "continue",
|
|
21
|
+
}, runner);
|
|
22
|
+
printResult(result);
|
|
23
|
+
return result.code;
|
|
24
|
+
}
|
|
25
|
+
function printResult(result) {
|
|
26
|
+
process.stdout.write(`${JSON.stringify({
|
|
27
|
+
session_id: result.sessionId,
|
|
28
|
+
session_status: result.status,
|
|
29
|
+
stop_reason: result.stopReason,
|
|
30
|
+
outputs: result.outputs,
|
|
31
|
+
stages: result.stages,
|
|
32
|
+
})}\n`);
|
|
33
|
+
}
|
|
34
|
+
function text(value) {
|
|
35
|
+
if (typeof value !== "string")
|
|
36
|
+
return null;
|
|
37
|
+
const stripped = value.trim();
|
|
38
|
+
return stripped === "" ? null : stripped;
|
|
39
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ModelCatalog } from "@nodeint/migent-adapters";
|
|
2
|
+
import { type AgentAdapter } from "@nodeint/migent-kernel";
|
|
3
|
+
import { type RunCommand, type Which } from "../provider.js";
|
|
4
|
+
export declare const PRESETS: readonly ["implement", "plan"];
|
|
5
|
+
export interface ModelChoice {
|
|
6
|
+
provider: string;
|
|
7
|
+
model: string;
|
|
8
|
+
options: Record<string, string>;
|
|
9
|
+
}
|
|
10
|
+
export interface Prompter {
|
|
11
|
+
selectProvider(rows: ReadonlyArray<readonly [string, string | null, AgentAdapter]>, message: string): string | Promise<string>;
|
|
12
|
+
selectModel(provider: string, catalog: ModelCatalog, message: string): string | Promise<string>;
|
|
13
|
+
selectOption(name: string, values: readonly string[], allowDefault: boolean): string | Promise<string>;
|
|
14
|
+
confirm(message: string): boolean | Promise<boolean>;
|
|
15
|
+
}
|
|
16
|
+
export declare function runInit(workspace: string, options?: {
|
|
17
|
+
provider?: string | null;
|
|
18
|
+
model?: string | null;
|
|
19
|
+
options?: readonly string[];
|
|
20
|
+
preset?: string;
|
|
21
|
+
interactive?: boolean;
|
|
22
|
+
adapters?: Record<string, AgentAdapter> | null;
|
|
23
|
+
which?: Which | null;
|
|
24
|
+
prompter?: Prompter | null;
|
|
25
|
+
runCommand?: RunCommand | null;
|
|
26
|
+
}): Promise<number>;
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { mkdirSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { confirm as clackConfirm, isCancel, select as clackSelect } from "@clack/prompts";
|
|
4
|
+
import { diagnose, whichSync, yamlScalar } from "@nodeint/migent-kernel";
|
|
5
|
+
import { printAlreadyInitialized, printCreated, printError, printInitConflicts, } from "../display.js";
|
|
6
|
+
import { cliHooks } from "../hooks.js";
|
|
7
|
+
import { runDoctor } from "./doctor.js";
|
|
8
|
+
import { acceptedOptions, adapterMap, asCatalog, detected, knownModel, loadCatalog, loadOptionValues, parseOptions, requireAdapter, runProviderCommand, } from "../provider.js";
|
|
9
|
+
export const PRESETS = ["implement", "plan"];
|
|
10
|
+
export async function runInit(workspace, options = {}) {
|
|
11
|
+
const provider = options.provider ?? null;
|
|
12
|
+
const model = options.model ?? null;
|
|
13
|
+
const preset = options.preset ?? "implement";
|
|
14
|
+
const interactive = options.interactive ?? false;
|
|
15
|
+
const chosen = options.adapters ?? adapterMap();
|
|
16
|
+
const resolve = options.which ?? cliHooks.which ?? whichSync;
|
|
17
|
+
const prompt = options.prompter ?? new ClackPrompter();
|
|
18
|
+
const execute = options.runCommand ?? runProviderCommand;
|
|
19
|
+
if (!PRESETS.includes(preset)) {
|
|
20
|
+
printError("preset must be implement or plan.");
|
|
21
|
+
return 2;
|
|
22
|
+
}
|
|
23
|
+
if ((provider === null) !== (model === null)) {
|
|
24
|
+
printError("Pass both --provider and --model, or omit both.");
|
|
25
|
+
return 2;
|
|
26
|
+
}
|
|
27
|
+
const optionPairs = options.options ?? [];
|
|
28
|
+
if (optionPairs.length > 0 && provider === null) {
|
|
29
|
+
printError("Pass --provider and --model with --option.");
|
|
30
|
+
return 2;
|
|
31
|
+
}
|
|
32
|
+
if (provider === null && !interactive) {
|
|
33
|
+
printError("Pass --provider and --model when init is not interactive.");
|
|
34
|
+
return 2;
|
|
35
|
+
}
|
|
36
|
+
const classified = classify(workspace, chosen, resolve);
|
|
37
|
+
if (classified.state === "ready") {
|
|
38
|
+
printAlreadyInitialized();
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
if (classified.state === "conflict") {
|
|
42
|
+
printInitConflicts(classified.conflicts);
|
|
43
|
+
return 1;
|
|
44
|
+
}
|
|
45
|
+
let primary;
|
|
46
|
+
let reviewer;
|
|
47
|
+
try {
|
|
48
|
+
[primary, reviewer] = await choices(chosen, resolve, prompt, {
|
|
49
|
+
provider,
|
|
50
|
+
model,
|
|
51
|
+
options: parseOptions(optionPairs),
|
|
52
|
+
preset,
|
|
53
|
+
interactive,
|
|
54
|
+
runCommand: execute,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
printError(error instanceof Error ? error.message : String(error));
|
|
59
|
+
return 2;
|
|
60
|
+
}
|
|
61
|
+
const paths = writeProject(workspace, preset, primary, reviewer);
|
|
62
|
+
printCreated(paths, `migent start ${preset} --task "..."`);
|
|
63
|
+
return runDoctor(workspace, { which: resolve });
|
|
64
|
+
}
|
|
65
|
+
async function choices(adapters, which, prompt, args) {
|
|
66
|
+
let provider = args.provider;
|
|
67
|
+
let model = args.model;
|
|
68
|
+
let stored;
|
|
69
|
+
if (provider === null) {
|
|
70
|
+
const rows = detected(adapters, which);
|
|
71
|
+
provider = await prompt.selectProvider(rows, "Default provider");
|
|
72
|
+
const adapter = adapters[provider];
|
|
73
|
+
if (adapter === undefined)
|
|
74
|
+
throw new Error(`Unknown provider: ${provider}.`);
|
|
75
|
+
const catalog = await loadCatalog(adapter, which, args.runCommand);
|
|
76
|
+
model = await prompt.selectModel(provider, catalog, "Model");
|
|
77
|
+
stored = await promptOptions(adapter, model, catalog, which, args.runCommand, prompt);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
const adapter = requireAdapter(adapters, provider);
|
|
81
|
+
if (model === null)
|
|
82
|
+
throw new Error("Model is required.");
|
|
83
|
+
const catalog = await loadCatalog(adapter, which, args.runCommand);
|
|
84
|
+
model = knownModel(model, catalog);
|
|
85
|
+
stored = await acceptedOptions(adapter, model, catalog, which, args.runCommand, args.options);
|
|
86
|
+
}
|
|
87
|
+
if (model.trim() === "")
|
|
88
|
+
throw new Error("Model is required.");
|
|
89
|
+
const primary = { provider, model: model.trim(), options: stored };
|
|
90
|
+
if (args.preset !== "plan" || !args.interactive)
|
|
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 }];
|
|
103
|
+
}
|
|
104
|
+
function classify(workspace, adapters, which) {
|
|
105
|
+
const root = path.join(workspace, ".migent");
|
|
106
|
+
const config = path.join(root, "config.yaml");
|
|
107
|
+
const workflows = path.join(root, "workflows");
|
|
108
|
+
const gitignore = path.join(root, ".gitignore");
|
|
109
|
+
const owned = [];
|
|
110
|
+
if (exists(config))
|
|
111
|
+
owned.push(".migent/config.yaml");
|
|
112
|
+
if (exists(workflows)) {
|
|
113
|
+
owned.push(".migent/workflows");
|
|
114
|
+
if (isDirectory(workflows)) {
|
|
115
|
+
for (const name of readdirSync(workflows).filter((item) => item.endsWith(".yaml")).sort()) {
|
|
116
|
+
if (isFile(path.join(workflows, name)))
|
|
117
|
+
owned.push(`.migent/workflows/${name}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (exists(gitignore))
|
|
122
|
+
owned.push(".migent/.gitignore");
|
|
123
|
+
if (owned.length === 0)
|
|
124
|
+
return { state: "create", conflicts: [] };
|
|
125
|
+
const yamlFiles = isDirectory(workflows)
|
|
126
|
+
? readdirSync(workflows).filter((name) => name.endsWith(".yaml") && isFile(path.join(workflows, name)))
|
|
127
|
+
: [];
|
|
128
|
+
if (isFile(config) && yamlFiles.length > 0) {
|
|
129
|
+
const checks = diagnose(workspace, { adapters, which });
|
|
130
|
+
const byName = new Map(checks.map((check) => [check.name, check]));
|
|
131
|
+
if (byName.get("config")?.ok && byName.get("workflows")?.ok)
|
|
132
|
+
return { state: "ready", conflicts: [] };
|
|
133
|
+
}
|
|
134
|
+
return { state: "conflict", conflicts: owned };
|
|
135
|
+
}
|
|
136
|
+
function writeProject(workspace, preset, primary, reviewer) {
|
|
137
|
+
const root = path.join(workspace, ".migent");
|
|
138
|
+
const workflows = path.join(root, "workflows");
|
|
139
|
+
mkdirSync(workflows, { recursive: true });
|
|
140
|
+
writeFileSync(path.join(root, "config.yaml"), renderConfig(preset, primary, reviewer), "utf8");
|
|
141
|
+
const workflowName = `${preset}.yaml`;
|
|
142
|
+
writeFileSync(path.join(workflows, workflowName), renderWorkflow(preset), "utf8");
|
|
143
|
+
writeFileSync(path.join(root, ".gitignore"), "sessions/\n", "utf8");
|
|
144
|
+
return [".migent/config.yaml", `.migent/workflows/${workflowName}`, ".migent/.gitignore"];
|
|
145
|
+
}
|
|
146
|
+
function renderConfig(preset, primary, reviewer) {
|
|
147
|
+
const models = [["default", primary]];
|
|
148
|
+
let roles;
|
|
149
|
+
if (preset === "implement")
|
|
150
|
+
roles = [["developer", "default"]];
|
|
151
|
+
else if (reviewer === null)
|
|
152
|
+
roles = [["planner", "default"], ["reviewer", "default"]];
|
|
153
|
+
else {
|
|
154
|
+
models.push(["review", reviewer]);
|
|
155
|
+
roles = [["planner", "default"], ["reviewer", "review"]];
|
|
156
|
+
}
|
|
157
|
+
const lines = ["schema_version: 1", "models:"];
|
|
158
|
+
for (const [name, choice] of models) {
|
|
159
|
+
lines.push(` ${name}:`, ` provider: ${yamlScalar(choice.provider)}`, ` model: ${yamlScalar(choice.model)}`);
|
|
160
|
+
const entries = Object.entries(choice.options);
|
|
161
|
+
if (entries.length > 0) {
|
|
162
|
+
lines.push(" options:");
|
|
163
|
+
for (const [optionName, value] of entries)
|
|
164
|
+
lines.push(` ${optionName}: ${yamlScalar(value)}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
lines.push("roles:");
|
|
168
|
+
for (const [role, modelName] of roles)
|
|
169
|
+
lines.push(` ${role}:`, ` default_model: ${modelName}`);
|
|
170
|
+
return `${lines.join("\n")}\n`;
|
|
171
|
+
}
|
|
172
|
+
function renderWorkflow(preset) {
|
|
173
|
+
if (preset === "implement") {
|
|
174
|
+
return ("schema_version: 1\n" +
|
|
175
|
+
"id: implement\n" +
|
|
176
|
+
"stages:\n" +
|
|
177
|
+
" - id: implement\n" +
|
|
178
|
+
" role: developer\n" +
|
|
179
|
+
" depends_on: []\n" +
|
|
180
|
+
" instructions:\n" +
|
|
181
|
+
" - Implement the requested change.\n" +
|
|
182
|
+
" - Verify the result with relevant checks or tests.\n" +
|
|
183
|
+
"completion:\n" +
|
|
184
|
+
" stage: implement\n");
|
|
185
|
+
}
|
|
186
|
+
return ("schema_version: 1\n" +
|
|
187
|
+
"id: plan\n" +
|
|
188
|
+
"stages:\n" +
|
|
189
|
+
" - id: plan\n" +
|
|
190
|
+
" role: planner\n" +
|
|
191
|
+
" depends_on: []\n" +
|
|
192
|
+
" max_revisions: 3\n" +
|
|
193
|
+
" instructions:\n" +
|
|
194
|
+
" - Analyze the task and write an implementation plan.\n" +
|
|
195
|
+
" produces:\n" +
|
|
196
|
+
" artifact: plan.md\n" +
|
|
197
|
+
" - id: review-plan\n" +
|
|
198
|
+
" role: reviewer\n" +
|
|
199
|
+
" depends_on:\n" +
|
|
200
|
+
" - plan\n" +
|
|
201
|
+
" instructions:\n" +
|
|
202
|
+
" - Review the plan for correctness, missing risks, and unnecessary scope.\n" +
|
|
203
|
+
" decision:\n" +
|
|
204
|
+
" values:\n" +
|
|
205
|
+
" - approved\n" +
|
|
206
|
+
" - revise\n" +
|
|
207
|
+
" routes:\n" +
|
|
208
|
+
" approved: complete\n" +
|
|
209
|
+
" revise: plan\n" +
|
|
210
|
+
"completion:\n" +
|
|
211
|
+
" stage: review-plan\n" +
|
|
212
|
+
" decision: approved\n" +
|
|
213
|
+
" outputs:\n" +
|
|
214
|
+
" - name: plan\n" +
|
|
215
|
+
" from_stage: plan\n" +
|
|
216
|
+
" artifact: plan.md\n");
|
|
217
|
+
}
|
|
218
|
+
async function promptOptions(adapter, model, catalog, which, runCommand, prompt) {
|
|
219
|
+
const stored = {};
|
|
220
|
+
for (const spec of providerOptions(adapter)) {
|
|
221
|
+
if (!spec.prompt)
|
|
222
|
+
continue;
|
|
223
|
+
const values = await loadOptionValues(adapter, spec.name, model, catalog, which, runCommand);
|
|
224
|
+
if (values === null || values.length === 0)
|
|
225
|
+
continue;
|
|
226
|
+
const picked = await prompt.selectOption(spec.name, values, spec.allowDefault ?? false);
|
|
227
|
+
if (picked)
|
|
228
|
+
stored[spec.name] = picked;
|
|
229
|
+
}
|
|
230
|
+
return stored;
|
|
231
|
+
}
|
|
232
|
+
function providerOptions(adapter) {
|
|
233
|
+
return asCatalog(adapter).providerOptions();
|
|
234
|
+
}
|
|
235
|
+
class ClackPrompter {
|
|
236
|
+
async selectProvider(rows, message) {
|
|
237
|
+
if (rows.length === 0)
|
|
238
|
+
throw new Error("No providers are available.");
|
|
239
|
+
const names = new Set(rows.map(([name]) => name));
|
|
240
|
+
const result = await clackSelect({
|
|
241
|
+
message,
|
|
242
|
+
options: rows.map(([name, found]) => ({ value: name, label: `${name} ${found ?? "not found"}` })),
|
|
243
|
+
initialValue: rows[0]?.[0],
|
|
244
|
+
});
|
|
245
|
+
if (isCancel(result) || typeof result !== "string" || !names.has(result))
|
|
246
|
+
throw new Error("Provider is required.");
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
async selectModel(_provider, catalog, message) {
|
|
250
|
+
if (catalog.ids.length === 0)
|
|
251
|
+
throw new Error("Model is required.");
|
|
252
|
+
const initial = catalog.defaultId && catalog.ids.includes(catalog.defaultId) ? catalog.defaultId : catalog.ids[0];
|
|
253
|
+
const result = await clackSelect({
|
|
254
|
+
message,
|
|
255
|
+
options: catalog.ids.map((id) => ({ value: id, label: id })),
|
|
256
|
+
initialValue: initial,
|
|
257
|
+
});
|
|
258
|
+
if (isCancel(result) || !catalog.ids.includes(String(result)))
|
|
259
|
+
throw new Error(`Unknown model: ${String(result)}.`);
|
|
260
|
+
return String(result);
|
|
261
|
+
}
|
|
262
|
+
async selectOption(name, values, allowDefault) {
|
|
263
|
+
if (values.length === 0)
|
|
264
|
+
return "";
|
|
265
|
+
const options = [
|
|
266
|
+
...(allowDefault ? [{ value: "", label: "provider default" }] : []),
|
|
267
|
+
...values.map((value) => ({ value, label: value })),
|
|
268
|
+
];
|
|
269
|
+
const result = await clackSelect({
|
|
270
|
+
message: name,
|
|
271
|
+
options,
|
|
272
|
+
initialValue: allowDefault ? "" : values[0],
|
|
273
|
+
});
|
|
274
|
+
const allowed = new Set(allowDefault ? ["", ...values] : values);
|
|
275
|
+
if (isCancel(result) || !allowed.has(String(result ?? ""))) {
|
|
276
|
+
throw new Error(`Unknown ${name} value: ${String(result)}.`);
|
|
277
|
+
}
|
|
278
|
+
return result == null ? "" : String(result);
|
|
279
|
+
}
|
|
280
|
+
async confirm(message) {
|
|
281
|
+
const result = await clackConfirm({ message, initialValue: true });
|
|
282
|
+
if (isCancel(result) || typeof result !== "boolean")
|
|
283
|
+
throw new Error("Answer yes or no.");
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
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,11 @@
|
|
|
1
|
+
import { serveForeground, serviceStatus, startDetached, stop } from "@nodeint/migent-registry";
|
|
2
|
+
export declare const serviceApi: {
|
|
3
|
+
serveForeground: typeof serveForeground;
|
|
4
|
+
serviceStatus: typeof serviceStatus;
|
|
5
|
+
startDetached: typeof startDetached;
|
|
6
|
+
stop: typeof stop;
|
|
7
|
+
};
|
|
8
|
+
export declare function runServiceStart(port: number | null, foreground: boolean): Promise<number>;
|
|
9
|
+
export declare function runServiceStop(): Promise<number>;
|
|
10
|
+
export declare function runServiceStatus(asJson: boolean): Promise<number>;
|
|
11
|
+
export declare function parsePort(raw: string): number;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ConfigurationError } from "@nodeint/migent-kernel";
|
|
2
|
+
import { ServiceError, ensureRegistryHome, registryHomePath, serveForeground, serviceStatus, startDetached, stop, } from "@nodeint/migent-registry";
|
|
3
|
+
import { printError } from "../display.js";
|
|
4
|
+
const DEFAULT_PORT = 47321;
|
|
5
|
+
export const serviceApi = {
|
|
6
|
+
serveForeground,
|
|
7
|
+
serviceStatus,
|
|
8
|
+
startDetached,
|
|
9
|
+
stop,
|
|
10
|
+
};
|
|
11
|
+
export async function runServiceStart(port, foreground) {
|
|
12
|
+
const home = ensureRegistryHome();
|
|
13
|
+
const chosen = port ?? DEFAULT_PORT;
|
|
14
|
+
try {
|
|
15
|
+
if (foreground) {
|
|
16
|
+
await serviceApi.serveForeground(home, chosen);
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
const endpoint = await serviceApi.startDetached(home, chosen);
|
|
20
|
+
process.stdout.write(`${endpoint.host}:${endpoint.port}\n`);
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error instanceof ServiceError) {
|
|
25
|
+
printError(error.message);
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function runServiceStop() {
|
|
32
|
+
const result = await serviceApi.stop(ensureRegistryHome());
|
|
33
|
+
if (result.outcome === "error") {
|
|
34
|
+
printError(result.message || "Registry service did not stop.");
|
|
35
|
+
return 1;
|
|
36
|
+
}
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
export async function runServiceStatus(asJson) {
|
|
40
|
+
const report = await serviceApi.serviceStatus(registryHomePath());
|
|
41
|
+
if (asJson) {
|
|
42
|
+
process.stdout.write(`${JSON.stringify(report)}\n`);
|
|
43
|
+
}
|
|
44
|
+
else if (report.state === "running") {
|
|
45
|
+
process.stdout.write("running\n");
|
|
46
|
+
process.stdout.write(`${String(report.host)}:${String(report.port)} pid=${String(report.pid)}\n`);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
process.stdout.write("stopped\n");
|
|
50
|
+
}
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
export function parsePort(raw) {
|
|
54
|
+
if (!/^[0-9]+$/.test(raw))
|
|
55
|
+
throw new ConfigurationError("--port must be an integer.");
|
|
56
|
+
const port = Number(raw);
|
|
57
|
+
if (port > 65535)
|
|
58
|
+
throw new ConfigurationError("--port must be an integer.");
|
|
59
|
+
return port;
|
|
60
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { listSessionRows } from "@nodeint/migent-kernel";
|
|
2
|
+
import { printSessions } from "../display.js";
|
|
3
|
+
export function listSessions(workspace, options = {}) {
|
|
4
|
+
const rows = listSessionRows(workspace);
|
|
5
|
+
if (options.asJson) {
|
|
6
|
+
const payload = rows.map((row) => ({
|
|
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`);
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
printSessions(rows);
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { printError } from "../display.js";
|
|
6
|
+
const SKILL_RELATIVE = path.join(".agents", "skills", "migent");
|
|
7
|
+
const STATE_FILENAME = ".migent-template.json";
|
|
8
|
+
const TEMPLATE_VERSION = 1;
|
|
9
|
+
export function installSkill(workspace) {
|
|
10
|
+
const skillDirectory = path.join(workspace, SKILL_RELATIVE);
|
|
11
|
+
if (existsSync(skillDirectory)) {
|
|
12
|
+
printError(`${displayPath(skillDirectory, workspace)} already exists; use \`migent skill update\`.`);
|
|
13
|
+
return 1;
|
|
14
|
+
}
|
|
15
|
+
const content = templateContent();
|
|
16
|
+
mkdirSync(skillDirectory, { recursive: true });
|
|
17
|
+
writeManagedSkill(skillDirectory, content);
|
|
18
|
+
process.stdout.write(`Installed ${displayPath(skillDirectory, workspace)}\n`);
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
export function updateSkill(workspace, options = {}) {
|
|
22
|
+
const skillDirectory = path.join(workspace, SKILL_RELATIVE);
|
|
23
|
+
const skillPath = path.join(skillDirectory, "SKILL.md");
|
|
24
|
+
const statePath = path.join(skillDirectory, STATE_FILENAME);
|
|
25
|
+
if (!isDirectory(skillDirectory) || !isFile(skillPath)) {
|
|
26
|
+
printError("Migent skill is not installed; run `migent skill install`.");
|
|
27
|
+
return 1;
|
|
28
|
+
}
|
|
29
|
+
const state = readState(statePath);
|
|
30
|
+
if (state === null) {
|
|
31
|
+
printError("Existing Migent skill is not managed by Migent; move it aside and run `migent skill install`.");
|
|
32
|
+
return 1;
|
|
33
|
+
}
|
|
34
|
+
const current = readFileSync(skillPath, "utf8");
|
|
35
|
+
const installedDigest = state.content_sha256;
|
|
36
|
+
const currentDigest = digest(current);
|
|
37
|
+
if (installedDigest !== currentDigest && !options.force) {
|
|
38
|
+
printError("Migent skill has local changes; rerun with `migent skill update --force` to replace them.");
|
|
39
|
+
return 1;
|
|
40
|
+
}
|
|
41
|
+
const content = templateContent();
|
|
42
|
+
if (current === content && installedDigest === currentDigest && state.template_version === TEMPLATE_VERSION) {
|
|
43
|
+
process.stdout.write(`Already up to date: ${displayPath(skillDirectory, workspace)}\n`);
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
writeManagedSkill(skillDirectory, content);
|
|
47
|
+
process.stdout.write(`Updated ${displayPath(skillDirectory, workspace)}\n`);
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
function templateContent() {
|
|
51
|
+
const nextToModule = fileURLToPath(new URL("../templates/migent/SKILL.md", import.meta.url));
|
|
52
|
+
return readFileSync(nextToModule, "utf8");
|
|
53
|
+
}
|
|
54
|
+
function writeManagedSkill(skillDirectory, content) {
|
|
55
|
+
writeFileSync(path.join(skillDirectory, "SKILL.md"), content, "utf8");
|
|
56
|
+
const state = {
|
|
57
|
+
schema_version: 1,
|
|
58
|
+
template_version: TEMPLATE_VERSION,
|
|
59
|
+
content_sha256: digest(content),
|
|
60
|
+
};
|
|
61
|
+
writeFileSync(path.join(skillDirectory, STATE_FILENAME), `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
62
|
+
}
|
|
63
|
+
function readState(filePath) {
|
|
64
|
+
try {
|
|
65
|
+
const payload = JSON.parse(readFileSync(filePath, "utf8"));
|
|
66
|
+
if (!isRecord(payload) || payload.schema_version !== 1)
|
|
67
|
+
return null;
|
|
68
|
+
return payload;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function digest(content) {
|
|
75
|
+
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
76
|
+
}
|
|
77
|
+
function displayPath(filePath, workspace) {
|
|
78
|
+
return path.relative(workspace, filePath).split(path.sep).join("/");
|
|
79
|
+
}
|
|
80
|
+
function isRecord(value) {
|
|
81
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
82
|
+
}
|
|
83
|
+
function isFile(filePath) {
|
|
84
|
+
try {
|
|
85
|
+
return statSync(filePath).isFile();
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function isDirectory(filePath) {
|
|
92
|
+
try {
|
|
93
|
+
return statSync(filePath).isDirectory();
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|