tanuki-telemetry 1.3.4 → 1.3.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.
Files changed (77) hide show
  1. package/Dockerfile +22 -0
  2. package/bin/tanuki.mjs +414 -0
  3. package/frontend/eslint.config.js +23 -0
  4. package/frontend/index.html +13 -0
  5. package/frontend/package-lock.json +4022 -0
  6. package/frontend/package.json +39 -0
  7. package/frontend/public/favicon.svg +1 -0
  8. package/frontend/public/icons.svg +24 -0
  9. package/frontend/src/App.tsx +232 -0
  10. package/frontend/src/assets/hero.png +0 -0
  11. package/frontend/src/assets/react.svg +1 -0
  12. package/frontend/src/assets/vite.svg +1 -0
  13. package/frontend/src/components/ArtifactsPanel.tsx +429 -0
  14. package/frontend/src/components/ChildStreams.tsx +176 -0
  15. package/frontend/src/components/CoordinatorPage.tsx +390 -0
  16. package/frontend/src/components/Header.tsx +144 -0
  17. package/frontend/src/components/InsightsPanel.tsx +142 -0
  18. package/frontend/src/components/IterationsTable.tsx +98 -0
  19. package/frontend/src/components/KnowledgePage.tsx +308 -0
  20. package/frontend/src/components/LoginPage.tsx +55 -0
  21. package/frontend/src/components/PlanProgress.tsx +163 -0
  22. package/frontend/src/components/QualityReport.tsx +276 -0
  23. package/frontend/src/components/ScreenshotUpload.tsx +117 -0
  24. package/frontend/src/components/ScreenshotsGrid.tsx +266 -0
  25. package/frontend/src/components/SessionDetail.tsx +265 -0
  26. package/frontend/src/components/SessionList.tsx +234 -0
  27. package/frontend/src/components/SettingsPage.tsx +213 -0
  28. package/frontend/src/components/StreamComms.tsx +228 -0
  29. package/frontend/src/components/TanukiLogo.tsx +16 -0
  30. package/frontend/src/components/Timeline.tsx +416 -0
  31. package/frontend/src/components/WalkthroughPage.tsx +458 -0
  32. package/frontend/src/hooks/useApi.ts +81 -0
  33. package/frontend/src/hooks/useAuth.ts +54 -0
  34. package/frontend/src/hooks/useKnowledge.ts +33 -0
  35. package/frontend/src/hooks/useWebSocket.ts +95 -0
  36. package/frontend/src/index.css +66 -0
  37. package/frontend/src/lib/api.ts +15 -0
  38. package/frontend/src/lib/utils.ts +58 -0
  39. package/frontend/src/main.tsx +10 -0
  40. package/frontend/src/types.ts +181 -0
  41. package/frontend/tsconfig.app.json +32 -0
  42. package/frontend/tsconfig.json +7 -0
  43. package/frontend/tsconfig.node.json +26 -0
  44. package/frontend/vite.config.ts +25 -0
  45. package/install.sh +87 -0
  46. package/package.json +54 -19
  47. package/{templates → skills}/cmux-guide.md +5 -14
  48. package/{templates → skills}/compare-image.md +78 -52
  49. package/{templates → skills}/coordinate.md +6 -25
  50. package/{templates → skills}/create-path.md +5 -5
  51. package/{templates → skills}/debug.md +4 -4
  52. package/{templates → skills}/edit-path.md +6 -6
  53. package/{templates → skills}/start-work.md +124 -62
  54. package/{templates → skills}/walkthrough.md +16 -14
  55. package/src/api-keys.ts +97 -0
  56. package/src/auth.ts +165 -0
  57. package/src/coordinator.ts +136 -0
  58. package/src/dashboard-server.ts +5 -0
  59. package/src/dashboard.ts +921 -0
  60. package/src/db.ts +1023 -0
  61. package/src/index.ts +20 -0
  62. package/src/middleware.ts +76 -0
  63. package/src/tools.ts +864 -0
  64. package/src/types-shim.d.ts +18 -0
  65. package/src/types.ts +171 -0
  66. package/tsconfig.json +19 -0
  67. package/README.md +0 -116
  68. package/dist/cli.d.ts +0 -2
  69. package/dist/cli.js +0 -131
  70. package/dist/commands.d.ts +0 -3
  71. package/dist/commands.js +0 -69
  72. package/dist/setup.d.ts +0 -2
  73. package/dist/setup.js +0 -303
  74. package/docker/docker-compose.yml +0 -15
  75. /package/{templates → skills}/review-code.md +0 -0
  76. /package/{templates → skills}/sessions.md +0 -0
  77. /package/{templates → skills}/speak.md +0 -0
