@nylorun/runtime 0.3.0-beta → 0.4.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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0-beta
4
+
5
+ ### Minor Changes
6
+
7
+ - fa1860a: Use standard MODEL_PROVIDER, MODEL, MODEL_PROVIDER_API_KEY, and MODEL_PROVIDER_BASE_URL environment configuration. Export starter Hono apps and provide CLI development and production Node launchers. Existing starters require manual migration. Release preparation must update the creator Runtime compatibility pin with this release.
8
+
3
9
  ## 0.3.0-beta
4
10
 
5
11
  ### Minor Changes
package/README.md CHANGED
@@ -29,18 +29,37 @@ Pass explicit `basePath` when a reverse proxy rewrites the public prefix.
29
29
 
30
30
  - `nylorun configure`
31
31
  - `nylorun dev [--no-studio] [--no-open]`
32
+ - `nylorun start [entry]` (default: `dist/src/index.js`)
32
33
  - `nylorun studio --agent-url http://localhost:3000/agents [--port 4161] [--no-open]`
33
34
 
34
- Studio attaches to an application you run. Use your own TypeScript/build tooling and a Node adapter such as `@hono/node-server` when applicable. `projectAsset("agents/skills/catalog")` resolves bundled application assets from source or a compiled `dist/` deployment.
35
+ Export your Hono application with `export default app`. The CLI supplies the Node server adapter. `nylorun dev` watches `src/index.ts`, waits for `/agents/v1/agents`, and starts project-local Studio. `PORT` defaults to 3000. `--no-studio` runs only the application; `--no-open` keeps the browser closed. Ctrl-C stops both processes. `nylorun start` serves the built app without Studio or development CORS.
35
36
 
36
- Provider credentials are stored in `.env/auth.json`, and selection in `.env/model.json`. `nylorun configure` can run before an agent graph is importable.
37
+ ### Environment configuration
37
38
 
38
- `nylorun dev` runs project-local `tsx watch src/index.ts`, waits for `/agents/v1/agents`, then starts project-local Studio. `PORT` defaults to 3000. `--no-studio` runs just the application; `--no-open` keeps the browser closed. Both flags can be combined. Ctrl-C stops both processes.
39
+ Copy `.env.example` to `.env` and fill it in, or run `nylorun configure` before your agent graph is importable:
40
+
41
+ ```dotenv
42
+ MODEL_PROVIDER=custom
43
+ MODEL=your-model-id
44
+ MODEL_PROVIDER_API_KEY=your-key
45
+ MODEL_PROVIDER_BASE_URL=https://your-provider.example/v1
46
+ ```
47
+
48
+ `MODEL_PROVIDER_BASE_URL` is required only for `MODEL_PROVIDER=custom`; omit it for built-in providers. `configure`, `dev`, and `start` load `.env` before importing the app. Existing process variables win; a missing `.env` is valid. `.env.local` is ignored by Git but is not automatically loaded. Direct imports of Runtime do not load dotenv files.
49
+
50
+ Explicit `piModel({ selection })` options take precedence over environment selection. Environment selection takes precedence over legacy files; incomplete selection produces an error. `MODEL_PROVIDER_API_KEY` overrides provider-native variables such as `OPENAI_API_KEY` and `ANTHROPIC_API_KEY`, which override stored credentials. API-key deployments require no credential files. Supply the same variables through your hosting provider's environment settings.
51
+
52
+ The optional wizard writes selection and entered API keys to `.env`, preserving unrelated configuration. It reuses environment credentials without copying them into the file. OAuth is an alternative where supported; its credentials and refresh state live in ignored `.nylorun/auth.json`. Keep `.env` and `.nylorun/` private.
39
53
 
40
54
  ### Upgrading an existing starter
41
55
 
