@agent-native/core-corpus 0.212.1 → 0.214.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/onboarding-journeys/SKILL.md +29 -14
  3. package/corpus/templates/analytics/CHANGELOG.md +11 -0
  4. package/corpus/templates/analytics/actions/estimate-onboarding-journey-cost.ts +23 -6
  5. package/corpus/templates/analytics/actions/get-onboarding-journey.ts +20 -5
  6. package/corpus/templates/analytics/actions/provider-api-register.ts +2 -7
  7. package/corpus/templates/analytics/changelog/2026-10-21-analytics-recognizes-questions-about-connected-data-product-.md +6 -0
  8. package/corpus/templates/analytics/changelog/2026-10-21-onboarding-journeys-now-honor-larger-event-row-budgets.md +6 -0
  9. package/corpus/templates/analytics/changelog/2026-10-21-preserve-unknown-replay-availability-in-partial-journeys.md +6 -0
  10. package/corpus/templates/analytics/package.json +6 -0
  11. package/corpus/templates/analytics/scripts/journey-capture-plan.ts +18 -7
  12. package/corpus/templates/analytics/scripts/journey-capture.ts +3 -1
  13. package/corpus/templates/analytics/server/lib/db-admin-connections.ts +19 -12
  14. package/corpus/templates/analytics/server/lib/journey-tree.ts +24 -9
  15. package/corpus/templates/analytics/server/lib/onboarding-journey-cost.ts +163 -94
  16. package/corpus/templates/analytics/server/lib/onboarding-journey.ts +162 -74
  17. package/corpus/templates/assets/server/lib/org-admin.ts +20 -16
  18. package/corpus/templates/brain/server/lib/zoom-governance.ts +11 -6
  19. package/corpus/templates/clips/CHANGELOG.md +1 -0
  20. package/corpus/templates/clips/changelog/2026-10-16-rewind-status-says-when-it-remembers-audio.md +6 -0
  21. package/corpus/templates/clips/desktop/src/app.tsx +7 -4
  22. package/corpus/templates/clips/desktop/src/components/RewindCaptureStatus.tsx +25 -0
  23. package/corpus/templates/clips/desktop/src/lib/rewind-status.ts +6 -1
  24. package/corpus/templates/clips/desktop/src/shared/config.ts +3 -0
  25. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory.rs +2 -0
  26. package/corpus/templates/clips/server/lib/recordings.ts +4 -19
  27. package/corpus/templates/community/qr/package.json +1 -1
  28. package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +15 -1
  29. package/corpus/templates/content/CHANGELOG.md +13 -0
  30. package/corpus/templates/content/actions/_content-space-access.ts +95 -6
  31. package/corpus/templates/content/actions/_database-utils.ts +20 -16
  32. package/corpus/templates/content/actions/_database-view-setup.ts +305 -11
  33. package/corpus/templates/content/actions/_github-source-freshness.ts +72 -17
  34. package/corpus/templates/content/actions/_github-source-metadata.ts +63 -17
  35. package/corpus/templates/content/actions/_github-source.ts +141 -77
  36. package/corpus/templates/content/actions/_property-utils.ts +3 -0
  37. package/corpus/templates/content/actions/check-github-connection.ts +15 -0
  38. package/corpus/templates/content/actions/create-github-collection.ts +180 -0
  39. package/corpus/templates/content/actions/disconnect-content-database-source.ts +5 -0
  40. package/corpus/templates/content/actions/link-github-folder.ts +104 -36
  41. package/corpus/templates/content/actions/refresh-content-database-source.ts +37 -4
  42. package/corpus/templates/content/actions/sync-github-folder.ts +23 -5
  43. package/corpus/templates/content/actions/update-content-database-view.ts +18 -2
  44. package/corpus/templates/content/actions/view-screen.ts +12 -0
  45. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +263 -35
  46. package/corpus/templates/content/app/components/editor/database/GitHubLinkDialog.tsx +938 -0
  47. package/corpus/templates/content/app/components/editor/database/GitHubSourcePanel.tsx +228 -0
  48. package/corpus/templates/content/app/components/editor/database/GitHubSourceStatusLine.tsx +561 -0
  49. package/corpus/templates/content/app/components/editor/database/GitHubUnlinkDialog.tsx +96 -0
  50. package/corpus/templates/content/app/components/editor/database/ListItemLayout.tsx +410 -0
  51. package/corpus/templates/content/app/components/editor/database/ListItemRow.tsx +426 -0
  52. package/corpus/templates/content/app/components/editor/database/ListView.tsx +136 -18
  53. package/corpus/templates/content/app/components/editor/database/github-availability.ts +147 -0
  54. package/corpus/templates/content/app/components/editor/database/github-source-text.ts +280 -0
  55. package/corpus/templates/content/app/components/editor/database/navigation-state.ts +3 -0
  56. package/corpus/templates/content/app/components/editor/database/settings.tsx +3 -3
  57. package/corpus/templates/content/app/components/editor/database/view-config.ts +4 -0
  58. package/corpus/templates/content/app/components/layout/Layout.tsx +2 -0
  59. package/corpus/templates/content/app/global.css +13 -0
  60. package/corpus/templates/content/app/hooks/content-action-refresh.ts +6 -0
  61. package/corpus/templates/content/app/hooks/use-content-database.ts +3 -1
  62. package/corpus/templates/content/app/hooks/use-github-folder.ts +199 -0
  63. package/corpus/templates/content/app/i18n-data.ts +186 -1
  64. package/corpus/templates/content/app/lib/github-link-dialog.ts +86 -0
  65. package/corpus/templates/content/changelog/2026-10-18-connected-ai-assistants-can-link-a-new-collection-to-a-github.md +1 -1
  66. package/corpus/templates/content/changelog/2026-10-18-link-a-new-collection-to-a-github-folder-from-its-sources.md +6 -0
  67. package/corpus/templates/content/changelog/2026-10-19-list-rows-can-be-customized.md +6 -0
  68. package/corpus/templates/content/changelog/2026-10-19-list-views-fit-phone-screens.md +6 -0
  69. package/corpus/templates/content/changelog/2026-10-21-organization-favorites-include-documents-shared-with-the-cur.md +6 -0
  70. package/corpus/templates/content/docs/product/capabilities/content.source.sync-policy.md +32 -7
  71. package/corpus/templates/content/docs/product/encyclopedia.md +3 -3
  72. package/corpus/templates/content/docs/product/features/content.feature.trust-your-connected-sources.md +2 -2
  73. package/corpus/templates/content/docs/product/roadmap.md +1 -1
  74. package/corpus/templates/content/parity/matrix.md +42 -42
  75. package/corpus/templates/content/parity/matrix.ts +14 -0
  76. package/corpus/templates/content/server/lib/github-source-client.ts +50 -1
  77. package/corpus/templates/content/shared/api.ts +21 -0
  78. package/corpus/templates/content/shared/database-item-layout.ts +591 -0
  79. package/corpus/templates/content/shared/github-source.ts +68 -0
  80. package/corpus/templates/design/CHANGELOG.md +1 -0
  81. package/corpus/templates/design/actions/connect-assets-mcp.ts +5 -9
  82. package/corpus/templates/design/changelog/2026-10-21-journey-canvases-accept-partial-analytics-samples-when-repla.md +6 -0
  83. package/corpus/templates/design/shared/journey-canvas.ts +6 -4
  84. package/corpus/templates/dispatch/.agents/skills/a2a-protocol/SKILL.md +16 -13
  85. package/corpus/templates/dispatch/CHANGELOG.md +4 -0
  86. package/corpus/templates/dispatch/changelog/2026-10-21-customer-ai-credit-questions-route-to-account-health-instead.md +6 -0
  87. package/corpus/templates/factory/server/lib/require-workspace-member.ts +5 -17
  88. package/corpus/templates/mail/server/lib/queued-drafts.ts +9 -4
  89. package/corpus/templates/slides/server/workspace-defaults.ts +12 -15
  90. package/package.json +1 -1
