shraga 0.0.3 → 0.1.1

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 (180) hide show
  1. package/README.md +82 -23
  2. package/defaults/agents/summarizer.md +16 -0
  3. package/defaults/agents/trace-extractor.md +84 -0
  4. package/defaults/bin/claude +45 -0
  5. package/defaults/bin/claude-revive +17 -0
  6. package/defaults/extensions/README.md +70 -0
  7. package/defaults/extensions/selftest.ext.ts +43 -0
  8. package/defaults/extensions/stripe-webhook.ext.ts +58 -0
  9. package/defaults/gmail-triage-prompt.md +42 -0
  10. package/defaults/scripts/README +4 -0
  11. package/defaults/scripts/agent-once.ts +67 -0
  12. package/defaults/scripts/backfill-slack-usernames.ts +82 -0
  13. package/defaults/scripts/notifier-throttle.ts +44 -0
  14. package/defaults/scripts/summarize-conversations.ts +5 -0
  15. package/defaults/shraga.config.ts +29 -0
  16. package/defaults/skills/add-skill.md +14 -0
  17. package/defaults/skills/artifacts.md +116 -0
  18. package/defaults/skills/code-review.md +26 -0
  19. package/defaults/skills/communications.md +54 -0
  20. package/defaults/skills/context-audit.md +87 -0
  21. package/defaults/skills/debug.md +10 -0
  22. package/defaults/skills/garden.md +179 -0
  23. package/defaults/skills/github-contributor.md +35 -0
  24. package/defaults/skills/identity.md +30 -0
  25. package/defaults/skills/mcp-server.md +62 -0
  26. package/defaults/skills/mcps-sync.md +105 -0
  27. package/defaults/skills/plan.md +9 -0
  28. package/defaults/skills/platform.md +177 -0
  29. package/defaults/skills/reconcile.md +239 -0
  30. package/defaults/skills/scheduler.md +192 -0
  31. package/defaults/skills/self-aware.md +136 -0
  32. package/defaults/skills/shraga-know.md +333 -0
  33. package/defaults/skills/stripe.md +55 -0
  34. package/defaults/skills/write-tests.md +10 -0
  35. package/defaults/skills-defaults.json +1 -0
  36. package/defaults/system-prompt.md +46 -0
  37. package/defaults/workspace/context.md +28 -0
  38. package/defaults/workspace.md +50 -0
  39. package/defaults/zdotdir/.gitignore +8 -0
  40. package/defaults/zdotdir/.zlogin +3 -0
  41. package/defaults/zdotdir/.zprofile +1 -0
  42. package/defaults/zdotdir/.zshenv +4 -0
  43. package/defaults/zdotdir/.zshrc +3 -0
  44. package/dist/client/assets/index-BoHttkMt.js +1940 -0
  45. package/dist/client/assets/index-DdibEb2O.css +10 -0
  46. package/dist/client/index.html +22 -0
  47. package/package.json +59 -14
  48. package/src/cli.ts +71 -46
  49. package/src/client/App.tsx +510 -0
  50. package/src/client/components/ArtifactCard.tsx +26 -0
  51. package/src/client/components/ArtifactPanel.tsx +138 -0
  52. package/src/client/components/AuthedImage.tsx +85 -0
  53. package/src/client/components/AutocompleteTextarea.tsx +149 -0
  54. package/src/client/components/ChatView.tsx +866 -0
  55. package/src/client/components/CliAuthConsent.tsx +98 -0
  56. package/src/client/components/ConfigPanel.tsx +328 -0
  57. package/src/client/components/ConversationHeader.tsx +156 -0
  58. package/src/client/components/ConversationPane.tsx +277 -0
  59. package/src/client/components/LoginPage.tsx +81 -0
  60. package/src/client/components/MachineStats.tsx +77 -0
  61. package/src/client/components/McpManager.tsx +209 -0
  62. package/src/client/components/MessageInput.tsx +263 -0
  63. package/src/client/components/OAuthConsent.tsx +103 -0
  64. package/src/client/components/SchedulesManager.tsx +99 -0
  65. package/src/client/components/Sidebar.tsx +235 -0
  66. package/src/client/components/SkillsManager.tsx +280 -0
  67. package/src/client/components/SmartChart.tsx +167 -0
  68. package/src/client/components/Toast.tsx +54 -0
  69. package/src/client/components/WorkspaceTree.tsx +313 -0
  70. package/src/client/components/ZoomableImage.tsx +123 -0
  71. package/src/client/components/artifact-presets.ts +10 -0
  72. package/src/client/components/schedules/ScheduleEditor.tsx +264 -0
  73. package/src/client/components/schedules/ScheduleList.tsx +271 -0
  74. package/src/client/components/ui/accordion.tsx +50 -0
  75. package/src/client/components/ui/button.tsx +43 -0
  76. package/src/client/components/ui/dialog.tsx +82 -0
  77. package/src/client/components/ui/input.tsx +19 -0
  78. package/src/client/components/ui/scroll-area.tsx +39 -0
  79. package/src/client/components/ui/textarea.tsx +18 -0
  80. package/src/client/globals.css +51 -0
  81. package/src/client/hooks/useAgentSocket.ts +79 -0
  82. package/src/client/hooks/useArtifacts.ts +89 -0
  83. package/src/client/hooks/useAuth.ts +127 -0
  84. package/src/client/hooks/useConversation.ts +412 -0
  85. package/src/client/hooks/useDarkMode.ts +57 -0
  86. package/src/client/hooks/useIsMobile.ts +23 -0
  87. package/src/client/hooks/usePush.ts +127 -0
  88. package/src/client/hooks/useSchedules.ts +73 -0
  89. package/src/client/hooks/useUnread.ts +238 -0
  90. package/src/client/lib/desktopAttention.ts +75 -0
  91. package/src/client/lib/firebase.ts +32 -0
  92. package/src/client/lib/googleAuthNative.ts +94 -0
  93. package/src/client/lib/native.ts +43 -0
  94. package/src/client/lib/schedule-types.ts +34 -0
  95. package/src/client/lib/sessionApi.ts +58 -0
  96. package/src/client/lib/slots.tsx +79 -0
  97. package/src/client/lib/storage.ts +39 -0
  98. package/src/client/lib/utils.ts +26 -0
  99. package/src/client/lib/workspaceContext.tsx +54 -0
  100. package/src/client/lib/ws.ts +203 -0
  101. package/src/client/main.tsx +14 -0
  102. package/src/mcp-stdio-bridge.ts +70 -0
  103. package/src/scripts/summarize-conversations.ts +5 -0
  104. package/src/scripts/typecheck.ts +43 -0
  105. package/src/server/agents.ts +54 -0
  106. package/src/server/api-keys.ts +63 -0
  107. package/src/server/artifacts/artifacts.export.ts +85 -0
  108. package/src/server/artifacts/artifacts.handler.ts +93 -0
  109. package/src/server/artifacts/artifacts.routes.ts +43 -0
  110. package/src/server/artifacts/artifacts.service.ts +100 -0
  111. package/src/server/artifacts/artifacts.types.ts +31 -0
  112. package/src/server/auth.ts +262 -0
  113. package/src/server/claude.ts +394 -0
  114. package/src/server/commands.ts +21 -0
  115. package/src/server/contacts.ts +177 -0
  116. package/src/server/conversation-summarizer.ts +204 -0
  117. package/src/server/data-sync.ts +664 -0
  118. package/src/server/directives.ts +91 -0
  119. package/src/server/engine/claude-code.ts +514 -0
  120. package/src/server/engine/index.ts +41 -0
  121. package/src/server/engine/registry.ts +21 -0
  122. package/src/server/engine/shared.ts +47 -0
  123. package/src/server/engine/types.ts +48 -0
  124. package/src/server/env-resolve.ts +71 -0
  125. package/src/server/env-sanitize.ts +9 -0
  126. package/src/server/events/bus.ts +29 -0
  127. package/src/server/events/dispatcher.ts +48 -0
  128. package/src/server/events/routes.ts +19 -0
  129. package/src/server/events/types.ts +9 -0
  130. package/src/server/extensions.ts +101 -0
  131. package/src/server/features.ts +109 -0
  132. package/src/server/file-inject.ts +45 -0
  133. package/src/server/hooks.ts +142 -0
  134. package/src/server/idempotency.ts +25 -0
  135. package/src/server/index.ts +1715 -0
  136. package/src/server/integrity-audit.ts +132 -0
  137. package/src/server/mcp-catalog.ts +70 -0
  138. package/src/server/mcp-oauth.ts +198 -0
  139. package/src/server/mcp-progress.ts +45 -0
  140. package/src/server/mcp-server.ts +456 -0
  141. package/src/server/mcp-sidecar.ts +87 -0
  142. package/src/server/mcp.ts +291 -0
  143. package/src/server/model-aliases.ts +76 -0
  144. package/src/server/paths.ts +24 -0
  145. package/src/server/polls.ts +175 -0
  146. package/src/server/push/apns.ts +113 -0
  147. package/src/server/push/fcm.ts +108 -0
  148. package/src/server/push/push.ts +66 -0
  149. package/src/server/push/store.ts +84 -0
  150. package/src/server/push/triggers.ts +99 -0
  151. package/src/server/scheduler/builtins.ts +157 -0
  152. package/src/server/scheduler/engine.ts +432 -0
  153. package/src/server/scheduler/index.ts +4 -0
  154. package/src/server/scheduler/runner.ts +334 -0
  155. package/src/server/scheduler/storage.ts +98 -0
  156. package/src/server/scheduler/timing.ts +70 -0
  157. package/src/server/scheduler/types.ts +62 -0
  158. package/src/server/sdk-utils.ts +45 -0
  159. package/src/server/seed.ts +174 -0
  160. package/src/server/session-bus.ts +18 -0
  161. package/src/server/sessions.ts +559 -0
  162. package/src/server/shraga-config.ts +167 -0
  163. package/src/server/skills.ts +372 -0
  164. package/src/server/slack/api.ts +37 -0
  165. package/src/server/slack/bot.ts +391 -0
  166. package/src/server/slack/context-cache.ts +42 -0
  167. package/src/server/slack/feature.ts +59 -0
  168. package/src/server/slack/mention-rewrite.ts +59 -0
  169. package/src/server/slack/oauth.ts +102 -0
  170. package/src/server/slack/questions.ts +112 -0
  171. package/src/server/slack/sessions.ts +139 -0
  172. package/src/server/stats.ts +106 -0
  173. package/src/server/summarize.ts +11 -0
  174. package/src/server/turn-context.ts +61 -0
  175. package/src/server/unclaw-config.ts +19 -0
  176. package/src/server/unread.ts +79 -0
  177. package/src/server/user-context.ts +33 -0
  178. package/src/server/vendor-sync.ts +52 -0
  179. package/src/server/voice-provider.ts +74 -0
  180. package/src/server/workspace.ts +249 -0
