@agent-native/core 0.84.21 → 0.84.22

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 (96) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +8 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/client/AssistantChat.tsx +27 -4
  5. package/corpus/core/src/client/guided-questions.tsx +11 -6
  6. package/corpus/core/src/client/tool-display.ts +8 -0
  7. package/corpus/core/src/file-upload/builder.ts +7 -2
  8. package/corpus/core/src/notifications/channels.ts +235 -38
  9. package/corpus/core/src/server/analytics.ts +32 -6
  10. package/corpus/core/src/server/ssr-handler.ts +31 -12
  11. package/corpus/templates/analytics/AGENTS.md +21 -0
  12. package/corpus/templates/analytics/README.md +35 -0
  13. package/corpus/templates/analytics/actions/delete-analytics-alert-rule.ts +23 -0
  14. package/corpus/templates/analytics/actions/list-analytics-alert-rules.ts +20 -0
  15. package/corpus/templates/analytics/actions/run-analytics-alerts.ts +23 -0
  16. package/corpus/templates/analytics/actions/save-analytics-alert-rule.ts +69 -0
  17. package/corpus/templates/analytics/changelog/2026-07-01-analytics-can-alert-when-first-party-events-spike.md +6 -0
  18. package/corpus/templates/analytics/changelog/2026-07-01-session-replays-play-back-in-production-instead-of-showing.md +6 -0
  19. package/corpus/templates/analytics/netlify.toml +3 -0
  20. package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +109 -0
  21. package/corpus/templates/analytics/server/db/schema.ts +55 -0
  22. package/corpus/templates/analytics/server/handlers/session-replay.ts +5 -2
  23. package/corpus/templates/analytics/server/jobs/analytics-alerts.ts +71 -0
  24. package/corpus/templates/analytics/server/lib/analytics-alerts.ts +734 -0
  25. package/corpus/templates/analytics/server/lib/session-replay.ts +10 -3
  26. package/corpus/templates/analytics/server/plugins/analytics-alert-jobs.ts +54 -0
  27. package/corpus/templates/analytics/server/plugins/db.ts +100 -0
  28. package/corpus/templates/analytics/server/routes/api/analytics-alerts/run.post.ts +60 -0
  29. package/corpus/templates/assets/README.md +30 -8
  30. package/corpus/templates/brain/README.md +20 -552
  31. package/corpus/templates/calendar/README.md +22 -28
  32. package/corpus/templates/chat/README.md +32 -0
  33. package/corpus/templates/clips/README.md +37 -0
  34. package/corpus/templates/clips/app/components/library/library-grid.tsx +18 -7
  35. package/corpus/templates/clips/app/components/library/recording-card.tsx +42 -5
  36. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +41 -1
  37. package/corpus/templates/clips/changelog/2026-07-01-move-to-folder-now-opens-folder-choices-directly-instead-of.md +6 -0
  38. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  39. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +37 -0
  40. package/corpus/templates/content/AGENTS.md +7 -3
  41. package/corpus/templates/content/README.md +22 -34
  42. package/corpus/templates/content/actions/list-trashed-content-databases.ts +4 -0
  43. package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +58 -6
  44. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +257 -24
  45. package/corpus/templates/content/app/components/sidebar/DocumentTreeItem.tsx +41 -15
  46. package/corpus/templates/content/app/i18n/zh-TW.ts +25 -7
  47. package/corpus/templates/content/app/i18n-data.ts +278 -40
  48. package/corpus/templates/content/changelog/2026-06-30-database-once-again-creates-an-inline-database-in-the-curren.md +6 -0
  49. package/corpus/templates/content/shared/api.ts +1 -0
  50. package/corpus/templates/design/AGENTS.md +7 -6
  51. package/corpus/templates/design/README.md +33 -0
  52. package/corpus/templates/design/actions/delete-file.ts +86 -10
  53. package/corpus/templates/design/actions/get-design-snapshot.ts +13 -0
  54. package/corpus/templates/design/actions/present-design-variants.ts +11 -13
  55. package/corpus/templates/design/app/hooks/use-question-flow.ts +2 -2
  56. package/corpus/templates/design/app/pages/DesignEditor.tsx +2 -2
  57. package/corpus/templates/dispatch/README.md +34 -0
  58. package/corpus/templates/forms/README.md +34 -0
  59. package/corpus/templates/macros/README.md +28 -0
  60. package/corpus/templates/mail/README.md +35 -0
  61. package/corpus/templates/plan/README.md +30 -118
  62. package/corpus/templates/slides/README.md +34 -0
  63. package/corpus/templates/videos/README.md +22 -201
  64. package/dist/client/AssistantChat.d.ts.map +1 -1
  65. package/dist/client/AssistantChat.js +28 -4
  66. package/dist/client/AssistantChat.js.map +1 -1
  67. package/dist/client/guided-questions.d.ts.map +1 -1
  68. package/dist/client/guided-questions.js +10 -6
  69. package/dist/client/guided-questions.js.map +1 -1
  70. package/dist/client/tool-display.d.ts.map +1 -1
  71. package/dist/client/tool-display.js +8 -0
  72. package/dist/client/tool-display.js.map +1 -1
  73. package/dist/collab/awareness.d.ts +2 -2
  74. package/dist/collab/awareness.d.ts.map +1 -1
  75. package/dist/collab/routes.d.ts +1 -1
  76. package/dist/file-upload/builder.d.ts.map +1 -1
  77. package/dist/file-upload/builder.js +6 -2
  78. package/dist/file-upload/builder.js.map +1 -1
  79. package/dist/notifications/channels.d.ts +10 -0
  80. package/dist/notifications/channels.d.ts.map +1 -1
  81. package/dist/notifications/channels.js +172 -20
  82. package/dist/notifications/channels.js.map +1 -1
  83. package/dist/notifications/routes.d.ts +3 -3
  84. package/dist/observability/routes.d.ts +5 -5
  85. package/dist/progress/routes.d.ts +1 -1
  86. package/dist/resources/handlers.d.ts +1 -1
  87. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  88. package/dist/server/analytics.d.ts +16 -0
  89. package/dist/server/analytics.d.ts.map +1 -1
  90. package/dist/server/analytics.js +30 -7
  91. package/dist/server/analytics.js.map +1 -1
  92. package/dist/server/ssr-handler.d.ts.map +1 -1
  93. package/dist/server/ssr-handler.js +28 -12
  94. package/dist/server/ssr-handler.js.map +1 -1
  95. package/dist/server/transcribe-voice.d.ts +1 -1
  96. package/package.json +1 -1
