@numairbaseer/scifcode 0.1.6 → 0.3.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/src/cli.js CHANGED
@@ -1,9 +1,8 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { fileURLToPath } from "node:url";
4
1
  import { ensureScifcodeConfig } from "./ensure-scifcode-config.js";
5
2
  import { loadScifcodeEnv } from "./env-config.js";
6
3
  import { runRuntime } from "./run-runtime.js";
4
+ import { login, logout, whoami } from "./control-plane-auth.js";
5
+ import packageJson from "../package.json" with { type: "json" };
7
6
 
8
7
  export function printHelp(stdout = console.log) {
9
8
  stdout(`Scifcode - coding CLI agent powered by Poolside Laguna
@@ -11,11 +10,13 @@ export function printHelp(stdout = console.log) {
11
10
  Usage:
12
11
  scifcode Start interactive coding agent
13
12
  scifcode -p "prompt" Run one-shot prompt
14
-
15
- Required environment:
16
- SCIFCODE_API_KEY API key or bearer token for the configured endpoint
13
+ scifcode login [--control-plane <url>]
14
+ Authenticate in the portal and start Scifcode
15
+ scifcode logout Revoke the current CLI session
16
+ scifcode whoami Show the authenticated user
17
17
 
18
18
  Optional environment:
19
+ SCIFCODE_CONTROL_PLANE_URL Control-plane origin (default: http://localhost:8080)
19
20
  SCIFCODE_RUNTIME_BIN Path to a compiled Scifcode runtime binary
20
21
  SCIFCODE_SOURCE_DIR Path to the Scifcode OpenCode fork
21
22
  SCIFCODE_BUN_BIN Path to Bun when launching the fork from source
@@ -27,11 +28,7 @@ Private config files:
27
28
  }
28
29
 
29
30
  export function getPackageVersion() {
30
- const currentFile = fileURLToPath(import.meta.url);
31
- const srcDir = path.dirname(currentFile);
32
- const packagePath = path.join(srcDir, "..", "package.json");
33
- const pkg = JSON.parse(fs.readFileSync(packagePath, "utf8"));
34
- return pkg.version;
31
+ return packageJson.version;
35
32
  }
36
33
 
37
34
  function createConsoleWriters(stdout, stderr) {
@@ -45,7 +42,31 @@ function createConsoleWriters(stdout, stderr) {
45
42
  };
46
43
  }
47
44
 
48
- export function main(options = {}) {
45
+ export function parseLoginControlPlaneUrl(args = []) {
46
+ const loginArgs = args.slice(1);
47
+ let controlPlaneUrl;
48
+ for (let index = 0; index < loginArgs.length; index += 1) {
49
+ const argument = loginArgs[index];
50
+ let value;
51
+ if (argument === "--control-plane") {
52
+ value = loginArgs[index + 1];
53
+ if (!value || value.startsWith("-")) {
54
+ throw new Error("--control-plane requires a URL.");
55
+ }
56
+ index += 1;
57
+ } else if (argument.startsWith("--control-plane=")) {
58
+ value = argument.slice("--control-plane=".length);
59
+ if (!value) throw new Error("--control-plane requires a URL.");
60
+ } else {
61
+ throw new Error(`Unknown login option: ${argument}`);
62
+ }
63
+ if (controlPlaneUrl) throw new Error("Specify --control-plane only once.");
64
+ controlPlaneUrl = value;
65
+ }
66
+ return controlPlaneUrl;
67
+ }
68
+
69
+ export async function main(options = {}) {
49
70
  const args = options.args || process.argv.slice(2);
50
71
  const command = args[0];
51
72
  const baseEnv = options.env || process.env;
@@ -66,6 +87,16 @@ export function main(options = {}) {
66
87
  options.stderr || console.error
67
88
  );
68
89
 
90
+ const authOptions = {
91
+ env,
92
+ homeDir: options.homeDir,
93
+ stdout: writers.log,
94
+ input: options.input,
95
+ output: options.output,
96
+ question: options.question,
97
+ fetchFn: options.fetchFn
98
+ };
99
+
69
100
  if (command === "--help" || command === "-h") {
70
101
  printHelp(writers.log);
71
102
  exit(0);
@@ -78,13 +109,43 @@ export function main(options = {}) {
78
109
  return;
79
110
  }
80
111
 
112
+ let runtimeArgs = args;
113
+ try {
114
+ if (command === "login") {
115
+ await (options.loginFn || login)({
116
+ ...authOptions,
117
+ controlPlaneUrl: parseLoginControlPlaneUrl(args)
118
+ });
119
+ runtimeArgs = [];
120
+ }
121
+ else if (command === "logout") {
122
+ await (options.logoutFn || logout)(authOptions);
123
+ writers.log("Logged out.");
124
+ exit(0);
125
+ return;
126
+ }
127
+ else if (command === "whoami") {
128
+ const identity = await (options.whoamiFn || whoami)(authOptions);
129
+ writers.log(identity.user.email);
130
+ writers.log(`Role: ${identity.user.role}`);
131
+ writers.log(`Session expires: ${identity.expiresAt}`);
132
+ writers.log(`Control plane: ${identity.controlPlaneUrl}`);
133
+ exit(0);
134
+ return;
135
+ }
136
+ } catch (error) {
137
+ writers.error(error.message);
138
+ exit(1);
139
+ return;
140
+ }
141
+
81
142
  try {
82
- ensureScifcodeConfigFn({ env });
143
+ ensureScifcodeConfigFn({ env, homeDir: options.homeDir });
83
144
  } catch (error) {
84
145
  writers.error(error.message);
85
146
  exit(1);
86
147
  return;
87
148
  }
88
149
 
89
- runRuntimeFn(args, { env, exit, stderr: writers.error });
150
+ runRuntimeFn(runtimeArgs, { env, exit, stderr: writers.error, homeDir: options.homeDir });
90
151
  }
@@ -0,0 +1,86 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { spawnSync } from "node:child_process";
6
+
7
+ function runGit(args, options = {}) {
8
+ const result = (options.spawnSyncFn || spawnSync)("git", args, {
9
+ cwd: options.cwd,
10
+ env: options.env,
11
+ encoding: "utf8",
12
+ stdio: ["ignore", "pipe", "pipe"]
13
+ });
14
+ if (result.error) throw result.error;
15
+ if (result.status !== 0) {
16
+ throw new Error(String(result.stderr || "Git command failed.").trim());
17
+ }
18
+ return String(result.stdout || "").trim();
19
+ }
20
+
21
+ function snapshotWorkingTree(repoDir, options = {}) {
22
+ const temporaryDir = fs.mkdtempSync(path.join(options.tmpDir || os.tmpdir(), "scifcode-git-index-"));
23
+ const indexPath = path.join(temporaryDir, "index");
24
+ const env = { ...(options.env || process.env), GIT_INDEX_FILE: indexPath };
25
+ try {
26
+ try {
27
+ runGit(["read-tree", "HEAD"], { ...options, cwd: repoDir, env });
28
+ } catch {
29
+ runGit(["read-tree", "--empty"], { ...options, cwd: repoDir, env });
30
+ }
31
+ runGit(["add", "-A"], { ...options, cwd: repoDir, env });
32
+ return runGit(["write-tree"], { ...options, cwd: repoDir, env });
33
+ } finally {
34
+ fs.rmSync(temporaryDir, { recursive: true, force: true });
35
+ }
36
+ }
37
+
38
+ function countChangedLines(numstat) {
39
+ return numstat.split("\n").reduce((total, line) => {
40
+ if (!line) return total;
41
+ const [added, deleted] = line.split("\t");
42
+ const additions = Number.parseInt(added, 10);
43
+ const deletions = Number.parseInt(deleted, 10);
44
+ return total
45
+ + (Number.isNaN(additions) ? 0 : additions)
46
+ + (Number.isNaN(deletions) ? 0 : deletions);
47
+ }, 0);
48
+ }
49
+
50
+ export function createCodeUsageReporter(options = {}) {
51
+ const cwd = options.cwd || process.cwd();
52
+ let repoDir;
53
+ let startingTree;
54
+ try {
55
+ repoDir = runGit(["rev-parse", "--show-toplevel"], { ...options, cwd });
56
+ startingTree = snapshotWorkingTree(repoDir, options);
57
+ } catch {
58
+ return null;
59
+ }
60
+
61
+ const eventId = (options.randomUUIDFn || crypto.randomUUID)();
62
+ return {
63
+ eventId,
64
+ async report() {
65
+ const endingTree = snapshotWorkingTree(repoDir, options);
66
+ const numstat = runGit(["diff", "--no-ext-diff", "--numstat", startingTree, endingTree], {
67
+ ...options,
68
+ cwd: repoDir,
69
+ env: options.env || process.env
70
+ });
71
+ const linesChanged = countChangedLines(numstat);
72
+ const response = await (options.fetchFn || fetch)(`${options.controlPlaneUrl}/usage/code`, {
73
+ method: "POST",
74
+ headers: {
75
+ authorization: `Bearer ${options.sessionToken}`,
76
+ "content-type": "application/json"
77
+ },
78
+ body: JSON.stringify({ eventId, linesChanged })
79
+ });
80
+ if (!response.ok) {
81
+ throw new Error(`Control-plane request failed: HTTP ${response.status}`);
82
+ }
83
+ return { eventId, linesChanged };
84
+ }
85
+ };
86
+ }
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 = "scifcode-1.0";
4
- export const DEFAULT_MODEL_NAME = "scifcode-1.0";
2
+ export const DEFAULT_CONTROL_PLANE_URL = "http://localhost:8080";
3
+ export const DEFAULT_MODEL = "scifcoder-1.0";
4
+ export const DEFAULT_MODEL_NAME = "scifcoder-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,9 @@ 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, getControlPlaneUrl } from "./constants.js";
7
+ import { readSession } from "./control-plane-auth.js";
8
+ import { createCodeUsageReporter } from "./code-usage.js";
7
9
 
8
10
  function executableOnPath(name, env = process.env) {
9
11
  return String(env.PATH || "").split(path.delimiter).map(dir => path.join(dir, name)).find(fs.existsSync);
@@ -24,12 +26,21 @@ function findInstalledRuntime(options = {}) {
24
26
  }
25
27
  }
26
28
 
29
+ function findBundledRuntime(options = {}) {
30
+ const runtimePlatform = options.platform || process.platform;
31
+ const binaryName = runtimePlatform === "win32" ? "scifcode-runtime.exe" : "scifcode-runtime";
32
+ const candidate = path.join(path.dirname(options.execPath || process.execPath), binaryName);
33
+ return fs.existsSync(candidate) ? candidate : undefined;
34
+ }
35
+
27
36
  export function resolveRuntime(options = {}) {
28
37
  const env = options.env || process.env;
29
38
  if (env.SCIFCODE_RUNTIME_BIN) {
30
39
  if (!fs.existsSync(env.SCIFCODE_RUNTIME_BIN)) throw new Error(`SCIFCODE_RUNTIME_BIN does not exist: ${env.SCIFCODE_RUNTIME_BIN}`);
31
40
  return { command: env.SCIFCODE_RUNTIME_BIN, argsPrefix: [], source: "binary" };
32
41
  }
42
+ const bundledBin = findBundledRuntime(options);
43
+ if (bundledBin) return { command: bundledBin, argsPrefix: [], source: "binary" };
33
44
  const installedBin = findInstalledRuntime(options);
34
45
  if (installedBin) return { command: installedBin, argsPrefix: [], source: "package" };
35
46
  const sourceDir = env.SCIFCODE_SOURCE_DIR || defaultSourceDir();
@@ -81,35 +92,62 @@ export function buildRuntimeArgs(userArgs = [], env = process.env, runtime = res
81
92
  return [...runtime.argsPrefix, "--model", model, ...withoutModelOverrides];
82
93
  }
83
94
 
84
- export function buildRuntimeEnv(env = process.env) {
95
+ export function buildRuntimeEnv(env = process.env, options = {}) {
85
96
  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];
97
+ if (!runtimeEnv[SESSION_TOKEN_ENV_NAME]) {
98
+ const session = (options.readSessionFn || readSession)({ homeDir: options.homeDir });
99
+ options.onSession?.(session);
100
+ const expiresAt = session?.expiresAt ? Date.parse(session.expiresAt) : Number.POSITIVE_INFINITY;
101
+ if (session?.sessionToken && expiresAt > Date.now()) {
102
+ runtimeEnv[SESSION_TOKEN_ENV_NAME] = session.sessionToken;
103
+ }
104
+ }
87
105
  return runtimeEnv;
88
106
  }
89
107
 
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}.`);
108
+ export function validateSession(env = process.env) { return Boolean(env[SESSION_TOKEN_ENV_NAME]); }
109
+ export function printMissingSession(stderr = console.error) {
110
+ stderr("Scifcode requires a control-plane session.");
93
111
  stderr("");
94
- stderr(`Run:\n export ${API_KEY_ENV_NAME}="endpoint_or_modal_bearer_token"\n scifcode`);
112
+ stderr("Run:\n scifcode login");
95
113
  }
96
114
 
97
115
  export function runRuntime(userArgs = [], options = {}) {
98
116
  const env = options.env || process.env;
99
117
  const stderr = options.stderr || console.error;
100
118
  const exit = options.exit || process.exit;
101
- if (!validateLagunaApiKey(env)) { printMissingApiKey(stderr); exit(1); return null; }
119
+ let storedSession;
120
+ const runtimeEnv = buildRuntimeEnv(env, {
121
+ ...options,
122
+ onSession: session => { storedSession = session; }
123
+ });
124
+ if (!validateSession(runtimeEnv)) { printMissingSession(stderr); exit(1); return null; }
102
125
  let runtime;
103
126
  try { runtime = (options.resolveRuntimeFn || resolveRuntime)({ env }); }
104
127
  catch (error) { stderr("Failed to resolve the Scifcode agent runtime."); stderr(error.message); exit(1); return null; }
105
- const runtimeEnv = buildRuntimeEnv(env);
106
128
  const projectDir = options.cwd || process.cwd();
129
+ let codeUsageReporter;
130
+ try {
131
+ codeUsageReporter = (options.createCodeUsageReporterFn || createCodeUsageReporter)({
132
+ cwd: projectDir,
133
+ env: runtimeEnv,
134
+ sessionToken: runtimeEnv[SESSION_TOKEN_ENV_NAME],
135
+ controlPlaneUrl: storedSession?.controlPlaneUrl || getControlPlaneUrl(env),
136
+ fetchFn: options.fetchFn
137
+ });
138
+ } catch (error) {
139
+ stderr(`Could not start Git diff tracking: ${error.message}`);
140
+ }
107
141
  const child = (options.spawnFn || spawn)(runtime.command, buildRuntimeArgs(userArgs, runtimeEnv, runtime, projectDir), {
108
142
  stdio: "inherit",
109
143
  env: runtimeEnv,
110
144
  cwd: runtime.cwd || projectDir
111
145
  });
112
- child.on("exit", code => exit(code ?? 0));
146
+ child.on("exit", code => {
147
+ Promise.resolve(codeUsageReporter?.report())
148
+ .catch(error => stderr(`Could not report code usage: ${error.message}`))
149
+ .finally(() => exit(code ?? 0));
150
+ });
113
151
  child.on("error", error => { stderr("Failed to launch Scifcode."); stderr(error.message); exit(1); });
114
152
  return child;
115
153
  }