@pingagent/sdk 0.1.10 → 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.
@@ -11,9 +11,11 @@ import {
11
11
  getActiveSessionFilePath,
12
12
  getSessionBindingAlertsFilePath,
13
13
  getSessionMapFilePath,
14
+ getTrustRecommendationActionLabel,
14
15
  loadIdentity,
15
16
  normalizeTrustPolicyDoc,
16
17
  readCurrentActiveSessionKey,
18
+ readIngressRuntimeStatus,
17
19
  readSessionBindingAlerts,
18
20
  readSessionBindings,
19
21
  removeSessionBinding,
@@ -21,12 +23,13 @@ import {
21
23
  summarizeTrustPolicyAudit,
22
24
  updateStoredToken,
23
25
  upsertTrustPolicyRecommendation
24
- } from "./chunk-2Y6YRKTO.js";
26
+ } from "./chunk-BLHMTUID.js";
25
27
 
26
28
  // src/web-server.ts
27
29
  import * as fs from "fs";
28
30
  import * as http from "http";
29
31
  import * as path from "path";
32
+ import { spawnSync } from "child_process";
30
33
  import { SCHEMA_TEXT } from "@pingagent/schemas";
31
34
 
32
35
  // src/host-panel-html.ts
@@ -75,6 +78,30 @@ function getHostPanelHtml() {
75
78
  padding: 16px;
76
79
  box-shadow: 0 14px 40px rgba(0, 0, 0, 0.22);
77
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; }
78
105
  .card h2, .card h3 { margin: 0 0 12px; font-size: 16px; }
79
106
  .stats .value { font-size: 28px; font-weight: 700; margin-top: 6px; }
80
107
  .runtime-layout { grid-template-columns: minmax(260px, 340px) minmax(0, 1fr); align-items: start; }
