@numairbaseer/scifcode 0.1.5 → 0.2.0

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/README.md CHANGED
@@ -21,23 +21,24 @@ Supported runtime packages cover:
21
21
  - Windows: arm64 and x64
22
22
  - baseline x64 builds for CPUs without AVX2
23
23
 
24
- ## Configure
24
+ ## Log in
25
25
 
26
- Scifcode does not store API keys in the repo. Set the API key or bearer token
27
- for your configured endpoint in the current shell:
26
+ Authenticate once through the Scifcode control-plane portal. The CLI opens a
27
+ short-lived authorization page and receives its own revocable session after you
28
+ approve it:
28
29
 
29
30
  ```bash
30
- export SCIFCODE_API_KEY="endpoint_or_modal_bearer_token"
31
+ scifcode login
31
32
  ```
32
33
 
33
- Scifcode uses the project's fixed Modal endpoint:
34
+ The session is stored at `~/.scifcode/session.json` with owner-only permissions.
35
+ Scifcode uses it only to authenticate with the control plane; upstream model
36
+ URLs and credentials remain server-side.
34
37
 
35
- ```bash
36
- https://danishshaik--reservoir-vllm-serve.modal.run/v1
37
- ```
38
-
39
- You usually only need to set `SCIFCODE_API_KEY`. The provider and model are
40
- fixed to the Scifcode Modal deployment.
38
+ The control plane defaults to `http://localhost:8080` for local development.
39
+ Self-hosted customers pass their URL once with
40
+ `scifcode login --control-plane https://scifcode.customer.example`; the URL is
41
+ stored with the session and reused. Non-local control planes must use HTTPS.
41
42
 
42
43
  ## Run
43
44
 
@@ -64,7 +65,8 @@ This happens before the runtime launches, so no separate initialization command
64
65
 
65
66
  | Variable | Required | Default | Description |
66
67
  | --- | ---: | --- | --- |
67
- | `SCIFCODE_API_KEY` | yes | none | API key or bearer token for the configured endpoint |
68
+ | `SCIFCODE_CONTROL_PLANE_URL` | no | `http://localhost:8080` | Alternative to `login --control-plane`; useful for managed installation and automation |
69
+ | `SCIFCODE_SESSION_TOKEN` | no | stored login session | Ephemeral process override for the control-plane session |
68
70
  | `SCIFCODE_CONTEXT_WINDOW` | no | `32768` | Context window reported for `scifcode-1.0` |
69
71
  | `SCIFCODE_MAX_TOKENS` | no | `8192` | Maximum output tokens reported for `scifcode-1.0` |
70
72
  | `SCIFCODE_RUNTIME_BIN` | no | installed platform package | Override path to a compiled Scifcode runtime binary |
@@ -78,23 +80,25 @@ Scifcode also loads private env defaults from:
78
80
  ./.scifcode.env
79
81
  ```
80
82
 
81
- Shell environment variables win over these files. Keep API keys in the global
82
- file or an ignored project `.scifcode.env`, not in committed source.
83
+ Shell environment variables win over these files. Do not put session tokens in
84
+ project env files or commit them to source control.
83
85
 
84
86
  ## Troubleshooting
85
87
 
86
- ### `Scifcode requires SCIFCODE_API_KEY`
88
+ ### `Scifcode requires a control-plane session`
87
89
 
88
- Set your key:
90
+ Log in through the browser. If you already have a portal session, you only need
91
+ to approve the CLI. Otherwise, complete the portal's email verification first:
89
92
 
90
93
  ```bash
91
- export SCIFCODE_API_KEY="endpoint_or_modal_bearer_token"
94
+ scifcode login
92
95
  ```
93
96
 
94
- Then retry:
97
+ Inspect or revoke the current session with:
95
98
 
96
99
  ```bash
97
- scifcode
100
+ scifcode whoami
101
+ scifcode logout
98
102
  ```
99
103
 
100
104
  ### Invalid Scifcode config
@@ -111,17 +115,12 @@ If this file contains invalid JSON, fix it or move it aside and rerun:
111
115
  scifcode
112
116
  ```
