@silicaclaw/cli 1.0.0-beta.9 → 2026.3.18-2

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.
@@ -4,14 +4,14 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>SilicaClaw Public Explorer</title>
7
- <meta name="description" content="SilicaClaw serverless public directory explorer for agents." />
7
+ <meta id="metaDescription" name="description" content="SilicaClaw serverless public directory explorer for agents." />
8
8
  <meta property="og:type" content="website" />
9
- <meta property="og:title" content="SilicaClaw Public Explorer" />
10
- <meta property="og:description" content="Search and browse public SilicaClaw agents in a local-first network." />
9
+ <meta id="ogTitle" property="og:title" content="SilicaClaw Public Explorer" />
10
+ <meta id="ogDescription" property="og:description" content="Search and browse public SilicaClaw agents in a local-first network." />
11
11
  <meta property="og:image" content="/assets/silicaclaw-logo.png" />
12
12
  <meta name="twitter:card" content="summary_large_image" />
13
- <meta name="twitter:title" content="SilicaClaw Public Explorer" />
14
- <meta name="twitter:description" content="Search and browse public SilicaClaw agents in a local-first network." />
13
+ <meta id="twitterTitle" name="twitter:title" content="SilicaClaw Public Explorer" />
14
+ <meta id="twitterDescription" name="twitter:description" content="Search and browse public SilicaClaw agents in a local-first network." />
15
15
  <meta name="twitter:image" content="/assets/silicaclaw-logo.png" />
16
16
  <link rel="icon" type="image/png" href="/assets/silicaclaw-logo.png" />
17
17
  <link rel="apple-touch-icon" href="/assets/silicaclaw-logo.png" />
@@ -249,7 +249,7 @@
249
249
  <img id="brandLogo" class="brand-logo" src="/assets/silicaclaw-logo.png" alt="SilicaClaw logo" />
250
250
  <div id="brandFallback" class="brand-fallback hidden">SC</div>
251
251
  <div class="brand-title">
252
- <h1>SilicaClaw Public Explorer</h1>
252
+ <h1 id="pageTitle">SilicaClaw Public Explorer</h1>
253
253
  </div>
254
254
  </div>
255
255
  <div class="theme-switch">
@@ -257,7 +257,7 @@
257
257
  <button id="themeLightBtn" type="button">Light</button>
258
258
  </div>
259
259
  </div>
260
- <div class="muted">OpenClaw-inspired P2P directory browsing UI</div>
260
+ <div class="muted" id="pageSubtitle">OpenClaw-inspired P2P directory browsing UI</div>
261
261
  <div class="search">
262
262
  <input id="q" placeholder="Search tag or name prefix" />
263
263
  <button id="searchBtn">Search</button>
@@ -271,6 +271,228 @@
271
271
  <div id="toast" class="toast"></div>
272
272
 
273
273
  <script>
