wkappbot-code 0.3.20 → 0.3.23
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 +2 -2
- package/bin/postinstall.js +1 -1
- package/bin/wkappbot-code.js +17 -3
- package/lib/allow-compact.js +0 -1
- package/lib/hook-wire.js +6 -3
- package/lib/python-acquire.js +14 -8
- package/lib/python-pick.js +9 -2
- package/lib/stale-wkskill.js +23 -0
- package/lib/wk-hook.js +10 -10
- package/package.json +1 -1
- package/plugin/wisdom-kernel.js +1 -1
- package/templates/global-CLAUDE.md +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ The plugin `plugin/wisdom-kernel.js` maps OpenCode plugin events onto the harnes
|
|
|
12
12
|
|---|---|---|
|
|
13
13
|
| `tool.execute.before` | `pre` | Claude-shaped PreToolUse JSON (`tool_name`, `tool_input`, `session_id`, `cwd`). Exit 2 throws, which refuses the tool call with the guard's own message as the reason. |
|
|
14
14
|
| `tool.execute.after` | `post` | fail-open |
|
|
15
|
-
| `chat.message` | `prompt` | UserPromptSubmit: the intent lock owes a skill search (`
|
|
15
|
+
| `chat.message` | `prompt` | UserPromptSubmit: the intent lock owes a skill search (`wkappbot skill search ...`) before real work |
|
|
16
16
|
| `event` `session.created` | `session` | SessionStart |
|
|
17
17
|
| `event` `session.idle` | `stop-archive` | archive only, fail-open |
|
|
18
18
|
| `experimental.chat.system.transform` | (none) | appends the reflex rules to the system prompt of EVERY model request, whatever the model (pinned to OpenCode 1.19.5; the e2e asserts every request carried them) |
|
|
@@ -57,7 +57,7 @@ The generated `opencode.json` holds the minimum: `$schema` and the plugin entry
|
|
|
57
57
|
|
|
58
58
|
## Uninstall
|
|
59
59
|
|
|
60
|
-
`install` records, once, the machine state it changes: the global `core.hooksPath`, whether `~/.claude/settings.json`, `~/.wk-harness` and `~/.wk-harness-hooks` existed, and which shims it created (`wkappbot` beside the CLI
|
|
60
|
+
`install` records, once, the machine state it changes: the global `core.hooksPath`, whether `~/.claude/settings.json`, `~/.wk-harness` and `~/.wk-harness-hooks` existed, and which shims it created (`wkappbot` beside the CLI and `wk-verify` in `~/.local/bin`; no `wkskill` command exists, the skill command is `wkappbot skill`). `wkappbot-code uninstall` gives exactly that back: the recorded `core.hooksPath` (only when the current value is a hook folder the install wrote), the harness hook entries leave `settings.json` while yours stay (a file the install created is removed when nothing else is left), the recorded shims go, `~/.wk-harness` and `~/.wk-harness-hooks` go when the install created them, and the plugin entry leaves `opencode.json` (backed up first). It keeps `opencode-ai`, the core and its skills; `npm rm -g wkappbot-code` removes the CLI.
|
|
61
61
|
|
|
62
62
|
## Model swap
|
|
63
63
|
|
package/bin/postinstall.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// CONTRACT: (a) ALWAYS exits 0 and returns within a moment -- the real work runs DETACHED and logs to
|
|
8
8
|
// ~/.wkappbot-code/postinstall.log; (b) the worker runs `wkappbot-code install` (lays the web harness, wires
|
|
9
9
|
// ~/.claude/settings.json through the lite harness -- PreToolUse, Stop, UserPromptSubmit, SessionStart -- the global git
|
|
10
|
-
// hooks, the
|
|
10
|
+
// hooks, the wk-verify shim and the public skills, keeping every user setting), then `wkappbot-code doctor`,
|
|
11
11
|
// then a short self-test (lib/setup-run.js: core --version, skill read on-load, one hook allow, one hook refuse), and
|
|
12
12
|
// writes ~/.wkappbot-code/postinstall.done {version, installRc, doctor:{pass,warn,fail,skip}, selftest:{n,m}, ...};
|
|
13
13
|
// postinstall.running exists while the worker lives; (c) the first `wkappbot-code` run afterwards prints ONE summary
|
package/bin/wkappbot-code.js
CHANGED
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
import { homedir, tmpdir } from "node:os";
|
|
41
41
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
42
42
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
43
|
-
import { quietCheck, runUpdate, updateRow } from "../lib/update.js"; import { rmTreeHard } from "../lib/rmtree.js"; import { plainFileUrl } from "../lib/fileurl.js";
|
|
43
|
+
import { quietCheck, runUpdate, updateRow } from "../lib/update.js"; import { rmTreeHard } from "../lib/rmtree.js"; import { plainFileUrl } from "../lib/fileurl.js"; import { removeStaleWkskill } from "../lib/stale-wkskill.js";
|
|
44
44
|
import { applySets, askEach, buildConfig, configRow, detect, lineDiff, printDetected } from "../lib/config.js";
|
|
45
45
|
import { aiCliRows, installAiClis, removeAiClis } from "../lib/ai-clis.js";
|
|
46
46
|
import { hooksRow, wireClaudeHooks } from "../lib/hook-wire.js";
|
|
@@ -205,6 +205,13 @@ function installBin(o) {
|
|
|
205
205
|
// need no core and still refuse the risky classes, but the install reports the missing core as a FAILED part (0.3.13).
|
|
206
206
|
if (r.status === 3) return { ok: false, degraded: true, dest, note: "core not fetched (release host unreachable); hooks + lite harness run without it -- rerun wkappbot-code install once github.com is reachable" };
|
|
207
207
|
if (r.status !== 0) return { ok: false, dest, note: "wk-bin-acquire rc " + r.status };
|
|
208
|
+
// Core recognizes this marker beside the actual executable; no Core rebuild needed.
|
|
209
|
+
if (IS_WIN) {
|
|
210
|
+
const core = join(target, "wkappbot-core.exe");
|
|
211
|
+
if (!existsSync(core)) return { ok: false, dest, note: "Windows core missing after acquire: " + core };
|
|
212
|
+
try { writeFileSync(join(target, ".wkappbot-code-install"), "wkappbot-code npm install\n"); }
|
|
213
|
+
catch (e) { return { ok: false, dest, note: "npm license marker: " + e.message }; }
|
|
214
|
+
}
|
|
208
215
|
// Put `wkappbot` on PATH beside wkappbot-code itself, unless that name is already taken by someone else.
|
|
209
216
|
let link = "";
|
|
210
217
|
const ld = linkDir();
|
|
@@ -393,7 +400,7 @@ function installPlugin(o, kih, kihSource) {
|
|
|
393
400
|
// The machine state an uninstall must give back, recorded ONCE (a re-install never overwrites the first record, or
|
|
394
401
|
// it would record our own dispatcher as the "prior" hooksPath). Shim paths are those install may create.
|
|
395
402
|
function shimCandidates() {
|
|
396
|
-
return [join(linkDir(), "wkappbot"), join(HARNESS_LINKS(), "
|
|
403
|
+
return [join(linkDir(), "wkappbot"), join(HARNESS_LINKS(), "wk-verify"), join(HARNESS_LINKS(), "wk-verify.cmd")]; // Windows (Git Bash) .cmd twins from harness-install.sh: recorded like the rest, so uninstall removes them, never a user file
|
|
397
404
|
}
|
|
398
405
|
const PRE_SETTINGS = () => join(STATE_HOME(), "settings.pre-install.json");
|
|
399
406
|
function recordPrior(prev) {
|
|
@@ -555,7 +562,7 @@ function uninstall(o) {
|
|
|
555
562
|
}
|
|
556
563
|
// 3b. the ~/.local/bin/wkappbot link: removed ONLY when it points at the core this install placed (the core file stays)
|
|
557
564
|
const core = (st.bin && st.bin.dest) || binDest(o);
|
|
558
|
-
removeStarterMd(st.starterMd, log);
|
|
565
|
+
removeStarterMd(st.starterMd, log); removeStaleWkskill(HARNESS_LINKS(), log); // an older harness install's counterfeit wkskill link (SDK e2e: local-bin=wkskill)
|
|
559
566
|
let linkRemoved = "";
|
|
560
567
|
for (const p of new Set(shimCandidates().concat([join(homedir(), ".local", "bin", "wkappbot")]))) {
|
|
561
568
|
if (!isSymlink(p) || (st.shims || []).includes(p)) continue;
|
|
@@ -571,6 +578,12 @@ function uninstall(o) {
|
|
|
571
578
|
if (body.includes(core)) { rmSync(cmdShim, { force: true }); linkRemoved = cmdShim; log("shim: removed " + cmdShim + " (it called the core this install placed)"); }
|
|
572
579
|
else log("shim: kept " + cmdShim + " (it no longer calls " + core + ")");
|
|
573
580
|
}
|
|
581
|
+
// Undo only our npm license marker; do not alter a marker owned by another installer.
|
|
582
|
+
if (IS_WIN && st.bin?.ok) {
|
|
583
|
+
const marker = join(dirname(core), ".wkappbot-code-install");
|
|
584
|
+
try { if (existsSync(marker) && readFileSync(marker, "utf8") === "wkappbot-code npm install\n") { rmSync(marker); log("bin: removed npm license marker " + marker); } }
|
|
585
|
+
catch (e) { rc = 1; log("bin: could not remove npm license marker (" + e.message + ")"); }
|
|
586
|
+
}
|
|
574
587
|
// 3d. the AI CLIs this install added (lib/ai-clis.js), never one the user had before
|
|
575
588
|
removeAiClis((st.aiClis && st.aiClis.installed) || [], log, process.env, undefined, (st.aiClis && st.aiClis.prefix) || "");
|
|
576
589
|
// 4. the harness dirs we created, the plugin entry and our config folder
|
|
@@ -578,6 +591,7 @@ function uninstall(o) {
|
|
|
578
591
|
const rmTree = (p, what) => {
|
|
579
592
|
const r = rmTreeHard(p); if (r.ok) { if (what) log(what + ": removed " + p); return; }
|
|
580
593
|
rc = 1; log((what || "remove") + ": could not remove " + p + " -- blocked by " + r.blockers.slice(0, 5).join("; ") + " -- delete it by hand");
|
|
594
|
+
if (IS_WIN) { const d = spawnSync("tasklist.exe", ["/v"], { encoding: "utf8", timeout: 15000 }); log("remove diagnostics (process list, not file owners): " + String(d.stdout || d.stderr || "").slice(0, 4000)); }
|
|
581
595
|
};
|
|
582
596
|
if (st.harness && st.harness.mode === "web" && pr.harnessDirExisted !== true && existsSync(HARNESS_DIR())) rmTree(HARNESS_DIR(), "harness");
|
|
583
597
|
if (pr.hooksDirExisted !== true && existsSync(HOOKS_DIR())) rmTree(HOOKS_DIR(), "push-gate");
|
package/lib/allow-compact.js
CHANGED
|
@@ -9,7 +9,6 @@ import { dirname } from "node:path";
|
|
|
9
9
|
import { readSettings } from "./hook-wire.js";
|
|
10
10
|
|
|
11
11
|
export const COMPACT_ALLOWS = [
|
|
12
|
-
"Bash(wkskill *)", // skill read / search / like (the user's own store)
|
|
13
12
|
"Bash(wkappbot skill *)", // the same through the core
|
|
14
13
|
"Bash(wkappbot file read *)", // read-only file view
|
|
15
14
|
"Bash(wk-verify)", // live hook check, no arguments
|
package/lib/hook-wire.js
CHANGED
|
@@ -20,7 +20,10 @@ import { pickPython, stubWarning } from "./python-pick.js";
|
|
|
20
20
|
// for harness hooks and deleted or rewrote them. Now a harness hook is a wkharness*.sh/.ps1/.cmd file name standing at a
|
|
21
21
|
// path or word start, a file under a .wk-harness folder, the kih .claude/hooks scripts, or the two named sync hooks.
|
|
22
22
|
const HARNESS_HOOK = /(^|[\s"'\\/])wkharness(-[\w-]+)?\.(sh|ps1|cmd)\b|\.wk-harness[\\/]|[\\/]\.claude[\\/]hooks[\\/](user-prompt-submit|session-start)\.sh\b|(^|[\s"'\\/])(wk-global-md-sync|wk-session-pull|wkappbot-shim)(\.[a-z0-9]+)?\b/;
|
|
23
|
-
|
|
23
|
+
// The history arms (owner 2026-11-12) push the reflex history like the repo hooks: PostToolUse only for the cross-session
|
|
24
|
+
// tools (a matcher, so no spawn per tool call), PreCompact, SessionEnd and StopFailure (a rate limit).
|
|
25
|
+
const ARMS = [["PreToolUse", "pre"], ["Stop", "stop"], ["UserPromptSubmit", "prompt"], ["SessionStart", "session"],
|
|
26
|
+
["PostToolUse", "post", "mcp__claude-code-remote__send_message|ReadNotifications"], ["PreCompact", "precompact"], ["SessionEnd", "sessionend"], ["StopFailure", "stopfail"]];
|
|
24
27
|
const fwd = (p) => String(p || "").replace(/\\/g, "/").replace(/\/+$/, "");
|
|
25
28
|
|
|
26
29
|
export function readSettings(path) {
|
|
@@ -80,10 +83,10 @@ export function wireSettings(path, lite) {
|
|
|
80
83
|
}
|
|
81
84
|
if (keep.length) out[ev] = keep;
|
|
82
85
|
}
|
|
83
|
-
for (const [ev, arm] of ARMS) {
|
|
86
|
+
for (const [ev, arm, matcher] of ARMS) {
|
|
84
87
|
const c = '"' + lite + '" ' + arm;
|
|
85
88
|
if (!(out[ev] || []).some((g) => (g.hooks || []).some((h) => h.command === c)))
|
|
86
|
-
(out[ev] = out[ev] || []).push({ hooks: [{ type: "command", command: c, timeout: 10 }] });
|
|
89
|
+
(out[ev] = out[ev] || []).push({ ...(matcher ? { matcher } : {}), hooks: [{ type: "command", command: c, timeout: 10 }] });
|
|
87
90
|
}
|
|
88
91
|
d.hooks = out;
|
|
89
92
|
mkdirSync(dirname(path), { recursive: true });
|
package/lib/python-acquire.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// Spec: wkappbot skill read harness-installer-owns-all-setup
|
|
11
11
|
import { spawnSync } from "node:child_process";
|
|
12
12
|
import { createHash } from "node:crypto";
|
|
13
|
-
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
|
|
13
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync } from "node:fs";
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
import { pickPython, stubWarning } from "./python-pick.js";
|
|
@@ -58,13 +58,13 @@ function setUserPath(dir, run, env) {
|
|
|
58
58
|
export function ensurePython(o = {}) {
|
|
59
59
|
const env = o.env || process.env, log = o.log || (() => {}), run = o.run || realRun;
|
|
60
60
|
const platform = o.platform || process.platform, exists = o.exists || existsSync, list = o.list || readdirSync;
|
|
61
|
-
const home = o.home || homedir(), read = o.read || readFileSync;
|
|
61
|
+
const home = o.home || homedir(), read = o.read || readFileSync, EMB = o.embed || EMBED;
|
|
62
62
|
const report = (ok, msg, extra = {}) => { log((ok ? "OK python " : "FAIL python: ") + msg); return { ok, note: msg, ...extra }; };
|
|
63
63
|
try {
|
|
64
64
|
let p = pickPython({ env, run: o.probeRun });
|
|
65
65
|
const warn = stubWarning(p.stubs);
|
|
66
66
|
if (p.cmd) {
|
|
67
|
-
const v = run(p.cmd, [...p.args, "-c", "import sys;print(sys.version.split()[0]);print(sys.executable)"], env
|
|
67
|
+
const v = run(p.cmd, [...p.args, "-c", "import sys;print(sys.version.split()[0]);print(sys.executable)"], env); // no shell: see python-pick.js
|
|
68
68
|
const [ver, exe] = String((v && v.stdout) || "").trim().split(/\r?\n/);
|
|
69
69
|
return report(true, (ver || "3") + " " + (exe || p.cmd), { cmd: p.cmd, args: p.args, via: "present" });
|
|
70
70
|
}
|
|
@@ -81,12 +81,18 @@ export function ensurePython(o = {}) {
|
|
|
81
81
|
if (!dir) {
|
|
82
82
|
const target = join(home, ".wk-harness", "python"), zip = join(home, ".wk-harness", "python-embed.zip");
|
|
83
83
|
mkdirSync(target, { recursive: true });
|
|
84
|
-
const dl = run("curl", ["-fsSL", "--retry", "6", "--retry-delay", "5", "-m", "300", "-o", zip,
|
|
85
|
-
if (!dl || dl.status !== 0) return report(false, "winget gave no Python and the download of " +
|
|
84
|
+
const dl = run("curl", ["-fsSL", "--retry", "6", "--retry-delay", "5", "-m", "300", "-o", zip, EMB.url], env);
|
|
85
|
+
if (!dl || dl.status !== 0) return report(false, "winget gave no Python and the download of " + EMB.url + " failed (curl rc " + (dl ? dl.status : "?") + ")", { via: "none" });
|
|
86
86
|
const sum = createHash("sha256").update(read(zip)).digest("hex");
|
|
87
|
-
if (sum !==
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
if (sum !== EMB.sha256) return report(false, "the embeddable zip sha256 " + sum + " is not the pinned " + EMB.sha256, { via: "none" });
|
|
88
|
+
// Windows: the bsdtar in System32 reads a zip; the GNU tar of Git Bash, first on PATH there, cannot (rc 128, 0.3.20)
|
|
89
|
+
const tarExe = platform === "win32" ? join(env.SystemRoot || env.windir || "C:\\Windows", "System32", "tar.exe") : "tar";
|
|
90
|
+
let ux = run(tarExe, ["-xf", zip, "-C", target], env);
|
|
91
|
+
if ((!ux || ux.status !== 0) && platform === "win32")
|
|
92
|
+
ux = run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", "Expand-Archive -Force -LiteralPath $env:WK_ZIP -DestinationPath $env:WK_DEST"], { ...env, WK_ZIP: zip, WK_DEST: target });
|
|
93
|
+
if (!ux || ux.status !== 0) return report(false, "could not unpack " + zip + " (" + tarExe + " rc " + (ux ? ux.status : "?") + ")", { via: "none" });
|
|
94
|
+
// the zip's pythonXY._pth keeps the script folder off sys.path, so harness scripts could not import their siblings
|
|
95
|
+
try { for (const n of list(target)) if (/^python\d*\._pth$/i.test(n)) (o.unlink || unlinkSync)(join(target, n)); } catch {}
|
|
90
96
|
dir = target; via = "embed";
|
|
91
97
|
}
|
|
92
98
|
// 3. a python3.exe beside python.exe (winget and the zip ship only python.exe, so `python3` would still hit the
|
package/lib/python-pick.js
CHANGED
|
@@ -8,10 +8,17 @@
|
|
|
8
8
|
import { spawnSync } from "node:child_process";
|
|
9
9
|
|
|
10
10
|
export const CANDIDATES = [["python3"], ["python"], ["py", "-3"]];
|
|
11
|
+
// Windows asks python and py -3 first: python.org and tool-cache installs ship python.exe with no python3.exe, so
|
|
12
|
+
// python3 there is usually the WindowsApps stub (spokesman order 2026-11-10).
|
|
13
|
+
export const candidatesFor = (platform) => (platform === "win32" ? [["python"], ["py", "-3"], ["python3"]] : CANDIDATES);
|
|
11
14
|
const PROBE = ["-c", "import sys;print(sys.version_info[0])"];
|
|
12
15
|
const STORE_RE = /Microsoft Store|WindowsApps|app execution alias/i;
|
|
13
16
|
|
|
14
|
-
|
|
17
|
+
// NEVER a shell: on win32 shell:true handed the probe to cmd.exe unquoted, so the ";" and "(" of -c broke it and every
|
|
18
|
+
// REAL Python failed (0.3.20, SDK os-qa 42730232937 win-x64). Node finds python.exe and py.exe on PATH by itself, and
|
|
19
|
+
// a WindowsApps stub still runs and exits 9009.
|
|
20
|
+
export const spawnOpts = (env, timeout = 15000) => ({ encoding: "utf8", env, timeout, shell: false, windowsHide: true });
|
|
21
|
+
const realRun = (cmd, args, env) => spawnSync(cmd, args, spawnOpts(env));
|
|
15
22
|
|
|
16
23
|
// -> { cmd, args, stubs: [names] } ; cmd "" when no working Python 3 exists.
|
|
17
24
|
export function pickPython(o = {}) {
|
|
@@ -20,7 +27,7 @@ export function pickPython(o = {}) {
|
|
|
20
27
|
const r = run(env.PYTHON_CMD, PROBE, env);
|
|
21
28
|
if (r && r.status === 0 && String(r.stdout || "").trim() === "3") return { cmd: env.PYTHON_CMD, args: [], stubs };
|
|
22
29
|
}
|
|
23
|
-
for (const [cmd, ...pre] of
|
|
30
|
+
for (const [cmd, ...pre] of candidatesFor(o.platform || process.platform)) {
|
|
24
31
|
const r = run(cmd, [...pre, ...PROBE], env);
|
|
25
32
|
if (!r) continue;
|
|
26
33
|
const out = String(r.stdout || "") + String(r.stderr || "");
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// stale-wkskill.js -- uninstall also removes the counterfeit wkskill command an OLDER harness install left in the link dir
|
|
2
|
+
// (owner 2026-11-11: the fake wkskill is deleted; spokesman 2026-11-12: SDK e2e "after: local-bin=wkskill", RESULT 20/21 on every leg).
|
|
3
|
+
// A link or .cmd twin is removed ONLY while it still points into a harness (its target or body names wkharness or wk-harness);
|
|
4
|
+
// a wkskill file or link of the user's own stays. Spec: wkappbot skill read harness-installer-owns-all-setup
|
|
5
|
+
import { lstatSync, readFileSync, readlinkSync, rmSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
|
|
8
|
+
const isHarness = (s) => /wkharness|wk-harness/.test(String(s || ""));
|
|
9
|
+
|
|
10
|
+
// -> the paths removed; log gets one line per candidate
|
|
11
|
+
export function removeStaleWkskill(dir, log = () => {}) {
|
|
12
|
+
const removed = [];
|
|
13
|
+
for (const n of ["wkskill", "wkskill.cmd"]) {
|
|
14
|
+
const p = join(dir, n);
|
|
15
|
+
let st;
|
|
16
|
+
try { st = lstatSync(p); } catch { continue; }
|
|
17
|
+
let body = "";
|
|
18
|
+
try { body = st.isSymbolicLink() ? readlinkSync(p) : readFileSync(p, "utf8"); } catch { /* unreadable: kept */ }
|
|
19
|
+
if (isHarness(body)) { rmSync(p, { force: true }); removed.push(p); log("shim: removed " + p + " (the counterfeit wkskill command an older harness install linked)"); }
|
|
20
|
+
else log("shim: kept " + p + " (not a harness link)");
|
|
21
|
+
}
|
|
22
|
+
return removed;
|
|
23
|
+
}
|
package/lib/wk-hook.js
CHANGED
|
@@ -301,16 +301,16 @@ export function isFastRead(tool, input) {
|
|
|
301
301
|
}
|
|
302
302
|
|
|
303
303
|
// SHIM PATH (SDK ROOT C 2026-11-06, run 42289814533): OpenCode's bash tool runs /bin/zsh on macOS, whose default PATH
|
|
304
|
-
// has no ~/.local/bin, so "zsh:1: command not found:
|
|
305
|
-
// that names
|
|
304
|
+
// has no ~/.local/bin, so "zsh:1: command not found: wkappbot" and a Mac user could never pay the intent lock. A bash call
|
|
305
|
+
// that names wkappbot or wk-verify gets the shim dir (WK_LINK_DIR, else ~/.local/bin, where harness install.sh links
|
|
306
306
|
// them) prepended to PATH for that one command; nothing else changes. Returns the command to run (unchanged when the
|
|
307
307
|
// shim is absent or the command names neither shim).
|
|
308
308
|
export function withShimPath(tool, args, linkDir) {
|
|
309
309
|
if (claudeToolName(tool) !== "Bash" || !args || typeof args.command !== "string") return args && args.command;
|
|
310
310
|
const c = args.command;
|
|
311
|
-
if (!/(^|[\s;&|(`])(
|
|
311
|
+
if (!/(^|[\s;&|(`])(wkappbot|wk-verify)(\s|$)/.test(c)) return c;
|
|
312
312
|
const dir = linkDir || process.env.WK_LINK_DIR || join(homedir(), ".local", "bin");
|
|
313
|
-
if (!existsSync(join(dir, "
|
|
313
|
+
if (!existsSync(join(dir, "wkappbot")) && !existsSync(join(dir, "wk-verify"))) return c;
|
|
314
314
|
if (c.startsWith("PATH=")) return c;
|
|
315
315
|
return "PATH=" + JSON.stringify(dir) + ':"$PATH"; export PATH; ' + c;
|
|
316
316
|
}
|
|
@@ -323,10 +323,10 @@ export const RULES_MARK = "[wisdom-kernel rules]";
|
|
|
323
323
|
export const RULES = [
|
|
324
324
|
RULES_MARK + " You run under the Wisdom Kernel harness (wkappbot-code). On every turn:",
|
|
325
325
|
"1. One command per shell call; never chain a write with anything else.",
|
|
326
|
-
"2. A refusal names a skill: run
|
|
326
|
+
"2. A refusal names a skill: run wkappbot skill read <id> and apply its remedy; never route around it.",
|
|
327
327
|
"3. Never use git --no-verify or git push --force, never change core.hooksPath, never rm -r a home directory, / or the project source.",
|
|
328
328
|
"4. Never edit or delete files under .git.",
|
|
329
|
-
"5. After a new user prompt, run
|
|
329
|
+
"5. After a new user prompt, run wkappbot skill search <three or more words> before the first write; read-only tools stay open.",
|
|
330
330
|
].join("\n");
|
|
331
331
|
|
|
332
332
|
// The top step lines of the on-load skill, fetched ONCE per process (one OpenCode server = its sessions) in the
|
|
@@ -337,7 +337,7 @@ export function onLoadLines(kihDir) {
|
|
|
337
337
|
if (_onload) return _onload;
|
|
338
338
|
_onload = new Promise((res) => {
|
|
339
339
|
const k = kihDir || resolveKihDir();
|
|
340
|
-
const cands = [process.env.WK_APPBOT_SKILL_CMD, k && join(k, "bin", "
|
|
340
|
+
const cands = [process.env.WK_APPBOT_SKILL_CMD, k && join(k, "bin", "wkappbot"), "wkappbot"].filter(Boolean);
|
|
341
341
|
let out = "", done = false;
|
|
342
342
|
const finish = () => {
|
|
343
343
|
if (done) return; done = true;
|
|
@@ -347,9 +347,9 @@ export function onLoadLines(kihDir) {
|
|
|
347
347
|
res(lines.slice(0, 3).map((l) => l.slice(0, 240)));
|
|
348
348
|
};
|
|
349
349
|
try {
|
|
350
|
-
const target = cands.find((p) => p === "
|
|
351
|
-
// Windows: a shell-less spawn cannot run a .cmd shim, the bare
|
|
352
|
-
const c = process.platform !== "win32" ? spawn(target, ["read", "on-load"], opt) : /\.exe$/i.test(target) ? spawn(target, ["read", "on-load"], opt) : /\.(cmd|bat)$/i.test(target) || target === "
|
|
350
|
+
const target = cands.find((p) => p === "wkappbot" || existsSync(p)), opt = { stdio: ["ignore", "pipe", "ignore"] };
|
|
351
|
+
// Windows: a shell-less spawn cannot run a .cmd shim, the bare wkappbot name (PATHEXT) or an extensionless sh script
|
|
352
|
+
const c = process.platform !== "win32" ? spawn(target, ["skill", "read", "on-load"], opt) : /\.exe$/i.test(target) ? spawn(target, ["skill", "read", "on-load"], opt) : /\.(cmd|bat)$/i.test(target) || target === "wkappbot" ? spawn(target, ["skill", "read", "on-load"], { ...opt, shell: true }) : spawn("sh", [target.replace(/\\/g, "/"), "skill", "read", "on-load"], opt);
|
|
353
353
|
c.stdout.on("data", (d) => { out += d; if (out.length > 65536) c.kill("SIGKILL"); });
|
|
354
354
|
c.on("error", finish); c.on("close", finish);
|
|
355
355
|
setTimeout(() => { try { c.kill("SIGKILL"); } catch {} finish(); }, 8000).unref();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wkappbot-code",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.23",
|
|
4
4
|
"description": "wkappbot-code -- Wisdom Kernel: the pinned OpenCode bridge plus the reflex harness plugin, the wkappbot core, the web harness and the latest public skills in one install",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
package/plugin/wisdom-kernel.js
CHANGED
|
@@ -51,7 +51,7 @@ export const WisdomKernel = async (ctx = {}, options = {}) => {
|
|
|
51
51
|
(tail ? ": " + tail : "") + ". Fix the harness (wkappbot-code doctor) and retry.");
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
-
// macOS /bin/zsh has no ~/.local/bin on PATH: a
|
|
54
|
+
// macOS /bin/zsh has no ~/.local/bin on PATH: a wkappbot call gets the shim dir for that one command (lib/wk-hook.js).
|
|
55
55
|
try { if (args && typeof args.command === "string") args.command = H.withShimPath(inp.tool, args); } catch {}
|
|
56
56
|
},
|
|
57
57
|
"tool.execute.after": async (input, output) => {
|
|
@@ -5,7 +5,7 @@ change it, install never touches it again, and `wkappbot-code uninstall` leaves
|
|
|
5
5
|
|
|
6
6
|
## How to work
|
|
7
7
|
- Reply in the language the user writes in. Keep replies short and concrete; lead with the result.
|
|
8
|
-
- Before real work, find the skill that governs it: `
|
|
8
|
+
- Before real work, find the skill that governs it: `wkappbot skill search <three or more words>`, then `wkappbot skill read <id>`,
|
|
9
9
|
and follow the road it gives. Search again for any term you are not sure of, instead of guessing.
|
|
10
10
|
- Run, do not guess: prove a change by running its test or command, and report the real output, failures included.
|
|
11
11
|
- One command per tool call. Change only what the task needs, and back up a config file before you edit it.
|