kaizen-agent 1.34.4 → 1.34.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/cli/install.ts +17 -1
- package/cli/qr.ts +243 -0
- package/cli/setup.test.ts +57 -0
- package/cli/setup.ts +108 -0
- package/cli/state.test.ts +5 -1
- package/cli/state.ts +16 -0
- package/cli/web.ts +4 -5
- package/package.json +1 -1
- package/web/board.html +3 -1
- package/web/board.js +1 -1
- package/web/notebook.js +1 -1
- package/web/term.js +21 -21
- package/web/term.ts +59 -24
package/README.md
CHANGED
|
@@ -70,6 +70,8 @@ kaizen web
|
|
|
70
70
|
|
|
71
71
|
The board opens in your browser. The side menu holds the board, notes, the icebox of gathered requests and settings, with a light and dark switch. From there you can start a run, approve or revise its plan, pick which review findings to fix, then commit and push.
|
|
72
72
|
|
|
73
|
+
Right after installing, `kaizen setup` walks you through your project, settings, this board and the phone app (it prints a QR code to scan).
|
|
74
|
+
|
|
73
75
|
To add a launcher (Spotlight on macOS, the app menu on Linux, a Desktop shortcut on Windows), run `kaizen web --shortcut` once.
|
|
74
76
|
|
|
75
77
|
## Development
|
package/cli/install.ts
CHANGED
|
@@ -340,6 +340,17 @@ async function openUrl(url: string) {
|
|
|
340
340
|
try { await Bun.$`${opener} ${url}`.quiet(); return true; } catch { return false; }
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
+
if (args.has("setup")) {
|
|
344
|
+
await runSetup();
|
|
345
|
+
process.exit(0);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function runSetup() {
|
|
349
|
+
const s = await import("./setup.ts");
|
|
350
|
+
if (!process.stdin.isTTY) { console.log(s.manual()); return; }
|
|
351
|
+
await s.setup({ select, c, ask: (q) => prompt(q) });
|
|
352
|
+
}
|
|
353
|
+
|
|
343
354
|
if (wantSettings) {
|
|
344
355
|
const { settings } = await import("./settings.ts");
|
|
345
356
|
await settings(await resolveRepoQuietly());
|
|
@@ -908,7 +919,7 @@ function kaizenAbove() {
|
|
|
908
919
|
}
|
|
909
920
|
|
|
910
921
|
if (inProject && !kaizenAbove()) {
|
|
911
|
-
const now = root === process.cwd()
|
|
922
|
+
const now = root === process.cwd() || args.has("--init-project")
|
|
912
923
|
? true
|
|
913
924
|
: interactive
|
|
914
925
|
? await select(`Set up ${basename(process.cwd())}/ for kaizen now`, [
|
|
@@ -976,9 +987,14 @@ function writePointer() {
|
|
|
976
987
|
${c.bold("Next")}
|
|
977
988
|
1 restart Claude Code ${c.dim("— skills load live, slash commands only at session start")}
|
|
978
989
|
2 ${c.cyan("/kaizen <what you want done>")} ${c.dim("in any project — it sets itself up on first run")}
|
|
990
|
+
3 ${c.cyan("kaizen setup")} ${c.dim("— settings, the board, and the phone app, any time")}
|
|
979
991
|
|
|
980
992
|
${c.dim("/kaizen-help lists every command. kaizen upgrade updates all of this.")}
|
|
981
993
|
`);
|
|
994
|
+
if (interactive && !blocked && await select("Run setup now?", [
|
|
995
|
+
{ label: "Not now", hint: "kaizen setup runs it later", value: false },
|
|
996
|
+
{ label: "Yes", hint: "a project folder, settings, the board, the phone", value: true },
|
|
997
|
+
])) await runSetup();
|
|
982
998
|
}
|
|
983
999
|
if (blocked) process.exit(1);
|
|
984
1000
|
|
package/cli/qr.ts
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// A QR code for a short URL, drawn with terminal half-blocks. Zero dependencies, so
|
|
2
|
+
// this is the small encoder: byte mode, error correction L, versions 1-6 (up to 134
|
|
3
|
+
// bytes), which is far more than a tailnet address needs.
|
|
4
|
+
|
|
5
|
+
// [codewords in all, error-correction codewords per block, blocks] for ECC L.
|
|
6
|
+
const VERSIONS = [[26, 7, 1], [44, 10, 1], [70, 15, 1], [100, 20, 1], [134, 26, 1], [172, 18, 2]] as const;
|
|
7
|
+
const ALIGN = [[], [6, 18], [6, 22], [6, 26], [6, 30], [6, 34]];
|
|
8
|
+
|
|
9
|
+
function gfMul(a: number, b: number) {
|
|
10
|
+
let r = 0;
|
|
11
|
+
for (; b; b >>= 1) { if (b & 1) r ^= a; a <<= 1; if (a & 0x100) a ^= 0x11d; }
|
|
12
|
+
return r;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function rsRemainder(data: number[], degree: number) {
|
|
16
|
+
const div = Array(degree).fill(0); div[degree - 1] = 1;
|
|
17
|
+
let root = 1;
|
|
18
|
+
for (let i = 0; i < degree; i++) {
|
|
19
|
+
for (let j = 0; j < degree; j++) { div[j] = gfMul(div[j], root); if (j + 1 < degree) div[j] ^= div[j + 1]; }
|
|
20
|
+
root = gfMul(root, 2);
|
|
21
|
+
}
|
|
22
|
+
const out: number[] = Array(degree).fill(0);
|
|
23
|
+
for (const b of data) {
|
|
24
|
+
const f = b ^ out.shift()!; out.push(0);
|
|
25
|
+
div.forEach((c, i) => { out[i] ^= gfMul(c, f); });
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function qrMatrix(text: string): boolean[][] {
|
|
31
|
+
const bytes = [...new TextEncoder().encode(text)];
|
|
32
|
+
const v = VERSIONS.findIndex(([total, ec, n]) => total - ec * n >= bytes.length + 2);
|
|
33
|
+
if (v < 0) throw new Error("text too long for qr()");
|
|
34
|
+
const [total, ecLen, nBlocks] = VERSIONS[v]!;
|
|
35
|
+
const dataLen = total - ecLen * nBlocks;
|
|
36
|
+
const size = 17 + 4 * (v + 1);
|
|
37
|
+
|
|
38
|
+
const bits: number[] = [];
|
|
39
|
+
const put = (val: number, n: number) => { for (let i = n - 1; i >= 0; i--) bits.push((val >>> i) & 1); };
|
|
40
|
+
put(4, 4); put(bytes.length, 8); bytes.forEach((b) => put(b, 8));
|
|
41
|
+
put(0, Math.min(4, dataLen * 8 - bits.length));
|
|
42
|
+
while (bits.length % 8) bits.push(0);
|
|
43
|
+
const data: number[] = [];
|
|
44
|
+
for (let i = 0; i < bits.length; i += 8) data.push(parseInt(bits.slice(i, i + 8).join(""), 2));
|
|
45
|
+
for (let pad = 0xec; data.length < dataLen; pad ^= 0xec ^ 0x11) data.push(pad);
|
|
46
|
+
|
|
47
|
+
const per = dataLen / nBlocks;
|
|
48
|
+
const blocks = Array.from({ length: nBlocks }, (_, i) => data.slice(i * per, (i + 1) * per));
|
|
49
|
+
const ecs = blocks.map((b) => rsRemainder(b, ecLen));
|
|
50
|
+
const words: number[] = [];
|
|
51
|
+
for (let i = 0; i < per; i++) blocks.forEach((b) => words.push(b[i]!));
|
|
52
|
+
for (let i = 0; i < ecLen; i++) ecs.forEach((e) => words.push(e[i]!));
|
|
53
|
+
|
|
54
|
+
const m: boolean[][] = Array.from({ length: size }, () => Array(size).fill(false));
|
|
55
|
+
const fn: boolean[][] = Array.from({ length: size }, () => Array(size).fill(false));
|
|
56
|
+
const set = (x: number, y: number, dark: boolean) => { m[y]![x] = dark; fn[y]![x] = true; };
|
|
57
|
+
|
|
58
|
+
for (let i = 0; i < size; i++) { set(6, i, i % 2 === 0); set(i, 6, i % 2 === 0); }
|
|
59
|
+
for (const [cx, cy] of [[3, 3], [size - 4, 3], [3, size - 4]] as const) {
|
|
60
|
+
for (let dy = -4; dy <= 4; dy++) for (let dx = -4; dx <= 4; dx++) {
|
|
61
|
+
const x = cx + dx, y = cy + dy, d = Math.max(Math.abs(dx), Math.abs(dy));
|
|
62
|
+
if (x >= 0 && x < size && y >= 0 && y < size) set(x, y, d !== 2 && d !== 4);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const al = ALIGN[v]!;
|
|
66
|
+
for (const cy of al) for (const cx of al) {
|
|
67
|
+
if ((cx === 6 && cy === 6) || (cx === 6 && cy === al.at(-1)) || (cx === al.at(-1) && cy === 6)) continue;
|
|
68
|
+
for (let dy = -2; dy <= 2; dy++) for (let dx = -2; dx <= 2; dx++) set(cx + dx, cy + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const format = (mask: number) => {
|
|
72
|
+
const d = (1 << 3) | mask; // ECC L is 01
|
|
73
|
+
let rem = d;
|
|
74
|
+
for (let i = 0; i < 10; i++) rem = (rem << 1) ^ ((rem >>> 9) * 0x537);
|
|
75
|
+
const f = ((d << 10) | rem) ^ 0x5412;
|
|
76
|
+
const bit = (i: number) => ((f >>> i) & 1) === 1;
|
|
77
|
+
for (let i = 0; i <= 5; i++) set(8, i, bit(i));
|
|
78
|
+
set(8, 7, bit(6)); set(8, 8, bit(7)); set(7, 8, bit(8));
|
|
79
|
+
for (let i = 9; i < 15; i++) set(14 - i, 8, bit(i));
|
|
80
|
+
for (let i = 0; i < 8; i++) set(size - 1 - i, 8, bit(i));
|
|
81
|
+
for (let i = 8; i < 15; i++) set(8, size - 15 + i, bit(i));
|
|
82
|
+
set(8, size - 8, true);
|
|
83
|
+
};
|
|
84
|
+
format(0);
|
|
85
|
+
|
|
86
|
+
let i = 0;
|
|
87
|
+
for (let right = size - 1; right >= 1; right -= 2) {
|
|
88
|
+
if (right === 6) right = 5;
|
|
89
|
+
for (let vert = 0; vert < size; vert++) for (let j = 0; j < 2; j++) {
|
|
90
|
+
const x = right - j, y = ((right + 1) & 2) === 0 ? size - 1 - vert : vert;
|
|
91
|
+
if (!fn[y]![x] && i < words.length * 8) { m[y]![x] = ((words[i >> 3]! >>> (7 - (i & 7))) & 1) === 1; i++; }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const MASKS = [
|
|
96
|
+
(x: number, y: number) => (x + y) % 2 === 0, (_: number, y: number) => y % 2 === 0,
|
|
97
|
+
(x: number) => x % 3 === 0, (x: number, y: number) => (x + y) % 3 === 0,
|
|
98
|
+
(x: number, y: number) => (Math.floor(x / 3) + Math.floor(y / 2)) % 2 === 0,
|
|
99
|
+
(x: number, y: number) => ((x * y) % 2) + ((x * y) % 3) === 0,
|
|
100
|
+
(x: number, y: number) => (((x * y) % 2) + ((x * y) % 3)) % 2 === 0,
|
|
101
|
+
(x: number, y: number) => (((x + y) % 2) + ((x * y) % 3)) % 2 === 0,
|
|
102
|
+
];
|
|
103
|
+
// ponytail: penalty counts only runs and 2x2 blocks (rules 1-2 of 4); every mask
|
|
104
|
+
// decodes, this just picks a calmer one. Add the other two rules if scans fail.
|
|
105
|
+
const penalty = (g: boolean[][]) => {
|
|
106
|
+
let p = 0;
|
|
107
|
+
for (let a = 0; a < size; a++) for (const row of [true, false]) {
|
|
108
|
+
let run = 1;
|
|
109
|
+
for (let b = 1; b < size; b++) {
|
|
110
|
+
const same = row ? g[a]![b] === g[a]![b - 1] : g[b]![a] === g[b - 1]![a];
|
|
111
|
+
run = same ? run + 1 : 1;
|
|
112
|
+
if (run === 5) p += 3; else if (run > 5) p++;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
for (let y = 0; y < size - 1; y++) for (let x = 0; x < size - 1; x++) {
|
|
116
|
+
if (g[y]![x] === g[y]![x + 1] && g[y]![x] === g[y + 1]![x] && g[y]![x] === g[y + 1]![x + 1]) p += 3;
|
|
117
|
+
}
|
|
118
|
+
return p;
|
|
119
|
+
};
|
|
120
|
+
const masked = (k: number) => m.map((row, y) => row.map((d, x) => (!fn[y]![x] && MASKS[k]!(x, y) ? !d : d)));
|
|
121
|
+
let best = 0, bestP = Infinity;
|
|
122
|
+
for (let k = 0; k < 8; k++) { const p = penalty(masked(k)); if (p < bestP) { bestP = p; best = k; } }
|
|
123
|
+
format(best);
|
|
124
|
+
return masked(best);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function qrMatrix(text: string): boolean[][] {
|
|
128
|
+
const bytes = [...new TextEncoder().encode(text)];
|
|
129
|
+
const v = VERSIONS.findIndex(([total, ec, n]) => total - ec * n >= bytes.length + 2);
|
|
130
|
+
if (v < 0) throw new Error("text too long for qr()");
|
|
131
|
+
const [total, ecLen, nBlocks] = VERSIONS[v]!;
|
|
132
|
+
const dataLen = total - ecLen * nBlocks;
|
|
133
|
+
const size = 17 + 4 * (v + 1);
|
|
134
|
+
|
|
135
|
+
const bits: number[] = [];
|
|
136
|
+
const put = (val: number, n: number) => { for (let i = n - 1; i >= 0; i--) bits.push((val >>> i) & 1); };
|
|
137
|
+
put(4, 4); put(bytes.length, 8); bytes.forEach((b) => put(b, 8));
|
|
138
|
+
put(0, Math.min(4, dataLen * 8 - bits.length));
|
|
139
|
+
while (bits.length % 8) bits.push(0);
|
|
140
|
+
const data: number[] = [];
|
|
141
|
+
for (let i = 0; i < bits.length; i += 8) data.push(parseInt(bits.slice(i, i + 8).join(""), 2));
|
|
142
|
+
for (let pad = 0xec; data.length < dataLen; pad ^= 0xec ^ 0x11) data.push(pad);
|
|
143
|
+
|
|
144
|
+
const per = dataLen / nBlocks;
|
|
145
|
+
const blocks = Array.from({ length: nBlocks }, (_, i) => data.slice(i * per, (i + 1) * per));
|
|
146
|
+
const ecs = blocks.map((b) => rsRemainder(b, ecLen));
|
|
147
|
+
const words: number[] = [];
|
|
148
|
+
for (let i = 0; i < per; i++) blocks.forEach((b) => words.push(b[i]!));
|
|
149
|
+
for (let i = 0; i < ecLen; i++) ecs.forEach((e) => words.push(e[i]!));
|
|
150
|
+
|
|
151
|
+
const m: boolean[][] = Array.from({ length: size }, () => Array(size).fill(false));
|
|
152
|
+
const fn: boolean[][] = Array.from({ length: size }, () => Array(size).fill(false));
|
|
153
|
+
const set = (x: number, y: number, dark: boolean) => { m[y]![x] = dark; fn[y]![x] = true; };
|
|
154
|
+
|
|
155
|
+
for (let i = 0; i < size; i++) { set(6, i, i % 2 === 0); set(i, 6, i % 2 === 0); }
|
|
156
|
+
for (const [cx, cy] of [[3, 3], [size - 4, 3], [3, size - 4]] as const) {
|
|
157
|
+
for (let dy = -4; dy <= 4; dy++) for (let dx = -4; dx <= 4; dx++) {
|
|
158
|
+
const x = cx + dx, y = cy + dy, d = Math.max(Math.abs(dx), Math.abs(dy));
|
|
159
|
+
if (x >= 0 && x < size && y >= 0 && y < size) set(x, y, d !== 2 && d !== 4);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const al = ALIGN[v]!;
|
|
163
|
+
for (const cy of al) for (const cx of al) {
|
|
164
|
+
if ((cx === 6 && cy === 6) || (cx === 6 && cy === al.at(-1)) || (cx === al.at(-1) && cy === 6)) continue;
|
|
165
|
+
for (let dy = -2; dy <= 2; dy++) for (let dx = -2; dx <= 2; dx++) set(cx + dx, cy + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const format = (mask: number) => {
|
|
169
|
+
const d = (1 << 3) | mask; // ECC L is 01
|
|
170
|
+
let rem = d;
|
|
171
|
+
for (let i = 0; i < 10; i++) rem = (rem << 1) ^ ((rem >>> 9) * 0x537);
|
|
172
|
+
const f = ((d << 10) | rem) ^ 0x5412;
|
|
173
|
+
const bit = (i: number) => ((f >>> i) & 1) === 1;
|
|
174
|
+
for (let i = 0; i <= 5; i++) set(8, i, bit(i));
|
|
175
|
+
set(8, 7, bit(6)); set(8, 8, bit(7)); set(7, 8, bit(8));
|
|
176
|
+
for (let i = 9; i < 15; i++) set(14 - i, 8, bit(i));
|
|
177
|
+
for (let i = 0; i < 8; i++) set(size - 1 - i, 8, bit(i));
|
|
178
|
+
for (let i = 8; i < 15; i++) set(8, size - 15 + i, bit(i));
|
|
179
|
+
set(8, size - 8, true);
|
|
180
|
+
};
|
|
181
|
+
format(0);
|
|
182
|
+
|
|
183
|
+
let i = 0;
|
|
184
|
+
for (let right = size - 1; right >= 1; right -= 2) {
|
|
185
|
+
if (right === 6) right = 5;
|
|
186
|
+
for (let vert = 0; vert < size; vert++) for (let j = 0; j < 2; j++) {
|
|
187
|
+
const x = right - j, y = ((right + 1) & 2) === 0 ? size - 1 - vert : vert;
|
|
188
|
+
if (!fn[y]![x] && i < words.length * 8) { m[y]![x] = ((words[i >> 3]! >>> (7 - (i & 7))) & 1) === 1; i++; }
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const MASKS = [
|
|
193
|
+
(x: number, y: number) => (x + y) % 2 === 0, (_: number, y: number) => y % 2 === 0,
|
|
194
|
+
(x: number) => x % 3 === 0, (x: number, y: number) => (x + y) % 3 === 0,
|
|
195
|
+
(x: number, y: number) => (Math.floor(x / 3) + Math.floor(y / 2)) % 2 === 0,
|
|
196
|
+
(x: number, y: number) => ((x * y) % 2) + ((x * y) % 3) === 0,
|
|
197
|
+
(x: number, y: number) => (((x * y) % 2) + ((x * y) % 3)) % 2 === 0,
|
|
198
|
+
(x: number, y: number) => (((x + y) % 2) + ((x * y) % 3)) % 2 === 0,
|
|
199
|
+
];
|
|
200
|
+
// ponytail: penalty counts only runs and 2x2 blocks (rules 1-2 of 4); every mask
|
|
201
|
+
// decodes, this just picks a calmer one. Add the other two rules if scans fail.
|
|
202
|
+
const penalty = (g: boolean[][]) => {
|
|
203
|
+
let p = 0;
|
|
204
|
+
for (let a = 0; a < size; a++) for (const row of [true, false]) {
|
|
205
|
+
let run = 1;
|
|
206
|
+
for (let b = 1; b < size; b++) {
|
|
207
|
+
const same = row ? g[a]![b] === g[a]![b - 1] : g[b]![a] === g[b - 1]![a];
|
|
208
|
+
run = same ? run + 1 : 1;
|
|
209
|
+
if (run === 5) p += 3; else if (run > 5) p++;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
for (let y = 0; y < size - 1; y++) for (let x = 0; x < size - 1; x++) {
|
|
213
|
+
if (g[y]![x] === g[y]![x + 1] && g[y]![x] === g[y + 1]![x] && g[y]![x] === g[y + 1]![x + 1]) p += 3;
|
|
214
|
+
}
|
|
215
|
+
return p;
|
|
216
|
+
};
|
|
217
|
+
const masked = (k: number) => m.map((row, y) => row.map((d, x) => (!fn[y]![x] && MASKS[k]!(x, y) ? !d : d)));
|
|
218
|
+
let best = 0, bestP = Infinity;
|
|
219
|
+
for (let k = 0; k < 8; k++) { const p = penalty(masked(k)); if (p < bestP) { bestP = p; best = k; } }
|
|
220
|
+
format(best);
|
|
221
|
+
const out = masked(best);
|
|
222
|
+
// masked() copied the format bits drawn for mask 0; redraw them into the result.
|
|
223
|
+
const fm = m;
|
|
224
|
+
for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) if (fn[y]![x]) out[y]![x] = fm[y]![x]!;
|
|
225
|
+
return out;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Light modules are the filled blocks, so a dark terminal shows dark modules on a
|
|
229
|
+
// white field with a two-module quiet zone, which is what phone cameras expect.
|
|
230
|
+
export function qr(text: string): string {
|
|
231
|
+
const g = qrMatrix(text), q = 2, n = g.length + 2 * q;
|
|
232
|
+
const dark = (x: number, y: number) => g[y - q]?.[x - q] === true;
|
|
233
|
+
const lines: string[] = [];
|
|
234
|
+
for (let y = 0; y < n; y += 2) {
|
|
235
|
+
let line = "";
|
|
236
|
+
for (let x = 0; x < n; x++) {
|
|
237
|
+
const t = dark(x, y), b = y + 1 < n && dark(x, y + 1);
|
|
238
|
+
line += t ? (b ? " " : "▄") : (b ? "▀" : "█");
|
|
239
|
+
}
|
|
240
|
+
lines.push(line);
|
|
241
|
+
}
|
|
242
|
+
return lines.join("\n");
|
|
243
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { test, expect } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync, existsSync, readdirSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { qrMatrix, qr } from "./qr.ts";
|
|
6
|
+
import { tailnetName } from "./setup.ts";
|
|
7
|
+
|
|
8
|
+
test("qr: right size per version, finder squares in three corners, deterministic", () => {
|
|
9
|
+
const a = qrMatrix("https://pc.tail1234.ts.net"); // 26 bytes: version 2, 25 modules
|
|
10
|
+
expect(a.length).toBe(25);
|
|
11
|
+
for (const [x, y] of [[0, 0], [18, 0], [0, 18]] as const) {
|
|
12
|
+
expect(a[y]![x]).toBe(true); expect(a[y + 3]![x + 3]).toBe(true); expect(a[y + 1]![x + 1]).toBe(false);
|
|
13
|
+
}
|
|
14
|
+
expect(qr("https://pc.tail1234.ts.net")).toBe(qr("https://pc.tail1234.ts.net"));
|
|
15
|
+
expect(qrMatrix("https://a.b").length).toBe(21); // version 1
|
|
16
|
+
expect(qrMatrix("x".repeat(130)).length).toBe(41); // version 6
|
|
17
|
+
expect(() => qrMatrix("x".repeat(200))).toThrow();
|
|
18
|
+
const lines = qr("https://a.b").split("\n");
|
|
19
|
+
expect(lines.length).toBe(13); // (21 + 4 quiet) / 2 rows, rounded up
|
|
20
|
+
expect(new Set(lines.map((l) => l.length))).toEqual(new Set([25]));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("tailnetName: the machine's DNS name only when Tailscale is running", () => {
|
|
24
|
+
const up = JSON.stringify({ BackendState: "Running", Self: { DNSName: "PC.tail1234.ts.net." } });
|
|
25
|
+
expect(tailnetName(up)).toBe("pc.tail1234.ts.net");
|
|
26
|
+
expect(tailnetName(JSON.stringify({ BackendState: "NeedsLogin", Self: { DNSName: "pc.x.ts.net." } }))).toBeNull();
|
|
27
|
+
expect(tailnetName(JSON.stringify({ BackendState: "Running" }))).toBeNull();
|
|
28
|
+
expect(tailnetName("not json")).toBeNull();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Scratch HOME only: setup must never touch the real ~/.kaizen.
|
|
32
|
+
test("kaizen setup without a terminal prints the manual steps and writes nothing", async () => {
|
|
33
|
+
const homeDir = mkdtempSync(join(tmpdir(), "kaizen-setup-"));
|
|
34
|
+
try {
|
|
35
|
+
const p = Bun.spawn([process.execPath, join(import.meta.dir, "install.ts"), "setup"], {
|
|
36
|
+
cwd: homeDir, env: { ...process.env, HOME: homeDir, USERPROFILE: homeDir }, stdin: "ignore", stdout: "pipe", stderr: "pipe",
|
|
37
|
+
});
|
|
38
|
+
const out = await new Response(p.stdout).text();
|
|
39
|
+
expect(await p.exited).toBe(0);
|
|
40
|
+
expect(out).toContain("tailscale serve --bg 127.0.0.1:7420");
|
|
41
|
+
expect(readdirSync(homeDir)).toEqual([]);
|
|
42
|
+
expect(existsSync(join(homeDir, ".kaizen"))).toBe(false);
|
|
43
|
+
} finally { rmSync(homeDir, { recursive: true, force: true }); }
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("web-hosts: names are normalized, deduped and merged with the env var", async () => {
|
|
47
|
+
const homeDir = mkdtempSync(join(tmpdir(), "kaizen-hosts2-"));
|
|
48
|
+
try {
|
|
49
|
+
const p = Bun.spawn([process.execPath, "-e", `
|
|
50
|
+
const s = await import(${JSON.stringify(join(import.meta.dir, "state.ts"))});
|
|
51
|
+
s.addWebHost("PC.Tail1.ts.net"); s.addWebHost("pc.tail1.ts.net");
|
|
52
|
+
console.log(JSON.stringify([...s.webHosts()].sort()));`], {
|
|
53
|
+
env: { ...process.env, HOME: homeDir, USERPROFILE: homeDir, KAIZEN_WEB_HOSTS: "Env.ts.net:443" }, stdout: "pipe",
|
|
54
|
+
});
|
|
55
|
+
expect(JSON.parse(await new Response(p.stdout).text())).toEqual(["env.ts.net", "pc.tail1.ts.net"]);
|
|
56
|
+
} finally { rmSync(homeDir, { recursive: true, force: true }); }
|
|
57
|
+
});
|
package/cli/setup.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// `kaizen setup`: the walk after install — a project, the settings, the board, the
|
|
2
|
+
// phone. Every step can be skipped, and nothing changes without an answer.
|
|
3
|
+
import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { addWebHost, home, remember } from "./state.ts";
|
|
6
|
+
import { applyPreset, detectPreset } from "./settings.ts";
|
|
7
|
+
import { qr } from "./qr.ts";
|
|
8
|
+
|
|
9
|
+
const PORT = 7420;
|
|
10
|
+
const INSTALL = join(import.meta.dir, "install.ts");
|
|
11
|
+
|
|
12
|
+
export type Ui = {
|
|
13
|
+
select: <T>(q: string, o: { label: string; hint: string; value: T }[]) => Promise<T>;
|
|
14
|
+
ask: (q: string) => string | null;
|
|
15
|
+
c: Record<"dim" | "bold" | "cyan" | "green", (s: string) => string>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// The DNS name `tailscale status --json` reports for this machine, or null when
|
|
19
|
+
// Tailscale is not running or not logged in.
|
|
20
|
+
export function tailnetName(json: string): string | null {
|
|
21
|
+
try {
|
|
22
|
+
const s = JSON.parse(json);
|
|
23
|
+
const name = String(s?.Self?.DNSName ?? "").replace(/\.$/, "").toLowerCase();
|
|
24
|
+
return s?.BackendState === "Running" && name ? name : null;
|
|
25
|
+
} catch { return null; }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// A folder already inside a project (.git or .kaizen on the way up to home).
|
|
29
|
+
function inProject(dir: string) {
|
|
30
|
+
for (let d = dir; d !== home; ) {
|
|
31
|
+
if (existsSync(join(d, ".git")) || existsSync(join(d, ".kaizen"))) return true;
|
|
32
|
+
const up = resolve(d, "..");
|
|
33
|
+
if (up === d) return false;
|
|
34
|
+
d = up;
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function manual() {
|
|
40
|
+
return `
|
|
41
|
+
kaizen setup needs a terminal. By hand:
|
|
42
|
+
kaizen settings pick a preset
|
|
43
|
+
kaizen web the board
|
|
44
|
+
tailscale serve --bg 127.0.0.1:${PORT} phone, then add the name it prints to ~/.kaizen/web-hosts
|
|
45
|
+
`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function setup(ui: Ui) {
|
|
49
|
+
const { c, select } = ui;
|
|
50
|
+
const out = (s = "") => console.log(s);
|
|
51
|
+
const run = (cmd: string[], cwd?: string) => Bun.spawnSync(cmd, { cwd, stdin: "inherit", stdout: "inherit", stderr: "inherit" }).exitCode === 0;
|
|
52
|
+
|
|
53
|
+
out(`\n ${c.bold("kaizen setup")} ${c.dim("three short steps, each can be skipped")}`);
|
|
54
|
+
|
|
55
|
+
// 1 -- project
|
|
56
|
+
if (!inProject(process.cwd())) {
|
|
57
|
+
out(`\n ${c.bold("1 Project")} ${c.dim("kaizen keeps its runs in a .kaizen/ folder inside your project")}`);
|
|
58
|
+
for (let tries = 0; tries < 2; tries++) {
|
|
59
|
+
const typed = ui.ask(" Project folder (Enter to skip): ")?.trim();
|
|
60
|
+
if (!typed) break;
|
|
61
|
+
const dir = resolve(typed.replace(/^~(?=$|[\\/])/, home));
|
|
62
|
+
if (dir === home || !existsSync(dir) || !statSync(dir).isDirectory()) { out(` ${c.dim(`${typed}: not a folder`)}`); continue; }
|
|
63
|
+
const r = Bun.spawnSync([process.execPath, INSTALL, "--yes", "--init-project"], { cwd: dir });
|
|
64
|
+
if (r.exitCode === 0 && existsSync(join(dir, ".kaizen"))) { remember(dir); out(` ${c.green("+")} set up ${dir}`); }
|
|
65
|
+
else out(` ${c.dim(`could not set up ${dir}`)}`);
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 2 -- settings
|
|
71
|
+
const defaults = readFileSync(join(import.meta.dir, "..", "skills/kaizen/config.default.yml"), "utf8");
|
|
72
|
+
const file = join(home, ".kaizen", "config.yml");
|
|
73
|
+
const current = existsSync(file) ? readFileSync(file, "utf8") : defaults;
|
|
74
|
+
const now = detectPreset(current, defaults);
|
|
75
|
+
const preset = await select(`2 Settings (now: ${now})`, [
|
|
76
|
+
{ label: "Keep", hint: "leave them as they are", value: null },
|
|
77
|
+
{ label: "Medium", hint: "balanced — subagents, plan approval, auto-fix high/critical", value: "medium" as const },
|
|
78
|
+
{ label: "Low", hint: "inline and manual — lite runner, manual approvals, no auto-fix", value: "low" as const },
|
|
79
|
+
{ label: "Ultra", hint: "autonomous — auto mode, auto-fix all findings", value: "ultra" as const },
|
|
80
|
+
]);
|
|
81
|
+
if (preset) { writeFileSync(file, applyPreset(current, preset, defaults)); out(` ${c.green("+")} ${preset} preset in ~/.kaizen/config.yml`); }
|
|
82
|
+
out(` ${c.dim("every setting: kaizen settings, or the gear on the board")}`);
|
|
83
|
+
|
|
84
|
+
// 3 -- board
|
|
85
|
+
out(`\n ${c.bold("3 Board")}`);
|
|
86
|
+
run([process.execPath, INSTALL, "web", "--daemon"]);
|
|
87
|
+
|
|
88
|
+
// 4 -- phone
|
|
89
|
+
out(`\n ${c.bold("4 Phone")} ${c.dim("the board on your phone, over Tailscale")}`);
|
|
90
|
+
let name: string | null = null;
|
|
91
|
+
try { name = tailnetName(Bun.spawnSync(["tailscale", "status", "--json"]).stdout.toString()); } catch { /* not installed */ }
|
|
92
|
+
if (!name) {
|
|
93
|
+
out(` Tailscale is not running here. Install it on this PC and your phone (${c.cyan("https://tailscale.com/download")}),`);
|
|
94
|
+
out(` sign in on both, then run ${c.cyan("kaizen setup")} again.\n`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const yes = await select(`Run tailscale serve so your phone can reach the board?`, [
|
|
98
|
+
{ label: "Not now", hint: "nothing changes", value: false },
|
|
99
|
+
{ label: "Yes", hint: `tailscale serve --bg 127.0.0.1:${PORT} — private to your tailnet`, value: true },
|
|
100
|
+
]);
|
|
101
|
+
if (!yes) { out(); return; }
|
|
102
|
+
if (!run(["tailscale", "serve", "--bg", `127.0.0.1:${PORT}`])) { out(` ${c.dim("tailscale serve failed — see its message above")}\n`); return; }
|
|
103
|
+
addWebHost(name);
|
|
104
|
+
const url = `https://${name}`;
|
|
105
|
+
out(`\n${qr(url)}\n\n Scan with your phone camera, or open ${c.cyan(url)}`);
|
|
106
|
+
out(` Then choose Add to Home screen: it opens full screen with the kaizen icon.`);
|
|
107
|
+
out(` ${c.dim("Anyone on your tailnet who can reach this PC can drive your agents; restrict it with Tailscale ACLs.")}\n`);
|
|
108
|
+
}
|
package/cli/state.test.ts
CHANGED
|
@@ -1019,7 +1019,7 @@ test("the board refuses an upgrade from another origin", async () => {
|
|
|
1019
1019
|
});
|
|
1020
1020
|
|
|
1021
1021
|
// KAIZEN_WEB_HOSTS (tailscale serve): the listed host gets in, any other is still refused.
|
|
1022
|
-
test("the board answers only the hosts listed in KAIZEN_WEB_HOSTS", async () => {
|
|
1022
|
+
test("the board answers only the hosts listed in KAIZEN_WEB_HOSTS or web-hosts", async () => {
|
|
1023
1023
|
const homeDir = mkdtempSync(join(tmpdir(), "kaizen-hosts-"));
|
|
1024
1024
|
for (const d of [join(homeDir, ".kaizen"), join(homeDir, ".claude")]) mkdirSync(d, { recursive: true });
|
|
1025
1025
|
const port = 20000 + Math.floor(Math.random() * 20000), repo = join(import.meta.dir, "..");
|
|
@@ -1032,6 +1032,10 @@ test("the board answers only the hosts listed in KAIZEN_WEB_HOSTS", async () =>
|
|
|
1032
1032
|
expect((await fetch(at + "/state", { headers: { host: "pc.tail1234.ts.net" } })).status).toBe(200);
|
|
1033
1033
|
expect((await fetch(at + "/state", { headers: { host: "evil.example" } })).status).toBe(403);
|
|
1034
1034
|
expect((await fetch(at + "/state", { headers: { host: "other.tail1234.ts.net" } })).status).toBe(403);
|
|
1035
|
+
// ~/.kaizen/web-hosts (written by `kaizen setup`) is read per request: no restart.
|
|
1036
|
+
writeFileSync(join(homeDir, ".kaizen", "web-hosts"), "# phone\nOther.tail1234.ts.net\n");
|
|
1037
|
+
expect((await fetch(at + "/state", { headers: { host: "other.tail1234.ts.net" } })).status).toBe(200);
|
|
1038
|
+
expect((await fetch(at + "/state", { headers: { host: "evil.example" } })).status).toBe(403);
|
|
1035
1039
|
expect((await fetch(at + "/upgrade", { method: "POST", headers: { origin: "https://evil.example" } })).status).toBe(403);
|
|
1036
1040
|
const m = await fetch(at + "/manifest.webmanifest");
|
|
1037
1041
|
expect(m.status).toBe(200);
|
package/cli/state.ts
CHANGED
|
@@ -277,6 +277,22 @@ if ($o -match '"pane_id":"([^"]*)"') { & ${q(herdr)} pane run $Matches[1] ${q(ty
|
|
|
277
277
|
// read and corrected in an editor; blanks and # lines ignored.
|
|
278
278
|
export const REGISTRY = join(home, ".kaizen", "projects");
|
|
279
279
|
|
|
280
|
+
// Names the board answers to besides loopback: KAIZEN_WEB_HOSTS and ~/.kaizen/web-hosts
|
|
281
|
+
// (one per line, written by `kaizen setup`). Read on every call, so a name added while
|
|
282
|
+
// the board runs works at once. Exact match only; a port or case is ignored.
|
|
283
|
+
export const WEB_HOSTS = join(home, ".kaizen", "web-hosts");
|
|
284
|
+
export function webHosts(): Set<string> {
|
|
285
|
+
const raw = (process.env.KAIZEN_WEB_HOSTS ?? "").split(",");
|
|
286
|
+
try { raw.push(...readFileSync(WEB_HOSTS, "utf8").split("\n")); } catch { /* none yet */ }
|
|
287
|
+
return new Set(raw.map((s) => s.trim().toLowerCase().replace(/:\d+$/, "")).filter((l) => l && !l.startsWith("#")));
|
|
288
|
+
}
|
|
289
|
+
export function addWebHost(name: string) {
|
|
290
|
+
name = name.trim().toLowerCase();
|
|
291
|
+
if (!name || webHosts().has(name)) return;
|
|
292
|
+
mkdirSync(dirname(WEB_HOSTS), { recursive: true });
|
|
293
|
+
appendFileSync(WEB_HOSTS, name + "\n");
|
|
294
|
+
}
|
|
295
|
+
|
|
280
296
|
// Windows spreads work across drives -- the profile on C:, the projects on D: or E: --
|
|
281
297
|
// and a search that only knows $HOME finds none of them. Every letter that answers is
|
|
282
298
|
// a root worth walking; elsewhere there is one filesystem and $HOME is where work
|
package/cli/web.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
readInbox, writeInbox, replaceIdea, moveIdea, abandonRun, launchRun, parseItem, agentChoices, projectOf, KNOWN_AGENTS, plainModel, short, setArchived, writeNotes,
|
|
8
8
|
appendNote, saveAttachment, pidOnPort, readText, cmdExe, sysExe, powershellExe, runGit, cardsGit, readCommit, commitPush, notice, notifier, LOGO,
|
|
9
9
|
reviewFindings, pickFindings, fixPrompt, approvalPrompt, canApproveAwaiting, nativePath, closeSessions, sweepSessions, issueUrl,
|
|
10
|
-
launchMode, readSessions, ownerOf, resumePrompt, runAgent, firstSession,
|
|
10
|
+
launchMode, readSessions, ownerOf, resumePrompt, runAgent, firstSession, webHosts,
|
|
11
11
|
} from "./state.ts";
|
|
12
12
|
import { attachPty, writePty, resizePty, ptyState, ptyKeys, adoptLoginEnv } from "./pty.ts";
|
|
13
13
|
import { readSources, addSource, removeSource, moveSource, setCategory, setWriteback, readIcebox, promote, dismiss, restore, markDone, markReplied, gatherSource, freshImages, gatherAll, gatherDirs, due, socialUrl, socialOf } from "./sources.ts";
|
|
@@ -132,10 +132,9 @@ export async function web(repoDir: string, opts: Opts = {}) {
|
|
|
132
132
|
: [cur]
|
|
133
133
|
).filter((d, i, arr) => arr.indexOf(d) === i && existsSync(d));
|
|
134
134
|
|
|
135
|
-
// Hostnames the user
|
|
136
|
-
//
|
|
137
|
-
const extraHosts =
|
|
138
|
-
.map((s) => s.trim().toLowerCase().replace(/:\d+$/, "")).filter(Boolean));
|
|
135
|
+
// Hostnames the user opted into (KAIZEN_WEB_HOSTS or ~/.kaizen/web-hosts, e.g. a
|
|
136
|
+
// `tailscale serve` name): matched exactly, empty when neither is set.
|
|
137
|
+
const extraHosts = { has: (h: string) => webHosts().has(h) };
|
|
139
138
|
|
|
140
139
|
// Any tab on this machine can reach loopback. GET only reads the user's own
|
|
141
140
|
// files, but a POST from a page on the wider web must not write them.
|
package/package.json
CHANGED
package/web/board.html
CHANGED
|
@@ -189,7 +189,7 @@ section h2 .clear{margin-left:auto;min-height:22px;padding:0 8px}
|
|
|
189
189
|
.notes{display:flex;flex-direction:column;gap:8px}
|
|
190
190
|
#pbody img{max-width:100%;border-radius:var(--r-s)}
|
|
191
191
|
/* the in-card agent: a wider panel, and the terminal filling its body */
|
|
192
|
-
aside.wide{width:100%;border-left:0}#pbody:has(.term){display:flex;flex-direction:column}#pbody .term{flex:1;min-height:0;min-height:320px;background:var(--bg);border-radius:var(--r-s)}#pbody .term .xterm{height:100%;padding:4px}#pbody .term .xterm-viewport{background:transparent!important}.termkeys{
|
|
192
|
+
aside.wide{width:100%;border-left:0}#pbody:has(.term){display:flex;flex-direction:column}#pbody .term{flex:1;min-height:0;min-height:320px;background:var(--bg);border-radius:var(--r-s)}#pbody .term .xterm{height:100%;padding:4px}#pbody .term .xterm-viewport{background:transparent!important}.termkeys{padding:6px 0}.termkeys button{height:52px;padding:0;border:1px solid var(--line);border-bottom-width:4px;border-radius:10px;background:var(--bg-2);color:var(--ink-hi);font:600 17px var(--mono);touch-action:manipulation}.termkeys button:active{transform:translateY(2px);border-bottom-width:2px;background:var(--hover)}.termkeys button.on{color:var(--on-accent);background:var(--accent);border-color:var(--accent)}.keygrid{display:grid;grid-template-columns:repeat(6,1fr);gap:8px}.ctrlpad{margin-bottom:8px}.ctrlpad div{display:flex;gap:4px;margin-bottom:5px}.ctrlpad div:nth-child(2){padding:0 5%}.ctrlpad div:nth-child(3){padding:0 10%}.ctrlpad button{flex:1;min-width:0;height:44px;font-size:15px}.termkeys [hidden]{display:none}.keytop{display:flex;gap:8px;margin-bottom:6px}.keytop button{flex:1;height:30px;border-bottom-width:1px;font-size:14px}
|
|
193
193
|
.desc{border-bottom:1px solid var(--line);padding-bottom:6px;margin-bottom:14px}.desc h3{margin-top:14px}
|
|
194
194
|
.chat{display:flex;flex-direction:column;gap:10px;margin:0 0 14px}.msg{border:1px solid var(--line);border-radius:var(--r);padding:10px 12px;background:var(--bg)}.msg time{display:block;font-size:11px;color:var(--dim);margin-bottom:4px}.msg p:last-child{margin-bottom:0}.msg img{max-width:100%;max-height:260px;border-radius:var(--r-s);display:block;margin:4px 0}
|
|
195
195
|
.notes .tools{display:flex;gap:6px;align-items:center}.notes .tools span{flex:1;font-size:11px;color:var(--dim)}.notes .att{font-size:11px;color:var(--dim)}
|
|
@@ -413,6 +413,8 @@ body:not(.paged) .src-sec,body:not(.paged) .src-scrim{display:none}
|
|
|
413
413
|
.src-add .acts .btn{flex-shrink:0}
|
|
414
414
|
@media(max-width:480px){.src-add .two,.src-add .two.plat{grid-template-columns:1fr}}
|
|
415
415
|
@media(max-width:900px){main{grid-template-columns:1fr;padding-inline:16px;gap:8px}.hdr,.ftr,aside.docked>header,aside.docked>.body{padding-inline:16px}.more{display:grid}.card:hover .age{display:inline}.toast{left:16px;max-width:calc(100% - 32px)}}
|
|
416
|
+
@media(max-width:640px){.hdr{gap:8px 12px}.hdr h1{order:0;flex:1 0 50%}#newtop{order:1}.hdr .combo{order:2;flex:1 1 calc(50% - 6px);min-width:0}.hdr .combo>.sel{width:100%;max-width:none}.hdr .sp{display:none}.ftr{gap:6px 12px}}
|
|
417
|
+
@media(hover:none){#kbd{display:none}}
|
|
416
418
|
@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}
|
|
417
419
|
</style>
|
|
418
420
|
</head>
|
package/web/board.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* kaizen board
|
|
1
|
+
/* kaizen board 15a573a6194b8517 -- generated by bun run build:web from web/board/main.ts; do not edit */
|
|
2
2
|
(()=>{var a={S:{project:null,projects:[],cards:[],now:Date.now(),all:!1},all:new URLSearchParams(location.search).get("all")!=="0",dir:null,openKey:null,mode:null,modeKey:null,menuKey:null,tab:"request",lastTab:"request",runDoc:null,clearMenu:!1,wantTerm:null,setQ:"",pageGen:0,PL:[],shown:new Set((()=>{try{return JSON.parse(localStorage.getItem("kz-show"))||[]}catch(e){return[]}})())};var t=(e)=>document.getElementById(e),p=(e)=>String(e).replace(/[&<>"]/g,(n)=>({"&":"&","<":"<",">":">",'"':"""})[n]),T=(e)=>`<svg><use href="#i-${e}"/></svg>`;var en;function S(e,n){let s=t("toast");if(s.textContent=e,s.classList.toggle("rep",!!n),n){let l=document.createElement("a");l.href="/issue?"+new URLSearchParams(n),l.target="_blank",l.rel="noopener",l.textContent="Report",s.append(" ",l)}s.classList.add("on"),clearTimeout(en),en=setTimeout(()=>s.classList.remove("on"),n?8000:4000)}function me(){try{if(typeof Notification<"u"&&Notification.permission==="default")Notification.requestPermission()}catch(e){}}function tn(e){try{if(typeof Notification<"u"&&Notification.permission==="granted")new Notification("kaizen",{body:e,icon:"/logo.png"})}catch(n){}}var Vn="https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.3.0",nn={tiny:"onnx-community/whisper-tiny",base:"onnx-community/whisper-base",small:"onnx-community/whisper-small","large-v3-turbo":"onnx-community/whisper-large-v3-turbo"},$t=null,Jn=`import{pipeline}from"${Vn}";let asr=null;
|
|
3
3
|
onmessage=async e=>{const m=e.data;try{
|
|
4
4
|
if(m.type==="load"){const prog=p=>{if(p.status==="progress")postMessage({type:"progress",file:p.file,pct:p.progress||0})};
|
package/web/notebook.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* kaizen notebook
|
|
1
|
+
/* kaizen notebook 15a573a6194b8517 -- generated by bun run build:web from web/notebook.ts; do not edit */
|
|
2
2
|
var Ss=[],ua=[];(()=>{let e="lc,34,7n,7,7b,19,,,,2,,2,,,20,b,1c,l,g,,2t,7,2,6,2,2,,4,z,,u,r,2j,b,1m,9,9,,o,4,,9,,3,,5,17,3,1n,9,16,o,,x,1i,3,,i,,7,a,2,t,3,1k,,,7,2,2,2,3,9,,a,2,q,,2,3,1k,,,5,4,2,2,3,3,,u,2,3,,b,3,1k,,,8,,3,,3,k,2,m,6,,3,1k,,,7,2,2,2,3,7,3,a,2,u,,1n,5,3,3,,4,9,,14,5,1j,,,7,,3,,4,7,2,b,2,t,3,1k,,,7,,3,,4,7,2,b,2,f,,c,4,1j,2,,7,,3,,4,9,,a,2,t,3,1y,,4,6,,,,8,i,2,1p,,,8,c,8,2q,,,a,b,7,21,2,r,,,,,,4,2,1d,k,,2,5,b,,10,9,,2u,b,,6,n,4,4,3,g,4,d,,,3,6,,f,,jj,3,qa,4,s,3,t,2,u,2,1s,w,9,,19,3,,,39,2,y,,3a,c,4,c,63,5,1l,a,,,,,2,o,2,,1c,1a,2,c,k,5,1b,h,12,9,c,3,u,d,1k,e,1c,k,48,3,,l,4,,6,,2,3,5i,1s,ek,,5f,x,2da,3,3x,,2o,w,fe,6,2x,2,n9w,4,,a,w,2,28,2,7k,,3,,4,,n,5,4,,2b,2,1e,i,q,i,d,,12,8,p,d,18,4,1b,e,10,,1v,e,c,,8,2,1a,,1f,,,3,2,2,5,2,,,15,5,5,2,6k,8,,2,fn4,,kh,g,g,g,a6,2,gt,,6a,,45,5,1ae,3,,2,5,4,14,3,4,,4l,2,fx,4,1t,5,8t,2,25,6,1y,b,1d,4,3e,3,1h,f,15,,2,2,a,4,19,b,7,,1p,3,10,e,g,2,18,,c,3,1c,e,8,4,,2,2k,c,6,,2,,4d,c,l,4,1j,2,,7,2,2,2,3,9,,a,2,2,7,3,5,1v,9,,,2,,,4,,5,,,e,2,2a,i,n,,29,k,6j,7,2,9,r,2,2a,h,2y,d,2t,3,2,a,74,f,6t,6,,2,2,4,,,,2,3x,7,2,7,3,,s,a,14,7,,4,8,,9,b,1a,g,5i,8,5j,8,,8,2a,m,,e,3e,6,3,,,2,,7,,,1u,5,,2,,5,9n,4,9,2,,,1c,7,3,5,n,,44l,,6,f,8ug,i,1xc,5,1n,7,t4,,,1j,7,4,29,,b,2,f57,2,3mp,1a,2,n,f2,5,3,6,8,8,2,7,u,4,44,3,1iz,1j,4,1e,8,,e,,m,5,,f,11s,7,,h,2,7,,2,,5,2s,,4g,7,af,,1p,4,e4,4,72,2,6r,,2,,7,2,5,,d6,7,31,7,240,5".split(",").map((t)=>t?parseInt(t,36):1);for(let t=0,i=0;t<e.length;t++)(t%2?ua:Ss).push(i=i+e[t])})();function op(e){if(e<768)return!1;for(let t=0,i=Ss.length;;){let n=t+i>>1;if(e<Ss[n])i=n;else if(e>=ua[n])t=n+1;else return!0;if(t==i)return!1}}function ha(e){return e>=127462&&e<=127487}var fa=8205;function da(e,t,i=!0,n=!0){return(i?Oa:lp)(e,t,n)}function Oa(e,t,i){if(t==e.length)return t;if(t&&pa(e.charCodeAt(t))&&ma(e.charCodeAt(t-1)))t--;let n=bs(e,t);t+=ca(n);while(t<e.length){let r=bs(e,t);if(n==fa||r==fa||i&&op(r))t+=ca(r),n=r;else if(ha(r)){let s=0,o=t-2;while(o>=0&&ha(bs(e,o)))s++,o-=2;if(s%2==0)break;else t+=2}else break}return t}function lp(e,t,i){while(t>1){let n=Oa(e,t-2,i);if(n<t)return n;t--}return 0}function bs(e,t){let i=e.charCodeAt(t);if(!ma(i)||t+1==e.length)return i;let n=e.charCodeAt(t+1);if(!pa(n))return i;return(i-55296<<10)+(n-56320)+65536}function pa(e){return e>=56320&&e<57344}function ma(e){return e>=55296&&e<56320}function ca(e){return e<65536?1:2}class _{lineAt(e){if(e<0||e>this.length)throw RangeError(`Invalid position ${e} in document of length ${this.length}`);return this.lineInner(e,!1,1,0)}line(e){if(e<1||e>this.lines)throw RangeError(`Invalid line number ${e} in ${this.lines}-line document`);return this.lineInner(e,!0,1,0)}replace(e,t,i){[e,t]=vi(this,e,t);let n=[];if(this.decompose(0,e,n,2),i.length)i.decompose(0,i.length,n,3);return this.decompose(t,this.length,n,1),ft.from(n,this.length-(t-e)+i.length)}append(e){return this.replace(this.length,this.length,e)}slice(e,t=this.length){[e,t]=vi(this,e,t);let i=[];return this.decompose(e,t,i,0),ft.from(i,t-e)}eq(e){if(e==this)return!0;if(e.length!=this.length||e.lines!=this.lines)return!1;let t=this.scanIdentical(e,1),i=this.length-this.scanIdentical(e,-1),n=new wi(this),r=new wi(e);for(let s=t,o=t;;){if(n.next(s),r.next(s),s=0,n.lineBreak!=r.lineBreak||n.done!=r.done||n.value!=r.value)return!1;if(o+=n.value.length,n.done||o>=i)return!0}}iter(e=1){return new wi(this,e)}iterRange(e,t=this.length){return new Ts(this,e,t)}iterLines(e,t){let i;if(e==null)i=this.iter();else{if(t==null)t=this.lines+1;let n=this.line(e).from;i=this.iterRange(n,Math.max(n,t==this.lines+1?this.length:t<=1?0:this.line(t-1).to))}return new As(i)}toString(){return this.sliceString(0)}toJSON(){let e=[];return this.flatten(e),e}constructor(){}static of(e){if(e.length==0)throw RangeError("A document must have at least one line");if(e.length==1&&!e[0])return _.empty;return e.length<=32?new oe(e):ft.from(oe.split(e,[]))}}class oe extends _{constructor(e,t=ap(e)){super();this.text=e,this.length=t}get lines(){return this.text.length}get children(){return null}lineInner(e,t,i,n){for(let r=0;;r++){let s=this.text[r],o=n+s.length;if((t?i:o)>=e)return new ka(n,o,i,s);n=o+1,i++}}decompose(e,t,i,n){let r=e<=0&&t>=this.length?this:new oe(ga(this.text,e,t),Math.min(t,this.length)-Math.max(0,e));if(n&1){let s=i.pop(),o=cr(r.text,s.text.slice(),0,r.length);if(o.length<=32)i.push(new oe(o,s.length+r.length));else{let l=o.length>>1;i.push(new oe(o.slice(0,l)),new oe(o.slice(l)))}}else i.push(r)}replace(e,t,i){if(!(i instanceof oe))return super.replace(e,t,i);[e,t]=vi(this,e,t);let n=cr(this.text,cr(i.text,ga(this.text,0,e)),t),r=this.length+i.length-(t-e);if(n.length<=32)return new oe(n,r);return ft.from(oe.split(n,[]),r)}sliceString(e,t=this.length,i=`
|
|
3
3
|
`){[e,t]=vi(this,e,t);let n="";for(let r=0,s=0;r<=t&&s<this.text.length;s++){let o=this.text[s],l=r+o.length;if(r>e&&s)n+=i;if(e<l&&t>r)n+=o.slice(Math.max(0,e-r),t-r);r=l+1}return n}flatten(e){for(let t of this.text)e.push(t)}scanIdentical(){return 0}static split(e,t){let i=[],n=-1;for(let r of e)if(i.push(r),n+=r.length+1,i.length==32)t.push(new oe(i,n)),i=[],n=-1;if(n>-1)t.push(new oe(i,n));return t}}class ft extends _{constructor(e,t){super();this.children=e,this.length=t,this.lines=0;for(let i of e)this.lines+=i.lines}lineInner(e,t,i,n){for(let r=0;;r++){let s=this.children[r],o=n+s.length,l=i+s.lines-1;if((t?l:o)>=e)return s.lineInner(e,t,i,n);n=o+1,i=l+1}}decompose(e,t,i,n){for(let r=0,s=0;s<=t&&r<this.children.length;r++){let o=this.children[r],l=s+o.length;if(e<=l&&t>=s){let a=n&((s<=e?1:0)|(l>=t?2:0));if(s>=e&&l<=t&&!a)i.push(o);else o.decompose(e-s,t-s,i,a)}s=l+1}}replace(e,t,i){if([e,t]=vi(this,e,t),i.lines<this.lines)for(let n=0,r=0;n<this.children.length;n++){let s=this.children[n],o=r+s.length;if(e>=r&&t<=o){let l=s.replace(e-r,t-r,i),a=this.lines-s.lines+l.lines;if(l.lines<a>>4&&l.lines>a>>6){let h=this.children.slice();return h[n]=l,new ft(h,this.length-(t-e)+i.length)}return super.replace(r,o,l)}r=o+1}return super.replace(e,t,i)}sliceString(e,t=this.length,i=`
|
|
4
4
|
`){[e,t]=vi(this,e,t);let n="";for(let r=0,s=0;r<this.children.length&&s<=t;r++){let o=this.children[r],l=s+o.length;if(s>e&&r)n+=i;if(e<l&&t>s)n+=o.sliceString(e-s,t-s,i);s=l+1}return n}flatten(e){for(let t of this.children)t.flatten(e)}scanIdentical(e,t){if(!(e instanceof ft))return 0;let i=0,[n,r,s,o]=t>0?[0,0,this.children.length,e.children.length]:[this.children.length-1,e.children.length-1,-1,-1];for(;;n+=t,r+=t){if(n==s||r==o)return i;let l=this.children[n],a=e.children[r];if(l!=a)return i+l.scanIdentical(a,t);i+=l.length+1}}static from(e,t=e.reduce((i,n)=>i+n.length+1,-1)){let i=0;for(let u of e)i+=u.lines;if(i<32){let u=[];for(let d of e)d.flatten(u);return new oe(u,t)}let n=Math.max(32,i>>5),r=n<<1,s=n>>1,o=[],l=0,a=-1,h=[];function f(u){let d;if(u.lines>r&&u instanceof ft)for(let O of u.children)f(O);else if(u.lines>s&&(l>s||!l))c(),o.push(u);else if(u instanceof oe&&l&&(d=h[h.length-1])instanceof oe&&u.lines+d.lines<=32)l+=u.lines,a+=u.length+1,h[h.length-1]=new oe(d.text.concat(u.text),d.length+1+u.length);else{if(l+u.lines>n)c();l+=u.lines,a+=u.length+1,h.push(u)}}function c(){if(l==0)return;o.push(h.length==1?h[0]:ft.from(h,a)),a=-1,l=h.length=0}for(let u of e)f(u);return c(),o.length==1?o[0]:new ft(o,t)}}_.empty=new oe([""],0);function ap(e){let t=-1;for(let i of e)t+=i.length+1;return t}function cr(e,t,i=0,n=1e9){for(let r=0,s=0,o=!0;s<e.length&&r<=n;s++){let l=e[s],a=r+l.length;if(a>=i){if(a>n)l=l.slice(0,n-r);if(r<i)l=l.slice(i-r);if(o)t[t.length-1]+=l,o=!1;else t.push(l)}r=a+1}return t}function ga(e,t,i){return cr(e,[""],t,i)}class wi{constructor(e,t=1){this.dir=t,this.done=!1,this.lineBreak=!1,this.value="",this.nodes=[e],this.offsets=[t>0?1:(e instanceof oe?e.text.length:e.children.length)<<1]}nextInner(e,t){this.done=this.lineBreak=!1;for(;;){let i=this.nodes.length-1,n=this.nodes[i],r=this.offsets[i],s=r>>1,o=n instanceof oe?n.text.length:n.children.length;if(s==(t>0?o:0)){if(i==0)return this.done=!0,this.value="",this;if(t>0)this.offsets[i-1]++;this.nodes.pop(),this.offsets.pop()}else if((r&1)==(t>0?0:1)){if(this.offsets[i]+=t,e==0)return this.lineBreak=!0,this.value=`
|