@@ -0,0 +1,82 @@
1
+ import * as React from 'react';
2
+ import * as DialogPrimitive from '@radix-ui/react-dialog';
3
+ import { X } from 'lucide-react';
4
+ import { cn } from '@/lib/utils';
5
+
6
+ const Dialog = DialogPrimitive.Root;
7
+ const DialogTrigger = DialogPrimitive.Trigger;
8
+ const DialogPortal = DialogPrimitive.Portal;
9
+ const DialogClose = DialogPrimitive.Close;
10
+
11
+ const DialogOverlay = React.forwardRef<
12
+ React.ElementRef<typeof DialogPrimitive.Overlay>,
13
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
14
+ >(({ className, ...props }, ref) => (
15
+ <DialogPrimitive.Overlay
16
+ ref={ref}
17
+ className={cn(
18
+ 'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
19
+ className
20
+ )}
21
+ {...props}
22
+ />
23
+ ));
24
+ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
25
+
26
+ const DialogContent = React.forwardRef<
27
+ React.ElementRef<typeof DialogPrimitive.Content>,
28
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
29
+ >(({ className, children, ...props }, ref) => (
30
+ <DialogPortal>
31
+ <DialogOverlay />
32
+ <DialogPrimitive.Content
33
+ ref={ref}
34
+ className={cn(
35
+ 'fixed left-[50%] top-[50%] z-50 flex flex-col w-[calc(100vw-2rem)] max-w-lg max-h-[85dvh] overflow-y-auto translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-4 sm:p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 sm:rounded-lg',
36
+ className
37
+ )}
38
+ {...props}
39
+ >
40
+ {children}
41
+ <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
42
+ <X className="h-4 w-4" />
43
+ <span className="sr-only">Close</span>
44
+ </DialogPrimitive.Close>
45
+ </DialogPrimitive.Content>
46
+ </DialogPortal>
47
+ ));
48
+ DialogContent.displayName = DialogPrimitive.Content.displayName;
49
+
50
+ function DialogHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
51
+ return <div className={cn('shrink-0 flex flex-col space-y-1.5 text-center sm:text-left', className)} {...props} />;
52
+ }
53
+
54
+ // Scrolling region between a fixed header and footer. Bleeds to the dialog edges
55
+ // (so the scrollbar sits at the edge) while keeping content padding.
56
+ function DialogBody({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
57
+ return <div data-dialog-body className={cn('flex-1 min-h-0 overflow-y-auto -mx-4 px-4 sm:-mx-6 sm:px-6', className)} {...props} />;
58
+ }
59
+
60
+ function DialogFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
61
+ return (
62
+ <div
63
+ className={cn(
64
+ // Fixed section below the scroll area — full-width divider, CTA always visible.
65
+ 'shrink-0 -mx-4 px-4 pt-3 border-t sm:-mx-6 sm:px-6',
66
+ 'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
67
+ className
68
+ )}
69
+ {...props}
70
+ />
71
+ );
72
+ }
73
+
74
+ const DialogTitle = React.forwardRef<
75
+ React.ElementRef<typeof DialogPrimitive.Title>,
76
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
77
+ >(({ className, ...props }, ref) => (
78
+ <DialogPrimitive.Title ref={ref} className={cn('text-lg font-semibold leading-none tracking-tight', className)} {...props} />
79
+ ));
80
+ DialogTitle.displayName = DialogPrimitive.Title.displayName;
81
+
82
+ export { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogBody, DialogFooter, DialogTitle, DialogClose };
@@ -0,0 +1,19 @@
1
+ import * as React from 'react';
2
+ import { cn } from '@/lib/utils';
3
+
4
+ const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
5
+ ({ className, type, ...props }, ref) => (
6
+ <input
7
+ type={type}
8
+ className={cn(
9
+ 'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
10
+ className
11
+ )}
12
+ ref={ref}
13
+ {...props}
14
+ />
15
+ )
16
+ );
17
+ Input.displayName = 'Input';
18
+
19
+ export { Input };
@@ -0,0 +1,39 @@
1
+ import * as React from 'react';
2
+ import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
3
+ import { cn } from '@/lib/utils';
4
+
5
+ const ScrollArea = React.forwardRef<
6
+ React.ElementRef<typeof ScrollAreaPrimitive.Root>,
7
+ React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
8
+ >(({ className, children, ...props }, ref) => (
9
+ <ScrollAreaPrimitive.Root ref={ref} className={cn('relative overflow-hidden', className)} {...props}>
10
+ <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
11
+ {children}
12
+ </ScrollAreaPrimitive.Viewport>
13
+ <ScrollBar />
14
+ <ScrollAreaPrimitive.Corner />
15
+ </ScrollAreaPrimitive.Root>
16
+ ));
17
+ ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
18
+
19
+ const ScrollBar = React.forwardRef<
20
+ React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
21
+ React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
22
+ >(({ className, orientation = 'vertical', ...props }, ref) => (
23
+ <ScrollAreaPrimitive.ScrollAreaScrollbar
24
+ ref={ref}
25
+ orientation={orientation}
26
+ className={cn(
27
+ 'flex touch-none select-none transition-colors',
28
+ orientation === 'vertical' && 'h-full w-2.5 border-l border-l-transparent p-[1px]',
29
+ orientation === 'horizontal' && 'h-2.5 flex-col border-t border-t-transparent p-[1px]',
30
+ className
31
+ )}
32
+ {...props}
33
+ >
34
+ <ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
35
+ </ScrollAreaPrimitive.ScrollAreaScrollbar>
36
+ ));
37
+ ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
38
+
39
+ export { ScrollArea, ScrollBar };
@@ -0,0 +1,18 @@
1
+ import * as React from 'react';
2
+ import { cn } from '@/lib/utils';
3
+
4
+ const Textarea = React.forwardRef<HTMLTextAreaElement, React.TextareaHTMLAttributes<HTMLTextAreaElement>>(
5
+ ({ className, ...props }, ref) => (
6
+ <textarea
7
+ className={cn(
8
+ 'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 resize-none',
9
+ className
10
+ )}
11
+ ref={ref}
12
+ {...props}
13
+ />
14
+ )
15
+ );
16
+ Textarea.displayName = 'Textarea';
17
+
18
+ export { Textarea };
@@ -0,0 +1,51 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer base {
6
+ :root {
7
+ --background: 0 0% 100%;
8
+ --foreground: 222.2 84% 4.9%;
9
+ --card: 0 0% 100%;
10
+ --card-foreground: 222.2 84% 4.9%;
11
+ --primary: 222.2 47.4% 11.2%;
12
+ --primary-foreground: 210 40% 98%;
13
+ --secondary: 210 40% 96.1%;
14
+ --secondary-foreground: 222.2 47.4% 11.2%;
15
+ --muted: 210 40% 96.1%;
16
+ --muted-foreground: 215.4 16.3% 46.9%;
17
+ --accent: 210 40% 96.1%;
18
+ --accent-foreground: 222.2 47.4% 11.2%;
19
+ --destructive: 0 84.2% 60.2%;
20
+ --destructive-foreground: 210 40% 98%;
21
+ --border: 214.3 31.8% 91.4%;
22
+ --input: 214.3 31.8% 91.4%;
23
+ --ring: 222.2 84% 4.9%;
24
+ --radius: 0.5rem;
25
+ }
26
+ .dark {
27
+ --background: 222.2 84% 4.9%;
28
+ --foreground: 210 40% 98%;
29
+ --card: 222.2 84% 4.9%;
30
+ --card-foreground: 210 40% 98%;
31
+ --primary: 210 40% 98%;
32
+ --primary-foreground: 222.2 47.4% 11.2%;
33
+ --secondary: 217.2 32.6% 17.5%;
34
+ --secondary-foreground: 210 40% 98%;
35
+ --muted: 217.2 32.6% 17.5%;
36
+ --muted-foreground: 215 20.2% 65.1%;
37
+ --accent: 217.2 32.6% 17.5%;
38
+ --accent-foreground: 210 40% 98%;
39
+ --destructive: 0 62.8% 30.6%;
40
+ --destructive-foreground: 210 40% 98%;
41
+ --border: 217.2 32.6% 17.5%;
42
+ --input: 217.2 32.6% 17.5%;
43
+ --ring: 212.7 26.8% 83.9%;
44
+ }
45
+ }
46
+
47
+ @layer base {
48
+ * { @apply border-border; }
49
+ body { @apply bg-background text-foreground font-sans antialiased; }
50
+ html, body, #root { height: 100%; }
51
+ }
@@ -0,0 +1,79 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { AgentSocket, type ServerEvent } from '@/lib/ws';
3
+
4
+ export type ConnectionStatus =
5
+ | 'disconnected'
6
+ | 'reconnected'
7
+ | 'update_available'
8
+ | 'server_restarting'
9
+ | null;
10
+
11
+ /**
12
+ * Owns the single shared {@link AgentSocket} connection for the whole app, plus connection-level
13
+ * state (status, auth error, server-build update detection). Every socket event is forwarded to
14
+ * `onEvent` for app-global routing (schedules, unread, sidebar, busy set). Per-conversation event
15
+ * handling lives in `useConversation`, which subscribes to the same socket independently.
16
+ */
17
+ export function useAgentSocket(
18
+ token: string | null,
19
+ getToken: () => Promise<string | null>,
20
+ onEvent?: (event: ServerEvent) => void,
21
+ ): { socket: AgentSocket | null; connectionStatus: ConnectionStatus; authError: string | null } {
22
+ const [socket, setSocket] = useState<AgentSocket | null>(null);
23
+ const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus>(null);
24
+ const [authError, setAuthError] = useState<string | null>(null);
25
+ const serverBuildIdRef = useRef<string | undefined>(undefined);
26
+ const getTokenRef = useRef(getToken);
27
+ getTokenRef.current = getToken;
28
+ const onEventRef = useRef(onEvent);
29
+ onEventRef.current = onEvent;
30
+
31
+ useEffect(() => {
32
+ if (!token) return;
33
+ let reconnectTimer: ReturnType<typeof setTimeout>;
34
+ const s = new AgentSocket();
35
+ setSocket(s);
36
+ // Provider returns a fresh token on every (re)connect so expired Firebase tokens auto-refresh.
37
+ s.connect(() => getTokenRef.current());
38
+
39
+ const off = s.on((event: ServerEvent) => {
40
+ switch (event.type) {
41
+ case 'auth_ok':
42
+ if (event.buildId) {
43
+ if (serverBuildIdRef.current && serverBuildIdRef.current !== event.buildId) {
44
+ setConnectionStatus('update_available');
45
+ } else {
46
+ serverBuildIdRef.current = event.buildId;
47
+ }
48
+ }
49
+ break;
50
+ case 'auth_error':
51
+ if (event.message?.toLowerCase().includes('whitelist')) setAuthError(event.message);
52
+ break;
53
+ case 'server_restarting':
54
+ setConnectionStatus('server_restarting');
55
+ break;
56
+ case 'disconnected':
57
+ setConnectionStatus((prev) => (prev === 'server_restarting' ? prev : 'disconnected'));
58
+ break;
59
+ case 'reconnected':
60
+ setConnectionStatus((prev) => (prev === 'update_available' ? prev : 'reconnected'));
61
+ reconnectTimer = setTimeout(
62
+ () => setConnectionStatus((prev) => (prev === 'update_available' ? prev : null)),
63
+ 2000,
64
+ );
65
+ break;
66
+ }
67
+ onEventRef.current?.(event);
68
+ });
69
+
70
+ return () => {
71
+ off();
72
+ clearTimeout(reconnectTimer);
73
+ s.disconnect();
74
+ setSocket(null);
75
+ };
76
+ }, [token]);
77
+
78
+ return { socket, connectionStatus, authError };
79
+ }
@@ -0,0 +1,89 @@
1
+ import { useState, useCallback, useEffect } from 'react';
2
+ import type { ServerEvent } from '@/lib/ws';
3
+
4
+ export interface ArtifactMeta {
5
+ id: string;
6
+ sessionId: string;
7
+ title: string;
8
+ dimensions: [number, number];
9
+ version: number;
10
+ }
11
+
12
+ interface ArtifactsState {
13
+ artifacts: ArtifactMeta[];
14
+ selectedId: string | null;
15
+ panelOpen: boolean;
16
+ }
17
+
18
+ const PANEL_KEY = 'shraga:artifactPanel';
19
+ const SELECTED_KEY = 'shraga:artifactSelected';
20
+ const isMobile = () => window.innerWidth < 640; // matches Tailwind sm: breakpoint
21
+
22
+ export function useArtifacts(sessionId: string | undefined, getToken?: () => Promise<string | null>, token?: string | null) {
23
+ const [state, setState] = useState<ArtifactsState>({
24
+ artifacts: [],
25
+ selectedId: null,
26
+ panelOpen: isMobile() ? false : localStorage.getItem(PANEL_KEY) === 'true',
27
+ });
28
+
29
+ const reset = useCallback(() => {
30
+ setState(s => ({ ...s, artifacts: [], selectedId: null }));
31
+ localStorage.removeItem(SELECTED_KEY);
32
+ }, []);
33
+
34
+ const handleArtifactEvent = useCallback((event: ServerEvent) => {
35
+ if (event.type !== 'artifact') return;
36
+ const e = event as any;
37
+ setState(s => {
38
+ const existing = s.artifacts.findIndex(a => a.id === e.id);
39
+ const meta: ArtifactMeta = { id: e.id, sessionId: e.sessionId, title: e.title, dimensions: e.dimensions, version: e.version };
40
+ const artifacts = existing >= 0
41
+ ? s.artifacts.map((a, i) => i === existing ? meta : a)
42
+ : [...s.artifacts, meta];
43
+ localStorage.setItem(SELECTED_KEY, e.id);
44
+ return { artifacts, selectedId: e.id, panelOpen: true };
45
+ });
46
+ }, []);
47
+
48
+ const selectArtifact = useCallback((id: string) => {
49
+ setState(s => ({ ...s, selectedId: id, panelOpen: true }));
50
+ localStorage.setItem(SELECTED_KEY, id);
51
+ }, []);
52
+
53
+ const togglePanel = useCallback(() => {
54
+ setState(s => {
55
+ const next = !s.panelOpen;
56
+ localStorage.setItem(PANEL_KEY, String(next));
57
+ return { ...s, panelOpen: next };
58
+ });
59
+ }, []);
60
+
61
+ const closePanel = useCallback(() => {
62
+ setState(s => ({ ...s, panelOpen: false }));
63
+ localStorage.setItem(PANEL_KEY, 'false');
64
+ }, []);
65
+
66
+ // Load artifacts when session changes
67
+ useEffect(() => {
68
+ if (!sessionId) { reset(); return; }
69
+ (async () => {
70
+ const token = await getToken?.();
71
+ if (!token) return;
72
+ const res = await fetch(`/api/artifacts/${sessionId}`, { headers: { Authorization: `Bearer ${token}` } });
73
+ if (!res.ok) return;
74
+ const artifacts: ArtifactMeta[] = await res.json();
75
+ const savedId = localStorage.getItem(SELECTED_KEY);
76
+ const restoredId = artifacts.find(a => a.id === savedId)?.id ?? artifacts[artifacts.length - 1]?.id ?? null;
77
+ setState(s => ({ ...s, artifacts, selectedId: restoredId }));
78
+ })().catch(() => {});
79
+ }, [sessionId, reset, getToken, token]);
80
+
81
+ return {
82
+ ...state,
83
+ handleArtifactEvent,
84
+ selectArtifact,
85
+ togglePanel,
86
+ closePanel,
87
+ reset,
88
+ };
89
+ }
@@ -0,0 +1,127 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { onAuth, signOutUser, hasFirebase } from '@/lib/firebase';
3
+
4
+ export interface AuthUser {
5
+ uid: string;
6
+ email: string | null;
7
+ }
8
+
9
+ export interface AuthState {
10
+ user: AuthUser | null;
11
+ token: string | null;
12
+ getToken: () => Promise<string | null>;
13
+ loading: boolean;
14
+ /** Active auth provider (from GET /api/auth/mode). */
15
+ mode: 'local' | 'firebase' | null;
16
+ /** Local mode, zero users yet → show the create-first-user form. */
17
+ needsSetup: boolean;
18
+ /** Local login/register. Resolves to an error message, or null on success. */
19
+ loginLocal: (email: string, password: string) => Promise<string | null>;
20
+ registerLocal: (email: string, password: string) => Promise<string | null>;
21
+ logout: () => Promise<void>;
22
+ }
23
+
24
+ const LS_TOKEN = 'shraga_token';
25
+
26
+ function decodeLocalEmail(token: string): string | null {
27
+ try {
28
+ const payload = atob(token.slice(4).split('.')[1].replace(/-/g, '+').replace(/_/g, '/'));
29
+ return payload.slice(0, payload.lastIndexOf(':')) || null;
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ export function useAuth(): AuthState {
36
+ const [user, setUser] = useState<AuthUser | null>(null);
37
+ const [token, setToken] = useState<string | null>(null);
38
+ const [loading, setLoading] = useState(true);
39
+ const [mode, setMode] = useState<'local' | 'firebase' | null>(null);
40
+ const [needsSetup, setNeedsSetup] = useState(false);
41
+ const tokenRef = useRef<string | null>(null);
42
+ const fbUserRef = useRef<any>(null);
43
+
44
+ useEffect(() => {
45
+ let unsub: (() => void) | undefined;
46
+ (async () => {
47
+ let m: 'local' | 'firebase' = hasFirebase ? 'firebase' : 'local';
48
+ try {
49
+ const r = await fetch('/api/auth/mode');
50
+ if (r.ok) {
51
+ const d = await r.json();
52
+ m = d.provider === 'firebase' ? 'firebase' : 'local';
53
+ setNeedsSetup(!!d.needsSetup);
54
+ }
55
+ } catch {
56
+ /* fall back to firebase-presence heuristic */
57
+ }
58
+ setMode(m);
59
+
60
+ if (m === 'firebase') {
61
+ unsub = onAuth(async (u) => {
62
+ fbUserRef.current = u;
63
+ const t = u ? await u.getIdToken() : null;
64
+ tokenRef.current = t;
65
+ setToken(t);
66
+ setUser(u ? { uid: u.uid, email: u.email } : null);
67
+ setLoading(false);
68
+ });
69
+ } else {
70
+ const t = localStorage.getItem(LS_TOKEN);
71
+ if (t) {
72
+ tokenRef.current = t;
73
+ setToken(t);
74
+ const email = decodeLocalEmail(t);
75
+ setUser({ uid: email ?? 'user', email });
76
+ }
77
+ setLoading(false);
78
+ }
79
+ })();
80
+ return () => unsub?.();
81
+ }, []);
82
+
83
+ const getToken = useCallback(async () => {
84
+ if (mode === 'firebase') return fbUserRef.current ? fbUserRef.current.getIdToken() : null;
85
+ return tokenRef.current;
86
+ }, [mode]);
87
+
88
+ const applyLocalToken = (t: string, email: string) => {
89
+ localStorage.setItem(LS_TOKEN, t);
90
+ tokenRef.current = t;
91
+ setToken(t);
92
+ setUser({ uid: email, email });
93
+ setNeedsSetup(false);
94
+ };
95
+
96
+ const localAuth = useCallback(async (path: 'login' | 'register', email: string, password: string): Promise<string | null> => {
97
+ try {
98
+ const r = await fetch(`/api/auth/${path}`, {
99
+ method: 'POST',
100
+ headers: { 'content-type': 'application/json' },
101
+ body: JSON.stringify({ email, password }),
102
+ });
103
+ const d = await r.json().catch(() => ({}));
104
+ if (!r.ok || !d.token) return d.error || 'Authentication failed';
105
+ applyLocalToken(d.token, email);
106
+ return null;
107
+ } catch (e: any) {
108
+ return e?.message || 'Network error';
109
+ }
110
+ }, []);
111
+
112
+ const loginLocal = useCallback((e: string, p: string) => localAuth('login', e, p), [localAuth]);
113
+ const registerLocal = useCallback((e: string, p: string) => localAuth('register', e, p), [localAuth]);
114
+
115
+ const logout = useCallback(async () => {
116
+ if (mode === 'firebase') {
117
+ await signOutUser();
118
+ } else {
119
+ localStorage.removeItem(LS_TOKEN);
120
+ tokenRef.current = null;
121
+ setToken(null);
122
+ setUser(null);
123
+ }
124
+ }, [mode]);
125
+
126
+ return { user, token, getToken, loading, mode, needsSetup, loginLocal, registerLocal, logout };
127
+ }