nylorun 0.13.0-beta → 0.15.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.
Files changed (76) hide show
  1. package/CHANGELOG.md +153 -0
  2. package/README.md +255 -166
  3. package/dist/cli.js +141 -47
  4. package/dist/client/agents.d.ts +6 -0
  5. package/dist/client/agents.js +148 -0
  6. package/dist/client/artifacts.d.ts +4 -0
  7. package/dist/client/artifacts.js +210 -0
  8. package/dist/client/doctor.d.ts +10 -4
  9. package/dist/client/doctor.js +28 -20
  10. package/dist/client/environment-manifests.d.ts +10 -0
  11. package/dist/client/environment-manifests.js +173 -0
  12. package/dist/client/environments.d.ts +4 -0
  13. package/dist/client/environments.js +295 -0
  14. package/dist/client/io.d.ts +31 -0
  15. package/dist/client/io.js +72 -0
  16. package/dist/client/mcp.d.ts +11 -0
  17. package/dist/{stack → client}/mcp.js +40 -26
  18. package/dist/client/model/pi-ai.d.ts +3 -3
  19. package/dist/client/model/pi-ai.js +4 -4
  20. package/dist/client/models.d.ts +13 -0
  21. package/dist/client/models.js +203 -0
  22. package/dist/client/runtime.d.ts +40 -0
  23. package/dist/client/runtime.js +134 -0
  24. package/dist/client/sessions.d.ts +4 -0
  25. package/dist/client/sessions.js +295 -0
  26. package/dist/client/settings.d.ts +12 -0
  27. package/dist/client/settings.js +103 -0
  28. package/dist/client/signing-keys.d.ts +3 -0
  29. package/dist/client/signing-keys.js +32 -0
  30. package/dist/client/target.d.ts +33 -0
  31. package/dist/client/target.js +65 -0
  32. package/dist/client/tenant.d.ts +22 -0
  33. package/dist/client/tenant.js +176 -0
  34. package/dist/doctor.js +3 -3
  35. package/dist/errors.d.ts +1 -1
  36. package/dist/errors.js +1 -1
  37. package/dist/nylo.js +20 -4
  38. package/dist/sandbox/cluster-file.d.ts +1 -1
  39. package/dist/sandbox/commands.d.ts +1 -1
  40. package/dist/sandbox/commands.js +7 -7
  41. package/dist/sandbox/manifests.d.ts +1 -1
  42. package/dist/sandbox/manifests.js +1 -1
  43. package/dist/sandbox/probe.d.ts +1 -1
  44. package/dist/stack/commands.d.ts +9 -4
  45. package/dist/stack/commands.js +22 -26
  46. package/dist/stack/compose-file.d.ts +1 -1
  47. package/dist/stack/compose-file.js +22 -13
  48. package/dist/stack/env-file.d.ts +2 -2
  49. package/dist/stack/env-file.js +2 -2
  50. package/dist/stack/images.d.ts +1 -1
  51. package/dist/stack/images.js +1 -1
  52. package/dist/stack/index.d.ts +1 -10
  53. package/dist/stack/index.js +3 -19
  54. package/dist/stack/keys.d.ts +2 -2
  55. package/dist/stack/keys.js +14 -11
  56. package/dist/stack/paths.d.ts +1 -1
  57. package/dist/stack/prepare.d.ts +1 -1
  58. package/dist/stack/stacks.js +1 -1
  59. package/dist/stack/versions.d.ts +2 -2
  60. package/dist/stack/versions.js +3 -3
  61. package/dist/telemetry.d.ts +2 -2
  62. package/dist/telemetry.js +4 -4
  63. package/package.json +10 -9
  64. package/dist/client/access/commands.d.ts +0 -8
  65. package/dist/client/access/commands.js +0 -33
  66. package/dist/client/cli.d.ts +0 -2
  67. package/dist/client/cli.js +0 -106
  68. package/dist/client/installation/commands.d.ts +0 -7
  69. package/dist/client/installation/commands.js +0 -80
  70. package/dist/client/project/connection.d.ts +0 -36
  71. package/dist/client/project/connection.js +0 -80
  72. package/dist/client/project/env.d.ts +0 -3
  73. package/dist/client/project/env.js +0 -19
  74. package/dist/stack/mcp.d.ts +0 -9
  75. package/dist/stack/sandbox.d.ts +0 -4
  76. package/dist/stack/sandbox.js +0 -106
