@numairbaseer/scifcode 0.1.6 → 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 +27 -28
- package/bin/scifcode.js +1 -1
- package/package.json +3 -3
- package/src/cli.js +73 -6
- package/src/constants.js +26 -3
- 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 +16 -9
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
|
-
##
|
|
24
|
+
## Log in
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
31
|
+
scifcode login
|
|
31
32
|
```
|
|
32
33
|
|
|
33
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
| `
|
|
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.
|
|
82
|
-
|
|
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
|
|
88
|
+
### `Scifcode requires a control-plane session`
|
|
87
89
|
|
|
88
|
-
|
|
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
|
-
|
|
94
|
+
scifcode login
|
|
92
95
|
```
|
|
93
96
|
|
|
94
|
-
|
|
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
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@numairbaseer/scifcode",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
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
|
-
|
|
16
|
-
|
|
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
|
|
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(
|
|
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
|
|
2
|
+
export const DEFAULT_CONTROL_PLANE_URL = "http://localhost:8080";
|
|
3
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
|
|
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,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 {
|
|
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[
|
|
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
|
|
91
|
-
export function
|
|
92
|
-
stderr(
|
|
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(
|
|
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
|
-
|
|
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",
|