@karmaniverous/jeeves-server 3.0.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 (260) hide show
  1. package/.env.local +13 -0
  2. package/.env.local.template +13 -0
  3. package/.tsbuildinfo +1 -0
  4. package/CHANGELOG.md +450 -0
  5. package/about.md +82 -0
  6. package/client/README.md +73 -0
  7. package/client/eslint.config.js +23 -0
  8. package/client/index.html +14 -0
  9. package/client/package-lock.json +5181 -0
  10. package/client/package.json +60 -0
  11. package/client/public/vite.svg +1 -0
  12. package/client/src/App.tsx +22 -0
  13. package/client/src/components/AccountMenu.tsx +167 -0
  14. package/client/src/components/ActionDropdown.tsx +120 -0
  15. package/client/src/components/CodeEditor.tsx +143 -0
  16. package/client/src/components/CodeViewer.tsx +113 -0
  17. package/client/src/components/ConfirmDialog.tsx +32 -0
  18. package/client/src/components/DirectoryRow.tsx +62 -0
  19. package/client/src/components/DirectoryTable.tsx +42 -0
  20. package/client/src/components/DownloadDropdown.tsx +116 -0
  21. package/client/src/components/DriveList.tsx +54 -0
  22. package/client/src/components/EmbeddedDiagramPanzoom.ts +28 -0
  23. package/client/src/components/FileContentView.tsx +155 -0
  24. package/client/src/components/InlineSvgPanzoom.ts +60 -0
  25. package/client/src/components/LazyDiagram.ts +93 -0
  26. package/client/src/components/LinkDropdown.tsx +134 -0
  27. package/client/src/components/MarkdownView.tsx +115 -0
  28. package/client/src/components/MermaidViewer.tsx +21 -0
  29. package/client/src/components/PlantUmlViewer.tsx +21 -0
  30. package/client/src/components/SearchModal.tsx +424 -0
  31. package/client/src/components/SvgViewer.tsx +107 -0
  32. package/client/src/components/TabBar.tsx +96 -0
  33. package/client/src/components/layout/Header.tsx +270 -0
  34. package/client/src/components/panzoom.ts +203 -0
  35. package/client/src/components/renderableUtils.ts +15 -0
  36. package/client/src/components/runner/JobTable.tsx +153 -0
  37. package/client/src/components/runner/RunHistory.tsx +140 -0
  38. package/client/src/components/runner/StatsBar.tsx +43 -0
  39. package/client/src/components/runner/StatusPill.tsx +27 -0
  40. package/client/src/components/runner/jobTableUtils.ts +65 -0
  41. package/client/src/components/scrollUtils.ts +39 -0
  42. package/client/src/components/ui/alert-dialog.tsx +107 -0
  43. package/client/src/components/ui/button.tsx +40 -0
  44. package/client/src/components/ui/dropdown-menu.tsx +79 -0
  45. package/client/src/components/ui/input.tsx +26 -0
  46. package/client/src/components/useActionState.ts +43 -0
  47. package/client/src/hooks/useFileBrowser.ts +102 -0
  48. package/client/src/hooks/useFileData.ts +78 -0
  49. package/client/src/hooks/useScrollAnchor.ts +70 -0
  50. package/client/src/hooks/useShareSettings.ts +22 -0
  51. package/client/src/hooks/useTopBar.ts +27 -0
  52. package/client/src/index.css +281 -0
  53. package/client/src/lib/AuthContext.ts +27 -0
  54. package/client/src/lib/api.ts +239 -0
  55. package/client/src/lib/auth.tsx +50 -0
  56. package/client/src/lib/codeBlockCm6.ts +129 -0
  57. package/client/src/lib/codeBlockCopy.ts +43 -0
  58. package/client/src/lib/codemirror.ts +77 -0
  59. package/client/src/lib/runner-api.ts +172 -0
  60. package/client/src/lib/svg.ts +50 -0
  61. package/client/src/lib/theme.ts +34 -0
  62. package/client/src/lib/utils.ts +6 -0
  63. package/client/src/main.tsx +11 -0
  64. package/client/src/pages/FileBrowser.tsx +135 -0
  65. package/client/src/pages/Home.tsx +46 -0
  66. package/client/src/pages/Runner.tsx +151 -0
  67. package/client/src/pages/RunnerJob.tsx +170 -0
  68. package/client/tsconfig.app.json +32 -0
  69. package/client/tsconfig.json +7 -0
  70. package/client/tsconfig.node.json +26 -0
  71. package/client/vite.config.ts +35 -0
  72. package/content/privacy.md +61 -0
  73. package/content/terms.md +41 -0
  74. package/dist/client/assets/CodeEditor-0XHVI8Nu.js +1 -0
  75. package/dist/client/assets/CodeViewer-CykMVsfX.js +1 -0
  76. package/dist/client/assets/index--MBieNJA.js +1 -0
  77. package/dist/client/assets/index-BENeXQI_.js +1 -0
  78. package/dist/client/assets/index-BbBpoOxz.js +1 -0
  79. package/dist/client/assets/index-BdV9g5AM.js +6 -0
  80. package/dist/client/assets/index-BjAilRri.js +2 -0
  81. package/dist/client/assets/index-BqbhWo2I.js +3 -0
  82. package/dist/client/assets/index-CVbycZ0H.js +1 -0
  83. package/dist/client/assets/index-Cs5oz2oJ.js +5 -0
  84. package/dist/client/assets/index-D8KZVveX.js +1 -0
  85. package/dist/client/assets/index-DC4HMHxY.js +13 -0
  86. package/dist/client/assets/index-DbMebkkd.css +1 -0
  87. package/dist/client/assets/index-DcY2RXqX.js +1 -0
  88. package/dist/client/assets/index-Duy-tZYV.js +1 -0
  89. package/dist/client/assets/index-Dw7rDFmE.js +7 -0
  90. package/dist/client/assets/index-FlCUvrjv.js +2 -0
  91. package/dist/client/assets/index-K6OVmfhg.js +1 -0
  92. package/dist/client/assets/index-LjwgzZ7F.js +62 -0
  93. package/dist/client/assets/index-MLwyFRN0.js +1 -0
  94. package/dist/client/assets/index-OpqBpSjn.js +1 -0
  95. package/dist/client/assets/index-SsHei0HE.js +1 -0
  96. package/dist/client/assets/index-uQa2yckk.js +1 -0
  97. package/dist/client/assets/index-udkXoIER.js +1 -0
  98. package/dist/client/index.html +15 -0
  99. package/dist/client/vite.svg +1 -0
  100. package/dist/src/auth/google.js +57 -0
  101. package/dist/src/auth/keys.js +185 -0
  102. package/dist/src/auth/resolve.js +102 -0
  103. package/dist/src/auth/session.js +57 -0
  104. package/dist/src/cli/commands/config.js +100 -0
  105. package/dist/src/cli/commands/config.test.js +84 -0
  106. package/dist/src/cli/commands/service.js +93 -0
  107. package/dist/src/cli/commands/start.js +24 -0
  108. package/dist/src/cli/index.js +20 -0
  109. package/dist/src/config/index.js +90 -0
  110. package/dist/src/config/loadConfig.test.js +127 -0
  111. package/dist/src/config/resolve.js +134 -0
  112. package/dist/src/config/resolve.test.js +148 -0
  113. package/dist/src/config/schema.js +159 -0
  114. package/dist/src/config/substituteEnvVars.js +45 -0
  115. package/dist/src/config/substituteEnvVars.test.js +51 -0
  116. package/dist/src/config/types.js +5 -0
  117. package/dist/src/routes/api/auth-status.js +56 -0
  118. package/dist/src/routes/api/diagrams.js +35 -0
  119. package/dist/src/routes/api/directory.js +93 -0
  120. package/dist/src/routes/api/drives.js +15 -0
  121. package/dist/src/routes/api/export.js +218 -0
  122. package/dist/src/routes/api/fileContent.js +286 -0
  123. package/dist/src/routes/api/index.js +33 -0
  124. package/dist/src/routes/api/linkInfo.js +71 -0
  125. package/dist/src/routes/api/linkInfo.test.js +104 -0
  126. package/dist/src/routes/api/middleware.js +117 -0
  127. package/dist/src/routes/api/raw.js +38 -0
  128. package/dist/src/routes/api/runner.js +59 -0
  129. package/dist/src/routes/api/search.js +236 -0
  130. package/dist/src/routes/api/sharing.js +203 -0
  131. package/dist/src/routes/api/status.js +68 -0
  132. package/dist/src/routes/api/status.test.js +62 -0
  133. package/dist/src/routes/auth.js +99 -0
  134. package/dist/src/routes/event.js +77 -0
  135. package/dist/src/routes/event.test.js +206 -0
  136. package/dist/src/routes/health.js +10 -0
  137. package/dist/src/routes/keys.js +129 -0
  138. package/dist/src/routes/path/index.js +17 -0
  139. package/dist/src/routes/static.js +30 -0
  140. package/dist/src/server.js +90 -0
  141. package/dist/src/services/deepShareLinks.js +163 -0
  142. package/dist/src/services/diagramCache.js +104 -0
  143. package/dist/src/services/embeddedDiagrams.js +136 -0
  144. package/dist/src/services/eventLog.js +55 -0
  145. package/dist/src/services/eventLog.test.js +113 -0
  146. package/dist/src/services/eventQueue.js +154 -0
  147. package/dist/src/services/eventQueue.test.js +104 -0
  148. package/dist/src/services/export.js +220 -0
  149. package/dist/src/services/exportCache.js +196 -0
  150. package/dist/src/services/markdown.js +147 -0
  151. package/dist/src/services/mermaid.js +97 -0
  152. package/dist/src/services/plantuml.js +145 -0
  153. package/dist/src/services/puppeteer.js +156 -0
  154. package/dist/src/util/breadcrumbs.js +22 -0
  155. package/dist/src/util/crypto.js +56 -0
  156. package/dist/src/util/crypto.test.js +99 -0
  157. package/dist/src/util/fileDetection.js +66 -0
  158. package/dist/src/util/fileDetection.test.js +89 -0
  159. package/dist/src/util/formatters.js +43 -0
  160. package/dist/src/util/formatters.test.js +83 -0
  161. package/dist/src/util/packageVersion.js +25 -0
  162. package/dist/src/util/platform.js +148 -0
  163. package/dist/src/util/state.js +46 -0
  164. package/dist/vitest.config.js +12 -0
  165. package/favicon.svg +3 -0
  166. package/guides/access-decision-flow.mmd +24 -0
  167. package/guides/access-decision-flow.svg +1 -0
  168. package/guides/api-integration.md +236 -0
  169. package/guides/deployment.md +287 -0
  170. package/guides/event-gateway.md +204 -0
  171. package/guides/event-gateway.mmd +17 -0
  172. package/guides/event-gateway.svg +1 -0
  173. package/guides/exports.md +239 -0
  174. package/guides/setup.md +313 -0
  175. package/guides/sharing.md +204 -0
  176. package/jeeves-server.config.template.json +25 -0
  177. package/package.json +124 -0
  178. package/scripts/download-plantuml.js +70 -0
  179. package/src/auth/google.ts +93 -0
  180. package/src/auth/keys.ts +252 -0
  181. package/src/auth/resolve.ts +157 -0
  182. package/src/auth/session.ts +77 -0
  183. package/src/cli/commands/config.test.ts +107 -0
  184. package/src/cli/commands/config.ts +113 -0
  185. package/src/cli/commands/service.ts +129 -0
  186. package/src/cli/commands/start.ts +27 -0
  187. package/src/cli/index.ts +25 -0
  188. package/src/config/index.ts +113 -0
  189. package/src/config/loadConfig.test.ts +155 -0
  190. package/src/config/resolve.test.ts +192 -0
  191. package/src/config/resolve.ts +173 -0
  192. package/src/config/schema.ts +179 -0
  193. package/src/config/substituteEnvVars.test.ts +64 -0
  194. package/src/config/substituteEnvVars.ts +52 -0
  195. package/src/config/types.ts +129 -0
  196. package/src/routes/api/auth-status.ts +85 -0
  197. package/src/routes/api/diagrams.ts +53 -0
  198. package/src/routes/api/directory.ts +123 -0
  199. package/src/routes/api/drives.ts +23 -0
  200. package/src/routes/api/export.ts +314 -0
  201. package/src/routes/api/fileContent.ts +414 -0
  202. package/src/routes/api/index.ts +37 -0
  203. package/src/routes/api/linkInfo.test.ts +132 -0
  204. package/src/routes/api/linkInfo.ts +83 -0
  205. package/src/routes/api/middleware.ts +156 -0
  206. package/src/routes/api/raw.ts +54 -0
  207. package/src/routes/api/runner.ts +107 -0
  208. package/src/routes/api/search.ts +321 -0
  209. package/src/routes/api/sharing.ts +259 -0
  210. package/src/routes/api/status.test.ts +72 -0
  211. package/src/routes/api/status.ts +82 -0
  212. package/src/routes/auth.ts +143 -0
  213. package/src/routes/event.test.ts +248 -0
  214. package/src/routes/event.ts +109 -0
  215. package/src/routes/health.ts +13 -0
  216. package/src/routes/keys.ts +192 -0
  217. package/src/routes/path/index.ts +24 -0
  218. package/src/routes/static.ts +54 -0
  219. package/src/server.ts +104 -0
  220. package/src/services/deepShareLinks.ts +203 -0
  221. package/src/services/diagramCache.ts +128 -0
  222. package/src/services/embeddedDiagrams.ts +168 -0
  223. package/src/services/eventLog.test.ts +144 -0
  224. package/src/services/eventLog.ts +68 -0
  225. package/src/services/eventQueue.test.ts +127 -0
  226. package/src/services/eventQueue.ts +196 -0
  227. package/src/services/export.ts +267 -0
  228. package/src/services/exportCache.ts +216 -0
  229. package/src/services/markdown.ts +189 -0
  230. package/src/services/mermaid.ts +113 -0
  231. package/src/services/plantuml.ts +172 -0
  232. package/src/services/puppeteer.ts +188 -0
  233. package/src/types/fastify.d.ts +13 -0
  234. package/src/types/jsonmap.d.ts +10 -0
  235. package/src/types/plantuml-encoder.d.ts +4 -0
  236. package/src/util/breadcrumbs.ts +33 -0
  237. package/src/util/crypto.test.ts +132 -0
  238. package/src/util/crypto.ts +79 -0
  239. package/src/util/fileDetection.test.ts +115 -0
  240. package/src/util/fileDetection.ts +70 -0
  241. package/src/util/formatters.test.ts +105 -0
  242. package/src/util/formatters.ts +44 -0
  243. package/src/util/packageVersion.ts +30 -0
  244. package/src/util/platform.ts +178 -0
  245. package/src/util/state.ts +55 -0
  246. package/test-docs/diagram-retry-test.md +18 -0
  247. package/test-docs/embedded-diagrams.md +52 -0
  248. package/test-docs/lazy-diagrams-test.md +333 -0
  249. package/test-docs/page-a.md +7 -0
  250. package/test-docs/page-b.md +7 -0
  251. package/test-docs/page-c.md +7 -0
  252. package/test-docs/sub/page-d.md +7 -0
  253. package/test-docs/test-diagram.puml +13 -0
  254. package/test-docs/validate-deep-share.js +318 -0
  255. package/tsconfig.json +37 -0
  256. package/tsdoc.json +13 -0
  257. package/vendor/.plantuml-version +1 -0
  258. package/vendor/plantuml.jar +0 -0
  259. package/vitest.config.js +12 -0
  260. package/vitest.config.ts +13 -0
