rterm-cli 3.2.9
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 +61 -0
- package/package.json +34 -0
- package/rterm-cli.mjs +338 -0
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# rterm CLI
|
|
2
|
+
|
|
3
|
+
A `gyll`-style command CLI for the RTerm / neuralOS backend. Speaks the backend's
|
|
4
|
+
WebSocket JSON-RPC gateway (`ws://host:17888`) natively — **zero dependencies**.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
# no install (Node >= 21)
|
|
8
|
+
npx rterm-cli ping
|
|
9
|
+
|
|
10
|
+
# global install
|
|
11
|
+
npm install -g rterm-cli
|
|
12
|
+
rterm ping
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Requires a running backend (`gybackend` from `npm i -g neuralos`, or the RTerm
|
|
16
|
+
desktop app) on `ws://127.0.0.1:17888`.
|
|
17
|
+
|
|
18
|
+
## Install / run
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# from a checkout
|
|
22
|
+
node apps/cli/rterm-cli.mjs ping
|
|
23
|
+
|
|
24
|
+
# link it
|
|
25
|
+
npm --workspace @rterm/cli run build 2>/dev/null || true
|
|
26
|
+
ln -s "$(pwd)/apps/cli/rterm-cli.mjs" /usr/local/bin/rterm
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Commands
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
rterm ping # liveness check
|
|
33
|
+
rterm version # backend version + method count
|
|
34
|
+
rterm methods [--category terminal] # self-describing RPC surface
|
|
35
|
+
rterm call <method> [json-params] # raw JSON-RPC call
|
|
36
|
+
rterm terminals # list terminal tabs
|
|
37
|
+
rterm open <saved-connection-name> # open a tab for a saved connection
|
|
38
|
+
rterm close <tabIdOrName> # close a terminal tab
|
|
39
|
+
rterm run <tabIdOrName> <command> # run a command in a tab (waits)
|
|
40
|
+
rterm fleet <tab1,tab2,...> <command> # run on many tabs at once
|
|
41
|
+
rterm sessions # list chat sessions
|
|
42
|
+
rterm chat <sessionId> <message> # send a message to the agent (blocking)
|
|
43
|
+
rterm dashboard # live dashboard state
|
|
44
|
+
rterm metrics [--format prometheus] # host metrics
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Configuration
|
|
48
|
+
|
|
49
|
+
| Env | Default | Meaning |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| `RTERM_URL` | `ws://127.0.0.1:17888` | Gateway URL |
|
|
52
|
+
| `RTERM_HOST` / `RTERM_PORT` | `127.0.0.1` / `17888` | Build the URL if `RTERM_URL` is unset |
|
|
53
|
+
| `RTERM_TOKEN` | — | Access token (required for non-localhost gateways) |
|
|
54
|
+
|
|
55
|
+
The CLI also auto-loads the first token from
|
|
56
|
+
`~/.gybackend-data/access-tokens.json` when present.
|
|
57
|
+
|
|
58
|
+
## Node version
|
|
59
|
+
|
|
60
|
+
Uses the native `WebSocket` client (Node ≥ 21). On older Node it falls back to
|
|
61
|
+
the `ws` package when resolvable.
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rterm-cli",
|
|
3
|
+
"version": "3.2.9",
|
|
4
|
+
"description": "rterm — zero-dependency command CLI for the RTerm / neuralOS backend WebSocket gateway (ping, terminals, run, fleet, chat, dashboard, metrics, raw call)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"rterm": "./rterm-cli.mjs",
|
|
8
|
+
"rterm-cli": "./rterm-cli.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"rterm-cli.mjs",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"rterm",
|
|
16
|
+
"neuralos",
|
|
17
|
+
"terminal",
|
|
18
|
+
"ssh",
|
|
19
|
+
"cli",
|
|
20
|
+
"websocket",
|
|
21
|
+
"json-rpc",
|
|
22
|
+
"devops",
|
|
23
|
+
"remote"
|
|
24
|
+
],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18"
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/DrOlu/RTerm.git",
|
|
32
|
+
"directory": "apps/cli"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/rterm-cli.mjs
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* rterm-cli — a `gyll`-style command CLI for the RTerm / neuralOS backend.
|
|
4
|
+
*
|
|
5
|
+
* Speaks the backend's WebSocket JSON-RPC gateway natively (ws://host:17888):
|
|
6
|
+
* rterm ping
|
|
7
|
+
* rterm methods [--category cat] [--prefix p]
|
|
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>
|
|
17
|
+
*
|
|
18
|
+
* Zero runtime dependencies: uses Node's built-in WebSocket (Node >= 21) and
|
|
19
|
+
* falls back to the `ws` package from the backend install when present.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { readFileSync, existsSync } from 'node:fs'
|
|
23
|
+
import { homedir } from 'node:os'
|
|
24
|
+
import { join } from 'node:path'
|
|
25
|
+
|
|
26
|
+
const DEFAULT_HOST = process.env.RTERM_HOST || '127.0.0.1'
|
|
27
|
+
const DEFAULT_PORT = Number(process.env.RTERM_PORT || 17888)
|
|
28
|
+
const DEFAULT_URL = process.env.RTERM_URL || `ws://${DEFAULT_HOST}:${DEFAULT_PORT}`
|
|
29
|
+
|
|
30
|
+
// ── tiny arg parser ─────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
function parseArgs(argv) {
|
|
33
|
+
const positional = []
|
|
34
|
+
const flags = {}
|
|
35
|
+
for (let i = 0; i < argv.length; i++) {
|
|
36
|
+
const arg = argv[i]
|
|
37
|
+
if (arg.startsWith('--')) {
|
|
38
|
+
const key = arg.slice(2)
|
|
39
|
+
const next = argv[i + 1]
|
|
40
|
+
if (next !== undefined && !next.startsWith('--')) {
|
|
41
|
+
flags[key] = next
|
|
42
|
+
i++
|
|
43
|
+
} else {
|
|
44
|
+
flags[key] = true
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
positional.push(arg)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { positional, flags }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── gateway client ──────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
let nextId = 1
|
|
56
|
+
|
|
57
|
+
function loadToken() {
|
|
58
|
+
if (process.env.RTERM_TOKEN) return process.env.RTERM_TOKEN
|
|
59
|
+
const candidates = [
|
|
60
|
+
join(homedir(), '.gybackend-data', 'access-tokens.json'),
|
|
61
|
+
join(process.cwd(), '.gybackend-data', 'access-tokens.json'),
|
|
62
|
+
]
|
|
63
|
+
for (const path of candidates) {
|
|
64
|
+
if (existsSync(path)) {
|
|
65
|
+
try {
|
|
66
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'))
|
|
67
|
+
const first = Array.isArray(parsed) ? parsed[0] : parsed
|
|
68
|
+
if (first && typeof first.token === 'string') return first.token
|
|
69
|
+
if (typeof parsed === 'string') return parsed
|
|
70
|
+
} catch { /* ignore malformed */ }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function openSocket(url) {
|
|
77
|
+
// Node >= 21 ships a native WebSocket client.
|
|
78
|
+
if (typeof globalThis.WebSocket === 'function') {
|
|
79
|
+
return await new Promise((resolve, reject) => {
|
|
80
|
+
const ws = new globalThis.WebSocket(url)
|
|
81
|
+
ws.onopen = () => resolve(ws)
|
|
82
|
+
ws.onerror = () => reject(new Error(`Cannot connect to ${url}. Is the backend running? (gybackend)`))
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
// Fallback: the `ws` package (present in a backend checkout/install).
|
|
86
|
+
try {
|
|
87
|
+
const { createRequire } = await import('node:module')
|
|
88
|
+
const require = createRequire(import.meta.url)
|
|
89
|
+
const WS = require('ws')
|
|
90
|
+
return await new Promise((resolve, reject) => {
|
|
91
|
+
const ws = new WS(url)
|
|
92
|
+
ws.on('open', () => resolve(ws))
|
|
93
|
+
ws.on('error', () => reject(new Error(`Cannot connect to ${url}. Is the backend running? (gybackend)`)))
|
|
94
|
+
})
|
|
95
|
+
} catch {
|
|
96
|
+
throw new Error('No WebSocket client available. Use Node >= 21 or install the `ws` package.')
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function call(url, method, params) {
|
|
101
|
+
const ws = await openSocket(url)
|
|
102
|
+
const id = String(nextId++)
|
|
103
|
+
return await new Promise((resolve, reject) => {
|
|
104
|
+
const timer = setTimeout(() => {
|
|
105
|
+
try { ws.close() } catch { /* ignore */ }
|
|
106
|
+
reject(new Error(`Timeout calling ${method} (60s)`))
|
|
107
|
+
}, 60_000)
|
|
108
|
+
const onMessage = (raw) => {
|
|
109
|
+
try {
|
|
110
|
+
const frame = JSON.parse(typeof raw === 'string' ? raw : raw.toString())
|
|
111
|
+
if (frame.type === 'gateway:response' && frame.id === id) {
|
|
112
|
+
clearTimeout(timer)
|
|
113
|
+
try { ws.close() } catch { /* ignore */ }
|
|
114
|
+
if (frame.ok) resolve(frame.result)
|
|
115
|
+
else reject(new Error(frame.error || `gateway error: ${method}`))
|
|
116
|
+
}
|
|
117
|
+
} catch { /* ignore non-JSON frames */ }
|
|
118
|
+
}
|
|
119
|
+
const onClose = () => { clearTimeout(timer); reject(new Error('Connection closed before response.')) }
|
|
120
|
+
|
|
121
|
+
// Normalize the native WebSocket (addEventListener) and the `ws` package
|
|
122
|
+
// (on/on('message')) behind one interface.
|
|
123
|
+
if (typeof ws.addEventListener === 'function') {
|
|
124
|
+
ws.addEventListener('message', (event) => onMessage(event.data))
|
|
125
|
+
ws.addEventListener('close', onClose)
|
|
126
|
+
ws.addEventListener('error', onClose)
|
|
127
|
+
} else if (typeof ws.on === 'function') {
|
|
128
|
+
ws.on('message', (data) => onMessage(data))
|
|
129
|
+
ws.on('close', onClose)
|
|
130
|
+
ws.on('error', onClose)
|
|
131
|
+
} else {
|
|
132
|
+
ws.onmessage = (event) => onMessage(event.data)
|
|
133
|
+
ws.onclose = onClose
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const payload = { id, method, ...(params !== undefined ? { params } : {}) }
|
|
137
|
+
ws.send(JSON.stringify(payload))
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── output helpers ──────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
function printJson(value) {
|
|
144
|
+
console.log(JSON.stringify(value, null, 2))
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function fail(message) {
|
|
148
|
+
console.error(`Error: ${message}`)
|
|
149
|
+
process.exit(1)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const HELP = `rterm — command CLI for the RTerm / neuralOS backend gateway
|
|
153
|
+
|
|
154
|
+
Usage:
|
|
155
|
+
rterm ping Liveness check
|
|
156
|
+
rterm methods [--category c] [--prefix p] List gateway RPC methods
|
|
157
|
+
rterm call <method> [json] Raw JSON-RPC call (params as JSON)
|
|
158
|
+
rterm terminals List terminal tabs
|
|
159
|
+
rterm open <connection-name> Open a terminal tab for a saved connection
|
|
160
|
+
rterm close <tabIdOrName> Close a terminal tab
|
|
161
|
+
rterm run <tabIdOrName> <command> Run a command in a terminal tab (waits)
|
|
162
|
+
rterm fleet <tab1,tab2,...> <command> Run a command on many tabs at once
|
|
163
|
+
rterm sessions List chat sessions
|
|
164
|
+
rterm chat <sessionId> <message> Send a message to the agent (blocking)
|
|
165
|
+
rterm dashboard Print the live dashboard state
|
|
166
|
+
rterm metrics [--format prometheus] Host metrics
|
|
167
|
+
rterm version Backend version + method count
|
|
168
|
+
|
|
169
|
+
Options:
|
|
170
|
+
--url ws://host:port Gateway URL (default ${DEFAULT_URL}, env RTERM_URL)
|
|
171
|
+
--token <token> Access token (env RTERM_TOKEN; non-localhost requires one)
|
|
172
|
+
|
|
173
|
+
Environment:
|
|
174
|
+
RTERM_URL, RTERM_HOST, RTERM_PORT, RTERM_TOKEN
|
|
175
|
+
`
|
|
176
|
+
|
|
177
|
+
// ── commands ────────────────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
async function main() {
|
|
180
|
+
const argv = process.argv.slice(2)
|
|
181
|
+
const { positional, flags } = parseArgs(argv)
|
|
182
|
+
const url = (typeof flags.url === 'string' && flags.url) || DEFAULT_URL
|
|
183
|
+
const command = positional[0]
|
|
184
|
+
|
|
185
|
+
if (!command || command === 'help' || flags.help) {
|
|
186
|
+
console.log(HELP)
|
|
187
|
+
process.exit(0)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
switch (command) {
|
|
192
|
+
case 'ping': {
|
|
193
|
+
const result = await call(url, 'gateway:ping')
|
|
194
|
+
printJson(result)
|
|
195
|
+
break
|
|
196
|
+
}
|
|
197
|
+
case 'version': {
|
|
198
|
+
const result = await call(url, 'gateway:describe')
|
|
199
|
+
printJson({ version: result.version, methodCount: result.count, categories: result.categories })
|
|
200
|
+
break
|
|
201
|
+
}
|
|
202
|
+
case 'methods': {
|
|
203
|
+
const params = {}
|
|
204
|
+
if (typeof flags.category === 'string') params.category = flags.category
|
|
205
|
+
if (typeof flags.prefix === 'string') params.prefix = flags.prefix
|
|
206
|
+
const result = await call(url, 'gateway:describe', params)
|
|
207
|
+
printJson(result.methods)
|
|
208
|
+
break
|
|
209
|
+
}
|
|
210
|
+
case 'call': {
|
|
211
|
+
const method = positional[1]
|
|
212
|
+
if (!method) fail('call needs a method name: rterm call <method> [json-params]')
|
|
213
|
+
let params
|
|
214
|
+
if (positional[2]) {
|
|
215
|
+
try { params = JSON.parse(positional[2]) } catch { fail('params must be valid JSON') }
|
|
216
|
+
}
|
|
217
|
+
const result = await call(url, method, params)
|
|
218
|
+
printJson(result)
|
|
219
|
+
break
|
|
220
|
+
}
|
|
221
|
+
case 'terminals': {
|
|
222
|
+
const result = await call(url, 'terminal:list')
|
|
223
|
+
printJson(result)
|
|
224
|
+
break
|
|
225
|
+
}
|
|
226
|
+
case 'open': {
|
|
227
|
+
const name = positional[1]
|
|
228
|
+
if (!name) fail('open needs a saved connection name: rterm open <name>')
|
|
229
|
+
const result = await call(url, 'terminal:createTab', { config: { savedConnectionName: name } })
|
|
230
|
+
printJson(result)
|
|
231
|
+
break
|
|
232
|
+
}
|
|
233
|
+
case 'close': {
|
|
234
|
+
const tab = positional[1]
|
|
235
|
+
if (!tab) fail('close needs a tab id or name: rterm close <tabIdOrName>')
|
|
236
|
+
const result = await call(url, 'terminal:kill', { terminalId: tab })
|
|
237
|
+
printJson(result)
|
|
238
|
+
break
|
|
239
|
+
}
|
|
240
|
+
case 'run': {
|
|
241
|
+
const tab = positional[1]
|
|
242
|
+
const commandText = positional.slice(2).join(' ')
|
|
243
|
+
if (!tab || !commandText) fail('run needs: rterm run <tabIdOrName> <command>')
|
|
244
|
+
// The gateway's terminal surface is write + buffer-delta: send the
|
|
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())
|
|
268
|
+
break
|
|
269
|
+
}
|
|
270
|
+
case 'fleet': {
|
|
271
|
+
const tabs = (positional[1] || '').split(',').map((s) => s.trim()).filter(Boolean)
|
|
272
|
+
const commandText = positional.slice(2).join(' ')
|
|
273
|
+
if (tabs.length === 0 || !commandText) fail('fleet needs: rterm fleet <tab1,tab2,...> <command>')
|
|
274
|
+
for (const tab of tabs) {
|
|
275
|
+
console.log(`── ${tab} ──`)
|
|
276
|
+
try {
|
|
277
|
+
const before = await call(url, 'terminal:getBufferDelta', { terminalId: tab, fromOffset: 0 })
|
|
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())
|
|
299
|
+
} catch (error) {
|
|
300
|
+
console.log(`Error: ${error instanceof Error ? error.message : String(error)}`)
|
|
301
|
+
process.exitCode = 2
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
break
|
|
305
|
+
}
|
|
306
|
+
case 'sessions': {
|
|
307
|
+
const result = await call(url, 'session:list')
|
|
308
|
+
printJson(result)
|
|
309
|
+
break
|
|
310
|
+
}
|
|
311
|
+
case 'chat': {
|
|
312
|
+
const sessionId = positional[1]
|
|
313
|
+
const message = positional.slice(2).join(' ')
|
|
314
|
+
if (!sessionId || !message) fail('chat needs: rterm chat <sessionId> <message>')
|
|
315
|
+
const result = await call(url, 'agent:startTask', { sessionId, userInput: message })
|
|
316
|
+
printJson(result)
|
|
317
|
+
break
|
|
318
|
+
}
|
|
319
|
+
case 'dashboard': {
|
|
320
|
+
const result = await call(url, 'observability:liveDashboardState')
|
|
321
|
+
printJson(result)
|
|
322
|
+
break
|
|
323
|
+
}
|
|
324
|
+
case 'metrics': {
|
|
325
|
+
const format = flags.format === 'prometheus' ? 'prometheus' : 'summary'
|
|
326
|
+
const result = await call(url, 'observability:metricsPrometheus', { format })
|
|
327
|
+
console.log(typeof result === 'string' ? result : JSON.stringify(result, null, 2))
|
|
328
|
+
break
|
|
329
|
+
}
|
|
330
|
+
default:
|
|
331
|
+
fail(`Unknown command: ${command}. Run "rterm help".`)
|
|
332
|
+
}
|
|
333
|
+
} catch (error) {
|
|
334
|
+
fail(error instanceof Error ? error.message : String(error))
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
main()
|