@pingagent/sdk 0.1.9 → 0.1.11

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.
@@ -8,17 +8,28 @@ import {
8
8
  decideTaskPolicy,
9
9
  defaultTrustPolicyDoc,
10
10
  ensureTokenValid,
11
+ getActiveSessionFilePath,
12
+ getSessionBindingAlertsFilePath,
13
+ getSessionMapFilePath,
14
+ getTrustRecommendationActionLabel,
11
15
  loadIdentity,
12
16
  normalizeTrustPolicyDoc,
17
+ readCurrentActiveSessionKey,
18
+ readIngressRuntimeStatus,
19
+ readSessionBindingAlerts,
20
+ readSessionBindings,
21
+ removeSessionBinding,
22
+ setSessionBinding,
13
23
  summarizeTrustPolicyAudit,
14
24
  updateStoredToken,
15
25
  upsertTrustPolicyRecommendation
16
- } from "./chunk-PFABO4C7.js";
26
+ } from "./chunk-BLHMTUID.js";
17
27
 
18
28
  // src/web-server.ts
19
29
  import * as fs from "fs";
20
30
  import * as http from "http";
21
31
  import * as path from "path";
32
+ import { spawnSync } from "child_process";
22
33
  import { SCHEMA_TEXT } from "@pingagent/schemas";
23
34
 
24
35
  // src/host-panel-html.ts
@@ -67,6 +78,30 @@ function getHostPanelHtml() {
67
78
  padding: 16px;
68
79
  box-shadow: 0 14px 40px rgba(0, 0, 0, 0.22);
69
80
  }