@@ -0,0 +1,37 @@
1
+ # Clips
2
+
3
+ An open-source, agent-native alternative to Loom, Granola, and Wispr Flow rolled
4
+ into one app. Screen recording, calendar-synced meeting notes, and push-to-talk
5
+ voice dictation — all transcribed, searchable, and yours to own.
6
+
7
+ **Live app: [clips.agent-native.com](https://clips.agent-native.com)**
8
+
9
+ The agent transcribes, titles, summarizes, and indexes everything you capture,
10
+ then lets you ask "find the clip where we discussed the rollout plan" across every
11
+ transcript. Shared clips are agent-readable: paste a share link into an agent and
12
+ it can read the transcript and see timestamped frames without the raw video.
13
+
14
+ ## Features
15
+
16
+ - Screen recording with webcam overlay, audio capture, and pause/trim.
17
+ - Calendar-synced meeting recordings with AI summary, notes, and action items.
18
+ - Push-to-talk (Fn-hold) dictation with searchable history and vocabulary biasing.
19
+ - Auto-generated titles, summaries, and chapter markers for every recording.
20
+ - Full-text search across recordings, meetings, and dictations in one library.
21
+ - Sharing with per-clip permissions, agent-readable links, and Slack unfurls.
22
+ - Chrome extension for capturing browser logs with a recording.
23
+
24
+ ## Develop locally
25
+
26
+ Scaffold your own copy and run it:
27
+
28
+ ```bash
29
+ npx @agent-native/core@latest create my-clips --standalone --template clips
30
+ cd my-clips
31
+ pnpm install
32
+ pnpm dev
33
+ ```
34
+
35
+ Clips needs a video storage backend (Builder.io or an S3-compatible bucket)
36
+ before recordings can upload — see the docs for setup.
37
+ Full docs: [agent-native.com/docs/template-clips](https://agent-native.com/docs/template-clips).
@@ -1,6 +1,5 @@
1
1
  import {
2
2
  getBrowserTabId,
3
- sendToAgentChat,
4
3
  setClientAppState,
5
4
  useSession,
6
5
  useT,
@@ -225,6 +224,21 @@ export function LibraryGrid({
225
224
  }
226
225
  };
227
226
 
227
+ const moveSingle = async (
228
+ rec: RecordingSummary,
229
+ targetFolderId: string | null,
230
+ ) => {
231
+ try {
232
+ await moveRecording.mutateAsync({
233
+ id: rec.id,
234
+ folderId: targetFolderId,
235
+ });
236
+ toast.success(t("libraryGrid.clipsMoved", { count: 1 }));
237
+ } catch (err: any) {
238
+ toast.error(err?.message ?? t("libraryGrid.moveFailed"));
239
+ }
240
+ };
241
+
228
242
  const openRenameDialog = (rec: RecordingSummary) => {
229
243
  setRenamingRec(rec);
230
244
  setRenameValue(isDefaultTitle(rec.title) ? "" : (rec.title ?? ""));
@@ -385,12 +399,9 @@ export function LibraryGrid({
385
399
  ? openRenameDialog
386
400
  : undefined
387
401
  }
388
- onMove={(rec) => {
389
- sendToAgentChat({
390
- message: `Move the clip "${rec.title}" (id: ${rec.id}) to a folder. Ask me which folder to move it to, or list available folders.`,
391
- background: false,
392
- });
393
- }}
402
+ moveTargets={moveTargets}
403
+ onMove={canMoveSelection ? moveSingle : undefined}
404
+ isMovePending={moveRecording.isPending}
394
405
  onTrash={(rec) => {
395
406
  trashRecording.mutate(
396
407
  { id: rec.id },
@@ -24,6 +24,9 @@ import {
24
24
  DropdownMenuContent,
25
25
  DropdownMenuItem,
26
26
  DropdownMenuSeparator,
27
+ DropdownMenuSub,
28
+ DropdownMenuSubContent,
29
+ DropdownMenuSubTrigger,
27
30
  DropdownMenuTrigger,
28
31
  } from "@/components/ui/dropdown-menu";
29
32
  import { isDefaultTitle } from "@/hooks/use-auto-title";
@@ -32,6 +35,8 @@ import { isStaleRecordingUpload } from "@/lib/recording-status";
32
35
  import { isStorageSetupFailureReason } from "@/lib/storage-failures";
33
36
  import { cn } from "@/lib/utils";
34
37
 
38
+ import type { BulkMoveTarget } from "./bulk-action-toolbar";
39
+
35
40
  function formatDuration(ms: number): string {
36
41
  const totalSeconds = Math.max(0, Math.floor(ms / 1000));
37
42
  const m = Math.floor(totalSeconds / 60);
@@ -59,7 +64,9 @@ interface RecordingCardProps {
59
64
  selectionMode?: boolean;
60
65
  onToggleSelect?: (id: string) => void;
61
66
  onShare?: (rec: RecordingSummary) => void;
62
- onMove?: (rec: RecordingSummary) => void;
67
+ onMove?: (rec: RecordingSummary, folderId: string | null) => void;
68
+ moveTargets?: BulkMoveTarget[];
69
+ isMovePending?: boolean;
63
70
  onRename?: (rec: RecordingSummary) => void;
64
71
  onArchive?: (rec: RecordingSummary) => void;
65
72
  onTrash?: (rec: RecordingSummary) => void;
@@ -73,6 +80,8 @@ export function RecordingCard({
73
80
  onToggleSelect,
74
81
  onShare,
75
82
  onMove,
83
+ moveTargets = [],
84
+ isMovePending = false,
76
85
  onRename,
77
86
  onArchive,
78
87
  onTrash,
@@ -112,6 +121,7 @@ export function RecordingCard({
112
121
  /native recording|native fullscreen|screencapture|avconvert/i.test(
113
122
  recording.failureReason ?? "",
114
123
  );
124
+ const canMove = Boolean(onMove && moveTargets.length > 0);
115
125
 
116
126
  const displayThumbnail = useMemo(() => {
117
127
  if (hovered && recording.animatedThumbnailUrl)
@@ -332,10 +342,37 @@ export function RecordingCard({
332
342
  <IconShare className="h-4 w-4 me-2" />{" "}
333
343
  {t("recordingPage.share")}
334
344
  </DropdownMenuItem>
335
- <DropdownMenuItem onSelect={() => onMove?.(recording)}>
336
- <IconFolder className="h-4 w-4 me-2" />{" "}
337
- {t("clipsFinalRaw.moveToFolder")}
338
- </DropdownMenuItem>
345
+ {canMove ? (
346
+ <DropdownMenuSub>
347
+ <DropdownMenuSubTrigger>
348
+ <IconFolder className="h-4 w-4 me-2" />{" "}
349
+ {t("clipsFinalRaw.moveToFolder")}
350
+ </DropdownMenuSubTrigger>
351
+ <DropdownMenuSubContent className="w-64">
352
+ {moveTargets.map((target, index) => (
353
+ <DropdownMenuItem
354
+ key={target.id ?? `root-${index}`}
355
+ disabled={target.disabled || isMovePending}
356
+ onSelect={() => onMove?.(recording, target.id)}
357
+ >
358
+ <span
359
+ className="truncate"
360
+ style={{
361
+ paddingInlineStart: (target.depth ?? 0) * 12,
362
+ }}
363
+ >
364
+ {target.name}
365
+ </span>
366
+ {target.disabled && (
367
+ <span className="ms-auto text-xs text-muted-foreground">
368
+ {t("clipsFinalRaw.current")}
369
+ </span>
370
+ )}
371
+ </DropdownMenuItem>
372
+ ))}
373
+ </DropdownMenuSubContent>
374
+ </DropdownMenuSub>
375
+ ) : null}
339
376
  {onRename ? (
340
377
  <>
341
378
  <DropdownMenuSeparator />
@@ -6,7 +6,11 @@
6
6
  * but no React state lives here — callers subscribe via `onState`, `onChunk`,
7
7
  * and `onError`.
8
8
  */
9
- import { appBasePath, captureClientException } from "@agent-native/core/client";
9
+ import {
10
+ appBasePath,
11
+ captureClientException,
12
+ trackEvent,
13
+ } from "@agent-native/core/client";
10
14
  import { waitForReadyRecordingAfterFinalizeError } from "@shared/finalize-recovery";
11
15
  import {
12
16
  chunkUploadUrl,
@@ -354,6 +358,19 @@ function isRetryableChunkUploadStatus(status: number): boolean {
354
358
  return RETRYABLE_CHUNK_UPLOAD_STATUSES.has(status);
355
359
  }
356
360
 
361
+ function trackClipUploadBlockingFailure(props: Record<string, unknown>): void {
362
+ try {
363
+ trackEvent("clips_upload_blocking_failure", {
364
+ app: "clips",
365
+ template: "clips",
366
+ surface: "web_recorder",
367
+ ...props,
368
+ });
369
+ } catch {
370
+ // Analytics should never change recording behavior.
371
+ }
372
+ }
373
+
357
374
  function retryDelayMs(attempt: number): number {
358
375
  return attempt === 1 ? 500 : 1500;
359
376
  }
@@ -1775,6 +1792,15 @@ export class RecorderEngine {
1775
1792
  }
1776
1793
 
1777
1794
  if (!res) {
1795
+ trackClipUploadBlockingFailure({
1796
+ stage: "chunk_upload",
1797
+ failureKind: "no_response",
1798
+ recordingId: this.opts.recordingId,
1799
+ chunkIndex: index,
1800
+ isFinal: extra.isFinal === true,
1801
+ chunkBytes: blob.size,
1802
+ uploadMode: this.opts.uploadMode,
1803
+ });
1778
1804
  throw new Error(`Chunk ${index} upload failed: no response`);
1779
1805
  }
1780
1806
 
@@ -1791,6 +1817,20 @@ export class RecorderEngine {
1791
1817
  const recovered = await this.recoverReadyAfterFinalUploadError();
1792
1818
  if (recovered) return recovered;
1793
1819
  }
1820
+ trackClipUploadBlockingFailure({
1821
+ stage: "chunk_upload",
1822
+ failureKind: "http_error",
1823
+ recordingId: this.opts.recordingId,
1824
+ chunkIndex: index,
1825
+ isFinal: extra.isFinal === true,
1826
+ httpStatus: res.status,
1827
+ statusText: res.statusText,
1828
+ chunkBytes: blob.size,
1829
+ uploadMode: this.opts.uploadMode,
1830
+ finalUploadRecoveryAttempted:
1831
+ extra.isFinal === true &&
1832
+ this.isFinalUploadRecoveryCandidate(res.status, err),
1833
+ });
1794
1834
  // Capture rich context to Sentry BEFORE throwing — when this hits
1795
1835
  // production we want enough breadcrumbs in the event to debug a
1796
1836
  // "Builder.io upload failed (500)" without re-running the upload.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-01
4
+ ---
5
+
6
+ Move to folder now opens folder choices directly instead of starting an agent chat.
@@ -2,7 +2,7 @@
2
2
  "manifest_version": 3,
3
3
  "name": "Agent-Native Clips",
4
4
  "short_name": "Clips",
5
- "version": "0.1.10",
5
+ "version": "0.1.11",
6
6
  "description": "Start Clips recordings from Chrome, capture optional diagnostics, and preview Clips links on GitHub.",
7
7
  "minimum_chrome_version": "116",
8
8
  "action": {
@@ -19,6 +19,7 @@ import {
19
19
  } from "@agent-native/core/application-state";
20
20
  import { getActiveFileUploadProvider } from "@agent-native/core/file-upload";
21
21
  import { runWithRequestContext } from "@agent-native/core/server";
22
+ import { track } from "@agent-native/core/tracking";
22
23
  import { normalizeChunkUploadNumber } from "@shared/recording-core.js";
23
24
  import { MAX_UPLOAD_BYTES as MAX_RECORDING_UPLOAD_BYTES } from "@shared/upload-limits.js";
24
25
  import { and, eq } from "drizzle-orm";
@@ -95,6 +96,26 @@ function stateNumber(
95
96
  return typeof raw === "number" && Number.isFinite(raw) ? raw : undefined;
96
97
  }
97
98
 
99
+ function trackUploadBlockingFailure(
100
+ ownerEmail: string,
101
+ properties: Record<string, unknown>,
102
+ ): void {
103
+ try {
104
+ track(
105
+ "clips_upload_blocking_failure",
106
+ {
107
+ app: "clips",
108
+ template: "clips",
109
+ surface: "server_upload",
110
+ ...properties,
111
+ },
112
+ { userId: ownerEmail },
113
+ );
114
+ } catch {
115
+ // Best-effort analytics must never change upload behavior.
116
+ }
117
+ }
118
+
98
119
  export default defineEventHandler(async (event: H3Event) => {
99
120
  const recordingId = getRouterParam(event, "recordingId");
100
121
  if (!recordingId) {
@@ -189,6 +210,7 @@ export default defineEventHandler(async (event: H3Event) => {
189
210
  isFinal,
190
211
  mimeType,
191
212
  query,
213
+ ownerEmail,
192
214
  );
193
215
  }
194
216
 
@@ -507,6 +529,13 @@ export default defineEventHandler(async (event: H3Event) => {
507
529
  hasCamera: committed.hasCamera,
508
530
  };
509
531
  }
532
+ trackUploadBlockingFailure(ownerEmail, {
533
+ stage: "finalize_recording",
534
+ failureKind: "finalize_error",
535
+ recordingId,
536
+ uploadMode: "buffered",
537
+ errorMessage: err instanceof Error ? err.message : String(err),
538
+ });
510
539
  await db
511
540
  .update(schema.recordings)
512
541
  .set({
@@ -566,6 +595,7 @@ async function handleResumableChunk(
566
595
  isFinal: boolean,
567
596
  mimeType: string,
568
597
  query: Record<string, unknown>,
598
+ ownerEmail: string,
569
599
  ) {
570
600
  const uploadProvider = getActiveFileUploadProvider();
571
601
  console.log(
@@ -681,6 +711,13 @@ async function handleResumableChunk(
681
711
  return { ok: true, finalized: true, ...result };
682
712
  } catch (err) {
683
713
  console.error(`[resumable-chunk-${recordingId}] finalize failed:`, err);
714
+ trackUploadBlockingFailure(ownerEmail, {
715
+ stage: "finalize_recording",
716
+ failureKind: "finalize_error",
717
+ recordingId,
718
+ uploadMode: "resumable",
719
+ errorMessage: err instanceof Error ? err.message : String(err),
720
+ });
684
721
  setResponseStatus(event, 500);
685
722
  return {
686
723
  ok: false,
@@ -108,7 +108,10 @@ cd templates/content && pnpm action <name> [args]
108
108
  | `list-local-component-files` | | List registered local MDX component source files |
109
109
  | `write-local-component-file` | `--workspaceId <id> --path <relative-component-path> --content <source>` | Create or update a file in a registered local `components/` folder |
110
110
  | `create-content-database` | `[--documentId <id>] [--parentId <id>] [--title <text>]` | Create a database page or convert an existing page into a database |
111
+ | `create-inline-content-database` | `--hostDocumentId <id> [--title <text>]` | Create a database owned by an inline database block in the host document |
111
112
  | `get-content-database` | `--databaseId <id>` or `--documentId <id>` | Get a database table with property schema and item pages |
113
+ | `list-trashed-content-databases` | | List soft-deleted databases visible in the sidebar Trash surface |
114
+ | `restore-content-database` | `--databaseId <id>` | Restore a soft-deleted database from the sidebar Trash surface |
112
115
  | `get-content-database-source` | `--databaseId <id>` or `--documentId <id>` | Inspect local/no-source or source-backed status, mappings, row identity, freshness, and change sets |
113
116
  | `attach-content-database-source` | `--databaseId <id>` or `--documentId <id> [--sourceType mock\|builder-cms] [--sourceName] [--sourceTable] [--relationshipMode items\|details] [--join <json>]` | Attach a source binding; use `items` to add more rows and `details` to match a source onto existing rows |
114
117
  | `change-content-database-source-role` | `--databaseId <id>` or `--documentId <id> --sourceId <id> --relationshipMode items\|details [--join <json>]` | Change an attached source between adding rows and adding matched detail columns without removing the source |
@@ -569,9 +572,10 @@ columns updates that row page's grouping property through
569
572
  multi-select, users can add a new board group from the board itself; this
570
573
  appends a new option to the grouped property definition.
571
574
  Use
572
- `create-content-database`, `get-content-database`,
573
- `add-database-item`, `duplicate-database-item`, `duplicate-database-items`,
574
- `delete-database-items`, `move-database-item`,
575
+ `create-content-database`, `create-inline-content-database`,
576
+ `get-content-database`, `list-trashed-content-databases`,
577
+ `restore-content-database`, `add-database-item`, `duplicate-database-item`,
578
+ `duplicate-database-items`, `delete-database-items`, `move-database-item`,
575
579
  `update-content-database-view`, `list-document-properties`,
576
580
  `configure-document-property`, `set-document-property`,
577
581
  `duplicate-document-property`, and `delete-document-property`; do not edit
@@ -1,46 +1,34 @@
1
1
  # Content
2
2
 
3
- Open-source Obsidian for MDX, built with the agent-native framework. Edit local
4
- Markdown/MDX files, generate rich interactive custom blocks, and organize
5
- hierarchical pages with an AI agent.
3
+ Open-source Obsidian for MDX an agent-native, local-file document workspace and
4
+ a self-hostable alternative to Notion and Obsidian. Edit local Markdown/MDX files,
5
+ build Notion-style databases, and write with an AI agent.
6
+
7
+ **Live app: [content.agent-native.com](https://content.agent-native.com)**
8
+
9
+ Open a doc and ask "rewrite this paragraph to be more concise" or "create a page
10
+ called Q4 Planning with sub-pages for Goals, Metrics, and Risks" — the same result
11
+ whether you do it yourself or ask. Documents can live in your SQL database or as
12
+ local Markdown/MDX files.
6
13
 
7
14
  ## Features
8
15
 
9
- - Hierarchical pages (unlimited nesting)
10
- - Rich text editor (Tiptap) with slash commands
11
- - Favorites for quick access
12
- - Full-text search
13
- - Local Markdown/MDX file editing
14
- - Custom interactive MDX blocks from local components
15
- - Agent can create, read, update, and search documents
16
- - Auto-save with debouncing
17
- - Dark/light theme
16
+ - Rich-text editor (Tiptap) with slash commands and hierarchical pages.
17
+ - Notion-style databases with table, board, calendar, gallery, and timeline views.
18
+ - Local Markdown/MDX file editing plus custom interactive MDX components.
19
+ - Inline comments, version history, and full-text search.
20
+ - Private-by-default sharing with per-user / per-org grants and public pages.
21
+ - Optional Notion and Builder.io sync.
18
22
 
19
- ## Getting Started
23
+ ## Develop locally
24
+
25
+ Scaffold your own copy and run it:
20
26
 
21
27
  ```bash
28
+ npx @agent-native/core@latest create my-content --standalone --template content
29
+ cd my-content
22
30
  pnpm install
23
31
  pnpm dev
24
32
  ```
25
33
 
26
- Open http://localhost:8080 and create your first page.
27
-
28
- ## Data
29
-
30
- Documents are stored in the app's SQL database. Local development defaults to SQLite at `data/app.db`; deployed apps should set `DATABASE_URL` to a persistent SQL database. The agent should use content actions for normal document operations and reserve `db-query` / `db-exec` for inspection or maintenance.
31
-
32
- ## Enable Builder live writes
33
-
34
- Connect Builder through the existing Builder Connect flow, the same connection used by the AI assistant. Once connected, Content resolves the key automatically for the user, or for org owners/admins through the org connection. There is no separate key entry. In local development, `BUILDER_PRIVATE_KEY` and `BUILDER_PUBLIC_KEY` in `.env.local` also work; see `DEVELOPING.md` for local env opt-in details.
35
-
36
- Live writes are only allowed for the safe write model `agent-native-blog-article-test` (`BUILDER_CMS_SAFE_WRITE_MODEL`). Other Builder models stay read-only by design.
37
-
38
- Write access is an intentional per-source choice from the source panel selector. Sources start at `read_only`, can move to `stage_only` to push approved changes as Builder autosave revisions, and can move to `publish_updates` for state-preserving live writes. Autosave staging stays quiet; update-in-place and publish writes trigger Builder webhooks so downstream rebuilds can run.
39
-
40
- In `publish_updates`, Content PATCHes the Builder entry in place and preserves its current publication state: published entries stay published and drafts stay draft. Content never sends a `published` field, so a normal content push cannot publish or unpublish. Builder merges the field changes and preserves the entry envelope, including scheduling and targeting.
41
-
42
- Publication transitions are explicit per-row choices in the review/diff, only when the source enables **Allow publish/unpublish per item**. Publishing a draft or unpublishing a published entry is never a bulk default, and unpublish requires confirmation because it takes live content down. New Builder entries are created as drafts.
43
-
44
- **Push all approved (N)** runs approved rows in a concurrency-capped batch. It continues after individual errors, reports per-item `succeeded`, `blocked`, or `failed` status, and can be resumed because already-succeeded items are skipped.
45
-
46
- Before each write, Content reads the target's current state from Builder. A write blocks if the entry changed or was deleted since the diff, or if a requested publish/unpublish transition no longer matches the real Builder state. Body diffs remain non-executable for now; they are planned for a later slice.
34
+ Full docs: [agent-native.com/docs/template-content](https://agent-native.com/docs/template-content).
@@ -33,6 +33,7 @@ export default defineAction({
33
33
  ownerDocumentId: schema.contentDatabases.ownerDocumentId,
34
34
  deletedAt: schema.contentDatabases.deletedAt,
35
35
  documentTitle: schema.documents.title,
36
+ documentParentId: schema.documents.parentId,
36
37
  })
37
38
  .from(schema.contentDatabases)
38
39
  .innerJoin(
@@ -80,6 +81,9 @@ export default defineAction({
80
81
  documentId: row.documentId,
81
82
  ownerDocumentId: row.ownerDocumentId,
82
83
  deletedAt: row.deletedAt!,
84
+ canPermanentlyDelete:
85
+ row.ownerDocumentId === null ||
86
+ row.documentParentId !== row.ownerDocumentId,
83
87
  })),
84
88
  };
85
89
  },
@@ -1,5 +1,7 @@
1
1
  import { useSendToAgentChat, useT } from "@agent-native/core/client";
2
+ import type { CreateInlineDatabaseResponse } from "@shared/api";
2
3
  import { collapseExactRepeatedNfm, docToNfm } from "@shared/nfm";
4
+ import { serializeRegistryBlockToMdx } from "@shared/nfm-registry";
3
5
  import {
4
6
  IconTypography,
5
7
  IconH1,
@@ -33,7 +35,7 @@ import {
33
35
  PopoverContent,
34
36
  PopoverTrigger,
35
37
  } from "@/components/ui/popover";
36
- import { useCreateContentDatabase } from "@/hooks/use-content-database";
38
+ import { useCreateInlineContentDatabase } from "@/hooks/use-content-database";
37
39
  import { useCreatePage } from "@/hooks/use-create-page";
38
40
  import { cn } from "@/lib/utils";
39
41
  import { localContentComponents } from "@/local-components";
@@ -146,6 +148,36 @@ export function parseSlashCommandQuery(textBeforeCursor: string) {
146
148
  return textBeforeCursor.match(/^\s*\/([a-zA-Z0-9]*)$/)?.[1] ?? null;
147
149
  }
148
150
 
151
+ export function inlineDatabaseBlockContent(
152
+ block: CreateInlineDatabaseResponse["block"],
153
+ ) {
154
+ return {
155
+ type: "registryBlock",
156
+ attrs: {
157
+ blockType: "inline-database",
158
+ blockId: block.ownerBlockId,
159
+ title: null,
160
+ summary: null,
161
+ __raw: serializeRegistryBlockToMdx("inline-database", {
162
+ id: block.ownerBlockId,
163
+ data: block,
164
+ }),
165
+ },
166
+ };
167
+ }
168
+
169
+ export function insertInlineDatabaseBlock(
170
+ editor: Editor,
171
+ block: CreateInlineDatabaseResponse["block"],
172
+ position?: number | { from: number; to: number } | null,
173
+ ) {
174
+ const content = inlineDatabaseBlockContent(block);
175
+ const chain = editor.chain().focus();
176
+ return position != null
177
+ ? chain.insertContentAt(position, content).run()
178
+ : chain.insertContent(content).run();
179
+ }
180
+
149
181
  const commands: CommandTemplate[] = [
150
182
  {
151
183
  titleKey: "editor.slash.text",
@@ -407,7 +439,9 @@ export function SlashCommandMenu({
407
439
  const { send } = useSendToAgentChat();
408
440
  const navigate = useNavigate();
409
441
  const createPage = useCreatePage({ navigate: false, awaitPersist: true });
410
- const createDatabase = useCreateContentDatabase(documentId ?? null);
442
+ const createInlineDatabase = useCreateInlineContentDatabase(
443
+ documentId ?? null,
444
+ );
411
445
 
412
446
  const [isOpen, setIsOpen] = useState(false);
413
447
  const [isTurnInto, setIsTurnInto] = useState(false);
@@ -609,18 +643,36 @@ export function SlashCommandMenu({
609
643
  title: t("editor.slash.database"),
610
644
  description: t("editor.slash.databaseDescription"),
611
645
  icon: IconDatabase,
612
- action: async () => {
646
+ preserveSlashRange: true,
647
+ action: async (editor, { slashRange }) => {
613
648
  if (!documentId) {
614
649
  toast.error(t("editor.noDocumentSelected"));
615
650
  return;
616
651
  }
652
+ if (slashRange) {
653
+ editor.chain().focus().deleteRange(slashRange).run();
654
+ }
617
655
  const toastId = toast.loading(t("editor.creatingDatabase"));
618
656
  try {
619
- const result = await createDatabase.mutateAsync({
620
- parentId: documentId,
657
+ const result = await createInlineDatabase.mutateAsync({
658
+ hostDocumentId: documentId,
621
659
  title: t("editor.untitledDatabase"),
622
660
  });
623
- navigate(`/page/${result.database.documentId}`, { flushSync: true });
661
+ const inserted = insertInlineDatabaseBlock(editor, result.block);
662
+ if (!inserted) throw new Error(t("empty.genericError"));
663
+ await waitForEditorUpdateFrame();
664
+ const content = collapseExactRepeatedNfm(
665
+ docToNfm(editor.getJSON() as any),
666
+ {
667
+ requiredText: result.block.ownerBlockId,
668
+ },
669
+ );
670
+ if (onDraftPersisted) {
671
+ const persisted = await onDraftPersisted(content);
672
+ if (!persisted) throw new Error(t("empty.genericError"));
673
+ } else {
674
+ await onDraftCommitted?.();
675
+ }
624
676
  toast.success(t("editor.databaseCreated"), { id: toastId });
625
677
  } catch (error) {
626
678
  toast.error(t("editor.failedToCreateDatabase"), {