@pingagent/sdk 0.1.9 → 0.1.10
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 +830 -0
- package/dist/chunk-2Y6YRKTO.js +3100 -0
- package/dist/chunk-RMIRCSQ6.js +3042 -0
- package/dist/index.d.ts +37 -1
- package/dist/index.js +27 -3
- package/dist/web-server.js +175 -3
- package/package.json +1 -1
package/bin/pingagent.js
CHANGED
|
@@ -3,6 +3,7 @@ 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';
|
|
6
7
|
import {
|
|
7
8
|
PingAgentClient,
|
|
8
9
|
generateIdentity,
|
|
@@ -15,6 +16,19 @@ import {
|
|
|
15
16
|
ContactManager,
|
|
16
17
|
HistoryManager,
|
|
17
18
|
A2AAdapter,
|
|
19
|
+
SessionManager,
|
|
20
|
+
TaskThreadManager,
|
|
21
|
+
TrustPolicyAuditManager,
|
|
22
|
+
defaultTrustPolicyDoc,
|
|
23
|
+
normalizeTrustPolicyDoc,
|
|
24
|
+
getActiveSessionFilePath,
|
|
25
|
+
getSessionMapFilePath,
|
|
26
|
+
getSessionBindingAlertsFilePath,
|
|
27
|
+
readCurrentActiveSessionKey,
|
|
28
|
+
readSessionBindings,
|
|
29
|
+
readSessionBindingAlerts,
|
|
30
|
+
setSessionBinding,
|
|
31
|
+
removeSessionBinding,
|
|
18
32
|
} from '../dist/index.js';
|
|
19
33
|
import { ERROR_HINTS, SCHEMA_TEXT } from '@pingagent/schemas';
|
|
20
34
|
|
|
@@ -77,6 +91,780 @@ function openStore(identityPath) {
|
|
|
77
91
|
return new LocalStore(getStorePath(identityPath));
|
|
78
92
|
}
|
|
79
93
|
|
|
94
|
+
function getTrustPolicyPath(identityPath) {
|
|
95
|
+
return process.env.PINGAGENT_TRUST_POLICY_PATH
|
|
96
|
+
? resolvePath(process.env.PINGAGENT_TRUST_POLICY_PATH)
|
|
97
|
+
: path.join(path.dirname(identityPath), 'trust-policy.json');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function readTrustPolicyDoc(identityPath) {
|
|
101
|
+
const policyPath = getTrustPolicyPath(identityPath);
|
|
102
|
+
try {
|
|
103
|
+
if (!fs.existsSync(policyPath)) return { path: policyPath, doc: defaultTrustPolicyDoc() };
|
|
104
|
+
const raw = JSON.parse(fs.readFileSync(policyPath, 'utf-8'));
|
|
105
|
+
return { path: policyPath, doc: normalizeTrustPolicyDoc(raw) };
|
|
106
|
+
} catch {
|
|
107
|
+
return { path: policyPath, doc: defaultTrustPolicyDoc() };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function clearScreen() {
|
|
112
|
+
process.stdout.write('\x1Bc');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function ansi(code, text) {
|
|
116
|
+
return process.stdout.isTTY ? `\x1b[${code}m${text}\x1b[0m` : text;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function formatStatusLine(level, message) {
|
|
120
|
+
const tag = level === 'ok'
|
|
121
|
+
? ansi('32', '[OK]')
|
|
122
|
+
: level === 'warn'
|
|
123
|
+
? ansi('33', '[WARN]')
|
|
124
|
+
: level === 'err'
|
|
125
|
+
? ansi('31', '[ERR]')
|
|
126
|
+
: ansi('36', '[INFO]');
|
|
127
|
+
return `${tag} ${message || '(ready)'}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function formatStatusTimestamp(tsMs) {
|
|
131
|
+
if (!tsMs) return '';
|
|
132
|
+
return formatTs(tsMs, false);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function truncateLine(value, max = 100) {
|
|
136
|
+
const s = String(value ?? '');
|
|
137
|
+
return s.length <= max ? s : `${s.slice(0, max - 1)}…`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function formatSessionRow(session, selected) {
|
|
141
|
+
const marker = selected ? '>' : ' ';
|
|
142
|
+
const rebind = session.binding_alert ? ' !rebind' : '';
|
|
143
|
+
const trust = session.trust_state || 'unknown';
|
|
144
|
+
const unread = session.unread_count ?? 0;
|
|
145
|
+
const who = truncateLine(session.remote_did || session.conversation_id || 'unknown', 40);
|
|
146
|
+
return `${marker} ${who} [${trust}] unread=${unread}${rebind}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function formatMessageRow(message) {
|
|
150
|
+
const ts = formatTs(message.ts_ms ?? message.receivedAtMs ?? Date.now(), false);
|
|
151
|
+
const direction = message.direction || 'unknown';
|
|
152
|
+
const schema = message.schema || 'unknown';
|
|
153
|
+
const payload = message.payload || {};
|
|
154
|
+
const summary = schema === SCHEMA_TEXT && payload.text
|
|
155
|
+
? payload.text
|
|
156
|
+
: JSON.stringify(payload);
|
|
157
|
+
return `- ${ts} ${direction} ${schema} ${truncateLine(summary, 90)}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function buildTaskExportBody(task, format = 'plain') {
|
|
161
|
+
const payload = {
|
|
162
|
+
task_id: task.task_id,
|
|
163
|
+
title: task.title || null,
|
|
164
|
+
status: task.status,
|
|
165
|
+
updated_at: task.updated_at ?? null,
|
|
166
|
+
updated_at_display: task.updated_at ? formatTs(task.updated_at, false) : null,
|
|
167
|
+
started_at: task.started_at ?? null,
|
|
168
|
+
started_at_display: task.started_at ? formatTs(task.started_at, false) : null,
|
|
169
|
+
result: task.result_summary || null,
|
|
170
|
+
error: task.error_message
|
|
171
|
+
? {
|
|
172
|
+
code: task.error_code || 'E_TASK',
|
|
173
|
+
message: task.error_message,
|
|
174
|
+
}
|
|
175
|
+
: null,
|
|
176
|
+
};
|
|
177
|
+
if (format === 'json') return JSON.stringify(payload, null, 2);
|
|
178
|
+
return [
|
|
179
|
+
`task_id=${payload.task_id}`,
|
|
180
|
+
`title=${payload.title || '(none)'}`,
|
|
181
|
+
`status=${payload.status}`,
|
|
182
|
+
`updated_at=${payload.updated_at_display || '(none)'}`,
|
|
183
|
+
payload.started_at_display ? `started_at=${payload.started_at_display}` : null,
|
|
184
|
+
'',
|
|
185
|
+
'[result]',
|
|
186
|
+
payload.result || '(none)',
|
|
187
|
+
'',
|
|
188
|
+
'[error]',
|
|
189
|
+
payload.error ? `${payload.error.code} ${payload.error.message}` : '(none)',
|
|
190
|
+
].filter(Boolean).join('\n');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function loadHistoryPage(historyManager, conversationId, pageIndex, pageSize) {
|
|
194
|
+
if (!conversationId) return { messages: [], pageIndex: 0, hasOlder: false, hasNewer: false };
|
|
195
|
+
let page = Math.max(0, pageIndex || 0);
|
|
196
|
+
let messages = historyManager.listRecent(conversationId, pageSize);
|
|
197
|
+
if (messages.length === 0) {
|
|
198
|
+
return { messages: [], pageIndex: 0, hasOlder: false, hasNewer: false };
|
|
199
|
+
}
|
|
200
|
+
for (let i = 0; i < page; i++) {
|
|
201
|
+
const first = messages[0];
|
|
202
|
+
if (!first) {
|
|
203
|
+
page = i;
|
|
204
|
+
messages = [];
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
const older = first.seq != null
|
|
208
|
+
? historyManager.listBeforeSeq(conversationId, first.seq, pageSize)
|
|
209
|
+
: historyManager.listBeforeTs(conversationId, first.ts_ms, pageSize);
|
|
210
|
+
if (!older.length) {
|
|
211
|
+
page = i;
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
messages = older;
|
|
215
|
+
}
|
|
216
|
+
const first = messages[0];
|
|
217
|
+
const hasOlder = !!first && (
|
|
218
|
+
first.seq != null
|
|
219
|
+
? historyManager.listBeforeSeq(conversationId, first.seq, 1).length > 0
|
|
220
|
+
: historyManager.listBeforeTs(conversationId, first.ts_ms, 1).length > 0
|
|
221
|
+
);
|
|
222
|
+
return {
|
|
223
|
+
messages,
|
|
224
|
+
pageIndex: page,
|
|
225
|
+
hasOlder,
|
|
226
|
+
hasNewer: page > 0,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function buildHostState(identityPath, selectedSessionKey = null, historyPageIndex = 0, historySearchQuery = '') {
|
|
231
|
+
const identity = loadIdentity(identityPath);
|
|
232
|
+
const store = openStore(identityPath);
|
|
233
|
+
try {
|
|
234
|
+
const sessionManager = new SessionManager(store);
|
|
235
|
+
const taskManager = new TaskThreadManager(store);
|
|
236
|
+
const historyManager = new HistoryManager(store);
|
|
237
|
+
const auditManager = new TrustPolicyAuditManager(store);
|
|
238
|
+
const sessions = sessionManager.listRecentSessions(50);
|
|
239
|
+
const bindings = readSessionBindings();
|
|
240
|
+
const alerts = readSessionBindingAlerts();
|
|
241
|
+
const bindingByConversation = new Map(bindings.map((row) => [row.conversation_id, row]));
|
|
242
|
+
const alertByConversation = new Map(alerts.map((row) => [row.conversation_id, row]));
|
|
243
|
+
const activeChatSession = readCurrentActiveSessionKey();
|
|
244
|
+
const desiredSelectedSessionKey =
|
|
245
|
+
(selectedSessionKey && sessions.some((session) => session.session_key === selectedSessionKey) ? selectedSessionKey : null) ??
|
|
246
|
+
sessionManager.getActiveSession()?.session_key ??
|
|
247
|
+
sessions[0]?.session_key ??
|
|
248
|
+
null;
|
|
249
|
+
const policy = readTrustPolicyDoc(identityPath);
|
|
250
|
+
const runtimeMode = process.env.PINGAGENT_RUNTIME_MODE || 'bridge';
|
|
251
|
+
const sessionsWithMeta = sessions.map((session) => ({
|
|
252
|
+
...session,
|
|
253
|
+
binding: session.conversation_id ? (bindingByConversation.get(session.conversation_id) ?? null) : null,
|
|
254
|
+
binding_alert: session.conversation_id ? (alertByConversation.get(session.conversation_id) ?? null) : null,
|
|
255
|
+
is_active_chat_session: session.session_key === activeChatSession,
|
|
256
|
+
}));
|
|
257
|
+
const selectedSession = sessionsWithMeta.find((session) => session.session_key === desiredSelectedSessionKey) ?? null;
|
|
258
|
+
const selectedTasks = selectedSession
|
|
259
|
+
? taskManager.listBySession(selectedSession.session_key, 12)
|
|
260
|
+
: [];
|
|
261
|
+
const selectedAuditEvents = selectedSession
|
|
262
|
+
? auditManager.listBySession(selectedSession.session_key, 12)
|
|
263
|
+
: [];
|
|
264
|
+
const selectedMessages = selectedSession?.conversation_id
|
|
265
|
+
? historyManager.listRecent(selectedSession.conversation_id, 12)
|
|
266
|
+
: [];
|
|
267
|
+
const selectedHistoryPage = selectedSession?.conversation_id
|
|
268
|
+
? loadHistoryPage(historyManager, selectedSession.conversation_id, historyPageIndex, 20)
|
|
269
|
+
: { messages: [], pageIndex: 0, hasOlder: false, hasNewer: false };
|
|
270
|
+
const selectedHistorySearchResults = selectedSession?.conversation_id && historySearchQuery.trim()
|
|
271
|
+
? historyManager.search(historySearchQuery.trim(), { conversationId: selectedSession.conversation_id, limit: 50 })
|
|
272
|
+
: [];
|
|
273
|
+
const unreadTotal = sessionsWithMeta.reduce((sum, session) => sum + (session.unread_count ?? 0), 0);
|
|
274
|
+
const alertSessions = sessionsWithMeta.filter((session) => !!session.binding_alert).length;
|
|
275
|
+
return {
|
|
276
|
+
identity,
|
|
277
|
+
runtimeMode,
|
|
278
|
+
activeChatSession,
|
|
279
|
+
activeChatSessionFile: getActiveSessionFilePath(),
|
|
280
|
+
sessionMapPath: getSessionMapFilePath(),
|
|
281
|
+
sessionBindingAlertsPath: getSessionBindingAlertsFilePath(),
|
|
282
|
+
policyPath: policy.path,
|
|
283
|
+
policyDoc: policy.doc,
|
|
284
|
+
sessions: sessionsWithMeta,
|
|
285
|
+
tasks: taskManager.listRecent(30),
|
|
286
|
+
auditEvents: auditManager.listRecent(40),
|
|
287
|
+
selectedSession,
|
|
288
|
+
selectedTasks,
|
|
289
|
+
selectedAuditEvents,
|
|
290
|
+
selectedMessages,
|
|
291
|
+
selectedHistoryPage,
|
|
292
|
+
selectedHistorySearchQuery: historySearchQuery.trim(),
|
|
293
|
+
selectedHistorySearchResults,
|
|
294
|
+
unreadTotal,
|
|
295
|
+
alertSessions,
|
|
296
|
+
};
|
|
297
|
+
} finally {
|
|
298
|
+
store.close();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function renderHostTuiScreen(hostState, uiState) {
|
|
303
|
+
const sessions = hostState.sessions || [];
|
|
304
|
+
const selected = hostState.selectedSession || sessions[0] || null;
|
|
305
|
+
const tasks = hostState.selectedTasks || [];
|
|
306
|
+
const auditEvents = hostState.selectedAuditEvents || [];
|
|
307
|
+
const messages = hostState.selectedMessages || [];
|
|
308
|
+
const historyPage = hostState.selectedHistoryPage || { messages: [], pageIndex: 0, hasOlder: false, hasNewer: false };
|
|
309
|
+
const historySearchQuery = hostState.selectedHistorySearchQuery || '';
|
|
310
|
+
const historySearchResults = hostState.selectedHistorySearchResults || [];
|
|
311
|
+
const view = uiState?.view || 'overview';
|
|
312
|
+
const selectedTaskIndex = Math.max(0, Math.min(uiState?.selectedTaskIndex || 0, Math.max(0, tasks.length - 1)));
|
|
313
|
+
const selectedTask = tasks[selectedTaskIndex] || null;
|
|
314
|
+
const statusCountdown = uiState?.statusExpiresAt
|
|
315
|
+
? ` (${Math.max(0, Math.ceil((uiState.statusExpiresAt - Date.now()) / 1000))}s)`
|
|
316
|
+
: '';
|
|
317
|
+
const statusTs = uiState?.statusAt ? ` @ ${formatStatusTimestamp(uiState.statusAt)}` : '';
|
|
318
|
+
const lines = [
|
|
319
|
+
'PingAgent Host TUI',
|
|
320
|
+
`DID: ${hostState.identity.did}`,
|
|
321
|
+
`status=${formatStatusLine(uiState?.statusLevel || 'info', uiState?.statusMessage || '(ready)')}${statusTs}${statusCountdown}`,
|
|
322
|
+
`runtime_mode=${hostState.runtimeMode} active_chat_session=${hostState.activeChatSession || '(none)'}`,
|
|
323
|
+
`sessions=${sessions.length} unread_total=${hostState.unreadTotal ?? 0} alert_sessions=${hostState.alertSessions ?? 0} view=${view}`,
|
|
324
|
+
`policy=${hostState.policyPath}`,
|
|
325
|
+
`session_map=${hostState.sessionMapPath}`,
|
|
326
|
+
`binding_alerts=${hostState.sessionBindingAlertsPath}`,
|
|
327
|
+
'',
|
|
328
|
+
];
|
|
329
|
+
|
|
330
|
+
if (view === 'help') {
|
|
331
|
+
lines.push('Help');
|
|
332
|
+
lines.push('- ↑/↓ or j/k: move selection');
|
|
333
|
+
lines.push('- Enter or l: open detailed session view / task detail');
|
|
334
|
+
lines.push('- Esc or h: back to previous view');
|
|
335
|
+
lines.push('- g / G: jump to first / last session');
|
|
336
|
+
lines.push('- r: refresh now');
|
|
337
|
+
lines.push('- a: approve selected pending contact');
|
|
338
|
+
lines.push('- m: mark selected session as read');
|
|
339
|
+
lines.push('- t: open task list view for selected session');
|
|
340
|
+
lines.push('- x: cancel selected task (in task views)');
|
|
341
|
+
lines.push('- p: multiline reply prompt (detail view)');
|
|
342
|
+
lines.push('- o: open local history paging (detail view)');
|
|
343
|
+
lines.push('- n / p: older / newer history page (history view)');
|
|
344
|
+
lines.push('- s or /: search local history (history view)');
|
|
345
|
+
lines.push('- y: dump task detail to stdout (task-detail view, choose json/plain)');
|
|
346
|
+
lines.push('- b: bind selected conversation to current chat session');
|
|
347
|
+
lines.push('- c: clear selected binding');
|
|
348
|
+
lines.push('- q: quit');
|
|
349
|
+
} else if (view === 'history') {
|
|
350
|
+
lines.push('Conversation History');
|
|
351
|
+
if (!selected) {
|
|
352
|
+
lines.push('No session selected.');
|
|
353
|
+
} else {
|
|
354
|
+
lines.push(`session=${selected.session_key}`);
|
|
355
|
+
lines.push(`conversation=${selected.conversation_id || '(none)'}`);
|
|
356
|
+
lines.push(`remote=${selected.remote_did || '(unknown)'}`);
|
|
357
|
+
if (historySearchQuery) {
|
|
358
|
+
lines.push(`search=${historySearchQuery} results=${historySearchResults.length}`);
|
|
359
|
+
lines.push('actions=[s or /] search [n] clear-search [h/Esc] back');
|
|
360
|
+
} else {
|
|
361
|
+
lines.push(`page=${historyPage.pageIndex + 1} has_newer=${historyPage.hasNewer} has_older=${historyPage.hasOlder}`);
|
|
362
|
+
lines.push('actions=[s or /] search [n] older [p] newer [h/Esc] back');
|
|
363
|
+
}
|
|
364
|
+
lines.push('');
|
|
365
|
+
lines.push(...((historySearchQuery ? historySearchResults : historyPage.messages).length
|
|
366
|
+
? (historySearchQuery ? historySearchResults : historyPage.messages).map((message) => formatMessageRow(message))
|
|
367
|
+
: ['- none']));
|
|
368
|
+
}
|
|
369
|
+
} else if (view === 'task-detail') {
|
|
370
|
+
lines.push('Task Result Detail');
|
|
371
|
+
if (!selected) {
|
|
372
|
+
lines.push('No session selected.');
|
|
373
|
+
} else if (!selectedTask) {
|
|
374
|
+
lines.push('No task selected.');
|
|
375
|
+
} else {
|
|
376
|
+
lines.push(`session=${selected.session_key}`);
|
|
377
|
+
lines.push(`conversation=${selected.conversation_id || '(none)'}`);
|
|
378
|
+
lines.push(`remote=${selected.remote_did || '(unknown)'}`);
|
|
379
|
+
lines.push('');
|
|
380
|
+
lines.push(`task=${selectedTask.task_id}`);
|
|
381
|
+
lines.push(`title=${selectedTask.title || '(none)'}`);
|
|
382
|
+
lines.push(`status=${selectedTask.status}`);
|
|
383
|
+
lines.push(`updated_at=${formatTs(selectedTask.updated_at, false)}`);
|
|
384
|
+
if (selectedTask.started_at) lines.push(`started_at=${formatTs(selectedTask.started_at, false)}`);
|
|
385
|
+
lines.push('actions=[x] cancel-task [y] dump-stdout [j/k] switch-task [h/Esc] back-to-tasks');
|
|
386
|
+
lines.push('');
|
|
387
|
+
lines.push('Result');
|
|
388
|
+
lines.push(selectedTask.result_summary || '(none)');
|
|
389
|
+
lines.push('');
|
|
390
|
+
lines.push('Error');
|
|
391
|
+
lines.push(selectedTask.error_message ? `${selectedTask.error_code || 'E_TASK'} ${selectedTask.error_message}` : '(none)');
|
|
392
|
+
lines.push('');
|
|
393
|
+
lines.push('Tasks In Session');
|
|
394
|
+
lines.push(...tasks.map((task, idx) => `${idx === selectedTaskIndex ? '>' : ' '} ${task.task_id} [${task.status}] ${truncateLine(task.title || task.result_summary || '', 70)}`));
|
|
395
|
+
}
|
|
396
|
+
} else if (view === 'tasks') {
|
|
397
|
+
lines.push('Task Detail');
|
|
398
|
+
if (!selected) {
|
|
399
|
+
lines.push('No session selected.');
|
|
400
|
+
} else if (!selectedTask) {
|
|
401
|
+
lines.push('No tasks in the selected session.');
|
|
402
|
+
} else {
|
|
403
|
+
lines.push(`session=${selected.session_key}`);
|
|
404
|
+
lines.push(`conversation=${selected.conversation_id || '(none)'}`);
|
|
405
|
+
lines.push(`remote=${selected.remote_did || '(unknown)'}`);
|
|
406
|
+
lines.push('');
|
|
407
|
+
lines.push(`task=${selectedTask.task_id}`);
|
|
408
|
+
lines.push(`title=${selectedTask.title || '(none)'}`);
|
|
409
|
+
lines.push(`status=${selectedTask.status}`);
|
|
410
|
+
lines.push(`updated_at=${formatTs(selectedTask.updated_at, false)}`);
|
|
411
|
+
if (selectedTask.started_at) lines.push(`started_at=${formatTs(selectedTask.started_at, false)}`);
|
|
412
|
+
if (selectedTask.result_summary) lines.push(`result=${selectedTask.result_summary}`);
|
|
413
|
+
if (selectedTask.error_code || selectedTask.error_message) {
|
|
414
|
+
lines.push(`error=${selectedTask.error_code || 'E_TASK'} ${selectedTask.error_message || ''}`.trim());
|
|
415
|
+
}
|
|
416
|
+
lines.push(`actions=[Enter/l] full-detail [x] cancel-task [h/Esc] back [j/k] select-task`);
|
|
417
|
+
lines.push('');
|
|
418
|
+
lines.push('Tasks In Session');
|
|
419
|
+
lines.push(...tasks.map((task, idx) => `${idx === selectedTaskIndex ? '>' : ' '} ${task.task_id} [${task.status}] ${truncateLine(task.title || task.result_summary || '', 70)}`));
|
|
420
|
+
}
|
|
421
|
+
} else {
|
|
422
|
+
lines.push('Sessions');
|
|
423
|
+
lines.push(...(sessions.length
|
|
424
|
+
? sessions.slice(0, 20).map((session) => formatSessionRow(session, session.session_key === (selected?.session_key || '')))
|
|
425
|
+
: [' (no sessions)']));
|
|
426
|
+
lines.push('');
|
|
427
|
+
|
|
428
|
+
if (selected) {
|
|
429
|
+
lines.push(view === 'detail' ? 'Selected Session Detail' : 'Selected Session');
|
|
430
|
+
lines.push(`session=${selected.session_key}`);
|
|
431
|
+
lines.push(`conversation=${selected.conversation_id || '(none)'}`);
|
|
432
|
+
lines.push(`remote=${selected.remote_did || '(unknown)'}`);
|
|
433
|
+
lines.push(`trust=${selected.trust_state} unread=${selected.unread_count}`);
|
|
434
|
+
lines.push(`last_preview=${selected.last_message_preview || '(none)'}`);
|
|
435
|
+
lines.push(`binding=${selected.binding?.session_key || '(unbound)'}`);
|
|
436
|
+
lines.push(`current_chat=${hostState.activeChatSession || '(none)'}`);
|
|
437
|
+
if (selected.binding_alert) {
|
|
438
|
+
lines.push(`needs_rebind=true`);
|
|
439
|
+
lines.push(`warning=${selected.binding_alert.message}`);
|
|
440
|
+
} else {
|
|
441
|
+
lines.push('needs_rebind=false');
|
|
442
|
+
}
|
|
443
|
+
const actionBar = [
|
|
444
|
+
selected.trust_state === 'pending' ? '[a] approve' : null,
|
|
445
|
+
'[m] mark-read',
|
|
446
|
+
'[p] reply',
|
|
447
|
+
'[o] history',
|
|
448
|
+
'[t] tasks',
|
|
449
|
+
'[b] bind-current',
|
|
450
|
+
'[c] clear-binding',
|
|
451
|
+
].filter(Boolean).join(' ');
|
|
452
|
+
lines.push(`actions=${actionBar}`);
|
|
453
|
+
lines.push('');
|
|
454
|
+
lines.push('Tasks');
|
|
455
|
+
lines.push(...(tasks.length
|
|
456
|
+
? tasks.map((task) => `- ${task.title || task.task_id} [${task.status}] ${truncateLine(task.result_summary || task.error_message || '', 80)}`)
|
|
457
|
+
: ['- none']));
|
|
458
|
+
if (view === 'detail') {
|
|
459
|
+
lines.push('');
|
|
460
|
+
lines.push('Recent Messages');
|
|
461
|
+
lines.push(...(messages.length ? messages.map((message) => formatMessageRow(message)) : ['- none']));
|
|
462
|
+
}
|
|
463
|
+
lines.push('');
|
|
464
|
+
lines.push('Audit');
|
|
465
|
+
lines.push(...(auditEvents.length
|
|
466
|
+
? auditEvents.map((event) => `- ${event.event_type} ${event.action || event.outcome || ''} ${truncateLine(event.explanation || '', 80)}`)
|
|
467
|
+
: ['- none']));
|
|
468
|
+
} else {
|
|
469
|
+
lines.push('No session selected.');
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
lines.push('');
|
|
474
|
+
lines.push('Keys: ↑/↓ or j/k select Enter/l open Esc/h back g/G jump r refresh a approve m read p reply o history s search t tasks x cancel-task y dump b bind c clear ? help q quit');
|
|
475
|
+
return lines.join('\n');
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async function runHostTui(identityPath, opts) {
|
|
479
|
+
const once = !!opts.once;
|
|
480
|
+
const refreshMs = Math.max(500, Number.parseInt(String(opts.refreshMs || '2000'), 10) || 2000);
|
|
481
|
+
|
|
482
|
+
const uiState = {
|
|
483
|
+
selectedSessionKey: null,
|
|
484
|
+
view: 'overview',
|
|
485
|
+
selectedTaskIndex: 0,
|
|
486
|
+
statusMessage: '(ready)',
|
|
487
|
+
statusLevel: 'info',
|
|
488
|
+
statusExpiresAt: 0,
|
|
489
|
+
statusAt: 0,
|
|
490
|
+
selectedHistoryPageIndex: 0,
|
|
491
|
+
historySearchQuery: '',
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
const render = () => {
|
|
495
|
+
if (uiState.statusExpiresAt && Date.now() >= uiState.statusExpiresAt) {
|
|
496
|
+
uiState.statusMessage = '(ready)';
|
|
497
|
+
uiState.statusLevel = 'info';
|
|
498
|
+
uiState.statusExpiresAt = 0;
|
|
499
|
+
uiState.statusAt = 0;
|
|
500
|
+
}
|
|
501
|
+
const hostState = buildHostState(
|
|
502
|
+
identityPath,
|
|
503
|
+
uiState.selectedSessionKey,
|
|
504
|
+
uiState.selectedHistoryPageIndex,
|
|
505
|
+
uiState.historySearchQuery,
|
|
506
|
+
);
|
|
507
|
+
uiState.selectedSessionKey = hostState.selectedSession?.session_key || hostState.sessions[0]?.session_key || null;
|
|
508
|
+
uiState.selectedHistoryPageIndex = hostState.selectedHistoryPage?.pageIndex || 0;
|
|
509
|
+
const screen = renderHostTuiScreen(hostState, uiState);
|
|
510
|
+
return { hostState, screen };
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
if (once) {
|
|
514
|
+
const { screen } = render();
|
|
515
|
+
console.log(screen);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
520
|
+
console.error('Host TUI requires a TTY. Use --once for a snapshot in non-interactive environments.');
|
|
521
|
+
process.exit(1);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
let interval = null;
|
|
525
|
+
const redraw = () => {
|
|
526
|
+
const rendered = render();
|
|
527
|
+
clearScreen();
|
|
528
|
+
process.stdout.write(`${rendered.screen}\n`);
|
|
529
|
+
return rendered.hostState;
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
let latestState = redraw();
|
|
533
|
+
readline.emitKeypressEvents(process.stdin);
|
|
534
|
+
if (process.stdin.setRawMode) process.stdin.setRawMode(true);
|
|
535
|
+
|
|
536
|
+
const cleanup = () => {
|
|
537
|
+
if (interval) clearInterval(interval);
|
|
538
|
+
if (process.stdin.setRawMode) process.stdin.setRawMode(false);
|
|
539
|
+
process.stdin.pause();
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const setStatus = (message, level = 'info', ttlMs = 5000) => {
|
|
543
|
+
uiState.statusMessage = truncateLine(message || '(ready)', 160);
|
|
544
|
+
uiState.statusLevel = level;
|
|
545
|
+
uiState.statusExpiresAt = ttlMs > 0 ? Date.now() + ttlMs : 0;
|
|
546
|
+
uiState.statusAt = Date.now();
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
const promptLine = async (question) => {
|
|
550
|
+
stopInterval();
|
|
551
|
+
if (process.stdin.setRawMode) process.stdin.setRawMode(false);
|
|
552
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
553
|
+
try {
|
|
554
|
+
const answer = await new Promise((resolve) => {
|
|
555
|
+
rl.question(question, resolve);
|
|
556
|
+
});
|
|
557
|
+
return String(answer ?? '');
|
|
558
|
+
} finally {
|
|
559
|
+
rl.close();
|
|
560
|
+
if (process.stdin.setRawMode) process.stdin.setRawMode(true);
|
|
561
|
+
startInterval();
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
const promptMultiline = async (question) => {
|
|
566
|
+
stopInterval();
|
|
567
|
+
if (process.stdin.setRawMode) process.stdin.setRawMode(false);
|
|
568
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
569
|
+
const lines = [];
|
|
570
|
+
try {
|
|
571
|
+
process.stdout.write(`${question}\nFinish with a single "." on its own line.\n`);
|
|
572
|
+
while (true) {
|
|
573
|
+
// eslint-disable-next-line no-await-in-loop
|
|
574
|
+
const line = await new Promise((resolve) => rl.question('> ', resolve));
|
|
575
|
+
const text = String(line ?? '');
|
|
576
|
+
if (text === '.') break;
|
|
577
|
+
lines.push(text);
|
|
578
|
+
}
|
|
579
|
+
return lines.join('\n').trim();
|
|
580
|
+
} finally {
|
|
581
|
+
rl.close();
|
|
582
|
+
if (process.stdin.setRawMode) process.stdin.setRawMode(true);
|
|
583
|
+
startInterval();
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
const dumpStdoutBlock = async (title, body) => {
|
|
588
|
+
stopInterval();
|
|
589
|
+
if (process.stdin.setRawMode) process.stdin.setRawMode(false);
|
|
590
|
+
try {
|
|
591
|
+
process.stdout.write(`\n===== ${title} =====\n${body}\n===== end =====\nPress Enter to return...`);
|
|
592
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
593
|
+
await new Promise((resolve) => rl.question('', resolve));
|
|
594
|
+
rl.close();
|
|
595
|
+
} finally {
|
|
596
|
+
if (process.stdin.setRawMode) process.stdin.setRawMode(true);
|
|
597
|
+
startInterval();
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
const confirmAction = async (question) => {
|
|
602
|
+
const answer = await promptLine(`${question} [y/N] `);
|
|
603
|
+
return answer.trim().toLowerCase() === 'y';
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
const moveSelection = (delta) => {
|
|
607
|
+
if (uiState.view === 'tasks' || uiState.view === 'task-detail') {
|
|
608
|
+
const tasks = latestState.selectedTasks || [];
|
|
609
|
+
if (!tasks.length) return;
|
|
610
|
+
uiState.selectedTaskIndex = Math.min(tasks.length - 1, Math.max(0, uiState.selectedTaskIndex + delta));
|
|
611
|
+
} else if (uiState.view === 'history' && !uiState.historySearchQuery) {
|
|
612
|
+
if (delta > 0 && latestState.selectedHistoryPage?.hasOlder) uiState.selectedHistoryPageIndex += 1;
|
|
613
|
+
if (delta < 0 && latestState.selectedHistoryPage?.hasNewer) uiState.selectedHistoryPageIndex = Math.max(0, uiState.selectedHistoryPageIndex - 1);
|
|
614
|
+
} else {
|
|
615
|
+
const sessions = latestState.sessions || [];
|
|
616
|
+
if (!sessions.length) return;
|
|
617
|
+
const idx = Math.max(0, sessions.findIndex((session) => session.session_key === uiState.selectedSessionKey));
|
|
618
|
+
const next = Math.min(sessions.length - 1, Math.max(0, idx + delta));
|
|
619
|
+
uiState.selectedSessionKey = sessions[next].session_key;
|
|
620
|
+
uiState.selectedTaskIndex = 0;
|
|
621
|
+
uiState.selectedHistoryPageIndex = 0;
|
|
622
|
+
uiState.historySearchQuery = '';
|
|
623
|
+
}
|
|
624
|
+
latestState = redraw();
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
const jumpSelection = (target) => {
|
|
628
|
+
if (uiState.view === 'tasks' || uiState.view === 'task-detail') {
|
|
629
|
+
const tasks = latestState.selectedTasks || [];
|
|
630
|
+
if (!tasks.length) return;
|
|
631
|
+
uiState.selectedTaskIndex = target;
|
|
632
|
+
} else if (uiState.view === 'history') {
|
|
633
|
+
uiState.selectedHistoryPageIndex = 0;
|
|
634
|
+
} else {
|
|
635
|
+
const sessions = latestState.sessions || [];
|
|
636
|
+
if (!sessions.length) return;
|
|
637
|
+
uiState.selectedSessionKey = sessions[target].session_key;
|
|
638
|
+
uiState.selectedTaskIndex = 0;
|
|
639
|
+
uiState.selectedHistoryPageIndex = 0;
|
|
640
|
+
uiState.historySearchQuery = '';
|
|
641
|
+
}
|
|
642
|
+
latestState = redraw();
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
const stopInterval = () => {
|
|
646
|
+
if (interval) clearInterval(interval);
|
|
647
|
+
interval = null;
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
const startInterval = () => {
|
|
651
|
+
stopInterval();
|
|
652
|
+
interval = setInterval(() => {
|
|
653
|
+
latestState = redraw();
|
|
654
|
+
}, refreshMs);
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
startInterval();
|
|
658
|
+
|
|
659
|
+
process.stdin.on('keypress', async (_str, key) => {
|
|
660
|
+
if (key?.name === 'q' || (key?.ctrl && key?.name === 'c')) {
|
|
661
|
+
cleanup();
|
|
662
|
+
process.stdout.write('\n');
|
|
663
|
+
process.exit(0);
|
|
664
|
+
}
|
|
665
|
+
if (key?.name === 'up' || key?.name === 'k') return moveSelection(-1);
|
|
666
|
+
if (key?.name === 'down' || key?.name === 'j') return moveSelection(1);
|
|
667
|
+
if (key?.name === 'g') return jumpSelection(0);
|
|
668
|
+
if (_str === 'G') {
|
|
669
|
+
const max = uiState.view === 'tasks' || uiState.view === 'task-detail'
|
|
670
|
+
? Math.max(0, (latestState.selectedTasks || []).length - 1)
|
|
671
|
+
: Math.max(0, (latestState.sessions || []).length - 1);
|
|
672
|
+
return jumpSelection(max);
|
|
673
|
+
}
|
|
674
|
+
if (key?.name === 'return' || key?.name === 'enter' || key?.name === 'l') {
|
|
675
|
+
if (uiState.view === 'tasks') {
|
|
676
|
+
uiState.view = 'task-detail';
|
|
677
|
+
} else {
|
|
678
|
+
uiState.view = 'detail';
|
|
679
|
+
}
|
|
680
|
+
latestState = redraw();
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
if (key?.name === 'escape' || key?.name === 'h') {
|
|
684
|
+
if (uiState.view === 'task-detail') uiState.view = 'tasks';
|
|
685
|
+
else if (uiState.view === 'history') uiState.view = 'detail';
|
|
686
|
+
else uiState.view = 'overview';
|
|
687
|
+
latestState = redraw();
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
if (_str === '?') {
|
|
691
|
+
uiState.view = uiState.view === 'help' ? 'overview' : 'help';
|
|
692
|
+
latestState = redraw();
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
if (_str === 'n' && uiState.view === 'history') {
|
|
696
|
+
if (uiState.historySearchQuery) {
|
|
697
|
+
uiState.historySearchQuery = '';
|
|
698
|
+
setStatus('Cleared history search.', 'info');
|
|
699
|
+
latestState = redraw();
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
if (latestState.selectedHistoryPage?.hasOlder) {
|
|
703
|
+
uiState.selectedHistoryPageIndex += 1;
|
|
704
|
+
setStatus(`History page ${uiState.selectedHistoryPageIndex + 1}`, 'info');
|
|
705
|
+
latestState = redraw();
|
|
706
|
+
}
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
if (_str === 'p' && uiState.view === 'history') {
|
|
710
|
+
if (uiState.historySearchQuery) return;
|
|
711
|
+
if (latestState.selectedHistoryPage?.hasNewer) {
|
|
712
|
+
uiState.selectedHistoryPageIndex = Math.max(0, uiState.selectedHistoryPageIndex - 1);
|
|
713
|
+
setStatus(`History page ${uiState.selectedHistoryPageIndex + 1}`, 'info');
|
|
714
|
+
latestState = redraw();
|
|
715
|
+
}
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
if (key?.name === 't') {
|
|
719
|
+
uiState.view = 'tasks';
|
|
720
|
+
uiState.selectedTaskIndex = 0;
|
|
721
|
+
uiState.selectedHistoryPageIndex = 0;
|
|
722
|
+
setStatus('Opened task list view.', 'info');
|
|
723
|
+
latestState = redraw();
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
if (key?.name === 'r') {
|
|
727
|
+
setStatus('Refreshed.', 'info');
|
|
728
|
+
latestState = redraw();
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
if (key?.name === 'm') {
|
|
732
|
+
const selected = latestState.selectedSession;
|
|
733
|
+
if (!selected) return;
|
|
734
|
+
const store = openStore(identityPath);
|
|
735
|
+
try {
|
|
736
|
+
const sessions = new SessionManager(store);
|
|
737
|
+
sessions.markRead(selected.session_key);
|
|
738
|
+
setStatus(`Marked session as read: ${selected.session_key}`, 'ok');
|
|
739
|
+
} finally {
|
|
740
|
+
store.close();
|
|
741
|
+
}
|
|
742
|
+
latestState = redraw();
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
if (key?.name === 'a') {
|
|
746
|
+
const selected = latestState.selectedSession;
|
|
747
|
+
if (!selected?.conversation_id || selected.trust_state !== 'pending') return;
|
|
748
|
+
const confirmed = await confirmAction(`Approve pending conversation ${selected.conversation_id}\nRemote DID: ${selected.remote_did || '(unknown)'}\nProceed?`);
|
|
749
|
+
if (confirmed) {
|
|
750
|
+
const { client, store } = await getClientWithStore(identityPath);
|
|
751
|
+
try {
|
|
752
|
+
const res = await client.approveContact(selected.conversation_id);
|
|
753
|
+
setStatus(res.ok
|
|
754
|
+
? `Approved conversation ${selected.conversation_id}${res.data?.dm_conversation_id ? ` -> ${res.data.dm_conversation_id}` : ''}`
|
|
755
|
+
: `Approve failed: ${res.error?.message || 'unknown error'}`, res.ok ? 'ok' : 'err');
|
|
756
|
+
} finally {
|
|
757
|
+
store.close();
|
|
758
|
+
}
|
|
759
|
+
} else {
|
|
760
|
+
setStatus('Approve cancelled.', 'warn');
|
|
761
|
+
}
|
|
762
|
+
latestState = redraw();
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
if (key?.name === 'p' && uiState.view === 'detail') {
|
|
766
|
+
const selected = latestState.selectedSession;
|
|
767
|
+
if (!selected?.conversation_id) return;
|
|
768
|
+
const input = await promptMultiline(`Reply to ${selected.remote_did || selected.conversation_id}`);
|
|
769
|
+
const message = input.trim();
|
|
770
|
+
if (!message) {
|
|
771
|
+
setStatus('Reply cancelled.', 'warn');
|
|
772
|
+
latestState = redraw();
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
const { client, store } = await getClientWithStore(identityPath);
|
|
776
|
+
try {
|
|
777
|
+
const res = await client.sendMessage(selected.conversation_id, SCHEMA_TEXT, { text: message });
|
|
778
|
+
setStatus(res.ok
|
|
779
|
+
? `Sent reply to ${selected.remote_did || selected.conversation_id} (${res.data?.message_id || 'queued'})`
|
|
780
|
+
: `Reply failed: ${res.error?.message || 'unknown error'}`, res.ok ? 'ok' : 'err');
|
|
781
|
+
} finally {
|
|
782
|
+
store.close();
|
|
783
|
+
}
|
|
784
|
+
latestState = redraw();
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
if (key?.name === 'o' && uiState.view === 'detail') {
|
|
788
|
+
uiState.view = 'history';
|
|
789
|
+
uiState.selectedHistoryPageIndex = 0;
|
|
790
|
+
uiState.historySearchQuery = '';
|
|
791
|
+
setStatus('Opened local history view.', 'info');
|
|
792
|
+
latestState = redraw();
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
if ((_str === '/' || key?.name === 's') && uiState.view === 'history') {
|
|
796
|
+
const answer = await promptLine(`Search history (${latestState.selectedSession?.conversation_id || 'current conversation'}). Empty clears search: `);
|
|
797
|
+
const query = answer.trim();
|
|
798
|
+
uiState.historySearchQuery = query;
|
|
799
|
+
uiState.selectedHistoryPageIndex = 0;
|
|
800
|
+
setStatus(query ? `History search: ${query}` : 'Cleared history search.', 'info');
|
|
801
|
+
latestState = redraw();
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
if (key?.name === 'b') {
|
|
805
|
+
const selected = (latestState.sessions || []).find((session) => session.session_key === uiState.selectedSessionKey);
|
|
806
|
+
if (!selected?.conversation_id) return;
|
|
807
|
+
const current = latestState.activeChatSession || '(none)';
|
|
808
|
+
const previous = selected.binding?.session_key || '(unbound)';
|
|
809
|
+
const confirmed = await confirmAction(`Rebind conversation ${selected.conversation_id}\nRemote DID: ${selected.remote_did || '(unknown)'}\nCurrent chat: ${current}\nPrevious binding: ${previous}\nProceed?`);
|
|
810
|
+
if (confirmed) {
|
|
811
|
+
if (!latestState.activeChatSession) {
|
|
812
|
+
setStatus('Rebind failed: no active chat session.', 'err');
|
|
813
|
+
latestState = redraw();
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
setSessionBinding(selected.conversation_id, latestState.activeChatSession);
|
|
817
|
+
setStatus(`Rebound ${selected.conversation_id} -> ${latestState.activeChatSession}`, 'ok');
|
|
818
|
+
} else {
|
|
819
|
+
setStatus('Rebind cancelled.', 'warn');
|
|
820
|
+
}
|
|
821
|
+
latestState = redraw();
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
if (key?.name === 'c') {
|
|
825
|
+
const selected = (latestState.sessions || []).find((session) => session.session_key === uiState.selectedSessionKey);
|
|
826
|
+
if (!selected?.conversation_id) return;
|
|
827
|
+
removeSessionBinding(selected.conversation_id);
|
|
828
|
+
setStatus(`Cleared binding for ${selected.conversation_id}`, 'ok');
|
|
829
|
+
latestState = redraw();
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
if (key?.name === 'x' && (uiState.view === 'tasks' || uiState.view === 'task-detail')) {
|
|
833
|
+
const selected = latestState.selectedSession;
|
|
834
|
+
const tasks = latestState.selectedTasks || [];
|
|
835
|
+
const task = tasks[Math.max(0, Math.min(uiState.selectedTaskIndex, Math.max(0, tasks.length - 1)))] || null;
|
|
836
|
+
if (!selected?.conversation_id || !task) return;
|
|
837
|
+
const confirmed = await confirmAction(`Cancel task ${task.task_id}\nTitle: ${task.title || '(none)'}\nStatus: ${task.status}\nProceed?`);
|
|
838
|
+
if (confirmed) {
|
|
839
|
+
const { client, store } = await getClientWithStore(identityPath);
|
|
840
|
+
try {
|
|
841
|
+
const res = await client.cancelTask(selected.conversation_id, task.task_id);
|
|
842
|
+
setStatus(res.ok
|
|
843
|
+
? `Cancel requested for task ${task.task_id}`
|
|
844
|
+
: `Cancel failed: ${res.error?.message || 'unknown error'}`, res.ok ? 'ok' : 'err');
|
|
845
|
+
} finally {
|
|
846
|
+
store.close();
|
|
847
|
+
}
|
|
848
|
+
} else {
|
|
849
|
+
setStatus('Cancel task aborted.', 'warn');
|
|
850
|
+
}
|
|
851
|
+
latestState = redraw();
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
if (key?.name === 'y' && uiState.view === 'task-detail') {
|
|
855
|
+
const tasks = latestState.selectedTasks || [];
|
|
856
|
+
const task = tasks[Math.max(0, Math.min(uiState.selectedTaskIndex, Math.max(0, tasks.length - 1)))] || null;
|
|
857
|
+
if (!task) return;
|
|
858
|
+
const formatAnswer = await promptLine('Export format [j]son / [p]lain (default plain): ');
|
|
859
|
+
const format = formatAnswer.trim().toLowerCase().startsWith('j') ? 'json' : 'plain';
|
|
860
|
+
await dumpStdoutBlock(`task ${task.task_id} (${format})`, buildTaskExportBody(task, format));
|
|
861
|
+
setStatus(`Dumped task ${task.task_id} to stdout as ${format}.`, 'ok');
|
|
862
|
+
latestState = redraw();
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
|
|
80
868
|
/** Format timestamp for display: human-readable by default, raw ms when --raw. */
|
|
81
869
|
function formatTs(tsMs, raw) {
|
|
82
870
|
if (raw) return String(tsMs);
|
|
@@ -94,6 +882,19 @@ function makeClient(id, identityPath) {
|
|
|
94
882
|
});
|
|
95
883
|
}
|
|
96
884
|
|
|
885
|
+
function makeClientWithStore(id, identityPath) {
|
|
886
|
+
const p = identityPath ?? getEffectiveIdentityPath();
|
|
887
|
+
const store = openStore(p);
|
|
888
|
+
const client = new PingAgentClient({
|
|
889
|
+
serverUrl: id.serverUrl ?? DEFAULT_SERVER,
|
|
890
|
+
identity: id,
|
|
891
|
+
accessToken: id.accessToken ?? '',
|
|
892
|
+
onTokenRefreshed: (token, expiresAt) => updateStoredToken(token, expiresAt, p),
|
|
893
|
+
store,
|
|
894
|
+
});
|
|
895
|
+
return { client, store };
|
|
896
|
+
}
|
|
897
|
+
|
|
97
898
|
/** Load identity, proactively refresh token if near/past expiry, then return client. */
|
|
98
899
|
async function getClient(identityPath) {
|
|
99
900
|
const p = identityPath ?? getEffectiveIdentityPath();
|
|
@@ -105,6 +906,16 @@ async function getClient(identityPath) {
|
|
|
105
906
|
return client;
|
|
106
907
|
}
|
|
107
908
|
|
|
909
|
+
async function getClientWithStore(identityPath) {
|
|
910
|
+
const p = identityPath ?? getEffectiveIdentityPath();
|
|
911
|
+
let id = loadIdentity(p);
|
|
912
|
+
await ensureTokenValid(p, id.serverUrl);
|
|
913
|
+
id = loadIdentity(p);
|
|
914
|
+
const { client, store } = makeClientWithStore(id, p);
|
|
915
|
+
await client.ensureEncryptionKeyPublished().catch(() => undefined);
|
|
916
|
+
return { client, store };
|
|
917
|
+
}
|
|
918
|
+
|
|
108
919
|
const program = new Command();
|
|
109
920
|
program
|
|
110
921
|
.name('pingagent')
|
|
@@ -1438,6 +2249,25 @@ billing
|
|
|
1438
2249
|
}
|
|
1439
2250
|
});
|
|
1440
2251
|
|
|
2252
|
+
const host = program
|
|
2253
|
+
.command('host')
|
|
2254
|
+
.description('Headless runtime inspection and control for PingAgent host state');
|
|
2255
|
+
|
|
2256
|
+
host
|
|
2257
|
+
.command('tui')
|
|
2258
|
+
.description('Start a terminal UI for runtime, sessions, bindings, and rebind actions')
|
|
2259
|
+
.option('--once', 'Print one snapshot and exit')
|
|
2260
|
+
.option('--refresh-ms <ms>', 'Refresh interval in interactive mode', '2000')
|
|
2261
|
+
.option('--profile <name>', 'Use profile from ~/.pingagent/<name>')
|
|
2262
|
+
.action(async (opts) => {
|
|
2263
|
+
const identityPath = getIdentityPathForCommand(opts);
|
|
2264
|
+
if (!identityExists(identityPath)) {
|
|
2265
|
+
console.error('No identity found. Run: pingagent init');
|
|
2266
|
+
process.exit(1);
|
|
2267
|
+
}
|
|
2268
|
+
await runHostTui(identityPath, opts);
|
|
2269
|
+
});
|
|
2270
|
+
|
|
1441
2271
|
program
|
|
1442
2272
|
.command('web')
|
|
1443
2273
|
.description('Start local web UI and host panel for debugging, runtime inspection, trust policy, and audit. By default scans ~/.pingagent for profiles; use --identity-dir to lock to one profile.')
|