@z0ne/installer 0.1.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/LICENSE +21 -0
- package/README.md +57 -0
- package/bin/cli.js +9 -0
- package/lib/api.js +59 -0
- package/lib/fsutil.js +88 -0
- package/lib/main.js +281 -0
- package/lib/toml.js +114 -0
- package/lib/tools/claude.js +112 -0
- package/lib/tools/cline.js +60 -0
- package/lib/tools/codex.js +136 -0
- package/lib/tools/hermes.js +71 -0
- package/lib/tools/openclaw.js +117 -0
- package/lib/tools/opencode.js +76 -0
- package/lib/ui.js +208 -0
- package/package.json +36 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Claude Code — ~/.claude/settings.json
|
|
3
|
+
// Talks to the gateway over the Anthropic wire format (/v1/messages).
|
|
4
|
+
// ANTHROPIC_BASE_URL keeps the /v1 suffix: the gateway also accepts the
|
|
5
|
+
// SDK's doubled /v1/v1/* form, so both styles work.
|
|
6
|
+
|
|
7
|
+
const path = require("path");
|
|
8
|
+
const fs = require("fs");
|
|
9
|
+
const { home } = require("../fsutil");
|
|
10
|
+
const fsu = require("../fsutil");
|
|
11
|
+
|
|
12
|
+
const settingsPath = () => path.join(home(), ".claude", "settings.json");
|
|
13
|
+
const statuslinePath = () => path.join(home(), ".claude", "z0ne-statusline.js");
|
|
14
|
+
|
|
15
|
+
const ENV_KEYS = [
|
|
16
|
+
"ANTHROPIC_BASE_URL",
|
|
17
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
18
|
+
"ANTHROPIC_DEFAULT_OPUS_MODEL",
|
|
19
|
+
"ANTHROPIC_DEFAULT_SONNET_MODEL",
|
|
20
|
+
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
|
21
|
+
"API_TIMEOUT_MS",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
// Rendered from the session JSON Claude Code pipes to statusline commands.
|
|
25
|
+
// Local data only — no network calls, no key access.
|
|
26
|
+
const STATUSLINE_SRC = `#!/usr/bin/env node
|
|
27
|
+
// z0ne.ai statusline for Claude Code (installed by @z0ne/installer).
|
|
28
|
+
let raw = "";
|
|
29
|
+
process.stdin.on("data", (d) => (raw += d));
|
|
30
|
+
process.stdin.on("end", () => {
|
|
31
|
+
let s = {};
|
|
32
|
+
try { s = JSON.parse(raw); } catch {}
|
|
33
|
+
const model = (s.model && (s.model.display_name || s.model.id)) || "?";
|
|
34
|
+
const dirSrc = (s.workspace && (s.workspace.current_dir || s.workspace.cwd)) || process.cwd();
|
|
35
|
+
const dir = String(dirSrc).split(/[\\\\/]/).filter(Boolean).pop() || "";
|
|
36
|
+
const cost = s.cost && typeof s.cost.total_cost_usd === "number"
|
|
37
|
+
? "$" + s.cost.total_cost_usd.toFixed(2)
|
|
38
|
+
: null;
|
|
39
|
+
const parts = ["z0ne", model];
|
|
40
|
+
if (cost) parts.push(cost);
|
|
41
|
+
if (dir) parts.push(dir);
|
|
42
|
+
process.stdout.write(parts.join(" | "));
|
|
43
|
+
});
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
module.exports = {
|
|
47
|
+
id: "claude",
|
|
48
|
+
title: "Claude Code",
|
|
49
|
+
hint: "~/.claude/settings.json",
|
|
50
|
+
|
|
51
|
+
detect() {
|
|
52
|
+
return fsu.commandExists("claude") || fs.existsSync(settingsPath());
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
apply(ctx) {
|
|
56
|
+
const file = settingsPath();
|
|
57
|
+
const current = fsu.readJson(file) || {};
|
|
58
|
+
const merged = {
|
|
59
|
+
...current,
|
|
60
|
+
hasCompletedOnboarding: true,
|
|
61
|
+
env: {
|
|
62
|
+
...(current.env || {}),
|
|
63
|
+
ANTHROPIC_BASE_URL: ctx.base + "/v1",
|
|
64
|
+
ANTHROPIC_AUTH_TOKEN: ctx.key,
|
|
65
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL: ctx.opus,
|
|
66
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: ctx.sonnet,
|
|
67
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: ctx.haiku,
|
|
68
|
+
API_TIMEOUT_MS: "600000",
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const notes = [];
|
|
73
|
+
if (ctx.statusline) {
|
|
74
|
+
const slFile = statuslinePath();
|
|
75
|
+
fsu.writeText(slFile, STATUSLINE_SRC);
|
|
76
|
+
try {
|
|
77
|
+
fs.chmodSync(slFile, 0o755);
|
|
78
|
+
} catch {}
|
|
79
|
+
const ours = 'node "' + slFile.replace(/\\/g, "/") + '"';
|
|
80
|
+
const existing = current.statusLine && current.statusLine.command;
|
|
81
|
+
if (existing && !existing.includes("z0ne-statusline")) {
|
|
82
|
+
notes.push("kept your existing statusLine (" + existing + ")");
|
|
83
|
+
} else {
|
|
84
|
+
merged.statusLine = { type: "command", command: ours };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
fsu.writeJson(file, merged);
|
|
89
|
+
return { paths: [file].concat(ctx.statusline ? [statuslinePath()] : []), notes };
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
remove() {
|
|
93
|
+
const file = settingsPath();
|
|
94
|
+
const current = fsu.readJson(file);
|
|
95
|
+
if (!current) return { paths: [], notes: ["no settings file found"] };
|
|
96
|
+
|
|
97
|
+
if (current.env) {
|
|
98
|
+
for (const k of ENV_KEYS) delete current.env[k];
|
|
99
|
+
if (!Object.keys(current.env).length) delete current.env;
|
|
100
|
+
}
|
|
101
|
+
const notes = [];
|
|
102
|
+
if (current.statusLine && String(current.statusLine.command || "").includes("z0ne-statusline")) {
|
|
103
|
+
delete current.statusLine;
|
|
104
|
+
try {
|
|
105
|
+
fs.unlinkSync(statuslinePath());
|
|
106
|
+
notes.push("removed " + statuslinePath());
|
|
107
|
+
} catch {}
|
|
108
|
+
}
|
|
109
|
+
fsu.writeJson(file, current);
|
|
110
|
+
return { paths: [file], notes };
|
|
111
|
+
},
|
|
112
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Cline CLI — ~/.cline/data/globalState.json + secrets.json
|
|
3
|
+
// Uses the OpenAI-compatible provider; Cline expects the base URL WITHOUT /v1.
|
|
4
|
+
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const { home } = require("../fsutil");
|
|
8
|
+
const fsu = require("../fsutil");
|
|
9
|
+
|
|
10
|
+
const dataDir = () => path.join(home(), ".cline", "data");
|
|
11
|
+
const statePath = () => path.join(dataDir(), "globalState.json");
|
|
12
|
+
const secretsPath = () => path.join(dataDir(), "secrets.json");
|
|
13
|
+
|
|
14
|
+
module.exports = {
|
|
15
|
+
id: "cline",
|
|
16
|
+
title: "Cline",
|
|
17
|
+
hint: "~/.cline/data/globalState.json",
|
|
18
|
+
|
|
19
|
+
detect() {
|
|
20
|
+
return fsu.commandExists("cline") || fs.existsSync(statePath());
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
apply(ctx) {
|
|
24
|
+
fsu.ensureDir(dataDir());
|
|
25
|
+
|
|
26
|
+
const state = fsu.readJson(statePath()) || {};
|
|
27
|
+
state.actModeApiProvider = "openai";
|
|
28
|
+
state.planModeApiProvider = "openai";
|
|
29
|
+
state.openAiBaseUrl = ctx.base;
|
|
30
|
+
state.openAiModelId = ctx.model;
|
|
31
|
+
state.planModeOpenAiModelId = ctx.model;
|
|
32
|
+
fsu.writeJson(statePath(), state);
|
|
33
|
+
|
|
34
|
+
const secrets = fsu.readJson(secretsPath()) || {};
|
|
35
|
+
secrets.openAiApiKey = ctx.key;
|
|
36
|
+
fsu.writeJson(secretsPath(), secrets);
|
|
37
|
+
|
|
38
|
+
return { paths: [statePath(), secretsPath()], notes: [] };
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
remove() {
|
|
42
|
+
const state = fsu.readJson(statePath());
|
|
43
|
+
if (!state) return { paths: [], notes: ["no state file found"] };
|
|
44
|
+
|
|
45
|
+
if (state.actModeApiProvider === "openai") {
|
|
46
|
+
delete state.openAiBaseUrl;
|
|
47
|
+
delete state.openAiModelId;
|
|
48
|
+
delete state.planModeOpenAiModelId;
|
|
49
|
+
state.actModeApiProvider = "cline";
|
|
50
|
+
state.planModeApiProvider = "cline";
|
|
51
|
+
}
|
|
52
|
+
fsu.writeJson(statePath(), state);
|
|
53
|
+
|
|
54
|
+
const secrets = fsu.readJson(secretsPath()) || {};
|
|
55
|
+
delete secrets.openAiApiKey;
|
|
56
|
+
fsu.writeJson(secretsPath(), secrets);
|
|
57
|
+
|
|
58
|
+
return { paths: [statePath(), secretsPath()], notes: [] };
|
|
59
|
+
},
|
|
60
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Codex CLI — ~/.codex/config.toml + auth.json + auth-command token helper.
|
|
3
|
+
//
|
|
4
|
+
// The token helper matters: Codex only refreshes the live model catalog
|
|
5
|
+
// (GET {base_url}/models) when [model_providers.*.auth] command is set AND
|
|
6
|
+
// actually starts. On Windows codex.exe spawns the command via CreateProcess,
|
|
7
|
+
// so a .sh file fails with os error 193 and the catalog fetch is silently
|
|
8
|
+
// skipped — the model picker would only show Codex's bundled models. Hence
|
|
9
|
+
// the .cmd shim with absolute paths baked in (HOME/CODEX_HOME are not set
|
|
10
|
+
// when Codex is launched from cmd or PowerShell).
|
|
11
|
+
//
|
|
12
|
+
// Provider key is "z0neai" to merge cleanly with configs written by the
|
|
13
|
+
// earlier z0ne-codex.sh bootstrap.
|
|
14
|
+
|
|
15
|
+
const path = require("path");
|
|
16
|
+
const fs = require("fs");
|
|
17
|
+
const { home, isWindows } = require("../fsutil");
|
|
18
|
+
const fsu = require("../fsutil");
|
|
19
|
+
const { tstr, setTomlKey, getTomlKey, removeTomlSection, removeTomlKey } = require("../toml");
|
|
20
|
+
|
|
21
|
+
const PROVIDER = "z0neai";
|
|
22
|
+
const codexDir = () => process.env.CODEX_HOME || path.join(home(), ".codex");
|
|
23
|
+
const configPath = () => path.join(codexDir(), "config.toml");
|
|
24
|
+
const authPath = () => path.join(codexDir(), "auth.json");
|
|
25
|
+
const helperPath = () => path.join(codexDir(), isWindows ? "z0ne-token.cmd" : "z0ne-token.sh");
|
|
26
|
+
|
|
27
|
+
const fwd = (p) => p.replace(/\\/g, "/");
|
|
28
|
+
|
|
29
|
+
function writeTokenHelper() {
|
|
30
|
+
const helper = helperPath();
|
|
31
|
+
const node = process.execPath;
|
|
32
|
+
const auth = authPath();
|
|
33
|
+
let src;
|
|
34
|
+
if (isWindows) {
|
|
35
|
+
src =
|
|
36
|
+
"@echo off\r\n" +
|
|
37
|
+
'"' + node + '" -e "console.log(require(\'' + fwd(auth) + "').OPENAI_API_KEY)\"\r\n";
|
|
38
|
+
} else {
|
|
39
|
+
src =
|
|
40
|
+
"#!/bin/sh\n" +
|
|
41
|
+
"# Prints the z0ne.ai API key for Codex provider auth.command.\n" +
|
|
42
|
+
'exec "' + node + '" -e "console.log(require(\'' + auth + "').OPENAI_API_KEY)\"\n";
|
|
43
|
+
}
|
|
44
|
+
fsu.writeText(helper, src);
|
|
45
|
+
if (!isWindows) fs.chmodSync(helper, 0o700);
|
|
46
|
+
return helper;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = {
|
|
50
|
+
id: "codex",
|
|
51
|
+
title: "Codex CLI",
|
|
52
|
+
hint: "~/.codex/config.toml",
|
|
53
|
+
|
|
54
|
+
detect() {
|
|
55
|
+
return fsu.commandExists("codex") || fs.existsSync(configPath());
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
apply(ctx) {
|
|
59
|
+
fsu.ensureDir(codexDir());
|
|
60
|
+
|
|
61
|
+
// auth.json — merge; keep any ChatGPT-login tokens untouched.
|
|
62
|
+
const auth = fsu.readJson(authPath()) || {};
|
|
63
|
+
auth.auth_mode = "apikey";
|
|
64
|
+
auth.OPENAI_API_KEY = ctx.key;
|
|
65
|
+
fsu.writeJson(authPath(), auth);
|
|
66
|
+
try {
|
|
67
|
+
fs.chmodSync(authPath(), 0o600);
|
|
68
|
+
} catch {}
|
|
69
|
+
|
|
70
|
+
const helper = writeTokenHelper();
|
|
71
|
+
|
|
72
|
+
// config.toml — line-based merge that preserves the user's comments,
|
|
73
|
+
// other providers, and unrelated settings.
|
|
74
|
+
let toml = fsu.readText(configPath()) || "";
|
|
75
|
+
toml = setTomlKey(toml, "model", tstr(ctx.model));
|
|
76
|
+
toml = setTomlKey(toml, "model_provider", tstr(PROVIDER));
|
|
77
|
+
toml = setTomlKey(toml, "model_reasoning_effort", tstr("medium"));
|
|
78
|
+
|
|
79
|
+
const prov = "model_providers." + PROVIDER;
|
|
80
|
+
toml = setTomlKey(toml, "name", tstr("z0ne.ai"), prov);
|
|
81
|
+
toml = setTomlKey(toml, "base_url", tstr(ctx.base + "/v1"), prov);
|
|
82
|
+
toml = setTomlKey(toml, "wire_api", tstr("responses"), prov);
|
|
83
|
+
|
|
84
|
+
toml = setTomlKey(toml, "command", tstr(fwd(helper)), prov + ".auth");
|
|
85
|
+
toml = setTomlKey(toml, "timeout_ms", "5000", prov + ".auth");
|
|
86
|
+
toml = setTomlKey(toml, "refresh_interval_ms", "0", prov + ".auth");
|
|
87
|
+
|
|
88
|
+
toml = setTomlKey(toml, "description", tstr("Default subagent"), "agents.subagent");
|
|
89
|
+
toml = setTomlKey(toml, "model", tstr(ctx.model), "agents.subagent");
|
|
90
|
+
|
|
91
|
+
fsu.writeText(configPath(), toml);
|
|
92
|
+
try {
|
|
93
|
+
fs.chmodSync(configPath(), 0o600);
|
|
94
|
+
} catch {}
|
|
95
|
+
|
|
96
|
+
return { paths: [configPath(), authPath(), helper], notes: [] };
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
remove() {
|
|
100
|
+
const notes = [];
|
|
101
|
+
let toml = fsu.readText(configPath());
|
|
102
|
+
if (toml !== null) {
|
|
103
|
+
const provider = getTomlKey(toml, "model_provider");
|
|
104
|
+
if (provider === PROVIDER || provider === "z0ne") {
|
|
105
|
+
toml = removeTomlKey(toml, "model");
|
|
106
|
+
toml = removeTomlKey(toml, "model_provider");
|
|
107
|
+
toml = removeTomlKey(toml, "model_reasoning_effort");
|
|
108
|
+
}
|
|
109
|
+
// Clean both the installer's section and the legacy console one.
|
|
110
|
+
toml = removeTomlSection(toml, "model_providers." + PROVIDER);
|
|
111
|
+
toml = removeTomlSection(toml, "model_providers.z0ne");
|
|
112
|
+
toml = removeTomlSection(toml, "agents.subagent");
|
|
113
|
+
fsu.writeText(configPath(), toml);
|
|
114
|
+
} else {
|
|
115
|
+
notes.push("no config.toml found");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const auth = fsu.readJson(authPath());
|
|
119
|
+
if (auth) {
|
|
120
|
+
delete auth.OPENAI_API_KEY;
|
|
121
|
+
delete auth.auth_mode;
|
|
122
|
+
if (Object.keys(auth).length) fsu.writeJson(authPath(), auth);
|
|
123
|
+
else {
|
|
124
|
+
fsu.backupFile(authPath());
|
|
125
|
+
fs.unlinkSync(authPath());
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (const f of [path.join(codexDir(), "z0ne-token.cmd"), path.join(codexDir(), "z0ne-token.sh")]) {
|
|
130
|
+
try {
|
|
131
|
+
fs.unlinkSync(f);
|
|
132
|
+
} catch {}
|
|
133
|
+
}
|
|
134
|
+
return { paths: toml !== null ? [configPath()] : [], notes };
|
|
135
|
+
},
|
|
136
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Hermes Agent — ~/.hermes/config.yaml (model block) + ~/.hermes/.env (key).
|
|
3
|
+
// The model block is replaced/inserted with a regex so the rest of the YAML
|
|
4
|
+
// (tools, memory, etc.) is preserved verbatim.
|
|
5
|
+
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
const { home } = require("../fsutil");
|
|
9
|
+
const fsu = require("../fsutil");
|
|
10
|
+
|
|
11
|
+
const hermesDir = () => path.join(home(), ".hermes");
|
|
12
|
+
const configPath = () => path.join(hermesDir(), "config.yaml");
|
|
13
|
+
const envPath = () => path.join(hermesDir(), ".env");
|
|
14
|
+
|
|
15
|
+
// Top-level "model:" block, up to the next non-indented line.
|
|
16
|
+
const MODEL_BLOCK_RE = /^model:[ \t]*\r?\n((?:[ \t]+.*\r?\n?|[ \t]*\r?\n)*)/m;
|
|
17
|
+
|
|
18
|
+
const buildModelBlock = (model, baseUrl) =>
|
|
19
|
+
'model:\n default: "' + model + '"\n provider: "custom"\n base_url: "' + baseUrl + '"\n';
|
|
20
|
+
|
|
21
|
+
const upsertEnvVar = (envText, key, value) => {
|
|
22
|
+
const re = new RegExp("^" + key + "=.*$", "m");
|
|
23
|
+
const line = key + "=" + value;
|
|
24
|
+
if (re.test(envText)) return envText.replace(re, line);
|
|
25
|
+
if (envText.length > 0 && !envText.endsWith("\n")) return envText + "\n" + line + "\n";
|
|
26
|
+
return envText + line + "\n";
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
module.exports = {
|
|
30
|
+
id: "hermes",
|
|
31
|
+
title: "Hermes Agent",
|
|
32
|
+
hint: "~/.hermes/config.yaml",
|
|
33
|
+
|
|
34
|
+
detect() {
|
|
35
|
+
return fsu.commandExists("hermes") || fs.existsSync(configPath());
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
apply(ctx) {
|
|
39
|
+
fsu.ensureDir(hermesDir());
|
|
40
|
+
|
|
41
|
+
const block = buildModelBlock(ctx.model, ctx.base + "/v1");
|
|
42
|
+
const yaml = fsu.readText(configPath()) || "";
|
|
43
|
+
const newYaml = MODEL_BLOCK_RE.test(yaml)
|
|
44
|
+
? yaml.replace(MODEL_BLOCK_RE, block)
|
|
45
|
+
: yaml.length > 0
|
|
46
|
+
? block + "\n" + yaml
|
|
47
|
+
: block;
|
|
48
|
+
fsu.writeText(configPath(), newYaml);
|
|
49
|
+
|
|
50
|
+
const env = fsu.readText(envPath()) || "";
|
|
51
|
+
fsu.writeText(envPath(), upsertEnvVar(env, "OPENAI_API_KEY", ctx.key));
|
|
52
|
+
try {
|
|
53
|
+
fs.chmodSync(envPath(), 0o600);
|
|
54
|
+
} catch {}
|
|
55
|
+
|
|
56
|
+
return { paths: [configPath(), envPath()], notes: [] };
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
remove() {
|
|
60
|
+
const yaml = fsu.readText(configPath());
|
|
61
|
+
if (yaml === null) return { paths: [], notes: ["no config file found"] };
|
|
62
|
+
|
|
63
|
+
fsu.writeText(configPath(), yaml.replace(MODEL_BLOCK_RE, "").replace(/^\n+/, ""));
|
|
64
|
+
|
|
65
|
+
const env = fsu.readText(envPath());
|
|
66
|
+
if (env !== null) {
|
|
67
|
+
fsu.writeText(envPath(), env.replace(/^OPENAI_API_KEY=.*\r?\n?/m, ""));
|
|
68
|
+
}
|
|
69
|
+
return { paths: [configPath()], notes: [] };
|
|
70
|
+
},
|
|
71
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// OpenClaw — ~/.openclaw/openclaw.json (+ per-agent models.json files).
|
|
3
|
+
// Registers z0ne.ai under models.providers, allowlists the model, and sets
|
|
4
|
+
// agents.defaults.model.primary to z0ne/<id>. OpenClaw 2026.5.x stores
|
|
5
|
+
// agents[].model either as a plain string or as { primary, fallbacks } —
|
|
6
|
+
// normalize before any .startsWith().
|
|
7
|
+
|
|
8
|
+
const path = require("path");
|
|
9
|
+
const fs = require("fs");
|
|
10
|
+
const { home } = require("../fsutil");
|
|
11
|
+
const fsu = require("../fsutil");
|
|
12
|
+
|
|
13
|
+
const clawDir = () => path.join(home(), ".openclaw");
|
|
14
|
+
const settingsPath = () => path.join(clawDir(), "openclaw.json");
|
|
15
|
+
|
|
16
|
+
const resolveAgentModel = (m) => {
|
|
17
|
+
if (typeof m === "string") return m;
|
|
18
|
+
if (m && typeof m === "object") return m.primary || "";
|
|
19
|
+
return "";
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function writeAgentModels(agentDir, model, baseUrl, apiKey) {
|
|
23
|
+
fsu.ensureDir(agentDir);
|
|
24
|
+
const modelsPath = path.join(agentDir, "models.json");
|
|
25
|
+
const existing = fsu.readJson(modelsPath) || {};
|
|
26
|
+
if (!existing.providers) existing.providers = {};
|
|
27
|
+
existing.providers.z0ne = {
|
|
28
|
+
baseUrl,
|
|
29
|
+
apiKey,
|
|
30
|
+
api: "openai-completions",
|
|
31
|
+
models: [{ id: model, name: model.split("/").pop() || model }],
|
|
32
|
+
};
|
|
33
|
+
fsu.writeJson(modelsPath, existing);
|
|
34
|
+
return modelsPath;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = {
|
|
38
|
+
id: "openclaw",
|
|
39
|
+
title: "OpenClaw",
|
|
40
|
+
hint: "~/.openclaw/openclaw.json",
|
|
41
|
+
|
|
42
|
+
detect() {
|
|
43
|
+
return fsu.commandExists("openclaw") || fs.existsSync(settingsPath());
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
apply(ctx) {
|
|
47
|
+
fsu.ensureDir(clawDir());
|
|
48
|
+
const settings = fsu.readJson(settingsPath()) || {};
|
|
49
|
+
|
|
50
|
+
if (!settings.agents) settings.agents = {};
|
|
51
|
+
if (!settings.agents.defaults) settings.agents.defaults = {};
|
|
52
|
+
if (!settings.agents.defaults.model) settings.agents.defaults.model = {};
|
|
53
|
+
if (!settings.agents.defaults.models) settings.agents.defaults.models = {};
|
|
54
|
+
if (!settings.models) settings.models = {};
|
|
55
|
+
if (!settings.models.providers) settings.models.providers = {};
|
|
56
|
+
|
|
57
|
+
const baseUrl = ctx.base + "/v1";
|
|
58
|
+
const fullModelId = "z0ne/" + ctx.model;
|
|
59
|
+
|
|
60
|
+
// Replace any stale z0ne/* allowlist entries with the fresh one.
|
|
61
|
+
for (const k of Object.keys(settings.agents.defaults.models)) {
|
|
62
|
+
if (k.startsWith("z0ne/")) delete settings.agents.defaults.models[k];
|
|
63
|
+
}
|
|
64
|
+
settings.agents.defaults.model.primary = fullModelId;
|
|
65
|
+
settings.agents.defaults.models[fullModelId] = {};
|
|
66
|
+
|
|
67
|
+
settings.models.providers.z0ne = {
|
|
68
|
+
baseUrl,
|
|
69
|
+
apiKey: ctx.key,
|
|
70
|
+
api: "openai-completions",
|
|
71
|
+
models: [{ id: ctx.model, name: ctx.model.split("/").pop() || ctx.model }],
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const paths = [settingsPath()];
|
|
75
|
+
if (Array.isArray(settings.agents.list)) {
|
|
76
|
+
// Drop stale per-agent z0ne models so the new default applies, then
|
|
77
|
+
// refresh each agent's models.json.
|
|
78
|
+
settings.agents.list = settings.agents.list.map((agent) => {
|
|
79
|
+
if (resolveAgentModel(agent.model).startsWith("z0ne/")) {
|
|
80
|
+
const { model: _drop, ...rest } = agent;
|
|
81
|
+
return rest;
|
|
82
|
+
}
|
|
83
|
+
return agent;
|
|
84
|
+
});
|
|
85
|
+
for (const agent of settings.agents.list) {
|
|
86
|
+
if (agent.agentDir) paths.push(writeAgentModels(agent.agentDir, ctx.model, baseUrl, ctx.key));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
fsu.writeJson(settingsPath(), settings);
|
|
91
|
+
return { paths, notes: [] };
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
remove() {
|
|
95
|
+
const settings = fsu.readJson(settingsPath());
|
|
96
|
+
if (!settings) return { paths: [], notes: ["no settings file found"] };
|
|
97
|
+
|
|
98
|
+
if (settings.models && settings.models.providers) {
|
|
99
|
+
delete settings.models.providers.z0ne;
|
|
100
|
+
if (!Object.keys(settings.models.providers).length) delete settings.models.providers;
|
|
101
|
+
}
|
|
102
|
+
if (settings.agents && settings.agents.defaults) {
|
|
103
|
+
const defaults = settings.agents.defaults;
|
|
104
|
+
if (defaults.models) {
|
|
105
|
+
for (const k of Object.keys(defaults.models)) {
|
|
106
|
+
if (k.startsWith("z0ne/")) delete defaults.models[k];
|
|
107
|
+
}
|
|
108
|
+
if (!Object.keys(defaults.models).length) delete defaults.models;
|
|
109
|
+
}
|
|
110
|
+
if (defaults.model && String(defaults.model.primary || "").startsWith("z0ne/")) {
|
|
111
|
+
delete defaults.model.primary;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
fsu.writeJson(settingsPath(), settings);
|
|
115
|
+
return { paths: [settingsPath()], notes: [] };
|
|
116
|
+
},
|
|
117
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// OpenCode — ~/.config/opencode/opencode.json
|
|
3
|
+
// Registers z0ne.ai as an @ai-sdk/openai-compatible provider and sets the
|
|
4
|
+
// active model to z0ne/<id>. Image input modality follows the live catalog.
|
|
5
|
+
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
const { home } = require("../fsutil");
|
|
9
|
+
const fsu = require("../fsutil");
|
|
10
|
+
|
|
11
|
+
const configDir = () => path.join(home(), ".config", "opencode");
|
|
12
|
+
const configPath = () => path.join(configDir(), "opencode.json");
|
|
13
|
+
|
|
14
|
+
module.exports = {
|
|
15
|
+
id: "opencode",
|
|
16
|
+
title: "OpenCode",
|
|
17
|
+
hint: "~/.config/opencode/opencode.json",
|
|
18
|
+
|
|
19
|
+
detect() {
|
|
20
|
+
return fsu.commandExists("opencode") || fs.existsSync(configPath());
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
apply(ctx) {
|
|
24
|
+
const config = fsu.readJson(configPath()) || {};
|
|
25
|
+
if (!config.provider) config.provider = {};
|
|
26
|
+
|
|
27
|
+
const existing = config.provider.z0ne || {
|
|
28
|
+
npm: "@ai-sdk/openai-compatible",
|
|
29
|
+
options: {},
|
|
30
|
+
models: {},
|
|
31
|
+
};
|
|
32
|
+
existing.options = {
|
|
33
|
+
...existing.options,
|
|
34
|
+
baseURL: ctx.base + "/v1",
|
|
35
|
+
apiKey: ctx.key,
|
|
36
|
+
};
|
|
37
|
+
existing.models = existing.models || {};
|
|
38
|
+
|
|
39
|
+
const row = ctx.rowsById[ctx.model];
|
|
40
|
+
const inputs =
|
|
41
|
+
row && Array.isArray(row.input_modalities) && row.input_modalities.includes("image")
|
|
42
|
+
? ["text", "image"]
|
|
43
|
+
: ["text"];
|
|
44
|
+
existing.models[ctx.model] = {
|
|
45
|
+
name: ctx.model,
|
|
46
|
+
modalities: { input: inputs, output: ["text"] },
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
config.provider.z0ne = existing;
|
|
50
|
+
config.model = "z0ne/" + ctx.model;
|
|
51
|
+
|
|
52
|
+
if (!config.agent) config.agent = {};
|
|
53
|
+
config.agent.explorer = {
|
|
54
|
+
description: "Fast explorer subagent for codebase exploration",
|
|
55
|
+
mode: "subagent",
|
|
56
|
+
model: "z0ne/" + ctx.model,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
fsu.writeJson(configPath(), config);
|
|
60
|
+
return { paths: [configPath()], notes: [] };
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
remove() {
|
|
64
|
+
const config = fsu.readJson(configPath());
|
|
65
|
+
if (!config) return { paths: [], notes: ["no config file found"] };
|
|
66
|
+
|
|
67
|
+
if (config.provider) delete config.provider.z0ne;
|
|
68
|
+
if (config.model && config.model.startsWith("z0ne/")) delete config.model;
|
|
69
|
+
if (config.agent && config.agent.explorer && String(config.agent.explorer.model || "").startsWith("z0ne/")) {
|
|
70
|
+
delete config.agent.explorer;
|
|
71
|
+
if (!Object.keys(config.agent).length) delete config.agent;
|
|
72
|
+
}
|
|
73
|
+
fsu.writeJson(configPath(), config);
|
|
74
|
+
return { paths: [configPath()], notes: [] };
|
|
75
|
+
},
|
|
76
|
+
};
|