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.
- package/Dockerfile +22 -0
- package/bin/tanuki.mjs +251 -0
- package/frontend/eslint.config.js +23 -0
- package/frontend/index.html +13 -0
- package/frontend/package.json +39 -0
- package/frontend/src/App.tsx +232 -0
- package/frontend/src/assets/hero.png +0 -0
- package/frontend/src/assets/react.svg +1 -0
- package/frontend/src/assets/vite.svg +1 -0
- package/frontend/src/components/ArtifactsPanel.tsx +429 -0
- package/frontend/src/components/ChildStreams.tsx +176 -0
- package/frontend/src/components/CoordinatorPage.tsx +317 -0
- package/frontend/src/components/Header.tsx +108 -0
- package/frontend/src/components/InsightsPanel.tsx +142 -0
- package/frontend/src/components/IterationsTable.tsx +98 -0
- package/frontend/src/components/KnowledgePage.tsx +308 -0
- package/frontend/src/components/LoginPage.tsx +55 -0
- package/frontend/src/components/PlanProgress.tsx +163 -0
- package/frontend/src/components/QualityReport.tsx +276 -0
- package/frontend/src/components/ScreenshotUpload.tsx +117 -0
- package/frontend/src/components/ScreenshotsGrid.tsx +266 -0
- package/frontend/src/components/SessionDetail.tsx +265 -0
- package/frontend/src/components/SessionList.tsx +234 -0
- package/frontend/src/components/SettingsPage.tsx +213 -0
- package/frontend/src/components/StreamComms.tsx +228 -0
- package/frontend/src/components/TanukiLogo.tsx +16 -0
- package/frontend/src/components/Timeline.tsx +416 -0
- package/frontend/src/components/WalkthroughPage.tsx +458 -0
- package/frontend/src/hooks/useApi.ts +81 -0
- package/frontend/src/hooks/useAuth.ts +54 -0
- package/frontend/src/hooks/useKnowledge.ts +33 -0
- package/frontend/src/hooks/useWebSocket.ts +95 -0
- package/frontend/src/index.css +66 -0
- package/frontend/src/lib/api.ts +15 -0
- package/frontend/src/lib/utils.ts +58 -0
- package/frontend/src/main.tsx +10 -0
- package/frontend/src/types.ts +181 -0
- package/frontend/tsconfig.app.json +32 -0
- package/frontend/tsconfig.json +7 -0
- package/frontend/vite.config.ts +25 -0
- package/install.sh +87 -0
- package/package.json +63 -0
- package/src/api-keys.ts +97 -0
- package/src/auth.ts +165 -0
- package/src/coordinator.ts +136 -0
- package/src/dashboard-server.ts +5 -0
- package/src/dashboard.ts +826 -0
- package/src/db.ts +1009 -0
- package/src/index.ts +20 -0
- package/src/middleware.ts +76 -0
- package/src/tools.ts +864 -0
- package/src/types-shim.d.ts +18 -0
- package/src/types.ts +171 -0
- 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,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,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
|
+
}
|