wkappbot-code 0.0.0-stage → 0.3.1
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 +73 -2
- package/bin/wkappbot-code.js +453 -0
- package/install.sh +16 -0
- package/lib/config.js +122 -0
- package/lib/update.js +127 -0
- package/lib/wk-bin-acquire.sh +305 -0
- package/opencode.json +31 -0
- package/package.json +31 -4
- package/plugin/wisdom-kernel.js +217 -0
package/lib/config.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// config.js -- the minimum opencode config for wkappbot-code (owner 2026-11-06: detect the needed items, let the user
|
|
2
|
+
// edit them, then generate). Four items only: provider and model (from which API-key env var is PRESENT, or an
|
|
3
|
+
// existing `opencode auth` login -- values are never read or printed), the wisdom-kernel plugin URL, the harness dir.
|
|
4
|
+
// The file is MERGED: a backup first, every user key kept in place, no secret written (providers read env vars).
|
|
5
|
+
// Spec: wkappbot skill read agent-harness-landscape-2026-howto
|
|
6
|
+
import { existsSync, readFileSync, readSync } from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
export const KEYS = ["provider", "model", "plugin", "harness"];
|
|
11
|
+
// env var -> opencode provider id -> default model ("provider/model", editable with --set model=...)
|
|
12
|
+
export const PROVIDERS = [
|
|
13
|
+
["ANTHROPIC_API_KEY", "anthropic", "anthropic/claude-sonnet-5-5"],
|
|
14
|
+
["OPENAI_API_KEY", "openai", "openai/gpt-5"],
|
|
15
|
+
["GEMINI_API_KEY", "google", "google/gemini-2.5-pro"],
|
|
16
|
+
["GROQ_API_KEY", "groq", "groq/llama-3.3-70b-versatile"],
|
|
17
|
+
["OPENROUTER_API_KEY", "openrouter", "openrouter/anthropic/claude-sonnet-4.5"],
|
|
18
|
+
];
|
|
19
|
+
const defaultModel = (prov) => { const d = PROVIDERS.find((p) => p[1] === prov); return d ? d[2] : prov + "/default"; };
|
|
20
|
+
|
|
21
|
+
// Provider ids of an existing `opencode auth login` (key names only).
|
|
22
|
+
export function authProviders(env = process.env) {
|
|
23
|
+
const p = join(env.XDG_DATA_HOME || join(homedir(), ".local", "share"), "opencode", "auth.json");
|
|
24
|
+
try { return Object.keys(JSON.parse(readFileSync(p, "utf8")) || {}); } catch { return []; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Detect the four items. user = the existing config object ({} when none).
|
|
28
|
+
export function detect({ env = process.env, user = {}, pluginUrl = "", kih = "", kihSource = "kih checkout" } = {}) {
|
|
29
|
+
const it = {};
|
|
30
|
+
if (user && user.model) it.model = { value: String(user.model), source: "existing config" };
|
|
31
|
+
else {
|
|
32
|
+
const hit = PROVIDERS.find(([k]) => env[k]);
|
|
33
|
+
if (hit) it.model = { value: hit[2], source: "env " + hit[0] + " present" };
|
|
34
|
+
else {
|
|
35
|
+
const a = authProviders(env)[0];
|
|
36
|
+
if (a) it.model = { value: defaultModel(a), source: "opencode auth login (" + a + ")" };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (it.model) it.provider = { value: it.model.value.split("/")[0], source: it.model.source };
|
|
40
|
+
if (pluginUrl) it.plugin = { value: pluginUrl, source: "installed package" };
|
|
41
|
+
if (kih) it.harness = { value: kih, source: kihSource };
|
|
42
|
+
return it;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function printDetected(it, log) {
|
|
46
|
+
for (const k of KEYS) log(it[k] ? "detected " + k + " = " + it[k].value + " (" + it[k].source + ")" : "missing " + k);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Keep provider and model consistent after an override.
|
|
50
|
+
function reconcile(it, changed) {
|
|
51
|
+
if (changed === "model" && it.model) it.provider = { value: it.model.value.split("/")[0], source: it.model.source, forced: true };
|
|
52
|
+
if (changed === "provider" && it.provider && (!it.model || it.model.value.split("/")[0] !== it.provider.value))
|
|
53
|
+
it.model = { value: defaultModel(it.provider.value), source: it.provider.source, forced: true };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// --set key=value overrides (provider|model|plugin|harness). Returns the unknown keys.
|
|
57
|
+
export function applySets(it, sets) {
|
|
58
|
+
const bad = [];
|
|
59
|
+
for (const s of sets) {
|
|
60
|
+
const i = s.indexOf("=");
|
|
61
|
+
const k = i > 0 ? s.slice(0, i) : s;
|
|
62
|
+
if (!KEYS.includes(k)) { bad.push(k); continue; }
|
|
63
|
+
it[k] = { value: s.slice(i + 1), source: "--set", forced: true };
|
|
64
|
+
reconcile(it, k);
|
|
65
|
+
}
|
|
66
|
+
return bad;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function readLineSync() {
|
|
70
|
+
const b = Buffer.alloc(1);
|
|
71
|
+
let s = "";
|
|
72
|
+
for (;;) {
|
|
73
|
+
let n = 0;
|
|
74
|
+
try { n = readSync(0, b, 0, 1, null); } catch (e) { if (e.code === "EAGAIN") continue; break; }
|
|
75
|
+
if (n === 0) break;
|
|
76
|
+
const c = b.toString("utf8");
|
|
77
|
+
if (c === "\n") break;
|
|
78
|
+
s += c;
|
|
79
|
+
}
|
|
80
|
+
return s.replace(/\r$/, "");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// At a TTY: one question per item; Enter keeps the detected value.
|
|
84
|
+
export function askEach(it, out = process.stdout) {
|
|
85
|
+
for (const k of KEYS) {
|
|
86
|
+
out.write(k + " [" + (it[k] ? it[k].value : "none") + "]: ");
|
|
87
|
+
const a = readLineSync().trim();
|
|
88
|
+
if (a) { it[k] = { value: a, source: "typed", forced: true }; reconcile(it, k); }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Merge the items into the user's config: user keys stay where they are; the wisdom-kernel plugin entry is replaced;
|
|
93
|
+
// the template's provider/permission are added only where the user has none; model only when absent or overridden.
|
|
94
|
+
export function buildConfig(user, it, template) {
|
|
95
|
+
const out = { ...user };
|
|
96
|
+
if (!out.$schema && template.$schema) out.$schema = template.$schema;
|
|
97
|
+
const ours = [it.plugin ? it.plugin.value : "", { kihDir: it.harness ? it.harness.value : "" }];
|
|
98
|
+
const keep = (Array.isArray(user.plugin) ? user.plugin : []).filter((p) => !String(Array.isArray(p) ? p[0] : p).includes("wisdom-kernel"));
|
|
99
|
+
out.plugin = [...keep, ours];
|
|
100
|
+
if (!out.provider) out.provider = template.provider;
|
|
101
|
+
else if (template.provider) for (const [k, v] of Object.entries(template.provider)) if (!(k in out.provider)) out.provider[k] = v;
|
|
102
|
+
if (!out.permission) out.permission = template.permission;
|
|
103
|
+
if (it.model && (it.model.forced || !user.model)) out.model = it.model.value;
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// A small line diff for the printout: removed lines first, then added lines.
|
|
108
|
+
export function lineDiff(before, after) {
|
|
109
|
+
const A = before.split("\n"), B = after.split("\n");
|
|
110
|
+
const sa = new Set(A), sb = new Set(B);
|
|
111
|
+
return [...A.filter((l) => l && !sb.has(l)).map((l) => "- " + l), ...B.filter((l) => l && !sa.has(l)).map((l) => "+ " + l)];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// The doctor row for an opencode.json path: { ok, detail }.
|
|
115
|
+
export function configRow(target) {
|
|
116
|
+
let c = null;
|
|
117
|
+
try { c = JSON.parse(readFileSync(target, "utf8")); } catch { return { ok: false, detail: existsSync(target) ? target + " is not plain JSON" : "missing " + target }; }
|
|
118
|
+
const wired = (c.plugin || []).some((p) => String(Array.isArray(p) ? p[0] : p).includes("wisdom-kernel"));
|
|
119
|
+
const model = c.model ? String(c.model) : "";
|
|
120
|
+
if (model && wired) return { ok: true, detail: "provider " + model.split("/")[0] + ", model " + model + ", plugin wired" };
|
|
121
|
+
return { ok: false, detail: [model ? "" : "missing model", wired ? "" : "plugin not wired"].filter(Boolean).join(", ") + " in " + target };
|
|
122
|
+
}
|
package/lib/update.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// update.js -- wkappbot-code self-update (owner 2026-11-06: an auto-update in the launcher, with QA).
|
|
2
|
+
// Remote: <base>/version.json = {"name":"wkappbot-code","version":"X.Y.Z","tgz":"wkappbot-code.tgz","sha256":"<64 hex>"}
|
|
3
|
+
// as the SDK Pages workflow publishes it; "tgz" (or "url"/"file") is resolved relative to version.json, and a missing
|
|
4
|
+
// sha256 is read from SHA256SUMS beside it. WKAPPBOT_CODE_UPDATE_URL overrides the address (tests);
|
|
5
|
+
// WKAPPBOT_CODE_NO_UPDATE_CHECK=1 turns the quiet check off. Spec: wkappbot skill read agent-harness-landscape-2026-howto
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { homedir, tmpdir } from "node:os";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
|
|
12
|
+
export const DEFAULT_UPDATE_URL = "https://kiexpert.github.io/wkappbot-kih/wkappbot-code/version.json";
|
|
13
|
+
export const updateUrl = () => process.env.WKAPPBOT_CODE_UPDATE_URL || DEFAULT_UPDATE_URL;
|
|
14
|
+
export const stampPath = () => join(homedir(), ".wkappbot-code", "last-update-check");
|
|
15
|
+
const DAY_MS = 24 * 3600 * 1000;
|
|
16
|
+
|
|
17
|
+
// -1 / 0 / 1 on the first three numeric parts ("0.3.10" > "0.3.9").
|
|
18
|
+
export function cmpVer(a, b) {
|
|
19
|
+
const p = (v) => String(v || "").replace(/^v/, "").split(/[.+-]/).slice(0, 3).map((n) => parseInt(n, 10) || 0);
|
|
20
|
+
const x = p(a), y = p(b);
|
|
21
|
+
for (let i = 0; i < 3; i++) if ((x[i] || 0) !== (y[i] || 0)) return (x[i] || 0) > (y[i] || 0) ? 1 : -1;
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function curl(url, out, secs) {
|
|
26
|
+
const r = spawnSync("curl", ["-fsSL", "-m", String(secs), url, "-o", out], { encoding: "utf8" });
|
|
27
|
+
return r.status === 0 ? "" : "curl " + url + " rc " + r.status + (r.stderr ? " " + r.stderr.trim() : "");
|
|
28
|
+
}
|
|
29
|
+
const rel = (base, p) => { try { return new URL(p, base).href; } catch { return p; } };
|
|
30
|
+
|
|
31
|
+
// Read the remote version.json: { ok, version, sha256, tgzUrl } or { ok: false, err }.
|
|
32
|
+
export function readRemote(secs = 20) {
|
|
33
|
+
const url = updateUrl();
|
|
34
|
+
const tmp = mkdtempSync(join(tmpdir(), "wkappbot-code-upd-"));
|
|
35
|
+
try {
|
|
36
|
+
const err = curl(url, join(tmp, "v.json"), secs);
|
|
37
|
+
if (err) return { ok: false, err };
|
|
38
|
+
let v = null;
|
|
39
|
+
try { v = JSON.parse(readFileSync(join(tmp, "v.json"), "utf8")); } catch { return { ok: false, err: "version.json is not JSON" }; }
|
|
40
|
+
if (!v || !v.version) return { ok: false, err: "version.json has no version" };
|
|
41
|
+
// A page for another package (or an unnamed one) is never an update: no downgrade, no cross-install.
|
|
42
|
+
if (!["wkappbot-code", "wk-appbot-code"].includes(String(v.name || ""))) return { ok: false, foreign: true, err: "version.json names package " + JSON.stringify(v.name || "") + ", not wkappbot-code" };
|
|
43
|
+
const tgzUrl = rel(url, v.url || v.tgz || v.file || "wkappbot-code.tgz");
|
|
44
|
+
let sha = String(v.sha256 || "").toLowerCase();
|
|
45
|
+
if (!/^[0-9a-f]{64}$/.test(sha) && !curl(rel(url, "SHA256SUMS"), join(tmp, "sums"), secs)) {
|
|
46
|
+
const name = tgzUrl.split("/").pop();
|
|
47
|
+
for (const l of readFileSync(join(tmp, "sums"), "utf8").split("\n")) {
|
|
48
|
+
const m = /^([0-9a-f]{64})\s+\*?(\S+)$/i.exec(l.trim());
|
|
49
|
+
if (m && m[2].split("/").pop() === name) sha = m[1].toLowerCase();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { ok: true, version: String(v.version), sha256: sha, tgzUrl, source: "pages" };
|
|
53
|
+
} finally { rmSync(tmp, { recursive: true, force: true }); }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// The second source (owner 2026-11-06: NPM_TOKEN added, the SDK publishes to the registry): npm view, silent offline.
|
|
57
|
+
// WKAPPBOT_CODE_NO_REGISTRY=1 skips it (tests).
|
|
58
|
+
export function readRegistry(secs = 15) {
|
|
59
|
+
if (process.env.WKAPPBOT_CODE_NO_REGISTRY === "1") return { ok: false, err: "registry check off" };
|
|
60
|
+
const r = spawnSync("npm", ["view", "wkappbot-code", "version"], { encoding: "utf8", timeout: secs * 1000 });
|
|
61
|
+
const v = (r.stdout || "").trim().split("\n").pop();
|
|
62
|
+
return r.status === 0 && /^\d+\.\d+\.\d+/.test(v) ? { ok: true, version: v, source: "registry" } : { ok: false, err: "npm view rc " + r.status };
|
|
63
|
+
}
|
|
64
|
+
const readAny = (secs) => { const p = readRemote(secs); if (p.ok || p.foreign) return p; const g = readRegistry(secs); return g.ok ? g : p; };
|
|
65
|
+
|
|
66
|
+
// `wkappbot-code update`: a newer remote is downloaded, sha256-checked, installed with npm i -g, then the NEW CLI
|
|
67
|
+
// re-runs `install --update` (core, harness, public skills). A sha mismatch changes nothing. Returns the exit code.
|
|
68
|
+
export function runUpdate({ current, cliPath, installArgs = [], log = console.log }) {
|
|
69
|
+
const r = readAny(30);
|
|
70
|
+
if (r.foreign) { log("PASS update: no update (" + r.err + ")"); return 0; }
|
|
71
|
+
if (!r.ok) { log("FAIL update: cannot read " + updateUrl() + " -- " + r.err + " (registry: none)"); return 1; }
|
|
72
|
+
if (cmpVer(r.version, current) <= 0) { log("PASS update: up to date (installed " + current + ", latest " + r.version + " from " + r.source + ")"); return 0; }
|
|
73
|
+
if (r.source === "registry") {
|
|
74
|
+
// npm verifies the registry tarball's own integrity hash.
|
|
75
|
+
log("PASS update: " + r.version + " available (installed " + current + ") on the npm registry");
|
|
76
|
+
const g = spawnSync("npm", ["install", "-g", "wkappbot-code@" + r.version, "--no-audit", "--no-fund", "--silent"], { encoding: "utf8" });
|
|
77
|
+
if (g.status !== 0) { log("FAIL update: npm install -g wkappbot-code@" + r.version + " rc " + g.status); return 1; }
|
|
78
|
+
log("PASS update: npm install -g wkappbot-code " + r.version);
|
|
79
|
+
return reinstall(cliPath, installArgs, log);
|
|
80
|
+
}
|
|
81
|
+
log("PASS update: " + r.version + " available (installed " + current + ") at " + r.tgzUrl);
|
|
82
|
+
if (!/^[0-9a-f]{64}$/.test(r.sha256)) { log("FAIL update: no sha256 published for " + r.version + "; nothing changed"); return 1; }
|
|
83
|
+
const tmp = mkdtempSync(join(tmpdir(), "wkappbot-code-upd-"));
|
|
84
|
+
try {
|
|
85
|
+
const tgz = join(tmp, "wkappbot-code-" + r.version + ".tgz");
|
|
86
|
+
const err = curl(r.tgzUrl, tgz, 300);
|
|
87
|
+
if (err) { log("FAIL update: download -- " + err + "; nothing changed"); return 1; }
|
|
88
|
+
const got = createHash("sha256").update(readFileSync(tgz)).digest("hex");
|
|
89
|
+
if (got !== r.sha256) { log("FAIL update: sha256 MISMATCH (expected " + r.sha256 + ", got " + got + "); nothing changed"); return 1; }
|
|
90
|
+
log("PASS update: sha256 ok " + got);
|
|
91
|
+
const n = spawnSync("npm", ["install", "-g", tgz, "--no-audit", "--no-fund", "--silent"], { encoding: "utf8" });
|
|
92
|
+
if (n.status !== 0) { log("FAIL update: npm install -g rc " + n.status + " " + (n.stderr || "").trim()); return 1; }
|
|
93
|
+
log("PASS update: npm install -g wkappbot-code " + r.version);
|
|
94
|
+
} finally { rmSync(tmp, { recursive: true, force: true }); }
|
|
95
|
+
return reinstall(cliPath, installArgs, log);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// The NEW CLI (npm's bin link now points at it) refreshes the core, the harness and the public skills.
|
|
99
|
+
function reinstall(cliPath, installArgs, log) {
|
|
100
|
+
const i = spawnSync(process.execPath, [cliPath, "install", "--update", ...installArgs],
|
|
101
|
+
{ stdio: "inherit", env: { ...process.env, WKAPPBOT_CODE_NO_UPDATE_CHECK: "1" } });
|
|
102
|
+
log((i.status === 0 ? "PASS" : "FAIL") + " update: install --update rc " + i.status);
|
|
103
|
+
return i.status === 0 ? 0 : 1;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// The quiet check every other subcommand runs: at most once per 24 h (stamp written first), silent on any failure.
|
|
107
|
+
// Returns the newer remote version, else "".
|
|
108
|
+
export function quietCheck(current, log = console.log) {
|
|
109
|
+
if (process.env.WKAPPBOT_CODE_NO_UPDATE_CHECK === "1") return "";
|
|
110
|
+
const sp = stampPath();
|
|
111
|
+
try { if (Date.now() - statSync(sp).mtimeMs < DAY_MS) return ""; } catch {}
|
|
112
|
+
try { mkdirSync(dirname(sp), { recursive: true }); writeFileSync(sp, new Date().toISOString() + "\n"); } catch {}
|
|
113
|
+
let r = null;
|
|
114
|
+
try { r = readAny(5); } catch { return ""; }
|
|
115
|
+
if (!r || !r.ok || cmpVer(r.version, current) <= 0) return "";
|
|
116
|
+
log("wkappbot-code " + r.version + " available -- run wkappbot-code update");
|
|
117
|
+
return r.version;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// The doctor row: { ok, skip, detail } -- PASS when current is the latest (or newer), WARN when older, SKIP offline.
|
|
121
|
+
export function updateRow(current) {
|
|
122
|
+
if (process.env.WKAPPBOT_CODE_NO_UPDATE_CHECK === "1") return { skip: true, detail: "current " + current + " (check off: WKAPPBOT_CODE_NO_UPDATE_CHECK=1)" };
|
|
123
|
+
let r = null;
|
|
124
|
+
try { r = readAny(10); } catch (e) { r = { ok: false, err: String(e) }; }
|
|
125
|
+
if (!r.ok) return { skip: true, detail: "current " + current + " / latest ? (offline: " + r.err + ")" };
|
|
126
|
+
return { ok: cmpVer(current, r.version) >= 0, detail: "current " + current + " / latest " + r.version + " (" + r.source + ")" };
|
|
127
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# wk-bin-acquire -- acquire, install and publish the wkappbot binary for every platform.
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# wk-bin-acquire.sh acquire [--dest PATH] [--dry-run] [--offline]
|
|
6
|
+
# Platform from uname: linux-x64, osx-arm64, osx-x64, win-x64. Unix: PATH is the binary file.
|
|
7
|
+
# Source 1 (unless --offline): SDK latest release asset wkappbot-PLATFORM, accepted only when its
|
|
8
|
+
# sha256 matches its SHA256SUMS line. Source 2: REPO/bin/PLATFORM/wkappbot, accepted only when
|
|
9
|
+
# its sha256 matches the MANIFEST.md row. Windows: PATH is a directory that receives
|
|
10
|
+
# wkappbot.exe and wkappbot-core.exe (xz -dc of bin/win-x64/wkappbot-core.exe.xz), from bin/ only.
|
|
11
|
+
# Copies, never links; writes via a .new temp then mv; "up to date" when the sha already matches.
|
|
12
|
+
# wk-bin-acquire.sh publish SRC [--platform P] [--dry-run]
|
|
13
|
+
# Stores SRC in REPO/bin/P/ (a Windows core .exe as wkappbot-core.exe.xz, xz -9e -T0), rewrites
|
|
14
|
+
# its MANIFEST.md row, commits the two paths with mode 100755 (non-Windows), then pushes.
|
|
15
|
+
#
|
|
16
|
+
# Env: WK_BIN_NO_PUSH=1 skips the push; WKAPPBOT_DIR locates bin/wkpush.sh;
|
|
17
|
+
# WKAPPBOT_HOME selects the Windows install directory; WK_BIN_BASE replaces the SDK release download
|
|
18
|
+
# base (a selftest points it at a file:// mock).
|
|
19
|
+
# Outside a git checkout (the copy shipped in the wkappbot-code npm package, wkappbot-code/lib/) REPO is
|
|
20
|
+
# empty: acquire then takes only the verified SDK asset, and the repo fallback refuses with a clear line.
|
|
21
|
+
set -euo pipefail
|
|
22
|
+
|
|
23
|
+
WK_DRY=0
|
|
24
|
+
WK_TMP=""
|
|
25
|
+
SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
26
|
+
REPO="$(git -C "$SELF_DIR" rev-parse --show-toplevel 2>/dev/null || true)"
|
|
27
|
+
|
|
28
|
+
die() {
|
|
29
|
+
command -v wk_stopwatch_lap >/dev/null 2>&1 && wk_stopwatch_lap "die" || true
|
|
30
|
+
echo "wk-bin-acquire: $*" >&2
|
|
31
|
+
exit 1
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
note() {
|
|
35
|
+
command -v wk_stopwatch_lap >/dev/null 2>&1 && wk_stopwatch_lap "note" || true
|
|
36
|
+
echo "wk-bin-acquire: $*" >&2
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
run() {
|
|
40
|
+
command -v wk_stopwatch_lap >/dev/null 2>&1 && wk_stopwatch_lap "run" || true
|
|
41
|
+
if [ "$WK_DRY" = 1 ]; then
|
|
42
|
+
echo "DRY: $*"
|
|
43
|
+
else
|
|
44
|
+
"$@"
|
|
45
|
+
fi
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
sha_of() {
|
|
49
|
+
command -v wk_stopwatch_lap >/dev/null 2>&1 && wk_stopwatch_lap "sha_of" || true
|
|
50
|
+
[ -f "$1" ] || return 1
|
|
51
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
52
|
+
sha256sum "$1" | cut -d' ' -f1
|
|
53
|
+
else
|
|
54
|
+
shasum -a 256 "$1" | cut -d' ' -f1
|
|
55
|
+
fi
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
platform_detect() {
|
|
59
|
+
command -v wk_stopwatch_lap >/dev/null 2>&1 && wk_stopwatch_lap "platform_detect" || true
|
|
60
|
+
local s m
|
|
61
|
+
s="$(uname -s)"
|
|
62
|
+
m="$(uname -m)"
|
|
63
|
+
case "$s" in
|
|
64
|
+
Linux)
|
|
65
|
+
if [ "$m" = x86_64 ]; then echo linux-x64; else die "unsupported Linux arch: $m"; fi ;;
|
|
66
|
+
Darwin)
|
|
67
|
+
case "$m" in
|
|
68
|
+
arm64) echo osx-arm64 ;;
|
|
69
|
+
x86_64) echo osx-x64 ;;
|
|
70
|
+
*) die "unsupported macOS arch: $m" ;;
|
|
71
|
+
esac ;;
|
|
72
|
+
MINGW*|MSYS*|CYGWIN*) echo win-x64 ;;
|
|
73
|
+
*) die "unsupported OS: $s" ;;
|
|
74
|
+
esac
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# manifest_field REL COL -- COL 3 platform, 4 version, 6 sha256 of the bin/MANIFEST.md row for REL.
|
|
78
|
+
manifest_field() {
|
|
79
|
+
command -v wk_stopwatch_lap >/dev/null 2>&1 && wk_stopwatch_lap "manifest_field" || true
|
|
80
|
+
awk -F'|' -v p="$1" -v c="$2" '
|
|
81
|
+
{ x = $2; gsub(/^[ \t]+|[ \t]+$/, "", x)
|
|
82
|
+
if (x == p) { y = $c; gsub(/^[ \t]+|[ \t]+$/, "", y); print y; exit } }' "$REPO/bin/MANIFEST.md"
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
default_dest_unix() {
|
|
86
|
+
command -v wk_stopwatch_lap >/dev/null 2>&1 && wk_stopwatch_lap "default_dest_unix" || true
|
|
87
|
+
if [ -w /opt/wkappbot ] || { [ ! -e /opt/wkappbot ] && [ -w /opt ]; }; then
|
|
88
|
+
echo /opt/wkappbot/wkappbot
|
|
89
|
+
else
|
|
90
|
+
echo "$HOME/.local/opt/wkappbot/wkappbot"
|
|
91
|
+
fi
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
default_dest_win() {
|
|
95
|
+
command -v wk_stopwatch_lap >/dev/null 2>&1 && wk_stopwatch_lap "default_dest_win" || true
|
|
96
|
+
if [ -n "${WKAPPBOT_HOME:-}" ]; then
|
|
97
|
+
echo "$WKAPPBOT_HOME/bin"
|
|
98
|
+
else
|
|
99
|
+
echo "$HOME/wkappbot/bin"
|
|
100
|
+
fi
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# sdk_fetch PLAT DIR -- prints the verified asset path, or returns 1 and prints nothing.
|
|
104
|
+
sdk_fetch() {
|
|
105
|
+
command -v wk_stopwatch_lap >/dev/null 2>&1 && wk_stopwatch_lap "sdk_fetch" || true
|
|
106
|
+
local plat="$1" dir="$2" asset base want got
|
|
107
|
+
asset="wkappbot-$plat"
|
|
108
|
+
base="${WK_BIN_BASE:-https://github.com/kiexpert/wkappbot-sdk/releases/latest/download}"
|
|
109
|
+
curl -fsSL -m 120 "$base/$asset" -o "$dir/$asset" || return 1
|
|
110
|
+
curl -fsSL -m 120 "$base/SHA256SUMS" -o "$dir/SHA256SUMS" || return 1
|
|
111
|
+
want="$(awk -v f="$asset" '{ n = $2; sub(/^\*/, "", n); if (n == f) { print $1; exit } }' "$dir/SHA256SUMS")"
|
|
112
|
+
[ -n "$want" ] || return 1
|
|
113
|
+
got="$(sha_of "$dir/$asset" || true)"
|
|
114
|
+
[ "$got" = "$want" ] || return 1
|
|
115
|
+
echo "$dir/$asset"
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# repo_file REL -- prints REPO/bin/REL when its sha256 matches the MANIFEST row, else dies.
|
|
119
|
+
repo_file() {
|
|
120
|
+
command -v wk_stopwatch_lap >/dev/null 2>&1 && wk_stopwatch_lap "repo_file" || true
|
|
121
|
+
local rel="$1" want got
|
|
122
|
+
[ -n "$REPO" ] || die "no verified SDK asset and no repo copy of bin/$rel (this copy is not inside a kih checkout)"
|
|
123
|
+
want="$(manifest_field "$rel" 6 || true)"
|
|
124
|
+
[ -n "$want" ] || die "no MANIFEST row for bin/$rel"
|
|
125
|
+
got="$(sha_of "$REPO/bin/$rel" || true)"
|
|
126
|
+
[ -n "$got" ] || die "missing bin/$rel"
|
|
127
|
+
[ "$got" = "$want" ] || die "sha256 mismatch for bin/$rel: MANIFEST $want, file $got"
|
|
128
|
+
echo "$REPO/bin/$rel"
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# install_file SRC DEST LABEL -- copy via .new then mv; "up to date" when DEST already matches.
|
|
132
|
+
install_file() {
|
|
133
|
+
command -v wk_stopwatch_lap >/dev/null 2>&1 && wk_stopwatch_lap "install_file" || true
|
|
134
|
+
local src="$1" dest="$2" label="$3" sum verb="installed"
|
|
135
|
+
sum="$(sha_of "$src")"
|
|
136
|
+
if [ -f "$dest" ] && [ "$(sha_of "$dest" || true)" = "$sum" ]; then
|
|
137
|
+
echo "up to date: $dest (source: $label) sha256=$sum"
|
|
138
|
+
return 0
|
|
139
|
+
fi
|
|
140
|
+
if [ "$WK_DRY" = 1 ]; then verb="would install"; fi
|
|
141
|
+
run mkdir -p "$(dirname "$dest")"
|
|
142
|
+
run cp "$src" "$dest.new"
|
|
143
|
+
run chmod +x "$dest.new"
|
|
144
|
+
run mv -f "$dest.new" "$dest"
|
|
145
|
+
echo "$verb: $dest (source: $label) sha256=$sum"
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
acquire_unix() {
|
|
149
|
+
command -v wk_stopwatch_lap >/dev/null 2>&1 && wk_stopwatch_lap "acquire_unix" || true
|
|
150
|
+
local dest="$1" plat="$2" offline="$3" src label
|
|
151
|
+
[ -n "$dest" ] || dest="$(default_dest_unix)"
|
|
152
|
+
if [ "$offline" = 0 ] && src="$(sdk_fetch "$plat" "$WK_TMP")"; then
|
|
153
|
+
label="SDK release wkappbot-$plat"
|
|
154
|
+
else
|
|
155
|
+
if [ "$offline" = 0 ]; then note "SDK asset unavailable or unverified, using repo bin/$plat"; fi
|
|
156
|
+
src="$(repo_file "$plat/wkappbot")"
|
|
157
|
+
label="repo bin/$plat/wkappbot"
|
|
158
|
+
fi
|
|
159
|
+
install_file "$src" "$dest" "$label"
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
acquire_win() {
|
|
163
|
+
command -v wk_stopwatch_lap >/dev/null 2>&1 && wk_stopwatch_lap "acquire_win" || true
|
|
164
|
+
local dest="$1" exe xzsrc core
|
|
165
|
+
[ -n "$dest" ] || dest="$(default_dest_win)"
|
|
166
|
+
note "win-x64 installs from bin/ only: the two-file set has no single SDK asset"
|
|
167
|
+
exe="$(repo_file "win-x64/wkappbot.exe")"
|
|
168
|
+
xzsrc="$(repo_file "win-x64/wkappbot-core.exe.xz")"
|
|
169
|
+
core="$WK_TMP/wkappbot-core.exe"
|
|
170
|
+
xz -dc "$xzsrc" > "$core"
|
|
171
|
+
install_file "$exe" "$dest/wkappbot.exe" "repo bin/win-x64/wkappbot.exe"
|
|
172
|
+
install_file "$core" "$dest/wkappbot-core.exe" "repo bin/win-x64/wkappbot-core.exe.xz"
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
acquire() {
|
|
176
|
+
command -v wk_stopwatch_lap >/dev/null 2>&1 && wk_stopwatch_lap "acquire" || true
|
|
177
|
+
local dest="" offline=0 plat
|
|
178
|
+
while [ $# -gt 0 ]; do
|
|
179
|
+
case "$1" in
|
|
180
|
+
--dest) dest="${2:-}"; [ -n "$dest" ] || die "--dest needs a path"; shift 2 ;;
|
|
181
|
+
--dry-run) WK_DRY=1; shift ;;
|
|
182
|
+
--offline) offline=1; shift ;;
|
|
183
|
+
*) die "acquire: unknown argument: $1" ;;
|
|
184
|
+
esac
|
|
185
|
+
done
|
|
186
|
+
plat="$(platform_detect)"
|
|
187
|
+
if [ "$plat" = win-x64 ]; then
|
|
188
|
+
acquire_win "$dest"
|
|
189
|
+
else
|
|
190
|
+
acquire_unix "$dest" "$plat" "$offline"
|
|
191
|
+
fi
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
# rewrite_manifest REL ROW -- replaces REL's row, or appends ROW after the last table row.
|
|
195
|
+
rewrite_manifest() {
|
|
196
|
+
command -v wk_stopwatch_lap >/dev/null 2>&1 && wk_stopwatch_lap "rewrite_manifest" || true
|
|
197
|
+
local rel="$1" row="$2" m="$REPO/bin/MANIFEST.md"
|
|
198
|
+
if [ "$WK_DRY" = 1 ]; then
|
|
199
|
+
echo "DRY: set bin/MANIFEST.md row: $row"
|
|
200
|
+
return 0
|
|
201
|
+
fi
|
|
202
|
+
[ -f "$m" ] || die "no $m"
|
|
203
|
+
grep -q '^|' "$m" || die "no table in $m"
|
|
204
|
+
awk -v p="$rel" -v nr="$row" '
|
|
205
|
+
function rowpath(s, a, x) { split(s, a, "|"); x = a[2]; gsub(/^[ \t]+|[ \t]+$/, "", x); return x }
|
|
206
|
+
{ L[NR] = $0
|
|
207
|
+
if ($0 ~ /^\|/) { last = NR; if (rowpath($0) == p) hit = NR } }
|
|
208
|
+
END {
|
|
209
|
+
for (i = 1; i <= NR; i++) {
|
|
210
|
+
if (i == hit) print nr; else print L[i]
|
|
211
|
+
if (hit == "" && i == last) print nr
|
|
212
|
+
}
|
|
213
|
+
}' "$m" > "$m.new"
|
|
214
|
+
mv -f "$m.new" "$m"
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
# push_bin GPATH -- pushes the two committed paths; skipped with WK_BIN_NO_PUSH=1.
|
|
218
|
+
push_bin() {
|
|
219
|
+
command -v wk_stopwatch_lap >/dev/null 2>&1 && wk_stopwatch_lap "push_bin" || true
|
|
220
|
+
local gpath="$1" wkp="${WKAPPBOT_DIR:-$(dirname "$REPO")/WKAppBot}/bin/wkpush.sh"
|
|
221
|
+
if [ "${WK_BIN_NO_PUSH:-0}" = 1 ]; then
|
|
222
|
+
echo "push skipped: WK_BIN_NO_PUSH=1"
|
|
223
|
+
return 0
|
|
224
|
+
fi
|
|
225
|
+
if [ -f "$wkp" ]; then
|
|
226
|
+
run bash "$wkp" -C "$REPO" -m "$gpath" bin/MANIFEST.md
|
|
227
|
+
else
|
|
228
|
+
run git -C "$REPO" push origin HEAD:main
|
|
229
|
+
fi
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
publish() {
|
|
233
|
+
command -v wk_stopwatch_lap >/dev/null 2>&1 && wk_stopwatch_lap "publish" || true
|
|
234
|
+
local src plat="" base rel gpath mode="copy" stored sha bytes v="" ver desc newrow dest msg
|
|
235
|
+
src="${1:-}"
|
|
236
|
+
[ -n "$src" ] || die "publish: SRC required"
|
|
237
|
+
shift
|
|
238
|
+
while [ $# -gt 0 ]; do
|
|
239
|
+
case "$1" in
|
|
240
|
+
--platform) plat="${2:-}"; [ -n "$plat" ] || die "--platform needs a value"; shift 2 ;;
|
|
241
|
+
--dry-run) WK_DRY=1; shift ;;
|
|
242
|
+
*) die "publish: unknown argument: $1" ;;
|
|
243
|
+
esac
|
|
244
|
+
done
|
|
245
|
+
[ -n "$plat" ] || plat="$(platform_detect)"
|
|
246
|
+
[ -f "$src" ] || die "publish: no such file: $src"
|
|
247
|
+
base="$(basename "$src")"
|
|
248
|
+
rel="$plat/$base"
|
|
249
|
+
if [ "$plat" = win-x64 ]; then
|
|
250
|
+
case "$base" in
|
|
251
|
+
*core*.exe) rel="$plat/wkappbot-core.exe.xz"; mode="xz" ;;
|
|
252
|
+
esac
|
|
253
|
+
fi
|
|
254
|
+
gpath="bin/$rel"
|
|
255
|
+
# Scratch copy only: the repo is touched by run() lines alone, so --dry-run changes nothing.
|
|
256
|
+
stored="$WK_TMP/stored"
|
|
257
|
+
if [ "$mode" = xz ]; then xz -9e -T0 -c "$src" > "$stored"; else cp "$src" "$stored"; fi
|
|
258
|
+
sha="$(sha_of "$stored")"
|
|
259
|
+
bytes="$(wc -c < "$stored" | tr -d ' ')"
|
|
260
|
+
# A Windows core is never executed here; every other SRC reports its own version line.
|
|
261
|
+
if [ "$mode" = copy ]; then
|
|
262
|
+
v="$("$src" --version 2>/dev/null | head -n 1 || true)"
|
|
263
|
+
fi
|
|
264
|
+
if [ -n "$v" ]; then v="${v#wkappbot }"; v="${v//|/}"; fi
|
|
265
|
+
ver="$(manifest_field "$rel" 4 || true)"
|
|
266
|
+
if [ -n "$v" ]; then ver="$v"; fi
|
|
267
|
+
[ -n "$ver" ] || ver="unknown"
|
|
268
|
+
desc="$(manifest_field "$rel" 3 || true)"
|
|
269
|
+
[ -n "$desc" ] || desc="$plat"
|
|
270
|
+
newrow="| $rel | $desc | $ver | $bytes | $sha |"
|
|
271
|
+
dest="$REPO/$gpath"
|
|
272
|
+
run mkdir -p "$(dirname "$dest")"
|
|
273
|
+
run cp "$stored" "$dest"
|
|
274
|
+
if [ "$plat" != win-x64 ]; then run chmod 755 "$dest"; fi
|
|
275
|
+
rewrite_manifest "$rel" "$newrow"
|
|
276
|
+
msg="bin: publish $plat binary ${sha:0:12}"
|
|
277
|
+
run git -C "$REPO" add -- "$gpath" bin/MANIFEST.md
|
|
278
|
+
if [ "$plat" != win-x64 ]; then run git -C "$REPO" update-index --chmod=+x -- "$gpath"; fi
|
|
279
|
+
run git -C "$REPO" commit -m "$msg" -- "$gpath" bin/MANIFEST.md
|
|
280
|
+
push_bin "$gpath"
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
usage() {
|
|
284
|
+
command -v wk_stopwatch_lap >/dev/null 2>&1 && wk_stopwatch_lap "usage" || true
|
|
285
|
+
cat <<'EOF'
|
|
286
|
+
usage: wk-bin-acquire.sh acquire [--dest PATH] [--dry-run] [--offline]
|
|
287
|
+
wk-bin-acquire.sh publish SRC [--platform P] [--dry-run]
|
|
288
|
+
EOF
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
main() {
|
|
292
|
+
command -v wk_stopwatch_lap >/dev/null 2>&1 && wk_stopwatch_lap "main" || true
|
|
293
|
+
local verb="${1:-}"
|
|
294
|
+
WK_TMP="$(mktemp -d)"
|
|
295
|
+
trap 'rm -rf "$WK_TMP"' EXIT
|
|
296
|
+
if [ $# -gt 0 ]; then shift; fi
|
|
297
|
+
case "$verb" in
|
|
298
|
+
acquire) acquire "$@" ;;
|
|
299
|
+
publish) publish "$@" ;;
|
|
300
|
+
-h|--help) usage ;;
|
|
301
|
+
*) usage; die "unknown verb: '$verb' (acquire|publish)" ;;
|
|
302
|
+
esac
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
main "$@"
|
package/opencode.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://opencode.ai/config.json",
|
|
3
|
+
"plugin": [
|
|
4
|
+
["__WK_PLUGIN_URL__", { "kihDir": "__WK_KIH_DIR__" }]
|
|
5
|
+
],
|
|
6
|
+
"permission": {
|
|
7
|
+
"edit": "ask",
|
|
8
|
+
"webfetch": "ask",
|
|
9
|
+
"bash": {
|
|
10
|
+
"*": "ask",
|
|
11
|
+
"ls*": "allow",
|
|
12
|
+
"pwd": "allow",
|
|
13
|
+
"git status*": "allow",
|
|
14
|
+
"git diff*": "allow",
|
|
15
|
+
"git log*": "allow"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"provider": {
|
|
19
|
+
"qwen-inhouse": {
|
|
20
|
+
"npm": "@ai-sdk/openai-compatible",
|
|
21
|
+
"name": "In-house Qwen (OpenAI-compatible)",
|
|
22
|
+
"options": {
|
|
23
|
+
"baseURL": "{env:QWEN_BASE_URL}",
|
|
24
|
+
"apiKey": "{env:QWEN_API_KEY}"
|
|
25
|
+
},
|
|
26
|
+
"models": {
|
|
27
|
+
"qwen3-coder": { "name": "Qwen3 Coder (in-house)" }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wkappbot-code",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.3.1",
|
|
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
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"wkappbot-code": "bin/wkappbot-code.js",
|
|
8
|
+
"wk-appbot-code": "bin/wkappbot-code.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"lib/",
|
|
13
|
+
"plugin/",
|
|
14
|
+
"opencode.json",
|
|
15
|
+
"install.sh",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"wkAppbotCode": {
|
|
19
|
+
"opencodeVersion": "1.19.5"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"license": "UNLICENSED",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/kiexpert/wkappbot-sdk.git"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/kiexpert/wkappbot-sdk",
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
}
|
|
33
|
+
}
|