nylorun 0.18.3-beta → 0.20.0-beta
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/CHANGELOG.md +75 -0
- package/README.md +171 -7
- package/dist/cli.js +34 -3
- package/dist/client/agents.d.ts +18 -0
- package/dist/client/agents.js +2 -2
- package/dist/client/apply-agent.d.ts +14 -0
- package/dist/client/apply-agent.js +71 -0
- package/dist/client/apply.d.ts +9 -2
- package/dist/client/apply.js +125 -31
- package/dist/client/chat.d.ts +8 -0
- package/dist/client/chat.js +145 -0
- package/dist/client/convert.d.ts +18 -0
- package/dist/client/convert.js +195 -0
- package/dist/client/folder.d.ts +13 -0
- package/dist/client/folder.js +177 -0
- package/dist/client/init.d.ts +21 -0
- package/dist/client/init.js +210 -0
- package/dist/client/migrate.js +8 -2
- package/dist/client/plan-diff.d.ts +14 -0
- package/dist/client/plan-diff.js +86 -0
- package/dist/client/watch.d.ts +23 -0
- package/dist/client/watch.js +87 -0
- package/dist/nylo.js +2 -2
- package/dist/stack/versions.d.ts +2 -0
- package/dist/stack/versions.js +12 -0
- package/package.json +6 -6
package/dist/client/apply.js
CHANGED
|
@@ -5,27 +5,47 @@
|
|
|
5
5
|
* files they name and checks the harness file's `requires`. Each manifest is planned first
|
|
6
6
|
* (a dry run: from → to, each change, warnings, errors), and saved under `If-Match` set to the
|
|
7
7
|
* version the plan was made against, so a save that raced another fails (`412 manifest_changed`)
|
|
8
|
-
* instead of overwriting it. A folder
|
|
9
|
-
* that has one (`nylorun apply agents/`).
|
|
8
|
+
* instead of overwriting it. A folder with neither an `AGENT.md` nor an `agent.manifest.json`
|
|
9
|
+
* applies each subfolder that has one (`nylorun apply agents/`).
|
|
10
|
+
*
|
|
11
|
+
* An agent folder (a folder with `AGENT.md`, track T6) is compiled offline first, and one that does
|
|
12
|
+
* not compile sends nothing; otherwise its files go up by hash and the Runtime compiles it again
|
|
13
|
+
* and keeps it as the version's source. Its plan adds the compiler's notes, and `--dry-run` prints
|
|
14
|
+
* the compiled manifest.
|
|
15
|
+
*
|
|
16
|
+
* Track T7: a plan's change to the instructions prints their text diff (`plan-diff.ts`);
|
|
17
|
+
* `--agent [<id>]` makes or moves the Agent after a save (`apply-agent.ts`); `--watch` checks and
|
|
18
|
+
* plans again on each change, saving only with `--save` (`watch.ts`).
|
|
10
19
|
*/
|
|
11
20
|
import { resolve } from "node:path";
|
|
12
|
-
import { ComposeError, PluginError, SkillsError, composeManifest, manifestFolders, } from "@nylorun/agents";
|
|
21
|
+
import { ComposeError, PluginError, SkillsError, checkFolder, composeManifest, isAgentFolder, manifestFolders, } from "@nylorun/agents";
|
|
13
22
|
import { CliError } from "../errors.js";
|
|
23
|
+
import { putAgent, takeAgentFlag } from "./apply-agent.js";
|
|
24
|
+
import { agentFolders, diagnosticLines, packed } from "./folder.js";
|
|
25
|
+
import { changeDiffLines } from "./plan-diff.js";
|
|
14
26
|
import { integerFlag, parseFlags, runtimeCall, runtimeClient, runtimeFlagsUsage, usageError } from "./runtime.js";
|
|
15
|
-
export const applyUsage = `nylorun apply [<dir>...] [--dry-run] [--if-match <n>]
|
|
27
|
+
export const applyUsage = `nylorun apply [<dir>...] [--dry-run] [--if-match <n>] [--agent [<id>]] [--watch [--save]]
|
|
16
28
|
|
|
17
29
|
Saves the agent manifest each folder describes (default .), through the Runtime API with an
|
|
18
|
-
application key (or NYLORUN_TOKEN).
|
|
19
|
-
|
|
20
|
-
|
|
30
|
+
application key (or NYLORUN_TOKEN). An agent folder holds AGENT.md (nylorun init): it is checked
|
|
31
|
+
offline as nylorun check does, sent by hash, and kept as the version's source. A folder may hold
|
|
32
|
+
agent.manifest.json instead, which names its harness.manifest.json and runtime.manifest.json
|
|
33
|
+
(paths relative to the file that names them). A folder with neither applies each subfolder that
|
|
34
|
+
has one.
|
|
21
35
|
|
|
22
36
|
Each manifest is planned first: the version it moves from and to, each change (section, path,
|
|
23
37
|
op), warnings and errors. With no errors it is saved, its skills', plugins' and runtime files
|
|
24
38
|
uploaded first, under If-Match set to the version planned against (0 for a new manifest).
|
|
25
39
|
Errors, the gaps between its sections included, save nothing and exit 1.
|
|
26
40
|
|
|
27
|
-
--dry-run print the plans and
|
|
41
|
+
--dry-run print the plans, and an agent folder's compiled manifest, and
|
|
42
|
+
save nothing
|
|
28
43
|
--if-match <n> save only over version n (one folder)
|
|
44
|
+
--agent [<id>] then make the Agent <id> (default the manifest's name) from
|
|
45
|
+
the manifest, or move it to the version saved, as nylorun
|
|
46
|
+
agents put does; --agent=<id> when <id> names a directory
|
|
47
|
+
--watch check and plan again on each change to the folders, until
|
|
48
|
+
interrupted; with --save, also save each change
|
|
29
49
|
|
|
30
50
|
${runtimeFlagsUsage}`;
|
|
31
51
|
/** What a folder could not be composed for, as a person reads it. */
|
|
@@ -43,55 +63,129 @@ export function planLines(plan) {
|
|
|
43
63
|
: `version ${plan.from} → ${plan.to}`;
|
|
44
64
|
return [
|
|
45
65
|
`${plan.name}: ${move}${plan.behind > 0 ? ` (${plan.behind} ${plan.behind === 1 ? "Agent" : "Agents"} on older versions)` : ""}`,
|
|
46
|
-
...plan.changes.
|
|
66
|
+
...plan.changes.flatMap((change) => [` ${change.section} ${change.path} ${change.op}`, ...changeDiffLines(change)]),
|
|
47
67
|
...plan.warnings.map((warning) => ` warning: ${warning.message}`),
|
|
48
68
|
...plan.errors.map((error) => ` error: ${error}`),
|
|
49
69
|
];
|
|
50
70
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
71
|
+
function reportSaved(deps, saved, label) {
|
|
72
|
+
deps.err(saved.unchanged
|
|
73
|
+
? `Agent manifest ${saved.manifest.name} is unchanged at version ${saved.manifest.version} on ${label}.`
|
|
74
|
+
: `Saved agent manifest ${saved.manifest.name} version ${saved.manifest.version} on ${label}.`);
|
|
75
|
+
for (const warning of saved.warnings)
|
|
76
|
+
deps.err(` warning: ${warning.message}`);
|
|
77
|
+
}
|
|
78
|
+
/** `nylorun apply [dir…] [--dry-run] [--if-match <n>] [--agent [<id>]] [--watch [--save]]` */
|
|
79
|
+
export async function applyCommand(deps, given, options = {}) {
|
|
80
|
+
if (given[0] === "--help" || given[0] === "-h") {
|
|
54
81
|
deps.out(applyUsage);
|
|
55
82
|
return 0;
|
|
56
83
|
}
|
|
57
|
-
const usage = "nylorun apply [<dir>...] [--dry-run] [--if-match <n>]";
|
|
58
|
-
const flags = parseFlags(args, { booleans: ["--dry-run"], values: ["--if-match"] }, usage);
|
|
59
|
-
const ifMatch = integerFlag(flags, "--if-match");
|
|
84
|
+
const usage = "nylorun apply [<dir>...] [--dry-run] [--if-match <n>] [--agent [<id>]] [--watch [--save]]";
|
|
60
85
|
const cwd = deps.cwd ?? process.cwd();
|
|
86
|
+
const { args, agent } = takeAgentFlag(given, cwd);
|
|
87
|
+
const flags = parseFlags(args, { booleans: ["--dry-run", "--watch", "--save"], values: ["--if-match"] }, usage);
|
|
88
|
+
if (flags.booleans.has("--save") && !flags.booleans.has("--watch"))
|
|
89
|
+
throw usageError("--save goes with --watch: apply saves anyway.");
|
|
90
|
+
if (flags.booleans.has("--watch")) {
|
|
91
|
+
if (flags.booleans.has("--dry-run"))
|
|
92
|
+
throw usageError("--watch saves nothing unless --save: drop --dry-run.");
|
|
93
|
+
const { watchApply } = await import("./watch.js");
|
|
94
|
+
return watchApply(deps, {
|
|
95
|
+
dirs: flags.rest,
|
|
96
|
+
args: given.filter((arg) => arg !== "--watch" && arg !== "--save"),
|
|
97
|
+
save: flags.booleans.has("--save"),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const ifMatch = integerFlag(flags, "--if-match");
|
|
61
101
|
const folders = [];
|
|
62
102
|
for (const dir of flags.rest.length > 0 ? flags.rest : ["."]) {
|
|
63
|
-
const
|
|
103
|
+
const at = resolve(cwd, dir);
|
|
104
|
+
const found = (await isAgentFolder(at))
|
|
105
|
+
? [at]
|
|
106
|
+
: [
|
|
107
|
+
...new Set([
|
|
108
|
+
...(await manifestFolders(at).catch((error) => composeFailure(dir, error))),
|
|
109
|
+
...(await agentFolders(at)),
|
|
110
|
+
]),
|
|
111
|
+
].sort();
|
|
64
112
|
if (found.length === 0)
|
|
65
|
-
throw usageError(`${dir} holds no agent.manifest.json, and no subfolder does.`);
|
|
113
|
+
throw usageError(`${dir} holds no AGENT.md or agent.manifest.json, and no subfolder does.`);
|
|
66
114
|
folders.push(...found);
|
|
67
115
|
}
|
|
68
116
|
if (ifMatch !== undefined && folders.length > 1)
|
|
69
117
|
throw usageError("--if-match takes one manifest: name one folder.");
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const { client, connection } = await runtimeClient(deps, flags);
|
|
118
|
+
if (agent?.id !== undefined && folders.length > 1)
|
|
119
|
+
throw usageError("--agent <id> takes one manifest: name one folder, or drop the id.");
|
|
120
|
+
// Every folder composes, or compiles, before anything is sent: a broken file stops the whole apply.
|
|
121
|
+
const work = [];
|
|
75
122
|
let blocked = false;
|
|
76
|
-
for (const
|
|
123
|
+
for (const folder of folders) {
|
|
124
|
+
if (await isAgentFolder(folder)) {
|
|
125
|
+
const pack = await packed(folder);
|
|
126
|
+
const compiled = await checkFolder(pack);
|
|
127
|
+
if (!compiled.manifest) {
|
|
128
|
+
if (!options.checked)
|
|
129
|
+
for (const line of diagnosticLines(folder, cwd, compiled.diagnostics))
|
|
130
|
+
deps.err(line);
|
|
131
|
+
blocked = true;
|
|
132
|
+
}
|
|
133
|
+
work.push({ folder, pack });
|
|
134
|
+
}
|
|
135
|
+
else
|
|
136
|
+
work.push({ folder, manifest: await composeManifest(folder).catch((error) => composeFailure(folder, error)) });
|
|
137
|
+
}
|
|
138
|
+
if (blocked) {
|
|
139
|
+
deps.err("Nothing was sent: an agent folder does not compile (nylorun check).");
|
|
140
|
+
return 1;
|
|
141
|
+
}
|
|
142
|
+
const { client, connection } = await runtimeClient(deps, flags);
|
|
143
|
+
const dryRun = flags.booleans.has("--dry-run") || options.planOnly === true;
|
|
144
|
+
// `--agent`: make or move the Agent once its manifest is saved, or found unchanged.
|
|
145
|
+
const agentFor = async (name) => {
|
|
146
|
+
if (agent && !dryRun)
|
|
147
|
+
await putAgent(deps, client, connection.label, agent.id ?? name, name);
|
|
148
|
+
};
|
|
149
|
+
for (const item of work) {
|
|
150
|
+
const saveOptions = ifMatch === undefined ? {} : { ifMatch };
|
|
151
|
+
if ("pack" in item) {
|
|
152
|
+
const plan = await runtimeCall(() => client.agentManifests.planFolder(item.pack, saveOptions));
|
|
153
|
+
for (const line of planLines(plan))
|
|
154
|
+
deps.out(line);
|
|
155
|
+
if (!options.checked)
|
|
156
|
+
for (const line of diagnosticLines(item.folder, cwd, (plan.diagnostics ?? []).filter((d) => d.severity !== "error")))
|
|
157
|
+
deps.out(` ${line}`);
|
|
158
|
+
if (flags.booleans.has("--dry-run") && plan.manifest)
|
|
159
|
+
deps.out(JSON.stringify(plan.manifest, null, 2));
|
|
160
|
+
if (plan.errors.length > 0) {
|
|
161
|
+
blocked = true;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (dryRun)
|
|
165
|
+
continue;
|
|
166
|
+
if (!plan.unchanged)
|
|
167
|
+
reportSaved(deps, await runtimeCall(() => client.agentManifests.saveFolder(item.pack, { ifMatch: ifMatch ?? plan.from ?? 0 })), connection.label);
|
|
168
|
+
await agentFor(plan.name);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const { manifest } = item;
|
|
77
172
|
for (const warning of manifest.warnings)
|
|
78
173
|
deps.err(`${manifest.manifest.name}: warning: ${warning}`);
|
|
79
174
|
// The plan's errors include the gaps the composer found (its sections, the harness's requires).
|
|
80
|
-
const plan = await runtimeCall(() => client.agentManifests.plan(manifest,
|
|
175
|
+
const plan = await runtimeCall(() => client.agentManifests.plan(manifest, saveOptions));
|
|
81
176
|
for (const line of planLines(plan))
|
|
82
177
|
deps.out(line);
|
|
83
178
|
if (plan.errors.length > 0) {
|
|
84
179
|
blocked = true;
|
|
85
180
|
continue;
|
|
86
181
|
}
|
|
87
|
-
if (
|
|
182
|
+
if (dryRun)
|
|
88
183
|
continue;
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
deps.err(` warning: ${warning.message}`);
|
|
184
|
+
if (!plan.unchanged) {
|
|
185
|
+
const saved = await runtimeCall(() => client.agentManifests.save(manifest, { ifMatch: ifMatch ?? plan.from ?? 0 }));
|
|
186
|
+
reportSaved(deps, saved, connection.label);
|
|
187
|
+
}
|
|
188
|
+
await agentFor(plan.name);
|
|
95
189
|
}
|
|
96
190
|
if (blocked)
|
|
97
191
|
deps.err("Nothing was saved for a manifest with errors.");
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { StackDeps } from "../stack/commands.js";
|
|
2
|
+
export declare const chatUsage: string;
|
|
3
|
+
/** Where `chat` reads its messages when `--message` is not given. */
|
|
4
|
+
export interface ChatIo {
|
|
5
|
+
lines?: AsyncIterable<string>;
|
|
6
|
+
}
|
|
7
|
+
/** `nylorun chat <agent> [--session <id>] [--message <text>] [--owner <user>]` */
|
|
8
|
+
export declare function chatCommand(deps: StackDeps, args: readonly string[], io?: ChatIo): Promise<number>;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nylorun chat <agent> [--session <id>] [--message <text>]` (track T7): a conversation with an
|
|
3
|
+
* Agent from the terminal. It opens a session on the Agent (or reuses `--session`, opening it
|
|
4
|
+
* with that id when it does not exist), sends `--message` once or each line of standard input,
|
|
5
|
+
* and prints the assistant's text as each model step lands, until the turn ends. A turn that
|
|
6
|
+
* waits for a person (an approval, a question) prints how to answer it with `nylorun sessions`.
|
|
7
|
+
* Exits 1 when a turn fails or is cancelled.
|
|
8
|
+
*/
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
|
+
import { createInterface } from "node:readline";
|
|
11
|
+
import { RuntimeError } from "@nylorun/agents";
|
|
12
|
+
import { CliError } from "../errors.js";
|
|
13
|
+
import { parseFlags, positional, runtimeCall, runtimeClient, runtimeFlagsUsage } from "./runtime.js";
|
|
14
|
+
export const chatUsage = `nylorun chat <agent> [--session <id>] [--message <text>] [--owner <user>]
|
|
15
|
+
|
|
16
|
+
Talks to Agent <agent> (nylorun apply <dir> --agent makes one): sends --message once, or each
|
|
17
|
+
line of standard input, and prints the reply as it comes. A new session is opened unless
|
|
18
|
+
--session names one, which is opened with that id when it does not exist.
|
|
19
|
+
|
|
20
|
+
--session <id> the session to use, kept across runs
|
|
21
|
+
--message <text> send this one message, print the reply, and exit
|
|
22
|
+
--owner <user> the session's owner (default the Agent's owner, else --as,
|
|
23
|
+
else $USER)
|
|
24
|
+
|
|
25
|
+
${runtimeFlagsUsage}`;
|
|
26
|
+
const errorStatus = (error) => (error instanceof RuntimeError ? error.status : undefined);
|
|
27
|
+
/** Sends `text` and prints the turn it starts as it streams; false when the turn failed. */
|
|
28
|
+
async function converse(deps, session, text) {
|
|
29
|
+
const accepted = await runtimeCall(() => session.input(text, { idempotencyKey: randomUUID() }));
|
|
30
|
+
// A message to a busy session waits in its queue; its turn starts when `queue.taken` names it.
|
|
31
|
+
let turnId = accepted.status === "accepted" ? accepted.turnId : null;
|
|
32
|
+
const queued = accepted.status === "queued" ? accepted.queuedMessageId : undefined;
|
|
33
|
+
if (queued)
|
|
34
|
+
deps.err(`(queued at position ${accepted.status === "queued" ? accepted.position : 0}: a turn is running)`);
|
|
35
|
+
const controller = new AbortController();
|
|
36
|
+
let ok = true;
|
|
37
|
+
let spoke = false;
|
|
38
|
+
try {
|
|
39
|
+
await runtimeCall(async () => {
|
|
40
|
+
for await (const event of session.observe({
|
|
41
|
+
signal: controller.signal,
|
|
42
|
+
...(accepted.cursor ? { cursor: accepted.cursor } : {}),
|
|
43
|
+
})) {
|
|
44
|
+
const payload = (event.payload ?? {});
|
|
45
|
+
if (event.type === "queue.taken" && queued && Array.isArray(payload.queuedMessageIds) && payload.queuedMessageIds.includes(queued))
|
|
46
|
+
turnId = String(payload.turnId);
|
|
47
|
+
if (turnId === null || event.turnId !== turnId)
|
|
48
|
+
continue;
|
|
49
|
+
if (event.type === "message.assistant") {
|
|
50
|
+
if (typeof payload.text === "string" && payload.text.trim() !== "") {
|
|
51
|
+
deps.out(payload.text.replace(/\n+$/, ""));
|
|
52
|
+
spoke = true;
|
|
53
|
+
}
|
|
54
|
+
for (const call of Array.isArray(payload.toolCalls) ? payload.toolCalls : [])
|
|
55
|
+
deps.err(`[${call.name}]`);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (event.type === "turn.completed") {
|
|
59
|
+
// A turn whose answer is structured output said no text: print the output.
|
|
60
|
+
if (!spoke && payload.output !== undefined && payload.output !== null)
|
|
61
|
+
deps.out(typeof payload.output === "string" ? payload.output : JSON.stringify(payload.output, null, 2));
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
if (event.type === "turn.failed" || event.type === "turn.cancelled") {
|
|
65
|
+
const why = payload.error?.message ?? payload.message ?? payload.reason;
|
|
66
|
+
deps.err(`The turn ${event.type === "turn.failed" ? "failed" : "was cancelled"}${why ? `: ${why}` : ""}.`);
|
|
67
|
+
ok = false;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
if (event.type === "turn.paused") {
|
|
71
|
+
for (const item of Array.isArray(payload.interactions) ? payload.interactions : []) {
|
|
72
|
+
const id = item?.interaction?.id;
|
|
73
|
+
const kind = item?.interaction?.kind;
|
|
74
|
+
deps.err(kind === "approval"
|
|
75
|
+
? `The turn waits for an approval: nylorun sessions approve ${session.id} ${id} [--deny]`
|
|
76
|
+
: `The turn waits for an answer (${kind}): nylorun sessions respond ${session.id} ${id} <json>`);
|
|
77
|
+
}
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
controller.abort();
|
|
85
|
+
}
|
|
86
|
+
return ok;
|
|
87
|
+
}
|
|
88
|
+
/** The session to talk in: `--session`, opened when missing, or a new one. */
|
|
89
|
+
async function sessionFor(deps, client, agentId, sessionId, owner) {
|
|
90
|
+
if (sessionId !== undefined) {
|
|
91
|
+
try {
|
|
92
|
+
await client.session(sessionId).inspect();
|
|
93
|
+
return client.session(sessionId);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (errorStatus(error) !== 404)
|
|
97
|
+
await runtimeCall(() => Promise.reject(error));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const id = sessionId ?? `chat-${randomUUID()}`;
|
|
101
|
+
const session = await runtimeCall(() => client.createSession({ agent: agentId, ownerUserId: owner, id }));
|
|
102
|
+
if (sessionId === undefined)
|
|
103
|
+
deps.err(`Session ${id} on Agent ${agentId} (continue it with --session ${id}).`);
|
|
104
|
+
return session;
|
|
105
|
+
}
|
|
106
|
+
/** Standard input's lines. */
|
|
107
|
+
async function* stdinLines() {
|
|
108
|
+
const lines = createInterface({ input: process.stdin, ...(process.stdin.isTTY ? { output: process.stderr, prompt: "> " } : {}) });
|
|
109
|
+
if (process.stdin.isTTY)
|
|
110
|
+
lines.prompt();
|
|
111
|
+
for await (const line of lines) {
|
|
112
|
+
yield line;
|
|
113
|
+
if (process.stdin.isTTY)
|
|
114
|
+
lines.prompt();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** `nylorun chat <agent> [--session <id>] [--message <text>] [--owner <user>]` */
|
|
118
|
+
export async function chatCommand(deps, args, io = {}) {
|
|
119
|
+
const usage = "nylorun chat <agent> [--session <id>] [--message <text>] [--owner <user>]";
|
|
120
|
+
if (args[0] === "--help" || args[0] === "-h") {
|
|
121
|
+
deps.out(chatUsage);
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
const flags = parseFlags(args, { values: ["--session", "--message", "--owner"] }, usage);
|
|
125
|
+
const [agentId] = positional(flags, 1, usage);
|
|
126
|
+
const message = flags.values.get("--message");
|
|
127
|
+
const { client } = await runtimeClient(deps, flags);
|
|
128
|
+
const agent = await client.agents.get(agentId).catch((error) => {
|
|
129
|
+
if (errorStatus(error) === 404)
|
|
130
|
+
throw new CliError(`No Agent ${agentId}: make one with nylorun apply <dir> --agent ${agentId}.`, 1);
|
|
131
|
+
return runtimeCall(() => Promise.reject(error));
|
|
132
|
+
});
|
|
133
|
+
const owner = flags.values.get("--owner") ?? agent.owner ?? flags.values.get("--as") ?? deps.env.USER ?? deps.env.USERNAME ?? "nylorun-cli";
|
|
134
|
+
const session = await sessionFor(deps, client, agentId, flags.values.get("--session"), owner);
|
|
135
|
+
if (message !== undefined)
|
|
136
|
+
return (await converse(deps, session, message)) ? 0 : 1;
|
|
137
|
+
let failed = false;
|
|
138
|
+
for await (const line of io.lines ?? stdinLines()) {
|
|
139
|
+
if (line.trim() === "")
|
|
140
|
+
continue;
|
|
141
|
+
if (!(await converse(deps, session, line)))
|
|
142
|
+
failed = true;
|
|
143
|
+
}
|
|
144
|
+
return failed ? 1 : 0;
|
|
145
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type StackDeps } from "../stack/commands.js";
|
|
2
|
+
export declare const exportUsage: string;
|
|
3
|
+
export declare const migrateFolderUsage = "nylorun migrate folder <dir> [--out <dir>]\n\nWrites the agent folder of the three-file folder <dir> (agent.manifest.json, with its\nharness.manifest.json and runtime.manifest.json), offline: the manifest nylorun apply composes\nfrom it, as AGENT.md, mcp.json, tools/, skills/ and runtime.json. A SKILL.md's secrets move to\nAGENT.md. It says whether the folder compiles to the same manifest. Reads no Runtime.\n\n --out <dir> where the folder goes (default ./<name>); no file is\n written over";
|
|
4
|
+
export declare const pullUsage: string;
|
|
5
|
+
/**
|
|
6
|
+
* Writes `files` under `out`. A file already there with the same bytes is left; one with other
|
|
7
|
+
* bytes is a conflict, and with any conflict nothing is written. Answers how many it wrote and left.
|
|
8
|
+
*/
|
|
9
|
+
export declare function writeFolder(out: string, files: ReadonlyMap<string, Uint8Array | string>, cwd: string): Promise<{
|
|
10
|
+
written: number;
|
|
11
|
+
same: number;
|
|
12
|
+
}>;
|
|
13
|
+
/** `nylorun export --folder <name>[@<version>] [--out <dir>]` */
|
|
14
|
+
export declare function exportCommand(deps: StackDeps, args: readonly string[]): Promise<number>;
|
|
15
|
+
/** `nylorun migrate folder <dir> [--out <dir>]` */
|
|
16
|
+
export declare function migrateFolderCommand(deps: StackDeps, args: readonly string[]): Promise<number>;
|
|
17
|
+
/** `nylorun pull <name>[@<version>] [--out <dir>]` */
|
|
18
|
+
export declare function pullCommand(deps: StackDeps, args: readonly string[]): Promise<number>;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converting to agent folders (track T7, D80):
|
|
3
|
+
*
|
|
4
|
+
* - `nylorun export --folder <name>[@<version>]` writes the agent folder of a saved manifest, from
|
|
5
|
+
* JSON or TypeScript (`folderOfManifest`, `@nylorun/core/folder`), read with the Runtime API.
|
|
6
|
+
* - `nylorun migrate folder <dir>` does the same, offline, for a three-file folder
|
|
7
|
+
* (`agent.manifest.json`), composed as `nylorun apply` composes it, with its skills' bytes.
|
|
8
|
+
* - `nylorun pull <name>[@<version>]` writes back the folder a version was saved from, byte for
|
|
9
|
+
* byte, from the version's source (`agentManifests.source` and `sourceFile`).
|
|
10
|
+
*
|
|
11
|
+
* None writes over a file whose bytes differ: a file that is already there with the same bytes
|
|
12
|
+
* is left as it is, and a conflict writes nothing.
|
|
13
|
+
*/
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
15
|
+
import { existsSync } from "node:fs";
|
|
16
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
17
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
18
|
+
import { AGENT_MANIFEST_FILE, ComposeError, PluginError, RuntimeError, SkillsError, composeManifest, } from "@nylorun/agents";
|
|
19
|
+
import { isAgentManifestName } from "@nylorun/core/contracts";
|
|
20
|
+
import { folderOfManifest } from "@nylorun/core/folder";
|
|
21
|
+
import { CliError } from "../errors.js";
|
|
22
|
+
import { parseStackFlags } from "../stack/commands.js";
|
|
23
|
+
import { parseFlags, runtimeCall, runtimeClient, runtimeFlagsUsage, usageError } from "./runtime.js";
|
|
24
|
+
export const exportUsage = `nylorun export --folder <name>[@<version>] [--out <dir>]
|
|
25
|
+
|
|
26
|
+
Writes the agent folder of a saved agent manifest (its latest version, or <version>), for one
|
|
27
|
+
saved as JSON or from TypeScript: AGENT.md with its instructions, instructions/, tools/, mcp.json,
|
|
28
|
+
triggers/ and runtime.json, subagents as agents/<name>/, and a flow as flow.json with its agents
|
|
29
|
+
under agents/. It says whether the folder compiles back to the same manifest, and notes what it
|
|
30
|
+
cannot write the same way. Skills' files are not in a saved manifest: see the notes.
|
|
31
|
+
|
|
32
|
+
--folder write an agent folder (the only export there is)
|
|
33
|
+
--out <dir> where the folder goes (default ./<name>); no file is
|
|
34
|
+
written over
|
|
35
|
+
|
|
36
|
+
${runtimeFlagsUsage}`;
|
|
37
|
+
export const migrateFolderUsage = `nylorun migrate folder <dir> [--out <dir>]
|
|
38
|
+
|
|
39
|
+
Writes the agent folder of the three-file folder <dir> (agent.manifest.json, with its
|
|
40
|
+
harness.manifest.json and runtime.manifest.json), offline: the manifest nylorun apply composes
|
|
41
|
+
from it, as AGENT.md, mcp.json, tools/, skills/ and runtime.json. A SKILL.md's secrets move to
|
|
42
|
+
AGENT.md. It says whether the folder compiles to the same manifest. Reads no Runtime.
|
|
43
|
+
|
|
44
|
+
--out <dir> where the folder goes (default ./<name>); no file is
|
|
45
|
+
written over`;
|
|
46
|
+
export const pullUsage = `nylorun pull <name>[@<version>] [--out <dir>]
|
|
47
|
+
|
|
48
|
+
Writes the agent folder a version of manifest <name> (default its latest) was saved from, byte
|
|
49
|
+
for byte, as the Runtime keeps it. A version saved as JSON has none: nylorun export --folder
|
|
50
|
+
writes one from its manifest.
|
|
51
|
+
|
|
52
|
+
--out <dir> where the folder goes (default ./<name>); a file with the
|
|
53
|
+
same bytes is left, and no file is written over
|
|
54
|
+
|
|
55
|
+
${runtimeFlagsUsage}`;
|
|
56
|
+
/** `name` or `name@version`. */
|
|
57
|
+
function nameAndVersion(ref, usage) {
|
|
58
|
+
const at = ref.lastIndexOf("@");
|
|
59
|
+
const name = at < 0 ? ref : ref.slice(0, at);
|
|
60
|
+
const version = at < 0 ? undefined : ref.slice(at + 1);
|
|
61
|
+
if (!isAgentManifestName(name))
|
|
62
|
+
throw usageError(`${name} is not an agent manifest's name.\nUsage: ${usage.split("\n")[0]}`);
|
|
63
|
+
if (version !== undefined && !/^[1-9]\d{0,8}$/.test(version))
|
|
64
|
+
throw usageError(`${ref}: the version after @ is a whole number from 1.\nUsage: ${usage.split("\n")[0]}`);
|
|
65
|
+
return version === undefined ? { name } : { name, version: Number(version) };
|
|
66
|
+
}
|
|
67
|
+
const sameBytes = (a, b) => a.byteLength === b.byteLength && a.every((byte, index) => byte === b[index]);
|
|
68
|
+
/**
|
|
69
|
+
* Writes `files` under `out`. A file already there with the same bytes is left; one with other
|
|
70
|
+
* bytes is a conflict, and with any conflict nothing is written. Answers how many it wrote and left.
|
|
71
|
+
*/
|
|
72
|
+
export async function writeFolder(out, files, cwd) {
|
|
73
|
+
const encoder = new TextEncoder();
|
|
74
|
+
const plan = [];
|
|
75
|
+
const conflicts = [];
|
|
76
|
+
let same = 0;
|
|
77
|
+
for (const [path, content] of files) {
|
|
78
|
+
const target = resolve(out, path);
|
|
79
|
+
if (!target.startsWith(`${out}${sep}`))
|
|
80
|
+
throw new CliError(`${path} would be written outside ${relative(cwd, out) || "."}: nothing was written.`, 1);
|
|
81
|
+
const bytes = typeof content === "string" ? encoder.encode(content) : content;
|
|
82
|
+
if (existsSync(target)) {
|
|
83
|
+
if (sameBytes(new Uint8Array(await readFile(target)), bytes))
|
|
84
|
+
same++;
|
|
85
|
+
else
|
|
86
|
+
conflicts.push(relative(cwd, target));
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
plan.push([target, bytes]);
|
|
90
|
+
}
|
|
91
|
+
if (conflicts.length > 0)
|
|
92
|
+
throw new CliError(`${conflicts.length === 1 ? `${conflicts[0]} exists` : `These files exist: ${conflicts.join(", ")}`} with other bytes: nothing was written. Pass another --out, or move ${conflicts.length === 1 ? "it" : "them"} first.`, 1);
|
|
93
|
+
for (const [target, bytes] of plan) {
|
|
94
|
+
await mkdir(dirname(target), { recursive: true });
|
|
95
|
+
await writeFile(target, bytes, { flag: "wx" });
|
|
96
|
+
}
|
|
97
|
+
return { written: plan.length, same };
|
|
98
|
+
}
|
|
99
|
+
/** "Wrote 3 files to triage" and, when some were there already, how many. */
|
|
100
|
+
function wroteLine(where, result) {
|
|
101
|
+
const files = (count) => `${count} ${count === 1 ? "file" : "files"}`;
|
|
102
|
+
return `Wrote ${files(result.written)} to ${where}${result.same > 0 ? ` (${files(result.same)} already there, the same)` : ""}.`;
|
|
103
|
+
}
|
|
104
|
+
/** The folder written, then what differs from the manifest, then the next step. */
|
|
105
|
+
async function writeDecompiled(deps, folder, out, cwd) {
|
|
106
|
+
const where = relative(cwd, out) || ".";
|
|
107
|
+
deps.out(wroteLine(where, await writeFolder(out, folder.files, cwd)));
|
|
108
|
+
for (const note of folder.notes)
|
|
109
|
+
deps.err(` note: ${note}`);
|
|
110
|
+
deps.err(folder.exact
|
|
111
|
+
? `It compiles back to the same manifest. Check it with nylorun check ${where}, and save it with nylorun apply ${where}.`
|
|
112
|
+
: `It compiles to a different manifest: see the notes, then check it with nylorun check ${where}.`);
|
|
113
|
+
return 0;
|
|
114
|
+
}
|
|
115
|
+
/** `nylorun export --folder <name>[@<version>] [--out <dir>]` */
|
|
116
|
+
export async function exportCommand(deps, args) {
|
|
117
|
+
const usage = exportUsage.split("\n")[0];
|
|
118
|
+
const flags = parseFlags(args, { booleans: ["--folder"], values: ["--out"] }, exportUsage);
|
|
119
|
+
if (!flags.booleans.has("--folder"))
|
|
120
|
+
throw usageError(`nylorun export writes an agent folder: pass --folder.\nUsage: ${usage}`);
|
|
121
|
+
if (flags.rest.length !== 1)
|
|
122
|
+
throw usageError(`Usage: ${usage}`);
|
|
123
|
+
const { name, version } = nameAndVersion(flags.rest[0], exportUsage);
|
|
124
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
125
|
+
const { client } = await runtimeClient(deps, flags);
|
|
126
|
+
const view = await runtimeCall(() => client.agentManifests.get(name, version));
|
|
127
|
+
if (view.source)
|
|
128
|
+
deps.err(`${name}@${view.version} was saved from a folder: nylorun pull ${name}@${view.version} writes it as it was saved.`);
|
|
129
|
+
const out = resolve(cwd, flags.values.get("--out") ?? name);
|
|
130
|
+
return writeDecompiled(deps, folderOfManifest(view.manifest), out, cwd);
|
|
131
|
+
}
|
|
132
|
+
/** What a three-file folder could not be composed for, as a person reads it. */
|
|
133
|
+
function composeFailure(dir, error) {
|
|
134
|
+
if (error instanceof ComposeError || error instanceof SkillsError || error instanceof PluginError || error instanceof TypeError)
|
|
135
|
+
throw new CliError(`${dir}: ${error.message}`, 1);
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
/** `nylorun migrate folder <dir> [--out <dir>]` */
|
|
139
|
+
export async function migrateFolderCommand(deps, args) {
|
|
140
|
+
const flags = parseStackFlags(args, { values: ["--out"] }, migrateFolderUsage);
|
|
141
|
+
if (flags.rest.length !== 1)
|
|
142
|
+
throw usageError(`Usage: ${migrateFolderUsage.split("\n")[0]}`);
|
|
143
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
144
|
+
const dir = resolve(cwd, flags.rest[0]);
|
|
145
|
+
if (!existsSync(join(dir, AGENT_MANIFEST_FILE)))
|
|
146
|
+
throw usageError(`${flags.rest[0]} holds no ${AGENT_MANIFEST_FILE}.`);
|
|
147
|
+
const composed = await composeManifest(dir).catch((error) => composeFailure(flags.rest[0], error));
|
|
148
|
+
const out = resolve(cwd, flags.values.get("--out") ?? composed.manifest.name);
|
|
149
|
+
if (out === dir)
|
|
150
|
+
throw usageError(`The folder would be written over the three files in ${relative(cwd, dir) || "."}: pass --out.`);
|
|
151
|
+
// The bytes behind the manifest's hashes, so its skills and runtime files are written as files.
|
|
152
|
+
const bytes = new Map();
|
|
153
|
+
for (const [hash, source] of composed.files)
|
|
154
|
+
bytes.set(hash, await source.read());
|
|
155
|
+
// This command is what the deprecation warning names.
|
|
156
|
+
for (const warning of composed.warnings.filter((item) => !item.includes("nylorun migrate folder")))
|
|
157
|
+
deps.err(` warning: ${warning}`);
|
|
158
|
+
for (const gap of composed.gaps)
|
|
159
|
+
deps.err(` fix before apply: ${gap}`);
|
|
160
|
+
return writeDecompiled(deps, folderOfManifest(composed.manifest, { files: bytes }), out, cwd);
|
|
161
|
+
}
|
|
162
|
+
/** The source of `name@version`, or a `CliError` that says a JSON save has none. */
|
|
163
|
+
async function sourceOf(client, name, version) {
|
|
164
|
+
try {
|
|
165
|
+
return await client.agentManifests.source(name, version);
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
if (error instanceof RuntimeError && error.status === 404 && error.body?.code !== "manifest_not_found")
|
|
169
|
+
throw new CliError(`${name}@${version} was saved as JSON, not from a folder, so it has no source. nylorun export --folder ${name}@${version} writes a folder from its manifest.`, 1);
|
|
170
|
+
return runtimeCall(() => Promise.reject(error));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/** `nylorun pull <name>[@<version>] [--out <dir>]` */
|
|
174
|
+
export async function pullCommand(deps, args) {
|
|
175
|
+
const flags = parseFlags(args, { values: ["--out"] }, pullUsage);
|
|
176
|
+
if (flags.rest.length !== 1)
|
|
177
|
+
throw usageError(`Usage: ${pullUsage.split("\n")[0]}`);
|
|
178
|
+
const ref = nameAndVersion(flags.rest[0], pullUsage);
|
|
179
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
180
|
+
const { client } = await runtimeClient(deps, flags);
|
|
181
|
+
const version = ref.version ?? (await runtimeCall(() => client.agentManifests.get(ref.name))).version;
|
|
182
|
+
const source = await sourceOf(client, ref.name, version);
|
|
183
|
+
const files = new Map();
|
|
184
|
+
for (const entry of source.entries) {
|
|
185
|
+
const bytes = await runtimeCall(() => client.agentManifests.sourceFile(ref.name, version, entry.path));
|
|
186
|
+
if (createHash("sha256").update(bytes).digest("hex") !== entry.sha256)
|
|
187
|
+
throw new CliError(`${entry.path} came back with other bytes than the source lists: nothing was written. Pull again.`, 1);
|
|
188
|
+
files.set(entry.path, bytes);
|
|
189
|
+
}
|
|
190
|
+
const out = resolve(cwd, flags.values.get("--out") ?? ref.name);
|
|
191
|
+
const where = relative(cwd, out) || ".";
|
|
192
|
+
deps.out(wroteLine(where, await writeFolder(out, files, cwd)));
|
|
193
|
+
deps.err(`${ref.name}@${version}, compiled by ${source.compiler}. Check it with nylorun check ${where}, and save an edit with nylorun apply ${where}.`);
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type PackedFolder } from "@nylorun/agents";
|
|
2
|
+
import { type AgentFolderDiagnostic } from "@nylorun/core/folder";
|
|
3
|
+
import { type StackDeps } from "../stack/commands.js";
|
|
4
|
+
export { initCommand, initUsage } from "./init.js";
|
|
5
|
+
export declare const checkUsage = "nylorun check [<dir>...] [--fix] [--json]\n\nCompiles each agent folder (default .) offline, with the compiler the Runtime runs, and prints\nevery problem by file, line and column. A folder holds AGENT.md; a folder without one checks each\nsubfolder that has one. Exits 1 when a folder does not compile. Reads no Runtime.\n\nA problem one edit fixes says (fixable with --fix): an unknown key with one did-you-mean, CRLF\nline ends, and secrets in a SKILL.md, which go in AGENT.md's skills block.\n\n --fix make those edits in the folder's files, print each, then\n check again; nothing is written without it, and a file\n that changed since it was checked is left as it is\n --json print { \"folders\": [{ path, ok, manifest?, diagnostics }] }\n for CI: whether each folder compiles, the manifest it\n compiles to, and every diagnostic, each fixable one with\n its fix; with --fix, also what was fixed and what was not";
|
|
6
|
+
/** The agent folders under `dir`: `dir` when it holds an `AGENT.md`, else each subfolder that does. */
|
|
7
|
+
export declare function agentFolders(dir: string): Promise<string[]>;
|
|
8
|
+
/** Packs the folder, or a `CliError` naming why it cannot be. */
|
|
9
|
+
export declare function packed(folder: string): Promise<PackedFolder>;
|
|
10
|
+
/** A folder's diagnostics as lines, each path relative to `cwd`; `fixable` marks those with a fix. */
|
|
11
|
+
export declare function diagnosticLines(folder: string, cwd: string, diagnostics: readonly AgentFolderDiagnostic[], fixable?: boolean): string[];
|
|
12
|
+
/** `nylorun check [dir…] [--fix] [--json]` */
|
|
13
|
+
export declare function checkCommand(deps: StackDeps, args: readonly string[]): Promise<number>;
|