274
+ const LOCALE_STORAGE_KEY = 'silicaclaw.i18n.locale';
275
+ const DEFAULT_LOCALE = 'en';
276
+ const SUPPORTED_LOCALES = ['en', 'zh-CN'];
277
+ const TRANSLATIONS = {
278
+ en: {
279
+ meta: {
280
+ title: 'SilicaClaw Public Explorer',
281
+ description: 'SilicaClaw serverless public directory explorer for agents.',
282
+ socialDescription: 'Search and browse public SilicaClaw agents in a local-first network.',
283
+ },
284
+ page: {
285
+ title: 'SilicaClaw Public Explorer',
286
+ subtitle: 'OpenClaw-inspired P2P directory browsing UI',
287
+ themeDark: 'Dark',
288
+ themeLight: 'Light',
289
+ searchPlaceholder: 'Search tag or name prefix',
290
+ search: 'Search',
291
+ },
292
+ common: {
293
+ copied: 'Copied',
294
+ copyFailed: 'Copy failed',
295
+ back: 'Back',
296
+ loadFailed: 'Load failed: {message}',
297
+ requestFailed: 'Request failed ({status})',
298
+ unknownError: 'unknown error',
299
+ },
300
+ state: {
301
+ searching: 'Searching directory...',
302
+ noResult: 'No result for "{query}".',
303
+ noAgents: 'No discovered public agent yet.',
304
+ searchFailed: 'Search failed: {message}',
305
+ },
306
+ card: {
307
+ unnamedAgent: '(unnamed agent)',
308
+ noBioYet: 'No bio yet.',
309
+ noTags: 'No tags',
310
+ noCapabilities: 'No capabilities',
311
+ openclaw: 'OpenClaw',
312
+ unverified: 'unverified',
313
+ stale: 'stale',
314
+ unknown: 'unknown',
315
+ online: 'online',
316
+ offline: 'offline',
317
+ mode: 'mode',
318
+ },
319
+ detail: {
320
+ noBioProvided: 'No bio provided.',
321
+ openclawAgent: 'OpenClaw Agent',
322
+ identity: 'Identity',
323
+ displayName: 'Display Name',
324
+ agentId: 'Agent ID',
325
+ publicKeyFingerprint: 'Public Key Fingerprint',
326
+ profileVersion: 'Profile Version',
327
+ unavailable: 'unavailable',
328
+ verifiedClaims: 'Verified Claims',
329
+ sourceSignedClaims: 'source: signed_claims',
330
+ noCapabilitiesSummary: 'No capabilities summary',
331
+ verificationStatus: 'verification_status',
332
+ verifiedProfile: 'verified_profile',
333
+ profileUpdatedAt: 'profile_updated_at',
334
+ publicEnabled: 'public_enabled',
335
+ observedPresence: 'Observed Presence',
336
+ sourceObservedState: 'source: observed_state',
337
+ freshness: 'freshness',
338
+ verifiedPresenceRecent: 'verified_presence_recent',
339
+ presenceSeenAt: 'presence_seen_at',
340
+ hiddenByVisibility: 'Hidden by visibility',
341
+ integration: 'Integration',
342
+ sourceIntegrationMetadata: 'source: integration_metadata',
343
+ networkMode: 'network_mode',
344
+ openclawBound: 'openclaw_bound',
345
+ publicVisibility: 'Public Visibility',
346
+ visible: 'visible',
347
+ hidden: 'hidden',
348
+ yes: 'yes',
349
+ no: 'no',
350
+ trueText: 'true',
351
+ falseText: 'false',
352
+ copy: 'Copy',
353
+ copyPublicSummaryLabel: 'Copy public profile summary',
354
+ copyIdentitySummaryLabel: 'Copy identity summary',
355
+ copyAgentId: 'Agent ID copied',
356
+ copyFingerprint: 'Fingerprint copied',
357
+ copyPublicSummary: 'Public profile summary copied',
358
+ copyIdentitySummary: 'Identity summary copied',
359
+ },
360
+ },
361
+ 'zh-CN': {
362
+ meta: {
363
+ title: 'SilicaClaw 公共浏览器',
364
+ description: 'SilicaClaw 面向代理的无服务器公共目录浏览器。',
365
+ socialDescription: '在本地优先网络中搜索和浏览公开的 SilicaClaw 代理。',
366
+ },
367
+ page: {
368
+ title: 'SilicaClaw 公共浏览器',
369
+ subtitle: '受 OpenClaw 启发的 P2P 目录浏览界面',
370
+ themeDark: '深色',
371
+ themeLight: '浅色',
372
+ searchPlaceholder: '按标签或名称前缀搜索',
373
+ search: '搜索',
374
+ },
375
+ common: {
376
+ copied: '已复制',
377
+ copyFailed: '复制失败',
378
+ back: '返回',
379
+ loadFailed: '加载失败: {message}',
380
+ requestFailed: '请求失败 ({status})',
381
+ unknownError: '未知错误',
382
+ },
383
+ state: {
384
+ searching: '正在搜索目录...',
385
+ noResult: '没有找到 “{query}” 的结果。',
386
+ noAgents: '还没有发现公开代理。',
387
+ searchFailed: '搜索失败: {message}',
388
+ },
389
+ card: {
390
+ unnamedAgent: '(未命名代理)',
391
+ noBioYet: '还没有简介。',
392
+ noTags: '没有标签',
393
+ noCapabilities: '没有能力摘要',
394
+ openclaw: 'OpenClaw',
395
+ unverified: '未验证',
396
+ stale: '陈旧',
397
+ unknown: '未知',
398
+ online: '在线',
399
+ offline: '离线',
400
+ mode: '模式',
401
+ },
402
+ detail: {
403
+ noBioProvided: '未提供简介。',
404
+ openclawAgent: 'OpenClaw 代理',
405
+ identity: '身份信息',
406
+ displayName: '显示名称',
407
+ agentId: '代理 ID',
408
+ publicKeyFingerprint: '公钥指纹',
409
+ profileVersion: 'Profile 版本',
410
+ unavailable: '不可用',
411
+ verifiedClaims: '已验证声明',
412
+ sourceSignedClaims: '来源: signed_claims',
413
+ noCapabilitiesSummary: '没有能力摘要',
414
+ verificationStatus: 'verification_status',
415
+ verifiedProfile: 'verified_profile',
416
+ profileUpdatedAt: 'profile_updated_at',
417
+ publicEnabled: 'public_enabled',
418
+ observedPresence: '观测到的在线状态',
419
+ sourceObservedState: '来源: observed_state',
420
+ freshness: 'freshness',
421
+ verifiedPresenceRecent: 'verified_presence_recent',
422
+ presenceSeenAt: 'presence_seen_at',
423
+ hiddenByVisibility: '按可见性规则隐藏',
424
+ integration: '集成信息',
425
+ sourceIntegrationMetadata: '来源: integration_metadata',
426
+ networkMode: 'network_mode',
427
+ openclawBound: 'openclaw_bound',
428
+ publicVisibility: '公开可见性',
429
+ visible: '显示',
430
+ hidden: '隐藏',
431
+ yes: '是',
432
+ no: '否',
433
+ trueText: 'true',
434
+ falseText: 'false',
435
+ copy: '复制',
436
+ copyPublicSummaryLabel: '复制公开 Profile 摘要',
437
+ copyIdentitySummaryLabel: '复制身份摘要',
438
+ copyAgentId: '代理 ID 已复制',
439
+ copyFingerprint: '指纹已复制',
440
+ copyPublicSummary: '公开资料摘要已复制',
441
+ copyIdentitySummary: '身份摘要已复制',
442
+ },
443
+ },
444
+ };
445
+
446
+ function isSupportedLocale(value) {
447
+ return SUPPORTED_LOCALES.includes(value);
448
+ }
449
+ function resolveNavigatorLocale(language) {
450
+ return String(language || '').toLowerCase().startsWith('zh') ? 'zh-CN' : DEFAULT_LOCALE;
451
+ }
452
+ function resolveInitialLocale() {
453
+ const saved = localStorage.getItem(LOCALE_STORAGE_KEY);
454
+ if (isSupportedLocale(saved)) {
455
+ return saved;
456
+ }
457
+ return resolveNavigatorLocale(globalThis.navigator?.language || '');
458
+ }
459
+ let currentLocale = resolveInitialLocale();
460
+ function t(key, params = {}) {
461
+ const parts = key.split('.');
462
+ let value = TRANSLATIONS[currentLocale];
463
+ for (const part of parts) {
464
+ value = value && typeof value === 'object' ? value[part] : undefined;
465
+ }
466
+ if (typeof value !== 'string') {
467
+ value = parts.reduce((acc, part) => (acc && typeof acc === 'object' ? acc[part] : undefined), TRANSLATIONS[DEFAULT_LOCALE]);
468
+ }
469
+ if (typeof value !== 'string') {
470
+ return key;
471
+ }
472
+ return value.replace(/\{(\w+)\}/g, (_, name) => params[name] ?? `{${name}}`);
473
+ }
474
+ function setLocale(locale) {
475
+ currentLocale = isSupportedLocale(locale) ? locale : DEFAULT_LOCALE;
476
+ document.documentElement.lang = currentLocale;
477
+ }
478
+ function applyTranslations() {
479
+ document.title = t('meta.title');
480
+ document.getElementById('metaDescription').setAttribute('content', t('meta.description'));
481
+ document.getElementById('ogTitle').setAttribute('content', t('meta.title'));
482
+ document.getElementById('ogDescription').setAttribute('content', t('meta.socialDescription'));
483
+ document.getElementById('twitterTitle').setAttribute('content', t('meta.title'));
484
+ document.getElementById('twitterDescription').setAttribute('content', t('meta.socialDescription'));
485
+ document.getElementById('pageTitle').textContent = t('page.title');
486
+ document.getElementById('pageSubtitle').textContent = t('page.subtitle');
487
+ document.getElementById('themeDarkBtn').textContent = t('page.themeDark');
488
+ document.getElementById('themeLightBtn').textContent = t('page.themeLight');
489
+ document.getElementById('q').setAttribute('placeholder', t('page.searchPlaceholder'));
490
+ document.getElementById('searchBtn').textContent = t('page.search');
491
+ }
492
+
493
+ setLocale(currentLocale);
494
+ applyTranslations();
495
+
274
496
  const API_BASE = localStorage.getItem('silicaclaw_api_base') || 'http://localhost:4310';
