@loreai/gateway 0.14.0 → 0.14.1

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/src/cli/run.ts DELETED
@@ -1,181 +0,0 @@
1
- /**
2
- * `lore run [command...]` — start gateway + launch an AI agent.
3
- *
4
- * If a command is given, launches it with gateway env vars injected.
5
- * If no command is given, auto-detects installed agents and either
6
- * uses the sole one found or prompts the user to pick.
7
- */
8
- import { spawn, type ChildProcess } from "node:child_process";
9
- import { createInterface } from "node:readline";
10
- import { startGateway, type StartOptions } from "./start";
11
- import { detectAgents, AGENTS, type DetectedAgent } from "./agents";
12
-
13
- // ---------------------------------------------------------------------------
14
- // Interactive agent picker (TTY only)
15
- // ---------------------------------------------------------------------------
16
-
17
- async function promptAgent(agents: DetectedAgent[]): Promise<DetectedAgent> {
18
- console.error("\n[lore] Multiple AI agents detected:\n");
19
- for (let i = 0; i < agents.length; i++) {
20
- console.error(` ${i + 1}) ${agents[i].def.displayName} (${agents[i].path})`);
21
- }
22
- console.error();
23
-
24
- const rl = createInterface({ input: process.stdin, output: process.stderr });
25
-
26
- return new Promise<DetectedAgent>((resolve) => {
27
- const ask = () => {
28
- rl.question("Choose an agent [1]: ", (answer) => {
29
- const trimmed = answer.trim();
30
- const idx = trimmed === "" ? 0 : Number.parseInt(trimmed, 10) - 1;
31
- if (Number.isInteger(idx) && idx >= 0 && idx < agents.length) {
32
- rl.close();
33
- resolve(agents[idx]);
34
- } else {
35
- console.error(` Invalid choice. Enter 1–${agents.length}.`);
36
- ask();
37
- }
38
- });
39
- };
40
- ask();
41
- });
42
- }
43
-
44
- // ---------------------------------------------------------------------------
45
- // Resolve what to launch
46
- // ---------------------------------------------------------------------------
47
-
48
- interface LaunchTarget {
49
- command: string;
50
- args: string[];
51
- env: Record<string, string>;
52
- }
53
-
54
- async function resolveLaunchTarget(
55
- gatewayUrl: string,
56
- cmdArgs: string[],
57
- ): Promise<LaunchTarget | null> {
58
- // --- Explicit command given: inject all known env vars ---
59
- if (cmdArgs.length > 0) {
60
- const env: Record<string, string> = {};
61
- for (const agent of AGENTS) {
62
- Object.assign(env, agent.envVars(gatewayUrl));
63
- }
64
- return { command: cmdArgs[0], args: cmdArgs.slice(1), env };
65
- }
66
-
67
- // --- No command: auto-detect agents ---
68
- const detected = detectAgents();
69
-
70
- if (detected.length === 0) {
71
- console.error("[lore] No known AI agents found on PATH.");
72
- console.error("[lore] Install one of: Claude Code (claude), Codex (codex), Pi (pi), OpenCode (opencode)");
73
- console.error(`[lore] Or run with an explicit command: lore run <command>`);
74
- return null;
75
- }
76
-
77
- let agent: DetectedAgent;
78
-
79
- if (detected.length === 1) {
80
- agent = detected[0];
81
- console.error(`[lore] Detected ${agent.def.displayName} at ${agent.path}`);
82
- } else if (process.stdin.isTTY) {
83
- agent = await promptAgent(detected);
84
- } else {
85
- // Non-TTY with multiple agents — can't prompt
86
- console.error("[lore] Multiple agents detected but stdin is not a TTY.");
87
- console.error("[lore] Specify which agent to run: lore run <command>");
88
- for (const a of detected) {
89
- console.error(` - ${a.def.displayName}: lore run ${a.def.binary}`);
90
- }
91
- return null;
92
- }
93
-
94
- return {
95
- command: agent.def.binary,
96
- args: [],
97
- env: agent.def.envVars(gatewayUrl),
98
- };
99
- }
100
-
101
- // ---------------------------------------------------------------------------
102
- // Child process management
103
- // ---------------------------------------------------------------------------
104
-
105
- function launchChild(target: LaunchTarget): ChildProcess {
106
- const env = { ...process.env, ...target.env };
107
-
108
- const child = spawn(target.command, target.args, {
109
- env,
110
- stdio: "inherit",
111
- });
112
-
113
- return child;
114
- }
115
-
116
- // ---------------------------------------------------------------------------
117
- // Command entry point
118
- // ---------------------------------------------------------------------------
119
-
120
- export async function commandRun(
121
- opts: StartOptions,
122
- cmdArgs: string[],
123
- ): Promise<void> {
124
- // 1. Start gateway
125
- const { config, port, shutdown } = startGateway(opts);
126
- const gatewayUrl = `http://${config.host}:${port}`;
127
-
128
- console.error(`[lore] Gateway listening on ${gatewayUrl}`);
129
-
130
- // 2. Resolve what to launch
131
- const target = await resolveLaunchTarget(gatewayUrl, cmdArgs);
132
-
133
- if (!target) {
134
- // No agent found / non-interactive — fall back to server-only mode
135
- console.error("[lore] Running in server-only mode. Point your agent at the gateway manually.");
136
- console.error(`[lore] export ANTHROPIC_BASE_URL=${gatewayUrl}`);
137
-
138
- const onSignal = async () => {
139
- await shutdown();
140
- process.exit(0);
141
- };
142
- process.on("SIGINT", () => onSignal());
143
- process.on("SIGTERM", () => onSignal());
144
-
145
- // Block forever
146
- return new Promise(() => {});
147
- }
148
-
149
- // 3. Launch agent child process
150
- console.error(`[lore] Launching: ${target.command} ${target.args.join(" ")}`.trimEnd());
151
-
152
- const child = launchChild(target);
153
-
154
- // Forward signals to child
155
- const forwardSignal = (signal: NodeJS.Signals) => {
156
- child.kill(signal);
157
- };
158
- process.on("SIGINT", () => forwardSignal("SIGINT"));
159
- process.on("SIGTERM", () => forwardSignal("SIGTERM"));
160
-
161
- // Wait for child to exit, then tear down gateway
162
- return new Promise<void>((resolve) => {
163
- child.on("exit", async (code, signal) => {
164
- await shutdown();
165
- // Exit with the child's code (or 128 + signal number for signal deaths)
166
- if (signal) {
167
- const SIGNAL_CODES: Record<string, number> = {
168
- SIGHUP: 1, SIGINT: 2, SIGQUIT: 3, SIGTERM: 15,
169
- };
170
- process.exit(128 + (SIGNAL_CODES[signal] ?? 1));
171
- }
172
- process.exit(code ?? 0);
173
- });
174
-
175
- child.on("error", async (err) => {
176
- console.error(`[lore] Failed to launch ${target.command}: ${err.message}`);
177
- await shutdown();
178
- process.exit(1);
179
- });
180
- });
181
- }
package/src/cli/start.ts DELETED
@@ -1,82 +0,0 @@
1
- /**
2
- * `lore start` — start the gateway server only.
3
- *
4
- * Extracted from the old top-level index.ts boot logic.
5
- */
6
- import { loadConfig, type GatewayConfig } from "../config";
7
- import { startServer } from "../server";
8
- import { resetPipelineState } from "../pipeline";
9
-
10
- export interface StartOptions {
11
- port?: number;
12
- host?: string;
13
- debug?: boolean;
14
- }
15
-
16
- /**
17
- * Start the gateway server, returning the actual port and a shutdown function.
18
- *
19
- * Merges CLI options on top of env-var config (CLI takes precedence).
20
- */
21
- export function startGateway(opts: StartOptions = {}): {
22
- config: GatewayConfig;
23
- port: number;
24
- shutdown: () => Promise<void>;
25
- } {
26
- const config = loadConfig();
27
-
28
- // CLI overrides
29
- if (opts.port !== undefined) config.port = opts.port;
30
- if (opts.host !== undefined) config.host = opts.host;
31
- if (opts.debug !== undefined) config.debug = opts.debug;
32
-
33
- const server = startServer(config);
34
- const actualPort = server.port;
35
-
36
- const shutdown = async () => {
37
- console.error("[lore] Shutting down…");
38
- server.stop();
39
- await resetPipelineState();
40
- };
41
-
42
- return { config, port: actualPort, shutdown };
43
- }
44
-
45
- /**
46
- * Run the `lore start` command — start gateway server and block until
47
- * SIGINT/SIGTERM.
48
- */
49
- export async function commandStart(opts: StartOptions): Promise<never> {
50
- const { config, port, shutdown } = startGateway(opts);
51
-
52
- const addr = `http://${config.host}:${port}`;
53
- console.error(`[lore] Gateway listening on ${addr}`);
54
- console.error(
55
- `[lore] Model routing: claude-* → Anthropic, nvidia/* → Nvidia NIM, gpt-* → OpenAI, …`,
56
- );
57
- console.error("");
58
- console.error("[lore] Point your AI agent at the gateway:");
59
- console.error(` export ANTHROPIC_BASE_URL=${addr}`);
60
- console.error(` export OPENAI_BASE_URL=${addr}/v1`);
61
- console.error("");
62
- console.error("[lore] Configuration (environment variables):");
63
- console.error(` LORE_LISTEN_PORT Port to listen on (current: ${port})`);
64
- console.error(` LORE_LISTEN_HOST Host to bind to (current: ${config.host})`);
65
- console.error(` LORE_UPSTREAM_ANTHROPIC Anthropic API URL (current: ${config.upstreamAnthropic})`);
66
- console.error(` LORE_UPSTREAM_OPENAI OpenAI API URL (current: ${config.upstreamOpenAI})`);
67
- console.error(` LORE_IDLE_TIMEOUT Idle timeout in seconds (current: ${config.idleTimeoutSeconds})`);
68
- console.error(` LORE_DEBUG Enable debug logging (current: ${config.debug})`);
69
- console.error(` LORE_BATCH_DISABLED Disable batch background work (current: ${process.env.LORE_BATCH_DISABLED === "1"})`);
70
-
71
- // Block until signal
72
- const onSignal = async () => {
73
- await shutdown();
74
- process.exit(0);
75
- };
76
-
77
- process.on("SIGINT", () => onSignal());
78
- process.on("SIGTERM", () => onSignal());
79
-
80
- // Keep the process alive (Bun.serve already does this, but be explicit)
81
- return new Promise(() => {});
82
- }
@@ -1,311 +0,0 @@
1
- /**
2
- * `lore upgrade [version]` — self-update command.
3
- *
4
- * Self-update the Lore CLI to the latest or a specific version.
5
- * After upgrading, replaces the running binary atomically.
6
- *
7
- * Supports two release channels:
8
- * - stable (default): tracks the latest GitHub release
9
- * - nightly: tracks the rolling nightly prerelease from GHCR
10
- *
11
- * The channel can be set via --channel or by passing "nightly"/"stable"
12
- * as the version argument. The choice is persisted in ~/.lore/channel.
13
- *
14
- * Flags:
15
- * --check Check for updates without installing
16
- * --force Force re-download even if up to date
17
- * --offline Upgrade from cached patches (no network)
18
- * --channel Set release channel (stable or nightly)
19
- *
20
- * Adapted from Sentry CLI's upgrade command — stripped of brew/npm
21
- * detection, setup spawning, release notes, and Sentry SDK telemetry.
22
- */
23
-
24
- import { parseArgs } from "node:util";
25
- import { dirname } from "node:path";
26
- import { VERSION } from "./version";
27
- import {
28
- isDowngrade,
29
- installBinary,
30
- determineInstallDir,
31
- releaseLock,
32
- } from "./lib/binary";
33
- import { UpgradeError } from "./lib/errors";
34
- import {
35
- executeUpgrade,
36
- fetchLatestVersion,
37
- getCurlInstallPaths,
38
- getReleaseChannel,
39
- NIGHTLY_TAG,
40
- type OfflineMode,
41
- type ReleaseChannel,
42
- setReleaseChannel,
43
- versionExists,
44
- VERSION_PREFIX_REGEX,
45
- } from "./lib/upgrade";
46
-
47
- /** Special version strings that select a channel */
48
- const CHANNEL_VERSIONS = new Set(["nightly", "stable"]);
49
-
50
- // ---------------------------------------------------------------------------
51
- // Flag parsing
52
- // ---------------------------------------------------------------------------
53
-
54
- interface UpgradeFlags {
55
- check: boolean;
56
- force: boolean;
57
- offline: boolean;
58
- channel?: string;
59
- }
60
-
61
- function parseUpgradeFlags(args: string[]): {
62
- flags: UpgradeFlags;
63
- versionArg: string | undefined;
64
- } {
65
- const { values, positionals } = parseArgs({
66
- args,
67
- options: {
68
- check: { type: "boolean", default: false },
69
- force: { type: "boolean", default: false },
70
- offline: { type: "boolean", default: false },
71
- channel: { type: "string" },
72
- },
73
- allowPositionals: true,
74
- strict: false,
75
- });
76
-
77
- return {
78
- flags: {
79
- check: !!values.check,
80
- force: !!values.force,
81
- offline: !!values.offline,
82
- channel: values.channel as string | undefined,
83
- },
84
- versionArg: positionals[0],
85
- };
86
- }
87
-
88
- // ---------------------------------------------------------------------------
89
- // Channel + version resolution
90
- // ---------------------------------------------------------------------------
91
-
92
- function resolveChannelAndVersion(
93
- versionArg: string | undefined,
94
- channelFlag: string | undefined,
95
- ): {
96
- channel: ReleaseChannel;
97
- cleanVersionArg: string | undefined;
98
- } {
99
- // "nightly" and "stable" as positional args select the channel
100
- const lower = versionArg?.toLowerCase();
101
- if (lower === "nightly" || lower === "stable") {
102
- return { channel: lower, cleanVersionArg: undefined };
103
- }
104
-
105
- // --channel flag overrides persisted channel
106
- if (channelFlag === "nightly" || channelFlag === "stable") {
107
- return { channel: channelFlag, cleanVersionArg: versionArg };
108
- }
109
-
110
- return {
111
- channel: getReleaseChannel(),
112
- cleanVersionArg: versionArg,
113
- };
114
- }
115
-
116
- // ---------------------------------------------------------------------------
117
- // Cached version for offline mode
118
- // ---------------------------------------------------------------------------
119
-
120
- /** Simple file-based cache for last-known latest version */
121
- function getCachedLatestVersion(): string | null {
122
- try {
123
- const fs = require("node:fs");
124
- const path = require("node:path");
125
- const { getConfigDir } = require("./lib/binary");
126
- const content = fs
127
- .readFileSync(path.join(getConfigDir(), "latest-version"), "utf-8")
128
- .trim();
129
- return content || null;
130
- } catch {
131
- return null;
132
- }
133
- }
134
-
135
- function setCachedLatestVersion(version: string): void {
136
- try {
137
- const fs = require("node:fs");
138
- const path = require("node:path");
139
- const { getConfigDir } = require("./lib/binary");
140
- const dir = getConfigDir();
141
- fs.mkdirSync(dir, { recursive: true });
142
- fs.writeFileSync(path.join(dir, "latest-version"), version, "utf-8");
143
- } catch {
144
- // Best-effort
145
- }
146
- }
147
-
148
- function resolveOfflineTarget(versionArg: string | undefined): string {
149
- if (versionArg) {
150
- return versionArg.replace(VERSION_PREFIX_REGEX, "");
151
- }
152
- const cached = getCachedLatestVersion();
153
- if (!cached) {
154
- throw new UpgradeError(
155
- "network_error",
156
- "No cached version available. Run `lore upgrade` with network access first, then retry with --offline.",
157
- );
158
- }
159
- return cached;
160
- }
161
-
162
- // ---------------------------------------------------------------------------
163
- // Main command
164
- // ---------------------------------------------------------------------------
165
-
166
- export async function commandUpgrade(args: string[]): Promise<void> {
167
- const { flags, versionArg: rawVersionArg } = parseUpgradeFlags(args);
168
-
169
- const { channel, cleanVersionArg } = resolveChannelAndVersion(
170
- rawVersionArg,
171
- flags.channel,
172
- );
173
-
174
- const currentChannel = getReleaseChannel();
175
- const channelChanged = channel !== currentChannel;
176
-
177
- console.error(`[lore] Current version: ${VERSION}`);
178
- console.error(`[lore] Channel: ${channel}`);
179
-
180
- // Resolve target version
181
- let target: string;
182
- let offline: OfflineMode = false;
183
-
184
- if (flags.offline) {
185
- // Read cached version BEFORE persisting channel
186
- target = resolveOfflineTarget(cleanVersionArg);
187
- offline = "explicit";
188
- console.error(`[lore] Offline mode: using cached target ${target}`);
189
- } else {
190
- try {
191
- const latest = await fetchLatestVersion(channel);
192
- target = cleanVersionArg?.replace(VERSION_PREFIX_REGEX, "") ?? latest;
193
-
194
- // Cache the latest version for future offline use
195
- setCachedLatestVersion(latest);
196
- } catch (error) {
197
- // Try offline fallback
198
- if (error instanceof UpgradeError && error.reason === "network_error") {
199
- try {
200
- target = resolveOfflineTarget(cleanVersionArg);
201
- offline = "network-fallback";
202
- console.error(
203
- "[lore] Network unavailable, falling back to cached upgrade target",
204
- );
205
- } catch {
206
- throw error; // Re-throw original network error
207
- }
208
- } else {
209
- throw error;
210
- }
211
- }
212
- }
213
-
214
- // Persist channel preference
215
- if (channelChanged || CHANNEL_VERSIONS.has(rawVersionArg ?? "")) {
216
- setReleaseChannel(channel);
217
- }
218
-
219
- // --check: just report status
220
- if (flags.check) {
221
- if (VERSION === target) {
222
- console.error(`[lore] Already up to date (${VERSION})`);
223
- } else {
224
- const direction = isDowngrade(VERSION, target)
225
- ? "Downgrade"
226
- : "Update";
227
- console.error(`[lore] ${direction} available: ${VERSION} -> ${target}`);
228
- console.error(`[lore] Run 'lore upgrade' to update.`);
229
- }
230
- if (offline) {
231
- console.error("[lore] (resolved from cache — network unavailable)");
232
- }
233
- return;
234
- }
235
-
236
- // Already on target — unless forced or switching channels
237
- if (VERSION === target && !flags.force && !channelChanged) {
238
- console.error(`[lore] Already up to date (${VERSION})`);
239
- return;
240
- }
241
-
242
- // Validate pinned version exists (skip for channel keywords)
243
- if (
244
- cleanVersionArg &&
245
- !CHANNEL_VERSIONS.has(cleanVersionArg) &&
246
- !offline
247
- ) {
248
- const exists = await versionExists(target, channel);
249
- if (!exists) {
250
- throw new UpgradeError(
251
- "version_not_found",
252
- `Version ${target} not found`,
253
- );
254
- }
255
- }
256
-
257
- const downgrade = isDowngrade(VERSION, target);
258
- const verb = downgrade ? "Downgrading" : "Upgrading";
259
- console.error(`[lore] ${verb} to ${target}...`);
260
-
261
- // Use the rolling "nightly" tag only when upgrading to latest nightly
262
- const downloadTag =
263
- channel === "nightly" && !cleanVersionArg ? NIGHTLY_TAG : undefined;
264
-
265
- // Download the new binary
266
- const downloadResult = await executeUpgrade(target, downloadTag, offline);
267
-
268
- if (downloadResult.patchBytes) {
269
- console.error(
270
- `[lore] Applied delta patch (${formatBytes(downloadResult.patchBytes)} downloaded)`,
271
- );
272
- }
273
-
274
- // Install: replace the current binary atomically
275
- try {
276
- const currentInstallDir = dirname(getCurlInstallPaths().installPath);
277
- const installDir = determineInstallDir(
278
- require("node:os").homedir(),
279
- process.env,
280
- );
281
- // Use current install dir if we're already in a known location
282
- const targetDir = process.execPath.startsWith(currentInstallDir)
283
- ? currentInstallDir
284
- : installDir;
285
-
286
- const installedPath = await installBinary(
287
- downloadResult.tempBinaryPath,
288
- targetDir,
289
- );
290
-
291
- console.error(
292
- `[lore] ${downgrade ? "Downgraded" : "Upgraded"} successfully: ${VERSION} -> ${target}`,
293
- );
294
- console.error(`[lore] Binary installed at: ${installedPath}`);
295
- if (offline) {
296
- console.error("[lore] (upgraded from cached patches)");
297
- }
298
- } finally {
299
- releaseLock(downloadResult.lockPath);
300
- }
301
- }
302
-
303
- // ---------------------------------------------------------------------------
304
- // Helpers
305
- // ---------------------------------------------------------------------------
306
-
307
- function formatBytes(bytes: number): string {
308
- if (bytes < 1024) return `${bytes} B`;
309
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
310
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
311
- }
@@ -1,22 +0,0 @@
1
- /**
2
- * CLI version — replaced at build time by esbuild `define`.
3
- *
4
- * During development (running via `bun run src/index.ts`), falls back
5
- * to reading package.json at runtime.
6
- */
7
-
8
- // esbuild replaces this with a string literal at bundle time.
9
- // In dev mode the identifier is left as-is and we fall back below.
10
- declare const LORE_CLI_VERSION: string | undefined;
11
-
12
- function readVersionFromPackageJson(): string {
13
- try {
14
- const pkg = require("../../package.json") as { version?: string };
15
- return pkg.version ?? "dev";
16
- } catch {
17
- return "dev";
18
- }
19
- }
20
-
21
- export const VERSION: string =
22
- typeof LORE_CLI_VERSION !== "undefined" ? LORE_CLI_VERSION : readVersionFromPackageJson();