@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 +1046 -27
- package/dist/chunk-6OA4F66H.js +3193 -0
- package/dist/chunk-BLHMTUID.js +3610 -0
- package/dist/chunk-HCQ7CEDE.js +3556 -0
- package/dist/chunk-R3D7LOGB.js +3553 -0
- package/dist/chunk-SMDQYV7Z.js +3173 -0
- package/dist/chunk-YBNFPOKO.js +3553 -0
- package/dist/index.d.ts +229 -8
- package/dist/index.js +21 -1
- package/dist/web-server.js +1041 -43
- package/package.json +2 -2
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
|
|
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}${
|
|
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}
|
|
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
|
-
`
|
|
326
|
-
`
|
|
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('-
|
|
347
|
-
lines.push('-
|
|
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(`
|
|
436
|
-
lines.push(`
|
|
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(
|
|
593
|
+
lines.push('needs_reconnect=true');
|
|
439
594
|
lines.push(`warning=${selected.binding_alert.message}`);
|
|
440
595
|
} else {
|
|
441
|
-
lines.push('
|
|
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]
|
|
450
|
-
'[c]
|
|
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
|
|
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 || '(
|
|
809
|
-
const confirmed = await confirmAction(`
|
|
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('
|
|
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(`
|
|
1212
|
+
setStatus(`Attached chat link ${selected.conversation_id} -> ${latestState.activeChatSession}`, 'ok');
|
|
818
1213
|
} else {
|
|
819
|
-
setStatus('
|
|
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(`
|
|
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('
|
|
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
|
|
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
|
|
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;
|