@4yi-dev/cli 0.1.5 → 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/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 code = await runCode({ session, argv: process.argv.slice(3) });
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-dev/cli",
3
- "version": "0.1.5",
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": {
package/src/config.mjs CHANGED
@@ -48,3 +48,13 @@ export function writeConfig(config, home = os.homedir()) {
48
48
  ensureDir(paths.homeDir);
49
49
  fs.writeFileSync(paths.configFile, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
50
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 claudeModels = (modelConfig.models || []).filter(isClaudeModel);
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 defaultModel = claudeModels.some((model) => model.id === modelConfig.default_model)
39
- ? modelConfig.default_model
40
- : claudeModels[0]?.id;
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
- if (!modelConfig.default_model) throw new Error("No chat models available for this organization.");
106
- if (!(modelConfig.models || []).some(isClaudeModel)) throw new Error("No Claude models available for this organization.");
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...");