@agent-native/core 0.82.0 → 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.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +6 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/CommandMenu.tsx +34 -12
- package/corpus/templates/analytics/actions/hubspot-deals.ts +64 -4
- package/corpus/templates/content/.env.local.example +6 -0
- package/corpus/templates/content/AGENTS.md +10 -1
- package/corpus/templates/content/actions/_builder-cms-read-client.ts +14 -1
- package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +43 -0
- package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +79 -7
- package/corpus/templates/content/actions/_database-source-utils.ts +987 -22
- package/corpus/templates/content/actions/_database-utils.ts +41 -0
- package/corpus/templates/content/actions/_local-file-documents.ts +74 -1
- package/corpus/templates/content/actions/attach-content-database-source.ts +33 -8
- package/corpus/templates/content/actions/change-content-database-source-role.ts +14 -0
- package/corpus/templates/content/actions/disconnect-content-database-source.ts +3 -0
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +71 -3
- package/corpus/templates/content/actions/list-content-databases.ts +41 -33
- package/corpus/templates/content/actions/process-builder-body-hydration.ts +47 -0
- package/corpus/templates/content/actions/update-document.ts +93 -0
- package/corpus/templates/content/app/blocks/SourceComponentBlock.tsx +277 -0
- package/corpus/templates/content/app/blocks/contentBlockRegistry.tsx +2 -0
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +45 -4
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +5 -1
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +92 -0
- package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +5 -3
- package/corpus/templates/content/app/components/editor/extensions/registryBlocks.ts +41 -0
- package/corpus/templates/content/app/components/editor/registrySlashItems.ts +4 -0
- package/corpus/templates/content/app/global.css +10 -3
- package/corpus/templates/content/app/hooks/use-content-database.ts +24 -0
- package/corpus/templates/content/app/i18n/zh-TW.ts +24 -0
- package/corpus/templates/content/app/i18n-data.ts +212 -0
- package/corpus/templates/content/app/lib/content-command-search.ts +64 -0
- package/corpus/templates/content/app/root.tsx +236 -8
- package/corpus/templates/content/changelog/2026-06-29-builder-cms-sources-now-sync-article-bodies-through-content.md +6 -0
- package/corpus/templates/content/changelog/2026-06-30-builder-article-media-now-render-as-images-and-embeds-when-r.md +6 -0
- package/corpus/templates/content/changelog/2026-06-30-builder-source-components-now-render-as-preserved-preview-bl.md +6 -0
- package/corpus/templates/content/changelog/2026-06-30-cmd-k-search-now-opens-from-the-editor-and-finds-real-content.md +6 -0
- package/corpus/templates/content/package.json +3 -1
- package/corpus/templates/content/scripts/check-native-deps.mjs +57 -0
- package/corpus/templates/content/scripts/dev-database.mjs +83 -0
- package/corpus/templates/content/scripts/seed-demo-user.mjs +107 -0
- package/corpus/templates/content/server/db/schema.ts +27 -0
- package/corpus/templates/content/server/plugins/db.ts +33 -0
- package/corpus/templates/content/shared/api.ts +45 -0
- package/corpus/templates/content/shared/builder-mdx.ts +905 -5
- package/corpus/templates/content/shared/nfm-registry.ts +2 -0
- package/corpus/templates/content/shared/source-component-block.ts +141 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +5 -1
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +229 -131
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +103 -6
- package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +6 -3
- package/corpus/templates/design/app/pages/DesignEditor.tsx +382 -72
- package/corpus/templates/design/changelog/2026-06-30-canvas-editing-now-keeps-undo-redo-and-cross-screen-layer-mo.md +6 -0
- package/dist/client/CommandMenu.d.ts +6 -2
- package/dist/client/CommandMenu.d.ts.map +1 -1
- package/dist/client/CommandMenu.js +25 -14
- package/dist/client/CommandMenu.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +8 -8
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- 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 {
|
|
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(
|
|
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.
|
|
@@ -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
|
);
|