@agent-native/core 0.176.4 → 0.176.5-nightly-20260902205402

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +6 -0
  3. package/corpus/templates/calendar/AGENTS.md +4 -0
  4. package/corpus/templates/calendar/actions/delete-event.ts +2 -2
  5. package/corpus/templates/calendar/actions/event-action-helpers.ts +7 -0
  6. package/corpus/templates/calendar/actions/get-event.ts +40 -3
  7. package/corpus/templates/calendar/actions/list-events.ts +49 -2
  8. package/corpus/templates/calendar/actions/list-google-calendars.ts +24 -0
  9. package/corpus/templates/calendar/actions/update-calendar-visual-preferences.ts +52 -1
  10. package/corpus/templates/calendar/actions/update-event.ts +2 -2
  11. package/corpus/templates/calendar/actions/view-screen.ts +54 -2
  12. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +33 -19
  13. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +31 -5
  14. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +102 -0
  15. package/corpus/templates/calendar/app/hooks/use-events.ts +45 -10
  16. package/corpus/templates/calendar/app/hooks/use-google-auth.ts +8 -1
  17. package/corpus/templates/calendar/app/hooks/use-google-calendars.ts +41 -0
  18. package/corpus/templates/calendar/app/hooks/use-view-preferences.ts +56 -4
  19. package/corpus/templates/calendar/app/lib/shared-calendar-demo.ts +187 -0
  20. package/corpus/templates/calendar/app/pages/CalendarView.tsx +50 -5
  21. package/corpus/templates/calendar/changelog/2026-09-01-calendars-shared-with-your-connected-google-accounts-can-now.md +6 -0
  22. package/corpus/templates/calendar/changelog/2026-09-02-updated-the-booking-link-og-preview-image-with-the-new-monoc.md +6 -0
  23. package/corpus/templates/calendar/server/lib/booking-og-image.ts +8 -18
  24. package/corpus/templates/calendar/server/lib/google-api.ts +10 -0
  25. package/corpus/templates/calendar/server/lib/google-calendar.ts +215 -21
  26. package/corpus/templates/calendar/server/plugins/agent-chat.ts +2 -0
  27. package/corpus/templates/calendar/server/plugins/feature-flags.ts +5 -0
  28. package/corpus/templates/calendar/shared/api.ts +20 -0
  29. package/corpus/templates/calendar/shared/calendar-view-preferences.ts +28 -1
  30. package/corpus/templates/calendar/shared/feature-flags.ts +8 -0
  31. package/corpus/templates/calendar/shared/google-calendar-sources.ts +46 -0
  32. package/corpus/templates/factory/.env.example +5 -0
  33. package/corpus/templates/factory/README.md +9 -0
  34. package/corpus/templates/factory/server/connectors/credentials.ts +28 -6
  35. package/corpus/templates/factory/server/triage/github-client.ts +144 -7
  36. package/corpus/templates/forms/server/lib/form-og-image.ts +7 -17
  37. package/corpus/templates/slides/actions/add-slide.ts +1 -0
  38. package/corpus/templates/slides/actions/create-deck.ts +1 -0
  39. package/corpus/templates/slides/actions/get-deck.ts +1 -0
  40. package/corpus/templates/slides/actions/navigate.ts +1 -0
  41. package/corpus/templates/slides/actions/patch-deck.ts +1 -0
  42. package/corpus/templates/slides/actions/update-slide.ts +1 -0
  43. package/corpus/templates/slides/actions/view-screen.ts +1 -0
  44. package/corpus/templates/slides/server/plugins/agent-chat.ts +2 -0
  45. package/dist/action.d.ts +4 -0
  46. package/dist/action.js +3 -0
  47. package/dist/agent/types.d.ts +1 -0
  48. package/dist/automation/index.d.ts +1 -1
  49. package/dist/client/webmcp.js +7 -2
  50. package/dist/collab/awareness.d.ts +2 -2
  51. package/dist/mcp/build-server.d.ts +2 -0
  52. package/dist/mcp/build-server.js +4 -0
  53. package/dist/notifications/routes.d.ts +3 -3
  54. package/dist/observability/routes.d.ts +6 -6
  55. package/dist/provider-api/actions/provider-api.d.ts +1 -1
  56. package/dist/secrets/routes.d.ts +9 -9
  57. package/dist/server/action-routes.d.ts +1 -0
  58. package/dist/server/action-routes.js +4 -0
  59. package/dist/server/agent-chat/mcp-options.d.ts +4 -0
  60. package/dist/server/agent-chat/mcp-options.js +1 -0
  61. package/dist/server/agent-chat-plugin.js +2 -0
  62. package/dist/server/realtime-token.d.ts +1 -1
  63. package/dist/server/social-og-image.js +6 -20
  64. package/dist/server/transcribe-voice.d.ts +1 -1
  65. package/dist/shared/agent-mcp-metadata.d.ts +3 -0
  66. package/dist/shared/agent-mcp-metadata.js +18 -0
  67. package/dist/shared/social-meta.d.ts +1 -1
  68. package/dist/shared/social-meta.js +1 -1
  69. package/package.json +1 -1
