wkappbot-code 0.3.8 → 0.3.10
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/bin/prepack.js +3 -1
- package/bin/wkappbot-code.js +16 -10
- package/lib/fileurl.js +12 -0
- package/lib/rmtree.js +35 -0
- package/lib/winpath.sh +12 -0
- package/lib/wk-bin-acquire.sh +17 -7
- package/lib/wk-hook.js +93 -5
- package/package.json +1 -1
- package/plugin/wisdom-kernel.js +2 -0
package/bin/prepack.js
CHANGED
|
@@ -43,7 +43,9 @@ if (!existsSync(join(stage, "tools", "wkharness-lite.sh"))) die("the baked tree
|
|
|
43
43
|
rmSync(OUT, { recursive: true, force: true });
|
|
44
44
|
mkdirSync(OUT, { recursive: true });
|
|
45
45
|
const tgz = join(OUT, "wk-harness-min.tar.gz");
|
|
46
|
-
|
|
46
|
+
// A RELATIVE archive name, run in OUT: GNU tar on Git for Windows reads "D:/a/..." as host "D" (rsh) and fails with
|
|
47
|
+
// "Cannot connect to D: resolve failed" (SDK windows-latest 2026-11-06); bsdtar and GNU tar both take a relative -f.
|
|
48
|
+
const t = spawnSync("tar", ["--exclude=./.git", "--exclude=./bin/linux-x64", "-czf", "wk-harness-min.tar.gz", "-C", sh(stage), "."], { encoding: "utf8", cwd: OUT });
|
|
47
49
|
if (t.status !== 0) die("tar rc " + t.status + " " + t.stderr);
|
|
48
50
|
const sha = createHash("sha256").update(readFileSync(tgz)).digest("hex");
|
|
49
51
|
writeFileSync(tgz + ".sha256", sha + " wk-harness-min.tar.gz\n");
|
package/bin/wkappbot-code.js
CHANGED
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
import { homedir, tmpdir } from "node:os";
|
|
39
39
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
40
40
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
41
|
-
import { quietCheck, runUpdate, updateRow } from "../lib/update.js";
|
|
41
|
+
import { quietCheck, runUpdate, updateRow } from "../lib/update.js"; import { rmTreeHard } from "../lib/rmtree.js"; import { plainFileUrl } from "../lib/fileurl.js";
|
|
42
42
|
import { applySets, askEach, buildConfig, configRow, detect, lineDiff, printDetected } from "../lib/config.js";
|
|
43
43
|
|
|
44
44
|
const PKG_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -48,8 +48,8 @@ const OWNER_LOGIN = "kiexpert";
|
|
|
48
48
|
// The harness this package carries (harness/, baked by bin/prepack.js at pack time) comes first: it needs no checkout
|
|
49
49
|
// and no network (spokesman 2026-11-06, a Default-environment VM). WK_HARNESS_BASE overrides; the web page serves an unbaked tree.
|
|
50
50
|
const BUNDLED_HARNESS = () => join(PKG_DIR, "harness");
|
|
51
|
-
const HARNESS_BASE = () => (process.env.WK_HARNESS_BASE || (existsSync(join(BUNDLED_HARNESS(), "install.sh")) ? pathToFileURL(BUNDLED_HARNESS()).href
|
|
52
|
-
: "https://kiexpert.github.io/wkappbot-kih/webvm/x64/harness")).replace(/\/+$/, "");
|
|
51
|
+
const HARNESS_BASE = () => plainFileUrl(process.env.WK_HARNESS_BASE || (existsSync(join(BUNDLED_HARNESS(), "install.sh")) ? pathToFileURL(BUNDLED_HARNESS()).href
|
|
52
|
+
: "https://kiexpert.github.io/wkappbot-kih/webvm/x64/harness")).replace(/\/+$/, ""); // plainFileUrl: curl on Windows reads RUNNER%7E1 literally
|
|
53
53
|
const HARNESS_DIR = () => process.env.WK_HARNESS_DIR || join(homedir(), ".wk-harness");
|
|
54
54
|
const HOOKS_DIR = () => join(homedir(), ".wk-harness-hooks"); // tools/wk-global-hooks.sh install default
|
|
55
55
|
const SETTINGS = () => process.env.WK_SETTINGS || join(homedir(), ".claude", "settings.json");
|
|
@@ -189,6 +189,9 @@ function installBin(o) {
|
|
|
189
189
|
log("bin: bash " + script + " acquire --dest " + dest);
|
|
190
190
|
const r = spawnSync("bash", [shPath(script), "acquire", "--dest", shPath(dest)], { encoding: "utf8", env: shEnv() });
|
|
191
191
|
for (const line of ((r.stdout || "") + (r.stderr || "")).split("\n").filter(Boolean)) log(" " + line);
|
|
192
|
+
// rc 3 = the release host could not be reached (a package-registry-only VM): no core, but NOT a failed install --
|
|
193
|
+
// the hooks and the lite harness need no core and still refuse the risky classes. A sha256 mismatch stays rc 1.
|
|
194
|
+
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" };
|
|
192
195
|
if (r.status !== 0) return { ok: false, dest, note: "wk-bin-acquire rc " + r.status };
|
|
193
196
|
// Put `wkappbot` on PATH beside wkappbot-code itself, unless that name is already taken by someone else.
|
|
194
197
|
let link = "";
|
|
@@ -368,7 +371,7 @@ function installPlugin(o, kih, kihSource) {
|
|
|
368
371
|
// The machine state an uninstall must give back, recorded ONCE (a re-install never overwrites the first record, or
|
|
369
372
|
// it would record our own dispatcher as the "prior" hooksPath). Shim paths are those install may create.
|
|
370
373
|
function shimCandidates() {
|
|
371
|
-
return [join(linkDir(), "wkappbot"), join(HARNESS_LINKS(), "wkskill"), join(HARNESS_LINKS(), "wk-verify")];
|
|
374
|
+
return [join(linkDir(), "wkappbot"), join(HARNESS_LINKS(), "wkskill"), join(HARNESS_LINKS(), "wk-verify"), join(HARNESS_LINKS(), "wkskill.cmd"), 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
|
|
372
375
|
}
|
|
373
376
|
const PRE_SETTINGS = () => join(STATE_HOME(), "settings.pre-install.json");
|
|
374
377
|
function recordPrior(prev) {
|
|
@@ -398,7 +401,8 @@ function install(o) {
|
|
|
398
401
|
}
|
|
399
402
|
const dest = binDest(o);
|
|
400
403
|
st.bin = o.bin ? installBin(o) : { skipped: true, dest };
|
|
401
|
-
if (o.bin &&
|
|
404
|
+
if (o.bin && st.bin.degraded) log("bin: WARN -- " + st.bin.note);
|
|
405
|
+
else if (o.bin && !st.bin.ok) { bad++; log("bin: FAILED -- " + st.bin.note); }
|
|
402
406
|
st.harness = o.harness ? installHarness(o) : { skipped: true };
|
|
403
407
|
if (o.harness && !st.harness.ok) { bad++; log("harness: FAILED -- " + st.harness.note); }
|
|
404
408
|
// The plugin entry names the harness the install just chose (a forced web harness wins over a checkout).
|
|
@@ -449,8 +453,10 @@ function uninstall(o) {
|
|
|
449
453
|
if (!st.version) log("no install record at " + statePath(o) + "; removing only what is ours by name");
|
|
450
454
|
// 1. global core.hooksPath back to its recorded value (only when it still points at a dir we wrote)
|
|
451
455
|
const cur = globalHooksPath();
|
|
456
|
+
// REAL paths: macOS /var is a symlink to /private/var, so git and HOME may spell one dir two ways (SDK run 42252316617).
|
|
457
|
+
const real = (x) => { const r = resolve(x.replace(/^~(?=\/|$)/, homedir())); try { return realpathSync.native(r); } catch { return r; } }; // .native expands a Windows 8.3 name (RUNNER~1) that git config spells in the long form
|
|
452
458
|
const oursHp = (p) => !!p && [HOOKS_DIR(), join(HARNESS_DIR(), ".githooks"), st.kihDir && join(st.kihDir, ".githooks")]
|
|
453
|
-
.filter(Boolean).some((x) =>
|
|
459
|
+
.filter(Boolean).some((x) => real(p) === real(x));
|
|
454
460
|
if (cur !== (pr.hooksPath ?? null) && (cur === null || oursHp(cur))) {
|
|
455
461
|
const r = pr.hooksPath ? gitGlobal("core.hooksPath", pr.hooksPath) : gitGlobal("--unset", "core.hooksPath");
|
|
456
462
|
log("hooksPath: " + (cur || "unset") + " -> " + (pr.hooksPath || "unset") + (r.status === 0 ? "" : " (git rc " + r.status + ")"));
|
|
@@ -489,11 +495,10 @@ function uninstall(o) {
|
|
|
489
495
|
if (isSymlink(p) || existsSync(p)) { rmSync(p, { force: true }); log("shim: removed " + p); }
|
|
490
496
|
}
|
|
491
497
|
// 4. the harness dirs we created, the plugin entry and our config folder
|
|
492
|
-
// Windows
|
|
493
|
-
// still fails, names the path and goes on with the rest instead of aborting the uninstall half way.
|
|
498
|
+
// Windows frees a closed handle late (ENOTEMPTY): lib/rmtree.js retries ~6 s, then a per-entry pass names the blocker; a failure goes on.
|
|
494
499
|
const rmTree = (p, what) => {
|
|
495
|
-
|
|
496
|
-
|
|
500
|
+
const r = rmTreeHard(p); if (r.ok) { if (what) log(what + ": removed " + p); return; }
|
|
501
|
+
rc = 1; log((what || "remove") + ": could not remove " + p + " -- blocked by " + r.blockers.slice(0, 5).join("; ") + " -- delete it by hand");
|
|
497
502
|
};
|
|
498
503
|
if (st.harness && st.harness.mode === "web" && pr.harnessDirExisted !== true && existsSync(HARNESS_DIR())) rmTree(HARNESS_DIR(), "harness");
|
|
499
504
|
if (pr.hooksDirExisted !== true && existsSync(HOOKS_DIR())) rmTree(HOOKS_DIR(), "push-gate");
|
|
@@ -549,6 +554,7 @@ function doctor(o) {
|
|
|
549
554
|
const dest = o.binDest ? resolve(o.binDest) : (st.bin && st.bin.dest) || binDest(o);
|
|
550
555
|
let core = "";
|
|
551
556
|
if (st.bin && st.bin.skipped) add("bin", false, "skipped (--no-bin)", false, true);
|
|
557
|
+
else if (st.bin && st.bin.degraded && !existsSync(dest)) add("bin", false, dest + " absent -- " + st.bin.note, false);
|
|
552
558
|
else {
|
|
553
559
|
const r = spawnSync(dest, ["--version"], { encoding: "utf8", timeout: 60000 });
|
|
554
560
|
core = r.status === 0 ? (r.stdout || "").trim().split("\n")[0] : "";
|
package/lib/fileurl.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// fileurl.js -- a file:// base that curl can open on every platform.
|
|
2
|
+
// pathToFileURL percent-encodes "~" ("RUNNER~1" -> "RUNNER%7E1") and Windows curl does not decode a file:// path, so a
|
|
3
|
+
// Windows profile with a short name, a space or a non-ASCII letter failed to read the harness. plainFileUrl hands curl the plain path.
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
export function plainFileUrl(u) {
|
|
7
|
+
if (!/^file:\/\//i.test(u)) return u;
|
|
8
|
+
let p;
|
|
9
|
+
try { p = fileURLToPath(u); } catch { return u; }
|
|
10
|
+
p = p.replace(/\\/g, "/");
|
|
11
|
+
return "file://" + (p.startsWith("/") ? "" : "/") + p;
|
|
12
|
+
}
|
package/lib/rmtree.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// rmtree.js -- delete a tree on a platform that releases a just-closed handle late (Windows: EBUSY/EPERM/ENOTEMPTY).
|
|
2
|
+
// rmTreeHard(p, rm) -> { ok, blockers[] }: a long bounded retry first, then a per-entry pass that names what stays.
|
|
3
|
+
import { existsSync, lstatSync, readdirSync, rmSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
const sleepMs = (ms) => { try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); } catch { /* no wait available */ } };
|
|
7
|
+
|
|
8
|
+
// rm is injectable so the selftest can fake a held handle (default: fs.rmSync with Node's own retry)
|
|
9
|
+
export function rmTreeHard(p, rm = (q, o) => rmSync(q, o), opts = {}) {
|
|
10
|
+
const tries = opts.tries || 6, gap = opts.gap || 1000;
|
|
11
|
+
const once = (q) => rm(q, { recursive: true, force: true, maxRetries: 10, retryDelay: 300 });
|
|
12
|
+
let last = null;
|
|
13
|
+
for (let i = 0; i < tries; i++) {
|
|
14
|
+
try { once(p); if (!existsSync(p)) return { ok: true, blockers: [] }; last = new Error("ENOTEMPTY"); }
|
|
15
|
+
catch (e) { last = e; }
|
|
16
|
+
sleepMs(gap);
|
|
17
|
+
}
|
|
18
|
+
// per-entry pass: remove what can go and name every entry (and nested file) that cannot
|
|
19
|
+
const blockers = [];
|
|
20
|
+
const walk = (d) => {
|
|
21
|
+
let names = [];
|
|
22
|
+
try { names = readdirSync(d); } catch (e) { blockers.push(d + " (" + (e.code || e.message) + ")"); return; }
|
|
23
|
+
for (const n of names) {
|
|
24
|
+
const q = join(d, n);
|
|
25
|
+
let dir = false; try { dir = lstatSync(q).isDirectory(); } catch { /* vanished */ }
|
|
26
|
+
if (dir) walk(q);
|
|
27
|
+
try { rm(q, { recursive: true, force: true }); } catch (e) { blockers.push(q + " (" + (e.code || e.message) + ")"); }
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
if (existsSync(p)) walk(p);
|
|
31
|
+
try { rm(p, { recursive: true, force: true }); } catch (e) { if (existsSync(p)) blockers.push(p + " (" + (e.code || e.message) + ")"); }
|
|
32
|
+
if (!existsSync(p)) return { ok: true, blockers: [] };
|
|
33
|
+
if (!blockers.length) blockers.push(p + " (" + ((last && (last.code || last.message)) || "not removed") + ")");
|
|
34
|
+
return { ok: false, blockers };
|
|
35
|
+
}
|
package/lib/winpath.sh
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# winpath.sh -- sourced by selftest-opencode-e2e.sh and selftest-win-paths.sh. wk_winpath PATH prints the form a NATIVE
|
|
3
|
+
# Windows program takes: `cygpath -m` (C:/x/y) when cygpath is on PATH, else PATH unchanged. A failed conversion also
|
|
4
|
+
# leaves the path as it was. bash 3.2 safe (macOS /bin/bash).
|
|
5
|
+
wk_winpath() {
|
|
6
|
+
local w
|
|
7
|
+
if command -v cygpath >/dev/null 2>&1; then
|
|
8
|
+
w="$(cygpath -m "$1" 2>/dev/null)"
|
|
9
|
+
if [ -n "$w" ]; then printf '%s\n' "$w"; return 0; fi
|
|
10
|
+
fi
|
|
11
|
+
printf '%s\n' "$1"
|
|
12
|
+
}
|
package/lib/wk-bin-acquire.sh
CHANGED
|
@@ -100,18 +100,20 @@ default_dest_win() {
|
|
|
100
100
|
fi
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
# sdk_fetch PLAT DIR -- prints the verified asset path
|
|
103
|
+
# sdk_fetch PLAT DIR -- prints the verified asset path; else prints nothing and returns
|
|
104
|
+
# 3 when a download failed (host unreachable, no such asset) or 4 when the asset is UNVERIFIED
|
|
105
|
+
# (no SHA256SUMS line for it, or a sha256 mismatch). Only 3 may degrade an install to hooks-only.
|
|
104
106
|
sdk_fetch() {
|
|
105
107
|
command -v wk_stopwatch_lap >/dev/null 2>&1 && wk_stopwatch_lap "sdk_fetch" || true
|
|
106
108
|
local plat="$1" dir="$2" asset base want got
|
|
107
109
|
asset="wkappbot-$plat"
|
|
108
110
|
base="${WK_BIN_BASE:-https://github.com/kiexpert/wkappbot-sdk/releases/latest/download}"
|
|
109
|
-
curl -fsSL -m 120 "$base/$asset" -o "$dir/$asset" || return
|
|
110
|
-
curl -fsSL -m 120 "$base/SHA256SUMS" -o "$dir/SHA256SUMS" || return
|
|
111
|
+
curl -fsSL -m "${WK_BIN_TIMEOUT:-120}" "$base/$asset" -o "$dir/$asset" || return 3
|
|
112
|
+
curl -fsSL -m "${WK_BIN_TIMEOUT:-120}" "$base/SHA256SUMS" -o "$dir/SHA256SUMS" || return 3
|
|
111
113
|
want="$(awk -v f="$asset" '{ n = $2; sub(/^\*/, "", n); if (n == f) { print $1; exit } }' "$dir/SHA256SUMS")"
|
|
112
|
-
[ -n "$want" ] || return
|
|
114
|
+
[ -n "$want" ] || return 4
|
|
113
115
|
got="$(sha_of "$dir/$asset" || true)"
|
|
114
|
-
[ "$got" = "$want" ] || return
|
|
116
|
+
[ "$got" = "$want" ] || return 4
|
|
115
117
|
echo "$dir/$asset"
|
|
116
118
|
}
|
|
117
119
|
|
|
@@ -147,12 +149,20 @@ install_file() {
|
|
|
147
149
|
|
|
148
150
|
acquire_unix() {
|
|
149
151
|
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
|
|
152
|
+
local dest="$1" plat="$2" offline="$3" src="" label frc=3
|
|
151
153
|
[ -n "$dest" ] || dest="$(default_dest_unix)"
|
|
152
|
-
if [ "$offline" = 0 ]
|
|
154
|
+
if [ "$offline" = 0 ]; then src="$(sdk_fetch "$plat" "$WK_TMP")" && frc=0 || frc=$?; fi
|
|
155
|
+
if [ "$frc" = 0 ]; then
|
|
153
156
|
label="SDK release wkappbot-$plat"
|
|
154
157
|
else
|
|
155
158
|
if [ "$offline" = 0 ]; then note "SDK asset unavailable or unverified, using repo bin/$plat"; fi
|
|
159
|
+
# EXIT 3 = NOTHING TO FETCH FROM, never a security refusal: the release host is unreachable (a
|
|
160
|
+
# package-registry-only VM) and no checkout holds a repo copy. The caller may then install hooks
|
|
161
|
+
# and the lite harness without the core. An UNVERIFIED asset (frc 4) still dies rc 1 below.
|
|
162
|
+
if [ -z "$REPO" ] && [ "$frc" = 3 ]; then
|
|
163
|
+
note "UNREACHABLE: no wkappbot-$plat could be downloaded from ${WK_BIN_BASE:-github.com/kiexpert/wkappbot-sdk releases} and no kih checkout holds bin/$plat; nothing installed"
|
|
164
|
+
exit 3
|
|
165
|
+
fi
|
|
156
166
|
src="$(repo_file "$plat/wkappbot")"
|
|
157
167
|
label="repo bin/$plat/wkappbot"
|
|
158
168
|
fi
|
package/lib/wk-hook.js
CHANGED
|
@@ -164,13 +164,84 @@ export function refusalReason(r) {
|
|
|
164
164
|
// a call in one of these classes is REFUSED instead of allowed: a public package must never let rm -rf HOME through
|
|
165
165
|
// because pwsh happened to be missing. Returns the class label, or "".
|
|
166
166
|
const HOME_T = /^(~|\$HOME|\$\{HOME\}|\/root|\/home\/[^/]+|\/Users\/[^/]+)\/?(\*|\.\*)?$/;
|
|
167
|
+
// The shell line as the risk floor reads it. A heredoc body (after <<'TAG', <<TAG or <<-TAG, up to the line TAG) is
|
|
168
|
+
// dropped, and a quoted string is ONE word with its quotes removed, so its text is never read as a git flag or a path.
|
|
169
|
+
// Words handed to a shell (sh/bash -c, eval) are also returned in seg.inner and are checked again as a command line.
|
|
170
|
+
const SHELL_NAME_RE = /^(sh|bash|zsh|dash|ksh)$/;
|
|
171
|
+
export function shellSegments(cmd) {
|
|
172
|
+
const s = String(cmd || "");
|
|
173
|
+
const segs = [];
|
|
174
|
+
let seg = { words: [], inner: [], redirect: false };
|
|
175
|
+
let word = null, quote = "", pending = [], i = 0;
|
|
176
|
+
const add = (ch) => { word = (word === null ? "" : word) + ch; };
|
|
177
|
+
const endWord = () => {
|
|
178
|
+
if (word === null) return;
|
|
179
|
+
const head = seg.words[0] || "";
|
|
180
|
+
const prev = seg.words[seg.words.length - 1];
|
|
181
|
+
if ((prev === "-c" && SHELL_NAME_RE.test(head)) || head === "eval") seg.inner.push(word);
|
|
182
|
+
seg.words.push(word);
|
|
183
|
+
word = null;
|
|
184
|
+
};
|
|
185
|
+
const endSeg = () => {
|
|
186
|
+
endWord();
|
|
187
|
+
if (seg.words.length || seg.redirect) segs.push(seg);
|
|
188
|
+
seg = { words: [], inner: [], redirect: false };
|
|
189
|
+
};
|
|
190
|
+
while (i < s.length) {
|
|
191
|
+
const ch = s[i];
|
|
192
|
+
if (quote === "'") { if (ch === "'") quote = ""; else add(ch); i++; continue; }
|
|
193
|
+
if (quote === '"') {
|
|
194
|
+
if (ch === '"') quote = "";
|
|
195
|
+
else if (ch === "\\" && i + 1 < s.length && /["\\$`]/.test(s[i + 1])) { add(s[i + 1]); i++; }
|
|
196
|
+
else add(ch);
|
|
197
|
+
i++; continue;
|
|
198
|
+
}
|
|
199
|
+
if (ch === "'" || ch === '"') { quote = ch; if (word === null) word = ""; i++; continue; }
|
|
200
|
+
if (ch === "\\" && i + 1 < s.length) { add(s[i + 1]); i += 2; continue; }
|
|
201
|
+
if (ch === "<" && s[i + 1] === "<" && s[i + 2] !== "<") {
|
|
202
|
+
endWord();
|
|
203
|
+
let j = i + 2;
|
|
204
|
+
const dash = s[j] === "-";
|
|
205
|
+
if (dash) j++;
|
|
206
|
+
while (s[j] === " " || s[j] === "\t") j++;
|
|
207
|
+
let tag = "";
|
|
208
|
+
if (s[j] === "'" || s[j] === '"') { const q = s[j]; j++; while (j < s.length && s[j] !== q && s[j] !== "\n") tag += s[j++]; j++; }
|
|
209
|
+
else while (j < s.length && !/[\s;&|<>()'"]/.test(s[j])) tag += s[j++];
|
|
210
|
+
if (tag) pending.push({ tag, dash });
|
|
211
|
+
i = j; continue;
|
|
212
|
+
}
|
|
213
|
+
if (ch === "\n") {
|
|
214
|
+
endSeg(); i++;
|
|
215
|
+
for (const h of pending) {
|
|
216
|
+
for (;;) {
|
|
217
|
+
if (i >= s.length) break;
|
|
218
|
+
let e = s.indexOf("\n", i);
|
|
219
|
+
if (e < 0) e = s.length;
|
|
220
|
+
const line = s.slice(i, e).replace(/\r$/, "");
|
|
221
|
+
i = e + 1;
|
|
222
|
+
if ((h.dash ? line.replace(/^\t+/, "") : line) === h.tag) break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
pending = [];
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (/\s/.test(ch)) { endWord(); i++; continue; }
|
|
229
|
+
if (ch === ";" || ch === "|" || ch === "&" || ch === "(" || ch === ")") { endSeg(); i++; continue; }
|
|
230
|
+
if (ch === ">") { endWord(); seg.redirect = true; i++; continue; }
|
|
231
|
+
if (ch === "<") { endWord(); i++; continue; }
|
|
232
|
+
add(ch); i++;
|
|
233
|
+
}
|
|
234
|
+
endSeg();
|
|
235
|
+
return segs;
|
|
236
|
+
}
|
|
237
|
+
|
|
167
238
|
export function riskClass(tool, input, cwd) {
|
|
168
239
|
const t = claudeToolName(tool);
|
|
169
240
|
const i = input || {};
|
|
170
241
|
if (t === "Bash") {
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
const
|
|
242
|
+
for (const seg of shellSegments(i.command)) {
|
|
243
|
+
const w = seg.words;
|
|
244
|
+
for (const inner of seg.inner) { const r = riskClass(tool, { command: inner }, cwd); if (r) return r; }
|
|
174
245
|
const gi = w.indexOf("git");
|
|
175
246
|
if (gi >= 0) {
|
|
176
247
|
const g = w.slice(gi + 1);
|
|
@@ -190,7 +261,7 @@ export function riskClass(tool, input, cwd) {
|
|
|
190
261
|
if (tg.some((x) => /(^|\/)\.git(\/|$)/.test(x))) return "write under .git";
|
|
191
262
|
}
|
|
192
263
|
}
|
|
193
|
-
if (/(
|
|
264
|
+
if (w.some((x) => /(^|\/)\.git(\/|$)/.test(x)) && (seg.redirect || /^(rm|mv|cp|tee|truncate|dd|ln|chmod|sed)$/.test(w[0] || ""))) return "write under .git";
|
|
194
265
|
}
|
|
195
266
|
return "";
|
|
196
267
|
}
|
|
@@ -219,6 +290,21 @@ export function isFastRead(tool, input) {
|
|
|
219
290
|
return true;
|
|
220
291
|
}
|
|
221
292
|
|
|
293
|
+
// SHIM PATH (SDK ROOT C 2026-11-06, run 42289814533): OpenCode's bash tool runs /bin/zsh on macOS, whose default PATH
|
|
294
|
+
// has no ~/.local/bin, so "zsh:1: command not found: wkskill" and a Mac user could never pay the intent lock. A bash call
|
|
295
|
+
// that names wkskill or wk-verify gets the shim dir (WK_LINK_DIR, else ~/.local/bin, where harness install.sh links
|
|
296
|
+
// them) prepended to PATH for that one command; nothing else changes. Returns the command to run (unchanged when the
|
|
297
|
+
// shim is absent or the command names neither shim).
|
|
298
|
+
export function withShimPath(tool, args, linkDir) {
|
|
299
|
+
if (claudeToolName(tool) !== "Bash" || !args || typeof args.command !== "string") return args && args.command;
|
|
300
|
+
const c = args.command;
|
|
301
|
+
if (!/(^|[\s;&|(`])(wkskill|wk-verify)(\s|$)/.test(c)) return c;
|
|
302
|
+
const dir = linkDir || process.env.WK_LINK_DIR || join(homedir(), ".local", "bin");
|
|
303
|
+
if (!existsSync(join(dir, "wkskill")) && !existsSync(join(dir, "wk-verify"))) return c;
|
|
304
|
+
if (c.startsWith("PATH=")) return c;
|
|
305
|
+
return "PATH=" + JSON.stringify(dir) + ':"$PATH"; export PATH; ' + c;
|
|
306
|
+
}
|
|
307
|
+
|
|
222
308
|
// PER-CALL INJECTION (owner 2026-11-06: "so we must force our settings onto every model call OpenCode makes").
|
|
223
309
|
// experimental.chat.system.transform appends RULES to the system array of EVERY model request, so a model that never
|
|
224
310
|
// saw kih (Qwen, a fresh provider) still gets the reflex body. Short, fixed, no secrets. RULES_MARK makes the append
|
|
@@ -251,7 +337,9 @@ export function onLoadLines(kihDir) {
|
|
|
251
337
|
res(lines.slice(0, 3).map((l) => l.slice(0, 240)));
|
|
252
338
|
};
|
|
253
339
|
try {
|
|
254
|
-
const
|
|
340
|
+
const target = cands.find((p) => p === "wkskill" || existsSync(p)), opt = { stdio: ["ignore", "pipe", "ignore"] };
|
|
341
|
+
// Windows: a shell-less spawn cannot run a .cmd shim, the bare wkskill name (PATHEXT) or an extensionless sh script
|
|
342
|
+
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 === "wkskill" ? spawn(target, ["read", "on-load"], { ...opt, shell: true }) : spawn("sh", [target.replace(/\\/g, "/"), "read", "on-load"], opt);
|
|
255
343
|
c.stdout.on("data", (d) => { out += d; if (out.length > 65536) c.kill("SIGKILL"); });
|
|
256
344
|
c.on("error", finish); c.on("close", finish);
|
|
257
345
|
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.10",
|
|
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,6 +51,8 @@ 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 wkskill call gets the shim dir for that one command (lib/wk-hook.js).
|
|
55
|
+
try { if (args && typeof args.command === "string") args.command = H.withShimPath(inp.tool, args); } catch {}
|
|
54
56
|
},
|
|
55
57
|
"tool.execute.after": async (input, output) => {
|
|
56
58
|
try {
|