tanuki-telemetry 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/Dockerfile +22 -0
  2. package/bin/tanuki.mjs +251 -0
  3. package/frontend/eslint.config.js +23 -0
  4. package/frontend/index.html +13 -0
  5. package/frontend/package.json +39 -0
  6. package/frontend/src/App.tsx +232 -0
  7. package/frontend/src/assets/hero.png +0 -0
  8. package/frontend/src/assets/react.svg +1 -0
  9. package/frontend/src/assets/vite.svg +1 -0
  10. package/frontend/src/components/ArtifactsPanel.tsx +429 -0
  11. package/frontend/src/components/ChildStreams.tsx +176 -0
  12. package/frontend/src/components/CoordinatorPage.tsx +317 -0
  13. package/frontend/src/components/Header.tsx +108 -0
  14. package/frontend/src/components/InsightsPanel.tsx +142 -0
  15. package/frontend/src/components/IterationsTable.tsx +98 -0
  16. package/frontend/src/components/KnowledgePage.tsx +308 -0
  17. package/frontend/src/components/LoginPage.tsx +55 -0
  18. package/frontend/src/components/PlanProgress.tsx +163 -0
  19. package/frontend/src/components/QualityReport.tsx +276 -0
  20. package/frontend/src/components/ScreenshotUpload.tsx +117 -0
  21. package/frontend/src/components/ScreenshotsGrid.tsx +266 -0
  22. package/frontend/src/components/SessionDetail.tsx +265 -0
  23. package/frontend/src/components/SessionList.tsx +234 -0
  24. package/frontend/src/components/SettingsPage.tsx +213 -0
  25. package/frontend/src/components/StreamComms.tsx +228 -0
  26. package/frontend/src/components/TanukiLogo.tsx +16 -0
  27. package/frontend/src/components/Timeline.tsx +416 -0
  28. package/frontend/src/components/WalkthroughPage.tsx +458 -0
  29. package/frontend/src/hooks/useApi.ts +81 -0
  30. package/frontend/src/hooks/useAuth.ts +54 -0
  31. package/frontend/src/hooks/useKnowledge.ts +33 -0
  32. package/frontend/src/hooks/useWebSocket.ts +95 -0
  33. package/frontend/src/index.css +66 -0
  34. package/frontend/src/lib/api.ts +15 -0
  35. package/frontend/src/lib/utils.ts +58 -0
  36. package/frontend/src/main.tsx +10 -0
  37. package/frontend/src/types.ts +181 -0
  38. package/frontend/tsconfig.app.json +32 -0
  39. package/frontend/tsconfig.json +7 -0
  40. package/frontend/vite.config.ts +25 -0
  41. package/install.sh +87 -0
  42. package/package.json +63 -0
  43. package/src/api-keys.ts +97 -0
  44. package/src/auth.ts +165 -0
  45. package/src/coordinator.ts +136 -0
  46. package/src/dashboard-server.ts +5 -0
  47. package/src/dashboard.ts +826 -0
  48. package/src/db.ts +1009 -0
  49. package/src/index.ts +20 -0
  50. package/src/middleware.ts +76 -0
  51. package/src/tools.ts +864 -0
  52. package/src/types-shim.d.ts +18 -0
  53. package/src/types.ts +171 -0
  54. package/tsconfig.json +19 -0
