@agent-native/core 0.81.3 → 0.83.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 (92) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +12 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/client/CommandMenu.tsx +34 -12
  5. package/corpus/core/src/file-upload/builder.ts +199 -37
  6. package/corpus/core/src/file-upload/index.ts +2 -0
  7. package/corpus/core/src/file-upload/types.ts +38 -0
  8. package/corpus/templates/analytics/actions/hubspot-deals.ts +64 -4
  9. package/corpus/templates/clips/actions/create-recording.ts +44 -0
  10. package/corpus/templates/clips/actions/finalize-recording.ts +198 -88
  11. package/corpus/templates/clips/actions/lib/create-recording-schema.ts +12 -0
  12. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +83 -29
  13. package/corpus/templates/clips/app/routes/record.tsx +12 -1
  14. package/corpus/templates/clips/server/lib/resumable-session.ts +39 -0
  15. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +2 -0
  16. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +186 -29
  17. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +4 -0
  18. package/corpus/templates/clips/shared/recording-core.ts +5 -0
  19. package/corpus/templates/content/.env.local.example +6 -0
  20. package/corpus/templates/content/AGENTS.md +10 -1
  21. package/corpus/templates/content/actions/_builder-cms-read-client.ts +14 -1
  22. package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +43 -0
  23. package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +79 -7
  24. package/corpus/templates/content/actions/_database-source-utils.ts +987 -22
  25. package/corpus/templates/content/actions/_database-utils.ts +41 -0
  26. package/corpus/templates/content/actions/_local-file-documents.ts +74 -1
  27. package/corpus/templates/content/actions/attach-content-database-source.ts +33 -8
  28. package/corpus/templates/content/actions/change-content-database-source-role.ts +14 -0
  29. package/corpus/templates/content/actions/disconnect-content-database-source.ts +3 -0
  30. package/corpus/templates/content/actions/execute-builder-source-execution.ts +71 -3
  31. package/corpus/templates/content/actions/list-content-databases.ts +41 -33
  32. package/corpus/templates/content/actions/process-builder-body-hydration.ts +47 -0
  33. package/corpus/templates/content/actions/update-document.ts +93 -0
  34. package/corpus/templates/content/app/blocks/SourceComponentBlock.tsx +277 -0
  35. package/corpus/templates/content/app/blocks/contentBlockRegistry.tsx +2 -0
  36. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +105 -17
  37. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +5 -1
  38. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +92 -0
  39. package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +5 -3
  40. package/corpus/templates/content/app/components/editor/extensions/registryBlocks.ts +41 -0
  41. package/corpus/templates/content/app/components/editor/registrySlashItems.ts +4 -0
  42. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +28 -33
  43. package/corpus/templates/content/app/components/sidebar/document-sidebar-sections.ts +47 -0
  44. package/corpus/templates/content/app/global.css +10 -3
  45. package/corpus/templates/content/app/hooks/use-content-database.ts +24 -0
  46. package/corpus/templates/content/app/hooks/use-documents.ts +37 -0
  47. package/corpus/templates/content/app/i18n/zh-TW.ts +24 -0
  48. package/corpus/templates/content/app/i18n-data.ts +212 -0
  49. package/corpus/templates/content/app/lib/content-command-search.ts +64 -0
  50. package/corpus/templates/content/app/root.tsx +236 -8
  51. package/corpus/templates/content/changelog/2026-06-29-builder-cms-sources-now-sync-article-bodies-through-content.md +6 -0
  52. package/corpus/templates/content/changelog/2026-06-30-builder-article-media-now-render-as-images-and-embeds-when-r.md +6 -0
  53. package/corpus/templates/content/changelog/2026-06-30-builder-source-components-now-render-as-preserved-preview-bl.md +6 -0
  54. package/corpus/templates/content/changelog/2026-06-30-cmd-k-search-now-opens-from-the-editor-and-finds-real-content.md +6 -0
  55. package/corpus/templates/content/changelog/2026-06-30-sidebar-favorites-now-keep-long-titles-tidy-and-stay-in-sync.md +6 -0
  56. package/corpus/templates/content/package.json +3 -1
  57. package/corpus/templates/content/scripts/check-native-deps.mjs +57 -0
  58. package/corpus/templates/content/scripts/dev-database.mjs +83 -0
  59. package/corpus/templates/content/scripts/seed-demo-user.mjs +107 -0
  60. package/corpus/templates/content/server/db/schema.ts +27 -0
  61. package/corpus/templates/content/server/plugins/db.ts +33 -0
  62. package/corpus/templates/content/shared/api.ts +45 -0
  63. package/corpus/templates/content/shared/builder-mdx.ts +905 -5
  64. package/corpus/templates/content/shared/nfm-registry.ts +2 -0
  65. package/corpus/templates/content/shared/source-component-block.ts +141 -0
  66. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +5 -1
  67. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +229 -131
  68. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +103 -6
  69. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +6 -3
  70. package/corpus/templates/design/app/pages/DesignEditor.tsx +382 -72
  71. package/corpus/templates/design/changelog/2026-06-30-canvas-editing-now-keeps-undo-redo-and-cross-screen-layer-mo.md +6 -0
  72. package/dist/client/CommandMenu.d.ts +6 -2
  73. package/dist/client/CommandMenu.d.ts.map +1 -1
  74. package/dist/client/CommandMenu.js +25 -14
  75. package/dist/client/CommandMenu.js.map +1 -1
  76. package/dist/collab/awareness.d.ts +2 -2
  77. package/dist/collab/awareness.d.ts.map +1 -1
  78. package/dist/collab/routes.d.ts +1 -1
  79. package/dist/file-upload/builder.d.ts.map +1 -1
  80. package/dist/file-upload/builder.js +137 -25
  81. package/dist/file-upload/builder.js.map +1 -1
  82. package/dist/file-upload/index.d.ts +1 -1
  83. package/dist/file-upload/index.d.ts.map +1 -1
  84. package/dist/file-upload/index.js.map +1 -1
  85. package/dist/file-upload/types.d.ts +26 -0
  86. package/dist/file-upload/types.d.ts.map +1 -1
  87. package/dist/file-upload/types.js.map +1 -1
  88. package/dist/notifications/routes.d.ts +3 -3
  89. package/dist/progress/routes.d.ts +1 -1
  90. package/dist/resources/handlers.d.ts +3 -3
  91. package/dist/server/transcribe-voice.d.ts +1 -1
  92. package/package.json +1 -1
