@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.
- package/bin/pingagent.js +1702 -0
- package/dist/chunk-2Y6YRKTO.js +3100 -0
- package/dist/chunk-6OA4F66H.js +3193 -0
- package/dist/chunk-BLHMTUID.js +3610 -0
- package/dist/chunk-HCQ7CEDE.js +3556 -0
- package/dist/chunk-R3D7LOGB.js +3553 -0
- package/dist/chunk-RMIRCSQ6.js +3042 -0
- package/dist/chunk-SMDQYV7Z.js +3173 -0
- package/dist/chunk-YBNFPOKO.js +3553 -0
- package/dist/index.d.ts +265 -8
- package/dist/index.js +47 -3
- package/dist/web-server.js +906 -15
- package/package.json +3 -3
package/dist/web-server.js
CHANGED
|
@@ -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-
|
|
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, ''');
|
|
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
|
|
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>' +
|
|
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) + '">
|
|
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
|
|
1331
|
-
if (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 (
|
|
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);
|