42
- After upgrading Runtime to a release containing `nylorun dev`, change the development script to `"dev": "nylorun dev"` and remove `dev:app` and `scripts/dev.mjs`. Remove the duplicated `basePath` option for normal Hono mounts.
56
+ Migration is manual; the CLI will not replace a legacy `.env` directory.
57
+
58
+ 1. Back up the existing `.env/` directory to a private location outside the project before replacing it with a file. Preserve any `config/model.json` too.
59
+ 2. Translate `model.json` fields `provider`, `model`, and `custom.baseUrl` into `MODEL_PROVIDER`, `MODEL`, and `MODEL_PROVIDER_BASE_URL`. Copy API keys into `MODEL_PROVIDER_API_KEY` or provider-native variables. Replace the old `NYLO_CUSTOM_API_KEY` variable with `MODEL_PROVIDER_API_KEY`.
60
+ 3. Merge `integrations.env` variables into `.env`. Move OAuth records into `.nylorun/auth.json`. Add `.env`, `.env.local`, and `.nylorun/` to `.gitignore`; remove the old `.env/` exceptions.
61
+ 4. Replace the entrypoint's `serve(...)` call and Node adapter import with `export default app`. Set scripts to `"dev": "nylorun dev"` and `"start": "nylorun start"`. Remove the application's `@hono/node-server` dependency if unused elsewhere. Keep your build and asset-copy steps.
43
62
 
44
- Run `npm run configure` to move model selection to `.env/model.json`. Runtime reads the legacy `config/model.json` only when the new file is absent. Successful configuration removes the legacy file and its directory only if empty; credentials remain in `.env/auth.json`. Do not set `NYLORUN_DEV` in production.
63
+ Runtime retains legacy `.env/model.json`, `config/model.json`, and `.env/auth.json` reads for existing applications using their own launcher. It does not automatically move or delete these files. Use the matching Runtime release pinned by the new creator; older releases cannot launch an exported app.
45
64
 
46
- The starter now uses one `tsconfig.json` with `rootDir: "."` and `outDir: "dist"`. Remove `noEmit` from that file and use `tsc --noEmit` for checks. Change the build's compiler invocation to `tsc -p tsconfig.json` before deleting `tsconfig.build.json`. Keep your existing asset-copy step. Projects whose checks include tests may retain separate build configuration.
65
+ The exported app follows Hono composition conventions. This change does not establish verified serverless persistence, execution lifetime, or Workers support. `projectAsset()` resolves bundled assets from source or compiled deployments.
package/dist/cli.js CHANGED
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync } from "node:fs";
3
- import { loadEnvFile } from "node:process";
4
2
  import { join } from "node:path";
5
3
  import { pathToFileURL } from "node:url";
6
4
  import { createRequire } from "node:module";
7
5
  import { ConfigurationCancelled, configureProvider, } from "./model/configure.js";
6
+ import { loadProjectEnvironment } from "./environment.js";
8
7
  import { develop } from "./dev.js";
9
- const usage = `nylorun <configure|dev|studio>
8
+ const usage = `nylorun <configure|dev|start|studio>
10
9
  dev [--no-studio] [--no-open]
10
+ start [entry]
11
11
  configure
12
12
  studio --agent-url <http(s)-url> [--port <n>] [--no-open]`;
13
13
  async function startStudio(agentServerUrl, open, port) {
@@ -37,6 +37,14 @@ async function main() {
37
37
  const [command, ...args] = process.argv.slice(2);
38
38
  if (!command || command === "--help" || command === "-h")
39
39
  return void console.log(usage);
40
+ if (["configure", "dev", "start"].includes(command))
41
+ loadProjectEnvironment();
42
+ if (command === "start") {
43
+ if (args.length > 1 || args[0]?.startsWith("--"))
44
+ throw new Error(usage);
45
+ await (await import("./launcher.js")).start(args[0]);
46
+ return;
47
+ }
40
48
  if (command === "dev") {
41
49
  process.exitCode = await develop(args);
42
50
  return;
@@ -48,9 +56,6 @@ async function main() {
48
56
  const cancel = (signal) => controller.abort(new ConfigurationCancelled(signal));
49
57
  process.once("SIGINT", () => cancel("SIGINT"));
50
58
  process.once("SIGTERM", () => cancel("SIGTERM"));
51
- const integrations = join(process.cwd(), ".env", "integrations.env");
52
- if (existsSync(integrations))
53
- loadEnvFile(integrations);
54
59
  await configureProvider({ signal: controller.signal });
55
60
  return;
56
61
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import { start } from "./launcher.js";
2
+ await start(process.argv[2] ?? "src/index.ts", true);
package/dist/dev.js CHANGED
@@ -69,7 +69,12 @@ export async function develop(args) {
69
69
  }));
70
70
  };
71
71
  try {
72
- launch([tsx, "watch", "src/index.ts"]);
72
+ launch([
73
+ tsx,
74
+ "watch",
75
+ fileURLToPath(new URL("./dev-entry.js", import.meta.url)),
76
+ "src/index.ts",
77
+ ]);
73
78
  if (!args.includes("--no-studio")) {
74
79
  const url = `http://127.0.0.1:${port}/agents/v1/agents`;