@@ -0,0 +1,72 @@
1
+ /**
2
+ * What the `tenant` commands read and print: a JSON body from a file or standard input (`-`),
3
+ * checked against the request's schema before anything is sent; a secret from standard input,
4
+ * never from the arguments; and tables.
5
+ */
6
+ import { readFile } from "node:fs/promises";
7
+ import { resolve } from "node:path";
8
+ import { CliError } from "../errors.js";
9
+ const usageError = (message) => new CliError(message, 2);
10
+ /** Standard input as text; a usage error when it is a terminal or there is none. */
11
+ async function readStdin(deps, refusal) {
12
+ if (!deps.stdin || deps.stdin.isTTY)
13
+ throw usageError(refusal);
14
+ return await deps.stdin.text();
15
+ }
16
+ /**
17
+ * The JSON body in file `source`, or on standard input for `-`, as `schema` parses it. Fields
18
+ * named in `readOnly` (a view's, so a `get` can be edited and put back) are dropped first. A
19
+ * body that does not parse is a usage error, before any request.
20
+ */
21
+ export async function readBody(deps, source, schema, readOnly = []) {
22
+ let text;
23
+ if (source === "-") {
24
+ text = await readStdin(deps, "Pipe the JSON body in on standard input, or name a file.");
25
+ }
26
+ else {
27
+ try {
28
+ text = await readFile(resolve(deps.cwd ?? process.cwd(), source), "utf8");
29
+ }
30
+ catch (error) {
31
+ throw usageError(`Cannot read ${source}: ${error.code ?? error}.`);
32
+ }
33
+ }
34
+ const where = source === "-" ? "Standard input" : source;
35
+ let value;
36
+ try {
37
+ value = JSON.parse(text);
38
+ }
39
+ catch (error) {
40
+ throw usageError(`${where} is not JSON: ${error.message}.`);
41
+ }
42
+ if (value && typeof value === "object" && !Array.isArray(value))
43
+ value = Object.fromEntries(Object.entries(value).filter(([key]) => !readOnly.includes(key)));
44
+ const parsed = schema.safeParse(value);
45
+ if (parsed.success)
46
+ return parsed.data;
47
+ const issues = parsed.error.issues.map((issue) => ` ${issue.path.length ? `${issue.path.map(String).join(".")}: ` : ""}${issue.message}`);
48
+ throw usageError(`${where} is not a valid body:\n${issues.join("\n")}`);
49
+ }
50
+ /**
51
+ * A secret piped in on standard input (its trailing newline dropped), so it is never in the
52
+ * arguments, the shell's history or the process list. `what` names it in the refusals.
53
+ */
54
+ export async function readSecret(deps, what, example) {
55
+ const secret = (await readStdin(deps, `Pipe the ${what} in on standard input, for example: ${example}. It is never taken as an argument.`)).trim();
56
+ if (!secret)
57
+ throw usageError(`Standard input held no ${what}.`);
58
+ return secret;
59
+ }
60
+ /** `rows` (the first is the header) as columns two spaces apart, with no trailing blanks. */
61
+ export function printTable(deps, rows) {
62
+ const widths = rows[0].map((_, column) => Math.max(...rows.map((row) => row[column].length)));
63
+ for (const row of rows)
64
+ deps.out(row
65
+ .map((cell, column) => (column === row.length - 1 ? cell : cell.padEnd(widths[column])))
66
+ .join(" ")
67
+ .trimEnd());
68
+ }
69
+ /** `value` as indented JSON on stdout. */
70
+ export function printJson(deps, value) {
71
+ deps.out(JSON.stringify(value, null, 2));
72
+ }
@@ -0,0 +1,11 @@
1
+ import type { McpPreview } from "@nylorun/core/contracts";
2
+ import { type StackDeps } from "../stack/commands.js";
3
+ export declare const mcpUsage = " mcp inspect <url> [--server <name>] [--environment <id>] [--sse] [--tenant <name>] [--json]\n list a remote MCP server's tools, with Environment <id>'s\n credential for <url> (none without it): model names, schema\n sizes, renames, or that it needs a person's sign-in (the\n Runtime calls no tool)";
4
+ /** What `--vault` says since protocol 11. */
5
+ export declare const MCP_VAULT_REMOVED = "--vault was removed in protocol 11: a credential is a secret of an Environment, in its own vault. Pass --environment <id> to send that Environment's credential for the URL.";
6
+ /** What the removed `connect` subcommand says since protocol 10. */
7
+ export declare const MCP_CONNECT_REMOVED: string;
8
+ /** A preview as a person reads it: the server, a table of tools, the renames, or the sign-in hint. */
9
+ export declare function previewLines(preview: McpPreview, environmentId?: string): string[];
10
+ /** `nylorun tenant mcp <inspect|connect> ...` */
11
+ export declare function mcpCommand(deps: StackDeps, args: readonly string[]): Promise<number>;
@@ -1,34 +1,45 @@
1
1
  /**
2
- * `nylorun mcp inspect <url>`: a remote MCP server's tools before an agent names it (R2b C12),
3
- * through the running local Tenant's Management API (`POST /v1/tenant/mcp/preview`) with the
4
- * Project link's management key, else the Host root's (`runningTenantApi`). It never starts the
5
- * Tenant, and the Runtime never calls a tool. Its `connect` subcommand (MCP OAuth) was removed
6
- * in protocol 10, and says so.
2
+ * `nylorun tenant mcp inspect <url>`: a remote MCP server's tools before an agent names it (R2b
3
+ * C12), through the Management API (`POST /v1/tenant/mcp/preview`) of the installation
4
+ * `target.ts` picks. It never starts a local Tenant, and the Runtime never calls a tool. `--environment <id>` sends that Environment's
5
+ * credential for the URL (protocol 11; it replaced `--vault`); an Environment's secrets are
6
+ * written on the Runtime API (`client.environments.secrets.put` of `@nylorun/agents`). Its
7
+ * `connect` subcommand (MCP OAuth) was removed in protocol 10, and says so.
7
8
  */
