kaizen-agent 1.3.10 → 1.4.0

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 CHANGED
@@ -574,11 +574,20 @@ with a comment on each key, if you would rather edit the file.
574
574
  | `--yes` | Take every default, ask nothing |
575
575
  | `--verbose` | List every link instead of a one-line summary |
576
576
  | `upgrade` | Pull, relink, and clear the installer cache. No prompts. |
577
+ | `uninstall` | Remove the links, the clone, and the launcher. Asks whether to keep your runs and settings. |
578
+ | `uninstall --purge` | The same, and delete `~/.kaizen` and this project's `.kaizen/` too. |
577
579
  | `settings` | Open the settings browser for the nearest `.kaizen/`. Also `config`. |
578
580
 
579
- A global install also writes `~/.local/bin/kaizen`, a two-line launcher pointing at
581
+ Uninstalling works the same on Linux, macOS and Windows — `kaizen uninstall`, or
582
+ `bunx kaizen-agent uninstall` when the command itself is already gone. It offers three
583
+ answers — keep your runs and settings, remove everything, or cancel — and takes the
584
+ first without asking when the output is not a terminal. It lists what it removed; the `## Kaizen workflow` line in each project's `CLAUDE.md` is left for
585
+ you to delete, since you may have edited around it.
586
+
587
+ A global install also writes `~/.local/bin/kaizen` (`kaizen.cmd` in bun's bin
588
+ directory on Windows), a two-line launcher pointing at
580
589
  the clone, so `kaizen upgrade` works from anywhere. It tells you if that directory is
581
- not on your PATH. Delete the file to remove it; `--project` installs skip it.
590
+ not on your PATH. `uninstall` removes it; `--project` installs skip it.
582
591
 
583
592
  Re-running is safe: correct links are left alone.
584
593
 