75
80
  const deadline = Date.now() + 20_000;
@@ -0,0 +1,2 @@
1
+ export declare function loadProjectEnvironment(root?: string): void;
2
+ export declare function saveEnvironment(root: string, updates: Record<string, string | undefined>, signal?: AbortSignal): Promise<void>;
@@ -0,0 +1,64 @@
1
+ import { readFileSync, statSync } from "node:fs";
2
+ import { writeFile, rename, rm } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { randomUUID } from "node:crypto";
5
+ import { loadEnvFile } from "node:process";
6
+ export function loadProjectEnvironment(root = process.cwd()) {
7
+ const file = join(root, ".env");
8
+ try {
9
+ if (statSync(file).isDirectory())
10
+ throw new Error("The .env directory must be migrated manually: back it up, create a .env file with MODEL_PROVIDER, MODEL and MODEL_PROVIDER_API_KEY, and move OAuth credentials to .nylorun/auth.json. See the Runtime migration guide.");
11
+ }
12
+ catch (error) {
13
+ if (error.code === "ENOENT")
14
+ return;
15
+ throw error;
16
+ }
17
+ loadEnvFile(file);
18
+ }
19
+ // Match complete dotenv assignments, including quoted multiline values.
20
+ const assignment = /^(?:export\s+)?([\w]+)[\t ]*=[\t ]*(?:"[^"]*"|'[^']*'|`[^`]*`|[^#\r\n]*)([^\r\n]*)(?:\r?\n|$)/gm;
21
+ export async function saveEnvironment(root, updates, signal) {
22
+ const file = join(root, ".env");
23
+ let contents = "";
24
+ try {
25
+ contents = readFileSync(file, "utf8");
26
+ }
27
+ catch (error) {
28
+ if (error.code !== "ENOENT")
29
+ throw error;
30
+ }
31
+ const encode = (value) => {
32
+ // Node's dotenv parser has no general quote-escaping syntax. Select a
33
+ // delimiter absent from the value rather than changing the credential.
34
+ for (const quote of ["'", '"', "`"]) {
35
+ if (!value.includes(quote) && !(quote === '"' && /\\[nr]/.test(value)))
36
+ return quote + value + quote;
37
+ }
38
+ throw new Error("This value contains all dotenv quote delimiters; set it through your process environment instead.");
39
+ };
40
+ const remaining = new Set(Object.keys(updates));
41
+ contents = contents.replace(assignment, (whole, key, suffix) => {
42
+ if (!(key in updates))
43
+ return whole;
44
+ if (!remaining.delete(key))
45
+ return "";
46
+ return updates[key] === undefined
47
+ ? ""
48
+ : `${key}=${encode(updates[key])}${suffix}\n`;
49
+ });
50
+ if (contents && !contents.endsWith("\n"))
51
+ contents += "\n";
52
+ for (const key of remaining)
53
+ if (updates[key] !== undefined)
54
+ contents += `${key}=${encode(updates[key])}\n`;
55
+ const temporary = join(root, `.env-${randomUUID()}.tmp`);
56
+ try {
57
+ await writeFile(temporary, contents, { mode: 0o600, signal });
58
+ signal?.throwIfAborted();
59
+ await rename(temporary, file);
60
+ }
61
+ finally {
62
+ await rm(temporary, { force: true });
63
+ }
64
+ }
@@ -0,0 +1 @@
1
+ export declare function start(entry?: string, development?: boolean): Promise<void>;
@@ -0,0 +1,28 @@
1
+ import { serve } from "@hono/node-server";
2
+ import { resolve } from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import { loadProjectEnvironment } from "./environment.js";
5
+ export async function start(entry = "dist/src/index.js", development = false) {
6
+ loadProjectEnvironment();
7
+ if (development)
8
+ process.env.NYLORUN_DEV = "1";
9
+ else
10
+ delete process.env.NYLORUN_DEV;
11
+ const port = Number(process.env.PORT ?? "3000");
12
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
13
+ throw new Error("PORT must be an integer between 1 and 65535.");
14
+ const { default: app } = await import(pathToFileURL(resolve(entry)).href);
15
+ if (!app || typeof app.fetch !== "function")
16
+ throw new Error(`${entry} must export a Hono application with 'export default app' and a callable fetch.`);
17
+ const server = serve({ fetch: app.fetch.bind(app), port }, (info) => {
18
+ console.log(`Server is running on http://localhost:${info.port}`);
19
+ });
20
+ const stop = () => {
21
+ process.removeListener("SIGINT", stop);
22
+ process.removeListener("SIGTERM", stop);
23
+ server.close(() => process.exit(0));
24
+ setTimeout(() => process.exit(0), 5_000).unref();
25
+ };
26
+ process.once("SIGINT", stop);
27
+ process.once("SIGTERM", stop);
28
+ }
@@ -2,7 +2,8 @@ import type { Credential, CredentialInfo, CredentialStore } from "@earendil-work
2
2
  export declare class ProjectCredentialStore implements CredentialStore {
3
3
  #private;
4
4
  private readonly file;
5
- constructor(file?: string);
5
+ private readonly legacyFile?;
6
+ constructor(file?: string, legacyFile?: string | undefined);
6
7
  read(providerId: string): Promise<Credential | undefined>;
7
8
  list(): Promise<readonly CredentialInfo[]>;
8
9
  modify(providerId: string, fn: (current: Credential | undefined) => Promise<Credential | undefined>): Promise<Credential | undefined>;
@@ -1,10 +1,13 @@
1
+ var _a;
1
2
  import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
2
3
  import { dirname, join } from "node:path";
3
4
  export class ProjectCredentialStore {
4
5
  file;
6
+ legacyFile;
5
7
  #chain = Promise.resolve();
6
- constructor(file = join(process.cwd(), ".env", "auth.json")) {
8
+ constructor(file = join(process.cwd(), ".nylorun", "auth.json"), legacyFile) {
7
9
  this.file = file;
10
+ this.legacyFile = legacyFile;
8
11
  }
9
12
  async read(providerId) {
10
13
  return (await this.#all())[providerId];
@@ -54,8 +57,11 @@ export class ProjectCredentialStore {
54
57
  return JSON.parse(await readFile(this.file, "utf8"));
55
58
  }
56
59
  catch (error) {
57
- if (error.code === "ENOENT")
60
+ if (["ENOENT", "ENOTDIR"].includes(error.code ?? "")) {
61
+ if (this.legacyFile)
62
+ return new _a(this.legacyFile).#all();
58
63
  return {};
64
+ }
59
65
  throw error;
60
66
  }
61
67
  }
@@ -83,3 +89,4 @@ export class ProjectCredentialStore {
83
89
  }
84
90
  }
85
91
  }
92
+ _a = ProjectCredentialStore;
@@ -1,7 +1,6 @@
1
- import { mkdir, writeFile, rename, rm, rmdir } from "node:fs/promises";
2
- import { randomUUID } from "node:crypto";
3
1
  import { join } from "node:path";
4
2
  import { createInterface } from "node:readline/promises";
3
+ import { saveEnvironment } from "../environment.js";
5
4
  import { ProjectCredentialStore } from "./auth-store.js";
6
5
  import { modelsFor } from "./models.js";
7
6
  export class ConfigurationCancelled extends Error {
@@ -21,7 +20,25 @@ export async function configureProvider(options = {}) {
21
20
  const signal = controller.signal;
22
21
  const forwardAbort = () => controller.abort(options.signal.reason);
23
22
  options.signal?.throwIfAborted();
24
- const store = new ProjectCredentialStore(join(root, ".env", "auth.json"));
23
+ let enteredKey;
24
+ let enteredEnvironment = {};
25
+ const oauthStore = new ProjectCredentialStore(join(root, ".nylorun", "auth.json"), join(root, ".env", "auth.json"));
26
+ const store = {
27
+ read: (id) => oauthStore.read(id),
28
+ list: () => oauthStore.list(),
29
+ delete: (id) => oauthStore.delete(id),
30
+ async modify(id, fn) {
31
+ const next = await fn(await oauthStore.read(id));
32
+ if (next?.type === "api_key") {
33
+ if (next.key === "")
34
+ throw new Error("An API key is required.");
35
+ enteredKey = next.key;
36
+ enteredEnvironment = { ...next.env };
37
+ return next;
38
+ }
39
+ return oauthStore.modify(id, async () => next);
40
+ },
41
+ };
25
42
  const models = modelsFor({ provider: "", model: "" }, store);
26
43
  const providers = models.getProviders();
27
44
  const prompt = createInterface({
@@ -59,7 +76,8 @@ export async function configureProvider(options = {}) {
59
76
  custom: { baseUrl },
60
77
  };
61
78
  const customModels = modelsFor(selection, store);
62
- await customModels.login("custom", "api_key", interaction());
79
+ if (!(await customModels.checkAuth("custom", { signal })))
80
+ await customModels.login("custom", "api_key", interaction());
63
81
  await save(selection);
64
82
  }
65
83
  else {
@@ -72,7 +90,17 @@ export async function configureProvider(options = {}) {
72
90
  if (!model)
73
91
  throw new Error("Choose a listed model.");
74
92
  if (!(await models.checkAuth(chosen.id, { signal }))) {
75
- await models.login(chosen.id, chosen.auth.oauth ? "oauth" : "api_key", interaction());
93
+ let method = chosen.auth.apiKey
94
+ ? "api_key"
95
+ : "oauth";
96
+ if (chosen.auth.apiKey && chosen.auth.oauth) {
97
+ const answer = (await question("Choose authentication: 1. API key (default), 2. OAuth: ")).trim();
98
+ if (answer && !["1", "2"].includes(answer))
99
+ throw new Error("Choose authentication 1 or 2.");
100
+ if (answer === "2")
101
+ method = "oauth";
102
+ }
103
+ await models.login(chosen.id, method, interaction());
76
104
  }
77
105
  await save({ provider: chosen.id, model: model.id });
78
106
  }
@@ -92,34 +120,36 @@ export async function configureProvider(options = {}) {
92
120
  function interaction() {
93
121
  return {
94
122
  signal,
95
- prompt: async (item) => question(item.message + ": "),
96
- notify: (event) => output.write((event.url ?? event.verificationUri ?? event.message) + "\n"),
123
+ prompt: async (item) => {
124
+ if (item.type !== "select")
125
+ return question(item.message + ": ");
126
+ item.options.forEach((option, index) => output.write(`${index + 1}. ${option.label}\n`));
127
+ const answer = (await question(item.message + " ")).trim();
128
+ const option = item.options.find((option) => option.id === answer) ??
129
+ item.options[Number(answer) - 1];
130
+ if (!option)
131
+ throw new Error("Choose a listed authentication option.");
132
+ return option.id;
133
+ },
134
+ notify: (event) => {
135
+ output.write(("url" in event
136
+ ? event.url
137
+ : "verificationUri" in event
138
+ ? event.verificationUri
139
+ : event.message) + "\n");
140
+ },
97
141
  };
98
142
  }
99
143
  async function save(selection) {
100
144
  signal.throwIfAborted();
101
- const directory = join(root, ".env");
102
- const temporary = join(directory, `.model-${randomUUID()}.json`);
103
- try {
104
- await mkdir(directory, { recursive: true });
105
- await writeFile(temporary, JSON.stringify(selection, null, 2) + "\n", {
106
- signal,
107
- });
108
- signal.throwIfAborted();
109
- await rename(temporary, join(directory, "model.json"));
110
- await rm(join(root, "config", "model.json"), { force: true });
111
- try {
112
- await rmdir(join(root, "config"));
113
- }
114
- catch (error) {
115
- if (!(error instanceof Error) ||
116
- !("code" in error) ||
117
- !["ENOENT", "ENOTEMPTY", "EEXIST"].includes(String(error.code)))
118
- throw error;
119
- }
120
- }
121
- finally {
122
- await rm(temporary, { force: true });
123
- }
145
+ await saveEnvironment(root, {
146
+ ...enteredEnvironment,
147
+ MODEL_PROVIDER: selection.provider,
148
+ MODEL: selection.model,
149
+ MODEL_PROVIDER_BASE_URL: selection.custom?.baseUrl,
150
+ ...(enteredKey === undefined
151
+ ? {}
152
+ : { MODEL_PROVIDER_API_KEY: enteredKey }),
153
+ }, signal);
124
154
  }
125
155
  }
@@ -1,4 +1,4 @@
1
- import type { ProjectCredentialStore } from "./auth-store.js";
1
+ import { type CredentialStore } from "@earendil-works/pi-ai";
2
2
  export type Selection = Readonly<{
3
3
  provider: string;
4
4
  model: string;
@@ -6,4 +6,4 @@ export type Selection = Readonly<{
6
6
  baseUrl: string;
7
7
  }>;
8
8
  }>;
9
- export declare function modelsFor(selection: Selection, credentials: ProjectCredentialStore): import("@earendil-works/pi-ai").MutableModels;
9
+ export declare function modelsFor(selection: Selection, credentials: CredentialStore): import("@earendil-works/pi-ai").MutableModels;
@@ -1,8 +1,29 @@
1
- import { createProvider, envApiKeyAuth, } from "@earendil-works/pi-ai";
1
+ import { createProvider, defaultProviderAuthContext, envApiKeyAuth, } from "@earendil-works/pi-ai";
2
2
  import { stream, streamSimple, } from "@earendil-works/pi-ai/api/openai-completions";
3
3
  import { builtinModels } from "@earendil-works/pi-ai/providers/all";
4
4
  export function modelsFor(selection, credentials) {
5
- const models = builtinModels({ credentials });
5
+ const environmentFirst = {
6
+ async read(providerId, options) {
7
+ const explicit = process.env.MODEL_PROVIDER_API_KEY;
8
+ if (explicit &&
9
+ (!selection.provider || selection.provider === providerId))
10
+ return { type: "api_key", key: explicit };
11
+ const provider = models
12
+ .getProviders()
13
+ .find((item) => item.id === providerId);
14
+ const ambient = await provider?.auth.apiKey?.resolve({
15
+ ctx: defaultProviderAuthContext(),
16
+ signal: options?.signal ?? new AbortController().signal,
17
+ });
18
+ if (ambient)
19
+ return undefined;
20
+ return credentials.read(providerId, options);
21
+ },
22
+ list: (options) => credentials.list(options),
23
+ modify: (id, fn, options) => credentials.modify(id, fn, options),
24
+ delete: (id, options) => credentials.delete(id, options),
25
+ };
26
+ const models = builtinModels({ credentials: environmentFirst });
6
27
  if (!selection.custom)
7
28
  return models;
8
29
  const model = {
@@ -22,7 +43,7 @@ export function modelsFor(selection, credentials) {
22
43
  name: "Custom OpenAI-compatible",
23
44
  baseUrl: selection.custom.baseUrl,
24
45
  auth: {
25
- apiKey: envApiKeyAuth("Custom API key", ["NYLO_CUSTOM_API_KEY"]),
46
+ apiKey: envApiKeyAuth("Custom API key", ["MODEL_PROVIDER_API_KEY"]),
26
47
  },
27
48
  models: [model],
28
49
  api: { stream, streamSimple },
@@ -17,7 +17,7 @@ export function piModel(options = {}) {
17
17
  context.signal.throwIfAborted();
18
18
  const root = options.root ?? process.cwd();
19
19
  const selection = options.selection ?? modelSelection(root);
20
- const registry = modelsFor(selection, new ProjectCredentialStore(join(root, ".env", "auth.json")));
20
+ const registry = modelsFor(selection, new ProjectCredentialStore(join(root, ".nylorun", "auth.json"), join(root, ".env", "auth.json")));
21
21
  const selected = registry.getModel(selection.provider, selection.model);
22
22
  if (!selected)
23
23
  throw new Error("Unknown model. Run nylorun configure.");
@@ -1,6 +1,27 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  export function modelSelection(root = process.cwd()) {
4
+ const { MODEL_PROVIDER: provider, MODEL: model, MODEL_PROVIDER_BASE_URL: baseUrl, } = process.env;
5
+ if (provider !== undefined || model !== undefined || baseUrl !== undefined) {
6
+ if (!provider?.trim() || !model?.trim())
7
+ throw new Error("Set both MODEL_PROVIDER and MODEL, or run nylorun configure.");
8
+ if (provider === "custom" && !baseUrl?.trim())
9
+ throw new Error("Set MODEL_PROVIDER_BASE_URL for MODEL_PROVIDER=custom.");
10
+ if (baseUrl && provider !== "custom")
11
+ throw new Error("MODEL_PROVIDER_BASE_URL requires MODEL_PROVIDER=custom.");
12
+ if (baseUrl) {
13
+ let url;
14
+ try {
15
+ url = new URL(baseUrl);
16
+ }
17
+ catch {
18
+ throw new Error("MODEL_PROVIDER_BASE_URL must be an HTTP(S) URL.");
19
+ }
20
+ if (!["http:", "https:"].includes(url.protocol))
21
+ throw new Error("MODEL_PROVIDER_BASE_URL must be an HTTP(S) URL.");
22
+ }
23
+ return { provider, model, ...(baseUrl ? { custom: { baseUrl } } : {}) };
24
+ }
4
25
  try {
5
26
  let contents;
6
27
  try {
@@ -9,7 +30,7 @@ export function modelSelection(root = process.cwd()) {
9
30
  catch (error) {
10
31
  if (!(error instanceof Error) ||
11
32
  !("code" in error) ||
12
- error.code !== "ENOENT")
33
+ !["ENOENT", "ENOTDIR"].includes(String(error.code)))
13
34
  throw error;
14
35
  contents = readFileSync(join(root, "config", "model.json"), "utf8");
15
36
  }
@@ -30,17 +51,18 @@ export function projectSecrets(root = process.cwd()) {
30
51
  const values = Object.entries(process.env)
31
52
  .filter(([key]) => /key|token|secret|password|credential/i.test(key))
32
53
  .flatMap(([, value]) => (value ? [value] : []));
33
- try {
34
- const collect = (value) => {
35
- if (typeof value === "string")
36
- values.push(value);
37
- else if (value && typeof value === "object")
38
- Object.values(value).forEach(collect);
39
- };
40
- collect(JSON.parse(readFileSync(join(root, ".env", "auth.json"), "utf8")));
41
- }
42
- catch {
43
- /* The vault may not exist before setup. */
44
- }
54
+ for (const directory of [".nylorun", ".env"])
55
+ try {
56
+ const collect = (value) => {
57
+ if (typeof value === "string")
58
+ values.push(value);
59
+ else if (value && typeof value === "object")
60
+ Object.values(value).forEach(collect);
61
+ };
62
+ collect(JSON.parse(readFileSync(join(root, directory, "auth.json"), "utf8")));
63
+ }
64
+ catch {
65
+ /* The vault may not exist before setup. */
66
+ }
45
67
  return values;
46
68
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nylorun/runtime",
3
- "version": "0.3.0-beta",
3
+ "version": "0.4.0-beta",
4
4
  "description": "Portable agent runtime, Hono protocol router, model providers, and the Nylorun CLI.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -39,7 +39,8 @@
39
39
  "prepack": "npm run build"
40
40
  },
41
41
  "dependencies": {
42
- "@earendil-works/pi-ai": "0.85.1"
42
+ "@earendil-works/pi-ai": "0.85.1",
43
+ "@hono/node-server": "^2.1.1"
43
44
  },
44
45
  "peerDependencies": {
45
46
  "hono": "^4.13.7"