8
9
  import { PROTOCOL_HEADER, PROTOCOL_VERSION } from "@nylorun/core/compatibility";
9
10
  import { CliError } from "../errors.js";
10
- import { parseStackFlags, runningTenantApi } from "./commands.js";
11
- export const mcpUsage = ` mcp inspect <url> [--server <name>] [--vault <id>] [--sse] [--tenant <name>] [--json]
12
- list a remote MCP server's tools with the installation vault's
13
- credential for <url>: model names, schema sizes, renames, or
14
- that it needs a person's sign-in (the Runtime calls no tool)`;
15
- const INSPECT_USAGE = "nylorun mcp inspect <url> [--server <name>] [--vault <id>] [--sse] [--tenant <name>] [--json]";
11
+ import { parseStackFlags } from "../stack/commands.js";
12
+ import { connect } from "./target.js";
13
+ export const mcpUsage = ` mcp inspect <url> [--server <name>] [--environment <id>] [--sse] [--tenant <name>] [--json]
14
+ list a remote MCP server's tools, with Environment <id>'s
15
+ credential for <url> (none without it): model names, schema
16
+ sizes, renames, or that it needs a person's sign-in (the
17
+ Runtime calls no tool)`;
18
+ const INSPECT_USAGE = "nylorun tenant mcp inspect <url> [--server <name>] [--environment <id>] [--sse] [--tenant <name>] [--json]";
19
+ /** Where a credential for an MCP server goes since protocol 11. */
20
+ const SECRET_HINT = "write it as a bearer or headers secret of an Environment (nylorun environments secrets put, client.environments.secrets.put of @nylorun/agents, or Studio)";
21
+ /** What `--vault` says since protocol 11. */
22
+ export const MCP_VAULT_REMOVED = "--vault was removed in protocol 11: a credential is a secret of an Environment, in its own vault. Pass --environment <id> to send that Environment's credential for the URL.";
16
23
  /** What the removed `connect` subcommand says since protocol 10. */
17
- export const MCP_CONNECT_REMOVED = "The connect subcommand of nylorun mcp was removed with MCP OAuth (protocol 10): add the server's key to an installation vault as a bearer or headers credential (Studio's Credentials page, or @nylorun/admin), or reach a server that needs a person's sign-in through a gateway (a credential with via). See its tools with nylorun mcp inspect <url>.";
24
+ export const MCP_CONNECT_REMOVED = `The connect subcommand of nylorun mcp was removed with MCP OAuth (protocol 10): ${SECRET_HINT.replace("write it", "write the server's key")}, or reach a server that needs a person's sign-in through a gateway (a secret with via). See its tools with nylorun tenant mcp inspect <url> --environment <id>.`;
18
25
  /** The preview takes up to 15 s in the Runtime; the hop gets a margin. */
19
26
  const PREVIEW_TIMEOUT_MS = 30_000;
20
27
  const DESCRIPTION_CHARS = 60;
21
28
  const usageError = (message) => new CliError(message, 2);
