wkappbot-code 0.3.0 → 0.3.2
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 +4 -5
- package/bin/wkappbot-code.js +65 -32
- package/lib/config.js +122 -0
- package/lib/update.js +127 -0
- package/package.json +2 -2
- package/plugin/wisdom-kernel.js +55 -12
package/README.md
CHANGED
|
@@ -26,13 +26,12 @@ bash wkappbot-code/selftest.sh # RESULT N/N; --synthetic-only where no
|
|
|
26
26
|
|
|
27
27
|
### One command installs everything
|
|
28
28
|
|
|
29
|
-
Get the CLI by one of
|
|
29
|
+
Get the CLI by one of three roads, then run `wkappbot-code install` once:
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
32
|
npm i -g https://kiexpert.github.io/wkappbot-kih/wkappbot-code/wkappbot-code.tgz # web: the scrub-gated tgz the SDK workflow ships daily (SHA256SUMS + version.json beside it)
|
|
33
33
|
npm i -g wkappbot-code # registry (published only when the SDK has NPM_TOKEN)
|
|
34
|
-
npm i -g
|
|
35
|
-
npm pack ./wkappbot-code && npm i -g ./wkappbot-code-0.3.0.tgz # from a checkout
|
|
34
|
+
npm pack ./wkappbot-code && npm i -g ./wkappbot-code-0.3.2.tgz # from a checkout
|
|
36
35
|
|
|
37
36
|
wkappbot-code install # every part below, idempotent
|
|
38
37
|
wkappbot-code doctor # one PASS/WARN/FAIL/SKIP row per part
|
|
@@ -40,7 +39,6 @@ wkappbot-code --version
|
|
|
40
39
|
# the old command name wk-appbot-code (0.2.x) still works: it is a second bin entry for the same file
|
|
41
40
|
```
|
|
42
41
|
|
|
43
|
-
The git road needs `--install-links`: without it npm 10 links the bin to a temp clone it then deletes.
|
|
44
42
|
|
|
45
43
|
| part | what `install` lays down | skip with |
|
|
46
44
|
|---|---|---|
|
|
@@ -72,4 +70,5 @@ The kih lite hooks stay live in the guest. Each question is a prompt event, and
|
|
|
72
70
|
|
|
73
71
|
## Floor
|
|
74
72
|
|
|
75
|
-
|
|
73
|
+
|
|
74
|
+
Signing, submitting and paying stay human-only.
|
package/bin/wkappbot-code.js
CHANGED
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
// a READ-ONLY copy with no git remote. A user's own skill edits go to the private
|
|
15
15
|
// home ~/.wkappbot/private-skills (a local git repo, no remote) skip: --no-skills
|
|
16
16
|
// wkappbot-code doctor [opts] one PASS/WARN/FAIL/SKIP row per part (exit 1 if a required part fails)
|
|
17
|
+
// wkappbot-code update [opts] newer version.json -> tgz sha256-checked -> npm i -g -> install --update (lib/update.js)
|
|
18
|
+
// wkappbot-code config [opts] detect provider/model/plugin/harness, edit at a TTY, merge into opencode.json (lib/config.js)
|
|
19
|
+
// install/doctor/config check for a newer version at most once a day (WKAPPBOT_CODE_NO_UPDATE_CHECK=1 turns it off;
|
|
20
|
+
// --auto-update or WKAPPBOT_CODE_AUTO_UPDATE=1 applies it). config opts: --yes, --project, --set key=value
|
|
17
21
|
// opts: --config-dir DIR, --kih-dir DIR, --bin-dest FILE, --hq-dir DIR
|
|
18
22
|
// env (mock and relocation): WK_BIN_BASE (SDK release base), WK_HARNESS_BASE (web harness base),
|
|
19
23
|
// WK_HARNESS_DIR (~/.wk-harness), WK_BIN_DEST, WK_APPBOT_HQ, WK_LINK_DIR, WK_PRIVATE_SKILLS,
|
|
@@ -30,6 +34,8 @@ import {
|
|
|
30
34
|
import { homedir, tmpdir } from "node:os";
|
|
31
35
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
32
36
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
37
|
+
import { quietCheck, runUpdate, updateRow } from "../lib/update.js";
|
|
38
|
+
import { applySets, askEach, buildConfig, configRow, detect, lineDiff, printDetected } from "../lib/config.js";
|
|
33
39
|
|
|
34
40
|
const PKG_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
35
41
|
const PKG = JSON.parse(readFileSync(join(PKG_DIR, "package.json"), "utf8"));
|
|
@@ -41,7 +47,7 @@ const IS_WIN = process.platform === "win32";
|
|
|
41
47
|
|
|
42
48
|
function parseArgs(argv) {
|
|
43
49
|
const o = { cmd: argv[0] || "help", npm: true, bin: true, harness: true, skills: true, webHarness: false,
|
|
44
|
-
configDir: "", kihDir: "", binDest: "", hqDir: "" };
|
|
50
|
+
configDir: "", kihDir: "", binDest: "", hqDir: "", update: false, autoUpdate: false, yes: false, project: false, sets: [] };
|
|
45
51
|
for (let i = 1; i < argv.length; i++) {
|
|
46
52
|
const a = argv[i];
|
|
47
53
|
if (a === "--no-npm") o.npm = false;
|
|
@@ -53,6 +59,11 @@ function parseArgs(argv) {
|
|
|
53
59
|
else if (a === "--kih-dir") o.kihDir = argv[++i] || "";
|
|
54
60
|
else if (a === "--bin-dest") o.binDest = argv[++i] || "";
|
|
55
61
|
else if (a === "--hq-dir") o.hqDir = argv[++i] || "";
|
|
62
|
+
else if (a === "--update") o.update = true;
|
|
63
|
+
else if (a === "--auto-update") o.autoUpdate = true;
|
|
64
|
+
else if (a === "--yes" || a === "-y") o.yes = true;
|
|
65
|
+
else if (a === "--project") o.project = true;
|
|
66
|
+
else if (a === "--set") o.sets.push(argv[++i] || "");
|
|
56
67
|
else { console.error("unknown option: " + a); process.exit(64); }
|
|
57
68
|
}
|
|
58
69
|
return o;
|
|
@@ -81,7 +92,7 @@ function kihDir(o) {
|
|
|
81
92
|
}
|
|
82
93
|
|
|
83
94
|
function opencodeVersion() {
|
|
84
|
-
try { return execFileSync("opencode", ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim(); }
|
|
95
|
+
try { return execFileSync("opencode", ["--version"], { shell: process.platform === "win32", encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim(); }
|
|
85
96
|
catch { return ""; }
|
|
86
97
|
}
|
|
87
98
|
|
|
@@ -101,15 +112,7 @@ export function platformId(platform = process.platform, arch = process.arch) {
|
|
|
101
112
|
|
|
102
113
|
// Merge our plugin entry into a user config: drop any older wisdom-kernel entry, append the new one.
|
|
103
114
|
export function mergeConfig(user, template, url, kih) {
|
|
104
|
-
|
|
105
|
-
const ours = [url, { kihDir: kih }];
|
|
106
|
-
const keep = (Array.isArray(user.plugin) ? user.plugin : []).filter((p) => !String(Array.isArray(p) ? p[0] : p).includes("wisdom-kernel"));
|
|
107
|
-
out.plugin = [...keep, ours];
|
|
108
|
-
if (!out.$schema && template.$schema) out.$schema = template.$schema;
|
|
109
|
-
if (!out.provider) out.provider = template.provider;
|
|
110
|
-
else if (template.provider) for (const [k, v] of Object.entries(template.provider)) if (!(k in out.provider)) out.provider[k] = v;
|
|
111
|
-
if (!out.permission) out.permission = template.permission;
|
|
112
|
-
return out;
|
|
115
|
+
return buildConfig(user, { plugin: { value: url }, harness: { value: kih } }, template);
|
|
113
116
|
}
|
|
114
117
|
|
|
115
118
|
// ---- part: bin ------------------------------------------------------------------------------------
|
|
@@ -266,7 +269,7 @@ function installSkills(o, dest) {
|
|
|
266
269
|
}
|
|
267
270
|
|
|
268
271
|
// ---- install --------------------------------------------------------------------------------------
|
|
269
|
-
function installPlugin(o, kih) {
|
|
272
|
+
function installPlugin(o, kih, kihSource) {
|
|
270
273
|
const cfg = configDir(o);
|
|
271
274
|
mkdirSync(join(cfg, "wkappbot-code"), { recursive: true });
|
|
272
275
|
const dest = join(cfg, "wkappbot-code", "wisdom-kernel.js");
|
|
@@ -275,29 +278,36 @@ function installPlugin(o, kih) {
|
|
|
275
278
|
writeFileSync(join(cfg, "wkappbot-code", "package.json"), '{ "type": "module" }\n');
|
|
276
279
|
const url = pathToFileURL(dest).href;
|
|
277
280
|
const tmpl = JSON.parse(readFileSync(join(PKG_DIR, "opencode.json"), "utf8"));
|
|
278
|
-
const target = join(cfg, "opencode.json");
|
|
279
|
-
let
|
|
281
|
+
const target = o.project ? join(process.cwd(), "opencode.json") : join(cfg, "opencode.json");
|
|
282
|
+
let user = {}, raw = "";
|
|
280
283
|
if (existsSync(target)) {
|
|
281
|
-
|
|
282
|
-
let user;
|
|
284
|
+
raw = readFileSync(target, "utf8");
|
|
283
285
|
try { user = JSON.parse(raw); } catch { user = null; }
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
286
|
+
}
|
|
287
|
+
const it = detect({ user: user || {}, pluginUrl: url, kih, kihSource: kihSource || "kih checkout" });
|
|
288
|
+
for (const k of applySets(it, o.sets)) log("config: unknown --set key " + k + " (provider|model|plugin|harness)");
|
|
289
|
+
printDetected(it, log);
|
|
290
|
+
if (process.stdin.isTTY && process.stdout.isTTY && !o.yes) askEach(it);
|
|
291
|
+
if (!user) {
|
|
292
|
+
// JSONC or broken: never rewrite what we cannot parse; leave a sidecar to merge by hand.
|
|
293
|
+
const side = join(dirname(target), "wkappbot-code.json");
|
|
294
|
+
writeFileSync(side, JSON.stringify(buildConfig({}, it, tmpl), null, 2) + "\n");
|
|
295
|
+
log(target + " is not plain JSON; left untouched, wrote " + side + " -- merge its plugin entry by hand");
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const text = JSON.stringify(buildConfig(user, it, tmpl), null, 2) + "\n";
|
|
299
|
+
let note = "";
|
|
300
|
+
if (raw && text === raw) note = " (unchanged)";
|
|
301
|
+
else {
|
|
302
|
+
if (raw) {
|
|
294
303
|
const bak = target + ".bak-" + stamp();
|
|
295
304
|
copyFileSync(target, bak);
|
|
296
305
|
note = " (merged; backup " + bak + ")";
|
|
297
306
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
307
|
+
for (const l of lineDiff(raw, text)) log("diff " + l);
|
|
308
|
+
writeFileSync(target + ".tmp", text);
|
|
309
|
+
renameSync(target + ".tmp", target);
|
|
310
|
+
}
|
|
301
311
|
log("plugin: " + url);
|
|
302
312
|
log("config: " + target + note);
|
|
303
313
|
}
|
|
@@ -305,12 +315,13 @@ function installPlugin(o, kih) {
|
|
|
305
315
|
function install(o) {
|
|
306
316
|
const st = { version: PKG.version, at: new Date().toISOString() };
|
|
307
317
|
let bad = 0;
|
|
318
|
+
if (o.update) log("install --update: refreshing the core, the harness and the public skills");
|
|
308
319
|
if (o.npm) {
|
|
309
320
|
const have = opencodeVersion();
|
|
310
321
|
if (have === OPENCODE_VERSION) log("opencode " + have + " already installed");
|
|
311
322
|
else {
|
|
312
323
|
log("npm install -g opencode-ai@" + OPENCODE_VERSION + " (was: " + (have || "none") + ")");
|
|
313
|
-
execFileSync("npm", ["install", "-g", "opencode-ai@" + OPENCODE_VERSION, "--no-audit", "--no-fund"], { stdio: "inherit" });
|
|
324
|
+
execFileSync("npm", ["install", "-g", "opencode-ai@" + OPENCODE_VERSION, "--no-audit", "--no-fund"], { shell: process.platform === "win32", stdio: "inherit" });
|
|
314
325
|
}
|
|
315
326
|
}
|
|
316
327
|
const dest = binDest(o);
|
|
@@ -330,7 +341,7 @@ function install(o) {
|
|
|
330
341
|
}
|
|
331
342
|
st.skills = o.skills ? installSkills(o, dest) : { skipped: true };
|
|
332
343
|
if (o.skills && !st.skills.ok) { bad++; log("skills: FAILED -- " + (st.skills.note || "on-load missing from the HQ")); }
|
|
333
|
-
installPlugin(o, kih);
|
|
344
|
+
installPlugin(o, kih, (st.harness.mode === "web" && st.harness.ok) ? "web harness" : "kih checkout");
|
|
334
345
|
st.kihDir = kih;
|
|
335
346
|
writeFileSync(statePath(o), JSON.stringify(st, null, 2) + "\n");
|
|
336
347
|
log("kih: " + (kih || "NOT FOUND -- plugin fails open until --kih-dir or WK_KIH_DIR points at a kih checkout"));
|
|
@@ -358,6 +369,8 @@ function doctor(o) {
|
|
|
358
369
|
let entry = null;
|
|
359
370
|
try { entry = (JSON.parse(readFileSync(join(cfg, "opencode.json"), "utf8")).plugin || []).find((p) => String(Array.isArray(p) ? p[0] : p).includes("wisdom-kernel")); } catch {}
|
|
360
371
|
add("config", !!entry, join(cfg, "opencode.json"), true);
|
|
372
|
+
const oc = configRow(join(cfg, "opencode.json"));
|
|
373
|
+
add("opencode-config", oc.ok, oc.detail, false);
|
|
361
374
|
// bin: the core answers --version
|
|
362
375
|
const dest = o.binDest ? resolve(o.binDest) : (st.bin && st.bin.dest) || binDest(o);
|
|
363
376
|
let core = "";
|
|
@@ -372,6 +385,14 @@ function doctor(o) {
|
|
|
372
385
|
const hk = hasHook(k);
|
|
373
386
|
if (st.harness && st.harness.skipped && !hk) add("kih-hook", false, "skipped (--no-harness)", false, true);
|
|
374
387
|
else add("kih-hook", hk, k ? k + (st.harness && st.harness.mode ? " (" + st.harness.mode + ")" : "") : "not found", !!(st.harness && st.harness.mode === "web"));
|
|
388
|
+
// guards: which guard set the plugin can run. The full set needs PowerShell; without it the plugin falls back to
|
|
389
|
+
// the bash lite harness (intent lock, study lock), so this row WARNs instead of passing silently (QA 2026-11-06).
|
|
390
|
+
if (hk) {
|
|
391
|
+
const psDirs = String(process.env.PATH || "").split(IS_WIN ? ";" : ":");
|
|
392
|
+
const ps = ["pwsh", "pwsh.exe", "powershell.exe", "powershell"].find((n) => psDirs.some((d) => d && existsSync(join(d, n))));
|
|
393
|
+
const lite = existsSync(join(k, "tools", "wkharness-lite.sh"));
|
|
394
|
+
add("guards", !!ps, ps ? "full guard set (" + ps + ")" : lite ? "no PowerShell on PATH: lite guards only (intent lock, study lock); install pwsh 7 for the full set" : "no PowerShell and no lite harness: every tool call is let through", !ps && !lite);
|
|
395
|
+
}
|
|
375
396
|
// skills: the HQ holds on-load and the core prints it
|
|
376
397
|
const hq = (st.skills && st.skills.hq) || hqDir(o, dest);
|
|
377
398
|
if (st.skills && st.skills.skipped) add("skills", false, "skipped (--no-skills)", false, true);
|
|
@@ -399,6 +420,8 @@ function doctor(o) {
|
|
|
399
420
|
const rem = existsSync(join(ph, ".git")) ? gitRemotes(ph) : [];
|
|
400
421
|
const remNote = rem && rem.length ? "; private home " + ph + " HAS remote(s) " + rem.join(",") : "; private home " + ph + " has no remote";
|
|
401
422
|
add("skill-push", login === OWNER_LOGIN && !(rem && rem.length), "= owner-only; a push would use github login " + login + remNote, false);
|
|
423
|
+
const ur = updateRow(PKG.version);
|
|
424
|
+
add("update", !!ur.ok, ur.detail, false, !!ur.skip);
|
|
402
425
|
let bad = 0;
|
|
403
426
|
for (const r of rows) {
|
|
404
427
|
console.log((r.skip ? "SKIP " : r.ok ? "PASS " : r.required ? "FAIL " : "WARN ") + r.name + " " + r.detail);
|
|
@@ -408,13 +431,23 @@ function doctor(o) {
|
|
|
408
431
|
}
|
|
409
432
|
|
|
410
433
|
const o = parseArgs(process.argv.slice(2));
|
|
434
|
+
// install --update (from update) and these flags are forwarded to the re-run install of a fresh version.
|
|
435
|
+
const fwd = process.argv.slice(3).filter((a) => a !== "--auto-update");
|
|
436
|
+
const upd = () => runUpdate({ current: PKG.version, cliPath: process.argv[1], installArgs: fwd, log: console.log });
|
|
437
|
+
if (["install", "doctor", "config"].includes(o.cmd) && !o.update) {
|
|
438
|
+
const nv = quietCheck(PKG.version, console.log);
|
|
439
|
+
if (nv && (o.autoUpdate || process.env.WKAPPBOT_CODE_AUTO_UPDATE === "1")) upd();
|
|
440
|
+
}
|
|
411
441
|
switch (o.cmd) {
|
|
412
442
|
case "--version": case "-v": case "version":
|
|
413
443
|
console.log("wkappbot-code " + PKG.version + " (opencode-ai " + OPENCODE_VERSION + ")"); process.exit(0);
|
|
414
444
|
case "install": process.exit(install(o));
|
|
415
445
|
case "doctor": process.exit(doctor(o));
|
|
446
|
+
case "update": process.exit(upd());
|
|
447
|
+
case "config": installPlugin(o, kihDir(o), kihCheckout(o) ? "kih checkout" : "web harness"); process.exit(0);
|
|
416
448
|
default:
|
|
417
449
|
console.log("usage: wkappbot-code --version | install [--no-npm] [--no-bin] [--no-harness] [--no-skills] [--web-harness]\n" +
|
|
418
|
-
" [--config-dir DIR] [--kih-dir DIR] [--bin-dest FILE] [--hq-dir DIR] | doctor [--config-dir DIR]"
|
|
450
|
+
" [--config-dir DIR] [--kih-dir DIR] [--bin-dest FILE] [--hq-dir DIR] [--update] [--auto-update] | doctor [--config-dir DIR]\n" +
|
|
451
|
+
" | update [install opts] | config [--yes] [--project] [--set key=value] [--config-dir DIR]");
|
|
419
452
|
process.exit(o.cmd === "help" || o.cmd === "--help" ? 0 : 64);
|
|
420
453
|
}
|
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"], { shell: process.platform === "win32", 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"], { shell: process.platform === "win32", 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"], { shell: process.platform === "win32", 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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wkappbot-code",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "wkappbot-code -- Wisdom Kernel: the pinned OpenCode bridge plus the
|
|
3
|
+
"version": "0.3.2",
|
|
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
|
"bin": {
|
|
7
7
|
"wkappbot-code": "bin/wkappbot-code.js",
|
package/plugin/wisdom-kernel.js
CHANGED
|
@@ -17,17 +17,17 @@ import { dirname, join, resolve } from "node:path";
|
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
18
|
|
|
19
19
|
// OpenCode tool id -> Claude tool name the kih guards already know.
|
|
20
|
-
|
|
20
|
+
const TOOL_MAP = {
|
|
21
21
|
bash: "Bash", edit: "Edit", write: "Write", read: "Read",
|
|
22
22
|
glob: "Glob", grep: "Grep", list: "LS", patch: "Edit", multiedit: "MultiEdit",
|
|
23
23
|
webfetch: "WebFetch", websearch: "WebSearch", task: "Agent", todowrite: "TodoWrite",
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
// pre arm budget: a hung hook is killed and the call fails OPEN, like a Claude hook timeout.
|
|
27
|
-
|
|
27
|
+
const PRE_TIMEOUT_MS = Number(process.env.WK_APPBOT_PRE_TIMEOUT_MS || 60000);
|
|
28
28
|
|
|
29
29
|
// OpenCode args (camelCase) -> Claude tool_input (snake_case) for the tools the guards inspect.
|
|
30
|
-
|
|
30
|
+
function toClaudeInput(tool, args) {
|
|
31
31
|
const a = args || {};
|
|
32
32
|
switch (tool) {
|
|
33
33
|
case "bash": {
|
|
@@ -55,12 +55,12 @@ export function toClaudeInput(tool, args) {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
function claudeToolName(tool) {
|
|
59
59
|
return TOOL_MAP[tool] || tool;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
// The Claude-shaped hook payload for one OpenCode tool call.
|
|
63
|
-
|
|
63
|
+
function buildPreToolUsePayload(input, args, cwd) {
|
|
64
64
|
return {
|
|
65
65
|
hook_event_name: "PreToolUse",
|
|
66
66
|
session_id: input.sessionID,
|
|
@@ -72,7 +72,7 @@ export function buildPreToolUsePayload(input, args, cwd) {
|
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
function buildPostToolUsePayload(input, output, cwd) {
|
|
76
76
|
const p = buildPreToolUsePayload(input, input.args, cwd);
|
|
77
77
|
p.hook_event_name = "PostToolUse";
|
|
78
78
|
p.tool_response = { output: output && output.output, title: output && output.title };
|
|
@@ -80,7 +80,7 @@ export function buildPostToolUsePayload(input, output, cwd) {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
// kih root: WK_KIH_DIR, else the checkout this file sits in (wkappbot-code/plugin -> ../..).
|
|
83
|
-
|
|
83
|
+
function resolveKihDir() {
|
|
84
84
|
if (process.env.WK_KIH_DIR) return process.env.WK_KIH_DIR;
|
|
85
85
|
try {
|
|
86
86
|
const up = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
@@ -89,11 +89,36 @@ export function resolveKihDir() {
|
|
|
89
89
|
return "";
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
// Pipe JSON into "wkharness-run.sh
|
|
92
|
+
// Pipe JSON into "<hook> <arm>" (wkharness-run.sh, or wkharness-lite.sh without PowerShell); resolves {code, stdout, stderr}. Never rejects.
|
|
93
93
|
// No hook found = fail-open (code 0, skipped) so a missing kih never bricks OpenCode.
|
|
94
|
-
|
|
94
|
+
// PowerShell on PATH? The full harness (wkharness-run.sh) runs its guards in PowerShell; without one it exits 127,
|
|
95
|
+
// which used to read as "no verdict" and let EVERY call through (QA 2026-11-06, fresh Linux HOME).
|
|
96
|
+
function onPath(names) {
|
|
97
|
+
const dirs = String(process.env.PATH || "").split(process.platform === "win32" ? ";" : ":");
|
|
98
|
+
for (const d of dirs) for (const n of names) { try { if (d && existsSync(join(d, n))) return true; } catch {} }
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
let _ps = null;
|
|
102
|
+
function hasPowerShell() {
|
|
103
|
+
if (process.env.WK_APPBOT_FORCE_LITE === "1") return false;
|
|
104
|
+
if (_ps === null) _ps = onPath(["pwsh", "pwsh.exe", "powershell.exe", "powershell"]);
|
|
105
|
+
return _ps;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Which hook script serves an arm: the full harness when PowerShell is there, else the bash lite harness
|
|
109
|
+
// (tools/wkharness-lite.sh: study lock, intent lock, stop lint). The prompt and session arms exist only in lite.
|
|
110
|
+
function pickHook(arm, kih) {
|
|
111
|
+
if (!kih) return "";
|
|
112
|
+
const full = join(kih, ".claude", "hooks", "wkharness-run.sh"), lite = join(kih, "tools", "wkharness-lite.sh");
|
|
113
|
+
const liteArm = arm === "prompt" || arm === "session";
|
|
114
|
+
if (!liteArm && hasPowerShell() && existsSync(full)) return full;
|
|
115
|
+
if (existsSync(lite)) return lite;
|
|
116
|
+
return liteArm ? "" : full;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function runHook(arm, payload, opts = {}) {
|
|
95
120
|
const kih = opts.kihDir ?? resolveKihDir();
|
|
96
|
-
const hook = opts.hook || process.env.WK_APPBOT_HOOK || (
|
|
121
|
+
const hook = opts.hook || process.env.WK_APPBOT_HOOK || pickHook(arm, kih);
|
|
97
122
|
return new Promise((done) => {
|
|
98
123
|
let settled = false;
|
|
99
124
|
const finish = (r) => { if (!settled) { settled = true; if (timer) clearTimeout(timer); done(r); } };
|
|
@@ -125,7 +150,7 @@ export function runHook(arm, payload, opts = {}) {
|
|
|
125
150
|
}
|
|
126
151
|
|
|
127
152
|
// Claude contract: exit 2 refuses; so does a stdout JSON deny/block verdict.
|
|
128
|
-
|
|
153
|
+
function isRefusal(r) {
|
|
129
154
|
if (r.timedOut || r.skipped) return false;
|
|
130
155
|
if (r.code === 2) return true;
|
|
131
156
|
try {
|
|
@@ -135,7 +160,7 @@ export function isRefusal(r) {
|
|
|
135
160
|
}
|
|
136
161
|
|
|
137
162
|
// A stdout deny JSON names its own reason; stderr then may hold only warn lines (wkharness-run.sh wk_pre_emit).
|
|
138
|
-
|
|
163
|
+
function refusalReason(r) {
|
|
139
164
|
try {
|
|
140
165
|
const j = JSON.parse(r.stdout);
|
|
141
166
|
const why = (j.hookSpecificOutput && j.hookSpecificOutput.permissionDecisionReason) || j.reason;
|
|
@@ -160,9 +185,23 @@ export const WisdomKernel = async (ctx = {}, options = {}) => {
|
|
|
160
185
|
"tool.execute.after": async (input, output) => {
|
|
161
186
|
try { await runHook("post", buildPostToolUsePayload(input || {}, output, cwd), { kihDir: k, timeoutMs: 30000 }); } catch {}
|
|
162
187
|
},
|
|
188
|
+
// UserPromptSubmit: arms the intent lock for the next real tool call (fail-open, never blocks the message).
|
|
189
|
+
"chat.message": async (input, output) => {
|
|
190
|
+
try {
|
|
191
|
+
const parts = (output && output.parts) || [];
|
|
192
|
+
const prompt = parts.filter((p) => p && p.type === "text" && !p.synthetic).map((p) => p.text || "").join("\n");
|
|
193
|
+
if (!prompt) return;
|
|
194
|
+
await runHook("prompt", { hook_event_name: "UserPromptSubmit", session_id: input && input.sessionID, cwd, prompt, agent_family: "opencode" }, { kihDir: k, timeoutMs: 30000 });
|
|
195
|
+
} catch {}
|
|
196
|
+
},
|
|
163
197
|
event: async (arg) => {
|
|
164
198
|
try {
|
|
165
199
|
const event = arg && arg.event;
|
|
200
|
+
if (event && event.type === "session.created") {
|
|
201
|
+
const info = (event.properties && event.properties.info) || {};
|
|
202
|
+
await runHook("session", { hook_event_name: "SessionStart", session_id: info.id, cwd, source: "startup", agent_family: "opencode" }, { kihDir: k, timeoutMs: 30000 });
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
166
205
|
if (!event || event.type !== "session.idle") return;
|
|
167
206
|
const sid = event.properties && event.properties.sessionID;
|
|
168
207
|
await runHook("stop-archive", { hook_event_name: "Stop", session_id: sid, cwd, agent_family: "opencode" }, { kihDir: k, timeoutMs: 30000 });
|
|
@@ -171,4 +210,8 @@ export const WisdomKernel = async (ctx = {}, options = {}) => {
|
|
|
171
210
|
};
|
|
172
211
|
};
|
|
173
212
|
|
|
213
|
+
// Helpers for the selftest ride on the plugin function, never as module exports (OpenCode calls every export).
|
|
214
|
+
WisdomKernel.internals = { TOOL_MAP, PRE_TIMEOUT_MS, toClaudeInput, claudeToolName, buildPreToolUsePayload,
|
|
215
|
+
buildPostToolUsePayload, resolveKihDir, runHook, isRefusal, refusalReason, hasPowerShell, pickHook };
|
|
216
|
+
|
|
174
217
|
export default WisdomKernel;
|