113
117
 
114
- ### Modal A100 endpoint
115
-
116
- Scifcode can also point at an OpenAI Chat Completions-compatible endpoint
117
- served from Modal. See `docs/modal-a100.md` for a Laguna XS.2 vLLM scaffold.
118
-
119
118
  ## Security
120
119
 
121
- Scifcode v0.1 does not store API keys in the repo. It writes an environment
122
- variable reference such as `{env:SCIFCODE_API_KEY}` into its runtime config and
123
- passes the current process environment to the agent process. `LAGUNA_API_KEY` is
124
- still accepted as a legacy fallback.
120
+ Scifcode never receives the upstream model credential. The control plane owns
121
+ the model endpoint, model-ID translation, and upstream authentication. The CLI
122
+ stores only a revocable user session token in an owner-readable file and injects
123
+ it into the runtime through `SCIFCODE_SESSION_TOKEN`.
125
124
 
126
125
  Scifcode launches the installed platform runtime directly with argument arrays
127
126
  and does not use shell string execution. Bun/source execution remains available
package/bin/scifcode.js CHANGED
@@ -2,4 +2,4 @@
2
2
 
3
3
  import { main } from "../src/cli.js";
4
4
 
5
- main();
5
+ await main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@numairbaseer/scifcode",
3
- "version": "0.1.5",
3
+ "version": "0.2.0",
4
4
  "description": "Scifcode: a coding CLI agent powered by Poolside Laguna.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -24,11 +24,11 @@
24
24
  ],
25
25
  "scripts": {
26
26
  "test": "node --test",
27
- "lint": "node --check bin/scifcode.js && node --check src/cli.js && node --check src/constants.js && node --check src/env-config.js && node --check src/ensure-scifcode-config.js && node --check src/paths.js && node --check src/run-runtime.js",
27
+ "lint": "node --check bin/scifcode.js && node --check src/cli.js && node --check src/constants.js && node --check src/control-plane-auth.js && node --check src/env-config.js && node --check src/ensure-scifcode-config.js && node --check src/paths.js && node --check src/run-runtime.js",
28
28
  "prepack": "npm test && npm run lint"
29
29
  },
30
30
  "dependencies": {
31
- "@numairbaseer/scifcode-runtime": "0.1.5"
31
+ "@numairbaseer/scifcode-runtime": "0.2.0"
32
32
  },
