@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/README.md +78 -28
- package/bin/scifcode.js +1 -1
- package/package.json +4 -3
- package/sbom/SHA256SUMS +1 -0
- package/sbom/SHA256SUMS.sigstore.json +1 -0
- package/sbom/scifcode-0.3.0-all-platforms.cdx.json +677 -0
- package/src/cli.js +75 -14
- package/src/code-usage.js +86 -0
- package/src/constants.js +28 -5
- package/src/control-plane-auth.js +143 -0
- package/src/ensure-scifcode-config.js +12 -11
- package/src/paths.js +1 -0
- package/src/run-runtime.js +48 -10
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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
3
|
-
export const DEFAULT_MODEL = "
|
|
4
|
-
export const DEFAULT_MODEL_NAME = "
|
|
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
|
|
8
|
-
|
|
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 {
|
|
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
|
|
23
|
-
|
|
24
|
-
?
|
|
25
|
-
:
|
|
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
|
|
34
|
-
apiKey:
|
|
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
|
|
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"); }
|
package/src/run-runtime.js
CHANGED
|
@@ -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 {
|
|
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[
|
|
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
|
|
91
|
-
export function
|
|
92
|
-
stderr(
|
|
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(
|
|
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
|
-
|
|
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 =>
|
|
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
|
}
|