@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.
Files changed (181) hide show
  1. package/dist/index.js +428 -0
  2. package/package.json +31 -0
  3. package/payload/maxy/.env.example +12 -0
  4. package/payload/maxy/app/admin/components/ActivityTimeline.tsx +348 -0
  5. package/payload/maxy/app/admin/components/MarkdownMessage.tsx +40 -0
  6. package/payload/maxy/app/api/admin/chat/route.ts +72 -0
  7. package/payload/maxy/app/api/admin/logs/route.ts +40 -0
  8. package/payload/maxy/app/api/admin/session/route.ts +74 -0
  9. package/payload/maxy/app/api/chat/route.ts +72 -0
  10. package/payload/maxy/app/api/health/route.ts +26 -0
  11. package/payload/maxy/app/api/onboarding/claude-auth/route.ts +216 -0
  12. package/payload/maxy/app/api/onboarding/set-pin/route.ts +44 -0
  13. package/payload/maxy/app/api/session/route.ts +51 -0
  14. package/payload/maxy/app/api/telegram/webhook/route.ts +107 -0
  15. package/payload/maxy/app/apple-icon.png +0 -0
  16. package/payload/maxy/app/bot/page.tsx +373 -0
  17. package/payload/maxy/app/favicon.ico +0 -0
  18. package/payload/maxy/app/globals.css +1681 -0
  19. package/payload/maxy/app/layout.tsx +58 -0
  20. package/payload/maxy/app/lib/claude-agent.ts +503 -0
  21. package/payload/maxy/app/og/layout.tsx +15 -0
  22. package/payload/maxy/app/og/page.tsx +252 -0
  23. package/payload/maxy/app/page.tsx +594 -0
  24. package/payload/maxy/app/privacy/page.tsx +72 -0
  25. package/payload/maxy/app/public/page.tsx +266 -0
  26. package/payload/maxy/next.config.mjs +26 -0
  27. package/payload/maxy/package-lock.json +2198 -0
  28. package/payload/maxy/package.json +25 -0
  29. package/payload/maxy/proxy.ts +41 -0
  30. package/payload/maxy/public/brand/claude.png +0 -0
  31. package/payload/maxy/public/brand/maxy-black.png +0 -0
  32. package/payload/maxy/public/brand/maxy.png +0 -0
  33. package/payload/maxy/public/favicon.ico +0 -0
  34. package/payload/maxy/public/og-landscape.png +0 -0
  35. package/payload/maxy/public/og-portrait.png +0 -0
  36. package/payload/maxy/public/og-square.png +0 -0
  37. package/payload/maxy/public/pi-5.jpg +0 -0
  38. package/payload/maxy/public/robots.txt +5 -0
  39. package/payload/maxy/tsconfig.json +41 -0
  40. package/payload/maxy/tsconfig.tsbuildinfo +1 -0
  41. package/payload/maxy/ui.md +28 -0
  42. package/payload/platform/config/cloudflared.yml +17 -0
  43. package/payload/platform/knowledge/maxy.md +161 -0
  44. package/payload/platform/neo4j/schema.cypher +108 -0
  45. package/payload/platform/package-lock.json +1835 -0
  46. package/payload/platform/package.json +17 -0
  47. package/payload/platform/plugins/admin/PLUGIN.md +24 -0
  48. package/payload/platform/plugins/admin/hooks/pre-tool-use.sh +56 -0
  49. package/payload/platform/plugins/admin/hooks/session-start.sh +20 -0
  50. package/payload/platform/plugins/admin/mcp/dist/index.d.ts +2 -0
  51. package/payload/platform/plugins/admin/mcp/dist/index.d.ts.map +1 -0
  52. package/payload/platform/plugins/admin/mcp/dist/index.js +149 -0
  53. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -0
  54. package/payload/platform/plugins/admin/mcp/package.json +18 -0
  55. package/payload/platform/plugins/anthropic/PLUGIN.md +30 -0
  56. package/payload/platform/plugins/anthropic/references/setup-guide.md +146 -0
  57. package/payload/platform/plugins/business-assistant/PLUGIN.md +46 -0
  58. package/payload/platform/plugins/business-assistant/references/crm.md +112 -0
  59. package/payload/platform/plugins/business-assistant/references/document-management.md +96 -0
  60. package/payload/platform/plugins/business-assistant/references/escalation.md +126 -0
  61. package/payload/platform/plugins/business-assistant/references/invoicing.md +163 -0
  62. package/payload/platform/plugins/business-assistant/references/quoting.md +56 -0
  63. package/payload/platform/plugins/business-assistant/references/scheduling.md +127 -0
  64. package/payload/platform/plugins/cloudflare/PLUGIN.md +31 -0
  65. package/payload/platform/plugins/cloudflare/mcp/dist/index.d.ts +2 -0
  66. package/payload/platform/plugins/cloudflare/mcp/dist/index.d.ts.map +1 -0
  67. package/payload/platform/plugins/cloudflare/mcp/dist/index.js +174 -0
  68. package/payload/platform/plugins/cloudflare/mcp/dist/index.js.map +1 -0
  69. package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.d.ts +45 -0
  70. package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.d.ts.map +1 -0
  71. package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js +256 -0
  72. package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js.map +1 -0
  73. package/payload/platform/plugins/cloudflare/mcp/package.json +18 -0
  74. package/payload/platform/plugins/cloudflare/references/setup-guide.md +110 -0
  75. package/payload/platform/plugins/contacts/PLUGIN.md +18 -0
  76. package/payload/platform/plugins/contacts/mcp/dist/index.d.ts +2 -0
  77. package/payload/platform/plugins/contacts/mcp/dist/index.d.ts.map +1 -0
  78. package/payload/platform/plugins/contacts/mcp/dist/index.js +182 -0
  79. package/payload/platform/plugins/contacts/mcp/dist/index.js.map +1 -0
  80. package/payload/platform/plugins/contacts/mcp/dist/lib/neo4j.d.ts +5 -0
  81. package/payload/platform/plugins/contacts/mcp/dist/lib/neo4j.d.ts.map +1 -0
  82. package/payload/platform/plugins/contacts/mcp/dist/lib/neo4j.js +34 -0
  83. package/payload/platform/plugins/contacts/mcp/dist/lib/neo4j.js.map +1 -0
  84. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.d.ts +19 -0
  85. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.d.ts.map +1 -0
  86. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.js +68 -0
  87. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.js.map +1 -0
  88. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-list.d.ts +22 -0
  89. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-list.d.ts.map +1 -0
  90. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-list.js +46 -0
  91. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-list.js.map +1 -0
  92. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-lookup.d.ts +20 -0
  93. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-lookup.d.ts.map +1 -0
  94. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-lookup.js +56 -0
  95. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-lookup.js.map +1 -0
  96. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-update.d.ts +13 -0
  97. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-update.d.ts.map +1 -0
  98. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-update.js +54 -0
  99. package/payload/platform/plugins/contacts/mcp/dist/tools/contact-update.js.map +1 -0
  100. package/payload/platform/plugins/contacts/mcp/package.json +19 -0
  101. package/payload/platform/plugins/documents/PLUGIN.md +12 -0
  102. package/payload/platform/plugins/documents/mcp/dist/index.d.ts +2 -0
  103. package/payload/platform/plugins/documents/mcp/dist/index.d.ts.map +1 -0
  104. package/payload/platform/plugins/documents/mcp/dist/index.js +82 -0
  105. package/payload/platform/plugins/documents/mcp/dist/index.js.map +1 -0
  106. package/payload/platform/plugins/documents/mcp/package.json +20 -0
  107. package/payload/platform/plugins/memory/PLUGIN.md +17 -0
  108. package/payload/platform/plugins/memory/mcp/dist/index.d.ts +2 -0
  109. package/payload/platform/plugins/memory/mcp/dist/index.d.ts.map +1 -0
  110. package/payload/platform/plugins/memory/mcp/dist/index.js +164 -0
  111. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -0
  112. package/payload/platform/plugins/memory/mcp/dist/lib/embeddings.d.ts +3 -0
  113. package/payload/platform/plugins/memory/mcp/dist/lib/embeddings.d.ts.map +1 -0
  114. package/payload/platform/plugins/memory/mcp/dist/lib/embeddings.js +29 -0
  115. package/payload/platform/plugins/memory/mcp/dist/lib/embeddings.js.map +1 -0
  116. package/payload/platform/plugins/memory/mcp/dist/lib/neo4j.d.ts +5 -0
  117. package/payload/platform/plugins/memory/mcp/dist/lib/neo4j.d.ts.map +1 -0
  118. package/payload/platform/plugins/memory/mcp/dist/lib/neo4j.js +34 -0
  119. package/payload/platform/plugins/memory/mcp/dist/lib/neo4j.js.map +1 -0
  120. package/payload/platform/plugins/memory/mcp/dist/tools/memory-reindex.d.ts +8 -0
  121. package/payload/platform/plugins/memory/mcp/dist/tools/memory-reindex.d.ts.map +1 -0
  122. package/payload/platform/plugins/memory/mcp/dist/tools/memory-reindex.js +71 -0
  123. package/payload/platform/plugins/memory/mcp/dist/tools/memory-reindex.js.map +1 -0
  124. package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.d.ts +24 -0
  125. package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.d.ts.map +1 -0
  126. package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js +125 -0
  127. package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js.map +1 -0
  128. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts +18 -0
  129. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -0
  130. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +56 -0
  131. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -0
  132. package/payload/platform/plugins/memory/mcp/package.json +19 -0
  133. package/payload/platform/plugins/sales/PLUGIN.md +65 -0
  134. package/payload/platform/plugins/sales/references/close-tracking.md +76 -0
  135. package/payload/platform/plugins/sales/references/closing-framework.md +108 -0
  136. package/payload/platform/plugins/sales/references/comparisons.md +99 -0
  137. package/payload/platform/plugins/sales/references/competitive-positioning.md +51 -0
  138. package/payload/platform/plugins/sales/references/faq.md +62 -0
  139. package/payload/platform/plugins/sales/references/objection-handling.md +157 -0
  140. package/payload/platform/plugins/sales/references/pricing.md +71 -0
  141. package/payload/platform/plugins/sales/references/waitlist.md +23 -0
  142. package/payload/platform/plugins/scheduling/PLUGIN.md +12 -0
  143. package/payload/platform/plugins/scheduling/mcp/dist/index.d.ts +2 -0
  144. package/payload/platform/plugins/scheduling/mcp/dist/index.d.ts.map +1 -0
  145. package/payload/platform/plugins/scheduling/mcp/dist/index.js +13 -0
  146. package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -0
  147. package/payload/platform/plugins/scheduling/mcp/package.json +18 -0
  148. package/payload/platform/plugins/telegram/PLUGIN.md +31 -0
  149. package/payload/platform/plugins/telegram/mcp/dist/index.d.ts +2 -0
  150. package/payload/platform/plugins/telegram/mcp/dist/index.d.ts.map +1 -0
  151. package/payload/platform/plugins/telegram/mcp/dist/index.js +101 -0
  152. package/payload/platform/plugins/telegram/mcp/dist/index.js.map +1 -0
  153. package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.d.ts +27 -0
  154. package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.d.ts.map +1 -0
  155. package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.js +41 -0
  156. package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.js.map +1 -0
  157. package/payload/platform/plugins/telegram/mcp/dist/tools/message-history.d.ts +16 -0
  158. package/payload/platform/plugins/telegram/mcp/dist/tools/message-history.d.ts.map +1 -0
  159. package/payload/platform/plugins/telegram/mcp/dist/tools/message-history.js +62 -0
  160. package/payload/platform/plugins/telegram/mcp/dist/tools/message-history.js.map +1 -0
  161. package/payload/platform/plugins/telegram/mcp/dist/tools/message.d.ts +20 -0
  162. package/payload/platform/plugins/telegram/mcp/dist/tools/message.d.ts.map +1 -0
  163. package/payload/platform/plugins/telegram/mcp/dist/tools/message.js +34 -0
  164. package/payload/platform/plugins/telegram/mcp/dist/tools/message.js.map +1 -0
  165. package/payload/platform/plugins/telegram/mcp/package.json +19 -0
  166. package/payload/platform/plugins/telegram/references/setup-guide.md +50 -0
  167. package/payload/platform/plugins/web/PLUGIN.md +12 -0
  168. package/payload/platform/plugins/web/mcp/dist/index.d.ts +2 -0
  169. package/payload/platform/plugins/web/mcp/dist/index.d.ts.map +1 -0
  170. package/payload/platform/plugins/web/mcp/dist/index.js +12 -0
  171. package/payload/platform/plugins/web/mcp/dist/index.js.map +1 -0
  172. package/payload/platform/plugins/web/mcp/package.json +18 -0
  173. package/payload/platform/scripts/seed-neo4j.sh +73 -0
  174. package/payload/platform/scripts/setup.sh +177 -0
  175. package/payload/platform/scripts/start.sh +62 -0
  176. package/payload/platform/templates/account.json +4 -0
  177. package/payload/platform/templates/agents/admin/IDENTITY.md +28 -0
  178. package/payload/platform/templates/agents/admin/SOUL.md +1 -0
  179. package/payload/platform/templates/agents/public/IDENTITY.md +21 -0
  180. package/payload/platform/templates/agents/public/SOUL.md +1 -0
  181. 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
+ }