wkappbot-code 0.3.26 → 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/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/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": {
|