@@ -0,0 +1,46 @@
1
+ export interface GoogleCalendarSourceIdentity {
2
+ accountEmail: string;
3
+ calendarId: string;
4
+ }
5
+
6
+ /**
7
+ * Calendar ids are provider data and may contain separators, so encode the
8
+ * complete identity rather than joining two user-controlled strings.
9
+ */
10
+ export function createGoogleCalendarSourceKey({
11
+ accountEmail,
12
+ calendarId,
13
+ }: GoogleCalendarSourceIdentity): string {
14
+ return `google-calendar:${Buffer.from(
15
+ JSON.stringify([accountEmail.trim().toLowerCase(), calendarId]),
16
+ ).toString("base64url")}`;
17
+ }
18
+
19
+ export function parseGoogleCalendarSourceKey(
20
+ sourceKey: string,
21
+ ): GoogleCalendarSourceIdentity | null {
22
+ const prefix = "google-calendar:";
23
+ if (!sourceKey.startsWith(prefix)) return null;
24
+ try {
25
+ const parsed: unknown = JSON.parse(
26
+ Buffer.from(sourceKey.slice(prefix.length), "base64url").toString("utf8"),
27
+ );
28
+ if (
29
+ !Array.isArray(parsed) ||
30
+ parsed.length !== 2 ||
31
+ typeof parsed[0] !== "string" ||
32
+ typeof parsed[1] !== "string" ||
33
+ !parsed[0].trim() ||
34
+ !parsed[1]
35
+ ) {
36
+ return null;
37
+ }
38
+ return {
39
+ accountEmail: parsed[0].trim().toLowerCase(),
40
+ calendarId: parsed[1],
41
+ };
42
+ } catch {
43
+ // coercion-ok: malformed opaque input is a typed invalid-key result.
44
+ return null;
45
+ }
46
+ }
@@ -22,6 +22,11 @@ FACTORY_PUBLIC_URL=https://agent-native-factory.netlify.app
22
22
  # or vault row exists. Hosted Factory ignores them.
23
23
  # SLACK_BOT_TOKEN=
24
24
  # GITHUB_TOKEN=
25
+ # Local-only GitHub App fallback during migration. Hosted Factory reads these
26
+ # from the shared vault, not deployment environment variables.
27
+ # GITHUB_APP_ID=
28
+ # GITHUB_APP_INSTALLATION_ID=
29
+ # GITHUB_APP_PRIVATE_KEY=
25
30
  # SENTRY_AUTH_TOKEN=
26
31
 
27
32
  # Skip login/signup (local dev / preview only)
@@ -49,6 +49,15 @@ env-only read in a provider client.
49
49
 
50
50
  ### GitHub token permissions
51
51
 
52
+ Factory prefers the new Agent-Native GitHub App. Configure `GITHUB_APP_ID`,
53
+ `GITHUB_APP_INSTALLATION_ID`, and `GITHUB_APP_PRIVATE_KEY` together. The App
54
+ needs repository `Pull requests: Read and write`, `Issues: Read and write`,
55
+ and `Checks: Read`, plus organization `Members: Read` for governance. Hosted
56
+ Factory stores the private key in the shared vault and generates short-lived
57
+ installation tokens server-side. Do not send a static token or private key to
58
+ developers. During migration, an existing `GITHUB_TOKEN` remains supported
59
+ when no App keys are configured.
60
+
52
61
  For Factory pull-request polling and babysitting, scope a fine-grained token to
53
62
  the target repository and grant these repository permissions:
54
63
 
@@ -20,8 +20,14 @@ const WORKSPACE_PROVIDER_BY_KEY: Record<string, string> = {
20
20
  SLACK_BOT_TOKEN: "slack",
21
21
  SLACK_BOT_TOKEN_2: "slack",
22
22
  };
23
+ const GITHUB_APP_KEYS = [
24
+ "GITHUB_APP_ID",
25
+ "GITHUB_APP_INSTALLATION_ID",
26
+ "GITHUB_APP_PRIVATE_KEY",
27
+ ] as const;
23
28
  const VAULT_ONLY_KEYS = new Set([
24
29
  ...Object.keys(WORKSPACE_PROVIDER_BY_KEY),
30
+ ...GITHUB_APP_KEYS,
25
31
  "SENTRY_ORG_SLUG",
26
32
  ]);
27
33
 
@@ -235,6 +241,19 @@ export async function hasConnectorSecret(
235
241
  return false;
236
242
  }
237
243
 