@@ -9,14 +9,24 @@ import {
9
9
  type LocaleCode,
10
10
  type LocaleMessages,
11
11
  type LocalizationPreference,
12
+ useActionQuery,
12
13
  useCommandMenuShortcut,
13
14
  useT,
14
15
  } from "@agent-native/core/client";
15
16
  import { configureTracking } from "@agent-native/core/client";
16
17
  import { resolveLocaleFromRequest } from "@agent-native/core/server";
17
- import { IconDeviceDesktop, IconMoon, IconSun } from "@tabler/icons-react";
18
+ import type { ListContentDatabasesResponse } from "@shared/api";
19
+ import {
20
+ IconDatabase,
21
+ IconDeviceDesktop,
22
+ IconFileText,
23
+ IconFolderOpen,
24
+ IconLoader2,
25
+ IconMoon,
26
+ IconSun,
27
+ } from "@tabler/icons-react";
18
28
  import { useTheme } from "next-themes";
19
- import { useCallback, useEffect, useState } from "react";
29
+ import { useCallback, useEffect, useMemo, useState } from "react";
20
30
  import {
21
31
  Links,
22
32
  Meta,
@@ -26,6 +36,7 @@ import {
26
36
  isRouteErrorResponse,
27
37
  useLoaderData,
28
38
  useLocation,
39
+ useNavigate,
29
40
  useRouteLoaderData,
30
41
  useRouteError,
31
42
  } from "react-router";
@@ -40,6 +51,11 @@ import changelog from "../CHANGELOG.md?raw";
40
51
  import { useDbSync } from "./hooks/use-db-sync";
41
52
  import { useNavigationState } from "./hooks/use-navigation-state";
42
53
  import { i18nCatalog } from "./i18n";
54
+ import {
55
+ contentCommandDocumentPath,
56
+ groupContentCommandSearchResults,
57
+ type CommandSearchDocumentsResponse,
58
+ } from "./lib/content-command-search";
43
59
 
44
60
  import stylesheet from "./global.css?url";
45
61
  configureTracking({
@@ -226,6 +242,211 @@ function ThemeToggleItem() {
226
242
  );
227
243
  }
228
244
 
245
+ function CommandStateMessage({
246
+ children,
247
+ icon,
248
+ }: {
249
+ children: React.ReactNode;
250
+ icon?: React.ReactNode;
251
+ }) {
252
+ return (
253
+ <div
254
+ className="flex items-center gap-2 px-2 py-2 text-sm text-muted-foreground"
255
+ role="status"
256
+ aria-live="polite"
257
+ >
258
+ {icon}
259
+ <span className="min-w-0 flex-1">{children}</span>
260
+ </div>
261
+ );
262
+ }
263
+
264
+ function useDebouncedValue<T>(value: T, delayMs: number) {
265
+ const [debouncedValue, setDebouncedValue] = useState(value);
266
+
267
+ useEffect(() => {
268
+ const id = window.setTimeout(() => setDebouncedValue(value), delayMs);
269
+ return () => window.clearTimeout(id);
270
+ }, [delayMs, value]);
271
+
272
+ return debouncedValue;
273
+ }
274
+
275
+ function ContentCommandSearchResults({
276
+ query,
277
+ onOpenChange,
278
+ }: {
279
+ query: string;
280
+ onOpenChange: (open: boolean) => void;
281
+ }) {
282
+ const t = useT();
283
+ const navigate = useNavigate();
284
+ const trimmedQuery = query.trim();
285
+ const debouncedQuery = useDebouncedValue(trimmedQuery, 200);
286
+ const searchEnabled = debouncedQuery.length > 0;
287
+ const documentsQuery = useActionQuery<CommandSearchDocumentsResponse>(
288
+ "search-documents",
289
+ searchEnabled ? { query: debouncedQuery, limit: 8 } : undefined,
290
+ { enabled: searchEnabled, retry: false },
291
+ );
292
+ const databasesQuery = useActionQuery<ListContentDatabasesResponse>(
293
+ "list-content-databases",
294
+ searchEnabled ? { query: debouncedQuery, limit: 6 } : undefined,
295
+ { enabled: searchEnabled, retry: false, staleTime: 60_000 },
296
+ );
297
+
298
+ const searchGroups = useMemo(
299
+ () =>
300
+ groupContentCommandSearchResults({
301
+ documents: documentsQuery.data?.documents ?? [],
302
+ databases: databasesQuery.data?.databases ?? [],
303
+ query: debouncedQuery,
304
+ }),
305
+ [
306
+ databasesQuery.data?.databases,
307
+ documentsQuery.data?.documents,
308
+ debouncedQuery,
309
+ ],
310
+ );
311
+
312
+ if (!trimmedQuery) return null;
313
+
314
+ const resultCount =
315
+ searchGroups.documents.length +
316
+ searchGroups.databases.length +
317
+ searchGroups.localFiles.length;
318
+ const isWaitingForDebounce = trimmedQuery !== debouncedQuery;
319
+ const isLoading =
320
+ (isWaitingForDebounce ||
321
+ documentsQuery.isLoading ||
322
+ databasesQuery.isLoading) &&
323
+ resultCount === 0;
324
+ const error = documentsQuery.error ?? databasesQuery.error;
325
+ const hasResults = resultCount > 0;
326
+
327
+ const openDocument = (documentId: string) => {
328
+ onOpenChange(false);
329
+ navigate(contentCommandDocumentPath(documentId));
330
+ };
331
+
332
+ if (isLoading) {
333
+ return (
334
+ <CommandMenu.Group heading={t("root.commandSearchHeading")}>
335
+ <CommandStateMessage
336
+ icon={<IconLoader2 className="size-4 animate-spin" />}
337
+ >
338
+ {t("root.commandSearchLoading")}
339
+ </CommandStateMessage>
340
+ </CommandMenu.Group>
341
+ );
342
+ }
343
+
344
+ if (error && !hasResults) {
345
+ return (
346
+ <CommandMenu.Group heading={t("root.commandSearchHeading")}>
347
+ <CommandStateMessage>
348
+ {t("root.commandSearchError")}
349
+ </CommandStateMessage>
350
+ </CommandMenu.Group>
351
+ );
352
+ }
353
+
354
+ if (!hasResults) {
355
+ return (
356
+ <CommandMenu.Group heading={t("root.commandSearchHeading")}>
357
+ <CommandStateMessage>
358
+ {t("root.commandSearchEmpty")}
359
+ </CommandStateMessage>
360
+ </CommandMenu.Group>
361
+ );
362
+ }
363
+
364
+ return (
365
+ <>
366
+ {error ? (
367
+ <CommandMenu.Group heading={t("root.commandSearchHeading")}>
368
+ <CommandStateMessage>
369
+ {t("root.commandSearchPartialError")}
370
+ </CommandStateMessage>
371
+ </CommandMenu.Group>
372
+ ) : null}
373
+
374
+ {searchGroups.documents.length > 0 ? (
375
+ <CommandMenu.Group heading={t("root.commandDocumentsHeading")}>
376
+ {searchGroups.documents.map((document) => (
377
+ <CommandMenu.Item
378
+ key={`document:${document.id}`}
379
+ onSelect={() => openDocument(document.id)}
380
+ deferSelect={false}
381
+ className="items-start py-2"
382
+ >
383
+ <IconFileText className="mt-0.5 size-4 shrink-0 text-muted-foreground" />
384
+ <span className="min-w-0 flex-1">
385
+ <span className="block truncate font-medium">
386
+ {document.title || t("sidebar.untitled")}
387
+ </span>
388
+ {document.snippet ? (
389
+ <span className="mt-0.5 block line-clamp-2 text-xs leading-snug text-muted-foreground">
390
+ {document.snippet}
391
+ </span>
392
+ ) : null}
393
+ </span>
394
+ </CommandMenu.Item>
395
+ ))}
396
+ </CommandMenu.Group>
397
+ ) : null}
398
+
399
+ {searchGroups.databases.length > 0 ? (
400
+ <CommandMenu.Group heading={t("root.commandDatabasesHeading")}>
401
+ {searchGroups.databases.map((database) => (
402
+ <CommandMenu.Item
403
+ key={`database:${database.databaseId}`}
404
+ onSelect={() => openDocument(database.documentId)}
405
+ deferSelect={false}
406
+ className="items-start py-2"
407
+ >
408
+ <IconDatabase className="mt-0.5 size-4 shrink-0 text-muted-foreground" />
409
+ <span className="min-w-0 flex-1">
410
+ <span className="block truncate font-medium">
411
+ {database.title || t("sidebar.untitled")}
412
+ </span>
413
+ <span className="mt-0.5 block text-xs leading-snug text-muted-foreground">
414
+ {t("root.commandDatabaseResultDescription")}
415
+ </span>
416
+ </span>
417
+ </CommandMenu.Item>
418
+ ))}
419
+ </CommandMenu.Group>
420
+ ) : null}
421
+
422
+ {searchGroups.localFiles.length > 0 ? (
423
+ <CommandMenu.Group heading={t("root.commandLocalFilesHeading")}>
424
+ {searchGroups.localFiles.map((document) => (
425
+ <CommandMenu.Item
426
+ key={`local-file:${document.id}`}
427
+ onSelect={() => openDocument(document.id)}
428
+ deferSelect={false}
429
+ className="items-start py-2"
430
+ >
431
+ <IconFolderOpen className="mt-0.5 size-4 shrink-0 text-muted-foreground" />
432
+ <span className="min-w-0 flex-1">
433
+ <span className="block truncate font-medium">
434
+ {document.title || t("sidebar.untitled")}
435
+ </span>
436
+ {document.snippet ? (
437
+ <span className="mt-0.5 block line-clamp-2 text-xs leading-snug text-muted-foreground">
438
+ {document.snippet}
439
+ </span>
440
+ ) : null}
441
+ </span>
442
+ </CommandMenu.Item>
443
+ ))}
444
+ </CommandMenu.Group>
445
+ ) : null}
446
+ </>
447
+ );
448
+ }
449
+
229
450
  function PublicAgentShell({ children }: { children: React.ReactNode }) {
230
451
  const [mounted, setMounted] = useState(false);
231
452
  const t = useT();
@@ -282,14 +503,16 @@ function ContentCommandMenu({
282
503
  <CommandMenu
283
504
  open={open}
284
505
  onOpenChange={onOpenChange}
506
+ placeholder={t("root.commandSearchPlaceholder")}
285
507
  changelog={changelog}
286
508
  changelogKey="content"
509
+ renderResults={(search) => (
510
+ <ContentCommandSearchResults
511
+ query={search}
512
+ onOpenChange={onOpenChange}
513
+ />
514
+ )}
287
515
  >
288
- <CommandMenu.Group heading={t("root.commandContent")}>
289
- <CommandMenu.Item onSelect={() => {}}>
290
- {t("root.commandSearchDocuments")}
291
- </CommandMenu.Item>
292
- </CommandMenu.Group>
293
516
  <CommandMenu.Group heading={t("root.commandAppearance")}>
294
517
  <ThemeToggleItem />
295
518
  </CommandMenu.Group>
@@ -302,7 +525,12 @@ export default function Root() {
302
525
  const [cmdkOpen, setCmdkOpen] = useState(false);
303
526
  const location = useLocation();
304
527
  const loaderData = useLoaderData<typeof loader>();
305
- useCommandMenuShortcut(useCallback(() => setCmdkOpen(true), []));
528
+ useCommandMenuShortcut(
529
+ useCallback(() => setCmdkOpen(true), []),
530
+ {
531
+ allowContentEditable: true,
532
+ },
533
+ );
306
534
 
307
535
  // Public document paths (/p/*) SSR real content without the ClientOnly gate
308
536
  // so crawlers and unauthenticated visitors receive full markup on first visit.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-06-29
4
+ ---
5
+
6
+ Builder CMS sources now sync article bodies into Content and push approved body edits through the Builder review flow.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-06-30
4
+ ---
5
+
6
+ Builder article media now render as images and embeds when readable body content syncs from Builder.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-06-30
4
+ ---
5
+
6
+ Builder source components now render as preserved preview blocks inside hydrated article bodies.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-06-30
4
+ ---
5
+
6
+ Cmd+K search now opens from the editor and finds real documents, databases, and local-file results.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-06-30
4
+ ---
5
+
6
+ Sidebar favorites now keep long titles tidy and stay in sync with saved page titles.
@@ -5,13 +5,15 @@
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "agent-native dev",
8
+ "dev:database": "node scripts/check-native-deps.mjs && node scripts/dev-database.mjs",
8
9
  "build": "agent-native build",
9
10
  "start": "agent-native start",
10
11
  "test": "vitest --run",
11
12
  "format.fix": "oxfmt --write .",
12
13
  "typecheck": "agent-native typecheck",
13
14
  "action": "agent-native action",
14
- "script": "agent-native script"
15
+ "script": "agent-native script",
16
+ "seed:demo": "node scripts/seed-demo-user.mjs"
15
17
  },
16
18
  "dependencies": {
17
19
  "@agent-native/core": "workspace:*",
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, readdirSync } from "node:fs";
3
+ import { createRequire } from "node:module";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const require = createRequire(import.meta.url);
9
+
10
+ function resolveBetterSqlite3() {
11
+ try {
12
+ return require.resolve("better-sqlite3");
13
+ } catch {
14
+ const pnpmDir = join(__dirname, "..", "..", "..", "node_modules", ".pnpm");
15
+ if (!existsSync(pnpmDir))
16
+ throw new Error("better-sqlite3 is not installed");
17
+ const packageDir = readdirSync(pnpmDir).find((entry) =>
18
+ entry.startsWith("better-sqlite3@"),
19
+ );
20
+ if (!packageDir) throw new Error("better-sqlite3 is not installed");
21
+ return join(
22
+ pnpmDir,
23
+ packageDir,
24
+ "node_modules",
25
+ "better-sqlite3",
26
+ "lib",
27
+ "index.js",
28
+ );
29
+ }
30
+ }
31
+
32
+ try {
33
+ const { default: Database } = await import(
34
+ pathToFileURL(resolveBetterSqlite3()).href
35
+ );
36
+ const db = new Database(":memory:");
37
+ db.close();
38
+ } catch (error) {
39
+ const message = error instanceof Error ? error.message : String(error);
40
+ console.error(`
41
+ [content dev preflight] better-sqlite3 could not load its native binding.
42
+
43
+ This usually means the local install was restored without rebuilding native
44
+ modules. Try:
45
+
46
+ pnpm rebuild better-sqlite3
47
+
48
+ If that still fails, rebuild the package directly:
49
+
50
+ cd ../../node_modules/.pnpm/better-sqlite3@*/node_modules/better-sqlite3
51
+ npm run build-release
52
+
53
+ Original error:
54
+ ${message}
55
+ `);
56
+ process.exit(1);
57
+ }
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import { randomBytes } from "node:crypto";
4
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { dirname, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const scriptDir = dirname(fileURLToPath(import.meta.url));
9
+ const appDir = resolve(scriptDir, "..");
10
+ const envPath = resolve(appDir, ".env.local");
11
+
12
+ function parseEnv(text) {
13
+ const result = new Map();
14
+ for (const line of text.split(/\r?\n/)) {
15
+ const match = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$/);
16
+ if (!match) continue;
17
+ let value = match[2] ?? "";
18
+ if (
19
+ (value.startsWith('"') && value.endsWith('"')) ||
20
+ (value.startsWith("'") && value.endsWith("'"))
21
+ ) {
22
+ value = value.slice(1, -1);
23
+ }
24
+ result.set(match[1], value);
25
+ }
26
+ return result;
27
+ }
28
+
29
+ function serializeEnv(values) {
30
+ return `${[...values.entries()]
31
+ .map(([key, value]) => `${key}=${value}`)
32
+ .join("\n")}\n`;
33
+ }
34
+
35
+ function ensureLocalEnv() {
36
+ const existing = existsSync(envPath) ? readFileSync(envPath, "utf8") : "";
37
+ const values = parseEnv(existing);
38
+ let changed = false;
39
+
40
+ if (values.get("AGENT_NATIVE_MODE") !== "database") {
41
+ values.set("AGENT_NATIVE_MODE", "database");
42
+ changed = true;
43
+ }
44
+ const authSecret = values.get("BETTER_AUTH_SECRET") ?? "";
45
+ if (authSecret.length < 32) {
46
+ values.set("BETTER_AUTH_SECRET", randomBytes(32).toString("base64url"));
47
+ changed = true;
48
+ }
49
+
50
+ if (changed) {
51
+ writeFileSync(envPath, serializeEnv(values));
52
+ console.log("[content dev] Wrote local database-mode env to .env.local");
53
+ }
54
+ return values;
55
+ }
56
+
57
+ const envValues = ensureLocalEnv();
58
+ const childEnv = {
59
+ ...process.env,
60
+ AGENT_NATIVE_MODE: "database",
61
+ BETTER_AUTH_SECRET:
62
+ process.env.BETTER_AUTH_SECRET || envValues.get("BETTER_AUTH_SECRET"),
63
+ };
64
+
65
+ const pnpmBin = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
66
+ const child = spawn(pnpmBin, ["exec", "agent-native", "dev"], {
67
+ cwd: appDir,
68
+ env: childEnv,
69
+ stdio: "inherit",
70
+ shell: process.platform === "win32",
71
+ });
72
+
73
+ child.on("error", (error) => {
74
+ console.error(
75
+ `[content dev] Failed to start agent-native dev: ${error.message}`,
76
+ );
77
+ process.exit(1);
78
+ });
79
+
80
+ child.on("exit", (code, signal) => {
81
+ if (signal) process.kill(process.pid, signal);
82
+ process.exit(code ?? 0);
83
+ });
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+
4
+ const DEFAULT_EMAIL = "demo@example.local";
5
+ const DEFAULT_PASSWORD = "demo-content-password";
6
+
7
+ const args = new Map();
8
+ for (let i = 2; i < process.argv.length; i += 1) {
9
+ const arg = process.argv[i];
10
+ if (!arg.startsWith("--")) continue;
11
+ const key = arg.slice(2);
12
+ const next = process.argv[i + 1];
13
+ if (next && !next.startsWith("--")) {
14
+ args.set(key, next);
15
+ i += 1;
16
+ } else {
17
+ args.set(key, "true");
18
+ }
19
+ }
20
+
21
+ const email =
22
+ args.get("email") || process.env.CONTENT_DEMO_EMAIL || DEFAULT_EMAIL;
23
+ const password =
24
+ args.get("password") || process.env.CONTENT_DEMO_PASSWORD || DEFAULT_PASSWORD;
25
+ const shouldOpen = args.get("open") !== "false";
26
+
27
+ function candidateBaseUrls() {
28
+ if (args.get("base-url") || process.env.CONTENT_BASE_URL) {
29
+ return [args.get("base-url") || process.env.CONTENT_BASE_URL];
30
+ }
31
+ const urls = [];
32
+ for (const port of [8080, 8081, 8082, 8083, 8084, 8085, 8090]) {
33
+ urls.push(`http://127.0.0.1:${port}`);
34
+ }
35
+ return urls;
36
+ }
37
+
38
+ async function postJson(baseUrl, path, body) {
39
+ const response = await fetch(`${baseUrl}${path}`, {
40
+ method: "POST",
41
+ headers: {
42
+ "content-type": "application/json",
43
+ "x-request-source": "clips-desktop",
44
+ origin: baseUrl,
45
+ },
46
+ body: JSON.stringify(body),
47
+ });
48
+ let data = {};
49
+ try {
50
+ data = await response.json();
51
+ } catch {}
52
+ return { ok: response.ok, status: response.status, data };
53
+ }
54
+
55
+ async function findRunningBaseUrl() {
56
+ for (const baseUrl of candidateBaseUrls()) {
57
+ try {
58
+ const response = await fetch(`${baseUrl}/`, { method: "HEAD" });
59
+ if (response.ok || response.status === 401 || response.status === 405) {
60
+ return baseUrl;
61
+ }
62
+ } catch {}
63
+ }
64
+ throw new Error(
65
+ "Content dev server was not reachable. Start it with `pnpm dev:database`.",
66
+ );
67
+ }
68
+
69
+ const baseUrl = await findRunningBaseUrl();
70
+ let login = await postJson(baseUrl, "/_agent-native/auth/login", {
71
+ email,
72
+ password,
73
+ });
74
+
75
+ if (!login.ok) {
76
+ await postJson(baseUrl, "/_agent-native/auth/register", {
77
+ email,
78
+ password,
79
+ name: email.split("@")[0] || "Demo",
80
+ callbackURL: "/",
81
+ });
82
+ login = await postJson(baseUrl, "/_agent-native/auth/login", {
83
+ email,
84
+ password,
85
+ });
86
+ }
87
+
88
+ if (!login.ok) {
89
+ throw new Error(
90
+ `Demo login failed (${login.status}): ${
91
+ login.data?.error || login.data?.message || "unknown error"
92
+ }`,
93
+ );
94
+ }
95
+
96
+ const token = login.data?.token;
97
+ const url = token
98
+ ? `${baseUrl}/?_session=${encodeURIComponent(token)}`
99
+ : `${baseUrl}/`;
100
+
101
+ console.log(`[content demo] URL: ${url}`);
102
+ console.log(`[content demo] Email: ${email}`);
103
+ console.log(`[content demo] Password: ${password}`);
104
+
105
+ if (shouldOpen && process.platform === "darwin") {
106
+ spawnSync("open", ["-a", "Google Chrome", url], { stdio: "ignore" });
107
+ }
@@ -139,10 +139,37 @@ export const contentDatabaseItems = table("content_database_items", {
139
139
  databaseId: text("database_id").notNull(),
140
140
  documentId: text("document_id").notNull(),
141
141
  position: integer("position").notNull().default(0),
142
+ bodyHydrationStatus: text("body_hydration_status")
143
+ .notNull()
144
+ .default("hydrated"),
145
+ bodyHydrationAttemptedAt: text("body_hydration_attempted_at"),
146
+ bodyHydrationError: text("body_hydration_error"),
147
+ bodyHydrationVersion: text("body_hydration_version"),
142
148
  createdAt: text("created_at").notNull().default(now()),
143
149
  updatedAt: text("updated_at").notNull().default(now()),
144
150
  });
145
151
 
152
+ export const contentDatabaseBodyHydrationQueue = table(
153
+ "content_database_body_hydration_queue",
154
+ {
155
+ id: text("id").primaryKey(),
156
+ ownerEmail: text("owner_email").notNull().default("local@localhost"),
157
+ orgId: text("org_id"),
158
+ sourceId: text("source_id").notNull(),
159
+ databaseItemId: text("database_item_id").notNull(),
160
+ documentId: text("document_id").notNull(),
161
+ sourceRowId: text("source_row_id").notNull(),
162
+ sourceTable: text("source_table").notNull(),
163
+ sourceEntryJson: text("source_entry_json").notNull().default("{}"),
164
+ priority: integer("priority").notNull().default(10),
165
+ attempts: integer("attempts").notNull().default(0),
166
+ lastAttemptedAt: text("last_attempted_at"),
167
+ lastError: text("last_error"),
168
+ createdAt: text("created_at").notNull().default(now()),
169
+ updatedAt: text("updated_at").notNull().default(now()),
170
+ },
171
+ );
172
+
146
173
  export const contentDatabaseSources = table("content_database_sources", {
147
174
  id: text("id").primaryKey(),
148
175
  ownerEmail: text("owner_email").notNull().default("local@localhost"),
@@ -535,6 +535,39 @@ const runContentMigrations = runMigrations(
535
535
  sql: `CREATE INDEX IF NOT EXISTS builder_doc_sidecars_document_idx ON builder_doc_sidecars (document_id);
536
536
  CREATE UNIQUE INDEX IF NOT EXISTS builder_doc_sidecars_doc_path_idx ON builder_doc_sidecars (document_id, path)`,
537
537
  },
538
+ {
539
+ version: 58,
540
+ sql: `ALTER TABLE content_database_items ADD COLUMN IF NOT EXISTS body_hydration_status TEXT NOT NULL DEFAULT 'hydrated';
541
+ ALTER TABLE content_database_items ADD COLUMN IF NOT EXISTS body_hydration_attempted_at TEXT;
542
+ ALTER TABLE content_database_items ADD COLUMN IF NOT EXISTS body_hydration_error TEXT;
543
+ ALTER TABLE content_database_items ADD COLUMN IF NOT EXISTS body_hydration_version TEXT`,
544
+ },
545
+ {
546
+ version: 59,
547
+ sql: `CREATE TABLE IF NOT EXISTS content_database_body_hydration_queue (
548
+ id TEXT PRIMARY KEY,
549
+ owner_email TEXT NOT NULL DEFAULT 'local@localhost',
550
+ org_id TEXT,
551
+ source_id TEXT NOT NULL,
552
+ database_item_id TEXT NOT NULL,
553
+ document_id TEXT NOT NULL,
554
+ source_row_id TEXT NOT NULL,
555
+ source_table TEXT NOT NULL,
556
+ source_entry_json TEXT NOT NULL DEFAULT '{}',
557
+ priority INTEGER NOT NULL DEFAULT 10,
558
+ attempts INTEGER NOT NULL DEFAULT 0,
559
+ last_attempted_at TEXT,
560
+ last_error TEXT,
561
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
562
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
563
+ )`,
564
+ },
565
+ {
566
+ version: 60,
567
+ sql: `CREATE INDEX IF NOT EXISTS content_database_body_hydration_queue_source_idx ON content_database_body_hydration_queue (source_id, priority, created_at);
568
+ CREATE UNIQUE INDEX IF NOT EXISTS content_database_body_hydration_queue_item_idx ON content_database_body_hydration_queue (database_item_id);
569
+ CREATE INDEX IF NOT EXISTS content_database_items_body_hydration_idx ON content_database_items (database_id, body_hydration_status)`,
570
+ },
538
571
  ],
539
572
  { table: "content_migrations" },
540
573
  );