@@ -31,6 +31,7 @@ import {
31
31
  GITHUB_SOURCE_MAX_IMAGE_BYTES,
32
32
  GITHUB_SOURCE_TYPE,
33
33
  githubBranchCandidates,
34
+ githubFieldExamples,
34
35
  githubFieldPropertyInput,
35
36
  githubFolderLabel,
36
37
  githubFolderWebUrl,
@@ -41,6 +42,7 @@ import {
41
42
  parseGitHubFolderUrl,
42
43
  type GitHubFieldType,
43
44
  type GitHubFolderLocation,
45
+ type GitHubFolderPreview,
44
46
  type GitHubSourceProblemCode,
45
47
  type GitHubSourceSkipReason,
46
48
  } from "../shared/github-source.js";
@@ -78,6 +80,7 @@ import {
78
80
  githubSourceMetadataRecord,
79
81
  readGitHubSourceState,
80
82
  rememberedSkips,
83
+ type GitHubReadSkip,
81
84
  type GitHubSourceState,
82
85
  } from "./_github-source-metadata.js";
83
86
  import {
@@ -201,6 +204,7 @@ const PROBLEM_STATUS: Record<GitHubSourceProblemCode, number> = {
201
204
  rate_limited: 429,
202
205
  too_large: 413,
203
206
  unavailable: 502,
207
+ sync_failed: 500,
204
208
  storage_unavailable: 503,
205
209
  };
206
210
 
@@ -337,37 +341,6 @@ function frontmatterValues(
337
341
  return draft.frontmatter.unmapped ?? {};
338
342
  }
339
343
 
340
- export interface GitHubFolderPreview {
341
- location: GitHubFolderLocation;
342
- label: string;
343
- folderUrl: string;
344
- commitSha: string;
345
- markdownFiles: number;
346
- skipped: Array<{ path: string; reason: GitHubSourceSkipReason }>;
347
- /** Columns the frontmatter would become, with the type each would get. */
348
- fields: Array<{ key: string; type: GitHubFieldType; examples: string[] }>;
349
- sample: Array<{ path: string; title: string }>;
350
- /** How many files the field types were read from. */
351
- sampledFiles: number;
352
- /** Image files in the folder, which pages show from Content's file storage. */
353
- images: number;
354
- /** Whether file storage is set up to hold those images. */
355
- storageReady: boolean;
356
- }
357
-
358
- function exampleText(value: unknown): string {
359
- if (typeof value === "string") return value.slice(0, 80);
360
- if (Array.isArray(value)) {
361
- return value
362
- .map((entry) =>
363
- typeof entry === "string" ? entry : JSON.stringify(entry),
364
- )
365
- .join(", ")
366
- .slice(0, 80);
367
- }
368
- return JSON.stringify(value).slice(0, 80);
369
- }
370
-
371
344
  /**
372
345
  * Reports what linking the folder would create without creating anything:
373
346
  * how many files become rows, which are skipped and why, and the columns the
@@ -439,10 +412,7 @@ export async function previewGitHubFolder(
439
412
  fields: [...valuesByKey].map(([key, values]) => ({
440
413
  key,
441
414
  type: inferGitHubFieldType(key, values),
442
- examples: values
443
- .filter((value) => value !== null && value !== "")
444
- .slice(0, 3)
445
- .map(exampleText),
415
+ examples: githubFieldExamples(values),
446
416
  })),
447
417
  sample,
448
418
  sampledFiles: sample.length,
@@ -788,6 +758,26 @@ function emptyResult(
788
758
  };
789
759
  }
790
760
 
761
+ /**
762
+ * The problem a sync records when it fails for a reason it can't name, like a
763
+ * database error. The GitHub client names its own failures, a dropped
764
+ * connection included, so this one isn't GitHub's.
765
+ */
766
+ function unexpectedProblem(
767
+ at: string,
768
+ ): NonNullable<GitHubSourceState["problem"]> {
769
+ return {
770
+ code: "sync_failed",
771
+ message: "The sync failed. The background sync tries again.",
772
+ at,
773
+ };
774
+ }
775
+
776
+ /** A sync that failed before it could record why. */
777
+ export function unexpectedSyncFailure(at: string): GitHubSyncResult {
778
+ return emptyResult("error", null, unexpectedProblem(at));
779
+ }
780
+
791
781
  /** The status problem for images a sync couldn't store, if there are any. */
792
782
  function storageProblem(
793
783
  imagesWaiting: number,
@@ -1643,6 +1633,11 @@ export interface SyncGitHubSourceOptions {
1643
1633
  force?: boolean;
1644
1634
  /** Rethrow a missing GitHub connection so an interactive caller can offer to connect. */
1645
1635
  throwConnectionErrors?: boolean;
1636
+ /**
1637
+ * Rethrow a failure the sync can't name, like a database error, once it's
1638
+ * on the link, for a caller that reports failures as errors.
1639
+ */
1640
+ throwUnexpectedErrors?: boolean;
1646
1641
  /** Ends the sync early, as its time budget would, such as when the server stops. */
1647
1642
  signal?: AbortSignal;
1648
1643
  }
@@ -1704,6 +1699,19 @@ export async function syncGitHubSource(
1704
1699
  () => budget.abort(),
1705
1700
  Math.max(0, deadline - Date.now()),
1706
1701
  );
1702
+ // Counts what this run writes. A run that fails partway still reports the
1703
+ // rows it changed first, so readers hear about them.
1704
+ const result = emptyResult("synced", null);
1705
+ // Files this run read and had to skip. A run that fails partway keeps
1706
+ // them too, so the next one doesn't read them again.
1707
+ let readSkipsSoFar: GitHubReadSkip[] | null = null;
1708
+ const failed = (problem: GitHubSourceState["problem"]): GitHubSyncResult => ({
1709
+ ...emptyResult("error", state.pendingCommitSha, problem),
1710
+ created: result.created,
1711
+ updated: result.updated,
1712
+ renamed: result.renamed,
1713
+ trashed: result.trashed,
1714
+ });
1707
1715
  const endBudget = () => budget.abort();
1708
1716
  if (options.signal?.aborted) endBudget();
1709
1717
  options.signal?.addEventListener("abort", endBudget, { once: true });
@@ -1862,7 +1870,18 @@ export async function syncGitHubSource(
1862
1870
  run.documentIdByPath.set(folderRelative(run, path), row.documentId);
1863
1871
  }
1864
1872
 
1873
+ // A file skipped once it was read is skipped again while its blob is the
1874
+ // same, so it isn't read again. It has no row to record that on.
1875
+ const readSkipsByPath = new Map(
1876
+ state.readSkips.map((skip) => [skip.path, skip] as const),
1877
+ );
1878
+ const stillSkipped: GitHubReadSkip[] = [];
1865
1879
  const work = planned.filter((file) => {
1880
+ const skip = readSkipsByPath.get(file.entry.path);
1881
+ if (skip?.blobSha === file.entry.sha) {
1882
+ stillSkipped.push(skip);
1883
+ return false;
1884
+ }
1866
1885
  const previous = file.row ? rowState(file.row) : null;
1867
1886
  return (
1868
1887
  !previous ||
@@ -1880,9 +1899,10 @@ export async function syncGitHubSource(
1880
1899
  (a, b) =>
1881
1900
  (cutShort.get(a.repoPath) ?? 0) - (cutShort.get(b.repoPath) ?? 0),
1882
1901
  );
1883
- const result = emptyResult("synced", head.sha);
1884
- result.unchanged = planned.length - work.length;
1885
- const skippedFiles = [...files.skipped];
1902
+ result.commitSha = head.sha;
1903
+ result.unchanged = planned.length - work.length - stillSkipped.length;
1904
+ const readSkips: GitHubReadSkip[] = [...stillSkipped];
1905
+ readSkipsSoFar = readSkips;
1886
1906
  let done = 0;
1887
1907
  let cutShortPath: string | null = null;
1888
1908
  for (const file of work) {
@@ -1923,7 +1943,11 @@ export async function syncGitHubSource(
1923
1943
  }
1924
1944
  if (typeof outcome === "string") result[outcome] += 1;
1925
1945
  else
1926
- skippedFiles.push({ path: file.entry.path, reason: outcome.skipped });
1946
+ readSkips.push({
1947
+ path: file.entry.path,
1948
+ reason: outcome.skipped,
1949
+ blobSha: file.entry.sha,
1950
+ });
1927
1951
  done += 1;
1928
1952
  }
1929
1953
  const remainingFiles = work.length - done;
@@ -1957,40 +1981,55 @@ export async function syncGitHubSource(
1957
1981
  // moved it for two minutes. One that got nowhere waits its turn, as a
1958
1982
  // failed check does.
1959
1983
  const countsAsCheck = complete || (done === 0 && result.trashed === 0);
1960
- const skipped = rememberedSkips(
1961
- skippedFiles.sort((a, b) => (a.path < b.path ? -1 : 1)),
1962
- );
1984
+ const skippedFiles = [...files.skipped, ...readSkips];
1985
+ const skipped = rememberedSkips(skippedFiles);
1963
1986
  await release(
1964
- (current) => ({
1965
- ...current,
1966
- checkedAt: now,
1967
- problem: result.problem,
1968
- files: files.markdown.length,
1969
- skipped,
1970
- imagesWaiting: result.imagesWaiting,
1971
- ...(complete
1972
- ? {
1973
- syncedCommitSha: head.sha,
1974
- syncedAt: new Date().toISOString(),
1975
- headEtag: head.etag,
1976
- pendingCommitSha: null,
1977
- progress: null,
1978
- cutShort: [],
1979
- }
1980
- : {
1981
- pendingCommitSha: head.sha,
1982
- cutShort: cutShortPath
1983
- ? [
1984
- ...current.cutShort.filter((path) => path !== cutShortPath),
1985
- cutShortPath,
1986
- ]
1987
- : current.cutShort,
1988
- progress: {
1989
- done: work.length - remainingFiles + result.trashed,
1990
- total: work.length + result.trashed + gone.size,
1991
- },
1992
- }),
1993
- }),
1987
+ (current) => {
1988
+ // A run that carries on a push sees only the files left, so it keeps
1989
+ // the total the push started with and counts what's left against it.
1990
+ // The count never goes back.
1991
+ const before =
1992
+ current.pendingCommitSha === head.sha ? current.progress : null;
1993
+ const total = before?.total ?? work.length + result.trashed + gone.size;
1994
+ const progress = {
1995
+ done: Math.min(
1996
+ total,
1997
+ Math.max(before?.done ?? 0, total - result.remaining),
1998
+ ),
1999
+ total,
2000
+ };
2001
+ return {
2002
+ ...current,
2003
+ checkedAt: now,
2004
+ problem: result.problem,
2005
+ files: files.markdown.length,
2006
+ skipped,
2007
+ skippedCount: skippedFiles.length,
2008
+ readSkips,
2009
+ imagesWaiting: result.imagesWaiting,
2010
+ ...(complete
2011
+ ? {
2012
+ syncedCommitSha: head.sha,
2013
+ syncedAt: new Date().toISOString(),
2014
+ headEtag: head.etag,
2015
+ pendingCommitSha: null,
2016
+ progress: null,
2017
+ cutShort: [],
2018
+ }
2019
+ : {
2020
+ pendingCommitSha: head.sha,
2021
+ cutShort: cutShortPath
2022
+ ? [
2023
+ ...current.cutShort.filter(
2024
+ (path) => path !== cutShortPath,
2025
+ ),
2026
+ cutShortPath,
2027
+ ]
2028
+ : current.cutShort,
2029
+ progress,
2030
+ }),
2031
+ };
2032
+ },
1994
2033
  () => ({
1995
2034
  syncState: complete ? "linked" : "refreshing",
1996
2035
  freshness: complete ? "fresh" : "stale",
@@ -2017,16 +2056,33 @@ export async function syncGitHubSource(
2017
2056
  lastRefreshedAt: now,
2018
2057
  }),
2019
2058
  ).catch(warnLeaseNotReleased);
2020
- return emptyResult("error", state.pendingCommitSha, {
2059
+ return failed({
2021
2060
  code: "unavailable",
2022
2061
  message:
2023
2062
  "GitHub didn't answer in time. The background sync tries again.",
2024
2063
  at: now,
2025
2064
  });
2026
2065
  }
2027
- const problem = problemFrom(error, now);
2066
+ // A contract error, like the link going away mid-sync, is the caller's to
2067
+ // see. Anything else the sync can't name is still a failed check: the
2068
+ // status line shows it and the next sync tries again.
2069
+ const named = problemFrom(error, now);
2070
+ const problem =
2071
+ named ??
2072
+ (error instanceof ActionContractError ? null : unexpectedProblem(now));
2073
+ if (!named && problem) {
2074
+ console.warn(
2075
+ "[github-source] sync failed:",
2076
+ error instanceof Error ? error.message : String(error),
2077
+ );
2078
+ }
2028
2079
  await release(
2029
- (current) => ({ ...current, checkedAt: now, problem }),
2080
+ (current) => ({
2081
+ ...current,
2082
+ checkedAt: now,
2083
+ problem,
2084
+ ...(readSkipsSoFar ? { readSkips: readSkipsSoFar } : {}),
2085
+ }),
2030
2086
  () => ({
2031
2087
  syncState: "error",
2032
2088
  freshness: "stale",
@@ -2038,7 +2094,8 @@ export async function syncGitHubSource(
2038
2094
  if (problem.code === "connection" && options.throwConnectionErrors) {
2039
2095
  throw error;
2040
2096
  }
2041
- return emptyResult("error", state.pendingCommitSha, problem);
2097
+ if (!named && options.throwUnexpectedErrors) throw error;
2098
+ return failed(problem);
2042
2099
  } finally {
2043
2100
  clearTimeout(budgetTimer);
2044
2101
  options.signal?.removeEventListener("abort", endBudget);
@@ -2078,6 +2135,8 @@ export async function createGitHubSourceLink(args: {
2078
2135
  cutShort: [],
2079
2136
  files: preview.markdownFiles,
2080
2137
  skipped: rememberedSkips(preview.skipped),
2138
+ skippedCount: preview.skipped.length,
2139
+ readSkips: [],
2081
2140
  problem: null,
2082
2141
  claim: null,
2083
2142
  };
@@ -2161,6 +2220,11 @@ export function githubPreviewSummary(preview: GitHubFolderPreview) {
2161
2220
  ),
2162
2221
  )}.`
2163
2222
  : " It has no frontmatter columns.";
2223
+ // Columns come from the first files only; later ones can differ.
2224
+ const sampled =
2225
+ preview.sampledFiles < preview.markdownFiles
2226
+ ? ` That frontmatter is read from ${preview.sampledFiles} of its ${preview.markdownFiles} files, so the others can add columns or change their types.`
2227
+ : "";
2164
2228
  const skipped = preview.skipped.length
2165
2229
  ? ` ${preview.skipped.length} skipped: ${listWithMore(
2166
2230
  preview.skipped.map(
@@ -2172,7 +2236,7 @@ export function githubPreviewSummary(preview: GitHubFolderPreview) {
2172
2236
  preview.images > 0 && !preview.storageReady
2173
2237
  ? ` Its ${preview.images === 1 ? "image won't" : `${preview.images} images won't`} show until file storage is set up in Settings → File uploads.`
2174
2238
  : "";
2175
- return `${preview.label} at ${preview.commitSha.slice(0, 7)} has ${preview.markdownFiles} Markdown ${preview.markdownFiles === 1 ? "file" : "files"}.${columns}${skipped}${images}`;
2239
+ return `${preview.label} at ${preview.commitSha.slice(0, 7)} has ${preview.markdownFiles} Markdown ${preview.markdownFiles === 1 ? "file" : "files"}.${columns}${sampled}${skipped}${images}`;
2176
2240
  }
2177
2241
 
2178
2242
  export function githubSyncSummary(label: string, result: GitHubSyncResult) {
@@ -30,6 +30,7 @@ import type {
30
30
  DocumentProperty,
31
31
  } from "../shared/api.js";
32
32
  import { blocksFieldId } from "../shared/blocks-field-identity.js";
33
+ import { contentDatabaseItemLayoutField } from "../shared/database-item-layout.js";
33
34
  import {
34
35
  DEFAULT_BLOCKS_FIELD_NAME,
35
36
  defaultPropertyOptions,
@@ -398,6 +399,7 @@ function defaultDatabaseView(
398
399
  rowDensity: normalizeDatabaseRowDensity(values.rowDensity),
399
400
  openPagesIn: normalizeDatabaseOpenPagesIn(values.openPagesIn),
400
401
  formQuestions: normalizeDatabaseFormQuestions(values.formQuestions),
402
+ ...contentDatabaseItemLayoutField(values.itemLayout),
401
403
  };
402
404
  }
403
405
 
@@ -460,6 +462,7 @@ function normalizeDatabaseView(value: unknown): ContentDatabaseView | null {
460
462
  rowDensity: normalizeDatabaseRowDensity(view.rowDensity),
461
463
  openPagesIn: normalizeDatabaseOpenPagesIn(view.openPagesIn),
462
464
  formQuestions: normalizeDatabaseFormQuestions(view.formQuestions),
465
+ ...contentDatabaseItemLayoutField(view.itemLayout),
463
466
  };
464
467
  }
465
468
 
@@ -0,0 +1,15 @@
1
+ import { defineAction } from "@agent-native/core/action";
2
+ import { z } from "zod";
3
+
4
+ import { readGitHubConnected } from "../server/lib/github-source-client.js";
5
+
6
+ export default defineAction({
7
+ description:
8
+ "Check whether the current user has a GitHub connection Content can read folders through, before linking one. Reads no repository. `connected` is null when GitHub couldn't be asked.",
9
+ schema: z.object({}).strict(),
10
+ http: { method: "GET" },
11
+ readOnly: true,
12
+ run: async (): Promise<{ connected: boolean | null }> => ({
13
+ connected: await readGitHubConnected(),
14
+ }),
15
+ });
@@ -0,0 +1,180 @@
1
+ import { ActionContractError, defineAction } from "@agent-native/core/action";
2
+ import { getRequestUserEmail } from "@agent-native/core/server/request-context";
3
+ import { assertAccess } from "@agent-native/core/sharing";
4
+ import { z } from "zod";
5
+
6
+ import { documentChangeResource } from "../server/lib/document-change-resource.js";
7
+ import { githubFolderName } from "../shared/github-source.js";
8
+ import { resolveContentSpaceAccess } from "./_content-space-access.js";
9
+ import type { DatabaseSetupReceipt } from "./_database-setup-mutation.js";
10
+ import { resolveDatabaseForSourceMutation } from "./_database-source-utils.js";
11
+ import {
12
+ githubActionError,
13
+ githubPreviewSummary,
14
+ previewGitHubFolder,
15
+ readLinkedGitHubSource,
16
+ resolveGitHubFolder,
17
+ } from "./_github-source.js";
18
+ import { nanoid, serializeDatabase } from "./_property-utils.js";
19
+ import createContentDatabase from "./create-content-database.js";
20
+ import deleteContentDatabase from "./delete-content-database.js";
21
+ import {
22
+ bringInGitHubLink,
23
+ linkPreviewedGitHubFolder,
24
+ } from "./link-github-folder.js";
25
+
26
+ const createGitHubCollectionSchema = z
27
+ .object({
28
+ url: z
29
+ .string()
30
+ .min(1)
31
+ .describe(
32
+ "GitHub folder link, like https://github.com/owner/repo/tree/main/docs, or owner/repo for the whole repository",
33
+ ),
34
+ spaceId: z
35
+ .string()
36
+ .min(1)
37
+ .describe("Exact Content space ID for the new collection"),
38
+ parentId: z
39
+ .string()
40
+ .min(1)
41
+ .nullish()
42
+ .describe("Page to put the collection under; leave out for the space"),
43
+ connectionId: z
44
+ .string()
45
+ .min(1)
46
+ .optional()
47
+ .describe(
48
+ "GitHub connection to read through when there is more than one",
49
+ ),
50
+ dryRun: z
51
+ .boolean()
52
+ .optional()
53
+ .describe("Preview only: read the folder without making the collection"),
54
+ idempotencyKey: z
55
+ .string()
56
+ .min(1)
57
+ .max(200)
58
+ .optional()
59
+ .describe(
60
+ "Creation intent key; reuse it unchanged after a lost response",
61
+ ),
62
+ })
63
+ .strict();
64
+
65
+ export default defineAction({
66
+ description:
67
+ "Make a new collection from a GitHub folder, named after the folder and linked to it view only: each Markdown file becomes a row and pushes to the branch arrive on their own. Run with dryRun first to see the files, skipped files and columns without making anything.",
68
+ mcpTool: true,
69
+ // It only makes a new collection, so it never replaces anyone's content.
70
+ mcpAnnotations: {
71
+ readOnlyHint: false,
72
+ destructiveHint: false,
73
+ openWorldHint: true,
74
+ },
75
+ agentInputSchema: createGitHubCollectionSchema,
76
+ schema: createGitHubCollectionSchema,
77
+ run: async (args) => {
78
+ const userEmail = getRequestUserEmail();
79
+ if (!userEmail) {
80
+ throw new ActionContractError("Sign in to link a GitHub folder.", {
81
+ errorCode: "AUTH_REQUIRED",
82
+ statusCode: 401,
83
+ });
84
+ }
85
+ // The same checks the new collection gets, so a preview doesn't promise
86
+ // a collection the person can't make.
87
+ await resolveContentSpaceAccess(args.spaceId, "contributor");
88
+ if (args.parentId) await assertAccess("document", args.parentId, "editor");
89
+
90
+ const connectionId = args.connectionId ?? null;
91
+ let resolved: Awaited<ReturnType<typeof resolveGitHubFolder>>;
92
+ let preview: Awaited<ReturnType<typeof previewGitHubFolder>>;
93
+ try {
94
+ resolved = await resolveGitHubFolder({ url: args.url, connectionId });
95
+ preview = await previewGitHubFolder(resolved, connectionId);
96
+ } catch (error) {
97
+ throw githubActionError(error);
98
+ }
99
+ const title = githubFolderName(preview.location);
100
+
101
+ if (args.dryRun) {
102
+ return {
103
+ dryRun: true as const,
104
+ preview,
105
+ title,
106
+ summary: `${githubPreviewSummary(preview)} Run again without dryRun to make the "${title}" collection and link it.`,
107
+ };
108
+ }
109
+
110
+ const created = await createContentDatabase.run({
111
+ spaceId: args.spaceId,
112
+ parentId: args.parentId ?? null,
113
+ title,
114
+ idempotencyKey: args.idempotencyKey ?? `github-collection-${nanoid()}`,
115
+ });
116
+ const database = await resolveDatabaseForSourceMutation({
117
+ databaseId: created.database.id,
118
+ });
119
+ if (!database) {
120
+ throw new ActionContractError("The new collection can't be found.", {
121
+ errorCode: "DATABASE_NOT_FOUND",
122
+ statusCode: 404,
123
+ });
124
+ }
125
+ // A retry after a lost response gets back the collection the first try
126
+ // made, which may be linked already.
127
+ const { receipt } = created as { receipt?: DatabaseSetupReceipt };
128
+ const madeEarlier = receipt?.idempotency.result === "replayed";
129
+ const earlierLink = madeEarlier
130
+ ? await readLinkedGitHubSource(database.id)
131
+ : null;
132
+ if (earlierLink && earlierLink.sourceName === preview.label) {
133
+ return {
134
+ dryRun: false as const,
135
+ database: serializeDatabase(database),
136
+ preview,
137
+ title,
138
+ ...(await bringInGitHubLink({
139
+ database,
140
+ preview,
141
+ sourceId: earlierLink.id,
142
+ })),
143
+ };
144
+ }
145
+ let linked: Awaited<ReturnType<typeof linkPreviewedGitHubFolder>>;
146
+ try {
147
+ linked = await linkPreviewedGitHubFolder({
148
+ database,
149
+ resolved,
150
+ preview,
151
+ connectionId,
152
+ userEmail,
153
+ });
154
+ } catch (error) {
155
+ // The collection was made for this folder, so it doesn't stay behind
156
+ // empty when the link can't be saved. One an earlier try made is left
157
+ // alone.
158
+ if (madeEarlier) throw error;
159
+ try {
160
+ await deleteContentDatabase.run({ databaseId: database.id });
161
+ } catch (cleanupError) {
162
+ console.warn(
163
+ "[create-github-collection] could not remove the new collection",
164
+ cleanupError,
165
+ );
166
+ }
167
+ throw error;
168
+ }
169
+ return {
170
+ dryRun: false as const,
171
+ database: serializeDatabase(database),
172
+ preview,
173
+ title,
174
+ ...linked,
175
+ };
176
+ },
177
+ // Everyone reading the new collection gets its rows, not just the caller.
178
+ changeResource: (_input, result) =>
179
+ result.dryRun ? null : documentChangeResource(result.database.documentId),
180
+ });
@@ -4,6 +4,7 @@ import { and, eq, inArray } from "drizzle-orm";
4
4
  import { z } from "zod";
5
5
 
6
6
  import { getDb, schema } from "../server/db/index.js";
7
+ import { documentChangeResource } from "../server/lib/document-change-resource.js";
7
8
  import type {
8
9
  ContentDatabaseResponse,
9
10
  DisconnectContentDatabaseSourceRequest,
@@ -125,4 +126,8 @@ export default defineAction({
125
126
 
126
127
  return getContentDatabaseResponse(database.id, { limit: 100, offset: 0 });
127
128
  },
129
+ // Everyone reading the collection sees the source go, and a GitHub
130
+ // collection's pages become editable for them too.
131
+ changeResource: (_input, result) =>
132
+ documentChangeResource(result.database.documentId),
128
133
  });