244
+ async function hasGitHubConnectorSecret(
245
+ ownerEmail: string,
246
+ options: ResolveConnectorSecretOptions,
247
+ ): Promise<boolean> {
248
+ if (await hasConnectorSecret("GITHUB_TOKEN", ownerEmail, options)) {
249
+ return true;
250
+ }
251
+ const appSecrets = await Promise.all(
252
+ GITHUB_APP_KEYS.map((key) => hasConnectorSecret(key, ownerEmail, options)),
253
+ );
254
+ return appSecrets.every(Boolean);
255
+ }
256
+
238
257
  export async function resolveFactoryConnectorReadiness(
239
258
  ownerEmail: string,
240
259
  options: ResolveConnectorSecretOptions = {},
@@ -248,7 +267,7 @@ export async function resolveFactoryConnectorReadiness(
248
267
  const [slack, slackSecondary, github, sentry] = await Promise.all([
249
268
  hasConnectorSecret("SLACK_BOT_TOKEN", ownerEmail, readinessOptions),
250
269
  hasConnectorSecret("SLACK_BOT_TOKEN_2", ownerEmail, readinessOptions),
251
- hasConnectorSecret("GITHUB_TOKEN", ownerEmail, readinessOptions),
270
+ hasGitHubConnectorSecret(ownerEmail, readinessOptions),
252
271
  hasConnectorSecret(
253
272
  ["SENTRY_SERVER_TOKEN", "SENTRY_AUTH_TOKEN"],
254
273
  ownerEmail,
@@ -269,11 +288,14 @@ export async function assertFactoryConnectorReady(
269
288
  const verb = options.verb ?? "creating";
270
289
  const label =
271
290
  source === "slack" ? "Slack" : source === "github" ? "GitHub" : "Sentry";
272
- const ready = await hasConnectorSecret(
273
- connectorKeysForSource(source, options.slackWorkspace),
274
- ownerEmail,
275
- options,
276
- );
291
+ const ready =
292
+ source === "github"
293
+ ? await hasGitHubConnectorSecret(ownerEmail, options)
294
+ : await hasConnectorSecret(
295
+ connectorKeysForSource(source, options.slackWorkspace),
296
+ ownerEmail,
297
+ options,
298
+ );
277
299
  if (!ready) {
278
300
  throw new Error(
279
301
  `Connect ${label} in Dispatch or add a vault token before ${verb} this job.`,
@@ -1,3 +1,5 @@
1
+ import { createSign } from "node:crypto";
2
+
1
3
  import { resolveConnectorSecret } from "../connectors/credentials.js";
2
4
  import type { TriageCoverage } from "./contracts.js";
3
5
  import type { ReviewCommentObservation } from "./pr-babysit.js";
@@ -21,6 +23,45 @@ export interface GitHubClientOptions extends GitHubClientIdentity {
21
23
  fetchImpl?: FetchLike;
22
24
  }
23
25
 
26
+ const GITHUB_APP_KEYS = [
27
+ "GITHUB_APP_ID",
28
+ "GITHUB_APP_INSTALLATION_ID",
29
+ "GITHUB_APP_PRIVATE_KEY",
30
+ ] as const;
31
+ const INSTALLATION_TOKEN_CACHE_BUFFER_MS = 60_000;
32
+
33
+ interface GitHubAppConfig {
34
+ appId: string;
35
+ installationId: string;
36
+ privateKey: string;
37
+ }
38
+
39
+ function base64Url(value: string): string {
40
+ return Buffer.from(value).toString("base64url");
41
+ }
42
+
43
+ function positiveIntegerString(value: string | undefined, key: string): string {
44
+ if (!value || !/^[1-9]\d*$/.test(value)) {
45
+ throw new Error(`${key} must be a positive integer string`);
46
+ }
47
+ return value;
48
+ }
49
+
50
+ function normalizePrivateKey(value: string): string {
51
+ return value.trim().replace(/\\n/g, "\n");
52
+ }
53
+
54
+ function createGitHubAppJwt(config: GitHubAppConfig): string {
55
+ const now = Math.floor(Date.now() / 1000);
56
+ const header = base64Url(JSON.stringify({ alg: "RS256", typ: "JWT" }));
57
+ const payload = base64Url(
58
+ JSON.stringify({ iat: now - 60, exp: now + 540, iss: config.appId }),
59
+ );
60
+ const signer = createSign("RSA-SHA256");
61
+ signer.update(`${header}.${payload}`);
62
+ return `${header}.${payload}.${signer.sign(config.privateKey, "base64url")}`;
63
+ }
64
+
24
65
  export interface GitHubRepositoryRef {
25
66
  owner: string;
26
67
  repo: string;
@@ -368,14 +409,83 @@ export function createGitHubClient(options: GitHubClientOptions) {
368
409
  const fetchImpl = options.fetchImpl ?? fetch;
369
410
  const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
370
411
 
371
- async function token(): Promise<string> {
372
- const value = await resolveConnectorSecret(
373
- "GITHUB_TOKEN",
374
- options.ownerEmail,
375
- {
376
- orgId: options.orgId,
377
- },
412
+ let cachedAppConfig: GitHubAppConfig | null | undefined;
413
+ let cachedInstallationToken: { value: string; expiresAt: number } | undefined;
414
+ let cachedAppBotIdentity: { login: string; id: number } | undefined;
415
+
416
+ async function connectorSecret(key: string): Promise<string | undefined> {
417
+ return resolveConnectorSecret(key, options.ownerEmail, {
418
+ orgId: options.orgId,
419
+ });
420
+ }
421
+
422
+ async function appConfig(): Promise<GitHubAppConfig | null> {
423
+ if (cachedAppConfig !== undefined) return cachedAppConfig;
424
+ const [appId, installationId, privateKey] = await Promise.all(
425
+ GITHUB_APP_KEYS.map((key) => connectorSecret(key)),
378
426
  );
427
+ const configured = [appId, installationId, privateKey].filter(
428
+ Boolean,
429
+ ).length;
430
+ if (configured === 0) {
431
+ cachedAppConfig = null;
432
+ return cachedAppConfig;
433
+ }
434
+ if (configured !== GITHUB_APP_KEYS.length) {
435
+ throw new Error(
436
+ "GitHub App configuration is incomplete; configure GITHUB_APP_ID, GITHUB_APP_INSTALLATION_ID, and GITHUB_APP_PRIVATE_KEY",
437
+ );
438
+ }
439
+ cachedAppConfig = {
440
+ appId: positiveIntegerString(appId, "GITHUB_APP_ID"),
441
+ installationId: positiveIntegerString(
442
+ installationId,
443
+ "GITHUB_APP_INSTALLATION_ID",
444
+ ),
445
+ privateKey: normalizePrivateKey(privateKey as string),
446
+ };
447
+ return cachedAppConfig;
448
+ }
449
+
450
+ async function token(): Promise<string> {
451
+ const app = await appConfig();
452
+ if (app) {
453
+ const now = Math.floor(Date.now() / 1000);
454
+ if (
455
+ cachedInstallationToken &&
456
+ cachedInstallationToken.expiresAt >
457
+ now * 1000 + INSTALLATION_TOKEN_CACHE_BUFFER_MS
458
+ ) {
459
+ return cachedInstallationToken.value;
460
+ }
461
+ const jwt = createGitHubAppJwt(app);
462
+ const response = (await fetchImpl(
463
+ `${baseUrl}/app/installations/${app.installationId}/access_tokens`,
464
+ {
465
+ method: "POST",
466
+ headers: {
467
+ Accept: "application/vnd.github+json",
468
+ Authorization: `Bearer ${jwt}`,
469
+ "X-GitHub-Api-Version": "2022-11-28",
470
+ },
471
+ },
472
+ )) as JsonResponse;
473
+ if (!response.ok) {
474
+ throw new Error(
475
+ `GitHub App installation token request failed: HTTP ${response.status}`,
476
+ );
477
+ }
478
+ const body = record(await response.json());
479
+ const value = requiredString(body.token, "GitHub App installation token");
480
+ const expiresAt = Date.parse(
481
+ requiredString(body.expires_at, "GitHub App token expiry"),
482
+ );
483
+ if (!Number.isFinite(expiresAt))
484
+ throw new Error("GitHub App token expiry is invalid");
485
+ cachedInstallationToken = { value, expiresAt };
486
+ return value;
487
+ }
488
+ const value = await connectorSecret("GITHUB_TOKEN");
379
489
  if (!value)
380
490
  throw new Error("GITHUB_TOKEN is not configured for this workspace");
381
491
  return value;
@@ -603,6 +713,33 @@ export function createGitHubClient(options: GitHubClientOptions) {
603
713
  },
604
714
 
605
715
  async getAuthenticatedUser() {
716
+ const app = await appConfig();
717
+ if (app) {
718
+ if (!cachedAppBotIdentity) {
719
+ const response = (await fetchImpl(`${baseUrl}/app`, {
720
+ headers: {
721
+ Accept: "application/vnd.github+json",
722
+ Authorization: `Bearer ${createGitHubAppJwt(app)}`,
723
+ "X-GitHub-Api-Version": "2022-11-28",
724
+ },
725
+ })) as JsonResponse;
726
+ if (!response.ok) {
727
+ throw new Error(
728
+ `GitHub App metadata request failed: HTTP ${response.status}`,
729
+ );
730
+ }
731
+ const metadata = record(await response.json());
732
+ const login = `${requiredString(metadata.slug, "GitHub App slug")}[bot]`;
733
+ const bot = record(
734
+ await request<unknown>(`/users/${encodeURIComponent(login)}`),
735
+ );
736
+ cachedAppBotIdentity = {
737
+ login: requiredString(bot.login, "GitHub App bot login"),
738
+ id: requiredNumber(bot.id, "GitHub App bot id"),
739
+ };
740
+ }
741
+ return cachedAppBotIdentity;
742
+ }
606
743
  const item = record(await request<unknown>("/user"));
607
744
  return {
608
745
  login: requiredString(item.login, "authenticated GitHub user login"),
@@ -13,21 +13,19 @@ interface FormOgRenderOptions {
13
13
 
14
14
  const WIDTH = 1200;
15
15
  const HEIGHT = 630;
16
- const BRAND_BLUE = "#00B5FF";
17
- const BRAND_MINT = "#48FFE4";
18
- const BG = "#000000";
16
+ const BG = "#0A0A0A";
19
17
  const SURFACE = "#0a0a0a";
20
18
  const BORDER = "#1f1f1f";
21
- const FG = "#ededed";
22
- const MUTED = "#a0a0a0";
19
+ const FG = "#FAF9F5";
20
+ const MUTED = "#9A9997";
23
21
  const FONT_FAMILY = "Liberation Sans, Arial, system-ui, sans-serif";
24
22
 
25
23
  const BADGE_CX = 996;
26
24
  const BADGE_CY = 170;
27
25
 
28
26
  const LOGO_MARK = `
29
- <path d="M24.5537 65.7695H0L15.0859 39.4619L37.708 0L60.4912 39.4619H39.6396L24.5537 65.7695Z" fill="white"/>
30
- <path d="M89.446 0H114L76.2921 65.7704H51.7383L89.446 0Z" fill="url(#brand)"/>
27
+ <path d="M26.8789 71.999H0L16.5146 43.1992L41.2793 0L66.2197 43.1992H43.3945L26.8789 71.999Z" fill="white"/>
28
+ <path d="M97.914 0H124.794L83.5143 72H56.6348L97.914 0Z" fill="white"/>
31
29
  `;
32
30
 
33
31
  const AVATAR_DATA_URL_RE =
@@ -182,28 +180,20 @@ export function renderFormOgImageSvg(input: FormOgImageInput = {}): string {
182
180
  );
183
181
  const avatarContent = profileImageDataUrl
184
182
  ? `<image x="${BADGE_CX - 86}" y="${BADGE_CY - 86}" width="172" height="172" href="${escapeSvg(profileImageDataUrl)}" preserveAspectRatio="xMidYMid slice" mask="url(#avatarMask)"/>`
185
- : `<circle cx="${BADGE_CX}" cy="${BADGE_CY}" r="72" fill="url(#brand)" fill-opacity="0.2"/>
183
+ : `<circle cx="${BADGE_CX}" cy="${BADGE_CY}" r="72" fill="${FG}" fill-opacity="0.12"/>
186
184
  <text x="${BADGE_CX}" y="${BADGE_CY + 20}" text-anchor="middle" font-family="${FONT_FAMILY}" font-size="56" font-weight="800" fill="${FG}">${escapeSvg(initials)}</text>`;
187
185
 
188
186
  return `<svg xmlns="http://www.w3.org/2000/svg" width="${WIDTH}" height="${HEIGHT}" viewBox="0 0 ${WIDTH} ${HEIGHT}">
189
187
  <title>${escapeSvg(title)} - Agent-Native Forms preview</title>
190
188
  <defs>
191
- <linearGradient id="brand" x1="101.702" y1="67.4791" x2="113.672" y2="-37.4275" gradientUnits="userSpaceOnUse">
192
- <stop stop-color="${BRAND_BLUE}"/>
193
- <stop offset="1" stop-color="${BRAND_MINT}"/>
194
- </linearGradient>
195
- <pattern id="grid" width="48" height="48" patternUnits="userSpaceOnUse">
196
- <path d="M 48 0 L 0 0 0 48" fill="none" stroke="#ffffff" stroke-opacity="0.07" stroke-width="1"/>
197
- </pattern>
198
189
  <mask id="avatarMask">
199
190
  <rect width="${WIDTH}" height="${HEIGHT}" fill="black"/>
200
191
  <circle cx="${BADGE_CX}" cy="${BADGE_CY}" r="78" fill="white"/>
201
192
  </mask>
202
193
  </defs>
203
194
  <rect width="${WIDTH}" height="${HEIGHT}" fill="${BG}"/>
204
- <rect width="${WIDTH}" height="${HEIGHT}" fill="url(#grid)"/>
205
195
  <g transform="translate(80 86)">
206
- <g transform="scale(0.62)">
196
+ <g transform="translate(0 14) scale(0.62)">
207
197
  ${LOGO_MARK}
208
198
  </g>
209
199
  <text x="90" y="31" font-family="${FONT_FAMILY}" font-size="28" font-weight="800" fill="${FG}">Agent-Native</text>
@@ -98,6 +98,7 @@ function deckCreativeContext(value: unknown): DeckCreativeContext | null {
98
98
  }
99
99
 
100
100
  export default defineAction({
101
+ title: "Add slide to deck",
101
102
  description:
102
103
  "Add a single slide to the real editable Agent-Native Slides deck. This is the primary Slides MCP edit action: use it after create-deck instead of creating or publishing a standalone HTML artifact. " +
103
104
  "Build decks slide-by-slide — " +
@@ -101,6 +101,7 @@ function deckDeepLink(deckId: string): string {
101
101
  }
102
102
 
103
103
  export default defineAction({
104
+ title: "Create Slides deck",
104
105
  description:
105
106
  "Create the real editable Agent-Native Slides deck, optionally already populated with slides, or atomically replace all slides in an existing deck. This is the primary Slides MCP write action: use it instead of creating or publishing a standalone HTML artifact with the host's file tools. Put slide markup in `slides[].content`; this action persists it and returns an Open in Slides link. " +
106
107
  "For short AI-generated decks in MCP app hosts, pass all generated slides in this call so the real deck editor opens inline already populated. " +
@@ -145,6 +145,7 @@ function deckDeepLink(deckId: string): string {
145
145
  }
146
146
 
147
147
  export default defineAction({
148
+ title: "Read Slides deck",
148
149
  description:
149
150
  "Get a specific deck. Pass slideId to return only that slide; targeted agent reads include full HTML by default. In-app agent calls without slideId return compact slide metadata by default; set compact=false when full deck HTML is needed. Frontend and CLI reads remain full unless compact=true. For any continuation or follow-up, call this first and use generationContext as the canonical original brief, references, theme, and target slide count. For source-preserving work, the compact result includes sourceCoverage; do not claim completion until sourceCoverage.complete is true and its expectedSlideIds and actualSlideIds match in order. User-visible slide numbers are 1-based and match the UI: slide 1 is the first slide. Use slideId for edits.",
150
151
  timeoutMs: 60_000,
@@ -4,6 +4,7 @@ import { z } from "zod";
4
4
  import { writeAppStateForCurrentTab } from "./_tab-state.js";
5
5
 
6
6
  export default defineAction({
7
+ title: "Navigate Slides",
7
8
  description:
8
9
  "Navigate the UI to a specific deck, slide, or view. Writes a navigate command to application state which the UI reads and auto-deletes.",
9
10
  schema: z.object({
@@ -610,6 +610,7 @@ export function isAgentPatchCaller(caller: string | undefined): boolean {
610
610
  // ---------------------------------------------------------------------------
611
611
 
612
612
  export default defineAction({
613
+ title: "Patch Slides deck",
613
614
  description:
614
615
  "Granular deck patch used by the browser editor for concurrent-safe writes. " +
615
616
  "Each operation touches only the target slide or field — concurrent writers " +
@@ -278,6 +278,7 @@ function assertStyleOnlyEdit(
278
278
  }
279
279
 
280
280
  export default defineAction({
281
+ title: "Edit one Slides slide",
281
282
  description:
282
283
  "Atomically patch a slide's HTML like a code editor: send several exact edits against the current source, optionally format it with Prettier, and sync the result live to open editors. Use exactly one input mode: edits, legacy find/replace, or fullContent. Mixed modes are rejected and write nothing. Prefer edits over fullContent so unrelated markup is not regenerated, especially for style-only requests, reorders, or changes that must stay consistent across lists, tables, or other representations. For style-only requests, set styleOnly=true and use edits that change only the requested CSS declarations and preserve text and layout properties; the action rejects text or markup changes and fullContent in that mode. Never use unresolved placeholder markers as stand-ins for preserved content. Use baseContentHash from get-deck to reject stale patches, then re-read the targeted slide to verify every affected representation and the requested scope. Content edits clear existing click-reveal metadata; style-only CSS edits preserve it because the HTML structure remains stable. Use patch-deck with the complete animations list when a content edit intentionally changes both content and reveals. Source-imported slides preserve their original images and factual copy by default. The action returns immediately after persistence; layoutFit.status=pending means the open editor will measure the new content asynchronously, and get-layout-overflows can check the returned contentHash plus layoutFitRevision later.",
283
284
  schema: z.object({
@@ -86,6 +86,7 @@ function getCurrentSlideFitMeasurement(
86
86
  }
87
87
 
88
88
  export default defineAction({
89
+ title: "Inspect current Slides screen",
89
90
  description:
90
91
  "See what the user is currently looking at. Returns the CURRENT deck ID, current slide ID, and the full list of slide IDs in the open deck (or the deck list if the user is on the home page). Call this before any slide operation to get the exact IDs you need for add-slide / update-slide / create-deck.",
91
92
  schema: z.object({}),
@@ -132,6 +132,8 @@ export default createAgentChatPlugin({
132
132
  initialToolNames: INITIAL_TOOL_NAMES,
133
133
  mcp: {
134
134
  connectorCatalog: INITIAL_TOOL_NAMES,
135
+ instructions:
136
+ "For deck edits, call view-screen first when the active deck or slide ID is unknown. Use get-deck to read the target, update-slide for one-slide edits, and patch-deck for deck-wide or multi-slide changes. Read back with get-deck after writing.",
135
137
  },
136
138
  durableBackgroundRuns: true,
137
139
  runSoftTimeoutMs: SLIDES_BACKGROUND_RUN_SOFT_TIMEOUT_MS,
package/dist/action.d.ts CHANGED
@@ -390,6 +390,8 @@ type InferParams<T extends Record<string, ParameterSchema> | undefined> = T exte
390
390
  */
391
391
  export type ActionOutputErrorStrategy = "strict" | "warn" | "fallback";
392
392
  interface DefineActionWithSchema<TSchema extends StandardSchemaV1, TReturn = any, TOutputSchema extends StandardSchemaV1 | undefined = undefined> {
393
+ /** Optional human-facing tool title used by WebMCP and MCP hosts. */
394
+ title?: string;
393
395
  description: string;
394
396
  /** Standard Schema-compatible schema (Zod, Valibot, ArkType). Provides runtime
395
397
  * validation and full TypeScript type inference for `run()` args. The schema is
@@ -616,6 +618,8 @@ interface DefineActionWithSchema<TSchema extends StandardSchemaV1, TReturn = any
616
618
  audit?: ActionAuditConfig;
617
619
  }
618
620
  interface DefineActionWithParams<TParams extends Record<string, ParameterSchema> | undefined = Record<string, ParameterSchema> | undefined, TReturn = any> {
621
+ /** Optional human-facing tool title used by WebMCP and MCP hosts. */
622
+ title?: string;
619
623
  description: string;
620
624
  /** Flat map of parameter names to their schema. Automatically wrapped in
621
625
  * `{ type: "object", properties: ... }` for the Claude API. */
package/dist/action.js CHANGED
@@ -214,6 +214,9 @@ export function defineAction(options) {
214
214
  const chatUI = normalizeActionChatUIConfig(options.chatUI);
215
215
  return {
216
216
  tool: {
217
+ ...(typeof options.title === "string" && options.title.trim()
218
+ ? { title: options.title.trim() }
219
+ : {}),
217
220
  description: options.description,
218
221
  parameters: toolParameters,
219
222
  },
@@ -25,6 +25,7 @@ export interface AgentNativeJsonSchema {
25
25
  maxItems?: number;
26
26
  }
27
27
  export interface ActionTool {
28
+ title?: string;
28
29
  description: string;
29
30
  parameters?: AgentNativeJsonSchema & {
30
31
  type: "object";
@@ -186,8 +186,8 @@ export declare function createAutomationCallbackHandler(runtime: AutomationRunti
186
186
  duplicate?: undefined;
187
187
  eventId?: undefined;
188
188
  } | {
189
- error?: undefined;
190
189
  accepted: boolean;
191
190
  duplicate: boolean;
192
191
  eventId: string;
192
+ error?: undefined;
193
193
  }>;
@@ -1,3 +1,4 @@
1
+ import { agentNativeToolTitle } from "../shared/agent-mcp-metadata.js";
1
2
  export class AgentNativeWebMcpUnsupportedError extends Error {
2
3
  constructor() {
3
4
  super("WebMCP is not supported by this browser or document");
@@ -254,6 +255,7 @@ export function createAgentNativeServerActionWebMcpRegistration(options) {
254
255
  }
255
256
  return manifest.map((action) => ({
256
257
  name: action.name,
258
+ title: agentNativeToolTitle(action.name, action.title),
257
259
  description: action.description,
258
260
  ...(action.inputSchema ? { schema: action.inputSchema } : {}),
259
261
  ...(action.readOnly ? { readOnly: true } : {}),
@@ -310,7 +312,10 @@ function sensitiveAction(action) {
310
312
  Boolean(action.approval));
311
313
  }
312
314
  function actionManifest(action) {
313
- const manifest = { ...action };
315
+ const manifest = {
316
+ ...action,
317
+ title: agentNativeToolTitle(action.name, action.title),
318
+ };
314
319
  delete manifest.run;
315
320
  return manifest;
316
321
  }
@@ -418,7 +423,7 @@ export function createAgentNativeWebMcpRegistration(options) {
418
423
  }
419
424
  await modelContext.registerTool({
420
425
  name: action.name,
421
- ...(action.title ? { title: action.title } : {}),
426
+ title: agentNativeToolTitle(action.name, action.title),
422
427
  description: action.description,
423
428
  inputSchema,
424
429
  annotations: {
@@ -62,11 +62,11 @@ export declare const postAwareness: import("h3").EventHandlerWithFetch<import("h
62
62
  error: string;
63
63
  states?: undefined;
64
64
  } | {
65
- error?: undefined;
66
65
  states: {
67
66
  clientId: number;
68
67
  state: string;
69
68
  }[];
69
+ error?: undefined;
70
70
  }>>;
71
71
  /**
72
72
  * GET /_agent-native/collab/:docId/users
@@ -77,9 +77,9 @@ export declare const getActiveUsers: import("h3").EventHandlerWithFetch<import("
77
77
  error: string;
78
78
  users?: undefined;
79
79
  } | {
80
- error?: undefined;
81
80
  users: {
82
81
  clientId: number;
83
82
  lastSeen: number;
84
83
  }[];
84
+ error?: undefined;
85
85
  }>>;
@@ -35,6 +35,8 @@ export interface MCPConfig {
35
35
  appId?: string;
36
36
  /** App description */
37
37
  description: string;
38
+ /** Additional host-facing guidance included in the MCP initialize response. */
39
+ instructions?: string;
38
40
  /** Optional canonical website URL for hosts that surface MCP app details. */
39
41
  websiteUrl?: string;
40
42
  /** Optional app icons for MCP hosts that render server branding. */
@@ -25,6 +25,7 @@ import { writeActionChangeMarker } from "../server/action-change-marker-write.js
25
25
  import { getConfiguredAppBasePath } from "../server/app-base-path.js";
26
26
  import { buildDeepLink, toAbsoluteOpenUrl, toDesktopOpenUrl, toVsCodeOpenUrl, } from "../server/deep-link.js";
27
27
  import { getRequestContext, getRequestOrgId, getRequestUserEmail, runWithRequestContext, } from "../server/request-context.js";
28
+ import { agentNativeMcpInstructions, agentNativeToolTitle, } from "../shared/agent-mcp-metadata.js";
28
29
  import { isAgentNativeOpenDeepLink, withCollapsedAgentSidebarParam, } from "../shared/agent-sidebar-url.js";
29
30
  import { MCP_APP_CHAT_BRIDGE_QUERY_PARAM } from "../shared/embed-auth.js";
30
31
  import { describeMcpError, readClientInfoFromRequest, trackMcpResourceRead, trackMcpResourcesList, trackMcpToolCall, trackMcpToolsList, } from "./analytics.js";
@@ -1315,6 +1316,7 @@ export async function createMCPServerForRequest(config, identity, requestMeta) {
1315
1316
  const supportsMcpApps = compactMcpAppCatalog ||
1316
1317
  Object.values(advertisedActions).some((entry) => Boolean(entry.mcpApp?.resource));
1317
1318
  const server = new Server(mcpServerInfo(config, requestMeta), {
1319
+ instructions: agentNativeMcpInstructions(config.instructions),
1318
1320
  capabilities: {
1319
1321
  tools: {},
1320
1322
  ...(supportsMcpApps
@@ -1486,7 +1488,9 @@ export async function createMCPServerForRequest(config, identity, requestMeta) {
1486
1488
  : {}),
1487
1489
  };
1488
1490
  const baseDescription = entry.tool.description ?? name;
1491
+ const title = agentNativeToolTitle(name, entry.tool.title);
1489
1492
  const annotations = {
1493
+ title,
1490
1494
  readOnlyHint: entry.readOnly === true,
1491
1495
  destructiveHint: entry.publicAgent?.isConsequential === true ||
1492
1496
  entry.needsApproval !== undefined,
@@ -11,14 +11,14 @@
11
11
  * DELETE /_agent-native/notifications/:id — delete
12
12
  */
13
13
  export declare function createNotificationsHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<"" | import("./types.js").Notification[] | {
14
- error?: undefined;
15
14
  count: number;
16
15
  updated?: undefined;
16
+ error?: undefined;
17
17
  ok?: undefined;
18
18
  } | {
19
- error?: undefined;
20
19
  count?: undefined;
21
20
  updated: number;
21
+ error?: undefined;
22
22
  ok?: undefined;
23
23
  } | {
24
24
  count?: undefined;
@@ -26,8 +26,8 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
26
26
  error: string;
27
27
  ok?: undefined;
28
28
  } | {
29
- error?: undefined;
30
29
  count?: undefined;
31
30
  updated?: undefined;
31
+ error?: undefined;
32
32
  ok: boolean;
33
33
  }>>;
@@ -41,27 +41,27 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
41
41
  thumbsUpRate: number;
42
42
  avgEvalScore: number;
43
43
  } | {
44
- error?: undefined;
45
- ok?: undefined;
46
44
  summary: import("./types.js").TraceSummary;
47
45
  spans: import("./types.js").TraceSpan[];
46
+ ok?: undefined;
47
+ error?: undefined;
48
48
  id?: undefined;
49
49
  } | {
50
- error?: undefined;
51
- ok?: undefined;
52
50
  summary?: undefined;
53
51
  spans?: undefined;
54
52
  id: string;
55
- } | {
56
53
  ok?: undefined;
54
+ error?: undefined;
55
+ } | {
57
56
  summary?: undefined;
58
57
  spans?: undefined;
59
58
  error: any;
59
+ ok?: undefined;
60
60
  id?: undefined;
61
61
  } | {
62
- error?: undefined;
63
62
  summary?: undefined;
64
63
  spans?: undefined;
65
64
  ok: boolean;
65
+ error?: undefined;
66
66
  id?: undefined;
67
67
  }>>;