22
29
  async function inspect(deps, args) {
23
- const flags = parseStackFlags(args, { booleans: ["--json", "--sse"], values: ["--server", "--vault", "--tenant"] }, INSPECT_USAGE);
30
+ if (args.some((arg) => arg === "--vault" || arg.startsWith("--vault=")))
31
+ throw usageError(MCP_VAULT_REMOVED);
32
+ const flags = parseStackFlags(args, { booleans: ["--json", "--sse"], values: ["--server", "--environment", "--tenant"] }, INSPECT_USAGE);
24
33
  if (flags.rest.length !== 1)
25
34
  throw usageError(`Usage: ${INSPECT_USAGE}`);
26
35
  const url = flags.rest[0];
27
36
  const name = flags.values.get("--server");
28
- const vaultId = flags.values.get("--vault");
37
+ const environmentId = flags.values.get("--environment");
29
38
  const tenant = flags.values.get("--tenant");
30
- const api = await runningTenantApi(deps, tenant === undefined ? {} : { name: tenant });
31
- const response = await deps.fetch(`${api.runtimeUrl}/v1/tenant/mcp/preview`, {
39
+ const api = await connect(deps, "management", tenant);
40
+ if (!api.managementKey)
41
+ throw new CliError(`No management key for ${api.label}: set NYLORUN_MANAGEMENT_KEY.`, 1);
42
+ const response = await deps.fetch(`${api.url}/v1/tenant/mcp/preview`, {
32
43
  method: "POST",
33
44
  headers: {
34
45
  authorization: `Bearer ${api.managementKey}`,
@@ -40,14 +51,14 @@ async function inspect(deps, args) {
40
51
  url,
41
52
  ...(flags.booleans.has("--sse") ? { type: "sse" } : {}),
42
53
  ...(name === undefined ? {} : { name }),
43
- ...(vaultId === undefined ? {} : { vaultId }),
54
+ ...(environmentId === undefined ? {} : { environmentId }),
44
55
  }),
45
56
  redirect: "error",
46
57
  signal: AbortSignal.timeout(PREVIEW_TIMEOUT_MS),
47
58
  });
48
59
  const body = (await response.json().catch(() => ({})));
49
60
  if (response.status === 404 && body.code === "not_found")
50
- throw new CliError(`Tenant ${api.name} runs a Runtime without MCP previews. Update it: npx nylorun@latest start.`, 1);
61
+ throw new CliError(`${api.label} runs a Runtime without MCP previews. Update it.`, 1);
51
62
  if (!response.ok) {
52
63
  const details = body.details;
53
64
  const failure = typeof details?.failure === "string" ? ` (${details.failure})` : "";
@@ -60,15 +71,17 @@ async function inspect(deps, args) {
60
71
  deps.out(JSON.stringify(preview, null, 2));
61
72
  return 0;
62
73
  }
63
- for (const line of previewLines(preview))
74
+ for (const line of previewLines(preview, environmentId))
64
75
  deps.out(line);
65
76
  return 0;
66
77
  }
67
78
  /** A preview as a person reads it: the server, a table of tools, the renames, or the sign-in hint. */
68
- export function previewLines(preview) {
79
+ export function previewLines(preview, environmentId) {
69
80
  const credential = preview.credentialSent
70
- ? "with the installation vault's credential"
71
- : "with no credential (no installation vault holds one for this URL)";
81
+ ? `with Environment ${environmentId ?? "(unnamed)"}'s credential`
82
+ : environmentId === undefined
83
+ ? "with no credential (pass --environment <id> to send an Environment's)"
84
+ : `with no credential (Environment ${environmentId} holds none for this URL)`;
72
85
  if (preview.authRequired) {
73
86
  const metadata = preview.authRequired.resourceMetadata;
74
87
  const servers = Array.isArray(metadata?.authorization_servers)
@@ -76,13 +89,14 @@ export function previewLines(preview) {
76
89
  : [];
77
90
  return [
78
91
  preview.credentialSent
79
- ? `The MCP server at ${preview.url} answered 401: it rejected the installation vault's credential.`
92
+ ? `The MCP server at ${preview.url} answered 401: it rejected Environment ${environmentId ?? "(unnamed)"}'s credential.`
80
93
  : `The MCP server at ${preview.url} answered 401: it needs a credential.`,
81
94
  ...(servers.length > 0
82
95
  ? [`It signs people in with ${servers.join(", ")} (${preview.authRequired.resourceMetadataUrl}).`]
83
96
  : []),
84
- "Nylorun holds no OAuth client: add the server's API key to an installation vault, or reach it",
85
- "through a gateway that keeps each person's sign-in (a credential with via and an identity header).",
97
+ `Nylorun holds no OAuth client: ${SECRET_HINT.replace("write it", "write the server's API key")},`,
98
+ "and pass --environment <id>; or reach it through a gateway that keeps each person's sign-in",
99
+ "(a secret with via and an identity header).",
86
100
  ];
87
101
  }
88
102
  const info = preview.serverInfo;
@@ -131,7 +145,7 @@ function oneLine(text) {
131
145
  const flat = text.replace(/\s+/g, " ").trim();
132
146
  return flat.length > DESCRIPTION_CHARS ? `${flat.slice(0, DESCRIPTION_CHARS - 1)}…` : flat;
133
147
  }
134
- /** `nylorun mcp <inspect|connect> ...` */
148
+ /** `nylorun tenant mcp <inspect|connect> ...` */
135
149
  export async function mcpCommand(deps, args) {
136
150
  const [sub, ...rest] = args;
137
151
  if (sub === "inspect")
@@ -1,9 +1,9 @@
1
1
  /**
2
- * The pi-ai `nylo configure` loads; nylorun's devDependency has the same version
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
5
  export declare const PI_AI_VERSION = "0.99.1";
6
- /** What `nylo configure` uses of pi-ai: its provider registry and login flows. */
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;
9
9
  builtinModels: typeof import("@earendil-works/pi-ai/providers/all").builtinModels;
@@ -12,7 +12,7 @@ export interface PiAi {
12
12
  }
13
13
  /**
14
14
  * pi-ai, with its provider SDKs about 100 MB, is not a dependency of nylorun, so
15
- * `npx nylorun` stays small: only `nylo configure` needs it. It loads pi-ai from beside nylorun
15
+ * `npx nylorun` stays small: only `nylorun tenant models set` needs it. It loads pi-ai from beside nylorun
16
16
  * when it resolves there (the workspace, or a project that installs it), else from
17
17
  * `<Nylorun home>/lib/pi-ai-<version>/`, which it installs with npm on first use.
18
18
  */
@@ -7,7 +7,7 @@ import { pathToFileURL } from "node:url";
7
7
  import { nylorunHome } from "@nylorun/core/project";
8
8
  import { CliError } from "../../errors.js";
9
9
  /**
10
- * The pi-ai `nylo configure` loads; nylorun's devDependency has the same version
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
13
  export const PI_AI_VERSION = "0.99.1";
@@ -35,7 +35,7 @@ const notInstalled = (error) => error?.code === "ERR_MODULE_NOT_FOUND" &&
35
35
  String(error.message).includes(`'${PI_AI}'`);
36
36
  /**
37
37
  * pi-ai, with its provider SDKs about 100 MB, is not a dependency of nylorun, so
38
- * `npx nylorun` stays small: only `nylo configure` needs it. It loads pi-ai from beside nylorun
38
+ * `npx nylorun` stays small: only `nylorun tenant models set` needs it. It loads pi-ai from beside nylorun
39
39
  * when it resolves there (the workspace, or a project that installs it), else from
40
40
  * `<Nylorun home>/lib/pi-ai-<version>/`, which it installs with npm on first use.
41
41
  */
@@ -49,7 +49,7 @@ export async function loadPiAi(options = {}) {
49
49
  }
50
50
  const directory = join(nylorunHome(options.home), "lib", `pi-ai-${PI_AI_VERSION}`);
51
51
  if (!existsSync(join(directory, ENTRY))) {
52
- (options.log ?? ((line) => console.error(line)))(`nylo configure uses ${PI_AI}@${PI_AI_VERSION} for the model providers' sign-in, which nylorun does not install. Installing it once into ${directory}…`);
52
+ (options.log ?? ((line) => console.error(line)))(`nylorun tenant models set uses ${PI_AI}@${PI_AI_VERSION} for the model providers' sign-in, which nylorun does not install. Installing it once into ${directory}…`);
53
53
  await installInto(directory, options.install ?? npmInstall);
54
54
  }
55
55
  return (await import(pathToFileURL(join(directory, ENTRY)).href));
@@ -68,7 +68,7 @@ async function installInto(directory, install) {
68
68
  });
69
69
  }
70
70
  catch (error) {
71
- throw new CliError(`Could not install ${PI_AI}@${PI_AI_VERSION} for nylo configure: ${error instanceof Error ? error.message : String(error)}. Install it beside nylorun (npm install ${PI_AI}@${PI_AI_VERSION}), or set the model provider in Studio.`, 1);
71
+ throw new CliError(`Could not install ${PI_AI}@${PI_AI_VERSION} for nylorun tenant models set: ${error instanceof Error ? error.message : String(error)}. Install it beside nylorun (npm install ${PI_AI}@${PI_AI_VERSION}), or set the model provider in Studio.`, 1);
72
72
  }
73
73
  finally {
74
74
  await rm(temporary, { recursive: true, force: true });
@@ -0,0 +1,13 @@
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)";
3
+ export declare const usageUsage = " usage [--scope tenant|agent|turn] [--id <id>] [--period day|month|total] [--tenant <name>]\n what the model calls used and cost (JSON)\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
+ /** `nylorun tenant models <verb> ...` */
5
+ export declare function modelsCommand(deps: StackDeps, args: readonly string[]): Promise<number>;
6
+ /** `nylorun tenant usage`: one scope's totals for one period, as JSON. */
7
+ export declare function usageCommand(deps: StackDeps, args: readonly string[]): Promise<number>;
8
+ /**
9
+ * `nylorun tenant calls`: every recorded model call from `--after`, one JSON object per line
10
+ * (NDJSON) on stdout, page by page (`--limit` a page) until caught up; then the cursor to resume
11
+ * from on stderr. Rows may repeat across runs: deduplicate by id.
12
+ */
13
+ export declare function callsCommand(deps: StackDeps, args: readonly string[]): Promise<number>;
@@ -0,0 +1,203 @@
1
+ /**
2
+ * `nylorun tenant models | usage | calls`: the Tenant's model, what its model calls used, and the
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,
5
+ * for CI. A key is never an argument.
6
+ */
7
+ import { randomUUID } from "node:crypto";
8
+ import { CliError } from "../errors.js";
9
+ import { parseStackFlags } from "../stack/commands.js";
10
+ import { printJson, printTable, readSecret } from "./io.js";
11
+ import { connect, managementClient } from "./target.js";
12
+ export const modelsUsage = ` models catalog [--tenant <name>] [--json]
13
+ every provider and model the Runtime knows
14
+ models providers [--tenant <name>] [--json]
15
+ the providers with a credential, and the one in use
16
+ models get [--tenant <name>] [--json]
17
+ the Tenant's model
18
+ models set [--tenant <name>] sign in to a model provider and set the Tenant's model
19
+ models set --provider <id> --model <id> [--base-url <url>] [--tenant <name>]
20
+ the same with an API key piped in on stdin (CI)
21
+ models select <provider> [--model <id>] [--tenant <name>]
22
+ use a provider that has a credential (default: its model)`;
23
+ export const usageUsage = ` usage [--scope tenant|agent|turn] [--id <id>] [--period day|month|total] [--tenant <name>]
24
+ what the model calls used and cost (JSON)
25
+ calls [--after <cursor>] [--limit <n>] [--tenant <name>]
26
+ every recorded model call, one JSON line each, until
27
+ caught up; prints the cursor to resume from on stderr`;
28
+ const usageError = (message = `Usage:\n${modelsUsage}`) => new CliError(message, 2);
29
+ async function client(deps, tenant) {
30
+ return managementClient(deps, await connect(deps, "management", tenant));
31
+ }
32
+ /** The Tenant's model in one line. */
33
+ function describe(view) {
34
+ if (!view.configured)
35
+ return "No model is set: run nylorun tenant models set.";
36
+ return [view.provider, view.model, view.authType, ...(view.baseUrl ? [view.baseUrl] : [])].join(" ");
37
+ }
38
+ /** `models catalog | providers | get`: a table or a line, else `--json`. */
39
+ async function read(deps, verb, args) {
40
+ const usage = `nylorun tenant models ${verb} [--tenant <name>] [--json]`;
41
+ const flags = parseStackFlags(args, { booleans: ["--json"], values: ["--tenant"] }, usage);
42
+ if (flags.rest.length)
43
+ throw usageError(`Usage: ${usage}`);
44
+ const admin = await client(deps, flags.values.get("--tenant"));
45
+ const json = flags.booleans.has("--json");
46
+ if (verb === "catalog") {
47
+ const catalog = await admin.models.catalog();
48
+ if (json)
49
+ printJson(deps, catalog);
50
+ else
51
+ printTable(deps, [
52
+ ["PROVIDER", "MODEL", "READS"],
53
+ ...catalog.providers.flatMap((provider) => provider.models.map((model) => [provider.id, model.id, model.inputs.join(",")])),
54
+ ]);
55
+ }
56
+ else if (verb === "providers") {
57
+ const { providers } = await admin.models.providers();
58
+ if (json)
59
+ printJson(deps, providers);
60
+ else if (!providers.length)
61
+ deps.err("No provider has a credential: run nylorun tenant models set.");
62
+ else
63
+ printTable(deps, [
64
+ ["PROVIDER", "MODEL", "AUTH", "IN USE"],
65
+ ...providers.map((p) => [p.id, p.model, p.authType, p.active ? "yes" : ""]),
66
+ ]);
67
+ }
68
+ else {
69
+ const view = await admin.models.get();
70
+ if (json)
71
+ printJson(deps, view);
72
+ else
73
+ deps.out(describe(view));
74
+ }
75
+ return 0;
76
+ }
77
+ const SET_USAGE = "nylorun tenant models set [--tenant <name>] | nylorun tenant models set --provider <id> --model <id> [--base-url <url>] [--tenant <name>]";
78
+ /**
79
+ * `models set`: at a terminal, the sign-in prompts; with `--provider` and `--model`, the API key
80
+ * on stdin, set as `PUT /v1/tenant/model` with an `api_key` credential (OAuth needs the prompts).
81
+ */
82
+ async function set(deps, args) {
83
+ const flags = parseStackFlags(args, { values: ["--tenant", "--provider", "--model", "--base-url"] }, SET_USAGE);
84
+ if (flags.rest.length)
85
+ throw usageError(`Usage: ${SET_USAGE}`);
86
+ const provider = flags.values.get("--provider");
87
+ const model = flags.values.get("--model");
88
+ const baseUrl = flags.values.get("--base-url");
89
+ if (provider !== undefined || model !== undefined || baseUrl !== undefined) {
90
+ if (provider === undefined || model === undefined)
91
+ throw usageError(`Pass both --provider and --model. Usage: ${SET_USAGE}`);
92
+ const key = await readSecret(deps, "API key", `printenv OPENAI_API_KEY | nylorun tenant models set --provider ${provider} --model ${model}`);
93
+ const admin = await client(deps, flags.values.get("--tenant"));
94
+ const view = await admin.models.put({
95
+ idempotencyKey: randomUUID(),
96
+ provider,
97
+ model,
98
+ ...(baseUrl ? { baseUrl } : {}),
99
+ auth: { type: "api_key", key },
100
+ });
101
+ deps.out(describe(view));
102
+ return 0;
103
+ }
104
+ const admin = await client(deps, flags.values.get("--tenant"));
105
+ const { ConfigurationCancelled, configureProvider, fetchModelCatalog } = await import("./model/configure.js");
106
+ const { putHostModel } = await import("./model/host-model.js");
107
+ const controller = new AbortController();
108
+ const cancel = (signal) => controller.abort(new ConfigurationCancelled(signal));
109
+ process.once("SIGINT", () => cancel("SIGINT"));
110
+ process.once("SIGTERM", () => cancel("SIGTERM"));
111
+ const catalog = await fetchModelCatalog(admin);
112
+ const prompted = await configureProvider({ signal: controller.signal, catalog });
113
+ await putHostModel(admin, prompted);
114
+ return 0;
115
+ }
116
+ /**
117
+ * `models select <provider>`: the provider's stored model and endpoint unless `--model` says
118
+ * otherwise (the Runtime keeps only the endpoint the request names).
119
+ */
120
+ async function select(deps, args) {
121
+ const usage = "nylorun tenant models select <provider> [--model <id>] [--tenant <name>]";
122
+ const flags = parseStackFlags(args, { values: ["--tenant", "--model"] }, usage);
123
+ if (flags.rest.length !== 1)
124
+ throw usageError(`Usage: ${usage}`);
125
+ const id = flags.rest[0];
126
+ const admin = await client(deps, flags.values.get("--tenant"));
127
+ const stored = (await admin.models.providers()).providers.find((p) => p.id === id);
128
+ if (!stored)
129
+ throw new CliError(`Provider ${id} has no credential: run nylorun tenant models set (nylorun tenant models providers lists those that do).`, 1);
130
+ const view = await admin.models.select({
131
+ idempotencyKey: randomUUID(),
132
+ provider: id,
133
+ model: flags.values.get("--model") ?? stored.model,
134
+ ...(stored.baseUrl ? { baseUrl: stored.baseUrl } : {}),
135
+ });
136
+ deps.out(describe(view));
137
+ return 0;
138
+ }
139
+ /** `nylorun tenant models <verb> ...` */
140
+ export async function modelsCommand(deps, args) {
141
+ const [verb, ...rest] = args;
142
+ if (verb === "catalog" || verb === "providers" || verb === "get")
143
+ return read(deps, verb, rest);
144
+ if (verb === "set")
145
+ return set(deps, rest);
146
+ if (verb === "select")
147
+ return select(deps, rest);
148
+ throw usageError();
149
+ }
150
+ const SCOPES = ["tenant", "agent", "turn"];
151
+ const PERIODS = ["day", "month", "total"];
152
+ const USAGE_USAGE = "nylorun tenant usage [--scope tenant|agent|turn] [--id <id>] [--period day|month|total] [--tenant <name>]";
153
+ /** `nylorun tenant usage`: one scope's totals for one period, as JSON. */
154
+ export async function usageCommand(deps, args) {
155
+ const flags = parseStackFlags(args, { values: ["--tenant", "--scope", "--id", "--period"] }, USAGE_USAGE);
156
+ if (flags.rest.length)
157
+ throw usageError(`Usage: ${USAGE_USAGE}`);
158
+ const scope = flags.values.get("--scope") ?? "tenant";
159
+ const period = flags.values.get("--period") ?? "total";
160
+ const id = flags.values.get("--id");
161
+ if (!SCOPES.includes(scope))
162
+ throw usageError(`--scope is one of ${SCOPES.join(", ")}.`);
163
+ if (!PERIODS.includes(period))
164
+ throw usageError(`--period is one of ${PERIODS.join(", ")}.`);
165
+ if ((scope === "tenant") !== (id === undefined))
166
+ throw usageError(scope === "tenant" ? "--id names an agent or a turn: pass --scope agent or turn." : `--scope ${scope} needs --id.`);
167
+ const query = {
168
+ scope: scope,
169
+ period: period,
170
+ ...(id === undefined ? {} : { id }),
171
+ };
172
+ printJson(deps, await (await client(deps, flags.values.get("--tenant"))).models.usage(query));
173
+ return 0;
174
+ }
175
+ const CALLS_USAGE = "nylorun tenant calls [--after <cursor>] [--limit <n>] [--tenant <name>]";
176
+ /**
177
+ * `nylorun tenant calls`: every recorded model call from `--after`, one JSON object per line
178
+ * (NDJSON) on stdout, page by page (`--limit` a page) until caught up; then the cursor to resume
179
+ * from on stderr. Rows may repeat across runs: deduplicate by id.
180
+ */
181
+ export async function callsCommand(deps, args) {
182
+ const flags = parseStackFlags(args, { values: ["--tenant", "--after", "--limit"] }, CALLS_USAGE);
183
+ if (flags.rest.length)
184
+ throw usageError(`Usage: ${CALLS_USAGE}`);
185
+ const after = flags.values.get("--after");
186
+ const limitFlag = flags.values.get("--limit");
187
+ const limit = limitFlag === undefined ? undefined : Number(limitFlag);
188
+ if (limit !== undefined && !(Number.isInteger(limit) && limit >= 1 && limit <= 1000))
189
+ throw usageError("--limit is a whole number from 1 to 1000.");
190
+ const admin = await client(deps, flags.values.get("--tenant"));
191
+ let next = null;
192
+ for await (const page of admin.models.exportCalls({
193
+ ...(after === undefined ? {} : { after }),
194
+ ...(limit === undefined ? {} : { limit }),
195
+ })) {
196
+ for (const call of page.calls)
197
+ deps.out(JSON.stringify(call));
198
+ next = page.next;
199
+ }
200
+ if (next !== null)
201
+ deps.err(`Caught up. Resume with --after ${next}`);
202
+ return 0;
203
+ }
@@ -0,0 +1,40 @@
1
+ import { AgentsClient } from "@nylorun/agents";
2
+ import { CliError } from "../errors.js";
3
+ import { parseStackFlags, type StackDeps } from "../stack/commands.js";
4
+ import { type Connection } from "./target.js";
5
+ /** Flags every Runtime API command takes. */
6
+ export declare const RUNTIME_FLAGS: {
7
+ readonly values: readonly ["--tenant", "--as"];
8
+ readonly lists: readonly ["--scope"];
9
+ };
10
+ export declare const runtimeFlagsUsage: string;
11
+ export declare const usageError: (message: string) => CliError;
12
+ export type Flags = ReturnType<typeof parseStackFlags>;
13
+ /** `args` parsed with the Runtime flags added to `allowed`. */
14
+ export declare function parseFlags(args: readonly string[], allowed: {
15
+ booleans?: readonly string[];
16
+ values?: readonly string[];
17
+ lists?: readonly string[];
18
+ }, usage: string): Flags;
19
+ /** Exactly `count` positional arguments, else a usage error. */
20
+ export declare function positional(flags: Flags, count: number, usage: string): string[];
21
+ /** The Runtime API client of the installation the flags (or the environment) name. */
22
+ export declare function runtimeClient(deps: StackDeps, flags: Flags): Promise<{
23
+ client: AgentsClient;
24
+ connection: Connection;
25
+ }>;
26
+ /** Runs `command`, turning a Runtime's refusal into a `CliError` with its message (exit 1). */
27
+ export declare function runtimeCall<T>(command: () => Promise<T>): Promise<T>;
28
+ /** Standard input, whole. */
29
+ export declare function readStdin(): Promise<Buffer>;
30
+ /** A JSON object from file `path`, or from standard input when `path` is `-`. */
31
+ export declare function readJson(path: string, what: string): Promise<Record<string, unknown>>;
32
+ /** A JSON value given on the command line. */
33
+ export declare function parseJsonArgument(text: string, what: string): unknown;
34
+ /** `key=value` pairs (`--label`), as a record. */
35
+ export declare function labelsOf(flags: Flags, name?: string): Record<string, string> | undefined;
36
+ /** A positive integer flag, when given. */
37
+ export declare function integerFlag(flags: Flags, name: string): number | undefined;
38
+ export { printJson, printTable } from "./io.js";
39
+ /** A page's `next cursor` hint, when there is one. */
40
+ export declare function printCursor(deps: StackDeps, cursor: string | null | undefined): void;