81
+ .status-strip {
82
+ display: flex;
83
+ justify-content: space-between;
84
+ gap: 16px;
85
+ align-items: flex-start;
86
+ flex-wrap: wrap;
87
+ }
88
+ .status-main { display: grid; gap: 8px; }
89
+ .status-main h2 { margin: 0; font-size: 18px; }
90
+ .status-state {
91
+ display: inline-flex;
92
+ align-items: center;
93
+ gap: 8px;
94
+ width: fit-content;
95
+ padding: 6px 12px;
96
+ border-radius: 999px;
97
+ font-size: 12px;
98
+ letter-spacing: 0.08em;
99
+ text-transform: uppercase;
100
+ border: 1px solid #334155;
101
+ }
102
+ .status-state.ready { background: rgba(34, 197, 94, 0.12); color: #86efac; border-color: rgba(34, 197, 94, 0.35); }
103
+ .status-state.degraded { background: rgba(248, 113, 113, 0.14); color: #fecaca; border-color: rgba(248, 113, 113, 0.45); }
104
+ .quickstart-row { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
70
105
  .card h2, .card h3 { margin: 0 0 12px; font-size: 16px; }
71
106
  .stats .value { font-size: 28px; font-weight: 700; margin-top: 6px; }
72
107
  .runtime-layout { grid-template-columns: minmax(260px, 340px) minmax(0, 1fr); align-items: start; }
@@ -81,10 +116,12 @@ function getHostPanelHtml() {
81
116
  .session-row .top, .task-row .top, .recommendation-row .top { display: flex; justify-content: space-between; gap: 12px; align-items: center; }
82
117
  .label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; color: #94a3b8; }
83
118
  .badge { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 999px; font-size: 11px; border: 1px solid #334155; }
119
+ button.badge { cursor: pointer; font: inherit; }
84
120
  .badge.trusted { background: rgba(34, 197, 94, 0.12); color: #86efac; border-color: rgba(34, 197, 94, 0.35); }
85
121
  .badge.pending { background: rgba(250, 204, 21, 0.12); color: #fde68a; border-color: rgba(250, 204, 21, 0.35); }
86
122
  .badge.blocked, .badge.revoked { background: rgba(248, 113, 113, 0.12); color: #fca5a5; border-color: rgba(248, 113, 113, 0.35); }
87
123
  .badge.stranger { background: rgba(148, 163, 184, 0.12); color: #cbd5e1; border-color: rgba(148, 163, 184, 0.35); }
124
+ .badge.alert { background: rgba(248, 113, 113, 0.14); color: #fecaca; border-color: rgba(248, 113, 113, 0.45); }
88
125
  .policy-grid { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); align-items: start; }
89
126
  .two-col { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 16px; }
90
127
  .panel { display: none; }
@@ -145,11 +182,17 @@ function getHostPanelHtml() {
145
182
  </div>
146
183
  <div class="header-actions">
147
184
  <span class="pill" id="runtimeModePill">runtime_mode=bridge</span>
185
+ <span class="pill" id="receiveModePill">receive_mode=webhook</span>
148
186
  <span class="pill" id="policyPathPill">policy=\u2026</span>
187
+ <button class="secondary-btn" id="fixHooksBtn" style="width:auto">Fix OpenClaw Hooks</button>
188
+ <button class="secondary-btn" id="publicLinkBtn" style="width:auto">Public Link</button>
189
+ <button class="secondary-btn" id="contactCardBtn" style="width:auto">Contact Card</button>
190
+ <a class="secondary-btn" href="/a/demo" target="_blank" rel="noreferrer" style="width:auto">Try Demo Agent</a>
149
191
  </div>
150
192
  </div>
151
193
 
152
194
  <section id="runtimePanel" class="panel active">
195
+ <div class="card" id="activationCard" style="margin-bottom:16px"></div>
153
196
  <div class="grid stats" id="statsGrid"></div>
154
197
  <div class="grid runtime-layout" style="margin-top:16px">
155
198
  <div class="card">
@@ -243,6 +286,40 @@ function getHostPanelHtml() {
243
286
  <div class="audit-list" id="policyAuditList"></div>
244
287
  </div>
245
288
  </div>
289
+
290
+ <div class="card" style="margin-top:16px">
291
+ <h2>Profile + Capability Card</h2>
292
+ <div class="form-grid">
293
+ <label class="label">Display name</label>
294
+ <input id="profileDisplayName" placeholder="Agent name">
295
+ <label class="label">Bio</label>
296
+ <textarea id="profileBio" placeholder="What this agent does"></textarea>
297
+ <label class="label">Tags (comma separated)</label>
298
+ <input id="profileTags" placeholder="coding, devops">
299
+ <label class="label">Legacy capabilities (comma separated)</label>
300
+ <input id="profileCapabilities" placeholder="coding, testing">
301
+ <label class="label">Capability summary</label>
302
+ <textarea id="capabilityCardSummary" placeholder="Short machine-readable summary"></textarea>
303
+ <label class="label">Accepts new work</label>
304
+ <select id="capabilityCardAcceptsNewWork">
305
+ <option value="">(unspecified)</option>
306
+ <option value="true">true</option>
307
+ <option value="false">false</option>
308
+ </select>
309
+ <label class="label">Preferred contact mode</label>
310
+ <select id="capabilityCardContactMode">
311
+ <option value="">(unspecified)</option>
312
+ <option value="dm">dm</option>
313
+ <option value="task">task</option>
314
+ <option value="either">either</option>
315
+ </select>
316
+ <label class="label">Capability entries JSON</label>
317
+ <textarea id="capabilityCardItems" placeholder='[{"id":"coding","label":"Coding","accepts_tasks":true}]'></textarea>
318
+ <div class="row-actions">
319
+ <button class="action-btn" id="saveProfileBtn">Save Profile</button>
320
+ </div>
321
+ </div>
322
+ </div>
246
323
  </section>
247
324
  </main>
248
325
  </div>
@@ -267,6 +344,13 @@ function getHostPanelHtml() {
267
344
  .replace(/'/g, '&#39;');
268
345
  }
269
346
 
347
+ function parseCsvList(value) {
348
+ return String(value == null ? '' : value)
349
+ .split(',')
350
+ .map(function (item) { return item.trim(); })
351
+ .filter(Boolean);
352
+ }
353
+
270
354
  function fmtTs(value) {
271
355
  if (!value) return '-';
272
356
  try { return new Date(value).toLocaleString(); } catch { return String(value); }
@@ -328,20 +412,117 @@ function getHostPanelHtml() {
328
412
  const profileLabel = state.selectedProfile ? 'profile=' + state.selectedProfile : 'Select profile';
329
413
  const title = overview ? ('Host Panel \xB7 ' + overview.did) : 'PingAgent Host Panel';
330
414
  const tier = overview && overview.subscription ? overview.subscription.tier : null;
415
+ const receiveMode = overview && overview.ingressRuntime ? overview.ingressRuntime.receive_mode : 'webhook';
331
416
  document.getElementById('headerTitle').textContent = title;
332
417
  document.getElementById('headerSubtitle').textContent = overview
333
418
  ? (profileLabel + ' \xB7 ' + overview.serverUrl + (tier ? (' \xB7 tier=' + tier) : '') + ' \xB7 sessions=' + overview.sessionsTotal + ' \xB7 unread=' + overview.unreadTotal)
334
419
  : profileLabel;
335
420
  document.getElementById('runtimeModePill').textContent = overview ? ('runtime_mode=' + overview.runtimeMode) : 'runtime_mode=\u2026';
421
+ document.getElementById('receiveModePill').textContent = 'receive_mode=' + receiveMode;
336
422
  document.getElementById('policyPathPill').textContent = overview ? ('policy=' + overview.trustPolicyPath) : 'policy=\u2026';
423
+ document.getElementById('fixHooksBtn').textContent = receiveMode === 'polling_degraded' || (overview && overview.ingressRuntime && overview.ingressRuntime.hooks_last_error) ? 'Fix now' : 'Fix OpenClaw Hooks';
424
+ }
425
+
426
+ function ingressStatusModel(overview) {
427
+ const ingress = overview && overview.ingressRuntime ? overview.ingressRuntime : null;
428
+ const degraded = !ingress || ingress.receive_mode === 'polling_degraded' || !!ingress.hooks_last_error;
429
+ return {
430
+ degraded: degraded,
431
+ label: degraded ? 'Degraded' : 'Ready',
432
+ className: degraded ? 'degraded' : 'ready',
433
+ detail: degraded
434
+ ? (ingress && ingress.hooks_last_error ? ingress.hooks_last_error : 'Webhook ingress is degraded. Fix hooks or keep running on polling fallback.')
435
+ : 'Webhook ingress is healthy. New inbound messages land on the main runtime path.',
436
+ };
437
+ }
438
+
439
+ function recommendationActionLabel(item) {
440
+ if (!item) return 'Apply Recommendation';
441
+ if (item.status === 'dismissed' || item.status === 'superseded') return 'Reopen';
442
+ if (item.status === 'applied') return 'Applied';
443
+ if (item.primary_action_label) return item.primary_action_label;
444
+ if (item.policy === 'contact' && item.action === 'approve') return 'Approve + remember sender';
445
+ if (item.policy === 'contact' && item.action === 'manual') return 'Keep contact manual';
446
+ if (item.policy === 'contact' && item.action === 'reject') return 'Block this sender';
447
+ if (item.policy === 'task' && item.action === 'bridge') return 'Keep tasks manual';
448
+ if (item.policy === 'task' && item.action === 'execute') return 'Allow tasks from this sender';
449
+ if (item.policy === 'task' && item.action === 'deny') return 'Block tasks from this sender';
450
+ return 'Apply Recommendation';
451
+ }
452
+
453
+ function formatCapabilityCardEditor(card) {
454
+ if (!card) return '';
455
+ try {
456
+ return JSON.stringify(Array.isArray(card.capabilities) ? card.capabilities : [], null, 2);
457
+ } catch {
458
+ return '';
459
+ }
460
+ }
461
+
462
+ function renderSummaryBlock(summary) {
463
+ if (!summary) {
464
+ return '<div class="empty">No carry-forward summary yet. Save one below to make delegation and session continuation cheaper.</div>';
465
+ }
466
+ const parts = [
467
+ summary.objective ? '<div><span class="label">Objective</span><div style="margin-top:6px">' + esc(summary.objective) + '</div></div>' : '',
468
+ summary.context ? '<div><span class="label">Context</span><div style="margin-top:6px">' + esc(summary.context) + '</div></div>' : '',
469
+ summary.constraints ? '<div><span class="label">Constraints</span><div style="margin-top:6px">' + esc(summary.constraints) + '</div></div>' : '',
470
+ summary.decisions ? '<div><span class="label">Decisions</span><div style="margin-top:6px">' + esc(summary.decisions) + '</div></div>' : '',
471
+ summary.open_questions ? '<div><span class="label">Open Questions</span><div style="margin-top:6px">' + esc(summary.open_questions) + '</div></div>' : '',
472
+ summary.next_action ? '<div><span class="label">Next Action</span><div style="margin-top:6px">' + esc(summary.next_action) + '</div></div>' : '',
473
+ summary.handoff_ready_text ? '<div><span class="label">Handoff Ready</span><pre style="margin-top:6px">' + esc(summary.handoff_ready_text) + '</pre></div>' : '',
474
+ ].filter(Boolean);
475
+ parts.push('<div class="muted small">updated=' + esc(fmtTs(summary.updated_at)) + '</div>');
476
+ return parts.join('');
477
+ }
478
+
479
+ function renderHandoffBlock(handoff) {
480
+ if (!handoff) return '';
481
+ return '' +
482
+ '<div style="margin-top:8px;padding:10px 12px;border:1px solid #1d4ed8;border-radius:10px;background:rgba(30,64,175,0.18)">' +
483
+ '<div class="label">Handoff</div>' +
484
+ (handoff.objective ? '<div class="muted small" style="margin-top:6px">objective=' + esc(handoff.objective) + '</div>' : '') +
485
+ (handoff.priority ? '<div class="muted small">priority=' + esc(handoff.priority) + '</div>' : '') +
486
+ (handoff.success_criteria ? '<div class="muted small">success=' + esc(handoff.success_criteria) + '</div>' : '') +
487
+ (handoff.callback_session_key ? '<div class="muted small">callback=' + esc(handoff.callback_session_key) + '</div>' : '') +
488
+ (handoff.delegated_by || handoff.delegated_to
489
+ ? '<div class="muted small">delegated_by=' + esc(handoff.delegated_by || '(unknown)') + ' \xB7 delegated_to=' + esc(handoff.delegated_to || '(unknown)') + '</div>'
490
+ : '') +
491
+ (handoff.carry_forward_summary
492
+ ? '<pre style="margin-top:8px">' + esc(handoff.carry_forward_summary) + '</pre>'
493
+ : '') +
494
+ '</div>';
337
495
  }
338
496
 
339
497
  function renderOverview() {
340
498
  const overview = state.overview;
341
499
  if (!overview) return;
500
+ const ingressState = ingressStatusModel(overview);
501
+ document.getElementById('activationCard').innerHTML =
502
+ '<div class="status-strip">' +
503
+ '<div class="status-main">' +
504
+ '<h2>Activation</h2>' +
505
+ '<div class="status-state ' + ingressState.className + '">' + esc(ingressState.label) + '</div>' +
506
+ '<div class="muted small">' + esc(ingressState.detail) + '</div>' +
507
+ '<div class="muted small">Public link: ' + esc(overview.publicSelf && overview.publicSelf.public_url ? overview.publicSelf.public_url : '(not ready yet)') + '</div>' +
508
+ '</div>' +
509
+ '<div style="min-width:320px">' +
510
+ '<div class="label">Quick Start</div>' +
511
+ '<div class="quickstart-row">' +
512
+ '<button class="action-btn demo-preset-btn" data-preset="hello">Demo: hello</button>' +
513
+ '<button class="secondary-btn demo-preset-btn" data-preset="delegate">Demo: delegate</button>' +
514
+ '<button class="secondary-btn demo-preset-btn" data-preset="trust">Demo: trust</button>' +
515
+ '</div>' +
516
+ '<div class="quickstart-row">' +
517
+ '<button class="secondary-btn" id="copyPublicLinkBtn">Copy Public Link</button>' +
518
+ '<button class="secondary-btn" id="makeContactCardBtn">Share Contact Card</button>' +
519
+ '</div>' +
520
+ '</div>' +
521
+ '</div>';
342
522
  const subscription = overview.subscription || null;
343
523
  const stats = [
344
524
  { label: 'Plan', value: subscription ? subscription.tier : 'ghost', sub: subscription ? subscription.summary : 'subscription unavailable' },
525
+ { label: 'Ingress', value: overview.ingressRuntime ? overview.ingressRuntime.receive_mode : 'webhook', sub: overview.ingressRuntime && overview.ingressRuntime.reason ? overview.ingressRuntime.reason : 'OpenClaw ingress receive mode' },
345
526
  { label: 'Relay', value: subscription ? (subscription.usage.relay_today + '/' + subscription.usage.relay_limit) : '-', sub: subscription ? ('retention=' + subscription.retention_label) : 'daily relay usage' },
346
527
  { label: 'Alias', value: subscription ? (subscription.usage.alias_count + '/' + subscription.usage.alias_limit) : '-', sub: subscription ? (subscription.audit_export_allowed ? 'audit export enabled' : 'audit export off on this plan') : 'identity limits' },
347
528
  { label: 'Sessions', value: overview.sessionsTotal, sub: JSON.stringify(overview.trustCounts || {}) },
@@ -349,6 +530,7 @@ function getHostPanelHtml() {
349
530
  { label: 'Tasks', value: overview.tasksTotal, sub: 'recent local task threads' },
350
531
  { label: 'Audit', value: overview.auditSummary.total_events, sub: 'policy / runtime audit events' },
351
532
  { label: 'Recommendations', value: overview.recommendationSummary ? overview.recommendationSummary.total : overview.recommendations.length, sub: overview.recommendationSummary ? JSON.stringify(overview.recommendationSummary.by_status || {}) : 'learned policy suggestions' },
533
+ { label: 'Public Link', value: overview.publicSelf && overview.publicSelf.public_url ? 'ready' : 'disabled', sub: overview.publicSelf && overview.publicSelf.public_url ? overview.publicSelf.public_url : 'create a hosted shareable profile link' },
352
534
  ];
353
535
  document.getElementById('statsGrid').innerHTML = stats.map(function (item) {
354
536
  return '<div class="card"><div class="label">' + esc(item.label) + '</div><div class="value">' + esc(item.value) + '</div><div class="muted small">' + esc(item.sub) + '</div></div>';
@@ -361,11 +543,17 @@ function getHostPanelHtml() {
361
543
  if (!state.selectedSessionKey) state.selectedSessionKey = sessions[0].session_key;
362
544
  document.getElementById('sessionList').innerHTML = sessions.map(function (session) {
363
545
  const active = session.session_key === state.selectedSessionKey ? ' active' : '';
364
- const badge = '<span class="badge ' + esc(session.trust_state) + '">' + esc(session.trust_state) + '</span>';
546
+ const badges = [
547
+ '<span class="badge ' + esc(session.trust_state) + '">' + esc(session.trust_state) + '</span>',
548
+ session.binding_alert
549
+ ? '<button type="button" class="badge alert rebind-badge-btn" data-session="' + esc(session.session_key) + '" data-conversation="' + esc(session.conversation_id || '') + '" data-bound-session="' + esc(session.mapped_work_session || '') + '" data-remote-did="' + esc(session.remote_did || '') + '" title="' + esc(session.binding_alert.message || 'Rebind this conversation to the current chat session') + '">Needs rebind</button>'
550
+ : '',
551
+ ].filter(Boolean).join('');
365
552
  return '<div class="session-row' + active + '" data-session="' + esc(session.session_key) + '">' +
366
- '<div class="top"><strong>' + esc(session.remote_did || session.conversation_id || 'unknown') + '</strong>' + badge + '</div>' +
553
+ '<div class="top"><strong>' + esc(session.remote_did || session.conversation_id || 'unknown') + '</strong><div style="display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end">' + badges + '</div></div>' +
367
554
  '<div class="muted small" style="margin-top:6px">conversation=' + esc(session.conversation_id || '(none)') + '</div>' +
368
555
  '<div class="muted small">unread=' + esc(session.unread_count) + ' \xB7 last=' + esc(fmtTs(session.last_remote_activity_at || session.updated_at)) + '</div>' +
556
+ '<div class="muted small">work_session=' + esc(session.mapped_work_session || '(unbound)') + (session.is_active_work_session ? ' \xB7 active_chat=true' : '') + '</div>' +
369
557
  '<div class="muted small" style="margin-top:6px">' + esc(session.last_message_preview || '(no preview)') + '</div>' +
370
558
  '</div>';
371
559
  }).join('');
@@ -376,6 +564,20 @@ function getHostPanelHtml() {
376
564
  loadSession(state.selectedSessionKey);
377
565
  });
378
566
  });
567
+ document.getElementById('sessionList').querySelectorAll('.rebind-badge-btn').forEach(function (btn) {
568
+ btn.addEventListener('click', async function (event) {
569
+ event.stopPropagation();
570
+ const sessionKey = btn.getAttribute('data-session');
571
+ const conversationId = btn.getAttribute('data-conversation');
572
+ const previousBinding = btn.getAttribute('data-bound-session');
573
+ const remoteDid = btn.getAttribute('data-remote-did');
574
+ if (!sessionKey || !conversationId) return;
575
+ state.selectedSessionKey = sessionKey;
576
+ renderOverview();
577
+ await loadSession(sessionKey);
578
+ await promptBindCurrentChat(conversationId, previousBinding, remoteDid);
579
+ });
580
+ });
379
581
  }
380
582
 
381
583
  const tasks = Array.isArray(overview.tasks) ? overview.tasks : [];
@@ -386,6 +588,7 @@ function getHostPanelHtml() {
386
588
  '<div class="muted small">updated=' + esc(fmtTs(task.updated_at)) + '</div>' +
387
589
  (task.result_summary ? '<div style="margin-top:8px">' + esc(task.result_summary) + '</div>' : '') +
388
590
  (task.error_message ? '<div style="margin-top:8px;color:#fca5a5">' + esc(task.error_message) + '</div>' : '') +
591
+ renderHandoffBlock(task.handoff) +
389
592
  '</div>';
390
593
  }).join('')
391
594
  : '<div class="empty">No recent task threads.</div>';
@@ -403,6 +606,56 @@ function getHostPanelHtml() {
403
606
  : '') +
404
607
  '</div>' + document.getElementById('taskList').innerHTML;
405
608
  }
609
+
610
+ document.querySelectorAll('.demo-preset-btn').forEach(function (btn) {
611
+ btn.addEventListener('click', async function () {
612
+ const preset = btn.getAttribute('data-preset');
613
+ const result = await api('/api/runtime/demo', {
614
+ method: 'POST',
615
+ headers: { 'Content-Type': 'application/json' },
616
+ body: JSON.stringify({ preset: preset }),
617
+ });
618
+ window.alert(result.sent
619
+ ? ('Demo agent messaged.
620
+
621
+ conversation=' + result.conversation_id)
622
+ : ('Demo agent ready.
623
+
624
+ conversation=' + result.conversation_id));
625
+ await refreshAll();
626
+ });
627
+ });
628
+ const copyPublicLinkBtn = document.getElementById('copyPublicLinkBtn');
629
+ if (copyPublicLinkBtn) {
630
+ copyPublicLinkBtn.addEventListener('click', async function () {
631
+ const url = overview.publicSelf && overview.publicSelf.public_url ? overview.publicSelf.public_url : '';
632
+ if (!url) {
633
+ window.alert('No public link yet. Use Public Link or refresh after hosted auto-create.');
634
+ return;
635
+ }
636
+ try {
637
+ await navigator.clipboard.writeText(url);
638
+ window.alert('Copied public link:
639
+ ' + url);
640
+ } catch {
641
+ window.prompt('Copy public link', url);
642
+ }
643
+ });
644
+ }
645
+ const makeContactCardBtn = document.getElementById('makeContactCardBtn');
646
+ if (makeContactCardBtn) {
647
+ makeContactCardBtn.addEventListener('click', async function () {
648
+ const result = await api('/api/public/contact-card', {
649
+ method: 'POST',
650
+ headers: { 'Content-Type': 'application/json' },
651
+ body: JSON.stringify({
652
+ target_did: overview && overview.did ? overview.did : undefined,
653
+ }),
654
+ });
655
+ window.alert('Contact card ready:
656
+ ' + (result.share_url || '(missing share_url)'));
657
+ });
658
+ }
406
659
  }
407
660
 
408
661
  function renderSession() {
@@ -419,6 +672,12 @@ function getHostPanelHtml() {
419
672
  const messages = Array.isArray(detail.messages) ? detail.messages : [];
420
673
  const auditEvents = Array.isArray(detail.auditEvents) ? detail.auditEvents : [];
421
674
  const recommendations = Array.isArray(detail.recommendations) ? detail.recommendations : [];
675
+ const openRecommendation = recommendations.find(function (item) { return item.status === 'open'; }) || null;
676
+ const reopenRecommendation = recommendations.find(function (item) { return item.status === 'dismissed' || item.status === 'superseded'; }) || null;
677
+ const binding = detail.binding || null;
678
+ const bindingAlert = detail.bindingAlert || null;
679
+ const activeWorkSession = detail.activeWorkSession || null;
680
+ const summary = detail.sessionSummary || null;
422
681
 
423
682
  el.innerHTML = '' +
424
683
  '<div class="two-col">' +
@@ -429,12 +688,54 @@ function getHostPanelHtml() {
429
688
  '<div class="muted small">conversation=' + esc(session.conversation_id || '(none)') + '</div>' +
430
689
  '<div class="muted small">trust=' + esc(session.trust_state) + ' \xB7 unread=' + esc(session.unread_count) + '</div>' +
431
690
  '<div class="muted small">last activity=' + esc(fmtTs(session.last_remote_activity_at || session.updated_at)) + '</div>' +
691
+ '<div class="muted small" style="margin-top:8px">active_chat_session=' + esc(activeWorkSession || '(none)') + '</div>' +
692
+ '<div class="muted small">binding=' + esc(binding ? binding.session_key : '(unbound)') + '</div>' +
693
+ (openRecommendation
694
+ ? '<div class="muted small" style="margin-top:8px">trust_action=' + esc(recommendationActionLabel(openRecommendation)) + '</div>'
695
+ : (reopenRecommendation ? '<div class="muted small" style="margin-top:8px">trust_action=' + esc(recommendationActionLabel(reopenRecommendation)) + '</div>' : '')) +
696
+ (bindingAlert
697
+ ? '<div style="margin-top:10px;padding:10px 12px;border:1px solid #ef4444;border-radius:10px;background:rgba(127,29,29,0.25);color:#fecaca"><strong>Needs rebind</strong><div class="small" style="margin-top:6px">' + esc(bindingAlert.message || 'Bound work session is missing. Rebind this PingAgent conversation to the current chat session.') + '</div></div>'
698
+ : '') +
699
+ '<div class="row-actions">' +
700
+ (openRecommendation
701
+ ? '<button class="action-btn apply-session-recommendation-btn" data-session="' + esc(session.session_key) + '">' + esc(recommendationActionLabel(openRecommendation)) + '</button>'
702
+ : '') +
703
+ (openRecommendation
704
+ ? '<button class="danger-btn dismiss-session-recommendation-btn" data-session="' + esc(session.session_key) + '">Dismiss</button>'
705
+ : '') +
706
+ (!openRecommendation && reopenRecommendation
707
+ ? '<button class="secondary-btn reopen-session-recommendation-btn" data-session="' + esc(session.session_key) + '">Reopen</button>'
708
+ : '') +
709
+ '<button class="action-btn bind-current-btn" data-conversation="' + esc(session.conversation_id || '') + '">Bind Current Chat</button>' +
710
+ '<button class="danger-btn clear-binding-btn" data-conversation="' + esc(session.conversation_id || '') + '">Clear Binding</button>' +
711
+ '</div>' +
432
712
  '</div>' +
433
713
  '<div>' +
434
714
  '<div class="label">Policy Decisions</div>' +
435
715
  '<pre style="margin-top:8px">[Contact]\\naction=' + esc(contact.action) + '\\nsource=' + esc(contact.source) + (contact.matched_rule ? '\\nmatched_rule=' + esc(contact.matched_rule) : '') + '\\n' + esc(contact.explanation) + '\\n\\n[Task]\\naction=' + esc(task.action) + '\\nsource=' + esc(task.source) + (task.matched_rule ? '\\nmatched_rule=' + esc(task.matched_rule) : '') + '\\n' + esc(task.explanation) + '</pre>' +
436
716
  '</div>' +
437
717
  '</div>' +
718
+ '<div class="grid two-col" style="margin-top:16px">' +
719
+ '<div>' +
720
+ '<div class="label">Carry-Forward Summary</div>' +
721
+ '<div style="margin-top:8px">' + renderSummaryBlock(summary) + '</div>' +
722
+ '</div>' +
723
+ '<div>' +
724
+ '<div class="label">Update Summary</div>' +
725
+ '<div class="form-grid" style="margin-top:8px">' +
726
+ '<input id="sessionSummaryObjective" placeholder="Objective" value="' + esc(summary && summary.objective ? summary.objective : '') + '">' +
727
+ '<textarea id="sessionSummaryContext" placeholder="Context">' + esc(summary && summary.context ? summary.context : '') + '</textarea>' +
728
+ '<textarea id="sessionSummaryConstraints" placeholder="Constraints">' + esc(summary && summary.constraints ? summary.constraints : '') + '</textarea>' +
729
+ '<textarea id="sessionSummaryDecisions" placeholder="Decisions">' + esc(summary && summary.decisions ? summary.decisions : '') + '</textarea>' +
730
+ '<textarea id="sessionSummaryOpenQuestions" placeholder="Open questions">' + esc(summary && summary.open_questions ? summary.open_questions : '') + '</textarea>' +
731
+ '<textarea id="sessionSummaryNextAction" placeholder="Next action">' + esc(summary && summary.next_action ? summary.next_action : '') + '</textarea>' +
732
+ '<textarea id="sessionSummaryHandoff" placeholder="Handoff-ready summary">' + esc(summary && summary.handoff_ready_text ? summary.handoff_ready_text : '') + '</textarea>' +
733
+ '<div class="row-actions">' +
734
+ '<button class="action-btn" id="saveSessionSummaryBtn">Save Summary</button>' +
735
+ '</div>' +
736
+ '</div>' +
737
+ '</div>' +
738
+ '</div>' +
438
739
  '<div class="grid two-col" style="margin-top:16px">' +
439
740
  '<div><div class="label">Task Threads</div><div class="task-list" style="margin-top:8px">' +
440
741
  (tasks.length ? tasks.map(function (taskItem) {
@@ -442,15 +743,26 @@ function getHostPanelHtml() {
442
743
  '<div class="muted small">updated=' + esc(fmtTs(taskItem.updated_at)) + '</div>' +
443
744
  (taskItem.result_summary ? '<div style="margin-top:8px">' + esc(taskItem.result_summary) + '</div>' : '') +
444
745
  (taskItem.error_message ? '<div style="margin-top:8px;color:#fca5a5">' + esc(taskItem.error_message) + '</div>' : '') +
746
+ renderHandoffBlock(taskItem.handoff) +
445
747
  '</div>';
446
748
  }).join('') : '<div class="empty">No tasks in this session.</div>') +
447
749
  '</div></div>' +
448
750
  '<div><div class="label">Learned Recommendations</div><div class="recommendation-list" style="margin-top:8px">' +
449
751
  (recommendations.length ? recommendations.map(function (item) {
752
+ const actionButton = item.status === 'open'
753
+ ? '<button class="action-btn apply-session-recommendation-btn" data-session="' + esc(session.session_key) + '">' + esc(recommendationActionLabel(item)) + '</button>'
754
+ : '';
755
+ const dismissButton = item.status === 'open'
756
+ ? '<button class="danger-btn dismiss-session-recommendation-btn" data-session="' + esc(session.session_key) + '">Dismiss</button>'
757
+ : '';
758
+ const reopenButton = (item.status === 'dismissed' || item.status === 'superseded')
759
+ ? '<button class="secondary-btn reopen-session-recommendation-btn" data-session="' + esc(session.session_key) + '">Reopen</button>'
760
+ : '';
450
761
  return '<div class="recommendation-row"><div class="top"><strong>' + esc(item.policy) + '</strong><span class="badge">' + esc(item.status + ' \xB7 ' + item.action) + '</span></div>' +
451
762
  '<div class="muted small">current=' + esc(item.current_action) + ' \xB7 confidence=' + esc(item.confidence) + '</div>' +
452
763
  '<div class="muted small">match=' + esc(item.match) + '</div>' +
453
764
  '<div style="margin-top:8px">' + esc(item.reason) + '</div>' +
765
+ '<div class="row-actions">' + actionButton + dismissButton + reopenButton + '</div>' +
454
766
  '</div>';
455
767
  }).join('') : '<div class="empty">No learned recommendation for this session.</div>') +
456
768
  '</div></div>' +
@@ -473,13 +785,130 @@ function getHostPanelHtml() {
473
785
  }).join('') : '<div class="empty">No audit events for this session.</div>') +
474
786
  '</div></div>' +
475
787
  '</div>';
788
+
789
+ el.querySelectorAll('.bind-current-btn').forEach(function (btn) {
790
+ btn.addEventListener('click', async function () {
791
+ const conversationId = btn.getAttribute('data-conversation');
792
+ if (!conversationId) return;
793
+ await promptBindCurrentChat(conversationId);
794
+ });
795
+ });
796
+ el.querySelectorAll('.clear-binding-btn').forEach(function (btn) {
797
+ btn.addEventListener('click', async function () {
798
+ await api('/api/runtime/session-bindings/clear', {
799
+ method: 'POST',
800
+ headers: { 'Content-Type': 'application/json' },
801
+ body: JSON.stringify({ conversation_id: btn.getAttribute('data-conversation') }),
802
+ });
803
+ await refreshAll();
804
+ setTab('runtime');
805
+ });
806
+ });
807
+ el.querySelectorAll('.apply-session-recommendation-btn').forEach(function (btn) {
808
+ btn.addEventListener('click', async function () {
809
+ await api('/api/runtime/policy/recommendations/apply-session', {
810
+ method: 'POST',
811
+ headers: { 'Content-Type': 'application/json' },
812
+ body: JSON.stringify({ session_key: btn.getAttribute('data-session') }),
813
+ });
814
+ await refreshAll();
815
+ setTab('runtime');
816
+ });
817
+ });
818
+ el.querySelectorAll('.dismiss-session-recommendation-btn').forEach(function (btn) {
819
+ btn.addEventListener('click', async function () {
820
+ await api('/api/runtime/policy/recommendations/dismiss-session', {
821
+ method: 'POST',
822
+ headers: { 'Content-Type': 'application/json' },
823
+ body: JSON.stringify({ session_key: btn.getAttribute('data-session') }),
824
+ });
825
+ await refreshAll();
826
+ setTab('runtime');
827
+ });
828
+ });
829
+ el.querySelectorAll('.reopen-session-recommendation-btn').forEach(function (btn) {
830
+ btn.addEventListener('click', async function () {
831
+ await api('/api/runtime/policy/recommendations/reopen-session', {
832
+ method: 'POST',
833
+ headers: { 'Content-Type': 'application/json' },
834
+ body: JSON.stringify({ session_key: btn.getAttribute('data-session') }),
835
+ });
836
+ await refreshAll();
837
+ setTab('runtime');
838
+ });
839
+ });
840
+ const saveSessionSummaryBtn = document.getElementById('saveSessionSummaryBtn');
841
+ if (saveSessionSummaryBtn) {
842
+ saveSessionSummaryBtn.addEventListener('click', async function () {
843
+ await api('/api/runtime/session-summary', {
844
+ method: 'POST',
845
+ headers: { 'Content-Type': 'application/json' },
846
+ body: JSON.stringify({
847
+ session_key: session.session_key,
848
+ objective: document.getElementById('sessionSummaryObjective').value.trim(),
849
+ context: document.getElementById('sessionSummaryContext').value.trim(),
850
+ constraints: document.getElementById('sessionSummaryConstraints').value.trim(),
851
+ decisions: document.getElementById('sessionSummaryDecisions').value.trim(),
852
+ open_questions: document.getElementById('sessionSummaryOpenQuestions').value.trim(),
853
+ next_action: document.getElementById('sessionSummaryNextAction').value.trim(),
854
+ handoff_ready_text: document.getElementById('sessionSummaryHandoff').value.trim(),
855
+ }),
856
+ });
857
+ await refreshAll();
858
+ setTab('runtime');
859
+ });
860
+ }
861
+ }
862
+
863
+ async function promptBindCurrentChat(conversationId, previousBinding, remoteDid) {
864
+ const current = state.session && state.session.activeWorkSession
865
+ ? state.session.activeWorkSession
866
+ : (state.overview && state.overview.activeWorkSession ? state.overview.activeWorkSession : null);
867
+ const previous = previousBinding || (state.session && state.session.binding ? state.session.binding.session_key : null) || '(unbound)';
868
+ const targetRemoteDid = remoteDid || (state.session && state.session.session ? state.session.session.remote_did : null) || '(unknown)';
869
+ const confirmed = window.confirm(
870
+ 'Rebind this PingAgent conversation to the current chat session?' +
871
+ '
872
+
873
+ Conversation: ' + conversationId +
874
+ '
875
+ Remote DID: ' + targetRemoteDid +
876
+ '
877
+
878
+ Current chat: ' + (current || '(none)') +
879
+ '
880
+ Previous binding: ' + previous
881
+ );
882
+ if (!confirmed) return;
883
+ await api('/api/runtime/session-bindings/bind-current', {
884
+ method: 'POST',
885
+ headers: { 'Content-Type': 'application/json' },
886
+ body: JSON.stringify({ conversation_id: conversationId }),
887
+ });
888
+ await refreshAll();
889
+ setTab('runtime');
476
890
  }
477
891
 
478
892
  function renderPolicy() {
479
893
  const policy = state.policy;
480
894
  if (!policy) return;
895
+ const profile = state.overview && state.overview.profile ? state.overview.profile : null;
481
896
  document.getElementById('contactDefault').value = policy.doc.contact_policy.default_action;
482
897
  document.getElementById('taskDefault').value = policy.doc.task_policy.default_action;
898
+ document.getElementById('profileDisplayName').value = profile && profile.display_name ? profile.display_name : '';
899
+ document.getElementById('profileBio').value = profile && profile.bio ? profile.bio : '';
900
+ document.getElementById('profileTags').value = profile && Array.isArray(profile.tags) ? profile.tags.join(', ') : '';
901
+ document.getElementById('profileCapabilities').value = profile && Array.isArray(profile.capabilities) ? profile.capabilities.join(', ') : '';
902
+ document.getElementById('capabilityCardSummary').value = profile && profile.capability_card && profile.capability_card.summary ? profile.capability_card.summary : '';
903
+ document.getElementById('capabilityCardAcceptsNewWork').value =
904
+ profile && profile.capability_card && typeof profile.capability_card.accepts_new_work === 'boolean'
905
+ ? String(profile.capability_card.accepts_new_work)
906
+ : '';
907
+ document.getElementById('capabilityCardContactMode').value =
908
+ profile && profile.capability_card && profile.capability_card.preferred_contact_mode
909
+ ? profile.capability_card.preferred_contact_mode
910
+ : '';
911
+ document.getElementById('capabilityCardItems').value = formatCapabilityCardEditor(profile && profile.capability_card ? profile.capability_card : null);
483
912
 
484
913
  const rules = [];
485
914
  policy.doc.contact_policy.rules.forEach(function (rule) {
@@ -520,7 +949,7 @@ function getHostPanelHtml() {
520
949
  if (!list.length) return '';
521
950
  return '<div><div class="label" style="margin-bottom:8px">' + esc(status) + '</div>' + list.map(function (item) {
522
951
  const applyButton = status !== 'applied'
523
- ? '<button class="action-btn apply-recommendation-btn" data-recommendation-id="' + esc(item.id) + '">Apply</button>'
952
+ ? '<button class="action-btn apply-recommendation-btn" data-recommendation-id="' + esc(item.id) + '">' + esc(recommendationActionLabel(item)) + '</button>'
524
953
  : '';
525
954
  const dismissButton = status === 'open'
526
955
  ? '<button class="danger-btn dismiss-recommendation-btn" data-recommendation-id="' + esc(item.id) + '">Dismiss</button>'
@@ -582,6 +1011,45 @@ function getHostPanelHtml() {
582
1011
  : '<div class="empty">No audit events yet.</div>';
583
1012
 
584
1013
  updateRuleActionOptions();
1014
+
1015
+ const saveProfileBtn = document.getElementById('saveProfileBtn');
1016
+ if (saveProfileBtn) {
1017
+ saveProfileBtn.onclick = async function () {
1018
+ let capabilityItems = [];
1019
+ const rawItems = document.getElementById('capabilityCardItems').value.trim();
1020
+ if (rawItems) {
1021
+ try {
1022
+ const parsed = JSON.parse(rawItems);
1023
+ if (!Array.isArray(parsed)) throw new Error('Capability entries JSON must be an array.');
1024
+ capabilityItems = parsed;
1025
+ } catch (err) {
1026
+ window.alert(err && err.message ? err.message : 'Invalid capability entries JSON');
1027
+ return;
1028
+ }
1029
+ }
1030
+ const acceptsNewWorkValue = document.getElementById('capabilityCardAcceptsNewWork').value;
1031
+ const capabilityCard = {
1032
+ version: '1',
1033
+ summary: document.getElementById('capabilityCardSummary').value.trim() || undefined,
1034
+ accepts_new_work: acceptsNewWorkValue === '' ? undefined : acceptsNewWorkValue === 'true',
1035
+ preferred_contact_mode: document.getElementById('capabilityCardContactMode').value || undefined,
1036
+ capabilities: capabilityItems,
1037
+ };
1038
+ await api('/api/profile', {
1039
+ method: 'POST',
1040
+ headers: { 'Content-Type': 'application/json' },
1041
+ body: JSON.stringify({
1042
+ display_name: document.getElementById('profileDisplayName').value.trim() || undefined,
1043
+ bio: document.getElementById('profileBio').value.trim() || undefined,
1044
+ tags: parseCsvList(document.getElementById('profileTags').value),
1045
+ capabilities: parseCsvList(document.getElementById('profileCapabilities').value),
1046
+ capability_card: capabilityCard,
1047
+ }),
1048
+ });
1049
+ await refreshAll();
1050
+ setTab('policy');
1051
+ };
1052
+ }
585
1053
  }
586
1054
 
587
1055
  function updateRuleActionOptions() {
@@ -668,6 +1136,46 @@ function getHostPanelHtml() {
668
1136
  document.getElementById('simulateOutput').textContent = JSON.stringify(result, null, 2);
669
1137
  setTab('policy');
670
1138
  });
1139
+ document.getElementById('fixHooksBtn').addEventListener('click', async function () {
1140
+ const confirmed = window.confirm('Repair OpenClaw hooks config now? A timestamped backup of openclaw.json will be written first.');
1141
+ if (!confirmed) return;
1142
+ const result = await api('/api/runtime/openclaw/fix-hooks', { method: 'POST' });
1143
+ window.alert((result.ok ? 'Hooks repaired.
1144
+
1145
+ ' : 'Hooks repair reported an error.
1146
+
1147
+ ') + (result.stdout || result.stderr || 'No output'));
1148
+ await refreshAll();
1149
+ });
1150
+ document.getElementById('publicLinkBtn').addEventListener('click', async function () {
1151
+ const suggested = state.overview && state.overview.publicSelf && state.overview.publicSelf.public_slug
1152
+ ? state.overview.publicSelf.public_slug
1153
+ : '';
1154
+ const slug = window.prompt('Public share slug (leave empty to use the recommended value):', suggested);
1155
+ if (slug == null) return;
1156
+ const result = await api('/api/public/link', {
1157
+ method: 'POST',
1158
+ headers: { 'Content-Type': 'application/json' },
1159
+ body: JSON.stringify({ slug: slug.trim() || undefined, enabled: true }),
1160
+ });
1161
+ window.alert('Public link ready:
1162
+ ' + (result.public_url || '(missing public_url)'));
1163
+ await refreshAll();
1164
+ });
1165
+ document.getElementById('contactCardBtn').addEventListener('click', async function () {
1166
+ const intro = window.prompt('Optional intro note for this contact card:', '');
1167
+ if (intro == null) return;
1168
+ const result = await api('/api/public/contact-card', {
1169
+ method: 'POST',
1170
+ headers: { 'Content-Type': 'application/json' },
1171
+ body: JSON.stringify({
1172
+ target_did: state.overview && state.overview.did ? state.overview.did : undefined,
1173
+ intro_note: intro.trim() || undefined,
1174
+ }),
1175
+ });
1176
+ window.alert('Contact card ready:
1177
+ ' + (result.share_url || '(missing share_url)'));
1178
+ });
671
1179
 
672
1180
  async function init() {
673
1181
  await loadProfiles();
@@ -691,6 +1199,29 @@ function resolvePath(p) {
691
1199
  if (!p || !p.startsWith("~")) return p;
692
1200
  return path.join(process.env.HOME || process.env.USERPROFILE || "", p.slice(1));
693
1201
  }
1202
+ function findOpenClawInstallScript() {
1203
+ const explicit = process.env.PINGAGENT_OPENCLAW_INSTALL_BIN?.trim();
1204
+ if (explicit) return { kind: "script", cmd: process.execPath, args: [path.resolve(explicit)] };
1205
+ const repoScript = path.resolve(process.cwd(), "packages", "openclaw-install", "install.mjs");
1206
+ if (fs.existsSync(repoScript)) return { kind: "script", cmd: process.execPath, args: [repoScript] };
1207
+ return null;
1208
+ }
1209
+ function runOpenClawInstall(args) {
1210
+ const resolved = findOpenClawInstallScript();
1211
+ if (!resolved) {
1212
+ return { ok: false, stdout: "", stderr: "OpenClaw installer script not found locally. Set PINGAGENT_OPENCLAW_INSTALL_BIN." };
1213
+ }
1214
+ const result = spawnSync(resolved.cmd, [...resolved.args, ...args], {
1215
+ encoding: "utf-8",
1216
+ env: process.env
1217
+ });
1218
+ return {
1219
+ ok: result.status === 0,
1220
+ stdout: String(result.stdout ?? ""),
1221
+ stderr: String(result.stderr ?? ""),
1222
+ status: result.status ?? 1
1223
+ };
1224
+ }
694
1225
  function listProfiles(rootDir) {
695
1226
  const root = resolvePath(rootDir);
696
1227
  const profiles = [];
@@ -749,6 +1280,30 @@ function listProfiles(rootDir) {
749
1280
  return profiles;
750
1281
  }
751
1282
  var DEFAULT_SERVER_URL = "https://pingagent.chat";
1283
+ var OFFICIAL_HOSTED_ORIGIN = new URL(DEFAULT_SERVER_URL).origin;
1284
+ var autoPublicLinkAttempts = /* @__PURE__ */ new Set();
1285
+ function normalizeOrigin(input) {
1286
+ try {
1287
+ return new URL(String(input ?? "")).origin;
1288
+ } catch {
1289
+ return null;
1290
+ }
1291
+ }
1292
+ function isOfficialHostedServer(serverUrl) {
1293
+ return normalizeOrigin(serverUrl) === OFFICIAL_HOSTED_ORIGIN;
1294
+ }
1295
+ async function maybeEnsureHostedPublicLink(ctx) {
1296
+ if (!isOfficialHostedServer(ctx.serverUrl)) return;
1297
+ const key = `${ctx.identityPath}:${normalizeOrigin(ctx.serverUrl)}`;
1298
+ if (autoPublicLinkAttempts.has(key)) return;
1299
+ autoPublicLinkAttempts.add(key);
1300
+ try {
1301
+ const current = await ctx.client.getPublicSelf().catch(() => ({ ok: false }));
1302
+ if (current.ok && current.data?.public_slug) return;
1303
+ await ctx.client.createPublicLink({ enabled: true }).catch(() => ({ ok: false }));
1304
+ } catch {
1305
+ }
1306
+ }
752
1307
  async function getContextForProfile(profile, defaultServerUrl) {
753
1308
  const identity = loadIdentity(profile.identityPath);
754
1309
  const serverUrl = identity.serverUrl ?? defaultServerUrl ?? DEFAULT_SERVER_URL;
@@ -954,11 +1509,14 @@ function describeHostedTier(tier) {
954
1509
  }
955
1510
  async function buildRuntimeOverviewPayload(ctx) {
956
1511
  const client = ctx.client;
1512
+ await maybeEnsureHostedPublicLink(ctx);
957
1513
  await client.listConversations({ type: "dm" });
958
1514
  const sessionManager = client.getSessionManager();
1515
+ const sessionSummaryManager = client.getSessionSummaryManager();
959
1516
  const taskManager = client.getTaskThreadManager();
1517
+ const taskHandoffManager = client.getTaskHandoffManager();
960
1518
  const historyManager = client.getHistoryManager();
961
- if (!sessionManager || !taskManager || !historyManager) {
1519
+ if (!sessionManager || !sessionSummaryManager || !taskManager || !taskHandoffManager || !historyManager) {
962
1520
  throw new Error("Runtime overview requires a writable local store");
963
1521
  }
964
1522
  const sessions = sessionManager.listRecentSessions(24);
@@ -981,6 +1539,10 @@ async function buildRuntimeOverviewPayload(ctx) {
981
1539
  const runtimeMode = getRuntimeMode();
982
1540
  const subRes = await client.getSubscription().catch(() => ({ ok: false }));
983
1541
  const subscription = subRes.ok && subRes.data ? subRes.data : null;
1542
+ const publicSelfRes = await client.getPublicSelf().catch(() => ({ ok: false }));
1543
+ const publicSelf = publicSelfRes.ok && publicSelfRes.data ? publicSelfRes.data : null;
1544
+ const profileRes = await client.getProfile().catch(() => ({ ok: false }));
1545
+ const profile = profileRes.ok && profileRes.data ? profileRes.data : null;
984
1546
  const recommendationState = syncTrustRecommendations(ctx.storePath, {
985
1547
  policyDoc: policy,
986
1548
  sessions,
@@ -990,6 +1552,12 @@ async function buildRuntimeOverviewPayload(ctx) {
990
1552
  limit: 12
991
1553
  });
992
1554
  const unreadTotal = sessions.reduce((sum, session) => sum + session.unread_count, 0);
1555
+ const sessionBindings = readSessionBindings();
1556
+ const sessionBindingAlerts = readSessionBindingAlerts();
1557
+ const ingressRuntime = readIngressRuntimeStatus();
1558
+ const activeWorkSession = readCurrentActiveSessionKey();
1559
+ const bindingByConversation = new Map(sessionBindings.map((row) => [row.conversation_id, row.session_key]));
1560
+ const bindingAlertByConversation = new Map(sessionBindingAlerts.map((row) => [row.conversation_id, row]));
993
1561
  const trustCounts = sessions.reduce((acc, session) => {
994
1562
  acc[session.trust_state] = (acc[session.trust_state] ?? 0) + 1;
995
1563
  return acc;
@@ -999,6 +1567,13 @@ async function buildRuntimeOverviewPayload(ctx) {
999
1567
  serverUrl: ctx.serverUrl,
1000
1568
  runtimeMode,
1001
1569
  trustPolicyPath: getTrustPolicyPath(ctx.identityPath),
1570
+ activeWorkSessionFile: getActiveSessionFilePath(),
1571
+ activeWorkSession,
1572
+ ingressRuntime,
1573
+ sessionMapPath: getSessionMapFilePath(),
1574
+ sessionBindingAlertsPath: getSessionBindingAlertsFilePath(),
1575
+ sessionBindings,
1576
+ sessionBindingAlerts,
1002
1577
  subscription: subscription ? {
1003
1578
  tier: subscription.tier,
1004
1579
  summary: describeHostedTier(subscription.tier),
@@ -1010,6 +1585,8 @@ async function buildRuntimeOverviewPayload(ctx) {
1010
1585
  retention_label: formatRetentionLabel(subscription.limits.store_forward_ttl_ms),
1011
1586
  audit_export_allowed: !!subscription.limits.audit_export_allowed
1012
1587
  } : null,
1588
+ publicSelf,
1589
+ profile,
1013
1590
  policyDefaults: {
1014
1591
  contact: policy.contact_policy.enabled ? policy.contact_policy.default_action : "disabled",
1015
1592
  task: policy.task_policy.enabled ? policy.task_policy.default_action : "disabled"
@@ -1021,19 +1598,31 @@ async function buildRuntimeOverviewPayload(ctx) {
1021
1598
  recommendationSummary: recommendationState.summary,
1022
1599
  sessions: sessions.map((session) => ({
1023
1600
  ...session,
1601
+ session_summary: sessionSummaryManager.get(session.session_key),
1602
+ mapped_work_session: session.conversation_id ? bindingByConversation.get(session.conversation_id) ?? null : null,
1603
+ binding_alert: session.conversation_id ? bindingAlertByConversation.get(session.conversation_id) ?? null : null,
1604
+ is_active_work_session: session.session_key === activeWorkSession,
1024
1605
  latest_messages: session.conversation_id ? historyManager.listRecent(session.conversation_id, 3) : []
1025
1606
  })),
1026
- tasks: refreshedTasks,
1607
+ tasks: refreshedTasks.map((task) => ({
1608
+ ...task,
1609
+ handoff: taskHandoffManager.get(task.task_id)
1610
+ })),
1027
1611
  auditSummary,
1028
- recommendations: recommendationState.recommendations
1612
+ recommendations: recommendationState.recommendations.map((item) => ({
1613
+ ...item,
1614
+ primary_action_label: getTrustRecommendationActionLabel(item)
1615
+ }))
1029
1616
  };
1030
1617
  }
1031
1618
  async function buildSessionOverviewPayload(ctx, sessionKey) {
1032
1619
  const client = ctx.client;
1033
1620
  const sessionManager = client.getSessionManager();
1621
+ const sessionSummaryManager = client.getSessionSummaryManager();
1034
1622
  const taskManager = client.getTaskThreadManager();
1623
+ const taskHandoffManager = client.getTaskHandoffManager();
1035
1624
  const historyManager = client.getHistoryManager();
1036
- if (!sessionManager || !taskManager || !historyManager) {
1625
+ if (!sessionManager || !sessionSummaryManager || !taskManager || !taskHandoffManager || !historyManager) {
1037
1626
  throw new Error("Session overview requires a writable local store");
1038
1627
  }
1039
1628
  const session = sessionKey ? sessionManager.get(sessionKey) : sessionManager.getActiveSession() ?? sessionManager.listRecentSessions(1)[0] ?? null;
@@ -1054,6 +1643,11 @@ async function buildSessionOverviewPayload(ctx, sessionKey) {
1054
1643
  auditStore.close();
1055
1644
  }
1056
1645
  const policy = readTrustPolicyDoc(ctx.identityPath);
1646
+ const bindings = readSessionBindings();
1647
+ const bindingAlerts = readSessionBindingAlerts();
1648
+ const binding = session.conversation_id ? bindings.find((row) => row.conversation_id === session.conversation_id) ?? null : null;
1649
+ const bindingAlert = session.conversation_id ? bindingAlerts.find((row) => row.conversation_id === session.conversation_id) ?? null : null;
1650
+ const activeWorkSession = readCurrentActiveSessionKey();
1057
1651
  const recommendationState = syncTrustRecommendations(ctx.storePath, {
1058
1652
  policyDoc: policy,
1059
1653
  sessions: sessionManager.listRecentSessions(50),
@@ -1064,11 +1658,25 @@ async function buildSessionOverviewPayload(ctx, sessionKey) {
1064
1658
  });
1065
1659
  return {
1066
1660
  session,
1661
+ sessionSummary: sessionSummaryManager.get(session.session_key),
1662
+ ingressRuntime: readIngressRuntimeStatus(),
1663
+ binding,
1664
+ bindingAlert,
1665
+ activeWorkSession,
1666
+ activeWorkSessionFile: getActiveSessionFilePath(),
1667
+ sessionMapPath: getSessionMapFilePath(),
1668
+ sessionBindingAlertsPath: getSessionBindingAlertsFilePath(),
1067
1669
  policyExplain: buildPolicyDecisionShape(ctx.identityPath, session.remote_did, { runtimeMode: getRuntimeMode() }),
1068
- tasks,
1670
+ tasks: tasks.map((task) => ({
1671
+ ...task,
1672
+ handoff: taskHandoffManager.get(task.task_id)
1673
+ })),
1069
1674
  messages,
1070
1675
  auditEvents,
1071
- recommendations: recommendationState.recommendations.filter((item) => item.remote_did === session.remote_did)
1676
+ recommendations: recommendationState.recommendations.filter((item) => item.remote_did === session.remote_did).map((item) => ({
1677
+ ...item,
1678
+ primary_action_label: getTrustRecommendationActionLabel(item)
1679
+ }))
1072
1680
  };
1073
1681
  }
1074
1682
  async function handleApi(pathname, req, ctx) {
@@ -1081,14 +1689,158 @@ async function handleApi(pathname, req, ctx) {
1081
1689
  return { did: myDid, serverUrl };
1082
1690
  }
1083
1691
  if (parts[0] === "runtime") {
1692
+ if (parts[1] === "session-summary") {
1693
+ const sessionManager = client.getSessionManager();
1694
+ const sessionSummaryManager = client.getSessionSummaryManager();
1695
+ if (!sessionManager || !sessionSummaryManager) throw new Error("Session summary requires a writable local store");
1696
+ const url = new URL(req.url || "", "http://x");
1697
+ const body = req.method === "POST" ? await readBody(req) : null;
1698
+ const sessionKey = String(
1699
+ body?.session_key ?? url.searchParams.get("session_key") ?? ""
1700
+ ).trim();
1701
+ const conversationId = String(
1702
+ body?.conversation_id ?? url.searchParams.get("conversation_id") ?? ""
1703
+ ).trim();
1704
+ const remoteDid = String(
1705
+ body?.remote_did ?? url.searchParams.get("remote_did") ?? ""
1706
+ ).trim();
1707
+ const session = sessionKey ? sessionManager.get(sessionKey) : conversationId ? sessionManager.getByConversationId(conversationId) : remoteDid ? sessionManager.listRecentSessions(100).find((item) => item.remote_did === remoteDid) ?? null : sessionManager.getActiveSession() ?? sessionManager.listRecentSessions(1)[0] ?? null;
1708
+ if (!session) throw new Error("No session available");
1709
+ if (req.method === "GET") {
1710
+ return { session, summary: sessionSummaryManager.get(session.session_key) };
1711
+ }
1712
+ const summary = sessionSummaryManager.upsert({
1713
+ session_key: session.session_key,
1714
+ objective: typeof body?.objective === "string" ? body.objective : void 0,
1715
+ context: typeof body?.context === "string" ? body.context : void 0,
1716
+ constraints: typeof body?.constraints === "string" ? body.constraints : void 0,
1717
+ decisions: typeof body?.decisions === "string" ? body.decisions : void 0,
1718
+ open_questions: typeof body?.open_questions === "string" ? body.open_questions : void 0,
1719
+ next_action: typeof body?.next_action === "string" ? body.next_action : void 0,
1720
+ handoff_ready_text: typeof body?.handoff_ready_text === "string" ? body.handoff_ready_text : void 0
1721
+ });
1722
+ return { ok: true, session, summary };
1723
+ }
1084
1724
  if (!parts[1] || parts[1] === "overview") {
1085
1725
  return buildRuntimeOverviewPayload(ctx);
1086
1726
  }
1727
+ if (parts[1] === "receive-mode" && req.method === "GET") {
1728
+ return {
1729
+ runtimeStatusPath: path.resolve(process.env.IM_INGRESS_RUNTIME_STATUS_FILE || ""),
1730
+ status: readIngressRuntimeStatus()
1731
+ };
1732
+ }
1733
+ if (parts[1] === "demo" && req.method === "POST") {
1734
+ const body = await readBody(req);
1735
+ const preset = typeof body?.preset === "string" ? body.preset.trim().toLowerCase() : "";
1736
+ const presetMessages = {
1737
+ hello: "Hello",
1738
+ delegate: "Please show me how task delegation works in PingAgent.",
1739
+ trust: "Show me how trust decisions and recommendations work."
1740
+ };
1741
+ const resolved = await client.resolveAlias("pingagent/demo");
1742
+ if (!resolved.ok || !resolved.data?.did) {
1743
+ throw new Error(resolved.error?.message ?? "Failed to resolve demo agent");
1744
+ }
1745
+ const convo = await client.openConversation(resolved.data.did);
1746
+ if (!convo.ok || !convo.data?.conversation_id) {
1747
+ throw new Error(convo.error?.message ?? "Failed to open demo conversation");
1748
+ }
1749
+ const message = typeof body?.message === "string" && body.message.trim() ? body.message.trim() : presetMessages[preset] ?? "";
1750
+ if (!message) {
1751
+ return {
1752
+ ok: true,
1753
+ did: resolved.data.did,
1754
+ conversation_id: convo.data.conversation_id,
1755
+ preset: preset || null,
1756
+ sent: false
1757
+ };
1758
+ }
1759
+ const sendRes = await client.sendMessage(convo.data.conversation_id, SCHEMA_TEXT, { text: message });
1760
+ if (!sendRes.ok) {
1761
+ throw new Error(sendRes.error?.message ?? "Failed to send demo message");
1762
+ }
1763
+ return {
1764
+ ok: true,
1765
+ did: resolved.data.did,
1766
+ conversation_id: convo.data.conversation_id,
1767
+ preset: preset || null,
1768
+ sent: true,
1769
+ message_id: sendRes.data?.message_id ?? null
1770
+ };
1771
+ }
1772
+ if (parts[1] === "openclaw" && parts[2] === "fix-hooks" && req.method === "POST") {
1773
+ const result = runOpenClawInstall(["fix-hooks"]);
1774
+ return {
1775
+ ok: result.ok,
1776
+ stdout: result.stdout,
1777
+ stderr: result.stderr,
1778
+ status: result.status,
1779
+ receiveMode: readIngressRuntimeStatus()
1780
+ };
1781
+ }
1087
1782
  if (parts[1] === "session") {
1088
1783
  const url = new URL(req.url || "", "http://x");
1089
1784
  const sessionKey = url.searchParams.get("session_key");
1090
1785
  return buildSessionOverviewPayload(ctx, sessionKey);
1091
1786
  }
1787
+ if (parts[1] === "session-bindings") {
1788
+ if ((!parts[2] || parts[2] === "list") && req.method === "GET") {
1789
+ const url = new URL(req.url || "", "http://x");
1790
+ const conversationId = url.searchParams.get("conversation_id");
1791
+ const rows = conversationId ? readSessionBindings().filter((row) => row.conversation_id === conversationId) : readSessionBindings();
1792
+ return {
1793
+ activeWorkSessionFile: getActiveSessionFilePath(),
1794
+ activeWorkSession: readCurrentActiveSessionKey(),
1795
+ sessionMapPath: getSessionMapFilePath(),
1796
+ sessionBindingAlertsPath: getSessionBindingAlertsFilePath(),
1797
+ alerts: conversationId ? readSessionBindingAlerts().filter((row) => row.conversation_id === conversationId) : readSessionBindingAlerts(),
1798
+ bindings: rows
1799
+ };
1800
+ }
1801
+ if (parts[2] === "bind-current" && req.method === "POST") {
1802
+ const body = await readBody(req);
1803
+ const conversationId = String(body?.conversation_id ?? "").trim();
1804
+ if (!conversationId) throw new Error("conversation_id is required");
1805
+ const current = readCurrentActiveSessionKey();
1806
+ if (!current) {
1807
+ throw new Error(`No active OpenClaw chat session found in ${getActiveSessionFilePath()}`);
1808
+ }
1809
+ const result = setSessionBinding(conversationId, current);
1810
+ return {
1811
+ ok: true,
1812
+ activeWorkSessionFile: getActiveSessionFilePath(),
1813
+ activeWorkSession: current,
1814
+ sessionMapPath: result.path,
1815
+ binding: result.binding
1816
+ };
1817
+ }
1818
+ if (parts[2] === "set" && req.method === "POST") {
1819
+ const body = await readBody(req);
1820
+ const conversationId = String(body?.conversation_id ?? "").trim();
1821
+ const sessionKey = String(body?.session_key ?? "").trim();
1822
+ if (!conversationId || !sessionKey) throw new Error("conversation_id and session_key are required");
1823
+ const result = setSessionBinding(conversationId, sessionKey);
1824
+ return {
1825
+ ok: true,
1826
+ activeWorkSessionFile: getActiveSessionFilePath(),
1827
+ activeWorkSession: readCurrentActiveSessionKey(),
1828
+ sessionMapPath: result.path,
1829
+ binding: result.binding
1830
+ };
1831
+ }
1832
+ if (parts[2] === "clear" && req.method === "POST") {
1833
+ const body = await readBody(req);
1834
+ const conversationId = String(body?.conversation_id ?? "").trim();
1835
+ if (!conversationId) throw new Error("conversation_id is required");
1836
+ const result = removeSessionBinding(conversationId);
1837
+ return {
1838
+ ok: true,
1839
+ removed: result.removed,
1840
+ sessionMapPath: result.path
1841
+ };
1842
+ }
1843
+ }
1092
1844
  if (parts[1] === "policy") {
1093
1845
  const policyPath = getTrustPolicyPath(ctx.identityPath);
1094
1846
  const runtimeMode = getRuntimeMode();
@@ -1118,7 +1870,10 @@ async function handleApi(pathname, req, ctx) {
1118
1870
  auditSummary: summarizeTrustPolicyAudit(auditEvents),
1119
1871
  auditEvents,
1120
1872
  recommendationSummary: recommendationState.summary,
1121
- recommendations: recommendationState.recommendations
1873
+ recommendations: recommendationState.recommendations.map((item) => ({
1874
+ ...item,
1875
+ primary_action_label: getTrustRecommendationActionLabel(item)
1876
+ }))
1122
1877
  };
1123
1878
  } finally {
1124
1879
  auditStore.close();
@@ -1316,6 +2071,141 @@ async function handleApi(pathname, req, ctx) {
1316
2071
  }
1317
2072
  };
1318
2073
  }
2074
+ if (parts[2] === "recommendations" && parts[3] === "apply-session" && req.method === "POST") {
2075
+ const body = await readBody(req);
2076
+ const sessionKey = typeof body?.session_key === "string" ? body.session_key : ctx.client.getSessionManager()?.getActiveSession()?.session_key ?? "";
2077
+ if (!sessionKey) throw new Error("session_key is required");
2078
+ const session = ctx.client.getSessionManager()?.get(sessionKey);
2079
+ if (!session?.remote_did) throw new Error("No active session recommendation target");
2080
+ const doc = readTrustPolicyDoc(ctx.identityPath);
2081
+ const sharedStore = new LocalStore(ctx.storePath);
2082
+ try {
2083
+ const auditManager = new TrustPolicyAuditManager(sharedStore);
2084
+ const recommendationManager = new TrustRecommendationManager(sharedStore);
2085
+ const auditEvents = auditManager.listRecent(200);
2086
+ recommendationManager.sync({
2087
+ policyDoc: doc,
2088
+ sessions: sessionManager.listRecentSessions(100),
2089
+ tasks: taskManager.listRecent(100),
2090
+ auditEvents,
2091
+ runtimeMode,
2092
+ limit: 50
2093
+ });
2094
+ const recommendation = recommendationManager.list({ remoteDid: session.remote_did, status: "open", limit: 1 })[0];
2095
+ if (!recommendation) throw new Error("No open recommendation for this session");
2096
+ const nextDoc = upsertTrustPolicyRecommendation(doc, recommendation);
2097
+ const savedPath = writeTrustPolicyDoc(ctx.identityPath, nextDoc);
2098
+ const stored = recommendationManager.apply(recommendation.id) ?? recommendation;
2099
+ auditManager.record({
2100
+ event_type: "recommendation_applied",
2101
+ policy_scope: recommendation.policy,
2102
+ remote_did: recommendation.remote_did,
2103
+ action: String(recommendation.action),
2104
+ outcome: "recommendation_applied",
2105
+ explanation: recommendation.reason,
2106
+ matched_rule: recommendation.match,
2107
+ detail: { recommendation_id: recommendation.id, session_key: sessionKey }
2108
+ });
2109
+ return { ok: true, path: savedPath, recommendation: stored, doc: nextDoc };
2110
+ } finally {
2111
+ sharedStore.close();
2112
+ }
2113
+ }
2114
+ if (parts[2] === "recommendations" && parts[3] === "dismiss-session" && req.method === "POST") {
2115
+ const body = await readBody(req);
2116
+ const sessionKey = typeof body?.session_key === "string" ? body.session_key : ctx.client.getSessionManager()?.getActiveSession()?.session_key ?? "";
2117
+ if (!sessionKey) throw new Error("session_key is required");
2118
+ const session = ctx.client.getSessionManager()?.get(sessionKey);
2119
+ if (!session?.remote_did) throw new Error("No active session recommendation target");
2120
+ const sharedStore = new LocalStore(ctx.storePath);
2121
+ try {
2122
+ const recommendationManager = new TrustRecommendationManager(sharedStore);
2123
+ const recommendation = recommendationManager.list({ remoteDid: session.remote_did, status: "open", limit: 1 })[0];
2124
+ if (!recommendation) throw new Error("No open recommendation for this session");
2125
+ const stored = recommendationManager.dismiss(recommendation.id) ?? recommendation;
2126
+ new TrustPolicyAuditManager(sharedStore).record({
2127
+ event_type: "recommendation_dismissed",
2128
+ policy_scope: recommendation.policy,
2129
+ remote_did: recommendation.remote_did,
2130
+ action: String(recommendation.action),
2131
+ outcome: "recommendation_dismissed",
2132
+ explanation: recommendation.reason,
2133
+ matched_rule: recommendation.match,
2134
+ detail: { recommendation_id: recommendation.id, session_key: sessionKey }
2135
+ });
2136
+ return { ok: true, recommendation: stored };
2137
+ } finally {
2138
+ sharedStore.close();
2139
+ }
2140
+ }
2141
+ if (parts[2] === "recommendations" && parts[3] === "reopen-session" && req.method === "POST") {
2142
+ const body = await readBody(req);
2143
+ const sessionKey = typeof body?.session_key === "string" ? body.session_key : ctx.client.getSessionManager()?.getActiveSession()?.session_key ?? "";
2144
+ if (!sessionKey) throw new Error("session_key is required");
2145
+ const session = ctx.client.getSessionManager()?.get(sessionKey);
2146
+ if (!session?.remote_did) throw new Error("No active session recommendation target");
2147
+ const sharedStore = new LocalStore(ctx.storePath);
2148
+ try {
2149
+ const recommendationManager = new TrustRecommendationManager(sharedStore);
2150
+ const recommendation = recommendationManager.list({ remoteDid: session.remote_did, status: ["dismissed", "superseded"], limit: 1 })[0];
2151
+ if (!recommendation) throw new Error("No dismissed or superseded recommendation for this session");
2152
+ const stored = recommendationManager.reopen(recommendation.id) ?? recommendation;
2153
+ new TrustPolicyAuditManager(sharedStore).record({
2154
+ event_type: "recommendation_reopened",
2155
+ policy_scope: recommendation.policy,
2156
+ remote_did: recommendation.remote_did,
2157
+ action: String(recommendation.action),
2158
+ outcome: "recommendation_reopened",
2159
+ explanation: recommendation.reason,
2160
+ matched_rule: recommendation.match,
2161
+ detail: { recommendation_id: recommendation.id, session_key: sessionKey }
2162
+ });
2163
+ return { ok: true, recommendation: stored };
2164
+ } finally {
2165
+ sharedStore.close();
2166
+ }
2167
+ }
2168
+ }
2169
+ }
2170
+ if (parts[0] === "public") {
2171
+ if ((!parts[1] || parts[1] === "self") && req.method === "GET") {
2172
+ await maybeEnsureHostedPublicLink(ctx);
2173
+ const res = await client.getPublicSelf();
2174
+ if (!res.ok) throw new Error(res.error?.message ?? "Failed to get public link state");
2175
+ return res.data;
2176
+ }
2177
+ if (parts[1] === "link" && req.method === "POST") {
2178
+ const body = await readBody(req);
2179
+ const res = await client.createPublicLink({
2180
+ slug: typeof body?.slug === "string" ? body.slug : void 0,
2181
+ enabled: typeof body?.enabled === "boolean" ? body.enabled : void 0
2182
+ });
2183
+ if (!res.ok) throw new Error(res.error?.message ?? "Failed to create public link");
2184
+ return res.data;
2185
+ }
2186
+ if (parts[1] === "contact-card" && req.method === "POST") {
2187
+ const body = await readBody(req);
2188
+ const res = await client.createContactCard({
2189
+ target_did: typeof body?.target_did === "string" ? body.target_did : void 0,
2190
+ referrer_did: typeof body?.referrer_did === "string" ? body.referrer_did : void 0,
2191
+ intro_note: typeof body?.intro_note === "string" ? body.intro_note : void 0,
2192
+ message_template: typeof body?.message_template === "string" ? body.message_template : void 0
2193
+ });
2194
+ if (!res.ok) throw new Error(res.error?.message ?? "Failed to create contact card");
2195
+ return res.data;
2196
+ }
2197
+ if (parts[1] === "task-share" && req.method === "POST") {
2198
+ const body = await readBody(req);
2199
+ const res = await client.createTaskShare({
2200
+ task_id: typeof body?.task_id === "string" ? body.task_id : void 0,
2201
+ title: typeof body?.title === "string" ? body.title : void 0,
2202
+ status: typeof body?.status === "string" ? body.status : void 0,
2203
+ summary: String(body?.summary ?? "").trim(),
2204
+ conversation_id: typeof body?.conversation_id === "string" ? body.conversation_id : void 0,
2205
+ redacted_metadata: body?.redacted_metadata && typeof body.redacted_metadata === "object" ? body.redacted_metadata : void 0
2206
+ });
2207
+ if (!res.ok) throw new Error(res.error?.message ?? "Failed to create task share");
2208
+ return res.data;
1319
2209
  }
1320
2210
  }
1321
2211
  if (parts[0] === "profile") {
@@ -1327,11 +2217,12 @@ async function handleApi(pathname, req, ctx) {
1327
2217
  if (req.method === "POST") {
1328
2218
  const body = await readBody(req);
1329
2219
  const profile = {};
1330
- if (body?.display_name != null) profile.display_name = body.display_name;
1331
- if (body?.bio != null) profile.bio = body.bio;
1332
- if (Array.isArray(body?.capabilities)) profile.capabilities = body.capabilities;
2220
+ if (typeof body?.display_name === "string") profile.display_name = body.display_name;
2221
+ if (typeof body?.bio === "string") profile.bio = body.bio;
2222
+ if (Array.isArray(body?.capabilities)) profile.capabilities = body.capabilities.map((value) => String(value ?? "").trim()).filter(Boolean);
1333
2223
  else if (typeof body?.capabilities === "string") profile.capabilities = body.capabilities.split(",").map((s) => s.trim()).filter(Boolean);
1334
- if (Array.isArray(body?.tags)) profile.tags = body.tags;
2224
+ if (body?.capability_card && typeof body.capability_card === "object") profile.capability_card = body.capability_card;
2225
+ if (Array.isArray(body?.tags)) profile.tags = body.tags.map((value) => String(value ?? "").trim()).filter(Boolean);
1335
2226
  else if (typeof body?.tags === "string") profile.tags = body.tags.split(",").map((s) => s.trim()).filter(Boolean);
1336
2227
  if (typeof body?.discoverable === "boolean") profile.discoverable = body.discoverable;
1337
2228
  const res = await client.updateProfile(profile);