wkappbot-code 0.3.12 → 0.3.13
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 +3 -0
- package/bin/prepack.js +8 -0
- package/bin/wkappbot-code.js +65 -11
- package/lib/ai-clis.js +68 -0
- package/lib/allow-compact.js +55 -0
- package/lib/hook-wire.js +173 -0
- package/lib/prereq.js +57 -0
- package/lib/setup-run.js +6 -3
- package/lib/starter-md.js +46 -0
- package/lib/wk-bin-acquire.sh +32 -13
- package/lib/wk-hook.js +8 -4
- package/package.json +2 -1
- package/templates/global-CLAUDE.md +20 -0
package/README.md
CHANGED
|
@@ -41,6 +41,7 @@ Other roads to the CLI: `npm i -g https://kiexpert.github.io/wkappbot-kih/wkappb
|
|
|
41
41
|
| part | what `install` lays down | skip with |
|
|
42
42
|
|---|---|---|
|
|
43
43
|
| opencode | pinned `opencode-ai@1.19.5` (`npm -g`) | `--no-npm` |
|
|
44
|
+
| ai-clis | the well-known AI coding CLIs `claude` (`@anthropic-ai/claude-code`), `codex` (`@openai/codex`), `gemini` (`@google/gemini-cli`) and `qwen` (`@qwen-code/qwen-code`), in one `npm -g` pass; no key or login is asked (each CLI asks for its own sign-in on first run), and a CLI that fails to install is a WARN, never a failed install | `--no-ai-clis` or `--no-npm` |
|
|
44
45
|
| plugin | `plugin/wisdom-kernel.js` + `lib/wk-hook.js` plus the `opencode.json` plugin entry | -- |
|
|
45
46
|
| bin | the `wkappbot` core for this platform from the SDK latest release asset `wkappbot-<platform>`, accepted only when it matches its `SHA256SUMS` line (`lib/wk-bin-acquire.sh`); placed at `~/.local/share/wkappbot/wkappbot` and linked as `wkappbot` beside `wkappbot-code` | `--no-bin` |
|
|
46
47
|
| harness | when no harness checkout is found, the scrubbed web harness to `~/.wk-harness` through its own published `install.sh` (sha256-checked); `--web-harness` forces it | `--no-harness` |
|
|
@@ -48,6 +49,8 @@ Other roads to the CLI: `npm i -g https://kiexpert.github.io/wkappbot-kih/wkappb
|
|
|
48
49
|
|
|
49
50
|
`install` prints whether `pwsh` was found and which harness will give the verdict. The harness install ends with an advisory line, `[wk-verify] WARN n/6 advisory, not a failure -- c2 ...; c5 ...; c6 ...`, naming each check that missed and why; in a good web install c2 (wkhippo), c5 (core skill read before the core is linked) and c6 (no harness git checkout) are expected to miss. The verdict file is `~/.wkappbot-code/verify.json`.
|
|
50
51
|
|
|
52
|
+
`npm install wkappbot-code` sets your global git `core.hooksPath` to `~/.wk-harness-hooks` (the push-owner gate, which then runs the repo's own hooks); `wkappbot-code uninstall` puts the previous value back, and leaves it alone if you changed it since.
|
|
53
|
+
|
|
51
54
|
The skills are a **read-only copy**: the HQ has no git remote, and a skill you edit yourself lands in the private home `~/.wkappbot/private-skills`, a local git repo with no remote. Doctor's `skill-push = owner-only` row names the GitHub login a push would use.
|
|
52
55
|
|
|
53
56
|
The generated `opencode.json` holds the minimum: `$schema` and the plugin entry (with `kihDir`, the harness the plugin calls). No provider and no permission block is added, because no refusal depends on one (every refusal comes from the plugin and the harness). An existing `opencode.json` is backed up (`opencode.json.bak-<stamp>`) and only the plugin entry is merged in; every other key you set stays. A config that is not plain JSON is left untouched, and `wkappbot-code.json` is written beside it for you to merge by hand. `wkappbot-code config` detects the provider and model from the API-key variables that are set (values are never read) and lets you edit them. Pass `--kih-dir` (or set `WK_KIH_DIR`) when a harness checkout is not auto-found (`~/wkappbot-kih` is searched). Relocation and mock overrides: `WK_BIN_BASE`, `WK_HARNESS_BASE`, `WK_HARNESS_DIR`, `WK_BIN_DEST`, `WK_APPBOT_HQ`, `WK_LINK_DIR`, `WK_PRIVATE_SKILLS`, `WK_SETTINGS`.
|
package/bin/prepack.js
CHANGED
|
@@ -39,6 +39,14 @@ if (!stage) {
|
|
|
39
39
|
if (r.status !== 0) die("harness bake rc " + r.status + "\n" + String(r.stderr).slice(-800));
|
|
40
40
|
}
|
|
41
41
|
if (!existsSync(join(stage, "tools", "wkharness-lite.sh"))) die("the baked tree " + stage + " has no tools/wkharness-lite.sh");
|
|
42
|
+
// The study-lock rule table, built here once (python3 on the build machine) and shipped as tools/wkharness-lite-rules.tsv,
|
|
43
|
+
// so a user machine with no python3 still gets the study lock (owner 2026-11-09: the web install failed without python).
|
|
44
|
+
{
|
|
45
|
+
const w = spawnSync("sh", [sh(join(stage, "tools", "wkharness-lite.sh")), "--wire"], { encoding: "utf8" });
|
|
46
|
+
const built = join(stage, ".tmp", "hook-lite", "rules.tsv");
|
|
47
|
+
if (w.status === 0 && existsSync(built)) { copyFileSync(built, join(stage, "tools", "wkharness-lite-rules.tsv")); rmSync(join(stage, ".tmp"), { recursive: true, force: true }); }
|
|
48
|
+
else die("study-lock rule table not built (needs python3 on the BUILD machine): rc " + w.status + " " + String(w.stderr || "").slice(-300));
|
|
49
|
+
}
|
|
42
50
|
|
|
43
51
|
rmSync(OUT, { recursive: true, force: true });
|
|
44
52
|
mkdirSync(OUT, { recursive: true });
|
package/bin/wkappbot-code.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// wkappbot-code --version package version and the pinned OpenCode release
|
|
4
4
|
// wkappbot-code install [opts] ONE COMMAND lays down every part (idempotent):
|
|
5
5
|
// opencode pinned opencode-ai (npm -g) skip: --no-npm
|
|
6
|
+
// ai-clis the famous AI CLIs claude, codex, gemini, qwen (npm -g, login-free; lib/ai-clis.js); a failure
|
|
7
|
+
// is a WARN, never a failed part skip: --no-ai-clis or --no-npm
|
|
6
8
|
// plugin plugin/wisdom-kernel.js + lib/wk-hook.js + the opencode.json plugin entry (always)
|
|
7
9
|
// bin the wkappbot core for this platform, from the SDK latest release asset verified by
|
|
8
10
|
// SHA256SUMS -- the SAME road as tools/wk-bin-acquire.sh of the harness repo, shipped
|
|
@@ -40,6 +42,11 @@ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
|
40
42
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
41
43
|
import { quietCheck, runUpdate, updateRow } from "../lib/update.js"; import { rmTreeHard } from "../lib/rmtree.js"; import { plainFileUrl } from "../lib/fileurl.js";
|
|
42
44
|
import { applySets, askEach, buildConfig, configRow, detect, lineDiff, printDetected } from "../lib/config.js";
|
|
45
|
+
import { aiCliRows, installAiClis, removeAiClis } from "../lib/ai-clis.js";
|
|
46
|
+
import { hooksRow, wireClaudeHooks } from "../lib/hook-wire.js";
|
|
47
|
+
import { installStarterMd, removeStarterMd } from "../lib/starter-md.js";
|
|
48
|
+
import { addAllows, removeAllows } from "../lib/allow-compact.js";
|
|
49
|
+
import { ensurePrereqs } from "../lib/prereq.js";
|
|
43
50
|
import { files as setupFiles, launchOpenCode, printBanner } from "../lib/setup-run.js";
|
|
44
51
|
|
|
45
52
|
const PKG_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -74,11 +81,13 @@ const tarArgs = (tgz, dir) => {
|
|
|
74
81
|
};
|
|
75
82
|
|
|
76
83
|
function parseArgs(argv) {
|
|
77
|
-
const o = { cmd: argv[0] || "help", npm: true, bin: true, harness: true, skills: true, webHarness: false, devSeat: false,
|
|
84
|
+
const o = { cmd: argv[0] || "help", npm: true, aiClis: true, allows: true, bin: true, harness: true, skills: true, webHarness: false, devSeat: false,
|
|
78
85
|
configDir: "", kihDir: "", binDest: "", hqDir: "", update: false, autoUpdate: false, yes: false, project: false, sets: [] };
|
|
79
86
|
for (let i = 1; i < argv.length; i++) {
|
|
80
87
|
const a = argv[i];
|
|
81
88
|
if (a === "--no-npm") o.npm = false;
|
|
89
|
+
else if (a === "--no-ai-clis") o.aiClis = false;
|
|
90
|
+
else if (a === "--no-allows") o.allows = false;
|
|
82
91
|
else if (a === "--no-bin") o.bin = false;
|
|
83
92
|
else if (a === "--no-harness") o.harness = false;
|
|
84
93
|
else if (a === "--no-skills") o.skills = false;
|
|
@@ -182,13 +191,14 @@ function installBin(o) {
|
|
|
182
191
|
const dest = binDest(o);
|
|
183
192
|
const plat = platformId();
|
|
184
193
|
if (!plat) return { ok: false, dest, note: "no wkappbot build for " + process.platform + "-" + process.arch };
|
|
185
|
-
if (plat === "win-x64") return { ok: false, dest, note: "win-x64 has no single SDK asset; run install.cmd from a harness checkout" };
|
|
186
194
|
// A checkout runs its own copy (same bytes, plus the MANIFEST-verified repo bin/ fallback); the package
|
|
187
|
-
// copy outside a checkout takes only the SDK release asset verified by SHA256SUMS.
|
|
195
|
+
// copy outside a checkout takes only the SDK release asset verified by SHA256SUMS. win-x64 (2026-11-09: no checkout
|
|
196
|
+
// meant NO core at all) takes the release pair wkappbot.exe + wkappbot-core.exe.xz into the DIRECTORY of dest.
|
|
188
197
|
const co = kihCheckout(o);
|
|
189
198
|
const script = co && existsSync(join(co, "tools", "wk-bin-acquire.sh")) ? join(co, "tools", "wk-bin-acquire.sh") : join(PKG_DIR, "lib", "wk-bin-acquire.sh");
|
|
190
|
-
|
|
191
|
-
|
|
199
|
+
const target = plat === "win-x64" ? dirname(dest) : dest;
|
|
200
|
+
log("bin: bash " + script + " acquire --dest " + target);
|
|
201
|
+
const r = spawnSync("bash", [shPath(script), "acquire", "--dest", shPath(target)], { encoding: "utf8", env: shEnv() });
|
|
192
202
|
for (const line of ((r.stdout || "") + (r.stderr || "")).split("\n").filter(Boolean)) log(" " + line);
|
|
193
203
|
// rc 3 = the release host could not be reached (a package-registry-only VM): no core, but NOT a failed install --
|
|
194
204
|
// the hooks and the lite harness need no core and still refuse the risky classes. A sha256 mismatch stays rc 1.
|
|
@@ -197,6 +207,16 @@ function installBin(o) {
|
|
|
197
207
|
// Put `wkappbot` on PATH beside wkappbot-code itself, unless that name is already taken by someone else.
|
|
198
208
|
let link = "";
|
|
199
209
|
const ld = linkDir();
|
|
210
|
+
if (IS_WIN) {
|
|
211
|
+
// Windows: a symlink needs developer mode, so the PATH entry is a .cmd shim beside wkappbot-code.cmd.
|
|
212
|
+
const cp = join(ld, "wkappbot.cmd");
|
|
213
|
+
try {
|
|
214
|
+
const mine = existsSync(cp) && readFileSync(cp, "utf8").includes(dest);
|
|
215
|
+
if (existsSync(cp) && !mine) log("bin: kept existing " + cp + " (not ours); call " + dest + " directly");
|
|
216
|
+
else { writeFileSync(cp, "@echo off\r\n\"" + dest + "\" %*\r\n"); link = cp; log("bin: wrote " + cp + " -> " + dest); }
|
|
217
|
+
} catch (e) { log("bin: could not write " + cp + " (" + e.message + ")"); }
|
|
218
|
+
return { ok: true, dest, link };
|
|
219
|
+
}
|
|
200
220
|
const lp = join(ld, "wkappbot");
|
|
201
221
|
try {
|
|
202
222
|
const taken = existsSync(lp) || isSymlink(lp);
|
|
@@ -401,6 +421,10 @@ function install(o) {
|
|
|
401
421
|
const prev = readState(o);
|
|
402
422
|
const st = { version: PKG.version, at: new Date().toISOString(), prior: recordPrior(prev) };
|
|
403
423
|
let bad = 0;
|
|
424
|
+
// 0.3.13 (owner 2026-11-09: Git Bash is a required tool too): bash, sh, curl, tar and git first; on Windows a Git that
|
|
425
|
+
// is not on PATH is used from its folder, and a missing one is installed with winget (lib/prereq.js)
|
|
426
|
+
st.prereq = ensurePrereqs({ log });
|
|
427
|
+
if (!st.prereq.ok) bad++;
|
|
404
428
|
if (o.update) log("install --update: refreshing the core, the harness and the public skills");
|
|
405
429
|
const ps = pwshPath();
|
|
406
430
|
log("pwsh: " + (ps || "not found") + " -- the FULL harness (PowerShell 7) runs only on a dev seat (a harness checkout, or --dev-seat);" +
|
|
@@ -416,15 +440,26 @@ function install(o) {
|
|
|
416
440
|
catch (e) { bad++; log("opencode: npm install FAILED (" + String(e.message).split("\n")[0] + ") -- run: npm install -g opencode-ai@" + OPENCODE_VERSION); }
|
|
417
441
|
}
|
|
418
442
|
}
|
|
443
|
+
// 0.3.13 (owner 2026-11-08): the famous AI CLIs come along, so a user who knows nothing about LLMs still has them.
|
|
444
|
+
st.aiClis = (o.npm && o.aiClis) ? installAiClis(log, process.env, undefined, (prev.aiClis && prev.aiClis.installed) || [])
|
|
445
|
+
: ((prev.aiClis && (prev.aiClis.installed || []).length) ? prev.aiClis : { skipped: true }); // a skip still owns what an earlier install added
|
|
419
446
|
const dest = binDest(o);
|
|
420
447
|
st.bin = o.bin ? installBin(o) : { skipped: true, dest };
|
|
421
|
-
|
|
422
|
-
|
|
448
|
+
// 0.3.13 (owner 2026-11-09: "check that a missing core is handled as a failure"): NO core is a failed part on every
|
|
449
|
+
// road, an unreachable release host included. The hooks and the lite harness still install, so the risky classes are
|
|
450
|
+
// refused, but the install exits 1 and the banner says so until a rerun fetches the core.
|
|
451
|
+
if (o.bin && !st.bin.ok) { bad++; log("bin: FAILED -- no wkappbot core installed: " + st.bin.note); }
|
|
423
452
|
st.harness = o.harness ? installHarness(o) : { skipped: true };
|
|
424
453
|
if (o.harness && !st.harness.ok) { bad++; log("harness: FAILED -- " + st.harness.note); }
|
|
425
454
|
// The plugin entry names the harness the install just chose (a forced web harness wins over a checkout).
|
|
426
455
|
const kih = (st.harness.mode === "web" && st.harness.ok) ? st.harness.dir : kihDir(o);
|
|
427
|
-
//
|
|
456
|
+
// 0.3.13 (owner 2026-11-09): read the Claude settings first, then point every harness hook at the SAME harness the
|
|
457
|
+
// plugin uses, on both roads (web harness or checkout); the user's own hooks stay; bad JSON is reported, not touched.
|
|
458
|
+
st.hooks = o.harness ? wireClaudeHooks({ settings: SETTINGS(), kih, pwsh: !!ps, log }) : { skipped: true };
|
|
459
|
+
// the compressed allow list (lib/allow-compact.js): five harness-own prefixes, never the wide rows learned on the owner's seats
|
|
460
|
+
st.allows = (o.harness && o.allows) ? addAllows(SETTINGS(), prev.allows || {}, log) : (prev.allows || { skipped: true });
|
|
461
|
+
// the standard global rules for a fresh machine (lib/starter-md.js): only where the user has no ~/.claude/CLAUDE.md
|
|
462
|
+
st.starterMd = o.harness ? installStarterMd({ claudeDir: dirname(SETTINGS()), pkgDir: PKG_DIR, prev: prev.starterMd, log }) : { skipped: true }; // Global pre-push dispatcher (owner 2026-11-06): the push-owner gate runs in every repo, then the repo's own hooks.
|
|
428
463
|
// A user's own absolute global core.hooksPath is kept (the script WARNs). Skipped with --no-harness.
|
|
429
464
|
st.pushGate = { skipped: true };
|
|
430
465
|
if (o.harness && kih && existsSync(join(kih, "tools", "wk-global-hooks.sh"))) {
|
|
@@ -478,8 +513,9 @@ function uninstall(o) {
|
|
|
478
513
|
const r = pr.hooksPath ? gitGlobal("core.hooksPath", pr.hooksPath) : gitGlobal("--unset", "core.hooksPath");
|
|
479
514
|
log("hooksPath: " + (cur || "unset") + " -> " + (pr.hooksPath || "unset") + (r.status === 0 ? "" : " (git rc " + r.status + ")"));
|
|
480
515
|
} else log("hooksPath: " + (cur || "unset") + (cur === (pr.hooksPath ?? null) ? " (as before install)" : " (set by you, kept)"));
|
|
481
|
-
// 2. settings.json: our arms out, yours kept
|
|
516
|
+
// 2. settings.json: our allow rules and arms out, yours kept
|
|
482
517
|
const sp = SETTINGS();
|
|
518
|
+
removeAllows(sp, st.allows, log);
|
|
483
519
|
if (existsSync(sp)) {
|
|
484
520
|
try {
|
|
485
521
|
const raw = readFileSync(sp, "utf8");
|
|
@@ -513,6 +549,7 @@ function uninstall(o) {
|
|
|
513
549
|
}
|
|
514
550
|
// 3b. the ~/.local/bin/wkappbot link: removed ONLY when it points at the core this install placed (the core file stays)
|
|
515
551
|
const core = (st.bin && st.bin.dest) || binDest(o);
|
|
552
|
+
removeStarterMd(st.starterMd, log);
|
|
516
553
|
let linkRemoved = "";
|
|
517
554
|
for (const p of new Set(shimCandidates().concat([join(homedir(), ".local", "bin", "wkappbot")]))) {
|
|
518
555
|
if (!isSymlink(p) || (st.shims || []).includes(p)) continue;
|
|
@@ -520,6 +557,16 @@ function uninstall(o) {
|
|
|
520
557
|
if (tgt && real(tgt) === real(core)) { rmSync(p, { force: true }); linkRemoved = p; log("shim: removed " + p + " (a link to the core this install placed, " + core + ")"); }
|
|
521
558
|
else log("shim: kept " + p + " (not a link to " + core + ")");
|
|
522
559
|
}
|
|
560
|
+
// 3c. the Windows wkappbot.cmd the bin step wrote is a FILE, not a link, so the loop above never saw it (SDK os-qa
|
|
561
|
+
// 2026-11-09 win-x64: local-bin kept wkappbot.cmd after uninstall). Removed only while it still calls this core.
|
|
562
|
+
const cmdShim = st.bin && st.bin.link;
|
|
563
|
+
if (cmdShim && /\.cmd$/i.test(cmdShim) && existsSync(cmdShim)) {
|
|
564
|
+
let body = ""; try { body = readFileSync(cmdShim, "utf8"); } catch {}
|
|
565
|
+
if (body.includes(core)) { rmSync(cmdShim, { force: true }); linkRemoved = cmdShim; log("shim: removed " + cmdShim + " (it called the core this install placed)"); }
|
|
566
|
+
else log("shim: kept " + cmdShim + " (it no longer calls " + core + ")");
|
|
567
|
+
}
|
|
568
|
+
// 3d. the AI CLIs this install added (lib/ai-clis.js), never one the user had before
|
|
569
|
+
removeAiClis((st.aiClis && st.aiClis.installed) || [], log);
|
|
523
570
|
// 4. the harness dirs we created, the plugin entry and our config folder
|
|
524
571
|
// 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.
|
|
525
572
|
const rmTree = (p, what) => {
|
|
@@ -567,6 +614,9 @@ function doctor(o) {
|
|
|
567
614
|
const st = readState(o);
|
|
568
615
|
const rows = [];
|
|
569
616
|
const add = (name, ok, detail, required, skip) => rows.push({ name, ok, detail, required, skip });
|
|
617
|
+
// prereq: the tools every part runs on (check only: doctor never installs; a Git off PATH is still found)
|
|
618
|
+
const pq = ensurePrereqs({ noInstall: true });
|
|
619
|
+
add("prereq", pq.ok, pq.ok ? Object.entries(pq.found).map(([t, p]) => t + "=" + p).join(" ") : pq.note, true);
|
|
570
620
|
const v = opencodeVersion();
|
|
571
621
|
add("opencode", v === OPENCODE_VERSION, v ? v + (v === OPENCODE_VERSION ? "" : " (pinned " + OPENCODE_VERSION + ")") : "not on PATH", false);
|
|
572
622
|
const pdest = join(cfg, "wkappbot-code", "wisdom-kernel.js");
|
|
@@ -576,11 +626,12 @@ function doctor(o) {
|
|
|
576
626
|
add("config", !!entry, join(cfg, "opencode.json"), true);
|
|
577
627
|
const oc = configRow(join(cfg, "opencode.json"));
|
|
578
628
|
add("opencode-config", oc.ok, oc.detail, false);
|
|
629
|
+
aiCliRows(add, !!(st.aiClis && st.aiClis.skipped));
|
|
579
630
|
// bin: the core answers --version
|
|
580
631
|
const dest = o.binDest ? resolve(o.binDest) : (st.bin && st.bin.dest) || binDest(o);
|
|
581
632
|
let core = "";
|
|
582
633
|
if (st.bin && st.bin.skipped) add("bin", false, "skipped (--no-bin)", false, true);
|
|
583
|
-
else if (
|
|
634
|
+
else if (!existsSync(dest)) add("bin", false, dest + " absent -- no wkappbot core" + (st.bin && st.bin.note ? " (" + st.bin.note + ")" : "") + "; rerun: wkappbot-code install", true);
|
|
584
635
|
else {
|
|
585
636
|
const r = spawnSync(dest, ["--version"], { encoding: "utf8", timeout: 60000 });
|
|
586
637
|
core = r.status === 0 ? (r.stdout || "").trim().split("\n")[0] : "";
|
|
@@ -595,6 +646,9 @@ function doctor(o) {
|
|
|
595
646
|
if (st.harness && st.harness.skipped && !hk) add("kih-hook", false, "skipped (--no-harness)", false, true);
|
|
596
647
|
else if (hk && !mode) add("kih-hook", false, k + ": pwsh is missing and the lite harness (tools/wkharness-lite.sh) is not there -- no hook verdict; reinstall the harness", true);
|
|
597
648
|
else add("kih-hook", hk, k ? k + (st.harness && st.harness.mode ? " (" + st.harness.mode + ")" : "") + (mode ? " verdict: " + (mode === "full" ? "full pwsh hub (" + ps + ") after lite" : "lite sh harness" + (ps ? " (user install)" : " (no pwsh)")) : "") : "not found", !!(st.harness && st.harness.mode === "web"));
|
|
649
|
+
// claude-hooks: the Claude Code hooks run the same harness as the plugin (a WARN, never a FAIL: Claude Code is optional).
|
|
650
|
+
if (st.hooks && st.hooks.skipped) add("claude-hooks", false, "skipped (--no-harness)", false, true);
|
|
651
|
+
else if (k) { const hr = hooksRow(SETTINGS(), k); add("claude-hooks", hr.ok, hr.detail, false); }
|
|
598
652
|
// skills: the HQ holds on-load and the core prints it
|
|
599
653
|
const hq = (st.skills && st.skills.hq) || hqDir(o, dest);
|
|
600
654
|
if (st.skills && st.skills.skipped) add("skills", false, "skipped (--no-skills)", false, true);
|
|
@@ -655,7 +709,7 @@ switch (o.cmd) {
|
|
|
655
709
|
case "update": process.exit(upd());
|
|
656
710
|
case "config": installPlugin(o, kihDir(o), kihCheckout(o) ? "harness checkout" : "web harness"); process.exit(0);
|
|
657
711
|
default:
|
|
658
|
-
console.log("usage: wkappbot-code --version | install [--no-npm] [--no-bin] [--no-harness] [--no-skills] [--web-harness] [--dev-seat]\n" +
|
|
712
|
+
console.log("usage: wkappbot-code --version | install [--no-npm] [--no-ai-clis] [--no-allows] [--no-bin] [--no-harness] [--no-skills] [--web-harness] [--dev-seat]\n" +
|
|
659
713
|
" [--config-dir DIR] [--kih-dir DIR] [--bin-dest FILE] [--hq-dir DIR] [--update] [--auto-update] | doctor [--config-dir DIR]\n" +
|
|
660
714
|
" | uninstall [--config-dir DIR] | update [install opts] | config [--yes] [--project] [--set key=value] [--config-dir DIR]");
|
|
661
715
|
process.exit(o.cmd === "help" || o.cmd === "--help" ? 0 : 64);
|
package/lib/ai-clis.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// ai-clis.js -- the well-known AI coding CLIs that `wkappbot-code install` lays down beside OpenCode (owner 2026-11-08:
|
|
2
|
+
// "install the famous CLIs in one go, so a user who knows nothing about LLMs still gets them"). No key or login is asked:
|
|
3
|
+
// each CLI installs login-free and asks for its own sign-in the first time the user runs it. One `npm install -g` pass for
|
|
4
|
+
// all the missing ones; when that pass fails, each is retried alone so one bad package never blocks the rest. A failure is
|
|
5
|
+
// a WARN (never a failed install part), and doctor prints one WARN-level row per CLI. Skip with --no-ai-clis (or --no-npm).
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
|
|
8
|
+
export const AI_CLIS = [
|
|
9
|
+
{ name: "claude", pkg: "@anthropic-ai/claude-code" },
|
|
10
|
+
{ name: "codex", pkg: "@openai/codex" },
|
|
11
|
+
{ name: "gemini", pkg: "@google/gemini-cli" },
|
|
12
|
+
{ name: "qwen", pkg: "@qwen-code/qwen-code" },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const IS_WIN = process.platform === "win32";
|
|
16
|
+
|
|
17
|
+
// The first line a CLI prints for --version, or "" when it is not on PATH or does not answer.
|
|
18
|
+
export function cliVersion(name, env = process.env) {
|
|
19
|
+
const r = spawnSync(name, ["--version"], { encoding: "utf8", timeout: 30000, shell: IS_WIN, env });
|
|
20
|
+
if (r.status !== 0) return "";
|
|
21
|
+
const line = String(r.stdout || "").trim().split("\n")[0] || "";
|
|
22
|
+
const m = line.match(/\d+\.\d+\.\d+[\w.-]*/);
|
|
23
|
+
return m ? m[0] : line.slice(0, 40);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function npmGlobal(pkgs, env) {
|
|
27
|
+
const r = spawnSync("npm", ["install", "-g", ...pkgs, "--no-audit", "--no-fund"], { stdio: "inherit", shell: IS_WIN, env });
|
|
28
|
+
return r.status === 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Installs every CLI of AI_CLIS that is not on PATH yet. Returns { installed: [...], present: [...], failed: [...] }.
|
|
32
|
+
// prevInstalled: the names an earlier install added; a rerun keeps owning them, so uninstall still takes them back.
|
|
33
|
+
export function installAiClis(log, env = process.env, list = AI_CLIS, prevInstalled = []) {
|
|
34
|
+
const present = [], missing = [];
|
|
35
|
+
for (const c of list) (cliVersion(c.name, env) ? present : missing).push(c);
|
|
36
|
+
if (present.length) log("ai-clis: already installed: " + present.map((c) => c.name).join(", "));
|
|
37
|
+
const res = { installed: present.map((c) => c.name).filter((n) => prevInstalled.includes(n)), present: present.map((c) => c.name), failed: [] };
|
|
38
|
+
if (!missing.length) return res;
|
|
39
|
+
log("ai-clis: npm install -g " + missing.map((c) => c.pkg).join(" "));
|
|
40
|
+
if (npmGlobal(missing.map((c) => c.pkg), env)) res.installed.push(...missing.map((c) => c.name));
|
|
41
|
+
else {
|
|
42
|
+
log("ai-clis: the one-pass install failed, retrying each CLI alone");
|
|
43
|
+
for (const c of missing) (npmGlobal([c.pkg], env) ? res.installed : res.failed).push(c.name);
|
|
44
|
+
}
|
|
45
|
+
for (const n of res.failed) log("ai-clis: WARN " + n + " not installed -- run: npm install -g " + list.find((c) => c.name === n).pkg);
|
|
46
|
+
return res;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// uninstall takes back what install added (SDK os-qa 2026-11-09, run 42552244938: the npm prefix bin kept claude, codex,
|
|
50
|
+
// gemini and qwen after uninstall on every leg). Only the names install recorded go; a CLI the user had stays. A failed
|
|
51
|
+
// npm uninstall is a WARN with the command to finish it by hand.
|
|
52
|
+
export function removeAiClis(names, log, env = process.env, list = AI_CLIS) {
|
|
53
|
+
const pkgs = list.filter((c) => (names || []).includes(c.name)).map((c) => c.pkg);
|
|
54
|
+
if (!pkgs.length) return true;
|
|
55
|
+
log("ai-clis: npm uninstall -g " + pkgs.join(" "));
|
|
56
|
+
const r = spawnSync("npm", ["uninstall", "-g", ...pkgs, "--no-audit", "--no-fund"], { stdio: "inherit", shell: IS_WIN, env });
|
|
57
|
+
if (r.status !== 0) log("ai-clis: WARN npm uninstall rc " + r.status + " -- run: npm uninstall -g " + pkgs.join(" "));
|
|
58
|
+
return r.status === 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// doctor rows: add(name, ok, detail, required, skip). Never required: a missing CLI is a WARN.
|
|
62
|
+
export function aiCliRows(add, skipped, env = process.env, list = AI_CLIS) {
|
|
63
|
+
for (const c of list) {
|
|
64
|
+
if (skipped) { add("ai-cli " + c.name, false, "skipped (--no-ai-clis)", false, true); continue; }
|
|
65
|
+
const v = cliVersion(c.name, env);
|
|
66
|
+
add("ai-cli " + c.name, !!v, v ? v + " (" + c.pkg + ")" : "not on PATH -- npm install -g " + c.pkg, false);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// lib/allow-compact.js -- the compressed allow list (owner 2026-11-09: "simplify the patterns, keep 3 to 5 keywords,
|
|
2
|
+
// make a compressed list"). The auto-mode classifier refused harmless harness-own commands on the owner's seats; the
|
|
3
|
+
// rows learned from those refusals (tools/wkharness-claude-allows-learned.txt: git -C *, python3 -I *, send_message ...)
|
|
4
|
+
// are far too wide for a stranger's machine. A fresh install gets ONLY these five read-or-own-store prefixes. Every call
|
|
5
|
+
// still meets the harness PreToolUse hook first, and a hook refusal beats an allow rule, so an entry here skips the
|
|
6
|
+
// permission prompt, never the harness. Uninstall removes exactly the entries this install added.
|
|
7
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { dirname } from "node:path";
|
|
9
|
+
import { readSettings } from "./hook-wire.js";
|
|
10
|
+
|
|
11
|
+
export const COMPACT_ALLOWS = [
|
|
12
|
+
"Bash(wkskill *)", // skill read / search / like (the user's own store)
|
|
13
|
+
"Bash(wkappbot skill *)", // the same through the core
|
|
14
|
+
"Bash(wkappbot file read *)", // read-only file view
|
|
15
|
+
"Bash(wk-verify)", // live hook check, no arguments
|
|
16
|
+
"Bash(wkappbot-code doctor)", // read-only health table
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
// prev: the state recorded by the last install ({ added, madeAllow, madePerms }); a reinstall keeps owning what it added.
|
|
20
|
+
export function addAllows(path, prev = {}, log = () => {}) {
|
|
21
|
+
const r = readSettings(path);
|
|
22
|
+
if (!r.ok) { log("allows: WARN " + path + " is not plain JSON (" + r.err + "); no allow rule added"); return { ok: false, added: prev.added || [] }; }
|
|
23
|
+
const d = r.json || {};
|
|
24
|
+
const madePerms = !!prev.madePerms || !(d.permissions && typeof d.permissions === "object" && !Array.isArray(d.permissions));
|
|
25
|
+
if (madePerms && !(d.permissions && typeof d.permissions === "object" && !Array.isArray(d.permissions))) d.permissions = {};
|
|
26
|
+
const madeAllow = !!prev.madeAllow || !Array.isArray(d.permissions.allow);
|
|
27
|
+
if (!Array.isArray(d.permissions.allow)) d.permissions.allow = [];
|
|
28
|
+
const allow = d.permissions.allow;
|
|
29
|
+
const kept = (prev.added || []).filter((e) => allow.includes(e));
|
|
30
|
+
const fresh = COMPACT_ALLOWS.filter((e) => !allow.includes(e));
|
|
31
|
+
allow.push(...fresh);
|
|
32
|
+
if (fresh.length || !existsSync(path)) { mkdirSync(dirname(path), { recursive: true }); writeFileSync(path, JSON.stringify(d, null, 2) + "\n"); }
|
|
33
|
+
const added = [...new Set([...kept, ...fresh])];
|
|
34
|
+
log("allows: " + (fresh.length ? "added " + fresh.length : "kept") + " of the " + COMPACT_ALLOWS.length + " harness allow rules in " + path + " (the harness hook still checks every call)");
|
|
35
|
+
return { ok: true, added, madeAllow, madePerms };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Removes only the entries addAllows recorded; drops an allow list or permissions object only when the install made it
|
|
39
|
+
// and it is empty now. Returns the count removed; bad JSON is left untouched.
|
|
40
|
+
export function removeAllows(path, st, log = () => {}) {
|
|
41
|
+
if (!st || !(st.added || []).length || !existsSync(path)) return 0;
|
|
42
|
+
const r = readSettings(path);
|
|
43
|
+
if (!r.ok) { log("allows: " + path + " is not plain JSON, left untouched (" + r.err + ")"); return 0; }
|
|
44
|
+
const d = r.json || {}, p = d.permissions;
|
|
45
|
+
if (!p || !Array.isArray(p.allow)) return 0;
|
|
46
|
+
const before = p.allow.length;
|
|
47
|
+
p.allow = p.allow.filter((e) => !st.added.includes(e));
|
|
48
|
+
const n = before - p.allow.length;
|
|
49
|
+
if (!n) return 0;
|
|
50
|
+
if (st.madeAllow && !p.allow.length) delete p.allow;
|
|
51
|
+
if (st.madePerms && !Object.keys(p).length) delete d.permissions;
|
|
52
|
+
writeFileSync(path, JSON.stringify(d, null, 2) + "\n");
|
|
53
|
+
log("allows: removed " + n + " harness allow rule" + (n === 1 ? "" : "s") + " from " + path);
|
|
54
|
+
return n;
|
|
55
|
+
}
|
package/lib/hook-wire.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// hook-wire.js -- read the user's Claude settings first, then bring the harness hooks to the harness this install chose
|
|
2
|
+
// (owner 2026-11-09: "analyze the customer environment, overwrite the needed settings and hooks with the latest, and ask
|
|
3
|
+
// for a change when needed"; "it must work without the harness repo, pick the road by whether the repo is there, with
|
|
4
|
+
// the most consistency"; "the web install failed where python is missing"). Install-mode QA before this module, 23/26:
|
|
5
|
+
// a checkout install wired NO Claude hooks, a switch from the web harness to a checkout left the Claude hooks on the old
|
|
6
|
+
// harness while the OpenCode plugin moved, a settings.json that is not plain JSON was skipped without a word, and with no
|
|
7
|
+
// python3 the web harness install stopped at "needs python3 on PATH".
|
|
8
|
+
// wireSettings is the node port of tools/wkharness-lite.sh --wire (python3, kept for the WebVM image build): every
|
|
9
|
+
// harness hook is repointed at <dir>/tools/wkharness-lite.sh, an unroutable harness hook is dropped, a user's own hook
|
|
10
|
+
// stays verbatim, and every lite arm is present once. The web installer calls it through the CLI form below when it has
|
|
11
|
+
// node but no python3: node hook-wire.js wire SETTINGS LITE
|
|
12
|
+
import { spawnSync } from "node:child_process";
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { dirname, join, resolve } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
|
|
17
|
+
// The same harness-hook pattern as tools/wkharness-lite.sh --wire HARNESS_HOOK. ANCHORED (2026-11-09 review, proven on
|
|
18
|
+
// fixtures): the loose form also took a user's own pwsh lint.ps1, bash ~/bin/session-start.sh and my-wkharness-notes.sh
|
|
19
|
+
// for harness hooks and deleted or rewrote them. Now a harness hook is a wkharness*.sh/.ps1/.cmd file name standing at a
|
|
20
|
+
// path or word start, a file under a .wk-harness folder, the kih .claude/hooks scripts, or the two named sync hooks.
|
|
21
|
+
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/;
|
|
22
|
+
const ARMS = [["PreToolUse", "pre"], ["Stop", "stop"], ["UserPromptSubmit", "prompt"], ["SessionStart", "session"]];
|
|
23
|
+
const fwd = (p) => String(p || "").replace(/\\/g, "/").replace(/\/+$/, "");
|
|
24
|
+
|
|
25
|
+
export function readSettings(path) {
|
|
26
|
+
if (!existsSync(path)) return { ok: true, json: null };
|
|
27
|
+
// a UTF-8 BOM (Windows editors write one) is valid JSON text; strip it (2026-11-09 review: it read as "not JSON")
|
|
28
|
+
const text = readFileSync(path, "utf8").replace(/^/, "");
|
|
29
|
+
if (!text.trim()) return { ok: true, json: null };
|
|
30
|
+
try { return { ok: true, json: JSON.parse(text) }; }
|
|
31
|
+
catch (e) { return { ok: false, err: String(e.message).split("\n")[0] }; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// An event's groups as an array: a single group object (hand-written settings) counts as one group, anything else as none
|
|
35
|
+
// (2026-11-09 review: a non-array value threw "(groups || []) is not iterable" and crashed the whole install).
|
|
36
|
+
const asGroups = (v) => (Array.isArray(v) ? v : v && typeof v === "object" ? [v] : []);
|
|
37
|
+
const asHooks = (g) => (g && Array.isArray(g.hooks) ? g.hooks : []);
|
|
38
|
+
|
|
39
|
+
// Every harness hook command, by event.
|
|
40
|
+
export function harnessHooks(json) {
|
|
41
|
+
const out = [];
|
|
42
|
+
const hk = json && typeof json.hooks === "object" && json.hooks ? json.hooks : {};
|
|
43
|
+
for (const [ev, groups] of Object.entries(hk))
|
|
44
|
+
for (const g of asGroups(groups)) for (const h of asHooks(g))
|
|
45
|
+
if (h && typeof h.command === "string" && HARNESS_HOOK.test(h.command)) out.push({ ev, command: h.command });
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// --wire route(): a harness hook command -> the lite command for the same arm, or null (not routable).
|
|
50
|
+
function route(cmd, lite) {
|
|
51
|
+
let m = /wkharness-lite\.sh"?\s+([a-z]+)/.exec(cmd);
|
|
52
|
+
if (m) return '"' + lite + '" ' + m[1];
|
|
53
|
+
m = /wkharness-run\.sh"?\s+([a-z-]+)/.exec(cmd);
|
|
54
|
+
if (m) return '"' + lite + '" ' + (m[1].startsWith("stop") ? "stop" : m[1]);
|
|
55
|
+
if (cmd.includes("user-prompt-submit.sh")) return '"' + lite + '" prompt';
|
|
56
|
+
if (cmd.includes("session-start.sh")) return '"' + lite + '" session';
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// The node port of --wire, plus the create step of the web installer. Throws on a settings file that is not JSON.
|
|
61
|
+
export function wireSettings(path, lite) {
|
|
62
|
+
const r = readSettings(path);
|
|
63
|
+
if (!r.ok) throw new Error(path + " is not plain JSON (" + r.err + ")");
|
|
64
|
+
const d = r.json || {};
|
|
65
|
+
const out = {};
|
|
66
|
+
for (const [ev, groups] of Object.entries(d.hooks && typeof d.hooks === "object" ? d.hooks : {})) {
|
|
67
|
+
const keep = [];
|
|
68
|
+
for (const g of asGroups(groups)) {
|
|
69
|
+
// dedupe INSIDE a group only (2026-11-09 review): a per-event set emptied the second matcher group (Edit|Write)
|
|
70
|
+
// when it held the same harness command as the first (Bash), so PreToolUse stopped firing for edits
|
|
71
|
+
const seen = new Set(), hs = [];
|
|
72
|
+
for (const h of asHooks(g)) {
|
|
73
|
+
const cmd = String((h && h.command) || "");
|
|
74
|
+
const c = HARNESS_HOOK.test(cmd) ? route(cmd, lite) : null; // only a harness hook is ever rewritten
|
|
75
|
+
if (c && !seen.has(c)) { seen.add(c); hs.push({ type: "command", command: c, timeout: 10 }); }
|
|
76
|
+
else if (!c && !HARNESS_HOOK.test(cmd)) hs.push(h); // a user's own hook stays verbatim
|
|
77
|
+
}
|
|
78
|
+
if (hs.length) keep.push({ ...g, hooks: hs });
|
|
79
|
+
}
|
|
80
|
+
if (keep.length) out[ev] = keep;
|
|
81
|
+
}
|
|
82
|
+
for (const [ev, arm] of ARMS) {
|
|
83
|
+
const c = '"' + lite + '" ' + arm;
|
|
84
|
+
if (!(out[ev] || []).some((g) => (g.hooks || []).some((h) => h.command === c)))
|
|
85
|
+
(out[ev] = out[ev] || []).push({ hooks: [{ type: "command", command: c, timeout: 10 }] });
|
|
86
|
+
}
|
|
87
|
+
d.hooks = out;
|
|
88
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
89
|
+
writeFileSync(path, JSON.stringify(d, null, 2) + "\n");
|
|
90
|
+
return Object.keys(out).sort();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// state: unreadable | none | ours (every harness hook points into kih and all four arms are there) | partial | other
|
|
94
|
+
export function analyzeHooks(path, kih) {
|
|
95
|
+
const r = readSettings(path);
|
|
96
|
+
if (!r.ok) return { state: "unreadable", err: r.err, hooks: [] };
|
|
97
|
+
const hooks = harnessHooks(r.json);
|
|
98
|
+
if (!hooks.length) return { state: "none", hooks };
|
|
99
|
+
const k = fwd(kih);
|
|
100
|
+
const mine = hooks.filter((h) => k && fwd(h.command).includes(k + "/"));
|
|
101
|
+
if (mine.length === hooks.length) {
|
|
102
|
+
const evs = new Set(hooks.map((h) => h.ev));
|
|
103
|
+
return { state: ARMS.every(([e]) => evs.has(e)) ? "ours" : "partial", hooks };
|
|
104
|
+
}
|
|
105
|
+
return { state: "other", hooks, elsewhere: [...new Set(hooks.filter((h) => !mine.includes(h)).map((h) => h.command))] };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// o: { settings, kih, pwsh, log }. Never throws: a surprise in a hand-written settings file becomes a WARN line and the
|
|
109
|
+
// rest of the install goes on (2026-11-09 review: a non-array hooks value crashed the whole install).
|
|
110
|
+
export function wireClaudeHooks(o) {
|
|
111
|
+
try { return wireClaudeHooksInner(o); }
|
|
112
|
+
catch (e) { (o.log || (() => {}))("hooks: WARN could not read the hooks in " + o.settings + " (" + e.message + "); left untouched -- please check it, then run: wkappbot-code install"); return { before: "error", after: "error", changed: false }; }
|
|
113
|
+
}
|
|
114
|
+
function wireClaudeHooksInner(o) {
|
|
115
|
+
const log = o.log || (() => {});
|
|
116
|
+
const before = analyzeHooks(o.settings, o.kih);
|
|
117
|
+
const res = { before: before.state, after: before.state, changed: false };
|
|
118
|
+
if (!o.kih) { log("hooks: no harness was installed, so " + o.settings + " is left as it is"); return res; }
|
|
119
|
+
if (before.state === "unreadable") {
|
|
120
|
+
log("hooks: WARN " + o.settings + " is not plain JSON (" + before.err + "); left untouched -- please fix it, then run: wkappbot-code install");
|
|
121
|
+
return res;
|
|
122
|
+
}
|
|
123
|
+
if (before.state === "ours") { log("hooks: " + o.settings + " already runs every harness hook from " + o.kih); return res; }
|
|
124
|
+
// A FULL harness hook (wkharness-run.sh, the pwsh hub of a dev seat) is never rewritten into the lite one where pwsh
|
|
125
|
+
// runs: the rewrite would downgrade the owner's own seats. Leave it and say what to do.
|
|
126
|
+
const full = before.hooks.find((h) => /wkharness-run\.sh/.test(h.command));
|
|
127
|
+
if (o.pwsh && full) {
|
|
128
|
+
log("hooks: WARN your Claude hooks run the full harness (" + full.command + ", state '" + before.state + "'); left as they are -- if they should run " + o.kih + ", rewire them from that checkout's own installer");
|
|
129
|
+
return res;
|
|
130
|
+
}
|
|
131
|
+
const lite = join(o.kih, "tools", "wkharness-lite.sh");
|
|
132
|
+
if (!existsSync(lite)) { log("hooks: WARN " + lite + " is missing, so the Claude hooks were not wired"); return res; }
|
|
133
|
+
try { wireSettings(o.settings, lite); }
|
|
134
|
+
catch (e) { log("hooks: WARN wiring " + o.settings + " failed (" + e.message + ")"); return res; }
|
|
135
|
+
ensureRules(o.kih, log);
|
|
136
|
+
const after = analyzeHooks(o.settings, o.kih);
|
|
137
|
+
res.after = after.state; res.changed = true;
|
|
138
|
+
log("hooks: " + o.settings + " was '" + before.state + "'" + (before.elsewhere ? " (pointing at " + before.elsewhere[0] + ")" : "") +
|
|
139
|
+
", now '" + after.state + "'; your own hooks were kept");
|
|
140
|
+
return res;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// The lite study lock reads <kih>/.tmp/hook-lite/rules.tsv. A web package ships it as tools/wkharness-lite-rules.tsv
|
|
144
|
+
// (built by prepack); a checkout builds it with python3 through the lite --wire; with neither, say what is missing.
|
|
145
|
+
export function ensureRules(kih, log = () => {}) {
|
|
146
|
+
const want = join(kih, ".tmp", "hook-lite", "rules.tsv");
|
|
147
|
+
if (existsSync(want)) return "present";
|
|
148
|
+
const shipped = join(kih, "tools", "wkharness-lite-rules.tsv");
|
|
149
|
+
if (existsSync(shipped)) { mkdirSync(dirname(want), { recursive: true }); writeFileSync(want, readFileSync(shipped)); return "copied"; }
|
|
150
|
+
const py = spawnSync("sh", ["-c", "command -v python3"], { encoding: "utf8" });
|
|
151
|
+
if (py.status === 0) {
|
|
152
|
+
const r = spawnSync("sh", [join(kih, "tools", "wkharness-lite.sh").replace(/\\/g, "/"), "--wire"], { encoding: "utf8" });
|
|
153
|
+
if (r.status === 0 && existsSync(want)) return "built";
|
|
154
|
+
}
|
|
155
|
+
log("hooks: WARN the study lock has no rule table (" + want + "): install python3, then run: wkappbot-code install");
|
|
156
|
+
return "missing";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// doctor row: the Claude hooks and the OpenCode plugin must run the same harness.
|
|
160
|
+
export function hooksRow(settings, kih) {
|
|
161
|
+
let a;
|
|
162
|
+
try { a = analyzeHooks(settings, kih); } catch (e) { return { ok: false, detail: settings + " could not be read (" + e.message + ")" }; }
|
|
163
|
+
if (a.state === "ours") return { ok: true, detail: settings + " runs every harness hook from " + kih };
|
|
164
|
+
if (a.state === "unreadable") return { ok: false, detail: settings + " is not plain JSON (" + a.err + ")" };
|
|
165
|
+
if (a.state === "none") return { ok: false, detail: "no harness hook in " + settings + " -- run: wkappbot-code install" };
|
|
166
|
+
return { ok: false, detail: settings + " is '" + a.state + "'" + (a.elsewhere ? ", pointing at " + a.elsewhere[0] : "") + " while the plugin uses " + kih + " -- run: wkappbot-code install" };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// CLI form for the web installer on a machine with node and no python3: node hook-wire.js wire SETTINGS LITE
|
|
170
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url) && process.argv[2] === "wire") {
|
|
171
|
+
try { console.log("[wk-harness] wired " + process.argv[3] + " with node: " + wireSettings(process.argv[3], process.argv[4]).join(", ")); }
|
|
172
|
+
catch (e) { console.error("[wk-harness] " + e.message); process.exit(1); }
|
|
173
|
+
}
|
package/lib/prereq.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// prereq.js -- the tools every install part runs on: bash and sh (the harness, the core acquire), curl and tar (the
|
|
2
|
+
// downloads), git (hooks, skills). Owner 2026-11-09: "Git Bash is a required tool too; check that it is installed along".
|
|
3
|
+
// On Windows they all come from Git for Windows (Git Bash): (1) a Git install that is not on PATH is found at its usual
|
|
4
|
+
// folders and put on this process's PATH; (2) no Git at all and winget present: `winget install --id Git.Git` (silent),
|
|
5
|
+
// then found as in (1); (3) neither: a FAILED part with the one line that says what to install. Every probe is
|
|
6
|
+
// injectable so the selftest drives all three roads on any OS.
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { delimiter, join } from "node:path";
|
|
10
|
+
|
|
11
|
+
export const TOOLS = ["bash", "sh", "curl", "tar", "git"];
|
|
12
|
+
const GIT_DIRS = (env) => [env.ProgramFiles && join(env.ProgramFiles, "Git"), env["ProgramFiles(x86)"] && join(env["ProgramFiles(x86)"], "Git"),
|
|
13
|
+
env.LOCALAPPDATA && join(env.LOCALAPPDATA, "Programs", "Git"), "C:\\Program Files\\Git"].filter(Boolean);
|
|
14
|
+
|
|
15
|
+
function realWhich(cmd, env) {
|
|
16
|
+
const win = process.platform === "win32";
|
|
17
|
+
const r = spawnSync(win ? "where" : "sh", win ? [cmd] : ["-c", "command -v " + cmd], { encoding: "utf8", env });
|
|
18
|
+
return r.status === 0 ? String(r.stdout || "").trim().split(/\r?\n/)[0] : "";
|
|
19
|
+
}
|
|
20
|
+
const realRun = (cmd, args, env) => spawnSync(cmd, args, { stdio: "inherit", env, shell: process.platform === "win32" }).status;
|
|
21
|
+
|
|
22
|
+
// o: { platform, env (mutated: PATH gains the Git dirs), which, exists, run, log, noInstall (doctor: check only) }
|
|
23
|
+
// -> { ok, missing: [...], found: {tool: path}, via: "path"|"git-dir"|"winget"|"", note }
|
|
24
|
+
export function ensurePrereqs(o = {}) {
|
|
25
|
+
const platform = o.platform || process.platform, env = o.env || process.env, log = o.log || (() => {});
|
|
26
|
+
const which = o.which || realWhich, exists = o.exists || existsSync, run = o.run || realRun;
|
|
27
|
+
const probe = () => { const found = {}, missing = []; for (const t of TOOLS) { const p = which(t, env); if (p) found[t] = p; else missing.push(t); } return { found, missing }; };
|
|
28
|
+
let p = probe(), via = "path";
|
|
29
|
+
if (!p.missing.length) return { ok: true, missing: [], found: p.found, via, note: "" };
|
|
30
|
+
if (platform !== "win32") {
|
|
31
|
+
const note = "missing " + p.missing.join(", ") + " -- install them with your package manager (e.g. apt install " + p.missing.join(" ") + "), then run: wkappbot-code install";
|
|
32
|
+
log("prereq: FAILED -- " + note);
|
|
33
|
+
return { ok: false, missing: p.missing, found: p.found, via: "", note };
|
|
34
|
+
}
|
|
35
|
+
const addGit = () => {
|
|
36
|
+
for (const d of GIT_DIRS(env)) {
|
|
37
|
+
if (!exists(join(d, "bin", "bash.exe"))) continue;
|
|
38
|
+
const key = Object.keys(env).find((k) => k.toUpperCase() === "PATH") || "PATH";
|
|
39
|
+
env[key] = [join(d, "bin"), join(d, "usr", "bin"), join(d, "cmd")].join(delimiter) + delimiter + (env[key] || "");
|
|
40
|
+
return d;
|
|
41
|
+
}
|
|
42
|
+
return "";
|
|
43
|
+
};
|
|
44
|
+
let dir = addGit();
|
|
45
|
+
if (dir) { p = probe(); via = "git-dir"; log("prereq: Git Bash found at " + dir + " (not on PATH) -- used for this install; add " + join(dir, "bin") + " to your PATH"); }
|
|
46
|
+
if (p.missing.length && !o.noInstall && which("winget", env)) {
|
|
47
|
+
log("prereq: Git for Windows (Git Bash) is missing -- installing it with winget");
|
|
48
|
+
const rc = run("winget", ["install", "--id", "Git.Git", "-e", "--source", "winget", "--silent", "--accept-package-agreements", "--accept-source-agreements"], env);
|
|
49
|
+
dir = addGit();
|
|
50
|
+
if (dir) { p = probe(); via = "winget"; log("prereq: winget installed Git for Windows at " + dir + " (rc " + rc + ")"); }
|
|
51
|
+
else log("prereq: winget rc " + rc + " but no Git Bash was found afterwards");
|
|
52
|
+
}
|
|
53
|
+
if (!p.missing.length) return { ok: true, missing: [], found: p.found, via, note: "" };
|
|
54
|
+
const note = "missing " + p.missing.join(", ") + " -- install Git for Windows (it brings Git Bash with bash, sh, curl and tar): https://git-scm.com/download/win , then run: wkappbot-code install";
|
|
55
|
+
log("prereq: FAILED -- " + note);
|
|
56
|
+
return { ok: false, missing: p.missing, found: p.found, via: "", note };
|
|
57
|
+
}
|
package/lib/setup-run.js
CHANGED
|
@@ -102,18 +102,21 @@ export function bannerFor({ done, running, seen, logPath, now = Date.now() }) {
|
|
|
102
102
|
if (age >= 0 && age < RUNNING_STALE_MS) return { line: "[wkappbot-code] setup still running (log: " + logPath + ") -- opening OpenCode anyway", mark: false };
|
|
103
103
|
return { line: "[wkappbot-code] setup did not finish (worker started " + (running.at || "?") + ", no result); log: " + logPath + " -- rerun: wkappbot-code install", mark: false };
|
|
104
104
|
}
|
|
105
|
-
if (!done
|
|
105
|
+
if (!done) return { line: "", mark: false };
|
|
106
106
|
const d = done.doctor || {}, s = done.selftest || {};
|
|
107
107
|
const total = (d.pass || 0) + (d.warn || 0) + (d.fail || 0);
|
|
108
108
|
const tail = "doctor " + (d.pass || 0) + "/" + total + (d.warn ? " (" + d.warn + " warn)" : "") + ", selftest " + (s.n || 0) + "/" + (s.m || 0);
|
|
109
109
|
const installRc = done.installRc ?? done.rc;
|
|
110
|
-
|
|
110
|
+
// only a GOOD result goes quiet once seen; a failed one is checked below and always printed
|
|
111
|
+
if (installRc === 0 && !d.fail && s.n === s.m && s.m > 0) return (seen && seen === done.at) ? { line: "", mark: false } : { line: "[wkappbot-code] setup ok, " + tail, mark: true };
|
|
111
112
|
const bad = [];
|
|
112
113
|
if (installRc !== 0) bad.push("install rc " + installRc);
|
|
113
114
|
for (const f of done.doctorFailed || []) bad.push("doctor " + f + " FAIL");
|
|
114
115
|
for (const i of done.items || []) bad.push("selftest " + i);
|
|
115
116
|
if (!bad.length) bad.push(d.fail ? "doctor " + d.fail + " FAIL" : "selftest " + (s.n || 0) + "/" + (s.m || 0));
|
|
116
|
-
|
|
117
|
+
// A failure is NEVER marked seen (owner 2026-11-09: a setup that failed for want of python3 was not noticed): it is
|
|
118
|
+
// printed on every run until a rerun of install gives a good result.
|
|
119
|
+
return { line: "[wkappbot-code] SETUP FAILED: " + bad.slice(0, 3).join("; ") + " (" + tail + "); log: " + logPath + " -- fix it, then run: wkappbot-code install", mark: false };
|
|
117
120
|
}
|
|
118
121
|
export function printBanner({ home = homedir(), out = console.log, now = Date.now() } = {}) {
|
|
119
122
|
const f = files(home);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// starter-md.js -- a standard global ~/.claude/CLAUDE.md for a new machine (owner 2026-11-09: "install CLAUDE.md as an
|
|
2
|
+
// initial standard copy too, so a raw untrained model is guarded on a new machine"). The starter
|
|
3
|
+
// (templates/global-CLAUDE.md) holds only user-safe working rules: reply language, skills first, run-not-guess, the
|
|
4
|
+
// risk classes the harness refuses, no secrets, no outward act without the user's go. Written only where the user has
|
|
5
|
+
// NO global CLAUDE.md; refreshed only while it still equals what this install wrote (sha256 kept in install-state.json);
|
|
6
|
+
// a file the user changed or wrote is never touched. Uninstall removes it only when it is still unchanged.
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
|
|
11
|
+
const sha = (s) => createHash("sha256").update(s).digest("hex");
|
|
12
|
+
|
|
13
|
+
// o: { claudeDir, pkgDir, prev (install-state starterMd), log } -> { path, sha, action }
|
|
14
|
+
export function installStarterMd(o) {
|
|
15
|
+
const log = o.log || (() => {});
|
|
16
|
+
const path = join(o.claudeDir, "CLAUDE.md");
|
|
17
|
+
const tplPath = join(o.pkgDir, "templates", "global-CLAUDE.md");
|
|
18
|
+
if (!existsSync(tplPath)) { log("claude-md: WARN the starter " + tplPath + " is missing from this package"); return { path, action: "missing-template" }; }
|
|
19
|
+
const tpl = readFileSync(tplPath, "utf8");
|
|
20
|
+
if (!existsSync(path)) {
|
|
21
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
22
|
+
writeFileSync(path, tpl);
|
|
23
|
+
log("claude-md: wrote the starter global rules to " + path + " (you had none; edit it freely)");
|
|
24
|
+
return { path, sha: sha(tpl), action: "written" };
|
|
25
|
+
}
|
|
26
|
+
const cur = readFileSync(path, "utf8");
|
|
27
|
+
// identical text is ours only when this install wrote it before (2026-11-09 review: a user's own copy that happened to
|
|
28
|
+
// equal the starter was recorded and then deleted by uninstall)
|
|
29
|
+
if (cur === tpl) return (o.prev && o.prev.sha === sha(cur)) ? { path, sha: sha(tpl), action: "current" } : { path, action: "kept" };
|
|
30
|
+
if (o.prev && o.prev.sha && o.prev.sha === sha(cur)) {
|
|
31
|
+
writeFileSync(path, tpl);
|
|
32
|
+
log("claude-md: refreshed the starter global rules in " + path + " (unchanged since the last install)");
|
|
33
|
+
return { path, sha: sha(tpl), action: "refreshed" };
|
|
34
|
+
}
|
|
35
|
+
log("claude-md: kept your " + path + " (yours); the starter is at " + tplPath + " if you want to compare");
|
|
36
|
+
return { path, action: "kept" };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// uninstall: remove the starter only when it is still exactly what this install wrote.
|
|
40
|
+
export function removeStarterMd(rec, log = () => {}) {
|
|
41
|
+
if (!rec || !rec.path || !rec.sha || !existsSync(rec.path)) return false;
|
|
42
|
+
if (sha(readFileSync(rec.path, "utf8")) !== rec.sha) { log("claude-md: kept " + rec.path + " (you changed it)"); return false; }
|
|
43
|
+
rmSync(rec.path, { force: true });
|
|
44
|
+
log("claude-md: removed the starter " + rec.path + " (unchanged since install)");
|
|
45
|
+
return true;
|
|
46
|
+
}
|
package/lib/wk-bin-acquire.sh
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
# Source 1 (unless --offline): SDK latest release asset wkappbot-PLATFORM, accepted only when its
|
|
8
8
|
# sha256 matches its SHA256SUMS line. Source 2: REPO/bin/PLATFORM/wkappbot, accepted only when
|
|
9
9
|
# its sha256 matches the MANIFEST.md row. Windows: PATH is a directory that receives
|
|
10
|
-
# wkappbot.exe and wkappbot-core.exe (xz -dc of
|
|
10
|
+
# wkappbot.exe and wkappbot-core.exe (xz -dc of wkappbot-core.exe.xz): first the SDK release pair, each file
|
|
11
|
+
# verified by its SHA256SUMS line, else repo bin/win-x64 (2026-11-09; bin/ only before, so no checkout = no core).
|
|
11
12
|
# Copies, never links; writes via a .new temp then mv; "up to date" when the sha already matches.
|
|
12
13
|
# wk-bin-acquire.sh publish SRC [--platform P] [--dry-run]
|
|
13
14
|
# Stores SRC in REPO/bin/P/ (a Windows core .exe as wkappbot-core.exe.xz, xz -9e -T0), rewrites
|
|
@@ -103,10 +104,12 @@ default_dest_win() {
|
|
|
103
104
|
# sdk_fetch PLAT DIR -- prints the verified asset path; else prints nothing and returns
|
|
104
105
|
# 3 when a download failed (host unreachable, no such asset) or 4 when the asset is UNVERIFIED
|
|
105
106
|
# (no SHA256SUMS line for it, or a sha256 mismatch). Only 3 may degrade an install to hooks-only.
|
|
106
|
-
sdk_fetch() {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
sdk_fetch() { sdk_fetch_asset "wkappbot-$1" "$2"; }
|
|
108
|
+
# sdk_fetch_asset ASSET DIR -- the same verified download for any release asset name (win-x64 takes two:
|
|
109
|
+
# wkappbot.exe and wkappbot-core.exe.xz, 2026-11-09: a Windows user with no kih checkout got NO core at all).
|
|
110
|
+
sdk_fetch_asset() {
|
|
111
|
+
command -v wk_stopwatch_lap >/dev/null 2>&1 && wk_stopwatch_lap "sdk_fetch_asset" || true
|
|
112
|
+
local asset="$1" dir="$2" base want got
|
|
110
113
|
base="${WK_BIN_BASE:-https://github.com/kiexpert/wkappbot-sdk/releases/latest/download}"
|
|
111
114
|
curl -fsSL -m "${WK_BIN_TIMEOUT:-120}" "$base/$asset" -o "$dir/$asset" || return 3
|
|
112
115
|
curl -fsSL -m "${WK_BIN_TIMEOUT:-120}" "$base/SHA256SUMS" -o "$dir/SHA256SUMS" || return 3
|
|
@@ -175,15 +178,31 @@ acquire_unix() {
|
|
|
175
178
|
|
|
176
179
|
acquire_win() {
|
|
177
180
|
command -v wk_stopwatch_lap >/dev/null 2>&1 && wk_stopwatch_lap "acquire_win" || true
|
|
178
|
-
local dest="$1" exe xzsrc core
|
|
181
|
+
local dest="$1" offline="${2:-0}" exe="" xzsrc="" core frc=3 xrc=3 label
|
|
179
182
|
[ -n "$dest" ] || dest="$(default_dest_win)"
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
+
command -v xz >/dev/null 2>&1 || die "win-x64 needs xz on PATH to unpack wkappbot-core.exe.xz (Git for Windows ships it in usr/bin)"
|
|
184
|
+
# Source 1: the SDK release pair, each file verified by its own SHA256SUMS line. Source 2: repo bin/win-x64.
|
|
185
|
+
if [ "$offline" = 0 ]; then
|
|
186
|
+
exe="$(sdk_fetch_asset wkappbot.exe "$WK_TMP")" && frc=0 || frc=$?
|
|
187
|
+
[ "$frc" = 0 ] && { xzsrc="$(sdk_fetch_asset wkappbot-core.exe.xz "$WK_TMP")" && xrc=0 || xrc=$?; }
|
|
188
|
+
fi
|
|
189
|
+
if [ "$frc" = 0 ] && [ "$xrc" = 0 ]; then
|
|
190
|
+
label="SDK release"
|
|
191
|
+
else
|
|
192
|
+
[ "$frc" = 4 ] || [ "$xrc" = 4 ] && [ -z "$REPO" ] && die "the SDK release asset for win-x64 is UNVERIFIED (no SHA256SUMS line or a sha256 mismatch); nothing installed"
|
|
193
|
+
if [ "$offline" = 0 ]; then note "SDK assets unavailable or unverified, using repo bin/win-x64"; fi
|
|
194
|
+
if [ -z "$REPO" ]; then
|
|
195
|
+
note "UNREACHABLE: wkappbot.exe and wkappbot-core.exe.xz could not be downloaded from ${WK_BIN_BASE:-github.com/kiexpert/wkappbot-sdk releases} and no kih checkout holds bin/win-x64; nothing installed"
|
|
196
|
+
exit 3
|
|
197
|
+
fi
|
|
198
|
+
exe="$(repo_file "win-x64/wkappbot.exe")"
|
|
199
|
+
xzsrc="$(repo_file "win-x64/wkappbot-core.exe.xz")"
|
|
200
|
+
label="repo bin/win-x64"
|
|
201
|
+
fi
|
|
183
202
|
core="$WK_TMP/wkappbot-core.exe"
|
|
184
|
-
xz -dc "$xzsrc" > "$core"
|
|
185
|
-
install_file "$exe" "$dest/wkappbot.exe" "
|
|
186
|
-
install_file "$core" "$dest/wkappbot-core.exe" "
|
|
203
|
+
xz -dc "$xzsrc" > "$core" || die "xz could not unpack $xzsrc"
|
|
204
|
+
install_file "$exe" "$dest/wkappbot.exe" "$label wkappbot.exe"
|
|
205
|
+
install_file "$core" "$dest/wkappbot-core.exe" "$label wkappbot-core.exe.xz"
|
|
187
206
|
}
|
|
188
207
|
|
|
189
208
|
acquire() {
|
|
@@ -199,7 +218,7 @@ acquire() {
|
|
|
199
218
|
done
|
|
200
219
|
plat="$(platform_detect)"
|
|
201
220
|
if [ "$plat" = win-x64 ]; then
|
|
202
|
-
acquire_win "$dest"
|
|
221
|
+
acquire_win "$dest" "$offline"
|
|
203
222
|
else
|
|
204
223
|
acquire_unix "$dest" "$plat" "$offline"
|
|
205
224
|
fi
|
package/lib/wk-hook.js
CHANGED
|
@@ -163,7 +163,11 @@ export function refusalReason(r) {
|
|
|
163
163
|
// The SAME classes tools/wkharness-lite.sh refuses by itself (wk_lite_risk there). With NO verdict from the harness,
|
|
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
|
+
// 0.3.13 strict path QA (owner 2026-11-09): a Windows home also arrives as C:\Users\x, C:/Users/x or MSYS /c/Users/x,
|
|
167
|
+
// and a drive root as C: -- the same forms tools/wkharness-lite.sh rk_home refuses (selftest-paths.mjs pins both).
|
|
168
|
+
const HOME_T = /^(~|\$HOME|\$\{HOME\}|\/root|\/home\/[^/\\]+|\/Users\/[^/\\]+|[A-Za-z]:[\\/]+Users[\\/]+[^/\\]+|\/[A-Za-z]\/Users\/[^/\\]+|[A-Za-z]:)[\\/]*(\*|\.\*)?$/i;
|
|
169
|
+
// A .git path segment with either separator, in any letter case (C:\r\.GIT\config on a case-blind disk).
|
|
170
|
+
const GIT_SEG = /(^|[\\/])\.git([\\/]|$)/i;
|
|
167
171
|
// The shell line as the risk floor reads it. A heredoc body (after <<'TAG', <<TAG or <<-TAG, up to the line TAG) is
|
|
168
172
|
// 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
173
|
// Words handed to a shell (sh/bash -c, eval) are also returned in seg.inner and are checked again as a command line.
|
|
@@ -258,16 +262,16 @@ export function riskClass(tool, input, cwd) {
|
|
|
258
262
|
const proj = cwd ? String(cwd).replace(/\/+$/, "") : "";
|
|
259
263
|
if (tg.some((x) => HOME_T.test(x) || x === "/" || x === "/*")) return "rm -r of HOME or /";
|
|
260
264
|
if (tg.some((x) => [".", "./", "./*", "*", "..", "../"].includes(x) || (proj && x.replace(/\/+$/, "") === proj))) return "rm -r of the project source";
|
|
261
|
-
if (tg.some((x) =>
|
|
265
|
+
if (tg.some((x) => GIT_SEG.test(x))) return "write under .git";
|
|
262
266
|
}
|
|
263
267
|
}
|
|
264
|
-
if (w.some((x) =>
|
|
268
|
+
if (w.some((x) => GIT_SEG.test(x)) && (seg.redirect || /^(rm|mv|cp|tee|truncate|dd|ln|chmod|sed)$/.test(w[0] || ""))) return "write under .git";
|
|
265
269
|
}
|
|
266
270
|
return "";
|
|
267
271
|
}
|
|
268
272
|
if (["Edit", "Write", "MultiEdit", "NotebookEdit"].includes(t)) {
|
|
269
273
|
const f = String(i.file_path || i.notebook_path || "");
|
|
270
|
-
if (/(^|[\\/])\.git[\\/]
|
|
274
|
+
if (/(^|[\\/])\.git[\\/]/i.test(f)) return "write under .git";
|
|
271
275
|
}
|
|
272
276
|
return "";
|
|
273
277
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wkappbot-code",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.13",
|
|
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": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"lib/*.js",
|
|
17
17
|
"lib/*.sh",
|
|
18
18
|
"plugin/*.js",
|
|
19
|
+
"templates/*",
|
|
19
20
|
"opencode.json",
|
|
20
21
|
"install.sh",
|
|
21
22
|
"README.md",
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Global rules for every session (installed by wkappbot-code)
|
|
2
|
+
|
|
3
|
+
This file was written by `wkappbot-code install` because you had no `~/.claude/CLAUDE.md`. Edit it freely: once you
|
|
4
|
+
change it, install never touches it again, and `wkappbot-code uninstall` leaves your version in place.
|
|
5
|
+
|
|
6
|
+
## How to work
|
|
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: `wkskill search <three or more words>`, then `wkskill read <id>`,
|
|
9
|
+
and follow the road it gives. Search again for any term you are not sure of, instead of guessing.
|
|
10
|
+
- Run, do not guess: prove a change by running its test or command, and report the real output, failures included.
|
|
11
|
+
- One command per tool call. Change only what the task needs, and back up a config file before you edit it.
|
|
12
|
+
|
|
13
|
+
## Never
|
|
14
|
+
- Skip or weaken a safety hook: no `git --no-verify`, no `git push --force`, no change of `core.hooksPath`, no
|
|
15
|
+
`rm -rf` of a home folder or a project, no writes inside `.git`. The harness refuses these; when it refuses, read
|
|
16
|
+
the skill the refusal names and take the road it prints, never a workaround.
|
|
17
|
+
- Put a secret (an API key, a token, a password) in a file, a commit, a log or a reply. Read keys from environment
|
|
18
|
+
variables.
|
|
19
|
+
- Do an irreversible outward act without the user's explicit go: sending a message or mail, paying, publishing,
|
|
20
|
+
deploying, or deleting someone else's data.
|