@pingagent/sdk 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/pingagent.js +1702 -0
- package/dist/chunk-2Y6YRKTO.js +3100 -0
- package/dist/chunk-6OA4F66H.js +3193 -0
- package/dist/chunk-BLHMTUID.js +3610 -0
- package/dist/chunk-HCQ7CEDE.js +3556 -0
- package/dist/chunk-R3D7LOGB.js +3553 -0
- package/dist/chunk-RMIRCSQ6.js +3042 -0
- package/dist/chunk-SMDQYV7Z.js +3173 -0
- package/dist/chunk-YBNFPOKO.js +3553 -0
- package/dist/index.d.ts +265 -8
- package/dist/index.js +47 -3
- package/dist/web-server.js +906 -15
- package/package.json +3 -3
package/bin/pingagent.js
CHANGED
|
@@ -3,6 +3,8 @@ import { Command } from 'commander';
|
|
|
3
3
|
import * as fs from 'node:fs';
|
|
4
4
|
import * as path from 'node:path';
|
|
5
5
|
import * as os from 'node:os';
|
|
6
|
+
import * as readline from 'node:readline';
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
6
8
|
import {
|
|
7
9
|
PingAgentClient,
|
|
8
10
|
generateIdentity,
|
|
@@ -15,12 +17,35 @@ import {
|
|
|
15
17
|
ContactManager,
|
|
16
18
|
HistoryManager,
|
|
17
19
|
A2AAdapter,
|
|
20
|
+
SessionManager,
|
|
21
|
+
SessionSummaryManager,
|
|
22
|
+
TaskThreadManager,
|
|
23
|
+
TaskHandoffManager,
|
|
24
|
+
TrustPolicyAuditManager,
|
|
25
|
+
TrustRecommendationManager,
|
|
26
|
+
getTrustRecommendationActionLabel,
|
|
27
|
+
formatCapabilityCardSummary,
|
|
28
|
+
defaultTrustPolicyDoc,
|
|
29
|
+
normalizeTrustPolicyDoc,
|
|
30
|
+
upsertTrustPolicyRecommendation,
|
|
31
|
+
getActiveSessionFilePath,
|
|
32
|
+
getSessionMapFilePath,
|
|
33
|
+
getSessionBindingAlertsFilePath,
|
|
34
|
+
readCurrentActiveSessionKey,
|
|
35
|
+
readSessionBindings,
|
|
36
|
+
readSessionBindingAlerts,
|
|
37
|
+
setSessionBinding,
|
|
38
|
+
removeSessionBinding,
|
|
39
|
+
readIngressRuntimeStatus,
|
|
18
40
|
} from '../dist/index.js';
|
|
19
41
|
import { ERROR_HINTS, SCHEMA_TEXT } from '@pingagent/schemas';
|
|
20
42
|
|
|
21
43
|
const DEFAULT_SERVER = 'https://pingagent.chat';
|
|
22
44
|
const UPGRADE_URL = 'https://pingagent.chat';
|
|
23
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'];
|
|
24
49
|
|
|
25
50
|
function resolvePath(p) {
|
|
26
51
|
if (!p) return p;
|
|
@@ -28,6 +53,18 @@ function resolvePath(p) {
|
|
|
28
53
|
return p;
|
|
29
54
|
}
|
|
30
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
|
+
|
|
31
68
|
function getEffectiveIdentityPath() {
|
|
32
69
|
const dir = program.opts().identityDir;
|
|
33
70
|
if (dir) return path.join(resolvePath(dir), 'identity.json');
|
|
@@ -77,6 +114,1123 @@ function openStore(identityPath) {
|
|
|
77
114
|
return new LocalStore(getStorePath(identityPath));
|
|
78
115
|
}
|
|
79
116
|
|
|
117
|
+
function getTrustPolicyPath(identityPath) {
|
|
118
|
+
return process.env.PINGAGENT_TRUST_POLICY_PATH
|
|
119
|
+
? resolvePath(process.env.PINGAGENT_TRUST_POLICY_PATH)
|
|
120
|
+
: path.join(path.dirname(identityPath), 'trust-policy.json');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function readTrustPolicyDoc(identityPath) {
|
|
124
|
+
const policyPath = getTrustPolicyPath(identityPath);
|
|
125
|
+
try {
|
|
126
|
+
if (!fs.existsSync(policyPath)) return { path: policyPath, doc: defaultTrustPolicyDoc() };
|
|
127
|
+
const raw = JSON.parse(fs.readFileSync(policyPath, 'utf-8'));
|
|
128
|
+
return { path: policyPath, doc: normalizeTrustPolicyDoc(raw) };
|
|
129
|
+
} catch {
|
|
130
|
+
return { path: policyPath, doc: defaultTrustPolicyDoc() };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
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
|
+
|
|
166
|
+
function clearScreen() {
|
|
167
|
+
process.stdout.write('\x1Bc');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function ansi(code, text) {
|
|
171
|
+
return process.stdout.isTTY ? `\x1b[${code}m${text}\x1b[0m` : text;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function formatStatusLine(level, message) {
|
|
175
|
+
const tag = level === 'ok'
|
|
176
|
+
? ansi('32', '[OK]')
|
|
177
|
+
: level === 'warn'
|
|
178
|
+
? ansi('33', '[WARN]')
|
|
179
|
+
: level === 'err'
|
|
180
|
+
? ansi('31', '[ERR]')
|
|
181
|
+
: ansi('36', '[INFO]');
|
|
182
|
+
return `${tag} ${message || '(ready)'}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function formatStatusTimestamp(tsMs) {
|
|
186
|
+
if (!tsMs) return '';
|
|
187
|
+
return formatTs(tsMs, false);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function truncateLine(value, max = 100) {
|
|
191
|
+
const s = String(value ?? '');
|
|
192
|
+
return s.length <= max ? s : `${s.slice(0, max - 1)}…`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function formatSessionRow(session, selected) {
|
|
196
|
+
const marker = selected ? '>' : ' ';
|
|
197
|
+
const rebind = session.binding_alert ? ' !rebind' : '';
|
|
198
|
+
const trust = session.trust_state || 'unknown';
|
|
199
|
+
const unread = session.unread_count ?? 0;
|
|
200
|
+
const who = truncateLine(session.remote_did || session.conversation_id || 'unknown', 40);
|
|
201
|
+
return `${marker} ${who} [${trust}] unread=${unread}${rebind}`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function formatMessageRow(message) {
|
|
205
|
+
const ts = formatTs(message.ts_ms ?? message.receivedAtMs ?? Date.now(), false);
|
|
206
|
+
const direction = message.direction || 'unknown';
|
|
207
|
+
const schema = message.schema || 'unknown';
|
|
208
|
+
const payload = message.payload || {};
|
|
209
|
+
const summary = schema === SCHEMA_TEXT && payload.text
|
|
210
|
+
? payload.text
|
|
211
|
+
: JSON.stringify(payload);
|
|
212
|
+
return `- ${ts} ${direction} ${schema} ${truncateLine(summary, 90)}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function buildTaskExportBody(task, format = 'plain') {
|
|
216
|
+
const payload = {
|
|
217
|
+
task_id: task.task_id,
|
|
218
|
+
title: task.title || null,
|
|
219
|
+
status: task.status,
|
|
220
|
+
updated_at: task.updated_at ?? null,
|
|
221
|
+
updated_at_display: task.updated_at ? formatTs(task.updated_at, false) : null,
|
|
222
|
+
started_at: task.started_at ?? null,
|
|
223
|
+
started_at_display: task.started_at ? formatTs(task.started_at, false) : null,
|
|
224
|
+
result: task.result_summary || null,
|
|
225
|
+
error: task.error_message
|
|
226
|
+
? {
|
|
227
|
+
code: task.error_code || 'E_TASK',
|
|
228
|
+
message: task.error_message,
|
|
229
|
+
}
|
|
230
|
+
: null,
|
|
231
|
+
handoff: task.handoff || null,
|
|
232
|
+
};
|
|
233
|
+
if (format === 'json') return JSON.stringify(payload, null, 2);
|
|
234
|
+
return [
|
|
235
|
+
`task_id=${payload.task_id}`,
|
|
236
|
+
`title=${payload.title || '(none)'}`,
|
|
237
|
+
`status=${payload.status}`,
|
|
238
|
+
`updated_at=${payload.updated_at_display || '(none)'}`,
|
|
239
|
+
payload.started_at_display ? `started_at=${payload.started_at_display}` : null,
|
|
240
|
+
'',
|
|
241
|
+
'[result]',
|
|
242
|
+
payload.result || '(none)',
|
|
243
|
+
'',
|
|
244
|
+
'[error]',
|
|
245
|
+
payload.error ? `${payload.error.code} ${payload.error.message}` : '(none)',
|
|
246
|
+
].filter(Boolean).join('\n');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function loadHistoryPage(historyManager, conversationId, pageIndex, pageSize) {
|
|
250
|
+
if (!conversationId) return { messages: [], pageIndex: 0, hasOlder: false, hasNewer: false };
|
|
251
|
+
let page = Math.max(0, pageIndex || 0);
|
|
252
|
+
let messages = historyManager.listRecent(conversationId, pageSize);
|
|
253
|
+
if (messages.length === 0) {
|
|
254
|
+
return { messages: [], pageIndex: 0, hasOlder: false, hasNewer: false };
|
|
255
|
+
}
|
|
256
|
+
for (let i = 0; i < page; i++) {
|
|
257
|
+
const first = messages[0];
|
|
258
|
+
if (!first) {
|
|
259
|
+
page = i;
|
|
260
|
+
messages = [];
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
const older = first.seq != null
|
|
264
|
+
? historyManager.listBeforeSeq(conversationId, first.seq, pageSize)
|
|
265
|
+
: historyManager.listBeforeTs(conversationId, first.ts_ms, pageSize);
|
|
266
|
+
if (!older.length) {
|
|
267
|
+
page = i;
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
messages = older;
|
|
271
|
+
}
|
|
272
|
+
const first = messages[0];
|
|
273
|
+
const hasOlder = !!first && (
|
|
274
|
+
first.seq != null
|
|
275
|
+
? historyManager.listBeforeSeq(conversationId, first.seq, 1).length > 0
|
|
276
|
+
: historyManager.listBeforeTs(conversationId, first.ts_ms, 1).length > 0
|
|
277
|
+
);
|
|
278
|
+
return {
|
|
279
|
+
messages,
|
|
280
|
+
pageIndex: page,
|
|
281
|
+
hasOlder,
|
|
282
|
+
hasNewer: page > 0,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function buildHostState(identityPath, selectedSessionKey = null, historyPageIndex = 0, historySearchQuery = '') {
|
|
287
|
+
const identity = loadIdentity(identityPath);
|
|
288
|
+
const store = openStore(identityPath);
|
|
289
|
+
try {
|
|
290
|
+
const sessionManager = new SessionManager(store);
|
|
291
|
+
const sessionSummaryManager = new SessionSummaryManager(store);
|
|
292
|
+
const taskManager = new TaskThreadManager(store);
|
|
293
|
+
const taskHandoffManager = new TaskHandoffManager(store);
|
|
294
|
+
const historyManager = new HistoryManager(store);
|
|
295
|
+
const auditManager = new TrustPolicyAuditManager(store);
|
|
296
|
+
const sessions = sessionManager.listRecentSessions(50);
|
|
297
|
+
const bindings = readSessionBindings();
|
|
298
|
+
const alerts = readSessionBindingAlerts();
|
|
299
|
+
const bindingByConversation = new Map(bindings.map((row) => [row.conversation_id, row]));
|
|
300
|
+
const alertByConversation = new Map(alerts.map((row) => [row.conversation_id, row]));
|
|
301
|
+
const activeChatSession = readCurrentActiveSessionKey();
|
|
302
|
+
const ingressRuntime = readIngressRuntimeStatus();
|
|
303
|
+
const desiredSelectedSessionKey =
|
|
304
|
+
(selectedSessionKey && sessions.some((session) => session.session_key === selectedSessionKey) ? selectedSessionKey : null) ??
|
|
305
|
+
sessionManager.getActiveSession()?.session_key ??
|
|
306
|
+
sessions[0]?.session_key ??
|
|
307
|
+
null;
|
|
308
|
+
const policy = readTrustPolicyDoc(identityPath);
|
|
309
|
+
const runtimeMode = process.env.PINGAGENT_RUNTIME_MODE || 'bridge';
|
|
310
|
+
const sessionsWithMeta = sessions.map((session) => ({
|
|
311
|
+
...session,
|
|
312
|
+
binding: session.conversation_id ? (bindingByConversation.get(session.conversation_id) ?? null) : null,
|
|
313
|
+
binding_alert: session.conversation_id ? (alertByConversation.get(session.conversation_id) ?? null) : null,
|
|
314
|
+
is_active_chat_session: session.session_key === activeChatSession,
|
|
315
|
+
}));
|
|
316
|
+
const selectedSession = sessionsWithMeta.find((session) => session.session_key === desiredSelectedSessionKey) ?? null;
|
|
317
|
+
const selectedSessionSummary = selectedSession
|
|
318
|
+
? sessionSummaryManager.get(selectedSession.session_key)
|
|
319
|
+
: null;
|
|
320
|
+
const selectedTasks = selectedSession
|
|
321
|
+
? taskManager.listBySession(selectedSession.session_key, 12).map((task) => ({
|
|
322
|
+
...task,
|
|
323
|
+
handoff: taskHandoffManager.get(task.task_id),
|
|
324
|
+
}))
|
|
325
|
+
: [];
|
|
326
|
+
const selectedAuditEvents = selectedSession
|
|
327
|
+
? auditManager.listBySession(selectedSession.session_key, 12)
|
|
328
|
+
: [];
|
|
329
|
+
const selectedMessages = selectedSession?.conversation_id
|
|
330
|
+
? historyManager.listRecent(selectedSession.conversation_id, 12)
|
|
331
|
+
: [];
|
|
332
|
+
const selectedHistoryPage = selectedSession?.conversation_id
|
|
333
|
+
? loadHistoryPage(historyManager, selectedSession.conversation_id, historyPageIndex, 20)
|
|
334
|
+
: { messages: [], pageIndex: 0, hasOlder: false, hasNewer: false };
|
|
335
|
+
const selectedHistorySearchResults = selectedSession?.conversation_id && historySearchQuery.trim()
|
|
336
|
+
? historyManager.search(historySearchQuery.trim(), { conversationId: selectedSession.conversation_id, limit: 50 })
|
|
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
|
+
: [];
|
|
350
|
+
const unreadTotal = sessionsWithMeta.reduce((sum, session) => sum + (session.unread_count ?? 0), 0);
|
|
351
|
+
const alertSessions = sessionsWithMeta.filter((session) => !!session.binding_alert).length;
|
|
352
|
+
return {
|
|
353
|
+
identity,
|
|
354
|
+
runtimeMode,
|
|
355
|
+
activeChatSession,
|
|
356
|
+
ingressRuntime,
|
|
357
|
+
activeChatSessionFile: getActiveSessionFilePath(),
|
|
358
|
+
sessionMapPath: getSessionMapFilePath(),
|
|
359
|
+
sessionBindingAlertsPath: getSessionBindingAlertsFilePath(),
|
|
360
|
+
policyPath: policy.path,
|
|
361
|
+
policyDoc: policy.doc,
|
|
362
|
+
sessions: sessionsWithMeta,
|
|
363
|
+
tasks: taskManager.listRecent(30).map((task) => ({
|
|
364
|
+
...task,
|
|
365
|
+
handoff: taskHandoffManager.get(task.task_id),
|
|
366
|
+
})),
|
|
367
|
+
auditEvents: auditManager.listRecent(40),
|
|
368
|
+
selectedSession,
|
|
369
|
+
selectedSessionSummary,
|
|
370
|
+
selectedTasks,
|
|
371
|
+
selectedAuditEvents,
|
|
372
|
+
selectedMessages,
|
|
373
|
+
selectedHistoryPage,
|
|
374
|
+
selectedHistorySearchQuery: historySearchQuery.trim(),
|
|
375
|
+
selectedHistorySearchResults,
|
|
376
|
+
selectedRecommendations,
|
|
377
|
+
unreadTotal,
|
|
378
|
+
alertSessions,
|
|
379
|
+
};
|
|
380
|
+
} finally {
|
|
381
|
+
store.close();
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function renderHostTuiScreen(hostState, uiState) {
|
|
386
|
+
const sessions = hostState.sessions || [];
|
|
387
|
+
const selected = hostState.selectedSession || sessions[0] || null;
|
|
388
|
+
const selectedSummary = hostState.selectedSessionSummary || null;
|
|
389
|
+
const tasks = hostState.selectedTasks || [];
|
|
390
|
+
const auditEvents = hostState.selectedAuditEvents || [];
|
|
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;
|
|
395
|
+
const historyPage = hostState.selectedHistoryPage || { messages: [], pageIndex: 0, hasOlder: false, hasNewer: false };
|
|
396
|
+
const historySearchQuery = hostState.selectedHistorySearchQuery || '';
|
|
397
|
+
const historySearchResults = hostState.selectedHistorySearchResults || [];
|
|
398
|
+
const view = uiState?.view || 'overview';
|
|
399
|
+
const selectedTaskIndex = Math.max(0, Math.min(uiState?.selectedTaskIndex || 0, Math.max(0, tasks.length - 1)));
|
|
400
|
+
const selectedTask = tasks[selectedTaskIndex] || null;
|
|
401
|
+
const statusCountdown = uiState?.statusExpiresAt
|
|
402
|
+
? ` (${Math.max(0, Math.ceil((uiState.statusExpiresAt - Date.now()) / 1000))}s)`
|
|
403
|
+
: '';
|
|
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';
|
|
409
|
+
const lines = [
|
|
410
|
+
'PingAgent Host TUI',
|
|
411
|
+
`DID: ${hostState.identity.did}`,
|
|
412
|
+
`status=${formatStatusLine(uiState?.statusLevel || 'info', uiState?.statusMessage || '(ready)')}${statusTs}${statusCountdown}`,
|
|
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,
|
|
416
|
+
`sessions=${sessions.length} unread_total=${hostState.unreadTotal ?? 0} alert_sessions=${hostState.alertSessions ?? 0} view=${view}`,
|
|
417
|
+
`policy=${hostState.policyPath}`,
|
|
418
|
+
`session_map=${hostState.sessionMapPath}`,
|
|
419
|
+
`binding_alerts=${hostState.sessionBindingAlertsPath}`,
|
|
420
|
+
hostState.ingressRuntime?.hooks_last_error ? `hooks_error=${truncateLine(hostState.ingressRuntime.hooks_last_error, 120)}` : null,
|
|
421
|
+
'',
|
|
422
|
+
].filter(Boolean);
|
|
423
|
+
|
|
424
|
+
if (view === 'help') {
|
|
425
|
+
lines.push('Help');
|
|
426
|
+
lines.push('- ↑/↓ or j/k: move selection');
|
|
427
|
+
lines.push('- Enter or l: open detailed session view / task detail');
|
|
428
|
+
lines.push('- Esc or h: back to previous view');
|
|
429
|
+
lines.push('- g / G: jump to first / last session');
|
|
430
|
+
lines.push('- r: refresh now');
|
|
431
|
+
lines.push('- a: approve selected pending contact');
|
|
432
|
+
lines.push('- m: mark selected session as read');
|
|
433
|
+
lines.push('- t: open task list view for selected session');
|
|
434
|
+
lines.push('- x: cancel selected task (in task views)');
|
|
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');
|
|
438
|
+
lines.push('- o: open local history paging (detail view)');
|
|
439
|
+
lines.push('- n / p: older / newer history page (history view)');
|
|
440
|
+
lines.push('- s or /: search local history (history view)');
|
|
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');
|
|
446
|
+
lines.push('- b: bind selected conversation to current chat session');
|
|
447
|
+
lines.push('- c: clear selected binding');
|
|
448
|
+
lines.push('- q: quit');
|
|
449
|
+
} else if (view === 'history') {
|
|
450
|
+
lines.push('Conversation History');
|
|
451
|
+
if (!selected) {
|
|
452
|
+
lines.push('No session selected.');
|
|
453
|
+
} else {
|
|
454
|
+
lines.push(`session=${selected.session_key}`);
|
|
455
|
+
lines.push(`conversation=${selected.conversation_id || '(none)'}`);
|
|
456
|
+
lines.push(`remote=${selected.remote_did || '(unknown)'}`);
|
|
457
|
+
if (historySearchQuery) {
|
|
458
|
+
lines.push(`search=${historySearchQuery} results=${historySearchResults.length}`);
|
|
459
|
+
lines.push('actions=[s or /] search [n] clear-search [h/Esc] back');
|
|
460
|
+
} else {
|
|
461
|
+
lines.push(`page=${historyPage.pageIndex + 1} has_newer=${historyPage.hasNewer} has_older=${historyPage.hasOlder}`);
|
|
462
|
+
lines.push('actions=[s or /] search [n] older [p] newer [h/Esc] back');
|
|
463
|
+
}
|
|
464
|
+
lines.push('');
|
|
465
|
+
lines.push(...((historySearchQuery ? historySearchResults : historyPage.messages).length
|
|
466
|
+
? (historySearchQuery ? historySearchResults : historyPage.messages).map((message) => formatMessageRow(message))
|
|
467
|
+
: ['- none']));
|
|
468
|
+
}
|
|
469
|
+
} else if (view === 'task-detail') {
|
|
470
|
+
lines.push('Task Result Detail');
|
|
471
|
+
if (!selected) {
|
|
472
|
+
lines.push('No session selected.');
|
|
473
|
+
} else if (!selectedTask) {
|
|
474
|
+
lines.push('No task selected.');
|
|
475
|
+
} else {
|
|
476
|
+
lines.push(`session=${selected.session_key}`);
|
|
477
|
+
lines.push(`conversation=${selected.conversation_id || '(none)'}`);
|
|
478
|
+
lines.push(`remote=${selected.remote_did || '(unknown)'}`);
|
|
479
|
+
lines.push('');
|
|
480
|
+
lines.push(`task=${selectedTask.task_id}`);
|
|
481
|
+
lines.push(`title=${selectedTask.title || '(none)'}`);
|
|
482
|
+
lines.push(`status=${selectedTask.status}`);
|
|
483
|
+
lines.push(`updated_at=${formatTs(selectedTask.updated_at, false)}`);
|
|
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}`);
|
|
489
|
+
lines.push('actions=[x] cancel-task [y] dump-stdout [j/k] switch-task [h/Esc] back-to-tasks');
|
|
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
|
+
}
|
|
496
|
+
lines.push('Result');
|
|
497
|
+
lines.push(selectedTask.result_summary || '(none)');
|
|
498
|
+
lines.push('');
|
|
499
|
+
lines.push('Error');
|
|
500
|
+
lines.push(selectedTask.error_message ? `${selectedTask.error_code || 'E_TASK'} ${selectedTask.error_message}` : '(none)');
|
|
501
|
+
lines.push('');
|
|
502
|
+
lines.push('Tasks In Session');
|
|
503
|
+
lines.push(...tasks.map((task, idx) => `${idx === selectedTaskIndex ? '>' : ' '} ${task.task_id} [${task.status}] ${truncateLine(task.title || task.result_summary || '', 70)}`));
|
|
504
|
+
}
|
|
505
|
+
} else if (view === 'tasks') {
|
|
506
|
+
lines.push('Task Detail');
|
|
507
|
+
if (!selected) {
|
|
508
|
+
lines.push('No session selected.');
|
|
509
|
+
} else if (!selectedTask) {
|
|
510
|
+
lines.push('No tasks in the selected session.');
|
|
511
|
+
} else {
|
|
512
|
+
lines.push(`session=${selected.session_key}`);
|
|
513
|
+
lines.push(`conversation=${selected.conversation_id || '(none)'}`);
|
|
514
|
+
lines.push(`remote=${selected.remote_did || '(unknown)'}`);
|
|
515
|
+
lines.push('');
|
|
516
|
+
lines.push(`task=${selectedTask.task_id}`);
|
|
517
|
+
lines.push(`title=${selectedTask.title || '(none)'}`);
|
|
518
|
+
lines.push(`status=${selectedTask.status}`);
|
|
519
|
+
lines.push(`updated_at=${formatTs(selectedTask.updated_at, false)}`);
|
|
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}`);
|
|
523
|
+
if (selectedTask.result_summary) lines.push(`result=${selectedTask.result_summary}`);
|
|
524
|
+
if (selectedTask.error_code || selectedTask.error_message) {
|
|
525
|
+
lines.push(`error=${selectedTask.error_code || 'E_TASK'} ${selectedTask.error_message || ''}`.trim());
|
|
526
|
+
}
|
|
527
|
+
lines.push(`actions=[Enter/l] full-detail [x] cancel-task [h/Esc] back [j/k] select-task`);
|
|
528
|
+
lines.push('');
|
|
529
|
+
lines.push('Tasks In Session');
|
|
530
|
+
lines.push(...tasks.map((task, idx) => `${idx === selectedTaskIndex ? '>' : ' '} ${task.task_id} [${task.status}] ${truncateLine(task.title || task.result_summary || '', 70)}`));
|
|
531
|
+
}
|
|
532
|
+
} else {
|
|
533
|
+
lines.push('Sessions');
|
|
534
|
+
lines.push(...(sessions.length
|
|
535
|
+
? sessions.slice(0, 20).map((session) => formatSessionRow(session, session.session_key === (selected?.session_key || '')))
|
|
536
|
+
: [' (no sessions)']));
|
|
537
|
+
lines.push('');
|
|
538
|
+
|
|
539
|
+
if (selected) {
|
|
540
|
+
lines.push(view === 'detail' ? 'Selected Session Detail' : 'Selected Session');
|
|
541
|
+
lines.push(`session=${selected.session_key}`);
|
|
542
|
+
lines.push(`conversation=${selected.conversation_id || '(none)'}`);
|
|
543
|
+
lines.push(`remote=${selected.remote_did || '(unknown)'}`);
|
|
544
|
+
lines.push(`trust=${selected.trust_state} unread=${selected.unread_count}`);
|
|
545
|
+
lines.push(`last_preview=${selected.last_message_preview || '(none)'}`);
|
|
546
|
+
lines.push(`binding=${selected.binding?.session_key || '(unbound)'}`);
|
|
547
|
+
lines.push(`current_chat=${hostState.activeChatSession || '(none)'}`);
|
|
548
|
+
if (selected.binding_alert) {
|
|
549
|
+
lines.push(`needs_rebind=true`);
|
|
550
|
+
lines.push(`warning=${selected.binding_alert.message}`);
|
|
551
|
+
} else {
|
|
552
|
+
lines.push('needs_rebind=false');
|
|
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
|
+
}
|
|
565
|
+
const actionBar = [
|
|
566
|
+
selected.trust_state === 'pending' ? '[a] approve' : null,
|
|
567
|
+
'[A] apply-rec',
|
|
568
|
+
'[D] dismiss-rec',
|
|
569
|
+
'[R] reopen-rec',
|
|
570
|
+
'[m] mark-read',
|
|
571
|
+
'[d] demo',
|
|
572
|
+
'[p] reply',
|
|
573
|
+
'[S] summary',
|
|
574
|
+
'[o] history',
|
|
575
|
+
'[t] tasks',
|
|
576
|
+
'[b] bind-current',
|
|
577
|
+
'[c] clear-binding',
|
|
578
|
+
].filter(Boolean).join(' ');
|
|
579
|
+
lines.push(`actions=${actionBar}`);
|
|
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('');
|
|
594
|
+
lines.push('Tasks');
|
|
595
|
+
lines.push(...(tasks.length
|
|
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)}`)
|
|
597
|
+
: ['- none']));
|
|
598
|
+
if (view === 'detail') {
|
|
599
|
+
lines.push('');
|
|
600
|
+
lines.push('Recent Messages');
|
|
601
|
+
lines.push(...(messages.length ? messages.map((message) => formatMessageRow(message)) : ['- none']));
|
|
602
|
+
}
|
|
603
|
+
lines.push('');
|
|
604
|
+
lines.push('Audit');
|
|
605
|
+
lines.push(...(auditEvents.length
|
|
606
|
+
? auditEvents.map((event) => `- ${event.event_type} ${event.action || event.outcome || ''} ${truncateLine(event.explanation || '', 80)}`)
|
|
607
|
+
: ['- none']));
|
|
608
|
+
} else {
|
|
609
|
+
lines.push('No session selected.');
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
lines.push('');
|
|
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');
|
|
615
|
+
return lines.join('\n');
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
async function runHostTui(identityPath, opts) {
|
|
619
|
+
const once = !!opts.once;
|
|
620
|
+
const refreshMs = Math.max(500, Number.parseInt(String(opts.refreshMs || '2000'), 10) || 2000);
|
|
621
|
+
|
|
622
|
+
const uiState = {
|
|
623
|
+
selectedSessionKey: null,
|
|
624
|
+
view: 'overview',
|
|
625
|
+
selectedTaskIndex: 0,
|
|
626
|
+
statusMessage: '(ready)',
|
|
627
|
+
statusLevel: 'info',
|
|
628
|
+
statusExpiresAt: 0,
|
|
629
|
+
statusAt: 0,
|
|
630
|
+
selectedHistoryPageIndex: 0,
|
|
631
|
+
historySearchQuery: '',
|
|
632
|
+
publicLinkUrl: '',
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
const render = () => {
|
|
636
|
+
if (uiState.statusExpiresAt && Date.now() >= uiState.statusExpiresAt) {
|
|
637
|
+
uiState.statusMessage = '(ready)';
|
|
638
|
+
uiState.statusLevel = 'info';
|
|
639
|
+
uiState.statusExpiresAt = 0;
|
|
640
|
+
uiState.statusAt = 0;
|
|
641
|
+
}
|
|
642
|
+
const hostState = buildHostState(
|
|
643
|
+
identityPath,
|
|
644
|
+
uiState.selectedSessionKey,
|
|
645
|
+
uiState.selectedHistoryPageIndex,
|
|
646
|
+
uiState.historySearchQuery,
|
|
647
|
+
);
|
|
648
|
+
uiState.selectedSessionKey = hostState.selectedSession?.session_key || hostState.sessions[0]?.session_key || null;
|
|
649
|
+
uiState.selectedHistoryPageIndex = hostState.selectedHistoryPage?.pageIndex || 0;
|
|
650
|
+
const screen = renderHostTuiScreen(hostState, uiState);
|
|
651
|
+
return { hostState, screen };
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
if (once) {
|
|
655
|
+
void maybeEnsureHostedPublicLink(identityPath).then((res) => {
|
|
656
|
+
if (res?.data?.public_url) uiState.publicLinkUrl = res.data.public_url;
|
|
657
|
+
});
|
|
658
|
+
const { screen } = render();
|
|
659
|
+
console.log(screen);
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
664
|
+
console.error('Host TUI requires a TTY. Use --once for a snapshot in non-interactive environments.');
|
|
665
|
+
process.exit(1);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
let interval = null;
|
|
669
|
+
const redraw = () => {
|
|
670
|
+
const rendered = render();
|
|
671
|
+
clearScreen();
|
|
672
|
+
process.stdout.write(`${rendered.screen}\n`);
|
|
673
|
+
return rendered.hostState;
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
let latestState = redraw();
|
|
677
|
+
readline.emitKeypressEvents(process.stdin);
|
|
678
|
+
if (process.stdin.setRawMode) process.stdin.setRawMode(true);
|
|
679
|
+
|
|
680
|
+
const cleanup = () => {
|
|
681
|
+
if (interval) clearInterval(interval);
|
|
682
|
+
if (process.stdin.setRawMode) process.stdin.setRawMode(false);
|
|
683
|
+
process.stdin.pause();
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
const setStatus = (message, level = 'info', ttlMs = 5000) => {
|
|
687
|
+
uiState.statusMessage = truncateLine(message || '(ready)', 160);
|
|
688
|
+
uiState.statusLevel = level;
|
|
689
|
+
uiState.statusExpiresAt = ttlMs > 0 ? Date.now() + ttlMs : 0;
|
|
690
|
+
uiState.statusAt = Date.now();
|
|
691
|
+
};
|
|
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
|
+
|
|
819
|
+
const promptLine = async (question) => {
|
|
820
|
+
stopInterval();
|
|
821
|
+
if (process.stdin.setRawMode) process.stdin.setRawMode(false);
|
|
822
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
823
|
+
try {
|
|
824
|
+
const answer = await new Promise((resolve) => {
|
|
825
|
+
rl.question(question, resolve);
|
|
826
|
+
});
|
|
827
|
+
return String(answer ?? '');
|
|
828
|
+
} finally {
|
|
829
|
+
rl.close();
|
|
830
|
+
if (process.stdin.setRawMode) process.stdin.setRawMode(true);
|
|
831
|
+
startInterval();
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
const promptMultiline = async (question) => {
|
|
836
|
+
stopInterval();
|
|
837
|
+
if (process.stdin.setRawMode) process.stdin.setRawMode(false);
|
|
838
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
839
|
+
const lines = [];
|
|
840
|
+
try {
|
|
841
|
+
process.stdout.write(`${question}\nFinish with a single "." on its own line.\n`);
|
|
842
|
+
while (true) {
|
|
843
|
+
// eslint-disable-next-line no-await-in-loop
|
|
844
|
+
const line = await new Promise((resolve) => rl.question('> ', resolve));
|
|
845
|
+
const text = String(line ?? '');
|
|
846
|
+
if (text === '.') break;
|
|
847
|
+
lines.push(text);
|
|
848
|
+
}
|
|
849
|
+
return lines.join('\n').trim();
|
|
850
|
+
} finally {
|
|
851
|
+
rl.close();
|
|
852
|
+
if (process.stdin.setRawMode) process.stdin.setRawMode(true);
|
|
853
|
+
startInterval();
|
|
854
|
+
}
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
const dumpStdoutBlock = async (title, body) => {
|
|
858
|
+
stopInterval();
|
|
859
|
+
if (process.stdin.setRawMode) process.stdin.setRawMode(false);
|
|
860
|
+
try {
|
|
861
|
+
process.stdout.write(`\n===== ${title} =====\n${body}\n===== end =====\nPress Enter to return...`);
|
|
862
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
863
|
+
await new Promise((resolve) => rl.question('', resolve));
|
|
864
|
+
rl.close();
|
|
865
|
+
} finally {
|
|
866
|
+
if (process.stdin.setRawMode) process.stdin.setRawMode(true);
|
|
867
|
+
startInterval();
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
|
|
871
|
+
const confirmAction = async (question) => {
|
|
872
|
+
const answer = await promptLine(`${question} [y/N] `);
|
|
873
|
+
return answer.trim().toLowerCase() === 'y';
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
const moveSelection = (delta) => {
|
|
877
|
+
if (uiState.view === 'tasks' || uiState.view === 'task-detail') {
|
|
878
|
+
const tasks = latestState.selectedTasks || [];
|
|
879
|
+
if (!tasks.length) return;
|
|
880
|
+
uiState.selectedTaskIndex = Math.min(tasks.length - 1, Math.max(0, uiState.selectedTaskIndex + delta));
|
|
881
|
+
} else if (uiState.view === 'history' && !uiState.historySearchQuery) {
|
|
882
|
+
if (delta > 0 && latestState.selectedHistoryPage?.hasOlder) uiState.selectedHistoryPageIndex += 1;
|
|
883
|
+
if (delta < 0 && latestState.selectedHistoryPage?.hasNewer) uiState.selectedHistoryPageIndex = Math.max(0, uiState.selectedHistoryPageIndex - 1);
|
|
884
|
+
} else {
|
|
885
|
+
const sessions = latestState.sessions || [];
|
|
886
|
+
if (!sessions.length) return;
|
|
887
|
+
const idx = Math.max(0, sessions.findIndex((session) => session.session_key === uiState.selectedSessionKey));
|
|
888
|
+
const next = Math.min(sessions.length - 1, Math.max(0, idx + delta));
|
|
889
|
+
uiState.selectedSessionKey = sessions[next].session_key;
|
|
890
|
+
uiState.selectedTaskIndex = 0;
|
|
891
|
+
uiState.selectedHistoryPageIndex = 0;
|
|
892
|
+
uiState.historySearchQuery = '';
|
|
893
|
+
}
|
|
894
|
+
latestState = redraw();
|
|
895
|
+
};
|
|
896
|
+
|
|
897
|
+
const jumpSelection = (target) => {
|
|
898
|
+
if (uiState.view === 'tasks' || uiState.view === 'task-detail') {
|
|
899
|
+
const tasks = latestState.selectedTasks || [];
|
|
900
|
+
if (!tasks.length) return;
|
|
901
|
+
uiState.selectedTaskIndex = target;
|
|
902
|
+
} else if (uiState.view === 'history') {
|
|
903
|
+
uiState.selectedHistoryPageIndex = 0;
|
|
904
|
+
} else {
|
|
905
|
+
const sessions = latestState.sessions || [];
|
|
906
|
+
if (!sessions.length) return;
|
|
907
|
+
uiState.selectedSessionKey = sessions[target].session_key;
|
|
908
|
+
uiState.selectedTaskIndex = 0;
|
|
909
|
+
uiState.selectedHistoryPageIndex = 0;
|
|
910
|
+
uiState.historySearchQuery = '';
|
|
911
|
+
}
|
|
912
|
+
latestState = redraw();
|
|
913
|
+
};
|
|
914
|
+
|
|
915
|
+
const stopInterval = () => {
|
|
916
|
+
if (interval) clearInterval(interval);
|
|
917
|
+
interval = null;
|
|
918
|
+
};
|
|
919
|
+
|
|
920
|
+
const startInterval = () => {
|
|
921
|
+
stopInterval();
|
|
922
|
+
interval = setInterval(() => {
|
|
923
|
+
latestState = redraw();
|
|
924
|
+
}, refreshMs);
|
|
925
|
+
};
|
|
926
|
+
|
|
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
|
+
});
|
|
935
|
+
|
|
936
|
+
process.stdin.on('keypress', async (_str, key) => {
|
|
937
|
+
if (key?.name === 'q' || (key?.ctrl && key?.name === 'c')) {
|
|
938
|
+
cleanup();
|
|
939
|
+
process.stdout.write('\n');
|
|
940
|
+
process.exit(0);
|
|
941
|
+
}
|
|
942
|
+
if (key?.name === 'up' || key?.name === 'k') return moveSelection(-1);
|
|
943
|
+
if (key?.name === 'down' || key?.name === 'j') return moveSelection(1);
|
|
944
|
+
if (key?.name === 'g') return jumpSelection(0);
|
|
945
|
+
if (_str === 'G') {
|
|
946
|
+
const max = uiState.view === 'tasks' || uiState.view === 'task-detail'
|
|
947
|
+
? Math.max(0, (latestState.selectedTasks || []).length - 1)
|
|
948
|
+
: Math.max(0, (latestState.sessions || []).length - 1);
|
|
949
|
+
return jumpSelection(max);
|
|
950
|
+
}
|
|
951
|
+
if (key?.name === 'return' || key?.name === 'enter' || key?.name === 'l') {
|
|
952
|
+
if (uiState.view === 'tasks') {
|
|
953
|
+
uiState.view = 'task-detail';
|
|
954
|
+
} else {
|
|
955
|
+
uiState.view = 'detail';
|
|
956
|
+
}
|
|
957
|
+
latestState = redraw();
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
if (key?.name === 'escape' || key?.name === 'h') {
|
|
961
|
+
if (uiState.view === 'task-detail') uiState.view = 'tasks';
|
|
962
|
+
else if (uiState.view === 'history') uiState.view = 'detail';
|
|
963
|
+
else uiState.view = 'overview';
|
|
964
|
+
latestState = redraw();
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
if (_str === '?') {
|
|
968
|
+
uiState.view = uiState.view === 'help' ? 'overview' : 'help';
|
|
969
|
+
latestState = redraw();
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
if (_str === 'n' && uiState.view === 'history') {
|
|
973
|
+
if (uiState.historySearchQuery) {
|
|
974
|
+
uiState.historySearchQuery = '';
|
|
975
|
+
setStatus('Cleared history search.', 'info');
|
|
976
|
+
latestState = redraw();
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
if (latestState.selectedHistoryPage?.hasOlder) {
|
|
980
|
+
uiState.selectedHistoryPageIndex += 1;
|
|
981
|
+
setStatus(`History page ${uiState.selectedHistoryPageIndex + 1}`, 'info');
|
|
982
|
+
latestState = redraw();
|
|
983
|
+
}
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
if (_str === 'p' && uiState.view === 'history') {
|
|
987
|
+
if (uiState.historySearchQuery) return;
|
|
988
|
+
if (latestState.selectedHistoryPage?.hasNewer) {
|
|
989
|
+
uiState.selectedHistoryPageIndex = Math.max(0, uiState.selectedHistoryPageIndex - 1);
|
|
990
|
+
setStatus(`History page ${uiState.selectedHistoryPageIndex + 1}`, 'info');
|
|
991
|
+
latestState = redraw();
|
|
992
|
+
}
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
if (key?.name === 't') {
|
|
996
|
+
uiState.view = 'tasks';
|
|
997
|
+
uiState.selectedTaskIndex = 0;
|
|
998
|
+
uiState.selectedHistoryPageIndex = 0;
|
|
999
|
+
setStatus('Opened task list view.', 'info');
|
|
1000
|
+
latestState = redraw();
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
if (key?.name === 'r') {
|
|
1004
|
+
setStatus('Refreshed.', 'info');
|
|
1005
|
+
latestState = redraw();
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
if (key?.name === 'm') {
|
|
1009
|
+
const selected = latestState.selectedSession;
|
|
1010
|
+
if (!selected) return;
|
|
1011
|
+
const store = openStore(identityPath);
|
|
1012
|
+
try {
|
|
1013
|
+
const sessions = new SessionManager(store);
|
|
1014
|
+
sessions.markRead(selected.session_key);
|
|
1015
|
+
setStatus(`Marked session as read: ${selected.session_key}`, 'ok');
|
|
1016
|
+
} finally {
|
|
1017
|
+
store.close();
|
|
1018
|
+
}
|
|
1019
|
+
latestState = redraw();
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
if (key?.name === 'a') {
|
|
1023
|
+
const selected = latestState.selectedSession;
|
|
1024
|
+
if (!selected?.conversation_id || selected.trust_state !== 'pending') return;
|
|
1025
|
+
const confirmed = await confirmAction(`Approve pending conversation ${selected.conversation_id}\nRemote DID: ${selected.remote_did || '(unknown)'}\nProceed?`);
|
|
1026
|
+
if (confirmed) {
|
|
1027
|
+
const { client, store } = await getClientWithStore(identityPath);
|
|
1028
|
+
try {
|
|
1029
|
+
const res = await client.approveContact(selected.conversation_id);
|
|
1030
|
+
setStatus(res.ok
|
|
1031
|
+
? `Approved conversation ${selected.conversation_id}${res.data?.dm_conversation_id ? ` -> ${res.data.dm_conversation_id}` : ''}`
|
|
1032
|
+
: `Approve failed: ${res.error?.message || 'unknown error'}`, res.ok ? 'ok' : 'err');
|
|
1033
|
+
} finally {
|
|
1034
|
+
store.close();
|
|
1035
|
+
}
|
|
1036
|
+
} else {
|
|
1037
|
+
setStatus('Approve cancelled.', 'warn');
|
|
1038
|
+
}
|
|
1039
|
+
latestState = redraw();
|
|
1040
|
+
return;
|
|
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
|
+
}
|
|
1071
|
+
if (key?.name === 'p' && uiState.view === 'detail') {
|
|
1072
|
+
const selected = latestState.selectedSession;
|
|
1073
|
+
if (!selected?.conversation_id) return;
|
|
1074
|
+
const input = await promptMultiline(`Reply to ${selected.remote_did || selected.conversation_id}`);
|
|
1075
|
+
const message = input.trim();
|
|
1076
|
+
if (!message) {
|
|
1077
|
+
setStatus('Reply cancelled.', 'warn');
|
|
1078
|
+
latestState = redraw();
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
const { client, store } = await getClientWithStore(identityPath);
|
|
1082
|
+
try {
|
|
1083
|
+
const res = await client.sendMessage(selected.conversation_id, SCHEMA_TEXT, { text: message });
|
|
1084
|
+
setStatus(res.ok
|
|
1085
|
+
? `Sent reply to ${selected.remote_did || selected.conversation_id} (${res.data?.message_id || 'queued'})`
|
|
1086
|
+
: `Reply failed: ${res.error?.message || 'unknown error'}`, res.ok ? 'ok' : 'err');
|
|
1087
|
+
} finally {
|
|
1088
|
+
store.close();
|
|
1089
|
+
}
|
|
1090
|
+
latestState = redraw();
|
|
1091
|
+
return;
|
|
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
|
+
}
|
|
1138
|
+
if (key?.name === 'o' && uiState.view === 'detail') {
|
|
1139
|
+
uiState.view = 'history';
|
|
1140
|
+
uiState.selectedHistoryPageIndex = 0;
|
|
1141
|
+
uiState.historySearchQuery = '';
|
|
1142
|
+
setStatus('Opened local history view.', 'info');
|
|
1143
|
+
latestState = redraw();
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
if ((_str === '/' || key?.name === 's') && uiState.view === 'history') {
|
|
1147
|
+
const answer = await promptLine(`Search history (${latestState.selectedSession?.conversation_id || 'current conversation'}). Empty clears search: `);
|
|
1148
|
+
const query = answer.trim();
|
|
1149
|
+
uiState.historySearchQuery = query;
|
|
1150
|
+
uiState.selectedHistoryPageIndex = 0;
|
|
1151
|
+
setStatus(query ? `History search: ${query}` : 'Cleared history search.', 'info');
|
|
1152
|
+
latestState = redraw();
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
if (key?.name === 'b') {
|
|
1156
|
+
const selected = (latestState.sessions || []).find((session) => session.session_key === uiState.selectedSessionKey);
|
|
1157
|
+
if (!selected?.conversation_id) return;
|
|
1158
|
+
const current = latestState.activeChatSession || '(none)';
|
|
1159
|
+
const previous = selected.binding?.session_key || '(unbound)';
|
|
1160
|
+
const confirmed = await confirmAction(`Rebind conversation ${selected.conversation_id}\nRemote DID: ${selected.remote_did || '(unknown)'}\nCurrent chat: ${current}\nPrevious binding: ${previous}\nProceed?`);
|
|
1161
|
+
if (confirmed) {
|
|
1162
|
+
if (!latestState.activeChatSession) {
|
|
1163
|
+
setStatus('Rebind failed: no active chat session.', 'err');
|
|
1164
|
+
latestState = redraw();
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
setSessionBinding(selected.conversation_id, latestState.activeChatSession);
|
|
1168
|
+
setStatus(`Rebound ${selected.conversation_id} -> ${latestState.activeChatSession}`, 'ok');
|
|
1169
|
+
} else {
|
|
1170
|
+
setStatus('Rebind cancelled.', 'warn');
|
|
1171
|
+
}
|
|
1172
|
+
latestState = redraw();
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
if (key?.name === 'c') {
|
|
1176
|
+
const selected = (latestState.sessions || []).find((session) => session.session_key === uiState.selectedSessionKey);
|
|
1177
|
+
if (!selected?.conversation_id) return;
|
|
1178
|
+
removeSessionBinding(selected.conversation_id);
|
|
1179
|
+
setStatus(`Cleared binding for ${selected.conversation_id}`, 'ok');
|
|
1180
|
+
latestState = redraw();
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
if (key?.name === 'x' && (uiState.view === 'tasks' || uiState.view === 'task-detail')) {
|
|
1184
|
+
const selected = latestState.selectedSession;
|
|
1185
|
+
const tasks = latestState.selectedTasks || [];
|
|
1186
|
+
const task = tasks[Math.max(0, Math.min(uiState.selectedTaskIndex, Math.max(0, tasks.length - 1)))] || null;
|
|
1187
|
+
if (!selected?.conversation_id || !task) return;
|
|
1188
|
+
const confirmed = await confirmAction(`Cancel task ${task.task_id}\nTitle: ${task.title || '(none)'}\nStatus: ${task.status}\nProceed?`);
|
|
1189
|
+
if (confirmed) {
|
|
1190
|
+
const { client, store } = await getClientWithStore(identityPath);
|
|
1191
|
+
try {
|
|
1192
|
+
const res = await client.cancelTask(selected.conversation_id, task.task_id);
|
|
1193
|
+
setStatus(res.ok
|
|
1194
|
+
? `Cancel requested for task ${task.task_id}`
|
|
1195
|
+
: `Cancel failed: ${res.error?.message || 'unknown error'}`, res.ok ? 'ok' : 'err');
|
|
1196
|
+
} finally {
|
|
1197
|
+
store.close();
|
|
1198
|
+
}
|
|
1199
|
+
} else {
|
|
1200
|
+
setStatus('Cancel task aborted.', 'warn');
|
|
1201
|
+
}
|
|
1202
|
+
latestState = redraw();
|
|
1203
|
+
return;
|
|
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
|
+
}
|
|
1220
|
+
if (key?.name === 'y' && uiState.view === 'task-detail') {
|
|
1221
|
+
const tasks = latestState.selectedTasks || [];
|
|
1222
|
+
const task = tasks[Math.max(0, Math.min(uiState.selectedTaskIndex, Math.max(0, tasks.length - 1)))] || null;
|
|
1223
|
+
if (!task) return;
|
|
1224
|
+
const formatAnswer = await promptLine('Export format [j]son / [p]lain (default plain): ');
|
|
1225
|
+
const format = formatAnswer.trim().toLowerCase().startsWith('j') ? 'json' : 'plain';
|
|
1226
|
+
await dumpStdoutBlock(`task ${task.task_id} (${format})`, buildTaskExportBody(task, format));
|
|
1227
|
+
setStatus(`Dumped task ${task.task_id} to stdout as ${format}.`, 'ok');
|
|
1228
|
+
latestState = redraw();
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
|
|
80
1234
|
/** Format timestamp for display: human-readable by default, raw ms when --raw. */
|
|
81
1235
|
function formatTs(tsMs, raw) {
|
|
82
1236
|
if (raw) return String(tsMs);
|
|
@@ -94,6 +1248,19 @@ function makeClient(id, identityPath) {
|
|
|
94
1248
|
});
|
|
95
1249
|
}
|
|
96
1250
|
|
|
1251
|
+
function makeClientWithStore(id, identityPath) {
|
|
1252
|
+
const p = identityPath ?? getEffectiveIdentityPath();
|
|
1253
|
+
const store = openStore(p);
|
|
1254
|
+
const client = new PingAgentClient({
|
|
1255
|
+
serverUrl: id.serverUrl ?? DEFAULT_SERVER,
|
|
1256
|
+
identity: id,
|
|
1257
|
+
accessToken: id.accessToken ?? '',
|
|
1258
|
+
onTokenRefreshed: (token, expiresAt) => updateStoredToken(token, expiresAt, p),
|
|
1259
|
+
store,
|
|
1260
|
+
});
|
|
1261
|
+
return { client, store };
|
|
1262
|
+
}
|
|
1263
|
+
|
|
97
1264
|
/** Load identity, proactively refresh token if near/past expiry, then return client. */
|
|
98
1265
|
async function getClient(identityPath) {
|
|
99
1266
|
const p = identityPath ?? getEffectiveIdentityPath();
|
|
@@ -105,6 +1272,61 @@ async function getClient(identityPath) {
|
|
|
105
1272
|
return client;
|
|
106
1273
|
}
|
|
107
1274
|
|
|
1275
|
+
async function getClientWithStore(identityPath) {
|
|
1276
|
+
const p = identityPath ?? getEffectiveIdentityPath();
|
|
1277
|
+
let id = loadIdentity(p);
|
|
1278
|
+
await ensureTokenValid(p, id.serverUrl);
|
|
1279
|
+
id = loadIdentity(p);
|
|
1280
|
+
const { client, store } = makeClientWithStore(id, p);
|
|
1281
|
+
await client.ensureEncryptionKeyPublished().catch(() => undefined);
|
|
1282
|
+
return { client, store };
|
|
1283
|
+
}
|
|
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
|
+
|
|
108
1330
|
const program = new Command();
|
|
109
1331
|
program
|
|
110
1332
|
.name('pingagent')
|
|
@@ -1438,6 +2660,486 @@ billing
|
|
|
1438
2660
|
}
|
|
1439
2661
|
});
|
|
1440
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
|
+
|
|
3124
|
+
const host = program
|
|
3125
|
+
.command('host')
|
|
3126
|
+
.description('Headless runtime inspection and control for PingAgent host state');
|
|
3127
|
+
|
|
3128
|
+
host
|
|
3129
|
+
.command('tui')
|
|
3130
|
+
.description('Start a terminal UI for runtime, sessions, bindings, and rebind actions')
|
|
3131
|
+
.option('--once', 'Print one snapshot and exit')
|
|
3132
|
+
.option('--refresh-ms <ms>', 'Refresh interval in interactive mode', '2000')
|
|
3133
|
+
.option('--profile <name>', 'Use profile from ~/.pingagent/<name>')
|
|
3134
|
+
.action(async (opts) => {
|
|
3135
|
+
const identityPath = getIdentityPathForCommand(opts);
|
|
3136
|
+
if (!identityExists(identityPath)) {
|
|
3137
|
+
console.error('No identity found. Run: pingagent init');
|
|
3138
|
+
process.exit(1);
|
|
3139
|
+
}
|
|
3140
|
+
await runHostTui(identityPath, opts);
|
|
3141
|
+
});
|
|
3142
|
+
|
|
1441
3143
|
program
|
|
1442
3144
|
.command('web')
|
|
1443
3145
|
.description('Start local web UI and host panel for debugging, runtime inspection, trust policy, and audit. By default scans ~/.pingagent for profiles; use --identity-dir to lock to one profile.')
|