wkappbot-code 0.3.25 → 0.3.27
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 +1 -1
- package/bin/wkappbot-code.js +8 -2
- package/lib/doctor-path.js +16 -2
- package/lib/hook-wire.js +22 -1
- package/lib/providers.js +126 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ The generated `opencode.json` holds the minimum: `$schema` and the plugin entry
|
|
|
63
63
|
|
|
64
64
|
- One headless turn: `opencode run -m provider/model "prompt"`
|
|
65
65
|
- Inside the TUI: `/models`
|
|
66
|
-
- Any OpenAI-compatible endpoint:
|
|
66
|
+
- Any OpenAI-compatible endpoint: `wkappbot-code provider add acme --base-url-env MY_BASE_URL --api-key-env MY_API_KEY --model my-model` writes an `@ai-sdk/openai-compatible` provider into your `opencode.json` (`"baseURL": "{env:MY_BASE_URL}"`, `"apiKey": "{env:MY_API_KEY}"`), then `opencode run -m acme/my-model`. The arguments are env var NAMES: a key or a URL typed in their place is refused, and no key is stored in a file. The file is backed up first, a config that is not plain JSON is left untouched, and an existing provider of the same id needs `--force`. `provider list` prints ids and env var names only, `provider remove ID` deletes just that provider, `uninstall` keeps your providers, and `doctor` WARNs (names only) when a referenced variable is unset.
|
|
67
67
|
|
|
68
68
|
## In the browser WebVM (no login needed)
|
|
69
69
|
|
package/bin/wkappbot-code.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
// wkappbot-code doctor [opts] one PASS/WARN/FAIL/SKIP row per part (exit 1 if a required part fails)
|
|
19
19
|
// wkappbot-code update [opts] newer version.json -> tgz sha256-checked -> npm i -g -> install --update (lib/update.js)
|
|
20
20
|
// wkappbot-code config [opts] detect provider/model/plugin/harness, edit at a TTY, merge into opencode.json (lib/config.js)
|
|
21
|
+
// wkappbot-code provider add|list|remove an OpenAI-compatible endpoint (env var NAMES only) in opencode.json (lib/providers.js)
|
|
21
22
|
// wkappbot-code uninstall [opts] undo install: the recorded pre-install global core.hooksPath comes back, the
|
|
22
23
|
// harness hook arms leave ~/.claude/settings.json (your own entries stay), the recorded shims
|
|
23
24
|
// leave the bin dirs, ~/.wk-harness and ~/.wk-harness-hooks go, the plugin entry leaves opencode.json
|
|
@@ -43,6 +44,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
43
44
|
import { quietCheck, runUpdate, updateRow } from "../lib/update.js"; import { rmTreeHard } from "../lib/rmtree.js"; import { plainFileUrl } from "../lib/fileurl.js"; import { removeStaleWkskill } from "../lib/stale-wkskill.js"; import { checkPath, installBothLinks, removeCreatedLinks } from "../lib/doctor-path.js";
|
|
44
45
|
import { applySets, askEach, buildConfig, configRow, detect, lineDiff, printDetected } from "../lib/config.js";
|
|
45
46
|
import { aiCliRows, installAiClis, removeAiClis } from "../lib/ai-clis.js";
|
|
47
|
+
import { providerCli, providerRow } from "../lib/providers.js";
|
|
46
48
|
import { hooksRow, wireClaudeHooks } from "../lib/hook-wire.js";
|
|
47
49
|
import { installStarterMd, removeStarterMd } from "../lib/starter-md.js";
|
|
48
50
|
import { addAllows, removeAllows } from "../lib/allow-compact.js";
|
|
@@ -400,7 +402,7 @@ function installPlugin(o, kih, kihSource) {
|
|
|
400
402
|
// The machine state an uninstall must give back, recorded ONCE (a re-install never overwrites the first record, or
|
|
401
403
|
// it would record our own dispatcher as the "prior" hooksPath). Shim paths are those install may create.
|
|
402
404
|
function shimCandidates() {
|
|
403
|
-
return [join(linkDir(), "wkappbot"), join(HARNESS_LINKS(), "wk-verify"), join(HARNESS_LINKS(), "wk-verify.cmd")]; // Windows (Git Bash) .cmd twins from harness-install.sh: recorded like the rest, so uninstall removes them, never a user file
|
|
405
|
+
return [join(linkDir(), "wkappbot"), join(linkDir(), "wkappbot-core"), join(HARNESS_LINKS(), "wk-verify"), join(HARNESS_LINKS(), "wk-verify.cmd")]; // wkappbot-core: the harness install.sh links it beside wkappbot (SDK e2e nopwsh u1: local-bin= vs local-bin=wkappbot-core) // Windows (Git Bash) .cmd twins from harness-install.sh: recorded like the rest, so uninstall removes them, never a user file
|
|
404
406
|
}
|
|
405
407
|
const PRE_SETTINGS = () => join(STATE_HOME(), "settings.pre-install.json");
|
|
406
408
|
function recordPrior(prev) {
|
|
@@ -646,6 +648,7 @@ function doctor(o) {
|
|
|
646
648
|
add("config", !!entry, join(cfg, "opencode.json"), true);
|
|
647
649
|
const oc = configRow(join(cfg, "opencode.json"));
|
|
648
650
|
add("opencode-config", oc.ok, oc.detail, false);
|
|
651
|
+
try { const pv = providerRow(JSON.parse(readFileSync(join(cfg, "opencode.json"), "utf8"))); if (!pv.none) add("provider", pv.ok, pv.detail, false); } catch {} // WARN names an unset env var (never its value)
|
|
649
652
|
aiCliRows(add, !!(st.aiClis && st.aiClis.skipped));
|
|
650
653
|
// bin: the core answers --version
|
|
651
654
|
const dest = o.binDest ? resolve(o.binDest) : (st.bin && st.bin.dest) || binDest(o);
|
|
@@ -708,6 +711,8 @@ function doctor(o) {
|
|
|
708
711
|
return bad ? 1 : 0;
|
|
709
712
|
}
|
|
710
713
|
|
|
714
|
+
// provider add|list|remove (lib/providers.js): env var NAMES into the user's opencode.json, no key stored
|
|
715
|
+
if (process.argv[2] === "provider") process.exit(providerCli(process.argv.slice(3), { target: join(configDir({}), "opencode.json"), stamp }));
|
|
711
716
|
const o = parseArgs(process.argv.slice(2));
|
|
712
717
|
// install --update (from update) and these flags are forwarded to the re-run install of a fresh version.
|
|
713
718
|
const fwd = process.argv.slice(3).filter((a) => a !== "--auto-update");
|
|
@@ -731,6 +736,7 @@ switch (o.cmd) {
|
|
|
731
736
|
default:
|
|
732
737
|
console.log("usage: wkappbot-code --version | install [--no-npm] [--no-ai-clis] [--no-allows] [--no-bin] [--no-harness] [--no-skills] [--web-harness] [--dev-seat]\n" +
|
|
733
738
|
" [--config-dir DIR] [--kih-dir DIR] [--bin-dest FILE] [--hq-dir DIR] [--update] [--auto-update] | doctor [--config-dir DIR]\n" +
|
|
734
|
-
" | uninstall [--config-dir DIR] | update [install opts] | config [--yes] [--project] [--set key=value] [--config-dir DIR]"
|
|
739
|
+
" | uninstall [--config-dir DIR] | update [install opts] | config [--yes] [--project] [--set key=value] [--config-dir DIR]\n" +
|
|
740
|
+
" | provider add ID --base-url-env VAR --api-key-env VAR --model NAME | provider list | provider remove ID");
|
|
735
741
|
process.exit(o.cmd === "help" || o.cmd === "--help" ? 0 : 64);
|
|
736
742
|
}
|
package/lib/doctor-path.js
CHANGED
|
@@ -224,13 +224,19 @@ export function ensureLinks({ home, platform = process.platform, binDir, env = p
|
|
|
224
224
|
// Windows runs the PATH logic even when both shims already existed: the doctor row reads the persistent PATH,
|
|
225
225
|
// so a missing entry is a FAIL whether or not this run wrote a shim. POSIX keeps its old rule (a link was written).
|
|
226
226
|
const pathWanted = needsPath && (created.length > 0 || platform === "win32");
|
|
227
|
+
// POSIX: note whether ~/.profile existed BEFORE addToPath, so uninstall deletes only a profile install created.
|
|
228
|
+
const profilePath = platform === "win32" ? "" : join(home, ".profile");
|
|
229
|
+
const profileExisted = !!profilePath && existsSync(profilePath);
|
|
227
230
|
const pathAdded = pathWanted ? !!addToPath({ dir, home, platform, readRegPath }) : false;
|
|
228
|
-
|
|
231
|
+
const profileCreated = pathAdded && !profileExisted && !!profilePath && existsSync(profilePath) ? profilePath : "";
|
|
232
|
+
return { created, dir, pathAdded, profileCreated };
|
|
229
233
|
}
|
|
230
234
|
|
|
231
235
|
// removeCreatedLinks(created, opts) -> the entries actually removed. Deletes only the listed entries, nothing else.
|
|
232
236
|
// A "PATH:<folder>" entry is the PATH entry install added: it is removed by removePath (exactly that folder),
|
|
233
|
-
// never by unlink.
|
|
237
|
+
// never by unlink. A "PROFILE:<path>" entry is the ~/.profile install created: it is deleted only when its text is
|
|
238
|
+
// blank again (trim() empty), so a profile the user wrote into is never removed. opts: platform, home, and an
|
|
239
|
+
// injectable removePath({dir, home, platform}) -> boolean.
|
|
234
240
|
export function removeCreatedLinks(created = [], { platform = process.platform, home = process.env.HOME || process.env.USERPROFILE || "", removePath = defaultRemovePath } = {}) {
|
|
235
241
|
const removed = [];
|
|
236
242
|
for (const p of created) {
|
|
@@ -238,6 +244,13 @@ export function removeCreatedLinks(created = [], { platform = process.platform,
|
|
|
238
244
|
try { if (removePath({ dir: p.slice(5), home, platform })) removed.push(p); } catch { /* not removed */ }
|
|
239
245
|
continue;
|
|
240
246
|
}
|
|
247
|
+
if (p.startsWith("PROFILE:")) {
|
|
248
|
+
try {
|
|
249
|
+
const f = p.slice(8);
|
|
250
|
+
if (existsSync(f) && readFileSync(f, "utf8").trim() === "") { unlinkSync(f); removed.push(p); }
|
|
251
|
+
} catch { /* not removed */ }
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
241
254
|
try { unlinkSync(p); removed.push(p); } catch { /* already gone */ }
|
|
242
255
|
}
|
|
243
256
|
return removed;
|
|
@@ -259,6 +272,7 @@ export function installBothLinks({ dest, home, platform = process.platform, binD
|
|
|
259
272
|
const r = ensureLinks({ home, platform, binDir: dir, env, targets, addToPath, readRegPath });
|
|
260
273
|
const created = BINS.map(pathOf).filter((p) => r.created.includes(p) || ours.some((bin) => pathOf(bin) === p));
|
|
261
274
|
if (r.pathAdded) created.push("PATH:" + dir); // recorded only when this install wrote the PATH entry
|
|
275
|
+
if (r.profileCreated) created.push("PROFILE:" + r.profileCreated); // right after PATH: the profile install made
|
|
262
276
|
return { created, pathAdded: r.pathAdded, kept, dir };
|
|
263
277
|
}
|
|
264
278
|
|
package/lib/hook-wire.js
CHANGED
|
@@ -61,11 +61,19 @@ function route(cmd, lite) {
|
|
|
61
61
|
return null;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
// A dev seat: a harness checkout beside this lite (its .claude/hooks/wkharness-run.sh exists) that is not a web user install
|
|
65
|
+
// (no .wk-user-install marker in the harness folder). Python --wire uses the same two tests (tools/wkharness-lite.sh).
|
|
66
|
+
export function isDevSeat(lite) {
|
|
67
|
+
const kih = dirname(dirname(lite));
|
|
68
|
+
return existsSync(join(kih, ".claude", "hooks", "wkharness-run.sh")) && !existsSync(join(kih, ".wk-user-install"));
|
|
69
|
+
}
|
|
70
|
+
|
|
64
71
|
// The node port of --wire, plus the create step of the web installer. Throws on a settings file that is not JSON.
|
|
65
72
|
export function wireSettings(path, lite) {
|
|
66
73
|
const r = readSettings(path);
|
|
67
74
|
if (!r.ok) throw new Error(path + " is not plain JSON (" + r.err + ")");
|
|
68
75
|
const d = r.json || {};
|
|
76
|
+
const dev = isDevSeat(lite) && harnessHooks(d).some((h) => /wkharness-run\.sh/.test(h.command));
|
|
69
77
|
const out = {};
|
|
70
78
|
for (const [ev, groups] of Object.entries(d.hooks && typeof d.hooks === "object" ? d.hooks : {})) {
|
|
71
79
|
const keep = [];
|
|
@@ -75,6 +83,8 @@ export function wireSettings(path, lite) {
|
|
|
75
83
|
const seen = new Set(), hs = [];
|
|
76
84
|
for (const h of asHooks(g)) {
|
|
77
85
|
const cmd = String((h && h.command) || "");
|
|
86
|
+
// a dev seat runs its full harness and never also the lite one (owner 2026-11-12): lite entries go, the rest stay verbatim
|
|
87
|
+
if (dev) { if (!/wkharness-lite\.sh/.test(cmd)) hs.push(h); continue; }
|
|
78
88
|
const c = HARNESS_HOOK.test(cmd) ? route(cmd, lite) : null; // only a harness hook is ever rewritten
|
|
79
89
|
if (c && !seen.has(c)) { seen.add(c); hs.push({ type: "command", command: c, timeout: 10 }); }
|
|
80
90
|
else if (!c && !HARNESS_HOOK.test(cmd)) hs.push(h); // a user's own hook stays verbatim
|
|
@@ -83,7 +93,8 @@ export function wireSettings(path, lite) {
|
|
|
83
93
|
}
|
|
84
94
|
if (keep.length) out[ev] = keep;
|
|
85
95
|
}
|
|
86
|
-
|
|
96
|
+
// a dev seat gets no lite arm added: its full harness is the only chain
|
|
97
|
+
if (!dev) for (const [ev, arm, matcher] of ARMS) {
|
|
87
98
|
const c = '"' + lite + '" ' + arm;
|
|
88
99
|
if (!(out[ev] || []).some((g) => (g.hooks || []).some((h) => h.command === c)))
|
|
89
100
|
(out[ev] = out[ev] || []).push({ ...(matcher ? { matcher } : {}), hooks: [{ type: "command", command: c, timeout: 10 }] });
|
|
@@ -124,6 +135,16 @@ function wireClaudeHooksInner(o) {
|
|
|
124
135
|
log("hooks: WARN " + o.settings + " is not plain JSON (" + before.err + "); left untouched -- please fix it, then run: wkappbot-code install");
|
|
125
136
|
return res;
|
|
126
137
|
}
|
|
138
|
+
// A dev seat with both its full hook and a lite entry (owner 2026-11-12: two Stop chains fired): drop the lite entries
|
|
139
|
+
// and keep the full hook, whatever state the settings reports as.
|
|
140
|
+
const liteOnDisk = join(o.kih, "tools", "wkharness-lite.sh");
|
|
141
|
+
if (before.hooks.some((h) => /wkharness-run\.sh/.test(h.command)) && before.hooks.some((h) => /wkharness-lite\.sh/.test(h.command)) && isDevSeat(liteOnDisk)) {
|
|
142
|
+
wireSettings(o.settings, liteOnDisk);
|
|
143
|
+
const dev = analyzeHooks(o.settings, o.kih);
|
|
144
|
+
res.after = dev.state; res.changed = true;
|
|
145
|
+
log("hooks: " + o.settings + " runs the full harness on this dev seat; its lite hook entries were removed");
|
|
146
|
+
return res;
|
|
147
|
+
}
|
|
127
148
|
if (before.state === "ours") { log("hooks: " + o.settings + " already runs every harness hook from " + o.kih); return res; }
|
|
128
149
|
// A FULL harness hook (wkharness-run.sh, the pwsh hub of a dev seat) is never rewritten into the lite one where pwsh
|
|
129
150
|
// runs: the rewrite would downgrade the owner's own seats. Leave it and say what to do.
|
package/lib/providers.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// providers.js -- `wkappbot-code provider add|list|remove`: an OpenAI-compatible endpoint written into the user's
|
|
2
|
+
// opencode.json as an @ai-sdk/openai-compatible provider (owner 2026-11-12). The file holds env var NAMES only
|
|
3
|
+
// ("{env:VAR}"); a key or a URL typed where a name belongs is refused and never echoed, and no env var value is read
|
|
4
|
+
// except by the doctor row, which only checks that a variable is set and prints its name.
|
|
5
|
+
// Pure build/merge/remove functions first, then the CLI body. Spec: wkappbot skill read agent-harness-landscape-2026
|
|
6
|
+
import { copyFileSync, existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
7
|
+
|
|
8
|
+
export const NPM = "@ai-sdk/openai-compatible";
|
|
9
|
+
export const ENV_NAME = /^[A-Z_][A-Z0-9_]*$/;
|
|
10
|
+
const ID_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
11
|
+
|
|
12
|
+
// "" when v is a usable env var NAME; else a reason that never contains v.
|
|
13
|
+
export function envNameProblem(v, label) {
|
|
14
|
+
const s = String(v == null ? "" : v);
|
|
15
|
+
if (!s) return label + " needs the NAME of an environment variable";
|
|
16
|
+
if (ENV_NAME.test(s)) return "";
|
|
17
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(s) || s.includes("/") || s.includes(".")) return label + " looks like a URL; pass the NAME of an env var that holds it (letters, digits, underscore, e.g. MY_BASE_URL)";
|
|
18
|
+
if (/^(sk|pk|gsk|ghp|xox|AIza|Bearer)[-_ ]?/i.test(s) || s.length > 40 || /[-\s]/.test(s)) return label + " looks like a secret; pass the NAME of an env var that holds it, never the value";
|
|
19
|
+
return label + " is not an env var name (it must match ^[A-Z_][A-Z0-9_]*$)";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// spec = { id, baseUrlEnv, apiKeyEnv, models: [names], label } -> { ok, error, id, block }
|
|
23
|
+
export function buildProviderBlock(spec) {
|
|
24
|
+
const id = String(spec.id || "");
|
|
25
|
+
if (!ID_RE.test(id)) return { ok: false, error: "provider id must be lowercase letters, digits, - or _ (got a " + id.length + "-character id)" };
|
|
26
|
+
const e = envNameProblem(spec.baseUrlEnv, "--base-url-env") || envNameProblem(spec.apiKeyEnv, "--api-key-env");
|
|
27
|
+
if (e) return { ok: false, error: e };
|
|
28
|
+
const models = (spec.models || []).map(String).filter(Boolean);
|
|
29
|
+
if (!models.length) return { ok: false, error: "at least one --model NAME is required" };
|
|
30
|
+
const block = { npm: NPM, name: spec.label || id, options: { baseURL: "{env:" + spec.baseUrlEnv + "}", apiKey: "{env:" + spec.apiKeyEnv + "}" }, models: {} };
|
|
31
|
+
for (const m of models) block.models[m] = { name: m, tool_call: true };
|
|
32
|
+
return { ok: true, id, block };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// A new config: every user key kept; an existing same-id provider stays unless force.
|
|
36
|
+
export function mergeProvider(cfg, id, block, force = false) {
|
|
37
|
+
const out = { ...cfg, provider: { ...(cfg.provider || {}) } };
|
|
38
|
+
if (out.provider[id] && !force) return { ok: false, error: "provider " + id + " already exists; pass --force to replace it" };
|
|
39
|
+
out.provider[id] = block;
|
|
40
|
+
return { ok: true, cfg: out };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// A new config without provider `id` (the provider key goes when it was the last one).
|
|
44
|
+
export function removeProvider(cfg, id) {
|
|
45
|
+
if (!cfg.provider || !(id in cfg.provider)) return { ok: false, error: "no provider " + id };
|
|
46
|
+
const out = { ...cfg, provider: { ...cfg.provider } };
|
|
47
|
+
delete out.provider[id];
|
|
48
|
+
if (!Object.keys(out.provider).length) delete out.provider;
|
|
49
|
+
return { ok: true, cfg: out };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The env var NAMES a provider block references ("{env:NAME}" in options); values are never read here.
|
|
53
|
+
export function envRefs(block) {
|
|
54
|
+
const names = [];
|
|
55
|
+
for (const v of Object.values((block && block.options) || {})) for (const m of String(v).matchAll(/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g)) if (!names.includes(m[1])) names.push(m[1]);
|
|
56
|
+
return names;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const listProviders = (cfg) => Object.entries(cfg.provider || {}).map(([id, b]) => ({ id, npm: (b && b.npm) || "", refs: envRefs(b), models: Object.keys((b && b.models) || {}) }));
|
|
60
|
+
|
|
61
|
+
// The doctor row: { ok, detail, none } -- none when the config declares no provider that references an env var.
|
|
62
|
+
export function providerRow(cfg, env = process.env) {
|
|
63
|
+
const rows = listProviders(cfg).filter((r) => r.refs.length);
|
|
64
|
+
if (!rows.length) return { ok: true, none: true, detail: "no provider with env var references" };
|
|
65
|
+
const unset = [];
|
|
66
|
+
for (const r of rows) for (const n of r.refs) if (!env[n] && !unset.includes(n)) unset.push(n);
|
|
67
|
+
const ids = rows.map((r) => r.id).join(",");
|
|
68
|
+
return unset.length ? { ok: false, detail: ids + ": env var not set: " + unset.join(", ") } : { ok: true, detail: ids + ": every referenced env var is set" };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function parse(argv) {
|
|
72
|
+
const o = { sub: argv[0] || "", id: "", models: [], force: false, baseUrlEnv: "", apiKeyEnv: "", label: "", bad: "" };
|
|
73
|
+
for (let i = 1; i < argv.length; i++) {
|
|
74
|
+
const a = argv[i];
|
|
75
|
+
if (a === "--base-url-env") o.baseUrlEnv = argv[++i] || "";
|
|
76
|
+
else if (a === "--api-key-env") o.apiKeyEnv = argv[++i] || "";
|
|
77
|
+
else if (a === "--model") o.models.push(argv[++i] || "");
|
|
78
|
+
else if (a === "--name") o.label = argv[++i] || "";
|
|
79
|
+
else if (a === "--force") o.force = true;
|
|
80
|
+
else if (a.startsWith("--")) o.bad = a;
|
|
81
|
+
else if (!o.id) o.id = a;
|
|
82
|
+
else o.bad = "extra argument";
|
|
83
|
+
}
|
|
84
|
+
return o;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const USAGE = "usage: wkappbot-code provider add ID --base-url-env VAR --api-key-env VAR --model NAME [--model NAME ...] [--name LABEL] [--force]\n" +
|
|
88
|
+
" | provider list | provider remove ID (VAR is an environment variable NAME; no key or URL is ever stored)";
|
|
89
|
+
|
|
90
|
+
// The CLI body. target = the opencode.json path. Returns the exit code.
|
|
91
|
+
export function providerCli(argv, { target, log = console.log, err = console.error, stamp = () => Date.now().toString() }) {
|
|
92
|
+
const o = parse(argv);
|
|
93
|
+
if (o.bad) { err("provider: unknown option " + o.bad); return 64; }
|
|
94
|
+
if (!["add", "list", "remove"].includes(o.sub)) { log(USAGE); return o.sub === "" || o.sub === "--help" ? 0 : 64; }
|
|
95
|
+
let raw = "", cfg = {};
|
|
96
|
+
if (existsSync(target)) {
|
|
97
|
+
raw = readFileSync(target, "utf8");
|
|
98
|
+
try { cfg = JSON.parse(raw); } catch { err("provider: " + target + " is not plain JSON; left untouched -- edit it by hand"); return 1; }
|
|
99
|
+
if (!cfg || typeof cfg !== "object" || Array.isArray(cfg)) { err("provider: " + target + " is not a JSON object; left untouched"); return 1; }
|
|
100
|
+
}
|
|
101
|
+
if (o.sub === "list") {
|
|
102
|
+
const rows = listProviders(cfg);
|
|
103
|
+
if (!rows.length) log("no providers in " + target);
|
|
104
|
+
for (const r of rows) log(r.id + " " + (r.npm || "-") + " env: " + (r.refs.join(", ") || "-") + " models: " + (r.models.join(", ") || "-"));
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
let next;
|
|
108
|
+
if (o.sub === "add") {
|
|
109
|
+
const b = buildProviderBlock({ id: o.id, baseUrlEnv: o.baseUrlEnv, apiKeyEnv: o.apiKeyEnv, models: o.models, label: o.label });
|
|
110
|
+
if (!b.ok) { err("provider: " + b.error); return 2; }
|
|
111
|
+
const m = mergeProvider(cfg, b.id, b.block, o.force);
|
|
112
|
+
if (!m.ok) { err("provider: " + m.error); return 3; }
|
|
113
|
+
next = m.cfg;
|
|
114
|
+
} else {
|
|
115
|
+
if (!o.id) { err("provider remove needs an ID"); return 64; }
|
|
116
|
+
const m = removeProvider(cfg, o.id);
|
|
117
|
+
if (!m.ok) { err("provider: " + m.error); return 3; }
|
|
118
|
+
next = m.cfg;
|
|
119
|
+
}
|
|
120
|
+
const text = JSON.stringify(next, null, 2) + "\n";
|
|
121
|
+
if (raw) { const bak = target + ".bak-" + stamp(); copyFileSync(target, bak); log("backup " + bak); }
|
|
122
|
+
writeFileSync(target + ".tmp", text);
|
|
123
|
+
renameSync(target + ".tmp", target);
|
|
124
|
+
log("provider " + o.id + (o.sub === "add" ? " added to " : " removed from ") + target);
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wkappbot-code",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.27",
|
|
4
4
|
"description": "wkappbot-code -- Wisdom Kernel: the pinned OpenCode bridge plus the reflex harness plugin, the wkappbot core, the web harness and the latest public skills in one install",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|