@agent-native/core 0.98.7 → 0.98.9
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 +12 -0
- package/corpus/core/docs/content/external-agents.mdx +17 -11
- package/corpus/core/docs/content/locales/ar-SA/external-agents.mdx +6 -2
- package/corpus/core/docs/content/locales/de-DE/external-agents.mdx +6 -2
- package/corpus/core/docs/content/locales/es-ES/external-agents.mdx +6 -2
- package/corpus/core/docs/content/locales/fr-FR/external-agents.mdx +6 -2
- package/corpus/core/docs/content/locales/hi-IN/external-agents.mdx +6 -2
- package/corpus/core/docs/content/locales/ja-JP/external-agents.mdx +6 -2
- package/corpus/core/docs/content/locales/ko-KR/external-agents.mdx +6 -2
- package/corpus/core/docs/content/locales/pt-BR/external-agents.mdx +6 -2
- package/corpus/core/docs/content/locales/zh-CN/external-agents.mdx +6 -2
- package/corpus/core/docs/content/locales/zh-TW/external-agents.mdx +6 -2
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/session-replay.ts +29 -7
- package/corpus/core/src/integrations/adapters/slack.ts +13 -2
- package/corpus/core/src/integrations/pending-tasks-retry-job.ts +2 -1
- package/corpus/core/src/integrations/pending-tasks-store.ts +1 -1
- package/corpus/core/src/integrations/plugin.ts +183 -48
- package/corpus/core/src/integrations/types.ts +7 -0
- package/corpus/core/src/mcp/build-server.ts +99 -26
- package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +31 -2
- package/corpus/templates/calendar/AGENTS.md +11 -0
- package/corpus/templates/calendar/README.md +2 -1
- package/corpus/templates/calendar/actions/create-event.ts +7 -6
- package/corpus/templates/calendar/actions/event-action-helpers.ts +49 -0
- package/corpus/templates/calendar/actions/update-event.ts +225 -14
- package/corpus/templates/calendar/app/components/calendar/DayView.tsx +185 -70
- package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +26 -4
- package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +112 -69
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +621 -524
- package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +275 -164
- package/corpus/templates/calendar/app/components/calendar/WorkingLocationEditor.tsx +222 -0
- package/corpus/templates/calendar/app/hooks/use-events.ts +151 -79
- package/corpus/templates/calendar/app/i18n/zh-TW.ts +6 -0
- package/corpus/templates/calendar/app/i18n-data.ts +60 -0
- package/corpus/templates/calendar/app/lib/all-day-layout.ts +126 -0
- package/corpus/templates/calendar/app/lib/event-form-utils.ts +18 -1
- package/corpus/templates/calendar/app/lib/event-mutation-inputs.ts +1 -1
- package/corpus/templates/calendar/app/lib/working-location.ts +163 -0
- package/corpus/templates/calendar/app/pages/CalendarView.tsx +21 -2
- package/corpus/templates/calendar/changelog/2026-07-07-working-locations-from-google-calendar-now-appear-as-native-.md +6 -0
- package/corpus/templates/calendar/server/lib/calendar-availability.ts +2 -0
- package/corpus/templates/calendar/server/lib/google-api.ts +6 -1
- package/corpus/templates/calendar/server/lib/google-calendar.ts +49 -19
- package/corpus/templates/calendar/shared/api.ts +2 -0
- package/dist/client/session-replay.d.ts.map +1 -1
- package/dist/client/session-replay.js +24 -7
- package/dist/client/session-replay.js.map +1 -1
- package/dist/collab/routes.d.ts +2 -2
- package/dist/integrations/adapters/slack.js +11 -2
- package/dist/integrations/adapters/slack.js.map +1 -1
- package/dist/integrations/pending-tasks-retry-job.d.ts.map +1 -1
- package/dist/integrations/pending-tasks-retry-job.js +2 -1
- package/dist/integrations/pending-tasks-retry-job.js.map +1 -1
- package/dist/integrations/pending-tasks-store.js +1 -1
- package/dist/integrations/pending-tasks-store.js.map +1 -1
- package/dist/integrations/plugin.d.ts.map +1 -1
- package/dist/integrations/plugin.js +140 -34
- package/dist/integrations/plugin.js.map +1 -1
- package/dist/integrations/types.d.ts +7 -0
- package/dist/integrations/types.d.ts.map +1 -1
- package/dist/integrations/types.js.map +1 -1
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +82 -28
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/secrets/routes.d.ts +9 -9
- package/docs/content/external-agents.mdx +17 -11
- package/docs/content/locales/ar-SA/external-agents.mdx +6 -2
- package/docs/content/locales/de-DE/external-agents.mdx +6 -2
- package/docs/content/locales/es-ES/external-agents.mdx +6 -2
- package/docs/content/locales/fr-FR/external-agents.mdx +6 -2
- package/docs/content/locales/hi-IN/external-agents.mdx +6 -2
- package/docs/content/locales/ja-JP/external-agents.mdx +6 -2
- package/docs/content/locales/ko-KR/external-agents.mdx +6 -2
- package/docs/content/locales/pt-BR/external-agents.mdx +6 -2
- package/docs/content/locales/zh-CN/external-agents.mdx +6 -2
- package/docs/content/locales/zh-TW/external-agents.mdx +6 -2
- package/package.json +1 -1
|
@@ -80,6 +80,7 @@ import {
|
|
|
80
80
|
claimPendingTask,
|
|
81
81
|
getNextPendingTaskIdForThread,
|
|
82
82
|
insertPendingTask,
|
|
83
|
+
isDuplicateEventError,
|
|
83
84
|
MAX_PENDING_TASK_ATTEMPTS,
|
|
84
85
|
markTaskCompleted,
|
|
85
86
|
markTaskFailed,
|
|
@@ -140,6 +141,7 @@ import type {
|
|
|
140
141
|
IntegrationsPluginOptions,
|
|
141
142
|
IntegrationStatus,
|
|
142
143
|
IntegrationExecutionContext,
|
|
144
|
+
IncomingMessage,
|
|
143
145
|
} from "./types.js";
|
|
144
146
|
import {
|
|
145
147
|
listIntegrationUsageBudgets,
|
|
@@ -259,6 +261,23 @@ const REMOTE_DEVICE_ONLINE_MS = 90_000;
|
|
|
259
261
|
// API outage every message would otherwise get another identical "try again"
|
|
260
262
|
// reply. Short enough that a persistent condition still reminds the sender.
|
|
261
263
|
const DECLINE_NOTICE_DEDUPE_TTL_MS = 5 * 60 * 1_000;
|
|
264
|
+
const SYSTEM_NOTICE_DEDUPE_TTL_MS = 24 * 60 * 60 * 1_000;
|
|
265
|
+
|
|
266
|
+
type IntegrationSystemNoticeTaskPayload = {
|
|
267
|
+
kind: "system-notice";
|
|
268
|
+
incoming: IncomingMessage;
|
|
269
|
+
text: string;
|
|
270
|
+
dedupeKey?: string;
|
|
271
|
+
dedupeTtlMs?: number;
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
function systemNoticeEventKey(
|
|
275
|
+
dedupeKey: string,
|
|
276
|
+
ttlMs: number,
|
|
277
|
+
now = Date.now(),
|
|
278
|
+
): string {
|
|
279
|
+
return `system-notice:${dedupeKey}:${Math.floor(now / ttlMs)}`;
|
|
280
|
+
}
|
|
262
281
|
|
|
263
282
|
export async function enqueueRemoteCommand(
|
|
264
283
|
envelope: RemoteCodeCommandEnvelope,
|
|
@@ -637,6 +656,66 @@ export function createIntegrationsPlugin(
|
|
|
637
656
|
const h3 = getH3App(nitroApp);
|
|
638
657
|
const P = `${FRAMEWORK_ROUTE_PREFIX}/integrations`;
|
|
639
658
|
|
|
659
|
+
async function enqueueSystemNotice(
|
|
660
|
+
event: any,
|
|
661
|
+
incoming: IncomingMessage,
|
|
662
|
+
text: string,
|
|
663
|
+
opts?: { dedupeKey?: string; dedupeTtlMs?: number },
|
|
664
|
+
): Promise<void> {
|
|
665
|
+
if (!text.trim()) return;
|
|
666
|
+
const taskId = `notice-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
667
|
+
const dedupeTtlMs = Math.max(
|
|
668
|
+
1,
|
|
669
|
+
opts?.dedupeTtlMs ?? SYSTEM_NOTICE_DEDUPE_TTL_MS,
|
|
670
|
+
);
|
|
671
|
+
const noticeThreadId = `system-notice:${taskId}`;
|
|
672
|
+
const payload: IntegrationSystemNoticeTaskPayload = {
|
|
673
|
+
kind: "system-notice",
|
|
674
|
+
incoming,
|
|
675
|
+
text,
|
|
676
|
+
...(opts?.dedupeKey ? { dedupeKey: opts.dedupeKey } : {}),
|
|
677
|
+
...(opts?.dedupeTtlMs ? { dedupeTtlMs: opts.dedupeTtlMs } : {}),
|
|
678
|
+
};
|
|
679
|
+
try {
|
|
680
|
+
await insertPendingTask({
|
|
681
|
+
id: taskId,
|
|
682
|
+
platform: incoming.platform,
|
|
683
|
+
// System notices are auxiliary delivery work, not the user's agent
|
|
684
|
+
// run. Give each notice its own queue lane so a retrying notice cannot
|
|
685
|
+
// block the real message task for this Slack/Telegram thread.
|
|
686
|
+
externalThreadId: noticeThreadId,
|
|
687
|
+
payload: JSON.stringify(payload),
|
|
688
|
+
ownerEmail: `integration@${incoming.platform}`,
|
|
689
|
+
externalEventKey: opts?.dedupeKey
|
|
690
|
+
? systemNoticeEventKey(opts.dedupeKey, dedupeTtlMs)
|
|
691
|
+
: undefined,
|
|
692
|
+
});
|
|
693
|
+
} catch (err) {
|
|
694
|
+
if (isDuplicateEventError(err)) return;
|
|
695
|
+
throw err;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// The SQL row is the durable source of truth. This best-effort self-call
|
|
699
|
+
// only reduces latency; the recurring pending-task sweep retries a row
|
|
700
|
+
// if the serverless host freezes this webhook execution immediately.
|
|
701
|
+
let token: string | undefined;
|
|
702
|
+
try {
|
|
703
|
+
token = signInternalToken(taskId);
|
|
704
|
+
} catch (err) {
|
|
705
|
+
if (process.env.NODE_ENV === "production") throw err;
|
|
706
|
+
}
|
|
707
|
+
void fetch(`${getBaseUrl(event)}${P}/process-task`, {
|
|
708
|
+
method: "POST",
|
|
709
|
+
headers: {
|
|
710
|
+
"Content-Type": "application/json",
|
|
711
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
712
|
+
},
|
|
713
|
+
body: JSON.stringify({ taskId }),
|
|
714
|
+
}).catch((err) => {
|
|
715
|
+
console.warn("[integrations] System notice dispatch failed:", err);
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
|
|
640
719
|
async function requireSession(event: any): Promise<boolean> {
|
|
641
720
|
const session = await getSession(event).catch(() => null);
|
|
642
721
|
if (session?.email) return true;
|
|
@@ -1550,6 +1629,34 @@ export function createIntegrationsPlugin(
|
|
|
1550
1629
|
isIntegrationCaller: true,
|
|
1551
1630
|
},
|
|
1552
1631
|
async () => {
|
|
1632
|
+
const taskPayload = JSON.parse(task.payload) as
|
|
1633
|
+
| IntegrationSystemNoticeTaskPayload
|
|
1634
|
+
| { kind?: undefined };
|
|
1635
|
+
if (taskPayload.kind === "system-notice") {
|
|
1636
|
+
if (!adapter.sendSystemNotice) {
|
|
1637
|
+
throw new Error(
|
|
1638
|
+
`Platform ${task.platform} cannot deliver system notices`,
|
|
1639
|
+
);
|
|
1640
|
+
}
|
|
1641
|
+
const config = await getIntegrationConfig(task.platform);
|
|
1642
|
+
const credentialContext =
|
|
1643
|
+
await credentialContextForIntegrationConfig(config);
|
|
1644
|
+
await withCredentialContext(credentialContext, () =>
|
|
1645
|
+
adapter.sendSystemNotice!(
|
|
1646
|
+
taskPayload.incoming,
|
|
1647
|
+
taskPayload.text,
|
|
1648
|
+
{
|
|
1649
|
+
...(taskPayload.dedupeKey
|
|
1650
|
+
? { dedupeKey: taskPayload.dedupeKey }
|
|
1651
|
+
: {}),
|
|
1652
|
+
...(taskPayload.dedupeTtlMs
|
|
1653
|
+
? { dedupeTtlMs: taskPayload.dedupeTtlMs }
|
|
1654
|
+
: {}),
|
|
1655
|
+
},
|
|
1656
|
+
),
|
|
1657
|
+
);
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1553
1660
|
const resources = await loadResourcesForPrompt(
|
|
1554
1661
|
task.ownerEmail,
|
|
1555
1662
|
true,
|
|
@@ -2378,7 +2485,8 @@ export function createIntegrationsPlugin(
|
|
|
2378
2485
|
null;
|
|
2379
2486
|
if (
|
|
2380
2487
|
incoming.platform === "slack" &&
|
|
2381
|
-
incoming.conversationType === "dm"
|
|
2488
|
+
incoming.conversationType === "dm" &&
|
|
2489
|
+
!options?.resolveExecutionContext
|
|
2382
2490
|
) {
|
|
2383
2491
|
try {
|
|
2384
2492
|
defaultExecutionContext = await withCredentialContext(
|
|
@@ -2386,51 +2494,48 @@ export function createIntegrationsPlugin(
|
|
|
2386
2494
|
() => resolveDefaultIntegrationExecutionContext(incoming!),
|
|
2387
2495
|
);
|
|
2388
2496
|
} catch (err) {
|
|
2389
|
-
//
|
|
2390
|
-
//
|
|
2391
|
-
//
|
|
2392
|
-
//
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
dedupeTtlMs: DECLINE_NOTICE_DEDUPE_TTL_MS,
|
|
2414
|
-
},
|
|
2415
|
-
)
|
|
2416
|
-
: Promise.resolve(),
|
|
2417
|
-
).catch((noticeErr) => {
|
|
2497
|
+
// The legacy owner-only resolver predates org-bound identities
|
|
2498
|
+
// and must not turn a rejected Slack DM into an authenticated
|
|
2499
|
+
// owner run. Custom resolveExecutionContext is checked above and
|
|
2500
|
+
// skips this default ladder entirely so apps can fully own auth
|
|
2501
|
+
// without framework membership checks or identity side effects.
|
|
2502
|
+
const declined =
|
|
2503
|
+
err instanceof IntegrationIdentityDeclinedError ? err : null;
|
|
2504
|
+
if (declined) {
|
|
2505
|
+
console.warn(
|
|
2506
|
+
`[integrations] default Slack DM identity declined message:`,
|
|
2507
|
+
declined.message,
|
|
2508
|
+
);
|
|
2509
|
+
if (adapter.sendSystemNotice) {
|
|
2510
|
+
try {
|
|
2511
|
+
await enqueueSystemNotice(
|
|
2512
|
+
event,
|
|
2513
|
+
incoming!,
|
|
2514
|
+
declined.userFacingMessage,
|
|
2515
|
+
{
|
|
2516
|
+
dedupeKey: `decline:${incoming!.tenantId ?? "unknown"}:${incoming!.senderId ?? "unknown"}:${declined.reason}`,
|
|
2517
|
+
dedupeTtlMs: DECLINE_NOTICE_DEDUPE_TTL_MS,
|
|
2518
|
+
},
|
|
2519
|
+
);
|
|
2520
|
+
} catch (noticeErr) {
|
|
2418
2521
|
console.warn(
|
|
2419
|
-
`[integrations] decline notice
|
|
2522
|
+
`[integrations] could not persist decline notice:`,
|
|
2420
2523
|
noticeErr instanceof Error
|
|
2421
2524
|
? noticeErr.message
|
|
2422
2525
|
: noticeErr,
|
|
2423
2526
|
);
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
`[integrations] default Slack DM identity denied message:`,
|
|
2428
|
-
err,
|
|
2429
|
-
);
|
|
2527
|
+
setResponseStatus(event, 500);
|
|
2528
|
+
return { error: "notice enqueue failed" };
|
|
2529
|
+
}
|
|
2430
2530
|
}
|
|
2431
|
-
|
|
2432
|
-
|
|
2531
|
+
} else {
|
|
2532
|
+
console.error(
|
|
2533
|
+
`[integrations] default Slack DM identity denied message:`,
|
|
2534
|
+
err,
|
|
2535
|
+
);
|
|
2433
2536
|
}
|
|
2537
|
+
setResponseStatus(event, 200);
|
|
2538
|
+
return "ok";
|
|
2434
2539
|
}
|
|
2435
2540
|
}
|
|
2436
2541
|
let executionContext: IntegrationExecutionContext = {
|
|
@@ -2456,14 +2561,42 @@ export function createIntegrationsPlugin(
|
|
|
2456
2561
|
} else if (defaultExecutionContext) {
|
|
2457
2562
|
executionContext = defaultExecutionContext;
|
|
2458
2563
|
if (defaultExecutionContext.anonymousMember) {
|
|
2564
|
+
if (!options?.allowAnonymousOrgScopedSlackDm) {
|
|
2565
|
+
const senderEmail =
|
|
2566
|
+
typeof incoming.senderEmail === "string" &&
|
|
2567
|
+
incoming.senderEmail.trim()
|
|
2568
|
+
? incoming.senderEmail.trim()
|
|
2569
|
+
: null;
|
|
2570
|
+
const noticeText = senderEmail
|
|
2571
|
+
? `I couldn't match your Slack account to an organization member, so I can't run this request. Ask an organization admin to add ${senderEmail}, then try again.`
|
|
2572
|
+
: "I couldn't verify your Slack account email, so I can't run this request. Ask an organization admin to reconnect Slack with the users:read.email scope, then try again.";
|
|
2573
|
+
if (adapter.sendSystemNotice) {
|
|
2574
|
+
try {
|
|
2575
|
+
await enqueueSystemNotice(event, incoming, noticeText, {
|
|
2576
|
+
dedupeKey: `anonymous-tier-disabled:${incoming.tenantId ?? "unknown"}:${incoming.senderId ?? "unknown"}`,
|
|
2577
|
+
});
|
|
2578
|
+
} catch (noticeErr) {
|
|
2579
|
+
console.warn(
|
|
2580
|
+
`[integrations] could not persist unlinked-member notice:`,
|
|
2581
|
+
noticeErr instanceof Error
|
|
2582
|
+
? noticeErr.message
|
|
2583
|
+
: noticeErr,
|
|
2584
|
+
);
|
|
2585
|
+
setResponseStatus(event, 500);
|
|
2586
|
+
return { error: "notice enqueue failed" };
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
setResponseStatus(event, 200);
|
|
2590
|
+
return "ok";
|
|
2591
|
+
}
|
|
2459
2592
|
// The anonymous tier must never be silent. (1) The agent run
|
|
2460
2593
|
// can tell: the note rides the serialized `incoming` into the
|
|
2461
2594
|
// queued task and surfaces via <integration-context>.
|
|
2462
2595
|
incoming.identityNote =
|
|
2463
2596
|
"Caller is an unlinked Slack workspace member running with organization-wide visibility only; personal or privately-shared data is not accessible. They can get personal access by having an admin add their Slack email to the organization (or by reconnecting Slack with the users:read.email scope).";
|
|
2464
|
-
// (2) The sender gets a one-time heads-up
|
|
2465
|
-
//
|
|
2466
|
-
//
|
|
2597
|
+
// (2) The sender gets a one-time heads-up through the same
|
|
2598
|
+
// durable SQL queue as agent work. The self-dispatch is only a
|
|
2599
|
+
// latency optimization; the retry sweep guarantees delivery.
|
|
2467
2600
|
if (adapter.sendSystemNotice) {
|
|
2468
2601
|
const senderEmail =
|
|
2469
2602
|
typeof incoming.senderEmail === "string" &&
|
|
@@ -2473,16 +2606,18 @@ export function createIntegrationsPlugin(
|
|
|
2473
2606
|
const noticeText = senderEmail
|
|
2474
2607
|
? `Heads up: I couldn't match your Slack account to an organization member, so I can only use org-wide data. Ask an admin to add ${senderEmail} to the organization for personal access.`
|
|
2475
2608
|
: "Heads up: I couldn't verify your Slack account's email, so I can only use org-wide data. Ask an admin to update the Slack connection with the users:read.email scope for personal access.";
|
|
2476
|
-
|
|
2477
|
-
|
|
2609
|
+
try {
|
|
2610
|
+
await enqueueSystemNotice(event, incoming, noticeText, {
|
|
2478
2611
|
dedupeKey: `anonymous-tier:${incoming.tenantId ?? "unknown"}:${incoming.senderId ?? "unknown"}`,
|
|
2479
|
-
})
|
|
2480
|
-
|
|
2612
|
+
});
|
|
2613
|
+
} catch (noticeErr) {
|
|
2481
2614
|
console.warn(
|
|
2482
|
-
`[integrations] anonymous-tier notice
|
|
2615
|
+
`[integrations] could not persist anonymous-tier notice:`,
|
|
2483
2616
|
noticeErr instanceof Error ? noticeErr.message : noticeErr,
|
|
2484
2617
|
);
|
|
2485
|
-
|
|
2618
|
+
setResponseStatus(event, 500);
|
|
2619
|
+
return { error: "notice enqueue failed" };
|
|
2620
|
+
}
|
|
2486
2621
|
}
|
|
2487
2622
|
}
|
|
2488
2623
|
} else if (options?.resolveOwner) {
|
|
@@ -438,6 +438,13 @@ export interface IntegrationsPluginOptions {
|
|
|
438
438
|
resolveExecutionContext?: (
|
|
439
439
|
incoming: IncomingMessage,
|
|
440
440
|
) => IntegrationExecutionContext | Promise<IntegrationExecutionContext>;
|
|
441
|
+
/**
|
|
442
|
+
* Explicitly allow an unlinked, verified Slack workspace member to run a DM
|
|
443
|
+
* with the installation organization's shared/service visibility. Disabled
|
|
444
|
+
* by default: DM identity resolution fails closed unless an app deliberately
|
|
445
|
+
* accepts this wider access tier.
|
|
446
|
+
*/
|
|
447
|
+
allowAnonymousOrgScopedSlackDm?: boolean;
|
|
441
448
|
/**
|
|
442
449
|
* Optional preprocessor for inbound platform messages. Can intercept special
|
|
443
450
|
* commands (such as `/link`) before the agent loop runs.
|
|
@@ -512,28 +512,91 @@ function routePathFromOpenUrl(value: string): string | null {
|
|
|
512
512
|
* `mcpApp.resource` (the resource path already strips them via
|
|
513
513
|
* `mcpAppStructuredContent`).
|
|
514
514
|
*
|
|
515
|
-
*
|
|
516
|
-
*
|
|
517
|
-
*
|
|
515
|
+
* Circular structures are replaced with a marker. Strings that embed an
|
|
516
|
+
* `isEmbedStartUrl` substring (e.g. a longer message that includes the URL)
|
|
517
|
+
* are replaced with `[hidden embed URL]`. Credential-like `ticket` fields are
|
|
518
|
+
* removed only inside an embed-signaled object/branch, so ordinary business
|
|
519
|
+
* fields from unrelated read actions remain faithful.
|
|
518
520
|
*/
|
|
519
|
-
|
|
520
|
-
|
|
521
|
+
const EMBED_RESULT_SENSITIVE_KEYS = new Set([
|
|
522
|
+
"embedTargetPath",
|
|
523
|
+
"embedExpiresAt",
|
|
524
|
+
"embedTicket",
|
|
525
|
+
]);
|
|
526
|
+
|
|
527
|
+
function isEmbedCredentialKey(key: string): boolean {
|
|
528
|
+
return key === "ticket" || /Ticket$/.test(key);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function containsEmbedRoutingSignal(
|
|
532
|
+
value: unknown,
|
|
533
|
+
seen = new WeakSet<object>(),
|
|
534
|
+
): boolean {
|
|
535
|
+
if (typeof value === "string") return isEmbedStartUrl(value);
|
|
536
|
+
if (!value || typeof value !== "object") return false;
|
|
537
|
+
if (seen.has(value)) return false;
|
|
538
|
+
seen.add(value);
|
|
539
|
+
if (Array.isArray(value)) {
|
|
540
|
+
const result = value.some((item) => containsEmbedRoutingSignal(item, seen));
|
|
541
|
+
seen.delete(value);
|
|
542
|
+
return result;
|
|
543
|
+
}
|
|
544
|
+
for (const [key, val] of Object.entries(value)) {
|
|
545
|
+
if (EMBED_RESULT_SENSITIVE_KEYS.has(key)) {
|
|
546
|
+
seen.delete(value);
|
|
547
|
+
return true;
|
|
548
|
+
}
|
|
549
|
+
if (containsEmbedRoutingSignal(val, seen)) {
|
|
550
|
+
seen.delete(value);
|
|
551
|
+
return true;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
seen.delete(value);
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function purgeEmbedStartUrls(
|
|
559
|
+
value: unknown,
|
|
560
|
+
seen = new WeakSet<object>(),
|
|
561
|
+
embedContext = false,
|
|
562
|
+
): unknown {
|
|
521
563
|
if (typeof value === "string") {
|
|
522
564
|
return isEmbedStartUrl(value) ? "[hidden embed URL]" : value;
|
|
523
565
|
}
|
|
524
566
|
if (Array.isArray(value)) {
|
|
525
|
-
|
|
567
|
+
if (seen.has(value)) return "[circular result]";
|
|
568
|
+
seen.add(value);
|
|
569
|
+
const out = value.map((item) =>
|
|
570
|
+
purgeEmbedStartUrls(
|
|
571
|
+
item,
|
|
572
|
+
seen,
|
|
573
|
+
embedContext || containsEmbedRoutingSignal(item),
|
|
574
|
+
),
|
|
575
|
+
);
|
|
576
|
+
seen.delete(value);
|
|
577
|
+
return out;
|
|
526
578
|
}
|
|
527
579
|
if (value && typeof value === "object") {
|
|
580
|
+
if (seen.has(value)) return "[circular result]";
|
|
581
|
+
seen.add(value);
|
|
582
|
+
const entries = Object.entries(value as Record<string, unknown>);
|
|
583
|
+
const localEmbedContext = embedContext || containsEmbedRoutingSignal(value);
|
|
528
584
|
const out: Record<string, unknown> = {};
|
|
529
|
-
for (const [key, val] of
|
|
585
|
+
for (const [key, val] of entries) {
|
|
586
|
+
if (
|
|
587
|
+
EMBED_RESULT_SENSITIVE_KEYS.has(key) ||
|
|
588
|
+
(localEmbedContext && isEmbedCredentialKey(key))
|
|
589
|
+
) {
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
530
592
|
if (typeof val === "string" && isEmbedStartUrl(val)) {
|
|
531
593
|
// Drop the key entirely for object-typed inputs so a tool result like
|
|
532
594
|
// `{ embedStartUrl: "..." }` does not appear at all in the LLM text.
|
|
533
595
|
continue;
|
|
534
596
|
}
|
|
535
|
-
out[key] = purgeEmbedStartUrls(val,
|
|
597
|
+
out[key] = purgeEmbedStartUrls(val, seen, localEmbedContext);
|
|
536
598
|
}
|
|
599
|
+
seen.delete(value);
|
|
537
600
|
return out;
|
|
538
601
|
}
|
|
539
602
|
return value;
|
|
@@ -1195,11 +1258,12 @@ function mcpAppStructuredContent(
|
|
|
1195
1258
|
result: unknown,
|
|
1196
1259
|
meta: Record<string, unknown> | undefined,
|
|
1197
1260
|
): Record<string, unknown> {
|
|
1261
|
+
const purged = purgeEmbedStartUrls(result);
|
|
1198
1262
|
const out: Record<string, unknown> =
|
|
1199
|
-
|
|
1200
|
-
? { ...(
|
|
1201
|
-
: primitiveValue(
|
|
1202
|
-
? { result }
|
|
1263
|
+
purged && typeof purged === "object" && !Array.isArray(purged)
|
|
1264
|
+
? { ...(purged as Record<string, unknown>) }
|
|
1265
|
+
: primitiveValue(purged)
|
|
1266
|
+
? { result: purged }
|
|
1203
1267
|
: {};
|
|
1204
1268
|
for (const key of ["embedStartUrl", "startUrl"]) {
|
|
1205
1269
|
const value = out[key];
|
|
@@ -1213,17 +1277,6 @@ function mcpAppStructuredContent(
|
|
|
1213
1277
|
// LLM). `embedTargetPath` reveals the exact route + thread/draft id the user
|
|
1214
1278
|
// is looking at; `embedExpiresAt` is an unintended timestamp; ticket-bearing
|
|
1215
1279
|
// fields are single-use credentials. Drop all of them unconditionally.
|
|
1216
|
-
for (const key of [
|
|
1217
|
-
"embedTargetPath",
|
|
1218
|
-
"embedExpiresAt",
|
|
1219
|
-
"ticket",
|
|
1220
|
-
"embedTicket",
|
|
1221
|
-
]) {
|
|
1222
|
-
delete out[key];
|
|
1223
|
-
}
|
|
1224
|
-
for (const key of Object.keys(out)) {
|
|
1225
|
-
if (/Ticket$/.test(key)) delete out[key];
|
|
1226
|
-
}
|
|
1227
1280
|
const openLink = meta?.["agent-native/openLink"];
|
|
1228
1281
|
if (openLink && typeof openLink === "object" && !Array.isArray(openLink)) {
|
|
1229
1282
|
const webUrl = (openLink as Record<string, unknown>).webUrl;
|
|
@@ -1275,12 +1328,25 @@ function isSuccessOnlyResult(value: Record<string, unknown>): boolean {
|
|
|
1275
1328
|
});
|
|
1276
1329
|
}
|
|
1277
1330
|
|
|
1278
|
-
function conciseToolResultText(
|
|
1331
|
+
function conciseToolResultText(
|
|
1332
|
+
name: string,
|
|
1333
|
+
result: unknown,
|
|
1334
|
+
options?: { preserveObjectResult?: boolean },
|
|
1335
|
+
): string {
|
|
1279
1336
|
const purged = purgeEmbedStartUrls(result);
|
|
1280
1337
|
if (typeof purged === "string") return truncateToolText(purged);
|
|
1281
1338
|
if (purged === true || purged == null) return `${name} completed.`;
|
|
1282
1339
|
if (purged && typeof purged === "object" && !Array.isArray(purged)) {
|
|
1283
1340
|
const record = purged as Record<string, unknown>;
|
|
1341
|
+
// Read-only actions are data reads, not mutations. Keep their object
|
|
1342
|
+
// payload available to MCP clients in the text fallback too; the
|
|
1343
|
+
// structuredContent branch below is the lossless path for clients that
|
|
1344
|
+
// support it. Mutating/action-style results retain the concise status
|
|
1345
|
+
// text so we do not unexpectedly dump write results into conversations.
|
|
1346
|
+
if (options?.preserveObjectResult) {
|
|
1347
|
+
const text = JSON.stringify(purged);
|
|
1348
|
+
return text === undefined ? `${name} completed.` : truncateToolText(text);
|
|
1349
|
+
}
|
|
1284
1350
|
const message = record.message ?? record.summary;
|
|
1285
1351
|
if (typeof message === "string" && message.trim()) {
|
|
1286
1352
|
return truncateToolText(message.trim());
|
|
@@ -1711,10 +1777,17 @@ export async function createMCPServerForRequest(
|
|
|
1711
1777
|
typeof rawResult === "object" &&
|
|
1712
1778
|
!Array.isArray(rawResult)
|
|
1713
1779
|
? (rawResult as Record<string, unknown>)
|
|
1714
|
-
:
|
|
1780
|
+
: entry.readOnly === true &&
|
|
1781
|
+
rawResult &&
|
|
1782
|
+
typeof rawResult === "object" &&
|
|
1783
|
+
!Array.isArray(rawResult)
|
|
1784
|
+
? mcpAppStructuredContent(rawResultForClient, responseMeta)
|
|
1785
|
+
: undefined;
|
|
1715
1786
|
const text = mcpAppResource
|
|
1716
1787
|
? conciseMcpAppToolText(name, resultForClient, structuredContent!)
|
|
1717
|
-
: conciseToolResultText(name, resultForClient
|
|
1788
|
+
: conciseToolResultText(name, resultForClient, {
|
|
1789
|
+
preserveObjectResult: entry.readOnly === true,
|
|
1790
|
+
});
|
|
1718
1791
|
const content: any[] = [{ type: "text", text }];
|
|
1719
1792
|
if (block) content.push(block);
|
|
1720
1793
|
return {
|
|
@@ -128,12 +128,26 @@ pnpm action create-event \
|
|
|
128
128
|
# Working location
|
|
129
129
|
pnpm action create-event \
|
|
130
130
|
--title "Working from home" \
|
|
131
|
-
--start 2026-04-
|
|
132
|
-
--end 2026-04-
|
|
131
|
+
--start 2026-04-03 \
|
|
132
|
+
--end 2026-04-04 \
|
|
133
|
+
--allDay true \
|
|
133
134
|
--eventType workingLocation \
|
|
134
135
|
--workingLocationType homeOffice
|
|
135
136
|
```
|
|
136
137
|
|
|
138
|
+
Working-location events sync from Google with `workingLocationProperties` and
|
|
139
|
+
render as native working locations in the UI instead of generic all-day events.
|
|
140
|
+
They are transparent/non-blocking for availability. Google allows timed working
|
|
141
|
+
locations or single-day all-day working locations; multi-day all-day ranges must
|
|
142
|
+
be represented as separate daily working-location events.
|
|
143
|
+
|
|
144
|
+
For a visible occurrence in a recurring working-location series, default to
|
|
145
|
+
`scope: "single"` and pass the occurrence's event `id`, not its
|
|
146
|
+
`recurringEventId`. Use `scope: "all"` only when the user explicitly asks to
|
|
147
|
+
change every day in the series. Keep office building/floor/desk metadata when
|
|
148
|
+
editing an office label, and clear incompatible location labels when changing
|
|
149
|
+
between Home, Office, and Other.
|
|
150
|
+
|
|
137
151
|
Do not use `eventType` for Tasks or appointment schedules. Google Calendar
|
|
138
152
|
Tasks are a separate product/API surface, and appointment schedules should use
|
|
139
153
|
booking links or availability workflows instead.
|
|
@@ -225,6 +239,12 @@ pnpm action update-event --id google-event-id --attendees "alice@example.com" --
|
|
|
225
239
|
pnpm action update-event --id google-event-id --addGoogleMeet=true
|
|
226
240
|
pnpm action update-event --id google-event-id --addZoom=true
|
|
227
241
|
|
|
242
|
+
# Update an existing working-location event's native metadata
|
|
243
|
+
pnpm action update-event \
|
|
244
|
+
--id google-working-location-id \
|
|
245
|
+
--workingLocationType officeLocation \
|
|
246
|
+
--workingLocationLabel "Pier 57"
|
|
247
|
+
|
|
228
248
|
# Add multiple alerts, a Google event color, and an attachment
|
|
229
249
|
pnpm action update-event \
|
|
230
250
|
--id google-event-id \
|
|
@@ -237,6 +257,15 @@ pnpm action update-event \
|
|
|
237
257
|
|
|
238
258
|
For "add Zoom to this meeting", fetch or use the visible event id and call `update-event --addZoom=true`. Do not create an extension for Zoom; Zoom is a first-party calendar integration handled by the event actions and the Settings page.
|
|
239
259
|
|
|
260
|
+
Google Calendar does not allow changing an existing event's `eventType`; use
|
|
261
|
+
`workingLocationType` and `workingLocationLabel` only on events that already
|
|
262
|
+
have `eventType: "workingLocation"`.
|
|
263
|
+
|
|
264
|
+
Google Calendar API v3 currently documents working locations on Events, but the
|
|
265
|
+
Settings API/discovery document does not expose working-hours settings. Treat
|
|
266
|
+
working-hours overlays or Find a Time constraints as a follow-up only after a
|
|
267
|
+
real provider data path exists.
|
|
268
|
+
|
|
240
269
|
For recurring events, pass a Google Calendar RRULE in `--recurrence`. Example: to make a daily event weekdays only, use:
|
|
241
270
|
|
|
242
271
|
```bash
|
|
@@ -37,6 +37,17 @@ Detailed event, availability, booking, storage, and UI rules live in
|
|
|
37
37
|
with `stageAs` and analyze them with `query-staged-dataset`.
|
|
38
38
|
- For Google Calendar, distinguish an empty calendar from missing auth,
|
|
39
39
|
reauth-needed, or fetch failures.
|
|
40
|
+
- Google Calendar working locations are status events (`eventType:
|
|
41
|
+
"workingLocation"`). Sync and display them as working locations, keep them
|
|
42
|
+
transparent/non-blocking, and preserve `workingLocationProperties` instead of
|
|
43
|
+
treating the summary as a generic all-day event title.
|
|
44
|
+
- When updating one visible occurrence in a recurring working-location series,
|
|
45
|
+
pass that occurrence's event `id` with `scope: "single"` by default. Use the
|
|
46
|
+
series scope only when the user explicitly chooses all days.
|
|
47
|
+
- Google Calendar API v3 exposes working locations through Events. The current
|
|
48
|
+
Settings API and Calendar v3 discovery document do not expose working-hours
|
|
49
|
+
settings, so do not promise working-hours UI or overlays unless a real
|
|
50
|
+
provider data path has been verified first.
|
|
40
51
|
- Use framework sharing actions for calendars/events/booking resources when
|
|
41
52
|
applicable.
|
|
42
53
|
- Booking-link sharing controls who can manage the link. Public booking access
|
|
@@ -13,7 +13,8 @@ the agent can do through the same actions.
|
|
|
13
13
|
## Features
|
|
14
14
|
|
|
15
15
|
- Day, week, and month views with multiple Google accounts overlayed.
|
|
16
|
-
- Google Calendar sync and read-only ICS feed
|
|
16
|
+
- Google Calendar sync, native working locations, and read-only ICS feed
|
|
17
|
+
subscriptions.
|
|
17
18
|
- Weekly availability with timezone support for slot-finding.
|
|
18
19
|
- Calendly-style public booking links at `/book/{slug}` with custom fields.
|
|
19
20
|
- Ask the agent anything schedule-related, from "am I free Thursday?" to
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
reminderMethodInput,
|
|
25
25
|
reminderMinutesInput,
|
|
26
26
|
remindersInput,
|
|
27
|
+
validateStatusEventTiming,
|
|
27
28
|
visibilityInput,
|
|
28
29
|
workingLocationTypeInput,
|
|
29
30
|
} from "./event-action-helpers.js";
|
|
@@ -116,12 +117,12 @@ export default defineAction({
|
|
|
116
117
|
if (args.addGoogleMeet && args.addZoom) {
|
|
117
118
|
throw new Error("Choose either Google Meet or Zoom, not both.");
|
|
118
119
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
args.allDay
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
120
|
+
validateStatusEventTiming({
|
|
121
|
+
eventType: args.eventType,
|
|
122
|
+
allDay: args.allDay,
|
|
123
|
+
start: args.start,
|
|
124
|
+
end: args.end,
|
|
125
|
+
});
|
|
125
126
|
|
|
126
127
|
if (!(await googleCalendar.isConnected(email))) {
|
|
127
128
|
throw new Error(
|