nylorun 0.19.0-beta → 0.21.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 +165 -11
- package/dist/cli.js +29 -5
- 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 +62 -25
- 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 +6 -8
- package/dist/client/folder.js +116 -38
- 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/model/pi-ai.d.ts +1 -1
- package/dist/client/model/pi-ai.js +1 -1
- package/dist/client/models.d.ts +1 -1
- package/dist/client/models.js +30 -4
- package/dist/client/plan-diff.d.ts +14 -0
- package/dist/client/plan-diff.js +86 -0
- package/dist/client/tenant.d.ts +1 -1
- package/dist/client/watch.d.ts +23 -0
- package/dist/client/watch.js +87 -0
- package/dist/nylo.js +2 -2
- package/dist/stack/compose-file.js +5 -1
- package/dist/stack/versions.d.ts +2 -0
- package/dist/stack/versions.js +12 -0
- package/package.json +7 -7
package/dist/client/folder.js
CHANGED
|
@@ -1,26 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Agent folders (track T6, D79) on the command line, offline: `nylorun check [dir…]` compiles each
|
|
3
3
|
* agent folder with the compiler the Runtime runs (`@nylorun/core/folder`) and prints every
|
|
4
|
-
* diagnostic by file, line and column
|
|
5
|
-
*
|
|
4
|
+
* diagnostic by file, line and column (`--json` for CI), and `--fix` makes the edits the fixable
|
|
5
|
+
* ones carry (track T7); `nylorun init <path>` (`init.ts`) writes a new folder. Neither reads a
|
|
6
|
+
* Runtime. `nylorun apply` saves a folder (`apply.ts`).
|
|
6
7
|
*/
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { readdir, readFile, writeFile } from "node:fs/promises";
|
|
9
10
|
import { basename, join, relative, resolve } from "node:path";
|
|
10
|
-
import { isAgentManifestName } from "@nylorun/core/contracts";
|
|
11
11
|
import { AGENT_FOLDER_FILE, FolderError, checkFolder, diagnosticLine, isAgentFolder, packFolder, } from "@nylorun/agents";
|
|
12
|
+
import { applyFolderFixes } from "@nylorun/core/folder";
|
|
12
13
|
import { CliError } from "../errors.js";
|
|
13
14
|
import { parseStackFlags } from "../stack/commands.js";
|
|
14
15
|
import { usageError } from "./runtime.js";
|
|
15
|
-
export
|
|
16
|
+
export { initCommand, initUsage } from "./init.js";
|
|
17
|
+
export const checkUsage = `nylorun check [<dir>...] [--fix] [--json]
|
|
16
18
|
|
|
17
19
|
Compiles each agent folder (default .) offline, with the compiler the Runtime runs, and prints
|
|
18
20
|
every problem by file, line and column. A folder holds AGENT.md; a folder without one checks each
|
|
19
|
-
subfolder that has one. Exits 1 when a folder does not compile. Reads no Runtime
|
|
20
|
-
export const initUsage = `nylorun init <name> [--description <text>]
|
|
21
|
+
subfolder that has one. Exits 1 when a folder does not compile. Reads no Runtime.
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
A problem one edit fixes says (fixable with --fix): an unknown key with one did-you-mean, CRLF
|
|
24
|
+
line ends, and secrets in a SKILL.md, which go in AGENT.md's skills block.
|
|
25
|
+
|
|
26
|
+
--fix make those edits in the folder's files, print each, then
|
|
27
|
+
check again; nothing is written without it, and a file
|
|
28
|
+
that changed since it was checked is left as it is
|
|
29
|
+
--json print { "folders": [{ path, ok, manifest?, diagnostics }] }
|
|
30
|
+
for CI: whether each folder compiles, the manifest it
|
|
31
|
+
compiles to, and every diagnostic, each fixable one with
|
|
32
|
+
its fix; with --fix, also what was fixed and what was not`;
|
|
24
33
|
/** The agent folders under `dir`: `dir` when it holds an `AGENT.md`, else each subfolder that does. */
|
|
25
34
|
export async function agentFolders(dir) {
|
|
26
35
|
const root = resolve(dir);
|
|
@@ -41,14 +50,68 @@ export async function packed(folder) {
|
|
|
41
50
|
throw error;
|
|
42
51
|
});
|
|
43
52
|
}
|
|
44
|
-
/** A folder's diagnostics as lines, each path relative to `cwd
|
|
45
|
-
export function diagnosticLines(folder, cwd, diagnostics) {
|
|
53
|
+
/** A folder's diagnostics as lines, each path relative to `cwd`; `fixable` marks those with a fix. */
|
|
54
|
+
export function diagnosticLines(folder, cwd, diagnostics, fixable = false) {
|
|
46
55
|
const prefix = relative(cwd, folder);
|
|
47
|
-
return diagnostics.map((item) => diagnosticLine(item.file && prefix ? { ...item, file: `${prefix}/${item.file}` } : item));
|
|
56
|
+
return diagnostics.map((item) => diagnosticLine(item.file && prefix ? { ...item, file: `${prefix}/${item.file}` } : item) + (fixable && item.fix ? " (fixable with --fix)" : ""));
|
|
48
57
|
}
|
|
49
|
-
/** `
|
|
58
|
+
/** How many times `check --fix` fixes and checks one folder, at most. */
|
|
59
|
+
const FIX_ROUNDS = 5;
|
|
60
|
+
const sha256Hex = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
61
|
+
const sameBytes = (a, b) => a.byteLength === b.byteLength && a.every((byte, index) => byte === b[index]);
|
|
62
|
+
/**
|
|
63
|
+
* Makes the fix-its `diagnostics` carry in the folder's files (`applyFolderFixes`). A file is
|
|
64
|
+
* edited only when its bytes on disk are the ones checked, and written only when they still are:
|
|
65
|
+
* when one has changed by then, nothing is written.
|
|
66
|
+
*/
|
|
67
|
+
async function fixFolder(pack, diagnostics, cwd) {
|
|
68
|
+
const fixes = diagnostics.flatMap((item) => (item.fix ? [item.fix] : []));
|
|
69
|
+
if (fixes.length === 0)
|
|
70
|
+
return { fixed: [], unfixed: [] };
|
|
71
|
+
const filesOf = (fix) => (fix.kind === "move_skill_secrets" ? [fix.file, AGENT_FOLDER_FILE] : [fix.file]);
|
|
72
|
+
const read = new Map();
|
|
73
|
+
const texts = new Map();
|
|
74
|
+
const unreadable = new Map();
|
|
75
|
+
for (const path of new Set(fixes.flatMap(filesOf))) {
|
|
76
|
+
if (path === "runtime.json" && pack.extended) {
|
|
77
|
+
unreadable.set(path, "runtime.json extends another file, and was checked merged with it: fix it by hand");
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const entry = pack.entries.find((item) => item.path === path);
|
|
81
|
+
const bytes = entry
|
|
82
|
+
? await readFile(join(pack.dir, ...path.split("/"))).then((data) => new Uint8Array(data), () => undefined)
|
|
83
|
+
: undefined;
|
|
84
|
+
if (!entry || !bytes || sha256Hex(bytes) !== entry.sha256) {
|
|
85
|
+
unreadable.set(path, "the file changed since it was checked: check again");
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
read.set(path, bytes);
|
|
89
|
+
// A byte-order mark stays: the fixes keep it.
|
|
90
|
+
texts.set(path, new TextDecoder("utf-8", { ignoreBOM: true }).decode(bytes));
|
|
91
|
+
}
|
|
92
|
+
const unfixed = [];
|
|
93
|
+
const applicable = fixes.filter((fix) => {
|
|
94
|
+
const blocked = filesOf(fix).find((path) => unreadable.has(path));
|
|
95
|
+
if (blocked)
|
|
96
|
+
unfixed.push({ file: fix.file, reason: unreadable.get(blocked) });
|
|
97
|
+
return !blocked;
|
|
98
|
+
});
|
|
99
|
+
const result = applyFolderFixes(texts, applicable);
|
|
100
|
+
unfixed.push(...result.skipped.map(({ fix, reason }) => ({ file: fix.file, reason })));
|
|
101
|
+
const encoder = new TextEncoder();
|
|
102
|
+
const writes = [...result.files].map(([path, text]) => ({ target: join(pack.dir, ...path.split("/")), before: read.get(path), after: encoder.encode(text) }));
|
|
103
|
+
for (const { target, before } of writes)
|
|
104
|
+
if (!sameBytes(new Uint8Array(await readFile(target)), before))
|
|
105
|
+
throw new CliError(`${relative(cwd, target)} changed while it was being fixed: nothing was written. Check again.`, 1);
|
|
106
|
+
for (const { target, after } of writes)
|
|
107
|
+
await writeFile(target, after);
|
|
108
|
+
return { fixed: result.changes, unfixed };
|
|
109
|
+
}
|
|
110
|
+
/** `nylorun check [dir…] [--fix] [--json]` */
|
|
50
111
|
export async function checkCommand(deps, args) {
|
|
51
|
-
const flags = parseStackFlags(args, {}, checkUsage);
|
|
112
|
+
const flags = parseStackFlags(args, { booleans: ["--json", "--fix"] }, checkUsage);
|
|
113
|
+
const json = flags.booleans.has("--json");
|
|
114
|
+
const fix = flags.booleans.has("--fix");
|
|
52
115
|
const cwd = deps.cwd ?? process.cwd();
|
|
53
116
|
const folders = [];
|
|
54
117
|
for (const dir of flags.rest.length > 0 ? flags.rest : ["."]) {
|
|
@@ -58,42 +121,57 @@ export async function checkCommand(deps, args) {
|
|
|
58
121
|
folders.push(...found);
|
|
59
122
|
}
|
|
60
123
|
let failed = false;
|
|
124
|
+
const report = [];
|
|
61
125
|
for (const folder of folders) {
|
|
62
|
-
|
|
63
|
-
|
|
126
|
+
let pack = await packed(folder);
|
|
127
|
+
let compiled = await checkFolder(pack);
|
|
128
|
+
const fixed = { fixed: [], unfixed: [] };
|
|
129
|
+
// Checked again after each round, so what is printed is the folder as it is now. The compiler
|
|
130
|
+
// stops at a stage with errors, so a fix can show the next stage's: a few rounds take them.
|
|
131
|
+
for (let round = 0; fix && round < FIX_ROUNDS; round++) {
|
|
132
|
+
const made = await fixFolder(pack, compiled.diagnostics, cwd);
|
|
133
|
+
fixed.fixed.push(...made.fixed);
|
|
134
|
+
fixed.unfixed = made.unfixed;
|
|
135
|
+
if (made.fixed.length === 0)
|
|
136
|
+
break;
|
|
137
|
+
pack = await packed(folder);
|
|
138
|
+
compiled = await checkFolder(pack);
|
|
139
|
+
}
|
|
140
|
+
if (!compiled.manifest)
|
|
141
|
+
failed = true;
|
|
142
|
+
if (json) {
|
|
143
|
+
report.push({
|
|
144
|
+
path: relative(cwd, folder) || ".",
|
|
145
|
+
ok: compiled.manifest !== undefined,
|
|
146
|
+
...(compiled.manifest ? { manifest: compiled.manifest } : {}),
|
|
147
|
+
diagnostics: compiled.diagnostics,
|
|
148
|
+
...(pack.skipped.length > 0 ? { skipped: pack.skipped } : {}),
|
|
149
|
+
...(fix ? { fixed: fixed.fixed, unfixed: fixed.unfixed } : {}),
|
|
150
|
+
});
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const shown = (file) => relative(cwd, join(folder, file));
|
|
154
|
+
for (const item of fixed.fixed)
|
|
155
|
+
deps.out(`fixed ${shown(item.file)}: ${item.change}`);
|
|
156
|
+
for (const item of fixed.unfixed)
|
|
157
|
+
deps.err(`not fixed ${shown(item.file)}: ${item.reason}`);
|
|
64
158
|
for (const skipped of pack.skipped)
|
|
65
159
|
deps.err(`${relative(cwd, folder) || "."}: skipped ${skipped}`);
|
|
66
|
-
for (const line of diagnosticLines(folder, cwd, compiled.diagnostics))
|
|
160
|
+
for (const line of diagnosticLines(folder, cwd, compiled.diagnostics, !fix))
|
|
67
161
|
if (compiled.manifest)
|
|
68
162
|
deps.out(line);
|
|
69
163
|
else
|
|
70
164
|
deps.err(line);
|
|
71
165
|
const label = relative(cwd, folder) || basename(folder);
|
|
166
|
+
const files = `${pack.entries.length} ${pack.entries.length === 1 ? "file" : "files"}`;
|
|
72
167
|
if (compiled.manifest)
|
|
73
|
-
deps.out(`${label}: ok, agent manifest ${compiled.manifest.name} (${
|
|
168
|
+
deps.out(`${label}: ok, agent manifest ${compiled.manifest.name} (${files}, ${compiled.compiler})`);
|
|
74
169
|
else {
|
|
75
|
-
failed = true;
|
|
76
170
|
const errors = compiled.diagnostics.filter((item) => item.severity === "error").length;
|
|
77
171
|
deps.err(`${label}: ${errors} ${errors === 1 ? "error" : "errors"}`);
|
|
78
172
|
}
|
|
79
173
|
}
|
|
174
|
+
if (json)
|
|
175
|
+
deps.out(JSON.stringify({ folders: report }, null, 2));
|
|
80
176
|
return failed ? 1 : 0;
|
|
81
177
|
}
|
|
82
|
-
/** `nylorun init <name> [--description <text>]` */
|
|
83
|
-
export async function initCommand(deps, args) {
|
|
84
|
-
const flags = parseStackFlags(args, { values: ["--description"] }, initUsage);
|
|
85
|
-
const [name, ...rest] = flags.rest;
|
|
86
|
-
if (name === undefined || rest.length > 0)
|
|
87
|
-
throw usageError(`Usage: ${initUsage.split("\n")[0]}`);
|
|
88
|
-
if (!isAgentManifestName(name))
|
|
89
|
-
throw usageError(`${name} is not an agent's name: 1-64 letters, digits, '.', '_' and '-', starting with a letter or digit.`);
|
|
90
|
-
const dir = resolve(deps.cwd ?? process.cwd(), name);
|
|
91
|
-
const file = join(dir, AGENT_FOLDER_FILE);
|
|
92
|
-
if (existsSync(file))
|
|
93
|
-
throw new CliError(`${relative(deps.cwd ?? process.cwd(), file)} exists already.`, 1);
|
|
94
|
-
const description = flags.values.get("--description") ?? `Describe what ${name} does in one sentence.`;
|
|
95
|
-
await mkdir(dir, { recursive: true });
|
|
96
|
-
await writeFile(file, `---\nname: ${name}\ndescription: ${JSON.stringify(description)}\n---\nYou are ${name}. Write the agent's instructions here, in Markdown.\n`, { flag: "wx" });
|
|
97
|
-
deps.out(`Wrote ${relative(deps.cwd ?? process.cwd(), file)}. Check it with nylorun check ${name}, and save it with nylorun apply ${name}.`);
|
|
98
|
-
return 0;
|
|
99
|
-
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type StackDeps } from "../stack/commands.js";
|
|
2
|
+
export declare const initUsage = "nylorun init <path> [--description <text>] [--example]\n\nWrites <path>/AGENT.md, the smallest agent folder: frontmatter with its name (the path's last\nsegment, so init agents/hello names it hello) and description, and its instructions below.\n\n --example also write tools/get_order.json, mcp.json and\n triggers/new-order.md, with a README.md that explains each\n\nThe first run in a project (the nearest directory with .git, else this one) also writes\n.gitattributes (* text=auto eol=lf), ignores .nylorun/credentials.json in .gitignore, and maps the\nfolder files to their JSON Schemas in .vscode/settings.json, each only when it is missing.";
|
|
3
|
+
/** Where the editor finds each agent folder file's schema (design §3.10). */
|
|
4
|
+
export declare const SCHEMA_BASE = "https://docs.nylorun.com/schemas/agent-folder/v1";
|
|
5
|
+
/** The project `init` sets up: the nearest directory from `cwd` up with `.git`, else `cwd`. */
|
|
6
|
+
export declare function projectRootOf(cwd: string): string;
|
|
7
|
+
/** `json.schemas` entries for the agent folders that sit where `folder` does, under `root`. */
|
|
8
|
+
export declare function schemaMappings(root: string, folder: string): {
|
|
9
|
+
fileMatch: string[];
|
|
10
|
+
url: string;
|
|
11
|
+
}[];
|
|
12
|
+
/**
|
|
13
|
+
* `settings` with `mappings` merged into its `json.schemas`: a mapping whose URL is there already
|
|
14
|
+
* gains the patterns it lacks. Undefined when nothing changes.
|
|
15
|
+
*/
|
|
16
|
+
export declare function mergeSchemaSettings(settings: Record<string, unknown>, mappings: readonly {
|
|
17
|
+
fileMatch: string[];
|
|
18
|
+
url: string;
|
|
19
|
+
}[]): Record<string, unknown> | undefined;
|
|
20
|
+
/** `nylorun init <path> [--description <text>] [--example]` */
|
|
21
|
+
export declare function initCommand(deps: StackDeps, args: readonly string[]): Promise<number>;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nylorun init <path>` (tracks T6 and T7): writes a new agent folder's `AGENT.md`, named after
|
|
3
|
+
* the path's last segment (`init agents/hello` names it `hello`). `--example` adds a tool, an
|
|
4
|
+
* MCP server and a trigger, each explained in the folder's README.md, and the result passes
|
|
5
|
+
* `nylorun check`. It also sets up the project, at the nearest directory with `.git` (else the
|
|
6
|
+
* working directory), once:
|
|
7
|
+
*
|
|
8
|
+
* - `.gitattributes` with `* text=auto eol=lf` when there is none, so a Windows checkout keeps
|
|
9
|
+
* LF line endings;
|
|
10
|
+
* - `.nylorun/credentials.json` in `.gitignore` when nothing ignores it yet;
|
|
11
|
+
* - the editor's schema mapping (design §3.10): `.vscode/settings.json` `json.schemas`, merged into
|
|
12
|
+
* the file, which is left alone when it is not plain JSON.
|
|
13
|
+
*
|
|
14
|
+
* It reads no Runtime.
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync } from "node:fs";
|
|
17
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
18
|
+
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
19
|
+
import { isAgentManifestName } from "@nylorun/core/contracts";
|
|
20
|
+
import { AGENT_FOLDER_FILE, ignoreMatcher } from "@nylorun/agents";
|
|
21
|
+
import { CliError } from "../errors.js";
|
|
22
|
+
import { parseStackFlags } from "../stack/commands.js";
|
|
23
|
+
import { usageError } from "./runtime.js";
|
|
24
|
+
export const initUsage = `nylorun init <path> [--description <text>] [--example]
|
|
25
|
+
|
|
26
|
+
Writes <path>/AGENT.md, the smallest agent folder: frontmatter with its name (the path's last
|
|
27
|
+
segment, so init agents/hello names it hello) and description, and its instructions below.
|
|
28
|
+
|
|
29
|
+
--example also write tools/get_order.json, mcp.json and
|
|
30
|
+
triggers/new-order.md, with a README.md that explains each
|
|
31
|
+
|
|
32
|
+
The first run in a project (the nearest directory with .git, else this one) also writes
|
|
33
|
+
.gitattributes (* text=auto eol=lf), ignores .nylorun/credentials.json in .gitignore, and maps the
|
|
34
|
+
folder files to their JSON Schemas in .vscode/settings.json, each only when it is missing.`;
|
|
35
|
+
/** Where the editor finds each agent folder file's schema (design §3.10). */
|
|
36
|
+
export const SCHEMA_BASE = "https://docs.nylorun.com/schemas/agent-folder/v1";
|
|
37
|
+
/** The project `init` sets up: the nearest directory from `cwd` up with `.git`, else `cwd`. */
|
|
38
|
+
export function projectRootOf(cwd) {
|
|
39
|
+
for (let directory = resolve(cwd);; directory = dirname(directory)) {
|
|
40
|
+
if (existsSync(join(directory, ".git")))
|
|
41
|
+
return directory;
|
|
42
|
+
if (dirname(directory) === directory)
|
|
43
|
+
return resolve(cwd);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** `json.schemas` entries for the agent folders that sit where `folder` does, under `root`. */
|
|
47
|
+
export function schemaMappings(root, folder) {
|
|
48
|
+
const parent = relative(root, dirname(folder)).split(sep).join("/");
|
|
49
|
+
// Folders under a parent (agents/) share one pattern; a folder at the root is named alone.
|
|
50
|
+
const base = parent === "" ? relative(root, folder).split(sep).join("/") : `${parent}/**`;
|
|
51
|
+
return [
|
|
52
|
+
{ fileMatch: [`${base}/runtime.json`], url: `${SCHEMA_BASE}/runtime.json` },
|
|
53
|
+
{ fileMatch: [`${base}/tools/*.json`], url: `${SCHEMA_BASE}/tool.json` },
|
|
54
|
+
{ fileMatch: [`${base}/flow.json`], url: `${SCHEMA_BASE}/flow.json` },
|
|
55
|
+
{ fileMatch: [`${base}/mcp.json`, `!${base}/plugins/**`], url: `${SCHEMA_BASE}/mcp.json` },
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
59
|
+
/**
|
|
60
|
+
* `settings` with `mappings` merged into its `json.schemas`: a mapping whose URL is there already
|
|
61
|
+
* gains the patterns it lacks. Undefined when nothing changes.
|
|
62
|
+
*/
|
|
63
|
+
export function mergeSchemaSettings(settings, mappings) {
|
|
64
|
+
const existing = Array.isArray(settings["json.schemas"]) ? settings["json.schemas"] : [];
|
|
65
|
+
const schemas = existing.map((item) => (isObject(item) ? { ...item } : item));
|
|
66
|
+
let changed = !Array.isArray(settings["json.schemas"]);
|
|
67
|
+
for (const mapping of mappings) {
|
|
68
|
+
const found = schemas.find((item) => isObject(item) && item.url === mapping.url);
|
|
69
|
+
if (!found) {
|
|
70
|
+
schemas.push({ fileMatch: [...mapping.fileMatch], url: mapping.url });
|
|
71
|
+
changed = true;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const patterns = Array.isArray(found.fileMatch) ? found.fileMatch : [];
|
|
75
|
+
const missing = mapping.fileMatch.filter((pattern) => !patterns.includes(pattern));
|
|
76
|
+
if (missing.length === 0)
|
|
77
|
+
continue;
|
|
78
|
+
found.fileMatch = [...patterns, ...missing];
|
|
79
|
+
changed = true;
|
|
80
|
+
}
|
|
81
|
+
return changed ? { ...settings, "json.schemas": schemas } : undefined;
|
|
82
|
+
}
|
|
83
|
+
/** A file's text, or undefined when there is none. */
|
|
84
|
+
const textOf = (path) => readFile(path, "utf8").catch(() => undefined);
|
|
85
|
+
/** Sets up the project at `root` for the agent folder `folder`; each line says what it wrote. */
|
|
86
|
+
async function setUpProject(root, folder, show) {
|
|
87
|
+
const notes = [];
|
|
88
|
+
const attributes = join(root, ".gitattributes");
|
|
89
|
+
if (!existsSync(attributes)) {
|
|
90
|
+
await writeFile(attributes, "* text=auto eol=lf\n", { flag: "wx" });
|
|
91
|
+
notes.push(`Wrote ${show(attributes)} (* text=auto eol=lf), so a Windows checkout keeps LF line endings.`);
|
|
92
|
+
}
|
|
93
|
+
const ignore = join(root, ".gitignore");
|
|
94
|
+
const ignoreText = (await textOf(ignore)) ?? "";
|
|
95
|
+
if (!ignoreMatcher(ignoreText)(".nylorun/credentials.json")) {
|
|
96
|
+
const separator = ignoreText === "" || ignoreText.endsWith("\n") ? "" : "\n";
|
|
97
|
+
await writeFile(ignore, `${ignoreText}${separator}.nylorun/credentials.json\n`);
|
|
98
|
+
notes.push(`Added .nylorun/credentials.json to ${show(ignore)}.`);
|
|
99
|
+
}
|
|
100
|
+
const settingsPath = join(root, ".vscode", "settings.json");
|
|
101
|
+
const settingsText = await textOf(settingsPath);
|
|
102
|
+
let settings = {};
|
|
103
|
+
if (settingsText !== undefined) {
|
|
104
|
+
try {
|
|
105
|
+
settings = JSON.parse(settingsText);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
settings = undefined;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const mappings = schemaMappings(root, folder);
|
|
112
|
+
const merged = isObject(settings) ? mergeSchemaSettings(settings, mappings) : undefined;
|
|
113
|
+
if (!isObject(settings))
|
|
114
|
+
notes.push(`${show(settingsPath)} is not plain JSON, so it was left alone: add the agent folder schemas to its json.schemas (${SCHEMA_BASE}/<kind>.json).`);
|
|
115
|
+
else if (merged) {
|
|
116
|
+
await mkdir(dirname(settingsPath), { recursive: true });
|
|
117
|
+
await writeFile(settingsPath, `${JSON.stringify(merged, null, 2)}\n`);
|
|
118
|
+
notes.push(`Mapped the agent folder files to their JSON Schemas in ${show(settingsPath)}.`);
|
|
119
|
+
}
|
|
120
|
+
return notes;
|
|
121
|
+
}
|
|
122
|
+
/** The files `--example` adds, by path in the folder. */
|
|
123
|
+
function exampleFiles(name) {
|
|
124
|
+
const tool = {
|
|
125
|
+
description: "Look up one order by its id: its status, items and total.",
|
|
126
|
+
inputSchema: {
|
|
127
|
+
type: "object",
|
|
128
|
+
properties: { orderId: { type: "string", description: "The order's id, such as A-1001." } },
|
|
129
|
+
required: ["orderId"],
|
|
130
|
+
additionalProperties: false,
|
|
131
|
+
},
|
|
132
|
+
http: { url: "https://orders.example.com/lookup", method: "POST", timeoutMs: 15000, credential: "ORDERS_TOKEN" },
|
|
133
|
+
};
|
|
134
|
+
const mcp = {
|
|
135
|
+
mcpServers: {
|
|
136
|
+
orders: {
|
|
137
|
+
type: "streamable-http",
|
|
138
|
+
url: "https://orders.example.com/mcp",
|
|
139
|
+
headers: { Authorization: "Bearer ${ORDERS_MCP_TOKEN}" },
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
const trigger = `---
|
|
144
|
+
# A trigger starts a turn with no person in the chat. This one is a webhook: the Runtime gives it
|
|
145
|
+
# a URL and a signing secret (nylorun agents triggers new-secret ${name} new-order), and each signed
|
|
146
|
+
# POST runs the prompt below.
|
|
147
|
+
# On a schedule instead: schedule: "0 9 * * 1-5" with timezone: Europe/London, and no webhook.
|
|
148
|
+
# Once, at a set time: at: 2026-11-01T09:00:00+05:30, and no webhook.
|
|
149
|
+
webhook: standard-webhooks
|
|
150
|
+
# Each run opens a new session. A session id here sends every run to that one session.
|
|
151
|
+
session: new
|
|
152
|
+
---
|
|
153
|
+
A new order arrived. Look it up with get_order and summarize it in two sentences.
|
|
154
|
+
`;
|
|
155
|
+
const readme = `# ${name}
|
|
156
|
+
|
|
157
|
+
An agent folder (\`nylorun init --example\`). \`nylorun check\` compiles it offline, and
|
|
158
|
+
\`nylorun apply --agent\` saves it and makes the Agent \`${name}\`. Every file but this one is part
|
|
159
|
+
of the agent; the example ones are placeholders to replace or delete.
|
|
160
|
+
|
|
161
|
+
- \`AGENT.md\`: the name and description (frontmatter), then the instructions.
|
|
162
|
+
- \`tools/get_order.json\`: an HTTP tool. The file name is the tool's name; the model reads its
|
|
163
|
+
\`description\` and \`inputSchema\`. \`http.credential\` names a secret slot, \`ORDERS_TOKEN\`, sent as
|
|
164
|
+
a bearer token: set it with \`nylorun agents secrets put ${name} ORDERS_TOKEN\` (piped in).
|
|
165
|
+
- \`mcp.json\`: MCP servers, in Agent Plugins syntax. \`\${ORDERS_MCP_TOKEN}\` in a header is a secret
|
|
166
|
+
slot too, never a value: never write a literal token here.
|
|
167
|
+
- \`triggers/new-order.md\`: a trigger. The frontmatter says when it fires (\`webhook\`, \`schedule\` or
|
|
168
|
+
\`at\`) and which session it runs in; the body is the prompt. Its comments show the other kinds.
|
|
169
|
+
|
|
170
|
+
Until a required secret is set, the Agent lists it under \`attention\` (\`nylorun agents get ${name}\`).
|
|
171
|
+
`;
|
|
172
|
+
return {
|
|
173
|
+
"tools/get_order.json": `${JSON.stringify(tool, null, 2)}\n`,
|
|
174
|
+
"mcp.json": `${JSON.stringify(mcp, null, 2)}\n`,
|
|
175
|
+
"triggers/new-order.md": trigger,
|
|
176
|
+
"README.md": readme,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/** `nylorun init <path> [--description <text>] [--example]` */
|
|
180
|
+
export async function initCommand(deps, args) {
|
|
181
|
+
const flags = parseStackFlags(args, { values: ["--description"], booleans: ["--example"] }, initUsage);
|
|
182
|
+
const [path, ...rest] = flags.rest;
|
|
183
|
+
if (path === undefined || rest.length > 0)
|
|
184
|
+
throw usageError(`Usage: ${initUsage.split("\n")[0]}`);
|
|
185
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
186
|
+
const dir = resolve(cwd, path);
|
|
187
|
+
const name = basename(dir);
|
|
188
|
+
if (!isAgentManifestName(name))
|
|
189
|
+
throw usageError(`${name} is not an agent's name: 1-64 letters, digits, '.', '_' and '-', starting with a letter or digit.`);
|
|
190
|
+
const show = (file) => relative(cwd, file) || ".";
|
|
191
|
+
const file = join(dir, AGENT_FOLDER_FILE);
|
|
192
|
+
const example = flags.booleans.has("--example");
|
|
193
|
+
const files = {
|
|
194
|
+
[AGENT_FOLDER_FILE]: `---\nname: ${name}\ndescription: ${JSON.stringify(flags.values.get("--description") ?? `Describe what ${name} does in one sentence.`)}\n---\nYou are ${name}. Write the agent's instructions here, in Markdown.\n${example ? "\nLook an order up with get_order before you answer a question about it.\n" : ""}`,
|
|
195
|
+
...(example ? exampleFiles(name) : {}),
|
|
196
|
+
};
|
|
197
|
+
for (const relativePath of Object.keys(files))
|
|
198
|
+
if (existsSync(join(dir, relativePath)))
|
|
199
|
+
throw new CliError(`${show(join(dir, relativePath))} exists already.`, 1);
|
|
200
|
+
for (const [relativePath, text] of Object.entries(files)) {
|
|
201
|
+
await mkdir(dirname(join(dir, relativePath)), { recursive: true });
|
|
202
|
+
await writeFile(join(dir, relativePath), text, { flag: "wx" });
|
|
203
|
+
}
|
|
204
|
+
deps.out(example ? `Wrote ${show(dir)}/: ${Object.keys(files).join(", ")}.` : `Wrote ${show(file)}.`);
|
|
205
|
+
for (const note of await setUpProject(projectRootOf(cwd), dir, show))
|
|
206
|
+
deps.out(note);
|
|
207
|
+
const at = show(dir);
|
|
208
|
+
deps.out(`Next: nylorun check ${at}, then nylorun apply ${at} --agent to save it and make the Agent ${name}, then nylorun chat ${name}.`);
|
|
209
|
+
return 0;
|
|
210
|
+
}
|
package/dist/client/migrate.js
CHANGED
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
*
|
|
14
14
|
* It writes nothing over an existing file, reads nothing from a Runtime, and prints the gaps
|
|
15
15
|
* `nylorun apply` would refuse, to fix in the files first.
|
|
16
|
+
*
|
|
17
|
+
* `nylorun migrate folder <dir>` (`convert.ts`, track T7) writes the agent folder of a three-file
|
|
18
|
+
* folder.
|
|
16
19
|
*/
|
|
17
20
|
import { existsSync } from "node:fs";
|
|
18
21
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
@@ -22,6 +25,7 @@ import { AgentManifestSchema, RuntimeManifestFileSchema, WorkflowManifestSchema,
|
|
|
22
25
|
import { harnessSectionOf, sectionGaps } from "@nylorun/core/define";
|
|
23
26
|
import { CliError } from "../errors.js";
|
|
24
27
|
import { parseStackFlags } from "../stack/commands.js";
|
|
28
|
+
import { migrateFolderCommand, migrateFolderUsage } from "./convert.js";
|
|
25
29
|
import { readJson, usageError } from "./runtime.js";
|
|
26
30
|
export const migrateUsage = `nylorun migrate v8 <agent.json> [--runtime <environment-manifest.json>] [--name <name>] [--out <dir>]
|
|
27
31
|
|
|
@@ -65,11 +69,13 @@ function hasSkills(loop) {
|
|
|
65
69
|
export async function migrateCommand(deps, args) {
|
|
66
70
|
const [version, ...rest] = args;
|
|
67
71
|
if (version === "--help" || version === "-h") {
|
|
68
|
-
deps.out(migrateUsage);
|
|
72
|
+
deps.out(`${migrateUsage}\n\n${migrateFolderUsage}`);
|
|
69
73
|
return 0;
|
|
70
74
|
}
|
|
75
|
+
if (version === "folder")
|
|
76
|
+
return migrateFolderCommand(deps, rest);
|
|
71
77
|
if (version !== "v8")
|
|
72
|
-
throw usageError(version === undefined ? migrateUsage : `nylorun migrate knows v8 only.\n${migrateUsage}`);
|
|
78
|
+
throw usageError(version === undefined ? migrateUsage : `nylorun migrate knows v8 and folder only.\n${migrateUsage}\n\n${migrateFolderUsage}`);
|
|
73
79
|
const flags = parseStackFlags(rest, { values: ["--runtime", "--name", "--out"] }, migrateUsage);
|
|
74
80
|
if (flags.rest.length !== 1)
|
|
75
81
|
throw usageError(`Usage: ${migrateUsage.split("\n")[0]}`);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The pi-ai `nylorun tenant models set` loads; nylorun's devDependency has the same version
|
|
3
3
|
* (scripts/check-package.mjs).
|
|
4
4
|
*/
|
|
5
|
-
export declare const PI_AI_VERSION = "1.
|
|
5
|
+
export declare const PI_AI_VERSION = "1.3.0";
|
|
6
6
|
/** What `nylorun tenant models set` uses of pi-ai: its provider registry and login flows. */
|
|
7
7
|
export interface PiAi {
|
|
8
8
|
createProvider: typeof import("@earendil-works/pi-ai").createProvider;
|
|
@@ -10,7 +10,7 @@ import { CliError } from "../../errors.js";
|
|
|
10
10
|
* The pi-ai `nylorun tenant models set` loads; nylorun's devDependency has the same version
|
|
11
11
|
* (scripts/check-package.mjs).
|
|
12
12
|
*/
|
|
13
|
-
export const PI_AI_VERSION = "1.
|
|
13
|
+
export const PI_AI_VERSION = "1.3.0";
|
|
14
14
|
const PI_AI = "@earendil-works/pi-ai";
|
|
15
15
|
/** The module written beside an on-demand install, which resolves pi-ai from there. */
|
|
16
16
|
const ENTRY = "pi-ai.js";
|
package/dist/client/models.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type StackDeps } from "../stack/commands.js";
|
|
2
|
-
export declare const modelsUsage = " models catalog [--tenant <name>] [--json]\n every provider and model the Runtime knows\n models providers [--tenant <name>] [--json]\n the providers with a credential, and the one in use\n models get [--tenant <name>] [--json]\n the Tenant's model\n models set [--tenant <name>] sign in to a model provider and set the Tenant's model\n models set --provider <id> --model <id> [--base-url <url>] [--tenant <name>]\n the same with an API key piped in on stdin (CI)\n models select <provider> [--model <id>] [--tenant <name>]\n use a provider that has a credential (default: its model)";
|
|
2
|
+
export declare const modelsUsage = " models catalog [--tenant <name>] [--json]\n every provider and model the Runtime knows\n models providers [--tenant <name>] [--json]\n the providers with a credential, and the one in use\n models get [--tenant <name>] [--json]\n the Tenant's model\n models set [--tenant <name>] sign in to a model provider and set the Tenant's model\n models set --provider <id> --model <id> [--base-url <url>] [--tenant <name>]\n the same with an API key piped in on stdin (CI)\n models set --provider google-vertex --model <id> --host-identity --project <id> --location <location> [--tenant <name>]\n the same with the Host's Google identity, no key: on a Host\n whose operator sets NYLORUN_MODEL_HOST_IDENTITY\n models select <provider> [--model <id>] [--tenant <name>]\n use a provider that has a credential (default: its model)";
|
|
3
3
|
export declare const usageUsage = " usage [--scope tenant|manifest|turn] [--id <id>] [--period day|month|total] [--tenant <name>]\n what the model calls used and cost (JSON); --scope\n manifest: the Agents made from agent manifest <id>\n calls [--after <cursor>] [--limit <n>] [--tenant <name>]\n every recorded model call, one JSON line each, until\n caught up; prints the cursor to resume from on stderr";
|
|
4
4
|
/** `nylorun tenant models <verb> ...` */
|
|
5
5
|
export declare function modelsCommand(deps: StackDeps, args: readonly string[]): Promise<number>;
|
package/dist/client/models.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `nylorun tenant models | usage | calls`: the Tenant's model, what its model calls used, and the
|
|
3
3
|
* calls themselves, through the Management API. `models set` signs in with pi-ai (loaded only
|
|
4
|
-
* there) at a terminal; with `--provider` and `--model` it takes an API key piped in instead,
|
|
4
|
+
* there) at a terminal; with `--provider` and `--model` it takes an API key piped in instead, or
|
|
5
|
+
* with `--host-identity` the Host's own Google identity (ADR-71),
|
|
5
6
|
* for CI. A key is never an argument.
|
|
6
7
|
*/
|
|
7
8
|
import { randomUUID } from "node:crypto";
|
|
@@ -18,6 +19,9 @@ export const modelsUsage = ` models catalog [--tenant <name>] [--json]
|
|
|
18
19
|
models set [--tenant <name>] sign in to a model provider and set the Tenant's model
|
|
19
20
|
models set --provider <id> --model <id> [--base-url <url>] [--tenant <name>]
|
|
20
21
|
the same with an API key piped in on stdin (CI)
|
|
22
|
+
models set --provider google-vertex --model <id> --host-identity --project <id> --location <location> [--tenant <name>]
|
|
23
|
+
the same with the Host's Google identity, no key: on a Host
|
|
24
|
+
whose operator sets NYLORUN_MODEL_HOST_IDENTITY
|
|
21
25
|
models select <provider> [--model <id>] [--tenant <name>]
|
|
22
26
|
use a provider that has a credential (default: its model)`;
|
|
23
27
|
export const usageUsage = ` usage [--scope tenant|manifest|turn] [--id <id>] [--period day|month|total] [--tenant <name>]
|
|
@@ -75,18 +79,40 @@ async function read(deps, verb, args) {
|
|
|
75
79
|
}
|
|
76
80
|
return 0;
|
|
77
81
|
}
|
|
78
|
-
const SET_USAGE = "nylorun tenant models set [--tenant <name>] | nylorun tenant models set --provider <id> --model <id> [--base-url <url>] [--tenant <name>]";
|
|
82
|
+
const SET_USAGE = "nylorun tenant models set [--tenant <name>] | nylorun tenant models set --provider <id> --model <id> [--base-url <url>] [--tenant <name>] | nylorun tenant models set --provider google-vertex --model <id> --host-identity --project <id> --location <location> [--tenant <name>]";
|
|
79
83
|
/**
|
|
80
84
|
* `models set`: at a terminal, the sign-in prompts; with `--provider` and `--model`, the API key
|
|
81
|
-
* on stdin, set as `PUT /v1/tenant/model` with an `api_key` credential (OAuth needs the prompts)
|
|
85
|
+
* on stdin, set as `PUT /v1/tenant/model` with an `api_key` credential (OAuth needs the prompts),
|
|
86
|
+
* or with `--host-identity` a `host_identity` credential: no key, only `--project` and `--location`.
|
|
82
87
|
*/
|
|
83
88
|
async function set(deps, args) {
|
|
84
|
-
const flags = parseStackFlags(args, {
|
|
89
|
+
const flags = parseStackFlags(args, {
|
|
90
|
+
values: ["--tenant", "--provider", "--model", "--base-url", "--project", "--location"],
|
|
91
|
+
booleans: ["--host-identity"],
|
|
92
|
+
}, SET_USAGE);
|
|
85
93
|
if (flags.rest.length)
|
|
86
94
|
throw usageError(`Usage: ${SET_USAGE}`);
|
|
87
95
|
const provider = flags.values.get("--provider");
|
|
88
96
|
const model = flags.values.get("--model");
|
|
89
97
|
const baseUrl = flags.values.get("--base-url");
|
|
98
|
+
const project = flags.values.get("--project");
|
|
99
|
+
const location = flags.values.get("--location");
|
|
100
|
+
const hostIdentity = flags.booleans.has("--host-identity");
|
|
101
|
+
if (hostIdentity || project !== undefined || location !== undefined) {
|
|
102
|
+
if (!hostIdentity || provider === undefined || model === undefined || !project || !location)
|
|
103
|
+
throw usageError(`Pass --host-identity with --provider, --model, --project and --location. Usage: ${SET_USAGE}`);
|
|
104
|
+
if (baseUrl !== undefined)
|
|
105
|
+
throw usageError("--host-identity takes no --base-url");
|
|
106
|
+
const admin = await client(deps, flags.values.get("--tenant"));
|
|
107
|
+
const view = await admin.models.put({
|
|
108
|
+
idempotencyKey: randomUUID(),
|
|
109
|
+
provider,
|
|
110
|
+
model,
|
|
111
|
+
auth: { type: "host_identity", project, location },
|
|
112
|
+
});
|
|
113
|
+
deps.out(describe(view));
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
90
116
|
if (provider !== undefined || model !== undefined || baseUrl !== undefined) {
|
|
91
117
|
if (provider === undefined || model === undefined)
|
|
92
118
|
throw usageError(`Pass both --provider and --model. Usage: ${SET_USAGE}`);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The text diff `nylorun apply` prints under a plan's change to the instructions (track T7): the
|
|
3
|
+
* plan carries the instructions before and after (`from`, `to`), and the lines that differ print
|
|
4
|
+
* as `-` and `+`, without the lines both keep. Pure.
|
|
5
|
+
*/
|
|
6
|
+
import type { AgentManifestPlan } from "@nylorun/agents";
|
|
7
|
+
/**
|
|
8
|
+
* The lines that differ between `before` and `after`, in order: `- line` for a removed one and
|
|
9
|
+
* `+ line` for an added one, with `…` between changes that are apart. A longest common
|
|
10
|
+
* subsequence of lines, so a moved block shows as removed and added.
|
|
11
|
+
*/
|
|
12
|
+
export declare function lineDiff(before: string, after: string): string[];
|
|
13
|
+
/** The diff lines to print under one of a plan's changes: the instructions' text, else none. */
|
|
14
|
+
export declare function changeDiffLines(change: AgentManifestPlan["changes"][number], indent?: string): string[];
|