@agent-native/core 0.109.1 → 0.109.3

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 (135) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +15 -0
  3. package/corpus/core/docs/content/integrations.mdx +87 -253
  4. package/corpus/core/package.json +1 -1
  5. package/corpus/core/src/cli/create.ts +18 -12
  6. package/corpus/core/src/client/blocks/library/diagram.config.ts +10 -1
  7. package/corpus/core/src/client/blocks/library/diagram.tsx +9 -4
  8. package/corpus/core/src/client/require-session.tsx +19 -3
  9. package/corpus/core/src/client/use-action.ts +145 -2
  10. package/corpus/core/src/client/use-session.ts +79 -48
  11. package/corpus/core/src/db/client.ts +13 -7
  12. package/corpus/core/src/db/create-get-db.ts +20 -13
  13. package/corpus/core/src/db/request-telemetry.ts +154 -0
  14. package/corpus/core/src/deploy/build.ts +63 -3
  15. package/corpus/core/src/integrations/index.ts +1 -0
  16. package/corpus/core/src/integrations/plugin.ts +9 -3
  17. package/corpus/core/src/integrations/remote-push-delivery-job.ts +48 -0
  18. package/corpus/core/src/integrations/remote-push-delivery.ts +370 -0
  19. package/corpus/core/src/integrations/remote-push-store.ts +275 -26
  20. package/corpus/core/src/server/better-auth-instance.ts +11 -3
  21. package/corpus/core/src/server/core-routes-plugin.ts +36 -14
  22. package/corpus/core/src/server/framework-request-handler.ts +11 -1
  23. package/corpus/core/src/server/http-response-telemetry.ts +233 -31
  24. package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +19 -0
  25. package/corpus/templates/clips/.agents/skills/dictate/SKILL.md +28 -5
  26. package/corpus/templates/clips/.agents/skills/meetings/SKILL.md +7 -0
  27. package/corpus/templates/clips/.agents/skills/recording/SKILL.md +23 -0
  28. package/corpus/templates/clips/AGENTS.md +10 -0
  29. package/corpus/templates/clips/actions/create-dictation.ts +29 -4
  30. package/corpus/templates/clips/actions/finalize-recording.ts +1 -1
  31. package/corpus/templates/clips/actions/get-recording-player-data.ts +16 -6
  32. package/corpus/templates/clips/actions/set-mobile-capture-state.ts +40 -0
  33. package/corpus/templates/clips/actions/update-dictation.ts +9 -1
  34. package/corpus/templates/clips/app/components/library/library-layout.tsx +1 -6
  35. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +13 -2
  36. package/corpus/templates/clips/app/routes/share.$shareId.tsx +13 -2
  37. package/corpus/templates/clips/changelog/2026-07-17-capture-dictation-background-meeting-audio-and-camera-videos.md +6 -0
  38. package/corpus/templates/clips/changelog/2026-07-17-delayed-upload-verification-keeps-local-backups-retryable.md +6 -0
  39. package/corpus/templates/clips/changelog/2026-07-18-new-recording-is-now-emphasized-as-the-primary-action-in-the.md +6 -0
  40. package/corpus/templates/clips/changelog/2026-07-18-recordings-now-remain-safely-recoverable-until-the-stored-me.md +6 -0
  41. package/corpus/templates/clips/desktop/src/app.tsx +17 -0
  42. package/corpus/templates/clips/desktop/src/lib/processing-backup-recovery.ts +19 -0
  43. package/corpus/templates/clips/desktop/src/lib/recorder.ts +60 -35
  44. package/corpus/templates/clips/desktop/src/lib/upload-verification.ts +12 -0
  45. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +1 -0
  46. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +14 -0
  47. package/corpus/templates/clips/server/db/schema.ts +9 -1
  48. package/corpus/templates/clips/server/lib/builder-media-compression.ts +59 -7
  49. package/corpus/templates/clips/server/lib/media-verification-state.ts +17 -0
  50. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +7 -0
  51. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +22 -1
  52. package/corpus/templates/plan/changelog/2026-07-17-diagrams-can-opt-into-a-polished-clean-rendering-mode-withou.md +6 -0
  53. package/corpus/templates/plan/shared/plan-content.ts +3 -0
  54. package/corpus/templates/tasks/AGENTS.md +3 -0
  55. package/corpus/templates/tasks/actions/render-task-list-inline.ts +202 -0
  56. package/corpus/templates/tasks/changelog/2026-07-18-task-lists-can-now-appear-as-interactive-widgets-inline-in-c.md +6 -0
  57. package/corpus/templates/tasks/docs/features/f8-chat.mdx +2 -0
  58. package/corpus/templates/tasks/server/plugins/agent-chat.ts +2 -0
  59. package/dist/cli/create.d.ts +10 -1
  60. package/dist/cli/create.d.ts.map +1 -1
  61. package/dist/cli/create.js +17 -13
  62. package/dist/cli/create.js.map +1 -1
  63. package/dist/client/blocks/library/diagram.config.d.ts +2 -0
  64. package/dist/client/blocks/library/diagram.config.d.ts.map +1 -1
  65. package/dist/client/blocks/library/diagram.config.js +8 -1
  66. package/dist/client/blocks/library/diagram.config.js.map +1 -1
  67. package/dist/client/blocks/library/diagram.d.ts.map +1 -1
  68. package/dist/client/blocks/library/diagram.js +9 -4
  69. package/dist/client/blocks/library/diagram.js.map +1 -1
  70. package/dist/client/require-session.d.ts.map +1 -1
  71. package/dist/client/require-session.js +6 -3
  72. package/dist/client/require-session.js.map +1 -1
  73. package/dist/client/use-action.d.ts.map +1 -1
  74. package/dist/client/use-action.js +117 -1
  75. package/dist/client/use-action.js.map +1 -1
  76. package/dist/client/use-session.d.ts.map +1 -1
  77. package/dist/client/use-session.js +72 -50
  78. package/dist/client/use-session.js.map +1 -1
  79. package/dist/collab/routes.d.ts +1 -1
  80. package/dist/db/client.d.ts.map +1 -1
  81. package/dist/db/client.js +7 -1
  82. package/dist/db/client.js.map +1 -1
  83. package/dist/db/create-get-db.d.ts +3 -6
  84. package/dist/db/create-get-db.d.ts.map +1 -1
  85. package/dist/db/create-get-db.js +9 -2
  86. package/dist/db/create-get-db.js.map +1 -1
  87. package/dist/db/request-telemetry.d.ts +23 -0
  88. package/dist/db/request-telemetry.d.ts.map +1 -0
  89. package/dist/db/request-telemetry.js +95 -0
  90. package/dist/db/request-telemetry.js.map +1 -0
  91. package/dist/deploy/build.d.ts +3 -0
  92. package/dist/deploy/build.d.ts.map +1 -1
  93. package/dist/deploy/build.js +45 -1
  94. package/dist/deploy/build.js.map +1 -1
  95. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  96. package/dist/integrations/index.d.ts +1 -0
  97. package/dist/integrations/index.d.ts.map +1 -1
  98. package/dist/integrations/index.js +1 -0
  99. package/dist/integrations/index.js.map +1 -1
  100. package/dist/integrations/plugin.d.ts.map +1 -1
  101. package/dist/integrations/plugin.js +8 -3
  102. package/dist/integrations/plugin.js.map +1 -1
  103. package/dist/integrations/remote-push-delivery-job.d.ts +4 -0
  104. package/dist/integrations/remote-push-delivery-job.d.ts.map +1 -0
  105. package/dist/integrations/remote-push-delivery-job.js +44 -0
  106. package/dist/integrations/remote-push-delivery-job.js.map +1 -0
  107. package/dist/integrations/remote-push-delivery.d.ts +11 -0
  108. package/dist/integrations/remote-push-delivery.d.ts.map +1 -0
  109. package/dist/integrations/remote-push-delivery.js +265 -0
  110. package/dist/integrations/remote-push-delivery.js.map +1 -0
  111. package/dist/integrations/remote-push-store.d.ts +33 -0
  112. package/dist/integrations/remote-push-store.d.ts.map +1 -1
  113. package/dist/integrations/remote-push-store.js +195 -26
  114. package/dist/integrations/remote-push-store.js.map +1 -1
  115. package/dist/observability/routes.d.ts +5 -5
  116. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  117. package/dist/resources/handlers.d.ts +1 -1
  118. package/dist/server/better-auth-instance.js +7 -3
  119. package/dist/server/better-auth-instance.js.map +1 -1
  120. package/dist/server/core-routes-plugin.d.ts +8 -0
  121. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  122. package/dist/server/core-routes-plugin.js +23 -8
  123. package/dist/server/core-routes-plugin.js.map +1 -1
  124. package/dist/server/framework-request-handler.d.ts.map +1 -1
  125. package/dist/server/framework-request-handler.js +9 -1
  126. package/dist/server/framework-request-handler.js.map +1 -1
  127. package/dist/server/http-response-telemetry.d.ts +3 -2
  128. package/dist/server/http-response-telemetry.d.ts.map +1 -1
  129. package/dist/server/http-response-telemetry.js +160 -27
  130. package/dist/server/http-response-telemetry.js.map +1 -1
  131. package/dist/server/transcribe-voice.d.ts +1 -1
  132. package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +19 -0
  133. package/docs/content/integrations.mdx +87 -253
  134. package/package.json +1 -1
  135. package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +19 -0