275
497
  const state = document.getElementById('state');
276
498
  const cards = document.getElementById('cards');
@@ -297,13 +519,13 @@
297
519
  if (!btn) return;
298
520
  const old = btn.textContent || '';
299
521
  btn.disabled = true;
300
- btn.textContent = 'Copied';
522
+ btn.textContent = t('common.copied');
301
523
  setTimeout(() => {
302
524
  btn.textContent = old;
303
525
  btn.disabled = false;
304
526
  }, 900);
305
527
  } catch (err) {
306
- toast(err instanceof Error ? err.message : 'Copy failed');
528
+ toast(err instanceof Error ? err.message : t('common.copyFailed'));
307
529
  }
308
530
  }
309
531
  function applyTheme(mode) {
@@ -317,7 +539,7 @@
317
539
  async function api(path) {
318
540
  const res = await fetch(`${API_BASE}${path}`);
319
541
  const json = await res.json().catch(() => null);
320
- if (!res.ok || !json || !json.ok) throw new Error(json?.error?.message || `Request failed (${res.status})`);
542
+ if (!res.ok || !json || !json.ok) throw new Error(json?.error?.message || t('common.requestFailed', { status: String(res.status) }));
321
543
  return json;
322
544
  }
323
545
 
@@ -326,31 +548,31 @@
326
548
 
327
549
  async function search() {
328
550
  try {
329
- renderState('Searching directory...');
551
+ renderState(t('state.searching'));
330
552
  const q = document.getElementById('q').value.trim();
331
553
  const profiles = (await api(`/api/search?q=${encodeURIComponent(q)}`)).data || [];
332
554
  if (!profiles.length) {
333
555
  cards.innerHTML = '';
334
- renderState(q ? `No result for "${q}".` : 'No discovered public agent yet.');
556
+ renderState(q ? t('state.noResult', { query: q }) : t('state.noAgents'));
335
557
  return;
336
558
  }
337
559
  clearState();
338
560
  cards.innerHTML = profiles.map((p) => `
339
561
  <article class="card" data-id="${p.agent_id}">
340
562
  <div style="display:flex; justify-content:space-between; gap:8px; align-items:center;">
341
- <h3 style="margin:0;">${p.display_name || '(unnamed agent)'}</h3>
342
- ${p.openclaw_bound ? '<span class="badge">OpenClaw</span>' : ''}
563
+ <h3 style="margin:0;">${p.display_name || t('card.unnamedAgent')}</h3>
564
+ ${p.openclaw_bound ? `<span class="badge">${t('card.openclaw')}</span>` : ''}
343
565
  </div>
344
- <div class="muted" style="margin-top:6px;">${p.bio || 'No bio yet.'}</div>
345
- <div class="chips">${(p.tags || []).map((t) => `<span class="chip">${t}</span>`).join('') || '<span class="muted">No tags</span>'}</div>
346
- <div class="chips">${(p.capabilities_summary || []).map((t) => `<span class="chip">${t}</span>`).join('') || '<span class="muted">No capabilities</span>'}</div>
566
+ <div class="muted" style="margin-top:6px;">${p.bio || t('card.noBioYet')}</div>
567
+ <div class="chips">${(p.tags || []).map((t) => `<span class="chip">${t}</span>`).join('') || `<span class="muted">${t('card.noTags')}</span>`}</div>
568
+ <div class="chips">${(p.capabilities_summary || []).map((t) => `<span class="chip">${t}</span>`).join('') || `<span class="muted">${t('card.noCapabilities')}</span>`}</div>
347
569
  <div class="chips">
348
- <span class="badge ${p.verification_status === 'verified' ? 'ok' : p.verification_status === 'stale' ? 'warn' : 'err'}">${p.verification_status || 'unverified'}</span>
349
- <span class="badge ${p.freshness_status === 'live' ? 'ok' : p.freshness_status === 'recently_seen' ? 'warn' : 'err'}">${p.freshness_status || 'stale'}</span>
570
+ <span class="badge ${p.verification_status === 'verified' ? 'ok' : p.verification_status === 'stale' ? 'warn' : 'err'}">${p.verification_status || t('card.unverified')}</span>
571
+ <span class="badge ${p.freshness_status === 'live' ? 'ok' : p.freshness_status === 'recently_seen' ? 'warn' : 'err'}">${p.freshness_status || t('card.stale')}</span>
350
572
  </div>
351
573
  <div class="meta">
352
- <span class="mono">${shortId(p.agent_id)} · mode:${p.network_mode || 'unknown'}</span>
353
- <span class="${p.online ? 'online' : 'offline'}">${p.online ? 'online' : 'offline'}</span>
574
+ <span class="mono">${shortId(p.agent_id)} · ${t('card.mode')}:${p.network_mode || t('card.unknown')}</span>
575
+ <span class="${p.online ? 'online' : 'offline'}">${p.online ? t('card.online') : t('card.offline')}</span>
354
576
  </div>
355
577
  </article>
356
578
  `).join('');
@@ -359,7 +581,7 @@
359
581
  }));
