@agent-native/core 0.80.5 → 0.80.7

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 (76) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +13 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/create.ts +1 -0
  5. package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +45 -0
  6. package/corpus/core/src/sharing/registry.ts +17 -0
  7. package/corpus/core/src/templates/default/pnpm-workspace.yaml +1 -0
  8. package/corpus/core/src/templates/workspace-root/pnpm-workspace.yaml +1 -0
  9. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +42 -4
  10. package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +2 -1
  11. package/corpus/templates/analytics/actions/update-dashboard.ts +23 -9
  12. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +72 -1
  13. package/corpus/templates/analytics/app/i18n/zh-TW.ts +2 -0
  14. package/corpus/templates/analytics/app/i18n-data.ts +34 -0
  15. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +88 -2
  16. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +7 -1
  17. package/corpus/templates/analytics/changelog/2026-06-29-dashboards-can-now-include-extension-panels-that-embed-a-san.md +6 -0
  18. package/corpus/templates/content/AGENTS.md +45 -44
  19. package/corpus/templates/content/README.md +16 -0
  20. package/corpus/templates/content/actions/_builder-cms-read-client.ts +125 -0
  21. package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +28 -6
  22. package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +238 -69
  23. package/corpus/templates/content/actions/_builder-cms-write-settings.ts +125 -43
  24. package/corpus/templates/content/actions/_database-source-utils.ts +746 -97
  25. package/corpus/templates/content/actions/_database-utils.ts +17 -14
  26. package/corpus/templates/content/actions/_federation-join.ts +14 -2
  27. package/corpus/templates/content/actions/_join-suggestion.ts +89 -14
  28. package/corpus/templates/content/actions/_local-file-documents.ts +2 -1
  29. package/corpus/templates/content/actions/attach-content-database-source.ts +124 -5
  30. package/corpus/templates/content/actions/bind-content-database-source-field.ts +256 -0
  31. package/corpus/templates/content/actions/change-content-database-source-role.ts +420 -0
  32. package/corpus/templates/content/actions/create-document.ts +8 -0
  33. package/corpus/templates/content/actions/execute-builder-source-batch.ts +282 -0
  34. package/corpus/templates/content/actions/execute-builder-source-execution.ts +220 -8
  35. package/corpus/templates/content/actions/get-content-database.ts +10 -2
  36. package/corpus/templates/content/actions/prepare-builder-source-execution.ts +19 -2
  37. package/corpus/templates/content/actions/prepare-builder-source-review.ts +47 -15
  38. package/corpus/templates/content/actions/refresh-content-database-source.ts +11 -4
  39. package/corpus/templates/content/actions/review-content-database-source-change-set.ts +6 -2
  40. package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +32 -12
  41. package/corpus/templates/content/actions/stage-builder-revision.ts +14 -10
  42. package/corpus/templates/content/actions/validate-builder-source-execution.ts +25 -3
  43. package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +1520 -553
  44. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +150 -1
  45. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +647 -66
  46. package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +543 -204
  47. package/corpus/templates/content/app/hooks/use-content-database.ts +62 -0
  48. package/corpus/templates/content/app/i18n/zh-TW.ts +64 -0
  49. package/corpus/templates/content/app/i18n-data.ts +784 -0
  50. package/corpus/templates/content/changelog/2026-06-29-builder-review-checks-now-preserve-publish-and-unpublish-cho.md +6 -0
  51. package/corpus/templates/content/changelog/2026-06-29-database-sources-can-now-be-added-as-more-rows-or-matched.md +6 -0
  52. package/corpus/templates/content/changelog/2026-06-29-fixed-loading-the-final-rows-in-larger-databases-without-bre.md +6 -0
  53. package/corpus/templates/content/shared/api.ts +91 -1
  54. package/corpus/templates/content/vite.config.ts +14 -7
  55. package/dist/cli/create.d.ts.map +1 -1
  56. package/dist/cli/create.js +1 -0
  57. package/dist/cli/create.js.map +1 -1
  58. package/dist/client/extensions/EmbeddedExtension.d.ts +10 -1
  59. package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
  60. package/dist/client/extensions/EmbeddedExtension.js +34 -3
  61. package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
  62. package/dist/collab/routes.d.ts +2 -2
  63. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  64. package/dist/notifications/routes.d.ts +1 -1
  65. package/dist/observability/routes.d.ts +7 -7
  66. package/dist/resources/handlers.d.ts +2 -2
  67. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  68. package/dist/server/transcribe-voice.d.ts +1 -1
  69. package/dist/sharing/registry.d.ts.map +1 -1
  70. package/dist/sharing/registry.js +11 -0
  71. package/dist/sharing/registry.js.map +1 -1
  72. package/dist/templates/default/pnpm-workspace.yaml +1 -0
  73. package/dist/templates/workspace-root/pnpm-workspace.yaml +1 -0
  74. package/package.json +1 -1
  75. package/src/templates/default/pnpm-workspace.yaml +1 -0
  76. package/src/templates/workspace-root/pnpm-workspace.yaml +1 -0
