@agent-native/core 0.102.2 → 0.103.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.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +6 -0
- package/corpus/core/docs/content/local-file-mode.mdx +99 -489
- package/corpus/core/docs/content/locales/ar-SA/local-file-mode.mdx +35 -490
- package/corpus/core/docs/content/locales/ar-SA/template-content.mdx +10 -24
- package/corpus/core/docs/content/locales/de-DE/local-file-mode.mdx +37 -491
- package/corpus/core/docs/content/locales/de-DE/template-content.mdx +10 -24
- package/corpus/core/docs/content/locales/es-ES/local-file-mode.mdx +35 -493
- package/corpus/core/docs/content/locales/es-ES/template-content.mdx +10 -24
- package/corpus/core/docs/content/locales/fr-FR/local-file-mode.mdx +38 -491
- package/corpus/core/docs/content/locales/fr-FR/template-content.mdx +10 -24
- package/corpus/core/docs/content/locales/hi-IN/local-file-mode.mdx +33 -492
- package/corpus/core/docs/content/locales/hi-IN/template-content.mdx +10 -24
- package/corpus/core/docs/content/locales/ja-JP/local-file-mode.mdx +36 -492
- package/corpus/core/docs/content/locales/ja-JP/template-content.mdx +10 -24
- package/corpus/core/docs/content/locales/ko-KR/local-file-mode.mdx +35 -490
- package/corpus/core/docs/content/locales/ko-KR/template-content.mdx +10 -24
- package/corpus/core/docs/content/locales/pt-BR/local-file-mode.mdx +35 -493
- package/corpus/core/docs/content/locales/pt-BR/template-content.mdx +10 -24
- package/corpus/core/docs/content/locales/zh-CN/local-file-mode.mdx +31 -489
- package/corpus/core/docs/content/locales/zh-CN/template-content.mdx +9 -24
- package/corpus/core/docs/content/locales/zh-TW/local-file-mode.mdx +31 -489
- package/corpus/core/docs/content/locales/zh-TW/template-content.mdx +9 -24
- package/corpus/core/docs/content/template-content.mdx +26 -42
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/content-local.ts +86 -18
- package/corpus/core/src/cli/index.ts +2 -2
- package/corpus/core/src/cli/skills-content/content-skill.ts +36 -25
- package/corpus/core/src/cli/skills.ts +47 -13
- package/corpus/core/src/local-artifacts/index.ts +65 -0
- package/corpus/templates/assets/app/components/library/GenerationPresetsPanel.tsx +325 -0
- package/corpus/templates/assets/app/hooks/use-navigation-state.ts +12 -0
- package/corpus/templates/assets/app/i18n/zh-TW.ts +21 -0
- package/corpus/templates/assets/app/i18n-data.ts +215 -0
- package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +19 -479
- package/corpus/templates/assets/app/routes/brand-kits.$id_.presets.$presetId.tsx +1 -1
- package/corpus/templates/assets/app/routes/brand-kits.$id_.settings.tsx +446 -0
- package/corpus/templates/clips/app/components/editor/editor-layout.tsx +12 -2
- package/corpus/templates/clips/app/components/player/settings-panel.tsx +30 -12
- package/corpus/templates/clips/app/components/player/video-player.tsx +5 -1
- package/corpus/templates/clips/app/components/recorder/microphone-visualizer.tsx +22 -3
- package/corpus/templates/clips/app/i18n/ar-SA.ts +3 -2
- package/corpus/templates/clips/app/i18n/de-DE.ts +4 -2
- package/corpus/templates/clips/app/i18n/en-US.ts +3 -2
- package/corpus/templates/clips/app/i18n/es-ES.ts +4 -2
- package/corpus/templates/clips/app/i18n/fr-FR.ts +3 -2
- package/corpus/templates/clips/app/i18n/hi-IN.ts +3 -2
- package/corpus/templates/clips/app/i18n/ja-JP.ts +4 -1
- package/corpus/templates/clips/app/i18n/ko-KR.ts +3 -2
- package/corpus/templates/clips/app/i18n/pt-BR.ts +4 -2
- package/corpus/templates/clips/app/i18n/zh-CN.ts +3 -2
- package/corpus/templates/clips/app/i18n/zh-TW.ts +3 -2
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +27 -1
- package/corpus/templates/clips/server/lib/share-password.ts +6 -3
- package/corpus/templates/content/.agents/skills/content/SKILL.md +26 -39
- package/corpus/templates/content/AGENTS.md +25 -30
- package/corpus/templates/content/actions/_builder-docs-client.ts +23 -0
- package/corpus/templates/content/actions/_content-database-source-adapters.ts +21 -3
- package/corpus/templates/content/actions/_content-files.ts +338 -0
- package/corpus/templates/content/actions/_content-space-access.ts +135 -0
- package/corpus/templates/content/actions/_content-spaces.ts +640 -0
- package/corpus/templates/content/actions/_database-source-utils.ts +90 -7
- package/corpus/templates/content/actions/_database-utils.ts +7 -1
- package/corpus/templates/content/actions/_local-file-documents.ts +0 -9
- package/corpus/templates/content/actions/_local-folder-source.ts +53 -0
- package/corpus/templates/content/actions/_property-utils.ts +9 -4
- package/corpus/templates/content/actions/add-database-item.ts +108 -65
- package/corpus/templates/content/actions/backfill-content-files.ts +22 -0
- package/corpus/templates/content/actions/connect-local-folder-source.ts +221 -0
- package/corpus/templates/content/actions/create-content-database.ts +129 -3
- package/corpus/templates/content/actions/create-document.ts +65 -55
- package/corpus/templates/content/actions/create-inline-content-database.ts +6 -1
- package/corpus/templates/content/actions/delete-content-database.ts +3 -0
- package/corpus/templates/content/actions/delete-document.ts +7 -11
- package/corpus/templates/content/actions/disconnect-local-folder-source.ts +175 -0
- package/corpus/templates/content/actions/duplicate-database-item.ts +10 -0
- package/corpus/templates/content/actions/duplicate-database-items.ts +13 -0
- package/corpus/templates/content/actions/edit-document.ts +2 -28
- package/corpus/templates/content/actions/ensure-content-spaces.ts +26 -0
- package/corpus/templates/content/actions/export-content-source.ts +47 -9
- package/corpus/templates/content/actions/get-document.ts +0 -14
- package/corpus/templates/content/actions/import-content-source.ts +43 -3
- package/corpus/templates/content/actions/list-content-spaces.ts +103 -0
- package/corpus/templates/content/actions/list-documents.ts +12 -9
- package/corpus/templates/content/actions/move-document.ts +3 -14
- package/corpus/templates/content/actions/pull-document.ts +0 -23
- package/corpus/templates/content/actions/remove-local-file-source.ts +50 -29
- package/corpus/templates/content/actions/resolve-local-folder-conflict.ts +442 -0
- package/corpus/templates/content/actions/reveal-local-source-file.ts +6 -17
- package/corpus/templates/content/actions/search-documents.ts +0 -34
- package/corpus/templates/content/actions/share-local-file-document.ts +25 -1
- package/corpus/templates/content/actions/submit-content-database-form.ts +12 -0
- package/corpus/templates/content/actions/sync-local-folder-source.ts +731 -0
- package/corpus/templates/content/actions/sync-manifest-local-folder-source.ts +123 -0
- package/corpus/templates/content/actions/update-content-database-view.ts +10 -1
- package/corpus/templates/content/actions/update-document.ts +0 -15
- package/corpus/templates/content/actions/view-screen.ts +2 -50
- package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +76 -62
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +70 -22
- package/corpus/templates/content/app/components/editor/database/grouping.ts +2 -1
- package/corpus/templates/content/app/components/editor/database/settings.tsx +23 -20
- package/corpus/templates/content/app/components/editor/database/shared.tsx +2 -0
- package/corpus/templates/content/app/components/editor/database/sidebar.tsx +330 -0
- package/corpus/templates/content/app/components/editor/database/types.ts +1 -0
- package/corpus/templates/content/app/components/editor/database/view-config.ts +3 -1
- package/corpus/templates/content/app/components/layout/Layout.tsx +25 -1
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +209 -113
- package/corpus/templates/content/app/components/sidebar/select-content-space.ts +31 -0
- package/corpus/templates/content/app/hooks/use-content-database.ts +16 -0
- package/corpus/templates/content/app/hooks/use-content-spaces.ts +39 -0
- package/corpus/templates/content/app/i18n/zh-TW.ts +11 -0
- package/corpus/templates/content/app/i18n-data.ts +75 -0
- package/corpus/templates/content/app/routes/_app.local-files.tsx +174 -65
- package/corpus/templates/content/changelog/2026-07-16-content-callouts-can-now-be-reviewed-and-published-to-builder.md +6 -0
- package/corpus/templates/content/changelog/2026-07-16-heading-5-and-heading-6-are-available-from-the-editor-slash-.md +6 -0
- package/corpus/templates/content/changelog/2026-07-16-organize-every-workspace-through-a-customizable-files-databa.md +6 -0
- package/corpus/templates/content/parity/matrix.md +2 -1
- package/corpus/templates/content/parity/matrix.ts +37 -1
- package/corpus/templates/content/scripts/dev-database.mjs +7 -5
- package/corpus/templates/content/server/db/schema.ts +113 -40
- package/corpus/templates/content/server/lib/document-context.ts +15 -1
- package/corpus/templates/content/server/lib/notion-sync.ts +6 -0
- package/corpus/templates/content/server/plugins/db.ts +153 -0
- package/corpus/templates/content/shared/api.ts +17 -3
- package/corpus/templates/content/shared/builder-mdx.ts +163 -16
- package/corpus/templates/design/.agents/skills/design-templates/SKILL.md +59 -0
- package/corpus/templates/design/AGENTS.md +6 -1
- package/corpus/templates/design/actions/create-design-from-template.ts +19 -7
- package/corpus/templates/design/actions/list-design-templates.ts +69 -21
- package/corpus/templates/design/actions/navigate.ts +2 -2
- package/corpus/templates/design/actions/view-screen.ts +15 -2
- package/corpus/templates/design/agent-native.app-skill.json +5 -0
- package/corpus/templates/design/app/components/editor/PromptDialog.tsx +281 -56
- package/corpus/templates/design/app/components/templates/TemplatePreview.tsx +14 -2
- package/corpus/templates/design/app/hooks/use-navigation-state.ts +3 -2
- package/corpus/templates/design/app/i18n/zh-TW.ts +3 -3
- package/corpus/templates/design/app/i18n-data.ts +21 -2
- package/corpus/templates/design/app/i18n-template-feature.ts +218 -10
- package/corpus/templates/design/app/pages/Index.tsx +195 -24
- package/corpus/templates/design/app/pages/Templates.tsx +144 -55
- package/corpus/templates/design/app/pages/design-editor/generation-prompt-directives.ts +12 -1
- package/corpus/templates/design/changelog/2026-07-14-templates-now-appear-in-the-new-design-picker-with-previews-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-15-templates-now-open-immediately-after-copying-even-while-the-.md +6 -0
- package/corpus/templates/design/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/mail/AGENTS.md +23 -18
- package/corpus/templates/mail/actions/bulk-archive.ts +32 -28
- package/corpus/templates/mail/actions/mark-read.ts +131 -20
- package/corpus/templates/mail/actions/mark-thread-read.ts +1 -1
- package/corpus/templates/mail/actions/move-email.ts +30 -24
- package/corpus/templates/mail/actions/send-email.ts +73 -68
- package/corpus/templates/mail/changelog/2026-07-15-unread-inbox-cleanup-now-completes-reliably.md +6 -0
- package/corpus/templates/mail/server/handlers/emails.ts +218 -209
- package/corpus/templates/mail/server/lib/bulk-mark-read.ts +30 -0
- package/corpus/templates/mail/server/lib/email-state.ts +198 -113
- package/corpus/templates/mail/server/lib/google-auth.ts +218 -25
- package/corpus/templates/mail/server/lib/jobs.ts +69 -74
- package/corpus/templates/mail/server/lib/local-email-store.ts +265 -0
- package/corpus/templates/mail/server/plugins/agent-chat.ts +6 -0
- package/dist/cli/content-local.d.ts +1 -0
- package/dist/cli/content-local.d.ts.map +1 -1
- package/dist/cli/content-local.js +75 -17
- package/dist/cli/content-local.js.map +1 -1
- package/dist/cli/index.js +2 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/skills-content/content-skill.d.ts +1 -1
- package/dist/cli/skills-content/content-skill.d.ts.map +1 -1
- package/dist/cli/skills-content/content-skill.js +36 -25
- package/dist/cli/skills-content/content-skill.js.map +1 -1
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +47 -14
- package/dist/cli/skills.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/local-artifacts/index.d.ts +10 -0
- package/dist/local-artifacts/index.d.ts.map +1 -1
- package/dist/local-artifacts/index.js +39 -0
- package/dist/local-artifacts/index.js.map +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/docs/content/local-file-mode.mdx +99 -489
- package/docs/content/locales/ar-SA/local-file-mode.mdx +35 -490
- package/docs/content/locales/ar-SA/template-content.mdx +10 -24
- package/docs/content/locales/de-DE/local-file-mode.mdx +37 -491
- package/docs/content/locales/de-DE/template-content.mdx +10 -24
- package/docs/content/locales/es-ES/local-file-mode.mdx +35 -493
- package/docs/content/locales/es-ES/template-content.mdx +10 -24
- package/docs/content/locales/fr-FR/local-file-mode.mdx +38 -491
- package/docs/content/locales/fr-FR/template-content.mdx +10 -24
- package/docs/content/locales/hi-IN/local-file-mode.mdx +33 -492
- package/docs/content/locales/hi-IN/template-content.mdx +10 -24
- package/docs/content/locales/ja-JP/local-file-mode.mdx +36 -492
- package/docs/content/locales/ja-JP/template-content.mdx +10 -24
- package/docs/content/locales/ko-KR/local-file-mode.mdx +35 -490
- package/docs/content/locales/ko-KR/template-content.mdx +10 -24
- package/docs/content/locales/pt-BR/local-file-mode.mdx +35 -493
- package/docs/content/locales/pt-BR/template-content.mdx +10 -24
- package/docs/content/locales/zh-CN/local-file-mode.mdx +31 -489
- package/docs/content/locales/zh-CN/template-content.mdx +9 -24
- package/docs/content/locales/zh-TW/local-file-mode.mdx +31 -489
- package/docs/content/locales/zh-TW/template-content.mdx +9 -24
- package/docs/content/template-content.mdx +26 -42
- package/package.json +1 -1
- package/corpus/templates/assets/app/components/library/EditLibraryDialog.tsx +0 -126
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { getUserSetting, putUserSetting } from "@agent-native/core/settings";
|
|
15
15
|
import { decodeCommonHtmlEntities } from "@shared/markdown.js";
|
|
16
16
|
|
|
17
|
+
import type { BulkMarkReadResult } from "./bulk-mark-read.js";
|
|
17
18
|
import {
|
|
18
19
|
createOAuth2Client,
|
|
19
20
|
gmailGetProfile,
|
|
@@ -31,6 +32,7 @@ import {
|
|
|
31
32
|
peopleGetProfile,
|
|
32
33
|
} from "./google-api.js";
|
|
33
34
|
import { resolveGoogleSenderIdentity } from "./sender-identity.js";
|
|
35
|
+
import { invalidateThreadCache } from "./thread-cache.js";
|
|
34
36
|
|
|
35
37
|
const SCOPES = [
|
|
36
38
|
"https://www.googleapis.com/auth/gmail.readonly",
|
|
@@ -1776,25 +1778,44 @@ export async function fetchGmailLabelMap(
|
|
|
1776
1778
|
// instead of one per message.
|
|
1777
1779
|
const GMAIL_BATCH_MODIFY_MAX_IDS = 1000;
|
|
1778
1780
|
|
|
1781
|
+
interface GmailBatchModifyResult {
|
|
1782
|
+
succeeded: string[];
|
|
1783
|
+
failed: Array<{ id: string; error: string }>;
|
|
1784
|
+
batchCount: number;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1779
1787
|
async function gmailBatchModify(
|
|
1780
1788
|
accessToken: string,
|
|
1781
1789
|
ids: string[],
|
|
1782
1790
|
addLabelIds?: string[],
|
|
1783
1791
|
removeLabelIds?: string[],
|
|
1784
|
-
): Promise<
|
|
1785
|
-
|
|
1792
|
+
): Promise<GmailBatchModifyResult> {
|
|
1793
|
+
const result: GmailBatchModifyResult = {
|
|
1794
|
+
succeeded: [],
|
|
1795
|
+
failed: [],
|
|
1796
|
+
batchCount: 0,
|
|
1797
|
+
};
|
|
1798
|
+
if (ids.length === 0) return result;
|
|
1786
1799
|
for (let i = 0; i < ids.length; i += GMAIL_BATCH_MODIFY_MAX_IDS) {
|
|
1787
1800
|
const chunk = ids.slice(i, i + GMAIL_BATCH_MODIFY_MAX_IDS);
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1801
|
+
result.batchCount += 1;
|
|
1802
|
+
try {
|
|
1803
|
+
await googleFetch(
|
|
1804
|
+
"https://gmail.googleapis.com/gmail/v1/users/me/messages/batchModify",
|
|
1805
|
+
accessToken,
|
|
1806
|
+
{
|
|
1807
|
+
method: "POST",
|
|
1808
|
+
headers: { "Content-Type": "application/json" },
|
|
1809
|
+
body: JSON.stringify({ ids: chunk, addLabelIds, removeLabelIds }),
|
|
1810
|
+
},
|
|
1811
|
+
);
|
|
1812
|
+
result.succeeded.push(...chunk);
|
|
1813
|
+
} catch (err: any) {
|
|
1814
|
+
const error = err?.message ?? "batchModify failed";
|
|
1815
|
+
result.failed.push(...chunk.map((id) => ({ id, error })));
|
|
1816
|
+
}
|
|
1797
1817
|
}
|
|
1818
|
+
return result;
|
|
1798
1819
|
}
|
|
1799
1820
|
|
|
1800
1821
|
export interface BatchModifyTarget {
|
|
@@ -1826,7 +1847,7 @@ export async function gmailBatchModifyByAccount(
|
|
|
1826
1847
|
): Promise<BatchModifyByAccountResult> {
|
|
1827
1848
|
const byAccount = new Map<string, BatchModifyTarget[]>();
|
|
1828
1849
|
for (const target of targets) {
|
|
1829
|
-
const key = target.accountEmail ||
|
|
1850
|
+
const key = target.accountEmail || "";
|
|
1830
1851
|
const list = byAccount.get(key);
|
|
1831
1852
|
if (list) list.push(target);
|
|
1832
1853
|
else byAccount.set(key, [target]);
|
|
@@ -1836,24 +1857,18 @@ export async function gmailBatchModifyByAccount(
|
|
|
1836
1857
|
const failed: Array<{ id: string; error: string }> = [];
|
|
1837
1858
|
|
|
1838
1859
|
for (const [accountEmail, accountTargets] of byAccount) {
|
|
1839
|
-
const client = await getClient(accountEmail);
|
|
1840
|
-
if (!client) {
|
|
1841
|
-
for (const t of accountTargets) {
|
|
1842
|
-
failed.push({
|
|
1843
|
-
id: t.id,
|
|
1844
|
-
error: `Account ${accountEmail} not connected`,
|
|
1845
|
-
});
|
|
1846
|
-
}
|
|
1847
|
-
continue;
|
|
1848
|
-
}
|
|
1849
1860
|
try {
|
|
1850
|
-
|
|
1851
|
-
|
|
1861
|
+
const accessToken = accountEmail
|
|
1862
|
+
? await getOwnedAccountAccessToken(ownerEmail, accountEmail)
|
|
1863
|
+
: await getDefaultOwnedAccountAccessToken(ownerEmail);
|
|
1864
|
+
const result = await gmailBatchModify(
|
|
1865
|
+
accessToken,
|
|
1852
1866
|
accountTargets.map((t) => t.id),
|
|
1853
1867
|
addLabelIds,
|
|
1854
1868
|
removeLabelIds,
|
|
1855
1869
|
);
|
|
1856
|
-
|
|
1870
|
+
succeeded.push(...result.succeeded);
|
|
1871
|
+
failed.push(...result.failed);
|
|
1857
1872
|
} catch (err: any) {
|
|
1858
1873
|
const message = err?.message ?? "batchModify failed";
|
|
1859
1874
|
for (const t of accountTargets) failed.push({ id: t.id, error: message });
|
|
@@ -1863,6 +1878,184 @@ export async function gmailBatchModifyByAccount(
|
|
|
1863
1878
|
return { succeeded, failed };
|
|
1864
1879
|
}
|
|
1865
1880
|
|
|
1881
|
+
interface GmailMessageReference {
|
|
1882
|
+
id: string;
|
|
1883
|
+
threadId: string;
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
async function getDefaultOwnedAccountAccessToken(
|
|
1887
|
+
ownerEmail: string,
|
|
1888
|
+
): Promise<string> {
|
|
1889
|
+
const accounts = await listOAuthAccountsByOwner("google", ownerEmail);
|
|
1890
|
+
const account =
|
|
1891
|
+
accounts.find(
|
|
1892
|
+
(candidate) =>
|
|
1893
|
+
candidate.accountId.toLowerCase() === ownerEmail.toLowerCase(),
|
|
1894
|
+
) ?? accounts[0];
|
|
1895
|
+
if (!account) throw new Error("No Google account connected");
|
|
1896
|
+
const tokens = account.tokens as unknown as GoogleTokens;
|
|
1897
|
+
if (!tokens?.access_token && !tokens?.refresh_token) {
|
|
1898
|
+
throw new Error(`No valid access token for ${account.accountId}`);
|
|
1899
|
+
}
|
|
1900
|
+
return getValidAccessToken(account.accountId, tokens, ownerEmail);
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
async function getOwnedAccountAccessToken(
|
|
1904
|
+
ownerEmail: string,
|
|
1905
|
+
accountEmail: string,
|
|
1906
|
+
): Promise<string> {
|
|
1907
|
+
const accounts = await listOAuthAccountsByOwner("google", ownerEmail);
|
|
1908
|
+
const account = accounts.find(
|
|
1909
|
+
(candidate) =>
|
|
1910
|
+
candidate.accountId.toLowerCase() === accountEmail.toLowerCase(),
|
|
1911
|
+
);
|
|
1912
|
+
if (!account) {
|
|
1913
|
+
throw new Error(`Account ${accountEmail} is not connected for this user`);
|
|
1914
|
+
}
|
|
1915
|
+
const tokens = account.tokens as unknown as GoogleTokens;
|
|
1916
|
+
if (!tokens?.access_token && !tokens?.refresh_token) {
|
|
1917
|
+
throw new Error(`No valid access token for ${account.accountId}`);
|
|
1918
|
+
}
|
|
1919
|
+
return getValidAccessToken(account.accountId, tokens, ownerEmail);
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
async function listGmailMessageReferences(
|
|
1923
|
+
accessToken: string,
|
|
1924
|
+
query: string,
|
|
1925
|
+
): Promise<GmailMessageReference[]> {
|
|
1926
|
+
const references = new Map<string, GmailMessageReference>();
|
|
1927
|
+
const seenPageTokens = new Set<string>();
|
|
1928
|
+
let pageToken: string | undefined;
|
|
1929
|
+
|
|
1930
|
+
do {
|
|
1931
|
+
const response = await gmailListMessages(accessToken, {
|
|
1932
|
+
q: query,
|
|
1933
|
+
maxResults: 500,
|
|
1934
|
+
pageToken,
|
|
1935
|
+
});
|
|
1936
|
+
for (const message of response.messages ?? []) {
|
|
1937
|
+
if (
|
|
1938
|
+
typeof message.id !== "string" ||
|
|
1939
|
+
typeof message.threadId !== "string"
|
|
1940
|
+
)
|
|
1941
|
+
continue;
|
|
1942
|
+
references.set(message.id, {
|
|
1943
|
+
id: message.id,
|
|
1944
|
+
threadId: message.threadId,
|
|
1945
|
+
});
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
const nextPageToken = response.nextPageToken;
|
|
1949
|
+
if (typeof nextPageToken !== "string" || !nextPageToken) break;
|
|
1950
|
+
if (seenPageTokens.has(nextPageToken)) {
|
|
1951
|
+
throw new Error(
|
|
1952
|
+
"Gmail repeated a pagination token while listing unread mail",
|
|
1953
|
+
);
|
|
1954
|
+
}
|
|
1955
|
+
seenPageTokens.add(nextPageToken);
|
|
1956
|
+
pageToken = nextPageToken;
|
|
1957
|
+
} while (pageToken);
|
|
1958
|
+
|
|
1959
|
+
return [...references.values()];
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
export async function markAllUnreadReadForAccount(input: {
|
|
1963
|
+
ownerEmail: string;
|
|
1964
|
+
accountEmail: string;
|
|
1965
|
+
excludeThreadIds: string[];
|
|
1966
|
+
}): Promise<BulkMarkReadResult> {
|
|
1967
|
+
const { ownerEmail, accountEmail } = input;
|
|
1968
|
+
const excludedThreadIds = new Set(input.excludeThreadIds.filter(Boolean));
|
|
1969
|
+
const accessToken = await getOwnedAccountAccessToken(
|
|
1970
|
+
ownerEmail,
|
|
1971
|
+
accountEmail,
|
|
1972
|
+
);
|
|
1973
|
+
const matched = await listGmailMessageReferences(accessToken, "is:unread");
|
|
1974
|
+
const excluded = matched.filter((message) =>
|
|
1975
|
+
excludedThreadIds.has(message.threadId),
|
|
1976
|
+
);
|
|
1977
|
+
const selected = matched.filter(
|
|
1978
|
+
(message) => !excludedThreadIds.has(message.threadId),
|
|
1979
|
+
);
|
|
1980
|
+
|
|
1981
|
+
const mutation = await gmailBatchModify(
|
|
1982
|
+
accessToken,
|
|
1983
|
+
selected.map((message) => message.id),
|
|
1984
|
+
undefined,
|
|
1985
|
+
["UNREAD"],
|
|
1986
|
+
);
|
|
1987
|
+
for (const threadId of new Set(selected.map((message) => message.threadId))) {
|
|
1988
|
+
invalidateThreadCache(ownerEmail, threadId);
|
|
1989
|
+
}
|
|
1990
|
+
invalidateListCacheForOwner(ownerEmail);
|
|
1991
|
+
|
|
1992
|
+
let remaining: GmailMessageReference[];
|
|
1993
|
+
try {
|
|
1994
|
+
remaining = await listGmailMessageReferences(accessToken, "is:unread");
|
|
1995
|
+
} catch (err: any) {
|
|
1996
|
+
return {
|
|
1997
|
+
mode: "all-unread",
|
|
1998
|
+
accountEmail,
|
|
1999
|
+
matchedMessages: matched.length,
|
|
2000
|
+
matchedThreads: new Set(matched.map((message) => message.threadId)).size,
|
|
2001
|
+
excludedMessages: excluded.length,
|
|
2002
|
+
excludedThreads: new Set(excluded.map((message) => message.threadId))
|
|
2003
|
+
.size,
|
|
2004
|
+
changedMessages: mutation.succeeded.length,
|
|
2005
|
+
batchCount: mutation.batchCount,
|
|
2006
|
+
failures: mutation.failed,
|
|
2007
|
+
remainingUnreadMessages: null,
|
|
2008
|
+
remainingUnreadThreads: null,
|
|
2009
|
+
remainingProtectedMessages: null,
|
|
2010
|
+
remainingProtectedThreads: null,
|
|
2011
|
+
unexpectedUnreadMessages: null,
|
|
2012
|
+
unexpectedUnreadThreads: null,
|
|
2013
|
+
newUnreadMessages: null,
|
|
2014
|
+
newUnreadThreads: null,
|
|
2015
|
+
verificationComplete: false,
|
|
2016
|
+
verificationError: err?.message ?? "Unread verification failed",
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
const matchedIds = new Set(matched.map((message) => message.id));
|
|
2020
|
+
const selectedIds = new Set(selected.map((message) => message.id));
|
|
2021
|
+
const remainingProtected = remaining.filter((message) =>
|
|
2022
|
+
excludedThreadIds.has(message.threadId),
|
|
2023
|
+
);
|
|
2024
|
+
const unexpectedRemaining = remaining.filter((message) =>
|
|
2025
|
+
selectedIds.has(message.id),
|
|
2026
|
+
);
|
|
2027
|
+
const newUnread = remaining.filter((message) => !matchedIds.has(message.id));
|
|
2028
|
+
|
|
2029
|
+
return {
|
|
2030
|
+
mode: "all-unread",
|
|
2031
|
+
accountEmail,
|
|
2032
|
+
matchedMessages: matched.length,
|
|
2033
|
+
matchedThreads: new Set(matched.map((message) => message.threadId)).size,
|
|
2034
|
+
excludedMessages: excluded.length,
|
|
2035
|
+
excludedThreads: new Set(excluded.map((message) => message.threadId)).size,
|
|
2036
|
+
changedMessages: mutation.succeeded.length,
|
|
2037
|
+
batchCount: mutation.batchCount,
|
|
2038
|
+
failures: mutation.failed,
|
|
2039
|
+
remainingUnreadMessages: remaining.length,
|
|
2040
|
+
remainingUnreadThreads: new Set(
|
|
2041
|
+
remaining.map((message) => message.threadId),
|
|
2042
|
+
).size,
|
|
2043
|
+
remainingProtectedMessages: remainingProtected.length,
|
|
2044
|
+
remainingProtectedThreads: new Set(
|
|
2045
|
+
remainingProtected.map((message) => message.threadId),
|
|
2046
|
+
).size,
|
|
2047
|
+
unexpectedUnreadMessages: unexpectedRemaining.length,
|
|
2048
|
+
unexpectedUnreadThreads: new Set(
|
|
2049
|
+
unexpectedRemaining.map((message) => message.threadId),
|
|
2050
|
+
).size,
|
|
2051
|
+
newUnreadMessages: newUnread.length,
|
|
2052
|
+
newUnreadThreads: new Set(newUnread.map((message) => message.threadId))
|
|
2053
|
+
.size,
|
|
2054
|
+
verificationComplete:
|
|
2055
|
+
mutation.failed.length === 0 && unexpectedRemaining.length === 0,
|
|
2056
|
+
};
|
|
2057
|
+
}
|
|
2058
|
+
|
|
1866
2059
|
/** Extract regular (non-inline) attachments from a Gmail message payload */
|
|
1867
2060
|
function getAttachments(
|
|
1868
2061
|
payload: any,
|
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
listOAuthAccountsByOwner,
|
|
6
6
|
setOAuthDisplayName,
|
|
7
7
|
} from "@agent-native/core/oauth-tokens";
|
|
8
|
-
import { getUserSetting, putUserSetting } from "@agent-native/core/settings";
|
|
9
8
|
import { markdownPreviewSnippet } from "@shared/markdown.js";
|
|
10
9
|
import type { ComposeAttachment, EmailMessage } from "@shared/types.js";
|
|
11
10
|
import { and, eq, inArray, isNull, lte, or } from "drizzle-orm";
|
|
@@ -28,6 +27,11 @@ import {
|
|
|
28
27
|
getOAuth2Credentials,
|
|
29
28
|
setAccountDisplayName,
|
|
30
29
|
} from "./google-auth.js";
|
|
30
|
+
import {
|
|
31
|
+
readLocalEmails as readEmails,
|
|
32
|
+
withLocalEmailMutationLock,
|
|
33
|
+
writeLocalEmails as writeEmails,
|
|
34
|
+
} from "./local-email-store.js";
|
|
31
35
|
import {
|
|
32
36
|
bodyToHtml as outgoingBodyToHtml,
|
|
33
37
|
buildRawEmail as buildOutgoingRawEmail,
|
|
@@ -135,21 +139,6 @@ async function getFirstAccountToken(
|
|
|
135
139
|
return null;
|
|
136
140
|
}
|
|
137
141
|
|
|
138
|
-
async function readEmails(ownerEmail: string): Promise<EmailMessage[]> {
|
|
139
|
-
const data = await getUserSetting(ownerEmail, "local-emails");
|
|
140
|
-
if (data && Array.isArray((data as any).emails)) {
|
|
141
|
-
return (data as any).emails;
|
|
142
|
-
}
|
|
143
|
-
return [];
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
async function writeEmails(
|
|
147
|
-
ownerEmail: string,
|
|
148
|
-
emails: EmailMessage[],
|
|
149
|
-
): Promise<void> {
|
|
150
|
-
await putUserSetting(ownerEmail, "local-emails", { emails });
|
|
151
|
-
}
|
|
152
|
-
|
|
153
142
|
async function fetchLabelMap(
|
|
154
143
|
accessToken: string,
|
|
155
144
|
): Promise<Map<string, string>> {
|
|
@@ -211,18 +200,20 @@ async function archiveThreadForSnooze(
|
|
|
211
200
|
}
|
|
212
201
|
}
|
|
213
202
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
203
|
+
await withLocalEmailMutationLock(ownerEmail, async () => {
|
|
204
|
+
const emails = await readEmails(ownerEmail);
|
|
205
|
+
for (let i = 0; i < emails.length; i++) {
|
|
206
|
+
const currentThreadId = emails[i].threadId || emails[i].id;
|
|
207
|
+
if (currentThreadId === threadId) {
|
|
208
|
+
emails[i] = {
|
|
209
|
+
...emails[i],
|
|
210
|
+
isArchived: true,
|
|
211
|
+
labelIds: emails[i].labelIds.filter((label) => label !== "inbox"),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
223
214
|
}
|
|
224
|
-
|
|
225
|
-
|
|
215
|
+
await writeEmails(ownerEmail, emails);
|
|
216
|
+
});
|
|
226
217
|
}
|
|
227
218
|
|
|
228
219
|
async function threadHasReplySinceSnooze(
|
|
@@ -396,22 +387,24 @@ export async function resurfaceEmail(
|
|
|
396
387
|
}
|
|
397
388
|
}
|
|
398
389
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
390
|
+
await withLocalEmailMutationLock(ownerEmail, async () => {
|
|
391
|
+
const emails = await readEmails(ownerEmail);
|
|
392
|
+
const targetThreadId = threadId || emailId;
|
|
393
|
+
for (let i = 0; i < emails.length; i++) {
|
|
394
|
+
const currentThreadId = emails[i].threadId || emails[i].id;
|
|
395
|
+
if (currentThreadId === targetThreadId) {
|
|
396
|
+
emails[i] = {
|
|
397
|
+
...emails[i],
|
|
398
|
+
isArchived: false,
|
|
399
|
+
isRead: false,
|
|
400
|
+
labelIds: emails[i].labelIds.includes("inbox")
|
|
401
|
+
? emails[i].labelIds
|
|
402
|
+
: ["inbox", ...emails[i].labelIds],
|
|
403
|
+
};
|
|
404
|
+
}
|
|
412
405
|
}
|
|
413
|
-
|
|
414
|
-
|
|
406
|
+
await writeEmails(ownerEmail, emails);
|
|
407
|
+
});
|
|
415
408
|
}
|
|
416
409
|
|
|
417
410
|
/**
|
|
@@ -638,39 +631,41 @@ export async function sendScheduledEmail(
|
|
|
638
631
|
if (!fallbackOwner) {
|
|
639
632
|
throw new Error("scheduleEmail: no owner email available");
|
|
640
633
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
634
|
+
await withLocalEmailMutationLock(fallbackOwner, async () => {
|
|
635
|
+
const emails = await readEmails(fallbackOwner);
|
|
636
|
+
emails.push({
|
|
637
|
+
id: `msg-${nanoid(8)}`,
|
|
638
|
+
threadId: threadId || `thread-${nanoid(8)}`,
|
|
639
|
+
from: { name: fallbackOwner, email: fallbackOwner },
|
|
640
|
+
to: to.split(",").map((item) => {
|
|
641
|
+
const email = item.trim();
|
|
642
|
+
return { name: email, email };
|
|
643
|
+
}),
|
|
644
|
+
subject,
|
|
645
|
+
snippet: markdownPreviewSnippet(body),
|
|
646
|
+
body,
|
|
647
|
+
bodyHtml: outgoingBodyToHtml(body),
|
|
648
|
+
date: new Date().toISOString(),
|
|
649
|
+
isRead: true,
|
|
650
|
+
isStarred: false,
|
|
651
|
+
isSent: true,
|
|
652
|
+
isArchived: false,
|
|
653
|
+
isTrashed: false,
|
|
654
|
+
labelIds: ["sent"],
|
|
655
|
+
...(attachments.length > 0
|
|
656
|
+
? {
|
|
657
|
+
attachments: attachments.map((att) => ({
|
|
658
|
+
id: att.filename,
|
|
659
|
+
filename: att.originalName,
|
|
660
|
+
mimeType: att.mimeType,
|
|
661
|
+
size: att.size,
|
|
662
|
+
url: att.url,
|
|
663
|
+
})),
|
|
664
|
+
}
|
|
665
|
+
: {}),
|
|
666
|
+
});
|
|
667
|
+
await writeEmails(fallbackOwner, emails);
|
|
672
668
|
});
|
|
673
|
-
await writeEmails(fallbackOwner, emails);
|
|
674
669
|
}
|
|
675
670
|
|
|
676
671
|
export async function cancelScheduledJobForOwner(
|