@4yi/cli 0.1.4 → 0.1.6
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 +17 -2
- package/bin/4yi.mjs +23 -2
- package/package.json +8 -2
- package/src/config.mjs +22 -1
- package/src/opencode.mjs +27 -11
package/README.md
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 4YI CLI
|
|
2
2
|
|
|
3
3
|
4YI CLI signs in through the 4YI web app and launches OpenCode with 4YI model routing.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Temporary dev environment:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g @4yi/cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Production environment:
|
|
6
14
|
|
|
7
15
|
```bash
|
|
8
16
|
npm install -g @4yi/cli
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Both packages install the `4yi` command. The dev package points at `https://xclaw-dev.bieases.com`; the production package points at `https://app.4yi.ai`.
|
|
20
|
+
|
|
21
|
+
## Commands
|
|
22
|
+
|
|
23
|
+
```bash
|
|
9
24
|
4yi login
|
|
10
25
|
4yi whoami
|
|
11
26
|
4yi code
|
package/bin/4yi.mjs
CHANGED
|
@@ -5,10 +5,30 @@ import { runCode } from "../src/opencode.mjs";
|
|
|
5
5
|
const command = process.argv[2] || "help";
|
|
6
6
|
|
|
7
7
|
if (command === "help" || command === "--help" || command === "-h") {
|
|
8
|
-
console.log("Usage: 4yi <login|whoami|logout|code>");
|
|
8
|
+
console.log("Usage: 4yi <login|whoami|logout|code [--model <id>]>");
|
|
9
|
+
console.log(" 4yi code launch OpenCode; switch models live with Tab / /models");
|
|
10
|
+
console.log(" 4yi code --model X pin model X as the default for future sessions");
|
|
9
11
|
process.exit(0);
|
|
10
12
|
}
|
|
11
13
|
|
|
14
|
+
/** Split out `--model <id>` / `--model=<id>` / `-m <id>`; the rest pass through to OpenCode. */
|
|
15
|
+
function parseCodeArgs(args) {
|
|
16
|
+
let preferredModel = null;
|
|
17
|
+
const passthrough = [];
|
|
18
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
19
|
+
const arg = args[i];
|
|
20
|
+
if (arg === "--model" || arg === "-m") {
|
|
21
|
+
preferredModel = args[i + 1] ?? null;
|
|
22
|
+
i += 1;
|
|
23
|
+
} else if (arg.startsWith("--model=")) {
|
|
24
|
+
preferredModel = arg.slice("--model=".length);
|
|
25
|
+
} else {
|
|
26
|
+
passthrough.push(arg);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return { preferredModel, passthrough };
|
|
30
|
+
}
|
|
31
|
+
|
|
12
32
|
if (command === "--version" || command === "-v") {
|
|
13
33
|
const pkg = await import("../package.json", { with: { type: "json" } });
|
|
14
34
|
console.log(pkg.default.version);
|
|
@@ -27,7 +47,8 @@ try {
|
|
|
27
47
|
console.log("Signed out.");
|
|
28
48
|
} else if (command === "code") {
|
|
29
49
|
const session = loadSession();
|
|
30
|
-
const
|
|
50
|
+
const { preferredModel, passthrough } = parseCodeArgs(process.argv.slice(3));
|
|
51
|
+
const code = await runCode({ session, argv: passthrough, preferredModel });
|
|
31
52
|
process.exit(Number(code || 0));
|
|
32
53
|
} else {
|
|
33
54
|
console.error(`Unknown command: ${command}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@4yi/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "4YI command-line launcher for OAuth login and OpenCode runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,13 @@
|
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
15
|
"test": "node --test",
|
|
16
|
-
"pack:dry": "npm pack --dry-run"
|
|
16
|
+
"pack:dry": "npm pack --dry-run",
|
|
17
|
+
"stage:dev": "node scripts/stage-package.mjs dev",
|
|
18
|
+
"stage:prod": "node scripts/stage-package.mjs prod",
|
|
19
|
+
"pack:dev": "node scripts/stage-package.mjs dev --pack",
|
|
20
|
+
"pack:prod": "node scripts/stage-package.mjs prod --pack",
|
|
21
|
+
"publish:dev": "node scripts/stage-package.mjs dev --publish",
|
|
22
|
+
"publish:prod": "node scripts/stage-package.mjs prod --publish"
|
|
17
23
|
},
|
|
18
24
|
"engines": {
|
|
19
25
|
"node": ">=20.0.0"
|
package/src/config.mjs
CHANGED
|
@@ -2,7 +2,18 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
-
export const
|
|
5
|
+
export const PACKAGE_NAME = "@4yi/cli";
|
|
6
|
+
|
|
7
|
+
const PACKAGE_BASE_URLS = {
|
|
8
|
+
"@4yi-dev/cli": "https://xclaw-dev.bieases.com",
|
|
9
|
+
"@4yi/cli": "https://app.4yi.ai",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function defaultBaseUrlForPackage(packageName = PACKAGE_NAME) {
|
|
13
|
+
return PACKAGE_BASE_URLS[packageName] || PACKAGE_BASE_URLS["@4yi/cli"];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const DEFAULT_BASE_URL = defaultBaseUrlForPackage(PACKAGE_NAME);
|
|
6
17
|
|
|
7
18
|
export function normalizeBaseUrl(value) {
|
|
8
19
|
return String(value || DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
@@ -37,3 +48,13 @@ export function writeConfig(config, home = os.homedir()) {
|
|
|
37
48
|
ensureDir(paths.homeDir);
|
|
38
49
|
fs.writeFileSync(paths.configFile, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
39
50
|
}
|
|
51
|
+
|
|
52
|
+
/** The user's pinned default model id for `4yi code`, or null if unset. */
|
|
53
|
+
export function getPreferredModel(home = os.homedir()) {
|
|
54
|
+
return readConfig(home).preferred_model || null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Persist the pinned default model id, preserving the rest of the config (e.g. session). */
|
|
58
|
+
export function setPreferredModel(model, home = os.homedir()) {
|
|
59
|
+
writeConfig({ ...readConfig(home), preferred_model: model }, home);
|
|
60
|
+
}
|
package/src/opencode.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { spawn, spawnSync } from "node:child_process";
|
|
5
|
-
import { pathsForHome, ensureDir } from "./config.mjs";
|
|
5
|
+
import { pathsForHome, ensureDir, getPreferredModel, setPreferredModel } from "./config.mjs";
|
|
6
6
|
import { requestJson } from "./http.mjs";
|
|
7
7
|
|
|
8
8
|
const DEFAULT_OPENCODE_PACKAGE = "opencode-ai";
|
|
@@ -23,10 +23,12 @@ function modelOutputLimit(model) {
|
|
|
23
23
|
return model.output_limit || model.max_output_tokens || model.max_tokens || DEFAULT_OUTPUT_LIMIT;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
export function buildOpenCodeConfig({ modelConfig, tokenEnv = "FOURYI_CLI_TOKEN", orgEnv = "FOURYI_ORG_ID" }) {
|
|
26
|
+
export function buildOpenCodeConfig({ modelConfig, preferredModel = null, tokenEnv = "FOURYI_CLI_TOKEN", orgEnv = "FOURYI_ORG_ID" }) {
|
|
27
|
+
// Register EVERY model the server returned so OpenCode's native switcher
|
|
28
|
+
// (`Tab` / `/models`) can move between them. Claude stays the default.
|
|
29
|
+
const list = modelConfig.models || [];
|
|
27
30
|
const models = {};
|
|
28
|
-
const
|
|
29
|
-
for (const model of claudeModels) {
|
|
31
|
+
for (const model of list) {
|
|
30
32
|
models[model.id] = {
|
|
31
33
|
name: model.display_name || model.id,
|
|
32
34
|
limit: {
|
|
@@ -35,9 +37,14 @@ export function buildOpenCodeConfig({ modelConfig, tokenEnv = "FOURYI_CLI_TOKEN"
|
|
|
35
37
|
},
|
|
36
38
|
};
|
|
37
39
|
}
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
const has = (id) => !!id && list.some((model) => model.id === id);
|
|
41
|
+
const firstClaude = list.find(isClaudeModel)?.id;
|
|
42
|
+
// Precedence: an explicit/persisted preferred model → server default → first Claude → first model.
|
|
43
|
+
const defaultModel =
|
|
44
|
+
(has(preferredModel) && preferredModel) ||
|
|
45
|
+
(has(modelConfig.default_model) && modelConfig.default_model) ||
|
|
46
|
+
firstClaude ||
|
|
47
|
+
list[0]?.id;
|
|
41
48
|
|
|
42
49
|
return {
|
|
43
50
|
"$schema": "https://opencode.ai/config.json",
|
|
@@ -99,18 +106,27 @@ export function ensureOpenCodeRuntime({ home = os.homedir(), stdout = console.lo
|
|
|
99
106
|
return bin;
|
|
100
107
|
}
|
|
101
108
|
|
|
102
|
-
export async function runCode({ session, home = os.homedir(), argv = [], stdout = console.log } = {}) {
|
|
109
|
+
export async function runCode({ session, home = os.homedir(), argv = [], stdout = console.log, preferredModel = null } = {}) {
|
|
103
110
|
if (!session?.token) throw new Error("Not signed in. Run `4yi login`.");
|
|
104
111
|
const modelConfig = await requestJson(session.baseUrl, "/api/cli/models", { token: session.token });
|
|
105
|
-
|
|
106
|
-
if (
|
|
112
|
+
const list = modelConfig.models || [];
|
|
113
|
+
if (list.length === 0) throw new Error("No chat models available for this organization.");
|
|
114
|
+
|
|
115
|
+
// Resolve the model to launch with: an explicit `--model` (validated + persisted)
|
|
116
|
+
// takes precedence over a previously persisted preference; both must be entitled.
|
|
117
|
+
const valid = (id) => !!id && list.some((model) => model.id === id);
|
|
118
|
+
const explicit = valid(preferredModel) ? preferredModel : null;
|
|
119
|
+
const persisted = !explicit && valid(getPreferredModel(home)) ? getPreferredModel(home) : null;
|
|
120
|
+
const effectivePreferred = explicit || persisted || null;
|
|
121
|
+
if (explicit) setPreferredModel(explicit, home);
|
|
122
|
+
if (preferredModel && !explicit) stdout(`Model "${preferredModel}" is not available; using the default instead.`);
|
|
107
123
|
|
|
108
124
|
const paths = pathsForHome(home);
|
|
109
125
|
ensureDir(paths.opencodeConfigDir);
|
|
110
126
|
for (const dir of ["config", "data", "cache", "state"]) {
|
|
111
127
|
ensureDir(path.join(paths.opencodeConfigDir, "xdg", dir));
|
|
112
128
|
}
|
|
113
|
-
fs.writeFileSync(paths.opencodeConfigFile, `${JSON.stringify(buildOpenCodeConfig({ modelConfig }), null, 2)}\n`, { mode: 0o600 });
|
|
129
|
+
fs.writeFileSync(paths.opencodeConfigFile, `${JSON.stringify(buildOpenCodeConfig({ modelConfig, preferredModel: effectivePreferred }), null, 2)}\n`, { mode: 0o600 });
|
|
114
130
|
|
|
115
131
|
const bin = ensureOpenCodeRuntime({ home, stdout });
|
|
116
132
|
stdout("Launching OpenCode with 4YI models...");
|