@@ -0,0 +1,95 @@
1
+ import { useEffect, useRef, useState, useCallback } from "react"
2
+ import type { Session, Event, Iteration, Screenshot, Artifact, Insight, PlanStep } from "@/types"
3
+
4
+ export type WsMessage =
5
+ | { type: "event"; data: Event }
6
+ | { type: "iteration"; data: Iteration }
7
+ | { type: "screenshot"; data: Screenshot }
8
+ | { type: "artifact"; data: Artifact }
9
+ | { type: "insight"; data: Insight }
10
+ | { type: "plan_step_new"; data: PlanStep }
11
+ | { type: "plan_step_update"; data: PlanStep }
12
+ | { type: "session_update"; data: Session }
13
+ | { type: "session_new"; data: Session }
14
+ | { type: "child_session_new"; data: Session }
15
+ | { type: "child_session_update"; data: Session }
16
+ | { type: "coordinator_update"; data: { session_id: string; updated_at: string } }
17
+
18
+ export type WsStatus = "connecting" | "connected" | "disconnected"
19
+
20
+ interface UseWebSocketReturn {
21
+ status: WsStatus
22
+ messages: WsMessage[]
23
+ activeSessions: Session[]
24
+ }
25
+
26
+ export function useWebSocket(): UseWebSocketReturn {
27
+ const [status, setStatus] = useState<WsStatus>("connecting")
28
+ const [messages, setMessages] = useState<WsMessage[]>([])
29
+ const [activeSessions, setActiveSessions] = useState<Session[]>([])
30
+ const wsRef = useRef<WebSocket | null>(null)
31
+ const reconnectTimeout = useRef<ReturnType<typeof setTimeout>>(undefined)
32
+
33
+ const connect = useCallback(() => {
34
+ const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"
35
+ const wsUrl = `${protocol}//${window.location.host}/ws`
36
+
37
+ try {
38
+ const ws = new WebSocket(wsUrl)
39
+ wsRef.current = ws
40
+
41
+ ws.onopen = () => {
42
+ setStatus("connected")
43
+ }
44
+
45
+ ws.onmessage = (e) => {
46
+ try {
47
+ const data = JSON.parse(e.data)
48
+
49
+ // Initial connection message
50
+ if (data.type === "connected") {
51
+ setActiveSessions(data.active_sessions || [])
52
+ return
53
+ }
54
+
55
+ // Batch messages
56
+ if (data.messages) {
57
+ setMessages((prev) => {
58
+ const next = [...prev, ...data.messages]
59
+ // Keep last 500 messages in memory
60
+ return next.slice(-500)
61
+ })
62
+ }
63
+ } catch {
64
+ // ignore parse errors
65
+ }
66
+ }
67
+
68
+ ws.onclose = () => {
69
+ setStatus("disconnected")
70
+ // Reconnect after 2s
71
+ reconnectTimeout.current = setTimeout(() => {
72
+ setStatus("connecting")
73
+ connect()
74
+ }, 2000)
75
+ }
76
+
77
+ ws.onerror = () => {
78
+ ws.close()
79
+ }
80
+ } catch {
81
+ setStatus("disconnected")
82
+ reconnectTimeout.current = setTimeout(connect, 2000)
83
+ }
84
+ }, [])
85
+
86
+ useEffect(() => {
87
+ connect()
88
+ return () => {
89
+ if (wsRef.current) wsRef.current.close()
90
+ if (reconnectTimeout.current) clearTimeout(reconnectTimeout.current)
91
+ }
92
+ }, [connect])
93
+
94
+ return { status, messages, activeSessions }
95
+ }
@@ -0,0 +1,66 @@
1
+ @import "tailwindcss";
2
+
3
+ @theme {
4
+ --font-mono: "JetBrains Mono", "SF Mono", "Fira Code", "Fira Mono", ui-monospace, monospace;
5
+
6
+ --color-bg: #0a0a0a;
7
+ --color-bg-elevated: #111111;
8
+ --color-bg-hover: #1a1a1a;
9
+ --color-border: #222222;
10
+ --color-border-bright: #333333;
11
+ --color-text: #c8c8c8;
12
+ --color-text-muted: #666666;
13
+ --color-text-dim: #444444;
14
+ --color-accent: #00ff88;
15
+ --color-accent-dim: #00994d;
16
+ --color-error: #ff4444;
17
+ --color-error-dim: #661a1a;
18
+ --color-warning: #ffaa00;
19
+ --color-warning-dim: #664400;
20
+ --color-info: #4488ff;
21
+ --color-info-dim: #1a3366;
22
+ --color-purple: #aa66ff;
23
+ --color-purple-dim: #442266;
24
+ }
25
+
26
+ * {
27
+ scrollbar-width: thin;
28
+ scrollbar-color: var(--color-border-bright) transparent;
29
+ border-radius: 0 !important;
30
+ }
31
+
32
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
33
+ ::-webkit-scrollbar-track { background: transparent; }
34
+ ::-webkit-scrollbar-thumb { background: var(--color-border-bright); }
35
+
36
+ ::selection {
37
+ background: var(--color-accent);
38
+ color: var(--color-bg);
39
+ }
40
+
41
+ body {
42
+ margin: 0;
43
+ font-family: var(--font-mono);
44
+ background: var(--color-bg);
45
+ color: var(--color-text);
46
+ }
47
+
48
+ @keyframes blink {
49
+ 0%, 100% { opacity: 1; }
50
+ 50% { opacity: 0; }
51
+ }
52
+
53
+ .session-item, .group-header {
54
+ transition: border-color 0.2s ease, background-color 0.15s ease;
55
+ }
56
+
57
+ /* Expand/collapse animation for dropdown content */
58
+ [data-expand] {
59
+ animation: expand 0.15s ease-out forwards;
60
+ transform-origin: top;
61
+ }
62
+
63
+ @keyframes expand {
64
+ from { opacity: 0; transform: scaleY(0.9); }
65
+ to { opacity: 1; transform: scaleY(1); }
66
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Fetch wrapper that redirects to Google login on 401.
3
+ */
4
+ export async function apiFetch(url: string, init?: RequestInit): Promise<Response> {
5
+ const res = await fetch(url, init);
6
+
7
+ if (res.status === 401) {
8
+ // Check if auth is enabled by trying /auth/me
9
+ // If we get a 401, redirect to Google login
10
+ window.location.href = "/auth/google";
11
+ throw new Error("Not authenticated — redirecting to login");
12
+ }
13
+
14
+ return res;
15
+ }
@@ -0,0 +1,58 @@
1
+ import { clsx, type ClassValue } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
7
+
8
+ export function formatDuration(seconds: number | null): string {
9
+ if (seconds == null) return "-"
10
+ if (seconds < 60) return seconds + "s"
11
+ const m = Math.floor(seconds / 60)
12
+ const s = seconds % 60
13
+ return m + "m " + s + "s"
14
+ }
15
+
16
+ export function formatTokens(n: number | null): string {
17
+ if (n == null || n === 0) return "0"
18
+ if (n >= 1000000) return (n / 1000000).toFixed(1) + "M"
19
+ if (n >= 1000) return (n / 1000).toFixed(1) + "k"
20
+ return String(n)
21
+ }
22
+
23
+ export function relativeTime(eventTs: string, sessionStart: string): string {
24
+ const diff = Math.max(
25
+ 0,
26
+ Math.floor(
27
+ (new Date(eventTs + "Z").getTime() - new Date(sessionStart + "Z").getTime()) / 1000
28
+ )
29
+ )
30
+ const m = Math.floor(diff / 60)
31
+ const s = diff % 60
32
+ return "+" + m + ":" + String(s).padStart(2, "0")
33
+ }
34
+
35
+ export function timeAgo(ts: string): string {
36
+ const now = Date.now()
37
+ const then = new Date(ts.endsWith("Z") ? ts : ts + "Z").getTime()
38
+ const diff = Math.max(0, Math.floor((now - then) / 1000))
39
+ if (diff < 60) return "just now"
40
+ if (diff < 3600) return Math.floor(diff / 60) + "m ago"
41
+ if (diff < 86400) return Math.floor(diff / 3600) + "h ago"
42
+ return Math.floor(diff / 86400) + "d ago"
43
+ }
44
+
45
+ export function screenshotUrl(filePath: string, screenshotId?: number, size?: "thumb" | "full"): string {
46
+ // Prefer by-id endpoint (self-contained, always works)
47
+ if (screenshotId) {
48
+ const sizeParam = size === "thumb" ? "?size=thumb" : ""
49
+ return `/api/screenshots/by-id/${screenshotId}${sizeParam}`
50
+ }
51
+ // Fallback to path-based serving
52
+ const idx = filePath.indexOf("outputs/")
53
+ if (idx !== -1) {
54
+ return "/api/screenshots/" + filePath.substring(idx + 8)
55
+ }
56
+ const parts = filePath.split("/")
57
+ return "/api/screenshots/" + parts[parts.length - 1]
58
+ }
@@ -0,0 +1,10 @@
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.tsx'
5
+
6
+ createRoot(document.getElementById('root')!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )
@@ -0,0 +1,181 @@
1
+ export interface Session {
2
+ id: string
3
+ worktree_name: string
4
+ ticket_id: string | null
5
+ ticket_title: string | null
6
+ branch_name: string | null
7
+ mode: "local" | "remote"
8
+ status: "in_progress" | "completed" | "failed" | "interrupted"
9
+ total_input_tokens: number
10
+ total_output_tokens: number
11
+ total_iterations: number
12
+ max_iterations: number
13
+ started_at: string
14
+ ended_at: string | null
15
+ duration_seconds: number | null
16
+ final_result: string | null
17
+ created_at: string
18
+ parent_session_id: string | null
19
+ user_email: string | null
20
+ }
21
+
22
+ export interface Event {
23
+ id: number
24
+ session_id: string
25
+ phase: string
26
+ event_type: EventType
27
+ message: string
28
+ metadata: string | null
29
+ tokens_used: number
30
+ timestamp: string
31
+ }
32
+
33
+ export type EventType =
34
+ | "decision"
35
+ | "action"
36
+ | "error"
37
+ | "fix"
38
+ | "info"
39
+ | "phase_change"
40
+ | "review_pass"
41
+ | "review_flag"
42
+ | "review_dispatch"
43
+ | "error_resolve"
44
+ | "cost_checkpoint"
45
+ | "pattern_detect"
46
+ | "knowledge_extract"
47
+
48
+ export interface Iteration {
49
+ id: number
50
+ session_id: string
51
+ iteration_number: number
52
+ trigger: string
53
+ error_summary: string
54
+ fix_description: string | null
55
+ files_changed: string | null
56
+ result: "pass" | "fail" | "partial"
57
+ duration_seconds: number | null
58
+ timestamp: string
59
+ }
60
+
61
+ export interface Screenshot {
62
+ id: number
63
+ session_id: string
64
+ iteration_number: number | null
65
+ phase: string
66
+ description: string
67
+ file_path: string
68
+ stored_path: string | null
69
+ thumb_path: string | null
70
+ event_id: number | null
71
+ timestamp: string
72
+ }
73
+
74
+ export interface FinalResult {
75
+ tests_passed?: boolean
76
+ lint_passed?: boolean
77
+ typecheck_passed?: boolean
78
+ pr_url?: string
79
+ pr_number?: number
80
+ }
81
+
82
+ export interface Stats {
83
+ total_sessions: number
84
+ avg_iterations: number
85
+ avg_duration_seconds: number
86
+ total_tokens: number
87
+ completed: number
88
+ failed: number
89
+ pass_rate: number
90
+ }
91
+
92
+ export interface PlanStep {
93
+ id: number
94
+ session_id: string
95
+ step_number: number
96
+ title: string
97
+ description: string
98
+ status: "pending" | "in_progress" | "completed" | "skipped" | "failed"
99
+ parent_step: number | null
100
+ file_targets: string | null
101
+ outcome: string | null
102
+ started_at: string | null
103
+ completed_at: string | null
104
+ duration_seconds: number | null
105
+ created_at: string
106
+ }
107
+
108
+ export interface Insight {
109
+ id: number
110
+ session_id: string
111
+ insight_type: "mistake" | "success_pattern" | "codebase_gotcha" | "optimization" | "rule_learned"
112
+ category: string
113
+ title: string
114
+ description: string
115
+ evidence: string | null
116
+ confidence: number
117
+ times_validated: number
118
+ file_patterns: string | null
119
+ error_patterns: string | null
120
+ created_at: string
121
+ }
122
+
123
+ export interface Artifact {
124
+ id: number
125
+ session_id: string
126
+ artifact_type: string
127
+ description: string
128
+ file_path: string
129
+ stored_path: string | null
130
+ mime_type: string | null
131
+ file_size: number | null
132
+ metadata: string | null
133
+ event_id: number | null
134
+ timestamp: string
135
+ }
136
+
137
+ export interface Walkthrough {
138
+ id: number
139
+ url: string
140
+ app_name: string | null
141
+ scenario: string | null
142
+ status: "in_progress" | "pass" | "fail" | "partial"
143
+ summary: string | null
144
+ total_actions: number
145
+ passed: number
146
+ failed: number
147
+ started_at: string
148
+ ended_at: string | null
149
+ }
150
+
151
+ export interface WalkthroughAction {
152
+ id: number
153
+ walkthrough_id: number
154
+ action_type: "navigate" | "click" | "type" | "assert" | "wait" | "screenshot"
155
+ target: string
156
+ value: string | null
157
+ status: "pass" | "fail"
158
+ message: string | null
159
+ timestamp: string
160
+ }
161
+
162
+ export interface WalkthroughScreenshot {
163
+ id: number
164
+ walkthrough_id: number
165
+ action_id: number | null
166
+ name: string
167
+ annotation: string | null
168
+ stored_path: string
169
+ timestamp: string
170
+ }
171
+
172
+ export interface SessionDetail {
173
+ session: Session
174
+ events: Event[]
175
+ iterations: Iteration[]
176
+ screenshots: Screenshot[]
177
+ artifacts: Artifact[]
178
+ insights: Insight[]
179
+ plan_steps: PlanStep[]
180
+ child_sessions: Session[]
181
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "ES2023",
5
+ "useDefineForClassFields": true,
6
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
7
+ "module": "ESNext",
8
+ "types": ["vite/client"],
9
+ "skipLibCheck": true,
10
+
11
+ /* Bundler mode */
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "moduleDetection": "force",
16
+ "noEmit": true,
17
+ "jsx": "react-jsx",
18
+
19
+ /* Linting */
20
+ "strict": true,
21
+ "noUnusedLocals": true,
22
+ "noUnusedParameters": true,
23
+ "erasableSyntaxOnly": true,
24
+ "noFallthroughCasesInSwitch": true,
25
+ "noUncheckedSideEffectImports": true,
26
+ "baseUrl": ".",
27
+ "paths": {
28
+ "@/*": ["./src/*"]
29
+ }
30
+ },
31
+ "include": ["src"]
32
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
@@ -0,0 +1,25 @@
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+ import tailwindcss from '@tailwindcss/vite'
4
+ import path from 'path'
5
+
6
+ export default defineConfig({
7
+ plugins: [react(), tailwindcss()],
8
+ resolve: {
9
+ alias: {
10
+ '@': path.resolve(__dirname, './src'),
11
+ },
12
+ },
13
+ server: {
14
+ proxy: {
15
+ '/api': 'http://localhost:3333',
16
+ '/ws': {
17
+ target: 'ws://localhost:3333',
18
+ ws: true,
19
+ },
20
+ },
21
+ },
22
+ build: {
23
+ outDir: 'dist',
24
+ },
25
+ })
package/install.sh ADDED
@@ -0,0 +1,87 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ # Tanuki — one-command install
5
+ # Usage: curl -fsSL <url>/install.sh | bash
6
+ # or: ./install.sh
7
+
8
+ TANUKI_DIR="${TANUKI_DIR:-$HOME/.claude/mcp-servers/telemetry}"
9
+ DATA_DIR="${DATA_DIR:-$HOME/.tanuki/data}"
10
+ VERSION="1.1.0"
11
+
12
+ echo ""
13
+ echo " ┌─────────────────────────┐"
14
+ echo " │ TANUKI // v${VERSION} │"
15
+ echo " └─────────────────────────┘"
16
+ echo ""
17
+
18
+ # Check prerequisites
19
+ command -v docker >/dev/null 2>&1 || { echo "Error: docker is required. Install Docker Desktop first."; exit 1; }
20
+ command -v git >/dev/null 2>&1 || { echo "Error: git is required."; exit 1; }
21
+
22
+ # Clone or update
23
+ if [ -d "$TANUKI_DIR/.git" ]; then
24
+ echo "[1/4] Updating existing installation..."
25
+ cd "$TANUKI_DIR"
26
+ git pull --ff-only 2>/dev/null || echo " (local changes detected — skipping git pull)"
27
+ else
28
+ echo "[1/4] Cloning tanuki..."
29
+ mkdir -p "$(dirname "$TANUKI_DIR")"
30
+ git clone https://github.com/anthropics/tanuki-telemetry.git "$TANUKI_DIR" 2>/dev/null || {
31
+ echo " Repo not accessible — using local copy"
32
+ }
33
+ fi
34
+
35
+ cd "$TANUKI_DIR"
36
+
37
+ # Create data directory
38
+ mkdir -p "$DATA_DIR"
39
+
40
+ # Build Docker images
41
+ echo "[2/4] Building Docker images..."
42
+ docker build -t telemetry-mcp:latest -t telemetry-dashboard:latest . -q
43
+
44
+ # Stop existing dashboard if running
45
+ echo "[3/4] Starting dashboard..."
46
+ docker rm -f telemetry-dashboard 2>/dev/null || true
47
+ docker run -d --rm \
48
+ --name telemetry-dashboard \
49
+ -p 3333:3333 \
50
+ -v "$DATA_DIR:/data" \
51
+ telemetry-dashboard:latest
52
+
53
+ # Configure Claude Code MCP
54
+ echo "[4/4] Configuring Claude Code..."
55
+ CLAUDE_CONFIG="$HOME/.claude.json"
56
+ if [ -f "$CLAUDE_CONFIG" ]; then
57
+ # Check if telemetry MCP is already configured
58
+ if grep -q '"telemetry"' "$CLAUDE_CONFIG" 2>/dev/null; then
59
+ echo " MCP already configured in .claude.json"
60
+ else
61
+ echo " Add this to your .claude.json mcpServers:"
62
+ echo ""
63
+ echo ' "telemetry": {'
64
+ echo ' "type": "stdio",'
65
+ echo ' "command": "docker",'
66
+ echo ' "args": ["run", "--rm", "-i", "-v", "'$DATA_DIR':/data", "--entrypoint", "node", "telemetry-mcp:latest", "dist/index.js"]'
67
+ echo ' }'
68
+ echo ""
69
+ fi
70
+ else
71
+ echo " No .claude.json found — create one with:"
72
+ echo ""
73
+ echo ' { "mcpServers": { "telemetry": { "type": "stdio", "command": "docker", "args": ["run", "--rm", "-i", "-v", "'$DATA_DIR':/data", "--entrypoint", "node", "telemetry-mcp:latest", "dist/index.js"] } } }'
74
+ echo ""
75
+ fi
76
+
77
+ # Wait for dashboard to start
78
+ sleep 2
79
+ if curl -s http://localhost:3333/health | grep -q '"ok":true'; then
80
+ echo ""
81
+ echo " Tanuki is running at http://localhost:3333"
82
+ echo ""
83
+ else
84
+ echo ""
85
+ echo " Dashboard may still be starting — check http://localhost:3333"
86
+ echo ""
87
+ fi
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "tanuki-telemetry",
3
+ "version": "1.1.0",
4
+ "description": "Workflow monitor and telemetry dashboard for Claude Code autonomous agents",
5
+ "type": "module",
6
+ "bin": {
7
+ "tanuki": "./bin/tanuki.mjs",
8
+ "tanuki-telemetry": "./bin/tanuki.mjs"
9
+ },
10
+ "files": [
11
+ "bin/",
12
+ "src/",
13
+ "frontend/src/",
14
+ "frontend/index.html",
15
+ "frontend/package.json",
16
+ "frontend/tsconfig.json",
17
+ "frontend/tsconfig.app.json",
18
+ "frontend/eslint.config.js",
19
+ "frontend/vite.config.ts",
20
+ "tsconfig.json",
21
+ "Dockerfile",
22
+ "install.sh"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsc",
26
+ "build:frontend": "cd frontend && npx vite build",
27
+ "build:all": "tsc && cd frontend && npx vite build",
28
+ "start": "node dist/index.js",
29
+ "dev": "tsx src/index.ts"
30
+ },
31
+ "keywords": ["claude", "mcp", "telemetry", "dashboard", "autonomous", "agents", "workflow"],
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/anthropics/tanuki-telemetry.git"
36
+ },
37
+ "dependencies": {
38
+ "@modelcontextprotocol/sdk": "^1.12.1",
39
+ "better-sqlite3": "^11.8.1",
40
+ "better-sqlite3-session-store": "^0.1.0",
41
+ "express": "^4.21.0",
42
+ "express-session": "^1.19.0",
43
+ "multer": "^2.1.1",
44
+ "passport": "^0.7.0",
45
+ "passport-google-oauth20": "^2.0.0",
46
+ "sharp": "^0.34.5",
47
+ "uuid": "^11.1.0",
48
+ "ws": "^8.19.0"
49
+ },
50
+ "devDependencies": {
51
+ "@types/better-sqlite3": "^7.6.12",
52
+ "@types/express": "^5.0.0",
53
+ "@types/express-session": "^1.18.2",
54
+ "@types/multer": "^2.1.0",
55
+ "@types/node": "^22.13.10",
56
+ "@types/passport": "^1.0.17",
57
+ "@types/passport-google-oauth20": "^2.0.17",
58
+ "@types/sharp": "^0.31.1",
59
+ "@types/uuid": "^10.0.0",
60
+ "@types/ws": "^8.18.1",
61
+ "typescript": "^5.7.3"
62
+ }
63
+ }