33
33
  "engines": {
34
34
  "node": ">=22.19.0"
package/src/cli.js CHANGED
@@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url";
4
4
  import { ensureScifcodeConfig } from "./ensure-scifcode-config.js";
5
5
  import { loadScifcodeEnv } from "./env-config.js";
6
6
  import { runRuntime } from "./run-runtime.js";
7
+ import { login, logout, whoami } from "./control-plane-auth.js";
7
8
 
8
9
  export function printHelp(stdout = console.log) {
9
10
  stdout(`Scifcode - coding CLI agent powered by Poolside Laguna
@@ -11,11 +12,13 @@ export function printHelp(stdout = console.log) {
11
12
  Usage:
12
13
  scifcode Start interactive coding agent
13
14
  scifcode -p "prompt" Run one-shot prompt
14
-
15
- Required environment:
16
- SCIFCODE_API_KEY API key or bearer token for the configured endpoint
15
+ scifcode login [--control-plane <url>]
16
+ Authenticate in the portal and start Scifcode
17
+ scifcode logout Revoke the current CLI session
18
+ scifcode whoami Show the authenticated user
17
19
 
18
20
  Optional environment:
21
+ SCIFCODE_CONTROL_PLANE_URL Control-plane origin (default: http://localhost:8080)
19
22
  SCIFCODE_RUNTIME_BIN Path to a compiled Scifcode runtime binary
20
23
  SCIFCODE_SOURCE_DIR Path to the Scifcode OpenCode fork
21
24
  SCIFCODE_BUN_BIN Path to Bun when launching the fork from source
@@ -45,7 +48,31 @@ function createConsoleWriters(stdout, stderr) {
45
48
  };
46
49
  }
47
50
 
48
- export function main(options = {}) {
51
+ export function parseLoginControlPlaneUrl(args = []) {
52
+ const loginArgs = args.slice(1);
53
+ let controlPlaneUrl;
54
+ for (let index = 0; index < loginArgs.length; index += 1) {
55
+ const argument = loginArgs[index];
56
+ let value;
57
+ if (argument === "--control-plane") {
58
+ value = loginArgs[index + 1];
59
+ if (!value || value.startsWith("-")) {
60
+ throw new Error("--control-plane requires a URL.");
61
+ }
62
+ index += 1;
63
+ } else if (argument.startsWith("--control-plane=")) {
64
+ value = argument.slice("--control-plane=".length);
65
+ if (!value) throw new Error("--control-plane requires a URL.");
66
+ } else {
67
+ throw new Error(`Unknown login option: ${argument}`);
68
+ }
69
+ if (controlPlaneUrl) throw new Error("Specify --control-plane only once.");
70
+ controlPlaneUrl = value;
71
+ }
72
+ return controlPlaneUrl;
73
+ }
74
+
75
+ export async function main(options = {}) {
49
76
  const args = options.args || process.argv.slice(2);
50
77
  const command = args[0];
51
78
  const baseEnv = options.env || process.env;
@@ -66,6 +93,16 @@ export function main(options = {}) {
66
93
  options.stderr || console.error
67
94
  );
68
95
 
96
+ const authOptions = {
97
+ env,
98
+ homeDir: options.homeDir,
99
+ stdout: writers.log,
100
+ input: options.input,
101
+ output: options.output,
102
+ question: options.question,
103
+ fetchFn: options.fetchFn
104
+ };
105
+
69
106
  if (command === "--help" || command === "-h") {
70
107
  printHelp(writers.log);
71
108
  exit(0);
@@ -78,13 +115,43 @@ export function main(options = {}) {
78
115
  return;
79
116
  }
80
117
 
118
+ let runtimeArgs = args;
119
+ try {
120
+ if (command === "login") {
121
+ await (options.loginFn || login)({
122
+ ...authOptions,
123
+ controlPlaneUrl: parseLoginControlPlaneUrl(args)
124
+ });
125
+ runtimeArgs = [];
126
+ }
127
+ else if (command === "logout") {
128
+ await (options.logoutFn || logout)(authOptions);
129
+ writers.log("Logged out.");
130
+ exit(0);
131
+ return;
132
+ }
133
+ else if (command === "whoami") {
134
+ const identity = await (options.whoamiFn || whoami)(authOptions);
135
+ writers.log(identity.user.email);
136
+ writers.log(`Role: ${identity.user.role}`);
137
+ writers.log(`Session expires: ${identity.expiresAt}`);
138
+ writers.log(`Control plane: ${identity.controlPlaneUrl}`);
139
+ exit(0);
140
+ return;
141
+ }
142
+ } catch (error) {
143
+ writers.error(error.message);
144
+ exit(1);
145
+ return;
146
+ }
147
+
81
148
  try {
82
- ensureScifcodeConfigFn({ env });
149
+ ensureScifcodeConfigFn({ env, homeDir: options.homeDir });
83
150
  } catch (error) {
84
151
  writers.error(error.message);
85
152
  exit(1);
86
153
  return;
87
154
  }
88
155
 
89
- runRuntimeFn(args, { env, exit, stderr: writers.error });
156
+ runRuntimeFn(runtimeArgs, { env, exit, stderr: writers.error, homeDir: options.homeDir });
90
157
  }
package/src/constants.js CHANGED
@@ -1,8 +1,31 @@
1
1
  export const DEFAULT_PROVIDER_NAME = "scifcode";
2
- export const DEFAULT_BASE_URL = "https://danishshaik--reservoir-vllm-serve.modal.run/v1";
3
- export const DEFAULT_MODEL = "reservoir-modal";
2
+ export const DEFAULT_CONTROL_PLANE_URL = "http://localhost:8080";
3
+ export const DEFAULT_MODEL = "scifcode-1.0";
4
4
  export const DEFAULT_MODEL_NAME = "scifcode-1.0";
5
5
  export const DEFAULT_CONTEXT_WINDOW = 32768;
6
6
  export const DEFAULT_MAX_TOKENS = 8192;
7
- export const API_KEY_ENV_NAME = "SCIFCODE_API_KEY";
8
- export const LEGACY_API_KEY_ENV_NAME = "LAGUNA_API_KEY";
7
+ export const SESSION_TOKEN_ENV_NAME = "SCIFCODE_SESSION_TOKEN";
8
+
9
+ export function normalizeControlPlaneUrl(value) {
10
+ const normalized = String(value || "").trim();
11
+ let url;
12
+ try {
13
+ url = new URL(normalized);
14
+ } catch {
15
+ throw new Error(`Invalid control-plane URL: ${normalized}`);
16
+ }
17
+ const local = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
18
+ if (url.protocol !== "https:" && !(local && url.protocol === "http:")) {
19
+ throw new Error("The control-plane URL must use HTTPS except on localhost.");
20
+ }
21
+ return url.toString().replace(/\/$/, "");
22
+ }
23
+
24
+ export function getControlPlaneUrl(env = process.env) {
25
+ const value = String(env.SCIFCODE_CONTROL_PLANE_URL || DEFAULT_CONTROL_PLANE_URL).trim();
26
+ return normalizeControlPlaneUrl(value);
27
+ }
28
+
29
+ export function getModelBaseUrl(env = process.env) {
30
+ return `${getControlPlaneUrl(env)}/v1`;
31
+ }
@@ -0,0 +1,143 @@
1
+ import fs from "node:fs";
2
+ import { spawn } from "node:child_process";
3
+ import { getControlPlaneUrl, normalizeControlPlaneUrl } from "./constants.js";
4
+ import { getScifcodeHomeDir, getScifcodeSessionPath } from "./paths.js";
5
+
6
+ async function requestJson(fetchFn, url, options = {}) {
7
+ let response;
8
+ try {
9
+ response = await fetchFn(url, options);
10
+ } catch (error) {
11
+ throw new Error(`Could not reach the Scifcode control plane. ${error.message}`);
12
+ }
13
+ const text = await response.text();
14
+ let body = {};
15
+ try { body = text ? JSON.parse(text) : {}; } catch {}
16
+ if (!response.ok) {
17
+ const reason = body.error || body.message || `HTTP ${response.status}`;
18
+ const error = new Error(`Control-plane request failed: ${reason}`);
19
+ error.status = response.status;
20
+ error.code = body.error;
21
+ throw error;
22
+ }
23
+ return body;
24
+ }
25
+
26
+ export function readSession(options = {}) {
27
+ const sessionPath = options.sessionPath || getScifcodeSessionPath(options.homeDir);
28
+ if (!fs.existsSync(sessionPath)) return null;
29
+ try {
30
+ const session = JSON.parse(fs.readFileSync(sessionPath, "utf8"));
31
+ if (!session.sessionToken || !session.controlPlaneUrl) return null;
32
+ return session;
33
+ } catch {
34
+ throw new Error(`Scifcode could not parse ${sessionPath}. Run \`scifcode login\` again.`);
35
+ }
36
+ }
37
+
38
+ export function writeSession(session, options = {}) {
39
+ const homeDir = options.homeDir;
40
+ const directory = options.sessionDir || getScifcodeHomeDir(homeDir);
41
+ const sessionPath = options.sessionPath || getScifcodeSessionPath(homeDir);
42
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
43
+ fs.chmodSync(directory, 0o700);
44
+ const temporaryPath = `${sessionPath}.${process.pid}.tmp`;
45
+ fs.writeFileSync(temporaryPath, `${JSON.stringify(session, null, 2)}\n`, { mode: 0o600 });
46
+ fs.renameSync(temporaryPath, sessionPath);
47
+ fs.chmodSync(sessionPath, 0o600);
48
+ return sessionPath;
49
+ }
50
+
51
+ export function deleteSession(options = {}) {
52
+ const sessionPath = options.sessionPath || getScifcodeSessionPath(options.homeDir);
53
+ if (fs.existsSync(sessionPath)) fs.unlinkSync(sessionPath);
54
+ }
55
+
56
+ export function openBrowser(url, options = {}) {
57
+ const platform = options.platform || process.platform;
58
+ const command = platform === "darwin" ? "open" : platform === "win32" ? "cmd.exe" : "xdg-open";
59
+ const args = platform === "win32" ? ["/c", "start", "", url] : [url];
60
+ const child = (options.spawnFn || spawn)(command, args, {
61
+ stdio: "ignore",
62
+ detached: true
63
+ });
64
+ child.on?.("error", () => {});
65
+ child.unref?.();
66
+ }
67
+
68
+ export async function login(options = {}) {
69
+ const fetchFn = options.fetchFn || fetch;
70
+ const stdout = options.stdout || console.log;
71
+ const controlPlaneUrl = normalizeControlPlaneUrl(
72
+ options.controlPlaneUrl || getControlPlaneUrl(options.env)
73
+ );
74
+ const authorization = await requestJson(fetchFn, `${controlPlaneUrl}/auth/device/request`, {
75
+ method: "POST",
76
+ headers: { "content-type": "application/json" },
77
+ body: "{}"
78
+ });
79
+ if (!authorization.deviceCode || !authorization.verificationUriComplete) {
80
+ throw new Error("The control plane returned an invalid device authorization.");
81
+ }
82
+
83
+ stdout("Authenticate Scifcode in your browser:");
84
+ stdout(authorization.verificationUriComplete);
85
+ try {
86
+ (options.openBrowserFn || openBrowser)(authorization.verificationUriComplete);
87
+ } catch {}
88
+
89
+ const now = options.nowFn || Date.now;
90
+ const sleep = options.sleepFn || (milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds)));
91
+ const interval = Math.max(Number(authorization.interval) || 2, 1) * 1000;
92
+ const deadline = now() + Math.max(Number(authorization.expiresIn) || 600, 1) * 1000;
93
+ let verified;
94
+ while (now() < deadline) {
95
+ await sleep(interval);
96
+ try {
97
+ verified = await requestJson(fetchFn, `${controlPlaneUrl}/auth/device/token`, {
98
+ method: "POST",
99
+ headers: { "content-type": "application/json" },
100
+ body: JSON.stringify({ deviceCode: authorization.deviceCode })
101
+ });
102
+ break;
103
+ } catch (error) {
104
+ if (error.code === "authorization_pending") continue;
105
+ throw error;
106
+ }
107
+ }
108
+ if (!verified?.sessionToken) {
109
+ throw new Error("CLI authorization expired. Run `scifcode login` again.");
110
+ }
111
+ const session = {
112
+ sessionToken: verified.sessionToken,
113
+ email: verified.user?.email,
114
+ expiresAt: verified.expiresAt,
115
+ controlPlaneUrl
116
+ };
117
+ writeSession(session, options);
118
+ stdout(`Logged in as ${session.email}.`);
119
+ return session;
120
+ }
121
+
122
+ export async function whoami(options = {}) {
123
+ const session = readSession(options);
124
+ if (!session) throw new Error("Not logged in. Run `scifcode login`.");
125
+ const result = await requestJson(options.fetchFn || fetch, `${session.controlPlaneUrl}/auth/me`, {
126
+ headers: { authorization: `Bearer ${session.sessionToken}` }
127
+ });
128
+ return { ...result, controlPlaneUrl: session.controlPlaneUrl };
129
+ }
130
+
131
+ export async function logout(options = {}) {
132
+ const session = readSession(options);
133
+ try {
134
+ if (session) {
135
+ await requestJson(options.fetchFn || fetch, `${session.controlPlaneUrl}/auth/logout`, {
136
+ method: "POST",
137
+ headers: { authorization: `Bearer ${session.sessionToken}` }
138
+ });
139
+ }
140
+ } finally {
141
+ deleteSession(options);
142
+ }
143
+ }
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs";
2
- import { API_KEY_ENV_NAME, DEFAULT_BASE_URL, DEFAULT_CONTEXT_WINDOW, DEFAULT_MAX_TOKENS, DEFAULT_MODEL, DEFAULT_MODEL_NAME, DEFAULT_PROVIDER_NAME, LEGACY_API_KEY_ENV_NAME } from "./constants.js";
2
+ import { DEFAULT_CONTEXT_WINDOW, DEFAULT_MAX_TOKENS, DEFAULT_MODEL, DEFAULT_MODEL_NAME, DEFAULT_PROVIDER_NAME, SESSION_TOKEN_ENV_NAME, getModelBaseUrl, normalizeControlPlaneUrl } from "./constants.js";
3
+ import { readSession } from "./control-plane-auth.js";
3
4
  import { getScifcodeConfigDir, getScifcodeConfigPath } from "./paths.js";
