@nylorun/cli 0.5.0-beta → 0.7.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 +167 -0
- package/README.md +47 -55
- package/dist/access/commands.d.ts +24 -0
- package/dist/access/commands.js +144 -0
- package/dist/cli.js +40 -39
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.js +11 -6
- package/dist/home.d.ts +3 -3
- package/dist/home.js +3 -3
- package/dist/installation/commands.d.ts +20 -0
- package/dist/installation/commands.js +146 -0
- package/dist/model/configure.d.ts +12 -1
- package/dist/model/configure.js +55 -7
- package/dist/model/host-model.d.ts +7 -11
- package/dist/model/host-model.js +7 -86
- package/dist/project/connection.d.ts +13 -0
- package/dist/project/connection.js +18 -0
- package/dist/project/credentials.d.ts +4 -12
- package/dist/project/credentials.js +6 -36
- package/dist/project/env.d.ts +1 -1
- package/dist/project/env.js +2 -3
- package/dist/project/link.d.ts +10 -17
- package/dist/project/link.js +28 -60
- package/package.json +5 -5
- package/dist/project/create-tenant.d.ts +0 -28
- package/dist/project/create-tenant.js +0 -66
- package/dist/project/seed.d.ts +0 -25
- package/dist/project/seed.js +0 -96
- package/dist/tenant/commands.d.ts +0 -4
- package/dist/tenant/commands.js +0 -345
package/dist/doctor.d.ts
CHANGED
|
@@ -11,6 +11,13 @@ export type SandboxReport = {
|
|
|
11
11
|
version?: string;
|
|
12
12
|
}[];
|
|
13
13
|
defaultImage?: string;
|
|
14
|
+
/** The Tenant's sandbox configuration (Runtimes with Sandboxes v3). */
|
|
15
|
+
config?: {
|
|
16
|
+
default: "none" | "virtual" | Record<string, unknown>;
|
|
17
|
+
limits?: {
|
|
18
|
+
network?: readonly string[];
|
|
19
|
+
};
|
|
20
|
+
};
|
|
14
21
|
};
|
|
15
22
|
/** `nylo doctor sandbox`: Tenant sandbox report via the Tenant API (F2-4). */
|
|
16
23
|
export declare function doctorSandbox(options: {
|
package/dist/doctor.js
CHANGED
|
@@ -11,11 +11,7 @@ function platformLine() {
|
|
|
11
11
|
}
|
|
12
12
|
async function fetchSandboxReport(options) {
|
|
13
13
|
const connection = await resolveConnection(options);
|
|
14
|
-
const client = createClient({
|
|
15
|
-
url: connection.url,
|
|
16
|
-
key: connection.key,
|
|
17
|
-
tenant: connection.tenant,
|
|
18
|
-
});
|
|
14
|
+
const client = createClient({ url: connection.url, key: connection.key });
|
|
19
15
|
return client.transport.json("/v1/tenant/sandbox", "GET", undefined);
|
|
20
16
|
}
|
|
21
17
|
/** `nylo doctor sandbox`: Tenant sandbox report via the Tenant API (F2-4). */
|
|
@@ -42,9 +38,18 @@ export async function doctorSandbox(options) {
|
|
|
42
38
|
rows.push([
|
|
43
39
|
"preference",
|
|
44
40
|
report.preference === "auto"
|
|
45
|
-
? "auto (
|
|
41
|
+
? "auto (npx nylorun start seeds the Tenant's sandbox.backend from .env NYLORUN_SANDBOX)"
|
|
46
42
|
: report.preference,
|
|
47
43
|
]);
|
|
44
|
+
if (report.config)
|
|
45
|
+
rows.push([
|
|
46
|
+
"default",
|
|
47
|
+
report.config.default === "none"
|
|
48
|
+
? "none: sessions get a sandbox only when opened with one (set a default with PUT /v1/tenant/sandbox)"
|
|
49
|
+
: report.config.default === "virtual"
|
|
50
|
+
? "virtual: sessions that name no sandbox get one"
|
|
51
|
+
: "a Tenant sandbox: sessions that name no sandbox get it",
|
|
52
|
+
]);
|
|
48
53
|
rows.push([
|
|
49
54
|
"selected",
|
|
50
55
|
report.backend
|
package/dist/home.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Where `nylo` keeps its own files (the installation id): an explicit home,
|
|
3
|
+
* `NYLORUN_HOME`, or `~/.nylorun`, which holds the local stacks' Host roots
|
|
4
|
+
* (`stacks/<name>/`). `@nylorun/admin` resolves a stack's Host root itself.
|
|
5
5
|
*/
|
|
6
6
|
export declare function resolveHome(home?: string, env?: Readonly<Record<string, string | undefined>>): string;
|
package/dist/home.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Where `nylo` keeps its own files (the installation id): an explicit home,
|
|
5
|
+
* `NYLORUN_HOME`, or `~/.nylorun`, which holds the local stacks' Host roots
|
|
6
|
+
* (`stacks/<name>/`). `@nylorun/admin` resolves a stack's Host root itself.
|
|
7
7
|
*/
|
|
8
8
|
export function resolveHome(home, env = process.env) {
|
|
9
9
|
if (home !== undefined && home.trim() !== "")
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** `nylo status [--json]`: the Tenant, its checks and counts; the Host's cause when it is not open. */
|
|
2
|
+
export declare function statusCommand(args: readonly string[]): Promise<void>;
|
|
3
|
+
/** `nylo reset [--sessions|--sandboxes|--all] [--yes]`; `--all` asks first. */
|
|
4
|
+
export declare function resetCommand(args: readonly string[]): Promise<void>;
|
|
5
|
+
/** `nylo endpoints [--json]` and `nylo endpoints ping <agent>`. */
|
|
6
|
+
export declare function endpointsCommand(args: readonly string[]): Promise<void>;
|
|
7
|
+
/** One line of `nylo endpoints`: the agent, its URL and version, and how it is doing. */
|
|
8
|
+
export declare function endpointLine(endpoint: {
|
|
9
|
+
agentId: string;
|
|
10
|
+
url: string;
|
|
11
|
+
implementationVersion: string;
|
|
12
|
+
health: {
|
|
13
|
+
consecutiveFailures: number;
|
|
14
|
+
lastSuccessAt?: string;
|
|
15
|
+
lastError?: {
|
|
16
|
+
code: string;
|
|
17
|
+
message: string;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
}): string;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nylo status`, `nylo reset` and `nylo endpoints`: the linked installation's one Tenant,
|
|
3
|
+
* through the Tenant API with the Project's application key.
|
|
4
|
+
*/
|
|
5
|
+
import { createInterface } from "node:readline/promises";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { createAdmin } from "@nylorun/admin";
|
|
8
|
+
import { createClient } from "@nylorun/agents";
|
|
9
|
+
import { CliError } from "../errors.js";
|
|
10
|
+
import { linkedConnection } from "../project/connection.js";
|
|
11
|
+
import { findProjectRoot } from "../project/root.js";
|
|
12
|
+
const projectRoot = () => findProjectRoot() ?? process.cwd();
|
|
13
|
+
const clientFor = (connection) => createClient({ url: connection.url, key: connection.key });
|
|
14
|
+
/** `nylo status [--json]`: the Tenant, its checks and counts; the Host's cause when it is not open. */
|
|
15
|
+
export async function statusCommand(args) {
|
|
16
|
+
const json = args.includes("--json");
|
|
17
|
+
if (args.some((a) => a !== "--json"))
|
|
18
|
+
throw new CliError("Usage: nylo status [--json]", 2);
|
|
19
|
+
const root = projectRoot();
|
|
20
|
+
const connection = await linkedConnection(root);
|
|
21
|
+
const stack = connection.link?.stack;
|
|
22
|
+
let reason;
|
|
23
|
+
try {
|
|
24
|
+
const body = await clientFor(connection).transport.json("/v1/tenant", "GET");
|
|
25
|
+
if (json) {
|
|
26
|
+
console.log(JSON.stringify({ ...(stack ? { stack } : {}), ...body }, null, 2));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
console.log(`${body.tenant.name} ${body.tenant.id}`);
|
|
30
|
+
if (stack)
|
|
31
|
+
console.log(`stack ${stack}`);
|
|
32
|
+
console.log(`runtime ${connection.url}`);
|
|
33
|
+
console.log(`path ${body.path}`);
|
|
34
|
+
console.log(`checks ${Object.entries(body.checks)
|
|
35
|
+
.map(([k, v]) => `${k}=${v ? "ok" : "fail"}`)
|
|
36
|
+
.join(" ")}`);
|
|
37
|
+
console.log(`counts sessions=${body.counts.sessions} running=${body.counts.runningSessions} pending=${body.counts.pendingActions}`);
|
|
38
|
+
console.log(`sandbox ${body.sandbox.backend ?? "none"}`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
// A Tenant that is not open does not answer: the Admin API says why.
|
|
43
|
+
reason = error;
|
|
44
|
+
}
|
|
45
|
+
let tenant;
|
|
46
|
+
try {
|
|
47
|
+
tenant = (await createAdmin({ cwd: root }).status()).tenant;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
throw new CliError(reason instanceof Error
|
|
51
|
+
? reason.message
|
|
52
|
+
: `The Tenant at ${connection.url} is not reachable.`, 1);
|
|
53
|
+
}
|
|
54
|
+
if (json) {
|
|
55
|
+
console.log(JSON.stringify({ ...(stack ? { stack } : {}), tenant }, null, 2));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
console.log(`${tenant.name ?? "(unreadable)"} ${tenant.id ?? "(unknown)"} ${tenant.state}`);
|
|
59
|
+
if (stack)
|
|
60
|
+
console.log(`stack ${stack}`);
|
|
61
|
+
console.log(`runtime ${connection.url}`);
|
|
62
|
+
if (tenant.cause) {
|
|
63
|
+
console.log(`reason ${tenant.cause.code}: ${tenant.cause.message}`);
|
|
64
|
+
console.log(`repair ${tenant.cause.repair}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const RESET_USAGE = "Usage: nylo reset [--sessions|--sandboxes|--all] [--yes]";
|
|
68
|
+
const SCOPES = { "--sessions": "sessions", "--sandboxes": "sandboxes", "--all": "all" };
|
|
69
|
+
/** `nylo reset [--sessions|--sandboxes|--all] [--yes]`; `--all` asks first. */
|
|
70
|
+
export async function resetCommand(args) {
|
|
71
|
+
const yes = args.includes("--yes");
|
|
72
|
+
const flags = args.filter((a) => a !== "--yes");
|
|
73
|
+
if (flags.some((f) => !(f in SCOPES)) || args.filter((a) => a === "--yes").length > 1)
|
|
74
|
+
throw new CliError(RESET_USAGE, 2);
|
|
75
|
+
if (flags.length > 1)
|
|
76
|
+
throw new CliError("Pass only one of --sessions, --sandboxes, --all.", 2);
|
|
77
|
+
const scope = flags.length ? SCOPES[flags[0]] : "sessions";
|
|
78
|
+
const connection = await linkedConnection(projectRoot());
|
|
79
|
+
const stack = connection.link?.stack;
|
|
80
|
+
const where = stack ? `stack ${stack} (${connection.url})` : connection.url;
|
|
81
|
+
if (scope === "all" && !yes)
|
|
82
|
+
await confirmOrThrow(`Reset ALL data of the Tenant on ${where}? The Project link and credentials are kept. [y/N] `);
|
|
83
|
+
try {
|
|
84
|
+
await clientFor(connection).transport.json("/v1/tenant/reset", "POST", {
|
|
85
|
+
requestId: randomUUID(),
|
|
86
|
+
scope,
|
|
87
|
+
activeWork: "drain",
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
throw new CliError(error instanceof Error ? error.message : `Tenant reset failed: ${String(error)}`, 1);
|
|
92
|
+
}
|
|
93
|
+
console.log(`Reset the Tenant on ${where} (${scope}).`);
|
|
94
|
+
}
|
|
95
|
+
async function confirmOrThrow(prompt) {
|
|
96
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
97
|
+
throw new CliError("Confirmation required; pass --yes for non-interactive use.", 2);
|
|
98
|
+
}
|
|
99
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
100
|
+
try {
|
|
101
|
+
const answer = (await rl.question(prompt)).trim().toLowerCase();
|
|
102
|
+
if (answer !== "y" && answer !== "yes") {
|
|
103
|
+
throw new CliError("Cancelled.", 1);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
rl.close();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** `nylo endpoints [--json]` and `nylo endpoints ping <agent>`. */
|
|
111
|
+
export async function endpointsCommand(args) {
|
|
112
|
+
if (args[0] === "ping") {
|
|
113
|
+
const agentId = args[1];
|
|
114
|
+
if (!agentId || args.length > 2)
|
|
115
|
+
throw new CliError("Usage: nylo endpoints ping <agent>", 2);
|
|
116
|
+
const client = clientFor(await linkedConnection(projectRoot()));
|
|
117
|
+
const answer = await client.transport.json(`/v1/endpoints/${encodeURIComponent(agentId)}/ping`, "POST");
|
|
118
|
+
console.log(`${answer.agentId} serves ${answer.implementationVersion}${answer.manifestHash ? ` ${answer.manifestHash}` : ""}`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const json = args.includes("--json");
|
|
122
|
+
if (args.some((a) => a !== "--json"))
|
|
123
|
+
throw new CliError("Usage: nylo endpoints [--json] | nylo endpoints ping <agent>", 2);
|
|
124
|
+
const client = clientFor(await linkedConnection(projectRoot()));
|
|
125
|
+
const body = await client.transport.json("/v1/endpoints", "GET");
|
|
126
|
+
if (json) {
|
|
127
|
+
console.log(JSON.stringify(body, null, 2));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (body.endpoints.length === 0) {
|
|
131
|
+
console.log("No Action endpoints. Register one with createActionHandler(...).register({ url }).");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
for (const endpoint of body.endpoints)
|
|
135
|
+
console.log(endpointLine(endpoint));
|
|
136
|
+
}
|
|
137
|
+
/** One line of `nylo endpoints`: the agent, its URL and version, and how it is doing. */
|
|
138
|
+
export function endpointLine(endpoint) {
|
|
139
|
+
const { health } = endpoint;
|
|
140
|
+
const state = health.consecutiveFailures > 0
|
|
141
|
+
? `failing (${health.consecutiveFailures}): ${health.lastError?.message || health.lastError?.code || "unknown"}`
|
|
142
|
+
: health.lastSuccessAt
|
|
143
|
+
? `ok (last ${health.lastSuccessAt})`
|
|
144
|
+
: "no deliveries yet";
|
|
145
|
+
return `${endpoint.agentId} ${endpoint.url} ${endpoint.implementationVersion} ${state}`;
|
|
146
|
+
}
|
|
@@ -9,6 +9,11 @@ export type PromptedModel = {
|
|
|
9
9
|
provider: string;
|
|
10
10
|
model: string;
|
|
11
11
|
baseUrl?: string;
|
|
12
|
+
/** A custom endpoint's context window and output limit, when given. */
|
|
13
|
+
settings?: {
|
|
14
|
+
contextWindow?: number;
|
|
15
|
+
maxTokens?: number;
|
|
16
|
+
};
|
|
12
17
|
auth: Credential;
|
|
13
18
|
};
|
|
14
19
|
export type ModelCatalog = {
|
|
@@ -36,7 +41,6 @@ export type Selection = Readonly<{
|
|
|
36
41
|
export declare function fetchModelCatalog(options: {
|
|
37
42
|
url: string;
|
|
38
43
|
key: string;
|
|
39
|
-
tenantId: string;
|
|
40
44
|
fetchImpl?: typeof fetch;
|
|
41
45
|
}): Promise<ModelCatalog>;
|
|
42
46
|
export declare function configureProvider(options?: {
|
|
@@ -46,4 +50,11 @@ export declare function configureProvider(options?: {
|
|
|
46
50
|
output?: Writable;
|
|
47
51
|
/** Tenant API catalog from GET /v1/tenant/models. */
|
|
48
52
|
catalog?: ModelCatalog;
|
|
53
|
+
/** This installation's stable id, for OAuth flows that need one (OpenAI). */
|
|
54
|
+
deviceId?: () => string;
|
|
49
55
|
}): Promise<PromptedModel>;
|
|
56
|
+
/**
|
|
57
|
+
* A stable id for this CLI installation, created on first use under the Host root.
|
|
58
|
+
* OpenAI's "Sign in with ChatGPT" sends it as the agent host id.
|
|
59
|
+
*/
|
|
60
|
+
export declare function installationId(home?: string): string;
|
package/dist/model/configure.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
2
5
|
import { builtinModels } from "@earendil-works/pi-ai/providers/all";
|
|
3
6
|
import { createProvider, } from "@earendil-works/pi-ai";
|
|
4
7
|
import { stream, streamSimple, } from "@earendil-works/pi-ai/api/openai-completions";
|
|
8
|
+
import { resolveHome } from "../home.js";
|
|
5
9
|
export class ConfigurationCancelled extends Error {
|
|
6
10
|
signal;
|
|
7
11
|
exitCode;
|
|
@@ -36,8 +40,14 @@ function modelsFor(selection, credentials, catalog) {
|
|
|
36
40
|
baseUrl: selection.custom.baseUrl,
|
|
37
41
|
auth: {
|
|
38
42
|
apiKey: {
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
name: "Custom API key",
|
|
44
|
+
async login(interaction) {
|
|
45
|
+
const key = await interaction.prompt({
|
|
46
|
+
type: "secret",
|
|
47
|
+
message: "API key",
|
|
48
|
+
});
|
|
49
|
+
return { type: "api_key", key: key.trim() };
|
|
50
|
+
},
|
|
41
51
|
async resolve() {
|
|
42
52
|
const credential = await credentials.read("custom");
|
|
43
53
|
if (!credential || credential.type !== "api_key")
|
|
@@ -102,11 +112,10 @@ function modelsFor(selection, credentials, catalog) {
|
|
|
102
112
|
/** Fetch GET /v1/tenant/models when a connection is available. */
|
|
103
113
|
export async function fetchModelCatalog(options) {
|
|
104
114
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
105
|
-
const { PROTOCOL_HEADER, PROTOCOL_VERSION
|
|
115
|
+
const { PROTOCOL_HEADER, PROTOCOL_VERSION } = await import("@nylorun/agents");
|
|
106
116
|
const response = await fetchImpl(`${options.url.replace(/\/$/, "")}/v1/tenant/models`, {
|
|
107
117
|
headers: {
|
|
108
118
|
authorization: `Bearer ${options.key}`,
|
|
109
|
-
[TENANT_HEADER]: options.tenantId,
|
|
110
119
|
[PROTOCOL_HEADER]: String(PROTOCOL_VERSION),
|
|
111
120
|
Accept: "application/json",
|
|
112
121
|
},
|
|
@@ -119,6 +128,7 @@ export async function fetchModelCatalog(options) {
|
|
|
119
128
|
}
|
|
120
129
|
// Internal options also allow isolated prompt tests without changing process globals.
|
|
121
130
|
export async function configureProvider(options = {}) {
|
|
131
|
+
const login = { getDeviceId: options.deviceId ?? installationId };
|
|
122
132
|
const output = options.output ?? process.stdout;
|
|
123
133
|
const controller = new AbortController();
|
|
124
134
|
const signal = controller.signal;
|
|
@@ -161,6 +171,16 @@ export async function configureProvider(options = {}) {
|
|
|
161
171
|
signal.throwIfAborted();
|
|
162
172
|
return prompt.question(message, { signal });
|
|
163
173
|
}
|
|
174
|
+
/** A positive whole number of tokens, or undefined for the default. */
|
|
175
|
+
async function tokens(message) {
|
|
176
|
+
const answer = (await question(message)).trim();
|
|
177
|
+
if (!answer)
|
|
178
|
+
return undefined;
|
|
179
|
+
const value = Number(answer);
|
|
180
|
+
if (!Number.isInteger(value) || value < 1)
|
|
181
|
+
throw new Error(`${message.split(" (")[0]} must be a whole number of tokens.`);
|
|
182
|
+
return value;
|
|
183
|
+
}
|
|
164
184
|
try {
|
|
165
185
|
signal.throwIfAborted();
|
|
166
186
|
output.write("0. Custom OpenAI-compatible provider\n");
|
|
@@ -173,6 +193,12 @@ export async function configureProvider(options = {}) {
|
|
|
173
193
|
const model = (await question("Model id: ")).trim();
|
|
174
194
|
if (!baseUrl || !model)
|
|
175
195
|
throw new Error("A base URL and model id are required.");
|
|
196
|
+
const contextWindow = await tokens("Context window in tokens (Enter for 32768): ");
|
|
197
|
+
const maxTokens = await tokens("Max output tokens (Enter for 8192): ");
|
|
198
|
+
const settings = {
|
|
199
|
+
...(contextWindow ? { contextWindow } : {}),
|
|
200
|
+
...(maxTokens ? { maxTokens } : {}),
|
|
201
|
+
};
|
|
176
202
|
const selection = {
|
|
177
203
|
provider: "custom",
|
|
178
204
|
model,
|
|
@@ -180,8 +206,11 @@ export async function configureProvider(options = {}) {
|
|
|
180
206
|
};
|
|
181
207
|
const custom = modelsFor(selection, store, options.catalog);
|
|
182
208
|
if (!(await custom.models.checkAuth("custom", { signal })))
|
|
183
|
-
await custom.models.login("custom", "api_key", interaction());
|
|
184
|
-
return
|
|
209
|
+
await custom.models.login("custom", "api_key", interaction(), login);
|
|
210
|
+
return {
|
|
211
|
+
...prompted(selection),
|
|
212
|
+
...(Object.keys(settings).length ? { settings } : {}),
|
|
213
|
+
};
|
|
185
214
|
}
|
|
186
215
|
else {
|
|
187
216
|
const chosen = providers[choice - 1];
|
|
@@ -203,7 +232,7 @@ export async function configureProvider(options = {}) {
|
|
|
203
232
|
if (answer === "2")
|
|
204
233
|
method = "oauth";
|
|
205
234
|
}
|
|
206
|
-
await registry.models.login(chosen.id, method, interaction());
|
|
235
|
+
await registry.models.login(chosen.id, method, interaction(), login);
|
|
207
236
|
}
|
|
208
237
|
return prompted({ provider: chosen.id, model: model.id });
|
|
209
238
|
}
|
|
@@ -256,3 +285,22 @@ export async function configureProvider(options = {}) {
|
|
|
256
285
|
};
|
|
257
286
|
}
|
|
258
287
|
}
|
|
288
|
+
/**
|
|
289
|
+
* A stable id for this CLI installation, created on first use under the Host root.
|
|
290
|
+
* OpenAI's "Sign in with ChatGPT" sends it as the agent host id.
|
|
291
|
+
*/
|
|
292
|
+
export function installationId(home = resolveHome()) {
|
|
293
|
+
const file = join(home, "cli-installation-id");
|
|
294
|
+
try {
|
|
295
|
+
const existing = readFileSync(file, "utf8").trim();
|
|
296
|
+
if (existing)
|
|
297
|
+
return existing;
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
/* Created below on first use. */
|
|
301
|
+
}
|
|
302
|
+
const id = randomUUID();
|
|
303
|
+
mkdirSync(home, { recursive: true });
|
|
304
|
+
writeFileSync(file, `${id}\n`, { mode: 0o600 });
|
|
305
|
+
return id;
|
|
306
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { PromptedModel } from "./configure.js";
|
|
2
2
|
/** Wire shape of GET/PUT `/v1/tenant/model` — local copy so CLI stays off `@nylorun/core`. */
|
|
3
3
|
type HostModelView = {
|
|
4
4
|
readonly configured: false;
|
|
@@ -8,15 +8,11 @@ type HostModelView = {
|
|
|
8
8
|
readonly model: string;
|
|
9
9
|
readonly authType: "api_key" | "oauth";
|
|
10
10
|
readonly baseUrl?: string;
|
|
11
|
+
readonly settings?: {
|
|
12
|
+
contextWindow?: number;
|
|
13
|
+
maxTokens?: number;
|
|
14
|
+
};
|
|
11
15
|
};
|
|
12
|
-
|
|
13
|
-
export declare function putHostModel(runtimeUrl: string, serverKey: string, model: PromptedModel,
|
|
14
|
-
export declare function ensureHostModel(options: {
|
|
15
|
-
runtimeUrl: string;
|
|
16
|
-
serverKey: string;
|
|
17
|
-
tenantId: string;
|
|
18
|
-
root?: string;
|
|
19
|
-
env?: Readonly<Record<string, string>>;
|
|
20
|
-
signal?: AbortSignal;
|
|
21
|
-
}): Promise<void>;
|
|
16
|
+
/** Set the installation's Tenant model provider (`PUT /v1/tenant/model`). */
|
|
17
|
+
export declare function putHostModel(runtimeUrl: string, serverKey: string, model: PromptedModel, fetchImpl?: typeof fetch): Promise<HostModelView>;
|
|
22
18
|
export {};
|
package/dist/model/host-model.js
CHANGED
|
@@ -1,38 +1,12 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import { loadProjectEnvironment } from "../environment.js";
|
|
7
|
-
function tenantHeaders(serverKey, tenantId) {
|
|
8
|
-
return {
|
|
9
|
-
authorization: `Bearer ${serverKey}`,
|
|
10
|
-
[TENANT_HEADER]: tenantId,
|
|
11
|
-
[PROTOCOL_HEADER]: String(PROTOCOL_VERSION),
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
function resolveTenantId(explicit) {
|
|
15
|
-
const tenant = explicit?.trim() || process.env.NYLORUN_TENANT?.trim();
|
|
16
|
-
if (!tenant) {
|
|
17
|
-
throw new Error("Set NYLORUN_TENANT (or pass tenant via Project link) before calling Tenant model routes");
|
|
18
|
-
}
|
|
19
|
-
return tenant;
|
|
20
|
-
}
|
|
21
|
-
export async function getHostModel(runtimeUrl, serverKey, tenantId) {
|
|
22
|
-
const tenant = resolveTenantId(tenantId);
|
|
23
|
-
const response = await fetch(`${runtimeUrl}/v1/tenant/model`, {
|
|
24
|
-
headers: tenantHeaders(serverKey, tenant),
|
|
25
|
-
});
|
|
26
|
-
if (!response.ok)
|
|
27
|
-
throw new Error(`Runtime model status returned ${response.status}`);
|
|
28
|
-
return response.json();
|
|
29
|
-
}
|
|
30
|
-
export async function putHostModel(runtimeUrl, serverKey, model, tenantId) {
|
|
31
|
-
const tenant = resolveTenantId(tenantId);
|
|
32
|
-
const response = await fetch(`${runtimeUrl}/v1/tenant/model`, {
|
|
2
|
+
import { PROTOCOL_HEADER, PROTOCOL_VERSION } from "@nylorun/agents";
|
|
3
|
+
/** Set the installation's Tenant model provider (`PUT /v1/tenant/model`). */
|
|
4
|
+
export async function putHostModel(runtimeUrl, serverKey, model, fetchImpl = fetch) {
|
|
5
|
+
const response = await fetchImpl(`${runtimeUrl}/v1/tenant/model`, {
|
|
33
6
|
method: "PUT",
|
|
34
7
|
headers: {
|
|
35
|
-
|
|
8
|
+
authorization: `Bearer ${serverKey}`,
|
|
9
|
+
[PROTOCOL_HEADER]: String(PROTOCOL_VERSION),
|
|
36
10
|
"content-type": "application/json",
|
|
37
11
|
},
|
|
38
12
|
body: JSON.stringify({
|
|
@@ -41,6 +15,7 @@ export async function putHostModel(runtimeUrl, serverKey, model, tenantId) {
|
|
|
41
15
|
provider: model.provider,
|
|
42
16
|
model: model.model,
|
|
43
17
|
...(model.baseUrl ? { baseUrl: model.baseUrl } : {}),
|
|
18
|
+
...(model.settings ? { settings: model.settings } : {}),
|
|
44
19
|
auth: model.auth,
|
|
45
20
|
}),
|
|
46
21
|
});
|
|
@@ -49,57 +24,3 @@ export async function putHostModel(runtimeUrl, serverKey, model, tenantId) {
|
|
|
49
24
|
throw new Error(body.message ?? `Runtime rejected the model provider (${response.status})`);
|
|
50
25
|
return body;
|
|
51
26
|
}
|
|
52
|
-
export async function ensureHostModel(options) {
|
|
53
|
-
const current = await getHostModel(options.runtimeUrl, options.serverKey, options.tenantId);
|
|
54
|
-
if (current.configured)
|
|
55
|
-
return;
|
|
56
|
-
const env = options.env ?? loadProjectEnvironment(options.root ?? process.cwd());
|
|
57
|
-
if (env.NYLORUN_DEV_MODEL?.trim() === "fixture")
|
|
58
|
-
return;
|
|
59
|
-
const seeded = seedFromEnv(env, options.root ?? process.cwd());
|
|
60
|
-
if (seeded) {
|
|
61
|
-
await putHostModel(options.runtimeUrl, options.serverKey, seeded, options.tenantId);
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
65
|
-
const prompted = await configureProvider({
|
|
66
|
-
root: options.root,
|
|
67
|
-
signal: options.signal,
|
|
68
|
-
});
|
|
69
|
-
await putHostModel(options.runtimeUrl, options.serverKey, prompted, options.tenantId);
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
throw new Error("Model provider is not configured. Run nylo configure in a terminal to set it up, or open Studio.");
|
|
73
|
-
}
|
|
74
|
-
function seedFromEnv(env, root) {
|
|
75
|
-
const provider = env.MODEL_PROVIDER?.trim();
|
|
76
|
-
const model = env.MODEL?.trim();
|
|
77
|
-
if (!provider || !model)
|
|
78
|
-
return undefined;
|
|
79
|
-
const baseUrl = env.MODEL_PROVIDER_BASE_URL?.trim();
|
|
80
|
-
const key = env.MODEL_PROVIDER_API_KEY;
|
|
81
|
-
const auth = key
|
|
82
|
-
? { type: "api_key", key }
|
|
83
|
-
: legacyCredential(root, provider);
|
|
84
|
-
if (!auth)
|
|
85
|
-
return undefined;
|
|
86
|
-
return {
|
|
87
|
-
provider,
|
|
88
|
-
model,
|
|
89
|
-
...(baseUrl ? { baseUrl } : {}),
|
|
90
|
-
auth,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
function legacyCredential(root, provider) {
|
|
94
|
-
for (const relative of [".nylorun/auth.json", ".env/auth.json"]) {
|
|
95
|
-
try {
|
|
96
|
-
const all = JSON.parse(readFileSync(join(root, relative), "utf8"));
|
|
97
|
-
const credential = all[provider];
|
|
98
|
-
if (credential?.type === "api_key" || credential?.type === "oauth")
|
|
99
|
-
return credential;
|
|
100
|
-
}
|
|
101
|
-
catch {
|
|
102
|
-
/* A project without a legacy credential file is set up interactively. */
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ProjectLink } from "./link.js";
|
|
2
|
+
/** The installation `nylo` acts on: a Runtime URL and an application key. */
|
|
3
|
+
export interface LinkedConnection {
|
|
4
|
+
url: string;
|
|
5
|
+
key: string;
|
|
6
|
+
/** The Project link it came from, when it came from one. */
|
|
7
|
+
link?: ProjectLink;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The Project's link and credentials, else `NYLORUN_RUNTIME_URL` and `NYLORUN_SERVER_KEY`. The
|
|
11
|
+
* installation serves one Tenant, so nothing names it.
|
|
12
|
+
*/
|
|
13
|
+
export declare function linkedConnection(projectRoot: string, env?: Readonly<Record<string, string | undefined>>): Promise<LinkedConnection>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { CliError } from "../errors.js";
|
|
2
|
+
import { readCredentials } from "./credentials.js";
|
|
3
|
+
import { readLink } from "./link.js";
|
|
4
|
+
/**
|
|
5
|
+
* The Project's link and credentials, else `NYLORUN_RUNTIME_URL` and `NYLORUN_SERVER_KEY`. The
|
|
6
|
+
* installation serves one Tenant, so nothing names it.
|
|
7
|
+
*/
|
|
8
|
+
export async function linkedConnection(projectRoot, env = process.env) {
|
|
9
|
+
const link = await readLink(projectRoot);
|
|
10
|
+
const credentials = await readCredentials(projectRoot);
|
|
11
|
+
if (link && credentials)
|
|
12
|
+
return { url: link.hostUrl, key: credentials.applicationKey, link };
|
|
13
|
+
const url = env.NYLORUN_RUNTIME_URL?.trim();
|
|
14
|
+
const key = env.NYLORUN_SERVER_KEY?.trim();
|
|
15
|
+
if (url && key)
|
|
16
|
+
return { url: url.replace(/\/$/, ""), key };
|
|
17
|
+
throw new CliError(`No Project link in ${projectRoot}. Run "npx nylorun start" in this project to start its stack and link it, or set NYLORUN_RUNTIME_URL and NYLORUN_SERVER_KEY.`, 1);
|
|
18
|
+
}
|
|
@@ -1,19 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Project-local credentials (mode 0600).
|
|
3
|
-
*
|
|
2
|
+
* Project-local credentials (mode 0600): the application key and its principal. A format 0
|
|
3
|
+
* file may still carry a legacy `executors` map from before protocol 3; it is ignored.
|
|
4
4
|
*/
|
|
5
5
|
export interface ProjectCredentials {
|
|
6
6
|
format: 0 | 1;
|
|
7
7
|
applicationKey: string;
|
|
8
8
|
principalId: string;
|
|
9
|
-
/** Present only on format 0 files; ignored by version 1. */
|
|
10
|
-
executors?: Record<string, string>;
|
|
11
9
|
}
|
|
12
|
-
/** The Project's credentials,
|
|
13
|
-
export declare function readCredentials(projectRoot: string
|
|
14
|
-
export declare function writeCredentials(projectRoot: string, credentials: {
|
|
15
|
-
applicationKey: string;
|
|
16
|
-
principalId: string;
|
|
17
|
-
format?: 0 | 1;
|
|
18
|
-
}, tenantId?: string): Promise<void>;
|
|
19
|
-
export declare function removeCredentials(projectRoot: string, tenantId?: string): Promise<void>;
|
|
10
|
+
/** The Project's credentials, which `nylorun start` writes beside the link. */
|
|
11
|
+
export declare function readCredentials(projectRoot: string): Promise<ProjectCredentials | undefined>;
|
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
import { chmod, readFile
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { chmod, readFile } from "node:fs/promises";
|
|
3
2
|
import { CliError } from "../errors.js";
|
|
4
|
-
import { credentialsPath
|
|
5
|
-
/** The Project's credentials,
|
|
6
|
-
export async function readCredentials(projectRoot
|
|
7
|
-
const path = credentialsPath(projectRoot
|
|
3
|
+
import { credentialsPath } from "./link.js";
|
|
4
|
+
/** The Project's credentials, which `nylorun start` writes beside the link. */
|
|
5
|
+
export async function readCredentials(projectRoot) {
|
|
6
|
+
const path = credentialsPath(projectRoot);
|
|
8
7
|
try {
|
|
9
8
|
const value = JSON.parse(await readFile(path, "utf8"));
|
|
10
9
|
if (typeof value?.applicationKey !== "string" ||
|
|
11
10
|
!/^[0-9a-f]{64}$/.test(value.applicationKey) ||
|
|
12
11
|
typeof value?.principalId !== "string" ||
|
|
13
12
|
value.principalId.length === 0) {
|
|
14
|
-
throw new CliError(`Invalid Project credentials at ${path}. Remove .nylorun/credentials.json and run
|
|
13
|
+
throw new CliError(`Invalid Project credentials at ${path}. Remove .nylorun/credentials.json and run "npx nylorun start".`, 1);
|
|
15
14
|
}
|
|
16
15
|
const format = value.format === undefined || value.format === 0
|
|
17
16
|
? 0
|
|
@@ -22,17 +21,10 @@ export async function readCredentials(projectRoot, tenantId) {
|
|
|
22
21
|
throw new CliError(`Project credentials at ${path} have an unsupported format. Upgrade the CLI.`, 1);
|
|
23
22
|
}
|
|
24
23
|
await chmod(path, 0o600);
|
|
25
|
-
const executors = format === 0 &&
|
|
26
|
-
value.executors &&
|
|
27
|
-
typeof value.executors === "object" &&
|
|
28
|
-
!Array.isArray(value.executors)
|
|
29
|
-
? value.executors
|
|
30
|
-
: undefined;
|
|
31
24
|
return {
|
|
32
25
|
format,
|
|
33
26
|
applicationKey: value.applicationKey,
|
|
34
27
|
principalId: value.principalId,
|
|
35
|
-
...(executors ? { executors } : {}),
|
|
36
28
|
};
|
|
37
29
|
}
|
|
38
30
|
catch (error) {
|
|
@@ -41,25 +33,3 @@ export async function readCredentials(projectRoot, tenantId) {
|
|
|
41
33
|
throw error;
|
|
42
34
|
}
|
|
43
35
|
}
|
|
44
|
-
export async function writeCredentials(projectRoot, credentials, tenantId) {
|
|
45
|
-
await ensureProjectNylorunDir(projectRoot);
|
|
46
|
-
const path = credentialsPath(projectRoot, tenantId);
|
|
47
|
-
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
48
|
-
// Version 1 writes format 1 without executors (D§3.7).
|
|
49
|
-
const body = `${JSON.stringify({
|
|
50
|
-
format: credentials.format ?? 1,
|
|
51
|
-
applicationKey: credentials.applicationKey,
|
|
52
|
-
principalId: credentials.principalId,
|
|
53
|
-
}, null, 2)}\n`;
|
|
54
|
-
try {
|
|
55
|
-
await writeFile(temporary, body, { mode: 0o600 });
|
|
56
|
-
await rename(temporary, path);
|
|
57
|
-
await chmod(path, 0o600);
|
|
58
|
-
}
|
|
59
|
-
finally {
|
|
60
|
-
await rm(temporary, { force: true });
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
export async function removeCredentials(projectRoot, tenantId) {
|
|
64
|
-
await rm(credentialsPath(projectRoot, tenantId), { force: true });
|
|
65
|
-
}
|