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/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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }