@xfxstudio/claworld 0.2.10-beta.3 → 0.2.12

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.
@@ -0,0 +1,192 @@
1
+ function normalizeText(value, fallback = null) {
2
+ if (value == null) return fallback;
3
+ const normalized = String(value).trim();
4
+ return normalized || fallback;
5
+ }
6
+
7
+ function normalizePositiveInteger(value, fallback = null) {
8
+ const normalized = Number(value);
9
+ if (!Number.isFinite(normalized) || normalized <= 0) return fallback;
10
+ return Math.floor(normalized);
11
+ }
12
+
13
+ function toIsoFromEpochSeconds(value, fallback = null) {
14
+ const normalized = normalizePositiveInteger(value, null);
15
+ return normalized ? new Date(normalized * 1000).toISOString() : fallback;
16
+ }
17
+
18
+ function createImageHostStorageError({
19
+ code,
20
+ message,
21
+ status = 502,
22
+ url = null,
23
+ responseStatus = null,
24
+ responseBody = null,
25
+ } = {}) {
26
+ const error = new Error(message || code || 'agent_card_image_host_failed');
27
+ error.code = code || 'agent_card_image_host_failed';
28
+ error.status = status;
29
+ error.responseBody = {
30
+ error: error.code,
31
+ message: error.message,
32
+ ...(url ? { url } : {}),
33
+ ...(Number.isInteger(responseStatus) ? { responseStatus } : {}),
34
+ ...(responseBody != null ? { responseBody } : {}),
35
+ };
36
+ return error;
37
+ }
38
+
39
+ async function parseJsonResponse(response) {
40
+ const text = await response.text();
41
+ if (!text) return null;
42
+ try {
43
+ return JSON.parse(text);
44
+ } catch {
45
+ return text;
46
+ }
47
+ }
48
+
49
+ async function postJson(url, { bearerToken, body }) {
50
+ const response = await fetch(url, {
51
+ method: 'POST',
52
+ headers: {
53
+ authorization: `Bearer ${bearerToken}`,
54
+ 'content-type': 'application/json',
55
+ },
56
+ body: JSON.stringify(body || {}),
57
+ signal: AbortSignal.timeout(10_000),
58
+ });
59
+ const payload = await parseJsonResponse(response);
60
+ if (!response.ok) {
61
+ throw createImageHostStorageError({
62
+ code: 'agent_card_image_host_sign_failed',
63
+ message: 'agent card signing request failed',
64
+ url,
65
+ responseStatus: response.status,
66
+ responseBody: payload,
67
+ });
68
+ }
69
+ return payload;
70
+ }
71
+
72
+ async function uploadPng(url, { bearerToken, pngBuffer, filename }) {
73
+ const form = new FormData();
74
+ form.append('file', new Blob([pngBuffer], { type: 'image/png' }), filename);
75
+ const response = await fetch(url, {
76
+ method: 'POST',
77
+ headers: {
78
+ authorization: `Bearer ${bearerToken}`,
79
+ },
80
+ body: form,
81
+ signal: AbortSignal.timeout(10_000),
82
+ });
83
+ const payload = await parseJsonResponse(response);
84
+ if (!response.ok) {
85
+ throw createImageHostStorageError({
86
+ code: 'agent_card_image_host_upload_failed',
87
+ message: 'agent card upload request failed',
88
+ url,
89
+ responseStatus: response.status,
90
+ responseBody: payload,
91
+ });
92
+ }
93
+ return payload;
94
+ }
95
+
96
+ export function createImageHostAgentCardStorage({
97
+ baseUrl = null,
98
+ uploadUrl = null,
99
+ signUrl = null,
100
+ bearerToken = null,
101
+ } = {}) {
102
+ const normalizedBaseUrl = normalizeText(baseUrl, null);
103
+ const normalizedUploadUrl = normalizeText(
104
+ uploadUrl,
105
+ normalizedBaseUrl ? `${normalizedBaseUrl.replace(/\/+$/, '')}/api/upload` : null,
106
+ );
107
+ const normalizedSignUrl = normalizeText(
108
+ signUrl,
109
+ normalizedBaseUrl ? `${normalizedBaseUrl.replace(/\/+$/, '')}/api/sign` : null,
110
+ );
111
+ const normalizedBearerToken = normalizeText(bearerToken, null);
112
+
113
+ if (!normalizedUploadUrl) {
114
+ throw new Error('agent_card_image_host_upload_url_required');
115
+ }
116
+ if (!normalizedSignUrl) {
117
+ throw new Error('agent_card_image_host_sign_url_required');
118
+ }
119
+ if (!normalizedBearerToken) {
120
+ throw new Error('agent_card_image_host_bearer_token_required');
121
+ }
122
+
123
+ return {
124
+ storageType: 'image_host',
125
+ describeTarget({ contentHash, templateVersion = 'v1', expiresAt = null, expiresInSeconds = null } = {}) {
126
+ const normalizedHash = normalizeText(contentHash, null);
127
+ if (!normalizedHash) throw new Error('agent_card_content_hash_required');
128
+ return {
129
+ contentHash: normalizedHash,
130
+ templateVersion: normalizeText(templateVersion, 'v1'),
131
+ expiresAt: normalizeText(expiresAt, null),
132
+ expiresInSeconds: normalizePositiveInteger(expiresInSeconds, null),
133
+ sourceFilename: `${normalizedHash}.png`,
134
+ };
135
+ },
136
+ async exists() {
137
+ return false;
138
+ },
139
+ async writePng(target, pngBuffer, { expiresInSeconds = null } = {}) {
140
+ const uploadPayload = await uploadPng(normalizedUploadUrl, {
141
+ bearerToken: normalizedBearerToken,
142
+ pngBuffer,
143
+ filename: target.sourceFilename,
144
+ });
145
+ const uploadedFilename = normalizeText(uploadPayload?.filename, null);
146
+ if (!uploadedFilename) {
147
+ throw createImageHostStorageError({
148
+ code: 'agent_card_image_host_invalid_upload_response',
149
+ message: 'agent card upload response did not include a filename',
150
+ url: normalizedUploadUrl,
151
+ responseBody: uploadPayload,
152
+ });
153
+ }
154
+ const ttlSeconds = normalizePositiveInteger(
155
+ expiresInSeconds,
156
+ normalizePositiveInteger(target.expiresInSeconds, null),
157
+ );
158
+ const signPayload = await postJson(normalizedSignUrl, {
159
+ bearerToken: normalizedBearerToken,
160
+ body: {
161
+ filename: uploadedFilename,
162
+ ttlSeconds,
163
+ },
164
+ });
165
+ const signedUrl = normalizeText(signPayload?.signedUrl, normalizeText(uploadPayload?.signedUrl, null));
166
+ if (!signedUrl) {
167
+ throw createImageHostStorageError({
168
+ code: 'agent_card_image_host_invalid_sign_response',
169
+ message: 'agent card sign response did not include a signedUrl',
170
+ url: normalizedSignUrl,
171
+ responseBody: signPayload,
172
+ });
173
+ }
174
+ const publicUrl = normalizeText(
175
+ uploadPayload?.publicUrl,
176
+ normalizedBaseUrl ? `${normalizedBaseUrl.replace(/\/+$/, '')}/i/${uploadedFilename}` : null,
177
+ );
178
+ return {
179
+ ...target,
180
+ filename: uploadedFilename,
181
+ publicUrl,
182
+ signedUrl,
183
+ imageUrl: signedUrl,
184
+ downloadUrl: signedUrl,
185
+ expiresAt: toIsoFromEpochSeconds(signPayload?.expiresAt, target.expiresAt),
186
+ expiresInSeconds: normalizePositiveInteger(signPayload?.ttlSeconds, ttlSeconds),
187
+ contentType: normalizeText(uploadPayload?.contentType, 'image/png'),
188
+ size: normalizePositiveInteger(uploadPayload?.size, pngBuffer.length),
189
+ };
190
+ },
191
+ };
192
+ }
@@ -0,0 +1,74 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+
4
+ function normalizeText(value, fallback = null) {
5
+ if (value == null) return fallback;
6
+ const normalized = String(value).trim();
7
+ return normalized || fallback;
8
+ }
9
+
10
+ function normalizePublicPath(filePath = '') {
11
+ return String(filePath).split(path.sep).join('/');
12
+ }
13
+
14
+ function parseExpiryEpochSeconds(value) {
15
+ const parsed = Number.parseInt(String(value || '').trim(), 10);
16
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
17
+ }
18
+
19
+ export function extractAgentCardExpiryEpochSeconds(relativeOrPublicPath = '') {
20
+ const normalizedPath = normalizePublicPath(String(relativeOrPublicPath || '')).replace(/^\/+/, '');
21
+ const parts = normalizedPath.split('/').filter(Boolean);
22
+ if (parts[0] !== 'ephemeral') return null;
23
+ return parseExpiryEpochSeconds(parts[1]);
24
+ }
25
+
26
+ export function createLocalPublicAgentCardStorage({
27
+ publicDir,
28
+ publicBasePath = '/public/agent-cards',
29
+ } = {}) {
30
+ const normalizedPublicDir = normalizeText(publicDir, null);
31
+ if (!normalizedPublicDir) {
32
+ throw new Error('agent_card_public_dir_required');
33
+ }
34
+
35
+ return {
36
+ describeTarget({ contentHash, templateVersion = 'v1', expiresAt = null } = {}) {
37
+ const normalizedHash = normalizeText(contentHash, null);
38
+ if (!normalizedHash) throw new Error('agent_card_content_hash_required');
39
+ const expiresAtEpochSeconds = expiresAt ? parseExpiryEpochSeconds(Math.floor(Date.parse(expiresAt) / 1000)) : null;
40
+ const relativePath = expiresAtEpochSeconds
41
+ ? path.join(
42
+ 'ephemeral',
43
+ String(expiresAtEpochSeconds),
44
+ normalizeText(templateVersion, 'v1'),
45
+ normalizedHash.slice(0, 2),
46
+ normalizedHash.slice(2, 4),
47
+ `${normalizedHash}.png`,
48
+ )
49
+ : path.join(
50
+ normalizeText(templateVersion, 'v1'),
51
+ normalizedHash.slice(0, 2),
52
+ normalizedHash.slice(2, 4),
53
+ `${normalizedHash}.png`,
54
+ );
55
+ return {
56
+ contentHash: normalizedHash,
57
+ expiresAt: expiresAtEpochSeconds ? new Date(expiresAtEpochSeconds * 1000).toISOString() : null,
58
+ expiresAtEpochSeconds,
59
+ isEphemeral: Boolean(expiresAtEpochSeconds),
60
+ relativePath,
61
+ absolutePath: path.join(normalizedPublicDir, relativePath),
62
+ publicPath: `${normalizeText(publicBasePath, '/public/agent-cards')}/${normalizePublicPath(relativePath)}`,
63
+ };
64
+ },
65
+ async exists(target) {
66
+ return await fs.pathExists(target.absolutePath);
67
+ },
68
+ async writePng(target, pngBuffer) {
69
+ await fs.ensureDir(path.dirname(target.absolutePath));
70
+ await fs.writeFile(target.absolutePath, pngBuffer);
71
+ return target;
72
+ },
73
+ };
74
+ }
@@ -0,0 +1,325 @@
1
+ import { Resvg } from '@resvg/resvg-js';
2
+ import QRCode from 'qrcode';
3
+
4
+ function normalizeText(value, fallback = null) {
5
+ if (value == null) return fallback;
6
+ const normalized = String(value).trim();
7
+ return normalized || fallback;
8
+ }
9
+
10
+ function escapeXml(value) {
11
+ return String(value || '')
12
+ .replaceAll('&', '&amp;')
13
+ .replaceAll('<', '&lt;')
14
+ .replaceAll('>', '&gt;')
15
+ .replaceAll('"', '&quot;')
16
+ .replaceAll("'", '&apos;');
17
+ }
18
+
19
+ function truncateText(value, maxLength, fallback = '') {
20
+ const normalized = normalizeText(value, fallback) || fallback;
21
+ if (normalized.length <= maxLength) return normalized;
22
+ return `${normalized.slice(0, Math.max(1, maxLength - 1)).trimEnd()}…`;
23
+ }
24
+
25
+ function buildCtaMarkup(lines = []) {
26
+ return lines
27
+ .map((line, index) => {
28
+ const dy = index === 0 ? '0' : '48';
29
+ return `<tspan x="96" dy="${dy}">${escapeXml(truncateText(line, 84, ''))}</tspan>`;
30
+ })
31
+ .join('');
32
+ }
33
+
34
+ function buildSubtitleMarkup(text) {
35
+ const normalized = truncateText(text, 180, 'Agent-to-agent worlds on OpenClaw.');
36
+ const words = normalized.split(/\s+/g).filter(Boolean);
37
+ const lines = [];
38
+ let currentLine = '';
39
+
40
+ for (const word of words) {
41
+ const nextLine = currentLine ? `${currentLine} ${word}` : word;
42
+ if (nextLine.length > 42 && currentLine) {
43
+ lines.push(currentLine);
44
+ currentLine = word;
45
+ continue;
46
+ }
47
+ currentLine = nextLine;
48
+ }
49
+
50
+ if (currentLine) lines.push(currentLine);
51
+
52
+ return lines.slice(0, 3).map((line, index) => {
53
+ const dy = index === 0 ? '0' : '46';
54
+ return `<tspan x="96" dy="${dy}">${escapeXml(line)}</tspan>`;
55
+ }).join('');
56
+ }
57
+
58
+ async function buildQrDataUrl(targetUrl) {
59
+ return await QRCode.toDataURL(targetUrl, {
60
+ errorCorrectionLevel: 'M',
61
+ margin: 1,
62
+ width: 220,
63
+ color: {
64
+ dark: '#10203a',
65
+ light: '#0000',
66
+ },
67
+ });
68
+ }
69
+
70
+ function applyTemplate(source, replacements) {
71
+ return Object.entries(replacements).reduce(
72
+ (result, [token, value]) => result.replaceAll(`{{${token}}}`, value),
73
+ source,
74
+ );
75
+ }
76
+
77
+ function splitGraphemes(value) {
78
+ const normalized = String(value || '');
79
+ if (!normalized) return [];
80
+ if (typeof Intl?.Segmenter === 'function') {
81
+ const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
82
+ return Array.from(segmenter.segment(normalized), ({ segment }) => segment);
83
+ }
84
+ return Array.from(normalized);
85
+ }
86
+
87
+ function isFullWidthCodePoint(codePoint) {
88
+ return (
89
+ (codePoint >= 0x1100 && codePoint <= 0x115f) ||
90
+ (codePoint >= 0x2329 && codePoint <= 0x232a) ||
91
+ (codePoint >= 0x2e80 && codePoint <= 0xa4cf) ||
92
+ (codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
93
+ (codePoint >= 0xf900 && codePoint <= 0xfaff) ||
94
+ (codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
95
+ (codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
96
+ (codePoint >= 0xff01 && codePoint <= 0xff60) ||
97
+ (codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
98
+ (codePoint >= 0x1f300 && codePoint <= 0x1fbff)
99
+ );
100
+ }
101
+
102
+ function estimateGlyphWidthUnits(grapheme) {
103
+ if (!grapheme) return 0;
104
+ if (/^\s$/u.test(grapheme)) return 0.32;
105
+ if (/^[.,:;!|`'"’“”‘]$/u.test(grapheme)) return 0.28;
106
+ if (/^[()\[\]{}]$/u.test(grapheme)) return 0.36;
107
+ if (/^[-_=+\\/<>]$/u.test(grapheme)) return 0.48;
108
+ if (/^[#@%&*]$/u.test(grapheme)) return 0.72;
109
+ if (/^[0-9]$/u.test(grapheme)) return 0.56;
110
+ if (/^[A-Z]$/u.test(grapheme)) return /[MW]/u.test(grapheme) ? 0.9 : 0.68;
111
+ if (/^[a-z]$/u.test(grapheme)) return /[mw]/u.test(grapheme) ? 0.78 : 0.56;
112
+
113
+ const codePoint = grapheme.codePointAt(0);
114
+ if (codePoint == null) return 0.6;
115
+ if (isFullWidthCodePoint(codePoint)) return 1;
116
+ return 0.62;
117
+ }
118
+
119
+ export function estimateTextWidth(text, fontSize, { letterSpacing = 0 } = {}) {
120
+ if (!text || !fontSize) return 0;
121
+ const graphemes = splitGraphemes(text);
122
+ const glyphUnits = graphemes.reduce((total, grapheme) => total + estimateGlyphWidthUnits(grapheme), 0);
123
+ const spacingWidth = Math.max(0, graphemes.length - 1) * letterSpacing;
124
+ return (glyphUnits * fontSize) + spacingWidth;
125
+ }
126
+
127
+ function parseNumericAttribute(value, fallback = null) {
128
+ if (value == null) return fallback;
129
+ const parsed = Number.parseFloat(String(value));
130
+ return Number.isFinite(parsed) ? parsed : fallback;
131
+ }
132
+
133
+ function parseLetterSpacing(value, fontSize) {
134
+ if (!value) return 0;
135
+ const normalized = String(value).trim();
136
+ if (!normalized || normalized === 'normal') return 0;
137
+ if (normalized.endsWith('em')) return parseNumericAttribute(normalized.slice(0, -2), 0) * fontSize;
138
+ if (normalized.endsWith('px')) return parseNumericAttribute(normalized.slice(0, -2), 0);
139
+ return parseNumericAttribute(normalized, 0);
140
+ }
141
+
142
+ function formatFontSize(value) {
143
+ const rounded = Math.round(value * 10) / 10;
144
+ return Number.isInteger(rounded) ? String(rounded) : String(rounded);
145
+ }
146
+
147
+ function shrinkTextToFitSingleLine(value, {
148
+ maxWidth,
149
+ maxFontSize,
150
+ minFontSize,
151
+ letterSpacing = 0,
152
+ } = {}) {
153
+ const text = String(value || '');
154
+ if (!text) {
155
+ return {
156
+ text,
157
+ fontSize: formatFontSize(maxFontSize || minFontSize || 16),
158
+ };
159
+ }
160
+
161
+ const widthAtMax = estimateTextWidth(text, maxFontSize, { letterSpacing });
162
+ if (!maxWidth || !maxFontSize || widthAtMax <= maxWidth) {
163
+ return {
164
+ text,
165
+ fontSize: formatFontSize(maxFontSize || minFontSize || 16),
166
+ };
167
+ }
168
+
169
+ const scaledFontSize = Math.max(
170
+ minFontSize || maxFontSize,
171
+ Math.min(maxFontSize, (maxFontSize * maxWidth) / Math.max(widthAtMax, 1)),
172
+ );
173
+
174
+ if (scaledFontSize > (minFontSize || maxFontSize)) {
175
+ return {
176
+ text,
177
+ fontSize: formatFontSize(scaledFontSize),
178
+ };
179
+ }
180
+
181
+ if (estimateTextWidth(text, minFontSize, { letterSpacing }) <= maxWidth) {
182
+ return {
183
+ text,
184
+ fontSize: formatFontSize(minFontSize),
185
+ };
186
+ }
187
+
188
+ const graphemes = splitGraphemes(text);
189
+ let keptLength = graphemes.length;
190
+ while (keptLength > 1) {
191
+ const candidate = `${graphemes.slice(0, keptLength).join('').trimEnd()}…`;
192
+ if (estimateTextWidth(candidate, minFontSize, { letterSpacing }) <= maxWidth) {
193
+ return {
194
+ text: candidate,
195
+ fontSize: formatFontSize(minFontSize),
196
+ };
197
+ }
198
+ keptLength -= 1;
199
+ }
200
+
201
+ return {
202
+ text: '…',
203
+ fontSize: formatFontSize(minFontSize || maxFontSize || 16),
204
+ };
205
+ }
206
+
207
+ function parseTextTagAttributes(attributeSource) {
208
+ const attributes = {};
209
+ const pattern = /([:@\w.-]+)\s*=\s*(['"])(.*?)\2/gs;
210
+ for (const match of attributeSource.matchAll(pattern)) {
211
+ attributes[match[1]] = match[3];
212
+ }
213
+ return attributes;
214
+ }
215
+
216
+ function escapeRegExp(value) {
217
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
218
+ }
219
+
220
+ function setSvgAttribute(attributeSource, name, value) {
221
+ const replacement = `${name}="${value}"`;
222
+ const escapedName = escapeRegExp(name);
223
+ const pattern = new RegExp(`(^|\\s)(${escapedName}\\s*=\\s*)(['"]).*?\\3`, 's');
224
+ if (pattern.test(attributeSource)) {
225
+ return attributeSource.replace(pattern, (match, prefix) => `${prefix}${replacement}`);
226
+ }
227
+ return `${attributeSource} ${replacement}`;
228
+ }
229
+
230
+ function resolveSlotTextValue({ field, token, spec }) {
231
+ const fieldValue = field ? spec?.[field] : undefined;
232
+ if (fieldValue != null && fieldValue !== '') return String(fieldValue);
233
+
234
+ switch (token) {
235
+ case 'DISPLAY_NAME':
236
+ return spec?.displayName ?? 'Claworld Agent';
237
+ case 'PUBLIC_HANDLE':
238
+ return spec?.publicHandle ?? 'agent#0000';
239
+ case 'TITLE':
240
+ return spec?.title ?? 'Claworld Agent Card';
241
+ case 'BADGE_TEXT':
242
+ return spec?.badgeText ?? 'Preview';
243
+ case 'FOOTER_LABEL':
244
+ return spec?.footerLabel ?? 'claworld.love';
245
+ case 'AVATAR_INITIALS':
246
+ return spec?.avatarInitials ?? 'C';
247
+ default:
248
+ return null;
249
+ }
250
+ }
251
+
252
+ export function applyAdaptiveTextSlots(source, { spec } = {}) {
253
+ if (!source || !spec) return source;
254
+
255
+ return source.replace(/<text\b(?<attributes>[^>]*)>(?<content>[\s\S]*?)<\/text>/g, (fullMatch, rawAttributes, rawContent) => {
256
+ const attributes = parseTextTagAttributes(rawAttributes);
257
+ const token = attributes['data-token'];
258
+ const field = attributes['data-field'];
259
+ if (!token && !field) return fullMatch;
260
+
261
+ const resolvedValue = resolveSlotTextValue({ field, token, spec });
262
+ if (resolvedValue == null) return fullMatch;
263
+
264
+ const trimWhitespace = attributes['data-trim-whitespace'] === 'true';
265
+ const normalizedValue = trimWhitespace ? normalizeText(resolvedValue, '') || '' : String(resolvedValue);
266
+ const layout = attributes['data-layout'];
267
+ const overflow = attributes['data-overflow'];
268
+
269
+ let nextText = normalizedValue;
270
+ let nextAttributes = rawAttributes;
271
+
272
+ if (layout === 'single-line' && overflow === 'shrink-font-only') {
273
+ const maxWidth = parseNumericAttribute(attributes['data-max-width']);
274
+ const maxFontSize = parseNumericAttribute(attributes['data-max-font-size'], parseNumericAttribute(attributes['font-size'], 16));
275
+ const minFontSize = parseNumericAttribute(attributes['data-min-font-size'], maxFontSize);
276
+ const letterSpacing = parseLetterSpacing(attributes['letter-spacing'], maxFontSize);
277
+ const fitted = shrinkTextToFitSingleLine(normalizedValue, {
278
+ maxWidth,
279
+ maxFontSize,
280
+ minFontSize,
281
+ letterSpacing,
282
+ });
283
+ nextText = fitted.text;
284
+ nextAttributes = setSvgAttribute(nextAttributes, 'font-size', fitted.fontSize);
285
+ }
286
+
287
+ return `<text${nextAttributes}>${escapeXml(nextText)}</text>`;
288
+ });
289
+ }
290
+
291
+ export function createAgentCardSvgRenderer() {
292
+ return {
293
+ async renderCard({ template, spec } = {}) {
294
+ if (!template?.source) throw new Error('agent_card_template_source_missing');
295
+ if (!spec || typeof spec !== 'object') throw new Error('agent_card_spec_missing');
296
+
297
+ const qrDataUrl = await buildQrDataUrl(spec.qrTargetUrl);
298
+ const svg = applyTemplate(applyAdaptiveTextSlots(template.source, { spec }), {
299
+ DISPLAY_NAME: escapeXml(truncateText(spec.displayName, 40, 'Claworld Agent')),
300
+ PUBLIC_HANDLE: escapeXml(truncateText(spec.publicHandle, 48, 'agent#0000')),
301
+ TITLE: escapeXml(truncateText(spec.title, 56, 'Claworld Agent Card')),
302
+ SUBTITLE_LINES: buildSubtitleMarkup(spec.subtitle),
303
+ CTA_LINES: buildCtaMarkup(spec.ctaLines),
304
+ FOOTER_LABEL: escapeXml(truncateText(spec.footerLabel, 80, 'claworld.love')),
305
+ BADGE_TEXT: escapeXml(truncateText(spec.badgeText, 24, 'Preview')),
306
+ AVATAR_INITIALS: escapeXml(truncateText(spec.avatarInitials, 2, 'C')),
307
+ QR_DATA_URL: escapeXml(qrDataUrl),
308
+ });
309
+
310
+ const resvg = new Resvg(svg, {
311
+ fitTo: {
312
+ mode: 'width',
313
+ value: template.width || 1200,
314
+ },
315
+ font: {
316
+ loadSystemFonts: true,
317
+ defaultFontFamily: 'Arial',
318
+ },
319
+ });
320
+
321
+ const image = resvg.render();
322
+ return image.asPng();
323
+ },
324
+ };
325
+ }