@pingagent/sdk 0.1.10 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/pingagent.js CHANGED
@@ -4,6 +4,7 @@ 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';
7
8
  import {
8
9
  PingAgentClient,
9
10
  generateIdentity,
@@ -17,10 +18,16 @@ import {
17
18
  HistoryManager,
18
19
  A2AAdapter,
19
20
  SessionManager,
21
+ SessionSummaryManager,
20
22
  TaskThreadManager,
23
+ TaskHandoffManager,
21
24
  TrustPolicyAuditManager,
25
+ TrustRecommendationManager,
26
+ getTrustRecommendationActionLabel,
27
+ formatCapabilityCardSummary,
22
28
  defaultTrustPolicyDoc,
23
29
  normalizeTrustPolicyDoc,
30
+ upsertTrustPolicyRecommendation,
24
31
  getActiveSessionFilePath,
25
32
  getSessionMapFilePath,
26
33
  getSessionBindingAlertsFilePath,
@@ -29,12 +36,16 @@ import {
29
36
  readSessionBindingAlerts,
30
37
  setSessionBinding,
31
38
  removeSessionBinding,
39
+ readIngressRuntimeStatus,
32
40
  } from '../dist/index.js';
33
41
  import { ERROR_HINTS, SCHEMA_TEXT } from '@pingagent/schemas';
34
42
 
35
43
  const DEFAULT_SERVER = 'https://pingagent.chat';
36
44
  const UPGRADE_URL = 'https://pingagent.chat';
37
45
  const DEFAULT_IDENTITY_PATH = path.join(os.homedir(), '.pingagent', 'identity.json');
46
+ const OFFICIAL_HOSTED_ORIGIN = new URL(DEFAULT_SERVER).origin;
47
+ const hostedPublicLinkAttempts = new Set();
48
+ const SESSION_SUMMARY_FIELDS = ['objective', 'context', 'constraints', 'decisions', 'open_questions', 'next_action', 'handoff_ready_text'];
38
49
 
39
50
  function resolvePath(p) {
40
51
  if (!p) return p;
@@ -42,6 +53,18 @@ function resolvePath(p) {
42
53
  return p;
43
54
  }
44
55
 
56
+ function normalizeOrigin(input) {
57
+ try {
58
+ return new URL(String(input ?? '')).origin;
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+
64
+ function isOfficialHostedServer(serverUrl) {
65
+ return normalizeOrigin(serverUrl) === OFFICIAL_HOSTED_ORIGIN;
66
+ }
67
+
45
68
  function getEffectiveIdentityPath() {
46
69
  const dir = program.opts().identityDir;
47
70
  if (dir) return path.join(resolvePath(dir), 'identity.json');
@@ -108,6 +131,38 @@ function readTrustPolicyDoc(identityPath) {
108
131
  }
109
132
  }
110
133
 
134
+ function writeTrustPolicyDoc(identityPath, doc) {
135
+ const policyPath = getTrustPolicyPath(identityPath);
136
+ fs.mkdirSync(path.dirname(policyPath), { recursive: true, mode: 0o700 });
137
+ fs.writeFileSync(policyPath, JSON.stringify(normalizeTrustPolicyDoc(doc), null, 2), 'utf-8');
138
+ return policyPath;
139
+ }
140
+
141
+ function findOpenClawInstallScript() {
142
+ const explicit = process.env.PINGAGENT_OPENCLAW_INSTALL_BIN;
143
+ if (explicit) return { cmd: process.execPath, args: [resolvePath(explicit)] };
144
+ const repoScript = path.resolve(process.cwd(), 'packages', 'openclaw-install', 'install.mjs');
145
+ if (fs.existsSync(repoScript)) return { cmd: process.execPath, args: [repoScript] };
146
+ return null;
147
+ }
148
+
149
+ function runOpenClawInstall(args) {
150
+ const resolved = findOpenClawInstallScript();
151
+ if (!resolved) {
152
+ return { ok: false, stdout: '', stderr: 'OpenClaw installer script not found locally. Set PINGAGENT_OPENCLAW_INSTALL_BIN.' };
153
+ }
154
+ const result = spawnSync(resolved.cmd, [...resolved.args, ...args], {
155
+ encoding: 'utf-8',
156
+ env: process.env,
157
+ });
158
+ return {
159
+ ok: result.status === 0,
160
+ stdout: String(result.stdout ?? ''),
161
+ stderr: String(result.stderr ?? ''),
162
+ status: result.status ?? 1,
163
+ };
164
+ }
165
+
111
166
  function clearScreen() {
112
167
  process.stdout.write('\x1Bc');
113
168
  }
@@ -173,6 +228,7 @@ function buildTaskExportBody(task, format = 'plain') {
173
228
  message: task.error_message,
174
229
  }
175
230
  : null,
231
+ handoff: task.handoff || null,
176
232
  };
177
233
  if (format === 'json') return JSON.stringify(payload, null, 2);
178
234
  return [
@@ -232,7 +288,9 @@ function buildHostState(identityPath, selectedSessionKey = null, historyPageInde
232
288
  const store = openStore(identityPath);
233
289
  try {
234
290
  const sessionManager = new SessionManager(store);
291
+ const sessionSummaryManager = new SessionSummaryManager(store);
235
292
  const taskManager = new TaskThreadManager(store);
293
+ const taskHandoffManager = new TaskHandoffManager(store);
236
294
  const historyManager = new HistoryManager(store);
237
295
  const auditManager = new TrustPolicyAuditManager(store);
238
296
  const sessions = sessionManager.listRecentSessions(50);
@@ -241,6 +299,7 @@ function buildHostState(identityPath, selectedSessionKey = null, historyPageInde
241
299
  const bindingByConversation = new Map(bindings.map((row) => [row.conversation_id, row]));
242
300
  const alertByConversation = new Map(alerts.map((row) => [row.conversation_id, row]));
243
301
  const activeChatSession = readCurrentActiveSessionKey();
302
+ const ingressRuntime = readIngressRuntimeStatus();
244
303
  const desiredSelectedSessionKey =
245
304
  (selectedSessionKey && sessions.some((session) => session.session_key === selectedSessionKey) ? selectedSessionKey : null) ??
246
305
  sessionManager.getActiveSession()?.session_key ??
@@ -255,8 +314,14 @@ function buildHostState(identityPath, selectedSessionKey = null, historyPageInde
255
314
  is_active_chat_session: session.session_key === activeChatSession,
256
315
  }));
257
316
  const selectedSession = sessionsWithMeta.find((session) => session.session_key === desiredSelectedSessionKey) ?? null;
317
+ const selectedSessionSummary = selectedSession
318
+ ? sessionSummaryManager.get(selectedSession.session_key)
319
+ : null;
258
320
  const selectedTasks = selectedSession
259
- ? taskManager.listBySession(selectedSession.session_key, 12)
321
+ ? taskManager.listBySession(selectedSession.session_key, 12).map((task) => ({
322
+ ...task,
323
+ handoff: taskHandoffManager.get(task.task_id),
324
+ }))
260
325
  : [];
261
326
  const selectedAuditEvents = selectedSession
262
327
  ? auditManager.listBySession(selectedSession.session_key, 12)
@@ -270,27 +335,45 @@ function buildHostState(identityPath, selectedSessionKey = null, historyPageInde
270
335
  const selectedHistorySearchResults = selectedSession?.conversation_id && historySearchQuery.trim()
271
336
  ? historyManager.search(historySearchQuery.trim(), { conversationId: selectedSession.conversation_id, limit: 50 })
272
337
  : [];
338
+ const recommendationManager = new TrustRecommendationManager(store);
339
+ recommendationManager.sync({
340
+ policyDoc: policy.doc,
341
+ sessions,
342
+ tasks: taskManager.listRecent(100),
343
+ auditEvents: auditManager.listRecent(200),
344
+ runtimeMode,
345
+ limit: 50,
346
+ });
347
+ const selectedRecommendations = selectedSession?.remote_did
348
+ ? recommendationManager.list({ remoteDid: selectedSession.remote_did, limit: 10 })
349
+ : [];
273
350
  const unreadTotal = sessionsWithMeta.reduce((sum, session) => sum + (session.unread_count ?? 0), 0);
274
351
  const alertSessions = sessionsWithMeta.filter((session) => !!session.binding_alert).length;
275
352
  return {
276
353
  identity,
277
354
  runtimeMode,
278
355
  activeChatSession,
356
+ ingressRuntime,
279
357
  activeChatSessionFile: getActiveSessionFilePath(),
280
358
  sessionMapPath: getSessionMapFilePath(),
281
359
  sessionBindingAlertsPath: getSessionBindingAlertsFilePath(),
282
360
  policyPath: policy.path,
283
361
  policyDoc: policy.doc,
284
362
  sessions: sessionsWithMeta,
285
- tasks: taskManager.listRecent(30),
363
+ tasks: taskManager.listRecent(30).map((task) => ({
364
+ ...task,
365
+ handoff: taskHandoffManager.get(task.task_id),
366
+ })),
286
367
  auditEvents: auditManager.listRecent(40),
287
368
  selectedSession,
369
+ selectedSessionSummary,
288
370
  selectedTasks,
289
371
  selectedAuditEvents,
290
372
  selectedMessages,
291
373
  selectedHistoryPage,
292
374
  selectedHistorySearchQuery: historySearchQuery.trim(),
293
375
  selectedHistorySearchResults,
376
+ selectedRecommendations,
294
377
  unreadTotal,
295
378
  alertSessions,
296
379
  };
@@ -302,9 +385,13 @@ function buildHostState(identityPath, selectedSessionKey = null, historyPageInde
302
385
  function renderHostTuiScreen(hostState, uiState) {
303
386
  const sessions = hostState.sessions || [];
304
387
  const selected = hostState.selectedSession || sessions[0] || null;
388
+ const selectedSummary = hostState.selectedSessionSummary || null;
305
389
  const tasks = hostState.selectedTasks || [];
306
390
  const auditEvents = hostState.selectedAuditEvents || [];
307
391
  const messages = hostState.selectedMessages || [];
392
+ const recommendations = hostState.selectedRecommendations || [];
393
+ const openRecommendation = recommendations.find((item) => item.status === 'open') || null;
394
+ const reopenRecommendation = recommendations.find((item) => item.status === 'dismissed' || item.status === 'superseded') || null;
308
395
  const historyPage = hostState.selectedHistoryPage || { messages: [], pageIndex: 0, hasOlder: false, hasNewer: false };
309
396
  const historySearchQuery = hostState.selectedHistorySearchQuery || '';
310
397
  const historySearchResults = hostState.selectedHistorySearchResults || [];
@@ -315,17 +402,24 @@ function renderHostTuiScreen(hostState, uiState) {
315
402
  ? ` (${Math.max(0, Math.ceil((uiState.statusExpiresAt - Date.now()) / 1000))}s)`
316
403
  : '';
317
404
  const statusTs = uiState?.statusAt ? ` @ ${formatStatusTimestamp(uiState.statusAt)}` : '';
405
+ const degraded = !hostState.ingressRuntime
406
+ || hostState.ingressRuntime.receive_mode === 'polling_degraded'
407
+ || !!hostState.ingressRuntime.hooks_last_error;
408
+ const ingressLabel = degraded ? 'Degraded' : 'Ready';
318
409
  const lines = [
319
410
  'PingAgent Host TUI',
320
411
  `DID: ${hostState.identity.did}`,
321
412
  `status=${formatStatusLine(uiState?.statusLevel || 'info', uiState?.statusMessage || '(ready)')}${statusTs}${statusCountdown}`,
322
- `runtime_mode=${hostState.runtimeMode} active_chat_session=${hostState.activeChatSession || '(none)'}`,
413
+ `runtime_mode=${hostState.runtimeMode} receive_mode=${hostState.ingressRuntime?.receive_mode || 'webhook'} active_chat_session=${hostState.activeChatSession || '(none)'}`,
414
+ `ingress=${ingressLabel}${degraded ? ' action=[f] fix-now' : ''}`,
415
+ uiState?.publicLinkUrl ? `public_link=${uiState.publicLinkUrl}` : null,
323
416
  `sessions=${sessions.length} unread_total=${hostState.unreadTotal ?? 0} alert_sessions=${hostState.alertSessions ?? 0} view=${view}`,
324
417
  `policy=${hostState.policyPath}`,
325
418
  `session_map=${hostState.sessionMapPath}`,
326
419
  `binding_alerts=${hostState.sessionBindingAlertsPath}`,
420
+ hostState.ingressRuntime?.hooks_last_error ? `hooks_error=${truncateLine(hostState.ingressRuntime.hooks_last_error, 120)}` : null,
327
421
  '',
328
- ];
422
+ ].filter(Boolean);
329
423
 
330
424
  if (view === 'help') {
331
425
  lines.push('Help');
@@ -339,10 +433,16 @@ function renderHostTuiScreen(hostState, uiState) {
339
433
  lines.push('- t: open task list view for selected session');
340
434
  lines.push('- x: cancel selected task (in task views)');
341
435
  lines.push('- p: multiline reply prompt (detail view)');
436
+ lines.push('- S: edit carry-forward summary (detail view)');
437
+ lines.push('- d: try demo agent preset');
342
438
  lines.push('- o: open local history paging (detail view)');
343
439
  lines.push('- n / p: older / newer history page (history view)');
344
440
  lines.push('- s or /: search local history (history view)');
345
441
  lines.push('- y: dump task detail to stdout (task-detail view, choose json/plain)');
442
+ lines.push('- f: repair OpenClaw hooks config');
443
+ lines.push('- A: apply first open trust recommendation for selected session');
444
+ lines.push('- D: dismiss current open recommendation');
445
+ lines.push('- R: reopen dismissed/superseded recommendation');
346
446
  lines.push('- b: bind selected conversation to current chat session');
347
447
  lines.push('- c: clear selected binding');
348
448
  lines.push('- q: quit');
@@ -382,8 +482,17 @@ function renderHostTuiScreen(hostState, uiState) {
382
482
  lines.push(`status=${selectedTask.status}`);
383
483
  lines.push(`updated_at=${formatTs(selectedTask.updated_at, false)}`);
384
484
  if (selectedTask.started_at) lines.push(`started_at=${formatTs(selectedTask.started_at, false)}`);
485
+ if (selectedTask.handoff?.objective) lines.push(`handoff_objective=${selectedTask.handoff.objective}`);
486
+ if (selectedTask.handoff?.priority) lines.push(`handoff_priority=${selectedTask.handoff.priority}`);
487
+ if (selectedTask.handoff?.success_criteria) lines.push(`handoff_success=${selectedTask.handoff.success_criteria}`);
488
+ if (selectedTask.handoff?.callback_session_key) lines.push(`handoff_callback=${selectedTask.handoff.callback_session_key}`);
385
489
  lines.push('actions=[x] cancel-task [y] dump-stdout [j/k] switch-task [h/Esc] back-to-tasks');
386
490
  lines.push('');
491
+ if (selectedTask.handoff?.carry_forward_summary) {
492
+ lines.push('Handoff Summary');
493
+ lines.push(selectedTask.handoff.carry_forward_summary);
494
+ lines.push('');
495
+ }
387
496
  lines.push('Result');
388
497
  lines.push(selectedTask.result_summary || '(none)');
389
498
  lines.push('');
@@ -409,6 +518,8 @@ function renderHostTuiScreen(hostState, uiState) {
409
518
  lines.push(`status=${selectedTask.status}`);
410
519
  lines.push(`updated_at=${formatTs(selectedTask.updated_at, false)}`);
411
520
  if (selectedTask.started_at) lines.push(`started_at=${formatTs(selectedTask.started_at, false)}`);
521
+ if (selectedTask.handoff?.objective) lines.push(`handoff_objective=${selectedTask.handoff.objective}`);
522
+ if (selectedTask.handoff?.priority) lines.push(`handoff_priority=${selectedTask.handoff.priority}`);
412
523
  if (selectedTask.result_summary) lines.push(`result=${selectedTask.result_summary}`);
413
524
  if (selectedTask.error_code || selectedTask.error_message) {
414
525
  lines.push(`error=${selectedTask.error_code || 'E_TASK'} ${selectedTask.error_message || ''}`.trim());
@@ -440,10 +551,26 @@ function renderHostTuiScreen(hostState, uiState) {
440
551
  } else {
441
552
  lines.push('needs_rebind=false');
442
553
  }
554
+ if (openRecommendation) {
555
+ lines.push(`trust_action=${getTrustRecommendationActionLabel(openRecommendation)}`);
556
+ } else if (reopenRecommendation) {
557
+ lines.push(`trust_action=${getTrustRecommendationActionLabel(reopenRecommendation)}`);
558
+ }
559
+ if (selectedSummary) {
560
+ lines.push(`summary_objective=${selectedSummary.objective || '(none)'}`);
561
+ lines.push(`summary_next_action=${selectedSummary.next_action || '(none)'}`);
562
+ } else {
563
+ lines.push('summary_objective=(none)');
564
+ }
443
565
  const actionBar = [
444
566
  selected.trust_state === 'pending' ? '[a] approve' : null,
567
+ '[A] apply-rec',
568
+ '[D] dismiss-rec',
569
+ '[R] reopen-rec',
445
570
  '[m] mark-read',
571
+ '[d] demo',
446
572
  '[p] reply',
573
+ '[S] summary',
447
574
  '[o] history',
448
575
  '[t] tasks',
449
576
  '[b] bind-current',
@@ -451,9 +578,22 @@ function renderHostTuiScreen(hostState, uiState) {
451
578
  ].filter(Boolean).join(' ');
452
579
  lines.push(`actions=${actionBar}`);
453
580
  lines.push('');
581
+ lines.push('Carry-Forward Summary');
582
+ if (selectedSummary) {
583
+ lines.push(`- objective: ${selectedSummary.objective || '(none)'}`);
584
+ lines.push(`- context: ${truncateLine(selectedSummary.context || '(none)', 100)}`);
585
+ lines.push(`- constraints: ${truncateLine(selectedSummary.constraints || '(none)', 100)}`);
586
+ lines.push(`- decisions: ${truncateLine(selectedSummary.decisions || '(none)', 100)}`);
587
+ lines.push(`- open_questions: ${truncateLine(selectedSummary.open_questions || '(none)', 100)}`);
588
+ lines.push(`- next_action: ${truncateLine(selectedSummary.next_action || '(none)', 100)}`);
589
+ lines.push(`- handoff_ready: ${truncateLine(selectedSummary.handoff_ready_text || '(none)', 100)}`);
590
+ } else {
591
+ lines.push('- none');
592
+ }
593
+ lines.push('');
454
594
  lines.push('Tasks');
455
595
  lines.push(...(tasks.length
456
- ? tasks.map((task) => `- ${task.title || task.task_id} [${task.status}] ${truncateLine(task.result_summary || task.error_message || '', 80)}`)
596
+ ? 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
597
  : ['- none']));
458
598
  if (view === 'detail') {
459
599
  lines.push('');
@@ -471,7 +611,7 @@ function renderHostTuiScreen(hostState, uiState) {
471
611
  }
472
612
 
473
613
  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');
614
+ 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 bind c clear ? help q quit');
475
615
  return lines.join('\n');
476
616
  }
477
617
 
@@ -489,6 +629,7 @@ async function runHostTui(identityPath, opts) {
489
629
  statusAt: 0,
490
630
  selectedHistoryPageIndex: 0,
491
631
  historySearchQuery: '',
632
+ publicLinkUrl: '',
492
633
  };
493
634
 
494
635
  const render = () => {
@@ -511,6 +652,9 @@ async function runHostTui(identityPath, opts) {
511
652
  };
512
653
 
513
654
  if (once) {
655
+ void maybeEnsureHostedPublicLink(identityPath).then((res) => {
656
+ if (res?.data?.public_url) uiState.publicLinkUrl = res.data.public_url;
657
+ });
514
658
  const { screen } = render();
515
659
  console.log(screen);
516
660
  return;
@@ -546,6 +690,132 @@ async function runHostTui(identityPath, opts) {
546
690
  uiState.statusAt = Date.now();
547
691
  };
548
692
 
693
+ const applySessionRecommendation = (selected) => {
694
+ if (!selected?.remote_did) return { ok: false, message: 'No remote DID for selected session.' };
695
+ const store = openStore(identityPath);
696
+ try {
697
+ const { path: policyPath, doc } = readTrustPolicyDoc(identityPath);
698
+ const auditManager = new TrustPolicyAuditManager(store);
699
+ const recommendationManager = new TrustRecommendationManager(store);
700
+ recommendationManager.sync({
701
+ policyDoc: doc,
702
+ sessions: new SessionManager(store).listRecentSessions(100),
703
+ tasks: new TaskThreadManager(store).listRecent(100),
704
+ auditEvents: auditManager.listRecent(200),
705
+ runtimeMode: process.env.PINGAGENT_RUNTIME_MODE || 'bridge',
706
+ limit: 50,
707
+ });
708
+ const recommendation = recommendationManager.list({
709
+ remoteDid: selected.remote_did,
710
+ status: 'open',
711
+ limit: 1,
712
+ })[0];
713
+ if (!recommendation) return { ok: false, message: 'No open recommendation for this session.' };
714
+ const nextDoc = upsertTrustPolicyRecommendation(doc, recommendation);
715
+ writeTrustPolicyDoc(identityPath, nextDoc);
716
+ recommendationManager.apply(recommendation.id);
717
+ auditManager.record({
718
+ event_type: 'recommendation_applied',
719
+ policy_scope: recommendation.policy,
720
+ remote_did: recommendation.remote_did,
721
+ action: String(recommendation.action),
722
+ outcome: 'recommendation_applied',
723
+ explanation: recommendation.reason,
724
+ matched_rule: recommendation.match,
725
+ detail: { recommendation_id: recommendation.id, session_key: selected.session_key },
726
+ });
727
+ return { ok: true, message: `${getTrustRecommendationActionLabel(recommendation)} (${recommendation.policy})`, path: policyPath };
728
+ } finally {
729
+ store.close();
730
+ }
731
+ };
732
+
733
+ const dismissSessionRecommendation = (selected) => {
734
+ if (!selected?.remote_did) return { ok: false, message: 'No remote DID for selected session.' };
735
+ const store = openStore(identityPath);
736
+ try {
737
+ const recommendationManager = new TrustRecommendationManager(store);
738
+ const recommendation = recommendationManager.list({
739
+ remoteDid: selected.remote_did,
740
+ status: 'open',
741
+ limit: 1,
742
+ })[0];
743
+ if (!recommendation) return { ok: false, message: 'No open recommendation for this session.' };
744
+ recommendationManager.dismiss(recommendation.id);
745
+ new TrustPolicyAuditManager(store).record({
746
+ event_type: 'recommendation_dismissed',
747
+ policy_scope: recommendation.policy,
748
+ remote_did: recommendation.remote_did,
749
+ action: String(recommendation.action),
750
+ outcome: 'recommendation_dismissed',
751
+ explanation: recommendation.reason,
752
+ matched_rule: recommendation.match,
753
+ detail: { recommendation_id: recommendation.id, session_key: selected.session_key },
754
+ });
755
+ return { ok: true, message: `Dismissed ${getTrustRecommendationActionLabel(recommendation)}` };
756
+ } finally {
757
+ store.close();
758
+ }
759
+ };
760
+
761
+ const reopenSessionRecommendation = (selected) => {
762
+ if (!selected?.remote_did) return { ok: false, message: 'No remote DID for selected session.' };
763
+ const store = openStore(identityPath);
764
+ try {
765
+ const recommendationManager = new TrustRecommendationManager(store);
766
+ const recommendation = recommendationManager.list({
767
+ remoteDid: selected.remote_did,
768
+ status: ['dismissed', 'superseded'],
769
+ limit: 1,
770
+ })[0];
771
+ if (!recommendation) return { ok: false, message: 'No dismissed or superseded recommendation for this session.' };
772
+ recommendationManager.reopen(recommendation.id);
773
+ new TrustPolicyAuditManager(store).record({
774
+ event_type: 'recommendation_reopened',
775
+ policy_scope: recommendation.policy,
776
+ remote_did: recommendation.remote_did,
777
+ action: String(recommendation.action),
778
+ outcome: 'recommendation_reopened',
779
+ explanation: recommendation.reason,
780
+ matched_rule: recommendation.match,
781
+ detail: { recommendation_id: recommendation.id, session_key: selected.session_key },
782
+ });
783
+ return { ok: true, message: 'Reopened recommendation' };
784
+ } finally {
785
+ store.close();
786
+ }
787
+ };
788
+
789
+ const sendDemoPreset = async () => {
790
+ const answer = await promptLine('Demo preset [hello/delegate/trust] (default hello): ');
791
+ const preset = (answer.trim().toLowerCase() || 'hello');
792
+ const presetMessages = {
793
+ hello: 'Hello',
794
+ delegate: 'Please show me how task delegation works in PingAgent.',
795
+ trust: 'Show me how trust decisions and recommendations work.',
796
+ };
797
+ const message = presetMessages[preset] || presetMessages.hello;
798
+ const { client, store } = await getClientWithStore(identityPath);
799
+ try {
800
+ const resolved = await client.resolveAlias('pingagent/demo');
801
+ if (!resolved.ok || !resolved.data?.did) {
802
+ setStatus(`Demo resolve failed: ${resolved.error?.message || 'unknown error'}`, 'err');
803
+ return;
804
+ }
805
+ const convo = await client.openConversation(resolved.data.did);
806
+ if (!convo.ok || !convo.data?.conversation_id) {
807
+ setStatus(`Demo open failed: ${convo.error?.message || 'unknown error'}`, 'err');
808
+ return;
809
+ }
810
+ const sendRes = await client.sendMessage(convo.data.conversation_id, SCHEMA_TEXT, { text: message });
811
+ setStatus(sendRes.ok
812
+ ? `Demo preset sent (${preset}) conversation=${convo.data.conversation_id}`
813
+ : `Demo send failed: ${sendRes.error?.message || 'unknown error'}`, sendRes.ok ? 'ok' : 'err', sendRes.ok ? 7000 : 9000);
814
+ } finally {
815
+ store.close();
816
+ }
817
+ };
818
+
549
819
  const promptLine = async (question) => {
550
820
  stopInterval();
551
821
  if (process.stdin.setRawMode) process.stdin.setRawMode(false);
@@ -655,6 +925,13 @@ async function runHostTui(identityPath, opts) {
655
925
  };
656
926
 
657
927
  startInterval();
928
+ void maybeEnsureHostedPublicLink(identityPath).then((result) => {
929
+ if (result?.data?.public_url) {
930
+ uiState.publicLinkUrl = result.data.public_url;
931
+ setStatus(result.created ? `Public link ready: ${result.data.public_url}` : `Public link available: ${result.data.public_url}`, 'ok', 7000);
932
+ latestState = redraw();
933
+ }
934
+ });
658
935
 
659
936
  process.stdin.on('keypress', async (_str, key) => {
660
937
  if (key?.name === 'q' || (key?.ctrl && key?.name === 'c')) {
@@ -762,6 +1039,35 @@ async function runHostTui(identityPath, opts) {
762
1039
  latestState = redraw();
763
1040
  return;
764
1041
  }
1042
+ if (_str === 'A') {
1043
+ const selected = latestState.selectedSession;
1044
+ if (!selected) return;
1045
+ const result = applySessionRecommendation(selected);
1046
+ setStatus(result.message, result.ok ? 'ok' : 'warn');
1047
+ latestState = redraw();
1048
+ return;
1049
+ }
1050
+ if (_str === 'D') {
1051
+ const selected = latestState.selectedSession;
1052
+ if (!selected) return;
1053
+ const result = dismissSessionRecommendation(selected);
1054
+ setStatus(result.message, result.ok ? 'ok' : 'warn');
1055
+ latestState = redraw();
1056
+ return;
1057
+ }
1058
+ if (_str === 'R') {
1059
+ const selected = latestState.selectedSession;
1060
+ if (!selected) return;
1061
+ const result = reopenSessionRecommendation(selected);
1062
+ setStatus(result.message, result.ok ? 'ok' : 'warn');
1063
+ latestState = redraw();
1064
+ return;
1065
+ }
1066
+ if (key?.name === 'd') {
1067
+ await sendDemoPreset();
1068
+ latestState = redraw();
1069
+ return;
1070
+ }
765
1071
  if (key?.name === 'p' && uiState.view === 'detail') {
766
1072
  const selected = latestState.selectedSession;
767
1073
  if (!selected?.conversation_id) return;
@@ -784,6 +1090,51 @@ async function runHostTui(identityPath, opts) {
784
1090
  latestState = redraw();
785
1091
  return;
786
1092
  }
1093
+ if (_str === 'S' && uiState.view === 'detail') {
1094
+ const selected = latestState.selectedSession;
1095
+ if (!selected) return;
1096
+ const existing = latestState.selectedSessionSummary || {};
1097
+ const input = await promptMultiline(
1098
+ `Edit carry-forward summary as JSON for ${selected.remote_did || selected.session_key}\nCurrent:\n${JSON.stringify({
1099
+ objective: existing.objective || '',
1100
+ context: existing.context || '',
1101
+ constraints: existing.constraints || '',
1102
+ decisions: existing.decisions || '',
1103
+ open_questions: existing.open_questions || '',
1104
+ next_action: existing.next_action || '',
1105
+ handoff_ready_text: existing.handoff_ready_text || '',
1106
+ }, null, 2)}`
1107
+ );
1108
+ if (!input.trim()) {
1109
+ setStatus('Summary update cancelled.', 'warn');
1110
+ latestState = redraw();
1111
+ return;
1112
+ }
1113
+ try {
1114
+ const parsed = JSON.parse(input);
1115
+ const store = openStore(identityPath);
1116
+ try {
1117
+ const manager = new SessionSummaryManager(store);
1118
+ manager.upsert({
1119
+ session_key: selected.session_key,
1120
+ objective: parsed.objective,
1121
+ context: parsed.context,
1122
+ constraints: parsed.constraints,
1123
+ decisions: parsed.decisions,
1124
+ open_questions: parsed.open_questions,
1125
+ next_action: parsed.next_action,
1126
+ handoff_ready_text: parsed.handoff_ready_text,
1127
+ });
1128
+ } finally {
1129
+ store.close();
1130
+ }
1131
+ setStatus(`Saved carry-forward summary for ${selected.session_key}`, 'ok');
1132
+ } catch (error) {
1133
+ setStatus(`Summary update failed: ${error?.message || 'invalid JSON'}`, 'err', 9000);
1134
+ }
1135
+ latestState = redraw();
1136
+ return;
1137
+ }
787
1138
  if (key?.name === 'o' && uiState.view === 'detail') {
788
1139
  uiState.view = 'history';
789
1140
  uiState.selectedHistoryPageIndex = 0;
@@ -851,6 +1202,21 @@ async function runHostTui(identityPath, opts) {
851
1202
  latestState = redraw();
852
1203
  return;
853
1204
  }
1205
+ if (key?.name === 'f') {
1206
+ const confirmed = await confirmAction('Repair OpenClaw hooks config now? A timestamped backup will be written first.');
1207
+ if (!confirmed) {
1208
+ setStatus('Hooks repair cancelled.', 'warn');
1209
+ latestState = redraw();
1210
+ return;
1211
+ }
1212
+ const result = runOpenClawInstall(['fix-hooks']);
1213
+ setStatus(result.ok
1214
+ ? `Hooks repaired.${result.stdout ? ` ${truncateLine(result.stdout.replace(/\s+/g, ' '), 100)}` : ''}`
1215
+ : `Hooks repair failed: ${truncateLine(result.stderr || result.stdout || 'unknown error', 120)}`,
1216
+ result.ok ? 'ok' : 'err', result.ok ? 7000 : 9000);
1217
+ latestState = redraw();
1218
+ return;
1219
+ }
854
1220
  if (key?.name === 'y' && uiState.view === 'task-detail') {
855
1221
  const tasks = latestState.selectedTasks || [];
856
1222
  const task = tasks[Math.max(0, Math.min(uiState.selectedTaskIndex, Math.max(0, tasks.length - 1)))] || null;
@@ -916,6 +1282,51 @@ async function getClientWithStore(identityPath) {
916
1282
  return { client, store };
917
1283
  }
918
1284
 
1285
+ function resolveSessionFromStore(store, args = {}) {
1286
+ const sessionManager = new SessionManager(store);
1287
+ let session = args.sessionKey ? sessionManager.get(args.sessionKey) : null;
1288
+ if (!session && args.conversationId) session = sessionManager.getByConversationId(args.conversationId);
1289
+ if (!session && args.remoteDid) {
1290
+ session = sessionManager.listRecentSessions(200).find((item) => item.remote_did === args.remoteDid) ?? null;
1291
+ }
1292
+ return session ?? sessionManager.getActiveSession() ?? sessionManager.listRecentSessions(1)[0] ?? null;
1293
+ }
1294
+
1295
+ async function resolveTarget(client, target) {
1296
+ const value = String(target ?? '').trim();
1297
+ if (!value) throw new Error('Missing target');
1298
+ if (value.startsWith('did:agent:')) return value;
1299
+ if (value.startsWith('@')) {
1300
+ const resolved = await client.resolveAlias(value.slice(1));
1301
+ if (!resolved.ok || !resolved.data?.did) {
1302
+ throw new Error(resolved.error?.message || `Cannot resolve alias ${value}`);
1303
+ }
1304
+ return resolved.data.did;
1305
+ }
1306
+ const publicAgent = await client.getPublicAgent(value).catch(() => ({ ok: false }));
1307
+ if (publicAgent.ok && publicAgent.data?.did) return publicAgent.data.did;
1308
+ return value;
1309
+ }
1310
+
1311
+ async function maybeEnsureHostedPublicLink(identityPath) {
1312
+ const p = identityPath ?? getEffectiveIdentityPath();
1313
+ const id = loadIdentity(p);
1314
+ if (!isOfficialHostedServer(id.serverUrl ?? DEFAULT_SERVER)) return null;
1315
+ const key = `${p}:${normalizeOrigin(id.serverUrl ?? DEFAULT_SERVER)}`;
1316
+ if (hostedPublicLinkAttempts.has(key)) return null;
1317
+ hostedPublicLinkAttempts.add(key);
1318
+ try {
1319
+ const client = await getClient(p);
1320
+ const current = await client.getPublicSelf().catch(() => ({ ok: false }));
1321
+ if (current.ok && current.data?.public_url) return { created: false, data: current.data };
1322
+ const created = await client.createPublicLink({ enabled: true }).catch(() => ({ ok: false }));
1323
+ if (created.ok && created.data) return { created: true, data: created.data };
1324
+ return null;
1325
+ } catch {
1326
+ return null;
1327
+ }
1328
+ }
1329
+
919
1330
  const program = new Command();
920
1331
  program
921
1332
  .name('pingagent')
@@ -2249,6 +2660,467 @@ billing
2249
2660
  }
2250
2661
  });
2251
2662
 
2663
+ const publicCmd = program.command('public').description('Hosted public growth surface: shareable profile links, contact cards, and task shares');
2664
+
2665
+ publicCmd
2666
+ .command('link')
2667
+ .description('Create or update your hosted public share link')
2668
+ .option('--slug <slug>', 'Preferred public slug')
2669
+ .option('--json', 'Output as JSON')
2670
+ .action(async (opts) => {
2671
+ const client = await getClient();
2672
+ const res = await client.createPublicLink({ slug: opts.slug });
2673
+ if (!res.ok) {
2674
+ if (opts.json) console.log(JSON.stringify(res, null, 2));
2675
+ else printError(res.error);
2676
+ process.exit(1);
2677
+ }
2678
+ if (opts.json) console.log(JSON.stringify(res.data, null, 2));
2679
+ else {
2680
+ console.log(`Public slug: ${res.data.public_slug || '(none)'}`);
2681
+ console.log(`Canonical: ${res.data.canonical_slug || '(none)'}`);
2682
+ console.log(`URL: ${res.data.public_url || '(none)'}`);
2683
+ }
2684
+ });
2685
+
2686
+ publicCmd
2687
+ .command('profile')
2688
+ .description('Show your hosted public share state')
2689
+ .option('--json', 'Output as JSON')
2690
+ .action(async (opts) => {
2691
+ const client = await getClient();
2692
+ await maybeEnsureHostedPublicLink();
2693
+ const res = await client.getPublicSelf();
2694
+ if (!res.ok) {
2695
+ if (opts.json) console.log(JSON.stringify(res, null, 2));
2696
+ else printError(res.error);
2697
+ process.exit(1);
2698
+ }
2699
+ if (opts.json) console.log(JSON.stringify(res.data, null, 2));
2700
+ else {
2701
+ console.log(`DID: ${res.data.did}`);
2702
+ console.log(`Alias: ${res.data.alias || '(none)'}`);
2703
+ console.log(`Public slug: ${res.data.public_slug || '(none)'}`);
2704
+ console.log(`Enabled: ${res.data.public_share_enabled ? 'yes' : 'no'}`);
2705
+ console.log(`Discoverable:${res.data.discoverable ? ' yes' : ' no'}`);
2706
+ console.log(`URL: ${res.data.public_url || '(none)'}`);
2707
+ }
2708
+ });
2709
+
2710
+ publicCmd
2711
+ .command('contact-card')
2712
+ .description('Create a shareable contact card for this agent or another target DID')
2713
+ .option('--target-did <did>', 'Target DID to place in the contact card')
2714
+ .option('--intro-note <text>', 'Intro note shown on the card')
2715
+ .option('--message-template <text>', 'Suggested first-message template')
2716
+ .option('--json', 'Output as JSON')
2717
+ .action(async (opts) => {
2718
+ const client = await getClient();
2719
+ const res = await client.createContactCard({
2720
+ target_did: opts.targetDid,
2721
+ intro_note: opts.introNote,
2722
+ message_template: opts.messageTemplate,
2723
+ });
2724
+ if (!res.ok) {
2725
+ if (opts.json) console.log(JSON.stringify(res, null, 2));
2726
+ else printError(res.error);
2727
+ process.exit(1);
2728
+ }
2729
+ if (opts.json) console.log(JSON.stringify(res.data, null, 2));
2730
+ else {
2731
+ console.log(`Contact card: ${res.data.id}`);
2732
+ console.log(`Target DID: ${res.data.target_did}`);
2733
+ console.log(`URL: ${res.data.share_url || '(none)'}`);
2734
+ }
2735
+ });
2736
+
2737
+ publicCmd
2738
+ .command('task-share')
2739
+ .description('Publish a shareable task result summary (explicit publish only)')
2740
+ .requiredOption('--summary <text>', 'Generated summary to publish')
2741
+ .option('--task-id <id>', 'Task ID')
2742
+ .option('--title <title>', 'Task title')
2743
+ .option('--status <status>', 'Task status', 'processed')
2744
+ .option('--conversation <id>', 'Conversation ID')
2745
+ .option('--json', 'Output as JSON')
2746
+ .action(async (opts) => {
2747
+ const client = await getClient();
2748
+ const res = await client.createTaskShare({
2749
+ task_id: opts.taskId,
2750
+ title: opts.title,
2751
+ status: opts.status,
2752
+ summary: opts.summary,
2753
+ conversation_id: opts.conversation,
2754
+ });
2755
+ if (!res.ok) {
2756
+ if (opts.json) console.log(JSON.stringify(res, null, 2));
2757
+ else printError(res.error);
2758
+ process.exit(1);
2759
+ }
2760
+ if (opts.json) console.log(JSON.stringify(res.data, null, 2));
2761
+ else {
2762
+ console.log(`Task share: ${res.data.id}`);
2763
+ console.log(`URL: ${res.data.share_url || '(none)'}`);
2764
+ }
2765
+ });
2766
+
2767
+ program
2768
+ .command('capability-card')
2769
+ .description('Show or update the structured machine-readable capability card for this agent')
2770
+ .option('--summary <text>', 'Capability card summary')
2771
+ .option('--accepts-new-work <value>', 'true or false')
2772
+ .option('--preferred-contact-mode <mode>', 'dm, task, or either')
2773
+ .option('--capability-item <json>', 'Capability item JSON; repeat to add/replace entries by id', (value, acc) => {
2774
+ acc.push(value);
2775
+ return acc;
2776
+ }, [])
2777
+ .option('--replace-items', 'Replace capability items with the provided --capability-item rows instead of merging by id')
2778
+ .option('--json', 'Output as JSON')
2779
+ .action(async (opts) => {
2780
+ const client = await getClient();
2781
+ const profileRes = await client.getProfile();
2782
+ if (!profileRes.ok || !profileRes.data) {
2783
+ if (opts.json) console.log(JSON.stringify(profileRes, null, 2));
2784
+ else printError(profileRes.error);
2785
+ process.exit(1);
2786
+ }
2787
+ const current = profileRes.data.capability_card || { version: '1', capabilities: [] };
2788
+ const shouldUpdate =
2789
+ opts.summary !== undefined
2790
+ || opts.acceptsNewWork !== undefined
2791
+ || opts.preferredContactMode !== undefined
2792
+ || (opts.capabilityItem && opts.capabilityItem.length > 0)
2793
+ || opts.replaceItems;
2794
+ if (!shouldUpdate) {
2795
+ if (opts.json) console.log(JSON.stringify(current, null, 2));
2796
+ else {
2797
+ console.log(`summary=${formatCapabilityCardSummary(current)}`);
2798
+ console.log(JSON.stringify(current, null, 2));
2799
+ }
2800
+ return;
2801
+ }
2802
+ let capabilityItems = Array.isArray(current.capabilities) ? [...current.capabilities] : [];
2803
+ if (opts.capabilityItem && opts.capabilityItem.length > 0) {
2804
+ const parsedItems = opts.capabilityItem.map((item) => JSON.parse(item));
2805
+ if (opts.replaceItems) {
2806
+ capabilityItems = parsedItems;
2807
+ } else {
2808
+ const byId = new Map(capabilityItems.map((item) => [item.id, item]));
2809
+ for (const item of parsedItems) {
2810
+ byId.set(item.id, { ...(byId.get(item.id) || {}), ...item });
2811
+ }
2812
+ capabilityItems = Array.from(byId.values());
2813
+ }
2814
+ } else if (opts.replaceItems) {
2815
+ capabilityItems = [];
2816
+ }
2817
+ const nextCard = {
2818
+ version: current.version || '1',
2819
+ summary: opts.summary !== undefined ? opts.summary : current.summary,
2820
+ accepts_new_work: opts.acceptsNewWork !== undefined
2821
+ ? String(opts.acceptsNewWork).trim().toLowerCase() === 'true'
2822
+ : current.accepts_new_work,
2823
+ preferred_contact_mode: opts.preferredContactMode !== undefined
2824
+ ? opts.preferredContactMode
2825
+ : current.preferred_contact_mode,
2826
+ capabilities: capabilityItems,
2827
+ };
2828
+ const updateRes = await client.updateProfile({ capability_card: nextCard });
2829
+ if (!updateRes.ok || !updateRes.data) {
2830
+ if (opts.json) console.log(JSON.stringify(updateRes, null, 2));
2831
+ else printError(updateRes.error);
2832
+ process.exit(1);
2833
+ }
2834
+ if (opts.json) console.log(JSON.stringify(updateRes.data.capability_card || nextCard, null, 2));
2835
+ else {
2836
+ console.log(`summary=${formatCapabilityCardSummary(updateRes.data.capability_card || nextCard)}`);
2837
+ console.log(JSON.stringify(updateRes.data.capability_card || nextCard, null, 2));
2838
+ }
2839
+ });
2840
+
2841
+ program
2842
+ .command('session-summary')
2843
+ .description('Show or update the local carry-forward summary for a session')
2844
+ .option('--session-key <key>', 'Exact session key')
2845
+ .option('--conversation-id <id>', 'Conversation ID')
2846
+ .option('--remote-did <did>', 'Remote DID')
2847
+ .option('--objective <text>', 'Current objective')
2848
+ .option('--context <text>', 'Current context')
2849
+ .option('--constraints <text>', 'Constraints')
2850
+ .option('--decisions <text>', 'Decisions already made')
2851
+ .option('--open-questions <text>', 'Open questions')
2852
+ .option('--next-action <text>', 'Next action')
2853
+ .option('--handoff-ready-text <text>', 'Handoff-ready summary text')
2854
+ .option('--clear-field <field>', 'Clear one summary field; repeatable', (value, acc) => {
2855
+ acc.push(value);
2856
+ return acc;
2857
+ }, [])
2858
+ .option('--json', 'Output as JSON')
2859
+ .action(async (opts) => {
2860
+ const identityPath = getIdentityPathForCommand(opts);
2861
+ const store = openStore(identityPath);
2862
+ try {
2863
+ const session = resolveSessionFromStore(store, {
2864
+ sessionKey: opts.sessionKey,
2865
+ conversationId: opts.conversationId,
2866
+ remoteDid: opts.remoteDid,
2867
+ });
2868
+ if (!session) {
2869
+ console.error('No session found. Use pingagent host tui or pingagent recent sessions first.');
2870
+ process.exit(1);
2871
+ }
2872
+ const manager = new SessionSummaryManager(store);
2873
+ const clearFields = Array.isArray(opts.clearField)
2874
+ ? opts.clearField
2875
+ .map((value) => String(value || '').trim())
2876
+ .filter((value) => SESSION_SUMMARY_FIELDS.includes(value))
2877
+ : [];
2878
+ if (Array.isArray(opts.clearField) && opts.clearField.length !== clearFields.length) {
2879
+ console.error(`clear-field must be one of: ${SESSION_SUMMARY_FIELDS.join(', ')}`);
2880
+ process.exit(1);
2881
+ }
2882
+ const shouldUpdate = [
2883
+ opts.objective,
2884
+ opts.context,
2885
+ opts.constraints,
2886
+ opts.decisions,
2887
+ opts.openQuestions,
2888
+ opts.nextAction,
2889
+ opts.handoffReadyText,
2890
+ ].some((value) => value !== undefined) || clearFields.length > 0;
2891
+ let summary = shouldUpdate
2892
+ ? manager.upsert({
2893
+ session_key: session.session_key,
2894
+ objective: opts.objective,
2895
+ context: opts.context,
2896
+ constraints: opts.constraints,
2897
+ decisions: opts.decisions,
2898
+ open_questions: opts.openQuestions,
2899
+ next_action: opts.nextAction,
2900
+ handoff_ready_text: opts.handoffReadyText,
2901
+ })
2902
+ : manager.get(session.session_key);
2903
+ if (clearFields.length > 0) {
2904
+ summary = manager.clearFields(session.session_key, clearFields);
2905
+ }
2906
+ if (opts.json) {
2907
+ console.log(JSON.stringify({ session, summary }, null, 2));
2908
+ } else {
2909
+ console.log(`session=${session.session_key}`);
2910
+ console.log(`conversation=${session.conversation_id || '(none)'}`);
2911
+ console.log(`remote=${session.remote_did || '(unknown)'}`);
2912
+ console.log(JSON.stringify(summary, null, 2));
2913
+ }
2914
+ } finally {
2915
+ store.close();
2916
+ }
2917
+ });
2918
+
2919
+ program
2920
+ .command('handoff')
2921
+ .description('Send a first-class delegation / handoff task using the current task-thread transport')
2922
+ .requiredOption('--to <target>', 'Target DID, alias, public slug, or connectable identity')
2923
+ .requiredOption('--title <title>', 'Task title')
2924
+ .option('--description <text>', 'Task description')
2925
+ .option('--objective <text>', 'Delegation objective')
2926
+ .option('--success-criteria <text>', 'Success criteria')
2927
+ .option('--priority <text>', 'Priority label')
2928
+ .option('--carry-forward-summary <text>', 'Explicit carry-forward summary; otherwise use the session summary')
2929
+ .option('--callback-session-key <key>', 'Callback session key to reference in the handoff')
2930
+ .option('--session-key <key>', 'Source session key whose summary should be used')
2931
+ .option('--conversation-id <id>', 'Source conversation ID whose summary should be used')
2932
+ .option('--remote-did <did>', 'Source remote DID whose summary should be used')
2933
+ .option('--json', 'Output as JSON')
2934
+ .action(async (opts) => {
2935
+ const identityPath = getIdentityPathForCommand(opts);
2936
+ const { client, store } = await getClientWithStore(identityPath);
2937
+ try {
2938
+ const targetDid = await resolveTarget(client, opts.to);
2939
+ const sourceSession = resolveSessionFromStore(store, {
2940
+ sessionKey: opts.sessionKey,
2941
+ conversationId: opts.conversationId,
2942
+ remoteDid: opts.remoteDid,
2943
+ });
2944
+ const result = await client.sendHandoff(targetDid, {
2945
+ title: opts.title,
2946
+ description: opts.description,
2947
+ objective: opts.objective,
2948
+ carry_forward_summary: opts.carryForwardSummary,
2949
+ success_criteria: opts.successCriteria,
2950
+ callback_session_key: opts.callbackSessionKey || sourceSession?.session_key,
2951
+ priority: opts.priority,
2952
+ }, {
2953
+ sessionKey: sourceSession?.session_key,
2954
+ conversationId: sourceSession?.conversation_id,
2955
+ });
2956
+ if (!result.ok || !result.data) {
2957
+ if (opts.json) console.log(JSON.stringify(result, null, 2));
2958
+ else printError(result.error);
2959
+ process.exit(1);
2960
+ }
2961
+ if (opts.json) {
2962
+ console.log(JSON.stringify({
2963
+ target_did: targetDid,
2964
+ source_session_key: sourceSession?.session_key ?? null,
2965
+ ...result.data,
2966
+ }, null, 2));
2967
+ } else {
2968
+ console.log(`task_id=${result.data.task_id}`);
2969
+ console.log(`conversation_id=${result.data.conversation_id}`);
2970
+ console.log(`target_did=${targetDid}`);
2971
+ console.log(`source_session_key=${sourceSession?.session_key || '(none)'}`);
2972
+ console.log(JSON.stringify(result.data.handoff, null, 2));
2973
+ }
2974
+ } finally {
2975
+ store.close();
2976
+ }
2977
+ });
2978
+
2979
+ program
2980
+ .command('demo')
2981
+ .description('Open or message the official PingAgent demo agent')
2982
+ .option('--preset <name>', 'Preset first message: hello, delegate, or trust')
2983
+ .option('--message <text>', 'Optional first message to send immediately')
2984
+ .option('--json', 'Output as JSON')
2985
+ .action(async (opts) => {
2986
+ const client = await getClient();
2987
+ const resolved = await client.resolveAlias('pingagent/demo');
2988
+ if (!resolved.ok || !resolved.data?.did) {
2989
+ if (opts.json) console.log(JSON.stringify(resolved, null, 2));
2990
+ else printError(resolved.error);
2991
+ process.exit(1);
2992
+ }
2993
+ const convo = await client.openConversation(resolved.data.did);
2994
+ if (!convo.ok || !convo.data) {
2995
+ if (opts.json) console.log(JSON.stringify(convo, null, 2));
2996
+ else printError(convo.error);
2997
+ process.exit(1);
2998
+ }
2999
+ const presetMessages = {
3000
+ hello: 'Hello',
3001
+ delegate: 'Please show me how task delegation works in PingAgent.',
3002
+ trust: 'Show me how trust decisions and recommendations work.',
3003
+ };
3004
+ const effectiveMessage = typeof opts.message === 'string' && opts.message.trim()
3005
+ ? opts.message
3006
+ : (typeof opts.preset === 'string' && presetMessages[opts.preset.trim().toLowerCase()] ? presetMessages[opts.preset.trim().toLowerCase()] : '');
3007
+ if (effectiveMessage) {
3008
+ const sendRes = await client.sendMessage(convo.data.conversation_id, SCHEMA_TEXT, { text: effectiveMessage });
3009
+ if (!sendRes.ok) {
3010
+ if (opts.json) console.log(JSON.stringify(sendRes, null, 2));
3011
+ else printError(sendRes.error);
3012
+ process.exit(1);
3013
+ }
3014
+ 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));
3015
+ else console.log(`Demo agent messaged. conversation=${convo.data.conversation_id} message=${sendRes.data?.message_id}`);
3016
+ return;
3017
+ }
3018
+ if (opts.json) console.log(JSON.stringify({ did: resolved.data.did, conversation_id: convo.data.conversation_id }, null, 2));
3019
+ else console.log(`Demo agent ready. did=${resolved.data.did} conversation=${convo.data.conversation_id}`);
3020
+ });
3021
+
3022
+ program
3023
+ .command('connect')
3024
+ .description('Consume a PingAgent public link or contact card and open a conversation')
3025
+ .argument('<target>', 'Share URL, contact card URL, public slug, alias, or DID')
3026
+ .option('--message <text>', 'Optional first message to send immediately')
3027
+ .option('--json', 'Output as JSON')
3028
+ .action(async (target, opts) => {
3029
+ const client = await getClient();
3030
+ let targetDid = '';
3031
+ let prefMessage = typeof opts.message === 'string' ? opts.message : '';
3032
+ try {
3033
+ if (/^https?:\/\//.test(target)) {
3034
+ const url = new URL(target);
3035
+ const parts = url.pathname.split('/').filter(Boolean);
3036
+ if (parts[0] === 'c' && parts[1]) {
3037
+ const card = await client.getContactCard(parts[1]);
3038
+ if (!card.ok || !card.data) {
3039
+ if (opts.json) console.log(JSON.stringify(card, null, 2));
3040
+ else printError(card.error);
3041
+ process.exit(1);
3042
+ }
3043
+ targetDid = card.data.target_did;
3044
+ if (!prefMessage && card.data.message_template) prefMessage = card.data.message_template;
3045
+ } else if (parts[0] === 'a' && parts[1]) {
3046
+ const agent = await client.getPublicAgent(parts[1]);
3047
+ if (!agent.ok || !agent.data?.did) {
3048
+ if (opts.json) console.log(JSON.stringify(agent, null, 2));
3049
+ else printError(agent.error);
3050
+ process.exit(1);
3051
+ }
3052
+ targetDid = agent.data.did;
3053
+ } else if (parts[0] === 'connect' && parts[1]) {
3054
+ const requestedTarget = decodeURIComponent(parts[1]);
3055
+ if (!prefMessage && url.searchParams.get('message')) {
3056
+ prefMessage = url.searchParams.get('message') || '';
3057
+ }
3058
+ if (requestedTarget.startsWith('did:agent:')) {
3059
+ targetDid = requestedTarget;
3060
+ } else if (requestedTarget.startsWith('@')) {
3061
+ const resolved = await client.resolveAlias(requestedTarget.slice(1));
3062
+ if (!resolved.ok || !resolved.data?.did) {
3063
+ if (opts.json) console.log(JSON.stringify(resolved, null, 2));
3064
+ else printError(resolved.error);
3065
+ process.exit(1);
3066
+ }
3067
+ targetDid = resolved.data.did;
3068
+ } else {
3069
+ const agent = await client.getPublicAgent(requestedTarget);
3070
+ if (!agent.ok || !agent.data?.did) {
3071
+ if (opts.json) console.log(JSON.stringify(agent, null, 2));
3072
+ else printError(agent.error);
3073
+ process.exit(1);
3074
+ }
3075
+ targetDid = agent.data.did;
3076
+ }
3077
+ }
3078
+ }
3079
+ } catch {
3080
+ // fall through to text target handling
3081
+ }
3082
+ if (!targetDid) {
3083
+ if (String(target).startsWith('did:agent:')) {
3084
+ targetDid = String(target);
3085
+ } else if (String(target).startsWith('@')) {
3086
+ const resolved = await client.resolveAlias(String(target).slice(1));
3087
+ if (!resolved.ok || !resolved.data?.did) {
3088
+ if (opts.json) console.log(JSON.stringify(resolved, null, 2));
3089
+ else printError(resolved.error);
3090
+ process.exit(1);
3091
+ }
3092
+ targetDid = resolved.data.did;
3093
+ } else {
3094
+ const agent = await client.getPublicAgent(String(target));
3095
+ if (!agent.ok || !agent.data?.did) {
3096
+ if (opts.json) console.log(JSON.stringify(agent, null, 2));
3097
+ else printError(agent.error);
3098
+ process.exit(1);
3099
+ }
3100
+ targetDid = agent.data.did;
3101
+ }
3102
+ }
3103
+ const convo = await client.openConversation(targetDid);
3104
+ if (!convo.ok || !convo.data) {
3105
+ if (opts.json) console.log(JSON.stringify(convo, null, 2));
3106
+ else printError(convo.error);
3107
+ process.exit(1);
3108
+ }
3109
+ if (prefMessage) {
3110
+ const sendRes = await client.sendMessage(convo.data.conversation_id, SCHEMA_TEXT, { text: prefMessage });
3111
+ if (!sendRes.ok) {
3112
+ if (opts.json) console.log(JSON.stringify(sendRes, null, 2));
3113
+ else printError(sendRes.error);
3114
+ process.exit(1);
3115
+ }
3116
+ if (opts.json) console.log(JSON.stringify({ did: targetDid, conversation_id: convo.data.conversation_id, message_id: sendRes.data?.message_id }, null, 2));
3117
+ else console.log(`Connected and sent first message. conversation=${convo.data.conversation_id} did=${targetDid}`);
3118
+ return;
3119
+ }
3120
+ if (opts.json) console.log(JSON.stringify({ did: targetDid, conversation_id: convo.data.conversation_id }, null, 2));
3121
+ else console.log(`Connected. conversation=${convo.data.conversation_id} did=${targetDid}`);
3122
+ });
3123
+
2252
3124
  const host = program
2253
3125
  .command('host')
2254
3126
  .description('Headless runtime inspection and control for PingAgent host state');