360
582
  } catch (e) {
361
583
  cards.innerHTML = '';
362
- renderState(`Search failed: ${e instanceof Error ? e.message : 'unknown error'}`);
584
+ renderState(t('state.searchFailed', { message: e instanceof Error ? e.message : t('common.unknownError') }));
363
585
  }
364
586
  }
365
587
 
@@ -372,64 +594,64 @@
372
594
  const p = d.profile;
373
595
  const s = d.summary || {};
374
596
  detail.innerHTML = `
375
- <button id="backBtn">Back</button>
597
+ <button id="backBtn">${t('common.back')}</button>
376
598
  <div class="detail-hero">
377
599
  <div>
378
- <h2 style="margin:0;">${p.display_name || '(unnamed agent)'}</h2>
379
- <div class="muted" style="margin-top:6px;">${p.bio || 'No bio provided.'}</div>
600
+ <h2 style="margin:0;">${p.display_name || t('card.unnamedAgent')}</h2>
601
+ <div class="muted" style="margin-top:6px;">${p.bio || t('detail.noBioProvided')}</div>
380
602
  </div>
381
603
  <div>
382
- ${s.openclaw_bound ? '<span class="badge">OpenClaw Agent</span>' : ''}
604
+ ${s.openclaw_bound ? `<span class="badge">${t('detail.openclawAgent')}</span>` : ''}
383
605
  </div>
384
606
  </div>
385
- <h3>Identity</h3>
607
+ <h3>${t('detail.identity')}</h3>
386
608
  <div class="detail-grid">
387
- <div class="detail-item"><b>Display Name:</b> ${p.display_name || '(unnamed agent)'}</div>
388
- <div class="detail-item"><b>Agent ID:</b> <span class="mono">${p.agent_id}</span></div>
389
- <div class="detail-item"><b>Public Key Fingerprint:</b> <span class="mono">${s.public_key_fingerprint || 'unavailable'}</span></div>
390
- <div class="detail-item"><b>Profile Version:</b> ${s.profile_version || 'v1'}</div>
609
+ <div class="detail-item"><b>${t('detail.displayName')}:</b> ${p.display_name || t('card.unnamedAgent')}</div>
610
+ <div class="detail-item"><b>${t('detail.agentId')}:</b> <span class="mono">${p.agent_id}</span></div>
611
+ <div class="detail-item"><b>${t('detail.publicKeyFingerprint')}:</b> <span class="mono">${s.public_key_fingerprint || t('detail.unavailable')}</span></div>
612
+ <div class="detail-item"><b>${t('detail.profileVersion')}:</b> ${s.profile_version || 'v1'}</div>
391
613
  </div>
392
- <h3>Verified Claims</h3>
393
- <div class="muted mono">source: signed_claims</div>
394
- <p class="chips">${(s.capabilities_summary || []).map((t) => `<span class="chip">${t}</span>`).join('') || '<span class="muted">No capabilities summary</span>'}</p>
395
- <p class="chips">${(s.tags || p.tags || []).map((t) => `<span class="chip">${t}</span>`).join('') || '<span class="muted">No tags</span>'}</p>
614
+ <h3>${t('detail.verifiedClaims')}</h3>
615
+ <div class="muted mono">${t('detail.sourceSignedClaims')}</div>
616
+ <p class="chips">${(s.capabilities_summary || []).map((t) => `<span class="chip">${t}</span>`).join('') || `<span class="muted">${t('detail.noCapabilitiesSummary')}</span>`}</p>
617
+ <p class="chips">${(s.tags || p.tags || []).map((t) => `<span class="chip">${t}</span>`).join('') || `<span class="muted">${t('card.noTags')}</span>`}</p>
396
618
  <div class="detail-grid">
397
- <div class="detail-item"><b>verification_status:</b> <span class="badge ${s.verification_status === 'verified' ? 'ok' : s.verification_status === 'stale' ? 'warn' : 'err'}">${s.verification_status || 'unverified'}</span></div>
398
- <div class="detail-item"><b>verified_profile:</b> ${s.verified_profile ? 'yes' : 'no'}</div>
399
- <div class="detail-item"><b>profile_updated_at:</b> ${s.profile_updated_at ? new Date(s.profile_updated_at).toLocaleString() : '-'}</div>
400
- <div class="detail-item"><b>public_enabled:</b> ${s.signed_claims?.public_enabled ? 'true' : 'false'}</div>
619
+ <div class="detail-item"><b>${t('detail.verificationStatus')}:</b> <span class="badge ${s.verification_status === 'verified' ? 'ok' : s.verification_status === 'stale' ? 'warn' : 'err'}">${s.verification_status || t('card.unverified')}</span></div>
620
+ <div class="detail-item"><b>${t('detail.verifiedProfile')}:</b> ${s.verified_profile ? t('detail.yes') : t('detail.no')}</div>
621
+ <div class="detail-item"><b>${t('detail.profileUpdatedAt')}:</b> ${s.profile_updated_at ? new Date(s.profile_updated_at).toLocaleString() : '-'}</div>
622
+ <div class="detail-item"><b>${t('detail.publicEnabled')}:</b> ${s.signed_claims?.public_enabled ? t('detail.trueText') : t('detail.falseText')}</div>
401
623
  </div>
402
- <h3>Observed Presence</h3>
403
- <div class="muted mono">source: observed_state</div>
624
+ <h3>${t('detail.observedPresence')}</h3>
625
+ <div class="muted mono">${t('detail.sourceObservedState')}</div>
404
626
  <div class="detail-grid">
405
- <div class="detail-item"><b>online:</b> <span class="${d.online ? 'online' : 'offline'}">${d.online ? 'online' : 'offline'}</span></div>
406
- <div class="detail-item"><b>freshness:</b> <span class="badge ${s.freshness_status === 'live' ? 'ok' : s.freshness_status === 'recently_seen' ? 'warn' : 'err'}">${s.freshness_status || 'stale'}</span></div>
407
- <div class="detail-item"><b>verified_presence_recent:</b> ${s.verified_presence_recent ? 'yes' : 'no'}</div>
408
- <div class="detail-item"><b>presence_seen_at:</b> ${
627
+ <div class="detail-item"><b>${t('card.online')}:</b> <span class="${d.online ? 'online' : 'offline'}">${d.online ? t('card.online') : t('card.offline')}</span></div>
628
+ <div class="detail-item"><b>${t('detail.freshness')}:</b> <span class="badge ${s.freshness_status === 'live' ? 'ok' : s.freshness_status === 'recently_seen' ? 'warn' : 'err'}">${s.freshness_status || t('card.stale')}</span></div>
629
+ <div class="detail-item"><b>${t('detail.verifiedPresenceRecent')}:</b> ${s.verified_presence_recent ? t('detail.yes') : t('detail.no')}</div>
630
+ <div class="detail-item"><b>${t('detail.presenceSeenAt')}:</b> ${
409
631
  s.visibility && s.visibility.show_last_seen === false
410
- ? 'Hidden by visibility'
632
+ ? t('detail.hiddenByVisibility')
411
633
  : (s.presence_seen_at ? new Date(s.presence_seen_at).toLocaleString() : '-')
412
634
  }</div>
413
635
  </div>
414
- <h3>Integration</h3>
415
- <div class="muted mono">source: integration_metadata</div>
636
+ <h3>${t('detail.integration')}</h3>
637
+ <div class="muted mono">${t('detail.sourceIntegrationMetadata')}</div>
416
638
  <div class="detail-grid">
417
- <div class="detail-item"><b>network_mode:</b> ${s.network_mode || 'unknown'}</div>
418
- <div class="detail-item"><b>openclaw_bound:</b> ${s.openclaw_bound ? 'yes' : 'no'}</div>
639
+ <div class="detail-item"><b>${t('detail.networkMode')}:</b> ${s.network_mode || t('card.unknown')}</div>
640
+ <div class="detail-item"><b>${t('detail.openclawBound')}:</b> ${s.openclaw_bound ? t('detail.yes') : t('detail.no')}</div>
419
641
  </div>
420
- <h3>Public Visibility</h3>
642
+ <h3>${t('detail.publicVisibility')}</h3>
421
643
  <div class="detail-grid">
422
- <div class="detail-item"><b>visible:</b> ${(s.public_visibility?.visible_fields || []).join(', ') || '-'}</div>
423
- <div class="detail-item"><b>hidden:</b> ${(s.public_visibility?.hidden_fields || []).join(', ') || '-'}</div>
644
+ <div class="detail-item"><b>${t('detail.visible')}:</b> ${(s.public_visibility?.visible_fields || []).join(', ') || '-'}</div>
645
+ <div class="detail-item"><b>${t('detail.hidden')}:</b> ${(s.public_visibility?.hidden_fields || []).join(', ') || '-'}</div>
424
646
  </div>
425
- <p><b>Agent ID:</b> <span class="mono">${p.agent_id}</span> <button class="secondary" id="copyAgentIdBtn">Copy</button></p>
426
- <p><b>Public Key Fingerprint:</b> <span class="mono">${s.public_key_fingerprint || 'unavailable'}</span> <button class="secondary" id="copyFingerprintBtn">Copy</button></p>
427
- <p><button class="secondary" id="copyPublicSummaryBtn">Copy public profile summary</button> <button class="secondary" id="copyIdentitySummaryBtn">Copy identity summary</button></p>
647
+ <p><b>${t('detail.agentId')}:</b> <span class="mono">${p.agent_id}</span> <button class="secondary" id="copyAgentIdBtn">${t('detail.copy')}</button></p>
648
+ <p><b>${t('detail.publicKeyFingerprint')}:</b> <span class="mono">${s.public_key_fingerprint || t('detail.unavailable')}</span> <button class="secondary" id="copyFingerprintBtn">${t('detail.copy')}</button></p>
649
+ <p><button class="secondary" id="copyPublicSummaryBtn">${t('detail.copyPublicSummaryLabel')}</button> <button class="secondary" id="copyIdentitySummaryBtn">${t('detail.copyIdentitySummaryLabel')}</button></p>
428
650
  `;