package/Dockerfile ADDED
@@ -0,0 +1,22 @@
1
+ FROM node:22-alpine
2
+
3
+ WORKDIR /app
4
+
5
+ # Install backend deps
6
+ COPY package.json ./
7
+ RUN npm install
8
+
9
+ # Build backend
10
+ COPY tsconfig.json ./
11
+ COPY src/ ./src/
12
+ RUN npm run build
13
+
14
+ # Build frontend
15
+ COPY frontend/package.json ./frontend/
16
+ RUN cd frontend && npm install --legacy-peer-deps
17
+ COPY frontend/ ./frontend/
18
+ RUN cd frontend && npx tsc -b && npx vite build
19
+
20
+ RUN mkdir -p /data
21
+
22
+ ENTRYPOINT ["node", "dist/dashboard-server.js"]
package/bin/tanuki.mjs ADDED
@@ -0,0 +1,414 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execSync } from "child_process";
4
+ import fs from "fs";
5
+ import path from "path";
6
+ import os from "os";
7
+ import { createRequire } from "module";
8
+
9
+ const require = createRequire(import.meta.url);
10
+ const pkg = require("../package.json");
11
+
12
+ const VERSION = pkg.version;
13
+ const TANUKI_DIR = process.env.TANUKI_DIR || path.join(os.homedir(), ".tanuki");
14
+ const DATA_DIR = process.env.TANUKI_DATA || path.join(os.homedir(), ".tanuki/data");
15
+ const PORT = process.env.TANUKI_PORT || "3333";
16
+
17
+ const args = process.argv.slice(2);
18
+ const command = args[0] || "start";
19
+
20
+ // --- Colors ---
21
+ const c = {
22
+ reset: "\x1b[0m",
23
+ bold: "\x1b[1m",
24
+ dim: "\x1b[2m",
25
+ green: "\x1b[32m",
26
+ yellow: "\x1b[33m",
27
+ blue: "\x1b[34m",
28
+ magenta: "\x1b[35m",
29
+ cyan: "\x1b[36m",
30
+ white: "\x1b[37m",
31
+ gray: "\x1b[90m",
32
+ red: "\x1b[31m",
33
+ bgGreen: "\x1b[42m",
34
+ bgRed: "\x1b[41m",
35
+ };
36
+
37
+ const g = c.green;
38
+ const d = c.dim;
39
+ const w = c.white;
40
+ const r = c.reset;
41
+ const b = c.bold;
42
+ const y = c.yellow;
43
+ const cy = c.cyan;
44
+
45
+ const LOGO = `
46
+ ${g} /\\ /\\${r}
47
+ ${g} / \\__/ \\${r}
48
+ ${g} | ${w}o${g} __ ${w}o${g} |${r}
49
+ ${g} \\ / \\ /${r}
50
+ ${g} \\/ \\/${r}
51
+ `;
52
+
53
+ const BANNER = ` ${g}${b}TANUKI${r} ${d}v${VERSION}${r}
54
+ ${d}workflow monitor for claude code${r}
55
+ `;
56
+
57
+ const HELP = `
58
+ ${g}${b} tanuki${r} ${d}v${VERSION}${r} — workflow monitor for Claude Code
59
+
60
+ ${w} Usage:${r}
61
+ ${cy}npx tanuki-telemetry${r} start dashboard
62
+ ${cy}npx tanuki-telemetry stop${r} stop dashboard
63
+ ${cy}npx tanuki-telemetry status${r} check if running
64
+ ${cy}npx tanuki-telemetry setup${r} configure claude code
65
+ ${cy}npx tanuki-telemetry update${r} rebuild with latest
66
+ ${cy}npx tanuki-telemetry skills${r} install/update skills
67
+ ${cy}npx tanuki-telemetry version${r} show version
68
+
69
+ ${w} Environment:${r}
70
+ ${d}TANUKI_PORT dashboard port (default: 3333)${r}
71
+ ${d}TANUKI_DATA data directory (default: ~/.tanuki/data)${r}
72
+ `;
73
+
74
+ function log(msg) { console.log(msg); }
75
+ function step(n, total, msg) { log(` ${g}[${n}/${total}]${r} ${msg}`); }
76
+ function ok(msg) { log(` ${d} ${g}✓${r} ${d}${msg}${r}`); }
77
+ function fail(msg) { log(` ${c.red} ✗ ${msg}${r}`); }
78
+ function info(msg) { log(` ${d} ${msg}${r}`); }
79
+
80
+ function run(cmd, opts = {}) {
81
+ try {
82
+ return execSync(cmd, { encoding: "utf-8", stdio: opts.quiet ? "pipe" : "inherit", ...opts });
83
+ } catch (e) {
84
+ if (!opts.ignoreError) {
85
+ fail(e.message);
86
+ process.exit(1);
87
+ }
88
+ return "";
89
+ }
90
+ }
91
+
92
+ function checkRequirements() {
93
+ let allOk = true;
94
+
95
+ try {
96
+ execSync("docker --version", { stdio: "pipe" });
97
+ } catch {
98
+ fail("docker not installed — https://docker.com/products/docker-desktop");
99
+ allOk = false;
100
+ }
101
+
102
+ if (allOk) {
103
+ try {
104
+ execSync("docker info", { stdio: "pipe" });
105
+ } catch {
106
+ fail("docker is not running — start Docker Desktop");
107
+ allOk = false;
108
+ }
109
+ }
110
+
111
+ const nodeVersion = parseInt(process.versions.node.split(".")[0], 10);
112
+ if (nodeVersion < 18) {
113
+ fail(`node ${process.versions.node} — need 18+`);
114
+ allOk = false;
115
+ }
116
+
117
+ try {
118
+ execSync("curl --version", { stdio: "pipe" });
119
+ } catch {
120
+ fail("curl not found");
121
+ allOk = false;
122
+ }
123
+
124
+ // cmux (optional but recommended)
125
+ let hasCmux = false;
126
+ try {
127
+ execSync("cmux --version", { stdio: "pipe" });
128
+ hasCmux = true;
129
+ } catch {}
130
+
131
+ if (!allOk) {
132
+ log("");
133
+ fail("fix the above and try again");
134
+ process.exit(1);
135
+ }
136
+
137
+ ok(`docker, node, curl${hasCmux ? ", cmux" : ""}`);
138
+ if (!hasCmux) {
139
+ info(`${y}cmux not found — needed for /coordinate and workspace skills${r}`);
140
+ info(`${d}install: https://github.com/anthropics/cmux${r}`);
141
+ }
142
+ }
143
+
144
+ function isRunning() {
145
+ try {
146
+ const out = execSync("docker ps --filter name=tanuki-dashboard --format '{{.ID}}'", { encoding: "utf-8", stdio: "pipe" });
147
+ return out.trim().length > 0;
148
+ } catch {
149
+ return false;
150
+ }
151
+ }
152
+
153
+ function copySourceToTanukiDir() {
154
+ const srcDir = path.resolve(path.dirname(import.meta.url.replace("file://", "")), "..");
155
+ if (srcDir === TANUKI_DIR) return;
156
+
157
+ fs.mkdirSync(TANUKI_DIR, { recursive: true });
158
+
159
+ for (const f of ["package.json", "tsconfig.json", "Dockerfile"]) {
160
+ const src = path.join(srcDir, f);
161
+ if (fs.existsSync(src)) fs.copyFileSync(src, path.join(TANUKI_DIR, f));
162
+ }
163
+
164
+ for (const d of ["src", "frontend"]) {
165
+ const src = path.join(srcDir, d);
166
+ if (fs.existsSync(src)) copyDirSync(src, path.join(TANUKI_DIR, d));
167
+ }
168
+ }
169
+
170
+ function copyDirSync(src, dest) {
171
+ fs.mkdirSync(dest, { recursive: true });
172
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
173
+ const srcPath = path.join(src, entry.name);
174
+ const destPath = path.join(dest, entry.name);
175
+ if (["node_modules", "dist", ".git"].includes(entry.name)) continue;
176
+ if (entry.isDirectory()) copyDirSync(srcPath, destPath);
177
+ else fs.copyFileSync(srcPath, destPath);
178
+ }
179
+ }
180
+
181
+ function buildImages() {
182
+ run(`docker build -t tanuki-mcp:latest -t tanuki-dashboard:latest -t telemetry-mcp:latest -t telemetry-dashboard:latest "${TANUKI_DIR}" -q`);
183
+ }
184
+
185
+ function startDashboard() {
186
+ run("docker rm -f tanuki-dashboard 2>/dev/null || true", { quiet: true, ignoreError: true });
187
+ run("docker rm -f telemetry-dashboard 2>/dev/null || true", { quiet: true, ignoreError: true });
188
+ run(`docker run -d --rm --name tanuki-dashboard -p ${PORT}:3333 -v "${DATA_DIR}:/data" tanuki-dashboard:latest`, { quiet: true });
189
+ }
190
+
191
+ function getMcpEntry() {
192
+ return {
193
+ type: "stdio",
194
+ command: "docker",
195
+ args: ["run", "--rm", "-i", "-v", `${DATA_DIR}:/data`, "--entrypoint", "node", "tanuki-mcp:latest", "dist/index.js"]
196
+ };
197
+ }
198
+
199
+ function installSkills() {
200
+ const skillsDir = path.resolve(path.dirname(import.meta.url.replace("file://", "")), "../skills");
201
+ const targetDir = path.join(os.homedir(), ".claude", "commands");
202
+
203
+ if (!fs.existsSync(skillsDir)) {
204
+ info("skills directory not found in package");
205
+ return 0;
206
+ }
207
+
208
+ fs.mkdirSync(targetDir, { recursive: true });
209
+
210
+ const skills = fs.readdirSync(skillsDir).filter((f) => f.endsWith(".md"));
211
+ let installed = 0;
212
+
213
+ for (const skill of skills) {
214
+ const src = path.join(skillsDir, skill);
215
+ const dest = path.join(targetDir, skill);
216
+
217
+ if (fs.existsSync(dest)) {
218
+ // Check if ours is newer/different
219
+ const srcContent = fs.readFileSync(src, "utf-8");
220
+ const destContent = fs.readFileSync(dest, "utf-8");
221
+ if (srcContent === destContent) continue;
222
+
223
+ // Back up existing
224
+ fs.copyFileSync(dest, dest + ".bak");
225
+ }
226
+
227
+ fs.copyFileSync(src, dest);
228
+ installed++;
229
+ }
230
+
231
+ if (installed > 0) {
232
+ ok(`${installed} skill${installed > 1 ? "s" : ""} installed to ~/.claude/commands/`);
233
+ } else {
234
+ ok(`${skills.length} skills up to date`);
235
+ }
236
+ return installed;
237
+ }
238
+
239
+ function autoConfigureClaude() {
240
+ const claudeConfig = path.join(os.homedir(), ".claude.json");
241
+
242
+ if (!fs.existsSync(claudeConfig)) {
243
+ const config = { mcpServers: { telemetry: getMcpEntry() } };
244
+ fs.writeFileSync(claudeConfig, JSON.stringify(config, null, 2) + "\n");
245
+ ok("created ~/.claude.json with MCP config");
246
+ return true;
247
+ }
248
+
249
+ try {
250
+ const raw = fs.readFileSync(claudeConfig, "utf-8");
251
+ const config = JSON.parse(raw);
252
+
253
+ if (config.mcpServers?.telemetry) {
254
+ ok("MCP already configured");
255
+ return false;
256
+ }
257
+
258
+ if (!config.mcpServers) config.mcpServers = {};
259
+ config.mcpServers.telemetry = getMcpEntry();
260
+ fs.writeFileSync(claudeConfig, JSON.stringify(config, null, 2) + "\n");
261
+ ok("added MCP config to ~/.claude.json");
262
+ return true;
263
+ } catch {
264
+ fail("could not auto-configure ~/.claude.json");
265
+ return false;
266
+ }
267
+ }
268
+
269
+ // --- Commands ---
270
+
271
+ if (command === "help" || command === "--help" || command === "-h") {
272
+ log(LOGO);
273
+ log(HELP);
274
+ process.exit(0);
275
+ }
276
+
277
+ if (command === "version" || command === "--version" || command === "-v") {
278
+ log(`${g}tanuki${r} ${d}v${VERSION}${r}`);
279
+ process.exit(0);
280
+ }
281
+
282
+ if (command === "start") {
283
+ log(LOGO);
284
+ log(BANNER);
285
+
286
+ step(1, 5, "checking requirements");
287
+ checkRequirements();
288
+
289
+ fs.mkdirSync(DATA_DIR, { recursive: true });
290
+
291
+ if (isRunning()) {
292
+ log("");
293
+ log(` ${g}already running${r} at ${cy}http://localhost:${PORT}${r}`);
294
+ log("");
295
+ process.exit(0);
296
+ }
297
+
298
+ copySourceToTanukiDir();
299
+
300
+ const hasImage = (() => {
301
+ try { execSync("docker image inspect tanuki-dashboard:latest", { stdio: "pipe" }); return true; }
302
+ catch { return false; }
303
+ })();
304
+
305
+ step(2, 5, hasImage ? "docker images" : "building docker images");
306
+ if (!hasImage) {
307
+ info("first run — this takes ~30s...");
308
+ buildImages();
309
+ }
310
+ ok(hasImage ? "cached" : "images built");
311
+
312
+ step(3, 5, "starting dashboard");
313
+ startDashboard();
314
+
315
+ let healthy = false;
316
+ for (let i = 0; i < 10; i++) {
317
+ try {
318
+ const res = execSync(`curl -s http://localhost:${PORT}/health`, { encoding: "utf-8", stdio: "pipe" });
319
+ if (res.includes('"ok":true')) { healthy = true; break; }
320
+ } catch {}
321
+ execSync("sleep 1", { stdio: "pipe" });
322
+ }
323
+ ok(healthy ? `running on port ${PORT}` : `starting — check port ${PORT}`);
324
+
325
+ step(4, 5, "configuring claude code");
326
+ autoConfigureClaude();
327
+
328
+ step(5, 5, "installing skills");
329
+ installSkills();
330
+
331
+ log("");
332
+ log(` ${d}─────────────────────────────────────────${r}`);
333
+ log("");
334
+ log(` ${g}${b}ready${r}`);
335
+ log("");
336
+ log(` ${w}dashboard${r} ${cy}http://localhost:${PORT}${r}`);
337
+ log(` ${w}data${r} ${d}${DATA_DIR}${r}`);
338
+ log("");
339
+ log(` ${w}skills installed:${r}`);
340
+ log(` ${g}/start-work${r} ${d}autonomous dev — context to PR${r}`);
341
+ log(` ${g}/coordinate${r} ${d}manage cmux workspaces${r}`);
342
+ log(` ${g}/walkthrough${r} ${d}execute UI test scenarios${r}`);
343
+ log(` ${g}/debug${r} ${d}systematic bug investigation${r}`);
344
+ log(` ${g}/review-code${r} ${d}PR code review${r}`);
345
+ log(` ${g}/sessions${r} ${d}browse past sessions${r}`);
346
+ log(` ${g}/create-path${r} ${d}generate walkthrough scenarios${r}`);
347
+ log(` ${g}/edit-path${r} ${d}update walkthrough scenarios${r}`);
348
+ log(` ${g}/cmux-guide${r} ${d}workspace navigation ref${r}`);
349
+ log("");
350
+ log(` ${y}restart claude code to load MCP tools + skills${r}`);
351
+ log("");
352
+ }
353
+
354
+ else if (command === "stop") {
355
+ run("docker rm -f tanuki-dashboard 2>/dev/null || true", { quiet: true, ignoreError: true });
356
+ run("docker rm -f telemetry-dashboard 2>/dev/null || true", { quiet: true, ignoreError: true });
357
+ log(` ${g}stopped${r}`);
358
+ log("");
359
+ }
360
+
361
+ else if (command === "status") {
362
+ if (isRunning()) {
363
+ try {
364
+ const health = execSync(`curl -s http://localhost:${PORT}/health`, { encoding: "utf-8", stdio: "pipe" });
365
+ const parsed = JSON.parse(health);
366
+ log(` ${g}running${r} at ${cy}http://localhost:${PORT}${r} ${d}(v${parsed.version})${r}`);
367
+ } catch {
368
+ log(` ${y}container running but health check failed${r}`);
369
+ }
370
+ } else {
371
+ log(` ${d}not running${r} — ${cy}npx tanuki-telemetry${r} to start`);
372
+ }
373
+ log("");
374
+ }
375
+
376
+ else if (command === "setup") {
377
+ log(BANNER);
378
+ autoConfigureClaude();
379
+ installSkills();
380
+ log("");
381
+ }
382
+
383
+ else if (command === "skills") {
384
+ log(BANNER);
385
+ installSkills();
386
+ log("");
387
+ }
388
+
389
+ else if (command === "update") {
390
+ log(BANNER);
391
+ step(1, 2, "rebuilding images");
392
+ checkRequirements();
393
+ copySourceToTanukiDir();
394
+ buildImages();
395
+ ok("images rebuilt");
396
+
397
+ if (isRunning()) {
398
+ step(2, 2, "restarting dashboard");
399
+ run("docker rm -f tanuki-dashboard 2>/dev/null || true", { quiet: true, ignoreError: true });
400
+ run("docker rm -f telemetry-dashboard 2>/dev/null || true", { quiet: true, ignoreError: true });
401
+ startDashboard();
402
+ ok(`running at http://localhost:${PORT}`);
403
+ } else {
404
+ step(2, 2, "done");
405
+ info("run 'npx tanuki-telemetry' to start");
406
+ }
407
+ log("");
408
+ }
409
+
410
+ else {
411
+ log(` ${c.red}unknown command: ${command}${r}`);
412
+ log(HELP);
413
+ process.exit(1);
414
+ }
@@ -0,0 +1,23 @@
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import tseslint from 'typescript-eslint'
6
+ import { defineConfig, globalIgnores } from 'eslint/config'
7
+
8
+ export default defineConfig([
9
+ globalIgnores(['dist']),
10
+ {
11
+ files: ['**/*.{ts,tsx}'],
12
+ extends: [
13
+ js.configs.recommended,
14
+ tseslint.configs.recommended,
15
+ reactHooks.configs.flat.recommended,
16
+ reactRefresh.configs.vite,
17
+ ],
18
+ languageOptions: {
19
+ ecmaVersion: 2020,
20
+ globals: globals.browser,
21
+ },
22
+ },
23
+ ])
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>tanuki</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>