kaizen-agent 1.22.0 → 1.22.1
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 +14 -0
- package/cli/install.test.ts +2 -2
- package/cli/notes.test.ts +3 -3
- package/cli/sources.test.ts +4 -3
- package/cli/state.test.ts +15 -9
- package/cli/state.ts +7 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,6 +49,20 @@ Then, in any project:
|
|
|
49
49
|
|
|
50
50
|
That's it. No init and no config.
|
|
51
51
|
|
|
52
|
+
kaizen needs an AI coding agent to run it, such as [Claude Code](https://docs.claude.com/en/docs/claude-code); install one first. Git is needed for the board's commit and push.
|
|
53
|
+
|
|
54
|
+
## Use it
|
|
55
|
+
|
|
56
|
+
There are three ways in, all on the same runs:
|
|
57
|
+
|
|
58
|
+
| | How | What you do there |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| **Your agent** | `/kaizen <request>` in Claude Code, Codex, Cursor, … | Start a run and answer it in the chat. All commands are under [Commands](#commands). |
|
|
61
|
+
| **The board** | `kaizen web` opens it in the browser; `kaizen` in a terminal. `kaizen web --shortcut` once adds a launcher: Spotlight on macOS, the app menu on Linux, a Desktop shortcut on Windows | Every project's ideas and runs. Start a run, approve or revise a plan, answer its questions, pick findings to fix, then commit and push. |
|
|
62
|
+
| **Kaizen Bot** | The [desktop app](#download-the-app), or `bun start` in `bot/` | Chat with the board: ask what's waiting on you, hand teammates their asks. The app starts the board itself; from `bot/`, run `kaizen web` first. See [Kaizen Bot](#kaizen-bot). |
|
|
63
|
+
|
|
64
|
+
Settings for a project or for all of them: `kaizen settings`. New version: `kaizen upgrade`.
|
|
65
|
+
|
|
52
66
|
## See it
|
|
53
67
|
|
|
54
68
|
```
|
package/cli/install.test.ts
CHANGED
|
@@ -90,7 +90,7 @@ const fromPackage = (make: (k: string) => void, custom = false) => {
|
|
|
90
90
|
const root = mkdtempSync(join(tmpdir(), "kz-replace-"));
|
|
91
91
|
const home = join(root, "home"), pkg = join(root, "pkg");
|
|
92
92
|
const k = custom ? join(root, "kaizen") : join(home, "kaizen");
|
|
93
|
-
mkdirSync(home);
|
|
93
|
+
mkdirSync(join(home, ".claude"), { recursive: true }); // an agent: the installer stops on a box with none
|
|
94
94
|
const src = dirname(dirname(import.meta.path));
|
|
95
95
|
for (const d of ["cli", "skills", "agents", "commands", "web", "package.json"]) {
|
|
96
96
|
cpSync(join(src, d), join(pkg, d), { recursive: true, filter: (p) => !p.includes("node_modules") });
|
|
@@ -129,7 +129,7 @@ it.skipIf(!Bun.which("git"))("an npm package never replaces a development checko
|
|
|
129
129
|
it("upgrade with npm unreachable says not upgraded, never up to date", () => {
|
|
130
130
|
const root = mkdtempSync(join(tmpdir(), "kz-offline-"));
|
|
131
131
|
const home = join(root, "home");
|
|
132
|
-
mkdirSync(home);
|
|
132
|
+
mkdirSync(join(home, ".claude"), { recursive: true }); // an agent: the installer stops on a box with none
|
|
133
133
|
kaizenFiles(join(home, "kaizen"));
|
|
134
134
|
const env = { PATH: process.env.PATH ?? "", HOME: home, TMPDIR: root, HTTPS_PROXY: "http://127.0.0.1:9", https_proxy: "http://127.0.0.1:9" };
|
|
135
135
|
const p = Bun.spawnSync([process.execPath, script, "upgrade"], { cwd: home, env });
|
package/cli/notes.test.ts
CHANGED
|
@@ -71,11 +71,11 @@ test("list: recursive, newest first, dot folders skipped", () => {
|
|
|
71
71
|
test("list: only names the notebook can save back; a symlinked folder once, never a loop", () => {
|
|
72
72
|
const n = join(state, "notes");
|
|
73
73
|
saveNote(state, "ok", "x", 0);
|
|
74
|
-
|
|
75
|
-
writeFileSync(join(n, "a: b.md"), "x");
|
|
74
|
+
// Windows refuses these names outright, so there is nothing to list.
|
|
75
|
+
if (process.platform !== "win32") { writeFileSync(join(n, "What?.md"), "x"); writeFileSync(join(n, "a: b.md"), "x"); }
|
|
76
76
|
mkdirSync(join(n, "1/2/3/4/5/6"), { recursive: true }); writeFileSync(join(n, "1/2/3/4/5/6/deep.md"), "x");
|
|
77
77
|
mkdirSync(join(n, "trail ")); writeFileSync(join(n, "trail ", "x.md"), "x");
|
|
78
|
-
symlinkSync("..", join(n, "up"));
|
|
78
|
+
symlinkSync(process.platform === "win32" ? dirname(n) : "..", join(n, "up"), "junction"); // a junction needs no admin
|
|
79
79
|
expect(listNotes(state).map((x) => x.name)).toEqual(["ok"]);
|
|
80
80
|
});
|
|
81
81
|
|
package/cli/sources.test.ts
CHANGED
|
@@ -498,11 +498,12 @@ test("gather everything: each source's ideas land in its own project and no othe
|
|
|
498
498
|
// The real server, in a scratch home on its own port. Nothing is fetched: only add, list and move.
|
|
499
499
|
test("the board refuses to add a source without a project, and moves one between two", async () => {
|
|
500
500
|
const homeDir = join(root, "home" + n), a = join(homeDir, "a", ".kaizen"), b = join(homeDir, "b", ".kaizen");
|
|
501
|
-
|
|
501
|
+
// ~/.claude: kaizen will not start on a machine with no agent, and a CI box has none.
|
|
502
|
+
for (const d of [a, b, join(homeDir, ".kaizen"), join(homeDir, ".claude")]) mkdirSync(d, { recursive: true });
|
|
502
503
|
writeFileSync(join(homeDir, ".kaizen", "projects"), [join(homeDir, "a"), join(homeDir, "b")].join("\n") + "\n");
|
|
503
504
|
const port = 20000 + Math.floor(Math.random() * 20000);
|
|
504
505
|
const repo = join(import.meta.dir, "..");
|
|
505
|
-
const proc = Bun.spawn([process.execPath, join(repo, "cli", "install.ts"), "web", "--port", String(port), "--no-open"], {
|
|
506
|
+
const proc = Bun.spawn([process.execPath, join(repo, "cli", "install.ts"), "web", "--port", String(port), "--no-open", "--foreground"], {
|
|
506
507
|
cwd: join(homeDir, "a"), env: { ...process.env, HOME: homeDir, USERPROFILE: homeDir, KAIZEN_HOME: repo }, stdout: "ignore", stderr: "ignore",
|
|
507
508
|
});
|
|
508
509
|
try {
|
|
@@ -530,4 +531,4 @@ test("the board refuses to add a source without a project, and moves one between
|
|
|
530
531
|
expect(await post({ op: "add", dir: b, url: SHEET })).toMatchObject({ status: 200 });
|
|
531
532
|
expect(await post({ op: "move", dir: b, to: a, url: SHEET })).toMatchObject({ status: 409, error: "that project already has this link" });
|
|
532
533
|
} finally { proc.kill(); await proc.exited; }
|
|
533
|
-
});
|
|
534
|
+
}, 30_000); // a cold board start is slow on Windows
|
package/cli/state.test.ts
CHANGED
|
@@ -399,6 +399,8 @@ test("commitPush: commits the run's files only, pushes, and records it; the mess
|
|
|
399
399
|
expect(readCommit(st, "r1")).toEqual({ sha: r.sha, pushed: true });
|
|
400
400
|
expect(cardsGit(st, ["r1", "r0", "nope"])).toEqual({ r1: "pushed", r0: "pushed" }); // r0: no record, but its file is in; c.txt still changed is not its business
|
|
401
401
|
|
|
402
|
+
// Windows has no file named b*.txt, so the rest is for everywhere else.
|
|
403
|
+
if (process.platform === "win32") return;
|
|
402
404
|
// A push that fails leaves a record that says so.
|
|
403
405
|
sh(proj, "remote", "set-url", "origin", join(root, "gone.git"));
|
|
404
406
|
// A fix round's report names files too, and a name with a star in it is not a pattern.
|
|
@@ -415,7 +417,7 @@ test("commitPush: commits the run's files only, pushes, and records it; the mess
|
|
|
415
417
|
expect(runGit(st, "r1")).toMatchObject({ files: [], ahead: 1, others: 2 });
|
|
416
418
|
expect(cardsGit(st, ["r1"])).toEqual({ r1: "pending" }); // committed, not pushed
|
|
417
419
|
expect(cardsGit(join(root, ".kaizen"), ["r1"])).toEqual({}); // not a repo
|
|
418
|
-
});
|
|
420
|
+
}, 60_000); // dozens of git calls, ~250ms each on Windows
|
|
419
421
|
|
|
420
422
|
test.skipIf(process.platform === "win32")("findTerminal: a running herdr wins, then tmux, else neither; the prompt stays one literal argument", () => {
|
|
421
423
|
const bin = mkdtempSync(join(tmpdir(), "kz-bin-"));
|
|
@@ -427,11 +429,11 @@ test.skipIf(process.platform === "win32")("findTerminal: a running herdr wins, t
|
|
|
427
429
|
for (const k of keys.slice(1)) delete process.env[k];
|
|
428
430
|
const prompt = `/kaizen it's "q" $HOME \`id\`\n\nnote`;
|
|
429
431
|
const full = ["/usr/bin/claude", "--model", "opus", prompt];
|
|
430
|
-
expect(findTerminal("/p", full)?.cmd[0]).not.toMatch(/^(sh|tmux)$/); // neither installed
|
|
432
|
+
expect(findTerminal("/p", full)?.cmd[0] ?? "none").not.toMatch(/^(sh|tmux)$/); // neither installed (none: no terminal, as on CI)
|
|
431
433
|
stub("herdr", 1); stub("tmux", 1);
|
|
432
|
-
expect(findTerminal("/p", full)?.cmd[0]).not.toMatch(/^(sh|tmux)$/); // installed, no server
|
|
434
|
+
expect(findTerminal("/p", full)?.cmd[0] ?? "none").not.toMatch(/^(sh|tmux)$/); // installed, no server
|
|
433
435
|
process.env.TMUX = "/tmp/x,1,0"; process.env.HERDR_ENV = "1";
|
|
434
|
-
expect(findTerminal("/p", full)?.cmd[0]).not.toMatch(/^(sh|tmux)$/); // stale env from a server since stopped
|
|
436
|
+
expect(findTerminal("/p", full)?.cmd[0] ?? "none").not.toMatch(/^(sh|tmux)$/); // stale env from a server since stopped
|
|
435
437
|
delete process.env.TMUX; delete process.env.HERDR_ENV; stub("tmux", 0);
|
|
436
438
|
const t = findTerminal("/p", full)!; // outside it, server up
|
|
437
439
|
expect(t.cmd.slice(0, 4)).toEqual(["tmux", "new-window", "-c", "/p"]);
|
|
@@ -456,8 +458,9 @@ test("nativePath on Windows reads the spellings Git Bash writes into the project
|
|
|
456
458
|
for (const p of ["/c/Users/me/proj", "C:/Users/me/proj", "c:\\Users\\me\\proj\\", "C:\\Users\\me\\proj"])
|
|
457
459
|
expect(nativePath(p)).toBe("C:\\Users\\me\\proj");
|
|
458
460
|
expect(nativePath("/d")).toBe("D:\\");
|
|
461
|
+
Object.defineProperty(process, "platform", { value: "linux" });
|
|
462
|
+
expect(nativePath("/c/Users/me")).toBe("/c/Users/me"); // elsewhere a path is left alone
|
|
459
463
|
} finally { Object.defineProperty(process, "platform", platform); }
|
|
460
|
-
expect(nativePath("/c/Users/me")).toBe("/c/Users/me"); // elsewhere a path is left alone
|
|
461
464
|
});
|
|
462
465
|
|
|
463
466
|
test("findTerminal on Windows names cmd and PowerShell by full path, so a PATH without System32 still launches", () => {
|
|
@@ -474,7 +477,9 @@ test("findTerminal on Windows names cmd and PowerShell by full path, so a PATH w
|
|
|
474
477
|
expect(t.cmd[6]).toBe("D:\\Win\\System32\\WindowsPowerShell\\v1.0\\powershell.exe");
|
|
475
478
|
process.env.ComSpec = "E:\\cmd.exe";
|
|
476
479
|
expect(findTerminal("C:\\p", ["claude"])!.cmd[0]).toBe("E:\\cmd.exe");
|
|
477
|
-
// A running herdr: a tab, and the run typed into it as one encoded line.
|
|
480
|
+
// A running herdr: a tab, and the run typed into it as one encoded line. The fake
|
|
481
|
+
// is a shell script, which real Windows cannot run with ComSpec pointed elsewhere.
|
|
482
|
+
if (platform.value === "win32") return;
|
|
478
483
|
writeFileSync(join(process.env.PATH, "herdr"), "#!/bin/sh\nexit 0\n", { mode: 0o755 });
|
|
479
484
|
const h = findTerminal("C:\\p", ["claude", "/kaizen it's\nnote"])!;
|
|
480
485
|
expect([h.cmd[0], h.cmd[1], h.cmd[2], h.detached]).toEqual(["D:\\Win\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", "-NoProfile", "-EncodedCommand", false]);
|
|
@@ -815,13 +820,14 @@ test("trustFolder: marks the repo root trusted in a temp config, keeps the rest,
|
|
|
815
820
|
mkdirSync(sub, { recursive: true }); mkdirSync(plain);
|
|
816
821
|
Bun.spawnSync(["git", "init", "-q", repo]);
|
|
817
822
|
const cfg = join(t, ".claude.json");
|
|
818
|
-
|
|
823
|
+
const key = (p: string) => process.platform === "win32" ? p.replace(/\\/g, "/") : p; // Claude Code's spelling
|
|
824
|
+
writeFileSync(cfg, JSON.stringify({ keep: 1, projects: { [key(plain)]: { x: 2 } } }));
|
|
819
825
|
trustFolder(sub, cfg);
|
|
820
826
|
trustFolder(plain, cfg);
|
|
821
827
|
const d = JSON.parse(readFileSync(cfg, "utf8"));
|
|
822
828
|
expect(d.keep).toBe(1);
|
|
823
|
-
expect(d.projects[realpathSync(repo)]).toEqual({ hasTrustDialogAccepted: true });
|
|
824
|
-
expect(d.projects[plain]).toEqual({ x: 2, hasTrustDialogAccepted: true });
|
|
829
|
+
expect(d.projects[key(realpathSync.native(repo))]).toEqual({ hasTrustDialogAccepted: true });
|
|
830
|
+
expect(d.projects[key(plain)]).toEqual({ x: 2, hasTrustDialogAccepted: true });
|
|
825
831
|
writeFileSync(cfg, "{bad");
|
|
826
832
|
trustFolder(plain, cfg);
|
|
827
833
|
expect(readFileSync(cfg, "utf8")).toBe("{bad");
|
package/cli/state.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// `.kaizen/` directory. No terminal, no HTTP: the TUI (`dashboard.ts`) and the web
|
|
3
3
|
// board (`web.ts`) both import this and draw it their own way.
|
|
4
4
|
import { existsSync, readFileSync, readdirSync, appendFileSync, writeFileSync, mkdirSync, statSync, realpathSync, rmSync, renameSync, copyFileSync } from "node:fs";
|
|
5
|
-
import { join, dirname, win32 } from "node:path";
|
|
5
|
+
import { join, dirname, win32, posix } from "node:path";
|
|
6
6
|
import { homedir, arch, cpus, release, totalmem, version as osVersion } from "node:os";
|
|
7
7
|
|
|
8
8
|
// The real path: process.cwd() is always physical, so a raw $HOME that crosses a
|
|
@@ -730,11 +730,12 @@ export function appendNote(state: string, target: { id: string } | { text: strin
|
|
|
730
730
|
// ponytail: no dedupe, no size accounting beyond the request cap in web.ts.
|
|
731
731
|
export function saveAttachment(state: string, id: string | null, name: string, bytes: Uint8Array) {
|
|
732
732
|
const safe = name.replace(/[^\w.-]+/g, "_").replace(/^\.+/, "") || "file";
|
|
733
|
-
|
|
733
|
+
// posix: the note links it as attachments/..., and a backslash on Windows breaks that.
|
|
734
|
+
const rel = id ? posix.join("runs", id, "attachments") : "attachments";
|
|
734
735
|
mkdirSync(join(state, rel), { recursive: true });
|
|
735
736
|
const file = `${Date.now().toString(36)}-${safe}`;
|
|
736
737
|
writeFileSync(join(state, rel, file), bytes);
|
|
737
|
-
return join(rel, file);
|
|
738
|
+
return posix.join(rel, file);
|
|
738
739
|
}
|
|
739
740
|
|
|
740
741
|
// What a card is worth interrupting someone for, in the words both UIs send: a run
|
|
@@ -1133,7 +1134,9 @@ export function sessionLaunch(projectDir: string, cmd: string, base: string[], p
|
|
|
1133
1134
|
export function trustFolder(dir: string, cfg = join(process.env.CLAUDE_CONFIG_DIR ?? homedir(), ".claude.json")) {
|
|
1134
1135
|
try {
|
|
1135
1136
|
const top = Bun.spawnSync(["git", "-C", dir, "rev-parse", "--show-toplevel"], { stderr: "ignore" });
|
|
1136
|
-
|
|
1137
|
+
// Claude Code keys its projects with forward slashes on Windows, as git prints them.
|
|
1138
|
+
const found = top.success ? top.stdout.toString().trim() || dir : dir;
|
|
1139
|
+
const root = process.platform === "win32" ? found.replace(/\\/g, "/") : found;
|
|
1137
1140
|
const d = JSON.parse(readFileSync(cfg, "utf8"));
|
|
1138
1141
|
if (d.projects?.[root]?.hasTrustDialogAccepted) return;
|
|
1139
1142
|
d.projects ??= {};
|
package/package.json
CHANGED