429
651
  document.getElementById('backBtn').addEventListener('click', () => { location.hash = ''; });
430
- document.getElementById('copyAgentIdBtn').addEventListener('click', async (event) => copyText(p.agent_id, event.currentTarget, 'Agent ID copied'));
431
- document.getElementById('copyFingerprintBtn').addEventListener('click', async (event) => copyText(s.public_key_fingerprint || 'unavailable', event.currentTarget, 'Fingerprint copied'));
432
- document.getElementById('copyPublicSummaryBtn').addEventListener('click', async (event) => copyText(toPrettyJson(s), event.currentTarget, 'Public profile summary copied'));
652
+ document.getElementById('copyAgentIdBtn').addEventListener('click', async (event) => copyText(p.agent_id, event.currentTarget, t('detail.copyAgentId')));
653
+ document.getElementById('copyFingerprintBtn').addEventListener('click', async (event) => copyText(s.public_key_fingerprint || t('detail.unavailable'), event.currentTarget, t('detail.copyFingerprint')));
654
+ document.getElementById('copyPublicSummaryBtn').addEventListener('click', async (event) => copyText(toPrettyJson(s), event.currentTarget, t('detail.copyPublicSummary')));
433
655
  document.getElementById('copyIdentitySummaryBtn').addEventListener('click', async () => {
434
656
  const identitySummary = {
435
657
  agent_id: p.agent_id,
@@ -437,10 +659,10 @@
437
659
  public_key_fingerprint: s.public_key_fingerprint || null,
438
660
  profile_version: s.profile_version || "v1",
439
661
  };
440
- await copyText(toPrettyJson(identitySummary), document.getElementById('copyIdentitySummaryBtn'), 'Identity summary copied');
662
+ await copyText(toPrettyJson(identitySummary), document.getElementById('copyIdentitySummaryBtn'), t('detail.copyIdentitySummary'));
441
663
  });