@@ -121,7 +121,9 @@ export function SqlChartCard({
121
121
  const [expanded, setExpanded] = useState(false);
122
122
  const [exportCsv, setExportCsv] = useState<(() => void) | null>(null);
123
123
  const [shouldLoadData, setShouldLoadData] = useState(
124
- eagerLoad || panel.chartType === "section",
124
+ eagerLoad ||
125
+ panel.chartType === "section" ||
126
+ panel.chartType === "extension",
125
127
  );
126
128
  const cardRef = useRef<HTMLDivElement | null>(null);
127
129
  const chartQueryKey = useMemo(
@@ -159,7 +161,9 @@ export function SqlChartCard({
159
161
  setShouldLoadData(true);
160
162
  return;
161
163
  }
162
- if (panel.chartType === "section") {
164
+ // Sections are layout-only and extensions render their own iframe — neither
165
+ // waits on the intersection observer that gates SQL panels.
166
+ if (panel.chartType === "section" || panel.chartType === "extension") {
163
167
  setShouldLoadData(true);
164
168
  return;
165
169
  }
@@ -286,6 +290,88 @@ export function SqlChartCard({
286
290
  );
287
291
  }
288
292
 
293
+ // Extension panels render their sandboxed iframe full-bleed with no card chrome
294
+ // or title — the extension owns its own UI. Editable dashboards still get a
295
+ // hover overlay for delete/drag. Editing routes through the agent (the manual
296
+ // panel editor has no extension picker), so no inline edit action here.
297
+ if (panel.chartType === "extension") {
298
+ return (
299
+ <div
300
+ ref={setCardNodeRef}
301
+ style={isDragSource ? { zIndex: 50 } : undefined}
302
+ data-dragging={isDragSource ? "true" : undefined}
303
+ className="dashboard-extension-card group relative h-full"
304
+ >
305
+ <SqlChart panel={panel} resolvedSql={resolvedSql} loadData />
306
+ {editable ? (
307
+ <div className="absolute right-1 top-1 flex items-center gap-1 opacity-0 group-hover:opacity-100">
308
+ <DropdownMenu>
309
+ <Tooltip>
310
+ <TooltipTrigger asChild>
311
+ <DropdownMenuTrigger asChild>
312
+ <button
313
+ className="p-1 rounded bg-background/80 text-muted-foreground hover:text-foreground"
314
+ aria-label={t("sqlDashboard.panelOptions")}
315
+ >
316
+ <IconDotsVertical className="h-3.5 w-3.5" />
317
+ </button>
318
+ </DropdownMenuTrigger>
319
+ </TooltipTrigger>
320
+ <TooltipContent>
321
+ {t("sqlDashboard.panelOptions")}
322
+ </TooltipContent>
323
+ </Tooltip>
324
+ <DropdownMenuContent align="end" className="w-40">
325
+ <DropdownMenuItem
326
+ onSelect={(e) => {
327
+ e.preventDefault();
328
+ setConfirmOpen(true);
329
+ }}
330
+ >
331
+ <IconTrash className="h-4 w-4 mr-2" />
332
+ {t("sidebar.delete")}
333
+ </DropdownMenuItem>
334
+ </DropdownMenuContent>
335
+ </DropdownMenu>
336
+ <PanelDragHandle
337
+ panelId={panel.id}
338
+ label={t("sqlDashboard.dragToReorder")}
339
+ className="p-1 rounded bg-background/80 cursor-grab active:cursor-grabbing text-muted-foreground/50 hover:text-muted-foreground"
340
+ iconClassName="h-3.5 w-3.5"
341
+ />
342
+ </div>
343
+ ) : null}
344
+ {editable ? (
345
+ <AlertDialog open={confirmOpen} onOpenChange={setConfirmOpen}>
346
+ <AlertDialogContent>
347
+ <AlertDialogHeader>
348
+ <AlertDialogTitle>
349
+ {t("sqlDashboard.deletePanelTitle")}
350
+ </AlertDialogTitle>
351
+ <AlertDialogDescription>
352
+ {t("sqlDashboard.deletePanelDescription", {
353
+ title: panel.title,
354
+ })}
355
+ </AlertDialogDescription>
356
+ </AlertDialogHeader>
357
+ <AlertDialogFooter>
358
+ <AlertDialogCancel>{t("sidebar.cancel")}</AlertDialogCancel>
359
+ <AlertDialogAction
360
+ onClick={() => {
361
+ setConfirmOpen(false);
362
+ onRemove();
363
+ }}
364
+ >
365
+ {t("sidebar.delete")}
366
+ </AlertDialogAction>
367
+ </AlertDialogFooter>
368
+ </AlertDialogContent>
369
+ </AlertDialog>
370
+ ) : null}
371
+ </div>
372
+ );
373
+ }
374
+
289
375
  // Every non-section panel exposes at least the Full screen view action, so the
290
376
  // options menu always renders — including on read-only / shared dashboards.
291
377
  const showPanelMenu = true;
@@ -15,7 +15,8 @@ export type ChartType =
15
15
  | "pie"
16
16
  | "section"
17
17
  | "heatmap"
18
- | "callout";
18
+ | "callout"
19
+ | "extension";
19
20
 
20
21
  export type FilterType =
21
22
  | "date"
@@ -79,6 +80,11 @@ export interface SqlPanelConfig {
79
80
  sortable?: boolean;
80
81
  columns?: TableColumnConfig[];
81
82
  limit?: number;
83
+ /**
84
+ * Extension panels only (`chartType: "extension"`): id of the extension to
85
+ * render inline as a sandboxed iframe instead of running the SQL pipeline.
86
+ */
87
+ extensionId?: string;
82
88
  }
83
89
 
84
90
  export interface SqlPanel {
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-06-29
4
+ ---
5
+
6
+ Dashboards can now include extension panels that embed a sandboxed extension inline instead of a SQL chart
@@ -87,50 +87,51 @@ cd templates/content && pnpm action <name> [args]
87
87
 
88
88
  ### Document Operations
89
89
 
90
- | Action | Args | Purpose |
91
- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
92
- | `list-documents` | `[--format json]` | List document metadata/tree; no full bodies |
93
- | `export-content-source` | `[--format json]` | Export editable docs as `content/*.mdx` source files |
94
- | `import-content-source` | `--files <json> [--dryRun true\|false]` | Import `.md`/`.mdx` source files into editable docs |
95
- | `list-builder-docs` | `[--model docs-content\|blog-article] [--limit <n>]` | List Builder docs/blog entries available for `.builder.mdx` pull |
96
- | `pull-builder-doc` | `--model <model> --entryId <builder-entry-id> [--dryRun true\|false]` | Pull one Builder entry into Content and return `.builder.mdx` plus `content/builder/.raw` sidecar files |
97
- | `check-builder-doc` | `--files <json> [--path <file.builder.mdx>]` or `--documentId <id>` | Validate Builder MDX round-trip, sidecar hashes, and remote conflict status before push |
98
- | `push-builder-doc` | `--files <json> [--path <file.builder.mdx>] [--dryRun true\|false]` | Guarded Builder autosave PATCH for the safe Builder test model; never publishes |
99
- | `navigate` | `--path <path>` or `--documentId <id>` or `--databaseId <id>` | Open a route, document page, or database page in the UI |
100
- | `search-documents` | `--query <text> [--format json]` | Search by title/content and return snippets |
101
- | `get-document` | `--id <id> [--format json]` | Get a single document with content |
102
- | `pull-document` | `--id <id> [--format markdown\|text]` | Collab-aware "ingest the final" read |
103
- | `create-document` | `--title <text> [--content] [--parentId] [--icon]` | Create a new document |
104
- | `edit-document` | `--id <id> --find <text> --replace <text>` | Surgical text edit (preferred for modifications) |
105
- | `edit-document` | `--id <id> --edits <json>` | Batch surgical text edits |
106
- | `update-document` | `--id <id> [--title] [--content] [--icon]` | Full rewrite of document fields |
107
- | `share-local-file-document` | `--id <local-file-document-id>` | Create or refresh a DB-backed shareable copy of a local file document |
108
- | `list-local-component-files` | | List registered local MDX component source files |
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
- | `create-content-database` | `[--documentId <id>] [--parentId <id>] [--title <text>]` | Create a database page or convert an existing page into a database |
111
- | `get-content-database` | `--databaseId <id>` or `--documentId <id>` | Get a database table with property schema and item pages |
112
- | `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
- | `attach-content-database-source` | `--databaseId <id>` or `--documentId <id> [--sourceType mock\|builder-cms] [--sourceName] [--sourceTable]` | Attach a source binding; Builder CMS live writes are disabled by default |
114
- | `refresh-content-database-source` | `--databaseId <id>` or `--documentId <id>` | Refresh the read-only source status envelope; Builder CMS reads live entries only when configured |
115
- | `set-content-database-source-write-mode` | `--databaseId <id>` or `--documentId <id> --liveWritesEnabled true\|false [--allowedWriteModes <json>]` | Enable/disable per-source Builder live writes; enabling is allowed only for `agent-native-blog-article-test` with explicit modes |
116
- | `stage-builder-revision` | `--databaseId <id>` or `--documentId <id>` | Stage pending local Builder CMS changes as a local-only save-revision record; never calls Builder |
117
- | `review-content-database-source-change-set` | `--databaseId <id>` or `--documentId <id> --changeSetId <id> --decision approve\|reject [--note]` | Approve or reject a local source change-set review record without provider writes |
118
- | `prepare-builder-source-execution` | `--databaseId <id>` or `--documentId <id> --changeSetId <id> [--pushModeConfirmation autosave\|draft\|publish]` | Prepare a dry-run Builder execution gate with request semantics/idempotency key; never calls Builder |
119
- | `validate-builder-source-execution` | `--databaseId <id>` or `--documentId <id> --changeSetId <id> [--idempotencyKey <key>]` | Validate/replay a prepared Builder execution gate locally as a dry run; never calls Builder |
120
- | `execute-builder-source-execution` | `--databaseId <id>` or `--documentId <id> --changeSetId <id> [--idempotencyKey <key>] [--pushModeConfirmation autosave\|draft\|publish]` | Execute a guarded live Builder write only when approved, validated, enabled, idempotent, and targeting `agent-native-blog-article-test` |
121
- | `add-database-item` | `--databaseId <id> [--title <text>] [--propertyValues <json>]` | Add a page row to a database, optionally seeding property values |
122
- | `duplicate-database-item` | `--itemId <id>` or `--documentId <id> [--title <text>]` | Duplicate a database row page and its stored property values |
123
- | `move-database-item` | `--itemId <id>` or `--documentId <id> --position <number>` | Move a database row page to a new zero-based table position |
124
- | `update-content-database-view` | `--databaseId <id> --viewConfig <json>` | Persist database views, sorts, filters, hidden properties, and view settings |
125
- | `list-document-properties` | `--documentId <id> [--format json]` | List Notion-style property definitions and values for a document |
126
- | `configure-document-property` | `--documentId <id> [--id <propertyId>] --name <name> --type <type> [--visibility always_show\|hide_when_empty\|always_hide]` | Create or update a property definition |
127
- | `duplicate-document-property` | `--documentId <id> --propertyId <propertyId>` | Duplicate a property definition and its stored values |
128
- | `delete-document-property` | `--documentId <id> --propertyId <propertyId>` | Delete a property definition and its stored values |
129
- | `set-document-property` | `--documentId <id> --propertyId <propertyId> --value <json>` | Set a document property value (for a `blocks` field, the value is its markdown content) |
130
- | `reorder-document-property` | `--documentId <id> --propertyId <propertyId> --targetPropertyId <id> [--position before\|after]` | Reorder a property definition within its database (used to reorder Blocks fields on the page) |
131
- | `set-document-discoverability` | `--id <id> --hideFromSearch true\|false [--includeChildren true\|false]` | Hide/show an org-accessible document in Organization/search while keeping link access |
132
- | `move-document` | `--id <id> [--parentId] [--position]` | Move or reorder a document in the page tree |
133
- | `delete-document` | `--id <id>` | Delete with recursive children |
90
+ | Action | Args | Purpose |
91
+ | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
92
+ | `list-documents` | `[--format json]` | List document metadata/tree; no full bodies |
93
+ | `export-content-source` | `[--format json]` | Export editable docs as `content/*.mdx` source files |
94
+ | `import-content-source` | `--files <json> [--dryRun true\|false]` | Import `.md`/`.mdx` source files into editable docs |
95
+ | `list-builder-docs` | `[--model docs-content\|blog-article] [--limit <n>]` | List Builder docs/blog entries available for `.builder.mdx` pull |
96
+ | `pull-builder-doc` | `--model <model> --entryId <builder-entry-id> [--dryRun true\|false]` | Pull one Builder entry into Content and return `.builder.mdx` plus `content/builder/.raw` sidecar files |
97
+ | `check-builder-doc` | `--files <json> [--path <file.builder.mdx>]` or `--documentId <id>` | Validate Builder MDX round-trip, sidecar hashes, and remote conflict status before push |
98
+ | `push-builder-doc` | `--files <json> [--path <file.builder.mdx>] [--dryRun true\|false]` | Guarded Builder autosave PATCH for the safe Builder test model; never publishes |
99
+ | `navigate` | `--path <path>` or `--documentId <id>` or `--databaseId <id>` | Open a route, document page, or database page in the UI |
100
+ | `search-documents` | `--query <text> [--format json]` | Search by title/content and return snippets |
101
+ | `get-document` | `--id <id> [--format json]` | Get a single document with content |
102
+ | `pull-document` | `--id <id> [--format markdown\|text]` | Collab-aware "ingest the final" read |
103
+ | `create-document` | `--title <text> [--content] [--parentId] [--icon]` | Create a new document |
104
+ | `edit-document` | `--id <id> --find <text> --replace <text>` | Surgical text edit (preferred for modifications) |
105
+ | `edit-document` | `--id <id> --edits <json>` | Batch surgical text edits |
106
+ | `update-document` | `--id <id> [--title] [--content] [--icon]` | Full rewrite of document fields |
107
+ | `share-local-file-document` | `--id <local-file-document-id>` | Create or refresh a DB-backed shareable copy of a local file document |
108
+ | `list-local-component-files` | | List registered local MDX component source files |
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
+ | `create-content-database` | `[--documentId <id>] [--parentId <id>] [--title <text>]` | Create a database page or convert an existing page into a database |
111
+ | `get-content-database` | `--databaseId <id>` or `--documentId <id>` | Get a database table with property schema and item pages |
112
+ | `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
+ | `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
+ | `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 |
115
+ | `refresh-content-database-source` | `--databaseId <id>` or `--documentId <id>` | Refresh the read-only source status envelope; Builder CMS reads live entries only when configured |
116
+ | `set-content-database-source-write-mode` | `--databaseId <id>` or `--documentId <id> --liveWritesEnabled true\|false [--allowedWriteModes <json>]` | Enable/disable per-source Builder live writes; enabling is allowed only for `agent-native-blog-article-test` with explicit modes |
117
+ | `stage-builder-revision` | `--databaseId <id>` or `--documentId <id>` | Stage pending local Builder CMS changes as a local-only save-revision record; never calls Builder |
118
+ | `review-content-database-source-change-set` | `--databaseId <id>` or `--documentId <id> --changeSetId <id> --decision approve\|reject [--note]` | Approve or reject a local source change-set review record without provider writes |
119
+ | `prepare-builder-source-execution` | `--databaseId <id>` or `--documentId <id> --changeSetId <id> [--pushModeConfirmation autosave\|draft\|publish]` | Prepare a dry-run Builder execution gate with request semantics/idempotency key; never calls Builder |
120
+ | `validate-builder-source-execution` | `--databaseId <id>` or `--documentId <id> --changeSetId <id> [--idempotencyKey <key>]` | Validate/replay a prepared Builder execution gate locally as a dry run; never calls Builder |
121
+ | `execute-builder-source-execution` | `--databaseId <id>` or `--documentId <id> --changeSetId <id> [--idempotencyKey <key>] [--pushModeConfirmation autosave\|draft\|publish]` | Execute a guarded live Builder write only when approved, validated, enabled, idempotent, and targeting `agent-native-blog-article-test` |
122
+ | `add-database-item` | `--databaseId <id> [--title <text>] [--propertyValues <json>]` | Add a page row to a database, optionally seeding property values |
123
+ | `duplicate-database-item` | `--itemId <id>` or `--documentId <id> [--title <text>]` | Duplicate a database row page and its stored property values |
124
+ | `move-database-item` | `--itemId <id>` or `--documentId <id> --position <number>` | Move a database row page to a new zero-based table position |
125
+ | `update-content-database-view` | `--databaseId <id> --viewConfig <json>` | Persist database views, sorts, filters, hidden properties, and view settings |
126
+ | `list-document-properties` | `--documentId <id> [--format json]` | List Notion-style property definitions and values for a document |
127
+ | `configure-document-property` | `--documentId <id> [--id <propertyId>] --name <name> --type <type> [--visibility always_show\|hide_when_empty\|always_hide]` | Create or update a property definition |
128
+ | `duplicate-document-property` | `--documentId <id> --propertyId <propertyId>` | Duplicate a property definition and its stored values |
129
+ | `delete-document-property` | `--documentId <id> --propertyId <propertyId>` | Delete a property definition and its stored values |
130
+ | `set-document-property` | `--documentId <id> --propertyId <propertyId> --value <json>` | Set a document property value (for a `blocks` field, the value is its markdown content) |
131
+ | `reorder-document-property` | `--documentId <id> --propertyId <propertyId> --targetPropertyId <id> [--position before\|after]` | Reorder a property definition within its database (used to reorder Blocks fields on the page) |
132
+ | `set-document-discoverability` | `--id <id> --hideFromSearch true\|false [--includeChildren true\|false]` | Hide/show an org-accessible document in Organization/search while keeping link access |
133
+ | `move-document` | `--id <id> [--parentId] [--position]` | Move or reorder a document in the page tree |
134
+ | `delete-document` | `--id <id>` | Delete with recursive children |
134
135
 
135
136
  Database views follow Notion-style tab labels. When creating or duplicating
136
137
  views in `viewConfig`, use unique default names (`Table 2`, `SEO copy 2`, etc.)
@@ -28,3 +28,19 @@ Open http://localhost:8080 and create your first page.
28
28
  ## Data
29
29
 
30
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.
@@ -19,6 +19,13 @@ export interface BuilderCmsReadResult {
19
19
  message: string | null;
20
20
  }
21
21
 
22
+ export interface BuilderCmsEntryLiveState {
23
+ exists: boolean;
24
+ published: "published" | "draft" | string | null;
25
+ lastUpdated: number | string | null;
26
+ id: string | null;
27
+ }
28
+
22
29
  type FetchLike = typeof fetch;
23
30
 
24
31
  type BuilderMcpContentPart = {
@@ -52,6 +59,80 @@ function entryArrayFromResponse(value: unknown) {
52
59
  return Array.isArray(record.results) ? record.results : [];
53
60
  }
54
61
 
62
+ function stringFromUnknown(value: unknown) {
63
+ return typeof value === "string" && value.trim() ? value.trim() : null;
64
+ }
65
+
66
+ function stringOrNumberFromUnknown(value: unknown) {
67
+ if (typeof value === "number" && Number.isFinite(value)) return value;
68
+ return stringFromUnknown(value);
69
+ }
70
+
71
+ function liveStateFromBuilderEntry(value: unknown): BuilderCmsEntryLiveState {
72
+ if (Array.isArray(value)) {
73
+ return value.length > 0
74
+ ? liveStateFromBuilderEntry(value[0])
75
+ : {
76
+ exists: false,
77
+ published: null,
78
+ lastUpdated: null,
79
+ id: null,
80
+ };
81
+ }
82
+ if (!value || typeof value !== "object") {
83
+ return {
84
+ exists: false,
85
+ published: null,
86
+ lastUpdated: null,
87
+ id: null,
88
+ };
89
+ }
90
+
91
+ const record = value as Record<string, unknown>;
92
+ if (Array.isArray(record.results)) {
93
+ return liveStateFromBuilderEntry(record.results);
94
+ }
95
+ if (Object.keys(record).length === 0) {
96
+ return {
97
+ exists: false,
98
+ published: null,
99
+ lastUpdated: null,
100
+ id: null,
101
+ };
102
+ }
103
+
104
+ const data =
105
+ record.data &&
106
+ typeof record.data === "object" &&
107
+ !Array.isArray(record.data)
108
+ ? (record.data as Record<string, unknown>)
109
+ : {};
110
+ const id =
111
+ stringFromUnknown(record.id) ??
112
+ stringFromUnknown(record["@id"]) ??
113
+ stringFromUnknown(record.uuid);
114
+ if (!id) {
115
+ return {
116
+ exists: false,
117
+ published: null,
118
+ lastUpdated: null,
119
+ id: null,
120
+ };
121
+ }
122
+
123
+ return {
124
+ exists: true,
125
+ published:
126
+ stringFromUnknown(record.published) ?? stringFromUnknown(data.published),
127
+ lastUpdated:
128
+ stringOrNumberFromUnknown(record.lastUpdated) ??
129
+ stringOrNumberFromUnknown(record.updatedDate) ??
130
+ stringOrNumberFromUnknown(record.updatedAt) ??
131
+ stringOrNumberFromUnknown(data.updatedAt),
132
+ id,
133
+ };
134
+ }
135
+
55
136
  function readLimit(limit: number | undefined) {
56
137
  if (typeof limit === "number" && Number.isFinite(limit) && limit > 0) {
57
138
  return Math.min(Math.floor(limit), BUILDER_CMS_MAX_READ_LIMIT);
@@ -506,6 +587,50 @@ async function readBuilderCmsContentEntriesViaContentApi(args: {
506
587
  };
507
588
  }
508
589
 
590
+ export async function readBuilderCmsEntryLiveState(args: {
591
+ model: string;
592
+ entryId: string;
593
+ fetchImpl?: FetchLike;
594
+ }): Promise<BuilderCmsEntryLiveState> {
595
+ const publicKey = await resolveBuilderCredential("BUILDER_PUBLIC_KEY");
596
+ if (!publicKey) {
597
+ throw new Error(
598
+ "Builder CMS live entry read skipped because BUILDER_PUBLIC_KEY is not configured.",
599
+ );
600
+ }
601
+
602
+ const url = new URL(
603
+ `/api/v3/content/${encodeURIComponent(args.model)}/${encodeURIComponent(
604
+ args.entryId,
605
+ )}`,
606
+ builderContentApiHost(),
607
+ );
608
+ url.searchParams.set("apiKey", publicKey);
609
+ url.searchParams.set("includeUnpublished", "true");
610
+ url.searchParams.set("cachebust", String(Date.now()));
611
+
612
+ const response = await fetchBuilderContentPage({
613
+ fetchImpl: args.fetchImpl ?? fetch,
614
+ url,
615
+ });
616
+ if (response.status === 404) {
617
+ return {
618
+ exists: false,
619
+ published: null,
620
+ lastUpdated: null,
621
+ id: null,
622
+ };
623
+ }
624
+ if (!response.ok) {
625
+ throw new Error(
626
+ `Builder CMS live entry read failed with HTTP ${response.status}.`,
627
+ );
628
+ }
629
+
630
+ const json = (await response.json()) as unknown;
631
+ return liveStateFromBuilderEntry(json);
632
+ }
633
+
509
634
  export async function listBuilderCmsModels(
510
635
  args: {
511
636
  fetchImpl?: FetchLike;
@@ -164,11 +164,13 @@ export function builderCmsSourceMetadata(sourceTable: string) {
164
164
  primaryKey: "id",
165
165
  titleField: "data.title",
166
166
  naturalKeyField: "/blog/[slug]",
167
- pushMode: "autosave" as const,
168
- pushModeLabel: "Save revision / autosave",
167
+ pushMode: "none" as const,
168
+ pushModeLabel: "No writes",
169
169
  pushModeDescription:
170
- "Local-only Builder revision staging. No Builder API write runs in this slice.",
171
- allowedWriteModes: ["autosave"],
170
+ "Read-only Builder source. Choose a write tier before any Builder API write can run.",
171
+ writeMode: "read_only" as const,
172
+ allowPublicationTransitions: false,
173
+ allowedWriteModes: [],
172
174
  allowDraftWrites: false,
173
175
  allowPublishWrites: false,
174
176
  notes:
@@ -196,6 +198,22 @@ function stringFromRecord(
196
198
  return null;
197
199
  }
198
200
 
201
+ function timestampStringFromRecord(
202
+ value: Record<string, unknown>,
203
+ keys: string[],
204
+ ): string | null {
205
+ for (const key of keys) {
206
+ const candidate = value[key];
207
+ if (typeof candidate === "number" && Number.isFinite(candidate)) {
208
+ return String(candidate);
209
+ }
210
+ if (typeof candidate === "string" && candidate.trim()) {
211
+ return candidate.trim();
212
+ }
213
+ }
214
+ return null;
215
+ }
216
+
199
217
  function pickStringField(
200
218
  obj: Record<string, unknown>,
201
219
  keys: string[],
@@ -317,8 +335,12 @@ export function normalizeBuilderCmsApiEntry(
317
335
  stringFromRecord(data, ["url", "urlPath", "path"]) ??
318
336
  (slug ? `/blog/${slug.replace(/^\/+/, "")}` : `/blog/${id}`);
319
337
  const updatedAt =
320
- stringFromRecord(record, ["lastUpdated", "updatedDate", "updatedAt"]) ??
321
- stringFromRecord(data, ["updatedAt"]) ??
338
+ timestampStringFromRecord(record, [
339
+ "lastUpdated",
340
+ "updatedDate",
341
+ "updatedAt",
342
+ ]) ??
343
+ timestampStringFromRecord(data, ["updatedAt"]) ??
322
344
  new Date().toISOString();
323
345
 
324
346
  return {