rterm-cli 3.2.10 → 3.2.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/package.json +2 -2
- package/rterm-cli.mjs +200 -93
package/README.md
CHANGED
|
@@ -34,6 +34,7 @@ rterm version # backend version + method count
|
|
|
34
34
|
rterm methods [--category terminal] # self-describing RPC surface
|
|
35
35
|
rterm call <method> [json-params] # raw JSON-RPC call
|
|
36
36
|
rterm terminals # list terminal tabs
|
|
37
|
+
rterm connections # list saved SSH/WinRM/Serial connections
|
|
37
38
|
rterm open <saved-connection-name> # open a tab for a saved connection
|
|
38
39
|
rterm close <tabIdOrName> # close a terminal tab
|
|
39
40
|
rterm run <tabIdOrName> <command> # run a command in a tab (waits)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rterm-cli",
|
|
3
|
-
"version": "3.2.
|
|
4
|
-
"description": "rterm — zero-dependency command CLI for the RTerm / neuralOS backend WebSocket gateway (ping, terminals, run, fleet, chat, dashboard, metrics, raw call)",
|
|
3
|
+
"version": "3.2.12",
|
|
4
|
+
"description": "rterm — zero-dependency command CLI for the RTerm / neuralOS backend WebSocket gateway (ping, terminals, connections, open, run, fleet, chat, dashboard, metrics, raw call)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"rterm": "./rterm-cli.mjs",
|
package/rterm-cli.mjs
CHANGED
|
@@ -2,21 +2,14 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* rterm-cli — a `gyll`-style command CLI for the RTerm / neuralOS backend.
|
|
4
4
|
*
|
|
5
|
-
* Speaks the backend's WebSocket JSON-RPC gateway natively (ws://host:17888)
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* rterm call <method> [json-params]
|
|
9
|
-
* rterm terminals
|
|
10
|
-
* rterm open <saved-connection-name>
|
|
11
|
-
* rterm run <tabIdOrName> <command>
|
|
12
|
-
* rterm chat <sessionId> <message> (blocking; prints the final answer)
|
|
13
|
-
* rterm sessions
|
|
14
|
-
* rterm dashboard
|
|
15
|
-
* rterm metrics [--format prometheus|summary]
|
|
16
|
-
* rterm fleet <tab1,tab2> <command>
|
|
5
|
+
* Speaks the backend's WebSocket JSON-RPC gateway natively (ws://host:17888).
|
|
6
|
+
* Zero runtime dependencies: Node's built-in WebSocket (>= 21) with a `ws`
|
|
7
|
+
* package fallback.
|
|
17
8
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
9
|
+
* Commands:
|
|
10
|
+
* rterm ping | version | methods | call | terminals | connections
|
|
11
|
+
* rterm open <name> | close <tab> | run <tab> <cmd> | fleet <tabs> <cmd>
|
|
12
|
+
* rterm sessions | chat <session> <msg> | dashboard | metrics
|
|
20
13
|
*/
|
|
21
14
|
|
|
22
15
|
import { readFileSync, existsSync } from 'node:fs'
|
|
@@ -50,6 +43,25 @@ function parseArgs(argv) {
|
|
|
50
43
|
return { positional, flags }
|
|
51
44
|
}
|
|
52
45
|
|
|
46
|
+
// ── error formatting (fix: errors used to print "[object Object]") ──────────
|
|
47
|
+
|
|
48
|
+
/** Extract a readable message from any thrown/rejected value. */
|
|
49
|
+
function errorMessage(value) {
|
|
50
|
+
if (value instanceof Error) return value.message
|
|
51
|
+
if (typeof value === 'string') return value
|
|
52
|
+
if (value && typeof value === 'object') {
|
|
53
|
+
// Gateway errors arrive as { code, message } or Error with .cause.
|
|
54
|
+
if (typeof value.message === 'string' && value.message) {
|
|
55
|
+
return value.code ? `${value.code}: ${value.message}` : value.message
|
|
56
|
+
}
|
|
57
|
+
if (typeof value.error === 'object' && value.error?.message) {
|
|
58
|
+
return `${value.error.code || 'ERROR'}: ${value.error.message}`
|
|
59
|
+
}
|
|
60
|
+
try { return JSON.stringify(value) } catch { return String(value) }
|
|
61
|
+
}
|
|
62
|
+
return String(value)
|
|
63
|
+
}
|
|
64
|
+
|
|
53
65
|
// ── gateway client ──────────────────────────────────────────────────────────
|
|
54
66
|
|
|
55
67
|
let nextId = 1
|
|
@@ -73,8 +85,8 @@ function loadToken() {
|
|
|
73
85
|
return null
|
|
74
86
|
}
|
|
75
87
|
|
|
76
|
-
async function openSocket(url) {
|
|
77
|
-
|
|
88
|
+
async function openSocket(url, token) {
|
|
89
|
+
const headers = token ? { Authorization: `Bearer ${token}` } : undefined
|
|
78
90
|
if (typeof globalThis.WebSocket === 'function') {
|
|
79
91
|
return await new Promise((resolve, reject) => {
|
|
80
92
|
const ws = new globalThis.WebSocket(url)
|
|
@@ -82,13 +94,12 @@ async function openSocket(url) {
|
|
|
82
94
|
ws.onerror = () => reject(new Error(`Cannot connect to ${url}. Is the backend running? (gybackend)`))
|
|
83
95
|
})
|
|
84
96
|
}
|
|
85
|
-
// Fallback: the `ws` package (present in a backend checkout/install).
|
|
86
97
|
try {
|
|
87
98
|
const { createRequire } = await import('node:module')
|
|
88
99
|
const require = createRequire(import.meta.url)
|
|
89
100
|
const WS = require('ws')
|
|
90
101
|
return await new Promise((resolve, reject) => {
|
|
91
|
-
const ws = new WS(url)
|
|
102
|
+
const ws = new WS(url, { headers })
|
|
92
103
|
ws.on('open', () => resolve(ws))
|
|
93
104
|
ws.on('error', () => reject(new Error(`Cannot connect to ${url}. Is the backend running? (gybackend)`)))
|
|
94
105
|
})
|
|
@@ -97,8 +108,8 @@ async function openSocket(url) {
|
|
|
97
108
|
}
|
|
98
109
|
}
|
|
99
110
|
|
|
100
|
-
async function call(url, method, params) {
|
|
101
|
-
const ws = await openSocket(url)
|
|
111
|
+
async function call(url, method, params, token) {
|
|
112
|
+
const ws = await openSocket(url, token)
|
|
102
113
|
const id = String(nextId++)
|
|
103
114
|
return await new Promise((resolve, reject) => {
|
|
104
115
|
const timer = setTimeout(() => {
|
|
@@ -112,14 +123,15 @@ async function call(url, method, params) {
|
|
|
112
123
|
clearTimeout(timer)
|
|
113
124
|
try { ws.close() } catch { /* ignore */ }
|
|
114
125
|
if (frame.ok) resolve(frame.result)
|
|
115
|
-
else reject(
|
|
126
|
+
else reject(frame.error || new Error(`gateway error: ${method}`))
|
|
116
127
|
}
|
|
117
128
|
} catch { /* ignore non-JSON frames */ }
|
|
118
129
|
}
|
|
119
|
-
const onClose = () => {
|
|
130
|
+
const onClose = () => {
|
|
131
|
+
clearTimeout(timer)
|
|
132
|
+
reject(new Error('Connection closed before response. Is the backend still running?'))
|
|
133
|
+
}
|
|
120
134
|
|
|
121
|
-
// Normalize the native WebSocket (addEventListener) and the `ws` package
|
|
122
|
-
// (on/on('message')) behind one interface.
|
|
123
135
|
if (typeof ws.addEventListener === 'function') {
|
|
124
136
|
ws.addEventListener('message', (event) => onMessage(event.data))
|
|
125
137
|
ws.addEventListener('close', onClose)
|
|
@@ -138,6 +150,13 @@ async function call(url, method, params) {
|
|
|
138
150
|
})
|
|
139
151
|
}
|
|
140
152
|
|
|
153
|
+
/** Client bound to a URL + token, so commands don't repeat them. */
|
|
154
|
+
function makeClient(url, token) {
|
|
155
|
+
return {
|
|
156
|
+
call: (method, params) => call(url, method, params, token),
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
141
160
|
// ── output helpers ──────────────────────────────────────────────────────────
|
|
142
161
|
|
|
143
162
|
function printJson(value) {
|
|
@@ -145,7 +164,7 @@ function printJson(value) {
|
|
|
145
164
|
}
|
|
146
165
|
|
|
147
166
|
function fail(message) {
|
|
148
|
-
console.error(`Error: ${message}`)
|
|
167
|
+
console.error(`Error: ${errorMessage(message)}`)
|
|
149
168
|
process.exit(1)
|
|
150
169
|
}
|
|
151
170
|
|
|
@@ -153,9 +172,11 @@ const HELP = `rterm — command CLI for the RTerm / neuralOS backend gateway
|
|
|
153
172
|
|
|
154
173
|
Usage:
|
|
155
174
|
rterm ping Liveness check
|
|
175
|
+
rterm version Backend version + method count
|
|
156
176
|
rterm methods [--category c] [--prefix p] List gateway RPC methods
|
|
157
177
|
rterm call <method> [json] Raw JSON-RPC call (params as JSON)
|
|
158
178
|
rterm terminals List terminal tabs
|
|
179
|
+
rterm connections List saved SSH/WinRM/Serial connections
|
|
159
180
|
rterm open <connection-name> Open a terminal tab for a saved connection
|
|
160
181
|
rterm close <tabIdOrName> Close a terminal tab
|
|
161
182
|
rterm run <tabIdOrName> <command> Run a command in a terminal tab (waits)
|
|
@@ -164,7 +185,6 @@ Usage:
|
|
|
164
185
|
rterm chat <sessionId> <message> Send a message to the agent (blocking)
|
|
165
186
|
rterm dashboard Print the live dashboard state
|
|
166
187
|
rterm metrics [--format prometheus] Host metrics
|
|
167
|
-
rterm version Backend version + method count
|
|
168
188
|
|
|
169
189
|
Options:
|
|
170
190
|
--url ws://host:port Gateway URL (default ${DEFAULT_URL}, env RTERM_URL)
|
|
@@ -174,12 +194,121 @@ Environment:
|
|
|
174
194
|
RTERM_URL, RTERM_HOST, RTERM_PORT, RTERM_TOKEN
|
|
175
195
|
`
|
|
176
196
|
|
|
197
|
+
// ── command helpers ─────────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
const ANSI_RE = /\x1b\[[0-9;?]*[a-zA-Z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g
|
|
200
|
+
|
|
201
|
+
function stripAnsi(text) {
|
|
202
|
+
return text.replace(ANSI_RE, '')
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Resolve a saved connection by name or id across ssh/winrm/serial and build
|
|
207
|
+
* an inline terminal config the backend accepts (createTab does NOT resolve
|
|
208
|
+
* saved-connection names itself).
|
|
209
|
+
*/
|
|
210
|
+
function connectionToConfig(entry, kind) {
|
|
211
|
+
if (kind === 'ssh') {
|
|
212
|
+
return {
|
|
213
|
+
type: 'ssh',
|
|
214
|
+
host: entry.host,
|
|
215
|
+
port: entry.port ?? 22,
|
|
216
|
+
username: entry.username,
|
|
217
|
+
...(entry.authMethod === 'privateKey'
|
|
218
|
+
? { privateKey: entry.privateKey }
|
|
219
|
+
: { password: entry.password }),
|
|
220
|
+
...(entry.algorithmsPreset ? { algorithmsPreset: entry.algorithmsPreset } : {}),
|
|
221
|
+
...(entry.termType ? { termType: entry.termType } : {}),
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (kind === 'winrm') {
|
|
225
|
+
return {
|
|
226
|
+
type: 'winrm',
|
|
227
|
+
host: entry.host,
|
|
228
|
+
port: entry.port ?? 5985,
|
|
229
|
+
username: entry.username,
|
|
230
|
+
password: entry.password,
|
|
231
|
+
...(entry.transport ? { transport: entry.transport } : {}),
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
type: 'serial',
|
|
236
|
+
path: entry.path,
|
|
237
|
+
baudRate: entry.baudRate ?? 9600,
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function fetchConnections(client) {
|
|
242
|
+
const settings = await client.call('settings:get', {})
|
|
243
|
+
const conns = settings?.connections || {}
|
|
244
|
+
return {
|
|
245
|
+
ssh: Array.isArray(conns.ssh) ? conns.ssh : [],
|
|
246
|
+
winrm: Array.isArray(conns.winrm) ? conns.winrm : [],
|
|
247
|
+
serial: Array.isArray(conns.serial) ? conns.serial : [],
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function resolveConnection(client, nameOrId) {
|
|
252
|
+
const { ssh, winrm, serial } = await fetchConnections(client)
|
|
253
|
+
const pools = [['ssh', ssh], ['winrm', winrm], ['serial', serial]]
|
|
254
|
+
for (const [kind, list] of pools) {
|
|
255
|
+
const hit = list.find((c) => c.id === nameOrId || c.name === nameOrId)
|
|
256
|
+
if (hit) return { kind, entry: hit }
|
|
257
|
+
}
|
|
258
|
+
return null
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Resolve a tab id-or-name to a real tab id via terminal:list. */
|
|
262
|
+
async function resolveTabId(client, tabIdOrName) {
|
|
263
|
+
const result = await client.call('terminal:list', {})
|
|
264
|
+
const terminals = result?.terminals || []
|
|
265
|
+
const hit = terminals.find((t) => t.id === tabIdOrName || t.title === tabIdOrName)
|
|
266
|
+
return hit?.id || null
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Run a command in a tab via write + buffer-delta polling; returns output.
|
|
271
|
+
* Validates the tab exists first — the gateway silently returns empty output
|
|
272
|
+
* for unknown terminal ids, which would otherwise look like success.
|
|
273
|
+
*/
|
|
274
|
+
async function runInTab(client, tabIdOrName, commandText) {
|
|
275
|
+
const tabId = await resolveTabId(client, tabIdOrName)
|
|
276
|
+
if (!tabId) {
|
|
277
|
+
const result = await client.call('terminal:list', {})
|
|
278
|
+
const known = (result?.terminals || []).map((t) => `${t.id} (${t.title})`).join(', ')
|
|
279
|
+
throw new Error(`No terminal tab "${tabIdOrName}". Open tabs: ${known || '(none)'}`)
|
|
280
|
+
}
|
|
281
|
+
const before = await client.call('terminal:getBufferDelta', { terminalId: tabId, fromOffset: 0 })
|
|
282
|
+
const startOffset = Number(before?.offset ?? 0)
|
|
283
|
+
await client.call('terminal:write', { terminalId: tabId, data: `${commandText}\n` })
|
|
284
|
+
let output = ''
|
|
285
|
+
let lastOffset = startOffset
|
|
286
|
+
let stable = 0
|
|
287
|
+
const deadline = Date.now() + 30_000
|
|
288
|
+
while (Date.now() < deadline) {
|
|
289
|
+
await new Promise((resolve) => setTimeout(resolve, 400))
|
|
290
|
+
const delta = await client.call('terminal:getBufferDelta', { terminalId: tabId, fromOffset: lastOffset })
|
|
291
|
+
const data = typeof delta?.data === 'string' ? delta.data : ''
|
|
292
|
+
const offset = Number(delta?.offset ?? lastOffset)
|
|
293
|
+
if (data) output += data
|
|
294
|
+
if (offset === lastOffset && !data) {
|
|
295
|
+
stable += 1
|
|
296
|
+
if (stable >= 3) break
|
|
297
|
+
} else {
|
|
298
|
+
stable = 0
|
|
299
|
+
}
|
|
300
|
+
lastOffset = offset
|
|
301
|
+
}
|
|
302
|
+
return stripAnsi(output).trimEnd()
|
|
303
|
+
}
|
|
304
|
+
|
|
177
305
|
// ── commands ────────────────────────────────────────────────────────────────
|
|
178
306
|
|
|
179
307
|
async function main() {
|
|
180
308
|
const argv = process.argv.slice(2)
|
|
181
309
|
const { positional, flags } = parseArgs(argv)
|
|
182
310
|
const url = (typeof flags.url === 'string' && flags.url) || DEFAULT_URL
|
|
311
|
+
const token = (typeof flags.token === 'string' && flags.token) || loadToken()
|
|
183
312
|
const command = positional[0]
|
|
184
313
|
|
|
185
314
|
if (!command || command === 'help' || flags.help) {
|
|
@@ -187,15 +316,16 @@ async function main() {
|
|
|
187
316
|
process.exit(0)
|
|
188
317
|
}
|
|
189
318
|
|
|
319
|
+
const client = makeClient(url, token)
|
|
320
|
+
|
|
190
321
|
try {
|
|
191
322
|
switch (command) {
|
|
192
323
|
case 'ping': {
|
|
193
|
-
|
|
194
|
-
printJson(result)
|
|
324
|
+
printJson(await client.call('gateway:ping'))
|
|
195
325
|
break
|
|
196
326
|
}
|
|
197
327
|
case 'version': {
|
|
198
|
-
const result = await call(
|
|
328
|
+
const result = await client.call('gateway:describe')
|
|
199
329
|
printJson({ version: result.version, methodCount: result.count, categories: result.categories })
|
|
200
330
|
break
|
|
201
331
|
}
|
|
@@ -203,7 +333,7 @@ async function main() {
|
|
|
203
333
|
const params = {}
|
|
204
334
|
if (typeof flags.category === 'string') params.category = flags.category
|
|
205
335
|
if (typeof flags.prefix === 'string') params.prefix = flags.prefix
|
|
206
|
-
const result = await call(
|
|
336
|
+
const result = await client.call('gateway:describe', params)
|
|
207
337
|
printJson(result.methods)
|
|
208
338
|
break
|
|
209
339
|
}
|
|
@@ -214,57 +344,58 @@ async function main() {
|
|
|
214
344
|
if (positional[2]) {
|
|
215
345
|
try { params = JSON.parse(positional[2]) } catch { fail('params must be valid JSON') }
|
|
216
346
|
}
|
|
217
|
-
|
|
218
|
-
printJson(result)
|
|
347
|
+
printJson(await client.call(method, params))
|
|
219
348
|
break
|
|
220
349
|
}
|
|
221
350
|
case 'terminals': {
|
|
222
|
-
|
|
223
|
-
|
|
351
|
+
printJson(await client.call('terminal:list'))
|
|
352
|
+
break
|
|
353
|
+
}
|
|
354
|
+
case 'connections': {
|
|
355
|
+
const { ssh, winrm, serial } = await fetchConnections(client)
|
|
356
|
+
const out = { connections: [] }
|
|
357
|
+
for (const kind of ['ssh', 'winrm', 'serial']) {
|
|
358
|
+
const list = kind === 'ssh' ? ssh : kind === 'winrm' ? winrm : serial
|
|
359
|
+
for (const c of list) {
|
|
360
|
+
out.connections.push({
|
|
361
|
+
kind,
|
|
362
|
+
name: c.name,
|
|
363
|
+
id: c.id,
|
|
364
|
+
host: c.host || c.path || '',
|
|
365
|
+
port: c.port ?? c.baudRate ?? '',
|
|
366
|
+
username: c.username || '',
|
|
367
|
+
})
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
printJson(out)
|
|
224
371
|
break
|
|
225
372
|
}
|
|
226
373
|
case 'open': {
|
|
227
374
|
const name = positional[1]
|
|
228
375
|
if (!name) fail('open needs a saved connection name: rterm open <name>')
|
|
229
|
-
const
|
|
230
|
-
|
|
376
|
+
const found = await resolveConnection(client, name)
|
|
377
|
+
if (!found) {
|
|
378
|
+
const { ssh, winrm, serial } = await fetchConnections(client)
|
|
379
|
+
const names = [...ssh, ...winrm, ...serial].map((c) => c.name).filter(Boolean)
|
|
380
|
+
fail(`No saved connection named "${name}". Available: ${names.join(', ') || '(none)'}`)
|
|
381
|
+
}
|
|
382
|
+
const config = connectionToConfig(found.entry, found.kind)
|
|
383
|
+
const result = await client.call('terminal:createTab', { config })
|
|
384
|
+
printJson({ opened: name, kind: found.kind, ...result })
|
|
231
385
|
break
|
|
232
386
|
}
|
|
233
387
|
case 'close': {
|
|
234
388
|
const tab = positional[1]
|
|
235
389
|
if (!tab) fail('close needs a tab id or name: rterm close <tabIdOrName>')
|
|
236
|
-
const
|
|
237
|
-
printJson(
|
|
390
|
+
const tabId = (await resolveTabId(client, tab)) || tab
|
|
391
|
+
printJson(await client.call('terminal:kill', { terminalId: tabId }))
|
|
238
392
|
break
|
|
239
393
|
}
|
|
240
394
|
case 'run': {
|
|
241
395
|
const tab = positional[1]
|
|
242
396
|
const commandText = positional.slice(2).join(' ')
|
|
243
397
|
if (!tab || !commandText) fail('run needs: rterm run <tabIdOrName> <command>')
|
|
244
|
-
|
|
245
|
-
// command with a newline, then poll the buffer until it settles.
|
|
246
|
-
const before = await call(url, 'terminal:getBufferDelta', { terminalId: tab, fromOffset: 0 })
|
|
247
|
-
const startOffset = Number(before?.offset ?? 0)
|
|
248
|
-
await call(url, 'terminal:write', { terminalId: tab, data: `${commandText}\n` })
|
|
249
|
-
let output = ''
|
|
250
|
-
let lastOffset = startOffset
|
|
251
|
-
let stable = 0
|
|
252
|
-
const deadline = Date.now() + 30_000
|
|
253
|
-
while (Date.now() < deadline) {
|
|
254
|
-
await new Promise((resolve) => setTimeout(resolve, 400))
|
|
255
|
-
const delta = await call(url, 'terminal:getBufferDelta', { terminalId: tab, fromOffset: lastOffset })
|
|
256
|
-
const data = typeof delta?.data === 'string' ? delta.data : ''
|
|
257
|
-
const offset = Number(delta?.offset ?? lastOffset)
|
|
258
|
-
if (data) output += data
|
|
259
|
-
if (offset === lastOffset && !data) {
|
|
260
|
-
stable += 1
|
|
261
|
-
if (stable >= 3) break
|
|
262
|
-
} else {
|
|
263
|
-
stable = 0
|
|
264
|
-
}
|
|
265
|
-
lastOffset = offset
|
|
266
|
-
}
|
|
267
|
-
console.log(output.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, '').trimEnd())
|
|
398
|
+
console.log(await runInTab(client, tab, commandText))
|
|
268
399
|
break
|
|
269
400
|
}
|
|
270
401
|
case 'fleet': {
|
|
@@ -274,56 +405,32 @@ async function main() {
|
|
|
274
405
|
for (const tab of tabs) {
|
|
275
406
|
console.log(`── ${tab} ──`)
|
|
276
407
|
try {
|
|
277
|
-
|
|
278
|
-
const startOffset = Number(before?.offset ?? 0)
|
|
279
|
-
await call(url, 'terminal:write', { terminalId: tab, data: `${commandText}\n` })
|
|
280
|
-
let output = ''
|
|
281
|
-
let lastOffset = startOffset
|
|
282
|
-
let stable = 0
|
|
283
|
-
const deadline = Date.now() + 30_000
|
|
284
|
-
while (Date.now() < deadline) {
|
|
285
|
-
await new Promise((resolve) => setTimeout(resolve, 400))
|
|
286
|
-
const delta = await call(url, 'terminal:getBufferDelta', { terminalId: tab, fromOffset: lastOffset })
|
|
287
|
-
const data = typeof delta?.data === 'string' ? delta.data : ''
|
|
288
|
-
const offset = Number(delta?.offset ?? lastOffset)
|
|
289
|
-
if (data) output += data
|
|
290
|
-
if (offset === lastOffset && !data) {
|
|
291
|
-
stable += 1
|
|
292
|
-
if (stable >= 3) break
|
|
293
|
-
} else {
|
|
294
|
-
stable = 0
|
|
295
|
-
}
|
|
296
|
-
lastOffset = offset
|
|
297
|
-
}
|
|
298
|
-
console.log(output.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, '').trimEnd())
|
|
408
|
+
console.log(await runInTab(client, tab, commandText))
|
|
299
409
|
} catch (error) {
|
|
300
|
-
console.log(`Error: ${
|
|
410
|
+
console.log(`Error: ${errorMessage(error)}`)
|
|
301
411
|
process.exitCode = 2
|
|
302
412
|
}
|
|
303
413
|
}
|
|
304
414
|
break
|
|
305
415
|
}
|
|
306
416
|
case 'sessions': {
|
|
307
|
-
|
|
308
|
-
printJson(result)
|
|
417
|
+
printJson(await client.call('session:list'))
|
|
309
418
|
break
|
|
310
419
|
}
|
|
311
420
|
case 'chat': {
|
|
312
421
|
const sessionId = positional[1]
|
|
313
422
|
const message = positional.slice(2).join(' ')
|
|
314
423
|
if (!sessionId || !message) fail('chat needs: rterm chat <sessionId> <message>')
|
|
315
|
-
|
|
316
|
-
printJson(result)
|
|
424
|
+
printJson(await client.call('agent:startTask', { sessionId, userInput: message }))
|
|
317
425
|
break
|
|
318
426
|
}
|
|
319
427
|
case 'dashboard': {
|
|
320
|
-
|
|
321
|
-
printJson(result)
|
|
428
|
+
printJson(await client.call('observability:liveDashboardState'))
|
|
322
429
|
break
|
|
323
430
|
}
|
|
324
431
|
case 'metrics': {
|
|
325
432
|
const format = flags.format === 'prometheus' ? 'prometheus' : 'summary'
|
|
326
|
-
const result = await call(
|
|
433
|
+
const result = await client.call('observability:metricsPrometheus', { format })
|
|
327
434
|
console.log(typeof result === 'string' ? result : JSON.stringify(result, null, 2))
|
|
328
435
|
break
|
|
329
436
|
}
|
|
@@ -331,7 +438,7 @@ async function main() {
|
|
|
331
438
|
fail(`Unknown command: ${command}. Run "rterm help".`)
|
|
332
439
|
}
|
|
333
440
|
} catch (error) {
|
|
334
|
-
fail(error
|
|
441
|
+
fail(error)
|
|
335
442
|
}
|
|
336
443
|
}
|
|
337
444
|
|