@@ -155,11 +182,17 @@ function getHostPanelHtml() {
155
182
  </div>
156
183
  <div class="header-actions">
157
184
  <span class="pill" id="runtimeModePill">runtime_mode=bridge</span>
185
+ <span class="pill" id="receiveModePill">receive_mode=webhook</span>
158
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>
159
191
  </div>
160
192
  </div>
161
193
 
162
194
  <section id="runtimePanel" class="panel active">
195
+ <div class="card" id="activationCard" style="margin-bottom:16px"></div>
163
196
  <div class="grid stats" id="statsGrid"></div>
164
197
  <div class="grid runtime-layout" style="margin-top:16px">
165
198
  <div class="card">
@@ -253,6 +286,40 @@ function getHostPanelHtml() {
253
286
  <div class="audit-list" id="policyAuditList"></div>
254
287
  </div>
255
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>
256
323
  </section>
257
324
  </main>
258
325
  </div>
@@ -277,6 +344,13 @@ function getHostPanelHtml() {
277
344
  .replace(/'/g, '&#39;');
278
345
  }
279
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
+
280
354
  function fmtTs(value) {
281
355
  if (!value) return '-';
282
356
  try { return new Date(value).toLocaleString(); } catch { return String(value); }
@@ -338,20 +412,117 @@ function getHostPanelHtml() {
338
412
  const profileLabel = state.selectedProfile ? 'profile=' + state.selectedProfile : 'Select profile';
339
413
  const title = overview ? ('Host Panel \xB7 ' + overview.did) : 'PingAgent Host Panel';
340
414
  const tier = overview && overview.subscription ? overview.subscription.tier : null;
415
+ const receiveMode = overview && overview.ingressRuntime ? overview.ingressRuntime.receive_mode : 'webhook';
341
416
  document.getElementById('headerTitle').textContent = title;
342
417
  document.getElementById('headerSubtitle').textContent = overview
343
418
  ? (profileLabel + ' \xB7 ' + overview.serverUrl + (tier ? (' \xB7 tier=' + tier) : '') + ' \xB7 sessions=' + overview.sessionsTotal + ' \xB7 unread=' + overview.unreadTotal)
344
419
  : profileLabel;
345
420
  document.getElementById('runtimeModePill').textContent = overview ? ('runtime_mode=' + overview.runtimeMode) : 'runtime_mode=\u2026';
421
+ document.getElementById('receiveModePill').textContent = 'receive_mode=' + receiveMode;
346
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>';
347
495
  }
348
496
 
349
497
  function renderOverview() {
350
498
  const overview = state.overview;
351
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>';
352
522
  const subscription = overview.subscription || null;
353
523
  const stats = [
354
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' },
355
526
  { label: 'Relay', value: subscription ? (subscription.usage.relay_today + '/' + subscription.usage.relay_limit) : '-', sub: subscription ? ('retention=' + subscription.retention_label) : 'daily relay usage' },
356
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' },
357
528
  { label: 'Sessions', value: overview.sessionsTotal, sub: JSON.stringify(overview.trustCounts || {}) },
@@ -359,6 +530,7 @@ function getHostPanelHtml() {
359
530
  { label: 'Tasks', value: overview.tasksTotal, sub: 'recent local task threads' },
360
531
  { label: 'Audit', value: overview.auditSummary.total_events, sub: 'policy / runtime audit events' },
361
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' },
362
534
  ];
363
535
  document.getElementById('statsGrid').innerHTML = stats.map(function (item) {
364
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>';
@@ -416,6 +588,7 @@ function getHostPanelHtml() {
416
588
  '<div class="muted small">updated=' + esc(fmtTs(task.updated_at)) + '</div>' +
417
589
  (task.result_summary ? '<div style="margin-top:8px">' + esc(task.result_summary) + '</div>' : '') +
418
590
  (task.error_message ? '<div style="margin-top:8px;color:#fca5a5">' + esc(task.error_message) + '</div>' : '') +
591
+ renderHandoffBlock(task.handoff) +
419
592
  '</div>';
420
593
  }).join('')
421
594
  : '<div class="empty">No recent task threads.</div>';
@@ -433,6 +606,56 @@ function getHostPanelHtml() {
433
606
  : '') +
434
607
  '</div>' + document.getElementById('taskList').innerHTML;
435
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
+ }
436
659
  }
437
660
 
438
661
  function renderSession() {
@@ -449,9 +672,12 @@ function getHostPanelHtml() {
449
672
  const messages = Array.isArray(detail.messages) ? detail.messages : [];
450
673
  const auditEvents = Array.isArray(detail.auditEvents) ? detail.auditEvents : [];
451
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;
452
677
  const binding = detail.binding || null;
453
678
  const bindingAlert = detail.bindingAlert || null;
454
679
  const activeWorkSession = detail.activeWorkSession || null;
680
+ const summary = detail.sessionSummary || null;
455
681
 
456
682
  el.innerHTML = '' +
457
683
  '<div class="two-col">' +
@@ -464,10 +690,22 @@ function getHostPanelHtml() {
464
690
  '<div class="muted small">last activity=' + esc(fmtTs(session.last_remote_activity_at || session.updated_at)) + '</div>' +
465
691
  '<div class="muted small" style="margin-top:8px">active_chat_session=' + esc(activeWorkSession || '(none)') + '</div>' +
466
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>' : '')) +
467
696
  (bindingAlert
468
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>'
469
698
  : '') +
470
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
+ : '') +
471
709
  '<button class="action-btn bind-current-btn" data-conversation="' + esc(session.conversation_id || '') + '">Bind Current Chat</button>' +
472
710
  '<button class="danger-btn clear-binding-btn" data-conversation="' + esc(session.conversation_id || '') + '">Clear Binding</button>' +
473
711
  '</div>' +
@@ -477,6 +715,27 @@ function getHostPanelHtml() {
477
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>' +
478
716
  '</div>' +
479
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>' +
480
739
  '<div class="grid two-col" style="margin-top:16px">' +
481
740
  '<div><div class="label">Task Threads</div><div class="task-list" style="margin-top:8px">' +
482
741
  (tasks.length ? tasks.map(function (taskItem) {
@@ -484,15 +743,26 @@ function getHostPanelHtml() {
484
743
  '<div class="muted small">updated=' + esc(fmtTs(taskItem.updated_at)) + '</div>' +
485
744
  (taskItem.result_summary ? '<div style="margin-top:8px">' + esc(taskItem.result_summary) + '</div>' : '') +
486
745
  (taskItem.error_message ? '<div style="margin-top:8px;color:#fca5a5">' + esc(taskItem.error_message) + '</div>' : '') +
746
+ renderHandoffBlock(taskItem.handoff) +
487
747
  '</div>';
488
748
  }).join('') : '<div class="empty">No tasks in this session.</div>') +
489
749
  '</div></div>' +
490
750
  '<div><div class="label">Learned Recommendations</div><div class="recommendation-list" style="margin-top:8px">' +
491
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
+ : '';
492
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>' +
493
762
  '<div class="muted small">current=' + esc(item.current_action) + ' \xB7 confidence=' + esc(item.confidence) + '</div>' +
494
763
  '<div class="muted small">match=' + esc(item.match) + '</div>' +
495
764
  '<div style="margin-top:8px">' + esc(item.reason) + '</div>' +
765
+ '<div class="row-actions">' + actionButton + dismissButton + reopenButton + '</div>' +
496
766
  '</div>';
497
767
  }).join('') : '<div class="empty">No learned recommendation for this session.</div>') +
498
768
  '</div></div>' +
@@ -534,6 +804,60 @@ function getHostPanelHtml() {
534
804
  setTab('runtime');
535
805
  });
536
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
+ }
537
861
  }
538
862
 
539
863
  async function promptBindCurrentChat(conversationId, previousBinding, remoteDid) {
@@ -568,8 +892,23 @@ Previous binding: ' + previous
568
892
  function renderPolicy() {
569
893
  const policy = state.policy;
570
894
  if (!policy) return;
895
+ const profile = state.overview && state.overview.profile ? state.overview.profile : null;
571
896
  document.getElementById('contactDefault').value = policy.doc.contact_policy.default_action;
572
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);
573
912
 
574
913
  const rules = [];
575
914
  policy.doc.contact_policy.rules.forEach(function (rule) {
@@ -610,7 +949,7 @@ Previous binding: ' + previous
610
949
  if (!list.length) return '';
611
950
  return '<div><div class="label" style="margin-bottom:8px">' + esc(status) + '</div>' + list.map(function (item) {
612
951
  const applyButton = status !== 'applied'
613
- ? '<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>'
614
953
  : '';
615
954
  const dismissButton = status === 'open'
616
955
  ? '<button class="danger-btn dismiss-recommendation-btn" data-recommendation-id="' + esc(item.id) + '">Dismiss</button>'
@@ -672,6 +1011,45 @@ Previous binding: ' + previous
672
1011
  : '<div class="empty">No audit events yet.</div>';
673
1012
 
674
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
+ }
675
1053
  }
676
1054
 
677
1055
  function updateRuleActionOptions() {
@@ -758,6 +1136,46 @@ Previous binding: ' + previous
758
1136
  document.getElementById('simulateOutput').textContent = JSON.stringify(result, null, 2);
759
1137
  setTab('policy');
760
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
+ });
761
1179
 
762
1180
  async function init() {
763
1181
  await loadProfiles();
@@ -781,6 +1199,29 @@ function resolvePath(p) {
781
1199
  if (!p || !p.startsWith("~")) return p;
782
1200
  return path.join(process.env.HOME || process.env.USERPROFILE || "", p.slice(1));
783
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
+ }
784
1225
  function listProfiles(rootDir) {
785
1226
  const root = resolvePath(rootDir);
786
1227
  const profiles = [];
@@ -839,6 +1280,30 @@ function listProfiles(rootDir) {
839
1280
  return profiles;
840
1281
  }
841
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
+ }
842
1307
  async function getContextForProfile(profile, defaultServerUrl) {
843
1308
  const identity = loadIdentity(profile.identityPath);
844
1309
  const serverUrl = identity.serverUrl ?? defaultServerUrl ?? DEFAULT_SERVER_URL;
@@ -1044,11 +1509,14 @@ function describeHostedTier(tier) {
1044
1509
  }
1045
1510
  async function buildRuntimeOverviewPayload(ctx) {
1046
1511
  const client = ctx.client;
1512
+ await maybeEnsureHostedPublicLink(ctx);
1047
1513
  await client.listConversations({ type: "dm" });
1048
1514
  const sessionManager = client.getSessionManager();
1515
+ const sessionSummaryManager = client.getSessionSummaryManager();
1049
1516
  const taskManager = client.getTaskThreadManager();
1517
+ const taskHandoffManager = client.getTaskHandoffManager();
1050
1518
  const historyManager = client.getHistoryManager();
1051
- if (!sessionManager || !taskManager || !historyManager) {
1519
+ if (!sessionManager || !sessionSummaryManager || !taskManager || !taskHandoffManager || !historyManager) {
1052
1520
  throw new Error("Runtime overview requires a writable local store");
1053
1521
  }
1054
1522
  const sessions = sessionManager.listRecentSessions(24);
@@ -1071,6 +1539,10 @@ async function buildRuntimeOverviewPayload(ctx) {
1071
1539
  const runtimeMode = getRuntimeMode();
1072
1540
  const subRes = await client.getSubscription().catch(() => ({ ok: false }));
1073
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;
1074
1546
  const recommendationState = syncTrustRecommendations(ctx.storePath, {
1075
1547
  policyDoc: policy,
1076
1548
  sessions,
@@ -1082,6 +1554,7 @@ async function buildRuntimeOverviewPayload(ctx) {
1082
1554
  const unreadTotal = sessions.reduce((sum, session) => sum + session.unread_count, 0);
1083
1555
  const sessionBindings = readSessionBindings();
1084
1556
  const sessionBindingAlerts = readSessionBindingAlerts();
1557
+ const ingressRuntime = readIngressRuntimeStatus();
1085
1558
  const activeWorkSession = readCurrentActiveSessionKey();
1086
1559
  const bindingByConversation = new Map(sessionBindings.map((row) => [row.conversation_id, row.session_key]));
1087
1560
  const bindingAlertByConversation = new Map(sessionBindingAlerts.map((row) => [row.conversation_id, row]));
@@ -1096,6 +1569,7 @@ async function buildRuntimeOverviewPayload(ctx) {
1096
1569
  trustPolicyPath: getTrustPolicyPath(ctx.identityPath),
1097
1570
  activeWorkSessionFile: getActiveSessionFilePath(),
1098
1571
  activeWorkSession,
1572
+ ingressRuntime,
1099
1573
  sessionMapPath: getSessionMapFilePath(),
1100
1574
  sessionBindingAlertsPath: getSessionBindingAlertsFilePath(),
1101
1575
  sessionBindings,
@@ -1111,6 +1585,8 @@ async function buildRuntimeOverviewPayload(ctx) {
1111
1585
  retention_label: formatRetentionLabel(subscription.limits.store_forward_ttl_ms),
1112
1586
  audit_export_allowed: !!subscription.limits.audit_export_allowed
1113
1587
  } : null,
1588
+ publicSelf,
1589
+ profile,
1114
1590
  policyDefaults: {
1115
1591
  contact: policy.contact_policy.enabled ? policy.contact_policy.default_action : "disabled",
1116
1592
  task: policy.task_policy.enabled ? policy.task_policy.default_action : "disabled"
@@ -1122,22 +1598,31 @@ async function buildRuntimeOverviewPayload(ctx) {
1122
1598
  recommendationSummary: recommendationState.summary,
1123
1599
  sessions: sessions.map((session) => ({
1124
1600
  ...session,
1601
+ session_summary: sessionSummaryManager.get(session.session_key),
1125
1602
  mapped_work_session: session.conversation_id ? bindingByConversation.get(session.conversation_id) ?? null : null,
1126
1603
  binding_alert: session.conversation_id ? bindingAlertByConversation.get(session.conversation_id) ?? null : null,
1127
1604
  is_active_work_session: session.session_key === activeWorkSession,
1128
1605
  latest_messages: session.conversation_id ? historyManager.listRecent(session.conversation_id, 3) : []
1129
1606
  })),
1130
- tasks: refreshedTasks,
1607
+ tasks: refreshedTasks.map((task) => ({
1608
+ ...task,
1609
+ handoff: taskHandoffManager.get(task.task_id)
1610
+ })),
1131
1611
  auditSummary,
1132
- recommendations: recommendationState.recommendations
1612
+ recommendations: recommendationState.recommendations.map((item) => ({
1613
+ ...item,
1614
+ primary_action_label: getTrustRecommendationActionLabel(item)
1615
+ }))
1133
1616
  };
1134
1617
  }
1135
1618
  async function buildSessionOverviewPayload(ctx, sessionKey) {
1136
1619
  const client = ctx.client;
1137
1620
  const sessionManager = client.getSessionManager();
1621
+ const sessionSummaryManager = client.getSessionSummaryManager();
1138
1622
  const taskManager = client.getTaskThreadManager();
1623
+ const taskHandoffManager = client.getTaskHandoffManager();
1139
1624
  const historyManager = client.getHistoryManager();
1140
- if (!sessionManager || !taskManager || !historyManager) {
1625
+ if (!sessionManager || !sessionSummaryManager || !taskManager || !taskHandoffManager || !historyManager) {
1141
1626
  throw new Error("Session overview requires a writable local store");
1142
1627
  }
1143
1628
  const session = sessionKey ? sessionManager.get(sessionKey) : sessionManager.getActiveSession() ?? sessionManager.listRecentSessions(1)[0] ?? null;
@@ -1173,6 +1658,8 @@ async function buildSessionOverviewPayload(ctx, sessionKey) {
1173
1658
  });
1174
1659
  return {
1175
1660
  session,
1661
+ sessionSummary: sessionSummaryManager.get(session.session_key),
1662
+ ingressRuntime: readIngressRuntimeStatus(),
1176
1663
  binding,
1177
1664
  bindingAlert,
1178
1665
  activeWorkSession,
@@ -1180,10 +1667,16 @@ async function buildSessionOverviewPayload(ctx, sessionKey) {
1180
1667
  sessionMapPath: getSessionMapFilePath(),
1181
1668
  sessionBindingAlertsPath: getSessionBindingAlertsFilePath(),
1182
1669
  policyExplain: buildPolicyDecisionShape(ctx.identityPath, session.remote_did, { runtimeMode: getRuntimeMode() }),
1183
- tasks,
1670
+ tasks: tasks.map((task) => ({
1671
+ ...task,
1672
+ handoff: taskHandoffManager.get(task.task_id)
1673
+ })),
1184
1674
  messages,
1185
1675
  auditEvents,
1186
- 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
+ }))
1187
1680
  };
1188
1681
  }
1189
1682
  async function handleApi(pathname, req, ctx) {
@@ -1196,9 +1689,96 @@ async function handleApi(pathname, req, ctx) {
1196
1689
  return { did: myDid, serverUrl };
1197
1690
  }
1198
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
+ }
1199
1724
  if (!parts[1] || parts[1] === "overview") {
1200
1725
  return buildRuntimeOverviewPayload(ctx);
1201
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
+ }
1202
1782
  if (parts[1] === "session") {
1203
1783
  const url = new URL(req.url || "", "http://x");
1204
1784
  const sessionKey = url.searchParams.get("session_key");
@@ -1290,7 +1870,10 @@ async function handleApi(pathname, req, ctx) {
1290
1870
  auditSummary: summarizeTrustPolicyAudit(auditEvents),
1291
1871
  auditEvents,
1292
1872
  recommendationSummary: recommendationState.summary,
1293
- recommendations: recommendationState.recommendations
1873
+ recommendations: recommendationState.recommendations.map((item) => ({
1874
+ ...item,
1875
+ primary_action_label: getTrustRecommendationActionLabel(item)
1876
+ }))
1294
1877
  };
1295
1878
  } finally {
1296
1879
  auditStore.close();
@@ -1488,6 +2071,141 @@ async function handleApi(pathname, req, ctx) {
1488
2071
  }
1489
2072
  };
1490
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;
1491
2209
  }
1492
2210
  }
1493
2211
  if (parts[0] === "profile") {
@@ -1499,11 +2217,12 @@ async function handleApi(pathname, req, ctx) {
1499
2217
  if (req.method === "POST") {
1500
2218
  const body = await readBody(req);
1501
2219
  const profile = {};
1502
- if (body?.display_name != null) profile.display_name = body.display_name;
1503
- if (body?.bio != null) profile.bio = body.bio;
1504
- 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);
1505
2223
  else if (typeof body?.capabilities === "string") profile.capabilities = body.capabilities.split(",").map((s) => s.trim()).filter(Boolean);
1506
- 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);
1507
2226
  else if (typeof body?.tags === "string") profile.tags = body.tags.split(",").map((s) => s.trim()).filter(Boolean);
1508
2227
  if (typeof body?.discoverable === "boolean") profile.discoverable = body.discoverable;
1509
2228
  const res = await client.updateProfile(profile);