@pingagent/sdk 0.1.8 → 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 +854 -3
- package/dist/chunk-2Y6YRKTO.js +3100 -0
- package/dist/chunk-3OEFISNL.js +2433 -0
- package/dist/chunk-5Z6HZWDA.js +2603 -0
- package/dist/chunk-BSDY6AKB.js +2918 -0
- package/dist/chunk-PFABO4C7.js +2961 -0
- package/dist/chunk-QK2GMSWC.js +2959 -0
- package/dist/chunk-RMIRCSQ6.js +3042 -0
- package/dist/chunk-TCYDOFRQ.js +2085 -0
- package/dist/chunk-V7HHUQT6.js +1962 -0
- package/dist/index.d.ts +439 -5
- package/dist/index.js +65 -3
- package/dist/web-server.js +1323 -16
- package/package.json +11 -3
- package/__tests__/cli.test.ts +0 -225
- package/__tests__/identity.test.ts +0 -47
- package/__tests__/store.test.ts +0 -332
- package/src/a2a-adapter.ts +0 -159
- package/src/auth.ts +0 -50
- package/src/client.ts +0 -582
- package/src/contacts.ts +0 -210
- package/src/history.ts +0 -269
- package/src/identity.ts +0 -86
- package/src/index.ts +0 -25
- package/src/paths.ts +0 -52
- package/src/store.ts +0 -62
- package/src/transport.ts +0 -141
- package/src/web-server.ts +0 -1148
- package/src/ws-subscription.ts +0 -428
- package/tsconfig.json +0 -8
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
|
|
|
@@ -57,10 +71,800 @@ function printError(err) {
|
|
|
57
71
|
}
|
|
58
72
|
}
|
|
59
73
|
|
|
74
|
+
function formatRetentionLabel(ttlMs) {
|
|
75
|
+
if (!ttlMs || ttlMs <= 0) return '-';
|
|
76
|
+
const days = ttlMs / 86_400_000;
|
|
77
|
+
if (Number.isInteger(days) && days >= 1) return `${days}d`;
|
|
78
|
+
const hours = ttlMs / 3_600_000;
|
|
79
|
+
if (Number.isInteger(hours) && hours >= 1) return `${hours}h`;
|
|
80
|
+
return `${ttlMs}ms`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function describeHostedTier(tier) {
|
|
84
|
+
if (tier === 'plus') return 'shareable identity + first alias + higher relay';
|
|
85
|
+
if (tier === 'pro') return 'multi-identity communication + audit export';
|
|
86
|
+
if (tier === 'enterprise') return 'high-scale governance + operational controls';
|
|
87
|
+
return 'free communication-first entry tier';
|
|
88
|
+
}
|
|
89
|
+
|
|
60
90
|
function openStore(identityPath) {
|
|
61
91
|
return new LocalStore(getStorePath(identityPath));
|
|
62
92
|
}
|
|
63
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
|
+
|
|
64
868
|
/** Format timestamp for display: human-readable by default, raw ms when --raw. */
|
|
65
869
|
function formatTs(tsMs, raw) {
|
|
66
870
|
if (raw) return String(tsMs);
|
|
@@ -78,13 +882,38 @@ function makeClient(id, identityPath) {
|
|
|
78
882
|
});
|
|
79
883
|
}
|
|
80
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
|
+
|
|
81
898
|
/** Load identity, proactively refresh token if near/past expiry, then return client. */
|
|
82
899
|
async function getClient(identityPath) {
|
|
83
900
|
const p = identityPath ?? getEffectiveIdentityPath();
|
|
84
901
|
let id = loadIdentity(p);
|
|
85
902
|
await ensureTokenValid(p, id.serverUrl);
|
|
86
903
|
id = loadIdentity(p);
|
|
87
|
-
|
|
904
|
+
const client = makeClient(id, p);
|
|
905
|
+
await client.ensureEncryptionKeyPublished().catch(() => undefined);
|
|
906
|
+
return client;
|
|
907
|
+
}
|
|
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 };
|
|
88
917
|
}
|
|
89
918
|
|
|
90
919
|
const program = new Command();
|
|
@@ -402,12 +1231,15 @@ program
|
|
|
402
1231
|
if (subRes.ok && subRes.data) {
|
|
403
1232
|
const d = subRes.data;
|
|
404
1233
|
console.log(`Tier: ${d.tier}`);
|
|
1234
|
+
console.log(`Plan: ${describeHostedTier(d.tier)}`);
|
|
405
1235
|
console.log(`Relay: ${d.usage.relay_today} / ${d.usage.relay_limit} today`);
|
|
1236
|
+
console.log(`Retention: ${formatRetentionLabel(d.limits.store_forward_ttl_ms)}`);
|
|
406
1237
|
console.log(`Artifact: ${(d.usage.artifact_bytes / 1024 / 1024).toFixed(2)} MB / ${d.limits.artifact_storage_mb} MB`);
|
|
407
1238
|
console.log(`Alias: ${d.usage.alias_count} / ${d.usage.alias_limit}`);
|
|
1239
|
+
console.log(`Audit export: ${d.limits.audit_export_allowed ? 'enabled' : 'not included on this plan'}`);
|
|
408
1240
|
if (d.billing_primary_did && d.billing_primary_did !== id.did) {
|
|
409
1241
|
console.log(`Billing: linked device (primary: ${d.billing_primary_did})`);
|
|
410
|
-
console.log(' Subscription managed on primary. Use primary
|
|
1242
|
+
console.log(' Subscription managed on primary. Use the primary device for checkout changes and customer portal access.');
|
|
411
1243
|
} else if (d.linked_device_count > 0) {
|
|
412
1244
|
console.log(`Billing: primary device (${d.linked_device_count} linked)`);
|
|
413
1245
|
}
|
|
@@ -1417,9 +2249,28 @@ billing
|
|
|
1417
2249
|
}
|
|
1418
2250
|
});
|
|
1419
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
|
+
|
|
1420
2271
|
program
|
|
1421
2272
|
.command('web')
|
|
1422
|
-
.description('Start local web UI for debugging and audit. By default scans ~/.pingagent for profiles; use --identity-dir to lock to one profile.')
|
|
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.')
|
|
1423
2274
|
.option('--port <port>', 'Port for the web server', '3846')
|
|
1424
2275
|
.action(async (opts) => {
|
|
1425
2276
|
const serverUrl = process.env.PINGAGENT_SERVER_URL || DEFAULT_SERVER;
|