@@ -0,0 +1,239 @@
1
+ /**
2
+ * API client for Jeeves Server backend
3
+ *
4
+ * Auth: session cookies (Google OAuth) or key-based (?key= URL param).
5
+ * When a key is present in the URL, it's stored and passed to all API calls.
6
+ */
7
+
8
+ const API_BASE = '/api';
9
+
10
+ /** Extract and cache auth params from URL (once) */
11
+ const _urlParams = new URLSearchParams(window.location.search);
12
+ const _urlKey = _urlParams.get('key');
13
+
14
+ /** Auth-related params to forward on every API call (key + deep share params) */
15
+ const _authSuffix = (() => {
16
+ if (!_urlKey) return '';
17
+ const params = new URLSearchParams();
18
+ params.set('key', _urlKey);
19
+ // Forward deep share params so the server can verify the key
20
+ for (const p of ['d', 'dirs', 's', 'exp'] as const) {
21
+ const v = _urlParams.get(p);
22
+ if (v !== null) params.set(p, v);
23
+ }
24
+ return params.toString();
25
+ })();
26
+
27
+ /** Append auth params to a URL if a key was provided */
28
+ export function withKey(url: string): string {
29
+ if (!_authSuffix) return url;
30
+ const sep = url.includes('?') ? '&' : '?';
31
+ return `${url}${sep}${_authSuffix}`;
32
+ }
33
+
34
+ export interface DirectoryEntry {
35
+ name: string;
36
+ type: 'directory' | 'file';
37
+ ext: string;
38
+ size: number | null;
39
+ mtime: string | null;
40
+ }
41
+
42
+ export interface DirectoryListing {
43
+ path: string;
44
+ entries: DirectoryEntry[];
45
+ breadcrumbs: BreadcrumbItem[];
46
+ isInsider: boolean;
47
+ renderAs?: string;
48
+ matchedRules?: string[];
49
+ }
50
+
51
+ export interface BreadcrumbItem {
52
+ label: string;
53
+ path: string;
54
+ }
55
+
56
+ export interface DriveEntry {
57
+ letter: string;
58
+ label: string;
59
+ }
60
+
61
+ export interface FileContent {
62
+ type: 'markdown' | 'text' | 'svg' | 'mermaid' | 'plantuml' | 'image' | 'binary';
63
+ content?: string;
64
+ html?: string;
65
+ headings?: { level: number; text: string; slug: string }[];
66
+ language?: string | null;
67
+ contentType?: string;
68
+ fileName: string;
69
+ breadcrumbs: BreadcrumbItem[];
70
+ isInsider: boolean;
71
+ }
72
+
73
+ export interface ShareResponse {
74
+ path: string;
75
+ url: string;
76
+ exp: string | null;
77
+ depth: number;
78
+ dirs: boolean;
79
+ }
80
+
81
+ export interface ShareSettings {
82
+ expiry: string;
83
+ depth: number;
84
+ dirs: boolean;
85
+ }
86
+
87
+ export interface AuthStatus {
88
+ authenticated: boolean;
89
+ email?: string;
90
+ picture?: string;
91
+ isInsider: boolean;
92
+ keyCreatedAt?: string | null;
93
+ searchEnabled?: boolean;
94
+ }
95
+
96
+ /** Rotate insider key — invalidates all existing shares */
97
+ export async function rotateKey(): Promise<{ ok: boolean; keyCreatedAt?: string }> {
98
+ return fetchJson('/api/rotate-key', { method: 'POST' });
99
+ }
100
+
101
+ async function fetchJson<T>(url: string, init?: RequestInit): Promise<T> {
102
+ const res = await fetch(withKey(url), {
103
+ ...init,
104
+ credentials: 'same-origin',
105
+ });
106
+
107
+ if (res.status === 401) {
108
+ window.location.href = `/auth/login?returnTo=${encodeURIComponent(window.location.pathname)}`;
109
+ throw new Error('Unauthorized');
110
+ }
111
+
112
+ if (!res.ok) {
113
+ const body = await res.text();
114
+ throw new Error(`API error ${String(res.status)}: ${body}`);
115
+ }
116
+
117
+ return res.json() as Promise<T>;
118
+ }
119
+
120
+ export async function getDrives(): Promise<DriveEntry[]> {
121
+ return fetchJson<DriveEntry[]>(`${API_BASE}/drives`);
122
+ }
123
+
124
+ export async function getDirectory(path: string): Promise<DirectoryListing> {
125
+ return fetchJson<DirectoryListing>(`${API_BASE}/path/${path}`);
126
+ }
127
+
128
+ export async function getFile(path: string): Promise<FileContent> {
129
+ // Forward render_diagrams param from page URL (used by PDF/DOCX export via Puppeteer)
130
+ const pageParams = new URLSearchParams(window.location.search);
131
+ const renderDiagrams = pageParams.get('render_diagrams');
132
+ const qs = renderDiagrams ? `?render_diagrams=${renderDiagrams}` : '';
133
+ return fetchJson<FileContent>(`${API_BASE}/file/${path}${qs}`);
134
+ }
135
+
136
+ export async function getFileRaw(path: string): Promise<FileContent> {
137
+ return fetchJson<FileContent>(`${API_BASE}/file/${path}?raw=1`);
138
+ }
139
+
140
+ export async function getAuthStatus(browsePath?: string): Promise<AuthStatus> {
141
+ let url = `${API_BASE}/auth/status`;
142
+ if (browsePath) {
143
+ url += `?path=${encodeURIComponent(browsePath)}`;
144
+ }
145
+ const res = await fetch(withKey(url), { credentials: 'same-origin' });
146
+ if (!res.ok) {
147
+ return { authenticated: false, isInsider: false };
148
+ }
149
+ return res.json() as Promise<AuthStatus>;
150
+ }
151
+
152
+ /** Generate an outsider share link — computed server-side, no keys on client */
153
+ export async function getShareLink(
154
+ targetPath: string,
155
+ expiry?: string,
156
+ depth?: number,
157
+ dirs?: boolean,
158
+ ): Promise<ShareResponse> {
159
+ return fetchJson<ShareResponse>(`${API_BASE}/share`, {
160
+ method: 'POST',
161
+ headers: { 'Content-Type': 'application/json' },
162
+ body: JSON.stringify({ path: targetPath, expiry, depth, dirs }),
163
+ });
164
+ }
165
+
166
+ export async function saveFile(path: string, content: string): Promise<{ ok: boolean; size: number }> {
167
+ return fetchJson<{ ok: boolean; size: number }>(`${API_BASE}/file/${path}`, {
168
+ method: 'PUT',
169
+ headers: { 'Content-Type': 'application/json' },
170
+ body: JSON.stringify({ content }),
171
+ });
172
+ }
173
+
174
+ export interface SearchChunk {
175
+ text: string;
176
+ index: number;
177
+ score: number;
178
+ }
179
+
180
+ export interface SearchResult {
181
+ filePath: string;
182
+ browsePath: string;
183
+ fileName: string;
184
+ bestScore: number;
185
+ mtime?: string;
186
+ domains?: string[];
187
+ title?: string;
188
+ author?: string;
189
+ participants?: string;
190
+ chunks: SearchChunk[];
191
+ }
192
+
193
+ export interface SearchMetadata {
194
+ domains: string[];
195
+ authors: string[];
196
+ participants: string[];
197
+ }
198
+
199
+ export interface SearchResponse {
200
+ results: SearchResult[];
201
+ metadata: SearchMetadata;
202
+ }
203
+
204
+ export async function searchDocuments(
205
+ query: string,
206
+ limit = 20,
207
+ filter?: Record<string, unknown>,
208
+ ): Promise<SearchResponse> {
209
+ return fetchJson<SearchResponse>(`${API_BASE}/search`, {
210
+ method: 'POST',
211
+ headers: { 'Content-Type': 'application/json' },
212
+ body: JSON.stringify({ query, limit, filter }),
213
+ });
214
+ }
215
+
216
+
217
+ export interface SearchFacet {
218
+ field: string;
219
+ type: string;
220
+ uiHint: string;
221
+ values: string[];
222
+ rules: string[];
223
+ }
224
+
225
+ export interface FacetsResponse {
226
+ facets: SearchFacet[];
227
+ }
228
+
229
+ export async function fetchFacets(): Promise<FacetsResponse> {
230
+ return fetchJson<FacetsResponse>(`${API_BASE}/search/facets`);
231
+ }
232
+
233
+ export async function clearCache(path: string): Promise<{ cleared: { exports: number; diagrams: number } }> {
234
+ return fetchJson<{ cleared: { exports: number; diagrams: number } }>(
235
+ `${API_BASE}/export-cache/${path}`,
236
+ { method: 'DELETE' },
237
+ );
238
+ }
239
+
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Auth provider component.
3
+ */
4
+ import { useCallback, useEffect, useState } from 'react';
5
+ import type { ReactNode } from 'react';
6
+
7
+ import { getAuthStatus, rotateKey as apiRotateKey } from './api';
8
+ import { AuthContext } from './AuthContext';
9
+
10
+ export function AuthProvider({ children }: { children: ReactNode }) {
11
+ const [loading, setLoading] = useState(true);
12
+ const [authenticated, setAuthenticated] = useState(false);
13
+ const [email, setEmail] = useState<string | undefined>();
14
+ const [picture, setPicture] = useState<string | undefined>();
15
+ const [isInsider, setIsInsider] = useState(false);
16
+ const [keyCreatedAt, setKeyCreatedAt] = useState<string | null | undefined>();
17
+ const [searchEnabled, setSearchEnabled] = useState(false);
18
+
19
+ useEffect(() => {
20
+ const browsePath = window.location.pathname.replace(/^\/browse/, '') || '/';
21
+ getAuthStatus(browsePath)
22
+ .then((status) => {
23
+ setAuthenticated(status.authenticated);
24
+ setEmail(status.email);
25
+ setPicture(status.picture);
26
+ setIsInsider(status.isInsider);
27
+ setSearchEnabled(!!status.searchEnabled);
28
+ setKeyCreatedAt(status.keyCreatedAt);
29
+ })
30
+ .catch(() => {
31
+ setAuthenticated(false);
32
+ })
33
+ .finally(() => setLoading(false));
34
+ }, []);
35
+
36
+ const rotateKey = useCallback(async () => {
37
+ const result = await apiRotateKey();
38
+ if (result.ok && result.keyCreatedAt) {
39
+ setKeyCreatedAt(result.keyCreatedAt);
40
+ }
41
+ }, []);
42
+
43
+ return (
44
+ <AuthContext.Provider
45
+ value={{ loading, authenticated, email, picture, isInsider, searchEnabled, keyCreatedAt, rotateKey }}
46
+ >
47
+ {children}
48
+ </AuthContext.Provider>
49
+ );
50
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Replaces server-rendered highlight.js code blocks in markdown with
3
+ * CodeMirror 6 read-only instances for syntax highlighting and code folding.
4
+ * Call after rendering markdown HTML.
5
+ */
6
+ import { getLanguageExtension, loadCodeMirror } from '@/lib/codemirror';
7
+
8
+ /** Map highlight.js language classes to file extensions for CM6 */
9
+ function langClassToExt(className: string): string | null {
10
+ const match = /language-(\w+)/.exec(className);
11
+ if (!match) return null;
12
+ const lang = match[1];
13
+ // Map hljs language names to file extensions
14
+ const map: Record<string, string> = {
15
+ javascript: 'js', typescript: 'ts', python: 'py',
16
+ json: 'json', yaml: 'yaml', yml: 'yaml',
17
+ html: 'html', css: 'css', xml: 'xml',
18
+ java: 'java', rust: 'rs', sql: 'sql', php: 'php',
19
+ cpp: 'cpp', c: 'c', markdown: 'md',
20
+ jsx: 'jsx', tsx: 'tsx', scss: 'scss',
21
+ bash: 'sh', shell: 'sh', sh: 'sh',
22
+ plaintext: '', text: '',
23
+ };
24
+ return map[lang] ?? lang;
25
+ }
26
+
27
+ export function initCodeBlockCm6(container: HTMLElement, theme: 'light' | 'dark' = 'dark'): () => void {
28
+ const cleanups: (() => void)[] = [];
29
+ const pres = container.querySelectorAll('pre');
30
+ const mounts: Promise<void>[] = [];
31
+
32
+ for (const pre of pres) {
33
+ const code = pre.querySelector('code');
34
+ if (!code) continue;
35
+
36
+ // Skip diagram placeholders
37
+ if (pre.closest('.embedded-diagram-lazy')) continue;
38
+ if (pre.closest('.embedded-diagram-rendered')) continue;
39
+
40
+ const ext = langClassToExt(code.className) ?? '';
41
+
42
+ const text = code.textContent ?? '';
43
+ if (!text.trim()) continue;
44
+
45
+ // Create a wrapper div for the CM6 instance
46
+ const wrapper = document.createElement('div');
47
+ wrapper.className = 'cm6-embedded-code';
48
+ pre.replaceWith(wrapper);
49
+
50
+ // Load CM6 async and mount
51
+ mounts.push(mountCm6(wrapper, text, ext, theme, cleanups));
52
+ }
53
+
54
+ // Signal completion for Puppeteer export
55
+ void Promise.all(mounts).then(() => {
56
+ container.setAttribute('data-cm6-ready', 'true');
57
+ });
58
+
59
+ return () => { for (const fn of cleanups) fn(); };
60
+ }
61
+
62
+ async function mountCm6(
63
+ wrapper: HTMLDivElement,
64
+ text: string,
65
+ ext: string,
66
+ theme: 'light' | 'dark',
67
+ cleanups: (() => void)[],
68
+ ): Promise<void> {
69
+ try {
70
+ const { EditorView, EditorState, basicSetup, oneDark } = await loadCodeMirror();
71
+ const langExt = await getLanguageExtension(ext);
72
+
73
+ // Check if wrapper is still in the DOM (component may have unmounted)
74
+ if (!wrapper.isConnected) return;
75
+
76
+ const extensions = [
77
+ basicSetup,
78
+ EditorState.readOnly.of(true),
79
+ EditorView.editable.of(false),
80
+ // Theme must come before custom overrides so our styles win
81
+ ...(theme === 'dark' ? [oneDark] : []),
82
+ EditorView.theme({
83
+ '&': {
84
+ fontSize: '14px',
85
+ borderRadius: '0.5rem',
86
+ overflow: 'hidden',
87
+ border: '1px solid var(--color-border)',
88
+ },
89
+ '.cm-scroller': { overflow: 'auto' },
90
+ '.cm-content': {
91
+ fontFamily: "'JetBrains Mono', 'Fira Code', 'Consolas', monospace",
92
+ padding: '0.75rem 0',
93
+ },
94
+ '.cm-gutters': {
95
+ fontFamily: "'JetBrains Mono', 'Fira Code', 'Consolas', monospace",
96
+ backgroundColor: 'var(--color-muted)',
97
+ borderRight: '1px solid var(--color-border)',
98
+ color: 'var(--color-muted-foreground)',
99
+ },
100
+ '&.cm-editor': {
101
+ backgroundColor: 'var(--color-muted)',
102
+ },
103
+ '.cm-cursor': { display: 'none' },
104
+ '.cm-activeLineGutter': {
105
+ backgroundColor: 'transparent',
106
+ },
107
+ '.cm-activeLine': {
108
+ backgroundColor: 'transparent',
109
+ },
110
+ }),
111
+ ];
112
+
113
+ if (langExt) {
114
+ extensions.push(langExt);
115
+ }
116
+
117
+ const state = EditorState.create({ doc: text, extensions });
118
+ const view = new EditorView({ state, parent: wrapper });
119
+
120
+ cleanups.push(() => view.destroy());
121
+ } catch {
122
+ // If CM6 fails to load, restore the original pre/code
123
+ wrapper.innerHTML = `<pre class="hljs rounded-lg overflow-x-auto text-sm border border-border p-4 bg-muted text-foreground"><code>${escapeHtml(text)}</code></pre>`;
124
+ }
125
+ }
126
+
127
+ function escapeHtml(str: string): string {
128
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
129
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Injects copy buttons into all <pre> blocks within a container element.
3
+ * Call after rendering markdown HTML.
4
+ */
5
+ import { createElement, Copy, Check } from 'lucide';
6
+
7
+ function createIcon(iconData: typeof Copy, size = 14): SVGSVGElement {
8
+ return createElement(iconData, { size }) as unknown as SVGSVGElement;
9
+ }
10
+
11
+ export function injectCopyButtons(container: HTMLElement) {
12
+ const pres = container.querySelectorAll('pre');
13
+ pres.forEach((pre) => {
14
+ if (pre.querySelector('.code-copy-btn')) return; // already injected
15
+
16
+ // Make pre relative for absolute positioning of button
17
+ pre.style.position = 'relative';
18
+
19
+ const btn = document.createElement('button');
20
+ btn.className = 'code-copy-btn';
21
+ btn.title = 'Copy to clipboard';
22
+ btn.innerHTML = '';
23
+ btn.appendChild(createIcon(Copy));
24
+
25
+ btn.addEventListener('click', () => {
26
+ const code = pre.querySelector('code');
27
+ const text = code?.textContent ?? pre.textContent ?? '';
28
+ void navigator.clipboard.writeText(text).then(() => {
29
+ btn.innerHTML = '';
30
+ const checkIcon = createIcon(Check);
31
+ btn.appendChild(checkIcon);
32
+ btn.style.color = '#4ade80';
33
+ setTimeout(() => {
34
+ btn.innerHTML = '';
35
+ btn.appendChild(createIcon(Copy));
36
+ btn.style.color = '';
37
+ }, 1500);
38
+ });
39
+ });
40
+
41
+ pre.appendChild(btn);
42
+ });
43
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Shared CodeMirror 6 utilities: core loader and language detection.
3
+ * Used by both CodeViewer (read-only) and CodeEditor (editable).
4
+ */
5
+
6
+ /** Lazy-load core CodeMirror modules */
7
+ export async function loadCodeMirror() {
8
+ const [
9
+ { EditorView, basicSetup },
10
+ { EditorState },
11
+ { keymap },
12
+ { oneDark },
13
+ ] = await Promise.all([
14
+ import('codemirror'),
15
+ import('@codemirror/state'),
16
+ import('@codemirror/view'),
17
+ import('@codemirror/theme-one-dark'),
18
+ ]);
19
+ return { EditorView, EditorState, basicSetup, keymap, oneDark };
20
+ }
21
+
22
+ /** Map file extensions to CodeMirror language support (lazy-loaded) */
23
+ export async function getLanguageExtension(ext: string) {
24
+ switch (ext.toLowerCase()) {
25
+ case 'js':
26
+ case 'jsx':
27
+ case 'mjs':
28
+ case 'cjs':
29
+ return (await import('@codemirror/lang-javascript')).javascript({ jsx: ext.includes('x') });
30
+ case 'ts':
31
+ case 'tsx':
32
+ case 'mts':
33
+ case 'cts':
34
+ return (await import('@codemirror/lang-javascript')).javascript({ jsx: ext.includes('x'), typescript: true });
35
+ case 'html':
36
+ case 'htm':
37
+ return (await import('@codemirror/lang-html')).html();
38
+ case 'css':
39
+ case 'scss':
40
+ return (await import('@codemirror/lang-css')).css();
41
+ case 'json':
42
+ case 'jsonl':
43
+ return (await import('@codemirror/lang-json')).json();
44
+ case 'md':
45
+ case 'mdx':
46
+ case 'markdown':
47
+ return (await import('@codemirror/lang-markdown')).markdown();
48
+ case 'py':
49
+ case 'pyw':
50
+ return (await import('@codemirror/lang-python')).python();
51
+ case 'xml':
52
+ case 'svg':
53
+ case 'xsl':
54
+ case 'xhtml':
55
+ return (await import('@codemirror/lang-xml')).xml();
56
+ case 'yaml':
57
+ case 'yml':
58
+ return (await import('@codemirror/lang-yaml')).yaml();
59
+ case 'c':
60
+ case 'h':
61
+ case 'cpp':
62
+ case 'hpp':
63
+ case 'cc':
64
+ case 'cxx':
65
+ return (await import('@codemirror/lang-cpp')).cpp();
66
+ case 'java':
67
+ return (await import('@codemirror/lang-java')).java();
68
+ case 'rs':
69
+ return (await import('@codemirror/lang-rust')).rust();
70
+ case 'sql':
71
+ return (await import('@codemirror/lang-sql')).sql();
72
+ case 'php':
73
+ return (await import('@codemirror/lang-php')).php();
74
+ default:
75
+ return null;
76
+ }
77
+ }
@@ -0,0 +1,172 @@
1
+ /**
2
+ * API client for jeeves-runner proxy endpoints.
3
+ * Unwraps runner API responses and maps snake_case to camelCase.
4
+ */
5
+
6
+ import { withKey } from './api';
7
+
8
+ const RUNNER_BASE = '/api/runner';
9
+
10
+ async function runnerFetch<T>(path: string, init?: RequestInit): Promise<T> {
11
+ const res = await fetch(withKey(`${RUNNER_BASE}${path}`), {
12
+ ...init,
13
+ credentials: 'same-origin',
14
+ });
15
+
16
+ if (res.status === 401) {
17
+ window.location.href = `/auth/login?returnTo=${encodeURIComponent(window.location.pathname)}`;
18
+ throw new Error('Unauthorized');
19
+ }
20
+
21
+ if (!res.ok) {
22
+ const body = await res.text();
23
+ throw new Error(`Runner API error ${String(res.status)}: ${body}`);
24
+ }
25
+
26
+ return res.json() as Promise<T>;
27
+ }
28
+
29
+ // --- Raw runner API shapes (snake_case) ---
30
+
31
+ interface RawJob {
32
+ id: string;
33
+ name: string;
34
+ type: string;
35
+ schedule: string;
36
+ enabled: number;
37
+ overlap_policy: string;
38
+ last_status: string | null;
39
+ last_run: string | null;
40
+ description: string | null;
41
+ }
42
+
43
+ interface RawRun {
44
+ id: number;
45
+ job_id: string;
46
+ status: string;
47
+ trigger: string;
48
+ started_at: string;
49
+ finished_at: string | null;
50
+ duration_ms: number | null;
51
+ exit_code: number | null;
52
+ stdout_tail: string;
53
+ stderr_tail: string;
54
+ }
55
+
56
+ interface RawStats {
57
+ totalJobs: number;
58
+ running: number;
59
+ okLastHour: number;
60
+ errorsLastHour: number;
61
+ }
62
+
63
+ // --- Public types (camelCase) ---
64
+
65
+ export interface RunnerStats {
66
+ totalJobs: number;
67
+ running: number;
68
+ okLastHour: number;
69
+ errorsLastHour: number;
70
+ }
71
+
72
+ export interface RunnerJob {
73
+ id: string;
74
+ name: string;
75
+ type: string;
76
+ schedule: string;
77
+ enabled: boolean;
78
+ overlapPolicy: string;
79
+ status: string | null;
80
+ lastRun: string | null;
81
+ description: string | null;
82
+ }
83
+
84
+ export interface RunEntry {
85
+ id: number;
86
+ jobId: string;
87
+ status: string;
88
+ trigger: string;
89
+ startedAt: string;
90
+ finishedAt: string | null;
91
+ durationMs: number | null;
92
+ exitCode: number | null;
93
+ stdoutTail: string;
94
+ stderrTail: string;
95
+ }
96
+
97
+ function mapJob(raw: RawJob): RunnerJob {
98
+ return {
99
+ id: raw.id,
100
+ name: raw.name,
101
+ type: raw.type,
102
+ schedule: raw.schedule,
103
+ enabled: raw.enabled === 1,
104
+ overlapPolicy: raw.overlap_policy,
105
+ status: raw.last_status,
106
+ lastRun: raw.last_run,
107
+ description: raw.description,
108
+ };
109
+ }
110
+
111
+ function mapRun(raw: RawRun): RunEntry {
112
+ return {
113
+ id: raw.id,
114
+ jobId: raw.job_id,
115
+ status: raw.status,
116
+ trigger: raw.trigger,
117
+ startedAt: raw.started_at,
118
+ finishedAt: raw.finished_at,
119
+ durationMs: raw.duration_ms,
120
+ exitCode: raw.exit_code,
121
+ stdoutTail: raw.stdout_tail,
122
+ stderrTail: raw.stderr_tail,
123
+ };
124
+ }
125
+
126
+ export async function getRunnerStats(): Promise<RunnerStats> {
127
+ const raw = await runnerFetch<RawStats>('/stats');
128
+ return raw;
129
+ }
130
+
131
+ export async function getRunnerJobs(): Promise<RunnerJob[]> {
132
+ const raw = await runnerFetch<{ jobs: RawJob[] }>('/jobs');
133
+ return raw.jobs.map(mapJob);
134
+ }
135
+
136
+ export async function getRunnerJob(id: string): Promise<RunnerJob> {
137
+ const raw = await runnerFetch<{ job: RawJob }>(
138
+ `/jobs/${encodeURIComponent(id)}`,
139
+ );
140
+ return mapJob(raw.job);
141
+ }
142
+
143
+ export async function getJobRuns(
144
+ id: string,
145
+ limit = 20,
146
+ ): Promise<RunEntry[]> {
147
+ const raw = await runnerFetch<{ runs: RawRun[] }>(
148
+ `/jobs/${encodeURIComponent(id)}/runs?limit=${String(limit)}`,
149
+ );
150
+ return raw.runs.map(mapRun);
151
+ }
152
+
153
+ export async function triggerJobRun(id: string): Promise<{ ok: boolean }> {
154
+ return runnerFetch<{ ok: boolean }>(
155
+ `/jobs/${encodeURIComponent(id)}/run`,
156
+ { method: 'POST' },
157
+ );
158
+ }
159
+
160
+ export async function enableJob(id: string): Promise<{ ok: boolean }> {
161
+ return runnerFetch<{ ok: boolean }>(
162
+ `/jobs/${encodeURIComponent(id)}/enable`,
163
+ { method: 'POST' },
164
+ );
165
+ }
166
+
167
+ export async function disableJob(id: string): Promise<{ ok: boolean }> {
168
+ return runnerFetch<{ ok: boolean }>(
169
+ `/jobs/${encodeURIComponent(id)}/disable`,
170
+ { method: 'POST' },
171
+ );
172
+ }