@agent-native/core-corpus 0.212.1 → 0.213.1
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/templates/analytics/.agents/skills/onboarding-journeys/SKILL.md +29 -14
- package/corpus/templates/analytics/CHANGELOG.md +11 -0
- package/corpus/templates/analytics/actions/estimate-onboarding-journey-cost.ts +23 -6
- package/corpus/templates/analytics/actions/get-onboarding-journey.ts +20 -5
- package/corpus/templates/analytics/actions/provider-api-register.ts +2 -7
- package/corpus/templates/analytics/changelog/2026-10-21-analytics-recognizes-questions-about-connected-data-product-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-21-onboarding-journeys-now-honor-larger-event-row-budgets.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-21-preserve-unknown-replay-availability-in-partial-journeys.md +6 -0
- package/corpus/templates/analytics/package.json +6 -0
- package/corpus/templates/analytics/scripts/journey-capture-plan.ts +18 -7
- package/corpus/templates/analytics/scripts/journey-capture.ts +3 -1
- package/corpus/templates/analytics/server/lib/db-admin-connections.ts +19 -12
- package/corpus/templates/analytics/server/lib/journey-tree.ts +24 -9
- package/corpus/templates/analytics/server/lib/onboarding-journey-cost.ts +163 -94
- package/corpus/templates/analytics/server/lib/onboarding-journey.ts +162 -74
- package/corpus/templates/assets/server/lib/org-admin.ts +20 -16
- package/corpus/templates/brain/server/lib/zoom-governance.ts +11 -6
- package/corpus/templates/clips/CHANGELOG.md +1 -0
- package/corpus/templates/clips/changelog/2026-10-16-rewind-status-says-when-it-remembers-audio.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +7 -4
- package/corpus/templates/clips/desktop/src/components/RewindCaptureStatus.tsx +25 -0
- package/corpus/templates/clips/desktop/src/lib/rewind-status.ts +6 -1
- package/corpus/templates/clips/desktop/src/shared/config.ts +3 -0
- package/corpus/templates/clips/desktop/src-tauri/src/screen_memory.rs +2 -0
- package/corpus/templates/clips/server/lib/recordings.ts +4 -19
- package/corpus/templates/community/qr/package.json +1 -1
- package/corpus/templates/content/CHANGELOG.md +8 -0
- package/corpus/templates/content/actions/_content-space-access.ts +95 -6
- package/corpus/templates/content/actions/_database-utils.ts +20 -16
- package/corpus/templates/content/actions/_github-source-freshness.ts +72 -17
- package/corpus/templates/content/actions/_github-source-metadata.ts +63 -17
- package/corpus/templates/content/actions/_github-source.ts +141 -77
- package/corpus/templates/content/actions/check-github-connection.ts +15 -0
- package/corpus/templates/content/actions/create-github-collection.ts +180 -0
- package/corpus/templates/content/actions/disconnect-content-database-source.ts +5 -0
- package/corpus/templates/content/actions/link-github-folder.ts +104 -36
- package/corpus/templates/content/actions/refresh-content-database-source.ts +37 -4
- package/corpus/templates/content/actions/sync-github-folder.ts +23 -5
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +172 -19
- package/corpus/templates/content/app/components/editor/database/GitHubLinkDialog.tsx +938 -0
- package/corpus/templates/content/app/components/editor/database/GitHubSourcePanel.tsx +228 -0
- package/corpus/templates/content/app/components/editor/database/GitHubSourceStatusLine.tsx +561 -0
- package/corpus/templates/content/app/components/editor/database/GitHubUnlinkDialog.tsx +96 -0
- package/corpus/templates/content/app/components/editor/database/github-availability.ts +147 -0
- package/corpus/templates/content/app/components/editor/database/github-source-text.ts +280 -0
- package/corpus/templates/content/app/components/layout/Layout.tsx +2 -0
- package/corpus/templates/content/app/hooks/content-action-refresh.ts +6 -0
- package/corpus/templates/content/app/hooks/use-content-database.ts +3 -1
- package/corpus/templates/content/app/hooks/use-github-folder.ts +199 -0
- package/corpus/templates/content/app/i18n-data.ts +154 -1
- package/corpus/templates/content/app/lib/github-link-dialog.ts +86 -0
- package/corpus/templates/content/changelog/2026-10-18-connected-ai-assistants-can-link-a-new-collection-to-a-github.md +1 -1
- package/corpus/templates/content/changelog/2026-10-18-link-a-new-collection-to-a-github-folder-from-its-sources.md +6 -0
- package/corpus/templates/content/changelog/2026-10-21-organization-favorites-include-documents-shared-with-the-cur.md +6 -0
- package/corpus/templates/content/docs/product/capabilities/content.source.sync-policy.md +32 -7
- package/corpus/templates/content/docs/product/encyclopedia.md +3 -3
- package/corpus/templates/content/docs/product/features/content.feature.trust-your-connected-sources.md +2 -2
- package/corpus/templates/content/docs/product/roadmap.md +1 -1
- package/corpus/templates/content/parity/matrix.md +42 -42
- package/corpus/templates/content/parity/matrix.ts +14 -0
- package/corpus/templates/content/server/lib/github-source-client.ts +50 -1
- package/corpus/templates/content/shared/api.ts +3 -0
- package/corpus/templates/content/shared/github-source.ts +68 -0
- package/corpus/templates/design/CHANGELOG.md +1 -0
- package/corpus/templates/design/actions/connect-assets-mcp.ts +5 -9
- package/corpus/templates/design/changelog/2026-10-21-journey-canvases-accept-partial-analytics-samples-when-repla.md +6 -0
- package/corpus/templates/design/shared/journey-canvas.ts +6 -4
- package/corpus/templates/dispatch/.agents/skills/a2a-protocol/SKILL.md +16 -13
- package/corpus/templates/dispatch/CHANGELOG.md +4 -0
- package/corpus/templates/dispatch/changelog/2026-10-21-customer-ai-credit-questions-route-to-account-health-instead.md +6 -0
- package/corpus/templates/factory/server/lib/require-workspace-member.ts +5 -17
- package/corpus/templates/mail/server/lib/queued-drafts.ts +9 -4
- package/corpus/templates/slides/server/workspace-defaults.ts +12 -15
- package/package.json +1 -1
|
@@ -14,6 +14,17 @@ import {
|
|
|
14
14
|
/** Most skipped files a link remembers for its status line. */
|
|
15
15
|
const MAX_REMEMBERED_SKIPS = 50;
|
|
16
16
|
|
|
17
|
+
/** A file the folder has that no row shows, and why. */
|
|
18
|
+
export interface GitHubSkippedFile {
|
|
19
|
+
path: string;
|
|
20
|
+
reason: GitHubSourceSkipReason;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** A file skipped once it was read, with the blob it was read at. */
|
|
24
|
+
export interface GitHubReadSkip extends GitHubSkippedFile {
|
|
25
|
+
blobSha: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
17
28
|
/**
|
|
18
29
|
* Everything a GitHub link keeps between syncs, stored under `github` in the
|
|
19
30
|
* source's metadata. Rows keep their own blob SHA, so a sync that stops
|
|
@@ -40,7 +51,15 @@ export interface GitHubSourceState extends GitHubFolderLocation {
|
|
|
40
51
|
*/
|
|
41
52
|
cutShort: string[];
|
|
42
53
|
files: number;
|
|
43
|
-
|
|
54
|
+
/** Up to 50 skipped files; `skippedCount` says how many there are. */
|
|
55
|
+
skipped: GitHubSkippedFile[];
|
|
56
|
+
skippedCount: number;
|
|
57
|
+
/**
|
|
58
|
+
* Every file skipped once it was read, so later syncs pass each by while
|
|
59
|
+
* its blob is unchanged rather than reading it again. One per Markdown
|
|
60
|
+
* file at most, so a link never keeps more than 2,000.
|
|
61
|
+
*/
|
|
62
|
+
readSkips: GitHubReadSkip[];
|
|
44
63
|
/** Images rows couldn't store yet because file storage isn't set up. */
|
|
45
64
|
imagesWaiting: number;
|
|
46
65
|
problem: {
|
|
@@ -103,6 +122,24 @@ function stringOrNull(value: unknown): string | null {
|
|
|
103
122
|
return typeof value === "string" && value ? value : null;
|
|
104
123
|
}
|
|
105
124
|
|
|
125
|
+
function readSkippedFiles(
|
|
126
|
+
value: unknown,
|
|
127
|
+
): Array<GitHubSkippedFile & { blobSha?: unknown }> {
|
|
128
|
+
return (Array.isArray(value) ? value : []).flatMap((raw) => {
|
|
129
|
+
const skip = asRecord(raw);
|
|
130
|
+
if (
|
|
131
|
+
!skip ||
|
|
132
|
+
typeof skip.path !== "string" ||
|
|
133
|
+
(skip.reason !== "mdx" &&
|
|
134
|
+
skip.reason !== "too-large" &&
|
|
135
|
+
skip.reason !== "not-text")
|
|
136
|
+
) {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
return [{ path: skip.path, reason: skip.reason, blobSha: skip.blobSha }];
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
106
143
|
export function readGitHubSourceState(
|
|
107
144
|
metadata: unknown,
|
|
108
145
|
): GitHubSourceState | null {
|
|
@@ -119,6 +156,16 @@ export function readGitHubSourceState(
|
|
|
119
156
|
const progress = asRecord(github.progress);
|
|
120
157
|
const problem = asRecord(github.problem);
|
|
121
158
|
const claim = asRecord(github.claim);
|
|
159
|
+
const skipped = readSkippedFiles(github.skipped).map(({ path, reason }) => ({
|
|
160
|
+
path,
|
|
161
|
+
reason,
|
|
162
|
+
}));
|
|
163
|
+
const readSkips = readSkippedFiles(github.readSkips).flatMap(
|
|
164
|
+
(skip): GitHubReadSkip[] =>
|
|
165
|
+
typeof skip.blobSha === "string"
|
|
166
|
+
? [{ path: skip.path, reason: skip.reason, blobSha: skip.blobSha }]
|
|
167
|
+
: [],
|
|
168
|
+
);
|
|
122
169
|
return {
|
|
123
170
|
owner: github.owner,
|
|
124
171
|
repo: github.repo,
|
|
@@ -143,18 +190,12 @@ export function readGitHubSourceState(
|
|
|
143
190
|
(path): path is string => typeof path === "string",
|
|
144
191
|
),
|
|
145
192
|
files: typeof github.files === "number" ? github.files : 0,
|
|
146
|
-
skipped
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
skip.reason === "too-large" ||
|
|
153
|
-
skip.reason === "not-text")
|
|
154
|
-
? [{ path: skip.path, reason: skip.reason }]
|
|
155
|
-
: [];
|
|
156
|
-
},
|
|
157
|
-
),
|
|
193
|
+
skipped,
|
|
194
|
+
skippedCount:
|
|
195
|
+
typeof github.skippedCount === "number"
|
|
196
|
+
? github.skippedCount
|
|
197
|
+
: skipped.length,
|
|
198
|
+
readSkips,
|
|
158
199
|
imagesWaiting:
|
|
159
200
|
typeof github.imagesWaiting === "number" ? github.imagesWaiting : 0,
|
|
160
201
|
problem:
|
|
@@ -174,10 +215,14 @@ export function readGitHubSourceState(
|
|
|
174
215
|
};
|
|
175
216
|
}
|
|
176
217
|
|
|
218
|
+
/** The first 50 skipped files in path order, which the status line lists. */
|
|
177
219
|
export function rememberedSkips(
|
|
178
|
-
skipped:
|
|
179
|
-
) {
|
|
180
|
-
return skipped
|
|
220
|
+
skipped: GitHubSkippedFile[],
|
|
221
|
+
): GitHubSkippedFile[] {
|
|
222
|
+
return [...skipped]
|
|
223
|
+
.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))
|
|
224
|
+
.slice(0, MAX_REMEMBERED_SKIPS)
|
|
225
|
+
.map(({ path, reason }) => ({ path, reason }));
|
|
181
226
|
}
|
|
182
227
|
|
|
183
228
|
/** The status agents and the status line read; never includes the claim. */
|
|
@@ -199,7 +244,8 @@ export function githubSourceStatus(
|
|
|
199
244
|
pendingCommitSha: state.pendingCommitSha,
|
|
200
245
|
progress: state.progress,
|
|
201
246
|
files: state.files,
|
|
202
|
-
skipped: state.skipped,
|
|
247
|
+
skipped: state.skipped.map(({ path, reason }) => ({ path, reason })),
|
|
248
|
+
skippedCount: state.skippedCount,
|
|
203
249
|
imagesWaiting: state.imagesWaiting,
|
|
204
250
|
problem: state.problem,
|
|
205
251
|
};
|
|
@@ -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
|
-
|
|
1884
|
-
result.unchanged = planned.length - work.length;
|
|
1885
|
-
const
|
|
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
|
-
|
|
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
|
|
1961
|
-
|
|
1962
|
-
);
|
|
1984
|
+
const skippedFiles = [...files.skipped, ...readSkips];
|
|
1985
|
+
const skipped = rememberedSkips(skippedFiles);
|
|
1963
1986
|
await release(
|
|
1964
|
-
(current) =>
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
:
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
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
|
|
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
|
-
|
|
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) => ({
|
|
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
|
-
|
|
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) {
|
|
@@ -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
|
});
|