tanuki-telemetry 1.1.2 → 1.1.3
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/bin/tanuki.mjs +139 -125
- package/package.json +1 -1
package/bin/tanuki.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { execSync
|
|
3
|
+
import { execSync } from "child_process";
|
|
4
4
|
import fs from "fs";
|
|
5
5
|
import path from "path";
|
|
6
6
|
import os from "os";
|
|
@@ -17,29 +17,75 @@ const PORT = process.env.TANUKI_PORT || "3333";
|
|
|
17
17
|
const args = process.argv.slice(2);
|
|
18
18
|
const command = args[0] || "start";
|
|
19
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} ▕ ▔▔▔▔▔▔▔ ▏${r}
|
|
49
|
+
${g} ╲ ╱▔▔▔▔▔╲ ╱${r}
|
|
50
|
+
${g} ╲╱ ▕▔▔▏ ╲╱${r}
|
|
51
|
+
${g} ╲▁▁▕▁▁▏▁▁╱${r}
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
const BANNER = `
|
|
55
|
+
${d} ┌─────────────────────────────────────────┐${r}
|
|
56
|
+
${d} │${r} ${g}${b}TANUKI${r} ${d}v${VERSION}${r} ${d}│${r}
|
|
57
|
+
${d} │${r} ${d}workflow monitor for claude code${r} ${d}│${r}
|
|
58
|
+
${d} └─────────────────────────────────────────┘${r}
|
|
59
|
+
`;
|
|
60
|
+
|
|
20
61
|
const HELP = `
|
|
21
|
-
tanuki v${VERSION} — workflow monitor for Claude Code
|
|
22
|
-
|
|
23
|
-
Usage
|
|
24
|
-
npx tanuki-telemetry
|
|
25
|
-
npx tanuki-telemetry
|
|
26
|
-
npx tanuki-telemetry
|
|
27
|
-
npx tanuki-telemetry
|
|
28
|
-
npx tanuki-telemetry
|
|
29
|
-
npx tanuki-telemetry
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
TANUKI_DATA Data directory (default: ~/.tanuki/data)
|
|
62
|
+
${g}${b} tanuki${r} ${d}v${VERSION}${r} — workflow monitor for Claude Code
|
|
63
|
+
|
|
64
|
+
${w} Usage:${r}
|
|
65
|
+
${cy}npx tanuki-telemetry${r} start dashboard
|
|
66
|
+
${cy}npx tanuki-telemetry stop${r} stop dashboard
|
|
67
|
+
${cy}npx tanuki-telemetry status${r} check if running
|
|
68
|
+
${cy}npx tanuki-telemetry setup${r} configure claude code
|
|
69
|
+
${cy}npx tanuki-telemetry update${r} rebuild with latest
|
|
70
|
+
${cy}npx tanuki-telemetry version${r} show version
|
|
71
|
+
|
|
72
|
+
${w} Environment:${r}
|
|
73
|
+
${d}TANUKI_PORT dashboard port (default: 3333)${r}
|
|
74
|
+
${d}TANUKI_DATA data directory (default: ~/.tanuki/data)${r}
|
|
35
75
|
`;
|
|
36
76
|
|
|
77
|
+
function log(msg) { console.log(msg); }
|
|
78
|
+
function step(n, total, msg) { log(` ${g}[${n}/${total}]${r} ${msg}`); }
|
|
79
|
+
function ok(msg) { log(` ${d} ${g}✓${r} ${d}${msg}${r}`); }
|
|
80
|
+
function fail(msg) { log(` ${c.red} ✗ ${msg}${r}`); }
|
|
81
|
+
function info(msg) { log(` ${d} ${msg}${r}`); }
|
|
82
|
+
|
|
37
83
|
function run(cmd, opts = {}) {
|
|
38
84
|
try {
|
|
39
85
|
return execSync(cmd, { encoding: "utf-8", stdio: opts.quiet ? "pipe" : "inherit", ...opts });
|
|
40
86
|
} catch (e) {
|
|
41
87
|
if (!opts.ignoreError) {
|
|
42
|
-
|
|
88
|
+
fail(e.message);
|
|
43
89
|
process.exit(1);
|
|
44
90
|
}
|
|
45
91
|
return "";
|
|
@@ -47,46 +93,44 @@ function run(cmd, opts = {}) {
|
|
|
47
93
|
}
|
|
48
94
|
|
|
49
95
|
function checkRequirements() {
|
|
50
|
-
let
|
|
96
|
+
let allOk = true;
|
|
51
97
|
|
|
52
|
-
// Docker installed?
|
|
53
98
|
try {
|
|
54
99
|
execSync("docker --version", { stdio: "pipe" });
|
|
55
100
|
} catch {
|
|
56
|
-
|
|
57
|
-
|
|
101
|
+
fail("docker not installed — https://docker.com/products/docker-desktop");
|
|
102
|
+
allOk = false;
|
|
58
103
|
}
|
|
59
104
|
|
|
60
|
-
|
|
61
|
-
if (ok) {
|
|
105
|
+
if (allOk) {
|
|
62
106
|
try {
|
|
63
107
|
execSync("docker info", { stdio: "pipe" });
|
|
64
108
|
} catch {
|
|
65
|
-
|
|
66
|
-
|
|
109
|
+
fail("docker is not running — start Docker Desktop");
|
|
110
|
+
allOk = false;
|
|
67
111
|
}
|
|
68
112
|
}
|
|
69
113
|
|
|
70
|
-
// Node version
|
|
71
114
|
const nodeVersion = parseInt(process.versions.node.split(".")[0], 10);
|
|
72
115
|
if (nodeVersion < 18) {
|
|
73
|
-
|
|
74
|
-
|
|
116
|
+
fail(`node ${process.versions.node} — need 18+`);
|
|
117
|
+
allOk = false;
|
|
75
118
|
}
|
|
76
119
|
|
|
77
|
-
// curl (used for health checks)
|
|
78
120
|
try {
|
|
79
121
|
execSync("curl --version", { stdio: "pipe" });
|
|
80
122
|
} catch {
|
|
81
|
-
|
|
82
|
-
|
|
123
|
+
fail("curl not found");
|
|
124
|
+
allOk = false;
|
|
83
125
|
}
|
|
84
126
|
|
|
85
|
-
if (!
|
|
86
|
-
|
|
87
|
-
|
|
127
|
+
if (!allOk) {
|
|
128
|
+
log("");
|
|
129
|
+
fail("fix the above and try again");
|
|
88
130
|
process.exit(1);
|
|
89
131
|
}
|
|
132
|
+
|
|
133
|
+
ok("docker, node, curl");
|
|
90
134
|
}
|
|
91
135
|
|
|
92
136
|
function isRunning() {
|
|
@@ -99,29 +143,19 @@ function isRunning() {
|
|
|
99
143
|
}
|
|
100
144
|
|
|
101
145
|
function copySourceToTanukiDir() {
|
|
102
|
-
// The source code is wherever this package was installed/run from
|
|
103
146
|
const srcDir = path.resolve(path.dirname(import.meta.url.replace("file://", "")), "..");
|
|
147
|
+
if (srcDir === TANUKI_DIR) return;
|
|
104
148
|
|
|
105
|
-
if (srcDir === TANUKI_DIR) return; // already in place
|
|
106
|
-
|
|
107
|
-
// Copy source to TANUKI_DIR for Docker builds
|
|
108
149
|
fs.mkdirSync(TANUKI_DIR, { recursive: true });
|
|
109
150
|
|
|
110
|
-
const
|
|
111
|
-
const dirsToCopy = ["src", "frontend"];
|
|
112
|
-
|
|
113
|
-
for (const f of filesToCopy) {
|
|
151
|
+
for (const f of ["package.json", "tsconfig.json", "Dockerfile"]) {
|
|
114
152
|
const src = path.join(srcDir, f);
|
|
115
|
-
if (fs.existsSync(src))
|
|
116
|
-
fs.copyFileSync(src, path.join(TANUKI_DIR, f));
|
|
117
|
-
}
|
|
153
|
+
if (fs.existsSync(src)) fs.copyFileSync(src, path.join(TANUKI_DIR, f));
|
|
118
154
|
}
|
|
119
155
|
|
|
120
|
-
for (const d of
|
|
156
|
+
for (const d of ["src", "frontend"]) {
|
|
121
157
|
const src = path.join(srcDir, d);
|
|
122
|
-
if (fs.existsSync(src))
|
|
123
|
-
copyDirSync(src, path.join(TANUKI_DIR, d));
|
|
124
|
-
}
|
|
158
|
+
if (fs.existsSync(src)) copyDirSync(src, path.join(TANUKI_DIR, d));
|
|
125
159
|
}
|
|
126
160
|
}
|
|
127
161
|
|
|
@@ -130,12 +164,9 @@ function copyDirSync(src, dest) {
|
|
|
130
164
|
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
131
165
|
const srcPath = path.join(src, entry.name);
|
|
132
166
|
const destPath = path.join(dest, entry.name);
|
|
133
|
-
if (
|
|
134
|
-
if (entry.isDirectory())
|
|
135
|
-
|
|
136
|
-
} else {
|
|
137
|
-
fs.copyFileSync(srcPath, destPath);
|
|
138
|
-
}
|
|
167
|
+
if (["node_modules", "dist", ".git"].includes(entry.name)) continue;
|
|
168
|
+
if (entry.isDirectory()) copyDirSync(srcPath, destPath);
|
|
169
|
+
else fs.copyFileSync(srcPath, destPath);
|
|
139
170
|
}
|
|
140
171
|
}
|
|
141
172
|
|
|
@@ -161,10 +192,9 @@ function autoConfigureClaude() {
|
|
|
161
192
|
const claudeConfig = path.join(os.homedir(), ".claude.json");
|
|
162
193
|
|
|
163
194
|
if (!fs.existsSync(claudeConfig)) {
|
|
164
|
-
// Create new config
|
|
165
195
|
const config = { mcpServers: { telemetry: getMcpEntry() } };
|
|
166
196
|
fs.writeFileSync(claudeConfig, JSON.stringify(config, null, 2) + "\n");
|
|
167
|
-
|
|
197
|
+
ok("created ~/.claude.json with MCP config");
|
|
168
198
|
return true;
|
|
169
199
|
}
|
|
170
200
|
|
|
@@ -173,19 +203,17 @@ function autoConfigureClaude() {
|
|
|
173
203
|
const config = JSON.parse(raw);
|
|
174
204
|
|
|
175
205
|
if (config.mcpServers?.telemetry) {
|
|
176
|
-
|
|
206
|
+
ok("MCP already configured");
|
|
177
207
|
return false;
|
|
178
208
|
}
|
|
179
209
|
|
|
180
|
-
// Add telemetry to existing config
|
|
181
210
|
if (!config.mcpServers) config.mcpServers = {};
|
|
182
211
|
config.mcpServers.telemetry = getMcpEntry();
|
|
183
212
|
fs.writeFileSync(claudeConfig, JSON.stringify(config, null, 2) + "\n");
|
|
184
|
-
|
|
213
|
+
ok("added MCP config to ~/.claude.json");
|
|
185
214
|
return true;
|
|
186
|
-
} catch
|
|
187
|
-
|
|
188
|
-
console.log(JSON.stringify({ telemetry: getMcpEntry() }, null, 2));
|
|
215
|
+
} catch {
|
|
216
|
+
fail("could not auto-configure ~/.claude.json");
|
|
189
217
|
return false;
|
|
190
218
|
}
|
|
191
219
|
}
|
|
@@ -193,58 +221,47 @@ function autoConfigureClaude() {
|
|
|
193
221
|
// --- Commands ---
|
|
194
222
|
|
|
195
223
|
if (command === "help" || command === "--help" || command === "-h") {
|
|
196
|
-
|
|
224
|
+
log(LOGO);
|
|
225
|
+
log(HELP);
|
|
197
226
|
process.exit(0);
|
|
198
227
|
}
|
|
199
228
|
|
|
200
229
|
if (command === "version" || command === "--version" || command === "-v") {
|
|
201
|
-
|
|
230
|
+
log(`${g}tanuki${r} ${d}v${VERSION}${r}`);
|
|
202
231
|
process.exit(0);
|
|
203
232
|
}
|
|
204
233
|
|
|
205
|
-
console.log("");
|
|
206
|
-
|
|
207
234
|
if (command === "start") {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
console.log("");
|
|
213
|
-
|
|
214
|
-
// Requirements
|
|
215
|
-
console.log(" [1/4] checking requirements...");
|
|
235
|
+
log(LOGO);
|
|
236
|
+
log(BANNER);
|
|
237
|
+
|
|
238
|
+
step(1, 4, "checking requirements");
|
|
216
239
|
checkRequirements();
|
|
217
|
-
console.log(" docker, node, curl — ok");
|
|
218
240
|
|
|
219
241
|
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
220
242
|
|
|
221
243
|
if (isRunning()) {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
244
|
+
log("");
|
|
245
|
+
log(` ${g}already running${r} at ${cy}http://localhost:${PORT}${r}`);
|
|
246
|
+
log("");
|
|
225
247
|
process.exit(0);
|
|
226
248
|
}
|
|
227
249
|
|
|
228
250
|
copySourceToTanukiDir();
|
|
229
251
|
|
|
230
|
-
// Build
|
|
231
252
|
const hasImage = (() => {
|
|
232
|
-
try {
|
|
233
|
-
|
|
234
|
-
return true;
|
|
235
|
-
} catch { return false; }
|
|
253
|
+
try { execSync("docker image inspect tanuki-dashboard:latest", { stdio: "pipe" }); return true; }
|
|
254
|
+
catch { return false; }
|
|
236
255
|
})();
|
|
237
256
|
|
|
257
|
+
step(2, 4, hasImage ? "docker images" : "building docker images");
|
|
238
258
|
if (!hasImage) {
|
|
239
|
-
|
|
259
|
+
info("first run — this takes ~30s...");
|
|
240
260
|
buildImages();
|
|
241
|
-
console.log(" images built");
|
|
242
|
-
} else {
|
|
243
|
-
console.log(" [2/4] docker images — cached");
|
|
244
261
|
}
|
|
262
|
+
ok(hasImage ? "cached" : "images built");
|
|
245
263
|
|
|
246
|
-
|
|
247
|
-
console.log(" [3/4] starting dashboard...");
|
|
264
|
+
step(3, 4, "starting dashboard");
|
|
248
265
|
startDashboard();
|
|
249
266
|
|
|
250
267
|
let healthy = false;
|
|
@@ -255,39 +272,31 @@ if (command === "start") {
|
|
|
255
272
|
} catch {}
|
|
256
273
|
execSync("sleep 1", { stdio: "pipe" });
|
|
257
274
|
}
|
|
275
|
+
ok(healthy ? `running on port ${PORT}` : `starting — check port ${PORT}`);
|
|
258
276
|
|
|
259
|
-
|
|
260
|
-
console.log(` running on port ${PORT}`);
|
|
261
|
-
} else {
|
|
262
|
-
console.log(` starting up — check port ${PORT}`);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Configure
|
|
266
|
-
console.log(" [4/4] configuring claude code...");
|
|
277
|
+
step(4, 4, "configuring claude code");
|
|
267
278
|
autoConfigureClaude();
|
|
268
279
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
console.log(" npx tanuki-telemetry update");
|
|
283
|
-
console.log("");
|
|
280
|
+
log("");
|
|
281
|
+
log(` ${d}─────────────────────────────────────────${r}`);
|
|
282
|
+
log("");
|
|
283
|
+
log(` ${w}dashboard${r} ${cy}http://localhost:${PORT}${r}`);
|
|
284
|
+
log(` ${w}data${r} ${d}${DATA_DIR}${r}`);
|
|
285
|
+
log("");
|
|
286
|
+
log(` ${y}if claude code is already open,${r}`);
|
|
287
|
+
log(` ${y}restart it to load MCP tools.${r}`);
|
|
288
|
+
log("");
|
|
289
|
+
log(` ${d}npx tanuki-telemetry stop${r}`);
|
|
290
|
+
log(` ${d}npx tanuki-telemetry status${r}`);
|
|
291
|
+
log(` ${d}npx tanuki-telemetry update${r}`);
|
|
292
|
+
log("");
|
|
284
293
|
}
|
|
285
294
|
|
|
286
295
|
else if (command === "stop") {
|
|
287
296
|
run("docker rm -f tanuki-dashboard 2>/dev/null || true", { quiet: true, ignoreError: true });
|
|
288
297
|
run("docker rm -f telemetry-dashboard 2>/dev/null || true", { quiet: true, ignoreError: true });
|
|
289
|
-
|
|
290
|
-
|
|
298
|
+
log(` ${g}stopped${r}`);
|
|
299
|
+
log("");
|
|
291
300
|
}
|
|
292
301
|
|
|
293
302
|
else if (command === "status") {
|
|
@@ -295,40 +304,45 @@ else if (command === "status") {
|
|
|
295
304
|
try {
|
|
296
305
|
const health = execSync(`curl -s http://localhost:${PORT}/health`, { encoding: "utf-8", stdio: "pipe" });
|
|
297
306
|
const parsed = JSON.parse(health);
|
|
298
|
-
|
|
307
|
+
log(` ${g}running${r} at ${cy}http://localhost:${PORT}${r} ${d}(v${parsed.version})${r}`);
|
|
299
308
|
} catch {
|
|
300
|
-
|
|
309
|
+
log(` ${y}container running but health check failed${r}`);
|
|
301
310
|
}
|
|
302
311
|
} else {
|
|
303
|
-
|
|
312
|
+
log(` ${d}not running${r} — ${cy}npx tanuki-telemetry${r} to start`);
|
|
304
313
|
}
|
|
305
|
-
|
|
314
|
+
log("");
|
|
306
315
|
}
|
|
307
316
|
|
|
308
317
|
else if (command === "setup") {
|
|
318
|
+
log(BANNER);
|
|
309
319
|
autoConfigureClaude();
|
|
310
|
-
|
|
320
|
+
log("");
|
|
311
321
|
}
|
|
312
322
|
|
|
313
323
|
else if (command === "update") {
|
|
324
|
+
log(BANNER);
|
|
325
|
+
step(1, 2, "rebuilding images");
|
|
314
326
|
checkRequirements();
|
|
315
327
|
copySourceToTanukiDir();
|
|
316
328
|
buildImages();
|
|
329
|
+
ok("images rebuilt");
|
|
317
330
|
|
|
318
331
|
if (isRunning()) {
|
|
319
|
-
|
|
332
|
+
step(2, 2, "restarting dashboard");
|
|
320
333
|
run("docker rm -f tanuki-dashboard 2>/dev/null || true", { quiet: true, ignoreError: true });
|
|
321
334
|
run("docker rm -f telemetry-dashboard 2>/dev/null || true", { quiet: true, ignoreError: true });
|
|
322
335
|
startDashboard();
|
|
323
|
-
|
|
336
|
+
ok(`running at http://localhost:${PORT}`);
|
|
324
337
|
} else {
|
|
325
|
-
|
|
338
|
+
step(2, 2, "done");
|
|
339
|
+
info("run 'npx tanuki-telemetry' to start");
|
|
326
340
|
}
|
|
327
|
-
|
|
341
|
+
log("");
|
|
328
342
|
}
|
|
329
343
|
|
|
330
344
|
else {
|
|
331
|
-
|
|
332
|
-
|
|
345
|
+
log(` ${c.red}unknown command: ${command}${r}`);
|
|
346
|
+
log(HELP);
|
|
333
347
|
process.exit(1);
|
|
334
348
|
}
|