4
5
 
5
6
  function positiveInteger(value, fallback) {
@@ -19,19 +20,16 @@ export function buildScifcodeModels(env = process.env) {
19
20
  };
20
21
  }
21
22
 
22
- export function getApiKeyReference(env = process.env) {
23
- return env[LEGACY_API_KEY_ENV_NAME] && !env[API_KEY_ENV_NAME]
24
- ? `{env:${LEGACY_API_KEY_ENV_NAME}}`
25
- : `{env:${API_KEY_ENV_NAME}}`;
26
- }
27
-
28
- export function buildScifcodeProvider(env = process.env) {
23
+ export function buildScifcodeProvider(env = process.env, options = {}) {
24
+ const baseURL = options.controlPlaneUrl
25
+ ? `${normalizeControlPlaneUrl(options.controlPlaneUrl)}/v1`
26
+ : getModelBaseUrl(env);
29
27
  return {
30
28
  npm: "@ai-sdk/openai-compatible",
31
29
  name: "Scifcode",
32
30
  options: {
33
- baseURL: DEFAULT_BASE_URL,
34
- apiKey: getApiKeyReference(env)
31
+ baseURL,
32
+ apiKey: `{env:${SESSION_TOKEN_ENV_NAME}}`
35
33
  },
36
34
  models: buildScifcodeModels(env)
37
35
  };
@@ -49,7 +47,10 @@ export function ensureScifcodeConfig(options = {}) {
49
47
  const configPath = getScifcodeConfigPath(options.homeDir);
50
48
  const providerName = DEFAULT_PROVIDER_NAME;
51
49
  const existing = readConfig(configPath);
52
- const provider = buildScifcodeProvider(env);
50
+ const session = (options.readSessionFn || readSession)({ homeDir: options.homeDir });
51
+ const provider = buildScifcodeProvider(env, {
52
+ controlPlaneUrl: session?.controlPlaneUrl || env.SCIFCODE_CONTROL_PLANE_URL
53
+ });
53
54
  const merged = {
54
55
  ...existing,
55
56
  $schema: existing.$schema || "https://opencode.ai/config.json",
package/src/paths.js CHANGED
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
 
4
4
  export function getScifcodeHomeDir(homeDir = os.homedir()) { return path.join(homeDir, ".scifcode"); }
5
5
  export function getScifcodeEnvPath(homeDir = os.homedir()) { return path.join(getScifcodeHomeDir(homeDir), "env"); }
6
+ export function getScifcodeSessionPath(homeDir = os.homedir()) { return path.join(getScifcodeHomeDir(homeDir), "session.json"); }
6
7
  export function getProjectScifcodeEnvPath(cwd = process.cwd()) { return path.join(cwd, ".scifcode.env"); }
7
8
  export function getScifcodeConfigDir(homeDir = os.homedir()) { return path.join(homeDir, ".config", "scifcode"); }
8
9
  export function getScifcodeConfigPath(homeDir = os.homedir()) { return path.join(getScifcodeConfigDir(homeDir), "scifcode.json"); }
@@ -3,7 +3,8 @@ import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { spawn } from "node:child_process";
5
5
  import { fileURLToPath } from "node:url";
6
- import { API_KEY_ENV_NAME, DEFAULT_MODEL, DEFAULT_PROVIDER_NAME, LEGACY_API_KEY_ENV_NAME } from "./constants.js";
6
+ import { DEFAULT_MODEL, DEFAULT_PROVIDER_NAME, SESSION_TOKEN_ENV_NAME } from "./constants.js";
7
+ import { readSession } from "./control-plane-auth.js";
7
8
 
8
9
  function executableOnPath(name, env = process.env) {
9
10
  return String(env.PATH || "").split(path.delimiter).map(dir => path.join(dir, name)).find(fs.existsSync);
@@ -81,28 +82,34 @@ export function buildRuntimeArgs(userArgs = [], env = process.env, runtime = res
81
82
  return [...runtime.argsPrefix, "--model", model, ...withoutModelOverrides];
82
83
  }
83
84
 
84
- export function buildRuntimeEnv(env = process.env) {
85
+ export function buildRuntimeEnv(env = process.env, options = {}) {
85
86
  const runtimeEnv = { ...env };
86
- if (!runtimeEnv[API_KEY_ENV_NAME] && env[LEGACY_API_KEY_ENV_NAME]) runtimeEnv[API_KEY_ENV_NAME] = env[LEGACY_API_KEY_ENV_NAME];
87
+ if (!runtimeEnv[SESSION_TOKEN_ENV_NAME]) {
88
+ const session = (options.readSessionFn || readSession)({ homeDir: options.homeDir });
89
+ const expiresAt = session?.expiresAt ? Date.parse(session.expiresAt) : Number.POSITIVE_INFINITY;
90
+ if (session?.sessionToken && expiresAt > Date.now()) {
91
+ runtimeEnv[SESSION_TOKEN_ENV_NAME] = session.sessionToken;
92
+ }
93
+ }
87
94
  return runtimeEnv;
88
95
  }
89
96
 
90
- export function validateLagunaApiKey(env = process.env) { return Boolean(env[API_KEY_ENV_NAME] || env[LEGACY_API_KEY_ENV_NAME]); }
91
- export function printMissingApiKey(stderr = console.error) {
92
- stderr(`Scifcode requires ${API_KEY_ENV_NAME}.`);
97
+ export function validateSession(env = process.env) { return Boolean(env[SESSION_TOKEN_ENV_NAME]); }
98
+ export function printMissingSession(stderr = console.error) {
99
+ stderr("Scifcode requires a control-plane session.");
93
100
  stderr("");
94
- stderr(`Run:\n export ${API_KEY_ENV_NAME}="endpoint_or_modal_bearer_token"\n scifcode`);
101
+ stderr("Run:\n scifcode login");
95
102
  }
96
103
 
97
104
  export function runRuntime(userArgs = [], options = {}) {
98
105
  const env = options.env || process.env;
99
106
  const stderr = options.stderr || console.error;
100
107
  const exit = options.exit || process.exit;
101
- if (!validateLagunaApiKey(env)) { printMissingApiKey(stderr); exit(1); return null; }
108
+ const runtimeEnv = buildRuntimeEnv(env, options);
109
+ if (!validateSession(runtimeEnv)) { printMissingSession(stderr); exit(1); return null; }
102
110
  let runtime;
103
111
  try { runtime = (options.resolveRuntimeFn || resolveRuntime)({ env }); }
104
112
  catch (error) { stderr("Failed to resolve the Scifcode agent runtime."); stderr(error.message); exit(1); return null; }
105
- const runtimeEnv = buildRuntimeEnv(env);
106
113
  const projectDir = options.cwd || process.cwd();
107
114
  const child = (options.spawnFn || spawn)(runtime.command, buildRuntimeArgs(userArgs, runtimeEnv, runtime, projectDir), {
108
115
  stdio: "inherit",