@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.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +13 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/create.ts +1 -0
- package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +45 -0
- package/corpus/core/src/sharing/registry.ts +17 -0
- package/corpus/core/src/templates/default/pnpm-workspace.yaml +1 -0
- package/corpus/core/src/templates/workspace-root/pnpm-workspace.yaml +1 -0
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +42 -4
- package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +2 -1
- package/corpus/templates/analytics/actions/update-dashboard.ts +23 -9
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +72 -1
- package/corpus/templates/analytics/app/i18n/zh-TW.ts +2 -0
- package/corpus/templates/analytics/app/i18n-data.ts +34 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +88 -2
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +7 -1
- package/corpus/templates/analytics/changelog/2026-06-29-dashboards-can-now-include-extension-panels-that-embed-a-san.md +6 -0
- package/corpus/templates/content/AGENTS.md +45 -44
- package/corpus/templates/content/README.md +16 -0
- package/corpus/templates/content/actions/_builder-cms-read-client.ts +125 -0
- package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +28 -6
- package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +238 -69
- package/corpus/templates/content/actions/_builder-cms-write-settings.ts +125 -43
- package/corpus/templates/content/actions/_database-source-utils.ts +746 -97
- package/corpus/templates/content/actions/_database-utils.ts +17 -14
- package/corpus/templates/content/actions/_federation-join.ts +14 -2
- package/corpus/templates/content/actions/_join-suggestion.ts +89 -14
- package/corpus/templates/content/actions/_local-file-documents.ts +2 -1
- package/corpus/templates/content/actions/attach-content-database-source.ts +124 -5
- package/corpus/templates/content/actions/bind-content-database-source-field.ts +256 -0
- package/corpus/templates/content/actions/change-content-database-source-role.ts +420 -0
- package/corpus/templates/content/actions/create-document.ts +8 -0
- package/corpus/templates/content/actions/execute-builder-source-batch.ts +282 -0
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +220 -8
- package/corpus/templates/content/actions/get-content-database.ts +10 -2
- package/corpus/templates/content/actions/prepare-builder-source-execution.ts +19 -2
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +47 -15
- package/corpus/templates/content/actions/refresh-content-database-source.ts +11 -4
- package/corpus/templates/content/actions/review-content-database-source-change-set.ts +6 -2
- package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +32 -12
- package/corpus/templates/content/actions/stage-builder-revision.ts +14 -10
- package/corpus/templates/content/actions/validate-builder-source-execution.ts +25 -3
- package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +1520 -553
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +150 -1
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +647 -66
- package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +543 -204
- package/corpus/templates/content/app/hooks/use-content-database.ts +62 -0
- package/corpus/templates/content/app/i18n/zh-TW.ts +64 -0
- package/corpus/templates/content/app/i18n-data.ts +784 -0
- package/corpus/templates/content/changelog/2026-06-29-builder-review-checks-now-preserve-publish-and-unpublish-cho.md +6 -0
- package/corpus/templates/content/changelog/2026-06-29-database-sources-can-now-be-added-as-more-rows-or-matched.md +6 -0
- package/corpus/templates/content/changelog/2026-06-29-fixed-loading-the-final-rows-in-larger-databases-without-bre.md +6 -0
- package/corpus/templates/content/shared/api.ts +91 -1
- package/corpus/templates/content/vite.config.ts +14 -7
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/create.js +1 -0
- package/dist/cli/create.js.map +1 -1
- package/dist/client/extensions/EmbeddedExtension.d.ts +10 -1
- package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
- package/dist/client/extensions/EmbeddedExtension.js +34 -3
- package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
- package/dist/collab/routes.d.ts +2 -2
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +7 -7
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/sharing/registry.d.ts.map +1 -1
- package/dist/sharing/registry.js +11 -0
- package/dist/sharing/registry.js.map +1 -1
- package/dist/templates/default/pnpm-workspace.yaml +1 -0
- package/dist/templates/workspace-root/pnpm-workspace.yaml +1 -0
- package/package.json +1 -1
- package/src/templates/default/pnpm-workspace.yaml +1 -0
- 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 ||
|
|
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
|
-
|
|
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 {
|
|
@@ -87,50 +87,51 @@ cd templates/content && pnpm action <name> [args]
|
|
|
87
87
|
|
|
88
88
|
### Document Operations
|
|
89
89
|
|
|
90
|
-
| Action | Args
|
|
91
|
-
| ------------------------------------------- |
|
|
92
|
-
| `list-documents` | `[--format json]`
|
|
93
|
-
| `export-content-source` | `[--format json]`
|
|
94
|
-
| `import-content-source` | `--files <json> [--dryRun true\|false]`
|
|
95
|
-
| `list-builder-docs` | `[--model docs-content\|blog-article] [--limit <n>]`
|
|
96
|
-
| `pull-builder-doc` | `--model <model> --entryId <builder-entry-id> [--dryRun true\|false]`
|
|
97
|
-
| `check-builder-doc` | `--files <json> [--path <file.builder.mdx>]` or `--documentId <id>`
|
|
98
|
-
| `push-builder-doc` | `--files <json> [--path <file.builder.mdx>] [--dryRun true\|false]`
|
|
99
|
-
| `navigate` | `--path <path>` or `--documentId <id>` or `--databaseId <id>`
|
|
100
|
-
| `search-documents` | `--query <text> [--format json]`
|
|
101
|
-
| `get-document` | `--id <id> [--format json]`
|
|
102
|
-
| `pull-document` | `--id <id> [--format markdown\|text]`
|
|
103
|
-
| `create-document` | `--title <text> [--content] [--parentId] [--icon]`
|
|
104
|
-
| `edit-document` | `--id <id> --find <text> --replace <text>`
|
|
105
|
-
| `edit-document` | `--id <id> --edits <json>`
|
|
106
|
-
| `update-document` | `--id <id> [--title] [--content] [--icon]`
|
|
107
|
-
| `share-local-file-document` | `--id <local-file-document-id>`
|
|
108
|
-
| `list-local-component-files` |
|
|
109
|
-
| `write-local-component-file` | `--workspaceId <id> --path <relative-component-path> --content <source>`
|
|
110
|
-
| `create-content-database` | `[--documentId <id>] [--parentId <id>] [--title <text>]`
|
|
111
|
-
| `get-content-database` | `--databaseId <id>` or `--documentId <id>`
|
|
112
|
-
| `get-content-database-source` | `--databaseId <id>` or `--documentId <id>`
|
|
113
|
-
| `attach-content-database-source` | `--databaseId <id>` or `--documentId <id> [--sourceType mock\|builder-cms] [--sourceName] [--sourceTable]`
|
|
114
|
-
| `
|
|
115
|
-
| `
|
|
116
|
-
| `
|
|
117
|
-
| `
|
|
118
|
-
| `
|
|
119
|
-
| `
|
|
120
|
-
| `
|
|
121
|
-
| `
|
|
122
|
-
| `
|
|
123
|
-
| `
|
|
124
|
-
| `
|
|
125
|
-
| `
|
|
126
|
-
| `
|
|
127
|
-
| `
|
|
128
|
-
| `
|
|
129
|
-
| `
|
|
130
|
-
| `
|
|
131
|
-
| `
|
|
132
|
-
| `
|
|
133
|
-
| `
|
|
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: "
|
|
168
|
-
pushModeLabel: "
|
|
167
|
+
pushMode: "none" as const,
|
|
168
|
+
pushModeLabel: "No writes",
|
|
169
169
|
pushModeDescription:
|
|
170
|
-
"
|
|
171
|
-
|
|
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
|
-
|
|
321
|
-
|
|
338
|
+
timestampStringFromRecord(record, [
|
|
339
|
+
"lastUpdated",
|
|
340
|
+
"updatedDate",
|
|
341
|
+
"updatedAt",
|
|
342
|
+
]) ??
|
|
343
|
+
timestampStringFromRecord(data, ["updatedAt"]) ??
|
|
322
344
|
new Date().toISOString();
|
|
323
345
|
|
|
324
346
|
return {
|