@agent-native/core 0.164.11 → 0.164.15
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/calendar/.agents/skills/event-management/SKILL.md +84 -0
- package/corpus/templates/calendar/AGENTS.md +5 -0
- package/corpus/templates/calendar/actions/delete-event.ts +1 -1
- package/corpus/templates/calendar/actions/delete-events.ts +508 -0
- package/corpus/templates/calendar/actions/event-action-helpers.ts +1 -1
- package/corpus/templates/calendar/actions/list-events.ts +53 -0
- package/corpus/templates/calendar/changelog/2026-08-18-the-agent-can-now-remove-many-meetings-at-once-such-as-every.md +6 -0
- package/corpus/templates/calendar/server/lib/event-weekday.ts +112 -0
- package/corpus/templates/content/actions/_database-source-utils.ts +25 -2
- package/corpus/templates/content/actions/_database-utils.ts +2 -0
- package/corpus/templates/content/actions/_local-folder-source.ts +186 -2
- package/corpus/templates/content/actions/connect-local-folder-source.ts +23 -8
- package/corpus/templates/content/actions/disconnect-local-folder-source.ts +3 -3
- package/corpus/templates/content/actions/resolve-local-folder-conflict.ts +2 -2
- package/corpus/templates/content/actions/sync-local-folder-source.ts +130 -12
- package/corpus/templates/content/app/components/LocalFolderLiveSync.tsx +77 -0
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +354 -8
- package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +23 -1
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +78 -1
- package/corpus/templates/content/app/components/editor/database/sidebar.tsx +36 -0
- package/corpus/templates/content/app/components/editor/local-source-write-state.ts +52 -0
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +244 -6
- package/corpus/templates/content/app/components/sidebar/local-source-hierarchy.ts +189 -0
- package/corpus/templates/content/app/hooks/use-create-page.ts +26 -4
- package/corpus/templates/content/app/hooks/use-documents.ts +83 -0
- package/corpus/templates/content/app/i18n-data.ts +79 -0
- package/corpus/templates/content/app/lib/desktop-content-files.ts +57 -1
- package/corpus/templates/content/app/lib/local-content-source-files.ts +210 -34
- package/corpus/templates/content/app/lib/local-folder-live-sync.ts +191 -0
- package/corpus/templates/content/app/root.tsx +2 -1
- package/corpus/templates/content/app/routes/_app.local-files.tsx +103 -38
- package/corpus/templates/content/docs/product/capabilities/content.source.file-folder.md +2 -2
- package/corpus/templates/content/docs/product/features/content.feature.bring-your-local-work.md +3 -3
- package/corpus/templates/content/docs/product/roadmap.md +2 -2
- package/corpus/templates/content/docs/solutions/2026-08-18-content-navigation-coherence-shape.md +482 -0
- package/corpus/templates/content/shared/api.ts +25 -0
- package/corpus/templates/factory/AGENTS.md +2 -1
- package/corpus/templates/factory/actions/get-factory-graph-version.ts +69 -0
- package/corpus/templates/factory/actions/list-factory-graph-versions.ts +82 -0
- package/corpus/templates/factory/actions/restore-factory-graph-version.ts +163 -0
- package/corpus/templates/factory/actions/save-factory-graph.ts +93 -60
- package/corpus/templates/factory/app/components/factory/FactoryHistoryView.tsx +709 -0
- package/corpus/templates/factory/app/components/factory/FactoryInspector.tsx +62 -2
- package/corpus/templates/factory/app/components/ui/alert-dialog.tsx +1 -0
- package/corpus/templates/factory/app/i18n/en-US.ts +43 -0
- package/corpus/templates/factory/app/routes/factory.tsx +115 -15
- package/corpus/templates/factory/server/plugins/agent-chat.ts +3 -1
- package/dist/app-config/auth.d.ts +4 -0
- package/dist/app-config/auth.js +7 -0
- package/dist/app-config/schema.d.ts +3 -0
- package/dist/app-config/schema.js +2 -0
- package/dist/cli/code-agent-executor.js +6 -1
- package/dist/cli/code-agent-mcp-config.d.ts +2 -2
- package/dist/cli/code-agent-mcp-config.js +34 -9
- package/dist/client/org/OrgSwitcher.js +8 -4
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/mcp-client/config.d.ts +11 -0
- package/dist/mcp-client/config.js +33 -26
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/auth.js +15 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/docs/content/actions-access-control.mdx +136 -2
- package/docs/content/actions-agent-tools.mdx +101 -0
- package/docs/content/actions-defining.mdx +2 -2
- package/docs/content/audit-log.mdx +1 -1
- package/docs/content/authentication.mdx +1 -1
- package/docs/content/cloudflare.mdx +43 -0
- package/docs/content/creating-templates.mdx +1 -1
- package/docs/content/deploy-an-app.mdx +109 -0
- package/docs/content/deployment-environment-variables.mdx +101 -0
- package/docs/content/deployment.mdx +125 -792
- package/docs/content/docs-components.mdx +2 -2
- package/docs/content/environment-variables.mdx +41 -41
- package/docs/content/file-uploads.mdx +74 -40
- package/docs/content/harness-agents.mdx +2 -2
- package/docs/content/internationalization.mdx +61 -65
- package/docs/content/key-concepts.mdx +3 -3
- package/docs/content/multi-app-workspace.mdx +2 -2
- package/docs/content/netlify.mdx +47 -0
- package/docs/content/node-docker.mdx +138 -0
- package/docs/content/organizations-teams-permissions.mdx +5 -5
- package/docs/content/other-platforms.mdx +27 -0
- package/docs/content/ssr-caching.mdx +45 -0
- package/docs/content/template-assets-developers.mdx +2 -2
- package/docs/content/template-chat-developers.mdx +2 -2
- package/docs/content/template-content-developers.mdx +2 -2
- package/docs/content/{local-file-mode.mdx → template-content-local-files.mdx} +2 -1
- package/docs/content/template-content-sync.mdx +2 -2
- package/docs/content/toolkit-agent-ux.mdx +1 -1
- package/docs/content/toolkit-resources.mdx +2 -2
- package/docs/content/updating-ui-in-production.mdx +35 -0
- package/docs/content/vercel.mdx +38 -0
- package/docs/content/what-is-agent-native.mdx +1 -1
- package/docs/content/workspace-deployment.mdx +350 -0
- package/package.json +1 -1
- package/docs/content/agents.mdx +0 -9
- package/docs/content/database.mdx +0 -317
- package/docs/content/human-approval.mdx +0 -156
|
@@ -554,6 +554,59 @@ async function listLocalBookingEvents(
|
|
|
554
554
|
});
|
|
555
555
|
}
|
|
556
556
|
|
|
557
|
+
/**
|
|
558
|
+
* Which of these Google event ids are backing a still-active booking.
|
|
559
|
+
*
|
|
560
|
+
* Deleting such an event without cancelling its booking leaves the row
|
|
561
|
+
* confirmed, and `shouldShowLocalBookingEvent` then republishes the booking as a
|
|
562
|
+
* local calendar event — so the "deleted" meeting reappears. Scoped through the
|
|
563
|
+
* same booking-link access filter as the calendar read.
|
|
564
|
+
*
|
|
565
|
+
* Returns `calendarAccountId` so callers can tell which account a booking'"'"'s event
|
|
566
|
+
* lives on. It is nullable — the column was added after bookings already
|
|
567
|
+
* existed — so a null must be read as "unknown account", never as "other
|
|
568
|
+
* account".
|
|
569
|
+
*/
|
|
570
|
+
export async function findBookedGoogleEvents(
|
|
571
|
+
googleEventIds: readonly string[],
|
|
572
|
+
): Promise<Array<{ googleEventId: string; calendarAccountId: string | null }>> {
|
|
573
|
+
const ids = Array.from(new Set(googleEventIds.filter(Boolean)));
|
|
574
|
+
if (ids.length === 0) return [];
|
|
575
|
+
|
|
576
|
+
const db = getDb();
|
|
577
|
+
const links = await db
|
|
578
|
+
.select({ slug: schema.bookingLinks.slug })
|
|
579
|
+
.from(schema.bookingLinks)
|
|
580
|
+
.where(accessFilter(schema.bookingLinks, schema.bookingLinkShares));
|
|
581
|
+
const slugs = links.map((link) => link.slug);
|
|
582
|
+
if (slugs.length === 0) return [];
|
|
583
|
+
|
|
584
|
+
const rows = await db
|
|
585
|
+
.select({
|
|
586
|
+
googleEventId: schema.bookings.googleEventId,
|
|
587
|
+
calendarAccountId: schema.bookings.calendarAccountId,
|
|
588
|
+
})
|
|
589
|
+
.from(schema.bookings)
|
|
590
|
+
.where(
|
|
591
|
+
and(
|
|
592
|
+
inArray(schema.bookings.slug, slugs),
|
|
593
|
+
ne(schema.bookings.status, "cancelled"),
|
|
594
|
+
inArray(schema.bookings.googleEventId, ids),
|
|
595
|
+
),
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
return rows.flatMap((row) =>
|
|
599
|
+
row.googleEventId
|
|
600
|
+
? [
|
|
601
|
+
{
|
|
602
|
+
googleEventId: row.googleEventId,
|
|
603
|
+
calendarAccountId: row.calendarAccountId ?? null,
|
|
604
|
+
},
|
|
605
|
+
]
|
|
606
|
+
: [],
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
|
|
557
610
|
function shouldShowLocalBookingEvent({
|
|
558
611
|
event,
|
|
559
612
|
googleEventIds,
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
const DATE_ONLY_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
2
|
+
|
|
3
|
+
export const WEEKDAY_NAMES = [
|
|
4
|
+
"sunday",
|
|
5
|
+
"monday",
|
|
6
|
+
"tuesday",
|
|
7
|
+
"wednesday",
|
|
8
|
+
"thursday",
|
|
9
|
+
"friday",
|
|
10
|
+
"saturday",
|
|
11
|
+
] as const;
|
|
12
|
+
|
|
13
|
+
export type WeekdayName = (typeof WEEKDAY_NAMES)[number];
|
|
14
|
+
|
|
15
|
+
const WEEKDAY_ALIASES = new Map<string, WeekdayName>([
|
|
16
|
+
...WEEKDAY_NAMES.map((day) => [day, day] as const),
|
|
17
|
+
...WEEKDAY_NAMES.map((day) => [day.slice(0, 3), day] as const),
|
|
18
|
+
["tues", "tuesday"],
|
|
19
|
+
["thur", "thursday"],
|
|
20
|
+
["thurs", "thursday"],
|
|
21
|
+
["weekend", "saturday"],
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Accepts the shapes a natural-language request arrives in — "Saturday",
|
|
26
|
+
* "sat", "SUN", or an array/comma string mixing them — and rejects anything
|
|
27
|
+
* else loudly. A silently dropped day name would delete a different set of
|
|
28
|
+
* events than the user asked about.
|
|
29
|
+
*/
|
|
30
|
+
export function normalizeWeekdays(
|
|
31
|
+
input: string | readonly string[] | undefined,
|
|
32
|
+
): WeekdayName[] {
|
|
33
|
+
if (input === undefined) return [];
|
|
34
|
+
const raw = (Array.isArray(input) ? input : String(input).split(","))
|
|
35
|
+
.map((value) => value.trim().toLowerCase())
|
|
36
|
+
.filter(Boolean);
|
|
37
|
+
const resolved = new Set<WeekdayName>();
|
|
38
|
+
for (const value of raw) {
|
|
39
|
+
if (value === "weekend" || value === "weekends") {
|
|
40
|
+
resolved.add("saturday");
|
|
41
|
+
resolved.add("sunday");
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (value === "weekday" || value === "weekdays") {
|
|
45
|
+
for (const day of WEEKDAY_NAMES) {
|
|
46
|
+
if (day !== "saturday" && day !== "sunday") resolved.add(day);
|
|
47
|
+
}
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const match = WEEKDAY_ALIASES.get(value.replace(/s$/, ""));
|
|
51
|
+
if (!match) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`Unrecognized day of week: ${value}. Use full or 3-letter English day names.`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
resolved.add(match);
|
|
57
|
+
}
|
|
58
|
+
return WEEKDAY_NAMES.filter((day) => resolved.has(day));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The weekday the user sees for an event, which is the weekday in the calendar
|
|
63
|
+
* view's timezone — not UTC. A Sunday 5pm America/Los_Angeles meeting is Monday
|
|
64
|
+
* in UTC, so filtering on the raw ISO string silently misses it and instead
|
|
65
|
+
* deletes a Monday meeting the user never mentioned.
|
|
66
|
+
*
|
|
67
|
+
* All-day starts carry no instant, so their `YYYY-MM-DD` date IS the local day
|
|
68
|
+
* and must not be re-projected through a timezone.
|
|
69
|
+
*/
|
|
70
|
+
export function eventWeekday(start: string, timezone: string): WeekdayName {
|
|
71
|
+
if (DATE_ONLY_RE.test(start)) {
|
|
72
|
+
const [year, month, day] = start.split("-").map(Number);
|
|
73
|
+
return WEEKDAY_NAMES[new Date(Date.UTC(year, month - 1, day)).getUTCDay()];
|
|
74
|
+
}
|
|
75
|
+
const parsed = new Date(start);
|
|
76
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
77
|
+
throw new Error(`Invalid event start: ${start}`);
|
|
78
|
+
}
|
|
79
|
+
const label = new Intl.DateTimeFormat("en-US", {
|
|
80
|
+
timeZone: timezone,
|
|
81
|
+
weekday: "long",
|
|
82
|
+
})
|
|
83
|
+
.format(parsed)
|
|
84
|
+
.toLowerCase();
|
|
85
|
+
const match = WEEKDAY_NAMES.find((day) => day === label);
|
|
86
|
+
if (!match) throw new Error(`Unable to resolve weekday for ${start}`);
|
|
87
|
+
return match;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function matchesWeekdays(
|
|
91
|
+
start: string,
|
|
92
|
+
timezone: string,
|
|
93
|
+
weekdays: readonly WeekdayName[],
|
|
94
|
+
): boolean {
|
|
95
|
+
if (weekdays.length === 0) return true;
|
|
96
|
+
return weekdays.includes(eventWeekday(start, timezone));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Validate a caller-supplied IANA timezone instead of falling back to UTC.
|
|
101
|
+
* `normalizeTimezone` silently returns UTC for an unparseable zone, which on a
|
|
102
|
+
* destructive weekday filter would quietly reclassify every event and delete a
|
|
103
|
+
* different set of days than the caller named.
|
|
104
|
+
*/
|
|
105
|
+
export function requireValidTimezone(timezone: string): string {
|
|
106
|
+
try {
|
|
107
|
+
new Intl.DateTimeFormat("en-US", { timeZone: timezone }).format();
|
|
108
|
+
} catch {
|
|
109
|
+
throw new Error(`Invalid IANA timezone: ${timezone}`);
|
|
110
|
+
}
|
|
111
|
+
return timezone;
|
|
112
|
+
}
|
|
@@ -60,6 +60,10 @@ import {
|
|
|
60
60
|
chunks,
|
|
61
61
|
processWithConcurrency,
|
|
62
62
|
} from "./_batch-utils.js";
|
|
63
|
+
import {
|
|
64
|
+
LOCAL_FOLDER_SOURCE_TYPE,
|
|
65
|
+
localFolderSourceIdentityFromMetadata,
|
|
66
|
+
} from "./_local-folder-source.js";
|
|
63
67
|
export { bulkChunkSizeForColumnCount } from "./_batch-utils.js";
|
|
64
68
|
import {
|
|
65
69
|
readBuilderCmsContentEntry,
|
|
@@ -176,6 +180,9 @@ type SourceMetadataRecord = {
|
|
|
176
180
|
connectionId?: string | null;
|
|
177
181
|
connectionLabel?: string | null;
|
|
178
182
|
truthPolicy?: ContentDatabaseSource["metadata"]["truthPolicy"];
|
|
183
|
+
syncPolicy?: "manual" | "keep_in_sync";
|
|
184
|
+
liveBridgeEnabled?: boolean;
|
|
185
|
+
localIdentity?: unknown;
|
|
179
186
|
liveReadConfigured?: boolean;
|
|
180
187
|
lastReadEntryCount?: number;
|
|
181
188
|
lastReadMatchedRowCount?: number;
|
|
@@ -4572,14 +4579,19 @@ async function loadSourceSnapshot(
|
|
|
4572
4579
|
? metadata.writeMode
|
|
4573
4580
|
: undefined;
|
|
4574
4581
|
const capabilities = normalizeCapabilities(source.capabilitiesJson);
|
|
4575
|
-
|
|
4582
|
+
const sourceType = normalizeSourceType(source.sourceType);
|
|
4583
|
+
if (sourceType === LOCAL_FOLDER_SOURCE_TYPE) {
|
|
4584
|
+
capabilities.liveWritesEnabled =
|
|
4585
|
+
metadata.liveBridgeEnabled === true &&
|
|
4586
|
+
metadata.syncPolicy === "keep_in_sync";
|
|
4587
|
+
} else if (normalizedWriteMode) {
|
|
4576
4588
|
capabilities.liveWritesEnabled = normalizedWriteMode !== "read_only";
|
|
4577
4589
|
}
|
|
4578
4590
|
|
|
4579
4591
|
// A local-table source shows the target database's *live* title, so renaming
|
|
4580
4592
|
// the underlying table is reflected here instead of the name frozen at attach.
|
|
4581
4593
|
let displaySourceName = source.sourceName;
|
|
4582
|
-
if (
|
|
4594
|
+
if (sourceType === "local-table") {
|
|
4583
4595
|
const [target] = await db
|
|
4584
4596
|
.select({ title: schema.contentDatabases.title })
|
|
4585
4597
|
.from(schema.contentDatabases)
|
|
@@ -4626,6 +4638,17 @@ async function loadSourceSnapshot(
|
|
|
4626
4638
|
metadata.truthPolicy === "reviewed_bidirectional"
|
|
4627
4639
|
? metadata.truthPolicy
|
|
4628
4640
|
: undefined,
|
|
4641
|
+
syncPolicy:
|
|
4642
|
+
metadata.syncPolicy === "manual" ||
|
|
4643
|
+
metadata.syncPolicy === "keep_in_sync"
|
|
4644
|
+
? metadata.syncPolicy
|
|
4645
|
+
: undefined,
|
|
4646
|
+
liveBridgeEnabled:
|
|
4647
|
+
metadata.liveBridgeEnabled === true &&
|
|
4648
|
+
metadata.syncPolicy === "keep_in_sync",
|
|
4649
|
+
localIdentity: localFolderSourceIdentityFromMetadata(
|
|
4650
|
+
metadata.localIdentity,
|
|
4651
|
+
),
|
|
4629
4652
|
liveReadConfigured: metadata.liveReadConfigured === true,
|
|
4630
4653
|
lastReadEntryCount:
|
|
4631
4654
|
typeof metadata.lastReadEntryCount === "number"
|
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
getDatabaseMutationContract,
|
|
51
51
|
} from "./_database-row-mutation.js";
|
|
52
52
|
import { getAllContentDatabaseSourceSnapshots } from "./_database-source-utils.js";
|
|
53
|
+
import { serializeDocumentSource } from "./_document-source.js";
|
|
53
54
|
import {
|
|
54
55
|
applyFederatedOverlayValues,
|
|
55
56
|
federateSources,
|
|
@@ -443,6 +444,7 @@ function serializeDocument(
|
|
|
443
444
|
databaseMembership: membership
|
|
444
445
|
? serializeDatabaseMembership(membership)
|
|
445
446
|
: undefined,
|
|
447
|
+
source: serializeDocumentSource(doc),
|
|
446
448
|
createdAt: doc.createdAt,
|
|
447
449
|
updatedAt: doc.updatedAt,
|
|
448
450
|
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
3
5
|
import type {
|
|
4
6
|
ContentDatabaseSourceCapabilities,
|
|
5
7
|
ContentDatabaseSourceTruthPolicy,
|
|
@@ -7,6 +9,171 @@ import type {
|
|
|
7
9
|
|
|
8
10
|
export const LOCAL_FOLDER_SOURCE_TYPE = "local-folder" as const;
|
|
9
11
|
|
|
12
|
+
export const localFolderOpaqueIdentitySchema = z
|
|
13
|
+
.string()
|
|
14
|
+
.trim()
|
|
15
|
+
.min(1)
|
|
16
|
+
.max(300)
|
|
17
|
+
.refine(
|
|
18
|
+
(value) =>
|
|
19
|
+
!value.includes("/") &&
|
|
20
|
+
!value.includes("\\") &&
|
|
21
|
+
!value.includes("..") &&
|
|
22
|
+
!value.includes("\0"),
|
|
23
|
+
"Local source identities must be opaque IDs, not paths.",
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const labelSchema = z.string().trim().min(1).max(200);
|
|
27
|
+
|
|
28
|
+
export const localFolderSourceConnectionMetadataSchema = z
|
|
29
|
+
.object({
|
|
30
|
+
repository: z
|
|
31
|
+
.object({
|
|
32
|
+
localId: localFolderOpaqueIdentitySchema,
|
|
33
|
+
providerBinding: z
|
|
34
|
+
.object({
|
|
35
|
+
provider: z.literal("github"),
|
|
36
|
+
repositoryId: localFolderOpaqueIdentitySchema,
|
|
37
|
+
})
|
|
38
|
+
.strict()
|
|
39
|
+
.optional(),
|
|
40
|
+
})
|
|
41
|
+
.strict()
|
|
42
|
+
.optional(),
|
|
43
|
+
workingCopy: z
|
|
44
|
+
.object({
|
|
45
|
+
id: localFolderOpaqueIdentitySchema,
|
|
46
|
+
repositoryId: localFolderOpaqueIdentitySchema.optional(),
|
|
47
|
+
kind: z.enum(["persistent", "temporary"]),
|
|
48
|
+
name: labelSchema,
|
|
49
|
+
branch: z.string().trim().min(1).max(300).optional(),
|
|
50
|
+
commit: z.string().trim().min(1).max(300).optional(),
|
|
51
|
+
deviceId: localFolderOpaqueIdentitySchema,
|
|
52
|
+
})
|
|
53
|
+
.strict()
|
|
54
|
+
.optional(),
|
|
55
|
+
liveBridgeEnabled: z.boolean().optional().default(false),
|
|
56
|
+
})
|
|
57
|
+
.strict();
|
|
58
|
+
|
|
59
|
+
export type LocalFolderSourceConnectionMetadata = z.infer<
|
|
60
|
+
typeof localFolderSourceConnectionMetadataSchema
|
|
61
|
+
>;
|
|
62
|
+
|
|
63
|
+
export type LocalFolderSourceIdentity = {
|
|
64
|
+
repository?: {
|
|
65
|
+
localId: string;
|
|
66
|
+
providerBinding?: { provider: "github"; repositoryId: string };
|
|
67
|
+
};
|
|
68
|
+
workingCopy: {
|
|
69
|
+
id: string;
|
|
70
|
+
repositoryId?: string;
|
|
71
|
+
kind: "persistent" | "temporary";
|
|
72
|
+
name: string;
|
|
73
|
+
branch?: string;
|
|
74
|
+
commit?: string;
|
|
75
|
+
deviceId: string;
|
|
76
|
+
localOnly: boolean;
|
|
77
|
+
shareable: boolean;
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export function localFolderSourceIdentityFromMetadata(
|
|
82
|
+
value: unknown,
|
|
83
|
+
): LocalFolderSourceIdentity | undefined {
|
|
84
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
const stored = value as {
|
|
88
|
+
repository?: unknown;
|
|
89
|
+
workingCopy?: Record<string, unknown>;
|
|
90
|
+
};
|
|
91
|
+
const connectionMetadata =
|
|
92
|
+
localFolderSourceConnectionMetadataSchema.safeParse({
|
|
93
|
+
repository: stored.repository,
|
|
94
|
+
workingCopy: stored.workingCopy
|
|
95
|
+
? Object.fromEntries(
|
|
96
|
+
Object.entries(stored.workingCopy).filter(
|
|
97
|
+
([key]) => key !== "localOnly" && key !== "shareable",
|
|
98
|
+
),
|
|
99
|
+
)
|
|
100
|
+
: undefined,
|
|
101
|
+
});
|
|
102
|
+
if (!connectionMetadata.success || !connectionMetadata.data.workingCopy) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
return normalizeLocalFolderSourceIdentity({
|
|
107
|
+
connectionId: "stored-local-source",
|
|
108
|
+
label: connectionMetadata.data.workingCopy.name,
|
|
109
|
+
connectionMetadata: connectionMetadata.data,
|
|
110
|
+
});
|
|
111
|
+
} catch {
|
|
112
|
+
// coercion-ok: invalid legacy metadata is represented as absent optional identity
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function normalizeLocalFolderSourceIdentity(input: {
|
|
118
|
+
connectionId: string;
|
|
119
|
+
label: string;
|
|
120
|
+
connectionMetadata?: LocalFolderSourceConnectionMetadata;
|
|
121
|
+
}): LocalFolderSourceIdentity {
|
|
122
|
+
const metadata = localFolderSourceConnectionMetadataSchema.parse(
|
|
123
|
+
input.connectionMetadata ?? {},
|
|
124
|
+
);
|
|
125
|
+
const workingCopy = metadata.workingCopy ?? {
|
|
126
|
+
id: input.connectionId,
|
|
127
|
+
kind: "persistent" as const,
|
|
128
|
+
name: input.label,
|
|
129
|
+
deviceId: "unknown-device",
|
|
130
|
+
};
|
|
131
|
+
if (
|
|
132
|
+
workingCopy.repositoryId &&
|
|
133
|
+
(!metadata.repository ||
|
|
134
|
+
workingCopy.repositoryId !== metadata.repository.localId)
|
|
135
|
+
) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
"Working-copy repositoryId must match the supplied repository identity.",
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
...(metadata.repository ? { repository: metadata.repository } : {}),
|
|
142
|
+
workingCopy: {
|
|
143
|
+
...workingCopy,
|
|
144
|
+
localOnly: workingCopy.kind === "temporary",
|
|
145
|
+
shareable: workingCopy.kind !== "temporary",
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function localFolderObservedRevision(input: {
|
|
151
|
+
contentHash: string;
|
|
152
|
+
metadataHash: string;
|
|
153
|
+
}) {
|
|
154
|
+
return `sha256:${createHash("sha256")
|
|
155
|
+
.update(`${input.contentHash}:${input.metadataHash}`)
|
|
156
|
+
.digest("hex")}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function localFolderSourceFileIdentity(input: {
|
|
160
|
+
workingCopyId: string;
|
|
161
|
+
relativePath: string;
|
|
162
|
+
observedRevision: string;
|
|
163
|
+
}) {
|
|
164
|
+
const relativePath = input.relativePath.replace(/\\/g, "/");
|
|
165
|
+
if (
|
|
166
|
+
!relativePath ||
|
|
167
|
+
relativePath.startsWith("/") ||
|
|
168
|
+
relativePath.split("/").some((part) => part === "." || part === "..")
|
|
169
|
+
) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
"Local source file identity requires a safe relative path.",
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
return { ...input, relativePath };
|
|
175
|
+
}
|
|
176
|
+
|
|
10
177
|
export function localFolderSourceId(databaseId: string, connectionId: string) {
|
|
11
178
|
return `content_database_source_local_folder_${createHash("sha256")
|
|
12
179
|
.update(`${databaseId}:${connectionId}`)
|
|
@@ -14,7 +181,9 @@ export function localFolderSourceId(databaseId: string, connectionId: string) {
|
|
|
14
181
|
.slice(0, 32)}`;
|
|
15
182
|
}
|
|
16
183
|
|
|
17
|
-
export function localFolderSourceCapabilities(
|
|
184
|
+
export function localFolderSourceCapabilities(input?: {
|
|
185
|
+
liveBridgeEnabled?: boolean;
|
|
186
|
+
}): ContentDatabaseSourceCapabilities {
|
|
18
187
|
return {
|
|
19
188
|
canRefresh: true,
|
|
20
189
|
canCreateChangeSets: true,
|
|
@@ -25,7 +194,7 @@ export function localFolderSourceCapabilities(): ContentDatabaseSourceCapabiliti
|
|
|
25
194
|
canPublish: false,
|
|
26
195
|
canDelete: true,
|
|
27
196
|
canStageLocalRevision: true,
|
|
28
|
-
liveWritesEnabled:
|
|
197
|
+
liveWritesEnabled: input?.liveBridgeEnabled === true,
|
|
29
198
|
readOnlyRefresh: false,
|
|
30
199
|
canRename: true,
|
|
31
200
|
canReveal: true,
|
|
@@ -37,7 +206,16 @@ export function localFolderSourceMetadata(input: {
|
|
|
37
206
|
connectionId: string;
|
|
38
207
|
label: string;
|
|
39
208
|
truthPolicy: ContentDatabaseSourceTruthPolicy;
|
|
209
|
+
connectionMetadata?: LocalFolderSourceConnectionMetadata;
|
|
40
210
|
}) {
|
|
211
|
+
const connectionMetadata = localFolderSourceConnectionMetadataSchema.parse(
|
|
212
|
+
input.connectionMetadata ?? {},
|
|
213
|
+
);
|
|
214
|
+
const identity = normalizeLocalFolderSourceIdentity({
|
|
215
|
+
connectionId: input.connectionId,
|
|
216
|
+
label: input.label,
|
|
217
|
+
connectionMetadata,
|
|
218
|
+
});
|
|
41
219
|
return {
|
|
42
220
|
primaryKey: "relative_path",
|
|
43
221
|
naturalKeyField: "relative_path",
|
|
@@ -47,6 +225,12 @@ export function localFolderSourceMetadata(input: {
|
|
|
47
225
|
connectionLabel: input.label,
|
|
48
226
|
truthPolicy: input.truthPolicy,
|
|
49
227
|
writeMode: "stage_only" as const,
|
|
228
|
+
syncPolicy:
|
|
229
|
+
connectionMetadata.liveBridgeEnabled === true
|
|
230
|
+
? ("keep_in_sync" as const)
|
|
231
|
+
: ("manual" as const),
|
|
232
|
+
liveBridgeEnabled: connectionMetadata.liveBridgeEnabled === true,
|
|
233
|
+
localIdentity: identity,
|
|
50
234
|
notes:
|
|
51
235
|
"The trusted local bridge owns the folder handle or path; SQL stores only this opaque connection identity.",
|
|
52
236
|
};
|
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
} from "./_content-spaces.js";
|
|
18
18
|
import {
|
|
19
19
|
LOCAL_FOLDER_SOURCE_TYPE,
|
|
20
|
+
localFolderSourceConnectionMetadataSchema,
|
|
21
|
+
localFolderOpaqueIdentitySchema,
|
|
20
22
|
localFolderSourceCapabilities,
|
|
21
23
|
localFolderSourceId,
|
|
22
24
|
localFolderSourceMetadata,
|
|
@@ -30,12 +32,15 @@ export default defineAction({
|
|
|
30
32
|
description:
|
|
31
33
|
"Connect an opaque trusted local-folder handle to a canonical Files database. The browser or Desktop bridge keeps the real handle/path; Content stores only safe connection metadata.",
|
|
32
34
|
schema: z.object({
|
|
33
|
-
connectionId:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
.max(300)
|
|
37
|
-
.describe("Stable opaque ID from the trusted browser/Desktop registry"),
|
|
35
|
+
connectionId: localFolderOpaqueIdentitySchema.describe(
|
|
36
|
+
"Stable opaque ID from the trusted browser/Desktop registry",
|
|
37
|
+
),
|
|
38
38
|
label: z.string().min(1).max(200).describe("User-visible folder label"),
|
|
39
|
+
connectionMetadata: localFolderSourceConnectionMetadataSchema
|
|
40
|
+
.optional()
|
|
41
|
+
.describe(
|
|
42
|
+
"Safe repository/working-copy identity from the trusted local bridge; raw paths and handles are not accepted.",
|
|
43
|
+
),
|
|
39
44
|
spaceId: z
|
|
40
45
|
.string()
|
|
41
46
|
.optional()
|
|
@@ -70,9 +75,10 @@ export default defineAction({
|
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
const db = getDb();
|
|
73
|
-
const connectionId = args.connectionId
|
|
78
|
+
const connectionId = args.connectionId;
|
|
74
79
|
const label = args.label.trim();
|
|
75
80
|
const truthPolicy = args.truthPolicy as ContentDatabaseSourceTruthPolicy;
|
|
81
|
+
const connectionMetadata = args.connectionMetadata;
|
|
76
82
|
if (args.dryRun) {
|
|
77
83
|
const [existing] = await db
|
|
78
84
|
.select({
|
|
@@ -185,9 +191,18 @@ export default defineAction({
|
|
|
185
191
|
sourceTable: connectionId,
|
|
186
192
|
syncState: "linked",
|
|
187
193
|
freshness: "unknown",
|
|
188
|
-
capabilitiesJson: JSON.stringify(
|
|
194
|
+
capabilitiesJson: JSON.stringify(
|
|
195
|
+
localFolderSourceCapabilities({
|
|
196
|
+
liveBridgeEnabled: connectionMetadata?.liveBridgeEnabled,
|
|
197
|
+
}),
|
|
198
|
+
),
|
|
189
199
|
metadataJson: JSON.stringify(
|
|
190
|
-
localFolderSourceMetadata({
|
|
200
|
+
localFolderSourceMetadata({
|
|
201
|
+
connectionId,
|
|
202
|
+
label,
|
|
203
|
+
truthPolicy,
|
|
204
|
+
connectionMetadata,
|
|
205
|
+
}),
|
|
191
206
|
),
|
|
192
207
|
lastError: null,
|
|
193
208
|
createdAt: now,
|
|
@@ -80,7 +80,7 @@ export default defineAction({
|
|
|
80
80
|
documentId: string;
|
|
81
81
|
sourceDisplayKey: string | null;
|
|
82
82
|
sourceValuesJson: string | null;
|
|
83
|
-
|
|
83
|
+
sourceTable: string;
|
|
84
84
|
};
|
|
85
85
|
const remainingLocalRows: RemainingLocalRow[] = documentIds.length
|
|
86
86
|
? await tx
|
|
@@ -90,7 +90,7 @@ export default defineAction({
|
|
|
90
90
|
schema.contentDatabaseSourceRows.sourceDisplayKey,
|
|
91
91
|
sourceValuesJson:
|
|
92
92
|
schema.contentDatabaseSourceRows.sourceValuesJson,
|
|
93
|
-
|
|
93
|
+
sourceTable: schema.contentDatabaseSources.sourceTable,
|
|
94
94
|
})
|
|
95
95
|
.from(schema.contentDatabaseSourceRows)
|
|
96
96
|
.innerJoin(
|
|
@@ -152,7 +152,7 @@ export default defineAction({
|
|
|
152
152
|
sourceMode: "local-files",
|
|
153
153
|
sourceKind: "file",
|
|
154
154
|
sourcePath: relativePath,
|
|
155
|
-
sourceRootPath: remaining.
|
|
155
|
+
sourceRootPath: remaining.sourceTable,
|
|
156
156
|
sourceUpdatedAt: now,
|
|
157
157
|
updatedAt: now,
|
|
158
158
|
})
|
|
@@ -227,7 +227,7 @@ export default defineAction({
|
|
|
227
227
|
.select({
|
|
228
228
|
sourceDisplayKey: schema.contentDatabaseSourceRows.sourceDisplayKey,
|
|
229
229
|
sourceValuesJson: schema.contentDatabaseSourceRows.sourceValuesJson,
|
|
230
|
-
|
|
230
|
+
sourceTable: schema.contentDatabaseSources.sourceTable,
|
|
231
231
|
})
|
|
232
232
|
.from(schema.contentDatabaseSourceRows)
|
|
233
233
|
.innerJoin(
|
|
@@ -267,7 +267,7 @@ export default defineAction({
|
|
|
267
267
|
sourceMode: "local-files",
|
|
268
268
|
sourceKind: "file",
|
|
269
269
|
sourcePath: remainingPath ?? null,
|
|
270
|
-
sourceRootPath: remainingLocalRow.
|
|
270
|
+
sourceRootPath: remainingLocalRow.sourceTable,
|
|
271
271
|
sourceUpdatedAt: now,
|
|
272
272
|
updatedAt: now,
|
|
273
273
|
}
|