@roj-ai/cli 0.0.2
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/dist/client.d.ts +8 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +28 -0
- package/dist/commands/answer.d.ts +3 -0
- package/dist/commands/answer.d.ts.map +1 -0
- package/dist/commands/answer.js +15 -0
- package/dist/commands/debug-format.d.ts +12 -0
- package/dist/commands/debug-format.d.ts.map +1 -0
- package/dist/commands/debug-format.js +262 -0
- package/dist/commands/debug.d.ts +14 -0
- package/dist/commands/debug.d.ts.map +1 -0
- package/dist/commands/debug.js +187 -0
- package/dist/commands/messages.d.ts +4 -0
- package/dist/commands/messages.d.ts.map +1 -0
- package/dist/commands/messages.js +77 -0
- package/dist/commands/presets.d.ts +3 -0
- package/dist/commands/presets.d.ts.map +1 -0
- package/dist/commands/presets.js +14 -0
- package/dist/commands/sessions.d.ts +6 -0
- package/dist/commands/sessions.d.ts.map +1 -0
- package/dist/commands/sessions.js +50 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +277 -0
- package/dist/repl/formatter.d.ts +16 -0
- package/dist/repl/formatter.d.ts.map +1 -0
- package/dist/repl/formatter.js +72 -0
- package/dist/repl/repl.d.ts +3 -0
- package/dist/repl/repl.d.ts.map +1 -0
- package/dist/repl/repl.js +212 -0
- package/dist/repl/session-picker.d.ts +6 -0
- package/dist/repl/session-picker.d.ts.map +1 -0
- package/dist/repl/session-picker.js +60 -0
- package/dist/unwrap.d.ts +4 -0
- package/dist/unwrap.d.ts.map +1 -0
- package/dist/unwrap.js +6 -0
- package/package.json +27 -0
- package/src/client.ts +41 -0
- package/src/commands/answer.ts +27 -0
- package/src/commands/debug-format.ts +299 -0
- package/src/commands/debug.ts +347 -0
- package/src/commands/messages.ts +92 -0
- package/src/commands/presets.ts +22 -0
- package/src/commands/sessions.ts +73 -0
- package/src/main.ts +319 -0
- package/src/repl/formatter.ts +84 -0
- package/src/repl/repl.ts +246 -0
- package/src/repl/session-picker.ts +81 -0
- package/src/unwrap.ts +8 -0
package/src/main.ts
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { SessionId } from '@roj-ai/shared'
|
|
2
|
+
import { RpcError } from '@roj-ai/shared/rpc'
|
|
3
|
+
import { createCliClient } from './client.js'
|
|
4
|
+
import { answerCommand } from './commands/answer.js'
|
|
5
|
+
import {
|
|
6
|
+
agentCommand,
|
|
7
|
+
agentsCommand,
|
|
8
|
+
debugSendCommand,
|
|
9
|
+
eventsCommand,
|
|
10
|
+
llmCallCommand,
|
|
11
|
+
llmCallsCommand,
|
|
12
|
+
mailboxCommand,
|
|
13
|
+
metricsCommand,
|
|
14
|
+
presetAgentsCommand,
|
|
15
|
+
spawnAgentCommand,
|
|
16
|
+
timelineCommand,
|
|
17
|
+
} from './commands/debug.js'
|
|
18
|
+
import { messagesCommand, sendCommand } from './commands/messages.js'
|
|
19
|
+
import { presetsCommand } from './commands/presets.js'
|
|
20
|
+
import { sessionCloseCommand, sessionCreateCommand, sessionGetCommand, sessionsListCommand } from './commands/sessions.js'
|
|
21
|
+
import { startRepl } from './repl/repl.js'
|
|
22
|
+
|
|
23
|
+
const USAGE = `Usage:
|
|
24
|
+
roj-cli [options] Start REPL
|
|
25
|
+
roj-cli <command> [args] [options] Non-interactive
|
|
26
|
+
|
|
27
|
+
Options:
|
|
28
|
+
--url <url> Server URL (default: http://localhost:2486, env: ROJ_URL)
|
|
29
|
+
--json Output as JSON (non-interactive only)
|
|
30
|
+
|
|
31
|
+
Commands:
|
|
32
|
+
presets List available presets
|
|
33
|
+
sessions [--status active|closed|errored] List sessions
|
|
34
|
+
session-create <presetId> Create new session
|
|
35
|
+
session-get <sessionId> Get session info
|
|
36
|
+
session-close <sessionId> Close session
|
|
37
|
+
messages <sessionId> Get messages
|
|
38
|
+
send <sessionId> <message> [--wait] Send message
|
|
39
|
+
answer <sessionId> <questionId> <answer> Answer agent question
|
|
40
|
+
|
|
41
|
+
Debug commands:
|
|
42
|
+
events <sessionId> [--type <type>] [--agent <id>] [--limit N] Show domain events
|
|
43
|
+
agents <sessionId> Show agent tree
|
|
44
|
+
agent <sessionId> <agentId> Show agent detail
|
|
45
|
+
mailbox <sessionId> Show global mailbox
|
|
46
|
+
timeline <sessionId> Show execution timeline
|
|
47
|
+
metrics <sessionId> Show session metrics
|
|
48
|
+
llm-calls <sessionId> [--limit N] List LLM calls
|
|
49
|
+
llm-call <sessionId> <callId> Show LLM call detail
|
|
50
|
+
preset-agents <sessionId> Show spawnable agents
|
|
51
|
+
debug-send <sessionId> <agentId> <message> [--from <sender>] Send debug message
|
|
52
|
+
spawn-agent <sessionId> <defName> <parentId> [--message <msg>] Spawn agent manually`
|
|
53
|
+
|
|
54
|
+
interface ParsedArgs {
|
|
55
|
+
url: string
|
|
56
|
+
json: boolean
|
|
57
|
+
command: string | null
|
|
58
|
+
positional: string[]
|
|
59
|
+
flags: Record<string, string | true>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseArgs(argv: string[]): ParsedArgs {
|
|
63
|
+
// Skip bun and script path
|
|
64
|
+
const args = argv.slice(2)
|
|
65
|
+
|
|
66
|
+
let url = process.env.ROJ_URL ?? 'http://localhost:2486'
|
|
67
|
+
let json = false
|
|
68
|
+
const positional: string[] = []
|
|
69
|
+
const flags: Record<string, string | true> = {}
|
|
70
|
+
|
|
71
|
+
for (let i = 0; i < args.length; i++) {
|
|
72
|
+
const arg = args[i]
|
|
73
|
+
if (arg === '--url') {
|
|
74
|
+
url = args[++i]
|
|
75
|
+
} else if (arg === '--json') {
|
|
76
|
+
json = true
|
|
77
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
78
|
+
console.log(USAGE)
|
|
79
|
+
process.exit(0)
|
|
80
|
+
} else if (arg.startsWith('--')) {
|
|
81
|
+
const key = arg.slice(2)
|
|
82
|
+
const next = args[i + 1]
|
|
83
|
+
if (next && !next.startsWith('--')) {
|
|
84
|
+
flags[key] = next
|
|
85
|
+
i++
|
|
86
|
+
} else {
|
|
87
|
+
flags[key] = true
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
positional.push(arg)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
url,
|
|
96
|
+
json,
|
|
97
|
+
command: positional[0] ?? null,
|
|
98
|
+
positional: positional.slice(1),
|
|
99
|
+
flags,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function main(): Promise<void> {
|
|
104
|
+
const parsed = parseArgs(process.argv)
|
|
105
|
+
const client = createCliClient(parsed.url)
|
|
106
|
+
|
|
107
|
+
// REPL mode if no command
|
|
108
|
+
if (!parsed.command) {
|
|
109
|
+
await startRepl(client, parsed.url)
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
switch (parsed.command) {
|
|
114
|
+
case 'presets':
|
|
115
|
+
await presetsCommand(client, parsed.json)
|
|
116
|
+
break
|
|
117
|
+
|
|
118
|
+
case 'sessions': {
|
|
119
|
+
const status = parsed.flags.status as 'active' | 'closed' | 'errored' | undefined
|
|
120
|
+
await sessionsListCommand(client, typeof status === 'string' ? status : undefined, parsed.json)
|
|
121
|
+
break
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
case 'session-create': {
|
|
125
|
+
const presetId = parsed.positional[0]
|
|
126
|
+
if (!presetId) {
|
|
127
|
+
console.error('Usage: session-create <presetId>')
|
|
128
|
+
process.exit(1)
|
|
129
|
+
}
|
|
130
|
+
await sessionCreateCommand(client, presetId, parsed.json)
|
|
131
|
+
break
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
case 'session-get': {
|
|
135
|
+
const sessionId = parsed.positional[0]
|
|
136
|
+
if (!sessionId) {
|
|
137
|
+
console.error('Usage: session-get <sessionId>')
|
|
138
|
+
process.exit(1)
|
|
139
|
+
}
|
|
140
|
+
await sessionGetCommand(client, sessionId, parsed.json)
|
|
141
|
+
break
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
case 'session-close': {
|
|
145
|
+
const sessionId = parsed.positional[0]
|
|
146
|
+
if (!sessionId) {
|
|
147
|
+
console.error('Usage: session-close <sessionId>')
|
|
148
|
+
process.exit(1)
|
|
149
|
+
}
|
|
150
|
+
await sessionCloseCommand(client, sessionId, parsed.json)
|
|
151
|
+
break
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
case 'messages': {
|
|
155
|
+
const sessionId = parsed.positional[0]
|
|
156
|
+
if (!sessionId) {
|
|
157
|
+
console.error('Usage: messages <sessionId>')
|
|
158
|
+
process.exit(1)
|
|
159
|
+
}
|
|
160
|
+
await messagesCommand(client, sessionId, parsed.json)
|
|
161
|
+
break
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
case 'send': {
|
|
165
|
+
const sessionId = parsed.positional[0]
|
|
166
|
+
const message = parsed.positional.slice(1).join(' ')
|
|
167
|
+
if (!sessionId || !message) {
|
|
168
|
+
console.error('Usage: send <sessionId> <message> [--wait]')
|
|
169
|
+
process.exit(1)
|
|
170
|
+
}
|
|
171
|
+
const wait = parsed.flags.wait === true
|
|
172
|
+
await sendCommand(client, parsed.url, sessionId, message, wait, parsed.json)
|
|
173
|
+
break
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
case 'answer': {
|
|
177
|
+
const sessionId = parsed.positional[0]
|
|
178
|
+
const questionId = parsed.positional[1]
|
|
179
|
+
const answer = parsed.positional.slice(2).join(' ')
|
|
180
|
+
if (!sessionId || !questionId || !answer) {
|
|
181
|
+
console.error('Usage: answer <sessionId> <questionId> <answer>')
|
|
182
|
+
process.exit(1)
|
|
183
|
+
}
|
|
184
|
+
await answerCommand(client, sessionId, questionId, answer, parsed.json)
|
|
185
|
+
break
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Debug commands
|
|
189
|
+
case 'events': {
|
|
190
|
+
const sessionId = parsed.positional[0]
|
|
191
|
+
if (!sessionId) {
|
|
192
|
+
console.error('Usage: events <sessionId> [--type <type>] [--agent <agentId>] [--limit N]')
|
|
193
|
+
process.exit(1)
|
|
194
|
+
}
|
|
195
|
+
await eventsCommand(client, sessionId, parsed.flags, parsed.json)
|
|
196
|
+
break
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
case 'agents': {
|
|
200
|
+
const sessionId = parsed.positional[0]
|
|
201
|
+
if (!sessionId) {
|
|
202
|
+
console.error('Usage: agents <sessionId>')
|
|
203
|
+
process.exit(1)
|
|
204
|
+
}
|
|
205
|
+
await agentsCommand(client, sessionId, parsed.json)
|
|
206
|
+
break
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
case 'agent': {
|
|
210
|
+
const sessionId = parsed.positional[0]
|
|
211
|
+
const agentId = parsed.positional[1]
|
|
212
|
+
if (!sessionId || !agentId) {
|
|
213
|
+
console.error('Usage: agent <sessionId> <agentId>')
|
|
214
|
+
process.exit(1)
|
|
215
|
+
}
|
|
216
|
+
await agentCommand(client, sessionId, agentId, parsed.json)
|
|
217
|
+
break
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
case 'mailbox': {
|
|
221
|
+
const sessionId = parsed.positional[0]
|
|
222
|
+
if (!sessionId) {
|
|
223
|
+
console.error('Usage: mailbox <sessionId>')
|
|
224
|
+
process.exit(1)
|
|
225
|
+
}
|
|
226
|
+
await mailboxCommand(client, sessionId, parsed.json)
|
|
227
|
+
break
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
case 'timeline': {
|
|
231
|
+
const sessionId = parsed.positional[0]
|
|
232
|
+
if (!sessionId) {
|
|
233
|
+
console.error('Usage: timeline <sessionId>')
|
|
234
|
+
process.exit(1)
|
|
235
|
+
}
|
|
236
|
+
await timelineCommand(client, sessionId, parsed.json)
|
|
237
|
+
break
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
case 'metrics': {
|
|
241
|
+
const sessionId = parsed.positional[0]
|
|
242
|
+
if (!sessionId) {
|
|
243
|
+
console.error('Usage: metrics <sessionId>')
|
|
244
|
+
process.exit(1)
|
|
245
|
+
}
|
|
246
|
+
await metricsCommand(client, sessionId, parsed.json)
|
|
247
|
+
break
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
case 'llm-calls': {
|
|
251
|
+
const sessionId = parsed.positional[0]
|
|
252
|
+
if (!sessionId) {
|
|
253
|
+
console.error('Usage: llm-calls <sessionId> [--limit N]')
|
|
254
|
+
process.exit(1)
|
|
255
|
+
}
|
|
256
|
+
await llmCallsCommand(client, sessionId, parsed.flags, parsed.json)
|
|
257
|
+
break
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
case 'llm-call': {
|
|
261
|
+
const sessionId = parsed.positional[0]
|
|
262
|
+
const callId = parsed.positional[1]
|
|
263
|
+
if (!sessionId || !callId) {
|
|
264
|
+
console.error('Usage: llm-call <sessionId> <callId>')
|
|
265
|
+
process.exit(1)
|
|
266
|
+
}
|
|
267
|
+
await llmCallCommand(client, sessionId, callId, parsed.json)
|
|
268
|
+
break
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
case 'preset-agents': {
|
|
272
|
+
const sessionId = parsed.positional[0]
|
|
273
|
+
if (!sessionId) {
|
|
274
|
+
console.error('Usage: preset-agents <sessionId>')
|
|
275
|
+
process.exit(1)
|
|
276
|
+
}
|
|
277
|
+
await presetAgentsCommand(client, SessionId(sessionId), parsed.json)
|
|
278
|
+
break
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
case 'debug-send': {
|
|
282
|
+
const sessionId = parsed.positional[0]
|
|
283
|
+
const agentId = parsed.positional[1]
|
|
284
|
+
const message = parsed.positional.slice(2).join(' ')
|
|
285
|
+
if (!sessionId || !agentId || !message) {
|
|
286
|
+
console.error('Usage: debug-send <sessionId> <agentId> <message> [--from <sender>]')
|
|
287
|
+
process.exit(1)
|
|
288
|
+
}
|
|
289
|
+
await debugSendCommand(client, sessionId, agentId, message, parsed.flags, parsed.json)
|
|
290
|
+
break
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
case 'spawn-agent': {
|
|
294
|
+
const sessionId = parsed.positional[0]
|
|
295
|
+
const definitionName = parsed.positional[1]
|
|
296
|
+
const parentId = parsed.positional[2]
|
|
297
|
+
if (!sessionId || !definitionName || !parentId) {
|
|
298
|
+
console.error('Usage: spawn-agent <sessionId> <definitionName> <parentId> [--message <msg>]')
|
|
299
|
+
process.exit(1)
|
|
300
|
+
}
|
|
301
|
+
await spawnAgentCommand(client, sessionId, definitionName, parentId, parsed.flags, parsed.json)
|
|
302
|
+
break
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
default:
|
|
306
|
+
console.error(`Unknown command: ${parsed.command}`)
|
|
307
|
+
console.error(USAGE)
|
|
308
|
+
process.exit(1)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
main().catch((error) => {
|
|
313
|
+
if (error instanceof RpcError) {
|
|
314
|
+
console.error(`RPC Error [${error.error.type}]: ${error.error.message}`)
|
|
315
|
+
} else {
|
|
316
|
+
console.error(error instanceof Error ? error.message : String(error))
|
|
317
|
+
}
|
|
318
|
+
process.exit(1)
|
|
319
|
+
})
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { AskUserInputTypeSchema } from '@roj-ai/sdk'
|
|
2
|
+
import type { ChatMessage } from '@roj-ai/shared/rpc'
|
|
3
|
+
|
|
4
|
+
// ANSI color codes
|
|
5
|
+
const RESET = '\x1b[0m'
|
|
6
|
+
const BOLD = '\x1b[1m'
|
|
7
|
+
const DIM = '\x1b[2m'
|
|
8
|
+
const CYAN = '\x1b[36m'
|
|
9
|
+
const GREEN = '\x1b[32m'
|
|
10
|
+
const YELLOW = '\x1b[33m'
|
|
11
|
+
const RED = '\x1b[31m'
|
|
12
|
+
const MAGENTA = '\x1b[35m'
|
|
13
|
+
|
|
14
|
+
export function formatAgentMessage(content: string): string {
|
|
15
|
+
return `${CYAN}${BOLD}Agent:${RESET} ${content}`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function formatUserMessage(content: string): string {
|
|
19
|
+
return `${GREEN}${BOLD}You:${RESET} ${content}`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function formatStatus(status: string): string {
|
|
23
|
+
return `${DIM}[${status}]${RESET}`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function formatError(message: string): string {
|
|
27
|
+
return `${RED}Error: ${message}${RESET}`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function formatQuestion(question: string, inputType: AskUserInputTypeSchema): string {
|
|
31
|
+
const lines = [`${YELLOW}${BOLD}Question:${RESET} ${question}`]
|
|
32
|
+
|
|
33
|
+
if (inputType.type === 'single_choice' || inputType.type === 'multi_choice') {
|
|
34
|
+
for (let i = 0; i < inputType.options.length; i++) {
|
|
35
|
+
const opt = inputType.options[i]
|
|
36
|
+
const desc = opt.description ? ` - ${opt.description}` : ''
|
|
37
|
+
lines.push(` ${MAGENTA}${i + 1})${RESET} ${opt.label}${DIM}${desc}${RESET}`)
|
|
38
|
+
}
|
|
39
|
+
if (inputType.type === 'single_choice') {
|
|
40
|
+
lines.push(`${DIM}Enter number to select:${RESET}`)
|
|
41
|
+
} else {
|
|
42
|
+
lines.push(`${DIM}Enter numbers separated by commas:${RESET}`)
|
|
43
|
+
}
|
|
44
|
+
} else if (inputType.type === 'confirm') {
|
|
45
|
+
const confirmLabel = inputType.confirmLabel ?? 'yes'
|
|
46
|
+
const cancelLabel = inputType.cancelLabel ?? 'no'
|
|
47
|
+
lines.push(`${DIM}(${confirmLabel}/${cancelLabel}):${RESET}`)
|
|
48
|
+
} else if (inputType.type === 'rating') {
|
|
49
|
+
lines.push(`${DIM}Enter a number from ${inputType.min} to ${inputType.max}:${RESET}`)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return lines.join('\n')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function formatChatMessage(msg: ChatMessage): string {
|
|
56
|
+
switch (msg.type) {
|
|
57
|
+
case 'user_message':
|
|
58
|
+
return formatUserMessage(msg.content)
|
|
59
|
+
case 'agent_message':
|
|
60
|
+
return formatAgentMessage(msg.content)
|
|
61
|
+
case 'ask_user': {
|
|
62
|
+
const answered = msg.answered ? `${DIM} (answered: ${JSON.stringify(msg.answer)})${RESET}` : ''
|
|
63
|
+
return formatQuestion(msg.question, msg.inputType) + answered
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function formatSessionInfo(session: { sessionId: string; presetId: string; status: string; createdAt: number }): string {
|
|
69
|
+
const date = new Date(session.createdAt).toLocaleString()
|
|
70
|
+
return `${BOLD}${session.sessionId}${RESET} ${DIM}[${session.presetId}]${RESET} ${session.status} ${DIM}${date}${RESET}`
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function formatTable(headers: string[], rows: string[][]): string {
|
|
74
|
+
const colWidths = headers.map((h, i) => {
|
|
75
|
+
const maxDataWidth = rows.reduce((max, row) => Math.max(max, (row[i] ?? '').length), 0)
|
|
76
|
+
return Math.max(h.length, maxDataWidth)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
const headerLine = headers.map((h, i) => h.padEnd(colWidths[i])).join(' ')
|
|
80
|
+
const separator = colWidths.map(w => '─'.repeat(w)).join('──')
|
|
81
|
+
const dataLines = rows.map(row => row.map((cell, i) => (cell ?? '').padEnd(colWidths[i])).join(' '))
|
|
82
|
+
|
|
83
|
+
return [headerLine, separator, ...dataLines].join('\n')
|
|
84
|
+
}
|
package/src/repl/repl.ts
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import type { ChatMessage, SessionId } from '@roj-ai/sdk'
|
|
2
|
+
import type { AskUserInputTypeSchema } from '@roj-ai/sdk'
|
|
3
|
+
import { AgentId, ChatMessageId } from '@roj-ai/shared'
|
|
4
|
+
|
|
5
|
+
function isChatMessage(msg: unknown): msg is ChatMessage {
|
|
6
|
+
return typeof msg === 'object' && msg !== null && 'type' in msg && 'timestamp' in msg
|
|
7
|
+
}
|
|
8
|
+
import type { RpcClient } from '@roj-ai/shared/rpc'
|
|
9
|
+
import * as readline from 'node:readline'
|
|
10
|
+
import { connectWebSocket, type NotificationHandlers, type WsConnection } from '../client.js'
|
|
11
|
+
import { unwrap } from '../unwrap.js'
|
|
12
|
+
import { formatAgentMessage, formatChatMessage, formatError, formatQuestion, formatStatus } from './formatter.js'
|
|
13
|
+
import { createNewSession, pickSession } from './session-picker.js'
|
|
14
|
+
|
|
15
|
+
const DIM = '\x1b[2m'
|
|
16
|
+
const RESET = '\x1b[0m'
|
|
17
|
+
|
|
18
|
+
export async function startRepl(client: RpcClient, baseUrl: string): Promise<void> {
|
|
19
|
+
const rl = readline.createInterface({
|
|
20
|
+
input: process.stdin,
|
|
21
|
+
output: process.stdout,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
console.log(`${DIM}Connected to ${baseUrl}${RESET}`)
|
|
25
|
+
console.log(`${DIM}Commands: /quit, /sessions, /new, /messages, /help${RESET}`)
|
|
26
|
+
|
|
27
|
+
let sessionId = await pickSession(client, rl)
|
|
28
|
+
let entryAgentId = await resolveEntryAgentId(client, sessionId)
|
|
29
|
+
await loadAndDisplayMessages(client, sessionId)
|
|
30
|
+
|
|
31
|
+
let pendingQuestion: { questionId: string; inputType: AskUserInputTypeSchema } | null = null
|
|
32
|
+
|
|
33
|
+
const handlers: NotificationHandlers = {
|
|
34
|
+
agentMessage: (payload) => {
|
|
35
|
+
if (typeof payload !== 'object' || payload === null) return
|
|
36
|
+
const input = payload as Record<string, unknown>
|
|
37
|
+
if (typeof input.content !== 'string') return
|
|
38
|
+
readline.clearLine(process.stdout, 0)
|
|
39
|
+
readline.cursorTo(process.stdout, 0)
|
|
40
|
+
console.log(formatAgentMessage(input.content))
|
|
41
|
+
rl.prompt()
|
|
42
|
+
},
|
|
43
|
+
askUser: (payload) => {
|
|
44
|
+
if (typeof payload !== 'object' || payload === null) return
|
|
45
|
+
const input = payload as Record<string, unknown>
|
|
46
|
+
if (typeof input.questionId !== 'string' || typeof input.question !== 'string') return
|
|
47
|
+
readline.clearLine(process.stdout, 0)
|
|
48
|
+
readline.cursorTo(process.stdout, 0)
|
|
49
|
+
console.log(formatQuestion(input.question, input.inputType as AskUserInputTypeSchema))
|
|
50
|
+
pendingQuestion = { questionId: input.questionId, inputType: input.inputType as AskUserInputTypeSchema }
|
|
51
|
+
rl.prompt()
|
|
52
|
+
},
|
|
53
|
+
agentStatus: (payload) => {
|
|
54
|
+
if (typeof payload !== 'object' || payload === null) return
|
|
55
|
+
const input = payload as Record<string, unknown>
|
|
56
|
+
if (typeof input.status !== 'string') return
|
|
57
|
+
const statusLabels: Record<string, string> = {
|
|
58
|
+
idle: 'Agent idle',
|
|
59
|
+
thinking: 'Agent thinking...',
|
|
60
|
+
responding: 'Agent responding...',
|
|
61
|
+
waiting_for_user: 'Waiting for your input',
|
|
62
|
+
error: 'Agent error',
|
|
63
|
+
}
|
|
64
|
+
readline.clearLine(process.stdout, 0)
|
|
65
|
+
readline.cursorTo(process.stdout, 0)
|
|
66
|
+
console.log(formatStatus(statusLabels[input.status] ?? input.status))
|
|
67
|
+
rl.prompt()
|
|
68
|
+
},
|
|
69
|
+
error: (payload) => {
|
|
70
|
+
if (typeof payload !== 'object' || payload === null) return
|
|
71
|
+
const input = payload as Record<string, unknown>
|
|
72
|
+
if (typeof input.message !== 'string') return
|
|
73
|
+
readline.clearLine(process.stdout, 0)
|
|
74
|
+
readline.cursorTo(process.stdout, 0)
|
|
75
|
+
console.log(formatError(input.message))
|
|
76
|
+
rl.prompt()
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let connection = connectWebSocket(baseUrl, sessionId, handlers)
|
|
81
|
+
await connection.connect()
|
|
82
|
+
|
|
83
|
+
rl.setPrompt('> ')
|
|
84
|
+
rl.prompt()
|
|
85
|
+
|
|
86
|
+
rl.on('line', async (line) => {
|
|
87
|
+
const input = line.trim()
|
|
88
|
+
if (!input) {
|
|
89
|
+
rl.prompt()
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (input.startsWith('/')) {
|
|
94
|
+
const handled = await handleSlashCommand(input, client, baseUrl, rl, sessionId, connection)
|
|
95
|
+
if (handled === 'quit') {
|
|
96
|
+
await connection.disconnect()
|
|
97
|
+
rl.close()
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
if (handled !== false) {
|
|
101
|
+
// Session switch
|
|
102
|
+
await connection.disconnect()
|
|
103
|
+
sessionId = handled
|
|
104
|
+
entryAgentId = await resolveEntryAgentId(client, sessionId)
|
|
105
|
+
connection = connectWebSocket(baseUrl, sessionId, handlers)
|
|
106
|
+
await connection.connect()
|
|
107
|
+
await loadAndDisplayMessages(client, sessionId)
|
|
108
|
+
}
|
|
109
|
+
rl.prompt()
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Handle pending question answer
|
|
114
|
+
if (pendingQuestion && entryAgentId) {
|
|
115
|
+
const answer = parseAnswer(input, pendingQuestion.inputType)
|
|
116
|
+
unwrap(
|
|
117
|
+
await client.call('user-chat.answerQuestion', {
|
|
118
|
+
sessionId,
|
|
119
|
+
agentId: AgentId(entryAgentId),
|
|
120
|
+
questionId: ChatMessageId(pendingQuestion.questionId),
|
|
121
|
+
answer,
|
|
122
|
+
}),
|
|
123
|
+
)
|
|
124
|
+
pendingQuestion = null
|
|
125
|
+
rl.prompt()
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Send message
|
|
130
|
+
if (entryAgentId) {
|
|
131
|
+
unwrap(await client.call('user-chat.sendMessage', { sessionId, agentId: AgentId(entryAgentId), content: input }))
|
|
132
|
+
}
|
|
133
|
+
rl.prompt()
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
rl.on('close', async () => {
|
|
137
|
+
await connection.disconnect()
|
|
138
|
+
process.exit(0)
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function resolveEntryAgentId(client: RpcClient, sessionId: SessionId): Promise<string | null> {
|
|
143
|
+
const session = unwrap(await client.call('sessions.get', { sessionId }))
|
|
144
|
+
return session.entryAgentId
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function loadAndDisplayMessages(client: RpcClient, sessionId: SessionId): Promise<void> {
|
|
148
|
+
const { messages: rawMessages } = unwrap(await client.call('user-chat.getMessages', { sessionId }))
|
|
149
|
+
const messages = rawMessages.filter(isChatMessage)
|
|
150
|
+
if (messages.length > 0) {
|
|
151
|
+
console.log()
|
|
152
|
+
console.log(`${DIM}--- Message history ---${RESET}`)
|
|
153
|
+
for (const msg of messages) {
|
|
154
|
+
console.log(formatChatMessage(msg))
|
|
155
|
+
console.log()
|
|
156
|
+
}
|
|
157
|
+
console.log(`${DIM}--- End of history ---${RESET}`)
|
|
158
|
+
console.log()
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
type SlashResult = 'quit' | SessionId | false
|
|
163
|
+
|
|
164
|
+
async function handleSlashCommand(
|
|
165
|
+
input: string,
|
|
166
|
+
client: RpcClient,
|
|
167
|
+
_baseUrl: string,
|
|
168
|
+
rl: readline.Interface,
|
|
169
|
+
currentSessionId: SessionId,
|
|
170
|
+
_connection: WsConnection,
|
|
171
|
+
): Promise<SlashResult> {
|
|
172
|
+
const [cmd] = input.split(/\s+/)
|
|
173
|
+
|
|
174
|
+
switch (cmd) {
|
|
175
|
+
case '/quit':
|
|
176
|
+
case '/exit':
|
|
177
|
+
case '/q':
|
|
178
|
+
return 'quit'
|
|
179
|
+
|
|
180
|
+
case '/sessions': {
|
|
181
|
+
const newSessionId = await pickSession(client, rl)
|
|
182
|
+
if (newSessionId !== currentSessionId) {
|
|
183
|
+
return newSessionId
|
|
184
|
+
}
|
|
185
|
+
return false
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
case '/new': {
|
|
189
|
+
const newSessionId = await createNewSession(client, rl)
|
|
190
|
+
return newSessionId
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
case '/messages': {
|
|
194
|
+
await loadAndDisplayMessages(client, currentSessionId)
|
|
195
|
+
return false
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
case '/help':
|
|
199
|
+
console.log(`${DIM}Commands:${RESET}`)
|
|
200
|
+
console.log(` /quit, /exit, /q - Exit the REPL`)
|
|
201
|
+
console.log(` /sessions - Switch to another session`)
|
|
202
|
+
console.log(` /new - Create a new session`)
|
|
203
|
+
console.log(` /messages - Show message history`)
|
|
204
|
+
console.log(` /help - Show this help`)
|
|
205
|
+
return false
|
|
206
|
+
|
|
207
|
+
default:
|
|
208
|
+
console.log(`Unknown command: ${cmd}. Type /help for available commands.`)
|
|
209
|
+
return false
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function parseAnswer(input: string, inputType: AskUserInputTypeSchema): unknown {
|
|
214
|
+
switch (inputType.type) {
|
|
215
|
+
case 'text':
|
|
216
|
+
return input
|
|
217
|
+
|
|
218
|
+
case 'single_choice': {
|
|
219
|
+
const index = parseInt(input, 10) - 1
|
|
220
|
+
if (index >= 0 && index < inputType.options.length) {
|
|
221
|
+
return inputType.options[index].value
|
|
222
|
+
}
|
|
223
|
+
return input
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
case 'multi_choice': {
|
|
227
|
+
const indices = input.split(',').map(s => parseInt(s.trim(), 10) - 1)
|
|
228
|
+
return indices
|
|
229
|
+
.filter(i => i >= 0 && i < inputType.options.length)
|
|
230
|
+
.map(i => inputType.options[i].value)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
case 'confirm': {
|
|
234
|
+
const lower = input.toLowerCase()
|
|
235
|
+
return lower === 'y' || lower === 'yes' || lower === '1' || lower === 'true'
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
case 'rating': {
|
|
239
|
+
const num = parseInt(input, 10)
|
|
240
|
+
if (num >= inputType.min && num <= inputType.max) {
|
|
241
|
+
return num
|
|
242
|
+
}
|
|
243
|
+
return parseInt(input, 10)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|