@rubytech/create-maxy 1.0.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/dist/index.js +428 -0
- package/package.json +31 -0
- package/payload/maxy/.env.example +12 -0
- package/payload/maxy/app/admin/components/ActivityTimeline.tsx +348 -0
- package/payload/maxy/app/admin/components/MarkdownMessage.tsx +40 -0
- package/payload/maxy/app/api/admin/chat/route.ts +72 -0
- package/payload/maxy/app/api/admin/logs/route.ts +40 -0
- package/payload/maxy/app/api/admin/session/route.ts +74 -0
- package/payload/maxy/app/api/chat/route.ts +72 -0
- package/payload/maxy/app/api/health/route.ts +26 -0
- package/payload/maxy/app/api/onboarding/claude-auth/route.ts +216 -0
- package/payload/maxy/app/api/onboarding/set-pin/route.ts +44 -0
- package/payload/maxy/app/api/session/route.ts +51 -0
- package/payload/maxy/app/api/telegram/webhook/route.ts +107 -0
- package/payload/maxy/app/apple-icon.png +0 -0
- package/payload/maxy/app/bot/page.tsx +373 -0
- package/payload/maxy/app/favicon.ico +0 -0
- package/payload/maxy/app/globals.css +1681 -0
- package/payload/maxy/app/layout.tsx +58 -0
- package/payload/maxy/app/lib/claude-agent.ts +503 -0
- package/payload/maxy/app/og/layout.tsx +15 -0
- package/payload/maxy/app/og/page.tsx +252 -0
- package/payload/maxy/app/page.tsx +594 -0
- package/payload/maxy/app/privacy/page.tsx +72 -0
- package/payload/maxy/app/public/page.tsx +266 -0
- package/payload/maxy/next.config.mjs +26 -0
- package/payload/maxy/package-lock.json +2198 -0
- package/payload/maxy/package.json +25 -0
- package/payload/maxy/proxy.ts +41 -0
- package/payload/maxy/public/brand/claude.png +0 -0
- package/payload/maxy/public/brand/maxy-black.png +0 -0
- package/payload/maxy/public/brand/maxy.png +0 -0
- package/payload/maxy/public/favicon.ico +0 -0
- package/payload/maxy/public/og-landscape.png +0 -0
- package/payload/maxy/public/og-portrait.png +0 -0
- package/payload/maxy/public/og-square.png +0 -0
- package/payload/maxy/public/pi-5.jpg +0 -0
- package/payload/maxy/public/robots.txt +5 -0
- package/payload/maxy/tsconfig.json +41 -0
- package/payload/maxy/tsconfig.tsbuildinfo +1 -0
- package/payload/maxy/ui.md +28 -0
- package/payload/platform/config/cloudflared.yml +17 -0
- package/payload/platform/knowledge/maxy.md +161 -0
- package/payload/platform/neo4j/schema.cypher +108 -0
- package/payload/platform/package-lock.json +1835 -0
- package/payload/platform/package.json +17 -0
- package/payload/platform/plugins/admin/PLUGIN.md +24 -0
- package/payload/platform/plugins/admin/hooks/pre-tool-use.sh +56 -0
- package/payload/platform/plugins/admin/hooks/session-start.sh +20 -0
- package/payload/platform/plugins/admin/mcp/dist/index.d.ts +2 -0
- package/payload/platform/plugins/admin/mcp/dist/index.d.ts.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/index.js +149 -0
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -0
- package/payload/platform/plugins/admin/mcp/package.json +18 -0
- package/payload/platform/plugins/anthropic/PLUGIN.md +30 -0
- package/payload/platform/plugins/anthropic/references/setup-guide.md +146 -0
- package/payload/platform/plugins/business-assistant/PLUGIN.md +46 -0
- package/payload/platform/plugins/business-assistant/references/crm.md +112 -0
- package/payload/platform/plugins/business-assistant/references/document-management.md +96 -0
- package/payload/platform/plugins/business-assistant/references/escalation.md +126 -0
- package/payload/platform/plugins/business-assistant/references/invoicing.md +163 -0
- package/payload/platform/plugins/business-assistant/references/quoting.md +56 -0
- package/payload/platform/plugins/business-assistant/references/scheduling.md +127 -0
- package/payload/platform/plugins/cloudflare/PLUGIN.md +31 -0
- package/payload/platform/plugins/cloudflare/mcp/dist/index.d.ts +2 -0
- package/payload/platform/plugins/cloudflare/mcp/dist/index.d.ts.map +1 -0
- package/payload/platform/plugins/cloudflare/mcp/dist/index.js +174 -0
- package/payload/platform/plugins/cloudflare/mcp/dist/index.js.map +1 -0
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.d.ts +45 -0
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.d.ts.map +1 -0
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js +256 -0
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js.map +1 -0
- package/payload/platform/plugins/cloudflare/mcp/package.json +18 -0
- package/payload/platform/plugins/cloudflare/references/setup-guide.md +110 -0
- package/payload/platform/plugins/contacts/PLUGIN.md +18 -0
- package/payload/platform/plugins/contacts/mcp/dist/index.d.ts +2 -0
- package/payload/platform/plugins/contacts/mcp/dist/index.d.ts.map +1 -0
- package/payload/platform/plugins/contacts/mcp/dist/index.js +182 -0
- package/payload/platform/plugins/contacts/mcp/dist/index.js.map +1 -0
- package/payload/platform/plugins/contacts/mcp/dist/lib/neo4j.d.ts +5 -0
- package/payload/platform/plugins/contacts/mcp/dist/lib/neo4j.d.ts.map +1 -0
- package/payload/platform/plugins/contacts/mcp/dist/lib/neo4j.js +34 -0
- package/payload/platform/plugins/contacts/mcp/dist/lib/neo4j.js.map +1 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.d.ts +19 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.d.ts.map +1 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.js +68 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.js.map +1 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-list.d.ts +22 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-list.d.ts.map +1 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-list.js +46 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-list.js.map +1 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-lookup.d.ts +20 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-lookup.d.ts.map +1 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-lookup.js +56 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-lookup.js.map +1 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-update.d.ts +13 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-update.d.ts.map +1 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-update.js +54 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-update.js.map +1 -0
- package/payload/platform/plugins/contacts/mcp/package.json +19 -0
- package/payload/platform/plugins/documents/PLUGIN.md +12 -0
- package/payload/platform/plugins/documents/mcp/dist/index.d.ts +2 -0
- package/payload/platform/plugins/documents/mcp/dist/index.d.ts.map +1 -0
- package/payload/platform/plugins/documents/mcp/dist/index.js +82 -0
- package/payload/platform/plugins/documents/mcp/dist/index.js.map +1 -0
- package/payload/platform/plugins/documents/mcp/package.json +20 -0
- package/payload/platform/plugins/memory/PLUGIN.md +17 -0
- package/payload/platform/plugins/memory/mcp/dist/index.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/index.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/index.js +164 -0
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/embeddings.d.ts +3 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/embeddings.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/embeddings.js +29 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/embeddings.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/neo4j.d.ts +5 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/neo4j.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/neo4j.js +34 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/neo4j.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-reindex.d.ts +8 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-reindex.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-reindex.js +71 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-reindex.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.d.ts +24 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js +125 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts +18 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +56 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/package.json +19 -0
- package/payload/platform/plugins/sales/PLUGIN.md +65 -0
- package/payload/platform/plugins/sales/references/close-tracking.md +76 -0
- package/payload/platform/plugins/sales/references/closing-framework.md +108 -0
- package/payload/platform/plugins/sales/references/comparisons.md +99 -0
- package/payload/platform/plugins/sales/references/competitive-positioning.md +51 -0
- package/payload/platform/plugins/sales/references/faq.md +62 -0
- package/payload/platform/plugins/sales/references/objection-handling.md +157 -0
- package/payload/platform/plugins/sales/references/pricing.md +71 -0
- package/payload/platform/plugins/sales/references/waitlist.md +23 -0
- package/payload/platform/plugins/scheduling/PLUGIN.md +12 -0
- package/payload/platform/plugins/scheduling/mcp/dist/index.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/index.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/index.js +13 -0
- package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/package.json +18 -0
- package/payload/platform/plugins/telegram/PLUGIN.md +31 -0
- package/payload/platform/plugins/telegram/mcp/dist/index.d.ts +2 -0
- package/payload/platform/plugins/telegram/mcp/dist/index.d.ts.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/index.js +101 -0
- package/payload/platform/plugins/telegram/mcp/dist/index.js.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.d.ts +27 -0
- package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.d.ts.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.js +41 -0
- package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.js.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/tools/message-history.d.ts +16 -0
- package/payload/platform/plugins/telegram/mcp/dist/tools/message-history.d.ts.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/tools/message-history.js +62 -0
- package/payload/platform/plugins/telegram/mcp/dist/tools/message-history.js.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/tools/message.d.ts +20 -0
- package/payload/platform/plugins/telegram/mcp/dist/tools/message.d.ts.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/tools/message.js +34 -0
- package/payload/platform/plugins/telegram/mcp/dist/tools/message.js.map +1 -0
- package/payload/platform/plugins/telegram/mcp/package.json +19 -0
- package/payload/platform/plugins/telegram/references/setup-guide.md +50 -0
- package/payload/platform/plugins/web/PLUGIN.md +12 -0
- package/payload/platform/plugins/web/mcp/dist/index.d.ts +2 -0
- package/payload/platform/plugins/web/mcp/dist/index.d.ts.map +1 -0
- package/payload/platform/plugins/web/mcp/dist/index.js +12 -0
- package/payload/platform/plugins/web/mcp/dist/index.js.map +1 -0
- package/payload/platform/plugins/web/mcp/package.json +18 -0
- package/payload/platform/scripts/seed-neo4j.sh +73 -0
- package/payload/platform/scripts/setup.sh +177 -0
- package/payload/platform/scripts/start.sh +62 -0
- package/payload/platform/templates/account.json +4 -0
- package/payload/platform/templates/agents/admin/IDENTITY.md +28 -0
- package/payload/platform/templates/agents/admin/SOUL.md +1 -0
- package/payload/platform/templates/agents/public/IDENTITY.md +21 -0
- package/payload/platform/templates/agents/public/SOUL.md +1 -0
- package/payload/platform/tsconfig.base.json +18 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef } from 'react'
|
|
4
|
+
import {
|
|
5
|
+
Lightbulb, Terminal, FileText, Search, Globe, Wrench, Bot,
|
|
6
|
+
Star, Sparkle, Sparkles,
|
|
7
|
+
Loader,
|
|
8
|
+
ChevronDown, ChevronRight, Check, X,
|
|
9
|
+
} from 'lucide-react'
|
|
10
|
+
import { MarkdownMessage } from './MarkdownMessage'
|
|
11
|
+
|
|
12
|
+
export type AdminEvent =
|
|
13
|
+
| { type: 'text'; content: string }
|
|
14
|
+
| { type: 'thinking'; content: string }
|
|
15
|
+
| { type: 'tool_use'; name: string; input: Record<string, unknown> }
|
|
16
|
+
| { type: 'tool_result'; name: string; output: string; error?: boolean }
|
|
17
|
+
| { type: 'subagent_start'; name: string; task: string }
|
|
18
|
+
| { type: 'subagent_end'; name: string; result: string }
|
|
19
|
+
| { type: 'status'; message: string }
|
|
20
|
+
| { type: 'usage'; input_tokens: number; output_tokens: number }
|
|
21
|
+
|
|
22
|
+
interface ActivityTimelineProps {
|
|
23
|
+
events: AdminEvent[]
|
|
24
|
+
isStreaming: boolean
|
|
25
|
+
elapsedSeconds: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// --- Star loader (cycles Sparkle → Star → Sparkles) ---
|
|
29
|
+
|
|
30
|
+
const STAR_FRAMES = [Sparkle, Star, Sparkles] as const
|
|
31
|
+
|
|
32
|
+
function StarLoader({ size = 13 }: { size?: number }) {
|
|
33
|
+
const [frame, setFrame] = useState(0)
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
const id = setInterval(() => setFrame(f => (f + 1) % 3), 380)
|
|
36
|
+
return () => clearInterval(id)
|
|
37
|
+
}, [])
|
|
38
|
+
const Icon = STAR_FRAMES[frame]
|
|
39
|
+
return <Icon size={size} className={`star-loader star-frame-${frame}`} />
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// --- Tool helpers ---
|
|
43
|
+
|
|
44
|
+
function toolSummary(name: string, input: Record<string, unknown>): string {
|
|
45
|
+
const str = (v: unknown) => String(v ?? '')
|
|
46
|
+
switch (name) {
|
|
47
|
+
case 'Read': return `Read ${str(input.file_path ?? input.path)}`
|
|
48
|
+
case 'Write': return `Write ${str(input.file_path ?? input.path)}`
|
|
49
|
+
case 'Edit': return `Edit ${str(input.file_path ?? input.path)}`
|
|
50
|
+
case 'Bash': return str(input.description ?? String(input.command ?? '').slice(0, 70))
|
|
51
|
+
case 'Glob': return `Glob ${str(input.pattern)}`
|
|
52
|
+
case 'Grep': return `Grep "${str(input.pattern)}" in ${str(input.path ?? '.')}`
|
|
53
|
+
case 'WebFetch': return `Fetch ${str(input.url)}`
|
|
54
|
+
case 'Agent': return str(input.description ?? input.task ?? 'Subagent')
|
|
55
|
+
default: {
|
|
56
|
+
// MCP tools: mcp__server__tool-name → show tool-name
|
|
57
|
+
const mcpMatch = name.match(/^mcp__[^_]+__(.+)$/)
|
|
58
|
+
const display = mcpMatch ? mcpMatch[1] : name
|
|
59
|
+
const firstKey = Object.keys(input)[0]
|
|
60
|
+
return firstKey ? `${display}: ${str(input[firstKey]).slice(0, 50)}` : display
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function ToolIcon({ name, size = 12 }: { name: string; size?: number }) {
|
|
66
|
+
switch (name) {
|
|
67
|
+
case 'Read':
|
|
68
|
+
case 'Write':
|
|
69
|
+
case 'Edit': return <FileText size={size} />
|
|
70
|
+
case 'Bash': return <Terminal size={size} />
|
|
71
|
+
case 'Glob':
|
|
72
|
+
case 'Grep': return <Search size={size} />
|
|
73
|
+
case 'WebFetch': return <Globe size={size} />
|
|
74
|
+
case 'Agent': return <Bot size={size} />
|
|
75
|
+
default: return <Wrench size={size} />
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Pair tool_use indices → tool_result indices by name, first-unmatched in stream order
|
|
80
|
+
function buildToolPairs(events: AdminEvent[]): Map<number, number> {
|
|
81
|
+
const pairs = new Map<number, number>()
|
|
82
|
+
const pending = new Map<string, number[]>()
|
|
83
|
+
events.forEach((e, i) => {
|
|
84
|
+
if (e.type === 'tool_use') {
|
|
85
|
+
const q = pending.get(e.name) ?? []
|
|
86
|
+
q.push(i)
|
|
87
|
+
pending.set(e.name, q)
|
|
88
|
+
} else if (e.type === 'tool_result') {
|
|
89
|
+
const q = pending.get(e.name)
|
|
90
|
+
if (q?.length) pairs.set(q.shift()!, i)
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
return pairs
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function formatTokens(n: number): string {
|
|
97
|
+
return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function formatElapsed(seconds: number): string {
|
|
101
|
+
if (seconds < 60) return `${seconds}s`
|
|
102
|
+
const m = Math.floor(seconds / 60)
|
|
103
|
+
const s = seconds % 60
|
|
104
|
+
return s > 0 ? `${m}m ${s}s` : `${m}m`
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// --- TimelineStep ---
|
|
108
|
+
|
|
109
|
+
interface StepProps {
|
|
110
|
+
icon: React.ReactNode
|
|
111
|
+
isPending: boolean
|
|
112
|
+
isError: boolean
|
|
113
|
+
summary: string
|
|
114
|
+
detail?: string
|
|
115
|
+
elapsed: number
|
|
116
|
+
isLast: boolean
|
|
117
|
+
expanded: boolean
|
|
118
|
+
onToggle: () => void
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function TimelineStep({ icon, isPending, isError, summary, detail, elapsed, isLast, expanded, onToggle }: StepProps) {
|
|
122
|
+
const hasDetail = !!detail
|
|
123
|
+
return (
|
|
124
|
+
<div className="tl-step">
|
|
125
|
+
<div className="tl-col">
|
|
126
|
+
<div className={`tl-icon${isPending ? ' tl-pending' : isError ? ' tl-error' : ' tl-done'}`}>
|
|
127
|
+
{icon}
|
|
128
|
+
</div>
|
|
129
|
+
{!isLast && <div className="tl-line tl-line-grow" />}
|
|
130
|
+
</div>
|
|
131
|
+
<div className="tl-body">
|
|
132
|
+
<div className="tl-row" onClick={hasDetail ? onToggle : undefined} style={{ cursor: hasDetail ? 'pointer' : 'default' }}>
|
|
133
|
+
<span className="tl-summary">{summary}</span>
|
|
134
|
+
<span className="tl-step-elapsed">{formatElapsed(elapsed)}</span>
|
|
135
|
+
{hasDetail && (
|
|
136
|
+
<span className="tl-chevron">
|
|
137
|
+
{expanded ? <ChevronDown size={10} /> : <ChevronRight size={10} />}
|
|
138
|
+
</span>
|
|
139
|
+
)}
|
|
140
|
+
</div>
|
|
141
|
+
{hasDetail && expanded && (
|
|
142
|
+
<pre className="tl-detail">{detail}</pre>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// --- Main ---
|
|
150
|
+
|
|
151
|
+
export function ActivityTimeline({ events, isStreaming, elapsedSeconds }: ActivityTimelineProps) {
|
|
152
|
+
const [expandedItems, setExpandedItems] = useState<Set<number>>(new Set())
|
|
153
|
+
// Track arrival time of each event index (client-side receipt time)
|
|
154
|
+
const arrivalRef = useRef<Map<number, number>>(new Map())
|
|
155
|
+
const prevLenRef = useRef(0)
|
|
156
|
+
// nowMs updates every second while streaming, to re-render per-step timers
|
|
157
|
+
const [nowMs, setNowMs] = useState(Date.now())
|
|
158
|
+
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
const ts = Date.now()
|
|
161
|
+
for (let i = prevLenRef.current; i < events.length; i++) {
|
|
162
|
+
if (!arrivalRef.current.has(i)) arrivalRef.current.set(i, ts)
|
|
163
|
+
}
|
|
164
|
+
prevLenRef.current = events.length
|
|
165
|
+
}, [events])
|
|
166
|
+
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
if (!isStreaming) return
|
|
169
|
+
const id = setInterval(() => setNowMs(Date.now()), 1000)
|
|
170
|
+
return () => clearInterval(id)
|
|
171
|
+
}, [isStreaming])
|
|
172
|
+
|
|
173
|
+
const toggleExpand = (i: number) => {
|
|
174
|
+
setExpandedItems(prev => {
|
|
175
|
+
const next = new Set(prev)
|
|
176
|
+
next.has(i) ? next.delete(i) : next.add(i)
|
|
177
|
+
return next
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const stepElapsed = (startIdx: number, endIdx?: number): number => {
|
|
182
|
+
const start = arrivalRef.current.get(startIdx) ?? nowMs
|
|
183
|
+
const end = endIdx !== undefined ? (arrivalRef.current.get(endIdx) ?? nowMs) : nowMs
|
|
184
|
+
return Math.max(0, Math.floor((end - start) / 1000))
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const toolPairs = buildToolPairs(events)
|
|
188
|
+
const resultIndices = new Set(toolPairs.values())
|
|
189
|
+
|
|
190
|
+
const textEvent = events.find(e => e.type === 'text')
|
|
191
|
+
const usageEvent = events.find(e => e.type === 'usage') as
|
|
192
|
+
{ type: 'usage'; input_tokens: number; output_tokens: number } | undefined
|
|
193
|
+
|
|
194
|
+
// Steps shown in timeline — exclude text, usage, done, and standalone tool_results
|
|
195
|
+
const steps = events
|
|
196
|
+
.map((e, i) => ({ e, i }))
|
|
197
|
+
.filter(({ e, i }) =>
|
|
198
|
+
e.type !== 'text' &&
|
|
199
|
+
e.type !== 'usage' &&
|
|
200
|
+
(e as { type: string }).type !== 'done' &&
|
|
201
|
+
!resultIndices.has(i)
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
const hasSteps = steps.length > 0
|
|
205
|
+
const hasText = !!textEvent
|
|
206
|
+
|
|
207
|
+
return (
|
|
208
|
+
<div className="admin-activity">
|
|
209
|
+
{/* Global loading indicator — shown before any steps arrive */}
|
|
210
|
+
{isStreaming && !hasSteps && !hasText && (
|
|
211
|
+
<div className="tl-loading">
|
|
212
|
+
<StarLoader size={13} />
|
|
213
|
+
<span className="tl-elapsed">{formatElapsed(elapsedSeconds)}</span>
|
|
214
|
+
</div>
|
|
215
|
+
)}
|
|
216
|
+
|
|
217
|
+
{/* Timeline steps */}
|
|
218
|
+
{hasSteps && (
|
|
219
|
+
<div className="tl-steps">
|
|
220
|
+
{steps.map(({ e, i }, displayIdx) => {
|
|
221
|
+
const isFirst = displayIdx === 0
|
|
222
|
+
const isLast = displayIdx === steps.length - 1
|
|
223
|
+
|
|
224
|
+
if (e.type === 'thinking') {
|
|
225
|
+
const isExpanded = !expandedItems.has(i) // thinking is expanded by default
|
|
226
|
+
const nextIdx = !isLast ? steps[displayIdx + 1].i : undefined
|
|
227
|
+
return (
|
|
228
|
+
<div key={i} className="tl-step">
|
|
229
|
+
<div className="tl-col">
|
|
230
|
+
<div className="tl-icon tl-dim"><Lightbulb size={11} /></div>
|
|
231
|
+
{!isLast && <div className="tl-line tl-line-grow" />}
|
|
232
|
+
</div>
|
|
233
|
+
<div className="tl-body">
|
|
234
|
+
<div className="tl-row tl-row-top" onClick={() => toggleExpand(i)} style={{ cursor: 'pointer' }}>
|
|
235
|
+
<div className="tl-thinking-col">
|
|
236
|
+
<span className="tl-summary tl-thinking-label">Thinking</span>
|
|
237
|
+
{isExpanded && (
|
|
238
|
+
<div className="tl-thinking-body">{e.content}</div>
|
|
239
|
+
)}
|
|
240
|
+
</div>
|
|
241
|
+
<span className="tl-step-elapsed">{formatElapsed(stepElapsed(i, nextIdx))}</span>
|
|
242
|
+
<span className="tl-chevron">
|
|
243
|
+
{isExpanded ? <ChevronDown size={10} /> : <ChevronRight size={10} />}
|
|
244
|
+
</span>
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (e.type === 'status') {
|
|
252
|
+
return <div key={i} className="tl-status">{e.message}</div>
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (e.type === 'tool_use') {
|
|
256
|
+
const resultIdx = toolPairs.get(i)
|
|
257
|
+
const result = resultIdx !== undefined ? events[resultIdx] : undefined
|
|
258
|
+
const isPending = result === undefined
|
|
259
|
+
const isError = result?.type === 'tool_result' && !!result.error
|
|
260
|
+
|
|
261
|
+
const icon = isPending
|
|
262
|
+
? <Loader size={11} className="tl-spinner" />
|
|
263
|
+
: isError ? <X size={11} /> : <ToolIcon name={e.name} size={11} />
|
|
264
|
+
|
|
265
|
+
const detail = result?.type === 'tool_result'
|
|
266
|
+
? `Input:\n${JSON.stringify(e.input, null, 2)}\n\nResult:\n${result.output}`
|
|
267
|
+
: `Input:\n${JSON.stringify(e.input, null, 2)}`
|
|
268
|
+
|
|
269
|
+
return (
|
|
270
|
+
<TimelineStep
|
|
271
|
+
key={i}
|
|
272
|
+
icon={icon}
|
|
273
|
+
isPending={isPending}
|
|
274
|
+
isError={isError}
|
|
275
|
+
summary={toolSummary(e.name, e.input)}
|
|
276
|
+
detail={detail}
|
|
277
|
+
elapsed={stepElapsed(i, resultIdx)}
|
|
278
|
+
|
|
279
|
+
isLast={isLast}
|
|
280
|
+
expanded={expandedItems.has(i)}
|
|
281
|
+
onToggle={() => toggleExpand(i)}
|
|
282
|
+
/>
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (e.type === 'subagent_start') {
|
|
287
|
+
const isDone = events.some(ev => ev.type === 'subagent_end' && ev.name === e.name)
|
|
288
|
+
const endIdx = isDone ? events.findIndex(ev => ev.type === 'subagent_end' && ev.name === e.name) : undefined
|
|
289
|
+
return (
|
|
290
|
+
<TimelineStep
|
|
291
|
+
key={i}
|
|
292
|
+
icon={isDone ? <Bot size={11} /> : <Loader size={11} className="tl-spinner" />}
|
|
293
|
+
isPending={!isDone}
|
|
294
|
+
isError={false}
|
|
295
|
+
summary={`Agent: ${e.task.slice(0, 60)}${e.task.length > 60 ? '…' : ''}`}
|
|
296
|
+
detail={e.task}
|
|
297
|
+
elapsed={stepElapsed(i, endIdx)}
|
|
298
|
+
|
|
299
|
+
isLast={isLast}
|
|
300
|
+
expanded={expandedItems.has(i)}
|
|
301
|
+
onToggle={() => toggleExpand(i)}
|
|
302
|
+
/>
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (e.type === 'subagent_end') {
|
|
307
|
+
return (
|
|
308
|
+
<TimelineStep
|
|
309
|
+
key={i}
|
|
310
|
+
icon={<Check size={11} />}
|
|
311
|
+
isPending={false}
|
|
312
|
+
isError={false}
|
|
313
|
+
summary={`Agent complete`}
|
|
314
|
+
detail={e.result}
|
|
315
|
+
elapsed={stepElapsed(i)}
|
|
316
|
+
|
|
317
|
+
isLast={isLast}
|
|
318
|
+
expanded={expandedItems.has(i)}
|
|
319
|
+
onToggle={() => toggleExpand(i)}
|
|
320
|
+
/>
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return null
|
|
325
|
+
})}
|
|
326
|
+
</div>
|
|
327
|
+
)}
|
|
328
|
+
|
|
329
|
+
{/* Text response */}
|
|
330
|
+
{textEvent && (
|
|
331
|
+
<MarkdownMessage content={textEvent.content} />
|
|
332
|
+
)}
|
|
333
|
+
|
|
334
|
+
{/* Footer: total elapsed + token usage */}
|
|
335
|
+
{(hasText || (!isStreaming && hasSteps)) && (
|
|
336
|
+
<div className="tl-footer">
|
|
337
|
+
<span>{formatElapsed(elapsedSeconds)}</span>
|
|
338
|
+
{usageEvent && (
|
|
339
|
+
<>
|
|
340
|
+
<span className="tl-footer-sep">·</span>
|
|
341
|
+
<span>{formatTokens(usageEvent.input_tokens + usageEvent.output_tokens)} tokens</span>
|
|
342
|
+
</>
|
|
343
|
+
)}
|
|
344
|
+
</div>
|
|
345
|
+
)}
|
|
346
|
+
</div>
|
|
347
|
+
)
|
|
348
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import ReactMarkdown from 'react-markdown'
|
|
4
|
+
import type { Components } from 'react-markdown'
|
|
5
|
+
|
|
6
|
+
interface MarkdownMessageProps {
|
|
7
|
+
content: string
|
|
8
|
+
elapsedSeconds?: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const components: Components = {
|
|
12
|
+
p: ({ children }) => <p className="md-p">{children}</p>,
|
|
13
|
+
h1: ({ children }) => <h1 className="md-heading">{children}</h1>,
|
|
14
|
+
h2: ({ children }) => <h2 className="md-heading">{children}</h2>,
|
|
15
|
+
h3: ({ children }) => <h3 className="md-heading">{children}</h3>,
|
|
16
|
+
code: ({ children, className }) => {
|
|
17
|
+
const isBlock = className?.startsWith('language-')
|
|
18
|
+
return isBlock
|
|
19
|
+
? <code className="md-code-block">{children}</code>
|
|
20
|
+
: <code className="md-code-inline">{children}</code>
|
|
21
|
+
},
|
|
22
|
+
pre: ({ children }) => <pre className="md-pre">{children}</pre>,
|
|
23
|
+
ul: ({ children }) => <ul className="md-list">{children}</ul>,
|
|
24
|
+
ol: ({ children }) => <ol className="md-list md-list-ordered">{children}</ol>,
|
|
25
|
+
li: ({ children }) => <li className="md-list-item">{children}</li>,
|
|
26
|
+
hr: () => <hr className="md-hr" />,
|
|
27
|
+
strong: ({ children }) => <strong className="md-strong">{children}</strong>,
|
|
28
|
+
a: ({ href, children }) => <a href={href} className="md-link" target="_blank" rel="noopener noreferrer">{children}</a>,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function MarkdownMessage({ content, elapsedSeconds }: MarkdownMessageProps) {
|
|
32
|
+
return (
|
|
33
|
+
<div className="markdown-message">
|
|
34
|
+
<ReactMarkdown components={components}>{content}</ReactMarkdown>
|
|
35
|
+
{elapsedSeconds !== undefined && (
|
|
36
|
+
<div className="message-meta">{elapsedSeconds}s</div>
|
|
37
|
+
)}
|
|
38
|
+
</div>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { NextRequest } from 'next/server'
|
|
2
|
+
import { invokeAgent, validateSession } from '../../../lib/claude-agent'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* POST /api/admin/chat
|
|
6
|
+
* Invokes the admin agent via Claude Agent SDK and streams full activity as typed SSE events.
|
|
7
|
+
* Body: { message: string, session_key: string }
|
|
8
|
+
* Requires a valid admin session (PIN-gated via /api/admin/session).
|
|
9
|
+
*/
|
|
10
|
+
export async function POST(req: NextRequest) {
|
|
11
|
+
let body: { message: string; session_key: string }
|
|
12
|
+
try {
|
|
13
|
+
body = await req.json()
|
|
14
|
+
} catch {
|
|
15
|
+
return new Response(JSON.stringify({ error: 'Invalid request' }), {
|
|
16
|
+
status: 400,
|
|
17
|
+
headers: { 'Content-Type': 'application/json' },
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!body.message || !body.session_key) {
|
|
22
|
+
return new Response(
|
|
23
|
+
JSON.stringify({ error: 'message and session_key required' }),
|
|
24
|
+
{ status: 400, headers: { 'Content-Type': 'application/json' } },
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Validate that this session was created via admin PIN auth
|
|
29
|
+
if (!validateSession(body.session_key, 'admin')) {
|
|
30
|
+
return new Response(
|
|
31
|
+
JSON.stringify({ error: 'Invalid or expired admin session' }),
|
|
32
|
+
{ status: 401, headers: { 'Content-Type': 'application/json' } },
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const encoder = new TextEncoder()
|
|
37
|
+
|
|
38
|
+
const readable = new ReadableStream({
|
|
39
|
+
async start(controller) {
|
|
40
|
+
try {
|
|
41
|
+
for await (const event of invokeAgent(
|
|
42
|
+
{ type: 'admin' },
|
|
43
|
+
body.message,
|
|
44
|
+
body.session_key,
|
|
45
|
+
)) {
|
|
46
|
+
const data = JSON.stringify(event)
|
|
47
|
+
controller.enqueue(encoder.encode(`data: ${data}\n\n`))
|
|
48
|
+
}
|
|
49
|
+
} catch (err) {
|
|
50
|
+
const message = err instanceof Error ? err.message : 'Agent error'
|
|
51
|
+
controller.enqueue(
|
|
52
|
+
encoder.encode(
|
|
53
|
+
`data: ${JSON.stringify({ type: 'text', content: `Error: ${message}` })}\n\n`,
|
|
54
|
+
),
|
|
55
|
+
)
|
|
56
|
+
controller.enqueue(
|
|
57
|
+
encoder.encode(`data: ${JSON.stringify({ type: 'done' })}\n\n`),
|
|
58
|
+
)
|
|
59
|
+
} finally {
|
|
60
|
+
controller.close()
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
return new Response(readable, {
|
|
66
|
+
headers: {
|
|
67
|
+
'Content-Type': 'text/event-stream',
|
|
68
|
+
'Cache-Control': 'no-cache',
|
|
69
|
+
Connection: 'keep-alive',
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs'
|
|
3
|
+
import { resolve } from 'node:path'
|
|
4
|
+
import { homedir } from 'node:os'
|
|
5
|
+
|
|
6
|
+
const LOG_DIR = resolve(homedir(), '.maxy/logs')
|
|
7
|
+
const TAIL_BYTES = 8192 // last 8KB per file
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* GET /api/admin/logs
|
|
11
|
+
* Returns the tail of every log file in ~/.maxy/logs, newest-modified first.
|
|
12
|
+
* No auth — logs are local-only and access is already restricted to the LAN.
|
|
13
|
+
*/
|
|
14
|
+
export async function GET() {
|
|
15
|
+
let files: string[]
|
|
16
|
+
try {
|
|
17
|
+
files = readdirSync(LOG_DIR).filter(f => f.endsWith('.log'))
|
|
18
|
+
} catch {
|
|
19
|
+
return NextResponse.json({ logs: {} })
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const logs: Record<string, string> = {}
|
|
23
|
+
|
|
24
|
+
files
|
|
25
|
+
.map(f => ({ name: f, mtime: statSync(resolve(LOG_DIR, f)).mtimeMs }))
|
|
26
|
+
.sort((a, b) => b.mtime - a.mtime)
|
|
27
|
+
.forEach(({ name }) => {
|
|
28
|
+
try {
|
|
29
|
+
const content = readFileSync(resolve(LOG_DIR, name))
|
|
30
|
+
const tail = content.length > TAIL_BYTES
|
|
31
|
+
? content.subarray(content.length - TAIL_BYTES).toString('utf-8')
|
|
32
|
+
: content.toString('utf-8')
|
|
33
|
+
logs[name] = tail.trim() || '(empty)'
|
|
34
|
+
} catch {
|
|
35
|
+
logs[name] = '(unreadable)'
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
return NextResponse.json({ logs })
|
|
40
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { registerSession } from '../../../lib/claude-agent'
|
|
3
|
+
import { readFileSync, readdirSync, existsSync } from 'node:fs'
|
|
4
|
+
import { resolve } from 'node:path'
|
|
5
|
+
import { createHash } from 'node:crypto'
|
|
6
|
+
|
|
7
|
+
const PERSISTENT_DIR = resolve(process.env.HOME ?? '/root', '.maxy')
|
|
8
|
+
const PIN_FILE = resolve(PERSISTENT_DIR, '.admin-pin')
|
|
9
|
+
const ACCOUNTS_DIR = resolve(process.cwd(), '../platform/config/accounts')
|
|
10
|
+
|
|
11
|
+
function hashPin(pin: string): string {
|
|
12
|
+
return createHash('sha256').update(pin).digest('hex')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getStoredPinHash(): string | null {
|
|
16
|
+
if (existsSync(PIN_FILE)) {
|
|
17
|
+
return readFileSync(PIN_FILE, 'utf-8').trim()
|
|
18
|
+
}
|
|
19
|
+
// Fallback to env var (legacy)
|
|
20
|
+
if (process.env.ADMIN_PIN) {
|
|
21
|
+
return hashPin(process.env.ADMIN_PIN)
|
|
22
|
+
}
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getDefaultAccountId(): string | null {
|
|
27
|
+
if (!existsSync(ACCOUNTS_DIR)) return null
|
|
28
|
+
const entries = readdirSync(ACCOUNTS_DIR, { withFileTypes: true })
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
if (!entry.isDirectory()) continue
|
|
31
|
+
const configPath = resolve(ACCOUNTS_DIR, entry.name, 'account.json')
|
|
32
|
+
if (!existsSync(configPath)) continue
|
|
33
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'))
|
|
34
|
+
return config.accountId
|
|
35
|
+
}
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* POST /api/admin/session
|
|
41
|
+
* PIN-gated session creation for the admin agent.
|
|
42
|
+
* Body: { pin: string }
|
|
43
|
+
*/
|
|
44
|
+
export async function POST(req: Request) {
|
|
45
|
+
const storedHash = getStoredPinHash()
|
|
46
|
+
|
|
47
|
+
if (!storedHash) {
|
|
48
|
+
return NextResponse.json(
|
|
49
|
+
{ error: 'PIN not configured. Complete onboarding first.' },
|
|
50
|
+
{ status: 503 },
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let body: { pin: string }
|
|
55
|
+
try {
|
|
56
|
+
body = await req.json()
|
|
57
|
+
} catch {
|
|
58
|
+
return NextResponse.json({ error: 'Invalid request' }, { status: 400 })
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!body.pin || hashPin(body.pin) !== storedHash) {
|
|
62
|
+
return NextResponse.json({ error: 'Invalid PIN' }, { status: 401 })
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const accountId = getDefaultAccountId() ?? 'default'
|
|
66
|
+
|
|
67
|
+
const sessionKey = crypto.randomUUID()
|
|
68
|
+
registerSession(sessionKey, 'admin', accountId)
|
|
69
|
+
|
|
70
|
+
return NextResponse.json({
|
|
71
|
+
session_key: sessionKey,
|
|
72
|
+
agent_id: 'admin',
|
|
73
|
+
})
|
|
74
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { NextRequest } from 'next/server'
|
|
2
|
+
import { invokeAgent, validateSession } from '../../lib/claude-agent'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* POST /api/chat
|
|
6
|
+
* Invokes the public agent via Claude Agent SDK and streams SSE responses.
|
|
7
|
+
* Body: { message: string, session_key: string }
|
|
8
|
+
* SSE format: data: { text: "..." } (matches existing frontend)
|
|
9
|
+
*/
|
|
10
|
+
export async function POST(req: NextRequest) {
|
|
11
|
+
let body: { message: string; session_key: string }
|
|
12
|
+
try {
|
|
13
|
+
body = await req.json()
|
|
14
|
+
} catch {
|
|
15
|
+
return new Response(JSON.stringify({ error: 'Invalid request' }), {
|
|
16
|
+
status: 400,
|
|
17
|
+
headers: { 'Content-Type': 'application/json' },
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!body.message || !body.session_key) {
|
|
22
|
+
return new Response(
|
|
23
|
+
JSON.stringify({ error: 'message and session_key required' }),
|
|
24
|
+
{ status: 400, headers: { 'Content-Type': 'application/json' } },
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!validateSession(body.session_key, 'public')) {
|
|
29
|
+
return new Response(
|
|
30
|
+
JSON.stringify({ error: 'Invalid or expired session' }),
|
|
31
|
+
{ status: 401, headers: { 'Content-Type': 'application/json' } },
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const encoder = new TextEncoder()
|
|
36
|
+
|
|
37
|
+
const readable = new ReadableStream({
|
|
38
|
+
async start(controller) {
|
|
39
|
+
try {
|
|
40
|
+
for await (const event of invokeAgent(
|
|
41
|
+
{ type: 'public' },
|
|
42
|
+
body.message,
|
|
43
|
+
body.session_key,
|
|
44
|
+
)) {
|
|
45
|
+
if (event.type === 'text') {
|
|
46
|
+
const data = JSON.stringify({ text: event.content })
|
|
47
|
+
controller.enqueue(encoder.encode(`data: ${data}\n\n`))
|
|
48
|
+
} else if (event.type === 'done') {
|
|
49
|
+
controller.enqueue(encoder.encode('data: [DONE]\n\n'))
|
|
50
|
+
}
|
|
51
|
+
// Public agent: only text events reach the client
|
|
52
|
+
}
|
|
53
|
+
} catch (err) {
|
|
54
|
+
const message = err instanceof Error ? err.message : 'Agent error'
|
|
55
|
+
controller.enqueue(
|
|
56
|
+
encoder.encode(`data: ${JSON.stringify({ error: message })}\n\n`),
|
|
57
|
+
)
|
|
58
|
+
controller.enqueue(encoder.encode('data: [DONE]\n\n'))
|
|
59
|
+
} finally {
|
|
60
|
+
controller.close()
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
return new Response(readable, {
|
|
66
|
+
headers: {
|
|
67
|
+
'Content-Type': 'text/event-stream',
|
|
68
|
+
'Cache-Control': 'no-cache',
|
|
69
|
+
Connection: 'keep-alive',
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { resolve } from 'node:path'
|
|
4
|
+
import { homedir } from 'node:os'
|
|
5
|
+
import { execFileSync } from 'node:child_process'
|
|
6
|
+
|
|
7
|
+
const PIN_FILE = resolve(homedir(), '.maxy/.admin-pin')
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* GET /api/health
|
|
11
|
+
* Returns platform state for the onboarding flow.
|
|
12
|
+
*/
|
|
13
|
+
export async function GET() {
|
|
14
|
+
const pinConfigured = existsSync(PIN_FILE)
|
|
15
|
+
|
|
16
|
+
let claudeAuthenticated = false
|
|
17
|
+
try {
|
|
18
|
+
execFileSync('claude', ['auth', 'status'], { encoding: 'utf-8', timeout: 5000 })
|
|
19
|
+
claudeAuthenticated = true
|
|
20
|
+
} catch { /* claude not installed or not authenticated — non-zero exit means not logged in */ }
|
|
21
|
+
|
|
22
|
+
return NextResponse.json({
|
|
23
|
+
pin_configured: pinConfigured,
|
|
24
|
+
claude_authenticated: claudeAuthenticated,
|
|
25
|
+
})
|
|
26
|
+
}
|