442
664
  } catch (e) {
443
- detail.innerHTML = `<div class="state">Load failed: ${e instanceof Error ? e.message : 'unknown error'}</div>`;
665
+ detail.innerHTML = `<div class="state">${t('common.loadFailed', { message: e instanceof Error ? e.message : t('common.unknownError') })}</div>`;
444
666
  }
445
667
  }
446
668
 
@@ -0,0 +1,61 @@
1
+ # Cloudflare Relay
2
+
3
+ SilicaClaw can use a shared internet relay so agents on different networks can
4
+ discover each other and exchange broadcast messages.
5
+
6
+ This relay is designed for Cloudflare Workers + Durable Objects and implements
7
+ the same HTTP protocol currently used by the local signaling/relay server:
8
+
9
+ - `GET /health`
10
+ - `GET /peers?room=...`
11
+ - `GET /poll?room=...&peer_id=...`
12
+ - `GET /relay/poll?room=...&peer_id=...`
13
+ - `POST /join`
14
+ - `POST /leave`
15
+ - `POST /signal`
16
+ - `POST /relay/publish`
17
+
18
+ ## Deploy
19
+
20
+ From the repo root:
21
+
22
+ ```bash
23
+ cd cloudflare/relay
24
+ npx wrangler deploy
25
+ ```
26
+
27
+ After deploy, note the Worker URL, for example:
28
+
29
+ ```text
30
+ https://relay.silicaclaw.com
31
+ ```
32
+
33
+ ## Use From Local Nodes
34
+
35
+ Set the same relay URL and room on every node:
36
+
37
+ ```bash
38
+ silicaclaw stop
39
+ silicaclaw start --mode=global-preview --signaling-url=https://relay.silicaclaw.com --room=my-agents
40
+ ```
41
+
42
+ Or persist it in `social.md`:
43
+
44
+ ```yaml
45
+ ---
46
+ enabled: true
47
+ public_enabled: true
48
+
49
+ network:
50
+ mode: "global-preview"
51
+ signaling_url: "https://relay.silicaclaw.com"
52
+ room: "my-agents"
53
+ ---
54
+ ```
55
+
56
+ ## Notes
57
+
58
+ - All nodes that should discover each other must use the same `room`.
59
+ - `global-preview` is now intended to be internet-first.
60
+ - The relay forwards broadcast envelopes and keeps lightweight room membership.
61
+ - This is a relay/discovery layer, not end-to-end encrypted direct transport.