@@ -0,0 +1,370 @@
1
+ import {
2
+ claimNextRemotePushDelivery,
3
+ deactivateRemotePushRegistration,
4
+ failRemotePushDelivery,
5
+ markRemotePushDelivered,
6
+ markRemotePushTicketAccepted,
7
+ retryRemotePushDelivery,
8
+ type ClaimedRemotePushDelivery,
9
+ } from "./remote-push-store.js";
10
+
11
+ const EXPO_PUSH_SEND_URL = "https://exp.host/--/api/v2/push/send";
12
+ const EXPO_PUSH_RECEIPTS_URL = "https://exp.host/--/api/v2/push/getReceipts";
13
+ const REQUEST_TIMEOUT_MS = 10_000;
14
+ const RECEIPT_CHECK_DELAY_MS = 15 * 60_000;
15
+ const MAX_DELIVERY_ATTEMPTS = 12;
16
+ const DEFAULT_DELIVERY_LIMIT = 100;
17
+
18
+ type ExpoResult = {
19
+ status?: unknown;
20
+ id?: unknown;
21
+ message?: unknown;
22
+ details?: { error?: unknown };
23
+ };
24
+
25
+ type ExpoPushResponse = {
26
+ data?: ExpoResult | ExpoResult[] | Record<string, ExpoResult>;
27
+ errors?: Array<{ code?: unknown; message?: unknown }>;
28
+ };
29
+
30
+ type DeliveryOutcome =
31
+ | { kind: "ticket"; ticketId: string }
32
+ | { kind: "delivered" }
33
+ | { kind: "retry"; errorCode: string; resend?: boolean }
34
+ | { kind: "failed"; errorCode: string; deactivate?: boolean };
35
+
36
+ export async function deliverPendingRemotePushNotifications(options?: {
37
+ fetchImpl?: typeof fetch;
38
+ now?: () => number;
39
+ limit?: number;
40
+ }): Promise<{
41
+ sent: number;
42
+ delivered: number;
43
+ retried: number;
44
+ failed: number;
45
+ }> {
46
+ const fetchImpl = options?.fetchImpl ?? fetch;
47
+ const now = options?.now ?? Date.now;
48
+ const limit = Math.max(
49
+ 1,
50
+ Math.min(options?.limit ?? DEFAULT_DELIVERY_LIMIT, 100),
51
+ );
52
+ const summary = { sent: 0, delivered: 0, retried: 0, failed: 0 };
53
+ const claimedDeliveries: ClaimedRemotePushDelivery[] = [];
54
+
55
+ for (let processed = 0; processed < limit; processed++) {
56
+ const delivery = await claimNextRemotePushDelivery({ now: now() });
57
+ if (!delivery) break;
58
+
59
+ if (delivery.attempts > MAX_DELIVERY_ATTEMPTS) {
60
+ await failRemotePushDelivery({
61
+ id: delivery.id,
62
+ phase: delivery.phase,
63
+ errorCode: "attempts_exhausted",
64
+ });
65
+ summary.failed++;
66
+ continue;
67
+ }
68
+
69
+ claimedDeliveries.push(delivery);
70
+ }
71
+
72
+ const sendDeliveries = claimedDeliveries.filter(
73
+ (delivery) => delivery.phase === "send",
74
+ );
75
+ if (sendDeliveries.length > 0) {
76
+ let outcomes: DeliveryOutcome[];
77
+ try {
78
+ outcomes = await sendExpoPushNotifications(sendDeliveries, fetchImpl);
79
+ } catch (error) {
80
+ const errorCode = classifyTransportError(error);
81
+ outcomes = sendDeliveries.map(() => ({ kind: "retry", errorCode }));
82
+ }
83
+ for (let index = 0; index < sendDeliveries.length; index++) {
84
+ await applyDeliveryOutcome(
85
+ sendDeliveries[index]!,
86
+ outcomes[index]!,
87
+ summary,
88
+ now,
89
+ );
90
+ }
91
+ }
92
+
93
+ for (const delivery of claimedDeliveries) {
94
+ if (delivery.phase !== "receipt") continue;
95
+ let outcome: DeliveryOutcome;
96
+ try {
97
+ outcome = await readExpoPushReceipt(delivery, fetchImpl);
98
+ } catch (error) {
99
+ outcome = {
100
+ kind: "retry",
101
+ errorCode: classifyTransportError(error),
102
+ };
103
+ }
104
+
105
+ await applyDeliveryOutcome(delivery, outcome, summary, now);
106
+ }
107
+
108
+ return summary;
109
+ }
110
+
111
+ async function applyDeliveryOutcome(
112
+ delivery: ClaimedRemotePushDelivery,
113
+ outcome: DeliveryOutcome,
114
+ summary: {
115
+ sent: number;
116
+ delivered: number;
117
+ retried: number;
118
+ failed: number;
119
+ },
120
+ now: () => number,
121
+ ): Promise<void> {
122
+ if (outcome.kind === "ticket") {
123
+ await markRemotePushTicketAccepted({
124
+ id: delivery.id,
125
+ providerTicketId: outcome.ticketId,
126
+ checkAfter: now() + RECEIPT_CHECK_DELAY_MS,
127
+ });
128
+ summary.sent++;
129
+ return;
130
+ }
131
+ if (outcome.kind === "delivered") {
132
+ await markRemotePushDelivered(delivery.id);
133
+ summary.delivered++;
134
+ return;
135
+ }
136
+ if (outcome.kind === "failed") {
137
+ await failRemotePushDelivery({
138
+ id: delivery.id,
139
+ phase: delivery.phase,
140
+ errorCode: outcome.errorCode,
141
+ });
142
+ if (outcome.deactivate) {
143
+ await deactivateRemotePushRegistration(delivery.registrationId);
144
+ }
145
+ summary.failed++;
146
+ return;
147
+ }
148
+
149
+ await retryRemotePushDelivery({
150
+ id: delivery.id,
151
+ phase: delivery.phase,
152
+ retryAt: now() + retryDelayMs(delivery.attempts),
153
+ errorCode: outcome.errorCode,
154
+ resend: outcome.resend,
155
+ });
156
+ summary.retried++;
157
+ }
158
+
159
+ async function sendExpoPushNotifications(
160
+ deliveries: ClaimedRemotePushDelivery[],
161
+ fetchImpl: typeof fetch,
162
+ ): Promise<DeliveryOutcome[]> {
163
+ const outcomes = new Map<string, DeliveryOutcome>();
164
+ const supportedDeliveries = deliveries.filter((delivery) => {
165
+ const supported =
166
+ delivery.provider === "expo" && isExpoPushToken(delivery.token);
167
+ if (!supported) {
168
+ outcomes.set(delivery.id, {
169
+ kind: "failed",
170
+ errorCode: "unsupported_push_registration",
171
+ deactivate: true,
172
+ });
173
+ }
174
+ return supported;
175
+ });
176
+ if (supportedDeliveries.length === 0) {
177
+ return deliveries.map((delivery) => outcomes.get(delivery.id)!);
178
+ }
179
+
180
+ const messages = supportedDeliveries.map(buildExpoMessage);
181
+ const response = await fetchWithTimeout(
182
+ fetchImpl,
183
+ EXPO_PUSH_SEND_URL,
184
+ expoRequestInit(messages.length === 1 ? messages[0] : messages),
185
+ );
186
+ const body = await readExpoResponse(response);
187
+ if (!response.ok) {
188
+ const outcome = responseFailure(response.status, body);
189
+ for (const delivery of supportedDeliveries) {
190
+ outcomes.set(delivery.id, outcome);
191
+ }
192
+ return deliveries.map((delivery) => outcomes.get(delivery.id)!);
193
+ }
194
+
195
+ const tickets = Array.isArray(body.data)
196
+ ? body.data
197
+ : body.data && isExpoResult(body.data)
198
+ ? [body.data]
199
+ : [];
200
+ if (
201
+ tickets.length !== supportedDeliveries.length ||
202
+ tickets.some((ticket) => !isExpoResult(ticket))
203
+ ) {
204
+ for (const delivery of supportedDeliveries) {
205
+ outcomes.set(delivery.id, {
206
+ kind: "retry",
207
+ errorCode: "invalid_push_ticket",
208
+ });
209
+ }
210
+ return deliveries.map((delivery) => outcomes.get(delivery.id)!);
211
+ }
212
+
213
+ for (let index = 0; index < supportedDeliveries.length; index++) {
214
+ const delivery = supportedDeliveries[index]!;
215
+ const ticket = tickets[index]!;
216
+ outcomes.set(
217
+ delivery.id,
218
+ ticket.status === "ok" && typeof ticket.id === "string"
219
+ ? { kind: "ticket", ticketId: ticket.id }
220
+ : expoResultFailure(ticket, false),
221
+ );
222
+ }
223
+ return deliveries.map((delivery) => outcomes.get(delivery.id)!);
224
+ }
225
+
226
+ async function readExpoPushReceipt(
227
+ delivery: ClaimedRemotePushDelivery,
228
+ fetchImpl: typeof fetch,
229
+ ): Promise<DeliveryOutcome> {
230
+ if (!delivery.providerTicketId) {
231
+ return { kind: "retry", errorCode: "missing_push_ticket", resend: true };
232
+ }
233
+ const response = await fetchWithTimeout(
234
+ fetchImpl,
235
+ EXPO_PUSH_RECEIPTS_URL,
236
+ expoRequestInit({ ids: [delivery.providerTicketId] }),
237
+ );
238
+ const body = await readExpoResponse(response);
239
+ if (!response.ok) return responseFailure(response.status, body);
240
+
241
+ const receipts = body.data;
242
+ if (!receipts || Array.isArray(receipts) || isExpoResult(receipts)) {
243
+ return { kind: "retry", errorCode: "push_receipt_unavailable" };
244
+ }
245
+ const receipt = receipts[delivery.providerTicketId];
246
+ if (!receipt) {
247
+ return { kind: "retry", errorCode: "push_receipt_unavailable" };
248
+ }
249
+ if (receipt.status === "ok") return { kind: "delivered" };
250
+ return expoResultFailure(receipt, true);
251
+ }
252
+
253
+ function buildExpoMessage(delivery: ClaimedRemotePushDelivery) {
254
+ const payload = readRecord(delivery.payload);
255
+ const title = boundedString(payload?.title, 120) ?? "Agent Native update";
256
+ const body = boundedString(payload?.body, 300);
257
+ const data = compactData(payload);
258
+ return {
259
+ to: delivery.token,
260
+ title,
261
+ ...(body ? { body } : {}),
262
+ sound: "default",
263
+ priority: "high",
264
+ data: {
265
+ url: "agentnative://sessions",
266
+ ...data,
267
+ },
268
+ };
269
+ }
270
+
271
+ function compactData(payload: Record<string, unknown> | null) {
272
+ if (!payload) return {};
273
+ const data: Record<string, string | number> = {};
274
+ for (const key of ["commandId", "hostId", "kind", "status"] as const) {
275
+ const value = boundedString(payload[key], 200);
276
+ if (value) data[key] = value;
277
+ }
278
+ if (
279
+ typeof payload.updatedAt === "number" &&
280
+ Number.isFinite(payload.updatedAt)
281
+ ) {
282
+ data.updatedAt = payload.updatedAt;
283
+ }
284
+ return data;
285
+ }
286
+
287
+ function expoRequestInit(body: unknown): RequestInit {
288
+ const headers: Record<string, string> = {
289
+ Accept: "application/json",
290
+ "Content-Type": "application/json",
291
+ };
292
+ const accessToken = process.env.EXPO_ACCESS_TOKEN?.trim();
293
+ if (accessToken) headers.Authorization = `Bearer ${accessToken}`;
294
+ return { method: "POST", headers, body: JSON.stringify(body) };
295
+ }
296
+
297
+ async function fetchWithTimeout(
298
+ fetchImpl: typeof fetch,
299
+ url: string,
300
+ init: RequestInit,
301
+ ): Promise<Response> {
302
+ const controller = new AbortController();
303
+ const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
304
+ try {
305
+ return await fetchImpl(url, { ...init, signal: controller.signal });
306
+ } finally {
307
+ clearTimeout(timeout);
308
+ }
309
+ }
310
+
311
+ async function readExpoResponse(response: Response): Promise<ExpoPushResponse> {
312
+ const body = await response.json().catch(() => null);
313
+ return (readRecord(body) ?? {}) as ExpoPushResponse;
314
+ }
315
+
316
+ function responseFailure(
317
+ status: number,
318
+ body: ExpoPushResponse,
319
+ ): DeliveryOutcome {
320
+ const code = boundedString(body.errors?.[0]?.code, 120);
321
+ if (status === 429 || status >= 500) {
322
+ return { kind: "retry", errorCode: code ?? `expo_http_${status}` };
323
+ }
324
+ return { kind: "failed", errorCode: code ?? `expo_http_${status}` };
325
+ }
326
+
327
+ function expoResultFailure(
328
+ result: ExpoResult,
329
+ fromReceipt: boolean,
330
+ ): DeliveryOutcome {
331
+ const code = boundedString(result.details?.error, 120) ?? "expo_push_error";
332
+ if (code === "DeviceNotRegistered") {
333
+ return { kind: "failed", errorCode: code, deactivate: true };
334
+ }
335
+ if (code === "MessageRateExceeded") {
336
+ return { kind: "retry", errorCode: code, resend: fromReceipt };
337
+ }
338
+ return { kind: "failed", errorCode: code };
339
+ }
340
+
341
+ function retryDelayMs(attempt: number): number {
342
+ return Math.min(60 * 60_000, 5_000 * 2 ** Math.max(0, attempt - 1));
343
+ }
344
+
345
+ function classifyTransportError(error: unknown): string {
346
+ if (error instanceof DOMException && error.name === "AbortError") {
347
+ return "expo_request_timeout";
348
+ }
349
+ return "expo_transport_error";
350
+ }
351
+
352
+ function isExpoPushToken(value: string): boolean {
353
+ return /^(Expo(nent)?PushToken)\[[A-Za-z0-9_-]+\]$/.test(value);
354
+ }
355
+
356
+ function isExpoResult(value: unknown): value is ExpoResult {
357
+ return value !== null && typeof value === "object" && "status" in value;
358
+ }
359
+
360
+ function readRecord(value: unknown): Record<string, unknown> | null {
361
+ return value && typeof value === "object" && !Array.isArray(value)
362
+ ? (value as Record<string, unknown>)
363
+ : null;
364
+ }
365
+
366
+ function boundedString(value: unknown, maxLength: number): string | null {
367
+ if (typeof value !== "string") return null;
368
+ const trimmed = value.trim();
369
+ return trimmed ? trimmed.slice(0, maxLength) : null;
370
+ }