@agent-native/core 0.80.5 → 0.80.6

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 (58) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +7 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/create.ts +1 -0
  5. package/corpus/core/src/sharing/registry.ts +17 -0
  6. package/corpus/core/src/templates/default/pnpm-workspace.yaml +1 -0
  7. package/corpus/core/src/templates/workspace-root/pnpm-workspace.yaml +1 -0
  8. package/corpus/templates/content/AGENTS.md +45 -44
  9. package/corpus/templates/content/README.md +16 -0
  10. package/corpus/templates/content/actions/_builder-cms-read-client.ts +125 -0
  11. package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +28 -6
  12. package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +238 -69
  13. package/corpus/templates/content/actions/_builder-cms-write-settings.ts +125 -43
  14. package/corpus/templates/content/actions/_database-source-utils.ts +746 -97
  15. package/corpus/templates/content/actions/_database-utils.ts +17 -14
  16. package/corpus/templates/content/actions/_federation-join.ts +14 -2
  17. package/corpus/templates/content/actions/_join-suggestion.ts +89 -14
  18. package/corpus/templates/content/actions/_local-file-documents.ts +2 -1
  19. package/corpus/templates/content/actions/attach-content-database-source.ts +124 -5
  20. package/corpus/templates/content/actions/bind-content-database-source-field.ts +256 -0
  21. package/corpus/templates/content/actions/change-content-database-source-role.ts +420 -0
  22. package/corpus/templates/content/actions/create-document.ts +8 -0
  23. package/corpus/templates/content/actions/execute-builder-source-batch.ts +282 -0
  24. package/corpus/templates/content/actions/execute-builder-source-execution.ts +220 -8
  25. package/corpus/templates/content/actions/get-content-database.ts +10 -2
  26. package/corpus/templates/content/actions/prepare-builder-source-execution.ts +19 -2
  27. package/corpus/templates/content/actions/prepare-builder-source-review.ts +47 -15
  28. package/corpus/templates/content/actions/refresh-content-database-source.ts +11 -4
  29. package/corpus/templates/content/actions/review-content-database-source-change-set.ts +6 -2
  30. package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +32 -12
  31. package/corpus/templates/content/actions/stage-builder-revision.ts +14 -10
  32. package/corpus/templates/content/actions/validate-builder-source-execution.ts +25 -3
  33. package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +1520 -553
  34. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +150 -1
  35. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +647 -66
  36. package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +543 -204
  37. package/corpus/templates/content/app/hooks/use-content-database.ts +62 -0
  38. package/corpus/templates/content/app/i18n/zh-TW.ts +64 -0
  39. package/corpus/templates/content/app/i18n-data.ts +784 -0
  40. package/corpus/templates/content/changelog/2026-06-29-builder-review-checks-now-preserve-publish-and-unpublish-cho.md +6 -0
  41. package/corpus/templates/content/changelog/2026-06-29-database-sources-can-now-be-added-as-more-rows-or-matched.md +6 -0
  42. package/corpus/templates/content/changelog/2026-06-29-fixed-loading-the-final-rows-in-larger-databases-without-bre.md +6 -0
  43. package/corpus/templates/content/shared/api.ts +91 -1
  44. package/corpus/templates/content/vite.config.ts +14 -7
  45. package/dist/cli/create.d.ts.map +1 -1
  46. package/dist/cli/create.js +1 -0
  47. package/dist/cli/create.js.map +1 -1
  48. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  49. package/dist/notifications/routes.d.ts +1 -1
  50. package/dist/observability/routes.d.ts +5 -5
  51. package/dist/sharing/registry.d.ts.map +1 -1
  52. package/dist/sharing/registry.js +11 -0
  53. package/dist/sharing/registry.js.map +1 -1
  54. package/dist/templates/default/pnpm-workspace.yaml +1 -0
  55. package/dist/templates/workspace-root/pnpm-workspace.yaml +1 -0
  56. package/package.json +1 -1
  57. package/src/templates/default/pnpm-workspace.yaml +1 -0
  58. package/src/templates/workspace-root/pnpm-workspace.yaml +1 -0
package/corpus/README.md CHANGED
@@ -28,4 +28,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
28
28
  ## Generated Counts
29
29
 
30
30
  - core files: 2041
31
- - template files: 4581
31
+ - template files: 4587
@@ -1,5 +1,12 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.80.6
4
+
5
+ ### Patch Changes
6
+
7
+ - f52eeb1: Pin the scaffolded workspace Nitro tracer dependency so fresh installs keep building reliably.
8
+ - f52eeb1: Ignore shareable resource registrations that originate from test modules during app runtime.
9
+
3
10
  ## 0.80.5
4
11
 
5
12
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.80.5",
3
+ "version": "0.80.6",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -1054,6 +1054,7 @@ function postProcessStandalone(
1054
1054
  sections.overrides = {
1055
1055
  '"@assistant-ui/store"': '">=0.2.9 <0.2.14"',
1056
1056
  '"@assistant-ui/tap"': '"^0.5.14"',
1057
+ nf3: '"0.3.17"',
1057
1058
  };
1058
1059
  }
1059
1060
  let updated = mergeWorkspaceYamlSections(existing, sections);
@@ -101,6 +101,22 @@ const REGISTRY_KEY = "__agentNativeShareableResources__";
101
101
  type RegistryStore = Map<string, ShareableResourceRegistration>;
102
102
  const globalRegistry: { [K in typeof REGISTRY_KEY]?: RegistryStore } =
103
103
  globalThis as any;
104
+
105
+ function isTestRuntime(): boolean {
106
+ return (
107
+ process.env.NODE_ENV === "test" ||
108
+ process.env.VITEST === "true" ||
109
+ process.env.VITEST === "1"
110
+ );
111
+ }
112
+
113
+ function registrationCameFromTestFile(): boolean {
114
+ const stack = new Error().stack ?? "";
115
+ return /[./\\](?:[^/\\]+[.-])(?:test|spec)\.[cm]?[jt]sx?(?::\d+)?(?::\d+)?/.test(
116
+ stack,
117
+ );
118
+ }
119
+
104
120
  function getRegistry(): RegistryStore {
105
121
  let r = globalRegistry[REGISTRY_KEY];
106
122
  if (!r) {
@@ -113,6 +129,7 @@ function getRegistry(): RegistryStore {
113
129
  export function registerShareableResource(
114
130
  entry: ShareableResourceRegistration,
115
131
  ): void {
132
+ if (!isTestRuntime() && registrationCameFromTestFile()) return;
116
133
  getRegistry().set(entry.type, entry);
117
134
  }
118
135
 
@@ -1,6 +1,7 @@
1
1
  overrides:
2
2
  "@assistant-ui/store": ">=0.2.9 <0.2.14"
3
3
  "@assistant-ui/tap": "^0.5.14"
4
+ nf3: "0.3.17"
4
5
 
5
6
  allowBuilds:
6
7
  better-sqlite3: true
@@ -6,6 +6,7 @@ overrides:
6
6
  "@assistant-ui/store": ">=0.2.9 <0.2.14"
7
7
  "@assistant-ui/tap": "^0.5.14"
8
8
  better-auth: "1.6.0"
9
+ nf3: "0.3.17"
9
10
 
10
11
  onlyBuiltDependencies:
11
12
  - "@swc/core"
@@ -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 {