package/cli/dashboard.ts CHANGED
@@ -440,8 +440,7 @@ export async function dashboard(
440
440
  // Only searching under $HOME misses every project on another drive or mount,
441
441
  // which on Windows is most of them. The folder the dashboard was opened in is
442
442
  // searched too, and it is where the projects being worked on actually are.
443
- const roots = [home, dirname(state ?? process.cwd()), process.cwd()]
444
- .filter((d, i, all) => all.indexOf(d) === i);
443
+ const roots = searchRoots([dirname(state ?? process.cwd()), process.cwd()]);
445
444
  const found = [...new Set(roots.flatMap((r) => findProjects(r)))];
446
445
  for (const dir of found) remember(dir);
447
446
  const added = knownProjects().length - before;
@@ -1289,6 +1288,21 @@ export async function dashboard(
1289
1288
  // read and corrected in an editor; blanks and # lines ignored.
1290
1289
  const REGISTRY = join(home, ".kaizen", "projects");
1291
1290
 
1291
+ // Windows spreads work across drives -- the profile on C:, the projects on D: or E: --
1292
+ // and a search that only knows $HOME finds none of them. Every letter that answers is
1293
+ // a root worth walking; elsewhere there is one filesystem and $HOME is where work
1294
+ // lives, so walking / would cost minutes to find nothing.
1295
+ export function searchRoots(extra: string[] = []): string[] {
1296
+ const roots = [home, ...extra];
1297
+ if (process.platform === "win32") {
1298
+ for (const letter of "ABCDEFGHIJKLMNOPQRSTUVWXYZ") {
1299
+ const drive = `${letter}:\\`;
1300
+ try { readdirSync(drive); roots.push(drive); } catch { /* no such drive */ }
1301
+ }
1302
+ }
1303
+ return roots.filter((d, i, all) => d && all.indexOf(d) === i);
1304
+ }
1305
+
1292
1306
  export function knownProjects(): string[] {
1293
1307
  if (!existsSync(REGISTRY)) return [];
1294
1308
  return readFileSync(REGISTRY, "utf8").split("\n")
@@ -1306,10 +1320,16 @@ export function remember(projectDir: string) {
1306
1320
  } catch { /* nothing here is worth an error */ }
1307
1321
  }
1308
1322
 
1323
+ const SKIP = new Set([
1324
+ "windows", "program files", "program files (x86)", "programdata", "$recycle.bin",
1325
+ "system volume information", "appdata", "node_modules", "recovery", "perflogs",
1326
+ "library", "applications", "onedrive", "onedrivetemp",
1327
+ ]);
1328
+
1309
1329
  // Bounded on purpose. Depth is what keeps this from becoming a filesystem walk, and
1310
1330
  // the skips are the directories that make one slow: node_modules, and anything dotted
1311
1331
  // (which includes .git, and every state directory that is not a project of its own).
1312
- function findProjects(root: string, depth = 4): string[] {
1332
+ export function findProjects(root: string, depth = 4): string[] {
1313
1333
  const found: string[] = [];
1314
1334
  const walk = (dir: string, left: number) => {
1315
1335
  let entries: string[];
@@ -1322,6 +1342,9 @@ function findProjects(root: string, depth = 4): string[] {
1322
1342
  if (left === 0) return;
1323
1343
  for (const name of entries) {
1324
1344
  if (name.startsWith(".") || name === "node_modules") continue;
1345
+ // Walking a whole drive means walking Windows itself otherwise: tens of
1346
+ // thousands of directories that have never held a project.
1347
+ if (SKIP.has(name.toLowerCase())) continue;
1325
1348
  try { if (statSync(join(dir, name)).isDirectory()) walk(join(dir, name), left - 1); }
1326
1349
  catch { /* unreadable or vanished mid-walk */ }
1327
1350
  }
package/cli/install.ts CHANGED
@@ -14,6 +14,7 @@ const args = new Set(Bun.argv.slice(2));
14
14
  const upgrade = args.has("upgrade") || args.has("--upgrade");
15
15
  const wantSettings = args.has("settings") || args.has("config");
16
16
  const check = args.has("--check");
17
+ const uninstall = args.has("uninstall") || args.has("--uninstall");
17
18
  const force = args.has("--force");
18
19
  const verbose = args.has("--verbose");
19
20
  const interactive = process.stdin.isTTY && !check && !upgrade && !args.has("--yes");
@@ -68,6 +69,87 @@ const onPath = new Set(
68
69
  const found = (r: string) =>
69
70
  AGENTS.filter((a) => existsSync(join(r, a.dir)) || onPath.has(a.dir));
70
71
 
72
+ if (uninstall) { await removeEverything(); process.exit(0); }
73
+
74
+ // Everything kaizen put on this machine, in one place, so uninstalling needs no
75
+ // clone to read: the links are known by name, the rest by path. State (~/.kaizen
76
+ // and any project's .kaizen) is left alone unless --purge says otherwise, because
77
+ // it holds runs and decisions this program did not write.
78
+ async function removeEverything() {
79
+ let purge = args.has("--purge") || args.has("--all");
80
+ // Deleting someone's runs is not a thing to do on a typo, so ask unless the
81
+ // answer already arrived as a flag.
82
+ if (interactive && !purge) {
83
+ const choice = await select("Uninstall kaizen", [
84
+ { label: "Keep my runs and settings", hint: "removes the app, keeps ~/.kaizen and each project's .kaizen/", value: "keep" },
85
+ { label: "Remove everything", hint: "also deletes runs, backlog, memory and settings", value: "all" },
86
+ { label: "Cancel", hint: "change nothing", value: "cancel" },
87
+ ]);
88
+ if (choice === "cancel") { console.log(`\n ${c.dim("Nothing removed.")}\n`); return; }
89
+ purge = choice === "all";
90
+ }
91
+ const gone: string[] = [];
92
+ const stuck: string[] = [];
93
+ const drop = (p: string) => {
94
+ if (!existsSync(p) && !isLink(p)) return;
95
+ // Windows refuses to delete a file another process holds open, and a silent
96
+ // failure here would read as a clean uninstall that removed nothing.
97
+ try { rmSync(p, { recursive: true, force: true }); gone.push(tilde(p)); }
98
+ catch { stuck.push(tilde(p)); }
99
+ };
100
+
101
+ // A --project install puts links in the project folder, and on Windows those
102
+ // projects live on other drives entirely. Every project kaizen knows about is
103
+ // checked, plus whatever a drive-wide scan turns up.
104
+ const { knownProjects, searchRoots, findProjects } = await import("./dashboard.ts");
105
+ step("looking for project installs");
106
+ const roots = [home, process.cwd(), ...knownProjects(),
107
+ ...searchRoots([process.cwd()]).flatMap((r) => findProjects(r))]
108
+ .filter((d, i, all) => all.indexOf(d) === i);
109
+
110
+ for (const root of roots) {
111
+ for (const a of AGENTS) {
112
+ for (const n of ["kaizen", "kaizen-help"]) drop(join(root, a.dir, "skills", n));
113
+ for (const sub of ["agents", "commands"]) {
114
+ const dir = join(root, a.dir, sub);
115
+ try {
116
+ for (const f of readdirSync(dir)) {
117
+ if (f.startsWith("kaizen-") && f.endsWith(".md")) drop(join(dir, f));
118
+ }
119
+ } catch { /* no such directory */ }
120
+ }
121
+ }
122
+ }
123
+
124
+ drop(process.env.KAIZEN_HOME ?? join(home, "kaizen"));
125
+ drop(join(process.env.BUN_INSTALL ?? join(home, ".bun"), "bin", "kaizen.cmd"));
126
+ drop(join(home, ".local", "bin", "kaizen"));
127
+ await clearBunxCache();
128
+
129
+ if (purge) {
130
+ drop(join(home, ".kaizen"));
131
+ drop(join(process.cwd(), ".kaizen"));
132
+ }
133
+
134
+ console.log(`\n ${c.bold("Uninstalled")}`);
135
+ for (const p of gone) console.log(` ${c.dim("removed " + p)}`);
136
+ if (!gone.length && !stuck.length) console.log(` ${c.dim("nothing left to remove")}`);
137
+ if (stuck.length) {
138
+ console.log(`\n ${c.bold("could not remove")} ${c.dim("— close any dashboard or agent still using these, then run it again")}`);
139
+ for (const p of stuck) console.log(` ${c.dim(p)}`);
140
+ }
141
+ console.log(purge
142
+ ? `\n ${c.dim("Runs and settings deleted too.")}\n`
143
+ : `\n ${c.dim("Runs and settings kept in")} ${c.cyan(tilde(join(home, ".kaizen")))}${c.dim(" — add --purge to delete them.")}\n`);
144
+ console.log(` ${c.dim("The CLAUDE.md / AGENTS.md line in each project is left for you to remove.")}\n`);
145
+ }
146
+
147
+ // A dangling symlink fails existsSync, and a stale link is exactly what wants
148
+ // removing here.
149
+ function isLink(p: string) {
150
+ try { return lstatSync(p).isSymbolicLink(); } catch { return false; }
151
+ }
152
+
71
153
  // Nothing to install into. kaizen is a workflow an agent runs; on its own it does
72
154
  // nothing, so send them to the agent first rather than laying out files no reader
73
155
  // will ever load.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kaizen-agent",
3
- "version": "1.3.10",
3
+ "version": "1.4.0",
4
4
  "description": "Staged workflow for AI agents — code, writing, research, operations: plan, human approval, build, independent review, bounded fix loop.",
5
5
  "type": "module",
6
6
  "bin": {