@pingagent/sdk 0.1.10 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/pingagent.js CHANGED
@@ -4,6 +4,9 @@ import * as fs from 'node:fs';
4
4
  import * as path from 'node:path';
5
5
  import * as os from 'node:os';
6
6
  import * as readline from 'node:readline';
7
+ import { spawnSync } from 'node:child_process';
8
+ import { createRequire } from 'node:module';
9
+ import { fileURLToPath } from 'node:url';
7
10
  import {
8
11
  PingAgentClient,
9
12
  generateIdentity,
@@ -17,10 +20,16 @@ import {
17
20
  HistoryManager,
18
21
  A2AAdapter,
19
22
  SessionManager,
23
+ SessionSummaryManager,
20
24
  TaskThreadManager,
25
+ TaskHandoffManager,
21
26
  TrustPolicyAuditManager,
27
+ TrustRecommendationManager,
28
+ getTrustRecommendationActionLabel,
29
+ formatCapabilityCardSummary,
22
30
  defaultTrustPolicyDoc,
23
31
  normalizeTrustPolicyDoc,
32
+ upsertTrustPolicyRecommendation,
24
33
  getActiveSessionFilePath,
25
34
  getSessionMapFilePath,
26
35
  getSessionBindingAlertsFilePath,
@@ -29,12 +38,18 @@ import {
29
38
  readSessionBindingAlerts,
30
39
  setSessionBinding,
31
40
  removeSessionBinding,
41
+ readIngressRuntimeStatus,
32
42
  } from '../dist/index.js';
33
43
  import { ERROR_HINTS, SCHEMA_TEXT } from '@pingagent/schemas';
34
44
 
45
+ const require = createRequire(import.meta.url);
46
+ const THIS_FILE = fileURLToPath(import.meta.url);
35
47
  const DEFAULT_SERVER = 'https://pingagent.chat';
36
48
  const UPGRADE_URL = 'https://pingagent.chat';
37
49
  const DEFAULT_IDENTITY_PATH = path.join(os.homedir(), '.pingagent', 'identity.json');
50
+ const OFFICIAL_HOSTED_ORIGIN = new URL(DEFAULT_SERVER).origin;
51
+ const hostedPublicLinkAttempts = new Set();
52
+ const SESSION_SUMMARY_FIELDS = ['objective', 'context', 'constraints', 'decisions', 'open_questions', 'next_action', 'handoff_ready_text'];
38
53
 
39
54
  function resolvePath(p) {
40
55
  if (!p) return p;
@@ -42,6 +57,18 @@ function resolvePath(p) {
42
57
  return p;
43
58
  }
44
59
 
60
+ function normalizeOrigin(input) {
61
+ try {
62
+ return new URL(String(input ?? '')).origin;
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+
68
+ function isOfficialHostedServer(serverUrl) {
69
+ return normalizeOrigin(serverUrl) === OFFICIAL_HOSTED_ORIGIN;
70
+ }
71
+
45
72
  function getEffectiveIdentityPath() {
46
73
  const dir = program.opts().identityDir;
47
74
  if (dir) return path.join(resolvePath(dir), 'identity.json');
@@ -108,6 +135,78 @@ function readTrustPolicyDoc(identityPath) {
108
135
  }
109
136
  }
110
137
 
138
+ function writeTrustPolicyDoc(identityPath, doc) {
139
+ const policyPath = getTrustPolicyPath(identityPath);
140
+ fs.mkdirSync(path.dirname(policyPath), { recursive: true, mode: 0o700 });
141
+ fs.writeFileSync(policyPath, JSON.stringify(normalizeTrustPolicyDoc(doc), null, 2), 'utf-8');
142
+ return policyPath;
143
+ }
144
+
145
+ function findOpenClawInstallScript() {
146
+ const explicit = process.env.PINGAGENT_OPENCLAW_INSTALL_BIN;
147
+ if (explicit) return { cmd: process.execPath, args: [resolvePath(explicit)], source: 'env' };
148
+
149
+ const localCandidates = [
150
+ path.resolve(process.cwd(), 'packages', 'openclaw-install', 'install.mjs'),
151
+ path.resolve(path.dirname(THIS_FILE), '..', '..', 'openclaw-install', 'install.mjs'),
152
+ ];
153
+ for (const candidate of localCandidates) {
154
+ if (fs.existsSync(candidate)) return { cmd: process.execPath, args: [candidate], source: 'repo' };
155
+ }
156
+
157
+ try {
158
+ const pkgJsonPath = require.resolve('@pingagent/openclaw-install/package.json');
159
+ const installedScript = path.join(path.dirname(pkgJsonPath), 'install.mjs');
160
+ if (fs.existsSync(installedScript)) {
161
+ return { cmd: process.execPath, args: [installedScript], source: 'installed-package' };
162
+ }
163
+ } catch {
164
+ // fall through to npx fallback
165
+ }
166
+
167
+ return { cmd: 'npx', args: ['-y', '@pingagent/openclaw-install'], source: 'npx' };
168
+ }
169
+
170
+ function runOpenClawInstall(args) {
171
+ const resolved = findOpenClawInstallScript();
172
+ const result = spawnSync(resolved.cmd, [...resolved.args, ...args], {
173
+ encoding: 'utf-8',
174
+ env: process.env,
175
+ });
176
+ const errorMessage = result.error
177
+ ? (result.error.code === 'ENOENT'
178
+ ? `Failed to launch ${resolved.cmd}. Install npx/Node tooling or set PINGAGENT_OPENCLAW_INSTALL_BIN.`
179
+ : String(result.error.message || result.error))
180
+ : '';
181
+ return {
182
+ ok: result.status === 0 && !result.error,
183
+ stdout: String(result.stdout ?? ''),
184
+ stderr: errorMessage || String(result.stderr ?? ''),
185
+ status: result.status ?? 1,
186
+ source: resolved.source,
187
+ };
188
+ }
189
+
190
+ function getHostPanelSurfaceUrl() {
191
+ const portRaw = String(process.env.PINGAGENT_WEB_PORT || '3846').trim();
192
+ const port = Number.parseInt(portRaw, 10);
193
+ return `http://127.0.0.1:${Number.isFinite(port) ? port : 3846}/host-panel`;
194
+ }
195
+
196
+ function getSurfaceRecommendationLines(primaryCommandPrefix = 'npx @pingagent/sdk', secondaryCommandPrefix = 'pingagent') {
197
+ return [
198
+ 'With GUI: Host Panel',
199
+ ` Start locally: ${primaryCommandPrefix} web`,
200
+ ...(secondaryCommandPrefix && secondaryCommandPrefix !== primaryCommandPrefix ? [` Or via local bin: ${secondaryCommandPrefix} web`] : []),
201
+ ` URL when running: ${getHostPanelSurfaceUrl()}`,
202
+ 'Headless / low-token: TUI',
203
+ ` ${primaryCommandPrefix} host tui`,
204
+ ...(secondaryCommandPrefix && secondaryCommandPrefix !== primaryCommandPrefix ? [` ${secondaryCommandPrefix} host tui`] : []),
205
+ ` ${primaryCommandPrefix} host tui --once`,
206
+ 'MCP: agent/runtime control surface, not the default human operator UI',
207
+ ];
208
+ }
209
+
111
210
  function clearScreen() {
112
211
  process.stdout.write('\x1Bc');
113
212
  }
@@ -139,11 +238,11 @@ function truncateLine(value, max = 100) {
139
238
 
140
239
  function formatSessionRow(session, selected) {
141
240
  const marker = selected ? '>' : ' ';
142
- const rebind = session.binding_alert ? ' !rebind' : '';
241
+ const reconnect = session.binding_alert ? ' !reconnect' : '';
143
242
  const trust = session.trust_state || 'unknown';
144
243
  const unread = session.unread_count ?? 0;
145
244
  const who = truncateLine(session.remote_did || session.conversation_id || 'unknown', 40);
146
- return `${marker} ${who} [${trust}] unread=${unread}${rebind}`;
245
+ return `${marker} ${who} [${trust}] unread=${unread}${reconnect}`;
147
246
  }
148
247
 
149
248
  function formatMessageRow(message) {
@@ -173,6 +272,7 @@ function buildTaskExportBody(task, format = 'plain') {
173
272
  message: task.error_message,
174
273
  }
175
274
  : null,
275
+ handoff: task.handoff || null,
176
276
  };
177
277
  if (format === 'json') return JSON.stringify(payload, null, 2);
178
278
  return [
@@ -232,7 +332,9 @@ function buildHostState(identityPath, selectedSessionKey = null, historyPageInde
232
332
  const store = openStore(identityPath);
233
333
  try {
234
334
  const sessionManager = new SessionManager(store);
335
+ const sessionSummaryManager = new SessionSummaryManager(store);
235
336
  const taskManager = new TaskThreadManager(store);
337
+ const taskHandoffManager = new TaskHandoffManager(store);
236
338
  const historyManager = new HistoryManager(store);
237
339
  const auditManager = new TrustPolicyAuditManager(store);
238
340
  const sessions = sessionManager.listRecentSessions(50);
@@ -241,6 +343,7 @@ function buildHostState(identityPath, selectedSessionKey = null, historyPageInde
241
343
  const bindingByConversation = new Map(bindings.map((row) => [row.conversation_id, row]));
242
344
  const alertByConversation = new Map(alerts.map((row) => [row.conversation_id, row]));
243
345
  const activeChatSession = readCurrentActiveSessionKey();
346
+ const ingressRuntime = readIngressRuntimeStatus();
244
347
  const desiredSelectedSessionKey =
245
348
  (selectedSessionKey && sessions.some((session) => session.session_key === selectedSessionKey) ? selectedSessionKey : null) ??
246
349
  sessionManager.getActiveSession()?.session_key ??
@@ -255,8 +358,14 @@ function buildHostState(identityPath, selectedSessionKey = null, historyPageInde
255
358
  is_active_chat_session: session.session_key === activeChatSession,
256
359
  }));
257
360
  const selectedSession = sessionsWithMeta.find((session) => session.session_key === desiredSelectedSessionKey) ?? null;
361
+ const selectedSessionSummary = selectedSession
362
+ ? sessionSummaryManager.get(selectedSession.session_key)
363
+ : null;
258
364
  const selectedTasks = selectedSession
259
- ? taskManager.listBySession(selectedSession.session_key, 12)
365
+ ? taskManager.listBySession(selectedSession.session_key, 12).map((task) => ({
366
+ ...task,
367
+ handoff: taskHandoffManager.get(task.task_id),
368
+ }))
260
369
  : [];
261
370
  const selectedAuditEvents = selectedSession
262
371
  ? auditManager.listBySession(selectedSession.session_key, 12)
@@ -270,27 +379,45 @@ function buildHostState(identityPath, selectedSessionKey = null, historyPageInde
270
379
  const selectedHistorySearchResults = selectedSession?.conversation_id && historySearchQuery.trim()
271
380
  ? historyManager.search(historySearchQuery.trim(), { conversationId: selectedSession.conversation_id, limit: 50 })
272
381
  : [];
382
+ const recommendationManager = new TrustRecommendationManager(store);
383
+ recommendationManager.sync({
384
+ policyDoc: policy.doc,
385
+ sessions,
386
+ tasks: taskManager.listRecent(100),
387
+ auditEvents: auditManager.listRecent(200),
388
+ runtimeMode,
389
+ limit: 50,
390
+ });
391
+ const selectedRecommendations = selectedSession?.remote_did
392
+ ? recommendationManager.list({ remoteDid: selectedSession.remote_did, limit: 10 })
393
+ : [];
273
394
  const unreadTotal = sessionsWithMeta.reduce((sum, session) => sum + (session.unread_count ?? 0), 0);
274
395
  const alertSessions = sessionsWithMeta.filter((session) => !!session.binding_alert).length;
275
396
  return {
276
397
  identity,
277
398
  runtimeMode,
278
399
  activeChatSession,
400
+ ingressRuntime,
279
401
  activeChatSessionFile: getActiveSessionFilePath(),
280
402
  sessionMapPath: getSessionMapFilePath(),
281
403
  sessionBindingAlertsPath: getSessionBindingAlertsFilePath(),
282
404
  policyPath: policy.path,
283
405
  policyDoc: policy.doc,
284
406
  sessions: sessionsWithMeta,
285
- tasks: taskManager.listRecent(30),
407
+ tasks: taskManager.listRecent(30).map((task) => ({
408
+ ...task,
409
+ handoff: taskHandoffManager.get(task.task_id),
410
+ })),
286
411
  auditEvents: auditManager.listRecent(40),
287
412
  selectedSession,
413
+ selectedSessionSummary,
288
414
  selectedTasks,
289
415
  selectedAuditEvents,
290
416
  selectedMessages,
291
417
  selectedHistoryPage,
292
418
  selectedHistorySearchQuery: historySearchQuery.trim(),
293
419
  selectedHistorySearchResults,
420
+ selectedRecommendations,
294
421
  unreadTotal,
295
422
  alertSessions,
296
423
  };
@@ -302,9 +429,13 @@ function buildHostState(identityPath, selectedSessionKey = null, historyPageInde
302
429
  function renderHostTuiScreen(hostState, uiState) {
303
430
  const sessions = hostState.sessions || [];
304
431
  const selected = hostState.selectedSession || sessions[0] || null;
432
+ const selectedSummary = hostState.selectedSessionSummary || null;
305
433
  const tasks = hostState.selectedTasks || [];
306
434
  const auditEvents = hostState.selectedAuditEvents || [];
307
435
  const messages = hostState.selectedMessages || [];
436
+ const recommendations = hostState.selectedRecommendations || [];
437
+ const openRecommendation = recommendations.find((item) => item.status === 'open') || null;
438
+ const reopenRecommendation = recommendations.find((item) => item.status === 'dismissed' || item.status === 'superseded') || null;
308
439
  const historyPage = hostState.selectedHistoryPage || { messages: [], pageIndex: 0, hasOlder: false, hasNewer: false };
309
440
  const historySearchQuery = hostState.selectedHistorySearchQuery || '';
310
441
  const historySearchResults = hostState.selectedHistorySearchResults || [];
@@ -315,17 +446,24 @@ function renderHostTuiScreen(hostState, uiState) {
315
446
  ? ` (${Math.max(0, Math.ceil((uiState.statusExpiresAt - Date.now()) / 1000))}s)`
316
447
  : '';
317
448
  const statusTs = uiState?.statusAt ? ` @ ${formatStatusTimestamp(uiState.statusAt)}` : '';
449
+ const degraded = !hostState.ingressRuntime
450
+ || hostState.ingressRuntime.receive_mode === 'polling_degraded'
451
+ || !!hostState.ingressRuntime.hooks_last_error;
452
+ const ingressLabel = degraded ? 'Degraded' : 'Ready';
318
453
  const lines = [
319
454
  'PingAgent Host TUI',
320
455
  `DID: ${hostState.identity.did}`,
321
456
  `status=${formatStatusLine(uiState?.statusLevel || 'info', uiState?.statusMessage || '(ready)')}${statusTs}${statusCountdown}`,
322
- `runtime_mode=${hostState.runtimeMode} active_chat_session=${hostState.activeChatSession || '(none)'}`,
457
+ `runtime_mode=${hostState.runtimeMode} receive_mode=${hostState.ingressRuntime?.receive_mode || 'webhook'} current_openclaw_chat=${hostState.activeChatSession || '(none)'}`,
458
+ `ingress=${ingressLabel}${degraded ? ' action=[f] fix-now' : ''}`,
459
+ uiState?.publicLinkUrl ? `public_link=${uiState.publicLinkUrl}` : null,
323
460
  `sessions=${sessions.length} unread_total=${hostState.unreadTotal ?? 0} alert_sessions=${hostState.alertSessions ?? 0} view=${view}`,
324
461
  `policy=${hostState.policyPath}`,
325
- `session_map=${hostState.sessionMapPath}`,
326
- `binding_alerts=${hostState.sessionBindingAlertsPath}`,
462
+ `chat_link_map=${hostState.sessionMapPath}`,
463
+ `chat_link_alerts=${hostState.sessionBindingAlertsPath}`,
464
+ hostState.ingressRuntime?.hooks_last_error ? `hooks_error=${truncateLine(hostState.ingressRuntime.hooks_last_error, 120)}` : null,
327
465
  '',
328
- ];
466
+ ].filter(Boolean);
329
467
 
330
468
  if (view === 'help') {
331
469
  lines.push('Help');
@@ -339,12 +477,18 @@ function renderHostTuiScreen(hostState, uiState) {
339
477
  lines.push('- t: open task list view for selected session');
340
478
  lines.push('- x: cancel selected task (in task views)');
341
479
  lines.push('- p: multiline reply prompt (detail view)');
480
+ lines.push('- S: edit carry-forward summary (detail view)');
481
+ lines.push('- d: try demo agent preset');
342
482
  lines.push('- o: open local history paging (detail view)');
343
483
  lines.push('- n / p: older / newer history page (history view)');
344
484
  lines.push('- s or /: search local history (history view)');
345
485
  lines.push('- y: dump task detail to stdout (task-detail view, choose json/plain)');
346
- lines.push('- b: bind selected conversation to current chat session');
347
- lines.push('- c: clear selected binding');
486
+ lines.push('- f: repair OpenClaw hooks config');
487
+ lines.push('- A: apply first open trust recommendation for selected session');
488
+ lines.push('- D: dismiss current open recommendation');
489
+ lines.push('- R: reopen dismissed/superseded recommendation');
490
+ lines.push('- b: attach selected session to the current OpenClaw chat');
491
+ lines.push('- c: detach the selected chat link');
348
492
  lines.push('- q: quit');
349
493
  } else if (view === 'history') {
350
494
  lines.push('Conversation History');
@@ -382,8 +526,17 @@ function renderHostTuiScreen(hostState, uiState) {
382
526
  lines.push(`status=${selectedTask.status}`);
383
527
  lines.push(`updated_at=${formatTs(selectedTask.updated_at, false)}`);
384
528
  if (selectedTask.started_at) lines.push(`started_at=${formatTs(selectedTask.started_at, false)}`);
529
+ if (selectedTask.handoff?.objective) lines.push(`handoff_objective=${selectedTask.handoff.objective}`);
530
+ if (selectedTask.handoff?.priority) lines.push(`handoff_priority=${selectedTask.handoff.priority}`);
531
+ if (selectedTask.handoff?.success_criteria) lines.push(`handoff_success=${selectedTask.handoff.success_criteria}`);
532
+ if (selectedTask.handoff?.callback_session_key) lines.push(`handoff_callback=${selectedTask.handoff.callback_session_key}`);
385
533
  lines.push('actions=[x] cancel-task [y] dump-stdout [j/k] switch-task [h/Esc] back-to-tasks');
386
534
  lines.push('');
535
+ if (selectedTask.handoff?.carry_forward_summary) {
536
+ lines.push('Handoff Summary');
537
+ lines.push(selectedTask.handoff.carry_forward_summary);
538
+ lines.push('');
539
+ }
387
540
  lines.push('Result');
388
541
  lines.push(selectedTask.result_summary || '(none)');
389
542
  lines.push('');
@@ -409,6 +562,8 @@ function renderHostTuiScreen(hostState, uiState) {
409
562
  lines.push(`status=${selectedTask.status}`);
410
563
  lines.push(`updated_at=${formatTs(selectedTask.updated_at, false)}`);
411
564
  if (selectedTask.started_at) lines.push(`started_at=${formatTs(selectedTask.started_at, false)}`);
565
+ if (selectedTask.handoff?.objective) lines.push(`handoff_objective=${selectedTask.handoff.objective}`);
566
+ if (selectedTask.handoff?.priority) lines.push(`handoff_priority=${selectedTask.handoff.priority}`);
412
567
  if (selectedTask.result_summary) lines.push(`result=${selectedTask.result_summary}`);
413
568
  if (selectedTask.error_code || selectedTask.error_message) {
414
569
  lines.push(`error=${selectedTask.error_code || 'E_TASK'} ${selectedTask.error_message || ''}`.trim());
@@ -432,28 +587,57 @@ function renderHostTuiScreen(hostState, uiState) {
432
587
  lines.push(`remote=${selected.remote_did || '(unknown)'}`);
433
588
  lines.push(`trust=${selected.trust_state} unread=${selected.unread_count}`);
434
589
  lines.push(`last_preview=${selected.last_message_preview || '(none)'}`);
435
- lines.push(`binding=${selected.binding?.session_key || '(unbound)'}`);
436
- lines.push(`current_chat=${hostState.activeChatSession || '(none)'}`);
590
+ lines.push(`chat_link=${selected.binding?.session_key || '(none)'}`);
591
+ lines.push(`current_openclaw_chat=${hostState.activeChatSession || '(none)'}`);
437
592
  if (selected.binding_alert) {
438
- lines.push(`needs_rebind=true`);
593
+ lines.push('needs_reconnect=true');
439
594
  lines.push(`warning=${selected.binding_alert.message}`);
440
595
  } else {
441
- lines.push('needs_rebind=false');
596
+ lines.push('needs_reconnect=false');
597
+ }
598
+ if (openRecommendation) {
599
+ lines.push(`trust_action=${getTrustRecommendationActionLabel(openRecommendation)}`);
600
+ } else if (reopenRecommendation) {
601
+ lines.push(`trust_action=${getTrustRecommendationActionLabel(reopenRecommendation)}`);
602
+ }
603
+ if (selectedSummary) {
604
+ lines.push(`summary_objective=${selectedSummary.objective || '(none)'}`);
605
+ lines.push(`summary_next_action=${selectedSummary.next_action || '(none)'}`);
606
+ } else {
607
+ lines.push('summary_objective=(none)');
442
608
  }
443
609
  const actionBar = [
444
610
  selected.trust_state === 'pending' ? '[a] approve' : null,
611
+ '[A] apply-rec',
612
+ '[D] dismiss-rec',
613
+ '[R] reopen-rec',
445
614
  '[m] mark-read',
615
+ '[d] demo',
446
616
  '[p] reply',
617
+ '[S] summary',
447
618
  '[o] history',
448
619
  '[t] tasks',
449
- '[b] bind-current',
450
- '[c] clear-binding',
620
+ '[b] attach-chat',
621
+ '[c] detach-chat',
451
622
  ].filter(Boolean).join(' ');
452
623
  lines.push(`actions=${actionBar}`);
453
624
  lines.push('');
625
+ lines.push('Carry-Forward Summary');
626
+ if (selectedSummary) {
627
+ lines.push(`- objective: ${selectedSummary.objective || '(none)'}`);
628
+ lines.push(`- context: ${truncateLine(selectedSummary.context || '(none)', 100)}`);
629
+ lines.push(`- constraints: ${truncateLine(selectedSummary.constraints || '(none)', 100)}`);
630
+ lines.push(`- decisions: ${truncateLine(selectedSummary.decisions || '(none)', 100)}`);
631
+ lines.push(`- open_questions: ${truncateLine(selectedSummary.open_questions || '(none)', 100)}`);
632
+ lines.push(`- next_action: ${truncateLine(selectedSummary.next_action || '(none)', 100)}`);
633
+ lines.push(`- handoff_ready: ${truncateLine(selectedSummary.handoff_ready_text || '(none)', 100)}`);
634
+ } else {
635
+ lines.push('- none');
636
+ }
637
+ lines.push('');
454
638
  lines.push('Tasks');
455
639
  lines.push(...(tasks.length
456
- ? tasks.map((task) => `- ${task.title || task.task_id} [${task.status}] ${truncateLine(task.result_summary || task.error_message || '', 80)}`)
640
+ ? tasks.map((task) => `- ${task.title || task.task_id} [${task.status}]${task.handoff?.objective ? ` handoff=${truncateLine(task.handoff.objective, 30)}` : ''} ${truncateLine(task.result_summary || task.error_message || '', 80)}`)
457
641
  : ['- none']));
458
642
  if (view === 'detail') {
459
643
  lines.push('');
@@ -471,7 +655,7 @@ function renderHostTuiScreen(hostState, uiState) {
471
655
  }
472
656
 
473
657
  lines.push('');
474
- lines.push('Keys: ↑/↓ or j/k select Enter/l open Esc/h back g/G jump r refresh a approve m read p reply o history s search t tasks x cancel-task y dump b bind c clear ? help q quit');
658
+ lines.push('Keys: ↑/↓ or j/k select Enter/l open Esc/h back g/G jump r refresh a approve A apply-rec D dismiss-rec R reopen-rec d demo m read p reply o history s search t tasks x cancel-task y dump f fix-hooks b attach-chat c detach-chat ? help q quit');
475
659
  return lines.join('\n');
476
660
  }
477
661
 
@@ -489,6 +673,7 @@ async function runHostTui(identityPath, opts) {
489
673
  statusAt: 0,
490
674
  selectedHistoryPageIndex: 0,
491
675
  historySearchQuery: '',
676
+ publicLinkUrl: '',
492
677
  };
493
678
 
494
679
  const render = () => {
@@ -511,6 +696,9 @@ async function runHostTui(identityPath, opts) {
511
696
  };
512
697
 
513
698
  if (once) {
699
+ void maybeEnsureHostedPublicLink(identityPath).then((res) => {
700
+ if (res?.data?.public_url) uiState.publicLinkUrl = res.data.public_url;
701
+ });
514
702
  const { screen } = render();
515
703
  console.log(screen);
516
704
  return;
@@ -546,6 +734,132 @@ async function runHostTui(identityPath, opts) {
546
734
  uiState.statusAt = Date.now();
547
735
  };
548
736
 
737
+ const applySessionRecommendation = (selected) => {
738
+ if (!selected?.remote_did) return { ok: false, message: 'No remote DID for selected session.' };
739
+ const store = openStore(identityPath);
740
+ try {
741
+ const { path: policyPath, doc } = readTrustPolicyDoc(identityPath);
742
+ const auditManager = new TrustPolicyAuditManager(store);
743
+ const recommendationManager = new TrustRecommendationManager(store);
744
+ recommendationManager.sync({
745
+ policyDoc: doc,
746
+ sessions: new SessionManager(store).listRecentSessions(100),
747
+ tasks: new TaskThreadManager(store).listRecent(100),
748
+ auditEvents: auditManager.listRecent(200),
749
+ runtimeMode: process.env.PINGAGENT_RUNTIME_MODE || 'bridge',
750
+ limit: 50,
751
+ });
752
+ const recommendation = recommendationManager.list({
753
+ remoteDid: selected.remote_did,
754
+ status: 'open',
755
+ limit: 1,
756
+ })[0];
757
+ if (!recommendation) return { ok: false, message: 'No open recommendation for this session.' };
758
+ const nextDoc = upsertTrustPolicyRecommendation(doc, recommendation);
759
+ writeTrustPolicyDoc(identityPath, nextDoc);
760
+ recommendationManager.apply(recommendation.id);
761
+ auditManager.record({
762
+ event_type: 'recommendation_applied',
763
+ policy_scope: recommendation.policy,
764
+ remote_did: recommendation.remote_did,
765
+ action: String(recommendation.action),
766
+ outcome: 'recommendation_applied',
767
+ explanation: recommendation.reason,
768
+ matched_rule: recommendation.match,
769
+ detail: { recommendation_id: recommendation.id, session_key: selected.session_key },
770
+ });
771
+ return { ok: true, message: `${getTrustRecommendationActionLabel(recommendation)} (${recommendation.policy})`, path: policyPath };
772
+ } finally {
773
+ store.close();
774
+ }
775
+ };
776
+
777
+ const dismissSessionRecommendation = (selected) => {
778
+ if (!selected?.remote_did) return { ok: false, message: 'No remote DID for selected session.' };
779
+ const store = openStore(identityPath);
780
+ try {
781
+ const recommendationManager = new TrustRecommendationManager(store);
782
+ const recommendation = recommendationManager.list({
783
+ remoteDid: selected.remote_did,
784
+ status: 'open',
785
+ limit: 1,
786
+ })[0];
787
+ if (!recommendation) return { ok: false, message: 'No open recommendation for this session.' };
788
+ recommendationManager.dismiss(recommendation.id);
789
+ new TrustPolicyAuditManager(store).record({
790
+ event_type: 'recommendation_dismissed',
791
+ policy_scope: recommendation.policy,
792
+ remote_did: recommendation.remote_did,
793
+ action: String(recommendation.action),
794
+ outcome: 'recommendation_dismissed',
795
+ explanation: recommendation.reason,
796
+ matched_rule: recommendation.match,
797
+ detail: { recommendation_id: recommendation.id, session_key: selected.session_key },
798
+ });
799
+ return { ok: true, message: `Dismissed ${getTrustRecommendationActionLabel(recommendation)}` };
800
+ } finally {
801
+ store.close();
802
+ }
803
+ };
804
+
805
+ const reopenSessionRecommendation = (selected) => {
806
+ if (!selected?.remote_did) return { ok: false, message: 'No remote DID for selected session.' };
807
+ const store = openStore(identityPath);
808
+ try {
809
+ const recommendationManager = new TrustRecommendationManager(store);
810
+ const recommendation = recommendationManager.list({
811
+ remoteDid: selected.remote_did,
812
+ status: ['dismissed', 'superseded'],
813
+ limit: 1,
814
+ })[0];
815
+ if (!recommendation) return { ok: false, message: 'No dismissed or superseded recommendation for this session.' };
816
+ recommendationManager.reopen(recommendation.id);
817
+ new TrustPolicyAuditManager(store).record({
818
+ event_type: 'recommendation_reopened',
819
+ policy_scope: recommendation.policy,
820
+ remote_did: recommendation.remote_did,
821
+ action: String(recommendation.action),
822
+ outcome: 'recommendation_reopened',
823
+ explanation: recommendation.reason,
824
+ matched_rule: recommendation.match,
825
+ detail: { recommendation_id: recommendation.id, session_key: selected.session_key },
826
+ });
827
+ return { ok: true, message: 'Reopened recommendation' };
828
+ } finally {
829
+ store.close();
830
+ }
831
+ };
832
+
833
+ const sendDemoPreset = async () => {
834
+ const answer = await promptLine('Demo preset [hello/delegate/trust] (default hello): ');
835
+ const preset = (answer.trim().toLowerCase() || 'hello');
836
+ const presetMessages = {
837
+ hello: 'Hello',
838
+ delegate: 'Please show me how task delegation works in PingAgent.',
839
+ trust: 'Show me how trust decisions and recommendations work.',
840
+ };
841
+ const message = presetMessages[preset] || presetMessages.hello;
842
+ const { client, store } = await getClientWithStore(identityPath);
843
+ try {
844
+ const resolved = await client.resolveAlias('pingagent/demo');
845
+ if (!resolved.ok || !resolved.data?.did) {
846
+ setStatus(`Demo resolve failed: ${resolved.error?.message || 'unknown error'}`, 'err');
847
+ return;
848
+ }
849
+ const convo = await client.openConversation(resolved.data.did);
850
+ if (!convo.ok || !convo.data?.conversation_id) {
851
+ setStatus(`Demo open failed: ${convo.error?.message || 'unknown error'}`, 'err');
852
+ return;
853
+ }
854
+ const sendRes = await client.sendMessage(convo.data.conversation_id, SCHEMA_TEXT, { text: message });
855
+ setStatus(sendRes.ok
856
+ ? `Demo preset sent (${preset}) conversation=${convo.data.conversation_id}`
857
+ : `Demo send failed: ${sendRes.error?.message || 'unknown error'}`, sendRes.ok ? 'ok' : 'err', sendRes.ok ? 7000 : 9000);
858
+ } finally {
859
+ store.close();
860
+ }
861
+ };
862
+
549
863
  const promptLine = async (question) => {
550
864
  stopInterval();
551
865
  if (process.stdin.setRawMode) process.stdin.setRawMode(false);
@@ -655,6 +969,13 @@ async function runHostTui(identityPath, opts) {
655
969
  };
656
970
 
657
971
  startInterval();
972
+ void maybeEnsureHostedPublicLink(identityPath).then((result) => {
973
+ if (result?.data?.public_url) {
974
+ uiState.publicLinkUrl = result.data.public_url;
975
+ setStatus(result.created ? `Public link ready: ${result.data.public_url}` : `Public link available: ${result.data.public_url}`, 'ok', 7000);
976
+ latestState = redraw();
977
+ }
978
+ });
658
979
 
659
980
  process.stdin.on('keypress', async (_str, key) => {
660
981
  if (key?.name === 'q' || (key?.ctrl && key?.name === 'c')) {
@@ -762,6 +1083,35 @@ async function runHostTui(identityPath, opts) {
762
1083
  latestState = redraw();
763
1084
  return;
764
1085
  }
1086
+ if (_str === 'A') {
1087
+ const selected = latestState.selectedSession;
1088
+ if (!selected) return;
1089
+ const result = applySessionRecommendation(selected);
1090
+ setStatus(result.message, result.ok ? 'ok' : 'warn');
1091
+ latestState = redraw();
1092
+ return;
1093
+ }
1094
+ if (_str === 'D') {
1095
+ const selected = latestState.selectedSession;
1096
+ if (!selected) return;
1097
+ const result = dismissSessionRecommendation(selected);
1098
+ setStatus(result.message, result.ok ? 'ok' : 'warn');
1099
+ latestState = redraw();
1100
+ return;
1101
+ }
1102
+ if (_str === 'R') {
1103
+ const selected = latestState.selectedSession;
1104
+ if (!selected) return;
1105
+ const result = reopenSessionRecommendation(selected);
1106
+ setStatus(result.message, result.ok ? 'ok' : 'warn');
1107
+ latestState = redraw();
1108
+ return;
1109
+ }
1110
+ if (key?.name === 'd') {
1111
+ await sendDemoPreset();
1112
+ latestState = redraw();
1113
+ return;
1114
+ }
765
1115
  if (key?.name === 'p' && uiState.view === 'detail') {
766
1116
  const selected = latestState.selectedSession;
767
1117
  if (!selected?.conversation_id) return;
@@ -784,6 +1134,51 @@ async function runHostTui(identityPath, opts) {
784
1134
  latestState = redraw();
785
1135
  return;
786
1136
  }
1137
+ if (_str === 'S' && uiState.view === 'detail') {
1138
+ const selected = latestState.selectedSession;
1139
+ if (!selected) return;
1140
+ const existing = latestState.selectedSessionSummary || {};
1141
+ const input = await promptMultiline(
1142
+ `Edit carry-forward summary as JSON for ${selected.remote_did || selected.session_key}\nCurrent:\n${JSON.stringify({
1143
+ objective: existing.objective || '',
1144
+ context: existing.context || '',
1145
+ constraints: existing.constraints || '',
1146
+ decisions: existing.decisions || '',
1147
+ open_questions: existing.open_questions || '',
1148
+ next_action: existing.next_action || '',
1149
+ handoff_ready_text: existing.handoff_ready_text || '',
1150
+ }, null, 2)}`
1151
+ );
1152
+ if (!input.trim()) {
1153
+ setStatus('Summary update cancelled.', 'warn');
1154
+ latestState = redraw();
1155
+ return;
1156
+ }
1157
+ try {
1158
+ const parsed = JSON.parse(input);
1159
+ const store = openStore(identityPath);
1160
+ try {
1161
+ const manager = new SessionSummaryManager(store);
1162
+ manager.upsert({
1163
+ session_key: selected.session_key,
1164
+ objective: parsed.objective,
1165
+ context: parsed.context,
1166
+ constraints: parsed.constraints,
1167
+ decisions: parsed.decisions,
1168
+ open_questions: parsed.open_questions,
1169
+ next_action: parsed.next_action,
1170
+ handoff_ready_text: parsed.handoff_ready_text,
1171
+ });
1172
+ } finally {
1173
+ store.close();
1174
+ }
1175
+ setStatus(`Saved carry-forward summary for ${selected.session_key}`, 'ok');
1176
+ } catch (error) {
1177
+ setStatus(`Summary update failed: ${error?.message || 'invalid JSON'}`, 'err', 9000);
1178
+ }
1179
+ latestState = redraw();
1180
+ return;
1181
+ }
787
1182
  if (key?.name === 'o' && uiState.view === 'detail') {
788
1183
  uiState.view = 'history';
789
1184
  uiState.selectedHistoryPageIndex = 0;
@@ -805,18 +1200,18 @@ async function runHostTui(identityPath, opts) {
805
1200
  const selected = (latestState.sessions || []).find((session) => session.session_key === uiState.selectedSessionKey);
806
1201
  if (!selected?.conversation_id) return;
807
1202
  const current = latestState.activeChatSession || '(none)';
808
- const previous = selected.binding?.session_key || '(unbound)';
809
- const confirmed = await confirmAction(`Rebind conversation ${selected.conversation_id}\nRemote DID: ${selected.remote_did || '(unknown)'}\nCurrent chat: ${current}\nPrevious binding: ${previous}\nProceed?`);
1203
+ const previous = selected.binding?.session_key || '(none)';
1204
+ const confirmed = await confirmAction(`Attach chat link for conversation ${selected.conversation_id}\nRemote DID: ${selected.remote_did || '(unknown)'}\nCurrent OpenClaw chat: ${current}\nPrevious chat link: ${previous}\nProceed?`);
810
1205
  if (confirmed) {
811
1206
  if (!latestState.activeChatSession) {
812
- setStatus('Rebind failed: no active chat session.', 'err');
1207
+ setStatus('Attach failed: no active OpenClaw chat.', 'err');
813
1208
  latestState = redraw();
814
1209
  return;
815
1210
  }
816
1211
  setSessionBinding(selected.conversation_id, latestState.activeChatSession);
817
- setStatus(`Rebound ${selected.conversation_id} -> ${latestState.activeChatSession}`, 'ok');
1212
+ setStatus(`Attached chat link ${selected.conversation_id} -> ${latestState.activeChatSession}`, 'ok');
818
1213
  } else {
819
- setStatus('Rebind cancelled.', 'warn');
1214
+ setStatus('Attach chat link cancelled.', 'warn');
820
1215
  }
821
1216
  latestState = redraw();
822
1217
  return;
@@ -825,7 +1220,7 @@ async function runHostTui(identityPath, opts) {
825
1220
  const selected = (latestState.sessions || []).find((session) => session.session_key === uiState.selectedSessionKey);
826
1221
  if (!selected?.conversation_id) return;
827
1222
  removeSessionBinding(selected.conversation_id);
828
- setStatus(`Cleared binding for ${selected.conversation_id}`, 'ok');
1223
+ setStatus(`Detached chat link for ${selected.conversation_id}`, 'ok');
829
1224
  latestState = redraw();
830
1225
  return;
831
1226
  }
@@ -851,6 +1246,21 @@ async function runHostTui(identityPath, opts) {
851
1246
  latestState = redraw();
852
1247
  return;
853
1248
  }
1249
+ if (key?.name === 'f') {
1250
+ const confirmed = await confirmAction('Repair OpenClaw hooks config now? A timestamped backup will be written first.');
1251
+ if (!confirmed) {
1252
+ setStatus('Hooks repair cancelled.', 'warn');
1253
+ latestState = redraw();
1254
+ return;
1255
+ }
1256
+ const result = runOpenClawInstall(['fix-hooks']);
1257
+ setStatus(result.ok
1258
+ ? `Hooks repaired.${result.stdout ? ` ${truncateLine(result.stdout.replace(/\s+/g, ' '), 100)}` : ''}`
1259
+ : `Hooks repair failed: ${truncateLine(result.stderr || result.stdout || 'unknown error', 120)}`,
1260
+ result.ok ? 'ok' : 'err', result.ok ? 7000 : 9000);
1261
+ latestState = redraw();
1262
+ return;
1263
+ }
854
1264
  if (key?.name === 'y' && uiState.view === 'task-detail') {
855
1265
  const tasks = latestState.selectedTasks || [];
856
1266
  const task = tasks[Math.max(0, Math.min(uiState.selectedTaskIndex, Math.max(0, tasks.length - 1)))] || null;
@@ -916,6 +1326,51 @@ async function getClientWithStore(identityPath) {
916
1326
  return { client, store };
917
1327
  }
918
1328
 
1329
+ function resolveSessionFromStore(store, args = {}) {
1330
+ const sessionManager = new SessionManager(store);
1331
+ let session = args.sessionKey ? sessionManager.get(args.sessionKey) : null;
1332
+ if (!session && args.conversationId) session = sessionManager.getByConversationId(args.conversationId);
1333
+ if (!session && args.remoteDid) {
1334
+ session = sessionManager.listRecentSessions(200).find((item) => item.remote_did === args.remoteDid) ?? null;
1335
+ }
1336
+ return session ?? sessionManager.getActiveSession() ?? sessionManager.listRecentSessions(1)[0] ?? null;
1337
+ }
1338
+
1339
+ async function resolveTarget(client, target) {
1340
+ const value = String(target ?? '').trim();
1341
+ if (!value) throw new Error('Missing target');
1342
+ if (value.startsWith('did:agent:')) return value;
1343
+ if (value.startsWith('@')) {
1344
+ const resolved = await client.resolveAlias(value.slice(1));
1345
+ if (!resolved.ok || !resolved.data?.did) {
1346
+ throw new Error(resolved.error?.message || `Cannot resolve alias ${value}`);
1347
+ }
1348
+ return resolved.data.did;
1349
+ }
1350
+ const publicAgent = await client.getPublicAgent(value).catch(() => ({ ok: false }));
1351
+ if (publicAgent.ok && publicAgent.data?.did) return publicAgent.data.did;
1352
+ return value;
1353
+ }
1354
+
1355
+ async function maybeEnsureHostedPublicLink(identityPath) {
1356
+ const p = identityPath ?? getEffectiveIdentityPath();
1357
+ const id = loadIdentity(p);
1358
+ if (!isOfficialHostedServer(id.serverUrl ?? DEFAULT_SERVER)) return null;
1359
+ const key = `${p}:${normalizeOrigin(id.serverUrl ?? DEFAULT_SERVER)}`;
1360
+ if (hostedPublicLinkAttempts.has(key)) return null;
1361
+ hostedPublicLinkAttempts.add(key);
1362
+ try {
1363
+ const client = await getClient(p);
1364
+ const current = await client.getPublicSelf().catch(() => ({ ok: false }));
1365
+ if (current.ok && current.data?.public_url) return { created: false, data: current.data };
1366
+ const created = await client.createPublicLink({ enabled: true }).catch(() => ({ ok: false }));
1367
+ if (created.ok && created.data) return { created: true, data: created.data };
1368
+ return null;
1369
+ } catch {
1370
+ return null;
1371
+ }
1372
+ }
1373
+
919
1374
  const program = new Command();
920
1375
  program
921
1376
  .name('pingagent')
@@ -2249,13 +2704,577 @@ billing
2249
2704
  }
2250
2705
  });
2251
2706
 
2707
+ const publicCmd = program.command('public').description('Hosted public growth surface: shareable profile links, contact cards, and task shares');
2708
+
2709
+ publicCmd
2710
+ .command('link')
2711
+ .description('Create or update your hosted public share link')
2712
+ .option('--slug <slug>', 'Preferred public slug')
2713
+ .option('--json', 'Output as JSON')
2714
+ .action(async (opts) => {
2715
+ const client = await getClient();
2716
+ const res = await client.createPublicLink({ slug: opts.slug });
2717
+ if (!res.ok) {
2718
+ if (opts.json) console.log(JSON.stringify(res, null, 2));
2719
+ else printError(res.error);
2720
+ process.exit(1);
2721
+ }
2722
+ if (opts.json) console.log(JSON.stringify(res.data, null, 2));
2723
+ else {
2724
+ console.log(`Public slug: ${res.data.public_slug || '(none)'}`);
2725
+ console.log(`Canonical: ${res.data.canonical_slug || '(none)'}`);
2726
+ console.log(`URL: ${res.data.public_url || '(none)'}`);
2727
+ }
2728
+ });
2729
+
2730
+ publicCmd
2731
+ .command('profile')
2732
+ .description('Show your hosted public share state')
2733
+ .option('--json', 'Output as JSON')
2734
+ .action(async (opts) => {
2735
+ const client = await getClient();
2736
+ await maybeEnsureHostedPublicLink();
2737
+ const res = await client.getPublicSelf();
2738
+ if (!res.ok) {
2739
+ if (opts.json) console.log(JSON.stringify(res, null, 2));
2740
+ else printError(res.error);
2741
+ process.exit(1);
2742
+ }
2743
+ if (opts.json) console.log(JSON.stringify(res.data, null, 2));
2744
+ else {
2745
+ console.log(`DID: ${res.data.did}`);
2746
+ console.log(`Alias: ${res.data.alias || '(none)'}`);
2747
+ console.log(`Public slug: ${res.data.public_slug || '(none)'}`);
2748
+ console.log(`Enabled: ${res.data.public_share_enabled ? 'yes' : 'no'}`);
2749
+ console.log(`Discoverable:${res.data.discoverable ? ' yes' : ' no'}`);
2750
+ console.log(`URL: ${res.data.public_url || '(none)'}`);
2751
+ }
2752
+ });
2753
+
2754
+ publicCmd
2755
+ .command('contact-card')
2756
+ .description('Create a shareable contact card for this agent or another target DID')
2757
+ .option('--target-did <did>', 'Target DID to place in the contact card')
2758
+ .option('--intro-note <text>', 'Intro note shown on the card')
2759
+ .option('--message-template <text>', 'Suggested first-message template')
2760
+ .option('--json', 'Output as JSON')
2761
+ .action(async (opts) => {
2762
+ const client = await getClient();
2763
+ const res = await client.createContactCard({
2764
+ target_did: opts.targetDid,
2765
+ intro_note: opts.introNote,
2766
+ message_template: opts.messageTemplate,
2767
+ });
2768
+ if (!res.ok) {
2769
+ if (opts.json) console.log(JSON.stringify(res, null, 2));
2770
+ else printError(res.error);
2771
+ process.exit(1);
2772
+ }
2773
+ if (opts.json) console.log(JSON.stringify(res.data, null, 2));
2774
+ else {
2775
+ console.log(`Contact card: ${res.data.id}`);
2776
+ console.log(`Target DID: ${res.data.target_did}`);
2777
+ console.log(`URL: ${res.data.share_url || '(none)'}`);
2778
+ }
2779
+ });
2780
+
2781
+ publicCmd
2782
+ .command('task-share')
2783
+ .description('Publish a shareable task result summary (explicit publish only)')
2784
+ .requiredOption('--summary <text>', 'Generated summary to publish')
2785
+ .option('--task-id <id>', 'Task ID')
2786
+ .option('--title <title>', 'Task title')
2787
+ .option('--status <status>', 'Task status', 'processed')
2788
+ .option('--conversation <id>', 'Conversation ID')
2789
+ .option('--json', 'Output as JSON')
2790
+ .action(async (opts) => {
2791
+ const client = await getClient();
2792
+ const res = await client.createTaskShare({
2793
+ task_id: opts.taskId,
2794
+ title: opts.title,
2795
+ status: opts.status,
2796
+ summary: opts.summary,
2797
+ conversation_id: opts.conversation,
2798
+ });
2799
+ if (!res.ok) {
2800
+ if (opts.json) console.log(JSON.stringify(res, null, 2));
2801
+ else printError(res.error);
2802
+ process.exit(1);
2803
+ }
2804
+ if (opts.json) console.log(JSON.stringify(res.data, null, 2));
2805
+ else {
2806
+ console.log(`Task share: ${res.data.id}`);
2807
+ console.log(`URL: ${res.data.share_url || '(none)'}`);
2808
+ }
2809
+ });
2810
+
2811
+ program
2812
+ .command('capability-card')
2813
+ .description('Show or update the structured machine-readable capability card for this agent')
2814
+ .option('--summary <text>', 'Capability card summary')
2815
+ .option('--accepts-new-work <value>', 'true or false')
2816
+ .option('--preferred-contact-mode <mode>', 'dm, task, or either')
2817
+ .option('--capability-item <json>', 'Capability item JSON; repeat to add/replace entries by id', (value, acc) => {
2818
+ acc.push(value);
2819
+ return acc;
2820
+ }, [])
2821
+ .option('--replace-items', 'Replace capability items with the provided --capability-item rows instead of merging by id')
2822
+ .option('--json', 'Output as JSON')
2823
+ .action(async (opts) => {
2824
+ const client = await getClient();
2825
+ const profileRes = await client.getProfile();
2826
+ if (!profileRes.ok || !profileRes.data) {
2827
+ if (opts.json) console.log(JSON.stringify(profileRes, null, 2));
2828
+ else printError(profileRes.error);
2829
+ process.exit(1);
2830
+ }
2831
+ const current = profileRes.data.capability_card || { version: '1', capabilities: [] };
2832
+ const shouldUpdate =
2833
+ opts.summary !== undefined
2834
+ || opts.acceptsNewWork !== undefined
2835
+ || opts.preferredContactMode !== undefined
2836
+ || (opts.capabilityItem && opts.capabilityItem.length > 0)
2837
+ || opts.replaceItems;
2838
+ if (!shouldUpdate) {
2839
+ if (opts.json) console.log(JSON.stringify(current, null, 2));
2840
+ else {
2841
+ console.log(`summary=${formatCapabilityCardSummary(current)}`);
2842
+ console.log(JSON.stringify(current, null, 2));
2843
+ }
2844
+ return;
2845
+ }
2846
+ let capabilityItems = Array.isArray(current.capabilities) ? [...current.capabilities] : [];
2847
+ if (opts.capabilityItem && opts.capabilityItem.length > 0) {
2848
+ const parsedItems = opts.capabilityItem.map((item) => JSON.parse(item));
2849
+ if (opts.replaceItems) {
2850
+ capabilityItems = parsedItems;
2851
+ } else {
2852
+ const byId = new Map(capabilityItems.map((item) => [item.id, item]));
2853
+ for (const item of parsedItems) {
2854
+ byId.set(item.id, { ...(byId.get(item.id) || {}), ...item });
2855
+ }
2856
+ capabilityItems = Array.from(byId.values());
2857
+ }
2858
+ } else if (opts.replaceItems) {
2859
+ capabilityItems = [];
2860
+ }
2861
+ const nextCard = {
2862
+ version: current.version || '1',
2863
+ summary: opts.summary !== undefined ? opts.summary : current.summary,
2864
+ accepts_new_work: opts.acceptsNewWork !== undefined
2865
+ ? String(opts.acceptsNewWork).trim().toLowerCase() === 'true'
2866
+ : current.accepts_new_work,
2867
+ preferred_contact_mode: opts.preferredContactMode !== undefined
2868
+ ? opts.preferredContactMode
2869
+ : current.preferred_contact_mode,
2870
+ capabilities: capabilityItems,
2871
+ };
2872
+ const updateRes = await client.updateProfile({ capability_card: nextCard });
2873
+ if (!updateRes.ok || !updateRes.data) {
2874
+ if (opts.json) console.log(JSON.stringify(updateRes, null, 2));
2875
+ else printError(updateRes.error);
2876
+ process.exit(1);
2877
+ }
2878
+ if (opts.json) console.log(JSON.stringify(updateRes.data.capability_card || nextCard, null, 2));
2879
+ else {
2880
+ console.log(`summary=${formatCapabilityCardSummary(updateRes.data.capability_card || nextCard)}`);
2881
+ console.log(JSON.stringify(updateRes.data.capability_card || nextCard, null, 2));
2882
+ }
2883
+ });
2884
+
2885
+ program
2886
+ .command('session-summary')
2887
+ .description('Show or update the local carry-forward summary for a session')
2888
+ .option('--session-key <key>', 'Exact session key')
2889
+ .option('--conversation-id <id>', 'Conversation ID')
2890
+ .option('--remote-did <did>', 'Remote DID')
2891
+ .option('--objective <text>', 'Current objective')
2892
+ .option('--context <text>', 'Current context')
2893
+ .option('--constraints <text>', 'Constraints')
2894
+ .option('--decisions <text>', 'Decisions already made')
2895
+ .option('--open-questions <text>', 'Open questions')
2896
+ .option('--next-action <text>', 'Next action')
2897
+ .option('--handoff-ready-text <text>', 'Handoff-ready summary text')
2898
+ .option('--clear-field <field>', 'Clear one summary field; repeatable', (value, acc) => {
2899
+ acc.push(value);
2900
+ return acc;
2901
+ }, [])
2902
+ .option('--json', 'Output as JSON')
2903
+ .action(async (opts) => {
2904
+ const identityPath = getIdentityPathForCommand(opts);
2905
+ const store = openStore(identityPath);
2906
+ try {
2907
+ const session = resolveSessionFromStore(store, {
2908
+ sessionKey: opts.sessionKey,
2909
+ conversationId: opts.conversationId,
2910
+ remoteDid: opts.remoteDid,
2911
+ });
2912
+ if (!session) {
2913
+ console.error('No session found. Use pingagent host tui or pingagent recent sessions first.');
2914
+ process.exit(1);
2915
+ }
2916
+ const manager = new SessionSummaryManager(store);
2917
+ const clearFields = Array.isArray(opts.clearField)
2918
+ ? opts.clearField
2919
+ .map((value) => String(value || '').trim())
2920
+ .filter((value) => SESSION_SUMMARY_FIELDS.includes(value))
2921
+ : [];
2922
+ if (Array.isArray(opts.clearField) && opts.clearField.length !== clearFields.length) {
2923
+ console.error(`clear-field must be one of: ${SESSION_SUMMARY_FIELDS.join(', ')}`);
2924
+ process.exit(1);
2925
+ }
2926
+ const shouldUpdate = [
2927
+ opts.objective,
2928
+ opts.context,
2929
+ opts.constraints,
2930
+ opts.decisions,
2931
+ opts.openQuestions,
2932
+ opts.nextAction,
2933
+ opts.handoffReadyText,
2934
+ ].some((value) => value !== undefined) || clearFields.length > 0;
2935
+ let summary = shouldUpdate
2936
+ ? manager.upsert({
2937
+ session_key: session.session_key,
2938
+ objective: opts.objective,
2939
+ context: opts.context,
2940
+ constraints: opts.constraints,
2941
+ decisions: opts.decisions,
2942
+ open_questions: opts.openQuestions,
2943
+ next_action: opts.nextAction,
2944
+ handoff_ready_text: opts.handoffReadyText,
2945
+ })
2946
+ : manager.get(session.session_key);
2947
+ if (clearFields.length > 0) {
2948
+ summary = manager.clearFields(session.session_key, clearFields);
2949
+ }
2950
+ if (opts.json) {
2951
+ console.log(JSON.stringify({ session, summary }, null, 2));
2952
+ } else {
2953
+ console.log(`session=${session.session_key}`);
2954
+ console.log(`conversation=${session.conversation_id || '(none)'}`);
2955
+ console.log(`remote=${session.remote_did || '(unknown)'}`);
2956
+ console.log(JSON.stringify(summary, null, 2));
2957
+ }
2958
+ } finally {
2959
+ store.close();
2960
+ }
2961
+ });
2962
+
2963
+ program
2964
+ .command('handoff')
2965
+ .description('Send a first-class delegation / handoff task using the current task-thread transport')
2966
+ .requiredOption('--to <target>', 'Target DID, alias, public slug, or connectable identity')
2967
+ .requiredOption('--title <title>', 'Task title')
2968
+ .option('--description <text>', 'Task description')
2969
+ .option('--objective <text>', 'Delegation objective')
2970
+ .option('--success-criteria <text>', 'Success criteria')
2971
+ .option('--priority <text>', 'Priority label')
2972
+ .option('--carry-forward-summary <text>', 'Explicit carry-forward summary; otherwise use the session summary')
2973
+ .option('--callback-session-key <key>', 'Callback session key to reference in the handoff')
2974
+ .option('--session-key <key>', 'Source session key whose summary should be used')
2975
+ .option('--conversation-id <id>', 'Source conversation ID whose summary should be used')
2976
+ .option('--remote-did <did>', 'Source remote DID whose summary should be used')
2977
+ .option('--json', 'Output as JSON')
2978
+ .action(async (opts) => {
2979
+ const identityPath = getIdentityPathForCommand(opts);
2980
+ const { client, store } = await getClientWithStore(identityPath);
2981
+ try {
2982
+ const targetDid = await resolveTarget(client, opts.to);
2983
+ const sourceSession = resolveSessionFromStore(store, {
2984
+ sessionKey: opts.sessionKey,
2985
+ conversationId: opts.conversationId,
2986
+ remoteDid: opts.remoteDid,
2987
+ });
2988
+ const result = await client.sendHandoff(targetDid, {
2989
+ title: opts.title,
2990
+ description: opts.description,
2991
+ objective: opts.objective,
2992
+ carry_forward_summary: opts.carryForwardSummary,
2993
+ success_criteria: opts.successCriteria,
2994
+ callback_session_key: opts.callbackSessionKey || sourceSession?.session_key,
2995
+ priority: opts.priority,
2996
+ }, {
2997
+ sessionKey: sourceSession?.session_key,
2998
+ conversationId: sourceSession?.conversation_id,
2999
+ });
3000
+ if (!result.ok || !result.data) {
3001
+ if (opts.json) console.log(JSON.stringify(result, null, 2));
3002
+ else printError(result.error);
3003
+ process.exit(1);
3004
+ }
3005
+ if (opts.json) {
3006
+ console.log(JSON.stringify({
3007
+ target_did: targetDid,
3008
+ source_session_key: sourceSession?.session_key ?? null,
3009
+ ...result.data,
3010
+ }, null, 2));
3011
+ } else {
3012
+ console.log(`task_id=${result.data.task_id}`);
3013
+ console.log(`conversation_id=${result.data.conversation_id}`);
3014
+ console.log(`target_did=${targetDid}`);
3015
+ console.log(`source_session_key=${sourceSession?.session_key || '(none)'}`);
3016
+ console.log(JSON.stringify(result.data.handoff, null, 2));
3017
+ }
3018
+ } finally {
3019
+ store.close();
3020
+ }
3021
+ });
3022
+
3023
+ program
3024
+ .command('demo')
3025
+ .description('Open or message the official PingAgent demo agent')
3026
+ .option('--preset <name>', 'Preset first message: hello, delegate, or trust')
3027
+ .option('--message <text>', 'Optional first message to send immediately')
3028
+ .option('--json', 'Output as JSON')
3029
+ .action(async (opts) => {
3030
+ const client = await getClient();
3031
+ const resolved = await client.resolveAlias('pingagent/demo');
3032
+ if (!resolved.ok || !resolved.data?.did) {
3033
+ if (opts.json) console.log(JSON.stringify(resolved, null, 2));
3034
+ else printError(resolved.error);
3035
+ process.exit(1);
3036
+ }
3037
+ const convo = await client.openConversation(resolved.data.did);
3038
+ if (!convo.ok || !convo.data) {
3039
+ if (opts.json) console.log(JSON.stringify(convo, null, 2));
3040
+ else printError(convo.error);
3041
+ process.exit(1);
3042
+ }
3043
+ const presetMessages = {
3044
+ hello: 'Hello',
3045
+ delegate: 'Please show me how task delegation works in PingAgent.',
3046
+ trust: 'Show me how trust decisions and recommendations work.',
3047
+ };
3048
+ const effectiveMessage = typeof opts.message === 'string' && opts.message.trim()
3049
+ ? opts.message
3050
+ : (typeof opts.preset === 'string' && presetMessages[opts.preset.trim().toLowerCase()] ? presetMessages[opts.preset.trim().toLowerCase()] : '');
3051
+ if (effectiveMessage) {
3052
+ const sendRes = await client.sendMessage(convo.data.conversation_id, SCHEMA_TEXT, { text: effectiveMessage });
3053
+ if (!sendRes.ok) {
3054
+ if (opts.json) console.log(JSON.stringify(sendRes, null, 2));
3055
+ else printError(sendRes.error);
3056
+ process.exit(1);
3057
+ }
3058
+ if (opts.json) console.log(JSON.stringify({ did: resolved.data.did, conversation_id: convo.data.conversation_id, message_id: sendRes.data?.message_id, preset: opts.preset ?? null }, null, 2));
3059
+ else console.log(`Demo agent messaged. conversation=${convo.data.conversation_id} message=${sendRes.data?.message_id}`);
3060
+ return;
3061
+ }
3062
+ if (opts.json) console.log(JSON.stringify({ did: resolved.data.did, conversation_id: convo.data.conversation_id }, null, 2));
3063
+ else console.log(`Demo agent ready. did=${resolved.data.did} conversation=${convo.data.conversation_id}`);
3064
+ });
3065
+
3066
+ program
3067
+ .command('connect')
3068
+ .description('Consume a PingAgent public link or contact card and open a conversation')
3069
+ .argument('<target>', 'Share URL, contact card URL, public slug, alias, or DID')
3070
+ .option('--message <text>', 'Optional first message to send immediately')
3071
+ .option('--json', 'Output as JSON')
3072
+ .action(async (target, opts) => {
3073
+ const client = await getClient();
3074
+ let targetDid = '';
3075
+ let prefMessage = typeof opts.message === 'string' ? opts.message : '';
3076
+ try {
3077
+ if (/^https?:\/\//.test(target)) {
3078
+ const url = new URL(target);
3079
+ const parts = url.pathname.split('/').filter(Boolean);
3080
+ if (parts[0] === 'c' && parts[1]) {
3081
+ const card = await client.getContactCard(parts[1]);
3082
+ if (!card.ok || !card.data) {
3083
+ if (opts.json) console.log(JSON.stringify(card, null, 2));
3084
+ else printError(card.error);
3085
+ process.exit(1);
3086
+ }
3087
+ targetDid = card.data.target_did;
3088
+ if (!prefMessage && card.data.message_template) prefMessage = card.data.message_template;
3089
+ } else if (parts[0] === 'a' && parts[1]) {
3090
+ const agent = await client.getPublicAgent(parts[1]);
3091
+ if (!agent.ok || !agent.data?.did) {
3092
+ if (opts.json) console.log(JSON.stringify(agent, null, 2));
3093
+ else printError(agent.error);
3094
+ process.exit(1);
3095
+ }
3096
+ targetDid = agent.data.did;
3097
+ } else if (parts[0] === 'connect' && parts[1]) {
3098
+ const requestedTarget = decodeURIComponent(parts[1]);
3099
+ if (!prefMessage && url.searchParams.get('message')) {
3100
+ prefMessage = url.searchParams.get('message') || '';
3101
+ }
3102
+ if (requestedTarget.startsWith('did:agent:')) {
3103
+ targetDid = requestedTarget;
3104
+ } else if (requestedTarget.startsWith('@')) {
3105
+ const resolved = await client.resolveAlias(requestedTarget.slice(1));
3106
+ if (!resolved.ok || !resolved.data?.did) {
3107
+ if (opts.json) console.log(JSON.stringify(resolved, null, 2));
3108
+ else printError(resolved.error);
3109
+ process.exit(1);
3110
+ }
3111
+ targetDid = resolved.data.did;
3112
+ } else {
3113
+ const agent = await client.getPublicAgent(requestedTarget);
3114
+ if (!agent.ok || !agent.data?.did) {
3115
+ if (opts.json) console.log(JSON.stringify(agent, null, 2));
3116
+ else printError(agent.error);
3117
+ process.exit(1);
3118
+ }
3119
+ targetDid = agent.data.did;
3120
+ }
3121
+ }
3122
+ }
3123
+ } catch {
3124
+ // fall through to text target handling
3125
+ }
3126
+ if (!targetDid) {
3127
+ if (String(target).startsWith('did:agent:')) {
3128
+ targetDid = String(target);
3129
+ } else if (String(target).startsWith('@')) {
3130
+ const resolved = await client.resolveAlias(String(target).slice(1));
3131
+ if (!resolved.ok || !resolved.data?.did) {
3132
+ if (opts.json) console.log(JSON.stringify(resolved, null, 2));
3133
+ else printError(resolved.error);
3134
+ process.exit(1);
3135
+ }
3136
+ targetDid = resolved.data.did;
3137
+ } else {
3138
+ const agent = await client.getPublicAgent(String(target));
3139
+ if (!agent.ok || !agent.data?.did) {
3140
+ if (opts.json) console.log(JSON.stringify(agent, null, 2));
3141
+ else printError(agent.error);
3142
+ process.exit(1);
3143
+ }
3144
+ targetDid = agent.data.did;
3145
+ }
3146
+ }
3147
+ const convo = await client.openConversation(targetDid);
3148
+ if (!convo.ok || !convo.data) {
3149
+ if (opts.json) console.log(JSON.stringify(convo, null, 2));
3150
+ else printError(convo.error);
3151
+ process.exit(1);
3152
+ }
3153
+ if (prefMessage) {
3154
+ const sendRes = await client.sendMessage(convo.data.conversation_id, SCHEMA_TEXT, { text: prefMessage });
3155
+ if (!sendRes.ok) {
3156
+ if (opts.json) console.log(JSON.stringify(sendRes, null, 2));
3157
+ else printError(sendRes.error);
3158
+ process.exit(1);
3159
+ }
3160
+ if (opts.json) console.log(JSON.stringify({ did: targetDid, conversation_id: convo.data.conversation_id, message_id: sendRes.data?.message_id }, null, 2));
3161
+ else console.log(`Connected and sent first message. conversation=${convo.data.conversation_id} did=${targetDid}`);
3162
+ return;
3163
+ }
3164
+ if (opts.json) console.log(JSON.stringify({ did: targetDid, conversation_id: convo.data.conversation_id }, null, 2));
3165
+ else console.log(`Connected. conversation=${convo.data.conversation_id} did=${targetDid}`);
3166
+ });
3167
+
3168
+ async function runHostBootstrap(opts) {
3169
+ const validTemplates = new Set(['launchd', 'systemd', 'docker', 'pm2', 'supervisord']);
3170
+ const template = opts.template ? String(opts.template).trim() : '';
3171
+ if (template && !validTemplates.has(template)) {
3172
+ console.error(`Unsupported template: ${template}. Valid: ${Array.from(validTemplates).join(', ')}`);
3173
+ process.exit(1);
3174
+ }
3175
+
3176
+ const steps = [];
3177
+ const runStep = (label, args) => {
3178
+ const result = runOpenClawInstall(args);
3179
+ steps.push({ label, args, result });
3180
+ return { label, args, result };
3181
+ };
3182
+
3183
+ const installStep = runStep('install', []);
3184
+ const hooksStep = installStep.result.ok ? runStep('hooks repair', ['fix-hooks']) : null;
3185
+ const verifyStep = installStep.result.ok && hooksStep?.result.ok
3186
+ ? runStep('runtime verify', ['verify-runtime', '--fix-hooks'])
3187
+ : null;
3188
+
3189
+ let runnerStep = null;
3190
+ if (verifyStep?.result.ok && opts.write) {
3191
+ const runnerArgs = ['init-runner', '--ingress', '--panel'];
3192
+ if (template) runnerArgs.push('--template', template);
3193
+ const supportsWrite = !template || template === 'launchd' || template === 'systemd';
3194
+ if (supportsWrite) runnerArgs.push('--write');
3195
+ runnerStep = runStep(supportsWrite ? 'runner setup' : 'runner template', runnerArgs);
3196
+ }
3197
+
3198
+ if (runnerStep?.result.stdout && opts.write) {
3199
+ console.log(runnerStep.result.stdout.trim());
3200
+ if (runnerStep.result.stderr.trim()) console.error(runnerStep.result.stderr.trim());
3201
+ console.log('');
3202
+ }
3203
+
3204
+ const failed = steps.find((step) => !step.result.ok) ?? null;
3205
+ const formatStepStatus = (step, fallback = 'skipped') => (step ? (step.result.ok ? 'ok' : 'failed') : fallback);
3206
+ const runnerStatus = !opts.write
3207
+ ? 'not_written'
3208
+ : !runnerStep
3209
+ ? 'skipped'
3210
+ : !runnerStep.result.ok
3211
+ ? 'failed'
3212
+ : (template && template !== 'launchd' && template !== 'systemd')
3213
+ ? 'template_printed_not_started'
3214
+ : 'written_not_started';
3215
+ const installerSource = installStep.result.source || 'unknown';
3216
+
3217
+ console.log('PingAgent Host Bootstrap');
3218
+ console.log('========================');
3219
+ console.log(`install=${formatStepStatus(installStep)}`);
3220
+ console.log(`hooks_repair=${formatStepStatus(hooksStep)}`);
3221
+ console.log(`runtime_verify=${formatStepStatus(verifyStep)}`);
3222
+ console.log(`installer_source=${installerSource}`);
3223
+ console.log(`runner=${runnerStatus}`);
3224
+ console.log(`host_panel_url=${getHostPanelSurfaceUrl()}`);
3225
+ console.log('host_panel_started_by_bootstrap=false');
3226
+ console.log('host_panel_start=npx @pingagent/sdk web');
3227
+ console.log('host_panel_start_local=pingagent web');
3228
+ console.log('tui=npx @pingagent/sdk host tui');
3229
+ console.log('tui_local=pingagent host tui');
3230
+ console.log('');
3231
+ console.log('Control surfaces:');
3232
+ for (const line of getSurfaceRecommendationLines('npx @pingagent/sdk', 'pingagent')) console.log(line);
3233
+ console.log('');
3234
+ if (!opts.write) {
3235
+ console.log('Next steps:');
3236
+ console.log(' Bootstrap validates and repairs config, but it does not start long-lived daemons.');
3237
+ console.log(' Start the Host Panel now with: npx @pingagent/sdk web (or pingagent web)');
3238
+ console.log(' Use the headless surface now with: npx @pingagent/sdk host tui (or pingagent host tui)');
3239
+ console.log(` Re-run with: npx @pingagent/sdk host bootstrap --write${template ? ` --template ${template}` : ''}`);
3240
+ console.log(' Manual path: npx @pingagent/openclaw-install init-runner --ingress --panel');
3241
+ } else if (runnerStep?.result.ok) {
3242
+ console.log('Next steps:');
3243
+ console.log(' Runner files/templates were generated, but bootstrap did not start those services.');
3244
+ if (!template || template === 'launchd' || template === 'systemd') {
3245
+ console.log(' Follow the printed launchctl/systemctl instructions to start them.');
3246
+ } else {
3247
+ console.log(' Start the generated runner with your chosen process manager.');
3248
+ }
3249
+ }
3250
+
3251
+ if (failed) {
3252
+ const stdout = failed.result.stdout.trim();
3253
+ const stderr = failed.result.stderr.trim();
3254
+ console.error('');
3255
+ console.error(`Bootstrap failed during ${failed.label}.`);
3256
+ if (stdout) console.error(stdout);
3257
+ if (stderr) console.error(stderr);
3258
+ process.exit(1);
3259
+ }
3260
+ }
3261
+
2252
3262
  const host = program
2253
3263
  .command('host')
2254
- .description('Headless runtime inspection and control for PingAgent host state');
3264
+ .description('OpenClaw host activation and operator control surfaces for GUI, headless, and low-token workflows');
3265
+
3266
+ host
3267
+ .command('bootstrap')
3268
+ .description('Run the idempotent OpenClaw activation flow and print the recommended Host Panel / TUI surfaces')
3269
+ .option('--write', 'Write launchd/systemd runner files when supported')
3270
+ .option('--template <name>', 'Runner template: launchd, systemd, docker, pm2, or supervisord')
3271
+ .action(async (opts) => {
3272
+ await runHostBootstrap(opts);
3273
+ });
2255
3274
 
2256
3275
  host
2257
3276
  .command('tui')
2258
- .description('Start a terminal UI for runtime, sessions, bindings, and rebind actions')
3277
+ .description('Start the headless / low-token terminal UI for runtime, sessions, chat links, and repair actions')
2259
3278
  .option('--once', 'Print one snapshot and exit')
2260
3279
  .option('--refresh-ms <ms>', 'Refresh interval in interactive mode', '2000')
2261
3280
  .option('--profile <name>', 'Use profile from ~/.pingagent/<name>')
@@ -2270,7 +3289,7 @@ host
2270
3289
 
2271
3290
  program
2272
3291
  .command('web')
2273
- .description('Start local web UI and host panel for debugging, runtime inspection, trust policy, and audit. By default scans ~/.pingagent for profiles; use --identity-dir to lock to one profile.')
3292
+ .description('Start the Host Panel, the primary GUI surface for runtime inspection, trust policy, and repair. Use pingagent host tui for headless or low-token operation.')
2274
3293
  .option('--port <port>', 'Port for the web server', '3846')
2275
3294
  .action(async (opts) => {
2276
3295
  const serverUrl = process.env.PINGAGENT_SERVER_URL || DEFAULT_SERVER;